1use crate::{
2 config::Config,
3 constants::WASM_PATH,
4 routes::{
5 ApiDoc, create_key, delete_key, hello, list_keys, sign_transaction, sign_transaction_hash,
6 verify_signature,
7 },
8 services::{
9 casper_keys_service::CasperKeysService,
10 cosmos_keys_service::CosmosKeysService,
11 crypto_service::CryptoService,
12 ethereum_keys_service::EthereumKeysService,
13 keys_service::KeysServiceTrait,
14 mocks::{
15 mock_casper_keys_service::MockCasperKeysService,
16 mock_cosmos_keys_service::MockCosmosKeysService,
17 mock_ethereum_keys_service::MockEthereumKeysService,
18 },
19 },
20 wasm_loader::WasmLoader,
21};
22use axum::Router;
23use axum::{
24 Extension,
25 routing::{delete, get, post},
26};
27use std::fs;
28use std::sync::Arc;
29use tokio::sync::Mutex;
30use tracing::{info, warn};
31use utoipa::OpenApi;
32use utoipa_swagger_ui::SwaggerUi;
33
34pub mod config;
35pub mod constants;
36pub mod routes;
37pub mod services;
38pub mod wasm_loader;
39
40#[cfg(test)]
41pub mod tests;
42
43static VERSION: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
44 let content = fs::read_to_string("Cargo.toml").unwrap_or_default();
45 let parsed: toml::Value =
46 toml::from_str(&content).unwrap_or_else(|_| toml::Value::Table(toml::map::Map::new()));
47 parsed
48 .get("package")
49 .and_then(|pkg| pkg.get("version"))
50 .and_then(|v| v.as_str())
51 .unwrap_or("unknown")
52 .to_string()
53});
54
55#[derive(Clone)]
56pub struct AppState {
57 pub config: Config,
58 pub keys_service: Arc<Mutex<Box<dyn KeysServiceTrait + Send + Sync>>>,
59}
60
61pub async fn create_app(config: Config) -> Router {
72 let wasm_loader = WasmLoader::new(WASM_PATH)
73 .await
74 .expect("Failed to load WASM module");
75
76 let crypto_service =
77 CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
78
79 let keys_service: Box<dyn KeysServiceTrait> = if config.is_testing_mode() {
80 if config.is_ethereum_mode() {
81 Box::new(
82 MockEthereumKeysService::new(config.clone(), crypto_service)
83 .expect("Failed to initialize MockEthereumKeysService"),
84 )
85 } else if config.is_casper_mode() {
86 Box::new(
87 MockCasperKeysService::new(config.clone(), crypto_service)
88 .expect("Failed to initialize MockCasperKeysService"),
89 )
90 } else if config.is_cosmos_mode() {
91 Box::new(
92 MockCosmosKeysService::new(config.clone(), crypto_service)
93 .expect("Failed to initialize MockCasperKeysService"),
94 )
95 } else {
96 unimplemented!()
97 }
98 } else if config.is_ethereum_mode() {
99 Box::new(
100 EthereumKeysService::new(config.clone(), crypto_service)
101 .await
102 .expect("Failed to initialize Failed to initialize EthereumKeysService"),
103 )
104 } else if config.is_casper_mode() {
105 Box::new(
106 CasperKeysService::new(config.clone(), crypto_service)
107 .await
108 .expect("Failed to initialize Failed to initialize CasperKeysService"),
109 )
110 } else if config.is_cosmos_mode() {
111 Box::new(
112 CosmosKeysService::new(config.clone(), crypto_service)
113 .await
114 .expect("Failed to initialize Failed to initialize CasperKeysService"),
115 )
116 } else {
117 unimplemented!()
118 };
119
120 let shared_state = AppState {
121 config: config.clone(),
122 keys_service: Arc::new(Mutex::new(keys_service)),
123 };
124
125 let mut app = Router::new()
126 .route("/", get(hello))
127 .route("/createKey", post(create_key))
128 .route("/signTransactionHash", post(sign_transaction_hash))
129 .route("/signTransaction", post(sign_transaction))
130 .route("/verifySignature", get(verify_signature));
131
132 if config.is_delete_mode() {
133 app = app.route("/deleteKey", delete(delete_key));
134 }
135
136 if config.is_list_mode() {
137 app = app.route("/listKeys", get(list_keys));
138 }
139
140 app = app.layer(Extension(shared_state));
141
142 let swagger_ui = SwaggerUi::new("/docs/").url("/docs/openapi.json", ApiDoc::openapi());
143
144 app.merge(swagger_ui)
145}
146
147pub async fn run_server(config: Config) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
153 let app = create_app(config.clone()).await;
154
155 let addr = format!("{}:{}", config.get_addr(), config.get_port());
156 info!("🚀 Listening on {addr}");
157
158 if config.is_testing_mode() {
159 warn!("TESTING_MODE ACTIVE");
160 }
161
162 let listener = tokio::net::TcpListener::bind(&addr).await?;
163 axum::serve(listener, app).await?;
164
165 Ok(())
166}
167
168#[cfg(test)]
169mod tests_lib {
170 use crate::config::ConfigBuilder;
171
172 use super::*;
173 use tokio::task;
174
175 #[tokio::test]
176 async fn test_run_server_creates_app() {
177 let config = ConfigBuilder::new()
178 .with_testing_mode(true)
179 .with_port(0)
180 .build();
181
182 let server_future = task::spawn(async move { run_server(config).await });
185
186 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
189
190 server_future.abort();
191
192 }
194
195 #[cfg(test)]
196 mod tests_create_app {
197 use super::*;
198 use crate::config::ConfigBuilder;
199 use axum::http;
200 use tower::ServiceExt;
201
202 #[tokio::test]
203 async fn test_create_app_routes() {
204 let config = ConfigBuilder::new()
205 .with_testing_mode(true)
206 .with_delete_mode(true)
207 .with_list_mode(true)
208 .build();
209
210 let app = create_app(config.clone()).await;
211
212 let response = app
213 .clone()
214 .oneshot(
215 http::Request::builder()
216 .uri("/")
217 .body(axum::body::Body::empty())
218 .unwrap(),
219 )
220 .await
221 .unwrap();
222
223 assert_eq!(response.status(), http::StatusCode::OK);
224
225 let delete_route = app
226 .clone()
227 .oneshot(
228 http::Request::builder()
229 .method("DELETE")
230 .uri("/deleteKey?key=test_key")
231 .body(axum::body::Body::empty())
232 .unwrap(),
233 )
234 .await
235 .unwrap();
236
237 assert!(
238 delete_route.status().is_success(),
239 "Expected /deleteKey route to exist or return 404",
240 );
241
242 let list_route = app
243 .clone()
244 .oneshot(
245 http::Request::builder()
246 .method("GET")
247 .uri("/listKeys")
248 .body(axum::body::Body::empty())
249 .unwrap(),
250 )
251 .await
252 .unwrap();
253
254 assert!(
255 list_route.status().is_server_error(),
256 "Expected /listKeys route to exist and return 500"
257 );
258
259 let create_key_response = app
260 .clone()
261 .oneshot(
262 http::Request::builder()
263 .method("POST")
264 .uri("/createKey")
265 .body(axum::body::Body::empty())
266 .unwrap(),
267 )
268 .await
269 .unwrap();
270
271 assert_ne!(create_key_response.status(), http::StatusCode::NOT_FOUND);
272
273 let list_route = app
274 .clone()
275 .oneshot(
276 http::Request::builder()
277 .method("GET")
278 .uri("/listKeys")
279 .body(axum::body::Body::empty())
280 .unwrap(),
281 )
282 .await
283 .unwrap();
284
285 assert!(
286 list_route.status().is_success(),
287 "Expected /listKeys route to exist"
288 );
289
290 let sign_hash_response = app
291 .clone()
292 .oneshot(
293 http::Request::builder()
294 .method("POST")
295 .uri("/signTransactionHash")
296 .body(axum::body::Body::empty())
297 .unwrap(),
298 )
299 .await
300 .unwrap();
301
302 assert_ne!(
303 sign_hash_response.status(),
304 http::StatusCode::NOT_FOUND,
305 "Expected /signTransactionHash route to exist"
306 );
307
308 let sign_tx_response = app
309 .clone()
310 .oneshot(
311 http::Request::builder()
312 .method("POST")
313 .uri("/signTransaction")
314 .body(axum::body::Body::empty())
315 .unwrap(),
316 )
317 .await
318 .unwrap();
319 assert_ne!(
320 sign_tx_response.status(),
321 http::StatusCode::NOT_FOUND,
322 "Expected /signTransaction route to exist"
323 );
324
325 let verify_response = app
326 .clone()
327 .oneshot(
328 http::Request::builder()
329 .method("GET")
330 .uri("/verifySignature")
331 .body(axum::body::Body::empty())
332 .unwrap(),
333 )
334 .await
335 .unwrap();
336
337 assert_ne!(
338 verify_response.status(),
339 http::StatusCode::NOT_FOUND,
340 "Expected /verifySignature route to exist"
341 );
342
343 let openapi_response = app
344 .clone()
345 .oneshot(
346 http::Request::builder()
347 .method("GET")
348 .uri("/docs/openapi.json")
349 .body(axum::body::Body::empty())
350 .unwrap(),
351 )
352 .await
353 .unwrap();
354
355 assert!(
356 openapi_response.status().is_success(),
357 "Expected OpenAPI JSON /docs/openapi.json route to exist and respond successfully"
358 );
359 }
360 }
361}