在 Solana 上做任何不算简单的事,你都会跟智能合约打交道(Solana 里叫"程序")。RPC 层就是这事发生的地方——每次跟程序交互都是过某个 RPC 方法。大多数开发者学完基础模式就停了;生产级模式要求你多懂一点底下到底在干什么。
这篇我们过一遍怎么从客户端代码高效跟 Solana 程序交互:读状态、发指令、订阅变化、把"能跑"和"生产级"分开的那些模式。
"智能合约 RPC"到底指什么
Solana 上术语是"程序"而不是"智能合约"——但概念一样:链上部署的代码,你能从客户端跟它交互。RPC 跟程序的交互分三类:
读——查询程序拥有的账户状态。通常是流量大头。
写——提交调用程序指令的交易。改链上状态的动作。
订阅——程序拥有的账户状态变化,或程序发出日志事件时,被通知。
每一类有自己的 RPC 模式。把它们做对,就是"灵活的 app"和"卡顿的 app"的区别。
读程序状态
标准 Solana RPC 读对程序状态都管用:
// 单账户
const accountInfo = await connection.getAccountInfo(pdaPublicKey);
// 一个 round-trip 拉多账户
const accounts = await connection.getMultipleAccountsInfo([
pda1, pda2, pda3,
]);
// 程序拥有的所有账户(必须用 filter!)
const accounts = await connection.getProgramAccounts(programId, {
filters: [
{ dataSize: 165 },
{ memcmp: { offset: 0, bytes: ownerPublicKey.toBase58() } },
],
});
重要模式:
用 PDA 当主访问入口。 程序通常把状态存在 Program Derived Address 里。客户端算 PDA、取账户、反序列化。
getProgramAccounts 永远带 filter。 不带 filter 能拉几 MB 不相关数据。Filter 在服务端应用,快很多。
反序列化后的状态缓存起来。 频繁读的同一程序拥有账户,缓存。用 websocket 订阅或聪明的重取逻辑保持缓存新鲜。
批量读用 getMultipleAccountsInfo。 一个 round-trip 比 N 个强。大多数生产代码这块做得不好。
调程序指令
要执行程序指令,构建一笔含指令的交易并提交:
import { Transaction, TransactionInstruction } from "@solana/web3.js";
const ix = new TransactionInstruction({
programId: yourProgramId,
keys: [
{ pubkey: account1, isSigner: false, isWritable: true },
{ pubkey: account2, isSigner: false, isWritable: false },
],
data: serialisedInstructionData,
});
const tx = new Transaction().add(ix);
const signature = await connection.sendTransaction(tx, [signer]);
Anchor 程序,SDK 帮你处理序列化:
import * as anchor from "@coral-xyz/anchor";
const tx = await program.methods
.yourInstruction(arg1, arg2)
.accounts({
account1: pda1,
account2: pda2,
})
.signers([signer])
.rpc();
Anchor 模式友好得多;原始 web3.js 工作量大但更灵活。
账户验证
Solana 程序要求账户按对的位置传。常见 bug:
- 程序期望账户按 [A, B, C] 顺序
- 你客户端按 [A, C, B] 传了
- 交易失败,报一个有用但隐晦的错误
Anchor 通过账户验证宏在客户端抓大多数这类错。没 Anchor 你得仔细读程序 IDL 或源码。
订阅程序事件
WebSocket 订阅是反应式应用的关键:
账户变化订阅——监视特定 PDA 的状态变化:
const subId = connection.onAccountChange(
pdaPublicKey,
(accountInfo, context) => {
const decoded = deserialise(accountInfo.data);
handleStateChange(decoded);
},
"confirmed"
);
程序日志订阅——监视程序发出特定日志事件(Anchor 程序发的是结构化事件,你能解析):
const subId = connection.onLogs(
programId,
(logs, context) => {
if (logs.err === null) {
// 从 logs.logs 解析事件
}
},
"confirmed"
);
签名订阅——特定交易确认时被通知(对发完不想轮询确认的场景有用):
const subId = connection.onSignature(
signature,
(result, context) => {
if (result.err === null) {
// 已确认
}
},
"confirmed"
);
事件量大的程序,订阅可能贵(WebSocket 流量大)。可以考虑用 RPC 服务商提供的专用流式数据源。
常见的智能合约 RPC 模式
生产里行得通的模式:
乐观 UI 在 processed commitment 上、对账到 confirmed。 上链到 processed 立刻给用户显示动作成功;后台 confirmed 验证。
订阅给实时更新、轮询当备份。 订阅会断;低频轮询抓漏掉的更新。
带订阅失效的缓存读数据。 存最近状态;订阅通知触发失效。
提交前模拟交易。 抓明显 bug 不花费用。(生产发送配 skipPreflight: true——开发期间显式模拟、生产跳过。)
每笔签名级别的遥测。 你的 app 每次程序调用,记签名、指令、账户、结果。让你不重跑就能 debug 失败。
常见错误
循环 getAccountInfo 而不是 getMultipleAccountsInfo。 N 个 round-trip 而 1 个就够。
getProgramAccounts 不带 filter 调。 拉所有的;又慢又贵。
该订阅时去轮询。 反应式 app,订阅明显更高效。
忽略程序日志事件。 Anchor 程序发结构化事件;很多客户端忽略,反而去重取状态检测变化。
硬编码反序列化而不是用 IDL。 程序升级了你客户端就崩。用 IDL 生成反序列化代码。
Commitment 层级处理不对。 "processed" 上读到的数据可能后被 reorg。根据做什么挑 commitment。
程序调用不确认就发了不管。 跟任何 sendTransaction 一样——之后要确认。
Anchor 特定模式
程序是 Anchor 写的:
// 自动反序列化读程序账户
const account = await program.account.yourAccountType.fetch(pda);
// 同类型多账户带 filter 读
const accounts = await program.account.yourAccountType.all([
{ memcmp: { offset: 8, bytes: ownerKey.toBase58() } },
]);
// 监听程序事件
const listener = program.addEventListener("YourEvent", (event, slot) => {
handleEvent(event);
});
// 清理
program.removeEventListener(listener);
Anchor 的 account.fetch 处理反序列化;account.all 处理过滤查询。事件 listener 建在日志订阅之上。
给智能合约负载选 RPC
不同程序交互模式要不同 RPC:
读重应用(仪表板、分析、NFT 浏览器)。 要读优化 RPC。慷慨的查询限、快的 getProgramAccounts、相关时解析交易历史。
写重应用(DEX、交易 app、提交多笔交易的程序)。 要写优化 RPC。亚秒级确认、Anti-MEV 路由、SWQoS 支持。
混合(大多数 dApp)。 两者都用。读服务商负责查询、写服务商负责发送。
交易相关程序(DEX UI、swap 聚合器、借贷 dApp),写侧常常主导用户体验——即便读流量量更大。用户对慢 swap 比对慢页面加载敏感得多。
这周可以做什么
搭一个跟程序集成的应用:
- 能用 Anchor 就用。 账户验证和 IDL 处理省很多 bug。
- 批量读用
getMultipleAccountsInfo。 别循环。 getProgramAccounts永远带 filter。- 订阅事件、别轮询。 反应式 UI 用。
- 写侧用带 Anti-MEV 的 RPC。 涉及 swap 的程序调用是三明治被夹目标。
- 写侧加每笔签名级别的遥测。 跟踪哪些程序调用真上链了。
- 变化慢的程序状态激进缓存。 代币元数据、程序所有者等。
智能合约写侧试一下 BoltTx
涉及交易的程序调用:
import { Connection } from "@solana/web3.js";
const writeConnection = new Connection(
"https://bolttx.io/?api-key=YOUR_API_KEY",
"processed"
);
const signature = await writeConnection.sendTransaction(tx, signers, {
skipPreflight: true,
maxRetries: 0,
});
亚秒级确认、原生 Anti-MEV、SWQoS 感知的投递。配一个读聚焦的 RPC 跑查询侧。
免费档注册。用真实程序调用跑一周,对比上链率。
常见问题
不用 RPC 能读程序状态吗? 能直接从 Solana 节点读,但实践上你都是用 RPC 间接读。光为了读自托管通常经济上不划算。
该用 IDL 吗? Anchor 程序——是的,Anchor 依赖它。非 Anchor 程序,IDL 可能不存在;你得手写反序列化。
Solana 上账户最大多大? 10 MB。大多数程序账户很小(<1 KB)。更大的要 realloc。
客户端怎么处理程序升级? 升级后重取 IDL、重生成客户端代码。老数据格式可能要迁移逻辑。
一笔交易能批多个程序调用吗? 能——一笔交易多个指令。约束:总 CU 预算、总交易大小(1232 字节)、对的账户顺序。