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, StellarFeePaymentStrategy,
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 horizon_url: Some("https://horizon.stellar.org".to_string()),
1849 }),
1850 };
1851
1852 let result =
1853 TransactionRepoModel::try_from((&stellar_request, &relayer_model, &network_model));
1854 assert!(result.is_ok());
1855 let transaction = result.unwrap();
1856
1857 assert_eq!(transaction.relayer_id, relayer_model.id);
1858 assert_eq!(transaction.status, TransactionStatus::Pending);
1859 assert_eq!(transaction.network_type, NetworkType::Stellar);
1860 assert_eq!(transaction.valid_until, None);
1861
1862 if let NetworkTransactionData::Stellar(stellar_data) = transaction.network_data {
1863 assert_eq!(
1864 stellar_data.source_account,
1865 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1866 );
1867 if let TransactionInput::Operations(ops) = &stellar_data.transaction_input {
1869 assert_eq!(ops.len(), 1);
1870 if let OperationSpec::Payment {
1871 destination,
1872 amount,
1873 asset,
1874 } = &ops[0]
1875 {
1876 assert_eq!(
1877 destination,
1878 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1879 );
1880 assert_eq!(amount, &1000000);
1881 assert_eq!(asset, &AssetSpec::Native);
1882 } else {
1883 panic!("Expected Payment operation");
1884 }
1885 } else {
1886 panic!("Expected Operations transaction input");
1887 }
1888 assert_eq!(
1889 stellar_data.memo,
1890 Some(MemoSpec::Text {
1891 value: "Test memo".to_string()
1892 })
1893 );
1894 assert_eq!(
1895 stellar_data.valid_until,
1896 Some("2024-12-31T23:59:59Z".to_string())
1897 );
1898 assert_eq!(stellar_data.signatures.len(), 0);
1899 assert_eq!(stellar_data.hash, None);
1900 assert_eq!(stellar_data.fee, None);
1901 assert_eq!(stellar_data.sequence_number, None);
1902 } else {
1903 panic!("Expected Stellar transaction data");
1904 }
1905 }
1906
1907 #[test]
1908 fn test_try_from_network_transaction_data_for_tx_eip1559() {
1909 let mut evm_tx_data = create_sample_evm_tx_data();
1911 evm_tx_data.max_fee_per_gas = Some(30_000_000_000);
1912 evm_tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1913 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1914
1915 let result = TxEip1559::try_from(network_data);
1917 assert!(result.is_ok());
1918 let tx_eip1559 = result.unwrap();
1919
1920 assert_eq!(tx_eip1559.chain_id, evm_tx_data.chain_id);
1922 assert_eq!(tx_eip1559.nonce, evm_tx_data.nonce.unwrap());
1923 assert_eq!(tx_eip1559.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1924 assert_eq!(
1925 tx_eip1559.max_fee_per_gas,
1926 evm_tx_data.max_fee_per_gas.unwrap()
1927 );
1928 assert_eq!(
1929 tx_eip1559.max_priority_fee_per_gas,
1930 evm_tx_data.max_priority_fee_per_gas.unwrap()
1931 );
1932 assert_eq!(tx_eip1559.value, evm_tx_data.value);
1933 assert!(tx_eip1559.access_list.0.is_empty());
1934
1935 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1937 transaction: Some("transaction_123".to_string()),
1938 ..Default::default()
1939 });
1940 assert!(TxEip1559::try_from(solana_data).is_err());
1941 }
1942
1943 #[test]
1944 fn test_evm_transaction_data_defaults() {
1945 let default_data = EvmTransactionData::default();
1946
1947 assert_eq!(
1948 default_data.from,
1949 "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
1950 );
1951 assert_eq!(
1952 default_data.to,
1953 Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string())
1954 );
1955 assert_eq!(default_data.gas_price, Some(20000000000));
1956 assert_eq!(default_data.value, U256::from(1000000000000000000u128));
1957 assert_eq!(default_data.data, Some("0x".to_string()));
1958 assert_eq!(default_data.nonce, Some(1));
1959 assert_eq!(default_data.chain_id, 1);
1960 assert_eq!(default_data.gas_limit, Some(21000));
1961 assert_eq!(default_data.hash, None);
1962 assert_eq!(default_data.signature, None);
1963 assert_eq!(default_data.speed, None);
1964 assert_eq!(default_data.max_fee_per_gas, None);
1965 assert_eq!(default_data.max_priority_fee_per_gas, None);
1966 assert_eq!(default_data.raw, None);
1967 }
1968
1969 #[test]
1970 fn test_transaction_repo_model_defaults() {
1971 let default_model = TransactionRepoModel::default();
1972
1973 assert_eq!(default_model.id, "00000000-0000-0000-0000-000000000001");
1974 assert_eq!(
1975 default_model.relayer_id,
1976 "00000000-0000-0000-0000-000000000002"
1977 );
1978 assert_eq!(default_model.status, TransactionStatus::Pending);
1979 assert_eq!(default_model.created_at, "2023-01-01T00:00:00Z");
1980 assert_eq!(default_model.status_reason, None);
1981 assert_eq!(default_model.sent_at, None);
1982 assert_eq!(default_model.confirmed_at, None);
1983 assert_eq!(default_model.valid_until, None);
1984 assert_eq!(default_model.delete_at, None);
1985 assert_eq!(default_model.network_type, NetworkType::Evm);
1986 assert_eq!(default_model.priced_at, None);
1987 assert_eq!(default_model.hashes.len(), 0);
1988 assert_eq!(default_model.noop_count, None);
1989 assert_eq!(default_model.is_canceled, Some(false));
1990 }
1991
1992 #[test]
1993 fn test_evm_tx_for_replacement_with_speed_fallback() {
1994 let mut old_data = create_sample_evm_tx_data();
1995 old_data.speed = Some(Speed::SafeLow);
1996
1997 let new_request = EvmTransactionRequest {
1999 to: Some("0xNewRecipient".to_string()),
2000 value: U256::from(2000000000000000000u64),
2001 data: Some("0xNewData".to_string()),
2002 gas_limit: Some(25000),
2003 gas_price: None,
2004 max_fee_per_gas: None,
2005 max_priority_fee_per_gas: None,
2006 speed: None,
2007 valid_until: None,
2008 };
2009
2010 let result = EvmTransactionData::for_replacement(&old_data, &new_request);
2011 assert_eq!(result.speed, Some(Speed::SafeLow));
2012
2013 let mut old_data_no_speed = create_sample_evm_tx_data();
2015 old_data_no_speed.speed = None;
2016
2017 let result2 = EvmTransactionData::for_replacement(&old_data_no_speed, &new_request);
2018 assert_eq!(result2.speed, Some(DEFAULT_TRANSACTION_SPEED));
2019 }
2020
2021 #[test]
2022 fn test_transaction_status_serialization() {
2023 use serde_json;
2024
2025 assert_eq!(
2027 serde_json::to_string(&TransactionStatus::Pending).unwrap(),
2028 "\"pending\""
2029 );
2030 assert_eq!(
2031 serde_json::to_string(&TransactionStatus::Sent).unwrap(),
2032 "\"sent\""
2033 );
2034 assert_eq!(
2035 serde_json::to_string(&TransactionStatus::Mined).unwrap(),
2036 "\"mined\""
2037 );
2038 assert_eq!(
2039 serde_json::to_string(&TransactionStatus::Failed).unwrap(),
2040 "\"failed\""
2041 );
2042 assert_eq!(
2043 serde_json::to_string(&TransactionStatus::Confirmed).unwrap(),
2044 "\"confirmed\""
2045 );
2046 assert_eq!(
2047 serde_json::to_string(&TransactionStatus::Canceled).unwrap(),
2048 "\"canceled\""
2049 );
2050 assert_eq!(
2051 serde_json::to_string(&TransactionStatus::Submitted).unwrap(),
2052 "\"submitted\""
2053 );
2054 assert_eq!(
2055 serde_json::to_string(&TransactionStatus::Expired).unwrap(),
2056 "\"expired\""
2057 );
2058 }
2059
2060 #[test]
2061 fn test_evm_tx_contract_creation() {
2062 let mut tx_data = create_sample_evm_tx_data();
2064 tx_data.to = None;
2065
2066 let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
2067 assert_eq!(tx_legacy.to, TxKind::Create);
2068
2069 let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
2070 assert_eq!(tx_eip1559.to, TxKind::Create);
2071 }
2072
2073 #[test]
2074 fn test_evm_tx_default_values_in_conversion() {
2075 let mut tx_data = create_sample_evm_tx_data();
2077 tx_data.nonce = None;
2078 tx_data.gas_price = None;
2079 tx_data.max_fee_per_gas = None;
2080 tx_data.max_priority_fee_per_gas = None;
2081
2082 let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
2083 assert_eq!(tx_legacy.nonce, 0); assert_eq!(tx_legacy.gas_price, 0); let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
2087 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); }
2091
2092 fn test_models() -> (NetworkRepoModel, RelayerRepoModel) {
2094 use crate::config::{NetworkConfigCommon, StellarNetworkConfig};
2095 use crate::constants::DEFAULT_STELLAR_MIN_BALANCE;
2096
2097 let network_config = NetworkConfigData::Stellar(StellarNetworkConfig {
2098 common: NetworkConfigCommon {
2099 network: "testnet".to_string(),
2100 from: None,
2101 rpc_urls: Some(vec!["https://test.stellar.org".to_string()]),
2102 explorer_urls: None,
2103 average_blocktime_ms: Some(5000), is_testnet: Some(true),
2105 tags: None,
2106 },
2107 passphrase: Some("Test SDF Network ; September 2015".to_string()),
2108 horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
2109 });
2110
2111 let network_model = NetworkRepoModel {
2112 id: "stellar:testnet".to_string(),
2113 name: "testnet".to_string(),
2114 network_type: NetworkType::Stellar,
2115 config: network_config,
2116 };
2117
2118 let relayer_model = RelayerRepoModel {
2119 id: "test-relayer".to_string(),
2120 name: "Test Relayer".to_string(),
2121 network: "stellar:testnet".to_string(),
2122 paused: false,
2123 network_type: NetworkType::Stellar,
2124 signer_id: "test-signer".to_string(),
2125 policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
2126 max_fee: None,
2127 timeout_seconds: None,
2128 min_balance: Some(DEFAULT_STELLAR_MIN_BALANCE),
2129 concurrent_transactions: None,
2130 allowed_tokens: None,
2131 fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
2132 slippage_percentage: None,
2133 fee_margin_percentage: None,
2134 swap_config: None,
2135 }),
2136 address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2137 notification_id: None,
2138 system_disabled: false,
2139 custom_rpc_urls: None,
2140 ..Default::default()
2141 };
2142
2143 (network_model, relayer_model)
2144 }
2145
2146 #[test]
2147 fn test_stellar_transaction_data_serialization_roundtrip() {
2148 use crate::models::transaction::stellar::asset::AssetSpec;
2149 use crate::models::transaction::stellar::operation::OperationSpec;
2150 use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
2151
2152 let hint = SignatureHint([1, 2, 3, 4]);
2154 let sig_bytes: Vec<u8> = vec![5u8; 64];
2155 let sig_bytes_m: BytesM<64> = sig_bytes.try_into().unwrap();
2156 let dummy_signature = DecoratedSignature {
2157 hint,
2158 signature: Signature(sig_bytes_m),
2159 };
2160
2161 let original_data = StellarTransactionData {
2163 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2164 fee: Some(100),
2165 sequence_number: Some(12345),
2166 memo: None,
2167 valid_until: None,
2168 network_passphrase: "Test SDF Network ; September 2015".to_string(),
2169 signatures: vec![dummy_signature.clone()],
2170 hash: Some("test-hash".to_string()),
2171 simulation_transaction_data: Some("simulation-data".to_string()),
2172 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
2173 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2174 amount: 1000,
2175 asset: AssetSpec::Native,
2176 }]),
2177 signed_envelope_xdr: Some("signed-xdr-data".to_string()),
2178 };
2179
2180 let json = serde_json::to_string(&original_data).expect("Failed to serialize");
2182
2183 let deserialized_data: StellarTransactionData =
2185 serde_json::from_str(&json).expect("Failed to deserialize");
2186
2187 match (
2189 &original_data.transaction_input,
2190 &deserialized_data.transaction_input,
2191 ) {
2192 (TransactionInput::Operations(orig_ops), TransactionInput::Operations(deser_ops)) => {
2193 assert_eq!(orig_ops.len(), deser_ops.len());
2194 assert_eq!(orig_ops, deser_ops);
2195 }
2196 _ => panic!("Transaction input type mismatch"),
2197 }
2198
2199 assert_eq!(
2201 original_data.signatures.len(),
2202 deserialized_data.signatures.len()
2203 );
2204 assert_eq!(original_data.signatures, deserialized_data.signatures);
2205
2206 assert_eq!(
2208 original_data.source_account,
2209 deserialized_data.source_account
2210 );
2211 assert_eq!(original_data.fee, deserialized_data.fee);
2212 assert_eq!(
2213 original_data.sequence_number,
2214 deserialized_data.sequence_number
2215 );
2216 assert_eq!(
2217 original_data.network_passphrase,
2218 deserialized_data.network_passphrase
2219 );
2220 assert_eq!(original_data.hash, deserialized_data.hash);
2221 assert_eq!(
2222 original_data.simulation_transaction_data,
2223 deserialized_data.simulation_transaction_data
2224 );
2225 assert_eq!(
2226 original_data.signed_envelope_xdr,
2227 deserialized_data.signed_envelope_xdr
2228 );
2229 }
2230
2231 #[test]
2232 fn test_stellar_xdr_transaction_input_conversion() {
2233 let (network_model, relayer_model) = test_models();
2234
2235 let stellar_request = StellarTransactionRequest {
2237 source_account: Some(
2238 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2239 ),
2240 network: "testnet".to_string(),
2241 operations: Some(vec![OperationSpec::Payment {
2242 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2243 amount: 1000000,
2244 asset: AssetSpec::Native,
2245 }]),
2246 memo: None,
2247 valid_until: None,
2248 transaction_xdr: None,
2249 fee_bump: None,
2250 max_fee: None,
2251 };
2252
2253 let request = NetworkTransactionRequest::Stellar(stellar_request);
2254 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2255 assert!(result.is_ok());
2256
2257 let tx_model = result.unwrap();
2258 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2259 assert!(matches!(
2260 stellar_data.transaction_input,
2261 TransactionInput::Operations(_)
2262 ));
2263 } else {
2264 panic!("Expected Stellar transaction data");
2265 }
2266
2267 let unsigned_xdr = "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAGQAAHAkAAAADgAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=";
2270 let stellar_request = StellarTransactionRequest {
2271 source_account: None,
2272 network: "testnet".to_string(),
2273 operations: Some(vec![]),
2274 memo: None,
2275 valid_until: None,
2276 transaction_xdr: Some(unsigned_xdr.to_string()),
2277 fee_bump: None,
2278 max_fee: None,
2279 };
2280
2281 let request = NetworkTransactionRequest::Stellar(stellar_request);
2282 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2283 assert!(result.is_ok());
2284
2285 let tx_model = result.unwrap();
2286 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2287 assert!(matches!(
2288 stellar_data.transaction_input,
2289 TransactionInput::UnsignedXdr(_)
2290 ));
2291 } else {
2292 panic!("Expected Stellar transaction data");
2293 }
2294
2295 let signed_xdr = {
2298 use soroban_rs::xdr::{Limits, TransactionEnvelope, TransactionV1Envelope, WriteXdr};
2299 use stellar_strkey::ed25519::PublicKey;
2300
2301 let source_pk =
2303 PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
2304 .unwrap();
2305 let dest_pk =
2306 PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
2307 .unwrap();
2308
2309 let payment_op = soroban_rs::xdr::PaymentOp {
2310 destination: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2311 dest_pk.0,
2312 )),
2313 asset: soroban_rs::xdr::Asset::Native,
2314 amount: 1000000,
2315 };
2316
2317 let operation = soroban_rs::xdr::Operation {
2318 source_account: None,
2319 body: soroban_rs::xdr::OperationBody::Payment(payment_op),
2320 };
2321
2322 let operations: soroban_rs::xdr::VecM<soroban_rs::xdr::Operation, 100> =
2323 vec![operation].try_into().unwrap();
2324
2325 let tx = soroban_rs::xdr::Transaction {
2326 source_account: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2327 source_pk.0,
2328 )),
2329 fee: 100,
2330 seq_num: soroban_rs::xdr::SequenceNumber(1),
2331 cond: soroban_rs::xdr::Preconditions::None,
2332 memo: soroban_rs::xdr::Memo::None,
2333 operations,
2334 ext: soroban_rs::xdr::TransactionExt::V0,
2335 };
2336
2337 let hint = soroban_rs::xdr::SignatureHint([0; 4]);
2339 let sig_bytes: Vec<u8> = vec![0u8; 64];
2340 let sig_bytes_m: soroban_rs::xdr::BytesM<64> = sig_bytes.try_into().unwrap();
2341 let sig = soroban_rs::xdr::DecoratedSignature {
2342 hint,
2343 signature: soroban_rs::xdr::Signature(sig_bytes_m),
2344 };
2345
2346 let envelope = TransactionV1Envelope {
2347 tx,
2348 signatures: vec![sig].try_into().unwrap(),
2349 };
2350
2351 let tx_envelope = TransactionEnvelope::Tx(envelope);
2352 tx_envelope.to_xdr_base64(Limits::none()).unwrap()
2353 };
2354 let stellar_request = StellarTransactionRequest {
2355 source_account: None,
2356 network: "testnet".to_string(),
2357 operations: Some(vec![]),
2358 memo: None,
2359 valid_until: None,
2360 transaction_xdr: Some(signed_xdr.to_string()),
2361 fee_bump: Some(true),
2362 max_fee: Some(20000000),
2363 };
2364
2365 let request = NetworkTransactionRequest::Stellar(stellar_request);
2366 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2367 assert!(result.is_ok());
2368
2369 let tx_model = result.unwrap();
2370 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2371 match &stellar_data.transaction_input {
2372 TransactionInput::SignedXdr { xdr, max_fee } => {
2373 assert_eq!(xdr, &signed_xdr);
2374 assert_eq!(*max_fee, 20000000);
2375 }
2376 _ => panic!("Expected SignedXdr transaction input"),
2377 }
2378 } else {
2379 panic!("Expected Stellar transaction data");
2380 }
2381
2382 let stellar_request = StellarTransactionRequest {
2384 source_account: None,
2385 network: "testnet".to_string(),
2386 operations: Some(vec![]),
2387 memo: None,
2388 valid_until: None,
2389 transaction_xdr: Some(signed_xdr.clone()),
2390 fee_bump: None,
2391 max_fee: None,
2392 };
2393
2394 let request = NetworkTransactionRequest::Stellar(stellar_request);
2395 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2396 assert!(result.is_err());
2397 assert!(result
2398 .unwrap_err()
2399 .to_string()
2400 .contains("Expected unsigned XDR but received signed XDR"));
2401
2402 let stellar_request = StellarTransactionRequest {
2404 source_account: Some(
2405 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2406 ),
2407 network: "testnet".to_string(),
2408 operations: Some(vec![OperationSpec::Payment {
2409 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2410 amount: 1000000,
2411 asset: AssetSpec::Native,
2412 }]),
2413 memo: None,
2414 valid_until: None,
2415 transaction_xdr: None,
2416 fee_bump: Some(true),
2417 max_fee: None,
2418 };
2419
2420 let request = NetworkTransactionRequest::Stellar(stellar_request);
2421 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2422 assert!(result.is_err());
2423 assert!(result
2424 .unwrap_err()
2425 .to_string()
2426 .contains("Cannot request fee_bump with operations mode"));
2427 }
2428
2429 #[test]
2430 fn test_invoke_host_function_must_be_exclusive() {
2431 let (network_model, relayer_model) = test_models();
2432
2433 let stellar_request = StellarTransactionRequest {
2435 source_account: Some(
2436 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2437 ),
2438 network: "testnet".to_string(),
2439 operations: Some(vec![OperationSpec::InvokeContract {
2440 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2441 .to_string(),
2442 function_name: "transfer".to_string(),
2443 args: vec![],
2444 auth: None,
2445 }]),
2446 memo: None,
2447 valid_until: None,
2448 transaction_xdr: None,
2449 fee_bump: None,
2450 max_fee: None,
2451 };
2452
2453 let request = NetworkTransactionRequest::Stellar(stellar_request);
2454 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2455 assert!(result.is_ok(), "Single InvokeHostFunction should succeed");
2456
2457 let stellar_request = StellarTransactionRequest {
2459 source_account: Some(
2460 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2461 ),
2462 network: "testnet".to_string(),
2463 operations: Some(vec![
2464 OperationSpec::Payment {
2465 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2466 .to_string(),
2467 amount: 1000,
2468 asset: AssetSpec::Native,
2469 },
2470 OperationSpec::InvokeContract {
2471 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2472 .to_string(),
2473 function_name: "transfer".to_string(),
2474 args: vec![],
2475 auth: None,
2476 },
2477 ]),
2478 memo: None,
2479 valid_until: None,
2480 transaction_xdr: None,
2481 fee_bump: None,
2482 max_fee: None,
2483 };
2484
2485 let request = NetworkTransactionRequest::Stellar(stellar_request);
2486 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2487
2488 match result {
2489 Ok(_) => panic!("Expected Soroban operation mixed with Payment to fail"),
2490 Err(err) => {
2491 let err_str = err.to_string();
2492 assert!(
2493 err_str.contains("Soroban operations must be exclusive"),
2494 "Expected error about Soroban operation exclusivity, got: {}",
2495 err_str
2496 );
2497 }
2498 }
2499
2500 let stellar_request = StellarTransactionRequest {
2502 source_account: Some(
2503 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2504 ),
2505 network: "testnet".to_string(),
2506 operations: Some(vec![
2507 OperationSpec::InvokeContract {
2508 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2509 .to_string(),
2510 function_name: "transfer".to_string(),
2511 args: vec![],
2512 auth: None,
2513 },
2514 OperationSpec::InvokeContract {
2515 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2516 .to_string(),
2517 function_name: "approve".to_string(),
2518 args: vec![],
2519 auth: None,
2520 },
2521 ]),
2522 memo: None,
2523 valid_until: None,
2524 transaction_xdr: None,
2525 fee_bump: None,
2526 max_fee: None,
2527 };
2528
2529 let request = NetworkTransactionRequest::Stellar(stellar_request);
2530 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2531
2532 match result {
2533 Ok(_) => panic!("Expected multiple Soroban operations to fail"),
2534 Err(err) => {
2535 let err_str = err.to_string();
2536 assert!(
2537 err_str.contains("Transaction can contain at most one Soroban operation"),
2538 "Expected error about multiple Soroban operations, got: {}",
2539 err_str
2540 );
2541 }
2542 }
2543
2544 let stellar_request = StellarTransactionRequest {
2546 source_account: Some(
2547 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2548 ),
2549 network: "testnet".to_string(),
2550 operations: Some(vec![
2551 OperationSpec::Payment {
2552 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2553 .to_string(),
2554 amount: 1000,
2555 asset: AssetSpec::Native,
2556 },
2557 OperationSpec::Payment {
2558 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
2559 .to_string(),
2560 amount: 2000,
2561 asset: AssetSpec::Native,
2562 },
2563 ]),
2564 memo: None,
2565 valid_until: None,
2566 transaction_xdr: None,
2567 fee_bump: None,
2568 max_fee: None,
2569 };
2570
2571 let request = NetworkTransactionRequest::Stellar(stellar_request);
2572 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2573 assert!(result.is_ok(), "Multiple Payment operations should succeed");
2574
2575 let stellar_request = StellarTransactionRequest {
2577 source_account: Some(
2578 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2579 ),
2580 network: "testnet".to_string(),
2581 operations: Some(vec![OperationSpec::InvokeContract {
2582 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2583 .to_string(),
2584 function_name: "transfer".to_string(),
2585 args: vec![],
2586 auth: None,
2587 }]),
2588 memo: Some(MemoSpec::Text {
2589 value: "This should fail".to_string(),
2590 }),
2591 valid_until: None,
2592 transaction_xdr: None,
2593 fee_bump: None,
2594 max_fee: None,
2595 };
2596
2597 let request = NetworkTransactionRequest::Stellar(stellar_request);
2598 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2599
2600 match result {
2601 Ok(_) => panic!("Expected InvokeHostFunction with non-None memo to fail"),
2602 Err(err) => {
2603 let err_str = err.to_string();
2604 assert!(
2605 err_str.contains("Soroban operations cannot have a memo"),
2606 "Expected error about memo restriction, got: {}",
2607 err_str
2608 );
2609 }
2610 }
2611
2612 let stellar_request = StellarTransactionRequest {
2614 source_account: Some(
2615 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2616 ),
2617 network: "testnet".to_string(),
2618 operations: Some(vec![OperationSpec::InvokeContract {
2619 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2620 .to_string(),
2621 function_name: "transfer".to_string(),
2622 args: vec![],
2623 auth: None,
2624 }]),
2625 memo: Some(MemoSpec::None),
2626 valid_until: None,
2627 transaction_xdr: None,
2628 fee_bump: None,
2629 max_fee: None,
2630 };
2631
2632 let request = NetworkTransactionRequest::Stellar(stellar_request);
2633 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2634 assert!(
2635 result.is_ok(),
2636 "InvokeHostFunction with MemoSpec::None should succeed"
2637 );
2638
2639 let stellar_request = StellarTransactionRequest {
2641 source_account: Some(
2642 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2643 ),
2644 network: "testnet".to_string(),
2645 operations: Some(vec![OperationSpec::InvokeContract {
2646 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2647 .to_string(),
2648 function_name: "transfer".to_string(),
2649 args: vec![],
2650 auth: None,
2651 }]),
2652 memo: None,
2653 valid_until: None,
2654 transaction_xdr: None,
2655 fee_bump: None,
2656 max_fee: None,
2657 };
2658
2659 let request = NetworkTransactionRequest::Stellar(stellar_request);
2660 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2661 assert!(
2662 result.is_ok(),
2663 "InvokeHostFunction with no memo should succeed"
2664 );
2665
2666 let stellar_request = StellarTransactionRequest {
2668 source_account: Some(
2669 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2670 ),
2671 network: "testnet".to_string(),
2672 operations: Some(vec![OperationSpec::Payment {
2673 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2674 amount: 1000,
2675 asset: AssetSpec::Native,
2676 }]),
2677 memo: Some(MemoSpec::Text {
2678 value: "Payment memo is allowed".to_string(),
2679 }),
2680 valid_until: None,
2681 transaction_xdr: None,
2682 fee_bump: None,
2683 max_fee: None,
2684 };
2685
2686 let request = NetworkTransactionRequest::Stellar(stellar_request);
2687 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2688 assert!(result.is_ok(), "Payment operation with memo should succeed");
2689 }
2690
2691 #[test]
2692 fn test_update_delete_at_if_final_status_does_not_update_when_delete_at_already_set() {
2693 let _lock = match ENV_MUTEX.lock() {
2694 Ok(guard) => guard,
2695 Err(poisoned) => poisoned.into_inner(),
2696 };
2697
2698 use std::env;
2699
2700 env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2702
2703 let mut transaction = create_test_transaction();
2704 transaction.delete_at = Some("2024-01-01T00:00:00Z".to_string());
2705 transaction.status = TransactionStatus::Confirmed; let original_delete_at = transaction.delete_at.clone();
2708
2709 transaction.update_delete_at_if_final_status();
2710
2711 assert_eq!(transaction.delete_at, original_delete_at);
2713
2714 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2716 }
2717
2718 #[test]
2719 fn test_update_delete_at_if_final_status_does_not_update_when_status_not_final() {
2720 let _lock = match ENV_MUTEX.lock() {
2721 Ok(guard) => guard,
2722 Err(poisoned) => poisoned.into_inner(),
2723 };
2724
2725 use std::env;
2726
2727 env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2729
2730 let mut transaction = create_test_transaction();
2731 transaction.delete_at = None;
2732 transaction.status = TransactionStatus::Pending; transaction.update_delete_at_if_final_status();
2735
2736 assert!(transaction.delete_at.is_none());
2738
2739 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2741 }
2742
2743 #[test]
2744 fn test_update_delete_at_if_final_status_sets_delete_at_for_final_statuses() {
2745 let _lock = match ENV_MUTEX.lock() {
2746 Ok(guard) => guard,
2747 Err(poisoned) => poisoned.into_inner(),
2748 };
2749
2750 use crate::config::ServerConfig;
2751 use chrono::{DateTime, Duration, Utc};
2752 use std::env;
2753
2754 env::set_var("TRANSACTION_EXPIRATION_HOURS", "3"); let actual_hours = ServerConfig::get_transaction_expiration_hours();
2759 assert_eq!(
2760 actual_hours, 3,
2761 "Environment variable should be set to 3 hours"
2762 );
2763
2764 let final_statuses = vec![
2765 TransactionStatus::Canceled,
2766 TransactionStatus::Confirmed,
2767 TransactionStatus::Failed,
2768 TransactionStatus::Expired,
2769 ];
2770
2771 for status in final_statuses {
2772 let mut transaction = create_test_transaction();
2773 transaction.delete_at = None;
2774 transaction.status = status.clone();
2775
2776 let before_update = Utc::now();
2777 transaction.update_delete_at_if_final_status();
2778
2779 assert!(
2781 transaction.delete_at.is_some(),
2782 "delete_at should be set for status: {:?}",
2783 status
2784 );
2785
2786 let delete_at_str = transaction.delete_at.unwrap();
2788 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2789 .expect("delete_at should be valid RFC3339")
2790 .with_timezone(&Utc);
2791
2792 let duration_from_before = delete_at.signed_duration_since(before_update);
2794 let expected_duration = Duration::hours(3);
2795 let tolerance = Duration::minutes(5); let actual_hours_at_runtime = ServerConfig::get_transaction_expiration_hours();
2799
2800 assert!(
2801 duration_from_before >= expected_duration - tolerance &&
2802 duration_from_before <= expected_duration + tolerance,
2803 "delete_at should be approximately 3 hours from now for status: {:?}. Duration from start: {:?}, Expected: {:?}, Config hours at runtime: {}",
2804 status, duration_from_before, expected_duration, actual_hours_at_runtime
2805 );
2806 }
2807
2808 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2810 }
2811
2812 #[test]
2813 fn test_update_delete_at_if_final_status_uses_default_expiration_hours() {
2814 let _lock = match ENV_MUTEX.lock() {
2815 Ok(guard) => guard,
2816 Err(poisoned) => poisoned.into_inner(),
2817 };
2818
2819 use chrono::{DateTime, Duration, Utc};
2820 use std::env;
2821
2822 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2824
2825 let mut transaction = create_test_transaction();
2826 transaction.delete_at = None;
2827 transaction.status = TransactionStatus::Confirmed;
2828
2829 let before_update = Utc::now();
2830 transaction.update_delete_at_if_final_status();
2831
2832 assert!(transaction.delete_at.is_some());
2834
2835 let delete_at_str = transaction.delete_at.unwrap();
2836 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2837 .expect("delete_at should be valid RFC3339")
2838 .with_timezone(&Utc);
2839
2840 let duration_from_before = delete_at.signed_duration_since(before_update);
2842 let expected_duration = Duration::hours(4);
2843 let tolerance = Duration::minutes(5); assert!(
2846 duration_from_before >= expected_duration - tolerance &&
2847 duration_from_before <= expected_duration + tolerance,
2848 "delete_at should be approximately 4 hours from now (default). Duration from start: {:?}, Expected: {:?}",
2849 duration_from_before, expected_duration
2850 );
2851 }
2852
2853 #[test]
2854 fn test_update_delete_at_if_final_status_with_custom_expiration_hours() {
2855 let _lock = match ENV_MUTEX.lock() {
2856 Ok(guard) => guard,
2857 Err(poisoned) => poisoned.into_inner(),
2858 };
2859
2860 use chrono::{DateTime, Duration, Utc};
2861 use std::env;
2862
2863 let test_cases = vec![1, 2, 6, 12]; for expiration_hours in test_cases {
2867 env::set_var("TRANSACTION_EXPIRATION_HOURS", expiration_hours.to_string());
2868
2869 let mut transaction = create_test_transaction();
2870 transaction.delete_at = None;
2871 transaction.status = TransactionStatus::Failed;
2872
2873 let before_update = Utc::now();
2874 transaction.update_delete_at_if_final_status();
2875
2876 assert!(
2877 transaction.delete_at.is_some(),
2878 "delete_at should be set for {} hours",
2879 expiration_hours
2880 );
2881
2882 let delete_at_str = transaction.delete_at.unwrap();
2883 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2884 .expect("delete_at should be valid RFC3339")
2885 .with_timezone(&Utc);
2886
2887 let duration_from_before = delete_at.signed_duration_since(before_update);
2888 let expected_duration = Duration::hours(expiration_hours as i64);
2889 let tolerance = Duration::minutes(5); assert!(
2892 duration_from_before >= expected_duration - tolerance &&
2893 duration_from_before <= expected_duration + tolerance,
2894 "delete_at should be approximately {} hours from now. Duration from start: {:?}, Expected: {:?}",
2895 expiration_hours, duration_from_before, expected_duration
2896 );
2897 }
2898
2899 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2901 }
2902
2903 #[test]
2904 fn test_calculate_delete_at_with_various_hours() {
2905 use chrono::{DateTime, Utc};
2906
2907 let test_cases = vec![0, 1, 6, 12, 24, 48];
2908
2909 for hours in test_cases {
2910 let before_calc = Utc::now();
2911 let result = TransactionRepoModel::calculate_delete_at(hours);
2912 let after_calc = Utc::now();
2913
2914 assert!(
2915 result.is_some(),
2916 "calculate_delete_at should return Some for {} hours",
2917 hours
2918 );
2919
2920 let delete_at_str = result.unwrap();
2921 let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2922 .expect("Result should be valid RFC3339")
2923 .with_timezone(&Utc);
2924
2925 let expected_min =
2926 before_calc + chrono::Duration::hours(hours as i64) - chrono::Duration::seconds(1);
2927 let expected_max =
2928 after_calc + chrono::Duration::hours(hours as i64) + chrono::Duration::seconds(1);
2929
2930 assert!(
2931 delete_at >= expected_min && delete_at <= expected_max,
2932 "Calculated delete_at should be approximately {} hours from now. Got: {}, Expected between: {} and {}",
2933 hours, delete_at, expected_min, expected_max
2934 );
2935 }
2936 }
2937
2938 #[test]
2939 fn test_update_delete_at_if_final_status_idempotent() {
2940 let _lock = match ENV_MUTEX.lock() {
2941 Ok(guard) => guard,
2942 Err(poisoned) => poisoned.into_inner(),
2943 };
2944
2945 use std::env;
2946
2947 env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
2948
2949 let mut transaction = create_test_transaction();
2950 transaction.delete_at = None;
2951 transaction.status = TransactionStatus::Confirmed;
2952
2953 transaction.update_delete_at_if_final_status();
2955 let first_delete_at = transaction.delete_at.clone();
2956 assert!(first_delete_at.is_some());
2957
2958 transaction.update_delete_at_if_final_status();
2960 assert_eq!(transaction.delete_at, first_delete_at);
2961
2962 transaction.update_delete_at_if_final_status();
2964 assert_eq!(transaction.delete_at, first_delete_at);
2965
2966 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2968 }
2969
2970 fn create_test_transaction() -> TransactionRepoModel {
2972 TransactionRepoModel {
2973 id: "test-transaction-id".to_string(),
2974 relayer_id: "test-relayer-id".to_string(),
2975 status: TransactionStatus::Pending,
2976 status_reason: None,
2977 created_at: "2024-01-01T00:00:00Z".to_string(),
2978 sent_at: None,
2979 confirmed_at: None,
2980 valid_until: None,
2981 delete_at: None,
2982 network_data: NetworkTransactionData::Evm(EvmTransactionData {
2983 gas_price: None,
2984 gas_limit: Some(21000),
2985 nonce: Some(0),
2986 value: U256::from(0),
2987 data: None,
2988 from: "0x1234567890123456789012345678901234567890".to_string(),
2989 to: Some("0x0987654321098765432109876543210987654321".to_string()),
2990 chain_id: 1,
2991 hash: None,
2992 signature: None,
2993 speed: None,
2994 max_fee_per_gas: None,
2995 max_priority_fee_per_gas: None,
2996 raw: None,
2997 }),
2998 priced_at: None,
2999 hashes: vec![],
3000 network_type: NetworkType::Evm,
3001 noop_count: None,
3002 is_canceled: None,
3003 }
3004 }
3005
3006 #[test]
3007 fn test_apply_partial_update() {
3008 let mut transaction = create_test_transaction();
3010
3011 let update = TransactionUpdateRequest {
3013 status: Some(TransactionStatus::Confirmed),
3014 status_reason: Some("Transaction confirmed".to_string()),
3015 sent_at: Some("2023-01-01T12:00:00Z".to_string()),
3016 confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
3017 hashes: Some(vec!["0x123".to_string(), "0x456".to_string()]),
3018 is_canceled: Some(false),
3019 ..Default::default()
3020 };
3021
3022 transaction.apply_partial_update(update);
3024
3025 assert_eq!(transaction.status, TransactionStatus::Confirmed);
3027 assert_eq!(
3028 transaction.status_reason,
3029 Some("Transaction confirmed".to_string())
3030 );
3031 assert_eq!(
3032 transaction.sent_at,
3033 Some("2023-01-01T12:00:00Z".to_string())
3034 );
3035 assert_eq!(
3036 transaction.confirmed_at,
3037 Some("2023-01-01T12:05:00Z".to_string())
3038 );
3039 assert_eq!(
3040 transaction.hashes,
3041 vec!["0x123".to_string(), "0x456".to_string()]
3042 );
3043 assert_eq!(transaction.is_canceled, Some(false));
3044
3045 assert!(transaction.delete_at.is_some());
3047 }
3048
3049 #[test]
3050 fn test_apply_partial_update_preserves_unchanged_fields() {
3051 let mut transaction = TransactionRepoModel {
3053 id: "test-tx".to_string(),
3054 relayer_id: "test-relayer".to_string(),
3055 status: TransactionStatus::Pending,
3056 status_reason: Some("Initial reason".to_string()),
3057 created_at: Utc::now().to_rfc3339(),
3058 sent_at: Some("2023-01-01T10:00:00Z".to_string()),
3059 confirmed_at: None,
3060 valid_until: None,
3061 delete_at: None,
3062 network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
3063 priced_at: None,
3064 hashes: vec!["0xoriginal".to_string()],
3065 network_type: NetworkType::Evm,
3066 noop_count: Some(5),
3067 is_canceled: Some(true),
3068 };
3069
3070 let update = TransactionUpdateRequest {
3072 status: Some(TransactionStatus::Sent),
3073 ..Default::default()
3074 };
3075
3076 transaction.apply_partial_update(update);
3078
3079 assert_eq!(transaction.status, TransactionStatus::Sent);
3081 assert_eq!(
3082 transaction.status_reason,
3083 Some("Initial reason".to_string())
3084 );
3085 assert_eq!(
3086 transaction.sent_at,
3087 Some("2023-01-01T10:00:00Z".to_string())
3088 );
3089 assert_eq!(transaction.confirmed_at, None);
3090 assert_eq!(transaction.hashes, vec!["0xoriginal".to_string()]);
3091 assert_eq!(transaction.noop_count, Some(5));
3092 assert_eq!(transaction.is_canceled, Some(true));
3093
3094 assert!(transaction.delete_at.is_none());
3096 }
3097
3098 #[test]
3099 fn test_apply_partial_update_empty_update() {
3100 let mut transaction = create_test_transaction();
3102 let original_transaction = transaction.clone();
3103
3104 let update = TransactionUpdateRequest::default();
3106 transaction.apply_partial_update(update);
3107
3108 assert_eq!(transaction.id, original_transaction.id);
3110 assert_eq!(transaction.status, original_transaction.status);
3111 assert_eq!(
3112 transaction.status_reason,
3113 original_transaction.status_reason
3114 );
3115 assert_eq!(transaction.sent_at, original_transaction.sent_at);
3116 assert_eq!(transaction.confirmed_at, original_transaction.confirmed_at);
3117 assert_eq!(transaction.hashes, original_transaction.hashes);
3118 assert_eq!(transaction.noop_count, original_transaction.noop_count);
3119 assert_eq!(transaction.is_canceled, original_transaction.is_canceled);
3120 assert_eq!(transaction.delete_at, original_transaction.delete_at);
3121 }
3122}