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