devimint/
gatewayd.rs

1use std::collections::HashMap;
2use std::ops::ControlFlow;
3use std::path::PathBuf;
4use std::str::FromStr;
5use std::time::SystemTime;
6
7use anyhow::{Context, Result, anyhow};
8use bitcoin::hashes::sha256;
9use chrono::{DateTime, Utc};
10use esplora_client::Txid;
11use fedimint_core::config::FederationId;
12use fedimint_core::secp256k1::PublicKey;
13use fedimint_core::util::{backoff_util, retry};
14use fedimint_core::{Amount, BitcoinAmountOrAll, BitcoinHash};
15use fedimint_gateway_common::envs::FM_GATEWAY_IROH_SECRET_KEY_OVERRIDE_ENV;
16use fedimint_gateway_common::{
17    ChannelInfo, CreateOfferResponse, GatewayBalances, GetInvoiceResponse,
18    ListTransactionsResponse, MnemonicResponse, PaymentDetails, PaymentStatus,
19    PaymentSummaryResponse, V1_API_ENDPOINT,
20};
21use fedimint_ln_server::common::lightning_invoice::Bolt11Invoice;
22use fedimint_lnv2_common::gateway_api::PaymentFee;
23use fedimint_logging::LOG_DEVIMINT;
24use fedimint_testing_core::node_type::LightningNodeType;
25use rand::rngs::OsRng;
26use semver::Version;
27use tracing::info;
28
29use crate::cmd;
30use crate::envs::{
31    FM_GATEWAY_API_ADDR_ENV, FM_GATEWAY_DATA_DIR_ENV, FM_GATEWAY_IROH_LISTEN_ADDR_ENV,
32    FM_GATEWAY_LISTEN_ADDR_ENV, FM_PORT_LDK_ENV,
33};
34use crate::external::{Bitcoind, LightningNode};
35use crate::federation::Federation;
36use crate::util::{Command, ProcessHandle, ProcessManager, poll, supports_lnv2};
37use crate::vars::utf8;
38use crate::version_constants::VERSION_0_9_0_ALPHA;
39
40#[derive(Clone)]
41pub struct Gatewayd {
42    pub(crate) process: ProcessHandle,
43    pub ln: LightningNode,
44    pub addr: String,
45    pub(crate) lightning_node_addr: String,
46    pub gatewayd_version: Version,
47    pub gw_name: String,
48    pub log_path: PathBuf,
49    pub gw_port: u16,
50    pub ldk_port: u16,
51    pub gateway_id: String,
52    pub iroh_port: u16,
53    pub node_id: iroh_base::NodeId,
54}
55
56impl Gatewayd {
57    pub async fn new(process_mgr: &ProcessManager, ln: LightningNode) -> Result<Self> {
58        let ln_type = ln.ln_type();
59        let (gw_name, port, lightning_node_port, iroh_port) = match &ln {
60            LightningNode::Lnd(_) => (
61                "gatewayd-lnd".to_string(),
62                process_mgr.globals.FM_PORT_GW_LND,
63                process_mgr.globals.FM_PORT_LND_LISTEN,
64                process_mgr.globals.FM_PORT_GW_LND_IROH,
65            ),
66            LightningNode::Ldk {
67                name,
68                gw_port,
69                ldk_port,
70                iroh_port,
71            } => (
72                name.to_owned(),
73                gw_port.to_owned(),
74                ldk_port.to_owned(),
75                iroh_port.to_owned(),
76            ),
77        };
78        let test_dir = &process_mgr.globals.FM_TEST_DIR;
79        let addr = format!("http://127.0.0.1:{port}/{V1_API_ENDPOINT}");
80        let lightning_node_addr = format!("127.0.0.1:{lightning_node_port}");
81        let iroh_sk = iroh_base::SecretKey::generate(&mut OsRng);
82
83        let mut gateway_env: HashMap<String, String> = HashMap::from_iter([
84            (
85                FM_GATEWAY_DATA_DIR_ENV.to_owned(),
86                format!("{}/{gw_name}", utf8(test_dir)),
87            ),
88            (
89                FM_GATEWAY_LISTEN_ADDR_ENV.to_owned(),
90                format!("127.0.0.1:{port}"),
91            ),
92            (FM_GATEWAY_API_ADDR_ENV.to_owned(), addr.clone()),
93            (FM_PORT_LDK_ENV.to_owned(), lightning_node_port.to_string()),
94            (
95                FM_GATEWAY_IROH_LISTEN_ADDR_ENV.to_owned(),
96                format!("127.0.0.1:{iroh_port}"),
97            ),
98            (
99                FM_GATEWAY_IROH_SECRET_KEY_OVERRIDE_ENV.to_owned(),
100                iroh_sk.to_string(),
101            ),
102        ]);
103
104        let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
105        if gatewayd_version < *VERSION_0_9_0_ALPHA {
106            let mode = if supports_lnv2() {
107                "All"
108            } else {
109                info!(target: LOG_DEVIMINT, "LNv2 is not supported, running gatewayd in LNv1 mode");
110                "LNv1"
111            };
112            gateway_env.insert(
113                "FM_GATEWAY_LIGHTNING_MODULE_MODE".to_owned(),
114                mode.to_string(),
115            );
116        }
117
118        if ln_type == LightningNodeType::Ldk {
119            gateway_env.insert("FM_LDK_ALIAS".to_owned(), gw_name.clone());
120
121            // Prior to `v0.9.0`, only LDK could connect to bitcoind
122            if gatewayd_version < *VERSION_0_9_0_ALPHA {
123                let btc_rpc_port = process_mgr.globals.FM_PORT_BTC_RPC;
124                gateway_env.insert(
125                    "FM_LDK_BITCOIND_RPC_URL".to_owned(),
126                    format!("http://bitcoin:bitcoin@127.0.0.1:{btc_rpc_port}"),
127                );
128            }
129        }
130
131        let process = process_mgr
132            .spawn_daemon(
133                &gw_name,
134                cmd!(crate::util::Gatewayd, ln_type).envs(gateway_env),
135            )
136            .await?;
137
138        let gateway_id = poll(
139            "waiting for gateway to be ready to respond to rpc",
140            || async {
141                // Once the gateway id is available via RPC, the gateway is ready
142                let info = cmd!(
143                    crate::util::get_gateway_cli_path(),
144                    "--rpcpassword",
145                    "theresnosecondbest",
146                    "-a",
147                    addr,
148                    "info"
149                )
150                .out_json()
151                .await
152                .map_err(ControlFlow::Continue)?;
153                let gateway_id = info["gateway_id"]
154                    .as_str()
155                    .context("gateway_id must be a string")
156                    .map_err(ControlFlow::Break)?
157                    .to_owned();
158                Ok(gateway_id)
159            },
160        )
161        .await?;
162
163        let log_path = process_mgr
164            .globals
165            .FM_LOGS_DIR
166            .join(format!("{gw_name}.log"));
167        let gatewayd = Self {
168            process,
169            ln,
170            addr,
171            lightning_node_addr,
172            gatewayd_version,
173            gw_name,
174            log_path,
175            gw_port: port,
176            ldk_port: lightning_node_port,
177            gateway_id,
178            iroh_port,
179            node_id: iroh_sk.public(),
180        };
181
182        Ok(gatewayd)
183    }
184
185    pub async fn terminate(self) -> Result<()> {
186        self.process.terminate().await
187    }
188
189    pub fn set_lightning_node(&mut self, ln_node: LightningNode) {
190        self.ln = ln_node;
191    }
192
193    pub async fn stop_lightning_node(&mut self) -> Result<()> {
194        info!(target: LOG_DEVIMINT, "Stopping lightning node");
195        match self.ln.clone() {
196            LightningNode::Lnd(lnd) => lnd.terminate().await,
197            LightningNode::Ldk {
198                name: _,
199                gw_port: _,
200                ldk_port: _,
201                iroh_port: _,
202            } => {
203                // This is not implemented because the LDK node lives in
204                // the gateway process and cannot be stopped independently.
205                unimplemented!("LDK node termination not implemented")
206            }
207        }
208    }
209
210    /// Restarts the gateway using the provided `bin_path`, which is useful for
211    /// testing upgrades.
212    pub async fn restart_with_bin(
213        &mut self,
214        process_mgr: &ProcessManager,
215        gatewayd_path: &PathBuf,
216        gateway_cli_path: &PathBuf,
217    ) -> Result<()> {
218        let ln = self.ln.clone();
219
220        self.process.terminate().await?;
221        // TODO: Audit that the environment access only happens in single-threaded code.
222        unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", gatewayd_path) };
223        // TODO: Audit that the environment access only happens in single-threaded code.
224        unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", gateway_cli_path) };
225
226        let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
227        if gatewayd_version < *VERSION_0_9_0_ALPHA && supports_lnv2() {
228            info!(target: LOG_DEVIMINT, "LNv2 is now supported, running in All mode");
229            // TODO: Audit that the environment access only happens in single-threaded code.
230            unsafe { std::env::set_var("FM_GATEWAY_LIGHTNING_MODULE_MODE", "All") };
231        }
232
233        let new_ln = ln;
234        let new_gw = Self::new(process_mgr, new_ln.clone()).await?;
235        self.process = new_gw.process;
236        self.set_lightning_node(new_ln);
237        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
238        info!(
239            target: LOG_DEVIMINT,
240            ?gatewayd_version,
241            ?gateway_cli_version,
242            "upgraded gatewayd and gateway-cli"
243        );
244        Ok(())
245    }
246
247    pub fn cmd(&self) -> Command {
248        cmd!(
249            crate::util::get_gateway_cli_path(),
250            "--rpcpassword=theresnosecondbest",
251            "-a",
252            &self.addr
253        )
254    }
255
256    pub async fn gateway_id(&self) -> Result<String> {
257        let info = self.get_info().await?;
258        let gateway_id = info["gateway_id"]
259            .as_str()
260            .context("gateway_id must be a string")?
261            .to_owned();
262        Ok(gateway_id)
263    }
264
265    pub async fn get_info(&self) -> Result<serde_json::Value> {
266        retry(
267            "Getting gateway info via gateway-cli info",
268            backoff_util::aggressive_backoff(),
269            || async { cmd!(self, "info").out_json().await },
270        )
271        .await
272        .context("Getting gateway info via gateway-cli info")
273    }
274
275    pub async fn get_info_iroh(&self) -> Result<serde_json::Value> {
276        cmd!(
277            crate::util::get_gateway_cli_path(),
278            "--rpcpassword=theresnosecondbest",
279            "--address",
280            format!("iroh://{}", self.node_id),
281            "--connection-override",
282            format!("http://127.0.0.1:{}", self.iroh_port),
283            "info",
284        )
285        .out_json()
286        .await
287    }
288
289    pub async fn lightning_pubkey(&self) -> Result<PublicKey> {
290        let info = self.get_info().await?;
291        let lightning_pub_key = info["lightning_pub_key"]
292            .as_str()
293            .context("lightning_pub_key must be a string")?
294            .to_owned();
295        Ok(lightning_pub_key.parse()?)
296    }
297
298    pub async fn connect_fed(&self, fed: &Federation) -> Result<()> {
299        let invite_code = fed.invite_code()?;
300        poll("gateway connect-fed", || async {
301            cmd!(self, "connect-fed", invite_code.clone())
302                .run()
303                .await
304                .map_err(ControlFlow::Continue)?;
305            Ok(())
306        })
307        .await?;
308        Ok(())
309    }
310
311    pub async fn recover_fed(&self, fed: &Federation) -> Result<()> {
312        let federation_id = fed.calculate_federation_id();
313        let invite_code = fed.invite_code()?;
314        info!(target: LOG_DEVIMINT, federation_id = %federation_id, "Recovering...");
315        poll("gateway connect-fed --recover=true", || async {
316            cmd!(self, "connect-fed", invite_code.clone(), "--recover=true")
317                .run()
318                .await
319                .map_err(ControlFlow::Continue)?;
320            Ok(())
321        })
322        .await?;
323        Ok(())
324    }
325
326    pub async fn backup_to_fed(&self, fed: &Federation) -> Result<()> {
327        let federation_id = fed.calculate_federation_id();
328        cmd!(self, "ecash", "backup", "--federation-id", federation_id)
329            .run()
330            .await?;
331        Ok(())
332    }
333
334    pub async fn get_pegin_addr(&self, fed_id: &str) -> Result<String> {
335        Ok(cmd!(self, "ecash", "pegin", "--federation-id={fed_id}")
336            .out_json()
337            .await?
338            .as_str()
339            .context("address must be a string")?
340            .to_owned())
341    }
342
343    pub async fn get_ln_onchain_address(&self) -> Result<String> {
344        cmd!(self, "onchain", "address").out_string().await
345    }
346
347    pub async fn get_mnemonic(&self) -> Result<MnemonicResponse> {
348        let value = retry(
349            "Getting gateway mnemonic",
350            backoff_util::aggressive_backoff(),
351            || async { cmd!(self, "seed").out_json().await },
352        )
353        .await
354        .context("Getting gateway mnemonic")?;
355
356        Ok(serde_json::from_value(value)?)
357    }
358
359    pub async fn leave_federation(&self, federation_id: FederationId) -> Result<()> {
360        cmd!(self, "leave-fed", "--federation-id", federation_id)
361            .run()
362            .await?;
363        Ok(())
364    }
365
366    pub async fn create_invoice(&self, amount_msats: u64) -> Result<Bolt11Invoice> {
367        Ok(Bolt11Invoice::from_str(
368            &cmd!(self, "lightning", "create-invoice", amount_msats)
369                .out_string()
370                .await?,
371        )?)
372    }
373
374    pub async fn pay_invoice(&self, invoice: Bolt11Invoice) -> Result<()> {
375        cmd!(self, "lightning", "pay-invoice", invoice.to_string())
376            .run()
377            .await?;
378
379        Ok(())
380    }
381
382    pub async fn send_ecash(&self, federation_id: String, amount_msats: u64) -> Result<String> {
383        let value = cmd!(
384            self,
385            "ecash",
386            "send",
387            "--federation-id",
388            federation_id,
389            amount_msats
390        )
391        .out_json()
392        .await?;
393        let ecash: String = serde_json::from_value(
394            value
395                .get("notes")
396                .expect("notes key does not exist")
397                .clone(),
398        )?;
399        Ok(ecash)
400    }
401
402    pub async fn receive_ecash(&self, ecash: String) -> Result<()> {
403        cmd!(self, "ecash", "receive", "--notes", ecash)
404            .run()
405            .await?;
406        Ok(())
407    }
408
409    pub async fn get_balances(&self) -> Result<GatewayBalances> {
410        let value = cmd!(self, "get-balances").out_json().await?;
411        Ok(serde_json::from_value(value)?)
412    }
413
414    pub async fn ecash_balance(&self, federation_id: String) -> anyhow::Result<u64> {
415        let federation_id = FederationId::from_str(&federation_id)?;
416        let balances = self.get_balances().await?;
417        let ecash_balance = balances
418            .ecash_balances
419            .into_iter()
420            .find(|info| info.federation_id == federation_id)
421            .ok_or(anyhow::anyhow!("Gateway is not joined to federation"))?
422            .ecash_balance_msats
423            .msats;
424        Ok(ecash_balance)
425    }
426
427    pub async fn send_onchain(
428        &self,
429        bitcoind: &Bitcoind,
430        amount: BitcoinAmountOrAll,
431        fee_rate: u64,
432    ) -> Result<bitcoin::Txid> {
433        let withdraw_address = bitcoind.get_new_address().await?;
434        let value = cmd!(
435            self,
436            "onchain",
437            "send",
438            "--address",
439            withdraw_address,
440            "--amount",
441            amount,
442            "--fee-rate-sats-per-vbyte",
443            fee_rate
444        )
445        .out_json()
446        .await?;
447
448        let txid: bitcoin::Txid = serde_json::from_value(value)?;
449        Ok(txid)
450    }
451
452    pub async fn close_channel(&self, remote_pubkey: PublicKey, force: bool) -> Result<()> {
453        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
454        let mut close_channel = if force && gateway_cli_version >= *VERSION_0_9_0_ALPHA {
455            cmd!(
456                self,
457                "lightning",
458                "close-channels-with-peer",
459                "--pubkey",
460                remote_pubkey,
461                "--force",
462            )
463        } else {
464            cmd!(
465                self,
466                "lightning",
467                "close-channels-with-peer",
468                "--pubkey",
469                remote_pubkey,
470            )
471        };
472
473        close_channel.run().await?;
474
475        Ok(())
476    }
477
478    pub async fn close_all_channels(&self, force: bool) -> Result<()> {
479        let channels = self.list_channels().await?;
480
481        for chan in channels {
482            let remote_pubkey = chan.remote_pubkey;
483            self.close_channel(remote_pubkey, force).await?;
484        }
485
486        Ok(())
487    }
488
489    /// Open a channel with the gateway's lightning node, returning the funding
490    /// transaction txid.
491    pub async fn open_channel(
492        &self,
493        gw: &Gatewayd,
494        channel_size_sats: u64,
495        push_amount_sats: Option<u64>,
496    ) -> Result<Txid> {
497        let pubkey = gw.lightning_pubkey().await?;
498
499        let mut command = cmd!(
500            self,
501            "lightning",
502            "open-channel",
503            "--pubkey",
504            pubkey,
505            "--host",
506            gw.lightning_node_addr,
507            "--channel-size-sats",
508            channel_size_sats,
509            "--push-amount-sats",
510            push_amount_sats.unwrap_or(0)
511        );
512
513        Ok(Txid::from_str(&command.out_string().await?)?)
514    }
515
516    pub async fn list_channels(&self) -> Result<Vec<ChannelInfo>> {
517        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
518        let channels = if gateway_cli_version >= *VERSION_0_9_0_ALPHA {
519            cmd!(self, "lightning", "list-channels").out_json().await?
520        } else {
521            cmd!(self, "lightning", "list-active-channels")
522                .out_json()
523                .await?
524        };
525
526        let channels = channels
527            .as_array()
528            .context("channels must be an array")?
529            .iter()
530            .map(|channel| {
531                let remote_pubkey = channel["remote_pubkey"]
532                    .as_str()
533                    .context("remote_pubkey must be a string")?
534                    .to_owned();
535                let channel_size_sats = channel["channel_size_sats"]
536                    .as_u64()
537                    .context("channel_size_sats must be a u64")?;
538                let outbound_liquidity_sats = channel["outbound_liquidity_sats"]
539                    .as_u64()
540                    .context("outbound_liquidity_sats must be a u64")?;
541                let inbound_liquidity_sats = channel["inbound_liquidity_sats"]
542                    .as_u64()
543                    .context("inbound_liquidity_sats must be a u64")?;
544                let is_active = channel["is_active"].as_bool().unwrap_or(true);
545                Ok(ChannelInfo {
546                    remote_pubkey: remote_pubkey
547                        .parse()
548                        .expect("Lightning node returned invalid remote channel pubkey"),
549                    channel_size_sats,
550                    outbound_liquidity_sats,
551                    inbound_liquidity_sats,
552                    is_active,
553                })
554            })
555            .collect::<Result<Vec<ChannelInfo>>>()?;
556        Ok(channels)
557    }
558
559    pub async fn wait_for_block_height(&self, target_block_height: u64) -> Result<()> {
560        poll("waiting for block height", || async {
561            let info = self.get_info().await.map_err(ControlFlow::Continue)?;
562            let value = info.get("block_height");
563            if let Some(height) = value {
564                let block_height: Option<u32> = serde_json::from_value(height.clone())
565                    .context("Could not parse block height")
566                    .map_err(ControlFlow::Continue)?;
567                let Some(block_height) = block_height else {
568                    return Err(ControlFlow::Continue(anyhow!("Not synced any blocks yet")));
569                };
570                let synced = info["synced_to_chain"]
571                    .as_bool()
572                    .expect("Could not get synced_to_chain");
573                if block_height >= target_block_height as u32 && synced {
574                    return Ok(());
575                }
576            }
577            Err(ControlFlow::Continue(anyhow!("Not synced to block")))
578        })
579        .await?;
580        Ok(())
581    }
582
583    pub async fn get_lightning_fee(&self, fed_id: String) -> Result<PaymentFee> {
584        let info_value = self.get_info().await?;
585        let federations = info_value["federations"]
586            .as_array()
587            .expect("federations is an array");
588
589        let fed = federations
590            .iter()
591            .find(|fed| {
592                serde_json::from_value::<String>(fed["federation_id"].clone())
593                    .expect("could not deserialize federation_id")
594                    == fed_id
595            })
596            .ok_or_else(|| anyhow!("Federation not found"))?;
597
598        let lightning_fee = fed["config"]["lightning_fee"].clone();
599        let base: Amount = serde_json::from_value(lightning_fee["base"].clone())
600            .map_err(|e| anyhow!("Couldnt parse base: {}", e))?;
601        let parts_per_million: u64 =
602            serde_json::from_value(lightning_fee["parts_per_million"].clone())
603                .map_err(|e| anyhow!("Couldnt parse parts_per_million: {}", e))?;
604
605        Ok(PaymentFee {
606            base,
607            parts_per_million,
608        })
609    }
610
611    pub async fn set_federation_routing_fee(
612        &self,
613        fed_id: String,
614        base: u64,
615        ppm: u64,
616    ) -> Result<()> {
617        cmd!(
618            self,
619            "cfg",
620            "set-fees",
621            "--federation-id",
622            fed_id,
623            "--ln-base",
624            base,
625            "--ln-ppm",
626            ppm
627        )
628        .run()
629        .await?;
630
631        Ok(())
632    }
633
634    pub async fn set_federation_transaction_fee(
635        &self,
636        fed_id: String,
637        base: u64,
638        ppm: u64,
639    ) -> Result<()> {
640        cmd!(
641            self,
642            "cfg",
643            "set-fees",
644            "--federation-id",
645            fed_id,
646            "--tx-base",
647            base,
648            "--tx-ppm",
649            ppm
650        )
651        .run()
652        .await?;
653
654        Ok(())
655    }
656
657    pub async fn payment_summary(&self) -> Result<PaymentSummaryResponse> {
658        let out_json = cmd!(self, "payment-summary").out_json().await?;
659        Ok(serde_json::from_value(out_json).expect("Could not deserialize PaymentSummaryResponse"))
660    }
661
662    pub async fn wait_bolt11_invoice(&self, payment_hash: Vec<u8>) -> Result<()> {
663        let payment_hash =
664            sha256::Hash::from_slice(&payment_hash).expect("Could not parse payment hash");
665        let invoice_val = cmd!(
666            self,
667            "lightning",
668            "get-invoice",
669            "--payment-hash",
670            payment_hash
671        )
672        .out_json()
673        .await?;
674        let invoice: GetInvoiceResponse =
675            serde_json::from_value(invoice_val).expect("Could not parse GetInvoiceResponse");
676        anyhow::ensure!(invoice.status == PaymentStatus::Succeeded);
677
678        Ok(())
679    }
680
681    pub async fn list_transactions(
682        &self,
683        start: SystemTime,
684        end: SystemTime,
685    ) -> Result<Vec<PaymentDetails>> {
686        let start_datetime: DateTime<Utc> = start.into();
687        let end_datetime: DateTime<Utc> = end.into();
688        let response = cmd!(
689            self,
690            "lightning",
691            "list-transactions",
692            "--start-time",
693            start_datetime.to_rfc3339(),
694            "--end-time",
695            end_datetime.to_rfc3339()
696        )
697        .out_json()
698        .await?;
699        let transactions = serde_json::from_value::<ListTransactionsResponse>(response)?;
700        Ok(transactions.transactions)
701    }
702
703    pub async fn create_offer(&self, amount: Option<Amount>) -> Result<String> {
704        let offer_value = if let Some(amount) = amount {
705            cmd!(
706                self,
707                "lightning",
708                "create-offer",
709                "--amount-msat",
710                amount.msats
711            )
712            .out_json()
713            .await?
714        } else {
715            cmd!(self, "lightning", "create-offer").out_json().await?
716        };
717        let offer_response = serde_json::from_value::<CreateOfferResponse>(offer_value)
718            .expect("Could not parse offer response");
719        Ok(offer_response.offer)
720    }
721
722    pub async fn pay_offer(&self, offer: String, amount: Option<Amount>) -> Result<()> {
723        if let Some(amount) = amount {
724            cmd!(
725                self,
726                "lightning",
727                "pay-offer",
728                "--offer",
729                offer,
730                "--amount-msat",
731                amount.msats
732            )
733            .run()
734            .await?;
735        } else {
736            cmd!(self, "lightning", "pay-offer", "--offer", offer)
737                .run()
738                .await?;
739        }
740
741        Ok(())
742    }
743}