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