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