Skip to main content

devimint/
gatewayd.rs

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