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 list_channels(&self) -> Result<Vec<ChannelInfo>> {
436 let channels = cmd!(self, "lightning", "list-channels").out_json().await?;
437
438 let channels = channels
439 .as_array()
440 .context("channels must be an array")?
441 .iter()
442 .map(|channel| {
443 let remote_pubkey = channel["remote_pubkey"]
444 .as_str()
445 .context("remote_pubkey must be a string")?
446 .to_owned();
447 let channel_size_sats = channel["channel_size_sats"]
448 .as_u64()
449 .context("channel_size_sats must be a u64")?;
450 let outbound_liquidity_sats = channel["outbound_liquidity_sats"]
451 .as_u64()
452 .context("outbound_liquidity_sats must be a u64")?;
453 let inbound_liquidity_sats = channel["inbound_liquidity_sats"]
454 .as_u64()
455 .context("inbound_liquidity_sats must be a u64")?;
456 let is_active = channel["is_active"].as_bool().unwrap_or(true);
457 let funding_outpoint = channel.get("funding_outpoint").map(|v| {
458 serde_json::from_value::<bitcoin::OutPoint>(v.clone())
459 .expect("Could not deserialize outpoint")
460 });
461 let remote_node_alias = channel
462 .get("remote_node_alias")
463 .map(std::string::ToString::to_string);
464 let remote_address = channel
465 .get("remote_address")
466 .map(std::string::ToString::to_string);
467 Ok(ChannelInfo {
468 remote_pubkey: remote_pubkey
469 .parse()
470 .expect("Lightning node returned invalid remote channel pubkey"),
471 channel_size_sats,
472 outbound_liquidity_sats,
473 inbound_liquidity_sats,
474 is_active,
475 funding_outpoint,
476 remote_node_alias,
477 remote_address,
478 })
479 })
480 .collect::<Result<Vec<ChannelInfo>>>()?;
481 Ok(channels)
482 }
483
484 pub async fn wait_for_block_height(&self, target_block_height: u64) -> Result<()> {
485 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
486 poll("waiting for block height", || async {
487 let info = self.get_info().await.map_err(ControlFlow::Continue)?;
488
489 let height_value = if gateway_cli_version < *VERSION_0_10_0_ALPHA {
490 info["block_height"].clone()
491 } else {
492 info["lightning_info"]["connected"]["block_height"].clone()
493 };
494
495 let block_height: Option<u32> = serde_json::from_value(height_value)
496 .context("Could not parse block height")
497 .map_err(ControlFlow::Continue)?;
498 let Some(block_height) = block_height else {
499 return Err(ControlFlow::Continue(anyhow!("Not synced any blocks yet")));
500 };
501
502 let synced_value = if gateway_cli_version < *VERSION_0_10_0_ALPHA {
503 info["synced_to_chain"].clone()
504 } else {
505 info["lightning_info"]["connected"]["synced_to_chain"].clone()
506 };
507 let synced = synced_value
508 .as_bool()
509 .expect("Could not get synced_to_chain");
510 if block_height >= target_block_height as u32 && synced {
511 return Ok(());
512 }
513
514 Err(ControlFlow::Continue(anyhow!("Not synced to block")))
515 })
516 .await?;
517 Ok(())
518 }
519
520 pub async fn get_lightning_fee(&self, fed_id: String) -> Result<PaymentFee> {
521 let info_value = self.get_info().await?;
522 let federations = info_value["federations"]
523 .as_array()
524 .expect("federations is an array");
525
526 let fed = federations
527 .iter()
528 .find(|fed| {
529 serde_json::from_value::<String>(fed["federation_id"].clone())
530 .expect("could not deserialize federation_id")
531 == fed_id
532 })
533 .ok_or_else(|| anyhow!("Federation not found"))?;
534
535 let lightning_fee = fed["config"]["lightning_fee"].clone();
536 let base: Amount = serde_json::from_value(lightning_fee["base"].clone())
537 .map_err(|e| anyhow!("Couldnt parse base: {e}"))?;
538 let parts_per_million: u64 =
539 serde_json::from_value(lightning_fee["parts_per_million"].clone())
540 .map_err(|e| anyhow!("Couldnt parse parts_per_million: {e}"))?;
541
542 Ok(PaymentFee {
543 base,
544 parts_per_million,
545 })
546 }
547
548 pub async fn set_federation_routing_fee(
549 &self,
550 fed_id: String,
551 base: u64,
552 ppm: u64,
553 ) -> Result<()> {
554 cmd!(
555 self,
556 "cfg",
557 "set-fees",
558 "--federation-id",
559 fed_id,
560 "--ln-base",
561 base,
562 "--ln-ppm",
563 ppm
564 )
565 .run()
566 .await?;
567
568 Ok(())
569 }
570
571 pub async fn set_federation_transaction_fee(
572 &self,
573 fed_id: String,
574 base: u64,
575 ppm: u64,
576 ) -> Result<()> {
577 cmd!(
578 self,
579 "cfg",
580 "set-fees",
581 "--federation-id",
582 fed_id,
583 "--tx-base",
584 base,
585 "--tx-ppm",
586 ppm
587 )
588 .run()
589 .await?;
590
591 Ok(())
592 }
593
594 pub async fn payment_summary(&self) -> Result<PaymentSummaryResponse> {
595 let out_json = cmd!(self, "payment-summary").out_json().await?;
596 Ok(serde_json::from_value(out_json).expect("Could not deserialize PaymentSummaryResponse"))
597 }
598
599 pub async fn wait_bolt11_invoice(&self, payment_hash: Vec<u8>) -> Result<()> {
600 let payment_hash =
601 sha256::Hash::from_slice(&payment_hash).expect("Could not parse payment hash");
602 let invoice_val = cmd!(
603 self,
604 "lightning",
605 "get-invoice",
606 "--payment-hash",
607 payment_hash
608 )
609 .out_json()
610 .await?;
611 let invoice: GetInvoiceResponse =
612 serde_json::from_value(invoice_val).expect("Could not parse GetInvoiceResponse");
613 anyhow::ensure!(invoice.status == PaymentStatus::Succeeded);
614
615 Ok(())
616 }
617
618 pub async fn list_transactions(
619 &self,
620 start: SystemTime,
621 end: SystemTime,
622 ) -> Result<Vec<PaymentDetails>> {
623 let start_datetime: DateTime<Utc> = start.into();
624 let end_datetime: DateTime<Utc> = end.into();
625 let response = cmd!(
626 self,
627 "lightning",
628 "list-transactions",
629 "--start-time",
630 start_datetime.to_rfc3339(),
631 "--end-time",
632 end_datetime.to_rfc3339()
633 )
634 .out_json()
635 .await?;
636 let transactions = serde_json::from_value::<ListTransactionsResponse>(response)?;
637 Ok(transactions.transactions)
638 }
639
640 pub async fn create_offer(&self, amount: Option<Amount>) -> Result<String> {
641 let offer_value = if let Some(amount) = amount {
642 cmd!(
643 self,
644 "lightning",
645 "create-offer",
646 "--amount-msat",
647 amount.msats
648 )
649 .out_json()
650 .await?
651 } else {
652 cmd!(self, "lightning", "create-offer").out_json().await?
653 };
654 let offer_response = serde_json::from_value::<CreateOfferResponse>(offer_value)
655 .expect("Could not parse offer response");
656 Ok(offer_response.offer)
657 }
658
659 pub async fn pay_offer(&self, offer: String, amount: Option<Amount>) -> Result<()> {
660 if let Some(amount) = amount {
661 cmd!(
662 self,
663 "lightning",
664 "pay-offer",
665 "--offer",
666 offer,
667 "--amount-msat",
668 amount.msats
669 )
670 .run()
671 .await?;
672 } else {
673 cmd!(self, "lightning", "pay-offer", "--offer", offer)
674 .run()
675 .await?;
676 }
677
678 Ok(())
679 }
680
681 pub async fn send_onchain(
682 &self,
683 bitcoind: &Bitcoind,
684 amount: BitcoinAmountOrAll,
685 fee_rate: u64,
686 ) -> Result<bitcoin::Txid> {
687 let withdraw_address = bitcoind.get_new_address().await?;
688 let value = cmd!(
689 self,
690 "onchain",
691 "send",
692 "--address",
693 withdraw_address,
694 "--amount",
695 amount,
696 "--fee-rate-sats-per-vbyte",
697 fee_rate
698 )
699 .out_json()
700 .await?;
701
702 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
703 let txid: bitcoin::Txid = if gateway_cli_version >= *VERSION_0_11_0_ALPHA {
704 serde_json::from_value(value["txid"].clone())?
706 } else {
707 serde_json::from_value(value)?
709 };
710 Ok(txid)
711 }
712
713 pub async fn pegout(
714 &self,
715 fed_id: String,
716 amount: u64,
717 address: Address,
718 ) -> Result<WithdrawResponse> {
719 let value = cmd!(
720 self,
721 "ecash",
722 "pegout",
723 "--federation-id",
724 fed_id,
725 "--amount",
726 amount,
727 "--address",
728 address
729 )
730 .out_json()
731 .await?;
732 Ok(serde_json::from_value(value)?)
733 }
734}
735
736#[derive(Clone)]
737pub struct Gatewayd {
738 pub(crate) process: ProcessHandle,
739 pub ln: LightningNode,
740 pub addr: String,
741 pub(crate) lightning_node_addr: String,
742 pub gatewayd_version: Version,
743 pub gw_name: String,
744 pub log_path: PathBuf,
745 pub gw_port: u16,
746 pub ldk_port: u16,
747 pub metrics_port: u16,
748 pub gateway_id: String,
749 pub iroh_gateway_id: Option<String>,
750 pub iroh_port: u16,
751 pub node_id: iroh_base::NodeId,
752 pub gateway_index: usize,
753}
754
755impl Gatewayd {
756 pub async fn new(
757 process_mgr: &ProcessManager,
758 ln: LightningNode,
759 gateway_index: usize,
760 ) -> Result<Self> {
761 let ln_type = ln.ln_type();
762 let (gw_name, port, lightning_node_port, metrics_port) = match &ln {
763 LightningNode::Lnd(_) => (
764 "gatewayd-lnd".to_string(),
765 process_mgr.globals.FM_PORT_GW_LND,
766 process_mgr.globals.FM_PORT_LND_LISTEN,
767 process_mgr.globals.FM_PORT_GW_LND_METRICS,
768 ),
769 LightningNode::Ldk {
770 name,
771 gw_port,
772 ldk_port,
773 metrics_port,
774 } => (
775 name.to_owned(),
776 gw_port.to_owned(),
777 ldk_port.to_owned(),
778 metrics_port.to_owned(),
779 ),
780 };
781 let test_dir = &process_mgr.globals.FM_TEST_DIR;
782 let addr = format!("http://127.0.0.1:{port}/{V1_API_ENDPOINT}");
783 let lightning_node_addr = format!("127.0.0.1:{lightning_node_port}");
784 let iroh_endpoint = process_mgr
785 .globals
786 .gatewayd_overrides
787 .gateway_iroh_endpoints
788 .get(gateway_index)
789 .expect("No gateway for index");
790
791 let mut gateway_env: HashMap<String, String> = HashMap::from_iter([
792 (
793 FM_GATEWAY_DATA_DIR_ENV.to_owned(),
794 format!("{}/{gw_name}", utf8(test_dir)),
795 ),
796 (
797 FM_GATEWAY_LISTEN_ADDR_ENV.to_owned(),
798 format!("127.0.0.1:{port}"),
799 ),
800 (FM_GATEWAY_API_ADDR_ENV.to_owned(), addr.clone()),
801 (FM_PORT_LDK_ENV.to_owned(), lightning_node_port.to_string()),
802 (
803 FM_GATEWAY_IROH_LISTEN_ADDR_ENV.to_owned(),
804 format!("127.0.0.1:{}", iroh_endpoint.port()),
805 ),
806 (
807 FM_GATEWAY_IROH_SECRET_KEY_OVERRIDE_ENV.to_owned(),
808 iroh_endpoint.secret_key(),
809 ),
810 (
811 FM_GATEWAY_METRICS_LISTEN_ADDR_ENV.to_owned(),
812 format!("127.0.0.1:{metrics_port}"),
813 ),
814 ]);
815
816 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
817
818 if ln_type == LightningNodeType::Ldk {
819 gateway_env.insert("FM_LDK_ALIAS".to_owned(), gw_name.clone());
820 }
821
822 let process = process_mgr
823 .spawn_daemon(
824 &gw_name,
825 cmd!(crate::util::Gatewayd, ln_type).envs(gateway_env),
826 )
827 .await?;
828
829 let timeout = if is_env_var_set(FM_PRE_DKG_ENV) {
830 Duration::from_secs(300)
831 } else {
832 Duration::from_secs(60)
833 };
834 let (gateway_id, iroh_gateway_id) = poll_with_timeout(
835 "waiting for gateway to be ready to respond to rpc",
836 timeout,
837 || async {
838 let info = cmd!(
840 crate::util::get_gateway_cli_path(),
841 "--rpcpassword",
842 "theresnosecondbest",
843 "-a",
844 addr,
845 "info"
846 )
847 .out_json()
848 .await
849 .map_err(ControlFlow::Continue)?;
850 let (gateway_id, iroh_gateway_id) = if gatewayd_version < *VERSION_0_10_0_ALPHA {
851 let gateway_id = info["gateway_id"]
852 .as_str()
853 .context("gateway_id must be a string")
854 .map_err(ControlFlow::Break)?
855 .to_owned();
856 (gateway_id, None)
857 } else {
858 let gateway_id = info["registrations"]["http"][1]
859 .as_str()
860 .context("gateway id must be a string")
861 .map_err(ControlFlow::Break)?
862 .to_owned();
863 let iroh_gateway_id = info["registrations"]["iroh"][1]
864 .as_str()
865 .context("gateway id must be a string")
866 .map_err(ControlFlow::Break)?
867 .to_owned();
868 (gateway_id, Some(iroh_gateway_id))
869 };
870
871 Ok((gateway_id, iroh_gateway_id))
872 },
873 )
874 .await?;
875
876 let log_path = process_mgr
877 .globals
878 .FM_LOGS_DIR
879 .join(format!("{gw_name}.log"));
880 let gatewayd = Self {
881 process,
882 ln,
883 addr,
884 lightning_node_addr,
885 gatewayd_version,
886 gw_name,
887 log_path,
888 gw_port: port,
889 ldk_port: lightning_node_port,
890 metrics_port,
891 gateway_id,
892 iroh_gateway_id,
893 iroh_port: iroh_endpoint.port(),
894 node_id: iroh_endpoint.node_id(),
895 gateway_index,
896 };
897
898 Ok(gatewayd)
899 }
900
901 pub async fn terminate(self) -> Result<()> {
902 self.process.terminate().await
903 }
904
905 pub fn set_lightning_node(&mut self, ln_node: LightningNode) {
906 self.ln = ln_node;
907 }
908
909 pub async fn stop_lightning_node(&mut self) -> Result<()> {
910 info!(target: LOG_DEVIMINT, "Stopping lightning node");
911 match self.ln.clone() {
912 LightningNode::Lnd(lnd) => lnd.terminate().await,
913 LightningNode::Ldk {
914 name: _,
915 gw_port: _,
916 ldk_port: _,
917 metrics_port: _,
918 } => {
919 unimplemented!("LDK node termination not implemented")
922 }
923 }
924 }
925
926 pub async fn restart_with_bin(
929 &mut self,
930 process_mgr: &ProcessManager,
931 gatewayd_path: &PathBuf,
932 gateway_cli_path: &PathBuf,
933 ) -> Result<()> {
934 let ln = self.ln.clone();
935
936 self.process.terminate().await?;
937 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", gatewayd_path) };
939 unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", gateway_cli_path) };
941
942 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
943 let new_ln = ln;
944 let new_gw = Self::new(process_mgr, new_ln.clone(), self.gateway_index).await?;
945 self.process = new_gw.process;
946 self.set_lightning_node(new_ln);
947 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
948 info!(
949 target: LOG_DEVIMINT,
950 ?gatewayd_version,
951 ?gateway_cli_version,
952 "upgraded gatewayd and gateway-cli"
953 );
954 Ok(())
955 }
956
957 pub fn client(&self) -> GatewayClient {
958 GatewayClient::new(self)
959 }
960}