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 NO_INCOMING_CIRCUIT, OpenChannelRequest, OpenChannelResponse, PayInvoiceResponse,
37 PaymentAction, SendOnchainRequest, 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: NO_INCOMING_CIRCUIT.0,
213 htlc_id: NO_INCOMING_CIRCUIT.1,
214 })
215 .await
216 {
217 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed send InterceptHtlcRequest to stream");
218 }
219 }
220 ldk_node::Event::ChannelPending {
221 channel_id,
222 user_channel_id,
223 former_temporary_channel_id: _,
224 counterparty_node_id: _,
225 funding_txo,
226 } => {
227 info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is pending");
228 let mut channels = pending_channels.write().await;
229 if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
230 let _ = sender.send(Ok(funding_txo));
231 } else {
232 debug!(
233 ?user_channel_id,
234 "No channel pending channel open for user channel id"
235 );
236 }
237 }
238 ldk_node::Event::ChannelClosed {
239 channel_id,
240 user_channel_id,
241 counterparty_node_id: _,
242 reason,
243 } => {
244 info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is closed");
245 let mut channels = pending_channels.write().await;
246 if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
247 let reason = if let Some(reason) = reason {
248 reason.to_string()
249 } else {
250 "Channel has been closed".to_string()
251 };
252 let _ = sender.send(Err(anyhow::anyhow!(reason)));
253 } else {
254 debug!(
255 ?user_channel_id,
256 "No channel pending channel open for user channel id"
257 );
258 }
259 }
260 _ => {}
261 }
262
263 if let Err(err) = node.event_handled() {
266 warn!(err = %err.fmt_compact(), "LDK could not mark event handled");
267 }
268 }
269}
270
271impl Drop for GatewayLdkClient {
272 fn drop(&mut self) {
273 self.task_group.shutdown();
274
275 info!(target: LOG_LIGHTNING, "Stopping LDK Node...");
276 match self.node.stop() {
277 Err(err) => {
278 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to stop LDK Node");
279 }
280 _ => {
281 info!(target: LOG_LIGHTNING, "LDK Node stopped.");
282 }
283 }
284 }
285}
286
287#[async_trait]
288impl ILnRpcClient for GatewayLdkClient {
289 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
290 let node_status = self.node.status();
291 let ldk_block_height = node_status.current_best_block.height;
292 let onchain_sync = node_status.latest_onchain_wallet_sync_timestamp;
293 let lightning_sync = node_status.latest_lightning_wallet_sync_timestamp;
294 let is_running = node_status.is_running;
295 debug!(target: LOG_LIGHTNING, ?onchain_sync, ?lightning_sync, ?is_running, "LDK Sync Status");
296
297 Ok(GetNodeInfoResponse {
298 pub_key: self.node.node_id(),
299 alias: match self.node.node_alias() {
300 Some(alias) => alias.to_string(),
301 None => format!("LDK Fedimint Gateway Node {}", self.node.node_id()),
302 },
303 network: self.node.config().network.to_string(),
304 block_height: ldk_block_height,
305 synced_to_chain: lightning_sync.is_some(),
308 })
309 }
310
311 async fn routehints(
312 &self,
313 _num_route_hints: usize,
314 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
315 Ok(GetRouteHintsResponse {
321 route_hints: vec![],
322 })
323 }
324
325 async fn pay(
326 &self,
327 invoice: Bolt11Invoice,
328 max_delay: u64,
329 max_fee: Amount,
330 ) -> Result<PayInvoiceResponse, LightningRpcError> {
331 let payment_id = PaymentId(*invoice.payment_hash().as_byte_array());
332
333 let _payment_lock_guard = self
339 .outbound_lightning_payment_lock_pool
340 .async_lock(payment_id)
341 .await;
342
343 if self.node.payment(&payment_id).is_none() {
350 assert_eq!(
351 self.node
352 .bolt11_payment()
353 .send(
354 &invoice,
355 Some(SendingParameters {
356 max_total_routing_fee_msat: Some(Some(max_fee.msats)),
357 max_total_cltv_expiry_delta: Some(max_delay as u32),
358 max_path_count: None,
359 max_channel_saturation_power_of_half: None,
360 }),
361 )
362 .map_err(|e| LightningRpcError::FailedPayment {
365 failure_reason: format!("LDK payment failed to initialize: {e:?}"),
366 })?,
367 payment_id
368 );
369 }
370
371 loop {
376 if let Some(payment_details) = self.node.payment(&payment_id) {
377 match payment_details.status {
378 PaymentStatus::Pending => {}
379 PaymentStatus::Succeeded => {
380 if let PaymentKind::Bolt11 {
381 preimage: Some(preimage),
382 ..
383 } = payment_details.kind
384 {
385 return Ok(PayInvoiceResponse {
386 preimage: Preimage(preimage.0),
387 });
388 }
389 }
390 PaymentStatus::Failed => {
391 return Err(LightningRpcError::FailedPayment {
392 failure_reason: "LDK payment failed".to_string(),
393 });
394 }
395 }
396 }
397 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
398 }
399 }
400
401 async fn route_htlcs<'a>(
402 mut self: Box<Self>,
403 _task_group: &TaskGroup,
404 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
405 let route_htlc_stream = match self.htlc_stream_receiver_or.take() {
406 Some(stream) => Ok(Box::pin(ReceiverStream::new(stream))),
407 None => Err(LightningRpcError::FailedToRouteHtlcs {
408 failure_reason:
409 "Stream does not exist. Likely was already taken by calling `route_htlcs()`."
410 .to_string(),
411 }),
412 }?;
413
414 Ok((route_htlc_stream, Arc::new(*self)))
415 }
416
417 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
418 let InterceptPaymentResponse {
419 action,
420 payment_hash,
421 incoming_chan_id: _,
422 htlc_id: _,
423 } = htlc;
424
425 let ph = PaymentHash(*payment_hash.clone().as_byte_array());
426
427 let claimable_amount_msat = 999_999_999_999_999;
433
434 let ph_hex_str = hex::encode(payment_hash);
435
436 if let PaymentAction::Settle(preimage) = action {
437 self.node
438 .bolt11_payment()
439 .claim_for_hash(ph, claimable_amount_msat, PaymentPreimage(preimage.0))
440 .map_err(|_| LightningRpcError::FailedToCompleteHtlc {
441 failure_reason: format!("Failed to claim LDK payment with hash {ph_hex_str}"),
442 })?;
443 } else {
444 warn!(target: LOG_LIGHTNING, payment_hash = %ph_hex_str, "Unwinding payment because the action was not `Settle`");
445 self.node.bolt11_payment().fail_for_hash(ph).map_err(|_| {
446 LightningRpcError::FailedToCompleteHtlc {
447 failure_reason: format!("Failed to unwind LDK payment with hash {ph_hex_str}"),
448 }
449 })?;
450 }
451
452 return Ok(());
453 }
454
455 async fn create_invoice(
456 &self,
457 create_invoice_request: CreateInvoiceRequest,
458 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
459 let payment_hash_or = if let Some(payment_hash) = create_invoice_request.payment_hash {
460 let ph = PaymentHash(*payment_hash.as_byte_array());
461 Some(ph)
462 } else {
463 None
464 };
465
466 let description = match create_invoice_request.description {
467 Some(InvoiceDescription::Direct(desc)) => {
468 Bolt11InvoiceDescription::Direct(Description::new(desc).map_err(|_| {
469 LightningRpcError::FailedToGetInvoice {
470 failure_reason: "Invalid description".to_string(),
471 }
472 })?)
473 }
474 Some(InvoiceDescription::Hash(hash)) => {
475 Bolt11InvoiceDescription::Hash(lightning_invoice::Sha256(hash))
476 }
477 None => Bolt11InvoiceDescription::Direct(Description::empty()),
478 };
479
480 let invoice = match payment_hash_or {
481 Some(payment_hash) => self.node.bolt11_payment().receive_for_hash(
482 create_invoice_request.amount_msat,
483 &description,
484 create_invoice_request.expiry_secs,
485 payment_hash,
486 ),
487 None => self.node.bolt11_payment().receive(
488 create_invoice_request.amount_msat,
489 &description,
490 create_invoice_request.expiry_secs,
491 ),
492 }
493 .map_err(|e| LightningRpcError::FailedToGetInvoice {
494 failure_reason: e.to_string(),
495 })?;
496
497 Ok(CreateInvoiceResponse {
498 invoice: invoice.to_string(),
499 })
500 }
501
502 async fn get_ln_onchain_address(
503 &self,
504 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
505 self.node
506 .onchain_payment()
507 .new_address()
508 .map(|address| GetLnOnchainAddressResponse {
509 address: address.to_string(),
510 })
511 .map_err(|e| LightningRpcError::FailedToGetLnOnchainAddress {
512 failure_reason: e.to_string(),
513 })
514 }
515
516 async fn send_onchain(
517 &self,
518 SendOnchainRequest {
519 address,
520 amount,
521 fee_rate_sats_per_vbyte,
522 }: SendOnchainRequest,
523 ) -> Result<SendOnchainResponse, LightningRpcError> {
524 let onchain = self.node.onchain_payment();
525
526 let retain_reserves = false;
527 let txid = match amount {
528 BitcoinAmountOrAll::All => onchain.send_all_to_address(
529 &address.assume_checked(),
530 retain_reserves,
531 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
532 ),
533 BitcoinAmountOrAll::Amount(amount_sats) => onchain.send_to_address(
534 &address.assume_checked(),
535 amount_sats.to_sat(),
536 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
537 ),
538 }
539 .map_err(|e| LightningRpcError::FailedToWithdrawOnchain {
540 failure_reason: e.to_string(),
541 })?;
542
543 Ok(SendOnchainResponse {
544 txid: txid.to_string(),
545 })
546 }
547
548 async fn open_channel(
549 &self,
550 OpenChannelRequest {
551 pubkey,
552 host,
553 channel_size_sats,
554 push_amount_sats,
555 }: OpenChannelRequest,
556 ) -> Result<OpenChannelResponse, LightningRpcError> {
557 let push_amount_msats_or = if push_amount_sats == 0 {
558 None
559 } else {
560 Some(push_amount_sats * 1000)
561 };
562
563 let (tx, rx) = oneshot::channel::<anyhow::Result<OutPoint>>();
564
565 {
566 let mut channels = self.pending_channels.write().await;
567 let user_channel_id = self
568 .node
569 .open_announced_channel(
570 pubkey,
571 SocketAddress::from_str(&host).map_err(|e| {
572 LightningRpcError::FailedToConnectToPeer {
573 failure_reason: e.to_string(),
574 }
575 })?,
576 channel_size_sats,
577 push_amount_msats_or,
578 None,
579 )
580 .map_err(|e| LightningRpcError::FailedToOpenChannel {
581 failure_reason: e.to_string(),
582 })?;
583
584 channels.insert(UserChannelId(user_channel_id), tx);
585 }
586
587 match rx
588 .await
589 .map_err(|err| LightningRpcError::FailedToOpenChannel {
590 failure_reason: err.to_string(),
591 })? {
592 Ok(outpoint) => {
593 let funding_txid = outpoint.txid;
594
595 Ok(OpenChannelResponse {
596 funding_txid: funding_txid.to_string(),
597 })
598 }
599 Err(err) => Err(LightningRpcError::FailedToOpenChannel {
600 failure_reason: err.to_string(),
601 }),
602 }
603 }
604
605 async fn close_channels_with_peer(
606 &self,
607 CloseChannelsWithPeerRequest {
608 pubkey,
609 force,
610 sats_per_vbyte: _,
611 }: CloseChannelsWithPeerRequest,
612 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
613 let mut num_channels_closed = 0;
614
615 info!(%pubkey, "Closing all channels with peer");
616 for channel_with_peer in self
617 .node
618 .list_channels()
619 .iter()
620 .filter(|channel| channel.counterparty_node_id == pubkey)
621 {
622 if force {
623 match self.node.force_close_channel(
624 &channel_with_peer.user_channel_id,
625 pubkey,
626 Some("User initiated force close".to_string()),
627 ) {
628 Ok(()) => num_channels_closed += 1,
629 Err(err) => {
630 error!(%pubkey, err = %err.fmt_compact(), "Could not force close channel");
631 }
632 }
633 } else {
634 match self
635 .node
636 .close_channel(&channel_with_peer.user_channel_id, pubkey)
637 {
638 Ok(()) => {
639 num_channels_closed += 1;
640 }
641 Err(err) => {
642 error!(%pubkey, err = %err.fmt_compact(), "Could not close channel");
643 }
644 }
645 }
646 }
647
648 Ok(CloseChannelsWithPeerResponse {
649 num_channels_closed,
650 })
651 }
652
653 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
654 let mut channels = Vec::new();
655 let network_graph = self.node.network_graph();
656
657 let peer_addresses: std::collections::HashMap<_, _> = self
659 .node
660 .list_peers()
661 .into_iter()
662 .map(|peer| (peer.node_id, peer.address.to_string()))
663 .collect();
664
665 for channel_details in self.node.list_channels().iter() {
666 let node_id = NodeId::from_pubkey(&channel_details.counterparty_node_id);
667 let node_info = network_graph.node(&node_id);
668
669 let remote_node_alias = node_info.as_ref().and_then(|info| {
671 info.announcement_info.as_ref().and_then(|announcement| {
672 let alias = announcement.alias().to_string();
673 if alias.is_empty() { None } else { Some(alias) }
674 })
675 });
676
677 let remote_address = peer_addresses
678 .get(&channel_details.counterparty_node_id)
679 .cloned();
680
681 channels.push(ChannelInfo {
682 remote_pubkey: channel_details.counterparty_node_id,
683 channel_size_sats: channel_details.channel_value_sats,
684 outbound_liquidity_sats: channel_details.outbound_capacity_msat / 1000,
685 inbound_liquidity_sats: channel_details.inbound_capacity_msat / 1000,
686 is_active: channel_details.is_usable,
687 funding_outpoint: channel_details.funding_txo,
688 remote_node_alias,
689 remote_address,
690 });
691 }
692
693 Ok(ListChannelsResponse { channels })
694 }
695
696 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
697 let balances = self.node.list_balances();
698 let channel_lists = self
699 .node
700 .list_channels()
701 .into_iter()
702 .filter(|chan| chan.is_usable)
703 .collect::<Vec<_>>();
704 let total_inbound_liquidity_balance_msat: u64 = channel_lists
706 .iter()
707 .map(|channel| channel.inbound_capacity_msat)
708 .sum();
709
710 Ok(GetBalancesResponse {
711 onchain_balance_sats: balances.total_onchain_balance_sats,
712 lightning_balance_msats: balances.total_lightning_balance_sats * 1000,
713 inbound_lightning_liquidity_msats: total_inbound_liquidity_balance_msat,
714 })
715 }
716
717 async fn get_invoice(
718 &self,
719 get_invoice_request: GetInvoiceRequest,
720 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
721 let invoices = self
722 .node
723 .list_payments_with_filter(|details| {
724 details.direction == PaymentDirection::Inbound
725 && details.id == PaymentId(get_invoice_request.payment_hash.to_byte_array())
726 && !matches!(details.kind, PaymentKind::Onchain { .. })
727 })
728 .iter()
729 .map(|details| {
730 let (preimage, payment_hash, _) = get_preimage_and_payment_hash(&details.kind);
731 let status = match details.status {
732 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
733 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
734 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
735 };
736 GetInvoiceResponse {
737 preimage: preimage.map(|p| p.to_string()),
738 payment_hash,
739 amount: Amount::from_msats(
740 details
741 .amount_msat
742 .expect("amountless invoices are not supported"),
743 ),
744 created_at: UNIX_EPOCH + Duration::from_secs(details.latest_update_timestamp),
745 status,
746 }
747 })
748 .collect::<Vec<_>>();
749
750 Ok(invoices.first().cloned())
751 }
752
753 async fn list_transactions(
754 &self,
755 start_secs: u64,
756 end_secs: u64,
757 ) -> Result<ListTransactionsResponse, LightningRpcError> {
758 let transactions = self
759 .node
760 .list_payments_with_filter(|details| {
761 !matches!(details.kind, PaymentKind::Onchain { .. })
762 && details.latest_update_timestamp >= start_secs
763 && details.latest_update_timestamp < end_secs
764 })
765 .iter()
766 .map(|details| {
767 let (preimage, payment_hash, payment_kind) =
768 get_preimage_and_payment_hash(&details.kind);
769 let direction = match details.direction {
770 PaymentDirection::Outbound => {
771 fedimint_gateway_common::PaymentDirection::Outbound
772 }
773 PaymentDirection::Inbound => fedimint_gateway_common::PaymentDirection::Inbound,
774 };
775 let status = match details.status {
776 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
777 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
778 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
779 };
780 fedimint_gateway_common::PaymentDetails {
781 payment_hash,
782 preimage: preimage.map(|p| p.to_string()),
783 payment_kind,
784 amount: Amount::from_msats(
785 details
786 .amount_msat
787 .expect("amountless invoices are not supported"),
788 ),
789 direction,
790 status,
791 timestamp_secs: details.latest_update_timestamp,
792 }
793 })
794 .collect::<Vec<_>>();
795 Ok(ListTransactionsResponse { transactions })
796 }
797
798 fn create_offer(
799 &self,
800 amount: Option<Amount>,
801 description: Option<String>,
802 expiry_secs: Option<u32>,
803 quantity: Option<u64>,
804 ) -> Result<String, LightningRpcError> {
805 let description = description.unwrap_or_default();
806 let offer = if let Some(amount) = amount {
807 self.node
808 .bolt12_payment()
809 .receive(amount.msats, &description, expiry_secs, quantity)
810 .map_err(|err| LightningRpcError::Bolt12Error {
811 failure_reason: err.to_string(),
812 })?
813 } else {
814 self.node
815 .bolt12_payment()
816 .receive_variable_amount(&description, expiry_secs)
817 .map_err(|err| LightningRpcError::Bolt12Error {
818 failure_reason: err.to_string(),
819 })?
820 };
821
822 Ok(offer.to_string())
823 }
824
825 async fn pay_offer(
826 &self,
827 offer: String,
828 quantity: Option<u64>,
829 amount: Option<Amount>,
830 payer_note: Option<String>,
831 ) -> Result<Preimage, LightningRpcError> {
832 let offer = Offer::from_str(&offer).map_err(|_| LightningRpcError::Bolt12Error {
833 failure_reason: "Failed to parse Bolt12 Offer".to_string(),
834 })?;
835
836 let _offer_lock_guard = self
837 .outbound_offer_lock_pool
838 .blocking_lock(LdkOfferId(offer.id()));
839
840 let payment_id = if let Some(amount) = amount {
841 self.node
842 .bolt12_payment()
843 .send_using_amount(&offer, amount.msats, quantity, payer_note)
844 .map_err(|err| LightningRpcError::Bolt12Error {
845 failure_reason: err.to_string(),
846 })?
847 } else {
848 self.node
849 .bolt12_payment()
850 .send(&offer, quantity, payer_note)
851 .map_err(|err| LightningRpcError::Bolt12Error {
852 failure_reason: err.to_string(),
853 })?
854 };
855
856 loop {
857 if let Some(payment_details) = self.node.payment(&payment_id) {
858 match payment_details.status {
859 PaymentStatus::Pending => {}
860 PaymentStatus::Succeeded => match payment_details.kind {
861 PaymentKind::Bolt12Offer {
862 preimage: Some(preimage),
863 ..
864 } => {
865 info!(target: LOG_LIGHTNING, offer = %offer, payment_id = %payment_id, preimage = %preimage, "Successfully paid offer");
866 return Ok(Preimage(preimage.0));
867 }
868 _ => {
869 return Err(LightningRpcError::FailedPayment {
870 failure_reason: "Unexpected payment kind".to_string(),
871 });
872 }
873 },
874 PaymentStatus::Failed => {
875 return Err(LightningRpcError::FailedPayment {
876 failure_reason: "Bolt12 payment failed".to_string(),
877 });
878 }
879 }
880 }
881 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
882 }
883 }
884
885 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
886 block_in_place(|| {
887 let _ = self.node.sync_wallets();
888 });
889 Ok(())
890 }
891}
892
893fn get_preimage_and_payment_hash(
896 kind: &PaymentKind,
897) -> (
898 Option<Preimage>,
899 Option<sha256::Hash>,
900 fedimint_gateway_common::PaymentKind,
901) {
902 match kind {
903 PaymentKind::Bolt11 {
904 hash,
905 preimage,
906 secret: _,
907 } => (
908 preimage.map(|p| Preimage(p.0)),
909 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
910 fedimint_gateway_common::PaymentKind::Bolt11,
911 ),
912 PaymentKind::Bolt11Jit {
913 hash,
914 preimage,
915 secret: _,
916 lsp_fee_limits: _,
917 ..
918 } => (
919 preimage.map(|p| Preimage(p.0)),
920 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
921 fedimint_gateway_common::PaymentKind::Bolt11,
922 ),
923 PaymentKind::Bolt12Offer {
924 hash,
925 preimage,
926 secret: _,
927 offer_id: _,
928 payer_note: _,
929 quantity: _,
930 } => (
931 preimage.map(|p| Preimage(p.0)),
932 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
933 fedimint_gateway_common::PaymentKind::Bolt12Offer,
934 ),
935 PaymentKind::Bolt12Refund {
936 hash,
937 preimage,
938 secret: _,
939 payer_note: _,
940 quantity: _,
941 } => (
942 preimage.map(|p| Preimage(p.0)),
943 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
944 fedimint_gateway_common::PaymentKind::Bolt12Refund,
945 ),
946 PaymentKind::Spontaneous { hash, preimage } => (
947 preimage.map(|p| Preimage(p.0)),
948 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
949 fedimint_gateway_common::PaymentKind::Bolt11,
950 ),
951 PaymentKind::Onchain { .. } => (None, None, fedimint_gateway_common::PaymentKind::Onchain),
952 }
953}
954
955fn get_esplora_url(server_url: SafeUrl) -> anyhow::Result<String> {
963 let host = server_url
965 .host_str()
966 .ok_or(anyhow::anyhow!("Missing esplora host"))?;
967 let server_url = if let Some(port) = server_url.port() {
968 format!("{}://{}:{}", server_url.scheme(), host, port)
969 } else {
970 server_url.to_string()
971 };
972 Ok(server_url)
973}
974
975#[derive(Debug, Clone, Copy, Eq, PartialEq)]
976struct LdkOfferId(OfferId);
977
978impl std::hash::Hash for LdkOfferId {
979 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
980 state.write(&self.0.0);
981 }
982}
983
984#[derive(Debug, Copy, Clone, PartialEq, Eq)]
985pub struct UserChannelId(pub ldk_node::UserChannelId);
986
987impl PartialOrd for UserChannelId {
988 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
989 Some(self.cmp(other))
990 }
991}
992
993impl Ord for UserChannelId {
994 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
995 self.0.0.cmp(&other.0.0)
996 }
997}
998
999#[cfg(test)]
1000mod tests;