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