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