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 { amountless: true }),
857            onchain: None,
858            custom: std::collections::HashMap::new(),
859        };
860        Ok(settings)
861    }
862
863    /// Create a new invoice
864    #[instrument(skip(self))]
865    async fn create_incoming_payment_request(
866        &self,
867        options: IncomingPaymentOptions,
868    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
869        match options {
870            IncomingPaymentOptions::Bolt11(bolt11_options) => {
871                let amount_msat: Amount = bolt11_options
872                    .amount
873                    .convert_to(&CurrencyUnit::Msat)?
874                    .into();
875                let description = bolt11_options.description.unwrap_or_default();
876                let time = match bolt11_options.unix_expiry {
877                    Some(t) => t
878                        .checked_sub(unix_time())
879                        .ok_or(payment::Error::InvalidExpiry)?,
880                    None => 36000,
881                };
882
883                let description = Bolt11InvoiceDescription::Direct(
884                    Description::new(description).map_err(|_| Error::InvalidDescription)?,
885                );
886
887                let payment = self
888                    .inner
889                    .bolt11_payment()
890                    .receive(amount_msat.into(), &description, time as u32)
891                    .map_err(Error::LdkNode)?;
892
893                let payment_hash = payment.payment_hash().to_string();
894                let payment_identifier = PaymentIdentifier::PaymentHash(
895                    hex::decode(&payment_hash)?
896                        .try_into()
897                        .map_err(|_| Error::InvalidPaymentHashLength)?,
898                );
899
900                Ok(CreateIncomingPaymentResponse {
901                    request_lookup_id: payment_identifier,
902                    request: payment.to_string(),
903                    expiry: Some(unix_time() + time),
904                    extra_json: None,
905                })
906            }
907            IncomingPaymentOptions::Bolt12(bolt12_options) => {
908                let Bolt12IncomingPaymentOptions {
909                    description,
910                    amount,
911                    unix_expiry,
912                } = *bolt12_options;
913
914                let time = unix_expiry
915                    .map(|t| {
916                        t.checked_sub(unix_time())
917                            .ok_or(payment::Error::InvalidExpiry)
918                            .map(|t| t as u32)
919                    })
920                    .transpose()?;
921
922                let offer = match amount {
923                    Some(amount) => {
924                        let amount_msat: Amount = amount.convert_to(&CurrencyUnit::Msat)?.into();
925
926                        self.inner
927                            .bolt12_payment()
928                            .receive(
929                                amount_msat.into(),
930                                &description.unwrap_or("".to_string()),
931                                time,
932                                None,
933                            )
934                            .map_err(Error::LdkNode)?
935                    }
936                    None => self
937                        .inner
938                        .bolt12_payment()
939                        .receive_variable_amount(&description.unwrap_or("".to_string()), time)
940                        .map_err(Error::LdkNode)?,
941                };
942                let payment_identifier = PaymentIdentifier::OfferId(offer.id().to_string());
943
944                Ok(CreateIncomingPaymentResponse {
945                    request_lookup_id: payment_identifier,
946                    request: offer.to_string(),
947                    expiry: unix_expiry,
948                    extra_json: None,
949                })
950            }
951            IncomingPaymentOptions::Custom(_) | IncomingPaymentOptions::Onchain(_) => {
952                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
953            }
954        }
955    }
956
957    /// Get payment quote
958    /// Used to get fee and amount required for a payment request
959    #[instrument(skip_all)]
960    async fn get_payment_quote(
961        &self,
962        unit: &CurrencyUnit,
963        options: OutgoingPaymentOptions,
964    ) -> Result<PaymentQuoteResponse, Self::Err> {
965        match options {
966            cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
967                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
968            }
969            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
970                let bolt11 = bolt11_options.bolt11;
971
972                let amount_msat = match bolt11_options.melt_options {
973                    Some(MeltOptions::Amountless { amountless }) => {
974                        let amount_msat = amountless.amount_msat;
975
976                        if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
977                            if invoice_amount != u64::from(amount_msat) {
978                                return Err(payment::Error::AmountMismatch);
979                            }
980                        }
981
982                        amount_msat
983                    }
984                    Some(MeltOptions::Mpp { mpp }) => mpp.amount,
985                    None => bolt11
986                        .amount_milli_satoshis()
987                        .ok_or(Error::UnknownInvoiceAmount)?
988                        .into(),
989                };
990
991                let amount =
992                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
993
994                let relative_fee_reserve =
995                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
996
997                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
998
999                let fee = match relative_fee_reserve > absolute_fee_reserve {
1000                    true => relative_fee_reserve,
1001                    false => absolute_fee_reserve,
1002                };
1003
1004                let payment_hash = bolt11.payment_hash().to_string();
1005                let payment_hash_bytes = hex::decode(&payment_hash)?
1006                    .try_into()
1007                    .map_err(|_| Error::InvalidPaymentHashLength)?;
1008
1009                Ok(PaymentQuoteResponse {
1010                    request_lookup_id: Some(PaymentIdentifier::PaymentHash(payment_hash_bytes)),
1011                    amount,
1012                    fee: Amount::new(fee, unit.clone()),
1013                    state: MeltQuoteState::Unpaid,
1014                    extra_json: None,
1015                    estimated_blocks: None,
1016                    fee_options: None,
1017                })
1018            }
1019            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
1020                let offer = bolt12_options.offer;
1021
1022                let amount_msat = match bolt12_options.melt_options {
1023                    Some(melt_options) => melt_options.amount_msat(),
1024                    None => {
1025                        let amount = offer.amount().ok_or(payment::Error::AmountMismatch)?;
1026
1027                        match amount {
1028                            ldk_node::lightning::offers::offer::Amount::Bitcoin {
1029                                amount_msats,
1030                            } => amount_msats.into(),
1031                            _ => return Err(payment::Error::AmountMismatch),
1032                        }
1033                    }
1034                };
1035                let amount =
1036                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
1037
1038                let relative_fee_reserve =
1039                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
1040
1041                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
1042
1043                let fee = match relative_fee_reserve > absolute_fee_reserve {
1044                    true => relative_fee_reserve,
1045                    false => absolute_fee_reserve,
1046                };
1047
1048                Ok(PaymentQuoteResponse {
1049                    request_lookup_id: Some(PaymentIdentifier::QuoteId(
1050                        bolt12_options.quote_id.clone(),
1051                    )),
1052                    amount,
1053                    fee: Amount::new(fee, unit.clone()),
1054                    state: MeltQuoteState::Unpaid,
1055                    extra_json: None,
1056                    estimated_blocks: None,
1057                    fee_options: None,
1058                })
1059            }
1060            OutgoingPaymentOptions::Onchain(_) => {
1061                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
1062            }
1063        }
1064    }
1065
1066    /// Pay request
1067    #[instrument(skip(self, options))]
1068    async fn make_payment(
1069        &self,
1070        unit: &CurrencyUnit,
1071        options: OutgoingPaymentOptions,
1072    ) -> Result<MakePaymentResponse, Self::Err> {
1073        match options {
1074            cdk_common::payment::OutgoingPaymentOptions::Custom(options) => {
1075                Ok(outgoing_payment_failure_response(
1076                    unit,
1077                    PaymentIdentifier::QuoteId(options.quote_id),
1078                ))
1079            }
1080            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
1081                let bolt11 = bolt11_options.bolt11;
1082                let payment_lookup_id =
1083                    PaymentIdentifier::PaymentHash(bolt11.payment_hash().to_byte_array());
1084
1085                let send_params = match bolt11_options
1086                    .max_fee_amount
1087                    .map(|f| {
1088                        f.convert_to(&CurrencyUnit::Msat)
1089                            .map(|amount_msat| RouteParametersConfig {
1090                                max_total_routing_fee_msat: Some(amount_msat.value()),
1091                                ..Default::default()
1092                            })
1093                    })
1094                    .transpose()
1095                {
1096                    Ok(params) => params,
1097                    Err(err) => {
1098                        tracing::error!("Failed to convert fee amount: {}", err);
1099                        return Ok(outgoing_payment_failure_response(unit, payment_lookup_id));
1100                    }
1101                };
1102
1103                // Subscribe before dispatch so an immediately completed
1104                // payment cannot race ahead of the waiter.
1105                let payment_event_receiver = self.outgoing_payment_sender.subscribe();
1106
1107                let payment_id = match bolt11_options.melt_options {
1108                    Some(MeltOptions::Amountless { amountless }) => {
1109                        if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
1110                            if invoice_amount != u64::from(amountless.amount_msat) {
1111                                return Ok(outgoing_payment_failure_response(
1112                                    unit,
1113                                    payment_lookup_id,
1114                                ));
1115                            }
1116                        }
1117
1118                        self.inner.bolt11_payment().send_using_amount(
1119                            &bolt11,
1120                            amountless.amount_msat.into(),
1121                            send_params,
1122                        )
1123                    }
1124                    None => self.inner.bolt11_payment().send(&bolt11, send_params),
1125                    _ => {
1126                        return Ok(outgoing_payment_failure_response(unit, payment_lookup_id));
1127                    }
1128                };
1129
1130                let payment_id = match payment_id {
1131                    Ok(payment_id) => payment_id,
1132                    Err(err) if bolt11_send_error_is_explicit_terminal_failure(&err) => {
1133                        tracing::warn!(
1134                            payment_hash = %bolt11.payment_hash(),
1135                            "LDK rejected BOLT11 payment before dispatch: {err}"
1136                        );
1137                        return Ok(outgoing_payment_failure_response(unit, payment_lookup_id));
1138                    }
1139                    Err(err) => {
1140                        tracing::warn!(
1141                            payment_hash = %bolt11.payment_hash(),
1142                            "LDK BOLT11 send outcome is indeterminate: {err}"
1143                        );
1144                        return Err(Error::LdkNode(err).into());
1145                    }
1146                };
1147
1148                let payment_details = self
1149                    .wait_for_payment_terminal_status(payment_id, payment_event_receiver)
1150                    .await?;
1151
1152                if payment_details.status == PaymentStatus::Failed {
1153                    tracing::error!(payment_id = %payment_id, "Bolt11 payment failed");
1154                }
1155
1156                Self::make_payment_response_from_details(unit, payment_lookup_id, &payment_details)
1157            }
1158            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
1159                let offer = bolt12_options.offer;
1160                let quote_id = bolt12_options.quote_id.clone();
1161                let quote_payment_identifier = PaymentIdentifier::QuoteId(quote_id.clone());
1162
1163                let send_params = match bolt12_options
1164                    .max_fee_amount
1165                    .map(|f| {
1166                        f.convert_to(&CurrencyUnit::Msat)
1167                            .map(|amount_msat| RouteParametersConfig {
1168                                max_total_routing_fee_msat: Some(amount_msat.value()),
1169                                ..Default::default()
1170                            })
1171                    })
1172                    .transpose()
1173                {
1174                    Ok(params) => params,
1175                    Err(err) => {
1176                        tracing::error!("Failed to convert fee amount: {}", err);
1177                        return Ok(outgoing_payment_failure_response(
1178                            unit,
1179                            quote_payment_identifier,
1180                        ));
1181                    }
1182                };
1183
1184                // Claim the quote with an absent-only sentinel write before
1185                // attempting the send: a duplicate or concurrent dispatch for
1186                // the same quote is rejected here, before any funds move. The
1187                // sentinel also keeps a crash during dispatch distinguishable
1188                // from "never dispatched" (mirrors cdk-cln's pre-dispatch
1189                // bolt12 quote mapping). The payment must not be attempted
1190                // unless this marker is durable.
1191                if let Err(err) =
1192                    write_bolt12_quote_payment_id(&self.kv_store, &quote_id, None).await
1193                {
1194                    tracing::error!(
1195                        quote_id = %quote_id,
1196                        "Could not persist BOLT12 dispatch claim before sending: {err}"
1197                    );
1198                    return Ok(outgoing_payment_failure_response(
1199                        unit,
1200                        quote_payment_identifier,
1201                    ));
1202                }
1203
1204                // BOLT12 payment ids are assigned by `send`, so subscribe
1205                // first and filter the queued terminal events once it returns.
1206                let payment_event_receiver = self.outgoing_payment_sender.subscribe();
1207
1208                let payment_id = match bolt12_options.melt_options {
1209                    Some(MeltOptions::Amountless { amountless }) => {
1210                        self.inner.bolt12_payment().send_using_amount(
1211                            &offer,
1212                            amountless.amount_msat.into(),
1213                            None,
1214                            None,
1215                            send_params,
1216                        )
1217                    }
1218                    None => self
1219                        .inner
1220                        .bolt12_payment()
1221                        .send(&offer, None, None, send_params),
1222                    _ => {
1223                        self.cleanup_bolt12_dispatch_binding(&quote_id, None).await;
1224                        return Ok(outgoing_payment_failure_response(
1225                            unit,
1226                            quote_payment_identifier,
1227                        ));
1228                    }
1229                };
1230
1231                let payment_id = match payment_id {
1232                    Ok(payment_id) => payment_id,
1233                    Err(err) => {
1234                        match bolt12_send_error_has_ambiguous_dispatch(&err) {
1235                            true => {
1236                                tracing::warn!(
1237                                    quote_id = %quote_id,
1238                                    "LDK payment persistence failed after BOLT12 send; retaining \
1239                                     the dispatch sentinel because the payment may have been dispatched"
1240                                );
1241                            }
1242                            false => {
1243                                self.cleanup_bolt12_dispatch_binding(&quote_id, None).await;
1244                                tracing::warn!(
1245                                    quote_id = %quote_id,
1246                                    "LDK rejected BOLT12 payment before dispatch: {err}"
1247                                );
1248                                return Ok(outgoing_payment_failure_response(
1249                                    unit,
1250                                    quote_payment_identifier,
1251                                ));
1252                            }
1253                        }
1254                        return Err(Error::LdkNode(err).into());
1255                    }
1256                };
1257
1258                // Record the payment id so QuoteId lookups resolve to the
1259                // dispatched payment. The write is conditional on owning the
1260                // dispatch claim. Best-effort: if this write fails the
1261                // sentinel remains and the payment resolves as Pending, keeping
1262                // the melt proofs reserved.
1263                if let Err(err) =
1264                    write_bolt12_quote_payment_id(&self.kv_store, &quote_id, Some(&payment_id))
1265                        .await
1266                {
1267                    tracing::error!(
1268                        "Could not record BOLT12 payment id for quote {quote_id}: {err}. \
1269                         The payment will remain Pending until manual intervention."
1270                    );
1271                }
1272
1273                let payment_details = self
1274                    .wait_for_payment_terminal_status(payment_id, payment_event_receiver)
1275                    .await?;
1276
1277                if payment_details.status == PaymentStatus::Failed {
1278                    tracing::error!(
1279                        payment_id = %payment_id,
1280                        amount_msat = ?payment_details.amount_msat,
1281                        fee_paid_msat = ?payment_details.fee_paid_msat,
1282                        payment_kind = ?payment_details.kind,
1283                        "Bolt12 payment failed"
1284                    );
1285                    self.cleanup_bolt12_dispatch_binding(&quote_id, Some(&payment_id))
1286                        .await;
1287                }
1288
1289                Self::make_payment_response_from_details(
1290                    unit,
1291                    quote_payment_identifier,
1292                    &payment_details,
1293                )
1294            }
1295            OutgoingPaymentOptions::Onchain(options) => Ok(outgoing_payment_failure_response(
1296                unit,
1297                PaymentIdentifier::QuoteId(options.quote_id),
1298            )),
1299        }
1300    }
1301
1302    /// Listen for invoices to be paid to the mint
1303    /// Returns a stream of request_lookup_id once invoices are paid
1304    #[instrument(skip(self))]
1305    async fn wait_payment_event(
1306        &self,
1307    ) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
1308        tracing::info!("Starting stream for invoices - wait_any_incoming_payment called");
1309
1310        // Set active flag to indicate stream is active
1311        self.wait_invoice_is_active.store(true, Ordering::SeqCst);
1312        tracing::debug!("wait_invoice_is_active set to true");
1313
1314        let receiver = self.receiver.clone();
1315
1316        tracing::info!("Receiver obtained successfully, creating response stream");
1317
1318        // Transform the String stream into a WaitPaymentResponse stream
1319        let response_stream = BroadcastStream::new(receiver.resubscribe());
1320
1321        // Map the stream to handle BroadcastStreamRecvError and wrap in Event
1322        let response_stream = response_stream.filter_map(|result| async move {
1323            match result {
1324                Ok(payment) => Some(cdk_common::payment::Event::PaymentReceived(payment)),
1325                Err(err) => {
1326                    tracing::warn!("Error in broadcast stream: {}", err);
1327                    None
1328                }
1329            }
1330        });
1331
1332        // Create a combined stream that also handles cancellation
1333        let cancel_token = self.wait_invoice_cancel_token.clone();
1334        let is_active = self.wait_invoice_is_active.clone();
1335
1336        let stream = Box::pin(response_stream);
1337
1338        // Set up a task to clean up when the stream is dropped
1339        tokio::spawn(async move {
1340            cancel_token.cancelled().await;
1341            tracing::info!("wait_invoice stream cancelled");
1342            is_active.store(false, Ordering::SeqCst);
1343        });
1344
1345        tracing::info!("wait_any_incoming_payment returning stream");
1346        Ok(stream)
1347    }
1348
1349    /// Is payment event stream active
1350    fn is_payment_event_stream_active(&self) -> bool {
1351        self.wait_invoice_is_active.load(Ordering::SeqCst)
1352    }
1353
1354    /// Cancel payment event stream
1355    fn cancel_payment_event_stream(&self) {
1356        self.wait_invoice_cancel_token.cancel()
1357    }
1358
1359    /// Check the status of an incoming payment
1360    async fn check_incoming_payment_status(
1361        &self,
1362        payment_identifier: &PaymentIdentifier,
1363    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
1364        // Bolt12 offers are identified by offer id and can be paid more than
1365        // once, so collect every settled inbound payment for the offer.
1366        if let PaymentIdentifier::OfferId(offer_id) = payment_identifier {
1367            let payments = self.inner.list_payments_with_filter(|p| {
1368                p.direction == PaymentDirection::Inbound
1369                    && p.status == PaymentStatus::Succeeded
1370                    && matches!(
1371                        &p.kind,
1372                        PaymentKind::Bolt12Offer { offer_id: oid, .. } if oid.to_string() == *offer_id
1373                    )
1374            });
1375
1376            return Ok(payments
1377                .into_iter()
1378                .filter_map(|p| {
1379                    let payment_id = match &p.kind {
1380                        PaymentKind::Bolt12Offer {
1381                            hash: Some(hash), ..
1382                        } => hash.to_string(),
1383                        _ => {
1384                            tracing::warn!("Bolt12 payment for offer {} missing hash", offer_id);
1385                            return None;
1386                        }
1387                    };
1388
1389                    Some(WaitPaymentResponse {
1390                        payment_identifier: payment_identifier.clone(),
1391                        payment_amount: Amount::new(p.amount_msat?, CurrencyUnit::Msat),
1392                        payment_id,
1393                    })
1394                })
1395                .collect());
1396        }
1397
1398        let payment_id_str = match payment_identifier {
1399            PaymentIdentifier::PaymentHash(hash) => hex::encode(hash),
1400            PaymentIdentifier::CustomId(id) => id.clone(),
1401            _ => return Err(Error::UnsupportedPaymentIdentifierType.into()),
1402        };
1403
1404        let payment_id = PaymentId(
1405            hex::decode(&payment_id_str)?
1406                .try_into()
1407                .map_err(|_| Error::InvalidPaymentIdLength)?,
1408        );
1409
1410        let payment_details = self
1411            .inner
1412            .payment(&payment_id)
1413            .ok_or(Error::PaymentNotFound)?;
1414
1415        if payment_details.direction == PaymentDirection::Outbound {
1416            return Err(Error::InvalidPaymentDirection.into());
1417        }
1418
1419        let amount = if payment_details.status == PaymentStatus::Succeeded {
1420            payment_details
1421                .amount_msat
1422                .ok_or(Error::CouldNotGetPaymentAmount)?
1423        } else {
1424            return Ok(vec![]);
1425        };
1426
1427        let response = WaitPaymentResponse {
1428            payment_identifier: payment_identifier.clone(),
1429            payment_amount: Amount::new(amount, CurrencyUnit::Msat),
1430            payment_id: payment_id_str,
1431        };
1432
1433        Ok(vec![response])
1434    }
1435
1436    /// Check the status of an outgoing payment
1437    async fn check_outgoing_payment(
1438        &self,
1439        request_lookup_id: &PaymentIdentifier,
1440    ) -> Result<MakePaymentResponse, Self::Err> {
1441        let payment_details = match request_lookup_id {
1442            PaymentIdentifier::PaymentHash(id_hash) => {
1443                Self::select_bolt11_payment_details(self.inner.list_payments_with_filter(|p| {
1444                    p.direction == PaymentDirection::Outbound
1445                        && matches!(&p.kind, PaymentKind::Bolt11 { hash, .. } if &hash.0 == id_hash)
1446                }))
1447            }
1448            PaymentIdentifier::PaymentId(id) => self.inner.payment(&PaymentId(*id)),
1449            PaymentIdentifier::QuoteId(quote_id) => {
1450                match read_bolt12_quote_payment_id(&self.kv_store, quote_id)
1451                    .await?
1452                    .resolve()
1453                {
1454                    Bolt12QuotePaymentIdResolution::PaymentId(payment_id) => {
1455                        self.inner.payment(&payment_id)
1456                    }
1457                    Bolt12QuotePaymentIdResolution::Status(status) => {
1458                        return Ok(MakePaymentResponse {
1459                            payment_lookup_id: request_lookup_id.clone(),
1460                            payment_proof: None,
1461                            status,
1462                            total_spent: Amount::new(0, CurrencyUnit::Msat),
1463                        });
1464                    }
1465                }
1466            }
1467            _ => {
1468                return Ok(MakePaymentResponse {
1469                    payment_lookup_id: request_lookup_id.clone(),
1470                    payment_proof: None,
1471                    status: MeltQuoteState::Unknown,
1472                    total_spent: Amount::new(0, CurrencyUnit::Msat),
1473                });
1474            }
1475        }
1476        .ok_or(Error::PaymentNotFound)?;
1477
1478        if payment_details.direction != PaymentDirection::Outbound {
1479            return Err(Error::InvalidPaymentDirection.into());
1480        }
1481
1482        if payment_details.status == PaymentStatus::Failed {
1483            if let PaymentIdentifier::QuoteId(quote_id) = request_lookup_id {
1484                self.cleanup_bolt12_dispatch_binding(quote_id, Some(&payment_details.id))
1485                    .await;
1486            }
1487        }
1488
1489        Self::make_payment_response_from_details(
1490            &CurrencyUnit::Msat,
1491            request_lookup_id.clone(),
1492            &payment_details,
1493        )
1494    }
1495}
1496
1497impl Drop for CdkLdkNode {
1498    fn drop(&mut self) {
1499        tracing::info!("Drop called on CdkLdkNode");
1500        self.wait_invoice_cancel_token.cancel();
1501        tracing::debug!("Cancelled wait_invoice token in drop");
1502    }
1503}
1504
1505/// KV key for the bolt12 melt quote id -> payment id mapping
1506fn bolt12_quote_payment_id_key(quote_id: &QuoteId) -> Result<String, Error> {
1507    match quote_id {
1508        QuoteId::UUID(uuid) => Ok(uuid.to_string()),
1509        QuoteId::BASE64(_) => Err(Error::InvalidQuoteId),
1510    }
1511}
1512
1513/// Records the bolt12 melt quote id -> payment id mapping.
1514///
1515/// `payment_id` of `None` atomically claims an absent quote with the
1516/// pre-dispatch sentinel. `Some` atomically replaces that sentinel with the
1517/// dispatched payment id. Repeating the same payment id is idempotent; every
1518/// other existing binding is rejected.
1519async fn write_bolt12_quote_payment_id(
1520    kv_store: &DynKVStore,
1521    quote_id: &QuoteId,
1522    payment_id: Option<&PaymentId>,
1523) -> Result<(), Error> {
1524    let key = bolt12_quote_payment_id_key(quote_id)?;
1525    let value = payment_id.map(|id| hex::encode(id.0)).unwrap_or_default();
1526    let mut tx = kv_store
1527        .begin_transaction()
1528        .await
1529        .map_err(|e| Error::Database(e.to_string()))?;
1530
1531    let written = match payment_id {
1532        None => {
1533            tx.kv_write_if_absent(
1534                LDK_KV_PRIMARY_NAMESPACE,
1535                LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1536                &key,
1537                value.as_bytes(),
1538            )
1539            .await
1540        }
1541        Some(_) => {
1542            tx.kv_write_if_equals(
1543                LDK_KV_PRIMARY_NAMESPACE,
1544                LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1545                &key,
1546                b"",
1547                value.as_bytes(),
1548            )
1549            .await
1550        }
1551    }
1552    .map_err(|e| Error::Database(e.to_string()))?;
1553
1554    if written {
1555        tx.commit()
1556            .await
1557            .map_err(|e| Error::Database(e.to_string()))?;
1558        return Ok(());
1559    }
1560
1561    let existing = tx
1562        .kv_read(
1563            LDK_KV_PRIMARY_NAMESPACE,
1564            LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1565            &key,
1566        )
1567        .await
1568        .map_err(|e| Error::Database(e.to_string()))?;
1569    tx.rollback()
1570        .await
1571        .map_err(|e| Error::Database(e.to_string()))?;
1572
1573    match existing {
1574        Some(existing) if payment_id.is_some() && existing.as_slice() == value.as_bytes() => Ok(()),
1575        _ => Err(Error::Bolt12QuoteAlreadyClaimed {
1576            quote_id: quote_id.to_string(),
1577        }),
1578    }
1579}
1580
1581/// Reads the bolt12 melt quote id -> payment id mapping
1582async fn read_bolt12_quote_payment_id(
1583    kv_store: &DynKVStore,
1584    quote_id: &QuoteId,
1585) -> Result<Bolt12QuotePaymentIdLookup, Error> {
1586    let key = bolt12_quote_payment_id_key(quote_id)?;
1587    let Some(stored) = kv_store
1588        .kv_read(
1589            LDK_KV_PRIMARY_NAMESPACE,
1590            LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1591            &key,
1592        )
1593        .await
1594        .map_err(|e| Error::Database(e.to_string()))?
1595    else {
1596        return Ok(Bolt12QuotePaymentIdLookup::Missing);
1597    };
1598
1599    if stored.is_empty() {
1600        return Ok(Bolt12QuotePaymentIdLookup::Dispatching);
1601    }
1602
1603    let payment_id_hex = match String::from_utf8(stored) {
1604        Ok(payment_id_hex) => payment_id_hex,
1605        Err(err) => {
1606            tracing::warn!(
1607                "LDK: invalid UTF-8 in BOLT12 payment id mapping for quote {quote_id}: {err}"
1608            );
1609            return Ok(Bolt12QuotePaymentIdLookup::Malformed);
1610        }
1611    };
1612
1613    let payment_id_bytes = match hex::decode(&payment_id_hex) {
1614        Ok(bytes) => bytes,
1615        Err(err) => {
1616            tracing::warn!(
1617                "LDK: invalid hex in BOLT12 payment id mapping for quote {quote_id}: {err}"
1618            );
1619            return Ok(Bolt12QuotePaymentIdLookup::Malformed);
1620        }
1621    };
1622
1623    let payment_id: [u8; 32] = match payment_id_bytes.try_into() {
1624        Ok(payment_id) => payment_id,
1625        Err(_) => {
1626            tracing::warn!("LDK: invalid payment id length in BOLT12 mapping for quote {quote_id}");
1627            return Ok(Bolt12QuotePaymentIdLookup::Malformed);
1628        }
1629    };
1630
1631    Ok(Bolt12QuotePaymentIdLookup::Found(PaymentId(payment_id)))
1632}
1633
1634/// Atomically removes the bolt12 quote binding only if it still matches the
1635/// expected sentinel (`None`) or payment id (`Some`).
1636async fn delete_bolt12_quote_payment_id_if_equals(
1637    kv_store: &DynKVStore,
1638    quote_id: &QuoteId,
1639    payment_id: Option<&PaymentId>,
1640) -> Result<bool, Error> {
1641    let key = bolt12_quote_payment_id_key(quote_id)?;
1642    let expected = payment_id.map(|id| hex::encode(id.0)).unwrap_or_default();
1643    let mut tx = kv_store
1644        .begin_transaction()
1645        .await
1646        .map_err(|e| Error::Database(e.to_string()))?;
1647
1648    let claimed = tx
1649        .kv_write_if_equals(
1650            LDK_KV_PRIMARY_NAMESPACE,
1651            LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1652            &key,
1653            expected.as_bytes(),
1654            LDK_KV_BOLT12_CLEANUP_MARKER,
1655        )
1656        .await
1657        .map_err(|e| Error::Database(e.to_string()))?;
1658
1659    if !claimed {
1660        tx.rollback()
1661            .await
1662            .map_err(|e| Error::Database(e.to_string()))?;
1663        return Ok(false);
1664    }
1665
1666    tx.kv_remove(
1667        LDK_KV_PRIMARY_NAMESPACE,
1668        LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1669        &key,
1670    )
1671    .await
1672    .map_err(|e| Error::Database(e.to_string()))?;
1673    tx.commit()
1674        .await
1675        .map_err(|e| Error::Database(e.to_string()))?;
1676
1677    Ok(true)
1678}
1679
1680#[cfg(test)]
1681mod tests {
1682    use super::*;
1683
1684    #[test]
1685    fn bitcoin_rpc_debug_redacts_password() {
1686        let source = ChainSource::BitcoinRpc(BitcoinRpcConfig {
1687            host: "127.0.0.1".to_string(),
1688            port: 8332,
1689            user: "rpc-user".to_string(),
1690            password: "rpc-password-secret".to_string(),
1691        });
1692
1693        let debug = format!("{source:?}");
1694
1695        assert!(debug.contains("127.0.0.1"));
1696        assert!(debug.contains("rpc-user"));
1697        assert!(debug.contains("[REDACTED]"));
1698        assert!(!debug.contains("rpc-password-secret"));
1699    }
1700
1701    #[test]
1702    fn chain_source_debug_redacts_url_credentials() {
1703        for source in [
1704            ChainSource::Esplora("https://esplora-user:esplora-secret@example.com/api".to_string()),
1705            ChainSource::Electrum(
1706                "ssl://electrum-user:electrum-secret@example.com:50002".to_string(),
1707            ),
1708        ] {
1709            let debug = format!("{source:?}");
1710
1711            assert!(debug.contains("example.com"));
1712            assert!(!debug.contains("-user"));
1713            assert!(!debug.contains("-secret"));
1714        }
1715    }
1716
1717    #[test]
1718    fn gossip_source_debug_redacts_url_credentials() {
1719        let source = GossipSource::RapidGossipSync(
1720            "https://rgs-user:rgs-secret@example.com/snapshot".to_string(),
1721        );
1722
1723        let debug = format!("{source:?}");
1724
1725        assert!(debug.contains("https://example.com/snapshot"));
1726        assert!(!debug.contains("rgs-user"));
1727        assert!(!debug.contains("rgs-secret"));
1728    }
1729
1730    fn test_payment_details(status: PaymentStatus, amount_msat: Option<u64>) -> PaymentDetails {
1731        PaymentDetails {
1732            id: PaymentId([2; 32]),
1733            kind: PaymentKind::Bolt11 {
1734                hash: PaymentHash([1; 32]),
1735                preimage: None,
1736                secret: None,
1737            },
1738            amount_msat,
1739            fee_paid_msat: None,
1740            direction: PaymentDirection::Outbound,
1741            status,
1742            latest_update_timestamp: 0,
1743        }
1744    }
1745
1746    fn test_payment_details_with_id(
1747        id: [u8; 32],
1748        status: PaymentStatus,
1749        latest_update_timestamp: u64,
1750    ) -> PaymentDetails {
1751        PaymentDetails {
1752            id: PaymentId(id),
1753            latest_update_timestamp,
1754            ..test_payment_details(status, None)
1755        }
1756    }
1757
1758    #[test]
1759    fn failed_payment_response_does_not_require_amount() {
1760        let details = test_payment_details(PaymentStatus::Failed, None);
1761
1762        let response = CdkLdkNode::make_payment_response_from_details(
1763            &CurrencyUnit::Msat,
1764            PaymentIdentifier::PaymentId([2; 32]),
1765            &details,
1766        )
1767        .expect("failed payment details should map without amount");
1768
1769        assert_eq!(response.status, MeltQuoteState::Failed);
1770        assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1771    }
1772
1773    #[test]
1774    fn pending_payment_response_does_not_require_amount() {
1775        let details = test_payment_details(PaymentStatus::Pending, None);
1776
1777        let response = CdkLdkNode::make_payment_response_from_details(
1778            &CurrencyUnit::Msat,
1779            PaymentIdentifier::PaymentId([2; 32]),
1780            &details,
1781        )
1782        .expect("pending payment details should map without amount");
1783
1784        assert_eq!(response.status, MeltQuoteState::Pending);
1785        assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1786    }
1787
1788    #[test]
1789    fn paid_payment_response_requires_amount() {
1790        let details = test_payment_details(PaymentStatus::Succeeded, None);
1791
1792        let err = CdkLdkNode::make_payment_response_from_details(
1793            &CurrencyUnit::Msat,
1794            PaymentIdentifier::PaymentId([2; 32]),
1795            &details,
1796        )
1797        .expect_err("paid payment details without amount should fail");
1798
1799        assert!(matches!(err, payment::Error::Backend(_)));
1800    }
1801
1802    #[test]
1803    fn bolt11_payment_selection_prefers_pending_over_failed() {
1804        let failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 2);
1805        let pending = test_payment_details_with_id([2; 32], PaymentStatus::Pending, 1);
1806
1807        let selected = CdkLdkNode::select_bolt11_payment_details([failed, pending])
1808            .expect("payment details should be selected");
1809
1810        assert_eq!(selected.id, PaymentId([2; 32]));
1811        assert_eq!(selected.status, PaymentStatus::Pending);
1812    }
1813
1814    #[test]
1815    fn bolt11_payment_selection_prefers_succeeded_over_pending() {
1816        let pending = test_payment_details_with_id([1; 32], PaymentStatus::Pending, 2);
1817        let succeeded = PaymentDetails {
1818            amount_msat: Some(1000),
1819            ..test_payment_details_with_id([2; 32], PaymentStatus::Succeeded, 1)
1820        };
1821
1822        let selected = CdkLdkNode::select_bolt11_payment_details([pending, succeeded])
1823            .expect("payment details should be selected");
1824
1825        assert_eq!(selected.id, PaymentId([2; 32]));
1826        assert_eq!(selected.status, PaymentStatus::Succeeded);
1827    }
1828
1829    #[test]
1830    fn bolt11_payment_selection_uses_latest_failed_when_all_failed() {
1831        let older_failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 1);
1832        let newer_failed = test_payment_details_with_id([2; 32], PaymentStatus::Failed, 2);
1833
1834        let selected = CdkLdkNode::select_bolt11_payment_details([older_failed, newer_failed])
1835            .expect("payment details should be selected");
1836
1837        assert_eq!(selected.id, PaymentId([2; 32]));
1838        assert_eq!(selected.status, PaymentStatus::Failed);
1839    }
1840
1841    #[tokio::test]
1842    async fn terminal_payment_event_wait_ignores_other_payments() {
1843        let (sender, mut receiver) = tokio::sync::broadcast::channel(4);
1844        let payment_id = PaymentId([2; 32]);
1845
1846        // Queue both events before entering the wait to exercise the race where
1847        // LDK completes immediately after dispatch returns.
1848        sender
1849            .send(PaymentId([1; 32]))
1850            .expect("receiver should be subscribed");
1851        sender
1852            .send(payment_id)
1853            .expect("receiver should be subscribed");
1854
1855        CdkLdkNode::wait_for_terminal_payment_event(&mut receiver, payment_id)
1856            .await
1857            .expect("matching terminal event should wake the waiter");
1858    }
1859
1860    #[tokio::test]
1861    async fn terminal_payment_event_wait_recovers_from_lagged_channel() {
1862        let (sender, mut receiver) = tokio::sync::broadcast::channel(2);
1863        let payment_id = PaymentId([3; 32]);
1864
1865        sender
1866            .send(PaymentId([1; 32]))
1867            .expect("receiver should be subscribed");
1868        sender
1869            .send(PaymentId([2; 32]))
1870            .expect("receiver should be subscribed");
1871        sender
1872            .send(PaymentId([4; 32]))
1873            .expect("receiver should be subscribed");
1874        sender
1875            .send(payment_id)
1876            .expect("receiver should be subscribed");
1877
1878        CdkLdkNode::wait_for_terminal_payment_event(&mut receiver, payment_id)
1879            .await
1880            .expect("receiver lag should not prevent a matching event from waking the waiter");
1881    }
1882
1883    #[tokio::test]
1884    async fn terminal_payment_event_wait_reports_closed_channel() {
1885        let (sender, mut receiver) = tokio::sync::broadcast::channel(1);
1886        drop(sender);
1887
1888        let err = CdkLdkNode::wait_for_terminal_payment_event(&mut receiver, PaymentId([2; 32]))
1889            .await
1890            .expect_err("a closed event channel should stop the wait");
1891
1892        assert!(matches!(
1893            err,
1894            tokio::sync::broadcast::error::RecvError::Closed
1895        ));
1896    }
1897
1898    #[test]
1899    fn bolt12_persistence_failure_has_ambiguous_dispatch() {
1900        assert!(bolt12_send_error_has_ambiguous_dispatch(
1901            &ldk_node::NodeError::PersistenceFailed
1902        ));
1903
1904        for not_dispatched in [
1905            ldk_node::NodeError::NotRunning,
1906            ldk_node::NodeError::UnsupportedCurrency,
1907            ldk_node::NodeError::InvalidOffer,
1908            ldk_node::NodeError::InvalidAmount,
1909            ldk_node::NodeError::DuplicatePayment,
1910            ldk_node::NodeError::InvoiceRequestCreationFailed,
1911            ldk_node::NodeError::PaymentSendingFailed,
1912        ] {
1913            assert!(
1914                !bolt12_send_error_has_ambiguous_dispatch(&not_dispatched),
1915                "{not_dispatched} must be treated as not dispatched"
1916            );
1917        }
1918    }
1919
1920    #[test]
1921    fn bolt11_send_errors_only_classify_explicit_rejections_as_terminal() {
1922        for terminal_error in [
1923            ldk_node::NodeError::NotRunning,
1924            ldk_node::NodeError::InvalidAmount,
1925            ldk_node::NodeError::InvalidInvoice,
1926            ldk_node::NodeError::PaymentSendingFailed,
1927        ] {
1928            assert!(
1929                bolt11_send_error_is_explicit_terminal_failure(&terminal_error),
1930                "{terminal_error} must be treated as a definitive failure"
1931            );
1932        }
1933
1934        for ambiguous_error in [
1935            ldk_node::NodeError::PersistenceFailed,
1936            ldk_node::NodeError::DuplicatePayment,
1937        ] {
1938            assert!(
1939                !bolt11_send_error_is_explicit_terminal_failure(&ambiguous_error),
1940                "{ambiguous_error} must not authorize proof release"
1941            );
1942        }
1943    }
1944
1945    #[test]
1946    fn authoritative_outgoing_failure_response_is_terminal_and_spends_nothing() {
1947        let payment_lookup_id = PaymentIdentifier::PaymentHash([42; 32]);
1948        let response =
1949            outgoing_payment_failure_response(&CurrencyUnit::Msat, payment_lookup_id.clone());
1950
1951        assert_eq!(response.payment_lookup_id, payment_lookup_id);
1952        assert_eq!(response.status, MeltQuoteState::Failed);
1953        assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1954        assert!(response.payment_proof.is_none());
1955    }
1956
1957    #[test]
1958    fn bolt12_quote_payment_id_lookup_resolution_is_safe() {
1959        assert_eq!(
1960            Bolt12QuotePaymentIdLookup::Dispatching.resolve(),
1961            Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Pending),
1962            "an indeterminate dispatch must keep melt proofs reserved"
1963        );
1964        assert_eq!(
1965            Bolt12QuotePaymentIdLookup::Missing.resolve(),
1966            Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unpaid),
1967            "a missing sentinel means the payment was never dispatched"
1968        );
1969        assert_eq!(
1970            Bolt12QuotePaymentIdLookup::Malformed.resolve(),
1971            Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unknown),
1972            "corrupt bookkeeping must remain indeterminate"
1973        );
1974    }
1975
1976    async fn test_kv_store() -> DynKVStore {
1977        std::sync::Arc::new(cdk_sqlite::mint::memory::empty().await.unwrap())
1978    }
1979
1980    /// The mapping must resolve Missing before any dispatch, Dispatching
1981    /// (indeterminate) while only the pre-dispatch sentinel exists, and Found
1982    /// after the payment id is recorded.
1983    #[tokio::test]
1984    async fn bolt12_quote_payment_id_mapping_lifecycle() {
1985        let kv_store = test_kv_store().await;
1986        let quote_id = QuoteId::new();
1987
1988        assert_eq!(
1989            read_bolt12_quote_payment_id(&kv_store, &quote_id)
1990                .await
1991                .unwrap(),
1992            Bolt12QuotePaymentIdLookup::Missing,
1993            "no record must resolve as never dispatched"
1994        );
1995
1996        // Pre-dispatch sentinel
1997        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
1998            .await
1999            .unwrap();
2000        assert_eq!(
2001            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2002                .await
2003                .unwrap(),
2004            Bolt12QuotePaymentIdLookup::Dispatching,
2005            "sentinel must resolve as indeterminate, never terminal"
2006        );
2007
2008        assert!(
2009            delete_bolt12_quote_payment_id_if_equals(&kv_store, &quote_id, None)
2010                .await
2011                .unwrap(),
2012            "an unambiguous pre-dispatch failure should release its sentinel"
2013        );
2014        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2015            .await
2016            .expect("a retry should reclaim the quote after sentinel cleanup");
2017
2018        // Record the payment id
2019        let payment_id = PaymentId([7; 32]);
2020        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&payment_id))
2021            .await
2022            .unwrap();
2023        assert_eq!(
2024            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2025                .await
2026                .unwrap(),
2027            Bolt12QuotePaymentIdLookup::Found(payment_id)
2028        );
2029
2030        // Removal returns to Missing (failed dispatch cleanup)
2031        assert!(
2032            delete_bolt12_quote_payment_id_if_equals(&kv_store, &quote_id, Some(&payment_id))
2033                .await
2034                .unwrap()
2035        );
2036        assert_eq!(
2037            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2038                .await
2039                .unwrap(),
2040            Bolt12QuotePaymentIdLookup::Missing
2041        );
2042    }
2043
2044    #[tokio::test]
2045    async fn bolt12_quote_payment_id_binding_is_write_once() {
2046        let kv_store = test_kv_store().await;
2047        let quote_id = QuoteId::new();
2048        let payment_id = PaymentId([7; 32]);
2049        let conflicting_payment_id = PaymentId([9; 32]);
2050
2051        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2052            .await
2053            .expect("first dispatch should claim the quote");
2054        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&payment_id))
2055            .await
2056            .expect("the claim owner should record its payment id");
2057        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&payment_id))
2058            .await
2059            .expect("repeating the same payment id must be idempotent");
2060
2061        let duplicate_dispatch = write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2062            .await
2063            .expect_err("a dispatched quote must not be claimed again");
2064        assert!(
2065            matches!(duplicate_dispatch, Error::Bolt12QuoteAlreadyClaimed { .. }),
2066            "unexpected error: {duplicate_dispatch}"
2067        );
2068
2069        let conflicting_binding =
2070            write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&conflicting_payment_id))
2071                .await
2072                .expect_err("a conflicting payment id must be rejected");
2073        assert!(
2074            matches!(conflicting_binding, Error::Bolt12QuoteAlreadyClaimed { .. }),
2075            "unexpected error: {conflicting_binding}"
2076        );
2077
2078        assert_eq!(
2079            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2080                .await
2081                .expect("payment id should remain readable"),
2082            Bolt12QuotePaymentIdLookup::Found(payment_id),
2083            "a duplicate dispatch must not redirect recovery"
2084        );
2085    }
2086
2087    #[tokio::test]
2088    async fn failed_bolt12_binding_can_be_released_without_removing_a_retry() {
2089        let kv_store = test_kv_store().await;
2090        let quote_id = QuoteId::new();
2091        let failed_payment_id = PaymentId([7; 32]);
2092        let retry_payment_id = PaymentId([9; 32]);
2093
2094        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2095            .await
2096            .expect("failed dispatch should claim the quote");
2097        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&failed_payment_id))
2098            .await
2099            .expect("failed payment id should be recorded");
2100        assert!(delete_bolt12_quote_payment_id_if_equals(
2101            &kv_store,
2102            &quote_id,
2103            Some(&failed_payment_id),
2104        )
2105        .await
2106        .expect("failed binding should be released"));
2107
2108        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2109            .await
2110            .expect("retry should claim the released quote");
2111        write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&retry_payment_id))
2112            .await
2113            .expect("retry payment id should be recorded");
2114        assert!(!delete_bolt12_quote_payment_id_if_equals(
2115            &kv_store,
2116            &quote_id,
2117            Some(&failed_payment_id),
2118        )
2119        .await
2120        .expect("stale cleanup should be checked atomically"));
2121
2122        assert_eq!(
2123            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2124                .await
2125                .expect("retry binding should remain readable"),
2126            Bolt12QuotePaymentIdLookup::Found(retry_payment_id),
2127            "stale failed-payment cleanup must not remove a newer retry"
2128        );
2129    }
2130
2131    #[tokio::test]
2132    async fn bolt12_quote_dispatch_concurrent_claims_have_single_winner() {
2133        let kv_store = test_kv_store().await;
2134        let quote_id = QuoteId::new();
2135
2136        let (first_result, second_result) = tokio::join!(
2137            write_bolt12_quote_payment_id(&kv_store, &quote_id, None),
2138            write_bolt12_quote_payment_id(&kv_store, &quote_id, None),
2139        );
2140
2141        let outcomes = [first_result, second_result];
2142        let winners = outcomes.iter().filter(|result| result.is_ok()).count();
2143        let conflicts = outcomes
2144            .iter()
2145            .filter(|result| matches!(result, Err(Error::Bolt12QuoteAlreadyClaimed { .. })))
2146            .count();
2147
2148        assert_eq!(winners, 1, "exactly one dispatch may claim the quote");
2149        assert_eq!(conflicts, 1, "the losing dispatch must be rejected");
2150    }
2151
2152    #[tokio::test]
2153    async fn bolt12_quote_payment_id_concurrent_resolution_has_single_winner() {
2154        let kv_store = test_kv_store().await;
2155        let quote_id = QuoteId::new();
2156        let first_payment_id = PaymentId([7; 32]);
2157        let second_payment_id = PaymentId([9; 32]);
2158
2159        write_bolt12_quote_payment_id(&kv_store, &quote_id, None)
2160            .await
2161            .expect("dispatch should claim the quote");
2162
2163        let (first_result, second_result) = tokio::join!(
2164            write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&first_payment_id)),
2165            write_bolt12_quote_payment_id(&kv_store, &quote_id, Some(&second_payment_id)),
2166        );
2167
2168        let outcomes = [&first_result, &second_result];
2169        let winners = outcomes.iter().filter(|result| result.is_ok()).count();
2170        let conflicts = outcomes
2171            .iter()
2172            .filter(|result| matches!(result, Err(Error::Bolt12QuoteAlreadyClaimed { .. })))
2173            .count();
2174
2175        assert_eq!(winners, 1, "exactly one payment id may resolve the claim");
2176        assert_eq!(conflicts, 1, "the losing resolution must be rejected");
2177
2178        let winner = if first_result.is_ok() {
2179            first_payment_id
2180        } else {
2181            second_payment_id
2182        };
2183        assert_eq!(
2184            read_bolt12_quote_payment_id(&kv_store, &quote_id)
2185                .await
2186                .expect("payment id should remain readable"),
2187            Bolt12QuotePaymentIdLookup::Found(winner)
2188        );
2189    }
2190
2191    /// A corrupted mapping must resolve as indeterminate (`Malformed`), never
2192    /// as a terminal state that could trigger compensation.
2193    #[tokio::test]
2194    async fn bolt12_quote_payment_id_mapping_malformed_is_indeterminate() {
2195        let kv_store = test_kv_store().await;
2196        let quote_id = QuoteId::new();
2197        let key = bolt12_quote_payment_id_key(&quote_id).unwrap();
2198
2199        for corrupt in ["not-hex", "0102", "zz"] {
2200            let mut tx = kv_store.begin_transaction().await.unwrap();
2201            tx.kv_write(
2202                LDK_KV_PRIMARY_NAMESPACE,
2203                LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
2204                &key,
2205                corrupt.as_bytes(),
2206            )
2207            .await
2208            .unwrap();
2209            tx.commit().await.unwrap();
2210
2211            assert_eq!(
2212                read_bolt12_quote_payment_id(&kv_store, &quote_id)
2213                    .await
2214                    .unwrap(),
2215                Bolt12QuotePaymentIdLookup::Malformed,
2216                "corrupt value {corrupt} must be indeterminate"
2217            );
2218        }
2219    }
2220}