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