本文介绍了如何使用Anchor在Solana链上创建、铸造和转移SPL代币,并通过TypeScript客户端直接与Token Program交互实现相同的功能。文章详细讲解了使用Anchor构建Solana程序,通过CPI调用Token Program,以及如何使用@solana/spl-token库在客户端直接创建和操作SPL代币。
在之前的教程中,我们学习了 SPL Token 的工作原理。在本教程中,我们将实现一个完整的 SPL Token 生命周期:使用两种方法创建、铸造、转移和查询 Token:
了解如何同时做到这两点至关重要,因为:
现在让我们从 Anchor 方法开始。
回想一下之前的 SPL Token 教程,每个 Token 都使用相同的链上程序(地址为 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
的 SPL Token Program)来创建 mint 账户并执行 Token 铸造、转移、批准等操作。
在本节中,我们将构建一个 Anchor 程序,该程序通过跨程序调用 (CPI) 创建和铸造 SPL Token 到 Token Program。
我们的程序将只有两个函数:
create_and_mint_token
函数,用于创建 mint 账户,并通过 CPI 向 Token Program 将初始供应量铸造到指定的关联 Token 账户 (ATA)。transfer_tokens
函数,用于通过 CPI 向 Token Program 将 Token 从源 ATA 移动到目标 ATA。现在,使用 anchor init spl_token
创建一个新的 Anchor 项目。打开项目并将 programs/spl_token/src/lib.rs
中的代码替换为以下代码:
在此代码中,我们:
anchor_spl::associated_token::AssociatedToken
。anchor_spl::token::{Mint, MintTo, Token, TokenAccount, Transfer}
(这些是我们铸造和转移所需的指令和账户类型)。create_and_mint_token
函数,该函数:mint_to
指令,将 100 个 Token(精度为 9)铸造到 ATA。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
// 此函数部署了一个新的 SPL token,精度为 9,并铸造了 100 个单位的 token
pub fn create_and_mint_token(ctx: Context<CreateMint>) -> Result<()> {
let mint_amount = 100_000_000_000; // 100 tokens with 9 decimals
// 100 个 Token,精度为 9
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 权限的代表new_mint
:一个 mint PDA 账户,它被初始化为 9 位小数,并使用 igner 作为 mint 权限和冻结权限new_ata
:一个将为新的 mint 创建的关联 Token 账户,并使用 igner 作为其权限(实际上,是持有 igner 余额的账户)
#[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.\
// 当创建一个没有冻结权限的 token 时,Solana 会阻止任何未来的更新。
// This makes the token more decentralized, as no authority can freeze a user's ATA.\
// 这使得 token 更加去中心化,因为没有任何权限可以冻结用户的 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…)
// 这代表 SPL Token Program (TokenkegQfeZ…)
// The same program we introduced in the previous article that owns and manages all mint and associated token account.
// 我们在上一篇文章中介绍的同一个程序,它拥有和管理所有 mint 账户和关联的 token 账户。
pub token_program: Program<'info, Token>,
// This represents the ATA program (ATokenGPvbdGV...)
// 这代表 ATA 程序 (ATokenGPvbdGV...)
// As mentioned in the previous tutorial, it is only in charge of creating the ATA.
// 正如前面的教程中提到的,它只负责创建 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"
# 添加了 "anchor-spl/idl-build"
[dependencies]
anchor-lang = "0.31.0"
anchor-spl = "0.31.0" # added this
# 添加了此项
这个 anchor-spl
让我们有权访问 SPL Token Program、ATA 程序及其指令。
现在,让我们检查程序代码中发生的事情。
我们从 CreateMint
结构体开始。
首先,我们声明支付 Token 部署交易费用的 igner,如下面紫色高亮显示的部分所示。
接下来,我们声明一个 new_mint
账户,它代表我们想要创建的 SPL Token(如下面红色高亮显示的部分)。它的账户类型是 Mint
(如下面黄色高亮显示的部分)。此账户类型表示 Solana 上的 mint 账户。
正如你在上图中看到的,我们将这个新的 mint 账户初始化为 Program Derived Address (PDA) 并设置其参数:Token 小数位数、mint 和冻结权限以及 PDA 种子。我们没有使用密钥对账户,而是从固定种子和程序 ID 中派生出 mint 作为 PDA,因此无需像密钥对账户那样生成或管理私钥。我们主要为了方便起见而使用 mint PDA。
如果你不熟悉 PDA 的工作原理或它们与密钥对账户的区别,请查看我们的文章“Solana 中的 PDA(程序派生地址)与密钥对账户”。
最后,init
约束告诉 Anchor 在 create_and_mint_token
运行时自动创建和初始化 mint 账户(我们将在接下来解释该函数)。
由于此 init
约束,Anchor 将在后台对 Token Program 的 InitializeMint
指令进行 CPI(跨程序调用)。此指令将 mint 的小数位数设置为 9,并将 mint 和冻结授权分配给 igner。
接下来是我们用来铸造此 Token 的关联 Token 账户 (ATA)(如下面黄色高亮显示的部分)。
注意:mint 账户不需要存在 ATA。我们只在这里创建一个,因为我们想将一些铸造给 igner。
ATA 的类型为 TokenAccount
,它表示 Solana 上的 ATA。与 mint 账户一样,我们设置其参数:ATA 的 mint 设置为我们正在创建的新 Token,并且 igner 成为其权限。这意味着只有 igner 才能授权修改 ATA 状态的指令。Anchor 在内部执行 CPI 到 Token Program 的 InitializeAccount
指令以应用这些设置。
注意:我们在此处可以安全地使用 init
,仅仅是因为 mint 账户(new_mint
)也在同一指令中创建。如果 mint 已经存在,则如果有人已经创建了该 ATA,在 ATA 上使用 init
可能会失败,从而导致拒绝服务。如果 mint 可能已经存在,则使用 init_if_needed
更安全。否则,有人可能会抢先执行该指令并代表 igner 创建 ATA,并导致此交易失败。
最后,我们声明创建 mint 和关联 Token 账户所需的本地 Solana 程序(如下面绿色高亮显示的部分)。这些是我们的 Anchor 程序与之交互的链上程序:Token Program 用于创建 mint 和铸造 Token,关联 Token 账户程序 用于创建用户的 ATA,以及 System Program 用于为账户分配空间和管理租金。
你可能已经注意到,ATA(new_ata
账户)没有像 mint 账户(new_mint
)那样的种子和 bump,这是因为 InitializeAccount
指令使用标准的关联 Token 账户派生过程,即 user_wallet_address + token_mint_address => associated_token_account_address
。因此我们不必传递种子和 bump。如果你尝试传递种子和 bump,Anchor 会抛出此错误。
我们也没有指定 mint 账户和 ATA 的 space
,因为 Anchor 也会在后台为我们添加空间。它知道这些信息,因为我们指定该程序是 AssociatedToken
。如果我们尝试为它们中的任何一个指定 space
,则会发生错误。
mint 和关联 Token 账户的实际大小分别为 82 字节和 165 字节。
现在我们已经声明了我们需要的所有账户,让我们检查用于铸造 SPL Token 的 create_and_mint_token
函数。
我们使用此函数将我们刚刚创建的 100 个(精度为 9)Token 铸造到 igner 的新创建的 ATA。
我们在上面的代码中构造了一个 MintTo
指令。以下三个字段定义了 MintTo
行为:
mint
:我们正在铸造哪个 Token,由 mint 账户指定to
:将接收铸造的 Token 的 ATA。authority
:允许为此 mint 铸造 Token 的账户。在我们的程序中,我们将 mint 权限设置为交易 igner(signer
),因此 igner 必须签名并且与 mint 的权限匹配才能成功铸造。然后,我们使用此指令对 Token Program 进行 CPI(如绿色高亮显示的部分所示),这会将 100 个单位的 Token 铸造到关联的 Token 账户。
此外,正如上一教程中讨论的那样,在调用 MintTo
指令之前,mint 账户和 ATA 都必须存在(这也适用于 Transfer
)。这就是我们使用 #[account(init…)]
约束 的原因;它确保这些账户在指令运行之前被创建。
注意:要在 Solana 上创建 NFT,你需要使用 mint::decimals = 0
初始化 mint,将正好 1 个 Token 铸造给接收者,然后通过将其设置为 None
来撤销 mint 权限。这确保了永远不会铸造更多 Token,并使 Token 具有唯一性和非同质性,因为它不是分数的,因为小数位数为零。
现在,我们将测试 createAndMintToken
函数。
将 tests/spl_token.ts
中的测试代码替换为以下代码。该测试以这种方式构建。
@coral-xyz/anchor
库中的 findProgramAddressSync
从链下派生 Token 的 mint 账户地址,使用与我们的 Anchor 程序中使用的相同的种子。此步骤不会部署 mint 账户,我们已经在 Anchor 程序中处理了它,如前所述。getAssociatedTokenAddressSync
函数计算 igner 的 ATA 地址。同样,这不会部署该账户。@solana/spl-token
库中的 getMint
和 getAccount
函数检索 mint 和 ATA 信息,并断言它们的内容与我们之前在 Anchor 程序中设置的内容匹配。我们断言 Token 小数位数、权限、Token 供应量、Token 的 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
// 使用与在 Anchor 程序中创建 mint 时使用的相同种子(“my_mint”+ signer 公钥)派生 mint 地址
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from("my_mint"), signerKp.publicKey.toBuffer()],
program.programId
);
// Get the associated token account address
// 获取关联的 Token 账户地址
// The boolean value here indicates whether the authority of the ATA is an "off-curve" address (i.e., a PDA).
// 此处的布尔值表示 ATA 的权限是否为“off-curve”地址(即 PDA)。
// A value of false means the owner is a normal wallet address.
// 值为 false 表示所有者是普通钱包地址。
// `signerKp` is the owner here and it is a normal wallet address, so we use false.
// `signerKp` 是此处的所有者,它是一个普通的钱包地址,因此我们使用 false。
const ata = splToken.getAssociatedTokenAddressSync(mint, signerKp.publicKey, false)
// Call the create_mint instruction
// 调用 create_mint 指令
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
// 验证 Token 详细信息
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)");
// 供应量应为 100 个 Token(精度为 9)
// Verify the ATA details
// 验证 ATA 详细信息
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)");
// Token 余额应为 100 个 Token(精度为 9)
assert.equal(tokenAccount.delegate, null, "Token account should not have a delegate");
// Token 账户不应有委托
});
});
运行 npm install @solana/spl-token
以安装 SPL Token 库。
现在运行 anchor test
并查看 Token 和 ATA 是否已成功部署。
要转移 Token,我们构造一个 Transfer
指令并对 Token Program 进行 CPI。此转移通过将指定的 Token 单位数量从源关联的 Token 账户移动到目标关联的 Token 账户来工作。此交易的 igner 必须是源 ATA 的权限。
现在将以下函数添加到你的程序中。它执行以下操作:
from_ata
),Token 将从中取出。to_ata
),Token 将发送到该账户(此 ATA 将在我们的测试代码中创建)。from
)。Transfer
指令。token::transfer
函数,该函数将 Token 从源 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
// 将 Token 从 from_ata 转移到 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
// 创建 CPI 上下文
token::transfer(cpi_ctx, amount)?;
Ok(())
}
在 ERC-20 中,transfer
假定 msg.sender
作为 Token 所有者,而 transferFrom
允许第三方(委托人)在获得批准的情况下代表他人移动 Token。SPL Token Program 将两者合并为一个 transfer
指令,但需要 显式地 将转移权限作为账户(在我们的 Anchor 代码中为 AccountInfo
)传递 - 这映射到 Transfer.authority
字段。此权限是允许移动 Token 的 igner;它可以是 Token 所有者或经过批准的委托人。
因此,在 transfer
指令中:
from
:是 Token 发送者的 ATAto
:是 Token 接收者的 ATAauthority
:是具有从 from
移动 Token 的权限的 igner(可以是所有者或具有批准的委托人)现在添加下面的 TransferSpl
账户结构体,它定义了执行 Token 转移所需的账户。
#[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
// 我们正在与 Token Program 交互
}
我们传递交易 igner,源和目标 ATA,最后是我们与之交互的 Token Program。
将此测试添加到我们的测试文件中。
我们在测试中执行以下操作。
findProgramAddressSync
函数派生出我们想要转移的 Token(mint 账户)的地址。@solana/spl-token
中的 getAssociatedTokenAddressSync
计算源(发送者的钱包)和目标(接收者的钱包)ATA 的 ATA 地址,它接受 mint 地址、各自的账户地址和一个布尔值,该值指示 ATA igner(signerKp
)是否为 PDA。在这种情况下,它不是。createAssociatedTokenAccount
函数为目标账户创建 ATA。我们没有为 igner 创建 ATA,因为它已经在之前的测试用例中完成。由于所有测试用例一起运行,因此该账户会持续存在。transfer_tokens
函数将 10 个 Token 转移到目标 ATA。然后,我们使用 getTokenAccountBalance
函数检索目标 ATA 的 Token 余额,并断言它是 10(我们发送的金额)。
it("Transfers tokens using CPI", async () => {
// Derive the PDA for the mint
// 派生 mint 的 PDA
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from("my_mint"), signerKp.publicKey.toBuffer()],
program.programId
);
// Get the ATAs
// 获取 ATA
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
// 创建 to_ata,因为它尚未存在
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
// 10 个 Token,精度为 9
// Transfer tokens
// 转移 Token
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"
);
// 接收者余额应与转移金额匹配
});
现在运行测试
将此函数添加到程序以检索 ATA Token 余额
pub fn get_balance(ctx: Context<GetBalance>) -> Result<()> {
// Get the token account address, its owner & balance
// 获取 Token 账户地址、其所有者和余额
let ata_pubkey = ctx.accounts.token_account.key();
let owner = ctx.accounts.token_account.owner; // the `owner` is a field in the ATA
// `owner` 是 ATA 中的一个字段
let balance = ctx.accounts.token_account.amount; // the `amount` is a field in the ATA
// `amount` 是 ATA 中的一个字段
// Print the balance information
// 打印余额信息
msg!("Token Account Address: {}", ata_pubkey);
msg!("Token Account Owner: {}", owner);
msg!("Token Account Balance: {}", balance);
Ok(())
}
ATA 中的 amount
字段保存 Token 余额。在此函数中,我们直接从 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
// 派生 mint 的 PDA
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from("my_mint"), signerKp.publicKey.toBuffer()],
program.programId
);
// Get the associated token account address
// 获取关联的 Token 账户地址
const ata = splToken.getAssociatedTokenAddressSync(mint, signerKp.publicKey, false);
// Call the get_balance instruction
// 调用 get_balance 指令
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");
// Token 余额应大于 0
});
如果我们运行验证器并检查日志,我们应该会看到 igner 的 ATA 余额减少了 10 个 Token(从 100 减少到 90)。这是我们在之前的测试用例中转移的金额。
也可以通过简单地使用 web3.js Typescript 客户端来创建 SPL Token 并与之交互,而无需 Solana 程序。
当你不需要带有自定义逻辑的链上程序时,这很有用。如果你只是铸造 Token、转移它们或读取余额,从客户端执行它会更快且更便宜。无需编写或部署程序。
让我们直接从 TypeScript 创建新的 Token 和 ATA,并转移它们。
创建一个新的 Anchor 项目 spl_token_ts
,并将测试替换为此部分后面显示的 TypeScript 代码块。
此 TypeScript 测试套件演示了如何使用 @solana/spl-token
库直接与 SPL Token 程序交互。
它执行以下操作:
splToken.createMint
。此函数向 Token Program 发送一个 InitializeMint
指令,以创建一个新的 SPL Token mint 账户。我们提供连接、付款人(signerKp
,我们的默认本地 igner)、mint 权限和冻结权限,以及所需的小数位数(在本例中为 6)。它返回新创建的 mint 的公钥。splToken.createAssociatedTokenAccount
为新创建的 mint 创建 signerKp
的 ATA。这是来自 @solana/spl-token
TypeScript SDK 的一个助手。在底层,它派生出 ATA 地址并将创建指令发送到关联的 Token 账户程序splToken.mintTo
以发行新的 Token 单位。它需要连接、交易的付款人(我们使用 signerKp
)、mint 的公钥、目标 ATA 地址、mint 权限的公钥(signerKp.PublicKey
)和要铸造的 Token 数量(我们考虑了小数位数)。splToken.getMint
获取 mint 账户的链上数据,并且我们断言小数位数和权限与我们指定的内容匹配。splToken.getAccount
获取 ATA 的数据,并且我们断言其 Token 余额与我们刚刚铸造的金额匹配。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
// 定义 mint 参数
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
// 创建 Mint
const mintPublicKey = await splToken.createMint(
provider.connection,
signerKp,
mintAuthority,
freezeAuthority,
mintDecimals
);
console.log("Created Mint:", mintPublicKey.toString());
// Create ATA for the signer
// 为 igner 创建 ATA
const ataAddress = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
signerKp.publicKey
);
console.log("Created ATA:", ataAddress.toString());
// Mint some tokens
// 铸造一些 Token
const mintAmount = BigInt(1000 * (10 ** mintDecimals)); // 1000 tokens
// 1000 个 Token
await splToken.mintTo(
provider.connection,
signerKp,
mintPublicKey,
ataAddress,
mintAuthority,
mintAmount
);
// 验证 mint const mintInfo = await splToken.getMint(provider.connection, mintPublicKey); assert.equal(mintInfo.decimals, mintDecimals, "Mint 的小数位数应该匹配"); assert.equal(mintInfo.mintAuthority?.toString(), mintAuthority.toString(), "Mint 的授权者应该匹配"); assert.equal(mintInfo.freezeAuthority?.toString(), freezeAuthority.toString(), "冻结授权者应该匹配");
// 验证余额
const accountInfo = await splToken.getAccount(provider.connection, ataAddress);
assert.equal(accountInfo.amount.toString(), mintAmount.toString(), "余额应该和 mint 的数量匹配");
}); });
### 在 TypeScript 中获取 Token 余额
现在,添加以下 `it` 测试块以读取 token 余额。
此测试块与第一个测试类似:
- 它为 `signerKp` 创建一个新的 mint 及其对应的 ATA,并将初始数量的 token(在本例中为 1000)mint 到此 ATA(`ataAddress`)。
- 这里的重点是演示余额检索。 我们展示了两种方法:
- `splToken.getAccount`:获取整个 token 帐户状态,我们可以从中访问 `.amount` 属性。
- `provider.connection.getTokenAccountBalance`:这是一个更直接的 RPC 调用,专门用于获取 token 帐户的余额。 它返回一个包含金额的对象。
- 为了便于说明,我们使用了这两种方法,并断言检索到的余额与 mint 的金额相符。
```tsx hljs language-typescript
it("使用 TypeScript 读取 token 余额", async () => {
// 为此测试创建一个新的 mint
const mintPublicKey = await splToken.createMint(
provider.connection,
signerKp,
mintAuthority,
freezeAuthority,
mintDecimals
);
// 创建 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
);
// 使用 getAccount 读取余额
const accountInfo = await splToken.getAccount(provider.connection, ataAddress);
console.log("Token 余额:", accountInfo.amount.toString());
assert.equal(accountInfo.amount.toString(), mintAmount.toString(), "余额应该和 mint 的数量匹配");
// 替代方案:使用 getTokenAccountBalance 读取余额
const balance = await provider.connection.getTokenAccountBalance(ataAddress);
assert.equal(balance.value.amount, mintAmount.toString(), "余额应该和 mint 的数量匹配");
});
最后,添加最后一个 it
测试块以转移 token。
此测试块:
signerKp
),一个用于目标 ATA (toKp
)。 请注意,toKp
是一个新生成的密钥对,代表另一个用户。signerKp
的 ATA)。splToken.transfer
函数。 此函数构建并发送交易以在 ATA 之间移动 token。 它需要连接、付款人/签名者 (signerKp
)、源 ATA、目标 ATA、源 ATA 的授权者(即 signerKp.publicKey
)以及要转移的金额(500 个 token)。provider.connection.getTokenAccountBalance
获取源 ATA 和目标 ATA 的余额来验证结果。 最后,我们断言源余额已减少转移金额,并且目标余额现在等于转移金额。
it("使用 TypeScript 转移 token", async () => {
// 创建一个新的 mint
const mintPublicKey = await splToken.createMint(
provider.connection,
signerKp,
mintAuthority,
freezeAuthority,
mintDecimals
);
// 创建源 ATA
const sourceAta = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
signerKp.publicKey
);
// 创建目标 ATA
const destinationAta = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
toKp.publicKey
);
// 将 token mint 到源
const mintAmount = BigInt(1000 * (10 ** mintDecimals)); // 1000 个 tokens
await splToken.mintTo(
provider.connection,
signerKp,
mintPublicKey,
sourceAta,
mintAuthority,
mintAmount
);
// 读取转移前的余额
const sourceBalanceBefore = await provider.connection.getTokenAccountBalance(sourceAta);
const destinationBalanceBefore = await provider.connection.getTokenAccountBalance(destinationAta);
console.log("转移前的源余额:", sourceBalanceBefore.value.amount);
console.log("转移前的目标余额:", destinationBalanceBefore.value.amount);
// 转移 token
const transferAmount = BigInt(500 * (10 ** mintDecimals)); // 500 个 tokens
await splToken.transfer(
provider.connection,
signerKp,
sourceAta,
destinationAta,
signerKp.publicKey,
transferAmount
);
// 读取转移后的余额
const sourceBalanceAfter = await provider.connection.getTokenAccountBalance(sourceAta);
const destinationBalanceAfter = await provider.connection.getTokenAccountBalance(destinationAta);
console.log("转移后的源余额:", sourceBalanceAfter.value.amount);
console.log("转移后的目标余额:", destinationBalanceAfter.value.amount);
assert.equal(sourceBalanceAfter.value.amount, (mintAmount - transferAmount).toString(), "源应该还剩 500 个 token");
assert.equal(destinationBalanceAfter.value.amount, transferAmount.toString(), "目标应该收到 500 个 token");
});
我们运行完整的测试,看看一切是否按预期工作。
练习:编写一个 disable_mint_authority
函数,通过 set_authority
指令将 mint 授权设置为 None
。 将授权类型设置为 AuthorityType::MintTokens
。 之后,编写一个测试来调用该函数,然后尝试 mint 更多 token,它应该会失败,并显示“供应已固定”错误。 另请检查 mint 权限现在是否为 null
。
你应该得到如下类似的结果。
本文 是Solana 教程系列的一部分。
- 原文链接: rareskills.io/post/spl-t...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!