openzeppelin_relayer/api/controllers/
relayer.rs

1//! # Relayer Controller
2//!
3//! Handles HTTP endpoints for relayer operations including:
4//! - Listing relayers
5//! - Getting relayer details
6//! - Creating relayers
7//! - Updating relayers
8//! - Deleting relayers
9//! - Submitting transactions
10//! - Signing messages
11//! - JSON-RPC proxy
12use 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
42/// Lists all relayers with pagination support.
43///
44/// # Arguments
45///
46/// * `query` - The pagination query parameters.
47/// * `state` - The application state containing the relayer repository.
48///
49/// # Returns
50///
51/// A paginated list of relayers.
52pub 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
82/// Retrieves details of a specific relayer by its ID.
83///
84/// # Arguments
85///
86/// * `relayer_id` - The ID of the relayer to retrieve.
87/// * `state` - The application state containing the relayer repository.
88///
89/// # Returns
90///
91/// The details of the specified relayer.
92pub 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
114/// Creates a new relayer.
115///
116/// # Arguments
117///
118/// * `request` - The relayer creation request.
119/// * `state` - The application state containing the relayer repository.
120///
121/// # Returns
122///
123/// The created relayer or an error if creation fails.
124///
125/// # Validation
126///
127/// This endpoint performs comprehensive dependency validation before creating the relayer:
128/// - **Signer Validation**: Ensures the specified signer exists in the system
129/// - **Signer Uniqueness**: Validates that the signer is not already in use by another relayer on the same network
130/// - **Notification Validation**: If a notification ID is provided, validates it exists
131/// - **Network Validation**: Confirms the specified network exists for the given network type
132///
133/// All validations must pass before the relayer is created, ensuring referential integrity and security constraints.
134pub 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    // Convert request to domain relayer (validates automatically)
150    let relayer = RelayerDomainModel::try_from(request)?;
151
152    // Check if signer exists
153    let signer_model = state
154        .signer_repository
155        .get_by_id(relayer.signer_id.clone())
156        .await?;
157
158    // Check if network exists for the given network type
159    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    // Check if signer is already in use by another relayer on the same network
173    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    // Check if notification exists (if provided)
185    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    // Convert domain model to repository model
193    let mut relayer_model = RelayerRepoModel::from(relayer);
194
195    // get address from signer and set it to relayer model
196    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
219/// Updates a relayer's information.
220///
221/// # Arguments
222///
223/// * `relayer_id` - The ID of the relayer to update.
224/// * `update_req` - The update request containing new relayer data.
225/// * `state` - The application state containing the relayer repository.
226///
227/// # Returns
228///
229/// The updated relayer information.
230pub 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    // convert patch to UpdateRelayerRequest to validate
249    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    // Check if notification exists (if setting one) by extracting from JSON patch
262    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    // Apply JSON merge patch directly to domain object
270    let updated_domain = RelayerDomainModel::from(relayer.clone())
271        .apply_json_patch(&patch)
272        .map_err(ApiError::from)?;
273
274    // Use existing RelayerRepoUpdater to preserve runtime fields
275    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
287/// Deletes a relayer by ID.
288///
289/// # Arguments
290///
291/// * `relayer_id` - The ID of the relayer to delete.
292/// * `state` - The application state containing the relayer repository.
293///
294/// # Returns
295///
296/// A success response or an error if deletion fails.
297///
298/// # Security
299///
300/// This endpoint ensures that relayers cannot be deleted if they have any pending
301/// or active transactions. This prevents data loss and maintains system integrity.
302pub 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    // Check if the relayer exists
318    let _relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
319
320    // Check if the relayer has any active transactions (pending or otherwise)
321    // Use optimized count_by_status
322    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    // Safe to delete - no transactions associated with this relayer
341    state.relayer_repository.delete_by_id(relayer_id).await?;
342
343    Ok(HttpResponse::Ok().json(ApiResponse::success("Relayer deleted successfully")))
344}
345
346/// Retrieves the status of a specific relayer.
347///
348/// # Arguments
349///
350/// * `relayer_id` - The ID of the relayer to check status for.
351/// * `state` - The application state containing the relayer repository.
352///
353/// # Returns
354///
355/// The status of the specified relayer.
356pub 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
378/// Retrieves the balance of a specific relayer.
379///
380/// # Arguments
381///
382/// * `relayer_id` - The ID of the relayer to check balance for.
383/// * `state` - The application state containing the relayer repository.
384///
385/// # Returns
386///
387/// The balance of the specified relayer.
388pub 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
410/// Sends a transaction through a specified relayer.
411///
412/// # Arguments
413///
414/// * `relayer_id` - The ID of the relayer to send the transaction through.
415/// * `request` - The transaction request data.
416/// * `state` - The application state containing the relayer repository.
417///
418/// # Returns
419///
420/// The response of the transaction processing.
421pub 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
443/// Retrieves a transaction by its ID for a specific relayer.
444///
445/// # Arguments
446///
447/// * `relayer_id` - The ID of the relayer.
448/// * `transaction_id` - The ID of the transaction to retrieve.
449/// * `state` - The application state containing the transaction repository.
450///
451/// # Returns
452///
453/// The details of the specified transaction.
454pub 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    // validation purpose only, checks if relayer exists
476    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
485/// Retrieves a transaction by its nonce for a specific relayer.
486///
487/// # Arguments
488///
489/// * `relayer_id` - The ID of the relayer.
490/// * `nonce` - The nonce of the transaction to retrieve.
491/// * `state` - The application state containing the transaction repository.
492///
493/// # Returns
494///
495/// The details of the specified transaction.
496pub 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    // get by nonce is only supported for EVM network
515    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
532/// Lists all transactions for a specific relayer with pagination support.
533///
534/// # Arguments
535///
536/// * `relayer_id` - The ID of the relayer.
537/// * `query` - The pagination query parameters.
538/// * `state` - The application state containing the transaction repository.
539///
540/// # Returns
541///
542/// A paginated list of transactions
543pub 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            // Hide cancelled-in-progress transactions from active-status listings: a
565            // cancelled tx keeps its non-terminal status with is_canceled=true until its
566            // replacement NOOP mines (then it becomes Canceled), so without this it would
567            // still appear under e.g. ?status=Submitted. We only exclude for non-terminal
568            // statuses, so ?status=Canceled still returns settled cancellations. The
569            // exclusion is applied in the repository (before pagination) so the page and
570            // total stay consistent.
571            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
604/// Deletes all pending transactions for a specific relayer.
605///
606/// # Arguments
607///
608/// * `relayer_id` - The ID of the relayer.
609/// * `state` - The application state containing the relayer repository.
610///
611/// # Returns
612///
613/// A success response with details about cancelled and failed transactions.
614pub 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
638/// Cancels a specific transaction for a relayer.
639///
640/// # Arguments
641///
642/// * `relayer_id` - The ID of the relayer.
643/// * `transaction_id` - The ID of the transaction to cancel.
644/// * `state` - The application state containing the transaction repository.
645///
646/// # Returns
647///
648/// The details of the canceled transaction.
649pub 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
670/// Replaces a specific transaction for a relayer.
671///
672/// # Arguments
673///
674/// * `relayer_id` - The ID of the relayer.
675/// * `transaction_id` - The ID of the transaction to replace.
676/// * `request` - The new transaction request data.
677/// * `state` - The application state containing the transaction repository.
678///
679/// # Returns
680///
681/// The details of the replaced transaction.
682pub 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
710/// Signs data using a specific relayer.
711///
712/// # Arguments
713///
714/// * `relayer_id` - The ID of the relayer.
715/// * `request` - The sign data request.
716/// * `state` - The application state containing the relayer repository.
717///
718/// # Returns
719///
720/// The signed data response.
721pub 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
750/// Signs typed data using a specific relayer.
751///
752/// # Arguments
753///
754/// * `relayer_id` - The ID of the relayer.
755/// * `request` - The sign typed data request.
756/// * `state` - The application state containing the relayer repository.
757///
758/// # Returns
759///
760/// The signed typed data response.
761pub 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
786/// Performs a JSON-RPC call through a specific relayer.
787///
788/// # Arguments
789///
790/// * `relayer_id` - The ID of the relayer.
791/// * `request` - The raw JSON-RPC request value.
792/// * `state` - The application state containing the relayer repository.
793///
794/// # Returns
795///
796/// The result of the JSON-RPC call.
797pub 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
823/// Signs a transaction using a specific relayer
824///
825/// # Arguments
826///
827/// * `relayer_id` - The ID of the relayer.
828/// * `request` - The sign transaction request containing unsigned XDR.
829/// * `state` - The application state containing the relayer repository.
830///
831/// # Returns
832///
833/// The signed transaction response.
834pub 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    // Get the network relayer and use its sign_transaction method
854    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
860/// Estimates fees for a transaction using gas abstraction.
861///
862/// # Arguments
863///
864/// * `relayer_id` - The ID of the relayer.
865/// * `params` - The fee estimate request parameters.
866/// * `state` - The application state containing the relayer repository.
867///
868/// # Returns
869///
870/// The fee estimate result.
871pub 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
898/// Prepares a transaction with fee payments using gas abstraction.
899///
900/// # Arguments
901///
902/// * `relayer_id` - The ID of the relayer.
903/// * `params` - The prepare transaction request parameters (network-agnostic).
904/// * `state` - The application state containing the relayer repository.
905///
906/// # Returns
907///
908/// The prepare transaction result.
909pub 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"); // noboost nosemgrep
962        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    /// Helper function to create a test relayer create request
971    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    /// Helper function to create a mock Solana network
992    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    /// Helper function to create a mock Stellar network
1015    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    // CREATE RELAYER TESTS
1042
1043    #[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", // Using "test" to match the mock network name
1063            "test", // Using "test" to match the mock signer id
1064            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"); // This one keeps custom name from the request
1080        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", // Using "test" to match the mock network name
1104            "test", // Using "test" to match the mock signer id
1105            None,
1106        );
1107
1108        // Add EVM policies
1109        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        // Verify policies are present in response
1136        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        // Add partial EVM policies
1165        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        // Verify partial policies are present in response
1188        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        // Change network type to Solana and add Solana policies
1217        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, // Simplified to avoid validation issues
1225            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        // Verify Solana policies are present in response
1248        assert!(data.policies.is_some());
1249        // verify policies are correct
1250        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        // Change network type to Stellar and add Stellar policies
1291        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        // Verify Stellar policies are present in response
1319        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        // Set network type to EVM but provide Solana policies (should fail)
1348        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        // Add notification manually since create_mock_app_state doesn't handle notifications
1382        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", // Using "test" to match the mock network name
1392            "test", // Using "test" to match the mock signer id
1393            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", // Using "test" to match the mock network name
1420            "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", // Using "test" to match the mock signer id
1445            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(); // Match the mock signer id
1465        existing_relayer.network = "test".to_string(); // Match the mock network name
1466        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", // Using "test" to match the mock network name
1480            "test", // Using "test" to match the mock signer id
1481            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", // Using "test" to match the mock network name
1515            "test", // Using "test" to match the mock signer id
1516            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    // LIST RELAYERS TESTS
1530
1531    #[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    // LIST TRANSACTIONS TESTS
1584
1585    /// Builds a transaction for `test-relayer` with a specific id, status and cancel flag.
1586    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    /// A cancelled-in-progress transaction (non-terminal status + is_canceled=true) must be
1612    /// hidden from active-status listings such as ?status=Submitted.
1613    #[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    /// Once the cancellation NOOP mines, the transaction becomes terminal Canceled and must
1649    /// remain visible under ?status=Canceled (the exclusion only applies to active statuses).
1650    #[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    // GET RELAYER TESTS
1677
1678    #[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"); // Mock utility creates name as "Relayer {id}"
1701    }
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    // UPDATE RELAYER TESTS
1722
1723    #[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        // For now, just verify that the policies field exists
1863        // The policy validation can be added once we understand the correct structure
1864        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        // First update with some policies
1874        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        // Create fresh app state for second update test
1892        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        // Second update with only gas_price_cap change
1897        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        // Just verify policies exist for now
1921        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        // Verify all fields were updated correctly
2158        assert_eq!(data.name, "Multi-Update Relayer");
2159        assert!(data.paused);
2160        assert_eq!(data.notification_id, None);
2161
2162        // Verify policies and RPC URLs were set
2163        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        // Create a Solana relayer (not the default EVM one)
2178        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        // Verify Solana policies are present and correctly updated
2214        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    // DELETE RELAYER TESTS
2232
2233    #[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        // The actual signing will fail in the mock environment, but we can test that
2337        // the function is callable with generics and processes the request
2338        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}