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