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