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