Skip to main content

gateway_cli/
lightning_commands.rs

1use bitcoin::hashes::sha256;
2use chrono::{DateTime, Utc};
3use clap::Subcommand;
4use fedimint_connectors::error::ServerError;
5use fedimint_core::Amount;
6use fedimint_gateway_client::{
7    close_channels_with_peer, create_invoice_for_self, create_offer, get_invoice, list_channels,
8    list_transactions, open_channel, open_channel_with_push, pay_invoice, pay_offer,
9    set_channel_fees,
10};
11use fedimint_gateway_common::{
12    CloseChannelsWithPeerRequest, CreateInvoiceForOperatorPayload, CreateOfferPayload,
13    GetInvoiceRequest, ListTransactionsPayload, OpenChannelRequest, PayInvoiceForOperatorPayload,
14    PayOfferPayload, SetChannelFeesRequest,
15};
16use fedimint_ln_common::client::GatewayApi;
17use lightning_invoice::Bolt11Invoice;
18
19use crate::{CliOutput, CliOutputResult, SafeUrl};
20
21/// Lightning node management commands for opening/closing channels,
22/// paying/creating invoices, paying/creating offers, or listing transactions.
23#[derive(Subcommand)]
24pub enum LightningCommands {
25    /// Create an invoice to receive lightning funds to the gateway.
26    CreateInvoice {
27        amount_msats: u64,
28
29        #[clap(long)]
30        expiry_secs: Option<u32>,
31
32        #[clap(long)]
33        description: Option<String>,
34    },
35    /// Pay a lightning invoice as the gateway (i.e. no e-cash exchange).
36    PayInvoice { invoice: Bolt11Invoice },
37    /// Open a channel with another lightning node.
38    OpenChannel {
39        /// The public key of the node to open a channel with
40        #[clap(long)]
41        pubkey: bitcoin::secp256k1::PublicKey,
42
43        #[clap(long)]
44        host: String,
45
46        /// The amount to fund the channel with
47        #[clap(long)]
48        channel_size_sats: u64,
49
50        /// The amount to push to the other side of the channel
51        #[clap(long)]
52        push_amount_sats: Option<u64>,
53
54        /// Feerate (sat/vB) for the channel-opening on-chain transaction.
55        /// Not honored by all backends (e.g. LDK).
56        #[clap(long)]
57        fee_rate_sats_per_vbyte: Option<u64>,
58
59        /// Base routing fee (msat) advertised for the new channel.
60        #[clap(long)]
61        base_fee_msat: Option<u64>,
62
63        /// Proportional routing fee (parts per million) advertised for the
64        /// new channel.
65        #[clap(long)]
66        parts_per_million: Option<u64>,
67    },
68    /// Close all channels with a peer, claiming the funds to the lightning
69    /// node's on-chain wallet.
70    CloseChannelsWithPeer {
71        /// The public key of the node to close channels with
72        #[clap(long)]
73        pubkey: bitcoin::secp256k1::PublicKey,
74
75        /// Flag to specify if the channel should be force closed
76        #[clap(long)]
77        force: bool,
78
79        /// Fee rate to use when closing the channel (required unless --force is
80        /// set)
81        #[clap(long, required_unless_present = "force")]
82        sats_per_vbyte: Option<u64>,
83    },
84    /// List channels.
85    ListChannels,
86    /// Update the local-side routing fees advertised on an existing channel.
87    SetChannelFees {
88        /// Funding outpoint of the channel, in `<txid>:<vout>` form
89        #[clap(long)]
90        funding_outpoint: bitcoin::OutPoint,
91
92        /// New base fee in millisatoshis
93        #[clap(long)]
94        base_fee_msat: u64,
95
96        /// New proportional fee in parts per million
97        #[clap(long)]
98        parts_per_million: u64,
99    },
100    /// List the Lightning transactions that the Lightning node has received and
101    /// sent
102    ListTransactions {
103        /// The timestamp to start listing transactions from (e.g.,
104        /// "2025-03-14T15:30:00Z")
105        #[arg(long, value_parser = parse_datetime)]
106        start_time: DateTime<Utc>,
107
108        /// The timestamp to end listing transactions from (e.g.,
109        /// "2025-03-15T15:30:00Z")
110        #[arg(long, value_parser = parse_datetime)]
111        end_time: DateTime<Utc>,
112    },
113    /// Get details about a specific invoice
114    GetInvoice {
115        /// The payment hash of the invoice
116        #[clap(long)]
117        payment_hash: sha256::Hash,
118    },
119    CreateOffer {
120        #[clap(long)]
121        amount_msat: Option<u64>,
122
123        #[clap(long)]
124        description: Option<String>,
125
126        #[clap(long)]
127        expiry_secs: Option<u32>,
128
129        #[clap(long)]
130        quantity: Option<u64>,
131    },
132    PayOffer {
133        #[clap(long)]
134        offer: String,
135
136        #[clap(long)]
137        amount_msat: Option<u64>,
138
139        #[clap(long)]
140        quantity: Option<u64>,
141
142        #[clap(long)]
143        payer_note: Option<String>,
144    },
145}
146
147fn parse_datetime(s: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
148    s.parse::<DateTime<Utc>>()
149}
150
151impl LightningCommands {
152    #![allow(clippy::too_many_lines)]
153    pub async fn handle(self, client: &GatewayApi, base_url: &SafeUrl) -> CliOutputResult {
154        match self {
155            Self::CreateInvoice {
156                amount_msats,
157                expiry_secs,
158                description,
159            } => {
160                let response = create_invoice_for_self(
161                    client,
162                    base_url,
163                    CreateInvoiceForOperatorPayload {
164                        amount_msats,
165                        expiry_secs,
166                        description,
167                    },
168                )
169                .await?;
170                Ok(CliOutput::Invoice {
171                    invoice: response.to_string(),
172                })
173            }
174            Self::PayInvoice { invoice } => {
175                let preimage =
176                    pay_invoice(client, base_url, PayInvoiceForOperatorPayload { invoice }).await?;
177                Ok(CliOutput::Preimage { preimage })
178            }
179            Self::OpenChannel {
180                pubkey,
181                host,
182                channel_size_sats,
183                push_amount_sats,
184                fee_rate_sats_per_vbyte,
185                base_fee_msat,
186                parts_per_million,
187            } => {
188                let payload = OpenChannelRequest {
189                    pubkey,
190                    host,
191                    channel_size_sats,
192                    push_amount_sats: push_amount_sats.unwrap_or(0),
193                    fee_rate_sats_per_vbyte,
194                    base_fee_msat,
195                    parts_per_million,
196                };
197                let funding_txid = if payload.push_amount_sats > 0 {
198                    open_channel_with_push(client, base_url, payload).await?
199                } else {
200                    open_channel(client, base_url, payload).await?
201                };
202                Ok(CliOutput::FundingTxid { funding_txid })
203            }
204            Self::CloseChannelsWithPeer {
205                pubkey,
206                force,
207                sats_per_vbyte,
208            } => {
209                let response = close_channels_with_peer(
210                    client,
211                    base_url,
212                    CloseChannelsWithPeerRequest {
213                        pubkey,
214                        force,
215                        sats_per_vbyte,
216                    },
217                )
218                .await?;
219                Ok(CliOutput::CloseChannels(response))
220            }
221            Self::ListChannels => {
222                let response = list_channels(client, base_url).await?;
223                Ok(CliOutput::Channels(response))
224            }
225            Self::SetChannelFees {
226                funding_outpoint,
227                base_fee_msat,
228                parts_per_million,
229            } => {
230                set_channel_fees(
231                    client,
232                    base_url,
233                    SetChannelFeesRequest {
234                        funding_outpoint,
235                        base_fee_msat,
236                        parts_per_million,
237                    },
238                )
239                .await?;
240                Ok(CliOutput::Empty)
241            }
242            Self::GetInvoice { payment_hash } => {
243                let response =
244                    get_invoice(client, base_url, GetInvoiceRequest { payment_hash }).await?;
245                Ok(CliOutput::InvoiceDetails(response))
246            }
247            Self::ListTransactions {
248                start_time,
249                end_time,
250            } => {
251                let start_secs = start_time
252                    .timestamp()
253                    .try_into()
254                    .map_err(|e| ServerError::InternalClientError(anyhow::anyhow!("{e}")))?;
255                let end_secs = end_time
256                    .timestamp()
257                    .try_into()
258                    .map_err(|e| ServerError::InternalClientError(anyhow::anyhow!("{e}")))?;
259                let response = list_transactions(
260                    client,
261                    base_url,
262                    ListTransactionsPayload {
263                        start_secs,
264                        end_secs,
265                    },
266                )
267                .await?;
268                Ok(CliOutput::Transactions(response))
269            }
270            Self::CreateOffer {
271                amount_msat,
272                description,
273                expiry_secs,
274                quantity,
275            } => {
276                let response = create_offer(
277                    client,
278                    base_url,
279                    CreateOfferPayload {
280                        amount: amount_msat.map(Amount::from_msats),
281                        description,
282                        expiry_secs,
283                        quantity,
284                    },
285                )
286                .await?;
287                Ok(CliOutput::Offer(response))
288            }
289            Self::PayOffer {
290                offer,
291                amount_msat,
292                quantity,
293                payer_note,
294            } => {
295                let response = pay_offer(
296                    client,
297                    base_url,
298                    PayOfferPayload {
299                        offer,
300                        amount: amount_msat.map(Amount::from_msats),
301                        quantity,
302                        payer_note,
303                    },
304                )
305                .await?;
306                Ok(CliOutput::OfferPayment(response))
307            }
308        }
309    }
310}