1use super::evm::Speed;
2use crate::{
3 constants::DEFAULT_TRANSACTION_SPEED,
4 domain::{
5 evm::PriceParams,
6 stellar::validation::{validate_operations, validate_soroban_memo_restriction},
7 xdr_utils::{is_signed, parse_transaction_xdr},
8 SignTransactionResponseEvm,
9 },
10 models::{
11 transaction::{
12 request::{evm::EvmTransactionRequest, stellar::StellarTransactionRequest},
13 stellar::{DecoratedSignature, MemoSpec, OperationSpec},
14 },
15 AddressError, EvmNetwork, NetworkRepoModel, NetworkTransactionRequest, NetworkType,
16 RelayerError, RelayerRepoModel, SignerError, StellarNetwork, StellarValidationError,
17 TransactionError, U256,
18 },
19};
20use alloy::{
21 consensus::{TxEip1559, TxLegacy},
22 primitives::{Address as AlloyAddress, Bytes, TxKind},
23 rpc::types::AccessList,
24};
25
26use chrono::Utc;
27use serde::{Deserialize, Serialize};
28use std::{convert::TryFrom, str::FromStr};
29
30use utoipa::ToSchema;
31use uuid::Uuid;
32
33use crate::constants::{STELLAR_DEFAULT_MAX_FEE, STELLAR_DEFAULT_TRANSACTION_FEE};
34use soroban_rs::xdr::{
35 Transaction as SorobanTransaction, TransactionEnvelope, TransactionV1Envelope, VecM,
36};
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
39#[serde(rename_all = "lowercase")]
40pub enum TransactionStatus {
41 Canceled,
42 Pending,
43 Sent,
44 Submitted,
45 Mined,
46 Confirmed,
47 Failed,
48 Expired,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, Default)]
52pub struct TransactionUpdateRequest {
53 pub status: Option<TransactionStatus>,
54 pub status_reason: Option<String>,
55 pub sent_at: Option<String>,
56 pub confirmed_at: Option<String>,
57 pub network_data: Option<NetworkTransactionData>,
58 pub priced_at: Option<String>,
60 pub hashes: Option<Vec<String>>,
62 pub noop_count: Option<u32>,
64 pub is_canceled: Option<bool>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct TransactionRepoModel {
70 pub id: String,
71 pub relayer_id: String,
72 pub status: TransactionStatus,
73 pub status_reason: Option<String>,
74 pub created_at: String,
75 pub sent_at: Option<String>,
76 pub confirmed_at: Option<String>,
77 pub valid_until: Option<String>,
78 pub network_data: NetworkTransactionData,
79 pub priced_at: Option<String>,
81 pub hashes: Vec<String>,
83 pub network_type: NetworkType,
84 pub noop_count: Option<u32>,
85 pub is_canceled: Option<bool>,
86}
87
88impl TransactionRepoModel {
89 pub fn validate(&self) -> Result<(), TransactionError> {
95 Ok(())
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(tag = "network_data", content = "data")]
101#[allow(clippy::large_enum_variant)]
102pub enum NetworkTransactionData {
103 Evm(EvmTransactionData),
104 Solana(SolanaTransactionData),
105 Stellar(StellarTransactionData),
106}
107
108impl NetworkTransactionData {
109 pub fn get_evm_transaction_data(&self) -> Result<EvmTransactionData, TransactionError> {
110 match self {
111 NetworkTransactionData::Evm(data) => Ok(data.clone()),
112 _ => Err(TransactionError::InvalidType(
113 "Expected EVM transaction".to_string(),
114 )),
115 }
116 }
117
118 pub fn get_solana_transaction_data(&self) -> Result<SolanaTransactionData, TransactionError> {
119 match self {
120 NetworkTransactionData::Solana(data) => Ok(data.clone()),
121 _ => Err(TransactionError::InvalidType(
122 "Expected Solana transaction".to_string(),
123 )),
124 }
125 }
126
127 pub fn get_stellar_transaction_data(&self) -> Result<StellarTransactionData, TransactionError> {
128 match self {
129 NetworkTransactionData::Stellar(data) => Ok(data.clone()),
130 _ => Err(TransactionError::InvalidType(
131 "Expected Stellar transaction".to_string(),
132 )),
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
138pub struct EvmTransactionDataSignature {
139 pub r: String,
140 pub s: String,
141 pub v: u8,
142 pub sig: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146
147pub struct EvmTransactionData {
148 pub gas_price: Option<u128>,
149 pub gas_limit: u64,
150 pub nonce: Option<u64>,
151 pub value: U256,
152 pub data: Option<String>,
153 pub from: String,
154 pub to: Option<String>,
155 pub chain_id: u64,
156 pub hash: Option<String>,
157 pub signature: Option<EvmTransactionDataSignature>,
158 pub speed: Option<Speed>,
159 pub max_fee_per_gas: Option<u128>,
160 pub max_priority_fee_per_gas: Option<u128>,
161 pub raw: Option<Vec<u8>>,
162}
163
164impl EvmTransactionData {
165 pub fn for_replacement(old_data: &EvmTransactionData, request: &EvmTransactionRequest) -> Self {
177 Self {
178 chain_id: old_data.chain_id,
180 from: old_data.from.clone(),
181 nonce: old_data.nonce, to: request.to.clone(),
185 value: request.value,
186 data: request.data.clone(),
187 gas_limit: request.gas_limit,
188 speed: request
189 .speed
190 .clone()
191 .or_else(|| old_data.speed.clone())
192 .or(Some(DEFAULT_TRANSACTION_SPEED)),
193
194 gas_price: None,
196 max_fee_per_gas: None,
197 max_priority_fee_per_gas: None,
198
199 signature: None,
201 hash: None,
202 raw: None,
203 }
204 }
205
206 pub fn with_price_params(mut self, price_params: PriceParams) -> Self {
214 self.gas_price = price_params.gas_price;
215 self.max_fee_per_gas = price_params.max_fee_per_gas;
216 self.max_priority_fee_per_gas = price_params.max_priority_fee_per_gas;
217
218 self
219 }
220
221 pub fn with_gas_estimate(mut self, gas_limit: u64) -> Self {
229 self.gas_limit = gas_limit;
230 self
231 }
232
233 pub fn with_nonce(mut self, nonce: u64) -> Self {
241 self.nonce = Some(nonce);
242 self
243 }
244
245 pub fn with_signed_transaction_data(mut self, sig: SignTransactionResponseEvm) -> Self {
253 self.signature = Some(sig.signature);
254 self.hash = Some(sig.hash);
255 self.raw = Some(sig.raw);
256 self
257 }
258}
259
260#[cfg(test)]
261impl Default for EvmTransactionData {
262 fn default() -> Self {
263 Self {
264 from: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(), to: Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string()), gas_price: Some(20000000000),
267 value: U256::from(1000000000000000000u128), data: Some("0x".to_string()),
269 nonce: Some(1),
270 chain_id: 1,
271 gas_limit: 21000,
272 hash: None,
273 signature: None,
274 speed: None,
275 max_fee_per_gas: None,
276 max_priority_fee_per_gas: None,
277 raw: None,
278 }
279 }
280}
281
282#[cfg(test)]
283impl Default for TransactionRepoModel {
284 fn default() -> Self {
285 Self {
286 id: "00000000-0000-0000-0000-000000000001".to_string(),
287 relayer_id: "00000000-0000-0000-0000-000000000002".to_string(),
288 status: TransactionStatus::Pending,
289 created_at: "2023-01-01T00:00:00Z".to_string(),
290 status_reason: None,
291 sent_at: None,
292 confirmed_at: None,
293 valid_until: None,
294 network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
295 network_type: NetworkType::Evm,
296 priced_at: None,
297 hashes: Vec::new(),
298 noop_count: None,
299 is_canceled: Some(false),
300 }
301 }
302}
303
304pub trait EvmTransactionDataTrait {
305 fn is_legacy(&self) -> bool;
306 fn is_eip1559(&self) -> bool;
307 fn is_speed(&self) -> bool;
308}
309
310impl EvmTransactionDataTrait for EvmTransactionData {
311 fn is_legacy(&self) -> bool {
312 self.gas_price.is_some()
313 }
314
315 fn is_eip1559(&self) -> bool {
316 self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some()
317 }
318
319 fn is_speed(&self) -> bool {
320 self.speed.is_some()
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct SolanaTransactionData {
326 pub recent_blockhash: Option<String>,
327 pub fee_payer: String,
328 pub instructions: Vec<String>,
329 pub hash: Option<String>,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub enum TransactionInput {
335 Operations(Vec<OperationSpec>),
337 UnsignedXdr(String),
339 SignedXdr { xdr: String, max_fee: i64 },
341}
342
343impl Default for TransactionInput {
344 fn default() -> Self {
345 TransactionInput::Operations(vec![])
346 }
347}
348
349impl TransactionInput {
350 pub fn from_stellar_request(
352 request: &StellarTransactionRequest,
353 ) -> Result<Self, TransactionError> {
354 if let Some(xdr) = &request.transaction_xdr {
356 let envelope = parse_transaction_xdr(xdr, false)
357 .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
358
359 return if request.fee_bump == Some(true) {
360 if !is_signed(&envelope) {
362 Err(TransactionError::ValidationError(
363 "Cannot request fee_bump with unsigned XDR".to_string(),
364 ))
365 } else {
366 let max_fee = request.max_fee.unwrap_or(STELLAR_DEFAULT_MAX_FEE);
367 Ok(TransactionInput::SignedXdr {
368 xdr: xdr.clone(),
369 max_fee,
370 })
371 }
372 } else {
373 if is_signed(&envelope) {
375 Err(TransactionError::ValidationError(
376 StellarValidationError::UnexpectedSignedXdr.to_string(),
377 ))
378 } else {
379 Ok(TransactionInput::UnsignedXdr(xdr.clone()))
380 }
381 };
382 }
383
384 if let Some(operations) = &request.operations {
386 if operations.is_empty() {
387 return Err(TransactionError::ValidationError(
388 "Operations must not be empty".to_string(),
389 ));
390 }
391
392 if request.fee_bump == Some(true) {
393 return Err(TransactionError::ValidationError(
394 "Cannot request fee_bump with operations mode".to_string(),
395 ));
396 }
397
398 validate_operations(operations)
400 .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
401
402 validate_soroban_memo_restriction(operations, &request.memo)
404 .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
405
406 return Ok(TransactionInput::Operations(operations.clone()));
407 }
408
409 Err(TransactionError::ValidationError(
411 "Must provide either operations or transaction_xdr".to_string(),
412 ))
413 }
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct StellarTransactionData {
418 pub source_account: String,
419 pub fee: Option<u32>,
420 pub sequence_number: Option<i64>,
421 pub memo: Option<MemoSpec>,
422 pub valid_until: Option<String>,
423 pub network_passphrase: String,
424 #[serde(skip_serializing, skip_deserializing)]
425 pub signatures: Vec<DecoratedSignature>,
426 pub hash: Option<String>,
427 #[serde(skip_serializing, skip_deserializing)]
428 pub simulation_transaction_data: Option<String>,
429 #[serde(skip)]
430 pub transaction_input: TransactionInput,
431 #[serde(skip_serializing, skip_deserializing)]
432 pub signed_envelope_xdr: Option<String>,
433}
434
435impl StellarTransactionData {
436 pub fn with_sequence_number(mut self, sequence_number: i64) -> Self {
444 self.sequence_number = Some(sequence_number);
445 self
446 }
447
448 pub fn build_unsigned_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
456 match &self.transaction_input {
457 TransactionInput::Operations(_) => {
458 self.build_envelope_from_operations_unsigned()
460 }
461 TransactionInput::UnsignedXdr(xdr) => {
462 self.parse_xdr_envelope(xdr)
464 }
465 TransactionInput::SignedXdr { xdr, .. } => {
466 self.parse_xdr_envelope(xdr)
468 }
469 }
470 }
471
472 pub fn get_envelope_for_simulation(&self) -> Result<TransactionEnvelope, SignerError> {
480 self.build_unsigned_envelope()
481 }
482
483 pub fn build_signed_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
491 if let Some(ref xdr) = self.signed_envelope_xdr {
493 return self.parse_xdr_envelope(xdr);
494 }
495
496 match &self.transaction_input {
498 TransactionInput::Operations(_) => {
499 self.build_envelope_from_operations_signed()
501 }
502 TransactionInput::UnsignedXdr(xdr) => {
503 let envelope = self.parse_xdr_envelope(xdr)?;
505 self.attach_signatures_to_envelope(envelope)
506 }
507 TransactionInput::SignedXdr { xdr, .. } => {
508 self.parse_xdr_envelope(xdr)
510 }
511 }
512 }
513
514 pub fn get_envelope_for_submission(&self) -> Result<TransactionEnvelope, SignerError> {
522 self.build_signed_envelope()
523 }
524
525 fn build_envelope_from_operations_unsigned(&self) -> Result<TransactionEnvelope, SignerError> {
527 let tx = SorobanTransaction::try_from(self.clone())?;
528 Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
529 tx,
530 signatures: VecM::default(),
531 }))
532 }
533
534 fn build_envelope_from_operations_signed(&self) -> Result<TransactionEnvelope, SignerError> {
536 let tx = SorobanTransaction::try_from(self.clone())?;
537 let signatures = VecM::try_from(self.signatures.clone())
538 .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
539 Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
540 tx,
541 signatures,
542 }))
543 }
544
545 fn parse_xdr_envelope(&self, xdr: &str) -> Result<TransactionEnvelope, SignerError> {
547 use soroban_rs::xdr::{Limits, ReadXdr};
548 TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
549 .map_err(|e| SignerError::ConversionError(format!("Invalid XDR: {}", e)))
550 }
551
552 fn attach_signatures_to_envelope(
554 &self,
555 envelope: TransactionEnvelope,
556 ) -> Result<TransactionEnvelope, SignerError> {
557 use soroban_rs::xdr::{Limits, ReadXdr, WriteXdr};
558
559 let envelope_xdr = envelope.to_xdr_base64(Limits::none()).map_err(|e| {
561 SignerError::ConversionError(format!("Failed to serialize envelope: {}", e))
562 })?;
563
564 let mut envelope = TransactionEnvelope::from_xdr_base64(&envelope_xdr, Limits::none())
565 .map_err(|e| {
566 SignerError::ConversionError(format!("Failed to parse envelope: {}", e))
567 })?;
568
569 let sigs = VecM::try_from(self.signatures.clone())
570 .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
571
572 match &mut envelope {
573 TransactionEnvelope::Tx(ref mut v1) => v1.signatures = sigs,
574 TransactionEnvelope::TxV0(ref mut v0) => v0.signatures = sigs,
575 TransactionEnvelope::TxFeeBump(_) => {
576 return Err(SignerError::ConversionError(
577 "Cannot attach signatures to fee-bump transaction directly".into(),
578 ));
579 }
580 }
581
582 Ok(envelope)
583 }
584
585 pub fn attach_signature(mut self, sig: DecoratedSignature) -> Self {
593 self.signatures.push(sig);
594 self
595 }
596
597 pub fn with_hash(mut self, hash: String) -> Self {
605 self.hash = Some(hash);
606 self
607 }
608
609 pub fn with_simulation_data(
611 mut self,
612 sim_response: soroban_rs::stellar_rpc_client::SimulateTransactionResponse,
613 operations_count: u64,
614 ) -> Result<Self, SignerError> {
615 use log::info;
616
617 let inclusion_fee = operations_count * STELLAR_DEFAULT_TRANSACTION_FEE as u64;
619 let resource_fee = sim_response.min_resource_fee;
620
621 let updated_fee = u32::try_from(inclusion_fee + resource_fee)
622 .map_err(|_| SignerError::ConversionError("Fee too high".to_string()))?
623 .max(STELLAR_DEFAULT_TRANSACTION_FEE);
624 self.fee = Some(updated_fee);
625
626 self.simulation_transaction_data = Some(sim_response.transaction_data);
628
629 info!(
630 "Applied simulation fee: {} stroops and stored transaction extension data",
631 updated_fee
632 );
633 Ok(self)
634 }
635}
636
637impl
638 TryFrom<(
639 &NetworkTransactionRequest,
640 &RelayerRepoModel,
641 &NetworkRepoModel,
642 )> for TransactionRepoModel
643{
644 type Error = RelayerError;
645
646 fn try_from(
647 (request, relayer_model, network_model): (
648 &NetworkTransactionRequest,
649 &RelayerRepoModel,
650 &NetworkRepoModel,
651 ),
652 ) -> Result<Self, Self::Error> {
653 let now = Utc::now().to_rfc3339();
654
655 match request {
656 NetworkTransactionRequest::Evm(evm_request) => {
657 let network = EvmNetwork::try_from(network_model.clone())?;
658 Ok(Self {
659 id: Uuid::new_v4().to_string(),
660 relayer_id: relayer_model.id.clone(),
661 status: TransactionStatus::Pending,
662 status_reason: None,
663 created_at: now,
664 sent_at: None,
665 confirmed_at: None,
666 valid_until: evm_request.valid_until.clone(),
667 network_type: NetworkType::Evm,
668 network_data: NetworkTransactionData::Evm(EvmTransactionData {
669 gas_price: evm_request.gas_price,
670 gas_limit: evm_request.gas_limit,
671 nonce: None,
672 value: evm_request.value,
673 data: evm_request.data.clone(),
674 from: relayer_model.address.clone(),
675 to: evm_request.to.clone(),
676 chain_id: network.id(),
677 hash: None,
678 signature: None,
679 speed: evm_request.speed.clone(),
680 max_fee_per_gas: evm_request.max_fee_per_gas,
681 max_priority_fee_per_gas: evm_request.max_priority_fee_per_gas,
682 raw: None,
683 }),
684 priced_at: None,
685 hashes: Vec::new(),
686 noop_count: None,
687 is_canceled: Some(false),
688 })
689 }
690 NetworkTransactionRequest::Solana(solana_request) => Ok(Self {
691 id: Uuid::new_v4().to_string(),
692 relayer_id: relayer_model.id.clone(),
693 status: TransactionStatus::Pending,
694 status_reason: None,
695 created_at: now,
696 sent_at: None,
697 confirmed_at: None,
698 valid_until: None,
699 network_type: NetworkType::Solana,
700 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
701 recent_blockhash: None,
702 fee_payer: solana_request.fee_payer.clone(),
703 instructions: solana_request.instructions.clone(),
704 hash: None,
705 }),
706 priced_at: None,
707 hashes: Vec::new(),
708 noop_count: None,
709 is_canceled: Some(false),
710 }),
711 NetworkTransactionRequest::Stellar(stellar_request) => {
712 let source_account = stellar_request.source_account.clone();
714
715 let stellar_data = StellarTransactionData {
717 source_account: source_account.unwrap_or_else(|| relayer_model.address.clone()),
718 memo: stellar_request.memo.clone(),
719 valid_until: stellar_request.valid_until.clone(),
720 network_passphrase: StellarNetwork::try_from(network_model.clone())?.passphrase,
721 signatures: Vec::new(),
722 hash: None,
723 fee: None,
724 sequence_number: None,
725 simulation_transaction_data: None,
726 transaction_input: TransactionInput::from_stellar_request(stellar_request)
727 .map_err(|e| RelayerError::ValidationError(e.to_string()))?,
728 signed_envelope_xdr: None,
729 };
730
731 Ok(Self {
732 id: Uuid::new_v4().to_string(),
733 relayer_id: relayer_model.id.clone(),
734 status: TransactionStatus::Pending,
735 status_reason: None,
736 created_at: now,
737 sent_at: None,
738 confirmed_at: None,
739 valid_until: None,
740 network_type: NetworkType::Stellar,
741 network_data: NetworkTransactionData::Stellar(stellar_data),
742 priced_at: None,
743 hashes: Vec::new(),
744 noop_count: None,
745 is_canceled: Some(false),
746 })
747 }
748 }
749 }
750}
751
752impl EvmTransactionData {
753 pub fn to_address(&self) -> Result<Option<AlloyAddress>, SignerError> {
760 Ok(match self.to.as_deref().filter(|s| !s.is_empty()) {
761 Some(addr_str) => Some(AlloyAddress::from_str(addr_str).map_err(|e| {
762 AddressError::ConversionError(format!("Invalid 'to' address: {}", e))
763 })?),
764 None => None,
765 })
766 }
767
768 pub fn data_to_bytes(&self) -> Result<Bytes, SignerError> {
774 Bytes::from_str(self.data.as_deref().unwrap_or(""))
775 .map_err(|e| SignerError::SigningError(format!("Invalid transaction data: {}", e)))
776 }
777}
778
779impl TryFrom<NetworkTransactionData> for TxLegacy {
780 type Error = SignerError;
781
782 fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
783 match tx {
784 NetworkTransactionData::Evm(tx) => {
785 let tx_kind = match tx.to_address()? {
786 Some(addr) => TxKind::Call(addr),
787 None => TxKind::Create,
788 };
789
790 Ok(Self {
791 chain_id: Some(tx.chain_id),
792 nonce: tx.nonce.unwrap_or(0),
793 gas_limit: tx.gas_limit,
794 gas_price: tx.gas_price.unwrap_or(0),
795 to: tx_kind,
796 value: tx.value,
797 input: tx.data_to_bytes()?,
798 })
799 }
800 _ => Err(SignerError::SigningError(
801 "Not an EVM transaction".to_string(),
802 )),
803 }
804 }
805}
806
807impl TryFrom<NetworkTransactionData> for TxEip1559 {
808 type Error = SignerError;
809
810 fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
811 match tx {
812 NetworkTransactionData::Evm(tx) => {
813 let tx_kind = match tx.to_address()? {
814 Some(addr) => TxKind::Call(addr),
815 None => TxKind::Create,
816 };
817
818 Ok(Self {
819 chain_id: tx.chain_id,
820 nonce: tx.nonce.unwrap_or(0),
821 gas_limit: tx.gas_limit,
822 max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
823 max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
824 to: tx_kind,
825 value: tx.value,
826 access_list: AccessList::default(),
827 input: tx.data_to_bytes()?,
828 })
829 }
830 _ => Err(SignerError::SigningError(
831 "Not an EVM transaction".to_string(),
832 )),
833 }
834 }
835}
836
837impl TryFrom<&EvmTransactionData> for TxLegacy {
838 type Error = SignerError;
839
840 fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
841 let tx_kind = match tx.to_address()? {
842 Some(addr) => TxKind::Call(addr),
843 None => TxKind::Create,
844 };
845
846 Ok(Self {
847 chain_id: Some(tx.chain_id),
848 nonce: tx.nonce.unwrap_or(0),
849 gas_limit: tx.gas_limit,
850 gas_price: tx.gas_price.unwrap_or(0),
851 to: tx_kind,
852 value: tx.value,
853 input: tx.data_to_bytes()?,
854 })
855 }
856}
857
858impl TryFrom<EvmTransactionData> for TxLegacy {
859 type Error = SignerError;
860
861 fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
862 Self::try_from(&tx)
863 }
864}
865
866impl TryFrom<&EvmTransactionData> for TxEip1559 {
867 type Error = SignerError;
868
869 fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
870 let tx_kind = match tx.to_address()? {
871 Some(addr) => TxKind::Call(addr),
872 None => TxKind::Create,
873 };
874
875 Ok(Self {
876 chain_id: tx.chain_id,
877 nonce: tx.nonce.unwrap_or(0),
878 gas_limit: tx.gas_limit,
879 max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
880 max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
881 to: tx_kind,
882 value: tx.value,
883 access_list: AccessList::default(),
884 input: tx.data_to_bytes()?,
885 })
886 }
887}
888
889impl TryFrom<EvmTransactionData> for TxEip1559 {
890 type Error = SignerError;
891
892 fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
893 Self::try_from(&tx)
894 }
895}
896
897impl From<&[u8; 65]> for EvmTransactionDataSignature {
898 fn from(bytes: &[u8; 65]) -> Self {
899 Self {
900 r: hex::encode(&bytes[0..32]),
901 s: hex::encode(&bytes[32..64]),
902 v: bytes[64],
903 sig: hex::encode(bytes),
904 }
905 }
906}
907
908#[cfg(test)]
909mod tests {
910 use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
911
912 use super::*;
913 use crate::{
914 config::{
915 EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
916 },
917 models::{
918 network::NetworkConfigData,
919 relayer::{
920 RelayerEvmPolicy, RelayerNetworkPolicy, RelayerSolanaPolicy, RelayerStellarPolicy,
921 },
922 transaction::stellar::AssetSpec,
923 },
924 };
925
926 #[test]
927 fn test_signature_from_bytes() {
928 let test_bytes: [u8; 65] = [
929 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
930 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,
932 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 27, ];
935
936 let signature = EvmTransactionDataSignature::from(&test_bytes);
937
938 assert_eq!(signature.r.len(), 64); assert_eq!(signature.s.len(), 64); assert_eq!(signature.v, 27);
941 assert_eq!(signature.sig.len(), 130); }
943
944 fn create_sample_evm_tx_data() -> EvmTransactionData {
946 EvmTransactionData {
947 gas_price: Some(20_000_000_000),
948 gas_limit: 21000,
949 nonce: Some(5),
950 value: U256::from(1000000000000000000u128), data: Some("0x".to_string()),
952 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
953 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
954 chain_id: 1,
955 hash: None,
956 signature: None,
957 speed: None,
958 max_fee_per_gas: None,
959 max_priority_fee_per_gas: None,
960 raw: None,
961 }
962 }
963
964 #[test]
966 fn test_evm_tx_with_price_params() {
967 let tx_data = create_sample_evm_tx_data();
968 let price_params = PriceParams {
969 gas_price: None,
970 max_fee_per_gas: Some(30_000_000_000),
971 max_priority_fee_per_gas: Some(2_000_000_000),
972 is_min_bumped: None,
973 extra_fee: None,
974 total_cost: U256::ZERO,
975 };
976
977 let updated_tx = tx_data.with_price_params(price_params);
978
979 assert_eq!(updated_tx.max_fee_per_gas, Some(30_000_000_000));
980 assert_eq!(updated_tx.max_priority_fee_per_gas, Some(2_000_000_000));
981 }
982
983 #[test]
984 fn test_evm_tx_with_gas_estimate() {
985 let tx_data = create_sample_evm_tx_data();
986 let new_gas_limit = 30000;
987
988 let updated_tx = tx_data.with_gas_estimate(new_gas_limit);
989
990 assert_eq!(updated_tx.gas_limit, new_gas_limit);
991 }
992
993 #[test]
994 fn test_evm_tx_with_nonce() {
995 let tx_data = create_sample_evm_tx_data();
996 let new_nonce = 10;
997
998 let updated_tx = tx_data.with_nonce(new_nonce);
999
1000 assert_eq!(updated_tx.nonce, Some(new_nonce));
1001 }
1002
1003 #[test]
1004 fn test_evm_tx_with_signed_transaction_data() {
1005 let tx_data = create_sample_evm_tx_data();
1006
1007 let signature = EvmTransactionDataSignature {
1008 r: "r_value".to_string(),
1009 s: "s_value".to_string(),
1010 v: 27,
1011 sig: "signature_value".to_string(),
1012 };
1013
1014 let signed_tx_response = SignTransactionResponseEvm {
1015 signature,
1016 hash: "0xabcdef1234567890".to_string(),
1017 raw: vec![1, 2, 3, 4, 5],
1018 };
1019
1020 let updated_tx = tx_data.with_signed_transaction_data(signed_tx_response);
1021
1022 assert_eq!(updated_tx.signature.as_ref().unwrap().r, "r_value");
1023 assert_eq!(updated_tx.signature.as_ref().unwrap().s, "s_value");
1024 assert_eq!(updated_tx.signature.as_ref().unwrap().v, 27);
1025 assert_eq!(updated_tx.hash, Some("0xabcdef1234567890".to_string()));
1026 assert_eq!(updated_tx.raw, Some(vec![1, 2, 3, 4, 5]));
1027 }
1028
1029 #[test]
1030 fn test_evm_tx_to_address() {
1031 let tx_data = create_sample_evm_tx_data();
1033 let address_result = tx_data.to_address();
1034 assert!(address_result.is_ok());
1035 let address_option = address_result.unwrap();
1036 assert!(address_option.is_some());
1037 assert_eq!(
1038 address_option.unwrap().to_string().to_lowercase(),
1039 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_lowercase()
1040 );
1041
1042 let mut contract_creation_tx = create_sample_evm_tx_data();
1044 contract_creation_tx.to = None;
1045 let address_result = contract_creation_tx.to_address();
1046 assert!(address_result.is_ok());
1047 assert!(address_result.unwrap().is_none());
1048
1049 let mut empty_address_tx = create_sample_evm_tx_data();
1051 empty_address_tx.to = Some("".to_string());
1052 let address_result = empty_address_tx.to_address();
1053 assert!(address_result.is_ok());
1054 assert!(address_result.unwrap().is_none());
1055
1056 let mut invalid_address_tx = create_sample_evm_tx_data();
1058 invalid_address_tx.to = Some("0xINVALID".to_string());
1059 let address_result = invalid_address_tx.to_address();
1060 assert!(address_result.is_err());
1061 }
1062
1063 #[test]
1064 fn test_evm_tx_data_to_bytes() {
1065 let mut tx_data = create_sample_evm_tx_data();
1067 tx_data.data = Some("0x1234".to_string());
1068 let bytes_result = tx_data.data_to_bytes();
1069 assert!(bytes_result.is_ok());
1070 assert_eq!(bytes_result.unwrap().as_ref(), &[0x12, 0x34]);
1071
1072 tx_data.data = Some("".to_string());
1074 assert!(tx_data.data_to_bytes().is_ok());
1075
1076 tx_data.data = None;
1078 assert!(tx_data.data_to_bytes().is_ok());
1079
1080 tx_data.data = Some("0xZZ".to_string());
1082 assert!(tx_data.data_to_bytes().is_err());
1083 }
1084
1085 #[test]
1087 fn test_evm_tx_is_legacy() {
1088 let mut tx_data = create_sample_evm_tx_data();
1089
1090 assert!(tx_data.is_legacy());
1092
1093 tx_data.gas_price = None;
1095 assert!(!tx_data.is_legacy());
1096 }
1097
1098 #[test]
1099 fn test_evm_tx_is_eip1559() {
1100 let mut tx_data = create_sample_evm_tx_data();
1101
1102 assert!(!tx_data.is_eip1559());
1104
1105 tx_data.max_fee_per_gas = Some(30_000_000_000);
1107 tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1108 assert!(tx_data.is_eip1559());
1109
1110 tx_data.max_priority_fee_per_gas = None;
1112 assert!(!tx_data.is_eip1559());
1113 }
1114
1115 #[test]
1116 fn test_evm_tx_is_speed() {
1117 let mut tx_data = create_sample_evm_tx_data();
1118
1119 assert!(!tx_data.is_speed());
1121
1122 tx_data.speed = Some(Speed::Fast);
1124 assert!(tx_data.is_speed());
1125 }
1126
1127 #[test]
1129 fn test_network_tx_data_get_evm_transaction_data() {
1130 let evm_tx_data = create_sample_evm_tx_data();
1131 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1132
1133 let result = network_data.get_evm_transaction_data();
1135 assert!(result.is_ok());
1136 assert_eq!(result.unwrap().chain_id, evm_tx_data.chain_id);
1137
1138 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1140 recent_blockhash: None,
1141 fee_payer: "test".to_string(),
1142 instructions: vec![],
1143 hash: None,
1144 });
1145 assert!(solana_data.get_evm_transaction_data().is_err());
1146 }
1147
1148 #[test]
1149 fn test_network_tx_data_get_solana_transaction_data() {
1150 let solana_tx_data = SolanaTransactionData {
1151 recent_blockhash: Some("hash123".to_string()),
1152 fee_payer: "payer123".to_string(),
1153 instructions: vec!["instruction1".to_string()],
1154 hash: None,
1155 };
1156 let network_data = NetworkTransactionData::Solana(solana_tx_data.clone());
1157
1158 let result = network_data.get_solana_transaction_data();
1160 assert!(result.is_ok());
1161 assert_eq!(result.unwrap().fee_payer, solana_tx_data.fee_payer);
1162
1163 let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1165 assert!(evm_data.get_solana_transaction_data().is_err());
1166 }
1167
1168 #[test]
1169 fn test_network_tx_data_get_stellar_transaction_data() {
1170 let stellar_tx_data = StellarTransactionData {
1171 source_account: "account123".to_string(),
1172 fee: Some(100),
1173 sequence_number: Some(5),
1174 memo: Some(MemoSpec::Text {
1175 value: "Test memo".to_string(),
1176 }),
1177 valid_until: Some("2025-01-01T00:00:00Z".to_string()),
1178 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1179 signatures: Vec::new(),
1180 hash: Some("hash123".to_string()),
1181 simulation_transaction_data: None,
1182 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1183 destination: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ".to_string(),
1184 amount: 100000000, asset: AssetSpec::Native,
1186 }]),
1187 signed_envelope_xdr: None,
1188 };
1189 let network_data = NetworkTransactionData::Stellar(stellar_tx_data.clone());
1190
1191 let result = network_data.get_stellar_transaction_data();
1193 assert!(result.is_ok());
1194 assert_eq!(
1195 result.unwrap().source_account,
1196 stellar_tx_data.source_account
1197 );
1198
1199 let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1201 assert!(evm_data.get_stellar_transaction_data().is_err());
1202 }
1203
1204 #[test]
1206 fn test_try_from_network_tx_data_for_tx_legacy() {
1207 let evm_tx_data = create_sample_evm_tx_data();
1209 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1210
1211 let result = TxLegacy::try_from(network_data);
1213 assert!(result.is_ok());
1214 let tx_legacy = result.unwrap();
1215
1216 assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1218 assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1219 assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit);
1220 assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1221 assert_eq!(tx_legacy.value, evm_tx_data.value);
1222
1223 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1225 recent_blockhash: None,
1226 fee_payer: "test".to_string(),
1227 instructions: vec![],
1228 hash: None,
1229 });
1230 assert!(TxLegacy::try_from(solana_data).is_err());
1231 }
1232
1233 #[test]
1234 fn test_try_from_evm_tx_data_for_tx_legacy() {
1235 let evm_tx_data = create_sample_evm_tx_data();
1237
1238 let result = TxLegacy::try_from(evm_tx_data.clone());
1240 assert!(result.is_ok());
1241 let tx_legacy = result.unwrap();
1242
1243 assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1245 assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1246 assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit);
1247 assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1248 assert_eq!(tx_legacy.value, evm_tx_data.value);
1249 }
1250
1251 fn dummy_signature() -> DecoratedSignature {
1252 let hint = SignatureHint([0; 4]);
1253 let bytes: Vec<u8> = vec![0u8; 64];
1254 let bytes_m: BytesM<64> = bytes.try_into().expect("BytesM conversion");
1255 DecoratedSignature {
1256 hint,
1257 signature: Signature(bytes_m),
1258 }
1259 }
1260
1261 fn test_stellar_tx_data() -> StellarTransactionData {
1262 StellarTransactionData {
1263 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1264 fee: Some(100),
1265 sequence_number: Some(1),
1266 memo: None,
1267 valid_until: None,
1268 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1269 signatures: Vec::new(),
1270 hash: None,
1271 simulation_transaction_data: None,
1272 transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1273 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1274 amount: 1000,
1275 asset: AssetSpec::Native,
1276 }]),
1277 signed_envelope_xdr: None,
1278 }
1279 }
1280
1281 #[test]
1282 fn test_with_sequence_number() {
1283 let tx = test_stellar_tx_data();
1284 let updated = tx.with_sequence_number(42);
1285 assert_eq!(updated.sequence_number, Some(42));
1286 }
1287
1288 #[test]
1289 fn test_get_envelope_for_simulation() {
1290 let tx = test_stellar_tx_data();
1291 let env = tx.get_envelope_for_simulation();
1292 assert!(env.is_ok());
1293 let env = env.unwrap();
1294 match env {
1296 soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1297 assert_eq!(tx_env.signatures.len(), 0);
1298 }
1299 _ => {
1300 panic!("Expected TransactionEnvelope::Tx variant");
1301 }
1302 }
1303 }
1304
1305 #[test]
1306 fn test_get_envelope_for_submission() {
1307 let mut tx = test_stellar_tx_data();
1308 tx.signatures.push(dummy_signature());
1309 let env = tx.get_envelope_for_submission();
1310 assert!(env.is_ok());
1311 let env = env.unwrap();
1312 match env {
1313 soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1314 assert_eq!(tx_env.signatures.len(), 1);
1315 }
1316 _ => {
1317 panic!("Expected TransactionEnvelope::Tx variant");
1318 }
1319 }
1320 }
1321
1322 #[test]
1323 fn test_attach_signature() {
1324 let tx = test_stellar_tx_data();
1325 let sig = dummy_signature();
1326 let updated = tx.attach_signature(sig.clone());
1327 assert_eq!(updated.signatures.len(), 1);
1328 assert_eq!(updated.signatures[0], sig);
1329 }
1330
1331 #[test]
1332 fn test_with_hash() {
1333 let tx = test_stellar_tx_data();
1334 let updated = tx.with_hash("hash123".to_string());
1335 assert_eq!(updated.hash, Some("hash123".to_string()));
1336 }
1337
1338 #[test]
1339 fn test_evm_tx_for_replacement() {
1340 let old_data = create_sample_evm_tx_data();
1341 let new_request = EvmTransactionRequest {
1342 to: Some("0xNewRecipient".to_string()),
1343 value: U256::from(2000000000000000000u64), data: Some("0xNewData".to_string()),
1345 gas_limit: 25000,
1346 gas_price: Some(30000000000), max_fee_per_gas: Some(40000000000), max_priority_fee_per_gas: Some(2000000000), speed: Some(Speed::Fast),
1350 valid_until: None,
1351 };
1352
1353 let result = EvmTransactionData::for_replacement(&old_data, &new_request);
1354
1355 assert_eq!(result.chain_id, old_data.chain_id);
1357 assert_eq!(result.from, old_data.from);
1358 assert_eq!(result.nonce, old_data.nonce);
1359
1360 assert_eq!(result.to, new_request.to);
1362 assert_eq!(result.value, new_request.value);
1363 assert_eq!(result.data, new_request.data);
1364 assert_eq!(result.gas_limit, new_request.gas_limit);
1365 assert_eq!(result.speed, new_request.speed);
1366
1367 assert_eq!(result.gas_price, None);
1369 assert_eq!(result.max_fee_per_gas, None);
1370 assert_eq!(result.max_priority_fee_per_gas, None);
1371
1372 assert_eq!(result.signature, None);
1374 assert_eq!(result.hash, None);
1375 assert_eq!(result.raw, None);
1376 }
1377
1378 #[test]
1379 fn test_transaction_repo_model_validate() {
1380 let transaction = TransactionRepoModel::default();
1381 let result = transaction.validate();
1382 assert!(result.is_ok());
1383 }
1384
1385 #[test]
1386 fn test_try_from_network_transaction_request_evm() {
1387 use crate::models::{NetworkRepoModel, NetworkType, RelayerRepoModel};
1388
1389 let evm_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1390 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1391 value: U256::from(1000000000000000000u128),
1392 data: Some("0x1234".to_string()),
1393 gas_limit: 21000,
1394 gas_price: Some(20000000000),
1395 max_fee_per_gas: None,
1396 max_priority_fee_per_gas: None,
1397 speed: Some(Speed::Fast),
1398 valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1399 });
1400
1401 let relayer_model = RelayerRepoModel {
1402 id: "relayer-id".to_string(),
1403 name: "Test Relayer".to_string(),
1404 network: "network-id".to_string(),
1405 paused: false,
1406 network_type: NetworkType::Evm,
1407 signer_id: "signer-id".to_string(),
1408 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
1409 address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1410 notification_id: None,
1411 system_disabled: false,
1412 custom_rpc_urls: None,
1413 };
1414
1415 let network_model = NetworkRepoModel {
1416 id: "evm:ethereum".to_string(),
1417 name: "ethereum".to_string(),
1418 network_type: NetworkType::Evm,
1419 config: NetworkConfigData::Evm(EvmNetworkConfig {
1420 common: NetworkConfigCommon {
1421 network: "ethereum".to_string(),
1422 from: None,
1423 rpc_urls: Some(vec!["https://mainnet.infura.io".to_string()]),
1424 explorer_urls: Some(vec!["https://etherscan.io".to_string()]),
1425 average_blocktime_ms: Some(12000),
1426 is_testnet: Some(false),
1427 tags: Some(vec!["mainnet".to_string()]),
1428 },
1429 chain_id: Some(1),
1430 required_confirmations: Some(12),
1431 features: None,
1432 symbol: Some("ETH".to_string()),
1433 }),
1434 };
1435
1436 let result = TransactionRepoModel::try_from((&evm_request, &relayer_model, &network_model));
1437 assert!(result.is_ok());
1438 let transaction = result.unwrap();
1439
1440 assert_eq!(transaction.relayer_id, relayer_model.id);
1441 assert_eq!(transaction.status, TransactionStatus::Pending);
1442 assert_eq!(transaction.network_type, NetworkType::Evm);
1443 assert_eq!(
1444 transaction.valid_until,
1445 Some("2024-12-31T23:59:59Z".to_string())
1446 );
1447 assert!(transaction.is_canceled == Some(false));
1448
1449 if let NetworkTransactionData::Evm(evm_data) = transaction.network_data {
1450 assert_eq!(evm_data.from, relayer_model.address);
1451 assert_eq!(
1452 evm_data.to,
1453 Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string())
1454 );
1455 assert_eq!(evm_data.value, U256::from(1000000000000000000u128));
1456 assert_eq!(evm_data.chain_id, 1);
1457 assert_eq!(evm_data.gas_limit, 21000);
1458 assert_eq!(evm_data.gas_price, Some(20000000000));
1459 assert_eq!(evm_data.speed, Some(Speed::Fast));
1460 } else {
1461 panic!("Expected EVM transaction data");
1462 }
1463 }
1464
1465 #[test]
1466 fn test_try_from_network_transaction_request_solana() {
1467 use crate::models::{
1468 NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1469 };
1470
1471 let solana_request = NetworkTransactionRequest::Solana(
1472 crate::models::transaction::request::solana::SolanaTransactionRequest {
1473 fee_payer: "fee_payer_address".to_string(),
1474 instructions: vec!["instruction1".to_string(), "instruction2".to_string()],
1475 },
1476 );
1477
1478 let relayer_model = RelayerRepoModel {
1479 id: "relayer-id".to_string(),
1480 name: "Test Solana Relayer".to_string(),
1481 network: "network-id".to_string(),
1482 paused: false,
1483 network_type: NetworkType::Solana,
1484 signer_id: "signer-id".to_string(),
1485 policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
1486 address: "solana_address".to_string(),
1487 notification_id: None,
1488 system_disabled: false,
1489 custom_rpc_urls: None,
1490 };
1491
1492 let network_model = NetworkRepoModel {
1493 id: "solana:mainnet".to_string(),
1494 name: "mainnet".to_string(),
1495 network_type: NetworkType::Solana,
1496 config: NetworkConfigData::Solana(SolanaNetworkConfig {
1497 common: NetworkConfigCommon {
1498 network: "mainnet".to_string(),
1499 from: None,
1500 rpc_urls: Some(vec!["https://api.mainnet-beta.solana.com".to_string()]),
1501 explorer_urls: Some(vec!["https://explorer.solana.com".to_string()]),
1502 average_blocktime_ms: Some(400),
1503 is_testnet: Some(false),
1504 tags: Some(vec!["mainnet".to_string()]),
1505 },
1506 }),
1507 };
1508
1509 let result =
1510 TransactionRepoModel::try_from((&solana_request, &relayer_model, &network_model));
1511 assert!(result.is_ok());
1512 let transaction = result.unwrap();
1513
1514 assert_eq!(transaction.relayer_id, relayer_model.id);
1515 assert_eq!(transaction.status, TransactionStatus::Pending);
1516 assert_eq!(transaction.network_type, NetworkType::Solana);
1517 assert_eq!(transaction.valid_until, None);
1518
1519 if let NetworkTransactionData::Solana(solana_data) = transaction.network_data {
1520 assert_eq!(solana_data.fee_payer, "fee_payer_address");
1521 assert_eq!(solana_data.instructions.len(), 2);
1522 assert_eq!(solana_data.instructions[0], "instruction1");
1523 assert_eq!(solana_data.instructions[1], "instruction2");
1524 assert_eq!(solana_data.recent_blockhash, None);
1525 assert_eq!(solana_data.hash, None);
1526 } else {
1527 panic!("Expected Solana transaction data");
1528 }
1529 }
1530
1531 #[test]
1532 fn test_try_from_network_transaction_request_stellar() {
1533 use crate::models::transaction::request::stellar::StellarTransactionRequest;
1534 use crate::models::{
1535 NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1536 };
1537
1538 let stellar_request = NetworkTransactionRequest::Stellar(StellarTransactionRequest {
1539 source_account: Some(
1540 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1541 ),
1542 network: "mainnet".to_string(),
1543 operations: Some(vec![OperationSpec::Payment {
1544 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1545 amount: 1000000,
1546 asset: AssetSpec::Native,
1547 }]),
1548 memo: Some(MemoSpec::Text {
1549 value: "Test memo".to_string(),
1550 }),
1551 valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1552 transaction_xdr: None,
1553 fee_bump: None,
1554 max_fee: None,
1555 });
1556
1557 let relayer_model = RelayerRepoModel {
1558 id: "relayer-id".to_string(),
1559 name: "Test Stellar Relayer".to_string(),
1560 network: "network-id".to_string(),
1561 paused: false,
1562 network_type: NetworkType::Stellar,
1563 signer_id: "signer-id".to_string(),
1564 policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
1565 address: "stellar_address".to_string(),
1566 notification_id: None,
1567 system_disabled: false,
1568 custom_rpc_urls: None,
1569 };
1570
1571 let network_model = NetworkRepoModel {
1572 id: "stellar:mainnet".to_string(),
1573 name: "mainnet".to_string(),
1574 network_type: NetworkType::Stellar,
1575 config: NetworkConfigData::Stellar(StellarNetworkConfig {
1576 common: NetworkConfigCommon {
1577 network: "mainnet".to_string(),
1578 from: None,
1579 rpc_urls: Some(vec!["https://horizon.stellar.org".to_string()]),
1580 explorer_urls: Some(vec!["https://stellarchain.io".to_string()]),
1581 average_blocktime_ms: Some(5000),
1582 is_testnet: Some(false),
1583 tags: Some(vec!["mainnet".to_string()]),
1584 },
1585 passphrase: Some("Public Global Stellar Network ; September 2015".to_string()),
1586 }),
1587 };
1588
1589 let result =
1590 TransactionRepoModel::try_from((&stellar_request, &relayer_model, &network_model));
1591 assert!(result.is_ok());
1592 let transaction = result.unwrap();
1593
1594 assert_eq!(transaction.relayer_id, relayer_model.id);
1595 assert_eq!(transaction.status, TransactionStatus::Pending);
1596 assert_eq!(transaction.network_type, NetworkType::Stellar);
1597 assert_eq!(transaction.valid_until, None);
1598
1599 if let NetworkTransactionData::Stellar(stellar_data) = transaction.network_data {
1600 assert_eq!(
1601 stellar_data.source_account,
1602 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1603 );
1604 if let TransactionInput::Operations(ops) = &stellar_data.transaction_input {
1606 assert_eq!(ops.len(), 1);
1607 if let OperationSpec::Payment {
1608 destination,
1609 amount,
1610 asset,
1611 } = &ops[0]
1612 {
1613 assert_eq!(
1614 destination,
1615 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1616 );
1617 assert_eq!(amount, &1000000);
1618 assert_eq!(asset, &AssetSpec::Native);
1619 } else {
1620 panic!("Expected Payment operation");
1621 }
1622 } else {
1623 panic!("Expected Operations transaction input");
1624 }
1625 assert_eq!(
1626 stellar_data.memo,
1627 Some(MemoSpec::Text {
1628 value: "Test memo".to_string()
1629 })
1630 );
1631 assert_eq!(
1632 stellar_data.valid_until,
1633 Some("2024-12-31T23:59:59Z".to_string())
1634 );
1635 assert_eq!(stellar_data.signatures.len(), 0);
1636 assert_eq!(stellar_data.hash, None);
1637 assert_eq!(stellar_data.fee, None);
1638 assert_eq!(stellar_data.sequence_number, None);
1639 } else {
1640 panic!("Expected Stellar transaction data");
1641 }
1642 }
1643
1644 #[test]
1645 fn test_try_from_network_transaction_data_for_tx_eip1559() {
1646 let mut evm_tx_data = create_sample_evm_tx_data();
1648 evm_tx_data.max_fee_per_gas = Some(30_000_000_000);
1649 evm_tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1650 let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1651
1652 let result = TxEip1559::try_from(network_data);
1654 assert!(result.is_ok());
1655 let tx_eip1559 = result.unwrap();
1656
1657 assert_eq!(tx_eip1559.chain_id, evm_tx_data.chain_id);
1659 assert_eq!(tx_eip1559.nonce, evm_tx_data.nonce.unwrap());
1660 assert_eq!(tx_eip1559.gas_limit, evm_tx_data.gas_limit);
1661 assert_eq!(
1662 tx_eip1559.max_fee_per_gas,
1663 evm_tx_data.max_fee_per_gas.unwrap()
1664 );
1665 assert_eq!(
1666 tx_eip1559.max_priority_fee_per_gas,
1667 evm_tx_data.max_priority_fee_per_gas.unwrap()
1668 );
1669 assert_eq!(tx_eip1559.value, evm_tx_data.value);
1670 assert!(tx_eip1559.access_list.0.is_empty());
1671
1672 let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1674 recent_blockhash: None,
1675 fee_payer: "test".to_string(),
1676 instructions: vec![],
1677 hash: None,
1678 });
1679 assert!(TxEip1559::try_from(solana_data).is_err());
1680 }
1681
1682 #[test]
1683 fn test_evm_transaction_data_defaults() {
1684 let default_data = EvmTransactionData::default();
1685
1686 assert_eq!(
1687 default_data.from,
1688 "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
1689 );
1690 assert_eq!(
1691 default_data.to,
1692 Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string())
1693 );
1694 assert_eq!(default_data.gas_price, Some(20000000000));
1695 assert_eq!(default_data.value, U256::from(1000000000000000000u128));
1696 assert_eq!(default_data.data, Some("0x".to_string()));
1697 assert_eq!(default_data.nonce, Some(1));
1698 assert_eq!(default_data.chain_id, 1);
1699 assert_eq!(default_data.gas_limit, 21000);
1700 assert_eq!(default_data.hash, None);
1701 assert_eq!(default_data.signature, None);
1702 assert_eq!(default_data.speed, None);
1703 assert_eq!(default_data.max_fee_per_gas, None);
1704 assert_eq!(default_data.max_priority_fee_per_gas, None);
1705 assert_eq!(default_data.raw, None);
1706 }
1707
1708 #[test]
1709 fn test_transaction_repo_model_defaults() {
1710 let default_model = TransactionRepoModel::default();
1711
1712 assert_eq!(default_model.id, "00000000-0000-0000-0000-000000000001");
1713 assert_eq!(
1714 default_model.relayer_id,
1715 "00000000-0000-0000-0000-000000000002"
1716 );
1717 assert_eq!(default_model.status, TransactionStatus::Pending);
1718 assert_eq!(default_model.created_at, "2023-01-01T00:00:00Z");
1719 assert_eq!(default_model.status_reason, None);
1720 assert_eq!(default_model.sent_at, None);
1721 assert_eq!(default_model.confirmed_at, None);
1722 assert_eq!(default_model.valid_until, None);
1723 assert_eq!(default_model.network_type, NetworkType::Evm);
1724 assert_eq!(default_model.priced_at, None);
1725 assert_eq!(default_model.hashes.len(), 0);
1726 assert_eq!(default_model.noop_count, None);
1727 assert_eq!(default_model.is_canceled, Some(false));
1728 }
1729
1730 #[test]
1731 fn test_evm_tx_for_replacement_with_speed_fallback() {
1732 let mut old_data = create_sample_evm_tx_data();
1733 old_data.speed = Some(Speed::SafeLow);
1734
1735 let new_request = EvmTransactionRequest {
1737 to: Some("0xNewRecipient".to_string()),
1738 value: U256::from(2000000000000000000u64),
1739 data: Some("0xNewData".to_string()),
1740 gas_limit: 25000,
1741 gas_price: None,
1742 max_fee_per_gas: None,
1743 max_priority_fee_per_gas: None,
1744 speed: None,
1745 valid_until: None,
1746 };
1747
1748 let result = EvmTransactionData::for_replacement(&old_data, &new_request);
1749 assert_eq!(result.speed, Some(Speed::SafeLow));
1750
1751 let mut old_data_no_speed = create_sample_evm_tx_data();
1753 old_data_no_speed.speed = None;
1754
1755 let result2 = EvmTransactionData::for_replacement(&old_data_no_speed, &new_request);
1756 assert_eq!(result2.speed, Some(DEFAULT_TRANSACTION_SPEED));
1757 }
1758
1759 #[test]
1760 fn test_transaction_status_serialization() {
1761 use serde_json;
1762
1763 assert_eq!(
1765 serde_json::to_string(&TransactionStatus::Pending).unwrap(),
1766 "\"pending\""
1767 );
1768 assert_eq!(
1769 serde_json::to_string(&TransactionStatus::Sent).unwrap(),
1770 "\"sent\""
1771 );
1772 assert_eq!(
1773 serde_json::to_string(&TransactionStatus::Mined).unwrap(),
1774 "\"mined\""
1775 );
1776 assert_eq!(
1777 serde_json::to_string(&TransactionStatus::Failed).unwrap(),
1778 "\"failed\""
1779 );
1780 assert_eq!(
1781 serde_json::to_string(&TransactionStatus::Confirmed).unwrap(),
1782 "\"confirmed\""
1783 );
1784 assert_eq!(
1785 serde_json::to_string(&TransactionStatus::Canceled).unwrap(),
1786 "\"canceled\""
1787 );
1788 assert_eq!(
1789 serde_json::to_string(&TransactionStatus::Submitted).unwrap(),
1790 "\"submitted\""
1791 );
1792 assert_eq!(
1793 serde_json::to_string(&TransactionStatus::Expired).unwrap(),
1794 "\"expired\""
1795 );
1796 }
1797
1798 #[test]
1799 fn test_evm_tx_contract_creation() {
1800 let mut tx_data = create_sample_evm_tx_data();
1802 tx_data.to = None;
1803
1804 let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
1805 assert_eq!(tx_legacy.to, TxKind::Create);
1806
1807 let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
1808 assert_eq!(tx_eip1559.to, TxKind::Create);
1809 }
1810
1811 #[test]
1812 fn test_evm_tx_default_values_in_conversion() {
1813 let mut tx_data = create_sample_evm_tx_data();
1815 tx_data.nonce = None;
1816 tx_data.gas_price = None;
1817 tx_data.max_fee_per_gas = None;
1818 tx_data.max_priority_fee_per_gas = None;
1819
1820 let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
1821 assert_eq!(tx_legacy.nonce, 0); assert_eq!(tx_legacy.gas_price, 0); let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
1825 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); }
1829
1830 fn test_models() -> (NetworkRepoModel, RelayerRepoModel) {
1832 use crate::config::{NetworkConfigCommon, StellarNetworkConfig};
1833 use crate::constants::DEFAULT_STELLAR_MIN_BALANCE;
1834
1835 let network_config = NetworkConfigData::Stellar(StellarNetworkConfig {
1836 common: NetworkConfigCommon {
1837 network: "testnet".to_string(),
1838 from: None,
1839 rpc_urls: Some(vec!["https://test.stellar.org".to_string()]),
1840 explorer_urls: None,
1841 average_blocktime_ms: Some(5000), is_testnet: Some(true),
1843 tags: None,
1844 },
1845 passphrase: Some("Test SDF Network ; September 2015".to_string()),
1846 });
1847
1848 let network_model = NetworkRepoModel {
1849 id: "stellar:testnet".to_string(),
1850 name: "testnet".to_string(),
1851 network_type: NetworkType::Stellar,
1852 config: network_config,
1853 };
1854
1855 let relayer_model = RelayerRepoModel {
1856 id: "test-relayer".to_string(),
1857 name: "Test Relayer".to_string(),
1858 network: "stellar:testnet".to_string(),
1859 paused: false,
1860 network_type: NetworkType::Stellar,
1861 signer_id: "test-signer".to_string(),
1862 policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
1863 max_fee: None,
1864 timeout_seconds: None,
1865 min_balance: DEFAULT_STELLAR_MIN_BALANCE,
1866 }),
1867 address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1868 notification_id: None,
1869 system_disabled: false,
1870 custom_rpc_urls: None,
1871 };
1872
1873 (network_model, relayer_model)
1874 }
1875
1876 #[test]
1877 fn test_stellar_xdr_transaction_input_conversion() {
1878 let (network_model, relayer_model) = test_models();
1879
1880 let stellar_request = StellarTransactionRequest {
1882 source_account: Some(
1883 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1884 ),
1885 network: "testnet".to_string(),
1886 operations: Some(vec![OperationSpec::Payment {
1887 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
1888 amount: 1000000,
1889 asset: AssetSpec::Native,
1890 }]),
1891 memo: None,
1892 valid_until: None,
1893 transaction_xdr: None,
1894 fee_bump: None,
1895 max_fee: None,
1896 };
1897
1898 let request = NetworkTransactionRequest::Stellar(stellar_request);
1899 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
1900 assert!(result.is_ok());
1901
1902 let tx_model = result.unwrap();
1903 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
1904 assert!(matches!(
1905 stellar_data.transaction_input,
1906 TransactionInput::Operations(_)
1907 ));
1908 } else {
1909 panic!("Expected Stellar transaction data");
1910 }
1911
1912 let unsigned_xdr = "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAGQAAHAkAAAADgAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=";
1915 let stellar_request = StellarTransactionRequest {
1916 source_account: None,
1917 network: "testnet".to_string(),
1918 operations: Some(vec![]),
1919 memo: None,
1920 valid_until: None,
1921 transaction_xdr: Some(unsigned_xdr.to_string()),
1922 fee_bump: None,
1923 max_fee: None,
1924 };
1925
1926 let request = NetworkTransactionRequest::Stellar(stellar_request);
1927 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
1928 assert!(result.is_ok());
1929
1930 let tx_model = result.unwrap();
1931 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
1932 assert!(matches!(
1933 stellar_data.transaction_input,
1934 TransactionInput::UnsignedXdr(_)
1935 ));
1936 } else {
1937 panic!("Expected Stellar transaction data");
1938 }
1939
1940 let signed_xdr = {
1943 use soroban_rs::xdr::{Limits, TransactionEnvelope, TransactionV1Envelope, WriteXdr};
1944 use stellar_strkey::ed25519::PublicKey;
1945
1946 let source_pk =
1948 PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1949 .unwrap();
1950 let dest_pk =
1951 PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1952 .unwrap();
1953
1954 let payment_op = soroban_rs::xdr::PaymentOp {
1955 destination: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
1956 dest_pk.0,
1957 )),
1958 asset: soroban_rs::xdr::Asset::Native,
1959 amount: 1000000,
1960 };
1961
1962 let operation = soroban_rs::xdr::Operation {
1963 source_account: None,
1964 body: soroban_rs::xdr::OperationBody::Payment(payment_op),
1965 };
1966
1967 let operations: soroban_rs::xdr::VecM<soroban_rs::xdr::Operation, 100> =
1968 vec![operation].try_into().unwrap();
1969
1970 let tx = soroban_rs::xdr::Transaction {
1971 source_account: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
1972 source_pk.0,
1973 )),
1974 fee: 100,
1975 seq_num: soroban_rs::xdr::SequenceNumber(1),
1976 cond: soroban_rs::xdr::Preconditions::None,
1977 memo: soroban_rs::xdr::Memo::None,
1978 operations,
1979 ext: soroban_rs::xdr::TransactionExt::V0,
1980 };
1981
1982 let hint = soroban_rs::xdr::SignatureHint([0; 4]);
1984 let sig_bytes: Vec<u8> = vec![0u8; 64];
1985 let sig_bytes_m: soroban_rs::xdr::BytesM<64> = sig_bytes.try_into().unwrap();
1986 let sig = soroban_rs::xdr::DecoratedSignature {
1987 hint,
1988 signature: soroban_rs::xdr::Signature(sig_bytes_m),
1989 };
1990
1991 let envelope = TransactionV1Envelope {
1992 tx,
1993 signatures: vec![sig].try_into().unwrap(),
1994 };
1995
1996 let tx_envelope = TransactionEnvelope::Tx(envelope);
1997 tx_envelope.to_xdr_base64(Limits::none()).unwrap()
1998 };
1999 let stellar_request = StellarTransactionRequest {
2000 source_account: None,
2001 network: "testnet".to_string(),
2002 operations: Some(vec![]),
2003 memo: None,
2004 valid_until: None,
2005 transaction_xdr: Some(signed_xdr.to_string()),
2006 fee_bump: Some(true),
2007 max_fee: Some(20000000),
2008 };
2009
2010 let request = NetworkTransactionRequest::Stellar(stellar_request);
2011 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2012 assert!(result.is_ok());
2013
2014 let tx_model = result.unwrap();
2015 if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2016 match &stellar_data.transaction_input {
2017 TransactionInput::SignedXdr { xdr, max_fee } => {
2018 assert_eq!(xdr, &signed_xdr);
2019 assert_eq!(*max_fee, 20000000);
2020 }
2021 _ => panic!("Expected SignedXdr transaction input"),
2022 }
2023 } else {
2024 panic!("Expected Stellar transaction data");
2025 }
2026
2027 let stellar_request = StellarTransactionRequest {
2029 source_account: None,
2030 network: "testnet".to_string(),
2031 operations: Some(vec![]),
2032 memo: None,
2033 valid_until: None,
2034 transaction_xdr: Some(signed_xdr.clone()),
2035 fee_bump: None,
2036 max_fee: None,
2037 };
2038
2039 let request = NetworkTransactionRequest::Stellar(stellar_request);
2040 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2041 assert!(result.is_err());
2042 assert!(result
2043 .unwrap_err()
2044 .to_string()
2045 .contains("Expected unsigned XDR but received signed XDR"));
2046
2047 let stellar_request = StellarTransactionRequest {
2049 source_account: Some(
2050 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2051 ),
2052 network: "testnet".to_string(),
2053 operations: Some(vec![OperationSpec::Payment {
2054 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2055 amount: 1000000,
2056 asset: AssetSpec::Native,
2057 }]),
2058 memo: None,
2059 valid_until: None,
2060 transaction_xdr: None,
2061 fee_bump: Some(true),
2062 max_fee: None,
2063 };
2064
2065 let request = NetworkTransactionRequest::Stellar(stellar_request);
2066 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2067 assert!(result.is_err());
2068 assert!(result
2069 .unwrap_err()
2070 .to_string()
2071 .contains("Cannot request fee_bump with operations mode"));
2072 }
2073
2074 #[test]
2075 fn test_invoke_host_function_must_be_exclusive() {
2076 let (network_model, relayer_model) = test_models();
2077
2078 let stellar_request = StellarTransactionRequest {
2080 source_account: Some(
2081 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2082 ),
2083 network: "testnet".to_string(),
2084 operations: Some(vec![OperationSpec::InvokeContract {
2085 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2086 .to_string(),
2087 function_name: "transfer".to_string(),
2088 args: vec![],
2089 auth: None,
2090 }]),
2091 memo: None,
2092 valid_until: None,
2093 transaction_xdr: None,
2094 fee_bump: None,
2095 max_fee: None,
2096 };
2097
2098 let request = NetworkTransactionRequest::Stellar(stellar_request);
2099 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2100 assert!(result.is_ok(), "Single InvokeHostFunction should succeed");
2101
2102 let stellar_request = StellarTransactionRequest {
2104 source_account: Some(
2105 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2106 ),
2107 network: "testnet".to_string(),
2108 operations: Some(vec![
2109 OperationSpec::Payment {
2110 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2111 .to_string(),
2112 amount: 1000,
2113 asset: AssetSpec::Native,
2114 },
2115 OperationSpec::InvokeContract {
2116 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2117 .to_string(),
2118 function_name: "transfer".to_string(),
2119 args: vec![],
2120 auth: None,
2121 },
2122 ]),
2123 memo: None,
2124 valid_until: None,
2125 transaction_xdr: None,
2126 fee_bump: None,
2127 max_fee: None,
2128 };
2129
2130 let request = NetworkTransactionRequest::Stellar(stellar_request);
2131 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2132
2133 match result {
2134 Ok(_) => panic!("Expected Soroban operation mixed with Payment to fail"),
2135 Err(err) => {
2136 let err_str = err.to_string();
2137 assert!(
2138 err_str.contains("Soroban operations must be exclusive"),
2139 "Expected error about Soroban operation exclusivity, got: {}",
2140 err_str
2141 );
2142 }
2143 }
2144
2145 let stellar_request = StellarTransactionRequest {
2147 source_account: Some(
2148 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2149 ),
2150 network: "testnet".to_string(),
2151 operations: Some(vec![
2152 OperationSpec::InvokeContract {
2153 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2154 .to_string(),
2155 function_name: "transfer".to_string(),
2156 args: vec![],
2157 auth: None,
2158 },
2159 OperationSpec::InvokeContract {
2160 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2161 .to_string(),
2162 function_name: "approve".to_string(),
2163 args: vec![],
2164 auth: None,
2165 },
2166 ]),
2167 memo: None,
2168 valid_until: None,
2169 transaction_xdr: None,
2170 fee_bump: None,
2171 max_fee: None,
2172 };
2173
2174 let request = NetworkTransactionRequest::Stellar(stellar_request);
2175 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2176
2177 match result {
2178 Ok(_) => panic!("Expected multiple Soroban operations to fail"),
2179 Err(err) => {
2180 let err_str = err.to_string();
2181 assert!(
2182 err_str.contains("Transaction can contain at most one Soroban operation"),
2183 "Expected error about multiple Soroban operations, got: {}",
2184 err_str
2185 );
2186 }
2187 }
2188
2189 let stellar_request = StellarTransactionRequest {
2191 source_account: Some(
2192 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2193 ),
2194 network: "testnet".to_string(),
2195 operations: Some(vec![
2196 OperationSpec::Payment {
2197 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2198 .to_string(),
2199 amount: 1000,
2200 asset: AssetSpec::Native,
2201 },
2202 OperationSpec::Payment {
2203 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
2204 .to_string(),
2205 amount: 2000,
2206 asset: AssetSpec::Native,
2207 },
2208 ]),
2209 memo: None,
2210 valid_until: None,
2211 transaction_xdr: None,
2212 fee_bump: None,
2213 max_fee: None,
2214 };
2215
2216 let request = NetworkTransactionRequest::Stellar(stellar_request);
2217 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2218 assert!(result.is_ok(), "Multiple Payment operations should succeed");
2219
2220 let stellar_request = StellarTransactionRequest {
2222 source_account: Some(
2223 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2224 ),
2225 network: "testnet".to_string(),
2226 operations: Some(vec![OperationSpec::InvokeContract {
2227 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2228 .to_string(),
2229 function_name: "transfer".to_string(),
2230 args: vec![],
2231 auth: None,
2232 }]),
2233 memo: Some(MemoSpec::Text {
2234 value: "This should fail".to_string(),
2235 }),
2236 valid_until: None,
2237 transaction_xdr: None,
2238 fee_bump: None,
2239 max_fee: None,
2240 };
2241
2242 let request = NetworkTransactionRequest::Stellar(stellar_request);
2243 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2244
2245 match result {
2246 Ok(_) => panic!("Expected InvokeHostFunction with non-None memo to fail"),
2247 Err(err) => {
2248 let err_str = err.to_string();
2249 assert!(
2250 err_str.contains("Soroban operations cannot have a memo"),
2251 "Expected error about memo restriction, got: {}",
2252 err_str
2253 );
2254 }
2255 }
2256
2257 let stellar_request = StellarTransactionRequest {
2259 source_account: Some(
2260 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2261 ),
2262 network: "testnet".to_string(),
2263 operations: Some(vec![OperationSpec::InvokeContract {
2264 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2265 .to_string(),
2266 function_name: "transfer".to_string(),
2267 args: vec![],
2268 auth: None,
2269 }]),
2270 memo: Some(MemoSpec::None),
2271 valid_until: None,
2272 transaction_xdr: None,
2273 fee_bump: None,
2274 max_fee: None,
2275 };
2276
2277 let request = NetworkTransactionRequest::Stellar(stellar_request);
2278 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2279 assert!(
2280 result.is_ok(),
2281 "InvokeHostFunction with MemoSpec::None should succeed"
2282 );
2283
2284 let stellar_request = StellarTransactionRequest {
2286 source_account: Some(
2287 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2288 ),
2289 network: "testnet".to_string(),
2290 operations: Some(vec![OperationSpec::InvokeContract {
2291 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2292 .to_string(),
2293 function_name: "transfer".to_string(),
2294 args: vec![],
2295 auth: None,
2296 }]),
2297 memo: None,
2298 valid_until: None,
2299 transaction_xdr: None,
2300 fee_bump: None,
2301 max_fee: None,
2302 };
2303
2304 let request = NetworkTransactionRequest::Stellar(stellar_request);
2305 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2306 assert!(
2307 result.is_ok(),
2308 "InvokeHostFunction with no memo should succeed"
2309 );
2310
2311 let stellar_request = StellarTransactionRequest {
2313 source_account: Some(
2314 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2315 ),
2316 network: "testnet".to_string(),
2317 operations: Some(vec![OperationSpec::Payment {
2318 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2319 amount: 1000,
2320 asset: AssetSpec::Native,
2321 }]),
2322 memo: Some(MemoSpec::Text {
2323 value: "Payment memo is allowed".to_string(),
2324 }),
2325 valid_until: None,
2326 transaction_xdr: None,
2327 fee_bump: None,
2328 max_fee: None,
2329 };
2330
2331 let request = NetworkTransactionRequest::Stellar(stellar_request);
2332 let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2333 assert!(result.is_ok(), "Payment operation with memo should succeed");
2334 }
2335}