1#![doc = include_str!("../README.md")]
4
5use std::net::SocketAddr;
6use std::pin::Pin;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use std::time::Duration;
10
11use async_trait::async_trait;
12use bip39::Mnemonic;
13use cdk_common::common::FeeReserve;
14use cdk_common::payment::{self, *};
15use cdk_common::util::{hex, unix_time};
16use cdk_common::{Amount, CurrencyUnit, MeltOptions, MeltQuoteState};
17use futures::{Stream, StreamExt};
18use ldk_node::bitcoin::hashes::Hash;
19use ldk_node::bitcoin::Network;
20use ldk_node::lightning::ln::channelmanager::PaymentId;
21use ldk_node::lightning::ln::msgs::SocketAddress;
22use ldk_node::lightning::routing::router::RouteParametersConfig;
23use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description};
24use ldk_node::lightning_types::payment::PaymentHash;
25use ldk_node::logger::{LogLevel, LogWriter};
26use ldk_node::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
27use ldk_node::{Builder, Event, Node};
28use tokio_stream::wrappers::BroadcastStream;
29use tokio_util::sync::CancellationToken;
30use tracing::instrument;
31
32use crate::error::Error;
33use crate::log::StdoutLogWriter;
34
35mod error;
36mod log;
37mod web;
38
39const PAYMENT_WAIT_TIMEOUT: Duration = Duration::from_secs(10);
41const PAYMENT_EVENT_CHANNEL_CAPACITY: usize = 64;
43
44#[derive(Clone)]
49pub struct CdkLdkNode {
50 inner: Arc<Node>,
51 fee_reserve: FeeReserve,
52 wait_invoice_cancel_token: CancellationToken,
53 wait_invoice_is_active: Arc<AtomicBool>,
54 sender: tokio::sync::broadcast::Sender<WaitPaymentResponse>,
55 receiver: Arc<tokio::sync::broadcast::Receiver<WaitPaymentResponse>>,
56 outgoing_payment_sender: tokio::sync::broadcast::Sender<PaymentId>,
57 events_cancel_token: CancellationToken,
58 web_addr: Option<SocketAddr>,
59}
60
61impl std::fmt::Debug for CdkLdkNode {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct("CdkLdkNode")
64 .field("fee_reserve", &self.fee_reserve)
65 .field("web_addr", &self.web_addr)
66 .finish_non_exhaustive()
67 }
68}
69
70#[derive(Debug, Clone)]
74pub struct BitcoinRpcConfig {
75 pub host: String,
77 pub port: u16,
79 pub user: String,
81 pub password: String,
83}
84
85#[derive(Debug, Clone)]
90pub enum ChainSource {
91 Esplora(String),
95 BitcoinRpc(BitcoinRpcConfig),
99}
100
101#[derive(Debug, Clone)]
106pub enum GossipSource {
107 P2P,
111 RapidGossipSync(String),
115}
116#[derive(Debug)]
118pub struct CdkLdkNodeBuilder {
119 network: Network,
120 chain_source: ChainSource,
121 gossip_source: GossipSource,
122 log_dir_path: Option<String>,
123 storage_dir_path: String,
124 fee_reserve: FeeReserve,
125 listening_addresses: Vec<SocketAddress>,
126 seed: Option<Mnemonic>,
127 announcement_addresses: Option<Vec<SocketAddress>>,
128}
129
130impl CdkLdkNodeBuilder {
131 pub fn new(
133 network: Network,
134 chain_source: ChainSource,
135 gossip_source: GossipSource,
136 storage_dir_path: String,
137 fee_reserve: FeeReserve,
138 listening_addresses: Vec<SocketAddress>,
139 ) -> Self {
140 Self {
141 network,
142 chain_source,
143 gossip_source,
144 storage_dir_path,
145 fee_reserve,
146 listening_addresses,
147 seed: None,
148 announcement_addresses: None,
149 log_dir_path: None,
150 }
151 }
152
153 pub fn with_seed(mut self, seed: Mnemonic) -> Self {
155 self.seed = Some(seed);
156 self
157 }
158 pub fn with_announcement_address(mut self, announcement_addresses: Vec<SocketAddress>) -> Self {
160 self.announcement_addresses = Some(announcement_addresses);
161 self
162 }
163 pub fn with_log_dir_path(mut self, log_dir_path: String) -> Self {
165 self.log_dir_path = Some(log_dir_path);
166 self
167 }
168
169 pub fn build(self) -> Result<CdkLdkNode, Error> {
174 let mut ldk = Builder::new();
175 ldk.set_network(self.network);
176 tracing::info!("Storage dir of node is {}", self.storage_dir_path);
177 ldk.set_storage_dir_path(self.storage_dir_path);
178
179 match self.chain_source {
180 ChainSource::Esplora(esplora_url) => {
181 ldk.set_chain_source_esplora(esplora_url, None);
182 }
183 ChainSource::BitcoinRpc(BitcoinRpcConfig {
184 host,
185 port,
186 user,
187 password,
188 }) => {
189 ldk.set_chain_source_bitcoind_rpc(host, port, user, password);
190 }
191 }
192
193 match self.gossip_source {
194 GossipSource::P2P => {
195 ldk.set_gossip_source_p2p();
196 }
197 GossipSource::RapidGossipSync(rgs_url) => {
198 ldk.set_gossip_source_rgs(rgs_url);
199 }
200 }
201
202 ldk.set_listening_addresses(self.listening_addresses)?;
203 if self.log_dir_path.is_some() {
204 ldk.set_filesystem_logger(self.log_dir_path, Some(LogLevel::Info));
205 } else {
206 ldk.set_custom_logger(Arc::new(StdoutLogWriter));
207 }
208
209 ldk.set_node_alias("cdk-ldk-node".to_string())?;
210 if let Some(seed) = self.seed {
212 ldk.set_entropy_bip39_mnemonic(seed, None);
213 }
214 if let Some(announcement_addresses) = self.announcement_addresses {
216 ldk.set_announcement_addresses(announcement_addresses)?;
217 }
218
219 let node = ldk.build()?;
220
221 tracing::info!("Creating tokio channel for payment notifications");
222 let (sender, receiver) = tokio::sync::broadcast::channel(8);
223 let (outgoing_payment_sender, _) =
224 tokio::sync::broadcast::channel(PAYMENT_EVENT_CHANNEL_CAPACITY);
225
226 let id = node.node_id();
227
228 let adr = node.announcement_addresses();
229
230 tracing::info!(
231 "Created node {} with address {:?} on network {}",
232 id,
233 adr,
234 self.network
235 );
236
237 Ok(CdkLdkNode {
238 inner: node.into(),
239 fee_reserve: self.fee_reserve,
240 wait_invoice_cancel_token: CancellationToken::new(),
241 wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
242 sender,
243 receiver: Arc::new(receiver),
244 outgoing_payment_sender,
245 events_cancel_token: CancellationToken::new(),
246 web_addr: None,
247 })
248 }
249}
250
251impl CdkLdkNode {
252 pub fn set_web_addr(&mut self, addr: Option<SocketAddr>) {
257 self.web_addr = addr;
258 }
259
260 pub fn default_web_addr() -> SocketAddr {
265 SocketAddr::from(([127, 0, 0, 1], 8091))
266 }
267
268 fn make_payment_response_from_details(
269 unit: &CurrencyUnit,
270 payment_lookup_id: PaymentIdentifier,
271 payment_details: &PaymentDetails,
272 ) -> Result<MakePaymentResponse, payment::Error> {
273 let status = match payment_details.status {
274 PaymentStatus::Pending => MeltQuoteState::Pending,
275 PaymentStatus::Succeeded => MeltQuoteState::Paid,
276 PaymentStatus::Failed => MeltQuoteState::Failed,
277 };
278
279 let payment_proof = match &payment_details.kind {
280 PaymentKind::Bolt11 { preimage, .. } => preimage.map(|p| p.to_string()),
281 PaymentKind::Bolt12Offer { preimage, .. } => preimage.map(|p| p.to_string()),
282 _ => return Err(Error::UnexpectedPaymentKind.into()),
283 };
284
285 let total_spent = if status == MeltQuoteState::Paid {
286 let total_spent = payment_details
287 .amount_msat
288 .ok_or(Error::CouldNotGetAmountSpent)?
289 + payment_details.fee_paid_msat.unwrap_or_default();
290 Amount::new(total_spent, CurrencyUnit::Msat).convert_to(unit)?
291 } else {
292 Amount::new(0, unit.clone())
293 };
294
295 Ok(MakePaymentResponse {
296 payment_lookup_id,
297 payment_proof,
298 status,
299 total_spent,
300 })
301 }
302
303 fn select_bolt11_payment_details(
304 payment_details: impl IntoIterator<Item = PaymentDetails>,
305 ) -> Option<PaymentDetails> {
306 payment_details.into_iter().min_by_key(|details| {
307 let status_order = match details.status {
308 PaymentStatus::Succeeded => 0_u8,
309 PaymentStatus::Pending => 1,
310 PaymentStatus::Failed => 2,
311 };
312
313 (
314 status_order,
315 std::cmp::Reverse(details.latest_update_timestamp),
316 )
317 })
318 }
319
320 async fn wait_for_terminal_payment_event(
321 receiver: &mut tokio::sync::broadcast::Receiver<PaymentId>,
322 payment_id: PaymentId,
323 ) -> Result<(), tokio::sync::broadcast::error::RecvError> {
324 loop {
325 match receiver.recv().await {
326 Ok(completed_payment_id) if completed_payment_id == payment_id => return Ok(()),
327 Ok(_) => continue,
328 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
329 tracing::warn!(
330 payment_id = %payment_id,
331 skipped,
332 "Terminal payment event receiver lagged; continuing to wait"
333 );
334 }
335 Err(err) => return Err(err),
336 }
337 }
338 }
339
340 async fn wait_for_payment_terminal_status(
341 &self,
342 payment_id: PaymentId,
343 mut receiver: tokio::sync::broadcast::Receiver<PaymentId>,
344 ) -> Result<PaymentDetails, payment::Error> {
345 let payment_details = self
346 .inner
347 .payment(&payment_id)
348 .ok_or(Error::PaymentNotFound)?;
349
350 if payment_details.status != PaymentStatus::Pending {
351 return Ok(payment_details);
352 }
353
354 match tokio::time::timeout(
355 PAYMENT_WAIT_TIMEOUT,
356 Self::wait_for_terminal_payment_event(&mut receiver, payment_id),
357 )
358 .await
359 {
360 Ok(Ok(())) => {}
361 Ok(Err(err)) => {
362 tracing::warn!(
363 payment_id = %payment_id,
364 "Could not wait for terminal LDK payment event: {err}"
365 );
366 }
367 Err(_) => {
368 tracing::warn!(
369 payment_id = %payment_id,
370 "Payment did not reach a terminal state within {} seconds",
371 PAYMENT_WAIT_TIMEOUT.as_secs()
372 );
373 }
374 }
375
376 let payment_details = self
377 .inner
378 .payment(&payment_id)
379 .ok_or(Error::PaymentNotFound)?;
380
381 if payment_details.status == PaymentStatus::Pending {
382 tracing::debug!(
383 payment_id = %payment_id,
384 "Payment remains pending after waiting for a terminal event"
385 );
386 }
387
388 Ok(payment_details)
389 }
390
391 pub fn start_ldk_node(&self) -> Result<(), Error> {
402 tracing::info!("Starting cdk-ldk node");
403 self.inner.start()?;
404 let node_config = self.inner.config();
405
406 tracing::info!("Starting node with network {}", node_config.network);
407
408 tracing::info!("Node status: {:?}", self.inner.status());
409
410 self.handle_events()?;
411
412 Ok(())
413 }
414
415 pub fn start_web_server(&self, web_addr: SocketAddr) -> Result<(), Error> {
430 let web_server = crate::web::WebServer::new(Arc::new(self.clone()));
431
432 tokio::spawn(async move {
433 if let Err(e) = web_server.serve(web_addr).await {
434 tracing::error!("Web server error: {}", e);
435 }
436 });
437
438 Ok(())
439 }
440
441 pub fn stop_ldk_node(&self) -> Result<(), Error> {
455 tracing::info!("Stopping CdkLdkNode");
456 tracing::info!("Cancelling event handler");
458 self.events_cancel_token.cancel();
459
460 if self.is_payment_event_stream_active() {
462 tracing::info!("Cancelling payment event stream");
463 self.wait_invoice_cancel_token.cancel();
464 }
465
466 tracing::info!("Stopping LDK node");
468 self.inner.stop()?;
469 tracing::info!("CdkLdkNode stopped successfully");
470 Ok(())
471 }
472
473 async fn handle_payment_received(
475 node: &Arc<Node>,
476 sender: &tokio::sync::broadcast::Sender<WaitPaymentResponse>,
477 payment_id: Option<PaymentId>,
478 payment_hash: PaymentHash,
479 amount_msat: u64,
480 ) {
481 tracing::info!(
482 "Received payment for hash={} of amount={} msat",
483 payment_hash,
484 amount_msat
485 );
486
487 let payment_id = match payment_id {
488 Some(id) => id,
489 None => {
490 tracing::warn!("Received payment without payment_id");
491 return;
492 }
493 };
494
495 let payment_id_hex = hex::encode(payment_id.0);
496
497 if amount_msat == 0 {
498 tracing::warn!("Payment of no amount");
499 return;
500 }
501
502 tracing::info!(
503 "Processing payment notification: id={}, amount={} msats",
504 payment_id_hex,
505 amount_msat
506 );
507
508 let payment_details = match node.payment(&payment_id) {
509 Some(details) => details,
510 None => {
511 tracing::error!("Could not find payment details for id={}", payment_id_hex);
512 return;
513 }
514 };
515
516 let (payment_identifier, payment_id) = match payment_details.kind {
517 PaymentKind::Bolt11 { hash, .. } => {
518 (PaymentIdentifier::PaymentHash(hash.0), hash.to_string())
519 }
520 PaymentKind::Bolt12Offer { hash, offer_id, .. } => match hash {
521 Some(h) => (
522 PaymentIdentifier::OfferId(offer_id.to_string()),
523 h.to_string(),
524 ),
525 None => {
526 tracing::error!("Bolt12 payment missing hash");
527 return;
528 }
529 },
530 k => {
531 tracing::warn!("Received payment of kind {:?} which is not supported", k);
532 return;
533 }
534 };
535
536 let wait_payment_response = WaitPaymentResponse {
537 payment_identifier,
538 payment_amount: Amount::new(amount_msat, CurrencyUnit::Msat),
539 payment_id,
540 };
541
542 match sender.send(wait_payment_response) {
543 Ok(_) => tracing::info!("Successfully sent payment notification to stream"),
544 Err(err) => tracing::error!(
545 "Could not send payment received notification on channel: {}",
546 err
547 ),
548 }
549 }
550
551 pub fn handle_events(&self) -> Result<(), Error> {
553 let node = self.inner.clone();
554 let sender = self.sender.clone();
555 let outgoing_payment_sender = self.outgoing_payment_sender.clone();
556 let cancel_token = self.events_cancel_token.clone();
557
558 tracing::info!("Starting event handler task");
559
560 tokio::spawn(async move {
561 tracing::info!("Event handler loop started");
562 loop {
563 tokio::select! {
564 _ = cancel_token.cancelled() => {
565 tracing::info!("Event handler cancelled");
566 break;
567 }
568 event = node.next_event_async() => {
569 match event {
570 Event::PaymentReceived {
571 payment_id,
572 payment_hash,
573 amount_msat,
574 custom_records: _
575 } => {
576 Self::handle_payment_received(
577 &node,
578 &sender,
579 payment_id,
580 payment_hash,
581 amount_msat
582 ).await;
583 }
584 Event::PaymentSuccessful {
585 payment_id,
586 payment_hash,
587 payment_preimage: _,
588 fee_paid_msat: _,
589 } => {
590 tracing::info!(
591 payment_id = ?payment_id,
592 payment_hash = %payment_hash,
593 "LDK node payment succeeded"
594 );
595 if let Some(payment_id) = payment_id {
596 let _ = outgoing_payment_sender.send(payment_id);
597 }
598 }
599 Event::PaymentFailed {
600 payment_id,
601 payment_hash,
602 reason,
603 } => {
604 tracing::error!(
605 payment_id = ?payment_id,
606 payment_hash = ?payment_hash,
607 reason = ?reason,
608 "LDK node payment failed"
609 );
610 if let Some(payment_id) = payment_id {
611 let _ = outgoing_payment_sender.send(payment_id);
612 }
613 }
614 event => {
615 tracing::debug!("Received other ldk node event: {:?}", event);
616 }
617 }
618
619 if let Err(err) = node.event_handled() {
620 tracing::error!("Error handling node event: {}", err);
621 } else {
622 tracing::debug!("Successfully handled node event");
623 }
624 }
625 }
626 }
627 tracing::info!("Event handler loop terminated");
628 });
629
630 tracing::info!("Event handler task spawned");
631 Ok(())
632 }
633
634 pub fn node(&self) -> Arc<Node> {
636 Arc::clone(&self.inner)
637 }
638}
639
640#[async_trait]
642impl MintPayment for CdkLdkNode {
643 type Err = payment::Error;
644
645 async fn start(&self) -> Result<(), Self::Err> {
648 self.start_ldk_node().map_err(|e| {
649 tracing::error!("Failed to start CdkLdkNode: {}", e);
650 e
651 })?;
652
653 tracing::info!("CdkLdkNode payment processor started successfully");
654
655 if let Some(web_addr) = self.web_addr {
657 tracing::info!("Starting LDK Node web interface on {}", web_addr);
658 self.start_web_server(web_addr).map_err(|e| {
659 tracing::error!("Failed to start web server: {}", e);
660 e
661 })?;
662 } else {
663 tracing::info!("No web server address configured, skipping web interface");
664 }
665
666 Ok(())
667 }
668
669 async fn stop(&self) -> Result<(), Self::Err> {
672 self.stop_ldk_node().map_err(|e| {
673 tracing::error!("Failed to stop CdkLdkNode: {}", e);
674 e.into()
675 })
676 }
677
678 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
680 let settings = SettingsResponse {
681 unit: CurrencyUnit::Msat.to_string(),
682 bolt11: Some(payment::Bolt11Settings {
683 mpp: false,
684 amountless: true,
685 invoice_description: true,
686 }),
687 bolt12: Some(payment::Bolt12Settings { amountless: true }),
688 onchain: None,
689 custom: std::collections::HashMap::new(),
690 };
691 Ok(settings)
692 }
693
694 #[instrument(skip(self))]
696 async fn create_incoming_payment_request(
697 &self,
698 options: IncomingPaymentOptions,
699 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
700 match options {
701 IncomingPaymentOptions::Bolt11(bolt11_options) => {
702 let amount_msat: Amount = bolt11_options
703 .amount
704 .convert_to(&CurrencyUnit::Msat)?
705 .into();
706 let description = bolt11_options.description.unwrap_or_default();
707 let time = match bolt11_options.unix_expiry {
708 Some(t) => t
709 .checked_sub(unix_time())
710 .ok_or(payment::Error::InvalidExpiry)?,
711 None => 36000,
712 };
713
714 let description = Bolt11InvoiceDescription::Direct(
715 Description::new(description).map_err(|_| Error::InvalidDescription)?,
716 );
717
718 let payment = self
719 .inner
720 .bolt11_payment()
721 .receive(amount_msat.into(), &description, time as u32)
722 .map_err(Error::LdkNode)?;
723
724 let payment_hash = payment.payment_hash().to_string();
725 let payment_identifier = PaymentIdentifier::PaymentHash(
726 hex::decode(&payment_hash)?
727 .try_into()
728 .map_err(|_| Error::InvalidPaymentHashLength)?,
729 );
730
731 Ok(CreateIncomingPaymentResponse {
732 request_lookup_id: payment_identifier,
733 request: payment.to_string(),
734 expiry: Some(unix_time() + time),
735 extra_json: None,
736 })
737 }
738 IncomingPaymentOptions::Bolt12(bolt12_options) => {
739 let Bolt12IncomingPaymentOptions {
740 description,
741 amount,
742 unix_expiry,
743 } = *bolt12_options;
744
745 let time = unix_expiry
746 .map(|t| {
747 t.checked_sub(unix_time())
748 .ok_or(payment::Error::InvalidExpiry)
749 .map(|t| t as u32)
750 })
751 .transpose()?;
752
753 let offer = match amount {
754 Some(amount) => {
755 let amount_msat: Amount = amount.convert_to(&CurrencyUnit::Msat)?.into();
756
757 self.inner
758 .bolt12_payment()
759 .receive(
760 amount_msat.into(),
761 &description.unwrap_or("".to_string()),
762 time,
763 None,
764 )
765 .map_err(Error::LdkNode)?
766 }
767 None => self
768 .inner
769 .bolt12_payment()
770 .receive_variable_amount(&description.unwrap_or("".to_string()), time)
771 .map_err(Error::LdkNode)?,
772 };
773 let payment_identifier = PaymentIdentifier::OfferId(offer.id().to_string());
774
775 Ok(CreateIncomingPaymentResponse {
776 request_lookup_id: payment_identifier,
777 request: offer.to_string(),
778 expiry: unix_expiry,
779 extra_json: None,
780 })
781 }
782 IncomingPaymentOptions::Custom(_) | IncomingPaymentOptions::Onchain(_) => {
783 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
784 }
785 }
786 }
787
788 #[instrument(skip_all)]
791 async fn get_payment_quote(
792 &self,
793 unit: &CurrencyUnit,
794 options: OutgoingPaymentOptions,
795 ) -> Result<PaymentQuoteResponse, Self::Err> {
796 match options {
797 cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
798 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
799 }
800 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
801 let bolt11 = bolt11_options.bolt11;
802
803 let amount_msat = match bolt11_options.melt_options {
804 Some(MeltOptions::Amountless { amountless }) => {
805 let amount_msat = amountless.amount_msat;
806
807 if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
808 if invoice_amount != u64::from(amount_msat) {
809 return Err(payment::Error::AmountMismatch);
810 }
811 }
812
813 amount_msat
814 }
815 Some(MeltOptions::Mpp { mpp }) => mpp.amount,
816 None => bolt11
817 .amount_milli_satoshis()
818 .ok_or(Error::UnknownInvoiceAmount)?
819 .into(),
820 };
821
822 let amount =
823 Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
824
825 let relative_fee_reserve =
826 (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
827
828 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
829
830 let fee = match relative_fee_reserve > absolute_fee_reserve {
831 true => relative_fee_reserve,
832 false => absolute_fee_reserve,
833 };
834
835 let payment_hash = bolt11.payment_hash().to_string();
836 let payment_hash_bytes = hex::decode(&payment_hash)?
837 .try_into()
838 .map_err(|_| Error::InvalidPaymentHashLength)?;
839
840 Ok(PaymentQuoteResponse {
841 request_lookup_id: Some(PaymentIdentifier::PaymentHash(payment_hash_bytes)),
842 amount,
843 fee: Amount::new(fee, unit.clone()),
844 state: MeltQuoteState::Unpaid,
845 extra_json: None,
846 estimated_blocks: None,
847 fee_options: None,
848 })
849 }
850 OutgoingPaymentOptions::Bolt12(bolt12_options) => {
851 let offer = bolt12_options.offer;
852
853 let amount_msat = match bolt12_options.melt_options {
854 Some(melt_options) => melt_options.amount_msat(),
855 None => {
856 let amount = offer.amount().ok_or(payment::Error::AmountMismatch)?;
857
858 match amount {
859 ldk_node::lightning::offers::offer::Amount::Bitcoin {
860 amount_msats,
861 } => amount_msats.into(),
862 _ => return Err(payment::Error::AmountMismatch),
863 }
864 }
865 };
866 let amount =
867 Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
868
869 let relative_fee_reserve =
870 (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
871
872 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
873
874 let fee = match relative_fee_reserve > absolute_fee_reserve {
875 true => relative_fee_reserve,
876 false => absolute_fee_reserve,
877 };
878
879 Ok(PaymentQuoteResponse {
880 request_lookup_id: None,
881 amount,
882 fee: Amount::new(fee, unit.clone()),
883 state: MeltQuoteState::Unpaid,
884 extra_json: None,
885 estimated_blocks: None,
886 fee_options: None,
887 })
888 }
889 OutgoingPaymentOptions::Onchain(_) => {
890 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
891 }
892 }
893 }
894
895 #[instrument(skip(self, options))]
897 async fn make_payment(
898 &self,
899 unit: &CurrencyUnit,
900 options: OutgoingPaymentOptions,
901 ) -> Result<MakePaymentResponse, Self::Err> {
902 match options {
903 cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
904 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
905 }
906 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
907 let bolt11 = bolt11_options.bolt11;
908
909 let send_params = match bolt11_options
910 .max_fee_amount
911 .map(|f| {
912 f.convert_to(&CurrencyUnit::Msat)
913 .map(|amount_msat| RouteParametersConfig {
914 max_total_routing_fee_msat: Some(amount_msat.value()),
915 ..Default::default()
916 })
917 })
918 .transpose()
919 {
920 Ok(params) => params,
921 Err(err) => {
922 tracing::error!("Failed to convert fee amount: {}", err);
923 return Err(payment::Error::Custom(format!("Invalid fee amount: {err}")));
924 }
925 };
926
927 let payment_event_receiver = self.outgoing_payment_sender.subscribe();
930
931 let payment_id = match bolt11_options.melt_options {
932 Some(MeltOptions::Amountless { amountless }) => {
933 if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
934 if invoice_amount != u64::from(amountless.amount_msat) {
935 return Err(payment::Error::AmountMismatch);
936 }
937 }
938
939 self.inner
940 .bolt11_payment()
941 .send_using_amount(&bolt11, amountless.amount_msat.into(), send_params)
942 .map_err(|err| {
943 tracing::error!("Could not send send amountless bolt11: {}", err);
944 Error::CouldNotSendBolt11WithoutAmount
945 })?
946 }
947 None => self
948 .inner
949 .bolt11_payment()
950 .send(&bolt11, send_params)
951 .map_err(|err| {
952 tracing::error!("Could not send bolt11 {}", err);
953 Error::CouldNotSendBolt11
954 })?,
955 _ => return Err(payment::Error::UnsupportedPaymentOption),
956 };
957
958 let payment_details = self
959 .wait_for_payment_terminal_status(payment_id, payment_event_receiver)
960 .await?;
961
962 if payment_details.status == PaymentStatus::Failed {
963 tracing::error!(payment_id = %payment_id, "Bolt11 payment failed");
964 }
965
966 Self::make_payment_response_from_details(
967 unit,
968 PaymentIdentifier::PaymentHash(bolt11.payment_hash().to_byte_array()),
969 &payment_details,
970 )
971 }
972 OutgoingPaymentOptions::Bolt12(bolt12_options) => {
973 let offer = bolt12_options.offer;
974
975 let send_params = match bolt12_options
976 .max_fee_amount
977 .map(|f| {
978 f.convert_to(&CurrencyUnit::Msat)
979 .map(|amount_msat| RouteParametersConfig {
980 max_total_routing_fee_msat: Some(amount_msat.value()),
981 ..Default::default()
982 })
983 })
984 .transpose()
985 {
986 Ok(params) => params,
987 Err(err) => {
988 tracing::error!("Failed to convert fee amount: {}", err);
989 return Err(payment::Error::Custom(format!("Invalid fee amount: {err}")));
990 }
991 };
992
993 let payment_event_receiver = self.outgoing_payment_sender.subscribe();
996
997 let payment_id = match bolt12_options.melt_options {
998 Some(MeltOptions::Amountless { amountless }) => self
999 .inner
1000 .bolt12_payment()
1001 .send_using_amount(
1002 &offer,
1003 amountless.amount_msat.into(),
1004 None,
1005 None,
1006 send_params,
1007 )
1008 .map_err(Error::LdkNode)?,
1009 None => self
1010 .inner
1011 .bolt12_payment()
1012 .send(&offer, None, None, send_params)
1013 .map_err(Error::LdkNode)?,
1014 _ => return Err(payment::Error::UnsupportedPaymentOption),
1015 };
1016
1017 let payment_details = self
1018 .wait_for_payment_terminal_status(payment_id, payment_event_receiver)
1019 .await?;
1020
1021 if payment_details.status == PaymentStatus::Failed {
1022 tracing::error!(
1023 payment_id = %payment_id,
1024 amount_msat = ?payment_details.amount_msat,
1025 fee_paid_msat = ?payment_details.fee_paid_msat,
1026 payment_kind = ?payment_details.kind,
1027 "Bolt12 payment failed"
1028 );
1029 }
1030
1031 Self::make_payment_response_from_details(
1032 unit,
1033 PaymentIdentifier::PaymentId(payment_id.0),
1034 &payment_details,
1035 )
1036 }
1037 OutgoingPaymentOptions::Onchain(_) => {
1038 Err(cdk_common::payment::Error::UnsupportedPaymentOption)
1039 }
1040 }
1041 }
1042
1043 #[instrument(skip(self))]
1046 async fn wait_payment_event(
1047 &self,
1048 ) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
1049 tracing::info!("Starting stream for invoices - wait_any_incoming_payment called");
1050
1051 self.wait_invoice_is_active.store(true, Ordering::SeqCst);
1053 tracing::debug!("wait_invoice_is_active set to true");
1054
1055 let receiver = self.receiver.clone();
1056
1057 tracing::info!("Receiver obtained successfully, creating response stream");
1058
1059 let response_stream = BroadcastStream::new(receiver.resubscribe());
1061
1062 let response_stream = response_stream.filter_map(|result| async move {
1064 match result {
1065 Ok(payment) => Some(cdk_common::payment::Event::PaymentReceived(payment)),
1066 Err(err) => {
1067 tracing::warn!("Error in broadcast stream: {}", err);
1068 None
1069 }
1070 }
1071 });
1072
1073 let cancel_token = self.wait_invoice_cancel_token.clone();
1075 let is_active = self.wait_invoice_is_active.clone();
1076
1077 let stream = Box::pin(response_stream);
1078
1079 tokio::spawn(async move {
1081 cancel_token.cancelled().await;
1082 tracing::info!("wait_invoice stream cancelled");
1083 is_active.store(false, Ordering::SeqCst);
1084 });
1085
1086 tracing::info!("wait_any_incoming_payment returning stream");
1087 Ok(stream)
1088 }
1089
1090 fn is_payment_event_stream_active(&self) -> bool {
1092 self.wait_invoice_is_active.load(Ordering::SeqCst)
1093 }
1094
1095 fn cancel_payment_event_stream(&self) {
1097 self.wait_invoice_cancel_token.cancel()
1098 }
1099
1100 async fn check_incoming_payment_status(
1102 &self,
1103 payment_identifier: &PaymentIdentifier,
1104 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
1105 if let PaymentIdentifier::OfferId(offer_id) = payment_identifier {
1108 let payments = self.inner.list_payments_with_filter(|p| {
1109 p.direction == PaymentDirection::Inbound
1110 && p.status == PaymentStatus::Succeeded
1111 && matches!(
1112 &p.kind,
1113 PaymentKind::Bolt12Offer { offer_id: oid, .. } if oid.to_string() == *offer_id
1114 )
1115 });
1116
1117 return Ok(payments
1118 .into_iter()
1119 .filter_map(|p| {
1120 let payment_id = match &p.kind {
1121 PaymentKind::Bolt12Offer {
1122 hash: Some(hash), ..
1123 } => hash.to_string(),
1124 _ => {
1125 tracing::warn!("Bolt12 payment for offer {} missing hash", offer_id);
1126 return None;
1127 }
1128 };
1129
1130 Some(WaitPaymentResponse {
1131 payment_identifier: payment_identifier.clone(),
1132 payment_amount: Amount::new(p.amount_msat?, CurrencyUnit::Msat),
1133 payment_id,
1134 })
1135 })
1136 .collect());
1137 }
1138
1139 let payment_id_str = match payment_identifier {
1140 PaymentIdentifier::PaymentHash(hash) => hex::encode(hash),
1141 PaymentIdentifier::CustomId(id) => id.clone(),
1142 _ => return Err(Error::UnsupportedPaymentIdentifierType.into()),
1143 };
1144
1145 let payment_id = PaymentId(
1146 hex::decode(&payment_id_str)?
1147 .try_into()
1148 .map_err(|_| Error::InvalidPaymentIdLength)?,
1149 );
1150
1151 let payment_details = self
1152 .inner
1153 .payment(&payment_id)
1154 .ok_or(Error::PaymentNotFound)?;
1155
1156 if payment_details.direction == PaymentDirection::Outbound {
1157 return Err(Error::InvalidPaymentDirection.into());
1158 }
1159
1160 let amount = if payment_details.status == PaymentStatus::Succeeded {
1161 payment_details
1162 .amount_msat
1163 .ok_or(Error::CouldNotGetPaymentAmount)?
1164 } else {
1165 return Ok(vec![]);
1166 };
1167
1168 let response = WaitPaymentResponse {
1169 payment_identifier: payment_identifier.clone(),
1170 payment_amount: Amount::new(amount, CurrencyUnit::Msat),
1171 payment_id: payment_id_str,
1172 };
1173
1174 Ok(vec![response])
1175 }
1176
1177 async fn check_outgoing_payment(
1179 &self,
1180 request_lookup_id: &PaymentIdentifier,
1181 ) -> Result<MakePaymentResponse, Self::Err> {
1182 let payment_details = match request_lookup_id {
1183 PaymentIdentifier::PaymentHash(id_hash) => {
1184 Self::select_bolt11_payment_details(self.inner.list_payments_with_filter(|p| {
1185 p.direction == PaymentDirection::Outbound
1186 && matches!(&p.kind, PaymentKind::Bolt11 { hash, .. } if &hash.0 == id_hash)
1187 }))
1188 }
1189 PaymentIdentifier::PaymentId(id) => self.inner.payment(&PaymentId(*id)),
1190 _ => {
1191 return Ok(MakePaymentResponse {
1192 payment_lookup_id: request_lookup_id.clone(),
1193 payment_proof: None,
1194 status: MeltQuoteState::Unknown,
1195 total_spent: Amount::new(0, CurrencyUnit::Msat),
1196 });
1197 }
1198 }
1199 .ok_or(Error::PaymentNotFound)?;
1200
1201 if payment_details.direction != PaymentDirection::Outbound {
1202 return Err(Error::InvalidPaymentDirection.into());
1203 }
1204
1205 Self::make_payment_response_from_details(
1206 &CurrencyUnit::Msat,
1207 request_lookup_id.clone(),
1208 &payment_details,
1209 )
1210 }
1211}
1212
1213impl Drop for CdkLdkNode {
1214 fn drop(&mut self) {
1215 tracing::info!("Drop called on CdkLdkNode");
1216 self.wait_invoice_cancel_token.cancel();
1217 tracing::debug!("Cancelled wait_invoice token in drop");
1218 }
1219}
1220
1221#[cfg(test)]
1222mod tests {
1223 use super::*;
1224
1225 fn test_payment_details(status: PaymentStatus, amount_msat: Option<u64>) -> PaymentDetails {
1226 PaymentDetails {
1227 id: PaymentId([2; 32]),
1228 kind: PaymentKind::Bolt11 {
1229 hash: PaymentHash([1; 32]),
1230 preimage: None,
1231 secret: None,
1232 },
1233 amount_msat,
1234 fee_paid_msat: None,
1235 direction: PaymentDirection::Outbound,
1236 status,
1237 latest_update_timestamp: 0,
1238 }
1239 }
1240
1241 fn test_payment_details_with_id(
1242 id: [u8; 32],
1243 status: PaymentStatus,
1244 latest_update_timestamp: u64,
1245 ) -> PaymentDetails {
1246 PaymentDetails {
1247 id: PaymentId(id),
1248 latest_update_timestamp,
1249 ..test_payment_details(status, None)
1250 }
1251 }
1252
1253 #[test]
1254 fn failed_payment_response_does_not_require_amount() {
1255 let details = test_payment_details(PaymentStatus::Failed, None);
1256
1257 let response = CdkLdkNode::make_payment_response_from_details(
1258 &CurrencyUnit::Msat,
1259 PaymentIdentifier::PaymentId([2; 32]),
1260 &details,
1261 )
1262 .expect("failed payment details should map without amount");
1263
1264 assert_eq!(response.status, MeltQuoteState::Failed);
1265 assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1266 }
1267
1268 #[test]
1269 fn pending_payment_response_does_not_require_amount() {
1270 let details = test_payment_details(PaymentStatus::Pending, None);
1271
1272 let response = CdkLdkNode::make_payment_response_from_details(
1273 &CurrencyUnit::Msat,
1274 PaymentIdentifier::PaymentId([2; 32]),
1275 &details,
1276 )
1277 .expect("pending payment details should map without amount");
1278
1279 assert_eq!(response.status, MeltQuoteState::Pending);
1280 assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Msat));
1281 }
1282
1283 #[test]
1284 fn paid_payment_response_requires_amount() {
1285 let details = test_payment_details(PaymentStatus::Succeeded, None);
1286
1287 let err = CdkLdkNode::make_payment_response_from_details(
1288 &CurrencyUnit::Msat,
1289 PaymentIdentifier::PaymentId([2; 32]),
1290 &details,
1291 )
1292 .expect_err("paid payment details without amount should fail");
1293
1294 assert!(matches!(err, payment::Error::Lightning(_)));
1295 }
1296
1297 #[test]
1298 fn bolt11_payment_selection_prefers_pending_over_failed() {
1299 let failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 2);
1300 let pending = test_payment_details_with_id([2; 32], PaymentStatus::Pending, 1);
1301
1302 let selected = CdkLdkNode::select_bolt11_payment_details([failed, pending])
1303 .expect("payment details should be selected");
1304
1305 assert_eq!(selected.id, PaymentId([2; 32]));
1306 assert_eq!(selected.status, PaymentStatus::Pending);
1307 }
1308
1309 #[test]
1310 fn bolt11_payment_selection_prefers_succeeded_over_pending() {
1311 let pending = test_payment_details_with_id([1; 32], PaymentStatus::Pending, 2);
1312 let succeeded = PaymentDetails {
1313 amount_msat: Some(1000),
1314 ..test_payment_details_with_id([2; 32], PaymentStatus::Succeeded, 1)
1315 };
1316
1317 let selected = CdkLdkNode::select_bolt11_payment_details([pending, succeeded])
1318 .expect("payment details should be selected");
1319
1320 assert_eq!(selected.id, PaymentId([2; 32]));
1321 assert_eq!(selected.status, PaymentStatus::Succeeded);
1322 }
1323
1324 #[test]
1325 fn bolt11_payment_selection_uses_latest_failed_when_all_failed() {
1326 let older_failed = test_payment_details_with_id([1; 32], PaymentStatus::Failed, 1);
1327 let newer_failed = test_payment_details_with_id([2; 32], PaymentStatus::Failed, 2);
1328
1329 let selected = CdkLdkNode::select_bolt11_payment_details([older_failed, newer_failed])
1330 .expect("payment details should be selected");
1331
1332 assert_eq!(selected.id, PaymentId([2; 32]));
1333 assert_eq!(selected.status, PaymentStatus::Failed);
1334 }
1335
1336 #[tokio::test]
1337 async fn terminal_payment_event_wait_ignores_other_payments() {
1338 let (sender, mut receiver) = tokio::sync::broadcast::channel(4);
1339 let payment_id = PaymentId([2; 32]);
1340
1341 sender
1344 .send(PaymentId([1; 32]))
1345 .expect("receiver should be subscribed");
1346 sender
1347 .send(payment_id)
1348 .expect("receiver should be subscribed");
1349
1350 CdkLdkNode::wait_for_terminal_payment_event(&mut receiver, payment_id)
1351 .await
1352 .expect("matching terminal event should wake the waiter");
1353 }
1354
1355 #[tokio::test]
1356 async fn terminal_payment_event_wait_recovers_from_lagged_channel() {
1357 let (sender, mut receiver) = tokio::sync::broadcast::channel(2);
1358 let payment_id = PaymentId([3; 32]);
1359
1360 sender
1361 .send(PaymentId([1; 32]))
1362 .expect("receiver should be subscribed");
1363 sender
1364 .send(PaymentId([2; 32]))
1365 .expect("receiver should be subscribed");
1366 sender
1367 .send(PaymentId([4; 32]))
1368 .expect("receiver should be subscribed");
1369 sender
1370 .send(payment_id)
1371 .expect("receiver should be subscribed");
1372
1373 CdkLdkNode::wait_for_terminal_payment_event(&mut receiver, payment_id)
1374 .await
1375 .expect("receiver lag should not prevent a matching event from waking the waiter");
1376 }
1377
1378 #[tokio::test]
1379 async fn terminal_payment_event_wait_reports_closed_channel() {
1380 let (sender, mut receiver) = tokio::sync::broadcast::channel(1);
1381 drop(sender);
1382
1383 let err = CdkLdkNode::wait_for_terminal_payment_event(&mut receiver, PaymentId([2; 32]))
1384 .await
1385 .expect_err("a closed event channel should stop the wait");
1386
1387 assert!(matches!(
1388 err,
1389 tokio::sync::broadcast::error::RecvError::Closed
1390 ));
1391 }
1392}