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