如何使用 Anchor 转移 SOL 和 SPL 代币

如何使用 Anchor 转移 SOL 和 SPL 代币

概述

Anchor 是一个加速在 Solana 区块链上开发安全程序的框架。在使用 Solana 和 Anchor 时,你可能会遇到需要在账户之间发送 SOL 或 SPL 代币的情况(例如,处理用户支付到你的国库,或让用户将他们的 NFT 发送到托管账户)。本指南将引导你通过使用 Anchor 转移 SOL 和 SPL 代币的过程。我们将涵盖程序和测试所需的代码,以确保账户之间的代币无缝转移。

你将做什么

在本指南中,你将:

  • 使用 Anchor 和 Solana Playground 创建一个 Solana 程序
  • 创建一个程序指令,将 SOL 在两个用户之间发送
  • 创建一个程序指令,将 SPL 代币在两个用户之间发送
  • 编写测试以验证代币转移

你将需要什么

本指南中使用的依赖

依赖项 版本
anchor-lang 0.26.0
anchor-spl 0.26.0
solana-program 1.14.12
spl-token 3.5.0

启动你的项目

通过访问 https://beta.solpg.io/ 在 Solana Playground 创建一个新项目。Solana Playground 是一个基于浏览器的 Solana 代码编辑器,可以让我们快速启动这个项目。你可以在自己的代码编辑器中跟随,但本指南将根据 Solana Playground 的必需步骤进行调整。首先,点击“创建新项目”:

创建新项目

输入项目名称“transfers”,选择“Anchor (Rust)”:

命名项目

创建并连接钱包

由于此项目仅用于演示目的,我们可以使用一个“临时”钱包。Solana Playground 使创建钱包变得简单。你应该看到浏览器窗口左下角显示红点“未连接”。点击它:

钱包设置按钮

Solana Playground 将为你生成一个钱包(或者你可以导入自己的钱包)。可以保存以备后用,当你准备好时点击继续。一个新的钱包将被初始化并连接到 Solana 开发网络。Solana Playground 会自动向你的新钱包空投一些 SOL,但我们会请求一些额外的 SOL,以确保我们有足够的资金来部署我们的程序。在浏览器终端中,你可以使用 Solana CLI 命令。输入 solana airdrop 2 向你的钱包空投 2 SOL。你的钱包现在应已连接到开发网络,余额为 6 SOL:

准备就绪

你准备好了!让我们开始构建!

创建转账程序

首先打开 lib.rs 并删除启动代码。拥有一个空白的界面后,我们可以开始构建程序。首先,我们需要导入一些依赖项。将以下内容添加到文件顶部:

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer as SplTransfer};
use solana_program::system_instruction;
declare_id!("11111111111111111111111111111111");

这些导入将允许我们使用 Anchor 框架、SPL 代币程序和系统程序。Solana Playground 在我们部署程序时将自动更新 declare_id!

创建转移 Lamports (SOL) 函数

要创建一个转移 SOL(或 lamports)的函数,我们必须为我们的转移上下文定义一个结构。将以下内容添加到你的程序中:

#[derive(Accounts)]
pub struct TransferLamports<'info> {
#[account(mut)]    
pub from: Signer<'info>,    
#[account(mut)]    
pub to: AccountInfo<'info>,    
pub system_program: Program<'info, System>,
}

该结构定义了一个将签名 transaction 并发送 SOL 的 from 账户,一个将接收 SOL 的 to 账户,以及处理转账的系统程序。属性 #[account(mut)] 表示程序将修改该账户。

接下来,我们将创建处理转账的函数。将以下内容添加到你的程序中:

#[program]
pub mod solana_lamport_transfer {
    use super::*;
    pub fn transfer_lamports(ctx: Context<TransferLamports>, amount: u64) -> Result<()> {
        let from_account = &ctx.accounts.from;
        let to_account = &ctx.accounts.to;

        // Create the transfer instruction
        let transfer_instruction = system_instruction::transfer(from_account.key, to_account.key, amount);

        // Invoke the transfer instruction
        anchor_lang::solana_program::program::invoke_signed(
            &transfer_instruction,
            &[
                from_account.to_account_info(),
                to_account.clone(),
                ctx.accounts.system_program.to_account_info(),
            ],
            &[],
        )?;

        Ok(())
    }
}

以下是该代码片段不同部分的简要解释:

  1. #[program] 属性将模块标记为 Anchor 程序。它生成所需的模板以定义程序的入口点,并自动处理账户验证和反序列化。

  2. solana_lamport_transfer 模块内部,使用 use super::*; 导入父模块所需的项。

  3. transfer_lamports 函数接受一个 Context 和一个 amount 作为其参数。Context 包含交易所需的账户信息,amount 是要转移的 lamports 数量。

  4. 我们创建对上下文中的 from_accountto_account 的引用,这些引用将用于转账。

  5. system_instruction::transfer 函数创建一个转账指令,该指令接受 from_account 的公钥、to_account 的公钥以及要转移的 amount 作为参数。

  6. anchor_lang::solana_program::program::invoke_signed 函数调用转账指令,并使用交易的签名者(from_account)。它接受转账指令、from_accountto_accountsystem_program 的账户信息数组,以及一个空数组作为签名者。

  7. transfer_lamports 函数返回 Ok(()) 以表示执行成功。

你可以通过点击 Build 按钮或在终端中输入 anchor build 来确保一切正常。如果出现错误,请检查你的代码与本指南中的代码并遵循错误响应的建议。如果你需要帮助,请随时通过 Discord 联系我们。

创建转移 SPL 代币的功能

在我们部署程序之前,让我们添加一个第二个功能来转移 SPL 代币。首先,创建该功能的新上下文。在你的程序中 _TransferLamports_ 结构下添加以下内容:

#[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>,
}

此结构将需要 from 钱包(我们的签名者)、来自钱包的关联代币账户(ATA)、目的钱包的 ATA 和代币程序。你不需要目的钱包的主账户,因为它将保持不变(只有其 ATA 会被修改)。现在让我们创建我们的函数。在 solana_lamport_transfer 模块中,在 transfer_lamports 指令下,添加以下内容:

pub fn transfer_spl_tokens(ctx: Context<TransferSpl>, amount: u64) -> Result<()> {
    let destination = &ctx.accounts.to_ata;
    let source = &ctx.accounts.from_ata;
    let token_program = &ctx.accounts.token_program;
    let authority = &ctx.accounts.from;

    // 从承接者转账给初始化者
    let cpi_accounts = SplTransfer {
        from: source.to_account_info().clone(),
        to: destination.to_account_info().clone(),
        authority: authority.to_account_info().clone(),
    };
    let cpi_program = token_program.to_account_info();

    token::transfer(
        CpiContext::new(cpi_program, cpi_accounts),
        amount
    )?;
    Ok(())
}

让我们逐步分析这个函数:

  1. transfer_spl_tokens 函数接受一个 Context 和一个 amount 作为参数。TransferSpl 上下文包含我们在前一步中定义的交易所需账户信息。

  2. 我们从上下文中创建对 destinationsourcetoken_programauthority 的引用。这些变量分别代表目的 ATA、源 ATA、代币程序和签名者的钱包。

  3. 使用 sourcedestinationauthority 的账户信息创建 SplTransfer 结构。当进行跨程序调用(CPI)到 SPL Token 程序时,此结构将提供账户信息。

  4. 使用 cpi_programcpi_accounts 以及要转移的 amount 调用 token::transfer 函数。此函数执行指定 ATA 之间的实际代币转移。

  5. 我们返回 Ok(()) 以表示成功执行。

继续构建你的程序,以确保一切正常工作,方法是点击“Build”或输入 anchor build

如果程序成功构建,你可以将其部署到 Solana 开发网络。

部署程序

点击页面左侧的工具图标 🛠,然后点击“Deploy”:

Deploy

这可能需要一分钟或两分钟,但完成后,你应该在浏览器终端中看到类似如下的信息:

Success

干得好!让我们测试一下。

测试程序

返回你编辑 lib.rs 文件的主文件窗口,点击页面左上角的 📑 图标。打开 anchor.test.ts 并用以下内容替换其内容:

import {
  createMint,
  createAssociatedTokenAccount,
  mintTo,
  TOKEN_PROGRAM_ID,
} from "@solana/spl-token";

describe("Test transfers", () => {});
简化调试的日志

现在你可以访问 RPC 端点的日志,帮助你更有效地排除问题。如果你遇到 RPC 调用的问题,只需在你的 QuickNode 仪表板中检查日志以快速识别和解决问题。了解更多关于日志历史限制的内容,请查看 我们的定价页面。

这段代码将导入 SPL Token 程序中创建铸币、创建关联代币账户和铸造代币到关联代币账户所需的函数。这还将为我们的程序创建一个测试套件。

测试转移 Lamports

在你的测试套件中,添加以下代码:

it("transferLamports", async () => {
    // 生成新账户的密钥对
    const newAccountKp = new web3.Keypair();
    // 发送交易
    const data = new BN(1000000);
    const tx = await pg.program.methods
      .transferLamports(data)
      .accounts({
        from: pg.wallet.publicKey,
        to: newAccountKp.publicKey,
      })
      .signers([pg.wallet.keypair])
      .transaction();
    const txHash = await web3.sendAndConfirmTransaction(pg.program.provider.connection, tx, [pg.wallet.keypair]);
    console.log(`https://explorer.solana.com/tx/${txHash}?cluster=devnet`);
    const newAccountBalance = await pg.program.provider.connection.getBalance(
      newAccountKp.publicKey
    );
    assert.strictEqual(
      newAccountBalance,
      data.toNumber(),
      "The new account should have the transferred lamports"
    );
});

在这个 transferLamports 测试中发生了什么:

  1. 我们为目的账户生成一个新的密钥对。
  2. 我们将转移的金额定义为 data,其值设为 1,000,000 lamports (0.001 SOL)(注意:Anchor 期望我们将此值作为大数字类型传递)。
  3. 我们通过调用 pg.program.methods.transferLamports(data) 执行 Solana 程序的 transferLamports 函数。该交易使用 accounts 方法指定所用账户,其中 from 账户是测试钱包的公钥,to 账户是新生成账户的公钥。Anchor 知道我们需要系统程序,因此这里不需要传递它。
  4. 交易使用测试钱包的密钥对进行签名,通过 signers 方法传递。
  5. 使用 transaction() 方法创建交易。
  6. 测试使用 sendAndConfirmTransaction() 等待交易被 确认。在我们检查新账户的余额时,确保它已更新为转移的金额是很重要的。
  7. 使用 getBalance() 获取新账户的余额,并将其存储在 newAccountBalance 变量中。
  8. 使用 assert.strictEqual 进行断言,以确认新账户的余额与转移金额匹配。只有当余额匹配预期金额时,测试才会成功。

测试转移 SPL 代币

在你的 transferLamports 测试之后,但在同一个测试套件中,添加一个测试以测试你的 SPL 代币转移:

it("transferSplTokens", async () => {
    // 为新账户生成密钥对
    const fromKp = pg.wallet.keypair;
    const toKp = new web3.Keypair();
    // 创建新的铸币并初始化它
    const mint = await createMint(
      pg.program.provider.connection,
      pg.wallet.keypair,
      fromKp.publicKey,
      null,
      0
    );
    // 为新账户创建关联代币账户
    const fromAta = await createAssociatedTokenAccount(
      pg.program.provider.connection,
      pg.wallet.keypair,
      mint,
      fromKp.publicKey
    );
    const toAta = await createAssociatedTokenAccount(
      pg.program.provider.connection,
      pg.wallet.keypair,
      mint,
      toKp.publicKey
    );
    // 铸造代币到 'from' 关联代币账户
    const mintAmount = 1000;
    await mintTo(
      pg.program.provider.connection,
      pg.wallet.keypair,
      mint,
      fromAta,
      pg.wallet.keypair.publicKey,
      mintAmount
    );
    // 发送交易
    const transferAmount = new BN(500);
    const tx = await pg.program.methods
      .transferSplTokens(transferAmount)
      .accounts({
        from: fromKp.publicKey,
        fromAta: fromAta,
        toAta: toAta,
        tokenProgram: TOKEN_PROGRAM_ID,
      })
      .signers([pg.wallet.keypair, fromKp])
      .transaction();
    const txHash = await web3.sendAndConfirmTransaction(pg.program.provider.connection, tx, [pg.wallet.keypair, fromKp]);
    console.log(`https://explorer.solana.com/tx/${txHash}?cluster=devnet`);
    const toTokenAccount = await pg.connection.getTokenAccountBalance(toAta);
    assert.strictEqual(
      toTokenAccount.value.uiAmount,
      transferAmount.toNumber(),
      "The 'to' token account should have the transferred tokens"
    );
});

以下是 transferSplTokens 测试中发生的事情:

  1. 我们为目标账户生成一个新的密钥对。
  2. 我们创建一个新的铸币并初始化它。
  3. 我们为源钱包和目标钱包创建与新代币铸币关联的关联代币账户。
  4. 我们向源(来自钱包)的关联代币账户铸造 1,000 个代币。
  5. 我们执行在程序中创建的 transferSplTokens 指令。此次交易使用的账户通过 accounts 方法指定,其中 from 帐户是测试钱包的公钥,fromAta 帐户是源关联代币账户,toAta 帐户是目标关联代币账户,而 tokenProgram 是 SPL Token 程序 ID。
  6. 使用 transaction() 方法创建交易。
  7. 测试通过 await sendAndConfirmTransaction() 等待交易被 最终确定。这对于确保当我们检查新账户的余额时,已更新为转移的金额非常重要。
  8. 使用 getTokenAccountBalance() 获取新代币账户的余额,并将其存储在 toTokenAccount 变量中。
  9. 使用 assert.strictEqual 进行断言以确认新账户的余额与转移金额匹配。仅当余额与预期金额匹配时,测试才会成功。

干得不错——让我们测试一下!按下屏幕左侧的 🧪 测试 按钮运行测试。你应该会看到两个测试都通过,如下所示:

Running tests...  anchor.test.ts:  Test    https://explorer.solana.com/tx/5DNZm9oCtzMFSqUte6bt9tW5iW95AS77hSz8uqjZjD41rkJpCDLHRti6X7iRDrCfHRRGpMeAAePrVcKW4Qg3C9GB?cluster=devnet    ✔ transferLamports (13409ms)    https://explorer.solana.com/tx/3KCDqrbUonDBQSZfN8jFYZH8vo4fWgLfa3nCSWfCmeNgva3yVCeiy7QfiH9Az8U8LQpS1VKMELCH2wPDL4BcgKD6?cluster=devnet    ✔ transferSplTokens (15975ms)  2 passing (29s)

就这样!干得好。

总结

你已经使用自己的 Solana 程序实现了本地 SOL 转账和 SPL 代币转账。这是构建自己的 NFT 项目、游戏或 DeFi 应用程序的一个很好的开始。继续构建吧!

如果你卡住了,有问题,或者想聊聊你正在构建的内容,欢迎通过 DiscordTwitter 联系我们!

我们 ❤️ 反馈!

如果你对本指南有任何反馈, 告诉我们 。我们很乐意听取你的意见。

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

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

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。