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