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