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