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