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 for channel_details in self.node.list_channels().iter() {
656 let remote_node_alias = {
658 let node_id = NodeId::from_pubkey(&channel_details.counterparty_node_id);
659 network_graph.node(&node_id).and_then(|node_info| {
660 node_info
661 .announcement_info
662 .as_ref()
663 .and_then(|announcement| {
664 let alias = announcement.alias().to_string();
665 if alias.is_empty() { None } else { Some(alias) }
666 })
667 })
668 };
669
670 channels.push(ChannelInfo {
671 remote_pubkey: channel_details.counterparty_node_id,
672 channel_size_sats: channel_details.channel_value_sats,
673 outbound_liquidity_sats: channel_details.outbound_capacity_msat / 1000,
674 inbound_liquidity_sats: channel_details.inbound_capacity_msat / 1000,
675 is_active: channel_details.is_usable,
676 funding_outpoint: channel_details.funding_txo,
677 remote_node_alias,
678 });
679 }
680
681 Ok(ListChannelsResponse { channels })
682 }
683
684 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
685 let balances = self.node.list_balances();
686 let channel_lists = self
687 .node
688 .list_channels()
689 .into_iter()
690 .filter(|chan| chan.is_usable)
691 .collect::<Vec<_>>();
692 let total_inbound_liquidity_balance_msat: u64 = channel_lists
694 .iter()
695 .map(|channel| channel.inbound_capacity_msat)
696 .sum();
697
698 Ok(GetBalancesResponse {
699 onchain_balance_sats: balances.total_onchain_balance_sats,
700 lightning_balance_msats: balances.total_lightning_balance_sats * 1000,
701 inbound_lightning_liquidity_msats: total_inbound_liquidity_balance_msat,
702 })
703 }
704
705 async fn get_invoice(
706 &self,
707 get_invoice_request: GetInvoiceRequest,
708 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
709 let invoices = self
710 .node
711 .list_payments_with_filter(|details| {
712 details.direction == PaymentDirection::Inbound
713 && details.id == PaymentId(get_invoice_request.payment_hash.to_byte_array())
714 && !matches!(details.kind, PaymentKind::Onchain { .. })
715 })
716 .iter()
717 .map(|details| {
718 let (preimage, payment_hash, _) = get_preimage_and_payment_hash(&details.kind);
719 let status = match details.status {
720 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
721 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
722 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
723 };
724 GetInvoiceResponse {
725 preimage: preimage.map(|p| p.to_string()),
726 payment_hash,
727 amount: Amount::from_msats(
728 details
729 .amount_msat
730 .expect("amountless invoices are not supported"),
731 ),
732 created_at: UNIX_EPOCH + Duration::from_secs(details.latest_update_timestamp),
733 status,
734 }
735 })
736 .collect::<Vec<_>>();
737
738 Ok(invoices.first().cloned())
739 }
740
741 async fn list_transactions(
742 &self,
743 start_secs: u64,
744 end_secs: u64,
745 ) -> Result<ListTransactionsResponse, LightningRpcError> {
746 let transactions = self
747 .node
748 .list_payments_with_filter(|details| {
749 !matches!(details.kind, PaymentKind::Onchain { .. })
750 && details.latest_update_timestamp >= start_secs
751 && details.latest_update_timestamp < end_secs
752 })
753 .iter()
754 .map(|details| {
755 let (preimage, payment_hash, payment_kind) =
756 get_preimage_and_payment_hash(&details.kind);
757 let direction = match details.direction {
758 PaymentDirection::Outbound => {
759 fedimint_gateway_common::PaymentDirection::Outbound
760 }
761 PaymentDirection::Inbound => fedimint_gateway_common::PaymentDirection::Inbound,
762 };
763 let status = match details.status {
764 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
765 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
766 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
767 };
768 fedimint_gateway_common::PaymentDetails {
769 payment_hash,
770 preimage: preimage.map(|p| p.to_string()),
771 payment_kind,
772 amount: Amount::from_msats(
773 details
774 .amount_msat
775 .expect("amountless invoices are not supported"),
776 ),
777 direction,
778 status,
779 timestamp_secs: details.latest_update_timestamp,
780 }
781 })
782 .collect::<Vec<_>>();
783 Ok(ListTransactionsResponse { transactions })
784 }
785
786 fn create_offer(
787 &self,
788 amount: Option<Amount>,
789 description: Option<String>,
790 expiry_secs: Option<u32>,
791 quantity: Option<u64>,
792 ) -> Result<String, LightningRpcError> {
793 let description = description.unwrap_or_default();
794 let offer = if let Some(amount) = amount {
795 self.node
796 .bolt12_payment()
797 .receive(amount.msats, &description, expiry_secs, quantity)
798 .map_err(|err| LightningRpcError::Bolt12Error {
799 failure_reason: err.to_string(),
800 })?
801 } else {
802 self.node
803 .bolt12_payment()
804 .receive_variable_amount(&description, expiry_secs)
805 .map_err(|err| LightningRpcError::Bolt12Error {
806 failure_reason: err.to_string(),
807 })?
808 };
809
810 Ok(offer.to_string())
811 }
812
813 async fn pay_offer(
814 &self,
815 offer: String,
816 quantity: Option<u64>,
817 amount: Option<Amount>,
818 payer_note: Option<String>,
819 ) -> Result<Preimage, LightningRpcError> {
820 let offer = Offer::from_str(&offer).map_err(|_| LightningRpcError::Bolt12Error {
821 failure_reason: "Failed to parse Bolt12 Offer".to_string(),
822 })?;
823
824 let _offer_lock_guard = self
825 .outbound_offer_lock_pool
826 .blocking_lock(LdkOfferId(offer.id()));
827
828 let payment_id = if let Some(amount) = amount {
829 self.node
830 .bolt12_payment()
831 .send_using_amount(&offer, amount.msats, quantity, payer_note)
832 .map_err(|err| LightningRpcError::Bolt12Error {
833 failure_reason: err.to_string(),
834 })?
835 } else {
836 self.node
837 .bolt12_payment()
838 .send(&offer, quantity, payer_note)
839 .map_err(|err| LightningRpcError::Bolt12Error {
840 failure_reason: err.to_string(),
841 })?
842 };
843
844 loop {
845 if let Some(payment_details) = self.node.payment(&payment_id) {
846 match payment_details.status {
847 PaymentStatus::Pending => {}
848 PaymentStatus::Succeeded => match payment_details.kind {
849 PaymentKind::Bolt12Offer {
850 preimage: Some(preimage),
851 ..
852 } => {
853 info!(target: LOG_LIGHTNING, offer = %offer, payment_id = %payment_id, preimage = %preimage, "Successfully paid offer");
854 return Ok(Preimage(preimage.0));
855 }
856 _ => {
857 return Err(LightningRpcError::FailedPayment {
858 failure_reason: "Unexpected payment kind".to_string(),
859 });
860 }
861 },
862 PaymentStatus::Failed => {
863 return Err(LightningRpcError::FailedPayment {
864 failure_reason: "Bolt12 payment failed".to_string(),
865 });
866 }
867 }
868 }
869 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
870 }
871 }
872
873 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
874 block_in_place(|| {
875 let _ = self.node.sync_wallets();
876 });
877 Ok(())
878 }
879}
880
881fn get_preimage_and_payment_hash(
884 kind: &PaymentKind,
885) -> (
886 Option<Preimage>,
887 Option<sha256::Hash>,
888 fedimint_gateway_common::PaymentKind,
889) {
890 match kind {
891 PaymentKind::Bolt11 {
892 hash,
893 preimage,
894 secret: _,
895 } => (
896 preimage.map(|p| Preimage(p.0)),
897 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
898 fedimint_gateway_common::PaymentKind::Bolt11,
899 ),
900 PaymentKind::Bolt11Jit {
901 hash,
902 preimage,
903 secret: _,
904 lsp_fee_limits: _,
905 ..
906 } => (
907 preimage.map(|p| Preimage(p.0)),
908 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
909 fedimint_gateway_common::PaymentKind::Bolt11,
910 ),
911 PaymentKind::Bolt12Offer {
912 hash,
913 preimage,
914 secret: _,
915 offer_id: _,
916 payer_note: _,
917 quantity: _,
918 } => (
919 preimage.map(|p| Preimage(p.0)),
920 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
921 fedimint_gateway_common::PaymentKind::Bolt12Offer,
922 ),
923 PaymentKind::Bolt12Refund {
924 hash,
925 preimage,
926 secret: _,
927 payer_note: _,
928 quantity: _,
929 } => (
930 preimage.map(|p| Preimage(p.0)),
931 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
932 fedimint_gateway_common::PaymentKind::Bolt12Refund,
933 ),
934 PaymentKind::Spontaneous { hash, preimage } => (
935 preimage.map(|p| Preimage(p.0)),
936 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
937 fedimint_gateway_common::PaymentKind::Bolt11,
938 ),
939 PaymentKind::Onchain { .. } => (None, None, fedimint_gateway_common::PaymentKind::Onchain),
940 }
941}
942
943fn get_esplora_url(server_url: SafeUrl) -> anyhow::Result<String> {
951 let host = server_url
953 .host_str()
954 .ok_or(anyhow::anyhow!("Missing esplora host"))?;
955 let server_url = if let Some(port) = server_url.port() {
956 format!("{}://{}:{}", server_url.scheme(), host, port)
957 } else {
958 server_url.to_string()
959 };
960 Ok(server_url)
961}
962
963#[derive(Debug, Clone, Copy, Eq, PartialEq)]
964struct LdkOfferId(OfferId);
965
966impl std::hash::Hash for LdkOfferId {
967 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
968 state.write(&self.0.0);
969 }
970}
971
972#[derive(Debug, Copy, Clone, PartialEq, Eq)]
973pub struct UserChannelId(pub ldk_node::UserChannelId);
974
975impl PartialOrd for UserChannelId {
976 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
977 Some(self.cmp(other))
978 }
979}
980
981impl Ord for UserChannelId {
982 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
983 self.0.0.cmp(&other.0.0)
984 }
985}
986
987#[cfg(test)]
988mod tests;