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