TRON 能量 API 到底做了什么
TRON 能量 API 卖的只有一样东西,而它不是手续费的折扣。您的账户要执行 USDT 合约就得有能量;没有能量,网络就改从您这里拿 TRX,每单位 100 sun。能量 API 做的,是安排另一个账户——一个质押过 TRX 的账户——把自己的能量借给将要签名发出转账的那个地址,借几分钟。下面写的全部,都是围着这一件链上事实展开的请求形态。
链上就是一次代理,而您一个字都不用签
出借的那个账户签一笔代理,里面写着您的地址。您的地址不签任何东西,不批准任何东西,也不往回授予任何东西:网络把它的能量上限抬高,整件事就到此为止(能量是什么)。所以能量 API 要的是一个地址,从来不要密钥——这套安排里唯一的那个签名,在对面。
跑得通的最小流程
基址是 https://api.nrg.market/v1,密钥走一个请求头:Authorization: Bearer nrg_live_…。它只在签发时显示一次,可以绑定到一份 IP 名单上。请从您自己的服务器调用它:需要鉴权的接口不接受任何浏览器来源,因为一把落到页面上的密钥,就是人人都有的密钥。还有第二个基址,在 nrg.market 的 sandbox 子域上——同一套 /v1 接口,跑在测试资金和模拟网络上,有自己的 nrg_test_… 密钥和自己的控制台,文档在 docs.nrg.market。
先从 POST /v1/estimate 开始。它不预留、不扣费,一次调用最多接 500 个收款方,返回的就是一个订单会算出来的东西。
curl -s https://api.nrg.market/v1/estimate \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"duration_s":300,
"transfers":[{"from":"TQ5NMqJjW8sBGqcpvUcXhbXm3jGnyDCmwK",
"to":"TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"}]}'
{"items":[{"to":"TN3W…","kind":"single","energy_units":65000,"warnings":[]}],
"zone":"day","duration_s":300}
# 有删减——答复里还带着每一笔的 price_trx、total_trx 和 burn_cost_trx
收款方已经持有 USDT 时,kind 是 single;还没有、需要先给它开一个代币账户时,是 double;收款方是合约时,是 custom,价格来自一次 dry-run,而不是上面那两个标准数字里的任何一个。from 是可选的,填不填都不动价格;填了,买到的是关于转出方的两个答案——inactive_sender 和 blacklisted_sender,都在 warnings 里——赶在您所用的那个模式里的订单因为其中之一拒单之前。
然后是下单。a 模式就是由您自己广播转账的那一种。
curl -s https://api.nrg.market/v1/orders \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: 8f3c1e02-6b47-4a1d-9d5c-0b2e7c4a1f90" \
-d '{"mode":"a","client_ref":"invoice-1042",
"transfers":[{"from":"TQ5NMqJjW8sBGqcpvUcXhbXm3jGnyDCmwK",
"to":"TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"}]}'
答复是 202,带两个值得直接读、而不值得自己拼出来的响应头:Location,订单自己的地址;还有 Retry-After,首次轮询之前和两次轮询之间该等的秒数。响应体就是这个订单,通常 status 停在 funded,send_before 还是 null——钱已经预留,采购已经开始。
send_before 要等能量在链上确认、订单走到 ready 的那一刻才出现。它是大约四分钟的一个窗口,从最后一笔确认的代理算起,所以一个批量里的每一笔共用同一个截止时刻;请读这个字段,别把那四分钟当成定数。要盯的也是这个字段,而不是 ready 这个词:mode A 里订单是靠能量交付来完成的,不是靠您的转账,所以 completed 可能在您还没发出任何东西的时候就到了。
有两种拒绝值得在第一单真金白银之前就接好:402 insufficient_balance,它在任何钱动起来之前就判定了,并带着 required_trx 和 available_trx;还有 422 invalid_address,details.reason 为 inactive_wallet——转出钱包从来没有被激活过,也就没有账户可以接受代理(这些拒绝背后的那些失败)。
幂等键
POST /v1/orders 要求带 Idempotency-Key,而它绑定的是请求体的原始字节,不是解析出来的 JSON。同样的字节拿回原来那个订单;同一个键配上不同的请求体,是 409 idempotency_conflict,而不是第二个订单;被校验挡下的请求不消耗这个键。到了批量的量级,靠的就是它把一次网络超时变成一个有答案的问题。
限流响应头在告诉您什么
每把 API 密钥下面数三个桶:订单每分钟 120 个,参考类请求 600 个,GET /v1/address-check 60 个——它每一次没命中缓存,都是向链上问一句话。带密钥的桶给出的每一个答复,成功的那些也在内,都带着 RateLimit-Limit、RateLimit-Remaining 和 RateLimit-Reset,好让客户端在被拒之前就慢下来,而不是之后;越过限额就是 429 rate_limited,附带 Retry-After。另有一种 429 的意思几乎相反——too_many_auth_failures,那里等是没有用的,因为被拒的是凭据本身。请按 code 分支,永远不要按 message。
Webhooks,让这个循环不必靠轮询
用 POST /v1/webhooks 登记一个接收端:url 必须是 https,必须解析到一个公网主机,而且不能带凭据。签名用的密钥只在创建时回给您一次,一个账户最多留五个在用的接收端。订阅您的流程用得着的那些——order.ready、order.completed、order.partially_completed、order.failed,加上按笔的 order.item.sent 和 order.item.expired。每一次投递都带着 X-NRG-Timestamp、X-NRG-Event-Id 和 X-NRG-Signature,签名是对时间戳、一个点和原始请求体做的 HMAC-SHA256:请拿您收到的那串字节去验,而不是您自己重新编码出来的 JSON,并按 event_id 去重。POST /v1/webhooks/{webhook_id}/test 会沿着真实路径投一条真正签过名的消息,再把您的接收端回了什么告诉您,而且不收钱。
mode B:您签,我们广播
b 模式把 from 和 to 换成 signed_tx,也就是十六进制的已签名交易,能量一落地我们就广播它——不用谁守着那个四分钟窗口。能接的东西窄得是故意的:只接 USDT 合约的一次 transfer 调用,别的都不接;只带一个所有者签名;不能附带 TRX;多签一律不收,因为一份权限的密钥集合放在链上,变动我们无从得知。下单的时候,这笔交易至少还要有三分钟的寿命,而它自己的过期时间会把窗口缩短——send_before 绝不会越过那个截止时刻减去一段广播余量之后的时刻。没能及时把它转发出去,该笔就以 failed 收尾,原因是 broadcast_window_missed,预留全额退回。
mode C:不写收款方的能量
c 模式只收 from 和 kind,根本不写收款方——用在那种您希望在批量还不存在时就先备好的钱包上。kind 是 single 或者 double,填对是您自己的事:没有收款方,就没有地方去读用量,所以 single 用在了本该 double 的地方,转账就会差一截,差的那部分要烧掉 TRX。custom 在这里是故意不提供的,因为它是针对某个具体合约收款方跑一次 dry-run 得出的结果。这些笔带着 to: null,永远走不到 sent,窗口一关就变成 expired:租期到了而已,这是正常的结局,不是失败——而且照常扣费,因为能量已经交付。
请求里永远不会有的东西
契约里没有任何一处有放私钥的字段,任何模式都没有。mode A 根本看不到您的交易。mode B 看到的是一笔您已经签好的交易,我们连它的一个字节都改不了,一改那个签名就废了;它加密存放,在订单到达终态 7 天之后抹掉。mode C 没有交易可看。API 拿到的是地址(我们还持有什么、不持有什么)。
不需要密钥的那几块
参考类接口和契约文件完全不需要密钥,限流按 IP 计,不按密钥。GET /v1/tariff 回答此刻一单位能量多少 sun、正在跑的是哪个时段、next_change_at,以及一份参考时刻表——价格页每次加载读的就是它。GET /v1/market 回答我们盯着的那些卖方各自公布了什么,每个数字都盖着读到它的那一刻,我们自己的价格也排在同一个榜单里,这就是行情页。请引用这个接口,而不是某个数字:我们的价格每隔几分钟就重算一次。
契约文件本身在 https://docs.nrg.market/openapi.yaml,逐字节相同的同一份文档也在 https://api.nrg.market/v1/contract.yaml——这一点值得知道,因为文档主机前面挡着一层边缘节点,有些程序化的客户端还没走到我们这里就被它拒了。那里的 ETag 是响应体的 SHA-256,所以什么都没变时,If-None-Match 拿到的是 304。请拿那个文件生成您的客户端:这个页面是摘要,而我们被约束的是那个文件。