概述
getProgramAccounts 是 Solana 区块链上一个功能强大的 RPC 方法,它允许开发者检索由特定程序(Program)所拥有的所有账户。这个方法在多种场景下都非常有用,例如查找特定钱包的所有代币账户、查询特定代币发行的所有持有者,或是获取某个去中心化应用的所有用户账户。
然而,由于目前该方法不支持分页,且处理大量数据时可能对 RPC 节点资源造成压力,因此在实际使用时需要配合一些参数来优化查询效率和精度。通过合理使用 dataSlice 和 filters 参数,可以显著提高响应速度并确保只返回预期的结果数据。
核心参数详解
基本参数
- programId:
string类型,必填。需要查询的程序的公钥,以 base58 编码的字符串形式提供。 - configOrCommitment:
object类型,可选。包含以下可选字段的配置对象:- commitment:
string类型,用于指定状态承诺级别,影响数据的 freshness。 - encoding:
string类型,指定账户数据的编码格式,可选值包括base58、base64或jsonParsed。注意:web3.js 用户建议使用getParsedProgramAccounts方法。 - dataSlice:
object类型,用于限制返回的账户数据量:offset:number类型,开始返回数据的字节偏移量。length:number类型,要返回的数据字节长度。
- filters:
array类型,可选。用于对结果进行筛选的过滤器数组,支持两种类型:memcmp:内存比较过滤器,匹配账户数据中特定偏移处的字节。offset:number类型,开始比较的字节偏移量。bytes:string类型,要匹配的 base58 编码数据(最多 129 字节)。
dataSize:number类型,根据账户数据的精确长度进行筛选。
- withContext:
boolean类型,可选。若为 true,则将结果包装在 RpcResponse JSON 对象中。
- commitment:
响应结构
默认情况下,getProgramAccounts 返回一个 JSON 对象数组,每个对象包含以下字段:
- pubkey:
string类型,账户的公钥(base58 编码)。 - account:
object类型,包含账户详情:- lamports:
number类型,账户持有的 lamports 数量。 - owner:
string类型,账户所属程序的公钥(base58 编码)。 -
data: stringobject类型,账户数据,其格式取决于编码参数。 - executable:
boolean类型,指示账户是否包含可执行程序。 - rentEpoch:
number类型,下一次需要支付租金的纪元(epoch)。
- lamports:
深入理解过滤器使用
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} 个持有该代币的账户`);
});
👉 获取进阶查询技巧
性能优化与最佳实践
-
** always 使用过滤器**:尽可能使用
dataSize和memcmp过滤器来缩小查询范围,减少不必要的网络传输和节点负载。 -
合理使用 dataSlice:如果只需要账户的存在性信息或数量统计,使用
dataSlice设置length: 0可以显著减少响应数据量。 -
了解数据布局:在使用
memcmp过滤器前,务必了解目标账户的数据结构,特别是字段的偏移量信息。 -
错误处理:实现适当的重试机制和错误处理,特别是处理可能超时的大型查询。
-
节点选择:对于生产环境,考虑使用付费的专用 RPC 节点以获得更好的性能和可靠性。
常见问题
getProgramAccounts 有哪些限制?
目前该方法不支持分页,如果查询结果过大,响应可能会被截断。此外,过于频繁或范围过大的查询可能导致连接超时。
为什么需要同时使用 dataSize 和 memcmp 过滤器?
dataSize 首先根据账户长度进行初步筛选,减少需要内存比较的数据量。memcmp then 进行精确的内容匹配。这种组合使用可以大幅提高查询效率。
如何确定 memcmp 过滤器的正确偏移量?
需要查阅相应程序的数据结构文档。对于 SPL-Token 账户,owner 字段的偏移量是 32 字节,因为前面有一个 32 字节的 mint 字段。
dataSlice 和 filters 有什么区别?
dataSlice 只影响返回数据的量,不改变返回的账户数量。filters 则实际改变返回的账户集合,只返回符合筛选条件的账户。
如何处理超时问题?
可以尝试增加超时设置,或者通过添加更严格的过滤器来缩小查询范围。对于非常大的查询,考虑分批进行或使用专门的索引服务。
是否支持比较大于或小于特定值的查询?
目前的 memcmp 实现只支持精确匹配,不支持范围比较。如果需要范围查询,需要在客户端处理后端返回的所有数据。
通过掌握 getProgramAccounts 方法的这些高级用法和优化技巧,开发者可以构建出更高效、可靠的 Solana 区块链应用,提供更好的用户体验。