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 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 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 unimplemented!("LDK node termination not implemented")
240 }
241 }
242 }
243
244 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 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", gatewayd_path) };
257 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 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 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}