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