Skip to main content

cdk_ldk_node/
lib.rs

1//! CDK lightning backend for ldk-node
2
3#![doc = include_str!("../README.md")]
4
5use std::net::SocketAddr;
6use std::pin::Pin;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use bip39::Mnemonic;
12use cdk_common::common::FeeReserve;
13use cdk_common::payment::{self, *};
14use cdk_common::util::{hex, unix_time};
15use cdk_common::{Amount, CurrencyUnit, MeltOptions, MeltQuoteState};
16use futures::{Stream, StreamExt};
17use ldk_node::bitcoin::hashes::Hash;
18use ldk_node::bitcoin::Network;
19use ldk_node::lightning::ln::channelmanager::PaymentId;
20use ldk_node::lightning::ln::msgs::SocketAddress;
21use ldk_node::lightning::routing::router::RouteParametersConfig;
22use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description};
23use ldk_node::lightning_types::payment::PaymentHash;
24use ldk_node::logger::{LogLevel, LogWriter};
25use ldk_node::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
26use ldk_node::{Builder, Event, Node};
27use tokio_stream::wrappers::BroadcastStream;
28use tokio_util::sync::CancellationToken;
29use tracing::instrument;
30
31use crate::error::Error;
32use crate::log::StdoutLogWriter;
33
34mod error;
35mod log;
36mod web;
37
38/// CDK Lightning backend using LDK Node
39///
40/// Provides Lightning Network functionality for CDK with support for Cashu operations.
41/// Handles payment creation, processing, and event management using the Lightning Development Kit.
42#[derive(Clone)]
43pub struct CdkLdkNode {
44    inner: Arc<Node>,
45    fee_reserve: FeeReserve,
46    wait_invoice_cancel_token: CancellationToken,
47    wait_invoice_is_active: Arc<AtomicBool>,
48    sender: tokio::sync::broadcast::Sender<WaitPaymentResponse>,
49    receiver: Arc<tokio::sync::broadcast::Receiver<WaitPaymentResponse>>,
50    events_cancel_token: CancellationToken,
51    web_addr: Option<SocketAddr>,
52}
53
54impl std::fmt::Debug for CdkLdkNode {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct("CdkLdkNode")
57            .field("fee_reserve", &self.fee_reserve)
58            .field("web_addr", &self.web_addr)
59            .finish_non_exhaustive()
60    }
61}
62
63/// Configuration for connecting to Bitcoin RPC
64///
65/// Contains the necessary connection parameters for Bitcoin Core RPC interface.
66#[derive(Debug, Clone)]
67pub struct BitcoinRpcConfig {
68    /// Bitcoin RPC server hostname or IP address
69    pub host: String,
70    /// Bitcoin RPC server port number
71    pub port: u16,
72    /// Username for Bitcoin RPC authentication
73    pub user: String,
74    /// Password for Bitcoin RPC authentication
75    pub password: String,
76}
77
78/// Source of blockchain data for the Lightning node
79///
80/// Specifies how the node should connect to the Bitcoin network to retrieve
81/// blockchain information and broadcast transactions.
82#[derive(Debug, Clone)]
83pub enum ChainSource {
84    /// Use an Esplora server for blockchain data
85    ///
86    /// Contains the URL of the Esplora server endpoint
87    Esplora(String),
88    /// Use Bitcoin Core RPC for blockchain data
89    ///
90    /// Contains the configuration for connecting to Bitcoin Core
91    BitcoinRpc(BitcoinRpcConfig),
92}
93
94/// Source of Lightning network gossip data
95///
96/// Specifies how the node should learn about the Lightning Network topology
97/// and routing information.
98#[derive(Debug, Clone)]
99pub enum GossipSource {
100    /// Learn gossip through peer-to-peer connections
101    ///
102    /// The node will connect to other Lightning nodes and exchange gossip data directly
103    P2P,
104    /// Use Rapid Gossip Sync for efficient gossip updates
105    ///
106    /// Contains the URL of the RGS server for compressed gossip data
107    RapidGossipSync(String),
108}
109/// A builder for an [`CdkLdkNode`] instance.
110#[derive(Debug)]
111pub struct CdkLdkNodeBuilder {
112    network: Network,
113    chain_source: ChainSource,
114    gossip_source: GossipSource,
115    log_dir_path: Option<String>,
116    storage_dir_path: String,
117    fee_reserve: FeeReserve,
118    listening_addresses: Vec<SocketAddress>,
119    seed: Option<Mnemonic>,
120    announcement_addresses: Option<Vec<SocketAddress>>,
121}
122
123impl CdkLdkNodeBuilder {
124    /// Creates a new builder instance.
125    pub fn new(
126        network: Network,
127        chain_source: ChainSource,
128        gossip_source: GossipSource,
129        storage_dir_path: String,
130        fee_reserve: FeeReserve,
131        listening_addresses: Vec<SocketAddress>,
132    ) -> Self {
133        Self {
134            network,
135            chain_source,
136            gossip_source,
137            storage_dir_path,
138            fee_reserve,
139            listening_addresses,
140            seed: None,
141            announcement_addresses: None,
142            log_dir_path: None,
143        }
144    }
145
146    /// Configures the [`CdkLdkNode`] to use the Mnemonic for entropy source configuration
147    pub fn with_seed(mut self, seed: Mnemonic) -> Self {
148        self.seed = Some(seed);
149        self
150    }
151    /// Configures the [`CdkLdkNode`] to use announce this address to the lightning network
152    pub fn with_announcement_address(mut self, announcement_addresses: Vec<SocketAddress>) -> Self {
153        self.announcement_addresses = Some(announcement_addresses);
154        self
155    }
156    /// Configures the [`CdkLdkNode`] to use announce this address to the lightning network
157    pub fn with_log_dir_path(mut self, log_dir_path: String) -> Self {
158        self.log_dir_path = Some(log_dir_path);
159        self
160    }
161
162    /// Builds the [`CdkLdkNode`] instance
163    ///
164    /// # Errors
165    /// Returns an error if the LDK node builder fails to create the node
166    pub fn build(self) -> Result<CdkLdkNode, Error> {
167        let mut ldk = Builder::new();
168        ldk.set_network(self.network);
169        tracing::info!("Storage dir of node is {}", self.storage_dir_path);
170        ldk.set_storage_dir_path(self.storage_dir_path);
171
172        match self.chain_source {
173            ChainSource::Esplora(esplora_url) => {
174                ldk.set_chain_source_esplora(esplora_url, None);
175            }
176            ChainSource::BitcoinRpc(BitcoinRpcConfig {
177                host,
178                port,
179                user,
180                password,
181            }) => {
182                ldk.set_chain_source_bitcoind_rpc(host, port, user, password);
183            }
184        }
185
186        match self.gossip_source {
187            GossipSource::P2P => {
188                ldk.set_gossip_source_p2p();
189            }
190            GossipSource::RapidGossipSync(rgs_url) => {
191                ldk.set_gossip_source_rgs(rgs_url);
192            }
193        }
194
195        ldk.set_listening_addresses(self.listening_addresses)?;
196        if self.log_dir_path.is_some() {
197            ldk.set_filesystem_logger(self.log_dir_path, Some(LogLevel::Info));
198        } else {
199            ldk.set_custom_logger(Arc::new(StdoutLogWriter));
200        }
201
202        ldk.set_node_alias("cdk-ldk-node".to_string())?;
203        // set the seed as bip39 entropy mnemonic
204        if let Some(seed) = self.seed {
205            ldk.set_entropy_bip39_mnemonic(seed, None);
206        }
207        // set the announcement addresses
208        if let Some(announcement_addresses) = self.announcement_addresses {
209            ldk.set_announcement_addresses(announcement_addresses)?;
210        }
211
212        let node = ldk.build()?;
213
214        tracing::info!("Creating tokio channel for payment notifications");
215        let (sender, receiver) = tokio::sync::broadcast::channel(8);
216
217        let id = node.node_id();
218
219        let adr = node.announcement_addresses();
220
221        tracing::info!(
222            "Created node {} with address {:?} on network {}",
223            id,
224            adr,
225            self.network
226        );
227
228        Ok(CdkLdkNode {
229            inner: node.into(),
230            fee_reserve: self.fee_reserve,
231            wait_invoice_cancel_token: CancellationToken::new(),
232            wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
233            sender,
234            receiver: Arc::new(receiver),
235            events_cancel_token: CancellationToken::new(),
236            web_addr: None,
237        })
238    }
239}
240
241impl CdkLdkNode {
242    /// Set the web server address for the LDK node management interface
243    ///
244    /// # Arguments
245    /// * `addr` - Socket address for the web server. If None, no web server will be started.
246    pub fn set_web_addr(&mut self, addr: Option<SocketAddr>) {
247        self.web_addr = addr;
248    }
249
250    /// Get a default web server address using an unused port
251    ///
252    /// Returns a SocketAddr with localhost and port 0, which will cause
253    /// the system to automatically assign an available port
254    pub fn default_web_addr() -> SocketAddr {
255        SocketAddr::from(([127, 0, 0, 1], 8091))
256    }
257
258    fn make_payment_response_from_details(
259        unit: &CurrencyUnit,
260        payment_lookup_id: PaymentIdentifier,
261        payment_details: &PaymentDetails,
262    ) -> Result<MakePaymentResponse, payment::Error> {
263        let status = match payment_details.status {
264            PaymentStatus::Pending => MeltQuoteState::Pending,
265            PaymentStatus::Succeeded => MeltQuoteState::Paid,
266            PaymentStatus::Failed => MeltQuoteState::Failed,
267        };
268
269        let payment_proof = match &payment_details.kind {
270            PaymentKind::Bolt11 { preimage, .. } => preimage.map(|p| p.to_string()),
271            PaymentKind::Bolt12Offer { preimage, .. } => preimage.map(|p| p.to_string()),
272            _ => return Err(Error::UnexpectedPaymentKind.into()),
273        };
274
275        let total_spent = if status == MeltQuoteState::Paid {
276            let total_spent = payment_details
277                .amount_msat
278                .ok_or(Error::CouldNotGetAmountSpent)?
279                + payment_details.fee_paid_msat.unwrap_or_default();
280            Amount::new(total_spent, CurrencyUnit::Msat).convert_to(unit)?
281        } else {
282            Amount::new(0, unit.clone())
283        };
284
285        Ok(MakePaymentResponse {
286            payment_lookup_id,
287            payment_proof,
288            status,
289            total_spent,
290        })
291    }
292
293    fn select_bolt11_payment_details(
294        payment_details: impl IntoIterator<Item = PaymentDetails>,
295    ) -> Option<PaymentDetails> {
296        payment_details.into_iter().min_by_key(|details| {
297            let status_order = match details.status {
298                PaymentStatus::Succeeded => 0_u8,
299                PaymentStatus::Pending => 1,
300                PaymentStatus::Failed => 2,
301            };
302
303            (
304                status_order,
305                std::cmp::Reverse(details.latest_update_timestamp),
306            )
307        })
308    }
309
310    /// Start the CDK LDK Node
311    ///
312    /// Starts the underlying LDK node and begins event processing.
313    /// Sets up event handlers to listen for Lightning events like payment received.
314    ///
315    /// # Returns
316    /// Returns `Ok(())` on successful start, error otherwise
317    ///
318    /// # Errors
319    /// Returns an error if the LDK node fails to start or event handling setup fails
320    pub fn start_ldk_node(&self) -> Result<(), Error> {
321        tracing::info!("Starting cdk-ldk node");
322        self.inner.start()?;
323        let node_config = self.inner.config();
324
325        tracing::info!("Starting node with network {}", node_config.network);
326
327        tracing::info!("Node status: {:?}", self.inner.status());
328
329        self.handle_events()?;
330
331        Ok(())
332    }
333
334    /// Start the web server for the LDK node management interface
335    ///
336    /// Starts a web server that provides a user interface for managing the LDK node.
337    /// The web interface allows users to view balances, manage channels, create invoices,
338    /// and send payments.
339    ///
340    /// # Arguments
341    /// * `web_addr` - The socket address to bind the web server to
342    ///
343    /// # Returns
344    /// Returns `Ok(())` on successful start, error otherwise
345    ///
346    /// # Errors
347    /// Returns an error if the web server fails to start
348    pub fn start_web_server(&self, web_addr: SocketAddr) -> Result<(), Error> {
349        let web_server = crate::web::WebServer::new(Arc::new(self.clone()));
350
351        tokio::spawn(async move {
352            if let Err(e) = web_server.serve(web_addr).await {
353                tracing::error!("Web server error: {}", e);
354            }
355        });
356
357        Ok(())
358    }
359
360    /// Stop the CDK LDK Node
361    ///
362    /// Gracefully stops the node by cancelling all active tasks and event handlers.
363    /// This includes:
364    /// - Cancelling the event handler task
365    /// - Cancelling any active wait_invoice streams
366    /// - Stopping the underlying LDK node
367    ///
368    /// # Returns
369    /// Returns `Ok(())` on successful shutdown, error otherwise
370    ///
371    /// # Errors
372    /// Returns an error if the underlying LDK node fails to stop
373    pub fn stop_ldk_node(&self) -> Result<(), Error> {
374        tracing::info!("Stopping CdkLdkNode");
375        // Cancel all tokio tasks
376        tracing::info!("Cancelling event handler");
377        self.events_cancel_token.cancel();
378
379        // Cancel any payment event streams
380        if self.is_payment_event_stream_active() {
381            tracing::info!("Cancelling payment event stream");
382            self.wait_invoice_cancel_token.cancel();
383        }
384
385        // Stop the LDK node
386        tracing::info!("Stopping LDK node");
387        self.inner.stop()?;
388        tracing::info!("CdkLdkNode stopped successfully");
389        Ok(())
390    }
391
392    /// Handle payment received event
393    async fn handle_payment_received(
394        node: &Arc<Node>,
395        sender: &tokio::sync::broadcast::Sender<WaitPaymentResponse>,
396        payment_id: Option<PaymentId>,
397        payment_hash: PaymentHash,
398        amount_msat: u64,
399    ) {
400        tracing::info!(
401            "Received payment for hash={} of amount={} msat",
402            payment_hash,
403            amount_msat
404        );
405
406        let payment_id = match payment_id {
407            Some(id) => id,
408            None => {
409                tracing::warn!("Received payment without payment_id");
410                return;
411            }
412        };
413
414        let payment_id_hex = hex::encode(payment_id.0);
415
416        if amount_msat == 0 {
417            tracing::warn!("Payment of no amount");
418            return;
419        }
420
421        tracing::info!(
422            "Processing payment notification: id={}, amount={} msats",
423            payment_id_hex,
424            amount_msat
425        );
426
427        let payment_details = match node.payment(&payment_id) {
428            Some(details) => details,
429            None => {
430                tracing::error!("Could not find payment details for id={}", payment_id_hex);
431                return;
432            }
433        };
434
435        let (payment_identifier, payment_id) = match payment_details.kind {
436            PaymentKind::Bolt11 { hash, .. } => {
437                (PaymentIdentifier::PaymentHash(hash.0), hash.to_string())
438            }
439            PaymentKind::Bolt12Offer { hash, offer_id, .. } => match hash {
440                Some(h) => (
441                    PaymentIdentifier::OfferId(offer_id.to_string()),
442                    h.to_string(),
443                ),
444                None => {
445                    tracing::error!("Bolt12 payment missing hash");
446                    return;
447                }
448            },
449            k => {
450                tracing::warn!("Received payment of kind {:?} which is not supported", k);
451                return;
452            }
453        };
454
455        let wait_payment_response = WaitPaymentResponse {
456            payment_identifier,
457            payment_amount: Amount::new(amount_msat, CurrencyUnit::Msat),
458            payment_id,
459        };
460
461        match sender.send(wait_payment_response) {
462            Ok(_) => tracing::info!("Successfully sent payment notification to stream"),
463            Err(err) => tracing::error!(
464                "Could not send payment received notification on channel: {}",
465                err
466            ),
467        }
468    }
469
470    /// Set up event handling for the node
471    pub fn handle_events(&self) -> Result<(), Error> {
472        let node = self.inner.clone();
473        let sender = self.sender.clone();
474        let cancel_token = self.events_cancel_token.clone();
475
476        tracing::info!("Starting event handler task");
477
478        tokio::spawn(async move {
479            tracing::info!("Event handler loop started");
480            loop {
481                tokio::select! {
482                    _ = cancel_token.cancelled() => {
483                        tracing::info!("Event handler cancelled");
484                        break;
485                    }
486                    event = node.next_event_async() => {
487                        match event {
488                            Event::PaymentReceived {
489                                payment_id,
490                                payment_hash,
491                                amount_msat,
492                                custom_records: _
493                            } => {
494                                Self::handle_payment_received(
495                                    &node,
496                                    &sender,
497                                    payment_id,
498                                    payment_hash,
499                                    amount_msat
500                                ).await;
501                            }
502                            Event::PaymentFailed {
503                                payment_id,
504                                payment_hash,
505                                reason,
506                            } => {
507                                tracing::error!(
508                                    payment_id = ?payment_id,
509                                    payment_hash = ?payment_hash,
510                                    reason = ?reason,
511                                    "LDK node payment failed"
512                                );
513                            }
514                            event => {
515                                tracing::debug!("Received other ldk node event: {:?}", event);
516                            }
517                        }
518
519                        if let Err(err) = node.event_handled() {
520                            tracing::error!("Error handling node event: {}", err);
521                        } else {
522                            tracing::debug!("Successfully handled node event");
523                        }
524                    }
525                }
526            }
527            tracing::info!("Event handler loop terminated");
528        });
529
530        tracing::info!("Event handler task spawned");
531        Ok(())
532    }
533
534    /// Get Node used
535    pub fn node(&self) -> Arc<Node> {
536        Arc::clone(&self.inner)
537    }
538}
539
540/// Mint payment trait
541#[async_trait]
542impl MintPayment for CdkLdkNode {
543    type Err = payment::Error;
544
545    /// Start the payment processor
546    /// Starts the LDK node and begins event processing
547    async fn start(&self) -> Result<(), Self::Err> {
548        self.start_ldk_node().map_err(|e| {
549            tracing::error!("Failed to start CdkLdkNode: {}", e);
550            e
551        })?;
552
553        tracing::info!("CdkLdkNode payment processor started successfully");
554
555        // Start web server if configured
556        if let Some(web_addr) = self.web_addr {
557            tracing::info!("Starting LDK Node web interface on {}", web_addr);
558            self.start_web_server(web_addr).map_err(|e| {
559                tracing::error!("Failed to start web server: {}", e);
560                e
561            })?;
562        } else {
563            tracing::info!("No web server address configured, skipping web interface");
564        }
565
566        Ok(())
567    }
568
569    /// Stop the payment processor
570    /// Gracefully stops the LDK node and cancels all background tasks
571    async fn stop(&self) -> Result<(), Self::Err> {
572        self.stop_ldk_node().map_err(|e| {
573            tracing::error!("Failed to stop CdkLdkNode: {}", e);
574            e.into()
575        })
576    }
577
578    /// Base Settings
579    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
580        let settings = SettingsResponse {
581            unit: CurrencyUnit::Msat.to_string(),
582            bolt11: Some(payment::Bolt11Settings {
583                mpp: false,
584                amountless: true,
585                invoice_description: true,
586            }),
587            bolt12: Some(payment::Bolt12Settings { amountless: true }),
588            onchain: None,
589            custom: std::collections::HashMap::new(),
590        };
591        Ok(settings)
592    }
593
594    /// Create a new invoice
595    #[instrument(skip(self))]
596    async fn create_incoming_payment_request(
597        &self,
598        options: IncomingPaymentOptions,
599    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
600        match options {
601            IncomingPaymentOptions::Bolt11(bolt11_options) => {
602                let amount_msat: Amount = bolt11_options
603                    .amount
604                    .convert_to(&CurrencyUnit::Msat)?
605                    .into();
606                let description = bolt11_options.description.unwrap_or_default();
607                let time = match bolt11_options.unix_expiry {
608                    Some(t) => t
609                        .checked_sub(unix_time())
610                        .ok_or(payment::Error::InvalidExpiry)?,
611                    None => 36000,
612                };
613
614                let description = Bolt11InvoiceDescription::Direct(
615                    Description::new(description).map_err(|_| Error::InvalidDescription)?,
616                );
617
618                let payment = self
619                    .inner
620                    .bolt11_payment()
621                    .receive(amount_msat.into(), &description, time as u32)
622                    .map_err(Error::LdkNode)?;
623
624                let payment_hash = payment.payment_hash().to_string();
625                let payment_identifier = PaymentIdentifier::PaymentHash(
626                    hex::decode(&payment_hash)?
627                        .try_into()
628                        .map_err(|_| Error::InvalidPaymentHashLength)?,
629                );
630
631                Ok(CreateIncomingPaymentResponse {
632                    request_lookup_id: payment_identifier,
633                    request: payment.to_string(),
634                    expiry: Some(unix_time() + time),
635                    extra_json: None,
636                })
637            }
638            IncomingPaymentOptions::Bolt12(bolt12_options) => {
639                let Bolt12IncomingPaymentOptions {
640                    description,
641                    amount,
642                    unix_expiry,
643                } = *bolt12_options;
644
645                let time = unix_expiry
646                    .map(|t| {
647                        t.checked_sub(unix_time())
648                            .ok_or(payment::Error::InvalidExpiry)
649                            .map(|t| t as u32)
650                    })
651                    .transpose()?;
652
653                let offer = match amount {
654                    Some(amount) => {
655                        let amount_msat: Amount = amount.convert_to(&CurrencyUnit::Msat)?.into();
656
657                        self.inner
658                            .bolt12_payment()
659                            .receive(
660                                amount_msat.into(),
661                                &description.unwrap_or("".to_string()),
662                                time,
663                                None,
664                            )
665                            .map_err(Error::LdkNode)?
666                    }
667                    None => self
668                        .inner
669                        .bolt12_payment()
670                        .receive_variable_amount(&description.unwrap_or("".to_string()), time)
671                        .map_err(Error::LdkNode)?,
672                };
673                let payment_identifier = PaymentIdentifier::OfferId(offer.id().to_string());
674
675                Ok(CreateIncomingPaymentResponse {
676                    request_lookup_id: payment_identifier,
677                    request: offer.to_string(),
678                    expiry: unix_expiry,
679                    extra_json: None,
680                })
681            }
682            IncomingPaymentOptions::Custom(_) | IncomingPaymentOptions::Onchain(_) => {
683                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
684            }
685        }
686    }
687
688    /// Get payment quote
689    /// Used to get fee and amount required for a payment request
690    #[instrument(skip_all)]
691    async fn get_payment_quote(
692        &self,
693        unit: &CurrencyUnit,
694        options: OutgoingPaymentOptions,
695    ) -> Result<PaymentQuoteResponse, Self::Err> {
696        match options {
697            cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
698                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
699            }
700            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
701                let bolt11 = bolt11_options.bolt11;
702
703                let amount_msat = match bolt11_options.melt_options {
704                    Some(MeltOptions::Amountless { amountless }) => {
705                        let amount_msat = amountless.amount_msat;
706
707                        if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
708                            if invoice_amount != u64::from(amount_msat) {
709                                return Err(payment::Error::AmountMismatch);
710                            }
711                        }
712
713                        amount_msat
714                    }
715                    Some(MeltOptions::Mpp { mpp }) => mpp.amount,
716                    None => bolt11
717                        .amount_milli_satoshis()
718                        .ok_or(Error::UnknownInvoiceAmount)?
719                        .into(),
720                };
721
722                let amount =
723                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
724
725                let relative_fee_reserve =
726                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
727
728                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
729
730                let fee = match relative_fee_reserve > absolute_fee_reserve {
731                    true => relative_fee_reserve,
732                    false => absolute_fee_reserve,
733                };
734
735                let payment_hash = bolt11.payment_hash().to_string();
736                let payment_hash_bytes = hex::decode(&payment_hash)?
737                    .try_into()
738                    .map_err(|_| Error::InvalidPaymentHashLength)?;
739
740                Ok(PaymentQuoteResponse {
741                    request_lookup_id: Some(PaymentIdentifier::PaymentHash(payment_hash_bytes)),
742                    amount,
743                    fee: Amount::new(fee, unit.clone()),
744                    state: MeltQuoteState::Unpaid,
745                    extra_json: None,
746                    estimated_blocks: None,
747                    fee_options: None,
748                })
749            }
750            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
751                let offer = bolt12_options.offer;
752
753                let amount_msat = match bolt12_options.melt_options {
754                    Some(melt_options) => melt_options.amount_msat(),
755                    None => {
756                        let amount = offer.amount().ok_or(payment::Error::AmountMismatch)?;
757
758                        match amount {
759                            ldk_node::lightning::offers::offer::Amount::Bitcoin {
760                                amount_msats,
761                            } => amount_msats.into(),
762                            _ => return Err(payment::Error::AmountMismatch),
763                        }
764                    }
765                };
766                let amount =
767                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
768
769                let relative_fee_reserve =
770                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
771
772                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
773
774                let fee = match relative_fee_reserve > absolute_fee_reserve {
775                    true => relative_fee_reserve,
776                    false => absolute_fee_reserve,
777                };
778
779                Ok(PaymentQuoteResponse {
780                    request_lookup_id: None,
781                    amount,
782                    fee: Amount::new(fee, unit.clone()),
783                    state: MeltQuoteState::Unpaid,
784                    extra_json: None,
785                    estimated_blocks: None,
786                    fee_options: None,
787                })
788            }
789            OutgoingPaymentOptions::Onchain(_) => {
790                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
791            }
792        }
793    }
794
795    /// Pay request
796    #[instrument(skip(self, options))]
797    async fn make_payment(
798        &self,
799        unit: &CurrencyUnit,
800        options: OutgoingPaymentOptions,
801    ) -> Result<MakePaymentResponse, Self::Err> {
802        match options {
803            cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
804                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
805            }
806            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
807                let bolt11 = bolt11_options.bolt11;
808
809                let send_params = match bolt11_options
810                    .max_fee_amount
811                    .map(|f| {
812                        f.convert_to(&CurrencyUnit::Msat)
813                            .map(|amount_msat| RouteParametersConfig {
814                                max_total_routing_fee_msat: Some(amount_msat.value()),
815                                ..Default::default()
816                            })
817                    })
818                    .transpose()
819                {
820                    Ok(params) => params,
821                    Err(err) => {
822                        tracing::error!("Failed to convert fee amount: {}", err);
823                        return Err(payment::Error::Custom(format!("Invalid fee amount: {err}")));
824                    }
825                };
826
827                let payment_id = match bolt11_options.melt_options {
828                    Some(MeltOptions::Amountless { amountless }) => {
829                        if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
830                            if invoice_amount != u64::from(amountless.amount_msat) {
831                                return Err(payment::Error::AmountMismatch);
832                            }
833                        }
834
835                        self.inner
836                            .bolt11_payment()
837                            .send_using_amount(&bolt11, amountless.amount_msat.into(), send_params)
838                            .map_err(|err| {
839                                tracing::error!("Could not send send amountless bolt11: {}", err);
840                                Error::CouldNotSendBolt11WithoutAmount
841                            })?
842                    }
843                    None => self
844                        .inner
845                        .bolt11_payment()
846                        .send(&bolt11, send_params)
847                        .map_err(|err| {
848                            tracing::error!("Could not send bolt11 {}", err);
849                            Error::CouldNotSendBolt11
850                        })?,
851                    _ => return Err(payment::Error::UnsupportedPaymentOption),
852                };
853
854                // Check payment status for up to 10 seconds
855                let start = std::time::Instant::now();
856                let timeout = std::time::Duration::from_secs(10);
857
858                let payment_details = loop {
859                    let details = self
860                        .inner
861                        .payment(&payment_id)
862                        .ok_or(Error::PaymentNotFound)?;
863
864                    match details.status {
865                        PaymentStatus::Succeeded => break details,
866                        PaymentStatus::Failed => {
867                            tracing::error!("Failed to pay bolt11 payment.");
868                            break details;
869                        }
870                        PaymentStatus::Pending => {
871                            if start.elapsed() > timeout {
872                                tracing::warn!(
873                                    "Paying bolt11 exceeded timeout 10 seconds no longer waitning."
874                                );
875                                break details;
876                            }
877                            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
878                            continue;
879                        }
880                    }
881                };
882
883                Self::make_payment_response_from_details(
884                    unit,
885                    PaymentIdentifier::PaymentHash(bolt11.payment_hash().to_byte_array()),
886                    &payment_details,
887                )
888            }
889            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
890                let offer = bolt12_options.offer;
891
892                let send_params = match bolt12_options
893                    .max_fee_amount
894                    .map(|f| {
895                        f.convert_to(&CurrencyUnit::Msat)
896                            .map(|amount_msat| RouteParametersConfig {
897                                max_total_routing_fee_msat: Some(amount_msat.value()),
898                                ..Default::default()
899                            })
900                    })
901                    .transpose()
902                {
903                    Ok(params) => params,
904                    Err(err) => {
905                        tracing::error!("Failed to convert fee amount: {}", err);
906                        return Err(payment::Error::Custom(format!("Invalid fee amount: {err}")));
907                    }
908                };
909
910                let payment_id = match bolt12_options.melt_options {
911                    Some(MeltOptions::Amountless { amountless }) => self
912                        .inner
913                        .bolt12_payment()
914                        .send_using_amount(
915                            &offer,
916                            amountless.amount_msat.into(),
917                            None,
918                            None,
919                            send_params,
920                        )
921                        .map_err(Error::LdkNode)?,
922                    None => self
923                        .inner
924                        .bolt12_payment()
925                        .send(&offer, None, None, send_params)
926                        .map_err(Error::LdkNode)?,
927                    _ => return Err(payment::Error::UnsupportedPaymentOption),
928                };
929
930                // Check payment status for up to 10 seconds
931                let start = std::time::Instant::now();
932                let timeout = std::time::Duration::from_secs(10);
933
934                let payment_details = loop {
935                    let details = self
936                        .inner
937                        .payment(&payment_id)
938                        .ok_or(Error::PaymentNotFound)?;
939
940                    match details.status {
941                        PaymentStatus::Succeeded => break details,
942                        PaymentStatus::Failed => {
943                            tracing::error!(
944                                payment_id = %payment_id,
945                                amount_msat = ?details.amount_msat,
946                                fee_paid_msat = ?details.fee_paid_msat,
947                                payment_kind = ?details.kind,
948                                "Bolt12 payment failed"
949                            );
950                            break details;
951                        }
952                        PaymentStatus::Pending => {
953                            if start.elapsed() > timeout {
954                                tracing::warn!(
955                                    "Payment has been being for 10 seconds. No longer waiting"
956                                );
957                                break details;
958                            }
959                            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
960                            continue;
961                        }
962                    }
963                };
964
965                Self::make_payment_response_from_details(
966                    unit,
967                    PaymentIdentifier::PaymentId(payment_id.0),
968                    &payment_details,
969                )
970            }
971            OutgoingPaymentOptions::Onchain(_) => {
972                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
973            }
974        }
975    }
976
977    /// Listen for invoices to be paid to the mint
978    /// Returns a stream of request_lookup_id once invoices are paid
979    #[instrument(skip(self))]
980    async fn wait_payment_event(
981        &self,
982    ) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
983        tracing::info!("Starting stream for invoices - wait_any_incoming_payment called");
984
985        // Set active flag to indicate stream is active
986        self.wait_invoice_is_active.store(true, Ordering::SeqCst);
987        tracing::debug!("wait_invoice_is_active set to true");
988
989        let receiver = self.receiver.clone();
990
991        tracing::info!("Receiver obtained successfully, creating response stream");
992
993        // Transform the String stream into a WaitPaymentResponse stream
994        let response_stream = BroadcastStream::new(receiver.resubscribe());
995
996        // Map the stream to handle BroadcastStreamRecvError and wrap in Event
997        let response_stream = response_stream.filter_map(|result| async move {
998            match result {
999                Ok(payment) => Some(cdk_common::payment::Event::PaymentReceived(payment)),
1000                Err(err) => {
1001                    tracing::warn!("Error in broadcast stream: {}", err);
1002                    None
1003                }
1004            }
1005        });
1006
1007        // Create a combined stream that also handles cancellation
1008        let cancel_token = self.wait_invoice_cancel_token.clone();
1009        let is_active = self.wait_invoice_is_active.clone();
1010
1011        let stream = Box::pin(response_stream);
1012
1013        // Set up a task to clean up when the stream is dropped
1014        tokio::spawn(async move {
1015            cancel_token.cancelled().await;
1016            tracing::info!("wait_invoice stream cancelled");
1017            is_active.store(false, Ordering::SeqCst);
1018        });
1019
1020        tracing::info!("wait_any_incoming_payment returning stream");
1021        Ok(stream)
1022    }
1023
1024    /// Is payment event stream active
1025    fn is_payment_event_stream_active(&self) -> bool {
1026        self.wait_invoice_is_active.load(Ordering::SeqCst)
1027    }
1028
1029    /// Cancel payment event stream
1030    fn cancel_payment_event_stream(&self) {
1031        self.wait_invoice_cancel_token.cancel()
1032    }
1033
1034    /// Check the status of an incoming payment
1035    async fn check_incoming_payment_status(
1036        &self,
1037        payment_identifier: &PaymentIdentifier,
1038    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
1039        // Bolt12 offers are identified by offer id and can be paid more than
1040        // once, so collect every settled inbound payment for the offer.
1041        if let PaymentIdentifier::OfferId(offer_id) = payment_identifier {
1042            let payments = self.inner.list_payments_with_filter(|p| {
1043                p.direction == PaymentDirection::Inbound
1044                    && p.status == PaymentStatus::Succeeded
1045                    && matches!(
1046                        &p.kind,
1047                        PaymentKind::Bolt12Offer { offer_id: oid, .. } if oid.to_string() == *offer_id
1048                    )
1049            });
1050
1051            return Ok(payments
1052                .into_iter()
1053                .filter_map(|p| {
1054                    let payment_id = match &p.kind {
1055                        PaymentKind::Bolt12Offer {
1056                            hash: Some(hash), ..
1057                        } => hash.to_string(),
1058                        _ => {
1059                            tracing::warn!("Bolt12 payment for offer {} missing hash", offer_id);
1060                            return None;
1061                        }
1062                    };
1063
1064                    Some(WaitPaymentResponse {
1065                        payment_identifier: payment_identifier.clone(),
1066                        payment_amount: Amount::new(p.amount_msat?, CurrencyUnit::Msat),
1067                        payment_id,
1068                    })
1069                })
1070                .collect());
1071        }
1072
1073        let payment_id_str = match payment_identifier {
1074            PaymentIdentifier::PaymentHash(hash) => hex::encode(hash),
1075            PaymentIdentifier::CustomId(id) => id.clone(),
1076            _ => return Err(Error::UnsupportedPaymentIdentifierType.into()),
1077        };
1078
1079        let payment_id = PaymentId(
1080            hex::decode(&payment_id_str)?
1081                .try_into()
1082                .map_err(|_| Error::InvalidPaymentIdLength)?,
1083        );
1084
1085        let payment_details = self
1086            .inner
1087            .payment(&payment_id)
1088            .ok_or(Error::PaymentNotFound)?;
1089
1090        if payment_details.direction == PaymentDirection::Outbound {
1091            return Err(Error::InvalidPaymentDirection.into());
1092        }
1093
1094        let amount = if payment_details.status == PaymentStatus::Succeeded {
1095            payment_details
1096                .amount_msat
1097                .ok_or(Error::CouldNotGetPaymentAmount)?
1098        } else {
1099            return Ok(vec![]);
1100        };
1101
1102        let response = WaitPaymentResponse {
1103            payment_identifier: payment_identifier.clone(),
1104            payment_amount: Amount::new(amount, CurrencyUnit::Msat),
1105            payment_id: payment_id_str,
1106        };
1107
1108        Ok(vec![response])
1109    }
1110
1111    /// Check the status of an outgoing payment
1112    async fn check_outgoing_payment(
1113        &self,
1114        request_lookup_id: &PaymentIdentifier,
1115    ) -> Result<MakePaymentResponse, Self::Err> {
1116        let payment_details = match request_lookup_id {
1117            PaymentIdentifier::PaymentHash(id_hash) => {
1118                Self::select_bolt11_payment_details(self.inner.list_payments_with_filter(|p| {
1119                    p.direction == PaymentDirection::Outbound
1120                        && matches!(&p.kind, PaymentKind::Bolt11 { hash, .. } if &hash.0 == id_hash)
1121                }))
1122            }
1123            PaymentIdentifier::PaymentId(id) => self.inner.payment(&PaymentId(*id)),
1124            _ => {
1125                return Ok(MakePaymentResponse {
1126                    payment_lookup_id: request_lookup_id.clone(),
1127                    payment_proof: None,
1128                    status: MeltQuoteState::Unknown,
1129                    total_spent: Amount::new(0, CurrencyUnit::Msat),
1130                });
1131            }
1132        }
1133        .ok_or(Error::PaymentNotFound)?;
1134
1135        if payment_details.direction != PaymentDirection::Outbound {
1136            return Err(Error::InvalidPaymentDirection.into());
1137        }
1138
1139        Self::make_payment_response_from_details(
1140            &CurrencyUnit::Msat,
1141            request_lookup_id.clone(),
1142            &payment_details,
1143        )
1144    }
1145}
1146
1147impl Drop for CdkLdkNode {
1148    fn drop(&mut self) {
1149        tracing::info!("Drop called on CdkLdkNode");
1150        self.wait_invoice_cancel_token.cancel();
1151        tracing::debug!("Cancelled wait_invoice token in drop");
1152    }
1153}
1154
1155#[cfg(test)]
1156mod tests {
1157    use super::*;
1158
1159    fn test_payment_details(status: PaymentStatus, amount_msat: Option<u64>) -> PaymentDetails {
1160        PaymentDetails {
1161            id: PaymentId([2; 32]),
1162            kind: PaymentKind::Bolt11 {
1163                hash: PaymentHash([1; 32]),
1164                preimage: None,
1165                secret: None,
1166            },
1167            amount_msat,
1168            fee_paid_msat: None,
1169            direction: PaymentDirection::Outbound,
1170            status,
1171            latest_update_timestamp: 0,
1172        }
1173    }
1174
1175    fn test_payment_details_with_id(
1176        id: [u8; 32],
1177        status: PaymentStatus,
1178        latest_update_timestamp: u64,
1179    ) -> PaymentDetails {
1180        PaymentDetails {
1181            id: PaymentId(id),
1182            latest_update_timestamp,
1183            ..test_payment_details(status, None)
1184        }
1185    }
1186
1187    #[test]
1188    fn failed_payment_response_does_not_require_amount() {
1189        let details = test_payment_details(PaymentStatus::Failed, None);
1190
1191        let response = CdkLdkNode::make_payment_response_from_details(
1192            &CurrencyUnit::Msat,
1193            PaymentIdentifier::PaymentId([2; 32]),
1194            &details,
1195        )
1196        .expect("failed payment details should map without amount");
1197
1198        assert_eq!(response.status, MeltQuoteState::Failed);
1199        assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1200    }
1201
1202    #[test]
1203    fn pending_payment_response_does_not_require_amount() {
1204        let details = test_payment_details(PaymentStatus::Pending, None);
1205
1206        let response = CdkLdkNode::make_payment_response_from_details(
1207            &CurrencyUnit::Msat,
1208            PaymentIdentifier::PaymentId([2; 32]),
1209            &details,
1210        )
1211        .expect("pending payment details should map without amount");
1212
1213        assert_eq!(response.status, MeltQuoteState::Pending);
1214        assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1215    }
1216
1217    #[test]
1218    fn paid_payment_response_requires_amount() {
1219        let details = test_payment_details(PaymentStatus::Succeeded, None);
1220
1221        let err = CdkLdkNode::make_payment_response_from_details(
1222            &CurrencyUnit::Msat,
1223            PaymentIdentifier::PaymentId([2; 32]),
1224            &details,
1225        )
1226        .expect_err("paid payment details without amount should fail");
1227
1228        assert!(matches!(err, payment::Error::Lightning(_)));
1229    }
1230
1231    #[test]
1232    fn bolt11_payment_selection_prefers_pending_over_failed() {
1233        let failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 2);
1234        let pending = test_payment_details_with_id([2; 32], PaymentStatus::Pending, 1);
1235
1236        let selected = CdkLdkNode::select_bolt11_payment_details([failed, pending])
1237            .expect("payment details should be selected");
1238
1239        assert_eq!(selected.id, PaymentId([2; 32]));
1240        assert_eq!(selected.status, PaymentStatus::Pending);
1241    }
1242
1243    #[test]
1244    fn bolt11_payment_selection_prefers_succeeded_over_pending() {
1245        let pending = test_payment_details_with_id([1; 32], PaymentStatus::Pending, 2);
1246        let succeeded = PaymentDetails {
1247            amount_msat: Some(1000),
1248            ..test_payment_details_with_id([2; 32], PaymentStatus::Succeeded, 1)
1249        };
1250
1251        let selected = CdkLdkNode::select_bolt11_payment_details([pending, succeeded])
1252            .expect("payment details should be selected");
1253
1254        assert_eq!(selected.id, PaymentId([2; 32]));
1255        assert_eq!(selected.status, PaymentStatus::Succeeded);
1256    }
1257
1258    #[test]
1259    fn bolt11_payment_selection_uses_latest_failed_when_all_failed() {
1260        let older_failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 1);
1261        let newer_failed = test_payment_details_with_id([2; 32], PaymentStatus::Failed, 2);
1262
1263        let selected = CdkLdkNode::select_bolt11_payment_details([older_failed, newer_failed])
1264            .expect("payment details should be selected");
1265
1266        assert_eq!(selected.id, PaymentId([2; 32]));
1267        assert_eq!(selected.status, PaymentStatus::Failed);
1268    }
1269}