Как автоматизировать аренду энергии TRON через API
Зачем вообще автоматизировать аренду энергии
Если у вас работает горячий кошелёк, движок вывода средств биржи или контракт, инициирующий переводы TRC-20 от имени пользователей, боль вам знакома: энергия TRON нужна в ту же секунду, когда срабатывает транзакция. Пополнять энергию вручную через интерфейс не масштабируется дальше пары переводов в день. Не успели в окно, и транзакция сожжёт TRX с адреса отправителя, обычно по гораздо более высокой цене, чем заранее арендованная энергия.
Решение: вызывать API аренды ровно в тот момент, когда ваш бэкенд собирается транслировать транзакцию, чтобы энергия выделялась программно, без человека в цикле. В этой статье разберём, как именно это сделать с tronenergyrent.com: реальную структуру запроса, асинхронный жизненный цикл заказа, разумные компромиссы по длительности и рабочий пример на Python, который можно сразу встроить в ваш сервис.
Что API на самом деле делает on-chain
Прежде чем писать код, полезно понимать механику on-chain, чтобы не отлаживаться вслепую.
Модель ресурсов TRON разделяет вычисления (энергию) и размер транзакции (bandwidth). Стандартный перевод USDT TRC-20 потребляет примерно 65,000 энергии, если у получателя уже есть USDT, и около 130,000, если баланс USDT у получателя нулевой. Первый входящий перевод оплачивает стоимость хранения новой записи баланса, поэтому стоимость зависит исключительно от получателя, а не от отправителя. Расход bandwidth на перевод составляет около 345 байт, что достаточно мало, и большинство аккаунтов покрывают это из ежедневной бесплатной квоты.
Когда вы вызываете API аренды, сервис стейкает собственные TRX и делегирует получившуюся энергию на указанный вами адрес, используя DelegateResourceContract из Stake 2.0. Делегирование привязано к адресу и ограничено по времени. Когда срок аренды истекает, энергия автоматически возвращается провайдеру.
Важная деталь: аренда асинхронная. Вызов API сразу возвращает orderId и state PAID_BY_USER, а транзакция делегирования on-chain транслируется в фоне и обычно попадает в блок в течение нескольких секунд. Ваша интеграция должна трактовать первичный ответ как подтверждение, что заказ принят и оплачен, а затем опрашивать эндпоинт деталей заказа, пока state не перейдёт в ENERGY_DELEGATED.
Как выбрать длительность аренды
API принимает параметр period с четырьмя допустимыми значениями: 1h, 1d, 3d, 30d. Цены плавают вместе с on-chain рынком энергии и меняются в течение дня, поэтому актуальные цифры всегда находятся на странице цен. Относительный порядок стабилен: 1h дешевле всего за один вызов, 30d дешевле всего в пересчёте на множество переводов с одного адреса.
Для событийно-управляемой системы, где вы инициируете одну аренду на каждый исходящий перевод, тариф 1h почти всегда правильный выбор. Вы платите минимальную абсолютную стоимость, а энергия расходуется за секунды после делегирования. Тарифы 1d и длиннее имеют смысл, когда у вас предсказуемые пакетные нагрузки, например ночной выплатной джоб, и хочется арендовать большой блок энергии один раз вместо десятков вызовов API.
Если ваша система стабильно выполняет более 20-30 переводов в час с одного адреса, арендовать блок 1d размером под ожидаемый объём чище, чем делать вызовы под каждый перевод. Авансовая стоимость растёт, зато накладные расходы на API и задержка on-chain подтверждения пропадают из критического пути.
Аутентификация и структура запроса
Эндпоинт аренды находится по адресу:
GET https://api.tronenergyrent.com/place-energy-order
Это обычный GET-запрос с query-параметрами. Аутентификация выполняется query-параметром apiKey, который вы генерируете в личном кабинете после регистрации и пополнения счёта. Для этого эндпоинта нет ни аутентификации через заголовки, ни JSON-тела запроса.
Параметры такие:
apiKey(обязательный): ваш API-ключ из личного кабинета.period(обязательный): длительность аренды, одно из1h,1d,3d,30d.energyAmount(обязательный): сколько энергии делегировать. Минимум15000. Для одного стандартного перевода USDT получателю, у которого уже есть USDT, безопасное число65000; для первого перевода новому держателю USDT используйте130000.destinationAddress(обязательный): адрес TRON (base58check, начинается сT), который получит делегированную энергию.preActivateDestinationAddress(необязательный, по умолчанию0): установите1, если адрес назначения никогда не получал TRX и потому не активирован on-chain. Тогда сервис отправит1.5 TRXс вашего предоплаченного баланса, чтобы активировать адрес перед делегированием энергии. Если адрес уже активен, оставьте0, чтобы избежать лишних расходов.
Ответ всегда возвращается с HTTP-статусом 200 независимо от того, успешен заказ или нет. Ветвите логику по полю status в JSON-теле, а не по HTTP-статусу. Успешное тело выглядит так:
{
"status": "SUCCESS",
"errorCode": null,
"errorDescription": null,
"requestId": "2651eacd-2428",
"payload": {
"orderId": "128de799-501e-44b2-8d6f-1fa825c2deed",
"totalPriceSun": 5662800,
"totalPriceTrx": 5.6628,
"state": "PAID_BY_USER"
}
}
В теле ошибки status: "ERROR", машиночитаемый errorCode и читаемое человеком errorDescription:
{
"status": "ERROR",
"errorCode": "INVALID_ENERGY_AMOUNT",
"errorDescription": "energyAmount is less than 15000",
"requestId": "71431087-4",
"payload": null
}
Всегда логируйте requestId в обеих ветках. Если когда-нибудь придётся попросить поддержку трассировать конкретную аренду, поиск идёт именно по этому ID.
Жизненный цикл заказа
Поле state в payload проходит небольшую фиксированную последовательность:
PAID_BY_USER: начальное состояние, заказ оплачен с вашего баланса, но on-chain действий ещё не было.WAITING_DELEGATION: сервис подхватил заказ и готовит транзакцию делегирования.ENERGY_DELEGATED: транзакция делегирования попала on-chain. На адресе назначения теперь арендованная энергия, и её можно потратить на следующей исходящей транзакции.ERROR_DELEGATION: делегирование по какой-то причине не удалось. Редко, но возможно при сильной перегрузке сети.CANCELLED: заказ отменён, средства возвращены на ваш баланс.
Чтобы узнать текущее состояние, вызовите:
GET https://api.tronenergyrent.com/single-order-details?apiKey=YOUR_API_KEY&orderId=ORDER_ID
Ответ использует тот же конверт status / errorCode / payload, с текущим state внутри payload. Опрашивайте этот эндпоинт раз в одну-две секунды после размещения заказа и переходите к переводу TRC-20 только после того, как увидите ENERGY_DELEGATED. На практике обычно хватает одного-двух опросов.
Рабочая интеграция на Python
Вот минимальный, но продакшен-образный паттерн. Он размещает аренду, опрашивает до момента, когда делегирование оказывается on-chain, и явно сигнализирует об ошибке, если что-то пошло не так.
import logging
import time
import requests
API_BASE = "https://api.tronenergyrent.com"
API_KEY = "your_api_key_here"
ENERGY_FOR_TRANSFER = 65_000 # use 130_000 if recipient has zero USDT balance
HTTP_TIMEOUT_SECS = 10
POLL_INTERVAL_SECS = 1
POLL_TIMEOUT_SECS = 30
def place_order(destination_address: str, period: str = "1h",
energy_amount: int = ENERGY_FOR_TRANSFER) -> dict:
resp = requests.get(
f"{API_BASE}/place-energy-order",
params={
"apiKey": API_KEY,
"period": period,
"energyAmount": energy_amount,
"destinationAddress": destination_address,
"preActivateDestinationAddress": 0,
},
timeout=HTTP_TIMEOUT_SECS,
)
resp.raise_for_status()
body = resp.json()
if body.get("status") != "SUCCESS":
raise RuntimeError(
f"place-energy-order failed: {body.get('errorCode')} "
f"({body.get('errorDescription')}) requestId={body.get('requestId')}"
)
return body["payload"]
def wait_for_delegation(order_id: str) -> dict:
deadline = time.monotonic() + POLL_TIMEOUT_SECS
while time.monotonic() < deadline:
resp = requests.get(
f"{API_BASE}/single-order-details",
params={"apiKey": API_KEY, "orderId": order_id},
timeout=HTTP_TIMEOUT_SECS,
)
resp.raise_for_status()
body = resp.json()
if body.get("status") != "SUCCESS":
raise RuntimeError(
f"single-order-details failed: {body.get('errorCode')} "
f"({body.get('errorDescription')})"
)
state = body["payload"]["state"]
if state == "ENERGY_DELEGATED":
return body["payload"]
if state == "ERROR_DELEGATION":
raise RuntimeError(f"Delegation failed for order {order_id}")
if state == "CANCELLED":
raise RuntimeError(f"Order {order_id} was cancelled")
time.sleep(POLL_INTERVAL_SECS)
raise TimeoutError(f"Order {order_id} did not reach ENERGY_DELEGATED in {POLL_TIMEOUT_SECS}s")
def rent_energy(destination_address: str) -> dict:
placed = place_order(destination_address)
logging.info(
"Order placed: id=%s priceSun=%s priceTrx=%s",
placed["orderId"], placed["totalPriceSun"], placed["totalPriceTrx"],
)
delegated = wait_for_delegation(placed["orderId"])
logging.info("Order delegated: id=%s", delegated["orderId"])
return delegated
Несколько моментов заслуживают внимания. Явная проверка status == "SUCCESS" важна, потому что HTTP-статус всегда 200, и сам по себе resp.raise_for_status() ничего не говорит вам о фактическом исходе. У цикла опроса жёсткий таймаут, чтобы зависший заказ не мог бесконечно блокировать ваш конвейер переводов. Ветвление по state явно обрабатывает три терминальных исхода (ENERGY_DELEGATED, ERROR_DELEGATION, CANCELLED), а не полагается на общий случай «не успех».
В вашем рабочем процессе перевода сначала вызывайте rent_energy(), и только потом транслируйте перевод TRC-20. Порядок операций важен: если транслировать первым, транзакция сожжёт TRX отправителя, потому что делегирование ещё не попало в блок.
Как подобрать размер предоплаченного баланса
Стоимость аренды списывается с предоплаченного баланса, который вы поддерживаете в аккаунте tronenergyrent.com. Настройте алерт или автопополнение, когда баланс опускается ниже порога. Разумный нижний уровень: то, что покрывает два часа пикового объёма.
Чтобы прочитать текущий баланс программно:
GET https://api.tronenergyrent.com/account-info?apiKey=YOUR_API_KEY
В payload приходит ваш текущий баланс и состояние аккаунта. Подключите это к своему мониторингу, и пустой баланс в 2 ночи больше никогда вас не удивит. Учтите, что минимальное пополнение баланса tronenergyrent.com составляет 10 TRX.
Обработка ошибок на практике
В продакшене чаще всего встречаются три режима отказа. Коды ошибок ниже приходят из реального API, и по ним можно безопасно ветвить логику.
INSUFFICIENT_BALANCE: ваш предоплаченный баланс слишком мал, чтобы покрыть запрошенную аренду плюс возможную преактивацию. Не ретраить, ретрай не починит. Ловите этот код отдельно и запускайте поток алерта или пополнения. ДобавьтеrequestIdв payload алерта.INVALID_ADDRESS:destinationAddressне прошёл декодирование base58check на сервере. Если ваша система собирает адреса динамически, например из пользовательского ввода, валидируйте их локально перед вызовом API. У библиотекиtronpyна Python для этого естьis_address(); отбрасывать плохой ввод на стороне клиента быстрее, чем ждать круговой проход.INACTIVE_DESTINATION_ADDRESS_ERROR: адрес назначения никогда не активировался on-chain, а вы не попросили сервис его активировать. Чинится одним из двух способов: предварительно пополнить адрес небольшим количеством TRX откуда-то ещё или передатьpreActivateDestinationAddress=1в следующем вызове, чтобы сервис активировал его за1.5 TRX.
Более тонкий случай: ORDER_IS_ALREADY_IN_PROGRESS может вернуться, если несколько воркеров одновременно размещают аренды на один и тот же адрес назначения. Чинится на стороне процесса, а не API. Возьмите распределённую блокировку (Redis отлично подходит) по ключу адреса назначения и держите её до завершения делегирования.
Верификация on-chain
Для большинства интеграций опроса /single-order-details до ENERGY_DELEGATED достаточно. Если хочется страховки в виде двух поясов, можно дополнительно запросить полный нод TRON напрямую, как только делегирование попало в блок:
GET https://api.trongrid.io/v1/accounts/{destinationAddress}
В ответе содержится текущее состояние ресурсов адреса, включая энергию, делегированную ему с внешних адресов. Точное имя поля зависит от текущего представления Stake 2.0, поэтому при подключении сверяйтесь с актуальной документацией TRON HTTP API. В роли мягкой проверки, которая логирует предупреждение, а не блокирует перевод, это полезный канарейка для редких случаев, когда что-то идёт не так уже после самого API аренды.
Эта дополнительная проверка сэкономит часы отладки в тот единственный раз, когда аренда в API выглядит успешно, но что-то ещё ломается on-chain.