1use crate::chains::{Balance, ChainClient, Token, Transaction};
32use crate::config::ChainsConfig;
33use crate::error::{Result, ScopeError};
34use async_trait::async_trait;
35use reqwest::Client;
36use serde::Deserialize;
37use serde_json;
38use sha2::{Digest, Sha256};
39
40const DEFAULT_TRON_API: &str = "https://api.trongrid.io";
42
43const DEXSCREENER_TRX_SEARCH: &str = "https://api.dexscreener.com/latest/dex/search?q=TRX%20USDT";
45
46const TRX_DECIMALS: u8 = 6;
48
49#[derive(Debug, Clone)]
53pub struct TronClient {
54 client: Client,
56
57 api_url: String,
59
60 api_key: Option<String>,
62}
63
64#[derive(Debug, Deserialize)]
66struct AccountResponse {
67 data: Vec<AccountData>,
68 success: bool,
69 error: Option<String>,
70}
71
72#[derive(Debug, Deserialize)]
74#[allow(dead_code)] struct AccountData {
76 balance: Option<u64>,
77 address: String,
78 create_time: Option<u64>,
79 #[serde(default)]
80 trc20: Vec<Trc20Balance>,
81}
82
83#[derive(Debug, Deserialize)]
85#[allow(dead_code)] struct Trc20Balance {
87 #[serde(flatten)]
88 balances: std::collections::HashMap<String, String>,
89}
90
91#[derive(Debug, Deserialize)]
93struct TransactionListResponse {
94 data: Vec<TronTransaction>,
95 success: bool,
96 error: Option<String>,
97}
98
99#[derive(Debug, Deserialize)]
101struct TronTransaction {
102 #[serde(rename = "txID")]
103 tx_id: String,
104 block_number: Option<u64>,
105 block_timestamp: Option<u64>,
106 raw_data: Option<RawData>,
107 ret: Option<Vec<TransactionResult>>,
108}
109
110#[derive(Debug, Deserialize)]
112struct RawData {
113 contract: Option<Vec<Contract>>,
114}
115
116#[derive(Debug, Deserialize)]
118#[allow(dead_code)] struct Contract {
120 parameter: Option<ContractParameter>,
121 #[serde(rename = "type")]
122 contract_type: Option<String>,
123}
124
125#[derive(Debug, Deserialize)]
127struct ContractParameter {
128 value: Option<ContractValue>,
129}
130
131#[derive(Debug, Deserialize)]
133struct ContractValue {
134 amount: Option<u64>,
135 owner_address: Option<String>,
136 to_address: Option<String>,
137}
138
139#[derive(Debug, Deserialize)]
141struct TransactionResult {
142 #[serde(rename = "contractRet")]
143 contract_ret: Option<String>,
144}
145
146impl TronClient {
147 pub fn new(config: &ChainsConfig) -> Result<Self> {
167 let client = Client::builder()
168 .timeout(std::time::Duration::from_secs(30))
169 .build()
170 .map_err(|e| ScopeError::Chain(format!("Failed to create HTTP client: {}", e)))?;
171
172 let api_url = config
173 .tron_api
174 .as_deref()
175 .unwrap_or(DEFAULT_TRON_API)
176 .to_string();
177
178 Ok(Self {
179 client,
180 api_url,
181 api_key: config.api_keys.get("tronscan").cloned(),
182 })
183 }
184
185 pub fn with_api_url(api_url: &str) -> Self {
191 Self {
192 client: Client::new(),
193 api_url: api_url.to_string(),
194 api_key: None,
195 }
196 }
197
198 pub fn chain_name(&self) -> &str {
200 "tron"
201 }
202
203 pub fn native_token_symbol(&self) -> &str {
205 "TRX"
206 }
207
208 pub async fn get_balance(&self, address: &str) -> Result<Balance> {
223 validate_tron_address(address)?;
225
226 let url = format!("{}/v1/accounts/{}", self.api_url, address);
227
228 tracing::debug!(url = %url, address = %address, "Fetching Tron balance");
229
230 let mut request = self.client.get(&url);
231 if let Some(ref key) = self.api_key {
232 request = request.header("TRON-PRO-API-KEY", key);
233 }
234
235 let response: AccountResponse = request.send().await?.json().await?;
236
237 if !response.success {
238 return Err(ScopeError::Chain(format!(
239 "TronGrid API error: {}",
240 response.error.unwrap_or_else(|| "Unknown error".into())
241 )));
242 }
243
244 let sun = response.data.first().and_then(|d| d.balance).unwrap_or(0);
246
247 let trx = sun as f64 / 10_f64.powi(TRX_DECIMALS as i32);
248
249 Ok(Balance {
250 raw: sun.to_string(),
251 formatted: format!("{:.6} TRX", trx),
252 decimals: TRX_DECIMALS,
253 symbol: "TRX".to_string(),
254 usd_value: None, })
256 }
257
258 pub async fn get_trc20_balances(&self, address: &str) -> Result<Vec<Trc20TokenBalance>> {
263 validate_tron_address(address)?;
264
265 let url = format!("{}/v1/accounts/{}", self.api_url, address);
266
267 tracing::debug!(url = %url, "Fetching TRC-20 token balances");
268
269 let mut request = self.client.get(&url);
270 if let Some(ref key) = self.api_key {
271 request = request.header("TRON-PRO-API-KEY", key);
272 }
273
274 let response: AccountResponse = request.send().await?.json().await?;
275
276 if !response.success {
277 return Err(ScopeError::Chain(format!(
278 "TronGrid API error: {}",
279 response.error.unwrap_or_else(|| "Unknown error".into())
280 )));
281 }
282
283 let account = match response.data.first() {
284 Some(data) => data,
285 None => return Ok(vec![]),
286 };
287
288 let mut balances = Vec::new();
289 for trc20 in &account.trc20 {
290 for (contract_address, raw_balance) in &trc20.balances {
291 if raw_balance == "0" {
293 continue;
294 }
295 balances.push(Trc20TokenBalance {
296 contract_address: contract_address.clone(),
297 raw_balance: raw_balance.clone(),
298 });
299 }
300 }
301
302 Ok(balances)
303 }
304
305 pub async fn enrich_balance_usd(&self, balance: &mut Balance) {
310 let url = DEXSCREENER_TRX_SEARCH;
312 if let Ok(response) = self.client.get(url).send().await
313 && let Ok(text) = response.text().await
314 && let Ok(search_result) = serde_json::from_str::<DexSearchResponse>(&text)
315 && let Some(pairs) = search_result.pairs
316 {
317 for pair in &pairs {
318 if (pair.base_token_symbol.as_deref() == Some("TRX")
319 || pair.base_token_symbol.as_deref() == Some("WTRX"))
320 && let Some(price) = pair.price_usd.as_ref().and_then(|p| p.parse::<f64>().ok())
321 {
322 let sun: f64 = balance.raw.parse().unwrap_or(0.0);
323 let trx = sun / 10_f64.powi(TRX_DECIMALS as i32);
324 balance.usd_value = Some(trx * price);
325 return;
326 }
327 }
328 }
329 }
330
331 pub async fn get_transaction(&self, hash: &str) -> Result<Transaction> {
341 validate_tron_tx_hash(hash)?;
343
344 let url = format!("{}/v1/transactions/{}", self.api_url, hash);
345
346 tracing::debug!(url = %url, hash = %hash, "Fetching Tron transaction");
347
348 let mut request = self.client.get(&url);
349 if let Some(ref key) = self.api_key {
350 request = request.header("TRON-PRO-API-KEY", key);
351 }
352
353 let response: TransactionListResponse = request.send().await?.json().await?;
354
355 if !response.success {
356 return Err(ScopeError::Chain(format!(
357 "TronGrid API error: {}",
358 response.error.unwrap_or_else(|| "Unknown error".into())
359 )));
360 }
361
362 let tx = response
363 .data
364 .into_iter()
365 .next()
366 .ok_or_else(|| ScopeError::Chain("Transaction not found".into()))?;
367
368 let (from, to, value) = tx
370 .raw_data
371 .and_then(|rd| rd.contract)
372 .and_then(|contracts| contracts.into_iter().next())
373 .and_then(|c| c.parameter)
374 .and_then(|p| p.value)
375 .map(|v| {
376 (
377 v.owner_address.unwrap_or_default(),
378 v.to_address,
379 v.amount.unwrap_or(0).to_string(),
380 )
381 })
382 .unwrap_or_else(|| (String::new(), None, "0".to_string()));
383
384 let status = tx
385 .ret
386 .and_then(|r| r.into_iter().next())
387 .and_then(|r| r.contract_ret)
388 .map(|s| s == "SUCCESS");
389
390 Ok(Transaction {
391 hash: tx.tx_id,
392 block_number: tx.block_number,
393 timestamp: tx.block_timestamp.map(|t| t / 1000), from,
395 to,
396 value,
397 gas_limit: 0, gas_used: None,
399 gas_price: "0".to_string(),
400 nonce: 0,
401 input: String::new(),
402 status,
403 })
404 }
405
406 pub async fn get_transactions(&self, address: &str, limit: u32) -> Result<Vec<Transaction>> {
417 validate_tron_address(address)?;
418
419 let url = format!(
420 "{}/v1/accounts/{}/transactions?limit={}",
421 self.api_url, address, limit
422 );
423
424 tracing::debug!(url = %url, address = %address, "Fetching Tron transactions");
425
426 let mut request = self.client.get(&url);
427 if let Some(ref key) = self.api_key {
428 request = request.header("TRON-PRO-API-KEY", key);
429 }
430
431 let response: TransactionListResponse = request.send().await?.json().await?;
432
433 if !response.success {
434 return Err(ScopeError::Chain(format!(
435 "TronGrid API error: {}",
436 response.error.unwrap_or_else(|| "Unknown error".into())
437 )));
438 }
439
440 let transactions = response
441 .data
442 .into_iter()
443 .map(|tx| {
444 let (from, to, value) = tx
445 .raw_data
446 .and_then(|rd| rd.contract)
447 .and_then(|contracts| contracts.into_iter().next())
448 .and_then(|c| c.parameter)
449 .and_then(|p| p.value)
450 .map(|v| {
451 (
452 v.owner_address.unwrap_or_default(),
453 v.to_address,
454 v.amount.unwrap_or(0).to_string(),
455 )
456 })
457 .unwrap_or_else(|| (String::new(), None, "0".to_string()));
458
459 let status = tx
460 .ret
461 .and_then(|r| r.into_iter().next())
462 .and_then(|r| r.contract_ret)
463 .map(|s| s == "SUCCESS");
464
465 Transaction {
466 hash: tx.tx_id,
467 block_number: tx.block_number,
468 timestamp: tx.block_timestamp.map(|t| t / 1000),
469 from,
470 to,
471 value,
472 gas_limit: 0,
473 gas_used: None,
474 gas_price: "0".to_string(),
475 nonce: 0,
476 input: String::new(),
477 status,
478 }
479 })
480 .collect();
481
482 Ok(transactions)
483 }
484
485 pub async fn get_block_number(&self) -> Result<u64> {
487 let url = format!("{}/wallet/getnowblock", self.api_url);
488
489 #[derive(Deserialize)]
490 struct BlockResponse {
491 block_header: Option<BlockHeader>,
492 }
493
494 #[derive(Deserialize)]
495 struct BlockHeader {
496 raw_data: Option<BlockRawData>,
497 }
498
499 #[derive(Deserialize)]
500 struct BlockRawData {
501 number: Option<u64>,
502 }
503
504 let response: BlockResponse = self.client.post(&url).send().await?.json().await?;
505
506 response
507 .block_header
508 .and_then(|h| h.raw_data)
509 .and_then(|d| d.number)
510 .ok_or_else(|| ScopeError::Chain("Invalid block response".into()))
511 }
512}
513
514impl Default for TronClient {
515 fn default() -> Self {
516 Self {
517 client: Client::new(),
518 api_url: DEFAULT_TRON_API.to_string(),
519 api_key: None,
520 }
521 }
522}
523
524#[derive(Debug, Clone)]
537pub struct Trc20TokenBalance {
538 pub contract_address: String,
540 pub raw_balance: String,
542}
543
544#[derive(Debug, Deserialize)]
546struct DexSearchResponse {
547 #[serde(default)]
548 pairs: Option<Vec<DexSearchPair>>,
549}
550
551#[derive(Debug, Deserialize)]
553#[serde(rename_all = "camelCase")]
554struct DexSearchPair {
555 #[serde(default)]
556 base_token_symbol: Option<String>,
557 #[serde(default)]
558 price_usd: Option<String>,
559}
560
561pub fn validate_tron_address(address: &str) -> Result<()> {
565 if address.is_empty() {
566 return Err(ScopeError::InvalidAddress("Address cannot be empty".into()));
567 }
568
569 if !address.starts_with('T') {
571 return Err(ScopeError::InvalidAddress(format!(
572 "Tron address must start with 'T': {}",
573 address
574 )));
575 }
576
577 if address.len() != 34 {
579 return Err(ScopeError::InvalidAddress(format!(
580 "Tron address must be 34 characters, got {}: {}",
581 address.len(),
582 address
583 )));
584 }
585
586 match bs58::decode(address).into_vec() {
588 Ok(bytes) => {
589 if bytes.len() != 25 {
591 return Err(ScopeError::InvalidAddress(format!(
592 "Tron address must decode to 25 bytes, got {}: {}",
593 bytes.len(),
594 address
595 )));
596 }
597
598 if bytes[0] != 0x41 {
600 return Err(ScopeError::InvalidAddress(format!(
601 "Invalid Tron address prefix: {}",
602 address
603 )));
604 }
605
606 let payload = &bytes[0..21];
608 let hash1 = Sha256::digest(payload);
609 let hash2 = Sha256::digest(hash1);
610 let expected_checksum = &hash2[0..4];
611 let actual_checksum = &bytes[21..25];
612
613 if expected_checksum != actual_checksum {
614 return Err(ScopeError::InvalidAddress(format!(
615 "Invalid Tron address checksum: {}",
616 address
617 )));
618 }
619 }
620 Err(e) => {
621 return Err(ScopeError::InvalidAddress(format!(
622 "Invalid base58 encoding: {}: {}",
623 e, address
624 )));
625 }
626 }
627
628 Ok(())
629}
630
631pub fn validate_tron_tx_hash(hash: &str) -> Result<()> {
643 if hash.is_empty() {
644 return Err(ScopeError::InvalidHash("Hash cannot be empty".into()));
645 }
646
647 if hash.len() != 64 {
649 return Err(ScopeError::InvalidHash(format!(
650 "Tron transaction hash must be 64 characters, got {}: {}",
651 hash.len(),
652 hash
653 )));
654 }
655
656 if !hash.chars().all(|c| c.is_ascii_hexdigit()) {
658 return Err(ScopeError::InvalidHash(format!(
659 "Tron hash contains invalid hex characters: {}",
660 hash
661 )));
662 }
663
664 Ok(())
665}
666
667#[async_trait]
672impl ChainClient for TronClient {
673 fn chain_name(&self) -> &str {
674 "tron"
675 }
676
677 fn native_token_symbol(&self) -> &str {
678 "TRX"
679 }
680
681 async fn get_balance(&self, address: &str) -> Result<Balance> {
682 self.get_balance(address).await
683 }
684
685 async fn enrich_balance_usd(&self, balance: &mut Balance) {
686 self.enrich_balance_usd(balance).await
687 }
688
689 async fn get_transaction(&self, hash: &str) -> Result<Transaction> {
690 self.get_transaction(hash).await
691 }
692
693 async fn get_transactions(&self, address: &str, limit: u32) -> Result<Vec<Transaction>> {
694 self.get_transactions(address, limit).await
695 }
696
697 async fn get_block_number(&self) -> Result<u64> {
698 self.get_block_number().await
699 }
700
701 async fn get_token_balances(&self, address: &str) -> Result<Vec<crate::chains::TokenBalance>> {
702 let trc20_balances = self.get_trc20_balances(address).await?;
703 Ok(trc20_balances
704 .into_iter()
705 .map(|tb| crate::chains::TokenBalance {
706 token: Token {
707 contract_address: tb.contract_address.clone(),
708 symbol: "TRC20".to_string(),
709 name: "TRC-20 Token".to_string(),
710 decimals: 0, },
712 balance: tb.raw_balance.clone(),
713 formatted_balance: tb.raw_balance,
714 usd_value: None,
715 })
716 .collect())
717 }
718}
719
720#[cfg(test)]
725mod tests {
726 use super::*;
727
728 const VALID_ADDRESS: &str = "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf";
730
731 const VALID_TX_HASH: &str = "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b";
733
734 #[test]
735 fn test_validate_tron_address_valid() {
736 assert!(validate_tron_address(VALID_ADDRESS).is_ok());
737 }
738
739 #[test]
740 fn test_validate_tron_address_empty() {
741 let result = validate_tron_address("");
742 assert!(result.is_err());
743 assert!(result.unwrap_err().to_string().contains("empty"));
744 }
745
746 #[test]
747 fn test_validate_tron_address_wrong_prefix() {
748 let result = validate_tron_address("ADqSquXBgUCLYvYC4XZgrprLK589dkhSCf");
749 assert!(result.is_err());
750 assert!(result.unwrap_err().to_string().contains("start with 'T'"));
751 }
752
753 #[test]
754 fn test_validate_tron_address_too_short() {
755 let result = validate_tron_address("TDqSquXBgUCLYvYC4XZ");
756 assert!(result.is_err());
757 assert!(result.unwrap_err().to_string().contains("34 characters"));
758 }
759
760 #[test]
761 fn test_validate_tron_address_too_long() {
762 let result = validate_tron_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCfAAAA");
763 assert!(result.is_err());
764 assert!(result.unwrap_err().to_string().contains("34 characters"));
765 }
766
767 #[test]
768 fn test_validate_tron_address_invalid_base58() {
769 let result = validate_tron_address("T0qSquXBgUCLYvYC4XZgrprLK589dkhSCf");
771 assert!(result.is_err());
772 assert!(result.unwrap_err().to_string().contains("base58"));
773 }
774
775 #[test]
776 fn test_validate_tron_tx_hash_valid() {
777 assert!(validate_tron_tx_hash(VALID_TX_HASH).is_ok());
778 }
779
780 #[test]
781 fn test_validate_tron_tx_hash_empty() {
782 let result = validate_tron_tx_hash("");
783 assert!(result.is_err());
784 assert!(result.unwrap_err().to_string().contains("empty"));
785 }
786
787 #[test]
788 fn test_validate_tron_tx_hash_too_short() {
789 let result = validate_tron_tx_hash("b3c12d62ad7e7b8b83b09a68");
790 assert!(result.is_err());
791 assert!(result.unwrap_err().to_string().contains("64 characters"));
792 }
793
794 #[test]
795 fn test_validate_tron_tx_hash_invalid_hex() {
796 let hash = "g3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b";
797 let result = validate_tron_tx_hash(hash);
798 assert!(result.is_err());
799 assert!(result.unwrap_err().to_string().contains("invalid hex"));
800 }
801
802 #[test]
803 fn test_tron_client_default() {
804 let client = TronClient::default();
805 assert_eq!(client.chain_name(), "tron");
806 assert_eq!(client.native_token_symbol(), "TRX");
807 assert!(client.api_url.contains("trongrid"));
808 }
809
810 #[test]
811 fn test_tron_client_with_api_url() {
812 let client = TronClient::with_api_url("https://custom.tron.api");
813 assert_eq!(client.api_url, "https://custom.tron.api");
814 }
815
816 #[test]
817 fn test_tron_client_new() {
818 let config = ChainsConfig::default();
819 let client = TronClient::new(&config);
820 assert!(client.is_ok());
821 }
822
823 #[test]
824 fn test_tron_client_new_with_custom_api() {
825 let config = ChainsConfig {
826 tron_api: Some("https://my-tron-api.com".to_string()),
827 ..Default::default()
828 };
829 let client = TronClient::new(&config).unwrap();
830 assert_eq!(client.api_url, "https://my-tron-api.com");
831 }
832
833 #[test]
834 fn test_tron_client_new_with_api_key() {
835 use std::collections::HashMap;
836
837 let mut api_keys = HashMap::new();
838 api_keys.insert("tronscan".to_string(), "test-key".to_string());
839
840 let config = ChainsConfig {
841 api_keys,
842 ..Default::default()
843 };
844
845 let client = TronClient::new(&config).unwrap();
846 assert_eq!(client.api_key, Some("test-key".to_string()));
847 }
848
849 #[test]
850 fn test_account_response_deserialization() {
851 let json = r#"{
852 "data": [{
853 "balance": 1000000,
854 "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
855 "create_time": 1600000000000,
856 "trc20": []
857 }],
858 "success": true
859 }"#;
860
861 let response: AccountResponse = serde_json::from_str(json).unwrap();
862 assert!(response.success);
863 assert_eq!(response.data.len(), 1);
864 assert_eq!(response.data[0].balance, Some(1_000_000));
865 }
866
867 #[test]
868 fn test_transaction_response_deserialization() {
869 let json = r#"{
870 "data": [{
871 "txID": "abc123",
872 "block_number": 12345,
873 "block_timestamp": 1600000000000,
874 "ret": [{"contractRet": "SUCCESS"}]
875 }],
876 "success": true
877 }"#;
878
879 let response: TransactionListResponse = serde_json::from_str(json).unwrap();
880 assert!(response.success);
881 assert_eq!(response.data.len(), 1);
882 assert_eq!(response.data[0].tx_id, "abc123");
883 }
884
885 #[tokio::test]
890 async fn test_get_balance() {
891 let mut server = mockito::Server::new_async().await;
892 let _mock = server
893 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
894 .with_status(200)
895 .with_header("content-type", "application/json")
896 .with_body(r#"{
897 "data": [{"balance": 5000000, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": []}],
898 "success": true
899 }"#)
900 .create_async()
901 .await;
902
903 let client = TronClient::with_api_url(&server.url());
904 let balance = client.get_balance(VALID_ADDRESS).await.unwrap();
905 assert_eq!(balance.raw, "5000000");
906 assert_eq!(balance.symbol, "TRX");
907 assert!(balance.formatted.contains("5.000000"));
908 }
909
910 #[tokio::test]
911 async fn test_get_balance_new_account() {
912 let mut server = mockito::Server::new_async().await;
913 let _mock = server
914 .mock(
915 "GET",
916 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
917 )
918 .with_status(200)
919 .with_header("content-type", "application/json")
920 .with_body(r#"{"data": [], "success": true}"#)
921 .create_async()
922 .await;
923
924 let client = TronClient::with_api_url(&server.url());
925 let balance = client.get_balance(VALID_ADDRESS).await.unwrap();
926 assert_eq!(balance.raw, "0");
927 assert!(balance.formatted.contains("0.000000"));
928 }
929
930 #[tokio::test]
931 async fn test_get_balance_api_error() {
932 let mut server = mockito::Server::new_async().await;
933 let _mock = server
934 .mock(
935 "GET",
936 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
937 )
938 .with_status(200)
939 .with_header("content-type", "application/json")
940 .with_body(r#"{"data": [], "success": false, "error": "Rate limit exceeded"}"#)
941 .create_async()
942 .await;
943
944 let client = TronClient::with_api_url(&server.url());
945 let result = client.get_balance(VALID_ADDRESS).await;
946 assert!(result.is_err());
947 assert!(result.unwrap_err().to_string().contains("Rate limit"));
948 }
949
950 #[tokio::test]
951 async fn test_get_balance_invalid_address() {
952 let client = TronClient::default();
953 let result = client.get_balance("invalid").await;
954 assert!(result.is_err());
955 }
956
957 #[tokio::test]
958 async fn test_get_transaction() {
959 let mut server = mockito::Server::new_async().await;
960 let _mock = server
961 .mock(
962 "GET",
963 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
964 )
965 .with_status(200)
966 .with_header("content-type", "application/json")
967 .with_body(
968 r#"{
969 "data": [{
970 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
971 "block_number": 50000000,
972 "block_timestamp": 1700000000000,
973 "raw_data": {
974 "contract": [{
975 "parameter": {
976 "value": {
977 "amount": 1000000,
978 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
979 "to_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"
980 }
981 },
982 "type": "TransferContract"
983 }]
984 },
985 "ret": [{"contractRet": "SUCCESS"}]
986 }],
987 "success": true
988 }"#,
989 )
990 .create_async()
991 .await;
992
993 let client = TronClient::with_api_url(&server.url());
994 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
995 assert_eq!(tx.hash, VALID_TX_HASH);
996 assert_eq!(tx.from, "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf");
997 assert_eq!(
998 tx.to,
999 Some("TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9".to_string())
1000 );
1001 assert_eq!(tx.value, "1000000");
1002 assert_eq!(tx.block_number, Some(50000000));
1003 assert_eq!(tx.timestamp, Some(1700000000)); assert!(tx.status.unwrap());
1005 }
1006
1007 #[tokio::test]
1008 async fn test_get_transaction_failed() {
1009 let mut server = mockito::Server::new_async().await;
1010 let _mock = server
1011 .mock(
1012 "GET",
1013 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1014 )
1015 .with_status(200)
1016 .with_header("content-type", "application/json")
1017 .with_body(
1018 r#"{
1019 "data": [{
1020 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1021 "block_number": 50000000,
1022 "block_timestamp": 1700000000000,
1023 "ret": [{"contractRet": "REVERT"}]
1024 }],
1025 "success": true
1026 }"#,
1027 )
1028 .create_async()
1029 .await;
1030
1031 let client = TronClient::with_api_url(&server.url());
1032 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
1033 assert!(!tx.status.unwrap()); }
1035
1036 #[tokio::test]
1037 async fn test_get_transaction_not_found() {
1038 let mut server = mockito::Server::new_async().await;
1039 let _mock = server
1040 .mock(
1041 "GET",
1042 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1043 )
1044 .with_status(200)
1045 .with_header("content-type", "application/json")
1046 .with_body(r#"{"data": [], "success": true}"#)
1047 .create_async()
1048 .await;
1049
1050 let client = TronClient::with_api_url(&server.url());
1051 let result = client.get_transaction(VALID_TX_HASH).await;
1052 assert!(result.is_err());
1053 }
1054
1055 #[tokio::test]
1056 async fn test_get_transactions() {
1057 let mut server = mockito::Server::new_async().await;
1058 let _mock = server
1059 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()))
1060 .with_status(200)
1061 .with_header("content-type", "application/json")
1062 .with_body(r#"{
1063 "data": [
1064 {
1065 "txID": "aaa111",
1066 "block_number": 50000000,
1067 "block_timestamp": 1700000000000,
1068 "raw_data": {"contract": [{"parameter": {"value": {"amount": 500000, "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "to_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"}}, "type": "TransferContract"}]},
1069 "ret": [{"contractRet": "SUCCESS"}]
1070 },
1071 {
1072 "txID": "bbb222",
1073 "block_number": 50000001,
1074 "block_timestamp": 1700000060000,
1075 "ret": [{"contractRet": "SUCCESS"}]
1076 }
1077 ],
1078 "success": true
1079 }"#)
1080 .create_async()
1081 .await;
1082
1083 let client = TronClient::with_api_url(&server.url());
1084 let txs = client.get_transactions(VALID_ADDRESS, 10).await.unwrap();
1085 assert_eq!(txs.len(), 2);
1086 assert_eq!(txs[0].hash, "aaa111");
1087 assert_eq!(txs[0].value, "500000");
1088 assert!(txs[0].status.unwrap());
1089 assert_eq!(txs[1].value, "0");
1091 }
1092
1093 #[tokio::test]
1094 async fn test_get_transactions_error() {
1095 let mut server = mockito::Server::new_async().await;
1096 let _mock = server
1097 .mock(
1098 "GET",
1099 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1100 )
1101 .with_status(200)
1102 .with_header("content-type", "application/json")
1103 .with_body(r#"{"data": [], "success": false, "error": "Invalid address"}"#)
1104 .create_async()
1105 .await;
1106
1107 let client = TronClient::with_api_url(&server.url());
1108 let result = client.get_transactions(VALID_ADDRESS, 10).await;
1109 assert!(result.is_err());
1110 }
1111
1112 #[tokio::test]
1113 async fn test_get_trc20_balances() {
1114 let mut server = mockito::Server::new_async().await;
1115 let _mock = server
1116 .mock(
1117 "GET",
1118 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1119 )
1120 .with_status(200)
1121 .with_header("content-type", "application/json")
1122 .with_body(
1123 r#"{
1124 "data": [{
1125 "balance": 1000000,
1126 "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1127 "trc20": [
1128 {"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t": "5000000"},
1129 {"TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8": "0"}
1130 ]
1131 }],
1132 "success": true
1133 }"#,
1134 )
1135 .create_async()
1136 .await;
1137
1138 let client = TronClient::with_api_url(&server.url());
1139 let balances = client.get_trc20_balances(VALID_ADDRESS).await.unwrap();
1140 assert_eq!(balances.len(), 1);
1142 assert_eq!(
1143 balances[0].contract_address,
1144 "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
1145 );
1146 assert_eq!(balances[0].raw_balance, "5000000");
1147 }
1148
1149 #[tokio::test]
1150 async fn test_get_trc20_balances_empty_account() {
1151 let mut server = mockito::Server::new_async().await;
1152 let _mock = server
1153 .mock(
1154 "GET",
1155 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1156 )
1157 .with_status(200)
1158 .with_header("content-type", "application/json")
1159 .with_body(r#"{"data": [], "success": true}"#)
1160 .create_async()
1161 .await;
1162
1163 let client = TronClient::with_api_url(&server.url());
1164 let balances = client.get_trc20_balances(VALID_ADDRESS).await.unwrap();
1165 assert!(balances.is_empty());
1166 }
1167
1168 #[tokio::test]
1169 async fn test_get_block_number() {
1170 let mut server = mockito::Server::new_async().await;
1171 let _mock = server
1172 .mock("POST", "/wallet/getnowblock")
1173 .with_status(200)
1174 .with_header("content-type", "application/json")
1175 .with_body(r#"{"block_header":{"raw_data":{"number":60000000}}}"#)
1176 .create_async()
1177 .await;
1178
1179 let client = TronClient::with_api_url(&server.url());
1180 let block = client.get_block_number().await.unwrap();
1181 assert_eq!(block, 60000000);
1182 }
1183
1184 #[tokio::test]
1185 async fn test_get_block_number_invalid_response() {
1186 let mut server = mockito::Server::new_async().await;
1187 let _mock = server
1188 .mock("POST", "/wallet/getnowblock")
1189 .with_status(200)
1190 .with_header("content-type", "application/json")
1191 .with_body(r#"{}"#)
1192 .create_async()
1193 .await;
1194
1195 let client = TronClient::with_api_url(&server.url());
1196 let result = client.get_block_number().await;
1197 assert!(result.is_err());
1198 }
1199
1200 #[test]
1201 fn test_validate_tron_address_wrong_decoded_length() {
1202 let result = validate_tron_address("TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT1");
1204 assert!(result.is_err());
1205 }
1206
1207 #[test]
1208 fn test_validate_tron_tx_hash_wrong_length() {
1209 let result = validate_tron_tx_hash("abc123");
1210 assert!(result.is_err());
1211 assert!(result.unwrap_err().to_string().contains("64 characters"));
1212 }
1213
1214 #[tokio::test]
1215 async fn test_get_transaction_success() {
1216 let mut server = mockito::Server::new_async().await;
1217 let valid_hash = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
1218 let _mock = server
1219 .mock(
1220 "GET",
1221 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1222 )
1223 .with_status(200)
1224 .with_header("content-type", "application/json")
1225 .with_body(
1226 r#"{"data":[{
1227 "txID":"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
1228 "blockNumber":60000000,
1229 "block_timestamp":1700000000000,
1230 "raw_data":{"contract":[{"parameter":{"value":{
1231 "owner_address":"TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1232 "to_address":"TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn",
1233 "amount":1000000
1234 }}}]},
1235 "ret":[{"contractRet":"SUCCESS"}]
1236 }],"success":true}"#,
1237 )
1238 .create_async()
1239 .await;
1240
1241 let client = TronClient::with_api_url(&server.url());
1242 let tx = client.get_transaction(valid_hash).await.unwrap();
1243 assert_eq!(tx.hash, valid_hash);
1244 assert_eq!(tx.status, Some(true));
1245 }
1246
1247 #[tokio::test]
1248 async fn test_get_transaction_api_error() {
1249 let mut server = mockito::Server::new_async().await;
1250 let valid_hash = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
1251 let _mock = server
1252 .mock(
1253 "GET",
1254 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1255 )
1256 .with_status(200)
1257 .with_header("content-type", "application/json")
1258 .with_body(r#"{"data":[],"success":false,"error":"Transaction not found"}"#)
1259 .create_async()
1260 .await;
1261
1262 let client = TronClient::with_api_url(&server.url());
1263 let result = client.get_transaction(valid_hash).await;
1264 assert!(result.is_err());
1265 }
1266
1267 #[tokio::test]
1268 async fn test_get_transactions_success() {
1269 let mut server = mockito::Server::new_async().await;
1270 let _mock = server
1271 .mock(
1272 "GET",
1273 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1274 )
1275 .with_status(200)
1276 .with_header("content-type", "application/json")
1277 .with_body(
1278 r#"{"data":[{
1279 "txID":"abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
1280 "blockNumber":60000000,
1281 "block_timestamp":1700000000000,
1282 "raw_data":{"contract":[{"parameter":{"value":{
1283 "owner_address":"TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1284 "amount":500000
1285 }}}]},
1286 "ret":[{"contractRet":"SUCCESS"}]
1287 }],"success":true}"#,
1288 )
1289 .create_async()
1290 .await;
1291
1292 let client = TronClient::with_api_url(&server.url());
1293 let txs = client.get_transactions(VALID_ADDRESS, 10).await.unwrap();
1294 assert_eq!(txs.len(), 1);
1295 }
1296
1297 #[tokio::test]
1298 async fn test_tron_chain_client_trait_accessors() {
1299 let client = TronClient::with_api_url("http://localhost");
1300 let chain_client: &dyn ChainClient = &client;
1301 assert_eq!(chain_client.chain_name(), "tron");
1302 assert_eq!(chain_client.native_token_symbol(), "TRX");
1303 }
1304
1305 #[tokio::test]
1306 async fn test_chain_client_trait_get_balance() {
1307 let mut server = mockito::Server::new_async().await;
1308 let _mock = server
1309 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
1310 .with_status(200)
1311 .with_header("content-type", "application/json")
1312 .with_body(r#"{"data": [{"balance": 1000000, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": []}], "success": true}"#)
1313 .create_async()
1314 .await;
1315
1316 let client = TronClient::with_api_url(&server.url());
1317 let chain_client: &dyn ChainClient = &client;
1318 let balance = chain_client.get_balance(VALID_ADDRESS).await.unwrap();
1319 assert_eq!(balance.symbol, "TRX");
1320 }
1321
1322 #[tokio::test]
1323 async fn test_chain_client_trait_get_block_number() {
1324 let mut server = mockito::Server::new_async().await;
1325 let _mock = server
1326 .mock("POST", "/wallet/getnowblock")
1327 .with_status(200)
1328 .with_header("content-type", "application/json")
1329 .with_body(r#"{"block_header":{"raw_data":{"number":60000000}}}"#)
1330 .create_async()
1331 .await;
1332
1333 let client = TronClient::with_api_url(&server.url());
1334 let chain_client: &dyn ChainClient = &client;
1335 let block = chain_client.get_block_number().await.unwrap();
1336 assert_eq!(block, 60000000);
1337 }
1338
1339 #[tokio::test]
1340 async fn test_chain_client_trait_get_token_balances() {
1341 let mut server = mockito::Server::new_async().await;
1342 let _mock = server
1343 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
1344 .with_status(200)
1345 .with_header("content-type", "application/json")
1346 .with_body(r#"{"data": [{"balance": 0, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": [{"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t": "5000000"}]}], "success": true}"#)
1347 .create_async()
1348 .await;
1349
1350 let client = TronClient::with_api_url(&server.url());
1351 let chain_client: &dyn ChainClient = &client;
1352 let balances = chain_client
1353 .get_token_balances(VALID_ADDRESS)
1354 .await
1355 .unwrap();
1356 assert_eq!(balances.len(), 1);
1357 assert_eq!(balances[0].token.symbol, "TRC20");
1359 assert_eq!(balances[0].token.name, "TRC-20 Token");
1360 }
1361
1362 #[tokio::test]
1363 async fn test_chain_client_trait_get_transaction_tron() {
1364 let mut server = mockito::Server::new_async().await;
1365 let _mock = server
1366 .mock(
1367 "GET",
1368 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1369 )
1370 .with_status(200)
1371 .with_header("content-type", "application/json")
1372 .with_body(
1373 r#"{"data": [{
1374 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1375 "block_number": 50000000,
1376 "block_timestamp": 1700000000000,
1377 "raw_data": {
1378 "contract": [{
1379 "parameter": {
1380 "value": {
1381 "amount": 1000000,
1382 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1383 "to_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCg"
1384 }
1385 },
1386 "type": "TransferContract"
1387 }]
1388 },
1389 "ret": [{"contractRet": "SUCCESS"}]
1390 }], "success": true}"#,
1391 )
1392 .create_async()
1393 .await;
1394
1395 let client = TronClient::with_api_url(&server.url());
1396 let chain_client: &dyn ChainClient = &client;
1397 let tx = chain_client.get_transaction(VALID_TX_HASH).await.unwrap();
1398 assert_eq!(tx.hash, VALID_TX_HASH);
1399 assert!(tx.status.unwrap());
1400 }
1401
1402 #[tokio::test]
1403 async fn test_chain_client_trait_get_transactions_tron() {
1404 let mut server = mockito::Server::new_async().await;
1405 let _mock = server
1406 .mock(
1407 "GET",
1408 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1409 )
1410 .with_status(200)
1411 .with_header("content-type", "application/json")
1412 .with_body(
1413 r#"{"data": [{
1414 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1415 "block_number": 50000000,
1416 "block_timestamp": 1700000000000,
1417 "raw_data": {
1418 "contract": [{
1419 "parameter": {
1420 "value": {
1421 "amount": 2000000,
1422 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1423 "to_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCg"
1424 }
1425 }
1426 }]
1427 },
1428 "ret": [{"contractRet": "REVERT"}]
1429 }], "success": true}"#,
1430 )
1431 .create_async()
1432 .await;
1433
1434 let client = TronClient::with_api_url(&server.url());
1435 let chain_client: &dyn ChainClient = &client;
1436 let txs = chain_client
1437 .get_transactions(VALID_ADDRESS, 10)
1438 .await
1439 .unwrap();
1440 assert_eq!(txs.len(), 1);
1441 assert!(!txs[0].status.unwrap()); }
1443
1444 #[tokio::test]
1445 async fn test_get_balance_with_api_key() {
1446 let mut server = mockito::Server::new_async().await;
1447 let _mock = server
1448 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
1449 .with_status(200)
1450 .with_header("content-type", "application/json")
1451 .with_body(
1452 r#"{"data": [{"balance": 10000000, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": []}], "success": true}"#,
1453 )
1454 .create_async()
1455 .await;
1456
1457 let config = ChainsConfig {
1458 tron_api: Some(server.url()),
1459 api_keys: {
1460 let mut m = std::collections::HashMap::new();
1461 m.insert("tronscan".to_string(), "test-api-key".to_string());
1462 m
1463 },
1464 ..Default::default()
1465 };
1466 let client = TronClient::new(&config).unwrap();
1467 let balance = client.get_balance(VALID_ADDRESS).await.unwrap();
1468 assert_eq!(balance.symbol, "TRX");
1469 assert!(balance.formatted.contains("TRX"));
1470 }
1471
1472 #[tokio::test]
1473 async fn test_get_trc20_balances_error_response() {
1474 let mut server = mockito::Server::new_async().await;
1475 let _mock = server
1476 .mock(
1477 "GET",
1478 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1479 )
1480 .with_status(200)
1481 .with_header("content-type", "application/json")
1482 .with_body(r#"{"data": [], "success": false, "error": "Rate limit exceeded"}"#)
1483 .create_async()
1484 .await;
1485
1486 let client = TronClient::with_api_url(&server.url());
1487 let result = client.get_trc20_balances(VALID_ADDRESS).await;
1488 assert!(result.is_err());
1489 assert!(result.unwrap_err().to_string().contains("Rate limit"));
1490 }
1491
1492 #[tokio::test]
1493 async fn test_get_trc20_balances_no_data() {
1494 let mut server = mockito::Server::new_async().await;
1495 let _mock = server
1496 .mock(
1497 "GET",
1498 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1499 )
1500 .with_status(200)
1501 .with_header("content-type", "application/json")
1502 .with_body(r#"{"data": [], "success": true}"#)
1503 .create_async()
1504 .await;
1505
1506 let client = TronClient::with_api_url(&server.url());
1507 let balances = client.get_trc20_balances(VALID_ADDRESS).await.unwrap();
1508 assert!(balances.is_empty());
1509 }
1510
1511 #[tokio::test]
1512 async fn test_get_trc20_balances_with_api_key() {
1513 let mut server = mockito::Server::new_async().await;
1514 let _mock = server
1515 .mock("GET", mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()))
1516 .with_status(200)
1517 .with_header("content-type", "application/json")
1518 .with_body(
1519 r#"{"data": [{"balance": 0, "address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "trc20": [{"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t": "10000000"}]}], "success": true}"#,
1520 )
1521 .create_async()
1522 .await;
1523
1524 let config = ChainsConfig {
1525 tron_api: Some(server.url()),
1526 api_keys: {
1527 let mut m = std::collections::HashMap::new();
1528 m.insert("tronscan".to_string(), "my-api-key".to_string());
1529 m
1530 },
1531 ..Default::default()
1532 };
1533 let client = TronClient::new(&config).unwrap();
1534 let balances = client.get_trc20_balances(VALID_ADDRESS).await.unwrap();
1535 assert_eq!(balances.len(), 1);
1536 }
1537
1538 #[test]
1539 fn test_validate_tron_address_bad_checksum() {
1540 let result = validate_tron_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCe");
1543 assert!(result.is_err());
1544 let err_str = result.unwrap_err().to_string();
1546 assert!(
1547 err_str.contains("checksum")
1548 || err_str.contains("base58")
1549 || err_str.contains("prefix")
1550 );
1551 }
1552
1553 #[tokio::test]
1554 async fn test_get_transaction_tron_success() {
1555 let mut server = mockito::Server::new_async().await;
1556 let _mock = server
1557 .mock(
1558 "GET",
1559 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1560 )
1561 .with_status(200)
1562 .with_header("content-type", "application/json")
1563 .with_body(
1564 r#"{"data": [{
1565 "txID": "b3c12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1566 "block_number": 50000000,
1567 "block_timestamp": 1700000000000,
1568 "raw_data": {
1569 "contract": [{
1570 "parameter": {
1571 "value": {
1572 "amount": 5000000,
1573 "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf",
1574 "to_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCg"
1575 }
1576 }
1577 }]
1578 },
1579 "ret": [{"contractRet": "SUCCESS"}]
1580 }], "success": true}"#,
1581 )
1582 .create_async()
1583 .await;
1584
1585 let client = TronClient::with_api_url(&server.url());
1586 let tx = client.get_transaction(VALID_TX_HASH).await.unwrap();
1587 assert_eq!(tx.hash, VALID_TX_HASH);
1588 assert!(tx.status.unwrap());
1589 assert_eq!(tx.value, "5000000");
1590 assert_eq!(tx.timestamp, Some(1700000000)); }
1592
1593 #[tokio::test]
1594 async fn test_get_transaction_tron_error() {
1595 let mut server = mockito::Server::new_async().await;
1596 let _mock = server
1597 .mock(
1598 "GET",
1599 mockito::Matcher::Regex(r"/v1/transactions/.*".to_string()),
1600 )
1601 .with_status(200)
1602 .with_header("content-type", "application/json")
1603 .with_body(r#"{"data": [], "success": false, "error": "Transaction not found"}"#)
1604 .create_async()
1605 .await;
1606
1607 let client = TronClient::with_api_url(&server.url());
1608 let result = client.get_transaction(VALID_TX_HASH).await;
1609 assert!(result.is_err());
1610 }
1611
1612 #[tokio::test]
1613 async fn test_get_transactions_tron_success() {
1614 let mut server = mockito::Server::new_async().await;
1615 let _mock = server
1616 .mock(
1617 "GET",
1618 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1619 )
1620 .with_status(200)
1621 .with_header("content-type", "application/json")
1622 .with_body(
1623 r#"{"data": [
1624 {
1625 "txID": "aaa12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1626 "block_number": 50000001,
1627 "block_timestamp": 1700000003000,
1628 "raw_data": {"contract": [{"parameter": {"value": {"amount": 1000000, "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"}}}]},
1629 "ret": [{"contractRet": "SUCCESS"}]
1630 },
1631 {
1632 "txID": "bbb12d62ad7e7b8b83b09a68b9b8f9b23a1b8f8b8f9b8f9b8f9b8f9b8f9b8f9b",
1633 "block_number": 50000002,
1634 "block_timestamp": 1700000006000,
1635 "raw_data": {"contract": [{"parameter": {"value": {"amount": 2000000, "owner_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "to_address": "TDqSquXBgUCLYvYC4XZgrprLK589dkhSCg"}}}]},
1636 "ret": [{"contractRet": "SUCCESS"}]
1637 }
1638 ], "success": true}"#,
1639 )
1640 .create_async()
1641 .await;
1642
1643 let client = TronClient::with_api_url(&server.url());
1644 let txs = client.get_transactions(VALID_ADDRESS, 10).await.unwrap();
1645 assert_eq!(txs.len(), 2);
1646 }
1647
1648 #[tokio::test]
1649 async fn test_get_transactions_tron_error() {
1650 let mut server = mockito::Server::new_async().await;
1651 let _mock = server
1652 .mock(
1653 "GET",
1654 mockito::Matcher::Regex(r"/v1/accounts/.*/transactions.*".to_string()),
1655 )
1656 .with_status(200)
1657 .with_header("content-type", "application/json")
1658 .with_body(r#"{"data": [], "success": false, "error": "Invalid address"}"#)
1659 .create_async()
1660 .await;
1661
1662 let client = TronClient::with_api_url(&server.url());
1663 let result = client.get_transactions(VALID_ADDRESS, 10).await;
1664 assert!(result.is_err());
1665 }
1666
1667 #[tokio::test]
1668 async fn test_get_balance_error_response() {
1669 let mut server = mockito::Server::new_async().await;
1670 let _mock = server
1671 .mock(
1672 "GET",
1673 mockito::Matcher::Regex(r"/v1/accounts/.*".to_string()),
1674 )
1675 .with_status(200)
1676 .with_header("content-type", "application/json")
1677 .with_body(r#"{"data": [], "success": false, "error": "Account not found"}"#)
1678 .create_async()
1679 .await;
1680
1681 let client = TronClient::with_api_url(&server.url());
1682 let result = client.get_balance(VALID_ADDRESS).await;
1683 assert!(result.is_err());
1684 assert!(
1685 result
1686 .unwrap_err()
1687 .to_string()
1688 .contains("Account not found")
1689 );
1690 }
1691}