Solana 获取程序账户的完整指南与实用技巧

Posted by AGA链讯 on April 14, 2025

概述

getProgramAccounts 是 Solana 区块链上一个功能强大的 RPC 方法,它允许开发者检索由特定程序(Program)所拥有的所有账户。这个方法在多种场景下都非常有用,例如查找特定钱包的所有代币账户、查询特定代币发行的所有持有者,或是获取某个去中心化应用的所有用户账户。

然而,由于目前该方法不支持分页,且处理大量数据时可能对 RPC 节点资源造成压力,因此在实际使用时需要配合一些参数来优化查询效率和精度。通过合理使用 dataSlicefilters 参数,可以显著提高响应速度并确保只返回预期的结果数据。

核心参数详解

基本参数

  • programIdstring 类型,必填。需要查询的程序的公钥,以 base58 编码的字符串形式提供。
  • configOrCommitmentobject 类型,可选。包含以下可选字段的配置对象:
    • commitmentstring 类型,用于指定状态承诺级别,影响数据的 freshness。
    • encodingstring 类型,指定账户数据的编码格式,可选值包括 base58base64jsonParsed。注意:web3.js 用户建议使用 getParsedProgramAccounts 方法。
    • dataSliceobject 类型,用于限制返回的账户数据量:
      • offsetnumber 类型,开始返回数据的字节偏移量。
      • lengthnumber 类型,要返回的数据字节长度。
    • filtersarray 类型,可选。用于对结果进行筛选的过滤器数组,支持两种类型:
      • memcmp:内存比较过滤器,匹配账户数据中特定偏移处的字节。
        • offsetnumber 类型,开始比较的字节偏移量。
        • bytesstring 类型,要匹配的 base58 编码数据(最多 129 字节)。
      • dataSizenumber 类型,根据账户数据的精确长度进行筛选。
    • withContextboolean 类型,可选。若为 true,则将结果包装在 RpcResponse JSON 对象中。

响应结构

默认情况下,getProgramAccounts 返回一个 JSON 对象数组,每个对象包含以下字段:

  • pubkeystring 类型,账户的公钥(base58 编码)。
  • accountobject 类型,包含账户详情:
    • lamportsnumber 类型,账户持有的 lamports 数量。
    • ownerstring 类型,账户所属程序的公钥(base58 编码)。
    • datastring object 类型,账户数据,其格式取决于编码参数。
    • executableboolean 类型,指示账户是否包含可执行程序。
    • rentEpochnumber 类型,下一次需要支付租金的纪元(epoch)。

深入理解过滤器使用

getProgramAccounts 的强大之处在于其灵活的过滤系统,能够帮助开发者精确缩小查询范围。在使用过滤器之前,了解目标账户的数据布局和序列化方式至关重要。

DataSize 过滤器

dataSize 过滤器允许我们根据账户数据的精确长度来筛选结果。例如,在 SPL-Token 标准中,一个代币账户的长度固定为 165 字节。通过添加 { dataSize: 165 } 过滤器,可以确保只返回符合该长度的账户,从而初步缩小查询范围。

Memcmp 过滤器

memcmp(内存比较)过滤器则提供了更精细的控制,允许我们匹配账户数据中特定位置的特定字节序列。它需要两个参数:

  • offset:开始比较的字节偏移量(整数)。
  • bytes:要匹配的 base58 编码数据字符串(最多 129 字节)。

需要注意的是,memcmp 只支持精确匹配,不支持范围比较或模糊匹配。

👉 查看实时数据查询工具

实战示例:查询代币账户

让我们通过一个具体示例来演示如何组合使用这些参数。假设我们想查找特定钱包地址所拥有的所有 SPL-Token 账户。

JavaScript / Web3.js 示例

import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { clusterApiUrl, Connection } from "@solana/web3.js";

(async () => {
  const MY_WALLET_ADDRESS = "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T";
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  
  const accounts = await connection.getParsedProgramAccounts(
    TOKEN_PROGRAM_ID,
    {
      filters: [
        {
          dataSize: 165, // 筛选代币账户
        },
        {
          memcmp: {
            offset: 32, // owner字段的偏移量
            bytes: MY_WALLET_ADDRESS, // 匹配特定所有者
          },
        },
      ],
    }
  );
  
  console.log(`找到 ${accounts.length} 个代币账户`);
  accounts.forEach((account, i) => {
    console.log(`账户 ${i + 1}: ${account.pubkey.toString()}`);
    console.log(`Mint: ${account.account.data["parsed"]["info"]["mint"]}`);
    console.log(`金额: ${account.account.data["parsed"]["info"]["tokenAmount"]["uiAmount"]}`);
  });
})();

Rust 示例

use solana_client::{
  rpc_client::RpcClient,
  rpc_filter::{RpcFilterType, Memcmp, MemcmpEncodedBytes},
  rpc_config::{RpcProgramAccountsConfig, RpcAccountInfoConfig},
};
use solana_sdk::{commitment_config::CommitmentConfig, program_pack::Pack};
use spl_token::state::Account;

fn main() {
  const MY_WALLET_ADDRESS: &str = "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T";
  let rpc_url = String::from("http://api.devnet.solana.com");
  let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
  
  let filters = Some(vec![
    RpcFilterType::Memcmp(Memcmp::new(
      32,
      MemcmpEncodedBytes::Base58(MY_WALLET_ADDRESS.to_string()),
    )),
    RpcFilterType::DataSize(165),
  ]);
  
  let accounts = connection.get_program_accounts_with_config(
    &spl_token::ID,
    RpcProgramAccountsConfig {
      filters,
      account_config: RpcAccountInfoConfig {
        encoding: Some(UiAccountEncoding::Base64),
        commitment: Some(connection.commitment()),
        ..RpcAccountInfoConfig::default()
      },
      ..RpcProgramAccountsConfig::default()
    },
  ).unwrap();
  
  println!("找到 {} 个代币账户", accounts.len());
  for (i, account) in accounts.iter().enumerate() {
    println!("账户 {}: {}", i, account.0);
    let token_account = Account::unpack_from_slice(account.1.data.as_slice()).unwrap();
    println!("Mint: {:?}", token_account.mint);
    println!("金额: {}", token_account.amount);
  }
}

cURL 示例

curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getProgramAccounts",
  "params": [
    "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
    {
      "encoding": "jsonParsed",
      "filters": [
        {
          "dataSize": 165
        },
        {
          "memcmp": {
            "offset": 32,
            "bytes": "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T"
          }
        }
      ]
    }
  ]
}'

DataSlice 参数的高级应用

dataSlice 参数不会减少返回的账户数量,但会限制每个账户返回的数据量。这在只需要元数据而不关心完整账户数据的场景下非常有用,例如统计特定代币的持有者数量。

使用示例

import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { clusterApiUrl, Connection } from "@solana/web3.js";

(async () => {
  const MY_TOKEN_MINT_ADDRESS = "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf";
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  
  const accounts = await connection.getProgramAccounts(
    TOKEN_PROGRAM_ID,
    {
      dataSlice: {
        offset: 0,
        length: 0, // 不返回实际数据
      },
      filters: [
        {
          dataSize: 165,
        },
        {
          memcmp: {
            offset: 0, // 匹配mint地址
            bytes: MY_TOKEN_MINT_ADDRESS,
          },
        },
      ],
    }
  );
  
  console.log(`找到 ${accounts.length} 个持有该代币的账户`);
});

👉 获取进阶查询技巧

性能优化与最佳实践

  1. ** always 使用过滤器**:尽可能使用 dataSizememcmp 过滤器来缩小查询范围,减少不必要的网络传输和节点负载。

  2. 合理使用 dataSlice:如果只需要账户的存在性信息或数量统计,使用 dataSlice 设置 length: 0 可以显著减少响应数据量。

  3. 了解数据布局:在使用 memcmp 过滤器前,务必了解目标账户的数据结构,特别是字段的偏移量信息。

  4. 错误处理:实现适当的重试机制和错误处理,特别是处理可能超时的大型查询。

  5. 节点选择:对于生产环境,考虑使用付费的专用 RPC 节点以获得更好的性能和可靠性。

常见问题

getProgramAccounts 有哪些限制?

目前该方法不支持分页,如果查询结果过大,响应可能会被截断。此外,过于频繁或范围过大的查询可能导致连接超时。

为什么需要同时使用 dataSize 和 memcmp 过滤器?

dataSize 首先根据账户长度进行初步筛选,减少需要内存比较的数据量。memcmp then 进行精确的内容匹配。这种组合使用可以大幅提高查询效率。

如何确定 memcmp 过滤器的正确偏移量?

需要查阅相应程序的数据结构文档。对于 SPL-Token 账户,owner 字段的偏移量是 32 字节,因为前面有一个 32 字节的 mint 字段。

dataSlice 和 filters 有什么区别?

dataSlice 只影响返回数据的量,不改变返回的账户数量。filters 则实际改变返回的账户集合,只返回符合筛选条件的账户。

如何处理超时问题?

可以尝试增加超时设置,或者通过添加更严格的过滤器来缩小查询范围。对于非常大的查询,考虑分批进行或使用专门的索引服务。

是否支持比较大于或小于特定值的查询?

目前的 memcmp 实现只支持精确匹配,不支持范围比较。如果需要范围查询,需要在客户端处理后端返回的所有数据。

通过掌握 getProgramAccounts 方法的这些高级用法和优化技巧,开发者可以构建出更高效、可靠的 Solana 区块链应用,提供更好的用户体验。