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