Skip to main content

fedimint_ln_client/
cli.rs

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    /// Create a lightning invoice to receive payment via gateway
25    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 a lightning invoice or lnurl via a gateway
37    Pay {
38        /// Lightning invoice or lnurl
39        payment_info: String,
40        /// Amount to pay, used for lnurl
41        #[clap(long)]
42        amount: Option<Amount>,
43        /// Invoice comment/description, used on lnurl
44        #[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    /// Wait for incoming invoice to be paid
52    AwaitInvoice {
53        /// The operation ID of the invoice operation to await
54        operation_id: OperationId,
55    },
56    /// Wait for a lightning payment to complete
57    AwaitPay {
58        /// The operation ID of the payment operation to await
59        operation_id: OperationId,
60    },
61    /// List registered gateways
62    ListGateways {
63        /// Don't fetch the registered gateways from the federation
64        #[clap(long, default_value = "false")]
65        no_update: bool,
66    },
67    /// Register and manage LNURLs
68    #[clap(subcommand)]
69    Lnurl(LnurlCommands),
70}
71
72#[derive(Subcommand, Serialize)]
73enum LnurlCommands {
74    /// Register a new LNURL payment code with a specific LNURL server
75    Register {
76        /// The LNURL server to register with
77        server_url: SafeUrl,
78        /// Set LNURL meta data, see LUD-06 for more details on the format
79        #[clap(long)]
80        meta: Option<String>,
81        ///Shrthand for setting the short description in the LNURL meta data
82        #[clap(long, default_value = "Fedimint LNURL Pay")]
83        description: String,
84    },
85    /// List all LNURLs registered
86    List,
87    /// List all invoices and their operation ids generated for a LNURL
88    Invoices { payment_code_idx: u64 },
89    /// List details for a specific invoice by operation id
90    InvoiceDetails { operation_id: OperationId },
91    /// Await a LNURL-triggered lightning receive operation to complete
92    AwaitInvoicePaid {
93        /// The operation ID of the receive operation to await
94        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                        // TODO: use time_to_iso8601
230                        "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            // TODO: wait for background sync to complete
246            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}