本教程演示了如何直接从 Solana web3 Javascript 客户端读取账户数据,以便 Web 应用程序可以在前端读取这些数据。
在之前的教程中,我们使用 solana account <account address> 来读取我们写入的数据,但如果我们在网站上构建 dApp,这种方法就行不通了。
相反,我们必须计算存储账户的地址,读取数据,并在 Solana web3 客户端中反序列化该数据。
想象一下在 Ethereum 中,如果我们想避免使用 public 变量或 view 函数,但仍然想在前端显示它们的值。要在不公开变量或添加 view 函数的情况下查看存储变量中的值,我们会使用 getStorageAt(contract_address, slot) API。我们将在 Solana 中执行类似的操作,只不过我们不需要传入 (contract_address, slot) 对,而是只需传入程序的地址并推导出其存储账户的地址。
以下是上一个教程中的 Rust 代码。它初始化了 MyStorage 并使用 set 函数写入 x。在本教程中我们不会对其进行修改:
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,
}
以下是 Typescript 单元测试,它:
- 初始化账户
- 将
170写入存储 - 使用
fetch函数重新读取该值:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";
describe("basic_storage", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicStorage as Program<BasicStorage>;
it("Is initialized!", async () => {
const seeds = []
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("the storage account address is", myStorage.toBase58());
await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
await program.methods.set(new anchor.BN(170)).accounts({myStorage: myStorage}).rpc();
// ***********************************
// *** NEW CODE TO READ THE STRUCT ***
// ***********************************
let myStorageStruct = await program.account.myStorage.fetch(myStorage);
console.log("The value of x is:",myStorageStruct.x.toString());
});
});
在 Anchor 中查看账户可以通过以下方式完成:
let myStorageStruct = await program.account.myStorage.fetch(myStorage);
console.log("The value of x is:", myStorageStruct.x.toString());
Anchor 会自动计算 MyStorage 账户的地址,读取它,并将其格式化为 Typescript 对象。
要了解 Anchor 是如何神奇地将 Rust 结构体转换为 Typescript 结构体的,让我们看看 target/idl/basic_storage.json 中的 IDL。在 JSON 的底部,我们可以看到我们的程序正在创建的结构体定义:
"accounts": [
{
"name": "MyStorage",
"type": {
"kind": "struct",
"fields": [
{
"name": "x",
"type": "u64"
}
]
}
}
],
这种方法仅适用于你的程序或客户端初始化或创建并拥有其 IDL 的账户,对于任意其他账户则不起作用。
也就是说,如果你在 Solana 上随机选择一个账户并使用上面的代码,反序列化几乎肯定会失败。在本文的后面,我们将以更“原始”的方式读取账户。
fetch 函数并没有什么魔法。那么,对于我们没有创建的账户,我们该如何做到这一点呢?
从 Anchor Solana 程序创建的账户中获取数据
如果我们知道另一个使用 Anchor 创建的程序的 IDL,我们就可以方便地读取其账户数据。
让我们在另一个 shell 中使用 anchor init 创建另一个程序,然后让它初始化一个账户,并将该结构体中的单个布尔变量设置为 true。我们将这个账户称为 other_program,将存储其布尔值的结构体称为 TrueOrFalse:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("4z4dduMSFKFJDnUAKaHnbhHySK8x1PwgArUBXzksjwa8");
#[program]
pub mod other_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn setbool(ctx: Context<SetFlag>, flag: bool) -> Result<()> {
ctx.accounts.true_or_false.flag = flag;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
#[account(init, payer = signer, space = size_of::<TrueOrFalse>() + 8, seeds=[], bump)]
true_or_false: Account<'info, TrueOrFalse>,
}
#[derive(Accounts)]
pub struct SetFlag<'info> {
#[account(mut)]
true_or_false: Account<'info, TrueOrFalse>,
}
#[account]
pub struct TrueOrFalse {
flag: bool,
}
Typescript 代码:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherProgram } from "../target/types/other_program";
describe("other_program", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.OtherProgram as Program<OtherProgram>;
it("Is initialized!", async () => {
const seeds = []
const [TrueOrFalse, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("address: ", program.programId.toBase58());
await program.methods.initialize().accounts({trueOrFalse: TrueOrFalse}).rpc();
await program.methods.setbool(true).accounts({trueOrFalse: TrueOrFalse}).rpc();
});
});
在另一个 shell 中针对本地验证节点运行测试。注意打印出来的 programId。我们将需要它来推导出 other_program 的账户地址。
read 程序
在另一个 shell 中,使用 anchor init 创建另一个程序。我们称之为 read。我们将只使用 Typescript 代码来读取 other_program 的 TrueOrFalse 结构体,而不会使用任何 Rust。这模拟了从另一个程序的存储账户中读取数据。
我们的目录布局如下:
parent_dir/
∟ other_program/
∟ read/
以下代码将从 other_program 中读取 TrueOrFalse 结构体。请确保:
otherProgramAddress与上面打印的地址相匹配- 确保你正在从正确的文件位置读取
other_program.jsonIDL - 确保使用
--skip-local-validator运行测试,以保证这段代码读取的是另一个程序所创建的账户
import * as anchor from "@coral-xyz/anchor";
describe("read", () => {
anchor.setProvider(anchor.AnchorProvider.env());
it("Read other account", async () => {
// the other program's programdId -- make sure the address is correct
const otherProgramAddress = "4z4dduMSFKFJDnUAKaHnbhHySK8x1PwgArUBXzksjwa8";
const otherProgramId = new anchor.web3.PublicKey(otherProgramAddress);
// load the other program's idl -- make sure the path is correct
const otherIdl = JSON.parse(
require("fs").readFileSync("../other_program/target/idl/other_program.json", "utf8")
);
const otherProgram = new anchor.Program(otherIdl, otherProgramId);
const seeds = []
const [trueOrFalseAcc, _bump] =
anchor.web3.PublicKey.findProgramAddressSync(seeds, otherProgramId);
let otherStorageStruct = await otherProgram.account.trueOrFalse.fetch(trueOrFalseAcc);
console.log("The value of flag is:", otherStorageStruct.flag.toString());
});
});
预期输出如下:

重申一遍,这仅在另一个 Solana 程序是使用 Anchor 构建时才有效。因为这依赖于 Anchor 序列化结构体的方式。
获取任意账户的数据
在接下来的部分中,我们将展示如何在没有 Anchor 魔法的情况下读取数据。
不幸的是,Solana 的 Typescript 客户端文档非常有限,并且该库已经更新了多次,使得关于该主题的许多教程已经过时。
要找到你需要的 Solana web3 Typescript 函数,最好的办法是查看 HTTP JSON RPC Methods 并寻找看起来很有用的那个方法。在我们的例子中,getAccountInfo 看起来很有希望(蓝色箭头)。

接下来我们要在 Solana web3 js 中尝试找到该方法。最好使用带有自动补全功能的 IDE,这样你就可以四处摸索,直到找到那个函数,如下面的视频所示:
下面我们展示再次运行测试的预期输出:

围绕十六进制 aa 字节的绿框表明我们成功检索到了存储在 set() 函数中的十进制值 170。
下一步是解析数据缓冲区,但这不在我们这里的讨论范围内。
Solana 账户中的数据序列化并没有“强制”的方式。Anchor 以自己的方式序列化结构体,但是如果有人用原生 Rust(没有使用 Anchor)编写了 Solana 程序,或者使用了他们自己的序列化算法,那么你就必须根据他们序列化数据的方式来定制你的反序列化算法。
继续学习 Solana
你可以在这里查看我们完整的 Solana course。
最初发布于 2024 年 2 月 26 日