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