1pub mod audit;
2pub mod handlers;
3pub mod types;
4
5use anyhow::Result;
6use axum::extract::{DefaultBodyLimit, Request};
7use axum::http::{HeaderMap, StatusCode};
8use axum::middleware::{self, Next};
9use axum::response::{IntoResponse, Response};
10use axum::routing::{get, post};
11use axum::{Json, Router};
12use bsv_wallet_toolbox::{Chain, Services, StorageSqlx, Wallet};
13use serde_json::json;
14use std::net::SocketAddr;
15use std::sync::Arc;
16use tower_http::cors::CorsLayer;
17use tower_http::trace::TraceLayer;
18
19use handlers::WalletState;
20
21pub type SpendingLock = Arc<tokio::sync::Mutex<()>>;
28
29#[derive(Clone)]
31#[allow(dead_code)]
32pub struct TlsConfig {
33 pub cert_path: String,
34 pub key_path: String,
35}
36
37#[derive(Clone)]
39pub struct ServerConfig {
40 pub auth_token: Option<String>,
43 pub tls: Option<TlsConfig>,
45 pub chain: Chain,
48 pub bind_addr: std::net::IpAddr,
52 pub callback_token: Option<String>,
56}
57
58impl Default for ServerConfig {
59 fn default() -> Self {
60 Self {
61 auth_token: None,
62 tls: None,
63 chain: Chain::Main,
64 bind_addr: std::net::IpAddr::from([127, 0, 0, 1]),
65 callback_token: None,
66 }
67 }
68}
69
70async fn auth_middleware(headers: HeaderMap, request: Request, next: Next) -> Response {
72 if request.uri().path() == "/arc-callback" {
76 return next.run(request).await;
77 }
78
79 let token = request
81 .extensions()
82 .get::<ServerConfig>()
83 .and_then(|c| c.auth_token.clone());
84
85 if let Some(expected) = token {
86 let provided = headers
87 .get("authorization")
88 .and_then(|v| v.to_str().ok())
89 .and_then(|v| v.strip_prefix("Bearer "));
90
91 match provided {
92 Some(t) if t == expected => {}
93 _ => {
94 return (
95 StatusCode::UNAUTHORIZED,
96 Json(json!({"code": "UNAUTHORIZED", "message": "Invalid or missing bearer token"})),
97 )
98 .into_response();
99 }
100 }
101 }
102
103 next.run(request).await
104}
105
106pub fn make_router(wallet: WalletState, config: ServerConfig) -> Router {
108 let cors = CorsLayer::very_permissive();
109 let cfg = config.clone();
110 let spending_lock: SpendingLock = Arc::new(tokio::sync::Mutex::new(()));
111 let verifier = crate::broadcast_verify::BroadcastVerifier::from_env(config.chain);
115
116 Router::new()
117 .route(
122 "/isAuthenticated",
123 get(handlers::is_authenticated).post(handlers::is_authenticated),
124 )
125 .route("/getPublicKey", post(handlers::get_public_key))
126 .route("/permission-audit", get(permission_audit))
130 .route("/permission-audit/reset", post(permission_audit_reset))
131 .route("/createSignature", post(handlers::create_signature))
132 .route("/createAction", post(handlers::create_action))
133 .route("/internalizeAction", post(handlers::internalize_action))
134 .route(
136 "/getHeight",
137 get(handlers::get_height).post(handlers::get_height),
138 )
139 .route(
140 "/getNetwork",
141 get(handlers::get_network).post(handlers::get_network),
142 )
143 .route(
144 "/getVersion",
145 get(handlers::get_version).post(handlers::get_version),
146 )
147 .route(
148 "/waitForAuthentication",
149 get(handlers::wait_for_authentication).post(handlers::wait_for_authentication),
150 )
151 .route("/getHeaderForHeight", post(handlers::get_header_for_height))
153 .route("/verifySignature", post(handlers::verify_signature))
155 .route("/encrypt", post(handlers::encrypt))
156 .route("/decrypt", post(handlers::decrypt))
157 .route("/createHmac", post(handlers::create_hmac))
158 .route("/verifyHmac", post(handlers::verify_hmac))
159 .route("/signAction", post(handlers::sign_action))
161 .route("/abortAction", post(handlers::abort_action))
162 .route("/listActions", post(handlers::list_actions))
163 .route("/listOutputs", post(handlers::list_outputs))
164 .route("/relinquishOutput", post(handlers::relinquish_output))
165 .route("/acquireCertificate", post(handlers::acquire_certificate))
167 .route("/listCertificates", post(handlers::list_certificates))
168 .route("/proveCertificate", post(handlers::prove_certificate))
169 .route(
170 "/relinquishCertificate",
171 post(handlers::relinquish_certificate),
172 )
173 .route(
175 "/discoverByIdentityKey",
176 post(handlers::discover_by_identity_key),
177 )
178 .route(
179 "/discoverByAttributes",
180 post(handlers::discover_by_attributes),
181 )
182 .route(
183 "/revealCounterpartyKeyLinkage",
184 post(handlers::reveal_counterparty_key_linkage),
185 )
186 .route(
187 "/revealSpecificKeyLinkage",
188 post(handlers::reveal_specific_key_linkage),
189 )
190 .route("/arc-callback", post(arc_callback))
193 .layer(cors)
195 .layer(middleware::from_fn(auth_middleware))
196 .layer(axum::Extension(cfg))
197 .layer(axum::Extension(spending_lock))
198 .layer(axum::Extension(verifier))
199 .layer(TraceLayer::new_for_http())
200 .layer(DefaultBodyLimit::max(50 * 1024 * 1024))
201 .with_state(wallet)
202}
203
204async fn arc_callback(
212 axum::extract::State(wallet): axum::extract::State<WalletState>,
213 request: Request,
214) -> Response {
215 let expected = request
216 .extensions()
217 .get::<ServerConfig>()
218 .and_then(|c| c.callback_token.clone());
219 let Some(expected) = expected else {
220 return (
221 StatusCode::NOT_FOUND,
222 Json(json!({"code": "CALLBACK_DISABLED", "message": "no callback token configured"})),
223 )
224 .into_response();
225 };
226
227 let headers = request.headers().clone();
228 let provided = headers
229 .get("authorization")
230 .and_then(|v| v.to_str().ok())
231 .and_then(|v| v.strip_prefix("Bearer "))
232 .map(str::to_string)
233 .or_else(|| {
234 headers
235 .get("x-callbacktoken")
236 .and_then(|v| v.to_str().ok())
237 .map(str::to_string)
238 });
239
240 if provided.as_deref() != Some(expected.as_str()) {
241 tracing::warn!(
245 has_authorization = headers.contains_key("authorization"),
246 has_x_callbacktoken = headers.contains_key("x-callbacktoken"),
247 "arc-callback: rejected POST with invalid/missing callback token"
248 );
249 return (
250 StatusCode::UNAUTHORIZED,
251 Json(json!({"code": "UNAUTHORIZED", "message": "invalid or missing callback token"})),
252 )
253 .into_response();
254 }
255
256 let bytes = match axum::body::to_bytes(request.into_body(), 1_000_000).await {
257 Ok(b) => b,
258 Err(_) => {
259 return (
260 StatusCode::PAYLOAD_TOO_LARGE,
261 Json(json!({"code": "TOO_LARGE", "message": "payload too large"})),
262 )
263 .into_response();
264 }
265 };
266 let payload: serde_json::Value = match serde_json::from_slice(&bytes) {
267 Ok(v) => v,
268 Err(e) => {
269 return (
270 StatusCode::BAD_REQUEST,
271 Json(json!({"code": "BAD_JSON", "message": e.to_string()})),
272 )
273 .into_response();
274 }
275 };
276
277 match crate::arc_ingest::ingest_arc_payload(wallet.storage(), &payload).await {
278 Ok(action) => (
279 StatusCode::OK,
280 Json(json!({"ok": true, "action": format!("{:?}", action)})),
281 )
282 .into_response(),
283 Err(e) => (
284 StatusCode::BAD_REQUEST,
285 Json(json!({"code": "BAD_PAYLOAD", "message": e.to_string()})),
286 )
287 .into_response(),
288 }
289}
290
291pub async fn run(wallet: WalletState, port: u16, config: ServerConfig) -> Result<()> {
292 let tls = config.tls.clone();
293 let bind_addr = config.bind_addr;
294 let app = make_router(wallet, config);
295 let addr = SocketAddr::from((bind_addr, port));
296
297 #[cfg(feature = "tls")]
298 if let Some(tls_cfg) = tls {
299 use axum_server::tls_rustls::RustlsConfig;
300 let rustls = RustlsConfig::from_pem_file(&tls_cfg.cert_path, &tls_cfg.key_path).await?;
301 tracing::info!("HTTPS server listening on {addr}");
302 eprintln!("HTTPS server listening on {addr}");
303 axum_server::bind_rustls(addr, rustls)
304 .serve(app.into_make_service())
305 .await?;
306 return Ok(());
307 }
308
309 #[cfg(not(feature = "tls"))]
310 if tls.is_some() {
311 anyhow::bail!("TLS requested but binary was built without `--features tls`");
312 }
313
314 tracing::info!("HTTP server listening on {addr}");
315 eprintln!("HTTP server listening on {addr}");
316
317 let listener = tokio::net::TcpListener::bind(addr).await?;
318 axum::serve(listener, app)
319 .with_graceful_shutdown(shutdown_signal())
320 .await?;
321
322 Ok(())
323}
324
325pub fn make_wallet_state(wallet: Wallet<StorageSqlx, Services>) -> WalletState {
326 Arc::new(wallet)
327}
328
329#[allow(dead_code)] pub fn make_wallet_state_from_arc(wallet: Arc<Wallet<StorageSqlx, Services>>) -> WalletState {
338 wallet
339}
340
341async fn shutdown_signal() {
342 tokio::signal::ctrl_c()
343 .await
344 .expect("failed to listen for ctrl-c");
345 eprintln!("\nShutting down...");
346}
347
348async fn permission_audit() -> Json<serde_json::Value> {
350 let entries = audit::snapshot();
351 Json(json!({ "count": entries.len(), "entries": entries }))
352}
353
354async fn permission_audit_reset() -> Json<serde_json::Value> {
356 audit::reset();
357 Json(json!({ "ok": true }))
358}