1use super::evm::Speed;
2use crate::{
3 config::ServerConfig,
4 constants::{
5 DEFAULT_GAS_LIMIT, DEFAULT_TRANSACTION_SPEED, FINAL_TRANSACTION_STATUSES,
6 STELLAR_DEFAULT_MAX_FEE, STELLAR_DEFAULT_TRANSACTION_FEE,
7 STELLAR_SPONSORED_TRANSACTION_VALIDITY_MINUTES,
8 },
9 domain::{
10 evm::PriceParams,
11 stellar::validation::{validate_operations, validate_soroban_memo_restriction},
12 transaction::stellar::utils::extract_time_bounds,
13 xdr_utils::{is_signed, parse_transaction_xdr},
14 SignTransactionResponseEvm,
15 },
16 models::{
17 transaction::{
18 request::{evm::EvmTransactionRequest, stellar::StellarTransactionRequest},
19 solana::SolanaInstructionSpec,
20 stellar::{DecoratedSignature, MemoSpec, OperationSpec},
21 },
22 AddressError, EvmNetwork, NetworkRepoModel, NetworkTransactionRequest, NetworkType,
23 RelayerError, RelayerRepoModel, SignerError, StellarNetwork, StellarValidationError,
24 TransactionError, U256,
25 },
26 utils::{deserialize_optional_u128, serialize_optional_u128},
27};
28use alloy::{
29 consensus::{TxEip1559, TxLegacy},
30 primitives::{Address as AlloyAddress, Bytes, TxKind},
31 rpc::types::AccessList,
32};
33
34use chrono::{Duration, Utc};
35use serde::{Deserialize, Serialize};
36use soroban_rs::xdr::{TransactionEnvelope, TransactionV1Envelope, VecM};
37use std::{convert::TryFrom, str::FromStr};
38use strum::Display;
39
40use utoipa::ToSchema;
41use uuid::Uuid;
42
43use soroban_rs::xdr::Transaction as SorobanTransaction;
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, Display)]
46#[serde(rename_all = "lowercase")]
47pub enum TransactionStatus {
48 Canceled,
49 Pending,
50 Sent,
51 Submitted,
52 Mined,
53 Confirmed,
54 Failed,
55 Expired,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
59pub struct TransactionMetadata {
61 #[serde(default)]
63 pub consecutive_failures: u32,
64 #[serde(default)]
65 pub total_failures: u32,
66 #[serde(default)]
68 pub insufficient_fee_retries: u32,
69 #[serde(default)]
71 pub try_again_later_retries: u32,
72 #[serde(default)]
74 pub nonce_too_high_retries: u32,
75}
76
77impl TransactionMetadata {
78 pub fn with_nonce_retries_reset(&self) -> Option<Self> {
80 if self.nonce_too_high_retries == 0 {
81 return None;
82 }
83 Some(Self {
84 nonce_too_high_retries: 0,
85 ..self.clone()
86 })
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, Default)]
91pub struct TransactionUpdateRequest {
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub status: Option<TransactionStatus>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub status_reason: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub sent_at: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub confirmed_at: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub network_data: Option<NetworkTransactionData>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub priced_at: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub hashes: Option<Vec<String>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub noop_count: Option<u32>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub is_canceled: Option<bool>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub delete_at: Option<String>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub metadata: Option<TransactionMetadata>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct TransactionRepoModel {
124 pub id: String,
125 pub relayer_id: String,
126 pub status: TransactionStatus,
127 pub status_reason: Option<String>,
128 pub created_at: String,
129 pub sent_at: Option<String>,
130 pub confirmed_at: Option<String>,
131 pub valid_until: Option<String>,
132 pub delete_at: Option<String>,
134 pub network_data: NetworkTransactionData,
135 pub priced_at: Option<String>,
137 pub hashes: Vec<String>,
139 pub network_type: NetworkType,
140 pub noop_count: Option<u32>,
141 pub is_canceled: Option<bool>,
142 #[serde(default)]
144 pub metadata: Option<TransactionMetadata>,
145}
146
147impl TransactionRepoModel {
148 pub fn validate(&self) -> Result<(), TransactionError> {
154 Ok(())
155 }
156
157 fn calculate_delete_at(expiration_hours: f64) -> Option<String> {
160 let seconds = (expiration_hours * 3600.0) as i64;
162 let delete_time = Utc::now() + Duration::seconds(seconds);
163 Some(delete_time.to_rfc3339())
164 }
165
166 pub fn update_delete_at_if_final_status(&mut self) {
168 if self.delete_at.is_none() && FINAL_TRANSACTION_STATUSES.contains(&self.status) {
169 let expiration_hours = ServerConfig::get_transaction_expiration_hours();
170 self.delete_at = Self::calculate_delete_at(expiration_hours);
171 }
172 }
173
174 pub fn apply_partial_update(&mut self, update: TransactionUpdateRequest) {
182 if let Some(status) = update.status {
184 self.status = status;
185 self.update_delete_at_if_final_status();
186 }
187 if let Some(status_reason) = update.status_reason {
188 self.status_reason = Some(status_reason);
189 }
190 if let Some(sent_at) = update.sent_at {
191 self.sent_at = Some(sent_at);
192 }
193 if let Some(confirmed_at) = update.confirmed_at {
194 self.confirmed_at = Some(confirmed_at);
195 }
196 if let Some(network_data) = update.network_data {
197 self.network_data = network_data;
198 }
199 if let Some(priced_at) = update.priced_at {
200 self.priced_at = Some(priced_at);
201 }
202 if let Some(hashes) = update.hashes {
203 self.hashes = hashes;
204 }
205 if let Some(noop_count) = update.noop_count {
206 self.noop_count = Some(noop_count);
207 }
208 if let Some(is_canceled) = update.is_canceled {
209 self.is_canceled = Some(is_canceled);
210 }
211 if let Some(delete_at) = update.delete_at {
212 self.delete_at = Some(delete_at);
213 }
214 if let Some(metadata) = update.metadata {
215 self.metadata = Some(metadata);
216 }
217 }
218
219 pub fn create_reset_update_request(
230 &self,
231 ) -> Result<TransactionUpdateRequest, TransactionError> {
232 let network_data = match &self.network_data {
233 NetworkTransactionData::Stellar(stellar_data) => Some(NetworkTransactionData::Stellar(
234 stellar_data.clone().reset_to_pre_prepare_state(),
235 )),
236 _ => None,
238 };
239
240 Ok(TransactionUpdateRequest {
241 status: Some(TransactionStatus::Pending),
242 status_reason: None,
243 sent_at: None,
244 confirmed_at: None,
245 network_data,
246 priced_at: None,
247 hashes: Some(vec![]),
248 noop_count: None,
249 is_canceled: None,
250 delete_at: None,
251 metadata: None,
252 })
253 }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(tag = "network_data", content = "data")]
258#[allow(clippy::large_enum_variant)]
259pub enum NetworkTransactionData {
260 Evm(EvmTransactionData),
261 Solana(SolanaTransactionData),
262 Stellar(StellarTransactionData),
263}
264
265impl NetworkTransactionData {
266 pub fn get_evm_transaction_data(&self) -> Result<EvmTransactionData, TransactionError> {
267 match self {
268 NetworkTransactionData::Evm(data) => Ok(data.clone()),
269 _ => Err(TransactionError::InvalidType(
270 "Expected EVM transaction".to_string(),
271 )),
272 }
273 }
274
275 pub fn get_solana_transaction_data(&self) -> Result<SolanaTransactionData, TransactionError> {
276 match self {
277 NetworkTransactionData::Solana(data) => Ok(data.clone()),
278 _ => Err(TransactionError::InvalidType(
279 "Expected Solana transaction".to_string(),
280 )),
281 }
282 }
283
284 pub fn get_stellar_transaction_data(&self) -> Result<StellarTransactionData, TransactionError> {
285 match self {
286 NetworkTransactionData::Stellar(data) => Ok(data.clone()),
287 _ => Err(TransactionError::InvalidType(
288 "Expected Stellar transaction".to_string(),
289 )),
290 }
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
295pub struct EvmTransactionDataSignature {
296 pub r: String,
297 pub s: String,
298 pub v: u8,
299 pub sig: String,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct EvmTransactionData {
304 #[serde(
305 serialize_with = "serialize_optional_u128",
306 deserialize_with = "deserialize_optional_u128",
307 default
308 )]
309 pub gas_price: Option<u128>,
310 pub gas_limit: Option<u64>,
311 pub nonce: Option<u64>,
312 pub value: U256,
313 pub data: Option<String>,
314 pub from: String,
315 pub to: Option<String>,
316 pub chain_id: u64,
317 pub hash: Option<String>,
318 pub signature: Option<EvmTransactionDataSignature>,
319 pub speed: Option<Speed>,
320 #[serde(
321 serialize_with = "serialize_optional_u128",
322 deserialize_with = "deserialize_optional_u128",
323 default
324 )]
325 pub max_fee_per_gas: Option<u128>,
326 #[serde(
327 serialize_with = "serialize_optional_u128",
328 deserialize_with = "deserialize_optional_u128",
329 default
330 )]
331 pub max_priority_fee_per_gas: Option<u128>,
332 pub raw: Option<Vec<u8>>,
333}
334
335impl EvmTransactionData {
336 pub fn for_replacement(old_data: &EvmTransactionData, request: &EvmTransactionRequest) -> Self {
348 Self {
349 chain_id: old_data.chain_id,
351 from: old_data.from.clone(),
352 nonce: old_data.nonce, to: request.to.clone(),
356 value: request.value,
357 data: request.data.clone(),
358 gas_limit: request.gas_limit,
359 speed: request
360 .speed
361 .clone()
362 .or_else(|| old_data.speed.clone())
363 .or(Some(DEFAULT_TRANSACTION_SPEED)),
364
365 gas_price: None,
367 max_fee_per_gas: None,
368 max_priority_fee_per_gas: None,
369
370 signature: None,
372 hash: None,
373 raw: None,
374 }
375 }
376
377 pub fn with_price_params(mut self, price_params: PriceParams) -> Self {
385 self.gas_price = price_params.gas_price;
386 self.max_fee_per_gas = price_params.max_fee_per_gas;
387 self.max_priority_fee_per_gas = price_params.max_priority_fee_per_gas;
388
389 self
390 }
391
392 pub fn with_gas_estimate(mut self, gas_limit: u64) -> Self {
400 self.gas_limit = Some(gas_limit);
401 self
402 }
403
404 pub fn with_nonce(mut self, nonce: u64) -> Self {
412 self.nonce = Some(nonce);
413 self
414 }
415
416 pub fn with_signed_transaction_data(mut self, sig: SignTransactionResponseEvm) -> Self {
424 self.signature = Some(sig.signature);
425 self.hash = Some(sig.hash);
426 self.raw = Some(sig.raw);
427 self
428 }
429}
430
431#[cfg(test)]
432impl Default for EvmTransactionData {
433 fn default() -> Self {
434 Self {
435 from: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(), to: Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string()), gas_price: Some(20000000000),
438 value: U256::from(1000000000000000000u128), data: Some("0x".to_string()),
440 nonce: Some(1),
441 chain_id: 1,
442 gas_limit: Some(DEFAULT_GAS_LIMIT),
443 hash: None,
444 signature: None,
445 speed: None,
446 max_fee_per_gas: None,
447 max_priority_fee_per_gas: None,
448 raw: None,
449 }
450 }
451}
452
453#[cfg(test)]
454impl Default for TransactionRepoModel {
455 fn default() -> Self {
456 Self {
457 id: "00000000-0000-0000-0000-000000000001".to_string(),
458 relayer_id: "00000000-0000-0000-0000-000000000002".to_string(),
459 status: TransactionStatus::Pending,
460 created_at: "2023-01-01T00:00:00Z".to_string(),
461 status_reason: None,
462 sent_at: None,
463 confirmed_at: None,
464 valid_until: None,
465 delete_at: None,
466 network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
467 network_type: NetworkType::Evm,
468 priced_at: None,
469 hashes: Vec::new(),
470 noop_count: None,
471 is_canceled: Some(false),
472 metadata: None,
473 }
474 }
475}
476
477pub trait EvmTransactionDataTrait {
478 fn is_legacy(&self) -> bool;
479 fn is_eip1559(&self) -> bool;
480 fn is_speed(&self) -> bool;
481}
482
483impl EvmTransactionDataTrait for EvmTransactionData {
484 fn is_legacy(&self) -> bool {
485 self.gas_price.is_some()
486 }
487
488 fn is_eip1559(&self) -> bool {
489 self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some()
490 }
491
492 fn is_speed(&self) -> bool {
493 self.speed.is_some()
494 }
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize, Default)]
498pub struct SolanaTransactionData {
499 pub transaction: Option<String>,
501 pub instructions: Option<Vec<SolanaInstructionSpec>>,
503 pub signature: Option<String>,
505}
506
507impl SolanaTransactionData {
508 pub fn with_signature(mut self, signature: String) -> Self {
511 self.signature = Some(signature);
512 self
513 }
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518pub enum TransactionInput {
519 Operations(Vec<OperationSpec>),
521 UnsignedXdr(String),
523 SignedXdr { xdr: String, max_fee: i64 },
525 SorobanGasAbstraction {
529 xdr: String,
530 signed_auth_entry: String,
531 },
532}
533
534impl Default for TransactionInput {
535 fn default() -> Self {
536 TransactionInput::Operations(vec![])
537 }
538}
539
540impl TransactionInput {
541 pub fn from_stellar_request(
543 request: &StellarTransactionRequest,
544 ) -> Result<Self, TransactionError> {
545 if let (Some(xdr), Some(signed_auth_entry)) =
547 (&request.transaction_xdr, &request.signed_auth_entry)
548 {
549 if request.fee_bump == Some(true) {
552 return Err(TransactionError::ValidationError(
553 "Cannot use both signed_auth_entry and fee_bump".to_string(),
554 ));
555 }
556
557 return Ok(TransactionInput::SorobanGasAbstraction {
558 xdr: xdr.clone(),
559 signed_auth_entry: signed_auth_entry.clone(),
560 });
561 }
562
563 if let Some(xdr) = &request.transaction_xdr {
565 let envelope = parse_transaction_xdr(xdr, false)
566 .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
567
568 return if request.fee_bump == Some(true) {
569 if !is_signed(&envelope) {
571 Err(TransactionError::ValidationError(
572 "Cannot request fee_bump with unsigned XDR".to_string(),
573 ))
574 } else {
575 let max_fee = request.max_fee.unwrap_or(STELLAR_DEFAULT_MAX_FEE);
576 Ok(TransactionInput::SignedXdr {
577 xdr: xdr.clone(),
578 max_fee,
579 })
580 }
581 } else {
582 if is_signed(&envelope) {
584 Err(TransactionError::ValidationError(
585 StellarValidationError::UnexpectedSignedXdr.to_string(),
586 ))
587 } else {
588 Ok(TransactionInput::UnsignedXdr(xdr.clone()))
589 }
590 };
591 }
592
593 if let Some(operations) = &request.operations {
595 if operations.is_empty() {
596 return Err(TransactionError::ValidationError(
597 "Operations must not be empty".to_string(),
598 ));
599 }
600
601 if request.fee_bump == Some(true) {
602 return Err(TransactionError::ValidationError(
603 "Cannot request fee_bump with operations mode".to_string(),
604 ));
605 }
606
607 validate_operations(operations)
609 .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
610
611 validate_soroban_memo_restriction(operations, &request.memo)
613 .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
614
615 return Ok(TransactionInput::Operations(operations.clone()));
616 }
617
618 Err(TransactionError::ValidationError(
620 "Must provide either operations or transaction_xdr".to_string(),
621 ))
622 }
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
626pub struct StellarTransactionData {
627 pub source_account: String,
628 pub fee: Option<u32>,
629 pub sequence_number: Option<i64>,
630 pub memo: Option<MemoSpec>,
631 pub valid_until: Option<String>,
632 pub network_passphrase: String,
633 pub signatures: Vec<DecoratedSignature>,
634 pub hash: Option<String>,
635 pub simulation_transaction_data: Option<String>,
636 pub transaction_input: TransactionInput,
637 pub signed_envelope_xdr: Option<String>,
638 pub transaction_result_xdr: Option<String>,
639}
640
641impl StellarTransactionData {
642 pub fn reset_to_pre_prepare_state(mut self) -> Self {
651 self.fee = None;
653 self.sequence_number = None;
654 self.signatures = vec![];
655 self.signed_envelope_xdr = None;
656 self.simulation_transaction_data = None;
657
658 self.hash = None;
660
661 self
662 }
663
664 pub fn with_sequence_number(mut self, sequence_number: i64) -> Self {
672 self.sequence_number = Some(sequence_number);
673 self
674 }
675
676 pub fn with_fee(mut self, fee: u32) -> Self {
684 self.fee = Some(fee);
685 self
686 }
687
688 pub fn with_transaction_result_xdr(mut self, transaction_result_xdr: String) -> Self {
696 self.transaction_result_xdr = Some(transaction_result_xdr);
697 self
698 }
699
700 pub fn build_unsigned_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
708 match &self.transaction_input {
709 TransactionInput::Operations(_) => {
710 self.build_envelope_from_operations_unsigned()
712 }
713 TransactionInput::UnsignedXdr(xdr) => {
714 self.parse_xdr_envelope(xdr)
716 }
717 TransactionInput::SignedXdr { xdr, .. } => {
718 self.parse_xdr_envelope(xdr)
720 }
721 TransactionInput::SorobanGasAbstraction { xdr, .. } => {
722 self.parse_xdr_envelope(xdr)
724 }
725 }
726 }
727
728 pub fn get_envelope_for_simulation(&self) -> Result<TransactionEnvelope, SignerError> {
736 self.build_unsigned_envelope()
737 }
738
739 pub fn build_signed_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
747 if let Some(ref xdr) = self.signed_envelope_xdr {
749 return self.parse_xdr_envelope(xdr);
750 }
751
752 match &self.transaction_input {
754 TransactionInput::Operations(_) => {
755 self.build_envelope_from_operations_signed()
757 }
758 TransactionInput::UnsignedXdr(xdr) => {
759 let envelope = self.parse_xdr_envelope(xdr)?;
761 self.attach_signatures_to_envelope(envelope)
762 }
763 TransactionInput::SignedXdr { xdr, .. } => {
764 self.parse_xdr_envelope(xdr)
766 }
767 TransactionInput::SorobanGasAbstraction { xdr, .. } => {
768 let envelope = self.parse_xdr_envelope(xdr)?;
771 self.attach_signatures_to_envelope(envelope)
772 }
773 }
774 }
775
776 pub fn get_envelope_for_submission(&self) -> Result<TransactionEnvelope, SignerError> {
784 self.build_signed_envelope()
785 }
786
787 fn build_envelope_from_operations_unsigned(&self) -> Result<TransactionEnvelope, SignerError> {
789 let tx = SorobanTransaction::try_from(self.clone())?;
790 Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
791 tx,
792 signatures: VecM::default(),
793 }))
794 }
795
796 fn build_envelope_from_operations_signed(&self) -> Result<TransactionEnvelope, SignerError> {
798 let tx = SorobanTransaction::try_from(self.clone())?;
799 let signatures = VecM::try_from(self.signatures.clone())
800 .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
801 Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
802 tx,
803 signatures,
804 }))
805 }
806
807 fn parse_xdr_envelope(&self, xdr: &str) -> Result<TransactionEnvelope, SignerError> {
809 use soroban_rs::xdr::{Limits, ReadXdr};
810 TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
811 .map_err(|e| SignerError::ConversionError(format!("Invalid XDR: {e}")))
812 }
813
814 fn attach_signatures_to_envelope(
816 &self,
817 envelope: TransactionEnvelope,
818 ) -> Result<TransactionEnvelope, SignerError> {
819 use soroban_rs::xdr::{Limits, ReadXdr, WriteXdr};
820
821 let envelope_xdr = envelope.to_xdr_base64(Limits::none()).map_err(|e| {
823 SignerError::ConversionError(format!("Failed to serialize envelope: {e}"))
824 })?;
825
826 let mut envelope = TransactionEnvelope::from_xdr_base64(&envelope_xdr, Limits::none())
827 .map_err(|e| SignerError::ConversionError(format!("Failed to parse envelope: {e}")))?;
828
829 let sigs = VecM::try_from(self.signatures.clone())
830 .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
831
832 match &mut envelope {
833 TransactionEnvelope::Tx(ref mut v1) => v1.signatures = sigs,
834 TransactionEnvelope::TxV0(ref mut v0) => v0.signatures = sigs,
835 TransactionEnvelope::TxFeeBump(_) => {
836 return Err(SignerError::ConversionError(
837 "Cannot attach signatures to fee-bump transaction directly".into(),
838 ));
839 }
840 }
841
842 Ok(envelope)
843 }
844
845 pub fn attach_signature(mut self, sig: DecoratedSignature) -> Self {
853 self.signatures.push(sig);
854 self
855 }
856
857 pub fn with_hash(mut self, hash: String) -> Self {
865 self.hash = Some(hash);
866 self
867 }
868
869 pub fn with_simulation_data(
871 mut self,
872 sim_response: soroban_rs::stellar_rpc_client::SimulateTransactionResponse,
873 operations_count: u64,
874 ) -> Result<Self, SignerError> {
875 use tracing::info;
876
877 let inclusion_fee = operations_count * STELLAR_DEFAULT_TRANSACTION_FEE as u64;
879 let resource_fee = sim_response.min_resource_fee;
880
881 let updated_fee = u32::try_from(inclusion_fee + resource_fee)
882 .map_err(|_| SignerError::ConversionError("Fee too high".to_string()))?
883 .max(STELLAR_DEFAULT_TRANSACTION_FEE);
884 self.fee = Some(updated_fee);
885
886 self.simulation_transaction_data = Some(sim_response.transaction_data);
888
889 info!(
890 "Applied simulation fee: {} stroops and stored transaction extension data",
891 updated_fee
892 );
893 Ok(self)
894 }
895}
896
897fn extract_stellar_valid_until(
899 stellar_request: &StellarTransactionRequest,
900 now: chrono::DateTime<Utc>,
901) -> Option<String> {
902 if let Some(vu) = &stellar_request.valid_until {
903 return Some(vu.clone());
904 }
905
906 if let Some(xdr) = &stellar_request.transaction_xdr {
907 if let Ok(envelope) = parse_transaction_xdr(xdr, false) {
908 if let Some(tb) = extract_time_bounds(&envelope) {
909 if tb.max_time.0 == 0 {
910 return None; }
912 if let Ok(timestamp) = i64::try_from(tb.max_time.0) {
913 if let Some(dt) = chrono::DateTime::from_timestamp(timestamp, 0) {
914 return Some(dt.to_rfc3339());
915 }
916 }
917 }
918 }
919 return None;
920 }
921
922 let default = now + Duration::minutes(STELLAR_SPONSORED_TRANSACTION_VALIDITY_MINUTES);
923 Some(default.to_rfc3339())
924}
925
926impl
927 TryFrom<(
928 &NetworkTransactionRequest,
929 &RelayerRepoModel,
930 &NetworkRepoModel,
931 )> for TransactionRepoModel
932{
933 type Error = RelayerError;
934
935 fn try_from(
936 (request, relayer_model, network_model): (
937 &NetworkTransactionRequest,
938 &RelayerRepoModel,
939 &NetworkRepoModel,
940 ),
941 ) -> Result<Self, Self::Error> {
942 let now = Utc::now().to_rfc3339();
943
944 match request {
945 NetworkTransactionRequest::Evm(evm_request) => {
946 let network = EvmNetwork::try_from(network_model.clone())?;
947 Ok(Self {
948 id: Uuid::new_v4().to_string(),
949 relayer_id: relayer_model.id.clone(),
950 status: TransactionStatus::Pending,
951 status_reason: None,
952 created_at: now,
953 sent_at: None,
954 confirmed_at: None,
955 valid_until: evm_request.valid_until.clone(),
956 delete_at: None,
957 network_type: NetworkType::Evm,
958 network_data: NetworkTransactionData::Evm(EvmTransactionData {
959 gas_price: evm_request.gas_price,
960 gas_limit: evm_request.gas_limit,
961 nonce: None,
962 value: evm_request.value,
963 data: evm_request.data.clone(),
964 from: relayer_model.address.clone(),
965 to: evm_request.to.clone(),
966 chain_id: network.id(),
967 hash: None,
968 signature: None,
969 speed: evm_request.speed.clone(),
970 max_fee_per_gas: evm_request.max_fee_per_gas,
971 max_priority_fee_per_gas: evm_request.max_priority_fee_per_gas,
972 raw: None,
973 }),
974 priced_at: None,
975 hashes: Vec::new(),
976 noop_count: None,
977 is_canceled: Some(false),
978 metadata: None,
979 })
980 }
981 NetworkTransactionRequest::Solana(solana_request) => Ok(Self {
982 id: Uuid::new_v4().to_string(),
983 relayer_id: relayer_model.id.clone(),
984 status: TransactionStatus::Pending,
985 status_reason: None,
986 created_at: now,
987 sent_at: None,
988 confirmed_at: None,
989 valid_until: solana_request.valid_until.clone(),
990 delete_at: None,
991 network_type: NetworkType::Solana,
992 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
993 transaction: solana_request.transaction.clone().map(|t| t.into_inner()),
994 instructions: solana_request.instructions.clone(),
995 signature: None,
996 }),
997 priced_at: None,
998 hashes: Vec::new(),
999 noop_count: None,
1000 is_canceled: Some(false),
1001 metadata: None,
1002 }),
1003 NetworkTransactionRequest::Stellar(stellar_request) => {
1004 let source_account = stellar_request.source_account.clone();
1006
1007 let valid_until = extract_stellar_valid_until(stellar_request, Utc::now());
1008
1009 let transaction_input = TransactionInput::from_stellar_request(stellar_request)
1010 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
1011
1012 let stellar_data = StellarTransactionData {
1013 source_account: source_account.unwrap_or_else(|| relayer_model.address.clone()),
1014 memo: stellar_request.memo.clone(),
1015 valid_until: valid_until.clone(),
1016 network_passphrase: StellarNetwork::try_from(network_model.clone())?.passphrase,
1017 signatures: Vec::new(),
1018 hash: None,
1019 fee: None,
1020 sequence_number: None,
1021 simulation_transaction_data: None,
1022 transaction_input,
1023 signed_envelope_xdr: None,
1024 transaction_result_xdr: None,
1025 };
1026
1027 Ok(Self {
1028 id: Uuid::new_v4().to_string(),
1029 relayer_id: relayer_model.id.clone(),
1030 status: TransactionStatus::Pending,
1031 status_reason: None,
1032 created_at: now,
1033 sent_at: None,
1034 confirmed_at: None,
1035 valid_until,
1036 delete_at: None,
1037 network_type: NetworkType::Stellar,
1038 network_data: NetworkTransactionData::Stellar(stellar_data),
1039 priced_at: None,
1040 hashes: Vec::new(),
1041 noop_count: None,
1042 is_canceled: Some(false),
1043 metadata: None,
1044 })
1045 }
1046 }
1047 }
1048}
1049
1050impl EvmTransactionData {
1051 pub fn to_address(&self) -> Result<Option<AlloyAddress>, SignerError> {
1058 Ok(match self.to.as_deref().filter(|s| !s.is_empty()) {
1059 Some(addr_str) => Some(AlloyAddress::from_str(addr_str).map_err(|e| {
1060 AddressError::ConversionError(format!("Invalid 'to' address: {e}"))
1061 })?),
1062 None => None,
1063 })
1064 }
1065
1066 pub fn data_to_bytes(&self) -> Result<Bytes, SignerError> {
1072 Bytes::from_str(self.data.as_deref().unwrap_or(""))
1073 .map_err(|e| SignerError::SigningError(format!("Invalid transaction data: {e}")))
1074 }
1075}
1076
1077impl TryFrom<NetworkTransactionData> for TxLegacy {
1078 type Error = SignerError;
1079
1080 fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
1081 match tx {
1082 NetworkTransactionData::Evm(tx) => {
1083 let tx_kind = match tx.to_address()? {
1084 Some(addr) => TxKind::Call(addr),
1085 None => TxKind::Create,
1086 };
1087
1088 Ok(Self {
1089 chain_id: Some(tx.chain_id),
1090 nonce: tx.nonce.unwrap_or(0),
1091 gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
1092 gas_price: tx.gas_price.unwrap_or(0),
1093 to: tx_kind,
1094 value: tx.value,
1095 input: tx.data_to_bytes()?,
1096 })
1097 }
1098 _ => Err(SignerError::SigningError(
1099 "Not an EVM transaction".to_string(),
1100 )),
1101 }
1102 }
1103}
1104
1105impl TryFrom<NetworkTransactionData> for TxEip1559 {
1106 type Error = SignerError;
1107
1108 fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
1109 match tx {
1110 NetworkTransactionData::Evm(tx) => {
1111 let tx_kind = match tx.to_address()? {
1112 Some(addr) => TxKind::Call(addr),
1113 None => TxKind::Create,
1114 };
1115
1116 Ok(Self {
1117 chain_id: tx.chain_id,
1118 nonce: tx.nonce.unwrap_or(0),
1119 gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
1120 max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
1121 max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
1122 to: tx_kind,
1123 value: tx.value,
1124 access_list: AccessList::default(),
1125 input: tx.data_to_bytes()?,
1126 })
1127 }
1128 _ => Err(SignerError::SigningError(
1129 "Not an EVM transaction".to_string(),
1130 )),
1131 }
1132 }
1133}
1134
1135impl TryFrom<&EvmTransactionData> for TxLegacy {
1136 type Error = SignerError;
1137
1138 fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
1139 let tx_kind = match tx.to_address()? {
1140 Some(addr) => TxKind::Call(addr),
1141 None => TxKind::Create,
1142 };
1143
1144 Ok(Self {
1145 chain_id: Some(tx.chain_id),
1146 nonce: tx.nonce.unwrap_or(0),
1147 gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
1148 gas_price: tx.gas_price.unwrap_or(0),
1149 to: tx_kind,
1150 value: tx.value,
1151 input: tx.data_to_bytes()?,
1152 })
1153 }
1154}
1155
1156impl TryFrom<EvmTransactionData> for TxLegacy {
1157 type Error = SignerError;
1158
1159 fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
1160 Self::try_from(&tx)
1161 }
1162}
1163
1164impl TryFrom<&EvmTransactionData> for TxEip1559 {
1165 type Error = SignerError;
1166
1167 fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
1168 let tx_kind = match tx.to_address()? {
1169 Some(addr) => TxKind::Call(addr),
1170 None => TxKind::Create,
1171 };
1172
1173 Ok(Self {
1174 chain_id: tx.chain_id,
1175 nonce: tx.nonce.unwrap_or(0),
1176 gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
1177 max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
1178 max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
1179 to: tx_kind,
1180 value: tx.value,
1181 access_list: AccessList::default(),
1182 input: tx.data_to_bytes()?,
1183 })
1184 }
1185}
1186
1187impl TryFrom<EvmTransactionData> for TxEip1559 {
1188 type Error = SignerError;
1189
1190 fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
1191 Self::try_from(&tx)
1192 }
1193}
1194
1195impl From<&[u8; 65]> for EvmTransactionDataSignature {
1196 fn from(bytes: &[u8; 65]) -> Self {
1197 Self {
1198 r: hex::encode(&bytes[0..32]),
1199 s: hex::encode(&bytes[32..64]),
1200 v: bytes[64],
1201 sig: hex::encode(bytes),
1202 }
1203 }
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208 use lazy_static::lazy_static;
1209 use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
1210 use std::sync::Mutex;
1211
1212 use super::*;
1213 use crate::{
1214 config::{
1215 EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
1216 },
1217 models::{
1218 network::NetworkConfigData,
1219 relayer::{
1220 RelayerEvmPolicy, RelayerNetworkPolicy, RelayerSolanaPolicy, RelayerStellarPolicy,
1221 },
1222 transaction::stellar::AssetSpec,
1223 EncodedSerializedTransaction, StellarFeePaymentStrategy,
1224 },
1225 };
1226
1227 lazy_static! {
1229 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
1230 }
1231
1232 #[test]
1233 fn test_signature_from_bytes() {
1234 let test_bytes: [u8; 65] = [
1235 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
1236 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
1238 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 27, ];
1241
1242 let signature = EvmTransactionDataSignature::from(&test_bytes);
1243
1244 assert_eq!(signature.r.len(), 64); assert_eq!(signature.s.len(), 64); assert_eq!(signature.v, 27);
1247 assert_eq!(signature.sig.len(), 130); }
1249
1250 #[test]
1251 fn test_transaction_metadata_nonce_too_high_retries_default() {
1252 let metadata = TransactionMetadata::default();
1253 assert_eq!(metadata.nonce_too_high_retries, 0);
1254 assert_eq!(metadata.consecutive_failures, 0);
1255 assert_eq!(metadata.total_failures, 0);
1256 assert_eq!(metadata.insufficient_fee_retries, 0);
1257 assert_eq!(metadata.try_again_later_retries, 0);
1258 }
1259
1260 #[test]
1261 fn test_transaction_metadata_backward_compatibility() {
1262 let old_json = r#"{
1264 "consecutive_failures": 1,
1265 "total_failures": 2,
1266 "insufficient_fee_retries": 3,
1267 "try_again_later_retries": 4
1268 }"#;
1269
1270 let metadata: TransactionMetadata = serde_json::from_str(old_json).unwrap();
1271 assert_eq!(metadata.consecutive_failures, 1);
1272 assert_eq!(metadata.total_failures, 2);
1273 assert_eq!(metadata.insufficient_fee_retries, 3);
1274 assert_eq!(metadata.try_again_later_retries, 4);
1275 assert_eq!(metadata.nonce_too_high_retries, 0);
1277 }
1278
1279 #[test]
1280 fn test_with_nonce_retries_reset_returns_none_when_zero() {
1281 let metadata = TransactionMetadata {
1282 consecutive_failures: 2,
1283 total_failures: 5,
1284 insufficient_fee_retries: 1,
1285 try_again_later_retries: 3,
1286 nonce_too_high_retries: 0,
1287 };
1288 assert!(metadata.with_nonce_retries_reset().is_none());
1289 }
1290
1291 #[test]
1292 fn test_with_nonce_retries_reset_returns_reset_metadata() {
1293 let metadata = TransactionMetadata {
1294 consecutive_failures: 2,
1295 total_failures: 5,
1296 insufficient_fee_retries: 1,
1297 try_again_later_retries: 3,
1298 nonce_too_high_retries: 3,
1299 };
1300 let result = metadata.with_nonce_retries_reset();
1301 assert!(result.is_some());
1302 let reset = result.unwrap();
1303 assert_eq!(reset.nonce_too_high_retries, 0);
1304 assert_eq!(reset.consecutive_failures, 2);
1305 assert_eq!(reset.total_failures, 5);
1306 assert_eq!(reset.insufficient_fee_retries, 1);
1307 assert_eq!(reset.try_again_later_retries, 3);
1308 }
1309
1310 #[test]
1311 fn test_stellar_transaction_data_reset_to_pre_prepare_state() {
1312 let stellar_data = StellarTransactionData {
1313 source_account: "GTEST".to_string(),
1314 fee: Some(100),
1315 sequence_number: Some(42),
1316 memo: Some(MemoSpec::Text {
1317 value: "test memo".to_string(),
1318 }),
1319 valid_until: Some("2024-12-31".to_string()),
1320 network_passphrase: "Test Network".to_string(),
1321 signatures: vec![], hash: Some("test-hash".to_string()),
1323 simulation_transaction_data: Some("simulation-data".to_string()),
1324 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1325 destination: "GDEST".to_string(),
1326 amount: 1000,
1327 asset: AssetSpec::Native,
1328 }]),
1329 signed_envelope_xdr: Some("signed-xdr".to_string()),
1330 transaction_result_xdr: None,
1331 };
1332
1333 let reset_data = stellar_data.clone().reset_to_pre_prepare_state();
1334
1335 assert_eq!(reset_data.source_account, stellar_data.source_account);
1337 assert_eq!(reset_data.memo, stellar_data.memo);
1338 assert_eq!(reset_data.valid_until, stellar_data.valid_until);
1339 assert_eq!(
1340 reset_data.network_passphrase,
1341 stellar_data.network_passphrase
1342 );
1343 assert!(matches!(
1344 reset_data.transaction_input,
1345 TransactionInput::Operations(_)
1346 ));
1347
1348 assert_eq!(reset_data.fee, None);
1350 assert_eq!(reset_data.sequence_number, None);
1351 assert!(reset_data.signatures.is_empty());
1352 assert_eq!(reset_data.hash, None);
1353 assert_eq!(reset_data.simulation_transaction_data, None);
1354 assert_eq!(reset_data.signed_envelope_xdr, None);
1355 }
1356
1357 #[test]
1358 fn test_transaction_repo_model_create_reset_update_request() {
1359 let stellar_data = StellarTransactionData {
1360 source_account: "GTEST".to_string(),
1361 fee: Some(100),
1362 sequence_number: Some(42),
1363 memo: None,
1364 valid_until: None,
1365 network_passphrase: "Test Network".to_string(),
1366 signatures: vec![],
1367 hash: Some("test-hash".to_string()),
1368 simulation_transaction_data: None,
1369 transaction_input: TransactionInput::Operations(vec![]),
1370 signed_envelope_xdr: Some("signed-xdr".to_string()),
1371 transaction_result_xdr: None,
1372 };
1373
1374 let tx = TransactionRepoModel {
1375 id: "tx-1".to_string(),
1376 relayer_id: "relayer-1".to_string(),
1377 status: TransactionStatus::Failed,
1378 status_reason: Some("Bad sequence".to_string()),
1379 created_at: "2024-01-01".to_string(),
1380 sent_at: Some("2024-01-02".to_string()),
1381 confirmed_at: Some("2024-01-03".to_string()),
1382 valid_until: None,
1383 network_data: NetworkTransactionData::Stellar(stellar_data),
1384 priced_at: None,
1385 hashes: vec!["hash1".to_string(), "hash2".to_string()],
1386 network_type: NetworkType::Stellar,
1387 noop_count: None,
1388 is_canceled: None,
1389 delete_at: None,
1390 metadata: None,
1391 };
1392
1393 let update_req = tx.create_reset_update_request().unwrap();
1394
1395 assert_eq!(update_req.status, Some(TransactionStatus::Pending));
1397 assert_eq!(update_req.status_reason, None);
1398 assert_eq!(update_req.sent_at, None);
1399 assert_eq!(update_req.confirmed_at, None);
1400 assert_eq!(update_req.hashes, Some(vec![]));
1401
1402 if let Some(NetworkTransactionData::Stellar(reset_data)) = update_req.network_data {
1404 assert_eq!(reset_data.fee, None);
1405 assert_eq!(reset_data.sequence_number, None);
1406 assert_eq!(reset_data.hash, None);
1407 assert_eq!(reset_data.signed_envelope_xdr, None);
1408 } else {
1409 panic!("Expected Stellar network data");
1410 }
1411 }
1412
1413 fn create_sample_evm_tx_data() -> EvmTransactionData {
1415 EvmTransactionData {
1416 gas_price: Some(20_000_000_000),
1417 gas_limit: Some(21000),
1418 nonce: Some(5),
1419 value: U256::from(1000000000000000000u128), data: Some("0x".to_string()),
1421 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1422 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1423 chain_id: 1,
1424 hash: None,
1425 signature: None,
1426 speed: None,
1427 max_fee_per_gas: None,
1428 max_priority_fee_per_gas: None,
1429 raw: None,
1430 }
1431 }
1432
1433 #[test]
1435 fn test_evm_tx_with_price_params() {
1436 let tx_data = create_sample_evm_tx_data();
1437 let price_params = PriceParams {
1438 gas_price: None,
1439 max_fee_per_gas: Some(30_000_000_000),
1440 max_priority_fee_per_gas: Some(2_000_000_000),
1441 is_min_bumped: None,
1442 extra_fee: None,
1443 total_cost: U256::ZERO,
1444 };
1445
1446 let updated_tx = tx_data.with_price_params(price_params);
1447
1448 assert_eq!(updated_tx.max_fee_per_gas, Some(30_000_000_000));
1449 assert_eq!(updated_tx.max_priority_fee_per_gas, Some(2_000_000_000));
1450 }
1451
1452 #[test]
1453 fn test_evm_tx_with_gas_estimate() {
1454 let tx_data = create_sample_evm_tx_data();
1455 let new_gas_limit = 30000;
1456
1457 let updated_tx = tx_data.with_gas_estimate(new_gas_limit);
1458
1459 assert_eq!(updated_tx.gas_limit, Some(new_gas_limit));
1460 }
1461
1462 #[test]
1463 fn test_evm_tx_with_nonce() {
1464 let tx_data = create_sample_evm_tx_data();
1465 let new_nonce = 10;
1466
1467 let updated_tx = tx_data.with_nonce(new_nonce);
1468
1469 assert_eq!(updated_tx.nonce, Some(new_nonce));
1470 }
1471
1472 #[test]
1473 fn test_evm_tx_with_signed_transaction_data() {
1474 let tx_data = create_sample_evm_tx_data();
1475
1476 let signature = EvmTransactionDataSignature {
1477 r: "r_value".to_string(),
1478 s: "s_value".to_string(),
1479 v: 27,
1480 sig: "signature_value".to_string(),
1481 };
1482
1483 let signed_tx_response = SignTransactionResponseEvm {
1484 signature,
1485 hash: "0xabcdef1234567890".to_string(),
1486 raw: vec![1, 2, 3, 4, 5],
1487 };
1488
1489 let updated_tx = tx_data.with_signed_transaction_data(signed_tx_response);
1490
1491 assert_eq!(updated_tx.signature.as_ref().unwrap().r, "r_value");
1492 assert_eq!(updated_tx.signature.as_ref().unwrap().s, "s_value");
1493 assert_eq!(updated_tx.signature.as_ref().unwrap().v, 27);
1494 assert_eq!(updated_tx.hash, Some("0xabcdef1234567890".to_string()));
1495 assert_eq!(updated_tx.raw, Some(vec![1, 2, 3, 4, 5]));
1496 }
1497
1498 #[test]
1499 fn test_evm_tx_to_address() {
1500 let tx_data = create_sample_evm_tx_data();
1502 let address_result = tx_data.to_address();
1503 assert!(address_result.is_ok());
1504 let address_option = address_result.unwrap();
1505 assert!(address_option.is_some());
1506 assert_eq!(
1507 address_option.unwrap().to_string().to_lowercase(),
1508 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_lowercase()
1509 );
1510
1511 let mut contract_creation_tx = create_sample_evm_tx_data();
1513 contract_creation_tx.to = None;
1514 let address_result = contract_creation_tx.to_address();
1515 assert!(address_result.is_ok());
1516 assert!(address_result.unwrap().is_none());
1517
1518 let mut empty_address_tx = create_sample_evm_tx_data();
1520 empty_address_tx.to = Some("".to_string());
1521 let address_result = empty_address_tx.to_address();
1522 assert!(address_result.is_ok());
1523 assert!(address_result.unwrap().is_none());
1524
1525 let mut invalid_address_tx = create_sample_evm_tx_data();
1527 invalid_address_tx.to = Some("0xINVALID".to_string());
1528 let address_result = invalid_address_tx.to_address();
1529 assert!(address_result.is_err());
1530 }
1531
1532 #[test]
1533 fn test_evm_tx_data_to_bytes() {
1534 let mut tx_data = create_sample_evm_tx_data();
1536 tx_data.data = Some("0x1234".to_string());
1537 let bytes_result = tx_data.data_to_bytes();
1538 assert!(bytes_result.is_ok());
1539 assert_eq!(bytes_result.unwrap().as_ref(), &[0x12, 0x34]);
1540
1541 tx_data.data = Some("".to_string());
1543 assert!(tx_data.data_to_bytes().is_ok());
1544
1545 tx_data.data = None;
1547 assert!(tx_data.data_to_bytes().is_ok());
1548
1549 tx_data.data = Some("0xZZ".to_string());
1551 assert!(tx_data.data_to_bytes().is_err());
1552 }
1553
1554 #[test]
1556 fn test_evm_tx_is_legacy() {
1557 let mut tx_data = create_sample_evm_tx_data();
1558
1559 assert!(tx_data.is_legacy());
1561
1562 tx_data.gas_price = None;
1564 assert!(!tx_data.is_legacy());
1565 }
1566
1567 #[test]
1568 fn test_evm_tx_is_eip1559() {
1569 let mut tx_data = create_sample_evm_tx_data();
1570
1571 assert!(!tx_data.is_eip1559());
1573
1574 tx_data.max_fee_per_gas = Some(30_000_000_000);
1576 tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1577 assert!(tx_data.is_eip1559());
1578
1579 tx_data.max_priority_fee_per_gas = None;
1581 assert!(!tx_data.is_eip1559());
1582 }
1583
1584 #[test]
1585 fn test_evm_tx_is_speed() {
1586 let mut tx_data = create_sample_evm_tx_data();
1587
1588 assert!(!tx_data.is_speed());
1590
1591 tx_data.speed = Some(Speed::Fast);
1593 assert!(tx_data.is_speed());
1594 }
1595
1596 #[test]
1598 fn test_network_tx_data_get_evm_transaction_data() {
1599 let evm_tx_data = create_sample_evm_tx_data();
1600 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1601
1602 let result = network_data.get_evm_transaction_data();
1604 assert!(result.is_ok());
1605 assert_eq!(result.unwrap().chain_id, evm_tx_data.chain_id);
1606
1607 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1609 transaction: Some("transaction_123".to_string()),
1610 ..Default::default()
1611 });
1612 assert!(solana_data.get_evm_transaction_data().is_err());
1613 }
1614
1615 #[test]
1616 fn test_network_tx_data_get_solana_transaction_data() {
1617 let solana_tx_data = SolanaTransactionData {
1618 transaction: Some("transaction_123".to_string()),
1619 ..Default::default()
1620 };
1621 let network_data = NetworkTransactionData::Solana(solana_tx_data.clone());
1622
1623 let result = network_data.get_solana_transaction_data();
1625 assert!(result.is_ok());
1626 assert_eq!(result.unwrap().transaction, solana_tx_data.transaction);
1627
1628 let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1630 assert!(evm_data.get_solana_transaction_data().is_err());
1631 }
1632
1633 #[test]
1634 fn test_network_tx_data_get_stellar_transaction_data() {
1635 let stellar_tx_data = StellarTransactionData {
1636 source_account: "account123".to_string(),
1637 fee: Some(100),
1638 sequence_number: Some(5),
1639 memo: Some(MemoSpec::Text {
1640 value: "Test memo".to_string(),
1641 }),
1642 valid_until: Some("2025-01-01T00:00:00Z".to_string()),
1643 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1644 signatures: Vec::new(),
1645 hash: Some("hash123".to_string()),
1646 simulation_transaction_data: None,
1647 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1648 destination: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ".to_string(),
1649 amount: 100000000, asset: AssetSpec::Native,
1651 }]),
1652 signed_envelope_xdr: None,
1653 transaction_result_xdr: None,
1654 };
1655 let network_data = NetworkTransactionData::Stellar(stellar_tx_data.clone());
1656
1657 let result = network_data.get_stellar_transaction_data();
1659 assert!(result.is_ok());
1660 assert_eq!(
1661 result.unwrap().source_account,
1662 stellar_tx_data.source_account
1663 );
1664
1665 let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1667 assert!(evm_data.get_stellar_transaction_data().is_err());
1668 }
1669
1670 #[test]
1672 fn test_try_from_network_tx_data_for_tx_legacy() {
1673 let evm_tx_data = create_sample_evm_tx_data();
1675 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1676
1677 let result = TxLegacy::try_from(network_data);
1679 assert!(result.is_ok());
1680 let tx_legacy = result.unwrap();
1681
1682 assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1684 assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1685 assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1686 assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1687 assert_eq!(tx_legacy.value, evm_tx_data.value);
1688
1689 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1691 transaction: Some("transaction_123".to_string()),
1692 ..Default::default()
1693 });
1694 assert!(TxLegacy::try_from(solana_data).is_err());
1695 }
1696
1697 #[test]
1698 fn test_try_from_evm_tx_data_for_tx_legacy() {
1699 let evm_tx_data = create_sample_evm_tx_data();
1701
1702 let result = TxLegacy::try_from(evm_tx_data.clone());
1704 assert!(result.is_ok());
1705 let tx_legacy = result.unwrap();
1706
1707 assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1709 assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1710 assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1711 assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1712 assert_eq!(tx_legacy.value, evm_tx_data.value);
1713 }
1714
1715 fn dummy_signature() -> DecoratedSignature {
1716 let hint = SignatureHint([0; 4]);
1717 let bytes: Vec<u8> = vec![0u8; 64];
1718 let bytes_m: BytesM<64> = bytes.try_into().expect("BytesM conversion");
1719 DecoratedSignature {
1720 hint,
1721 signature: Signature(bytes_m),
1722 }
1723 }
1724
1725 fn test_stellar_tx_data() -> StellarTransactionData {
1726 StellarTransactionData {
1727 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1728 fee: Some(100),
1729 sequence_number: Some(1),
1730 memo: None,
1731 valid_until: None,
1732 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1733 signatures: Vec::new(),
1734 hash: None,
1735 simulation_transaction_data: None,
1736 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1737 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1738 amount: 1000,
1739 asset: AssetSpec::Native,
1740 }]),
1741 signed_envelope_xdr: None,
1742 transaction_result_xdr: None,
1743 }
1744 }
1745
1746 #[test]
1747 fn test_with_sequence_number() {
1748 let tx = test_stellar_tx_data();
1749 let updated = tx.with_sequence_number(42);
1750 assert_eq!(updated.sequence_number, Some(42));
1751 }
1752
1753 #[test]
1754 fn test_get_envelope_for_simulation() {
1755 let tx = test_stellar_tx_data();
1756 let env = tx.get_envelope_for_simulation();
1757 assert!(env.is_ok());
1758 let env = env.unwrap();
1759 match env {
1761 soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1762 assert_eq!(tx_env.signatures.len(), 0);
1763 }
1764 _ => {
1765 panic!("Expected TransactionEnvelope::Tx variant");
1766 }
1767 }
1768 }
1769
1770 #[test]
1771 fn test_get_envelope_for_submission() {
1772 let mut tx = test_stellar_tx_data();
1773 tx.signatures.push(dummy_signature());
1774 let env = tx.get_envelope_for_submission();
1775 assert!(env.is_ok());
1776 let env = env.unwrap();
1777 match env {
1778 soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1779 assert_eq!(tx_env.signatures.len(), 1);
1780 }
1781 _ => {
1782 panic!("Expected TransactionEnvelope::Tx variant");
1783 }
1784 }
1785 }
1786
1787 #[test]
1788 fn test_attach_signature() {
1789 let tx = test_stellar_tx_data();
1790 let sig = dummy_signature();
1791 let updated = tx.attach_signature(sig.clone());
1792 assert_eq!(updated.signatures.len(), 1);
1793 assert_eq!(updated.signatures[0], sig);
1794 }
1795
1796 #[test]
1797 fn test_with_hash() {
1798 let tx = test_stellar_tx_data();
1799 let updated = tx.with_hash("hash123".to_string());
1800 assert_eq!(updated.hash, Some("hash123".to_string()));
1801 }
1802
1803 #[test]
1804 fn test_evm_tx_for_replacement() {
1805 let old_data = create_sample_evm_tx_data();
1806 let new_request = EvmTransactionRequest {
1807 to: Some("0xNewRecipient".to_string()),
1808 value: U256::from(2000000000000000000u64), data: Some("0xNewData".to_string()),
1810 gas_limit: Some(25000),
1811 gas_price: Some(30000000000), max_fee_per_gas: Some(40000000000), max_priority_fee_per_gas: Some(2000000000), speed: Some(Speed::Fast),
1815 valid_until: None,
1816 };
1817
1818 let result = EvmTransactionData::for_replacement(&old_data, &new_request);
1819
1820 assert_eq!(result.chain_id, old_data.chain_id);
1822 assert_eq!(result.from, old_data.from);
1823 assert_eq!(result.nonce, old_data.nonce);
1824
1825 assert_eq!(result.to, new_request.to);
1827 assert_eq!(result.value, new_request.value);
1828 assert_eq!(result.data, new_request.data);
1829 assert_eq!(result.gas_limit, new_request.gas_limit);
1830 assert_eq!(result.speed, new_request.speed);
1831
1832 assert_eq!(result.gas_price, None);
1834 assert_eq!(result.max_fee_per_gas, None);
1835 assert_eq!(result.max_priority_fee_per_gas, None);
1836
1837 assert_eq!(result.signature, None);
1839 assert_eq!(result.hash, None);
1840 assert_eq!(result.raw, None);
1841 }
1842
1843 #[test]
1844 fn test_transaction_repo_model_validate() {
1845 let transaction = TransactionRepoModel::default();
1846 let result = transaction.validate();
1847 assert!(result.is_ok());
1848 }
1849
1850 #[test]
1851 fn test_try_from_network_transaction_request_evm() {
1852 use crate::models::{NetworkRepoModel, NetworkType, RelayerRepoModel};
1853
1854 let evm_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1855 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1856 value: U256::from(1000000000000000000u128),
1857 data: Some("0x1234".to_string()),
1858 gas_limit: Some(21000),
1859 gas_price: Some(20000000000),
1860 max_fee_per_gas: None,
1861 max_priority_fee_per_gas: None,
1862 speed: Some(Speed::Fast),
1863 valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1864 });
1865
1866 let relayer_model = RelayerRepoModel {
1867 id: "relayer-id".to_string(),
1868 name: "Test Relayer".to_string(),
1869 network: "network-id".to_string(),
1870 paused: false,
1871 network_type: NetworkType::Evm,
1872 signer_id: "signer-id".to_string(),
1873 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
1874 address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1875 notification_id: None,
1876 system_disabled: false,
1877 custom_rpc_urls: None,
1878 ..Default::default()
1879 };
1880
1881 let network_model = NetworkRepoModel {
1882 id: "evm:ethereum".to_string(),
1883 name: "ethereum".to_string(),
1884 network_type: NetworkType::Evm,
1885 config: NetworkConfigData::Evm(EvmNetworkConfig {
1886 common: NetworkConfigCommon {
1887 network: "ethereum".to_string(),
1888 from: None,
1889 rpc_urls: Some(vec![crate::models::RpcConfig::new(
1890 "https://mainnet.infura.io".to_string(),
1891 )]),
1892 explorer_urls: Some(vec!["https://etherscan.io".to_string()]),
1893 average_blocktime_ms: Some(12000),
1894 is_testnet: Some(false),
1895 tags: Some(vec!["mainnet".to_string()]),
1896 },
1897 chain_id: Some(1),
1898 required_confirmations: Some(12),
1899 features: None,
1900 symbol: Some("ETH".to_string()),
1901 gas_price_cache: None,
1902 }),
1903 };
1904
1905 let result = TransactionRepoModel::try_from((&evm_request, &relayer_model, &network_model));
1906 assert!(result.is_ok());
1907 let transaction = result.unwrap();
1908
1909 assert_eq!(transaction.relayer_id, relayer_model.id);
1910 assert_eq!(transaction.status, TransactionStatus::Pending);
1911 assert_eq!(transaction.network_type, NetworkType::Evm);
1912 assert_eq!(
1913 transaction.valid_until,
1914 Some("2024-12-31T23:59:59Z".to_string())
1915 );
1916 assert!(transaction.is_canceled == Some(false));
1917
1918 if let NetworkTransactionData::Evm(evm_data) = transaction.network_data {
1919 assert_eq!(evm_data.from, relayer_model.address);
1920 assert_eq!(
1921 evm_data.to,
1922 Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string())
1923 );
1924 assert_eq!(evm_data.value, U256::from(1000000000000000000u128));
1925 assert_eq!(evm_data.chain_id, 1);
1926 assert_eq!(evm_data.gas_limit, Some(21000));
1927 assert_eq!(evm_data.gas_price, Some(20000000000));
1928 assert_eq!(evm_data.speed, Some(Speed::Fast));
1929 } else {
1930 panic!("Expected EVM transaction data");
1931 }
1932 }
1933
1934 #[test]
1935 fn test_try_from_network_transaction_request_solana() {
1936 use crate::models::{
1937 NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1938 };
1939
1940 let solana_request = NetworkTransactionRequest::Solana(
1941 crate::models::transaction::request::solana::SolanaTransactionRequest {
1942 transaction: Some(EncodedSerializedTransaction::new(
1943 "transaction_123".to_string(),
1944 )),
1945 instructions: None,
1946 valid_until: None,
1947 },
1948 );
1949
1950 let relayer_model = RelayerRepoModel {
1951 id: "relayer-id".to_string(),
1952 name: "Test Solana Relayer".to_string(),
1953 network: "network-id".to_string(),
1954 paused: false,
1955 network_type: NetworkType::Solana,
1956 signer_id: "signer-id".to_string(),
1957 policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
1958 address: "solana_address".to_string(),
1959 notification_id: None,
1960 system_disabled: false,
1961 custom_rpc_urls: None,
1962 ..Default::default()
1963 };
1964
1965 let network_model = NetworkRepoModel {
1966 id: "solana:mainnet".to_string(),
1967 name: "mainnet".to_string(),
1968 network_type: NetworkType::Solana,
1969 config: NetworkConfigData::Solana(SolanaNetworkConfig {
1970 common: NetworkConfigCommon {
1971 network: "mainnet".to_string(),
1972 from: None,
1973 rpc_urls: Some(vec![crate::models::RpcConfig::new(
1974 "https://api.mainnet-beta.solana.com".to_string(),
1975 )]),
1976 explorer_urls: Some(vec!["https://explorer.solana.com".to_string()]),
1977 average_blocktime_ms: Some(400),
1978 is_testnet: Some(false),
1979 tags: Some(vec!["mainnet".to_string()]),
1980 },
1981 }),
1982 };
1983
1984 let result =
1985 TransactionRepoModel::try_from((&solana_request, &relayer_model, &network_model));
1986 assert!(result.is_ok());
1987 let transaction = result.unwrap();
1988
1989 assert_eq!(transaction.relayer_id, relayer_model.id);
1990 assert_eq!(transaction.status, TransactionStatus::Pending);
1991 assert_eq!(transaction.network_type, NetworkType::Solana);
1992 assert_eq!(transaction.valid_until, None);
1993
1994 if let NetworkTransactionData::Solana(solana_data) = transaction.network_data {
1995 assert_eq!(solana_data.transaction, Some("transaction_123".to_string()));
1996 assert_eq!(solana_data.signature, None);
1997 } else {
1998 panic!("Expected Solana transaction data");
1999 }
2000 }
2001
2002 #[test]
2003 fn test_try_from_network_transaction_request_stellar() {
2004 use crate::models::transaction::request::stellar::StellarTransactionRequest;
2005 use crate::models::{
2006 NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
2007 };
2008
2009 let stellar_request = NetworkTransactionRequest::Stellar(StellarTransactionRequest {
2010 source_account: Some(
2011 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2012 ),
2013 network: "mainnet".to_string(),
2014 operations: Some(vec![OperationSpec::Payment {
2015 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2016 amount: 1000000,
2017 asset: AssetSpec::Native,
2018 }]),
2019 memo: Some(MemoSpec::Text {
2020 value: "Test memo".to_string(),
2021 }),
2022 valid_until: Some("2024-12-31T23:59:59Z".to_string()),
2023 transaction_xdr: None,
2024 fee_bump: None,
2025 max_fee: None,
2026 signed_auth_entry: None,
2027 });
2028
2029 let relayer_model = RelayerRepoModel {
2030 id: "relayer-id".to_string(),
2031 name: "Test Stellar Relayer".to_string(),
2032 network: "network-id".to_string(),
2033 paused: false,
2034 network_type: NetworkType::Stellar,
2035 signer_id: "signer-id".to_string(),
2036 policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
2037 address: "stellar_address".to_string(),
2038 notification_id: None,
2039 system_disabled: false,
2040 custom_rpc_urls: None,
2041 ..Default::default()
2042 };
2043
2044 let network_model = NetworkRepoModel {
2045 id: "stellar:mainnet".to_string(),
2046 name: "mainnet".to_string(),
2047 network_type: NetworkType::Stellar,
2048 config: NetworkConfigData::Stellar(StellarNetworkConfig {
2049 common: NetworkConfigCommon {
2050 network: "mainnet".to_string(),
2051 from: None,
2052 rpc_urls: Some(vec![crate::models::RpcConfig::new(
2053 "https://horizon.stellar.org".to_string(),
2054 )]),
2055 explorer_urls: Some(vec!["https://stellarchain.io".to_string()]),
2056 average_blocktime_ms: Some(5000),
2057 is_testnet: Some(false),
2058 tags: Some(vec!["mainnet".to_string()]),
2059 },
2060 passphrase: Some("Public Global Stellar Network ; September 2015".to_string()),
2061 horizon_url: Some("https://horizon.stellar.org".to_string()),
2062 }),
2063 };
2064
2065 let result =
2066 TransactionRepoModel::try_from((&stellar_request, &relayer_model, &network_model));
2067 assert!(result.is_ok());
2068 let transaction = result.unwrap();
2069
2070 assert_eq!(transaction.relayer_id, relayer_model.id);
2071 assert_eq!(transaction.status, TransactionStatus::Pending);
2072 assert_eq!(transaction.network_type, NetworkType::Stellar);
2073 assert_eq!(
2075 transaction.valid_until,
2076 Some("2024-12-31T23:59:59Z".to_string())
2077 );
2078
2079 if let NetworkTransactionData::Stellar(stellar_data) = transaction.network_data {
2080 assert_eq!(
2081 stellar_data.source_account,
2082 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2083 );
2084 if let TransactionInput::Operations(ops) = &stellar_data.transaction_input {
2086 assert_eq!(ops.len(), 1);
2087 if let OperationSpec::Payment {
2088 destination,
2089 amount,
2090 asset,
2091 } = &ops[0]
2092 {
2093 assert_eq!(
2094 destination,
2095 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2096 );
2097 assert_eq!(amount, &1000000);
2098 assert_eq!(asset, &AssetSpec::Native);
2099 } else {
2100 panic!("Expected Payment operation");
2101 }
2102 } else {
2103 panic!("Expected Operations transaction input");
2104 }
2105 assert_eq!(
2106 stellar_data.memo,
2107 Some(MemoSpec::Text {
2108 value: "Test memo".to_string()
2109 })
2110 );
2111 assert_eq!(
2112 stellar_data.valid_until,
2113 Some("2024-12-31T23:59:59Z".to_string())
2114 );
2115 assert_eq!(stellar_data.signatures.len(), 0);
2116 assert_eq!(stellar_data.hash, None);
2117 assert_eq!(stellar_data.fee, None);
2118 assert_eq!(stellar_data.sequence_number, None);
2119 } else {
2120 panic!("Expected Stellar transaction data");
2121 }
2122 }
2123
2124 #[test]
2125 fn test_try_from_network_transaction_data_for_tx_eip1559() {
2126 let mut evm_tx_data = create_sample_evm_tx_data();
2128 evm_tx_data.max_fee_per_gas = Some(30_000_000_000);
2129 evm_tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
2130 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
2131
2132 let result = TxEip1559::try_from(network_data);
2134 assert!(result.is_ok());
2135 let tx_eip1559 = result.unwrap();
2136
2137 assert_eq!(tx_eip1559.chain_id, evm_tx_data.chain_id);
2139 assert_eq!(tx_eip1559.nonce, evm_tx_data.nonce.unwrap());
2140 assert_eq!(tx_eip1559.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
2141 assert_eq!(
2142 tx_eip1559.max_fee_per_gas,
2143 evm_tx_data.max_fee_per_gas.unwrap()
2144 );
2145 assert_eq!(
2146 tx_eip1559.max_priority_fee_per_gas,
2147 evm_tx_data.max_priority_fee_per_gas.unwrap()
2148 );
2149 assert_eq!(tx_eip1559.value, evm_tx_data.value);
2150 assert!(tx_eip1559.access_list.0.is_empty());
2151
2152 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
2154 transaction: Some("transaction_123".to_string()),
2155 ..Default::default()
2156 });
2157 assert!(TxEip1559::try_from(solana_data).is_err());
2158 }
2159
2160 #[test]
2161 fn test_evm_transaction_data_defaults() {
2162 let default_data = EvmTransactionData::default();
2163
2164 assert_eq!(
2165 default_data.from,
2166 "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
2167 );
2168 assert_eq!(
2169 default_data.to,
2170 Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string())
2171 );
2172 assert_eq!(default_data.gas_price, Some(20000000000));
2173 assert_eq!(default_data.value, U256::from(1000000000000000000u128));
2174 assert_eq!(default_data.data, Some("0x".to_string()));
2175 assert_eq!(default_data.nonce, Some(1));
2176 assert_eq!(default_data.chain_id, 1);
2177 assert_eq!(default_data.gas_limit, Some(21000));
2178 assert_eq!(default_data.hash, None);
2179 assert_eq!(default_data.signature, None);
2180 assert_eq!(default_data.speed, None);
2181 assert_eq!(default_data.max_fee_per_gas, None);
2182 assert_eq!(default_data.max_priority_fee_per_gas, None);
2183 assert_eq!(default_data.raw, None);
2184 }
2185
2186 #[test]
2187 fn test_transaction_repo_model_defaults() {
2188 let default_model = TransactionRepoModel::default();
2189
2190 assert_eq!(default_model.id, "00000000-0000-0000-0000-000000000001");
2191 assert_eq!(
2192 default_model.relayer_id,
2193 "00000000-0000-0000-0000-000000000002"
2194 );
2195 assert_eq!(default_model.status, TransactionStatus::Pending);
2196 assert_eq!(default_model.created_at, "2023-01-01T00:00:00Z");
2197 assert_eq!(default_model.status_reason, None);
2198 assert_eq!(default_model.sent_at, None);
2199 assert_eq!(default_model.confirmed_at, None);
2200 assert_eq!(default_model.valid_until, None);
2201 assert_eq!(default_model.delete_at, None);
2202 assert_eq!(default_model.network_type, NetworkType::Evm);
2203 assert_eq!(default_model.priced_at, None);
2204 assert_eq!(default_model.hashes.len(), 0);
2205 assert_eq!(default_model.noop_count, None);
2206 assert_eq!(default_model.is_canceled, Some(false));
2207 }
2208
2209 #[test]
2210 fn test_evm_tx_for_replacement_with_speed_fallback() {
2211 let mut old_data = create_sample_evm_tx_data();
2212 old_data.speed = Some(Speed::SafeLow);
2213
2214 let new_request = EvmTransactionRequest {
2216 to: Some("0xNewRecipient".to_string()),
2217 value: U256::from(2000000000000000000u64),
2218 data: Some("0xNewData".to_string()),
2219 gas_limit: Some(25000),
2220 gas_price: None,
2221 max_fee_per_gas: None,
2222 max_priority_fee_per_gas: None,
2223 speed: None,
2224 valid_until: None,
2225 };
2226
2227 let result = EvmTransactionData::for_replacement(&old_data, &new_request);
2228 assert_eq!(result.speed, Some(Speed::SafeLow));
2229
2230 let mut old_data_no_speed = create_sample_evm_tx_data();
2232 old_data_no_speed.speed = None;
2233
2234 let result2 = EvmTransactionData::for_replacement(&old_data_no_speed, &new_request);
2235 assert_eq!(result2.speed, Some(DEFAULT_TRANSACTION_SPEED));
2236 }
2237
2238 #[test]
2239 fn test_transaction_status_serialization() {
2240 use serde_json;
2241
2242 assert_eq!(
2244 serde_json::to_string(&TransactionStatus::Pending).unwrap(),
2245 "\"pending\""
2246 );
2247 assert_eq!(
2248 serde_json::to_string(&TransactionStatus::Sent).unwrap(),
2249 "\"sent\""
2250 );
2251 assert_eq!(
2252 serde_json::to_string(&TransactionStatus::Mined).unwrap(),
2253 "\"mined\""
2254 );
2255 assert_eq!(
2256 serde_json::to_string(&TransactionStatus::Failed).unwrap(),
2257 "\"failed\""
2258 );
2259 assert_eq!(
2260 serde_json::to_string(&TransactionStatus::Confirmed).unwrap(),
2261 "\"confirmed\""
2262 );
2263 assert_eq!(
2264 serde_json::to_string(&TransactionStatus::Canceled).unwrap(),
2265 "\"canceled\""
2266 );
2267 assert_eq!(
2268 serde_json::to_string(&TransactionStatus::Submitted).unwrap(),
2269 "\"submitted\""
2270 );
2271 assert_eq!(
2272 serde_json::to_string(&TransactionStatus::Expired).unwrap(),
2273 "\"expired\""
2274 );
2275 }
2276
2277 #[test]
2278 fn test_evm_tx_contract_creation() {
2279 let mut tx_data = create_sample_evm_tx_data();
2281 tx_data.to = None;
2282
2283 let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
2284 assert_eq!(tx_legacy.to, TxKind::Create);
2285
2286 let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
2287 assert_eq!(tx_eip1559.to, TxKind::Create);
2288 }
2289
2290 #[test]
2291 fn test_evm_tx_default_values_in_conversion() {
2292 let mut tx_data = create_sample_evm_tx_data();
2294 tx_data.nonce = None;
2295 tx_data.gas_price = None;
2296 tx_data.max_fee_per_gas = None;
2297 tx_data.max_priority_fee_per_gas = None;
2298
2299 let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
2300 assert_eq!(tx_legacy.nonce, 0); assert_eq!(tx_legacy.gas_price, 0); let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
2304 assert_eq!(tx_eip1559.nonce, 0); assert_eq!(tx_eip1559.max_fee_per_gas, 0); assert_eq!(tx_eip1559.max_priority_fee_per_gas, 0); }
2308
2309 fn test_models() -> (NetworkRepoModel, RelayerRepoModel) {
2311 use crate::config::{NetworkConfigCommon, StellarNetworkConfig};
2312 use crate::constants::DEFAULT_STELLAR_MIN_BALANCE;
2313
2314 let network_config = NetworkConfigData::Stellar(StellarNetworkConfig {
2315 common: NetworkConfigCommon {
2316 network: "testnet".to_string(),
2317 from: None,
2318 rpc_urls: Some(vec![crate::models::RpcConfig::new(
2319 "https://test.stellar.org".to_string(),
2320 )]),
2321 explorer_urls: None,
2322 average_blocktime_ms: Some(5000), is_testnet: Some(true),
2324 tags: None,
2325 },
2326 passphrase: Some("Test SDF Network ; September 2015".to_string()),
2327 horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
2328 });
2329
2330 let network_model = NetworkRepoModel {
2331 id: "stellar:testnet".to_string(),
2332 name: "testnet".to_string(),
2333 network_type: NetworkType::Stellar,
2334 config: network_config,
2335 };
2336
2337 let relayer_model = RelayerRepoModel {
2338 id: "test-relayer".to_string(),
2339 name: "Test Relayer".to_string(),
2340 network: "stellar:testnet".to_string(),
2341 paused: false,
2342 network_type: NetworkType::Stellar,
2343 signer_id: "test-signer".to_string(),
2344 policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
2345 max_fee: None,
2346 timeout_seconds: None,
2347 min_balance: Some(DEFAULT_STELLAR_MIN_BALANCE),
2348 concurrent_transactions: None,
2349 allowed_tokens: None,
2350 fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
2351 slippage_percentage: None,
2352 fee_margin_percentage: None,
2353 swap_config: None,
2354 }),
2355 address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2356 notification_id: None,
2357 system_disabled: false,
2358 custom_rpc_urls: None,
2359 ..Default::default()
2360 };
2361
2362 (network_model, relayer_model)
2363 }
2364
2365 #[test]
2366 fn test_stellar_transaction_data_serialization_roundtrip() {
2367 use crate::models::transaction::stellar::asset::AssetSpec;
2368 use crate::models::transaction::stellar::operation::OperationSpec;
2369 use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
2370
2371 let hint = SignatureHint([1, 2, 3, 4]);
2373 let sig_bytes: Vec<u8> = vec![5u8; 64];
2374 let sig_bytes_m: BytesM<64> = sig_bytes.try_into().unwrap();
2375 let dummy_signature = DecoratedSignature {
2376 hint,
2377 signature: Signature(sig_bytes_m),
2378 };
2379
2380 let original_data = StellarTransactionData {
2382 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2383 fee: Some(100),
2384 sequence_number: Some(12345),
2385 memo: None,
2386 valid_until: None,
2387 network_passphrase: "Test SDF Network ; September 2015".to_string(),
2388 signatures: vec![dummy_signature.clone()],
2389 hash: Some("test-hash".to_string()),
2390 simulation_transaction_data: Some("simulation-data".to_string()),
2391 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
2392 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2393 amount: 1000,
2394 asset: AssetSpec::Native,
2395 }]),
2396 signed_envelope_xdr: Some("signed-xdr-data".to_string()),
2397 transaction_result_xdr: None,
2398 };
2399
2400 let json = serde_json::to_string(&original_data).expect("Failed to serialize");
2402
2403 let deserialized_data: StellarTransactionData =
2405 serde_json::from_str(&json).expect("Failed to deserialize");
2406
2407 match (
2409 &original_data.transaction_input,
2410 &deserialized_data.transaction_input,
2411 ) {
2412 (TransactionInput::Operations(orig_ops), TransactionInput::Operations(deser_ops)) => {
2413 assert_eq!(orig_ops.len(), deser_ops.len());
2414 assert_eq!(orig_ops, deser_ops);
2415 }
2416 _ => panic!("Transaction input type mismatch"),
2417 }
2418
2419 assert_eq!(
2421 original_data.signatures.len(),
2422 deserialized_data.signatures.len()
2423 );
2424 assert_eq!(original_data.signatures, deserialized_data.signatures);
2425
2426 assert_eq!(
2428 original_data.source_account,
2429 deserialized_data.source_account
2430 );
2431 assert_eq!(original_data.fee, deserialized_data.fee);
2432 assert_eq!(
2433 original_data.sequence_number,
2434 deserialized_data.sequence_number
2435 );
2436 assert_eq!(
2437 original_data.network_passphrase,
2438 deserialized_data.network_passphrase
2439 );
2440 assert_eq!(original_data.hash, deserialized_data.hash);
2441 assert_eq!(
2442 original_data.simulation_transaction_data,
2443 deserialized_data.simulation_transaction_data
2444 );
2445 assert_eq!(
2446 original_data.signed_envelope_xdr,
2447 deserialized_data.signed_envelope_xdr
2448 );
2449 }
2450
2451 #[test]
2452 fn test_stellar_xdr_transaction_input_conversion() {
2453 let (network_model, relayer_model) = test_models();
2454
2455 let stellar_request = StellarTransactionRequest {
2457 source_account: Some(
2458 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2459 ),
2460 network: "testnet".to_string(),
2461 operations: Some(vec![OperationSpec::Payment {
2462 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2463 amount: 1000000,
2464 asset: AssetSpec::Native,
2465 }]),
2466 memo: None,
2467 valid_until: None,
2468 transaction_xdr: None,
2469 fee_bump: None,
2470 max_fee: None,
2471 signed_auth_entry: None,
2472 };
2473
2474 let request = NetworkTransactionRequest::Stellar(stellar_request);
2475 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2476 assert!(result.is_ok());
2477
2478 let tx_model = result.unwrap();
2479 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2480 assert!(matches!(
2481 stellar_data.transaction_input,
2482 TransactionInput::Operations(_)
2483 ));
2484 } else {
2485 panic!("Expected Stellar transaction data");
2486 }
2487
2488 let unsigned_xdr = "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAGQAAHAkAAAADgAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=";
2491 let stellar_request = StellarTransactionRequest {
2492 source_account: None,
2493 network: "testnet".to_string(),
2494 operations: Some(vec![]),
2495 memo: None,
2496 valid_until: None,
2497 transaction_xdr: Some(unsigned_xdr.to_string()),
2498 fee_bump: None,
2499 max_fee: None,
2500 signed_auth_entry: None,
2501 };
2502
2503 let request = NetworkTransactionRequest::Stellar(stellar_request);
2504 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2505 assert!(result.is_ok());
2506
2507 let tx_model = result.unwrap();
2508 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2509 assert!(matches!(
2510 stellar_data.transaction_input,
2511 TransactionInput::UnsignedXdr(_)
2512 ));
2513 } else {
2514 panic!("Expected Stellar transaction data");
2515 }
2516
2517 let signed_xdr = {
2520 use soroban_rs::xdr::{Limits, TransactionEnvelope, TransactionV1Envelope, WriteXdr};
2521 use stellar_strkey::ed25519::PublicKey;
2522
2523 let source_pk =
2525 PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
2526 .unwrap();
2527 let dest_pk =
2528 PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
2529 .unwrap();
2530
2531 let payment_op = soroban_rs::xdr::PaymentOp {
2532 destination: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2533 dest_pk.0,
2534 )),
2535 asset: soroban_rs::xdr::Asset::Native,
2536 amount: 1000000,
2537 };
2538
2539 let operation = soroban_rs::xdr::Operation {
2540 source_account: None,
2541 body: soroban_rs::xdr::OperationBody::Payment(payment_op),
2542 };
2543
2544 let operations: soroban_rs::xdr::VecM<soroban_rs::xdr::Operation, 100> =
2545 vec![operation].try_into().unwrap();
2546
2547 let tx = soroban_rs::xdr::Transaction {
2548 source_account: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2549 source_pk.0,
2550 )),
2551 fee: 100,
2552 seq_num: soroban_rs::xdr::SequenceNumber(1),
2553 cond: soroban_rs::xdr::Preconditions::None,
2554 memo: soroban_rs::xdr::Memo::None,
2555 operations,
2556 ext: soroban_rs::xdr::TransactionExt::V0,
2557 };
2558
2559 let hint = soroban_rs::xdr::SignatureHint([0; 4]);
2561 let sig_bytes: Vec<u8> = vec![0u8; 64];
2562 let sig_bytes_m: soroban_rs::xdr::BytesM<64> = sig_bytes.try_into().unwrap();
2563 let sig = soroban_rs::xdr::DecoratedSignature {
2564 hint,
2565 signature: soroban_rs::xdr::Signature(sig_bytes_m),
2566 };
2567
2568 let envelope = TransactionV1Envelope {
2569 tx,
2570 signatures: vec![sig].try_into().unwrap(),
2571 };
2572
2573 let tx_envelope = TransactionEnvelope::Tx(envelope);
2574 tx_envelope.to_xdr_base64(Limits::none()).unwrap()
2575 };
2576 let stellar_request = StellarTransactionRequest {
2577 source_account: None,
2578 network: "testnet".to_string(),
2579 operations: Some(vec![]),
2580 memo: None,
2581 valid_until: None,
2582 transaction_xdr: Some(signed_xdr.to_string()),
2583 fee_bump: Some(true),
2584 max_fee: Some(20000000),
2585 signed_auth_entry: None,
2586 };
2587
2588 let request = NetworkTransactionRequest::Stellar(stellar_request);
2589 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2590 assert!(result.is_ok());
2591
2592 let tx_model = result.unwrap();
2593 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2594 match &stellar_data.transaction_input {
2595 TransactionInput::SignedXdr { xdr, max_fee } => {
2596 assert_eq!(xdr, &signed_xdr);
2597 assert_eq!(*max_fee, 20000000);
2598 }
2599 _ => panic!("Expected SignedXdr transaction input"),
2600 }
2601 } else {
2602 panic!("Expected Stellar transaction data");
2603 }
2604
2605 let stellar_request = StellarTransactionRequest {
2607 source_account: None,
2608 network: "testnet".to_string(),
2609 operations: Some(vec![]),
2610 memo: None,
2611 valid_until: None,
2612 transaction_xdr: Some(signed_xdr.clone()),
2613 fee_bump: None,
2614 max_fee: None,
2615 signed_auth_entry: None,
2616 };
2617
2618 let request = NetworkTransactionRequest::Stellar(stellar_request);
2619 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2620 assert!(result.is_err());
2621 assert!(result
2622 .unwrap_err()
2623 .to_string()
2624 .contains("Expected unsigned XDR but received signed XDR"));
2625
2626 let stellar_request = StellarTransactionRequest {
2628 source_account: Some(
2629 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2630 ),
2631 network: "testnet".to_string(),
2632 operations: Some(vec![OperationSpec::Payment {
2633 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2634 amount: 1000000,
2635 asset: AssetSpec::Native,
2636 }]),
2637 memo: None,
2638 valid_until: None,
2639 transaction_xdr: None,
2640 fee_bump: Some(true),
2641 max_fee: None,
2642 signed_auth_entry: None,
2643 };
2644
2645 let request = NetworkTransactionRequest::Stellar(stellar_request);
2646 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2647 assert!(result.is_err());
2648 assert!(result
2649 .unwrap_err()
2650 .to_string()
2651 .contains("Cannot request fee_bump with operations mode"));
2652 }
2653
2654 #[test]
2655 fn test_invoke_host_function_must_be_exclusive() {
2656 let (network_model, relayer_model) = test_models();
2657
2658 let stellar_request = StellarTransactionRequest {
2660 source_account: Some(
2661 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2662 ),
2663 network: "testnet".to_string(),
2664 operations: Some(vec![OperationSpec::InvokeContract {
2665 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2666 .to_string(),
2667 function_name: "transfer".to_string(),
2668 args: vec![],
2669 auth: None,
2670 }]),
2671 memo: None,
2672 valid_until: None,
2673 transaction_xdr: None,
2674 fee_bump: None,
2675 max_fee: None,
2676 signed_auth_entry: None,
2677 };
2678
2679 let request = NetworkTransactionRequest::Stellar(stellar_request);
2680 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2681 assert!(result.is_ok(), "Single InvokeHostFunction should succeed");
2682
2683 let stellar_request = StellarTransactionRequest {
2685 source_account: Some(
2686 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2687 ),
2688 network: "testnet".to_string(),
2689 operations: Some(vec![
2690 OperationSpec::Payment {
2691 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2692 .to_string(),
2693 amount: 1000,
2694 asset: AssetSpec::Native,
2695 },
2696 OperationSpec::InvokeContract {
2697 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2698 .to_string(),
2699 function_name: "transfer".to_string(),
2700 args: vec![],
2701 auth: None,
2702 },
2703 ]),
2704 memo: None,
2705 valid_until: None,
2706 transaction_xdr: None,
2707 fee_bump: None,
2708 max_fee: None,
2709 signed_auth_entry: None,
2710 };
2711
2712 let request = NetworkTransactionRequest::Stellar(stellar_request);
2713 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2714
2715 match result {
2716 Ok(_) => panic!("Expected Soroban operation mixed with Payment to fail"),
2717 Err(err) => {
2718 let err_str = err.to_string();
2719 assert!(
2720 err_str.contains("Soroban operations must be exclusive"),
2721 "Expected error about Soroban operation exclusivity, got: {err_str}"
2722 );
2723 }
2724 }
2725
2726 let stellar_request = StellarTransactionRequest {
2728 source_account: Some(
2729 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2730 ),
2731 network: "testnet".to_string(),
2732 operations: Some(vec![
2733 OperationSpec::InvokeContract {
2734 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2735 .to_string(),
2736 function_name: "transfer".to_string(),
2737 args: vec![],
2738 auth: None,
2739 },
2740 OperationSpec::InvokeContract {
2741 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2742 .to_string(),
2743 function_name: "approve".to_string(),
2744 args: vec![],
2745 auth: None,
2746 },
2747 ]),
2748 memo: None,
2749 valid_until: None,
2750 transaction_xdr: None,
2751 fee_bump: None,
2752 max_fee: None,
2753 signed_auth_entry: None,
2754 };
2755
2756 let request = NetworkTransactionRequest::Stellar(stellar_request);
2757 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2758
2759 match result {
2760 Ok(_) => panic!("Expected multiple Soroban operations to fail"),
2761 Err(err) => {
2762 let err_str = err.to_string();
2763 assert!(
2764 err_str.contains("Transaction can contain at most one Soroban operation"),
2765 "Expected error about multiple Soroban operations, got: {err_str}"
2766 );
2767 }
2768 }
2769
2770 let stellar_request = StellarTransactionRequest {
2772 source_account: Some(
2773 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2774 ),
2775 network: "testnet".to_string(),
2776 operations: Some(vec![
2777 OperationSpec::Payment {
2778 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2779 .to_string(),
2780 amount: 1000,
2781 asset: AssetSpec::Native,
2782 },
2783 OperationSpec::Payment {
2784 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
2785 .to_string(),
2786 amount: 2000,
2787 asset: AssetSpec::Native,
2788 },
2789 ]),
2790 memo: None,
2791 valid_until: None,
2792 transaction_xdr: None,
2793 fee_bump: None,
2794 max_fee: None,
2795 signed_auth_entry: None,
2796 };
2797
2798 let request = NetworkTransactionRequest::Stellar(stellar_request);
2799 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2800 assert!(result.is_ok(), "Multiple Payment operations should succeed");
2801
2802 let stellar_request = StellarTransactionRequest {
2804 source_account: Some(
2805 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2806 ),
2807 network: "testnet".to_string(),
2808 operations: Some(vec![OperationSpec::InvokeContract {
2809 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2810 .to_string(),
2811 function_name: "transfer".to_string(),
2812 args: vec![],
2813 auth: None,
2814 }]),
2815 memo: Some(MemoSpec::Text {
2816 value: "This should fail".to_string(),
2817 }),
2818 valid_until: None,
2819 transaction_xdr: None,
2820 fee_bump: None,
2821 max_fee: None,
2822 signed_auth_entry: None,
2823 };
2824
2825 let request = NetworkTransactionRequest::Stellar(stellar_request);
2826 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2827
2828 match result {
2829 Ok(_) => panic!("Expected InvokeHostFunction with non-None memo to fail"),
2830 Err(err) => {
2831 let err_str = err.to_string();
2832 assert!(
2833 err_str.contains("Soroban operations cannot have a memo"),
2834 "Expected error about memo restriction, got: {err_str}"
2835 );
2836 }
2837 }
2838
2839 let stellar_request = StellarTransactionRequest {
2841 source_account: Some(
2842 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2843 ),
2844 network: "testnet".to_string(),
2845 operations: Some(vec![OperationSpec::InvokeContract {
2846 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2847 .to_string(),
2848 function_name: "transfer".to_string(),
2849 args: vec![],
2850 auth: None,
2851 }]),
2852 memo: Some(MemoSpec::None),
2853 valid_until: None,
2854 transaction_xdr: None,
2855 fee_bump: None,
2856 max_fee: None,
2857 signed_auth_entry: None,
2858 };
2859
2860 let request = NetworkTransactionRequest::Stellar(stellar_request);
2861 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2862 assert!(
2863 result.is_ok(),
2864 "InvokeHostFunction with MemoSpec::None should succeed"
2865 );
2866
2867 let stellar_request = StellarTransactionRequest {
2869 source_account: Some(
2870 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2871 ),
2872 network: "testnet".to_string(),
2873 operations: Some(vec![OperationSpec::InvokeContract {
2874 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2875 .to_string(),
2876 function_name: "transfer".to_string(),
2877 args: vec![],
2878 auth: None,
2879 }]),
2880 memo: None,
2881 valid_until: None,
2882 transaction_xdr: None,
2883 fee_bump: None,
2884 max_fee: None,
2885 signed_auth_entry: None,
2886 };
2887
2888 let request = NetworkTransactionRequest::Stellar(stellar_request);
2889 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2890 assert!(
2891 result.is_ok(),
2892 "InvokeHostFunction with no memo should succeed"
2893 );
2894
2895 let stellar_request = StellarTransactionRequest {
2897 source_account: Some(
2898 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2899 ),
2900 network: "testnet".to_string(),
2901 operations: Some(vec![OperationSpec::Payment {
2902 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2903 amount: 1000,
2904 asset: AssetSpec::Native,
2905 }]),
2906 memo: Some(MemoSpec::Text {
2907 value: "Payment memo is allowed".to_string(),
2908 }),
2909 valid_until: None,
2910 transaction_xdr: None,
2911 fee_bump: None,
2912 max_fee: None,
2913 signed_auth_entry: None,
2914 };
2915
2916 let request = NetworkTransactionRequest::Stellar(stellar_request);
2917 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2918 assert!(result.is_ok(), "Payment operation with memo should succeed");
2919 }
2920
2921 #[test]
2922 fn test_update_delete_at_if_final_status_does_not_update_when_delete_at_already_set() {
2923 let _lock = match ENV_MUTEX.lock() {
2924 Ok(guard) => guard,
2925 Err(poisoned) => poisoned.into_inner(),
2926 };
2927
2928 use std::env;
2929
2930 env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2932
2933 let mut transaction = create_test_transaction();
2934 transaction.delete_at = Some("2024-01-01T00:00:00Z".to_string());
2935 transaction.status = TransactionStatus::Confirmed; let original_delete_at = transaction.delete_at.clone();
2938
2939 transaction.update_delete_at_if_final_status();
2940
2941 assert_eq!(transaction.delete_at, original_delete_at);
2943
2944 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2946 }
2947
2948 #[test]
2949 fn test_update_delete_at_if_final_status_does_not_update_when_status_not_final() {
2950 let _lock = match ENV_MUTEX.lock() {
2951 Ok(guard) => guard,
2952 Err(poisoned) => poisoned.into_inner(),
2953 };
2954
2955 use std::env;
2956
2957 env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2959
2960 let mut transaction = create_test_transaction();
2961 transaction.delete_at = None;
2962 transaction.status = TransactionStatus::Pending; transaction.update_delete_at_if_final_status();
2965
2966 assert!(transaction.delete_at.is_none());
2968
2969 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2971 }
2972
2973 #[test]
2974 fn test_update_delete_at_if_final_status_sets_delete_at_for_final_statuses() {
2975 let _lock = match ENV_MUTEX.lock() {
2976 Ok(guard) => guard,
2977 Err(poisoned) => poisoned.into_inner(),
2978 };
2979
2980 use crate::config::ServerConfig;
2981 use chrono::{DateTime, Duration, Utc};
2982 use std::env;
2983
2984 env::set_var("TRANSACTION_EXPIRATION_HOURS", "3"); let actual_hours = ServerConfig::get_transaction_expiration_hours();
2989 assert_eq!(
2990 actual_hours, 3.0,
2991 "Environment variable should be set to 3 hours"
2992 );
2993
2994 let final_statuses = vec![
2995 TransactionStatus::Canceled,
2996 TransactionStatus::Confirmed,
2997 TransactionStatus::Failed,
2998 TransactionStatus::Expired,
2999 ];
3000
3001 for status in final_statuses {
3002 let mut transaction = create_test_transaction();
3003 transaction.delete_at = None;
3004 transaction.status = status.clone();
3005
3006 let before_update = Utc::now();
3007 transaction.update_delete_at_if_final_status();
3008
3009 assert!(
3011 transaction.delete_at.is_some(),
3012 "delete_at should be set for status: {status:?}"
3013 );
3014
3015 let delete_at_str = transaction.delete_at.unwrap();
3017 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
3018 .expect("delete_at should be valid RFC3339")
3019 .with_timezone(&Utc);
3020
3021 let duration_from_before = delete_at.signed_duration_since(before_update);
3023 let expected_duration = Duration::hours(3);
3024 let tolerance = Duration::minutes(5); let actual_hours_at_runtime = ServerConfig::get_transaction_expiration_hours();
3028
3029 assert!(
3030 duration_from_before >= expected_duration - tolerance &&
3031 duration_from_before <= expected_duration + tolerance,
3032 "delete_at should be approximately 3 hours from now for status: {status:?}. Duration from start: {duration_from_before:?}, Expected: {expected_duration:?}, Config hours at runtime: {actual_hours_at_runtime}"
3033 );
3034 }
3035
3036 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
3038 }
3039
3040 #[test]
3041 fn test_update_delete_at_if_final_status_uses_default_expiration_hours() {
3042 let _lock = match ENV_MUTEX.lock() {
3043 Ok(guard) => guard,
3044 Err(poisoned) => poisoned.into_inner(),
3045 };
3046
3047 use chrono::{DateTime, Duration, Utc};
3048 use std::env;
3049
3050 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
3052
3053 let mut transaction = create_test_transaction();
3054 transaction.delete_at = None;
3055 transaction.status = TransactionStatus::Confirmed;
3056
3057 let before_update = Utc::now();
3058 transaction.update_delete_at_if_final_status();
3059
3060 assert!(transaction.delete_at.is_some());
3062
3063 let delete_at_str = transaction.delete_at.unwrap();
3064 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
3065 .expect("delete_at should be valid RFC3339")
3066 .with_timezone(&Utc);
3067
3068 let duration_from_before = delete_at.signed_duration_since(before_update);
3070 let expected_duration = Duration::hours(4);
3071 let tolerance = Duration::minutes(5); assert!(
3074 duration_from_before >= expected_duration - tolerance &&
3075 duration_from_before <= expected_duration + tolerance,
3076 "delete_at should be approximately 4 hours from now (default). Duration from start: {duration_from_before:?}, Expected: {expected_duration:?}"
3077 );
3078 }
3079
3080 #[test]
3081 fn test_update_delete_at_if_final_status_with_custom_expiration_hours() {
3082 let _lock = match ENV_MUTEX.lock() {
3083 Ok(guard) => guard,
3084 Err(poisoned) => poisoned.into_inner(),
3085 };
3086
3087 use chrono::{DateTime, Duration, Utc};
3088 use std::env;
3089
3090 let test_cases = vec![1, 2, 6, 12]; for expiration_hours in test_cases {
3094 env::set_var("TRANSACTION_EXPIRATION_HOURS", expiration_hours.to_string());
3095
3096 let mut transaction = create_test_transaction();
3097 transaction.delete_at = None;
3098 transaction.status = TransactionStatus::Failed;
3099
3100 let before_update = Utc::now();
3101 transaction.update_delete_at_if_final_status();
3102
3103 assert!(
3104 transaction.delete_at.is_some(),
3105 "delete_at should be set for {expiration_hours} hours"
3106 );
3107
3108 let delete_at_str = transaction.delete_at.unwrap();
3109 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
3110 .expect("delete_at should be valid RFC3339")
3111 .with_timezone(&Utc);
3112
3113 let duration_from_before = delete_at.signed_duration_since(before_update);
3114 let expected_duration = Duration::hours(expiration_hours as i64);
3115 let tolerance = Duration::minutes(5); assert!(
3118 duration_from_before >= expected_duration - tolerance &&
3119 duration_from_before <= expected_duration + tolerance,
3120 "delete_at should be approximately {expiration_hours} hours from now. Duration from start: {duration_from_before:?}, Expected: {expected_duration:?}"
3121 );
3122 }
3123
3124 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
3126 }
3127
3128 #[test]
3129 fn test_calculate_delete_at_with_various_hours() {
3130 use chrono::{DateTime, Utc};
3131
3132 let test_cases = vec![0, 1, 6, 12, 24, 48];
3133
3134 for hours in test_cases {
3135 let before_calc = Utc::now();
3136 let result = TransactionRepoModel::calculate_delete_at(hours as f64);
3137 let after_calc = Utc::now();
3138
3139 assert!(
3140 result.is_some(),
3141 "calculate_delete_at should return Some for {hours} hours"
3142 );
3143
3144 let delete_at_str = result.unwrap();
3145 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
3146 .expect("Result should be valid RFC3339")
3147 .with_timezone(&Utc);
3148
3149 let expected_min =
3150 before_calc + chrono::Duration::hours(hours as i64) - chrono::Duration::seconds(1);
3151 let expected_max =
3152 after_calc + chrono::Duration::hours(hours as i64) + chrono::Duration::seconds(1);
3153
3154 assert!(
3155 delete_at >= expected_min && delete_at <= expected_max,
3156 "Calculated delete_at should be approximately {hours} hours from now. Got: {delete_at}, Expected between: {expected_min} and {expected_max}"
3157 );
3158 }
3159 }
3160
3161 #[test]
3162 fn test_update_delete_at_if_final_status_idempotent() {
3163 let _lock = match ENV_MUTEX.lock() {
3164 Ok(guard) => guard,
3165 Err(poisoned) => poisoned.into_inner(),
3166 };
3167
3168 use std::env;
3169
3170 env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
3171
3172 let mut transaction = create_test_transaction();
3173 transaction.delete_at = None;
3174 transaction.status = TransactionStatus::Confirmed;
3175
3176 transaction.update_delete_at_if_final_status();
3178 let first_delete_at = transaction.delete_at.clone();
3179 assert!(first_delete_at.is_some());
3180
3181 transaction.update_delete_at_if_final_status();
3183 assert_eq!(transaction.delete_at, first_delete_at);
3184
3185 transaction.update_delete_at_if_final_status();
3187 assert_eq!(transaction.delete_at, first_delete_at);
3188
3189 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
3191 }
3192
3193 fn create_test_transaction() -> TransactionRepoModel {
3195 TransactionRepoModel {
3196 id: "test-transaction-id".to_string(),
3197 relayer_id: "test-relayer-id".to_string(),
3198 status: TransactionStatus::Pending,
3199 status_reason: None,
3200 created_at: "2024-01-01T00:00:00Z".to_string(),
3201 sent_at: None,
3202 confirmed_at: None,
3203 valid_until: None,
3204 delete_at: None,
3205 network_data: NetworkTransactionData::Evm(EvmTransactionData {
3206 gas_price: None,
3207 gas_limit: Some(21000),
3208 nonce: Some(0),
3209 value: U256::from(0),
3210 data: None,
3211 from: "0x1234567890123456789012345678901234567890".to_string(),
3212 to: Some("0x0987654321098765432109876543210987654321".to_string()),
3213 chain_id: 1,
3214 hash: None,
3215 signature: None,
3216 speed: None,
3217 max_fee_per_gas: None,
3218 max_priority_fee_per_gas: None,
3219 raw: None,
3220 }),
3221 priced_at: None,
3222 hashes: vec![],
3223 network_type: NetworkType::Evm,
3224 noop_count: None,
3225 is_canceled: None,
3226 metadata: None,
3227 }
3228 }
3229
3230 #[test]
3231 fn test_apply_partial_update() {
3232 let mut transaction = create_test_transaction();
3234
3235 let update = TransactionUpdateRequest {
3237 status: Some(TransactionStatus::Confirmed),
3238 status_reason: Some("Transaction confirmed".to_string()),
3239 sent_at: Some("2023-01-01T12:00:00Z".to_string()),
3240 confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
3241 hashes: Some(vec!["0x123".to_string(), "0x456".to_string()]),
3242 is_canceled: Some(false),
3243 ..Default::default()
3244 };
3245
3246 transaction.apply_partial_update(update);
3248
3249 assert_eq!(transaction.status, TransactionStatus::Confirmed);
3251 assert_eq!(
3252 transaction.status_reason,
3253 Some("Transaction confirmed".to_string())
3254 );
3255 assert_eq!(
3256 transaction.sent_at,
3257 Some("2023-01-01T12:00:00Z".to_string())
3258 );
3259 assert_eq!(
3260 transaction.confirmed_at,
3261 Some("2023-01-01T12:05:00Z".to_string())
3262 );
3263 assert_eq!(
3264 transaction.hashes,
3265 vec!["0x123".to_string(), "0x456".to_string()]
3266 );
3267 assert_eq!(transaction.is_canceled, Some(false));
3268
3269 assert!(transaction.delete_at.is_some());
3271 }
3272
3273 #[test]
3274 fn test_apply_partial_update_preserves_unchanged_fields() {
3275 let mut transaction = TransactionRepoModel {
3277 id: "test-tx".to_string(),
3278 relayer_id: "test-relayer".to_string(),
3279 status: TransactionStatus::Pending,
3280 status_reason: Some("Initial reason".to_string()),
3281 created_at: Utc::now().to_rfc3339(),
3282 sent_at: Some("2023-01-01T10:00:00Z".to_string()),
3283 confirmed_at: None,
3284 valid_until: None,
3285 delete_at: None,
3286 network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
3287 priced_at: None,
3288 hashes: vec!["0xoriginal".to_string()],
3289 network_type: NetworkType::Evm,
3290 noop_count: Some(5),
3291 is_canceled: Some(true),
3292 metadata: None,
3293 };
3294
3295 let update = TransactionUpdateRequest {
3297 status: Some(TransactionStatus::Sent),
3298 ..Default::default()
3299 };
3300
3301 transaction.apply_partial_update(update);
3303
3304 assert_eq!(transaction.status, TransactionStatus::Sent);
3306 assert_eq!(
3307 transaction.status_reason,
3308 Some("Initial reason".to_string())
3309 );
3310 assert_eq!(
3311 transaction.sent_at,
3312 Some("2023-01-01T10:00:00Z".to_string())
3313 );
3314 assert_eq!(transaction.confirmed_at, None);
3315 assert_eq!(transaction.hashes, vec!["0xoriginal".to_string()]);
3316 assert_eq!(transaction.noop_count, Some(5));
3317 assert_eq!(transaction.is_canceled, Some(true));
3318
3319 assert!(transaction.delete_at.is_none());
3321 }
3322
3323 #[test]
3324 fn test_apply_partial_update_empty_update() {
3325 let mut transaction = create_test_transaction();
3327 let original_transaction = transaction.clone();
3328
3329 let update = TransactionUpdateRequest::default();
3331 transaction.apply_partial_update(update);
3332
3333 assert_eq!(transaction.id, original_transaction.id);
3335 assert_eq!(transaction.status, original_transaction.status);
3336 assert_eq!(
3337 transaction.status_reason,
3338 original_transaction.status_reason
3339 );
3340 assert_eq!(transaction.sent_at, original_transaction.sent_at);
3341 assert_eq!(transaction.confirmed_at, original_transaction.confirmed_at);
3342 assert_eq!(transaction.hashes, original_transaction.hashes);
3343 assert_eq!(transaction.noop_count, original_transaction.noop_count);
3344 assert_eq!(transaction.is_canceled, original_transaction.is_canceled);
3345 assert_eq!(transaction.delete_at, original_transaction.delete_at);
3346 }
3347
3348 mod extract_stellar_valid_until_tests {
3349 use super::*;
3350 use crate::models::transaction::request::stellar::StellarTransactionRequest;
3351 use chrono::{Duration, Utc};
3352
3353 fn make_stellar_request(
3354 valid_until: Option<String>,
3355 transaction_xdr: Option<String>,
3356 ) -> StellarTransactionRequest {
3357 StellarTransactionRequest {
3358 source_account: Some(
3359 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
3360 ),
3361 network: "testnet".to_string(),
3362 operations: Some(vec![OperationSpec::Payment {
3363 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
3364 .to_string(),
3365 amount: 1000000,
3366 asset: AssetSpec::Native,
3367 }]),
3368 memo: None,
3369 valid_until,
3370 transaction_xdr,
3371 fee_bump: None,
3372 max_fee: None,
3373 signed_auth_entry: None,
3374 }
3375 }
3376
3377 #[test]
3378 fn test_with_explicit_valid_until_from_request() {
3379 let request = make_stellar_request(Some("2025-12-31T23:59:59Z".to_string()), None);
3380 let now = Utc::now();
3381
3382 let result = extract_stellar_valid_until(&request, now);
3383
3384 assert_eq!(result, Some("2025-12-31T23:59:59Z".to_string()));
3385 }
3386
3387 #[test]
3388 fn test_operations_without_valid_until_uses_default() {
3389 let request = make_stellar_request(None, None);
3390 let now = Utc::now();
3391
3392 let result = extract_stellar_valid_until(&request, now);
3393
3394 assert!(result.is_some());
3396 let valid_until = result.unwrap();
3397 let parsed = chrono::DateTime::parse_from_rfc3339(&valid_until).unwrap();
3398 let expected_min = now + Duration::minutes(1);
3399 let expected_max = now + Duration::minutes(3);
3400 assert!(parsed.with_timezone(&Utc) > expected_min);
3401 assert!(parsed.with_timezone(&Utc) < expected_max);
3402 }
3403
3404 #[test]
3405 fn test_xdr_without_time_bounds_returns_none() {
3406 let request = make_stellar_request(None, Some("invalid_xdr".to_string()));
3410 let now = Utc::now();
3411
3412 let result = extract_stellar_valid_until(&request, now);
3413
3414 assert!(result.is_none());
3416 }
3417 }
3418}