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#[derive(Subcommand)]
24pub enum LightningCommands {
25 CreateInvoice {
27 amount_msats: u64,
28
29 #[clap(long)]
30 expiry_secs: Option<u32>,
31
32 #[clap(long)]
33 description: Option<String>,
34 },
35 PayInvoice { invoice: Bolt11Invoice },
37 OpenChannel {
39 #[clap(long)]
41 pubkey: bitcoin::secp256k1::PublicKey,
42
43 #[clap(long)]
44 host: String,
45
46 #[clap(long)]
48 channel_size_sats: u64,
49
50 #[clap(long)]
52 push_amount_sats: Option<u64>,
53
54 #[clap(long)]
57 fee_rate_sats_per_vbyte: Option<u64>,
58
59 #[clap(long)]
61 base_fee_msat: Option<u64>,
62
63 #[clap(long)]
66 parts_per_million: Option<u64>,
67 },
68 CloseChannelsWithPeer {
71 #[clap(long)]
73 pubkey: bitcoin::secp256k1::PublicKey,
74
75 #[clap(long)]
77 force: bool,
78
79 #[clap(long, required_unless_present = "force")]
82 sats_per_vbyte: Option<u64>,
83 },
84 ListChannels,
86 SetChannelFees {
88 #[clap(long)]
90 funding_outpoint: bitcoin::OutPoint,
91
92 #[clap(long)]
94 base_fee_msat: u64,
95
96 #[clap(long)]
98 parts_per_million: u64,
99 },
100 ListTransactions {
103 #[arg(long, value_parser = parse_datetime)]
106 start_time: DateTime<Utc>,
107
108 #[arg(long, value_parser = parse_datetime)]
111 end_time: DateTime<Utc>,
112 },
113 GetInvoice {
115 #[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}