1use 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
193pub fn init(cfg: &mut web::ServiceConfig) {
195 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(sign_transaction); cfg.service(rpc); cfg.service(get_relayer); cfg.service(create_relayer); cfg.service(update_relayer); cfg.service(delete_relayer); cfg.service(list_relayers); }
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}