发送之前预估 Solana 交易手续费

getFeeForMessage 涵盖什么、漏掉什么,为什么预估是给用户看的而不是拿来发送的,以及那笔根本不算手续费的租金。

BoltTx Team··13 min read
solana手续费预估优先费租金交易上链

给用户展示一笔交易要花多少钱,看起来该是一次调用。它是三个组成部分,而其中只有一个是固定的。

★预估是你展示出来的数字。而你发送时用的那个数字,必须在发送那一刻计算 —— 两者不是一回事。★

三个组成部分

基础手续费      ★每个签名 5,000 lamport —— 固定★
优先费          ★compute unit price × compute unit limit★
租金            ★只有创建账户时才有 —— 而且它不是手续费★

基础手续费是唯一可预测的那个。 它按签名计,所以一笔单签名交易不管做什么都是 5,000 lamport。

优先费是一个乘积,不是一个值。 人们引用价格,却忘了它要乘以你请求的上限 —— 这意味着一个虚高的上限会按比例抬高手续费。

租金根本不是手续费。 它是被锁住、并在账户关闭时退回的,所以一份把它和手续费混在一起的成本预估,歪曲了用户实际花掉的东西。

getFeeForMessage 涵盖什么

const message = new TransactionMessage({
  payerKey: payer.publicKey,
  recentBlockhash: blockhash,
  instructions,
}).compileToV0Message();

const { value: fee } = await connection.getFeeForMessage(message);

★它返回的是基础手续费,加上 message 里已有的 compute budget 指令所隐含的优先费。★

两个会绊住人的后果:

它不会替你加优先费。 如果你没放 setComputeUnitPrice 指令,结果就只是基础手续费 —— 它看起来便宜得让人安心,而这不是你加上优先费之后要付的。

它不含租金。 账户创建成本对它是不可见的,所以一笔要创建代币账户的 swap,实际花费会明显高于这个调用报告的数字。

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

预估优先费

输入必须是你将要争用的那些账户,不是笼统的全网:

const fees = await connection.getRecentPrioritizationFees({
  lockedWritableAccounts: writableAccounts,   // ★具体的,不是全局的★
});

const sorted = fees.map((f) => f.prioritizationFee).sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
const p75 = sorted[Math.floor(sorted.length * 0.75)] ?? median;

★手续费压力是按账户的。★ 一个清淡市场几乎不需要付什么,而一个波动事件里被争抢的池子是完全不同的处境 —— 而全网平均值哪一个都描述不了。

上限那一半来自模拟:

const sim = await connection.simulateTransaction(tx, {
  replaceRecentBlockhash: true, sigVerify: false,
});
const limit = Math.ceil((sim.value.unitsConsumed ?? 200_000) * 1.2);
const priorityFee = Math.ceil((limit * microLamportsPerCu) / 1_000_000);

★留着默认上限,意味着按一个远高于真实用量的数字付费。★ 按 unitsConsumed 设置它,是少数几个"降低成本却不降低竞争力"的改动。

预估用于展示,发送要现算

这是运维上真正要紧的区分:

// ★展示:一个区间,算一次,给人看。★
const estimate = {
  low:  baseFee + feeAt(median),
  high: baseFee + feeAt(p95) + rentIfCreating,
};

// ★发送:每一次都在发送那一刻现算。★
const sendFee = clamp(feeAt(currentMedian) * multiplier, FLOOR, CEILING);

★一份报出去的预估,在你展示它的那一刻就已经陈旧了。★ 手续费按 slot 移动,所以一个花十秒钟去确认的用户,确认的是一个已经不再描述这个市场的数字。

展示一个区间,而不是一个点。 单个数字会招来"实际花费不一样"的抱怨;一个区间设定的预期,能扛住正常波动。

这能防住的那个失败

预留不足,是糟糕预估的实际后果:

const required = amount
  + baseFee
  + worstCasePriorityFee      // ★不是中位数★
  + rentIfCreatingAccounts
  + rentExemptMinimum;        // ★必须留在账户里★

★按中位数手续费预留,意味着在尖峰时失败★ —— 而那恰恰是这笔交易重要到被人争抢的时候。一个按典型条件做预算的机器人,会在最糟的时刻用光。

该怎么告诉用户

组成部分 该怎么展示
基础手续费 ★固定,微不足道★
优先费 ★一个区间,随拥堵变化★
租金 ★押金,可收回★
滑点 ★不是手续费,但是真实成本★

★把租金说成手续费,会让你的产品为一笔用户能拿回去的钱显得很贵。★ 把它标成可退押金,既更准确也更不吓人。

滑点虽然不是手续费,但属于成本讨论 —— 对一笔容忍度很宽的交易,它可以超过所有手续费之和,而一份省略它的成本预估,描述的是更小的那一半。

上链应该是什么水平

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

★手续费买的是位置,不是确定性。★ 一份准确的预估告诉用户一次成功的尝试要花多少;它没说一个很宽的落块分布会需要多少次尝试。

BoltTx 在这里的位置

我们负责提交。手续费策略完全留在你的代码里 —— 我们不修改交易内容,这包括绝不调整你的 compute budget 指令。

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

你在本地签名。我们不托管资金、不代签。★tip 带在交易里、从你自己的钱包链上支付,所以它属于你要展示的那份预估★ —— 交易失败时它跟着一起 revert,这是 Solana 原子交易的机制决定的。没有月费,只有到达链上的交易才计费。

免费领 API key:

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

常见问题

怎么预估一笔 Solana 交易的手续费? getFeeForMessage 返回基础手续费,加上 message 里已有的 compute budget 指令所隐含的优先费。如果交易会创建账户,租金要单独加。

getFeeForMessage 包含优先费吗? 只有当你的 message 里已经有 setComputeUnitPrice 指令时才包含。 没有的话它只返回基础手续费 —— 看着便宜,而那不是你要付的。

Solana 的基础手续费是多少? 每个签名 5,000 lamport。一笔单签名交易不管多复杂都付这么多,这让它成为唯一一个你能精确预测的组成部分。

优先费怎么算? compute unit price 乘以你请求的 compute unit limit。因为它是乘积,一个虚高的上限会按比例抬高费用 —— 哪怕交易实际用得少得多。

实际手续费为什么和预估不一样? 手续费按 slot 移动,所以展示给用户的预估在他确认时已经陈旧。在发送那一刻现算,并且展示区间而不是一个点。

租金该算进手续费预估吗? 单独展示,并标成可退押金。 它是被锁住而不是被花掉、账户关闭时会退回的,把它叫手续费会高估用户的损失。

swap 的手续费怎么估? 基础手续费,加上按池子账户近期活动推导的优先费,再加上需要创建代币账户时的租金。对一个新代币来说,第三项往往最大。

该查哪些账户的近期费用? 你的交易会写入的那些账户。 手续费压力是按账户的,全网平均值既描述不了清淡市场,也描述不了被争抢的市场。

怎么找到正确的 compute unit limit? 模拟交易并读 unitsConsumed,然后加余量。 留着默认值意味着按一个远高于真实用量的数字支付优先费。

该按中位数还是最坏情况预留? 最坏情况。 按中位数预留意味着在尖峰时失败 —— 而那恰恰是这笔交易被争抢到值得在意的时候。

滑点算手续费吗? 不算,但它是真实成本,属于任何诚实的成本讨论。在一笔容忍度很宽的交易上,它可以超过所有手续费之和。

怎么展示手续费才不吓到用户? 优先费给一个区间,基础手续费标注为固定且微不足道,租金标成可收回的押金而不是一笔支出。

能不构建交易就估手续费吗? 只能粗估。getFeeForMessage 需要一个编译好的 message,而计算上限需要模拟 —— 所以准确的预估需要你打算发送的那些指令。

拥堵时我的预估为什么偏低? 因为它是用平静时段的样本算的。在发送那一刻用具体账户的当前数据重算,并把结果夹在下限和上限之间。

失败的交易还会花掉预估的手续费吗? 如果它落块并 revert 了,基础手续费和优先费都付了。如果它从没落块,不产生手续费 —— 这就是过期和 revert 必须分开统计的原因。

手续费上限该设多少? 从这笔交易的价值推导,而不是取一个整数。 撞到它应该触发告警,这样你知道行情极端,而不是在尖峰里默默付过去。

延伸阅读

← 返回博客列表