在上一篇教程中,我们学习了 SPL 代币的工作原理。在本教程中,我们将实现完整的 SPL 代币生命周期:创建、铸造、转账以及查询代币,并使用以下两种方法:
- 使用 Anchor 进行链上操作:我们将使用 Anchor 创建一个 Solana 程序,该程序会铸造 SPL 代币,直到达到预设的供应量上限。
- 使用 TypeScript 进行客户端操作:我们还将展示如何直接从 TypeScript 客户端与 Token Program 交互,以创建 SPL mint、ATA、铸造代币、转账并读取余额。
为什么需要两种方法?
了解这两种操作方式至关重要,因为:
- 使用 Anchor,我们可以在 SPL 代币之上构建自定义的链上逻辑(例如:归属计划、条件铸造),或者创建一个由我们的程序而非钱包控制的 SPL 代币。
- 使用 TypeScript,我们可以直接与 SPL 程序交互,执行诸如转账 SPL 代币或授权/撤销委托等简单操作。
现在让我们从 Anchor 方法开始。
在 Anchor 中创建 SPL 代币
回顾上一篇 SPL 代币教程,每个代币都使用相同的链上程序(即位于地址 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA 的 SPL Token Program)来创建 mint 账户并执行代币铸造、转账、授权等操作。
在本节中,我们将构建一个 Anchor 程序,通过向 Token Program 发起跨程序调用 (CPI) 来创建和铸造 SPL 代币。
我们的程序仅包含两个函数:
- 一个
create_and_mint_token函数,用于创建 mint 账户,并通过向 Token Program 发起 CPI,将初始供应量铸造到指定的关联代币账户 (ATA) 中。 - 一个
transfer_tokens函数,用于通过向 Token Program 发起 CPI,将代币从源 ATA 转移到目标 ATA。
现在,使用 anchor init spl_token 创建一个新的 Anchor 项目。打开该项目,并将 programs/spl_token/src/lib.rs 中的代码替换为以下内容:
在这段代码中,我们执行了以下操作:
- 引入相关依赖项:
anchor_spl::associated_token::AssociatedToken,用于创建关联代币账户 (ATA)。anchor_spl::token::{Mint, MintTo, Token, TokenAccount, Transfer},用于与 SPL Token Program 交互(这些是我们进行铸造和转账所需的指令与账户类型)。
- 定义一个
create_and_mint_token函数,该函数:- 使用提供的 mint 账户和目标 ATA(铸造的代币将存入该账户)。
- 构建一个指向 Token Program 的 CPI 上下文。
- 调用 Token Program 的
mint_to指令,向 ATA 铸造 100 个代币(精度为 9)。 - 代币铸造完成后返回成功状态。
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken; // Needed for ATA creation
use anchor_spl::token::{self, Mint, MintTo, Token, TokenAccount, Transfer}; // Needed for mint account creation/handling
declare_id!("6zndm8QQsPxbjTRC8yh5mxqfjmUchTaJyu2yKbP7ZT2x");
#[program]
pub mod spl_token {
use super::*;
// This function deploys a new SPL token with decimal of 9 and mints 100 units of the token
pub fn create_and_mint_token(ctx: Context<CreateMint>) -> Result<()> {
let mint_amount = 100_000_000_000; // 100 tokens with 9 decimals
let mint = ctx.accounts.new_mint.clone();
let destination_ata = &ctx.accounts.new_ata;
let authority = ctx.accounts.signer.clone();
let token_program = ctx.accounts.token_program.clone();
let mint_to_instruction = MintTo {
mint: mint.to_account_info(),
to: destination_ata.to_account_info(),
authority: authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(token_program.to_account_info(), mint_to_instruction);
token::mint_to(cpi_ctx, mint_amount)?;
Ok(())
}
}
添加 CreateMint 账户结构体。它包含以下账户:
signer:支付交易费用并同时充当铸造权限 (mint authority) 的账户。new_mint:一个初始精度为 9 并且使用 signer 作为铸造和冻结权限的 mint PDA 账户。new_ata:将为新的 mint 创建的关联代币账户,使用 signer 作为其权限拥有者(实际上就是保存 signer 余额的账户)。- 最后,我们传入 Token Program、Associated Token Program 和 System Program。这些是我们通过 CPI 交互的原生程序。
#[derive(Accounts)]
pub struct CreateMint<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
init,
payer = signer,
mint::decimals = 9,
mint::authority = signer,
// Commenting out or removing this line permanently disables the freeze authority.
mint::freeze_authority = signer,
// When a token is created without a freeze authority, Solana prevents any future updates to it.
// This makes the token more decentralized, as no authority can freeze a user's ATA.
seeds = [b"my_mint", signer.key().as_ref()],
bump
)]
pub new_mint: Account<'info, Mint>,
#[account(
init,
payer = signer,
associated_token::mint = new_mint,
associated_token::authority = signer,
)]
pub new_ata: Account<'info, TokenAccount>,
// This represents the SPL Token Program (TokenkegQfeZ…)
// The same program we introduced in the previous article that owns and manages all mint and associated token account.
pub token_program: Program<'info, Token>,
// This represents the ATA program (ATokenGPvbdGV...)
// As mentioned in the previous tutorial, it is only in charge of creating the ATA.
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
现在运行 anchor keys sync 来同步你的 Program ID。
接下来,更新 programs/spl_token/Cargo.toml 文件,将 anchor-spl crate 作为依赖项添加到我们的项目中:
[package]
name = "spl_token"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "spl_token"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] # added "anchor-spl/idl-build"
[dependencies]
anchor-lang = "0.31.0"
anchor-spl = "0.31.0" # added this
这个 anchor-spl 让我们可以访问 SPL Token Program、ATA 程序及其指令。
现在,让我们仔细看看程序代码中发生了什么。
我们从 CreateMint 结构体开始。

signer
首先,我们声明支付代币部署交易费用的 signer,如下方紫色高亮所示。

mint
接下来,我们声明一个 new_mint 账户,它代表我们要创建的 SPL 代币(如下方 红色 高亮所示)。它的账户类型是 Mint(如下方黄色高亮所示)。此账户类型代表 Solana 上的 mint 账户。

正如在上图中所看到的,我们将这个新的 mint 账户初始化为程序派生地址 (PDA) 并设置其参数:代币精度 (decimals)、铸造和冻结权限以及 PDA 种子 (seeds)。我们没有使用密钥对 (keypair) 账户,而是通过固定种子和程序 ID 派生出 mint 作为一个 PDA,因此不需要像密钥对账户那样生成或管理私钥。我们使用 mint PDA 主要是为了方便。
如果你是刚接触 PDA 的工作原理或者不知道它与密钥对账户的区别,请查看我们的文章 “PDA (Program Derived Address) vs Keypair Account in Solana.”
最后,init 约束告诉 Anchor 在运行 create_and_mint_token 时自动创建并初始化 mint 账户(我们接下来会解释该函数)。
由于有了这个 init 约束,Anchor 会在后台对 Token Program 的 InitializeMint 指令发起 CPI(跨程序调用)。该指令将 mint 的精度设置为 9,并将铸造和冻结权限全部分配给 signer。
Associated Token Account
接下来是我们将要把该代币铸造到的关联代币账户 (ATA)(如下方黄色高亮所示)。
注意:mint 账户的存在并不需要依赖 ATA。我们在这里创建一个 ATA,仅仅是因为我们想给 signer 铸造一些代币。

该 ATA 的类型为 TokenAccount,代表 Solana 上的 ATA。和 mint 账户一样,我们设置了它的参数:将 ATA 的 mint 设置为我们正在创建的新代币,并让 signer 成为其权限拥有者。这意味着只有 signer 才能授权修改 ATA 状态的指令。Anchor 会在内部向 Token Program 的 InitializeAccount 指令发起 CPI,以应用这些设置。
注意:我们在此能够安全地使用 init,仅仅是因为 mint 账户 (new_mint) 也在同一个指令中被创建。如果 mint 已经存在,并且有人已经创建了该 ATA,那么在 ATA 上使用 init 可能会失败,从而导致拒绝服务 (denial-of-service)。在 mint 可能已经存在的情况下,使用 init_if_needed 会更加安全。否则,其他人可能会抢跑 (frontrun) 指令,代表 signer 创建一个 ATA,从而导致这笔交易失败。
native programs accounts
最后,我们声明创建 mint 和关联代币账户所需的原生 Solana 程序(如下方绿色高亮所示)。这些都是我们的 Anchor 程序与之交互的链上程序:用于创建 mint 并铸造代币的 Token Program、用于创建用户 ATA 的 Associated Token Account Program,以及用于为账户分配空间和管理租金的 System Program。

你可能已经注意到,ATA(new_ata 账户)并不像 mint 账户(new_mint)那样拥有 seed 和 bump,这是因为 InitializeAccount 指令使用了标准的关联代币账户派生流程,即 user_wallet_address + token_mint_address => associated_token_account_address。所以我们不需要传入 seed 和 bump。如果你尝试传入 seed 和 bump,Anchor 将抛出如下错误。

我们同样没有为 mint 账户和 ATA 指定 space,因为 Anchor 也会在后台为我们添加所需的空间。由于我们指定了程序是 AssociatedToken,Anchor 能够获知这些信息。如果我们试图为它们中任何一个指定 space,就会发生错误。

mint 和关联代币账户的实际大小分别为 82 字节和 165 字节。
既然我们已经声明了所有需要的账户,接下来让我们看看用于铸造 SPL 代币的 create_and_mint_token 函数。
Minting the SPL token
我们使用这个函数将 100 个(精度为 9)刚刚创建的代币,铸造到 signer 新创建的 ATA 中。

在上面的代码中,我们构建了一个 MintTo 指令。这三个字段定义了 MintTo 的行为:
mint:我们正在铸造哪种代币,由 mint 账户指定。to:将接收铸造出代币的 ATA。authority:被允许为该 mint 账户进行代币铸造的账户。在我们的程序中,我们将铸造权限设置为了交易的签名者 (signer),因此 signer 必须进行签名且与 mint 的权限相匹配,铸造才能成功。
随后,我们使用该指令向 Token Program 发起 CPI(如绿色高亮部分所示),这将把 100 个单位的代币铸造到关联代币账户中。
另外,正如在上一篇教程中所讨论的,mint 账户和 ATA 都必须在调用 MintTo 指令之前存在(这同样适用于 Transfer)。这就是我们使用 #[account(init…)] 约束的原因;它能确保这些账户在指令运行之前被创建。

注意:在 Solana 上创建 NFT,你需要在初始化 mint 时将 mint::decimals = 0,向接收者精确铸造 1 个代币,然后通过将铸造权限设置为 None 来撤销它。这能确保以后再也无法铸造更多的代币,并且因为其精度为零不可分割,使得该代币变得独一无二且不可替代。
Test the createAndMintToken function
现在,我们将测试 createAndMintToken 函数。
将 tests/spl_token.ts 中的测试代码替换为以下代码。测试的结构如下:
- 我们使用
@coral-xyz/anchor库中的findProgramAddressSync在链下派生代币的 mint 账户地址,所用的 seeds 与我们的 Anchor 程序中创建 mint 时所用的完全相同。这一步并不会部署 mint 账户,正如前面所解释的,我们已经在 Anchor 程序内部处理了该操作。 - 接下来我们使用
getAssociatedTokenAddressSync函数计算 signer 的 ATA 地址。同样,这也不会部署该账户。 - 我们传入相应的账户(signer、mint、ATA、Token Program、ATA Program 以及 System Program)调用 Anchor 程序函数,并打印交易哈希、代币地址以及 signer 的 ATA 地址。
- 最后,我们使用
@solana/spl-token库中的getMint和getAccount函数获取 mint 和 ATA 的信息,并断言它们的内容与我们之前在 Anchor 程序中设置的一致。我们断言了代币的精度、权限、代币总供应量、ATA 中的代币余额等。
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import * as splToken from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import { assert } from 'chai';
import { SplToken } from "../target/types/spl_token";
describe("spl_token", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.splToken as Program<SplToken>;
const provider = anchor.AnchorProvider.env();
const signerKp = provider.wallet.payer;
const toKp = new web3.Keypair();
it("Creates a new mint and associated token account using CPI", async () => {
// Derive the mint address using the same seeds ("my_mint" + signer public key) we used when the mint was created in our Anchor program
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from("my_mint"), signerKp.publicKey.toBuffer()],
program.programId
);
// Get the associated token account address
// The boolean value here indicates whether the authority of the ATA is an "off-curve" address (i.e., a PDA).
// A value of false means the owner is a normal wallet address.
// `signerKp` is the owner here and it is a normal wallet address, so we use false.
const ata = splToken.getAssociatedTokenAddressSync(mint, signerKp.publicKey, false)
// Call the create_mint instruction
const tx = await program.methods
.createAndMintToken()
.accounts({
signer: signerKp.publicKey,
newMint: mint,
newAta: ata,
tokenProgram: splToken.TOKEN_PROGRAM_ID,
associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Transaction signature:", tx);
console.log("Token (Mint Account) Address:", mint.toString());
console.log("Associated Token Account:", ata.toString());
/// Verify the token details
const mintInfo = await splToken.getMint(provider.connection, mint);
assert.equal(mintInfo.decimals, 9, "Mint decimals should be 9");
assert.equal(mintInfo.mintAuthority?.toString(), signerKp.publicKey.toString(), "Mint authority should be the signer");
assert.equal(mintInfo.freezeAuthority?.toString(), signerKp.publicKey.toString(), "Freeze authority should be the signer");
assert.equal(mintInfo.supply.toString(), "100000000000", "Supply should be 100 tokens (with 9 decimals)");
// Verify the ATA details
const tokenAccount = await splToken.getAccount(provider.connection, ata);
assert.equal(tokenAccount.mint.toString(), mint.toString(), "Token account mint should match the mint PDA");
assert.equal(tokenAccount.owner.toString(), signerKp.publicKey.toString(), "Token account owner should be the signer");
assert.equal(tokenAccount.amount.toString(), "100000000000", "Token balance should be 100 tokens (with 9 decimals)");
assert.equal(tokenAccount.delegate, null, "Token account should not have a delegate");
});
});
运行 npm install @solana/spl-token 来安装 SPL token 库。
现在运行 anchor test,可以看到代币和 ATA 都已成功部署。

Transferring SPL Tokens
为了转账代币,我们需要构建一个 Transfer 指令,并向 Token Program 发起 CPI。该转账的工作机制是将指定单位数量的代币从源关联代币账户移动到目标关联代币账户。这笔交易的签名者必须是源 ATA 的权限拥有者。
现在将以下函数添加到你的程序中。它执行以下操作:
- 加载源关联代币账户 (
from_ata),代币将从此处扣除。 - 加载目标关联代币账户 (
to_ata),代币将发送至此处(此 ATA 将在我们的测试代码中创建)。 - 加载必须对转账进行签名并批准的权限账户 (
from)。 - 加载将处理该转账的 Token Program 账户。
- 使用源账户、目标账户和权限账户构建
Transfer指令。 - 创建一个包装了 Token Program 和转账指令的 CPI(跨程序调用)上下文。
- 使用该 CPI 上下文及金额调用
token::transfer函数,从而将代币从源 ATA 转移到目标 ATA。
pub fn transfer_tokens(ctx: Context<TransferSpl>, amount: u64) -> Result<()> {
let source_ata = &ctx.accounts.from_ata;
let destination_ata = &ctx.accounts.to_ata;
let authority = &ctx.accounts.from;
let token_program = &ctx.accounts.token_program;
// Transfer tokens from from_ata to to_ata
let cpi_accounts = Transfer { // Transfer instruction
from: source_ata.to_account_info().clone(),
to: destination_ata.to_account_info().clone(),
authority: authority.to_account_info().clone(),
};
let cpi_ctx = CpiContext::new(token_program.to_account_info(), cpi_accounts); // Create a CPI context
token::transfer(cpi_ctx, amount)?;
Ok(())
}
在 ERC-20 中,transfer 假设 msg.sender 就是代币所有者,而 transferFrom 允许第三方(委托人)在获得授权的情况下代表他人移动代币。SPL Token Program 将这两者合并为了一个 transfer 指令,但要求显式地将转账权限作为一个账户(在我们的 Anchor 代码中即为 AccountInfo)传入——这映射到了 Transfer.authority 字段。该权限是被允许移动代币的签名者;它可以是代币所有者,也可以是已获批准的委托人。
因此,在 transfer 指令中:
from:代币发送者的 ATAto:代币接收者的 ATAauthority:拥有从from中移动代币权限的签名者(可以是所有者,也可以是已获批准的委托人)
现在添加下方的 TransferSpl 账户结构体,它定义了执行代币转账所需的账户。
#[derive(Accounts)]
pub struct TransferSpl<'info> {
pub from: Signer<'info>,
#[account(mut)]
pub from_ata: Account<'info, TokenAccount>,
#[account(mut)]
pub to_ata: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>, // We are interacting with the Token Program
}
我们传入交易签名者、源和目标 ATA,最后传入我们将与之交互的 Token Program。
将此测试添加到我们的测试文件中。
我们在测试中执行以下操作:
- 首先,我们使用
findProgramAddressSync函数派生想要转账的代币(mint 账户)地址。 - 接着,我们使用
@solana/spl-token中的getAssociatedTokenAddressSync来计算源(发送者钱包)和目标(接收者钱包)的 ATA 地址,该函数接收 mint 地址、各自的账户地址,以及一个用于指示 ATA 签名者 (signerKp) 是否为 PDA 的布尔值。在本例中,它不是 PDA。 - 我们使用
createAssociatedTokenAccount函数为目标账户创建 ATA。我们没有为 signer 创建 ATA,因为在之前的测试用例中已经完成了该操作。由于所有测试用例是一起运行的,该账户状态得以保留。 - 最后,我们使用程序中的
transfer_tokens函数向目标 ATA 转账 10 个代币。然后,我们通过getTokenAccountBalance函数获取目标 ATA 的代币余额,并断言其值为 10(即我们发送的数量)。
it("Transfers tokens using CPI", async () => {
// Derive the PDA for the mint
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from("my_mint"), signerKp.publicKey.toBuffer()],
program.programId
);
// Get the ATAs
const fromAta = splToken.getAssociatedTokenAddressSync(mint, signerKp.publicKey, false);
const toAta = splToken.getAssociatedTokenAddressSync(mint, toKp.publicKey, false);
// Create to_ata as it doesn't exist yet
try {
await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mint,
toKp.publicKey
);
} catch (error) {
throw new Error(error)
}
const transferAmount = new anchor.BN(10_000_000_000); // 10 tokens with 9 decimals
// Transfer tokens
const tx = await program.methods
.transferTokens(transferAmount)
.accounts({
from: signerKp.publicKey,
fromAta: fromAta,
toAta: toAta,
tokenProgram: splToken.TOKEN_PROGRAM_ID,
})
.rpc();
console.log("Transfer Transaction signature:", tx);
// Verify the transfer
const toBalance = await provider.connection.getTokenAccountBalance(toAta);
assert.equal(
toBalance.value.amount,
transferAmount.toString(),
"Recipient balance should match transfer amount"
);
});
现在运行测试

Retrieving token balances
将此函数添加到程序中,以获取 ATA 的代币余额
pub fn get_balance(ctx: Context<GetBalance>) -> Result<()> {
// Get the token account address, its owner & balance
let ata_pubkey = ctx.accounts.token_account.key();
let owner = ctx.accounts.token_account.owner; // the `owner` is a field in the ATA
let balance = ctx.accounts.token_account.amount; // the `amount` is a field in the ATA
// Print the balance information
msg!("Token Account Address: {}", ata_pubkey);
msg!("Token Account Owner: {}", owner);
msg!("Token Account Balance: {}", balance);
Ok(())
}
ATA 中的 amount 字段存储着代币余额。在这个函数中,我们直接从 ctx.accounts.token_account 中访问它以打印余额。
添加对应的上下文结构体:
#[derive(Accounts)]
pub struct GetBalance<'info> {
#[account(mut)]
pub token_account: Account<'info, TokenAccount>,
}
更新测试代码
it("Reads token balance using CPI", async () => {
// Derive the PDA for the mint
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from("my_mint"), signerKp.publicKey.toBuffer()],
program.programId
);
// Get the associated token account address
const ata = splToken.getAssociatedTokenAddressSync(mint, signerKp.publicKey, false);
// Call the get_balance instruction
const tx = await program.methods
.getBalance()
.accounts({
tokenAccount: ata,
})
.rpc();
console.log("Get Balance Transaction signature:", tx);
// Verify balance through direct query
const balance = await provider.connection.getTokenAccountBalance(ata);
assert.isTrue(balance.value.uiAmount > 0, "Token balance should be greater than 0");
});
如果我们运行验证器并检查日志,应该可以看到 signer 的 ATA 余额减少了 10 个代币(从 100 变为了 90)。这正是我们在上一个测试用例中转账的数量。

Create and Transfer Tokens with the Typescript client directly
就算没有 Solana 程序,你也可以仅仅通过使用 web3.js Typescript 客户端来创建 SPL 代币并与之交互。
这在你不需要具有自定义逻辑的链上程序时非常有用。如果你只是在铸造代币、转账或读取余额,从客户端进行操作会更快、更便宜。你无需编写或部署程序。
让我们直接通过 TypeScript 创建新的代币和 ATA 并对它们进行转账。
Creating a Mint and ATA in TypeScript
创建一个新的 Anchor 项目 spl_token_ts,并将测试代码替换为本节后面展示的 TypeScript 代码块。
这个 TypeScript 测试套件展示了如何使用 @solana/spl-token 库直接与 SPL Token program 进行交互。
它执行以下操作:
- 首先,它调用
splToken.createMint。该函数向 Token Program 发送一条InitializeMint指令来创建一个新的 SPL 代币 mint 账户。我们提供了网络连接 (connection)、付款人(signerKp,我们的默认本地 signer)、铸造权限和冻结权限,以及所需的小数位数(本例中为 6)。它将返回新创建 mint 的公钥。 - 接着,它使用
splToken.createAssociatedTokenAccount为signerKp针对新创建的 mint 创建 ATA。这是@solana/spl-tokenTypeScript SDK 提供的一个辅助函数。在底层,它会派生出 ATA 地址并向 Associated Token Account Program 发送创建指令。 - 然后,调用
splToken.mintTo来发行新的代币单位。它需要网络连接 (connection)、交易付款人(我们使用signerKp)、mint 的公钥、目标 ATA 地址、铸造权限的公钥 (signerKp.PublicKey),以及要铸造的代币数量(需要考虑精度换算)。 - 最后,它验证了相关设置。
splToken.getMint用于获取 mint 账户的链上数据,随后我们断言其精度和权限与我们的指定相匹配。splToken.getAccount用于获取 ATA 数据,并断言其代币余额与我们刚刚铸造的数量一致。
import * as anchor from "@coral-xyz/anchor";
import * as splToken from "@solana/spl-token";
import * as web3 from "@solana/web3.js";
import { assert } from 'chai';
describe("TypeScript SPL Token Tests", () => {
const provider = anchor.AnchorProvider.env();
const signerKp = provider.wallet.payer;
const toKp = new web3.Keypair();
// Define mint parameters
const mintDecimals = 6;
const mintAuthority = provider.wallet.publicKey;
const freezeAuthority = provider.wallet.publicKey;
it("Creates a mint account and ATA using TypeScript", async () => {
// Create the Mint
const mintPublicKey = await splToken.createMint(
provider.connection,
signerKp,
mintAuthority,
freezeAuthority,
mintDecimals
);
console.log("Created Mint:", mintPublicKey.toString());
// Create ATA for the signer
const ataAddress = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
signerKp.publicKey
);
console.log("Created ATA:", ataAddress.toString());
// Mint some tokens
const mintAmount = BigInt(1000 * (10 ** mintDecimals)); // 1000 tokens
await splToken.mintTo(
provider.connection,
signerKp,
mintPublicKey,
ataAddress,
mintAuthority,
mintAmount
);
// Verify the mint
const mintInfo = await splToken.getMint(provider.connection, mintPublicKey);
assert.equal(mintInfo.decimals, mintDecimals, "Mint decimals should match");
assert.equal(mintInfo.mintAuthority?.toString(), mintAuthority.toString(), "Mint authority should match");
assert.equal(mintInfo.freezeAuthority?.toString(), freezeAuthority.toString(), "Freeze authority should match");
// Verify the balance
const accountInfo = await splToken.getAccount(provider.connection, ataAddress);
assert.equal(accountInfo.amount.toString(), mintAmount.toString(), "Balance should match minted amount");
});
});
Fetching Token Balances in TypeScript
现在,添加下面的 it 测试块来读取代币余额。
这个测试块与第一个测试类似:
- 它创建了一个新的 mint 并为
signerKp创建对应的 ATA,并向这个 ATA(ataAddress)中铸造了初始数量的代币(本例中为 1000 个)。 - 这里的重点是演示余额的检索。我们展示了两种方法:
splToken.getAccount:获取完整的代币账户状态,我们可以从中访问.amount属性。provider.connection.getTokenAccountBalance:这是一个更直接的 RPC 调用,专门用于获取代币账户的余额。它返回一个包含金额信息的对象。
- 为了便于说明,我们使用了这两种方法,并断言检索到的余额与铸造的数量相符。
it("Reads token balance using TypeScript", async () => {
// Create a new mint for this test
const mintPublicKey = await splToken.createMint(
provider.connection,
signerKp,
mintAuthority,
freezeAuthority,
mintDecimals
);
// Create ATA
const ataAddress = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
signerKp.publicKey
);
// Mint tokens
const mintAmount = BigInt(1000 * (10 ** mintDecimals)); // 1000 tokens
await splToken.mintTo(
provider.connection,
signerKp,
mintPublicKey,
ataAddress,
mintAuthority,
mintAmount
);
// Read balance using getAccount
const accountInfo = await splToken.getAccount(provider.connection, ataAddress);
console.log("Token Balance:", accountInfo.amount.toString());
assert.equal(accountInfo.amount.toString(), mintAmount.toString(), "Balance should match minted amount");
// Alternative: Read balance using getTokenAccountBalance
const balance = await provider.connection.getTokenAccountBalance(ataAddress);
assert.equal(balance.value.amount, mintAmount.toString(), "Balance should match minted amount");
});
Testing Moving Tokens Between Accounts
最后,添加最后一个 it 测试块来进行代币转账。
这个测试块:
- 创建一个新的 mint。
- 随后为该 mint 创建两个 ATA:一个用作源 ATA(
signerKp),另一个用作目标 ATA(toKp)。请注意,toKp是一个新生成的密钥对,代表另一个用户。 - 向源 ATA(即
signerKp的 ATA)中铸造 1000 单位的代币。 - 此测试的核心是
splToken.transfer函数。该函数用于构建并发送在 ATA 之间移动代币的交易。它需要网络连接、付款人/签名者(signerKp)、源 ATA、目标 ATA、源 ATA 的权限拥有者(即signerKp.publicKey)以及要转账的金额(500 个代币)。 - 转账完成后,它使用
provider.connection.getTokenAccountBalance获取源和目标 ATA 的余额以验证结果。最后,我们断言源余额已减去转账的金额,并且目标余额现在等于转账金额。
it("Transfers tokens using TypeScript", async () => {
// Create a new mint
const mintPublicKey = await splToken.createMint(
provider.connection,
signerKp,
mintAuthority,
freezeAuthority,
mintDecimals
);
// Create source ATA
const sourceAta = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
signerKp.publicKey
);
// Create destination ATA
const destinationAta = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
toKp.publicKey
);
// Mint tokens to source
const mintAmount = BigInt(1000 * (10 ** mintDecimals)); // 1000 tokens
await splToken.mintTo(
provider.connection,
signerKp,
mintPublicKey,
sourceAta,
mintAuthority,
mintAmount
);
// Read balance before transfer
const sourceBalanceBefore = await provider.connection.getTokenAccountBalance(sourceAta);
const destinationBalanceBefore = await provider.connection.getTokenAccountBalance(destinationAta);
console.log("Source Balance before transfer:", sourceBalanceBefore.value.amount);
console.log("Destination Balance before transfer:", destinationBalanceBefore.value.amount);
// Transfer tokens
const transferAmount = BigInt(500 * (10 ** mintDecimals)); // 500 tokens
await splToken.transfer(
provider.connection,
signerKp,
sourceAta,
destinationAta,
signerKp.publicKey,
transferAmount
);
// Read balance after transfer
const sourceBalanceAfter = await provider.connection.getTokenAccountBalance(sourceAta);
const destinationBalanceAfter = await provider.connection.getTokenAccountBalance(destinationAta);
console.log("Source Balance after transfer:", sourceBalanceAfter.value.amount);
console.log("Destination Balance after transfer:", destinationBalanceAfter.value.amount);
assert.equal(sourceBalanceAfter.value.amount, (mintAmount - transferAmount).toString(), "Source should have 500 tokens left");
assert.equal(destinationBalanceAfter.value.amount, transferAmount.toString(), "Destination should have received 500 tokens");
});
我们运行完整的测试,看看是否一切都按预期工作。

练习:编写一个 disable_mint_authority 函数,通过 set_authority 指令将铸造权限设置为 None。将权限类型 (authority type) 设置为 AuthorityType::MintTokens。完成后,编写一个测试来调用该函数,然后尝试铸造更多代币,此时应触发“supply is fixed”的错误并失败。此外还要检查铸造权限现在是否为 null。
你应该会得到与下面类似的结果。

本文是 Solana 教程系列 的一部分。