1use crate::chains::{Balance, ChainClient, Token, TokenHolder, Transaction};
32use crate::config::ChainsConfig;
33use crate::error::{Result, ScopeError};
34use crate::http::{HttpClient, Request};
35use async_trait::async_trait;
36use serde::Deserialize;
37use serde_json;
38use sha2::{Digest, Sha256};
39use std::sync::Arc;
40
41const DEFAULT_TRON_API: &str = "https://api.trongrid.io";
43
44const TRONSCAN_API: &str = "https://apilist.tronscanapi.com";
46
47const DEXSCREENER_TRX_SEARCH: &str = "https://api.dexscreener.com/latest/dex/search?q=TRX%20USDT";
49
50const TRX_DECIMALS: u8 = 6;
52
53#[derive(Clone)]
57pub struct TronClient {
58 http: Arc<dyn HttpClient>,
60
61 api_url: String,
63
64 api_key: Option<String>,
66}
67
68#[derive(Debug, Deserialize)]
70struct AccountResponse {
71 data: Vec<AccountData>,
72 success: bool,
73 error: Option<String>,
74}
75
76#[derive(Debug, Deserialize)]
78#[allow(dead_code)] struct AccountData {
80 balance: Option<u64>,
81 address: String,
82 create_time: Option<u64>,
83 #[serde(default)]
84 trc20: Vec<Trc20Balance>,
85}
86
87#[derive(Debug, Deserialize)]
89#[allow(dead_code)] struct Trc20Balance {
91 #[serde(flatten)]
92 balances: std::collections::HashMap<String, String>,
93}
94
95#[derive(Debug, Deserialize)]
97struct TransactionListResponse {
98 data: Vec<TronTransaction>,
99 success: bool,
100 error: Option<String>,
101}
102
103#[derive(Debug, Deserialize)]
105struct TronTransaction {
106 #[serde(rename = "txID")]
107 tx_id: String,
108 block_number: Option<u64>,
109 block_timestamp: Option<u64>,
110 raw_data: Option<RawData>,
111 ret: Option<Vec<TransactionResult>>,
112}
113
114#[derive(Debug, Deserialize)]
116struct RawData {
117 contract: Option<Vec<Contract>>,
118}
119
120#[derive(Debug, Deserialize)]
122#[allow(dead_code)] struct Contract {
124 parameter: Option<ContractParameter>,
125 #[serde(rename = "type")]
126 contract_type: Option<String>,
127}
128
129#[derive(Debug, Deserialize)]
131struct ContractParameter {
132 value: Option<ContractValue>,
133}
134
135#[derive(Debug, Deserialize)]
137struct ContractValue {
138 amount: Option<u64>,
139 owner_address: Option<String>,
140 to_address: Option<String>,
141}
142
143#[derive(Debug, Deserialize)]
145struct TransactionResult {
146 #[serde(rename = "contractRet")]
147 contract_ret: Option<String>,
148}
149
150impl TronClient {
151 pub fn new(config: &ChainsConfig) -> Result<Self> {
171 Self::new_with_http(config, crate::http::transport())
172 }
173
174 pub fn new_with_http(config: &ChainsConfig, http: Arc<dyn HttpClient>) -> Result<Self> {
176 let api_url = config
177 .tron_api
178 .as_deref()
179 .unwrap_or(DEFAULT_TRON_API)
180 .to_string();
181
182 Ok(Self {
183 http,
184 api_url,
185 api_key: config.api_keys.get("tronscan").cloned(),
186 })
187 }
188
189 pub fn with_api_url(api_url: &str) -> Self {
195 Self {
196 http: crate::http::transport(),
197 api_url: api_url.to_string(),
198 api_key: None,
199 }
200 }
201
202 pub fn chain_name(&self) -> &str {
204 "tron"
205 }
206
207 pub fn native_token_symbol(&self) -> &str {
209 "TRX"
210 }
211
212 pub async fn get_balance(&self, address: &str) -> Result<Balance> {
227 validate_tron_address(address)?;
229
230 let url = format!("{}/v1/accounts/{}", self.api_url, address);
231
232 tracing::debug!(url = %url, address = %address, "Fetching Tron balance");
233
234 let mut req = Request::get(&url);
235 if let Some(ref key) = self.api_key {
236 req = req.with_header("TRON-PRO-API-KEY", key);
237 }
238
239 let response: AccountResponse = self.http.send(req).await?.json()?;
240
241 if !response.success {
242 return Err(ScopeError::Chain(format!(
243 "TronGrid API error: {}",
244 response.error.unwrap_or_else(|| "Unknown error".into())
245 )));
246 }
247
248 let sun = response.data.first().and_then(|d| d.balance).unwrap_or(0);
250
251 let trx = sun as f64 / 10_f64.powi(TRX_DECIMALS as i32);
252
253 Ok(Balance {
254 raw: sun.to_string(),
255 formatted: format!("{:.6} TRX", trx),
256 decimals: TRX_DECIMALS,
257 symbol: "TRX".to_string(),
258 usd_value: None, })
260 }
261
262 pub async fn get_trc20_balances(&self, address: &str) -> Result<Vec<Trc20TokenBalance>> {
267 validate_tron_address(address)?;
268
269 let url = format!("{}/v1/accounts/{}", self.api_url, address);
270
271 tracing::debug!(url = %url, "Fetching TRC-20 token balances");
272
273 let mut req = Request::get(&url);
274 if let Some(ref key) = self.api_key {
275 req = req.with_header("TRON-PRO-API-KEY", key);
276 }
277
278 let response: AccountResponse = self.http.send(req).await?.json()?;
279
280 if !response.success {
281 return Err(ScopeError::Chain(format!(
282 "TronGrid API error: {}",
283 response.error.unwrap_or_else(|| "Unknown error".into())
284 )));
285 }
286
287 let account = match response.data.first() {
288 Some(data) => data,
289 None => return Ok(vec![]),
290 };
291
292 let mut balances = Vec::new();
293 for trc20 in &account.trc20 {
294 for (contract_address, raw_balance) in &trc20.balances {
295 if raw_balance == "0" {
297 continue;
298 }
299 balances.push(Trc20TokenBalance {
300 contract_address: contract_address.clone(),
301 raw_balance: raw_balance.clone(),
302 });
303 }
304 }
305
306 Ok(balances)
307 }
308
309 pub async fn get_token_info(&self, contract_address: &str) -> Result<Token> {
313 validate_tron_address(contract_address)?;
314
315 let url = format!(
316 "{}/api/token_trc20?contract={}&showAll=1",
317 TRONSCAN_API, contract_address
318 );
319
320 tracing::debug!(url = %url, "Fetching TRC-20 token info via Tronscan");
321
322 let mut req = Request::get(&url);
323 if let Some(ref key) = self.api_key {
324 req = req.with_header("TRON-PRO-API-KEY", key);
325 }
326
327 let resp = self.http.send(req).await?;
328 let json: serde_json::Value = serde_json::from_str(&resp.body)
329 .map_err(|e| ScopeError::Api(format!("Failed to parse Tronscan response: {}", e)))?;
330
331 let tokens = json
332 .get("trc20_tokens")
333 .and_then(|v| v.as_array())
334 .ok_or_else(|| {
335 ScopeError::NotFound(format!(
336 "No token info found for TRC-20 contract {}",
337 contract_address
338 ))
339 })?;
340
341 let token_data = tokens.first().ok_or_else(|| {
342 ScopeError::NotFound(format!(
343 "No token info found for TRC-20 contract {}",
344 contract_address
345 ))
346 })?;
347
348 let symbol = token_data
349 .get("symbol")
350 .and_then(|v| v.as_str())
351 .unwrap_or("UNKNOWN")
352 .to_string();
353 let name = token_data
354 .get("contract_name")
355 .or_else(|| token_data.get("name"))
356 .and_then(|v| v.as_str())
357 .unwrap_or("Unknown Token")
358 .to_string();
359 let decimals = token_data
360 .get("decimals")
361 .and_then(|v| v.as_u64())
362 .unwrap_or(6) as u8;
363
364 Ok(Token {
365 contract_address: contract_address.to_string(),
366 symbol,
367 name,
368 decimals,
369 })
370 }
371
372 pub async fn get_token_holders(
376 &self,
377 contract_address: &str,
378 limit: u32,
379 ) -> Result<Vec<TokenHolder>> {
380 validate_tron_address(contract_address)?;
381
382 let effective_limit = limit.min(100);
383 let url = format!(
384 "{}/api/token_trc20/holders?contract_address={}&start=0&limit={}",
385 TRONSCAN_API, contract_address, effective_limit
386 );
387
388 tracing::debug!(url = %url, "Fetching TRC-20 token holders via Tronscan");
389
390 let mut req = Request::get(&url);
391 if let Some(ref key) = self.api_key {
392 req = req.with_header("TRON-PRO-API-KEY", key);
393 }
394
395 let resp = self.http.send(req).await?;
396 let json: serde_json::Value = serde_json::from_str(&resp.body)
397 .map_err(|e| ScopeError::Api(format!("Failed to parse Tronscan holders: {}", e)))?;
398
399 let holders_data: &[serde_json::Value] = json
400 .get("trc20_tokens")
401 .and_then(|v| v.as_array())
402 .map(|v| v.as_slice())
403 .unwrap_or(&[]);
404
405 let token_info = self.get_token_info(contract_address).await;
407 let decimals = token_info.as_ref().map(|t| t.decimals).unwrap_or(6);
408
409 let total_balance: f64 = holders_data
411 .iter()
412 .filter_map(|h| h.get("balance").and_then(|v| v.as_str()))
413 .filter_map(|s| s.parse::<f64>().ok())
414 .sum();
415
416 let token_holders: Vec<TokenHolder> = holders_data
417 .iter()
418 .enumerate()
419 .filter_map(|(i, h)| {
420 let holder_address = h.get("holder_address")?.as_str()?.to_string();
421 let balance_raw = h.get("balance")?.as_str()?.to_string();
422 let balance: f64 = balance_raw.parse().ok()?;
423 let percentage = if total_balance > 0.0 {
424 (balance / total_balance) * 100.0
425 } else {
426 0.0
427 };
428 let divisor = 10_f64.powi(decimals as i32);
429 let formatted = format!("{:.6}", balance / divisor);
430
431 Some(TokenHolder {
432 address: holder_address,
433 balance: balance_raw,
434 formatted_balance: formatted,
435 percentage,
436 rank: (i + 1) as u32,
437 })
438 })
439 .collect();
440
441 Ok(token_holders)
442 }
443
444 pub async fn get_token_holder_count(&self, contract_address: &str) -> Result<u64> {
446 validate_tron_address(contract_address)?;
447
448 let url = format!(
449 "{}/api/token_trc20/holders?contract_address={}&start=0&limit=1",
450 TRONSCAN_API, contract_address
451 );
452
453 let mut req = Request::get(&url);
454 if let Some(ref key) = self.api_key {
455 req = req.with_header("TRON-PRO-API-KEY", key);
456 }
457
458 let resp = self.http.send(req).await?;
459 let json: serde_json::Value = resp
460 .json()
461 .map_err(|e| ScopeError::Api(format!("Failed to parse Tronscan response: {}", e)))?;
462
463 let count = json.get("rangeTotal").and_then(|v| v.as_u64()).unwrap_or(0);
464
465 Ok(count)
466 }
467
468 pub async fn enrich_balance_usd(&self, balance: &mut Balance) {
473 let url = DEXSCREENER_TRX_SEARCH;
475 if let Ok(resp) = self.http.send(Request::get(url)).await
476 && let Ok(search_result) = serde_json::from_str::<DexSearchResponse>(&resp.body)
477 && let Some(pairs) = search_result.pairs
478 {
479 for pair in &pairs {
480 if (pair.base_token_symbol.as_deref() == Some("TRX")
481 || pair.base_token_symbol.as_deref() == Some("WTRX"))
482 && let Some(price) = pair.price_usd.as_ref().and_then(|p| p.parse::<f64>().ok())
483 {
484 let sun: f64 = balance.raw.parse().unwrap_or(0.0);
485 let trx = sun / 10_f64.powi(TRX_DECIMALS as i32);
486 balance.usd_value = Some(trx * price);
487 return;
488 }
489 }
490 }
491 }
492
493 pub async fn get_transaction(&self, hash: &str) -> Result<Transaction> {
503 validate_tron_tx_hash(hash)?;
505
506 let url = format!("{}/v1/transactions/{}", self.api_url, hash);
507
508 tracing::debug!(url = %url, hash = %hash, "Fetching Tron transaction");
509
510 let mut req = Request::get(&url);
511 if let Some(ref key) = self.api_key {
512 req = req.with_header("TRON-PRO-API-KEY", key);
513 }
514
515 let response: TransactionListResponse = self.http.send(req).await?.json()?;
516
517 if !response.success {
518 return Err(ScopeError::Chain(format!(
519 "TronGrid API error: {}",
520 response.error.unwrap_or_else(|| "Unknown error".into())
521 )));
522 }
523
524 let tx = response
525 .data
526 .into_iter()
527 .next()
528 .ok_or_else(|| ScopeError::Chain("Transaction not found".into()))?;
529
530 let (from, to, value) = tx
532 .raw_data
533 .and_then(|rd| rd.contract)
534 .and_then(|contracts| contracts.into_iter().next())
535 .and_then(|c| c.parameter)
536 .and_then(|p| p.value)
537 .map(|v| {
538 (
539 v.owner_address.unwrap_or_default(),
540 v.to_address,
541 v.amount.unwrap_or(0).to_string(),
542 )
543 })
544 .unwrap_or_else(|| (String::new(), None, "0".to_string()));
545
546 let status = tx
547 .ret
548 .and_then(|r| r.into_iter().next())
549 .and_then(|r| r.contract_ret)
550 .map(|s| s == "SUCCESS");
551
552 Ok(Transaction {
553 hash: tx.tx_id,
554 block_number: tx.block_number,
555 timestamp: tx.block_timestamp.map(|t| t / 1000), from,
557 to,
558 value,
559 gas_limit: 0, gas_used: None,
561 gas_price: "0".to_string(),
562 nonce: 0,
563 input: String::new(),
564 status,
565 })
566 }
567
568 pub async fn get_transactions(&self, address: &str, limit: u32) -> Result<Vec<Transaction>> {
579 validate_tron_address(address)?;
580
581 let url = format!(
582 "{}/v1/accounts/{}/transactions?limit={}",
583 self.api_url, address, limit
584 );
585
586 tracing::debug!(url = %url, address = %address, "Fetching Tron transactions");
587
588 let mut req = Request::get(&url);
589 if let Some(ref key) = self.api_key {
590 req = req.with_header("TRON-PRO-API-KEY", key);
591 }
592
593 let response: TransactionListResponse = self.http.send(req).await?.json()?;
594
595 if !response.success {
596 return Err(ScopeError::Chain(format!(
597 "TronGrid API error: {}",
598 response.error.unwrap_or_else(|| "Unknown error".into())
599 )));
600 }
601
602 let transactions = response
603 .data
604 .into_iter()
605 .map(|tx| {
606 let (from, to, value) = tx
607 .raw_data
608 .and_then(|rd| rd.contract)
609 .and_then(|contracts| contracts.into_iter().next())
610 .and_then(|c| c.parameter)
611 .and_then(|p| p.value)
612 .map(|v| {
613 (
614 v.owner_address.unwrap_or_default(),
615 v.to_address,
616 v.amount.unwrap_or(0).to_string(),
617 )
618 })
619 .unwrap_or_else(|| (String::new(), None, "0".to_string()));
620
621 let status = tx
622 .ret
623 .and_then(|r| r.into_iter().next())
624 .and_then(|r| r.contract_ret)
625 .map(|s| s == "SUCCESS");
626
627 Transaction {
628 hash: tx.tx_id,
629 block_number: tx.block_number,
630 timestamp: tx.block_timestamp.map(|t| t / 1000),
631 from,
632 to,
633 value,
634 gas_limit: 0,
635 gas_used: None,
636 gas_price: "0".to_string(),
637 nonce: 0,
638 input: String::new(),
639 status,
640 }
641 })
642 .collect();
643
644 Ok(transactions)
645 }
646
647 pub async fn get_block_number(&self) -> Result<u64> {
649 let url = format!("{}/wallet/getnowblock", self.api_url);
650
651 #[derive(Deserialize)]
652 struct BlockResponse {
653 block_header: Option<BlockHeader>,
654 }
655
656 #[derive(Deserialize)]
657 struct BlockHeader {
658 raw_data: Option<BlockRawData>,
659 }
660
661 #[derive(Deserialize)]
662 struct BlockRawData {
663 number: Option<u64>,
664 }
665
666 let resp = self.http.send(Request::post_json(&url, "")).await?;
667 let response: BlockResponse = resp.json()?;
668
669 response
670 .block_header
671 .and_then(|h| h.raw_data)
672 .and_then(|d| d.number)
673 .ok_or_else(|| ScopeError::Chain("Invalid block response".into()))
674 }
675}
676
677impl Default for TronClient {
678 fn default() -> Self {
679 Self {
680 http: crate::http::transport(),
681 api_url: DEFAULT_TRON_API.to_string(),
682 api_key: None,
683 }
684 }
685}
686
687#[derive(Debug, Clone)]
700pub struct Trc20TokenBalance {
701 pub contract_address: String,
703 pub raw_balance: String,
705}
706
707#[derive(Debug, Deserialize)]
709struct DexSearchResponse {
710 #[serde(default)]
711 pairs: Option<Vec<DexSearchPair>>,
712}
713
714#[derive(Debug, Deserialize)]
716#[serde(rename_all = "camelCase")]
717struct DexSearchPair {
718 #[serde(default)]
719 base_token_symbol: Option<String>,
720 #[serde(default)]
721 price_usd: Option<String>,
722}
723
724pub fn validate_tron_address(address: &str) -> Result<()> {
728 if address.is_empty() {
729 return Err(ScopeError::InvalidAddress("Address cannot be empty".into()));
730 }
731
732 if !address.starts_with('T') {
734 return Err(ScopeError::InvalidAddress(format!(
735 "Tron address must start with 'T': {}",
736 address
737 )));
738 }
739
740 if address.len() != 34 {
742 return Err(ScopeError::InvalidAddress(format!(
743 "Tron address must be 34 characters, got {}: {}",
744 address.len(),
745 address
746 )));
747 }
748
749 match bs58::decode(address).into_vec() {
751 Ok(bytes) => {
752 if bytes.len() != 25 {
754 return Err(ScopeError::InvalidAddress(format!(
755 "Tron address must decode to 25 bytes, got {}: {}",
756 bytes.len(),
757 address
758 )));
759 }
760
761 if bytes[0] != 0x41 {
763 return Err(ScopeError::InvalidAddress(format!(
764 "Invalid Tron address prefix: {}",
765 address
766 )));
767 }
768
769 let payload = &bytes[0..21];
771 let hash1 = Sha256::digest(payload);
772 let hash2 = Sha256::digest(hash1);
773 let expected_checksum = &hash2[0..4];
774 let actual_checksum = &bytes[21..25];
775
776 if expected_checksum != actual_checksum {
777 return Err(ScopeError::InvalidAddress(format!(
778 "Invalid Tron address checksum: {}",
779 address
780 )));
781 }
782 }
783 Err(e) => {
784 return Err(ScopeError::InvalidAddress(format!(
785 "Invalid base58 encoding: {}: {}",
786 e, address
787 )));
788 }
789 }
790
791 Ok(())
792}
793
794pub fn validate_tron_tx_hash(hash: &str) -> Result<()> {
806 if hash.is_empty() {
807 return Err(ScopeError::InvalidHash("Hash cannot be empty".into()));
808 }
809
810 if hash.len() != 64 {
812 return Err(ScopeError::InvalidHash(format!(
813 "Tron transaction hash must be 64 characters, got {}: {}",
814 hash.len(),
815 hash
816 )));
817 }
818
819 if !hash.chars().all(|c| c.is_ascii_hexdigit()) {
821 return Err(ScopeError::InvalidHash(format!(
822 "Tron hash contains invalid hex characters: {}",
823 hash
824 )));
825 }
826
827 Ok(())
828}
829
830#[async_trait]
835impl ChainClient for TronClient {
836 fn chain_name(&self) -> &str {
837 "tron"
838 }
839
840 fn native_token_symbol(&self) -> &str {
841 "TRX"
842 }
843
844 async fn get_balance(&self, address: &str) -> Result<Balance> {
845 self.get_balance(address).await
846 }
847
848 async fn enrich_balance_usd(&self, balance: &mut Balance) {
849 self.enrich_balance_usd(balance).await
850 }
851
852 async fn get_transaction(&self, hash: &str) -> Result<Transaction> {
853 self.get_transaction(hash).await
854 }
855
856 async fn get_transactions(&self, address: &str, limit: u32) -> Result<Vec<Transaction>> {
857 self.get_transactions(address, limit).await
858 }
859
860 async fn get_block_number(&self) -> Result<u64> {
861 self.get_block_number().await
862 }
863
864 async fn get_token_balances(&self, address: &str) -> Result<Vec<crate::chains::TokenBalance>> {
865 let trc20_balances = self.get_trc20_balances(address).await?;
866 let mut result = Vec::with_capacity(trc20_balances.len());
867
868 for tb in trc20_balances {
869 let token = match self.get_token_info(&tb.contract_address).await {
870 Ok(info) => info,
871 Err(e) => {
872 tracing::debug!(
873 contract = %tb.contract_address,
874 error = %e,
875 "Could not fetch TRC-20 token info, using placeholder"
876 );
877 Token {
878 contract_address: tb.contract_address.clone(),
879 symbol: "TRC20".to_string(),
880 name: "TRC-20 Token".to_string(),
881 decimals: 6, }
883 }
884 };
885
886 let raw: f64 = tb.raw_balance.parse().unwrap_or(0.0);
887 let divisor = 10_f64.powi(token.decimals as i32);
888 let formatted = format!("{:.6}", raw / divisor);
889
890 result.push(crate::chains::TokenBalance {
891 token,
892 balance: tb.raw_balance,
893 formatted_balance: formatted,
894 usd_value: None,
895 });
896 }
897
898 Ok(result)
899 }
900
901 async fn get_token_info(&self, address: &str) -> Result<Token> {
902 self.get_token_info(address).await
903 }
904
905 async fn get_token_holders(&self, address: &str, limit: u32) -> Result<Vec<TokenHolder>> {
906 self.get_token_holders(address, limit).await
907 }
908
909 async fn get_token_holder_count(&self, address: &str) -> Result<u64> {
910 self.get_token_holder_count(address).await
911 }
912}
913
914#[cfg(test)]
919mod tests {
920 use super::*;
921
922 const VALID_ADDRESS: &str = "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf";
924
925 const VALID_TX_HASH: &str = "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b";
927
928 #[test]
929 fn test_validate_tron_address_valid() {
930 assert!(validate_tron_address(VALID_ADDRESS).is_ok());
931 }
932
933 #[test]
934 fn test_validate_tron_address_empty() {
935 let result = validate_tron_address("");
936 assert!(result.is_err());
937 assert!(result.unwrap_err().to_string().contains("empty"));
938 }
939
940 #[test]
941 fn test_validate_tron_address_wrong_prefix() {
942 let result = validate_tron_address("ADqSquXBgUCLYvYC4XZgrprLK589dkhSCf");
943 assert!(result.is_err());
944 assert!(result.unwrap_err().to_string().contains("start with 'T'"));
945 }
946
947 #[test]
948 fn test_validate_tron_address_too_short() {
949 let result = validate_tron_address("TDqSquXBgUCLYvYC4XZ");
950 assert!(result.is_err());
951 assert!(result.unwrap_err().to_string().contains("34 characters"));
952 }
953
954 #[test]
955 fn test_validate_tron_address_too_long() {
956 let result = validate_tron_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCfAAAA");
957 assert!(result.is_err());
958 assert!(result.unwrap_err().to_string().contains("34 characters"));
959 }
960
961 #[test]
962 fn test_validate_tron_address_invalid_base58() {
963 let result = validate_tron_address("T0qSquXBgUCLYvYC4XZgrprLK589dkhSCf");
965 assert!(result.is_err());
966 assert!(result.unwrap_err().to_string().contains("base58"));
967 }
968
969 #[test]
970 fn test_validate_tron_tx_hash_valid() {
971 assert!(validate_tron_tx_hash(VALID_TX_HASH).is_ok());
972 }
973
974 #[test]
975 fn test_validate_tron_tx_hash_empty() {
976 let result = validate_tron_tx_hash("");
977 assert!(result.is_err());
978 assert!(result.unwrap_err().to_string().contains("empty"));
979 }
980
981 #[test]
982 fn test_validate_tron_tx_hash_too_short() {
983 let result = validate_tron_tx_hash("b3c12d62ad7e7b8b83b09a68");
984 assert!(result.is_err());
985 assert!(result.unwrap_err().to_string().contains("64 characters"));
986 }
987
988 #[test]
989 fn test_validate_tron_tx_hash_invalid_hex() {
990 let hash = "g3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b";
991 let result = validate_tron_tx_hash(hash);
992 assert!(result.is_err());
993 assert!(result.unwrap_err().to_string().contains("invalid hex"));
994 }
995
996 #[test]
997 fn test_tron_client_default() {
998 let client = TronClient::default();
999 assert_eq!(client.chain_name(), "tron");
1000 assert_eq!(client.native_token_symbol(), "TRX");
1001 assert!(client.api_url.contains("trongrid"));
1002 }
1003
1004 #[test]
1005 fn test_tron_client_with_api_url() {
1006 let client = TronClient::with_api_url("https://custom.tron.api");
1007 assert_eq!(client.api_url, "https://custom.tron.api");
1008 }
1009
1010 #[test]
1011 fn test_tron_client_new() {
1012 let config = ChainsConfig::default();
1013 let client = TronClient::new(&config);
1014 assert!(client.is_ok());
1015 }
1016
1017 #[test]
1018 fn test_tron_client_new_with_custom_api() {
1019 let config = ChainsConfig {
1020 tron_api: Some("https://my-tron-api.com".to_string()),
1021 ..Default::default()
1022 };
1023 let client = TronClient::new(&config).unwrap();
1024 assert_eq!(client.api_url, "https://my-tron-api.com");
1025 }
1026
1027 #[test]
1028 fn test_tron_client_new_with_api_key() {
1029 use std::collections::HashMap;
1030
1031 let mut api_keys = HashMap::new();
1032 api_keys.insert("tronscan".to_string(), "test-key".to_string());
1033
1034 let config = ChainsConfig {
1035 api_keys,
1036 ..Default::default()
1037 };
1038
1039 let client = TronClient::new(&config).unwrap();
1040 assert_eq!(client.api_key, Some("test-key".to_string()));
1041 }
1042
1043 #[test]
1044 fn test_account_response_deserialization() {
1045 let json = r#"{
1046 "data": [{
1047 "balance": 1000000,
1048 "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1049 "create_time": 1600000000000,
1050 "trc20": []
1051 }],
1052 "success": true
1053 }"#;
1054
1055 let response: AccountResponse = serde_json::from_str(json).unwrap();
1056 assert!(response.success);
1057 assert_eq!(response.data.len(), 1);
1058 assert_eq!(response.data[0].balance, Some(1_000_000));
1059 }
1060
1061 #[test]
1062 fn test_transaction_response_deserialization() {
1063 let json = r#"{
1064 "data": [{
1065 "txID": "abc123",
1066 "block_number": 12345,
1067 "block_timestamp": 1600000000000,
1068 "ret": [{"contractRet": "SUCCESS"}]
1069 }],
1070 "success": true
1071 }"#;
1072
1073 let response: TransactionListResponse = serde_json::from_str(json).unwrap();
1074 assert!(response.success);
1075 assert_eq!(response.data.len(), 1);
1076 assert_eq!(response.data[0].tx_id, "abc123");
1077 }
1078
1079 #[tokio::test]
1084 async fn test_get_balance() {
1085 let mut server = mockito::Server::new_async().await;
1086 let _mock = server
1087 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
1088 .with_status(200)
1089 .with_header("content-type", "application/json")
1090 .with_body(r#"{
1091 "data": [{"balance": 5000000, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": []}],
1092 "success": true
1093 }"#)
1094 .create_async()
1095 .await;
1096
1097 let client = TronClient::with_api_url(&server.url());
1098 let balance = client.get_balance(VALID_ADDRESS).await.unwrap();
1099 assert_eq!(balance.raw, "5000000");
1100 assert_eq!(balance.symbol, "TRX");
1101 assert!(balance.formatted.contains("5.000000"));
1102 }
1103
1104 #[tokio::test]
1105 async fn test_get_balance_new_account() {
1106 let mut server = mockito::Server::new_async().await;
1107 let _mock = server
1108 .mock(
1109 "GET",
1110 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1111 )
1112 .with_status(200)
1113 .with_header("content-type", "application/json")
1114 .with_body(r#"{"data": [], "success": true}"#)
1115 .create_async()
1116 .await;
1117
1118 let client = TronClient::with_api_url(&server.url());
1119 let balance = client.get_balance(VALID_ADDRESS).await.unwrap();
1120 assert_eq!(balance.raw, "0");
1121 assert!(balance.formatted.contains("0.000000"));
1122 }
1123
1124 #[tokio::test]
1125 async fn test_get_balance_api_error() {
1126 let mut server = mockito::Server::new_async().await;
1127 let _mock = server
1128 .mock(
1129 "GET",
1130 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1131 )
1132 .with_status(200)
1133 .with_header("content-type", "application/json")
1134 .with_body(r#"{"data": [], "success": false, "error": "Rate limit exceeded"}"#)
1135 .create_async()
1136 .await;
1137
1138 let client = TronClient::with_api_url(&server.url());
1139 let result = client.get_balance(VALID_ADDRESS).await;
1140 assert!(result.is_err());
1141 assert!(result.unwrap_err().to_string().contains("Rate limit"));
1142 }
1143
1144 #[tokio::test]
1145 async fn test_get_balance_invalid_address() {
1146 let client = TronClient::default();
1147 let result = client.get_balance("invalid").await;
1148 assert!(result.is_err());
1149 }
1150
1151 #[tokio::test]
1152 async fn test_get_transaction() {
1153 let mut server = mockito::Server::new_async().await;
1154 let _mock = server
1155 .mock(
1156 "GET",
1157 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1158 )
1159 .with_status(200)
1160 .with_header("content-type", "application/json")
1161 .with_body(
1162 r#"{
1163 "data": [{
1164 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1165 "block_number": 50000000,
1166 "block_timestamp": 1700000000000,
1167 "raw_data": {
1168 "contract": [{
1169 "parameter": {
1170 "value": {
1171 "amount": 1000000,
1172 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1173 "to_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"
1174 }
1175 },
1176 "type": "TransferContract"
1177 }]
1178 },
1179 "ret": [{"contractRet": "SUCCESS"}]
1180 }],
1181 "success": true
1182 }"#,
1183 )
1184 .create_async()
1185 .await;
1186
1187 let client = TronClient::with_api_url(&server.url());
1188 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
1189 assert_eq!(tx.hash, VALID_TX_HASH);
1190 assert_eq!(tx.from, "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf");
1191 assert_eq!(
1192 tx.to,
1193 Some("TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9".to_string())
1194 );
1195 assert_eq!(tx.value, "1000000");
1196 assert_eq!(tx.block_number, Some(50000000));
1197 assert_eq!(tx.timestamp, Some(1700000000)); assert!(tx.status.unwrap());
1199 }
1200
1201 #[tokio::test]
1202 async fn test_get_transaction_failed() {
1203 let mut server = mockito::Server::new_async().await;
1204 let _mock = server
1205 .mock(
1206 "GET",
1207 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1208 )
1209 .with_status(200)
1210 .with_header("content-type", "application/json")
1211 .with_body(
1212 r#"{
1213 "data": [{
1214 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1215 "block_number": 50000000,
1216 "block_timestamp": 1700000000000,
1217 "ret": [{"contractRet": "REVERT"}]
1218 }],
1219 "success": true
1220 }"#,
1221 )
1222 .create_async()
1223 .await;
1224
1225 let client = TronClient::with_api_url(&server.url());
1226 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
1227 assert!(!tx.status.unwrap()); }
1229
1230 #[tokio::test]
1231 async fn test_get_transaction_not_found() {
1232 let mut server = mockito::Server::new_async().await;
1233 let _mock = server
1234 .mock(
1235 "GET",
1236 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1237 )
1238 .with_status(200)
1239 .with_header("content-type", "application/json")
1240 .with_body(r#"{"data": [], "success": true}"#)
1241 .create_async()
1242 .await;
1243
1244 let client = TronClient::with_api_url(&server.url());
1245 let result = client.get_transaction(VALID_TX_HASH).await;
1246 assert!(result.is_err());
1247 }
1248
1249 #[tokio::test]
1250 async fn test_get_transactions() {
1251 let mut server = mockito::Server::new_async().await;
1252 let _mock = server
1253 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()))
1254 .with_status(200)
1255 .with_header("content-type", "application/json")
1256 .with_body(r#"{
1257 "data": [
1258 {
1259 "txID": "aaa111",
1260 "block_number": 50000000,
1261 "block_timestamp": 1700000000000,
1262 "raw_data": {"contract": [{"parameter": {"value": {"amount": 500000, "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "to_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"}}, "type": "TransferContract"}]},
1263 "ret": [{"contractRet": "SUCCESS"}]
1264 },
1265 {
1266 "txID": "bbb222",
1267 "block_number": 50000001,
1268 "block_timestamp": 1700000060000,
1269 "ret": [{"contractRet": "SUCCESS"}]
1270 }
1271 ],
1272 "success": true
1273 }"#)
1274 .create_async()
1275 .await;
1276
1277 let client = TronClient::with_api_url(&server.url());
1278 let txs = client.get_transactions(VALID_ADDRESS, 10).await.unwrap();
1279 assert_eq!(txs.len(), 2);
1280 assert_eq!(txs[0].hash, "aaa111");
1281 assert_eq!(txs[0].value, "500000");
1282 assert!(txs[0].status.unwrap());
1283 assert_eq!(txs[1].value, "0");
1285 }
1286
1287 #[tokio::test]
1288 async fn test_get_transactions_error() {
1289 let mut server = mockito::Server::new_async().await;
1290 let _mock = server
1291 .mock(
1292 "GET",
1293 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1294 )
1295 .with_status(200)
1296 .with_header("content-type", "application/json")
1297 .with_body(r#"{"data": [], "success": false, "error": "Invalid address"}"#)
1298 .create_async()
1299 .await;
1300
1301 let client = TronClient::with_api_url(&server.url());
1302 let result = client.get_transactions(VALID_ADDRESS, 10).await;
1303 assert!(result.is_err());
1304 }
1305
1306 #[tokio::test]
1307 async fn test_get_trc20_balances() {
1308 let mut server = mockito::Server::new_async().await;
1309 let _mock = server
1310 .mock(
1311 "GET",
1312 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1313 )
1314 .with_status(200)
1315 .with_header("content-type", "application/json")
1316 .with_body(
1317 r#"{
1318 "data": [{
1319 "balance": 1000000,
1320 "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1321 "trc20": [
1322 {"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t": "5000000"},
1323 {"TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8": "0"}
1324 ]
1325 }],
1326 "success": true
1327 }"#,
1328 )
1329 .create_async()
1330 .await;
1331
1332 let client = TronClient::with_api_url(&server.url());
1333 let balances = client.get_trc20_balances(VALID_ADDRESS).await.unwrap();
1334 assert_eq!(balances.len(), 1);
1336 assert_eq!(
1337 balances[0].contract_address,
1338 "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
1339 );
1340 assert_eq!(balances[0].raw_balance, "5000000");
1341 }
1342
1343 #[tokio::test]
1344 async fn test_get_trc20_balances_empty_account() {
1345 let mut server = mockito::Server::new_async().await;
1346 let _mock = server
1347 .mock(
1348 "GET",
1349 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1350 )
1351 .with_status(200)
1352 .with_header("content-type", "application/json")
1353 .with_body(r#"{"data": [], "success": true}"#)
1354 .create_async()
1355 .await;
1356
1357 let client = TronClient::with_api_url(&server.url());
1358 let balances = client.get_trc20_balances(VALID_ADDRESS).await.unwrap();
1359 assert!(balances.is_empty());
1360 }
1361
1362 #[tokio::test]
1363 async fn test_get_block_number() {
1364 let mut server = mockito::Server::new_async().await;
1365 let _mock = server
1366 .mock("POST", "/wallet/getnowblock")
1367 .with_status(200)
1368 .with_header("content-type", "application/json")
1369 .with_body(r#"{"block_header":{"raw_data":{"number":60000000}}}"#)
1370 .create_async()
1371 .await;
1372
1373 let client = TronClient::with_api_url(&server.url());
1374 let block = client.get_block_number().await.unwrap();
1375 assert_eq!(block, 60000000);
1376 }
1377
1378 #[tokio::test]
1379 async fn test_get_block_number_invalid_response() {
1380 let mut server = mockito::Server::new_async().await;
1381 let _mock = server
1382 .mock("POST", "/wallet/getnowblock")
1383 .with_status(200)
1384 .with_header("content-type", "application/json")
1385 .with_body(r#"{}"#)
1386 .create_async()
1387 .await;
1388
1389 let client = TronClient::with_api_url(&server.url());
1390 let result = client.get_block_number().await;
1391 assert!(result.is_err());
1392 }
1393
1394 #[test]
1395 fn test_validate_tron_address_wrong_decoded_length() {
1396 let result = validate_tron_address("TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT1");
1398 assert!(result.is_err());
1399 }
1400
1401 #[test]
1402 fn test_validate_tron_tx_hash_wrong_length() {
1403 let result = validate_tron_tx_hash("abc123");
1404 assert!(result.is_err());
1405 assert!(result.unwrap_err().to_string().contains("64 characters"));
1406 }
1407
1408 #[tokio::test]
1409 async fn test_get_transaction_success() {
1410 let mut server = mockito::Server::new_async().await;
1411 let valid_hash = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
1412 let _mock = server
1413 .mock(
1414 "GET",
1415 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1416 )
1417 .with_status(200)
1418 .with_header("content-type", "application/json")
1419 .with_body(
1420 r#"{"data":[{
1421 "txID":"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
1422 "blockNumber":60000000,
1423 "block_timestamp":1700000000000,
1424 "raw_data":{"contract":[{"parameter":{"value":{
1425 "owner_address":"TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1426 "to_address":"TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn",
1427 "amount":1000000
1428 }}}]},
1429 "ret":[{"contractRet":"SUCCESS"}]
1430 }],"success":true}"#,
1431 )
1432 .create_async()
1433 .await;
1434
1435 let client = TronClient::with_api_url(&server.url());
1436 let tx = client.get_transaction(valid_hash).await.unwrap();
1437 assert_eq!(tx.hash, valid_hash);
1438 assert_eq!(tx.status, Some(true));
1439 }
1440
1441 #[tokio::test]
1442 async fn test_get_transaction_api_error() {
1443 let mut server = mockito::Server::new_async().await;
1444 let valid_hash = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
1445 let _mock = server
1446 .mock(
1447 "GET",
1448 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1449 )
1450 .with_status(200)
1451 .with_header("content-type", "application/json")
1452 .with_body(r#"{"data":[],"success":false,"error":"Transaction not found"}"#)
1453 .create_async()
1454 .await;
1455
1456 let client = TronClient::with_api_url(&server.url());
1457 let result = client.get_transaction(valid_hash).await;
1458 assert!(result.is_err());
1459 }
1460
1461 #[tokio::test]
1462 async fn test_get_transactions_success() {
1463 let mut server = mockito::Server::new_async().await;
1464 let _mock = server
1465 .mock(
1466 "GET",
1467 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1468 )
1469 .with_status(200)
1470 .with_header("content-type", "application/json")
1471 .with_body(
1472 r#"{"data":[{
1473 "txID":"abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
1474 "blockNumber":60000000,
1475 "block_timestamp":1700000000000,
1476 "raw_data":{"contract":[{"parameter":{"value":{
1477 "owner_address":"TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1478 "amount":500000
1479 }}}]},
1480 "ret":[{"contractRet":"SUCCESS"}]
1481 }],"success":true}"#,
1482 )
1483 .create_async()
1484 .await;
1485
1486 let client = TronClient::with_api_url(&server.url());
1487 let txs = client.get_transactions(VALID_ADDRESS, 10).await.unwrap();
1488 assert_eq!(txs.len(), 1);
1489 }
1490
1491 #[tokio::test]
1492 async fn test_tron_chain_client_trait_accessors() {
1493 let client = TronClient::with_api_url("http://localhost");
1494 let chain_client: &dyn ChainClient = &client;
1495 assert_eq!(chain_client.chain_name(), "tron");
1496 assert_eq!(chain_client.native_token_symbol(), "TRX");
1497 }
1498
1499 #[tokio::test]
1500 async fn test_chain_client_trait_get_balance() {
1501 let mut server = mockito::Server::new_async().await;
1502 let _mock = server
1503 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
1504 .with_status(200)
1505 .with_header("content-type", "application/json")
1506 .with_body(r#"{"data": [{"balance": 1000000, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": []}], "success": true}"#)
1507 .create_async()
1508 .await;
1509
1510 let client = TronClient::with_api_url(&server.url());
1511 let chain_client: &dyn ChainClient = &client;
1512 let balance = chain_client.get_balance(VALID_ADDRESS).await.unwrap();
1513 assert_eq!(balance.symbol, "TRX");
1514 }
1515
1516 #[tokio::test]
1517 async fn test_chain_client_trait_get_block_number() {
1518 let mut server = mockito::Server::new_async().await;
1519 let _mock = server
1520 .mock("POST", "/wallet/getnowblock")
1521 .with_status(200)
1522 .with_header("content-type", "application/json")
1523 .with_body(r#"{"block_header":{"raw_data":{"number":60000000}}}"#)
1524 .create_async()
1525 .await;
1526
1527 let client = TronClient::with_api_url(&server.url());
1528 let chain_client: &dyn ChainClient = &client;
1529 let block = chain_client.get_block_number().await.unwrap();
1530 assert_eq!(block, 60000000);
1531 }
1532
1533 #[tokio::test]
1534 async fn test_chain_client_trait_get_token_balances() {
1535 let mut server = mockito::Server::new_async().await;
1536 let _mock = server
1537 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
1538 .with_status(200)
1539 .with_header("content-type", "application/json")
1540 .with_body(r#"{"data": [{"balance": 0, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": [{"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t": "5000000"}]}], "success": true}"#)
1541 .create_async()
1542 .await;
1543
1544 let client = TronClient::with_api_url(&server.url());
1545 let chain_client: &dyn ChainClient = &client;
1546 let balances = chain_client
1547 .get_token_balances(VALID_ADDRESS)
1548 .await
1549 .unwrap();
1550 assert_eq!(balances.len(), 1);
1551 assert!(
1553 balances[0].token.symbol == "USDT" || balances[0].token.symbol == "TRC20",
1554 "symbol should be USDT (Tronscan) or TRC20 (fallback)"
1555 );
1556 assert!(!balances[0].token.name.is_empty(), "name must be set");
1559 }
1560
1561 #[tokio::test]
1562 async fn test_chain_client_trait_get_transaction_tron() {
1563 let mut server = mockito::Server::new_async().await;
1564 let _mock = server
1565 .mock(
1566 "GET",
1567 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1568 )
1569 .with_status(200)
1570 .with_header("content-type", "application/json")
1571 .with_body(
1572 r#"{"data": [{
1573 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1574 "block_number": 50000000,
1575 "block_timestamp": 1700000000000,
1576 "raw_data": {
1577 "contract": [{
1578 "parameter": {
1579 "value": {
1580 "amount": 1000000,
1581 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1582 "to_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCg"
1583 }
1584 },
1585 "type": "TransferContract"
1586 }]
1587 },
1588 "ret": [{"contractRet": "SUCCESS"}]
1589 }], "success": true}"#,
1590 )
1591 .create_async()
1592 .await;
1593
1594 let client = TronClient::with_api_url(&server.url());
1595 let chain_client: &dyn ChainClient = &client;
1596 let tx = chain_client.get_transaction(VALID_TX_HASH).await.unwrap();
1597 assert_eq!(tx.hash, VALID_TX_HASH);
1598 assert!(tx.status.unwrap());
1599 }
1600
1601 #[tokio::test]
1602 async fn test_chain_client_trait_get_transactions_tron() {
1603 let mut server = mockito::Server::new_async().await;
1604 let _mock = server
1605 .mock(
1606 "GET",
1607 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1608 )
1609 .with_status(200)
1610 .with_header("content-type", "application/json")
1611 .with_body(
1612 r#"{"data": [{
1613 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1614 "block_number": 50000000,
1615 "block_timestamp": 1700000000000,
1616 "raw_data": {
1617 "contract": [{
1618 "parameter": {
1619 "value": {
1620 "amount": 2000000,
1621 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1622 "to_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCg"
1623 }
1624 }
1625 }]
1626 },
1627 "ret": [{"contractRet": "REVERT"}]
1628 }], "success": true}"#,
1629 )
1630 .create_async()
1631 .await;
1632
1633 let client = TronClient::with_api_url(&server.url());
1634 let chain_client: &dyn ChainClient = &client;
1635 let txs = chain_client
1636 .get_transactions(VALID_ADDRESS, 10)
1637 .await
1638 .unwrap();
1639 assert_eq!(txs.len(), 1);
1640 assert!(!txs[0].status.unwrap()); }
1642
1643 #[tokio::test]
1644 async fn test_get_balance_with_api_key() {
1645 let mut server = mockito::Server::new_async().await;
1646 let _mock = server
1647 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
1648 .with_status(200)
1649 .with_header("content-type", "application/json")
1650 .with_body(
1651 r#"{"data": [{"balance": 10000000, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": []}], "success": true}"#,
1652 )
1653 .create_async()
1654 .await;
1655
1656 let config = ChainsConfig {
1657 tron_api: Some(server.url()),
1658 api_keys: {
1659 let mut m = std::collections::HashMap::new();
1660 m.insert("tronscan".to_string(), "test-api-key".to_string());
1661 m
1662 },
1663 ..Default::default()
1664 };
1665 let client = TronClient::new(&config).unwrap();
1666 let balance = client.get_balance(VALID_ADDRESS).await.unwrap();
1667 assert_eq!(balance.symbol, "TRX");
1668 assert!(balance.formatted.contains("TRX"));
1669 }
1670
1671 #[tokio::test]
1672 async fn test_get_trc20_balances_error_response() {
1673 let mut server = mockito::Server::new_async().await;
1674 let _mock = server
1675 .mock(
1676 "GET",
1677 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1678 )
1679 .with_status(200)
1680 .with_header("content-type", "application/json")
1681 .with_body(r#"{"data": [], "success": false, "error": "Rate limit exceeded"}"#)
1682 .create_async()
1683 .await;
1684
1685 let client = TronClient::with_api_url(&server.url());
1686 let result = client.get_trc20_balances(VALID_ADDRESS).await;
1687 assert!(result.is_err());
1688 assert!(result.unwrap_err().to_string().contains("Rate limit"));
1689 }
1690
1691 #[tokio::test]
1692 async fn test_get_trc20_balances_no_data() {
1693 let mut server = mockito::Server::new_async().await;
1694 let _mock = server
1695 .mock(
1696 "GET",
1697 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1698 )
1699 .with_status(200)
1700 .with_header("content-type", "application/json")
1701 .with_body(r#"{"data": [], "success": true}"#)
1702 .create_async()
1703 .await;
1704
1705 let client = TronClient::with_api_url(&server.url());
1706 let balances = client.get_trc20_balances(VALID_ADDRESS).await.unwrap();
1707 assert!(balances.is_empty());
1708 }
1709
1710 #[tokio::test]
1711 async fn test_get_trc20_balances_with_api_key() {
1712 let mut server = mockito::Server::new_async().await;
1713 let _mock = server
1714 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
1715 .with_status(200)
1716 .with_header("content-type", "application/json")
1717 .with_body(
1718 r#"{"data": [{"balance": 0, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": [{"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t": "10000000"}]}], "success": true}"#,
1719 )
1720 .create_async()
1721 .await;
1722
1723 let config = ChainsConfig {
1724 tron_api: Some(server.url()),
1725 api_keys: {
1726 let mut m = std::collections::HashMap::new();
1727 m.insert("tronscan".to_string(), "my-api-key".to_string());
1728 m
1729 },
1730 ..Default::default()
1731 };
1732 let client = TronClient::new(&config).unwrap();
1733 let balances = client.get_trc20_balances(VALID_ADDRESS).await.unwrap();
1734 assert_eq!(balances.len(), 1);
1735 }
1736
1737 #[test]
1738 fn test_validate_tron_address_bad_checksum() {
1739 let result = validate_tron_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCe");
1742 assert!(result.is_err());
1743 let err_str = result.unwrap_err().to_string();
1745 assert!(
1746 err_str.contains("checksum")
1747 || err_str.contains("base58")
1748 || err_str.contains("prefix")
1749 );
1750 }
1751
1752 #[tokio::test]
1753 async fn test_get_transaction_tron_success() {
1754 let mut server = mockito::Server::new_async().await;
1755 let _mock = server
1756 .mock(
1757 "GET",
1758 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1759 )
1760 .with_status(200)
1761 .with_header("content-type", "application/json")
1762 .with_body(
1763 r#"{"data": [{
1764 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1765 "block_number": 50000000,
1766 "block_timestamp": 1700000000000,
1767 "raw_data": {
1768 "contract": [{
1769 "parameter": {
1770 "value": {
1771 "amount": 5000000,
1772 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1773 "to_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCg"
1774 }
1775 }
1776 }]
1777 },
1778 "ret": [{"contractRet": "SUCCESS"}]
1779 }], "success": true}"#,
1780 )
1781 .create_async()
1782 .await;
1783
1784 let client = TronClient::with_api_url(&server.url());
1785 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
1786 assert_eq!(tx.hash, VALID_TX_HASH);
1787 assert!(tx.status.unwrap());
1788 assert_eq!(tx.value, "5000000");
1789 assert_eq!(tx.timestamp, Some(1700000000)); }
1791
1792 #[tokio::test]
1793 async fn test_get_transaction_tron_error() {
1794 let mut server = mockito::Server::new_async().await;
1795 let _mock = server
1796 .mock(
1797 "GET",
1798 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1799 )
1800 .with_status(200)
1801 .with_header("content-type", "application/json")
1802 .with_body(r#"{"data": [], "success": false, "error": "Transaction not found"}"#)
1803 .create_async()
1804 .await;
1805
1806 let client = TronClient::with_api_url(&server.url());
1807 let result = client.get_transaction(VALID_TX_HASH).await;
1808 assert!(result.is_err());
1809 }
1810
1811 #[tokio::test]
1812 async fn test_get_transactions_tron_success() {
1813 let mut server = mockito::Server::new_async().await;
1814 let _mock = server
1815 .mock(
1816 "GET",
1817 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1818 )
1819 .with_status(200)
1820 .with_header("content-type", "application/json")
1821 .with_body(
1822 r#"{"data": [
1823 {
1824 "txID": "aaa12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1825 "block_number": 50000001,
1826 "block_timestamp": 1700000003000,
1827 "raw_data": {"contract": [{"parameter": {"value": {"amount": 1000000, "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"}}}]},
1828 "ret": [{"contractRet": "SUCCESS"}]
1829 },
1830 {
1831 "txID": "bbb12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1832 "block_number": 50000002,
1833 "block_timestamp": 1700000006000,
1834 "raw_data": {"contract": [{"parameter": {"value": {"amount": 2000000, "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "to_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCg"}}}]},
1835 "ret": [{"contractRet": "SUCCESS"}]
1836 }
1837 ], "success": true}"#,
1838 )
1839 .create_async()
1840 .await;
1841
1842 let client = TronClient::with_api_url(&server.url());
1843 let txs = client.get_transactions(VALID_ADDRESS, 10).await.unwrap();
1844 assert_eq!(txs.len(), 2);
1845 }
1846
1847 #[tokio::test]
1848 async fn test_get_transactions_tron_error() {
1849 let mut server = mockito::Server::new_async().await;
1850 let _mock = server
1851 .mock(
1852 "GET",
1853 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1854 )
1855 .with_status(200)
1856 .with_header("content-type", "application/json")
1857 .with_body(r#"{"data": [], "success": false, "error": "Invalid address"}"#)
1858 .create_async()
1859 .await;
1860
1861 let client = TronClient::with_api_url(&server.url());
1862 let result = client.get_transactions(VALID_ADDRESS, 10).await;
1863 assert!(result.is_err());
1864 }
1865
1866 #[tokio::test]
1867 async fn test_get_balance_error_response() {
1868 let mut server = mockito::Server::new_async().await;
1869 let _mock = server
1870 .mock(
1871 "GET",
1872 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1873 )
1874 .with_status(200)
1875 .with_header("content-type", "application/json")
1876 .with_body(r#"{"data": [], "success": false, "error": "Account not found"}"#)
1877 .create_async()
1878 .await;
1879
1880 let client = TronClient::with_api_url(&server.url());
1881 let result = client.get_balance(VALID_ADDRESS).await;
1882 assert!(result.is_err());
1883 assert!(
1884 result
1885 .unwrap_err()
1886 .to_string()
1887 .contains("Account not found")
1888 );
1889 }
1890
1891 #[tokio::test]
1892 async fn test_get_token_info_success() {
1893 let info: serde_json::Value = serde_json::from_str(
1896 r#"{"trc20_tokens": [{"symbol": "USDT", "contract_name": "TetherToken", "decimals": 6}]}"#,
1897 )
1898 .unwrap();
1899 let tokens = info.get("trc20_tokens").and_then(|v| v.as_array()).unwrap();
1900 let token_data = tokens.first().unwrap();
1901 let symbol = token_data
1902 .get("symbol")
1903 .and_then(|v| v.as_str())
1904 .unwrap_or("UNKNOWN");
1905 assert_eq!(symbol, "USDT");
1906 let name = token_data
1907 .get("contract_name")
1908 .and_then(|v| v.as_str())
1909 .unwrap_or("Unknown Token");
1910 assert_eq!(name, "TetherToken");
1911 let decimals = token_data
1912 .get("decimals")
1913 .and_then(|v| v.as_u64())
1914 .unwrap_or(6) as u8;
1915 assert_eq!(decimals, 6);
1916 }
1917
1918 #[tokio::test]
1919 async fn test_get_token_info_no_tokens() {
1920 let info: serde_json::Value = serde_json::from_str(r#"{"trc20_tokens": []}"#).unwrap();
1921 let tokens = info.get("trc20_tokens").and_then(|v| v.as_array()).unwrap();
1922 assert!(tokens.is_empty());
1923 }
1924
1925 #[tokio::test]
1926 async fn test_get_token_info_missing_field() {
1927 let info: serde_json::Value =
1928 serde_json::from_str(r#"{"trc20_tokens": [{"symbol": "TEST"}]}"#).unwrap();
1929 let tokens = info.get("trc20_tokens").and_then(|v| v.as_array()).unwrap();
1930 let token_data = tokens.first().unwrap();
1931 let name = token_data
1932 .get("contract_name")
1933 .or_else(|| token_data.get("name"))
1934 .and_then(|v| v.as_str())
1935 .unwrap_or("Unknown Token");
1936 assert_eq!(name, "Unknown Token");
1937 let decimals = token_data
1938 .get("decimals")
1939 .and_then(|v| v.as_u64())
1940 .unwrap_or(6) as u8;
1941 assert_eq!(decimals, 6);
1942 }
1943
1944 #[tokio::test]
1945 async fn test_token_holder_response_parsing() {
1946 let json: serde_json::Value = serde_json::from_str(
1947 r#"{"trc20_tokens": [
1948 {"holder_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "balance": "5000000"},
1949 {"holder_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9", "balance": "3000000"}
1950 ]}"#,
1951 )
1952 .unwrap();
1953 let holders_data: &[serde_json::Value] = json
1954 .get("trc20_tokens")
1955 .and_then(|v| v.as_array())
1956 .map(|v| v.as_slice())
1957 .unwrap_or(&[]);
1958 assert_eq!(holders_data.len(), 2);
1959
1960 let total_balance: f64 = holders_data
1961 .iter()
1962 .filter_map(|h| h.get("balance").and_then(|v| v.as_str()))
1963 .filter_map(|s| s.parse::<f64>().ok())
1964 .sum();
1965 assert_eq!(total_balance, 8000000.0);
1966
1967 let decimals: u8 = 6;
1968 let holders: Vec<TokenHolder> = holders_data
1969 .iter()
1970 .enumerate()
1971 .filter_map(|(i, h)| {
1972 let holder_address = h.get("holder_address")?.as_str()?.to_string();
1973 let balance_raw = h.get("balance")?.as_str()?.to_string();
1974 let balance: f64 = balance_raw.parse().ok()?;
1975 let percentage = if total_balance > 0.0 {
1976 (balance / total_balance) * 100.0
1977 } else {
1978 0.0
1979 };
1980 let divisor = 10_f64.powi(decimals as i32);
1981 let formatted = format!("{:.6}", balance / divisor);
1982 Some(TokenHolder {
1983 address: holder_address,
1984 balance: balance_raw,
1985 formatted_balance: formatted,
1986 percentage,
1987 rank: (i + 1) as u32,
1988 })
1989 })
1990 .collect();
1991 assert_eq!(holders.len(), 2);
1992 assert_eq!(holders[0].rank, 1);
1993 assert_eq!(holders[1].rank, 2);
1994 assert!(holders[0].percentage > 60.0);
1995 assert!(holders[0].formatted_balance.contains("5.000000"));
1996 }
1997
1998 #[tokio::test]
1999 async fn test_token_holder_count_parsing() {
2000 let json: serde_json::Value = serde_json::from_str(r#"{"rangeTotal": 12345}"#).unwrap();
2001 let count = json.get("rangeTotal").and_then(|v| v.as_u64()).unwrap_or(0);
2002 assert_eq!(count, 12345);
2003
2004 let json_no_field: serde_json::Value = serde_json::from_str(r#"{}"#).unwrap();
2005 let count2 = json_no_field
2006 .get("rangeTotal")
2007 .and_then(|v| v.as_u64())
2008 .unwrap_or(0);
2009 assert_eq!(count2, 0);
2010 }
2011
2012 #[test]
2013 fn test_dex_search_response_deserialization() {
2014 let json = r#"{"pairs":[{"baseTokenSymbol":"TRX","priceUsd":"0.08"}]}"#;
2015 let result: std::result::Result<DexSearchResponse, _> = serde_json::from_str(json);
2016 assert!(result.is_ok());
2017 let resp = result.unwrap();
2018 let pairs = resp.pairs.unwrap();
2019 assert_eq!(pairs.len(), 1);
2020 assert_eq!(pairs[0].price_usd, Some("0.08".to_string()));
2021 }
2022
2023 #[test]
2024 fn test_dex_search_response_empty() {
2025 let json = r#"{"pairs":[]}"#;
2026 let result: std::result::Result<DexSearchResponse, _> = serde_json::from_str(json);
2027 assert!(result.is_ok());
2028 assert!(result.unwrap().pairs.unwrap().is_empty());
2029 }
2030
2031 #[test]
2032 fn test_dex_search_response_no_pairs() {
2033 let json = r#"{}"#;
2034 let result: std::result::Result<DexSearchResponse, _> = serde_json::from_str(json);
2035 assert!(result.is_ok());
2036 assert!(result.unwrap().pairs.is_none());
2037 }
2038
2039 #[tokio::test]
2044 async fn test_get_transaction_invalid_hash() {
2045 let client = TronClient::default();
2046 let result = client.get_transaction("not-a-valid-hash").await;
2047 assert!(result.is_err());
2048 assert!(result.unwrap_err().to_string().contains("64 characters"));
2049 }
2050
2051 #[tokio::test]
2052 async fn test_get_transactions_invalid_address() {
2053 let client = TronClient::default();
2054 let result = client.get_transactions("invalid-address", 10).await;
2055 assert!(result.is_err());
2056 }
2057
2058 #[tokio::test]
2059 async fn test_get_token_info_invalid_address() {
2060 let client = TronClient::default();
2061 let result = client.get_token_info("bad-address").await;
2062 assert!(result.is_err());
2063 }
2064
2065 #[tokio::test]
2066 async fn test_get_token_holders_invalid_address() {
2067 let client = TronClient::default();
2068 let result = client.get_token_holders("bad-address", 10).await;
2069 assert!(result.is_err());
2070 }
2071
2072 #[tokio::test]
2073 async fn test_get_token_holder_count_invalid_address() {
2074 let client = TronClient::default();
2075 let result = client.get_token_holder_count("bad-address").await;
2076 assert!(result.is_err());
2077 }
2078
2079 #[tokio::test]
2080 async fn test_chain_client_get_token_info_invalid_address() {
2081 let client = TronClient::default();
2082 let chain_client: &dyn ChainClient = &client;
2083 let result = chain_client.get_token_info("x").await;
2084 assert!(result.is_err());
2085 }
2086
2087 #[tokio::test]
2088 async fn test_chain_client_get_token_holders_invalid_address() {
2089 let client = TronClient::default();
2090 let chain_client: &dyn ChainClient = &client;
2091 let result = chain_client.get_token_holders("x", 10).await;
2092 assert!(result.is_err());
2093 }
2094
2095 #[tokio::test]
2096 async fn test_chain_client_get_token_holder_count_invalid_address() {
2097 let client = TronClient::default();
2098 let chain_client: &dyn ChainClient = &client;
2099 let result = chain_client.get_token_holder_count("x").await;
2100 assert!(result.is_err());
2101 }
2102
2103 #[tokio::test]
2108 async fn test_get_balance_api_error_unknown_error() {
2109 let mut server = mockito::Server::new_async().await;
2110 let _mock = server
2111 .mock(
2112 "GET",
2113 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
2114 )
2115 .with_status(200)
2116 .with_header("content-type", "application/json")
2117 .with_body(r#"{"data": [], "success": false}"#)
2118 .create_async()
2119 .await;
2120
2121 let client = TronClient::with_api_url(&server.url());
2122 let result = client.get_balance(VALID_ADDRESS).await;
2123 assert!(result.is_err());
2124 assert!(result.unwrap_err().to_string().contains("Unknown error"));
2125 }
2126
2127 #[tokio::test]
2128 async fn test_get_transaction_status_none() {
2129 let mut server = mockito::Server::new_async().await;
2130 let _mock = server
2131 .mock(
2132 "GET",
2133 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
2134 )
2135 .with_status(200)
2136 .with_header("content-type", "application/json")
2137 .with_body(
2138 r#"{
2139 "data": [{
2140 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
2141 "block_number": 50000000,
2142 "block_timestamp": 1700000000000,
2143 "ret": []
2144 }],
2145 "success": true
2146 }"#,
2147 )
2148 .create_async()
2149 .await;
2150
2151 let client = TronClient::with_api_url(&server.url());
2152 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
2153 assert_eq!(tx.status, None);
2154 }
2155
2156 #[tokio::test]
2157 async fn test_get_transaction_no_ret_field() {
2158 let mut server = mockito::Server::new_async().await;
2159 let _mock = server
2160 .mock(
2161 "GET",
2162 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
2163 )
2164 .with_status(200)
2165 .with_header("content-type", "application/json")
2166 .with_body(
2167 r#"{
2168 "data": [{
2169 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
2170 "block_number": 50000000,
2171 "block_timestamp": 1700000000000
2172 }],
2173 "success": true
2174 }"#,
2175 )
2176 .create_async()
2177 .await;
2178
2179 let client = TronClient::with_api_url(&server.url());
2180 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
2181 assert_eq!(tx.status, None);
2182 }
2183
2184 #[tokio::test]
2185 async fn test_get_transaction_to_address_none() {
2186 let mut server = mockito::Server::new_async().await;
2187 let _mock = server
2188 .mock(
2189 "GET",
2190 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
2191 )
2192 .with_status(200)
2193 .with_header("content-type", "application/json")
2194 .with_body(
2195 r#"{
2196 "data": [{
2197 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
2198 "block_number": 50000000,
2199 "block_timestamp": 1700000000000,
2200 "raw_data": {
2201 "contract": [{
2202 "parameter": {
2203 "value": {
2204 "amount": 1000000,
2205 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"
2206 }
2207 }
2208 }]
2209 },
2210 "ret": [{"contractRet": "SUCCESS"}]
2211 }],
2212 "success": true
2213 }"#,
2214 )
2215 .create_async()
2216 .await;
2217
2218 let client = TronClient::with_api_url(&server.url());
2219 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
2220 assert_eq!(tx.to, None);
2221 assert_eq!(tx.value, "1000000");
2222 }
2223
2224 #[tokio::test]
2225 async fn test_get_transaction_amount_none() {
2226 let mut server = mockito::Server::new_async().await;
2227 let _mock = server
2228 .mock(
2229 "GET",
2230 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
2231 )
2232 .with_status(200)
2233 .with_header("content-type", "application/json")
2234 .with_body(
2235 r#"{
2236 "data": [{
2237 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
2238 "block_number": 50000000,
2239 "block_timestamp": 1700000000000,
2240 "raw_data": {
2241 "contract": [{
2242 "parameter": {
2243 "value": {
2244 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
2245 "to_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"
2246 }
2247 }
2248 }]
2249 },
2250 "ret": [{"contractRet": "SUCCESS"}]
2251 }],
2252 "success": true
2253 }"#,
2254 )
2255 .create_async()
2256 .await;
2257
2258 let client = TronClient::with_api_url(&server.url());
2259 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
2260 assert_eq!(tx.value, "0");
2261 }
2262
2263 #[tokio::test]
2264 async fn test_get_transaction_no_raw_data() {
2265 let mut server = mockito::Server::new_async().await;
2266 let _mock = server
2267 .mock(
2268 "GET",
2269 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
2270 )
2271 .with_status(200)
2272 .with_header("content-type", "application/json")
2273 .with_body(
2274 r#"{
2275 "data": [{
2276 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
2277 "block_number": 50000000,
2278 "block_timestamp": 1700000000000,
2279 "ret": [{"contractRet": "SUCCESS"}]
2280 }],
2281 "success": true
2282 }"#,
2283 )
2284 .create_async()
2285 .await;
2286
2287 let client = TronClient::with_api_url(&server.url());
2288 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
2289 assert_eq!(tx.from, "");
2290 assert_eq!(tx.to, None);
2291 assert_eq!(tx.value, "0");
2292 }
2293
2294 #[tokio::test]
2295 async fn test_get_block_number_block_header_none() {
2296 let mut server = mockito::Server::new_async().await;
2297 let _mock = server
2298 .mock("POST", "/wallet/getnowblock")
2299 .with_status(200)
2300 .with_header("content-type", "application/json")
2301 .with_body(r#"{"other": "data"}"#)
2302 .create_async()
2303 .await;
2304
2305 let client = TronClient::with_api_url(&server.url());
2306 let result = client.get_block_number().await;
2307 assert!(result.is_err());
2308 assert!(result.unwrap_err().to_string().contains("Invalid block"));
2309 }
2310
2311 #[tokio::test]
2312 async fn test_get_block_number_raw_data_none() {
2313 let mut server = mockito::Server::new_async().await;
2314 let _mock = server
2315 .mock("POST", "/wallet/getnowblock")
2316 .with_status(200)
2317 .with_header("content-type", "application/json")
2318 .with_body(r#"{"block_header":{}}"#)
2319 .create_async()
2320 .await;
2321
2322 let client = TronClient::with_api_url(&server.url());
2323 let result = client.get_block_number().await;
2324 assert!(result.is_err());
2325 }
2326
2327 #[tokio::test]
2328 async fn test_get_block_number_number_none() {
2329 let mut server = mockito::Server::new_async().await;
2330 let _mock = server
2331 .mock("POST", "/wallet/getnowblock")
2332 .with_status(200)
2333 .with_header("content-type", "application/json")
2334 .with_body(r#"{"block_header":{"raw_data":{}}}"#)
2335 .create_async()
2336 .await;
2337
2338 let client = TronClient::with_api_url(&server.url());
2339 let result = client.get_block_number().await;
2340 assert!(result.is_err());
2341 }
2342
2343 #[tokio::test]
2344 async fn test_get_trc20_balances_api_error_unknown() {
2345 let mut server = mockito::Server::new_async().await;
2346 let _mock = server
2347 .mock(
2348 "GET",
2349 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
2350 )
2351 .with_status(200)
2352 .with_header("content-type", "application/json")
2353 .with_body(r#"{"data": [], "success": false}"#)
2354 .create_async()
2355 .await;
2356
2357 let client = TronClient::with_api_url(&server.url());
2358 let result = client.get_trc20_balances(VALID_ADDRESS).await;
2359 assert!(result.is_err());
2360 assert!(result.unwrap_err().to_string().contains("Unknown error"));
2361 }
2362
2363 #[tokio::test]
2364 async fn test_get_transactions_api_error_unknown() {
2365 let mut server = mockito::Server::new_async().await;
2366 let _mock = server
2367 .mock(
2368 "GET",
2369 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
2370 )
2371 .with_status(200)
2372 .with_header("content-type", "application/json")
2373 .with_body(r#"{"data": [], "success": false}"#)
2374 .create_async()
2375 .await;
2376
2377 let client = TronClient::with_api_url(&server.url());
2378 let result = client.get_transactions(VALID_ADDRESS, 10).await;
2379 assert!(result.is_err());
2380 assert!(result.unwrap_err().to_string().contains("Unknown error"));
2381 }
2382
2383 #[tokio::test]
2384 async fn test_get_transaction_api_error_unknown() {
2385 let mut server = mockito::Server::new_async().await;
2386 let _mock = server
2387 .mock(
2388 "GET",
2389 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
2390 )
2391 .with_status(200)
2392 .with_header("content-type", "application/json")
2393 .with_body(r#"{"data": [], "success": false}"#)
2394 .create_async()
2395 .await;
2396
2397 let client = TronClient::with_api_url(&server.url());
2398 let result = client.get_transaction(VALID_TX_HASH).await;
2399 assert!(result.is_err());
2400 }
2401
2402 #[test]
2407 fn test_trc20_token_balance_struct() {
2408 let balance = Trc20TokenBalance {
2409 contract_address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t".to_string(),
2410 raw_balance: "5000000".to_string(),
2411 };
2412 assert_eq!(
2413 balance.contract_address,
2414 "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
2415 );
2416 assert_eq!(balance.raw_balance, "5000000");
2417 let debug_str = format!("{:?}", balance);
2418 assert!(debug_str.contains("Trc20TokenBalance"));
2419 }
2420
2421 #[test]
2422 fn test_account_response_with_error_field() {
2423 let json = r#"{
2424 "data": [],
2425 "success": false,
2426 "error": "Custom error message"
2427 }"#;
2428 let response: AccountResponse = serde_json::from_str(json).unwrap();
2429 assert!(!response.success);
2430 assert_eq!(response.error, Some("Custom error message".to_string()));
2431 }
2432
2433 #[test]
2434 fn test_account_response_trc20_balances() {
2435 let json = r#"{
2436 "data": [{
2437 "balance": 1000000,
2438 "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
2439 "trc20": [
2440 {"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t": "10000000"}
2441 ]
2442 }],
2443 "success": true
2444 }"#;
2445 let response: AccountResponse = serde_json::from_str(json).unwrap();
2446 assert_eq!(response.data.len(), 1);
2447 assert_eq!(response.data[0].trc20.len(), 1);
2448 let trc20 = &response.data[0].trc20[0];
2449 assert_eq!(
2450 trc20.balances.get("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
2451 Some(&"10000000".to_string())
2452 );
2453 }
2454
2455 #[test]
2456 fn test_transaction_list_response_with_error() {
2457 let json = r#"{
2458 "data": [],
2459 "success": false,
2460 "error": "Transaction not found"
2461 }"#;
2462 let response: TransactionListResponse = serde_json::from_str(json).unwrap();
2463 assert!(!response.success);
2464 assert_eq!(response.error, Some("Transaction not found".to_string()));
2465 }
2466
2467 #[test]
2468 fn test_full_transaction_deserialization() {
2469 let json = r#"{
2470 "data": [{
2471 "txID": "abc123def456",
2472 "block_number": 12345,
2473 "block_timestamp": 1600000000000,
2474 "raw_data": {
2475 "contract": [{
2476 "parameter": {
2477 "value": {
2478 "amount": 999999,
2479 "owner_address": "TFrom123",
2480 "to_address": "TTo456"
2481 }
2482 },
2483 "type": "TransferContract"
2484 }]
2485 },
2486 "ret": [{"contractRet": "SUCCESS"}]
2487 }],
2488 "success": true
2489 }"#;
2490 let response: TransactionListResponse = serde_json::from_str(json).unwrap();
2491 let tx = &response.data[0];
2492 assert_eq!(tx.tx_id, "abc123def456");
2493 assert_eq!(tx.block_number, Some(12345));
2494 assert_eq!(tx.block_timestamp, Some(1600000000000));
2495 let contract_value = tx
2496 .raw_data
2497 .as_ref()
2498 .and_then(|r| r.contract.as_ref())
2499 .and_then(|c| c.first())
2500 .and_then(|c| c.parameter.as_ref())
2501 .and_then(|p| p.value.as_ref())
2502 .unwrap();
2503 assert_eq!(contract_value.amount, Some(999999));
2504 assert_eq!(contract_value.owner_address.as_deref(), Some("TFrom123"));
2505 assert_eq!(contract_value.to_address.as_deref(), Some("TTo456"));
2506 assert_eq!(
2507 tx.ret
2508 .as_ref()
2509 .and_then(|r| r.first())
2510 .and_then(|r| r.contract_ret.as_deref()),
2511 Some("SUCCESS")
2512 );
2513 }
2514
2515 #[test]
2516 fn test_tron_client_default_trait() {
2517 let client = TronClient::default();
2518 assert_eq!(client.chain_name(), "tron");
2519 assert_eq!(client.native_token_symbol(), "TRX");
2520 assert_eq!(client.api_url, DEFAULT_TRON_API);
2521 }
2522
2523 #[test]
2524 fn test_dex_search_pair_wtrx() {
2525 let json = r#"{"pairs":[{"baseTokenSymbol":"WTRX","priceUsd":"0.08"}]}"#;
2526 let result: std::result::Result<DexSearchResponse, _> = serde_json::from_str(json);
2527 assert!(result.is_ok());
2528 let resp = result.unwrap();
2529 let pairs = resp.pairs.unwrap();
2530 assert_eq!(pairs[0].base_token_symbol, Some("WTRX".to_string()));
2531 assert_eq!(pairs[0].price_usd, Some("0.08".to_string()));
2532 }
2533
2534 #[tokio::test]
2535 async fn test_enrich_balance_usd_no_panic() {
2536 let mut balance = Balance {
2537 raw: "1000000".to_string(),
2538 formatted: "1.000000 TRX".to_string(),
2539 decimals: TRX_DECIMALS,
2540 symbol: "TRX".to_string(),
2541 usd_value: None,
2542 };
2543 let client = TronClient::default();
2544 client.enrich_balance_usd(&mut balance).await;
2545 }
2547
2548 #[test]
2549 fn test_trc20_token_balance_debug_format() {
2550 let b = Trc20TokenBalance {
2551 contract_address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t".to_string(),
2552 raw_balance: "1000000".to_string(),
2553 };
2554 let s = format!("{:?}", b);
2555 assert!(s.contains("Trc20TokenBalance"));
2556 assert!(s.contains("1000000"));
2557 }
2558
2559 #[tokio::test]
2560 async fn test_get_transactions_with_minimal_contract_data() {
2561 let mut server = mockito::Server::new_async().await;
2562 let _mock = server
2563 .mock(
2564 "GET",
2565 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
2566 )
2567 .with_status(200)
2568 .with_header("content-type", "application/json")
2569 .with_body(
2570 r#"{
2571 "data": [{
2572 "txID": "c4d23e73be8f8c9c94c10b79c0c0a0c24b2c9a9c0a0c0a0c0a0c0a0c0a0c0a0c0a0c",
2573 "block_number": 50000000,
2574 "block_timestamp": 1700000000000,
2575 "raw_data": {"contract": [{}]},
2576 "ret": []
2577 }],
2578 "success": true
2579 }"#,
2580 )
2581 .create_async()
2582 .await;
2583
2584 let client = TronClient::with_api_url(&server.url());
2585 let txs = client.get_transactions(VALID_ADDRESS, 5).await.unwrap();
2586 assert_eq!(txs.len(), 1);
2587 assert_eq!(txs[0].value, "0");
2588 assert_eq!(txs[0].status, None);
2589 }
2590}