本文详细介绍了如何在Solana平台上创建一个由系统程序(System Program)拥有的程序派生地址(PDA),并展示了如何向PDA转账以及从PDA取款的实现过程。文章提供了对相关概念的深入解读,并通过示例代码和测试用例,指导读者掌握该操作的步骤和注意事项。
程序派生账户(Program-derived Accounts)是 Solana 中的一个独特工具,它允许程序拥有并对这些账户上的状态更改进行签名。这种所有权模型对各种应用程序至关重要,从去中心化金融(DeFi)到非同质化代币(NFT),因为它允许更复杂和安全的资产与数据处理。通常,PDAs 由该程序创建和拥有(例如,Token程序创建一个Token账户)。然而,有一些情况使得由系统程序拥有 PDA 变得有用。特别是,如果你正在为新账户支付租金豁免或与其他程序交互,期望系统程序账户参与交易(例如,向系统账户转移 SOL 的 CPI)。请查看该 Marinande Claim 函数 中的示例:
#[account(\
mut,\
address = ticket_account.beneficiary @ MarinadeError::WrongBeneficiary\
)]
pub transfer_sol_to: SystemAccount<'info>,
在上述账户上下文中,Anchor 约束 SystemAccount 将限制交互仅限于由系统程序拥有的账户。本指南将向你展示如何在你的 Anchor 项目中创建一个由系统程序拥有的 PDA。
编写一个 Anchor 程序和测试:
依赖项 | 版本 |
---|---|
anchor-lang | 0.29.0 |
anchor-spl | 0.29.0 |
solana-program | 1.16.24 |
spl-token | 4.0.0 |
solana-cli | 1.17.14 |
通过访问 https://beta.solpg.io/ 创建一个新的 Solana Playground 项目。Solana Playground 是一个基于浏览器的 Solana 代码编辑器,使我们能够快速启动并完成此项目。你可以在自己的代码编辑器中跟随,但本指南将针对 Solana Playground 的必要步骤进行调整。首先,点击“创建新项目”:
输入项目名称“system-pda”,并选择“Anchor (Rust)”:
由于这个项目仅用于演示目的,我们可以使用一个“临时”钱包。Solana Playground 使它变得简单。你应该在浏览器窗口左下角看到一个红点“未连接”。点击它:
Solana Playground 将为你生成一个钱包(或者你可以导入自己的钱包)。请随意保存以备后用,并在准备好后点击继续。一个新的钱包将被初始化并连接到 Solana devnet。
你准备好了!让我们开始构建吧!
现在,让我们打开 lib.rs
并删除起始代码。一旦你拥有一个空白的页面,我们可以开始构建我们的程序。首先,让我们导入一些依赖并框定我们的程序。将以下内容添加到文件顶部:
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};
declare_id!("11111111111111111111111111111111");
#[program]
mod sys_pda_example {
use super::*;
pub fn transfer_to_pda(ctx: Context<Example>, fund_lamports: u64) -> Result<()> {
// 在这里添加代码
Ok(())
}
pub fn transfer_from_pda(ctx: Context<Example>, return_lamports: u64) -> Result<()> {
// 在这里添加代码
Ok(())
}
}
#[derive(Accounts)]
pub struct Example<'info> {
#[account(\
mut,\
seeds = [b"vault".as_ref()],\
bump\
)]
pub pda: SystemAccount<'info>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
我们在这里做的是框定我们的程序。我们从 Anchor 的预导入和系统程序中导入。我们还有一个 declare_id!
宏,它将定义我们程序构建后的程序 ID。我们在 sys_pda_example
程序中定义了两个指令:transfer_to_pda
和 transfer_from_pda
。它们将用于“初始化” PDA 和从 PDA 转移资金。两个指令都将使用相同的 Example 账户上下文。Example 上下文被定义为包含三种账户:pda
(一个 SystemAccount)、signer
和 system_program
。pda
账户是我们将要创建的 PDA,signer
账户是将签署交易并支付交易费用的账户,而 system_program
账户是将拥有 PDA 并允许我们转移 SOL 的账户。
你可能已经注意到我们在整个指南中提到系统 PDA 的初始化是用引号标记的。我们还将第一个函数命名为 transfer_to_pda
而非 initialize_pda
。这是因为我们实际上并没有初始化 PDA,而只是将资金转移到它。所有账户默认为系统账户,且通过将 SOL 转移到我们从种子派生的账户,我们实际上是在“初始化”它。我们只需要确保转移足够的资金来覆盖 0 字节账户的租金。
将你的 transfer_to_pda
函数更新为通过添加以下代码将资金转移到我们的 PDA:
pub fn transfer_to_pda(ctx: Context<Example>, fund_lamports: u64) -> Result<()> {
let pda = &mut ctx.accounts.pda;
let signer = &mut ctx.accounts.signer;
let system_program = &ctx.accounts.system_program;
let pda_balance_before = pda.get_lamports();
transfer(
CpiContext::new(
system_program.to_account_info(),
Transfer {
from: signer.to_account_info(),
to: pda.to_account_info(),
},
),
fund_lamports,
)?;
let pda_balance_after = pda.get_lamports();
require_eq!(pda_balance_after, pda_balance_before + fund_lamports);
Ok(())
}
这个函数实际上做了两件事情:
signer
账户到 pda
账户转移资金。注意:正如我们之前讨论的,这并不是真正的初始化函数。这个函数可以在任何时候被调用并向 PDA 转移 SOL。
既然我们有了在 SystemAccount PDA 中的 SOL,我们需要知道如何转移这些资金。由于它是一个 PDA,我们将不得不使用带有签名种子的 CPI。现在让我们实现这个功能。
更新你的 transfer_from_pda
函数,通过添加以下代码从我们的 PDA 转移资金:
pub fn transfer_from_pda(ctx: Context<Example>, return_lamports: u64) -> Result<()> {
let pda = &mut ctx.accounts.pda;
let signer = &mut ctx.accounts.signer;
let system_program = &ctx.accounts.system_program;
let pda_balance_before = pda.get_lamports();
let bump = &[ctx.bumps.pda];
let seeds: &[&[u8]] = &[b"vault".as_ref(), bump];
let signer_seeds = &[&seeds[..]];
transfer(
CpiContext::new(
system_program.to_account_info(),
Transfer {
from: pda.to_account_info(),
to: signer.to_account_info(),
},
).with_signer(signer_seeds),
return_lamports,
)?;
let pda_balance_after = pda.get_lamports();
require_eq!(pda_balance_after, pda_balance_before - return_lamports);
Ok(())
}
这个指令与我们之前的几乎完全相同,但有一个重要的不同之处。我们现在必须向 CpiContext
提供一个 signer_seeds
数组来签署交易。这个数组是一个用于派生 PDA 地址的种子列表。在本例中,我们使用与之前相同的种子“vault”以及在创建 PDA 时生成的 bump。
在编写测试之前,请先构建你的程序以检查错误。你可以通过点击 Solana Playground 窗口左上角的“构建”按钮或者在 playground 终端输入 build
来完成此操作。如果你有任何错误,你将在下面的控制台中看到它们。控制台应该提供一些明确的指导,以更正任何问题或错误。修复后,让我们编写我们的测试。
随时可以尝试编写自己的测试。由于这不是本指南的重点,我们已经为你编写了测试。用以下代码替换 anchor.test.ts
文件中的所有现有代码:
describe("测试", () => {
const [pda] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("vault")],
pg.PROGRAM_ID
);
it("向 PDA 转移资金", async () => {
const lamports = web3.LAMPORTS_PER_SOL;
const data = new BN(lamports);
const initialBalance = await pg.connection.getBalance(pda);
try {
const tx = await pg.program.methods
.transferToPda(data)
.accounts({
pda,
signer: pg.wallet.publicKey,
})
.transaction();
const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair]);
const balance = await pg.connection.getBalance(pda);
assert.strictEqual(
balance,
initialBalance + lamports,
"余额不正确"
);
} catch (error) {
assert.fail(`交易错误:${error}`);
}
});
it("从 PDA 转移资金", async () => {
const lamports = 0.5 * web3.LAMPORTS_PER_SOL;
const data = new BN(lamports);
const initialBalance = await pg.connection.getBalance(pda);
try {
const tx = await pg.program.methods
.transferFromPda(data)
.accounts({
pda,
signer: pg.wallet.publicKey,
})
.transaction();
const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair]);
const balance = await pg.connection.getBalance(pda);
assert.strictEqual(
balance,
initialBalance - lamports,
"余额不正确"
);
} catch (error) {
assert.fail(`交易错误:${error}`);
}
});
});
这个文件做了三件事:
我们的测试依赖于对 pda
账户的前后余额检查,以确保资金按预期被贷记或扣除。
打开一个新的终端窗口,然后运行以下命令以启动本地验证器:
solana-test-validator
验证器运行后,你必须确保 Solana Playground 连接到本地主机。你可以通过点击浏览器窗口左下角的“⚙️”并在端点下拉菜单中选择“Localhost”来做到这一点。你的浏览器现在应已连接到本地验证器。
通过在 playground 终端中输入以下命令为你的钱包分发一些本地 SOL:
solana airdrop 100
确认后,请在你的 playground 终端中输入 deploy
或点击工具“🛠️”菜单中的“部署”按钮。一分钟左右,你的程序应已部署到集群中。
最后,通过在 playground 终端中输入 test
或点击主浏览器屏幕中的“测试”按钮来运行你的测试。你应该看到两个成功的测试:
正在运行测试...
anchor.test.ts:
测试
✔ 向 PDA 转移资金 (552ms)
✔ 从 PDA 转移资金 (546ms)
2 个通过 (1s)
随时浏览 Solana Explorer 中的交易(确保选择自定义/本地集群)以查看已执行的交易并浏览新创建的 PDA。
恭喜你!你已成功创建一个由系统程序拥有的 PDA,并向其转移了资金,也从中提取了资金。
你现在拥有一个能够管理由系统程序拥有的 PDA 的工作程序。这可以帮助你在通过 CPI 与其他程序交互或从你的程序为新账户支付租金豁免时使用。
如果你遇到问题、想问问题或者只是想讨论你正在构建的内容,请通过 Discord 或 Twitter 联系我们!
告诉我们 如果你有任何反馈或希望讨论的新主题。我们很乐意听取你的想法。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!