8. 创建 PDA 数据账户

创建 PDA 数据账户

在上一章我们处理了入口参数,现在我们要编写核心逻辑:在链上为用户开辟一块存储空间。

这涉及到 Solana 开发中最强大的模式:CPI (Cross-Program Invocation) 配合 PDA 签名


1. 确定需要的账户

为了创建一个账户,我们需要与 Solana 的底层操作系统——System Program 进行交互。 根据 create_account 指令的要求,我们需要以下 4 个账户:

索引账户签名可写说明
0User Wallet✅ Yes✅ YesPayer。负责支付创建账户所需的租金 (SOL)。
1Data Account❌ No✅ YesPDA。这是我们要创建的新账户。
2System Program❌ No❌ NoFactory。系统程序,提供创建账户的逻辑。
3Sysvar Rent❌ No❌ NoOracle。提供当前的租金费率查询。

获取账户信息 (Rust)

使用迭代器按顺序提取各个账户:

rust
let accounts_iter = &mut accounts.iter(); // 1. 付款人 (必须签名) let account_user = next_account_info(accounts_iter)?; if !account_user.is_signer { return Err(ProgramError::MissingRequiredSignature); } // 2. 数据账户 (PDA) let account_data = next_account_info(accounts_iter)?; // 3. 系统程序 let _system_program = next_account_info(accounts_iter)?; // 4. 租金变量 (用于计算豁免金额) // 注:较新版本的 Solana SDK 可以直接使用 Rent::get() 而不需要传入 Sysvar 账户, // 但为了兼容性,很多教程仍保留此账户。

2. 计算租赁豁免 (Rent Exemption)

Solana 账户需要维持一定余额以避免被回收(垃圾回收机制)。我们必须计算出存储数据所需的最低 SOL。

rust
// 计算我们要存的数据长度 let space = data.len(); // 查询租金豁免门槛 (Lamports) let rent_exemption = Rent::get()?.minimum_balance(space);

3. 派生 PDA 地址

我们需要验证客户端传入的 account_data 地址是否正确。 我们使用 find_program_address 重新计算一遍 PDA。

rust
let (pda_address, bump_seed) = Pubkey::find_program_address( &[account_user.key.as_ref()], // 种子: 用户的公钥 program_id // 种子: 本程序 ID ); if pda_address != *account_data.key { return Err(ProgramError::InvalidSeeds); }

注意:这里我们获取了 bump_seed,这在后面签名时至关重要。


4. 判断账户是否已存在

Solana SDK 没有直接提供 exists() 方法。我们利用一个事实:所有存在的账户必须有租金豁免余额。如果余额为 0,说明账户未初始化。

rust
if **account_data.try_borrow_lamports()? == 0 { // 账户不存在,执行创建逻辑... }

5. 创建账户 (CPI + Invoke Signed)

这是最关键的一步。我们需要调用 System Program 来创建账户。 但是,创建账户通常需要新账户的私钥签名。PDA 没有私钥,怎么办?

我们使用 invoke_signed。 它允许当前程序告诉 Runtime:“我是这个 PDA 的所有者,我用我的程序 ID 和这个 Bump Seed 为它担保。”

rust
// 1. 构建指令 let create_ix = solana_program::system_instruction::create_account( account_user.key, // 付款方 account_data.key, // 新账户地址 rent_exemption, // 初始余额 data.len() as u64, // 空间大小 program_id, // 新账户的所有者 (设置为当前程序) ); // 2. 带着签名调用 (CPI) invoke_signed( &create_ix, accounts, // 传递所有涉及的账户 &[&[ // 签名种子 (Signer Seeds) account_user.key.as_ref(), // Seed 1 &[bump_seed] // Seed 2 (Bump) ]], )?;

6. 写入数据

账户创建成功后,它现在属于我们的程序了。我们可以直接修改它的数据区。

rust
// 获取可变引用 let mut account_data_content = account_data.data.borrow_mut(); // 将输入数据写入账户内存 // 注意:这里假设输入数据长度与申请的空间完全一致 account_data_content.copy_from_slice(data);

完整代码预览

rust
pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8], ) -> ProgramResult { // ... 账户提取 ... // 计算租金 let rent_exemption = Rent::get()?.minimum_balance(data.len()); // 派生 PDA let (pda, bump_seed) = Pubkey::find_program_address( &[account_user.key.as_ref()], program_id ); // 只有当账户为空时才创建 if **account_data.try_borrow_lamports()? == 0 { // CPI 调用 invoke_signed( &system_instruction::create_account( account_user.key, account_data.key, rent_exemption, data.len() as u64, program_id, ), accounts, &[&[account_user.key.as_ref(), &[bump_seed]]], )?; // 写入数据 account_data.data.borrow_mut().copy_from_slice(data); } Ok(()) }
RUSTPlayground
EDITOR ACTIVE
Initializing RUST Environment...

PDA 铸造工厂

Input Context
User123...
5 bytes
--- lamports
---
On-Chain State
Empty Slot