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