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