Solana 智能合约 RPC 指南:调程序、读状态、做订阅

怎么从应用跟 Solana 程序(智能合约)交互。读账户状态、发指令、订阅变化的 RPC 模式。

BoltTx Team··11 min read
solana智能合约程序rpcanchor开发者

在 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:

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 比对慢页面加载敏感得多。

这周可以做什么

搭一个跟程序集成的应用:

  1. 能用 Anchor 就用。 账户验证和 IDL 处理省很多 bug。
  2. 批量读用 getMultipleAccountsInfo 别循环。
  3. getProgramAccounts 永远带 filter。
  4. 订阅事件、别轮询。 反应式 UI 用。
  5. 写侧用带 Anti-MEV 的 RPC。 涉及 swap 的程序调用是三明治被夹目标。
  6. 写侧加每笔签名级别的遥测。 跟踪哪些程序调用真上链了。
  7. 变化慢的程序状态激进缓存。 代币元数据、程序所有者等。

智能合约写侧试一下 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 字节)、对的账户顺序。

延伸阅读

返回博客列表