1use 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
166pub fn init(cfg: &mut web::ServiceConfig) {
168 cfg.service(delete_pending_transactions); cfg.service(cancel_transaction); cfg.service(replace_transaction); cfg.service(get_transaction_by_id); cfg.service(get_transaction_by_nonce); cfg.service(send_transaction); cfg.service(list_transactions); cfg.service(get_relayer_status); cfg.service(get_relayer_balance); cfg.service(sign); cfg.service(sign_typed_data); cfg.service(rpc); cfg.service(get_relayer); cfg.service(update_relayer); cfg.service(list_relayers); }
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}