use std::fmt::{Debug, Formatter};
use std::time::Duration;
use alloy_primitives::{Address as EvmAddress, Bytes, U256, hex};
use alloy_signer::Signer;
use alloy_signer_local::PrivateKeySigner;
use r402_core::chain::{ChainId, ChainProvider};
use serde::Deserialize;
use serde_json::{Value, json};
use url::Url;
use crate::chain::{Address, TronChainReference};
use crate::exact::TronExactError;
#[derive(Debug, Clone)]
pub struct UnsignedTransaction {
pub tx_id: [u8; 32],
raw: Value,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TransactionInfo {
#[serde(default, rename = "id")]
pub id: String,
#[serde(default)]
pub receipt: Option<TransactionReceipt>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TransactionReceipt {
#[serde(default)]
pub result: Option<String>,
}
impl TransactionInfo {
#[must_use]
pub const fn is_confirmed(&self) -> bool {
self.receipt.is_some()
}
#[must_use]
pub fn is_success(&self) -> bool {
self.receipt
.as_ref()
.and_then(|r| r.result.as_deref())
.is_some_and(|r| r == "SUCCESS")
}
}
pub struct TronGridClient {
base_url: Url,
http: reqwest::Client,
}
impl Debug for TronGridClient {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TronGridClient")
.field("base_url", &self.base_url)
.finish_non_exhaustive()
}
}
impl TronGridClient {
#[must_use]
pub fn new(base_url: Url) -> Self {
Self {
base_url,
http: reqwest::Client::new(),
}
}
#[must_use]
pub const fn with_http_client(base_url: Url, http: reqwest::Client) -> Self {
Self { base_url, http }
}
fn endpoint(&self, path: &str) -> Url {
#[allow(
clippy::expect_used,
reason = "path is a hardcoded literal at every call site"
)]
self.base_url.join(path).expect("invalid TronGrid path")
}
async fn post_json(&self, path: &str, body: Value) -> Result<Value, TronExactError> {
let response = self
.http
.post(self.endpoint(path))
.json(&body)
.send()
.await
.map_err(|e| TronExactError::TronGrid(e.to_string()))?;
response
.json::<Value>()
.await
.map_err(|e| TronExactError::TronGrid(e.to_string()))
}
pub async fn trigger_constant_contract(
&self,
owner: EvmAddress,
contract: EvmAddress,
calldata: &Bytes,
) -> Result<Bytes, TronExactError> {
let (selector, parameter) = split_calldata(calldata);
let body = json!({
"owner_address": format!("41{}", hex::encode(owner)),
"contract_address": format!("41{}", hex::encode(contract)),
"function_selector": selector,
"parameter": parameter,
"visible": false,
});
let response = self
.post_json("wallet/triggerconstantcontract", body)
.await?;
let ok = response
.get("result")
.and_then(|r| r.get("result"))
.and_then(Value::as_bool)
.unwrap_or(false);
if !ok {
let message = response
.get("result")
.and_then(|r| r.get("message"))
.and_then(Value::as_str)
.unwrap_or("triggerconstantcontract failed")
.to_owned();
return Err(TronExactError::TronGrid(message));
}
let hex_result = response
.get("constant_result")
.and_then(Value::as_array)
.and_then(|arr| arr.first())
.and_then(Value::as_str)
.ok_or_else(|| TronExactError::TronGrid("missing constant_result".to_owned()))?;
let bytes = hex::decode(hex_result)
.map_err(|e| TronExactError::TronGrid(format!("invalid hex result: {e}")))?;
Ok(Bytes::from(bytes))
}
pub async fn trigger_smart_contract(
&self,
owner: EvmAddress,
contract: EvmAddress,
calldata: &Bytes,
fee_limit: u64,
) -> Result<UnsignedTransaction, TronExactError> {
let (selector, parameter) = split_calldata(calldata);
let body = json!({
"owner_address": format!("41{}", hex::encode(owner)),
"contract_address": format!("41{}", hex::encode(contract)),
"function_selector": selector,
"parameter": parameter,
"fee_limit": fee_limit,
"call_value": 0,
"visible": false,
});
let response = self.post_json("wallet/triggersmartcontract", body).await?;
let ok = response
.get("result")
.and_then(|r| r.get("result"))
.and_then(Value::as_bool)
.unwrap_or(false);
if !ok {
let message = response
.get("result")
.and_then(|r| r.get("message"))
.and_then(Value::as_str)
.unwrap_or("triggersmartcontract failed")
.to_owned();
return Err(TronExactError::TronGrid(message));
}
let transaction = response
.get("transaction")
.cloned()
.ok_or_else(|| TronExactError::TronGrid("missing transaction".to_owned()))?;
let tx_id_hex = transaction
.get("txID")
.and_then(Value::as_str)
.ok_or_else(|| TronExactError::TronGrid("missing txID".to_owned()))?;
let tx_id_vec = hex::decode(tx_id_hex)
.map_err(|e| TronExactError::TronGrid(format!("invalid txID: {e}")))?;
let tx_id: [u8; 32] = tx_id_vec
.try_into()
.map_err(|_| TronExactError::TronGrid("txID is not 32 bytes".to_owned()))?;
Ok(UnsignedTransaction {
tx_id,
raw: transaction,
})
}
pub async fn broadcast_transaction(
&self,
mut unsigned: UnsignedTransaction,
signature: &[u8],
) -> Result<String, TronExactError> {
let Value::Object(ref mut map) = unsigned.raw else {
return Err(TronExactError::TronGrid(
"malformed transaction envelope".to_owned(),
));
};
let _ = map.insert(
"signature".to_owned(),
Value::Array(vec![Value::String(hex::encode(signature))]),
);
let response = self
.post_json("wallet/broadcasttransaction", unsigned.raw)
.await?;
let ok = response
.get("result")
.and_then(Value::as_bool)
.unwrap_or(false);
if !ok {
let message = response.get("message").and_then(Value::as_str).map_or_else(
|| "broadcasttransaction failed".to_owned(),
|b64_or_text| {
hex::decode(b64_or_text)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
.unwrap_or_else(|| b64_or_text.to_owned())
},
);
return Err(TronExactError::TransactionFailed(message));
}
Ok(hex::encode(unsigned.tx_id))
}
pub async fn get_transaction_info(
&self,
tx_id_hex: &str,
) -> Result<TransactionInfo, TronExactError> {
let body = json!({ "value": tx_id_hex });
let response = self
.post_json("wallet/gettransactioninfobyid", body)
.await?;
serde_json::from_value(response)
.map_err(|e| TronExactError::TronGrid(format!("malformed transaction info: {e}")))
}
pub async fn wait_for_confirmation(
&self,
tx_id_hex: &str,
timeout: Duration,
poll_interval: Duration,
) -> Result<TransactionInfo, TronExactError> {
let deadline = tokio::time::Instant::now() + timeout;
loop {
let info = self.get_transaction_info(tx_id_hex).await?;
if info.is_confirmed() {
return Self::confirm_result(tx_id_hex, info);
}
if tokio::time::Instant::now() >= deadline {
return Err(TronExactError::ConfirmationTimeout);
}
tokio::time::sleep(poll_interval).await;
}
}
fn confirm_result(
tx_id_hex: &str,
info: TransactionInfo,
) -> Result<TransactionInfo, TronExactError> {
if info.is_success() {
Ok(info)
} else {
Err(TronExactError::TransactionFailed(format!(
"transaction {tx_id_hex} reverted"
)))
}
}
}
fn split_calldata(calldata: &Bytes) -> (String, String) {
let selector = hex::encode(calldata.get(..4).unwrap_or_default());
let parameter = hex::encode(calldata.get(4..).unwrap_or_default());
(selector, parameter)
}
#[derive(Debug, Clone)]
pub struct TronChainProviderConfig {
pub chain_reference: TronChainReference,
pub base_url: Url,
pub signer: PrivateKeySigner,
pub fee_limit: u64,
pub confirmation_timeout: Duration,
pub confirmation_poll_interval: Duration,
}
pub struct TronChainProvider {
chain_reference: TronChainReference,
grid: TronGridClient,
signer: PrivateKeySigner,
fee_limit: u64,
confirmation_timeout: Duration,
confirmation_poll_interval: Duration,
}
impl Debug for TronChainProvider {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TronChainProvider")
.field("chain_reference", &self.chain_reference)
.field("signer", &self.signer.address())
.field("fee_limit", &self.fee_limit)
.finish_non_exhaustive()
}
}
impl TronChainProvider {
#[must_use]
pub fn new(config: TronChainProviderConfig) -> Self {
#[cfg(feature = "telemetry")]
tracing::info!(
chain = %ChainId::from(config.chain_reference),
signer = %config.signer.address(),
base_url = %config.base_url,
"Using Tron provider"
);
Self {
chain_reference: config.chain_reference,
grid: TronGridClient::new(config.base_url),
signer: config.signer,
fee_limit: config.fee_limit,
confirmation_timeout: config.confirmation_timeout,
confirmation_poll_interval: config.confirmation_poll_interval,
}
}
#[must_use]
pub const fn chain_reference(&self) -> TronChainReference {
self.chain_reference
}
#[must_use]
pub const fn grid(&self) -> &TronGridClient {
&self.grid
}
#[must_use]
pub const fn signer_address(&self) -> EvmAddress {
self.signer.address()
}
pub async fn trc20_balance_of(
&self,
token: EvmAddress,
account: EvmAddress,
) -> Result<U256, TronExactError> {
let call = crate::chain::contracts::trc20::balanceOfCall { account };
let calldata =
<crate::chain::contracts::trc20::balanceOfCall as alloy_sol_types::SolCall>::abi_encode(
&call,
);
let result = self
.grid
.trigger_constant_contract(self.signer_address(), token, &Bytes::from(calldata))
.await?;
let padded: [u8; 32] = result
.get(..32)
.and_then(|slice| slice.try_into().ok())
.ok_or_else(|| TronExactError::TronGrid("malformed balanceOf result".to_owned()))?;
Ok(U256::from_be_bytes(padded))
}
pub async fn send_contract_call(
&self,
contract: EvmAddress,
calldata: Bytes,
) -> Result<String, TronExactError> {
let unsigned = self
.grid
.trigger_smart_contract(self.signer_address(), contract, &calldata, self.fee_limit)
.await?;
let signature = self
.signer
.sign_hash(&unsigned.tx_id.into())
.await
.map_err(|e| TronExactError::SignatureRecovery(e.to_string()))?;
let tx_id = self
.grid
.broadcast_transaction(unsigned, signature.as_bytes().as_ref())
.await?;
let info = self
.grid
.wait_for_confirmation(
&tx_id,
self.confirmation_timeout,
self.confirmation_poll_interval,
)
.await?;
Ok(info.id)
}
}
impl ChainProvider for TronChainProvider {
fn signer_addresses(&self) -> Vec<String> {
vec![Address::from_evm(self.signer.address()).to_string()]
}
fn chain_id(&self) -> ChainId {
self.chain_reference.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_calldata_extracts_selector_and_parameter() {
let calldata = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04]);
let (selector, parameter) = split_calldata(&calldata);
assert_eq!(selector, "deadbeef");
assert_eq!(parameter, "01020304");
}
}