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