Solana - 使用 Solana Web3.js 2.0 反序列化账户数据

  • QuickNode
  • 发布于 2025-01-15 19:49
  • 阅读 20

本文详细讲解了如何使用Solana Web3.js 2.0的新编码解码工具来解析Solana账户数据结构,特别是针对Raydium AMM配置文件的解码。文章不仅介绍了二进制数据的基本概念,还提供了逐步的实施指南,如何设置项目、定义常量、创建账户解码器等,以便有效解析账户信息。

概述

Solana 账户以原始字节形式存储数据,这些数据必须正确解码才能在你的应用中发挥作用。本指南演示如何使用 Solana Web3.js 2.0 的新编解码工具( @solana/codecs-core)来编码和解码 Solana 数据结构。在本指南中,我们将生成一个解码器,用于解析 Raydium AMM 配置文件。让我们开始吧!

使用 Solana Web3.js Legacy(v1.x)进行构建

本指南将带你了解如何使用 Solana Web3.js 2.0 进行账户反序列化。

如果你更喜欢使用 Solana Web3.js Legacy(v1.x)进行构建,请查看我们在 GitHub 的示例代码或我们的 指南:如何使用 Solana Web3.js 1.x 反序列化账户数据

你将会做什么

  • 学习 Solana 账户中二进制数据编码的工作原理
  • 理解字节序和字节顺序
  • 创建用于解析复杂账户结构的解码器
  • 获取并解码 Raydium AMM 配置账户

你需要准备什么

理解 Solana 中的二进制数据

在深入实现之前,让我们了解一些关键概念,关于 Solana 中二进制数据的工作方式。

二进制数据布局

Solana 账户将数据存储为字节序列。当读取这些数据时,我们需要:

  1. 知道每个字段的确切顺序和大小
  2. 按顺序读取字节 - 不能跳来跳去,因为每个字段的位置依赖于前面的字段
  3. 使用适当的解码器根据数据类型解码每个字段

例如,一个简单的账户具有一个 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 编码

Solana 的 getAccountInfo 可以返回编码的账户数据,而不是原始字节。我们将指定 Base64 编码的字符串,因为:

  • Base64 是一种安全的将二进制数据以文本形式传输的方式
  • 它在不同平台和语言中是统一的
  • 它在传输过程中防止数据损坏

这意味着我们需要:

  1. 请求用 Base64 编码的账户数据
  2. 解码 Base64 字符串以获取原始字节
  3. 将原始字节解析入我们的数据结构

实现

让我们通过为 Raydium AMM 配置账户 9iFER3bpjf1PTTCQCfTRu17EJgvsxo9pVyA9QWwEuX4x (SolScan) 实现一个账户数据解码器。

解析后的账户数据 - Solscan

我们的目标是提取在 SolScan 数据标签中发现的与 getAccountInfo 调用中的相同信息。让我们来做吧!

1. 设置项目

创建一个新项目并安装依赖项:

mkdir account-decoder && cd account-decoder

然后,初始化项目:

npm init -y

并安装依赖项:

npm install @solana/web3.js@2 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 Mainnet 端点。

复制 HTTP 提供者链接:

创建一个 .env 文件,并填写你的 Solana RPC 端点:

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 要求我们将缓冲区定义为 ReadonlyUint8Arrayu64(及更大)必须声明为 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 配置 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:

解析 AMM 配置 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 配置 PDA: ${configPda}`);

    // 👇 添加此部分
    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(`未找到账户: ${configPda.toString()}`);
    }
    let bytes = base64Encoder.encode(value.data[0]);
    const decoded = ammConfigDecoder.decode(bytes);
    console.log(decoded);
}

让我们拆解一下:

  • 首先,我们使用 createSolanaRpc 函数和我们的 Solana Mainnet 端点定义我们的 rpc
  • 接下来,我们创建一个 Base64 编码器--我们将使用这个编码器将 Base64 getAccountInfo 数据编码为字节
  • 然后,我们使用 Base64 编码获取我们的 configPda 账户信息
  • 如果收到响应,我们将编码响应数据以获取其字节
  • 最后,我们使用我们的 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 Web3.js 2.0 反序列化和解析 Solana 账户数据所需的工具。

使用 Codama 生成编解码器

Codama 是一个将 Solana 程序标准化为称为 Codama IDL 格式的工具。这些 Codama IDL 可用于生成各种输出,包括程序客户端。生成的程序客户端自动创建账户编解码器以序列化和反序列化数据 - 例如,像 getAmmConfigurationAccountDecoder 这样的方法可以通过传入 Raydium IDL 自动创建(在后台,这些方法使用 @solana/codecs 包)。

要了解有关使用 Codama 构建程序客户端的更多信息,请查看我们的指南:

关键注意事项

在处理 Solana 中的二进制数据时,错误的余地很小。如果有一个字节位置错误,你的整个响应可能会扭曲。在处理字节数据时,这里有一些重要的考虑事项:

  1. 字段顺序很重要:字段必须以存储的确切顺序进行解码
  2. 检查字节序:验证程序使用的字节序(检查其源代码或文档)
  3. 验证大小:确保你的解码器匹配程序中字段的确切大小
  4. 处理鉴别符:Anchor 程序通常以 8 字节鉴别符开始(不同程序在这里采用不同的方法)
  5. 彻底测试:二进制解析错误可能是微妙的——使用我们在此示例中所做的已知数据进行测试

请注意,这些工具也可用于指令数据!你只需确保理解预期的输入参数以派生你的解码器结构。

我们很高兴看到你在做什么——在 QuickNode DiscordTwitter 上给我们留言,告诉我们你构建了什么!

我们 ❤️ 反馈!

让我们知道 如果你有任何反馈或对新主题的请求。我们很乐意听到你的声音。

资源

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。