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