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::{PaymentDirection, PaymentKind, PaymentStatus, SendingParameters};
26use lightning::ln::channelmanager::PaymentId;
27use lightning::offers::offer::{Offer, OfferId};
28use lightning::types::payment::{PaymentHash, PaymentPreimage};
29use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
30use tokio::sync::mpsc::Sender;
31use tokio::sync::{RwLock, oneshot};
32use tokio_stream::wrappers::ReceiverStream;
33use tracing::{debug, error, info, trace, warn};
34
35use super::{ChannelInfo, ILnRpcClient, LightningRpcError, ListChannelsResponse, RouteHtlcStream};
36use crate::{
37 CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, CreateInvoiceRequest,
38 CreateInvoiceResponse, GetBalancesResponse, GetLnOnchainAddressResponse, GetNodeInfoResponse,
39 GetRouteHintsResponse, InterceptPaymentRequest, InterceptPaymentResponse, InvoiceDescription,
40 NO_INCOMING_CIRCUIT, OpenChannelRequest, OpenChannelResponse, PayInvoiceResponse,
41 PaymentAction, SendOnchainRequest, SendOnchainResponse,
42};
43
44struct LdkTracingLogger {
52 in_test_env: bool,
56}
57
58impl LdkTracingLogger {
59 fn downgraded_level(&self, record: &LogRecord<'_>) -> LogLevel {
69 if self.in_test_env
70 && record.level == LogLevel::Error
71 && record.module_path == "ldk_node::chain"
72 && format!("{}", record.args).contains("Failed to retrieve fee rate estimates")
73 {
74 LogLevel::Debug
75 } else {
76 record.level
77 }
78 }
79}
80
81impl LogWriter for LdkTracingLogger {
82 fn log(&self, record: LogRecord<'_>) {
83 match self.downgraded_level(&record) {
85 LogLevel::Gossip | LogLevel::Trace => trace!(
86 target: LOG_LIGHTNING_LDK,
87 ldk_module = record.module_path, line = record.line, "{}", record.args,
88 ),
89 LogLevel::Debug => debug!(
90 target: LOG_LIGHTNING_LDK,
91 ldk_module = record.module_path, line = record.line, "{}", record.args,
92 ),
93 LogLevel::Info => info!(
94 target: LOG_LIGHTNING_LDK,
95 ldk_module = record.module_path, line = record.line, "{}", record.args,
96 ),
97 LogLevel::Warn => warn!(
98 target: LOG_LIGHTNING_LDK,
99 ldk_module = record.module_path, line = record.line, "{}", record.args,
100 ),
101 LogLevel::Error => error!(
102 target: LOG_LIGHTNING_LDK,
103 ldk_module = record.module_path, line = record.line, "{}", record.args,
104 ),
105 }
106 }
107}
108
109pub struct GatewayLdkClient {
110 node: Arc<ldk_node::Node>,
112
113 task_group: TaskGroup,
114
115 htlc_stream_receiver_or: Option<tokio::sync::mpsc::Receiver<InterceptPaymentRequest>>,
118
119 outbound_lightning_payment_lock_pool: lockable::LockPool<PaymentId>,
123
124 outbound_offer_lock_pool: lockable::LockPool<LdkOfferId>,
129
130 pending_channels:
135 Arc<RwLock<BTreeMap<UserChannelId, oneshot::Sender<anyhow::Result<OutPoint>>>>>,
136
137 pending_payments: Arc<RwLock<HashMap<PaymentId, oneshot::Sender<()>>>>,
143}
144
145impl std::fmt::Debug for GatewayLdkClient {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.debug_struct("GatewayLdkClient").finish_non_exhaustive()
148 }
149}
150
151impl GatewayLdkClient {
152 pub fn new(
157 data_dir: &Path,
158 chain_source: ChainSource,
159 network: Network,
160 lightning_port: u16,
161 alias: String,
162 mnemonic: Mnemonic,
163 runtime: Arc<tokio::runtime::Runtime>,
164 ) -> anyhow::Result<Self> {
165 let mut bytes = [0u8; 32];
166 let alias = if alias.is_empty() {
167 "LDK Gateway".to_string()
168 } else {
169 alias
170 };
171 let alias_bytes = alias.as_bytes();
172 let truncated = &alias_bytes[..alias_bytes.len().min(32)];
173 bytes[..truncated.len()].copy_from_slice(truncated);
174 let node_alias = Some(NodeAlias(bytes));
175
176 let mut node_builder = ldk_node::Builder::from_config(ldk_node::config::Config {
177 network,
178 listening_addresses: Some(vec![SocketAddress::TcpIpV4 {
179 addr: [0, 0, 0, 0],
180 port: lightning_port,
181 }]),
182 node_alias,
183 ..Default::default()
184 });
185
186 node_builder.set_custom_logger(Arc::new(LdkTracingLogger {
190 in_test_env: is_running_in_test_env(),
191 }));
192
193 node_builder.set_entropy_bip39_mnemonic(mnemonic, None);
194
195 match chain_source.clone() {
196 ChainSource::Bitcoind {
197 username,
198 password,
199 server_url,
200 } => {
201 node_builder.set_chain_source_bitcoind_rpc(
202 server_url
203 .host_str()
204 .expect("Could not retrieve host from bitcoind RPC url")
205 .to_string(),
206 server_url
207 .port()
208 .expect("Could not retrieve port from bitcoind RPC url"),
209 username,
210 password,
211 );
212 }
213 ChainSource::Esplora { server_url } => {
214 node_builder.set_chain_source_esplora(get_esplora_url(server_url)?, None);
215 }
216 };
217 let Some(data_dir_str) = data_dir.to_str() else {
218 return Err(anyhow::anyhow!("Invalid data dir path"));
219 };
220 node_builder.set_storage_dir_path(data_dir_str.to_string());
221
222 info!(chain_source = %chain_source, data_dir = %data_dir_str, alias = %alias, "Starting LDK Node...");
223 let node = Arc::new(node_builder.build()?);
224 node.start_with_runtime(runtime).map_err(|err| {
225 crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to start LDK Node");
226 LightningRpcError::FailedToConnect
227 })?;
228
229 let (htlc_stream_sender, htlc_stream_receiver) = tokio::sync::mpsc::channel(1024);
230 let task_group = TaskGroup::new();
231
232 let node_clone = node.clone();
233 let pending_channels = Arc::new(RwLock::new(BTreeMap::new()));
234 let pending_channels_clone = pending_channels.clone();
235 let pending_payments = Arc::new(RwLock::new(HashMap::new()));
236 let pending_payments_clone = pending_payments.clone();
237 task_group.spawn("ldk lightning node event handler", |handle| async move {
238 loop {
239 Self::handle_next_event(
240 &node_clone,
241 &htlc_stream_sender,
242 &handle,
243 pending_channels_clone.clone(),
244 pending_payments_clone.clone(),
245 )
246 .await;
247 }
248 });
249
250 info!("Successfully started LDK Gateway");
251 Ok(GatewayLdkClient {
252 node,
253 task_group,
254 htlc_stream_receiver_or: Some(htlc_stream_receiver),
255 outbound_lightning_payment_lock_pool: lockable::LockPool::new(),
256 outbound_offer_lock_pool: lockable::LockPool::new(),
257 pending_channels,
258 pending_payments,
259 })
260 }
261
262 async fn handle_next_event(
263 node: &ldk_node::Node,
264 htlc_stream_sender: &Sender<InterceptPaymentRequest>,
265 handle: &TaskHandle,
266 pending_channels: Arc<
267 RwLock<BTreeMap<UserChannelId, oneshot::Sender<anyhow::Result<OutPoint>>>>,
268 >,
269 pending_payments: Arc<RwLock<HashMap<PaymentId, oneshot::Sender<()>>>>,
270 ) {
271 let event = tokio::select! {
275 event = node.next_event_async() => {
276 event
277 }
278 () = handle.make_shutdown_rx() => {
279 return;
280 }
281 };
282
283 match event {
284 ldk_node::Event::PaymentClaimable {
285 payment_id: _,
286 payment_hash,
287 claimable_amount_msat,
288 claim_deadline,
289 custom_records: _,
290 } => {
291 if let Err(err) = htlc_stream_sender
292 .send(InterceptPaymentRequest {
293 payment_hash: Hash::from_slice(&payment_hash.0)
294 .expect("Failed to create Hash"),
295 amount_msat: claimable_amount_msat,
296 expiry: claim_deadline.unwrap_or_default(),
297 short_channel_id: None,
298 incoming_chan_id: NO_INCOMING_CIRCUIT.0,
301 htlc_id: NO_INCOMING_CIRCUIT.1,
302 })
303 .await
304 {
305 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed send InterceptHtlcRequest to stream");
306 }
307 }
308 ldk_node::Event::ChannelPending {
309 channel_id,
310 user_channel_id,
311 former_temporary_channel_id: _,
312 counterparty_node_id: _,
313 funding_txo,
314 } => {
315 info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is pending");
316 let mut channels = pending_channels.write().await;
317 if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
318 let _ = sender.send(Ok(funding_txo));
319 } else {
320 debug!(
321 ?user_channel_id,
322 "No channel pending channel open for user channel id"
323 );
324 }
325 }
326 ldk_node::Event::ChannelClosed {
327 channel_id,
328 user_channel_id,
329 counterparty_node_id: _,
330 reason,
331 } => {
332 info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is closed");
333 let mut channels = pending_channels.write().await;
334 if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
335 let reason = if let Some(reason) = reason {
336 reason.to_string()
337 } else {
338 "Channel has been closed".to_string()
339 };
340 let _ = sender.send(Err(anyhow::anyhow!(reason)));
341 } else {
342 debug!(
343 ?user_channel_id,
344 "No channel pending channel open for user channel id"
345 );
346 }
347 }
348 ldk_node::Event::PaymentSuccessful {
349 payment_id: Some(payment_id),
350 ..
351 }
352 | ldk_node::Event::PaymentFailed {
353 payment_id: Some(payment_id),
354 ..
355 } => {
356 Self::wake_pending_payment(&pending_payments, payment_id).await;
357 }
358 _ => {}
359 }
360
361 if let Err(err) = node.event_handled() {
366 warn!(err = %err.fmt_compact(), "LDK could not mark event handled");
367 }
368 }
369
370 async fn wake_pending_payment(
374 pending_payments: &Arc<RwLock<HashMap<PaymentId, oneshot::Sender<()>>>>,
375 payment_id: PaymentId,
376 ) -> PendingPaymentWakeup {
377 let Some(sender) = pending_payments.write().await.remove(&payment_id) else {
378 return PendingPaymentWakeup::NoWaiter;
379 };
380
381 if sender.send(()).is_ok() {
382 PendingPaymentWakeup::Woken
383 } else {
384 PendingPaymentWakeup::ReceiverDropped
385 }
386 }
387
388 fn ldk_payment_result(
393 &self,
394 payment_id: PaymentId,
395 ) -> Option<Result<PayInvoiceResponse, LightningRpcError>> {
396 let payment_details = self.node.payment(&payment_id)?;
397 match payment_details.status {
398 PaymentStatus::Pending => None,
399 PaymentStatus::Succeeded => {
400 if let PaymentKind::Bolt11 {
401 preimage: Some(preimage),
402 ..
403 } = payment_details.kind
404 {
405 Some(Ok(PayInvoiceResponse {
406 preimage: Preimage(preimage.0),
407 }))
408 } else {
409 Some(Err(LightningRpcError::FailedPayment {
410 failure_reason: "LDK payment succeeded without preimage".to_string(),
411 }))
412 }
413 }
414 PaymentStatus::Failed => Some(Err(LightningRpcError::FailedPayment {
415 failure_reason: "LDK payment failed".to_string(),
416 })),
417 }
418 }
419}
420
421impl Drop for GatewayLdkClient {
422 fn drop(&mut self) {
423 self.task_group.shutdown();
424
425 info!(target: LOG_LIGHTNING, "Stopping LDK Node...");
426 match self.node.stop() {
427 Err(err) => {
428 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to stop LDK Node");
429 }
430 _ => {
431 info!(target: LOG_LIGHTNING, "LDK Node stopped.");
432 }
433 }
434 }
435}
436
437#[async_trait]
438impl ILnRpcClient for GatewayLdkClient {
439 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
440 let node_status = self.node.status();
441 let ldk_block_height = node_status.current_best_block.height;
442 let onchain_sync = node_status.latest_onchain_wallet_sync_timestamp;
443 let lightning_sync = node_status.latest_lightning_wallet_sync_timestamp;
444 let is_running = node_status.is_running;
445 debug!(target: LOG_LIGHTNING, ?onchain_sync, ?lightning_sync, ?is_running, "LDK Sync Status");
446
447 Ok(GetNodeInfoResponse {
448 pub_key: self.node.node_id(),
449 alias: match self.node.node_alias() {
450 Some(alias) => alias.to_string(),
451 None => format!("LDK Fedimint Gateway Node {}", self.node.node_id()),
452 },
453 network: self.node.config().network.to_string(),
454 block_height: ldk_block_height,
455 synced_to_chain: lightning_sync.is_some(),
458 })
459 }
460
461 async fn routehints(
462 &self,
463 _num_route_hints: usize,
464 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
465 Ok(GetRouteHintsResponse {
471 route_hints: vec![],
472 })
473 }
474
475 async fn pay(
476 &self,
477 invoice: Bolt11Invoice,
478 max_delay: u64,
479 max_fee: Amount,
480 ) -> Result<PayInvoiceResponse, LightningRpcError> {
481 let payment_id = PaymentId(*invoice.payment_hash().as_byte_array());
482
483 let _payment_lock_guard = self
489 .outbound_lightning_payment_lock_pool
490 .async_lock(payment_id)
491 .await;
492
493 let (payment_sender, payment_receiver) = oneshot::channel();
497 self.pending_payments
498 .write()
499 .await
500 .insert(payment_id, payment_sender);
501
502 if self.node.payment(&payment_id).is_none() {
509 let sent_payment_id = match self.node.bolt11_payment().send(
510 &invoice,
511 Some(SendingParameters {
512 max_total_routing_fee_msat: Some(Some(max_fee.msats)),
513 max_total_cltv_expiry_delta: Some(max_delay as u32),
514 max_path_count: None,
515 max_channel_saturation_power_of_half: None,
516 }),
517 ) {
518 Ok(sent_payment_id) => sent_payment_id,
519 Err(err) => {
520 self.pending_payments.write().await.remove(&payment_id);
521 return Err(LightningRpcError::FailedPayment {
524 failure_reason: format!("LDK payment failed to initialize: {err:?}"),
525 });
526 }
527 };
528 assert_eq!(sent_payment_id, payment_id);
529 }
530
531 if let Some(result) = self.ldk_payment_result(payment_id) {
535 self.pending_payments.write().await.remove(&payment_id);
536 return result;
537 }
538
539 let _ = payment_receiver.await;
544
545 self.pending_payments.write().await.remove(&payment_id);
546 self.ldk_payment_result(payment_id).unwrap_or_else(|| {
547 Err(LightningRpcError::FailedPayment {
548 failure_reason: "LDK payment event fired without terminal payment status"
549 .to_string(),
550 })
551 })
552 }
553
554 async fn route_htlcs<'a>(
555 mut self: Box<Self>,
556 _task_group: &TaskGroup,
557 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
558 let route_htlc_stream = match self.htlc_stream_receiver_or.take() {
559 Some(stream) => Ok(Box::pin(ReceiverStream::new(stream))),
560 None => Err(LightningRpcError::FailedToRouteHtlcs {
561 failure_reason:
562 "Stream does not exist. Likely was already taken by calling `route_htlcs()`."
563 .to_string(),
564 }),
565 }?;
566
567 Ok((route_htlc_stream, Arc::new(*self)))
568 }
569
570 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
571 let InterceptPaymentResponse {
572 action,
573 payment_hash,
574 incoming_chan_id: _,
575 htlc_id: _,
576 } = htlc;
577
578 let ph = PaymentHash(*payment_hash.clone().as_byte_array());
579
580 let claimable_amount_msat = 999_999_999_999_999;
586
587 let ph_hex_str = hex::encode(payment_hash);
588
589 if let PaymentAction::Settle(preimage) = action {
590 self.node
591 .bolt11_payment()
592 .claim_for_hash(ph, claimable_amount_msat, PaymentPreimage(preimage.0))
593 .map_err(|_| LightningRpcError::FailedToCompleteHtlc {
594 failure_reason: format!("Failed to claim LDK payment with hash {ph_hex_str}"),
595 })?;
596 } else {
597 warn!(target: LOG_LIGHTNING, payment_hash = %ph_hex_str, "Unwinding payment because the action was not `Settle`");
598 self.node.bolt11_payment().fail_for_hash(ph).map_err(|_| {
599 LightningRpcError::FailedToCompleteHtlc {
600 failure_reason: format!("Failed to unwind LDK payment with hash {ph_hex_str}"),
601 }
602 })?;
603 }
604
605 return Ok(());
606 }
607
608 async fn create_invoice(
609 &self,
610 create_invoice_request: CreateInvoiceRequest,
611 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
612 let payment_hash_or = if let Some(payment_hash) = create_invoice_request.payment_hash {
613 let ph = PaymentHash(*payment_hash.as_byte_array());
614 Some(ph)
615 } else {
616 None
617 };
618
619 let description = match create_invoice_request.description {
620 Some(InvoiceDescription::Direct(desc)) => {
621 Bolt11InvoiceDescription::Direct(Description::new(desc).map_err(|_| {
622 LightningRpcError::FailedToGetInvoice {
623 failure_reason: "Invalid description".to_string(),
624 }
625 })?)
626 }
627 Some(InvoiceDescription::Hash(hash)) => {
628 Bolt11InvoiceDescription::Hash(lightning_invoice::Sha256(hash))
629 }
630 None => Bolt11InvoiceDescription::Direct(Description::empty()),
631 };
632
633 let invoice = match payment_hash_or {
634 Some(payment_hash) => self.node.bolt11_payment().receive_for_hash(
635 create_invoice_request.amount_msat,
636 &description,
637 create_invoice_request.expiry_secs,
638 payment_hash,
639 ),
640 None => self.node.bolt11_payment().receive(
641 create_invoice_request.amount_msat,
642 &description,
643 create_invoice_request.expiry_secs,
644 ),
645 }
646 .map_err(|e| LightningRpcError::FailedToGetInvoice {
647 failure_reason: e.to_string(),
648 })?;
649
650 Ok(CreateInvoiceResponse {
651 invoice: invoice.to_string(),
652 })
653 }
654
655 async fn get_ln_onchain_address(
656 &self,
657 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
658 self.node
659 .onchain_payment()
660 .new_address()
661 .map(|address| GetLnOnchainAddressResponse {
662 address: address.to_string(),
663 })
664 .map_err(|e| LightningRpcError::FailedToGetLnOnchainAddress {
665 failure_reason: e.to_string(),
666 })
667 }
668
669 async fn send_onchain(
670 &self,
671 SendOnchainRequest {
672 address,
673 amount,
674 fee_rate_sats_per_vbyte,
675 }: SendOnchainRequest,
676 ) -> Result<SendOnchainResponse, LightningRpcError> {
677 let onchain = self.node.onchain_payment();
678
679 let retain_reserves = false;
680 let txid = match amount {
681 BitcoinAmountOrAll::All => onchain.send_all_to_address(
682 &address.assume_checked(),
683 retain_reserves,
684 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
685 ),
686 BitcoinAmountOrAll::Amount(amount_sats) => onchain.send_to_address(
687 &address.assume_checked(),
688 amount_sats.to_sat(),
689 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
690 ),
691 }
692 .map_err(|e| LightningRpcError::FailedToWithdrawOnchain {
693 failure_reason: e.to_string(),
694 })?;
695
696 Ok(SendOnchainResponse {
697 txid: txid.to_string(),
698 })
699 }
700
701 async fn open_channel(
702 &self,
703 OpenChannelRequest {
704 pubkey,
705 host,
706 channel_size_sats,
707 push_amount_sats,
708 fee_rate_sats_per_vbyte,
709 base_fee_msat,
710 parts_per_million,
711 }: OpenChannelRequest,
712 ) -> Result<OpenChannelResponse, LightningRpcError> {
713 let push_amount_msats_or = if push_amount_sats == 0 {
714 None
715 } else {
716 Some(push_amount_sats * 1000)
717 };
718
719 if fee_rate_sats_per_vbyte.is_some() {
720 warn!(
723 target: LOG_LIGHTNING,
724 "Ignoring fee_rate_sats_per_vbyte on LDK channel open; LDK uses its built-in fee estimator"
725 );
726 }
727
728 let channel_config = match (base_fee_msat, parts_per_million) {
729 (None, None) => None,
730 (base, ppm) => {
731 let mut config = ChannelConfig::default();
732 if let Some(base) = base {
733 config.forwarding_fee_base_msat = u32::try_from(base).map_err(|_| {
734 LightningRpcError::FailedToOpenChannel {
735 failure_reason: format!(
736 "base_fee_msat {base} does not fit in u32 (LDK limit)"
737 ),
738 }
739 })?;
740 }
741 if let Some(ppm) = ppm {
742 config.forwarding_fee_proportional_millionths =
743 u32::try_from(ppm).map_err(|_| LightningRpcError::FailedToOpenChannel {
744 failure_reason: format!(
745 "parts_per_million {ppm} does not fit in u32 (LDK limit)"
746 ),
747 })?;
748 }
749 Some(config)
750 }
751 };
752
753 let (tx, rx) = oneshot::channel::<anyhow::Result<OutPoint>>();
754
755 {
756 let mut channels = self.pending_channels.write().await;
757 let user_channel_id = self
758 .node
759 .open_announced_channel(
760 pubkey,
761 SocketAddress::from_str(&host).map_err(|e| {
762 LightningRpcError::FailedToConnectToPeer {
763 failure_reason: e.to_string(),
764 }
765 })?,
766 channel_size_sats,
767 push_amount_msats_or,
768 channel_config,
769 )
770 .map_err(|e| LightningRpcError::FailedToOpenChannel {
771 failure_reason: e.to_string(),
772 })?;
773
774 channels.insert(UserChannelId(user_channel_id), tx);
775 }
776
777 match rx
778 .await
779 .map_err(|err| LightningRpcError::FailedToOpenChannel {
780 failure_reason: err.to_string(),
781 })? {
782 Ok(outpoint) => {
783 let funding_txid = outpoint.txid;
784
785 Ok(OpenChannelResponse {
786 funding_txid: funding_txid.to_string(),
787 })
788 }
789 Err(err) => Err(LightningRpcError::FailedToOpenChannel {
790 failure_reason: err.to_string(),
791 }),
792 }
793 }
794
795 async fn connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError> {
796 let NodeAddress { pubkey, address } = payload.node_address;
797 self.node.connect(pubkey, address, true).map_err(|e| {
803 LightningRpcError::FailedToConnectToPeer {
804 failure_reason: e.to_string(),
805 }
806 })
807 }
808
809 async fn close_channels_with_peer(
810 &self,
811 CloseChannelsWithPeerRequest {
812 pubkey,
813 force,
814 sats_per_vbyte: _,
815 }: CloseChannelsWithPeerRequest,
816 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
817 let mut num_channels_closed = 0;
818
819 info!(%pubkey, "Closing all channels with peer");
820 for channel_with_peer in self
821 .node
822 .list_channels()
823 .iter()
824 .filter(|channel| channel.counterparty_node_id == pubkey)
825 {
826 if force {
827 match self.node.force_close_channel(
828 &channel_with_peer.user_channel_id,
829 pubkey,
830 Some("User initiated force close".to_string()),
831 ) {
832 Ok(()) => num_channels_closed += 1,
833 Err(err) => {
834 error!(%pubkey, err = %err.fmt_compact(), "Could not force close channel");
835 }
836 }
837 } else {
838 match self
839 .node
840 .close_channel(&channel_with_peer.user_channel_id, pubkey)
841 {
842 Ok(()) => {
843 num_channels_closed += 1;
844 }
845 Err(err) => {
846 error!(%pubkey, err = %err.fmt_compact(), "Could not close channel");
847 }
848 }
849 }
850 }
851
852 Ok(CloseChannelsWithPeerResponse {
853 num_channels_closed,
854 })
855 }
856
857 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
858 let mut channels = Vec::new();
859 let network_graph = self.node.network_graph();
860
861 let peer_addresses: std::collections::HashMap<_, _> = self
863 .node
864 .list_peers()
865 .into_iter()
866 .map(|peer| (peer.node_id, peer.address.to_string()))
867 .collect();
868
869 for channel_details in self.node.list_channels().iter() {
870 let node_id = NodeId::from_pubkey(&channel_details.counterparty_node_id);
871 let node_info = network_graph.node(&node_id);
872
873 let remote_node_alias = node_info.as_ref().and_then(|info| {
875 info.announcement_info.as_ref().and_then(|announcement| {
876 let alias = announcement.alias().to_string();
877 if alias.is_empty() { None } else { Some(alias) }
878 })
879 });
880
881 let remote_address = peer_addresses
882 .get(&channel_details.counterparty_node_id)
883 .cloned();
884
885 channels.push(ChannelInfo {
886 remote_pubkey: channel_details.counterparty_node_id,
887 channel_size_sats: channel_details.channel_value_sats,
888 outbound_liquidity_sats: channel_details.outbound_capacity_msat / 1000,
889 inbound_liquidity_sats: channel_details.inbound_capacity_msat / 1000,
890 is_active: channel_details.is_usable,
891 funding_outpoint: channel_details.funding_txo,
892 remote_node_alias,
893 remote_address,
894 base_fee_msat: Some(u64::from(channel_details.config.forwarding_fee_base_msat)),
895 parts_per_million: Some(u64::from(
896 channel_details
897 .config
898 .forwarding_fee_proportional_millionths,
899 )),
900 });
901 }
902
903 Ok(ListChannelsResponse { channels })
904 }
905
906 async fn set_channel_fees(
907 &self,
908 payload: SetChannelFeesRequest,
909 ) -> Result<(), LightningRpcError> {
910 let channel = self
914 .node
915 .list_channels()
916 .into_iter()
917 .find(|c| c.funding_txo == Some(payload.funding_outpoint))
918 .ok_or_else(|| LightningRpcError::FailedToSetChannelFees {
919 failure_reason: format!(
920 "No channel found with funding outpoint {}",
921 payload.funding_outpoint,
922 ),
923 })?;
924
925 let forwarding_fee_base_msat = u32::try_from(payload.base_fee_msat).map_err(|_| {
926 LightningRpcError::FailedToSetChannelFees {
927 failure_reason: format!(
928 "base_fee_msat {} does not fit in u32 (LDK limit)",
929 payload.base_fee_msat,
930 ),
931 }
932 })?;
933 let forwarding_fee_proportional_millionths = u32::try_from(payload.parts_per_million)
934 .map_err(|_| LightningRpcError::FailedToSetChannelFees {
935 failure_reason: format!(
936 "parts_per_million {} does not fit in u32 (LDK limit)",
937 payload.parts_per_million,
938 ),
939 })?;
940
941 let new_config = ChannelConfig {
944 forwarding_fee_base_msat,
945 forwarding_fee_proportional_millionths,
946 ..channel.config
947 };
948
949 self.node
950 .update_channel_config(
951 &channel.user_channel_id,
952 channel.counterparty_node_id,
953 new_config,
954 )
955 .map_err(|e| LightningRpcError::FailedToSetChannelFees {
956 failure_reason: e.to_string(),
957 })?;
958
959 Ok(())
960 }
961
962 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
963 let balances = self.node.list_balances();
964 let channel_lists = self
965 .node
966 .list_channels()
967 .into_iter()
968 .filter(|chan| chan.is_usable)
969 .collect::<Vec<_>>();
970 let total_inbound_liquidity_balance_msat: u64 = channel_lists
972 .iter()
973 .map(|channel| channel.inbound_capacity_msat)
974 .sum();
975
976 Ok(GetBalancesResponse {
977 onchain_balance_sats: balances.total_onchain_balance_sats,
978 lightning_balance_msats: balances.total_lightning_balance_sats * 1000,
979 inbound_lightning_liquidity_msats: total_inbound_liquidity_balance_msat,
980 })
981 }
982
983 async fn get_invoice(
984 &self,
985 get_invoice_request: GetInvoiceRequest,
986 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
987 let invoices = self
988 .node
989 .list_payments_with_filter(|details| {
990 details.direction == PaymentDirection::Inbound
991 && details.id == PaymentId(get_invoice_request.payment_hash.to_byte_array())
992 && !matches!(details.kind, PaymentKind::Onchain { .. })
993 })
994 .iter()
995 .map(|details| {
996 let (preimage, payment_hash, _) = get_preimage_and_payment_hash(&details.kind);
997 let status = match details.status {
998 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
999 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
1000 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
1001 };
1002 GetInvoiceResponse {
1003 preimage: preimage.map(|p| p.to_string()),
1004 payment_hash,
1005 amount: Amount::from_msats(
1006 details
1007 .amount_msat
1008 .expect("amountless invoices are not supported"),
1009 ),
1010 created_at: UNIX_EPOCH + Duration::from_secs(details.latest_update_timestamp),
1011 status,
1012 }
1013 })
1014 .collect::<Vec<_>>();
1015
1016 Ok(invoices.first().cloned())
1017 }
1018
1019 async fn list_transactions(
1020 &self,
1021 start_secs: u64,
1022 end_secs: u64,
1023 ) -> Result<ListTransactionsResponse, LightningRpcError> {
1024 let transactions = self
1025 .node
1026 .list_payments_with_filter(|details| {
1027 !matches!(details.kind, PaymentKind::Onchain { .. })
1028 && details.latest_update_timestamp >= start_secs
1029 && details.latest_update_timestamp < end_secs
1030 })
1031 .iter()
1032 .map(|details| {
1033 let (preimage, payment_hash, payment_kind) =
1034 get_preimage_and_payment_hash(&details.kind);
1035 let direction = match details.direction {
1036 PaymentDirection::Outbound => {
1037 fedimint_gateway_common::PaymentDirection::Outbound
1038 }
1039 PaymentDirection::Inbound => fedimint_gateway_common::PaymentDirection::Inbound,
1040 };
1041 let status = match details.status {
1042 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
1043 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
1044 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
1045 };
1046 fedimint_gateway_common::PaymentDetails {
1047 payment_hash,
1048 preimage: preimage.map(|p| p.to_string()),
1049 payment_kind,
1050 amount: Amount::from_msats(
1051 details
1052 .amount_msat
1053 .expect("amountless invoices are not supported"),
1054 ),
1055 direction,
1056 status,
1057 timestamp_secs: details.latest_update_timestamp,
1058 }
1059 })
1060 .collect::<Vec<_>>();
1061 Ok(ListTransactionsResponse { transactions })
1062 }
1063
1064 fn create_offer(
1065 &self,
1066 amount: Option<Amount>,
1067 description: Option<String>,
1068 expiry_secs: Option<u32>,
1069 quantity: Option<u64>,
1070 ) -> Result<String, LightningRpcError> {
1071 let description = description.unwrap_or_default();
1072 let offer = if let Some(amount) = amount {
1073 self.node
1074 .bolt12_payment()
1075 .receive(amount.msats, &description, expiry_secs, quantity)
1076 .map_err(|err| LightningRpcError::Bolt12Error {
1077 failure_reason: err.to_string(),
1078 })?
1079 } else {
1080 self.node
1081 .bolt12_payment()
1082 .receive_variable_amount(&description, expiry_secs)
1083 .map_err(|err| LightningRpcError::Bolt12Error {
1084 failure_reason: err.to_string(),
1085 })?
1086 };
1087
1088 Ok(offer.to_string())
1089 }
1090
1091 async fn pay_offer(
1092 &self,
1093 offer: String,
1094 quantity: Option<u64>,
1095 amount: Option<Amount>,
1096 payer_note: Option<String>,
1097 ) -> Result<Preimage, LightningRpcError> {
1098 let offer = Offer::from_str(&offer).map_err(|_| LightningRpcError::Bolt12Error {
1099 failure_reason: "Failed to parse Bolt12 Offer".to_string(),
1100 })?;
1101
1102 let _offer_lock_guard = self
1103 .outbound_offer_lock_pool
1104 .blocking_lock(LdkOfferId(offer.id()));
1105
1106 let payment_id = if let Some(amount) = amount {
1107 self.node
1108 .bolt12_payment()
1109 .send_using_amount(&offer, amount.msats, quantity, payer_note)
1110 .map_err(|err| LightningRpcError::Bolt12Error {
1111 failure_reason: err.to_string(),
1112 })?
1113 } else {
1114 self.node
1115 .bolt12_payment()
1116 .send(&offer, quantity, payer_note)
1117 .map_err(|err| LightningRpcError::Bolt12Error {
1118 failure_reason: err.to_string(),
1119 })?
1120 };
1121
1122 loop {
1123 if let Some(payment_details) = self.node.payment(&payment_id) {
1124 match payment_details.status {
1125 PaymentStatus::Pending => {}
1126 PaymentStatus::Succeeded => match payment_details.kind {
1127 PaymentKind::Bolt12Offer {
1128 preimage: Some(preimage),
1129 ..
1130 } => {
1131 info!(target: LOG_LIGHTNING, offer = %offer, payment_id = %payment_id, preimage = %preimage, "Successfully paid offer");
1132 return Ok(Preimage(preimage.0));
1133 }
1134 _ => {
1135 return Err(LightningRpcError::FailedPayment {
1136 failure_reason: "Unexpected payment kind".to_string(),
1137 });
1138 }
1139 },
1140 PaymentStatus::Failed => {
1141 return Err(LightningRpcError::FailedPayment {
1142 failure_reason: "Bolt12 payment failed".to_string(),
1143 });
1144 }
1145 }
1146 }
1147 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
1148 }
1149 }
1150
1151 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
1152 block_in_place(|| {
1153 let _ = self.node.sync_wallets();
1154 });
1155 Ok(())
1156 }
1157}
1158
1159fn get_preimage_and_payment_hash(
1162 kind: &PaymentKind,
1163) -> (
1164 Option<Preimage>,
1165 Option<sha256::Hash>,
1166 fedimint_gateway_common::PaymentKind,
1167) {
1168 match kind {
1169 PaymentKind::Bolt11 {
1170 hash,
1171 preimage,
1172 secret: _,
1173 } => (
1174 preimage.map(|p| Preimage(p.0)),
1175 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
1176 fedimint_gateway_common::PaymentKind::Bolt11,
1177 ),
1178 PaymentKind::Bolt11Jit {
1179 hash,
1180 preimage,
1181 secret: _,
1182 lsp_fee_limits: _,
1183 ..
1184 } => (
1185 preimage.map(|p| Preimage(p.0)),
1186 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
1187 fedimint_gateway_common::PaymentKind::Bolt11,
1188 ),
1189 PaymentKind::Bolt12Offer {
1190 hash,
1191 preimage,
1192 secret: _,
1193 offer_id: _,
1194 payer_note: _,
1195 quantity: _,
1196 } => (
1197 preimage.map(|p| Preimage(p.0)),
1198 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
1199 fedimint_gateway_common::PaymentKind::Bolt12Offer,
1200 ),
1201 PaymentKind::Bolt12Refund {
1202 hash,
1203 preimage,
1204 secret: _,
1205 payer_note: _,
1206 quantity: _,
1207 } => (
1208 preimage.map(|p| Preimage(p.0)),
1209 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
1210 fedimint_gateway_common::PaymentKind::Bolt12Refund,
1211 ),
1212 PaymentKind::Spontaneous { hash, preimage } => (
1213 preimage.map(|p| Preimage(p.0)),
1214 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
1215 fedimint_gateway_common::PaymentKind::Bolt11,
1216 ),
1217 PaymentKind::Onchain { .. } => (None, None, fedimint_gateway_common::PaymentKind::Onchain),
1218 }
1219}
1220
1221fn get_esplora_url(server_url: SafeUrl) -> anyhow::Result<String> {
1229 let host = server_url
1231 .host_str()
1232 .ok_or(anyhow::anyhow!("Missing esplora host"))?;
1233 let server_url = if let Some(port) = server_url.port() {
1234 format!("{}://{}:{}", server_url.scheme(), host, port)
1235 } else {
1236 server_url.to_string()
1237 };
1238 Ok(server_url)
1239}
1240
1241#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1243enum PendingPaymentWakeup {
1244 NoWaiter,
1246 Woken,
1248 ReceiverDropped,
1250}
1251
1252#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1253struct LdkOfferId(OfferId);
1254
1255impl std::hash::Hash for LdkOfferId {
1256 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1257 state.write(&self.0.0);
1258 }
1259}
1260
1261#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1262pub struct UserChannelId(pub ldk_node::UserChannelId);
1263
1264impl PartialOrd for UserChannelId {
1265 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1266 Some(self.cmp(other))
1267 }
1268}
1269
1270impl Ord for UserChannelId {
1271 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1272 self.0.0.cmp(&other.0.0)
1273 }
1274}
1275
1276#[cfg(test)]
1277mod tests;