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