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 },
8 domain::{
9 evm::PriceParams,
10 stellar::validation::{validate_operations, validate_soroban_memo_restriction},
11 xdr_utils::{is_signed, parse_transaction_xdr},
12 SignTransactionResponseEvm,
13 },
14 models::{
15 transaction::{
16 request::{evm::EvmTransactionRequest, stellar::StellarTransactionRequest},
17 solana::SolanaInstructionSpec,
18 stellar::{DecoratedSignature, MemoSpec, OperationSpec},
19 },
20 AddressError, EvmNetwork, NetworkRepoModel, NetworkTransactionRequest, NetworkType,
21 RelayerError, RelayerRepoModel, SignerError, StellarNetwork, StellarValidationError,
22 TransactionError, U256,
23 },
24 utils::{deserialize_optional_u128, serialize_optional_u128},
25};
26use alloy::{
27 consensus::{TxEip1559, TxLegacy},
28 primitives::{Address as AlloyAddress, Bytes, TxKind},
29 rpc::types::AccessList,
30};
31
32use chrono::{Duration, Utc};
33use serde::{Deserialize, Serialize};
34use std::{convert::TryFrom, str::FromStr};
35use strum::Display;
36
37use utoipa::ToSchema;
38use uuid::Uuid;
39
40use soroban_rs::xdr::{
41 Transaction as SorobanTransaction, TransactionEnvelope, TransactionV1Envelope, VecM,
42};
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, Display)]
45#[serde(rename_all = "lowercase")]
46pub enum TransactionStatus {
47 Canceled,
48 Pending,
49 Sent,
50 Submitted,
51 Mined,
52 Confirmed,
53 Failed,
54 Expired,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, Default)]
58pub struct TransactionUpdateRequest {
59 pub status: Option<TransactionStatus>,
60 pub status_reason: Option<String>,
61 pub sent_at: Option<String>,
62 pub confirmed_at: Option<String>,
63 pub network_data: Option<NetworkTransactionData>,
64 pub priced_at: Option<String>,
66 pub hashes: Option<Vec<String>>,
68 pub noop_count: Option<u32>,
70 pub is_canceled: Option<bool>,
72 pub delete_at: Option<String>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct TransactionRepoModel {
78 pub id: String,
79 pub relayer_id: String,
80 pub status: TransactionStatus,
81 pub status_reason: Option<String>,
82 pub created_at: String,
83 pub sent_at: Option<String>,
84 pub confirmed_at: Option<String>,
85 pub valid_until: Option<String>,
86 pub delete_at: Option<String>,
88 pub network_data: NetworkTransactionData,
89 pub priced_at: Option<String>,
91 pub hashes: Vec<String>,
93 pub network_type: NetworkType,
94 pub noop_count: Option<u32>,
95 pub is_canceled: Option<bool>,
96}
97
98impl TransactionRepoModel {
99 pub fn validate(&self) -> Result<(), TransactionError> {
105 Ok(())
106 }
107
108 fn calculate_delete_at(expiration_hours: u64) -> Option<String> {
110 let delete_time = Utc::now() + Duration::hours(expiration_hours as i64);
111 Some(delete_time.to_rfc3339())
112 }
113
114 pub fn update_delete_at_if_final_status(&mut self) {
116 if self.delete_at.is_none() && FINAL_TRANSACTION_STATUSES.contains(&self.status) {
117 let expiration_hours = ServerConfig::get_transaction_expiration_hours();
118 self.delete_at = Self::calculate_delete_at(expiration_hours);
119 }
120 }
121
122 pub fn apply_partial_update(&mut self, update: TransactionUpdateRequest) {
130 if let Some(status) = update.status {
132 self.status = status;
133 self.update_delete_at_if_final_status();
134 }
135 if let Some(status_reason) = update.status_reason {
136 self.status_reason = Some(status_reason);
137 }
138 if let Some(sent_at) = update.sent_at {
139 self.sent_at = Some(sent_at);
140 }
141 if let Some(confirmed_at) = update.confirmed_at {
142 self.confirmed_at = Some(confirmed_at);
143 }
144 if let Some(network_data) = update.network_data {
145 self.network_data = network_data;
146 }
147 if let Some(priced_at) = update.priced_at {
148 self.priced_at = Some(priced_at);
149 }
150 if let Some(hashes) = update.hashes {
151 self.hashes = hashes;
152 }
153 if let Some(noop_count) = update.noop_count {
154 self.noop_count = Some(noop_count);
155 }
156 if let Some(is_canceled) = update.is_canceled {
157 self.is_canceled = Some(is_canceled);
158 }
159 if let Some(delete_at) = update.delete_at {
160 self.delete_at = Some(delete_at);
161 }
162 }
163
164 pub fn create_reset_update_request(
175 &self,
176 ) -> Result<TransactionUpdateRequest, TransactionError> {
177 let network_data = match &self.network_data {
178 NetworkTransactionData::Stellar(stellar_data) => Some(NetworkTransactionData::Stellar(
179 stellar_data.clone().reset_to_pre_prepare_state(),
180 )),
181 _ => None,
183 };
184
185 Ok(TransactionUpdateRequest {
186 status: Some(TransactionStatus::Pending),
187 status_reason: None,
188 sent_at: None,
189 confirmed_at: None,
190 network_data,
191 priced_at: None,
192 hashes: Some(vec![]),
193 noop_count: None,
194 is_canceled: None,
195 delete_at: None,
196 })
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(tag = "network_data", content = "data")]
202#[allow(clippy::large_enum_variant)]
203pub enum NetworkTransactionData {
204 Evm(EvmTransactionData),
205 Solana(SolanaTransactionData),
206 Stellar(StellarTransactionData),
207}
208
209impl NetworkTransactionData {
210 pub fn get_evm_transaction_data(&self) -> Result<EvmTransactionData, TransactionError> {
211 match self {
212 NetworkTransactionData::Evm(data) => Ok(data.clone()),
213 _ => Err(TransactionError::InvalidType(
214 "Expected EVM transaction".to_string(),
215 )),
216 }
217 }
218
219 pub fn get_solana_transaction_data(&self) -> Result<SolanaTransactionData, TransactionError> {
220 match self {
221 NetworkTransactionData::Solana(data) => Ok(data.clone()),
222 _ => Err(TransactionError::InvalidType(
223 "Expected Solana transaction".to_string(),
224 )),
225 }
226 }
227
228 pub fn get_stellar_transaction_data(&self) -> Result<StellarTransactionData, TransactionError> {
229 match self {
230 NetworkTransactionData::Stellar(data) => Ok(data.clone()),
231 _ => Err(TransactionError::InvalidType(
232 "Expected Stellar transaction".to_string(),
233 )),
234 }
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
239pub struct EvmTransactionDataSignature {
240 pub r: String,
241 pub s: String,
242 pub v: u8,
243 pub sig: String,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct EvmTransactionData {
248 #[serde(
249 serialize_with = "serialize_optional_u128",
250 deserialize_with = "deserialize_optional_u128",
251 default
252 )]
253 pub gas_price: Option<u128>,
254 pub gas_limit: Option<u64>,
255 pub nonce: Option<u64>,
256 pub value: U256,
257 pub data: Option<String>,
258 pub from: String,
259 pub to: Option<String>,
260 pub chain_id: u64,
261 pub hash: Option<String>,
262 pub signature: Option<EvmTransactionDataSignature>,
263 pub speed: Option<Speed>,
264 #[serde(
265 serialize_with = "serialize_optional_u128",
266 deserialize_with = "deserialize_optional_u128",
267 default
268 )]
269 pub max_fee_per_gas: Option<u128>,
270 #[serde(
271 serialize_with = "serialize_optional_u128",
272 deserialize_with = "deserialize_optional_u128",
273 default
274 )]
275 pub max_priority_fee_per_gas: Option<u128>,
276 pub raw: Option<Vec<u8>>,
277}
278
279impl EvmTransactionData {
280 pub fn for_replacement(old_data: &EvmTransactionData, request: &EvmTransactionRequest) -> Self {
292 Self {
293 chain_id: old_data.chain_id,
295 from: old_data.from.clone(),
296 nonce: old_data.nonce, to: request.to.clone(),
300 value: request.value,
301 data: request.data.clone(),
302 gas_limit: request.gas_limit,
303 speed: request
304 .speed
305 .clone()
306 .or_else(|| old_data.speed.clone())
307 .or(Some(DEFAULT_TRANSACTION_SPEED)),
308
309 gas_price: None,
311 max_fee_per_gas: None,
312 max_priority_fee_per_gas: None,
313
314 signature: None,
316 hash: None,
317 raw: None,
318 }
319 }
320
321 pub fn with_price_params(mut self, price_params: PriceParams) -> Self {
329 self.gas_price = price_params.gas_price;
330 self.max_fee_per_gas = price_params.max_fee_per_gas;
331 self.max_priority_fee_per_gas = price_params.max_priority_fee_per_gas;
332
333 self
334 }
335
336 pub fn with_gas_estimate(mut self, gas_limit: u64) -> Self {
344 self.gas_limit = Some(gas_limit);
345 self
346 }
347
348 pub fn with_nonce(mut self, nonce: u64) -> Self {
356 self.nonce = Some(nonce);
357 self
358 }
359
360 pub fn with_signed_transaction_data(mut self, sig: SignTransactionResponseEvm) -> Self {
368 self.signature = Some(sig.signature);
369 self.hash = Some(sig.hash);
370 self.raw = Some(sig.raw);
371 self
372 }
373}
374
375#[cfg(test)]
376impl Default for EvmTransactionData {
377 fn default() -> Self {
378 Self {
379 from: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(), to: Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string()), gas_price: Some(20000000000),
382 value: U256::from(1000000000000000000u128), data: Some("0x".to_string()),
384 nonce: Some(1),
385 chain_id: 1,
386 gas_limit: Some(DEFAULT_GAS_LIMIT),
387 hash: None,
388 signature: None,
389 speed: None,
390 max_fee_per_gas: None,
391 max_priority_fee_per_gas: None,
392 raw: None,
393 }
394 }
395}
396
397#[cfg(test)]
398impl Default for TransactionRepoModel {
399 fn default() -> Self {
400 Self {
401 id: "00000000-0000-0000-0000-000000000001".to_string(),
402 relayer_id: "00000000-0000-0000-0000-000000000002".to_string(),
403 status: TransactionStatus::Pending,
404 created_at: "2023-01-01T00:00:00Z".to_string(),
405 status_reason: None,
406 sent_at: None,
407 confirmed_at: None,
408 valid_until: None,
409 delete_at: None,
410 network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
411 network_type: NetworkType::Evm,
412 priced_at: None,
413 hashes: Vec::new(),
414 noop_count: None,
415 is_canceled: Some(false),
416 }
417 }
418}
419
420pub trait EvmTransactionDataTrait {
421 fn is_legacy(&self) -> bool;
422 fn is_eip1559(&self) -> bool;
423 fn is_speed(&self) -> bool;
424}
425
426impl EvmTransactionDataTrait for EvmTransactionData {
427 fn is_legacy(&self) -> bool {
428 self.gas_price.is_some()
429 }
430
431 fn is_eip1559(&self) -> bool {
432 self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some()
433 }
434
435 fn is_speed(&self) -> bool {
436 self.speed.is_some()
437 }
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize, Default)]
441pub struct SolanaTransactionData {
442 pub transaction: Option<String>,
444 pub instructions: Option<Vec<SolanaInstructionSpec>>,
446 pub signature: Option<String>,
448}
449
450impl SolanaTransactionData {
451 pub fn with_signature(mut self, signature: String) -> Self {
454 self.signature = Some(signature);
455 self
456 }
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize)]
461pub enum TransactionInput {
462 Operations(Vec<OperationSpec>),
464 UnsignedXdr(String),
466 SignedXdr { xdr: String, max_fee: i64 },
468}
469
470impl Default for TransactionInput {
471 fn default() -> Self {
472 TransactionInput::Operations(vec![])
473 }
474}
475
476impl TransactionInput {
477 pub fn from_stellar_request(
479 request: &StellarTransactionRequest,
480 ) -> Result<Self, TransactionError> {
481 if let Some(xdr) = &request.transaction_xdr {
483 let envelope = parse_transaction_xdr(xdr, false)
484 .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
485
486 return if request.fee_bump == Some(true) {
487 if !is_signed(&envelope) {
489 Err(TransactionError::ValidationError(
490 "Cannot request fee_bump with unsigned XDR".to_string(),
491 ))
492 } else {
493 let max_fee = request.max_fee.unwrap_or(STELLAR_DEFAULT_MAX_FEE);
494 Ok(TransactionInput::SignedXdr {
495 xdr: xdr.clone(),
496 max_fee,
497 })
498 }
499 } else {
500 if is_signed(&envelope) {
502 Err(TransactionError::ValidationError(
503 StellarValidationError::UnexpectedSignedXdr.to_string(),
504 ))
505 } else {
506 Ok(TransactionInput::UnsignedXdr(xdr.clone()))
507 }
508 };
509 }
510
511 if let Some(operations) = &request.operations {
513 if operations.is_empty() {
514 return Err(TransactionError::ValidationError(
515 "Operations must not be empty".to_string(),
516 ));
517 }
518
519 if request.fee_bump == Some(true) {
520 return Err(TransactionError::ValidationError(
521 "Cannot request fee_bump with operations mode".to_string(),
522 ));
523 }
524
525 validate_operations(operations)
527 .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
528
529 validate_soroban_memo_restriction(operations, &request.memo)
531 .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
532
533 return Ok(TransactionInput::Operations(operations.clone()));
534 }
535
536 Err(TransactionError::ValidationError(
538 "Must provide either operations or transaction_xdr".to_string(),
539 ))
540 }
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct StellarTransactionData {
545 pub source_account: String,
546 pub fee: Option<u32>,
547 pub sequence_number: Option<i64>,
548 pub memo: Option<MemoSpec>,
549 pub valid_until: Option<String>,
550 pub network_passphrase: String,
551 pub signatures: Vec<DecoratedSignature>,
552 pub hash: Option<String>,
553 pub simulation_transaction_data: Option<String>,
554 pub transaction_input: TransactionInput,
555 pub signed_envelope_xdr: Option<String>,
556}
557
558impl StellarTransactionData {
559 pub fn reset_to_pre_prepare_state(mut self) -> Self {
568 self.fee = None;
570 self.sequence_number = None;
571 self.signatures = vec![];
572 self.signed_envelope_xdr = None;
573 self.simulation_transaction_data = None;
574
575 self.hash = None;
577
578 self
579 }
580
581 pub fn with_sequence_number(mut self, sequence_number: i64) -> Self {
589 self.sequence_number = Some(sequence_number);
590 self
591 }
592
593 pub fn with_fee(mut self, fee: u32) -> Self {
601 self.fee = Some(fee);
602 self
603 }
604
605 pub fn build_unsigned_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
613 match &self.transaction_input {
614 TransactionInput::Operations(_) => {
615 self.build_envelope_from_operations_unsigned()
617 }
618 TransactionInput::UnsignedXdr(xdr) => {
619 self.parse_xdr_envelope(xdr)
621 }
622 TransactionInput::SignedXdr { xdr, .. } => {
623 self.parse_xdr_envelope(xdr)
625 }
626 }
627 }
628
629 pub fn get_envelope_for_simulation(&self) -> Result<TransactionEnvelope, SignerError> {
637 self.build_unsigned_envelope()
638 }
639
640 pub fn build_signed_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
648 if let Some(ref xdr) = self.signed_envelope_xdr {
650 return self.parse_xdr_envelope(xdr);
651 }
652
653 match &self.transaction_input {
655 TransactionInput::Operations(_) => {
656 self.build_envelope_from_operations_signed()
658 }
659 TransactionInput::UnsignedXdr(xdr) => {
660 let envelope = self.parse_xdr_envelope(xdr)?;
662 self.attach_signatures_to_envelope(envelope)
663 }
664 TransactionInput::SignedXdr { xdr, .. } => {
665 self.parse_xdr_envelope(xdr)
667 }
668 }
669 }
670
671 pub fn get_envelope_for_submission(&self) -> Result<TransactionEnvelope, SignerError> {
679 self.build_signed_envelope()
680 }
681
682 fn build_envelope_from_operations_unsigned(&self) -> Result<TransactionEnvelope, SignerError> {
684 let tx = SorobanTransaction::try_from(self.clone())?;
685 Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
686 tx,
687 signatures: VecM::default(),
688 }))
689 }
690
691 fn build_envelope_from_operations_signed(&self) -> Result<TransactionEnvelope, SignerError> {
693 let tx = SorobanTransaction::try_from(self.clone())?;
694 let signatures = VecM::try_from(self.signatures.clone())
695 .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
696 Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
697 tx,
698 signatures,
699 }))
700 }
701
702 fn parse_xdr_envelope(&self, xdr: &str) -> Result<TransactionEnvelope, SignerError> {
704 use soroban_rs::xdr::{Limits, ReadXdr};
705 TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
706 .map_err(|e| SignerError::ConversionError(format!("Invalid XDR: {e}")))
707 }
708
709 fn attach_signatures_to_envelope(
711 &self,
712 envelope: TransactionEnvelope,
713 ) -> Result<TransactionEnvelope, SignerError> {
714 use soroban_rs::xdr::{Limits, ReadXdr, WriteXdr};
715
716 let envelope_xdr = envelope.to_xdr_base64(Limits::none()).map_err(|e| {
718 SignerError::ConversionError(format!("Failed to serialize envelope: {e}"))
719 })?;
720
721 let mut envelope = TransactionEnvelope::from_xdr_base64(&envelope_xdr, Limits::none())
722 .map_err(|e| SignerError::ConversionError(format!("Failed to parse envelope: {e}")))?;
723
724 let sigs = VecM::try_from(self.signatures.clone())
725 .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
726
727 match &mut envelope {
728 TransactionEnvelope::Tx(ref mut v1) => v1.signatures = sigs,
729 TransactionEnvelope::TxV0(ref mut v0) => v0.signatures = sigs,
730 TransactionEnvelope::TxFeeBump(_) => {
731 return Err(SignerError::ConversionError(
732 "Cannot attach signatures to fee-bump transaction directly".into(),
733 ));
734 }
735 }
736
737 Ok(envelope)
738 }
739
740 pub fn attach_signature(mut self, sig: DecoratedSignature) -> Self {
748 self.signatures.push(sig);
749 self
750 }
751
752 pub fn with_hash(mut self, hash: String) -> Self {
760 self.hash = Some(hash);
761 self
762 }
763
764 pub fn with_simulation_data(
766 mut self,
767 sim_response: soroban_rs::stellar_rpc_client::SimulateTransactionResponse,
768 operations_count: u64,
769 ) -> Result<Self, SignerError> {
770 use tracing::info;
771
772 let inclusion_fee = operations_count * STELLAR_DEFAULT_TRANSACTION_FEE as u64;
774 let resource_fee = sim_response.min_resource_fee;
775
776 let updated_fee = u32::try_from(inclusion_fee + resource_fee)
777 .map_err(|_| SignerError::ConversionError("Fee too high".to_string()))?
778 .max(STELLAR_DEFAULT_TRANSACTION_FEE);
779 self.fee = Some(updated_fee);
780
781 self.simulation_transaction_data = Some(sim_response.transaction_data);
783
784 info!(
785 "Applied simulation fee: {} stroops and stored transaction extension data",
786 updated_fee
787 );
788 Ok(self)
789 }
790}
791
792impl
793 TryFrom<(
794 &NetworkTransactionRequest,
795 &RelayerRepoModel,
796 &NetworkRepoModel,
797 )> for TransactionRepoModel
798{
799 type Error = RelayerError;
800
801 fn try_from(
802 (request, relayer_model, network_model): (
803 &NetworkTransactionRequest,
804 &RelayerRepoModel,
805 &NetworkRepoModel,
806 ),
807 ) -> Result<Self, Self::Error> {
808 let now = Utc::now().to_rfc3339();
809
810 match request {
811 NetworkTransactionRequest::Evm(evm_request) => {
812 let network = EvmNetwork::try_from(network_model.clone())?;
813 Ok(Self {
814 id: Uuid::new_v4().to_string(),
815 relayer_id: relayer_model.id.clone(),
816 status: TransactionStatus::Pending,
817 status_reason: None,
818 created_at: now,
819 sent_at: None,
820 confirmed_at: None,
821 valid_until: evm_request.valid_until.clone(),
822 delete_at: None,
823 network_type: NetworkType::Evm,
824 network_data: NetworkTransactionData::Evm(EvmTransactionData {
825 gas_price: evm_request.gas_price,
826 gas_limit: evm_request.gas_limit,
827 nonce: None,
828 value: evm_request.value,
829 data: evm_request.data.clone(),
830 from: relayer_model.address.clone(),
831 to: evm_request.to.clone(),
832 chain_id: network.id(),
833 hash: None,
834 signature: None,
835 speed: evm_request.speed.clone(),
836 max_fee_per_gas: evm_request.max_fee_per_gas,
837 max_priority_fee_per_gas: evm_request.max_priority_fee_per_gas,
838 raw: None,
839 }),
840 priced_at: None,
841 hashes: Vec::new(),
842 noop_count: None,
843 is_canceled: Some(false),
844 })
845 }
846 NetworkTransactionRequest::Solana(solana_request) => Ok(Self {
847 id: Uuid::new_v4().to_string(),
848 relayer_id: relayer_model.id.clone(),
849 status: TransactionStatus::Pending,
850 status_reason: None,
851 created_at: now,
852 sent_at: None,
853 confirmed_at: None,
854 valid_until: solana_request.valid_until.clone(),
855 delete_at: None,
856 network_type: NetworkType::Solana,
857 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
858 transaction: solana_request.transaction.clone().map(|t| t.into_inner()),
859 instructions: solana_request.instructions.clone(),
860 signature: None,
861 }),
862 priced_at: None,
863 hashes: Vec::new(),
864 noop_count: None,
865 is_canceled: Some(false),
866 }),
867 NetworkTransactionRequest::Stellar(stellar_request) => {
868 let source_account = stellar_request.source_account.clone();
870
871 let stellar_data = StellarTransactionData {
873 source_account: source_account.unwrap_or_else(|| relayer_model.address.clone()),
874 memo: stellar_request.memo.clone(),
875 valid_until: stellar_request.valid_until.clone(),
876 network_passphrase: StellarNetwork::try_from(network_model.clone())?.passphrase,
877 signatures: Vec::new(),
878 hash: None,
879 fee: None,
880 sequence_number: None,
881 simulation_transaction_data: None,
882 transaction_input: TransactionInput::from_stellar_request(stellar_request)
883 .map_err(|e| RelayerError::ValidationError(e.to_string()))?,
884 signed_envelope_xdr: None,
885 };
886
887 Ok(Self {
888 id: Uuid::new_v4().to_string(),
889 relayer_id: relayer_model.id.clone(),
890 status: TransactionStatus::Pending,
891 status_reason: None,
892 created_at: now,
893 sent_at: None,
894 confirmed_at: None,
895 valid_until: None,
896 delete_at: None,
897 network_type: NetworkType::Stellar,
898 network_data: NetworkTransactionData::Stellar(stellar_data),
899 priced_at: None,
900 hashes: Vec::new(),
901 noop_count: None,
902 is_canceled: Some(false),
903 })
904 }
905 }
906 }
907}
908
909impl EvmTransactionData {
910 pub fn to_address(&self) -> Result<Option<AlloyAddress>, SignerError> {
917 Ok(match self.to.as_deref().filter(|s| !s.is_empty()) {
918 Some(addr_str) => Some(AlloyAddress::from_str(addr_str).map_err(|e| {
919 AddressError::ConversionError(format!("Invalid 'to' address: {e}"))
920 })?),
921 None => None,
922 })
923 }
924
925 pub fn data_to_bytes(&self) -> Result<Bytes, SignerError> {
931 Bytes::from_str(self.data.as_deref().unwrap_or(""))
932 .map_err(|e| SignerError::SigningError(format!("Invalid transaction data: {e}")))
933 }
934}
935
936impl TryFrom<NetworkTransactionData> for TxLegacy {
937 type Error = SignerError;
938
939 fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
940 match tx {
941 NetworkTransactionData::Evm(tx) => {
942 let tx_kind = match tx.to_address()? {
943 Some(addr) => TxKind::Call(addr),
944 None => TxKind::Create,
945 };
946
947 Ok(Self {
948 chain_id: Some(tx.chain_id),
949 nonce: tx.nonce.unwrap_or(0),
950 gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
951 gas_price: tx.gas_price.unwrap_or(0),
952 to: tx_kind,
953 value: tx.value,
954 input: tx.data_to_bytes()?,
955 })
956 }
957 _ => Err(SignerError::SigningError(
958 "Not an EVM transaction".to_string(),
959 )),
960 }
961 }
962}
963
964impl TryFrom<NetworkTransactionData> for TxEip1559 {
965 type Error = SignerError;
966
967 fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
968 match tx {
969 NetworkTransactionData::Evm(tx) => {
970 let tx_kind = match tx.to_address()? {
971 Some(addr) => TxKind::Call(addr),
972 None => TxKind::Create,
973 };
974
975 Ok(Self {
976 chain_id: tx.chain_id,
977 nonce: tx.nonce.unwrap_or(0),
978 gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
979 max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
980 max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
981 to: tx_kind,
982 value: tx.value,
983 access_list: AccessList::default(),
984 input: tx.data_to_bytes()?,
985 })
986 }
987 _ => Err(SignerError::SigningError(
988 "Not an EVM transaction".to_string(),
989 )),
990 }
991 }
992}
993
994impl TryFrom<&EvmTransactionData> for TxLegacy {
995 type Error = SignerError;
996
997 fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
998 let tx_kind = match tx.to_address()? {
999 Some(addr) => TxKind::Call(addr),
1000 None => TxKind::Create,
1001 };
1002
1003 Ok(Self {
1004 chain_id: Some(tx.chain_id),
1005 nonce: tx.nonce.unwrap_or(0),
1006 gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
1007 gas_price: tx.gas_price.unwrap_or(0),
1008 to: tx_kind,
1009 value: tx.value,
1010 input: tx.data_to_bytes()?,
1011 })
1012 }
1013}
1014
1015impl TryFrom<EvmTransactionData> for TxLegacy {
1016 type Error = SignerError;
1017
1018 fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
1019 Self::try_from(&tx)
1020 }
1021}
1022
1023impl TryFrom<&EvmTransactionData> for TxEip1559 {
1024 type Error = SignerError;
1025
1026 fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
1027 let tx_kind = match tx.to_address()? {
1028 Some(addr) => TxKind::Call(addr),
1029 None => TxKind::Create,
1030 };
1031
1032 Ok(Self {
1033 chain_id: tx.chain_id,
1034 nonce: tx.nonce.unwrap_or(0),
1035 gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
1036 max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
1037 max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
1038 to: tx_kind,
1039 value: tx.value,
1040 access_list: AccessList::default(),
1041 input: tx.data_to_bytes()?,
1042 })
1043 }
1044}
1045
1046impl TryFrom<EvmTransactionData> for TxEip1559 {
1047 type Error = SignerError;
1048
1049 fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
1050 Self::try_from(&tx)
1051 }
1052}
1053
1054impl From<&[u8; 65]> for EvmTransactionDataSignature {
1055 fn from(bytes: &[u8; 65]) -> Self {
1056 Self {
1057 r: hex::encode(&bytes[0..32]),
1058 s: hex::encode(&bytes[32..64]),
1059 v: bytes[64],
1060 sig: hex::encode(bytes),
1061 }
1062 }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use lazy_static::lazy_static;
1068 use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
1069 use std::sync::Mutex;
1070
1071 use super::*;
1072 use crate::{
1073 config::{
1074 EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
1075 },
1076 models::{
1077 network::NetworkConfigData,
1078 relayer::{
1079 RelayerEvmPolicy, RelayerNetworkPolicy, RelayerSolanaPolicy, RelayerStellarPolicy,
1080 },
1081 transaction::stellar::AssetSpec,
1082 EncodedSerializedTransaction,
1083 },
1084 };
1085
1086 lazy_static! {
1088 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
1089 }
1090
1091 #[test]
1092 fn test_signature_from_bytes() {
1093 let test_bytes: [u8; 65] = [
1094 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
1095 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,
1097 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 27, ];
1100
1101 let signature = EvmTransactionDataSignature::from(&test_bytes);
1102
1103 assert_eq!(signature.r.len(), 64); assert_eq!(signature.s.len(), 64); assert_eq!(signature.v, 27);
1106 assert_eq!(signature.sig.len(), 130); }
1108
1109 #[test]
1110 fn test_stellar_transaction_data_reset_to_pre_prepare_state() {
1111 let stellar_data = StellarTransactionData {
1112 source_account: "GTEST".to_string(),
1113 fee: Some(100),
1114 sequence_number: Some(42),
1115 memo: Some(MemoSpec::Text {
1116 value: "test memo".to_string(),
1117 }),
1118 valid_until: Some("2024-12-31".to_string()),
1119 network_passphrase: "Test Network".to_string(),
1120 signatures: vec![], hash: Some("test-hash".to_string()),
1122 simulation_transaction_data: Some("simulation-data".to_string()),
1123 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1124 destination: "GDEST".to_string(),
1125 amount: 1000,
1126 asset: AssetSpec::Native,
1127 }]),
1128 signed_envelope_xdr: Some("signed-xdr".to_string()),
1129 };
1130
1131 let reset_data = stellar_data.clone().reset_to_pre_prepare_state();
1132
1133 assert_eq!(reset_data.source_account, stellar_data.source_account);
1135 assert_eq!(reset_data.memo, stellar_data.memo);
1136 assert_eq!(reset_data.valid_until, stellar_data.valid_until);
1137 assert_eq!(
1138 reset_data.network_passphrase,
1139 stellar_data.network_passphrase
1140 );
1141 assert!(matches!(
1142 reset_data.transaction_input,
1143 TransactionInput::Operations(_)
1144 ));
1145
1146 assert_eq!(reset_data.fee, None);
1148 assert_eq!(reset_data.sequence_number, None);
1149 assert!(reset_data.signatures.is_empty());
1150 assert_eq!(reset_data.hash, None);
1151 assert_eq!(reset_data.simulation_transaction_data, None);
1152 assert_eq!(reset_data.signed_envelope_xdr, None);
1153 }
1154
1155 #[test]
1156 fn test_transaction_repo_model_create_reset_update_request() {
1157 let stellar_data = StellarTransactionData {
1158 source_account: "GTEST".to_string(),
1159 fee: Some(100),
1160 sequence_number: Some(42),
1161 memo: None,
1162 valid_until: None,
1163 network_passphrase: "Test Network".to_string(),
1164 signatures: vec![],
1165 hash: Some("test-hash".to_string()),
1166 simulation_transaction_data: None,
1167 transaction_input: TransactionInput::Operations(vec![]),
1168 signed_envelope_xdr: Some("signed-xdr".to_string()),
1169 };
1170
1171 let tx = TransactionRepoModel {
1172 id: "tx-1".to_string(),
1173 relayer_id: "relayer-1".to_string(),
1174 status: TransactionStatus::Failed,
1175 status_reason: Some("Bad sequence".to_string()),
1176 created_at: "2024-01-01".to_string(),
1177 sent_at: Some("2024-01-02".to_string()),
1178 confirmed_at: Some("2024-01-03".to_string()),
1179 valid_until: None,
1180 network_data: NetworkTransactionData::Stellar(stellar_data),
1181 priced_at: None,
1182 hashes: vec!["hash1".to_string(), "hash2".to_string()],
1183 network_type: NetworkType::Stellar,
1184 noop_count: None,
1185 is_canceled: None,
1186 delete_at: None,
1187 };
1188
1189 let update_req = tx.create_reset_update_request().unwrap();
1190
1191 assert_eq!(update_req.status, Some(TransactionStatus::Pending));
1193 assert_eq!(update_req.status_reason, None);
1194 assert_eq!(update_req.sent_at, None);
1195 assert_eq!(update_req.confirmed_at, None);
1196 assert_eq!(update_req.hashes, Some(vec![]));
1197
1198 if let Some(NetworkTransactionData::Stellar(reset_data)) = update_req.network_data {
1200 assert_eq!(reset_data.fee, None);
1201 assert_eq!(reset_data.sequence_number, None);
1202 assert_eq!(reset_data.hash, None);
1203 assert_eq!(reset_data.signed_envelope_xdr, None);
1204 } else {
1205 panic!("Expected Stellar network data");
1206 }
1207 }
1208
1209 fn create_sample_evm_tx_data() -> EvmTransactionData {
1211 EvmTransactionData {
1212 gas_price: Some(20_000_000_000),
1213 gas_limit: Some(21000),
1214 nonce: Some(5),
1215 value: U256::from(1000000000000000000u128), data: Some("0x".to_string()),
1217 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1218 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1219 chain_id: 1,
1220 hash: None,
1221 signature: None,
1222 speed: None,
1223 max_fee_per_gas: None,
1224 max_priority_fee_per_gas: None,
1225 raw: None,
1226 }
1227 }
1228
1229 #[test]
1231 fn test_evm_tx_with_price_params() {
1232 let tx_data = create_sample_evm_tx_data();
1233 let price_params = PriceParams {
1234 gas_price: None,
1235 max_fee_per_gas: Some(30_000_000_000),
1236 max_priority_fee_per_gas: Some(2_000_000_000),
1237 is_min_bumped: None,
1238 extra_fee: None,
1239 total_cost: U256::ZERO,
1240 };
1241
1242 let updated_tx = tx_data.with_price_params(price_params);
1243
1244 assert_eq!(updated_tx.max_fee_per_gas, Some(30_000_000_000));
1245 assert_eq!(updated_tx.max_priority_fee_per_gas, Some(2_000_000_000));
1246 }
1247
1248 #[test]
1249 fn test_evm_tx_with_gas_estimate() {
1250 let tx_data = create_sample_evm_tx_data();
1251 let new_gas_limit = 30000;
1252
1253 let updated_tx = tx_data.with_gas_estimate(new_gas_limit);
1254
1255 assert_eq!(updated_tx.gas_limit, Some(new_gas_limit));
1256 }
1257
1258 #[test]
1259 fn test_evm_tx_with_nonce() {
1260 let tx_data = create_sample_evm_tx_data();
1261 let new_nonce = 10;
1262
1263 let updated_tx = tx_data.with_nonce(new_nonce);
1264
1265 assert_eq!(updated_tx.nonce, Some(new_nonce));
1266 }
1267
1268 #[test]
1269 fn test_evm_tx_with_signed_transaction_data() {
1270 let tx_data = create_sample_evm_tx_data();
1271
1272 let signature = EvmTransactionDataSignature {
1273 r: "r_value".to_string(),
1274 s: "s_value".to_string(),
1275 v: 27,
1276 sig: "signature_value".to_string(),
1277 };
1278
1279 let signed_tx_response = SignTransactionResponseEvm {
1280 signature,
1281 hash: "0xabcdef1234567890".to_string(),
1282 raw: vec![1, 2, 3, 4, 5],
1283 };
1284
1285 let updated_tx = tx_data.with_signed_transaction_data(signed_tx_response);
1286
1287 assert_eq!(updated_tx.signature.as_ref().unwrap().r, "r_value");
1288 assert_eq!(updated_tx.signature.as_ref().unwrap().s, "s_value");
1289 assert_eq!(updated_tx.signature.as_ref().unwrap().v, 27);
1290 assert_eq!(updated_tx.hash, Some("0xabcdef1234567890".to_string()));
1291 assert_eq!(updated_tx.raw, Some(vec![1, 2, 3, 4, 5]));
1292 }
1293
1294 #[test]
1295 fn test_evm_tx_to_address() {
1296 let tx_data = create_sample_evm_tx_data();
1298 let address_result = tx_data.to_address();
1299 assert!(address_result.is_ok());
1300 let address_option = address_result.unwrap();
1301 assert!(address_option.is_some());
1302 assert_eq!(
1303 address_option.unwrap().to_string().to_lowercase(),
1304 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_lowercase()
1305 );
1306
1307 let mut contract_creation_tx = create_sample_evm_tx_data();
1309 contract_creation_tx.to = None;
1310 let address_result = contract_creation_tx.to_address();
1311 assert!(address_result.is_ok());
1312 assert!(address_result.unwrap().is_none());
1313
1314 let mut empty_address_tx = create_sample_evm_tx_data();
1316 empty_address_tx.to = Some("".to_string());
1317 let address_result = empty_address_tx.to_address();
1318 assert!(address_result.is_ok());
1319 assert!(address_result.unwrap().is_none());
1320
1321 let mut invalid_address_tx = create_sample_evm_tx_data();
1323 invalid_address_tx.to = Some("0xINVALID".to_string());
1324 let address_result = invalid_address_tx.to_address();
1325 assert!(address_result.is_err());
1326 }
1327
1328 #[test]
1329 fn test_evm_tx_data_to_bytes() {
1330 let mut tx_data = create_sample_evm_tx_data();
1332 tx_data.data = Some("0x1234".to_string());
1333 let bytes_result = tx_data.data_to_bytes();
1334 assert!(bytes_result.is_ok());
1335 assert_eq!(bytes_result.unwrap().as_ref(), &[0x12, 0x34]);
1336
1337 tx_data.data = Some("".to_string());
1339 assert!(tx_data.data_to_bytes().is_ok());
1340
1341 tx_data.data = None;
1343 assert!(tx_data.data_to_bytes().is_ok());
1344
1345 tx_data.data = Some("0xZZ".to_string());
1347 assert!(tx_data.data_to_bytes().is_err());
1348 }
1349
1350 #[test]
1352 fn test_evm_tx_is_legacy() {
1353 let mut tx_data = create_sample_evm_tx_data();
1354
1355 assert!(tx_data.is_legacy());
1357
1358 tx_data.gas_price = None;
1360 assert!(!tx_data.is_legacy());
1361 }
1362
1363 #[test]
1364 fn test_evm_tx_is_eip1559() {
1365 let mut tx_data = create_sample_evm_tx_data();
1366
1367 assert!(!tx_data.is_eip1559());
1369
1370 tx_data.max_fee_per_gas = Some(30_000_000_000);
1372 tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1373 assert!(tx_data.is_eip1559());
1374
1375 tx_data.max_priority_fee_per_gas = None;
1377 assert!(!tx_data.is_eip1559());
1378 }
1379
1380 #[test]
1381 fn test_evm_tx_is_speed() {
1382 let mut tx_data = create_sample_evm_tx_data();
1383
1384 assert!(!tx_data.is_speed());
1386
1387 tx_data.speed = Some(Speed::Fast);
1389 assert!(tx_data.is_speed());
1390 }
1391
1392 #[test]
1394 fn test_network_tx_data_get_evm_transaction_data() {
1395 let evm_tx_data = create_sample_evm_tx_data();
1396 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1397
1398 let result = network_data.get_evm_transaction_data();
1400 assert!(result.is_ok());
1401 assert_eq!(result.unwrap().chain_id, evm_tx_data.chain_id);
1402
1403 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1405 transaction: Some("transaction_123".to_string()),
1406 ..Default::default()
1407 });
1408 assert!(solana_data.get_evm_transaction_data().is_err());
1409 }
1410
1411 #[test]
1412 fn test_network_tx_data_get_solana_transaction_data() {
1413 let solana_tx_data = SolanaTransactionData {
1414 transaction: Some("transaction_123".to_string()),
1415 ..Default::default()
1416 };
1417 let network_data = NetworkTransactionData::Solana(solana_tx_data.clone());
1418
1419 let result = network_data.get_solana_transaction_data();
1421 assert!(result.is_ok());
1422 assert_eq!(result.unwrap().transaction, solana_tx_data.transaction);
1423
1424 let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1426 assert!(evm_data.get_solana_transaction_data().is_err());
1427 }
1428
1429 #[test]
1430 fn test_network_tx_data_get_stellar_transaction_data() {
1431 let stellar_tx_data = StellarTransactionData {
1432 source_account: "account123".to_string(),
1433 fee: Some(100),
1434 sequence_number: Some(5),
1435 memo: Some(MemoSpec::Text {
1436 value: "Test memo".to_string(),
1437 }),
1438 valid_until: Some("2025-01-01T00:00:00Z".to_string()),
1439 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1440 signatures: Vec::new(),
1441 hash: Some("hash123".to_string()),
1442 simulation_transaction_data: None,
1443 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1444 destination: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ".to_string(),
1445 amount: 100000000, asset: AssetSpec::Native,
1447 }]),
1448 signed_envelope_xdr: None,
1449 };
1450 let network_data = NetworkTransactionData::Stellar(stellar_tx_data.clone());
1451
1452 let result = network_data.get_stellar_transaction_data();
1454 assert!(result.is_ok());
1455 assert_eq!(
1456 result.unwrap().source_account,
1457 stellar_tx_data.source_account
1458 );
1459
1460 let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1462 assert!(evm_data.get_stellar_transaction_data().is_err());
1463 }
1464
1465 #[test]
1467 fn test_try_from_network_tx_data_for_tx_legacy() {
1468 let evm_tx_data = create_sample_evm_tx_data();
1470 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1471
1472 let result = TxLegacy::try_from(network_data);
1474 assert!(result.is_ok());
1475 let tx_legacy = result.unwrap();
1476
1477 assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1479 assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1480 assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1481 assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1482 assert_eq!(tx_legacy.value, evm_tx_data.value);
1483
1484 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1486 transaction: Some("transaction_123".to_string()),
1487 ..Default::default()
1488 });
1489 assert!(TxLegacy::try_from(solana_data).is_err());
1490 }
1491
1492 #[test]
1493 fn test_try_from_evm_tx_data_for_tx_legacy() {
1494 let evm_tx_data = create_sample_evm_tx_data();
1496
1497 let result = TxLegacy::try_from(evm_tx_data.clone());
1499 assert!(result.is_ok());
1500 let tx_legacy = result.unwrap();
1501
1502 assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1504 assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1505 assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1506 assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1507 assert_eq!(tx_legacy.value, evm_tx_data.value);
1508 }
1509
1510 fn dummy_signature() -> DecoratedSignature {
1511 let hint = SignatureHint([0; 4]);
1512 let bytes: Vec<u8> = vec![0u8; 64];
1513 let bytes_m: BytesM<64> = bytes.try_into().expect("BytesM conversion");
1514 DecoratedSignature {
1515 hint,
1516 signature: Signature(bytes_m),
1517 }
1518 }
1519
1520 fn test_stellar_tx_data() -> StellarTransactionData {
1521 StellarTransactionData {
1522 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1523 fee: Some(100),
1524 sequence_number: Some(1),
1525 memo: None,
1526 valid_until: None,
1527 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1528 signatures: Vec::new(),
1529 hash: None,
1530 simulation_transaction_data: None,
1531 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1532 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1533 amount: 1000,
1534 asset: AssetSpec::Native,
1535 }]),
1536 signed_envelope_xdr: None,
1537 }
1538 }
1539
1540 #[test]
1541 fn test_with_sequence_number() {
1542 let tx = test_stellar_tx_data();
1543 let updated = tx.with_sequence_number(42);
1544 assert_eq!(updated.sequence_number, Some(42));
1545 }
1546
1547 #[test]
1548 fn test_get_envelope_for_simulation() {
1549 let tx = test_stellar_tx_data();
1550 let env = tx.get_envelope_for_simulation();
1551 assert!(env.is_ok());
1552 let env = env.unwrap();
1553 match env {
1555 soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1556 assert_eq!(tx_env.signatures.len(), 0);
1557 }
1558 _ => {
1559 panic!("Expected TransactionEnvelope::Tx variant");
1560 }
1561 }
1562 }
1563
1564 #[test]
1565 fn test_get_envelope_for_submission() {
1566 let mut tx = test_stellar_tx_data();
1567 tx.signatures.push(dummy_signature());
1568 let env = tx.get_envelope_for_submission();
1569 assert!(env.is_ok());
1570 let env = env.unwrap();
1571 match env {
1572 soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1573 assert_eq!(tx_env.signatures.len(), 1);
1574 }
1575 _ => {
1576 panic!("Expected TransactionEnvelope::Tx variant");
1577 }
1578 }
1579 }
1580
1581 #[test]
1582 fn test_attach_signature() {
1583 let tx = test_stellar_tx_data();
1584 let sig = dummy_signature();
1585 let updated = tx.attach_signature(sig.clone());
1586 assert_eq!(updated.signatures.len(), 1);
1587 assert_eq!(updated.signatures[0], sig);
1588 }
1589
1590 #[test]
1591 fn test_with_hash() {
1592 let tx = test_stellar_tx_data();
1593 let updated = tx.with_hash("hash123".to_string());
1594 assert_eq!(updated.hash, Some("hash123".to_string()));
1595 }
1596
1597 #[test]
1598 fn test_evm_tx_for_replacement() {
1599 let old_data = create_sample_evm_tx_data();
1600 let new_request = EvmTransactionRequest {
1601 to: Some("0xNewRecipient".to_string()),
1602 value: U256::from(2000000000000000000u64), data: Some("0xNewData".to_string()),
1604 gas_limit: Some(25000),
1605 gas_price: Some(30000000000), max_fee_per_gas: Some(40000000000), max_priority_fee_per_gas: Some(2000000000), speed: Some(Speed::Fast),
1609 valid_until: None,
1610 };
1611
1612 let result = EvmTransactionData::for_replacement(&old_data, &new_request);
1613
1614 assert_eq!(result.chain_id, old_data.chain_id);
1616 assert_eq!(result.from, old_data.from);
1617 assert_eq!(result.nonce, old_data.nonce);
1618
1619 assert_eq!(result.to, new_request.to);
1621 assert_eq!(result.value, new_request.value);
1622 assert_eq!(result.data, new_request.data);
1623 assert_eq!(result.gas_limit, new_request.gas_limit);
1624 assert_eq!(result.speed, new_request.speed);
1625
1626 assert_eq!(result.gas_price, None);
1628 assert_eq!(result.max_fee_per_gas, None);
1629 assert_eq!(result.max_priority_fee_per_gas, None);
1630
1631 assert_eq!(result.signature, None);
1633 assert_eq!(result.hash, None);
1634 assert_eq!(result.raw, None);
1635 }
1636
1637 #[test]
1638 fn test_transaction_repo_model_validate() {
1639 let transaction = TransactionRepoModel::default();
1640 let result = transaction.validate();
1641 assert!(result.is_ok());
1642 }
1643
1644 #[test]
1645 fn test_try_from_network_transaction_request_evm() {
1646 use crate::models::{NetworkRepoModel, NetworkType, RelayerRepoModel};
1647
1648 let evm_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1649 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1650 value: U256::from(1000000000000000000u128),
1651 data: Some("0x1234".to_string()),
1652 gas_limit: Some(21000),
1653 gas_price: Some(20000000000),
1654 max_fee_per_gas: None,
1655 max_priority_fee_per_gas: None,
1656 speed: Some(Speed::Fast),
1657 valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1658 });
1659
1660 let relayer_model = RelayerRepoModel {
1661 id: "relayer-id".to_string(),
1662 name: "Test Relayer".to_string(),
1663 network: "network-id".to_string(),
1664 paused: false,
1665 network_type: NetworkType::Evm,
1666 signer_id: "signer-id".to_string(),
1667 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
1668 address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1669 notification_id: None,
1670 system_disabled: false,
1671 custom_rpc_urls: None,
1672 ..Default::default()
1673 };
1674
1675 let network_model = NetworkRepoModel {
1676 id: "evm:ethereum".to_string(),
1677 name: "ethereum".to_string(),
1678 network_type: NetworkType::Evm,
1679 config: NetworkConfigData::Evm(EvmNetworkConfig {
1680 common: NetworkConfigCommon {
1681 network: "ethereum".to_string(),
1682 from: None,
1683 rpc_urls: Some(vec!["https://mainnet.infura.io".to_string()]),
1684 explorer_urls: Some(vec!["https://etherscan.io".to_string()]),
1685 average_blocktime_ms: Some(12000),
1686 is_testnet: Some(false),
1687 tags: Some(vec!["mainnet".to_string()]),
1688 },
1689 chain_id: Some(1),
1690 required_confirmations: Some(12),
1691 features: None,
1692 symbol: Some("ETH".to_string()),
1693 gas_price_cache: None,
1694 }),
1695 };
1696
1697 let result = TransactionRepoModel::try_from((&evm_request, &relayer_model, &network_model));
1698 assert!(result.is_ok());
1699 let transaction = result.unwrap();
1700
1701 assert_eq!(transaction.relayer_id, relayer_model.id);
1702 assert_eq!(transaction.status, TransactionStatus::Pending);
1703 assert_eq!(transaction.network_type, NetworkType::Evm);
1704 assert_eq!(
1705 transaction.valid_until,
1706 Some("2024-12-31T23:59:59Z".to_string())
1707 );
1708 assert!(transaction.is_canceled == Some(false));
1709
1710 if let NetworkTransactionData::Evm(evm_data) = transaction.network_data {
1711 assert_eq!(evm_data.from, relayer_model.address);
1712 assert_eq!(
1713 evm_data.to,
1714 Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string())
1715 );
1716 assert_eq!(evm_data.value, U256::from(1000000000000000000u128));
1717 assert_eq!(evm_data.chain_id, 1);
1718 assert_eq!(evm_data.gas_limit, Some(21000));
1719 assert_eq!(evm_data.gas_price, Some(20000000000));
1720 assert_eq!(evm_data.speed, Some(Speed::Fast));
1721 } else {
1722 panic!("Expected EVM transaction data");
1723 }
1724 }
1725
1726 #[test]
1727 fn test_try_from_network_transaction_request_solana() {
1728 use crate::models::{
1729 NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1730 };
1731
1732 let solana_request = NetworkTransactionRequest::Solana(
1733 crate::models::transaction::request::solana::SolanaTransactionRequest {
1734 transaction: Some(EncodedSerializedTransaction::new(
1735 "transaction_123".to_string(),
1736 )),
1737 instructions: None,
1738 valid_until: None,
1739 },
1740 );
1741
1742 let relayer_model = RelayerRepoModel {
1743 id: "relayer-id".to_string(),
1744 name: "Test Solana Relayer".to_string(),
1745 network: "network-id".to_string(),
1746 paused: false,
1747 network_type: NetworkType::Solana,
1748 signer_id: "signer-id".to_string(),
1749 policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
1750 address: "solana_address".to_string(),
1751 notification_id: None,
1752 system_disabled: false,
1753 custom_rpc_urls: None,
1754 ..Default::default()
1755 };
1756
1757 let network_model = NetworkRepoModel {
1758 id: "solana:mainnet".to_string(),
1759 name: "mainnet".to_string(),
1760 network_type: NetworkType::Solana,
1761 config: NetworkConfigData::Solana(SolanaNetworkConfig {
1762 common: NetworkConfigCommon {
1763 network: "mainnet".to_string(),
1764 from: None,
1765 rpc_urls: Some(vec!["https://api.mainnet-beta.solana.com".to_string()]),
1766 explorer_urls: Some(vec!["https://explorer.solana.com".to_string()]),
1767 average_blocktime_ms: Some(400),
1768 is_testnet: Some(false),
1769 tags: Some(vec!["mainnet".to_string()]),
1770 },
1771 }),
1772 };
1773
1774 let result =
1775 TransactionRepoModel::try_from((&solana_request, &relayer_model, &network_model));
1776 assert!(result.is_ok());
1777 let transaction = result.unwrap();
1778
1779 assert_eq!(transaction.relayer_id, relayer_model.id);
1780 assert_eq!(transaction.status, TransactionStatus::Pending);
1781 assert_eq!(transaction.network_type, NetworkType::Solana);
1782 assert_eq!(transaction.valid_until, None);
1783
1784 if let NetworkTransactionData::Solana(solana_data) = transaction.network_data {
1785 assert_eq!(solana_data.transaction, Some("transaction_123".to_string()));
1786 assert_eq!(solana_data.signature, None);
1787 } else {
1788 panic!("Expected Solana transaction data");
1789 }
1790 }
1791
1792 #[test]
1793 fn test_try_from_network_transaction_request_stellar() {
1794 use crate::models::transaction::request::stellar::StellarTransactionRequest;
1795 use crate::models::{
1796 NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1797 };
1798
1799 let stellar_request = NetworkTransactionRequest::Stellar(StellarTransactionRequest {
1800 source_account: Some(
1801 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1802 ),
1803 network: "mainnet".to_string(),
1804 operations: Some(vec![OperationSpec::Payment {
1805 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1806 amount: 1000000,
1807 asset: AssetSpec::Native,
1808 }]),
1809 memo: Some(MemoSpec::Text {
1810 value: "Test memo".to_string(),
1811 }),
1812 valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1813 transaction_xdr: None,
1814 fee_bump: None,
1815 max_fee: None,
1816 });
1817
1818 let relayer_model = RelayerRepoModel {
1819 id: "relayer-id".to_string(),
1820 name: "Test Stellar Relayer".to_string(),
1821 network: "network-id".to_string(),
1822 paused: false,
1823 network_type: NetworkType::Stellar,
1824 signer_id: "signer-id".to_string(),
1825 policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
1826 address: "stellar_address".to_string(),
1827 notification_id: None,
1828 system_disabled: false,
1829 custom_rpc_urls: None,
1830 ..Default::default()
1831 };
1832
1833 let network_model = NetworkRepoModel {
1834 id: "stellar:mainnet".to_string(),
1835 name: "mainnet".to_string(),
1836 network_type: NetworkType::Stellar,
1837 config: NetworkConfigData::Stellar(StellarNetworkConfig {
1838 common: NetworkConfigCommon {
1839 network: "mainnet".to_string(),
1840 from: None,
1841 rpc_urls: Some(vec!["https://horizon.stellar.org".to_string()]),
1842 explorer_urls: Some(vec!["https://stellarchain.io".to_string()]),
1843 average_blocktime_ms: Some(5000),
1844 is_testnet: Some(false),
1845 tags: Some(vec!["mainnet".to_string()]),
1846 },
1847 passphrase: Some("Public Global Stellar Network ; September 2015".to_string()),
1848 }),
1849 };
1850
1851 let result =
1852 TransactionRepoModel::try_from((&stellar_request, &relayer_model, &network_model));
1853 assert!(result.is_ok());
1854 let transaction = result.unwrap();
1855
1856 assert_eq!(transaction.relayer_id, relayer_model.id);
1857 assert_eq!(transaction.status, TransactionStatus::Pending);
1858 assert_eq!(transaction.network_type, NetworkType::Stellar);
1859 assert_eq!(transaction.valid_until, None);
1860
1861 if let NetworkTransactionData::Stellar(stellar_data) = transaction.network_data {
1862 assert_eq!(
1863 stellar_data.source_account,
1864 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1865 );
1866 if let TransactionInput::Operations(ops) = &stellar_data.transaction_input {
1868 assert_eq!(ops.len(), 1);
1869 if let OperationSpec::Payment {
1870 destination,
1871 amount,
1872 asset,
1873 } = &ops[0]
1874 {
1875 assert_eq!(
1876 destination,
1877 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1878 );
1879 assert_eq!(amount, &1000000);
1880 assert_eq!(asset, &AssetSpec::Native);
1881 } else {
1882 panic!("Expected Payment operation");
1883 }
1884 } else {
1885 panic!("Expected Operations transaction input");
1886 }
1887 assert_eq!(
1888 stellar_data.memo,
1889 Some(MemoSpec::Text {
1890 value: "Test memo".to_string()
1891 })
1892 );
1893 assert_eq!(
1894 stellar_data.valid_until,
1895 Some("2024-12-31T23:59:59Z".to_string())
1896 );
1897 assert_eq!(stellar_data.signatures.len(), 0);
1898 assert_eq!(stellar_data.hash, None);
1899 assert_eq!(stellar_data.fee, None);
1900 assert_eq!(stellar_data.sequence_number, None);
1901 } else {
1902 panic!("Expected Stellar transaction data");
1903 }
1904 }
1905
1906 #[test]
1907 fn test_try_from_network_transaction_data_for_tx_eip1559() {
1908 let mut evm_tx_data = create_sample_evm_tx_data();
1910 evm_tx_data.max_fee_per_gas = Some(30_000_000_000);
1911 evm_tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1912 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1913
1914 let result = TxEip1559::try_from(network_data);
1916 assert!(result.is_ok());
1917 let tx_eip1559 = result.unwrap();
1918
1919 assert_eq!(tx_eip1559.chain_id, evm_tx_data.chain_id);
1921 assert_eq!(tx_eip1559.nonce, evm_tx_data.nonce.unwrap());
1922 assert_eq!(tx_eip1559.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1923 assert_eq!(
1924 tx_eip1559.max_fee_per_gas,
1925 evm_tx_data.max_fee_per_gas.unwrap()
1926 );
1927 assert_eq!(
1928 tx_eip1559.max_priority_fee_per_gas,
1929 evm_tx_data.max_priority_fee_per_gas.unwrap()
1930 );
1931 assert_eq!(tx_eip1559.value, evm_tx_data.value);
1932 assert!(tx_eip1559.access_list.0.is_empty());
1933
1934 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1936 transaction: Some("transaction_123".to_string()),
1937 ..Default::default()
1938 });
1939 assert!(TxEip1559::try_from(solana_data).is_err());
1940 }
1941
1942 #[test]
1943 fn test_evm_transaction_data_defaults() {
1944 let default_data = EvmTransactionData::default();
1945
1946 assert_eq!(
1947 default_data.from,
1948 "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
1949 );
1950 assert_eq!(
1951 default_data.to,
1952 Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string())
1953 );
1954 assert_eq!(default_data.gas_price, Some(20000000000));
1955 assert_eq!(default_data.value, U256::from(1000000000000000000u128));
1956 assert_eq!(default_data.data, Some("0x".to_string()));
1957 assert_eq!(default_data.nonce, Some(1));
1958 assert_eq!(default_data.chain_id, 1);
1959 assert_eq!(default_data.gas_limit, Some(21000));
1960 assert_eq!(default_data.hash, None);
1961 assert_eq!(default_data.signature, None);
1962 assert_eq!(default_data.speed, None);
1963 assert_eq!(default_data.max_fee_per_gas, None);
1964 assert_eq!(default_data.max_priority_fee_per_gas, None);
1965 assert_eq!(default_data.raw, None);
1966 }
1967
1968 #[test]
1969 fn test_transaction_repo_model_defaults() {
1970 let default_model = TransactionRepoModel::default();
1971
1972 assert_eq!(default_model.id, "00000000-0000-0000-0000-000000000001");
1973 assert_eq!(
1974 default_model.relayer_id,
1975 "00000000-0000-0000-0000-000000000002"
1976 );
1977 assert_eq!(default_model.status, TransactionStatus::Pending);
1978 assert_eq!(default_model.created_at, "2023-01-01T00:00:00Z");
1979 assert_eq!(default_model.status_reason, None);
1980 assert_eq!(default_model.sent_at, None);
1981 assert_eq!(default_model.confirmed_at, None);
1982 assert_eq!(default_model.valid_until, None);
1983 assert_eq!(default_model.delete_at, None);
1984 assert_eq!(default_model.network_type, NetworkType::Evm);
1985 assert_eq!(default_model.priced_at, None);
1986 assert_eq!(default_model.hashes.len(), 0);
1987 assert_eq!(default_model.noop_count, None);
1988 assert_eq!(default_model.is_canceled, Some(false));
1989 }
1990
1991 #[test]
1992 fn test_evm_tx_for_replacement_with_speed_fallback() {
1993 let mut old_data = create_sample_evm_tx_data();
1994 old_data.speed = Some(Speed::SafeLow);
1995
1996 let new_request = EvmTransactionRequest {
1998 to: Some("0xNewRecipient".to_string()),
1999 value: U256::from(2000000000000000000u64),
2000 data: Some("0xNewData".to_string()),
2001 gas_limit: Some(25000),
2002 gas_price: None,
2003 max_fee_per_gas: None,
2004 max_priority_fee_per_gas: None,
2005 speed: None,
2006 valid_until: None,
2007 };
2008
2009 let result = EvmTransactionData::for_replacement(&old_data, &new_request);
2010 assert_eq!(result.speed, Some(Speed::SafeLow));
2011
2012 let mut old_data_no_speed = create_sample_evm_tx_data();
2014 old_data_no_speed.speed = None;
2015
2016 let result2 = EvmTransactionData::for_replacement(&old_data_no_speed, &new_request);
2017 assert_eq!(result2.speed, Some(DEFAULT_TRANSACTION_SPEED));
2018 }
2019
2020 #[test]
2021 fn test_transaction_status_serialization() {
2022 use serde_json;
2023
2024 assert_eq!(
2026 serde_json::to_string(&TransactionStatus::Pending).unwrap(),
2027 "\"pending\""
2028 );
2029 assert_eq!(
2030 serde_json::to_string(&TransactionStatus::Sent).unwrap(),
2031 "\"sent\""
2032 );
2033 assert_eq!(
2034 serde_json::to_string(&TransactionStatus::Mined).unwrap(),
2035 "\"mined\""
2036 );
2037 assert_eq!(
2038 serde_json::to_string(&TransactionStatus::Failed).unwrap(),
2039 "\"failed\""
2040 );
2041 assert_eq!(
2042 serde_json::to_string(&TransactionStatus::Confirmed).unwrap(),
2043 "\"confirmed\""
2044 );
2045 assert_eq!(
2046 serde_json::to_string(&TransactionStatus::Canceled).unwrap(),
2047 "\"canceled\""
2048 );
2049 assert_eq!(
2050 serde_json::to_string(&TransactionStatus::Submitted).unwrap(),
2051 "\"submitted\""
2052 );
2053 assert_eq!(
2054 serde_json::to_string(&TransactionStatus::Expired).unwrap(),
2055 "\"expired\""
2056 );
2057 }
2058
2059 #[test]
2060 fn test_evm_tx_contract_creation() {
2061 let mut tx_data = create_sample_evm_tx_data();
2063 tx_data.to = None;
2064
2065 let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
2066 assert_eq!(tx_legacy.to, TxKind::Create);
2067
2068 let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
2069 assert_eq!(tx_eip1559.to, TxKind::Create);
2070 }
2071
2072 #[test]
2073 fn test_evm_tx_default_values_in_conversion() {
2074 let mut tx_data = create_sample_evm_tx_data();
2076 tx_data.nonce = None;
2077 tx_data.gas_price = None;
2078 tx_data.max_fee_per_gas = None;
2079 tx_data.max_priority_fee_per_gas = None;
2080
2081 let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
2082 assert_eq!(tx_legacy.nonce, 0); assert_eq!(tx_legacy.gas_price, 0); let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
2086 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); }
2090
2091 fn test_models() -> (NetworkRepoModel, RelayerRepoModel) {
2093 use crate::config::{NetworkConfigCommon, StellarNetworkConfig};
2094 use crate::constants::DEFAULT_STELLAR_MIN_BALANCE;
2095
2096 let network_config = NetworkConfigData::Stellar(StellarNetworkConfig {
2097 common: NetworkConfigCommon {
2098 network: "testnet".to_string(),
2099 from: None,
2100 rpc_urls: Some(vec!["https://test.stellar.org".to_string()]),
2101 explorer_urls: None,
2102 average_blocktime_ms: Some(5000), is_testnet: Some(true),
2104 tags: None,
2105 },
2106 passphrase: Some("Test SDF Network ; September 2015".to_string()),
2107 });
2108
2109 let network_model = NetworkRepoModel {
2110 id: "stellar:testnet".to_string(),
2111 name: "testnet".to_string(),
2112 network_type: NetworkType::Stellar,
2113 config: network_config,
2114 };
2115
2116 let relayer_model = RelayerRepoModel {
2117 id: "test-relayer".to_string(),
2118 name: "Test Relayer".to_string(),
2119 network: "stellar:testnet".to_string(),
2120 paused: false,
2121 network_type: NetworkType::Stellar,
2122 signer_id: "test-signer".to_string(),
2123 policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
2124 max_fee: None,
2125 timeout_seconds: None,
2126 min_balance: Some(DEFAULT_STELLAR_MIN_BALANCE),
2127 concurrent_transactions: None,
2128 }),
2129 address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2130 notification_id: None,
2131 system_disabled: false,
2132 custom_rpc_urls: None,
2133 ..Default::default()
2134 };
2135
2136 (network_model, relayer_model)
2137 }
2138
2139 #[test]
2140 fn test_stellar_transaction_data_serialization_roundtrip() {
2141 use crate::models::transaction::stellar::asset::AssetSpec;
2142 use crate::models::transaction::stellar::operation::OperationSpec;
2143 use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
2144
2145 let hint = SignatureHint([1, 2, 3, 4]);
2147 let sig_bytes: Vec<u8> = vec![5u8; 64];
2148 let sig_bytes_m: BytesM<64> = sig_bytes.try_into().unwrap();
2149 let dummy_signature = DecoratedSignature {
2150 hint,
2151 signature: Signature(sig_bytes_m),
2152 };
2153
2154 let original_data = StellarTransactionData {
2156 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2157 fee: Some(100),
2158 sequence_number: Some(12345),
2159 memo: None,
2160 valid_until: None,
2161 network_passphrase: "Test SDF Network ; September 2015".to_string(),
2162 signatures: vec![dummy_signature.clone()],
2163 hash: Some("test-hash".to_string()),
2164 simulation_transaction_data: Some("simulation-data".to_string()),
2165 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
2166 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2167 amount: 1000,
2168 asset: AssetSpec::Native,
2169 }]),
2170 signed_envelope_xdr: Some("signed-xdr-data".to_string()),
2171 };
2172
2173 let json = serde_json::to_string(&original_data).expect("Failed to serialize");
2175
2176 let deserialized_data: StellarTransactionData =
2178 serde_json::from_str(&json).expect("Failed to deserialize");
2179
2180 match (
2182 &original_data.transaction_input,
2183 &deserialized_data.transaction_input,
2184 ) {
2185 (TransactionInput::Operations(orig_ops), TransactionInput::Operations(deser_ops)) => {
2186 assert_eq!(orig_ops.len(), deser_ops.len());
2187 assert_eq!(orig_ops, deser_ops);
2188 }
2189 _ => panic!("Transaction input type mismatch"),
2190 }
2191
2192 assert_eq!(
2194 original_data.signatures.len(),
2195 deserialized_data.signatures.len()
2196 );
2197 assert_eq!(original_data.signatures, deserialized_data.signatures);
2198
2199 assert_eq!(
2201 original_data.source_account,
2202 deserialized_data.source_account
2203 );
2204 assert_eq!(original_data.fee, deserialized_data.fee);
2205 assert_eq!(
2206 original_data.sequence_number,
2207 deserialized_data.sequence_number
2208 );
2209 assert_eq!(
2210 original_data.network_passphrase,
2211 deserialized_data.network_passphrase
2212 );
2213 assert_eq!(original_data.hash, deserialized_data.hash);
2214 assert_eq!(
2215 original_data.simulation_transaction_data,
2216 deserialized_data.simulation_transaction_data
2217 );
2218 assert_eq!(
2219 original_data.signed_envelope_xdr,
2220 deserialized_data.signed_envelope_xdr
2221 );
2222 }
2223
2224 #[test]
2225 fn test_stellar_xdr_transaction_input_conversion() {
2226 let (network_model, relayer_model) = test_models();
2227
2228 let stellar_request = StellarTransactionRequest {
2230 source_account: Some(
2231 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2232 ),
2233 network: "testnet".to_string(),
2234 operations: Some(vec![OperationSpec::Payment {
2235 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2236 amount: 1000000,
2237 asset: AssetSpec::Native,
2238 }]),
2239 memo: None,
2240 valid_until: None,
2241 transaction_xdr: None,
2242 fee_bump: None,
2243 max_fee: None,
2244 };
2245
2246 let request = NetworkTransactionRequest::Stellar(stellar_request);
2247 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2248 assert!(result.is_ok());
2249
2250 let tx_model = result.unwrap();
2251 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2252 assert!(matches!(
2253 stellar_data.transaction_input,
2254 TransactionInput::Operations(_)
2255 ));
2256 } else {
2257 panic!("Expected Stellar transaction data");
2258 }
2259
2260 let unsigned_xdr = "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAGQAAHAkAAAADgAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=";
2263 let stellar_request = StellarTransactionRequest {
2264 source_account: None,
2265 network: "testnet".to_string(),
2266 operations: Some(vec![]),
2267 memo: None,
2268 valid_until: None,
2269 transaction_xdr: Some(unsigned_xdr.to_string()),
2270 fee_bump: None,
2271 max_fee: None,
2272 };
2273
2274 let request = NetworkTransactionRequest::Stellar(stellar_request);
2275 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2276 assert!(result.is_ok());
2277
2278 let tx_model = result.unwrap();
2279 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2280 assert!(matches!(
2281 stellar_data.transaction_input,
2282 TransactionInput::UnsignedXdr(_)
2283 ));
2284 } else {
2285 panic!("Expected Stellar transaction data");
2286 }
2287
2288 let signed_xdr = {
2291 use soroban_rs::xdr::{Limits, TransactionEnvelope, TransactionV1Envelope, WriteXdr};
2292 use stellar_strkey::ed25519::PublicKey;
2293
2294 let source_pk =
2296 PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
2297 .unwrap();
2298 let dest_pk =
2299 PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
2300 .unwrap();
2301
2302 let payment_op = soroban_rs::xdr::PaymentOp {
2303 destination: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2304 dest_pk.0,
2305 )),
2306 asset: soroban_rs::xdr::Asset::Native,
2307 amount: 1000000,
2308 };
2309
2310 let operation = soroban_rs::xdr::Operation {
2311 source_account: None,
2312 body: soroban_rs::xdr::OperationBody::Payment(payment_op),
2313 };
2314
2315 let operations: soroban_rs::xdr::VecM<soroban_rs::xdr::Operation, 100> =
2316 vec![operation].try_into().unwrap();
2317
2318 let tx = soroban_rs::xdr::Transaction {
2319 source_account: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2320 source_pk.0,
2321 )),
2322 fee: 100,
2323 seq_num: soroban_rs::xdr::SequenceNumber(1),
2324 cond: soroban_rs::xdr::Preconditions::None,
2325 memo: soroban_rs::xdr::Memo::None,
2326 operations,
2327 ext: soroban_rs::xdr::TransactionExt::V0,
2328 };
2329
2330 let hint = soroban_rs::xdr::SignatureHint([0; 4]);
2332 let sig_bytes: Vec<u8> = vec![0u8; 64];
2333 let sig_bytes_m: soroban_rs::xdr::BytesM<64> = sig_bytes.try_into().unwrap();
2334 let sig = soroban_rs::xdr::DecoratedSignature {
2335 hint,
2336 signature: soroban_rs::xdr::Signature(sig_bytes_m),
2337 };
2338
2339 let envelope = TransactionV1Envelope {
2340 tx,
2341 signatures: vec![sig].try_into().unwrap(),
2342 };
2343
2344 let tx_envelope = TransactionEnvelope::Tx(envelope);
2345 tx_envelope.to_xdr_base64(Limits::none()).unwrap()
2346 };
2347 let stellar_request = StellarTransactionRequest {
2348 source_account: None,
2349 network: "testnet".to_string(),
2350 operations: Some(vec![]),
2351 memo: None,
2352 valid_until: None,
2353 transaction_xdr: Some(signed_xdr.to_string()),
2354 fee_bump: Some(true),
2355 max_fee: Some(20000000),
2356 };
2357
2358 let request = NetworkTransactionRequest::Stellar(stellar_request);
2359 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2360 assert!(result.is_ok());
2361
2362 let tx_model = result.unwrap();
2363 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2364 match &stellar_data.transaction_input {
2365 TransactionInput::SignedXdr { xdr, max_fee } => {
2366 assert_eq!(xdr, &signed_xdr);
2367 assert_eq!(*max_fee, 20000000);
2368 }
2369 _ => panic!("Expected SignedXdr transaction input"),
2370 }
2371 } else {
2372 panic!("Expected Stellar transaction data");
2373 }
2374
2375 let stellar_request = StellarTransactionRequest {
2377 source_account: None,
2378 network: "testnet".to_string(),
2379 operations: Some(vec![]),
2380 memo: None,
2381 valid_until: None,
2382 transaction_xdr: Some(signed_xdr.clone()),
2383 fee_bump: None,
2384 max_fee: None,
2385 };
2386
2387 let request = NetworkTransactionRequest::Stellar(stellar_request);
2388 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2389 assert!(result.is_err());
2390 assert!(result
2391 .unwrap_err()
2392 .to_string()
2393 .contains("Expected unsigned XDR but received signed XDR"));
2394
2395 let stellar_request = StellarTransactionRequest {
2397 source_account: Some(
2398 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2399 ),
2400 network: "testnet".to_string(),
2401 operations: Some(vec![OperationSpec::Payment {
2402 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2403 amount: 1000000,
2404 asset: AssetSpec::Native,
2405 }]),
2406 memo: None,
2407 valid_until: None,
2408 transaction_xdr: None,
2409 fee_bump: Some(true),
2410 max_fee: None,
2411 };
2412
2413 let request = NetworkTransactionRequest::Stellar(stellar_request);
2414 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2415 assert!(result.is_err());
2416 assert!(result
2417 .unwrap_err()
2418 .to_string()
2419 .contains("Cannot request fee_bump with operations mode"));
2420 }
2421
2422 #[test]
2423 fn test_invoke_host_function_must_be_exclusive() {
2424 let (network_model, relayer_model) = test_models();
2425
2426 let stellar_request = StellarTransactionRequest {
2428 source_account: Some(
2429 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2430 ),
2431 network: "testnet".to_string(),
2432 operations: Some(vec![OperationSpec::InvokeContract {
2433 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2434 .to_string(),
2435 function_name: "transfer".to_string(),
2436 args: vec![],
2437 auth: None,
2438 }]),
2439 memo: None,
2440 valid_until: None,
2441 transaction_xdr: None,
2442 fee_bump: None,
2443 max_fee: None,
2444 };
2445
2446 let request = NetworkTransactionRequest::Stellar(stellar_request);
2447 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2448 assert!(result.is_ok(), "Single InvokeHostFunction should succeed");
2449
2450 let stellar_request = StellarTransactionRequest {
2452 source_account: Some(
2453 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2454 ),
2455 network: "testnet".to_string(),
2456 operations: Some(vec![
2457 OperationSpec::Payment {
2458 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2459 .to_string(),
2460 amount: 1000,
2461 asset: AssetSpec::Native,
2462 },
2463 OperationSpec::InvokeContract {
2464 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2465 .to_string(),
2466 function_name: "transfer".to_string(),
2467 args: vec![],
2468 auth: None,
2469 },
2470 ]),
2471 memo: None,
2472 valid_until: None,
2473 transaction_xdr: None,
2474 fee_bump: None,
2475 max_fee: None,
2476 };
2477
2478 let request = NetworkTransactionRequest::Stellar(stellar_request);
2479 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2480
2481 match result {
2482 Ok(_) => panic!("Expected Soroban operation mixed with Payment to fail"),
2483 Err(err) => {
2484 let err_str = err.to_string();
2485 assert!(
2486 err_str.contains("Soroban operations must be exclusive"),
2487 "Expected error about Soroban operation exclusivity, got: {}",
2488 err_str
2489 );
2490 }
2491 }
2492
2493 let stellar_request = StellarTransactionRequest {
2495 source_account: Some(
2496 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2497 ),
2498 network: "testnet".to_string(),
2499 operations: Some(vec![
2500 OperationSpec::InvokeContract {
2501 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2502 .to_string(),
2503 function_name: "transfer".to_string(),
2504 args: vec![],
2505 auth: None,
2506 },
2507 OperationSpec::InvokeContract {
2508 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2509 .to_string(),
2510 function_name: "approve".to_string(),
2511 args: vec![],
2512 auth: None,
2513 },
2514 ]),
2515 memo: None,
2516 valid_until: None,
2517 transaction_xdr: None,
2518 fee_bump: None,
2519 max_fee: None,
2520 };
2521
2522 let request = NetworkTransactionRequest::Stellar(stellar_request);
2523 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2524
2525 match result {
2526 Ok(_) => panic!("Expected multiple Soroban operations to fail"),
2527 Err(err) => {
2528 let err_str = err.to_string();
2529 assert!(
2530 err_str.contains("Transaction can contain at most one Soroban operation"),
2531 "Expected error about multiple Soroban operations, got: {}",
2532 err_str
2533 );
2534 }
2535 }
2536
2537 let stellar_request = StellarTransactionRequest {
2539 source_account: Some(
2540 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2541 ),
2542 network: "testnet".to_string(),
2543 operations: Some(vec![
2544 OperationSpec::Payment {
2545 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2546 .to_string(),
2547 amount: 1000,
2548 asset: AssetSpec::Native,
2549 },
2550 OperationSpec::Payment {
2551 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
2552 .to_string(),
2553 amount: 2000,
2554 asset: AssetSpec::Native,
2555 },
2556 ]),
2557 memo: None,
2558 valid_until: None,
2559 transaction_xdr: None,
2560 fee_bump: None,
2561 max_fee: None,
2562 };
2563
2564 let request = NetworkTransactionRequest::Stellar(stellar_request);
2565 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2566 assert!(result.is_ok(), "Multiple Payment operations should succeed");
2567
2568 let stellar_request = StellarTransactionRequest {
2570 source_account: Some(
2571 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2572 ),
2573 network: "testnet".to_string(),
2574 operations: Some(vec![OperationSpec::InvokeContract {
2575 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2576 .to_string(),
2577 function_name: "transfer".to_string(),
2578 args: vec![],
2579 auth: None,
2580 }]),
2581 memo: Some(MemoSpec::Text {
2582 value: "This should fail".to_string(),
2583 }),
2584 valid_until: None,
2585 transaction_xdr: None,
2586 fee_bump: None,
2587 max_fee: None,
2588 };
2589
2590 let request = NetworkTransactionRequest::Stellar(stellar_request);
2591 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2592
2593 match result {
2594 Ok(_) => panic!("Expected InvokeHostFunction with non-None memo to fail"),
2595 Err(err) => {
2596 let err_str = err.to_string();
2597 assert!(
2598 err_str.contains("Soroban operations cannot have a memo"),
2599 "Expected error about memo restriction, got: {}",
2600 err_str
2601 );
2602 }
2603 }
2604
2605 let stellar_request = StellarTransactionRequest {
2607 source_account: Some(
2608 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2609 ),
2610 network: "testnet".to_string(),
2611 operations: Some(vec![OperationSpec::InvokeContract {
2612 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2613 .to_string(),
2614 function_name: "transfer".to_string(),
2615 args: vec![],
2616 auth: None,
2617 }]),
2618 memo: Some(MemoSpec::None),
2619 valid_until: None,
2620 transaction_xdr: None,
2621 fee_bump: None,
2622 max_fee: None,
2623 };
2624
2625 let request = NetworkTransactionRequest::Stellar(stellar_request);
2626 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2627 assert!(
2628 result.is_ok(),
2629 "InvokeHostFunction with MemoSpec::None should succeed"
2630 );
2631
2632 let stellar_request = StellarTransactionRequest {
2634 source_account: Some(
2635 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2636 ),
2637 network: "testnet".to_string(),
2638 operations: Some(vec![OperationSpec::InvokeContract {
2639 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2640 .to_string(),
2641 function_name: "transfer".to_string(),
2642 args: vec![],
2643 auth: None,
2644 }]),
2645 memo: None,
2646 valid_until: None,
2647 transaction_xdr: None,
2648 fee_bump: None,
2649 max_fee: None,
2650 };
2651
2652 let request = NetworkTransactionRequest::Stellar(stellar_request);
2653 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2654 assert!(
2655 result.is_ok(),
2656 "InvokeHostFunction with no memo should succeed"
2657 );
2658
2659 let stellar_request = StellarTransactionRequest {
2661 source_account: Some(
2662 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2663 ),
2664 network: "testnet".to_string(),
2665 operations: Some(vec![OperationSpec::Payment {
2666 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2667 amount: 1000,
2668 asset: AssetSpec::Native,
2669 }]),
2670 memo: Some(MemoSpec::Text {
2671 value: "Payment memo is allowed".to_string(),
2672 }),
2673 valid_until: None,
2674 transaction_xdr: None,
2675 fee_bump: None,
2676 max_fee: 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(), "Payment operation with memo should succeed");
2682 }
2683
2684 #[test]
2685 fn test_update_delete_at_if_final_status_does_not_update_when_delete_at_already_set() {
2686 let _lock = match ENV_MUTEX.lock() {
2687 Ok(guard) => guard,
2688 Err(poisoned) => poisoned.into_inner(),
2689 };
2690
2691 use std::env;
2692
2693 env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2695
2696 let mut transaction = create_test_transaction();
2697 transaction.delete_at = Some("2024-01-01T00:00:00Z".to_string());
2698 transaction.status = TransactionStatus::Confirmed; let original_delete_at = transaction.delete_at.clone();
2701
2702 transaction.update_delete_at_if_final_status();
2703
2704 assert_eq!(transaction.delete_at, original_delete_at);
2706
2707 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2709 }
2710
2711 #[test]
2712 fn test_update_delete_at_if_final_status_does_not_update_when_status_not_final() {
2713 let _lock = match ENV_MUTEX.lock() {
2714 Ok(guard) => guard,
2715 Err(poisoned) => poisoned.into_inner(),
2716 };
2717
2718 use std::env;
2719
2720 env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2722
2723 let mut transaction = create_test_transaction();
2724 transaction.delete_at = None;
2725 transaction.status = TransactionStatus::Pending; transaction.update_delete_at_if_final_status();
2728
2729 assert!(transaction.delete_at.is_none());
2731
2732 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2734 }
2735
2736 #[test]
2737 fn test_update_delete_at_if_final_status_sets_delete_at_for_final_statuses() {
2738 let _lock = match ENV_MUTEX.lock() {
2739 Ok(guard) => guard,
2740 Err(poisoned) => poisoned.into_inner(),
2741 };
2742
2743 use crate::config::ServerConfig;
2744 use chrono::{DateTime, Duration, Utc};
2745 use std::env;
2746
2747 env::set_var("TRANSACTION_EXPIRATION_HOURS", "3"); let actual_hours = ServerConfig::get_transaction_expiration_hours();
2752 assert_eq!(
2753 actual_hours, 3,
2754 "Environment variable should be set to 3 hours"
2755 );
2756
2757 let final_statuses = vec![
2758 TransactionStatus::Canceled,
2759 TransactionStatus::Confirmed,
2760 TransactionStatus::Failed,
2761 TransactionStatus::Expired,
2762 ];
2763
2764 for status in final_statuses {
2765 let mut transaction = create_test_transaction();
2766 transaction.delete_at = None;
2767 transaction.status = status.clone();
2768
2769 let before_update = Utc::now();
2770 transaction.update_delete_at_if_final_status();
2771
2772 assert!(
2774 transaction.delete_at.is_some(),
2775 "delete_at should be set for status: {:?}",
2776 status
2777 );
2778
2779 let delete_at_str = transaction.delete_at.unwrap();
2781 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2782 .expect("delete_at should be valid RFC3339")
2783 .with_timezone(&Utc);
2784
2785 let duration_from_before = delete_at.signed_duration_since(before_update);
2787 let expected_duration = Duration::hours(3);
2788 let tolerance = Duration::minutes(5); let actual_hours_at_runtime = ServerConfig::get_transaction_expiration_hours();
2792
2793 assert!(
2794 duration_from_before >= expected_duration - tolerance &&
2795 duration_from_before <= expected_duration + tolerance,
2796 "delete_at should be approximately 3 hours from now for status: {:?}. Duration from start: {:?}, Expected: {:?}, Config hours at runtime: {}",
2797 status, duration_from_before, expected_duration, actual_hours_at_runtime
2798 );
2799 }
2800
2801 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2803 }
2804
2805 #[test]
2806 fn test_update_delete_at_if_final_status_uses_default_expiration_hours() {
2807 let _lock = match ENV_MUTEX.lock() {
2808 Ok(guard) => guard,
2809 Err(poisoned) => poisoned.into_inner(),
2810 };
2811
2812 use chrono::{DateTime, Duration, Utc};
2813 use std::env;
2814
2815 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2817
2818 let mut transaction = create_test_transaction();
2819 transaction.delete_at = None;
2820 transaction.status = TransactionStatus::Confirmed;
2821
2822 let before_update = Utc::now();
2823 transaction.update_delete_at_if_final_status();
2824
2825 assert!(transaction.delete_at.is_some());
2827
2828 let delete_at_str = transaction.delete_at.unwrap();
2829 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2830 .expect("delete_at should be valid RFC3339")
2831 .with_timezone(&Utc);
2832
2833 let duration_from_before = delete_at.signed_duration_since(before_update);
2835 let expected_duration = Duration::hours(4);
2836 let tolerance = Duration::minutes(5); assert!(
2839 duration_from_before >= expected_duration - tolerance &&
2840 duration_from_before <= expected_duration + tolerance,
2841 "delete_at should be approximately 4 hours from now (default). Duration from start: {:?}, Expected: {:?}",
2842 duration_from_before, expected_duration
2843 );
2844 }
2845
2846 #[test]
2847 fn test_update_delete_at_if_final_status_with_custom_expiration_hours() {
2848 let _lock = match ENV_MUTEX.lock() {
2849 Ok(guard) => guard,
2850 Err(poisoned) => poisoned.into_inner(),
2851 };
2852
2853 use chrono::{DateTime, Duration, Utc};
2854 use std::env;
2855
2856 let test_cases = vec![1, 2, 6, 12]; for expiration_hours in test_cases {
2860 env::set_var("TRANSACTION_EXPIRATION_HOURS", expiration_hours.to_string());
2861
2862 let mut transaction = create_test_transaction();
2863 transaction.delete_at = None;
2864 transaction.status = TransactionStatus::Failed;
2865
2866 let before_update = Utc::now();
2867 transaction.update_delete_at_if_final_status();
2868
2869 assert!(
2870 transaction.delete_at.is_some(),
2871 "delete_at should be set for {} hours",
2872 expiration_hours
2873 );
2874
2875 let delete_at_str = transaction.delete_at.unwrap();
2876 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2877 .expect("delete_at should be valid RFC3339")
2878 .with_timezone(&Utc);
2879
2880 let duration_from_before = delete_at.signed_duration_since(before_update);
2881 let expected_duration = Duration::hours(expiration_hours as i64);
2882 let tolerance = Duration::minutes(5); assert!(
2885 duration_from_before >= expected_duration - tolerance &&
2886 duration_from_before <= expected_duration + tolerance,
2887 "delete_at should be approximately {} hours from now. Duration from start: {:?}, Expected: {:?}",
2888 expiration_hours, duration_from_before, expected_duration
2889 );
2890 }
2891
2892 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2894 }
2895
2896 #[test]
2897 fn test_calculate_delete_at_with_various_hours() {
2898 use chrono::{DateTime, Utc};
2899
2900 let test_cases = vec![0, 1, 6, 12, 24, 48];
2901
2902 for hours in test_cases {
2903 let before_calc = Utc::now();
2904 let result = TransactionRepoModel::calculate_delete_at(hours);
2905 let after_calc = Utc::now();
2906
2907 assert!(
2908 result.is_some(),
2909 "calculate_delete_at should return Some for {} hours",
2910 hours
2911 );
2912
2913 let delete_at_str = result.unwrap();
2914 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2915 .expect("Result should be valid RFC3339")
2916 .with_timezone(&Utc);
2917
2918 let expected_min =
2919 before_calc + chrono::Duration::hours(hours as i64) - chrono::Duration::seconds(1);
2920 let expected_max =
2921 after_calc + chrono::Duration::hours(hours as i64) + chrono::Duration::seconds(1);
2922
2923 assert!(
2924 delete_at >= expected_min && delete_at <= expected_max,
2925 "Calculated delete_at should be approximately {} hours from now. Got: {}, Expected between: {} and {}",
2926 hours, delete_at, expected_min, expected_max
2927 );
2928 }
2929 }
2930
2931 #[test]
2932 fn test_update_delete_at_if_final_status_idempotent() {
2933 let _lock = match ENV_MUTEX.lock() {
2934 Ok(guard) => guard,
2935 Err(poisoned) => poisoned.into_inner(),
2936 };
2937
2938 use std::env;
2939
2940 env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
2941
2942 let mut transaction = create_test_transaction();
2943 transaction.delete_at = None;
2944 transaction.status = TransactionStatus::Confirmed;
2945
2946 transaction.update_delete_at_if_final_status();
2948 let first_delete_at = transaction.delete_at.clone();
2949 assert!(first_delete_at.is_some());
2950
2951 transaction.update_delete_at_if_final_status();
2953 assert_eq!(transaction.delete_at, first_delete_at);
2954
2955 transaction.update_delete_at_if_final_status();
2957 assert_eq!(transaction.delete_at, first_delete_at);
2958
2959 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2961 }
2962
2963 fn create_test_transaction() -> TransactionRepoModel {
2965 TransactionRepoModel {
2966 id: "test-transaction-id".to_string(),
2967 relayer_id: "test-relayer-id".to_string(),
2968 status: TransactionStatus::Pending,
2969 status_reason: None,
2970 created_at: "2024-01-01T00:00:00Z".to_string(),
2971 sent_at: None,
2972 confirmed_at: None,
2973 valid_until: None,
2974 delete_at: None,
2975 network_data: NetworkTransactionData::Evm(EvmTransactionData {
2976 gas_price: None,
2977 gas_limit: Some(21000),
2978 nonce: Some(0),
2979 value: U256::from(0),
2980 data: None,
2981 from: "0x1234567890123456789012345678901234567890".to_string(),
2982 to: Some("0x0987654321098765432109876543210987654321".to_string()),
2983 chain_id: 1,
2984 hash: None,
2985 signature: None,
2986 speed: None,
2987 max_fee_per_gas: None,
2988 max_priority_fee_per_gas: None,
2989 raw: None,
2990 }),
2991 priced_at: None,
2992 hashes: vec![],
2993 network_type: NetworkType::Evm,
2994 noop_count: None,
2995 is_canceled: None,
2996 }
2997 }
2998
2999 #[test]
3000 fn test_apply_partial_update() {
3001 let mut transaction = create_test_transaction();
3003
3004 let update = TransactionUpdateRequest {
3006 status: Some(TransactionStatus::Confirmed),
3007 status_reason: Some("Transaction confirmed".to_string()),
3008 sent_at: Some("2023-01-01T12:00:00Z".to_string()),
3009 confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
3010 hashes: Some(vec!["0x123".to_string(), "0x456".to_string()]),
3011 is_canceled: Some(false),
3012 ..Default::default()
3013 };
3014
3015 transaction.apply_partial_update(update);
3017
3018 assert_eq!(transaction.status, TransactionStatus::Confirmed);
3020 assert_eq!(
3021 transaction.status_reason,
3022 Some("Transaction confirmed".to_string())
3023 );
3024 assert_eq!(
3025 transaction.sent_at,
3026 Some("2023-01-01T12:00:00Z".to_string())
3027 );
3028 assert_eq!(
3029 transaction.confirmed_at,
3030 Some("2023-01-01T12:05:00Z".to_string())
3031 );
3032 assert_eq!(
3033 transaction.hashes,
3034 vec!["0x123".to_string(), "0x456".to_string()]
3035 );
3036 assert_eq!(transaction.is_canceled, Some(false));
3037
3038 assert!(transaction.delete_at.is_some());
3040 }
3041
3042 #[test]
3043 fn test_apply_partial_update_preserves_unchanged_fields() {
3044 let mut transaction = TransactionRepoModel {
3046 id: "test-tx".to_string(),
3047 relayer_id: "test-relayer".to_string(),
3048 status: TransactionStatus::Pending,
3049 status_reason: Some("Initial reason".to_string()),
3050 created_at: Utc::now().to_rfc3339(),
3051 sent_at: Some("2023-01-01T10:00:00Z".to_string()),
3052 confirmed_at: None,
3053 valid_until: None,
3054 delete_at: None,
3055 network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
3056 priced_at: None,
3057 hashes: vec!["0xoriginal".to_string()],
3058 network_type: NetworkType::Evm,
3059 noop_count: Some(5),
3060 is_canceled: Some(true),
3061 };
3062
3063 let update = TransactionUpdateRequest {
3065 status: Some(TransactionStatus::Sent),
3066 ..Default::default()
3067 };
3068
3069 transaction.apply_partial_update(update);
3071
3072 assert_eq!(transaction.status, TransactionStatus::Sent);
3074 assert_eq!(
3075 transaction.status_reason,
3076 Some("Initial reason".to_string())
3077 );
3078 assert_eq!(
3079 transaction.sent_at,
3080 Some("2023-01-01T10:00:00Z".to_string())
3081 );
3082 assert_eq!(transaction.confirmed_at, None);
3083 assert_eq!(transaction.hashes, vec!["0xoriginal".to_string()]);
3084 assert_eq!(transaction.noop_count, Some(5));
3085 assert_eq!(transaction.is_canceled, Some(true));
3086
3087 assert!(transaction.delete_at.is_none());
3089 }
3090
3091 #[test]
3092 fn test_apply_partial_update_empty_update() {
3093 let mut transaction = create_test_transaction();
3095 let original_transaction = transaction.clone();
3096
3097 let update = TransactionUpdateRequest::default();
3099 transaction.apply_partial_update(update);
3100
3101 assert_eq!(transaction.id, original_transaction.id);
3103 assert_eq!(transaction.status, original_transaction.status);
3104 assert_eq!(
3105 transaction.status_reason,
3106 original_transaction.status_reason
3107 );
3108 assert_eq!(transaction.sent_at, original_transaction.sent_at);
3109 assert_eq!(transaction.confirmed_at, original_transaction.confirmed_at);
3110 assert_eq!(transaction.hashes, original_transaction.hashes);
3111 assert_eq!(transaction.noop_count, original_transaction.noop_count);
3112 assert_eq!(transaction.is_canceled, original_transaction.is_canceled);
3113 assert_eq!(transaction.delete_at, original_transaction.delete_at);
3114 }
3115}