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 payment_log(
200 &self,
201 fed_id: &str,
202 event_kinds: &[&str],
203 pagination_size: usize,
204 ) -> Result<serde_json::Value> {
205 cmd!(
206 self,
207 "payment-log",
208 "--federation-id={fed_id}",
209 "--pagination-size={pagination_size}"
210 )
211 .args(event_kinds.iter().flat_map(|kind| ["--event-kinds", *kind]))
212 .out_json()
213 .await
214 }
215
216 pub async fn get_ln_onchain_address(&self) -> Result<String> {
217 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
218 if gateway_cli_version >= *VERSION_0_11_0_ALPHA {
219 let value = cmd!(self, "onchain", "address").out_json().await?;
221 Ok(value["address"]
222 .as_str()
223 .context("address must be a string")?
224 .to_owned())
225 } else {
226 cmd!(self, "onchain", "address").out_string().await
228 }
229 }
230
231 pub async fn get_mnemonic(&self) -> Result<MnemonicResponse> {
232 let value = retry(
233 "Getting gateway mnemonic",
234 backoff_util::aggressive_backoff(),
235 || async { cmd!(self, "seed").out_json().await },
236 )
237 .await
238 .context("Getting gateway mnemonic")?;
239
240 Ok(serde_json::from_value(value)?)
241 }
242
243 pub async fn leave_federation(&self, federation_id: FederationId) -> Result<serde_json::Value> {
244 let fed_info = cmd!(self, "leave-fed", "--federation-id", federation_id)
245 .out_json()
246 .await?;
247 Ok(fed_info)
248 }
249
250 pub async fn create_invoice(&self, amount_msats: u64) -> Result<Bolt11Invoice> {
251 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
252 let invoice_str = if gateway_cli_version >= *VERSION_0_11_0_ALPHA {
253 let value = cmd!(self, "lightning", "create-invoice", amount_msats)
255 .out_json()
256 .await?;
257 value["invoice"]
258 .as_str()
259 .context("invoice must be a string")?
260 .to_owned()
261 } else {
262 cmd!(self, "lightning", "create-invoice", amount_msats)
264 .out_string()
265 .await?
266 };
267 Ok(Bolt11Invoice::from_str(&invoice_str)?)
268 }
269
270 pub async fn pay_invoice(&self, invoice: Bolt11Invoice) -> Result<()> {
271 cmd!(self, "lightning", "pay-invoice", invoice.to_string())
272 .run()
273 .await?;
274
275 Ok(())
276 }
277
278 pub async fn send_ecash(&self, federation_id: String, amount_msats: u64) -> Result<String> {
279 let value = cmd!(
280 self,
281 "ecash",
282 "send",
283 "--federation-id",
284 federation_id,
285 amount_msats
286 )
287 .out_json()
288 .await?;
289 let ecash: String = serde_json::from_value(
290 value
291 .get("notes")
292 .expect("notes key does not exist")
293 .clone(),
294 )?;
295 Ok(ecash)
296 }
297
298 pub async fn receive_ecash(&self, ecash: String) -> Result<()> {
299 cmd!(self, "ecash", "receive", "--notes", ecash)
300 .run()
301 .await?;
302 Ok(())
303 }
304
305 pub async fn get_balances(&self) -> Result<GatewayBalances> {
306 let value = cmd!(self, "get-balances").out_json().await?;
307 Ok(serde_json::from_value(value)?)
308 }
309
310 pub async fn ecash_balance(&self, federation_id: String) -> anyhow::Result<u64> {
311 let federation_id = FederationId::from_str(&federation_id)?;
312 let balances = self.get_balances().await?;
313 let ecash_balance = balances
314 .ecash_balances
315 .into_iter()
316 .find(|info| info.federation_id == federation_id)
317 .ok_or(anyhow::anyhow!("Gateway is not joined to federation"))?
318 .ecash_balance_msats
319 .msats;
320 Ok(ecash_balance)
321 }
322
323 pub async fn close_channel(&self, remote_pubkey: PublicKey, force: bool) -> Result<()> {
324 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
325 let mut close_channel = if force {
326 cmd!(
327 self,
328 "lightning",
329 "close-channels-with-peer",
330 "--pubkey",
331 remote_pubkey,
332 "--force",
333 )
334 } else if gateway_cli_version < *VERSION_0_10_0_ALPHA {
335 cmd!(
336 self,
337 "lightning",
338 "close-channels-with-peer",
339 "--pubkey",
340 remote_pubkey,
341 )
342 } else {
343 cmd!(
344 self,
345 "lightning",
346 "close-channels-with-peer",
347 "--pubkey",
348 remote_pubkey,
349 "--sats-per-vbyte",
350 "10",
351 )
352 };
353
354 close_channel.run().await?;
355
356 Ok(())
357 }
358
359 pub async fn close_all_channels_no_wait(&self, force: bool) -> Result<()> {
363 let channels = self.list_channels().await?;
364
365 for chan in channels {
366 let remote_pubkey = chan.remote_pubkey;
367 self.close_channel(remote_pubkey, force).await?;
368 }
369
370 Ok(())
371 }
372
373 pub async fn close_all_channels(&self, force: bool, timeout: Duration) -> Result<()> {
378 let channels = self.list_channels().await?;
379 let closing_peers: HashSet<_> = channels.iter().map(|chan| chan.remote_pubkey).collect();
380
381 for chan in channels {
382 self.close_channel(chan.remote_pubkey, force).await?;
383 }
384
385 poll_with_timeout("waiting for channels to close", timeout, || async {
386 let channels = self.list_channels().await.map_err(ControlFlow::Continue)?;
387 if channels
388 .iter()
389 .any(|chan| closing_peers.contains(&chan.remote_pubkey) && chan.is_active)
390 {
391 return Err(ControlFlow::Continue(anyhow::anyhow!(
392 "Some channels are still active"
393 )));
394 }
395 Ok(())
396 })
397 .await
398 }
399
400 pub async fn open_channel(
403 &self,
404 gw: &Gatewayd,
405 channel_size_sats: u64,
406 push_amount_sats: Option<u64>,
407 ) -> Result<Txid> {
408 let pubkey = gw.client().lightning_pubkey().await?;
409 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
410
411 let txid_str = if gateway_cli_version >= *VERSION_0_11_0_ALPHA {
412 let value = cmd!(
414 self,
415 "lightning",
416 "open-channel",
417 "--pubkey",
418 pubkey,
419 "--host",
420 gw.lightning_node_addr,
421 "--channel-size-sats",
422 channel_size_sats,
423 "--push-amount-sats",
424 push_amount_sats.unwrap_or(0)
425 )
426 .out_json()
427 .await?;
428
429 value["funding_txid"]
430 .as_str()
431 .context("funding_txid must be a string")?
432 .to_owned()
433 } else {
434 cmd!(
436 self,
437 "lightning",
438 "open-channel",
439 "--pubkey",
440 pubkey,
441 "--host",
442 gw.lightning_node_addr,
443 "--channel-size-sats",
444 channel_size_sats,
445 "--push-amount-sats",
446 push_amount_sats.unwrap_or(0)
447 )
448 .out_string()
449 .await?
450 };
451
452 Ok(Txid::from_str(&txid_str)?)
453 }
454
455 pub async fn set_channel_fees(
456 &self,
457 funding_outpoint: bitcoin::OutPoint,
458 base_fee_msat: u64,
459 parts_per_million: u64,
460 ) -> Result<()> {
461 cmd!(
462 self,
463 "lightning",
464 "set-channel-fees",
465 "--funding-outpoint",
466 funding_outpoint,
467 "--base-fee-msat",
468 base_fee_msat,
469 "--parts-per-million",
470 parts_per_million,
471 )
472 .run()
473 .await?;
474 Ok(())
475 }
476
477 pub async fn list_channels(&self) -> Result<Vec<ChannelInfo>> {
478 let channels = cmd!(self, "lightning", "list-channels").out_json().await?;
479
480 let channels = channels
481 .as_array()
482 .context("channels must be an array")?
483 .iter()
484 .map(|channel| {
485 let remote_pubkey = channel["remote_pubkey"]
486 .as_str()
487 .context("remote_pubkey must be a string")?
488 .to_owned();
489 let channel_size_sats = channel["channel_size_sats"]
490 .as_u64()
491 .context("channel_size_sats must be a u64")?;
492 let outbound_liquidity_sats = channel["outbound_liquidity_sats"]
493 .as_u64()
494 .context("outbound_liquidity_sats must be a u64")?;
495 let inbound_liquidity_sats = channel["inbound_liquidity_sats"]
496 .as_u64()
497 .context("inbound_liquidity_sats must be a u64")?;
498 let is_active = channel["is_active"].as_bool().unwrap_or(true);
499 let funding_outpoint = channel.get("funding_outpoint").map(|v| {
500 serde_json::from_value::<bitcoin::OutPoint>(v.clone())
501 .expect("Could not deserialize outpoint")
502 });
503 let remote_node_alias = channel
504 .get("remote_node_alias")
505 .map(std::string::ToString::to_string);
506 let remote_address = channel
507 .get("remote_address")
508 .map(std::string::ToString::to_string);
509 let base_fee_msat = channel
510 .get("base_fee_msat")
511 .and_then(serde_json::Value::as_u64);
512 let parts_per_million = channel
513 .get("parts_per_million")
514 .and_then(serde_json::Value::as_u64);
515 Ok(ChannelInfo {
516 remote_pubkey: remote_pubkey
517 .parse()
518 .expect("Lightning node returned invalid remote channel pubkey"),
519 channel_size_sats,
520 outbound_liquidity_sats,
521 inbound_liquidity_sats,
522 is_active,
523 funding_outpoint,
524 remote_node_alias,
525 remote_address,
526 base_fee_msat,
527 parts_per_million,
528 })
529 })
530 .collect::<Result<Vec<ChannelInfo>>>()?;
531 Ok(channels)
532 }
533
534 pub async fn wait_for_block_height(&self, target_block_height: u64) -> Result<()> {
535 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
536 poll("waiting for block height", || async {
537 let info = self.get_info().await.map_err(ControlFlow::Continue)?;
538
539 let height_value = if gateway_cli_version < *VERSION_0_10_0_ALPHA {
540 info["block_height"].clone()
541 } else {
542 info["lightning_info"]["connected"]["block_height"].clone()
543 };
544
545 let block_height: Option<u32> = serde_json::from_value(height_value)
546 .context("Could not parse block height")
547 .map_err(ControlFlow::Continue)?;
548 let Some(block_height) = block_height else {
549 return Err(ControlFlow::Continue(anyhow!("Not synced any blocks yet")));
550 };
551
552 let synced_value = if gateway_cli_version < *VERSION_0_10_0_ALPHA {
553 info["synced_to_chain"].clone()
554 } else {
555 info["lightning_info"]["connected"]["synced_to_chain"].clone()
556 };
557 let synced = synced_value
558 .as_bool()
559 .expect("Could not get synced_to_chain");
560 if block_height >= target_block_height as u32 && synced {
561 return Ok(());
562 }
563
564 Err(ControlFlow::Continue(anyhow!("Not synced to block")))
565 })
566 .await?;
567 Ok(())
568 }
569
570 pub async fn get_lightning_fee(&self, fed_id: String) -> Result<PaymentFee> {
571 let info_value = self.get_info().await?;
572 let federations = info_value["federations"]
573 .as_array()
574 .expect("federations is an array");
575
576 let fed = federations
577 .iter()
578 .find(|fed| {
579 serde_json::from_value::<String>(fed["federation_id"].clone())
580 .expect("could not deserialize federation_id")
581 == fed_id
582 })
583 .ok_or_else(|| anyhow!("Federation not found"))?;
584
585 let lightning_fee = fed["config"]["lightning_fee"].clone();
586 let base: Amount = serde_json::from_value(lightning_fee["base"].clone())
587 .map_err(|e| anyhow!("Couldnt parse base: {e}"))?;
588 let parts_per_million: u64 =
589 serde_json::from_value(lightning_fee["parts_per_million"].clone())
590 .map_err(|e| anyhow!("Couldnt parse parts_per_million: {e}"))?;
591
592 Ok(PaymentFee {
593 base,
594 parts_per_million,
595 })
596 }
597
598 pub async fn set_federation_routing_fee(
599 &self,
600 fed_id: String,
601 base: u64,
602 ppm: u64,
603 ) -> Result<()> {
604 cmd!(
605 self,
606 "cfg",
607 "set-fees",
608 "--federation-id",
609 fed_id,
610 "--ln-base",
611 base,
612 "--ln-ppm",
613 ppm
614 )
615 .run()
616 .await?;
617
618 Ok(())
619 }
620
621 pub async fn set_federation_transaction_fee(
622 &self,
623 fed_id: String,
624 base: u64,
625 ppm: u64,
626 ) -> Result<()> {
627 cmd!(
628 self,
629 "cfg",
630 "set-fees",
631 "--federation-id",
632 fed_id,
633 "--tx-base",
634 base,
635 "--tx-ppm",
636 ppm
637 )
638 .run()
639 .await?;
640
641 Ok(())
642 }
643
644 pub async fn payment_summary(&self) -> Result<PaymentSummaryResponse> {
645 let out_json = cmd!(self, "payment-summary").out_json().await?;
646 Ok(serde_json::from_value(out_json).expect("Could not deserialize PaymentSummaryResponse"))
647 }
648
649 pub async fn wait_bolt11_invoice(&self, payment_hash: Vec<u8>) -> Result<()> {
650 let payment_hash =
651 sha256::Hash::from_slice(&payment_hash).expect("Could not parse payment hash");
652 let invoice_val = cmd!(
653 self,
654 "lightning",
655 "get-invoice",
656 "--payment-hash",
657 payment_hash
658 )
659 .out_json()
660 .await?;
661 let invoice: GetInvoiceResponse =
662 serde_json::from_value(invoice_val).expect("Could not parse GetInvoiceResponse");
663 anyhow::ensure!(invoice.status == PaymentStatus::Succeeded);
664
665 Ok(())
666 }
667
668 pub async fn list_transactions(
669 &self,
670 start: SystemTime,
671 end: SystemTime,
672 ) -> Result<Vec<PaymentDetails>> {
673 let start_datetime: DateTime<Utc> = start.into();
674 let end_datetime: DateTime<Utc> = end.into();
675 let response = cmd!(
676 self,
677 "lightning",
678 "list-transactions",
679 "--start-time",
680 start_datetime.to_rfc3339(),
681 "--end-time",
682 end_datetime.to_rfc3339()
683 )
684 .out_json()
685 .await?;
686 let transactions = serde_json::from_value::<ListTransactionsResponse>(response)?;
687 Ok(transactions.transactions)
688 }
689
690 pub async fn create_offer(&self, amount: Option<Amount>) -> Result<String> {
691 let offer_value = if let Some(amount) = amount {
692 cmd!(
693 self,
694 "lightning",
695 "create-offer",
696 "--amount-msat",
697 amount.msats
698 )
699 .out_json()
700 .await?
701 } else {
702 cmd!(self, "lightning", "create-offer").out_json().await?
703 };
704 let offer_response = serde_json::from_value::<CreateOfferResponse>(offer_value)
705 .expect("Could not parse offer response");
706 Ok(offer_response.offer)
707 }
708
709 pub async fn pay_offer(&self, offer: String, amount: Option<Amount>) -> Result<()> {
710 if let Some(amount) = amount {
711 cmd!(
712 self,
713 "lightning",
714 "pay-offer",
715 "--offer",
716 offer,
717 "--amount-msat",
718 amount.msats
719 )
720 .run()
721 .await?;
722 } else {
723 cmd!(self, "lightning", "pay-offer", "--offer", offer)
724 .run()
725 .await?;
726 }
727
728 Ok(())
729 }
730
731 pub async fn send_onchain(
732 &self,
733 bitcoind: &Bitcoind,
734 amount: BitcoinAmountOrAll,
735 fee_rate: u64,
736 ) -> Result<bitcoin::Txid> {
737 let withdraw_address = bitcoind.get_new_address().await?;
738 let value = cmd!(
739 self,
740 "onchain",
741 "send",
742 "--address",
743 withdraw_address,
744 "--amount",
745 amount,
746 "--fee-rate-sats-per-vbyte",
747 fee_rate
748 )
749 .out_json()
750 .await?;
751
752 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
753 let txid: bitcoin::Txid = if gateway_cli_version >= *VERSION_0_11_0_ALPHA {
754 serde_json::from_value(value["txid"].clone())?
756 } else {
757 serde_json::from_value(value)?
759 };
760 Ok(txid)
761 }
762
763 pub async fn pegout(
764 &self,
765 fed_id: String,
766 amount: u64,
767 address: Address,
768 ) -> Result<WithdrawResponse> {
769 let value = cmd!(
770 self,
771 "ecash",
772 "pegout",
773 "--federation-id",
774 fed_id,
775 "--amount",
776 amount,
777 "--address",
778 address
779 )
780 .out_json()
781 .await?;
782 Ok(serde_json::from_value(value)?)
783 }
784
785 pub async fn pegout_all(&self, fed_id: String, address: Address) -> Result<WithdrawResponse> {
788 let value = cmd!(
789 self,
790 "ecash",
791 "pegout",
792 "--federation-id",
793 fed_id,
794 "--amount",
795 "all",
796 "--address",
797 address
798 )
799 .out_json()
800 .await?;
801 Ok(serde_json::from_value(value)?)
802 }
803}
804
805#[derive(Clone)]
806pub struct Gatewayd {
807 pub(crate) process: ProcessHandle,
808 pub ln: LightningNode,
809 pub addr: String,
810 pub(crate) lightning_node_addr: String,
811 pub gatewayd_version: Version,
812 pub gw_name: String,
813 pub log_path: PathBuf,
814 pub gw_port: u16,
815 pub ldk_port: u16,
816 pub metrics_port: u16,
817 pub gateway_id: String,
818 pub iroh_gateway_id: Option<String>,
819 pub iroh_port: u16,
820 pub node_id: iroh_base::NodeId,
821 pub gateway_index: usize,
822}
823
824impl Gatewayd {
825 pub async fn new(
826 process_mgr: &ProcessManager,
827 ln: LightningNode,
828 gateway_index: usize,
829 ) -> Result<Self> {
830 let ln_type = ln.ln_type();
831 let (gw_name, port, lightning_node_port, metrics_port) = match &ln {
832 LightningNode::Lnd(_) => (
833 "gatewayd-lnd".to_string(),
834 process_mgr.globals.FM_PORT_GW_LND,
835 process_mgr.globals.FM_PORT_LND_LISTEN,
836 process_mgr.globals.FM_PORT_GW_LND_METRICS,
837 ),
838 LightningNode::Ldk {
839 name,
840 gw_port,
841 ldk_port,
842 metrics_port,
843 } => (
844 name.to_owned(),
845 gw_port.to_owned(),
846 ldk_port.to_owned(),
847 metrics_port.to_owned(),
848 ),
849 };
850 let test_dir = &process_mgr.globals.FM_TEST_DIR;
851 let addr = format!("http://127.0.0.1:{port}/{V1_API_ENDPOINT}");
852 let lightning_node_addr = format!("127.0.0.1:{lightning_node_port}");
853 let iroh_endpoint = process_mgr
854 .globals
855 .gatewayd_overrides
856 .gateway_iroh_endpoints
857 .get(gateway_index)
858 .expect("No gateway for index");
859
860 let mut gateway_env: HashMap<String, String> = HashMap::from_iter([
861 (
862 FM_GATEWAY_DATA_DIR_ENV.to_owned(),
863 format!("{}/{gw_name}", utf8(test_dir)),
864 ),
865 (
866 FM_GATEWAY_LISTEN_ADDR_ENV.to_owned(),
867 format!("127.0.0.1:{port}"),
868 ),
869 (FM_GATEWAY_API_ADDR_ENV.to_owned(), addr.clone()),
870 (FM_PORT_LDK_ENV.to_owned(), lightning_node_port.to_string()),
871 (
872 FM_GATEWAY_IROH_LISTEN_ADDR_ENV.to_owned(),
873 format!("127.0.0.1:{}", iroh_endpoint.port()),
874 ),
875 (
876 FM_GATEWAY_IROH_SECRET_KEY_OVERRIDE_ENV.to_owned(),
877 iroh_endpoint.secret_key(),
878 ),
879 (
880 FM_GATEWAY_METRICS_LISTEN_ADDR_ENV.to_owned(),
881 format!("127.0.0.1:{metrics_port}"),
882 ),
883 ]);
884
885 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
886
887 if ln_type == LightningNodeType::Ldk {
888 gateway_env.insert("FM_LDK_ALIAS".to_owned(), gw_name.clone());
889 }
890
891 let process = process_mgr
897 .spawn_daemon(
898 &gw_name,
899 cmd!(crate::util::Gatewayd, ln_type).envs(gateway_env),
900 )
901 .await?;
902
903 let timeout = if is_env_var_set(FM_PRE_DKG_ENV) {
904 Duration::from_secs(300)
905 } else {
906 Duration::from_secs(60)
907 };
908 let (gateway_id, iroh_gateway_id) = poll_with_timeout(
909 "waiting for gateway to be ready to respond to rpc",
910 timeout,
911 || async {
912 let info = cmd!(
914 crate::util::get_gateway_cli_path(),
915 "--rpcpassword",
916 "theresnosecondbest",
917 "-a",
918 addr,
919 "info"
920 )
921 .out_json()
922 .await
923 .map_err(ControlFlow::Continue)?;
924 let (gateway_id, iroh_gateway_id) = if gatewayd_version < *VERSION_0_10_0_ALPHA {
925 let gateway_id = info["gateway_id"]
926 .as_str()
927 .context("gateway_id must be a string")
928 .map_err(ControlFlow::Break)?
929 .to_owned();
930 (gateway_id, None)
931 } else {
932 let gateway_id = info["registrations"]["http"][1]
933 .as_str()
934 .context("gateway id must be a string")
935 .map_err(ControlFlow::Break)?
936 .to_owned();
937 let iroh_gateway_id = info["registrations"]["iroh"][1]
938 .as_str()
939 .context("gateway id must be a string")
940 .map_err(ControlFlow::Break)?
941 .to_owned();
942 (gateway_id, Some(iroh_gateway_id))
943 };
944
945 Ok((gateway_id, iroh_gateway_id))
946 },
947 )
948 .await?;
949
950 let log_path = process_mgr
951 .globals
952 .FM_LOGS_DIR
953 .join(format!("{gw_name}.log"));
954 let gatewayd = Self {
955 process,
956 ln,
957 addr,
958 lightning_node_addr,
959 gatewayd_version,
960 gw_name,
961 log_path,
962 gw_port: port,
963 ldk_port: lightning_node_port,
964 metrics_port,
965 gateway_id,
966 iroh_gateway_id,
967 iroh_port: iroh_endpoint.port(),
968 node_id: iroh_endpoint.node_id(),
969 gateway_index,
970 };
971
972 Ok(gatewayd)
973 }
974
975 pub async fn terminate(self) -> Result<()> {
976 self.process.terminate().await
977 }
978
979 pub fn set_lightning_node(&mut self, ln_node: LightningNode) {
980 self.ln = ln_node;
981 }
982
983 pub async fn stop_lightning_node(&mut self) -> Result<()> {
984 info!(target: LOG_DEVIMINT, "Stopping lightning node");
985 match self.ln.clone() {
986 LightningNode::Lnd(lnd) => lnd.terminate().await,
987 LightningNode::Ldk {
988 name: _,
989 gw_port: _,
990 ldk_port: _,
991 metrics_port: _,
992 } => {
993 unimplemented!("LDK node termination not implemented")
996 }
997 }
998 }
999
1000 pub async fn restart_with_bin(
1003 &mut self,
1004 process_mgr: &ProcessManager,
1005 gatewayd_path: &PathBuf,
1006 gateway_cli_path: &PathBuf,
1007 ) -> Result<()> {
1008 let ln = self.ln.clone();
1009
1010 self.process.terminate().await?;
1011 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", gatewayd_path) };
1013 unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", gateway_cli_path) };
1015
1016 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
1017 let new_ln = ln;
1018 let new_gw = Self::new(process_mgr, new_ln.clone(), self.gateway_index).await?;
1019 self.process = new_gw.process;
1020 self.set_lightning_node(new_ln);
1021 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
1022 info!(
1023 target: LOG_DEVIMINT,
1024 ?gatewayd_version,
1025 ?gateway_cli_version,
1026 "upgraded gatewayd and gateway-cli"
1027 );
1028 Ok(())
1029 }
1030
1031 pub fn client(&self) -> GatewayClient {
1032 GatewayClient::new(self)
1033 }
1034}