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