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