1
2#[macro_use]
3extern crate anyhow;
4
5pub mod api;
6pub mod auth;
7pub mod config;
8pub mod error;
9
10use crate::auth::AuthToken;
11pub use crate::config::Config;
12pub use axum::http;
13use chrono::{DateTime, Utc};
14
15
16use std::collections::{HashMap};
17use std::pin::Pin;
18use std::sync::Arc;
19
20use anyhow::Context;
21use axum::routing::get;
22use log::{error, warn, info};
23use tokio::sync::RwLock;
24use tokio::task::JoinHandle;
25use tokio_util::sync::CancellationToken;
26use axum::http::{header, Method, HeaderValue};
27use tower_http::cors::CorsLayer;
28use utoipa::{Modify, OpenApi};
29use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
30use utoipa_axum::router::OpenApiRouter;
31use utoipa_swagger_ui::SwaggerUi;
32
33use bark::Wallet;
34use bark::onchain::OnchainWallet;
35use bark_json::web::CreateWalletRequest;
36
37type BoxFuture<T> =
38 Pin<Box<dyn Future<Output = T> + Send + 'static>>;
39
40pub type OnWalletCreate = dyn Fn(CreateWalletRequest)
41 -> BoxFuture<anyhow::Result<ServerWallet>> + Send + Sync;
42
43pub type OnWalletDelete = dyn Fn()
44 -> BoxFuture<anyhow::Result<()>> + Send + Sync;
45
46const CRATE_VERSION : &'static str = env!("CARGO_PKG_VERSION");
47
48const API_DESCRIPTION: &str = "\
50A simple REST API for barkd, a wallet daemon for integrating bitcoin payments into your app over HTTP. Supports self-custodial Lightning, Ark, and on-chain out of the box.
51
52barkd is a long-running daemon best suited for always-on or high-connectivity environments like nodes, servers, desktops, and point-of-sale terminals.
53
54All endpoints return JSON. Amounts are denominated in satoshis.";
55
56#[derive(OpenApi)]
57#[openapi(
58 paths(
59 ping,
60 ),
61 nest(
62 (path = "/api/v1/boards", api = api::v1::boards::BoardsApiDoc),
63 (path = "/api/v1/exits", api = api::v1::exits::ExitsApiDoc),
64 (path = "/api/v1/fees", api = api::v1::fees::FeesApiDoc),
65 (path = "/api/v1/history", api = api::v1::history::HistoryApiDoc),
66 (path = "/api/v1/lightning", api = api::v1::lightning::LightningApiDoc),
67 (path = "/api/v1/onchain", api = api::v1::onchain::OnchainApiDoc),
68 (path = "/api/v1/wallet", api = api::v1::wallet::WalletApiDoc),
69 (path = "/api/v1/bitcoin", api = api::v1::bitcoin::BitcoinApiDoc),
70 (path = "/api/v1/notifications", api = api::v1::notifications::NotificationApiDoc),
71 ),
72 info(
73 title = "barkd REST API",
74 version = CRATE_VERSION,
75 description = API_DESCRIPTION,
76 ),
77 security(
78 ("bearer" = []),
79 ),
80 modifiers(&BearerSecurity),
81)]
82pub struct ApiDoc;
83
84struct BearerSecurity;
85
86impl Modify for BearerSecurity {
87 fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
88 let components = openapi.components.get_or_insert_with(Default::default);
89 components.add_security_scheme(
90 "bearer",
91 SecurityScheme::Http(
92 HttpBuilder::new()
93 .scheme(HttpAuthScheme::Bearer)
94 .bearer_format("AuthToken")
95 .description(Some("Base64url-encoded auth token"))
96 .build(),
97 ),
98 );
99 }
100}
101
102fn cors_layer(config: &Config) -> CorsLayer {
103 if config.allowed_origins.is_empty() {
104 return CorsLayer::new(); }
106
107 let origins: Vec<HeaderValue> = config.allowed_origins.iter()
109 .map(|o| o.parse().expect("pre-validated"))
110 .collect();
111
112 CorsLayer::new()
113 .allow_origin(origins)
114 .allow_methods([Method::GET, Method::POST, Method::DELETE])
115 .allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION])
116}
117
118async fn shutdown_signal(shutdown: CancellationToken) {
119 shutdown.cancelled().await;
120}
121
122pub struct RestServer {
124 shutdown: CancellationToken,
125 jh: JoinHandle<()>,
126}
127
128pub struct ServerWallet {
131 pub wallet: Wallet,
132 pub onchain: Arc<RwLock<OnchainWallet>>,
133}
134
135impl ServerWallet {
136 pub fn new(wallet: Wallet, onchain: Arc<RwLock<OnchainWallet>>) -> Self {
137 Self { wallet, onchain }
138 }
139}
140
141#[derive(Clone)]
142pub struct ServerState {
143 wallet: Arc<parking_lot::RwLock<Option<ServerWallet>>>,
144 auth_token: Option<AuthToken>,
145
146 on_wallet_create: Option<Arc<OnWalletCreate>>,
149 on_wallet_delete: Option<Arc<OnWalletDelete>>,
152
153 websocket_tickets: Arc<RwLock<HashMap<String, DateTime<Utc>>>>,
158}
159
160impl ServerState {
161 pub fn new(
162 wallet: Option<ServerWallet>,
163 auth_token: Option<AuthToken>,
164 on_wallet_create: Option<Arc<OnWalletCreate>>,
165 on_wallet_delete: Option<Arc<OnWalletDelete>>,
166 ) -> Self {
167 ServerState {
168 wallet: Arc::new(parking_lot::RwLock::new(wallet)),
169 on_wallet_create,
170 auth_token,
171 on_wallet_delete,
172
173 websocket_tickets: Arc::new(RwLock::new(HashMap::new())),
174 }
175 }
176
177 pub fn require_wallet(&self) -> anyhow::Result<Wallet> {
178 let wallet = self.wallet.read().as_ref()
179 .ok_or_else(|| anyhow!("No wallet set"))?.wallet.clone();
180 Ok(wallet)
181 }
182
183 pub fn require_onchain(&self) -> anyhow::Result<Arc<RwLock<OnchainWallet>>> {
184 let onchain = self.wallet.read().as_ref()
185 .ok_or_else(|| anyhow!("No onchain set"))?.onchain.clone();
186 Ok(onchain)
187 }
188
189 pub fn auth_token(&self) -> Option<&AuthToken> {
190 self.auth_token.as_ref()
191 }
192}
193
194impl RestServer {
195 pub async fn start(
201 config: &Config,
202 auth_token: Option<AuthToken>,
203 wallet: Option<ServerWallet>,
204 on_wallet_create: Option<Arc<OnWalletCreate>>,
205 on_wallet_delete: Option<Arc<OnWalletDelete>>,
206 ) -> anyhow::Result<Self> {
207 let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
208 .split_for_parts();
209
210 let socket_addr = config.socket_addr();
211
212 if auth_token.is_none() {
213 warn!("No auth token configured — all authentication is disabled");
214 }
215
216 let state = ServerState::new(wallet, auth_token, on_wallet_create, on_wallet_delete);
217
218 let router = router
219 .route("/ping", get(ping))
220 .nest("/api/v1", api::v1::router(&state))
221 .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", api.clone()))
222 .layer(cors_layer(config))
223 .with_state(state)
224 .fallback(error::route_not_found);
225
226 log::info!("Server starting on http://{}", socket_addr);
228
229 let listener = tokio::net::TcpListener::bind(socket_addr).await
230 .context("Failed to bind to address")?;
231
232 let shutdown = CancellationToken::new();
233
234 let shutdown2 = shutdown.clone();
235 let jh = tokio::spawn(async move {
236 if let Err(e) = axum::serve(listener, router.into_make_service())
237 .with_graceful_shutdown(shutdown_signal(shutdown2)).await
238 {
239 error!("Error running server: {:#}", e);
240 } else {
241 info!("Server stopped running");
242 }
243 });
244
245 Ok(RestServer { shutdown, jh })
246 }
247
248 pub fn stop(&self) {
250 self.shutdown.cancel();
251 }
252
253 pub async fn stop_wait(self) -> anyhow::Result<()> {
255 self.stop();
256 self.jh.await?;
257 Ok(())
258 }
259}
260
261#[utoipa::path(
262 get,
263 path = "/ping",
264 summary = "Ping",
265 security(()),
266 extensions(
267 ("x-hidden" = json!(true))
268 ),
269 responses(
270 (status = 200, description = "Returns pong")
271 )
272)]
273pub async fn ping() -> &'static str { "pong" }