查询 Solana 的历史交易

RPC 到底能往回查多久、用 before 和 until 怎么翻页,以及为什么对账必须靠你自己存记录。

BoltTx Team··13 min read
solana历史getSignaturesForAddress对账rpc记账

每一笔 Solana 交易都被永久记录下来。但这不代表你的 RPC 会把它给你。

★标准 RPC 节点会裁剪旧数据。链记得,而你正在查询的那个端点通常不记得。★ 团队总是在最糟的时候发现这件事 —— 在做月末对账、然后发现最早那些交易全返回 null 的时候。

两个调用,两件事

找某个地址的签名:

const sigs = await connection.getSignaturesForAddress(wallet, {
  limit: 1000,                    // ★单次上限★
});

sigs.forEach((s) => {
  console.log(s.signature, s.slot, s.blockTime, s.err ? "失败" : "成功");
});

取某一笔交易的详情:

const tx = await connection.getTransaction(signature, {
  maxSupportedTransactionVersion: 0,   // ★不加的话版本化交易返回 null★
});

★这个选项在实践中不是可选的。★ 不加它,任何 v0 交易都会返回 null —— 而这读起来像"这笔交易不存在",而不是"你的客户端没声明能解析这种格式"。

向后翻页

getSignaturesForAddress 从新到旧返回,单次上限 1000 条。想往更早翻,用 before:

async function allSignatures(address, connection, stopAtSlot = 0) {
  const out = [];
  let before = undefined;

  while (true) {
    const page = await connection.getSignaturesForAddress(address, {
      limit: 1000,
      before,                        // ★是签名,不是 slot★
    });
    if (!page.length) break;

    out.push(...page.filter((s) => s.slot >= stopAtSlot));
    const last = page[page.length - 1];
    if (last.slot < stopAtSlot) break;

    before = last.signature;         // ★从这里继续★
    await sleep(200);                // ★这个调用很重★
  }
  return out;
}

beforeuntil 收的是签名,不是 slot。★ 传一个 slot 号会静默地返回没用的东西 —— 这是个让人困惑的失败,因为调用本身是成功的。

until 才是增量同步真正该用的那个。 存下你处理过的最新签名、把它作为 until 传进去,每次运行就只取新增的部分,而不是把历史重走一遍。

如果你的历史查询没问题、问题在交易落不了块,免费领个 BoltTx key 改一行就能测测提交路径。

你实际能往回查多久

这部分决定了你的架构:

节点类型 典型保留时长
标准 RPC ★只有最近的 slot★
扩展历史套餐 数月
Archive 节点 ★完整历史★
你自己的数据库 ★你存了多少就有多少★

★一个只保留近期历史的标准端点是默认状态,不是缺陷。★ 存完整链数据很贵,所以大多数服务商会裁剪,并把更深的历史作为单独一档来卖

对任何涉及财务的事情,后果是:你不能拿 RPC 当你的记录系统。 一份税务报表、一份盈亏表、或者六个月后的一次客户争议,要的都是你自己保留下来的数据

// ★边跑边存,不要等到需要时才去取。★
await db.transactions.insert({
  signature: sig,
  slot: outcome.slot,
  blockTime: outcome.blockTime,
  err: outcome.err,
  meta: extractWhatYouNeed(tx),
});

从 meta 里读余额变化

一笔历史交易里最有用的部分不是指令 —— 而是到底什么东西动了:

const pre  = tx.meta.preTokenBalances ?? [];
const post = tx.meta.postTokenBalances ?? [];

for (const p of post) {
  const before = pre.find((b) => b.accountIndex === p.accountIndex);
  const delta =
    BigInt(p.uiTokenAmount.amount) -
    BigInt(before?.uiTokenAmount.amount ?? "0");
  if (delta !== 0n) console.log(p.mint, delta);
}

preTokenBalancespostTokenBalances 不用解码任何一条指令,就告诉你什么变了。★ 对账时它比解析指令数据可靠得多,因为它反映的是真实结果,包括 CPI 内部发生的一切

原生 SOL 用 preBalancespostBalances,按账户索引对应。注意手续费是从支付方的差额里出的 —— 所以 accountKeys[0] 的差额里含着它。

失败的交易也在里面

getSignaturesForAddress 会把失败交易和成功交易一起返回,err 区分:

const failed = sigs.filter((s) => s.err !== null);

★一笔失败的交易照样落了块、照样消耗了基础手续费。★ 忽略它们的对账对不上钱包余额,因为那些手续费花在了什么都没改变的交易上。

注意什么在这个列表里:从没落块的交易。它们在任何地方都没留下记录 —— 这正是你自己的日志重要的原因:链没法告诉你一次过期掉的尝试。

限流与这个调用

这两个方法都很重,而历史回填是耗尽配额的常见方式:

有意识地翻页。 一次 limit: 1000 是一个请求;为了充实它而发的一千次 getTransaction 是另外一千个

只对需要的取详情。 签名列表本身已经带了 slot、区块时间和错误状态。如果这就够了,就不要去取完整交易。

增量运行用 until ★每次运行都把同一段历史重走一遍,是回填撞上限流最常见的单一原因。★

回填和交易分开。 历史任务和机器人共用一个端点,意味着回填可以把发送饿死

上链应该是什么水平

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

★只有落了块的交易才有历史可言。★ 一笔过期的交易在链上不留痕迹,所以你提交路径的可靠性,决定了你的活动中有多少是日后还能被重建出来的。

BoltTx 在这里的位置

我们负责提交,不做索引也不做历史。你查询用什么,继续用什么。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。因为端点互相独立,一次历史回填耗尽不了你交易发出去的那条路径。

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

免费领 API key,没有月费:

const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

常见问题

Solana 交易能往回查多久? 在标准 RPC 上只有近期历史,更旧的会被裁剪掉。要完整历史得用 archive 节点,或者一家提供扩展保留时长的服务商套餐。

getTransaction 查一笔旧交易为什么返回 null? 要么节点把它裁剪了,要么你漏了 maxSupportedTransactionVersion: 0 而它是版本化交易。先查后者,再去假设前者。

getSignaturesForAddress 怎么翻页? 把上一页最后一条签名作为 before 传进去。它从新到旧返回、单次最多 1000 条,所以往更早翻意味着重复调用。

before 和 until 有什么区别? before 从某个签名往回走,until 在某个签名处停下。增量同步用 until 配你处理过的最后一条签名,这样只取新增的。

能给 before 或 until 传 slot 号吗? 不能,两个都要签名。传 slot 会返回没用的东西、而调用本身还是成功的 —— 这让它成为一个很难诊断的失败。

怎么看一笔交易实际动了什么? 比对 meta 里的 preTokenBalancespostTokenBalances,原生 SOL 比对 preBalancespostBalances它反映的是真实结果,包括 CPI 造成的影响。

手续费会出现在余额差额里吗? 会,在手续费支付方的原生余额变化里accountKeys[0] 的差额包含手续费,所以把这笔变动归因到某次交易时要把它减掉。

签名列表里包含失败的交易吗? 包含,带着 err。它们落了块并消耗了基础手续费,所以跳过它们的对账会对不上钱包余额。

能找到从没落块的交易吗? 不能。一笔过期的交易在链上任何地方都不留记录 —— 这就是你自己的提交日志是那些尝试唯一来源的原因。

能拿 RPC 当记账记录吗? 不能。标准节点会裁剪,所以任何几个月后还要用的东西,都必须在它发生时就存下来。把 RPC 当实时数据源,不要当记录系统。

怎么回填历史又不撞限流? 有意识地翻页、只在签名元数据不够时才取完整交易详情,并用 until 让每次运行只覆盖新增活动。

不取详情的话,签名列表里有什么? 签名、slot、区块时间、错误状态和 memo。如果这就回答了你的问题,就完全跳过 getTransaction,每条签名省一个请求。

我的对账为什么和钱包余额对不上? 常见原因是:忽略了照样付了手续费的失败交易、漏算了支付方差额里的手续费,或者 RPC 在你存下来之前就把那段历史裁掉了。

blockTime 总是有值吗? 通常有,但某些更早的条目可能是 null。slot 永远存在而且单调递增,所以排序优先用它,展示再用区块时间。

一次调用最多返回多少条签名? 1000 条。超出就必须用 before 翻页,这让深度历史变成一串请求,而不是一次查询。

历史查询该和我的机器人共用端点吗? 最好不要。回填很重,能把共享配额耗尽 —— 而那恰恰会在机器人最需要发送的时候把它饿死。

延伸阅读

返回博客列表