| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- use axum::body::Body;
- use axum::extract::State;
- use axum::http::StatusCode;
- use axum::response::{Html, Response};
- use axum::Form;
- use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description};
- use maud::html;
- use serde::Deserialize;
- use crate::web::handlers::utils::{deserialize_optional_f64, deserialize_optional_u32};
- use crate::web::handlers::AppState;
- use crate::web::templates::{
- error_message, form_card, format_sats_as_btc, info_card, layout, success_message,
- };
- #[derive(Deserialize)]
- pub struct CreateBolt11Form {
- amount_btc: u64,
- description: Option<String>,
- #[serde(deserialize_with = "deserialize_optional_u32")]
- expiry_seconds: Option<u32>,
- }
- #[derive(Deserialize)]
- pub struct CreateBolt12Form {
- #[serde(deserialize_with = "deserialize_optional_f64")]
- amount_btc: Option<f64>,
- description: Option<String>,
- #[serde(deserialize_with = "deserialize_optional_u32")]
- expiry_seconds: Option<u32>,
- }
- pub async fn invoices_page(State(_state): State<AppState>) -> Result<Html<String>, StatusCode> {
- let content = html! {
- h2 style="text-align: center; margin-bottom: 3rem;" { "Invoices" }
- div class="grid" {
- (form_card(
- "Create BOLT11 Invoice",
- html! {
- form method="post" action="/invoices/bolt11" {
- div class="form-group" {
- label for="amount_btc" { "Amount" }
- input type="number" id="amount_btc" name="amount_btc" required placeholder="₿0" step="0.00000001" {}
- }
- div class="form-group" {
- label for="description" { "Description (optional)" }
- input type="text" id="description" name="description" placeholder="Payment for..." {}
- }
- div class="form-group" {
- label for="expiry_seconds" { "Expiry (seconds, optional)" }
- input type="number" id="expiry_seconds" name="expiry_seconds" placeholder="3600" {}
- }
- button type="submit" { "Create BOLT11 Invoice" }
- }
- }
- ))
- (form_card(
- "Create BOLT12 Offer",
- html! {
- form method="post" action="/invoices/bolt12" {
- div class="form-group" {
- label for="amount_btc" { "Amount (optional for variable amount)" }
- input type="number" id="amount_btc" name="amount_btc" placeholder="₿0" step="0.00000001" {}
- }
- div class="form-group" {
- label for="description" { "Description (optional)" }
- input type="text" id="description" name="description" placeholder="Payment for..." {}
- }
- div class="form-group" {
- label for="expiry_seconds" { "Expiry (seconds, optional)" }
- input type="number" id="expiry_seconds" name="expiry_seconds" placeholder="3600" {}
- }
- button type="submit" { "Create BOLT12 Offer" }
- }
- }
- ))
- }
- };
- Ok(Html(layout("Create Invoices", content).into_string()))
- }
- pub async fn post_create_bolt11(
- State(state): State<AppState>,
- Form(form): Form<CreateBolt11Form>,
- ) -> Result<Response, StatusCode> {
- tracing::info!(
- "Web interface: Creating BOLT11 invoice for amount={} sats, description={:?}, expiry={}s",
- form.amount_btc,
- form.description,
- form.expiry_seconds.unwrap_or(3600)
- );
- // Handle optional description
- let description_text = form.description.clone().unwrap_or_else(|| "".to_string());
- let description = if description_text.is_empty() {
- // Use empty description for empty or missing description
- match Description::new("".to_string()) {
- Ok(desc) => Bolt11InvoiceDescription::Direct(desc),
- Err(_) => {
- // Fallback to a minimal valid description
- let desc = Description::new(" ".to_string()).unwrap();
- Bolt11InvoiceDescription::Direct(desc)
- }
- }
- } else {
- match Description::new(description_text.clone()) {
- Ok(desc) => Bolt11InvoiceDescription::Direct(desc),
- Err(e) => {
- tracing::warn!(
- "Web interface: Invalid description for BOLT11 invoice: {}",
- e
- );
- let content = html! {
- (error_message(&format!("Invalid description: {e}")))
- div class="card" {
- a href="/invoices" { button { "← Try Again" } }
- }
- };
- return Ok(Response::builder()
- .status(StatusCode::BAD_REQUEST)
- .header("content-type", "text/html")
- .body(Body::from(
- layout("Create Invoice Error", content).into_string(),
- ))
- .unwrap());
- }
- }
- };
- // Convert Bitcoin to millisatoshis
- let amount_msats = form.amount_btc * 1_000;
- let expiry_seconds = form.expiry_seconds.unwrap_or(3600);
- let invoice_result =
- state
- .node
- .inner
- .bolt11_payment()
- .receive(amount_msats, &description, expiry_seconds);
- let content = match invoice_result {
- Ok(invoice) => {
- tracing::info!(
- "Web interface: Successfully created BOLT11 invoice with payment_hash={}",
- invoice.payment_hash()
- );
- let current_time = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs();
- let description_display = if description_text.is_empty() {
- "None".to_string()
- } else {
- description_text.clone()
- };
- html! {
- (success_message("BOLT11 Invoice created successfully!"))
- (info_card(
- "Invoice Details",
- vec![
- ("Payment Hash", invoice.payment_hash().to_string()),
- ("Amount", format_sats_as_btc(form.amount_btc)),
- ("Description", description_display),
- ("Expires At", format!("{}", current_time + expiry_seconds as u64)),
- ]
- ))
- div class="card" {
- h3 { "Invoice (copy this to share)" }
- textarea readonly style="width: 100%; height: 150px; font-family: monospace; font-size: 0.8rem;" {
- (invoice.to_string())
- }
- }
- div class="card" {
- a href="/invoices" { button { "← Create Another Invoice" } }
- }
- }
- }
- Err(e) => {
- tracing::error!("Web interface: Failed to create BOLT11 invoice: {}", e);
- html! {
- (error_message(&format!("Failed to create invoice: {e}")))
- div class="card" {
- a href="/invoices" { button { "← Try Again" } }
- }
- }
- }
- };
- Ok(Response::builder()
- .header("content-type", "text/html")
- .body(Body::from(
- layout("BOLT11 Invoice Created", content).into_string(),
- ))
- .unwrap())
- }
- pub async fn post_create_bolt12(
- State(state): State<AppState>,
- Form(form): Form<CreateBolt12Form>,
- ) -> Result<Response, StatusCode> {
- let expiry_seconds = form.expiry_seconds.unwrap_or(3600);
- let description_text = form.description.unwrap_or_else(|| "".to_string());
- tracing::info!(
- "Web interface: Creating BOLT12 offer for amount={:?} btc, description={:?}, expiry={}s",
- form.amount_btc,
- description_text,
- expiry_seconds
- );
- let offer_result = if let Some(amount_btc) = form.amount_btc {
- // Convert Bitcoin to millisatoshis (1 BTC = 100,000,000,000 msats)
- let amount_msats = (amount_btc * 100_000_000_000.0) as u64;
- state.node.inner.bolt12_payment().receive(
- amount_msats,
- &description_text,
- Some(expiry_seconds),
- None,
- )
- } else {
- state
- .node
- .inner
- .bolt12_payment()
- .receive_variable_amount(&description_text, Some(expiry_seconds))
- };
- let content = match offer_result {
- Ok(offer) => {
- tracing::info!(
- "Web interface: Successfully created BOLT12 offer with offer_id={}",
- offer.id()
- );
- let current_time = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs();
- let amount_display = form
- .amount_btc
- .map(|a| format_sats_as_btc((a * 100_000_000.0) as u64))
- .unwrap_or_else(|| "Variable amount".to_string());
- let description_display = if description_text.is_empty() {
- "None".to_string()
- } else {
- description_text
- };
- html! {
- (success_message("BOLT12 Offer created successfully!"))
- (info_card(
- "Offer Details",
- vec![
- ("Offer ID", offer.id().to_string()),
- ("Amount", amount_display),
- ("Description", description_display),
- ("Expires At", format!("{}", current_time + expiry_seconds as u64)),
- ]
- ))
- div class="card" {
- h3 { "Offer (copy this to share)" }
- textarea readonly style="width: 100%; height: 150px; font-family: monospace; font-size: 0.8rem;" {
- (offer.to_string())
- }
- }
- div class="card" {
- a href="/invoices" { button { "← Create Another Offer" } }
- }
- }
- }
- Err(e) => {
- tracing::error!("Web interface: Failed to create BOLT12 offer: {}", e);
- html! {
- (error_message(&format!("Failed to create offer: {e}")))
- div class="card" {
- a href="/invoices" { button { "← Try Again" } }
- }
- }
- }
- };
- Ok(Response::builder()
- .header("content-type", "text/html")
- .body(Body::from(
- layout("BOLT12 Offer Created", content).into_string(),
- ))
- .unwrap())
- }
|