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