Solana 60 天课程

2025年02月27日更新 75 人订阅
原价: ¥ 28 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token

使用 Anchor 和 Web3.js 转移 SPL Token

本文介绍了如何使用Anchor在Solana链上创建、铸造和转移SPL代币,并通过TypeScript客户端直接与Token Program交互实现相同的功能。文章详细讲解了使用Anchor构建Solana程序,通过CPI调用Token Program,以及如何使用@solana/spl-token库在客户端直接创建和操作SPL代币。

在之前的教程中,我们学习了 SPL Token 的工作原理。在本教程中,我们将实现一个完整的 SPL Token 生命周期:使用两种方法创建、铸造、转移和查询 Token:

  • 使用 Anchor 的链上:我们将创建一个带有 Anchor 的 Solana 程序,该程序铸造 SPL Token,直到达到预定义的供应上限。
  • 使用 TypeScript 的客户端:我们还将展示如何直接从 TypeScript 客户端与 Token Program 交互,以创建 SPL mints、ATAs、铸造 Token、转移和读取余额。

为什么采用两种方法?

了解如何同时做到这两点至关重要,因为:

  • 通过 Anchor,我们可以在 SPL Token 之上构建自定义链上逻辑(例如,归属时间表、有条件铸造),或者创建一个由我们的程序而不是钱包控制的 SPL Token。
  • 通过 TypeScript,我们可以直接与 SPL 程序交互,以进行简单的活动,例如转移 SPL Token 或授权/撤销委托。

现在让我们从 Anchor 方法开始。

在 Anchor 中创建 SPL Token

回想一下之前的 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 中的代码替换为以下代码:

在此代码中,我们:

  • 导入我们的依赖项:
    • 用于创建关联 Token 账户 (ATA) 的 anchor_spl::associated_token::AssociatedToken
    • 用于操作 SPL Token Program 的 anchor_spl::token::{Mint, MintTo, Token, TokenAccount, Transfer}(这些是我们铸造和转移所需的指令和账户类型)。
  • 定义一个 create_and_mint_token 函数,该函数:
  1. 使用提供的 mint 账户和目标 ATA(将在其中存入铸造的 Token)。
  2. 构建一个指向 Token Program 的 CPI 上下文。
  3. 调用 Token Program 的 mint_to 指令,将 100 个 Token(精度为 9)铸造到 ATA。
  4. 一旦 Token 被铸造,则返回成功。
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 余额的账户)
  • 最后,我们传递 Token Program、关联 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.\
                // 当创建一个没有冻结权限的 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 结构体开始。

用于列出创建 mint 账户所需账户的代码截图

signer

首先,我们声明支付 Token 部署交易费用的 igner,如下面紫色高亮显示的部分所示。

创建 mint 的 signer 的代码

mint

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

显示创建新 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 的关联 Token 账户 (ATA)(如下面黄色高亮显示的部分)。

注意:mint 账户不需要存在 ATA。我们只在这里创建一个,因为我们想将一些铸造给 igner。

用于创建新 ata 的 Anchor 约束

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 用于为账户分配空间和管理租金。

显示 Token Program、关联 Token Program 和 System Program 的代码

你可能已经注意到,ATA(new_ata 账户)没有像 mint 账户(new_mint)那样的种子和 bump,这是因为 InitializeAccount 指令使用标准的关联 Token 账户派生过程,即 user_wallet_address + token_mint_address => associated_token_account_address。因此我们不必传递种子和 bump。如果你尝试传递种子和 bump,Anchor 会抛出此错误。

显示 ATA 不能使用种子的语法错误

我们也没有指定 mint 账户和 ATA 的 space,因为 Anchor 也会在后台为我们添加空间。它知道这些信息,因为我们指定该程序是 AssociatedToken。如果我们尝试为它们中的任何一个指定 space,则会发生错误。

显示 ATA 不能指定空间的语法错误

mint 和关联 Token 账户的实际大小分别为 82 字节和 165 字节。

现在我们已经声明了我们需要的所有账户,让我们检查用于铸造 SPL Token 的 create_and_mint_token 函数。

铸造 SPL Token

我们使用此函数将我们刚刚创建的 100 个(精度为 9)Token 铸造到 igner 的新创建的 ATA。

显示 MintTo 指令的账户的代码

我们在上面的代码中构造了一个 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…)] 约束 的原因;它确保这些账户在指令运行之前被创建。

确保在运行创建 mint 账户的指令之前创建账户的代码

注意:要在 Solana 上创建 NFT,你需要使用 mint::decimals = 0 初始化 mint,将正好 1 个 Token 铸造给接收者,然后通过将其设置为 None 来撤销 mint 权限。这确保了永远不会铸造更多 Token,并使 Token 具有唯一性和非同质性,因为它不是分数的,因为小数位数为零。

测试 createAndMintToken 函数

现在,我们将测试 createAndMintToken 函数。

tests/spl_token.ts 中的测试代码替换为以下代码。该测试以这种方式构建。

  1. 我们使用 @coral-xyz/anchor 库中的 findProgramAddressSync 从链下派生 Token 的 mint 账户地址,使用与我们的 Anchor 程序中使用的相同的种子。此步骤不会部署 mint 账户,我们已经在 Anchor 程序中处理了它,如前所述。
  2. 接下来,我们使用 getAssociatedTokenAddressSync 函数计算 igner 的 ATA 地址。同样,这不会部署该账户。
  3. 我们使用适当的账户( igner、mint、ATA、Token Program、ATA Program 和 System Program)调用 Anchor 程序函数,并打印交易哈希、Token 地址和 igner 的 ATA 地址。
  4. 最后,我们使用 @solana/spl-token 库中的 getMintgetAccount 函数检索 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 是否已成功部署。

显示 Anchor 测试成功的截图

转移 SPL Token

要转移 Token,我们构造一个 Transfer 指令并对 Token Program 进行 CPI。此转移通过将指定的 Token 单位数量从源关联的 Token 账户移动到目标关联的 Token 账户来工作。此交易的 igner 必须是源 ATA 的权限。

现在将以下函数添加到你的程序中。它执行以下操作:

  1. 加载源关联的 Token 账户(from_ata),Token 将从中取出。
  2. 加载目标关联的 Token 账户(to_ata),Token 将发送到该账户(此 ATA 将在我们的测试代码中创建)。
  3. 加载必须签名并批准转移的权限账户(from)。
  4. 加载将处理转移的 Token Program 账户。
  5. 使用源、目标和权限账户构建 Transfer 指令。
  6. 创建一个包装 Token Program 和转移指令的 CPI(跨程序调用)上下文。
  7. 使用 CPI 上下文和金额调用 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 发送者的 ATA
  • to:是 Token 接收者的 ATA
  • authority:是具有从 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。

将此测试添加到我们的测试文件中。

我们在测试中执行以下操作。

  1. 首先,我们使用 findProgramAddressSync 函数派生出我们想要转移的 Token(mint 账户)的地址。
  2. 接下来,我们使用 @solana/spl-token 中的 getAssociatedTokenAddressSync 计算源(发送者的钱包)和目标(接收者的钱包)ATA 的 ATA 地址,它接受 mint 地址、各自的账户地址和一个布尔值,该值指示 ATA igner(signerKp)是否为 PDA。在这种情况下,它不是。
  3. 我们使用 createAssociatedTokenAccount 函数为目标账户创建 ATA。我们没有为 igner 创建 ATA,因为它已经在之前的测试用例中完成。由于所有测试用例一起运行,因此该账户会持续存在。
  4. 最后,我们使用程序中的 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"
    );
    // 接收者余额应与转移金额匹配
  });

现在运行测试

成功的测试运行

检索 Token 余额

将此函数添加到程序以检索 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)。这是我们在之前的测试用例中转移的金额。

成功转移 SPL Token 的测试

直接使用 Typescript 客户端创建和转移 Token

也可以通过简单地使用 web3.js Typescript 客户端来创建 SPL Token 并与之交互,而无需 Solana 程序。

当你不需要带有自定义逻辑的链上程序时,这很有用。如果你只是铸造 Token、转移它们或读取余额,从客户端执行它会更快且更便宜。无需编写或部署程序。

让我们直接从 TypeScript 创建新的 Token 和 ATA,并转移它们。

在 TypeScript 中创建 Mint 和 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 的数量匹配");
  });

测试在帐户之间转移 Token

最后,添加最后一个 it 测试块以转移 token。

此测试块:

  • 创建一个新的 mint。
  • 然后,它为此 mint 创建两个 ATA:一个用于源 ATA (signerKp),一个用于目标 ATA (toKp)。 请注意,toKp 是一个新生成的密钥对,代表另一个用户。
  • 将 1000 个单位的 token mint 到源 ATA (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");
  });

我们运行完整的测试,看看一切是否按预期工作。

一个显示 SPL 转移成功的测试

练习:编写一个 disable_mint_authority 函数,通过 set_authority 指令将 mint 授权设置为 None。 将授权类型设置为 AuthorityType::MintTokens。 之后,编写一个测试来调用该函数,然后尝试 mint 更多 token,它应该会失败,并显示“供应已固定”错误。 另请检查 mint 权限现在是否为 null

你应该得到如下类似的结果。

成功编程练习的预期结果

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

  • 原文链接: rareskills.io/post/spl-t...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论