API로 TRON 에너지 임대를 자동화하는 방법
왜 에너지 임대를 자동화해야 하는가
핫월렛이나 거래소 출금 엔진, 또는 사용자를 대신해 TRC-20 전송을 트리거하는 컨트랙트를 운영 중이라면, 이미 그 고통을 알고 계실 겁니다. 트랜잭션이 발생하는 바로 그 순간 TRON 에너지가 준비되어 있어야 합니다. UI를 통해 수동으로 에너지를 충전하는 방식은 하루에 몇 건 이상의 전송으로는 확장되지 않습니다. 타이밍을 놓치면 트랜잭션은 대신 발신 주소에서 TRX를 소각하게 되며, 이는 사전 임대한 에너지보다 훨씬 비싼 비용을 초래합니다.
해결책은 백엔드가 트랜잭션을 브로드캐스트하기 직전에 임대 API를 호출하여, 사람이 개입하지 않고 프로그램적으로 에너지를 공급받는 것입니다. 이 글에서는 tronenergyrent.com에서 이를 정확히 어떻게 수행하는지 안내합니다: 실제 요청 형식, 비동기 주문 라이프사이클, 합리적인 기간 선택 기준, 그리고 서비스에 바로 적용할 수 있는 동작하는 Python 예제까지 다룹니다.
이 API가 온체인에서 실제로 하는 일
코드를 작성하기 전에 온체인 메커니즘을 이해해 두면 디버깅을 맹목적으로 하지 않아도 됩니다.
TRON의 리소스 모델은 연산(에너지)과 트랜잭션 크기(bandwidth)를 분리합니다. 표준 USDT TRC-20 전송은 수신자가 이미 USDT를 보유한 경우 약 65,000 에너지를, 수신자의 USDT 잔액이 0인 경우 약 130,000 에너지를 소비합니다. 첫 입금 전송이 잔액 엔트리 생성에 따른 저장 비용을 지불하기 때문에, 비용은 전적으로 수신자에게 달려 있으며 발신자와는 무관합니다. 전송 한 건의 bandwidth 사용량은 약 345 바이트로, 대부분의 계정에서 일일 무료 한도로 충당할 수 있을 만큼 작습니다.
임대 API를 호출하면, 서비스는 자체 TRX를 스테이킹하고 Stake 2.0의 DelegateResourceContract를 사용하여 결과로 생성된 에너지를 지정한 주소로 위임합니다. 위임은 주소별로 이루어지며 시간 제한이 있습니다. 임대 기간이 만료되면 에너지는 자동으로 제공자에게 회수됩니다.
중요한 한 가지 디테일: 임대는 비동기적입니다. API 호출은 orderId와 PAID_BY_USER 상태와 함께 즉시 반환되지만, 온체인 위임 트랜잭션은 백그라운드에서 브로드캐스트되며 보통 몇 초 안에 체결됩니다. 통합 시 초기 응답은 주문이 수락되고 결제되었다는 확인으로 처리하고, 그다음 주문 상세 엔드포인트를 폴링하여 상태가 ENERGY_DELEGATED로 전환될 때까지 기다려야 합니다.
적절한 임대 기간 선택
이 API는 period 파라미터로 네 가지 허용 값을 받습니다: 1h, 1d, 3d, 30d. 가격은 온체인 에너지 시장에 따라 변동하고 하루 동안에도 달라지므로, 실시간 수치는 항상 가격 페이지에 있습니다. 상대적 순서는 안정적입니다: 1h는 호출당 가장 저렴하고, 30d는 같은 주소에서 다수의 전송에 걸쳐 비용을 분산할 때 가장 저렴합니다.
발신 전송 한 건당 임대 한 번을 트리거하는 이벤트 기반 시스템에서는 거의 항상 1h 등급이 정답입니다. 절대 비용이 가장 낮고, 위임된 에너지는 몇 초 안에 소비됩니다. 1d 이상의 등급은 예측 가능한 배치 워크로드를 운영할 때, 예를 들어 야간 일괄 지급 작업에서 API를 수십 번 호출하는 대신 큰 에너지 블록을 한 번에 임대하고 싶을 때 의미가 있습니다.
시스템이 같은 주소에서 시간당 20-30건 이상의 전송을 지속적으로 발생시킨다면, 예상 볼륨을 커버할 만한 크기의 1d 블록을 임대하는 편이 건별 호출보다 깔끔합니다. 초기 비용은 올라가지만 API 오버헤드와 온체인 확인 지연이 핫패스에서 사라집니다.
인증과 요청 구조
임대 엔드포인트의 위치는 다음과 같습니다:
GET https://api.tronenergyrent.com/place-energy-order
쿼리 파라미터를 사용하는 단순한 GET 요청입니다. 인증은 apiKey 쿼리 파라미터로 처리되며, 가입하고 계정에 자금을 충전한 후 대시보드에서 발급받을 수 있습니다. 이 엔드포인트에는 헤더 기반 인증도, JSON 요청 본문도 없습니다.
파라미터는 다음과 같습니다:
apiKey(필수): 대시보드에서 발급받은 API 키.period(필수): 임대 기간,1h,1d,3d,30d중 하나.energyAmount(필수): 위임할 에너지의 양. 최소값은15000입니다. USDT를 이미 보유한 수신자에게 보내는 표준 USDT 전송 한 건에는65000이 안전한 수치이며, USDT를 처음 받는 수신자에게 보내는 첫 전송에는130000을 사용하세요.destinationAddress(필수): 위임된 에너지를 받을 TRON 주소 (T로 시작하는 base58check 형식).preActivateDestinationAddress(선택, 기본값0): 대상 주소가 한 번도 TRX를 받은 적이 없어 온체인에서 활성화되지 않은 경우1로 설정하세요. 그러면 서비스가 에너지를 위임하기 전에 사전 결제된 잔액에서1.5 TRX를 보내 주소를 활성화합니다. 주소가 이미 활성화되어 있다면, 추가 비용을 피하기 위해0그대로 두세요.
응답은 주문의 성공 여부와 관계없이 항상 HTTP 상태 200으로 반환됩니다. HTTP 상태가 아닌 JSON 본문의 status 필드로 분기 처리하세요. 성공 응답 본문은 다음과 같이 보입니다:
{
"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입니다.
주문 라이프사이클
payload의 state 필드는 작고 고정된 시퀀스를 따라 진행됩니다:
PAID_BY_USER: 초기 상태. 주문이 잔액에서 결제되었으나 아직 온체인 동작은 발생하지 않은 상태입니다.WAITING_DELEGATION: 서비스가 주문을 픽업하고 위임 트랜잭션을 준비 중인 상태입니다.ENERGY_DELEGATED: 위임 트랜잭션이 온체인에 체결된 상태입니다. 대상 주소가 임대된 에너지를 보유하고 있으며 다음 발신 트랜잭션에서 사용할 수 있습니다.ERROR_DELEGATION: 어떤 이유로 위임이 실패한 상태입니다. 드물지만 네트워크 혼잡이 심할 때 발생할 수 있습니다.CANCELLED: 주문이 취소되고 자금이 잔액으로 환불된 상태입니다.
현재 상태를 확인하려면 다음을 호출하세요:
GET https://api.tronenergyrent.com/single-order-details?apiKey=YOUR_API_KEY&orderId=ORDER_ID
응답은 동일한 status / errorCode / payload 봉투를 사용하며, 현재 state는 payload 안에 들어 있습니다. 주문 후 1-2초마다 이 엔드포인트를 폴링하고, ENERGY_DELEGATED를 확인한 후에만 TRC-20 전송을 진행하세요. 실제로는 보통 한두 번의 폴링이면 충분합니다.
동작하는 Python 통합 예제
다음은 최소한이지만 프로덕션 형태를 갖춘 패턴입니다. 임대를 주문하고, 위임이 온체인에 체결될 때까지 폴링하며, 문제가 발생하면 명확한 에러를 표면화합니다.
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를 호출하기 전에 로컬에서 검증하세요.tronpyPython 라이브러리에is_address()가 있습니다. 잘못된 입력을 클라이언트 측에서 거절하는 편이 왕복 호출을 기다리는 것보다 빠릅니다.INACTIVE_DESTINATION_ADDRESS_ERROR: 대상 주소가 온체인에서 한 번도 활성화된 적이 없는데 서비스에 활성화를 요청하지 않은 경우입니다. 다른 곳에서 소량의 TRX를 미리 보내 주소에 자금을 공급하거나, 다음 호출에서preActivateDestinationAddress=1을 전달하여 서비스가1.5 TRX로 활성화하도록 하여 해결하세요.
좀 더 미묘한 케이스: 여러 워커가 동시에 같은 대상 주소에 임대를 주문하는 경우 ORDER_IS_ALREADY_IN_PROGRESS가 반환될 수 있습니다. 해결책은 API 측이 아니라 프로세스 측에 있습니다. 대상 주소를 키로 하는 분산 락 (Redis로 충분합니다)을 위임 완료 시점까지 보유하세요.
온체인 검증
대부분의 통합에서는 ENERGY_DELEGATED가 될 때까지 /single-order-details를 폴링하는 것으로 충분합니다. 안전벨트와 멜빵을 모두 채우는 식의 검증을 원한다면, 위임이 체결된 후 TRON 풀노드에 직접 질의할 수도 있습니다:
GET https://api.trongrid.io/v1/accounts/{destinationAddress}
응답에는 외부 주소로부터 위임받은 에너지를 포함한 해당 주소의 현재 리소스 상태가 들어 있습니다. 정확한 필드명은 현재 Stake 2.0 뷰에 따라 다르므로, 연결할 때 최신 TRON HTTP API 문서를 참조하세요. 전송을 차단하지 않고 경고만 로깅하는 소프트 체크로서, 임대 API 자체의 하류에서 무언가 잘못되는 드문 경우에 대한 유용한 카나리가 됩니다.
이 추가 체크는 임대가 API에서는 성공해 보이지만 온체인에서 무언가 잘못된 그 한 번의 경우에, 디버깅에 들이는 몇 시간을 절약해 줄 것입니다.