pub mod handlers;
pub mod types;
use anyhow::Result;
use axum::extract::{DefaultBodyLimit, Request};
use axum::http::{HeaderMap, StatusCode};
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use bsv_wallet_toolbox::{Services, StorageSqlx, Wallet};
use serde_json::json;
use std::net::SocketAddr;
use std::sync::Arc;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use handlers::WalletState;
pub type SpendingLock = Arc<tokio::sync::Mutex<()>>;
#[derive(Clone)]
#[allow(dead_code)]
pub struct TlsConfig {
pub cert_path: String,
pub key_path: String,
}
#[derive(Clone, Default)]
pub struct ServerConfig {
pub auth_token: Option<String>,
pub tls: Option<TlsConfig>,
}
async fn auth_middleware(headers: HeaderMap, request: Request, next: Next) -> Response {
let token = request
.extensions()
.get::<ServerConfig>()
.and_then(|c| c.auth_token.clone());
if let Some(expected) = token {
let provided = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "));
match provided {
Some(t) if t == expected => {}
_ => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"code": "UNAUTHORIZED", "message": "Invalid or missing bearer token"})),
)
.into_response();
}
}
}
next.run(request).await
}
pub fn make_router(wallet: WalletState, config: ServerConfig) -> Router {
let cors = CorsLayer::very_permissive();
let cfg = config.clone();
let spending_lock: SpendingLock = Arc::new(tokio::sync::Mutex::new(()));
Router::new()
.route("/isAuthenticated", get(handlers::is_authenticated))
.route("/getPublicKey", post(handlers::get_public_key))
.route("/createSignature", post(handlers::create_signature))
.route("/createAction", post(handlers::create_action))
.route("/internalizeAction", post(handlers::internalize_action))
.route("/getHeight", get(handlers::get_height))
.route("/getNetwork", get(handlers::get_network))
.route("/getVersion", get(handlers::get_version))
.route(
"/waitForAuthentication",
get(handlers::wait_for_authentication),
)
.route("/getHeaderForHeight", post(handlers::get_header_for_height))
.route("/verifySignature", post(handlers::verify_signature))
.route("/encrypt", post(handlers::encrypt))
.route("/decrypt", post(handlers::decrypt))
.route("/createHmac", post(handlers::create_hmac))
.route("/verifyHmac", post(handlers::verify_hmac))
.route("/signAction", post(handlers::sign_action))
.route("/abortAction", post(handlers::abort_action))
.route("/listActions", post(handlers::list_actions))
.route("/listOutputs", post(handlers::list_outputs))
.route("/relinquishOutput", post(handlers::relinquish_output))
.route("/acquireCertificate", post(handlers::acquire_certificate))
.route("/listCertificates", post(handlers::list_certificates))
.route("/proveCertificate", post(handlers::prove_certificate))
.route(
"/relinquishCertificate",
post(handlers::relinquish_certificate),
)
.route(
"/discoverByIdentityKey",
post(handlers::discover_by_identity_key),
)
.route(
"/discoverByAttributes",
post(handlers::discover_by_attributes),
)
.route(
"/revealCounterpartyKeyLinkage",
post(handlers::reveal_counterparty_key_linkage),
)
.route(
"/revealSpecificKeyLinkage",
post(handlers::reveal_specific_key_linkage),
)
.layer(cors)
.layer(middleware::from_fn(auth_middleware))
.layer(axum::Extension(cfg))
.layer(axum::Extension(spending_lock))
.layer(TraceLayer::new_for_http())
.layer(DefaultBodyLimit::max(50 * 1024 * 1024))
.with_state(wallet)
}
pub async fn run(wallet: WalletState, port: u16, config: ServerConfig) -> Result<()> {
let tls = config.tls.clone();
let app = make_router(wallet, config);
let addr = SocketAddr::from(([127, 0, 0, 1], port));
#[cfg(feature = "tls")]
if let Some(tls_cfg) = tls {
use axum_server::tls_rustls::RustlsConfig;
let rustls = RustlsConfig::from_pem_file(&tls_cfg.cert_path, &tls_cfg.key_path).await?;
tracing::info!("HTTPS server listening on {addr}");
eprintln!("HTTPS server listening on {addr}");
axum_server::bind_rustls(addr, rustls)
.serve(app.into_make_service())
.await?;
return Ok(());
}
#[cfg(not(feature = "tls"))]
if tls.is_some() {
anyhow::bail!("TLS requested but binary was built without `--features tls`");
}
tracing::info!("HTTP server listening on {addr}");
eprintln!("HTTP server listening on {addr}");
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
Ok(())
}
pub fn make_wallet_state(wallet: Wallet<StorageSqlx, Services>) -> WalletState {
Arc::new(wallet)
}
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("failed to listen for ctrl-c");
eprintln!("\nShutting down...");
}