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 AwaitInvoice {
53 operation_id: OperationId,
55 },
56 AwaitPay {
58 operation_id: OperationId,
60 },
61 ListGateways {
63 #[clap(long, default_value = "false")]
65 no_update: bool,
66 },
67 #[clap(subcommand)]
69 Lnurl(LnurlCommands),
70}
71
72#[derive(Subcommand, Serialize)]
73enum LnurlCommands {
74 Register {
76 server_url: SafeUrl,
78 #[clap(long)]
80 meta: Option<String>,
81 #[clap(long, default_value = "Fedimint LNURL Pay")]
83 description: String,
84 },
85 List,
87 Invoices { payment_code_idx: u64 },
89 InvoiceDetails { operation_id: OperationId },
91 AwaitInvoicePaid {
93 operation_id: OperationId,
95 },
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub struct LnInvoiceResponse {
101 pub operation_id: OperationId,
102 pub invoice: String,
103}
104
105pub(crate) async fn handle_cli_command(
106 module: &super::LightningClientModule,
107 args: &[ffi::OsString],
108) -> anyhow::Result<serde_json::Value> {
109 let opts = Opts::parse_from(iter::once(&ffi::OsString::from("meta")).chain(args.iter()));
110
111 Ok(match opts {
112 Opts::Invoice {
113 amount,
114 description,
115 expiry_time,
116 gateway_id,
117 force_internal,
118 } => {
119 let ln_gateway = module.get_gateway(gateway_id, force_internal).await?;
120
121 let desc = Description::new(description)?;
122 let (operation_id, invoice, _) = module
123 .create_bolt11_invoice(
124 amount,
125 Bolt11InvoiceDescription::Direct(desc),
126 expiry_time,
127 (),
128 ln_gateway,
129 )
130 .await?;
131 serde_json::to_value(LnInvoiceResponse {
132 operation_id,
133 invoice: invoice.to_string(),
134 })
135 .expect("Can't fail")
136 }
137 Opts::Pay {
138 payment_info,
139 amount,
140 lnurl_comment,
141 gateway_id,
142 force_internal,
143 } => {
144 let bolt11 = crate::get_invoice(&payment_info, amount, lnurl_comment).await?;
145 info!("Paying invoice: {bolt11}");
146 let ln_gateway = module.get_gateway(gateway_id, force_internal).await?;
147
148 let OutgoingLightningPayment {
149 payment_type,
150 contract_id: _,
151 fee,
152 } = module.pay_bolt11_invoice(ln_gateway, bolt11, ()).await?;
153 let operation_id = payment_type.operation_id();
154 info!(
155 "Gateway fee: {fee}, payment operation id: {}",
156 operation_id.fmt_short()
157 );
158 let outcome = module.await_outgoing_payment(operation_id).await?;
159 serde_json::to_value(outcome).expect("cant fail")
160 }
161 Opts::AwaitInvoice { operation_id } => {
162 let mut updates = module
163 .subscribe_ln_receive(operation_id)
164 .await?
165 .into_stream();
166 while let Some(update) = updates.next().await {
167 debug!(?update, "Await invoice state update");
168 match update {
169 LnReceiveState::Claimed => {
170 return Ok(json!({
171 "status": "paid"
172 }));
173 }
174 LnReceiveState::Canceled { reason } => {
175 return Err(reason.into());
176 }
177 _ => {}
178 }
179 }
180 unreachable!("Stream should not end without an outcome");
181 }
182 Opts::AwaitPay { operation_id } => {
183 let outcome = module.await_outgoing_payment(operation_id).await?;
184 serde_json::to_value(outcome).expect("serialization can't fail")
185 }
186 Opts::ListGateways { no_update } => {
187 if !no_update {
188 module.update_gateway_cache().await?;
189 }
190 let gateways = module.list_gateways().await;
191 if gateways.is_empty() {
192 return Ok(
193 serde_json::to_value(Vec::<String>::new()).expect("serialization can't fail")
194 );
195 }
196 json!(&gateways)
197 }
198 Opts::Lnurl(LnurlCommands::Register {
199 server_url,
200 meta,
201 description,
202 }) => {
203 let meta = meta.unwrap_or_else(|| {
204 serde_json::to_string(&json!([["text/plain", description]]))
205 .expect("serialization can't fail")
206 });
207 let recurring_payment_code = module
208 .register_recurring_payment_code(RecurringPaymentProtocol::LNURL, server_url, &meta)
209 .await?;
210 json!({
211 "lnurl": recurring_payment_code.code,
212 })
213 }
214 Opts::Lnurl(LnurlCommands::List) => {
215 let codes: BTreeMap<u64, serde_json::Value> = module
216 .list_recurring_payment_codes()
217 .await
218 .into_iter()
219 .map(|(idx, code)| {
220 let root_public_key = PaymentCodeRootKey(code.root_keypair.public_key());
221 let recurring_payment_code_id = root_public_key.to_payment_code_id();
222 let creation_timestamp = code
223 .creation_time
224 .duration_since(UNIX_EPOCH)
225 .expect("Time went backwards")
226 .as_secs();
227 let code_json = json!({
228 "lnurl": code.code,
229 "creation_timestamp": creation_timestamp,
231 "root_public_key": root_public_key,
232 "recurring_payment_code_id": recurring_payment_code_id,
233 "recurringd_api": code.recurringd_api,
234 "last_derivation_index": code.last_derivation_index,
235 });
236 (idx, code_json)
237 })
238 .collect();
239
240 json!({
241 "codes": codes,
242 })
243 }
244 Opts::Lnurl(LnurlCommands::Invoices { payment_code_idx }) => {
245 let invoices = module
247 .list_recurring_payment_code_invoices(payment_code_idx)
248 .await
249 .context("Unknown payment code index")?
250 .into_iter()
251 .map(|(idx, operation_id)| {
252 let invoice = json!({
253 "operation_id": operation_id,
254 });
255 (idx, invoice)
256 })
257 .collect::<BTreeMap<_, _>>();
258 json!({
259 "invoices": invoices,
260 })
261 }
262 Opts::Lnurl(LnurlCommands::InvoiceDetails { operation_id }) => {
263 let LightningOperationMetaVariant::RecurringPaymentReceive(operation_meta) = module
264 .client_ctx
265 .get_operation(operation_id)
266 .await?
267 .meta::<LightningOperationMeta>()
268 .variant
269 else {
270 bail!("Operation is not a recurring lightning receive");
271 };
272
273 json!({
274 "payment_code_id": operation_meta.payment_code_id,
275 "invoice": operation_meta.invoice,
276 "amount_msat": operation_meta.invoice.amount_milli_satoshis(),
277 })
278 }
279 Opts::Lnurl(LnurlCommands::AwaitInvoicePaid { operation_id }) => {
280 let LightningOperationMetaVariant::RecurringPaymentReceive(operation_meta) = module
281 .client_ctx
282 .get_operation(operation_id)
283 .await?
284 .meta::<LightningOperationMeta>()
285 .variant
286 else {
287 bail!("Operation is not a recurring lightning receive")
288 };
289 let mut stream = module
290 .subscribe_ln_recurring_receive(operation_id)
291 .await?
292 .into_stream();
293 while let Some(update) = stream.next().await {
294 debug!(?update, "Await invoice state update");
295 match update {
296 LnReceiveState::Claimed => {
297 let amount_msat = operation_meta.invoice.amount_milli_satoshis();
298 return Ok(json!({
299 "payment_code_id": operation_meta.payment_code_id,
300 "invoice": operation_meta.invoice,
301 "amount_msat": amount_msat,
302 }));
303 }
304 LnReceiveState::Canceled { reason } => {
305 return Err(reason.into());
306 }
307 _ => {}
308 }
309 }
310 unreachable!("Stream should not end without an outcome");
311 }
312 })
313}