Skip to main content

fedimint_lightning/
ldk.rs

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