跟我一起从0开始学习Solana合约开发,一起实操,一起做项目。这是一个系列文章,系统地记录了我的学习笔记。
在之前的文章中,我们探讨了 Solana 账户的基础理论并进行了实操。然而,为了进一步加深理解,我们需要结合实际案例来应用这些知识。今天,我们将用两种方法实现账户之间的 SOL 转账。相信通过这个真实场景的学习,你一定能够更加透彻地掌握 Solana 的账户模型和转账机制。
在开始之前,请先思考一个问题:我写的程序可以直接从我的钱包里转出 SOL 吗? 带着这个问题,让我们开始今天的学习。
回顾一下 Solana 的账户模型:每个账户都有一个 owner 字段,标明该账户受哪个程序管理。只有账户的 owner 程序可以对账户执行操作,比如减少余额或修改数据。
这意味着,如果我们希望从自己的钱包里转出 SOL,只有系统程序(SystemProgram)能够直接操作我们的钱包账户。我们自己编写的程序无法直接控制钱包余额。
那么问题来了:如果我们想编写一个程序来转账 SOL,该如何实现呢? 有两种方法:一种是创建受程序管理的账户
,另一种是使用 CPI(跨程序调用)
。
我们可以创建一个新账户,并将其 owner 设置为我们自己编写的程序。通过这种方式,我们的程序就可以完全控制该账户,执行各种操作,实现转账等功能。
创建一个新账户,命名为 programOwnedAccount。
设置账户的 owner 为程序,将账户的权限交给我们自定义的程序。
向新账户存入 SOL,将其作为一个“中转账户”。
实现转账逻辑,从 programOwnedAccount 转出 SOL 到接收者的钱包。
这种方式有效避免了程序直接操作用户钱包的限制,同时保证账户的安全和程序的可控性。
理清了这个过程,接下来我们就可以写代码了。
打开 tests/{your-program}.ts
文件,我们将通过调用 Solana 的 SystemProgram.createAccount
方法来实现账户的创建。以下是具体代码:
it('Create and fund account owned by our program', async () => {
const instruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey, // 我的钱包地址
newAccountPubkey: programOwnedAccount.publicKey, // 新账户地址
space: 0, // 新账户的存储空间 (0 表示不存储额外数据)
lamports: 1 * LAMPORTS_PER_SOL, // 存入新账户的初始余额 (1 SOL)
programId: program.programId, // 设置 owner 为自定义程序的 ID
});
const transaction = new Transaction().add(instruction);
// 发送交易并确认结果
await sendAndConfirmTransaction(provider.connection, transaction, [payer.payer, programOwnedAccount]);
});
通过这段代码,我们创建了一个受程序管理的账户,并存入了 1 SOL
用作转账。需要注意的是在最后发送交易时,参数 [payer.payer, programOwnedAccount]
指明了签名账户,用户的钱包签名表示授权转账,programOwnedAccount 签名则激活新账户。
完成账户创建后,我们可以实现从 programOwnedAccount 到接收者钱包的转账逻辑。
打开 src/lib.rs
文件,编写以下代码:
注意:以下代码示例默认你已经看过了之前的文章,具备了账户的实操能力。如果你感到不理解下面的代码,请回过头阅读我之前的文章。
#[derive(Accounts)]
pub struct TransferSolWithProgram<'info> {
#[account(mut, owner = id())]
payer: UncheckedAccount<'info>, // 支付账户
#[account(mut)]
recipient: SystemAccount<'info>, // 接收账户
}
在账户上下文中,我们可以看到 payer
和 recipient
两个字段的定义。分别使用了 UncheckedAccount
和 SystemAccount
两种类型,分别对应不同的账户类型和用途。
payer 字段
#[account(mut, owner = id())]
的含义如下:
mut
:表示这个账户的余额会被修改。
owner = id()
:确保 payer
是由当前程序(通过 id()
获取的程序 ID)管理的账户,即一个受程序控制的账户。
UncheckedAccount
:表明这个账户不需要额外的数据验证。因为 payer
是我们之前创建并初始化的中转账户 (programOwnedAccount
),已经明确由程序控制,所以无需额外检查。
需要注意的是,这里没有使用 init
,是因为 payer
是一个已存在的账户,并且在创建时已完成了初始化。因此,不需要重复初始化。
recipient 字段
#[account(mut)]
的含义如下:
mut
:表示该账户的余额会被修改,即接收 SOL。
SystemAccount
:标明这个账户的所有者是系统程序(SystemProgram),它是普通用户钱包的默认类型。
#[program]
pub mod transfer_sol {
use super::*;
pub fn transfer_sol_with_program(ctx: Context<TransferSolWithProgram>, amount: u64) -> Result<()> {
**ctx.accounts.payer.try_borrow_mut_lamports()? -= amount;
**ctx.accounts.recipient.try_borrow_mut_lamports()? += amount;
Ok(())
}
}
代码解析
try_borrow_mut_lamports
:获取账户余额的可变引用,允许我们直接操作账户的余额。
**
运算符:解引用以访问账户的 lamports 字段,从而操作账户余额。错误通过 ?
运算符向上传递,要求调用者处理错误。
我们通过以下测试代码验证程序的正确性:
it('Transfer SOL with Program', async () => {
// 打印转账前的余额
await program.methods
.transferSolWithProgram(new anchor.BN(transferAmount))
.accounts({
payer: programOwnedAccount.publicKey,
recipient: payer.publicKey,
})
.rpc();
// 打印转账后的余额
});
Solana 提供了一个强大的机制,称为 CPI(Cross-Program Invocation,跨程序调用)。通过 CPI,我们的程序可以调用其他程序的功能,包括系统程序。利用这一机制,我们可以在自己的程序中调用系统程序,间接操作钱包余额,实现更复杂的功能。
编写转账程序,通过 CPI 调用系统程序的 transfer 方法。
执行转账逻辑,从钱包直接转出 SOL 到接收者的钱包。
在上面的 src/lib.rs
文件里,我们定义一个函数,调用系统程序的 transfer 方法来实现转账功能。代码如下:
pub fn transfer_sol_with_cpi(ctx: Context<TransferSolWithCpi>, amount: u64) -> Result<()> {
system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
},
),
amount,
)?;
Ok(())
}
在以上代码中,我们使用 system_program::transfer
通过 CPI 机制从 payer
账户向 recipient
账户转账。CpiContext::new
用于构造一个新的上下文,它包含了系统程序的账户信息和两个账户的地址(payer 和 recipient)。
另外,在代码中使用系统程序时,需要引入它。你可以通过以下方式来导入 system_program
:
use anchor_lang::system_program;
为了让程序能够与系统程序交互,我们需要在账户上下文中添加一个字段来引用 system_program。这是因为我们需要告诉 Solana 哪个程序提供了 transfer 方法。以下是账户上下文
代码:
#[derive(Accounts)]
pub struct TransferSolWithCpi<'info> {
#[account(mut)]
payer: Signer<'info>,
#[account(mut)]
recipient: SystemAccount<'info>,
system_program: Program<'info, System>,
}
为了验证转账功能,我们需要编写相应的测试代码。以下是一个简单的测试示例,展示如何在程序中调用 transfer_sol_with_cpi
方法进行转账:
it('Transfer SOL with CPI', async () => {
// 打印转账前的信息: payer余额、recipient
await program.methods
.transferSolWithCpi(new anchor.BN(transferAmount))
.accounts({
payer: payer.publicKey,
recipient: recipient.publicKey,
})
.rpc();
// 打印转账后的信息: payer余额、recipient
});
第一种方法:创建受程序管理的账户灵活性高,程序可以完全控制 programOwnedAccount,自由设计逻辑。这个在合约自动化转账领域非常有用。缺点是中转账户需要额外的存储费用。
第二种方法:使用 CPI简洁、高效,不需要额外创建中转账户,直接操作钱包账户,节省存储费用。缺点是程序必须经过审慎设计,避免误操作或安全漏洞。
特性 | 创建受程序管理的账户 | 使用 CPI |
---|---|---|
灵活性 | 程序完全控制账户 | 程序受系统程序的限制 |
存储成本 | 需要支付新账户的存储费用 | 无额外存储费用 |
开发复杂度 | 需要管理多个账户 | 较为简单,直接调用系统程序 |
安全性 | 程序外部无法直接操作中转账户 | 钱包账户操作依赖程序逻辑,存在潜在风险 |
现在回答本文开头的问题:“程序可以直接操作我的钱包转出 SOL 吗?” 答案是:通过 CPI 调用系统程序,可以间接实现操作钱包的功能。下一篇文章我将详细介绍 IDL
。如果你想提前看到我的更新,可以扫描下方二维码关注我的公众号:认知那些事
。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!