use std::str::FromStr;
use axum::extract::{Path, State};
use axum::routing::{get, post};
use axum::{debug_handler, Json, Router};
use bitcoin::Amount;
use anyhow::Context;
use utoipa::OpenApi;
use ark::lightning::Offer;
use bark::lightning_invoice::Bolt11Invoice;
use bark::lnurllib::lightning_address::LightningAddress;
use crate::error::{self, badarg, not_found, ContextExt, HandlerResult};
use crate::ServerState;
#[derive(OpenApi)]
#[openapi(
paths(
generate_invoice,
get_receive_status,
list_receive_statuses,
cancel_receive,
pay,
),
components(schemas(
bark_json::web::LightningInvoiceRequest,
bark_json::cli::InvoiceInfo,
bark_json::cli::LightningReceiveInfo,
bark_json::web::LightningPayRequest,
bark_json::web::LightningPayResponse,
)),
tags((name = "lightning", description = "Create Lightning invoices and track receives."))
)]
pub struct LightningApiDoc;
pub fn router() -> Router<ServerState> {
Router::new()
.route("/receives/invoice", post(generate_invoice))
.route("/receives/{identifier}", get(get_receive_status).delete(cancel_receive))
.route("/receives", get(list_receive_statuses))
.route("/pay", post(pay))
}
#[utoipa::path(
post,
path = "/receives/invoice",
summary = "Create a BOLT11 invoice",
request_body = bark_json::web::LightningInvoiceRequest,
responses(
(status = 200, description = "Returns the created invoice", body = bark_json::cli::InvoiceInfo),
(status = 500, description = "Internal server error", body = error::InternalServerError)
),
description = "Generates a new BOLT11 invoice for the specified amount via the Ark server, \
creating a pending Lightning receive.",
tag = "lightning"
)]
#[debug_handler]
pub async fn generate_invoice(
State(state): State<ServerState>,
Json(body): Json<bark_json::web::LightningInvoiceRequest>,
) -> HandlerResult<Json<bark_json::cli::InvoiceInfo>> {
let wallet = state.require_wallet()?;
let amount = Amount::from_sat(body.amount_sat);
let invoice = wallet.bolt11_invoice(amount, body.description).await
.context("Failed to create invoice")?;
Ok(axum::Json(bark_json::cli::InvoiceInfo {
invoice: invoice.to_string(),
}))
}
#[utoipa::path(
get,
path = "/receives/{identifier}",
summary = "Get receive status",
params(
("identifier" = String, Path, description = "Payment hash, invoice string or preimage to search for"),
),
responses(
(status = 200, description = "Returns the Lightning receive status", body = bark_json::cli::LightningReceiveInfo),
(status = 400, description = "Bad request", body = error::BadRequestError),
(status = 404, description = "Not found", body = error::NotFoundError),
(status = 500, description = "Internal server error", body = error::InternalServerError)
),
description = "Returns the status of a specified Lightning receive, identified by its \
payment hash, invoice string, or preimage. The response tracks progress through \
timestamps: `htlc_vtxos` is populated once HTLCs are created by the Ark server, \
`preimage_revealed_at` records when the preimage was sent, and `finished_at` \
indicates the receive has settled or been canceled.",
tag = "lightning"
)]
#[debug_handler]
pub async fn get_receive_status(
State(state): State<ServerState>,
Path(identifier): Path<String>,
) -> HandlerResult<Json<bark_json::cli::LightningReceiveInfo>> {
let wallet = state.require_wallet()?;
let payment_hash = if let Ok(h) = ark::lightning::PaymentHash::from_str(&identifier) {
h
} else if let Ok(i) = Bolt11Invoice::from_str(&identifier) {
i.into()
} else if let Ok(p) = ark::lightning::Preimage::from_str(&identifier) {
p.into()
} else {
badarg!("identifier is not a valid payment hash, invoice or preimage");
};
if let Some(status) = wallet.lightning_receive_status(payment_hash).await
.context("Failed to get lightning receive status")? {
Ok(axum::Json(status.into()))
} else {
not_found!([payment_hash], "No invoice found");
}
}
#[utoipa::path(
get,
path = "/receives",
summary = "List all pending receive statuses",
responses(
(status = 200, description = "Returns all pending receive statuses", body = Vec<bark_json::cli::LightningReceiveInfo>),
(status = 500, description = "Internal server error", body = error::InternalServerError)
),
description = "Returns the statuses of all pending Lightning receives, ordered from oldest \
to newest. A receive is pending until its `finished_at` timestamp is set, indicating \
it has settled or been canceled.",
tag = "lightning"
)]
#[debug_handler]
pub async fn list_receive_statuses(
State(state): State<ServerState>,
) -> HandlerResult<Json<Vec<bark_json::cli::LightningReceiveInfo>>> {
let wallet = state.require_wallet()?;
let mut receives = wallet.pending_lightning_receives().await
.context("Failed to get lightning receives")?;
receives.reverse();
let receives = receives.into_iter()
.map(bark_json::cli::LightningReceiveInfo::from).collect::<Vec<_>>();
Ok(axum::Json(receives))
}
#[utoipa::path(
delete,
path = "/receives/{identifier}",
summary = "Cancel a pending receive",
params(
("identifier" = String, Path, description = "Payment hash or invoice string"),
),
responses(
(status = 200, description = "Receive canceled successfully"),
(status = 400, description = "Bad request", body = error::BadRequestError),
(status = 404, description = "Not found", body = error::NotFoundError),
(status = 500, description = "Internal server error", body = error::InternalServerError)
),
description = "Cancels a pending Lightning receive identified by its payment hash or \
invoice string. The server will refuse cancellation if HTLC-recv VTXOs have already \
been granted. Bark also prevents cancellation when the preimage has been revealed.",
tag = "lightning"
)]
#[debug_handler]
pub async fn cancel_receive(
State(state): State<ServerState>,
Path(identifier): Path<String>,
) -> HandlerResult<()> {
let wallet = state.require_wallet()?;
let payment_hash = if let Ok(h) = ark::lightning::PaymentHash::from_str(&identifier) {
h
} else if let Ok(i) = Bolt11Invoice::from_str(&identifier) {
i.into()
} else {
badarg!("identifier is not a valid payment hash or invoice");
};
wallet.cancel_lightning_receive(payment_hash).await
.context("Failed to cancel lightning receive")?;
Ok(())
}
#[utoipa::path(
post,
path = "/pay",
summary = "Send a Lightning payment",
request_body = bark_json::web::LightningPayRequest,
responses(
(status = 200, description = "Returns success message, optionally with \
preimage if payment was immediately settled", body = bark_json::web::LightningPayResponse),
(status = 400, description = "The provided destination is not a valid \
BOLT11 invoice, BOLT12 offer, or Lightning address", body = error::BadRequestError),
(status = 500, description = "Internal server error", body = error::InternalServerError)
),
description = "Sends a payment to a Lightning destination. Accepts a BOLT11 invoice, \
BOLT12 offer, or Lightning address. The `amount_sat` field is required for Lightning \
addresses but optional for invoices and offers. Comments are only supported for \
Lightning addresses.",
tag = "lightning"
)]
#[debug_handler]
pub async fn pay(
State(state): State<ServerState>,
Json(body): Json<bark_json::web::LightningPayRequest>,
) -> HandlerResult<Json<bark_json::web::LightningPayResponse>> {
let wallet = state.require_wallet()?;
let amount = body.amount_sat.map(|a| Amount::from_sat(a));
if let Ok(invoice) = Bolt11Invoice::from_str(&body.destination) {
if body.comment.is_some() {
badarg!("comment is not supported for BOLT-11 invoices");
}
wallet.pay_lightning_invoice(invoice, amount, false).await?
} else if let Ok(offer) = Offer::from_str(&body.destination) {
if body.comment.is_some() {
badarg!("comment is not supported for BOLT-12 offers");
}
wallet.pay_lightning_offer(offer, amount, false).await?
} else if let Ok(lnaddr) = LightningAddress::from_str(&body.destination) {
let amount = amount.badarg("amount is required for Lightning addresses")?;
wallet.pay_lightning_address(&lnaddr, amount, body.comment, false).await?
} else {
badarg!("argument is not a valid BOLT-11 invoice, BOLT-12 offer or Lightning address");
};
Ok(axum::Json(bark_json::web::LightningPayResponse {
message: "Payment initiated successfully".to_string(),
}))
}