OUT_OF_ENERGY:花掉手续费、什么都没发出去的那个 TRON 报错
一笔 USDT 转账发了出去,钱包上显示扣了手续费,收款方那里什么也没有。交易在区块里——它没有被拒绝,也没有哪个节点宕机——而一笔成功的转账在那个位置写的是 SUCCESS,这一笔写的是 OUT_OF_ENERGY。在 TRON 上这个报错就是它字面的意思:这次调用跑到一半把能量花光了,于是停下。下面是它的机制、一轮批量付款撞上它的三条路,以及那些解法——从最快的,到真正最省钱的。
这个结果到底在说什么
发 USDT 是对一个智能合约的调用,而 TRON 上的合约执行是按能量计量的,不是收一笔固定手续费(能量是什么)。先花的是转出方自己的能量。不够这次调用用时,网络并不就此停下——它按每单位 100 sun 的协议价烧掉转出方的 TRX 把剩下的买齐,但只买到这笔交易自己的 fee_limit 允许的地方为止。那个字段不是谁收的手续费。它是这一次调用最多被允许烧掉转出方多少 TRX 的天花板。
调用还没跑完就顶到天花板,执行就停住。它已经做出的每一处状态改动都会回滚,所以代币一点没动,而网络会把它的判词写在一次完整调用写 SUCCESS 的那个位置上。那个判词就是区块浏览器在一笔失败交易旁边显示的那个词,也是节点作为 contractRet 返回的那个字段。这笔交易没有哪一处没能到达网络。它到了网络,并且一直被执行到给它买单的钱花光为止。
那笔 TRX 横竖都花掉了
回滚撤掉的是转账。它撤不掉燃烧:消耗掉的能量就是付过钱的能量,于是转出方少了这次尝试在顶到天花板路上烧掉的那些,代币一点没动,还留下一个交易哈希,而对账脚本会心安理得地把它记成一笔付款。
这就让顺手重试成了最贵的一步。同样的转账配同样的上限,花同样的 TRX 换同样的结果,而一个会自动重试失败项的批量付款循环,能在有人读到那个结果字符串之前做上好几遍。一个付款团队会碰到的那些失败,多数是悄悄花钱的。这一种花得很响,却照样被漏掉,因为钱包报告扣了手续费,而钱哪儿都不在。
昨天还好用的上限,为什么今天不好用了
收款方换了,上限没换。这是一轮批量付款最先碰上的一种。转给已经持有 USDT 的地址,大约要 65,000 单位能量;转给从来没有持有过的地址,这笔转账还得先给收款方开一个代币账户,那就是大约 131,000——搬同样多的钱,胃口翻了一倍。按协议价烧的话,前者是 6.5 TRX,后者是 13.1 TRX。按寻常情况定的天花板,在第一次给新客户付款时就差了一半,而这一批里没有任何东西看上去有什么不同:它是收款方的属性,不是这笔转账的属性。
能量本来有,现在没了。一次代理是一段时限内的一个数量,不是订阅。第一笔转账花掉的东西,第二笔就没有了;时限一到,剩下的回到它主人那里。同一个窗口里已经发过一次的钱包,手上比那次代理的名义数字看上去要少,而停下来的是后面那一笔——从外面看,就像同一笔转账在随机失败。
上限是零。能量确定无疑时,零就是那个正确的值;而在 mode B 里,能量由我们交付、交易也由我们广播,fee_limit 正是我们请您别设的那个字段:那里的一个上限,除了在出岔子时给烧掉转出方 TRX 留一份常设许可,别无用处。它同时也是一道硬闸。零许可意味着哪怕只差一个单位,调用也会停下,而不是掏钱买下来,所以一个留在零上的上限,是一个只在能量成立时才成立的决定。
在交易签名之前就看见它
第一条诱因背后的那个问题——这一个收款方要花多少能量——在任何东西被签名之前就能回答,靠的是两个既不预留也不扣费的调用。
GET /v1/address-check 回答单个地址:activated、holds_usdt、blacklisted、is_contract,以及由它们推出的 expected_kind。定下这次调用大小的是 holds_usdt。
curl -s -H "Authorization: Bearer $KEY" \ "https://api.nrg.market/v1/address-check?address=TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9" # expected_kind 是 "single"、"double" 或 "custom"
POST /v1/estimate 回答一整批——一次调用最多 500 个收款方,每个都带着一个 kind、它背后的 energy_units,以及任何 warnings。single 是寻常的转账,double 是要开代币账户的那种。custom 是没有哪条经验法则管得住的那一笔:收款方原来是一个带着自己 transfer 逻辑的合约,于是两个标准数字都不适用,energy_units 来自对真实调用跑的一次 dry-run。这些是要一笔一笔单独定上限的项,而旁边那个 contract_recipient 警告,往往是“某个付款地址根本不是钱包”的第一个迹象。
解法,从最省事的开始
把上限调高。一个字段,不用动基础设施,那道停顿就没了:调用跑完,网络拿走它需要的。跟着没了的不是成本。上限是烧钱的许可,所以大方的上限就是大方的燃烧——原来会停住的那笔转账,现在以 13.1 TRX 跑完。作为一轮批量付款的兜底是对的,作为方案是错的。
把能量放到转出方身上。那样上限就没有什么可许可的了。能量来自质押 TRX,也就是锁住一笔周转资金再盯着一份资源预算;或者来自为一笔转账所需的那几分钟租一次代理(此刻那要花多少钱)。两条路都是用能量给这次调用买单,根本不发生燃烧。
在窗口之内发出去。租来的能量在钱包上待一段时限,而订单的答复里带着 send_before——交付窗口关闭的时刻,比您买下的时限早三秒。要盯的是这个字段的出现,而不是 ready 这个词:订单是靠能量交付来完成的,所以 completed 紧跟着它就到。在 send_before 之后才把转账发进网络,那时能量可能已经回家了,而那正是这篇文章讲的那种亏空。
把签好的交易交出来。mode B 把窗口整个从您这一侧拿走:您提交一笔已经签好的转账,我们替您攥着,等能量在链上确认之后再广播。如果一笔由我们广播的交易仍然进了区块却没有执行,该笔会以 failed 收尾,原因是 transaction_reverted,整份预留全额释放——这个原因,和名单上其他每一个原因一样,从来不收钱。请照字面读这个名字:transaction_reverted 是我们对“它进了区块但没有执行”的说法,不管网络自己对那次拒绝用的是什么词,所以一个 OUT_OF_ENERGY 和一个合约自己的 REVERT 会落在同一个状态下面。这样一笔交易在走到那个判词的路上烧掉的 TRX 是转出方的,不是我们的,而这正是支持本节开头那次估价、而不是支持本节末尾那个重试循环的理由。