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 { pubkey, force }: CloseChannelsWithPeerRequest,
579 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
580 let mut num_channels_closed = 0;
581
582 info!(%pubkey, "Closing all channels with peer");
583 for channel_with_peer in self
584 .node
585 .list_channels()
586 .iter()
587 .filter(|channel| channel.counterparty_node_id == pubkey)
588 {
589 if force {
590 match self.node.force_close_channel(
591 &channel_with_peer.user_channel_id,
592 pubkey,
593 Some("User initiated force close".to_string()),
594 ) {
595 Ok(()) => num_channels_closed += 1,
596 Err(err) => {
597 error!(%pubkey, err = %err.fmt_compact(), "Could not force close channel");
598 }
599 }
600 } else {
601 match self
602 .node
603 .close_channel(&channel_with_peer.user_channel_id, pubkey)
604 {
605 Ok(()) => {
606 num_channels_closed += 1;
607 }
608 Err(err) => {
609 error!(%pubkey, err = %err.fmt_compact(), "Could not close channel");
610 }
611 }
612 }
613 }
614
615 Ok(CloseChannelsWithPeerResponse {
616 num_channels_closed,
617 })
618 }
619
620 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
621 let mut channels = Vec::new();
622
623 for channel_details in self.node.list_channels().iter() {
624 channels.push(ChannelInfo {
625 remote_pubkey: channel_details.counterparty_node_id,
626 channel_size_sats: channel_details.channel_value_sats,
627 outbound_liquidity_sats: channel_details.outbound_capacity_msat / 1000,
628 inbound_liquidity_sats: channel_details.inbound_capacity_msat / 1000,
629 is_active: channel_details.is_usable,
630 });
631 }
632
633 Ok(ListChannelsResponse { channels })
634 }
635
636 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
637 let balances = self.node.list_balances();
638 let channel_lists = self
639 .node
640 .list_channels()
641 .into_iter()
642 .filter(|chan| chan.is_usable)
643 .collect::<Vec<_>>();
644 let total_inbound_liquidity_balance_msat: u64 = channel_lists
646 .iter()
647 .map(|channel| channel.inbound_capacity_msat)
648 .sum();
649
650 Ok(GetBalancesResponse {
651 onchain_balance_sats: balances.total_onchain_balance_sats,
652 lightning_balance_msats: balances.total_lightning_balance_sats * 1000,
653 inbound_lightning_liquidity_msats: total_inbound_liquidity_balance_msat,
654 })
655 }
656
657 async fn get_invoice(
658 &self,
659 get_invoice_request: GetInvoiceRequest,
660 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
661 let invoices = self
662 .node
663 .list_payments_with_filter(|details| {
664 details.direction == PaymentDirection::Inbound
665 && details.id == PaymentId(get_invoice_request.payment_hash.to_byte_array())
666 && !matches!(details.kind, PaymentKind::Onchain { .. })
667 })
668 .iter()
669 .map(|details| {
670 let (preimage, payment_hash, _) = get_preimage_and_payment_hash(&details.kind);
671 let status = match details.status {
672 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
673 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
674 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
675 };
676 GetInvoiceResponse {
677 preimage: preimage.map(|p| p.to_string()),
678 payment_hash,
679 amount: Amount::from_msats(
680 details
681 .amount_msat
682 .expect("amountless invoices are not supported"),
683 ),
684 created_at: UNIX_EPOCH + Duration::from_secs(details.latest_update_timestamp),
685 status,
686 }
687 })
688 .collect::<Vec<_>>();
689
690 Ok(invoices.first().cloned())
691 }
692
693 async fn list_transactions(
694 &self,
695 start_secs: u64,
696 end_secs: u64,
697 ) -> Result<ListTransactionsResponse, LightningRpcError> {
698 let transactions = self
699 .node
700 .list_payments_with_filter(|details| {
701 !matches!(details.kind, PaymentKind::Onchain { .. })
702 && details.latest_update_timestamp >= start_secs
703 && details.latest_update_timestamp < end_secs
704 })
705 .iter()
706 .map(|details| {
707 let (preimage, payment_hash, payment_kind) =
708 get_preimage_and_payment_hash(&details.kind);
709 let direction = match details.direction {
710 PaymentDirection::Outbound => {
711 fedimint_gateway_common::PaymentDirection::Outbound
712 }
713 PaymentDirection::Inbound => fedimint_gateway_common::PaymentDirection::Inbound,
714 };
715 let status = match details.status {
716 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
717 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
718 PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
719 };
720 fedimint_gateway_common::PaymentDetails {
721 payment_hash,
722 preimage: preimage.map(|p| p.to_string()),
723 payment_kind,
724 amount: Amount::from_msats(
725 details
726 .amount_msat
727 .expect("amountless invoices are not supported"),
728 ),
729 direction,
730 status,
731 timestamp_secs: details.latest_update_timestamp,
732 }
733 })
734 .collect::<Vec<_>>();
735 Ok(ListTransactionsResponse { transactions })
736 }
737
738 fn create_offer(
739 &self,
740 amount: Option<Amount>,
741 description: Option<String>,
742 expiry_secs: Option<u32>,
743 quantity: Option<u64>,
744 ) -> Result<String, LightningRpcError> {
745 let description = description.unwrap_or_default();
746 let offer = if let Some(amount) = amount {
747 self.node
748 .bolt12_payment()
749 .receive(amount.msats, &description, expiry_secs, quantity)
750 .map_err(|err| LightningRpcError::Bolt12Error {
751 failure_reason: err.to_string(),
752 })?
753 } else {
754 self.node
755 .bolt12_payment()
756 .receive_variable_amount(&description, expiry_secs)
757 .map_err(|err| LightningRpcError::Bolt12Error {
758 failure_reason: err.to_string(),
759 })?
760 };
761
762 Ok(offer.to_string())
763 }
764
765 async fn pay_offer(
766 &self,
767 offer: String,
768 quantity: Option<u64>,
769 amount: Option<Amount>,
770 payer_note: Option<String>,
771 ) -> Result<Preimage, LightningRpcError> {
772 let offer = Offer::from_str(&offer).map_err(|_| LightningRpcError::Bolt12Error {
773 failure_reason: "Failed to parse Bolt12 Offer".to_string(),
774 })?;
775
776 let _offer_lock_guard = self
777 .outbound_offer_lock_pool
778 .blocking_lock(LdkOfferId(offer.id()));
779
780 let payment_id = if let Some(amount) = amount {
781 self.node
782 .bolt12_payment()
783 .send_using_amount(&offer, amount.msats, quantity, payer_note)
784 .map_err(|err| LightningRpcError::Bolt12Error {
785 failure_reason: err.to_string(),
786 })?
787 } else {
788 self.node
789 .bolt12_payment()
790 .send(&offer, quantity, payer_note)
791 .map_err(|err| LightningRpcError::Bolt12Error {
792 failure_reason: err.to_string(),
793 })?
794 };
795
796 loop {
797 if let Some(payment_details) = self.node.payment(&payment_id) {
798 match payment_details.status {
799 PaymentStatus::Pending => {}
800 PaymentStatus::Succeeded => match payment_details.kind {
801 PaymentKind::Bolt12Offer {
802 preimage: Some(preimage),
803 ..
804 } => {
805 info!(target: LOG_LIGHTNING, offer = %offer, payment_id = %payment_id, preimage = %preimage, "Successfully paid offer");
806 return Ok(Preimage(preimage.0));
807 }
808 _ => {
809 return Err(LightningRpcError::FailedPayment {
810 failure_reason: "Unexpected payment kind".to_string(),
811 });
812 }
813 },
814 PaymentStatus::Failed => {
815 return Err(LightningRpcError::FailedPayment {
816 failure_reason: "Bolt12 payment failed".to_string(),
817 });
818 }
819 }
820 }
821 fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
822 }
823 }
824}
825
826fn get_preimage_and_payment_hash(
829 kind: &PaymentKind,
830) -> (
831 Option<Preimage>,
832 Option<sha256::Hash>,
833 fedimint_gateway_common::PaymentKind,
834) {
835 match kind {
836 PaymentKind::Bolt11 {
837 hash,
838 preimage,
839 secret: _,
840 } => (
841 preimage.map(|p| Preimage(p.0)),
842 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
843 fedimint_gateway_common::PaymentKind::Bolt11,
844 ),
845 PaymentKind::Bolt11Jit {
846 hash,
847 preimage,
848 secret: _,
849 lsp_fee_limits: _,
850 ..
851 } => (
852 preimage.map(|p| Preimage(p.0)),
853 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
854 fedimint_gateway_common::PaymentKind::Bolt11,
855 ),
856 PaymentKind::Bolt12Offer {
857 hash,
858 preimage,
859 secret: _,
860 offer_id: _,
861 payer_note: _,
862 quantity: _,
863 } => (
864 preimage.map(|p| Preimage(p.0)),
865 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
866 fedimint_gateway_common::PaymentKind::Bolt12Offer,
867 ),
868 PaymentKind::Bolt12Refund {
869 hash,
870 preimage,
871 secret: _,
872 payer_note: _,
873 quantity: _,
874 } => (
875 preimage.map(|p| Preimage(p.0)),
876 hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
877 fedimint_gateway_common::PaymentKind::Bolt12Refund,
878 ),
879 PaymentKind::Spontaneous { hash, preimage } => (
880 preimage.map(|p| Preimage(p.0)),
881 Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
882 fedimint_gateway_common::PaymentKind::Bolt11,
883 ),
884 PaymentKind::Onchain { .. } => (None, None, fedimint_gateway_common::PaymentKind::Onchain),
885 }
886}
887
888fn get_esplora_url(server_url: SafeUrl) -> anyhow::Result<String> {
896 let host = server_url
898 .host_str()
899 .ok_or(anyhow::anyhow!("Missing esplora host"))?;
900 let server_url = if let Some(port) = server_url.port() {
901 format!("{}://{}:{}", server_url.scheme(), host, port)
902 } else {
903 server_url.to_string()
904 };
905 Ok(server_url)
906}
907
908#[derive(Debug, Clone, Copy, Eq, PartialEq)]
909struct LdkOfferId(OfferId);
910
911impl std::hash::Hash for LdkOfferId {
912 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
913 state.write(&self.0.0);
914 }
915}
916
917#[derive(Debug, Copy, Clone, PartialEq, Eq)]
918pub struct UserChannelId(pub ldk_node::UserChannelId);
919
920impl PartialOrd for UserChannelId {
921 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
922 Some(self.cmp(other))
923 }
924}
925
926impl Ord for UserChannelId {
927 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
928 self.0.0.cmp(&other.0.0)
929 }
930}
931
932#[cfg(test)]
933mod tests;