openzeppelin_relayer/api/routes/
relayer.rs

1//! This module defines the HTTP routes for relayer operations.
2//! It includes handlers for listing, retrieving, updating, and managing relayer transactions.
3//! The routes are integrated with the Actix-web framework and interact with the relayer controller.
4use crate::{
5    api::controllers::relayer,
6    domain::{SignDataRequest, SignTransactionRequest, SignTypedDataRequest},
7    models::{
8        transaction::request::{
9            SponsoredTransactionBuildRequest, SponsoredTransactionQuoteRequest,
10        },
11        CreateRelayerRequest, DefaultAppState, PaginationQuery,
12    },
13};
14use actix_web::{delete, get, patch, post, put, web, Responder};
15use serde::Deserialize;
16use utoipa::ToSchema;
17
18/// Lists all relayers with pagination support.
19#[get("/relayers")]
20async fn list_relayers(
21    query: web::Query<PaginationQuery>,
22    data: web::ThinData<DefaultAppState>,
23) -> impl Responder {
24    relayer::list_relayers(query.into_inner(), data).await
25}
26
27/// Retrieves details of a specific relayer by ID.
28#[get("/relayers/{relayer_id}")]
29async fn get_relayer(
30    relayer_id: web::Path<String>,
31    data: web::ThinData<DefaultAppState>,
32) -> impl Responder {
33    relayer::get_relayer(relayer_id.into_inner(), data).await
34}
35
36/// Creates a new relayer.
37#[post("/relayers")]
38async fn create_relayer(
39    request: web::Json<CreateRelayerRequest>,
40    data: web::ThinData<DefaultAppState>,
41) -> impl Responder {
42    relayer::create_relayer(request.into_inner(), data).await
43}
44
45/// Updates a relayer's information using JSON Merge Patch (RFC 7396).
46#[patch("/relayers/{relayer_id}")]
47async fn update_relayer(
48    relayer_id: web::Path<String>,
49    patch: web::Json<serde_json::Value>,
50    data: web::ThinData<DefaultAppState>,
51) -> impl Responder {
52    relayer::update_relayer(relayer_id.into_inner(), patch.into_inner(), data).await
53}
54
55/// Deletes a relayer by ID.
56#[delete("/relayers/{relayer_id}")]
57async fn delete_relayer(
58    relayer_id: web::Path<String>,
59    data: web::ThinData<DefaultAppState>,
60) -> impl Responder {
61    relayer::delete_relayer(relayer_id.into_inner(), data).await
62}
63
64/// Fetches the current status of a specific relayer.
65#[get("/relayers/{relayer_id}/status")]
66async fn get_relayer_status(
67    relayer_id: web::Path<String>,
68    data: web::ThinData<DefaultAppState>,
69) -> impl Responder {
70    relayer::get_relayer_status(relayer_id.into_inner(), data).await
71}
72
73/// Retrieves the balance of a specific relayer.
74#[get("/relayers/{relayer_id}/balance")]
75async fn get_relayer_balance(
76    relayer_id: web::Path<String>,
77    data: web::ThinData<DefaultAppState>,
78) -> impl Responder {
79    relayer::get_relayer_balance(relayer_id.into_inner(), data).await
80}
81
82/// Sends a transaction through the specified relayer.
83#[post("/relayers/{relayer_id}/transactions")]
84async fn send_transaction(
85    relayer_id: web::Path<String>,
86    req: web::Json<serde_json::Value>,
87    data: web::ThinData<DefaultAppState>,
88) -> impl Responder {
89    relayer::send_transaction(relayer_id.into_inner(), req.into_inner(), data).await
90}
91
92#[derive(Deserialize, ToSchema)]
93pub struct TransactionPath {
94    relayer_id: String,
95    transaction_id: String,
96}
97
98/// Retrieves a specific transaction by its ID.
99#[get("/relayers/{relayer_id}/transactions/{transaction_id}")]
100async fn get_transaction_by_id(
101    path: web::Path<TransactionPath>,
102    data: web::ThinData<DefaultAppState>,
103) -> impl Responder {
104    let path = path.into_inner();
105    relayer::get_transaction_by_id(path.relayer_id, path.transaction_id, data).await
106}
107
108/// Retrieves a transaction by its nonce value.
109#[get("/relayers/{relayer_id}/transactions/by-nonce/{nonce}")]
110async fn get_transaction_by_nonce(
111    params: web::Path<(String, u64)>,
112    data: web::ThinData<DefaultAppState>,
113) -> impl Responder {
114    let params = params.into_inner();
115    relayer::get_transaction_by_nonce(params.0, params.1, data).await
116}
117
118/// Lists all transactions for a specific relayer with pagination.
119#[get("/relayers/{relayer_id}/transactions")]
120async fn list_transactions(
121    relayer_id: web::Path<String>,
122    query: web::Query<PaginationQuery>,
123    data: web::ThinData<DefaultAppState>,
124) -> impl Responder {
125    relayer::list_transactions(relayer_id.into_inner(), query.into_inner(), data).await
126}
127
128/// Deletes all pending transactions for a specific relayer.
129#[delete("/relayers/{relayer_id}/transactions/pending")]
130async fn delete_pending_transactions(
131    relayer_id: web::Path<String>,
132    data: web::ThinData<DefaultAppState>,
133) -> impl Responder {
134    relayer::delete_pending_transactions(relayer_id.into_inner(), data).await
135}
136
137/// Cancels a specific transaction by its ID.
138#[delete("/relayers/{relayer_id}/transactions/{transaction_id}")]
139async fn cancel_transaction(
140    path: web::Path<TransactionPath>,
141    data: web::ThinData<DefaultAppState>,
142) -> impl Responder {
143    let path = path.into_inner();
144    relayer::cancel_transaction(path.relayer_id, path.transaction_id, data).await
145}
146
147/// Replaces a specific transaction with a new one.
148#[put("/relayers/{relayer_id}/transactions/{transaction_id}")]
149async fn replace_transaction(
150    path: web::Path<TransactionPath>,
151    req: web::Json<serde_json::Value>,
152    data: web::ThinData<DefaultAppState>,
153) -> impl Responder {
154    let path = path.into_inner();
155    relayer::replace_transaction(path.relayer_id, path.transaction_id, req.into_inner(), data).await
156}
157
158/// Signs data using the specified relayer.
159#[post("/relayers/{relayer_id}/sign")]
160async fn sign(
161    relayer_id: web::Path<String>,
162    req: web::Json<SignDataRequest>,
163    data: web::ThinData<DefaultAppState>,
164) -> impl Responder {
165    relayer::sign_data(relayer_id.into_inner(), req.into_inner(), data).await
166}
167
168/// Signs typed data using the specified relayer.
169#[post("/relayers/{relayer_id}/sign-typed-data")]
170async fn sign_typed_data(
171    relayer_id: web::Path<String>,
172    req: web::Json<SignTypedDataRequest>,
173    data: web::ThinData<DefaultAppState>,
174) -> impl Responder {
175    relayer::sign_typed_data(relayer_id.into_inner(), req.into_inner(), data).await
176}
177
178/// Signs a transaction using the specified relayer (Stellar only).
179#[post("/relayers/{relayer_id}/sign-transaction")]
180async fn sign_transaction(
181    relayer_id: web::Path<String>,
182    req: web::Json<SignTransactionRequest>,
183    data: web::ThinData<DefaultAppState>,
184) -> impl Responder {
185    relayer::sign_transaction(relayer_id.into_inner(), req.into_inner(), data).await
186}
187
188/// Performs a JSON-RPC call using the specified relayer.
189#[post("/relayers/{relayer_id}/rpc")]
190async fn rpc(
191    relayer_id: web::Path<String>,
192    req: web::Json<serde_json::Value>,
193    data: web::ThinData<DefaultAppState>,
194) -> impl Responder {
195    relayer::relayer_rpc(relayer_id.into_inner(), req.into_inner(), data).await
196}
197
198/// Estimates fees for a transaction (gas abstraction endpoint).
199#[post("/relayers/{relayer_id}/transactions/sponsored/quote")]
200async fn quote_sponsored_transaction(
201    relayer_id: web::Path<String>,
202    req: web::Json<SponsoredTransactionQuoteRequest>,
203    data: web::ThinData<DefaultAppState>,
204) -> impl Responder {
205    relayer::quote_sponsored_transaction(relayer_id.into_inner(), req.into_inner(), data).await
206}
207
208/// Prepares a transaction with fee payments (gas abstraction endpoint).
209#[post("/relayers/{relayer_id}/transactions/sponsored/build")]
210async fn build_sponsored_transaction(
211    relayer_id: web::Path<String>,
212    req: web::Json<SponsoredTransactionBuildRequest>,
213    data: web::ThinData<DefaultAppState>,
214) -> impl Responder {
215    relayer::build_sponsored_transaction(relayer_id.into_inner(), req.into_inner(), data).await
216}
217
218/// Initializes the routes for the relayer module.
219pub fn init(cfg: &mut web::ServiceConfig) {
220    // Register routes with literal segments before routes with path parameters
221    cfg.service(delete_pending_transactions); // /relayers/{id}/transactions/pending
222    cfg.service(quote_sponsored_transaction); // /relayers/{id}/transactions/sponsored/quote
223    cfg.service(build_sponsored_transaction); // /relayers/{id}/transactions/sponsored/build
224
225    // Then register other routes
226    cfg.service(cancel_transaction); // /relayers/{id}/transactions/{tx_id}
227    cfg.service(replace_transaction); // /relayers/{id}/transactions/{tx_id}
228    cfg.service(get_transaction_by_id); // /relayers/{id}/transactions/{tx_id}
229    cfg.service(get_transaction_by_nonce); // /relayers/{id}/transactions/by-nonce/{nonce}
230    cfg.service(send_transaction); // /relayers/{id}/transactions
231    cfg.service(list_transactions); // /relayers/{id}/transactions
232    cfg.service(get_relayer_status); // /relayers/{id}/status
233    cfg.service(get_relayer_balance); // /relayers/{id}/balance
234    cfg.service(sign); // /relayers/{id}/sign
235    cfg.service(sign_typed_data); // /relayers/{id}/sign-typed-data
236    cfg.service(sign_transaction); // /relayers/{id}/sign-transaction
237    cfg.service(rpc); // /relayers/{id}/rpc
238    cfg.service(get_relayer); // /relayers/{id}
239    cfg.service(create_relayer); // /relayers
240    cfg.service(update_relayer); // /relayers/{id}
241    cfg.service(delete_relayer); // /relayers/{id}
242    cfg.service(list_relayers); // /relayers
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::{
249        config::{EvmNetworkConfig, NetworkConfigCommon},
250        jobs::MockJobProducerTrait,
251        models::{
252            ApiKeyRepoModel, AppState, EvmTransactionData, LocalSignerConfigStorage,
253            NetworkConfigData, NetworkRepoModel, NetworkTransactionData, NetworkType,
254            RelayerEvmPolicy, RelayerNetworkPolicy, RelayerRepoModel, RpcConfig, SecretString,
255            SignerConfigStorage, SignerRepoModel, TransactionRepoModel, TransactionStatus, U256,
256        },
257        repositories::{
258            ApiKeyRepositoryStorage, ApiKeyRepositoryTrait, NetworkRepositoryStorage,
259            NotificationRepositoryStorage, PluginRepositoryStorage, RelayerRepositoryStorage,
260            Repository, SignerRepositoryStorage, TransactionCounterRepositoryStorage,
261            TransactionRepositoryStorage,
262        },
263    };
264    use actix_web::{http::StatusCode, test, App};
265    use std::sync::Arc;
266
267    // Simple mock for AppState
268    async fn get_test_app_state() -> AppState<
269        MockJobProducerTrait,
270        RelayerRepositoryStorage,
271        TransactionRepositoryStorage,
272        NetworkRepositoryStorage,
273        NotificationRepositoryStorage,
274        SignerRepositoryStorage,
275        TransactionCounterRepositoryStorage,
276        PluginRepositoryStorage,
277        ApiKeyRepositoryStorage,
278    > {
279        let relayer_repo = Arc::new(RelayerRepositoryStorage::new_in_memory());
280        let transaction_repo = Arc::new(TransactionRepositoryStorage::new_in_memory());
281        let signer_repo = Arc::new(SignerRepositoryStorage::new_in_memory());
282        let network_repo = Arc::new(NetworkRepositoryStorage::new_in_memory());
283        let api_key_repo = Arc::new(ApiKeyRepositoryStorage::new_in_memory());
284
285        // Create test entities so routes don't return 404
286
287        // Create test network configuration first
288        let test_network = NetworkRepoModel {
289            id: "evm:ethereum".to_string(),
290            name: "ethereum".to_string(),
291            network_type: NetworkType::Evm,
292            config: NetworkConfigData::Evm(EvmNetworkConfig {
293                common: NetworkConfigCommon {
294                    network: "ethereum".to_string(),
295                    from: None,
296                    rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
297                    explorer_urls: None,
298                    average_blocktime_ms: Some(12000),
299                    is_testnet: Some(false),
300                    tags: None,
301                },
302                chain_id: Some(1),
303                required_confirmations: Some(12),
304                features: None,
305                symbol: Some("ETH".to_string()),
306                gas_price_cache: None,
307            }),
308        };
309        network_repo.create(test_network).await.unwrap();
310
311        // Create local signer first
312        let test_signer = SignerRepoModel {
313            id: "test-signer".to_string(),
314            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
315                raw_key: secrets::SecretVec::new(32, |v| v.copy_from_slice(&[0u8; 32])),
316            }),
317        };
318        signer_repo.create(test_signer).await.unwrap();
319
320        // Create test relayer
321        let test_relayer = RelayerRepoModel {
322            id: "test-id".to_string(),
323            name: "Test Relayer".to_string(),
324            network: "ethereum".to_string(),
325            network_type: NetworkType::Evm,
326            signer_id: "test-signer".to_string(),
327            address: "0x1234567890123456789012345678901234567890".to_string(),
328            paused: false,
329            system_disabled: false,
330            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
331            notification_id: None,
332            custom_rpc_urls: None,
333            ..Default::default()
334        };
335        relayer_repo.create(test_relayer).await.unwrap();
336
337        // Create test transaction
338        let test_transaction = TransactionRepoModel {
339            id: "tx-123".to_string(),
340            relayer_id: "test-id".to_string(),
341            status: TransactionStatus::Pending,
342            status_reason: None,
343            created_at: chrono::Utc::now().to_rfc3339(),
344            sent_at: None,
345            confirmed_at: None,
346            valid_until: None,
347            network_data: NetworkTransactionData::Evm(EvmTransactionData {
348                gas_price: Some(20000000000u128),
349                gas_limit: Some(21000u64),
350                nonce: Some(1u64),
351                value: U256::from(0u64),
352                data: Some("0x".to_string()),
353                from: "0x1234567890123456789012345678901234567890".to_string(),
354                to: Some("0x9876543210987654321098765432109876543210".to_string()),
355                chain_id: 1u64,
356                hash: Some("0xabcdef".to_string()),
357                signature: None,
358                speed: None,
359                max_fee_per_gas: None,
360                max_priority_fee_per_gas: None,
361                raw: None,
362            }),
363            priced_at: None,
364            hashes: vec!["0xabcdef".to_string()],
365            network_type: NetworkType::Evm,
366            noop_count: None,
367            is_canceled: Some(false),
368            delete_at: None,
369            metadata: None,
370        };
371        transaction_repo.create(test_transaction).await.unwrap();
372
373        // Create test api key
374        let test_api_key = ApiKeyRepoModel {
375            id: "test-api-key".to_string(),
376            name: "Test API Key".to_string(),
377            value: SecretString::new("test-value"),
378            permissions: vec!["test-permission".to_string()],
379            created_at: chrono::Utc::now().to_rfc3339(),
380            allowed_origins: vec!["*".to_string()],
381        };
382        api_key_repo.create(test_api_key).await.unwrap();
383
384        AppState {
385            relayer_repository: relayer_repo,
386            transaction_repository: transaction_repo,
387            signer_repository: signer_repo,
388            notification_repository: Arc::new(NotificationRepositoryStorage::new_in_memory()),
389            network_repository: network_repo,
390            transaction_counter_store: Arc::new(
391                TransactionCounterRepositoryStorage::new_in_memory(),
392            ),
393            job_producer: Arc::new(MockJobProducerTrait::new()),
394            plugin_repository: Arc::new(PluginRepositoryStorage::new_in_memory()),
395            api_key_repository: api_key_repo,
396        }
397    }
398
399    #[actix_web::test]
400    async fn test_routes_are_registered() -> Result<(), color_eyre::eyre::Error> {
401        // Create a test app with our routes
402        let app = test::init_service(
403            App::new()
404                .app_data(web::Data::new(get_test_app_state().await))
405                .configure(init),
406        )
407        .await;
408
409        // Test that routes are registered by checking they return 500 (not 404)
410
411        // Test GET /relayers
412        let req = test::TestRequest::get().uri("/relayers").to_request();
413        let resp = test::call_service(&app, req).await;
414        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
415
416        // Test GET /relayers/{id}
417        let req = test::TestRequest::get()
418            .uri("/relayers/test-id")
419            .to_request();
420        let resp = test::call_service(&app, req).await;
421        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
422
423        // Test PATCH /relayers/{id}
424        let req = test::TestRequest::patch()
425            .uri("/relayers/test-id")
426            .set_json(serde_json::json!({"paused": false}))
427            .to_request();
428        let resp = test::call_service(&app, req).await;
429        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
430
431        // Test GET /relayers/{id}/status
432        let req = test::TestRequest::get()
433            .uri("/relayers/test-id/status")
434            .to_request();
435        let resp = test::call_service(&app, req).await;
436        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
437
438        // Test GET /relayers/{id}/balance
439        let req = test::TestRequest::get()
440            .uri("/relayers/test-id/balance")
441            .to_request();
442        let resp = test::call_service(&app, req).await;
443        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
444
445        // Test POST /relayers/{id}/transactions
446        let req = test::TestRequest::post()
447            .uri("/relayers/test-id/transactions")
448            .set_json(serde_json::json!({}))
449            .to_request();
450        let resp = test::call_service(&app, req).await;
451        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
452
453        // Test GET /relayers/{id}/transactions/{tx_id}
454        let req = test::TestRequest::get()
455            .uri("/relayers/test-id/transactions/tx-123")
456            .to_request();
457        let resp = test::call_service(&app, req).await;
458        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
459
460        // Test GET /relayers/{id}/transactions/by-nonce/{nonce}
461        let req = test::TestRequest::get()
462            .uri("/relayers/test-id/transactions/by-nonce/123")
463            .to_request();
464        let resp = test::call_service(&app, req).await;
465        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
466
467        // Test GET /relayers/{id}/transactions
468        let req = test::TestRequest::get()
469            .uri("/relayers/test-id/transactions")
470            .to_request();
471        let resp = test::call_service(&app, req).await;
472        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
473
474        // Test DELETE /relayers/{id}/transactions/pending
475        let req = test::TestRequest::delete()
476            .uri("/relayers/test-id/transactions/pending")
477            .to_request();
478        let resp = test::call_service(&app, req).await;
479        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
480
481        // Test DELETE /relayers/{id}/transactions/{tx_id}
482        let req = test::TestRequest::delete()
483            .uri("/relayers/test-id/transactions/tx-123")
484            .to_request();
485        let resp = test::call_service(&app, req).await;
486        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
487
488        // Test PUT /relayers/{id}/transactions/{tx_id}
489        let req = test::TestRequest::put()
490            .uri("/relayers/test-id/transactions/tx-123")
491            .set_json(serde_json::json!({}))
492            .to_request();
493        let resp = test::call_service(&app, req).await;
494        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
495
496        // Test POST /relayers/{id}/sign
497        let req = test::TestRequest::post()
498            .uri("/relayers/test-id/sign")
499            .set_json(serde_json::json!({
500                "message": "0x1234567890abcdef"
501            }))
502            .to_request();
503        let resp = test::call_service(&app, req).await;
504        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
505
506        // Test POST /relayers/{id}/sign-typed-data
507        let req = test::TestRequest::post()
508            .uri("/relayers/test-id/sign-typed-data")
509            .set_json(serde_json::json!({
510                "domain_separator": "0x1234567890abcdef",
511                "hash_struct_message": "0x1234567890abcdef"
512            }))
513            .to_request();
514        let resp = test::call_service(&app, req).await;
515        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
516
517        // Test POST /relayers/{id}/rpc
518        let req = test::TestRequest::post()
519            .uri("/relayers/test-id/rpc")
520            .set_json(serde_json::json!({
521                "jsonrpc": "2.0",
522                "method": "eth_getBlockByNumber",
523                "params": ["0x1", true],
524                "id": 1
525            }))
526            .to_request();
527        let resp = test::call_service(&app, req).await;
528        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
529
530        // Test POST /relayers/{id}/transactions/sponsored/quote
531        let req = test::TestRequest::post()
532            .uri("/relayers/test-id/transactions/sponsored/quote")
533            .set_json(serde_json::json!({
534                "stellar": {
535                    "transaction_xdr": "test-xdr",
536                    "fee_token": "native"
537                }
538            }))
539            .to_request();
540        let resp = test::call_service(&app, req).await;
541        // Route exists if status is not 404 (could be 400 for validation errors or 500 for internal errors)
542        assert_ne!(resp.status(), StatusCode::NOT_FOUND);
543
544        // Test POST /relayers/{id}/transactions/sponsored/build
545        let req = test::TestRequest::post()
546            .uri("/relayers/test-id/transactions/sponsored/build")
547            .set_json(serde_json::json!({
548                "stellar": {
549                    "transaction_xdr": "test-xdr",
550                    "fee_token": "native"
551                }
552            }))
553            .to_request();
554        let resp = test::call_service(&app, req).await;
555        // Route exists if status is not 404 (could be 400 for validation errors or 500 for internal errors)
556        assert_ne!(resp.status(), StatusCode::NOT_FOUND);
557
558        Ok(())
559    }
560}