本文详细介绍了如何使用 Solana Kit(原 Solana Web3.js 2.0)反序列化 Solana 账户数据。
🛠️ 更新通知
本指南已更新,以体现 Solana Web3.js 2.0 的新名称 —— Solana Kit。我们遵循最新的最佳实践,以帮助你保持领先。在此了解更多关于 Solana Kit 的信息。
Solana 账户以原始字节形式存储数据,必须正确解码才能在应用程序中使用。本指南演示如何使用 Solana Kit 的编解码工具([ https://github.com/anza-xyz/kit/tree/main/packages/codecs-core])来编码和解码 Solana 数据结构。在本指南中,我们将生成一个解码器来解析 Raydium AMM 配置文件。让我们开始吧!
使用 Solana Web3.js Legacy(v1.x)构建
本指南将带你使用 Solana Kit 进行账户反序列化。
如果你更倾向于使用 Solana Web3.js Legacy(v1.x)构建,请查看我们在 GitHub 上的示例代码,或我们的 指南:如何使用 Solana Web3.js 1.x 反序列化账户数据。
在深入实现之前,让我们先了解一些关于 Solana 中二进制数据工作原理的关键概念。
Solana 账户将数据存储为字节序列。在读取这些数据时,我们需要:
例如,一个包含 u8(1 字节)后跟 11 字节/字符字符串的简单账户将被这样读取:
[1, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
└─┘ └──────────────────────────────────────────────────┘
│ └── 字符串 (11 字节) = "hello world"
│ (104='h', 101='e', 108='l', 108='l', 111='o', 32=' ',
│ 119='w', 111='o', 114='r', 108='l', 100='d')
└────── u8 (1 字节) = 1
因此,这反序列化为:
查看这个有用的 空间参考表,了解 Solana 编程中常见类型的空间分配。
字节序决定了多字节数字在内存中的存储方式:
Solana 程序可以根据程序规范使用任一种字节序。在我们的示例中,Raydium 的程序使用大端序(如他们的 源代码 中使用了 to_be_bytes() 而非 to_le_bytes()),因此我们也必须使用大端序解码。
一般不推荐二进制/Base64 编码
在大多数情况下,获取账户数据时应使用 encoding: 'jsonParsed',因为它返回已解析的数据,更易于使用。然而,对于像本指南中进行的自定义反序列化,我们需要原始二进制数据。
当进行自定义反序列化时,Solana 的 getAccountInfo 可以返回编码后的账户数据而非原始字节。我们将指定 base64 编码的字符串,因为:
这意味着我们需要:
让我们逐步实现一个 Raydium AMM 配置账户 9iFER3bpjf1PTTCQCfTRu17EJgvsxo9pVyA9QWwEuX4x (SolScan) 的账户数据解码器。

我们的目标是从 getAccountInfo 调用中提取与 SolScan 数据选项卡中相同的信息。开始吧!
创建一个新项目并安装依赖:
mkdir account-decoder && cd account-decoder
然后,初始化项目:
npm init -y
并安装依赖:
npm install @solana/kit dotenv
如果全局未安装,添加以下开发依赖:
npm install --save-dev typescript ts-node @types/node
初始化 tsconfig:
tsc --init
将以下脚本添加到 package.json:
"start": "ts-node app.ts"
要在 Solana 上构建,你需要一个 API 端点连接到网络。你可以使用公共节点或自行部署和管理基础设施;但如果你想要 8 倍更快的响应时间,可以将繁重的工作交给我们。
了解为什么超过 50% 的 Solana 项目选择 Quicknode,并在此 开始免费试用。我们将使用一个 Solana 主网端点。
复制 HTTP Provider 链接:
创建一个包含你的 Solana RPC 端点的 .env 文件:
HTTP_ENDPOINT=https://your-quicknode-endpoint.example
创建一个新文件 app.ts,并添加以下导入和常量:
import {
createSolanaRpc
} from "@solana/rpc"
import {
Address,
address,
getAddressDecoder,
getProgramDerivedAddress,
} from "@solana/addresses";
import {
Endian,
getU16Encoder,
getBase64Encoder,
getStructDecoder,
FixedSizeDecoder,
fixDecoderSize,
getBytesDecoder,
getU8Decoder,
getU16Decoder,
getU32Decoder,
getU64Decoder,
getArrayDecoder,
ReadonlyUint8Array
} from "@solana/codecs";
import dotenv from "dotenv";
dotenv.config();
const PROGRAM_ID = address('CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK');
const AMM_CONFIG_SEED = "amm_config";
const AMM_CONFIG_INDEX = 4;
注意,我们从几个不同的包中导入 Solana 元素——这些包在 @solana/web3.js 中也都可以使用,但我们只是想强调你可以选择导入特定的包。你将看到 codecs 包提供了许多用于根据类型编码和解码数据的工具。我们稍后将使用它们。
我们定义的常量是:
PROGRAM_ID 是 CLMM 程序的 ID(参考:GitHub)AMM_CONFIG_SEED 在程序中指定,用于派生配置账户 PDA。(参考:GitHub)AMM_CONFIG_INDEX 是一个管理员指定的值,Raydium 使用它来定义配置账户 PDA。(参考:GitHub)添加描述我们账户数据结构的接口。我们直接从 Raydium IDL 获取(在 idl.accounts.find(account => account.name == "AmmConfig") 处)。数据结构必须按正确顺序列出,并且类型正确,以避免在解码时出现任何解析或类型错误:
interface AmmConfig {
anchorDiscriminator: ReadonlyUint8Array;
bump: number;
index: number;
owner: Address;
protocolFeeRate: number;
tradeFeeRate: number;
tickSpacing: number;
fundFeeRate: number;
paddingU32: number;
fundOwner: Address;
padding: bigint[];
}
注意,SW3js2 要求我们将 Buffers 定义为 ReadonlyUint8Array,而 u64(及更大)必须声明为 bigint。
解码器指定如何从二进制数据中读取每个字段,并应与 IDL 以及我们刚刚定义的接口匹配。将以下代码添加到你的代码中:
const ammConfigDecoder: FixedSizeDecoder<AmmConfig> =
getStructDecoder([\
["anchorDiscriminator", fixDecoderSize(getBytesDecoder(), 8)],\
["bump", getU8Decoder()],\
["index", getU16Decoder()],\
["owner", getAddressDecoder()],\
["protocolFeeRate", getU32Decoder()],\
["tradeFeeRate", getU32Decoder()],\
["tickSpacing", getU16Decoder()],\
["fundFeeRate", getU32Decoder()],\
["paddingU32", getU32Decoder()],\
["fundOwner", getAddressDecoder()],\
["padding", getArrayDecoder(\
getU64Decoder(),\
{ size: 3 }\
)]\
]);
解码器中的每个字段指定:
由于我们已经知道要查找的地址,理论上不需要执行此步骤,但对于使用 Solana 地址和编解码库来说,这是一个好习惯。
添加主要函数来获取和解码账户数据:
async function main() {
// 为 PDA 派生创建编码器
const u16BEEncoder = getU16Encoder({ endian: Endian.Big });
// 派生配置账户地址
const [configPda] = await getProgramDerivedAddress({
programAddress: PROGRAM_ID,
seeds: [\
AMM_CONFIG_SEED,\
u16BEEncoder.encode(AMM_CONFIG_INDEX),\
]
});
console.log(`正在解析 AMM Config PDA: ${configPda}`);
// TODO - 获取并解析 PDA
}
main().catch(console.error);
这里,我们使用 getProgramDerivedAddress 来派生我们的 PDA(来源:GitHub)。该方法需要一个程序地址和一个 Seeds 数组,定义为 type Seed = ReadonlyUint8Array | string;。这意味着我们可以直接使用 AMM_CONFIG_SEED,但需要先将 AMM_CONFIG_INDEX 编码为 ReadonlyUint8Array。
为此,我们定义了一个 u16 大端序编码器(回想一下,Raydium 程序使用了 .to_be_bytes()),然后对我们的索引调用 encode 方法。
我们将程序 ID 和种子(按顺序)传入 getProgramDerivedAddress 函数并等待结果。
在添加账户解码器之前,让我们确保正确派生 PDA。如果常量定义正确且种子编码正确,我们应该返回正确的 PDA 9iFER3bpjf1PTTCQCfTRu17EJgvsxo9pVyA9QWwEuX4x。
在终端中,运行脚本:
npm start
你应该会在终端中看到正确的 PDA 被记录:
Parsing AMM Config PDA: 9iFER3bpjf1PTTCQCfTRu17EJgvsxo9pVyA9QWwEuX4x
做得好!
现在,让我们更新 main 函数中的 TODO。在你的 main 函数中,在现有代码下方添加以下内容:
async function main() {
// 为 PDA 派生创建编码器
const u16BEEncoder = getU16Encoder({ endian: Endian.Big });
// 派生配置账户地址
const [configPda] = await getProgramDerivedAddress({
programAddress: PROGRAM_ID,
seeds: [\
AMM_CONFIG_SEED,\
u16BEEncoder.encode(AMM_CONFIG_INDEX),\
]
});
console.log(`正在解析 AMM Config PDA: ${configPda}`);
// 注意:这里使用 'base64' 编码,因为我们需要原始二进制数据进行自定义反序列化。
// 在大多数情况下,使用 'jsonParsed' 编码会返回已解析的数据。
const rpc = createSolanaRpc(process.env.HTTP_ENDPOINT as string);
const base64Encoder = getBase64Encoder();
const { value } = await rpc.getAccountInfo(configPda, { encoding: 'base64' }).send();
if (!value || !value?.data) {
throw new Error(`Account not found: ${configPda.toString()}`);
}
let bytes = base64Encoder.encode(value.data[0]);
const decoded = ammConfigDecoder.decode(bytes);
console.log(decoded);
}
我们来分解一下:
createSolanaRpc 函数和 Solana 主网端点定义 rpcgetAccountInfo 数据编码为字节configPda 账户信息(这是自定义反序列化的特殊情况;通常你会使用 jsonParsed)ammConfigDecoder 调用 decode 方法来反序列化原始数据让我们试一下。
在终端中,运行脚本:
npm start
你应该会在控制台中看到解码后的 AMM 配置数据:
{
anchorDiscriminator: Uint8Array(8) [\
218, 244, 33, 104,\
203, 203, 43, 111\
],
bump: 249,
index: 4,
owner: 'projjosVCPQH49d5em7VYS7fJZzaqKixqKtus7yk416',
protocolFeeRate: 120000,
tradeFeeRate: 100,
tickSpacing: 1,
fundFeeRate: 40000,
paddingU32: 0,
fundOwner: 'FundHfY8oo8J9KYGyfXFFuQCHe7Z1VBNmsj84eMcdYs4',
padding: [ 0n, 0n, 0n ]
}
做得好!你现在拥有了使用 Solana Kit 反序列化和解析 Solana 账户数据所需的工具。
Codama 是一个工具,将 Solana 程序标准化为称为 Codama IDL 的格式。这些 Codama IDL 可用于生成各种输出,包括程序客户端。生成的程序客户端会自动创建用于序列化和反序列化数据的账户编解码器——例如,像 getAmmConfigurationAccountDecoder 这样的方法可以通过传入 Raydium IDL 自动创建(在底层,这些方法使用 @solana/codecs 包)。
要了解有关使用 Codama 构建程序客户端的更多信息,请查看我们的指南:
在 Solana 中处理二进制数据时,容不得半点差错。如果一个字节位置错误,整个响应都可能被扭曲。以下是处理字节数据时需要考虑的一些重要事项:
请注意,这些工具也可用于指令数据!你只需确保了解预期的输入参数以推导你的解码器结构。
我们期待看到你的成果——在 Quicknode Discord 或 Twitter 上与我们联系,告诉我们你构建了什么!
告诉我们 如果你有任何反馈或对新主题的请求。我们期待听到你的声音。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码