Skip to main content

devimint/
gatewayd.rs

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