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::fmt;
6use std::net::SocketAddr;
7use std::pin::Pin;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10use std::time::Duration;
11
12use async_trait::async_trait;
13use bip39::Mnemonic;
14use cdk_common::common::FeeReserve;
15use cdk_common::database::DynKVStore;
16use cdk_common::payment::{self, *};
17use cdk_common::redact::url_for_logs;
18use cdk_common::util::{hex, unix_time};
19use cdk_common::{Amount, CurrencyUnit, MeltOptions, MeltQuoteState, QuoteId};
20use futures::{Stream, StreamExt};
21use ldk_node::bitcoin::hashes::Hash;
22use ldk_node::bitcoin::Network;
23use ldk_node::lightning::ln::channelmanager::PaymentId;
24use ldk_node::lightning::ln::msgs::SocketAddress;
25use ldk_node::lightning::routing::router::RouteParametersConfig;
26use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description};
27use ldk_node::lightning_types::payment::PaymentHash;
28use ldk_node::logger::{LogLevel, LogWriter};
29use ldk_node::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
30use ldk_node::{Builder, Event, Node};
31use tokio_stream::wrappers::BroadcastStream;
32use tokio_util::sync::CancellationToken;
33use tracing::instrument;
34
35use crate::error::Error;
36use crate::log::StdoutLogWriter;
37
38mod error;
39mod log;
40mod web;
41
42/// Primary KV namespace for the ldk-node backend's durable bookkeeping
43const LDK_KV_PRIMARY_NAMESPACE: &str = "cdk_ldk_node_lightning_backend";
44/// Secondary KV namespace holding the bolt12 melt quote id -> payment id
45/// mapping used to resolve `PaymentIdentifier::QuoteId` lookups
46const LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE: &str = "bolt12_outgoing_payments";
47/// Maximum time a synchronous payment request waits for an LDK terminal event
48const PAYMENT_WAIT_TIMEOUT: Duration = Duration::from_secs(10);
49/// Capacity for terminal outgoing payment notifications
50const PAYMENT_EVENT_CHANNEL_CAPACITY: usize = 64;
51const LDK_KV_BOLT12_CLEANUP_MARKER: &[u8] = b"cleanup-in-progress";
52
53/// Result of looking up the payment id recorded for a bolt12 melt quote
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55enum Bolt12QuotePaymentIdLookup {
56    /// A payment id was recorded: the payment was dispatched and is tracked
57    Found(PaymentId),
58    /// The dispatch sentinel is present: `send` was started but no payment id
59    /// was recorded (crash during dispatch, or dispatch errored before the
60    /// sentinel could be cleaned up). The payment state is indeterminate.
61    Dispatching,
62    /// No record exists: the payment was never dispatched
63    Missing,
64    /// A record exists but cannot be parsed
65    Malformed,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69enum Bolt12QuotePaymentIdResolution {
70    PaymentId(PaymentId),
71    Status(MeltQuoteState),
72}
73
74impl Bolt12QuotePaymentIdLookup {
75    fn resolve(self) -> Bolt12QuotePaymentIdResolution {
76        match self {
77            Self::Found(payment_id) => Bolt12QuotePaymentIdResolution::PaymentId(payment_id),
78            // Dispatch was attempted but no payment id was recorded. Pending
79            // prevents the live melt saga from compensating an indeterminate
80            // payment after a dispatch-ambiguous error.
81            Self::Dispatching => Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Pending),
82            // Without the pre-dispatch sentinel, the payment was never sent.
83            Self::Missing => Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unpaid),
84            // Corrupt bookkeeping cannot establish any payment state.
85            Self::Malformed => Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unknown),
86        }
87    }
88}
89
90/// Whether an LDK BOLT12 send error can occur after dispatch was accepted.
91///
92/// In `ldk-node` 0.7, [`ldk_node::NodeError::PersistenceFailed`] can be returned
93/// while persisting the payment record after `ChannelManager::pay_for_offer`
94/// accepted the payment. All other errors returned by the BOLT12 send methods
95/// occur before dispatch or after `pay_for_offer` rejected the attempt.
96fn bolt12_send_error_has_ambiguous_dispatch(err: &ldk_node::NodeError) -> bool {
97    matches!(err, ldk_node::NodeError::PersistenceFailed)
98}
99
100/// Whether a BOLT11 send error authoritatively proves that this invocation
101/// cannot settle.
102///
103/// `PersistenceFailed` is ambiguous because `ldk-node` may return it after the
104/// channel manager accepted the payment but before the pending payment record
105/// was persisted. `DuplicatePayment` is also non-terminal for this invocation:
106/// the existing payment may already be pending or succeeded. The remaining
107/// errors listed here are returned only when dispatch was rejected.
108fn bolt11_send_error_is_explicit_terminal_failure(err: &ldk_node::NodeError) -> bool {
109    matches!(
110        err,
111        ldk_node::NodeError::NotRunning
112            | ldk_node::NodeError::InvalidAmount
113            | ldk_node::NodeError::InvalidInvoice
114            | ldk_node::NodeError::PaymentSendingFailed
115    )
116}
117
118fn outgoing_payment_failure_response(
119    unit: &CurrencyUnit,
120    payment_lookup_id: PaymentIdentifier,
121) -> MakePaymentResponse {
122    MakePaymentResponse {
123        payment_lookup_id,
124        payment_proof: None,
125        status: MeltQuoteState::Failed,
126        total_spent: Amount::new(0, unit.clone()),
127    }
128}
129
130/// CDK Lightning backend using LDK Node
131///
132/// Provides Lightning Network functionality for CDK with support for Cashu operations.
133/// Handles payment creation, processing, and event management using the Lightning Development Kit.
134#[derive(Clone)]
135pub struct CdkLdkNode {
136    inner: Arc<Node>,
137    fee_reserve: FeeReserve,
138    kv_store: DynKVStore,
139    wait_invoice_cancel_token: CancellationToken,
140    wait_invoice_is_active: Arc<AtomicBool>,
141    sender: tokio::sync::broadcast::Sender<WaitPaymentResponse>,
142    receiver: Arc<tokio::sync::broadcast::Receiver<WaitPaymentResponse>>,
143    outgoing_payment_sender: tokio::sync::broadcast::Sender<PaymentId>,
144    events_cancel_token: CancellationToken,
145    web_addr: Option<SocketAddr>,
146}
147
148impl fmt::Debug for CdkLdkNode {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.debug_struct("CdkLdkNode")
151            .field("fee_reserve", &self.fee_reserve)
152            .field("web_addr", &self.web_addr)
153            .finish_non_exhaustive()
154    }
155}
156
157/// Configuration for connecting to Bitcoin RPC
158///
159/// Contains the necessary connection parameters for Bitcoin Core RPC interface.
160#[derive(Clone)]
161pub struct BitcoinRpcConfig {
162    /// Bitcoin RPC server hostname or IP address
163    pub host: String,
164    /// Bitcoin RPC server port number
165    pub port: u16,
166    /// Username for Bitcoin RPC authentication
167    pub user: String,
168    /// Password for Bitcoin RPC authentication
169    pub password: String,
170}
171
172impl fmt::Debug for BitcoinRpcConfig {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        f.debug_struct("BitcoinRpcConfig")
175            .field("host", &self.host)
176            .field("port", &self.port)
177            .field("user", &self.user)
178            .field("password", &"[REDACTED]")
179            .finish()
180    }
181}
182
183/// Source of blockchain data for the Lightning node
184///
185/// Specifies how the node should connect to the Bitcoin network to retrieve
186/// blockchain information and broadcast transactions.
187#[derive(Clone)]
188pub enum ChainSource {
189    /// Use an Esplora server for blockchain data
190    ///
191    /// Contains the URL of the Esplora server endpoint
192    Esplora(String),
193    /// Use an Electrum server for blockchain data
194    ///
195    /// Contains the URL of the Electrum server endpoint
196    Electrum(String),
197    /// Use Bitcoin Core RPC for blockchain data
198    ///
199    /// Contains the configuration for connecting to Bitcoin Core
200    BitcoinRpc(BitcoinRpcConfig),
201}
202
203impl fmt::Debug for ChainSource {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match self {
206            Self::Esplora(url) => f.debug_tuple("Esplora").field(&url_for_logs(url)).finish(),
207            Self::Electrum(url) => f.debug_tuple("Electrum").field(&url_for_logs(url)).finish(),
208            Self::BitcoinRpc(config) => f.debug_tuple("BitcoinRpc").field(config).finish(),
209        }
210    }
211}
212
213/// Source of Lightning network gossip data
214///
215/// Specifies how the node should learn about the Lightning Network topology
216/// and routing information.
217#[derive(Clone)]
218pub enum GossipSource {
219    /// Learn gossip through peer-to-peer connections
220    ///
221    /// The node will connect to other Lightning nodes and exchange gossip data directly
222    P2P,
223    /// Use Rapid Gossip Sync for efficient gossip updates
224    ///
225    /// Contains the URL of the RGS server for compressed gossip data
226    RapidGossipSync(String),
227}
228
229impl fmt::Debug for GossipSource {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        match self {
232            Self::P2P => f.write_str("P2P"),
233            Self::RapidGossipSync(url) => f
234                .debug_tuple("RapidGossipSync")
235                .field(&url_for_logs(url))
236                .finish(),
237        }
238    }
239}
240/// A builder for an [`CdkLdkNode`] instance.
241pub struct CdkLdkNodeBuilder {
242    network: Network,
243    chain_source: ChainSource,
244    gossip_source: GossipSource,
245    log_dir_path: Option<String>,
246    storage_dir_path: String,
247    fee_reserve: FeeReserve,
248    kv_store: DynKVStore,
249    listening_addresses: Vec<SocketAddress>,
250    seed: Option<Mnemonic>,
251    announcement_addresses: Option<Vec<SocketAddress>>,
252}
253
254impl std::fmt::Debug for CdkLdkNodeBuilder {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        f.debug_struct("CdkLdkNodeBuilder")
257            .field("network", &self.network)
258            .field("chain_source", &self.chain_source)
259            .field("gossip_source", &self.gossip_source)
260            .field("log_dir_path", &self.log_dir_path)
261            .field("storage_dir_path", &self.storage_dir_path)
262            .field("fee_reserve", &self.fee_reserve)
263            .field("listening_addresses", &self.listening_addresses)
264            .field("announcement_addresses", &self.announcement_addresses)
265            .finish_non_exhaustive()
266    }
267}
268
269impl CdkLdkNodeBuilder {
270    /// Creates a new builder instance.
271    pub fn new(
272        network: Network,
273        chain_source: ChainSource,
274        gossip_source: GossipSource,
275        storage_dir_path: String,
276        fee_reserve: FeeReserve,
277        listening_addresses: Vec<SocketAddress>,
278        kv_store: DynKVStore,
279    ) -> Self {
280        Self {
281            network,
282            chain_source,
283            gossip_source,
284            storage_dir_path,
285            fee_reserve,
286            kv_store,
287            listening_addresses,
288            seed: None,
289            announcement_addresses: None,
290            log_dir_path: None,
291        }
292    }
293
294    /// Configures the [`CdkLdkNode`] to use the Mnemonic for entropy source configuration
295    pub fn with_seed(mut self, seed: Mnemonic) -> Self {
296        self.seed = Some(seed);
297        self
298    }
299    /// Configures the [`CdkLdkNode`] to use announce this address to the lightning network
300    pub fn with_announcement_address(mut self, announcement_addresses: Vec<SocketAddress>) -> Self {
301        self.announcement_addresses = Some(announcement_addresses);
302        self
303    }
304    /// Configures the [`CdkLdkNode`] to use announce this address to the lightning network
305    pub fn with_log_dir_path(mut self, log_dir_path: String) -> Self {
306        self.log_dir_path = Some(log_dir_path);
307        self
308    }
309
310    /// Builds the [`CdkLdkNode`] instance
311    ///
312    /// # Errors
313    /// Returns an error if the LDK node builder fails to create the node
314    pub fn build(self) -> Result<CdkLdkNode, Error> {
315        let mut ldk = Builder::new();
316        ldk.set_network(self.network);
317        tracing::info!("Storage dir of node is {}", self.storage_dir_path);
318        ldk.set_storage_dir_path(self.storage_dir_path);
319
320        match self.chain_source {
321            ChainSource::Esplora(esplora_url) => {
322                ldk.set_chain_source_esplora(esplora_url, None);
323            }
324            ChainSource::Electrum(electrum_url) => {
325                ldk.set_chain_source_electrum(electrum_url, None);
326            }
327            ChainSource::BitcoinRpc(BitcoinRpcConfig {
328                host,
329                port,
330                user,
331                password,
332            }) => {
333                ldk.set_chain_source_bitcoind_rpc(host, port, user, password);
334            }
335        }
336
337        match self.gossip_source {
338            GossipSource::P2P => {
339                ldk.set_gossip_source_p2p();
340            }
341            GossipSource::RapidGossipSync(rgs_url) => {
342                ldk.set_gossip_source_rgs(rgs_url);
343            }
344        }
345
346        ldk.set_listening_addresses(self.listening_addresses)?;
347        if self.log_dir_path.is_some() {
348            ldk.set_filesystem_logger(self.log_dir_path, Some(LogLevel::Info));
349        } else {
350            ldk.set_custom_logger(Arc::new(StdoutLogWriter));
351        }
352
353        ldk.set_node_alias("cdk-ldk-node".to_string())?;
354        // set the seed as bip39 entropy mnemonic
355        if let Some(seed) = self.seed {
356            ldk.set_entropy_bip39_mnemonic(seed, None);
357        }
358        // set the announcement addresses
359        if let Some(announcement_addresses) = self.announcement_addresses {
360            ldk.set_announcement_addresses(announcement_addresses)?;
361        }
362
363        let node = ldk.build()?;
364
365        tracing::info!("Creating tokio channel for payment notifications");
366        let (sender, receiver) = tokio::sync::broadcast::channel(8);
367        let (outgoing_payment_sender, _) =
368            tokio::sync::broadcast::channel(PAYMENT_EVENT_CHANNEL_CAPACITY);
369
370        let id = node.node_id();
371
372        let adr = node.announcement_addresses();
373
374        tracing::info!(
375            "Created node {} with address {:?} on network {}",
376            id,
377            adr,
378            self.network
379        );
380
381        Ok(CdkLdkNode {
382            inner: node.into(),
383            fee_reserve: self.fee_reserve,
384            kv_store: self.kv_store,
385            wait_invoice_cancel_token: CancellationToken::new(),
386            wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
387            sender,
388            receiver: Arc::new(receiver),
389            outgoing_payment_sender,
390            events_cancel_token: CancellationToken::new(),
391            web_addr: None,
392        })
393    }
394}
395
396impl CdkLdkNode {
397    /// Set the web server address for the LDK node management interface
398    ///
399    /// # Arguments
400    /// * `addr` - Socket address for the web server. If None, no web server will be started.
401    pub fn set_web_addr(&mut self, addr: Option<SocketAddr>) {
402        self.web_addr = addr;
403    }
404
405    /// Get a default web server address using an unused port
406    ///
407    /// Returns a SocketAddr with localhost and port 0, which will cause
408    /// the system to automatically assign an available port
409    pub fn default_web_addr() -> SocketAddr {
410        SocketAddr::from(([127, 0, 0, 1], 8091))
411    }
412
413    /// Best-effort release of an exact sentinel or payment-id binding that is
414    /// known not to represent an in-flight or successful payment.
415    async fn cleanup_bolt12_dispatch_binding(
416        &self,
417        quote_id: &QuoteId,
418        payment_id: Option<&PaymentId>,
419    ) {
420        match delete_bolt12_quote_payment_id_if_equals(&self.kv_store, quote_id, payment_id).await {
421            Ok(true) => {}
422            Ok(false) => {
423                tracing::debug!(
424                    quote_id = %quote_id,
425                    "BOLT12 dispatch binding changed before cleanup"
426                );
427            }
428            Err(err) => {
429                tracing::warn!(
430                    quote_id = %quote_id,
431                    "Could not release BOLT12 dispatch binding: {err}"
432                );
433            }
434        }
435    }
436
437    fn make_payment_response_from_details(
438        unit: &CurrencyUnit,
439        payment_lookup_id: PaymentIdentifier,
440        payment_details: &PaymentDetails,
441    ) -> Result<MakePaymentResponse, payment::Error> {
442        let status = match payment_details.status {
443            PaymentStatus::Pending => MeltQuoteState::Pending,
444            PaymentStatus::Succeeded => MeltQuoteState::Paid,
445            PaymentStatus::Failed => MeltQuoteState::Failed,
446        };
447
448        let payment_proof = match &payment_details.kind {
449            PaymentKind::Bolt11 { preimage, .. } => preimage.map(|p| p.to_string()),
450            PaymentKind::Bolt12Offer { preimage, .. } => preimage.map(|p| p.to_string()),
451            _ => return Err(Error::UnexpectedPaymentKind.into()),
452        };
453
454        let total_spent = if status == MeltQuoteState::Paid {
455            let total_spent = payment_details
456                .amount_msat
457                .ok_or(Error::CouldNotGetAmountSpent)?
458                + payment_details.fee_paid_msat.unwrap_or_default();
459            Amount::new(total_spent, CurrencyUnit::Msat).convert_to(unit)?
460        } else {
461            Amount::new(0, unit.clone())
462        };
463
464        Ok(MakePaymentResponse {
465            payment_lookup_id,
466            payment_proof,
467            status,
468            total_spent,
469        })
470    }
471
472    fn select_bolt11_payment_details(
473        payment_details: impl IntoIterator<Item = PaymentDetails>,
474    ) -> Option<PaymentDetails> {
475        payment_details.into_iter().min_by_key(|details| {
476            let status_order = match details.status {
477                PaymentStatus::Succeeded => 0_u8,
478                PaymentStatus::Pending => 1,
479                PaymentStatus::Failed => 2,
480            };
481
482            (
483                status_order,
484                std::cmp::Reverse(details.latest_update_timestamp),
485            )
486        })
487    }
488
489    async fn wait_for_terminal_payment_event(
490        receiver: &mut tokio::sync::broadcast::Receiver<PaymentId>,
491        payment_id: PaymentId,
492    ) -> Result<(), tokio::sync::broadcast::error::RecvError> {
493        loop {
494            match receiver.recv().await {
495                Ok(completed_payment_id) if completed_payment_id == payment_id => return Ok(()),
496                Ok(_) => continue,
497                Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
498                    tracing::warn!(
499                        payment_id = %payment_id,
500                        skipped,
501                        "Terminal payment event receiver lagged; continuing to wait"
502                    );
503                }
504                Err(err) => return Err(err),
505            }
506        }
507    }
508
509    async fn wait_for_payment_terminal_status(
510        &self,
511        payment_id: PaymentId,
512        mut receiver: tokio::sync::broadcast::Receiver<PaymentId>,
513    ) -> Result<PaymentDetails, payment::Error> {
514        let payment_details = self
515            .inner
516            .payment(&payment_id)
517            .ok_or(Error::PaymentNotFound)?;
518
519        if payment_details.status != PaymentStatus::Pending {
520            return Ok(payment_details);
521        }
522
523        match tokio::time::timeout(
524            PAYMENT_WAIT_TIMEOUT,
525            Self::wait_for_terminal_payment_event(&mut receiver, payment_id),
526        )
527        .await
528        {
529            Ok(Ok(())) => {}
530            Ok(Err(err)) => {
531                tracing::warn!(
532                    payment_id = %payment_id,
533                    "Could not wait for terminal LDK payment event: {err}"
534                );
535            }
536            Err(_) => {
537                tracing::warn!(
538                    payment_id = %payment_id,
539                    "Payment did not reach a terminal state within {} seconds",
540                    PAYMENT_WAIT_TIMEOUT.as_secs()
541                );
542            }
543        }
544
545        let payment_details = self
546            .inner
547            .payment(&payment_id)
548            .ok_or(Error::PaymentNotFound)?;
549
550        if payment_details.status == PaymentStatus::Pending {
551            tracing::debug!(
552                payment_id = %payment_id,
553                "Payment remains pending after waiting for a terminal event"
554            );
555        }
556
557        Ok(payment_details)
558    }
559
560    /// Start the CDK LDK Node
561    ///
562    /// Starts the underlying LDK node and begins event processing.
563    /// Sets up event handlers to listen for Lightning events like payment received.
564    ///
565    /// # Returns
566    /// Returns `Ok(())` on successful start, error otherwise
567    ///
568    /// # Errors
569    /// Returns an error if the LDK node fails to start or event handling setup fails
570    pub fn start_ldk_node(&self) -> Result<(), Error> {
571        tracing::info!("Starting cdk-ldk node");
572        self.inner.start()?;
573        let node_config = self.inner.config();
574
575        tracing::info!("Starting node with network {}", node_config.network);
576
577        tracing::info!("Node status: {:?}", self.inner.status());
578
579        self.handle_events()?;
580
581        Ok(())
582    }
583
584    /// Start the web server for the LDK node management interface
585    ///
586    /// Starts a web server that provides a user interface for managing the LDK node.
587    /// The web interface allows users to view balances, manage channels, create invoices,
588    /// and send payments.
589    ///
590    /// # Arguments
591    /// * `web_addr` - The socket address to bind the web server to
592    ///
593    /// # Returns
594    /// Returns `Ok(())` on successful start, error otherwise
595    ///
596    /// # Errors
597    /// Returns an error if the web server fails to start
598    pub fn start_web_server(&self, web_addr: SocketAddr) -> Result<(), Error> {
599        let web_server = crate::web::WebServer::new(Arc::new(self.clone()));
600
601        tokio::spawn(async move {
602            if let Err(e) = web_server.serve(web_addr).await {
603                tracing::error!("Web server error: {}", e);
604            }
605        });
606
607        Ok(())
608    }
609
610    /// Stop the CDK LDK Node
611    ///
612    /// Gracefully stops the node by cancelling all active tasks and event handlers.
613    /// This includes:
614    /// - Cancelling the event handler task
615    /// - Cancelling any active wait_invoice streams
616    /// - Stopping the underlying LDK node
617    ///
618    /// # Returns
619    /// Returns `Ok(())` on successful shutdown, error otherwise
620    ///
621    /// # Errors
622    /// Returns an error if the underlying LDK node fails to stop
623    pub fn stop_ldk_node(&self) -> Result<(), Error> {
624        tracing::info!("Stopping CdkLdkNode");
625        // Cancel all tokio tasks
626        tracing::info!("Cancelling event handler");
627        self.events_cancel_token.cancel();
628
629        // Cancel any payment event streams
630        if self.is_payment_event_stream_active() {
631            tracing::info!("Cancelling payment event stream");
632            self.wait_invoice_cancel_token.cancel();
633        }
634
635        // Stop the LDK node
636        tracing::info!("Stopping LDK node");
637        self.inner.stop()?;
638        tracing::info!("CdkLdkNode stopped successfully");
639        Ok(())
640    }
641
642    /// Handle payment received event
643    async fn handle_payment_received(
644        node: &Arc<Node>,
645        sender: &tokio::sync::broadcast::Sender<WaitPaymentResponse>,
646        payment_id: Option<PaymentId>,
647        payment_hash: PaymentHash,
648        amount_msat: u64,
649    ) {
650        tracing::info!(
651            "Received payment for hash={} of amount={} msat",
652            payment_hash,
653            amount_msat
654        );
655
656        let payment_id = match payment_id {
657            Some(id) => id,
658            None => {
659                tracing::warn!("Received payment without payment_id");
660                return;
661            }
662        };
663
664        let payment_id_hex = hex::encode(payment_id.0);
665
666        if amount_msat == 0 {
667            tracing::warn!("Payment of no amount");
668            return;
669        }
670
671        tracing::info!(
672            "Processing payment notification: id={}, amount={} msats",
673            payment_id_hex,
674            amount_msat
675        );
676
677        let payment_details = match node.payment(&payment_id) {
678            Some(details) => details,
679            None => {
680                tracing::error!("Could not find payment details for id={}", payment_id_hex);
681                return;
682            }
683        };
684
685        let (payment_identifier, payment_id) = match payment_details.kind {
686            PaymentKind::Bolt11 { hash, .. } => {
687                (PaymentIdentifier::PaymentHash(hash.0), hash.to_string())
688            }
689            PaymentKind::Bolt12Offer { hash, offer_id, .. } => match hash {
690                Some(h) => (
691                    PaymentIdentifier::OfferId(offer_id.to_string()),
692                    h.to_string(),
693                ),
694                None => {
695                    tracing::error!("Bolt12 payment missing hash");
696                    return;
697                }
698            },
699            k => {
700                tracing::warn!("Received payment of kind {:?} which is not supported", k);
701                return;
702            }
703        };
704
705        let wait_payment_response = WaitPaymentResponse {
706            payment_identifier,
707            payment_amount: Amount::new(amount_msat, CurrencyUnit::Msat),
708            payment_id,
709        };
710
711        match sender.send(wait_payment_response) {
712            Ok(_) => tracing::info!("Successfully sent payment notification to stream"),
713            Err(err) => tracing::error!(
714                "Could not send payment received notification on channel: {}",
715                err
716            ),
717        }
718    }
719
720    /// Set up event handling for the node
721    pub fn handle_events(&self) -> Result<(), Error> {
722        let node = self.inner.clone();
723        let sender = self.sender.clone();
724        let outgoing_payment_sender = self.outgoing_payment_sender.clone();
725        let cancel_token = self.events_cancel_token.clone();
726
727        tracing::info!("Starting event handler task");
728
729        tokio::spawn(async move {
730            tracing::info!("Event handler loop started");
731            loop {
732                tokio::select! {
733                    _ = cancel_token.cancelled() => {
734                        tracing::info!("Event handler cancelled");
735                        break;
736                    }
737                    event = node.next_event_async() => {
738                        match event {
739                            Event::PaymentReceived {
740                                payment_id,
741                                payment_hash,
742                                amount_msat,
743                                custom_records: _
744                            } => {
745                                Self::handle_payment_received(
746                                    &node,
747                                    &sender,
748                                    payment_id,
749                                    payment_hash,
750                                    amount_msat
751                                ).await;
752                            }
753                            Event::PaymentSuccessful {
754                                payment_id,
755                                payment_hash,
756                                payment_preimage: _,
757                                fee_paid_msat: _,
758                            } => {
759                                tracing::info!(
760                                    payment_id = ?payment_id,
761                                    payment_hash = %payment_hash,
762                                    "LDK node payment succeeded"
763                                );
764                                if let Some(payment_id) = payment_id {
765                                    let _ = outgoing_payment_sender.send(payment_id);
766                                }
767                            }
768                            Event::PaymentFailed {
769                                payment_id,
770                                payment_hash,
771                                reason,
772                            } => {
773                                tracing::error!(
774                                    payment_id = ?payment_id,
775                                    payment_hash = ?payment_hash,
776                                    reason = ?reason,
777                                    "LDK node payment failed"
778                                );
779                                if let Some(payment_id) = payment_id {
780                                    let _ = outgoing_payment_sender.send(payment_id);
781                                }
782                            }
783                            event => {
784                                tracing::debug!("Received other ldk node event: {:?}", event);
785                            }
786                        }
787
788                        if let Err(err) = node.event_handled() {
789                            tracing::error!("Error handling node event: {}", err);
790                        } else {
791                            tracing::debug!("Successfully handled node event");
792                        }
793                    }
794                }
795            }
796            tracing::info!("Event handler loop terminated");
797        });
798
799        tracing::info!("Event handler task spawned");
800        Ok(())
801    }
802
803    /// Get Node used
804    pub fn node(&self) -> Arc<Node> {
805        Arc::clone(&self.inner)
806    }
807}
808
809/// Mint payment trait
810#[async_trait]
811impl MintPayment for CdkLdkNode {
812    type Err = payment::Error;
813
814    /// Start the payment processor
815    /// Starts the LDK node and begins event processing
816    async fn start(&self) -> Result<(), Self::Err> {
817        self.start_ldk_node().map_err(|e| {
818            tracing::error!("Failed to start CdkLdkNode: {}", e);
819            e
820        })?;
821
822        tracing::info!("CdkLdkNode payment processor started successfully");
823
824        // Start web server if configured
825        if let Some(web_addr) = self.web_addr {
826            tracing::info!("Starting LDK Node web interface on {}", web_addr);
827            self.start_web_server(web_addr).map_err(|e| {
828                tracing::error!("Failed to start web server: {}", e);
829                e
830            })?;
831        } else {
832            tracing::info!("No web server address configured, skipping web interface");
833        }
834
835        Ok(())
836    }
837
838    /// Stop the payment processor
839    /// Gracefully stops the LDK node and cancels all background tasks
840    async fn stop(&self) -> Result<(), Self::Err> {
841        self.stop_ldk_node().map_err(|e| {
842            tracing::error!("Failed to stop CdkLdkNode: {}", e);
843            e.into()
844        })
845    }
846
847    /// Base Settings
848    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
849        let settings = SettingsResponse {
850            unit: CurrencyUnit::Msat.to_string(),
851            bolt11: Some(payment::Bolt11Settings {
852                mpp: false,
853                amountless: true,
854                invoice_description: true,
855            }),
856            bolt12: Some(payment::Bolt12Settings {
857                amountless: true,
858                invoice_description: true,
859            }),
860            onchain: None,
861            custom: std::collections::HashMap::new(),
862        };
863        Ok(settings)
864    }
865
866    /// Create a new invoice
867    #[instrument(skip(self))]
868    async fn create_incoming_payment_request(
869        &self,
870        options: IncomingPaymentOptions,
871    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
872        match options {
873            IncomingPaymentOptions::Bolt11(bolt11_options) => {
874                let amount_msat: Amount = bolt11_options
875                    .amount
876                    .convert_to(&CurrencyUnit::Msat)?
877                    .into();
878                let description = bolt11_options.description.unwrap_or_default();
879                let time = match bolt11_options.unix_expiry {
880                    Some(t) => t
881                        .checked_sub(unix_time())
882                        .ok_or(payment::Error::InvalidExpiry)?,
883                    None => 36000,
884                };
885
886                let description = Bolt11InvoiceDescription::Direct(
887                    Description::new(description).map_err(|_| Error::InvalidDescription)?,
888                );
889
890                let payment = self
891                    .inner
892                    .bolt11_payment()
893                    .receive(amount_msat.into(), &description, time as u32)
894                    .map_err(Error::LdkNode)?;
895
896                let payment_hash = payment.payment_hash().to_string();
897                let payment_identifier = PaymentIdentifier::PaymentHash(
898                    hex::decode(&payment_hash)?
899                        .try_into()
900                        .map_err(|_| Error::InvalidPaymentHashLength)?,
901                );
902
903                Ok(CreateIncomingPaymentResponse {
904                    request_lookup_id: payment_identifier,
905                    request: payment.to_string(),
906                    expiry: Some(unix_time() + time),
907                    extra_json: None,
908                })
909            }
910            IncomingPaymentOptions::Bolt12(bolt12_options) => {
911                let Bolt12IncomingPaymentOptions {
912                    description,
913                    amount,
914                    unix_expiry,
915                } = *bolt12_options;
916
917                let time = unix_expiry
918                    .map(|t| {
919                        t.checked_sub(unix_time())
920                            .ok_or(payment::Error::InvalidExpiry)
921                            .map(|t| t as u32)
922                    })
923                    .transpose()?;
924
925                let offer = match amount {
926                    Some(amount) => {
927                        let amount_msat: Amount = amount.convert_to(&CurrencyUnit::Msat)?.into();
928
929                        self.inner
930                            .bolt12_payment()
931                            .receive(
932                                amount_msat.into(),
933                                &description.unwrap_or("".to_string()),
934                                time,
935                                None,
936                            )
937                            .map_err(Error::LdkNode)?
938                    }
939                    None => self
940                        .inner
941                        .bolt12_payment()
942                        .receive_variable_amount(&description.unwrap_or("".to_string()), time)
943                        .map_err(Error::LdkNode)?,
944                };
945                let payment_identifier = PaymentIdentifier::OfferId(offer.id().to_string());
946
947                Ok(CreateIncomingPaymentResponse {
948                    request_lookup_id: payment_identifier,
949                    request: offer.to_string(),
950                    expiry: unix_expiry,
951                    extra_json: None,
952                })
953            }
954            IncomingPaymentOptions::Custom(_) | IncomingPaymentOptions::Onchain(_) => {
955                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
956            }
957        }
958    }
959
960    /// Get payment quote
961    /// Used to get fee and amount required for a payment request
962    #[instrument(skip_all)]
963    async fn get_payment_quote(
964        &self,
965        unit: &CurrencyUnit,
966        options: OutgoingPaymentOptions,
967    ) -> Result<PaymentQuoteResponse, Self::Err> {
968        match options {
969            cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
970                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
971            }
972            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
973                let bolt11 = bolt11_options.bolt11;
974
975                let amount_msat = match bolt11_options.melt_options {
976                    Some(MeltOptions::Amountless { amountless }) => {
977                        let amount_msat = amountless.amount_msat;
978
979                        if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
980                            if invoice_amount != u64::from(amount_msat) {
981                                return Err(payment::Error::AmountMismatch);
982                            }
983                        }
984
985                        amount_msat
986                    }
987                    Some(MeltOptions::Mpp { mpp }) => mpp.amount,
988                    None => bolt11
989                        .amount_milli_satoshis()
990                        .ok_or(Error::UnknownInvoiceAmount)?
991                        .into(),
992                };
993
994                let amount =
995                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
996
997                let relative_fee_reserve =
998                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
999
1000                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
1001
1002                let fee = match relative_fee_reserve > absolute_fee_reserve {
1003                    true => relative_fee_reserve,
1004                    false => absolute_fee_reserve,
1005                };
1006
1007                let payment_hash = bolt11.payment_hash().to_string();
1008                let payment_hash_bytes = hex::decode(&payment_hash)?
1009                    .try_into()
1010                    .map_err(|_| Error::InvalidPaymentHashLength)?;
1011
1012                Ok(PaymentQuoteResponse {
1013                    request_lookup_id: Some(PaymentIdentifier::PaymentHash(payment_hash_bytes)),
1014                    amount,
1015                    fee: Amount::new(fee, unit.clone()),
1016                    state: MeltQuoteState::Unpaid,
1017                    extra_json: None,
1018                    estimated_blocks: None,
1019                    fee_options: None,
1020                })
1021            }
1022            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
1023                let offer = bolt12_options.offer;
1024
1025                let amount_msat = match bolt12_options.melt_options {
1026                    Some(melt_options) => melt_options.amount_msat(),
1027                    None => {
1028                        let amount = offer.amount().ok_or(payment::Error::AmountMismatch)?;
1029
1030                        match amount {
1031                            ldk_node::lightning::offers::offer::Amount::Bitcoin {
1032                                amount_msats,
1033                            } => amount_msats.into(),
1034                            _ => return Err(payment::Error::AmountMismatch),
1035                        }
1036                    }
1037                };
1038                let amount =
1039                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
1040
1041                let relative_fee_reserve =
1042                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
1043
1044                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
1045
1046                let fee = match relative_fee_reserve > absolute_fee_reserve {
1047                    true => relative_fee_reserve,
1048                    false => absolute_fee_reserve,
1049                };
1050
1051                Ok(PaymentQuoteResponse {
1052                    request_lookup_id: Some(PaymentIdentifier::QuoteId(
1053                        bolt12_options.quote_id.clone(),
1054                    )),
1055                    amount,
1056                    fee: Amount::new(fee, unit.clone()),
1057                    state: MeltQuoteState::Unpaid,
1058                    extra_json: None,
1059                    estimated_blocks: None,
1060                    fee_options: None,
1061                })
1062            }
1063            OutgoingPaymentOptions::Onchain(_) => {
1064                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
1065            }
1066        }
1067    }
1068
1069    /// Pay request
1070    #[instrument(skip(self, options))]
1071    async fn make_payment(
1072        &self,
1073        unit: &CurrencyUnit,
1074        options: OutgoingPaymentOptions,
1075    ) -> Result<MakePaymentResponse, Self::Err> {
1076        match options {
1077            cdk_common::payment::OutgoingPaymentOptions::Custom(options) => {
1078                Ok(outgoing_payment_failure_response(
1079                    unit,
1080                    PaymentIdentifier::QuoteId(options.quote_id),
1081                ))
1082            }
1083            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
1084                let bolt11 = bolt11_options.bolt11;
1085                let payment_lookup_id =
1086                    PaymentIdentifier::PaymentHash(bolt11.payment_hash().to_byte_array());
1087
1088                let send_params = match bolt11_options
1089                    .max_fee_amount
1090                    .map(|f| {
1091                        f.convert_to(&CurrencyUnit::Msat)
1092                            .map(|amount_msat| RouteParametersConfig {
1093                                max_total_routing_fee_msat: Some(amount_msat.value()),
1094                                ..Default::default()
1095                            })
1096                    })
1097                    .transpose()
1098                {
1099                    Ok(params) => params,
1100                    Err(err) => {
1101                        tracing::error!("Failed to convert fee amount: {}", err);
1102                        return Ok(outgoing_payment_failure_response(unit, payment_lookup_id));
1103                    }
1104                };
1105
1106                // Subscribe before dispatch so an immediately completed
1107                // payment cannot race ahead of the waiter.
1108                let payment_event_receiver = self.outgoing_payment_sender.subscribe();
1109
1110                let payment_id = match bolt11_options.melt_options {
1111                    Some(MeltOptions::Amountless { amountless }) => {
1112                        if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
1113                            if invoice_amount != u64::from(amountless.amount_msat) {
1114                                return Ok(outgoing_payment_failure_response(
1115                                    unit,
1116                                    payment_lookup_id,
1117                                ));
1118                            }
1119                        }
1120
1121                        self.inner.bolt11_payment().send_using_amount(
1122                            &bolt11,
1123                            amountless.amount_msat.into(),
1124                            send_params,
1125                        )
1126                    }
1127                    None => self.inner.bolt11_payment().send(&bolt11, send_params),
1128                    _ => {
1129                        return Ok(outgoing_payment_failure_response(unit, payment_lookup_id));
1130                    }
1131                };
1132
1133                let payment_id = match payment_id {
1134                    Ok(payment_id) => payment_id,
1135                    Err(err) if bolt11_send_error_is_explicit_terminal_failure(&err) => {
1136                        tracing::warn!(
1137                            payment_hash = %bolt11.payment_hash(),
1138                            "LDK rejected BOLT11 payment before dispatch: {err}"
1139                        );
1140                        return Ok(outgoing_payment_failure_response(unit, payment_lookup_id));
1141                    }
1142                    Err(err) => {
1143                        tracing::warn!(
1144                            payment_hash = %bolt11.payment_hash(),
1145                            "LDK BOLT11 send outcome is indeterminate: {err}"
1146                        );
1147                        return Err(Error::LdkNode(err).into());
1148                    }
1149                };
1150
1151                let payment_details = self
1152                    .wait_for_payment_terminal_status(payment_id, payment_event_receiver)
1153                    .await?;
1154
1155                if payment_details.status == PaymentStatus::Failed {
1156                    tracing::error!(payment_id = %payment_id, "Bolt11 payment failed");
1157                }
1158
1159                Self::make_payment_response_from_details(unit, payment_lookup_id, &payment_details)
1160            }
1161            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
1162                let offer = bolt12_options.offer;
1163                let quote_id = bolt12_options.quote_id.clone();
1164                let quote_payment_identifier = PaymentIdentifier::QuoteId(quote_id.clone());
1165
1166                let send_params = match bolt12_options
1167                    .max_fee_amount
1168                    .map(|f| {
1169                        f.convert_to(&CurrencyUnit::Msat)
1170                            .map(|amount_msat| RouteParametersConfig {
1171                                max_total_routing_fee_msat: Some(amount_msat.value()),
1172                                ..Default::default()
1173                            })
1174                    })
1175                    .transpose()
1176                {
1177                    Ok(params) => params,
1178                    Err(err) => {
1179                        tracing::error!("Failed to convert fee amount: {}", err);
1180                        return Ok(outgoing_payment_failure_response(
1181                            unit,
1182                            quote_payment_identifier,
1183                        ));
1184                    }
1185                };
1186
1187                // Claim the quote with an absent-only sentinel write before
1188                // attempting the send: a duplicate or concurrent dispatch for
1189                // the same quote is rejected here, before any funds move. The
1190                // sentinel also keeps a crash during dispatch distinguishable
1191                // from "never dispatched" (mirrors cdk-cln's pre-dispatch
1192                // bolt12 quote mapping). The payment must not be attempted
1193                // unless this marker is durable.
1194                if let Err(err) =
1195                    write_bolt12_quote_payment_id(&self.kv_store, &quote_id, None).await
1196                {
1197                    tracing::error!(
1198                        quote_id = %quote_id,
1199                        "Could not persist BOLT12 dispatch claim before sending: {err}"
1200                    );
1201                    return Ok(outgoing_payment_failure_response(
1202                        unit,
1203                        quote_payment_identifier,
1204                    ));
1205                }
1206
1207                // BOLT12 payment ids are assigned by `send`, so subscribe
1208                // first and filter the queued terminal events once it returns.
1209                let payment_event_receiver = self.outgoing_payment_sender.subscribe();
1210
1211                let payment_id = match bolt12_options.melt_options {
1212                    Some(MeltOptions::Amountless { amountless }) => {
1213                        self.inner.bolt12_payment().send_using_amount(
1214                            &offer,
1215                            amountless.amount_msat.into(),
1216                            None,
1217                            None,
1218                            send_params,
1219                        )
1220                    }
1221                    None => self
1222                        .inner
1223                        .bolt12_payment()
1224                        .send(&offer, None, None, send_params),
1225                    _ => {
1226                        self.cleanup_bolt12_dispatch_binding(&quote_id, None).await;
1227                        return Ok(outgoing_payment_failure_response(
1228                            unit,
1229                            quote_payment_identifier,
1230                        ));
1231                    }
1232                };
1233
1234                let payment_id = match payment_id {
1235                    Ok(payment_id) => payment_id,
1236                    Err(err) => {
1237                        match bolt12_send_error_has_ambiguous_dispatch(&err) {
1238                            true => {
1239                                tracing::warn!(
1240                                    quote_id = %quote_id,
1241                                    "LDK payment persistence failed after BOLT12 send; retaining \
1242                                     the dispatch sentinel because the payment may have been dispatched"
1243                                );
1244                            }
1245                            false => {
1246                                self.cleanup_bolt12_dispatch_binding(&quote_id, None).await;
1247                                tracing::warn!(
1248                                    quote_id = %quote_id,
1249                                    "LDK rejected BOLT12 payment before dispatch: {err}"
1250                                );
1251                                return Ok(outgoing_payment_failure_response(
1252                                    unit,
1253                                    quote_payment_identifier,
1254                                ));
1255                            }
1256                        }
1257                        return Err(Error::LdkNode(err).into());
1258                    }
1259                };
1260
1261                // Record the payment id so QuoteId lookups resolve to the
1262                // dispatched payment. The write is conditional on owning the
1263                // dispatch claim. Best-effort: if this write fails the
1264                // sentinel remains and the payment resolves as Pending, keeping
1265                // the melt proofs reserved.
1266                if let Err(err) =
1267                    write_bolt12_quote_payment_id(&self.kv_store, &quote_id, Some(&payment_id))
1268                        .await
1269                {
1270                    tracing::error!(
1271                        "Could not record BOLT12 payment id for quote {quote_id}: {err}. \
1272                         The payment will remain Pending until manual intervention."
1273                    );
1274                }
1275
1276                let payment_details = self
1277                    .wait_for_payment_terminal_status(payment_id, payment_event_receiver)
1278                    .await?;
1279
1280                if payment_details.status == PaymentStatus::Failed {
1281                    tracing::error!(
1282                        payment_id = %payment_id,
1283                        amount_msat = ?payment_details.amount_msat,
1284                        fee_paid_msat = ?payment_details.fee_paid_msat,
1285                        payment_kind = ?payment_details.kind,
1286                        "Bolt12 payment failed"
1287                    );
1288                    self.cleanup_bolt12_dispatch_binding(&quote_id, Some(&payment_id))
1289                        .await;
1290                }
1291
1292                Self::make_payment_response_from_details(
1293                    unit,
1294                    quote_payment_identifier,
1295                    &payment_details,
1296                )
1297            }
1298            OutgoingPaymentOptions::Onchain(options) => Ok(outgoing_payment_failure_response(
1299                unit,
1300                PaymentIdentifier::QuoteId(options.quote_id),
1301            )),
1302        }
1303    }
1304
1305    /// Listen for invoices to be paid to the mint
1306    /// Returns a stream of request_lookup_id once invoices are paid
1307    #[instrument(skip(self))]
1308    async fn wait_payment_event(
1309        &self,
1310    ) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
1311        tracing::info!("Starting stream for invoices - wait_any_incoming_payment called");
1312
1313        // Set active flag to indicate stream is active
1314        self.wait_invoice_is_active.store(true, Ordering::SeqCst);
1315        tracing::debug!("wait_invoice_is_active set to true");
1316
1317        let receiver = self.receiver.clone();
1318
1319        tracing::info!("Receiver obtained successfully, creating response stream");
1320
1321        // Transform the String stream into a WaitPaymentResponse stream
1322        let response_stream = BroadcastStream::new(receiver.resubscribe());
1323
1324        // Map the stream to handle BroadcastStreamRecvError and wrap in Event
1325        let response_stream = response_stream.filter_map(|result| async move {
1326            match result {
1327                Ok(payment) => Some(cdk_common::payment::Event::PaymentReceived(payment)),
1328                Err(err) => {
1329                    tracing::warn!("Error in broadcast stream: {}", err);
1330                    None
1331                }
1332            }
1333        });
1334
1335        // Create a combined stream that also handles cancellation
1336        let cancel_token = self.wait_invoice_cancel_token.clone();
1337        let is_active = self.wait_invoice_is_active.clone();
1338
1339        let stream = Box::pin(response_stream);
1340
1341        // Set up a task to clean up when the stream is dropped
1342        tokio::spawn(async move {
1343            cancel_token.cancelled().await;
1344            tracing::info!("wait_invoice stream cancelled");
1345            is_active.store(false, Ordering::SeqCst);
1346        });
1347
1348        tracing::info!("wait_any_incoming_payment returning stream");
1349        Ok(stream)
1350    }
1351
1352    /// Is payment event stream active
1353    fn is_payment_event_stream_active(&self) -> bool {
1354        self.wait_invoice_is_active.load(Ordering::SeqCst)
1355    }
1356
1357    /// Cancel payment event stream
1358    fn cancel_payment_event_stream(&self) {
1359        self.wait_invoice_cancel_token.cancel()
1360    }
1361
1362    /// Check the status of an incoming payment
1363    async fn check_incoming_payment_status(
1364        &self,
1365        payment_identifier: &PaymentIdentifier,
1366    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
1367        // Bolt12 offers are identified by offer id and can be paid more than
1368        // once, so collect every settled inbound payment for the offer.
1369        if let PaymentIdentifier::OfferId(offer_id) = payment_identifier {
1370            let payments = self.inner.list_payments_with_filter(|p| {
1371                p.direction == PaymentDirection::Inbound
1372                    && p.status == PaymentStatus::Succeeded
1373                    && matches!(
1374                        &p.kind,
1375                        PaymentKind::Bolt12Offer { offer_id: oid, .. } if oid.to_string() == *offer_id
1376                    )
1377            });
1378
1379            return Ok(payments
1380                .into_iter()
1381                .filter_map(|p| {
1382                    let payment_id = match &p.kind {
1383                        PaymentKind::Bolt12Offer {
1384                            hash: Some(hash), ..
1385                        } => hash.to_string(),
1386                        _ => {
1387                            tracing::warn!("Bolt12 payment for offer {} missing hash", offer_id);
1388                            return None;
1389                        }
1390                    };
1391
1392                    Some(WaitPaymentResponse {
1393                        payment_identifier: payment_identifier.clone(),
1394                        payment_amount: Amount::new(p.amount_msat?, CurrencyUnit::Msat),
1395                        payment_id,
1396                    })
1397                })
1398                .collect());
1399        }
1400
1401        let payment_id_str = match payment_identifier {
1402            PaymentIdentifier::PaymentHash(hash) => hex::encode(hash),
1403            PaymentIdentifier::CustomId(id) => id.clone(),
1404            _ => return Err(Error::UnsupportedPaymentIdentifierType.into()),
1405        };
1406
1407        let payment_id = PaymentId(
1408            hex::decode(&payment_id_str)?
1409                .try_into()
1410                .map_err(|_| Error::InvalidPaymentIdLength)?,
1411        );
1412
1413        let payment_details = self
1414            .inner
1415            .payment(&payment_id)
1416            .ok_or(Error::PaymentNotFound)?;
1417
1418        if payment_details.direction == PaymentDirection::Outbound {
1419            return Err(Error::InvalidPaymentDirection.into());
1420        }
1421
1422        let amount = if payment_details.status == PaymentStatus::Succeeded {
1423            payment_details
1424                .amount_msat
1425                .ok_or(Error::CouldNotGetPaymentAmount)?
1426        } else {
1427            return Ok(vec![]);
1428        };
1429
1430        let response = WaitPaymentResponse {
1431            payment_identifier: payment_identifier.clone(),
1432            payment_amount: Amount::new(amount, CurrencyUnit::Msat),
1433            payment_id: payment_id_str,
1434        };
1435
1436        Ok(vec![response])
1437    }
1438
1439    /// Check the status of an outgoing payment
1440    async fn check_outgoing_payment(
1441        &self,
1442        request_lookup_id: &PaymentIdentifier,
1443    ) -> Result<MakePaymentResponse, Self::Err> {
1444        let payment_details = match request_lookup_id {
1445            PaymentIdentifier::PaymentHash(id_hash) => {
1446                Self::select_bolt11_payment_details(self.inner.list_payments_with_filter(|p| {
1447                    p.direction == PaymentDirection::Outbound
1448                        && matches!(&p.kind, PaymentKind::Bolt11 { hash, .. } if &hash.0 == id_hash)
1449                }))
1450            }
1451            PaymentIdentifier::PaymentId(id) => self.inner.payment(&PaymentId(*id)),
1452            PaymentIdentifier::QuoteId(quote_id) => {
1453                match read_bolt12_quote_payment_id(&self.kv_store, quote_id)
1454                    .await?
1455                    .resolve()
1456                {
1457                    Bolt12QuotePaymentIdResolution::PaymentId(payment_id) => {
1458                        self.inner.payment(&payment_id)
1459                    }
1460                    Bolt12QuotePaymentIdResolution::Status(status) => {
1461                        return Ok(MakePaymentResponse {
1462                            payment_lookup_id: request_lookup_id.clone(),
1463                            payment_proof: None,
1464                            status,
1465                            total_spent: Amount::new(0, CurrencyUnit::Msat),
1466                        });
1467                    }
1468                }
1469            }
1470            _ => {
1471                return Ok(MakePaymentResponse {
1472                    payment_lookup_id: request_lookup_id.clone(),
1473                    payment_proof: None,
1474                    status: MeltQuoteState::Unknown,
1475                    total_spent: Amount::new(0, CurrencyUnit::Msat),
1476                });
1477            }
1478        }
1479        .ok_or(Error::PaymentNotFound)?;
1480
1481        if payment_details.direction != PaymentDirection::Outbound {
1482            return Err(Error::InvalidPaymentDirection.into());
1483        }
1484
1485        if payment_details.status == PaymentStatus::Failed {
1486            if let PaymentIdentifier::QuoteId(quote_id) = request_lookup_id {
1487                self.cleanup_bolt12_dispatch_binding(quote_id, Some(&payment_details.id))
1488                    .await;
1489            }
1490        }
1491
1492        Self::make_payment_response_from_details(
1493            &CurrencyUnit::Msat,
1494            request_lookup_id.clone(),
1495            &payment_details,
1496        )
1497    }
1498}
1499
1500impl Drop for CdkLdkNode {
1501    fn drop(&mut self) {
1502        tracing::info!("Drop called on CdkLdkNode");
1503        self.wait_invoice_cancel_token.cancel();
1504        tracing::debug!("Cancelled wait_invoice token in drop");
1505    }
1506}
1507
1508/// KV key for the bolt12 melt quote id -> payment id mapping
1509fn bolt12_quote_payment_id_key(quote_id: &QuoteId) -> Result<String, Error> {
1510    match quote_id {
1511        QuoteId::UUID(uuid) => Ok(uuid.to_string()),
1512        QuoteId::BASE64(_) => Err(Error::InvalidQuoteId),
1513    }
1514}
1515
1516/// Records the bolt12 melt quote id -> payment id mapping.
1517///
1518/// `payment_id` of `None` atomically claims an absent quote with the
1519/// pre-dispatch sentinel. `Some` atomically replaces that sentinel with the
1520/// dispatched payment id. Repeating the same payment id is idempotent; every
1521/// other existing binding is rejected.
1522async fn write_bolt12_quote_payment_id(
1523    kv_store: &DynKVStore,
1524    quote_id: &QuoteId,
1525    payment_id: Option<&PaymentId>,
1526) -> Result<(), Error> {
1527    let key = bolt12_quote_payment_id_key(quote_id)?;
1528    let value = payment_id.map(|id| hex::encode(id.0)).unwrap_or_default();
1529    let mut tx = kv_store
1530        .begin_transaction()
1531        .await
1532        .map_err(|e| Error::Database(e.to_string()))?;
1533
1534    let written = match payment_id {
1535        None => {
1536            tx.kv_write_if_absent(
1537                LDK_KV_PRIMARY_NAMESPACE,
1538                LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1539                &key,
1540                value.as_bytes(),
1541            )
1542            .await
1543        }
1544        Some(_) => {
1545            tx.kv_write_if_equals(
1546                LDK_KV_PRIMARY_NAMESPACE,
1547                LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1548                &key,
1549                b"",
1550                value.as_bytes(),
1551            )
1552            .await
1553        }
1554    }
1555    .map_err(|e| Error::Database(e.to_string()))?;
1556
1557    if written {
1558        tx.commit()
1559            .await
1560            .map_err(|e| Error::Database(e.to_string()))?;
1561        return Ok(());
1562    }
1563
1564    let existing = tx
1565        .kv_read(
1566            LDK_KV_PRIMARY_NAMESPACE,
1567            LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1568            &key,
1569        )
1570        .await
1571        .map_err(|e| Error::Database(e.to_string()))?;
1572    tx.rollback()
1573        .await
1574        .map_err(|e| Error::Database(e.to_string()))?;
1575
1576    match existing {
1577        Some(existing) if payment_id.is_some() && existing.as_slice() == value.as_bytes() => Ok(()),
1578        _ => Err(Error::Bolt12QuoteAlreadyClaimed {
1579            quote_id: quote_id.to_string(),
1580        }),
1581    }
1582}
1583
1584/// Reads the bolt12 melt quote id -> payment id mapping
1585async fn read_bolt12_quote_payment_id(
1586    kv_store: &DynKVStore,
1587    quote_id: &QuoteId,
1588) -> Result<Bolt12QuotePaymentIdLookup, Error> {
1589    let key = bolt12_quote_payment_id_key(quote_id)?;
1590    let Some(stored) = kv_store
1591        .kv_read(
1592            LDK_KV_PRIMARY_NAMESPACE,
1593            LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1594            &key,
1595        )
1596        .await
1597        .map_err(|e| Error::Database(e.to_string()))?
1598    else {
1599        return Ok(Bolt12QuotePaymentIdLookup::Missing);
1600    };
1601
1602    if stored.is_empty() {
1603        return Ok(Bolt12QuotePaymentIdLookup::Dispatching);
1604    }
1605
1606    let payment_id_hex = match String::from_utf8(stored) {
1607        Ok(payment_id_hex) => payment_id_hex,
1608        Err(err) => {
1609            tracing::warn!(
1610                "LDK: invalid UTF-8 in BOLT12 payment id mapping for quote {quote_id}: {err}"
1611            );
1612            return Ok(Bolt12QuotePaymentIdLookup::Malformed);
1613        }
1614    };
1615
1616    let payment_id_bytes = match hex::decode(&payment_id_hex) {
1617        Ok(bytes) => bytes,
1618        Err(err) => {
1619            tracing::warn!(
1620                "LDK: invalid hex in BOLT12 payment id mapping for quote {quote_id}: {err}"
1621            );
1622            return Ok(Bolt12QuotePaymentIdLookup::Malformed);
1623        }
1624    };
1625
1626    let payment_id: [u8; 32] = match payment_id_bytes.try_into() {
1627        Ok(payment_id) => payment_id,
1628        Err(_) => {
1629            tracing::warn!("LDK: invalid payment id length in BOLT12 mapping for quote {quote_id}");
1630            return Ok(Bolt12QuotePaymentIdLookup::Malformed);
1631        }
1632    };
1633
1634    Ok(Bolt12QuotePaymentIdLookup::Found(PaymentId(payment_id)))
1635}
1636
1637/// Atomically removes the bolt12 quote binding only if it still matches the
1638/// expected sentinel (`None`) or payment id (`Some`).
1639async fn delete_bolt12_quote_payment_id_if_equals(
1640    kv_store: &DynKVStore,
1641    quote_id: &QuoteId,
1642    payment_id: Option<&PaymentId>,
1643) -> Result<bool, Error> {
1644    let key = bolt12_quote_payment_id_key(quote_id)?;
1645    let expected = payment_id.map(|id| hex::encode(id.0)).unwrap_or_default();
1646    let mut tx = kv_store
1647        .begin_transaction()
1648        .await
1649        .map_err(|e| Error::Database(e.to_string()))?;
1650
1651    let claimed = tx
1652        .kv_write_if_equals(
1653            LDK_KV_PRIMARY_NAMESPACE,
1654            LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1655            &key,
1656            expected.as_bytes(),
1657            LDK_KV_BOLT12_CLEANUP_MARKER,
1658        )
1659        .await
1660        .map_err(|e| Error::Database(e.to_string()))?;
1661
1662    if !claimed {
1663        tx.rollback()
1664            .await
1665            .map_err(|e| Error::Database(e.to_string()))?;
1666        return Ok(false);
1667    }
1668
1669    tx.kv_remove(
1670        LDK_KV_PRIMARY_NAMESPACE,
1671        LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1672        &key,
1673    )
1674    .await
1675    .map_err(|e| Error::Database(e.to_string()))?;
1676    tx.commit()
1677        .await
1678        .map_err(|e| Error::Database(e.to_string()))?;
1679
1680    Ok(true)
1681}
1682
1683#[cfg(test)]
1684mod tests {
1685    use super::*;
1686
1687    #[test]
1688    fn bitcoin_rpc_debug_redacts_password() {
1689        let source = ChainSource::BitcoinRpc(BitcoinRpcConfig {
1690            host: "127.0.0.1".to_string(),
1691            port: 8332,
1692            user: "rpc-user".to_string(),
1693            password: "rpc-password-secret".to_string(),
1694        });
1695
1696        let debug = format!("{source:?}");
1697
1698        assert!(debug.contains("127.0.0.1"));
1699        assert!(debug.contains("rpc-user"));
1700        assert!(debug.contains("[REDACTED]"));
1701        assert!(!debug.contains("rpc-password-secret"));
1702    }
1703
1704    #[test]
1705    fn chain_source_debug_redacts_url_credentials() {
1706        for source in [
1707            ChainSource::Esplora("https://esplora-user:esplora-secret@example.com/api".to_string()),
1708            ChainSource::Electrum(
1709                "ssl://electrum-user:electrum-secret@example.com:50002".to_string(),
1710            ),
1711        ] {
1712            let debug = format!("{source:?}");
1713
1714            assert!(debug.contains("example.com"));
1715            assert!(!debug.contains("-user"));
1716            assert!(!debug.contains("-secret"));
1717        }
1718    }
1719
1720    #[test]
1721    fn gossip_source_debug_redacts_url_credentials() {
1722        let source = GossipSource::RapidGossipSync(
1723            "https://rgs-user:rgs-secret@example.com/snapshot".to_string(),
1724        );
1725
1726        let debug = format!("{source:?}");
1727
1728        assert!(debug.contains("https://example.com/snapshot"));
1729        assert!(!debug.contains("rgs-user"));
1730        assert!(!debug.contains("rgs-secret"));
1731    }
1732
1733    fn test_payment_details(status: PaymentStatus, amount_msat: Option<u64>) -> PaymentDetails {
1734        PaymentDetails {
1735            id: PaymentId([2; 32]),
1736            kind: PaymentKind::Bolt11 {
1737                hash: PaymentHash([1; 32]),
1738                preimage: None,
1739                secret: None,
1740            },
1741            amount_msat,
1742            fee_paid_msat: None,
1743            direction: PaymentDirection::Outbound,
1744            status,
1745            latest_update_timestamp: 0,
1746        }
1747    }
1748
1749    fn test_payment_details_with_id(
1750        id: [u8; 32],
1751        status: PaymentStatus,
1752        latest_update_timestamp: u64,
1753    ) -> PaymentDetails {
1754        PaymentDetails {
1755            id: PaymentId(id),
1756            latest_update_timestamp,
1757            ..test_payment_details(status, None)
1758        }
1759    }
1760
1761    #[test]
1762    fn failed_payment_response_does_not_require_amount() {
1763        let details = test_payment_details(PaymentStatus::Failed, None);
1764
1765        let response = CdkLdkNode::make_payment_response_from_details(
1766            &CurrencyUnit::Msat,
1767            PaymentIdentifier::PaymentId([2; 32]),
1768            &details,
1769        )
1770        .expect("failed payment details should map without amount");
1771
1772        assert_eq!(response.status, MeltQuoteState::Failed);
1773        assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1774    }
1775
1776    #[test]
1777    fn pending_payment_response_does_not_require_amount() {
1778        let details = test_payment_details(PaymentStatus::Pending, None);
1779
1780        let response = CdkLdkNode::make_payment_response_from_details(
1781            &CurrencyUnit::Msat,
1782            PaymentIdentifier::PaymentId([2; 32]),
1783            &details,
1784        )
1785        .expect("pending payment details should map without amount");
1786
1787        assert_eq!(response.status, MeltQuoteState::Pending);
1788        assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1789    }
1790
1791    #[test]
1792    fn paid_payment_response_requires_amount() {
1793        let details = test_payment_details(PaymentStatus::Succeeded, None);
1794
1795        let err = CdkLdkNode::make_payment_response_from_details(
1796            &CurrencyUnit::Msat,
1797            PaymentIdentifier::PaymentId([2; 32]),
1798            &details,
1799        )
1800        .expect_err("paid payment details without amount should fail");
1801
1802        assert!(matches!(err, payment::Error::Backend(_)));
1803    }
1804
1805    #[test]
1806    fn bolt11_payment_selection_prefers_pending_over_failed() {
1807        let failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 2);
1808        let pending = test_payment_details_with_id([2; 32], PaymentStatus::Pending, 1);
1809
1810        let selected = CdkLdkNode::select_bolt11_payment_details([failed, pending])
1811            .expect("payment details should be selected");
1812
1813        assert_eq!(selected.id, PaymentId([2; 32]));
1814        assert_eq!(selected.status, PaymentStatus::Pending);
1815    }
1816
1817    #[test]
1818    fn bolt11_payment_selection_prefers_succeeded_over_pending() {
1819        let pending = test_payment_details_with_id([1; 32], PaymentStatus::Pending, 2);
1820        let succeeded = PaymentDetails {
1821            amount_msat: Some(1000),
1822            ..test_payment_details_with_id([2; 32], PaymentStatus::Succeeded, 1)
1823        };
1824
1825        let selected = CdkLdkNode::select_bolt11_payment_details([pending, succeeded])
1826            .expect("payment details should be selected");
1827
1828        assert_eq!(selected.id, PaymentId([2; 32]));
1829        assert_eq!(selected.status, PaymentStatus::Succeeded);
1830    }
1831
1832    #[test]
1833    fn bolt11_payment_selection_uses_latest_failed_when_all_failed() {
1834        let older_failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 1);
1835        let newer_failed = test_payment_details_with_id([2; 32], PaymentStatus::Failed, 2);
1836
1837        let selected = CdkLdkNode::select_bolt11_payment_details([older_failed, newer_failed])
1838            .expect("payment details should be selected");
1839
1840        assert_eq!(selected.id, PaymentId([2; 32]));
1841        assert_eq!(selected.status, PaymentStatus::Failed);
1842    }
1843
1844    #[tokio::test]
1845    async fn terminal_payment_event_wait_ignores_other_payments() {
1846        let (sender, mut receiver) = tokio::sync::broadcast::channel(4);
1847        let payment_id = PaymentId([2; 32]);
1848
1849        // Queue both events before entering the wait to exercise the race where
1850        // LDK completes immediately after dispatch returns.
1851        sender
1852            .send(PaymentId([1; 32]))
1853            .expect("receiver should be subscribed");
1854        sender
1855            .send(payment_id)
1856            .expect("receiver should be subscribed");
1857
1858        CdkLdkNode::wait_for_terminal_payment_event(&mut receiver, payment_id)
1859            .await
1860            .expect("matching terminal event should wake the waiter");
1861    }
1862
1863    #[tokio::test]
1864    async fn terminal_payment_event_wait_recovers_from_lagged_channel() {
1865        let (sender, mut receiver) = tokio::sync::broadcast::channel(2);
1866        let payment_id = PaymentId([3; 32]);
1867
1868        sender
1869            .send(PaymentId([1; 32]))
1870            .expect("receiver should be subscribed");
1871        sender
1872            .send(PaymentId([2; 32]))
1873            .expect("receiver should be subscribed");
1874        sender
1875            .send(PaymentId([4; 32]))
1876            .expect("receiver should be subscribed");
1877        sender
1878            .send(payment_id)
1879            .expect("receiver should be subscribed");
1880
1881        CdkLdkNode::wait_for_terminal_payment_event(&mut receiver, payment_id)
1882            .await
1883            .expect("receiver lag should not prevent a matching event from waking the waiter");
1884    }
1885
1886    #[tokio::test]
1887    async fn terminal_payment_event_wait_reports_closed_channel() {
1888        let (sender, mut receiver) = tokio::sync::broadcast::channel(1);
1889        drop(sender);
1890
1891        let err = CdkLdkNode::wait_for_terminal_payment_event(&mut receiver, PaymentId([2; 32]))
1892            .await
1893            .expect_err("a closed event channel should stop the wait");
1894
1895        assert!(matches!(
1896            err,
1897            tokio::sync::broadcast::error::RecvError::Closed
1898        ));
1899    }
1900
1901    #[test]
1902    fn bolt12_persistence_failure_has_ambiguous_dispatch() {
1903        assert!(bolt12_send_error_has_ambiguous_dispatch(
1904            &ldk_node::NodeError::PersistenceFailed
1905        ));
1906
1907        for not_dispatched in [
1908            ldk_node::NodeError::NotRunning,
1909            ldk_node::NodeError::UnsupportedCurrency,
1910            ldk_node::NodeError::InvalidOffer,
1911            ldk_node::NodeError::InvalidAmount,
1912            ldk_node::NodeError::DuplicatePayment,
1913            ldk_node::NodeError::InvoiceRequestCreationFailed,
1914            ldk_node::NodeError::PaymentSendingFailed,
1915        ] {
1916            assert!(
1917                !bolt12_send_error_has_ambiguous_dispatch(&not_dispatched),
1918                "{not_dispatched} must be treated as not dispatched"
1919            );
1920        }
1921    }
1922
1923    #[test]
1924    fn bolt11_send_errors_only_classify_explicit_rejections_as_terminal() {
1925        for terminal_error in [
1926            ldk_node::NodeError::NotRunning,
1927            ldk_node::NodeError::InvalidAmount,
1928            ldk_node::NodeError::InvalidInvoice,
1929            ldk_node::NodeError::PaymentSendingFailed,
1930        ] {
1931            assert!(
1932                bolt11_send_error_is_explicit_terminal_failure(&terminal_error),
1933                "{terminal_error} must be treated as a definitive failure"
1934            );
1935        }
1936
1937        for ambiguous_error in [
1938            ldk_node::NodeError::PersistenceFailed,
1939            ldk_node::NodeError::DuplicatePayment,
1940        ] {
1941            assert!(
1942                !bolt11_send_error_is_explicit_terminal_failure(&ambiguous_error),
1943                "{ambiguous_error} must not authorize proof release"
1944            );
1945        }
1946    }
1947
1948    #[test]
1949    fn authoritative_outgoing_failure_response_is_terminal_and_spends_nothing() {
1950        let payment_lookup_id = PaymentIdentifier::PaymentHash([42; 32]);
1951        let response =
1952            outgoing_payment_failure_response(&CurrencyUnit::Msat, payment_lookup_id.clone());
1953
1954        assert_eq!(response.payment_lookup_id, payment_lookup_id);
1955        assert_eq!(response.status, MeltQuoteState::Failed);
1956        assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1957        assert!(response.payment_proof.is_none());
1958    }
1959
1960    #[test]
1961    fn bolt12_quote_payment_id_lookup_resolution_is_safe() {
1962        assert_eq!(
1963            Bolt12QuotePaymentIdLookup::Dispatching.resolve(),
1964            Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Pending),
1965            "an indeterminate dispatch must keep melt proofs reserved"
1966        );
1967        assert_eq!(
1968            Bolt12QuotePaymentIdLookup::Missing.resolve(),
1969            Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unpaid),
1970            "a missing sentinel means the payment was never dispatched"
1971        );
1972        assert_eq!(
1973            Bolt12QuotePaymentIdLookup::Malformed.resolve(),
1974            Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unknown),
1975            "corrupt bookkeeping must remain indeterminate"
1976        );
1977    }
1978
1979    async fn test_kv_store() -> DynKVStore {
1980        std::sync::Arc::new(cdk_sqlite::mint::memory::empty().await.unwrap())
1981    }
1982
1983    /// The mapping must resolve Missing before any dispatch, Dispatching
1984    /// (indeterminate) while only the pre-dispatch sentinel exists, and Found
1985    /// after the payment id is recorded.
1986    #[tokio::test]
1987    async fn bolt12_quote_payment_id_mapping_lifecycle() {
1988        let kv_store = test_kv_store().await;
1989        let quote_id = QuoteId::new();
1990
1991        assert_eq!(
1992            read_bolt12_quote_payment_id(&kv_store, &quote_id)
1993                .await
1994                .unwrap(),
1995            Bolt12QuotePaymentIdLookup::Missing,
1996            "no record must resolve as never dispatched"
1997        );
1998
1999        // Pre-dispatch sentinel
2000        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2001            .await
2002            .unwrap();
2003        assert_eq!(
2004            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2005                .await
2006                .unwrap(),
2007            Bolt12QuotePaymentIdLookup::Dispatching,
2008            "sentinel must resolve as indeterminate, never terminal"
2009        );
2010
2011        assert!(
2012            delete_bolt12_quote_payment_id_if_equals(&kv_store, &quote_id, None)
2013                .await
2014                .unwrap(),
2015            "an unambiguous pre-dispatch failure should release its sentinel"
2016        );
2017        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2018            .await
2019            .expect("a retry should reclaim the quote after sentinel cleanup");
2020
2021        // Record the payment id
2022        let payment_id = PaymentId([7; 32]);
2023        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&payment_id))
2024            .await
2025            .unwrap();
2026        assert_eq!(
2027            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2028                .await
2029                .unwrap(),
2030            Bolt12QuotePaymentIdLookup::Found(payment_id)
2031        );
2032
2033        // Removal returns to Missing (failed dispatch cleanup)
2034        assert!(
2035            delete_bolt12_quote_payment_id_if_equals(&kv_store, &quote_id, Some(&payment_id))
2036                .await
2037                .unwrap()
2038        );
2039        assert_eq!(
2040            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2041                .await
2042                .unwrap(),
2043            Bolt12QuotePaymentIdLookup::Missing
2044        );
2045    }
2046
2047    #[tokio::test]
2048    async fn bolt12_quote_payment_id_binding_is_write_once() {
2049        let kv_store = test_kv_store().await;
2050        let quote_id = QuoteId::new();
2051        let payment_id = PaymentId([7; 32]);
2052        let conflicting_payment_id = PaymentId([9; 32]);
2053
2054        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2055            .await
2056            .expect("first dispatch should claim the quote");
2057        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&payment_id))
2058            .await
2059            .expect("the claim owner should record its payment id");
2060        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&payment_id))
2061            .await
2062            .expect("repeating the same payment id must be idempotent");
2063
2064        let duplicate_dispatch = write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2065            .await
2066            .expect_err("a dispatched quote must not be claimed again");
2067        assert!(
2068            matches!(duplicate_dispatch, Error::Bolt12QuoteAlreadyClaimed { .. }),
2069            "unexpected error: {duplicate_dispatch}"
2070        );
2071
2072        let conflicting_binding =
2073            write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&conflicting_payment_id))
2074                .await
2075                .expect_err("a conflicting payment id must be rejected");
2076        assert!(
2077            matches!(conflicting_binding, Error::Bolt12QuoteAlreadyClaimed { .. }),
2078            "unexpected error: {conflicting_binding}"
2079        );
2080
2081        assert_eq!(
2082            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2083                .await
2084                .expect("payment id should remain readable"),
2085            Bolt12QuotePaymentIdLookup::Found(payment_id),
2086            "a duplicate dispatch must not redirect recovery"
2087        );
2088    }
2089
2090    #[tokio::test]
2091    async fn failed_bolt12_binding_can_be_released_without_removing_a_retry() {
2092        let kv_store = test_kv_store().await;
2093        let quote_id = QuoteId::new();
2094        let failed_payment_id = PaymentId([7; 32]);
2095        let retry_payment_id = PaymentId([9; 32]);
2096
2097        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2098            .await
2099            .expect("failed dispatch should claim the quote");
2100        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&failed_payment_id))
2101            .await
2102            .expect("failed payment id should be recorded");
2103        assert!(delete_bolt12_quote_payment_id_if_equals(
2104            &kv_store,
2105            &quote_id,
2106            Some(&failed_payment_id),
2107        )
2108        .await
2109        .expect("failed binding should be released"));
2110
2111        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2112            .await
2113            .expect("retry should claim the released quote");
2114        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&retry_payment_id))
2115            .await
2116            .expect("retry payment id should be recorded");
2117        assert!(!delete_bolt12_quote_payment_id_if_equals(
2118            &kv_store,
2119            &quote_id,
2120            Some(&failed_payment_id),
2121        )
2122        .await
2123        .expect("stale cleanup should be checked atomically"));
2124
2125        assert_eq!(
2126            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2127                .await
2128                .expect("retry binding should remain readable"),
2129            Bolt12QuotePaymentIdLookup::Found(retry_payment_id),
2130            "stale failed-payment cleanup must not remove a newer retry"
2131        );
2132    }
2133
2134    #[tokio::test]
2135    async fn bolt12_quote_dispatch_concurrent_claims_have_single_winner() {
2136        let kv_store = test_kv_store().await;
2137        let quote_id = QuoteId::new();
2138
2139        let (first_result, second_result) = tokio::join!(
2140            write_bolt12_quote_payment_id(&kv_store, &quote_id, None),
2141            write_bolt12_quote_payment_id(&kv_store, &quote_id, None),
2142        );
2143
2144        let outcomes = [first_result, second_result];
2145        let winners = outcomes.iter().filter(|result| result.is_ok()).count();
2146        let conflicts = outcomes
2147            .iter()
2148            .filter(|result| matches!(result, Err(Error::Bolt12QuoteAlreadyClaimed { .. })))
2149            .count();
2150
2151        assert_eq!(winners, 1, "exactly one dispatch may claim the quote");
2152        assert_eq!(conflicts, 1, "the losing dispatch must be rejected");
2153    }
2154
2155    #[tokio::test]
2156    async fn bolt12_quote_payment_id_concurrent_resolution_has_single_winner() {
2157        let kv_store = test_kv_store().await;
2158        let quote_id = QuoteId::new();
2159        let first_payment_id = PaymentId([7; 32]);
2160        let second_payment_id = PaymentId([9; 32]);
2161
2162        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2163            .await
2164            .expect("dispatch should claim the quote");
2165
2166        let (first_result, second_result) = tokio::join!(
2167            write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&first_payment_id)),
2168            write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&second_payment_id)),
2169        );
2170
2171        let outcomes = [&first_result, &second_result];
2172        let winners = outcomes.iter().filter(|result| result.is_ok()).count();
2173        let conflicts = outcomes
2174            .iter()
2175            .filter(|result| matches!(result, Err(Error::Bolt12QuoteAlreadyClaimed { .. })))
2176            .count();
2177
2178        assert_eq!(winners, 1, "exactly one payment id may resolve the claim");
2179        assert_eq!(conflicts, 1, "the losing resolution must be rejected");
2180
2181        let winner = if first_result.is_ok() {
2182            first_payment_id
2183        } else {
2184            second_payment_id
2185        };
2186        assert_eq!(
2187            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2188                .await
2189                .expect("payment id should remain readable"),
2190            Bolt12QuotePaymentIdLookup::Found(winner)
2191        );
2192    }
2193
2194    /// A corrupted mapping must resolve as indeterminate (`Malformed`), never
2195    /// as a terminal state that could trigger compensation.
2196    #[tokio::test]
2197    async fn bolt12_quote_payment_id_mapping_malformed_is_indeterminate() {
2198        let kv_store = test_kv_store().await;
2199        let quote_id = QuoteId::new();
2200        let key = bolt12_quote_payment_id_key(&quote_id).unwrap();
2201
2202        for corrupt in ["not-hex", "0102", "zz"] {
2203            let mut tx = kv_store.begin_transaction().await.unwrap();
2204            tx.kv_write(
2205                LDK_KV_PRIMARY_NAMESPACE,
2206                LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
2207                &key,
2208                corrupt.as_bytes(),
2209            )
2210            .await
2211            .unwrap();
2212            tx.commit().await.unwrap();
2213
2214            assert_eq!(
2215                read_bolt12_quote_payment_id(&kv_store, &quote_id)
2216                    .await
2217                    .unwrap(),
2218                Bolt12QuotePaymentIdLookup::Malformed,
2219                "corrupt value {corrupt} must be indeterminate"
2220            );
2221        }
2222    }
2223}