kms_secp256k1_api/
lib.rs

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
61/// Creates an Axum application instance with the given configuration.
62///
63/// # Panics
64///
65/// This function will panic if:
66/// - Loading the WASM module fails.
67/// - Initializing the `CryptoService` fails.
68/// - Initializing the key service (`CasperKeysService`) fails.
69///
70/// These errors are propagated via calls to `expect`.
71pub 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
147/// Starts the HTTP server with the given configuration.
148///
149/// # Errors
150/// This function returns an error if the TCP listener cannot be bound,
151/// or if the server fails to start.
152pub 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        // Spawn the server in a background task but abort immediately,
183        // just test that it starts without panics or errors.
184        let server_future = task::spawn(async move { run_server(config).await });
185
186        // Wait briefly or abort since we don't want it running forever.
187        // Here just wait a little and then abort.
188        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
189
190        server_future.abort();
191
192        // If spawn didn't panic, assume success.
193    }
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}