本教程详细介绍了如何在Anchor框架下向已初始化的Solana账户写入数据,并解释了相关代码的实现原理。
在之前的教程中,我们讨论了如何初始化账户以便将数据持久化存储。本教程将展示如何向我们已初始化的账户写入数据。
以下是之前关于初始化 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(())
}
// ****************************
// *** 这个函数是新添加的 ***
// ****************************
pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
ctx.accounts.my_storage.x = new_x;
Ok(())
}
}
// **************************
// *** 这个结构体是新添加的 ***
// **************************
##[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 转换为大数。
set() 函数解释
下面,我们稍微重新排列了代码,将 set() 函数、Set 结构体和 MyStorage 结构体放在一起:
我们现在解释 ctx.accounts.my_storage.x = new_x 的工作原理:
ctx中的accounts字段(顶部蓝色框)让我们可以访问Set结构体中的所有键。这不是在 Rust 中列出结构体键的方式。accounts能够引用Set结构体中的键,是由于#[derive(Accounts)]宏(底部蓝色框)神奇地插入的。my_storage账户(橙色框)被设置为mut或可变的(绿色框),因为我们打算更改其中的值x(红色框)。- 键
my_storage(橙色框)通过将MyStorage作为泛型参数传递给Account,为我们提供了对MyStorage账户(黄色框)的引用。我们使用键my_storage和存储结构体MyStorage是为了可读性,它们不需要是彼此的大小写变体。将它们“绑定在一起”的是黄色框和黄色箭头。
本质上,当调用 set() 时,调用者(Typescript 客户端)将 myStorage 账户传递给 set()。在这个账户内部是存储的地址。在幕后,set 将加载存储,写入 x 的新值,序列化结构体,然后将其存储回去。
Set 的 Context 结构体
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),它为我们提供了一个“Handle”来操作值。考虑以下对 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 函数重新运行测试。如果你使用的是本地测试网,别忘了重置验证器。
查看我们的存储账户
如果你正在为测试运行本地验证器,你可以使用以下 Solana 命令行指令查看账户数据:
## 将地址替换为你的测试中的地址
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,你应该会看到数字被打印出来。
练习:编写一个增量函数,读取 x 并将 x + 1 存储回 x。
在 Solana 中,一切都是帐户,可以潜在地保存数据。有时我们将一个帐户称为“程序帐户”,另一个帐户称为“存储帐户”,但唯一的区别在于可执行标志是否设置为真,以及我们打算如何使用帐户的数据字段。



在幕后,这是一串字节序列。上面示例中的结构:
我们将在后面的教程中学习更多,但 Solana 要求我们提前指定一笔交易将与哪些帐户交互。由于我们正在与存储 


正如预期的那样,我们得到了一个错误,因为签名者的公钥不等于所有者的公钥。
