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 *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 unimplemented!("LDK node termination not implemented")
196 }
197 }
198 }
199
200 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 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", gatewayd_path) };
213 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 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 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 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 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}