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