程序错误通常是写给"读日志的开发者"看的。★而它的主要消费者,是一个要在下一个 slot 里决定"要不要再试一次"的机器人。★
那个机器人读不了你的错误消息。它只能读错误码 —— 而它接下来做的一切,都由这个码告诉它的东西决定。
对调用方唯一要紧的那个区分
调用方看到的每一次失败,都落在两类之一:
| 类别 | 正确的应对 | 例子 |
|---|---|---|
| ★暂时性★ | ★用同样的意图重试★ | 滑点超限、价格陈旧 |
| ★永久性★ | ★重建或放弃★ | 未授权、账户已关闭、参数无效 |
★如果你的错误不把这两者分开,调用方只能猜。★ 而两种猜法的失败方向恰好相反:
当成暂时性 —— 机器人对着一个未授权调用不停重试直到 blockhash 过期,为一件永远不可能成功的事付手续费。
当成永久性 —— 机器人放弃了一笔再过一个 slot 就能成功的交易。
这两者调用方都没法自己挽救。 那个信息只存在于你的程序里。
Anchor 从 6000 开始编号
#[error_code]
pub enum SwapError {
SlippageExceeded, // ★6000★
PoolNotInitialized, // 6001
Unauthorized, // 6002
}
变体的顺序就是编号,这产出一个值得知道的兼容性陷阱:
#[error_code]
pub enum SwapError {
SlippageExceeded, // 6000
NewErrorInserted, // ★6001 —— 把下面全部挪位★
PoolNotInitialized, // ★现在是 6002,原来是 6001★
Unauthorized, // ★现在是 6003,原来是 6002★
}
★插入一个变体,会给它之后的每一个错误重新编号。★ 一个把 6001 = 重试 写死的调用方,现在会对一个完全不同的条件重试 —— 而交易里没有任何东西告诉他们含义变了。
新变体一律追加到末尾。永远不要插入,永远不要重排。 这不花任何成本,而做错了不可逆。
如果你的程序错误已经很清晰、交易还是会错过,免费领个 BoltTx key 你的用户改一行就能测测提交路径。
按"能不能重试"分组,不按子系统
对调用方帮助最大的组织原则:
#[error_code]
pub enum SwapError {
// ★6000-6099:暂时性 —— 重试可能成功★
SlippageExceeded = 0,
PriceStale = 1,
InsufficientLiquidityNow = 2,
// ★6100-6199:永久性 —— 重试不可能成功★
Unauthorized = 100,
PoolClosed = 101,
InvalidTokenPair = 102,
}
★这样调用方就能按区间分支,而不必维护一份你的单个错误码清单。★
const code = parseCustomError(err);
if (code >= 6000 && code < 6100) return retry();
if (code >= 6100) return abandon();
这能扛住你新增错误 —— 因为一个新的暂时性错误会落在暂时性区间里,已有的调用方逻辑不用更新就能正确处理它。
让错误说清楚出了什么问题
// ★对调用方毫无用处。★
require!(valid, SwapError::InvalidInput);
// ★可行动。★
require!(amount >= MIN_AMOUNT, SwapError::AmountBelowMinimum);
require!(amount <= max_for_pool, SwapError::AmountExceedsPoolCapacity);
require!(deadline > clock.unix_timestamp, SwapError::DeadlinePassed);
★三个具体错误,分别让机器人去缩小规模、去等待、去放弃。一个笼统错误,逼它三种情况都放弃。★
一个含糊错误的代价不是困惑 —— 而是调用方采取了可选项里最保守的那个动作,通常是放弃一笔"下小一点就能成"的交易。
区分"现在不行"和"永远不行"
这是机器人最常缺的那个区分,而提供它很便宜:
// ★含糊:是池子坏了,还是只是此刻空的?★
require!(pool.liquidity > 0, SwapError::NoLiquidity);
// ★清晰。★
require!(!pool.is_closed, SwapError::PoolPermanentlyClosed); // ★永远不行★
require!(pool.liquidity > 0, SwapError::PoolEmptyNow); // ★现在不行★
看到 PoolEmptyNow 的机器人可以把这个池子留在观察列表里。看到 NoLiquidity 的机器人,没有任何依据判断要不要再看一眼。
★对任何调用方会轮询或重试的东西,这一对错误比多少日志都值钱。★
公布错误码,不只是名字
IDL 里带着变体名字,这帮的是开发者,不是运行时的机器人。调用方真正需要的是:
6000 SlippageExceeded ★重试 —— 价格动了★
6001 PriceStale ★重试 —— 刷新后重新提交★
6100 Unauthorized ★永久 —— 检查签名者★
6101 PoolClosed ★永久 —— 从观察列表移除★
★把"能不能重试"和错误码一起公布,就是"调用方实现了正确行为"和"调用方在猜"之间的差别。★
大多数程序两样都不公布 —— 这就是为什么大多数机器人对所有程序错误一视同仁,通常是放弃,而这让他们的用户损失掉本来能成功的交易。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★一个程序错误意味着交易落块并 revert 了 —— 基础手续费已经付掉。★ 好的错误设计阻止不了这份成本,但它能阻止调用方为一个永远不会解除的条件反复付它。
BoltTx 在这里的位置
我们为调用你程序的那些人处理提交。错误语义完全在你的程序里决定。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。
你的调用方在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从他们自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
程序错误码为什么对交易机器人要紧? 因为机器人只凭错误码决定要不要重试。如果你的错误不区分暂时性和永久性失败,它只能猜,而两种猜法都很贵。
Anchor 的错误码从多少开始? 从 6000 开始,按枚举里变体的顺序编号。那个顺序就是线上格式 —— 这正是插入变体属于破坏性变更的原因。
插入一个新的错误变体会怎样? 它之后的每一个错误都被重新编号。 写死了错误码的调用方现在解读的是另一个条件,而交易里没有任何东西提示含义变了。
错误码该怎么组织? 按能不能重试分组,而不是按子系统。 预留区间让调用方能按区间分支而不必维护清单,而且能扛住你之后新增错误。
暂时性和永久性错误有什么区别? 暂时性意味着用同样意图重试可能成功,比如滑点。永久性意味着不可能,比如签名者未授权。 调用方需要这个区分,而且自己推导不出来。
笼统的错误码为什么有害? 因为它逼调用方采取最保守的应对。三个具体错误能让机器人分别缩小规模、等待、放弃,而一个笼统错误让它三种情况都放弃。
该把"现在空的"和"永久关闭"分开吗? 该。机器人可以继续轮询一个暂时空的池子,但应该把永久关闭的从观察列表移除。 一个合并的错误让它没有依据判断。
错误消息对机器人有帮助吗? 没有,能被程序读到的只有错误码。消息帮的是读日志的开发者,所以要把可行动的信息放进码和它被公布的含义里。
怎么把错误码文档写得有用? 公布数字、名字,以及重试能不能成功。IDL 带名字但不带"能不能重试" —— 而那正是调用方在运行时真正需要的部分。
移除一个变体之后,能复用它的错误码吗? 不能。留一个空位。 复用一个数字意味着老调用方会把新条件当成旧的,这比一个未知的码更糟。
调用方怎么读一个自定义错误? 从交易错误里读一个 custom program error 数字。他们减掉 6000 得到 Anchor 变体索引,或者按你公布的区间匹配。
0x1771 是什么错误? 十进制的 6001,是某个 Anchor 程序的第二个变体。光这个数字、没有那个程序的错误文档,是没有意义的。
错误里该包含数值吗,比如所需金额? Anchor 支持消息,但它们不是机器可读的。如果调用方需要一个数字来行动,通过账户状态暴露它,而不是通过错误。
一个程序该有多少个错误变体? 多到每一个都对应一种不同的调用方应对。 两个导致同样动作的错误可以合并,而一个覆盖两种动作的错误应该拆开。
错误码影响计算消耗吗? 可忽略。成本在检查本身,不在错误定义上 —— 所以做得具体基本是免费的。
最常见的错误设计失误是什么? 一个覆盖很多条件的笼统校验错误,紧随其后的是在枚举中间插入变体、静默地把后面全部重新编号。
延伸阅读
- Solana 交易错误码
- Solana 交易重试模式
- 你的程序消耗多少计算,就是你的用户付多少手续费
- Solana 程序升级安全
- Solana 交易上链完全指南