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