1use std::collections::BTreeMap;
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::task::{TaskGroup, TaskHandle, block_in_place};
12use fedimint_core::util::{FmtCompact, SafeUrl};
13use fedimint_core::{Amount, BitcoinAmountOrAll, crit};
14use fedimint_gateway_common::{
15 ChainSource, GetInvoiceRequest, GetInvoiceResponse, ListTransactionsResponse,
16};
17use fedimint_ln_common::contracts::Preimage;
18use fedimint_logging::LOG_LIGHTNING;
19use ldk_node::lightning::ln::msgs::SocketAddress;
20use ldk_node::lightning::routing::gossip::{NodeAlias, NodeId};
21use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus, SendingParameters};
22use lightning::ln::channelmanager::PaymentId;
23use lightning::offers::offer::{Offer, OfferId};
24use lightning::types::payment::{PaymentHash, PaymentPreimage};
25use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
26use tokio::sync::mpsc::Sender;
27use tokio::sync::{RwLock, oneshot};
28use tokio_stream::wrappers::ReceiverStream;
29use tracing::{debug, error, info, warn};
30
31use super::{ChannelInfo, ILnRpcClient, LightningRpcError, ListChannelsResponse, RouteHtlcStream};
32use crate::{
33 CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, CreateInvoiceRequest,
34 CreateInvoiceResponse, GetBalancesResponse, GetLnOnchainAddressResponse, GetNodeInfoResponse,
35 GetRouteHintsResponse, InterceptPaymentRequest, InterceptPaymentResponse, InvoiceDescription,
36 OpenChannelRequest, OpenChannelResponse, PayInvoiceResponse, PaymentAction, SendOnchainRequest,
37 SendOnchainResponse,
38};
39
40pub struct GatewayLdkClient {
41 node: Arc<ldk_node::Node>,
43
44 task_group: TaskGroup,
45
46 htlc_stream_receiver_or: Option<tokio::sync::mpsc::Receiver<InterceptPaymentRequest>>,
49
50 outbound_lightning_payment_lock_pool: lockable::LockPool<PaymentId>,
54
55 outbound_offer_lock_pool: lockable::LockPool<LdkOfferId>,
60
61 pending_channels:
66 Arc<RwLock<BTreeMap<UserChannelId, oneshot::Sender<anyhow::Result<OutPoint>>>>>,
67}
68
69impl std::fmt::Debug for GatewayLdkClient {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("GatewayLdkClient").finish_non_exhaustive()
72 }
73}
74
75impl GatewayLdkClient {
76 pub fn new(
81 data_dir: &Path,
82 chain_source: ChainSource,
83 network: Network,
84 lightning_port: u16,
85 alias: String,
86 mnemonic: Mnemonic,
87 runtime: Arc<tokio::runtime::Runtime>,
88 ) -> anyhow::Result<Self> {
89 let mut bytes = [0u8; 32];
90 let alias = if alias.is_empty() {
91 "LDK Gateway".to_string()
92 } else {
93 alias
94 };
95 let alias_bytes = alias.as_bytes();
96 let truncated = &alias_bytes[..alias_bytes.len().min(32)];
97 bytes[..truncated.len()].copy_from_slice(truncated);
98 let node_alias = Some(NodeAlias(bytes));
99
100 let mut node_builder = ldk_node::Builder::from_config(ldk_node::config::Config {
101 network,
102 listening_addresses: Some(vec![SocketAddress::TcpIpV4 {
103 addr: [0, 0, 0, 0],
104 port: lightning_port,
105 }]),
106 node_alias,
107 ..Default::default()
108 });
109
110 node_builder.set_entropy_bip39_mnemonic(mnemonic, None);
111
112 match chain_source.clone() {
113 ChainSource::Bitcoind {
114 username,
115 password,
116 server_url,
117 } => {
118 node_builder.set_chain_source_bitcoind_rpc(
119 server_url
120 .host_str()
121 .expect("Could not retrieve host from bitcoind RPC url")
122 .to_string(),
123 server_url
124 .port()
125 .expect("Could not retrieve port from bitcoind RPC url"),
126 username,
127 password,
128 );
129 }
130 ChainSource::Esplora { server_url } => {
131 node_builder.set_chain_source_esplora(get_esplora_url(server_url)?, None);
132 }
133 };
134 let Some(data_dir_str) = data_dir.to_str() else {
135 return Err(anyhow::anyhow!("Invalid data dir path"));
136 };
137 node_builder.set_storage_dir_path(data_dir_str.to_string());
138
139 info!(chain_source = %chain_source, data_dir = %data_dir_str, alias = %alias, "Starting LDK Node...");
140 let node = Arc::new(node_builder.build()?);
141 node.start_with_runtime(runtime).map_err(|err| {
142 crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to start LDK Node");
143 LightningRpcError::FailedToConnect
144 })?;
145
146 let (htlc_stream_sender, htlc_stream_receiver) = tokio::sync::mpsc::channel(1024);
147 let task_group = TaskGroup::new();
148
149 let node_clone = node.clone();
150 let pending_channels = Arc::new(RwLock::new(BTreeMap::new()));
151 let pending_channels_clone = pending_channels.clone();
152 task_group.spawn("ldk lightning node event handler", |handle| async move {
153 loop {
154 Self::handle_next_event(
155 &node_clone,
156 &htlc_stream_sender,
157 &handle,
158 pending_channels_clone.clone(),
159 )
160 .await;
161 }
162 });
163
164 info!("Successfully started LDK Gateway");
165 Ok(GatewayLdkClient {
166 node,
167 task_group,
168 htlc_stream_receiver_or: Some(htlc_stream_receiver),
169 outbound_lightning_payment_lock_pool: lockable::LockPool::new(),
170 outbound_offer_lock_pool: lockable::LockPool::new(),
171 pending_channels,
172 })
173 }
174
175 async fn handle_next_event(
176 node: &ldk_node::Node,
177 htlc_stream_sender: &Sender<InterceptPaymentRequest>,
178 handle: &TaskHandle,
179 pending_channels: Arc<
180 RwLock<BTreeMap<UserChannelId, oneshot::Sender<anyhow::Result<OutPoint>>>>,
181 >,
182 ) {
183 let event = tokio::select! {
187 event = node.next_event_async() => {
188 event
189 }
190 () = handle.make_shutdown_rx() => {
191 return;
192 }
193 };
194
195 match event {
196 ldk_node::Event::PaymentClaimable {
197 payment_id: _,
198 payment_hash,
199 claimable_amount_msat,
200 claim_deadline,
201 custom_records: _,
202 } => {
203 if let Err(err) = htlc_stream_sender
204 .send(InterceptPaymentRequest {
205 payment_hash: Hash::from_slice(&payment_hash.0)
206 .expect("Failed to create Hash"),
207 amount_msat: claimable_amount_msat,
208 expiry: claim_deadline.unwrap_or_default(),
209 short_channel_id: None,
210 incoming_chan_id: 0,
211 htlc_id: 0,
212 })
213 .await
214 {
215 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed send InterceptHtlcRequest to stream");
216 }
217 }
218 ldk_node::Event::ChannelPending {
219 channel_id,
220 user_channel_id,
221 former_temporary_channel_id: _,
222 counterparty_node_id: _,
223 funding_txo,
224 } => {
225 info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is pending");
226 let mut channels = pending_channels.write().await;
227 if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
228 let _ = sender.send(Ok(funding_txo));
229 } else {
230 debug!(
231 ?user_channel_id,
232 "No channel pending channel open for user channel id"
233 );
234 }
235 }
236 ldk_node::Event::ChannelClosed {
237 channel_id,
238 user_channel_id,
239 counterparty_node_id: _,
240 reason,
241 } => {
242 info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is closed");
243 let mut channels = pending_channels.write().await;
244 if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
245 let reason = if let Some(reason) = reason {
246 reason.to_string()
247 } else {
248 "Channel has been closed".to_string()
249 };
250 let _ = sender.send(Err(anyhow::anyhow!(reason)));
251 } else {
252 debug!(
253 ?user_channel_id,
254 "No channel pending channel open for user channel id"
255 );
256 }
257 }
258 _ => {}
259 }
260
261 if let Err(err) = node.event_handled() {
264 warn!(err = %err.fmt_compact(), "LDK could not mark event handled");
265 }
266 }
267}
268
269impl Drop for GatewayLdkClient {
270 fn drop(&mut self) {
271 self.task_group.shutdown();
272
273 info!(target: LOG_LIGHTNING, "Stopping LDK Node...");
274 match self.node.stop() {
275 Err(err) => {
276 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to stop LDK Node");
277 }
278 _ => {
279 info!(target: LOG_LIGHTNING, "LDK Node stopped.");
280 }
281 }
282 }
283}
284
285#[async_trait]
286impl ILnRpcClient for GatewayLdkClient {
287 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
288 let node_status = self.node.status();
289 let ldk_block_height = node_status.current_best_block.height;
290 let onchain_sync = node_status.latest_onchain_wallet_sync_timestamp;
291 let lightning_sync = node_status.latest_lightning_wallet_sync_timestamp;
292 let is_running = node_status.is_running;
293 debug!(target: LOG_LIGHTNING, ?onchain_sync, ?lightning_sync, ?is_running, "LDK Sync Status");
294
295 Ok(GetNodeInfoResponse {
296 pub_key: self.node.node_id(),
297 alias: match self.node.node_alias() {
298 Some(alias) => alias.to_string(),
299 None => format!("LDK Fedimint Gateway Node {}", self.node.node_id()),
300 },
301 network: self.node.config().network.to_string(),
302 block_height: ldk_block_height,
303 synced_to_chain: lightning_sync.is_some(),
306 })
307 }
308
309 async fn routehints(
310 &self,
311 _num_route_hints: usize,
312 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
313 Ok(GetRouteHintsResponse {
319 route_hints: vec![],
320 })
321 }
322
323 async fn pay(
324 &self,
325 invoice: Bolt11Invoice,
326 max_delay: u64,
327 max_fee: Amount,
328 ) -> Result<PayInvoiceResponse, LightningRpcError> {
329 let payment_id = PaymentId(*invoice.payment_hash().as_byte_array());
330
331 let _payment_lock_guard = self
337 .outbound_lightning_payment_lock_pool
338 .async_lock(payment_id)
339 .await;
340
341 if self.node.payment(&payment_id).is_none() {
348 assert_eq!(
349 self.node
350 .bolt11_payment()
351 .send(
352 &invoice,
353 Some(SendingParameters {
354 max_total_routing_fee_msat: Some(Some(max_fee.msats)),
355 max_total_cltv_expiry_delta: Some(max_delay as u32),
356 max_path_count: None,
357 max_channel_saturation_power_of_half: None,
358 }),
359 )
360 .map_err(|e| LightningRpcError::FailedPayment {
363 failure_reason: format!("LDK payment failed to initialize: {e:?}"),
364 })?,
365 payment_id
366 );
367 }
368
369 loop {
374 if let Some(payment_details) = self.node.payment(&payment_id) {
375 match payment_details.status {
376 PaymentStatus::Pending => {}
377 PaymentStatus::Succeeded => {
378 if let PaymentKind::Bolt11 {
379 preimage: Some(preimage),
380 ..
381 } = payment_details.kind
382 {
383 return Ok(PayInvoiceResponse {
384 preimage: Preimage(preimage.0),
385 });
386 }
387 }
388 PaymentStatus::Failed => {
389 return Err(LightningRpcError::FailedPayment {
390 failure_reason: "LDK payment failed".to_string(),
391 });
392 }
393 }
394 }
395 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
396 }
397 }
398
399 async fn route_htlcs<'a>(
400 mut self: Box<Self>,
401 _task_group: &TaskGroup,
402 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
403 let route_htlc_stream = match self.htlc_stream_receiver_or.take() {
404 Some(stream) => Ok(Box::pin(ReceiverStream::new(stream))),
405 None => Err(LightningRpcError::FailedToRouteHtlcs {
406 failure_reason:
407 "Stream does not exist. Likely was already taken by calling `route_htlcs()`."
408 .to_string(),
409 }),
410 }?;
411
412 Ok((route_htlc_stream, Arc::new(*self)))
413 }
414
415 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
416 let InterceptPaymentResponse {
417 action,
418 payment_hash,
419 incoming_chan_id: _,
420 htlc_id: _,
421 } = htlc;
422
423 let ph = PaymentHash(*payment_hash.clone().as_byte_array());
424
425 let claimable_amount_msat = 999_999_999_999_999;
431
432 let ph_hex_str = hex::encode(payment_hash);
433
434 if let PaymentAction::Settle(preimage) = action {
435 self.node
436 .bolt11_payment()
437 .claim_for_hash(ph, claimable_amount_msat, PaymentPreimage(preimage.0))
438 .map_err(|_| LightningRpcError::FailedToCompleteHtlc {
439 failure_reason: format!("Failed to claim LDK payment with hash {ph_hex_str}"),
440 })?;
441 } else {
442 warn!(target: LOG_LIGHTNING, payment_hash = %ph_hex_str, "Unwinding payment because the action was not `Settle`");
443 self.node.bolt11_payment().fail_for_hash(ph).map_err(|_| {
444 LightningRpcError::FailedToCompleteHtlc {
445 failure_reason: format!("Failed to unwind LDK payment with hash {ph_hex_str}"),
446 }
447 })?;
448 }
449
450 return Ok(());
451 }
452
453 async fn create_invoice(
454 &self,
455 create_invoice_request: CreateInvoiceRequest,
456 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
457 let payment_hash_or = if let Some(payment_hash) = create_invoice_request.payment_hash {
458 let ph = PaymentHash(*payment_hash.as_byte_array());
459 Some(ph)
460 } else {
461 None
462 };
463
464 let description = match create_invoice_request.description {
465 Some(InvoiceDescription::Direct(desc)) => {
466 Bolt11InvoiceDescription::Direct(Description::new(desc).map_err(|_| {
467 LightningRpcError::FailedToGetInvoice {
468 failure_reason: "Invalid description".to_string(),
469 }
470 })?)
471 }
472 Some(InvoiceDescription::Hash(hash)) => {
473 Bolt11InvoiceDescription::Hash(lightning_invoice::Sha256(hash))
474 }
475 None => Bolt11InvoiceDescription::Direct(Description::empty()),
476 };
477
478 let invoice = match payment_hash_or {
479 Some(payment_hash) => self.node.bolt11_payment().receive_for_hash(
480 create_invoice_request.amount_msat,
481 &description,
482 create_invoice_request.expiry_secs,
483 payment_hash,
484 ),
485 None => self.node.bolt11_payment().receive(
486 create_invoice_request.amount_msat,
487 &description,
488 create_invoice_request.expiry_secs,
489 ),
490 }
491 .map_err(|e| LightningRpcError::FailedToGetInvoice {
492 failure_reason: e.to_string(),
493 })?;
494
495 Ok(CreateInvoiceResponse {
496 invoice: invoice.to_string(),
497 })
498 }
499
500 async fn get_ln_onchain_address(
501 &self,
502 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
503 self.node
504 .onchain_payment()
505 .new_address()
506 .map(|address| GetLnOnchainAddressResponse {
507 address: address.to_string(),
508 })
509 .map_err(|e| LightningRpcError::FailedToGetLnOnchainAddress {
510 failure_reason: e.to_string(),
511 })
512 }
513
514 async fn send_onchain(
515 &self,
516 SendOnchainRequest {
517 address,
518 amount,
519 fee_rate_sats_per_vbyte,
520 }: SendOnchainRequest,
521 ) -> Result<SendOnchainResponse, LightningRpcError> {
522 let onchain = self.node.onchain_payment();
523
524 let retain_reserves = false;
525 let txid = match amount {
526 BitcoinAmountOrAll::All => onchain.send_all_to_address(
527 &address.assume_checked(),
528 retain_reserves,
529 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
530 ),
531 BitcoinAmountOrAll::Amount(amount_sats) => onchain.send_to_address(
532 &address.assume_checked(),
533 amount_sats.to_sat(),
534 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
535 ),
536 }
537 .map_err(|e| LightningRpcError::FailedToWithdrawOnchain {
538 failure_reason: e.to_string(),
539 })?;
540
541 Ok(SendOnchainResponse {
542 txid: txid.to_string(),
543 })
544 }
545
546 async fn open_channel(
547 &self,
548 OpenChannelRequest {
549 pubkey,
550 host,
551 channel_size_sats,
552 push_amount_sats,
553 }: OpenChannelRequest,
554 ) -> Result<OpenChannelResponse, LightningRpcError> {
555 let push_amount_msats_or = if push_amount_sats == 0 {
556 None
557 } else {
558 Some(push_amount_sats * 1000)
559 };
560
561 let (tx, rx) = oneshot::channel::<anyhow::Result<OutPoint>>();
562
563 {
564 let mut channels = self.pending_channels.write().await;
565 let user_channel_id = self
566 .node
567 .open_announced_channel(
568 pubkey,
569 SocketAddress::from_str(&host).map_err(|e| {
570 LightningRpcError::FailedToConnectToPeer {
571 failure_reason: e.to_string(),
572 }
573 })?,
574 channel_size_sats,
575 push_amount_msats_or,
576 None,
577 )
578 .map_err(|e| LightningRpcError::FailedToOpenChannel {
579 failure_reason: e.to_string(),
580 })?;
581
582 channels.insert(UserChannelId(user_channel_id), tx);
583 }
584
585 match rx
586 .await
587 .map_err(|err| LightningRpcError::FailedToOpenChannel {
588 failure_reason: err.to_string(),
589 })? {
590 Ok(outpoint) => {
591 let funding_txid = outpoint.txid;
592
593 Ok(OpenChannelResponse {
594 funding_txid: funding_txid.to_string(),
595 })
596 }
597 Err(err) => Err(LightningRpcError::FailedToOpenChannel {
598 failure_reason: err.to_string(),
599 }),
600 }
601 }
602
603 async fn close_channels_with_peer(
604 &self,
605 CloseChannelsWithPeerRequest {
606 pubkey,
607 force,
608 sats_per_vbyte: _,
609 }: CloseChannelsWithPeerRequest,
610 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
611 let mut num_channels_closed = 0;
612
613 info!(%pubkey, "Closing all channels with peer");
614 for channel_with_peer in self
615 .node
616 .list_channels()
617 .iter()
618 .filter(|channel| channel.counterparty_node_id == pubkey)
619 {
620 if force {
621 match self.node.force_close_channel(
622 &channel_with_peer.user_channel_id,
623 pubkey,
624 Some("User initiated force close".to_string()),
625 ) {
626 Ok(()) => num_channels_closed += 1,
627 Err(err) => {
628 error!(%pubkey, err = %err.fmt_compact(), "Could not force close channel");
629 }
630 }
631 } else {
632 match self
633 .node
634 .close_channel(&channel_with_peer.user_channel_id, pubkey)
635 {
636 Ok(()) => {
637 num_channels_closed += 1;
638 }
639 Err(err) => {
640 error!(%pubkey, err = %err.fmt_compact(), "Could not close channel");
641 }
642 }
643 }
644 }
645
646 Ok(CloseChannelsWithPeerResponse {
647 num_channels_closed,
648 })
649 }
650
651 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
652 let mut channels = Vec::new();
653 let network_graph = self.node.network_graph();
654
655 let peer_addresses: std::collections::HashMap<_, _> = self
657 .node
658 .list_peers()
659 .into_iter()
660 .map(|peer| (peer.node_id, peer.address.to_string()))
661 .collect();
662
663 for channel_details in self.node.list_channels().iter() {
664 let node_id = NodeId::from_pubkey(&channel_details.counterparty_node_id);
665 let node_info = network_graph.node(&node_id);
666
667 let remote_node_alias = node_info.as_ref().and_then(|info| {
669 info.announcement_info.as_ref().and_then(|announcement| {
670 let alias = announcement.alias().to_string();
671 if alias.is_empty() { None } else { Some(alias) }
672 })
673 });
674
675 let remote_address = peer_addresses
676 .get(&channel_details.counterparty_node_id)
677 .cloned();
678
679 channels.push(ChannelInfo {
680 remote_pubkey: channel_details.counterparty_node_id,
681 channel_size_sats: channel_details.channel_value_sats,
682 outbound_liquidity_sats: channel_details.outbound_capacity_msat / 1000,
683 inbound_liquidity_sats: channel_details.inbound_capacity_msat / 1000,
684 is_active: channel_details.is_usable,
685 funding_outpoint: channel_details.funding_txo,
686 remote_node_alias,
687 remote_address,
688 });
689 }
690
691 Ok(ListChannelsResponse { channels })
692 }
693
694 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
695 let balances = self.node.list_balances();
696 let channel_lists = self
697 .node
698 .list_channels()
699 .into_iter()
700 .filter(|chan| chan.is_usable)
701 .collect::<Vec<_>>();
702 let total_inbound_liquidity_balance_msat: u64 = channel_lists
704 .iter()
705 .map(|channel| channel.inbound_capacity_msat)
706 .sum();
707
708 Ok(GetBalancesResponse {
709 onchain_balance_sats: balances.total_onchain_balance_sats,
710 lightning_balance_msats: balances.total_lightning_balance_sats * 1000,
711 inbound_lightning_liquidity_msats: total_inbound_liquidity_balance_msat,
712 })
713 }
714
715 async fn get_invoice(
716 &self,
717 get_invoice_request: GetInvoiceRequest,
718 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
719 let invoices = self
720 .node
721 .list_payments_with_filter(|details| {
722 details.direction == PaymentDirection::Inbound
723 && details.id == PaymentId(get_invoice_request.payment_hash.to_byte_array())
724 && !matches!(details.kind, PaymentKind::Onchain { .. })
725 })
726 .iter()
727 .map(|details| {
728 let (preimage, payment_hash, _) = get_preimage_and_payment_hash(&details.kind);
729 let status = match details.status {
730 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
731 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
732 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
733 };
734 GetInvoiceResponse {
735 preimage: preimage.map(|p| p.to_string()),
736 payment_hash,
737 amount: Amount::from_msats(
738 details
739 .amount_msat
740 .expect("amountless invoices are not supported"),
741 ),
742 created_at: UNIX_EPOCH + Duration::from_secs(details.latest_update_timestamp),
743 status,
744 }
745 })
746 .collect::<Vec<_>>();
747
748 Ok(invoices.first().cloned())
749 }
750
751 async fn list_transactions(
752 &self,
753 start_secs: u64,
754 end_secs: u64,
755 ) -> Result<ListTransactionsResponse, LightningRpcError> {
756 let transactions = self
757 .node
758 .list_payments_with_filter(|details| {
759 !matches!(details.kind, PaymentKind::Onchain { .. })
760 && details.latest_update_timestamp >= start_secs
761 && details.latest_update_timestamp < end_secs
762 })
763 .iter()
764 .map(|details| {
765 let (preimage, payment_hash, payment_kind) =
766 get_preimage_and_payment_hash(&details.kind);
767 let direction = match details.direction {
768 PaymentDirection::Outbound => {
769 fedimint_gateway_common::PaymentDirection::Outbound
770 }
771 PaymentDirection::Inbound => fedimint_gateway_common::PaymentDirection::Inbound,
772 };
773 let status = match details.status {
774 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
775 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
776 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
777 };
778 fedimint_gateway_common::PaymentDetails {
779 payment_hash,
780 preimage: preimage.map(|p| p.to_string()),
781 payment_kind,
782 amount: Amount::from_msats(
783 details
784 .amount_msat
785 .expect("amountless invoices are not supported"),
786 ),
787 direction,
788 status,
789 timestamp_secs: details.latest_update_timestamp,
790 }
791 })
792 .collect::<Vec<_>>();
793 Ok(ListTransactionsResponse { transactions })
794 }
795
796 fn create_offer(
797 &self,
798 amount: Option<Amount>,
799 description: Option<String>,
800 expiry_secs: Option<u32>,
801 quantity: Option<u64>,
802 ) -> Result<String, LightningRpcError> {
803 let description = description.unwrap_or_default();
804 let offer = if let Some(amount) = amount {
805 self.node
806 .bolt12_payment()
807 .receive(amount.msats, &description, expiry_secs, quantity)
808 .map_err(|err| LightningRpcError::Bolt12Error {
809 failure_reason: err.to_string(),
810 })?
811 } else {
812 self.node
813 .bolt12_payment()
814 .receive_variable_amount(&description, expiry_secs)
815 .map_err(|err| LightningRpcError::Bolt12Error {
816 failure_reason: err.to_string(),
817 })?
818 };
819
820 Ok(offer.to_string())
821 }
822
823 async fn pay_offer(
824 &self,
825 offer: String,
826 quantity: Option<u64>,
827 amount: Option<Amount>,
828 payer_note: Option<String>,
829 ) -> Result<Preimage, LightningRpcError> {
830 let offer = Offer::from_str(&offer).map_err(|_| LightningRpcError::Bolt12Error {
831 failure_reason: "Failed to parse Bolt12 Offer".to_string(),
832 })?;
833
834 let _offer_lock_guard = self
835 .outbound_offer_lock_pool
836 .blocking_lock(LdkOfferId(offer.id()));
837
838 let payment_id = if let Some(amount) = amount {
839 self.node
840 .bolt12_payment()
841 .send_using_amount(&offer, amount.msats, quantity, payer_note)
842 .map_err(|err| LightningRpcError::Bolt12Error {
843 failure_reason: err.to_string(),
844 })?
845 } else {
846 self.node
847 .bolt12_payment()
848 .send(&offer, quantity, payer_note)
849 .map_err(|err| LightningRpcError::Bolt12Error {
850 failure_reason: err.to_string(),
851 })?
852 };
853
854 loop {
855 if let Some(payment_details) = self.node.payment(&payment_id) {
856 match payment_details.status {
857 PaymentStatus::Pending => {}
858 PaymentStatus::Succeeded => match payment_details.kind {
859 PaymentKind::Bolt12Offer {
860 preimage: Some(preimage),
861 ..
862 } => {
863 info!(target: LOG_LIGHTNING, offer = %offer, payment_id = %payment_id, preimage = %preimage, "Successfully paid offer");
864 return Ok(Preimage(preimage.0));
865 }
866 _ => {
867 return Err(LightningRpcError::FailedPayment {
868 failure_reason: "Unexpected payment kind".to_string(),
869 });
870 }
871 },
872 PaymentStatus::Failed => {
873 return Err(LightningRpcError::FailedPayment {
874 failure_reason: "Bolt12 payment failed".to_string(),
875 });
876 }
877 }
878 }
879 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
880 }
881 }
882
883 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
884 block_in_place(|| {
885 let _ = self.node.sync_wallets();
886 });
887 Ok(())
888 }
889}
890
891fn get_preimage_and_payment_hash(
894 kind: &PaymentKind,
895) -> (
896 Option<Preimage>,
897 Option<sha256::Hash>,
898 fedimint_gateway_common::PaymentKind,
899) {
900 match kind {
901 PaymentKind::Bolt11 {
902 hash,
903 preimage,
904 secret: _,
905 } => (
906 preimage.map(|p| Preimage(p.0)),
907 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
908 fedimint_gateway_common::PaymentKind::Bolt11,
909 ),
910 PaymentKind::Bolt11Jit {
911 hash,
912 preimage,
913 secret: _,
914 lsp_fee_limits: _,
915 ..
916 } => (
917 preimage.map(|p| Preimage(p.0)),
918 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
919 fedimint_gateway_common::PaymentKind::Bolt11,
920 ),
921 PaymentKind::Bolt12Offer {
922 hash,
923 preimage,
924 secret: _,
925 offer_id: _,
926 payer_note: _,
927 quantity: _,
928 } => (
929 preimage.map(|p| Preimage(p.0)),
930 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
931 fedimint_gateway_common::PaymentKind::Bolt12Offer,
932 ),
933 PaymentKind::Bolt12Refund {
934 hash,
935 preimage,
936 secret: _,
937 payer_note: _,
938 quantity: _,
939 } => (
940 preimage.map(|p| Preimage(p.0)),
941 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
942 fedimint_gateway_common::PaymentKind::Bolt12Refund,
943 ),
944 PaymentKind::Spontaneous { hash, preimage } => (
945 preimage.map(|p| Preimage(p.0)),
946 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
947 fedimint_gateway_common::PaymentKind::Bolt11,
948 ),
949 PaymentKind::Onchain { .. } => (None, None, fedimint_gateway_common::PaymentKind::Onchain),
950 }
951}
952
953fn get_esplora_url(server_url: SafeUrl) -> anyhow::Result<String> {
961 let host = server_url
963 .host_str()
964 .ok_or(anyhow::anyhow!("Missing esplora host"))?;
965 let server_url = if let Some(port) = server_url.port() {
966 format!("{}://{}:{}", server_url.scheme(), host, port)
967 } else {
968 server_url.to_string()
969 };
970 Ok(server_url)
971}
972
973#[derive(Debug, Clone, Copy, Eq, PartialEq)]
974struct LdkOfferId(OfferId);
975
976impl std::hash::Hash for LdkOfferId {
977 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
978 state.write(&self.0.0);
979 }
980}
981
982#[derive(Debug, Copy, Clone, PartialEq, Eq)]
983pub struct UserChannelId(pub ldk_node::UserChannelId);
984
985impl PartialOrd for UserChannelId {
986 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
987 Some(self.cmp(other))
988 }
989}
990
991impl Ord for UserChannelId {
992 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
993 self.0.0.cmp(&other.0.0)
994 }
995}
996
997#[cfg(test)]
998mod tests;