1use crate::{
13 domain::{
14 get_network_relayer, get_network_relayer_by_model, get_relayer_by_id,
15 get_relayer_transaction_by_model, get_transaction_by_id as get_tx_by_id,
16 transaction::is_final_state, GasAbstractionTrait, Relayer, RelayerFactory,
17 RelayerFactoryTrait, SignDataRequest, SignDataResponse, SignTransactionRequest,
18 SignTypedDataRequest, Transaction,
19 },
20 jobs::JobProducerTrait,
21 models::{
22 convert_to_internal_rpc_request, deserialize_policy_for_network_type,
23 transaction::request::{
24 SponsoredTransactionBuildRequest, SponsoredTransactionQuoteRequest,
25 },
26 ApiError, ApiResponse, CreateRelayerRequest, DefaultAppState, NetworkRepoModel,
27 NetworkTransactionRequest, NetworkType, NotificationRepoModel, PaginationMeta,
28 PaginationQuery, Relayer as RelayerDomainModel, RelayerRepoModel, RelayerRepoUpdater,
29 RelayerResponse, Signer as SignerDomainModel, SignerRepoModel, ThinDataAppState,
30 TransactionListQuery, TransactionRepoModel, TransactionResponse, TransactionStatus,
31 UpdateRelayerRequestRaw,
32 },
33 repositories::{
34 ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
35 Repository, TransactionCounterTrait, TransactionRepository,
36 },
37 services::signer::{Signer, SignerFactory},
38};
39use actix_web::{web, HttpResponse};
40use eyre::Result;
41
42pub async fn list_relayers<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
53 query: PaginationQuery,
54 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
55) -> Result<HttpResponse, ApiError>
56where
57 J: JobProducerTrait + Send + Sync + 'static,
58 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
59 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
60 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
61 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
62 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
63 TCR: TransactionCounterTrait + Send + Sync + 'static,
64 PR: PluginRepositoryTrait + Send + Sync + 'static,
65 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
66{
67 let relayers = state.relayer_repository.list_paginated(query).await?;
68
69 let mapped_relayers: Vec<RelayerResponse> =
70 relayers.items.into_iter().map(|r| r.into()).collect();
71
72 Ok(HttpResponse::Ok().json(ApiResponse::paginated(
73 mapped_relayers,
74 PaginationMeta {
75 total_items: relayers.total,
76 current_page: relayers.page,
77 per_page: relayers.per_page,
78 },
79 )))
80}
81
82pub async fn get_relayer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
93 relayer_id: String,
94 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
95) -> Result<HttpResponse, ApiError>
96where
97 J: JobProducerTrait + Send + Sync + 'static,
98 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
99 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
100 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
101 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
102 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
103 TCR: TransactionCounterTrait + Send + Sync + 'static,
104 PR: PluginRepositoryTrait + Send + Sync + 'static,
105 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
106{
107 let relayer = get_relayer_by_id(relayer_id, &state).await?;
108
109 let relayer_response: RelayerResponse = relayer.into();
110
111 Ok(HttpResponse::Ok().json(ApiResponse::success(relayer_response)))
112}
113
114pub async fn create_relayer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
135 request: CreateRelayerRequest,
136 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
137) -> Result<HttpResponse, ApiError>
138where
139 J: JobProducerTrait + Send + Sync + 'static,
140 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
141 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
142 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
143 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
144 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
145 TCR: TransactionCounterTrait + Send + Sync + 'static,
146 PR: PluginRepositoryTrait + Send + Sync + 'static,
147 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
148{
149 let relayer = RelayerDomainModel::try_from(request)?;
151
152 let signer_model = state
154 .signer_repository
155 .get_by_id(relayer.signer_id.clone())
156 .await?;
157
158 let network = state
160 .network_repository
161 .get_by_name(relayer.network_type, &relayer.network)
162 .await?;
163
164 if network.is_none() {
165 return Err(ApiError::BadRequest(format!(
166 "Network '{}' not found for network type '{}'. Please ensure the network configuration exists.",
167 relayer.network,
168 relayer.network_type
169 )));
170 }
171
172 let relayers = state
174 .relayer_repository
175 .list_by_signer_id(&relayer.signer_id)
176 .await?;
177 if let Some(existing_relayer) = relayers.iter().find(|r| r.network == relayer.network) {
178 return Err(ApiError::BadRequest(format!(
179 "Cannot create relayer: signer '{}' is already in use by relayer '{}' on network '{}'. Each signer can only be connected to one relayer per network for security reasons. Please use a different signer or create the relayer on a different network.",
180 relayer.signer_id, existing_relayer.id, relayer.network
181 )));
182 }
183
184 if let Some(notification_id) = &relayer.notification_id {
186 let _notification = state
187 .notification_repository
188 .get_by_id(notification_id.clone())
189 .await?;
190 }
191
192 let mut relayer_model = RelayerRepoModel::from(relayer);
194
195 let signer_service = SignerFactory::create_signer(
197 &relayer_model.network_type,
198 &SignerDomainModel::from(signer_model.clone()),
199 )
200 .await
201 .map_err(|e| ApiError::InternalError(e.to_string()))?;
202 let address = signer_service
203 .address()
204 .await
205 .map_err(|e| ApiError::InternalError(e.to_string()))?;
206 relayer_model.address = address.to_string();
207
208 let created_relayer = state.relayer_repository.create(relayer_model).await?;
209
210 let relayer =
211 RelayerFactory::create_relayer(created_relayer.clone(), signer_model, &state).await?;
212
213 relayer.initialize_relayer().await?;
214
215 let response = RelayerResponse::from(created_relayer);
216 Ok(HttpResponse::Created().json(ApiResponse::success(response)))
217}
218
219pub async fn update_relayer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
231 relayer_id: String,
232 patch: serde_json::Value,
233 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
234) -> Result<HttpResponse, ApiError>
235where
236 J: JobProducerTrait + Send + Sync + 'static,
237 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
238 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
239 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
240 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
241 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
242 TCR: TransactionCounterTrait + Send + Sync + 'static,
243 PR: PluginRepositoryTrait + Send + Sync + 'static,
244 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
245{
246 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
247
248 let update_request: UpdateRelayerRequestRaw = serde_json::from_value(patch.clone())
250 .map_err(|e| ApiError::BadRequest(format!("Invalid update request: {e}")))?;
251
252 if let Some(policies) = update_request.policies {
253 deserialize_policy_for_network_type(&policies, relayer.network_type)
254 .map_err(|e| ApiError::BadRequest(format!("Invalid policy: {e}")))?;
255 }
256
257 if relayer.system_disabled {
258 return Err(ApiError::BadRequest("Relayer is disabled".into()));
259 }
260
261 if let Some(notification_id) = update_request.notification_id {
263 state
264 .notification_repository
265 .get_by_id(notification_id.to_string())
266 .await?;
267 }
268
269 let updated_domain = RelayerDomainModel::from(relayer.clone())
271 .apply_json_patch(&patch)
272 .map_err(ApiError::from)?;
273
274 let updated_repo_model =
276 RelayerRepoUpdater::from_existing(relayer).apply_domain_update(updated_domain);
277
278 let saved_relayer = state
279 .relayer_repository
280 .update(relayer_id.clone(), updated_repo_model)
281 .await?;
282
283 let relayer_response: RelayerResponse = saved_relayer.into();
284 Ok(HttpResponse::Ok().json(ApiResponse::success(relayer_response)))
285}
286
287pub async fn delete_relayer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
303 relayer_id: String,
304 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
305) -> Result<HttpResponse, ApiError>
306where
307 J: JobProducerTrait + Send + Sync + 'static,
308 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
309 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
310 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
311 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
312 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
313 TCR: TransactionCounterTrait + Send + Sync + 'static,
314 PR: PluginRepositoryTrait + Send + Sync + 'static,
315 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
316{
317 let _relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
319
320 let active_transaction_count = state
323 .transaction_repository
324 .count_by_status(
325 &relayer_id,
326 &[
327 TransactionStatus::Pending,
328 TransactionStatus::Sent,
329 TransactionStatus::Submitted,
330 ],
331 )
332 .await?;
333
334 if active_transaction_count > 0 {
335 return Err(ApiError::BadRequest(format!(
336 "Cannot delete relayer '{relayer_id}' because it has {active_transaction_count} transaction(s). Please wait for all transactions to complete or cancel them before deleting the relayer.",
337 )));
338 }
339
340 state.relayer_repository.delete_by_id(relayer_id).await?;
342
343 Ok(HttpResponse::Ok().json(ApiResponse::success("Relayer deleted successfully")))
344}
345
346pub async fn get_relayer_status<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
357 relayer_id: String,
358 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
359) -> Result<HttpResponse, ApiError>
360where
361 J: JobProducerTrait + Send + Sync + 'static,
362 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
363 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
364 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
365 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
366 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
367 TCR: TransactionCounterTrait + Send + Sync + 'static,
368 PR: PluginRepositoryTrait + Send + Sync + 'static,
369 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
370{
371 let relayer = get_network_relayer(relayer_id, &state).await?;
372
373 let status = relayer.get_status().await?;
374
375 Ok(HttpResponse::Ok().json(ApiResponse::success(status)))
376}
377
378pub async fn get_relayer_balance<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
389 relayer_id: String,
390 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
391) -> Result<HttpResponse, ApiError>
392where
393 J: JobProducerTrait + Send + Sync + 'static,
394 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
395 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
396 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
397 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
398 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
399 TCR: TransactionCounterTrait + Send + Sync + 'static,
400 PR: PluginRepositoryTrait + Send + Sync + 'static,
401 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
402{
403 let relayer = get_network_relayer(relayer_id, &state).await?;
404
405 let result = relayer.get_balance().await?;
406
407 Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
408}
409
410pub async fn send_transaction(
422 relayer_id: String,
423 request: serde_json::Value,
424 state: web::ThinData<DefaultAppState>,
425) -> Result<HttpResponse, ApiError> {
426 let relayer_repo_model = get_relayer_by_id(relayer_id, &state).await?;
427 relayer_repo_model.validate_active_state()?;
428
429 let relayer = get_network_relayer(relayer_repo_model.id.clone(), &state).await?;
430
431 let tx_request: NetworkTransactionRequest =
432 NetworkTransactionRequest::from_json(&relayer_repo_model.network_type, request.clone())?;
433
434 tx_request.validate(&relayer_repo_model)?;
435
436 let transaction = relayer.process_transaction_request(tx_request).await?;
437
438 let transaction_response: TransactionResponse = transaction.into();
439
440 Ok(HttpResponse::Ok().json(ApiResponse::success(transaction_response)))
441}
442
443pub async fn get_transaction_by_id<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
455 relayer_id: String,
456 transaction_id: String,
457 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
458) -> Result<HttpResponse, ApiError>
459where
460 J: JobProducerTrait + Send + Sync + 'static,
461 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
462 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
463 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
464 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
465 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
466 TCR: TransactionCounterTrait + Send + Sync + 'static,
467 PR: PluginRepositoryTrait + Send + Sync + 'static,
468 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
469{
470 if relayer_id.is_empty() || transaction_id.is_empty() {
471 return Ok(HttpResponse::Ok().json(ApiResponse::<()>::error(
472 "Invalid relayer or transaction ID".to_string(),
473 )));
474 }
475 get_relayer_by_id(relayer_id, &state).await?;
477
478 let transaction = get_tx_by_id(transaction_id, &state).await?;
479
480 let transaction_response: TransactionResponse = transaction.into();
481
482 Ok(HttpResponse::Ok().json(ApiResponse::success(transaction_response)))
483}
484
485pub async fn get_transaction_by_nonce<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
497 relayer_id: String,
498 nonce: u64,
499 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
500) -> Result<HttpResponse, ApiError>
501where
502 J: JobProducerTrait + Send + Sync + 'static,
503 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
504 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
505 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
506 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
507 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
508 TCR: TransactionCounterTrait + Send + Sync + 'static,
509 PR: PluginRepositoryTrait + Send + Sync + 'static,
510 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
511{
512 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
513
514 if relayer.network_type != NetworkType::Evm {
516 return Err(ApiError::NotSupported(
517 "Nonce lookup only supported for EVM networks".into(),
518 ));
519 }
520
521 let transaction = state
522 .transaction_repository
523 .find_by_nonce(&relayer_id, nonce)
524 .await?
525 .ok_or_else(|| ApiError::NotFound(format!("Transaction with nonce {nonce} not found")))?;
526
527 let transaction_response: TransactionResponse = transaction.into();
528
529 Ok(HttpResponse::Ok().json(ApiResponse::success(transaction_response)))
530}
531
532pub async fn list_transactions<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
544 relayer_id: String,
545 query: TransactionListQuery,
546 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
547) -> Result<HttpResponse, ApiError>
548where
549 J: JobProducerTrait + Send + Sync + 'static,
550 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
551 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
552 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
553 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
554 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
555 TCR: TransactionCounterTrait + Send + Sync + 'static,
556 PR: PluginRepositoryTrait + Send + Sync + 'static,
557 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
558{
559 get_relayer_by_id(relayer_id.clone(), &state).await?;
560
561 let pagination = PaginationQuery::from(query.clone());
562 let transactions = match &query.status {
563 Some(status) => {
564 let exclude_canceled = !is_final_state(status);
572 state
573 .transaction_repository
574 .find_by_status_paginated_filtered(
575 &relayer_id,
576 std::slice::from_ref(status),
577 pagination,
578 false,
579 exclude_canceled,
580 )
581 .await?
582 }
583 None => {
584 state
585 .transaction_repository
586 .find_by_relayer_id(&relayer_id, pagination)
587 .await?
588 }
589 };
590
591 let transaction_response_list: Vec<TransactionResponse> =
592 transactions.items.into_iter().map(|t| t.into()).collect();
593
594 Ok(HttpResponse::Ok().json(ApiResponse::paginated(
595 transaction_response_list,
596 PaginationMeta {
597 total_items: transactions.total,
598 current_page: transactions.page,
599 per_page: transactions.per_page,
600 },
601 )))
602}
603
604pub async fn delete_pending_transactions<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
615 relayer_id: String,
616 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
617) -> Result<HttpResponse, ApiError>
618where
619 J: JobProducerTrait + Send + Sync + 'static,
620 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
621 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
622 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
623 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
624 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
625 TCR: TransactionCounterTrait + Send + Sync + 'static,
626 PR: PluginRepositoryTrait + Send + Sync + 'static,
627 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
628{
629 let relayer = get_relayer_by_id(relayer_id, &state).await?;
630 relayer.validate_active_state()?;
631 let network_relayer = get_network_relayer_by_model(relayer.clone(), &state).await?;
632
633 let result = network_relayer.delete_pending_transactions().await?;
634
635 Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
636}
637
638pub async fn cancel_transaction(
650 relayer_id: String,
651 transaction_id: String,
652 state: web::ThinData<DefaultAppState>,
653) -> Result<HttpResponse, ApiError> {
654 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
655 relayer.validate_active_state()?;
656
657 let relayer_transaction = get_relayer_transaction_by_model(relayer.clone(), &state).await?;
658
659 let transaction_to_cancel = get_tx_by_id(transaction_id, &state).await?;
660
661 let canceled_transaction = relayer_transaction
662 .cancel_transaction(transaction_to_cancel)
663 .await?;
664
665 let transaction_response: TransactionResponse = canceled_transaction.into();
666
667 Ok(HttpResponse::Ok().json(ApiResponse::success(transaction_response)))
668}
669
670pub async fn replace_transaction(
683 relayer_id: String,
684 transaction_id: String,
685 request: serde_json::Value,
686 state: web::ThinData<DefaultAppState>,
687) -> Result<HttpResponse, ApiError> {
688 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
689 relayer.validate_active_state()?;
690
691 let new_tx_request: NetworkTransactionRequest =
692 NetworkTransactionRequest::from_json(&relayer.network_type, request.clone())?;
693 new_tx_request.validate(&relayer)?;
694
695 let transaction_to_replace = state
696 .transaction_repository
697 .get_by_id(transaction_id)
698 .await?;
699
700 let relayer_transaction = get_relayer_transaction_by_model(relayer.clone(), &state).await?;
701 let replaced_transaction = relayer_transaction
702 .replace_transaction(transaction_to_replace, new_tx_request)
703 .await?;
704
705 let transaction_response: TransactionResponse = replaced_transaction.into();
706
707 Ok(HttpResponse::Ok().json(ApiResponse::success(transaction_response)))
708}
709
710pub async fn sign_data<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
722 relayer_id: String,
723 request: SignDataRequest,
724 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
725) -> Result<HttpResponse, ApiError>
726where
727 J: JobProducerTrait + Send + Sync + 'static,
728 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
729 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
730 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
731 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
732 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
733 TCR: TransactionCounterTrait + Send + Sync + 'static,
734 PR: PluginRepositoryTrait + Send + Sync + 'static,
735 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
736{
737 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
738 relayer.validate_active_state()?;
739 let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
740
741 let result = network_relayer.sign_data(request).await?;
742
743 if let SignDataResponse::Evm(sign) = result {
744 Ok(HttpResponse::Ok().json(ApiResponse::success(sign)))
745 } else {
746 Err(ApiError::NotSupported("Sign data not supported".into()))
747 }
748}
749
750pub async fn sign_typed_data<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
762 relayer_id: String,
763 request: SignTypedDataRequest,
764 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
765) -> Result<HttpResponse, ApiError>
766where
767 J: JobProducerTrait + Send + Sync + 'static,
768 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
769 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
770 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
771 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
772 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
773 TCR: TransactionCounterTrait + Send + Sync + 'static,
774 PR: PluginRepositoryTrait + Send + Sync + 'static,
775 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
776{
777 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
778 relayer.validate_active_state()?;
779 let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
780
781 let result = network_relayer.sign_typed_data(request).await?;
782
783 Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
784}
785
786pub async fn relayer_rpc<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
798 relayer_id: String,
799 request: serde_json::Value,
800 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
801) -> Result<HttpResponse, ApiError>
802where
803 J: JobProducerTrait + Send + Sync + 'static,
804 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
805 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
806 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
807 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
808 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
809 TCR: TransactionCounterTrait + Send + Sync + 'static,
810 PR: PluginRepositoryTrait + Send + Sync + 'static,
811 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
812{
813 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
814 relayer.validate_active_state()?;
815 let network_relayer = get_network_relayer_by_model(relayer.clone(), &state).await?;
816
817 let internal_request = convert_to_internal_rpc_request(request, &relayer.network_type)?;
818 let result = network_relayer.rpc(internal_request).await?;
819
820 Ok(HttpResponse::Ok().json(result))
821}
822
823pub async fn sign_transaction<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
835 relayer_id: String,
836 request: SignTransactionRequest,
837 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
838) -> Result<HttpResponse, ApiError>
839where
840 J: JobProducerTrait + Send + Sync + 'static,
841 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
842 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
843 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
844 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
845 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
846 TCR: TransactionCounterTrait + Send + Sync + 'static,
847 PR: PluginRepositoryTrait + Send + Sync + 'static,
848 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
849{
850 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
851 relayer.validate_active_state()?;
852
853 let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
855 let result = network_relayer.sign_transaction(&request).await?;
856
857 Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
858}
859
860pub async fn quote_sponsored_transaction<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
872 relayer_id: String,
873 request: SponsoredTransactionQuoteRequest,
874 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
875) -> Result<HttpResponse, ApiError>
876where
877 J: JobProducerTrait + Send + Sync + 'static,
878 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
879 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
880 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
881 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
882 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
883 TCR: TransactionCounterTrait + Send + Sync + 'static,
884 PR: PluginRepositoryTrait + Send + Sync + 'static,
885 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
886{
887 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
888 relayer.validate_active_state()?;
889
890 request.validate()?;
891
892 let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
893
894 let result = network_relayer.quote_sponsored_transaction(request).await?;
895 Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
896}
897
898pub async fn build_sponsored_transaction<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
910 relayer_id: String,
911 request: SponsoredTransactionBuildRequest,
912 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
913) -> Result<HttpResponse, ApiError>
914where
915 J: JobProducerTrait + Send + Sync + 'static,
916 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
917 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
918 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
919 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
920 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
921 TCR: TransactionCounterTrait + Send + Sync + 'static,
922 PR: PluginRepositoryTrait + Send + Sync + 'static,
923 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
924{
925 let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
926 relayer.validate_active_state()?;
927
928 request.validate()?;
929
930 let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
931
932 let result = network_relayer.build_sponsored_transaction(request).await?;
933 Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939 use crate::{
940 domain::SignTransactionRequestStellar,
941 models::{
942 ApiResponse, CreateRelayerPolicyRequest, CreateRelayerRequest, RelayerEvmPolicy,
943 RelayerNetworkPolicyResponse, RelayerNetworkType, RelayerResponse, RelayerSolanaPolicy,
944 RelayerStellarPolicy, SolanaFeePaymentStrategy, StellarFeePaymentStrategy,
945 },
946 utils::mocks::mockutils::{
947 create_mock_app_state, create_mock_network, create_mock_notification,
948 create_mock_relayer, create_mock_signer, create_mock_transaction,
949 },
950 };
951 use actix_web::body::to_bytes;
952 use lazy_static::lazy_static;
953 use std::env;
954 use tokio::sync::Mutex;
955
956 lazy_static! {
957 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
958 }
959
960 fn setup_test_env() {
961 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D"); env::set_var("REDIS_URL", "redis://localhost:6379");
963 }
964
965 fn cleanup_test_env() {
966 env::remove_var("API_KEY");
967 env::remove_var("REDIS_URL");
968 }
969
970 fn create_test_relayer_create_request(
972 id: Option<String>,
973 name: &str,
974 network: &str,
975 signer_id: &str,
976 notification_id: Option<String>,
977 ) -> CreateRelayerRequest {
978 CreateRelayerRequest {
979 id,
980 name: name.to_string(),
981 network: network.to_string(),
982 network_type: RelayerNetworkType::Evm,
983 paused: false,
984 policies: None,
985 signer_id: signer_id.to_string(),
986 notification_id,
987 custom_rpc_urls: None,
988 }
989 }
990
991 fn create_mock_solana_network() -> crate::models::NetworkRepoModel {
993 use crate::config::{NetworkConfigCommon, SolanaNetworkConfig};
994 use crate::models::{NetworkConfigData, NetworkRepoModel, NetworkType, RpcConfig};
995
996 NetworkRepoModel {
997 id: "test".to_string(),
998 name: "test".to_string(),
999 network_type: NetworkType::Solana,
1000 config: NetworkConfigData::Solana(SolanaNetworkConfig {
1001 common: NetworkConfigCommon {
1002 network: "test".to_string(),
1003 from: None,
1004 rpc_urls: Some(vec![RpcConfig::new("http://localhost:8899".to_string())]),
1005 explorer_urls: None,
1006 average_blocktime_ms: Some(400),
1007 is_testnet: Some(true),
1008 tags: None,
1009 },
1010 }),
1011 }
1012 }
1013
1014 fn create_mock_stellar_network() -> crate::models::NetworkRepoModel {
1016 use crate::config::{NetworkConfigCommon, StellarNetworkConfig};
1017 use crate::models::{NetworkConfigData, NetworkRepoModel, NetworkType, RpcConfig};
1018
1019 NetworkRepoModel {
1020 id: "test".to_string(),
1021 name: "test".to_string(),
1022 network_type: NetworkType::Stellar,
1023 config: NetworkConfigData::Stellar(StellarNetworkConfig {
1024 common: NetworkConfigCommon {
1025 network: "test".to_string(),
1026 from: None,
1027 rpc_urls: Some(vec![RpcConfig::new(
1028 "https://horizon-testnet.stellar.org".to_string(),
1029 )]),
1030 explorer_urls: None,
1031 average_blocktime_ms: Some(5000),
1032 is_testnet: Some(true),
1033 tags: None,
1034 },
1035 passphrase: Some("Test Network ; September 2015".to_string()),
1036 horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
1037 }),
1038 }
1039 }
1040
1041 #[actix_web::test]
1044 async fn test_create_relayer_success() {
1045 let _lock = ENV_MUTEX.lock().await;
1046 setup_test_env();
1047 let network = create_mock_network();
1048 let signer = create_mock_signer();
1049 let app_state = create_mock_app_state(
1050 None,
1051 None,
1052 Some(vec![signer]),
1053 Some(vec![network]),
1054 None,
1055 None,
1056 )
1057 .await;
1058
1059 let request = create_test_relayer_create_request(
1060 Some("test-relayer".to_string()),
1061 "Test Relayer",
1062 "test", "test", None,
1065 );
1066
1067 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1068
1069 assert!(result.is_ok());
1070 let response = result.unwrap();
1071 assert_eq!(response.status(), 201);
1072
1073 let body = to_bytes(response.into_body()).await.unwrap();
1074 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1075
1076 assert!(api_response.success);
1077 let data = api_response.data.unwrap();
1078 assert_eq!(data.id, "test-relayer");
1079 assert_eq!(data.name, "Test Relayer"); assert_eq!(data.network, "test");
1081 cleanup_test_env();
1082 }
1083
1084 #[actix_web::test]
1085 async fn test_create_relayer_with_evm_policies() {
1086 let _lock = ENV_MUTEX.lock().await;
1087 setup_test_env();
1088 let network = create_mock_network();
1089 let signer = create_mock_signer();
1090 let app_state = create_mock_app_state(
1091 None,
1092 None,
1093 Some(vec![signer]),
1094 Some(vec![network]),
1095 None,
1096 None,
1097 )
1098 .await;
1099
1100 let mut request = create_test_relayer_create_request(
1101 Some("test-relayer-policies".to_string()),
1102 "Test Relayer with Policies",
1103 "test", "test", None,
1106 );
1107
1108 request.policies = Some(CreateRelayerPolicyRequest::Evm(RelayerEvmPolicy {
1110 gas_price_cap: Some(50000000000),
1111 min_balance: Some(1000000000000000000),
1112 eip1559_pricing: Some(true),
1113 private_transactions: Some(false),
1114 gas_limit_estimation: Some(true),
1115 whitelist_receivers: Some(vec![
1116 "0x1234567890123456789012345678901234567890".to_string()
1117 ]),
1118 }));
1119
1120 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1121
1122 assert!(result.is_ok());
1123 let response = result.unwrap();
1124 assert_eq!(response.status(), 201);
1125
1126 let body = to_bytes(response.into_body()).await.unwrap();
1127 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1128
1129 assert!(api_response.success);
1130 let data = api_response.data.unwrap();
1131 assert_eq!(data.id, "test-relayer-policies");
1132 assert_eq!(data.name, "Test Relayer with Policies");
1133 assert_eq!(data.network, "test");
1134
1135 assert!(data.policies.is_some());
1137 cleanup_test_env();
1138 }
1139
1140 #[actix_web::test]
1141 async fn test_create_relayer_with_partial_evm_policies() {
1142 let _lock = ENV_MUTEX.lock().await;
1143 setup_test_env();
1144 let network = create_mock_network();
1145 let signer = create_mock_signer();
1146 let app_state = create_mock_app_state(
1147 None,
1148 None,
1149 Some(vec![signer]),
1150 Some(vec![network]),
1151 None,
1152 None,
1153 )
1154 .await;
1155
1156 let mut request = create_test_relayer_create_request(
1157 Some("test-relayer-partial".to_string()),
1158 "Test Relayer with Partial Policies",
1159 "test",
1160 "test",
1161 None,
1162 );
1163
1164 request.policies = Some(CreateRelayerPolicyRequest::Evm(RelayerEvmPolicy {
1166 gas_price_cap: Some(30000000000),
1167 eip1559_pricing: Some(false),
1168 min_balance: None,
1169 private_transactions: None,
1170 gas_limit_estimation: None,
1171 whitelist_receivers: None,
1172 }));
1173
1174 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1175
1176 assert!(result.is_ok());
1177 let response = result.unwrap();
1178 assert_eq!(response.status(), 201);
1179
1180 let body = to_bytes(response.into_body()).await.unwrap();
1181 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1182
1183 assert!(api_response.success);
1184 let data = api_response.data.unwrap();
1185 assert_eq!(data.id, "test-relayer-partial");
1186
1187 assert!(data.policies.is_some());
1189 cleanup_test_env();
1190 }
1191
1192 #[actix_web::test]
1193 async fn test_create_relayer_with_solana_policies() {
1194 let _lock = ENV_MUTEX.lock().await;
1195 setup_test_env();
1196 let network = create_mock_solana_network();
1197 let signer = create_mock_signer();
1198 let app_state = create_mock_app_state(
1199 None,
1200 None,
1201 Some(vec![signer]),
1202 Some(vec![network]),
1203 None,
1204 None,
1205 )
1206 .await;
1207
1208 let mut request = create_test_relayer_create_request(
1209 Some("test-solana-relayer".to_string()),
1210 "Test Solana Relayer",
1211 "test",
1212 "test",
1213 None,
1214 );
1215
1216 request.network_type = RelayerNetworkType::Solana;
1218 request.policies = Some(CreateRelayerPolicyRequest::Solana(RelayerSolanaPolicy {
1219 fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
1220 min_balance: Some(5000000),
1221 max_signatures: Some(10),
1222 max_tx_data_size: Some(1232),
1223 max_allowed_fee_lamports: Some(50000),
1224 allowed_programs: None, allowed_tokens: None,
1226 fee_margin_percentage: Some(10.0),
1227 allowed_accounts: None,
1228 disallowed_accounts: None,
1229 swap_config: None,
1230 }));
1231
1232 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1233
1234 assert!(result.is_ok());
1235 let response = result.unwrap();
1236 assert_eq!(response.status(), 201);
1237
1238 let body = to_bytes(response.into_body()).await.unwrap();
1239 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1240
1241 assert!(api_response.success);
1242 let data = api_response.data.unwrap();
1243 assert_eq!(data.id, "test-solana-relayer");
1244 assert_eq!(data.network_type, RelayerNetworkType::Solana);
1245 assert_eq!(data.name, "Test Solana Relayer");
1246
1247 assert!(data.policies.is_some());
1249 let policies = data.policies.unwrap();
1251 if let RelayerNetworkPolicyResponse::Solana(solana_policy) = policies {
1252 assert_eq!(
1253 solana_policy.fee_payment_strategy,
1254 Some(SolanaFeePaymentStrategy::Relayer)
1255 );
1256 assert_eq!(solana_policy.min_balance, 5000000);
1257 assert_eq!(solana_policy.max_signatures, Some(10));
1258 assert_eq!(solana_policy.max_tx_data_size, 1232);
1259 assert_eq!(solana_policy.max_allowed_fee_lamports, Some(50000));
1260 } else {
1261 panic!("Expected Solana policies");
1262 }
1263 cleanup_test_env();
1264 }
1265
1266 #[actix_web::test]
1267 async fn test_create_relayer_with_stellar_policies() {
1268 let _lock = ENV_MUTEX.lock().await;
1269 setup_test_env();
1270 let network = create_mock_stellar_network();
1271 let signer = create_mock_signer();
1272 let app_state = create_mock_app_state(
1273 None,
1274 None,
1275 Some(vec![signer]),
1276 Some(vec![network]),
1277 None,
1278 None,
1279 )
1280 .await;
1281
1282 let mut request = create_test_relayer_create_request(
1283 Some("test-stellar-relayer".to_string()),
1284 "Test Stellar Relayer",
1285 "test",
1286 "test",
1287 None,
1288 );
1289
1290 request.network_type = RelayerNetworkType::Stellar;
1292 request.policies = Some(CreateRelayerPolicyRequest::Stellar(RelayerStellarPolicy {
1293 min_balance: Some(10000000),
1294 max_fee: Some(100),
1295 timeout_seconds: Some(30),
1296 concurrent_transactions: None,
1297 allowed_tokens: None,
1298 fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
1299 slippage_percentage: None,
1300 fee_margin_percentage: None,
1301 swap_config: None,
1302 }));
1303
1304 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1305
1306 assert!(result.is_ok());
1307 let response = result.unwrap();
1308 assert_eq!(response.status(), 201);
1309
1310 let body = to_bytes(response.into_body()).await.unwrap();
1311 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1312
1313 assert!(api_response.success);
1314 let data = api_response.data.unwrap();
1315 assert_eq!(data.id, "test-stellar-relayer");
1316 assert_eq!(data.network_type, RelayerNetworkType::Stellar);
1317
1318 assert!(data.policies.is_some());
1320 cleanup_test_env();
1321 }
1322
1323 #[actix_web::test]
1324 async fn test_create_relayer_with_policy_type_mismatch() {
1325 let _lock = ENV_MUTEX.lock().await;
1326 setup_test_env();
1327 let network = create_mock_network();
1328 let signer = create_mock_signer();
1329 let app_state = create_mock_app_state(
1330 None,
1331 None,
1332 Some(vec![signer]),
1333 Some(vec![network]),
1334 None,
1335 None,
1336 )
1337 .await;
1338
1339 let mut request = create_test_relayer_create_request(
1340 Some("test-mismatch-relayer".to_string()),
1341 "Test Mismatch Relayer",
1342 "test",
1343 "test",
1344 None,
1345 );
1346
1347 request.network_type = RelayerNetworkType::Evm;
1349 request.policies = Some(CreateRelayerPolicyRequest::Solana(
1350 RelayerSolanaPolicy::default(),
1351 ));
1352
1353 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1354
1355 assert!(result.is_err());
1356 if let Err(ApiError::BadRequest(msg)) = result {
1357 assert!(msg.contains("Policy type does not match relayer network type"));
1358 } else {
1359 panic!("Expected BadRequest error for policy type mismatch");
1360 }
1361 cleanup_test_env();
1362 }
1363
1364 #[actix_web::test]
1365 async fn test_create_relayer_with_notification() {
1366 let _lock = ENV_MUTEX.lock().await;
1367 setup_test_env();
1368 let network = create_mock_network();
1369 let signer = create_mock_signer();
1370 let notification = create_mock_notification("test-notification".to_string());
1371 let app_state = create_mock_app_state(
1372 None,
1373 None,
1374 Some(vec![signer]),
1375 Some(vec![network]),
1376 None,
1377 None,
1378 )
1379 .await;
1380
1381 app_state
1383 .notification_repository
1384 .create(notification)
1385 .await
1386 .unwrap();
1387
1388 let request = create_test_relayer_create_request(
1389 Some("test-relayer".to_string()),
1390 "Test Relayer",
1391 "test", "test", Some("test-notification".to_string()),
1394 );
1395
1396 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1397
1398 assert!(result.is_ok());
1399 let response = result.unwrap();
1400 assert_eq!(response.status(), 201);
1401 let body = to_bytes(response.into_body()).await.unwrap();
1402 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1403
1404 assert!(api_response.success);
1405 let data = api_response.data.unwrap();
1406 assert_eq!(data.notification_id, Some("test-notification".to_string()));
1407 cleanup_test_env();
1408 }
1409
1410 #[actix_web::test]
1411 async fn test_create_relayer_nonexistent_signer() {
1412 let network = create_mock_network();
1413 let app_state =
1414 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
1415
1416 let request = create_test_relayer_create_request(
1417 Some("test-relayer".to_string()),
1418 "Test Relayer",
1419 "test", "nonexistent-signer",
1421 None,
1422 );
1423
1424 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1425
1426 assert!(result.is_err());
1427 if let Err(ApiError::NotFound(msg)) = result {
1428 assert!(msg.contains("Signer with ID nonexistent-signer not found"));
1429 } else {
1430 panic!("Expected NotFound error for nonexistent signer");
1431 }
1432 }
1433
1434 #[actix_web::test]
1435 async fn test_create_relayer_nonexistent_network() {
1436 let signer = create_mock_signer();
1437 let app_state =
1438 create_mock_app_state(None, None, Some(vec![signer]), None, None, None).await;
1439
1440 let request = create_test_relayer_create_request(
1441 Some("test-relayer".to_string()),
1442 "Test Relayer",
1443 "nonexistent-network",
1444 "test", None,
1446 );
1447
1448 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1449
1450 assert!(result.is_err());
1451 if let Err(ApiError::BadRequest(msg)) = result {
1452 assert!(msg.contains("Network 'nonexistent-network' not found"));
1453 assert!(msg.contains("network configuration exists"));
1454 } else {
1455 panic!("Expected BadRequest error for nonexistent network");
1456 }
1457 }
1458
1459 #[actix_web::test]
1460 async fn test_create_relayer_signer_already_in_use() {
1461 let network = create_mock_network();
1462 let signer = create_mock_signer();
1463 let mut existing_relayer = create_mock_relayer("existing-relayer".to_string(), false);
1464 existing_relayer.signer_id = "test".to_string(); existing_relayer.network = "test".to_string(); let app_state = create_mock_app_state(
1467 None,
1468 Some(vec![existing_relayer]),
1469 Some(vec![signer]),
1470 Some(vec![network]),
1471 None,
1472 None,
1473 )
1474 .await;
1475
1476 let request = create_test_relayer_create_request(
1477 Some("test-relayer".to_string()),
1478 "Test Relayer",
1479 "test", "test", None,
1482 );
1483
1484 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1485
1486 assert!(result.is_err());
1487 if let Err(ApiError::BadRequest(msg)) = result {
1488 assert!(msg.contains("signer 'test' is already in use"));
1489 assert!(msg.contains("relayer 'existing-relayer'"));
1490 assert!(msg.contains("network 'test'"));
1491 assert!(msg.contains("security reasons"));
1492 } else {
1493 panic!("Expected BadRequest error for signer already in use");
1494 }
1495 }
1496
1497 #[actix_web::test]
1498 async fn test_create_relayer_nonexistent_notification() {
1499 let network = create_mock_network();
1500 let signer = create_mock_signer();
1501 let app_state = create_mock_app_state(
1502 None,
1503 None,
1504 Some(vec![signer]),
1505 Some(vec![network]),
1506 None,
1507 None,
1508 )
1509 .await;
1510
1511 let request = create_test_relayer_create_request(
1512 Some("test-relayer".to_string()),
1513 "Test Relayer",
1514 "test", "test", Some("nonexistent-notification".to_string()),
1517 );
1518
1519 let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1520
1521 assert!(result.is_err());
1522 if let Err(ApiError::NotFound(msg)) = result {
1523 assert!(msg.contains("Notification with ID 'nonexistent-notification' not found"));
1524 } else {
1525 panic!("Expected NotFound error for nonexistent notification");
1526 }
1527 }
1528
1529 #[actix_web::test]
1532 async fn test_list_relayers_success() {
1533 let relayer1 = create_mock_relayer("relayer-1".to_string(), false);
1534 let relayer2 = create_mock_relayer("relayer-2".to_string(), false);
1535 let app_state =
1536 create_mock_app_state(None, Some(vec![relayer1, relayer2]), None, None, None, None)
1537 .await;
1538
1539 let query = PaginationQuery {
1540 page: 1,
1541 per_page: 10,
1542 };
1543
1544 let result = list_relayers(query, actix_web::web::ThinData(app_state)).await;
1545
1546 assert!(result.is_ok());
1547 let response = result.unwrap();
1548 assert_eq!(response.status(), 200);
1549
1550 let body = to_bytes(response.into_body()).await.unwrap();
1551 let api_response: ApiResponse<Vec<RelayerResponse>> =
1552 serde_json::from_slice(&body).unwrap();
1553
1554 assert!(api_response.success);
1555 let data = api_response.data.unwrap();
1556 assert_eq!(data.len(), 2);
1557 }
1558
1559 #[actix_web::test]
1560 async fn test_list_relayers_empty() {
1561 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
1562
1563 let query = PaginationQuery {
1564 page: 1,
1565 per_page: 10,
1566 };
1567
1568 let result = list_relayers(query, actix_web::web::ThinData(app_state)).await;
1569
1570 assert!(result.is_ok());
1571 let response = result.unwrap();
1572 assert_eq!(response.status(), 200);
1573
1574 let body = to_bytes(response.into_body()).await.unwrap();
1575 let api_response: ApiResponse<Vec<RelayerResponse>> =
1576 serde_json::from_slice(&body).unwrap();
1577
1578 assert!(api_response.success);
1579 let data = api_response.data.unwrap();
1580 assert_eq!(data.len(), 0);
1581 }
1582
1583 fn tx_for_listing(
1587 id: &str,
1588 status: TransactionStatus,
1589 is_canceled: Option<bool>,
1590 ) -> TransactionRepoModel {
1591 let mut tx = create_mock_transaction();
1592 tx.id = id.to_string();
1593 tx.relayer_id = "test-relayer".to_string();
1594 tx.status = status;
1595 tx.is_canceled = is_canceled;
1596 tx
1597 }
1598
1599 fn returned_ids(body: &[u8]) -> Vec<String> {
1600 let api_response: ApiResponse<Vec<serde_json::Value>> =
1601 serde_json::from_slice(body).unwrap();
1602 assert!(api_response.success);
1603 api_response
1604 .data
1605 .unwrap()
1606 .iter()
1607 .map(|t| t["id"].as_str().unwrap().to_string())
1608 .collect()
1609 }
1610
1611 #[actix_web::test]
1614 async fn test_list_transactions_excludes_canceled_from_active_status() {
1615 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1616 let txs = vec![
1617 tx_for_listing("tx-submitted-1", TransactionStatus::Submitted, None),
1618 tx_for_listing("tx-submitted-2", TransactionStatus::Submitted, Some(false)),
1619 tx_for_listing(
1620 "tx-cancel-inflight",
1621 TransactionStatus::Submitted,
1622 Some(true),
1623 ),
1624 ];
1625 let app_state =
1626 create_mock_app_state(None, Some(vec![relayer]), None, None, None, Some(txs)).await;
1627
1628 let query = TransactionListQuery {
1629 page: 1,
1630 per_page: 10,
1631 status: Some(TransactionStatus::Submitted),
1632 };
1633
1634 let result =
1635 list_transactions("test-relayer".to_string(), query, web::ThinData(app_state)).await;
1636
1637 let response = result.unwrap();
1638 assert_eq!(response.status(), 200);
1639 let ids = returned_ids(&to_bytes(response.into_body()).await.unwrap());
1640
1641 assert!(
1642 !ids.contains(&"tx-cancel-inflight".to_string()),
1643 "cancelled-in-progress tx must not appear under ?status=Submitted, got {ids:?}"
1644 );
1645 assert_eq!(ids.len(), 2, "only the two non-cancelled txs should remain");
1646 }
1647
1648 #[actix_web::test]
1651 async fn test_list_transactions_includes_canceled_under_canceled_status() {
1652 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1653 let txs = vec![
1654 tx_for_listing("tx-submitted-1", TransactionStatus::Submitted, None),
1655 tx_for_listing("tx-canceled-done", TransactionStatus::Canceled, Some(true)),
1656 ];
1657 let app_state =
1658 create_mock_app_state(None, Some(vec![relayer]), None, None, None, Some(txs)).await;
1659
1660 let query = TransactionListQuery {
1661 page: 1,
1662 per_page: 10,
1663 status: Some(TransactionStatus::Canceled),
1664 };
1665
1666 let result =
1667 list_transactions("test-relayer".to_string(), query, web::ThinData(app_state)).await;
1668
1669 let response = result.unwrap();
1670 assert_eq!(response.status(), 200);
1671 let ids = returned_ids(&to_bytes(response.into_body()).await.unwrap());
1672
1673 assert_eq!(ids, vec!["tx-canceled-done".to_string()]);
1674 }
1675
1676 #[actix_web::test]
1679 async fn test_get_relayer_success() {
1680 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1681 let app_state =
1682 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1683
1684 let result = get_relayer(
1685 "test-relayer".to_string(),
1686 actix_web::web::ThinData(app_state),
1687 )
1688 .await;
1689
1690 assert!(result.is_ok());
1691 let response = result.unwrap();
1692 assert_eq!(response.status(), 200);
1693
1694 let body = to_bytes(response.into_body()).await.unwrap();
1695 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1696
1697 assert!(api_response.success);
1698 let data = api_response.data.unwrap();
1699 assert_eq!(data.id, "test-relayer");
1700 assert_eq!(data.name, "Relayer test-relayer"); }
1702
1703 #[actix_web::test]
1704 async fn test_get_relayer_not_found() {
1705 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
1706
1707 let result = get_relayer(
1708 "nonexistent".to_string(),
1709 actix_web::web::ThinData(app_state),
1710 )
1711 .await;
1712
1713 assert!(result.is_err());
1714 if let Err(ApiError::NotFound(msg)) = result {
1715 assert!(msg.contains("Relayer with ID nonexistent not found"));
1716 } else {
1717 panic!("Expected NotFound error");
1718 }
1719 }
1720
1721 #[actix_web::test]
1724 async fn test_update_relayer_success() {
1725 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1726 let app_state =
1727 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1728
1729 let patch = serde_json::json!({
1730 "name": "Updated Relayer Name",
1731 "paused": true
1732 });
1733
1734 let result = update_relayer(
1735 "test-relayer".to_string(),
1736 patch,
1737 actix_web::web::ThinData(app_state),
1738 )
1739 .await;
1740
1741 assert!(result.is_ok());
1742 let response = result.unwrap();
1743 assert_eq!(response.status(), 200);
1744
1745 let body = to_bytes(response.into_body()).await.unwrap();
1746 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1747
1748 assert!(api_response.success);
1749 let data = api_response.data.unwrap();
1750 assert_eq!(data.name, "Updated Relayer Name");
1751 assert!(data.paused);
1752 }
1753
1754 #[actix_web::test]
1755 async fn test_update_relayer_system_disabled() {
1756 let mut relayer = create_mock_relayer("disabled-relayer".to_string(), false);
1757 relayer.system_disabled = true;
1758 let app_state =
1759 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1760
1761 let patch = serde_json::json!({
1762 "name": "Updated Name"
1763 });
1764
1765 let result = update_relayer(
1766 "disabled-relayer".to_string(),
1767 patch,
1768 actix_web::web::ThinData(app_state),
1769 )
1770 .await;
1771
1772 assert!(result.is_err());
1773 if let Err(ApiError::BadRequest(msg)) = result {
1774 assert!(msg.contains("Relayer is disabled"));
1775 } else {
1776 panic!("Expected BadRequest error for disabled relayer");
1777 }
1778 }
1779
1780 #[actix_web::test]
1781 async fn test_update_relayer_invalid_patch() {
1782 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1783 let app_state =
1784 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1785
1786 let patch = serde_json::json!({
1787 "invalid_field": "value"
1788 });
1789
1790 let result = update_relayer(
1791 "test-relayer".to_string(),
1792 patch,
1793 actix_web::web::ThinData(app_state),
1794 )
1795 .await;
1796
1797 assert!(result.is_err());
1798 if let Err(ApiError::BadRequest(msg)) = result {
1799 assert!(msg.contains("Invalid update request"));
1800 } else {
1801 panic!("Expected BadRequest error for invalid patch");
1802 }
1803 }
1804
1805 #[actix_web::test]
1806 async fn test_update_relayer_nonexistent() {
1807 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
1808
1809 let patch = serde_json::json!({
1810 "name": "Updated Name"
1811 });
1812
1813 let result = update_relayer(
1814 "nonexistent-relayer".to_string(),
1815 patch,
1816 actix_web::web::ThinData(app_state),
1817 )
1818 .await;
1819
1820 assert!(result.is_err());
1821 if let Err(ApiError::NotFound(msg)) = result {
1822 assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
1823 } else {
1824 panic!("Expected NotFound error for nonexistent relayer");
1825 }
1826 }
1827
1828 #[actix_web::test]
1829 async fn test_update_relayer_set_evm_policies() {
1830 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1831 let app_state =
1832 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1833
1834 let patch = serde_json::json!({
1835 "policies": {
1836 "gas_price_cap": 50000000000u64,
1837 "min_balance": 1000000000000000000u64,
1838 "eip1559_pricing": true,
1839 "private_transactions": false,
1840 "gas_limit_estimation": true,
1841 "whitelist_receivers": ["0x1234567890123456789012345678901234567890"]
1842 }
1843 });
1844
1845 let result = update_relayer(
1846 "test-relayer".to_string(),
1847 patch,
1848 actix_web::web::ThinData(app_state),
1849 )
1850 .await;
1851
1852 assert!(result.is_ok());
1853 let response = result.unwrap();
1854 assert_eq!(response.status(), 200);
1855
1856 let body = to_bytes(response.into_body()).await.unwrap();
1857 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1858
1859 assert!(api_response.success);
1860 let data = api_response.data.unwrap();
1861
1862 assert!(data.policies.is_some());
1865 }
1866
1867 #[actix_web::test]
1868 async fn test_update_relayer_partial_policy_update() {
1869 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1870 let app_state =
1871 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1872
1873 let patch1 = serde_json::json!({
1875 "policies": {
1876 "gas_price_cap": 30000000000u64,
1877 "min_balance": 500000000000000000u64,
1878 "eip1559_pricing": false
1879 }
1880 });
1881
1882 let result1 = update_relayer(
1883 "test-relayer".to_string(),
1884 patch1,
1885 actix_web::web::ThinData(app_state),
1886 )
1887 .await;
1888
1889 assert!(result1.is_ok());
1890
1891 let relayer2 = create_mock_relayer("test-relayer".to_string(), false);
1893 let app_state2 =
1894 create_mock_app_state(None, Some(vec![relayer2]), None, None, None, None).await;
1895
1896 let patch2 = serde_json::json!({
1898 "policies": {
1899 "gas_price_cap": 60000000000u64
1900 }
1901 });
1902
1903 let result2 = update_relayer(
1904 "test-relayer".to_string(),
1905 patch2,
1906 actix_web::web::ThinData(app_state2),
1907 )
1908 .await;
1909
1910 assert!(result2.is_ok());
1911 let response = result2.unwrap();
1912 assert_eq!(response.status(), 200);
1913
1914 let body = to_bytes(response.into_body()).await.unwrap();
1915 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1916
1917 assert!(api_response.success);
1918 let data = api_response.data.unwrap();
1919
1920 assert!(data.policies.is_some());
1922 }
1923
1924 #[actix_web::test]
1925 async fn test_update_relayer_unset_notification() {
1926 let mut relayer = create_mock_relayer("test-relayer".to_string(), false);
1927 relayer.notification_id = Some("test-notification".to_string());
1928 let app_state =
1929 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1930
1931 let patch = serde_json::json!({
1932 "notification_id": null
1933 });
1934
1935 let result = update_relayer(
1936 "test-relayer".to_string(),
1937 patch,
1938 actix_web::web::ThinData(app_state),
1939 )
1940 .await;
1941
1942 assert!(result.is_ok());
1943 let response = result.unwrap();
1944 assert_eq!(response.status(), 200);
1945
1946 let body = to_bytes(response.into_body()).await.unwrap();
1947 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1948
1949 assert!(api_response.success);
1950 let data = api_response.data.unwrap();
1951 assert_eq!(data.notification_id, None);
1952 }
1953
1954 #[actix_web::test]
1955 async fn test_update_relayer_unset_custom_rpc_urls() {
1956 let mut relayer = create_mock_relayer("test-relayer".to_string(), false);
1957 relayer.custom_rpc_urls = Some(vec![crate::models::RpcConfig {
1958 url: "https://custom-rpc.example.com".to_string(),
1959 weight: 50,
1960 ..Default::default()
1961 }]);
1962 let app_state =
1963 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1964
1965 let patch = serde_json::json!({
1966 "custom_rpc_urls": null
1967 });
1968
1969 let result = update_relayer(
1970 "test-relayer".to_string(),
1971 patch,
1972 actix_web::web::ThinData(app_state),
1973 )
1974 .await;
1975
1976 assert!(result.is_ok());
1977 let response = result.unwrap();
1978 assert_eq!(response.status(), 200);
1979
1980 let body = to_bytes(response.into_body()).await.unwrap();
1981 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1982
1983 assert!(api_response.success);
1984 let data = api_response.data.unwrap();
1985 assert_eq!(data.custom_rpc_urls, None);
1986 }
1987
1988 #[actix_web::test]
1989 async fn test_update_relayer_set_custom_rpc_urls() {
1990 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1991 let app_state =
1992 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1993
1994 let patch = serde_json::json!({
1995 "custom_rpc_urls": [
1996 {
1997 "url": "https://rpc1.example.com",
1998 "weight": 80
1999 },
2000 {
2001 "url": "https://rpc2.example.com",
2002 "weight": 60
2003 }
2004 ]
2005 });
2006
2007 let result = update_relayer(
2008 "test-relayer".to_string(),
2009 patch,
2010 actix_web::web::ThinData(app_state),
2011 )
2012 .await;
2013
2014 assert!(result.is_ok());
2015 let response = result.unwrap();
2016 assert_eq!(response.status(), 200);
2017
2018 let body = to_bytes(response.into_body()).await.unwrap();
2019 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
2020
2021 assert!(api_response.success);
2022 let data = api_response.data.unwrap();
2023
2024 assert!(data.custom_rpc_urls.is_some());
2025 let rpc_urls = data.custom_rpc_urls.unwrap();
2026 assert_eq!(rpc_urls.len(), 2);
2027 assert_eq!(rpc_urls[0].url, "https://rpc1.example.com");
2028 assert_eq!(rpc_urls[0].weight, 80);
2029 assert_eq!(rpc_urls[1].url, "https://rpc2.example.com");
2030 assert_eq!(rpc_urls[1].weight, 60);
2031 }
2032
2033 #[actix_web::test]
2034 async fn test_update_relayer_clear_policies() {
2035 let relayer = create_mock_relayer("test-relayer".to_string(), false);
2036 let app_state =
2037 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2038
2039 let patch = serde_json::json!({
2040 "policies": null
2041 });
2042
2043 let result = update_relayer(
2044 "test-relayer".to_string(),
2045 patch,
2046 actix_web::web::ThinData(app_state),
2047 )
2048 .await;
2049
2050 assert!(result.is_ok());
2051 let response = result.unwrap();
2052 assert_eq!(response.status(), 200);
2053
2054 let body = to_bytes(response.into_body()).await.unwrap();
2055 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
2056
2057 assert!(api_response.success);
2058 let data = api_response.data.unwrap();
2059 assert_eq!(data.policies, None);
2060 }
2061
2062 #[actix_web::test]
2063 async fn test_update_relayer_invalid_policy_structure() {
2064 let relayer = create_mock_relayer("test-relayer".to_string(), false);
2065 let app_state =
2066 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2067
2068 let patch = serde_json::json!({
2069 "policies": {
2070 "invalid_field_name": "some_value"
2071 }
2072 });
2073
2074 let result = update_relayer(
2075 "test-relayer".to_string(),
2076 patch,
2077 actix_web::web::ThinData(app_state),
2078 )
2079 .await;
2080
2081 assert!(result.is_err());
2082 if let Err(ApiError::BadRequest(msg)) = result {
2083 assert!(msg.contains("Invalid policy"));
2084 } else {
2085 panic!("Expected BadRequest error for invalid policy structure");
2086 }
2087 }
2088
2089 #[actix_web::test]
2090 async fn test_update_relayer_invalid_evm_policy_values() {
2091 let relayer = create_mock_relayer("test-relayer".to_string(), false);
2092 let app_state =
2093 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2094
2095 let patch = serde_json::json!({
2096 "policies": {
2097 "gas_price_cap": "invalid_number",
2098 "min_balance": -1
2099 }
2100 });
2101
2102 let result = update_relayer(
2103 "test-relayer".to_string(),
2104 patch,
2105 actix_web::web::ThinData(app_state),
2106 )
2107 .await;
2108
2109 assert!(result.is_err());
2110 if let Err(ApiError::BadRequest(msg)) = result {
2111 assert!(msg.contains("Invalid policy") || msg.contains("Invalid update request"));
2112 } else {
2113 panic!("Expected BadRequest error for invalid policy values");
2114 }
2115 }
2116
2117 #[actix_web::test]
2118 async fn test_update_relayer_multiple_fields_at_once() {
2119 let mut relayer = create_mock_relayer("test-relayer".to_string(), false);
2120 relayer.notification_id = Some("old-notification".to_string());
2121 let app_state =
2122 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2123
2124 let patch = serde_json::json!({
2125 "name": "Multi-Update Relayer",
2126 "paused": true,
2127 "notification_id": null,
2128 "policies": {
2129 "gas_price_cap": 40000000000u64,
2130 "eip1559_pricing": true
2131 },
2132 "custom_rpc_urls": [
2133 {
2134 "url": "https://new-rpc.example.com",
2135 "weight": 90
2136 }
2137 ]
2138 });
2139
2140 let result = update_relayer(
2141 "test-relayer".to_string(),
2142 patch,
2143 actix_web::web::ThinData(app_state),
2144 )
2145 .await;
2146
2147 assert!(result.is_ok());
2148 let response = result.unwrap();
2149 assert_eq!(response.status(), 200);
2150
2151 let body = to_bytes(response.into_body()).await.unwrap();
2152 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
2153
2154 assert!(api_response.success);
2155 let data = api_response.data.unwrap();
2156
2157 assert_eq!(data.name, "Multi-Update Relayer");
2159 assert!(data.paused);
2160 assert_eq!(data.notification_id, None);
2161
2162 assert!(data.policies.is_some());
2164 assert!(data.custom_rpc_urls.is_some());
2165 let rpc_urls = data.custom_rpc_urls.unwrap();
2166 assert_eq!(rpc_urls.len(), 1);
2167 assert_eq!(rpc_urls[0].url, "https://new-rpc.example.com");
2168 assert_eq!(rpc_urls[0].weight, 90);
2169 }
2170
2171 #[actix_web::test]
2172 async fn test_update_relayer_solana_policies() {
2173 use crate::models::{
2174 NetworkType, RelayerNetworkPolicy, RelayerSolanaPolicy, SolanaFeePaymentStrategy,
2175 };
2176
2177 let mut solana_relayer = create_mock_relayer("test-solana-relayer".to_string(), false);
2179 solana_relayer.network_type = NetworkType::Solana;
2180 solana_relayer.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default());
2181
2182 let app_state =
2183 create_mock_app_state(None, Some(vec![solana_relayer]), None, None, None, None).await;
2184
2185 let patch = serde_json::json!({
2186 "policies": {
2187 "fee_payment_strategy": "user",
2188 "min_balance": 2000000,
2189 "max_signatures": 5,
2190 "max_tx_data_size": 800,
2191 "max_allowed_fee_lamports": 25000,
2192 "fee_margin_percentage": 15.0
2193 }
2194 });
2195
2196 let result = update_relayer(
2197 "test-solana-relayer".to_string(),
2198 patch,
2199 actix_web::web::ThinData(app_state),
2200 )
2201 .await;
2202
2203 assert!(result.is_ok());
2204 let response = result.unwrap();
2205 assert_eq!(response.status(), 200);
2206
2207 let body = to_bytes(response.into_body()).await.unwrap();
2208 let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
2209
2210 assert!(api_response.success);
2211 let data = api_response.data.unwrap();
2212
2213 assert!(data.policies.is_some());
2215 let policies = data.policies.unwrap();
2216 if let RelayerNetworkPolicyResponse::Solana(solana_policy) = policies {
2217 assert_eq!(
2218 solana_policy.fee_payment_strategy,
2219 Some(SolanaFeePaymentStrategy::User)
2220 );
2221 assert_eq!(solana_policy.min_balance, 2000000);
2222 assert_eq!(solana_policy.max_signatures, Some(5));
2223 assert_eq!(solana_policy.max_tx_data_size, 800);
2224 assert_eq!(solana_policy.max_allowed_fee_lamports, Some(25000));
2225 assert_eq!(solana_policy.fee_margin_percentage, Some(15.0));
2226 } else {
2227 panic!("Expected Solana policies in response");
2228 }
2229 }
2230
2231 #[actix_web::test]
2234 async fn test_delete_relayer_success() {
2235 let relayer = create_mock_relayer("test-relayer".to_string(), false);
2236 let app_state =
2237 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2238
2239 let result = delete_relayer(
2240 "test-relayer".to_string(),
2241 actix_web::web::ThinData(app_state),
2242 )
2243 .await;
2244
2245 assert!(result.is_ok());
2246 let response = result.unwrap();
2247 assert_eq!(response.status(), 200);
2248
2249 let body = to_bytes(response.into_body()).await.unwrap();
2250 let api_response: ApiResponse<String> = serde_json::from_slice(&body).unwrap();
2251
2252 assert!(api_response.success);
2253 let data = api_response.data.unwrap();
2254 assert!(data.contains("Relayer deleted successfully"));
2255 }
2256
2257 #[actix_web::test]
2258 async fn test_delete_relayer_with_transactions() {
2259 let relayer = create_mock_relayer("relayer-with-tx".to_string(), false);
2260 let mut transaction = create_mock_transaction();
2261 transaction.id = "test-tx".to_string();
2262 transaction.relayer_id = "relayer-with-tx".to_string();
2263 let app_state = create_mock_app_state(
2264 None,
2265 Some(vec![relayer]),
2266 None,
2267 None,
2268 None,
2269 Some(vec![transaction]),
2270 )
2271 .await;
2272
2273 let result = delete_relayer(
2274 "relayer-with-tx".to_string(),
2275 actix_web::web::ThinData(app_state),
2276 )
2277 .await;
2278
2279 assert!(result.is_err());
2280 if let Err(ApiError::BadRequest(msg)) = result {
2281 assert!(msg.contains("Cannot delete relayer 'relayer-with-tx'"));
2282 assert!(msg.contains("has 1 transaction(s)"));
2283 assert!(msg.contains("wait for all transactions to complete"));
2284 } else {
2285 panic!("Expected BadRequest error for relayer with transactions");
2286 }
2287 }
2288
2289 #[actix_web::test]
2290 async fn test_delete_relayer_nonexistent() {
2291 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
2292
2293 let result = delete_relayer(
2294 "nonexistent-relayer".to_string(),
2295 actix_web::web::ThinData(app_state),
2296 )
2297 .await;
2298
2299 assert!(result.is_err());
2300 if let Err(ApiError::NotFound(msg)) = result {
2301 assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
2302 } else {
2303 panic!("Expected NotFound error for nonexistent relayer");
2304 }
2305 }
2306
2307 #[actix_web::test]
2308 async fn test_sign_transaction_success() {
2309 let _lock = ENV_MUTEX.lock().await;
2310 setup_test_env();
2311 let network = create_mock_stellar_network();
2312 let signer = create_mock_signer();
2313 let mut relayer = create_mock_relayer("test-relayer".to_string(), false);
2314 relayer.network_type = NetworkType::Stellar;
2315 let app_state = create_mock_app_state(
2316 None,
2317 Some(vec![relayer]),
2318 Some(vec![signer]),
2319 Some(vec![network]),
2320 None,
2321 None,
2322 )
2323 .await;
2324
2325 let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
2326 unsigned_xdr: "test-unsigned-xdr".to_string(),
2327 });
2328
2329 let result = sign_transaction(
2330 "test-relayer".to_string(),
2331 request,
2332 actix_web::web::ThinData(app_state),
2333 )
2334 .await;
2335
2336 assert!(result.is_err());
2339 cleanup_test_env();
2340 }
2341
2342 #[actix_web::test]
2343 async fn test_sign_transaction_relayer_not_found() {
2344 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
2345
2346 let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
2347 unsigned_xdr: "test-unsigned-xdr".to_string(),
2348 });
2349
2350 let result = sign_transaction(
2351 "nonexistent-relayer".to_string(),
2352 request,
2353 actix_web::web::ThinData(app_state),
2354 )
2355 .await;
2356
2357 assert!(result.is_err());
2358 if let Err(ApiError::NotFound(msg)) = result {
2359 assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
2360 } else {
2361 panic!("Expected NotFound error for nonexistent relayer");
2362 }
2363 }
2364
2365 #[actix_web::test]
2366 async fn test_sign_transaction_relayer_disabled() {
2367 let mut relayer = create_mock_relayer("disabled-relayer".to_string(), false);
2368 relayer.paused = true;
2369 let app_state =
2370 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2371
2372 let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
2373 unsigned_xdr: "test-unsigned-xdr".to_string(),
2374 });
2375
2376 let result = sign_transaction(
2377 "disabled-relayer".to_string(),
2378 request,
2379 actix_web::web::ThinData(app_state),
2380 )
2381 .await;
2382
2383 assert!(result.is_err());
2384 if let Err(ApiError::ForbiddenError(msg)) = result {
2385 assert!(msg.contains("Relayer paused"));
2386 } else {
2387 panic!("Expected ForbiddenError for paused relayer");
2388 }
2389 }
2390
2391 #[actix_web::test]
2392 async fn test_sign_transaction_system_disabled() {
2393 let mut relayer = create_mock_relayer("system-disabled-relayer".to_string(), false);
2394 relayer.system_disabled = true;
2395 let app_state =
2396 create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2397
2398 let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
2399 unsigned_xdr: "test-unsigned-xdr".to_string(),
2400 });
2401
2402 let result = sign_transaction(
2403 "system-disabled-relayer".to_string(),
2404 request,
2405 actix_web::web::ThinData(app_state),
2406 )
2407 .await;
2408
2409 assert!(result.is_err());
2410 if let Err(ApiError::ForbiddenError(msg)) = result {
2411 assert!(msg.contains("Relayer disabled"));
2412 } else {
2413 panic!("Expected ForbiddenError for system disabled relayer");
2414 }
2415 }
2416
2417 #[actix_web::test]
2418 async fn test_quote_sponsored_transaction_relayer_not_found() {
2419 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
2420
2421 let request = SponsoredTransactionQuoteRequest::Stellar(
2422 crate::models::StellarFeeEstimateRequestParams {
2423 transaction_xdr: Some("test-xdr".to_string()),
2424 operations: None,
2425 source_account: None,
2426 fee_token: "native".to_string(),
2427 },
2428 );
2429
2430 let result = quote_sponsored_transaction(
2431 "nonexistent-relayer".to_string(),
2432 request,
2433 actix_web::web::ThinData(app_state),
2434 )
2435 .await;
2436
2437 assert!(result.is_err());
2438 if let Err(ApiError::NotFound(msg)) = result {
2439 assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
2440 } else {
2441 panic!("Expected NotFound error for nonexistent relayer");
2442 }
2443 }
2444
2445 #[actix_web::test]
2446 async fn test_quote_sponsored_transaction_relayer_disabled() {
2447 let mut relayer = create_mock_relayer("disabled-relayer".to_string(), false);
2448 relayer.paused = true;
2449 relayer.network_type = NetworkType::Stellar;
2450 relayer.policies = crate::models::RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
2451 fee_payment_strategy: Some(StellarFeePaymentStrategy::User),
2452 ..Default::default()
2453 });
2454 let network = create_mock_stellar_network();
2455 let signer = create_mock_signer();
2456 let app_state = create_mock_app_state(
2457 None,
2458 Some(vec![relayer]),
2459 Some(vec![signer]),
2460 Some(vec![network]),
2461 None,
2462 None,
2463 )
2464 .await;
2465
2466 let request = SponsoredTransactionQuoteRequest::Stellar(
2467 crate::models::StellarFeeEstimateRequestParams {
2468 transaction_xdr: Some("test-xdr".to_string()),
2469 operations: None,
2470 source_account: None,
2471 fee_token: "native".to_string(),
2472 },
2473 );
2474
2475 let result = quote_sponsored_transaction(
2476 "disabled-relayer".to_string(),
2477 request,
2478 actix_web::web::ThinData(app_state),
2479 )
2480 .await;
2481
2482 assert!(result.is_err());
2483 if let Err(ApiError::ForbiddenError(msg)) = result {
2484 assert!(msg.contains("Relayer paused"));
2485 } else {
2486 panic!("Expected ForbiddenError for paused relayer");
2487 }
2488 }
2489
2490 #[actix_web::test]
2491 async fn test_build_sponsored_transaction_relayer_not_found() {
2492 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
2493
2494 let request = SponsoredTransactionBuildRequest::Stellar(
2495 crate::models::StellarPrepareTransactionRequestParams {
2496 transaction_xdr: Some("test-xdr".to_string()),
2497 operations: None,
2498 source_account: None,
2499 fee_token: "native".to_string(),
2500 },
2501 );
2502
2503 let result = build_sponsored_transaction(
2504 "nonexistent-relayer".to_string(),
2505 request,
2506 actix_web::web::ThinData(app_state),
2507 )
2508 .await;
2509
2510 assert!(result.is_err());
2511 if let Err(ApiError::NotFound(msg)) = result {
2512 assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
2513 } else {
2514 panic!("Expected NotFound error for nonexistent relayer");
2515 }
2516 }
2517
2518 #[actix_web::test]
2519 async fn test_build_sponsored_transaction_relayer_disabled() {
2520 let mut relayer = create_mock_relayer("disabled-relayer".to_string(), false);
2521 relayer.paused = true;
2522 relayer.network_type = NetworkType::Stellar;
2523 relayer.policies = crate::models::RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
2524 fee_payment_strategy: Some(StellarFeePaymentStrategy::User),
2525 ..Default::default()
2526 });
2527 let network = create_mock_stellar_network();
2528 let signer = create_mock_signer();
2529 let app_state = create_mock_app_state(
2530 None,
2531 Some(vec![relayer]),
2532 Some(vec![signer]),
2533 Some(vec![network]),
2534 None,
2535 None,
2536 )
2537 .await;
2538
2539 let request = SponsoredTransactionBuildRequest::Stellar(
2540 crate::models::StellarPrepareTransactionRequestParams {
2541 transaction_xdr: Some("test-xdr".to_string()),
2542 operations: None,
2543 source_account: None,
2544 fee_token: "native".to_string(),
2545 },
2546 );
2547
2548 let result = build_sponsored_transaction(
2549 "disabled-relayer".to_string(),
2550 request,
2551 actix_web::web::ThinData(app_state),
2552 )
2553 .await;
2554
2555 assert!(result.is_err());
2556 if let Err(ApiError::ForbiddenError(msg)) = result {
2557 assert!(msg.contains("Relayer paused"));
2558 } else {
2559 panic!("Expected ForbiddenError for paused relayer");
2560 }
2561 }
2562}