1#![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
42const LDK_KV_PRIMARY_NAMESPACE: &str = "cdk_ldk_node_lightning_backend";
44const LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE: &str = "bolt12_outgoing_payments";
47const PAYMENT_WAIT_TIMEOUT: Duration = Duration::from_secs(10);
49const PAYMENT_EVENT_CHANNEL_CAPACITY: usize = 64;
51const LDK_KV_BOLT12_CLEANUP_MARKER: &[u8] = b"cleanup-in-progress";
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55enum Bolt12QuotePaymentIdLookup {
56 Found(PaymentId),
58 Dispatching,
62 Missing,
64 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 Self::Dispatching => Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Pending),
82 Self::Missing => Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unpaid),
84 Self::Malformed => Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unknown),
86 }
87 }
88}
89
90fn bolt12_send_error_has_ambiguous_dispatch(err: &ldk_node::NodeError) -> bool {
97 matches!(err, ldk_node::NodeError::PersistenceFailed)
98}
99
100fn 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#[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#[derive(Clone)]
161pub struct BitcoinRpcConfig {
162 pub host: String,
164 pub port: u16,
166 pub user: String,
168 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#[derive(Clone)]
188pub enum ChainSource {
189 Esplora(String),
193 Electrum(String),
197 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#[derive(Clone)]
218pub enum GossipSource {
219 P2P,
223 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}
240pub 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 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 pub fn with_seed(mut self, seed: Mnemonic) -> Self {
296 self.seed = Some(seed);
297 self
298 }
299 pub fn with_announcement_address(mut self, announcement_addresses: Vec<SocketAddress>) -> Self {
301 self.announcement_addresses = Some(announcement_addresses);
302 self
303 }
304 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 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 if let Some(seed) = self.seed {
356 ldk.set_entropy_bip39_mnemonic(seed, None);
357 }
358 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 pub fn set_web_addr(&mut self, addr: Option<SocketAddr>) {
402 self.web_addr = addr;
403 }
404
405 pub fn default_web_addr() -> SocketAddr {
410 SocketAddr::from(([127, 0, 0, 1], 8091))
411 }
412
413 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 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 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 pub fn stop_ldk_node(&self) -> Result<(), Error> {
624 tracing::info!("Stopping CdkLdkNode");
625 tracing::info!("Cancelling event handler");
627 self.events_cancel_token.cancel();
628
629 if self.is_payment_event_stream_active() {
631 tracing::info!("Cancelling payment event stream");
632 self.wait_invoice_cancel_token.cancel();
633 }
634
635 tracing::info!("Stopping LDK node");
637 self.inner.stop()?;
638 tracing::info!("CdkLdkNode stopped successfully");
639 Ok(())
640 }
641
642 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 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 pub fn node(&self) -> Arc<Node> {
805 Arc::clone(&self.inner)
806 }
807}
808
809#[async_trait]
811impl MintPayment for CdkLdkNode {
812 type Err = payment::Error;
813
814 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 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 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 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 #[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 #[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 #[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 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 if let Err(err) =
1192 write_bolt12_quote_payment_id(&self.kv_store, "e_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 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("e_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("e_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 if let Err(err) =
1264 write_bolt12_quote_payment_id(&self.kv_store, "e_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("e_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 #[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 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 let response_stream = BroadcastStream::new(receiver.resubscribe());
1320
1321 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 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 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 fn is_payment_event_stream_active(&self) -> bool {
1351 self.wait_invoice_is_active.load(Ordering::SeqCst)
1352 }
1353
1354 fn cancel_payment_event_stream(&self) {
1356 self.wait_invoice_cancel_token.cancel()
1357 }
1358
1359 async fn check_incoming_payment_status(
1361 &self,
1362 payment_identifier: &PaymentIdentifier,
1363 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
1364 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 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
1505fn 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
1513async 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
1581async 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
1634async 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 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(¬_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 #[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, "e_id)
1990 .await
1991 .unwrap(),
1992 Bolt12QuotePaymentIdLookup::Missing,
1993 "no record must resolve as never dispatched"
1994 );
1995
1996 write_bolt12_quote_payment_id(&kv_store, "e_id, None)
1998 .await
1999 .unwrap();
2000 assert_eq!(
2001 read_bolt12_quote_payment_id(&kv_store, "e_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, "e_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, "e_id, None)
2015 .await
2016 .expect("a retry should reclaim the quote after sentinel cleanup");
2017
2018 let payment_id = PaymentId([7; 32]);
2020 write_bolt12_quote_payment_id(&kv_store, "e_id, Some(&payment_id))
2021 .await
2022 .unwrap();
2023 assert_eq!(
2024 read_bolt12_quote_payment_id(&kv_store, "e_id)
2025 .await
2026 .unwrap(),
2027 Bolt12QuotePaymentIdLookup::Found(payment_id)
2028 );
2029
2030 assert!(
2032 delete_bolt12_quote_payment_id_if_equals(&kv_store, "e_id, Some(&payment_id))
2033 .await
2034 .unwrap()
2035 );
2036 assert_eq!(
2037 read_bolt12_quote_payment_id(&kv_store, "e_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, "e_id, None)
2052 .await
2053 .expect("first dispatch should claim the quote");
2054 write_bolt12_quote_payment_id(&kv_store, "e_id, Some(&payment_id))
2055 .await
2056 .expect("the claim owner should record its payment id");
2057 write_bolt12_quote_payment_id(&kv_store, "e_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, "e_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, "e_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, "e_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, "e_id, None)
2095 .await
2096 .expect("failed dispatch should claim the quote");
2097 write_bolt12_quote_payment_id(&kv_store, "e_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 "e_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, "e_id, None)
2109 .await
2110 .expect("retry should claim the released quote");
2111 write_bolt12_quote_payment_id(&kv_store, "e_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 "e_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, "e_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, "e_id, None),
2138 write_bolt12_quote_payment_id(&kv_store, "e_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, "e_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, "e_id, Some(&first_payment_id)),
2165 write_bolt12_quote_payment_id(&kv_store, "e_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, "e_id)
2185 .await
2186 .expect("payment id should remain readable"),
2187 Bolt12QuotePaymentIdLookup::Found(winner)
2188 );
2189 }
2190
2191 #[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("e_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, "e_id)
2213 .await
2214 .unwrap(),
2215 Bolt12QuotePaymentIdLookup::Malformed,
2216 "corrupt value {corrupt} must be indeterminate"
2217 );
2218 }
2219 }
2220}