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