Что на самом деле делает API энергии TRON
API энергии TRON продаёт ровно одну вещь, и это не скидка на комиссию. Вашему счёту нужна энергия, чтобы выполнить контракт USDT, а без неё сеть берёт TRX — по 100 sun за каждую единицу. API энергии договаривается о том, чтобы другой счёт — тот, у которого TRX в стейкинге, — одолжил свою энергию адресу, который подпишет ваш перевод, на несколько минут. Всё, что ниже, — форма запросов вокруг этого единственного факта в сети.
В сети это делегация, и вы в ней ничего не подписываете
Одалживающий счёт подписывает делегацию, в которой назван ваш адрес. Ваш адрес ничего не подписывает, ничего не одобряет и ничего не выдаёт в ответ: сеть поднимает его лимит энергии — и в этом всё событие целиком (что такое энергия). Поэтому API энергии просит адрес и никогда не просит ключ: единственная подпись в этой схеме принадлежит другой стороне.
Самый короткий рабочий сценарий
Базовый адрес — https://api.nrg.market/v1, а ключ едет в одном заголовке: Authorization: Bearer nrg_live_…. Показывается он один раз, при выпуске, и его можно привязать к списку IP. Вызывайте API со своего сервера: эндпоинты с ключом не разрешают ни одного браузерного источника — ключ, попавший на страницу, есть у всех. Есть и второй базовый адрес, на поддомене sandbox у nrg.market: тот же /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
kind равен single для адресата, у которого USDT уже есть; double — для того, у кого их нет и кому сначала нужно завести счёт токена; custom — для контракта, цена которому считается по dry-run, а не по одной из двух стандартных цифр. from необязателен и на цену не влияет; если его указать, вы получаете два ответа про отправителя — inactive_sender и blacklisted_sender среди warnings — до того, как заказ в вашем режиме откажет по одному из них.
Дальше заказ. Mode 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: подписываете вы, отправляем мы
Mode b заменяет from и to на signed_tx — подписанную транзакцию в hex, — и мы отправляем её в сеть в тот момент, когда энергия села: никто не сидит и не ждёт четырёхминутного окна. Принимается намеренно узкое: вызов transfer контракта USDT и ничего больше, ровно одна подпись владельца, без приложенных TRX, и мультиподпись отклоняется, потому что состав ключей разрешения живёт в сети и меняется без нашего ведома. В момент создания заказа у транзакции должно оставаться три минуты жизни, и её собственный срок укорачивает окно: send_before никогда не выходит за этот срок минус запас на отправку. Не успели передать вовремя — позиция закрывается как failed с broadcast_window_missed, резерв возвращается целиком.
Mode C: энергия без названного получателя
Mode c принимает from и kind и не принимает получателя вовсе — для кошелька, который вы хотите держать готовым ещё до того, как появится пакет. kind здесь single или double, и попасть в него — ваша забота: получателя нет, объём читать не с чего, поэтому single там, где нужен был double, оставит перевод недобранным, а разницу сожжёт в TRX. custom здесь намеренно не предлагается: он получается из dry-run против конкретного получателя-контракта. У позиций стоит to: null, до sent они не доходят и с закрытием окна становятся expired: аренда кончилась — это нормальный финал, а не отказ, и он оплачивается, потому что энергия была доставлена.
Чего в запросе нет никогда
Поля для приватного ключа в контракте нет нигде и ни в одном режиме. Mode A вашу транзакцию не видит вовсе. Mode B видит уже подписанную вами, и изменить в ней байт мы не можем, не сломав подпись; лежит она зашифрованной и стирается через семь дней после того, как заказ дошёл до финального статуса. У 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, когда ничего не изменилось. Клиента генерируйте из этого файла: страница — пересказ, а отвечаем мы за файл.