Solana WebSocket 订阅:onLogs、账户变更,以及那些坑

怎么订阅 Solana 账户和程序日志才不漏事件。承诺级别、断线重连,以及为什么订阅不能代替验证。

BoltTx Team··14 min read
solanawebsocketonlogs订阅交易机器人rpc

定时轮询 RPC 一直够用 —— 直到你需要"某件事发生的那一刻就知道"。然后你换成 WebSocket 订阅,接着发现了另一批问题。

订阅确实更快。但它也会静默丢事件,而轮询不会;而订阅代码里的大多数 bug,都源于把"推送"当成了"保证"

值得知道的四种订阅

Solana 提供了好几种,但这四种覆盖几乎所有真实场景:

onAccountChange(对应 accountSubscribe 这个 RPC 方法)—— 某个特定账户的数据变化时触发。适合盯一个池子、一条曲线、或一个代币账户。

onProgramAccountChange(对应 programSubscribe 这个 RPC 方法)—— 某个程序拥有的任意账户变化时触发,可加过滤。很强大,也很容易变得很贵。

onLogs —— 某个程序或地址出现在交易日志里时触发。这是大多数交易机器人在用的,因为它能捕捉到那些你事先猜不到账户地址的活动。

onSignature —— 某个特定签名达到承诺级别时触发一次。适合做确认,不适合做发现。

承诺级别是第一个决策

每个订阅都要选一个承诺级别,而这是个真实的取舍,不是一个可以照单全收的默认值。

connection.onLogs(
  programId,
  (logs) => handle(logs),
  "processed", // 对比 "confirmed" 和 "finalized"
);
级别 速度 风险
processed ★最快★ 可能被回滚
confirmed 慢一个以上 slot 很少被回滚
finalized 慢很多 slot 基本永久

★对时间敏感的场景用 processed,然后在动钱之前验证。★ 等 confirmed 就是在等你正在抢的那个东西。代价是你看到的东西里有一小部分可能活不下来 —— 而这靠验证来处理,不是靠等。

而任何要写进你自己数据库当作真相的东西,用 confirmed 或更高。

如果订阅已经跑通、问题在发送侧,免费领个 BoltTx key 改一行就能拿来做对比。

断线重连的问题

WebSocket 连接会断。不是偶尔 —— 是经常:网络抖动、服务商重启、空闲超时。

客户端的默认行为和大多数人以为的不一样:

// 这样写看着没问题,断线之后就静默地不工作了。
connection.onLogs(programId, handler, "processed");

★socket 一死,你的 handler 就再也不会被调用。没有报错,没有异常 —— 只有沉默。★ 一个机器人可以连着几小时什么都没收到,而看起来一切正常。

解法是一个**不依赖"事件有没有到"**的存活检查 —— 因为当你盯的东西本来就安静时,"没有事件"和"连接已死"是分不出来的:

let lastEventAt = Date.now();
let subId: number | null = null;

async function subscribe() {
  subId = connection.onLogs(
    programId,
    (logs) => {
      lastEventAt = Date.now();
      handle(logs);
    },
    "processed",
  );
}

// 独立心跳:用一个结果可预期的调用来证明连接还活着,
// 而不是靠等事件。
setInterval(async () => {
  try {
    await connection.getSlot("processed");   // socket 没了就会失败
    if (Date.now() - lastEventAt > 120_000) {
      // 一个本该繁忙的程序安静了两分钟。
      // 重新订阅,别默认网络就是闲的。
      if (subId !== null) await connection.removeOnLogsListener(subId);
      await subscribe();
    }
  } catch {
    if (subId !== null) await connection.removeOnLogsListener(subId).catch(() => {});
    await subscribe();
  }
}, 30_000);

日志不是数据

onLogs 给你的是日志行和一个签名。它不给你权威状态

connection.onLogs(programId, async (logs) => {
  // 错:从日志字符串里解析金额。
  // 日志格式不是稳定接口,不同版本之间会变。
  const amount = parseAmountFromLogs(logs.logs);

  // 对:把日志当触发器,然后去读状态。
  const account = await connection.getAccountInfo(derivedPda);
  const state = deserialize(account.data);
});

★把订阅当通知,别当数据本身。★ 日志告诉你"发生了某件事",账户告诉你"事实是什么"。

这一点在日志格式变化时最要命 —— 程序升级时会变,而且解析器是静默地坏掉,不是响亮地报错。

订阅在什么时候变得很贵

在一个繁忙的程序上不加过滤地用 onProgramAccountChange,会把你淹了。该程序拥有的每个账户、每次变化,全推给你的进程。

// 贵:该程序下的每一个账户。
connection.onProgramAccountChange(programId, handler);

// 好一点:在服务端过滤,只接收你需要的。
connection.onProgramAccountChange(
  programId,
  handler,
  "processed",
  [
    { dataSize: 165 },                                  // 只要这个布局
    { memcmp: { offset: 32, bytes: ownerPubkey.toBase58() } },
  ],
);

★过滤是在服务商那一侧执行的,所以不加过滤的订阅在两端都要付带宽和 CPU。★ 在共享档上,这也是最快撞到限流的方式。

订阅对"发送"没有帮助

这一点值得明说,因为它是个常见误解。

WebSocket 订阅让你更早知道事件发生了。它对"你自己的交易多快到达出块方"没有任何帮助 —— 因为提交是一次 HTTP 调用,走的是另一条路。

检测   → WebSocket 订阅在这里起作用
提交   → ★和你的订阅配置完全无关★

这就是为什么很多生产环境用一家做流式推送、另一家专做投递。两种负载想要的东西不同,而集成点互相独立。

★一个检测快到极致、提交路径却很慢的机器人,照样迟到。★

上链应该是什么水平

我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。

如果你的检测是瞬时的、却稳定在事件发生后四个以上 slot 才落块,差距在发送侧,不在订阅代码里。

BoltTx 在这里的位置

我们不做订阅。我们做提交。

BoltTx 把签好名的交易路由到我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前,传输途中观察不到。检测那一半,你配一家适合自己需求的流式服务商就行,两个集成点互相独立。

你在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时 tip 跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。

免费领 API key,没有月费:

// 检测那边不用动,只换发送端点。
const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

常见问题

Solana 的 WebSocket 订阅是什么? 一条持久连接,由 RPC 把事件推给你,而不是你去轮询。常用的有 onAccountChangeonProgramAccountChangeonLogsonSignature

订阅该用哪个承诺级别? 时间敏感的检测用 processed,因为等 confirmed 会花掉一个以上 slot。任何要当作真相记录下来的东西,用 confirmedfinalized —— processed 的结果可能被回滚。

我的 Solana WebSocket 订阅为什么不触发了? 连接断了,而你的 handler 再也没被调用。不会抛错,所以这是静默失败。 加一个独立心跳来验证存活,而不是靠"有没有收到事件"来推断。

怎么检测 Solana WebSocket 断线了? 别靠事件沉默来判断 —— 一个安静的程序和一个死掉的 socket 看起来一模一样。定时调一个结果可预期的接口,失败时重订阅;或者当安静时间超过该程序合理的沉默上限时也重订阅。

onLogs 可靠到能拿来交易吗?触发器可以,当数据源不行。用日志知道"发生了什么事",然后读账户拿权威状态。日志格式在程序升级时会变,而且是静默地把解析器搞坏。

onLogs 和 onProgramAccountChange 有什么区别? onLogs 在程序出现在交易日志里时触发,能捕捉到那些你事先猜不到账户的活动。onProgramAccountChange 在该程序拥有的账户变化时触发,更精准,但前提是你知道该盯什么。

怎么避免被 onProgramAccountChange 淹掉? 加过滤 —— dataSizememcmp —— 让服务商只发匹配的账户。在繁忙程序上不加过滤会把一切都推过来,两端都要付带宽,还会烧掉限流额度。

WebSocket 订阅算不算进我的限流额度? 通常和 HTTP 请求算法不同,而且各家政策不一样。但有一点是共通的:不加过滤的高流量订阅,是最快撞到任何限制的方式

能用 WebSocket 订阅来发送交易吗? 发送是 HTTP 调用,不是订阅。有些客户端会开一条 WebSocket 接收确认通知,但提交本身走的是另一条路 —— 这就是为什么检测速度和提交速度是两个独立的问题。

为什么我的机器人瞬间就检测到事件,交易还是晚? 因为检测和提交互相独立。订阅让你更早知道,但对"交易多快到达出块方"毫无作用。把检测时的 slot 和落块时的 slot 都记下来,就知道是哪一半在拖后腿。

一条连接上能开多少个订阅? 实际上比大多数机器人需要的多,不过各家有自己的上限。真正的约束通常是流经的事件量,而不是订阅的数量。

订阅和发送该用两家服务商吗? 很多生产环境就是这么配的。流式推送要吞吐和灵活的过滤;发送要一条到出块方的短路径。用一个端点同时优化两者,意味着两边都要妥协,而集成点本来就是独立的。

WebSocket 重连期间的事件会怎样? 丢了。没有重放缓冲区。 如果漏事件不可接受,重连之后用一次轮询查询把中间那段补回来。

onSignature 对交易机器人有用吗? 用来确认你自己发出去的交易,有用 —— 它触发一次然后自动取消订阅。用来发现新活动,没用,因为你得事先知道签名。

怎么测试重连逻辑是对的? 把跑机器人那台机器的网络断三十秒,然后验证它有没有重订阅并恢复。等真实断线来测,等于在你最在意的时段才发现问题。

延伸阅读

返回博客列表