Как автоматизировать аренду энергии TRON через API

2026-05-13

Зачем вообще автоматизировать аренду энергии

Если у вас работает горячий кошелёк, движок вывода средств биржи или контракт, инициирующий переводы 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, и по ним можно безопасно ветвить логику.

  1. INSUFFICIENT_BALANCE: ваш предоплаченный баланс слишком мал, чтобы покрыть запрошенную аренду плюс возможную преактивацию. Не ретраить, ретрай не починит. Ловите этот код отдельно и запускайте поток алерта или пополнения. Добавьте requestId в payload алерта.
  2. INVALID_ADDRESS: destinationAddress не прошёл декодирование base58check на сервере. Если ваша система собирает адреса динамически, например из пользовательского ввода, валидируйте их локально перед вызовом API. У библиотеки tronpy на Python для этого есть is_address(); отбрасывать плохой ввод на стороне клиента быстрее, чем ждать круговой проход.
  3. 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.

Хотите сэкономить на комиссиях TRON? Проверьте цены на энергию. Рассчитать цену
Назад к блогу