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