你的仓位账本,和链上说的不一样

机器人的内部记录会以特定且可预测的方式偏离链上现实。这些差距从哪来,以及怎么对账。

BoltTx Team··13 min read
solana仓位追踪对账记账交易机器人交易上链

你的机器人认为自己持有 1,000 个代币。链上说是 987。 两个数字都不是通常意义上的 bug —— 它们是在每一个单独步骤都正确的情况下分岔的。

★链是唯一的权威。你的账本是它的一份缓存,而每一份缓存都会漂移。★

漂移从哪里来

原因 方向
★记录了意图,而不是结果★ ★账本偏高★
★转账费被扣下★ ★账本偏高★
滑点 —— 成交得比预期差 账本偏高
★revert 的交易被记成成交★ ★账本偏高★
没记录的手工交易 账本偏低
★你放弃之后才落块的重试★ ★账本偏低★

★注意大部分原因都朝同一个方向推。★ 一个记录"意图"而不是"结果"的机器人,会累积出一个乐观的仓位 —— 而那是更糟的错误:你会去卖你根本没有的代币。

记录结果,不是意图

// ★错:签名只意味着 RPC 接收了字节。★
const sig = await connection.sendRawTransaction(raw);
positions.add(mint, expectedAmount);

// ★对:读实际动了什么。★
const outcome = await resolve(sig, lastValidBlockHeight);
if (outcome.status !== "success") return;

const tx = await connection.getTransaction(sig, { maxSupportedTransactionVersion: 0 });
const delta = tokenDelta(tx.meta, myTokenAccount);   // ★来自 pre/post 余额★
positions.add(mint, delta);

★preTokenBalances 和 postTokenBalances 是"什么变了"的权威记录★,而且它们自动涵盖一切 —— 滑点、转账费,以及 CPI 内部做的任何事。

用它们一次性消除掉六个漂移原因里的四个,因为你不再预测,而是开始读取。

如果你的记账是对的、交易还是会错过,免费领个 BoltTx key 改一行就能测测提交路径。

三终态规则在这里同样适用

switch (outcome.status) {
  case "success":  applyDelta(readDeltaFromMeta(sig)); break;
  case "reverted": recordFeeOnly(sig);                 break;   // ★仓位不变★
  case "expired":  recordNothing();                    break;   // ★从没执行★
}

★一笔 revert 的交易落了块、花了手续费,但没有改变任何仓位。★ 把它记成成交,是漂移最常见的单一来源 —— 而且它会在很多笔交易上静默复利。

过期的交易是相反的陷阱 —— 它从没执行,所以把它记成失败是对的,但前提是你确认了过期,而不是从一个超时推断出来的。

按计划对着链上核对

async function reconcile(connection, wallet) {
  const [classic, t22] = await Promise.all([
    connection.getParsedTokenAccountsByOwner(wallet, { programId: TOKEN_PROGRAM_ID }),
    connection.getParsedTokenAccountsByOwner(wallet, { programId: TOKEN_2022_PROGRAM_ID }),
  ]);

  const onChain = new Map();
  for (const { account } of [...classic.value, ...t22.value]) {
    const info = account.data.parsed.info;
    onChain.set(info.mint, BigInt(info.tokenAmount.amount));   // ★字符串,精确★
  }

  for (const [mint, believed] of ledger) {
    const actual = onChain.get(mint) ?? 0n;
    if (actual !== believed) await flagDiscrepancy(mint, believed, actual);
  }
}

★有两个细节决定它是"正确"还是"大致正确"。★

两个代币程序都要查。 只查一个程序会静默漏掉 Token-2022 的余额,而那读起来像是一个丢失的仓位,而不是一次不完整的查询。

用 bigint 在原始单位上比较。 用 uiAmount 会在 2^53 以上引入浮点误差,在大额余额上产出幽灵差异,同时掩盖真实的差异。

差距的方向指向哪里

★差异的正负号告诉你该往哪看,而这比它的大小更有用。★

账本高于链上: 你记录了一件没发生的事。 去找被记成成交的 revert 交易,或者"在结果之前就记录了意图"。

账本低于链上: 发生了一件你没记录的事。 ★去找一个你放弃之后才落块的重试,或者一笔由你的密钥签名、而你的机器人没有发起的交易。★

第二种情况值得一个告警,而不是一次修正。 一个你没创建过的仓位有一种良性解释 —— 迟到的重试 —— 和一种不良性的,而对账正是你搞清楚是哪一种的地方。

SOL 要单独对账

原生 SOL 需要它自己的处理方式,因为手续费从同一份余额里出:

const lamports = await connection.getBalance(wallet);
const expected = lastKnown - feesSpent + inflows - outflows;

★一个和你的手续费开销恰好吻合的 SOL 差异,不是差异★ —— 那是你的记账没有把手续费减掉。把手续费作为单独一行追踪,这样交易变动和手续费变动才可区分。

另外记得 wrapped SOL 是一个代币账户,不属于这份余额。一个会包装和解包的机器人需要两者都追踪,而且要知道它们不同步变动。

出现差异时该怎么办

★永远不要用链上的值静默覆盖你的账本。★ 那修好了数字,却销毁了"它为什么错"的证据。

保住信息的处理顺序:

1. 把两个值和时间戳都记下来。 差异的历史,是你找到系统性原因的方式。

2. 尝试归因。 取最近的签名,找一笔你的账本不知道的交易。

3. 归因不了就告警。 ★一个你解释不了的差距,和一个你能解释的,是两个不同的问题。★

4. 然后再修正,以链为权威,并留下这次修正的记录。

5. 差距很大就停掉那个仓位的交易。 对着一个你无法核实的仓位交易,正是小的记账错误变成大错误的方式。

上链应该是什么水平

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

★可预测的落块,缩小了"一笔交易命运不明"的那个含糊窗口。★ 大多数记账漂移,都始于一笔结果不明的交易 —— 不明得足够久,久到有人记下了一个猜测。

BoltTx 在这里的位置

我们负责提交。仓位追踪和对账完全留在你的代码里。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。

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

免费领 API key,没有月费:

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

常见问题

我机器人的仓位为什么和链上不一样? 通常是因为它记录的是意图而不是结果。滑点、转账费、revert 的交易,都会让实际结果和预期不同。

怎么记录一笔交易实际做了什么? 从交易 meta 里读 preTokenBalances 和 postTokenBalances。它们反映真实结果,包括 CPI 影响、滑点,以及转账时被扣下的任何费用。

拿到签名时该更新账本吗? 不该。签名只意味着 RPC 接收了字节。 要在收敛到终态、并读出实际余额变化之后再更新。

记账里该怎么处理 revert 的交易? 记手续费,仓位不变。 把 revert 记成成交是漂移最常见的单一来源,而且它会静默复利。

多久该对着链上核对一次? 频繁到能在几分钟而不是几天内发现差异。 任何持续交易的系统,每几分钟查一次都是有益的。

我的对账为什么漏掉一些代币? 多半是因为你只查了一个代币程序。Token-2022 的余额需要单独查,而它们的缺席读起来像一个丢失的仓位。

余额该按数字还是字符串比较? 按原始金额字符串转成 bigint。 用 uiAmount 会在 2^53 以上引入浮点误差,制造幽灵差异并掩盖真实差异。

账本高于链上意味着什么? 你记录了一件没发生的事。 去找被记成成交的 revert 交易,或者"在提交时而不是在结果时"更新的仓位。

账本低于链上意味着什么? 发生了一件你没记录的事。 通常是一个你放弃之后才落块的重试,但也可能是一笔由你的密钥签名、而机器人没有发起的交易。

该自动修正差异吗? 不要静默修正。 先把两个值都记下来、尝试归因、解释不了就告警,然后再以链为权威修正,并留下记录。

什么时候差异该停掉交易? 当它大到"继续交易可能放大这个错误"的时候。 对着一个你无法核实的余额交易,会把小问题变成大问题。

我的 SOL 余额为什么从来对不上? 因为手续费和交易从同一份余额里出。把手续费开销作为单独一行追踪,否则每一笔手续费看起来都像一个无法解释的差异。

wrapped SOL 算在我的 SOL 余额里吗? 不算,它是一个独立的代币账户。两者不同步变动,所以会包装和解包的机器人必须分别追踪。

转账费怎么影响仓位追踪? 到账的比发出的少,所以一个记录"发出金额"的账本会永久偏高。读取交易后的余额,能自动处理这一点。

能只靠链上历史重建仓位吗? 大部分可以,用签名和余额差额 —— 但只限于已落块的交易。关于意图、以及过期掉的尝试,只存在于你自己的日志里。

最有价值的对账告警是什么? 你的账本解释不了的链上活动。 它有一种良性成因和一种不良性成因 —— 而那正是值得把人叫起来的那个区分。

延伸阅读

← 返回博客列表