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#[get("/relayers/{relayer_id}/status")]
66async fn get_relayer_status(
67 relayer_id: web::Path<String>,
68 data: web::ThinData<DefaultAppState>,
69) -> impl Responder {
70 relayer::get_relayer_status(relayer_id.into_inner(), data).await
71}
72
73#[get("/relayers/{relayer_id}/balance")]
75async fn get_relayer_balance(
76 relayer_id: web::Path<String>,
77 data: web::ThinData<DefaultAppState>,
78) -> impl Responder {
79 relayer::get_relayer_balance(relayer_id.into_inner(), data).await
80}
81
82#[post("/relayers/{relayer_id}/transactions")]
84async fn send_transaction(
85 relayer_id: web::Path<String>,
86 req: web::Json<serde_json::Value>,
87 data: web::ThinData<DefaultAppState>,
88) -> impl Responder {
89 relayer::send_transaction(relayer_id.into_inner(), req.into_inner(), data).await
90}
91
92#[derive(Deserialize, ToSchema)]
93pub struct TransactionPath {
94 relayer_id: String,
95 transaction_id: String,
96}
97
98#[get("/relayers/{relayer_id}/transactions/{transaction_id}")]
100async fn get_transaction_by_id(
101 path: web::Path<TransactionPath>,
102 data: web::ThinData<DefaultAppState>,
103) -> impl Responder {
104 let path = path.into_inner();
105 relayer::get_transaction_by_id(path.relayer_id, path.transaction_id, data).await
106}
107
108#[get("/relayers/{relayer_id}/transactions/by-nonce/{nonce}")]
110async fn get_transaction_by_nonce(
111 params: web::Path<(String, u64)>,
112 data: web::ThinData<DefaultAppState>,
113) -> impl Responder {
114 let params = params.into_inner();
115 relayer::get_transaction_by_nonce(params.0, params.1, data).await
116}
117
118#[get("/relayers/{relayer_id}/transactions")]
120async fn list_transactions(
121 relayer_id: web::Path<String>,
122 query: web::Query<PaginationQuery>,
123 data: web::ThinData<DefaultAppState>,
124) -> impl Responder {
125 relayer::list_transactions(relayer_id.into_inner(), query.into_inner(), data).await
126}
127
128#[delete("/relayers/{relayer_id}/transactions/pending")]
130async fn delete_pending_transactions(
131 relayer_id: web::Path<String>,
132 data: web::ThinData<DefaultAppState>,
133) -> impl Responder {
134 relayer::delete_pending_transactions(relayer_id.into_inner(), data).await
135}
136
137#[delete("/relayers/{relayer_id}/transactions/{transaction_id}")]
139async fn cancel_transaction(
140 path: web::Path<TransactionPath>,
141 data: web::ThinData<DefaultAppState>,
142) -> impl Responder {
143 let path = path.into_inner();
144 relayer::cancel_transaction(path.relayer_id, path.transaction_id, data).await
145}
146
147#[put("/relayers/{relayer_id}/transactions/{transaction_id}")]
149async fn replace_transaction(
150 path: web::Path<TransactionPath>,
151 req: web::Json<serde_json::Value>,
152 data: web::ThinData<DefaultAppState>,
153) -> impl Responder {
154 let path = path.into_inner();
155 relayer::replace_transaction(path.relayer_id, path.transaction_id, req.into_inner(), data).await
156}
157
158#[post("/relayers/{relayer_id}/sign")]
160async fn sign(
161 relayer_id: web::Path<String>,
162 req: web::Json<SignDataRequest>,
163 data: web::ThinData<DefaultAppState>,
164) -> impl Responder {
165 relayer::sign_data(relayer_id.into_inner(), req.into_inner(), data).await
166}
167
168#[post("/relayers/{relayer_id}/sign-typed-data")]
170async fn sign_typed_data(
171 relayer_id: web::Path<String>,
172 req: web::Json<SignTypedDataRequest>,
173 data: web::ThinData<DefaultAppState>,
174) -> impl Responder {
175 relayer::sign_typed_data(relayer_id.into_inner(), req.into_inner(), data).await
176}
177
178#[post("/relayers/{relayer_id}/sign-transaction")]
180async fn sign_transaction(
181 relayer_id: web::Path<String>,
182 req: web::Json<SignTransactionRequest>,
183 data: web::ThinData<DefaultAppState>,
184) -> impl Responder {
185 relayer::sign_transaction(relayer_id.into_inner(), req.into_inner(), data).await
186}
187
188#[post("/relayers/{relayer_id}/rpc")]
190async fn rpc(
191 relayer_id: web::Path<String>,
192 req: web::Json<serde_json::Value>,
193 data: web::ThinData<DefaultAppState>,
194) -> impl Responder {
195 relayer::relayer_rpc(relayer_id.into_inner(), req.into_inner(), data).await
196}
197
198#[post("/relayers/{relayer_id}/transactions/sponsored/quote")]
200async fn quote_sponsored_transaction(
201 relayer_id: web::Path<String>,
202 req: web::Json<SponsoredTransactionQuoteRequest>,
203 data: web::ThinData<DefaultAppState>,
204) -> impl Responder {
205 relayer::quote_sponsored_transaction(relayer_id.into_inner(), req.into_inner(), data).await
206}
207
208#[post("/relayers/{relayer_id}/transactions/sponsored/build")]
210async fn build_sponsored_transaction(
211 relayer_id: web::Path<String>,
212 req: web::Json<SponsoredTransactionBuildRequest>,
213 data: web::ThinData<DefaultAppState>,
214) -> impl Responder {
215 relayer::build_sponsored_transaction(relayer_id.into_inner(), req.into_inner(), data).await
216}
217
218pub fn init(cfg: &mut web::ServiceConfig) {
220 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); }
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::{
249 config::{EvmNetworkConfig, NetworkConfigCommon},
250 jobs::MockJobProducerTrait,
251 models::{
252 ApiKeyRepoModel, AppState, EvmTransactionData, LocalSignerConfigStorage,
253 NetworkConfigData, NetworkRepoModel, NetworkTransactionData, NetworkType,
254 RelayerEvmPolicy, RelayerNetworkPolicy, RelayerRepoModel, RpcConfig, SecretString,
255 SignerConfigStorage, SignerRepoModel, TransactionRepoModel, TransactionStatus, U256,
256 },
257 repositories::{
258 ApiKeyRepositoryStorage, ApiKeyRepositoryTrait, NetworkRepositoryStorage,
259 NotificationRepositoryStorage, PluginRepositoryStorage, RelayerRepositoryStorage,
260 Repository, SignerRepositoryStorage, TransactionCounterRepositoryStorage,
261 TransactionRepositoryStorage,
262 },
263 };
264 use actix_web::{http::StatusCode, test, App};
265 use std::sync::Arc;
266
267 async fn get_test_app_state() -> AppState<
269 MockJobProducerTrait,
270 RelayerRepositoryStorage,
271 TransactionRepositoryStorage,
272 NetworkRepositoryStorage,
273 NotificationRepositoryStorage,
274 SignerRepositoryStorage,
275 TransactionCounterRepositoryStorage,
276 PluginRepositoryStorage,
277 ApiKeyRepositoryStorage,
278 > {
279 let relayer_repo = Arc::new(RelayerRepositoryStorage::new_in_memory());
280 let transaction_repo = Arc::new(TransactionRepositoryStorage::new_in_memory());
281 let signer_repo = Arc::new(SignerRepositoryStorage::new_in_memory());
282 let network_repo = Arc::new(NetworkRepositoryStorage::new_in_memory());
283 let api_key_repo = Arc::new(ApiKeyRepositoryStorage::new_in_memory());
284
285 let test_network = NetworkRepoModel {
289 id: "evm:ethereum".to_string(),
290 name: "ethereum".to_string(),
291 network_type: NetworkType::Evm,
292 config: NetworkConfigData::Evm(EvmNetworkConfig {
293 common: NetworkConfigCommon {
294 network: "ethereum".to_string(),
295 from: None,
296 rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
297 explorer_urls: None,
298 average_blocktime_ms: Some(12000),
299 is_testnet: Some(false),
300 tags: None,
301 },
302 chain_id: Some(1),
303 required_confirmations: Some(12),
304 features: None,
305 symbol: Some("ETH".to_string()),
306 gas_price_cache: None,
307 }),
308 };
309 network_repo.create(test_network).await.unwrap();
310
311 let test_signer = SignerRepoModel {
313 id: "test-signer".to_string(),
314 config: SignerConfigStorage::Local(LocalSignerConfigStorage {
315 raw_key: secrets::SecretVec::new(32, |v| v.copy_from_slice(&[0u8; 32])),
316 }),
317 };
318 signer_repo.create(test_signer).await.unwrap();
319
320 let test_relayer = RelayerRepoModel {
322 id: "test-id".to_string(),
323 name: "Test Relayer".to_string(),
324 network: "ethereum".to_string(),
325 network_type: NetworkType::Evm,
326 signer_id: "test-signer".to_string(),
327 address: "0x1234567890123456789012345678901234567890".to_string(),
328 paused: false,
329 system_disabled: false,
330 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
331 notification_id: None,
332 custom_rpc_urls: None,
333 ..Default::default()
334 };
335 relayer_repo.create(test_relayer).await.unwrap();
336
337 let test_transaction = TransactionRepoModel {
339 id: "tx-123".to_string(),
340 relayer_id: "test-id".to_string(),
341 status: TransactionStatus::Pending,
342 status_reason: None,
343 created_at: chrono::Utc::now().to_rfc3339(),
344 sent_at: None,
345 confirmed_at: None,
346 valid_until: None,
347 network_data: NetworkTransactionData::Evm(EvmTransactionData {
348 gas_price: Some(20000000000u128),
349 gas_limit: Some(21000u64),
350 nonce: Some(1u64),
351 value: U256::from(0u64),
352 data: Some("0x".to_string()),
353 from: "0x1234567890123456789012345678901234567890".to_string(),
354 to: Some("0x9876543210987654321098765432109876543210".to_string()),
355 chain_id: 1u64,
356 hash: Some("0xabcdef".to_string()),
357 signature: None,
358 speed: None,
359 max_fee_per_gas: None,
360 max_priority_fee_per_gas: None,
361 raw: None,
362 }),
363 priced_at: None,
364 hashes: vec!["0xabcdef".to_string()],
365 network_type: NetworkType::Evm,
366 noop_count: None,
367 is_canceled: Some(false),
368 delete_at: None,
369 metadata: None,
370 };
371 transaction_repo.create(test_transaction).await.unwrap();
372
373 let test_api_key = ApiKeyRepoModel {
375 id: "test-api-key".to_string(),
376 name: "Test API Key".to_string(),
377 value: SecretString::new("test-value"),
378 permissions: vec!["test-permission".to_string()],
379 created_at: chrono::Utc::now().to_rfc3339(),
380 allowed_origins: vec!["*".to_string()],
381 };
382 api_key_repo.create(test_api_key).await.unwrap();
383
384 AppState {
385 relayer_repository: relayer_repo,
386 transaction_repository: transaction_repo,
387 signer_repository: signer_repo,
388 notification_repository: Arc::new(NotificationRepositoryStorage::new_in_memory()),
389 network_repository: network_repo,
390 transaction_counter_store: Arc::new(
391 TransactionCounterRepositoryStorage::new_in_memory(),
392 ),
393 job_producer: Arc::new(MockJobProducerTrait::new()),
394 plugin_repository: Arc::new(PluginRepositoryStorage::new_in_memory()),
395 api_key_repository: api_key_repo,
396 }
397 }
398
399 #[actix_web::test]
400 async fn test_routes_are_registered() -> Result<(), color_eyre::eyre::Error> {
401 let app = test::init_service(
403 App::new()
404 .app_data(web::Data::new(get_test_app_state().await))
405 .configure(init),
406 )
407 .await;
408
409 let req = test::TestRequest::get().uri("/relayers").to_request();
413 let resp = test::call_service(&app, req).await;
414 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
415
416 let req = test::TestRequest::get()
418 .uri("/relayers/test-id")
419 .to_request();
420 let resp = test::call_service(&app, req).await;
421 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
422
423 let req = test::TestRequest::patch()
425 .uri("/relayers/test-id")
426 .set_json(serde_json::json!({"paused": false}))
427 .to_request();
428 let resp = test::call_service(&app, req).await;
429 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
430
431 let req = test::TestRequest::get()
433 .uri("/relayers/test-id/status")
434 .to_request();
435 let resp = test::call_service(&app, req).await;
436 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
437
438 let req = test::TestRequest::get()
440 .uri("/relayers/test-id/balance")
441 .to_request();
442 let resp = test::call_service(&app, req).await;
443 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
444
445 let req = test::TestRequest::post()
447 .uri("/relayers/test-id/transactions")
448 .set_json(serde_json::json!({}))
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::get()
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::get()
462 .uri("/relayers/test-id/transactions/by-nonce/123")
463 .to_request();
464 let resp = test::call_service(&app, req).await;
465 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
466
467 let req = test::TestRequest::get()
469 .uri("/relayers/test-id/transactions")
470 .to_request();
471 let resp = test::call_service(&app, req).await;
472 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
473
474 let req = test::TestRequest::delete()
476 .uri("/relayers/test-id/transactions/pending")
477 .to_request();
478 let resp = test::call_service(&app, req).await;
479 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
480
481 let req = test::TestRequest::delete()
483 .uri("/relayers/test-id/transactions/tx-123")
484 .to_request();
485 let resp = test::call_service(&app, req).await;
486 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
487
488 let req = test::TestRequest::put()
490 .uri("/relayers/test-id/transactions/tx-123")
491 .set_json(serde_json::json!({}))
492 .to_request();
493 let resp = test::call_service(&app, req).await;
494 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
495
496 let req = test::TestRequest::post()
498 .uri("/relayers/test-id/sign")
499 .set_json(serde_json::json!({
500 "message": "0x1234567890abcdef"
501 }))
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::post()
508 .uri("/relayers/test-id/sign-typed-data")
509 .set_json(serde_json::json!({
510 "domain_separator": "0x1234567890abcdef",
511 "hash_struct_message": "0x1234567890abcdef"
512 }))
513 .to_request();
514 let resp = test::call_service(&app, req).await;
515 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
516
517 let req = test::TestRequest::post()
519 .uri("/relayers/test-id/rpc")
520 .set_json(serde_json::json!({
521 "jsonrpc": "2.0",
522 "method": "eth_getBlockByNumber",
523 "params": ["0x1", true],
524 "id": 1
525 }))
526 .to_request();
527 let resp = test::call_service(&app, req).await;
528 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
529
530 let req = test::TestRequest::post()
532 .uri("/relayers/test-id/transactions/sponsored/quote")
533 .set_json(serde_json::json!({
534 "stellar": {
535 "transaction_xdr": "test-xdr",
536 "fee_token": "native"
537 }
538 }))
539 .to_request();
540 let resp = test::call_service(&app, req).await;
541 assert_ne!(resp.status(), StatusCode::NOT_FOUND);
543
544 let req = test::TestRequest::post()
546 .uri("/relayers/test-id/transactions/sponsored/build")
547 .set_json(serde_json::json!({
548 "stellar": {
549 "transaction_xdr": "test-xdr",
550 "fee_token": "native"
551 }
552 }))
553 .to_request();
554 let resp = test::call_service(&app, req).await;
555 assert_ne!(resp.status(), StatusCode::NOT_FOUND);
557
558 Ok(())
559 }
560}