Skip to main content

fedimint_lightning/
lib.rs

1pub mod ldk;
2pub mod lnd;
3pub mod metrics;
4
5use std::fmt::Debug;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use bitcoin::Network;
11use bitcoin::hashes::sha256;
12use fedimint_core::Amount;
13use fedimint_core::encoding::{Decodable, Encodable};
14use fedimint_core::envs::{FM_IN_DEVIMINT_ENV, is_env_var_set};
15use fedimint_core::secp256k1::PublicKey;
16use fedimint_core::task::TaskGroup;
17use fedimint_core::time::now;
18use fedimint_core::util::{FmtCompactResult as _, backoff_util, retry};
19use fedimint_gateway_common::{
20    ChannelInfo, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, ConnectPeerRequest,
21    GetInvoiceRequest, GetInvoiceResponse, LightningInfo, ListTransactionsResponse,
22    OpenChannelRequest, SendOnchainRequest, SetChannelFeesRequest,
23};
24use fedimint_ln_common::PrunedInvoice;
25pub use fedimint_ln_common::contracts::Preimage;
26use fedimint_ln_common::route_hints::RouteHint;
27use fedimint_logging::LOG_LIGHTNING;
28use fedimint_metrics::HistogramExt as _;
29use futures::future::BoxFuture;
30use futures::stream::BoxStream;
31use lightning_invoice::Bolt11Invoice;
32use serde::{Deserialize, Serialize};
33use thiserror::Error;
34use tracing::{info, trace, warn};
35
36pub const MAX_LIGHTNING_RETRIES: u32 = 10;
37
38pub type RouteHtlcStream<'a> = BoxStream<'a, InterceptPaymentRequest>;
39
40/// Returns `true` if the given payment hash corresponds to a HOLD invoice that
41/// the gateway created on behalf of a federation. Used by the LND backend to
42/// ignore unrelated HOLD invoices on a shared LND node, which would otherwise
43/// be mistaken for federation-bound payments and produce invalid responses on
44/// LND's HTLC interceptor wire.
45pub type Lnv2HoldInvoiceFilter =
46    Arc<dyn Fn(sha256::Hash) -> BoxFuture<'static, bool> + Send + Sync + 'static>;
47
48#[derive(
49    Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
50)]
51pub enum LightningRpcError {
52    #[error("Failed to connect to Lightning node")]
53    FailedToConnect,
54    #[error("Failed to retrieve node info: {failure_reason}")]
55    FailedToGetNodeInfo { failure_reason: String },
56    #[error("Failed to retrieve route hints: {failure_reason}")]
57    FailedToGetRouteHints { failure_reason: String },
58    #[error("Payment failed: {failure_reason}")]
59    FailedPayment { failure_reason: String },
60    #[error("Failed to route HTLCs: {failure_reason}")]
61    FailedToRouteHtlcs { failure_reason: String },
62    #[error("Failed to complete HTLC: {failure_reason}")]
63    FailedToCompleteHtlc { failure_reason: String },
64    #[error("Failed to open channel: {failure_reason}")]
65    FailedToOpenChannel { failure_reason: String },
66    #[error("Failed to close channel: {failure_reason}")]
67    FailedToCloseChannelsWithPeer { failure_reason: String },
68    #[error("Failed to set channel fees: {failure_reason}")]
69    FailedToSetChannelFees { failure_reason: String },
70    #[error("Failed to get Invoice: {failure_reason}")]
71    FailedToGetInvoice { failure_reason: String },
72    #[error("Failed to list transactions: {failure_reason}")]
73    FailedToListTransactions { failure_reason: String },
74    #[error("Failed to get funding address: {failure_reason}")]
75    FailedToGetLnOnchainAddress { failure_reason: String },
76    #[error("Failed to withdraw funds on-chain: {failure_reason}")]
77    FailedToWithdrawOnchain { failure_reason: String },
78    #[error("Failed to connect to peer: {failure_reason}")]
79    FailedToConnectToPeer { failure_reason: String },
80    #[error("Failed to list active channels: {failure_reason}")]
81    FailedToListChannels { failure_reason: String },
82    #[error("Failed to get balances: {failure_reason}")]
83    FailedToGetBalances { failure_reason: String },
84    #[error("Failed to sync to chain: {failure_reason}")]
85    FailedToSyncToChain { failure_reason: String },
86    #[error("Invalid metadata: {failure_reason}")]
87    InvalidMetadata { failure_reason: String },
88    #[error("Bolt12 Error: {failure_reason}")]
89    Bolt12Error { failure_reason: String },
90    // This type is consensus-encoded with positional variant indices and is
91    // persisted in gateway client state machines: only append new variants.
92    #[error("HTLC completion cannot reach the requested outcome: {failure_reason}")]
93    HtlcCompletionRejected { failure_reason: String },
94}
95
96/// Represents an active connection to the lightning node.
97#[derive(Clone, Debug)]
98pub struct LightningContext {
99    pub lnrpc: Arc<dyn ILnRpcClient>,
100    pub lightning_public_key: PublicKey,
101    pub lightning_alias: String,
102    pub lightning_network: Network,
103}
104
105/// A trait that the gateway uses to interact with a lightning node. This allows
106/// the gateway to be agnostic to the specific lightning node implementation
107/// being used.
108#[async_trait]
109pub trait ILnRpcClient: Debug + Send + Sync {
110    /// Returns high-level info about the lightning node.
111    async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
112
113    /// Returns route hints to the lightning node.
114    ///
115    /// Note: This is only used for inbound LNv1 payments and will be removed
116    /// when we switch to LNv2.
117    async fn routehints(
118        &self,
119        num_route_hints: usize,
120    ) -> Result<GetRouteHintsResponse, LightningRpcError>;
121
122    /// Attempts to pay an invoice using the lightning node, waiting for the
123    /// payment to complete and returning the preimage.
124    ///
125    /// Caller restrictions:
126    /// May be called multiple times for the same invoice, but _should_ be done
127    /// with all the same parameters. This is because the payment may be
128    /// in-flight from a previous call, in which case fee or delay limits cannot
129    /// be changed and will be ignored.
130    ///
131    /// Implementor restrictions:
132    /// This _must_ be idempotent for a given invoice, since it is called by
133    /// state machines. In more detail, when called for a given invoice:
134    /// * If the payment is already in-flight, wait for that payment to complete
135    ///   as if it were the first call.
136    /// * If the payment has already been attempted and failed, return an error.
137    /// * If the payment has already succeeded, return a success response.
138    async fn pay(
139        &self,
140        invoice: Bolt11Invoice,
141        max_delay: u64,
142        max_fee: Amount,
143    ) -> Result<PayInvoiceResponse, LightningRpcError> {
144        self.pay_private(
145            PrunedInvoice::try_from(invoice).map_err(|_| LightningRpcError::FailedPayment {
146                failure_reason: "Invoice has no amount".to_string(),
147            })?,
148            max_delay,
149            max_fee,
150        )
151        .await
152    }
153
154    /// Attempts to pay an invoice using the lightning node, waiting for the
155    /// payment to complete and returning the preimage.
156    ///
157    /// This is more private than [`ILnRpcClient::pay`], as it does not require
158    /// the invoice description. If this is implemented,
159    /// [`ILnRpcClient::supports_private_payments`] must return true.
160    async fn pay_private(
161        &self,
162        _invoice: PrunedInvoice,
163        _max_delay: u64,
164        _max_fee: Amount,
165    ) -> Result<PayInvoiceResponse, LightningRpcError> {
166        Err(LightningRpcError::FailedPayment {
167            failure_reason: "Private payments not supported".to_string(),
168        })
169    }
170
171    /// Returns true if the lightning backend supports payments without full
172    /// invoices. If this returns true, [`ILnRpcClient::pay_private`] must
173    /// be implemented.
174    fn supports_private_payments(&self) -> bool {
175        false
176    }
177
178    /// Consumes the current client and returns a stream of intercepted HTLCs
179    /// and a new client. `complete_htlc` must be called for all successfully
180    /// intercepted HTLCs sent to the returned stream.
181    ///
182    /// `route_htlcs` can only be called once for a given client, since the
183    /// returned stream grants exclusive routing decisions to the caller.
184    /// For this reason, `route_htlc` consumes the client and returns one
185    /// wrapped in an `Arc`. This lets the compiler enforce that `route_htlcs`
186    /// can only be called once for a given client, since the value inside
187    /// the `Arc` cannot be consumed.
188    async fn route_htlcs<'a>(
189        self: Box<Self>,
190        task_group: &TaskGroup,
191    ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
192
193    /// Completes an HTLC that was intercepted by the gateway. Must be called
194    /// for all successfully intercepted HTLCs sent to the stream returned
195    /// by `route_htlcs`.
196    async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
197
198    /// Requests the lightning node to create an invoice. The presence of a
199    /// payment hash in the `CreateInvoiceRequest` determines if the invoice is
200    /// intended to be an ecash payment or a direct payment to this lightning
201    /// node.
202    async fn create_invoice(
203        &self,
204        create_invoice_request: CreateInvoiceRequest,
205    ) -> Result<CreateInvoiceResponse, LightningRpcError>;
206
207    /// Gets a funding address belonging to the lightning node's on-chain
208    /// wallet.
209    async fn get_ln_onchain_address(
210        &self,
211    ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
212
213    /// Executes an onchain transaction using the lightning node's on-chain
214    /// wallet.
215    async fn send_onchain(
216        &self,
217        payload: SendOnchainRequest,
218    ) -> Result<SendOnchainResponse, LightningRpcError>;
219
220    /// Opens a channel with a peer lightning node.
221    async fn open_channel(
222        &self,
223        payload: OpenChannelRequest,
224    ) -> Result<OpenChannelResponse, LightningRpcError>;
225
226    /// Connects to a peer lightning node without opening a channel.
227    async fn connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError>;
228
229    /// Closes all channels with a peer lightning node.
230    async fn close_channels_with_peer(
231        &self,
232        payload: CloseChannelsWithPeerRequest,
233    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
234
235    /// Lists the lightning node's active channels with all peers.
236    async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
237
238    /// Updates the local-side routing fee policy (base fee in msat and
239    /// proportional fee in parts-per-million) advertised on a single channel
240    /// identified by its funding outpoint.
241    async fn set_channel_fees(
242        &self,
243        payload: SetChannelFeesRequest,
244    ) -> Result<(), LightningRpcError>;
245
246    /// Returns a summary of the lightning node's balance, including the onchain
247    /// wallet, outbound liquidity, and inbound liquidity.
248    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
249
250    async fn get_invoice(
251        &self,
252        get_invoice_request: GetInvoiceRequest,
253    ) -> Result<Option<GetInvoiceResponse>, LightningRpcError>;
254
255    async fn list_transactions(
256        &self,
257        start_secs: u64,
258        end_secs: u64,
259    ) -> Result<ListTransactionsResponse, LightningRpcError>;
260
261    fn create_offer(
262        &self,
263        amount: Option<Amount>,
264        description: Option<String>,
265        expiry_secs: Option<u32>,
266        quantity: Option<u64>,
267    ) -> Result<String, LightningRpcError>;
268
269    async fn pay_offer(
270        &self,
271        offer: String,
272        quantity: Option<u64>,
273        amount: Option<Amount>,
274        payer_note: Option<String>,
275    ) -> Result<Preimage, LightningRpcError>;
276
277    fn sync_wallet(&self) -> Result<(), LightningRpcError>;
278}
279
280impl dyn ILnRpcClient {
281    /// Retrieve route hints from the Lightning node, capped at
282    /// `num_route_hints`. The route hints should be ordered based on liquidity
283    /// of incoming channels.
284    pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
285        if num_route_hints == 0 {
286            return vec![];
287        }
288
289        let route_hints =
290            self.routehints(num_route_hints as usize)
291                .await
292                .unwrap_or(GetRouteHintsResponse {
293                    route_hints: Vec::new(),
294                });
295        route_hints.route_hints
296    }
297
298    /// Retrieves the basic information about the Gateway's connected Lightning
299    /// node.
300    pub async fn parsed_node_info(&self) -> LightningInfo {
301        if let Ok(info) = self.info().await
302            && let Ok(network) =
303                Network::from_str(&info.network).map_err(|e| LightningRpcError::InvalidMetadata {
304                    failure_reason: format!("Invalid network {}: {e}", info.network),
305                })
306        {
307            return LightningInfo::Connected {
308                public_key: info.pub_key,
309                alias: info.alias,
310                network,
311                block_height: info.block_height as u64,
312                synced_to_chain: info.synced_to_chain,
313            };
314        }
315
316        LightningInfo::NotConnected
317    }
318
319    /// Waits for the Lightning node to be synced to the Bitcoin blockchain.
320    pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
321        // In devimint, we explicitly sync the onchain wallet to start the sync quicker
322        // than background sync would. In production, background sync is
323        // sufficient
324        if is_env_var_set(FM_IN_DEVIMINT_ENV) {
325            self.sync_wallet()?;
326        }
327
328        // Wait for the Lightning node to sync
329        retry(
330            "Wait for chain sync",
331            backoff_util::background_backoff(),
332            || async {
333                let info = self.info().await?;
334                let block_height = info.block_height;
335                if info.synced_to_chain {
336                    Ok(())
337                } else {
338                    warn!(target: LOG_LIGHTNING, block_height = %block_height, "Lightning node is not synced yet");
339                    Err(anyhow::anyhow!("Not synced yet"))
340                }
341            },
342        )
343        .await
344        .map_err(|e| LightningRpcError::FailedToSyncToChain {
345            failure_reason: format!("Failed to sync to chain: {e:?}"),
346        })?;
347
348        info!(target: LOG_LIGHTNING, "Gateway successfully synced with the chain");
349        Ok(())
350    }
351}
352
353#[derive(Debug, Serialize, Deserialize, Clone)]
354pub struct GetNodeInfoResponse {
355    pub pub_key: PublicKey,
356    pub alias: String,
357    pub network: String,
358    pub block_height: u32,
359    pub synced_to_chain: bool,
360}
361
362/// The `(incoming_chan_id, htlc_id)` circuit key reported for a payment that
363/// did not arrive as an intercepted forward and therefore has no incoming
364/// circuit to resolve: an LNv2 payment held by a HOLD invoice on the gateway's
365/// own node, and every payment reported by the LDK backend.
366///
367/// Zero is unambiguous as a marker because no channel is assigned a zero short
368/// channel id: a confirmed channel's id encodes its funding block height, and
369/// an unconfirmed one gets an alias from a high range. LND reserves zero for
370/// locally originated payments and exit hops, neither of which is a forward
371/// the gateway intercepts.
372pub const NO_INCOMING_CIRCUIT: (u64, u64) = (0, 0);
373
374#[derive(Debug, Serialize, Deserialize, Clone)]
375pub struct InterceptPaymentRequest {
376    pub payment_hash: sha256::Hash,
377    pub amount_msat: u64,
378    pub expiry: u32,
379    pub incoming_chan_id: u64,
380    pub short_channel_id: Option<u64>,
381    pub htlc_id: u64,
382}
383
384#[derive(Debug, Serialize, Deserialize, Clone)]
385pub struct InterceptPaymentResponse {
386    pub incoming_chan_id: u64,
387    pub htlc_id: u64,
388    pub payment_hash: sha256::Hash,
389    pub action: PaymentAction,
390}
391
392impl InterceptPaymentResponse {
393    /// The incoming circuit this response resolves, or `None` when the payment
394    /// was not an intercepted forward (see [`NO_INCOMING_CIRCUIT`]).
395    ///
396    /// Backends must pick how to resolve a payment from this, never from the
397    /// payment hash. The hash is chosen by whoever is being paid, so two
398    /// unrelated payments — one intercepted forward and one HOLD invoice —
399    /// can carry the same hash, and resolving by hash would let a completion
400    /// for one settle or cancel the other.
401    pub fn incoming_circuit(&self) -> Option<(u64, u64)> {
402        let circuit = (self.incoming_chan_id, self.htlc_id);
403        (circuit != NO_INCOMING_CIRCUIT).then_some(circuit)
404    }
405}
406
407#[derive(Debug, Serialize, Deserialize, Clone)]
408pub enum PaymentAction {
409    Settle(Preimage),
410    Cancel,
411    Forward,
412}
413
414#[derive(Debug, Serialize, Deserialize, Clone)]
415pub struct GetRouteHintsResponse {
416    pub route_hints: Vec<RouteHint>,
417}
418
419#[derive(Debug, Serialize, Deserialize, Clone)]
420pub struct PayInvoiceResponse {
421    pub preimage: Preimage,
422}
423
424#[derive(Debug, Serialize, Deserialize, Clone)]
425pub struct CreateInvoiceRequest {
426    pub payment_hash: Option<sha256::Hash>,
427    pub amount_msat: u64,
428    pub expiry_secs: u32,
429    pub description: Option<InvoiceDescription>,
430}
431
432#[derive(Debug, Serialize, Deserialize, Clone)]
433pub enum InvoiceDescription {
434    Direct(String),
435    Hash(sha256::Hash),
436}
437
438#[derive(Debug, Serialize, Deserialize, Clone)]
439pub struct CreateInvoiceResponse {
440    pub invoice: String,
441}
442
443#[derive(Debug, Serialize, Deserialize, Clone)]
444pub struct GetLnOnchainAddressResponse {
445    pub address: String,
446}
447
448#[derive(Debug, Serialize, Deserialize, Clone)]
449pub struct SendOnchainResponse {
450    pub txid: String,
451}
452
453#[derive(Debug, Serialize, Deserialize, Clone)]
454pub struct OpenChannelResponse {
455    pub funding_txid: String,
456}
457
458#[derive(Debug, Serialize, Deserialize, Clone)]
459pub struct ListChannelsResponse {
460    pub channels: Vec<ChannelInfo>,
461}
462
463#[derive(Debug, Serialize, Deserialize, Clone)]
464pub struct GetBalancesResponse {
465    pub onchain_balance_sats: u64,
466    pub lightning_balance_msats: u64,
467    pub inbound_lightning_liquidity_msats: u64,
468}
469
470/// A wrapper around `Arc<dyn ILnRpcClient>` that tracks metrics for each RPC
471/// call.
472///
473/// This wrapper records the duration and success/error status of each
474/// Lightning RPC call to Prometheus metrics, allowing monitoring of
475/// Lightning node connectivity and performance.
476///
477/// Note: This wrapper is designed to wrap the `Arc<dyn ILnRpcClient>` returned
478/// from `route_htlcs`. Calling `route_htlcs` on this wrapper will panic, as
479/// `route_htlcs` should only be called once on the original client before
480/// wrapping.
481pub struct LnRpcTracked {
482    inner: Arc<dyn ILnRpcClient>,
483    name: &'static str,
484}
485
486impl std::fmt::Debug for LnRpcTracked {
487    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488        f.debug_struct("LnRpcTracked")
489            .field("name", &self.name)
490            .field("inner", &self.inner)
491            .finish()
492    }
493}
494
495impl LnRpcTracked {
496    /// Wraps an `Arc<dyn ILnRpcClient>` with metrics tracking.
497    ///
498    /// The `name` parameter is used to distinguish different uses of the
499    /// Lightning RPC client in metrics (e.g., "gateway").
500    #[allow(clippy::new_ret_no_self)]
501    pub fn new(inner: Arc<dyn ILnRpcClient>, name: &'static str) -> Arc<dyn ILnRpcClient> {
502        Arc::new(Self { inner, name })
503    }
504
505    fn record_call<T, E>(&self, method: &str, result: &Result<T, E>) {
506        let result_label = if result.is_ok() { "success" } else { "error" };
507        metrics::LN_RPC_REQUESTS_TOTAL
508            .with_label_values(&[method, self.name, result_label])
509            .inc();
510    }
511}
512
513macro_rules! tracked_call {
514    ($self:ident, $method:expr, $call:expr) => {{
515        trace!(
516            target: LOG_LIGHTNING,
517            method = $method,
518            name = $self.name,
519            "starting lightning rpc"
520        );
521        let start = now();
522        let timer = metrics::LN_RPC_DURATION_SECONDS
523            .with_label_values(&[$method, $self.name])
524            .start_timer_ext();
525        let result = $call;
526        timer.observe_duration();
527        $self.record_call($method, &result);
528        let duration_ms = now()
529            .duration_since(start)
530            .unwrap_or_default()
531            .as_secs_f64()
532            * 1000.0;
533        trace!(
534            target: LOG_LIGHTNING,
535            method = $method,
536            name = $self.name,
537            duration_ms,
538            error = %result.fmt_compact_result(),
539            "completed lightning rpc"
540        );
541        result
542    }};
543}
544
545#[async_trait]
546impl ILnRpcClient for LnRpcTracked {
547    async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
548        tracked_call!(self, "info", self.inner.info().await)
549    }
550
551    async fn routehints(
552        &self,
553        num_route_hints: usize,
554    ) -> Result<GetRouteHintsResponse, LightningRpcError> {
555        tracked_call!(
556            self,
557            "routehints",
558            self.inner.routehints(num_route_hints).await
559        )
560    }
561
562    async fn pay(
563        &self,
564        invoice: Bolt11Invoice,
565        max_delay: u64,
566        max_fee: Amount,
567    ) -> Result<PayInvoiceResponse, LightningRpcError> {
568        tracked_call!(
569            self,
570            "pay",
571            self.inner.pay(invoice, max_delay, max_fee).await
572        )
573    }
574
575    async fn pay_private(
576        &self,
577        invoice: PrunedInvoice,
578        max_delay: u64,
579        max_fee: Amount,
580    ) -> Result<PayInvoiceResponse, LightningRpcError> {
581        tracked_call!(
582            self,
583            "pay_private",
584            self.inner.pay_private(invoice, max_delay, max_fee).await
585        )
586    }
587
588    fn supports_private_payments(&self) -> bool {
589        self.inner.supports_private_payments()
590    }
591
592    async fn route_htlcs<'a>(
593        self: Box<Self>,
594        _task_group: &TaskGroup,
595    ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
596        // route_htlcs should only be called once on the original client before
597        // wrapping with LnRpcTracked. The Arc returned from route_htlcs should
598        // be wrapped with LnRpcTracked::new.
599        panic!(
600            "route_htlcs should not be called on LnRpcTracked. \
601             Wrap the Arc returned from route_htlcs instead."
602        );
603    }
604
605    async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
606        tracked_call!(self, "complete_htlc", self.inner.complete_htlc(htlc).await)
607    }
608
609    async fn create_invoice(
610        &self,
611        create_invoice_request: CreateInvoiceRequest,
612    ) -> Result<CreateInvoiceResponse, LightningRpcError> {
613        tracked_call!(
614            self,
615            "create_invoice",
616            self.inner.create_invoice(create_invoice_request).await
617        )
618    }
619
620    async fn get_ln_onchain_address(
621        &self,
622    ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
623        tracked_call!(
624            self,
625            "get_ln_onchain_address",
626            self.inner.get_ln_onchain_address().await
627        )
628    }
629
630    async fn send_onchain(
631        &self,
632        payload: SendOnchainRequest,
633    ) -> Result<SendOnchainResponse, LightningRpcError> {
634        tracked_call!(self, "send_onchain", self.inner.send_onchain(payload).await)
635    }
636
637    async fn open_channel(
638        &self,
639        payload: OpenChannelRequest,
640    ) -> Result<OpenChannelResponse, LightningRpcError> {
641        tracked_call!(self, "open_channel", self.inner.open_channel(payload).await)
642    }
643
644    async fn connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError> {
645        tracked_call!(self, "connect_peer", self.inner.connect_peer(payload).await)
646    }
647
648    async fn close_channels_with_peer(
649        &self,
650        payload: CloseChannelsWithPeerRequest,
651    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
652        tracked_call!(
653            self,
654            "close_channels_with_peer",
655            self.inner.close_channels_with_peer(payload).await
656        )
657    }
658
659    async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
660        tracked_call!(self, "list_channels", self.inner.list_channels().await)
661    }
662
663    async fn set_channel_fees(
664        &self,
665        payload: SetChannelFeesRequest,
666    ) -> Result<(), LightningRpcError> {
667        tracked_call!(
668            self,
669            "set_channel_fees",
670            self.inner.set_channel_fees(payload).await
671        )
672    }
673
674    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
675        tracked_call!(self, "get_balances", self.inner.get_balances().await)
676    }
677
678    async fn get_invoice(
679        &self,
680        get_invoice_request: GetInvoiceRequest,
681    ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
682        tracked_call!(
683            self,
684            "get_invoice",
685            self.inner.get_invoice(get_invoice_request).await
686        )
687    }
688
689    async fn list_transactions(
690        &self,
691        start_secs: u64,
692        end_secs: u64,
693    ) -> Result<ListTransactionsResponse, LightningRpcError> {
694        tracked_call!(
695            self,
696            "list_transactions",
697            self.inner.list_transactions(start_secs, end_secs).await
698        )
699    }
700
701    fn create_offer(
702        &self,
703        amount: Option<Amount>,
704        description: Option<String>,
705        expiry_secs: Option<u32>,
706        quantity: Option<u64>,
707    ) -> Result<String, LightningRpcError> {
708        tracked_call!(
709            self,
710            "create_offer",
711            self.inner
712                .create_offer(amount, description, expiry_secs, quantity)
713        )
714    }
715
716    async fn pay_offer(
717        &self,
718        offer: String,
719        quantity: Option<u64>,
720        amount: Option<Amount>,
721        payer_note: Option<String>,
722    ) -> Result<Preimage, LightningRpcError> {
723        tracked_call!(
724            self,
725            "pay_offer",
726            self.inner
727                .pay_offer(offer, quantity, amount, payer_note)
728                .await
729        )
730    }
731
732    fn sync_wallet(&self) -> Result<(), LightningRpcError> {
733        tracked_call!(self, "sync_wallet", self.inner.sync_wallet())
734    }
735}
736
737#[cfg(test)]
738mod tests {
739    use bitcoin::hashes::{Hash as _, sha256};
740
741    use super::{InterceptPaymentResponse, NO_INCOMING_CIRCUIT, PaymentAction, Preimage};
742
743    fn response(incoming_chan_id: u64, htlc_id: u64) -> InterceptPaymentResponse {
744        InterceptPaymentResponse {
745            incoming_chan_id,
746            htlc_id,
747            payment_hash: sha256::Hash::all_zeros(),
748            action: PaymentAction::Settle(Preimage([0; 32])),
749        }
750    }
751
752    #[test]
753    fn payment_without_incoming_circuit_is_recognized() {
754        let (chan_id, htlc_id) = NO_INCOMING_CIRCUIT;
755        assert_eq!(response(chan_id, htlc_id).incoming_circuit(), None);
756    }
757
758    #[test]
759    fn intercepted_forward_keeps_its_circuit() {
760        // An intercepted forward must never be mistaken for a payment held by
761        // a HOLD invoice, including when it is the first HTLC on its channel
762        // and so has htlc id 0.
763        assert_eq!(response(101, 0).incoming_circuit(), Some((101, 0)));
764        assert_eq!(response(101, 7).incoming_circuit(), Some((101, 7)));
765    }
766}