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;
10
11use async_trait::async_trait;
12use bip39::Mnemonic;
13use cdk_common::common::FeeReserve;
14use cdk_common::database::DynKVStore;
15use cdk_common::payment::{self, *};
16use cdk_common::redact::url_for_logs;
17use cdk_common::util::{hex, unix_time};
18use cdk_common::{Amount, CurrencyUnit, MeltOptions, MeltQuoteState, QuoteId};
19use futures::{Stream, StreamExt};
20use ldk_node::bitcoin::hashes::Hash;
21use ldk_node::bitcoin::Network;
22use ldk_node::lightning::ln::channelmanager::PaymentId;
23use ldk_node::lightning::ln::msgs::SocketAddress;
24use ldk_node::lightning::routing::router::RouteParametersConfig;
25use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description};
26use ldk_node::lightning_types::payment::PaymentHash;
27use ldk_node::logger::{LogLevel, LogWriter};
28use ldk_node::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
29use ldk_node::{Builder, Event, Node};
30use tokio_stream::wrappers::BroadcastStream;
31use tokio_util::sync::CancellationToken;
32use tracing::instrument;
33
34use crate::error::Error;
35use crate::log::StdoutLogWriter;
36
37mod error;
38mod log;
39mod web;
40
41const LDK_KV_PRIMARY_NAMESPACE: &str = "cdk_ldk_node_lightning_backend";
43const LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE: &str = "bolt12_outgoing_payments";
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49enum Bolt12QuotePaymentIdLookup {
50 Found(PaymentId),
52 Dispatching,
56 Missing,
58 Malformed,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63enum Bolt12QuotePaymentIdResolution {
64 PaymentId(PaymentId),
65 Status(MeltQuoteState),
66}
67
68impl Bolt12QuotePaymentIdLookup {
69 fn resolve(self) -> Bolt12QuotePaymentIdResolution {
70 match self {
71 Self::Found(payment_id) => Bolt12QuotePaymentIdResolution::PaymentId(payment_id),
72 Self::Dispatching => Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Pending),
76 Self::Missing => Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unpaid),
78 Self::Malformed => Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unknown),
80 }
81 }
82}
83
84fn bolt12_send_error_has_ambiguous_dispatch(err: &ldk_node::NodeError) -> bool {
91 matches!(err, ldk_node::NodeError::PersistenceFailed)
92}
93
94#[derive(Clone)]
99pub struct CdkLdkNode {
100 inner: Arc<Node>,
101 fee_reserve: FeeReserve,
102 kv_store: DynKVStore,
103 wait_invoice_cancel_token: CancellationToken,
104 wait_invoice_is_active: Arc<AtomicBool>,
105 sender: tokio::sync::broadcast::Sender<WaitPaymentResponse>,
106 receiver: Arc<tokio::sync::broadcast::Receiver<WaitPaymentResponse>>,
107 events_cancel_token: CancellationToken,
108 web_addr: Option<SocketAddr>,
109}
110
111impl fmt::Debug for CdkLdkNode {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 f.debug_struct("CdkLdkNode")
114 .field("fee_reserve", &self.fee_reserve)
115 .field("web_addr", &self.web_addr)
116 .finish_non_exhaustive()
117 }
118}
119
120#[derive(Clone)]
124pub struct BitcoinRpcConfig {
125 pub host: String,
127 pub port: u16,
129 pub user: String,
131 pub password: String,
133}
134
135impl fmt::Debug for BitcoinRpcConfig {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.debug_struct("BitcoinRpcConfig")
138 .field("host", &self.host)
139 .field("port", &self.port)
140 .field("user", &self.user)
141 .field("password", &"[REDACTED]")
142 .finish()
143 }
144}
145
146#[derive(Clone)]
151pub enum ChainSource {
152 Esplora(String),
156 Electrum(String),
160 BitcoinRpc(BitcoinRpcConfig),
164}
165
166impl fmt::Debug for ChainSource {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 match self {
169 Self::Esplora(url) => f.debug_tuple("Esplora").field(&url_for_logs(url)).finish(),
170 Self::Electrum(url) => f.debug_tuple("Electrum").field(&url_for_logs(url)).finish(),
171 Self::BitcoinRpc(config) => f.debug_tuple("BitcoinRpc").field(config).finish(),
172 }
173 }
174}
175
176#[derive(Clone)]
181pub enum GossipSource {
182 P2P,
186 RapidGossipSync(String),
190}
191
192impl fmt::Debug for GossipSource {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 Self::P2P => f.write_str("P2P"),
196 Self::RapidGossipSync(url) => f
197 .debug_tuple("RapidGossipSync")
198 .field(&url_for_logs(url))
199 .finish(),
200 }
201 }
202}
203pub struct CdkLdkNodeBuilder {
205 network: Network,
206 chain_source: ChainSource,
207 gossip_source: GossipSource,
208 log_dir_path: Option<String>,
209 storage_dir_path: String,
210 fee_reserve: FeeReserve,
211 kv_store: DynKVStore,
212 listening_addresses: Vec<SocketAddress>,
213 seed: Option<Mnemonic>,
214 announcement_addresses: Option<Vec<SocketAddress>>,
215}
216
217impl std::fmt::Debug for CdkLdkNodeBuilder {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.debug_struct("CdkLdkNodeBuilder")
220 .field("network", &self.network)
221 .field("chain_source", &self.chain_source)
222 .field("gossip_source", &self.gossip_source)
223 .field("log_dir_path", &self.log_dir_path)
224 .field("storage_dir_path", &self.storage_dir_path)
225 .field("fee_reserve", &self.fee_reserve)
226 .field("listening_addresses", &self.listening_addresses)
227 .field("announcement_addresses", &self.announcement_addresses)
228 .finish_non_exhaustive()
229 }
230}
231
232impl CdkLdkNodeBuilder {
233 pub fn new(
235 network: Network,
236 chain_source: ChainSource,
237 gossip_source: GossipSource,
238 storage_dir_path: String,
239 fee_reserve: FeeReserve,
240 listening_addresses: Vec<SocketAddress>,
241 kv_store: DynKVStore,
242 ) -> Self {
243 Self {
244 network,
245 chain_source,
246 gossip_source,
247 storage_dir_path,
248 fee_reserve,
249 kv_store,
250 listening_addresses,
251 seed: None,
252 announcement_addresses: None,
253 log_dir_path: None,
254 }
255 }
256
257 pub fn with_seed(mut self, seed: Mnemonic) -> Self {
259 self.seed = Some(seed);
260 self
261 }
262 pub fn with_announcement_address(mut self, announcement_addresses: Vec<SocketAddress>) -> Self {
264 self.announcement_addresses = Some(announcement_addresses);
265 self
266 }
267 pub fn with_log_dir_path(mut self, log_dir_path: String) -> Self {
269 self.log_dir_path = Some(log_dir_path);
270 self
271 }
272
273 pub fn build(self) -> Result<CdkLdkNode, Error> {
278 let mut ldk = Builder::new();
279 ldk.set_network(self.network);
280 tracing::info!("Storage dir of node is {}", self.storage_dir_path);
281 ldk.set_storage_dir_path(self.storage_dir_path);
282
283 match self.chain_source {
284 ChainSource::Esplora(esplora_url) => {
285 ldk.set_chain_source_esplora(esplora_url, None);
286 }
287 ChainSource::Electrum(electrum_url) => {
288 ldk.set_chain_source_electrum(electrum_url, None);
289 }
290 ChainSource::BitcoinRpc(BitcoinRpcConfig {
291 host,
292 port,
293 user,
294 password,
295 }) => {
296 ldk.set_chain_source_bitcoind_rpc(host, port, user, password);
297 }
298 }
299
300 match self.gossip_source {
301 GossipSource::P2P => {
302 ldk.set_gossip_source_p2p();
303 }
304 GossipSource::RapidGossipSync(rgs_url) => {
305 ldk.set_gossip_source_rgs(rgs_url);
306 }
307 }
308
309 ldk.set_listening_addresses(self.listening_addresses)?;
310 if self.log_dir_path.is_some() {
311 ldk.set_filesystem_logger(self.log_dir_path, Some(LogLevel::Info));
312 } else {
313 ldk.set_custom_logger(Arc::new(StdoutLogWriter));
314 }
315
316 ldk.set_node_alias("cdk-ldk-node".to_string())?;
317 if let Some(seed) = self.seed {
319 ldk.set_entropy_bip39_mnemonic(seed, None);
320 }
321 if let Some(announcement_addresses) = self.announcement_addresses {
323 ldk.set_announcement_addresses(announcement_addresses)?;
324 }
325
326 let node = ldk.build()?;
327
328 tracing::info!("Creating tokio channel for payment notifications");
329 let (sender, receiver) = tokio::sync::broadcast::channel(8);
330
331 let id = node.node_id();
332
333 let adr = node.announcement_addresses();
334
335 tracing::info!(
336 "Created node {} with address {:?} on network {}",
337 id,
338 adr,
339 self.network
340 );
341
342 Ok(CdkLdkNode {
343 inner: node.into(),
344 fee_reserve: self.fee_reserve,
345 kv_store: self.kv_store,
346 wait_invoice_cancel_token: CancellationToken::new(),
347 wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
348 sender,
349 receiver: Arc::new(receiver),
350 events_cancel_token: CancellationToken::new(),
351 web_addr: None,
352 })
353 }
354}
355
356impl CdkLdkNode {
357 pub fn set_web_addr(&mut self, addr: Option<SocketAddr>) {
362 self.web_addr = addr;
363 }
364
365 pub fn default_web_addr() -> SocketAddr {
370 SocketAddr::from(([127, 0, 0, 1], 8091))
371 }
372
373 async fn cleanup_bolt12_dispatch_sentinel(&self, quote_id: &QuoteId) {
377 if let Err(err) = delete_bolt12_quote_payment_id(&self.kv_store, quote_id).await {
378 tracing::warn!(
379 "Could not remove BOLT12 dispatch sentinel for quote {quote_id}: {err}. \
380 The payment will remain Pending."
381 );
382 }
383 }
384
385 fn make_payment_response_from_details(
386 unit: &CurrencyUnit,
387 payment_lookup_id: PaymentIdentifier,
388 payment_details: &PaymentDetails,
389 ) -> Result<MakePaymentResponse, payment::Error> {
390 let status = match payment_details.status {
391 PaymentStatus::Pending => MeltQuoteState::Pending,
392 PaymentStatus::Succeeded => MeltQuoteState::Paid,
393 PaymentStatus::Failed => MeltQuoteState::Failed,
394 };
395
396 let payment_proof = match &payment_details.kind {
397 PaymentKind::Bolt11 { preimage, .. } => preimage.map(|p| p.to_string()),
398 PaymentKind::Bolt12Offer { preimage, .. } => preimage.map(|p| p.to_string()),
399 _ => return Err(Error::UnexpectedPaymentKind.into()),
400 };
401
402 let total_spent = if status == MeltQuoteState::Paid {
403 let total_spent = payment_details
404 .amount_msat
405 .ok_or(Error::CouldNotGetAmountSpent)?
406 + payment_details.fee_paid_msat.unwrap_or_default();
407 Amount::new(total_spent, CurrencyUnit::Msat).convert_to(unit)?
408 } else {
409 Amount::new(0, unit.clone())
410 };
411
412 Ok(MakePaymentResponse {
413 payment_lookup_id,
414 payment_proof,
415 status,
416 total_spent,
417 })
418 }
419
420 fn select_bolt11_payment_details(
421 payment_details: impl IntoIterator<Item = PaymentDetails>,
422 ) -> Option<PaymentDetails> {
423 payment_details.into_iter().min_by_key(|details| {
424 let status_order = match details.status {
425 PaymentStatus::Succeeded => 0_u8,
426 PaymentStatus::Pending => 1,
427 PaymentStatus::Failed => 2,
428 };
429
430 (
431 status_order,
432 std::cmp::Reverse(details.latest_update_timestamp),
433 )
434 })
435 }
436
437 pub fn start_ldk_node(&self) -> Result<(), Error> {
448 tracing::info!("Starting cdk-ldk node");
449 self.inner.start()?;
450 let node_config = self.inner.config();
451
452 tracing::info!("Starting node with network {}", node_config.network);
453
454 tracing::info!("Node status: {:?}", self.inner.status());
455
456 self.handle_events()?;
457
458 Ok(())
459 }
460
461 pub fn start_web_server(&self, web_addr: SocketAddr) -> Result<(), Error> {
476 let web_server = crate::web::WebServer::new(Arc::new(self.clone()));
477
478 tokio::spawn(async move {
479 if let Err(e) = web_server.serve(web_addr).await {
480 tracing::error!("Web server error: {}", e);
481 }
482 });
483
484 Ok(())
485 }
486
487 pub fn stop_ldk_node(&self) -> Result<(), Error> {
501 tracing::info!("Stopping CdkLdkNode");
502 tracing::info!("Cancelling event handler");
504 self.events_cancel_token.cancel();
505
506 if self.is_payment_event_stream_active() {
508 tracing::info!("Cancelling payment event stream");
509 self.wait_invoice_cancel_token.cancel();
510 }
511
512 tracing::info!("Stopping LDK node");
514 self.inner.stop()?;
515 tracing::info!("CdkLdkNode stopped successfully");
516 Ok(())
517 }
518
519 async fn handle_payment_received(
521 node: &Arc<Node>,
522 sender: &tokio::sync::broadcast::Sender<WaitPaymentResponse>,
523 payment_id: Option<PaymentId>,
524 payment_hash: PaymentHash,
525 amount_msat: u64,
526 ) {
527 tracing::info!(
528 "Received payment for hash={} of amount={} msat",
529 payment_hash,
530 amount_msat
531 );
532
533 let payment_id = match payment_id {
534 Some(id) => id,
535 None => {
536 tracing::warn!("Received payment without payment_id");
537 return;
538 }
539 };
540
541 let payment_id_hex = hex::encode(payment_id.0);
542
543 if amount_msat == 0 {
544 tracing::warn!("Payment of no amount");
545 return;
546 }
547
548 tracing::info!(
549 "Processing payment notification: id={}, amount={} msats",
550 payment_id_hex,
551 amount_msat
552 );
553
554 let payment_details = match node.payment(&payment_id) {
555 Some(details) => details,
556 None => {
557 tracing::error!("Could not find payment details for id={}", payment_id_hex);
558 return;
559 }
560 };
561
562 let (payment_identifier, payment_id) = match payment_details.kind {
563 PaymentKind::Bolt11 { hash, .. } => {
564 (PaymentIdentifier::PaymentHash(hash.0), hash.to_string())
565 }
566 PaymentKind::Bolt12Offer { hash, offer_id, .. } => match hash {
567 Some(h) => (
568 PaymentIdentifier::OfferId(offer_id.to_string()),
569 h.to_string(),
570 ),
571 None => {
572 tracing::error!("Bolt12 payment missing hash");
573 return;
574 }
575 },
576 k => {
577 tracing::warn!("Received payment of kind {:?} which is not supported", k);
578 return;
579 }
580 };
581
582 let wait_payment_response = WaitPaymentResponse {
583 payment_identifier,
584 payment_amount: Amount::new(amount_msat, CurrencyUnit::Msat),
585 payment_id,
586 };
587
588 match sender.send(wait_payment_response) {
589 Ok(_) => tracing::info!("Successfully sent payment notification to stream"),
590 Err(err) => tracing::error!(
591 "Could not send payment received notification on channel: {}",
592 err
593 ),
594 }
595 }
596
597 pub fn handle_events(&self) -> Result<(), Error> {
599 let node = self.inner.clone();
600 let sender = self.sender.clone();
601 let cancel_token = self.events_cancel_token.clone();
602
603 tracing::info!("Starting event handler task");
604
605 tokio::spawn(async move {
606 tracing::info!("Event handler loop started");
607 loop {
608 tokio::select! {
609 _ = cancel_token.cancelled() => {
610 tracing::info!("Event handler cancelled");
611 break;
612 }
613 event = node.next_event_async() => {
614 match event {
615 Event::PaymentReceived {
616 payment_id,
617 payment_hash,
618 amount_msat,
619 custom_records: _
620 } => {
621 Self::handle_payment_received(
622 &node,
623 &sender,
624 payment_id,
625 payment_hash,
626 amount_msat
627 ).await;
628 }
629 Event::PaymentFailed {
630 payment_id,
631 payment_hash,
632 reason,
633 } => {
634 tracing::error!(
635 payment_id = ?payment_id,
636 payment_hash = ?payment_hash,
637 reason = ?reason,
638 "LDK node payment failed"
639 );
640 }
641 event => {
642 tracing::debug!("Received other ldk node event: {:?}", event);
643 }
644 }
645
646 if let Err(err) = node.event_handled() {
647 tracing::error!("Error handling node event: {}", err);
648 } else {
649 tracing::debug!("Successfully handled node event");
650 }
651 }
652 }
653 }
654 tracing::info!("Event handler loop terminated");
655 });
656
657 tracing::info!("Event handler task spawned");
658 Ok(())
659 }
660
661 pub fn node(&self) -> Arc<Node> {
663 Arc::clone(&self.inner)
664 }
665}
666
667#[async_trait]
669impl MintPayment for CdkLdkNode {
670 type Err = payment::Error;
671
672 async fn start(&self) -> Result<(), Self::Err> {
675 self.start_ldk_node().map_err(|e| {
676 tracing::error!("Failed to start CdkLdkNode: {}", e);
677 e
678 })?;
679
680 tracing::info!("CdkLdkNode payment processor started successfully");
681
682 if let Some(web_addr) = self.web_addr {
684 tracing::info!("Starting LDK Node web interface on {}", web_addr);
685 self.start_web_server(web_addr).map_err(|e| {
686 tracing::error!("Failed to start web server: {}", e);
687 e
688 })?;
689 } else {
690 tracing::info!("No web server address configured, skipping web interface");
691 }
692
693 Ok(())
694 }
695
696 async fn stop(&self) -> Result<(), Self::Err> {
699 self.stop_ldk_node().map_err(|e| {
700 tracing::error!("Failed to stop CdkLdkNode: {}", e);
701 e.into()
702 })
703 }
704
705 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
707 let settings = SettingsResponse {
708 unit: CurrencyUnit::Msat.to_string(),
709 bolt11: Some(payment::Bolt11Settings {
710 mpp: false,
711 amountless: true,
712 invoice_description: true,
713 }),
714 bolt12: Some(payment::Bolt12Settings { amountless: true }),
715 onchain: None,
716 custom: std::collections::HashMap::new(),
717 };
718 Ok(settings)
719 }
720
721 #[instrument(skip(self))]
723 async fn create_incoming_payment_request(
724 &self,
725 options: IncomingPaymentOptions,
726 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
727 match options {
728 IncomingPaymentOptions::Bolt11(bolt11_options) => {
729 let amount_msat: Amount = bolt11_options
730 .amount
731 .convert_to(&CurrencyUnit::Msat)?
732 .into();
733 let description = bolt11_options.description.unwrap_or_default();
734 let time = match bolt11_options.unix_expiry {
735 Some(t) => t
736 .checked_sub(unix_time())
737 .ok_or(payment::Error::InvalidExpiry)?,
738 None => 36000,
739 };
740
741 let description = Bolt11InvoiceDescription::Direct(
742 Description::new(description).map_err(|_| Error::InvalidDescription)?,
743 );
744
745 let payment = self
746 .inner
747 .bolt11_payment()
748 .receive(amount_msat.into(), &description, time as u32)
749 .map_err(Error::LdkNode)?;
750
751 let payment_hash = payment.payment_hash().to_string();
752 let payment_identifier = PaymentIdentifier::PaymentHash(
753 hex::decode(&payment_hash)?
754 .try_into()
755 .map_err(|_| Error::InvalidPaymentHashLength)?,
756 );
757
758 Ok(CreateIncomingPaymentResponse {
759 request_lookup_id: payment_identifier,
760 request: payment.to_string(),
761 expiry: Some(unix_time() + time),
762 extra_json: None,
763 })
764 }
765 IncomingPaymentOptions::Bolt12(bolt12_options) => {
766 let Bolt12IncomingPaymentOptions {
767 description,
768 amount,
769 unix_expiry,
770 } = *bolt12_options;
771
772 let time = unix_expiry
773 .map(|t| {
774 t.checked_sub(unix_time())
775 .ok_or(payment::Error::InvalidExpiry)
776 .map(|t| t as u32)
777 })
778 .transpose()?;
779
780 let offer = match amount {
781 Some(amount) => {
782 let amount_msat: Amount = amount.convert_to(&CurrencyUnit::Msat)?.into();
783
784 self.inner
785 .bolt12_payment()
786 .receive(
787 amount_msat.into(),
788 &description.unwrap_or("".to_string()),
789 time,
790 None,
791 )
792 .map_err(Error::LdkNode)?
793 }
794 None => self
795 .inner
796 .bolt12_payment()
797 .receive_variable_amount(&description.unwrap_or("".to_string()), time)
798 .map_err(Error::LdkNode)?,
799 };
800 let payment_identifier = PaymentIdentifier::OfferId(offer.id().to_string());
801
802 Ok(CreateIncomingPaymentResponse {
803 request_lookup_id: payment_identifier,
804 request: offer.to_string(),
805 expiry: unix_expiry,
806 extra_json: None,
807 })
808 }
809 IncomingPaymentOptions::Custom(_) | IncomingPaymentOptions::Onchain(_) => {
810 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
811 }
812 }
813 }
814
815 #[instrument(skip_all)]
818 async fn get_payment_quote(
819 &self,
820 unit: &CurrencyUnit,
821 options: OutgoingPaymentOptions,
822 ) -> Result<PaymentQuoteResponse, Self::Err> {
823 match options {
824 cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
825 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
826 }
827 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
828 let bolt11 = bolt11_options.bolt11;
829
830 let amount_msat = match bolt11_options.melt_options {
831 Some(MeltOptions::Amountless { amountless }) => {
832 let amount_msat = amountless.amount_msat;
833
834 if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
835 if invoice_amount != u64::from(amount_msat) {
836 return Err(payment::Error::AmountMismatch);
837 }
838 }
839
840 amount_msat
841 }
842 Some(MeltOptions::Mpp { mpp }) => mpp.amount,
843 None => bolt11
844 .amount_milli_satoshis()
845 .ok_or(Error::UnknownInvoiceAmount)?
846 .into(),
847 };
848
849 let amount =
850 Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
851
852 let relative_fee_reserve =
853 (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
854
855 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
856
857 let fee = match relative_fee_reserve > absolute_fee_reserve {
858 true => relative_fee_reserve,
859 false => absolute_fee_reserve,
860 };
861
862 let payment_hash = bolt11.payment_hash().to_string();
863 let payment_hash_bytes = hex::decode(&payment_hash)?
864 .try_into()
865 .map_err(|_| Error::InvalidPaymentHashLength)?;
866
867 Ok(PaymentQuoteResponse {
868 request_lookup_id: Some(PaymentIdentifier::PaymentHash(payment_hash_bytes)),
869 amount,
870 fee: Amount::new(fee, unit.clone()),
871 state: MeltQuoteState::Unpaid,
872 extra_json: None,
873 estimated_blocks: None,
874 fee_options: None,
875 })
876 }
877 OutgoingPaymentOptions::Bolt12(bolt12_options) => {
878 let offer = bolt12_options.offer;
879
880 let amount_msat = match bolt12_options.melt_options {
881 Some(melt_options) => melt_options.amount_msat(),
882 None => {
883 let amount = offer.amount().ok_or(payment::Error::AmountMismatch)?;
884
885 match amount {
886 ldk_node::lightning::offers::offer::Amount::Bitcoin {
887 amount_msats,
888 } => amount_msats.into(),
889 _ => return Err(payment::Error::AmountMismatch),
890 }
891 }
892 };
893 let amount =
894 Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
895
896 let relative_fee_reserve =
897 (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
898
899 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
900
901 let fee = match relative_fee_reserve > absolute_fee_reserve {
902 true => relative_fee_reserve,
903 false => absolute_fee_reserve,
904 };
905
906 Ok(PaymentQuoteResponse {
907 request_lookup_id: Some(PaymentIdentifier::QuoteId(
908 bolt12_options.quote_id.clone(),
909 )),
910 amount,
911 fee: Amount::new(fee, unit.clone()),
912 state: MeltQuoteState::Unpaid,
913 extra_json: None,
914 estimated_blocks: None,
915 fee_options: None,
916 })
917 }
918 OutgoingPaymentOptions::Onchain(_) => {
919 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
920 }
921 }
922 }
923
924 #[instrument(skip(self, options))]
926 async fn make_payment(
927 &self,
928 unit: &CurrencyUnit,
929 options: OutgoingPaymentOptions,
930 ) -> Result<MakePaymentResponse, Self::Err> {
931 match options {
932 cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
933 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
934 }
935 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
936 let bolt11 = bolt11_options.bolt11;
937
938 let send_params = match bolt11_options
939 .max_fee_amount
940 .map(|f| {
941 f.convert_to(&CurrencyUnit::Msat)
942 .map(|amount_msat| RouteParametersConfig {
943 max_total_routing_fee_msat: Some(amount_msat.value()),
944 ..Default::default()
945 })
946 })
947 .transpose()
948 {
949 Ok(params) => params,
950 Err(err) => {
951 tracing::error!("Failed to convert fee amount: {}", err);
952 return Err(payment::Error::Custom(format!("Invalid fee amount: {err}")));
953 }
954 };
955
956 let payment_id = match bolt11_options.melt_options {
957 Some(MeltOptions::Amountless { amountless }) => {
958 if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
959 if invoice_amount != u64::from(amountless.amount_msat) {
960 return Err(payment::Error::AmountMismatch);
961 }
962 }
963
964 self.inner
965 .bolt11_payment()
966 .send_using_amount(&bolt11, amountless.amount_msat.into(), send_params)
967 .map_err(|err| {
968 tracing::error!("Could not send send amountless bolt11: {}", err);
969 Error::CouldNotSendBolt11WithoutAmount
970 })?
971 }
972 None => self
973 .inner
974 .bolt11_payment()
975 .send(&bolt11, send_params)
976 .map_err(|err| {
977 tracing::error!("Could not send bolt11 {}", err);
978 Error::CouldNotSendBolt11
979 })?,
980 _ => return Err(payment::Error::UnsupportedPaymentOption),
981 };
982
983 let start = std::time::Instant::now();
985 let timeout = std::time::Duration::from_secs(10);
986
987 let payment_details = loop {
988 let details = self
989 .inner
990 .payment(&payment_id)
991 .ok_or(Error::PaymentNotFound)?;
992
993 match details.status {
994 PaymentStatus::Succeeded => break details,
995 PaymentStatus::Failed => {
996 tracing::error!("Failed to pay bolt11 payment.");
997 break details;
998 }
999 PaymentStatus::Pending => {
1000 if start.elapsed() > timeout {
1001 tracing::warn!(
1002 "Paying bolt11 exceeded timeout 10 seconds no longer waitning."
1003 );
1004 break details;
1005 }
1006 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1007 continue;
1008 }
1009 }
1010 };
1011
1012 Self::make_payment_response_from_details(
1013 unit,
1014 PaymentIdentifier::PaymentHash(bolt11.payment_hash().to_byte_array()),
1015 &payment_details,
1016 )
1017 }
1018 OutgoingPaymentOptions::Bolt12(bolt12_options) => {
1019 let offer = bolt12_options.offer;
1020 let quote_id = bolt12_options.quote_id.clone();
1021 let quote_payment_identifier = PaymentIdentifier::QuoteId(quote_id.clone());
1022
1023 let send_params = match bolt12_options
1024 .max_fee_amount
1025 .map(|f| {
1026 f.convert_to(&CurrencyUnit::Msat)
1027 .map(|amount_msat| RouteParametersConfig {
1028 max_total_routing_fee_msat: Some(amount_msat.value()),
1029 ..Default::default()
1030 })
1031 })
1032 .transpose()
1033 {
1034 Ok(params) => params,
1035 Err(err) => {
1036 tracing::error!("Failed to convert fee amount: {}", err);
1037 return Err(payment::Error::Custom(format!("Invalid fee amount: {err}")));
1038 }
1039 };
1040
1041 write_bolt12_quote_payment_id(&self.kv_store, "e_id, None).await?;
1047
1048 let payment_id = match bolt12_options.melt_options {
1049 Some(MeltOptions::Amountless { amountless }) => {
1050 self.inner.bolt12_payment().send_using_amount(
1051 &offer,
1052 amountless.amount_msat.into(),
1053 None,
1054 None,
1055 send_params,
1056 )
1057 }
1058 None => self
1059 .inner
1060 .bolt12_payment()
1061 .send(&offer, None, None, send_params),
1062 _ => {
1063 self.cleanup_bolt12_dispatch_sentinel("e_id).await;
1064 return Err(payment::Error::UnsupportedPaymentOption);
1065 }
1066 };
1067
1068 let payment_id = match payment_id {
1069 Ok(payment_id) => payment_id,
1070 Err(err) => {
1071 match bolt12_send_error_has_ambiguous_dispatch(&err) {
1072 true => {
1073 tracing::warn!(
1074 quote_id = %quote_id,
1075 "LDK payment persistence failed after BOLT12 send; retaining \
1076 the dispatch sentinel because the payment may have been dispatched"
1077 );
1078 }
1079 false => {
1080 self.cleanup_bolt12_dispatch_sentinel("e_id).await;
1081 }
1082 }
1083 return Err(Error::LdkNode(err).into());
1084 }
1085 };
1086
1087 if let Err(err) =
1092 write_bolt12_quote_payment_id(&self.kv_store, "e_id, Some(&payment_id))
1093 .await
1094 {
1095 tracing::error!(
1096 "Could not record BOLT12 payment id for quote {quote_id}: {err}. \
1097 The payment will remain Pending until manual intervention."
1098 );
1099 }
1100
1101 let start = std::time::Instant::now();
1103 let timeout = std::time::Duration::from_secs(10);
1104
1105 let payment_details = loop {
1106 let details = self
1107 .inner
1108 .payment(&payment_id)
1109 .ok_or(Error::PaymentNotFound)?;
1110
1111 match details.status {
1112 PaymentStatus::Succeeded => break details,
1113 PaymentStatus::Failed => {
1114 tracing::error!(
1115 payment_id = %payment_id,
1116 amount_msat = ?details.amount_msat,
1117 fee_paid_msat = ?details.fee_paid_msat,
1118 payment_kind = ?details.kind,
1119 "Bolt12 payment failed"
1120 );
1121 break details;
1122 }
1123 PaymentStatus::Pending => {
1124 if start.elapsed() > timeout {
1125 tracing::warn!(
1126 "Payment has been being for 10 seconds. No longer waiting"
1127 );
1128 break details;
1129 }
1130 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1131 continue;
1132 }
1133 }
1134 };
1135
1136 Self::make_payment_response_from_details(
1137 unit,
1138 quote_payment_identifier,
1139 &payment_details,
1140 )
1141 }
1142 OutgoingPaymentOptions::Onchain(_) => {
1143 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
1144 }
1145 }
1146 }
1147
1148 #[instrument(skip(self))]
1151 async fn wait_payment_event(
1152 &self,
1153 ) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
1154 tracing::info!("Starting stream for invoices - wait_any_incoming_payment called");
1155
1156 self.wait_invoice_is_active.store(true, Ordering::SeqCst);
1158 tracing::debug!("wait_invoice_is_active set to true");
1159
1160 let receiver = self.receiver.clone();
1161
1162 tracing::info!("Receiver obtained successfully, creating response stream");
1163
1164 let response_stream = BroadcastStream::new(receiver.resubscribe());
1166
1167 let response_stream = response_stream.filter_map(|result| async move {
1169 match result {
1170 Ok(payment) => Some(cdk_common::payment::Event::PaymentReceived(payment)),
1171 Err(err) => {
1172 tracing::warn!("Error in broadcast stream: {}", err);
1173 None
1174 }
1175 }
1176 });
1177
1178 let cancel_token = self.wait_invoice_cancel_token.clone();
1180 let is_active = self.wait_invoice_is_active.clone();
1181
1182 let stream = Box::pin(response_stream);
1183
1184 tokio::spawn(async move {
1186 cancel_token.cancelled().await;
1187 tracing::info!("wait_invoice stream cancelled");
1188 is_active.store(false, Ordering::SeqCst);
1189 });
1190
1191 tracing::info!("wait_any_incoming_payment returning stream");
1192 Ok(stream)
1193 }
1194
1195 fn is_payment_event_stream_active(&self) -> bool {
1197 self.wait_invoice_is_active.load(Ordering::SeqCst)
1198 }
1199
1200 fn cancel_payment_event_stream(&self) {
1202 self.wait_invoice_cancel_token.cancel()
1203 }
1204
1205 async fn check_incoming_payment_status(
1207 &self,
1208 payment_identifier: &PaymentIdentifier,
1209 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
1210 if let PaymentIdentifier::OfferId(offer_id) = payment_identifier {
1213 let payments = self.inner.list_payments_with_filter(|p| {
1214 p.direction == PaymentDirection::Inbound
1215 && p.status == PaymentStatus::Succeeded
1216 && matches!(
1217 &p.kind,
1218 PaymentKind::Bolt12Offer { offer_id: oid, .. } if oid.to_string() == *offer_id
1219 )
1220 });
1221
1222 return Ok(payments
1223 .into_iter()
1224 .filter_map(|p| {
1225 let payment_id = match &p.kind {
1226 PaymentKind::Bolt12Offer {
1227 hash: Some(hash), ..
1228 } => hash.to_string(),
1229 _ => {
1230 tracing::warn!("Bolt12 payment for offer {} missing hash", offer_id);
1231 return None;
1232 }
1233 };
1234
1235 Some(WaitPaymentResponse {
1236 payment_identifier: payment_identifier.clone(),
1237 payment_amount: Amount::new(p.amount_msat?, CurrencyUnit::Msat),
1238 payment_id,
1239 })
1240 })
1241 .collect());
1242 }
1243
1244 let payment_id_str = match payment_identifier {
1245 PaymentIdentifier::PaymentHash(hash) => hex::encode(hash),
1246 PaymentIdentifier::CustomId(id) => id.clone(),
1247 _ => return Err(Error::UnsupportedPaymentIdentifierType.into()),
1248 };
1249
1250 let payment_id = PaymentId(
1251 hex::decode(&payment_id_str)?
1252 .try_into()
1253 .map_err(|_| Error::InvalidPaymentIdLength)?,
1254 );
1255
1256 let payment_details = self
1257 .inner
1258 .payment(&payment_id)
1259 .ok_or(Error::PaymentNotFound)?;
1260
1261 if payment_details.direction == PaymentDirection::Outbound {
1262 return Err(Error::InvalidPaymentDirection.into());
1263 }
1264
1265 let amount = if payment_details.status == PaymentStatus::Succeeded {
1266 payment_details
1267 .amount_msat
1268 .ok_or(Error::CouldNotGetPaymentAmount)?
1269 } else {
1270 return Ok(vec![]);
1271 };
1272
1273 let response = WaitPaymentResponse {
1274 payment_identifier: payment_identifier.clone(),
1275 payment_amount: Amount::new(amount, CurrencyUnit::Msat),
1276 payment_id: payment_id_str,
1277 };
1278
1279 Ok(vec![response])
1280 }
1281
1282 async fn check_outgoing_payment(
1284 &self,
1285 request_lookup_id: &PaymentIdentifier,
1286 ) -> Result<MakePaymentResponse, Self::Err> {
1287 let payment_details = match request_lookup_id {
1288 PaymentIdentifier::PaymentHash(id_hash) => {
1289 Self::select_bolt11_payment_details(self.inner.list_payments_with_filter(|p| {
1290 p.direction == PaymentDirection::Outbound
1291 && matches!(&p.kind, PaymentKind::Bolt11 { hash, .. } if &hash.0 == id_hash)
1292 }))
1293 }
1294 PaymentIdentifier::PaymentId(id) => self.inner.payment(&PaymentId(*id)),
1295 PaymentIdentifier::QuoteId(quote_id) => {
1296 match read_bolt12_quote_payment_id(&self.kv_store, quote_id)
1297 .await?
1298 .resolve()
1299 {
1300 Bolt12QuotePaymentIdResolution::PaymentId(payment_id) => {
1301 self.inner.payment(&payment_id)
1302 }
1303 Bolt12QuotePaymentIdResolution::Status(status) => {
1304 return Ok(MakePaymentResponse {
1305 payment_lookup_id: request_lookup_id.clone(),
1306 payment_proof: None,
1307 status,
1308 total_spent: Amount::new(0, CurrencyUnit::Msat),
1309 });
1310 }
1311 }
1312 }
1313 _ => {
1314 return Ok(MakePaymentResponse {
1315 payment_lookup_id: request_lookup_id.clone(),
1316 payment_proof: None,
1317 status: MeltQuoteState::Unknown,
1318 total_spent: Amount::new(0, CurrencyUnit::Msat),
1319 });
1320 }
1321 }
1322 .ok_or(Error::PaymentNotFound)?;
1323
1324 if payment_details.direction != PaymentDirection::Outbound {
1325 return Err(Error::InvalidPaymentDirection.into());
1326 }
1327
1328 Self::make_payment_response_from_details(
1329 &CurrencyUnit::Msat,
1330 request_lookup_id.clone(),
1331 &payment_details,
1332 )
1333 }
1334}
1335
1336impl Drop for CdkLdkNode {
1337 fn drop(&mut self) {
1338 tracing::info!("Drop called on CdkLdkNode");
1339 self.wait_invoice_cancel_token.cancel();
1340 tracing::debug!("Cancelled wait_invoice token in drop");
1341 }
1342}
1343
1344fn bolt12_quote_payment_id_key(quote_id: &QuoteId) -> Result<String, Error> {
1346 match quote_id {
1347 QuoteId::UUID(uuid) => Ok(uuid.to_string()),
1348 QuoteId::BASE64(_) => Err(Error::InvalidQuoteId),
1349 }
1350}
1351
1352async fn write_bolt12_quote_payment_id(
1359 kv_store: &DynKVStore,
1360 quote_id: &QuoteId,
1361 payment_id: Option<&PaymentId>,
1362) -> Result<(), Error> {
1363 let key = bolt12_quote_payment_id_key(quote_id)?;
1364 let value = payment_id.map(|id| hex::encode(id.0)).unwrap_or_default();
1365 let mut tx = kv_store
1366 .begin_transaction()
1367 .await
1368 .map_err(|e| Error::Database(e.to_string()))?;
1369
1370 tx.kv_write(
1371 LDK_KV_PRIMARY_NAMESPACE,
1372 LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1373 &key,
1374 value.as_bytes(),
1375 )
1376 .await
1377 .map_err(|e| Error::Database(e.to_string()))?;
1378 tx.commit()
1379 .await
1380 .map_err(|e| Error::Database(e.to_string()))?;
1381
1382 Ok(())
1383}
1384
1385async fn read_bolt12_quote_payment_id(
1387 kv_store: &DynKVStore,
1388 quote_id: &QuoteId,
1389) -> Result<Bolt12QuotePaymentIdLookup, Error> {
1390 let key = bolt12_quote_payment_id_key(quote_id)?;
1391 let Some(stored) = kv_store
1392 .kv_read(
1393 LDK_KV_PRIMARY_NAMESPACE,
1394 LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1395 &key,
1396 )
1397 .await
1398 .map_err(|e| Error::Database(e.to_string()))?
1399 else {
1400 return Ok(Bolt12QuotePaymentIdLookup::Missing);
1401 };
1402
1403 if stored.is_empty() {
1404 return Ok(Bolt12QuotePaymentIdLookup::Dispatching);
1405 }
1406
1407 let payment_id_hex = match String::from_utf8(stored) {
1408 Ok(payment_id_hex) => payment_id_hex,
1409 Err(err) => {
1410 tracing::warn!(
1411 "LDK: invalid UTF-8 in BOLT12 payment id mapping for quote {quote_id}: {err}"
1412 );
1413 return Ok(Bolt12QuotePaymentIdLookup::Malformed);
1414 }
1415 };
1416
1417 let payment_id_bytes = match hex::decode(&payment_id_hex) {
1418 Ok(bytes) => bytes,
1419 Err(err) => {
1420 tracing::warn!(
1421 "LDK: invalid hex in BOLT12 payment id mapping for quote {quote_id}: {err}"
1422 );
1423 return Ok(Bolt12QuotePaymentIdLookup::Malformed);
1424 }
1425 };
1426
1427 let payment_id: [u8; 32] = match payment_id_bytes.try_into() {
1428 Ok(payment_id) => payment_id,
1429 Err(_) => {
1430 tracing::warn!("LDK: invalid payment id length in BOLT12 mapping for quote {quote_id}");
1431 return Ok(Bolt12QuotePaymentIdLookup::Malformed);
1432 }
1433 };
1434
1435 Ok(Bolt12QuotePaymentIdLookup::Found(PaymentId(payment_id)))
1436}
1437
1438async fn delete_bolt12_quote_payment_id(
1440 kv_store: &DynKVStore,
1441 quote_id: &QuoteId,
1442) -> Result<(), Error> {
1443 let key = bolt12_quote_payment_id_key(quote_id)?;
1444 let mut tx = kv_store
1445 .begin_transaction()
1446 .await
1447 .map_err(|e| Error::Database(e.to_string()))?;
1448
1449 tx.kv_remove(
1450 LDK_KV_PRIMARY_NAMESPACE,
1451 LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1452 &key,
1453 )
1454 .await
1455 .map_err(|e| Error::Database(e.to_string()))?;
1456 tx.commit()
1457 .await
1458 .map_err(|e| Error::Database(e.to_string()))?;
1459
1460 Ok(())
1461}
1462
1463#[cfg(test)]
1464mod tests {
1465 use super::*;
1466
1467 #[test]
1468 fn bitcoin_rpc_debug_redacts_password() {
1469 let source = ChainSource::BitcoinRpc(BitcoinRpcConfig {
1470 host: "127.0.0.1".to_string(),
1471 port: 8332,
1472 user: "rpc-user".to_string(),
1473 password: "rpc-password-secret".to_string(),
1474 });
1475
1476 let debug = format!("{source:?}");
1477
1478 assert!(debug.contains("127.0.0.1"));
1479 assert!(debug.contains("rpc-user"));
1480 assert!(debug.contains("[REDACTED]"));
1481 assert!(!debug.contains("rpc-password-secret"));
1482 }
1483
1484 #[test]
1485 fn chain_source_debug_redacts_url_credentials() {
1486 for source in [
1487 ChainSource::Esplora("https://esplora-user:esplora-secret@example.com/api".to_string()),
1488 ChainSource::Electrum(
1489 "ssl://electrum-user:electrum-secret@example.com:50002".to_string(),
1490 ),
1491 ] {
1492 let debug = format!("{source:?}");
1493
1494 assert!(debug.contains("example.com"));
1495 assert!(!debug.contains("-user"));
1496 assert!(!debug.contains("-secret"));
1497 }
1498 }
1499
1500 #[test]
1501 fn gossip_source_debug_redacts_url_credentials() {
1502 let source = GossipSource::RapidGossipSync(
1503 "https://rgs-user:rgs-secret@example.com/snapshot".to_string(),
1504 );
1505
1506 let debug = format!("{source:?}");
1507
1508 assert!(debug.contains("https://example.com/snapshot"));
1509 assert!(!debug.contains("rgs-user"));
1510 assert!(!debug.contains("rgs-secret"));
1511 }
1512
1513 fn test_payment_details(status: PaymentStatus, amount_msat: Option<u64>) -> PaymentDetails {
1514 PaymentDetails {
1515 id: PaymentId([2; 32]),
1516 kind: PaymentKind::Bolt11 {
1517 hash: PaymentHash([1; 32]),
1518 preimage: None,
1519 secret: None,
1520 },
1521 amount_msat,
1522 fee_paid_msat: None,
1523 direction: PaymentDirection::Outbound,
1524 status,
1525 latest_update_timestamp: 0,
1526 }
1527 }
1528
1529 fn test_payment_details_with_id(
1530 id: [u8; 32],
1531 status: PaymentStatus,
1532 latest_update_timestamp: u64,
1533 ) -> PaymentDetails {
1534 PaymentDetails {
1535 id: PaymentId(id),
1536 latest_update_timestamp,
1537 ..test_payment_details(status, None)
1538 }
1539 }
1540
1541 #[test]
1542 fn failed_payment_response_does_not_require_amount() {
1543 let details = test_payment_details(PaymentStatus::Failed, None);
1544
1545 let response = CdkLdkNode::make_payment_response_from_details(
1546 &CurrencyUnit::Msat,
1547 PaymentIdentifier::PaymentId([2; 32]),
1548 &details,
1549 )
1550 .expect("failed payment details should map without amount");
1551
1552 assert_eq!(response.status, MeltQuoteState::Failed);
1553 assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1554 }
1555
1556 #[test]
1557 fn pending_payment_response_does_not_require_amount() {
1558 let details = test_payment_details(PaymentStatus::Pending, None);
1559
1560 let response = CdkLdkNode::make_payment_response_from_details(
1561 &CurrencyUnit::Msat,
1562 PaymentIdentifier::PaymentId([2; 32]),
1563 &details,
1564 )
1565 .expect("pending payment details should map without amount");
1566
1567 assert_eq!(response.status, MeltQuoteState::Pending);
1568 assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1569 }
1570
1571 #[test]
1572 fn paid_payment_response_requires_amount() {
1573 let details = test_payment_details(PaymentStatus::Succeeded, None);
1574
1575 let err = CdkLdkNode::make_payment_response_from_details(
1576 &CurrencyUnit::Msat,
1577 PaymentIdentifier::PaymentId([2; 32]),
1578 &details,
1579 )
1580 .expect_err("paid payment details without amount should fail");
1581
1582 assert!(matches!(err, payment::Error::Backend(_)));
1583 }
1584
1585 #[test]
1586 fn bolt11_payment_selection_prefers_pending_over_failed() {
1587 let failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 2);
1588 let pending = test_payment_details_with_id([2; 32], PaymentStatus::Pending, 1);
1589
1590 let selected = CdkLdkNode::select_bolt11_payment_details([failed, pending])
1591 .expect("payment details should be selected");
1592
1593 assert_eq!(selected.id, PaymentId([2; 32]));
1594 assert_eq!(selected.status, PaymentStatus::Pending);
1595 }
1596
1597 #[test]
1598 fn bolt11_payment_selection_prefers_succeeded_over_pending() {
1599 let pending = test_payment_details_with_id([1; 32], PaymentStatus::Pending, 2);
1600 let succeeded = PaymentDetails {
1601 amount_msat: Some(1000),
1602 ..test_payment_details_with_id([2; 32], PaymentStatus::Succeeded, 1)
1603 };
1604
1605 let selected = CdkLdkNode::select_bolt11_payment_details([pending, succeeded])
1606 .expect("payment details should be selected");
1607
1608 assert_eq!(selected.id, PaymentId([2; 32]));
1609 assert_eq!(selected.status, PaymentStatus::Succeeded);
1610 }
1611
1612 #[test]
1613 fn bolt11_payment_selection_uses_latest_failed_when_all_failed() {
1614 let older_failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 1);
1615 let newer_failed = test_payment_details_with_id([2; 32], PaymentStatus::Failed, 2);
1616
1617 let selected = CdkLdkNode::select_bolt11_payment_details([older_failed, newer_failed])
1618 .expect("payment details should be selected");
1619
1620 assert_eq!(selected.id, PaymentId([2; 32]));
1621 assert_eq!(selected.status, PaymentStatus::Failed);
1622 }
1623
1624 #[test]
1625 fn bolt12_persistence_failure_has_ambiguous_dispatch() {
1626 assert!(bolt12_send_error_has_ambiguous_dispatch(
1627 &ldk_node::NodeError::PersistenceFailed
1628 ));
1629
1630 for not_dispatched in [
1631 ldk_node::NodeError::NotRunning,
1632 ldk_node::NodeError::UnsupportedCurrency,
1633 ldk_node::NodeError::InvalidOffer,
1634 ldk_node::NodeError::InvalidAmount,
1635 ldk_node::NodeError::DuplicatePayment,
1636 ldk_node::NodeError::InvoiceRequestCreationFailed,
1637 ldk_node::NodeError::PaymentSendingFailed,
1638 ] {
1639 assert!(
1640 !bolt12_send_error_has_ambiguous_dispatch(¬_dispatched),
1641 "{not_dispatched} must be treated as not dispatched"
1642 );
1643 }
1644 }
1645
1646 #[test]
1647 fn bolt12_quote_payment_id_lookup_resolution_is_safe() {
1648 assert_eq!(
1649 Bolt12QuotePaymentIdLookup::Dispatching.resolve(),
1650 Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Pending),
1651 "an indeterminate dispatch must keep melt proofs reserved"
1652 );
1653 assert_eq!(
1654 Bolt12QuotePaymentIdLookup::Missing.resolve(),
1655 Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unpaid),
1656 "a missing sentinel means the payment was never dispatched"
1657 );
1658 assert_eq!(
1659 Bolt12QuotePaymentIdLookup::Malformed.resolve(),
1660 Bolt12QuotePaymentIdResolution::Status(MeltQuoteState::Unknown),
1661 "corrupt bookkeeping must remain indeterminate"
1662 );
1663 }
1664
1665 async fn test_kv_store() -> DynKVStore {
1666 std::sync::Arc::new(cdk_sqlite::mint::memory::empty().await.unwrap())
1667 }
1668
1669 #[tokio::test]
1673 async fn bolt12_quote_payment_id_mapping_lifecycle() {
1674 let kv_store = test_kv_store().await;
1675 let quote_id = QuoteId::new();
1676
1677 assert_eq!(
1678 read_bolt12_quote_payment_id(&kv_store, "e_id)
1679 .await
1680 .unwrap(),
1681 Bolt12QuotePaymentIdLookup::Missing,
1682 "no record must resolve as never dispatched"
1683 );
1684
1685 write_bolt12_quote_payment_id(&kv_store, "e_id, None)
1687 .await
1688 .unwrap();
1689 assert_eq!(
1690 read_bolt12_quote_payment_id(&kv_store, "e_id)
1691 .await
1692 .unwrap(),
1693 Bolt12QuotePaymentIdLookup::Dispatching,
1694 "sentinel must resolve as indeterminate, never terminal"
1695 );
1696
1697 let payment_id = PaymentId([7; 32]);
1699 write_bolt12_quote_payment_id(&kv_store, "e_id, Some(&payment_id))
1700 .await
1701 .unwrap();
1702 assert_eq!(
1703 read_bolt12_quote_payment_id(&kv_store, "e_id)
1704 .await
1705 .unwrap(),
1706 Bolt12QuotePaymentIdLookup::Found(payment_id)
1707 );
1708
1709 delete_bolt12_quote_payment_id(&kv_store, "e_id)
1711 .await
1712 .unwrap();
1713 assert_eq!(
1714 read_bolt12_quote_payment_id(&kv_store, "e_id)
1715 .await
1716 .unwrap(),
1717 Bolt12QuotePaymentIdLookup::Missing
1718 );
1719 }
1720
1721 #[tokio::test]
1724 async fn bolt12_quote_payment_id_mapping_malformed_is_indeterminate() {
1725 let kv_store = test_kv_store().await;
1726 let quote_id = QuoteId::new();
1727 let key = bolt12_quote_payment_id_key("e_id).unwrap();
1728
1729 for corrupt in ["not-hex", "0102", "zz"] {
1730 let mut tx = kv_store.begin_transaction().await.unwrap();
1731 tx.kv_write(
1732 LDK_KV_PRIMARY_NAMESPACE,
1733 LDK_KV_BOLT12_OUTGOING_SECONDARY_NAMESPACE,
1734 &key,
1735 corrupt.as_bytes(),
1736 )
1737 .await
1738 .unwrap();
1739 tx.commit().await.unwrap();
1740
1741 assert_eq!(
1742 read_bolt12_quote_payment_id(&kv_store, "e_id)
1743 .await
1744 .unwrap(),
1745 Bolt12QuotePaymentIdLookup::Malformed,
1746 "corrupt value {corrupt} must be indeterminate"
1747 );
1748 }
1749 }
1750}