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