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        GasAbstractionTrait, Relayer, RelayerFactory, RelayerFactoryTrait, SignDataRequest,
17        SignDataResponse, SignTransactionRequest, SignTypedDataRequest, Transaction,
18    },
19    jobs::JobProducerTrait,
20    models::{
21        convert_to_internal_rpc_request, deserialize_policy_for_network_type,
22        transaction::request::{
23            SponsoredTransactionBuildRequest, SponsoredTransactionQuoteRequest,
24        },
25        ApiError, ApiResponse, CreateRelayerRequest, DefaultAppState, GetStatusOptions,
26        NetworkRepoModel, NetworkTransactionRequest, NetworkType, NotificationRepoModel,
27        PaginationMeta, PaginationQuery, Relayer as RelayerDomainModel, RelayerRepoModel,
28        RelayerRepoUpdater, RelayerResponse, Signer as SignerDomainModel, SignerRepoModel,
29        ThinDataAppState, TransactionRepoModel, TransactionResponse, TransactionStatus,
30        UpdateRelayerRequestRaw,
31    },
32    repositories::{
33        ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
34        Repository, TransactionCounterTrait, TransactionRepository,
35    },
36    services::signer::{Signer, SignerFactory},
37};
38use actix_web::{web, HttpResponse};
39use eyre::Result;
40
41/// Lists all relayers with pagination support.
42///
43/// # Arguments
44///
45/// * `query` - The pagination query parameters.
46/// * `state` - The application state containing the relayer repository.
47///
48/// # Returns
49///
50/// A paginated list of relayers.
51pub async fn list_relayers<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
52    query: PaginationQuery,
53    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
54) -> Result<HttpResponse, ApiError>
55where
56    J: JobProducerTrait + Send + Sync + 'static,
57    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
58    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
59    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
60    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
61    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
62    TCR: TransactionCounterTrait + Send + Sync + 'static,
63    PR: PluginRepositoryTrait + Send + Sync + 'static,
64    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
65{
66    let relayers = state.relayer_repository.list_paginated(query).await?;
67
68    let mapped_relayers: Vec<RelayerResponse> =
69        relayers.items.into_iter().map(|r| r.into()).collect();
70
71    Ok(HttpResponse::Ok().json(ApiResponse::paginated(
72        mapped_relayers,
73        PaginationMeta {
74            total_items: relayers.total,
75            current_page: relayers.page,
76            per_page: relayers.per_page,
77        },
78    )))
79}
80
81/// Retrieves details of a specific relayer by its ID.
82///
83/// # Arguments
84///
85/// * `relayer_id` - The ID of the relayer to retrieve.
86/// * `state` - The application state containing the relayer repository.
87///
88/// # Returns
89///
90/// The details of the specified relayer.
91pub async fn get_relayer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
92    relayer_id: String,
93    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
94) -> Result<HttpResponse, ApiError>
95where
96    J: JobProducerTrait + Send + Sync + 'static,
97    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
98    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
99    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
100    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
101    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
102    TCR: TransactionCounterTrait + Send + Sync + 'static,
103    PR: PluginRepositoryTrait + Send + Sync + 'static,
104    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
105{
106    let relayer = get_relayer_by_id(relayer_id, &state).await?;
107
108    let relayer_response: RelayerResponse = relayer.into();
109
110    Ok(HttpResponse::Ok().json(ApiResponse::success(relayer_response)))
111}
112
113/// Creates a new relayer.
114///
115/// # Arguments
116///
117/// * `request` - The relayer creation request.
118/// * `state` - The application state containing the relayer repository.
119///
120/// # Returns
121///
122/// The created relayer or an error if creation fails.
123///
124/// # Validation
125///
126/// This endpoint performs comprehensive dependency validation before creating the relayer:
127/// - **Signer Validation**: Ensures the specified signer exists in the system
128/// - **Signer Uniqueness**: Validates that the signer is not already in use by another relayer on the same network
129/// - **Notification Validation**: If a notification ID is provided, validates it exists
130/// - **Network Validation**: Confirms the specified network exists for the given network type
131///
132/// All validations must pass before the relayer is created, ensuring referential integrity and security constraints.
133pub async fn create_relayer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
134    request: CreateRelayerRequest,
135    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
136) -> Result<HttpResponse, ApiError>
137where
138    J: JobProducerTrait + Send + Sync + 'static,
139    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
140    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
141    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
142    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
143    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
144    TCR: TransactionCounterTrait + Send + Sync + 'static,
145    PR: PluginRepositoryTrait + Send + Sync + 'static,
146    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
147{
148    // Convert request to domain relayer (validates automatically)
149    let relayer = RelayerDomainModel::try_from(request)?;
150
151    // Check if signer exists
152    let signer_model = state
153        .signer_repository
154        .get_by_id(relayer.signer_id.clone())
155        .await?;
156
157    // Check if network exists for the given network type
158    let network = state
159        .network_repository
160        .get_by_name(relayer.network_type, &relayer.network)
161        .await?;
162
163    if network.is_none() {
164        return Err(ApiError::BadRequest(format!(
165            "Network '{}' not found for network type '{}'. Please ensure the network configuration exists.",
166            relayer.network,
167            relayer.network_type
168        )));
169    }
170
171    // Check if signer is already in use by another relayer on the same network
172    let relayers = state
173        .relayer_repository
174        .list_by_signer_id(&relayer.signer_id)
175        .await?;
176    if let Some(existing_relayer) = relayers.iter().find(|r| r.network == relayer.network) {
177        return Err(ApiError::BadRequest(format!(
178            "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.",
179            relayer.signer_id, existing_relayer.id, relayer.network
180        )));
181    }
182
183    // Check if notification exists (if provided)
184    if let Some(notification_id) = &relayer.notification_id {
185        let _notification = state
186            .notification_repository
187            .get_by_id(notification_id.clone())
188            .await?;
189    }
190
191    // Convert domain model to repository model
192    let mut relayer_model = RelayerRepoModel::from(relayer);
193
194    // get address from signer and set it to relayer model
195    let signer_service = SignerFactory::create_signer(
196        &relayer_model.network_type,
197        &SignerDomainModel::from(signer_model.clone()),
198    )
199    .await
200    .map_err(|e| ApiError::InternalError(e.to_string()))?;
201    let address = signer_service
202        .address()
203        .await
204        .map_err(|e| ApiError::InternalError(e.to_string()))?;
205    relayer_model.address = address.to_string();
206
207    let created_relayer = state.relayer_repository.create(relayer_model).await?;
208
209    let relayer =
210        RelayerFactory::create_relayer(created_relayer.clone(), signer_model, &state).await?;
211
212    relayer.initialize_relayer().await?;
213
214    let response = RelayerResponse::from(created_relayer);
215    Ok(HttpResponse::Created().json(ApiResponse::success(response)))
216}
217
218/// Updates a relayer's information.
219///
220/// # Arguments
221///
222/// * `relayer_id` - The ID of the relayer to update.
223/// * `update_req` - The update request containing new relayer data.
224/// * `state` - The application state containing the relayer repository.
225///
226/// # Returns
227///
228/// The updated relayer information.
229pub async fn update_relayer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
230    relayer_id: String,
231    patch: serde_json::Value,
232    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
233) -> Result<HttpResponse, ApiError>
234where
235    J: JobProducerTrait + Send + Sync + 'static,
236    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
237    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
238    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
239    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
240    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
241    TCR: TransactionCounterTrait + Send + Sync + 'static,
242    PR: PluginRepositoryTrait + Send + Sync + 'static,
243    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
244{
245    let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
246
247    // convert patch to UpdateRelayerRequest to validate
248    let update_request: UpdateRelayerRequestRaw = serde_json::from_value(patch.clone())
249        .map_err(|e| ApiError::BadRequest(format!("Invalid update request: {e}")))?;
250
251    if let Some(policies) = update_request.policies {
252        deserialize_policy_for_network_type(&policies, relayer.network_type)
253            .map_err(|e| ApiError::BadRequest(format!("Invalid policy: {e}")))?;
254    }
255
256    if relayer.system_disabled {
257        return Err(ApiError::BadRequest("Relayer is disabled".into()));
258    }
259
260    // Check if notification exists (if setting one) by extracting from JSON patch
261    if let Some(notification_id) = update_request.notification_id {
262        state
263            .notification_repository
264            .get_by_id(notification_id.to_string())
265            .await?;
266    }
267
268    // Apply JSON merge patch directly to domain object
269    let updated_domain = RelayerDomainModel::from(relayer.clone())
270        .apply_json_patch(&patch)
271        .map_err(ApiError::from)?;
272
273    // Use existing RelayerRepoUpdater to preserve runtime fields
274    let updated_repo_model =
275        RelayerRepoUpdater::from_existing(relayer).apply_domain_update(updated_domain);
276
277    let saved_relayer = state
278        .relayer_repository
279        .update(relayer_id.clone(), updated_repo_model)
280        .await?;
281
282    let relayer_response: RelayerResponse = saved_relayer.into();
283    Ok(HttpResponse::Ok().json(ApiResponse::success(relayer_response)))
284}
285
286/// Deletes a relayer by ID.
287///
288/// # Arguments
289///
290/// * `relayer_id` - The ID of the relayer to delete.
291/// * `state` - The application state containing the relayer repository.
292///
293/// # Returns
294///
295/// A success response or an error if deletion fails.
296///
297/// # Security
298///
299/// This endpoint ensures that relayers cannot be deleted if they have any pending
300/// or active transactions. This prevents data loss and maintains system integrity.
301pub async fn delete_relayer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
302    relayer_id: String,
303    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
304) -> Result<HttpResponse, ApiError>
305where
306    J: JobProducerTrait + Send + Sync + 'static,
307    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
308    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
309    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
310    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
311    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
312    TCR: TransactionCounterTrait + Send + Sync + 'static,
313    PR: PluginRepositoryTrait + Send + Sync + 'static,
314    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
315{
316    // Check if the relayer exists
317    let _relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
318
319    // Check if the relayer has any active transactions (pending or otherwise)
320    // Use optimized count_by_status
321    let active_transaction_count = state
322        .transaction_repository
323        .count_by_status(
324            &relayer_id,
325            &[
326                TransactionStatus::Pending,
327                TransactionStatus::Sent,
328                TransactionStatus::Submitted,
329            ],
330        )
331        .await?;
332
333    if active_transaction_count > 0 {
334        return Err(ApiError::BadRequest(format!(
335            "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.",
336        )));
337    }
338
339    // Safe to delete - no transactions associated with this relayer
340    state.relayer_repository.delete_by_id(relayer_id).await?;
341
342    Ok(HttpResponse::Ok().json(ApiResponse::success("Relayer deleted successfully")))
343}
344
345/// Retrieves the status of a specific relayer.
346///
347/// # Arguments
348///
349/// * `relayer_id` - The ID of the relayer to check status for.
350/// * `state` - The application state containing the relayer repository.
351///
352/// # Returns
353///
354/// The status of the specified relayer.
355pub async fn get_relayer_status<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
356    relayer_id: String,
357    options: GetStatusOptions,
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(options).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: PaginationQuery,
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 transactions = state
562        .transaction_repository
563        .find_by_relayer_id(&relayer_id, query)
564        .await?;
565
566    let transaction_response_list: Vec<TransactionResponse> =
567        transactions.items.into_iter().map(|t| t.into()).collect();
568
569    Ok(HttpResponse::Ok().json(ApiResponse::paginated(
570        transaction_response_list,
571        PaginationMeta {
572            total_items: transactions.total,
573            current_page: transactions.page,
574            per_page: transactions.per_page,
575        },
576    )))
577}
578
579/// Deletes all pending transactions for a specific relayer.
580///
581/// # Arguments
582///
583/// * `relayer_id` - The ID of the relayer.
584/// * `state` - The application state containing the relayer repository.
585///
586/// # Returns
587///
588/// A success response with details about cancelled and failed transactions.
589pub async fn delete_pending_transactions<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
590    relayer_id: String,
591    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
592) -> Result<HttpResponse, ApiError>
593where
594    J: JobProducerTrait + Send + Sync + 'static,
595    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
596    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
597    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
598    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
599    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
600    TCR: TransactionCounterTrait + Send + Sync + 'static,
601    PR: PluginRepositoryTrait + Send + Sync + 'static,
602    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
603{
604    let relayer = get_relayer_by_id(relayer_id, &state).await?;
605    relayer.validate_active_state()?;
606    let network_relayer = get_network_relayer_by_model(relayer.clone(), &state).await?;
607
608    let result = network_relayer.delete_pending_transactions().await?;
609
610    Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
611}
612
613/// Cancels a specific transaction for a relayer.
614///
615/// # Arguments
616///
617/// * `relayer_id` - The ID of the relayer.
618/// * `transaction_id` - The ID of the transaction to cancel.
619/// * `state` - The application state containing the transaction repository.
620///
621/// # Returns
622///
623/// The details of the canceled transaction.
624pub async fn cancel_transaction(
625    relayer_id: String,
626    transaction_id: String,
627    state: web::ThinData<DefaultAppState>,
628) -> Result<HttpResponse, ApiError> {
629    let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
630    relayer.validate_active_state()?;
631
632    let relayer_transaction = get_relayer_transaction_by_model(relayer.clone(), &state).await?;
633
634    let transaction_to_cancel = get_tx_by_id(transaction_id, &state).await?;
635
636    let canceled_transaction = relayer_transaction
637        .cancel_transaction(transaction_to_cancel)
638        .await?;
639
640    let transaction_response: TransactionResponse = canceled_transaction.into();
641
642    Ok(HttpResponse::Ok().json(ApiResponse::success(transaction_response)))
643}
644
645/// Replaces a specific transaction for a relayer.
646///
647/// # Arguments
648///
649/// * `relayer_id` - The ID of the relayer.
650/// * `transaction_id` - The ID of the transaction to replace.
651/// * `request` - The new transaction request data.
652/// * `state` - The application state containing the transaction repository.
653///
654/// # Returns
655///
656/// The details of the replaced transaction.
657pub async fn replace_transaction(
658    relayer_id: String,
659    transaction_id: String,
660    request: serde_json::Value,
661    state: web::ThinData<DefaultAppState>,
662) -> Result<HttpResponse, ApiError> {
663    let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
664    relayer.validate_active_state()?;
665
666    let new_tx_request: NetworkTransactionRequest =
667        NetworkTransactionRequest::from_json(&relayer.network_type, request.clone())?;
668    new_tx_request.validate(&relayer)?;
669
670    let transaction_to_replace = state
671        .transaction_repository
672        .get_by_id(transaction_id)
673        .await?;
674
675    let relayer_transaction = get_relayer_transaction_by_model(relayer.clone(), &state).await?;
676    let replaced_transaction = relayer_transaction
677        .replace_transaction(transaction_to_replace, new_tx_request)
678        .await?;
679
680    let transaction_response: TransactionResponse = replaced_transaction.into();
681
682    Ok(HttpResponse::Ok().json(ApiResponse::success(transaction_response)))
683}
684
685/// Signs data using a specific relayer.
686///
687/// # Arguments
688///
689/// * `relayer_id` - The ID of the relayer.
690/// * `request` - The sign data request.
691/// * `state` - The application state containing the relayer repository.
692///
693/// # Returns
694///
695/// The signed data response.
696pub async fn sign_data<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
697    relayer_id: String,
698    request: SignDataRequest,
699    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
700) -> Result<HttpResponse, ApiError>
701where
702    J: JobProducerTrait + Send + Sync + 'static,
703    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
704    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
705    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
706    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
707    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
708    TCR: TransactionCounterTrait + Send + Sync + 'static,
709    PR: PluginRepositoryTrait + Send + Sync + 'static,
710    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
711{
712    let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
713    relayer.validate_active_state()?;
714    let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
715
716    let result = network_relayer.sign_data(request).await?;
717
718    if let SignDataResponse::Evm(sign) = result {
719        Ok(HttpResponse::Ok().json(ApiResponse::success(sign)))
720    } else {
721        Err(ApiError::NotSupported("Sign data not supported".into()))
722    }
723}
724
725/// Signs typed data using a specific relayer.
726///
727/// # Arguments
728///
729/// * `relayer_id` - The ID of the relayer.
730/// * `request` - The sign typed data request.
731/// * `state` - The application state containing the relayer repository.
732///
733/// # Returns
734///
735/// The signed typed data response.
736pub async fn sign_typed_data<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
737    relayer_id: String,
738    request: SignTypedDataRequest,
739    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
740) -> Result<HttpResponse, ApiError>
741where
742    J: JobProducerTrait + Send + Sync + 'static,
743    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
744    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
745    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
746    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
747    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
748    TCR: TransactionCounterTrait + Send + Sync + 'static,
749    PR: PluginRepositoryTrait + Send + Sync + 'static,
750    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
751{
752    let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
753    relayer.validate_active_state()?;
754    let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
755
756    let result = network_relayer.sign_typed_data(request).await?;
757
758    Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
759}
760
761/// Performs a JSON-RPC call through a specific relayer.
762///
763/// # Arguments
764///
765/// * `relayer_id` - The ID of the relayer.
766/// * `request` - The raw JSON-RPC request value.
767/// * `state` - The application state containing the relayer repository.
768///
769/// # Returns
770///
771/// The result of the JSON-RPC call.
772pub async fn relayer_rpc<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
773    relayer_id: String,
774    request: serde_json::Value,
775    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
776) -> Result<HttpResponse, ApiError>
777where
778    J: JobProducerTrait + Send + Sync + 'static,
779    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
780    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
781    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
782    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
783    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
784    TCR: TransactionCounterTrait + Send + Sync + 'static,
785    PR: PluginRepositoryTrait + Send + Sync + 'static,
786    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
787{
788    let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
789    relayer.validate_active_state()?;
790    let network_relayer = get_network_relayer_by_model(relayer.clone(), &state).await?;
791
792    let internal_request = convert_to_internal_rpc_request(request, &relayer.network_type)?;
793    let result = network_relayer.rpc(internal_request).await?;
794
795    Ok(HttpResponse::Ok().json(result))
796}
797
798/// Signs a transaction using a specific relayer
799///
800/// # Arguments
801///
802/// * `relayer_id` - The ID of the relayer.
803/// * `request` - The sign transaction request containing unsigned XDR.
804/// * `state` - The application state containing the relayer repository.
805///
806/// # Returns
807///
808/// The signed transaction response.
809pub async fn sign_transaction<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
810    relayer_id: String,
811    request: SignTransactionRequest,
812    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
813) -> Result<HttpResponse, ApiError>
814where
815    J: JobProducerTrait + Send + Sync + 'static,
816    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
817    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
818    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
819    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
820    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
821    TCR: TransactionCounterTrait + Send + Sync + 'static,
822    PR: PluginRepositoryTrait + Send + Sync + 'static,
823    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
824{
825    let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
826    relayer.validate_active_state()?;
827
828    // Get the network relayer and use its sign_transaction method
829    let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
830    let result = network_relayer.sign_transaction(&request).await?;
831
832    Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
833}
834
835/// Estimates fees for a transaction using gas abstraction.
836///
837/// # Arguments
838///
839/// * `relayer_id` - The ID of the relayer.
840/// * `params` - The fee estimate request parameters.
841/// * `state` - The application state containing the relayer repository.
842///
843/// # Returns
844///
845/// The fee estimate result.
846pub async fn quote_sponsored_transaction<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
847    relayer_id: String,
848    request: SponsoredTransactionQuoteRequest,
849    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
850) -> Result<HttpResponse, ApiError>
851where
852    J: JobProducerTrait + Send + Sync + 'static,
853    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
854    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
855    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
856    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
857    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
858    TCR: TransactionCounterTrait + Send + Sync + 'static,
859    PR: PluginRepositoryTrait + Send + Sync + 'static,
860    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
861{
862    let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
863    relayer.validate_active_state()?;
864
865    request.validate()?;
866
867    let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
868
869    let result = network_relayer.quote_sponsored_transaction(request).await?;
870    Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
871}
872
873/// Prepares a transaction with fee payments using gas abstraction.
874///
875/// # Arguments
876///
877/// * `relayer_id` - The ID of the relayer.
878/// * `params` - The prepare transaction request parameters (network-agnostic).
879/// * `state` - The application state containing the relayer repository.
880///
881/// # Returns
882///
883/// The prepare transaction result.
884pub async fn build_sponsored_transaction<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
885    relayer_id: String,
886    request: SponsoredTransactionBuildRequest,
887    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
888) -> Result<HttpResponse, ApiError>
889where
890    J: JobProducerTrait + Send + Sync + 'static,
891    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
892    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
893    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
894    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
895    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
896    TCR: TransactionCounterTrait + Send + Sync + 'static,
897    PR: PluginRepositoryTrait + Send + Sync + 'static,
898    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
899{
900    let relayer = get_relayer_by_id(relayer_id.clone(), &state).await?;
901    relayer.validate_active_state()?;
902
903    request.validate()?;
904
905    let network_relayer = get_network_relayer_by_model(relayer, &state).await?;
906
907    let result = network_relayer.build_sponsored_transaction(request).await?;
908    Ok(HttpResponse::Ok().json(ApiResponse::success(result)))
909}
910
911#[cfg(test)]
912mod tests {
913    use super::*;
914    use crate::{
915        domain::SignTransactionRequestStellar,
916        models::{
917            ApiResponse, CreateRelayerPolicyRequest, CreateRelayerRequest, RelayerEvmPolicy,
918            RelayerNetworkPolicyResponse, RelayerNetworkType, RelayerResponse, RelayerSolanaPolicy,
919            RelayerStellarPolicy, SolanaFeePaymentStrategy, StellarFeePaymentStrategy,
920        },
921        utils::mocks::mockutils::{
922            create_mock_app_state, create_mock_network, create_mock_notification,
923            create_mock_relayer, create_mock_signer, create_mock_transaction,
924        },
925    };
926    use actix_web::body::to_bytes;
927    use lazy_static::lazy_static;
928    use std::env;
929    use tokio::sync::Mutex;
930
931    lazy_static! {
932        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
933    }
934
935    fn setup_test_env() {
936        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D"); // noboost nosemgrep
937        env::set_var("REDIS_URL", "redis://localhost:6379");
938    }
939
940    fn cleanup_test_env() {
941        env::remove_var("API_KEY");
942        env::remove_var("REDIS_URL");
943    }
944
945    /// Helper function to create a test relayer create request
946    fn create_test_relayer_create_request(
947        id: Option<String>,
948        name: &str,
949        network: &str,
950        signer_id: &str,
951        notification_id: Option<String>,
952    ) -> CreateRelayerRequest {
953        CreateRelayerRequest {
954            id,
955            name: name.to_string(),
956            network: network.to_string(),
957            network_type: RelayerNetworkType::Evm,
958            paused: false,
959            policies: None,
960            signer_id: signer_id.to_string(),
961            notification_id,
962            custom_rpc_urls: None,
963        }
964    }
965
966    /// Helper function to create a mock Solana network
967    fn create_mock_solana_network() -> crate::models::NetworkRepoModel {
968        use crate::config::{NetworkConfigCommon, SolanaNetworkConfig};
969        use crate::models::{NetworkConfigData, NetworkRepoModel, NetworkType, RpcConfig};
970
971        NetworkRepoModel {
972            id: "test".to_string(),
973            name: "test".to_string(),
974            network_type: NetworkType::Solana,
975            config: NetworkConfigData::Solana(SolanaNetworkConfig {
976                common: NetworkConfigCommon {
977                    network: "test".to_string(),
978                    from: None,
979                    rpc_urls: Some(vec![RpcConfig::new("http://localhost:8899".to_string())]),
980                    explorer_urls: None,
981                    average_blocktime_ms: Some(400),
982                    is_testnet: Some(true),
983                    tags: None,
984                },
985            }),
986        }
987    }
988
989    /// Helper function to create a mock Stellar network
990    fn create_mock_stellar_network() -> crate::models::NetworkRepoModel {
991        use crate::config::{NetworkConfigCommon, StellarNetworkConfig};
992        use crate::models::{NetworkConfigData, NetworkRepoModel, NetworkType, RpcConfig};
993
994        NetworkRepoModel {
995            id: "test".to_string(),
996            name: "test".to_string(),
997            network_type: NetworkType::Stellar,
998            config: NetworkConfigData::Stellar(StellarNetworkConfig {
999                common: NetworkConfigCommon {
1000                    network: "test".to_string(),
1001                    from: None,
1002                    rpc_urls: Some(vec![RpcConfig::new(
1003                        "https://horizon-testnet.stellar.org".to_string(),
1004                    )]),
1005                    explorer_urls: None,
1006                    average_blocktime_ms: Some(5000),
1007                    is_testnet: Some(true),
1008                    tags: None,
1009                },
1010                passphrase: Some("Test Network ; September 2015".to_string()),
1011                horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
1012            }),
1013        }
1014    }
1015
1016    // CREATE RELAYER TESTS
1017
1018    #[actix_web::test]
1019    async fn test_create_relayer_success() {
1020        let _lock = ENV_MUTEX.lock().await;
1021        setup_test_env();
1022        let network = create_mock_network();
1023        let signer = create_mock_signer();
1024        let app_state = create_mock_app_state(
1025            None,
1026            None,
1027            Some(vec![signer]),
1028            Some(vec![network]),
1029            None,
1030            None,
1031        )
1032        .await;
1033
1034        let request = create_test_relayer_create_request(
1035            Some("test-relayer".to_string()),
1036            "Test Relayer",
1037            "test", // Using "test" to match the mock network name
1038            "test", // Using "test" to match the mock signer id
1039            None,
1040        );
1041
1042        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1043
1044        assert!(result.is_ok());
1045        let response = result.unwrap();
1046        assert_eq!(response.status(), 201);
1047
1048        let body = to_bytes(response.into_body()).await.unwrap();
1049        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1050
1051        assert!(api_response.success);
1052        let data = api_response.data.unwrap();
1053        assert_eq!(data.id, "test-relayer");
1054        assert_eq!(data.name, "Test Relayer"); // This one keeps custom name from the request
1055        assert_eq!(data.network, "test");
1056        cleanup_test_env();
1057    }
1058
1059    #[actix_web::test]
1060    async fn test_create_relayer_with_evm_policies() {
1061        let _lock = ENV_MUTEX.lock().await;
1062        setup_test_env();
1063        let network = create_mock_network();
1064        let signer = create_mock_signer();
1065        let app_state = create_mock_app_state(
1066            None,
1067            None,
1068            Some(vec![signer]),
1069            Some(vec![network]),
1070            None,
1071            None,
1072        )
1073        .await;
1074
1075        let mut request = create_test_relayer_create_request(
1076            Some("test-relayer-policies".to_string()),
1077            "Test Relayer with Policies",
1078            "test", // Using "test" to match the mock network name
1079            "test", // Using "test" to match the mock signer id
1080            None,
1081        );
1082
1083        // Add EVM policies
1084        request.policies = Some(CreateRelayerPolicyRequest::Evm(RelayerEvmPolicy {
1085            gas_price_cap: Some(50000000000),
1086            min_balance: Some(1000000000000000000),
1087            eip1559_pricing: Some(true),
1088            private_transactions: Some(false),
1089            gas_limit_estimation: Some(true),
1090            whitelist_receivers: Some(vec![
1091                "0x1234567890123456789012345678901234567890".to_string()
1092            ]),
1093        }));
1094
1095        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1096
1097        assert!(result.is_ok());
1098        let response = result.unwrap();
1099        assert_eq!(response.status(), 201);
1100
1101        let body = to_bytes(response.into_body()).await.unwrap();
1102        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1103
1104        assert!(api_response.success);
1105        let data = api_response.data.unwrap();
1106        assert_eq!(data.id, "test-relayer-policies");
1107        assert_eq!(data.name, "Test Relayer with Policies");
1108        assert_eq!(data.network, "test");
1109
1110        // Verify policies are present in response
1111        assert!(data.policies.is_some());
1112        cleanup_test_env();
1113    }
1114
1115    #[actix_web::test]
1116    async fn test_create_relayer_with_partial_evm_policies() {
1117        let _lock = ENV_MUTEX.lock().await;
1118        setup_test_env();
1119        let network = create_mock_network();
1120        let signer = create_mock_signer();
1121        let app_state = create_mock_app_state(
1122            None,
1123            None,
1124            Some(vec![signer]),
1125            Some(vec![network]),
1126            None,
1127            None,
1128        )
1129        .await;
1130
1131        let mut request = create_test_relayer_create_request(
1132            Some("test-relayer-partial".to_string()),
1133            "Test Relayer with Partial Policies",
1134            "test",
1135            "test",
1136            None,
1137        );
1138
1139        // Add partial EVM policies
1140        request.policies = Some(CreateRelayerPolicyRequest::Evm(RelayerEvmPolicy {
1141            gas_price_cap: Some(30000000000),
1142            eip1559_pricing: Some(false),
1143            min_balance: None,
1144            private_transactions: None,
1145            gas_limit_estimation: None,
1146            whitelist_receivers: None,
1147        }));
1148
1149        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1150
1151        assert!(result.is_ok());
1152        let response = result.unwrap();
1153        assert_eq!(response.status(), 201);
1154
1155        let body = to_bytes(response.into_body()).await.unwrap();
1156        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1157
1158        assert!(api_response.success);
1159        let data = api_response.data.unwrap();
1160        assert_eq!(data.id, "test-relayer-partial");
1161
1162        // Verify partial policies are present in response
1163        assert!(data.policies.is_some());
1164        cleanup_test_env();
1165    }
1166
1167    #[actix_web::test]
1168    async fn test_create_relayer_with_solana_policies() {
1169        let _lock = ENV_MUTEX.lock().await;
1170        setup_test_env();
1171        let network = create_mock_solana_network();
1172        let signer = create_mock_signer();
1173        let app_state = create_mock_app_state(
1174            None,
1175            None,
1176            Some(vec![signer]),
1177            Some(vec![network]),
1178            None,
1179            None,
1180        )
1181        .await;
1182
1183        let mut request = create_test_relayer_create_request(
1184            Some("test-solana-relayer".to_string()),
1185            "Test Solana Relayer",
1186            "test",
1187            "test",
1188            None,
1189        );
1190
1191        // Change network type to Solana and add Solana policies
1192        request.network_type = RelayerNetworkType::Solana;
1193        request.policies = Some(CreateRelayerPolicyRequest::Solana(RelayerSolanaPolicy {
1194            fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
1195            min_balance: Some(5000000),
1196            max_signatures: Some(10),
1197            max_tx_data_size: Some(1232),
1198            max_allowed_fee_lamports: Some(50000),
1199            allowed_programs: None, // Simplified to avoid validation issues
1200            allowed_tokens: None,
1201            fee_margin_percentage: Some(10.0),
1202            allowed_accounts: None,
1203            disallowed_accounts: None,
1204            swap_config: None,
1205        }));
1206
1207        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1208
1209        assert!(result.is_ok());
1210        let response = result.unwrap();
1211        assert_eq!(response.status(), 201);
1212
1213        let body = to_bytes(response.into_body()).await.unwrap();
1214        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1215
1216        assert!(api_response.success);
1217        let data = api_response.data.unwrap();
1218        assert_eq!(data.id, "test-solana-relayer");
1219        assert_eq!(data.network_type, RelayerNetworkType::Solana);
1220        assert_eq!(data.name, "Test Solana Relayer");
1221
1222        // Verify Solana policies are present in response
1223        assert!(data.policies.is_some());
1224        // verify policies are correct
1225        let policies = data.policies.unwrap();
1226        if let RelayerNetworkPolicyResponse::Solana(solana_policy) = policies {
1227            assert_eq!(
1228                solana_policy.fee_payment_strategy,
1229                Some(SolanaFeePaymentStrategy::Relayer)
1230            );
1231            assert_eq!(solana_policy.min_balance, 5000000);
1232            assert_eq!(solana_policy.max_signatures, Some(10));
1233            assert_eq!(solana_policy.max_tx_data_size, 1232);
1234            assert_eq!(solana_policy.max_allowed_fee_lamports, Some(50000));
1235        } else {
1236            panic!("Expected Solana policies");
1237        }
1238        cleanup_test_env();
1239    }
1240
1241    #[actix_web::test]
1242    async fn test_create_relayer_with_stellar_policies() {
1243        let _lock = ENV_MUTEX.lock().await;
1244        setup_test_env();
1245        let network = create_mock_stellar_network();
1246        let signer = create_mock_signer();
1247        let app_state = create_mock_app_state(
1248            None,
1249            None,
1250            Some(vec![signer]),
1251            Some(vec![network]),
1252            None,
1253            None,
1254        )
1255        .await;
1256
1257        let mut request = create_test_relayer_create_request(
1258            Some("test-stellar-relayer".to_string()),
1259            "Test Stellar Relayer",
1260            "test",
1261            "test",
1262            None,
1263        );
1264
1265        // Change network type to Stellar and add Stellar policies
1266        request.network_type = RelayerNetworkType::Stellar;
1267        request.policies = Some(CreateRelayerPolicyRequest::Stellar(RelayerStellarPolicy {
1268            min_balance: Some(10000000),
1269            max_fee: Some(100),
1270            timeout_seconds: Some(30),
1271            concurrent_transactions: None,
1272            allowed_tokens: None,
1273            fee_payment_strategy: Some(StellarFeePaymentStrategy::Relayer),
1274            slippage_percentage: None,
1275            fee_margin_percentage: None,
1276            swap_config: None,
1277        }));
1278
1279        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1280
1281        assert!(result.is_ok());
1282        let response = result.unwrap();
1283        assert_eq!(response.status(), 201);
1284
1285        let body = to_bytes(response.into_body()).await.unwrap();
1286        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1287
1288        assert!(api_response.success);
1289        let data = api_response.data.unwrap();
1290        assert_eq!(data.id, "test-stellar-relayer");
1291        assert_eq!(data.network_type, RelayerNetworkType::Stellar);
1292
1293        // Verify Stellar policies are present in response
1294        assert!(data.policies.is_some());
1295        cleanup_test_env();
1296    }
1297
1298    #[actix_web::test]
1299    async fn test_create_relayer_with_policy_type_mismatch() {
1300        let _lock = ENV_MUTEX.lock().await;
1301        setup_test_env();
1302        let network = create_mock_network();
1303        let signer = create_mock_signer();
1304        let app_state = create_mock_app_state(
1305            None,
1306            None,
1307            Some(vec![signer]),
1308            Some(vec![network]),
1309            None,
1310            None,
1311        )
1312        .await;
1313
1314        let mut request = create_test_relayer_create_request(
1315            Some("test-mismatch-relayer".to_string()),
1316            "Test Mismatch Relayer",
1317            "test",
1318            "test",
1319            None,
1320        );
1321
1322        // Set network type to EVM but provide Solana policies (should fail)
1323        request.network_type = RelayerNetworkType::Evm;
1324        request.policies = Some(CreateRelayerPolicyRequest::Solana(
1325            RelayerSolanaPolicy::default(),
1326        ));
1327
1328        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1329
1330        assert!(result.is_err());
1331        if let Err(ApiError::BadRequest(msg)) = result {
1332            assert!(msg.contains("Policy type does not match relayer network type"));
1333        } else {
1334            panic!("Expected BadRequest error for policy type mismatch");
1335        }
1336        cleanup_test_env();
1337    }
1338
1339    #[actix_web::test]
1340    async fn test_create_relayer_with_notification() {
1341        let _lock = ENV_MUTEX.lock().await;
1342        setup_test_env();
1343        let network = create_mock_network();
1344        let signer = create_mock_signer();
1345        let notification = create_mock_notification("test-notification".to_string());
1346        let app_state = create_mock_app_state(
1347            None,
1348            None,
1349            Some(vec![signer]),
1350            Some(vec![network]),
1351            None,
1352            None,
1353        )
1354        .await;
1355
1356        // Add notification manually since create_mock_app_state doesn't handle notifications
1357        app_state
1358            .notification_repository
1359            .create(notification)
1360            .await
1361            .unwrap();
1362
1363        let request = create_test_relayer_create_request(
1364            Some("test-relayer".to_string()),
1365            "Test Relayer",
1366            "test", // Using "test" to match the mock network name
1367            "test", // Using "test" to match the mock signer id
1368            Some("test-notification".to_string()),
1369        );
1370
1371        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1372
1373        assert!(result.is_ok());
1374        let response = result.unwrap();
1375        assert_eq!(response.status(), 201);
1376        let body = to_bytes(response.into_body()).await.unwrap();
1377        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1378
1379        assert!(api_response.success);
1380        let data = api_response.data.unwrap();
1381        assert_eq!(data.notification_id, Some("test-notification".to_string()));
1382        cleanup_test_env();
1383    }
1384
1385    #[actix_web::test]
1386    async fn test_create_relayer_nonexistent_signer() {
1387        let network = create_mock_network();
1388        let app_state =
1389            create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
1390
1391        let request = create_test_relayer_create_request(
1392            Some("test-relayer".to_string()),
1393            "Test Relayer",
1394            "test", // Using "test" to match the mock network name
1395            "nonexistent-signer",
1396            None,
1397        );
1398
1399        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1400
1401        assert!(result.is_err());
1402        if let Err(ApiError::NotFound(msg)) = result {
1403            assert!(msg.contains("Signer with ID nonexistent-signer not found"));
1404        } else {
1405            panic!("Expected NotFound error for nonexistent signer");
1406        }
1407    }
1408
1409    #[actix_web::test]
1410    async fn test_create_relayer_nonexistent_network() {
1411        let signer = create_mock_signer();
1412        let app_state =
1413            create_mock_app_state(None, None, Some(vec![signer]), None, None, None).await;
1414
1415        let request = create_test_relayer_create_request(
1416            Some("test-relayer".to_string()),
1417            "Test Relayer",
1418            "nonexistent-network",
1419            "test", // Using "test" to match the mock signer id
1420            None,
1421        );
1422
1423        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1424
1425        assert!(result.is_err());
1426        if let Err(ApiError::BadRequest(msg)) = result {
1427            assert!(msg.contains("Network 'nonexistent-network' not found"));
1428            assert!(msg.contains("network configuration exists"));
1429        } else {
1430            panic!("Expected BadRequest error for nonexistent network");
1431        }
1432    }
1433
1434    #[actix_web::test]
1435    async fn test_create_relayer_signer_already_in_use() {
1436        let network = create_mock_network();
1437        let signer = create_mock_signer();
1438        let mut existing_relayer = create_mock_relayer("existing-relayer".to_string(), false);
1439        existing_relayer.signer_id = "test".to_string(); // Match the mock signer id
1440        existing_relayer.network = "test".to_string(); // Match the mock network name
1441        let app_state = create_mock_app_state(
1442            None,
1443            Some(vec![existing_relayer]),
1444            Some(vec![signer]),
1445            Some(vec![network]),
1446            None,
1447            None,
1448        )
1449        .await;
1450
1451        let request = create_test_relayer_create_request(
1452            Some("test-relayer".to_string()),
1453            "Test Relayer",
1454            "test", // Using "test" to match the mock network name
1455            "test", // Using "test" to match the mock signer id
1456            None,
1457        );
1458
1459        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1460
1461        assert!(result.is_err());
1462        if let Err(ApiError::BadRequest(msg)) = result {
1463            assert!(msg.contains("signer 'test' is already in use"));
1464            assert!(msg.contains("relayer 'existing-relayer'"));
1465            assert!(msg.contains("network 'test'"));
1466            assert!(msg.contains("security reasons"));
1467        } else {
1468            panic!("Expected BadRequest error for signer already in use");
1469        }
1470    }
1471
1472    #[actix_web::test]
1473    async fn test_create_relayer_nonexistent_notification() {
1474        let network = create_mock_network();
1475        let signer = create_mock_signer();
1476        let app_state = create_mock_app_state(
1477            None,
1478            None,
1479            Some(vec![signer]),
1480            Some(vec![network]),
1481            None,
1482            None,
1483        )
1484        .await;
1485
1486        let request = create_test_relayer_create_request(
1487            Some("test-relayer".to_string()),
1488            "Test Relayer",
1489            "test", // Using "test" to match the mock network name
1490            "test", // Using "test" to match the mock signer id
1491            Some("nonexistent-notification".to_string()),
1492        );
1493
1494        let result = create_relayer(request, actix_web::web::ThinData(app_state)).await;
1495
1496        assert!(result.is_err());
1497        if let Err(ApiError::NotFound(msg)) = result {
1498            assert!(msg.contains("Notification with ID 'nonexistent-notification' not found"));
1499        } else {
1500            panic!("Expected NotFound error for nonexistent notification");
1501        }
1502    }
1503
1504    // LIST RELAYERS TESTS
1505
1506    #[actix_web::test]
1507    async fn test_list_relayers_success() {
1508        let relayer1 = create_mock_relayer("relayer-1".to_string(), false);
1509        let relayer2 = create_mock_relayer("relayer-2".to_string(), false);
1510        let app_state =
1511            create_mock_app_state(None, Some(vec![relayer1, relayer2]), None, None, None, None)
1512                .await;
1513
1514        let query = PaginationQuery {
1515            page: 1,
1516            per_page: 10,
1517        };
1518
1519        let result = list_relayers(query, actix_web::web::ThinData(app_state)).await;
1520
1521        assert!(result.is_ok());
1522        let response = result.unwrap();
1523        assert_eq!(response.status(), 200);
1524
1525        let body = to_bytes(response.into_body()).await.unwrap();
1526        let api_response: ApiResponse<Vec<RelayerResponse>> =
1527            serde_json::from_slice(&body).unwrap();
1528
1529        assert!(api_response.success);
1530        let data = api_response.data.unwrap();
1531        assert_eq!(data.len(), 2);
1532    }
1533
1534    #[actix_web::test]
1535    async fn test_list_relayers_empty() {
1536        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
1537
1538        let query = PaginationQuery {
1539            page: 1,
1540            per_page: 10,
1541        };
1542
1543        let result = list_relayers(query, actix_web::web::ThinData(app_state)).await;
1544
1545        assert!(result.is_ok());
1546        let response = result.unwrap();
1547        assert_eq!(response.status(), 200);
1548
1549        let body = to_bytes(response.into_body()).await.unwrap();
1550        let api_response: ApiResponse<Vec<RelayerResponse>> =
1551            serde_json::from_slice(&body).unwrap();
1552
1553        assert!(api_response.success);
1554        let data = api_response.data.unwrap();
1555        assert_eq!(data.len(), 0);
1556    }
1557
1558    // GET RELAYER TESTS
1559
1560    #[actix_web::test]
1561    async fn test_get_relayer_success() {
1562        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1563        let app_state =
1564            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1565
1566        let result = get_relayer(
1567            "test-relayer".to_string(),
1568            actix_web::web::ThinData(app_state),
1569        )
1570        .await;
1571
1572        assert!(result.is_ok());
1573        let response = result.unwrap();
1574        assert_eq!(response.status(), 200);
1575
1576        let body = to_bytes(response.into_body()).await.unwrap();
1577        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1578
1579        assert!(api_response.success);
1580        let data = api_response.data.unwrap();
1581        assert_eq!(data.id, "test-relayer");
1582        assert_eq!(data.name, "Relayer test-relayer"); // Mock utility creates name as "Relayer {id}"
1583    }
1584
1585    #[actix_web::test]
1586    async fn test_get_relayer_not_found() {
1587        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
1588
1589        let result = get_relayer(
1590            "nonexistent".to_string(),
1591            actix_web::web::ThinData(app_state),
1592        )
1593        .await;
1594
1595        assert!(result.is_err());
1596        if let Err(ApiError::NotFound(msg)) = result {
1597            assert!(msg.contains("Relayer with ID nonexistent not found"));
1598        } else {
1599            panic!("Expected NotFound error");
1600        }
1601    }
1602
1603    // UPDATE RELAYER TESTS
1604
1605    #[actix_web::test]
1606    async fn test_update_relayer_success() {
1607        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1608        let app_state =
1609            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1610
1611        let patch = serde_json::json!({
1612            "name": "Updated Relayer Name",
1613            "paused": true
1614        });
1615
1616        let result = update_relayer(
1617            "test-relayer".to_string(),
1618            patch,
1619            actix_web::web::ThinData(app_state),
1620        )
1621        .await;
1622
1623        assert!(result.is_ok());
1624        let response = result.unwrap();
1625        assert_eq!(response.status(), 200);
1626
1627        let body = to_bytes(response.into_body()).await.unwrap();
1628        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1629
1630        assert!(api_response.success);
1631        let data = api_response.data.unwrap();
1632        assert_eq!(data.name, "Updated Relayer Name");
1633        assert!(data.paused);
1634    }
1635
1636    #[actix_web::test]
1637    async fn test_update_relayer_system_disabled() {
1638        let mut relayer = create_mock_relayer("disabled-relayer".to_string(), false);
1639        relayer.system_disabled = true;
1640        let app_state =
1641            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1642
1643        let patch = serde_json::json!({
1644            "name": "Updated Name"
1645        });
1646
1647        let result = update_relayer(
1648            "disabled-relayer".to_string(),
1649            patch,
1650            actix_web::web::ThinData(app_state),
1651        )
1652        .await;
1653
1654        assert!(result.is_err());
1655        if let Err(ApiError::BadRequest(msg)) = result {
1656            assert!(msg.contains("Relayer is disabled"));
1657        } else {
1658            panic!("Expected BadRequest error for disabled relayer");
1659        }
1660    }
1661
1662    #[actix_web::test]
1663    async fn test_update_relayer_invalid_patch() {
1664        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1665        let app_state =
1666            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1667
1668        let patch = serde_json::json!({
1669            "invalid_field": "value"
1670        });
1671
1672        let result = update_relayer(
1673            "test-relayer".to_string(),
1674            patch,
1675            actix_web::web::ThinData(app_state),
1676        )
1677        .await;
1678
1679        assert!(result.is_err());
1680        if let Err(ApiError::BadRequest(msg)) = result {
1681            assert!(msg.contains("Invalid update request"));
1682        } else {
1683            panic!("Expected BadRequest error for invalid patch");
1684        }
1685    }
1686
1687    #[actix_web::test]
1688    async fn test_update_relayer_nonexistent() {
1689        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
1690
1691        let patch = serde_json::json!({
1692            "name": "Updated Name"
1693        });
1694
1695        let result = update_relayer(
1696            "nonexistent-relayer".to_string(),
1697            patch,
1698            actix_web::web::ThinData(app_state),
1699        )
1700        .await;
1701
1702        assert!(result.is_err());
1703        if let Err(ApiError::NotFound(msg)) = result {
1704            assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
1705        } else {
1706            panic!("Expected NotFound error for nonexistent relayer");
1707        }
1708    }
1709
1710    #[actix_web::test]
1711    async fn test_update_relayer_set_evm_policies() {
1712        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1713        let app_state =
1714            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1715
1716        let patch = serde_json::json!({
1717            "policies": {
1718                "gas_price_cap": 50000000000u64,
1719                "min_balance": 1000000000000000000u64,
1720                "eip1559_pricing": true,
1721                "private_transactions": false,
1722                "gas_limit_estimation": true,
1723                "whitelist_receivers": ["0x1234567890123456789012345678901234567890"]
1724            }
1725        });
1726
1727        let result = update_relayer(
1728            "test-relayer".to_string(),
1729            patch,
1730            actix_web::web::ThinData(app_state),
1731        )
1732        .await;
1733
1734        assert!(result.is_ok());
1735        let response = result.unwrap();
1736        assert_eq!(response.status(), 200);
1737
1738        let body = to_bytes(response.into_body()).await.unwrap();
1739        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1740
1741        assert!(api_response.success);
1742        let data = api_response.data.unwrap();
1743
1744        // For now, just verify that the policies field exists
1745        // The policy validation can be added once we understand the correct structure
1746        assert!(data.policies.is_some());
1747    }
1748
1749    #[actix_web::test]
1750    async fn test_update_relayer_partial_policy_update() {
1751        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1752        let app_state =
1753            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1754
1755        // First update with some policies
1756        let patch1 = serde_json::json!({
1757            "policies": {
1758                "gas_price_cap": 30000000000u64,
1759                "min_balance": 500000000000000000u64,
1760                "eip1559_pricing": false
1761            }
1762        });
1763
1764        let result1 = update_relayer(
1765            "test-relayer".to_string(),
1766            patch1,
1767            actix_web::web::ThinData(app_state),
1768        )
1769        .await;
1770
1771        assert!(result1.is_ok());
1772
1773        // Create fresh app state for second update test
1774        let relayer2 = create_mock_relayer("test-relayer".to_string(), false);
1775        let app_state2 =
1776            create_mock_app_state(None, Some(vec![relayer2]), None, None, None, None).await;
1777
1778        // Second update with only gas_price_cap change
1779        let patch2 = serde_json::json!({
1780            "policies": {
1781                "gas_price_cap": 60000000000u64
1782            }
1783        });
1784
1785        let result2 = update_relayer(
1786            "test-relayer".to_string(),
1787            patch2,
1788            actix_web::web::ThinData(app_state2),
1789        )
1790        .await;
1791
1792        assert!(result2.is_ok());
1793        let response = result2.unwrap();
1794        assert_eq!(response.status(), 200);
1795
1796        let body = to_bytes(response.into_body()).await.unwrap();
1797        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1798
1799        assert!(api_response.success);
1800        let data = api_response.data.unwrap();
1801
1802        // Just verify policies exist for now
1803        assert!(data.policies.is_some());
1804    }
1805
1806    #[actix_web::test]
1807    async fn test_update_relayer_unset_notification() {
1808        let mut relayer = create_mock_relayer("test-relayer".to_string(), false);
1809        relayer.notification_id = Some("test-notification".to_string());
1810        let app_state =
1811            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1812
1813        let patch = serde_json::json!({
1814            "notification_id": null
1815        });
1816
1817        let result = update_relayer(
1818            "test-relayer".to_string(),
1819            patch,
1820            actix_web::web::ThinData(app_state),
1821        )
1822        .await;
1823
1824        assert!(result.is_ok());
1825        let response = result.unwrap();
1826        assert_eq!(response.status(), 200);
1827
1828        let body = to_bytes(response.into_body()).await.unwrap();
1829        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1830
1831        assert!(api_response.success);
1832        let data = api_response.data.unwrap();
1833        assert_eq!(data.notification_id, None);
1834    }
1835
1836    #[actix_web::test]
1837    async fn test_update_relayer_unset_custom_rpc_urls() {
1838        let mut relayer = create_mock_relayer("test-relayer".to_string(), false);
1839        relayer.custom_rpc_urls = Some(vec![crate::models::RpcConfig {
1840            url: "https://custom-rpc.example.com".to_string(),
1841            weight: 50,
1842            ..Default::default()
1843        }]);
1844        let app_state =
1845            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1846
1847        let patch = serde_json::json!({
1848            "custom_rpc_urls": null
1849        });
1850
1851        let result = update_relayer(
1852            "test-relayer".to_string(),
1853            patch,
1854            actix_web::web::ThinData(app_state),
1855        )
1856        .await;
1857
1858        assert!(result.is_ok());
1859        let response = result.unwrap();
1860        assert_eq!(response.status(), 200);
1861
1862        let body = to_bytes(response.into_body()).await.unwrap();
1863        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1864
1865        assert!(api_response.success);
1866        let data = api_response.data.unwrap();
1867        assert_eq!(data.custom_rpc_urls, None);
1868    }
1869
1870    #[actix_web::test]
1871    async fn test_update_relayer_set_custom_rpc_urls() {
1872        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1873        let app_state =
1874            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1875
1876        let patch = serde_json::json!({
1877            "custom_rpc_urls": [
1878                {
1879                    "url": "https://rpc1.example.com",
1880                    "weight": 80
1881                },
1882                {
1883                    "url": "https://rpc2.example.com",
1884                    "weight": 60
1885                }
1886            ]
1887        });
1888
1889        let result = update_relayer(
1890            "test-relayer".to_string(),
1891            patch,
1892            actix_web::web::ThinData(app_state),
1893        )
1894        .await;
1895
1896        assert!(result.is_ok());
1897        let response = result.unwrap();
1898        assert_eq!(response.status(), 200);
1899
1900        let body = to_bytes(response.into_body()).await.unwrap();
1901        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1902
1903        assert!(api_response.success);
1904        let data = api_response.data.unwrap();
1905
1906        assert!(data.custom_rpc_urls.is_some());
1907        let rpc_urls = data.custom_rpc_urls.unwrap();
1908        assert_eq!(rpc_urls.len(), 2);
1909        assert_eq!(rpc_urls[0].url, "https://rpc1.example.com");
1910        assert_eq!(rpc_urls[0].weight, 80);
1911        assert_eq!(rpc_urls[1].url, "https://rpc2.example.com");
1912        assert_eq!(rpc_urls[1].weight, 60);
1913    }
1914
1915    #[actix_web::test]
1916    async fn test_update_relayer_clear_policies() {
1917        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1918        let app_state =
1919            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1920
1921        let patch = serde_json::json!({
1922            "policies": null
1923        });
1924
1925        let result = update_relayer(
1926            "test-relayer".to_string(),
1927            patch,
1928            actix_web::web::ThinData(app_state),
1929        )
1930        .await;
1931
1932        assert!(result.is_ok());
1933        let response = result.unwrap();
1934        assert_eq!(response.status(), 200);
1935
1936        let body = to_bytes(response.into_body()).await.unwrap();
1937        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
1938
1939        assert!(api_response.success);
1940        let data = api_response.data.unwrap();
1941        assert_eq!(data.policies, None);
1942    }
1943
1944    #[actix_web::test]
1945    async fn test_update_relayer_invalid_policy_structure() {
1946        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1947        let app_state =
1948            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1949
1950        let patch = serde_json::json!({
1951            "policies": {
1952                "invalid_field_name": "some_value"
1953            }
1954        });
1955
1956        let result = update_relayer(
1957            "test-relayer".to_string(),
1958            patch,
1959            actix_web::web::ThinData(app_state),
1960        )
1961        .await;
1962
1963        assert!(result.is_err());
1964        if let Err(ApiError::BadRequest(msg)) = result {
1965            assert!(msg.contains("Invalid policy"));
1966        } else {
1967            panic!("Expected BadRequest error for invalid policy structure");
1968        }
1969    }
1970
1971    #[actix_web::test]
1972    async fn test_update_relayer_invalid_evm_policy_values() {
1973        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1974        let app_state =
1975            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
1976
1977        let patch = serde_json::json!({
1978            "policies": {
1979                "gas_price_cap": "invalid_number",
1980                "min_balance": -1
1981            }
1982        });
1983
1984        let result = update_relayer(
1985            "test-relayer".to_string(),
1986            patch,
1987            actix_web::web::ThinData(app_state),
1988        )
1989        .await;
1990
1991        assert!(result.is_err());
1992        if let Err(ApiError::BadRequest(msg)) = result {
1993            assert!(msg.contains("Invalid policy") || msg.contains("Invalid update request"));
1994        } else {
1995            panic!("Expected BadRequest error for invalid policy values");
1996        }
1997    }
1998
1999    #[actix_web::test]
2000    async fn test_update_relayer_multiple_fields_at_once() {
2001        let mut relayer = create_mock_relayer("test-relayer".to_string(), false);
2002        relayer.notification_id = Some("old-notification".to_string());
2003        let app_state =
2004            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2005
2006        let patch = serde_json::json!({
2007            "name": "Multi-Update Relayer",
2008            "paused": true,
2009            "notification_id": null,
2010            "policies": {
2011                "gas_price_cap": 40000000000u64,
2012                "eip1559_pricing": true
2013            },
2014            "custom_rpc_urls": [
2015                {
2016                    "url": "https://new-rpc.example.com",
2017                    "weight": 90
2018                }
2019            ]
2020        });
2021
2022        let result = update_relayer(
2023            "test-relayer".to_string(),
2024            patch,
2025            actix_web::web::ThinData(app_state),
2026        )
2027        .await;
2028
2029        assert!(result.is_ok());
2030        let response = result.unwrap();
2031        assert_eq!(response.status(), 200);
2032
2033        let body = to_bytes(response.into_body()).await.unwrap();
2034        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
2035
2036        assert!(api_response.success);
2037        let data = api_response.data.unwrap();
2038
2039        // Verify all fields were updated correctly
2040        assert_eq!(data.name, "Multi-Update Relayer");
2041        assert!(data.paused);
2042        assert_eq!(data.notification_id, None);
2043
2044        // Verify policies and RPC URLs were set
2045        assert!(data.policies.is_some());
2046        assert!(data.custom_rpc_urls.is_some());
2047        let rpc_urls = data.custom_rpc_urls.unwrap();
2048        assert_eq!(rpc_urls.len(), 1);
2049        assert_eq!(rpc_urls[0].url, "https://new-rpc.example.com");
2050        assert_eq!(rpc_urls[0].weight, 90);
2051    }
2052
2053    #[actix_web::test]
2054    async fn test_update_relayer_solana_policies() {
2055        use crate::models::{
2056            NetworkType, RelayerNetworkPolicy, RelayerSolanaPolicy, SolanaFeePaymentStrategy,
2057        };
2058
2059        // Create a Solana relayer (not the default EVM one)
2060        let mut solana_relayer = create_mock_relayer("test-solana-relayer".to_string(), false);
2061        solana_relayer.network_type = NetworkType::Solana;
2062        solana_relayer.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default());
2063
2064        let app_state =
2065            create_mock_app_state(None, Some(vec![solana_relayer]), None, None, None, None).await;
2066
2067        let patch = serde_json::json!({
2068            "policies": {
2069                "fee_payment_strategy": "user",
2070                "min_balance": 2000000,
2071                "max_signatures": 5,
2072                "max_tx_data_size": 800,
2073                "max_allowed_fee_lamports": 25000,
2074                "fee_margin_percentage": 15.0
2075            }
2076        });
2077
2078        let result = update_relayer(
2079            "test-solana-relayer".to_string(),
2080            patch,
2081            actix_web::web::ThinData(app_state),
2082        )
2083        .await;
2084
2085        assert!(result.is_ok());
2086        let response = result.unwrap();
2087        assert_eq!(response.status(), 200);
2088
2089        let body = to_bytes(response.into_body()).await.unwrap();
2090        let api_response: ApiResponse<RelayerResponse> = serde_json::from_slice(&body).unwrap();
2091
2092        assert!(api_response.success);
2093        let data = api_response.data.unwrap();
2094
2095        // Verify Solana policies are present and correctly updated
2096        assert!(data.policies.is_some());
2097        let policies = data.policies.unwrap();
2098        if let RelayerNetworkPolicyResponse::Solana(solana_policy) = policies {
2099            assert_eq!(
2100                solana_policy.fee_payment_strategy,
2101                Some(SolanaFeePaymentStrategy::User)
2102            );
2103            assert_eq!(solana_policy.min_balance, 2000000);
2104            assert_eq!(solana_policy.max_signatures, Some(5));
2105            assert_eq!(solana_policy.max_tx_data_size, 800);
2106            assert_eq!(solana_policy.max_allowed_fee_lamports, Some(25000));
2107            assert_eq!(solana_policy.fee_margin_percentage, Some(15.0));
2108        } else {
2109            panic!("Expected Solana policies in response");
2110        }
2111    }
2112
2113    // DELETE RELAYER TESTS
2114
2115    #[actix_web::test]
2116    async fn test_delete_relayer_success() {
2117        let relayer = create_mock_relayer("test-relayer".to_string(), false);
2118        let app_state =
2119            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2120
2121        let result = delete_relayer(
2122            "test-relayer".to_string(),
2123            actix_web::web::ThinData(app_state),
2124        )
2125        .await;
2126
2127        assert!(result.is_ok());
2128        let response = result.unwrap();
2129        assert_eq!(response.status(), 200);
2130
2131        let body = to_bytes(response.into_body()).await.unwrap();
2132        let api_response: ApiResponse<String> = serde_json::from_slice(&body).unwrap();
2133
2134        assert!(api_response.success);
2135        let data = api_response.data.unwrap();
2136        assert!(data.contains("Relayer deleted successfully"));
2137    }
2138
2139    #[actix_web::test]
2140    async fn test_delete_relayer_with_transactions() {
2141        let relayer = create_mock_relayer("relayer-with-tx".to_string(), false);
2142        let mut transaction = create_mock_transaction();
2143        transaction.id = "test-tx".to_string();
2144        transaction.relayer_id = "relayer-with-tx".to_string();
2145        let app_state = create_mock_app_state(
2146            None,
2147            Some(vec![relayer]),
2148            None,
2149            None,
2150            None,
2151            Some(vec![transaction]),
2152        )
2153        .await;
2154
2155        let result = delete_relayer(
2156            "relayer-with-tx".to_string(),
2157            actix_web::web::ThinData(app_state),
2158        )
2159        .await;
2160
2161        assert!(result.is_err());
2162        if let Err(ApiError::BadRequest(msg)) = result {
2163            assert!(msg.contains("Cannot delete relayer 'relayer-with-tx'"));
2164            assert!(msg.contains("has 1 transaction(s)"));
2165            assert!(msg.contains("wait for all transactions to complete"));
2166        } else {
2167            panic!("Expected BadRequest error for relayer with transactions");
2168        }
2169    }
2170
2171    #[actix_web::test]
2172    async fn test_delete_relayer_nonexistent() {
2173        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
2174
2175        let result = delete_relayer(
2176            "nonexistent-relayer".to_string(),
2177            actix_web::web::ThinData(app_state),
2178        )
2179        .await;
2180
2181        assert!(result.is_err());
2182        if let Err(ApiError::NotFound(msg)) = result {
2183            assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
2184        } else {
2185            panic!("Expected NotFound error for nonexistent relayer");
2186        }
2187    }
2188
2189    #[actix_web::test]
2190    async fn test_sign_transaction_success() {
2191        let _lock = ENV_MUTEX.lock().await;
2192        setup_test_env();
2193        let network = create_mock_stellar_network();
2194        let signer = create_mock_signer();
2195        let mut relayer = create_mock_relayer("test-relayer".to_string(), false);
2196        relayer.network_type = NetworkType::Stellar;
2197        let app_state = create_mock_app_state(
2198            None,
2199            Some(vec![relayer]),
2200            Some(vec![signer]),
2201            Some(vec![network]),
2202            None,
2203            None,
2204        )
2205        .await;
2206
2207        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
2208            unsigned_xdr: "test-unsigned-xdr".to_string(),
2209        });
2210
2211        let result = sign_transaction(
2212            "test-relayer".to_string(),
2213            request,
2214            actix_web::web::ThinData(app_state),
2215        )
2216        .await;
2217
2218        // The actual signing will fail in the mock environment, but we can test that
2219        // the function is callable with generics and processes the request
2220        assert!(result.is_err());
2221        cleanup_test_env();
2222    }
2223
2224    #[actix_web::test]
2225    async fn test_sign_transaction_relayer_not_found() {
2226        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
2227
2228        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
2229            unsigned_xdr: "test-unsigned-xdr".to_string(),
2230        });
2231
2232        let result = sign_transaction(
2233            "nonexistent-relayer".to_string(),
2234            request,
2235            actix_web::web::ThinData(app_state),
2236        )
2237        .await;
2238
2239        assert!(result.is_err());
2240        if let Err(ApiError::NotFound(msg)) = result {
2241            assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
2242        } else {
2243            panic!("Expected NotFound error for nonexistent relayer");
2244        }
2245    }
2246
2247    #[actix_web::test]
2248    async fn test_sign_transaction_relayer_disabled() {
2249        let mut relayer = create_mock_relayer("disabled-relayer".to_string(), false);
2250        relayer.paused = true;
2251        let app_state =
2252            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2253
2254        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
2255            unsigned_xdr: "test-unsigned-xdr".to_string(),
2256        });
2257
2258        let result = sign_transaction(
2259            "disabled-relayer".to_string(),
2260            request,
2261            actix_web::web::ThinData(app_state),
2262        )
2263        .await;
2264
2265        assert!(result.is_err());
2266        if let Err(ApiError::ForbiddenError(msg)) = result {
2267            assert!(msg.contains("Relayer paused"));
2268        } else {
2269            panic!("Expected ForbiddenError for paused relayer");
2270        }
2271    }
2272
2273    #[actix_web::test]
2274    async fn test_sign_transaction_system_disabled() {
2275        let mut relayer = create_mock_relayer("system-disabled-relayer".to_string(), false);
2276        relayer.system_disabled = true;
2277        let app_state =
2278            create_mock_app_state(None, Some(vec![relayer]), None, None, None, None).await;
2279
2280        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
2281            unsigned_xdr: "test-unsigned-xdr".to_string(),
2282        });
2283
2284        let result = sign_transaction(
2285            "system-disabled-relayer".to_string(),
2286            request,
2287            actix_web::web::ThinData(app_state),
2288        )
2289        .await;
2290
2291        assert!(result.is_err());
2292        if let Err(ApiError::ForbiddenError(msg)) = result {
2293            assert!(msg.contains("Relayer disabled"));
2294        } else {
2295            panic!("Expected ForbiddenError for system disabled relayer");
2296        }
2297    }
2298
2299    #[actix_web::test]
2300    async fn test_quote_sponsored_transaction_relayer_not_found() {
2301        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
2302
2303        let request = SponsoredTransactionQuoteRequest::Stellar(
2304            crate::models::StellarFeeEstimateRequestParams {
2305                transaction_xdr: Some("test-xdr".to_string()),
2306                operations: None,
2307                source_account: None,
2308                fee_token: "native".to_string(),
2309            },
2310        );
2311
2312        let result = quote_sponsored_transaction(
2313            "nonexistent-relayer".to_string(),
2314            request,
2315            actix_web::web::ThinData(app_state),
2316        )
2317        .await;
2318
2319        assert!(result.is_err());
2320        if let Err(ApiError::NotFound(msg)) = result {
2321            assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
2322        } else {
2323            panic!("Expected NotFound error for nonexistent relayer");
2324        }
2325    }
2326
2327    #[actix_web::test]
2328    async fn test_quote_sponsored_transaction_relayer_disabled() {
2329        let mut relayer = create_mock_relayer("disabled-relayer".to_string(), false);
2330        relayer.paused = true;
2331        relayer.network_type = NetworkType::Stellar;
2332        relayer.policies = crate::models::RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
2333            fee_payment_strategy: Some(StellarFeePaymentStrategy::User),
2334            ..Default::default()
2335        });
2336        let network = create_mock_stellar_network();
2337        let signer = create_mock_signer();
2338        let app_state = create_mock_app_state(
2339            None,
2340            Some(vec![relayer]),
2341            Some(vec![signer]),
2342            Some(vec![network]),
2343            None,
2344            None,
2345        )
2346        .await;
2347
2348        let request = SponsoredTransactionQuoteRequest::Stellar(
2349            crate::models::StellarFeeEstimateRequestParams {
2350                transaction_xdr: Some("test-xdr".to_string()),
2351                operations: None,
2352                source_account: None,
2353                fee_token: "native".to_string(),
2354            },
2355        );
2356
2357        let result = quote_sponsored_transaction(
2358            "disabled-relayer".to_string(),
2359            request,
2360            actix_web::web::ThinData(app_state),
2361        )
2362        .await;
2363
2364        assert!(result.is_err());
2365        if let Err(ApiError::ForbiddenError(msg)) = result {
2366            assert!(msg.contains("Relayer paused"));
2367        } else {
2368            panic!("Expected ForbiddenError for paused relayer");
2369        }
2370    }
2371
2372    #[actix_web::test]
2373    async fn test_build_sponsored_transaction_relayer_not_found() {
2374        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
2375
2376        let request = SponsoredTransactionBuildRequest::Stellar(
2377            crate::models::StellarPrepareTransactionRequestParams {
2378                transaction_xdr: Some("test-xdr".to_string()),
2379                operations: None,
2380                source_account: None,
2381                fee_token: "native".to_string(),
2382            },
2383        );
2384
2385        let result = build_sponsored_transaction(
2386            "nonexistent-relayer".to_string(),
2387            request,
2388            actix_web::web::ThinData(app_state),
2389        )
2390        .await;
2391
2392        assert!(result.is_err());
2393        if let Err(ApiError::NotFound(msg)) = result {
2394            assert!(msg.contains("Relayer with ID nonexistent-relayer not found"));
2395        } else {
2396            panic!("Expected NotFound error for nonexistent relayer");
2397        }
2398    }
2399
2400    #[actix_web::test]
2401    async fn test_build_sponsored_transaction_relayer_disabled() {
2402        let mut relayer = create_mock_relayer("disabled-relayer".to_string(), false);
2403        relayer.paused = true;
2404        relayer.network_type = NetworkType::Stellar;
2405        relayer.policies = crate::models::RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
2406            fee_payment_strategy: Some(StellarFeePaymentStrategy::User),
2407            ..Default::default()
2408        });
2409        let network = create_mock_stellar_network();
2410        let signer = create_mock_signer();
2411        let app_state = create_mock_app_state(
2412            None,
2413            Some(vec![relayer]),
2414            Some(vec![signer]),
2415            Some(vec![network]),
2416            None,
2417            None,
2418        )
2419        .await;
2420
2421        let request = SponsoredTransactionBuildRequest::Stellar(
2422            crate::models::StellarPrepareTransactionRequestParams {
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 = build_sponsored_transaction(
2431            "disabled-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::ForbiddenError(msg)) = result {
2439            assert!(msg.contains("Relayer paused"));
2440        } else {
2441            panic!("Expected ForbiddenError for paused relayer");
2442        }
2443    }
2444}