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, 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("Invalid transaction format: {0}")]
71 InvalidTransactionFormat(String),
72
73 #[error("Invalid account address '{0}': {1}")]
74 InvalidAccountAddress(String, String),
75
76 #[error("Invalid contract address '{0}': {1}")]
77 InvalidContractAddress(String, String),
78
79 #[error("Failed to create {0} symbol: {1:?}")]
80 SymbolCreationFailed(String, String),
81
82 #[error("Failed to create {0} key vector: {1:?}")]
83 KeyVectorCreationFailed(String, String),
84
85 #[error("Failed to query contract data (Persistent) for {0}: {1}")]
86 ContractDataQueryPersistentFailed(String, String),
87
88 #[error("Failed to query contract data (Temporary) for {0}: {1}")]
89 ContractDataQueryTemporaryFailed(String, String),
90
91 #[error("Failed to parse ledger entry XDR for {0}: {1}")]
92 LedgerEntryParseFailed(String, String),
93
94 #[error("No entries found for {0}")]
95 NoEntriesFound(String),
96
97 #[error("Empty entries for {0}")]
98 EmptyEntries(String),
99
100 #[error("Unexpected ledger entry type for {0} (expected ContractData)")]
101 UnexpectedLedgerEntryType(String),
102
103 #[error("Asset code cannot be empty in asset identifier: {0}")]
105 EmptyAssetCode(String),
106
107 #[error("Issuer address cannot be empty in asset identifier: {0}")]
108 EmptyIssuerAddress(String),
109
110 #[error("Invalid issuer address length (expected {0} characters): {1}")]
111 InvalidIssuerLength(usize, String),
112
113 #[error("Invalid issuer address format (must start with '{0}'): {1}")]
114 InvalidIssuerPrefix(char, String),
115
116 #[error("Failed to fetch account for balance: {0}")]
117 AccountFetchFailed(String),
118
119 #[error("Failed to query trustline for asset {0}: {1}")]
120 TrustlineQueryFailed(String, String),
121
122 #[error("No trustline found for asset {0} on account {1}")]
123 NoTrustlineFound(String, String),
124
125 #[error("Unsupported trustline entry version")]
126 UnsupportedTrustlineVersion,
127
128 #[error("Unexpected ledger entry type for trustline query")]
129 UnexpectedTrustlineEntryType,
130
131 #[error("Balance too large (i128 hi={0}, lo={1}) to fit in u64")]
132 BalanceTooLarge(i64, u64),
133
134 #[error("Negative balance not allowed: i128 lo={0}")]
135 NegativeBalanceI128(u64),
136
137 #[error("Negative balance not allowed: i64={0}")]
138 NegativeBalanceI64(i64),
139
140 #[error("Unexpected balance value type in contract data: {0:?}. Expected I128, U64, or I64")]
141 UnexpectedBalanceType(String),
142
143 #[error("Unexpected ledger entry type for contract data query")]
144 UnexpectedContractDataEntryType,
145
146 #[error("Native asset should be handled before trustline query")]
147 NativeAssetInTrustlineQuery,
148
149 #[error("Failed to invoke contract function '{0}': {1}")]
150 ContractInvocationFailed(String, String),
151}
152
153impl From<StellarTransactionUtilsError> for RelayerError {
154 fn from(error: StellarTransactionUtilsError) -> Self {
155 match &error {
156 StellarTransactionUtilsError::SequenceOverflow(msg)
157 | StellarTransactionUtilsError::SimulationCheckFailed(msg)
158 | StellarTransactionUtilsError::SimulationFailed(msg)
159 | StellarTransactionUtilsError::XdrParseFailed(msg)
160 | StellarTransactionUtilsError::OperationExtractionFailed(msg)
161 | StellarTransactionUtilsError::DexQuoteFailed(msg) => {
162 RelayerError::Internal(msg.clone())
163 }
164 StellarTransactionUtilsError::SimulationNoResults => RelayerError::Internal(
165 "Transaction simulation failed: no results returned".to_string(),
166 ),
167 StellarTransactionUtilsError::InvalidAssetFormat(msg)
168 | StellarTransactionUtilsError::InvalidTransactionFormat(msg) => {
169 RelayerError::ValidationError(msg.clone())
170 }
171 StellarTransactionUtilsError::AssetCodeTooLong(max_len, code) => {
172 RelayerError::ValidationError(format!(
173 "Asset code too long (max {max_len} characters): {code}"
174 ))
175 }
176 StellarTransactionUtilsError::TooManyOperations(max) => {
177 RelayerError::ValidationError(format!("Too many operations (max {max})"))
178 }
179 StellarTransactionUtilsError::CannotModifyFeeBump => RelayerError::ValidationError(
180 "Cannot add operations to fee-bump transactions".to_string(),
181 ),
182 StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump => {
183 RelayerError::ValidationError(
184 "Cannot set time bounds on fee-bump transactions".to_string(),
185 )
186 }
187 StellarTransactionUtilsError::InvalidAccountAddress(_, msg)
188 | StellarTransactionUtilsError::InvalidContractAddress(_, msg)
189 | StellarTransactionUtilsError::SymbolCreationFailed(_, msg)
190 | StellarTransactionUtilsError::KeyVectorCreationFailed(_, msg)
191 | StellarTransactionUtilsError::ContractDataQueryPersistentFailed(_, msg)
192 | StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(_, msg)
193 | StellarTransactionUtilsError::LedgerEntryParseFailed(_, msg) => {
194 RelayerError::Internal(msg.clone())
195 }
196 StellarTransactionUtilsError::NoEntriesFound(_)
197 | StellarTransactionUtilsError::EmptyEntries(_)
198 | StellarTransactionUtilsError::UnexpectedLedgerEntryType(_)
199 | StellarTransactionUtilsError::EmptyAssetCode(_)
200 | StellarTransactionUtilsError::EmptyIssuerAddress(_)
201 | StellarTransactionUtilsError::NoTrustlineFound(_, _)
202 | StellarTransactionUtilsError::UnsupportedTrustlineVersion
203 | StellarTransactionUtilsError::UnexpectedTrustlineEntryType
204 | StellarTransactionUtilsError::BalanceTooLarge(_, _)
205 | StellarTransactionUtilsError::NegativeBalanceI128(_)
206 | StellarTransactionUtilsError::NegativeBalanceI64(_)
207 | StellarTransactionUtilsError::UnexpectedBalanceType(_)
208 | StellarTransactionUtilsError::UnexpectedContractDataEntryType
209 | StellarTransactionUtilsError::NativeAssetInTrustlineQuery => {
210 RelayerError::ValidationError(error.to_string())
211 }
212 StellarTransactionUtilsError::InvalidIssuerLength(expected, actual) => {
213 RelayerError::ValidationError(format!(
214 "Invalid issuer address length (expected {expected} characters): {actual}"
215 ))
216 }
217 StellarTransactionUtilsError::InvalidIssuerPrefix(prefix, addr) => {
218 RelayerError::ValidationError(format!(
219 "Invalid issuer address format (must start with '{prefix}'): {addr}"
220 ))
221 }
222 StellarTransactionUtilsError::AccountFetchFailed(msg)
223 | StellarTransactionUtilsError::TrustlineQueryFailed(_, msg)
224 | StellarTransactionUtilsError::ContractInvocationFailed(_, msg) => {
225 RelayerError::ProviderError(msg.clone())
226 }
227 }
228 }
229}
230
231pub fn needs_simulation(operations: &[OperationSpec]) -> bool {
233 operations.iter().any(|op| {
234 matches!(
235 op,
236 OperationSpec::InvokeContract { .. }
237 | OperationSpec::CreateContract { .. }
238 | OperationSpec::UploadWasm { .. }
239 )
240 })
241}
242
243pub fn next_sequence_u64(seq_num: i64) -> Result<u64, RelayerError> {
244 let next_i64 = seq_num
245 .checked_add(1)
246 .ok_or_else(|| RelayerError::ProviderError("sequence overflow".into()))?;
247 u64::try_from(next_i64)
248 .map_err(|_| RelayerError::ProviderError("sequence overflows u64".into()))
249}
250
251pub fn i64_from_u64(value: u64) -> Result<i64, RelayerError> {
252 i64::try_from(value).map_err(|_| RelayerError::ProviderError("u64→i64 overflow".into()))
253}
254
255pub fn is_bad_sequence_error(error_msg: &str) -> bool {
258 let error_lower = error_msg.to_lowercase();
259 error_lower.contains("txbadseq")
260}
261
262pub async fn fetch_next_sequence_from_chain<P>(
268 provider: &P,
269 relayer_address: &str,
270) -> Result<u64, String>
271where
272 P: StellarProviderTrait,
273{
274 debug!(
275 "Fetching sequence from chain for address: {}",
276 relayer_address
277 );
278
279 let account = provider
281 .get_account(relayer_address)
282 .await
283 .map_err(|e| format!("Failed to fetch account from chain: {e}"))?;
284
285 let on_chain_seq = account.seq_num.0; let next_usable = next_sequence_u64(on_chain_seq)
287 .map_err(|e| format!("Failed to calculate next sequence: {e}"))?;
288
289 debug!(
290 "Fetched sequence from chain: on-chain={}, next usable={}",
291 on_chain_seq, next_usable
292 );
293 Ok(next_usable)
294}
295
296pub fn convert_v0_to_v1_transaction(
299 v0_tx: &soroban_rs::xdr::TransactionV0,
300) -> soroban_rs::xdr::Transaction {
301 soroban_rs::xdr::Transaction {
302 source_account: soroban_rs::xdr::MuxedAccount::Ed25519(
303 v0_tx.source_account_ed25519.clone(),
304 ),
305 fee: v0_tx.fee,
306 seq_num: v0_tx.seq_num.clone(),
307 cond: match v0_tx.time_bounds.clone() {
308 Some(tb) => soroban_rs::xdr::Preconditions::Time(tb),
309 None => soroban_rs::xdr::Preconditions::None,
310 },
311 memo: v0_tx.memo.clone(),
312 operations: v0_tx.operations.clone(),
313 ext: soroban_rs::xdr::TransactionExt::V0,
314 }
315}
316
317pub fn create_signature_payload(
319 envelope: &soroban_rs::xdr::TransactionEnvelope,
320 network_id: &soroban_rs::xdr::Hash,
321) -> Result<soroban_rs::xdr::TransactionSignaturePayload, RelayerError> {
322 let tagged_transaction = match envelope {
323 soroban_rs::xdr::TransactionEnvelope::TxV0(e) => {
324 let v1_tx = convert_v0_to_v1_transaction(&e.tx);
326 soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::Tx(v1_tx)
327 }
328 soroban_rs::xdr::TransactionEnvelope::Tx(e) => {
329 soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::Tx(e.tx.clone())
330 }
331 soroban_rs::xdr::TransactionEnvelope::TxFeeBump(e) => {
332 soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::TxFeeBump(e.tx.clone())
333 }
334 };
335
336 Ok(soroban_rs::xdr::TransactionSignaturePayload {
337 network_id: network_id.clone(),
338 tagged_transaction,
339 })
340}
341
342pub fn create_transaction_signature_payload(
344 transaction: &soroban_rs::xdr::Transaction,
345 network_id: &soroban_rs::xdr::Hash,
346) -> soroban_rs::xdr::TransactionSignaturePayload {
347 soroban_rs::xdr::TransactionSignaturePayload {
348 network_id: network_id.clone(),
349 tagged_transaction: soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::Tx(
350 transaction.clone(),
351 ),
352 }
353}
354
355pub fn parse_account_id(account_id: &str) -> Result<AccountId, StellarTransactionUtilsError> {
369 let account_pk = PublicKey::from_str(account_id).map_err(|e| {
370 StellarTransactionUtilsError::InvalidAccountAddress(account_id.to_string(), e.to_string())
371 })?;
372 let account_uint256 = Uint256(account_pk.0);
373 let account_xdr_pk = XdrPublicKey::PublicKeyTypeEd25519(account_uint256);
374 Ok(AccountId(account_xdr_pk))
375}
376
377pub fn parse_contract_address(
387 contract_address: &str,
388) -> Result<Hash, StellarTransactionUtilsError> {
389 let contract_id = ContractId::from_str(contract_address).map_err(|e| {
390 StellarTransactionUtilsError::InvalidContractAddress(
391 contract_address.to_string(),
392 e.to_string(),
393 )
394 })?;
395 Ok(contract_id.0)
396}
397
398pub fn create_contract_data_key(
416 symbol: &str,
417 address: Option<ScAddress>,
418) -> Result<ScVal, StellarTransactionUtilsError> {
419 if address.is_none() {
420 let sym = ScSymbol::try_from(symbol).map_err(|e| {
421 StellarTransactionUtilsError::SymbolCreationFailed(symbol.to_string(), format!("{e:?}"))
422 })?;
423 return Ok(ScVal::Symbol(sym));
424 }
425
426 let mut key_items: Vec<ScVal> =
427 vec![ScVal::Symbol(ScSymbol::try_from(symbol).map_err(|e| {
428 StellarTransactionUtilsError::SymbolCreationFailed(symbol.to_string(), format!("{e:?}"))
429 })?)];
430
431 if let Some(addr) = address {
432 key_items.push(ScVal::Address(addr));
433 }
434
435 let key_vec: VecM<ScVal, { u32::MAX }> = VecM::try_from(key_items).map_err(|e| {
436 StellarTransactionUtilsError::KeyVectorCreationFailed(symbol.to_string(), format!("{e:?}"))
437 })?;
438
439 Ok(ScVal::Vec(Some(soroban_rs::xdr::ScVec(key_vec))))
440}
441
442pub async fn query_contract_data_with_fallback<P>(
459 provider: &P,
460 contract_hash: Hash,
461 key: ScVal,
462 error_context: &str,
463) -> Result<soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse, StellarTransactionUtilsError>
464where
465 P: StellarProviderTrait + Send + Sync,
466{
467 let contract_address_sc =
468 soroban_rs::xdr::ScAddress::Contract(soroban_rs::xdr::ContractId(contract_hash));
469
470 let mut ledger_key = LedgerKey::ContractData(LedgerKeyContractData {
471 contract: contract_address_sc.clone(),
472 key: key.clone(),
473 durability: soroban_rs::xdr::ContractDataDurability::Persistent,
474 });
475
476 let mut ledger_entries = provider
478 .get_ledger_entries(&[ledger_key.clone()])
479 .await
480 .map_err(|e| {
481 StellarTransactionUtilsError::ContractDataQueryPersistentFailed(
482 error_context.to_string(),
483 e.to_string(),
484 )
485 })?;
486
487 if ledger_entries
489 .entries
490 .as_ref()
491 .map(|e| e.is_empty())
492 .unwrap_or(true)
493 {
494 ledger_key = LedgerKey::ContractData(LedgerKeyContractData {
495 contract: contract_address_sc,
496 key,
497 durability: soroban_rs::xdr::ContractDataDurability::Temporary,
498 });
499 ledger_entries = provider
500 .get_ledger_entries(&[ledger_key])
501 .await
502 .map_err(|e| {
503 StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(
504 error_context.to_string(),
505 e.to_string(),
506 )
507 })?;
508 }
509
510 Ok(ledger_entries)
511}
512
513pub fn parse_ledger_entry_from_xdr(
527 xdr_string: &str,
528 context: &str,
529) -> Result<LedgerEntryData, StellarTransactionUtilsError> {
530 let trimmed_xdr = xdr_string.trim();
531
532 if general_purpose::STANDARD.decode(trimmed_xdr).is_err() {
534 return Err(StellarTransactionUtilsError::LedgerEntryParseFailed(
535 context.to_string(),
536 "Invalid base64".to_string(),
537 ));
538 }
539
540 match LedgerEntryData::from_xdr_base64(trimmed_xdr, Limits::none()) {
542 Ok(data) => Ok(data),
543 Err(e) => Err(StellarTransactionUtilsError::LedgerEntryParseFailed(
544 context.to_string(),
545 format!("Failed to parse LedgerEntryData: {e}"),
546 )),
547 }
548}
549
550pub fn extract_scval_from_contract_data(
564 ledger_entries: &soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse,
565 context: &str,
566) -> Result<ScVal, StellarTransactionUtilsError> {
567 let entries = ledger_entries
568 .entries
569 .as_ref()
570 .ok_or_else(|| StellarTransactionUtilsError::NoEntriesFound(context.into()))?;
571
572 if entries.is_empty() {
573 return Err(StellarTransactionUtilsError::EmptyEntries(context.into()));
574 }
575
576 let entry_xdr = &entries[0].xdr;
577 let entry = parse_ledger_entry_from_xdr(entry_xdr, context)?;
578
579 match entry {
580 LedgerEntryData::ContractData(ContractDataEntry { val, .. }) => Ok(val.clone()),
581
582 _ => Err(StellarTransactionUtilsError::UnexpectedLedgerEntryType(
583 context.into(),
584 )),
585 }
586}
587
588pub fn extract_u32_from_scval(val: &ScVal, context: &str) -> Option<u32> {
601 let result = match val {
602 ScVal::U32(n) => Ok(*n),
603 ScVal::I32(n) => (*n).try_into().map_err(|_| "Negative I32"),
604 ScVal::U64(n) => (*n).try_into().map_err(|_| "U64 overflow"),
605 ScVal::I64(n) => (*n).try_into().map_err(|_| "I64 overflow/negative"),
606 ScVal::U128(n) => {
607 if n.hi == 0 {
608 n.lo.try_into().map_err(|_| "U128 lo overflow")
609 } else {
610 Err("U128 hi set")
611 }
612 }
613 ScVal::I128(n) => {
614 if n.hi == 0 {
615 n.lo.try_into().map_err(|_| "I128 lo overflow")
616 } else {
617 Err("I128 hi set/negative")
618 }
619 }
620 _ => Err("Unsupported ScVal type"),
621 };
622
623 match result {
624 Ok(v) => Some(v),
625 Err(msg) => {
626 warn!(context = %context, val = ?val, "Failed to extract u32: {}", msg);
627 None
628 }
629 }
630}
631
632pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> String {
641 if decimals == 0 {
642 return amount.to_string();
643 }
644
645 let amount_str = amount.to_string();
646 let len = amount_str.len();
647 let decimals_usize = decimals as usize;
648
649 let combined = if len > decimals_usize {
650 let split_idx = len - decimals_usize;
651 let whole = &amount_str[..split_idx];
652 let frac = &amount_str[split_idx..];
653 format!("{whole}.{frac}")
654 } else {
655 let zeros = "0".repeat(decimals_usize - len);
657 format!("0.{zeros}{amount_str}")
658 };
659
660 let mut trimmed = combined.trim_end_matches('0').to_string();
662 if trimmed.ends_with('.') {
663 trimmed.pop();
664 }
665
666 if trimmed.is_empty() {
668 "0".to_string()
669 } else {
670 trimmed
671 }
672}
673
674pub fn count_operations_from_xdr(xdr: &str) -> Result<usize, StellarTransactionUtilsError> {
678 let envelope = TransactionEnvelope::from_xdr_base64(xdr, Limits::none()).map_err(|e| {
679 StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
680 })?;
681
682 let operations = extract_operations(&envelope).map_err(|e| {
683 StellarTransactionUtilsError::OperationExtractionFailed(format!(
684 "Failed to extract operations: {e}"
685 ))
686 })?;
687
688 Ok(operations.len())
689}
690
691pub fn parse_transaction_and_count_operations(
695 transaction_json: &serde_json::Value,
696) -> Result<usize, StellarTransactionUtilsError> {
697 if let Some(xdr_str) = transaction_json.as_str() {
699 let envelope =
700 TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
701 StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
702 })?;
703
704 let operations = extract_operations(&envelope).map_err(|e| {
705 StellarTransactionUtilsError::OperationExtractionFailed(format!(
706 "Failed to extract operations: {e}"
707 ))
708 })?;
709
710 return Ok(operations.len());
711 }
712
713 if let Some(ops_array) = transaction_json.as_array() {
715 return Ok(ops_array.len());
716 }
717
718 if let Some(obj) = transaction_json.as_object() {
720 if let Some(ops) = obj.get("operations") {
721 if let Some(ops_array) = ops.as_array() {
722 return Ok(ops_array.len());
723 }
724 }
725 if let Some(xdr_str) = obj.get("transaction_xdr").and_then(|v| v.as_str()) {
726 let envelope =
727 TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
728 StellarTransactionUtilsError::XdrParseFailed(format!(
729 "Failed to parse XDR: {e}"
730 ))
731 })?;
732
733 let operations = extract_operations(&envelope).map_err(|e| {
734 StellarTransactionUtilsError::OperationExtractionFailed(format!(
735 "Failed to extract operations: {e}"
736 ))
737 })?;
738
739 return Ok(operations.len());
740 }
741 }
742
743 Err(StellarTransactionUtilsError::InvalidTransactionFormat(
744 "Transaction must be either XDR string or operations array".to_string(),
745 ))
746}
747
748#[derive(Debug)]
750pub struct FeeQuote {
751 pub fee_in_token: u64,
752 pub fee_in_token_ui: String,
753 pub fee_in_stroops: u64,
754 pub conversion_rate: f64,
755}
756
757pub fn estimate_base_fee(num_operations: usize) -> u64 {
761 (num_operations.max(1) as u64) * STELLAR_DEFAULT_TRANSACTION_FEE as u64
762}
763
764pub async fn estimate_fee<P>(
779 envelope: &TransactionEnvelope,
780 provider: &P,
781 operations_override: Option<usize>,
782) -> Result<u64, StellarTransactionUtilsError>
783where
784 P: StellarProviderTrait + Send + Sync,
785{
786 let needs_sim = xdr_needs_simulation(envelope).map_err(|e| {
788 StellarTransactionUtilsError::SimulationCheckFailed(format!(
789 "Failed to check if simulation is needed: {e}"
790 ))
791 })?;
792
793 if needs_sim {
794 debug!("Transaction contains Soroban operations, simulating to get accurate fee");
795
796 let simulation_result = provider
798 .simulate_transaction_envelope(envelope)
799 .await
800 .map_err(|e| {
801 StellarTransactionUtilsError::SimulationFailed(format!(
802 "Failed to simulate transaction: {e}"
803 ))
804 })?;
805
806 if simulation_result.results.is_empty() {
808 return Err(StellarTransactionUtilsError::SimulationNoResults);
809 }
810
811 let resource_fee = simulation_result.min_resource_fee as u64;
814 let inclusion_fee = STELLAR_DEFAULT_TRANSACTION_FEE as u64;
815 let required_fee = inclusion_fee + resource_fee;
816
817 debug!("Simulation returned fee: {} stroops", required_fee);
818 Ok(required_fee)
819 } else {
820 let num_operations = if let Some(override_count) = operations_override {
822 override_count
823 } else {
824 let operations = extract_operations(envelope).map_err(|e| {
825 StellarTransactionUtilsError::OperationExtractionFailed(format!(
826 "Failed to extract operations: {e}"
827 ))
828 })?;
829 operations.len()
830 };
831
832 let fee = estimate_base_fee(num_operations);
833 debug!(
834 "No simulation needed, estimated fee from {} operations: {} stroops",
835 num_operations, fee
836 );
837 Ok(fee)
838 }
839}
840
841pub async fn convert_xlm_fee_to_token<D>(
858 dex_service: &D,
859 policy: &RelayerStellarPolicy,
860 xlm_fee: u64,
861 fee_token: &str,
862) -> Result<FeeQuote, StellarTransactionUtilsError>
863where
864 D: StellarDexServiceTrait + Send + Sync,
865{
866 if fee_token == "native" || fee_token.is_empty() {
868 debug!("Converting XLM fee to native XLM: {}", xlm_fee);
869 let buffered_fee = if let Some(margin) = policy.fee_margin_percentage {
870 (xlm_fee as f64 * (1.0 + margin as f64 / 100.0)) as u64
871 } else {
872 xlm_fee
873 };
874
875 return Ok(FeeQuote {
876 fee_in_token: buffered_fee,
877 fee_in_token_ui: amount_to_ui_amount(buffered_fee, 7),
878 fee_in_stroops: buffered_fee,
879 conversion_rate: 1.0,
880 });
881 }
882
883 debug!("Converting XLM fee to token: {}", fee_token);
884
885 let buffered_xlm_fee = if let Some(margin) = policy.fee_margin_percentage {
887 (xlm_fee as f64 * (1.0 + margin as f64 / 100.0)) as u64
888 } else {
889 xlm_fee
890 };
891
892 let slippage = policy
894 .get_allowed_token_entry(fee_token)
895 .and_then(|token| {
896 token
897 .swap_config
898 .as_ref()
899 .and_then(|config| config.slippage_percentage)
900 })
901 .or(policy.slippage_percentage)
902 .unwrap_or(DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE);
903
904 let token_decimals = policy.get_allowed_token_decimals(fee_token);
907 let quote = dex_service
908 .get_xlm_to_token_quote(fee_token, buffered_xlm_fee, slippage, token_decimals)
909 .await
910 .map_err(|e| {
911 StellarTransactionUtilsError::DexQuoteFailed(format!("Failed to get quote: {e}"))
912 })?;
913
914 debug!(
915 "Quote from DEX: input={} stroops XLM, output={} stroops token, input_asset={}, output_asset={}",
916 quote.in_amount, quote.out_amount, quote.input_asset, quote.output_asset
917 );
918
919 let conversion_rate = if buffered_xlm_fee > 0 {
921 quote.out_amount as f64 / buffered_xlm_fee as f64
922 } else {
923 0.0
924 };
925
926 let fee_quote = FeeQuote {
927 fee_in_token: quote.out_amount,
928 fee_in_token_ui: amount_to_ui_amount(quote.out_amount, token_decimals.unwrap_or(7)),
929 fee_in_stroops: buffered_xlm_fee,
930 conversion_rate,
931 };
932
933 debug!(
934 "Final fee quote: fee_in_token={} stroops ({} {}), fee_in_stroops={} stroops XLM, conversion_rate={}",
935 fee_quote.fee_in_token, fee_quote.fee_in_token_ui, fee_token, fee_quote.fee_in_stroops, fee_quote.conversion_rate
936 );
937
938 Ok(fee_quote)
939}
940
941pub fn parse_transaction_envelope(
943 transaction_json: &serde_json::Value,
944) -> Result<TransactionEnvelope, StellarTransactionUtilsError> {
945 if let Some(xdr_str) = transaction_json.as_str() {
947 return TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
948 StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
949 });
950 }
951
952 if let Some(obj) = transaction_json.as_object() {
954 if let Some(xdr_str) = obj.get("transaction_xdr").and_then(|v| v.as_str()) {
955 return TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
956 StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
957 });
958 }
959 }
960
961 Err(StellarTransactionUtilsError::InvalidTransactionFormat(
962 "Transaction must be XDR string or object with transaction_xdr field".to_string(),
963 ))
964}
965
966pub fn create_fee_payment_operation(
968 destination: &str,
969 asset_id: &str,
970 amount: i64,
971) -> Result<OperationSpec, StellarTransactionUtilsError> {
972 let asset = if asset_id == "native" || asset_id.is_empty() {
974 AssetSpec::Native
975 } else {
976 if let Some(colon_pos) = asset_id.find(':') {
978 let code = asset_id[..colon_pos].to_string();
979 let issuer = asset_id[colon_pos + 1..].to_string();
980
981 if code.len() <= 4 {
983 AssetSpec::Credit4 { code, issuer }
984 } else if code.len() <= 12 {
985 AssetSpec::Credit12 { code, issuer }
986 } else {
987 return Err(StellarTransactionUtilsError::AssetCodeTooLong(
988 12, code,
990 ));
991 }
992 } else {
993 return Err(StellarTransactionUtilsError::InvalidAssetFormat(format!(
994 "Invalid asset identifier format. Expected 'native' or 'CODE:ISSUER', got: {asset_id}"
995 )));
996 }
997 };
998
999 Ok(OperationSpec::Payment {
1000 destination: destination.to_string(),
1001 amount,
1002 asset,
1003 })
1004}
1005
1006pub fn add_operation_to_envelope(
1008 envelope: &mut TransactionEnvelope,
1009 operation: Operation,
1010) -> Result<(), StellarTransactionUtilsError> {
1011 match envelope {
1012 TransactionEnvelope::TxV0(ref mut e) => {
1013 let mut ops: Vec<Operation> = e.tx.operations.iter().cloned().collect();
1015 ops.push(operation);
1016
1017 let operations: VecM<Operation, 100> = ops.try_into().map_err(|_| {
1019 StellarTransactionUtilsError::TooManyOperations(STELLAR_MAX_OPERATIONS)
1020 })?;
1021
1022 e.tx.operations = operations;
1023
1024 e.tx.fee = (e.tx.operations.len() as u32) * STELLAR_DEFAULT_TRANSACTION_FEE;
1026 }
1028 TransactionEnvelope::Tx(ref mut e) => {
1029 let mut ops: Vec<Operation> = e.tx.operations.iter().cloned().collect();
1031 ops.push(operation);
1032
1033 let operations: VecM<Operation, 100> = ops.try_into().map_err(|_| {
1035 StellarTransactionUtilsError::TooManyOperations(STELLAR_MAX_OPERATIONS)
1036 })?;
1037
1038 e.tx.operations = operations;
1039
1040 e.tx.fee = (e.tx.operations.len() as u32) * STELLAR_DEFAULT_TRANSACTION_FEE;
1042 }
1044 TransactionEnvelope::TxFeeBump(_) => {
1045 return Err(StellarTransactionUtilsError::CannotModifyFeeBump);
1046 }
1047 }
1048 Ok(())
1049}
1050
1051pub fn extract_time_bounds(envelope: &TransactionEnvelope) -> Option<&TimeBounds> {
1062 match envelope {
1063 TransactionEnvelope::TxV0(e) => e.tx.time_bounds.as_ref(),
1064 TransactionEnvelope::Tx(e) => match &e.tx.cond {
1065 Preconditions::Time(tb) => Some(tb),
1066 _ => None,
1067 },
1068 TransactionEnvelope::TxFeeBump(fb) => {
1069 match &fb.tx.inner_tx {
1071 soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_tx) => {
1072 match &inner_tx.tx.cond {
1073 Preconditions::Time(tb) => Some(tb),
1074 _ => None,
1075 }
1076 }
1077 }
1078 }
1079 }
1080}
1081
1082pub fn set_time_bounds(
1084 envelope: &mut TransactionEnvelope,
1085 valid_until: DateTime<Utc>,
1086) -> Result<(), StellarTransactionUtilsError> {
1087 let max_time = valid_until.timestamp() as u64;
1088 let time_bounds = TimeBounds {
1089 min_time: TimePoint(0),
1090 max_time: TimePoint(max_time),
1091 };
1092
1093 match envelope {
1094 TransactionEnvelope::TxV0(ref mut e) => {
1095 e.tx.time_bounds = Some(time_bounds);
1096 }
1097 TransactionEnvelope::Tx(ref mut e) => {
1098 e.tx.cond = Preconditions::Time(time_bounds);
1099 }
1100 TransactionEnvelope::TxFeeBump(_) => {
1101 return Err(StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump);
1102 }
1103 }
1104 Ok(())
1105}
1106
1107fn credit_alphanum4_to_asset_id(
1109 alpha4: &AlphaNum4,
1110) -> Result<String, StellarTransactionUtilsError> {
1111 let code_bytes = alpha4.asset_code.0;
1113 let code_len = code_bytes.iter().position(|&b| b == 0).unwrap_or(4);
1114 let code = String::from_utf8(code_bytes[..code_len].to_vec()).map_err(|e| {
1115 StellarTransactionUtilsError::InvalidAssetFormat(format!("Invalid asset code: {e}"))
1116 })?;
1117
1118 let issuer = match &alpha4.issuer.0 {
1120 XdrPublicKey::PublicKeyTypeEd25519(uint256) => {
1121 let bytes: [u8; 32] = uint256.0;
1122 let pk = PublicKey(bytes);
1123 pk.to_string()
1124 }
1125 };
1126
1127 Ok(format!("{code}:{issuer}"))
1128}
1129
1130fn credit_alphanum12_to_asset_id(
1132 alpha12: &AlphaNum12,
1133) -> Result<String, StellarTransactionUtilsError> {
1134 let code_bytes = alpha12.asset_code.0;
1136 let code_len = code_bytes.iter().position(|&b| b == 0).unwrap_or(12);
1137 let code = String::from_utf8(code_bytes[..code_len].to_vec()).map_err(|e| {
1138 StellarTransactionUtilsError::InvalidAssetFormat(format!("Invalid asset code: {e}"))
1139 })?;
1140
1141 let issuer = match &alpha12.issuer.0 {
1143 XdrPublicKey::PublicKeyTypeEd25519(uint256) => {
1144 let bytes: [u8; 32] = uint256.0;
1145 let pk = PublicKey(bytes);
1146 pk.to_string()
1147 }
1148 };
1149
1150 Ok(format!("{code}:{issuer}"))
1151}
1152
1153pub fn change_trust_asset_to_asset_id(
1166 change_trust_asset: &ChangeTrustAsset,
1167) -> Result<Option<String>, StellarTransactionUtilsError> {
1168 match change_trust_asset {
1169 ChangeTrustAsset::Native | ChangeTrustAsset::PoolShare(_) => Ok(None),
1170 ChangeTrustAsset::CreditAlphanum4(alpha4) => {
1171 let asset = Asset::CreditAlphanum4(alpha4.clone());
1173 asset_to_asset_id(&asset).map(Some)
1174 }
1175 ChangeTrustAsset::CreditAlphanum12(alpha12) => {
1176 let asset = Asset::CreditAlphanum12(alpha12.clone());
1178 asset_to_asset_id(&asset).map(Some)
1179 }
1180 }
1181}
1182
1183pub fn asset_to_asset_id(asset: &Asset) -> Result<String, StellarTransactionUtilsError> {
1193 match asset {
1194 Asset::Native => Ok("native".to_string()),
1195 Asset::CreditAlphanum4(alpha4) => credit_alphanum4_to_asset_id(alpha4),
1196 Asset::CreditAlphanum12(alpha12) => credit_alphanum12_to_asset_id(alpha12),
1197 }
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202 use super::*;
1203 use crate::domain::transaction::stellar::test_helpers::TEST_PK;
1204 use crate::models::AssetSpec;
1205 use crate::models::{AuthSpec, ContractSource, WasmSource};
1206
1207 fn payment_op(destination: &str) -> OperationSpec {
1208 OperationSpec::Payment {
1209 destination: destination.to_string(),
1210 amount: 100,
1211 asset: AssetSpec::Native,
1212 }
1213 }
1214
1215 #[test]
1216 fn returns_false_for_only_payment_ops() {
1217 let ops = vec![payment_op(TEST_PK)];
1218 assert!(!needs_simulation(&ops));
1219 }
1220
1221 #[test]
1222 fn returns_true_for_invoke_contract_ops() {
1223 let ops = vec![OperationSpec::InvokeContract {
1224 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1225 .to_string(),
1226 function_name: "transfer".to_string(),
1227 args: vec![],
1228 auth: None,
1229 }];
1230 assert!(needs_simulation(&ops));
1231 }
1232
1233 #[test]
1234 fn returns_true_for_upload_wasm_ops() {
1235 let ops = vec![OperationSpec::UploadWasm {
1236 wasm: WasmSource::Hex {
1237 hex: "deadbeef".to_string(),
1238 },
1239 auth: None,
1240 }];
1241 assert!(needs_simulation(&ops));
1242 }
1243
1244 #[test]
1245 fn returns_true_for_create_contract_ops() {
1246 let ops = vec![OperationSpec::CreateContract {
1247 source: ContractSource::Address {
1248 address: TEST_PK.to_string(),
1249 },
1250 wasm_hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
1251 .to_string(),
1252 salt: None,
1253 constructor_args: None,
1254 auth: None,
1255 }];
1256 assert!(needs_simulation(&ops));
1257 }
1258
1259 #[test]
1260 fn returns_true_for_single_invoke_host_function() {
1261 let ops = vec![OperationSpec::InvokeContract {
1262 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1263 .to_string(),
1264 function_name: "transfer".to_string(),
1265 args: vec![],
1266 auth: Some(AuthSpec::SourceAccount),
1267 }];
1268 assert!(needs_simulation(&ops));
1269 }
1270
1271 #[test]
1272 fn returns_false_for_multiple_payment_ops() {
1273 let ops = vec![payment_op(TEST_PK), payment_op(TEST_PK)];
1274 assert!(!needs_simulation(&ops));
1275 }
1276
1277 mod next_sequence_u64_tests {
1278 use super::*;
1279
1280 #[test]
1281 fn test_increment() {
1282 assert_eq!(next_sequence_u64(0).unwrap(), 1);
1283
1284 assert_eq!(next_sequence_u64(12345).unwrap(), 12346);
1285 }
1286
1287 #[test]
1288 fn test_error_path_overflow_i64_max() {
1289 let result = next_sequence_u64(i64::MAX);
1290 assert!(result.is_err());
1291 match result.unwrap_err() {
1292 RelayerError::ProviderError(msg) => assert_eq!(msg, "sequence overflow"),
1293 _ => panic!("Unexpected error type"),
1294 }
1295 }
1296 }
1297
1298 mod i64_from_u64_tests {
1299 use super::*;
1300
1301 #[test]
1302 fn test_happy_path_conversion() {
1303 assert_eq!(i64_from_u64(0).unwrap(), 0);
1304 assert_eq!(i64_from_u64(12345).unwrap(), 12345);
1305 assert_eq!(i64_from_u64(i64::MAX as u64).unwrap(), i64::MAX);
1306 }
1307
1308 #[test]
1309 fn test_error_path_overflow_u64_max() {
1310 let result = i64_from_u64(u64::MAX);
1311 assert!(result.is_err());
1312 match result.unwrap_err() {
1313 RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1314 _ => panic!("Unexpected error type"),
1315 }
1316 }
1317
1318 #[test]
1319 fn test_edge_case_just_above_i64_max() {
1320 let value = (i64::MAX as u64) + 1;
1322 let result = i64_from_u64(value);
1323 assert!(result.is_err());
1324 match result.unwrap_err() {
1325 RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1326 _ => panic!("Unexpected error type"),
1327 }
1328 }
1329 }
1330
1331 mod is_bad_sequence_error_tests {
1332 use super::*;
1333
1334 #[test]
1335 fn test_detects_txbadseq() {
1336 assert!(is_bad_sequence_error(
1337 "Failed to send transaction: transaction submission failed: TxBadSeq"
1338 ));
1339 assert!(is_bad_sequence_error("Error: TxBadSeq"));
1340 assert!(is_bad_sequence_error("txbadseq"));
1341 assert!(is_bad_sequence_error("TXBADSEQ"));
1342 }
1343
1344 #[test]
1345 fn test_returns_false_for_other_errors() {
1346 assert!(!is_bad_sequence_error("network timeout"));
1347 assert!(!is_bad_sequence_error("insufficient balance"));
1348 assert!(!is_bad_sequence_error("tx_insufficient_fee"));
1349 assert!(!is_bad_sequence_error("bad_auth"));
1350 assert!(!is_bad_sequence_error(""));
1351 }
1352 }
1353
1354 mod status_check_utils_tests {
1355 use crate::models::{
1356 NetworkTransactionData, StellarTransactionData, TransactionError, TransactionInput,
1357 TransactionRepoModel,
1358 };
1359 use crate::utils::mocks::mockutils::create_mock_transaction;
1360 use chrono::{Duration, Utc};
1361
1362 fn create_test_tx_with_age(seconds_ago: i64) -> TransactionRepoModel {
1364 let created_at = (Utc::now() - Duration::seconds(seconds_ago)).to_rfc3339();
1365 let mut tx = create_mock_transaction();
1366 tx.id = format!("test-tx-{}", seconds_ago);
1367 tx.created_at = created_at;
1368 tx.network_data = NetworkTransactionData::Stellar(StellarTransactionData {
1369 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1370 .to_string(),
1371 fee: None,
1372 sequence_number: None,
1373 memo: None,
1374 valid_until: None,
1375 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1376 signatures: vec![],
1377 hash: Some("test-hash-12345".to_string()),
1378 simulation_transaction_data: None,
1379 transaction_input: TransactionInput::Operations(vec![]),
1380 signed_envelope_xdr: None,
1381 });
1382 tx
1383 }
1384
1385 mod get_age_since_created_tests {
1386 use crate::domain::transaction::util::get_age_since_created;
1387
1388 use super::*;
1389
1390 #[test]
1391 fn test_returns_correct_age_for_recent_transaction() {
1392 let tx = create_test_tx_with_age(30); let age = get_age_since_created(&tx).unwrap();
1394
1395 assert!(age.num_seconds() >= 29 && age.num_seconds() <= 31);
1397 }
1398
1399 #[test]
1400 fn test_returns_correct_age_for_old_transaction() {
1401 let tx = create_test_tx_with_age(3600); let age = get_age_since_created(&tx).unwrap();
1403
1404 assert!(age.num_seconds() >= 3599 && age.num_seconds() <= 3601);
1406 }
1407
1408 #[test]
1409 fn test_returns_zero_age_for_just_created_transaction() {
1410 let tx = create_test_tx_with_age(0); let age = get_age_since_created(&tx).unwrap();
1412
1413 assert!(age.num_seconds() >= 0 && age.num_seconds() <= 1);
1415 }
1416
1417 #[test]
1418 fn test_handles_negative_age_gracefully() {
1419 let created_at = (Utc::now() + Duration::seconds(10)).to_rfc3339();
1421 let mut tx = create_mock_transaction();
1422 tx.created_at = created_at;
1423
1424 let age = get_age_since_created(&tx).unwrap();
1425
1426 assert!(age.num_seconds() < 0);
1428 }
1429
1430 #[test]
1431 fn test_returns_error_for_invalid_created_at() {
1432 let mut tx = create_mock_transaction();
1433 tx.created_at = "invalid-timestamp".to_string();
1434
1435 let result = get_age_since_created(&tx);
1436 assert!(result.is_err());
1437
1438 match result.unwrap_err() {
1439 TransactionError::UnexpectedError(msg) => {
1440 assert!(msg.contains("Invalid created_at timestamp"));
1441 }
1442 _ => panic!("Expected UnexpectedError"),
1443 }
1444 }
1445
1446 #[test]
1447 fn test_returns_error_for_empty_created_at() {
1448 let mut tx = create_mock_transaction();
1449 tx.created_at = "".to_string();
1450
1451 let result = get_age_since_created(&tx);
1452 assert!(result.is_err());
1453 }
1454
1455 #[test]
1456 fn test_handles_various_rfc3339_formats() {
1457 let mut tx = create_mock_transaction();
1458
1459 tx.created_at = "2025-01-01T12:00:00Z".to_string();
1461 assert!(get_age_since_created(&tx).is_ok());
1462
1463 tx.created_at = "2025-01-01T12:00:00+00:00".to_string();
1465 assert!(get_age_since_created(&tx).is_ok());
1466
1467 tx.created_at = "2025-01-01T12:00:00.123Z".to_string();
1469 assert!(get_age_since_created(&tx).is_ok());
1470 }
1471 }
1472 }
1473
1474 #[test]
1475 fn test_create_signature_payload_functions() {
1476 use soroban_rs::xdr::{
1477 Hash, SequenceNumber, TransactionEnvelope, TransactionV0, TransactionV0Envelope,
1478 Uint256,
1479 };
1480
1481 let transaction = soroban_rs::xdr::Transaction {
1483 source_account: soroban_rs::xdr::MuxedAccount::Ed25519(Uint256([1u8; 32])),
1484 fee: 100,
1485 seq_num: SequenceNumber(123),
1486 cond: soroban_rs::xdr::Preconditions::None,
1487 memo: soroban_rs::xdr::Memo::None,
1488 operations: vec![].try_into().unwrap(),
1489 ext: soroban_rs::xdr::TransactionExt::V0,
1490 };
1491 let network_id = Hash([2u8; 32]);
1492
1493 let payload = create_transaction_signature_payload(&transaction, &network_id);
1494 assert_eq!(payload.network_id, network_id);
1495
1496 let v0_tx = TransactionV0 {
1498 source_account_ed25519: Uint256([1u8; 32]),
1499 fee: 100,
1500 seq_num: SequenceNumber(123),
1501 time_bounds: None,
1502 memo: soroban_rs::xdr::Memo::None,
1503 operations: vec![].try_into().unwrap(),
1504 ext: soroban_rs::xdr::TransactionV0Ext::V0,
1505 };
1506 let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1507 tx: v0_tx,
1508 signatures: vec![].try_into().unwrap(),
1509 });
1510
1511 let v0_payload = create_signature_payload(&v0_envelope, &network_id).unwrap();
1512 assert_eq!(v0_payload.network_id, network_id);
1513 }
1514
1515 mod convert_v0_to_v1_transaction_tests {
1516 use super::*;
1517 use soroban_rs::xdr::{SequenceNumber, TransactionV0, Uint256};
1518
1519 #[test]
1520 fn test_convert_v0_to_v1_transaction() {
1521 let v0_tx = TransactionV0 {
1523 source_account_ed25519: Uint256([1u8; 32]),
1524 fee: 100,
1525 seq_num: SequenceNumber(123),
1526 time_bounds: None,
1527 memo: soroban_rs::xdr::Memo::None,
1528 operations: vec![].try_into().unwrap(),
1529 ext: soroban_rs::xdr::TransactionV0Ext::V0,
1530 };
1531
1532 let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1534
1535 assert_eq!(v1_tx.fee, v0_tx.fee);
1537 assert_eq!(v1_tx.seq_num, v0_tx.seq_num);
1538 assert_eq!(v1_tx.memo, v0_tx.memo);
1539 assert_eq!(v1_tx.operations, v0_tx.operations);
1540 assert!(matches!(v1_tx.ext, soroban_rs::xdr::TransactionExt::V0));
1541 assert!(matches!(v1_tx.cond, soroban_rs::xdr::Preconditions::None));
1542
1543 match v1_tx.source_account {
1545 soroban_rs::xdr::MuxedAccount::Ed25519(addr) => {
1546 assert_eq!(addr, v0_tx.source_account_ed25519);
1547 }
1548 _ => panic!("Expected Ed25519 muxed account"),
1549 }
1550 }
1551
1552 #[test]
1553 fn test_convert_v0_to_v1_transaction_with_time_bounds() {
1554 let time_bounds = soroban_rs::xdr::TimeBounds {
1556 min_time: soroban_rs::xdr::TimePoint(100),
1557 max_time: soroban_rs::xdr::TimePoint(200),
1558 };
1559
1560 let v0_tx = TransactionV0 {
1561 source_account_ed25519: Uint256([2u8; 32]),
1562 fee: 200,
1563 seq_num: SequenceNumber(456),
1564 time_bounds: Some(time_bounds.clone()),
1565 memo: soroban_rs::xdr::Memo::Text("test".try_into().unwrap()),
1566 operations: vec![].try_into().unwrap(),
1567 ext: soroban_rs::xdr::TransactionV0Ext::V0,
1568 };
1569
1570 let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1572
1573 match v1_tx.cond {
1575 soroban_rs::xdr::Preconditions::Time(tb) => {
1576 assert_eq!(tb, time_bounds);
1577 }
1578 _ => panic!("Expected Time preconditions"),
1579 }
1580 }
1581 }
1582}
1583
1584#[cfg(test)]
1585mod parse_contract_address_tests {
1586 use super::*;
1587 use crate::domain::transaction::stellar::test_helpers::{
1588 TEST_CONTRACT, TEST_PK as TEST_ACCOUNT,
1589 };
1590
1591 #[test]
1592 fn test_parse_valid_contract_address() {
1593 let result = parse_contract_address(TEST_CONTRACT);
1594 assert!(result.is_ok());
1595
1596 let hash = result.unwrap();
1597 assert_eq!(hash.0.len(), 32);
1598 }
1599
1600 #[test]
1601 fn test_parse_invalid_contract_address() {
1602 let result = parse_contract_address("INVALID_CONTRACT");
1603 assert!(result.is_err());
1604
1605 match result.unwrap_err() {
1606 StellarTransactionUtilsError::InvalidContractAddress(addr, _) => {
1607 assert_eq!(addr, "INVALID_CONTRACT");
1608 }
1609 _ => panic!("Expected InvalidContractAddress error"),
1610 }
1611 }
1612
1613 #[test]
1614 fn test_parse_contract_address_wrong_prefix() {
1615 let result = parse_contract_address(TEST_ACCOUNT);
1617 assert!(result.is_err());
1618 }
1619
1620 #[test]
1621 fn test_parse_empty_contract_address() {
1622 let result = parse_contract_address("");
1623 assert!(result.is_err());
1624 }
1625}
1626
1627#[cfg(test)]
1632mod create_contract_data_key_tests {
1633 use super::*;
1634 use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
1635 use stellar_strkey::ed25519::PublicKey;
1636
1637 #[test]
1638 fn test_create_key_without_address() {
1639 let result = create_contract_data_key("Balance", None);
1640 assert!(result.is_ok());
1641
1642 match result.unwrap() {
1643 ScVal::Symbol(sym) => {
1644 assert_eq!(sym.to_string(), "Balance");
1645 }
1646 _ => panic!("Expected Symbol ScVal"),
1647 }
1648 }
1649
1650 #[test]
1651 fn test_create_key_with_address() {
1652 let pk = PublicKey::from_string(TEST_ACCOUNT).unwrap();
1653 let uint256 = Uint256(pk.0);
1654 let account_id = AccountId(soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(uint256));
1655 let sc_address = ScAddress::Account(account_id);
1656
1657 let result = create_contract_data_key("Balance", Some(sc_address.clone()));
1658 assert!(result.is_ok());
1659
1660 match result.unwrap() {
1661 ScVal::Vec(Some(vec)) => {
1662 assert_eq!(vec.0.len(), 2);
1663 match &vec.0[0] {
1664 ScVal::Symbol(sym) => assert_eq!(sym.to_string(), "Balance"),
1665 _ => panic!("Expected Symbol as first element"),
1666 }
1667 match &vec.0[1] {
1668 ScVal::Address(addr) => assert_eq!(addr, &sc_address),
1669 _ => panic!("Expected Address as second element"),
1670 }
1671 }
1672 _ => panic!("Expected Vec ScVal"),
1673 }
1674 }
1675
1676 #[test]
1677 fn test_create_key_invalid_symbol() {
1678 let very_long_symbol = "a".repeat(100);
1680 let result = create_contract_data_key(&very_long_symbol, None);
1681 assert!(result.is_err());
1682
1683 match result.unwrap_err() {
1684 StellarTransactionUtilsError::SymbolCreationFailed(_, _) => {}
1685 _ => panic!("Expected SymbolCreationFailed error"),
1686 }
1687 }
1688
1689 #[test]
1690 fn test_create_key_decimals() {
1691 let result = create_contract_data_key("Decimals", None);
1692 assert!(result.is_ok());
1693 }
1694}
1695
1696#[cfg(test)]
1701mod extract_scval_from_contract_data_tests {
1702 use super::*;
1703 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1704 use soroban_rs::xdr::{
1705 ContractDataDurability, ContractDataEntry, ExtensionPoint, Hash, LedgerEntry,
1706 LedgerEntryData, LedgerEntryExt, ScSymbol, ScVal, WriteXdr,
1707 };
1708
1709 #[test]
1710 fn test_extract_scval_success() {
1711 let contract_data = ContractDataEntry {
1712 ext: ExtensionPoint::V0,
1713 contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1714 key: ScVal::Symbol(ScSymbol::try_from("test").unwrap()),
1715 durability: ContractDataDurability::Persistent,
1716 val: ScVal::U32(42),
1717 };
1718
1719 let ledger_entry = LedgerEntry {
1720 last_modified_ledger_seq: 100,
1721 data: LedgerEntryData::ContractData(contract_data),
1722 ext: LedgerEntryExt::V0,
1723 };
1724
1725 let xdr = ledger_entry
1726 .data
1727 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1728 .unwrap();
1729
1730 let response = GetLedgerEntriesResponse {
1731 entries: Some(vec![LedgerEntryResult {
1732 key: "test_key".to_string(),
1733 xdr,
1734 last_modified_ledger: 100,
1735 live_until_ledger_seq_ledger_seq: None,
1736 }]),
1737 latest_ledger: 100,
1738 };
1739
1740 let result = extract_scval_from_contract_data(&response, "test");
1741 assert!(result.is_ok());
1742
1743 match result.unwrap() {
1744 ScVal::U32(val) => assert_eq!(val, 42),
1745 _ => panic!("Expected U32 ScVal"),
1746 }
1747 }
1748
1749 #[test]
1750 fn test_extract_scval_no_entries() {
1751 let response = GetLedgerEntriesResponse {
1752 entries: None,
1753 latest_ledger: 100,
1754 };
1755
1756 let result = extract_scval_from_contract_data(&response, "test");
1757 assert!(result.is_err());
1758
1759 match result.unwrap_err() {
1760 StellarTransactionUtilsError::NoEntriesFound(_) => {}
1761 _ => panic!("Expected NoEntriesFound error"),
1762 }
1763 }
1764
1765 #[test]
1766 fn test_extract_scval_empty_entries() {
1767 let response = GetLedgerEntriesResponse {
1768 entries: Some(vec![]),
1769 latest_ledger: 100,
1770 };
1771
1772 let result = extract_scval_from_contract_data(&response, "test");
1773 assert!(result.is_err());
1774
1775 match result.unwrap_err() {
1776 StellarTransactionUtilsError::EmptyEntries(_) => {}
1777 _ => panic!("Expected EmptyEntries error"),
1778 }
1779 }
1780}
1781
1782#[cfg(test)]
1787mod extract_u32_from_scval_tests {
1788 use super::*;
1789 use soroban_rs::xdr::{Int128Parts, ScVal, UInt128Parts};
1790
1791 #[test]
1792 fn test_extract_from_u32() {
1793 let val = ScVal::U32(42);
1794 assert_eq!(extract_u32_from_scval(&val, "test"), Some(42));
1795 }
1796
1797 #[test]
1798 fn test_extract_from_i32_positive() {
1799 let val = ScVal::I32(100);
1800 assert_eq!(extract_u32_from_scval(&val, "test"), Some(100));
1801 }
1802
1803 #[test]
1804 fn test_extract_from_i32_negative() {
1805 let val = ScVal::I32(-1);
1806 assert_eq!(extract_u32_from_scval(&val, "test"), None);
1807 }
1808
1809 #[test]
1810 fn test_extract_from_u64() {
1811 let val = ScVal::U64(1000);
1812 assert_eq!(extract_u32_from_scval(&val, "test"), Some(1000));
1813 }
1814
1815 #[test]
1816 fn test_extract_from_u64_overflow() {
1817 let val = ScVal::U64(u64::MAX);
1818 assert_eq!(extract_u32_from_scval(&val, "test"), None);
1819 }
1820
1821 #[test]
1822 fn test_extract_from_i64_positive() {
1823 let val = ScVal::I64(500);
1824 assert_eq!(extract_u32_from_scval(&val, "test"), Some(500));
1825 }
1826
1827 #[test]
1828 fn test_extract_from_i64_negative() {
1829 let val = ScVal::I64(-500);
1830 assert_eq!(extract_u32_from_scval(&val, "test"), None);
1831 }
1832
1833 #[test]
1834 fn test_extract_from_u128_small() {
1835 let val = ScVal::U128(UInt128Parts { hi: 0, lo: 255 });
1836 assert_eq!(extract_u32_from_scval(&val, "test"), Some(255));
1837 }
1838
1839 #[test]
1840 fn test_extract_from_u128_hi_set() {
1841 let val = ScVal::U128(UInt128Parts { hi: 1, lo: 0 });
1842 assert_eq!(extract_u32_from_scval(&val, "test"), None);
1843 }
1844
1845 #[test]
1846 fn test_extract_from_i128_small() {
1847 let val = ScVal::I128(Int128Parts { hi: 0, lo: 123 });
1848 assert_eq!(extract_u32_from_scval(&val, "test"), Some(123));
1849 }
1850
1851 #[test]
1852 fn test_extract_from_unsupported_type() {
1853 let val = ScVal::Bool(true);
1854 assert_eq!(extract_u32_from_scval(&val, "test"), None);
1855 }
1856}
1857
1858#[cfg(test)]
1863mod amount_to_ui_amount_tests {
1864 use super::*;
1865
1866 #[test]
1867 fn test_zero_decimals() {
1868 assert_eq!(amount_to_ui_amount(100, 0), "100");
1869 assert_eq!(amount_to_ui_amount(0, 0), "0");
1870 }
1871
1872 #[test]
1873 fn test_with_decimals_no_padding() {
1874 assert_eq!(amount_to_ui_amount(1000000, 6), "1");
1875 assert_eq!(amount_to_ui_amount(1500000, 6), "1.5");
1876 assert_eq!(amount_to_ui_amount(1234567, 6), "1.234567");
1877 }
1878
1879 #[test]
1880 fn test_with_decimals_needs_padding() {
1881 assert_eq!(amount_to_ui_amount(1, 6), "0.000001");
1882 assert_eq!(amount_to_ui_amount(100, 6), "0.0001");
1883 assert_eq!(amount_to_ui_amount(1000, 3), "1");
1884 }
1885
1886 #[test]
1887 fn test_trailing_zeros_removed() {
1888 assert_eq!(amount_to_ui_amount(1000000, 6), "1");
1889 assert_eq!(amount_to_ui_amount(1500000, 7), "0.15");
1890 assert_eq!(amount_to_ui_amount(10000000, 7), "1");
1891 }
1892
1893 #[test]
1894 fn test_zero_amount() {
1895 assert_eq!(amount_to_ui_amount(0, 6), "0");
1896 assert_eq!(amount_to_ui_amount(0, 0), "0");
1897 }
1898
1899 #[test]
1900 fn test_xlm_7_decimals() {
1901 assert_eq!(amount_to_ui_amount(10000000, 7), "1");
1902 assert_eq!(amount_to_ui_amount(15000000, 7), "1.5");
1903 assert_eq!(amount_to_ui_amount(100, 7), "0.00001");
1904 }
1905}
1906
1907#[cfg(test)]
1913mod count_operations_tests {
1914 use super::*;
1915 use soroban_rs::xdr::{
1916 Limits, MuxedAccount, Operation, OperationBody, PaymentOp, TransactionV1Envelope, Uint256,
1917 WriteXdr,
1918 };
1919
1920 #[test]
1921 fn test_count_operations_from_xdr() {
1922 use soroban_rs::xdr::{Memo, Preconditions, SequenceNumber, Transaction, TransactionExt};
1923
1924 let payment_op = Operation {
1926 source_account: None,
1927 body: OperationBody::Payment(PaymentOp {
1928 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
1929 asset: Asset::Native,
1930 amount: 100,
1931 }),
1932 };
1933
1934 let operations = vec![payment_op.clone(), payment_op].try_into().unwrap();
1935
1936 let tx = Transaction {
1937 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
1938 fee: 100,
1939 seq_num: SequenceNumber(1),
1940 cond: Preconditions::None,
1941 memo: Memo::None,
1942 operations,
1943 ext: TransactionExt::V0,
1944 };
1945
1946 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1947 tx,
1948 signatures: vec![].try_into().unwrap(),
1949 });
1950
1951 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
1952 let count = count_operations_from_xdr(&xdr).unwrap();
1953
1954 assert_eq!(count, 2);
1955 }
1956
1957 #[test]
1958 fn test_count_operations_invalid_xdr() {
1959 let result = count_operations_from_xdr("invalid_xdr");
1960 assert!(result.is_err());
1961
1962 match result.unwrap_err() {
1963 StellarTransactionUtilsError::XdrParseFailed(_) => {}
1964 _ => panic!("Expected XdrParseFailed error"),
1965 }
1966 }
1967}
1968
1969#[cfg(test)]
1974mod estimate_base_fee_tests {
1975 use super::*;
1976
1977 #[test]
1978 fn test_single_operation() {
1979 assert_eq!(estimate_base_fee(1), 100);
1980 }
1981
1982 #[test]
1983 fn test_multiple_operations() {
1984 assert_eq!(estimate_base_fee(5), 500);
1985 assert_eq!(estimate_base_fee(10), 1000);
1986 }
1987
1988 #[test]
1989 fn test_zero_operations() {
1990 assert_eq!(estimate_base_fee(0), 100);
1992 }
1993
1994 #[test]
1995 fn test_large_number_of_operations() {
1996 assert_eq!(estimate_base_fee(100), 10000);
1997 }
1998}
1999
2000#[cfg(test)]
2005mod create_fee_payment_operation_tests {
2006 use super::*;
2007 use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
2008
2009 #[test]
2010 fn test_create_native_payment() {
2011 let result = create_fee_payment_operation(TEST_ACCOUNT, "native", 1000);
2012 assert!(result.is_ok());
2013
2014 match result.unwrap() {
2015 OperationSpec::Payment {
2016 destination,
2017 amount,
2018 asset,
2019 } => {
2020 assert_eq!(destination, TEST_ACCOUNT);
2021 assert_eq!(amount, 1000);
2022 assert!(matches!(asset, AssetSpec::Native));
2023 }
2024 _ => panic!("Expected Payment operation"),
2025 }
2026 }
2027
2028 #[test]
2029 fn test_create_credit4_payment() {
2030 let result = create_fee_payment_operation(
2031 TEST_ACCOUNT,
2032 "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2033 5000,
2034 );
2035 assert!(result.is_ok());
2036
2037 match result.unwrap() {
2038 OperationSpec::Payment {
2039 destination,
2040 amount,
2041 asset,
2042 } => {
2043 assert_eq!(destination, TEST_ACCOUNT);
2044 assert_eq!(amount, 5000);
2045 match asset {
2046 AssetSpec::Credit4 { code, issuer } => {
2047 assert_eq!(code, "USDC");
2048 assert_eq!(
2049 issuer,
2050 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2051 );
2052 }
2053 _ => panic!("Expected Credit4 asset"),
2054 }
2055 }
2056 _ => panic!("Expected Payment operation"),
2057 }
2058 }
2059
2060 #[test]
2061 fn test_create_credit12_payment() {
2062 let result = create_fee_payment_operation(
2063 TEST_ACCOUNT,
2064 "LONGASSETNAM:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2065 2000,
2066 );
2067 assert!(result.is_ok());
2068
2069 match result.unwrap() {
2070 OperationSpec::Payment {
2071 destination,
2072 amount,
2073 asset,
2074 } => {
2075 assert_eq!(destination, TEST_ACCOUNT);
2076 assert_eq!(amount, 2000);
2077 match asset {
2078 AssetSpec::Credit12 { code, issuer } => {
2079 assert_eq!(code, "LONGASSETNAM");
2080 assert_eq!(
2081 issuer,
2082 "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2083 );
2084 }
2085 _ => panic!("Expected Credit12 asset"),
2086 }
2087 }
2088 _ => panic!("Expected Payment operation"),
2089 }
2090 }
2091
2092 #[test]
2093 fn test_create_payment_empty_asset() {
2094 let result = create_fee_payment_operation(TEST_ACCOUNT, "", 1000);
2095 assert!(result.is_ok());
2096
2097 match result.unwrap() {
2098 OperationSpec::Payment { asset, .. } => {
2099 assert!(matches!(asset, AssetSpec::Native));
2100 }
2101 _ => panic!("Expected Payment operation"),
2102 }
2103 }
2104
2105 #[test]
2106 fn test_create_payment_invalid_format() {
2107 let result = create_fee_payment_operation(TEST_ACCOUNT, "INVALID_FORMAT", 1000);
2108 assert!(result.is_err());
2109
2110 match result.unwrap_err() {
2111 StellarTransactionUtilsError::InvalidAssetFormat(_) => {}
2112 _ => panic!("Expected InvalidAssetFormat error"),
2113 }
2114 }
2115
2116 #[test]
2117 fn test_create_payment_asset_code_too_long() {
2118 let result = create_fee_payment_operation(
2119 TEST_ACCOUNT,
2120 "VERYLONGASSETCODE:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2121 1000,
2122 );
2123 assert!(result.is_err());
2124
2125 match result.unwrap_err() {
2126 StellarTransactionUtilsError::AssetCodeTooLong(max_len, _) => {
2127 assert_eq!(max_len, 12);
2128 }
2129 _ => panic!("Expected AssetCodeTooLong error"),
2130 }
2131 }
2132}
2133
2134#[cfg(test)]
2135mod parse_account_id_tests {
2136 use super::*;
2137 use crate::domain::transaction::stellar::test_helpers::TEST_PK;
2138
2139 #[test]
2140 fn test_parse_account_id_valid() {
2141 let result = parse_account_id(TEST_PK);
2142 assert!(result.is_ok());
2143
2144 let account_id = result.unwrap();
2145 match account_id.0 {
2146 soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(_) => {}
2147 }
2148 }
2149
2150 #[test]
2151 fn test_parse_account_id_invalid() {
2152 let result = parse_account_id("INVALID_ADDRESS");
2153 assert!(result.is_err());
2154
2155 match result.unwrap_err() {
2156 StellarTransactionUtilsError::InvalidAccountAddress(addr, _) => {
2157 assert_eq!(addr, "INVALID_ADDRESS");
2158 }
2159 _ => panic!("Expected InvalidAccountAddress error"),
2160 }
2161 }
2162
2163 #[test]
2164 fn test_parse_account_id_empty() {
2165 let result = parse_account_id("");
2166 assert!(result.is_err());
2167 }
2168
2169 #[test]
2170 fn test_parse_account_id_wrong_prefix() {
2171 let result = parse_account_id("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM");
2173 assert!(result.is_err());
2174 }
2175}
2176
2177#[cfg(test)]
2178mod parse_transaction_and_count_operations_tests {
2179 use super::*;
2180 use crate::domain::transaction::stellar::test_helpers::{
2181 create_native_payment_operation, create_xdr_with_operations, TEST_PK, TEST_PK_2,
2182 };
2183 use serde_json::json;
2184
2185 fn create_test_xdr_with_operations(num_ops: usize) -> String {
2186 let payment_op = create_native_payment_operation(TEST_PK_2, 100);
2187 let operations = vec![payment_op; num_ops];
2188 create_xdr_with_operations(TEST_PK, operations, false)
2189 }
2190
2191 #[test]
2192 fn test_parse_xdr_string() {
2193 let xdr = create_test_xdr_with_operations(2);
2194 let json_value = json!(xdr);
2195
2196 let result = parse_transaction_and_count_operations(&json_value);
2197 assert!(result.is_ok());
2198 assert_eq!(result.unwrap(), 2);
2199 }
2200
2201 #[test]
2202 fn test_parse_operations_array() {
2203 let json_value = json!([
2204 {"type": "payment"},
2205 {"type": "payment"},
2206 {"type": "payment"}
2207 ]);
2208
2209 let result = parse_transaction_and_count_operations(&json_value);
2210 assert!(result.is_ok());
2211 assert_eq!(result.unwrap(), 3);
2212 }
2213
2214 #[test]
2215 fn test_parse_object_with_operations() {
2216 let json_value = json!({
2217 "operations": [
2218 {"type": "payment"},
2219 {"type": "payment"}
2220 ]
2221 });
2222
2223 let result = parse_transaction_and_count_operations(&json_value);
2224 assert!(result.is_ok());
2225 assert_eq!(result.unwrap(), 2);
2226 }
2227
2228 #[test]
2229 fn test_parse_object_with_transaction_xdr() {
2230 let xdr = create_test_xdr_with_operations(3);
2231 let json_value = json!({
2232 "transaction_xdr": xdr
2233 });
2234
2235 let result = parse_transaction_and_count_operations(&json_value);
2236 assert!(result.is_ok());
2237 assert_eq!(result.unwrap(), 3);
2238 }
2239
2240 #[test]
2241 fn test_parse_invalid_xdr() {
2242 let json_value = json!("INVALID_XDR");
2243
2244 let result = parse_transaction_and_count_operations(&json_value);
2245 assert!(result.is_err());
2246
2247 match result.unwrap_err() {
2248 StellarTransactionUtilsError::XdrParseFailed(_) => {}
2249 _ => panic!("Expected XdrParseFailed error"),
2250 }
2251 }
2252
2253 #[test]
2254 fn test_parse_invalid_format() {
2255 let json_value = json!(123);
2256
2257 let result = parse_transaction_and_count_operations(&json_value);
2258 assert!(result.is_err());
2259
2260 match result.unwrap_err() {
2261 StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2262 _ => panic!("Expected InvalidTransactionFormat error"),
2263 }
2264 }
2265
2266 #[test]
2267 fn test_parse_empty_operations() {
2268 let json_value = json!([]);
2269
2270 let result = parse_transaction_and_count_operations(&json_value);
2271 assert!(result.is_ok());
2272 assert_eq!(result.unwrap(), 0);
2273 }
2274}
2275
2276#[cfg(test)]
2277mod parse_transaction_envelope_tests {
2278 use super::*;
2279 use crate::domain::transaction::stellar::test_helpers::{
2280 create_unsigned_xdr, TEST_PK, TEST_PK_2,
2281 };
2282 use serde_json::json;
2283
2284 fn create_test_xdr() -> String {
2285 create_unsigned_xdr(TEST_PK, TEST_PK_2)
2286 }
2287
2288 #[test]
2289 fn test_parse_xdr_string() {
2290 let xdr = create_test_xdr();
2291 let json_value = json!(xdr);
2292
2293 let result = parse_transaction_envelope(&json_value);
2294 assert!(result.is_ok());
2295
2296 match result.unwrap() {
2297 TransactionEnvelope::Tx(_) => {}
2298 _ => panic!("Expected Tx envelope"),
2299 }
2300 }
2301
2302 #[test]
2303 fn test_parse_object_with_transaction_xdr() {
2304 let xdr = create_test_xdr();
2305 let json_value = json!({
2306 "transaction_xdr": xdr
2307 });
2308
2309 let result = parse_transaction_envelope(&json_value);
2310 assert!(result.is_ok());
2311
2312 match result.unwrap() {
2313 TransactionEnvelope::Tx(_) => {}
2314 _ => panic!("Expected Tx envelope"),
2315 }
2316 }
2317
2318 #[test]
2319 fn test_parse_invalid_xdr() {
2320 let json_value = json!("INVALID_XDR");
2321
2322 let result = parse_transaction_envelope(&json_value);
2323 assert!(result.is_err());
2324
2325 match result.unwrap_err() {
2326 StellarTransactionUtilsError::XdrParseFailed(_) => {}
2327 _ => panic!("Expected XdrParseFailed error"),
2328 }
2329 }
2330
2331 #[test]
2332 fn test_parse_invalid_format() {
2333 let json_value = json!(123);
2334
2335 let result = parse_transaction_envelope(&json_value);
2336 assert!(result.is_err());
2337
2338 match result.unwrap_err() {
2339 StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2340 _ => panic!("Expected InvalidTransactionFormat error"),
2341 }
2342 }
2343
2344 #[test]
2345 fn test_parse_object_without_xdr() {
2346 let json_value = json!({
2347 "some_field": "value"
2348 });
2349
2350 let result = parse_transaction_envelope(&json_value);
2351 assert!(result.is_err());
2352
2353 match result.unwrap_err() {
2354 StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2355 _ => panic!("Expected InvalidTransactionFormat error"),
2356 }
2357 }
2358}
2359
2360#[cfg(test)]
2361mod add_operation_to_envelope_tests {
2362 use super::*;
2363 use soroban_rs::xdr::{
2364 Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2365 Transaction, TransactionExt, TransactionV0, TransactionV0Envelope, TransactionV1Envelope,
2366 Uint256,
2367 };
2368
2369 fn create_payment_op() -> Operation {
2370 Operation {
2371 source_account: None,
2372 body: OperationBody::Payment(PaymentOp {
2373 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2374 asset: Asset::Native,
2375 amount: 100,
2376 }),
2377 }
2378 }
2379
2380 #[test]
2381 fn test_add_operation_to_tx_v0() {
2382 let payment_op = create_payment_op();
2383 let operations = vec![payment_op.clone()].try_into().unwrap();
2384
2385 let tx = TransactionV0 {
2386 source_account_ed25519: Uint256([0u8; 32]),
2387 fee: 100,
2388 seq_num: SequenceNumber(1),
2389 time_bounds: None,
2390 memo: Memo::None,
2391 operations,
2392 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2393 };
2394
2395 let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2396 tx,
2397 signatures: vec![].try_into().unwrap(),
2398 });
2399
2400 let new_op = create_payment_op();
2401 let result = add_operation_to_envelope(&mut envelope, new_op);
2402
2403 assert!(result.is_ok());
2404
2405 match envelope {
2406 TransactionEnvelope::TxV0(e) => {
2407 assert_eq!(e.tx.operations.len(), 2);
2408 assert_eq!(e.tx.fee, 200); }
2410 _ => panic!("Expected TxV0 envelope"),
2411 }
2412 }
2413
2414 #[test]
2415 fn test_add_operation_to_tx_v1() {
2416 let payment_op = create_payment_op();
2417 let operations = vec![payment_op.clone()].try_into().unwrap();
2418
2419 let tx = Transaction {
2420 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2421 fee: 100,
2422 seq_num: SequenceNumber(1),
2423 cond: Preconditions::None,
2424 memo: Memo::None,
2425 operations,
2426 ext: TransactionExt::V0,
2427 };
2428
2429 let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2430 tx,
2431 signatures: vec![].try_into().unwrap(),
2432 });
2433
2434 let new_op = create_payment_op();
2435 let result = add_operation_to_envelope(&mut envelope, new_op);
2436
2437 assert!(result.is_ok());
2438
2439 match envelope {
2440 TransactionEnvelope::Tx(e) => {
2441 assert_eq!(e.tx.operations.len(), 2);
2442 assert_eq!(e.tx.fee, 200); }
2444 _ => panic!("Expected Tx envelope"),
2445 }
2446 }
2447
2448 #[test]
2449 fn test_add_operation_to_fee_bump_fails() {
2450 let payment_op = create_payment_op();
2452 let operations = vec![payment_op].try_into().unwrap();
2453
2454 let tx = Transaction {
2455 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2456 fee: 100,
2457 seq_num: SequenceNumber(1),
2458 cond: Preconditions::None,
2459 memo: Memo::None,
2460 operations,
2461 ext: TransactionExt::V0,
2462 };
2463
2464 let inner_envelope = TransactionV1Envelope {
2465 tx,
2466 signatures: vec![].try_into().unwrap(),
2467 };
2468
2469 let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
2470
2471 let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
2472 fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
2473 fee: 200,
2474 inner_tx,
2475 ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
2476 };
2477
2478 let mut envelope =
2479 TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
2480 tx: fee_bump_tx,
2481 signatures: vec![].try_into().unwrap(),
2482 });
2483
2484 let new_op = create_payment_op();
2485 let result = add_operation_to_envelope(&mut envelope, new_op);
2486
2487 assert!(result.is_err());
2488
2489 match result.unwrap_err() {
2490 StellarTransactionUtilsError::CannotModifyFeeBump => {}
2491 _ => panic!("Expected CannotModifyFeeBump error"),
2492 }
2493 }
2494}
2495
2496#[cfg(test)]
2497mod extract_time_bounds_tests {
2498 use super::*;
2499 use soroban_rs::xdr::{
2500 Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2501 TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
2502 TransactionV1Envelope, Uint256,
2503 };
2504
2505 fn create_payment_op() -> Operation {
2506 Operation {
2507 source_account: None,
2508 body: OperationBody::Payment(PaymentOp {
2509 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2510 asset: Asset::Native,
2511 amount: 100,
2512 }),
2513 }
2514 }
2515
2516 #[test]
2517 fn test_extract_time_bounds_from_tx_v0_with_bounds() {
2518 let payment_op = create_payment_op();
2519 let operations = vec![payment_op].try_into().unwrap();
2520
2521 let time_bounds = TimeBounds {
2522 min_time: TimePoint(0),
2523 max_time: TimePoint(1000),
2524 };
2525
2526 let tx = TransactionV0 {
2527 source_account_ed25519: Uint256([0u8; 32]),
2528 fee: 100,
2529 seq_num: SequenceNumber(1),
2530 time_bounds: Some(time_bounds.clone()),
2531 memo: Memo::None,
2532 operations,
2533 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2534 };
2535
2536 let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2537 tx,
2538 signatures: vec![].try_into().unwrap(),
2539 });
2540
2541 let result = extract_time_bounds(&envelope);
2542 assert!(result.is_some());
2543
2544 let bounds = result.unwrap();
2545 assert_eq!(bounds.min_time.0, 0);
2546 assert_eq!(bounds.max_time.0, 1000);
2547 }
2548
2549 #[test]
2550 fn test_extract_time_bounds_from_tx_v0_without_bounds() {
2551 let payment_op = create_payment_op();
2552 let operations = vec![payment_op].try_into().unwrap();
2553
2554 let tx = TransactionV0 {
2555 source_account_ed25519: Uint256([0u8; 32]),
2556 fee: 100,
2557 seq_num: SequenceNumber(1),
2558 time_bounds: None,
2559 memo: Memo::None,
2560 operations,
2561 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2562 };
2563
2564 let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2565 tx,
2566 signatures: vec![].try_into().unwrap(),
2567 });
2568
2569 let result = extract_time_bounds(&envelope);
2570 assert!(result.is_none());
2571 }
2572
2573 #[test]
2574 fn test_extract_time_bounds_from_tx_v1_with_time_precondition() {
2575 let payment_op = create_payment_op();
2576 let operations = vec![payment_op].try_into().unwrap();
2577
2578 let time_bounds = TimeBounds {
2579 min_time: TimePoint(0),
2580 max_time: TimePoint(2000),
2581 };
2582
2583 let tx = Transaction {
2584 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2585 fee: 100,
2586 seq_num: SequenceNumber(1),
2587 cond: Preconditions::Time(time_bounds.clone()),
2588 memo: Memo::None,
2589 operations,
2590 ext: TransactionExt::V0,
2591 };
2592
2593 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2594 tx,
2595 signatures: vec![].try_into().unwrap(),
2596 });
2597
2598 let result = extract_time_bounds(&envelope);
2599 assert!(result.is_some());
2600
2601 let bounds = result.unwrap();
2602 assert_eq!(bounds.min_time.0, 0);
2603 assert_eq!(bounds.max_time.0, 2000);
2604 }
2605
2606 #[test]
2607 fn test_extract_time_bounds_from_tx_v1_without_time_precondition() {
2608 let payment_op = create_payment_op();
2609 let operations = vec![payment_op].try_into().unwrap();
2610
2611 let tx = Transaction {
2612 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2613 fee: 100,
2614 seq_num: SequenceNumber(1),
2615 cond: Preconditions::None,
2616 memo: Memo::None,
2617 operations,
2618 ext: TransactionExt::V0,
2619 };
2620
2621 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2622 tx,
2623 signatures: vec![].try_into().unwrap(),
2624 });
2625
2626 let result = extract_time_bounds(&envelope);
2627 assert!(result.is_none());
2628 }
2629
2630 #[test]
2631 fn test_extract_time_bounds_from_fee_bump() {
2632 let payment_op = create_payment_op();
2634 let operations = vec![payment_op].try_into().unwrap();
2635
2636 let time_bounds = TimeBounds {
2637 min_time: TimePoint(0),
2638 max_time: TimePoint(3000),
2639 };
2640
2641 let tx = Transaction {
2642 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2643 fee: 100,
2644 seq_num: SequenceNumber(1),
2645 cond: Preconditions::Time(time_bounds.clone()),
2646 memo: Memo::None,
2647 operations,
2648 ext: TransactionExt::V0,
2649 };
2650
2651 let inner_envelope = TransactionV1Envelope {
2652 tx,
2653 signatures: vec![].try_into().unwrap(),
2654 };
2655
2656 let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
2657
2658 let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
2659 fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
2660 fee: 200,
2661 inner_tx,
2662 ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
2663 };
2664
2665 let envelope =
2666 TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
2667 tx: fee_bump_tx,
2668 signatures: vec![].try_into().unwrap(),
2669 });
2670
2671 let result = extract_time_bounds(&envelope);
2672 assert!(result.is_some());
2673
2674 let bounds = result.unwrap();
2675 assert_eq!(bounds.min_time.0, 0);
2676 assert_eq!(bounds.max_time.0, 3000);
2677 }
2678}
2679
2680#[cfg(test)]
2681mod set_time_bounds_tests {
2682 use super::*;
2683 use chrono::Utc;
2684 use soroban_rs::xdr::{
2685 Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2686 TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
2687 TransactionV1Envelope, Uint256,
2688 };
2689
2690 fn create_payment_op() -> Operation {
2691 Operation {
2692 source_account: None,
2693 body: OperationBody::Payment(PaymentOp {
2694 destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2695 asset: Asset::Native,
2696 amount: 100,
2697 }),
2698 }
2699 }
2700
2701 #[test]
2702 fn test_set_time_bounds_on_tx_v0() {
2703 let payment_op = create_payment_op();
2704 let operations = vec![payment_op].try_into().unwrap();
2705
2706 let tx = TransactionV0 {
2707 source_account_ed25519: Uint256([0u8; 32]),
2708 fee: 100,
2709 seq_num: SequenceNumber(1),
2710 time_bounds: None,
2711 memo: Memo::None,
2712 operations,
2713 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2714 };
2715
2716 let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2717 tx,
2718 signatures: vec![].try_into().unwrap(),
2719 });
2720
2721 let valid_until = Utc::now() + chrono::Duration::seconds(300);
2722 let result = set_time_bounds(&mut envelope, valid_until);
2723
2724 assert!(result.is_ok());
2725
2726 match envelope {
2727 TransactionEnvelope::TxV0(e) => {
2728 assert!(e.tx.time_bounds.is_some());
2729 let bounds = e.tx.time_bounds.unwrap();
2730 assert_eq!(bounds.min_time.0, 0);
2731 assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
2732 }
2733 _ => panic!("Expected TxV0 envelope"),
2734 }
2735 }
2736
2737 #[test]
2738 fn test_set_time_bounds_on_tx_v1() {
2739 let payment_op = create_payment_op();
2740 let operations = vec![payment_op].try_into().unwrap();
2741
2742 let tx = Transaction {
2743 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2744 fee: 100,
2745 seq_num: SequenceNumber(1),
2746 cond: Preconditions::None,
2747 memo: Memo::None,
2748 operations,
2749 ext: TransactionExt::V0,
2750 };
2751
2752 let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2753 tx,
2754 signatures: vec![].try_into().unwrap(),
2755 });
2756
2757 let valid_until = Utc::now() + chrono::Duration::seconds(300);
2758 let result = set_time_bounds(&mut envelope, valid_until);
2759
2760 assert!(result.is_ok());
2761
2762 match envelope {
2763 TransactionEnvelope::Tx(e) => match e.tx.cond {
2764 Preconditions::Time(bounds) => {
2765 assert_eq!(bounds.min_time.0, 0);
2766 assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
2767 }
2768 _ => panic!("Expected Time precondition"),
2769 },
2770 _ => panic!("Expected Tx envelope"),
2771 }
2772 }
2773
2774 #[test]
2775 fn test_set_time_bounds_on_fee_bump_fails() {
2776 let payment_op = create_payment_op();
2778 let operations = vec![payment_op].try_into().unwrap();
2779
2780 let tx = Transaction {
2781 source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2782 fee: 100,
2783 seq_num: SequenceNumber(1),
2784 cond: Preconditions::None,
2785 memo: Memo::None,
2786 operations,
2787 ext: TransactionExt::V0,
2788 };
2789
2790 let inner_envelope = TransactionV1Envelope {
2791 tx,
2792 signatures: vec![].try_into().unwrap(),
2793 };
2794
2795 let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
2796
2797 let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
2798 fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
2799 fee: 200,
2800 inner_tx,
2801 ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
2802 };
2803
2804 let mut envelope =
2805 TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
2806 tx: fee_bump_tx,
2807 signatures: vec![].try_into().unwrap(),
2808 });
2809
2810 let valid_until = Utc::now() + chrono::Duration::seconds(300);
2811 let result = set_time_bounds(&mut envelope, valid_until);
2812
2813 assert!(result.is_err());
2814
2815 match result.unwrap_err() {
2816 StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump => {}
2817 _ => panic!("Expected CannotSetTimeBoundsOnFeeBump error"),
2818 }
2819 }
2820
2821 #[test]
2822 fn test_set_time_bounds_replaces_existing() {
2823 let payment_op = create_payment_op();
2824 let operations = vec![payment_op].try_into().unwrap();
2825
2826 let old_time_bounds = TimeBounds {
2827 min_time: TimePoint(100),
2828 max_time: TimePoint(1000),
2829 };
2830
2831 let tx = TransactionV0 {
2832 source_account_ed25519: Uint256([0u8; 32]),
2833 fee: 100,
2834 seq_num: SequenceNumber(1),
2835 time_bounds: Some(old_time_bounds),
2836 memo: Memo::None,
2837 operations,
2838 ext: soroban_rs::xdr::TransactionV0Ext::V0,
2839 };
2840
2841 let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2842 tx,
2843 signatures: vec![].try_into().unwrap(),
2844 });
2845
2846 let valid_until = Utc::now() + chrono::Duration::seconds(300);
2847 let result = set_time_bounds(&mut envelope, valid_until);
2848
2849 assert!(result.is_ok());
2850
2851 match envelope {
2852 TransactionEnvelope::TxV0(e) => {
2853 assert!(e.tx.time_bounds.is_some());
2854 let bounds = e.tx.time_bounds.unwrap();
2855 assert_eq!(bounds.min_time.0, 0);
2857 assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
2858 }
2859 _ => panic!("Expected TxV0 envelope"),
2860 }
2861 }
2862}