1use crate::constants::{
3 DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE, STELLAR_DEFAULT_TRANSACTION_FEE, STELLAR_MAX_OPERATIONS,
4};
5use crate::domain::relayer::xdr_utils::{extract_operations, xdr_needs_simulation};
6use crate::models::{AssetSpec, OperationSpec, RelayerError, RelayerStellarPolicy};
7use crate::services::provider::StellarProviderTrait;
8use crate::services::stellar_dex::StellarDexServiceTrait;
9use base64::{engine::general_purpose, Engine};
10use chrono::{DateTime, Utc};
11use serde::Serialize;
12use soroban_rs::xdr::{
13 AccountId, AlphaNum12, AlphaNum4, Asset, ChangeTrustAsset, ContractDataEntry, ContractId, Hash,
14 LedgerEntryData, LedgerKey, LedgerKeyContractData, Limits, Operation, Preconditions,
15 PublicKey as XdrPublicKey, ReadXdr, ScAddress, ScSymbol, ScVal, TimeBounds, TimePoint,
16 TransactionEnvelope, TransactionMeta, TransactionResult, Uint256, VecM,
17};
18use std::str::FromStr;
19use stellar_strkey::ed25519::PublicKey;
20use thiserror::Error;
21use tracing::{debug, warn};
22
23#[derive(Error, Debug, Serialize)]
33pub enum StellarTransactionUtilsError {
34 #[error("Sequence overflow: {0}")]
35 SequenceOverflow(String),
36
37 #[error("Failed to parse XDR: {0}")]
38 XdrParseFailed(String),
39
40 #[error("Failed to extract operations: {0}")]
41 OperationExtractionFailed(String),
42
43 #[error("Failed to check if simulation is needed: {0}")]
44 SimulationCheckFailed(String),
45
46 #[error("Failed to simulate transaction: {0}")]
47 SimulationFailed(String),
48
49 #[error("Transaction simulation returned no results")]
50 SimulationNoResults,
51
52 #[error("Failed to get DEX quote: {0}")]
53 DexQuoteFailed(String),
54
55 #[error("Invalid asset identifier format: {0}")]
56 InvalidAssetFormat(String),
57
58 #[error("Asset code too long (max {0} characters): {1}")]
59 AssetCodeTooLong(usize, String),
60
61 #[error("Too many operations (max {0})")]
62 TooManyOperations(usize),
63
64 #[error("Cannot add operations to fee-bump transactions")]
65 CannotModifyFeeBump,
66
67 #[error("Cannot set time bounds on fee-bump transactions")]
68 CannotSetTimeBoundsOnFeeBump,
69
70 #[error("V0 transactions are not supported")]
71 V0TransactionsNotSupported,
72
73 #[error("Cannot update sequence number on fee bump transaction")]
74 CannotUpdateSequenceOnFeeBump,
75
76 #[error("Invalid transaction format: {0}")]
77 InvalidTransactionFormat(String),
78
79 #[error("Invalid account address '{0}': {1}")]
80 InvalidAccountAddress(String, String),
81
82 #[error("Invalid contract address '{0}': {1}")]
83 InvalidContractAddress(String, String),
84
85 #[error("Failed to create {0} symbol: {1:?}")]
86 SymbolCreationFailed(String, String),
87
88 #[error("Failed to create {0} key vector: {1:?}")]
89 KeyVectorCreationFailed(String, String),
90
91 #[error("Failed to query contract data (Persistent) for {0}: {1}")]
92 ContractDataQueryPersistentFailed(String, String),
93
94 #[error("Failed to query contract data (Temporary) for {0}: {1}")]
95 ContractDataQueryTemporaryFailed(String, String),
96
97 #[error("Failed to parse ledger entry XDR for {0}: {1}")]
98 LedgerEntryParseFailed(String, String),
99
100 #[error("No entries found for {0}")]
101 NoEntriesFound(String),
102
103 #[error("Empty entries for {0}")]
104 EmptyEntries(String),
105
106 #[error("Unexpected ledger entry type for {0} (expected ContractData)")]
107 UnexpectedLedgerEntryType(String),
108
109 #[error("Asset code cannot be empty in asset identifier: {0}")]
111 EmptyAssetCode(String),
112
113 #[error("Issuer address cannot be empty in asset identifier: {0}")]
114 EmptyIssuerAddress(String),
115
116 #[error("Invalid issuer address length (expected {0} characters): {1}")]
117 InvalidIssuerLength(usize, String),
118
119 #[error("Invalid issuer address format (must start with '{0}'): {1}")]
120 InvalidIssuerPrefix(char, String),
121
122 #[error("Failed to fetch account for balance: {0}")]
123 AccountFetchFailed(String),
124
125 #[error("Failed to query trustline for asset {0}: {1}")]
126 TrustlineQueryFailed(String, String),
127
128 #[error("No trustline found for asset {0} on account {1}")]
129 NoTrustlineFound(String, String),
130
131 #[error("Unsupported trustline entry version")]
132 UnsupportedTrustlineVersion,
133
134 #[error("Unexpected ledger entry type for trustline query")]
135 UnexpectedTrustlineEntryType,
136
137 #[error("Balance too large (i128 hi={0}, lo={1}) to fit in u64")]
138 BalanceTooLarge(i64, u64),
139
140 #[error("Negative balance not allowed: i128 lo={0}")]
141 NegativeBalanceI128(u64),
142
143 #[error("Negative balance not allowed: i64={0}")]
144 NegativeBalanceI64(i64),
145
146 #[error("Unexpected balance value type in contract data: {0:?}. Expected I128, U64, or I64")]
147 UnexpectedBalanceType(String),
148
149 #[error("Unexpected ledger entry type for contract data query")]
150 UnexpectedContractDataEntryType,
151
152 #[error("Native asset should be handled before trustline query")]
153 NativeAssetInTrustlineQuery,
154
155 #[error("Failed to invoke contract function '{0}': {1}")]
156 ContractInvocationFailed(String, String),
157}
158
159impl From<StellarTransactionUtilsError> for RelayerError {
160 fn from(error: StellarTransactionUtilsError) -> Self {
161 match &error {
162 StellarTransactionUtilsError::SequenceOverflow(msg)
163 | StellarTransactionUtilsError::SimulationCheckFailed(msg)
164 | StellarTransactionUtilsError::SimulationFailed(msg)
165 | StellarTransactionUtilsError::XdrParseFailed(msg)
166 | StellarTransactionUtilsError::OperationExtractionFailed(msg)
167 | StellarTransactionUtilsError::DexQuoteFailed(msg) => {
168 RelayerError::Internal(msg.clone())
169 }
170 StellarTransactionUtilsError::SimulationNoResults => RelayerError::Internal(
171 "Transaction simulation failed: no results returned".to_string(),
172 ),
173 StellarTransactionUtilsError::InvalidAssetFormat(msg)
174 | StellarTransactionUtilsError::InvalidTransactionFormat(msg) => {
175 RelayerError::ValidationError(msg.clone())
176 }
177 StellarTransactionUtilsError::AssetCodeTooLong(max_len, code) => {
178 RelayerError::ValidationError(format!(
179 "Asset code too long (max {max_len} characters): {code}"
180 ))
181 }
182 StellarTransactionUtilsError::TooManyOperations(max) => {
183 RelayerError::ValidationError(format!("Too many operations (max {max})"))
184 }
185 StellarTransactionUtilsError::CannotModifyFeeBump => RelayerError::ValidationError(
186 "Cannot add operations to fee-bump transactions".to_string(),
187 ),
188 StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump => {
189 RelayerError::ValidationError(
190 "Cannot set time bounds on fee-bump transactions".to_string(),
191 )
192 }
193 StellarTransactionUtilsError::V0TransactionsNotSupported => {
194 RelayerError::ValidationError("V0 transactions are not supported".to_string())
195 }
196 StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump => {
197 RelayerError::ValidationError(
198 "Cannot update sequence number on fee bump transaction".to_string(),
199 )
200 }
201 StellarTransactionUtilsError::InvalidAccountAddress(_, msg)
202 | StellarTransactionUtilsError::InvalidContractAddress(_, msg)
203 | StellarTransactionUtilsError::SymbolCreationFailed(_, msg)
204 | StellarTransactionUtilsError::KeyVectorCreationFailed(_, msg)
205 | StellarTransactionUtilsError::ContractDataQueryPersistentFailed(_, msg)
206 | StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(_, msg)
207 | StellarTransactionUtilsError::LedgerEntryParseFailed(_, msg) => {
208 RelayerError::Internal(msg.clone())
209 }
210 StellarTransactionUtilsError::NoEntriesFound(_)
211 | StellarTransactionUtilsError::EmptyEntries(_)
212 | StellarTransactionUtilsError::UnexpectedLedgerEntryType(_)
213 | StellarTransactionUtilsError::EmptyAssetCode(_)
214 | StellarTransactionUtilsError::EmptyIssuerAddress(_)
215 | StellarTransactionUtilsError::NoTrustlineFound(_, _)
216 | StellarTransactionUtilsError::UnsupportedTrustlineVersion
217 | StellarTransactionUtilsError::UnexpectedTrustlineEntryType
218 | StellarTransactionUtilsError::BalanceTooLarge(_, _)
219 | StellarTransactionUtilsError::NegativeBalanceI128(_)
220 | StellarTransactionUtilsError::NegativeBalanceI64(_)
221 | StellarTransactionUtilsError::UnexpectedBalanceType(_)
222 | StellarTransactionUtilsError::UnexpectedContractDataEntryType
223 | StellarTransactionUtilsError::NativeAssetInTrustlineQuery => {
224 RelayerError::ValidationError(error.to_string())
225 }
226 StellarTransactionUtilsError::InvalidIssuerLength(expected, actual) => {
227 RelayerError::ValidationError(format!(
228 "Invalid issuer address length (expected {expected} characters): {actual}"
229 ))
230 }
231 StellarTransactionUtilsError::InvalidIssuerPrefix(prefix, addr) => {
232 RelayerError::ValidationError(format!(
233 "Invalid issuer address format (must start with '{prefix}'): {addr}"
234 ))
235 }
236 StellarTransactionUtilsError::AccountFetchFailed(msg)
237 | StellarTransactionUtilsError::TrustlineQueryFailed(_, msg)
238 | StellarTransactionUtilsError::ContractInvocationFailed(_, msg) => {
239 RelayerError::ProviderError(msg.clone())
240 }
241 }
242 }
243}
244
245pub fn needs_simulation(operations: &[OperationSpec]) -> bool {
247 operations.iter().any(|op| {
248 matches!(
249 op,
250 OperationSpec::InvokeContract { .. }
251 | OperationSpec::CreateContract { .. }
252 | OperationSpec::UploadWasm { .. }
253 )
254 })
255}
256
257pub fn next_sequence_u64(seq_num: i64) -> Result<u64, RelayerError> {
258 let next_i64 = seq_num
259 .checked_add(1)
260 .ok_or_else(|| RelayerError::ProviderError("sequence overflow".into()))?;
261 u64::try_from(next_i64)
262 .map_err(|_| RelayerError::ProviderError("sequence overflows u64".into()))
263}
264
265pub fn i64_from_u64(value: u64) -> Result<i64, RelayerError> {
266 i64::try_from(value).map_err(|_| RelayerError::ProviderError("u64→i64 overflow".into()))
267}
268
269pub fn decode_tx_result_code(error_result_xdr: &str) -> Option<String> {
274 TransactionResult::from_xdr_base64(error_result_xdr, Limits::none())
275 .ok()
276 .map(|r| r.result.name().to_string())
277}
278
279pub fn is_bad_sequence_error(error_msg: &str) -> bool {
282 let error_lower = error_msg.to_lowercase();
283 error_lower.contains("txbadseq")
284}
285
286pub fn decode_transaction_result_code(xdr_base64: &str) -> Option<String> {
291 use soroban_rs::xdr::{Limits, TransactionResult};
292
293 let result = TransactionResult::from_xdr_base64(xdr_base64, Limits::none()).ok()?;
294 Some(result.result.name().to_string())
295}
296
297pub fn is_insufficient_fee_error(result_code: &str) -> bool {
299 result_code.eq_ignore_ascii_case("TxInsufficientFee")
300 || result_code.eq_ignore_ascii_case("tx_insufficient_fee")
301}
302
303pub async fn fetch_next_sequence_from_chain<P>(
309 provider: &P,
310 relayer_address: &str,
311) -> Result<u64, String>
312where
313 P: StellarProviderTrait,
314{
315 debug!(
316 "Fetching sequence from chain for address: {}",
317 relayer_address
318 );
319
320 let account = provider.get_account(relayer_address).await.map_err(|e| {
322 warn!(
323 address = %relayer_address,
324 error = %e,
325 "get_account failed in fetch_next_sequence_from_chain"
326 );
327 format!("Failed to fetch account from chain: {e}")
328 })?;
329
330 let on_chain_seq = account.seq_num.0; let next_usable = next_sequence_u64(on_chain_seq)
332 .map_err(|e| format!("Failed to calculate next sequence: {e}"))?;
333
334 debug!(
335 "Fetched sequence from chain: on-chain={}, next usable={}",
336 on_chain_seq, next_usable
337 );
338 Ok(next_usable)
339}
340
341pub fn convert_v0_to_v1_transaction(
344 v0_tx: &soroban_rs::xdr::TransactionV0,
345) -> soroban_rs::xdr::Transaction {
346 soroban_rs::xdr::Transaction {
347 source_account: soroban_rs::xdr::MuxedAccount::Ed25519(
348 v0_tx.source_account_ed25519.clone(),
349 ),
350 fee: v0_tx.fee,
351 seq_num: v0_tx.seq_num.clone(),
352 cond: match v0_tx.time_bounds.clone() {
353 Some(tb) => soroban_rs::xdr::Preconditions::Time(tb),
354 None => soroban_rs::xdr::Preconditions::None,
355 },
356 memo: v0_tx.memo.clone(),
357 operations: v0_tx.operations.clone(),
358 ext: soroban_rs::xdr::TransactionExt::V0,
359 }
360}
361
362pub fn create_signature_payload(
364 envelope: &soroban_rs::xdr::TransactionEnvelope,
365 network_id: &soroban_rs::xdr::Hash,
366) -> Result<soroban_rs::xdr::TransactionSignaturePayload, RelayerError> {
367 let tagged_transaction = match envelope {
368 soroban_rs::xdr::TransactionEnvelope::TxV0(e) => {
369 let v1_tx = convert_v0_to_v1_transaction(&e.tx);
371 soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::Tx(v1_tx)
372 }
373 soroban_rs::xdr::TransactionEnvelope::Tx(e) => {
374 soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::Tx(e.tx.clone())
375 }
376 soroban_rs::xdr::TransactionEnvelope::TxFeeBump(e) => {
377 soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::TxFeeBump(e.tx.clone())
378 }
379 };
380
381 Ok(soroban_rs::xdr::TransactionSignaturePayload {
382 network_id: network_id.clone(),
383 tagged_transaction,
384 })
385}
386
387pub fn create_transaction_signature_payload(
389 transaction: &soroban_rs::xdr::Transaction,
390 network_id: &soroban_rs::xdr::Hash,
391) -> soroban_rs::xdr::TransactionSignaturePayload {
392 soroban_rs::xdr::TransactionSignaturePayload {
393 network_id: network_id.clone(),
394 tagged_transaction: soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::Tx(
395 transaction.clone(),
396 ),
397 }
398}
399
400pub fn update_envelope_sequence(
404 envelope: &mut TransactionEnvelope,
405 sequence: i64,
406) -> Result<(), StellarTransactionUtilsError> {
407 match envelope {
408 TransactionEnvelope::Tx(v1) => {
409 v1.tx.seq_num = soroban_rs::xdr::SequenceNumber(sequence);
410 Ok(())
411 }
412 TransactionEnvelope::TxV0(_) => {
413 Err(StellarTransactionUtilsError::V0TransactionsNotSupported)
414 }
415 TransactionEnvelope::TxFeeBump(_) => {
416 Err(StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump)
417 }
418 }
419}
420
421pub fn envelope_fee_in_stroops(
423 envelope: &TransactionEnvelope,
424) -> Result<u64, StellarTransactionUtilsError> {
425 match envelope {
426 TransactionEnvelope::Tx(env) => Ok(u64::from(env.tx.fee)),
427 _ => Err(StellarTransactionUtilsError::InvalidTransactionFormat(
428 "Expected V1 transaction envelope".to_string(),
429 )),
430 }
431}
432
433pub fn parse_account_id(account_id: &str) -> Result<AccountId, StellarTransactionUtilsError> {
447 let account_pk = PublicKey::from_str(account_id).map_err(|e| {
448 StellarTransactionUtilsError::InvalidAccountAddress(account_id.to_string(), e.to_string())
449 })?;
450 let account_uint256 = Uint256(account_pk.0);
451 let account_xdr_pk = XdrPublicKey::PublicKeyTypeEd25519(account_uint256);
452 Ok(AccountId(account_xdr_pk))
453}
454
455pub fn parse_contract_address(
465 contract_address: &str,
466) -> Result<Hash, StellarTransactionUtilsError> {
467 let contract_id = ContractId::from_str(contract_address).map_err(|e| {
468 StellarTransactionUtilsError::InvalidContractAddress(
469 contract_address.to_string(),
470 e.to_string(),
471 )
472 })?;
473 Ok(contract_id.0)
474}
475
476pub fn create_contract_data_key(
494 symbol: &str,
495 address: Option<ScAddress>,
496) -> Result<ScVal, StellarTransactionUtilsError> {
497 if address.is_none() {
498 let sym = ScSymbol::try_from(symbol).map_err(|e| {
499 StellarTransactionUtilsError::SymbolCreationFailed(symbol.to_string(), format!("{e:?}"))
500 })?;
501 return Ok(ScVal::Symbol(sym));
502 }
503
504 let mut key_items: Vec<ScVal> =
505 vec![ScVal::Symbol(ScSymbol::try_from(symbol).map_err(|e| {
506 StellarTransactionUtilsError::SymbolCreationFailed(symbol.to_string(), format!("{e:?}"))
507 })?)];
508
509 if let Some(addr) = address {
510 key_items.push(ScVal::Address(addr));
511 }
512
513 let key_vec: VecM<ScVal, { u32::MAX }> = VecM::try_from(key_items).map_err(|e| {
514 StellarTransactionUtilsError::KeyVectorCreationFailed(symbol.to_string(), format!("{e:?}"))
515 })?;
516
517 Ok(ScVal::Vec(Some(soroban_rs::xdr::ScVec(key_vec))))
518}
519
520pub async fn query_contract_data_with_fallback<P>(
537 provider: &P,
538 contract_hash: Hash,
539 key: ScVal,
540 error_context: &str,
541) -> Result<soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse, StellarTransactionUtilsError>
542where
543 P: StellarProviderTrait + Send + Sync,
544{
545 let contract_address_sc =
546 soroban_rs::xdr::ScAddress::Contract(soroban_rs::xdr::ContractId(contract_hash));
547
548 let mut ledger_key = LedgerKey::ContractData(LedgerKeyContractData {
549 contract: contract_address_sc.clone(),
550 key: key.clone(),
551 durability: soroban_rs::xdr::ContractDataDurability::Persistent,
552 });
553
554 let mut ledger_entries = provider
556 .get_ledger_entries(&[ledger_key.clone()])
557 .await
558 .map_err(|e| {
559 StellarTransactionUtilsError::ContractDataQueryPersistentFailed(
560 error_context.to_string(),
561 e.to_string(),
562 )
563 })?;
564
565 if ledger_entries
567 .entries
568 .as_ref()
569 .map(|e| e.is_empty())
570 .unwrap_or(true)
571 {
572 ledger_key = LedgerKey::ContractData(LedgerKeyContractData {
573 contract: contract_address_sc,
574 key,
575 durability: soroban_rs::xdr::ContractDataDurability::Temporary,
576 });
577 ledger_entries = provider
578 .get_ledger_entries(&[ledger_key])
579 .await
580 .map_err(|e| {
581 StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(
582 error_context.to_string(),
583 e.to_string(),
584 )
585 })?;
586 }
587
588 Ok(ledger_entries)
589}
590
591pub fn parse_ledger_entry_from_xdr(
605 xdr_string: &str,
606 context: &str,
607) -> Result<LedgerEntryData, StellarTransactionUtilsError> {
608 let trimmed_xdr = xdr_string.trim();
609
610 if general_purpose::STANDARD.decode(trimmed_xdr).is_err() {
612 return Err(StellarTransactionUtilsError::LedgerEntryParseFailed(
613 context.to_string(),
614 "Invalid base64".to_string(),
615 ));
616 }
617
618 match LedgerEntryData::from_xdr_base64(trimmed_xdr, Limits::none()) {
620 Ok(data) => Ok(data),
621 Err(e) => Err(StellarTransactionUtilsError::LedgerEntryParseFailed(
622 context.to_string(),
623 format!("Failed to parse LedgerEntryData: {e}"),
624 )),
625 }
626}
627
628pub fn extract_scval_from_contract_data(
642 ledger_entries: &soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse,
643 context: &str,
644) -> Result<ScVal, StellarTransactionUtilsError> {
645 let entries = ledger_entries
646 .entries
647 .as_ref()
648 .ok_or_else(|| StellarTransactionUtilsError::NoEntriesFound(context.into()))?;
649
650 if entries.is_empty() {
651 return Err(StellarTransactionUtilsError::EmptyEntries(context.into()));
652 }
653
654 let entry_xdr = &entries[0].xdr;
655 let entry = parse_ledger_entry_from_xdr(entry_xdr, context)?;
656
657 match entry {
658 LedgerEntryData::ContractData(ContractDataEntry { val, .. }) => Ok(val.clone()),
659
660 _ => Err(StellarTransactionUtilsError::UnexpectedLedgerEntryType(
661 context.into(),
662 )),
663 }
664}
665
666pub fn extract_return_value_from_meta(result_meta: &TransactionMeta) -> Option<&ScVal> {
680 match result_meta {
681 TransactionMeta::V3(meta_v3) => meta_v3.soroban_meta.as_ref().map(|m| &m.return_value),
682 TransactionMeta::V4(meta_v4) => meta_v4
683 .soroban_meta
684 .as_ref()
685 .and_then(|m| m.return_value.as_ref()),
686 _ => None,
687 }
688}
689
690pub fn extract_u32_from_scval(val: &ScVal, context: &str) -> Option<u32> {
703 let result = match val {
704 ScVal::U32(n) => Ok(*n),
705 ScVal::I32(n) => (*n).try_into().map_err(|_| "Negative I32"),
706 ScVal::U64(n) => (*n).try_into().map_err(|_| "U64 overflow"),
707 ScVal::I64(n) => (*n).try_into().map_err(|_| "I64 overflow/negative"),
708 ScVal::U128(n) => {
709 if n.hi == 0 {
710 n.lo.try_into().map_err(|_| "U128 lo overflow")
711 } else {
712 Err("U128 hi set")
713 }
714 }
715 ScVal::I128(n) => {
716 if n.hi == 0 {
717 n.lo.try_into().map_err(|_| "I128 lo overflow")
718 } else {
719 Err("I128 hi set/negative")
720 }
721 }
722 _ => Err("Unsupported ScVal type"),
723 };
724
725 match result {
726 Ok(v) => Some(v),
727 Err(msg) => {
728 warn!(context = %context, val = ?val, "Failed to extract u32: {}", msg);
729 None
730 }
731 }
732}
733
734pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> String {
743 if decimals == 0 {
744 return amount.to_string();
745 }
746
747 let amount_str = amount.to_string();
748 let len = amount_str.len();
749 let decimals_usize = decimals as usize;
750
751 let combined = if len > decimals_usize {
752 let split_idx = len - decimals_usize;
753 let whole = &amount_str[..split_idx];
754 let frac = &amount_str[split_idx..];
755 format!("{whole}.{frac}")
756 } else {
757 let zeros = "0".repeat(decimals_usize - len);
759 format!("0.{zeros}{amount_str}")
760 };
761
762 let mut trimmed = combined.trim_end_matches('0').to_string();
764 if trimmed.ends_with('.') {
765 trimmed.pop();
766 }
767
768 if trimmed.is_empty() {
770 "0".to_string()
771 } else {
772 trimmed
773 }
774}
775
776pub fn count_operations_from_xdr(xdr: &str) -> Result<usize, StellarTransactionUtilsError> {
780 let envelope = TransactionEnvelope::from_xdr_base64(xdr, Limits::none()).map_err(|e| {
781 StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
782 })?;
783
784 let operations = extract_operations(&envelope).map_err(|e| {
785 StellarTransactionUtilsError::OperationExtractionFailed(format!(
786 "Failed to extract operations: {e}"
787 ))
788 })?;
789
790 Ok(operations.len())
791}
792
793pub fn parse_transaction_and_count_operations(
797 transaction_json: &serde_json::Value,
798) -> Result<usize, StellarTransactionUtilsError> {
799 if let Some(xdr_str) = transaction_json.as_str() {
801 let envelope =
802 TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
803 StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
804 })?;
805
806 let operations = extract_operations(&envelope).map_err(|e| {
807 StellarTransactionUtilsError::OperationExtractionFailed(format!(
808 "Failed to extract operations: {e}"
809 ))
810 })?;
811
812 return Ok(operations.len());
813 }
814
815 if let Some(ops_array) = transaction_json.as_array() {
817 return Ok(ops_array.len());
818 }
819
820 if let Some(obj) = transaction_json.as_object() {
822 if let Some(ops) = obj.get("operations") {
823 if let Some(ops_array) = ops.as_array() {
824 return Ok(ops_array.len());
825 }
826 }
827 if let Some(xdr_str) = obj.get("transaction_xdr").and_then(|v| v.as_str()) {
828 let envelope =
829 TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
830 StellarTransactionUtilsError::XdrParseFailed(format!(
831 "Failed to parse XDR: {e}"
832 ))
833 })?;
834
835 let operations = extract_operations(&envelope).map_err(|e| {
836 StellarTransactionUtilsError::OperationExtractionFailed(format!(
837 "Failed to extract operations: {e}"
838 ))
839 })?;
840
841 return Ok(operations.len());
842 }
843 }
844
845 Err(StellarTransactionUtilsError::InvalidTransactionFormat(
846 "Transaction must be either XDR string or operations array".to_string(),
847 ))
848}
849
850#[derive(Debug)]
852pub struct FeeQuote {
853 pub fee_in_token: u64,
854 pub fee_in_token_ui: String,
855 pub fee_in_stroops: u64,
856 pub conversion_rate: f64,
857}
858
859pub fn estimate_base_fee(num_operations: usize) -> u64 {
863 (num_operations.max(1) as u64) * STELLAR_DEFAULT_TRANSACTION_FEE as u64
864}
865
866pub async fn estimate_fee<P>(
881 envelope: &TransactionEnvelope,
882 provider: &P,
883 operations_override: Option<usize>,
884) -> Result<u64, StellarTransactionUtilsError>
885where
886 P: StellarProviderTrait + Send + Sync,
887{
888 let needs_sim = xdr_needs_simulation(envelope).map_err(|e| {
890 StellarTransactionUtilsError::SimulationCheckFailed(format!(
891 "Failed to check if simulation is needed: {e}"
892 ))
893 })?;
894
895 if needs_sim {
896 debug!("Transaction contains Soroban operations, simulating to get accurate fee");
897
898 let simulation_result = provider
900 .simulate_transaction_envelope(envelope)
901 .await
902 .map_err(|e| {
903 StellarTransactionUtilsError::SimulationFailed(format!(
904 "Failed to simulate transaction: {e}"
905 ))
906 })?;
907
908 if simulation_result.results.is_empty() {
910 return Err(StellarTransactionUtilsError::SimulationNoResults);
911 }
912
913 let resource_fee = simulation_result.min_resource_fee as u64;
916 let inclusion_fee = STELLAR_DEFAULT_TRANSACTION_FEE as u64;
917 let required_fee = inclusion_fee + resource_fee;
918
919 debug!("Simulation returned fee: {} stroops", required_fee);
920 Ok(required_fee)
921 } else {
922 let num_operations = if let Some(override_count) = operations_override {
924 override_count
925 } else {
926 let operations = extract_operations(envelope).map_err(|e| {
927 StellarTransactionUtilsError::OperationExtractionFailed(format!(
928 "Failed to extract operations: {e}"
929 ))
930 })?;
931 operations.len()
932 };
933
934 let fee = estimate_base_fee(num_operations);
935 debug!(
936 "No simulation needed, estimated fee from {} operations: {} stroops",
937 num_operations, fee
938 );
939 Ok(fee)
940 }
941}
942
943pub async fn convert_xlm_fee_to_token<D>(
960 dex_service: &D,
961 policy: &RelayerStellarPolicy,
962 xlm_fee: u64,
963 fee_token: &str,
964) -> Result<FeeQuote, StellarTransactionUtilsError>
965where
966 D: StellarDexServiceTrait + Send + Sync,
967{
968 if fee_token == "native" || fee_token.is_empty() {
970 debug!("Converting XLM fee to native XLM: {}", xlm_fee);
971 let buffered_fee = if let Some(margin) = policy.fee_margin_percentage {
972 (xlm_fee as f64 * (1.0 + margin as f64 / 100.0)) as u64
973 } else {
974 xlm_fee
975 };
976
977 return Ok(FeeQuote {
978 fee_in_token: buffered_fee,
979 fee_in_token_ui: amount_to_ui_amount(buffered_fee, 7),
980 fee_in_stroops: buffered_fee,
981 conversion_rate: 1.0,
982 });
983 }
984
985 debug!("Converting XLM fee to token: {}", fee_token);
986
987 let buffered_xlm_fee = if let Some(margin) = policy.fee_margin_percentage {
989 (xlm_fee as f64 * (1.0 + margin as f64 / 100.0)) as u64
990 } else {
991 xlm_fee
992 };
993
994 let slippage = policy
996 .get_allowed_token_entry(fee_token)
997 .and_then(|token| {
998 token
999 .swap_config
1000 .as_ref()
1001 .and_then(|config| config.slippage_percentage)
1002 })
1003 .or(policy.slippage_percentage)
1004 .unwrap_or(DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE);
1005
1006 let token_decimals = policy.get_allowed_token_decimals(fee_token);
1009 let quote = dex_service
1010 .get_xlm_to_token_quote(fee_token, buffered_xlm_fee, slippage, token_decimals)
1011 .await
1012 .map_err(|e| {
1013 StellarTransactionUtilsError::DexQuoteFailed(format!("Failed to get quote: {e}"))
1014 })?;
1015
1016 debug!(
1017 "Quote from DEX: input={} stroops XLM, output={} stroops token, input_asset={}, output_asset={}",
1018 quote.in_amount, quote.out_amount, quote.input_asset, quote.output_asset
1019 );
1020
1021 let conversion_rate = if buffered_xlm_fee > 0 {
1023 quote.out_amount as f64 / buffered_xlm_fee as f64
1024 } else {
1025 0.0
1026 };
1027
1028 let fee_quote = FeeQuote {
1029 fee_in_token: quote.out_amount,
1030 fee_in_token_ui: amount_to_ui_amount(quote.out_amount, token_decimals.unwrap_or(7)),
1031 fee_in_stroops: buffered_xlm_fee,
1032 conversion_rate,
1033 };
1034
1035 debug!(
1036 "Final fee quote: fee_in_token={} stroops ({} {}), fee_in_stroops={} stroops XLM, conversion_rate={}",
1037 fee_quote.fee_in_token, fee_quote.fee_in_token_ui, fee_token, fee_quote.fee_in_stroops, fee_quote.conversion_rate
1038 );
1039
1040 Ok(fee_quote)
1041}
1042
1043pub fn parse_transaction_envelope(
1045 transaction_json: &serde_json::Value,
1046) -> Result<TransactionEnvelope, StellarTransactionUtilsError> {
1047 if let Some(xdr_str) = transaction_json.as_str() {
1049 return TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
1050 StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
1051 });
1052 }
1053
1054 if let Some(obj) = transaction_json.as_object() {
1056 if let Some(xdr_str) = obj.get("transaction_xdr").and_then(|v| v.as_str()) {
1057 return TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
1058 StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
1059 });
1060 }
1061 }
1062
1063 Err(StellarTransactionUtilsError::InvalidTransactionFormat(
1064 "Transaction must be XDR string or object with transaction_xdr field".to_string(),
1065 ))
1066}
1067
1068pub fn create_fee_payment_operation(
1070 destination: &str,
1071 asset_id: &str,
1072 amount: i64,
1073) -> Result<OperationSpec, StellarTransactionUtilsError> {
1074 let asset = if asset_id == "native" || asset_id.is_empty() {
1076 AssetSpec::Native
1077 } else {
1078 if let Some(colon_pos) = asset_id.find(':') {
1080 let code = asset_id[..colon_pos].to_string();
1081 let issuer = asset_id[colon_pos + 1..].to_string();
1082
1083 if code.len() <= 4 {
1085 AssetSpec::Credit4 { code, issuer }
1086 } else if code.len() <= 12 {
1087 AssetSpec::Credit12 { code, issuer }
1088 } else {
1089 return Err(StellarTransactionUtilsError::AssetCodeTooLong(
1090 12, code,
1092 ));
1093 }
1094 } else {
1095 return Err(StellarTransactionUtilsError::InvalidAssetFormat(format!(
1096 "Invalid asset identifier format. Expected 'native' or 'CODE:ISSUER', got: {asset_id}"
1097 )));
1098 }
1099 };
1100
1101 Ok(OperationSpec::Payment {
1102 destination: destination.to_string(),
1103 amount,
1104 asset,
1105 })
1106}
1107
1108pub fn add_operation_to_envelope(
1110 envelope: &mut TransactionEnvelope,
1111 operation: Operation,
1112) -> Result<(), StellarTransactionUtilsError> {
1113 match envelope {
1114 TransactionEnvelope::TxV0(ref mut e) => {
1115 let mut ops: Vec<Operation> = e.tx.operations.iter().cloned().collect();
1117 ops.push(operation);
1118
1119 let operations: VecM<Operation, 100> = ops.try_into().map_err(|_| {
1121 StellarTransactionUtilsError::TooManyOperations(STELLAR_MAX_OPERATIONS)
1122 })?;
1123
1124 e.tx.operations = operations;
1125
1126 e.tx.fee = (e.tx.operations.len() as u32) * STELLAR_DEFAULT_TRANSACTION_FEE;
1128 }
1130 TransactionEnvelope::Tx(ref mut e) => {
1131 let mut ops: Vec<Operation> = e.tx.operations.iter().cloned().collect();
1133 ops.push(operation);
1134
1135 let operations: VecM<Operation, 100> = ops.try_into().map_err(|_| {
1137 StellarTransactionUtilsError::TooManyOperations(STELLAR_MAX_OPERATIONS)
1138 })?;
1139
1140 e.tx.operations = operations;
1141
1142 e.tx.fee = (e.tx.operations.len() as u32) * STELLAR_DEFAULT_TRANSACTION_FEE;
1144 }
1146 TransactionEnvelope::TxFeeBump(_) => {
1147 return Err(StellarTransactionUtilsError::CannotModifyFeeBump);
1148 }
1149 }
1150 Ok(())
1151}
1152
1153pub fn extract_time_bounds(envelope: &TransactionEnvelope) -> Option<&TimeBounds> {
1164 match envelope {
1165 TransactionEnvelope::TxV0(e) => e.tx.time_bounds.as_ref(),
1166 TransactionEnvelope::Tx(e) => match &e.tx.cond {
1167 Preconditions::Time(tb) => Some(tb),
1168 Preconditions::V2(v2) => v2.time_bounds.as_ref(),
1169 Preconditions::None => None,
1170 },
1171 TransactionEnvelope::TxFeeBump(fb) => {
1172 match &fb.tx.inner_tx {
1174 soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_tx) => {
1175 match &inner_tx.tx.cond {
1176 Preconditions::Time(tb) => Some(tb),
1177 Preconditions::V2(v2) => v2.time_bounds.as_ref(),
1178 Preconditions::None => None,
1179 }
1180 }
1181 }
1182 }
1183 }
1184}
1185
1186pub fn set_time_bounds(
1188 envelope: &mut TransactionEnvelope,
1189 valid_until: DateTime<Utc>,
1190) -> Result<(), StellarTransactionUtilsError> {
1191 let max_time = valid_until.timestamp() as u64;
1192 let time_bounds = TimeBounds {
1193 min_time: TimePoint(0),
1194 max_time: TimePoint(max_time),
1195 };
1196
1197 match envelope {
1198 TransactionEnvelope::TxV0(ref mut e) => {
1199 e.tx.time_bounds = Some(time_bounds);
1200 }
1201 TransactionEnvelope::Tx(ref mut e) => {
1202 e.tx.cond = Preconditions::Time(time_bounds);
1203 }
1204 TransactionEnvelope::TxFeeBump(_) => {
1205 return Err(StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump);
1206 }
1207 }
1208 Ok(())
1209}
1210
1211fn credit_alphanum4_to_asset_id(
1213 alpha4: &AlphaNum4,
1214) -> Result<String, StellarTransactionUtilsError> {
1215 let code_bytes = alpha4.asset_code.0;
1217 let code_len = code_bytes.iter().position(|&b| b == 0).unwrap_or(4);
1218 let code = String::from_utf8(code_bytes[..code_len].to_vec()).map_err(|e| {
1219 StellarTransactionUtilsError::InvalidAssetFormat(format!("Invalid asset code: {e}"))
1220 })?;
1221
1222 let issuer = match &alpha4.issuer.0 {
1224 XdrPublicKey::PublicKeyTypeEd25519(uint256) => {
1225 let bytes: [u8; 32] = uint256.0;
1226 let pk = PublicKey(bytes);
1227 pk.to_string()
1228 }
1229 };
1230
1231 Ok(format!("{code}:{issuer}"))
1232}
1233
1234fn credit_alphanum12_to_asset_id(
1236 alpha12: &AlphaNum12,
1237) -> Result<String, StellarTransactionUtilsError> {
1238 let code_bytes = alpha12.asset_code.0;
1240 let code_len = code_bytes.iter().position(|&b| b == 0).unwrap_or(12);
1241 let code = String::from_utf8(code_bytes[..code_len].to_vec()).map_err(|e| {
1242 StellarTransactionUtilsError::InvalidAssetFormat(format!("Invalid asset code: {e}"))
1243 })?;
1244
1245 let issuer = match &alpha12.issuer.0 {
1247 XdrPublicKey::PublicKeyTypeEd25519(uint256) => {
1248 let bytes: [u8; 32] = uint256.0;
1249 let pk = PublicKey(bytes);
1250 pk.to_string()
1251 }
1252 };
1253
1254 Ok(format!("{code}:{issuer}"))
1255}
1256
1257pub fn change_trust_asset_to_asset_id(
1270 change_trust_asset: &ChangeTrustAsset,
1271) -> Result<Option<String>, StellarTransactionUtilsError> {
1272 match change_trust_asset {
1273 ChangeTrustAsset::Native | ChangeTrustAsset::PoolShare(_) => Ok(None),
1274 ChangeTrustAsset::CreditAlphanum4(alpha4) => {
1275 let asset = Asset::CreditAlphanum4(alpha4.clone());
1277 asset_to_asset_id(&asset).map(Some)
1278 }
1279 ChangeTrustAsset::CreditAlphanum12(alpha12) => {
1280 let asset = Asset::CreditAlphanum12(alpha12.clone());
1282 asset_to_asset_id(&asset).map(Some)
1283 }
1284 }
1285}
1286
1287pub fn asset_to_asset_id(asset: &Asset) -> Result<String, StellarTransactionUtilsError> {
1297 match asset {
1298 Asset::Native => Ok("native".to_string()),
1299 Asset::CreditAlphanum4(alpha4) => credit_alphanum4_to_asset_id(alpha4),
1300 Asset::CreditAlphanum12(alpha12) => credit_alphanum12_to_asset_id(alpha12),
1301 }
1302}
1303
1304pub fn compute_resubmit_backoff_interval(
1315 total_age: chrono::Duration,
1316 base_interval_secs: i64,
1317 max_interval_secs: i64,
1318) -> Option<chrono::Duration> {
1319 let age_secs = total_age.num_seconds();
1320
1321 if age_secs < base_interval_secs {
1322 return None;
1323 }
1324
1325 let ratio = age_secs / base_interval_secs; let n = (ratio as u64).ilog2(); let interval = base_interval_secs.saturating_mul(1_i64.wrapping_shl(n));
1329 let capped = interval.min(max_interval_secs);
1330
1331 Some(chrono::Duration::seconds(capped))
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use super::*;
1337 use crate::domain::transaction::stellar::test_helpers::TEST_PK;
1338 use crate::models::AssetSpec;
1339 use crate::models::{AuthSpec, ContractSource, WasmSource};
1340
1341 fn payment_op(destination: &str) -> OperationSpec {
1342 OperationSpec::Payment {
1343 destination: destination.to_string(),
1344 amount: 100,
1345 asset: AssetSpec::Native,
1346 }
1347 }
1348
1349 #[test]
1350 fn returns_false_for_only_payment_ops() {
1351 let ops = vec![payment_op(TEST_PK)];
1352 assert!(!needs_simulation(&ops));
1353 }
1354
1355 #[test]
1356 fn returns_true_for_invoke_contract_ops() {
1357 let ops = vec![OperationSpec::InvokeContract {
1358 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1359 .to_string(),
1360 function_name: "transfer".to_string(),
1361 args: vec![],
1362 auth: None,
1363 }];
1364 assert!(needs_simulation(&ops));
1365 }
1366
1367 #[test]
1368 fn returns_true_for_upload_wasm_ops() {
1369 let ops = vec![OperationSpec::UploadWasm {
1370 wasm: WasmSource::Hex {
1371 hex: "deadbeef".to_string(),
1372 },
1373 auth: None,
1374 }];
1375 assert!(needs_simulation(&ops));
1376 }
1377
1378 #[test]
1379 fn returns_true_for_create_contract_ops() {
1380 let ops = vec![OperationSpec::CreateContract {
1381 source: ContractSource::Address {
1382 address: TEST_PK.to_string(),
1383 },
1384 wasm_hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
1385 .to_string(),
1386 salt: None,
1387 constructor_args: None,
1388 auth: None,
1389 }];
1390 assert!(needs_simulation(&ops));
1391 }
1392
1393 #[test]
1394 fn returns_true_for_single_invoke_host_function() {
1395 let ops = vec![OperationSpec::InvokeContract {
1396 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1397 .to_string(),
1398 function_name: "transfer".to_string(),
1399 args: vec![],
1400 auth: Some(AuthSpec::SourceAccount),
1401 }];
1402 assert!(needs_simulation(&ops));
1403 }
1404
1405 #[test]
1406 fn returns_false_for_multiple_payment_ops() {
1407 let ops = vec![payment_op(TEST_PK), payment_op(TEST_PK)];
1408 assert!(!needs_simulation(&ops));
1409 }
1410
1411 mod next_sequence_u64_tests {
1412 use super::*;
1413
1414 #[test]
1415 fn test_increment() {
1416 assert_eq!(next_sequence_u64(0).unwrap(), 1);
1417
1418 assert_eq!(next_sequence_u64(12345).unwrap(), 12346);
1419 }
1420
1421 #[test]
1422 fn test_error_path_overflow_i64_max() {
1423 let result = next_sequence_u64(i64::MAX);
1424 assert!(result.is_err());
1425 match result.unwrap_err() {
1426 RelayerError::ProviderError(msg) => assert_eq!(msg, "sequence overflow"),
1427 _ => panic!("Unexpected error type"),
1428 }
1429 }
1430 }
1431
1432 mod i64_from_u64_tests {
1433 use super::*;
1434
1435 #[test]
1436 fn test_happy_path_conversion() {
1437 assert_eq!(i64_from_u64(0).unwrap(), 0);
1438 assert_eq!(i64_from_u64(12345).unwrap(), 12345);
1439 assert_eq!(i64_from_u64(i64::MAX as u64).unwrap(), i64::MAX);
1440 }
1441
1442 #[test]
1443 fn test_error_path_overflow_u64_max() {
1444 let result = i64_from_u64(u64::MAX);
1445 assert!(result.is_err());
1446 match result.unwrap_err() {
1447 RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1448 _ => panic!("Unexpected error type"),
1449 }
1450 }
1451
1452 #[test]
1453 fn test_edge_case_just_above_i64_max() {
1454 let value = (i64::MAX as u64) + 1;
1456 let result = i64_from_u64(value);
1457 assert!(result.is_err());
1458 match result.unwrap_err() {
1459 RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1460 _ => panic!("Unexpected error type"),
1461 }
1462 }
1463 }
1464
1465 mod is_bad_sequence_error_tests {
1466 use super::*;
1467
1468 #[test]
1469 fn test_detects_txbadseq() {
1470 assert!(is_bad_sequence_error(
1471 "Failed to send transaction: transaction submission failed: TxBadSeq"
1472 ));
1473 assert!(is_bad_sequence_error("Error: TxBadSeq"));
1474 assert!(is_bad_sequence_error("txbadseq"));
1475 assert!(is_bad_sequence_error("TXBADSEQ"));
1476 }
1477
1478 #[test]
1479 fn test_returns_false_for_other_errors() {
1480 assert!(!is_bad_sequence_error("network timeout"));
1481 assert!(!is_bad_sequence_error("insufficient balance"));
1482 assert!(!is_bad_sequence_error("tx_insufficient_fee"));
1483 assert!(!is_bad_sequence_error("bad_auth"));
1484 assert!(!is_bad_sequence_error(""));
1485 }
1486 }
1487
1488 mod decode_tx_result_code_tests {
1489 use super::*;
1490 use soroban_rs::xdr::{TransactionResult, TransactionResultResult, WriteXdr};
1491
1492 #[test]
1493 fn test_decodes_tx_bad_seq() {
1494 let result = TransactionResult {
1495 fee_charged: 100,
1496 result: TransactionResultResult::TxBadSeq,
1497 ext: soroban_rs::xdr::TransactionResultExt::V0,
1498 };
1499 let xdr = result.to_xdr_base64(Limits::none()).unwrap();
1500 assert_eq!(decode_tx_result_code(&xdr), Some("TxBadSeq".to_string()));
1501 }
1502
1503 #[test]
1504 fn test_decodes_tx_insufficient_balance() {
1505 let result = TransactionResult {
1506 fee_charged: 100,
1507 result: TransactionResultResult::TxInsufficientBalance,
1508 ext: soroban_rs::xdr::TransactionResultExt::V0,
1509 };
1510 let xdr = result.to_xdr_base64(Limits::none()).unwrap();
1511 assert_eq!(
1512 decode_tx_result_code(&xdr),
1513 Some("TxInsufficientBalance".to_string())
1514 );
1515 }
1516
1517 #[test]
1518 fn test_returns_none_for_invalid_xdr() {
1519 assert_eq!(decode_tx_result_code("not-valid-xdr"), None);
1520 }
1521
1522 #[test]
1523 fn test_returns_none_for_empty_string() {
1524 assert_eq!(decode_tx_result_code(""), None);
1525 }
1526 }
1527
1528 mod is_insufficient_fee_error_tests {
1529 use super::*;
1530
1531 #[test]
1532 fn test_detects_txinsufficientfee() {
1533 assert!(is_insufficient_fee_error("TxInsufficientFee"));
1534 assert!(is_insufficient_fee_error("txinsufficientfee"));
1535 assert!(is_insufficient_fee_error("TXINSUFFICIENTFEE"));
1536 }
1537
1538 #[test]
1539 fn test_returns_false_for_other_errors() {
1540 assert!(!is_insufficient_fee_error("network timeout"));
1541 assert!(!is_insufficient_fee_error("TxBadSeq"));
1542 assert!(!is_insufficient_fee_error("TxInsufficientBalance"));
1543 assert!(!is_insufficient_fee_error("TxBadAuth"));
1544 assert!(!is_insufficient_fee_error(""));
1545 }
1546 }
1547
1548 mod decode_transaction_result_code_tests {
1549 use super::*;
1550
1551 #[test]
1552 fn test_decodes_insufficient_fee_result_xdr() {
1553 let result_code = decode_transaction_result_code("AAAAAAAAY/n////3AAAAAA==").unwrap();
1554 assert_eq!(result_code, "TxInsufficientFee");
1555 }
1556
1557 #[test]
1558 fn test_returns_none_for_invalid_xdr() {
1559 assert!(decode_transaction_result_code("not-base64").is_none());
1560 }
1561 }
1562
1563 mod status_check_utils_tests {
1564 use crate::models::{
1565 NetworkTransactionData, StellarTransactionData, TransactionError, TransactionInput,
1566 TransactionRepoModel,
1567 };
1568 use crate::utils::mocks::mockutils::create_mock_transaction;
1569 use chrono::{Duration, Utc};
1570
1571 fn create_test_tx_with_age(seconds_ago: i64) -> TransactionRepoModel {
1573 let created_at = (Utc::now() - Duration::seconds(seconds_ago)).to_rfc3339();
1574 let mut tx = create_mock_transaction();
1575 tx.id = format!("test-tx-{seconds_ago}");
1576 tx.created_at = created_at;
1577 tx.network_data = NetworkTransactionData::Stellar(StellarTransactionData {
1578 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1579 .to_string(),
1580 fee: None,
1581 sequence_number: None,
1582 memo: None,
1583 valid_until: None,
1584 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1585 signatures: vec![],
1586 hash: Some("test-hash-12345".to_string()),
1587 simulation_transaction_data: None,
1588 transaction_input: TransactionInput::Operations(vec![]),
1589 signed_envelope_xdr: None,
1590 transaction_result_xdr: None,
1591 });
1592 tx
1593 }
1594
1595 mod get_age_since_created_tests {
1596 use crate::domain::transaction::util::get_age_since_created;
1597
1598 use super::*;
1599
1600 #[test]
1601 fn test_returns_correct_age_for_recent_transaction() {
1602 let tx = create_test_tx_with_age(30); let age = get_age_since_created(&tx).unwrap();
1604
1605 assert!(age.num_seconds() >= 29 && age.num_seconds() <= 31);
1607 }
1608
1609 #[test]
1610 fn test_returns_correct_age_for_old_transaction() {
1611 let tx = create_test_tx_with_age(3600); let age = get_age_since_created(&tx).unwrap();
1613
1614 assert!(age.num_seconds() >= 3599 && age.num_seconds() <= 3601);
1616 }
1617
1618 #[test]
1619 fn test_returns_zero_age_for_just_created_transaction() {
1620 let tx = create_test_tx_with_age(0); let age = get_age_since_created(&tx).unwrap();
1622
1623 assert!(age.num_seconds() >= 0 && age.num_seconds() <= 1);
1625 }
1626
1627 #[test]
1628 fn test_handles_negative_age_gracefully() {
1629 let created_at = (Utc::now() + Duration::seconds(10)).to_rfc3339();
1631 let mut tx = create_mock_transaction();
1632 tx.created_at = created_at;
1633
1634 let age = get_age_since_created(&tx).unwrap();
1635
1636 assert!(age.num_seconds() < 0);
1638 }
1639
1640 #[test]
1641 fn test_returns_error_for_invalid_created_at() {
1642 let mut tx = create_mock_transaction();
1643 tx.created_at = "invalid-timestamp".to_string();
1644
1645 let result = get_age_since_created(&tx);
1646 assert!(result.is_err());
1647
1648 match result.unwrap_err() {
1649 TransactionError::UnexpectedError(msg) => {
1650 assert!(msg.contains("Invalid created_at timestamp"));
1651 }
1652 _ => panic!("Expected UnexpectedError"),
1653 }
1654 }
1655
1656 #[test]
1657 fn test_returns_error_for_empty_created_at() {
1658 let mut tx = create_mock_transaction();
1659 tx.created_at = "".to_string();
1660
1661 let result = get_age_since_created(&tx);
1662 assert!(result.is_err());
1663 }
1664
1665 #[test]
1666 fn test_handles_various_rfc3339_formats() {
1667 let mut tx = create_mock_transaction();
1668
1669 tx.created_at = "2025-01-01T12:00:00Z".to_string();
1671 assert!(get_age_since_created(&tx).is_ok());
1672
1673 tx.created_at = "2025-01-01T12:00:00+00:00".to_string();
1675 assert!(get_age_since_created(&tx).is_ok());
1676
1677 tx.created_at = "2025-01-01T12:00:00.123Z".to_string();
1679 assert!(get_age_since_created(&tx).is_ok());
1680 }
1681 }
1682 }
1683
1684 #[test]
1685 fn test_create_signature_payload_functions() {
1686 use soroban_rs::xdr::{
1687 Hash, SequenceNumber, TransactionEnvelope, TransactionV0, TransactionV0Envelope,
1688 Uint256,
1689 };
1690
1691 let transaction = soroban_rs::xdr::Transaction {
1693 source_account: soroban_rs::xdr::MuxedAccount::Ed25519(Uint256([1u8; 32])),
1694 fee: 100,
1695 seq_num: SequenceNumber(123),
1696 cond: soroban_rs::xdr::Preconditions::None,
1697 memo: soroban_rs::xdr::Memo::None,
1698 operations: vec![].try_into().unwrap(),
1699 ext: soroban_rs::xdr::TransactionExt::V0,
1700 };
1701 let network_id = Hash([2u8; 32]);
1702
1703 let payload = create_transaction_signature_payload(&transaction, &network_id);
1704 assert_eq!(payload.network_id, network_id);
1705
1706 let v0_tx = TransactionV0 {
1708 source_account_ed25519: Uint256([1u8; 32]),
1709 fee: 100,
1710 seq_num: SequenceNumber(123),
1711 time_bounds: None,
1712 memo: soroban_rs::xdr::Memo::None,
1713 operations: vec![].try_into().unwrap(),
1714 ext: soroban_rs::xdr::TransactionV0Ext::V0,
1715 };
1716 let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1717 tx: v0_tx,
1718 signatures: vec![].try_into().unwrap(),
1719 });
1720
1721 let v0_payload = create_signature_payload(&v0_envelope, &network_id).unwrap();
1722 assert_eq!(v0_payload.network_id, network_id);
1723 }
1724
1725 mod convert_v0_to_v1_transaction_tests {
1726 use super::*;
1727 use soroban_rs::xdr::{SequenceNumber, TransactionV0, Uint256};
1728
1729 #[test]
1730 fn test_convert_v0_to_v1_transaction() {
1731 let v0_tx = TransactionV0 {
1733 source_account_ed25519: Uint256([1u8; 32]),
1734 fee: 100,
1735 seq_num: SequenceNumber(123),
1736 time_bounds: None,
1737 memo: soroban_rs::xdr::Memo::None,
1738 operations: vec![].try_into().unwrap(),
1739 ext: soroban_rs::xdr::TransactionV0Ext::V0,
1740 };
1741
1742 let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1744
1745 assert_eq!(v1_tx.fee, v0_tx.fee);
1747 assert_eq!(v1_tx.seq_num, v0_tx.seq_num);
1748 assert_eq!(v1_tx.memo, v0_tx.memo);
1749 assert_eq!(v1_tx.operations, v0_tx.operations);
1750 assert!(matches!(v1_tx.ext, soroban_rs::xdr::TransactionExt::V0));
1751 assert!(matches!(v1_tx.cond, soroban_rs::xdr::Preconditions::None));
1752
1753 match v1_tx.source_account {
1755 soroban_rs::xdr::MuxedAccount::Ed25519(addr) => {
1756 assert_eq!(addr, v0_tx.source_account_ed25519);
1757 }
1758 _ => panic!("Expected Ed25519 muxed account"),
1759 }
1760 }
1761
1762 #[test]
1763 fn test_convert_v0_to_v1_transaction_with_time_bounds() {
1764 let time_bounds = soroban_rs::xdr::TimeBounds {
1766 min_time: soroban_rs::xdr::TimePoint(100),
1767 max_time: soroban_rs::xdr::TimePoint(200),
1768 };
1769
1770 let v0_tx = TransactionV0 {
1771 source_account_ed25519: Uint256([2u8; 32]),
1772 fee: 200,
1773 seq_num: SequenceNumber(456),
1774 time_bounds: Some(time_bounds.clone()),
1775 memo: soroban_rs::xdr::Memo::Text("test".try_into().unwrap()),
1776 operations: vec![].try_into().unwrap(),
1777 ext: soroban_rs::xdr::TransactionV0Ext::V0,
1778 };
1779
1780 let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1782
1783 match v1_tx.cond {
1785 soroban_rs::xdr::Preconditions::Time(tb) => {
1786 assert_eq!(tb, time_bounds);
1787 }
1788 _ => panic!("Expected Time preconditions"),
1789 }
1790 }
1791 }
1792}
1793
1794#[cfg(test)]
1795mod parse_contract_address_tests {
1796 use super::*;
1797 use crate::domain::transaction::stellar::test_helpers::{
1798 TEST_CONTRACT, TEST_PK as TEST_ACCOUNT,
1799 };
1800
1801 #[test]
1802 fn test_parse_valid_contract_address() {
1803 let result = parse_contract_address(TEST_CONTRACT);
1804 assert!(result.is_ok());
1805
1806 let hash = result.unwrap();
1807 assert_eq!(hash.0.len(), 32);
1808 }
1809
1810 #[test]
1811 fn test_parse_invalid_contract_address() {
1812 let result = parse_contract_address("INVALID_CONTRACT");
1813 assert!(result.is_err());
1814
1815 match result.unwrap_err() {
1816 StellarTransactionUtilsError::InvalidContractAddress(addr, _) => {
1817 assert_eq!(addr, "INVALID_CONTRACT");
1818 }
1819 _ => panic!("Expected InvalidContractAddress error"),
1820 }
1821 }
1822
1823 #[test]
1824 fn test_parse_contract_address_wrong_prefix() {
1825 let result = parse_contract_address(TEST_ACCOUNT);
1827 assert!(result.is_err());
1828 }
1829
1830 #[test]
1831 fn test_parse_empty_contract_address() {
1832 let result = parse_contract_address("");
1833 assert!(result.is_err());
1834 }
1835}
1836
1837#[cfg(test)]
1842mod update_envelope_sequence_tests {
1843 use super::*;
1844 use soroban_rs::xdr::{
1845 FeeBumpTransaction, FeeBumpTransactionEnvelope, FeeBumpTransactionExt,
1846 FeeBumpTransactionInnerTx, Memo, MuxedAccount, Preconditions, SequenceNumber, Transaction,
1847 TransactionExt, TransactionV0, TransactionV0Envelope, TransactionV0Ext,
1848 TransactionV1Envelope, Uint256, VecM,
1849 };
1850
1851 fn create_minimal_v1_envelope() -> TransactionEnvelope {
1852 let tx = Transaction {
1853 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
1854 fee: 100,
1855 seq_num: SequenceNumber(0),
1856 cond: Preconditions::None,
1857 memo: Memo::None,
1858 operations: VecM::default(),
1859 ext: TransactionExt::V0,
1860 };
1861 TransactionEnvelope::Tx(TransactionV1Envelope {
1862 tx,
1863 signatures: VecM::default(),
1864 })
1865 }
1866
1867 fn create_v0_envelope() -> TransactionEnvelope {
1868 let tx = TransactionV0 {
1869 source_account_ed25519: Uint256([0u8; 32]),
1870 fee: 100,
1871 seq_num: SequenceNumber(0),
1872 time_bounds: None,
1873 memo: Memo::None,
1874 operations: VecM::default(),
1875 ext: TransactionV0Ext::V0,
1876 };
1877 TransactionEnvelope::TxV0(TransactionV0Envelope {
1878 tx,
1879 signatures: VecM::default(),
1880 })
1881 }
1882
1883 fn create_fee_bump_envelope() -> TransactionEnvelope {
1884 let inner_tx = Transaction {
1885 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
1886 fee: 100,
1887 seq_num: SequenceNumber(0),
1888 cond: Preconditions::None,
1889 memo: Memo::None,
1890 operations: VecM::default(),
1891 ext: TransactionExt::V0,
1892 };
1893 let inner_envelope = TransactionV1Envelope {
1894 tx: inner_tx,
1895 signatures: VecM::default(),
1896 };
1897 let fee_bump_tx = FeeBumpTransaction {
1898 fee_source: MuxedAccount::Ed25519(Uint256([1u8; 32])),
1899 fee: 200,
1900 inner_tx: FeeBumpTransactionInnerTx::Tx(inner_envelope),
1901 ext: FeeBumpTransactionExt::V0,
1902 };
1903 TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
1904 tx: fee_bump_tx,
1905 signatures: VecM::default(),
1906 })
1907 }
1908
1909 #[test]
1910 fn test_update_envelope_sequence() {
1911 let mut envelope = create_minimal_v1_envelope();
1912 update_envelope_sequence(&mut envelope, 12345).unwrap();
1913 if let TransactionEnvelope::Tx(v1) = &envelope {
1914 assert_eq!(v1.tx.seq_num.0, 12345);
1915 } else {
1916 panic!("Expected Tx envelope");
1917 }
1918 }
1919
1920 #[test]
1921 fn test_update_envelope_sequence_v0_returns_error() {
1922 let mut envelope = create_v0_envelope();
1923 let result = update_envelope_sequence(&mut envelope, 12345);
1924 assert!(result.is_err());
1925 match result.unwrap_err() {
1926 StellarTransactionUtilsError::V0TransactionsNotSupported => {}
1927 _ => panic!("Expected V0TransactionsNotSupported error"),
1928 }
1929 }
1930
1931 #[test]
1932 fn test_update_envelope_sequence_fee_bump_returns_error() {
1933 let mut envelope = create_fee_bump_envelope();
1934 let result = update_envelope_sequence(&mut envelope, 12345);
1935 assert!(result.is_err());
1936 match result.unwrap_err() {
1937 StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump => {}
1938 _ => panic!("Expected CannotUpdateSequenceOnFeeBump error"),
1939 }
1940 }
1941
1942 #[test]
1943 fn test_update_envelope_sequence_zero() {
1944 let mut envelope = create_minimal_v1_envelope();
1945 update_envelope_sequence(&mut envelope, 0).unwrap();
1946 if let TransactionEnvelope::Tx(v1) = &envelope {
1947 assert_eq!(v1.tx.seq_num.0, 0);
1948 } else {
1949 panic!("Expected Tx envelope");
1950 }
1951 }
1952
1953 #[test]
1954 fn test_update_envelope_sequence_max_value() {
1955 let mut envelope = create_minimal_v1_envelope();
1956 update_envelope_sequence(&mut envelope, i64::MAX).unwrap();
1957 if let TransactionEnvelope::Tx(v1) = &envelope {
1958 assert_eq!(v1.tx.seq_num.0, i64::MAX);
1959 } else {
1960 panic!("Expected Tx envelope");
1961 }
1962 }
1963
1964 #[test]
1965 fn test_envelope_fee_in_stroops_v1() {
1966 let envelope = create_minimal_v1_envelope();
1967 let fee = envelope_fee_in_stroops(&envelope).unwrap();
1968 assert_eq!(fee, 100);
1969 }
1970
1971 #[test]
1972 fn test_envelope_fee_in_stroops_v0_returns_error() {
1973 let envelope = create_v0_envelope();
1974 let result = envelope_fee_in_stroops(&envelope);
1975 assert!(result.is_err());
1976 match result.unwrap_err() {
1977 StellarTransactionUtilsError::InvalidTransactionFormat(msg) => {
1978 assert!(msg.contains("Expected V1"));
1979 }
1980 _ => panic!("Expected InvalidTransactionFormat error"),
1981 }
1982 }
1983
1984 #[test]
1985 fn test_envelope_fee_in_stroops_fee_bump_returns_error() {
1986 let envelope = create_fee_bump_envelope();
1987 let result = envelope_fee_in_stroops(&envelope);
1988 assert!(result.is_err());
1989 }
1990}
1991
1992#[cfg(test)]
1997mod create_contract_data_key_tests {
1998 use super::*;
1999 use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
2000 use stellar_strkey::ed25519::PublicKey;
2001
2002 #[test]
2003 fn test_create_key_without_address() {
2004 let result = create_contract_data_key("Balance", None);
2005 assert!(result.is_ok());
2006
2007 match result.unwrap() {
2008 ScVal::Symbol(sym) => {
2009 assert_eq!(sym.to_string(), "Balance");
2010 }
2011 _ => panic!("Expected Symbol ScVal"),
2012 }
2013 }
2014
2015 #[test]
2016 fn test_create_key_with_address() {
2017 let pk = PublicKey::from_string(TEST_ACCOUNT).unwrap();
2018 let uint256 = Uint256(pk.0);
2019 let account_id = AccountId(soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(uint256));
2020 let sc_address = ScAddress::Account(account_id);
2021
2022 let result = create_contract_data_key("Balance", Some(sc_address.clone()));
2023 assert!(result.is_ok());
2024
2025 match result.unwrap() {
2026 ScVal::Vec(Some(vec)) => {
2027 assert_eq!(vec.0.len(), 2);
2028 match &vec.0[0] {
2029 ScVal::Symbol(sym) => assert_eq!(sym.to_string(), "Balance"),
2030 _ => panic!("Expected Symbol as first element"),
2031 }
2032 match &vec.0[1] {
2033 ScVal::Address(addr) => assert_eq!(addr, &sc_address),
2034 _ => panic!("Expected Address as second element"),
2035 }
2036 }
2037 _ => panic!("Expected Vec ScVal"),
2038 }
2039 }
2040
2041 #[test]
2042 fn test_create_key_invalid_symbol() {
2043 let very_long_symbol = "a".repeat(100);
2045 let result = create_contract_data_key(&very_long_symbol, None);
2046 assert!(result.is_err());
2047
2048 match result.unwrap_err() {
2049 StellarTransactionUtilsError::SymbolCreationFailed(_, _) => {}
2050 _ => panic!("Expected SymbolCreationFailed error"),
2051 }
2052 }
2053
2054 #[test]
2055 fn test_create_key_decimals() {
2056 let result = create_contract_data_key("Decimals", None);
2057 assert!(result.is_ok());
2058 }
2059}
2060
2061#[cfg(test)]
2066mod extract_scval_from_contract_data_tests {
2067 use super::*;
2068 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
2069 use soroban_rs::xdr::{
2070 ContractDataDurability, ContractDataEntry, ExtensionPoint, Hash, LedgerEntry,
2071 LedgerEntryData, LedgerEntryExt, ScSymbol, ScVal, WriteXdr,
2072 };
2073
2074 #[test]
2075 fn test_extract_scval_success() {
2076 let contract_data = ContractDataEntry {
2077 ext: ExtensionPoint::V0,
2078 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
2079 key: ScVal::Symbol(ScSymbol::try_from("test").unwrap()),
2080 durability: ContractDataDurability::Persistent,
2081 val: ScVal::U32(42),
2082 };
2083
2084 let ledger_entry = LedgerEntry {
2085 last_modified_ledger_seq: 100,
2086 data: LedgerEntryData::ContractData(contract_data),
2087 ext: LedgerEntryExt::V0,
2088 };
2089
2090 let xdr = ledger_entry
2091 .data
2092 .to_xdr_base64(soroban_rs::xdr::Limits::none())
2093 .unwrap();
2094
2095 let response = GetLedgerEntriesResponse {
2096 entries: Some(vec![LedgerEntryResult {
2097 key: "test_key".to_string(),
2098 xdr,
2099 last_modified_ledger: 100,
2100 live_until_ledger_seq_ledger_seq: None,
2101 }]),
2102 latest_ledger: 100,
2103 };
2104
2105 let result = extract_scval_from_contract_data(&response, "test");
2106 assert!(result.is_ok());
2107
2108 match result.unwrap() {
2109 ScVal::U32(val) => assert_eq!(val, 42),
2110 _ => panic!("Expected U32 ScVal"),
2111 }
2112 }
2113
2114 #[test]
2115 fn test_extract_scval_no_entries() {
2116 let response = GetLedgerEntriesResponse {
2117 entries: None,
2118 latest_ledger: 100,
2119 };
2120
2121 let result = extract_scval_from_contract_data(&response, "test");
2122 assert!(result.is_err());
2123
2124 match result.unwrap_err() {
2125 StellarTransactionUtilsError::NoEntriesFound(_) => {}
2126 _ => panic!("Expected NoEntriesFound error"),
2127 }
2128 }
2129
2130 #[test]
2131 fn test_extract_scval_empty_entries() {
2132 let response = GetLedgerEntriesResponse {
2133 entries: Some(vec![]),
2134 latest_ledger: 100,
2135 };
2136
2137 let result = extract_scval_from_contract_data(&response, "test");
2138 assert!(result.is_err());
2139
2140 match result.unwrap_err() {
2141 StellarTransactionUtilsError::EmptyEntries(_) => {}
2142 _ => panic!("Expected EmptyEntries error"),
2143 }
2144 }
2145}
2146
2147#[cfg(test)]
2152mod extract_u32_from_scval_tests {
2153 use super::*;
2154 use soroban_rs::xdr::{Int128Parts, ScVal, UInt128Parts};
2155
2156 #[test]
2157 fn test_extract_from_u32() {
2158 let val = ScVal::U32(42);
2159 assert_eq!(extract_u32_from_scval(&val, "test"), Some(42));
2160 }
2161
2162 #[test]
2163 fn test_extract_from_i32_positive() {
2164 let val = ScVal::I32(100);
2165 assert_eq!(extract_u32_from_scval(&val, "test"), Some(100));
2166 }
2167
2168 #[test]
2169 fn test_extract_from_i32_negative() {
2170 let val = ScVal::I32(-1);
2171 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2172 }
2173
2174 #[test]
2175 fn test_extract_from_u64() {
2176 let val = ScVal::U64(1000);
2177 assert_eq!(extract_u32_from_scval(&val, "test"), Some(1000));
2178 }
2179
2180 #[test]
2181 fn test_extract_from_u64_overflow() {
2182 let val = ScVal::U64(u64::MAX);
2183 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2184 }
2185
2186 #[test]
2187 fn test_extract_from_i64_positive() {
2188 let val = ScVal::I64(500);
2189 assert_eq!(extract_u32_from_scval(&val, "test"), Some(500));
2190 }
2191
2192 #[test]
2193 fn test_extract_from_i64_negative() {
2194 let val = ScVal::I64(-500);
2195 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2196 }
2197
2198 #[test]
2199 fn test_extract_from_u128_small() {
2200 let val = ScVal::U128(UInt128Parts { hi: 0, lo: 255 });
2201 assert_eq!(extract_u32_from_scval(&val, "test"), Some(255));
2202 }
2203
2204 #[test]
2205 fn test_extract_from_u128_hi_set() {
2206 let val = ScVal::U128(UInt128Parts { hi: 1, lo: 0 });
2207 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2208 }
2209
2210 #[test]
2211 fn test_extract_from_i128_small() {
2212 let val = ScVal::I128(Int128Parts { hi: 0, lo: 123 });
2213 assert_eq!(extract_u32_from_scval(&val, "test"), Some(123));
2214 }
2215
2216 #[test]
2217 fn test_extract_from_unsupported_type() {
2218 let val = ScVal::Bool(true);
2219 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2220 }
2221}
2222
2223#[cfg(test)]
2228mod amount_to_ui_amount_tests {
2229 use super::*;
2230
2231 #[test]
2232 fn test_zero_decimals() {
2233 assert_eq!(amount_to_ui_amount(100, 0), "100");
2234 assert_eq!(amount_to_ui_amount(0, 0), "0");
2235 }
2236
2237 #[test]
2238 fn test_with_decimals_no_padding() {
2239 assert_eq!(amount_to_ui_amount(1000000, 6), "1");
2240 assert_eq!(amount_to_ui_amount(1500000, 6), "1.5");
2241 assert_eq!(amount_to_ui_amount(1234567, 6), "1.234567");
2242 }
2243
2244 #[test]
2245 fn test_with_decimals_needs_padding() {
2246 assert_eq!(amount_to_ui_amount(1, 6), "0.000001");
2247 assert_eq!(amount_to_ui_amount(100, 6), "0.0001");
2248 assert_eq!(amount_to_ui_amount(1000, 3), "1");
2249 }
2250
2251 #[test]
2252 fn test_trailing_zeros_removed() {
2253 assert_eq!(amount_to_ui_amount(1000000, 6), "1");
2254 assert_eq!(amount_to_ui_amount(1500000, 7), "0.15");
2255 assert_eq!(amount_to_ui_amount(10000000, 7), "1");
2256 }
2257
2258 #[test]
2259 fn test_zero_amount() {
2260 assert_eq!(amount_to_ui_amount(0, 6), "0");
2261 assert_eq!(amount_to_ui_amount(0, 0), "0");
2262 }
2263
2264 #[test]
2265 fn test_xlm_7_decimals() {
2266 assert_eq!(amount_to_ui_amount(10000000, 7), "1");
2267 assert_eq!(amount_to_ui_amount(15000000, 7), "1.5");
2268 assert_eq!(amount_to_ui_amount(100, 7), "0.00001");
2269 }
2270}
2271
2272#[cfg(test)]
2278mod count_operations_tests {
2279 use super::*;
2280 use soroban_rs::xdr::{
2281 Limits, MuxedAccount, Operation, OperationBody, PaymentOp, TransactionV1Envelope, Uint256,
2282 WriteXdr,
2283 };
2284
2285 #[test]
2286 fn test_count_operations_from_xdr() {
2287 use soroban_rs::xdr::{Memo, Preconditions, SequenceNumber, Transaction, TransactionExt};
2288
2289 let payment_op = Operation {
2291 source_account: None,
2292 body: OperationBody::Payment(PaymentOp {
2293 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2294 asset: Asset::Native,
2295 amount: 100,
2296 }),
2297 };
2298
2299 let operations = vec![payment_op.clone(), payment_op].try_into().unwrap();
2300
2301 let tx = Transaction {
2302 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2303 fee: 100,
2304 seq_num: SequenceNumber(1),
2305 cond: Preconditions::None,
2306 memo: Memo::None,
2307 operations,
2308 ext: TransactionExt::V0,
2309 };
2310
2311 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2312 tx,
2313 signatures: vec![].try_into().unwrap(),
2314 });
2315
2316 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2317 let count = count_operations_from_xdr(&xdr).unwrap();
2318
2319 assert_eq!(count, 2);
2320 }
2321
2322 #[test]
2323 fn test_count_operations_invalid_xdr() {
2324 let result = count_operations_from_xdr("invalid_xdr");
2325 assert!(result.is_err());
2326
2327 match result.unwrap_err() {
2328 StellarTransactionUtilsError::XdrParseFailed(_) => {}
2329 _ => panic!("Expected XdrParseFailed error"),
2330 }
2331 }
2332}
2333
2334#[cfg(test)]
2339mod estimate_base_fee_tests {
2340 use super::*;
2341
2342 #[test]
2343 fn test_single_operation() {
2344 assert_eq!(estimate_base_fee(1), 100);
2345 }
2346
2347 #[test]
2348 fn test_multiple_operations() {
2349 assert_eq!(estimate_base_fee(5), 500);
2350 assert_eq!(estimate_base_fee(10), 1000);
2351 }
2352
2353 #[test]
2354 fn test_zero_operations() {
2355 assert_eq!(estimate_base_fee(0), 100);
2357 }
2358
2359 #[test]
2360 fn test_large_number_of_operations() {
2361 assert_eq!(estimate_base_fee(100), 10000);
2362 }
2363}
2364
2365#[cfg(test)]
2370mod create_fee_payment_operation_tests {
2371 use super::*;
2372 use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
2373
2374 #[test]
2375 fn test_create_native_payment() {
2376 let result = create_fee_payment_operation(TEST_ACCOUNT, "native", 1000);
2377 assert!(result.is_ok());
2378
2379 match result.unwrap() {
2380 OperationSpec::Payment {
2381 destination,
2382 amount,
2383 asset,
2384 } => {
2385 assert_eq!(destination, TEST_ACCOUNT);
2386 assert_eq!(amount, 1000);
2387 assert!(matches!(asset, AssetSpec::Native));
2388 }
2389 _ => panic!("Expected Payment operation"),
2390 }
2391 }
2392
2393 #[test]
2394 fn test_create_credit4_payment() {
2395 let result = create_fee_payment_operation(
2396 TEST_ACCOUNT,
2397 "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2398 5000,
2399 );
2400 assert!(result.is_ok());
2401
2402 match result.unwrap() {
2403 OperationSpec::Payment {
2404 destination,
2405 amount,
2406 asset,
2407 } => {
2408 assert_eq!(destination, TEST_ACCOUNT);
2409 assert_eq!(amount, 5000);
2410 match asset {
2411 AssetSpec::Credit4 { code, issuer } => {
2412 assert_eq!(code, "USDC");
2413 assert_eq!(
2414 issuer,
2415 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2416 );
2417 }
2418 _ => panic!("Expected Credit4 asset"),
2419 }
2420 }
2421 _ => panic!("Expected Payment operation"),
2422 }
2423 }
2424
2425 #[test]
2426 fn test_create_credit12_payment() {
2427 let result = create_fee_payment_operation(
2428 TEST_ACCOUNT,
2429 "LONGASSETNAM:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2430 2000,
2431 );
2432 assert!(result.is_ok());
2433
2434 match result.unwrap() {
2435 OperationSpec::Payment {
2436 destination,
2437 amount,
2438 asset,
2439 } => {
2440 assert_eq!(destination, TEST_ACCOUNT);
2441 assert_eq!(amount, 2000);
2442 match asset {
2443 AssetSpec::Credit12 { code, issuer } => {
2444 assert_eq!(code, "LONGASSETNAM");
2445 assert_eq!(
2446 issuer,
2447 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2448 );
2449 }
2450 _ => panic!("Expected Credit12 asset"),
2451 }
2452 }
2453 _ => panic!("Expected Payment operation"),
2454 }
2455 }
2456
2457 #[test]
2458 fn test_create_payment_empty_asset() {
2459 let result = create_fee_payment_operation(TEST_ACCOUNT, "", 1000);
2460 assert!(result.is_ok());
2461
2462 match result.unwrap() {
2463 OperationSpec::Payment { asset, .. } => {
2464 assert!(matches!(asset, AssetSpec::Native));
2465 }
2466 _ => panic!("Expected Payment operation"),
2467 }
2468 }
2469
2470 #[test]
2471 fn test_create_payment_invalid_format() {
2472 let result = create_fee_payment_operation(TEST_ACCOUNT, "INVALID_FORMAT", 1000);
2473 assert!(result.is_err());
2474
2475 match result.unwrap_err() {
2476 StellarTransactionUtilsError::InvalidAssetFormat(_) => {}
2477 _ => panic!("Expected InvalidAssetFormat error"),
2478 }
2479 }
2480
2481 #[test]
2482 fn test_create_payment_asset_code_too_long() {
2483 let result = create_fee_payment_operation(
2484 TEST_ACCOUNT,
2485 "VERYLONGASSETCODE:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2486 1000,
2487 );
2488 assert!(result.is_err());
2489
2490 match result.unwrap_err() {
2491 StellarTransactionUtilsError::AssetCodeTooLong(max_len, _) => {
2492 assert_eq!(max_len, 12);
2493 }
2494 _ => panic!("Expected AssetCodeTooLong error"),
2495 }
2496 }
2497}
2498
2499#[cfg(test)]
2500mod parse_account_id_tests {
2501 use super::*;
2502 use crate::domain::transaction::stellar::test_helpers::TEST_PK;
2503
2504 #[test]
2505 fn test_parse_account_id_valid() {
2506 let result = parse_account_id(TEST_PK);
2507 assert!(result.is_ok());
2508
2509 let account_id = result.unwrap();
2510 match account_id.0 {
2511 soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(_) => {}
2512 }
2513 }
2514
2515 #[test]
2516 fn test_parse_account_id_invalid() {
2517 let result = parse_account_id("INVALID_ADDRESS");
2518 assert!(result.is_err());
2519
2520 match result.unwrap_err() {
2521 StellarTransactionUtilsError::InvalidAccountAddress(addr, _) => {
2522 assert_eq!(addr, "INVALID_ADDRESS");
2523 }
2524 _ => panic!("Expected InvalidAccountAddress error"),
2525 }
2526 }
2527
2528 #[test]
2529 fn test_parse_account_id_empty() {
2530 let result = parse_account_id("");
2531 assert!(result.is_err());
2532 }
2533
2534 #[test]
2535 fn test_parse_account_id_wrong_prefix() {
2536 let result = parse_account_id("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM");
2538 assert!(result.is_err());
2539 }
2540}
2541
2542#[cfg(test)]
2543mod parse_transaction_and_count_operations_tests {
2544 use super::*;
2545 use crate::domain::transaction::stellar::test_helpers::{
2546 create_native_payment_operation, create_xdr_with_operations, TEST_PK, TEST_PK_2,
2547 };
2548 use serde_json::json;
2549
2550 fn create_test_xdr_with_operations(num_ops: usize) -> String {
2551 let payment_op = create_native_payment_operation(TEST_PK_2, 100);
2552 let operations = vec![payment_op; num_ops];
2553 create_xdr_with_operations(TEST_PK, operations, false)
2554 }
2555
2556 #[test]
2557 fn test_parse_xdr_string() {
2558 let xdr = create_test_xdr_with_operations(2);
2559 let json_value = json!(xdr);
2560
2561 let result = parse_transaction_and_count_operations(&json_value);
2562 assert!(result.is_ok());
2563 assert_eq!(result.unwrap(), 2);
2564 }
2565
2566 #[test]
2567 fn test_parse_operations_array() {
2568 let json_value = json!([
2569 {"type": "payment"},
2570 {"type": "payment"},
2571 {"type": "payment"}
2572 ]);
2573
2574 let result = parse_transaction_and_count_operations(&json_value);
2575 assert!(result.is_ok());
2576 assert_eq!(result.unwrap(), 3);
2577 }
2578
2579 #[test]
2580 fn test_parse_object_with_operations() {
2581 let json_value = json!({
2582 "operations": [
2583 {"type": "payment"},
2584 {"type": "payment"}
2585 ]
2586 });
2587
2588 let result = parse_transaction_and_count_operations(&json_value);
2589 assert!(result.is_ok());
2590 assert_eq!(result.unwrap(), 2);
2591 }
2592
2593 #[test]
2594 fn test_parse_object_with_transaction_xdr() {
2595 let xdr = create_test_xdr_with_operations(3);
2596 let json_value = json!({
2597 "transaction_xdr": xdr
2598 });
2599
2600 let result = parse_transaction_and_count_operations(&json_value);
2601 assert!(result.is_ok());
2602 assert_eq!(result.unwrap(), 3);
2603 }
2604
2605 #[test]
2606 fn test_parse_invalid_xdr() {
2607 let json_value = json!("INVALID_XDR");
2608
2609 let result = parse_transaction_and_count_operations(&json_value);
2610 assert!(result.is_err());
2611
2612 match result.unwrap_err() {
2613 StellarTransactionUtilsError::XdrParseFailed(_) => {}
2614 _ => panic!("Expected XdrParseFailed error"),
2615 }
2616 }
2617
2618 #[test]
2619 fn test_parse_invalid_format() {
2620 let json_value = json!(123);
2621
2622 let result = parse_transaction_and_count_operations(&json_value);
2623 assert!(result.is_err());
2624
2625 match result.unwrap_err() {
2626 StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2627 _ => panic!("Expected InvalidTransactionFormat error"),
2628 }
2629 }
2630
2631 #[test]
2632 fn test_parse_empty_operations() {
2633 let json_value = json!([]);
2634
2635 let result = parse_transaction_and_count_operations(&json_value);
2636 assert!(result.is_ok());
2637 assert_eq!(result.unwrap(), 0);
2638 }
2639}
2640
2641#[cfg(test)]
2642mod parse_transaction_envelope_tests {
2643 use super::*;
2644 use crate::domain::transaction::stellar::test_helpers::{
2645 create_unsigned_xdr, TEST_PK, TEST_PK_2,
2646 };
2647 use serde_json::json;
2648
2649 fn create_test_xdr() -> String {
2650 create_unsigned_xdr(TEST_PK, TEST_PK_2)
2651 }
2652
2653 #[test]
2654 fn test_parse_xdr_string() {
2655 let xdr = create_test_xdr();
2656 let json_value = json!(xdr);
2657
2658 let result = parse_transaction_envelope(&json_value);
2659 assert!(result.is_ok());
2660
2661 match result.unwrap() {
2662 TransactionEnvelope::Tx(_) => {}
2663 _ => panic!("Expected Tx envelope"),
2664 }
2665 }
2666
2667 #[test]
2668 fn test_parse_object_with_transaction_xdr() {
2669 let xdr = create_test_xdr();
2670 let json_value = json!({
2671 "transaction_xdr": xdr
2672 });
2673
2674 let result = parse_transaction_envelope(&json_value);
2675 assert!(result.is_ok());
2676
2677 match result.unwrap() {
2678 TransactionEnvelope::Tx(_) => {}
2679 _ => panic!("Expected Tx envelope"),
2680 }
2681 }
2682
2683 #[test]
2684 fn test_parse_invalid_xdr() {
2685 let json_value = json!("INVALID_XDR");
2686
2687 let result = parse_transaction_envelope(&json_value);
2688 assert!(result.is_err());
2689
2690 match result.unwrap_err() {
2691 StellarTransactionUtilsError::XdrParseFailed(_) => {}
2692 _ => panic!("Expected XdrParseFailed error"),
2693 }
2694 }
2695
2696 #[test]
2697 fn test_parse_invalid_format() {
2698 let json_value = json!(123);
2699
2700 let result = parse_transaction_envelope(&json_value);
2701 assert!(result.is_err());
2702
2703 match result.unwrap_err() {
2704 StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2705 _ => panic!("Expected InvalidTransactionFormat error"),
2706 }
2707 }
2708
2709 #[test]
2710 fn test_parse_object_without_xdr() {
2711 let json_value = json!({
2712 "some_field": "value"
2713 });
2714
2715 let result = parse_transaction_envelope(&json_value);
2716 assert!(result.is_err());
2717
2718 match result.unwrap_err() {
2719 StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2720 _ => panic!("Expected InvalidTransactionFormat error"),
2721 }
2722 }
2723}
2724
2725#[cfg(test)]
2726mod add_operation_to_envelope_tests {
2727 use super::*;
2728 use soroban_rs::xdr::{
2729 Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2730 Transaction, TransactionExt, TransactionV0, TransactionV0Envelope, TransactionV1Envelope,
2731 Uint256,
2732 };
2733
2734 fn create_payment_op() -> Operation {
2735 Operation {
2736 source_account: None,
2737 body: OperationBody::Payment(PaymentOp {
2738 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2739 asset: Asset::Native,
2740 amount: 100,
2741 }),
2742 }
2743 }
2744
2745 #[test]
2746 fn test_add_operation_to_tx_v0() {
2747 let payment_op = create_payment_op();
2748 let operations = vec![payment_op.clone()].try_into().unwrap();
2749
2750 let tx = TransactionV0 {
2751 source_account_ed25519: Uint256([0u8; 32]),
2752 fee: 100,
2753 seq_num: SequenceNumber(1),
2754 time_bounds: None,
2755 memo: Memo::None,
2756 operations,
2757 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2758 };
2759
2760 let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2761 tx,
2762 signatures: vec![].try_into().unwrap(),
2763 });
2764
2765 let new_op = create_payment_op();
2766 let result = add_operation_to_envelope(&mut envelope, new_op);
2767
2768 assert!(result.is_ok());
2769
2770 match envelope {
2771 TransactionEnvelope::TxV0(e) => {
2772 assert_eq!(e.tx.operations.len(), 2);
2773 assert_eq!(e.tx.fee, 200); }
2775 _ => panic!("Expected TxV0 envelope"),
2776 }
2777 }
2778
2779 #[test]
2780 fn test_add_operation_to_tx_v1() {
2781 let payment_op = create_payment_op();
2782 let operations = vec![payment_op.clone()].try_into().unwrap();
2783
2784 let tx = Transaction {
2785 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2786 fee: 100,
2787 seq_num: SequenceNumber(1),
2788 cond: Preconditions::None,
2789 memo: Memo::None,
2790 operations,
2791 ext: TransactionExt::V0,
2792 };
2793
2794 let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2795 tx,
2796 signatures: vec![].try_into().unwrap(),
2797 });
2798
2799 let new_op = create_payment_op();
2800 let result = add_operation_to_envelope(&mut envelope, new_op);
2801
2802 assert!(result.is_ok());
2803
2804 match envelope {
2805 TransactionEnvelope::Tx(e) => {
2806 assert_eq!(e.tx.operations.len(), 2);
2807 assert_eq!(e.tx.fee, 200); }
2809 _ => panic!("Expected Tx envelope"),
2810 }
2811 }
2812
2813 #[test]
2814 fn test_add_operation_to_fee_bump_fails() {
2815 let payment_op = create_payment_op();
2817 let operations = vec![payment_op].try_into().unwrap();
2818
2819 let tx = Transaction {
2820 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2821 fee: 100,
2822 seq_num: SequenceNumber(1),
2823 cond: Preconditions::None,
2824 memo: Memo::None,
2825 operations,
2826 ext: TransactionExt::V0,
2827 };
2828
2829 let inner_envelope = TransactionV1Envelope {
2830 tx,
2831 signatures: vec![].try_into().unwrap(),
2832 };
2833
2834 let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
2835
2836 let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
2837 fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
2838 fee: 200,
2839 inner_tx,
2840 ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
2841 };
2842
2843 let mut envelope =
2844 TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
2845 tx: fee_bump_tx,
2846 signatures: vec![].try_into().unwrap(),
2847 });
2848
2849 let new_op = create_payment_op();
2850 let result = add_operation_to_envelope(&mut envelope, new_op);
2851
2852 assert!(result.is_err());
2853
2854 match result.unwrap_err() {
2855 StellarTransactionUtilsError::CannotModifyFeeBump => {}
2856 _ => panic!("Expected CannotModifyFeeBump error"),
2857 }
2858 }
2859}
2860
2861#[cfg(test)]
2862mod extract_time_bounds_tests {
2863 use super::*;
2864 use soroban_rs::xdr::{
2865 Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2866 TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
2867 TransactionV1Envelope, Uint256,
2868 };
2869
2870 fn create_payment_op() -> Operation {
2871 Operation {
2872 source_account: None,
2873 body: OperationBody::Payment(PaymentOp {
2874 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2875 asset: Asset::Native,
2876 amount: 100,
2877 }),
2878 }
2879 }
2880
2881 #[test]
2882 fn test_extract_time_bounds_from_tx_v0_with_bounds() {
2883 let payment_op = create_payment_op();
2884 let operations = vec![payment_op].try_into().unwrap();
2885
2886 let time_bounds = TimeBounds {
2887 min_time: TimePoint(0),
2888 max_time: TimePoint(1000),
2889 };
2890
2891 let tx = TransactionV0 {
2892 source_account_ed25519: Uint256([0u8; 32]),
2893 fee: 100,
2894 seq_num: SequenceNumber(1),
2895 time_bounds: Some(time_bounds.clone()),
2896 memo: Memo::None,
2897 operations,
2898 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2899 };
2900
2901 let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2902 tx,
2903 signatures: vec![].try_into().unwrap(),
2904 });
2905
2906 let result = extract_time_bounds(&envelope);
2907 assert!(result.is_some());
2908
2909 let bounds = result.unwrap();
2910 assert_eq!(bounds.min_time.0, 0);
2911 assert_eq!(bounds.max_time.0, 1000);
2912 }
2913
2914 #[test]
2915 fn test_extract_time_bounds_from_tx_v0_without_bounds() {
2916 let payment_op = create_payment_op();
2917 let operations = vec![payment_op].try_into().unwrap();
2918
2919 let tx = TransactionV0 {
2920 source_account_ed25519: Uint256([0u8; 32]),
2921 fee: 100,
2922 seq_num: SequenceNumber(1),
2923 time_bounds: None,
2924 memo: Memo::None,
2925 operations,
2926 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2927 };
2928
2929 let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2930 tx,
2931 signatures: vec![].try_into().unwrap(),
2932 });
2933
2934 let result = extract_time_bounds(&envelope);
2935 assert!(result.is_none());
2936 }
2937
2938 #[test]
2939 fn test_extract_time_bounds_from_tx_v1_with_time_precondition() {
2940 let payment_op = create_payment_op();
2941 let operations = vec![payment_op].try_into().unwrap();
2942
2943 let time_bounds = TimeBounds {
2944 min_time: TimePoint(0),
2945 max_time: TimePoint(2000),
2946 };
2947
2948 let tx = Transaction {
2949 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2950 fee: 100,
2951 seq_num: SequenceNumber(1),
2952 cond: Preconditions::Time(time_bounds.clone()),
2953 memo: Memo::None,
2954 operations,
2955 ext: TransactionExt::V0,
2956 };
2957
2958 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2959 tx,
2960 signatures: vec![].try_into().unwrap(),
2961 });
2962
2963 let result = extract_time_bounds(&envelope);
2964 assert!(result.is_some());
2965
2966 let bounds = result.unwrap();
2967 assert_eq!(bounds.min_time.0, 0);
2968 assert_eq!(bounds.max_time.0, 2000);
2969 }
2970
2971 #[test]
2972 fn test_extract_time_bounds_from_tx_v1_without_time_precondition() {
2973 let payment_op = create_payment_op();
2974 let operations = vec![payment_op].try_into().unwrap();
2975
2976 let tx = Transaction {
2977 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2978 fee: 100,
2979 seq_num: SequenceNumber(1),
2980 cond: Preconditions::None,
2981 memo: Memo::None,
2982 operations,
2983 ext: TransactionExt::V0,
2984 };
2985
2986 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2987 tx,
2988 signatures: vec![].try_into().unwrap(),
2989 });
2990
2991 let result = extract_time_bounds(&envelope);
2992 assert!(result.is_none());
2993 }
2994
2995 #[test]
2996 fn test_extract_time_bounds_from_fee_bump() {
2997 let payment_op = create_payment_op();
2999 let operations = vec![payment_op].try_into().unwrap();
3000
3001 let time_bounds = TimeBounds {
3002 min_time: TimePoint(0),
3003 max_time: TimePoint(3000),
3004 };
3005
3006 let tx = Transaction {
3007 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
3008 fee: 100,
3009 seq_num: SequenceNumber(1),
3010 cond: Preconditions::Time(time_bounds.clone()),
3011 memo: Memo::None,
3012 operations,
3013 ext: TransactionExt::V0,
3014 };
3015
3016 let inner_envelope = TransactionV1Envelope {
3017 tx,
3018 signatures: vec![].try_into().unwrap(),
3019 };
3020
3021 let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
3022
3023 let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
3024 fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
3025 fee: 200,
3026 inner_tx,
3027 ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
3028 };
3029
3030 let envelope =
3031 TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
3032 tx: fee_bump_tx,
3033 signatures: vec![].try_into().unwrap(),
3034 });
3035
3036 let result = extract_time_bounds(&envelope);
3037 assert!(result.is_some());
3038
3039 let bounds = result.unwrap();
3040 assert_eq!(bounds.min_time.0, 0);
3041 assert_eq!(bounds.max_time.0, 3000);
3042 }
3043}
3044
3045#[cfg(test)]
3046mod set_time_bounds_tests {
3047 use super::*;
3048 use chrono::Utc;
3049 use soroban_rs::xdr::{
3050 Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
3051 TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
3052 TransactionV1Envelope, Uint256,
3053 };
3054
3055 fn create_payment_op() -> Operation {
3056 Operation {
3057 source_account: None,
3058 body: OperationBody::Payment(PaymentOp {
3059 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
3060 asset: Asset::Native,
3061 amount: 100,
3062 }),
3063 }
3064 }
3065
3066 #[test]
3067 fn test_set_time_bounds_on_tx_v0() {
3068 let payment_op = create_payment_op();
3069 let operations = vec![payment_op].try_into().unwrap();
3070
3071 let tx = TransactionV0 {
3072 source_account_ed25519: Uint256([0u8; 32]),
3073 fee: 100,
3074 seq_num: SequenceNumber(1),
3075 time_bounds: None,
3076 memo: Memo::None,
3077 operations,
3078 ext: soroban_rs::xdr::TransactionV0Ext::V0,
3079 };
3080
3081 let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
3082 tx,
3083 signatures: vec![].try_into().unwrap(),
3084 });
3085
3086 let valid_until = Utc::now() + chrono::Duration::seconds(300);
3087 let result = set_time_bounds(&mut envelope, valid_until);
3088
3089 assert!(result.is_ok());
3090
3091 match envelope {
3092 TransactionEnvelope::TxV0(e) => {
3093 assert!(e.tx.time_bounds.is_some());
3094 let bounds = e.tx.time_bounds.unwrap();
3095 assert_eq!(bounds.min_time.0, 0);
3096 assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3097 }
3098 _ => panic!("Expected TxV0 envelope"),
3099 }
3100 }
3101
3102 #[test]
3103 fn test_set_time_bounds_on_tx_v1() {
3104 let payment_op = create_payment_op();
3105 let operations = vec![payment_op].try_into().unwrap();
3106
3107 let tx = Transaction {
3108 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
3109 fee: 100,
3110 seq_num: SequenceNumber(1),
3111 cond: Preconditions::None,
3112 memo: Memo::None,
3113 operations,
3114 ext: TransactionExt::V0,
3115 };
3116
3117 let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
3118 tx,
3119 signatures: vec![].try_into().unwrap(),
3120 });
3121
3122 let valid_until = Utc::now() + chrono::Duration::seconds(300);
3123 let result = set_time_bounds(&mut envelope, valid_until);
3124
3125 assert!(result.is_ok());
3126
3127 match envelope {
3128 TransactionEnvelope::Tx(e) => match e.tx.cond {
3129 Preconditions::Time(bounds) => {
3130 assert_eq!(bounds.min_time.0, 0);
3131 assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3132 }
3133 _ => panic!("Expected Time precondition"),
3134 },
3135 _ => panic!("Expected Tx envelope"),
3136 }
3137 }
3138
3139 #[test]
3140 fn test_set_time_bounds_on_fee_bump_fails() {
3141 let payment_op = create_payment_op();
3143 let operations = vec![payment_op].try_into().unwrap();
3144
3145 let tx = Transaction {
3146 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
3147 fee: 100,
3148 seq_num: SequenceNumber(1),
3149 cond: Preconditions::None,
3150 memo: Memo::None,
3151 operations,
3152 ext: TransactionExt::V0,
3153 };
3154
3155 let inner_envelope = TransactionV1Envelope {
3156 tx,
3157 signatures: vec![].try_into().unwrap(),
3158 };
3159
3160 let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
3161
3162 let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
3163 fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
3164 fee: 200,
3165 inner_tx,
3166 ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
3167 };
3168
3169 let mut envelope =
3170 TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
3171 tx: fee_bump_tx,
3172 signatures: vec![].try_into().unwrap(),
3173 });
3174
3175 let valid_until = Utc::now() + chrono::Duration::seconds(300);
3176 let result = set_time_bounds(&mut envelope, valid_until);
3177
3178 assert!(result.is_err());
3179
3180 match result.unwrap_err() {
3181 StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump => {}
3182 _ => panic!("Expected CannotSetTimeBoundsOnFeeBump error"),
3183 }
3184 }
3185
3186 #[test]
3187 fn test_set_time_bounds_replaces_existing() {
3188 let payment_op = create_payment_op();
3189 let operations = vec![payment_op].try_into().unwrap();
3190
3191 let old_time_bounds = TimeBounds {
3192 min_time: TimePoint(100),
3193 max_time: TimePoint(1000),
3194 };
3195
3196 let tx = TransactionV0 {
3197 source_account_ed25519: Uint256([0u8; 32]),
3198 fee: 100,
3199 seq_num: SequenceNumber(1),
3200 time_bounds: Some(old_time_bounds),
3201 memo: Memo::None,
3202 operations,
3203 ext: soroban_rs::xdr::TransactionV0Ext::V0,
3204 };
3205
3206 let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
3207 tx,
3208 signatures: vec![].try_into().unwrap(),
3209 });
3210
3211 let valid_until = Utc::now() + chrono::Duration::seconds(300);
3212 let result = set_time_bounds(&mut envelope, valid_until);
3213
3214 assert!(result.is_ok());
3215
3216 match envelope {
3217 TransactionEnvelope::TxV0(e) => {
3218 assert!(e.tx.time_bounds.is_some());
3219 let bounds = e.tx.time_bounds.unwrap();
3220 assert_eq!(bounds.min_time.0, 0);
3222 assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3223 }
3224 _ => panic!("Expected TxV0 envelope"),
3225 }
3226 }
3227}
3228
3229#[cfg(test)]
3234mod stellar_transaction_utils_error_conversion_tests {
3235 use super::*;
3236
3237 #[test]
3238 fn test_v0_transactions_not_supported_converts_to_validation_error() {
3239 let err = StellarTransactionUtilsError::V0TransactionsNotSupported;
3240 let relayer_err: RelayerError = err.into();
3241 match relayer_err {
3242 RelayerError::ValidationError(msg) => {
3243 assert_eq!(msg, "V0 transactions are not supported");
3244 }
3245 _ => panic!("Expected ValidationError"),
3246 }
3247 }
3248
3249 #[test]
3250 fn test_cannot_update_sequence_on_fee_bump_converts_to_validation_error() {
3251 let err = StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump;
3252 let relayer_err: RelayerError = err.into();
3253 match relayer_err {
3254 RelayerError::ValidationError(msg) => {
3255 assert_eq!(msg, "Cannot update sequence number on fee bump transaction");
3256 }
3257 _ => panic!("Expected ValidationError"),
3258 }
3259 }
3260
3261 #[test]
3262 fn test_cannot_set_time_bounds_on_fee_bump_converts_to_validation_error() {
3263 let err = StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump;
3264 let relayer_err: RelayerError = err.into();
3265 match relayer_err {
3266 RelayerError::ValidationError(msg) => {
3267 assert_eq!(msg, "Cannot set time bounds on fee-bump transactions");
3268 }
3269 _ => panic!("Expected ValidationError"),
3270 }
3271 }
3272
3273 #[test]
3274 fn test_invalid_transaction_format_converts_to_validation_error() {
3275 let err = StellarTransactionUtilsError::InvalidTransactionFormat("bad format".to_string());
3276 let relayer_err: RelayerError = err.into();
3277 match relayer_err {
3278 RelayerError::ValidationError(msg) => {
3279 assert_eq!(msg, "bad format");
3280 }
3281 _ => panic!("Expected ValidationError"),
3282 }
3283 }
3284
3285 #[test]
3286 fn test_cannot_modify_fee_bump_converts_to_validation_error() {
3287 let err = StellarTransactionUtilsError::CannotModifyFeeBump;
3288 let relayer_err: RelayerError = err.into();
3289 match relayer_err {
3290 RelayerError::ValidationError(msg) => {
3291 assert_eq!(msg, "Cannot add operations to fee-bump transactions");
3292 }
3293 _ => panic!("Expected ValidationError"),
3294 }
3295 }
3296
3297 #[test]
3298 fn test_too_many_operations_converts_to_validation_error() {
3299 let err = StellarTransactionUtilsError::TooManyOperations(100);
3300 let relayer_err: RelayerError = err.into();
3301 match relayer_err {
3302 RelayerError::ValidationError(msg) => {
3303 assert!(msg.contains("Too many operations"));
3304 assert!(msg.contains("100"));
3305 }
3306 _ => panic!("Expected ValidationError"),
3307 }
3308 }
3309
3310 #[test]
3311 fn test_sequence_overflow_converts_to_internal_error() {
3312 let err = StellarTransactionUtilsError::SequenceOverflow("overflow msg".to_string());
3313 let relayer_err: RelayerError = err.into();
3314 match relayer_err {
3315 RelayerError::Internal(msg) => {
3316 assert_eq!(msg, "overflow msg");
3317 }
3318 _ => panic!("Expected Internal error"),
3319 }
3320 }
3321
3322 #[test]
3323 fn test_simulation_no_results_converts_to_internal_error() {
3324 let err = StellarTransactionUtilsError::SimulationNoResults;
3325 let relayer_err: RelayerError = err.into();
3326 match relayer_err {
3327 RelayerError::Internal(msg) => {
3328 assert!(msg.contains("no results"));
3329 }
3330 _ => panic!("Expected Internal error"),
3331 }
3332 }
3333
3334 #[test]
3335 fn test_asset_code_too_long_converts_to_validation_error() {
3336 let err =
3337 StellarTransactionUtilsError::AssetCodeTooLong(12, "VERYLONGASSETCODE".to_string());
3338 let relayer_err: RelayerError = err.into();
3339 match relayer_err {
3340 RelayerError::ValidationError(msg) => {
3341 assert!(msg.contains("Asset code too long"));
3342 assert!(msg.contains("12"));
3343 }
3344 _ => panic!("Expected ValidationError"),
3345 }
3346 }
3347
3348 #[test]
3349 fn test_invalid_asset_format_converts_to_validation_error() {
3350 let err = StellarTransactionUtilsError::InvalidAssetFormat("bad asset".to_string());
3351 let relayer_err: RelayerError = err.into();
3352 match relayer_err {
3353 RelayerError::ValidationError(msg) => {
3354 assert_eq!(msg, "bad asset");
3355 }
3356 _ => panic!("Expected ValidationError"),
3357 }
3358 }
3359
3360 #[test]
3361 fn test_invalid_account_address_converts_to_internal_error() {
3362 let err = StellarTransactionUtilsError::InvalidAccountAddress(
3363 "GABC".to_string(),
3364 "parse error".to_string(),
3365 );
3366 let relayer_err: RelayerError = err.into();
3367 match relayer_err {
3368 RelayerError::Internal(msg) => {
3369 assert_eq!(msg, "parse error");
3370 }
3371 _ => panic!("Expected Internal error"),
3372 }
3373 }
3374
3375 #[test]
3376 fn test_invalid_contract_address_converts_to_internal_error() {
3377 let err = StellarTransactionUtilsError::InvalidContractAddress(
3378 "CABC".to_string(),
3379 "contract parse error".to_string(),
3380 );
3381 let relayer_err: RelayerError = err.into();
3382 match relayer_err {
3383 RelayerError::Internal(msg) => {
3384 assert_eq!(msg, "contract parse error");
3385 }
3386 _ => panic!("Expected Internal error"),
3387 }
3388 }
3389
3390 #[test]
3391 fn test_symbol_creation_failed_converts_to_internal_error() {
3392 let err = StellarTransactionUtilsError::SymbolCreationFailed(
3393 "Balance".to_string(),
3394 "too long".to_string(),
3395 );
3396 let relayer_err: RelayerError = err.into();
3397 match relayer_err {
3398 RelayerError::Internal(msg) => {
3399 assert_eq!(msg, "too long");
3400 }
3401 _ => panic!("Expected Internal error"),
3402 }
3403 }
3404
3405 #[test]
3406 fn test_key_vector_creation_failed_converts_to_internal_error() {
3407 let err = StellarTransactionUtilsError::KeyVectorCreationFailed(
3408 "Balance".to_string(),
3409 "vec error".to_string(),
3410 );
3411 let relayer_err: RelayerError = err.into();
3412 match relayer_err {
3413 RelayerError::Internal(msg) => {
3414 assert_eq!(msg, "vec error");
3415 }
3416 _ => panic!("Expected Internal error"),
3417 }
3418 }
3419
3420 #[test]
3421 fn test_contract_data_query_persistent_failed_converts_to_internal_error() {
3422 let err = StellarTransactionUtilsError::ContractDataQueryPersistentFailed(
3423 "balance".to_string(),
3424 "rpc error".to_string(),
3425 );
3426 let relayer_err: RelayerError = err.into();
3427 match relayer_err {
3428 RelayerError::Internal(msg) => {
3429 assert_eq!(msg, "rpc error");
3430 }
3431 _ => panic!("Expected Internal error"),
3432 }
3433 }
3434
3435 #[test]
3436 fn test_contract_data_query_temporary_failed_converts_to_internal_error() {
3437 let err = StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(
3438 "balance".to_string(),
3439 "temp error".to_string(),
3440 );
3441 let relayer_err: RelayerError = err.into();
3442 match relayer_err {
3443 RelayerError::Internal(msg) => {
3444 assert_eq!(msg, "temp error");
3445 }
3446 _ => panic!("Expected Internal error"),
3447 }
3448 }
3449
3450 #[test]
3451 fn test_ledger_entry_parse_failed_converts_to_internal_error() {
3452 let err = StellarTransactionUtilsError::LedgerEntryParseFailed(
3453 "entry".to_string(),
3454 "xdr error".to_string(),
3455 );
3456 let relayer_err: RelayerError = err.into();
3457 match relayer_err {
3458 RelayerError::Internal(msg) => {
3459 assert_eq!(msg, "xdr error");
3460 }
3461 _ => panic!("Expected Internal error"),
3462 }
3463 }
3464
3465 #[test]
3466 fn test_no_entries_found_converts_to_validation_error() {
3467 let err = StellarTransactionUtilsError::NoEntriesFound("balance".to_string());
3468 let relayer_err: RelayerError = err.into();
3469 match relayer_err {
3470 RelayerError::ValidationError(msg) => {
3471 assert!(msg.contains("No entries found"));
3472 }
3473 _ => panic!("Expected ValidationError"),
3474 }
3475 }
3476
3477 #[test]
3478 fn test_empty_entries_converts_to_validation_error() {
3479 let err = StellarTransactionUtilsError::EmptyEntries("balance".to_string());
3480 let relayer_err: RelayerError = err.into();
3481 match relayer_err {
3482 RelayerError::ValidationError(msg) => {
3483 assert!(msg.contains("Empty entries"));
3484 }
3485 _ => panic!("Expected ValidationError"),
3486 }
3487 }
3488
3489 #[test]
3490 fn test_unexpected_ledger_entry_type_converts_to_validation_error() {
3491 let err = StellarTransactionUtilsError::UnexpectedLedgerEntryType("balance".to_string());
3492 let relayer_err: RelayerError = err.into();
3493 match relayer_err {
3494 RelayerError::ValidationError(msg) => {
3495 assert!(msg.contains("Unexpected ledger entry type"));
3496 }
3497 _ => panic!("Expected ValidationError"),
3498 }
3499 }
3500
3501 #[test]
3502 fn test_invalid_issuer_length_converts_to_validation_error() {
3503 let err = StellarTransactionUtilsError::InvalidIssuerLength(56, "SHORT".to_string());
3504 let relayer_err: RelayerError = err.into();
3505 match relayer_err {
3506 RelayerError::ValidationError(msg) => {
3507 assert!(msg.contains("56"));
3508 assert!(msg.contains("SHORT"));
3509 }
3510 _ => panic!("Expected ValidationError"),
3511 }
3512 }
3513
3514 #[test]
3515 fn test_invalid_issuer_prefix_converts_to_validation_error() {
3516 let err = StellarTransactionUtilsError::InvalidIssuerPrefix('G', "CABC123".to_string());
3517 let relayer_err: RelayerError = err.into();
3518 match relayer_err {
3519 RelayerError::ValidationError(msg) => {
3520 assert!(msg.contains("'G'"));
3521 assert!(msg.contains("CABC123"));
3522 }
3523 _ => panic!("Expected ValidationError"),
3524 }
3525 }
3526
3527 #[test]
3528 fn test_account_fetch_failed_converts_to_provider_error() {
3529 let err = StellarTransactionUtilsError::AccountFetchFailed("fetch error".to_string());
3530 let relayer_err: RelayerError = err.into();
3531 match relayer_err {
3532 RelayerError::ProviderError(msg) => {
3533 assert_eq!(msg, "fetch error");
3534 }
3535 _ => panic!("Expected ProviderError"),
3536 }
3537 }
3538
3539 #[test]
3540 fn test_trustline_query_failed_converts_to_provider_error() {
3541 let err = StellarTransactionUtilsError::TrustlineQueryFailed(
3542 "USDC".to_string(),
3543 "rpc fail".to_string(),
3544 );
3545 let relayer_err: RelayerError = err.into();
3546 match relayer_err {
3547 RelayerError::ProviderError(msg) => {
3548 assert_eq!(msg, "rpc fail");
3549 }
3550 _ => panic!("Expected ProviderError"),
3551 }
3552 }
3553
3554 #[test]
3555 fn test_contract_invocation_failed_converts_to_provider_error() {
3556 let err = StellarTransactionUtilsError::ContractInvocationFailed(
3557 "transfer".to_string(),
3558 "invoke error".to_string(),
3559 );
3560 let relayer_err: RelayerError = err.into();
3561 match relayer_err {
3562 RelayerError::ProviderError(msg) => {
3563 assert_eq!(msg, "invoke error");
3564 }
3565 _ => panic!("Expected ProviderError"),
3566 }
3567 }
3568
3569 #[test]
3570 fn test_xdr_parse_failed_converts_to_internal_error() {
3571 let err = StellarTransactionUtilsError::XdrParseFailed("xdr parse fail".to_string());
3572 let relayer_err: RelayerError = err.into();
3573 match relayer_err {
3574 RelayerError::Internal(msg) => {
3575 assert_eq!(msg, "xdr parse fail");
3576 }
3577 _ => panic!("Expected Internal error"),
3578 }
3579 }
3580
3581 #[test]
3582 fn test_operation_extraction_failed_converts_to_internal_error() {
3583 let err =
3584 StellarTransactionUtilsError::OperationExtractionFailed("extract fail".to_string());
3585 let relayer_err: RelayerError = err.into();
3586 match relayer_err {
3587 RelayerError::Internal(msg) => {
3588 assert_eq!(msg, "extract fail");
3589 }
3590 _ => panic!("Expected Internal error"),
3591 }
3592 }
3593
3594 #[test]
3595 fn test_simulation_failed_converts_to_internal_error() {
3596 let err = StellarTransactionUtilsError::SimulationFailed("sim error".to_string());
3597 let relayer_err: RelayerError = err.into();
3598 match relayer_err {
3599 RelayerError::Internal(msg) => {
3600 assert_eq!(msg, "sim error");
3601 }
3602 _ => panic!("Expected Internal error"),
3603 }
3604 }
3605
3606 #[test]
3607 fn test_simulation_check_failed_converts_to_internal_error() {
3608 let err = StellarTransactionUtilsError::SimulationCheckFailed("check fail".to_string());
3609 let relayer_err: RelayerError = err.into();
3610 match relayer_err {
3611 RelayerError::Internal(msg) => {
3612 assert_eq!(msg, "check fail");
3613 }
3614 _ => panic!("Expected Internal error"),
3615 }
3616 }
3617
3618 #[test]
3619 fn test_dex_quote_failed_converts_to_internal_error() {
3620 let err = StellarTransactionUtilsError::DexQuoteFailed("dex error".to_string());
3621 let relayer_err: RelayerError = err.into();
3622 match relayer_err {
3623 RelayerError::Internal(msg) => {
3624 assert_eq!(msg, "dex error");
3625 }
3626 _ => panic!("Expected Internal error"),
3627 }
3628 }
3629
3630 #[test]
3631 fn test_empty_asset_code_converts_to_validation_error() {
3632 let err = StellarTransactionUtilsError::EmptyAssetCode("CODE:ISSUER".to_string());
3633 let relayer_err: RelayerError = err.into();
3634 match relayer_err {
3635 RelayerError::ValidationError(msg) => {
3636 assert!(msg.contains("Asset code cannot be empty"));
3637 }
3638 _ => panic!("Expected ValidationError"),
3639 }
3640 }
3641
3642 #[test]
3643 fn test_empty_issuer_address_converts_to_validation_error() {
3644 let err = StellarTransactionUtilsError::EmptyIssuerAddress("USDC:".to_string());
3645 let relayer_err: RelayerError = err.into();
3646 match relayer_err {
3647 RelayerError::ValidationError(msg) => {
3648 assert!(msg.contains("Issuer address cannot be empty"));
3649 }
3650 _ => panic!("Expected ValidationError"),
3651 }
3652 }
3653
3654 #[test]
3655 fn test_no_trustline_found_converts_to_validation_error() {
3656 let err =
3657 StellarTransactionUtilsError::NoTrustlineFound("USDC".to_string(), "GABC".to_string());
3658 let relayer_err: RelayerError = err.into();
3659 match relayer_err {
3660 RelayerError::ValidationError(msg) => {
3661 assert!(msg.contains("No trustline found"));
3662 }
3663 _ => panic!("Expected ValidationError"),
3664 }
3665 }
3666
3667 #[test]
3668 fn test_unsupported_trustline_version_converts_to_validation_error() {
3669 let err = StellarTransactionUtilsError::UnsupportedTrustlineVersion;
3670 let relayer_err: RelayerError = err.into();
3671 match relayer_err {
3672 RelayerError::ValidationError(msg) => {
3673 assert!(msg.contains("Unsupported trustline"));
3674 }
3675 _ => panic!("Expected ValidationError"),
3676 }
3677 }
3678
3679 #[test]
3680 fn test_unexpected_trustline_entry_type_converts_to_validation_error() {
3681 let err = StellarTransactionUtilsError::UnexpectedTrustlineEntryType;
3682 let relayer_err: RelayerError = err.into();
3683 match relayer_err {
3684 RelayerError::ValidationError(msg) => {
3685 assert!(msg.contains("Unexpected ledger entry type"));
3686 }
3687 _ => panic!("Expected ValidationError"),
3688 }
3689 }
3690
3691 #[test]
3692 fn test_balance_too_large_converts_to_validation_error() {
3693 let err = StellarTransactionUtilsError::BalanceTooLarge(1, 999);
3694 let relayer_err: RelayerError = err.into();
3695 match relayer_err {
3696 RelayerError::ValidationError(msg) => {
3697 assert!(msg.contains("Balance too large"));
3698 }
3699 _ => panic!("Expected ValidationError"),
3700 }
3701 }
3702
3703 #[test]
3704 fn test_negative_balance_i128_converts_to_validation_error() {
3705 let err = StellarTransactionUtilsError::NegativeBalanceI128(42);
3706 let relayer_err: RelayerError = err.into();
3707 match relayer_err {
3708 RelayerError::ValidationError(msg) => {
3709 assert!(msg.contains("Negative balance"));
3710 }
3711 _ => panic!("Expected ValidationError"),
3712 }
3713 }
3714
3715 #[test]
3716 fn test_negative_balance_i64_converts_to_validation_error() {
3717 let err = StellarTransactionUtilsError::NegativeBalanceI64(-5);
3718 let relayer_err: RelayerError = err.into();
3719 match relayer_err {
3720 RelayerError::ValidationError(msg) => {
3721 assert!(msg.contains("Negative balance"));
3722 }
3723 _ => panic!("Expected ValidationError"),
3724 }
3725 }
3726
3727 #[test]
3728 fn test_unexpected_balance_type_converts_to_validation_error() {
3729 let err = StellarTransactionUtilsError::UnexpectedBalanceType("Bool(true)".to_string());
3730 let relayer_err: RelayerError = err.into();
3731 match relayer_err {
3732 RelayerError::ValidationError(msg) => {
3733 assert!(msg.contains("Unexpected balance value type"));
3734 }
3735 _ => panic!("Expected ValidationError"),
3736 }
3737 }
3738
3739 #[test]
3740 fn test_unexpected_contract_data_entry_type_converts_to_validation_error() {
3741 let err = StellarTransactionUtilsError::UnexpectedContractDataEntryType;
3742 let relayer_err: RelayerError = err.into();
3743 match relayer_err {
3744 RelayerError::ValidationError(msg) => {
3745 assert!(msg.contains("Unexpected ledger entry type"));
3746 }
3747 _ => panic!("Expected ValidationError"),
3748 }
3749 }
3750
3751 #[test]
3752 fn test_native_asset_in_trustline_query_converts_to_validation_error() {
3753 let err = StellarTransactionUtilsError::NativeAssetInTrustlineQuery;
3754 let relayer_err: RelayerError = err.into();
3755 match relayer_err {
3756 RelayerError::ValidationError(msg) => {
3757 assert!(msg.contains("Native asset"));
3758 }
3759 _ => panic!("Expected ValidationError"),
3760 }
3761 }
3762}
3763
3764#[cfg(test)]
3765mod compute_resubmit_backoff_interval_tests {
3766 use super::compute_resubmit_backoff_interval;
3767 use chrono::Duration;
3768
3769 const BASE: i64 = 10;
3770 const MAX: i64 = 120;
3771
3772 #[test]
3773 fn returns_none_below_base() {
3774 assert!(compute_resubmit_backoff_interval(Duration::seconds(0), BASE, MAX).is_none());
3775 assert!(compute_resubmit_backoff_interval(Duration::seconds(5), BASE, MAX).is_none());
3776 assert!(compute_resubmit_backoff_interval(Duration::seconds(9), BASE, MAX).is_none());
3777 }
3778
3779 #[test]
3780 fn base_interval_at_1x() {
3781 assert_eq!(
3783 compute_resubmit_backoff_interval(Duration::seconds(10), BASE, MAX),
3784 Some(Duration::seconds(10))
3785 );
3786 assert_eq!(
3787 compute_resubmit_backoff_interval(Duration::seconds(19), BASE, MAX),
3788 Some(Duration::seconds(10))
3789 );
3790 }
3791
3792 #[test]
3793 fn doubles_at_2x() {
3794 assert_eq!(
3796 compute_resubmit_backoff_interval(Duration::seconds(20), BASE, MAX),
3797 Some(Duration::seconds(20))
3798 );
3799 assert_eq!(
3800 compute_resubmit_backoff_interval(Duration::seconds(39), BASE, MAX),
3801 Some(Duration::seconds(20))
3802 );
3803 }
3804
3805 #[test]
3806 fn quadruples_at_4x() {
3807 assert_eq!(
3809 compute_resubmit_backoff_interval(Duration::seconds(40), BASE, MAX),
3810 Some(Duration::seconds(40))
3811 );
3812 assert_eq!(
3813 compute_resubmit_backoff_interval(Duration::seconds(79), BASE, MAX),
3814 Some(Duration::seconds(40))
3815 );
3816 }
3817
3818 #[test]
3819 fn interval_at_8x() {
3820 assert_eq!(
3822 compute_resubmit_backoff_interval(Duration::seconds(80), BASE, MAX),
3823 Some(Duration::seconds(80))
3824 );
3825 assert_eq!(
3826 compute_resubmit_backoff_interval(Duration::seconds(119), BASE, MAX),
3827 Some(Duration::seconds(80))
3828 );
3829 }
3830
3831 #[test]
3832 fn capped_at_max() {
3833 assert_eq!(
3835 compute_resubmit_backoff_interval(Duration::seconds(160), BASE, MAX),
3836 Some(Duration::seconds(MAX))
3837 );
3838 assert_eq!(
3840 compute_resubmit_backoff_interval(Duration::seconds(1280), BASE, MAX),
3841 Some(Duration::seconds(MAX))
3842 );
3843 }
3844
3845 #[test]
3846 fn works_with_different_base_and_max() {
3847 let base = 5;
3849 let max = 30;
3850 assert_eq!(
3852 compute_resubmit_backoff_interval(Duration::seconds(5), base, max),
3853 Some(Duration::seconds(5))
3854 );
3855 assert_eq!(
3857 compute_resubmit_backoff_interval(Duration::seconds(10), base, max),
3858 Some(Duration::seconds(10))
3859 );
3860 assert_eq!(
3862 compute_resubmit_backoff_interval(Duration::seconds(40), base, max),
3863 Some(Duration::seconds(30))
3864 );
3865 }
3866}