本文介绍了如何使用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 = spl...                                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!