动态数据更新与租金管理
问题场景
在上一章我们成功创建了账户并写入了初始数据。但如果用户想要修改数据怎么办? 新数据可能比旧数据更长(例如从 "Hello" 变成 "Hello World"),也可能更短。这带来了两个必须解决的问题:
- 空间问题:账户的
data数组长度是固定的。如果新数据更长,写入会越界报错。 - 租金问题:
- 数据变长:不仅空间不够,原有的租金余额也不足以维持新的租赁豁免门槛(Rent Exempt Minimum)。
- 数据变短:空间浪费,且用户多付了租金,应该退还。
我们需要在更新数据前,动态调整账户大小和余额。
1. 重新分配空间 (Realloc)
Solana 在较新版本中引入了 realloc 方法,允许程序动态修改所拥有账户的大小。
rust// 调整账户数据长度 // 参数2 (false): 是否将新增的字节初始化为 0。 // 因为我们马上会覆盖写入新数据,所以这里填 false 以节省计算资源。 account_data.realloc(new_data.len(), false)?;
注意:单个交易中
realloc增加的大小有限制(通常单次不超过 10KB),且总大小不能超过 10MB。
2. 租金补足 (Top-up)
场景:数据变长。 逻辑:账户现在的余额 < 新长度所需的最低租金。我们需要让用户补差价。
因为我们要从用户钱包扣钱,这属于转移用户资产,所以必须通过 System Program 并要求用户签名。
rustlet rent_exemption = Rent::get()?.minimum_balance(new_data.len()); if rent_exemption > account_data.lamports() { let diff = rent_exemption - account_data.lamports(); // 调用系统程序转账 (CPI) invoke( &system_instruction::transfer( account_user.key, // 从用户 account_data.key, // 到 PDA diff, ), accounts, )?; }
思考:为什么这里用 invoke 而不是 invoke_signed?
因为付款方是用户钱包,用户已经对这笔交易进行了签名授权,所以程序只需要转发指令即可。
3. 租金退款 (Refund)
场景:数据变短。 逻辑:账户现在的余额 > 新长度所需的最低租金。多余的钱应该退给用户。
因为我们要从 PDA 账户 扣钱,而该 PDA 的所有者正是当前程序。程序有权直接修改自己拥有的账户余额,不需要调用 System Program。
rustif rent_exemption < account_data.lamports() { let diff = account_data.lamports() - rent_exemption; // 直接修改 Lamports (这是允许的,因为 PDA 归本程序所有) **account_data.try_borrow_mut_lamports()? -= diff; **account_user.try_borrow_mut_lamports()? += diff; }
关键区别:
- 用户 -> PDA:必须走 System Program Transfer (需要用户签名)。
- PDA -> 用户:可以直接修改 Lamports (程序是 PDA 的主人)。
4. 写入新数据
处理完空间和租金后,就可以安全地写入数据了:
rustaccount_data.data.borrow_mut().copy_from_slice(&new_data);
这一套逻辑(Realloc + Rent Logic)是 Solana 原生开发中的标准范式,虽然写起来略显繁琐,但它精确控制了每一分钱的流向。