使用 Solana Kit 反序列化账户数据
本文详细介绍了如何使用 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 账户中二进制数据编码的工作原理
- 理解字节序和字节顺序
- 创建用于解析复杂账户结构的解码器
- 获取并解码一个 Raydium AMM 配置账户
你需要准备的内容
- 一个 Quicknode 账户,并启用 Solana 主网端点。
- Node.js(建议版本 21.0 或更高)
- 对 TypeScript 和 Solana 开发概念有基本了解
- 建议具备 Solana Kit 的使用经验
- 理解 程序派生地址
理解 Solana 中的二进制数据
在深入实现之前,让我们先了解一些关于 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
因此,这反序列化为:
- 第一个字段 (u8): 1
- 第二个字段 (字符串): "hello world"
查看这个有用的 空间参考表,了解 Solana 编程中常见类型的空间分配。
字节序
字节序决定了多字节数字在内存中的存储方式:
- 小端序:最低有效字节在前(0x1234 存储为 [0x34, 0x12])
- 大端序:最高有效字节在前(0x1234 存储为 [0x12, 0x34])
Solana 程序可以根据程序规范使用任一种字节序。在我们的示例中,Raydium 的程序使用大端序(如他们的 源代码 中使用了 to_be_bytes() 而非 to_le_bytes()),因此我们也必须使用大端序解码。
Base64 编码
一般不推荐二进制/Base64 编码
在大多数情况下,获取账户数据时应使用 encoding: 'jsonParsed',因为它返回已解析的数据,更易于使用。然而,对于像本指南中进行的自定义反序列化,我们需要原始二进制数据。
当进行自定义反序列化时,Solana 的 getAccountInfo 可以返回编码后的账户数据而非原始字节。我们将指定 base64 编码的字符串,因为:
- Base64 是一种安全的传输二进制数据的方式,作为文本
- 在不同平台和语言之间保持一致
- 防止传输过程中数据损坏
这意味着我们需要:
- 请求带有 base64 编码的账户数据(当需要自定义反序列化时)
- 解码 base64 字符串以获取原始字节
- 将原始字节解析为我们的数据结构
实现
让我们逐步实现一个 Raydium AMM 配置账户 9iFER3bpjf1PTTCQCfTRu17EJgvsxo9pVyA9QWwEuX4x (SolScan) 的账户数据解码器。

我们的目标是从 getAccountInfo 调用中提取与 SolScan 数据选项卡中相同的信息。开始吧!
1. 设置项目
创建一个新项目并安装依赖:
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"
使用你的 Quicknode 端点连接到 Solana 集群
要在 Solana 上构建,你需要一个 API 端点连接到网络。你可以使用公共节点或自行部署和管理基础设施;但如果你想要 8 倍更快的响应时间,可以将繁重的工作交给我们。
了解为什么超过 50% 的 Solana 项目选择 Quicknode,并在此 开始免费试用。我们将使用一个 Solana 主网端点。
复制 HTTP Provider 链接:
创建一个包含你的 Solana RPC 端点的 .env 文件:
HTTP_ENDPOINT=https://your-quicknode-endpoint.example
2. 定义常量和类型
创建一个新文件 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)
3. 定义账户结构
添加描述我们账户数据结构的接口。我们直接从 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。
4. 创建账户解码器
解码器指定如何从二进制数据中读取每个字段,并应与 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 }\
)]\
]);
解码器中的每个字段指定:
- 字段名称(与我们的接口匹配)
- 适用于该字段数据类型的解码器
- 需要时的尺寸约束(如 8 字节的区分符或数组解码器中的大小)
5. 派生 PDA
由于我们已经知道要查找的地址,理论上不需要执行此步骤,但对于使用 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
做得好!
6. 解码账户
现在,让我们更新 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 主网端点定义rpc - 接下来,我们创建一个 base64 编码器——我们将用它来将 base64
getAccountInfo数据编码为字节 - 然后,我们使用 base64 编码获取
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 生成编解码器
Codama 是一个工具,将 Solana 程序标准化为称为 Codama IDL 的格式。这些 Codama IDL 可用于生成各种输出,包括程序客户端。生成的程序客户端会自动创建用于序列化和反序列化数据的账户编解码器——例如,像 getAmmConfigurationAccountDecoder 这样的方法可以通过传入 Raydium IDL 自动创建(在底层,这些方法使用 @solana/codecs 包)。
要了解有关使用 Codama 构建程序客户端的更多信息,请查看我们的指南:
关键考虑因素
在 Solana 中处理二进制数据时,容不得半点差错。如果一个字节位置错误,整个响应都可能被扭曲。以下是处理字节数据时需要考虑的一些重要事项:
- 字段顺序重要:字段必须按照存储的确切顺序解码
- 检查字节序:验证程序使用的字节序(检查其源代码或文档)
- 验证大小:确保你的解码器与程序中的确切字段大小匹配
- 处理区分符:Anchor 程序通常以 8 字节的区分符开头(不同的程序在此使用不同的方法)
- 彻底测试:二进制解析错误可能很微妙——像本示例中那样使用已知数据进行测试
请注意,这些工具也可用于指令数据!你只需确保了解预期的输入参数以推导你的解码器结构。
我们期待看到你的成果——在 Quicknode Discord 或 Twitter 上与我们联系,告诉我们你构建了什么!
我们 ❤️ 反馈!
告诉我们 如果你有任何反馈或对新主题的请求。我们期待听到你的声音。
资源
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~