1use std::collections::BTreeMap;
2use std::time::UNIX_EPOCH;
3use std::{ffi, iter};
4
5use anyhow::{Context as _, bail};
6use clap::{Parser, Subcommand};
7use fedimint_core::Amount;
8use fedimint_core::core::OperationId;
9use fedimint_core::secp256k1::PublicKey;
10use fedimint_core::util::SafeUrl;
11use futures::StreamExt;
12use lightning_invoice::{Bolt11InvoiceDescription, Description};
13use serde::{Deserialize, Serialize};
14use serde_json::json;
15use tracing::{debug, info};
16
17use crate::recurring::{PaymentCodeRootKey, RecurringPaymentProtocol};
18use crate::{
19 LightningOperationMeta, LightningOperationMetaVariant, LnReceiveState, OutgoingLightningPayment,
20};
21
22#[derive(Parser, Serialize)]
23enum Opts {
24 Invoice {
26 amount: Amount,
27 #[clap(long, default_value = "")]
28 description: String,
29 #[clap(long)]
30 expiry_time: Option<u64>,
31 #[clap(long)]
32 gateway_id: Option<PublicKey>,
33 #[clap(long, default_value = "false")]
34 force_internal: bool,
35 },
36 Pay {
38 payment_info: String,
40 #[clap(long)]
42 amount: Option<Amount>,
43 #[clap(long)]
45 lnurl_comment: Option<String>,
46 #[clap(long)]
47 gateway_id: Option<PublicKey>,
48 #[clap(long, default_value = "false")]
49 force_internal: bool,
50 },
51 #[clap(subcommand)]
53 Lnurl(LnurlCommands),
54}
55
56#[derive(Subcommand, Serialize)]
57enum LnurlCommands {
58 Register {
60 server_url: SafeUrl,
62 #[clap(long)]
64 meta: Option<String>,
65 #[clap(long, default_value = "Fedimint LNURL Pay")]
67 description: String,
68 },
69 List,
71 Invoices { payment_code_idx: u64 },
73 InvoiceDetails { operation_id: OperationId },
75 AwaitInvoicePaid {
77 operation_id: OperationId,
79 },
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub struct LnInvoiceResponse {
85 pub operation_id: OperationId,
86 pub invoice: String,
87}
88
89pub(crate) async fn handle_cli_command(
90 module: &super::LightningClientModule,
91 args: &[ffi::OsString],
92) -> anyhow::Result<serde_json::Value> {
93 let opts = Opts::parse_from(iter::once(&ffi::OsString::from("meta")).chain(args.iter()));
94
95 Ok(match opts {
96 Opts::Invoice {
97 amount,
98 description,
99 expiry_time,
100 gateway_id,
101 force_internal,
102 } => {
103 let ln_gateway = module.get_gateway(gateway_id, force_internal).await?;
104
105 let desc = Description::new(description)?;
106 let (operation_id, invoice, _) = module
107 .create_bolt11_invoice(
108 amount,
109 Bolt11InvoiceDescription::Direct(desc),
110 expiry_time,
111 (),
112 ln_gateway,
113 )
114 .await?;
115 serde_json::to_value(LnInvoiceResponse {
116 operation_id,
117 invoice: invoice.to_string(),
118 })
119 .expect("Can't fail")
120 }
121 Opts::Pay {
122 payment_info,
123 amount,
124 lnurl_comment,
125 gateway_id,
126 force_internal,
127 } => {
128 let bolt11 = crate::get_invoice(&payment_info, amount, lnurl_comment).await?;
129 info!("Paying invoice: {bolt11}");
130 let ln_gateway = module.get_gateway(gateway_id, force_internal).await?;
131
132 let OutgoingLightningPayment {
133 payment_type,
134 contract_id: _,
135 fee,
136 } = module.pay_bolt11_invoice(ln_gateway, bolt11, ()).await?;
137 let operation_id = payment_type.operation_id();
138 info!(
139 "Gateway fee: {fee}, payment operation id: {}",
140 operation_id.fmt_short()
141 );
142 let outcome = module.await_outgoing_payment(operation_id).await?;
143 serde_json::to_value(outcome).expect("cant fail")
144 }
145 Opts::Lnurl(LnurlCommands::Register {
146 server_url,
147 meta,
148 description,
149 }) => {
150 let meta = meta.unwrap_or_else(|| {
151 serde_json::to_string(&json!([["text/plain", description]]))
152 .expect("serialization can't fail")
153 });
154 let recurring_payment_code = module
155 .register_recurring_payment_code(RecurringPaymentProtocol::LNURL, server_url, &meta)
156 .await?;
157 json!({
158 "lnurl": recurring_payment_code.code,
159 })
160 }
161 Opts::Lnurl(LnurlCommands::List) => {
162 let codes: BTreeMap<u64, serde_json::Value> = module
163 .list_recurring_payment_codes()
164 .await
165 .into_iter()
166 .map(|(idx, code)| {
167 let root_public_key = PaymentCodeRootKey(code.root_keypair.public_key());
168 let recurring_payment_code_id = root_public_key.to_payment_code_id();
169 let creation_timestamp = code
170 .creation_time
171 .duration_since(UNIX_EPOCH)
172 .expect("Time went backwards")
173 .as_secs();
174 let code_json = json!({
175 "lnurl": code.code,
176 "creation_timestamp": creation_timestamp,
178 "root_public_key": root_public_key,
179 "recurring_payment_code_id": recurring_payment_code_id,
180 "recurringd_api": code.recurringd_api,
181 "last_derivation_index": code.last_derivation_index,
182 });
183 (idx, code_json)
184 })
185 .collect();
186
187 json!({
188 "codes": codes,
189 })
190 }
191 Opts::Lnurl(LnurlCommands::Invoices { payment_code_idx }) => {
192 let invoices = module
194 .list_recurring_payment_code_invoices(payment_code_idx)
195 .await
196 .context("Unknown payment code index")?
197 .into_iter()
198 .map(|(idx, operation_id)| {
199 let invoice = json!({
200 "operation_id": operation_id,
201 });
202 (idx, invoice)
203 })
204 .collect::<BTreeMap<_, _>>();
205 json!({
206 "invoices": invoices,
207 })
208 }
209 Opts::Lnurl(LnurlCommands::InvoiceDetails { operation_id }) => {
210 let LightningOperationMetaVariant::RecurringPaymentReceive(operation_meta) = module
211 .client_ctx
212 .get_operation(operation_id)
213 .await?
214 .meta::<LightningOperationMeta>()
215 .variant
216 else {
217 bail!("Operation is not a recurring lightning receive");
218 };
219
220 json!({
221 "payment_code_id": operation_meta.payment_code_id,
222 "invoice": operation_meta.invoice,
223 "amount_msat": operation_meta.invoice.amount_milli_satoshis(),
224 })
225 }
226 Opts::Lnurl(LnurlCommands::AwaitInvoicePaid { operation_id }) => {
227 let LightningOperationMetaVariant::RecurringPaymentReceive(operation_meta) = module
228 .client_ctx
229 .get_operation(operation_id)
230 .await?
231 .meta::<LightningOperationMeta>()
232 .variant
233 else {
234 bail!("Operation is not a recurring lightning receive")
235 };
236 let mut stream = module
237 .subscribe_ln_recurring_receive(operation_id)
238 .await?
239 .into_stream();
240 while let Some(update) = stream.next().await {
241 debug!(?update, "Await invoice state update");
242 match update {
243 LnReceiveState::Claimed => {
244 let amount_msat = operation_meta.invoice.amount_milli_satoshis();
245 return Ok(json!({
246 "payment_code_id": operation_meta.payment_code_id,
247 "invoice": operation_meta.invoice,
248 "amount_msat": amount_msat,
249 }));
250 }
251 LnReceiveState::Canceled { reason } => {
252 return Err(reason.into());
253 }
254 _ => {}
255 }
256 }
257 unreachable!("Stream should not end without an outcome");
258 }
259 })
260}