1use std::collections::{BTreeMap, HashMap};
2use std::path::Path;
3use std::str::FromStr;
4use std::sync::Arc;
5use std::time::{Duration, UNIX_EPOCH};
6
7use async_trait::async_trait;
8use bitcoin::hashes::{Hash, sha256};
9use bitcoin::{FeeRate, Network, OutPoint};
10use fedimint_bip39::Mnemonic;
11use fedimint_core::envs::is_running_in_test_env;
12use fedimint_core::task::{TaskGroup, TaskHandle, block_in_place};
13use fedimint_core::util::{FmtCompact, SafeUrl};
14use fedimint_core::{Amount, BitcoinAmountOrAll, crit};
15use fedimint_gateway_common::{
16 ChainSource, ConnectPeerRequest, GetInvoiceRequest, GetInvoiceResponse,
17 ListTransactionsResponse, NodeAddress, SetChannelFeesRequest,
18};
19use fedimint_ln_common::contracts::Preimage;
20use fedimint_logging::{LOG_LIGHTNING, LOG_LIGHTNING_LDK};
21use ldk_node::config::ChannelConfig;
22use ldk_node::lightning::ln::msgs::SocketAddress;
23use ldk_node::lightning::routing::gossip::{NodeAlias, NodeId};
24use ldk_node::logger::{LogLevel, LogRecord, LogWriter};
25use ldk_node::payment::{
26 PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, SendingParameters,
27};
28use lightning::ln::channelmanager::PaymentId;
29use lightning::offers::offer::{Offer, OfferId};
30use lightning::types::payment::{PaymentHash, PaymentPreimage};
31use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
32use tokio::sync::mpsc::Sender;
33use tokio::sync::{RwLock, oneshot};
34use tokio_stream::wrappers::ReceiverStream;
35use tracing::{debug, error, info, trace, warn};
36
37use super::{ChannelInfo, ILnRpcClient, LightningRpcError, ListChannelsResponse, RouteHtlcStream};
38use crate::{
39 CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, CreateInvoiceRequest,
40 CreateInvoiceResponse, GetBalancesResponse, GetLnOnchainAddressResponse, GetNodeInfoResponse,
41 GetRouteHintsResponse, InterceptPaymentRequest, InterceptPaymentResponse, InvoiceDescription,
42 NO_INCOMING_CIRCUIT, OpenChannelRequest, OpenChannelResponse, PayInvoiceResponse,
43 PaymentAction, SendOnchainRequest, SendOnchainResponse,
44};
45
46struct LdkTracingLogger {
54 in_test_env: bool,
58}
59
60impl LdkTracingLogger {
61 fn downgraded_level(&self, record: &LogRecord<'_>) -> LogLevel {
71 if self.in_test_env
72 && record.level == LogLevel::Error
73 && record.module_path == "ldk_node::chain"
74 && format!("{}", record.args).contains("Failed to retrieve fee rate estimates")
75 {
76 LogLevel::Debug
77 } else {
78 record.level
79 }
80 }
81}
82
83impl LogWriter for LdkTracingLogger {
84 fn log(&self, record: LogRecord<'_>) {
85 match self.downgraded_level(&record) {
87 LogLevel::Gossip | LogLevel::Trace => trace!(
88 target: LOG_LIGHTNING_LDK,
89 ldk_module = record.module_path, line = record.line, "{}", record.args,
90 ),
91 LogLevel::Debug => debug!(
92 target: LOG_LIGHTNING_LDK,
93 ldk_module = record.module_path, line = record.line, "{}", record.args,
94 ),
95 LogLevel::Info => info!(
96 target: LOG_LIGHTNING_LDK,
97 ldk_module = record.module_path, line = record.line, "{}", record.args,
98 ),
99 LogLevel::Warn => warn!(
100 target: LOG_LIGHTNING_LDK,
101 ldk_module = record.module_path, line = record.line, "{}", record.args,
102 ),
103 LogLevel::Error => error!(
104 target: LOG_LIGHTNING_LDK,
105 ldk_module = record.module_path, line = record.line, "{}", record.args,
106 ),
107 }
108 }
109}
110
111pub struct GatewayLdkClient {
112 node: Arc<ldk_node::Node>,
114
115 task_group: TaskGroup,
116
117 htlc_stream_receiver_or: Option<tokio::sync::mpsc::Receiver<InterceptPaymentRequest>>,
120
121 outbound_lightning_payment_lock_pool: lockable::LockPool<PaymentId>,
125
126 outbound_offer_lock_pool: lockable::LockPool<LdkOfferId>,
131
132 pending_channels:
137 Arc<RwLock<BTreeMap<UserChannelId, oneshot::Sender<anyhow::Result<OutPoint>>>>>,
138
139 pending_payments: Arc<RwLock<HashMap<PaymentId, oneshot::Sender<()>>>>,
145}
146
147impl std::fmt::Debug for GatewayLdkClient {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct("GatewayLdkClient").finish_non_exhaustive()
150 }
151}
152
153impl GatewayLdkClient {
154 pub fn new(
159 data_dir: &Path,
160 chain_source: ChainSource,
161 network: Network,
162 lightning_port: u16,
163 alias: String,
164 mnemonic: Mnemonic,
165 runtime: Arc<tokio::runtime::Runtime>,
166 ) -> anyhow::Result<Self> {
167 let mut bytes = [0u8; 32];
168 let alias = if alias.is_empty() {
169 "LDK Gateway".to_string()
170 } else {
171 alias
172 };
173 let alias_bytes = alias.as_bytes();
174 let truncated = &alias_bytes[..alias_bytes.len().min(32)];
175 bytes[..truncated.len()].copy_from_slice(truncated);
176 let node_alias = Some(NodeAlias(bytes));
177
178 let mut node_builder = ldk_node::Builder::from_config(ldk_node::config::Config {
179 network,
180 listening_addresses: Some(vec![SocketAddress::TcpIpV4 {
181 addr: [0, 0, 0, 0],
182 port: lightning_port,
183 }]),
184 node_alias,
185 ..Default::default()
186 });
187
188 node_builder.set_custom_logger(Arc::new(LdkTracingLogger {
192 in_test_env: is_running_in_test_env(),
193 }));
194
195 node_builder.set_entropy_bip39_mnemonic(mnemonic, None);
196
197 match chain_source.clone() {
198 ChainSource::Bitcoind {
199 username,
200 password,
201 server_url,
202 } => {
203 node_builder.set_chain_source_bitcoind_rpc(
204 server_url
205 .host_str()
206 .expect("Could not retrieve host from bitcoind RPC url")
207 .to_string(),
208 server_url
209 .port()
210 .expect("Could not retrieve port from bitcoind RPC url"),
211 username,
212 password,
213 );
214 }
215 ChainSource::Esplora { server_url } => {
216 node_builder.set_chain_source_esplora(get_esplora_url(server_url)?, None);
217 }
218 };
219 let Some(data_dir_str) = data_dir.to_str() else {
220 return Err(anyhow::anyhow!("Invalid data dir path"));
221 };
222 node_builder.set_storage_dir_path(data_dir_str.to_string());
223
224 info!(chain_source = %chain_source, data_dir = %data_dir_str, alias = %alias, "Starting LDK Node...");
225 let node = Arc::new(node_builder.build()?);
226 node.start_with_runtime(runtime).map_err(|err| {
227 crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to start LDK Node");
228 LightningRpcError::FailedToConnect
229 })?;
230
231 let (htlc_stream_sender, htlc_stream_receiver) = tokio::sync::mpsc::channel(1024);
232 let task_group = TaskGroup::new();
233
234 let node_clone = node.clone();
235 let pending_channels = Arc::new(RwLock::new(BTreeMap::new()));
236 let pending_channels_clone = pending_channels.clone();
237 let pending_payments = Arc::new(RwLock::new(HashMap::new()));
238 let pending_payments_clone = pending_payments.clone();
239 task_group.spawn("ldk lightning node event handler", |handle| async move {
240 loop {
241 Self::handle_next_event(
242 &node_clone,
243 &htlc_stream_sender,
244 &handle,
245 pending_channels_clone.clone(),
246 pending_payments_clone.clone(),
247 )
248 .await;
249 }
250 });
251
252 info!("Successfully started LDK Gateway");
253 Ok(GatewayLdkClient {
254 node,
255 task_group,
256 htlc_stream_receiver_or: Some(htlc_stream_receiver),
257 outbound_lightning_payment_lock_pool: lockable::LockPool::new(),
258 outbound_offer_lock_pool: lockable::LockPool::new(),
259 pending_channels,
260 pending_payments,
261 })
262 }
263
264 async fn handle_next_event(
265 node: &ldk_node::Node,
266 htlc_stream_sender: &Sender<InterceptPaymentRequest>,
267 handle: &TaskHandle,
268 pending_channels: Arc<
269 RwLock<BTreeMap<UserChannelId, oneshot::Sender<anyhow::Result<OutPoint>>>>,
270 >,
271 pending_payments: Arc<RwLock<HashMap<PaymentId, oneshot::Sender<()>>>>,
272 ) {
273 let event = tokio::select! {
277 event = node.next_event_async() => {
278 event
279 }
280 () = handle.make_shutdown_rx() => {
281 return;
282 }
283 };
284
285 match event {
286 ldk_node::Event::PaymentClaimable {
287 payment_id: _,
288 payment_hash,
289 claimable_amount_msat,
290 claim_deadline,
291 custom_records: _,
292 } => {
293 if let Err(err) = htlc_stream_sender
294 .send(InterceptPaymentRequest {
295 payment_hash: Hash::from_slice(&payment_hash.0)
296 .expect("Failed to create Hash"),
297 amount_msat: claimable_amount_msat,
300 incoming_amount_msat: claimable_amount_msat,
301 expiry: claim_deadline.unwrap_or_default(),
302 short_channel_id: None,
303 incoming_chan_id: NO_INCOMING_CIRCUIT.0,
306 htlc_id: NO_INCOMING_CIRCUIT.1,
307 })
308 .await
309 {
310 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed send InterceptHtlcRequest to stream");
311 }
312 }
313 ldk_node::Event::ChannelPending {
314 channel_id,
315 user_channel_id,
316 former_temporary_channel_id: _,
317 counterparty_node_id: _,
318 funding_txo,
319 } => {
320 info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is pending");
321 let mut channels = pending_channels.write().await;
322 if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
323 let _ = sender.send(Ok(funding_txo));
324 } else {
325 debug!(
326 ?user_channel_id,
327 "No channel pending channel open for user channel id"
328 );
329 }
330 }
331 ldk_node::Event::ChannelClosed {
332 channel_id,
333 user_channel_id,
334 counterparty_node_id: _,
335 reason,
336 } => {
337 info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is closed");
338 let mut channels = pending_channels.write().await;
339 if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
340 let reason = if let Some(reason) = reason {
341 reason.to_string()
342 } else {
343 "Channel has been closed".to_string()
344 };
345 let _ = sender.send(Err(anyhow::anyhow!(reason)));
346 } else {
347 debug!(
348 ?user_channel_id,
349 "No channel pending channel open for user channel id"
350 );
351 }
352 }
353 ldk_node::Event::PaymentSuccessful {
354 payment_id: Some(payment_id),
355 ..
356 }
357 | ldk_node::Event::PaymentFailed {
358 payment_id: Some(payment_id),
359 ..
360 } => {
361 Self::wake_pending_payment(&pending_payments, payment_id).await;
362 }
363 _ => {}
364 }
365
366 if let Err(err) = node.event_handled() {
371 warn!(err = %err.fmt_compact(), "LDK could not mark event handled");
372 }
373 }
374
375 async fn wake_pending_payment(
379 pending_payments: &Arc<RwLock<HashMap<PaymentId, oneshot::Sender<()>>>>,
380 payment_id: PaymentId,
381 ) -> PendingPaymentWakeup {
382 let Some(sender) = pending_payments.write().await.remove(&payment_id) else {
383 return PendingPaymentWakeup::NoWaiter;
384 };
385
386 if sender.send(()).is_ok() {
387 PendingPaymentWakeup::Woken
388 } else {
389 PendingPaymentWakeup::ReceiverDropped
390 }
391 }
392
393 fn outbound_payment(&self, payment_id: PaymentId) -> Option<PaymentDetails> {
403 self.node
404 .payment(&payment_id)
405 .filter(|details| details.direction == PaymentDirection::Outbound)
406 }
407
408 fn ldk_payment_result(
413 &self,
414 payment_id: PaymentId,
415 ) -> Option<Result<PayInvoiceResponse, LightningRpcError>> {
416 let payment_details = self.outbound_payment(payment_id)?;
417 match payment_details.status {
418 PaymentStatus::Pending => None,
419 PaymentStatus::Succeeded => {
420 if let PaymentKind::Bolt11 {
421 preimage: Some(preimage),
422 ..
423 } = payment_details.kind
424 {
425 Some(Ok(PayInvoiceResponse {
426 preimage: Preimage(preimage.0),
427 }))
428 } else {
429 Some(Err(LightningRpcError::FailedPayment {
430 failure_reason: "LDK payment succeeded without preimage".to_string(),
431 }))
432 }
433 }
434 PaymentStatus::Failed => Some(Err(LightningRpcError::FailedPayment {
435 failure_reason: "LDK payment failed".to_string(),
436 })),
437 }
438 }
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443enum InboundRegistrationRefusal {
444 OutboundInFlight,
447 OutboundRecorded,
449}
450
451impl std::fmt::Display for InboundRegistrationRefusal {
452 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453 match self {
454 Self::OutboundInFlight => {
455 write!(f, "an outbound payment for this hash is in flight")
456 }
457 Self::OutboundRecorded => {
458 write!(f, "the node holds an outbound payment for this hash")
459 }
460 }
461 }
462}
463
464fn check_inbound_registration(
481 outbound_lock_acquired: bool,
482 existing_direction: Option<PaymentDirection>,
483) -> Result<(), InboundRegistrationRefusal> {
484 if !outbound_lock_acquired {
485 return Err(InboundRegistrationRefusal::OutboundInFlight);
486 }
487
488 match existing_direction {
489 Some(PaymentDirection::Outbound) => Err(InboundRegistrationRefusal::OutboundRecorded),
490 Some(PaymentDirection::Inbound) | None => Ok(()),
491 }
492}
493
494fn htlc_completion_error(err: &ldk_node::NodeError, payment_hash: &str) -> LightningRpcError {
506 match err {
507 ldk_node::NodeError::InvalidPaymentHash
508 | ldk_node::NodeError::InvalidPaymentPreimage
509 | ldk_node::NodeError::InvalidAmount => LightningRpcError::HtlcCompletionRejected {
510 failure_reason: format!(
511 "LDK rejected completion of payment with hash {payment_hash}: {err}"
512 ),
513 },
514 _ => LightningRpcError::FailedToCompleteHtlc {
515 failure_reason: format!(
516 "Failed to complete LDK payment with hash {payment_hash}: {err}"
517 ),
518 },
519 }
520}
521
522impl Drop for GatewayLdkClient {
523 fn drop(&mut self) {
524 self.task_group.shutdown();
525
526 info!(target: LOG_LIGHTNING, "Stopping LDK Node...");
527 match self.node.stop() {
528 Err(err) => {
529 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to stop LDK Node");
530 }
531 _ => {
532 info!(target: LOG_LIGHTNING, "LDK Node stopped.");
533 }
534 }
535 }
536}
537
538#[async_trait]
539impl ILnRpcClient for GatewayLdkClient {
540 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
541 let node_status = self.node.status();
542 let ldk_block_height = node_status.current_best_block.height;
543 let onchain_sync = node_status.latest_onchain_wallet_sync_timestamp;
544 let lightning_sync = node_status.latest_lightning_wallet_sync_timestamp;
545 let is_running = node_status.is_running;
546 debug!(target: LOG_LIGHTNING, ?onchain_sync, ?lightning_sync, ?is_running, "LDK Sync Status");
547
548 Ok(GetNodeInfoResponse {
549 pub_key: self.node.node_id(),
550 alias: match self.node.node_alias() {
551 Some(alias) => alias.to_string(),
552 None => format!("LDK Fedimint Gateway Node {}", self.node.node_id()),
553 },
554 network: self.node.config().network.to_string(),
555 block_height: ldk_block_height,
556 synced_to_chain: lightning_sync.is_some(),
559 })
560 }
561
562 async fn routehints(
563 &self,
564 _num_route_hints: usize,
565 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
566 Ok(GetRouteHintsResponse {
572 route_hints: vec![],
573 })
574 }
575
576 async fn pay(
577 &self,
578 invoice: Bolt11Invoice,
579 max_delay: u64,
580 max_fee: Amount,
581 ) -> Result<PayInvoiceResponse, LightningRpcError> {
582 let payment_id = PaymentId(*invoice.payment_hash().as_byte_array());
583
584 let _payment_lock_guard = self
590 .outbound_lightning_payment_lock_pool
591 .async_lock(payment_id)
592 .await;
593
594 let (payment_sender, payment_receiver) = oneshot::channel();
598 self.pending_payments
599 .write()
600 .await
601 .insert(payment_id, payment_sender);
602
603 if self.outbound_payment(payment_id).is_none() {
614 let sent_payment_id = match self.node.bolt11_payment().send(
615 &invoice,
616 Some(SendingParameters {
617 max_total_routing_fee_msat: Some(Some(max_fee.msats)),
618 max_total_cltv_expiry_delta: Some(max_delay as u32),
619 max_path_count: None,
620 max_channel_saturation_power_of_half: None,
621 }),
622 ) {
623 Ok(sent_payment_id) => sent_payment_id,
624 Err(err) => {
625 self.pending_payments.write().await.remove(&payment_id);
626 return Err(LightningRpcError::FailedPayment {
629 failure_reason: format!("LDK payment failed to initialize: {err:?}"),
630 });
631 }
632 };
633 assert_eq!(sent_payment_id, payment_id);
634 }
635
636 if let Some(result) = self.ldk_payment_result(payment_id) {
640 self.pending_payments.write().await.remove(&payment_id);
641 return result;
642 }
643
644 let _ = payment_receiver.await;
649
650 self.pending_payments.write().await.remove(&payment_id);
651 self.ldk_payment_result(payment_id).unwrap_or_else(|| {
652 Err(LightningRpcError::FailedPayment {
653 failure_reason: "LDK payment event fired without terminal payment status"
654 .to_string(),
655 })
656 })
657 }
658
659 async fn outbound_payment_exists(
660 &self,
661 payment_hash: sha256::Hash,
662 ) -> Result<bool, LightningRpcError> {
663 Ok(self
664 .outbound_payment(PaymentId(payment_hash.to_byte_array()))
665 .is_some())
666 }
667
668 async fn route_htlcs<'a>(
669 mut self: Box<Self>,
670 _task_group: &TaskGroup,
671 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
672 let route_htlc_stream = match self.htlc_stream_receiver_or.take() {
673 Some(stream) => Ok(Box::pin(ReceiverStream::new(stream))),
674 None => Err(LightningRpcError::FailedToRouteHtlcs {
675 failure_reason:
676 "Stream does not exist. Likely was already taken by calling `route_htlcs()`."
677 .to_string(),
678 }),
679 }?;
680
681 Ok((route_htlc_stream, Arc::new(*self)))
682 }
683
684 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
685 let InterceptPaymentResponse {
686 action,
687 payment_hash,
688 incoming_chan_id: _,
689 htlc_id: _,
690 } = htlc;
691
692 let ph = PaymentHash(*payment_hash.clone().as_byte_array());
693
694 let claimable_amount_msat = 999_999_999_999_999;
700
701 let ph_hex_str = hex::encode(payment_hash);
702
703 if let PaymentAction::Settle(preimage) = action {
704 self.node
705 .bolt11_payment()
706 .claim_for_hash(ph, claimable_amount_msat, PaymentPreimage(preimage.0))
707 .map_err(|err| htlc_completion_error(&err, &ph_hex_str))?;
708 } else {
709 warn!(target: LOG_LIGHTNING, payment_hash = %ph_hex_str, "Unwinding payment because the action was not `Settle`");
710 self.node
711 .bolt11_payment()
712 .fail_for_hash(ph)
713 .map_err(|err| htlc_completion_error(&err, &ph_hex_str))?;
714 }
715
716 return Ok(());
717 }
718
719 async fn create_invoice(
720 &self,
721 create_invoice_request: CreateInvoiceRequest,
722 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
723 let payment_hash_or = if let Some(payment_hash) = create_invoice_request.payment_hash {
724 let ph = PaymentHash(*payment_hash.as_byte_array());
725 Some(ph)
726 } else {
727 None
728 };
729
730 let description = match create_invoice_request.description {
731 Some(InvoiceDescription::Direct(desc)) => {
732 Bolt11InvoiceDescription::Direct(Description::new(desc).map_err(|_| {
733 LightningRpcError::FailedToGetInvoice {
734 failure_reason: "Invalid description".to_string(),
735 }
736 })?)
737 }
738 Some(InvoiceDescription::Hash(hash)) => {
739 Bolt11InvoiceDescription::Hash(lightning_invoice::Sha256(hash))
740 }
741 None => Bolt11InvoiceDescription::Direct(Description::empty()),
742 };
743
744 let invoice = match payment_hash_or {
745 Some(payment_hash) => {
746 let payment_id = PaymentId(payment_hash.0);
753 let outbound_lock_guard = self
754 .outbound_lightning_payment_lock_pool
755 .try_lock(payment_id);
756 let existing_direction = self
757 .node
758 .payment(&payment_id)
759 .map(|details| details.direction);
760 if let Err(refusal) =
761 check_inbound_registration(outbound_lock_guard.is_some(), existing_direction)
762 {
763 warn!(
764 target: LOG_LIGHTNING,
765 payment_hash = %hex::encode(payment_hash.0),
766 %refusal,
767 "Refusing to register an invoice for a payment hash we pay outbound"
768 );
769 return Err(LightningRpcError::FailedToGetInvoice {
770 failure_reason: format!(
771 "Payment hash cannot be registered for an invoice: {refusal}"
772 ),
773 });
774 }
775
776 self.node.bolt11_payment().receive_for_hash(
777 create_invoice_request.amount_msat,
778 &description,
779 create_invoice_request.expiry_secs,
780 payment_hash,
781 )
782 }
783 None => self.node.bolt11_payment().receive(
784 create_invoice_request.amount_msat,
785 &description,
786 create_invoice_request.expiry_secs,
787 ),
788 }
789 .map_err(|e| LightningRpcError::FailedToGetInvoice {
790 failure_reason: e.to_string(),
791 })?;
792
793 Ok(CreateInvoiceResponse {
794 invoice: invoice.to_string(),
795 })
796 }
797
798 async fn get_ln_onchain_address(
799 &self,
800 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
801 self.node
802 .onchain_payment()
803 .new_address()
804 .map(|address| GetLnOnchainAddressResponse {
805 address: address.to_string(),
806 })
807 .map_err(|e| LightningRpcError::FailedToGetLnOnchainAddress {
808 failure_reason: e.to_string(),
809 })
810 }
811
812 async fn send_onchain(
813 &self,
814 SendOnchainRequest {
815 address,
816 amount,
817 fee_rate_sats_per_vbyte,
818 }: SendOnchainRequest,
819 ) -> Result<SendOnchainResponse, LightningRpcError> {
820 let onchain = self.node.onchain_payment();
821
822 let retain_reserves = false;
823 let txid = match amount {
824 BitcoinAmountOrAll::All => onchain.send_all_to_address(
825 &address.assume_checked(),
826 retain_reserves,
827 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
828 ),
829 BitcoinAmountOrAll::Amount(amount_sats) => onchain.send_to_address(
830 &address.assume_checked(),
831 amount_sats.to_sat(),
832 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
833 ),
834 }
835 .map_err(|e| LightningRpcError::FailedToWithdrawOnchain {
836 failure_reason: e.to_string(),
837 })?;
838
839 Ok(SendOnchainResponse {
840 txid: txid.to_string(),
841 })
842 }
843
844 async fn open_channel(
845 &self,
846 OpenChannelRequest {
847 pubkey,
848 host,
849 channel_size_sats,
850 push_amount_sats,
851 fee_rate_sats_per_vbyte,
852 base_fee_msat,
853 parts_per_million,
854 }: OpenChannelRequest,
855 ) -> Result<OpenChannelResponse, LightningRpcError> {
856 let push_amount_msats_or = if push_amount_sats == 0 {
857 None
858 } else {
859 Some(push_amount_sats * 1000)
860 };
861
862 if fee_rate_sats_per_vbyte.is_some() {
863 warn!(
866 target: LOG_LIGHTNING,
867 "Ignoring fee_rate_sats_per_vbyte on LDK channel open; LDK uses its built-in fee estimator"
868 );
869 }
870
871 let channel_config = match (base_fee_msat, parts_per_million) {
872 (None, None) => None,
873 (base, ppm) => {
874 let mut config = ChannelConfig::default();
875 if let Some(base) = base {
876 config.forwarding_fee_base_msat = u32::try_from(base).map_err(|_| {
877 LightningRpcError::FailedToOpenChannel {
878 failure_reason: format!(
879 "base_fee_msat {base} does not fit in u32 (LDK limit)"
880 ),
881 }
882 })?;
883 }
884 if let Some(ppm) = ppm {
885 config.forwarding_fee_proportional_millionths =
886 u32::try_from(ppm).map_err(|_| LightningRpcError::FailedToOpenChannel {
887 failure_reason: format!(
888 "parts_per_million {ppm} does not fit in u32 (LDK limit)"
889 ),
890 })?;
891 }
892 Some(config)
893 }
894 };
895
896 let (tx, rx) = oneshot::channel::<anyhow::Result<OutPoint>>();
897
898 {
899 let mut channels = self.pending_channels.write().await;
900 let user_channel_id = self
901 .node
902 .open_announced_channel(
903 pubkey,
904 SocketAddress::from_str(&host).map_err(|e| {
905 LightningRpcError::FailedToConnectToPeer {
906 failure_reason: e.to_string(),
907 }
908 })?,
909 channel_size_sats,
910 push_amount_msats_or,
911 channel_config,
912 )
913 .map_err(|e| LightningRpcError::FailedToOpenChannel {
914 failure_reason: e.to_string(),
915 })?;
916
917 channels.insert(UserChannelId(user_channel_id), tx);
918 }
919
920 match rx
921 .await
922 .map_err(|err| LightningRpcError::FailedToOpenChannel {
923 failure_reason: err.to_string(),
924 })? {
925 Ok(outpoint) => {
926 let funding_txid = outpoint.txid;
927
928 Ok(OpenChannelResponse {
929 funding_txid: funding_txid.to_string(),
930 })
931 }
932 Err(err) => Err(LightningRpcError::FailedToOpenChannel {
933 failure_reason: err.to_string(),
934 }),
935 }
936 }
937
938 async fn connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError> {
939 let NodeAddress { pubkey, address } = payload.node_address;
940 self.node.connect(pubkey, address, true).map_err(|e| {
946 LightningRpcError::FailedToConnectToPeer {
947 failure_reason: e.to_string(),
948 }
949 })
950 }
951
952 async fn close_channels_with_peer(
953 &self,
954 CloseChannelsWithPeerRequest {
955 pubkey,
956 force,
957 sats_per_vbyte: _,
958 }: CloseChannelsWithPeerRequest,
959 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
960 let mut num_channels_closed = 0;
961
962 info!(%pubkey, "Closing all channels with peer");
963 for channel_with_peer in self
964 .node
965 .list_channels()
966 .iter()
967 .filter(|channel| channel.counterparty_node_id == pubkey)
968 {
969 if force {
970 match self.node.force_close_channel(
971 &channel_with_peer.user_channel_id,
972 pubkey,
973 Some("User initiated force close".to_string()),
974 ) {
975 Ok(()) => num_channels_closed += 1,
976 Err(err) => {
977 error!(%pubkey, err = %err.fmt_compact(), "Could not force close channel");
978 }
979 }
980 } else {
981 match self
982 .node
983 .close_channel(&channel_with_peer.user_channel_id, pubkey)
984 {
985 Ok(()) => {
986 num_channels_closed += 1;
987 }
988 Err(err) => {
989 error!(%pubkey, err = %err.fmt_compact(), "Could not close channel");
990 }
991 }
992 }
993 }
994
995 Ok(CloseChannelsWithPeerResponse {
996 num_channels_closed,
997 })
998 }
999
1000 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
1001 let mut channels = Vec::new();
1002 let network_graph = self.node.network_graph();
1003
1004 let peer_addresses: std::collections::HashMap<_, _> = self
1006 .node
1007 .list_peers()
1008 .into_iter()
1009 .map(|peer| (peer.node_id, peer.address.to_string()))
1010 .collect();
1011
1012 for channel_details in self.node.list_channels().iter() {
1013 let node_id = NodeId::from_pubkey(&channel_details.counterparty_node_id);
1014 let node_info = network_graph.node(&node_id);
1015
1016 let remote_node_alias = node_info.as_ref().and_then(|info| {
1018 info.announcement_info.as_ref().and_then(|announcement| {
1019 let alias = announcement.alias().to_string();
1020 if alias.is_empty() { None } else { Some(alias) }
1021 })
1022 });
1023
1024 let remote_address = peer_addresses
1025 .get(&channel_details.counterparty_node_id)
1026 .cloned();
1027
1028 channels.push(ChannelInfo {
1029 remote_pubkey: channel_details.counterparty_node_id,
1030 channel_size_sats: channel_details.channel_value_sats,
1031 outbound_liquidity_sats: channel_details.outbound_capacity_msat / 1000,
1032 inbound_liquidity_sats: channel_details.inbound_capacity_msat / 1000,
1033 is_active: channel_details.is_usable,
1034 funding_outpoint: channel_details.funding_txo,
1035 remote_node_alias,
1036 remote_address,
1037 base_fee_msat: Some(u64::from(channel_details.config.forwarding_fee_base_msat)),
1038 parts_per_million: Some(u64::from(
1039 channel_details
1040 .config
1041 .forwarding_fee_proportional_millionths,
1042 )),
1043 });
1044 }
1045
1046 Ok(ListChannelsResponse { channels })
1047 }
1048
1049 async fn set_channel_fees(
1050 &self,
1051 payload: SetChannelFeesRequest,
1052 ) -> Result<(), LightningRpcError> {
1053 let channel = self
1057 .node
1058 .list_channels()
1059 .into_iter()
1060 .find(|c| c.funding_txo == Some(payload.funding_outpoint))
1061 .ok_or_else(|| LightningRpcError::FailedToSetChannelFees {
1062 failure_reason: format!(
1063 "No channel found with funding outpoint {}",
1064 payload.funding_outpoint,
1065 ),
1066 })?;
1067
1068 let forwarding_fee_base_msat = u32::try_from(payload.base_fee_msat).map_err(|_| {
1069 LightningRpcError::FailedToSetChannelFees {
1070 failure_reason: format!(
1071 "base_fee_msat {} does not fit in u32 (LDK limit)",
1072 payload.base_fee_msat,
1073 ),
1074 }
1075 })?;
1076 let forwarding_fee_proportional_millionths = u32::try_from(payload.parts_per_million)
1077 .map_err(|_| LightningRpcError::FailedToSetChannelFees {
1078 failure_reason: format!(
1079 "parts_per_million {} does not fit in u32 (LDK limit)",
1080 payload.parts_per_million,
1081 ),
1082 })?;
1083
1084 let new_config = ChannelConfig {
1087 forwarding_fee_base_msat,
1088 forwarding_fee_proportional_millionths,
1089 ..channel.config
1090 };
1091
1092 self.node
1093 .update_channel_config(
1094 &channel.user_channel_id,
1095 channel.counterparty_node_id,
1096 new_config,
1097 )
1098 .map_err(|e| LightningRpcError::FailedToSetChannelFees {
1099 failure_reason: e.to_string(),
1100 })?;
1101
1102 Ok(())
1103 }
1104
1105 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
1106 let balances = self.node.list_balances();
1107 let channel_lists = self
1108 .node
1109 .list_channels()
1110 .into_iter()
1111 .filter(|chan| chan.is_usable)
1112 .collect::<Vec<_>>();
1113 let total_inbound_liquidity_balance_msat: u64 = channel_lists
1115 .iter()
1116 .map(|channel| channel.inbound_capacity_msat)
1117 .sum();
1118
1119 Ok(GetBalancesResponse {
1120 onchain_balance_sats: balances.total_onchain_balance_sats,
1121 lightning_balance_msats: balances.total_lightning_balance_sats * 1000,
1122 inbound_lightning_liquidity_msats: total_inbound_liquidity_balance_msat,
1123 })
1124 }
1125
1126 async fn get_invoice(
1127 &self,
1128 get_invoice_request: GetInvoiceRequest,
1129 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
1130 let invoices = self
1131 .node
1132 .list_payments_with_filter(|details| {
1133 details.direction == PaymentDirection::Inbound
1134 && details.id == PaymentId(get_invoice_request.payment_hash.to_byte_array())
1135 && !matches!(details.kind, PaymentKind::Onchain { .. })
1136 })
1137 .iter()
1138 .map(|details| {
1139 let (preimage, payment_hash, _) = get_preimage_and_payment_hash(&details.kind);
1140 let status = match details.status {
1141 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
1142 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
1143 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
1144 };
1145 GetInvoiceResponse {
1146 preimage: preimage.map(|p| p.to_string()),
1147 payment_hash,
1148 amount: Amount::from_msats(
1149 details
1150 .amount_msat
1151 .expect("amountless invoices are not supported"),
1152 ),
1153 created_at: UNIX_EPOCH + Duration::from_secs(details.latest_update_timestamp),
1154 status,
1155 }
1156 })
1157 .collect::<Vec<_>>();
1158
1159 Ok(invoices.first().cloned())
1160 }
1161
1162 async fn list_transactions(
1163 &self,
1164 start_secs: u64,
1165 end_secs: u64,
1166 ) -> Result<ListTransactionsResponse, LightningRpcError> {
1167 let transactions = self
1168 .node
1169 .list_payments_with_filter(|details| {
1170 !matches!(details.kind, PaymentKind::Onchain { .. })
1171 && details.latest_update_timestamp >= start_secs
1172 && details.latest_update_timestamp < end_secs
1173 })
1174 .iter()
1175 .map(|details| {
1176 let (preimage, payment_hash, payment_kind) =
1177 get_preimage_and_payment_hash(&details.kind);
1178 let direction = match details.direction {
1179 PaymentDirection::Outbound => {
1180 fedimint_gateway_common::PaymentDirection::Outbound
1181 }
1182 PaymentDirection::Inbound => fedimint_gateway_common::PaymentDirection::Inbound,
1183 };
1184 let status = match details.status {
1185 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
1186 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
1187 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
1188 };
1189 fedimint_gateway_common::PaymentDetails {
1190 payment_hash,
1191 preimage: preimage.map(|p| p.to_string()),
1192 payment_kind,
1193 amount: Amount::from_msats(
1194 details
1195 .amount_msat
1196 .expect("amountless invoices are not supported"),
1197 ),
1198 direction,
1199 status,
1200 timestamp_secs: details.latest_update_timestamp,
1201 }
1202 })
1203 .collect::<Vec<_>>();
1204 Ok(ListTransactionsResponse { transactions })
1205 }
1206
1207 fn create_offer(
1208 &self,
1209 amount: Option<Amount>,
1210 description: Option<String>,
1211 expiry_secs: Option<u32>,
1212 quantity: Option<u64>,
1213 ) -> Result<String, LightningRpcError> {
1214 let description = description.unwrap_or_default();
1215 let offer = if let Some(amount) = amount {
1216 self.node
1217 .bolt12_payment()
1218 .receive(amount.msats, &description, expiry_secs, quantity)
1219 .map_err(|err| LightningRpcError::Bolt12Error {
1220 failure_reason: err.to_string(),
1221 })?
1222 } else {
1223 self.node
1224 .bolt12_payment()
1225 .receive_variable_amount(&description, expiry_secs)
1226 .map_err(|err| LightningRpcError::Bolt12Error {
1227 failure_reason: err.to_string(),
1228 })?
1229 };
1230
1231 Ok(offer.to_string())
1232 }
1233
1234 async fn pay_offer(
1235 &self,
1236 offer: String,
1237 quantity: Option<u64>,
1238 amount: Option<Amount>,
1239 payer_note: Option<String>,
1240 ) -> Result<Preimage, LightningRpcError> {
1241 let offer = Offer::from_str(&offer).map_err(|_| LightningRpcError::Bolt12Error {
1242 failure_reason: "Failed to parse Bolt12 Offer".to_string(),
1243 })?;
1244
1245 let _offer_lock_guard = self
1246 .outbound_offer_lock_pool
1247 .blocking_lock(LdkOfferId(offer.id()));
1248
1249 let payment_id = if let Some(amount) = amount {
1250 self.node
1251 .bolt12_payment()
1252 .send_using_amount(&offer, amount.msats, quantity, payer_note)
1253 .map_err(|err| LightningRpcError::Bolt12Error {
1254 failure_reason: err.to_string(),
1255 })?
1256 } else {
1257 self.node
1258 .bolt12_payment()
1259 .send(&offer, quantity, payer_note)
1260 .map_err(|err| LightningRpcError::Bolt12Error {
1261 failure_reason: err.to_string(),
1262 })?
1263 };
1264
1265 loop {
1266 if let Some(payment_details) = self.node.payment(&payment_id) {
1267 match payment_details.status {
1268 PaymentStatus::Pending => {}
1269 PaymentStatus::Succeeded => match payment_details.kind {
1270 PaymentKind::Bolt12Offer {
1271 preimage: Some(preimage),
1272 ..
1273 } => {
1274 info!(target: LOG_LIGHTNING, offer = %offer, payment_id = %payment_id, preimage = %preimage, "Successfully paid offer");
1275 return Ok(Preimage(preimage.0));
1276 }
1277 _ => {
1278 return Err(LightningRpcError::FailedPayment {
1279 failure_reason: "Unexpected payment kind".to_string(),
1280 });
1281 }
1282 },
1283 PaymentStatus::Failed => {
1284 return Err(LightningRpcError::FailedPayment {
1285 failure_reason: "Bolt12 payment failed".to_string(),
1286 });
1287 }
1288 }
1289 }
1290 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
1291 }
1292 }
1293
1294 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
1295 block_in_place(|| {
1296 let _ = self.node.sync_wallets();
1297 });
1298 Ok(())
1299 }
1300}
1301
1302fn get_preimage_and_payment_hash(
1305 kind: &PaymentKind,
1306) -> (
1307 Option<Preimage>,
1308 Option<sha256::Hash>,
1309 fedimint_gateway_common::PaymentKind,
1310) {
1311 match kind {
1312 PaymentKind::Bolt11 {
1313 hash,
1314 preimage,
1315 secret: _,
1316 } => (
1317 preimage.map(|p| Preimage(p.0)),
1318 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
1319 fedimint_gateway_common::PaymentKind::Bolt11,
1320 ),
1321 PaymentKind::Bolt11Jit {
1322 hash,
1323 preimage,
1324 secret: _,
1325 lsp_fee_limits: _,
1326 ..
1327 } => (
1328 preimage.map(|p| Preimage(p.0)),
1329 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
1330 fedimint_gateway_common::PaymentKind::Bolt11,
1331 ),
1332 PaymentKind::Bolt12Offer {
1333 hash,
1334 preimage,
1335 secret: _,
1336 offer_id: _,
1337 payer_note: _,
1338 quantity: _,
1339 } => (
1340 preimage.map(|p| Preimage(p.0)),
1341 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
1342 fedimint_gateway_common::PaymentKind::Bolt12Offer,
1343 ),
1344 PaymentKind::Bolt12Refund {
1345 hash,
1346 preimage,
1347 secret: _,
1348 payer_note: _,
1349 quantity: _,
1350 } => (
1351 preimage.map(|p| Preimage(p.0)),
1352 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
1353 fedimint_gateway_common::PaymentKind::Bolt12Refund,
1354 ),
1355 PaymentKind::Spontaneous { hash, preimage } => (
1356 preimage.map(|p| Preimage(p.0)),
1357 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
1358 fedimint_gateway_common::PaymentKind::Bolt11,
1359 ),
1360 PaymentKind::Onchain { .. } => (None, None, fedimint_gateway_common::PaymentKind::Onchain),
1361 }
1362}
1363
1364fn get_esplora_url(server_url: SafeUrl) -> anyhow::Result<String> {
1372 let host = server_url
1374 .host_str()
1375 .ok_or(anyhow::anyhow!("Missing esplora host"))?;
1376 let server_url = if let Some(port) = server_url.port() {
1377 format!("{}://{}:{}", server_url.scheme(), host, port)
1378 } else {
1379 server_url.to_string()
1380 };
1381 Ok(server_url)
1382}
1383
1384#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1386enum PendingPaymentWakeup {
1387 NoWaiter,
1389 Woken,
1391 ReceiverDropped,
1393}
1394
1395#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1396struct LdkOfferId(OfferId);
1397
1398impl std::hash::Hash for LdkOfferId {
1399 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1400 state.write(&self.0.0);
1401 }
1402}
1403
1404#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1405pub struct UserChannelId(pub ldk_node::UserChannelId);
1406
1407impl PartialOrd for UserChannelId {
1408 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1409 Some(self.cmp(other))
1410 }
1411}
1412
1413impl Ord for UserChannelId {
1414 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1415 self.0.0.cmp(&other.0.0)
1416 }
1417}
1418
1419#[cfg(test)]
1420mod tests;