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