1use crate::{
24 constants::STELLAR_SMALLEST_UNIT_NAME,
25 domain::{
26 transaction::stellar::fetch_next_sequence_from_chain, BalanceResponse, SignDataRequest,
27 SignDataResponse, SignTransactionExternalResponse, SignTransactionExternalResponseStellar,
28 SignTransactionRequest, SignTypedDataRequest,
29 },
30 jobs::{JobProducerTrait, TransactionRequest},
31 models::{
32 produce_relayer_disabled_payload, DeletePendingTransactionsResponse, JsonRpcRequest,
33 JsonRpcResponse, NetworkRepoModel, NetworkRpcRequest, NetworkRpcResult,
34 NetworkTransactionRequest, NetworkType, RelayerRepoModel, RelayerStatus, RepositoryError,
35 StellarNetwork, StellarRpcResult, TransactionRepoModel, TransactionStatus,
36 },
37 repositories::{NetworkRepository, RelayerRepository, Repository, TransactionRepository},
38 services::{
39 StellarProvider, StellarProviderTrait, StellarSignTrait, StellarSigner,
40 TransactionCounterService, TransactionCounterServiceTrait,
41 },
42};
43use async_trait::async_trait;
44use eyre::Result;
45use log::{info, warn};
46use std::sync::Arc;
47
48use crate::domain::relayer::{Relayer, RelayerError};
49
50pub struct StellarRelayerDependencies<RR, NR, TR, J, TCS>
52where
53 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
54 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
55 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
56 J: JobProducerTrait + Send + Sync + 'static,
57 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
58{
59 pub relayer_repository: Arc<RR>,
60 pub network_repository: Arc<NR>,
61 pub transaction_repository: Arc<TR>,
62 pub transaction_counter_service: Arc<TCS>,
63 pub job_producer: Arc<J>,
64}
65
66impl<RR, NR, TR, J, TCS> StellarRelayerDependencies<RR, NR, TR, J, TCS>
67where
68 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
69 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
70 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
71 J: JobProducerTrait + Send + Sync,
72 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
73{
74 pub fn new(
88 relayer_repository: Arc<RR>,
89 network_repository: Arc<NR>,
90 transaction_repository: Arc<TR>,
91 transaction_counter_service: Arc<TCS>,
92 job_producer: Arc<J>,
93 ) -> Self {
94 Self {
95 relayer_repository,
96 network_repository,
97 transaction_repository,
98 transaction_counter_service,
99 job_producer,
100 }
101 }
102}
103
104#[allow(dead_code)]
105pub struct StellarRelayer<P, RR, NR, TR, J, TCS, S>
106where
107 P: StellarProviderTrait + Send + Sync,
108 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
109 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
110 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
111 J: JobProducerTrait + Send + Sync + 'static,
112 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
113 S: StellarSignTrait + Send + Sync + 'static,
114{
115 relayer: RelayerRepoModel,
116 signer: S,
117 network: StellarNetwork,
118 provider: P,
119 relayer_repository: Arc<RR>,
120 network_repository: Arc<NR>,
121 transaction_repository: Arc<TR>,
122 transaction_counter_service: Arc<TCS>,
123 job_producer: Arc<J>,
124}
125
126pub type DefaultStellarRelayer<J, TR, NR, RR, TCR> =
127 StellarRelayer<StellarProvider, RR, NR, TR, J, TransactionCounterService<TCR>, StellarSigner>;
128
129impl<P, RR, NR, TR, J, TCS, S> StellarRelayer<P, RR, NR, TR, J, TCS, S>
130where
131 P: StellarProviderTrait + Send + Sync,
132 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
133 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
134 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
135 J: JobProducerTrait + Send + Sync + 'static,
136 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
137 S: StellarSignTrait + Send + Sync + 'static,
138{
139 #[allow(clippy::too_many_arguments)]
157 pub async fn new(
158 relayer: RelayerRepoModel,
159 signer: S,
160 provider: P,
161 dependencies: StellarRelayerDependencies<RR, NR, TR, J, TCS>,
162 ) -> Result<Self, RelayerError> {
163 let network_repo = dependencies
164 .network_repository
165 .get_by_name(NetworkType::Stellar, &relayer.network)
166 .await
167 .ok()
168 .flatten()
169 .ok_or_else(|| {
170 RelayerError::NetworkConfiguration(format!("Network {} not found", relayer.network))
171 })?;
172
173 let network = StellarNetwork::try_from(network_repo)?;
174
175 Ok(Self {
176 relayer,
177 signer,
178 network,
179 provider,
180 relayer_repository: dependencies.relayer_repository,
181 network_repository: dependencies.network_repository,
182 transaction_repository: dependencies.transaction_repository,
183 transaction_counter_service: dependencies.transaction_counter_service,
184 job_producer: dependencies.job_producer,
185 })
186 }
187
188 async fn sync_sequence(&self) -> Result<(), RelayerError> {
189 info!(
190 "Syncing sequence for relayer: {} ({})",
191 self.relayer.id, self.relayer.address
192 );
193
194 let next = fetch_next_sequence_from_chain(&self.provider, &self.relayer.address)
195 .await
196 .map_err(RelayerError::ProviderError)?;
197
198 info!(
199 "Setting next sequence {} for relayer {}",
200 next, self.relayer.id
201 );
202 self.transaction_counter_service
203 .set(next)
204 .await
205 .map_err(RelayerError::from)?;
206 Ok(())
207 }
208
209 async fn disable_relayer(&self, reasons: &[String]) -> Result<(), RelayerError> {
210 let reason = reasons.join(", ");
211 warn!("Disabling relayer {} due to: {}", self.relayer.id, reason);
212
213 let updated = self
214 .relayer_repository
215 .disable_relayer(self.relayer.id.clone())
216 .await?;
217
218 if let Some(nid) = &self.relayer.notification_id {
219 self.job_producer
220 .produce_send_notification_job(
221 produce_relayer_disabled_payload(nid, &updated, &reason),
222 None,
223 )
224 .await?;
225 }
226 Ok(())
227 }
228}
229
230#[async_trait]
231impl<P, RR, NR, TR, J, TCS, S> Relayer for StellarRelayer<P, RR, NR, TR, J, TCS, S>
232where
233 P: StellarProviderTrait + Send + Sync,
234 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
235 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
236 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
237 J: JobProducerTrait + Send + Sync + 'static,
238 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
239 S: StellarSignTrait + Send + Sync + 'static,
240{
241 async fn process_transaction_request(
242 &self,
243 network_transaction: NetworkTransactionRequest,
244 ) -> Result<TransactionRepoModel, RelayerError> {
245 let network_model = self
246 .network_repository
247 .get_by_name(NetworkType::Stellar, &self.relayer.network)
248 .await?
249 .ok_or_else(|| {
250 RelayerError::NetworkConfiguration(format!(
251 "Network {} not found",
252 self.relayer.network
253 ))
254 })?;
255 let transaction =
256 TransactionRepoModel::try_from((&network_transaction, &self.relayer, &network_model))?;
257
258 self.transaction_repository
259 .create(transaction.clone())
260 .await
261 .map_err(|e| RepositoryError::TransactionFailure(e.to_string()))?;
262
263 self.job_producer
264 .produce_transaction_request_job(
265 TransactionRequest::new(transaction.id.clone(), transaction.relayer_id.clone()),
266 None,
267 )
268 .await?;
269
270 Ok(transaction)
271 }
272
273 async fn get_balance(&self) -> Result<BalanceResponse, RelayerError> {
274 let account_entry = self
275 .provider
276 .get_account(&self.relayer.address)
277 .await
278 .map_err(|e| {
279 RelayerError::ProviderError(format!("Failed to fetch account for balance: {}", e))
280 })?;
281
282 Ok(BalanceResponse {
283 balance: account_entry.balance as u128,
284 unit: STELLAR_SMALLEST_UNIT_NAME.to_string(),
285 })
286 }
287
288 async fn get_status(&self) -> Result<RelayerStatus, RelayerError> {
289 let relayer_model = &self.relayer;
290
291 let account_entry = self
292 .provider
293 .get_account(&relayer_model.address)
294 .await
295 .map_err(|e| {
296 RelayerError::ProviderError(format!("Failed to get account details: {}", e))
297 })?;
298
299 let sequence_number_str = account_entry.seq_num.0.to_string();
300
301 let balance_response = self.get_balance().await?;
302
303 let pending_statuses = [TransactionStatus::Pending, TransactionStatus::Submitted];
304 let pending_transactions = self
305 .transaction_repository
306 .find_by_status(&relayer_model.id, &pending_statuses[..])
307 .await
308 .map_err(RelayerError::from)?;
309 let pending_transactions_count = pending_transactions.len() as u64;
310
311 let confirmed_statuses = [TransactionStatus::Confirmed];
312 let confirmed_transactions = self
313 .transaction_repository
314 .find_by_status(&relayer_model.id, &confirmed_statuses[..])
315 .await
316 .map_err(RelayerError::from)?;
317
318 let last_confirmed_transaction_timestamp = confirmed_transactions
319 .iter()
320 .filter_map(|tx| tx.confirmed_at.as_ref())
321 .max()
322 .cloned();
323
324 Ok(RelayerStatus::Stellar {
325 balance: balance_response.balance.to_string(),
326 pending_transactions_count,
327 last_confirmed_transaction_timestamp,
328 system_disabled: relayer_model.system_disabled,
329 paused: relayer_model.paused,
330 sequence_number: sequence_number_str,
331 })
332 }
333
334 async fn delete_pending_transactions(
335 &self,
336 ) -> Result<DeletePendingTransactionsResponse, RelayerError> {
337 println!("Stellar delete_pending_transactions...");
338 Ok(DeletePendingTransactionsResponse {
339 queued_for_cancellation_transaction_ids: vec![],
340 failed_to_queue_transaction_ids: vec![],
341 total_processed: 0,
342 })
343 }
344
345 async fn sign_data(&self, _request: SignDataRequest) -> Result<SignDataResponse, RelayerError> {
346 Err(RelayerError::NotSupported(
347 "Signing data not supported for Stellar".to_string(),
348 ))
349 }
350
351 async fn sign_typed_data(
352 &self,
353 _request: SignTypedDataRequest,
354 ) -> Result<SignDataResponse, RelayerError> {
355 Err(RelayerError::NotSupported(
356 "Signing typed data not supported for Stellar".to_string(),
357 ))
358 }
359
360 async fn rpc(
361 &self,
362 _request: JsonRpcRequest<NetworkRpcRequest>,
363 ) -> Result<JsonRpcResponse<NetworkRpcResult>, RelayerError> {
364 println!("Stellar rpc...");
365 Ok(JsonRpcResponse {
366 id: None,
367 jsonrpc: "2.0".to_string(),
368 result: Some(NetworkRpcResult::Stellar(
369 StellarRpcResult::GenericRpcResult("".to_string()),
370 )),
371 error: None,
372 })
373 }
374
375 async fn validate_min_balance(&self) -> Result<(), RelayerError> {
376 Ok(())
377 }
378
379 async fn initialize_relayer(&self) -> Result<(), RelayerError> {
380 info!("Initializing Stellar relayer: {}", self.relayer.id);
381
382 let seq_res = self.sync_sequence().await.err();
383
384 let mut failures: Vec<String> = Vec::new();
385 if let Some(e) = seq_res {
386 failures.push(format!("Sequence sync failed: {}", e));
387 }
388
389 if !failures.is_empty() {
390 self.disable_relayer(&failures).await?;
391 return Ok(()); }
393
394 info!(
395 "Stellar relayer initialized successfully: {}",
396 self.relayer.id
397 );
398 Ok(())
399 }
400
401 async fn sign_transaction(
402 &self,
403 request: &SignTransactionRequest,
404 ) -> Result<SignTransactionExternalResponse, RelayerError> {
405 let stellar_req = match request {
406 SignTransactionRequest::Stellar(req) => req,
407 _ => {
408 return Err(RelayerError::NotSupported(
409 "Invalid request type for Stellar relayer".to_string(),
410 ))
411 }
412 };
413
414 let response = self
416 .signer
417 .sign_xdr_transaction(&stellar_req.unsigned_xdr, &self.network.passphrase)
418 .await
419 .map_err(RelayerError::SignerError)?;
420
421 let signature_bytes = &response.signature.signature.0;
423 let signature_string =
424 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature_bytes);
425
426 Ok(SignTransactionExternalResponse::Stellar(
427 SignTransactionExternalResponseStellar {
428 signed_xdr: response.signed_xdr,
429 signature: signature_string,
430 },
431 ))
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::{
439 config::{NetworkConfigCommon, StellarNetworkConfig},
440 constants::STELLAR_SMALLEST_UNIT_NAME,
441 domain::{SignTransactionRequestStellar, SignXdrTransactionResponseStellar},
442 jobs::MockJobProducerTrait,
443 models::{
444 NetworkConfigData, NetworkRepoModel, NetworkType, RelayerNetworkPolicy,
445 RelayerRepoModel, RelayerStellarPolicy, SignerError,
446 },
447 repositories::{
448 InMemoryNetworkRepository, MockRelayerRepository, MockTransactionRepository,
449 },
450 services::{
451 MockStellarProviderTrait, MockStellarSignTrait, MockTransactionCounterServiceTrait,
452 },
453 };
454 use eyre::eyre;
455 use mockall::predicate::*;
456 use soroban_rs::xdr::{
457 AccountEntry, AccountEntryExt, AccountId, DecoratedSignature, PublicKey, SequenceNumber,
458 Signature, SignatureHint, String32, Thresholds, Uint256, VecM,
459 };
460 use std::future::ready;
461 use std::sync::Arc;
462
463 struct TestCtx {
465 relayer_model: RelayerRepoModel,
466 network_repository: Arc<InMemoryNetworkRepository>,
467 }
468
469 impl Default for TestCtx {
470 fn default() -> Self {
471 let network_repository = Arc::new(InMemoryNetworkRepository::new());
472
473 let relayer_model = RelayerRepoModel {
474 id: "test-relayer-id".to_string(),
475 name: "Test Relayer".to_string(),
476 network: "testnet".to_string(),
477 paused: false,
478 network_type: NetworkType::Stellar,
479 signer_id: "signer-id".to_string(),
480 policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
481 address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
482 notification_id: Some("notification-id".to_string()),
483 system_disabled: false,
484 custom_rpc_urls: None,
485 };
486
487 TestCtx {
488 relayer_model,
489 network_repository,
490 }
491 }
492 }
493
494 impl TestCtx {
495 async fn setup_network(&self) {
496 let test_network = NetworkRepoModel {
497 id: "stellar:testnet".to_string(),
498 name: "testnet".to_string(),
499 network_type: NetworkType::Stellar,
500 config: NetworkConfigData::Stellar(StellarNetworkConfig {
501 common: NetworkConfigCommon {
502 network: "testnet".to_string(),
503 from: None,
504 rpc_urls: Some(vec!["https://horizon-testnet.stellar.org".to_string()]),
505 explorer_urls: None,
506 average_blocktime_ms: Some(5000),
507 is_testnet: Some(true),
508 tags: None,
509 },
510 passphrase: Some("Test SDF Network ; September 2015".to_string()),
511 }),
512 };
513
514 self.network_repository.create(test_network).await.unwrap();
515 }
516 }
517
518 #[tokio::test]
519 async fn test_sync_sequence_success() {
520 let ctx = TestCtx::default();
521 ctx.setup_network().await;
522 let relayer_model = ctx.relayer_model.clone();
523 let mut provider = MockStellarProviderTrait::new();
524 provider
525 .expect_get_account()
526 .with(eq(relayer_model.address.clone()))
527 .returning(|_| {
528 Box::pin(async {
529 Ok(AccountEntry {
530 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
531 balance: 0,
532 ext: AccountEntryExt::V0,
533 flags: 0,
534 home_domain: String32::default(),
535 inflation_dest: None,
536 seq_num: SequenceNumber(5),
537 num_sub_entries: 0,
538 signers: VecM::default(),
539 thresholds: Thresholds([0, 0, 0, 0]),
540 })
541 })
542 });
543 let mut counter = MockTransactionCounterServiceTrait::new();
544 counter
545 .expect_set()
546 .with(eq(6u64))
547 .returning(|_| Box::pin(async { Ok(()) }));
548 let relayer_repo = MockRelayerRepository::new();
549 let tx_repo = MockTransactionRepository::new();
550 let job_producer = MockJobProducerTrait::new();
551 let signer = MockStellarSignTrait::new();
552
553 let relayer = StellarRelayer::new(
554 relayer_model.clone(),
555 signer,
556 provider,
557 StellarRelayerDependencies::new(
558 Arc::new(relayer_repo),
559 ctx.network_repository.clone(),
560 Arc::new(tx_repo),
561 Arc::new(counter),
562 Arc::new(job_producer),
563 ),
564 )
565 .await
566 .unwrap();
567
568 let result = relayer.sync_sequence().await;
569 assert!(result.is_ok());
570 }
571
572 #[tokio::test]
573 async fn test_sync_sequence_provider_error() {
574 let ctx = TestCtx::default();
575 ctx.setup_network().await;
576 let relayer_model = ctx.relayer_model.clone();
577 let mut provider = MockStellarProviderTrait::new();
578 provider
579 .expect_get_account()
580 .with(eq(relayer_model.address.clone()))
581 .returning(|_| Box::pin(async { Err(eyre!("fail")) }));
582 let counter = MockTransactionCounterServiceTrait::new();
583 let relayer_repo = MockRelayerRepository::new();
584 let tx_repo = MockTransactionRepository::new();
585 let job_producer = MockJobProducerTrait::new();
586 let signer = MockStellarSignTrait::new();
587
588 let relayer = StellarRelayer::new(
589 relayer_model.clone(),
590 signer,
591 provider,
592 StellarRelayerDependencies::new(
593 Arc::new(relayer_repo),
594 ctx.network_repository.clone(),
595 Arc::new(tx_repo),
596 Arc::new(counter),
597 Arc::new(job_producer),
598 ),
599 )
600 .await
601 .unwrap();
602
603 let result = relayer.sync_sequence().await;
604 assert!(matches!(result, Err(RelayerError::ProviderError(_))));
605 }
606
607 #[tokio::test]
608 async fn test_disable_relayer() {
609 let ctx = TestCtx::default();
610 ctx.setup_network().await;
611 let relayer_model = ctx.relayer_model.clone();
612 let provider = MockStellarProviderTrait::new();
613 let mut relayer_repo = MockRelayerRepository::new();
614 let mut updated_model = relayer_model.clone();
615 updated_model.system_disabled = true;
616 relayer_repo
617 .expect_disable_relayer()
618 .with(eq(relayer_model.id.clone()))
619 .returning(move |_| Ok::<RelayerRepoModel, RepositoryError>(updated_model.clone()));
620 let mut job_producer = MockJobProducerTrait::new();
621 job_producer
622 .expect_produce_send_notification_job()
623 .returning(|_, _| Box::pin(async { Ok(()) }));
624 let tx_repo = MockTransactionRepository::new();
625 let counter = MockTransactionCounterServiceTrait::new();
626 let signer = MockStellarSignTrait::new();
627
628 let relayer = StellarRelayer::new(
629 relayer_model.clone(),
630 signer,
631 provider,
632 StellarRelayerDependencies::new(
633 Arc::new(relayer_repo),
634 ctx.network_repository.clone(),
635 Arc::new(tx_repo),
636 Arc::new(counter),
637 Arc::new(job_producer),
638 ),
639 )
640 .await
641 .unwrap();
642
643 let reasons = vec!["reason1".to_string(), "reason2".to_string()];
644 let result = relayer.disable_relayer(&reasons).await;
645 assert!(result.is_ok());
646 }
647
648 #[tokio::test]
649 async fn test_get_status_success_stellar() {
650 let ctx = TestCtx::default();
651 ctx.setup_network().await;
652 let relayer_model = ctx.relayer_model.clone();
653 let mut provider_mock = MockStellarProviderTrait::new();
654 let mut tx_repo_mock = MockTransactionRepository::new();
655 let relayer_repo_mock = MockRelayerRepository::new();
656 let job_producer_mock = MockJobProducerTrait::new();
657 let counter_mock = MockTransactionCounterServiceTrait::new();
658
659 provider_mock.expect_get_account().times(2).returning(|_| {
660 Box::pin(ready(Ok(AccountEntry {
661 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
662 balance: 10000000,
663 seq_num: SequenceNumber(12345),
664 ext: AccountEntryExt::V0,
665 flags: 0,
666 home_domain: String32::default(),
667 inflation_dest: None,
668 num_sub_entries: 0,
669 signers: VecM::default(),
670 thresholds: Thresholds([0, 0, 0, 0]),
671 })))
672 });
673
674 tx_repo_mock
675 .expect_find_by_status()
676 .withf(|relayer_id, statuses| {
677 relayer_id == "test-relayer-id"
678 && statuses == [TransactionStatus::Pending, TransactionStatus::Submitted]
679 })
680 .returning(|_, _| Ok(vec![]) as Result<Vec<TransactionRepoModel>, RepositoryError>)
681 .once();
682
683 let confirmed_tx = TransactionRepoModel {
684 id: "tx1_stellar".to_string(),
685 relayer_id: relayer_model.id.clone(),
686 status: TransactionStatus::Confirmed,
687 confirmed_at: Some("2023-02-01T12:00:00Z".to_string()),
688 ..TransactionRepoModel::default()
689 };
690 tx_repo_mock
691 .expect_find_by_status()
692 .withf(|relayer_id, statuses| {
693 relayer_id == "test-relayer-id" && statuses == [TransactionStatus::Confirmed]
694 })
695 .returning(move |_, _| {
696 Ok(vec![confirmed_tx.clone()]) as Result<Vec<TransactionRepoModel>, RepositoryError>
697 })
698 .once();
699 let signer = MockStellarSignTrait::new();
700
701 let stellar_relayer = StellarRelayer::new(
702 relayer_model.clone(),
703 signer,
704 provider_mock,
705 StellarRelayerDependencies::new(
706 Arc::new(relayer_repo_mock),
707 ctx.network_repository.clone(),
708 Arc::new(tx_repo_mock),
709 Arc::new(counter_mock),
710 Arc::new(job_producer_mock),
711 ),
712 )
713 .await
714 .unwrap();
715
716 let status = stellar_relayer.get_status().await.unwrap();
717
718 match status {
719 RelayerStatus::Stellar {
720 balance,
721 pending_transactions_count,
722 last_confirmed_transaction_timestamp,
723 system_disabled,
724 paused,
725 sequence_number,
726 } => {
727 assert_eq!(balance, "10000000");
728 assert_eq!(pending_transactions_count, 0);
729 assert_eq!(
730 last_confirmed_transaction_timestamp,
731 Some("2023-02-01T12:00:00Z".to_string())
732 );
733 assert_eq!(system_disabled, relayer_model.system_disabled);
734 assert_eq!(paused, relayer_model.paused);
735 assert_eq!(sequence_number, "12345");
736 }
737 _ => panic!("Expected Stellar RelayerStatus"),
738 }
739 }
740
741 #[tokio::test]
742 async fn test_get_status_stellar_provider_error() {
743 let ctx = TestCtx::default();
744 ctx.setup_network().await;
745 let relayer_model = ctx.relayer_model.clone();
746 let mut provider_mock = MockStellarProviderTrait::new();
747 let tx_repo_mock = MockTransactionRepository::new();
748 let relayer_repo_mock = MockRelayerRepository::new();
749 let job_producer_mock = MockJobProducerTrait::new();
750 let counter_mock = MockTransactionCounterServiceTrait::new();
751
752 provider_mock
753 .expect_get_account()
754 .with(eq(relayer_model.address.clone()))
755 .returning(|_| Box::pin(async { Err(eyre!("Stellar provider down")) }));
756 let signer = MockStellarSignTrait::new();
757
758 let stellar_relayer = StellarRelayer::new(
759 relayer_model.clone(),
760 signer,
761 provider_mock,
762 StellarRelayerDependencies::new(
763 Arc::new(relayer_repo_mock),
764 ctx.network_repository.clone(),
765 Arc::new(tx_repo_mock),
766 Arc::new(counter_mock),
767 Arc::new(job_producer_mock),
768 ),
769 )
770 .await
771 .unwrap();
772
773 let result = stellar_relayer.get_status().await;
774 assert!(result.is_err());
775 match result.err().unwrap() {
776 RelayerError::ProviderError(msg) => {
777 assert!(msg.contains("Failed to get account details"))
778 }
779 _ => panic!("Expected ProviderError for get_account failure"),
780 }
781 }
782
783 #[tokio::test]
784 async fn test_get_balance_success() {
785 let ctx = TestCtx::default();
786 ctx.setup_network().await;
787 let relayer_model = ctx.relayer_model.clone();
788 let mut provider = MockStellarProviderTrait::new();
789 let expected_balance = 100_000_000i64; provider
792 .expect_get_account()
793 .with(eq(relayer_model.address.clone()))
794 .returning(move |_| {
795 Box::pin(async move {
796 Ok(AccountEntry {
797 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
798 balance: expected_balance,
799 ext: AccountEntryExt::V0,
800 flags: 0,
801 home_domain: String32::default(),
802 inflation_dest: None,
803 seq_num: SequenceNumber(5),
804 num_sub_entries: 0,
805 signers: VecM::default(),
806 thresholds: Thresholds([0, 0, 0, 0]),
807 })
808 })
809 });
810
811 let relayer_repo = Arc::new(MockRelayerRepository::new());
812 let tx_repo = Arc::new(MockTransactionRepository::new());
813 let job_producer = Arc::new(MockJobProducerTrait::new());
814 let counter = Arc::new(MockTransactionCounterServiceTrait::new());
815 let signer = MockStellarSignTrait::new();
816
817 let relayer = StellarRelayer::new(
818 relayer_model,
819 signer,
820 provider,
821 StellarRelayerDependencies::new(
822 relayer_repo,
823 ctx.network_repository.clone(),
824 tx_repo,
825 counter,
826 job_producer,
827 ),
828 )
829 .await
830 .unwrap();
831
832 let result = relayer.get_balance().await;
833 assert!(result.is_ok());
834 let balance_response = result.unwrap();
835 assert_eq!(balance_response.balance, expected_balance as u128);
836 assert_eq!(balance_response.unit, STELLAR_SMALLEST_UNIT_NAME);
837 }
838
839 #[tokio::test]
840 async fn test_get_balance_provider_error() {
841 let ctx = TestCtx::default();
842 ctx.setup_network().await;
843 let relayer_model = ctx.relayer_model.clone();
844 let mut provider = MockStellarProviderTrait::new();
845
846 provider
847 .expect_get_account()
848 .with(eq(relayer_model.address.clone()))
849 .returning(|_| Box::pin(async { Err(eyre!("provider failed")) }));
850
851 let relayer_repo = Arc::new(MockRelayerRepository::new());
852 let tx_repo = Arc::new(MockTransactionRepository::new());
853 let job_producer = Arc::new(MockJobProducerTrait::new());
854 let counter = Arc::new(MockTransactionCounterServiceTrait::new());
855 let signer = MockStellarSignTrait::new();
856
857 let relayer = StellarRelayer::new(
858 relayer_model,
859 signer,
860 provider,
861 StellarRelayerDependencies::new(
862 relayer_repo,
863 ctx.network_repository.clone(),
864 tx_repo,
865 counter,
866 job_producer,
867 ),
868 )
869 .await
870 .unwrap();
871
872 let result = relayer.get_balance().await;
873 assert!(result.is_err());
874 match result.err().unwrap() {
875 RelayerError::ProviderError(msg) => {
876 assert!(msg.contains("Failed to fetch account for balance: provider failed"));
877 }
878 _ => panic!("Unexpected error type"),
879 }
880 }
881
882 #[tokio::test]
883 async fn test_sign_transaction_success() {
884 let ctx = TestCtx::default();
885 ctx.setup_network().await;
886 let relayer_model = ctx.relayer_model.clone();
887 let provider = MockStellarProviderTrait::new();
888 let mut signer = MockStellarSignTrait::new();
889
890 let unsigned_xdr = "AAAAAgAAAAD///8AAAAAAAAAAQAAAAAAAAACAAAAAQAAAAAAAAAB";
891 let expected_signed_xdr =
892 "AAAAAgAAAAD///8AAAAAAAABAAAAAAAAAAIAAAABAAAAAAAAAAEAAAABAAAAA...";
893 let expected_signature = DecoratedSignature {
894 hint: SignatureHint([1, 2, 3, 4]),
895 signature: Signature([5u8; 64].try_into().unwrap()),
896 };
897 let expected_signature_for_closure = expected_signature.clone();
898
899 signer
900 .expect_sign_xdr_transaction()
901 .with(eq(unsigned_xdr), eq("Test SDF Network ; September 2015"))
902 .returning(move |_, _| {
903 Ok(SignXdrTransactionResponseStellar {
904 signed_xdr: expected_signed_xdr.to_string(),
905 signature: expected_signature_for_closure.clone(),
906 })
907 });
908
909 let relayer_repo = Arc::new(MockRelayerRepository::new());
910 let tx_repo = Arc::new(MockTransactionRepository::new());
911 let job_producer = Arc::new(MockJobProducerTrait::new());
912 let counter = Arc::new(MockTransactionCounterServiceTrait::new());
913
914 let relayer = StellarRelayer::new(
915 relayer_model,
916 signer,
917 provider,
918 StellarRelayerDependencies::new(
919 relayer_repo,
920 ctx.network_repository.clone(),
921 tx_repo,
922 counter,
923 job_producer,
924 ),
925 )
926 .await
927 .unwrap();
928
929 let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
930 unsigned_xdr: unsigned_xdr.to_string(),
931 });
932 let result = relayer.sign_transaction(&request).await;
933 assert!(result.is_ok());
934
935 match result.unwrap() {
936 SignTransactionExternalResponse::Stellar(response) => {
937 assert_eq!(response.signed_xdr, expected_signed_xdr);
938 let expected_signature_base64 = base64::Engine::encode(
940 &base64::engine::general_purpose::STANDARD,
941 &expected_signature.signature.0,
942 );
943 assert_eq!(response.signature, expected_signature_base64);
944 }
945 _ => panic!("Expected Stellar response"),
946 }
947 }
948
949 #[tokio::test]
950 async fn test_sign_transaction_signer_error() {
951 let ctx = TestCtx::default();
952 ctx.setup_network().await;
953 let relayer_model = ctx.relayer_model.clone();
954 let provider = MockStellarProviderTrait::new();
955 let mut signer = MockStellarSignTrait::new();
956
957 let unsigned_xdr = "INVALID_XDR";
958
959 signer
960 .expect_sign_xdr_transaction()
961 .with(eq(unsigned_xdr), eq("Test SDF Network ; September 2015"))
962 .returning(|_, _| Err(SignerError::SigningError("Invalid XDR format".to_string())));
963
964 let relayer_repo = Arc::new(MockRelayerRepository::new());
965 let tx_repo = Arc::new(MockTransactionRepository::new());
966 let job_producer = Arc::new(MockJobProducerTrait::new());
967 let counter = Arc::new(MockTransactionCounterServiceTrait::new());
968
969 let relayer = StellarRelayer::new(
970 relayer_model,
971 signer,
972 provider,
973 StellarRelayerDependencies::new(
974 relayer_repo,
975 ctx.network_repository.clone(),
976 tx_repo,
977 counter,
978 job_producer,
979 ),
980 )
981 .await
982 .unwrap();
983
984 let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
985 unsigned_xdr: unsigned_xdr.to_string(),
986 });
987 let result = relayer.sign_transaction(&request).await;
988 assert!(result.is_err());
989
990 match result.err().unwrap() {
991 RelayerError::SignerError(err) => match err {
992 SignerError::SigningError(msg) => {
993 assert_eq!(msg, "Invalid XDR format");
994 }
995 _ => panic!("Expected SigningError"),
996 },
997 _ => panic!("Expected RelayerError::SignerError"),
998 }
999 }
1000
1001 #[tokio::test]
1002 async fn test_sign_transaction_with_different_network_passphrase() {
1003 let ctx = TestCtx::default();
1004 let custom_network = NetworkRepoModel {
1006 id: "stellar:mainnet".to_string(),
1007 name: "mainnet".to_string(),
1008 network_type: NetworkType::Stellar,
1009 config: NetworkConfigData::Stellar(StellarNetworkConfig {
1010 common: NetworkConfigCommon {
1011 network: "mainnet".to_string(),
1012 from: None,
1013 rpc_urls: Some(vec!["https://horizon.stellar.org".to_string()]),
1014 explorer_urls: None,
1015 average_blocktime_ms: Some(5000),
1016 is_testnet: Some(false),
1017 tags: None,
1018 },
1019 passphrase: Some("Public Global Stellar Network ; September 2015".to_string()),
1020 }),
1021 };
1022 ctx.network_repository.create(custom_network).await.unwrap();
1023
1024 let mut relayer_model = ctx.relayer_model.clone();
1025 relayer_model.network = "mainnet".to_string();
1026
1027 let provider = MockStellarProviderTrait::new();
1028 let mut signer = MockStellarSignTrait::new();
1029
1030 let unsigned_xdr = "AAAAAgAAAAD///8AAAAAAAAAAQAAAAAAAAACAAAAAQAAAAAAAAAB";
1031 let expected_signature = DecoratedSignature {
1032 hint: SignatureHint([10, 20, 30, 40]),
1033 signature: Signature([15u8; 64].try_into().unwrap()),
1034 };
1035 let expected_signature_for_closure = expected_signature.clone();
1036
1037 signer
1038 .expect_sign_xdr_transaction()
1039 .with(
1040 eq(unsigned_xdr),
1041 eq("Public Global Stellar Network ; September 2015"),
1042 )
1043 .returning(move |_, _| {
1044 Ok(SignXdrTransactionResponseStellar {
1045 signed_xdr: "mainnet_signed_xdr".to_string(),
1046 signature: expected_signature_for_closure.clone(),
1047 })
1048 });
1049
1050 let relayer_repo = Arc::new(MockRelayerRepository::new());
1051 let tx_repo = Arc::new(MockTransactionRepository::new());
1052 let job_producer = Arc::new(MockJobProducerTrait::new());
1053 let counter = Arc::new(MockTransactionCounterServiceTrait::new());
1054
1055 let relayer = StellarRelayer::new(
1056 relayer_model,
1057 signer,
1058 provider,
1059 StellarRelayerDependencies::new(
1060 relayer_repo,
1061 ctx.network_repository.clone(),
1062 tx_repo,
1063 counter,
1064 job_producer,
1065 ),
1066 )
1067 .await
1068 .unwrap();
1069
1070 let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
1071 unsigned_xdr: unsigned_xdr.to_string(),
1072 });
1073 let result = relayer.sign_transaction(&request).await;
1074 assert!(result.is_ok());
1075
1076 match result.unwrap() {
1077 SignTransactionExternalResponse::Stellar(response) => {
1078 assert_eq!(response.signed_xdr, "mainnet_signed_xdr");
1079 let expected_signature_string = base64::Engine::encode(
1081 &base64::engine::general_purpose::STANDARD,
1082 &expected_signature.signature.0,
1083 );
1084 assert_eq!(response.signature, expected_signature_string);
1085 }
1086 _ => panic!("Expected Stellar response"),
1087 }
1088 }
1089}