概述
Anchor 是一个加速在 Solana 区块链上开发安全程序的框架。在使用 Solana 和 Anchor 时,你可能会遇到需要在账户之间发送 SOL 或 SPL 代币的情况(例如,处理用户支付到你的国库,或让用户将他们的 NFT 发送到托管账户)。本指南将引导你通过使用 Anchor 转移 SOL 和 SPL 代币的过程。我们将涵盖程序和测试所需的代码,以确保账户之间的代币无缝转移。
你将做什么
在本指南中,你将:
- 使用 Anchor 和 Solana Playground 创建一个 Solana 程序
- 创建一个程序指令,将 SOL 在两个用户之间发送
- 创建一个程序指令,将 SPL 代币在两个用户之间发送
- 编写测试以验证代币转移
你将需要什么
- 具有使用 Anchor 开发的基本经验( 指南:使用 Anchor 入门 )
- 具有 Solana 转账的经验( 指南:如何使用 JavaScript 发送 SOL)和 SPL 代币转账( 指南:如何转移 SPL 代币 )
- 对 JavaScript/TypeScript 和 Rust 编程语言的基本知识
- 一款现代网页浏览器(例如, Google Chrome)
本指南中使用的依赖
依赖项 | 版本 |
---|---|
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。你的钱包现在应已连接到开发网络,余额为 2 SOL:
https://explorer.solana.com/address/DGbfDHnXsQSyswLjkEkadxjiJmK3SUfrPueokNHYXEpi?cluster=devnet
你准备好了!让我们开始构建!
创建转账程序
首先打开 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(())
}
}
以下是该代码片段不同部分的简要解释:
#[program]
属性将模块标记为 Anchor 程序。它生成所需的模板以定义程序的入口点,并自动处理账户验证和反序列化。- 在
solana_lamport_transfer
模块内部,使用use super::*;
导入父模块所需的项。 transfer_lamports
函数接受一个 Context 和一个amount
作为其参数。Context 包含交易所需的账户信息,amount
是要转移的 lamports 数量。- 我们创建对上下文中的
from_account
和to_account
的引用,这些引用将用于转账。 system_instruction::transfer
函数创建一个转账指令,该指令接受from_account
的公钥、to_account
的公钥以及要转移的amount
作为参数。anchor_lang::solana_program::program::invoke_signed
函数调用转账指令,并使用交易的签名者(from_account
)。它接受转账指令、from_account
、to_account
和system_program
的账户信息数组,以及一个空数组作为签名者。transfer_lamports
函数返回Ok(())
以表示执行成功。
你可以通过点击 Build 按钮或在终端中输入 anchor build 来确保一切正常。如果出现错误,请检查你的代码与本指南中的代码并遵循错误响应的建议。
创建转移 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: Associated Token Account)、目的钱包的 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(())
}
让我们逐步分析这个函数:
transfer_spl_tokens
函数接受一个 Context 和一个amount
作为参数。TransferSpl 上下文包含我们在前一步中定义的交易所需账户信息。- 我们从上下文中创建对
destination
、source
、token_program
和authority
的引用。这些变量分别代表目的 ATA、源 ATA、代币程序和签名者的钱包。 - 使用
source
、destination
和authority
的账户信息创建 SplTransfer 结构。当进行跨程序调用(CPI)到 SPL Token 程序时,此结构将提供账户信息。 - 使用
cpi_program
和cpi_accounts
以及要转移的amount
调用token::transfer
函数。此函数执行指定 ATA 之间的实际代币转移。 - 我们返回
Ok(())
以表示成功执行。
继续构建你的程序,以确保一切正常工作,方法是点击“Build”或输入 anchor build
。
如果程序成功构建,你可以将其部署到 Solana 开发网络。
部署程序
点击页面左侧的工具图标,然后点击“Deploy”:
当前测试时至少需要5 SOL, 如果不足,使用https://faucet.solana.com/ 进行领水
这可能需要一分钟或两分钟,但完成后,你应该在浏览器终端中看到类似如下的信息:
Program ID:4d39PLXUXdtmWN571CUETGpvnXgf7iAQ35WYSX5J5kGX
干得好!让我们测试一下。
测试程序
返回你编辑 lib.rs 文件的主文件窗口,点击页面左上角的图标。打开 anchor.test.ts 并用以下内容替换其内容:
import {
createMint,
createAssociatedTokenAccount,
mintTo,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
describe("Test transfers", () => {});
测试转移 Lamports
在你的测试套件中,添加以下代码:
describe("Test transfers", () => {
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"
);
});
});
https://explorer.solana.com/tx/126HvQaMXvPYoJQbwZLk68rmPf98uMvmV7ucqd6KtypD3xNqUYh5XjD6VVsVsXHsysKvgFQrTC1ER1SGND1LHaQ4?cluster=devnet
在这个 transferLamports
测试中发生了什么:
- 我们为目的账户生成一个新的密钥对。
- 我们将转移的金额定义为
data
,其值设为 1,000,000 lamports (0.001 SOL)(注意:Anchor 期望我们将此值作为大数字类型传递)。 - 我们通过调用
pg.program.methods.transferLamports(data)
执行 Solana 程序的transferLamports
函数。该交易使用accounts
方法指定所用账户,其中from
账户是测试钱包的公钥,to
账户是新生成账户的公钥。Anchor 知道我们需要系统程序,因此这里不需要传递它。 - 交易使用测试钱包的密钥对进行签名,通过
signers
方法传递。 - 使用
transaction()
方法创建交易。 - 测试使用
sendAndConfirmTransaction()
等待交易被 确认。在我们检查新账户的余额时,确保它已更新为转移的金额是很重要的。 - 使用
getBalance()
获取新账户的余额,并将其存储在newAccountBalance
变量中。 - 使用
assert.strictEqual
进行断言,以确认新账户的余额与转移金额匹配。只有当余额匹配预期金额时,测试才会成功。
测试转移 SPL 代币
在你的 transferLamports 测试之后,但在同一个测试套件中,添加一个测试以测试你的 SPL 代币转移:
describe("Test transfers", () => {
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,000 个代币。
- 我们执行在程序中创建的
transferSplTokens
指令。此次交易使用的账户通过 accounts 方法指定,其中from
帐户是测试钱包的公钥,fromAta
帐户是源关联代币账户,toAta
帐户是目标关联代币账户,而tokenProgram
是 SPL Token 程序 ID。 - 使用
transaction()
方法创建交易。 - 测试通过 await
sendAndConfirmTransaction()
等待交易被 最终确定。这对于确保当我们检查新账户的余额时,已更新为转移的金额非常重要。 - 使用
getTokenAccountBalance()
获取新代币账户的余额,并将其存储在toTokenAccount
变量中。 - 使用
assert.strictEqual
进行断言以确认新账户的余额与转移金额匹配。仅当余额与预期金额匹配时,测试才会成功。
干得不错——让我们测试一下!按下屏幕左侧的 测试 按钮运行测试,如下所示:
https://explorer.solana.com/tx/2eAQANiLddDh3fxyqkc28LmVNpWQHNeQUi3waxnu3qoNev9h1juX5npkbprFen3u8Xm8JzpdrstMbBevxcUWcEuB?cluster=devnet
总结
你已经使用自己的 Solana 程序实现了本地 SOL 转账和 SPL 代币转账。这是构建自己的 NFT 项目、游戏或 DeFi 应用程序的一个很好的开始。继续构建吧!
完整代码
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer as SplTransfer};
use solana_program::system_instruction;
declare_id!("4d39PLXUXdtmWN571CUETGpvnXgf7iAQ35WYSX5J5kGX");
#[derive(Accounts)]
pub struct TransferLamports<'info> {
#[account(mut)]
pub from: Signer<'info>,
#[account(mut)]
pub to: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[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>,
}
#[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(())
}
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(())
}
}
原文:https://www.quicknode.com/guides/solana-development/anchor/transfer-tokens