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