在我们之前的教程中,我们讨论了如何初始化账户以便能够将数据持久化到存储中。本教程将展示如何向我们已经初始化的账户中写入数据。
以下是上一篇关于初始化 Solana 账户教程中的代码。我们添加了一个 set() 函数,用于在 MyStorage 和关联的 Set 结构体中存储一个数字。
其余代码保持不变:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");
#[program]
pub mod basic_storage {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
// ****************************
// *** THIS FUNCTION IS NEW ***
// ****************************
pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
ctx.accounts.my_storage.x = new_x;
Ok(())
}
}
// **************************
// *** THIS STRUCT IS NEW ***
// **************************
#[derive(Accounts)]
pub struct Set<'info> {
#[account(mut, seeds = [], bump)]
pub my_storage: Account<'info, MyStorage>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space=size_of::<MyStorage>() + 8,
seeds = [],
bump)]
pub my_storage: Account<'info, MyStorage>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyStorage {
x: u64,
}
练习:修改测试,以参数 170 调用 set()。这是我们试图在 MyStorage 中持久化的 x 的值。你需要在调用 initialize() 之后调用 set()。别忘了将 170 转换为 bignumber。
set() 函数解析
在下方,我们稍微重新排列了代码,以便将 set() 函数、Set 结构体和 MyStorage 结构体放在一起展示:

现在我们来解释 ctx.accounts.my_storage.x = new_x 是如何工作的:
ctx中的 accounts 字段(顶部蓝框)使我们能够访问Set结构体中的所有键(keys)。在 Rust 中通常不是这样列出结构体的键的。accounts 能够引用Set结构体中的键,是由于#[derive(Accounts)]宏(底部蓝框)神奇地注入了这一能力。- 账户
my_storage(橙框)被设置为 mut 或可变的(绿框),因为我们打算修改其中的一个值,即x(红框)。 - 键
my_storage(橙框)通过将MyStorage作为泛型参数传递给Account,为我们提供了对MyStorage账户(黄框)的引用。我们使用键my_storage和存储结构体MyStorage主要是为了可读性,它们不必须是彼此的驼峰命名变体。真正将它们“绑定在一起”的是由黄框和黄色箭头所说明的部分。
本质上,当调用 set() 时,调用者(Typescript 客户端)会将 myStorage 账户传递给 set()。该账户内部包含了存储的地址。在后台,set 会加载该存储,写入 x 的新值,序列化结构体,然后再将其存回。
Context 结构体 Set
用于 set() 的 Context 结构体比 initialize 简单得多,因为它只需要一个资源:对 MyStorage 账户的可变引用。
#[derive(Accounts)]
pub struct Set<'info> {
#[account(mut, seeds = [], bump)]
pub my_storage: Account<'info, MyStorage>,
}
回想一下,Solana 交易必须提前指定它将访问哪些账户。用于 set() 函数的结构体指定了它将可变地(mut)访问 my_storage 账户。
seeds = [] 和 bump 用于派生我们将要修改的账户的地址。尽管用户为我们传入了该账户,Anchor 还是会通过重新派生地址并将其与用户提供的地址进行比较,来验证用户传入的确实是该程序所拥有的账户。
术语 bump 目前可以当作样板代码来对待。但对于好奇的人来说,它是用来确保该账户不是一个密码学上有效的公钥。这就是运行时如何知道这将被用作程序的数据存储。
尽管我们的 Solana 程序可以自行派生存储账户的地址,但用户仍然需要提供 myStorage 账户。这是 Solana 运行时的要求,具体原因我们将在后续的教程中讨论。
编写 set 函数的另一种方式
如果我们向账户写入多个变量,像下面这样一遍又一遍地编写 ctx.accounts.my_storage 会显得相当笨拙:
ctx.accounts.my_storage.x = new_x;
ctx.accounts.my_storage.y = new_y;
ctx.accounts.my_storage.z = new_z;
相反,我们可以使用 Rust 中的“可变引用”(&mut),它为我们提供了一个便于操作对应值的“句柄”。请看以下对我们 set() 函数的重写:
pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
let my_storage = &mut ctx.accounts.my_storage;
my_storage.x = new_x;
Ok(())
}
练习:使用新的 set 函数重新运行测试。如果你正在使用本地测试网,别忘了重置 validator。
查看我们的存储账户
如果你正在为测试运行一个本地 validator,你可以使用以下 Solana 命令行指令来查看账户数据:
# replace the address with the one in your test
solana account 9opwLZhoPdEh12DYpksnSmKQ4HTPSAmMVnRZKymMfGvn
请将地址替换为单元测试在控制台打印出来的地址。
输出如下所示:

前 8 个字节(绿框)是鉴别器。我们的测试在结构体中存储了数字 170,它的十六进制表示为 aa,显示在红框中。
当然,命令行并不是我们在前端用来查看账户数据的机制,也不适用于希望我们的程序查看另一个程序账户的情况。这将会在下一篇教程中讨论。
在 Rust 程序中查看我们的存储账户
不过,在 Rust 程序内部读取我们自己的存储值是非常直接的。
我们将以下函数添加到 pub mod basic_storage 中:
pub fn print_x(ctx: Context<PrintX>) -> Result<()> {
let x = ctx.accounts.my_storage.x;
msg!("The value of x is {}", x);
Ok(())
}
然后我们为 PrintX 添加以下结构体:
#[derive(Accounts)]
pub struct PrintX<'info> {
pub my_storage: Account<'info, MyStorage>,
}
请注意,my_storage 没有使用 #[account(mut)] 宏,因为我们不需要它是可变的,我们只是在读取它。
接着我们在测试中添加以下代码:
await program.methods.printX().accounts({myStorage: myStorage}).rpc();
如果你在后台运行着 solana logs,你应该会看到该数字被打印出来。
练习:编写一个 increment 函数,读取 x 并将 x + 1 存回 x 中。
原文发布于 2024 年 2 月 25 日