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