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