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