Solana 60 天课程

2025年02月27日更新 80 人订阅
原价: ¥ 36 限时优惠
专栏简介 开始 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 Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 中的 Ed25519 签名验证 Solana 指令自省

使用 Anchor 和 Web3.js 转移 SPL Token

  • RareSkills
  • 发布于 2025-10-14 08:11
  • 阅读 871

本文介绍了如何使用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 = spl...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论