Solana sendTransaction:生产负载的最佳实践

怎么在 Solana 上正确用 sendTransaction。skipPreflight、maxRetries、blockhash 管理、priority fee,以及决定交易能否上链的模式。

BoltTx Team··11 min read
solanasendtransactionskippreflightblockhashpriority-feerpc

如果你写过够多 Solana 代码,你发现 sendTransaction 看似简单。签名短、参数看着直白、文档暗示用默认值就 work。生产里不 work——至少不一致——大部分问题来自同一批关于 sendTransaction 实际做什么的误解。

这篇过一遍我们学到的怎么在生产正确用:重要参数、用什么模式、避免什么模式、交易没上链怎么 debug。

sendTransaction 实际做什么

朴素心智模型:"提交交易给网络等确认。"

实际发生:

  1. 客户端序列化签名交易
  2. RPC 接收、可选模拟(preflight)、决定要不要转发
  3. RPC 转发给下一个排定的 validator
  4. Validator 把它包进块(或不)
  5. 后续块确认包含

每步都有失败模式。大多数教程展示的朴素代码路径把它们都遮起来。

重要参数

相关选项:

connection.sendTransaction(tx, signers, {
  skipPreflight: false,
  maxRetries: 0,
  preflightCommitment: "processed",
});

skipPreflight。 false 时(默认),RPC 在转发前模拟交易。早抓明显失败但加延迟。生产发预验证交易、设 true——你已经客户端验证过了。

maxRetries。 大多数生产代码该设 0。默认行为是 RPC 自动重试,常静默过期 blockhash。自己用新 blockhash 管重试。

preflightCommitment。 只在 skipPreflight: false 时重要。开发反馈快用 "processed";默认大多数情况行。

大多数生产代码想要的组合:skipPreflight: true, maxRetries: 0。你管重试;你客户端模拟;RPC 只投递。

Blockhash 管理

Solana 交易针对特定最近 blockhash 签名。Blockhash 大约 60-90 秒后过期。过期后交易不可处理。

常见错误:

多次重试用同一 blockhash。 提交、没响应、5 秒后重提是 OK 的。如果你用同一 blockhash 重试了 90+ 秒,你在静默提交过期交易。

太早取 blockhash。 用 blockhash 预构建交易然后等 60 秒再签名,意味着 blockhash 已经老了。

不处理"blockhash not found"错误。 如果你针对已被回收的 blockhash 模拟、会得到这个错误。重取重建。

忘了 processed-commitment blockhash 不太稳定。 短分叉能让它失效。生产用 confirmed-commitment blockhash 安全;processed-commitment 只在你优化每毫秒时用。

行得通的模式:

// 取新 blockhash、构建 tx、签名、发送——都在几百毫秒内
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed");
tx.recentBlockhash = blockhash;
tx.feePayer = payer.publicKey;

// sendTransaction 第二个参数传 signers,内部完成签名
const signature = await connection.sendTransaction(tx, [payer], {
  skipPreflight: true,
  maxRetries: 0,
});

// 需要重试就用新 blockhash 构建新交易
// 别复用同一 blockhash

Priority Fee 和 Compute Unit

两个决定你交易能否赢得包含的参数:

Priority fee。 链上的每 CU 费用,作用是把你交易推到打包队列前列。给得越高 = 越可能快被打包。

Compute unit budget。 你交易允许消耗的最大 CU。设太低交易失败;设太高浪费 priority fee 预算。

生产发送:

import { ComputeBudgetProgram, Transaction } from "@solana/web3.js";

const tx = new Transaction()
  .add(ComputeBudgetProgram.setComputeUnitLimit({ units: 250_000 }))
  .add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100_000 }))
  .add(yourActualInstruction);

重试逻辑做对

朴素重试循环:

// 别这么做
for (let i = 0; i < 5; i++) {
  try {
    const sig = await connection.sendTransaction(tx, signers);
    return sig;
  } catch (e) {
    await sleep(500);
  }
}

问题:

行得通的模式:

// 更好:用新 blockhash 给重试构建新交易
async function sendWithRetry(builder, connection, signers, maxAttempts = 2) {
  for (let i = 0; i < maxAttempts; i++) {
    const { blockhash, lastValidBlockHeight } =
      await connection.getLatestBlockhash("confirmed");
    const tx = builder(blockhash);  // builder 给每次尝试产新 tx
    tx.sign(...signers);

    try {
      const sig = await connection.sendTransaction(tx, signers, {
        skipPreflight: true,
        maxRetries: 0,
      });

      // 用 connection.confirmTransaction 等确认
      const status = await connection.confirmTransaction(
        { signature: sig, blockhash, lastValidBlockHeight },
        "confirmed"
      );
      if (status.value?.err === null) return sig;
    } catch (e) {
      // 日志、决定要不要继续重试
    }
  }
  throw new Error("重试后失败");
}

关键原则: 同一机会上别重试超过两次。两次后要么机会没了、要么有更深问题。

确认策略

sendTransaction 返回签名后,交易可能上链也可能没。要确认:

// 轮询方法
const status = await connection.confirmTransaction(signature, "confirmed");

// 或开发反馈快:
const status = await connection.confirmTransaction(signature, "processed");

"processed" commitment 最快但短分叉能反转。开发反馈行,生产财务决策有风险。

"confirmed" commitment 是生产默认。三分之二 validator 投了;实际终结。

"finalized" commitment 等完全终结。慢但最安全。给 reorg 风险有意义的高价值交易用。

延迟敏感机器人有时想在 processed 上动作(如触发下游订单)、后台对账到 confirmed。别假装 processed 终结,但策略能容忍 reorg 风险时也别等 finalized

常见 sendTransaction 失败模式

按频率大致排:

Blockhash 过期。 针对老 blockhash 提交。RPC 可能或可能不暴露这个——有时静默丢弃。

滑点超过。 Swap 交易,价格移动过容差。链上失败。你仍付费。

Compute unit 超过。 交易需要的 CU 比预算多。部分执行后失败。你为消耗的 CU 付费。

Priority fee 太低。 交易卡队列里、blockhash 在被包含前过期。

Account not found。 经常是还没存在的 ATA。预创建或在交易里加创建并配好 CU 预算。

余额不够。 听起来显而易见,但加上 priority fee 和(可选的)Jito tip 之后容易忽略。

交易太大。 Solana 1232 字节限。多跳 swap 和 ALT 解析指令能撞到。

Nonce / blockhash 竞争。 罕见但可能——你交易处理了但重复也上链。应用层幂等防真损害。

这周可以做什么

如果你在改善代码库 sendTransaction 可靠性:

  1. skipPreflight: true, maxRetries: 0 当默认。 显式管重试。
  2. 审重试逻辑。 如果重试超过两次、你有更深问题。
  3. profile 真实交易 CU 用量。 设 1.2-1.5x 测量的显式预算。
  4. 按利润动态调 priority fee。 基于交易预期值改变给的数额。
  5. 搭每笔签名级别的遥测。 每笔提交交易、日志签名、blockhash、尝试次数、延迟、结果。让你能 debug 失败。
  6. 用带投递遥测的 RPC。 交易静默失败时、"它发生什么"需要可回答。

BoltTx 提供什么

BoltTx 为生产 sendTransaction 负载做:

import { Connection } from "@solana/web3.js";

const connection = new Connection(
  "https://bolttx.io/?api-key=YOUR_API_KEY",
  "processed"
);

const signature = await connection.sendTransaction(tx, signers, {
  skipPreflight: true,
  maxRetries: 0,
});

免费档注册。用真实负载跑一周对比上链率、P95 延迟、被夹敞口和当前配置。

常见问题

生产用 skipPreflight: true 吗? 预验证交易、是。去掉一个 round-trip 和少量延迟。如果交易不一定 well-formed 别用。

maxRetries 对的值是什么? 生产 0。自己用新 blockhash 管重试。

重试间隔多久? 够长不重复在飞(~500ms-2s)、够短机会没过期。最多两次。

sendRawTransaction vs sendTransaction? sendRawTransaction 低一层——你自己序列化。略快、几乎察觉不到。用你 SDK 暴露最干净的。

为什么交易上链但亏钱? 通常被夹敞口。对比 AMM 数学预期输出和实际成交。系统性差距是被夹了。Anti-MEV RPC 路由关掉。

延伸阅读

返回博客列表