1use std::fmt;
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};
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::{GetInvoiceRequest, GetInvoiceResponse, ListTransactionsResponse};
16use fedimint_ln_common::contracts::Preimage;
17use fedimint_logging::LOG_LIGHTNING;
18use ldk_node::lightning::ln::msgs::SocketAddress;
19use ldk_node::lightning::routing::gossip::NodeAlias;
20use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus, SendingParameters};
21use lightning::ln::channelmanager::PaymentId;
22use lightning::offers::offer::{Offer, OfferId};
23use lightning::types::payment::{PaymentHash, PaymentPreimage};
24use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
25use tokio::sync::mpsc::Sender;
26use tokio_stream::wrappers::ReceiverStream;
27use tracing::{info, warn};
28
29use super::{
30 ChannelInfo, ILnRpcClient, LightningRpcError, ListActiveChannelsResponse, RouteHtlcStream,
31};
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
40#[derive(Clone)]
41pub enum GatewayLdkChainSourceConfig {
42 Bitcoind { server_url: SafeUrl },
43 Esplora { server_url: SafeUrl },
44}
45
46impl fmt::Display for GatewayLdkChainSourceConfig {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 GatewayLdkChainSourceConfig::Bitcoind { server_url } => {
50 write!(f, "Bitcoind source with URL: {}", server_url)
51 }
52 GatewayLdkChainSourceConfig::Esplora { server_url } => {
53 write!(f, "Esplora source with URL: {}", server_url)
54 }
55 }
56 }
57}
58
59pub struct GatewayLdkClient {
60 node: Arc<ldk_node::Node>,
62
63 task_group: TaskGroup,
64
65 htlc_stream_receiver_or: Option<tokio::sync::mpsc::Receiver<InterceptPaymentRequest>>,
68
69 outbound_lightning_payment_lock_pool: lockable::LockPool<PaymentId>,
73
74 outbound_offer_lock_pool: lockable::LockPool<LdkOfferId>,
79}
80
81impl std::fmt::Debug for GatewayLdkClient {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("GatewayLdkClient").finish_non_exhaustive()
84 }
85}
86
87impl GatewayLdkClient {
88 pub fn new(
93 data_dir: &Path,
94 chain_source_config: GatewayLdkChainSourceConfig,
95 network: Network,
96 lightning_port: u16,
97 alias: String,
98 mnemonic: Mnemonic,
99 runtime: Arc<tokio::runtime::Runtime>,
100 ) -> anyhow::Result<Self> {
101 let mut bytes = [0u8; 32];
102 let alias = if alias.is_empty() {
103 "LDK Gateway".to_string()
104 } else {
105 alias
106 };
107 let alias_bytes = alias.as_bytes();
108 let truncated = &alias_bytes[..alias_bytes.len().min(32)];
109 bytes[..truncated.len()].copy_from_slice(truncated);
110 let node_alias = Some(NodeAlias(bytes));
111
112 let mut node_builder = ldk_node::Builder::from_config(ldk_node::config::Config {
113 network,
114 listening_addresses: Some(vec![SocketAddress::TcpIpV4 {
115 addr: [0, 0, 0, 0],
116 port: lightning_port,
117 }]),
118 node_alias,
119 ..Default::default()
120 });
121
122 node_builder.set_entropy_bip39_mnemonic(mnemonic, None);
123
124 match chain_source_config.clone() {
125 GatewayLdkChainSourceConfig::Bitcoind { server_url } => {
126 node_builder.set_chain_source_bitcoind_rpc(
127 server_url
128 .host_str()
129 .expect("Could not retrieve host from bitcoind RPC url")
130 .to_string(),
131 server_url
132 .port()
133 .expect("Could not retrieve port from bitcoind RPC url"),
134 server_url.username().to_string(),
135 server_url.password().unwrap_or_default().to_string(),
136 );
137 }
138 GatewayLdkChainSourceConfig::Esplora { server_url } => {
139 node_builder.set_chain_source_esplora(get_esplora_url(server_url)?, None);
140 }
141 };
142 let Some(data_dir_str) = data_dir.to_str() else {
143 return Err(anyhow::anyhow!("Invalid data dir path"));
144 };
145 node_builder.set_storage_dir_path(data_dir_str.to_string());
146
147 info!(chain_source = %chain_source_config, data_dir = %data_dir_str, alias = %alias, "Starting LDK Node...");
148 let node = Arc::new(node_builder.build()?);
149 node.start_with_runtime(runtime).map_err(|err| {
150 crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to start LDK Node");
151 LightningRpcError::FailedToConnect
152 })?;
153
154 let (htlc_stream_sender, htlc_stream_receiver) = tokio::sync::mpsc::channel(1024);
155 let task_group = TaskGroup::new();
156
157 let node_clone = node.clone();
158 task_group.spawn("ldk lightning node event handler", |handle| async move {
159 loop {
160 Self::handle_next_event(&node_clone, &htlc_stream_sender, &handle).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 })
172 }
173
174 async fn handle_next_event(
175 node: &ldk_node::Node,
176 htlc_stream_sender: &Sender<InterceptPaymentRequest>,
177 handle: &TaskHandle,
178 ) {
179 let event = tokio::select! {
183 event = node.next_event_async() => {
184 event
185 }
186 () = handle.make_shutdown_rx() => {
187 return;
188 }
189 };
190
191 if let ldk_node::Event::PaymentClaimable {
192 payment_id: _,
193 payment_hash,
194 claimable_amount_msat,
195 claim_deadline,
196 ..
197 } = event
198 {
199 if let Err(err) = htlc_stream_sender
200 .send(InterceptPaymentRequest {
201 payment_hash: Hash::from_slice(&payment_hash.0).expect("Failed to create Hash"),
202 amount_msat: claimable_amount_msat,
203 expiry: claim_deadline.unwrap_or_default(),
204 short_channel_id: None,
205 incoming_chan_id: 0,
206 htlc_id: 0,
207 })
208 .await
209 {
210 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed send InterceptHtlcRequest to stream");
211 }
212 }
213
214 if let Err(err) = node.event_handled() {
217 warn!(err = %err.fmt_compact(), "LDK could not mark event handled");
218 }
219 }
220}
221
222impl Drop for GatewayLdkClient {
223 fn drop(&mut self) {
224 self.task_group.shutdown();
225
226 info!(target: LOG_LIGHTNING, "Stopping LDK Node...");
227 match self.node.stop() {
228 Err(err) => {
229 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to stop LDK Node");
230 }
231 _ => {
232 info!(target: LOG_LIGHTNING, "LDK Node stopped.");
233 }
234 }
235 }
236}
237
238#[async_trait]
239impl ILnRpcClient for GatewayLdkClient {
240 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
241 if is_env_var_set("FM_IN_DEVIMINT") {
244 block_in_place(|| {
245 let _ = self.node.sync_wallets();
246 });
247 }
248 let node_status = self.node.status();
249
250 let ldk_block_height = node_status.current_best_block.height;
251 let synced_to_chain = node_status.latest_onchain_wallet_sync_timestamp.is_some();
252
253 Ok(GetNodeInfoResponse {
254 pub_key: self.node.node_id(),
255 alias: match self.node.node_alias() {
256 Some(alias) => alias.to_string(),
257 None => format!("LDK Fedimint Gateway Node {}", self.node.node_id()),
258 },
259 network: self.node.config().network.to_string(),
260 block_height: ldk_block_height,
261 synced_to_chain,
262 })
263 }
264
265 async fn routehints(
266 &self,
267 _num_route_hints: usize,
268 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
269 Ok(GetRouteHintsResponse {
275 route_hints: vec![],
276 })
277 }
278
279 async fn pay(
280 &self,
281 invoice: Bolt11Invoice,
282 max_delay: u64,
283 max_fee: Amount,
284 ) -> Result<PayInvoiceResponse, LightningRpcError> {
285 let payment_id = PaymentId(*invoice.payment_hash().as_byte_array());
286
287 let _payment_lock_guard = self
293 .outbound_lightning_payment_lock_pool
294 .async_lock(payment_id)
295 .await;
296
297 if self.node.payment(&payment_id).is_none() {
304 assert_eq!(
305 self.node
306 .bolt11_payment()
307 .send(
308 &invoice,
309 Some(SendingParameters {
310 max_total_routing_fee_msat: Some(Some(max_fee.msats)),
311 max_total_cltv_expiry_delta: Some(max_delay as u32),
312 max_path_count: None,
313 max_channel_saturation_power_of_half: None,
314 }),
315 )
316 .map_err(|e| LightningRpcError::FailedPayment {
319 failure_reason: format!("LDK payment failed to initialize: {e:?}"),
320 })?,
321 payment_id
322 );
323 }
324
325 loop {
330 if let Some(payment_details) = self.node.payment(&payment_id) {
331 match payment_details.status {
332 PaymentStatus::Pending => {}
333 PaymentStatus::Succeeded => {
334 if let PaymentKind::Bolt11 {
335 preimage: Some(preimage),
336 ..
337 } = payment_details.kind
338 {
339 return Ok(PayInvoiceResponse {
340 preimage: Preimage(preimage.0),
341 });
342 }
343 }
344 PaymentStatus::Failed => {
345 return Err(LightningRpcError::FailedPayment {
346 failure_reason: "LDK payment failed".to_string(),
347 });
348 }
349 }
350 }
351 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
352 }
353 }
354
355 async fn route_htlcs<'a>(
356 mut self: Box<Self>,
357 _task_group: &TaskGroup,
358 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
359 let route_htlc_stream = match self.htlc_stream_receiver_or.take() {
360 Some(stream) => Ok(Box::pin(ReceiverStream::new(stream))),
361 None => Err(LightningRpcError::FailedToRouteHtlcs {
362 failure_reason:
363 "Stream does not exist. Likely was already taken by calling `route_htlcs()`."
364 .to_string(),
365 }),
366 }?;
367
368 Ok((route_htlc_stream, Arc::new(*self)))
369 }
370
371 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
372 let InterceptPaymentResponse {
373 action,
374 payment_hash,
375 incoming_chan_id: _,
376 htlc_id: _,
377 } = htlc;
378
379 let ph = PaymentHash(*payment_hash.clone().as_byte_array());
380
381 let claimable_amount_msat = 999_999_999_999_999;
387
388 let ph_hex_str = hex::encode(payment_hash);
389
390 if let PaymentAction::Settle(preimage) = action {
391 self.node
392 .bolt11_payment()
393 .claim_for_hash(ph, claimable_amount_msat, PaymentPreimage(preimage.0))
394 .map_err(|_| LightningRpcError::FailedToCompleteHtlc {
395 failure_reason: format!("Failed to claim LDK payment with hash {ph_hex_str}"),
396 })?;
397 } else {
398 warn!(target: LOG_LIGHTNING, payment_hash = %ph_hex_str, "Unwinding payment because the action was not `Settle`");
399 self.node.bolt11_payment().fail_for_hash(ph).map_err(|_| {
400 LightningRpcError::FailedToCompleteHtlc {
401 failure_reason: format!("Failed to unwind LDK payment with hash {ph_hex_str}"),
402 }
403 })?;
404 }
405
406 return Ok(());
407 }
408
409 async fn create_invoice(
410 &self,
411 create_invoice_request: CreateInvoiceRequest,
412 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
413 let payment_hash_or = if let Some(payment_hash) = create_invoice_request.payment_hash {
414 let ph = PaymentHash(*payment_hash.as_byte_array());
415 Some(ph)
416 } else {
417 None
418 };
419
420 let description = match create_invoice_request.description {
421 Some(InvoiceDescription::Direct(desc)) => {
422 Bolt11InvoiceDescription::Direct(Description::new(desc).map_err(|_| {
423 LightningRpcError::FailedToGetInvoice {
424 failure_reason: "Invalid description".to_string(),
425 }
426 })?)
427 }
428 Some(InvoiceDescription::Hash(hash)) => {
429 Bolt11InvoiceDescription::Hash(lightning_invoice::Sha256(hash))
430 }
431 None => Bolt11InvoiceDescription::Direct(Description::empty()),
432 };
433
434 let invoice = match payment_hash_or {
435 Some(payment_hash) => self.node.bolt11_payment().receive_for_hash(
436 create_invoice_request.amount_msat,
437 &description,
438 create_invoice_request.expiry_secs,
439 payment_hash,
440 ),
441 None => self.node.bolt11_payment().receive(
442 create_invoice_request.amount_msat,
443 &description,
444 create_invoice_request.expiry_secs,
445 ),
446 }
447 .map_err(|e| LightningRpcError::FailedToGetInvoice {
448 failure_reason: e.to_string(),
449 })?;
450
451 Ok(CreateInvoiceResponse {
452 invoice: invoice.to_string(),
453 })
454 }
455
456 async fn get_ln_onchain_address(
457 &self,
458 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
459 self.node
460 .onchain_payment()
461 .new_address()
462 .map(|address| GetLnOnchainAddressResponse {
463 address: address.to_string(),
464 })
465 .map_err(|e| LightningRpcError::FailedToGetLnOnchainAddress {
466 failure_reason: e.to_string(),
467 })
468 }
469
470 async fn send_onchain(
471 &self,
472 SendOnchainRequest {
473 address,
474 amount,
475 fee_rate_sats_per_vbyte,
476 }: SendOnchainRequest,
477 ) -> Result<SendOnchainResponse, LightningRpcError> {
478 let onchain = self.node.onchain_payment();
479
480 let retain_reserves = false;
481 let txid = match amount {
482 BitcoinAmountOrAll::All => onchain.send_all_to_address(
483 &address.assume_checked(),
484 retain_reserves,
485 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
486 ),
487 BitcoinAmountOrAll::Amount(amount_sats) => onchain.send_to_address(
488 &address.assume_checked(),
489 amount_sats.to_sat(),
490 FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
491 ),
492 }
493 .map_err(|e| LightningRpcError::FailedToWithdrawOnchain {
494 failure_reason: e.to_string(),
495 })?;
496
497 Ok(SendOnchainResponse {
498 txid: txid.to_string(),
499 })
500 }
501
502 async fn open_channel(
503 &self,
504 OpenChannelRequest {
505 pubkey,
506 host,
507 channel_size_sats,
508 push_amount_sats,
509 }: OpenChannelRequest,
510 ) -> Result<OpenChannelResponse, LightningRpcError> {
511 let push_amount_msats_or = if push_amount_sats == 0 {
512 None
513 } else {
514 Some(push_amount_sats * 1000)
515 };
516
517 let user_channel_id = self
518 .node
519 .open_announced_channel(
520 pubkey,
521 SocketAddress::from_str(&host).map_err(|e| {
522 LightningRpcError::FailedToConnectToPeer {
523 failure_reason: e.to_string(),
524 }
525 })?,
526 channel_size_sats,
527 push_amount_msats_or,
528 None,
529 )
530 .map_err(|e| LightningRpcError::FailedToOpenChannel {
531 failure_reason: e.to_string(),
532 })?;
533
534 for _ in 0..10 {
536 let funding_txid_or = self
537 .node
538 .list_channels()
539 .iter()
540 .find(|channel| channel.user_channel_id == user_channel_id)
541 .and_then(|channel| channel.funding_txo)
542 .map(|funding_txo| funding_txo.txid);
543
544 if let Some(funding_txid) = funding_txid_or {
545 return Ok(OpenChannelResponse {
546 funding_txid: funding_txid.to_string(),
547 });
548 }
549
550 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
551 }
552
553 Err(LightningRpcError::FailedToOpenChannel {
554 failure_reason: "Channel could not be opened".to_string(),
555 })
556 }
557
558 async fn close_channels_with_peer(
559 &self,
560 CloseChannelsWithPeerRequest { pubkey }: CloseChannelsWithPeerRequest,
561 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
562 let mut num_channels_closed = 0;
563
564 for channel_with_peer in self
565 .node
566 .list_channels()
567 .iter()
568 .filter(|channel| channel.counterparty_node_id == pubkey)
569 {
570 if self
571 .node
572 .close_channel(&channel_with_peer.user_channel_id, pubkey)
573 .is_ok()
574 {
575 num_channels_closed += 1;
576 }
577 }
578
579 Ok(CloseChannelsWithPeerResponse {
580 num_channels_closed,
581 })
582 }
583
584 async fn list_active_channels(&self) -> Result<ListActiveChannelsResponse, LightningRpcError> {
585 let mut channels = Vec::new();
586
587 for channel_details in self
588 .node
589 .list_channels()
590 .iter()
591 .filter(|channel| channel.is_usable)
592 {
593 channels.push(ChannelInfo {
594 remote_pubkey: channel_details.counterparty_node_id,
595 channel_size_sats: channel_details.channel_value_sats,
596 outbound_liquidity_sats: channel_details.outbound_capacity_msat / 1000,
597 inbound_liquidity_sats: channel_details.inbound_capacity_msat / 1000,
598 });
599 }
600
601 Ok(ListActiveChannelsResponse { channels })
602 }
603
604 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
605 let balances = self.node.list_balances();
606 let channel_lists = self
607 .node
608 .list_channels()
609 .into_iter()
610 .filter(|chan| chan.is_usable)
611 .collect::<Vec<_>>();
612 let total_inbound_liquidity_balance_msat: u64 = channel_lists
614 .iter()
615 .map(|channel| channel.inbound_capacity_msat)
616 .sum();
617
618 Ok(GetBalancesResponse {
619 onchain_balance_sats: balances.total_onchain_balance_sats,
620 lightning_balance_msats: balances.total_lightning_balance_sats * 1000,
621 inbound_lightning_liquidity_msats: total_inbound_liquidity_balance_msat,
622 })
623 }
624
625 async fn get_invoice(
626 &self,
627 get_invoice_request: GetInvoiceRequest,
628 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
629 let invoices = self
630 .node
631 .list_payments_with_filter(|details| {
632 details.direction == PaymentDirection::Inbound
633 && details.id == PaymentId(get_invoice_request.payment_hash.to_byte_array())
634 && !matches!(details.kind, PaymentKind::Onchain { .. })
635 })
636 .iter()
637 .map(|details| {
638 let (preimage, payment_hash, _) = get_preimage_and_payment_hash(&details.kind);
639 let status = match details.status {
640 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
641 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
642 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
643 };
644 GetInvoiceResponse {
645 preimage: preimage.map(|p| p.to_string()),
646 payment_hash,
647 amount: Amount::from_msats(
648 details
649 .amount_msat
650 .expect("amountless invoices are not supported"),
651 ),
652 created_at: UNIX_EPOCH + Duration::from_secs(details.latest_update_timestamp),
653 status,
654 }
655 })
656 .collect::<Vec<_>>();
657
658 Ok(invoices.first().cloned())
659 }
660
661 async fn list_transactions(
662 &self,
663 start_secs: u64,
664 end_secs: u64,
665 ) -> Result<ListTransactionsResponse, LightningRpcError> {
666 let transactions = self
667 .node
668 .list_payments_with_filter(|details| {
669 !matches!(details.kind, PaymentKind::Onchain { .. })
670 && details.latest_update_timestamp >= start_secs
671 && details.latest_update_timestamp < end_secs
672 })
673 .iter()
674 .map(|details| {
675 let (preimage, payment_hash, payment_kind) =
676 get_preimage_and_payment_hash(&details.kind);
677 let direction = match details.direction {
678 PaymentDirection::Outbound => {
679 fedimint_gateway_common::PaymentDirection::Outbound
680 }
681 PaymentDirection::Inbound => fedimint_gateway_common::PaymentDirection::Inbound,
682 };
683 let status = match details.status {
684 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
685 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
686 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
687 };
688 fedimint_gateway_common::PaymentDetails {
689 payment_hash,
690 preimage: preimage.map(|p| p.to_string()),
691 payment_kind,
692 amount: Amount::from_msats(
693 details
694 .amount_msat
695 .expect("amountless invoices are not supported"),
696 ),
697 direction,
698 status,
699 timestamp_secs: details.latest_update_timestamp,
700 }
701 })
702 .collect::<Vec<_>>();
703 Ok(ListTransactionsResponse { transactions })
704 }
705
706 fn create_offer(
707 &self,
708 amount: Option<Amount>,
709 description: Option<String>,
710 expiry_secs: Option<u32>,
711 quantity: Option<u64>,
712 ) -> Result<String, LightningRpcError> {
713 let description = description.unwrap_or_default();
714 let offer = if let Some(amount) = amount {
715 self.node
716 .bolt12_payment()
717 .receive(amount.msats, &description, expiry_secs, quantity)
718 .map_err(|err| LightningRpcError::Bolt12Error {
719 failure_reason: err.to_string(),
720 })?
721 } else {
722 self.node
723 .bolt12_payment()
724 .receive_variable_amount(&description, expiry_secs)
725 .map_err(|err| LightningRpcError::Bolt12Error {
726 failure_reason: err.to_string(),
727 })?
728 };
729
730 Ok(offer.to_string())
731 }
732
733 async fn pay_offer(
734 &self,
735 offer: String,
736 quantity: Option<u64>,
737 amount: Option<Amount>,
738 payer_note: Option<String>,
739 ) -> Result<Preimage, LightningRpcError> {
740 let offer = Offer::from_str(&offer).map_err(|_| LightningRpcError::Bolt12Error {
741 failure_reason: "Failed to parse Bolt12 Offer".to_string(),
742 })?;
743
744 let _offer_lock_guard = self
745 .outbound_offer_lock_pool
746 .blocking_lock(LdkOfferId(offer.id()));
747
748 let payment_id = if let Some(amount) = amount {
749 self.node
750 .bolt12_payment()
751 .send_using_amount(&offer, amount.msats, quantity, payer_note)
752 .map_err(|err| LightningRpcError::Bolt12Error {
753 failure_reason: err.to_string(),
754 })?
755 } else {
756 self.node
757 .bolt12_payment()
758 .send(&offer, quantity, payer_note)
759 .map_err(|err| LightningRpcError::Bolt12Error {
760 failure_reason: err.to_string(),
761 })?
762 };
763
764 loop {
765 if let Some(payment_details) = self.node.payment(&payment_id) {
766 match payment_details.status {
767 PaymentStatus::Pending => {}
768 PaymentStatus::Succeeded => match payment_details.kind {
769 PaymentKind::Bolt12Offer {
770 preimage: Some(preimage),
771 ..
772 } => {
773 info!(target: LOG_LIGHTNING, offer = %offer, payment_id = %payment_id, preimage = %preimage, "Successfully paid offer");
774 return Ok(Preimage(preimage.0));
775 }
776 _ => {
777 return Err(LightningRpcError::FailedPayment {
778 failure_reason: "Unexpected payment kind".to_string(),
779 });
780 }
781 },
782 PaymentStatus::Failed => {
783 return Err(LightningRpcError::FailedPayment {
784 failure_reason: "Bolt12 payment failed".to_string(),
785 });
786 }
787 }
788 }
789 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
790 }
791 }
792}
793
794fn get_preimage_and_payment_hash(
797 kind: &PaymentKind,
798) -> (
799 Option<Preimage>,
800 Option<sha256::Hash>,
801 fedimint_gateway_common::PaymentKind,
802) {
803 match kind {
804 PaymentKind::Bolt11 {
805 hash,
806 preimage,
807 secret: _,
808 } => (
809 preimage.map(|p| Preimage(p.0)),
810 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
811 fedimint_gateway_common::PaymentKind::Bolt11,
812 ),
813 PaymentKind::Bolt11Jit {
814 hash,
815 preimage,
816 secret: _,
817 lsp_fee_limits: _,
818 ..
819 } => (
820 preimage.map(|p| Preimage(p.0)),
821 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
822 fedimint_gateway_common::PaymentKind::Bolt11,
823 ),
824 PaymentKind::Bolt12Offer {
825 hash,
826 preimage,
827 secret: _,
828 offer_id: _,
829 payer_note: _,
830 quantity: _,
831 } => (
832 preimage.map(|p| Preimage(p.0)),
833 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
834 fedimint_gateway_common::PaymentKind::Bolt12Offer,
835 ),
836 PaymentKind::Bolt12Refund {
837 hash,
838 preimage,
839 secret: _,
840 payer_note: _,
841 quantity: _,
842 } => (
843 preimage.map(|p| Preimage(p.0)),
844 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
845 fedimint_gateway_common::PaymentKind::Bolt12Refund,
846 ),
847 PaymentKind::Spontaneous { hash, preimage } => (
848 preimage.map(|p| Preimage(p.0)),
849 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
850 fedimint_gateway_common::PaymentKind::Bolt11,
851 ),
852 PaymentKind::Onchain { .. } => (None, None, fedimint_gateway_common::PaymentKind::Onchain),
853 }
854}
855
856fn get_esplora_url(server_url: SafeUrl) -> anyhow::Result<String> {
864 let host = server_url
866 .host_str()
867 .ok_or(anyhow::anyhow!("Missing esplora host"))?;
868 let server_url = if let Some(port) = server_url.port() {
869 format!("{}://{}:{}", server_url.scheme(), host, port)
870 } else {
871 server_url.to_string()
872 };
873 Ok(server_url)
874}
875
876#[derive(Debug, Clone, Copy, Eq, PartialEq)]
877struct LdkOfferId(OfferId);
878
879impl std::hash::Hash for LdkOfferId {
880 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
881 state.write(&self.0.0);
882 }
883}
884
885#[cfg(test)]
886mod tests;