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(
1318 total_age: chrono::Duration,
1319 base_interval_secs: i64,
1320 max_interval_secs: i64,
1321 growth_factor: f64,
1322) -> Option<chrono::Duration> {
1323 let age_secs = total_age.num_seconds();
1324
1325 if age_secs < base_interval_secs || base_interval_secs <= 0 || max_interval_secs <= 0 {
1326 return None;
1327 }
1328
1329 if growth_factor <= 1.0 {
1331 return Some(chrono::Duration::seconds(
1332 base_interval_secs.min(max_interval_secs),
1333 ));
1334 }
1335
1336 let mut interval = base_interval_secs as f64;
1339 let mut tier_end = base_interval_secs as f64 * growth_factor;
1340
1341 while tier_end <= age_secs as f64 {
1342 interval = (interval * growth_factor).min(max_interval_secs as f64);
1343 tier_end *= growth_factor;
1344 }
1345
1346 let capped = (interval as i64).min(max_interval_secs);
1347 Some(chrono::Duration::seconds(capped))
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352 use super::*;
1353 use crate::domain::transaction::stellar::test_helpers::TEST_PK;
1354 use crate::models::AssetSpec;
1355 use crate::models::{AuthSpec, ContractSource, WasmSource};
1356
1357 fn payment_op(destination: &str) -> OperationSpec {
1358 OperationSpec::Payment {
1359 destination: destination.to_string(),
1360 amount: 100,
1361 asset: AssetSpec::Native,
1362 }
1363 }
1364
1365 #[test]
1366 fn returns_false_for_only_payment_ops() {
1367 let ops = vec![payment_op(TEST_PK)];
1368 assert!(!needs_simulation(&ops));
1369 }
1370
1371 #[test]
1372 fn returns_true_for_invoke_contract_ops() {
1373 let ops = vec![OperationSpec::InvokeContract {
1374 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1375 .to_string(),
1376 function_name: "transfer".to_string(),
1377 args: vec![],
1378 auth: None,
1379 }];
1380 assert!(needs_simulation(&ops));
1381 }
1382
1383 #[test]
1384 fn returns_true_for_upload_wasm_ops() {
1385 let ops = vec![OperationSpec::UploadWasm {
1386 wasm: WasmSource::Hex {
1387 hex: "deadbeef".to_string(),
1388 },
1389 auth: None,
1390 }];
1391 assert!(needs_simulation(&ops));
1392 }
1393
1394 #[test]
1395 fn returns_true_for_create_contract_ops() {
1396 let ops = vec![OperationSpec::CreateContract {
1397 source: ContractSource::Address {
1398 address: TEST_PK.to_string(),
1399 },
1400 wasm_hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
1401 .to_string(),
1402 salt: None,
1403 constructor_args: None,
1404 auth: None,
1405 }];
1406 assert!(needs_simulation(&ops));
1407 }
1408
1409 #[test]
1410 fn returns_true_for_single_invoke_host_function() {
1411 let ops = vec![OperationSpec::InvokeContract {
1412 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1413 .to_string(),
1414 function_name: "transfer".to_string(),
1415 args: vec![],
1416 auth: Some(AuthSpec::SourceAccount),
1417 }];
1418 assert!(needs_simulation(&ops));
1419 }
1420
1421 #[test]
1422 fn returns_false_for_multiple_payment_ops() {
1423 let ops = vec![payment_op(TEST_PK), payment_op(TEST_PK)];
1424 assert!(!needs_simulation(&ops));
1425 }
1426
1427 mod next_sequence_u64_tests {
1428 use super::*;
1429
1430 #[test]
1431 fn test_increment() {
1432 assert_eq!(next_sequence_u64(0).unwrap(), 1);
1433
1434 assert_eq!(next_sequence_u64(12345).unwrap(), 12346);
1435 }
1436
1437 #[test]
1438 fn test_error_path_overflow_i64_max() {
1439 let result = next_sequence_u64(i64::MAX);
1440 assert!(result.is_err());
1441 match result.unwrap_err() {
1442 RelayerError::ProviderError(msg) => assert_eq!(msg, "sequence overflow"),
1443 _ => panic!("Unexpected error type"),
1444 }
1445 }
1446 }
1447
1448 mod i64_from_u64_tests {
1449 use super::*;
1450
1451 #[test]
1452 fn test_happy_path_conversion() {
1453 assert_eq!(i64_from_u64(0).unwrap(), 0);
1454 assert_eq!(i64_from_u64(12345).unwrap(), 12345);
1455 assert_eq!(i64_from_u64(i64::MAX as u64).unwrap(), i64::MAX);
1456 }
1457
1458 #[test]
1459 fn test_error_path_overflow_u64_max() {
1460 let result = i64_from_u64(u64::MAX);
1461 assert!(result.is_err());
1462 match result.unwrap_err() {
1463 RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1464 _ => panic!("Unexpected error type"),
1465 }
1466 }
1467
1468 #[test]
1469 fn test_edge_case_just_above_i64_max() {
1470 let value = (i64::MAX as u64) + 1;
1472 let result = i64_from_u64(value);
1473 assert!(result.is_err());
1474 match result.unwrap_err() {
1475 RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1476 _ => panic!("Unexpected error type"),
1477 }
1478 }
1479 }
1480
1481 mod is_bad_sequence_error_tests {
1482 use super::*;
1483
1484 #[test]
1485 fn test_detects_txbadseq() {
1486 assert!(is_bad_sequence_error(
1487 "Failed to send transaction: transaction submission failed: TxBadSeq"
1488 ));
1489 assert!(is_bad_sequence_error("Error: TxBadSeq"));
1490 assert!(is_bad_sequence_error("txbadseq"));
1491 assert!(is_bad_sequence_error("TXBADSEQ"));
1492 }
1493
1494 #[test]
1495 fn test_returns_false_for_other_errors() {
1496 assert!(!is_bad_sequence_error("network timeout"));
1497 assert!(!is_bad_sequence_error("insufficient balance"));
1498 assert!(!is_bad_sequence_error("tx_insufficient_fee"));
1499 assert!(!is_bad_sequence_error("bad_auth"));
1500 assert!(!is_bad_sequence_error(""));
1501 }
1502 }
1503
1504 mod decode_tx_result_code_tests {
1505 use super::*;
1506 use soroban_rs::xdr::{TransactionResult, TransactionResultResult, WriteXdr};
1507
1508 #[test]
1509 fn test_decodes_tx_bad_seq() {
1510 let result = TransactionResult {
1511 fee_charged: 100,
1512 result: TransactionResultResult::TxBadSeq,
1513 ext: soroban_rs::xdr::TransactionResultExt::V0,
1514 };
1515 let xdr = result.to_xdr_base64(Limits::none()).unwrap();
1516 assert_eq!(decode_tx_result_code(&xdr), Some("TxBadSeq".to_string()));
1517 }
1518
1519 #[test]
1520 fn test_decodes_tx_insufficient_balance() {
1521 let result = TransactionResult {
1522 fee_charged: 100,
1523 result: TransactionResultResult::TxInsufficientBalance,
1524 ext: soroban_rs::xdr::TransactionResultExt::V0,
1525 };
1526 let xdr = result.to_xdr_base64(Limits::none()).unwrap();
1527 assert_eq!(
1528 decode_tx_result_code(&xdr),
1529 Some("TxInsufficientBalance".to_string())
1530 );
1531 }
1532
1533 #[test]
1534 fn test_returns_none_for_invalid_xdr() {
1535 assert_eq!(decode_tx_result_code("not-valid-xdr"), None);
1536 }
1537
1538 #[test]
1539 fn test_returns_none_for_empty_string() {
1540 assert_eq!(decode_tx_result_code(""), None);
1541 }
1542 }
1543
1544 mod is_insufficient_fee_error_tests {
1545 use super::*;
1546
1547 #[test]
1548 fn test_detects_txinsufficientfee() {
1549 assert!(is_insufficient_fee_error("TxInsufficientFee"));
1550 assert!(is_insufficient_fee_error("txinsufficientfee"));
1551 assert!(is_insufficient_fee_error("TXINSUFFICIENTFEE"));
1552 }
1553
1554 #[test]
1555 fn test_returns_false_for_other_errors() {
1556 assert!(!is_insufficient_fee_error("network timeout"));
1557 assert!(!is_insufficient_fee_error("TxBadSeq"));
1558 assert!(!is_insufficient_fee_error("TxInsufficientBalance"));
1559 assert!(!is_insufficient_fee_error("TxBadAuth"));
1560 assert!(!is_insufficient_fee_error(""));
1561 }
1562 }
1563
1564 mod decode_transaction_result_code_tests {
1565 use super::*;
1566
1567 #[test]
1568 fn test_decodes_insufficient_fee_result_xdr() {
1569 let result_code = decode_transaction_result_code("AAAAAAAAY/n////3AAAAAA==").unwrap();
1570 assert_eq!(result_code, "TxInsufficientFee");
1571 }
1572
1573 #[test]
1574 fn test_returns_none_for_invalid_xdr() {
1575 assert!(decode_transaction_result_code("not-base64").is_none());
1576 }
1577 }
1578
1579 mod status_check_utils_tests {
1580 use crate::models::{
1581 NetworkTransactionData, StellarTransactionData, TransactionError, TransactionInput,
1582 TransactionRepoModel,
1583 };
1584 use crate::utils::mocks::mockutils::create_mock_transaction;
1585 use chrono::{Duration, Utc};
1586
1587 fn create_test_tx_with_age(seconds_ago: i64) -> TransactionRepoModel {
1589 let created_at = (Utc::now() - Duration::seconds(seconds_ago)).to_rfc3339();
1590 let mut tx = create_mock_transaction();
1591 tx.id = format!("test-tx-{seconds_ago}");
1592 tx.created_at = created_at;
1593 tx.network_data = NetworkTransactionData::Stellar(StellarTransactionData {
1594 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1595 .to_string(),
1596 fee: None,
1597 sequence_number: None,
1598 memo: None,
1599 valid_until: None,
1600 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1601 signatures: vec![],
1602 hash: Some("test-hash-12345".to_string()),
1603 simulation_transaction_data: None,
1604 transaction_input: TransactionInput::Operations(vec![]),
1605 signed_envelope_xdr: None,
1606 transaction_result_xdr: None,
1607 });
1608 tx
1609 }
1610
1611 mod get_age_since_created_tests {
1612 use crate::domain::transaction::util::get_age_since_created;
1613
1614 use super::*;
1615
1616 #[test]
1617 fn test_returns_correct_age_for_recent_transaction() {
1618 let tx = create_test_tx_with_age(30); let age = get_age_since_created(&tx).unwrap();
1620
1621 assert!(age.num_seconds() >= 29 && age.num_seconds() <= 31);
1623 }
1624
1625 #[test]
1626 fn test_returns_correct_age_for_old_transaction() {
1627 let tx = create_test_tx_with_age(3600); let age = get_age_since_created(&tx).unwrap();
1629
1630 assert!(age.num_seconds() >= 3599 && age.num_seconds() <= 3601);
1632 }
1633
1634 #[test]
1635 fn test_returns_zero_age_for_just_created_transaction() {
1636 let tx = create_test_tx_with_age(0); let age = get_age_since_created(&tx).unwrap();
1638
1639 assert!(age.num_seconds() >= 0 && age.num_seconds() <= 1);
1641 }
1642
1643 #[test]
1644 fn test_handles_negative_age_gracefully() {
1645 let created_at = (Utc::now() + Duration::seconds(10)).to_rfc3339();
1647 let mut tx = create_mock_transaction();
1648 tx.created_at = created_at;
1649
1650 let age = get_age_since_created(&tx).unwrap();
1651
1652 assert!(age.num_seconds() < 0);
1654 }
1655
1656 #[test]
1657 fn test_returns_error_for_invalid_created_at() {
1658 let mut tx = create_mock_transaction();
1659 tx.created_at = "invalid-timestamp".to_string();
1660
1661 let result = get_age_since_created(&tx);
1662 assert!(result.is_err());
1663
1664 match result.unwrap_err() {
1665 TransactionError::UnexpectedError(msg) => {
1666 assert!(msg.contains("Invalid created_at timestamp"));
1667 }
1668 _ => panic!("Expected UnexpectedError"),
1669 }
1670 }
1671
1672 #[test]
1673 fn test_returns_error_for_empty_created_at() {
1674 let mut tx = create_mock_transaction();
1675 tx.created_at = "".to_string();
1676
1677 let result = get_age_since_created(&tx);
1678 assert!(result.is_err());
1679 }
1680
1681 #[test]
1682 fn test_handles_various_rfc3339_formats() {
1683 let mut tx = create_mock_transaction();
1684
1685 tx.created_at = "2025-01-01T12:00:00Z".to_string();
1687 assert!(get_age_since_created(&tx).is_ok());
1688
1689 tx.created_at = "2025-01-01T12:00:00+00:00".to_string();
1691 assert!(get_age_since_created(&tx).is_ok());
1692
1693 tx.created_at = "2025-01-01T12:00:00.123Z".to_string();
1695 assert!(get_age_since_created(&tx).is_ok());
1696 }
1697 }
1698 }
1699
1700 #[test]
1701 fn test_create_signature_payload_functions() {
1702 use soroban_rs::xdr::{
1703 Hash, SequenceNumber, TransactionEnvelope, TransactionV0, TransactionV0Envelope,
1704 Uint256,
1705 };
1706
1707 let transaction = soroban_rs::xdr::Transaction {
1709 source_account: soroban_rs::xdr::MuxedAccount::Ed25519(Uint256([1u8; 32])),
1710 fee: 100,
1711 seq_num: SequenceNumber(123),
1712 cond: soroban_rs::xdr::Preconditions::None,
1713 memo: soroban_rs::xdr::Memo::None,
1714 operations: vec![].try_into().unwrap(),
1715 ext: soroban_rs::xdr::TransactionExt::V0,
1716 };
1717 let network_id = Hash([2u8; 32]);
1718
1719 let payload = create_transaction_signature_payload(&transaction, &network_id);
1720 assert_eq!(payload.network_id, network_id);
1721
1722 let v0_tx = TransactionV0 {
1724 source_account_ed25519: Uint256([1u8; 32]),
1725 fee: 100,
1726 seq_num: SequenceNumber(123),
1727 time_bounds: None,
1728 memo: soroban_rs::xdr::Memo::None,
1729 operations: vec![].try_into().unwrap(),
1730 ext: soroban_rs::xdr::TransactionV0Ext::V0,
1731 };
1732 let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1733 tx: v0_tx,
1734 signatures: vec![].try_into().unwrap(),
1735 });
1736
1737 let v0_payload = create_signature_payload(&v0_envelope, &network_id).unwrap();
1738 assert_eq!(v0_payload.network_id, network_id);
1739 }
1740
1741 mod convert_v0_to_v1_transaction_tests {
1742 use super::*;
1743 use soroban_rs::xdr::{SequenceNumber, TransactionV0, Uint256};
1744
1745 #[test]
1746 fn test_convert_v0_to_v1_transaction() {
1747 let v0_tx = TransactionV0 {
1749 source_account_ed25519: Uint256([1u8; 32]),
1750 fee: 100,
1751 seq_num: SequenceNumber(123),
1752 time_bounds: None,
1753 memo: soroban_rs::xdr::Memo::None,
1754 operations: vec![].try_into().unwrap(),
1755 ext: soroban_rs::xdr::TransactionV0Ext::V0,
1756 };
1757
1758 let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1760
1761 assert_eq!(v1_tx.fee, v0_tx.fee);
1763 assert_eq!(v1_tx.seq_num, v0_tx.seq_num);
1764 assert_eq!(v1_tx.memo, v0_tx.memo);
1765 assert_eq!(v1_tx.operations, v0_tx.operations);
1766 assert!(matches!(v1_tx.ext, soroban_rs::xdr::TransactionExt::V0));
1767 assert!(matches!(v1_tx.cond, soroban_rs::xdr::Preconditions::None));
1768
1769 match v1_tx.source_account {
1771 soroban_rs::xdr::MuxedAccount::Ed25519(addr) => {
1772 assert_eq!(addr, v0_tx.source_account_ed25519);
1773 }
1774 _ => panic!("Expected Ed25519 muxed account"),
1775 }
1776 }
1777
1778 #[test]
1779 fn test_convert_v0_to_v1_transaction_with_time_bounds() {
1780 let time_bounds = soroban_rs::xdr::TimeBounds {
1782 min_time: soroban_rs::xdr::TimePoint(100),
1783 max_time: soroban_rs::xdr::TimePoint(200),
1784 };
1785
1786 let v0_tx = TransactionV0 {
1787 source_account_ed25519: Uint256([2u8; 32]),
1788 fee: 200,
1789 seq_num: SequenceNumber(456),
1790 time_bounds: Some(time_bounds.clone()),
1791 memo: soroban_rs::xdr::Memo::Text("test".try_into().unwrap()),
1792 operations: vec![].try_into().unwrap(),
1793 ext: soroban_rs::xdr::TransactionV0Ext::V0,
1794 };
1795
1796 let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1798
1799 match v1_tx.cond {
1801 soroban_rs::xdr::Preconditions::Time(tb) => {
1802 assert_eq!(tb, time_bounds);
1803 }
1804 _ => panic!("Expected Time preconditions"),
1805 }
1806 }
1807 }
1808}
1809
1810#[cfg(test)]
1811mod parse_contract_address_tests {
1812 use super::*;
1813 use crate::domain::transaction::stellar::test_helpers::{
1814 TEST_CONTRACT, TEST_PK as TEST_ACCOUNT,
1815 };
1816
1817 #[test]
1818 fn test_parse_valid_contract_address() {
1819 let result = parse_contract_address(TEST_CONTRACT);
1820 assert!(result.is_ok());
1821
1822 let hash = result.unwrap();
1823 assert_eq!(hash.0.len(), 32);
1824 }
1825
1826 #[test]
1827 fn test_parse_invalid_contract_address() {
1828 let result = parse_contract_address("INVALID_CONTRACT");
1829 assert!(result.is_err());
1830
1831 match result.unwrap_err() {
1832 StellarTransactionUtilsError::InvalidContractAddress(addr, _) => {
1833 assert_eq!(addr, "INVALID_CONTRACT");
1834 }
1835 _ => panic!("Expected InvalidContractAddress error"),
1836 }
1837 }
1838
1839 #[test]
1840 fn test_parse_contract_address_wrong_prefix() {
1841 let result = parse_contract_address(TEST_ACCOUNT);
1843 assert!(result.is_err());
1844 }
1845
1846 #[test]
1847 fn test_parse_empty_contract_address() {
1848 let result = parse_contract_address("");
1849 assert!(result.is_err());
1850 }
1851}
1852
1853#[cfg(test)]
1858mod update_envelope_sequence_tests {
1859 use super::*;
1860 use soroban_rs::xdr::{
1861 FeeBumpTransaction, FeeBumpTransactionEnvelope, FeeBumpTransactionExt,
1862 FeeBumpTransactionInnerTx, Memo, MuxedAccount, Preconditions, SequenceNumber, Transaction,
1863 TransactionExt, TransactionV0, TransactionV0Envelope, TransactionV0Ext,
1864 TransactionV1Envelope, Uint256, VecM,
1865 };
1866
1867 fn create_minimal_v1_envelope() -> TransactionEnvelope {
1868 let tx = Transaction {
1869 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
1870 fee: 100,
1871 seq_num: SequenceNumber(0),
1872 cond: Preconditions::None,
1873 memo: Memo::None,
1874 operations: VecM::default(),
1875 ext: TransactionExt::V0,
1876 };
1877 TransactionEnvelope::Tx(TransactionV1Envelope {
1878 tx,
1879 signatures: VecM::default(),
1880 })
1881 }
1882
1883 fn create_v0_envelope() -> TransactionEnvelope {
1884 let tx = TransactionV0 {
1885 source_account_ed25519: Uint256([0u8; 32]),
1886 fee: 100,
1887 seq_num: SequenceNumber(0),
1888 time_bounds: None,
1889 memo: Memo::None,
1890 operations: VecM::default(),
1891 ext: TransactionV0Ext::V0,
1892 };
1893 TransactionEnvelope::TxV0(TransactionV0Envelope {
1894 tx,
1895 signatures: VecM::default(),
1896 })
1897 }
1898
1899 fn create_fee_bump_envelope() -> TransactionEnvelope {
1900 let inner_tx = Transaction {
1901 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
1902 fee: 100,
1903 seq_num: SequenceNumber(0),
1904 cond: Preconditions::None,
1905 memo: Memo::None,
1906 operations: VecM::default(),
1907 ext: TransactionExt::V0,
1908 };
1909 let inner_envelope = TransactionV1Envelope {
1910 tx: inner_tx,
1911 signatures: VecM::default(),
1912 };
1913 let fee_bump_tx = FeeBumpTransaction {
1914 fee_source: MuxedAccount::Ed25519(Uint256([1u8; 32])),
1915 fee: 200,
1916 inner_tx: FeeBumpTransactionInnerTx::Tx(inner_envelope),
1917 ext: FeeBumpTransactionExt::V0,
1918 };
1919 TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
1920 tx: fee_bump_tx,
1921 signatures: VecM::default(),
1922 })
1923 }
1924
1925 #[test]
1926 fn test_update_envelope_sequence() {
1927 let mut envelope = create_minimal_v1_envelope();
1928 update_envelope_sequence(&mut envelope, 12345).unwrap();
1929 if let TransactionEnvelope::Tx(v1) = &envelope {
1930 assert_eq!(v1.tx.seq_num.0, 12345);
1931 } else {
1932 panic!("Expected Tx envelope");
1933 }
1934 }
1935
1936 #[test]
1937 fn test_update_envelope_sequence_v0_returns_error() {
1938 let mut envelope = create_v0_envelope();
1939 let result = update_envelope_sequence(&mut envelope, 12345);
1940 assert!(result.is_err());
1941 match result.unwrap_err() {
1942 StellarTransactionUtilsError::V0TransactionsNotSupported => {}
1943 _ => panic!("Expected V0TransactionsNotSupported error"),
1944 }
1945 }
1946
1947 #[test]
1948 fn test_update_envelope_sequence_fee_bump_returns_error() {
1949 let mut envelope = create_fee_bump_envelope();
1950 let result = update_envelope_sequence(&mut envelope, 12345);
1951 assert!(result.is_err());
1952 match result.unwrap_err() {
1953 StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump => {}
1954 _ => panic!("Expected CannotUpdateSequenceOnFeeBump error"),
1955 }
1956 }
1957
1958 #[test]
1959 fn test_update_envelope_sequence_zero() {
1960 let mut envelope = create_minimal_v1_envelope();
1961 update_envelope_sequence(&mut envelope, 0).unwrap();
1962 if let TransactionEnvelope::Tx(v1) = &envelope {
1963 assert_eq!(v1.tx.seq_num.0, 0);
1964 } else {
1965 panic!("Expected Tx envelope");
1966 }
1967 }
1968
1969 #[test]
1970 fn test_update_envelope_sequence_max_value() {
1971 let mut envelope = create_minimal_v1_envelope();
1972 update_envelope_sequence(&mut envelope, i64::MAX).unwrap();
1973 if let TransactionEnvelope::Tx(v1) = &envelope {
1974 assert_eq!(v1.tx.seq_num.0, i64::MAX);
1975 } else {
1976 panic!("Expected Tx envelope");
1977 }
1978 }
1979
1980 #[test]
1981 fn test_envelope_fee_in_stroops_v1() {
1982 let envelope = create_minimal_v1_envelope();
1983 let fee = envelope_fee_in_stroops(&envelope).unwrap();
1984 assert_eq!(fee, 100);
1985 }
1986
1987 #[test]
1988 fn test_envelope_fee_in_stroops_v0_returns_error() {
1989 let envelope = create_v0_envelope();
1990 let result = envelope_fee_in_stroops(&envelope);
1991 assert!(result.is_err());
1992 match result.unwrap_err() {
1993 StellarTransactionUtilsError::InvalidTransactionFormat(msg) => {
1994 assert!(msg.contains("Expected V1"));
1995 }
1996 _ => panic!("Expected InvalidTransactionFormat error"),
1997 }
1998 }
1999
2000 #[test]
2001 fn test_envelope_fee_in_stroops_fee_bump_returns_error() {
2002 let envelope = create_fee_bump_envelope();
2003 let result = envelope_fee_in_stroops(&envelope);
2004 assert!(result.is_err());
2005 }
2006}
2007
2008#[cfg(test)]
2013mod create_contract_data_key_tests {
2014 use super::*;
2015 use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
2016 use stellar_strkey::ed25519::PublicKey;
2017
2018 #[test]
2019 fn test_create_key_without_address() {
2020 let result = create_contract_data_key("Balance", None);
2021 assert!(result.is_ok());
2022
2023 match result.unwrap() {
2024 ScVal::Symbol(sym) => {
2025 assert_eq!(sym.to_string(), "Balance");
2026 }
2027 _ => panic!("Expected Symbol ScVal"),
2028 }
2029 }
2030
2031 #[test]
2032 fn test_create_key_with_address() {
2033 let pk = PublicKey::from_string(TEST_ACCOUNT).unwrap();
2034 let uint256 = Uint256(pk.0);
2035 let account_id = AccountId(soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(uint256));
2036 let sc_address = ScAddress::Account(account_id);
2037
2038 let result = create_contract_data_key("Balance", Some(sc_address.clone()));
2039 assert!(result.is_ok());
2040
2041 match result.unwrap() {
2042 ScVal::Vec(Some(vec)) => {
2043 assert_eq!(vec.0.len(), 2);
2044 match &vec.0[0] {
2045 ScVal::Symbol(sym) => assert_eq!(sym.to_string(), "Balance"),
2046 _ => panic!("Expected Symbol as first element"),
2047 }
2048 match &vec.0[1] {
2049 ScVal::Address(addr) => assert_eq!(addr, &sc_address),
2050 _ => panic!("Expected Address as second element"),
2051 }
2052 }
2053 _ => panic!("Expected Vec ScVal"),
2054 }
2055 }
2056
2057 #[test]
2058 fn test_create_key_invalid_symbol() {
2059 let very_long_symbol = "a".repeat(100);
2061 let result = create_contract_data_key(&very_long_symbol, None);
2062 assert!(result.is_err());
2063
2064 match result.unwrap_err() {
2065 StellarTransactionUtilsError::SymbolCreationFailed(_, _) => {}
2066 _ => panic!("Expected SymbolCreationFailed error"),
2067 }
2068 }
2069
2070 #[test]
2071 fn test_create_key_decimals() {
2072 let result = create_contract_data_key("Decimals", None);
2073 assert!(result.is_ok());
2074 }
2075}
2076
2077#[cfg(test)]
2082mod extract_scval_from_contract_data_tests {
2083 use super::*;
2084 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
2085 use soroban_rs::xdr::{
2086 ContractDataDurability, ContractDataEntry, ExtensionPoint, Hash, LedgerEntry,
2087 LedgerEntryData, LedgerEntryExt, ScSymbol, ScVal, WriteXdr,
2088 };
2089
2090 #[test]
2091 fn test_extract_scval_success() {
2092 let contract_data = ContractDataEntry {
2093 ext: ExtensionPoint::V0,
2094 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
2095 key: ScVal::Symbol(ScSymbol::try_from("test").unwrap()),
2096 durability: ContractDataDurability::Persistent,
2097 val: ScVal::U32(42),
2098 };
2099
2100 let ledger_entry = LedgerEntry {
2101 last_modified_ledger_seq: 100,
2102 data: LedgerEntryData::ContractData(contract_data),
2103 ext: LedgerEntryExt::V0,
2104 };
2105
2106 let xdr = ledger_entry
2107 .data
2108 .to_xdr_base64(soroban_rs::xdr::Limits::none())
2109 .unwrap();
2110
2111 let response = GetLedgerEntriesResponse {
2112 entries: Some(vec![LedgerEntryResult {
2113 key: "test_key".to_string(),
2114 xdr,
2115 last_modified_ledger: 100,
2116 live_until_ledger_seq_ledger_seq: None,
2117 }]),
2118 latest_ledger: 100,
2119 };
2120
2121 let result = extract_scval_from_contract_data(&response, "test");
2122 assert!(result.is_ok());
2123
2124 match result.unwrap() {
2125 ScVal::U32(val) => assert_eq!(val, 42),
2126 _ => panic!("Expected U32 ScVal"),
2127 }
2128 }
2129
2130 #[test]
2131 fn test_extract_scval_no_entries() {
2132 let response = GetLedgerEntriesResponse {
2133 entries: None,
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::NoEntriesFound(_) => {}
2142 _ => panic!("Expected NoEntriesFound error"),
2143 }
2144 }
2145
2146 #[test]
2147 fn test_extract_scval_empty_entries() {
2148 let response = GetLedgerEntriesResponse {
2149 entries: Some(vec![]),
2150 latest_ledger: 100,
2151 };
2152
2153 let result = extract_scval_from_contract_data(&response, "test");
2154 assert!(result.is_err());
2155
2156 match result.unwrap_err() {
2157 StellarTransactionUtilsError::EmptyEntries(_) => {}
2158 _ => panic!("Expected EmptyEntries error"),
2159 }
2160 }
2161}
2162
2163#[cfg(test)]
2168mod extract_u32_from_scval_tests {
2169 use super::*;
2170 use soroban_rs::xdr::{Int128Parts, ScVal, UInt128Parts};
2171
2172 #[test]
2173 fn test_extract_from_u32() {
2174 let val = ScVal::U32(42);
2175 assert_eq!(extract_u32_from_scval(&val, "test"), Some(42));
2176 }
2177
2178 #[test]
2179 fn test_extract_from_i32_positive() {
2180 let val = ScVal::I32(100);
2181 assert_eq!(extract_u32_from_scval(&val, "test"), Some(100));
2182 }
2183
2184 #[test]
2185 fn test_extract_from_i32_negative() {
2186 let val = ScVal::I32(-1);
2187 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2188 }
2189
2190 #[test]
2191 fn test_extract_from_u64() {
2192 let val = ScVal::U64(1000);
2193 assert_eq!(extract_u32_from_scval(&val, "test"), Some(1000));
2194 }
2195
2196 #[test]
2197 fn test_extract_from_u64_overflow() {
2198 let val = ScVal::U64(u64::MAX);
2199 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2200 }
2201
2202 #[test]
2203 fn test_extract_from_i64_positive() {
2204 let val = ScVal::I64(500);
2205 assert_eq!(extract_u32_from_scval(&val, "test"), Some(500));
2206 }
2207
2208 #[test]
2209 fn test_extract_from_i64_negative() {
2210 let val = ScVal::I64(-500);
2211 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2212 }
2213
2214 #[test]
2215 fn test_extract_from_u128_small() {
2216 let val = ScVal::U128(UInt128Parts { hi: 0, lo: 255 });
2217 assert_eq!(extract_u32_from_scval(&val, "test"), Some(255));
2218 }
2219
2220 #[test]
2221 fn test_extract_from_u128_hi_set() {
2222 let val = ScVal::U128(UInt128Parts { hi: 1, lo: 0 });
2223 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2224 }
2225
2226 #[test]
2227 fn test_extract_from_i128_small() {
2228 let val = ScVal::I128(Int128Parts { hi: 0, lo: 123 });
2229 assert_eq!(extract_u32_from_scval(&val, "test"), Some(123));
2230 }
2231
2232 #[test]
2233 fn test_extract_from_unsupported_type() {
2234 let val = ScVal::Bool(true);
2235 assert_eq!(extract_u32_from_scval(&val, "test"), None);
2236 }
2237}
2238
2239#[cfg(test)]
2244mod amount_to_ui_amount_tests {
2245 use super::*;
2246
2247 #[test]
2248 fn test_zero_decimals() {
2249 assert_eq!(amount_to_ui_amount(100, 0), "100");
2250 assert_eq!(amount_to_ui_amount(0, 0), "0");
2251 }
2252
2253 #[test]
2254 fn test_with_decimals_no_padding() {
2255 assert_eq!(amount_to_ui_amount(1000000, 6), "1");
2256 assert_eq!(amount_to_ui_amount(1500000, 6), "1.5");
2257 assert_eq!(amount_to_ui_amount(1234567, 6), "1.234567");
2258 }
2259
2260 #[test]
2261 fn test_with_decimals_needs_padding() {
2262 assert_eq!(amount_to_ui_amount(1, 6), "0.000001");
2263 assert_eq!(amount_to_ui_amount(100, 6), "0.0001");
2264 assert_eq!(amount_to_ui_amount(1000, 3), "1");
2265 }
2266
2267 #[test]
2268 fn test_trailing_zeros_removed() {
2269 assert_eq!(amount_to_ui_amount(1000000, 6), "1");
2270 assert_eq!(amount_to_ui_amount(1500000, 7), "0.15");
2271 assert_eq!(amount_to_ui_amount(10000000, 7), "1");
2272 }
2273
2274 #[test]
2275 fn test_zero_amount() {
2276 assert_eq!(amount_to_ui_amount(0, 6), "0");
2277 assert_eq!(amount_to_ui_amount(0, 0), "0");
2278 }
2279
2280 #[test]
2281 fn test_xlm_7_decimals() {
2282 assert_eq!(amount_to_ui_amount(10000000, 7), "1");
2283 assert_eq!(amount_to_ui_amount(15000000, 7), "1.5");
2284 assert_eq!(amount_to_ui_amount(100, 7), "0.00001");
2285 }
2286}
2287
2288#[cfg(test)]
2294mod count_operations_tests {
2295 use super::*;
2296 use soroban_rs::xdr::{
2297 Limits, MuxedAccount, Operation, OperationBody, PaymentOp, TransactionV1Envelope, Uint256,
2298 WriteXdr,
2299 };
2300
2301 #[test]
2302 fn test_count_operations_from_xdr() {
2303 use soroban_rs::xdr::{Memo, Preconditions, SequenceNumber, Transaction, TransactionExt};
2304
2305 let payment_op = Operation {
2307 source_account: None,
2308 body: OperationBody::Payment(PaymentOp {
2309 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2310 asset: Asset::Native,
2311 amount: 100,
2312 }),
2313 };
2314
2315 let operations = vec![payment_op.clone(), payment_op].try_into().unwrap();
2316
2317 let tx = Transaction {
2318 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2319 fee: 100,
2320 seq_num: SequenceNumber(1),
2321 cond: Preconditions::None,
2322 memo: Memo::None,
2323 operations,
2324 ext: TransactionExt::V0,
2325 };
2326
2327 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2328 tx,
2329 signatures: vec![].try_into().unwrap(),
2330 });
2331
2332 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2333 let count = count_operations_from_xdr(&xdr).unwrap();
2334
2335 assert_eq!(count, 2);
2336 }
2337
2338 #[test]
2339 fn test_count_operations_invalid_xdr() {
2340 let result = count_operations_from_xdr("invalid_xdr");
2341 assert!(result.is_err());
2342
2343 match result.unwrap_err() {
2344 StellarTransactionUtilsError::XdrParseFailed(_) => {}
2345 _ => panic!("Expected XdrParseFailed error"),
2346 }
2347 }
2348}
2349
2350#[cfg(test)]
2355mod estimate_base_fee_tests {
2356 use super::*;
2357
2358 #[test]
2359 fn test_single_operation() {
2360 assert_eq!(estimate_base_fee(1), 100);
2361 }
2362
2363 #[test]
2364 fn test_multiple_operations() {
2365 assert_eq!(estimate_base_fee(5), 500);
2366 assert_eq!(estimate_base_fee(10), 1000);
2367 }
2368
2369 #[test]
2370 fn test_zero_operations() {
2371 assert_eq!(estimate_base_fee(0), 100);
2373 }
2374
2375 #[test]
2376 fn test_large_number_of_operations() {
2377 assert_eq!(estimate_base_fee(100), 10000);
2378 }
2379}
2380
2381#[cfg(test)]
2386mod create_fee_payment_operation_tests {
2387 use super::*;
2388 use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
2389
2390 #[test]
2391 fn test_create_native_payment() {
2392 let result = create_fee_payment_operation(TEST_ACCOUNT, "native", 1000);
2393 assert!(result.is_ok());
2394
2395 match result.unwrap() {
2396 OperationSpec::Payment {
2397 destination,
2398 amount,
2399 asset,
2400 } => {
2401 assert_eq!(destination, TEST_ACCOUNT);
2402 assert_eq!(amount, 1000);
2403 assert!(matches!(asset, AssetSpec::Native));
2404 }
2405 _ => panic!("Expected Payment operation"),
2406 }
2407 }
2408
2409 #[test]
2410 fn test_create_credit4_payment() {
2411 let result = create_fee_payment_operation(
2412 TEST_ACCOUNT,
2413 "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2414 5000,
2415 );
2416 assert!(result.is_ok());
2417
2418 match result.unwrap() {
2419 OperationSpec::Payment {
2420 destination,
2421 amount,
2422 asset,
2423 } => {
2424 assert_eq!(destination, TEST_ACCOUNT);
2425 assert_eq!(amount, 5000);
2426 match asset {
2427 AssetSpec::Credit4 { code, issuer } => {
2428 assert_eq!(code, "USDC");
2429 assert_eq!(
2430 issuer,
2431 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2432 );
2433 }
2434 _ => panic!("Expected Credit4 asset"),
2435 }
2436 }
2437 _ => panic!("Expected Payment operation"),
2438 }
2439 }
2440
2441 #[test]
2442 fn test_create_credit12_payment() {
2443 let result = create_fee_payment_operation(
2444 TEST_ACCOUNT,
2445 "LONGASSETNAM:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2446 2000,
2447 );
2448 assert!(result.is_ok());
2449
2450 match result.unwrap() {
2451 OperationSpec::Payment {
2452 destination,
2453 amount,
2454 asset,
2455 } => {
2456 assert_eq!(destination, TEST_ACCOUNT);
2457 assert_eq!(amount, 2000);
2458 match asset {
2459 AssetSpec::Credit12 { code, issuer } => {
2460 assert_eq!(code, "LONGASSETNAM");
2461 assert_eq!(
2462 issuer,
2463 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2464 );
2465 }
2466 _ => panic!("Expected Credit12 asset"),
2467 }
2468 }
2469 _ => panic!("Expected Payment operation"),
2470 }
2471 }
2472
2473 #[test]
2474 fn test_create_payment_empty_asset() {
2475 let result = create_fee_payment_operation(TEST_ACCOUNT, "", 1000);
2476 assert!(result.is_ok());
2477
2478 match result.unwrap() {
2479 OperationSpec::Payment { asset, .. } => {
2480 assert!(matches!(asset, AssetSpec::Native));
2481 }
2482 _ => panic!("Expected Payment operation"),
2483 }
2484 }
2485
2486 #[test]
2487 fn test_create_payment_invalid_format() {
2488 let result = create_fee_payment_operation(TEST_ACCOUNT, "INVALID_FORMAT", 1000);
2489 assert!(result.is_err());
2490
2491 match result.unwrap_err() {
2492 StellarTransactionUtilsError::InvalidAssetFormat(_) => {}
2493 _ => panic!("Expected InvalidAssetFormat error"),
2494 }
2495 }
2496
2497 #[test]
2498 fn test_create_payment_asset_code_too_long() {
2499 let result = create_fee_payment_operation(
2500 TEST_ACCOUNT,
2501 "VERYLONGASSETCODE:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2502 1000,
2503 );
2504 assert!(result.is_err());
2505
2506 match result.unwrap_err() {
2507 StellarTransactionUtilsError::AssetCodeTooLong(max_len, _) => {
2508 assert_eq!(max_len, 12);
2509 }
2510 _ => panic!("Expected AssetCodeTooLong error"),
2511 }
2512 }
2513}
2514
2515#[cfg(test)]
2516mod parse_account_id_tests {
2517 use super::*;
2518 use crate::domain::transaction::stellar::test_helpers::TEST_PK;
2519
2520 #[test]
2521 fn test_parse_account_id_valid() {
2522 let result = parse_account_id(TEST_PK);
2523 assert!(result.is_ok());
2524
2525 let account_id = result.unwrap();
2526 match account_id.0 {
2527 soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(_) => {}
2528 }
2529 }
2530
2531 #[test]
2532 fn test_parse_account_id_invalid() {
2533 let result = parse_account_id("INVALID_ADDRESS");
2534 assert!(result.is_err());
2535
2536 match result.unwrap_err() {
2537 StellarTransactionUtilsError::InvalidAccountAddress(addr, _) => {
2538 assert_eq!(addr, "INVALID_ADDRESS");
2539 }
2540 _ => panic!("Expected InvalidAccountAddress error"),
2541 }
2542 }
2543
2544 #[test]
2545 fn test_parse_account_id_empty() {
2546 let result = parse_account_id("");
2547 assert!(result.is_err());
2548 }
2549
2550 #[test]
2551 fn test_parse_account_id_wrong_prefix() {
2552 let result = parse_account_id("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM");
2554 assert!(result.is_err());
2555 }
2556}
2557
2558#[cfg(test)]
2559mod parse_transaction_and_count_operations_tests {
2560 use super::*;
2561 use crate::domain::transaction::stellar::test_helpers::{
2562 create_native_payment_operation, create_xdr_with_operations, TEST_PK, TEST_PK_2,
2563 };
2564 use serde_json::json;
2565
2566 fn create_test_xdr_with_operations(num_ops: usize) -> String {
2567 let payment_op = create_native_payment_operation(TEST_PK_2, 100);
2568 let operations = vec![payment_op; num_ops];
2569 create_xdr_with_operations(TEST_PK, operations, false)
2570 }
2571
2572 #[test]
2573 fn test_parse_xdr_string() {
2574 let xdr = create_test_xdr_with_operations(2);
2575 let json_value = json!(xdr);
2576
2577 let result = parse_transaction_and_count_operations(&json_value);
2578 assert!(result.is_ok());
2579 assert_eq!(result.unwrap(), 2);
2580 }
2581
2582 #[test]
2583 fn test_parse_operations_array() {
2584 let json_value = json!([
2585 {"type": "payment"},
2586 {"type": "payment"},
2587 {"type": "payment"}
2588 ]);
2589
2590 let result = parse_transaction_and_count_operations(&json_value);
2591 assert!(result.is_ok());
2592 assert_eq!(result.unwrap(), 3);
2593 }
2594
2595 #[test]
2596 fn test_parse_object_with_operations() {
2597 let json_value = json!({
2598 "operations": [
2599 {"type": "payment"},
2600 {"type": "payment"}
2601 ]
2602 });
2603
2604 let result = parse_transaction_and_count_operations(&json_value);
2605 assert!(result.is_ok());
2606 assert_eq!(result.unwrap(), 2);
2607 }
2608
2609 #[test]
2610 fn test_parse_object_with_transaction_xdr() {
2611 let xdr = create_test_xdr_with_operations(3);
2612 let json_value = json!({
2613 "transaction_xdr": xdr
2614 });
2615
2616 let result = parse_transaction_and_count_operations(&json_value);
2617 assert!(result.is_ok());
2618 assert_eq!(result.unwrap(), 3);
2619 }
2620
2621 #[test]
2622 fn test_parse_invalid_xdr() {
2623 let json_value = json!("INVALID_XDR");
2624
2625 let result = parse_transaction_and_count_operations(&json_value);
2626 assert!(result.is_err());
2627
2628 match result.unwrap_err() {
2629 StellarTransactionUtilsError::XdrParseFailed(_) => {}
2630 _ => panic!("Expected XdrParseFailed error"),
2631 }
2632 }
2633
2634 #[test]
2635 fn test_parse_invalid_format() {
2636 let json_value = json!(123);
2637
2638 let result = parse_transaction_and_count_operations(&json_value);
2639 assert!(result.is_err());
2640
2641 match result.unwrap_err() {
2642 StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2643 _ => panic!("Expected InvalidTransactionFormat error"),
2644 }
2645 }
2646
2647 #[test]
2648 fn test_parse_empty_operations() {
2649 let json_value = json!([]);
2650
2651 let result = parse_transaction_and_count_operations(&json_value);
2652 assert!(result.is_ok());
2653 assert_eq!(result.unwrap(), 0);
2654 }
2655}
2656
2657#[cfg(test)]
2658mod parse_transaction_envelope_tests {
2659 use super::*;
2660 use crate::domain::transaction::stellar::test_helpers::{
2661 create_unsigned_xdr, TEST_PK, TEST_PK_2,
2662 };
2663 use serde_json::json;
2664
2665 fn create_test_xdr() -> String {
2666 create_unsigned_xdr(TEST_PK, TEST_PK_2)
2667 }
2668
2669 #[test]
2670 fn test_parse_xdr_string() {
2671 let xdr = create_test_xdr();
2672 let json_value = json!(xdr);
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_object_with_transaction_xdr() {
2685 let xdr = create_test_xdr();
2686 let json_value = json!({
2687 "transaction_xdr": xdr
2688 });
2689
2690 let result = parse_transaction_envelope(&json_value);
2691 assert!(result.is_ok());
2692
2693 match result.unwrap() {
2694 TransactionEnvelope::Tx(_) => {}
2695 _ => panic!("Expected Tx envelope"),
2696 }
2697 }
2698
2699 #[test]
2700 fn test_parse_invalid_xdr() {
2701 let json_value = json!("INVALID_XDR");
2702
2703 let result = parse_transaction_envelope(&json_value);
2704 assert!(result.is_err());
2705
2706 match result.unwrap_err() {
2707 StellarTransactionUtilsError::XdrParseFailed(_) => {}
2708 _ => panic!("Expected XdrParseFailed error"),
2709 }
2710 }
2711
2712 #[test]
2713 fn test_parse_invalid_format() {
2714 let json_value = json!(123);
2715
2716 let result = parse_transaction_envelope(&json_value);
2717 assert!(result.is_err());
2718
2719 match result.unwrap_err() {
2720 StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2721 _ => panic!("Expected InvalidTransactionFormat error"),
2722 }
2723 }
2724
2725 #[test]
2726 fn test_parse_object_without_xdr() {
2727 let json_value = json!({
2728 "some_field": "value"
2729 });
2730
2731 let result = parse_transaction_envelope(&json_value);
2732 assert!(result.is_err());
2733
2734 match result.unwrap_err() {
2735 StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2736 _ => panic!("Expected InvalidTransactionFormat error"),
2737 }
2738 }
2739}
2740
2741#[cfg(test)]
2742mod add_operation_to_envelope_tests {
2743 use super::*;
2744 use soroban_rs::xdr::{
2745 Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2746 Transaction, TransactionExt, TransactionV0, TransactionV0Envelope, TransactionV1Envelope,
2747 Uint256,
2748 };
2749
2750 fn create_payment_op() -> Operation {
2751 Operation {
2752 source_account: None,
2753 body: OperationBody::Payment(PaymentOp {
2754 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2755 asset: Asset::Native,
2756 amount: 100,
2757 }),
2758 }
2759 }
2760
2761 #[test]
2762 fn test_add_operation_to_tx_v0() {
2763 let payment_op = create_payment_op();
2764 let operations = vec![payment_op.clone()].try_into().unwrap();
2765
2766 let tx = TransactionV0 {
2767 source_account_ed25519: Uint256([0u8; 32]),
2768 fee: 100,
2769 seq_num: SequenceNumber(1),
2770 time_bounds: None,
2771 memo: Memo::None,
2772 operations,
2773 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2774 };
2775
2776 let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2777 tx,
2778 signatures: vec![].try_into().unwrap(),
2779 });
2780
2781 let new_op = create_payment_op();
2782 let result = add_operation_to_envelope(&mut envelope, new_op);
2783
2784 assert!(result.is_ok());
2785
2786 match envelope {
2787 TransactionEnvelope::TxV0(e) => {
2788 assert_eq!(e.tx.operations.len(), 2);
2789 assert_eq!(e.tx.fee, 200); }
2791 _ => panic!("Expected TxV0 envelope"),
2792 }
2793 }
2794
2795 #[test]
2796 fn test_add_operation_to_tx_v1() {
2797 let payment_op = create_payment_op();
2798 let operations = vec![payment_op.clone()].try_into().unwrap();
2799
2800 let tx = Transaction {
2801 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2802 fee: 100,
2803 seq_num: SequenceNumber(1),
2804 cond: Preconditions::None,
2805 memo: Memo::None,
2806 operations,
2807 ext: TransactionExt::V0,
2808 };
2809
2810 let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2811 tx,
2812 signatures: vec![].try_into().unwrap(),
2813 });
2814
2815 let new_op = create_payment_op();
2816 let result = add_operation_to_envelope(&mut envelope, new_op);
2817
2818 assert!(result.is_ok());
2819
2820 match envelope {
2821 TransactionEnvelope::Tx(e) => {
2822 assert_eq!(e.tx.operations.len(), 2);
2823 assert_eq!(e.tx.fee, 200); }
2825 _ => panic!("Expected Tx envelope"),
2826 }
2827 }
2828
2829 #[test]
2830 fn test_add_operation_to_fee_bump_fails() {
2831 let payment_op = create_payment_op();
2833 let operations = vec![payment_op].try_into().unwrap();
2834
2835 let tx = Transaction {
2836 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2837 fee: 100,
2838 seq_num: SequenceNumber(1),
2839 cond: Preconditions::None,
2840 memo: Memo::None,
2841 operations,
2842 ext: TransactionExt::V0,
2843 };
2844
2845 let inner_envelope = TransactionV1Envelope {
2846 tx,
2847 signatures: vec![].try_into().unwrap(),
2848 };
2849
2850 let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
2851
2852 let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
2853 fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
2854 fee: 200,
2855 inner_tx,
2856 ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
2857 };
2858
2859 let mut envelope =
2860 TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
2861 tx: fee_bump_tx,
2862 signatures: vec![].try_into().unwrap(),
2863 });
2864
2865 let new_op = create_payment_op();
2866 let result = add_operation_to_envelope(&mut envelope, new_op);
2867
2868 assert!(result.is_err());
2869
2870 match result.unwrap_err() {
2871 StellarTransactionUtilsError::CannotModifyFeeBump => {}
2872 _ => panic!("Expected CannotModifyFeeBump error"),
2873 }
2874 }
2875}
2876
2877#[cfg(test)]
2878mod extract_time_bounds_tests {
2879 use super::*;
2880 use soroban_rs::xdr::{
2881 Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2882 TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
2883 TransactionV1Envelope, Uint256,
2884 };
2885
2886 fn create_payment_op() -> Operation {
2887 Operation {
2888 source_account: None,
2889 body: OperationBody::Payment(PaymentOp {
2890 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2891 asset: Asset::Native,
2892 amount: 100,
2893 }),
2894 }
2895 }
2896
2897 #[test]
2898 fn test_extract_time_bounds_from_tx_v0_with_bounds() {
2899 let payment_op = create_payment_op();
2900 let operations = vec![payment_op].try_into().unwrap();
2901
2902 let time_bounds = TimeBounds {
2903 min_time: TimePoint(0),
2904 max_time: TimePoint(1000),
2905 };
2906
2907 let tx = TransactionV0 {
2908 source_account_ed25519: Uint256([0u8; 32]),
2909 fee: 100,
2910 seq_num: SequenceNumber(1),
2911 time_bounds: Some(time_bounds.clone()),
2912 memo: Memo::None,
2913 operations,
2914 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2915 };
2916
2917 let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2918 tx,
2919 signatures: vec![].try_into().unwrap(),
2920 });
2921
2922 let result = extract_time_bounds(&envelope);
2923 assert!(result.is_some());
2924
2925 let bounds = result.unwrap();
2926 assert_eq!(bounds.min_time.0, 0);
2927 assert_eq!(bounds.max_time.0, 1000);
2928 }
2929
2930 #[test]
2931 fn test_extract_time_bounds_from_tx_v0_without_bounds() {
2932 let payment_op = create_payment_op();
2933 let operations = vec![payment_op].try_into().unwrap();
2934
2935 let tx = TransactionV0 {
2936 source_account_ed25519: Uint256([0u8; 32]),
2937 fee: 100,
2938 seq_num: SequenceNumber(1),
2939 time_bounds: None,
2940 memo: Memo::None,
2941 operations,
2942 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2943 };
2944
2945 let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2946 tx,
2947 signatures: vec![].try_into().unwrap(),
2948 });
2949
2950 let result = extract_time_bounds(&envelope);
2951 assert!(result.is_none());
2952 }
2953
2954 #[test]
2955 fn test_extract_time_bounds_from_tx_v1_with_time_precondition() {
2956 let payment_op = create_payment_op();
2957 let operations = vec![payment_op].try_into().unwrap();
2958
2959 let time_bounds = TimeBounds {
2960 min_time: TimePoint(0),
2961 max_time: TimePoint(2000),
2962 };
2963
2964 let tx = Transaction {
2965 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2966 fee: 100,
2967 seq_num: SequenceNumber(1),
2968 cond: Preconditions::Time(time_bounds.clone()),
2969 memo: Memo::None,
2970 operations,
2971 ext: TransactionExt::V0,
2972 };
2973
2974 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2975 tx,
2976 signatures: vec![].try_into().unwrap(),
2977 });
2978
2979 let result = extract_time_bounds(&envelope);
2980 assert!(result.is_some());
2981
2982 let bounds = result.unwrap();
2983 assert_eq!(bounds.min_time.0, 0);
2984 assert_eq!(bounds.max_time.0, 2000);
2985 }
2986
2987 #[test]
2988 fn test_extract_time_bounds_from_tx_v1_without_time_precondition() {
2989 let payment_op = create_payment_op();
2990 let operations = vec![payment_op].try_into().unwrap();
2991
2992 let tx = Transaction {
2993 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2994 fee: 100,
2995 seq_num: SequenceNumber(1),
2996 cond: Preconditions::None,
2997 memo: Memo::None,
2998 operations,
2999 ext: TransactionExt::V0,
3000 };
3001
3002 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
3003 tx,
3004 signatures: vec![].try_into().unwrap(),
3005 });
3006
3007 let result = extract_time_bounds(&envelope);
3008 assert!(result.is_none());
3009 }
3010
3011 #[test]
3012 fn test_extract_time_bounds_from_fee_bump() {
3013 let payment_op = create_payment_op();
3015 let operations = vec![payment_op].try_into().unwrap();
3016
3017 let time_bounds = TimeBounds {
3018 min_time: TimePoint(0),
3019 max_time: TimePoint(3000),
3020 };
3021
3022 let tx = Transaction {
3023 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
3024 fee: 100,
3025 seq_num: SequenceNumber(1),
3026 cond: Preconditions::Time(time_bounds.clone()),
3027 memo: Memo::None,
3028 operations,
3029 ext: TransactionExt::V0,
3030 };
3031
3032 let inner_envelope = TransactionV1Envelope {
3033 tx,
3034 signatures: vec![].try_into().unwrap(),
3035 };
3036
3037 let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
3038
3039 let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
3040 fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
3041 fee: 200,
3042 inner_tx,
3043 ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
3044 };
3045
3046 let envelope =
3047 TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
3048 tx: fee_bump_tx,
3049 signatures: vec![].try_into().unwrap(),
3050 });
3051
3052 let result = extract_time_bounds(&envelope);
3053 assert!(result.is_some());
3054
3055 let bounds = result.unwrap();
3056 assert_eq!(bounds.min_time.0, 0);
3057 assert_eq!(bounds.max_time.0, 3000);
3058 }
3059}
3060
3061#[cfg(test)]
3062mod set_time_bounds_tests {
3063 use super::*;
3064 use chrono::Utc;
3065 use soroban_rs::xdr::{
3066 Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
3067 TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
3068 TransactionV1Envelope, Uint256,
3069 };
3070
3071 fn create_payment_op() -> Operation {
3072 Operation {
3073 source_account: None,
3074 body: OperationBody::Payment(PaymentOp {
3075 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
3076 asset: Asset::Native,
3077 amount: 100,
3078 }),
3079 }
3080 }
3081
3082 #[test]
3083 fn test_set_time_bounds_on_tx_v0() {
3084 let payment_op = create_payment_op();
3085 let operations = vec![payment_op].try_into().unwrap();
3086
3087 let tx = TransactionV0 {
3088 source_account_ed25519: Uint256([0u8; 32]),
3089 fee: 100,
3090 seq_num: SequenceNumber(1),
3091 time_bounds: None,
3092 memo: Memo::None,
3093 operations,
3094 ext: soroban_rs::xdr::TransactionV0Ext::V0,
3095 };
3096
3097 let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
3098 tx,
3099 signatures: vec![].try_into().unwrap(),
3100 });
3101
3102 let valid_until = Utc::now() + chrono::Duration::seconds(300);
3103 let result = set_time_bounds(&mut envelope, valid_until);
3104
3105 assert!(result.is_ok());
3106
3107 match envelope {
3108 TransactionEnvelope::TxV0(e) => {
3109 assert!(e.tx.time_bounds.is_some());
3110 let bounds = e.tx.time_bounds.unwrap();
3111 assert_eq!(bounds.min_time.0, 0);
3112 assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3113 }
3114 _ => panic!("Expected TxV0 envelope"),
3115 }
3116 }
3117
3118 #[test]
3119 fn test_set_time_bounds_on_tx_v1() {
3120 let payment_op = create_payment_op();
3121 let operations = vec![payment_op].try_into().unwrap();
3122
3123 let tx = Transaction {
3124 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
3125 fee: 100,
3126 seq_num: SequenceNumber(1),
3127 cond: Preconditions::None,
3128 memo: Memo::None,
3129 operations,
3130 ext: TransactionExt::V0,
3131 };
3132
3133 let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
3134 tx,
3135 signatures: vec![].try_into().unwrap(),
3136 });
3137
3138 let valid_until = Utc::now() + chrono::Duration::seconds(300);
3139 let result = set_time_bounds(&mut envelope, valid_until);
3140
3141 assert!(result.is_ok());
3142
3143 match envelope {
3144 TransactionEnvelope::Tx(e) => match e.tx.cond {
3145 Preconditions::Time(bounds) => {
3146 assert_eq!(bounds.min_time.0, 0);
3147 assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3148 }
3149 _ => panic!("Expected Time precondition"),
3150 },
3151 _ => panic!("Expected Tx envelope"),
3152 }
3153 }
3154
3155 #[test]
3156 fn test_set_time_bounds_on_fee_bump_fails() {
3157 let payment_op = create_payment_op();
3159 let operations = vec![payment_op].try_into().unwrap();
3160
3161 let tx = Transaction {
3162 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
3163 fee: 100,
3164 seq_num: SequenceNumber(1),
3165 cond: Preconditions::None,
3166 memo: Memo::None,
3167 operations,
3168 ext: TransactionExt::V0,
3169 };
3170
3171 let inner_envelope = TransactionV1Envelope {
3172 tx,
3173 signatures: vec![].try_into().unwrap(),
3174 };
3175
3176 let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
3177
3178 let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
3179 fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
3180 fee: 200,
3181 inner_tx,
3182 ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
3183 };
3184
3185 let mut envelope =
3186 TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
3187 tx: fee_bump_tx,
3188 signatures: vec![].try_into().unwrap(),
3189 });
3190
3191 let valid_until = Utc::now() + chrono::Duration::seconds(300);
3192 let result = set_time_bounds(&mut envelope, valid_until);
3193
3194 assert!(result.is_err());
3195
3196 match result.unwrap_err() {
3197 StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump => {}
3198 _ => panic!("Expected CannotSetTimeBoundsOnFeeBump error"),
3199 }
3200 }
3201
3202 #[test]
3203 fn test_set_time_bounds_replaces_existing() {
3204 let payment_op = create_payment_op();
3205 let operations = vec![payment_op].try_into().unwrap();
3206
3207 let old_time_bounds = TimeBounds {
3208 min_time: TimePoint(100),
3209 max_time: TimePoint(1000),
3210 };
3211
3212 let tx = TransactionV0 {
3213 source_account_ed25519: Uint256([0u8; 32]),
3214 fee: 100,
3215 seq_num: SequenceNumber(1),
3216 time_bounds: Some(old_time_bounds),
3217 memo: Memo::None,
3218 operations,
3219 ext: soroban_rs::xdr::TransactionV0Ext::V0,
3220 };
3221
3222 let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
3223 tx,
3224 signatures: vec![].try_into().unwrap(),
3225 });
3226
3227 let valid_until = Utc::now() + chrono::Duration::seconds(300);
3228 let result = set_time_bounds(&mut envelope, valid_until);
3229
3230 assert!(result.is_ok());
3231
3232 match envelope {
3233 TransactionEnvelope::TxV0(e) => {
3234 assert!(e.tx.time_bounds.is_some());
3235 let bounds = e.tx.time_bounds.unwrap();
3236 assert_eq!(bounds.min_time.0, 0);
3238 assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3239 }
3240 _ => panic!("Expected TxV0 envelope"),
3241 }
3242 }
3243}
3244
3245#[cfg(test)]
3250mod stellar_transaction_utils_error_conversion_tests {
3251 use super::*;
3252
3253 #[test]
3254 fn test_v0_transactions_not_supported_converts_to_validation_error() {
3255 let err = StellarTransactionUtilsError::V0TransactionsNotSupported;
3256 let relayer_err: RelayerError = err.into();
3257 match relayer_err {
3258 RelayerError::ValidationError(msg) => {
3259 assert_eq!(msg, "V0 transactions are not supported");
3260 }
3261 _ => panic!("Expected ValidationError"),
3262 }
3263 }
3264
3265 #[test]
3266 fn test_cannot_update_sequence_on_fee_bump_converts_to_validation_error() {
3267 let err = StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump;
3268 let relayer_err: RelayerError = err.into();
3269 match relayer_err {
3270 RelayerError::ValidationError(msg) => {
3271 assert_eq!(msg, "Cannot update sequence number on fee bump transaction");
3272 }
3273 _ => panic!("Expected ValidationError"),
3274 }
3275 }
3276
3277 #[test]
3278 fn test_cannot_set_time_bounds_on_fee_bump_converts_to_validation_error() {
3279 let err = StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump;
3280 let relayer_err: RelayerError = err.into();
3281 match relayer_err {
3282 RelayerError::ValidationError(msg) => {
3283 assert_eq!(msg, "Cannot set time bounds on fee-bump transactions");
3284 }
3285 _ => panic!("Expected ValidationError"),
3286 }
3287 }
3288
3289 #[test]
3290 fn test_invalid_transaction_format_converts_to_validation_error() {
3291 let err = StellarTransactionUtilsError::InvalidTransactionFormat("bad format".to_string());
3292 let relayer_err: RelayerError = err.into();
3293 match relayer_err {
3294 RelayerError::ValidationError(msg) => {
3295 assert_eq!(msg, "bad format");
3296 }
3297 _ => panic!("Expected ValidationError"),
3298 }
3299 }
3300
3301 #[test]
3302 fn test_cannot_modify_fee_bump_converts_to_validation_error() {
3303 let err = StellarTransactionUtilsError::CannotModifyFeeBump;
3304 let relayer_err: RelayerError = err.into();
3305 match relayer_err {
3306 RelayerError::ValidationError(msg) => {
3307 assert_eq!(msg, "Cannot add operations to fee-bump transactions");
3308 }
3309 _ => panic!("Expected ValidationError"),
3310 }
3311 }
3312
3313 #[test]
3314 fn test_too_many_operations_converts_to_validation_error() {
3315 let err = StellarTransactionUtilsError::TooManyOperations(100);
3316 let relayer_err: RelayerError = err.into();
3317 match relayer_err {
3318 RelayerError::ValidationError(msg) => {
3319 assert!(msg.contains("Too many operations"));
3320 assert!(msg.contains("100"));
3321 }
3322 _ => panic!("Expected ValidationError"),
3323 }
3324 }
3325
3326 #[test]
3327 fn test_sequence_overflow_converts_to_internal_error() {
3328 let err = StellarTransactionUtilsError::SequenceOverflow("overflow msg".to_string());
3329 let relayer_err: RelayerError = err.into();
3330 match relayer_err {
3331 RelayerError::Internal(msg) => {
3332 assert_eq!(msg, "overflow msg");
3333 }
3334 _ => panic!("Expected Internal error"),
3335 }
3336 }
3337
3338 #[test]
3339 fn test_simulation_no_results_converts_to_internal_error() {
3340 let err = StellarTransactionUtilsError::SimulationNoResults;
3341 let relayer_err: RelayerError = err.into();
3342 match relayer_err {
3343 RelayerError::Internal(msg) => {
3344 assert!(msg.contains("no results"));
3345 }
3346 _ => panic!("Expected Internal error"),
3347 }
3348 }
3349
3350 #[test]
3351 fn test_asset_code_too_long_converts_to_validation_error() {
3352 let err =
3353 StellarTransactionUtilsError::AssetCodeTooLong(12, "VERYLONGASSETCODE".to_string());
3354 let relayer_err: RelayerError = err.into();
3355 match relayer_err {
3356 RelayerError::ValidationError(msg) => {
3357 assert!(msg.contains("Asset code too long"));
3358 assert!(msg.contains("12"));
3359 }
3360 _ => panic!("Expected ValidationError"),
3361 }
3362 }
3363
3364 #[test]
3365 fn test_invalid_asset_format_converts_to_validation_error() {
3366 let err = StellarTransactionUtilsError::InvalidAssetFormat("bad asset".to_string());
3367 let relayer_err: RelayerError = err.into();
3368 match relayer_err {
3369 RelayerError::ValidationError(msg) => {
3370 assert_eq!(msg, "bad asset");
3371 }
3372 _ => panic!("Expected ValidationError"),
3373 }
3374 }
3375
3376 #[test]
3377 fn test_invalid_account_address_converts_to_internal_error() {
3378 let err = StellarTransactionUtilsError::InvalidAccountAddress(
3379 "GABC".to_string(),
3380 "parse error".to_string(),
3381 );
3382 let relayer_err: RelayerError = err.into();
3383 match relayer_err {
3384 RelayerError::Internal(msg) => {
3385 assert_eq!(msg, "parse error");
3386 }
3387 _ => panic!("Expected Internal error"),
3388 }
3389 }
3390
3391 #[test]
3392 fn test_invalid_contract_address_converts_to_internal_error() {
3393 let err = StellarTransactionUtilsError::InvalidContractAddress(
3394 "CABC".to_string(),
3395 "contract parse error".to_string(),
3396 );
3397 let relayer_err: RelayerError = err.into();
3398 match relayer_err {
3399 RelayerError::Internal(msg) => {
3400 assert_eq!(msg, "contract parse error");
3401 }
3402 _ => panic!("Expected Internal error"),
3403 }
3404 }
3405
3406 #[test]
3407 fn test_symbol_creation_failed_converts_to_internal_error() {
3408 let err = StellarTransactionUtilsError::SymbolCreationFailed(
3409 "Balance".to_string(),
3410 "too long".to_string(),
3411 );
3412 let relayer_err: RelayerError = err.into();
3413 match relayer_err {
3414 RelayerError::Internal(msg) => {
3415 assert_eq!(msg, "too long");
3416 }
3417 _ => panic!("Expected Internal error"),
3418 }
3419 }
3420
3421 #[test]
3422 fn test_key_vector_creation_failed_converts_to_internal_error() {
3423 let err = StellarTransactionUtilsError::KeyVectorCreationFailed(
3424 "Balance".to_string(),
3425 "vec error".to_string(),
3426 );
3427 let relayer_err: RelayerError = err.into();
3428 match relayer_err {
3429 RelayerError::Internal(msg) => {
3430 assert_eq!(msg, "vec error");
3431 }
3432 _ => panic!("Expected Internal error"),
3433 }
3434 }
3435
3436 #[test]
3437 fn test_contract_data_query_persistent_failed_converts_to_internal_error() {
3438 let err = StellarTransactionUtilsError::ContractDataQueryPersistentFailed(
3439 "balance".to_string(),
3440 "rpc error".to_string(),
3441 );
3442 let relayer_err: RelayerError = err.into();
3443 match relayer_err {
3444 RelayerError::Internal(msg) => {
3445 assert_eq!(msg, "rpc error");
3446 }
3447 _ => panic!("Expected Internal error"),
3448 }
3449 }
3450
3451 #[test]
3452 fn test_contract_data_query_temporary_failed_converts_to_internal_error() {
3453 let err = StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(
3454 "balance".to_string(),
3455 "temp error".to_string(),
3456 );
3457 let relayer_err: RelayerError = err.into();
3458 match relayer_err {
3459 RelayerError::Internal(msg) => {
3460 assert_eq!(msg, "temp error");
3461 }
3462 _ => panic!("Expected Internal error"),
3463 }
3464 }
3465
3466 #[test]
3467 fn test_ledger_entry_parse_failed_converts_to_internal_error() {
3468 let err = StellarTransactionUtilsError::LedgerEntryParseFailed(
3469 "entry".to_string(),
3470 "xdr error".to_string(),
3471 );
3472 let relayer_err: RelayerError = err.into();
3473 match relayer_err {
3474 RelayerError::Internal(msg) => {
3475 assert_eq!(msg, "xdr error");
3476 }
3477 _ => panic!("Expected Internal error"),
3478 }
3479 }
3480
3481 #[test]
3482 fn test_no_entries_found_converts_to_validation_error() {
3483 let err = StellarTransactionUtilsError::NoEntriesFound("balance".to_string());
3484 let relayer_err: RelayerError = err.into();
3485 match relayer_err {
3486 RelayerError::ValidationError(msg) => {
3487 assert!(msg.contains("No entries found"));
3488 }
3489 _ => panic!("Expected ValidationError"),
3490 }
3491 }
3492
3493 #[test]
3494 fn test_empty_entries_converts_to_validation_error() {
3495 let err = StellarTransactionUtilsError::EmptyEntries("balance".to_string());
3496 let relayer_err: RelayerError = err.into();
3497 match relayer_err {
3498 RelayerError::ValidationError(msg) => {
3499 assert!(msg.contains("Empty entries"));
3500 }
3501 _ => panic!("Expected ValidationError"),
3502 }
3503 }
3504
3505 #[test]
3506 fn test_unexpected_ledger_entry_type_converts_to_validation_error() {
3507 let err = StellarTransactionUtilsError::UnexpectedLedgerEntryType("balance".to_string());
3508 let relayer_err: RelayerError = err.into();
3509 match relayer_err {
3510 RelayerError::ValidationError(msg) => {
3511 assert!(msg.contains("Unexpected ledger entry type"));
3512 }
3513 _ => panic!("Expected ValidationError"),
3514 }
3515 }
3516
3517 #[test]
3518 fn test_invalid_issuer_length_converts_to_validation_error() {
3519 let err = StellarTransactionUtilsError::InvalidIssuerLength(56, "SHORT".to_string());
3520 let relayer_err: RelayerError = err.into();
3521 match relayer_err {
3522 RelayerError::ValidationError(msg) => {
3523 assert!(msg.contains("56"));
3524 assert!(msg.contains("SHORT"));
3525 }
3526 _ => panic!("Expected ValidationError"),
3527 }
3528 }
3529
3530 #[test]
3531 fn test_invalid_issuer_prefix_converts_to_validation_error() {
3532 let err = StellarTransactionUtilsError::InvalidIssuerPrefix('G', "CABC123".to_string());
3533 let relayer_err: RelayerError = err.into();
3534 match relayer_err {
3535 RelayerError::ValidationError(msg) => {
3536 assert!(msg.contains("'G'"));
3537 assert!(msg.contains("CABC123"));
3538 }
3539 _ => panic!("Expected ValidationError"),
3540 }
3541 }
3542
3543 #[test]
3544 fn test_account_fetch_failed_converts_to_provider_error() {
3545 let err = StellarTransactionUtilsError::AccountFetchFailed("fetch error".to_string());
3546 let relayer_err: RelayerError = err.into();
3547 match relayer_err {
3548 RelayerError::ProviderError(msg) => {
3549 assert_eq!(msg, "fetch error");
3550 }
3551 _ => panic!("Expected ProviderError"),
3552 }
3553 }
3554
3555 #[test]
3556 fn test_trustline_query_failed_converts_to_provider_error() {
3557 let err = StellarTransactionUtilsError::TrustlineQueryFailed(
3558 "USDC".to_string(),
3559 "rpc fail".to_string(),
3560 );
3561 let relayer_err: RelayerError = err.into();
3562 match relayer_err {
3563 RelayerError::ProviderError(msg) => {
3564 assert_eq!(msg, "rpc fail");
3565 }
3566 _ => panic!("Expected ProviderError"),
3567 }
3568 }
3569
3570 #[test]
3571 fn test_contract_invocation_failed_converts_to_provider_error() {
3572 let err = StellarTransactionUtilsError::ContractInvocationFailed(
3573 "transfer".to_string(),
3574 "invoke error".to_string(),
3575 );
3576 let relayer_err: RelayerError = err.into();
3577 match relayer_err {
3578 RelayerError::ProviderError(msg) => {
3579 assert_eq!(msg, "invoke error");
3580 }
3581 _ => panic!("Expected ProviderError"),
3582 }
3583 }
3584
3585 #[test]
3586 fn test_xdr_parse_failed_converts_to_internal_error() {
3587 let err = StellarTransactionUtilsError::XdrParseFailed("xdr parse fail".to_string());
3588 let relayer_err: RelayerError = err.into();
3589 match relayer_err {
3590 RelayerError::Internal(msg) => {
3591 assert_eq!(msg, "xdr parse fail");
3592 }
3593 _ => panic!("Expected Internal error"),
3594 }
3595 }
3596
3597 #[test]
3598 fn test_operation_extraction_failed_converts_to_internal_error() {
3599 let err =
3600 StellarTransactionUtilsError::OperationExtractionFailed("extract fail".to_string());
3601 let relayer_err: RelayerError = err.into();
3602 match relayer_err {
3603 RelayerError::Internal(msg) => {
3604 assert_eq!(msg, "extract fail");
3605 }
3606 _ => panic!("Expected Internal error"),
3607 }
3608 }
3609
3610 #[test]
3611 fn test_simulation_failed_converts_to_internal_error() {
3612 let err = StellarTransactionUtilsError::SimulationFailed("sim error".to_string());
3613 let relayer_err: RelayerError = err.into();
3614 match relayer_err {
3615 RelayerError::Internal(msg) => {
3616 assert_eq!(msg, "sim error");
3617 }
3618 _ => panic!("Expected Internal error"),
3619 }
3620 }
3621
3622 #[test]
3623 fn test_simulation_check_failed_converts_to_internal_error() {
3624 let err = StellarTransactionUtilsError::SimulationCheckFailed("check fail".to_string());
3625 let relayer_err: RelayerError = err.into();
3626 match relayer_err {
3627 RelayerError::Internal(msg) => {
3628 assert_eq!(msg, "check fail");
3629 }
3630 _ => panic!("Expected Internal error"),
3631 }
3632 }
3633
3634 #[test]
3635 fn test_dex_quote_failed_converts_to_internal_error() {
3636 let err = StellarTransactionUtilsError::DexQuoteFailed("dex error".to_string());
3637 let relayer_err: RelayerError = err.into();
3638 match relayer_err {
3639 RelayerError::Internal(msg) => {
3640 assert_eq!(msg, "dex error");
3641 }
3642 _ => panic!("Expected Internal error"),
3643 }
3644 }
3645
3646 #[test]
3647 fn test_empty_asset_code_converts_to_validation_error() {
3648 let err = StellarTransactionUtilsError::EmptyAssetCode("CODE:ISSUER".to_string());
3649 let relayer_err: RelayerError = err.into();
3650 match relayer_err {
3651 RelayerError::ValidationError(msg) => {
3652 assert!(msg.contains("Asset code cannot be empty"));
3653 }
3654 _ => panic!("Expected ValidationError"),
3655 }
3656 }
3657
3658 #[test]
3659 fn test_empty_issuer_address_converts_to_validation_error() {
3660 let err = StellarTransactionUtilsError::EmptyIssuerAddress("USDC:".to_string());
3661 let relayer_err: RelayerError = err.into();
3662 match relayer_err {
3663 RelayerError::ValidationError(msg) => {
3664 assert!(msg.contains("Issuer address cannot be empty"));
3665 }
3666 _ => panic!("Expected ValidationError"),
3667 }
3668 }
3669
3670 #[test]
3671 fn test_no_trustline_found_converts_to_validation_error() {
3672 let err =
3673 StellarTransactionUtilsError::NoTrustlineFound("USDC".to_string(), "GABC".to_string());
3674 let relayer_err: RelayerError = err.into();
3675 match relayer_err {
3676 RelayerError::ValidationError(msg) => {
3677 assert!(msg.contains("No trustline found"));
3678 }
3679 _ => panic!("Expected ValidationError"),
3680 }
3681 }
3682
3683 #[test]
3684 fn test_unsupported_trustline_version_converts_to_validation_error() {
3685 let err = StellarTransactionUtilsError::UnsupportedTrustlineVersion;
3686 let relayer_err: RelayerError = err.into();
3687 match relayer_err {
3688 RelayerError::ValidationError(msg) => {
3689 assert!(msg.contains("Unsupported trustline"));
3690 }
3691 _ => panic!("Expected ValidationError"),
3692 }
3693 }
3694
3695 #[test]
3696 fn test_unexpected_trustline_entry_type_converts_to_validation_error() {
3697 let err = StellarTransactionUtilsError::UnexpectedTrustlineEntryType;
3698 let relayer_err: RelayerError = err.into();
3699 match relayer_err {
3700 RelayerError::ValidationError(msg) => {
3701 assert!(msg.contains("Unexpected ledger entry type"));
3702 }
3703 _ => panic!("Expected ValidationError"),
3704 }
3705 }
3706
3707 #[test]
3708 fn test_balance_too_large_converts_to_validation_error() {
3709 let err = StellarTransactionUtilsError::BalanceTooLarge(1, 999);
3710 let relayer_err: RelayerError = err.into();
3711 match relayer_err {
3712 RelayerError::ValidationError(msg) => {
3713 assert!(msg.contains("Balance too large"));
3714 }
3715 _ => panic!("Expected ValidationError"),
3716 }
3717 }
3718
3719 #[test]
3720 fn test_negative_balance_i128_converts_to_validation_error() {
3721 let err = StellarTransactionUtilsError::NegativeBalanceI128(42);
3722 let relayer_err: RelayerError = err.into();
3723 match relayer_err {
3724 RelayerError::ValidationError(msg) => {
3725 assert!(msg.contains("Negative balance"));
3726 }
3727 _ => panic!("Expected ValidationError"),
3728 }
3729 }
3730
3731 #[test]
3732 fn test_negative_balance_i64_converts_to_validation_error() {
3733 let err = StellarTransactionUtilsError::NegativeBalanceI64(-5);
3734 let relayer_err: RelayerError = err.into();
3735 match relayer_err {
3736 RelayerError::ValidationError(msg) => {
3737 assert!(msg.contains("Negative balance"));
3738 }
3739 _ => panic!("Expected ValidationError"),
3740 }
3741 }
3742
3743 #[test]
3744 fn test_unexpected_balance_type_converts_to_validation_error() {
3745 let err = StellarTransactionUtilsError::UnexpectedBalanceType("Bool(true)".to_string());
3746 let relayer_err: RelayerError = err.into();
3747 match relayer_err {
3748 RelayerError::ValidationError(msg) => {
3749 assert!(msg.contains("Unexpected balance value type"));
3750 }
3751 _ => panic!("Expected ValidationError"),
3752 }
3753 }
3754
3755 #[test]
3756 fn test_unexpected_contract_data_entry_type_converts_to_validation_error() {
3757 let err = StellarTransactionUtilsError::UnexpectedContractDataEntryType;
3758 let relayer_err: RelayerError = err.into();
3759 match relayer_err {
3760 RelayerError::ValidationError(msg) => {
3761 assert!(msg.contains("Unexpected ledger entry type"));
3762 }
3763 _ => panic!("Expected ValidationError"),
3764 }
3765 }
3766
3767 #[test]
3768 fn test_native_asset_in_trustline_query_converts_to_validation_error() {
3769 let err = StellarTransactionUtilsError::NativeAssetInTrustlineQuery;
3770 let relayer_err: RelayerError = err.into();
3771 match relayer_err {
3772 RelayerError::ValidationError(msg) => {
3773 assert!(msg.contains("Native asset"));
3774 }
3775 _ => panic!("Expected ValidationError"),
3776 }
3777 }
3778}
3779
3780#[cfg(test)]
3781mod compute_resubmit_backoff_interval_tests {
3782 use super::compute_resubmit_backoff_interval;
3783 use chrono::Duration;
3784
3785 const BASE: i64 = 10;
3786 const MAX: i64 = 120;
3787 const FACTOR: f64 = 1.5;
3788
3789 #[test]
3790 fn returns_none_below_base() {
3791 assert!(
3792 compute_resubmit_backoff_interval(Duration::seconds(0), BASE, MAX, FACTOR).is_none()
3793 );
3794 assert!(
3795 compute_resubmit_backoff_interval(Duration::seconds(5), BASE, MAX, FACTOR).is_none()
3796 );
3797 assert!(
3798 compute_resubmit_backoff_interval(Duration::seconds(9), BASE, MAX, FACTOR).is_none()
3799 );
3800 }
3801
3802 #[test]
3803 fn base_interval_at_first_tier() {
3804 assert_eq!(
3806 compute_resubmit_backoff_interval(Duration::seconds(10), BASE, MAX, FACTOR),
3807 Some(Duration::seconds(10))
3808 );
3809 assert_eq!(
3810 compute_resubmit_backoff_interval(Duration::seconds(14), BASE, MAX, FACTOR),
3811 Some(Duration::seconds(10))
3812 );
3813 }
3814
3815 #[test]
3816 fn grows_by_factor_at_second_tier() {
3817 assert_eq!(
3819 compute_resubmit_backoff_interval(Duration::seconds(15), BASE, MAX, FACTOR),
3820 Some(Duration::seconds(15))
3821 );
3822 assert_eq!(
3823 compute_resubmit_backoff_interval(Duration::seconds(22), BASE, MAX, FACTOR),
3824 Some(Duration::seconds(15))
3825 );
3826 }
3827
3828 #[test]
3829 fn grows_by_factor_squared_at_third_tier() {
3830 assert_eq!(
3832 compute_resubmit_backoff_interval(Duration::seconds(23), BASE, MAX, FACTOR),
3833 Some(Duration::seconds(22))
3834 );
3835 assert_eq!(
3836 compute_resubmit_backoff_interval(Duration::seconds(33), BASE, MAX, FACTOR),
3837 Some(Duration::seconds(22))
3838 );
3839 }
3840
3841 #[test]
3842 fn fourth_tier() {
3843 assert_eq!(
3845 compute_resubmit_backoff_interval(Duration::seconds(34), BASE, MAX, FACTOR),
3846 Some(Duration::seconds(33))
3847 );
3848 assert_eq!(
3849 compute_resubmit_backoff_interval(Duration::seconds(50), BASE, MAX, FACTOR),
3850 Some(Duration::seconds(33))
3851 );
3852 }
3853
3854 #[test]
3855 fn capped_at_max() {
3856 assert_eq!(
3858 compute_resubmit_backoff_interval(Duration::seconds(300), BASE, MAX, FACTOR),
3859 Some(Duration::seconds(MAX))
3860 );
3861 assert_eq!(
3862 compute_resubmit_backoff_interval(Duration::seconds(1000), BASE, MAX, FACTOR),
3863 Some(Duration::seconds(MAX))
3864 );
3865 }
3866
3867 #[test]
3868 fn works_with_factor_2_doubling() {
3869 let factor = 2.0;
3871 assert_eq!(
3872 compute_resubmit_backoff_interval(Duration::seconds(10), BASE, MAX, factor),
3873 Some(Duration::seconds(10))
3874 );
3875 assert_eq!(
3876 compute_resubmit_backoff_interval(Duration::seconds(19), BASE, MAX, factor),
3877 Some(Duration::seconds(10))
3878 );
3879 assert_eq!(
3880 compute_resubmit_backoff_interval(Duration::seconds(20), BASE, MAX, factor),
3881 Some(Duration::seconds(20))
3882 );
3883 assert_eq!(
3884 compute_resubmit_backoff_interval(Duration::seconds(39), BASE, MAX, factor),
3885 Some(Duration::seconds(20))
3886 );
3887 assert_eq!(
3888 compute_resubmit_backoff_interval(Duration::seconds(40), BASE, MAX, factor),
3889 Some(Duration::seconds(40))
3890 );
3891 assert_eq!(
3892 compute_resubmit_backoff_interval(Duration::seconds(80), BASE, MAX, factor),
3893 Some(Duration::seconds(80))
3894 );
3895 assert_eq!(
3896 compute_resubmit_backoff_interval(Duration::seconds(160), BASE, MAX, factor),
3897 Some(Duration::seconds(MAX))
3898 );
3899 }
3900
3901 #[test]
3902 fn works_with_different_base_and_max() {
3903 let base = 5;
3905 let max = 30;
3906 assert_eq!(
3908 compute_resubmit_backoff_interval(Duration::seconds(5), base, max, FACTOR),
3909 Some(Duration::seconds(5))
3910 );
3911 assert_eq!(
3913 compute_resubmit_backoff_interval(Duration::seconds(8), base, max, FACTOR),
3914 Some(Duration::seconds(7))
3915 );
3916 assert_eq!(
3918 compute_resubmit_backoff_interval(Duration::seconds(100), base, max, FACTOR),
3919 Some(Duration::seconds(30))
3920 );
3921 }
3922
3923 #[test]
3924 fn factor_at_or_below_one_returns_min_base_max() {
3925 assert_eq!(
3927 compute_resubmit_backoff_interval(Duration::seconds(100), BASE, MAX, 1.0),
3928 Some(Duration::seconds(std::cmp::min(BASE, MAX)))
3929 );
3930 assert_eq!(
3931 compute_resubmit_backoff_interval(Duration::seconds(100), BASE, MAX, 0.5),
3932 Some(Duration::seconds(std::cmp::min(BASE, MAX)))
3933 );
3934 assert_eq!(
3936 compute_resubmit_backoff_interval(Duration::seconds(200), 200, MAX, 1.0),
3937 Some(Duration::seconds(MAX))
3938 );
3939 assert!(compute_resubmit_backoff_interval(Duration::seconds(5), BASE, MAX, 1.0).is_none());
3941 }
3942
3943 #[test]
3944 fn non_positive_base_or_max_returns_none() {
3945 assert!(
3947 compute_resubmit_backoff_interval(Duration::seconds(100), 0, MAX, FACTOR).is_none()
3948 );
3949 assert!(
3950 compute_resubmit_backoff_interval(Duration::seconds(100), -5, MAX, FACTOR).is_none()
3951 );
3952 assert!(
3954 compute_resubmit_backoff_interval(Duration::seconds(100), BASE, 0, FACTOR).is_none()
3955 );
3956 }
3957}