use std::collections::BTreeMap;
use std::time::UNIX_EPOCH;
use std::{ffi, iter};
use anyhow::{Context as _, bail};
use clap::{Parser, Subcommand};
use fedimint_core::Amount;
use fedimint_core::core::OperationId;
use fedimint_core::secp256k1::PublicKey;
use fedimint_core::util::SafeUrl;
use futures::StreamExt;
use lightning_invoice::{Bolt11InvoiceDescription, Description};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{debug, info};
use crate::recurring::{PaymentCodeRootKey, RecurringPaymentProtocol};
use crate::{
LightningOperationMeta, LightningOperationMetaVariant, LnReceiveState, OutgoingLightningPayment,
};
#[derive(Parser, Serialize)]
enum Opts {
Invoice {
amount: Amount,
#[clap(long, default_value = "")]
description: String,
#[clap(long)]
expiry_time: Option<u64>,
#[clap(long)]
gateway_id: Option<PublicKey>,
#[clap(long, default_value = "false")]
force_internal: bool,
},
Pay {
payment_info: String,
#[clap(long)]
amount: Option<Amount>,
#[clap(long)]
lnurl_comment: Option<String>,
#[clap(long)]
gateway_id: Option<PublicKey>,
#[clap(long, default_value = "false")]
force_internal: bool,
},
AwaitInvoice {
operation_id: OperationId,
},
AwaitPay {
operation_id: OperationId,
},
ListGateways {
#[clap(long, default_value = "false")]
no_update: bool,
},
#[clap(subcommand)]
Lnurl(LnurlCommands),
}
#[derive(Subcommand, Serialize)]
enum LnurlCommands {
Register {
server_url: SafeUrl,
#[clap(long)]
meta: Option<String>,
#[clap(long, default_value = "Fedimint LNURL Pay")]
description: String,
},
List,
Invoices { payment_code_idx: u64 },
InvoiceDetails { operation_id: OperationId },
AwaitInvoicePaid {
operation_id: OperationId,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct LnInvoiceResponse {
pub operation_id: OperationId,
pub invoice: String,
}
pub(crate) async fn handle_cli_command(
module: &super::LightningClientModule,
args: &[ffi::OsString],
) -> anyhow::Result<serde_json::Value> {
let opts = Opts::parse_from(iter::once(&ffi::OsString::from("meta")).chain(args.iter()));
Ok(match opts {
Opts::Invoice {
amount,
description,
expiry_time,
gateway_id,
force_internal,
} => {
let ln_gateway = module.get_gateway(gateway_id, force_internal).await?;
let desc = Description::new(description)?;
let (operation_id, invoice, _) = module
.create_bolt11_invoice(
amount,
Bolt11InvoiceDescription::Direct(desc),
expiry_time,
(),
ln_gateway,
)
.await?;
serde_json::to_value(LnInvoiceResponse {
operation_id,
invoice: invoice.to_string(),
})
.expect("Can't fail")
}
Opts::Pay {
payment_info,
amount,
lnurl_comment,
gateway_id,
force_internal,
} => {
let bolt11 = crate::get_invoice(&payment_info, amount, lnurl_comment).await?;
info!("Paying invoice: {bolt11}");
let ln_gateway = module.get_gateway(gateway_id, force_internal).await?;
let OutgoingLightningPayment {
payment_type,
contract_id: _,
fee,
} = module.pay_bolt11_invoice(ln_gateway, bolt11, ()).await?;
let operation_id = payment_type.operation_id();
info!(
"Gateway fee: {fee}, payment operation id: {}",
operation_id.fmt_short()
);
let outcome = module.await_outgoing_payment(operation_id).await?;
serde_json::to_value(outcome).expect("cant fail")
}
Opts::AwaitInvoice { operation_id } => {
let mut updates = module
.subscribe_ln_receive(operation_id)
.await?
.into_stream();
while let Some(update) = updates.next().await {
debug!(?update, "Await invoice state update");
match update {
LnReceiveState::Claimed => {
return Ok(json!({
"status": "paid"
}));
}
LnReceiveState::Canceled { reason } => {
return Err(reason.into());
}
_ => {}
}
}
unreachable!("Stream should not end without an outcome");
}
Opts::AwaitPay { operation_id } => {
let outcome = module.await_outgoing_payment(operation_id).await?;
serde_json::to_value(outcome).expect("serialization can't fail")
}
Opts::ListGateways { no_update } => {
if !no_update {
module.update_gateway_cache().await?;
}
let gateways = module.list_gateways().await;
if gateways.is_empty() {
return Ok(
serde_json::to_value(Vec::<String>::new()).expect("serialization can't fail")
);
}
json!(&gateways)
}
Opts::Lnurl(LnurlCommands::Register {
server_url,
meta,
description,
}) => {
let meta = meta.unwrap_or_else(|| {
serde_json::to_string(&json!([["text/plain", description]]))
.expect("serialization can't fail")
});
let recurring_payment_code = module
.register_recurring_payment_code(RecurringPaymentProtocol::LNURL, server_url, &meta)
.await?;
json!({
"lnurl": recurring_payment_code.code,
})
}
Opts::Lnurl(LnurlCommands::List) => {
let codes: BTreeMap<u64, serde_json::Value> = module
.list_recurring_payment_codes()
.await
.into_iter()
.map(|(idx, code)| {
let root_public_key = PaymentCodeRootKey(code.root_keypair.public_key());
let recurring_payment_code_id = root_public_key.to_payment_code_id();
let creation_timestamp = code
.creation_time
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
let code_json = json!({
"lnurl": code.code,
"creation_timestamp": creation_timestamp,
"root_public_key": root_public_key,
"recurring_payment_code_id": recurring_payment_code_id,
"recurringd_api": code.recurringd_api,
"last_derivation_index": code.last_derivation_index,
});
(idx, code_json)
})
.collect();
json!({
"codes": codes,
})
}
Opts::Lnurl(LnurlCommands::Invoices { payment_code_idx }) => {
let invoices = module
.list_recurring_payment_code_invoices(payment_code_idx)
.await
.context("Unknown payment code index")?
.into_iter()
.map(|(idx, operation_id)| {
let invoice = json!({
"operation_id": operation_id,
});
(idx, invoice)
})
.collect::<BTreeMap<_, _>>();
json!({
"invoices": invoices,
})
}
Opts::Lnurl(LnurlCommands::InvoiceDetails { operation_id }) => {
let LightningOperationMetaVariant::RecurringPaymentReceive(operation_meta) = module
.client_ctx
.get_operation(operation_id)
.await?
.meta::<LightningOperationMeta>()
.variant
else {
bail!("Operation is not a recurring lightning receive");
};
json!({
"payment_code_id": operation_meta.payment_code_id,
"invoice": operation_meta.invoice,
"amount_msat": operation_meta.invoice.amount_milli_satoshis(),
})
}
Opts::Lnurl(LnurlCommands::AwaitInvoicePaid { operation_id }) => {
let LightningOperationMetaVariant::RecurringPaymentReceive(operation_meta) = module
.client_ctx
.get_operation(operation_id)
.await?
.meta::<LightningOperationMeta>()
.variant
else {
bail!("Operation is not a recurring lightning receive")
};
let mut stream = module
.subscribe_ln_recurring_receive(operation_id)
.await?
.into_stream();
while let Some(update) = stream.next().await {
debug!(?update, "Await invoice state update");
match update {
LnReceiveState::Claimed => {
let amount_msat = operation_meta.invoice.amount_milli_satoshis();
return Ok(json!({
"payment_code_id": operation_meta.payment_code_id,
"invoice": operation_meta.invoice,
"amount_msat": amount_msat,
}));
}
LnReceiveState::Canceled { reason } => {
return Err(reason.into());
}
_ => {}
}
}
unreachable!("Stream should not end without an outcome");
}
})
}