1use 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#[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#[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#[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#[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#[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#[derive(Debug, Deserialize)]
66pub struct StatusQuery {
67 #[serde(default = "default_true")]
68 pub include_balance: bool,
69 #[serde(default = "default_true")]
70 pub include_pending_count: bool,
71 #[serde(default = "default_true")]
72 pub include_last_confirmed_tx: bool,
73}
74
75fn default_true() -> bool {
76 true
77}
78
79#[get("/relayers/{relayer_id}/status")]
81async fn get_relayer_status(
82 relayer_id: web::Path<String>,
83 query: web::Query<StatusQuery>,
84 data: web::ThinData<DefaultAppState>,
85) -> impl Responder {
86 let q = query.into_inner();
87 relayer::get_relayer_status(
88 relayer_id.into_inner(),
89 crate::models::GetStatusOptions {
90 include_balance: q.include_balance,
91 include_pending_count: q.include_pending_count,
92 include_last_confirmed_tx: q.include_last_confirmed_tx,
93 },
94 data,
95 )
96 .await
97}
98
99#[get("/relayers/{relayer_id}/balance")]
101async fn get_relayer_balance(
102 relayer_id: web::Path<String>,
103 data: web::ThinData<DefaultAppState>,
104) -> impl Responder {
105 relayer::get_relayer_balance(relayer_id.into_inner(), data).await
106}
107
108#[post("/relayers/{relayer_id}/transactions")]
110async fn send_transaction(
111 relayer_id: web::Path<String>,
112 req: web::Json<serde_json::Value>,
113 data: web::ThinData<DefaultAppState>,
114) -> impl Responder {
115 relayer::send_transaction(relayer_id.into_inner(), req.into_inner(), data).await
116}
117
118#[derive(Deserialize, ToSchema)]
119pub struct TransactionPath {
120 relayer_id: String,
121 transaction_id: String,
122}
123
124#[get("/relayers/{relayer_id}/transactions/{transaction_id}")]
126async fn get_transaction_by_id(
127 path: web::Path<TransactionPath>,
128 data: web::ThinData<DefaultAppState>,
129) -> impl Responder {
130 let path = path.into_inner();
131 relayer::get_transaction_by_id(path.relayer_id, path.transaction_id, data).await
132}
133
134#[get("/relayers/{relayer_id}/transactions/by-nonce/{nonce}")]
136async fn get_transaction_by_nonce(
137 params: web::Path<(String, u64)>,
138 data: web::ThinData<DefaultAppState>,
139) -> impl Responder {
140 let params = params.into_inner();
141 relayer::get_transaction_by_nonce(params.0, params.1, data).await
142}
143
144#[get("/relayers/{relayer_id}/transactions")]
146async fn list_transactions(
147 relayer_id: web::Path<String>,
148 query: web::Query<PaginationQuery>,
149 data: web::ThinData<DefaultAppState>,
150) -> impl Responder {
151 relayer::list_transactions(relayer_id.into_inner(), query.into_inner(), data).await
152}
153
154#[delete("/relayers/{relayer_id}/transactions/pending")]
156async fn delete_pending_transactions(
157 relayer_id: web::Path<String>,
158 data: web::ThinData<DefaultAppState>,
159) -> impl Responder {
160 relayer::delete_pending_transactions(relayer_id.into_inner(), data).await
161}
162
163#[delete("/relayers/{relayer_id}/transactions/{transaction_id}")]
165async fn cancel_transaction(
166 path: web::Path<TransactionPath>,
167 data: web::ThinData<DefaultAppState>,
168) -> impl Responder {
169 let path = path.into_inner();
170 relayer::cancel_transaction(path.relayer_id, path.transaction_id, data).await
171}
172
173#[put("/relayers/{relayer_id}/transactions/{transaction_id}")]
175async fn replace_transaction(
176 path: web::Path<TransactionPath>,
177 req: web::Json<serde_json::Value>,
178 data: web::ThinData<DefaultAppState>,
179) -> impl Responder {
180 let path = path.into_inner();
181 relayer::replace_transaction(path.relayer_id, path.transaction_id, req.into_inner(), data).await
182}
183
184#[post("/relayers/{relayer_id}/sign")]
186async fn sign(
187 relayer_id: web::Path<String>,
188 req: web::Json<SignDataRequest>,
189 data: web::ThinData<DefaultAppState>,
190) -> impl Responder {
191 relayer::sign_data(relayer_id.into_inner(), req.into_inner(), data).await
192}
193
194#[post("/relayers/{relayer_id}/sign-typed-data")]
196async fn sign_typed_data(
197 relayer_id: web::Path<String>,
198 req: web::Json<SignTypedDataRequest>,
199 data: web::ThinData<DefaultAppState>,
200) -> impl Responder {
201 relayer::sign_typed_data(relayer_id.into_inner(), req.into_inner(), data).await
202}
203
204#[post("/relayers/{relayer_id}/sign-transaction")]
206async fn sign_transaction(
207 relayer_id: web::Path<String>,
208 req: web::Json<SignTransactionRequest>,
209 data: web::ThinData<DefaultAppState>,
210) -> impl Responder {
211 relayer::sign_transaction(relayer_id.into_inner(), req.into_inner(), data).await
212}
213
214#[post("/relayers/{relayer_id}/rpc")]
216async fn rpc(
217 relayer_id: web::Path<String>,
218 req: web::Json<serde_json::Value>,
219 data: web::ThinData<DefaultAppState>,
220) -> impl Responder {
221 relayer::relayer_rpc(relayer_id.into_inner(), req.into_inner(), data).await
222}
223
224#[post("/relayers/{relayer_id}/transactions/sponsored/quote")]
226async fn quote_sponsored_transaction(
227 relayer_id: web::Path<String>,
228 req: web::Json<SponsoredTransactionQuoteRequest>,
229 data: web::ThinData<DefaultAppState>,
230) -> impl Responder {
231 relayer::quote_sponsored_transaction(relayer_id.into_inner(), req.into_inner(), data).await
232}
233
234#[post("/relayers/{relayer_id}/transactions/sponsored/build")]
236async fn build_sponsored_transaction(
237 relayer_id: web::Path<String>,
238 req: web::Json<SponsoredTransactionBuildRequest>,
239 data: web::ThinData<DefaultAppState>,
240) -> impl Responder {
241 relayer::build_sponsored_transaction(relayer_id.into_inner(), req.into_inner(), data).await
242}
243
244pub fn init(cfg: &mut web::ServiceConfig) {
246 cfg.service(delete_pending_transactions); cfg.service(quote_sponsored_transaction); cfg.service(build_sponsored_transaction); 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); }
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::{
275 config::{EvmNetworkConfig, NetworkConfigCommon},
276 jobs::MockJobProducerTrait,
277 models::{
278 ApiKeyRepoModel, AppState, EvmTransactionData, LocalSignerConfigStorage,
279 NetworkConfigData, NetworkRepoModel, NetworkTransactionData, NetworkType,
280 RelayerEvmPolicy, RelayerNetworkPolicy, RelayerRepoModel, RpcConfig, SecretString,
281 SignerConfigStorage, SignerRepoModel, TransactionRepoModel, TransactionStatus, U256,
282 },
283 repositories::{
284 ApiKeyRepositoryStorage, ApiKeyRepositoryTrait, NetworkRepositoryStorage,
285 NotificationRepositoryStorage, PluginRepositoryStorage, RelayerRepositoryStorage,
286 Repository, SignerRepositoryStorage, TransactionCounterRepositoryStorage,
287 TransactionRepositoryStorage,
288 },
289 };
290 use actix_web::{http::StatusCode, test, App};
291 use std::sync::Arc;
292
293 async fn get_test_app_state() -> AppState<
295 MockJobProducerTrait,
296 RelayerRepositoryStorage,
297 TransactionRepositoryStorage,
298 NetworkRepositoryStorage,
299 NotificationRepositoryStorage,
300 SignerRepositoryStorage,
301 TransactionCounterRepositoryStorage,
302 PluginRepositoryStorage,
303 ApiKeyRepositoryStorage,
304 > {
305 let relayer_repo = Arc::new(RelayerRepositoryStorage::new_in_memory());
306 let transaction_repo = Arc::new(TransactionRepositoryStorage::new_in_memory());
307 let signer_repo = Arc::new(SignerRepositoryStorage::new_in_memory());
308 let network_repo = Arc::new(NetworkRepositoryStorage::new_in_memory());
309 let api_key_repo = Arc::new(ApiKeyRepositoryStorage::new_in_memory());
310
311 let test_network = NetworkRepoModel {
315 id: "evm:ethereum".to_string(),
316 name: "ethereum".to_string(),
317 network_type: NetworkType::Evm,
318 config: NetworkConfigData::Evm(EvmNetworkConfig {
319 common: NetworkConfigCommon {
320 network: "ethereum".to_string(),
321 from: None,
322 rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
323 explorer_urls: None,
324 average_blocktime_ms: Some(12000),
325 is_testnet: Some(false),
326 tags: None,
327 },
328 chain_id: Some(1),
329 required_confirmations: Some(12),
330 features: None,
331 symbol: Some("ETH".to_string()),
332 gas_price_cache: None,
333 }),
334 };
335 network_repo.create(test_network).await.unwrap();
336
337 let test_signer = SignerRepoModel {
339 id: "test-signer".to_string(),
340 config: SignerConfigStorage::Local(LocalSignerConfigStorage {
341 raw_key: secrets::SecretVec::new(32, |v| v.copy_from_slice(&[0u8; 32])),
342 }),
343 };
344 signer_repo.create(test_signer).await.unwrap();
345
346 let test_relayer = RelayerRepoModel {
348 id: "test-id".to_string(),
349 name: "Test Relayer".to_string(),
350 network: "ethereum".to_string(),
351 network_type: NetworkType::Evm,
352 signer_id: "test-signer".to_string(),
353 address: "0x1234567890123456789012345678901234567890".to_string(),
354 paused: false,
355 system_disabled: false,
356 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
357 notification_id: None,
358 custom_rpc_urls: None,
359 ..Default::default()
360 };
361 relayer_repo.create(test_relayer).await.unwrap();
362
363 let test_transaction = TransactionRepoModel {
365 id: "tx-123".to_string(),
366 relayer_id: "test-id".to_string(),
367 status: TransactionStatus::Pending,
368 status_reason: None,
369 created_at: chrono::Utc::now().to_rfc3339(),
370 sent_at: None,
371 confirmed_at: None,
372 valid_until: None,
373 network_data: NetworkTransactionData::Evm(EvmTransactionData {
374 gas_price: Some(20000000000u128),
375 gas_limit: Some(21000u64),
376 nonce: Some(1u64),
377 value: U256::from(0u64),
378 data: Some("0x".to_string()),
379 from: "0x1234567890123456789012345678901234567890".to_string(),
380 to: Some("0x9876543210987654321098765432109876543210".to_string()),
381 chain_id: 1u64,
382 hash: Some("0xabcdef".to_string()),
383 signature: None,
384 speed: None,
385 max_fee_per_gas: None,
386 max_priority_fee_per_gas: None,
387 raw: None,
388 }),
389 priced_at: None,
390 hashes: vec!["0xabcdef".to_string()],
391 network_type: NetworkType::Evm,
392 noop_count: None,
393 is_canceled: Some(false),
394 delete_at: None,
395 };
396 transaction_repo.create(test_transaction).await.unwrap();
397
398 let test_api_key = ApiKeyRepoModel {
400 id: "test-api-key".to_string(),
401 name: "Test API Key".to_string(),
402 value: SecretString::new("test-value"),
403 permissions: vec!["test-permission".to_string()],
404 created_at: chrono::Utc::now().to_rfc3339(),
405 allowed_origins: vec!["*".to_string()],
406 };
407 api_key_repo.create(test_api_key).await.unwrap();
408
409 AppState {
410 relayer_repository: relayer_repo,
411 transaction_repository: transaction_repo,
412 signer_repository: signer_repo,
413 notification_repository: Arc::new(NotificationRepositoryStorage::new_in_memory()),
414 network_repository: network_repo,
415 transaction_counter_store: Arc::new(
416 TransactionCounterRepositoryStorage::new_in_memory(),
417 ),
418 job_producer: Arc::new(MockJobProducerTrait::new()),
419 plugin_repository: Arc::new(PluginRepositoryStorage::new_in_memory()),
420 api_key_repository: api_key_repo,
421 }
422 }
423
424 #[actix_web::test]
425 async fn test_routes_are_registered() -> Result<(), color_eyre::eyre::Error> {
426 let app = test::init_service(
428 App::new()
429 .app_data(web::Data::new(get_test_app_state().await))
430 .configure(init),
431 )
432 .await;
433
434 let req = test::TestRequest::get().uri("/relayers").to_request();
438 let resp = test::call_service(&app, req).await;
439 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
440
441 let req = test::TestRequest::get()
443 .uri("/relayers/test-id")
444 .to_request();
445 let resp = test::call_service(&app, req).await;
446 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
447
448 let req = test::TestRequest::patch()
450 .uri("/relayers/test-id")
451 .set_json(serde_json::json!({"paused": false}))
452 .to_request();
453 let resp = test::call_service(&app, req).await;
454 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
455
456 let req = test::TestRequest::get()
458 .uri("/relayers/test-id/status")
459 .to_request();
460 let resp = test::call_service(&app, req).await;
461 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
462
463 let req = test::TestRequest::get()
465 .uri("/relayers/test-id/balance")
466 .to_request();
467 let resp = test::call_service(&app, req).await;
468 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
469
470 let req = test::TestRequest::post()
472 .uri("/relayers/test-id/transactions")
473 .set_json(serde_json::json!({}))
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::get()
480 .uri("/relayers/test-id/transactions/tx-123")
481 .to_request();
482 let resp = test::call_service(&app, req).await;
483 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
484
485 let req = test::TestRequest::get()
487 .uri("/relayers/test-id/transactions/by-nonce/123")
488 .to_request();
489 let resp = test::call_service(&app, req).await;
490 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
491
492 let req = test::TestRequest::get()
494 .uri("/relayers/test-id/transactions")
495 .to_request();
496 let resp = test::call_service(&app, req).await;
497 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
498
499 let req = test::TestRequest::delete()
501 .uri("/relayers/test-id/transactions/pending")
502 .to_request();
503 let resp = test::call_service(&app, req).await;
504 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
505
506 let req = test::TestRequest::delete()
508 .uri("/relayers/test-id/transactions/tx-123")
509 .to_request();
510 let resp = test::call_service(&app, req).await;
511 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
512
513 let req = test::TestRequest::put()
515 .uri("/relayers/test-id/transactions/tx-123")
516 .set_json(serde_json::json!({}))
517 .to_request();
518 let resp = test::call_service(&app, req).await;
519 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
520
521 let req = test::TestRequest::post()
523 .uri("/relayers/test-id/sign")
524 .set_json(serde_json::json!({
525 "message": "0x1234567890abcdef"
526 }))
527 .to_request();
528 let resp = test::call_service(&app, req).await;
529 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
530
531 let req = test::TestRequest::post()
533 .uri("/relayers/test-id/sign-typed-data")
534 .set_json(serde_json::json!({
535 "domain_separator": "0x1234567890abcdef",
536 "hash_struct_message": "0x1234567890abcdef"
537 }))
538 .to_request();
539 let resp = test::call_service(&app, req).await;
540 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
541
542 let req = test::TestRequest::post()
544 .uri("/relayers/test-id/rpc")
545 .set_json(serde_json::json!({
546 "jsonrpc": "2.0",
547 "method": "eth_getBlockByNumber",
548 "params": ["0x1", true],
549 "id": 1
550 }))
551 .to_request();
552 let resp = test::call_service(&app, req).await;
553 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
554
555 let req = test::TestRequest::post()
557 .uri("/relayers/test-id/transactions/sponsored/quote")
558 .set_json(serde_json::json!({
559 "stellar": {
560 "transaction_xdr": "test-xdr",
561 "fee_token": "native"
562 }
563 }))
564 .to_request();
565 let resp = test::call_service(&app, req).await;
566 assert_ne!(resp.status(), StatusCode::NOT_FOUND);
568
569 let req = test::TestRequest::post()
571 .uri("/relayers/test-id/transactions/sponsored/build")
572 .set_json(serde_json::json!({
573 "stellar": {
574 "transaction_xdr": "test-xdr",
575 "fee_token": "native"
576 }
577 }))
578 .to_request();
579 let resp = test::call_service(&app, req).await;
580 assert_ne!(resp.status(), StatusCode::NOT_FOUND);
582
583 Ok(())
584 }
585
586 #[actix_web::test]
587 async fn test_status_query_defaults() {
588 let query: StatusQuery = serde_json::from_value(serde_json::json!({})).unwrap();
590 assert!(query.include_balance);
591 assert!(query.include_pending_count);
592 assert!(query.include_last_confirmed_tx);
593 }
594
595 #[actix_web::test]
596 async fn test_status_query_all_false() {
597 let query: StatusQuery = serde_json::from_value(serde_json::json!({
598 "include_balance": false,
599 "include_pending_count": false,
600 "include_last_confirmed_tx": false,
601 }))
602 .unwrap();
603 assert!(!query.include_balance);
604 assert!(!query.include_pending_count);
605 assert!(!query.include_last_confirmed_tx);
606 }
607
608 #[actix_web::test]
609 async fn test_status_query_partial() {
610 let query: StatusQuery = serde_json::from_value(serde_json::json!({
612 "include_balance": false,
613 }))
614 .unwrap();
615 assert!(!query.include_balance);
616 assert!(query.include_pending_count);
617 assert!(query.include_last_confirmed_tx);
618 }
619
620 #[actix_web::test]
621 async fn test_status_route_accepts_query_params() {
622 let app = test::init_service(
623 App::new()
624 .app_data(web::Data::new(get_test_app_state().await))
625 .configure(init),
626 )
627 .await;
628
629 let req = test::TestRequest::get()
631 .uri("/relayers/test-id/status?include_balance=false&include_pending_count=false")
632 .to_request();
633 let resp = test::call_service(&app, req).await;
634 assert_ne!(resp.status(), StatusCode::NOT_FOUND);
635 }
636}