fedimint_lightning/
ldk.rs

1use std::fmt;
2use std::path::Path;
3use std::str::FromStr;
4use std::sync::Arc;
5use std::time::{Duration, UNIX_EPOCH};
6
7use async_trait::async_trait;
8use bitcoin::hashes::{Hash, sha256};
9use bitcoin::{Network, OutPoint};
10use fedimint_bip39::Mnemonic;
11use fedimint_bitcoind::{DynBitcoindRpc, create_bitcoind};
12use fedimint_core::envs::{BitcoinRpcConfig, is_env_var_set};
13use fedimint_core::task::{TaskGroup, TaskHandle, block_in_place};
14use fedimint_core::util::{FmtCompact, SafeUrl};
15use fedimint_core::{Amount, BitcoinAmountOrAll, crit};
16use fedimint_gateway_common::{GetInvoiceRequest, GetInvoiceResponse, ListTransactionsResponse};
17use fedimint_ln_common::contracts::Preimage;
18use fedimint_logging::LOG_LIGHTNING;
19use ldk_node::lightning::ln::PaymentHash;
20use ldk_node::lightning::ln::msgs::SocketAddress;
21use ldk_node::lightning::routing::gossip::NodeAlias;
22use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus, SendingParameters};
23use lightning::ln::PaymentPreimage;
24use lightning::ln::channelmanager::PaymentId;
25use lightning::offers::offer::{Offer, OfferId};
26use lightning::util::scid_utils::scid_from_parts;
27use lightning_invoice::Bolt11Invoice;
28use tokio::sync::mpsc::Sender;
29use tokio_stream::wrappers::ReceiverStream;
30use tracing::{info, warn};
31
32use super::{
33    ChannelInfo, ILnRpcClient, LightningRpcError, ListActiveChannelsResponse, RouteHtlcStream,
34};
35use crate::{
36    CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, CreateInvoiceRequest,
37    CreateInvoiceResponse, GetBalancesResponse, GetLnOnchainAddressResponse, GetNodeInfoResponse,
38    GetRouteHintsResponse, InterceptPaymentRequest, InterceptPaymentResponse, InvoiceDescription,
39    OpenChannelRequest, OpenChannelResponse, PayInvoiceResponse, PaymentAction, SendOnchainRequest,
40    SendOnchainResponse,
41};
42
43#[derive(Clone)]
44pub enum GatewayLdkChainSourceConfig {
45    Bitcoind { server_url: SafeUrl },
46    Esplora { server_url: SafeUrl },
47}
48
49impl fmt::Display for GatewayLdkChainSourceConfig {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            GatewayLdkChainSourceConfig::Bitcoind { server_url } => {
53                write!(f, "Bitcoind source with URL: {}", server_url)
54            }
55            GatewayLdkChainSourceConfig::Esplora { server_url } => {
56                write!(f, "Esplora source with URL: {}", server_url)
57            }
58        }
59    }
60}
61
62impl GatewayLdkChainSourceConfig {
63    fn bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
64        match self {
65            Self::Bitcoind { server_url } => BitcoinRpcConfig {
66                kind: "bitcoind".to_string(),
67                url: server_url.clone(),
68            },
69            Self::Esplora { server_url } => BitcoinRpcConfig {
70                kind: "esplora".to_string(),
71                url: server_url.clone(),
72            },
73        }
74    }
75}
76
77pub struct GatewayLdkClient {
78    /// The underlying lightning node.
79    node: Arc<ldk_node::Node>,
80
81    /// The client for querying data about the blockchain.
82    bitcoind_rpc: DynBitcoindRpc,
83
84    task_group: TaskGroup,
85
86    /// The HTLC stream, until it is taken by calling
87    /// `ILnRpcClient::route_htlcs`.
88    htlc_stream_receiver_or: Option<tokio::sync::mpsc::Receiver<InterceptPaymentRequest>>,
89
90    /// Lock pool used to ensure that our implementation of `ILnRpcClient::pay`
91    /// doesn't allow for multiple simultaneous calls with the same invoice to
92    /// execute in parallel. This helps ensure that the function is idempotent.
93    outbound_lightning_payment_lock_pool: lockable::LockPool<PaymentId>,
94
95    /// Lock pool used to ensure that our implementation of
96    /// `ILnRpcClient::pay_offer` doesn't allow for multiple simultaneous
97    /// calls with the same offer to execute in parallel. This helps ensure
98    /// that the function is idempotent.
99    outbound_offer_lock_pool: lockable::LockPool<LdkOfferId>,
100}
101
102impl std::fmt::Debug for GatewayLdkClient {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("GatewayLdkClient").finish_non_exhaustive()
105    }
106}
107
108impl GatewayLdkClient {
109    /// Creates a new `GatewayLdkClient` instance and starts the underlying
110    /// lightning node. All resources, including the lightning node, will be
111    /// cleaned up when the returned `GatewayLdkClient` instance is dropped.
112    /// There's no need to manually stop the node.
113    pub fn new(
114        data_dir: &Path,
115        chain_source_config: GatewayLdkChainSourceConfig,
116        network: Network,
117        lightning_port: u16,
118        alias: String,
119        mnemonic: Mnemonic,
120        runtime: Arc<tokio::runtime::Runtime>,
121    ) -> anyhow::Result<Self> {
122        let mut bytes = [0u8; 32];
123        let alias = if alias.is_empty() {
124            "LDK Gateway".to_string()
125        } else {
126            alias
127        };
128        let alias_bytes = alias.as_bytes();
129        let truncated = &alias_bytes[..alias_bytes.len().min(32)];
130        bytes[..truncated.len()].copy_from_slice(truncated);
131        let node_alias = Some(NodeAlias(bytes));
132
133        let mut node_builder = ldk_node::Builder::from_config(ldk_node::config::Config {
134            network,
135            listening_addresses: Some(vec![SocketAddress::TcpIpV4 {
136                addr: [0, 0, 0, 0],
137                port: lightning_port,
138            }]),
139            node_alias,
140            ..Default::default()
141        });
142
143        node_builder.set_entropy_bip39_mnemonic(mnemonic, None);
144
145        let bitcoind_rpc = create_bitcoind(&chain_source_config.bitcoin_rpc_config())?;
146
147        match chain_source_config.clone() {
148            GatewayLdkChainSourceConfig::Bitcoind { server_url } => {
149                node_builder.set_chain_source_bitcoind_rpc(
150                    server_url
151                        .host_str()
152                        .expect("Could not retrieve host from bitcoind RPC url")
153                        .to_string(),
154                    server_url
155                        .port()
156                        .expect("Could not retrieve port from bitcoind RPC url"),
157                    server_url.username().to_string(),
158                    server_url.password().unwrap_or_default().to_string(),
159                );
160            }
161            GatewayLdkChainSourceConfig::Esplora { server_url } => {
162                // Esplora client cannot handle trailing slashes
163                let host = server_url
164                    .host_str()
165                    .ok_or(anyhow::anyhow!("Missing esplora host"))?;
166                let port = server_url
167                    .port()
168                    .ok_or(anyhow::anyhow!("Missing esplora port"))?;
169                let server_url = format!("{}://{}:{}", server_url.scheme(), host, port);
170                node_builder.set_chain_source_esplora(server_url, None);
171            }
172        };
173        let Some(data_dir_str) = data_dir.to_str() else {
174            return Err(anyhow::anyhow!("Invalid data dir path"));
175        };
176        node_builder.set_storage_dir_path(data_dir_str.to_string());
177
178        info!(chain_source = %chain_source_config, data_dir = %data_dir_str, alias = %alias, "Starting LDK Node...");
179        let node = Arc::new(node_builder.build()?);
180        node.start_with_runtime(runtime).map_err(|err| {
181            crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to start LDK Node");
182            LightningRpcError::FailedToConnect
183        })?;
184
185        let (htlc_stream_sender, htlc_stream_receiver) = tokio::sync::mpsc::channel(1024);
186        let task_group = TaskGroup::new();
187
188        let node_clone = node.clone();
189        task_group.spawn("ldk lightning node event handler", |handle| async move {
190            loop {
191                Self::handle_next_event(&node_clone, &htlc_stream_sender, &handle).await;
192            }
193        });
194
195        info!("Successfully started LDK Gateway");
196        Ok(GatewayLdkClient {
197            node,
198            bitcoind_rpc,
199            task_group,
200            htlc_stream_receiver_or: Some(htlc_stream_receiver),
201            outbound_lightning_payment_lock_pool: lockable::LockPool::new(),
202            outbound_offer_lock_pool: lockable::LockPool::new(),
203        })
204    }
205
206    async fn handle_next_event(
207        node: &ldk_node::Node,
208        htlc_stream_sender: &Sender<InterceptPaymentRequest>,
209        handle: &TaskHandle,
210    ) {
211        // We manually check for task termination in case we receive a payment while the
212        // task is shutting down. In that case, we want to finish the payment
213        // before shutting this task down.
214        let event = tokio::select! {
215            event = node.next_event_async() => {
216                event
217            }
218            () = handle.make_shutdown_rx() => {
219                return;
220            }
221        };
222
223        if let ldk_node::Event::PaymentClaimable {
224            payment_id: _,
225            payment_hash,
226            claimable_amount_msat,
227            claim_deadline,
228        } = event
229        {
230            if let Err(err) = htlc_stream_sender
231                .send(InterceptPaymentRequest {
232                    payment_hash: Hash::from_slice(&payment_hash.0).expect("Failed to create Hash"),
233                    amount_msat: claimable_amount_msat,
234                    expiry: claim_deadline.unwrap_or_default(),
235                    short_channel_id: None,
236                    incoming_chan_id: 0,
237                    htlc_id: 0,
238                })
239                .await
240            {
241                warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed send InterceptHtlcRequest to stream");
242            }
243        }
244
245        // The `PaymentClaimable` event is the only event type that we are interested
246        // in. We can safely ignore all other events.
247        node.event_handled();
248    }
249
250    /// Converts a transaction outpoint to a short channel ID by querying the
251    /// blockchain.
252    async fn outpoint_to_scid(&self, funding_txo: OutPoint) -> anyhow::Result<u64> {
253        let block_hash = self
254            .bitcoind_rpc
255            .get_txout_proof(funding_txo.txid)
256            .await?
257            .block_header
258            .block_hash();
259
260        let block_height = self
261            .bitcoind_rpc
262            .get_tx_block_height(&funding_txo.txid)
263            .await?
264            .ok_or(anyhow::anyhow!("Failed to get block height"))?;
265
266        let block = self.bitcoind_rpc.get_block(&block_hash).await?;
267
268        let tx_index = block
269            .txdata
270            .iter()
271            .enumerate()
272            .find(|(_, tx)| tx.compute_txid() == funding_txo.txid)
273            .ok_or(anyhow::anyhow!("Failed to find transaction"))?
274            .0 as u32;
275
276        let output_index = funding_txo.vout;
277
278        scid_from_parts(block_height, u64::from(tx_index), u64::from(output_index))
279            .map_err(|e| anyhow::anyhow!("Failed to convert to short channel ID: {e:?}"))
280    }
281}
282
283impl Drop for GatewayLdkClient {
284    fn drop(&mut self) {
285        self.task_group.shutdown();
286
287        info!(target: LOG_LIGHTNING, "Stopping LDK Node...");
288        match self.node.stop() {
289            Err(err) => {
290                warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to stop LDK Node");
291            }
292            _ => {
293                info!(target: LOG_LIGHTNING, "LDK Node stopped.");
294            }
295        }
296    }
297}
298
299#[async_trait]
300impl ILnRpcClient for GatewayLdkClient {
301    async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
302        // HACK: https://github.com/lightningdevkit/ldk-node/issues/339 when running in devimint
303        // to speed up tests
304        if is_env_var_set("FM_IN_DEVIMINT") {
305            block_in_place(|| {
306                let _ = self.node.sync_wallets();
307            });
308        }
309        let node_status = self.node.status();
310
311        let chain_tip_block_height =
312            u32::try_from(self.bitcoind_rpc.get_block_count().await.map_err(|e| {
313                LightningRpcError::FailedToGetNodeInfo {
314                    failure_reason: format!("Failed to get block count from chain source: {e}"),
315                }
316            })?)
317            .expect("Failed to convert block count to u32")
318                - 1;
319        let ldk_block_height = node_status.current_best_block.height;
320        let synced_to_chain = chain_tip_block_height == ldk_block_height;
321
322        assert!(
323            chain_tip_block_height >= ldk_block_height,
324            "LDK Block Height is in the future"
325        );
326
327        Ok(GetNodeInfoResponse {
328            pub_key: self.node.node_id(),
329            alias: match self.node.node_alias() {
330                Some(alias) => alias.to_string(),
331                None => format!("LDK Fedimint Gateway Node {}", self.node.node_id()),
332            },
333            network: self.node.config().network.to_string(),
334            block_height: ldk_block_height,
335            synced_to_chain,
336        })
337    }
338
339    async fn routehints(
340        &self,
341        _num_route_hints: usize,
342    ) -> Result<GetRouteHintsResponse, LightningRpcError> {
343        // `ILnRpcClient::routehints()` is currently only ever used for LNv1 payment
344        // receives and will be removed when we switch to LNv2. The LDK gateway will
345        // never support LNv1 payment receives, only LNv2 payment receives, which
346        // require that the gateway's lightning node generates invoices rather than the
347        // fedimint client, so it is able to insert the proper route hints on its own.
348        Ok(GetRouteHintsResponse {
349            route_hints: vec![],
350        })
351    }
352
353    async fn pay(
354        &self,
355        invoice: Bolt11Invoice,
356        max_delay: u64,
357        max_fee: Amount,
358    ) -> Result<PayInvoiceResponse, LightningRpcError> {
359        let payment_id = PaymentId(*invoice.payment_hash().as_byte_array());
360
361        // Lock by the payment hash to prevent multiple simultaneous calls with the same
362        // invoice from executing. This prevents `ldk-node::Bolt11Payment::send()` from
363        // being called multiple times with the same invoice. This is important because
364        // `ldk-node::Bolt11Payment::send()` is not idempotent, but this function must
365        // be idempotent.
366        let _payment_lock_guard = self
367            .outbound_lightning_payment_lock_pool
368            .async_lock(payment_id)
369            .await;
370
371        // If a payment is not known to the node we can initiate it, and if it is known
372        // we can skip calling `ldk-node::Bolt11Payment::send()` and wait for the
373        // payment to complete. The lock guard above guarantees that this block is only
374        // executed once at a time for a given payment hash, ensuring that there is no
375        // race condition between checking if a payment is known and initiating a new
376        // payment if it isn't.
377        if self.node.payment(&payment_id).is_none() {
378            assert_eq!(
379                self.node
380                    .bolt11_payment()
381                    .send(
382                        &invoice,
383                        Some(SendingParameters {
384                            max_total_routing_fee_msat: Some(Some(max_fee.msats)),
385                            max_total_cltv_expiry_delta: Some(max_delay as u32),
386                            max_path_count: None,
387                            max_channel_saturation_power_of_half: None,
388                        }),
389                    )
390                    // TODO: Investigate whether all error types returned by `Bolt11Payment::send()`
391                    // result in idempotency.
392                    .map_err(|e| LightningRpcError::FailedPayment {
393                        failure_reason: format!("LDK payment failed to initialize: {e:?}"),
394                    })?,
395                payment_id
396            );
397        }
398
399        // TODO: Find a way to avoid looping/polling to know when a payment is
400        // completed. `ldk-node` provides `PaymentSuccessful` and `PaymentFailed`
401        // events, but interacting with the node event queue here isn't
402        // straightforward.
403        loop {
404            if let Some(payment_details) = self.node.payment(&payment_id) {
405                match payment_details.status {
406                    PaymentStatus::Pending => {}
407                    PaymentStatus::Succeeded => {
408                        if let PaymentKind::Bolt11 {
409                            preimage: Some(preimage),
410                            ..
411                        } = payment_details.kind
412                        {
413                            return Ok(PayInvoiceResponse {
414                                preimage: Preimage(preimage.0),
415                            });
416                        }
417                    }
418                    PaymentStatus::Failed => {
419                        return Err(LightningRpcError::FailedPayment {
420                            failure_reason: "LDK payment failed".to_string(),
421                        });
422                    }
423                }
424            }
425            fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
426        }
427    }
428
429    async fn route_htlcs<'a>(
430        mut self: Box<Self>,
431        _task_group: &TaskGroup,
432    ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
433        let route_htlc_stream = match self.htlc_stream_receiver_or.take() {
434            Some(stream) => Ok(Box::pin(ReceiverStream::new(stream))),
435            None => Err(LightningRpcError::FailedToRouteHtlcs {
436                failure_reason:
437                    "Stream does not exist. Likely was already taken by calling `route_htlcs()`."
438                        .to_string(),
439            }),
440        }?;
441
442        Ok((route_htlc_stream, Arc::new(*self)))
443    }
444
445    async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
446        let InterceptPaymentResponse {
447            action,
448            payment_hash,
449            incoming_chan_id: _,
450            htlc_id: _,
451        } = htlc;
452
453        let ph = PaymentHash(*payment_hash.clone().as_byte_array());
454
455        // TODO: Get the actual amount from the LDK node. Probably makes the
456        // most sense to pipe it through the `InterceptHtlcResponse` struct.
457        // This value is only used by `ldk-node` to ensure that the amount
458        // claimed isn't less than the amount expected, but we've already
459        // verified that the amount is correct when we intercepted the payment.
460        let claimable_amount_msat = 999_999_999_999_999;
461
462        let ph_hex_str = hex::encode(payment_hash);
463
464        if let PaymentAction::Settle(preimage) = action {
465            self.node
466                .bolt11_payment()
467                .claim_for_hash(ph, claimable_amount_msat, PaymentPreimage(preimage.0))
468                .map_err(|_| LightningRpcError::FailedToCompleteHtlc {
469                    failure_reason: format!("Failed to claim LDK payment with hash {ph_hex_str}"),
470                })?;
471        } else {
472            warn!(target: LOG_LIGHTNING, payment_hash = %ph_hex_str, "Unwinding payment because the action was not `Settle`");
473            self.node.bolt11_payment().fail_for_hash(ph).map_err(|_| {
474                LightningRpcError::FailedToCompleteHtlc {
475                    failure_reason: format!("Failed to unwind LDK payment with hash {ph_hex_str}"),
476                }
477            })?;
478        }
479
480        return Ok(());
481    }
482
483    async fn create_invoice(
484        &self,
485        create_invoice_request: CreateInvoiceRequest,
486    ) -> Result<CreateInvoiceResponse, LightningRpcError> {
487        let payment_hash_or = if let Some(payment_hash) = create_invoice_request.payment_hash {
488            let ph = PaymentHash(*payment_hash.as_byte_array());
489            Some(ph)
490        } else {
491            None
492        };
493
494        // Currently `ldk-node` only supports direct descriptions.
495        // See https://github.com/lightningdevkit/ldk-node/issues/325.
496        // TODO: Once the above issue is resolved, we should support
497        // description hashes as well.
498        let description_str = match create_invoice_request.description {
499            Some(InvoiceDescription::Direct(desc)) => desc,
500            _ => String::new(),
501        };
502
503        let invoice = match payment_hash_or {
504            Some(payment_hash) => self.node.bolt11_payment().receive_for_hash(
505                create_invoice_request.amount_msat,
506                description_str.as_str(),
507                create_invoice_request.expiry_secs,
508                payment_hash,
509            ),
510            None => self.node.bolt11_payment().receive(
511                create_invoice_request.amount_msat,
512                description_str.as_str(),
513                create_invoice_request.expiry_secs,
514            ),
515        }
516        .map_err(|e| LightningRpcError::FailedToGetInvoice {
517            failure_reason: e.to_string(),
518        })?;
519
520        Ok(CreateInvoiceResponse {
521            invoice: invoice.to_string(),
522        })
523    }
524
525    async fn get_ln_onchain_address(
526        &self,
527    ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
528        self.node
529            .onchain_payment()
530            .new_address()
531            .map(|address| GetLnOnchainAddressResponse {
532                address: address.to_string(),
533            })
534            .map_err(|e| LightningRpcError::FailedToGetLnOnchainAddress {
535                failure_reason: e.to_string(),
536            })
537    }
538
539    async fn send_onchain(
540        &self,
541        SendOnchainRequest {
542            address,
543            amount,
544            // TODO: Respect this fee rate once `ldk-node` supports setting a custom fee rate.
545            // This work is planned to be in `ldk-node` v0.4 and is tracked here:
546            // https://github.com/lightningdevkit/ldk-node/issues/176
547            fee_rate_sats_per_vbyte: _,
548        }: SendOnchainRequest,
549    ) -> Result<SendOnchainResponse, LightningRpcError> {
550        let onchain = self.node.onchain_payment();
551
552        let txid = match amount {
553            BitcoinAmountOrAll::All => onchain.send_all_to_address(&address.assume_checked()),
554            BitcoinAmountOrAll::Amount(amount_sats) => {
555                onchain.send_to_address(&address.assume_checked(), amount_sats.to_sat())
556            }
557        }
558        .map_err(|e| LightningRpcError::FailedToWithdrawOnchain {
559            failure_reason: e.to_string(),
560        })?;
561
562        Ok(SendOnchainResponse {
563            txid: txid.to_string(),
564        })
565    }
566
567    async fn open_channel(
568        &self,
569        OpenChannelRequest {
570            pubkey,
571            host,
572            channel_size_sats,
573            push_amount_sats,
574        }: OpenChannelRequest,
575    ) -> Result<OpenChannelResponse, LightningRpcError> {
576        let push_amount_msats_or = if push_amount_sats == 0 {
577            None
578        } else {
579            Some(push_amount_sats * 1000)
580        };
581
582        let user_channel_id = self
583            .node
584            .open_announced_channel(
585                pubkey,
586                SocketAddress::from_str(&host).map_err(|e| {
587                    LightningRpcError::FailedToConnectToPeer {
588                        failure_reason: e.to_string(),
589                    }
590                })?,
591                channel_size_sats,
592                push_amount_msats_or,
593                None,
594            )
595            .map_err(|e| LightningRpcError::FailedToOpenChannel {
596                failure_reason: e.to_string(),
597            })?;
598
599        // The channel isn't always visible immediately, so we need to poll for it.
600        for _ in 0..10 {
601            let funding_txid_or = self
602                .node
603                .list_channels()
604                .iter()
605                .find(|channel| channel.user_channel_id == user_channel_id)
606                .and_then(|channel| channel.funding_txo)
607                .map(|funding_txo| funding_txo.txid);
608
609            if let Some(funding_txid) = funding_txid_or {
610                return Ok(OpenChannelResponse {
611                    funding_txid: funding_txid.to_string(),
612                });
613            }
614
615            fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
616        }
617
618        Err(LightningRpcError::FailedToOpenChannel {
619            failure_reason: "Channel could not be opened".to_string(),
620        })
621    }
622
623    async fn close_channels_with_peer(
624        &self,
625        CloseChannelsWithPeerRequest { pubkey }: CloseChannelsWithPeerRequest,
626    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
627        let mut num_channels_closed = 0;
628
629        for channel_with_peer in self
630            .node
631            .list_channels()
632            .iter()
633            .filter(|channel| channel.counterparty_node_id == pubkey)
634        {
635            if self
636                .node
637                .close_channel(&channel_with_peer.user_channel_id, pubkey)
638                .is_ok()
639            {
640                num_channels_closed += 1;
641            }
642        }
643
644        Ok(CloseChannelsWithPeerResponse {
645            num_channels_closed,
646        })
647    }
648
649    async fn list_active_channels(&self) -> Result<ListActiveChannelsResponse, LightningRpcError> {
650        let mut channels = Vec::new();
651
652        for channel_details in self
653            .node
654            .list_channels()
655            .iter()
656            .filter(|channel| channel.is_usable)
657        {
658            channels.push(ChannelInfo {
659                remote_pubkey: channel_details.counterparty_node_id,
660                channel_size_sats: channel_details.channel_value_sats,
661                outbound_liquidity_sats: channel_details.outbound_capacity_msat / 1000,
662                inbound_liquidity_sats: channel_details.inbound_capacity_msat / 1000,
663                short_channel_id: match channel_details.funding_txo {
664                    Some(funding_txo) => self.outpoint_to_scid(funding_txo).await.unwrap_or(0),
665                    None => 0,
666                },
667            });
668        }
669
670        Ok(ListActiveChannelsResponse { channels })
671    }
672
673    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
674        let balances = self.node.list_balances();
675        let channel_lists = self
676            .node
677            .list_channels()
678            .into_iter()
679            .filter(|chan| chan.is_usable)
680            .collect::<Vec<_>>();
681        // map and get the total inbound_capacity_msat in the channels
682        let total_inbound_liquidity_balance_msat: u64 = channel_lists
683            .iter()
684            .map(|channel| channel.inbound_capacity_msat)
685            .sum();
686
687        Ok(GetBalancesResponse {
688            onchain_balance_sats: balances.total_onchain_balance_sats,
689            lightning_balance_msats: balances.total_lightning_balance_sats * 1000,
690            inbound_lightning_liquidity_msats: total_inbound_liquidity_balance_msat,
691        })
692    }
693
694    async fn get_invoice(
695        &self,
696        get_invoice_request: GetInvoiceRequest,
697    ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
698        let invoices = self
699            .node
700            .list_payments_with_filter(|details| {
701                details.direction == PaymentDirection::Inbound
702                    && details.id == PaymentId(get_invoice_request.payment_hash.to_byte_array())
703                    && !matches!(details.kind, PaymentKind::Onchain)
704            })
705            .iter()
706            .map(|details| {
707                let (preimage, payment_hash, _) = get_preimage_and_payment_hash(&details.kind);
708                let status = match details.status {
709                    PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
710                    PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
711                    PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
712                };
713                GetInvoiceResponse {
714                    preimage: preimage.map(|p| p.to_string()),
715                    payment_hash,
716                    amount: Amount::from_msats(
717                        details
718                            .amount_msat
719                            .expect("amountless invoices are not supported"),
720                    ),
721                    created_at: UNIX_EPOCH + Duration::from_secs(details.latest_update_timestamp),
722                    status,
723                }
724            })
725            .collect::<Vec<_>>();
726
727        Ok(invoices.first().cloned())
728    }
729
730    async fn list_transactions(
731        &self,
732        start_secs: u64,
733        end_secs: u64,
734    ) -> Result<ListTransactionsResponse, LightningRpcError> {
735        let transactions = self
736            .node
737            .list_payments_with_filter(|details| {
738                details.kind != PaymentKind::Onchain
739                    && details.latest_update_timestamp >= start_secs
740                    && details.latest_update_timestamp < end_secs
741            })
742            .iter()
743            .map(|details| {
744                let (preimage, payment_hash, payment_kind) =
745                    get_preimage_and_payment_hash(&details.kind);
746                let direction = match details.direction {
747                    PaymentDirection::Outbound => {
748                        fedimint_gateway_common::PaymentDirection::Outbound
749                    }
750                    PaymentDirection::Inbound => fedimint_gateway_common::PaymentDirection::Inbound,
751                };
752                let status = match details.status {
753                    PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
754                    PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
755                    PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
756                };
757                fedimint_gateway_common::PaymentDetails {
758                    payment_hash,
759                    preimage: preimage.map(|p| p.to_string()),
760                    payment_kind,
761                    amount: Amount::from_msats(
762                        details
763                            .amount_msat
764                            .expect("amountless invoices are not supported"),
765                    ),
766                    direction,
767                    status,
768                    timestamp_secs: details.latest_update_timestamp,
769                }
770            })
771            .collect::<Vec<_>>();
772        Ok(ListTransactionsResponse { transactions })
773    }
774
775    fn create_offer(
776        &self,
777        amount: Option<Amount>,
778        description: Option<String>,
779        expiry_secs: Option<u32>,
780        quantity: Option<u64>,
781    ) -> Result<String, LightningRpcError> {
782        let description = description.unwrap_or_default();
783        let offer = if let Some(amount) = amount {
784            self.node
785                .bolt12_payment()
786                .receive(amount.msats, &description, expiry_secs, quantity)
787                .map_err(|err| LightningRpcError::Bolt12Error {
788                    failure_reason: err.to_string(),
789                })?
790        } else {
791            self.node
792                .bolt12_payment()
793                .receive_variable_amount(&description, expiry_secs)
794                .map_err(|err| LightningRpcError::Bolt12Error {
795                    failure_reason: err.to_string(),
796                })?
797        };
798
799        Ok(offer.to_string())
800    }
801
802    async fn pay_offer(
803        &self,
804        offer: String,
805        quantity: Option<u64>,
806        amount: Option<Amount>,
807        payer_note: Option<String>,
808    ) -> Result<Preimage, LightningRpcError> {
809        let offer = Offer::from_str(&offer).map_err(|_| LightningRpcError::Bolt12Error {
810            failure_reason: "Failed to parse Bolt12 Offer".to_string(),
811        })?;
812
813        let _offer_lock_guard = self
814            .outbound_offer_lock_pool
815            .blocking_lock(LdkOfferId(offer.id()));
816
817        let payment_id = if let Some(amount) = amount {
818            self.node
819                .bolt12_payment()
820                .send_using_amount(&offer, amount.msats, quantity, payer_note)
821                .map_err(|err| LightningRpcError::Bolt12Error {
822                    failure_reason: err.to_string(),
823                })?
824        } else {
825            self.node
826                .bolt12_payment()
827                .send(&offer, quantity, payer_note)
828                .map_err(|err| LightningRpcError::Bolt12Error {
829                    failure_reason: err.to_string(),
830                })?
831        };
832
833        loop {
834            if let Some(payment_details) = self.node.payment(&payment_id) {
835                match payment_details.status {
836                    PaymentStatus::Pending => {}
837                    PaymentStatus::Succeeded => match payment_details.kind {
838                        PaymentKind::Bolt12Offer {
839                            preimage: Some(preimage),
840                            ..
841                        } => {
842                            info!(target: LOG_LIGHTNING, offer = %offer, payment_id = %payment_id, preimage = %preimage, "Successfully paid offer");
843                            return Ok(Preimage(preimage.0));
844                        }
845                        _ => {
846                            return Err(LightningRpcError::FailedPayment {
847                                failure_reason: "Unexpected payment kind".to_string(),
848                            });
849                        }
850                    },
851                    PaymentStatus::Failed => {
852                        return Err(LightningRpcError::FailedPayment {
853                            failure_reason: "Bolt12 payment failed".to_string(),
854                        });
855                    }
856                }
857            }
858            fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
859        }
860    }
861}
862
863/// Maps LDK's `PaymentKind` to an optional preimage and an optional payment
864/// hash depending on the type of payment.
865fn get_preimage_and_payment_hash(
866    kind: &PaymentKind,
867) -> (
868    Option<Preimage>,
869    Option<sha256::Hash>,
870    fedimint_gateway_common::PaymentKind,
871) {
872    match kind {
873        PaymentKind::Bolt11 {
874            hash,
875            preimage,
876            secret: _,
877        } => (
878            preimage.map(|p| Preimage(p.0)),
879            Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
880            fedimint_gateway_common::PaymentKind::Bolt11,
881        ),
882        PaymentKind::Bolt11Jit {
883            hash,
884            preimage,
885            secret: _,
886            lsp_fee_limits: _,
887        } => (
888            preimage.map(|p| Preimage(p.0)),
889            Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
890            fedimint_gateway_common::PaymentKind::Bolt11,
891        ),
892        PaymentKind::Bolt12Offer {
893            hash,
894            preimage,
895            secret: _,
896            offer_id: _,
897            payer_note: _,
898            quantity: _,
899        } => (
900            preimage.map(|p| Preimage(p.0)),
901            hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
902            fedimint_gateway_common::PaymentKind::Bolt12Offer,
903        ),
904        PaymentKind::Bolt12Refund {
905            hash,
906            preimage,
907            secret: _,
908            payer_note: _,
909            quantity: _,
910        } => (
911            preimage.map(|p| Preimage(p.0)),
912            hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
913            fedimint_gateway_common::PaymentKind::Bolt12Refund,
914        ),
915        PaymentKind::Spontaneous { hash, preimage } => (
916            preimage.map(|p| Preimage(p.0)),
917            Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
918            fedimint_gateway_common::PaymentKind::Bolt11,
919        ),
920        PaymentKind::Onchain => (None, None, fedimint_gateway_common::PaymentKind::Onchain),
921    }
922}
923
924#[derive(Debug, Clone, Copy, Eq, PartialEq)]
925struct LdkOfferId(OfferId);
926
927impl std::hash::Hash for LdkOfferId {
928    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
929        state.write(&self.0.0);
930    }
931}