1use crate::domain::transaction::stellar::utils::{
3 create_contract_data_key, extract_scval_from_contract_data, extract_u32_from_scval,
4 parse_account_id, parse_contract_address, parse_ledger_entry_from_xdr,
5 query_contract_data_with_fallback, StellarTransactionUtilsError,
6};
7use crate::models::{StellarTokenKind, StellarTokenMetadata};
8use crate::services::provider::StellarProviderTrait;
9use soroban_rs::xdr::{
10 AccountId, AlphaNum12, AlphaNum4, Asset, AssetCode12, AssetCode4, ContractDataEntry,
11 ContractId, Hash, LedgerEntryData, LedgerKey, ScAddress, ScSymbol, ScVal, TrustLineEntry,
12 TrustLineEntryExt, TrustLineEntryV1,
13};
14use std::str::FromStr;
15use tracing::{debug, trace, warn};
16
17const STELLAR_ADDRESS_LENGTH: usize = 56;
19const MAX_ASSET_CODE_LENGTH: usize = 12;
20const DEFAULT_STELLAR_DECIMALS: u32 = 7;
21const STELLAR_ACCOUNT_PREFIX: char = 'G';
22
23fn parse_asset_identifier(asset_id: &str) -> Result<(&str, &str), StellarTransactionUtilsError> {
37 asset_id
38 .split_once(':')
39 .ok_or_else(|| StellarTransactionUtilsError::InvalidAssetFormat(asset_id.to_string()))
40}
41
42fn validate_and_parse_issuer(
59 issuer: &str,
60 asset_id: &str,
61) -> Result<AccountId, StellarTransactionUtilsError> {
62 if issuer.is_empty() {
63 return Err(StellarTransactionUtilsError::EmptyIssuerAddress(
64 asset_id.to_string(),
65 ));
66 }
67
68 if issuer.len() != STELLAR_ADDRESS_LENGTH {
69 return Err(StellarTransactionUtilsError::InvalidIssuerLength(
70 STELLAR_ADDRESS_LENGTH,
71 issuer.to_string(),
72 ));
73 }
74
75 if !issuer.starts_with(STELLAR_ACCOUNT_PREFIX) {
76 return Err(StellarTransactionUtilsError::InvalidIssuerPrefix(
77 STELLAR_ACCOUNT_PREFIX,
78 issuer.to_string(),
79 ));
80 }
81
82 parse_account_id(issuer)
84}
85
86pub async fn get_token_balance<P>(
112 provider: &P,
113 account_id: &str,
114 asset_id: &str,
115) -> Result<u64, StellarTransactionUtilsError>
116where
117 P: StellarProviderTrait + Send + Sync,
118{
119 if asset_id == "native" || asset_id == "XLM" {
121 let account_entry = provider
122 .get_account(account_id)
123 .await
124 .map_err(|e| StellarTransactionUtilsError::AccountFetchFailed(e.to_string()))?;
125 return Ok(account_entry.balance as u64);
126 }
127
128 if ContractId::from_str(asset_id).is_ok() {
130 return get_contract_token_balance(provider, account_id, asset_id).await;
131 }
132
133 get_asset_trustline_balance(provider, account_id, asset_id).await
135}
136
137async fn get_asset_trustline_balance<P>(
143 provider: &P,
144 account_id: &str,
145 asset_id: &str,
146) -> Result<u64, StellarTransactionUtilsError>
147where
148 P: StellarProviderTrait + Send + Sync,
149{
150 let (code, issuer) = parse_asset_identifier(asset_id)?;
151
152 if code.is_empty() || code.len() > MAX_ASSET_CODE_LENGTH {
155 return Err(StellarTransactionUtilsError::AssetCodeTooLong(
156 MAX_ASSET_CODE_LENGTH,
157 code.to_string(),
158 ));
159 }
160
161 let issuer_id = parse_account_id(issuer)?;
162 let account_xdr = parse_account_id(account_id)?;
163
164 let asset = if code.len() <= 4 {
165 let mut buf = [0u8; 4];
166 buf[..code.len()].copy_from_slice(code.as_bytes());
167 Asset::CreditAlphanum4(AlphaNum4 {
168 asset_code: AssetCode4(buf),
169 issuer: issuer_id,
170 })
171 } else {
172 let mut buf = [0u8; 12];
173 buf[..code.len()].copy_from_slice(code.as_bytes());
174 Asset::CreditAlphanum12(AlphaNum12 {
175 asset_code: AssetCode12(buf),
176 issuer: issuer_id,
177 })
178 };
179
180 let ledger_key = LedgerKey::Trustline(soroban_rs::xdr::LedgerKeyTrustLine {
181 account_id: account_xdr,
182 asset: match asset {
183 Asset::CreditAlphanum4(a) => soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(a),
184 Asset::CreditAlphanum12(a) => soroban_rs::xdr::TrustLineAsset::CreditAlphanum12(a),
185 Asset::Native => return Err(StellarTransactionUtilsError::NativeAssetInTrustlineQuery),
186 },
187 });
188
189 let resp = provider
190 .get_ledger_entries(&[ledger_key])
191 .await
192 .map_err(|e| {
193 StellarTransactionUtilsError::TrustlineQueryFailed(asset_id.into(), e.to_string())
194 })?;
195
196 let entries = resp.entries.ok_or_else(|| {
197 StellarTransactionUtilsError::NoTrustlineFound(asset_id.into(), account_id.into())
198 })?;
199
200 if entries.is_empty() {
201 return Err(StellarTransactionUtilsError::NoTrustlineFound(
202 asset_id.into(),
203 account_id.into(),
204 ));
205 }
206
207 let entry = parse_ledger_entry_from_xdr(&entries[0].xdr, asset_id)?;
208
209 match entry {
210 LedgerEntryData::Trustline(TrustLineEntry {
211 balance,
212 ext: TrustLineEntryExt::V1(TrustLineEntryV1 { liabilities, .. }),
213 ..
214 }) => {
215 let available_balance = balance.saturating_sub(liabilities.selling);
218 debug!(
219 account_id = %account_id,
220 asset_id = %asset_id,
221 total_balance = balance,
222 selling_liabilities = liabilities.selling,
223 buying_liabilities = liabilities.buying,
224 available_balance = available_balance,
225 "Trustline balance retrieved (V1 with liabilities)"
226 );
227 Ok(available_balance.max(0) as u64)
228 }
229 LedgerEntryData::Trustline(TrustLineEntry {
230 balance,
231 ext: TrustLineEntryExt::V0,
232 ..
233 }) => {
234 debug!(
236 account_id = %account_id,
237 asset_id = %asset_id,
238 balance_raw = balance,
239 balance_u64 = balance as u64,
240 "Trustline balance retrieved (V0, no liabilities)"
241 );
242 Ok(balance.max(0) as u64)
243 }
244
245 _ => Err(StellarTransactionUtilsError::UnexpectedTrustlineEntryType),
246 }
247}
248
249async fn get_contract_token_balance<P>(
251 provider: &P,
252 account_id: &str,
253 contract_address: &str,
254) -> Result<u64, StellarTransactionUtilsError>
255where
256 P: StellarProviderTrait + Send + Sync,
257{
258 let contract_hash = parse_contract_address(contract_address)?;
260 let account_xdr_id = parse_account_id(account_id)?;
261 let account_sc_address = ScAddress::Account(account_xdr_id);
262
263 let balance_key = create_contract_data_key("Balance", Some(account_sc_address))?;
265
266 let error_context = format!("contract {contract_address} balance for account {account_id}");
268 let ledger_entries =
269 query_contract_data_with_fallback(provider, contract_hash, balance_key, &error_context)
270 .await?;
271
272 let entries = match ledger_entries.entries {
274 Some(entries) if !entries.is_empty() => entries,
275 _ => {
276 warn!(
278 "No balance entry found for contract {} on account {}, assuming zero balance",
279 contract_address, account_id
280 );
281 return Ok(0);
282 }
283 };
284
285 let entry_result = &entries[0];
286 let entry = parse_ledger_entry_from_xdr(&entry_result.xdr, &error_context)?;
287
288 match entry {
289 LedgerEntryData::ContractData(ContractDataEntry { val, .. }) => match val {
290 ScVal::I128(parts) => {
291 if parts.hi != 0 {
292 return Err(StellarTransactionUtilsError::BalanceTooLarge(
293 parts.hi, parts.lo,
294 ));
295 }
296 let lo_as_i64 = parts.lo as i64;
299 if lo_as_i64 < 0 {
300 return Err(StellarTransactionUtilsError::NegativeBalanceI128(parts.lo));
301 }
302 Ok(lo_as_i64 as u64)
303 }
304 ScVal::U64(n) => Ok(n),
305 ScVal::I64(n) => {
306 if n < 0 {
307 return Err(StellarTransactionUtilsError::NegativeBalanceI64(n));
308 }
309 Ok(n as u64)
310 }
311 other => Err(StellarTransactionUtilsError::UnexpectedBalanceType(
312 format!("{other:?}"),
313 )),
314 },
315 _ => Err(StellarTransactionUtilsError::UnexpectedContractDataEntryType),
316 }
317}
318
319pub async fn get_token_metadata<P>(
349 provider: &P,
350 asset_id: &str,
351) -> Result<StellarTokenMetadata, StellarTransactionUtilsError>
352where
353 P: StellarProviderTrait + Send + Sync,
354{
355 if asset_id == "native" || asset_id == "XLM" || asset_id.is_empty() {
357 return Ok(StellarTokenMetadata {
358 kind: StellarTokenKind::Native,
359 decimals: DEFAULT_STELLAR_DECIMALS,
360 canonical_asset_id: "native".to_string(),
361 });
362 }
363
364 if ContractId::from_str(asset_id).is_ok() {
366 let decimals = get_contract_token_decimals(provider, asset_id)
368 .await
369 .unwrap_or_else(|| {
370 warn!(
371 contract_address = %asset_id,
372 "Could not fetch decimals from contract, using default"
373 );
374 DEFAULT_STELLAR_DECIMALS
375 });
376
377 return Ok(StellarTokenMetadata {
378 kind: StellarTokenKind::Contract {
379 contract_id: asset_id.to_string(),
380 },
381 decimals,
382 canonical_asset_id: asset_id.to_uppercase().to_string(),
383 });
384 }
385
386 let (code, issuer) = parse_asset_identifier(asset_id)?;
389
390 if code.is_empty() {
392 return Err(StellarTransactionUtilsError::EmptyAssetCode(
393 asset_id.to_string(),
394 ));
395 }
396
397 if code.len() > MAX_ASSET_CODE_LENGTH {
398 return Err(StellarTransactionUtilsError::AssetCodeTooLong(
399 MAX_ASSET_CODE_LENGTH,
400 code.to_string(),
401 ));
402 }
403
404 validate_and_parse_issuer(issuer, asset_id)?;
406
407 Ok(StellarTokenMetadata {
410 kind: StellarTokenKind::Classic {
411 code: code.to_string(),
412 issuer: issuer.to_string(),
413 },
414 decimals: DEFAULT_STELLAR_DECIMALS,
415 canonical_asset_id: asset_id.to_string(),
416 })
417}
418
419pub async fn get_contract_token_decimals<P>(provider: &P, contract_address: &str) -> Option<u32>
441where
442 P: StellarProviderTrait + Send + Sync,
443{
444 debug!(
445 contract_address = %contract_address,
446 "Fetching decimals for contract token"
447 );
448
449 let contract_hash = match parse_contract_address(contract_address) {
451 Ok(hash) => hash,
452 Err(e) => {
453 warn!(
454 contract_address = %contract_address,
455 error = %e,
456 "Failed to parse contract address"
457 );
458 return None;
459 }
460 };
461
462 if let Some(decimals) = invoke_decimals_function(provider, contract_address).await {
464 debug!(
465 contract_address = %contract_address,
466 decimals = %decimals,
467 "Successfully fetched decimals via contract invocation"
468 );
469 return Some(decimals);
470 }
471
472 debug!(
474 contract_address = %contract_address,
475 "Contract invocation failed, trying storage query"
476 );
477
478 query_decimals_from_storage(provider, contract_address, contract_hash).await
479}
480
481async fn invoke_decimals_function<P>(provider: &P, contract_address: &str) -> Option<u32>
494where
495 P: StellarProviderTrait + Send + Sync,
496{
497 let function_name = match ScSymbol::try_from("decimals") {
499 Ok(sym) => sym,
500 Err(e) => {
501 warn!(contract_address = %contract_address, error = ?e, "Failed to create decimals symbol");
502 return None;
503 }
504 };
505
506 let args: Vec<ScVal> = vec![];
508
509 match provider
511 .call_contract(contract_address, &function_name, args)
512 .await
513 {
514 Ok(result) => extract_u32_from_scval(&result, "decimals() result"),
515 Err(e) => {
516 debug!(contract_address = %contract_address, error = %e, "Failed to invoke decimals() function");
517 None
518 }
519 }
520}
521
522async fn query_decimals_from_storage<P>(
537 provider: &P,
538 contract_address: &str,
539 contract_hash: Hash,
540) -> Option<u32>
541where
542 P: StellarProviderTrait + Send + Sync,
543{
544 let decimals_key = match create_contract_data_key("Decimals", None) {
546 Ok(key) => key,
547 Err(e) => {
548 warn!(
549 contract_address = %contract_address,
550 error = %e,
551 "Failed to create Decimals key"
552 );
553 return None;
554 }
555 };
556
557 let error_context = format!("contract {contract_address} decimals");
559 let ledger_entries = match query_contract_data_with_fallback(
560 provider,
561 contract_hash,
562 decimals_key,
563 &error_context,
564 )
565 .await
566 {
567 Ok(entries) => entries,
568 Err(e) => {
569 debug!(
570 contract_address = %contract_address,
571 error = %e,
572 "Failed to query contract data for decimals"
573 );
574 return None;
575 }
576 };
577
578 let val = match extract_scval_from_contract_data(&ledger_entries, &error_context) {
580 Ok(v) => v,
581 Err(_) => {
582 trace!(
583 contract_address = %contract_address,
584 "No decimals entry found in contract data"
585 );
586 return None;
587 }
588 };
589
590 extract_u32_from_scval(&val, "decimals storage value")
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::domain::transaction::stellar::test_helpers::{create_account_id, TEST_PK};
598 use crate::services::provider::MockStellarProviderTrait;
599 use futures::future::ready;
600 use mockall::predicate::*;
601 use soroban_rs::xdr::{AccountEntry, AccountEntryExt, SequenceNumber, Thresholds};
602 use std::str::FromStr;
603
604 fn create_mock_provider() -> MockStellarProviderTrait {
606 MockStellarProviderTrait::new()
607 }
608
609 fn create_mock_account_entry(balance: i64) -> AccountEntry {
611 AccountEntry {
612 account_id: create_account_id(TEST_PK),
613 balance,
614 seq_num: SequenceNumber(1),
615 num_sub_entries: 0,
616 inflation_dest: None,
617 flags: 0,
618 home_domain: Default::default(),
619 thresholds: Thresholds([1, 0, 0, 0]),
620 signers: Default::default(),
621 ext: AccountEntryExt::V0,
622 }
623 }
624
625 #[test]
626 fn test_parse_asset_identifier_valid() {
627 let result =
628 parse_asset_identifier("USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5");
629 assert!(result.is_ok());
630 let (code, issuer) = result.unwrap();
631 assert_eq!(code, "USDC");
632 assert_eq!(
633 issuer,
634 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
635 );
636 }
637
638 #[test]
639 fn test_parse_asset_identifier_invalid() {
640 let result =
642 parse_asset_identifier("USDCGBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5");
643 assert!(result.is_err());
644 match result.unwrap_err() {
645 StellarTransactionUtilsError::InvalidAssetFormat(_) => {}
646 e => panic!("Expected InvalidAssetFormat, got: {:?}", e),
647 }
648
649 let result = parse_asset_identifier("");
651 assert!(result.is_err());
652 }
653
654 #[test]
655 fn test_parse_asset_identifier_multiple_colons() {
656 let result = parse_asset_identifier(
658 "USD:C:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
659 );
660 assert!(result.is_ok());
661 let (code, issuer) = result.unwrap();
662 assert_eq!(code, "USD");
663 assert_eq!(
664 issuer,
665 "C:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
666 );
667 }
668
669 #[test]
670 fn test_validate_and_parse_issuer_valid() {
671 let issuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
672 let result = validate_and_parse_issuer(issuer, "USDC:GBBD47...");
673 assert!(result.is_ok());
674 }
675
676 #[test]
677 fn test_validate_and_parse_issuer_empty() {
678 let result = validate_and_parse_issuer("", "USDC:");
679 assert!(result.is_err());
680 match result.unwrap_err() {
681 StellarTransactionUtilsError::EmptyIssuerAddress(_) => {}
682 e => panic!("Expected EmptyIssuerAddress, got: {:?}", e),
683 }
684 }
685
686 #[test]
687 fn test_validate_and_parse_issuer_wrong_length() {
688 let result = validate_and_parse_issuer("SHORTADDR", "USDC:SHORTADDR");
689 assert!(result.is_err());
690 match result.unwrap_err() {
691 StellarTransactionUtilsError::InvalidIssuerLength(expected, _) => {
692 assert_eq!(expected, STELLAR_ADDRESS_LENGTH);
693 }
694 e => panic!("Expected InvalidIssuerLength, got: {:?}", e),
695 }
696 }
697
698 #[test]
699 fn test_validate_and_parse_issuer_wrong_prefix() {
700 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
702 let result = validate_and_parse_issuer(contract_addr, "USDC:C...");
703 assert!(result.is_err());
704 match result.unwrap_err() {
705 StellarTransactionUtilsError::InvalidIssuerPrefix(expected, _) => {
706 assert_eq!(expected, STELLAR_ACCOUNT_PREFIX);
707 }
708 e => panic!("Expected InvalidIssuerPrefix, got: {:?}", e),
709 }
710 }
711
712 #[test]
713 fn test_validate_and_parse_issuer_invalid_checksum() {
714 let bad_issuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA6"; let result = validate_and_parse_issuer(bad_issuer, "USDC:G...");
717 assert!(result.is_err());
718 }
719
720 #[tokio::test]
721 async fn test_get_token_balance_native_xlm() {
722 let mut provider = create_mock_provider();
723
724 let test_balance = 100_0000000i64; let account_entry = create_mock_account_entry(test_balance);
726
727 provider
728 .expect_get_account()
729 .with(eq(TEST_PK))
730 .times(1)
731 .returning(move |_| Box::pin(ready(Ok(account_entry.clone()))));
732
733 let result = get_token_balance(&provider, TEST_PK, "native").await;
734
735 assert!(result.is_ok());
736 assert_eq!(result.unwrap(), test_balance as u64);
737 }
738
739 #[tokio::test]
740 async fn test_get_token_balance_xlm_identifier() {
741 let mut provider = create_mock_provider();
742
743 let test_balance = 50_0000000i64; let account_entry = create_mock_account_entry(test_balance);
745
746 provider
747 .expect_get_account()
748 .with(eq(TEST_PK))
749 .times(1)
750 .returning(move |_| Box::pin(ready(Ok(account_entry.clone()))));
751
752 let result = get_token_balance(&provider, TEST_PK, "XLM").await;
754
755 assert!(result.is_ok());
756 assert_eq!(result.unwrap(), test_balance as u64);
757 }
758
759 #[test]
760 fn test_asset_code_length_validation() {
761 assert!(parse_asset_identifier(
763 "A:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
764 )
765 .is_ok());
766 assert!(parse_asset_identifier(
767 "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
768 )
769 .is_ok());
770 assert!(parse_asset_identifier(
771 "MAXLENCODE12:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
772 )
773 .is_ok());
774
775 let (code, _) =
777 parse_asset_identifier(":GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5")
778 .unwrap();
779 assert_eq!(code, "");
780 }
781
782 #[tokio::test]
783 async fn test_get_token_metadata_native() {
784 let provider = create_mock_provider();
785
786 let result = get_token_metadata(&provider, "native").await;
787 assert!(result.is_ok());
788 let metadata = result.unwrap();
789
790 assert_eq!(metadata.kind, StellarTokenKind::Native);
791 assert_eq!(metadata.decimals, DEFAULT_STELLAR_DECIMALS);
792 assert_eq!(metadata.canonical_asset_id, "native");
793 }
794
795 #[tokio::test]
796 async fn test_get_token_metadata_xlm_identifier() {
797 let provider = create_mock_provider();
798
799 let result = get_token_metadata(&provider, "XLM").await;
800 assert!(result.is_ok());
801 let metadata = result.unwrap();
802
803 assert_eq!(metadata.kind, StellarTokenKind::Native);
804 assert_eq!(metadata.decimals, DEFAULT_STELLAR_DECIMALS);
805 assert_eq!(metadata.canonical_asset_id, "native");
806 }
807
808 #[tokio::test]
809 async fn test_get_token_metadata_empty_string() {
810 let provider = create_mock_provider();
811
812 let result = get_token_metadata(&provider, "").await;
814 assert!(result.is_ok());
815 let metadata = result.unwrap();
816
817 assert_eq!(metadata.kind, StellarTokenKind::Native);
818 assert_eq!(metadata.decimals, DEFAULT_STELLAR_DECIMALS);
819 assert_eq!(metadata.canonical_asset_id, "native");
820 }
821
822 #[tokio::test]
823 async fn test_get_token_metadata_classic_asset() {
824 let provider = create_mock_provider();
825
826 let asset_id = "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
827 let result = get_token_metadata(&provider, asset_id).await;
828 assert!(result.is_ok());
829 let metadata = result.unwrap();
830
831 match metadata.kind {
832 StellarTokenKind::Classic { code, issuer } => {
833 assert_eq!(code, "USDC");
834 assert_eq!(
835 issuer,
836 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
837 );
838 }
839 _ => panic!("Expected Classic token kind"),
840 }
841 assert_eq!(metadata.decimals, DEFAULT_STELLAR_DECIMALS);
842 assert_eq!(metadata.canonical_asset_id, asset_id);
843 }
844
845 #[tokio::test]
846 async fn test_get_token_metadata_classic_asset_credit12() {
847 let provider = create_mock_provider();
848
849 let asset_id = "LONGASSETCD:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
850 let result = get_token_metadata(&provider, asset_id).await;
851 assert!(result.is_ok());
852 let metadata = result.unwrap();
853
854 match metadata.kind {
855 StellarTokenKind::Classic { code, issuer } => {
856 assert_eq!(code, "LONGASSETCD");
857 assert_eq!(
858 issuer,
859 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
860 );
861 }
862 _ => panic!("Expected Classic token kind"),
863 }
864 assert_eq!(metadata.decimals, DEFAULT_STELLAR_DECIMALS);
865 }
866
867 #[tokio::test]
868 async fn test_get_token_metadata_invalid_format() {
869 let provider = create_mock_provider();
870
871 let result = get_token_metadata(&provider, "INVALID_NO_COLON").await;
872 assert!(result.is_err());
873 match result.unwrap_err() {
874 StellarTransactionUtilsError::InvalidAssetFormat(_) => {}
875 e => panic!("Expected InvalidAssetFormat, got: {:?}", e),
876 }
877 }
878
879 #[tokio::test]
880 async fn test_get_token_metadata_empty_code() {
881 let provider = create_mock_provider();
882
883 let result = get_token_metadata(
884 &provider,
885 ":GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
886 )
887 .await;
888 assert!(result.is_err());
889 match result.unwrap_err() {
890 StellarTransactionUtilsError::EmptyAssetCode(_) => {}
891 e => panic!("Expected EmptyAssetCode, got: {:?}", e),
892 }
893 }
894
895 #[tokio::test]
896 async fn test_get_token_metadata_code_too_long() {
897 let provider = create_mock_provider();
898
899 let result = get_token_metadata(
900 &provider,
901 "VERYLONGASSETCODE:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
902 )
903 .await;
904 assert!(result.is_err());
905 match result.unwrap_err() {
906 StellarTransactionUtilsError::AssetCodeTooLong(max, code) => {
907 assert_eq!(max, MAX_ASSET_CODE_LENGTH);
908 assert_eq!(code, "VERYLONGASSETCODE");
909 }
910 e => panic!("Expected AssetCodeTooLong, got: {:?}", e),
911 }
912 }
913
914 #[tokio::test]
915 async fn test_get_token_metadata_empty_issuer() {
916 let provider = create_mock_provider();
917
918 let result = get_token_metadata(&provider, "USDC:").await;
919 assert!(result.is_err());
920 match result.unwrap_err() {
921 StellarTransactionUtilsError::EmptyIssuerAddress(_) => {}
922 e => panic!("Expected EmptyIssuerAddress, got: {:?}", e),
923 }
924 }
925
926 #[tokio::test]
927 async fn test_get_token_metadata_invalid_issuer_length() {
928 let provider = create_mock_provider();
929
930 let result = get_token_metadata(&provider, "USDC:INVALID").await;
931 assert!(result.is_err());
932 match result.unwrap_err() {
933 StellarTransactionUtilsError::InvalidIssuerLength(expected, _) => {
934 assert_eq!(expected, STELLAR_ADDRESS_LENGTH);
935 }
936 e => panic!("Expected InvalidIssuerLength, got: {:?}", e),
937 }
938 }
939
940 #[tokio::test]
941 async fn test_get_token_metadata_invalid_issuer_prefix() {
942 let provider = create_mock_provider();
943
944 let result = get_token_metadata(
946 &provider,
947 "USDC:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
948 )
949 .await;
950 assert!(result.is_err());
951 match result.unwrap_err() {
952 StellarTransactionUtilsError::InvalidIssuerPrefix(expected, _) => {
953 assert_eq!(expected, STELLAR_ACCOUNT_PREFIX);
954 }
955 e => panic!("Expected InvalidIssuerPrefix, got: {:?}", e),
956 }
957 }
958
959 #[tokio::test]
960 async fn test_get_token_metadata_contract_valid() {
961 let mut provider = create_mock_provider();
962
963 provider.expect_call_contract().returning(|_, _, _| {
965 Box::pin(ready(Err(crate::services::provider::ProviderError::Other(
966 "Contract call failed".to_string(),
967 ))))
968 });
969
970 provider.expect_get_ledger_entries().returning(|_| {
971 Box::pin(ready(Ok(
972 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
973 entries: None,
974 latest_ledger: 0,
975 },
976 )))
977 });
978
979 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
980 let result = get_token_metadata(&provider, contract_addr).await;
981 assert!(result.is_ok());
982 let metadata = result.unwrap();
983
984 match metadata.kind {
985 StellarTokenKind::Contract { contract_id } => {
986 assert_eq!(contract_id, contract_addr);
987 }
988 _ => panic!("Expected Contract token kind"),
989 }
990 assert_eq!(metadata.decimals, DEFAULT_STELLAR_DECIMALS);
991 assert_eq!(metadata.canonical_asset_id, contract_addr.to_uppercase());
992 }
993
994 #[tokio::test]
995 async fn test_get_token_balance_trustline_v0_success() {
996 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
997 use soroban_rs::xdr::{
998 LedgerEntry, LedgerEntryData, LedgerEntryExt, TrustLineAsset, WriteXdr,
999 };
1000
1001 let mut provider = create_mock_provider();
1002
1003 provider.expect_get_ledger_entries().returning(|_| {
1005 let trustline_entry = TrustLineEntry {
1006 account_id: create_account_id(TEST_PK),
1007 asset: TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1008 asset_code: AssetCode4(*b"USDC"),
1009 issuer: create_account_id(
1010 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
1011 ),
1012 }),
1013 balance: 10_0000000, limit: 1000_0000000,
1015 flags: 1,
1016 ext: TrustLineEntryExt::V0,
1017 };
1018
1019 let ledger_entry = LedgerEntry {
1020 last_modified_ledger_seq: 0,
1021 data: LedgerEntryData::Trustline(trustline_entry),
1022 ext: LedgerEntryExt::V0,
1023 };
1024
1025 let xdr_base64 = ledger_entry
1026 .data
1027 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1028 .unwrap();
1029
1030 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1031 entries: Some(vec![LedgerEntryResult {
1032 key: String::new(),
1033 xdr: xdr_base64,
1034 last_modified_ledger: 0,
1035 live_until_ledger_seq_ledger_seq: None,
1036 }]),
1037 latest_ledger: 0,
1038 })))
1039 });
1040
1041 let account = TEST_PK;
1042 let asset = "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
1043
1044 let result = get_token_balance(&provider, account, asset).await;
1045 assert!(result.is_ok());
1046 assert_eq!(result.unwrap(), 10_0000000);
1047 }
1048
1049 #[tokio::test]
1050 async fn test_get_token_balance_trustline_v1_with_liabilities() {
1051 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1052 use soroban_rs::xdr::{
1053 LedgerEntry, LedgerEntryData, LedgerEntryExt, Liabilities, TrustLineAsset,
1054 TrustLineEntryV1, TrustLineEntryV1Ext, WriteXdr,
1055 };
1056
1057 let mut provider = create_mock_provider();
1058
1059 provider.expect_get_ledger_entries().returning(|_| {
1061 let trustline_entry = TrustLineEntry {
1062 account_id: create_account_id(TEST_PK),
1063 asset: TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1064 asset_code: AssetCode4(*b"USDC"),
1065 issuer: create_account_id(
1066 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
1067 ),
1068 }),
1069 balance: 10_0000000, limit: 1000_0000000,
1071 flags: 1,
1072 ext: TrustLineEntryExt::V1(TrustLineEntryV1 {
1073 liabilities: Liabilities {
1074 buying: 1_0000000, selling: 2_0000000, },
1077 ext: TrustLineEntryV1Ext::V0,
1078 }),
1079 };
1080
1081 let ledger_entry = LedgerEntry {
1082 last_modified_ledger_seq: 0,
1083 data: LedgerEntryData::Trustline(trustline_entry),
1084 ext: LedgerEntryExt::V0,
1085 };
1086
1087 let xdr_base64 = ledger_entry
1088 .data
1089 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1090 .unwrap();
1091
1092 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1093 entries: Some(vec![LedgerEntryResult {
1094 key: String::new(),
1095 xdr: xdr_base64,
1096 last_modified_ledger: 0,
1097 live_until_ledger_seq_ledger_seq: None,
1098 }]),
1099 latest_ledger: 0,
1100 })))
1101 });
1102
1103 let account = TEST_PK;
1104 let asset = "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
1105
1106 let result = get_token_balance(&provider, account, asset).await;
1107 assert!(result.is_ok());
1108 assert_eq!(result.unwrap(), 8_0000000);
1110 }
1111
1112 #[tokio::test]
1113 async fn test_get_token_balance_trustline_v1_selling_exceeds_balance() {
1114 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1115 use soroban_rs::xdr::{
1116 LedgerEntry, LedgerEntryData, LedgerEntryExt, Liabilities, TrustLineAsset,
1117 TrustLineEntryV1, TrustLineEntryV1Ext, WriteXdr,
1118 };
1119
1120 let mut provider = create_mock_provider();
1121
1122 provider.expect_get_ledger_entries().returning(|_| {
1124 let trustline_entry = TrustLineEntry {
1125 account_id: create_account_id(TEST_PK),
1126 asset: TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1127 asset_code: AssetCode4(*b"USDC"),
1128 issuer: create_account_id(
1129 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
1130 ),
1131 }),
1132 balance: 5_0000000, limit: 1000_0000000,
1134 flags: 1,
1135 ext: TrustLineEntryExt::V1(TrustLineEntryV1 {
1136 liabilities: Liabilities {
1137 buying: 0,
1138 selling: 10_0000000, },
1140 ext: TrustLineEntryV1Ext::V0,
1141 }),
1142 };
1143
1144 let ledger_entry = LedgerEntry {
1145 last_modified_ledger_seq: 0,
1146 data: LedgerEntryData::Trustline(trustline_entry),
1147 ext: LedgerEntryExt::V0,
1148 };
1149
1150 let xdr_base64 = ledger_entry
1151 .data
1152 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1153 .unwrap();
1154
1155 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1156 entries: Some(vec![LedgerEntryResult {
1157 key: String::new(),
1158 xdr: xdr_base64,
1159 last_modified_ledger: 0,
1160 live_until_ledger_seq_ledger_seq: None,
1161 }]),
1162 latest_ledger: 0,
1163 })))
1164 });
1165
1166 let account = TEST_PK;
1167 let asset = "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
1168
1169 let result = get_token_balance(&provider, account, asset).await;
1170 assert!(result.is_ok());
1171 assert_eq!(result.unwrap(), 0);
1173 }
1174
1175 #[tokio::test]
1176 async fn test_get_token_balance_trustline_not_found() {
1177 use soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse;
1178
1179 let mut provider = create_mock_provider();
1180
1181 provider.expect_get_ledger_entries().returning(|_| {
1183 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1184 entries: None,
1185 latest_ledger: 0,
1186 })))
1187 });
1188
1189 let account = TEST_PK;
1190 let asset = "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
1191
1192 let result = get_token_balance(&provider, account, asset).await;
1193 assert!(result.is_err());
1194 match result.unwrap_err() {
1195 StellarTransactionUtilsError::NoTrustlineFound(asset_id, account_id) => {
1196 assert_eq!(asset_id, asset);
1197 assert_eq!(account_id, account);
1198 }
1199 e => panic!("Expected NoTrustlineFound, got: {:?}", e),
1200 }
1201 }
1202
1203 #[tokio::test]
1204 async fn test_get_token_balance_trustline_empty_entries() {
1205 use soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse;
1206
1207 let mut provider = create_mock_provider();
1208
1209 provider.expect_get_ledger_entries().returning(|_| {
1211 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1212 entries: Some(vec![]),
1213 latest_ledger: 0,
1214 })))
1215 });
1216
1217 let account = TEST_PK;
1218 let asset = "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
1219
1220 let result = get_token_balance(&provider, account, asset).await;
1221 assert!(result.is_err());
1222 match result.unwrap_err() {
1223 StellarTransactionUtilsError::NoTrustlineFound(asset_id, account_id) => {
1224 assert_eq!(asset_id, asset);
1225 assert_eq!(account_id, account);
1226 }
1227 e => panic!("Expected NoTrustlineFound, got: {:?}", e),
1228 }
1229 }
1230
1231 #[tokio::test]
1232 async fn test_get_token_balance_trustline_credit12() {
1233 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1234 use soroban_rs::xdr::{
1235 LedgerEntry, LedgerEntryData, LedgerEntryExt, TrustLineAsset, WriteXdr,
1236 };
1237
1238 let mut provider = create_mock_provider();
1239
1240 provider.expect_get_ledger_entries().returning(|_| {
1242 let trustline_entry = TrustLineEntry {
1243 account_id: create_account_id(TEST_PK),
1244 asset: TrustLineAsset::CreditAlphanum12(AlphaNum12 {
1245 asset_code: AssetCode12(*b"LONGASSETCD\0"),
1246 issuer: create_account_id(
1247 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
1248 ),
1249 }),
1250 balance: 25_0000000, limit: 1000_0000000,
1252 flags: 1,
1253 ext: TrustLineEntryExt::V0,
1254 };
1255
1256 let ledger_entry = LedgerEntry {
1257 last_modified_ledger_seq: 0,
1258 data: LedgerEntryData::Trustline(trustline_entry),
1259 ext: LedgerEntryExt::V0,
1260 };
1261
1262 let xdr_base64 = ledger_entry
1263 .data
1264 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1265 .unwrap();
1266
1267 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1268 entries: Some(vec![LedgerEntryResult {
1269 key: String::new(),
1270 xdr: xdr_base64,
1271 last_modified_ledger: 0,
1272 live_until_ledger_seq_ledger_seq: None,
1273 }]),
1274 latest_ledger: 0,
1275 })))
1276 });
1277
1278 let account = TEST_PK;
1279 let asset = "LONGASSETCD:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
1280
1281 let result = get_token_balance(&provider, account, asset).await;
1282 assert!(result.is_ok());
1283 assert_eq!(result.unwrap(), 25_0000000);
1284 }
1285
1286 #[tokio::test]
1287 async fn test_get_token_balance_trustline_invalid_asset_code_too_long() {
1288 let provider = create_mock_provider();
1289
1290 let account = TEST_PK;
1291 let asset = "VERYLONGASSETCODE:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
1292
1293 let result = get_token_balance(&provider, account, asset).await;
1294 assert!(result.is_err());
1295 match result.unwrap_err() {
1296 StellarTransactionUtilsError::AssetCodeTooLong(max, code) => {
1297 assert_eq!(max, MAX_ASSET_CODE_LENGTH);
1298 assert_eq!(code, "VERYLONGASSETCODE");
1299 }
1300 e => panic!("Expected AssetCodeTooLong, got: {:?}", e),
1301 }
1302 }
1303
1304 #[test]
1305 fn test_constants() {
1306 assert_eq!(STELLAR_ADDRESS_LENGTH, 56);
1308 assert_eq!(MAX_ASSET_CODE_LENGTH, 12);
1309 assert_eq!(DEFAULT_STELLAR_DECIMALS, 7);
1310 assert_eq!(STELLAR_ACCOUNT_PREFIX, 'G');
1311 }
1312
1313 #[test]
1314 fn test_contract_id_validation() {
1315 let valid_contract = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1317 assert!(ContractId::from_str(valid_contract).is_ok());
1318
1319 let account = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
1321 assert!(ContractId::from_str(account).is_err());
1322
1323 assert!(ContractId::from_str("INVALID").is_err());
1325 assert!(ContractId::from_str("").is_err());
1326 }
1327
1328 #[test]
1329 fn test_asset_identifier_edge_cases() {
1330 let result =
1332 parse_asset_identifier("USD :GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5");
1333 assert!(result.is_ok());
1334 let (code, _) = result.unwrap();
1335 assert_eq!(code, "USD "); let result =
1339 parse_asset_identifier("U$D:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5");
1340 assert!(result.is_ok());
1341 let (code, _) = result.unwrap();
1342 assert_eq!(code, "U$D");
1343 }
1344
1345 #[tokio::test]
1346 async fn test_get_token_balance_contract_token_no_balance_entry() {
1347 let mut provider = create_mock_provider();
1348
1349 provider.expect_get_ledger_entries().returning(|_| {
1351 Box::pin(ready(Ok(
1352 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
1353 entries: None,
1354 latest_ledger: 0,
1355 },
1356 )))
1357 });
1358
1359 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1360 let account = TEST_PK;
1361
1362 let result = get_token_balance(&provider, account, contract_addr).await;
1363
1364 assert!(result.is_ok());
1366 assert_eq!(result.unwrap(), 0);
1367 }
1368
1369 #[tokio::test]
1370 async fn test_get_token_balance_contract_token_i128_balance() {
1371 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1372 use soroban_rs::xdr::{
1373 ContractDataDurability, ContractDataEntry, ExtensionPoint, Int128Parts, LedgerEntry,
1374 LedgerEntryData, LedgerEntryExt, ScVal, WriteXdr,
1375 };
1376
1377 let mut provider = create_mock_provider();
1378
1379 provider.expect_get_ledger_entries().returning(|_| {
1381 let balance_val = ScVal::I128(Int128Parts { hi: 0, lo: 1000000 });
1382
1383 let contract_data = ContractDataEntry {
1384 ext: ExtensionPoint::V0,
1385 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1386 key: ScVal::Vec(None),
1387 durability: ContractDataDurability::Persistent,
1388 val: balance_val,
1389 };
1390
1391 let ledger_entry = LedgerEntry {
1392 last_modified_ledger_seq: 0,
1393 data: LedgerEntryData::ContractData(contract_data),
1394 ext: LedgerEntryExt::V0,
1395 };
1396
1397 let xdr_base64 = ledger_entry
1398 .data
1399 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1400 .unwrap();
1401
1402 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1403 entries: Some(vec![LedgerEntryResult {
1404 key: String::new(),
1405 xdr: xdr_base64,
1406 last_modified_ledger: 0,
1407 live_until_ledger_seq_ledger_seq: None,
1408 }]),
1409 latest_ledger: 0,
1410 })))
1411 });
1412
1413 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1414 let account = TEST_PK;
1415
1416 let result = get_token_balance(&provider, account, contract_addr).await;
1417 assert!(result.is_ok());
1418 assert_eq!(result.unwrap(), 1000000);
1419 }
1420
1421 #[tokio::test]
1422 async fn test_get_token_balance_contract_token_i128_balance_too_large() {
1423 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1424 use soroban_rs::xdr::{
1425 ContractDataDurability, ContractDataEntry, ExtensionPoint, Int128Parts, LedgerEntry,
1426 LedgerEntryData, LedgerEntryExt, ScVal, WriteXdr,
1427 };
1428
1429 let mut provider = create_mock_provider();
1430
1431 provider.expect_get_ledger_entries().returning(|_| {
1433 let balance_val = ScVal::I128(Int128Parts {
1434 hi: 1, lo: 1000000,
1436 });
1437
1438 let contract_data = ContractDataEntry {
1439 ext: ExtensionPoint::V0,
1440 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1441 key: ScVal::Vec(None),
1442 durability: ContractDataDurability::Persistent,
1443 val: balance_val,
1444 };
1445
1446 let ledger_entry = LedgerEntry {
1447 last_modified_ledger_seq: 0,
1448 data: LedgerEntryData::ContractData(contract_data),
1449 ext: LedgerEntryExt::V0,
1450 };
1451
1452 let xdr_base64 = ledger_entry
1453 .data
1454 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1455 .unwrap();
1456
1457 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1458 entries: Some(vec![LedgerEntryResult {
1459 key: String::new(),
1460 xdr: xdr_base64,
1461 last_modified_ledger: 0,
1462 live_until_ledger_seq_ledger_seq: None,
1463 }]),
1464 latest_ledger: 0,
1465 })))
1466 });
1467
1468 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1469 let account = TEST_PK;
1470
1471 let result = get_token_balance(&provider, account, contract_addr).await;
1472 assert!(result.is_err());
1473 match result.unwrap_err() {
1474 StellarTransactionUtilsError::BalanceTooLarge(hi, lo) => {
1475 assert_eq!(hi, 1);
1476 assert_eq!(lo, 1000000);
1477 }
1478 e => panic!("Expected BalanceTooLarge, got: {:?}", e),
1479 }
1480 }
1481
1482 #[tokio::test]
1483 async fn test_get_token_balance_contract_token_i128_negative() {
1484 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1485 use soroban_rs::xdr::{
1486 ContractDataDurability, ContractDataEntry, ExtensionPoint, Int128Parts, LedgerEntry,
1487 LedgerEntryData, LedgerEntryExt, ScVal, WriteXdr,
1488 };
1489
1490 let mut provider = create_mock_provider();
1491
1492 provider.expect_get_ledger_entries().returning(|_| {
1494 let balance_val = ScVal::I128(Int128Parts {
1495 hi: 0,
1496 lo: u64::MAX, });
1498
1499 let contract_data = ContractDataEntry {
1500 ext: ExtensionPoint::V0,
1501 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1502 key: ScVal::Vec(None),
1503 durability: ContractDataDurability::Persistent,
1504 val: balance_val,
1505 };
1506
1507 let ledger_entry = LedgerEntry {
1508 last_modified_ledger_seq: 0,
1509 data: LedgerEntryData::ContractData(contract_data),
1510 ext: LedgerEntryExt::V0,
1511 };
1512
1513 let xdr_base64 = ledger_entry
1514 .data
1515 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1516 .unwrap();
1517
1518 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1519 entries: Some(vec![LedgerEntryResult {
1520 key: String::new(),
1521 xdr: xdr_base64,
1522 last_modified_ledger: 0,
1523 live_until_ledger_seq_ledger_seq: None,
1524 }]),
1525 latest_ledger: 0,
1526 })))
1527 });
1528
1529 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1530 let account = TEST_PK;
1531
1532 let result = get_token_balance(&provider, account, contract_addr).await;
1533 assert!(result.is_err());
1534 match result.unwrap_err() {
1535 StellarTransactionUtilsError::NegativeBalanceI128(_) => {}
1536 e => panic!("Expected NegativeBalanceI128, got: {:?}", e),
1537 }
1538 }
1539
1540 #[tokio::test]
1541 async fn test_get_token_balance_contract_token_u64_balance() {
1542 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1543 use soroban_rs::xdr::{
1544 ContractDataDurability, ContractDataEntry, ExtensionPoint, LedgerEntry,
1545 LedgerEntryData, LedgerEntryExt, ScVal, WriteXdr,
1546 };
1547
1548 let mut provider = create_mock_provider();
1549
1550 provider.expect_get_ledger_entries().returning(|_| {
1552 let balance_val = ScVal::U64(5000000);
1553
1554 let contract_data = ContractDataEntry {
1555 ext: ExtensionPoint::V0,
1556 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1557 key: ScVal::Vec(None),
1558 durability: ContractDataDurability::Persistent,
1559 val: balance_val,
1560 };
1561
1562 let ledger_entry = LedgerEntry {
1563 last_modified_ledger_seq: 0,
1564 data: LedgerEntryData::ContractData(contract_data),
1565 ext: LedgerEntryExt::V0,
1566 };
1567
1568 let xdr_base64 = ledger_entry
1569 .data
1570 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1571 .unwrap();
1572
1573 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1574 entries: Some(vec![LedgerEntryResult {
1575 key: String::new(),
1576 xdr: xdr_base64,
1577 last_modified_ledger: 0,
1578 live_until_ledger_seq_ledger_seq: None,
1579 }]),
1580 latest_ledger: 0,
1581 })))
1582 });
1583
1584 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1585 let account = TEST_PK;
1586
1587 let result = get_token_balance(&provider, account, contract_addr).await;
1588 assert!(result.is_ok());
1589 assert_eq!(result.unwrap(), 5000000);
1590 }
1591
1592 #[tokio::test]
1593 async fn test_get_token_balance_contract_token_i64_positive() {
1594 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1595 use soroban_rs::xdr::{
1596 ContractDataDurability, ContractDataEntry, ExtensionPoint, LedgerEntry,
1597 LedgerEntryData, LedgerEntryExt, ScVal, WriteXdr,
1598 };
1599
1600 let mut provider = create_mock_provider();
1601
1602 provider.expect_get_ledger_entries().returning(|_| {
1604 let balance_val = ScVal::I64(3000000);
1605
1606 let contract_data = ContractDataEntry {
1607 ext: ExtensionPoint::V0,
1608 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1609 key: ScVal::Vec(None),
1610 durability: ContractDataDurability::Persistent,
1611 val: balance_val,
1612 };
1613
1614 let ledger_entry = LedgerEntry {
1615 last_modified_ledger_seq: 0,
1616 data: LedgerEntryData::ContractData(contract_data),
1617 ext: LedgerEntryExt::V0,
1618 };
1619
1620 let xdr_base64 = ledger_entry
1621 .data
1622 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1623 .unwrap();
1624
1625 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1626 entries: Some(vec![LedgerEntryResult {
1627 key: String::new(),
1628 xdr: xdr_base64,
1629 last_modified_ledger: 0,
1630 live_until_ledger_seq_ledger_seq: None,
1631 }]),
1632 latest_ledger: 0,
1633 })))
1634 });
1635
1636 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1637 let account = TEST_PK;
1638
1639 let result = get_token_balance(&provider, account, contract_addr).await;
1640 assert!(result.is_ok());
1641 assert_eq!(result.unwrap(), 3000000);
1642 }
1643
1644 #[tokio::test]
1645 async fn test_get_token_balance_contract_token_i64_negative() {
1646 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1647 use soroban_rs::xdr::{
1648 ContractDataDurability, ContractDataEntry, ExtensionPoint, LedgerEntry,
1649 LedgerEntryData, LedgerEntryExt, ScVal, WriteXdr,
1650 };
1651
1652 let mut provider = create_mock_provider();
1653
1654 provider.expect_get_ledger_entries().returning(|_| {
1656 let balance_val = ScVal::I64(-1000);
1657
1658 let contract_data = ContractDataEntry {
1659 ext: ExtensionPoint::V0,
1660 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1661 key: ScVal::Vec(None),
1662 durability: ContractDataDurability::Persistent,
1663 val: balance_val,
1664 };
1665
1666 let ledger_entry = LedgerEntry {
1667 last_modified_ledger_seq: 0,
1668 data: LedgerEntryData::ContractData(contract_data),
1669 ext: LedgerEntryExt::V0,
1670 };
1671
1672 let xdr_base64 = ledger_entry
1673 .data
1674 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1675 .unwrap();
1676
1677 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1678 entries: Some(vec![LedgerEntryResult {
1679 key: String::new(),
1680 xdr: xdr_base64,
1681 last_modified_ledger: 0,
1682 live_until_ledger_seq_ledger_seq: None,
1683 }]),
1684 latest_ledger: 0,
1685 })))
1686 });
1687
1688 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1689 let account = TEST_PK;
1690
1691 let result = get_token_balance(&provider, account, contract_addr).await;
1692 assert!(result.is_err());
1693 match result.unwrap_err() {
1694 StellarTransactionUtilsError::NegativeBalanceI64(n) => {
1695 assert_eq!(n, -1000);
1696 }
1697 e => panic!("Expected NegativeBalanceI64, got: {:?}", e),
1698 }
1699 }
1700
1701 #[tokio::test]
1702 async fn test_get_token_balance_contract_token_unexpected_balance_type() {
1703 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1704 use soroban_rs::xdr::{
1705 ContractDataDurability, ContractDataEntry, ExtensionPoint, LedgerEntry,
1706 LedgerEntryData, LedgerEntryExt, ScVal, WriteXdr,
1707 };
1708
1709 let mut provider = create_mock_provider();
1710
1711 provider.expect_get_ledger_entries().returning(|_| {
1713 let balance_val = ScVal::Bool(true);
1714
1715 let contract_data = ContractDataEntry {
1716 ext: ExtensionPoint::V0,
1717 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1718 key: ScVal::Vec(None),
1719 durability: ContractDataDurability::Persistent,
1720 val: balance_val,
1721 };
1722
1723 let ledger_entry = LedgerEntry {
1724 last_modified_ledger_seq: 0,
1725 data: LedgerEntryData::ContractData(contract_data),
1726 ext: LedgerEntryExt::V0,
1727 };
1728
1729 let xdr_base64 = ledger_entry
1730 .data
1731 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1732 .unwrap();
1733
1734 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1735 entries: Some(vec![LedgerEntryResult {
1736 key: String::new(),
1737 xdr: xdr_base64,
1738 last_modified_ledger: 0,
1739 live_until_ledger_seq_ledger_seq: None,
1740 }]),
1741 latest_ledger: 0,
1742 })))
1743 });
1744
1745 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1746 let account = TEST_PK;
1747
1748 let result = get_token_balance(&provider, account, contract_addr).await;
1749 assert!(result.is_err());
1750 match result.unwrap_err() {
1751 StellarTransactionUtilsError::UnexpectedBalanceType(_) => {}
1752 e => panic!("Expected UnexpectedBalanceType, got: {:?}", e),
1753 }
1754 }
1755
1756 #[test]
1757 fn test_asset_code_boundary_cases() {
1758 let (code, _) =
1760 parse_asset_identifier("ABCD:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5")
1761 .unwrap();
1762 assert_eq!(code, "ABCD");
1763 assert!(code.len() <= 4);
1764
1765 let (code, _) = parse_asset_identifier(
1767 "ABCDE:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
1768 )
1769 .unwrap();
1770 assert_eq!(code, "ABCDE");
1771 assert!(code.len() > 4 && code.len() <= 12);
1772
1773 let (code, _) = parse_asset_identifier(
1775 "ABCDEFGHIJKL:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
1776 )
1777 .unwrap();
1778 assert_eq!(code, "ABCDEFGHIJKL");
1779 assert_eq!(code.len(), 12);
1780
1781 let (code, _) = parse_asset_identifier(
1783 "ABCDEFGHIJKLM:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
1784 )
1785 .unwrap();
1786 assert_eq!(code, "ABCDEFGHIJKLM");
1787 assert!(code.len() > MAX_ASSET_CODE_LENGTH);
1788 }
1789
1790 #[tokio::test]
1791 async fn test_get_token_metadata_contract_with_decimals() {
1792 let mut provider = create_mock_provider();
1793
1794 let decimals_value = ScVal::U32(6);
1796 provider
1797 .expect_call_contract()
1798 .returning(move |_, _, _| Box::pin(ready(Ok(decimals_value.clone()))));
1799
1800 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1801 let result = get_token_metadata(&provider, contract_addr).await;
1802 assert!(result.is_ok());
1803 let metadata = result.unwrap();
1804
1805 assert_eq!(metadata.decimals, 6);
1806 match metadata.kind {
1807 StellarTokenKind::Contract { contract_id } => {
1808 assert_eq!(contract_id, contract_addr);
1809 }
1810 _ => panic!("Expected Contract token kind"),
1811 }
1812 }
1813
1814 #[tokio::test]
1815 async fn test_get_contract_token_decimals_from_storage_fallback() {
1816 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1817 use soroban_rs::xdr::{
1818 ContractDataDurability, ContractDataEntry, ExtensionPoint, LedgerEntry,
1819 LedgerEntryData, LedgerEntryExt, ScVal, WriteXdr,
1820 };
1821
1822 let mut provider = create_mock_provider();
1823
1824 provider.expect_call_contract().returning(|_, _, _| {
1826 Box::pin(ready(Err(crate::services::provider::ProviderError::Other(
1827 "Contract call failed".to_string(),
1828 ))))
1829 });
1830
1831 provider.expect_get_ledger_entries().returning(|_| {
1833 let decimals_val = ScVal::U32(8);
1834
1835 let contract_data = ContractDataEntry {
1836 ext: ExtensionPoint::V0,
1837 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1838 key: ScVal::Vec(None),
1839 durability: ContractDataDurability::Persistent,
1840 val: decimals_val,
1841 };
1842
1843 let ledger_entry = LedgerEntry {
1844 last_modified_ledger_seq: 0,
1845 data: LedgerEntryData::ContractData(contract_data),
1846 ext: LedgerEntryExt::V0,
1847 };
1848
1849 let xdr_base64 = ledger_entry
1850 .data
1851 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1852 .unwrap();
1853
1854 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1855 entries: Some(vec![LedgerEntryResult {
1856 key: String::new(),
1857 xdr: xdr_base64,
1858 last_modified_ledger: 0,
1859 live_until_ledger_seq_ledger_seq: None,
1860 }]),
1861 latest_ledger: 0,
1862 })))
1863 });
1864
1865 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1866 let result = get_contract_token_decimals(&provider, contract_addr).await;
1867
1868 assert!(result.is_some());
1869 assert_eq!(result.unwrap(), 8);
1870 }
1871
1872 #[tokio::test]
1873 async fn test_get_contract_token_decimals_both_methods_fail() {
1874 use soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse;
1875
1876 let mut provider = create_mock_provider();
1877
1878 provider.expect_call_contract().returning(|_, _, _| {
1880 Box::pin(ready(Err(crate::services::provider::ProviderError::Other(
1881 "Contract call failed".to_string(),
1882 ))))
1883 });
1884
1885 provider.expect_get_ledger_entries().returning(|_| {
1887 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1888 entries: None,
1889 latest_ledger: 0,
1890 })))
1891 });
1892
1893 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1894 let result = get_contract_token_decimals(&provider, contract_addr).await;
1895
1896 assert!(result.is_none());
1898 }
1899
1900 #[tokio::test]
1901 async fn test_get_contract_token_decimals_invalid_contract_address() {
1902 let provider = create_mock_provider();
1903
1904 let invalid_addr = "INVALID_CONTRACT_ADDRESS";
1905 let result = get_contract_token_decimals(&provider, invalid_addr).await;
1906
1907 assert!(result.is_none());
1909 }
1910
1911 #[tokio::test]
1912 async fn test_invoke_decimals_function_success() {
1913 let mut provider = create_mock_provider();
1914
1915 let decimals_value = ScVal::U32(9);
1916 provider
1917 .expect_call_contract()
1918 .returning(move |_, _, _| Box::pin(ready(Ok(decimals_value.clone()))));
1919
1920 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1921 let result = invoke_decimals_function(&provider, contract_addr).await;
1922
1923 assert!(result.is_some());
1924 assert_eq!(result.unwrap(), 9);
1925 }
1926
1927 #[tokio::test]
1928 async fn test_invoke_decimals_function_failure() {
1929 let mut provider = create_mock_provider();
1930
1931 provider.expect_call_contract().returning(|_, _, _| {
1932 Box::pin(ready(Err(crate::services::provider::ProviderError::Other(
1933 "Contract not found".to_string(),
1934 ))))
1935 });
1936
1937 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1938 let result = invoke_decimals_function(&provider, contract_addr).await;
1939
1940 assert!(result.is_none());
1941 }
1942
1943 #[tokio::test]
1944 async fn test_query_decimals_from_storage_success() {
1945 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1946 use soroban_rs::xdr::{
1947 ContractDataDurability, ContractDataEntry, ExtensionPoint, LedgerEntry,
1948 LedgerEntryData, LedgerEntryExt, ScVal, WriteXdr,
1949 };
1950
1951 let mut provider = create_mock_provider();
1952
1953 provider.expect_get_ledger_entries().returning(|_| {
1954 let decimals_val = ScVal::U32(18);
1955
1956 let contract_data = ContractDataEntry {
1957 ext: ExtensionPoint::V0,
1958 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1959 key: ScVal::Vec(None),
1960 durability: ContractDataDurability::Persistent,
1961 val: decimals_val,
1962 };
1963
1964 let ledger_entry = LedgerEntry {
1965 last_modified_ledger_seq: 0,
1966 data: LedgerEntryData::ContractData(contract_data),
1967 ext: LedgerEntryExt::V0,
1968 };
1969
1970 let xdr_base64 = ledger_entry
1971 .data
1972 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1973 .unwrap();
1974
1975 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1976 entries: Some(vec![LedgerEntryResult {
1977 key: String::new(),
1978 xdr: xdr_base64,
1979 last_modified_ledger: 0,
1980 live_until_ledger_seq_ledger_seq: None,
1981 }]),
1982 latest_ledger: 0,
1983 })))
1984 });
1985
1986 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
1987 let contract_hash = parse_contract_address(contract_addr).unwrap();
1988 let result = query_decimals_from_storage(&provider, contract_addr, contract_hash).await;
1989
1990 assert!(result.is_some());
1991 assert_eq!(result.unwrap(), 18);
1992 }
1993
1994 #[tokio::test]
1995 async fn test_query_decimals_from_storage_no_entry() {
1996 use soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse;
1997
1998 let mut provider = create_mock_provider();
1999
2000 provider.expect_get_ledger_entries().returning(|_| {
2001 Box::pin(ready(Ok(GetLedgerEntriesResponse {
2002 entries: None,
2003 latest_ledger: 0,
2004 })))
2005 });
2006
2007 let contract_addr = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
2008 let contract_hash = parse_contract_address(contract_addr).unwrap();
2009 let result = query_decimals_from_storage(&provider, contract_addr, contract_hash).await;
2010
2011 assert!(result.is_none());
2012 }
2013
2014 #[test]
2015 fn test_stellar_address_validation() {
2016 let valid_account = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
2018 assert_eq!(valid_account.len(), STELLAR_ADDRESS_LENGTH);
2019 assert!(valid_account.starts_with(STELLAR_ACCOUNT_PREFIX));
2020
2021 let valid_contract = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
2023 assert_eq!(valid_contract.len(), STELLAR_ADDRESS_LENGTH);
2024 assert!(!valid_contract.starts_with(STELLAR_ACCOUNT_PREFIX));
2025
2026 let short = "GBBD47IF6LWK7P7";
2028 assert!(short.len() != STELLAR_ADDRESS_LENGTH);
2029
2030 let muxed = "MAAAAAAAAAAABBBBBBBBBBBBCCCCCCCCCCCCDDDDDDDDDDDDEEEEEEEE";
2032 assert!(!muxed.starts_with(STELLAR_ACCOUNT_PREFIX));
2033 }
2034
2035 #[tokio::test]
2036 async fn test_get_token_balance_different_identifiers() {
2037 let mut provider = create_mock_provider();
2038
2039 let test_balance = 75_0000000i64; let account_entry = create_mock_account_entry(test_balance);
2041
2042 provider
2044 .expect_get_account()
2045 .times(2)
2046 .returning(move |_| Box::pin(ready(Ok(account_entry.clone()))));
2047
2048 let account = TEST_PK;
2049
2050 let result1 = get_token_balance(&provider, account, "native").await;
2052 assert!(result1.is_ok());
2053 assert_eq!(result1.unwrap(), test_balance as u64);
2054
2055 let result2 = get_token_balance(&provider, account, "XLM").await;
2057 assert!(result2.is_ok());
2058 assert_eq!(result2.unwrap(), test_balance as u64);
2059 }
2060
2061 #[test]
2062 fn test_parse_asset_identifier_colon_in_issuer() {
2063 let result = parse_asset_identifier(
2066 "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5:EXTRA",
2067 );
2068 assert!(result.is_ok());
2069 let (code, issuer) = result.unwrap();
2070 assert_eq!(code, "USDC");
2071 assert_eq!(
2072 issuer,
2073 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5:EXTRA"
2074 );
2075 }
2076
2077 #[tokio::test]
2078 async fn test_get_token_metadata_case_sensitivity() {
2079 let provider = create_mock_provider();
2080
2081 let asset_id = "usdc:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
2083 let result = get_token_metadata(&provider, asset_id).await;
2084 assert!(result.is_ok());
2085 let metadata = result.unwrap();
2086
2087 match metadata.kind {
2088 StellarTokenKind::Classic { code, .. } => {
2089 assert_eq!(code, "usdc"); }
2091 _ => panic!("Expected Classic token kind"),
2092 }
2093 }
2094
2095 #[test]
2096 fn test_max_asset_code_length() {
2097 assert_eq!(MAX_ASSET_CODE_LENGTH, 12);
2099
2100 for len in 1..=4 {
2102 let code = "A".repeat(len);
2103 assert!(code.len() <= 4);
2104 }
2105
2106 for len in 5..=12 {
2108 let code = "A".repeat(len);
2109 assert!(code.len() > 4 && code.len() <= MAX_ASSET_CODE_LENGTH);
2110 }
2111
2112 let too_long = "A".repeat(13);
2114 assert!(too_long.len() > MAX_ASSET_CODE_LENGTH);
2115 }
2116}