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