1use actix_web::web::ThinData;
12use serde::{Deserialize, Serialize};
13use std::sync::Arc;
14use tracing::instrument;
15use utoipa::ToSchema;
16
17#[cfg(test)]
18use mockall::automock;
19
20use crate::{
21 jobs::JobProducerTrait,
22 models::{
23 transaction::request::{
24 SponsoredTransactionBuildRequest, SponsoredTransactionQuoteRequest,
25 },
26 AppState, DecoratedSignature, DeletePendingTransactionsResponse,
27 EncodedSerializedTransaction, EvmNetwork, EvmTransactionDataSignature, JsonRpcRequest,
28 JsonRpcResponse, NetworkRepoModel, NetworkRpcRequest, NetworkRpcResult,
29 NetworkTransactionRequest, NetworkType, NotificationRepoModel, RelayerError,
30 RelayerRepoModel, RelayerStatus, SignerRepoModel, SponsoredTransactionBuildResponse,
31 SponsoredTransactionQuoteResponse, TransactionError, TransactionRepoModel,
32 },
33 repositories::{
34 ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
35 Repository, TransactionCounterTrait, TransactionRepository,
36 },
37 services::{
38 provider::get_network_provider, signer::EvmSignerFactory, TransactionCounterService,
39 },
40};
41
42use async_trait::async_trait;
43use eyre::Result;
44
45mod evm;
46mod solana;
47mod stellar;
48mod util;
49
50pub use evm::*;
51pub use solana::*;
52pub use stellar::*;
53pub use util::*;
54
55pub use solana::SwapResult;
57
58#[async_trait]
62#[cfg_attr(test, automock)]
63#[allow(dead_code)]
64pub trait Relayer {
65 async fn process_transaction_request(
76 &self,
77 tx_request: NetworkTransactionRequest,
78 ) -> Result<TransactionRepoModel, RelayerError>;
79
80 async fn get_balance(&self) -> Result<BalanceResponse, RelayerError>;
87
88 async fn delete_pending_transactions(
95 &self,
96 ) -> Result<DeletePendingTransactionsResponse, RelayerError>;
97
98 async fn sign_data(&self, request: SignDataRequest) -> Result<SignDataResponse, RelayerError>;
109
110 async fn sign_typed_data(
121 &self,
122 request: SignTypedDataRequest,
123 ) -> Result<SignDataResponse, RelayerError>;
124
125 async fn rpc(
136 &self,
137 request: JsonRpcRequest<NetworkRpcRequest>,
138 ) -> Result<JsonRpcResponse<NetworkRpcResult>, RelayerError>;
139
140 async fn get_status(&self) -> Result<RelayerStatus, RelayerError>;
147
148 async fn initialize_relayer(&self) -> Result<(), RelayerError>;
154
155 async fn check_health(&self) -> Result<(), Vec<crate::models::HealthCheckFailure>>;
165
166 async fn validate_min_balance(&self) -> Result<(), RelayerError>;
172
173 async fn sign_transaction(
184 &self,
185 request: &SignTransactionRequest,
186 ) -> Result<SignTransactionExternalResponse, RelayerError>;
187
188 async fn handle_health_action(
193 &self,
194 _metadata: &std::collections::HashMap<String, String>,
195 ) -> Result<bool, RelayerError> {
196 Ok(false)
197 }
198}
199
200#[async_trait]
203#[allow(dead_code)]
204#[cfg_attr(test, automock)]
205pub trait SolanaRelayerDexTrait {
206 async fn handle_token_swap_request(
208 &self,
209 relayer_id: String,
210 ) -> Result<Vec<SwapResult>, RelayerError>;
211}
212
213#[async_trait]
215#[allow(dead_code)]
216#[cfg_attr(test, automock)]
217pub trait StellarRelayerDexTrait {
218 async fn handle_token_swap_request(
220 &self,
221 relayer_id: String,
222 ) -> Result<Vec<SwapResult>, RelayerError>;
223}
224
225#[async_trait]
230#[allow(dead_code)]
231#[cfg_attr(test, automock)]
232pub trait GasAbstractionTrait {
233 async fn quote_sponsored_transaction(
243 &self,
244 params: SponsoredTransactionQuoteRequest,
245 ) -> Result<SponsoredTransactionQuoteResponse, RelayerError>;
246
247 async fn build_sponsored_transaction(
257 &self,
258 params: SponsoredTransactionBuildRequest,
259 ) -> Result<SponsoredTransactionBuildResponse, RelayerError>;
260}
261
262pub enum NetworkRelayer<
263 J: JobProducerTrait + 'static,
264 T: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
265 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
266 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
267 TCR: TransactionCounterTrait + Send + Sync + 'static,
268> {
269 Evm(Box<DefaultEvmRelayer<J, T, RR, NR, TCR>>),
270 Solana(DefaultSolanaRelayer<J, T, RR, NR>),
271 Stellar(DefaultStellarRelayer<J, T, NR, RR, TCR>),
272}
273
274#[async_trait]
275impl<
276 J: JobProducerTrait + 'static,
277 T: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
278 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
279 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
280 TCR: TransactionCounterTrait + Send + Sync + 'static,
281 > Relayer for NetworkRelayer<J, T, RR, NR, TCR>
282{
283 async fn process_transaction_request(
284 &self,
285 tx_request: NetworkTransactionRequest,
286 ) -> Result<TransactionRepoModel, RelayerError> {
287 match self {
288 NetworkRelayer::Evm(relayer) => relayer.process_transaction_request(tx_request).await,
289 NetworkRelayer::Solana(relayer) => {
290 relayer.process_transaction_request(tx_request).await
291 }
292 NetworkRelayer::Stellar(relayer) => {
293 relayer.process_transaction_request(tx_request).await
294 }
295 }
296 }
297
298 async fn get_balance(&self) -> Result<BalanceResponse, RelayerError> {
299 match self {
300 NetworkRelayer::Evm(relayer) => relayer.get_balance().await,
301 NetworkRelayer::Solana(relayer) => relayer.get_balance().await,
302 NetworkRelayer::Stellar(relayer) => relayer.get_balance().await,
303 }
304 }
305
306 async fn delete_pending_transactions(
307 &self,
308 ) -> Result<DeletePendingTransactionsResponse, RelayerError> {
309 match self {
310 NetworkRelayer::Evm(relayer) => relayer.delete_pending_transactions().await,
311 NetworkRelayer::Solana(_) => solana_not_supported_relayer(),
312 NetworkRelayer::Stellar(relayer) => relayer.delete_pending_transactions().await,
313 }
314 }
315
316 async fn sign_data(&self, request: SignDataRequest) -> Result<SignDataResponse, RelayerError> {
317 match self {
318 NetworkRelayer::Evm(relayer) => relayer.sign_data(request).await,
319 NetworkRelayer::Solana(_) => solana_not_supported_relayer(),
320 NetworkRelayer::Stellar(relayer) => relayer.sign_data(request).await,
321 }
322 }
323
324 async fn sign_typed_data(
325 &self,
326 request: SignTypedDataRequest,
327 ) -> Result<SignDataResponse, RelayerError> {
328 match self {
329 NetworkRelayer::Evm(relayer) => relayer.sign_typed_data(request).await,
330 NetworkRelayer::Solana(_) => solana_not_supported_relayer(),
331 NetworkRelayer::Stellar(relayer) => relayer.sign_typed_data(request).await,
332 }
333 }
334
335 async fn rpc(
336 &self,
337 request: JsonRpcRequest<NetworkRpcRequest>,
338 ) -> Result<JsonRpcResponse<NetworkRpcResult>, RelayerError> {
339 match self {
340 NetworkRelayer::Evm(relayer) => relayer.rpc(request).await,
341 NetworkRelayer::Solana(relayer) => relayer.rpc(request).await,
342 NetworkRelayer::Stellar(relayer) => relayer.rpc(request).await,
343 }
344 }
345
346 async fn get_status(&self) -> Result<RelayerStatus, RelayerError> {
347 match self {
348 NetworkRelayer::Evm(relayer) => relayer.get_status().await,
349 NetworkRelayer::Solana(relayer) => relayer.get_status().await,
350 NetworkRelayer::Stellar(relayer) => relayer.get_status().await,
351 }
352 }
353
354 async fn validate_min_balance(&self) -> Result<(), RelayerError> {
355 match self {
356 NetworkRelayer::Evm(relayer) => relayer.validate_min_balance().await,
357 NetworkRelayer::Solana(relayer) => relayer.validate_min_balance().await,
358 NetworkRelayer::Stellar(relayer) => relayer.validate_min_balance().await,
359 }
360 }
361
362 async fn initialize_relayer(&self) -> Result<(), RelayerError> {
363 match self {
364 NetworkRelayer::Evm(relayer) => relayer.initialize_relayer().await,
365 NetworkRelayer::Solana(relayer) => relayer.initialize_relayer().await,
366 NetworkRelayer::Stellar(relayer) => relayer.initialize_relayer().await,
367 }
368 }
369
370 async fn check_health(&self) -> Result<(), Vec<crate::models::HealthCheckFailure>> {
371 match self {
372 NetworkRelayer::Evm(relayer) => relayer.check_health().await,
373 NetworkRelayer::Solana(relayer) => relayer.check_health().await,
374 NetworkRelayer::Stellar(relayer) => relayer.check_health().await,
375 }
376 }
377
378 async fn sign_transaction(
379 &self,
380 request: &SignTransactionRequest,
381 ) -> Result<SignTransactionExternalResponse, RelayerError> {
382 match self {
383 NetworkRelayer::Evm(_) => Err(RelayerError::NotSupported(
384 "sign_transaction not supported for EVM".to_string(),
385 )),
386 NetworkRelayer::Solana(relayer) => relayer.sign_transaction(request).await,
387 NetworkRelayer::Stellar(relayer) => relayer.sign_transaction(request).await,
388 }
389 }
390
391 async fn handle_health_action(
392 &self,
393 metadata: &std::collections::HashMap<String, String>,
394 ) -> Result<bool, RelayerError> {
395 match self {
396 NetworkRelayer::Evm(relayer) => relayer.handle_health_action(metadata).await,
397 NetworkRelayer::Solana(_) | NetworkRelayer::Stellar(_) => Ok(false),
398 }
399 }
400}
401
402#[async_trait]
403impl<
404 J: JobProducerTrait + 'static,
405 T: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
406 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
407 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
408 TCR: TransactionCounterTrait + Send + Sync + 'static,
409 > GasAbstractionTrait for NetworkRelayer<J, T, RR, NR, TCR>
410{
411 async fn quote_sponsored_transaction(
412 &self,
413 params: SponsoredTransactionQuoteRequest,
414 ) -> Result<SponsoredTransactionQuoteResponse, RelayerError> {
415 match params {
416 SponsoredTransactionQuoteRequest::Solana(params) => match self {
417 NetworkRelayer::Solana(relayer) => {
418 relayer
419 .quote_sponsored_transaction(SponsoredTransactionQuoteRequest::Solana(
420 params,
421 ))
422 .await
423 }
424 NetworkRelayer::Stellar(_) => Err(RelayerError::ValidationError(
425 "Solana request type does not match Stellar relayer type".to_string(),
426 )),
427 NetworkRelayer::Evm(_) => Err(RelayerError::NotSupported(
428 "Gas abstraction not supported for EVM relayers".to_string(),
429 )),
430 },
431 SponsoredTransactionQuoteRequest::Stellar(params) => match self {
432 NetworkRelayer::Stellar(relayer) => {
433 relayer
434 .quote_sponsored_transaction(SponsoredTransactionQuoteRequest::Stellar(
435 params,
436 ))
437 .await
438 }
439 NetworkRelayer::Solana(_) => Err(RelayerError::ValidationError(
440 "Stellar request type does not match Solana relayer type".to_string(),
441 )),
442 NetworkRelayer::Evm(_) => Err(RelayerError::NotSupported(
443 "Gas abstraction not supported for EVM relayers".to_string(),
444 )),
445 },
446 }
447 }
448
449 async fn build_sponsored_transaction(
450 &self,
451 params: SponsoredTransactionBuildRequest,
452 ) -> Result<SponsoredTransactionBuildResponse, RelayerError> {
453 match params {
454 SponsoredTransactionBuildRequest::Solana(params) => match self {
455 NetworkRelayer::Solana(relayer) => {
456 relayer
457 .build_sponsored_transaction(SponsoredTransactionBuildRequest::Solana(
458 params,
459 ))
460 .await
461 }
462 NetworkRelayer::Stellar(_) => Err(RelayerError::ValidationError(
463 "Solana request type does not match Stellar relayer type".to_string(),
464 )),
465 NetworkRelayer::Evm(_) => Err(RelayerError::NotSupported(
466 "Gas abstraction not supported for EVM relayers".to_string(),
467 )),
468 },
469 SponsoredTransactionBuildRequest::Stellar(params) => match self {
470 NetworkRelayer::Stellar(relayer) => {
471 relayer
472 .build_sponsored_transaction(SponsoredTransactionBuildRequest::Stellar(
473 params,
474 ))
475 .await
476 }
477 NetworkRelayer::Solana(_) => Err(RelayerError::ValidationError(
478 "Stellar request type does not match Solana relayer type".to_string(),
479 )),
480 NetworkRelayer::Evm(_) => Err(RelayerError::NotSupported(
481 "Gas abstraction not supported for EVM relayers".to_string(),
482 )),
483 },
484 }
485 }
486}
487
488impl<
489 J: JobProducerTrait + 'static,
490 T: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
491 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
492 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
493 TCR: TransactionCounterTrait + Send + Sync + 'static,
494 > NetworkRelayer<J, T, RR, NR, TCR>
495{
496 pub async fn handle_token_swap_request(
503 &self,
504 relayer_id: String,
505 ) -> Result<Vec<SwapResult>, RelayerError> {
506 match self {
507 NetworkRelayer::Evm(_) => Err(RelayerError::NotSupported(
508 "Token swap not supported for EVM relayers".to_string(),
509 )),
510 NetworkRelayer::Solana(relayer) => relayer.handle_token_swap_request(relayer_id).await,
511 NetworkRelayer::Stellar(relayer) => relayer.handle_token_swap_request(relayer_id).await,
512 }
513 }
514}
515
516#[async_trait]
517pub trait RelayerFactoryTrait<
518 J: JobProducerTrait + Send + Sync + 'static,
519 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
520 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
521 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
522 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
523 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
524 TCR: TransactionCounterTrait + Send + Sync + 'static,
525 PR: PluginRepositoryTrait + Send + Sync + 'static,
526 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
527>
528{
529 async fn create_relayer(
530 relayer: RelayerRepoModel,
531 signer: SignerRepoModel,
532 state: &ThinData<AppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>>,
533 ) -> Result<NetworkRelayer<J, TR, RR, NR, TCR>, RelayerError>;
534}
535
536pub struct RelayerFactory;
537
538#[async_trait]
539impl<
540 J: JobProducerTrait + 'static,
541 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
542 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
543 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
544 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
545 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
546 TCR: TransactionCounterTrait + Send + Sync + 'static,
547 PR: PluginRepositoryTrait + Send + Sync + 'static,
548 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
549 > RelayerFactoryTrait<J, RR, TR, NR, NFR, SR, TCR, PR, AKR> for RelayerFactory
550{
551 #[instrument(
552 level = "debug",
553 skip(relayer, signer, state),
554 fields(
555 request_id = ?crate::observability::request_id::get_request_id(),
556 relayer_id = %relayer.id,
557 network_type = ?relayer.network_type,
558 )
559 )]
560 async fn create_relayer(
561 relayer: RelayerRepoModel,
562 signer: SignerRepoModel,
563 state: &ThinData<AppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>>,
564 ) -> Result<NetworkRelayer<J, TR, RR, NR, TCR>, RelayerError> {
565 match relayer.network_type {
566 NetworkType::Evm => {
567 let network_repo = state
568 .network_repository()
569 .get_by_name(NetworkType::Evm, &relayer.network)
570 .await
571 .ok()
572 .flatten()
573 .ok_or_else(|| {
574 RelayerError::NetworkConfiguration(format!(
575 "Network {} not found",
576 relayer.network
577 ))
578 })?;
579
580 let network = EvmNetwork::try_from(network_repo)?;
581
582 let evm_provider = get_network_provider(&network, relayer.custom_rpc_urls.clone())?;
583 let signer_service = EvmSignerFactory::create_evm_signer(signer.into()).await?;
584 let transaction_counter_service = Arc::new(TransactionCounterService::new(
585 relayer.id.clone(),
586 relayer.address.clone(),
587 state.transaction_counter_store(),
588 ));
589 let relayer = DefaultEvmRelayer::new(
590 relayer,
591 signer_service,
592 evm_provider,
593 network,
594 state.relayer_repository(),
595 state.network_repository(),
596 state.transaction_repository(),
597 transaction_counter_service,
598 state.job_producer(),
599 )?;
600
601 Ok(NetworkRelayer::Evm(Box::new(relayer)))
602 }
603 NetworkType::Solana => {
604 let solana_relayer = create_solana_relayer(
605 relayer,
606 signer,
607 state.relayer_repository(),
608 state.network_repository(),
609 state.transaction_repository(),
610 state.job_producer(),
611 )
612 .await?;
613 Ok(NetworkRelayer::Solana(solana_relayer))
614 }
615 NetworkType::Stellar => {
616 let stellar_relayer = create_stellar_relayer(
617 relayer,
618 signer,
619 state.relayer_repository(),
620 state.network_repository(),
621 state.transaction_repository(),
622 state.job_producer(),
623 state.transaction_counter_store(),
624 )
625 .await?;
626 Ok(NetworkRelayer::Stellar(stellar_relayer))
627 }
628 }
629 }
630}
631
632#[derive(Serialize, Deserialize, ToSchema)]
633pub struct SignDataRequest {
634 pub message: String,
635}
636
637#[derive(Serialize, Deserialize, ToSchema)]
638pub struct SignDataResponseEvm {
639 pub r: String,
640 pub s: String,
641 pub v: u8,
642 pub sig: String,
643}
644
645#[derive(Serialize, Deserialize, ToSchema)]
646pub struct SignDataResponseSolana {
647 pub signature: String,
648 pub public_key: String,
649}
650
651#[derive(Serialize, Deserialize, ToSchema)]
652#[serde(untagged)]
653pub enum SignDataResponse {
654 Evm(SignDataResponseEvm),
655 Solana(SignDataResponseSolana),
656}
657
658#[derive(Serialize, Deserialize, ToSchema)]
659pub struct SignTypedDataRequest {
660 pub domain_separator: String,
661 pub hash_struct_message: String,
662}
663
664#[derive(Debug, Serialize, Deserialize, ToSchema)]
665pub struct SignTransactionRequestStellar {
666 pub unsigned_xdr: String,
667}
668
669#[derive(Debug, Serialize, Deserialize, ToSchema)]
670pub struct SignTransactionRequestSolana {
671 pub transaction: EncodedSerializedTransaction,
672}
673
674#[derive(Debug, Serialize, Deserialize, ToSchema)]
675#[serde(untagged)]
676pub enum SignTransactionRequest {
677 Stellar(SignTransactionRequestStellar),
678 Evm(Vec<u8>),
679 Solana(SignTransactionRequestSolana),
680}
681
682#[derive(Debug, Serialize, Deserialize, Clone)]
683pub struct SignTransactionResponseEvm {
684 pub hash: String,
685 pub signature: EvmTransactionDataSignature,
686 pub raw: Vec<u8>,
687}
688
689#[derive(Debug, Serialize, Deserialize, Clone)]
690pub struct SignTransactionResponseStellar {
691 pub signature: DecoratedSignature,
692}
693
694#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)]
695pub struct SignTransactionResponseSolana {
696 pub transaction: EncodedSerializedTransaction,
697 pub signature: String,
698}
699
700#[derive(Debug, Serialize, Deserialize)]
701#[serde(rename_all = "camelCase")]
702pub struct SignXdrTransactionResponseStellar {
703 pub signed_xdr: String,
704 pub signature: DecoratedSignature,
705}
706
707#[derive(Debug, Serialize, Deserialize, Clone)]
708pub enum SignTransactionResponse {
709 Evm(SignTransactionResponseEvm),
710 Solana(SignTransactionResponseSolana),
711 Stellar(SignTransactionResponseStellar),
712}
713
714#[derive(Debug, Serialize, Deserialize, ToSchema)]
715#[serde(rename_all = "camelCase")]
716#[schema(as = SignTransactionResponseStellar)]
717pub struct SignTransactionExternalResponseStellar {
718 pub signed_xdr: String,
719 pub signature: String,
720}
721
722#[derive(Debug, Serialize, Deserialize, ToSchema)]
723#[serde(untagged)]
724#[schema(as = SignTransactionResponse)]
725pub enum SignTransactionExternalResponse {
726 Stellar(SignTransactionExternalResponseStellar),
727 Evm(Vec<u8>),
728 Solana(SignTransactionResponseSolana),
729}
730
731impl SignTransactionResponse {
732 pub fn into_evm(self) -> Result<SignTransactionResponseEvm, TransactionError> {
733 match self {
734 SignTransactionResponse::Evm(e) => Ok(e),
735 _ => Err(TransactionError::InvalidType(
736 "Expected EVM signature".to_string(),
737 )),
738 }
739 }
740}
741
742#[derive(Debug, Serialize, ToSchema)]
743pub struct BalanceResponse {
744 pub balance: u128,
745 #[schema(example = "wei")]
746 pub unit: String,
747}