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    /// Query the gateway's payment log for `fed_id`, filtered to
197    /// `event_kinds`, returning the newest `pagination_size` matching entries
198    /// as raw JSON.
199    pub async fn payment_log(
200        &self,
201        fed_id: &str,
202        event_kinds: &[&str],
203        pagination_size: usize,
204    ) -> Result<serde_json::Value> {
205        cmd!(
206            self,
207            "payment-log",
208            "--federation-id={fed_id}",
209            "--pagination-size={pagination_size}"
210        )
211        .args(event_kinds.iter().flat_map(|kind| ["--event-kinds", *kind]))
212        .out_json()
213        .await
214    }
215
216    pub async fn get_ln_onchain_address(&self) -> Result<String> {
217        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
218        if gateway_cli_version >= *VERSION_0_11_0_ALPHA {
219            // New format: JSON object with "address" field
220            let value = cmd!(self, "onchain", "address").out_json().await?;
221            Ok(value["address"]
222                .as_str()
223                .context("address must be a string")?
224                .to_owned())
225        } else {
226            // Old format: raw address string
227            cmd!(self, "onchain", "address").out_string().await
228        }
229    }
230
231    pub async fn get_mnemonic(&self) -> Result<MnemonicResponse> {
232        let value = retry(
233            "Getting gateway mnemonic",
234            backoff_util::aggressive_backoff(),
235            || async { cmd!(self, "seed").out_json().await },
236        )
237        .await
238        .context("Getting gateway mnemonic")?;
239
240        Ok(serde_json::from_value(value)?)
241    }
242
243    pub async fn leave_federation(&self, federation_id: FederationId) -> Result<serde_json::Value> {
244        let fed_info = cmd!(self, "leave-fed", "--federation-id", federation_id)
245            .out_json()
246            .await?;
247        Ok(fed_info)
248    }
249
250    pub async fn create_invoice(&self, amount_msats: u64) -> Result<Bolt11Invoice> {
251        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
252        let invoice_str = if gateway_cli_version >= *VERSION_0_11_0_ALPHA {
253            // New format: JSON object with "invoice" field
254            let value = cmd!(self, "lightning", "create-invoice", amount_msats)
255                .out_json()
256                .await?;
257            value["invoice"]
258                .as_str()
259                .context("invoice must be a string")?
260                .to_owned()
261        } else {
262            // Old format: raw invoice string
263            cmd!(self, "lightning", "create-invoice", amount_msats)
264                .out_string()
265                .await?
266        };
267        Ok(Bolt11Invoice::from_str(&invoice_str)?)
268    }
269
270    pub async fn pay_invoice(&self, invoice: Bolt11Invoice) -> Result<()> {
271        cmd!(self, "lightning", "pay-invoice", invoice.to_string())
272            .run()
273            .await?;
274
275        Ok(())
276    }
277
278    pub async fn send_ecash(&self, federation_id: String, amount_msats: u64) -> Result<String> {
279        let value = cmd!(
280            self,
281            "ecash",
282            "send",
283            "--federation-id",
284            federation_id,
285            amount_msats
286        )
287        .out_json()
288        .await?;
289        let ecash: String = serde_json::from_value(
290            value
291                .get("notes")
292                .expect("notes key does not exist")
293                .clone(),
294        )?;
295        Ok(ecash)
296    }
297
298    pub async fn receive_ecash(&self, ecash: String) -> Result<()> {
299        cmd!(self, "ecash", "receive", "--notes", ecash)
300            .run()
301            .await?;
302        Ok(())
303    }
304
305    pub async fn get_balances(&self) -> Result<GatewayBalances> {
306        let value = cmd!(self, "get-balances").out_json().await?;
307        Ok(serde_json::from_value(value)?)
308    }
309
310    pub async fn ecash_balance(&self, federation_id: String) -> anyhow::Result<u64> {
311        let federation_id = FederationId::from_str(&federation_id)?;
312        let balances = self.get_balances().await?;
313        let ecash_balance = balances
314            .ecash_balances
315            .into_iter()
316            .find(|info| info.federation_id == federation_id)
317            .ok_or(anyhow::anyhow!("Gateway is not joined to federation"))?
318            .ecash_balance_msats
319            .msats;
320        Ok(ecash_balance)
321    }
322
323    pub async fn close_channel(&self, remote_pubkey: PublicKey, force: bool) -> Result<()> {
324        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
325        let mut close_channel = if force {
326            cmd!(
327                self,
328                "lightning",
329                "close-channels-with-peer",
330                "--pubkey",
331                remote_pubkey,
332                "--force",
333            )
334        } else if gateway_cli_version < *VERSION_0_10_0_ALPHA {
335            cmd!(
336                self,
337                "lightning",
338                "close-channels-with-peer",
339                "--pubkey",
340                remote_pubkey,
341            )
342        } else {
343            cmd!(
344                self,
345                "lightning",
346                "close-channels-with-peer",
347                "--pubkey",
348                remote_pubkey,
349                "--sats-per-vbyte",
350                "10",
351            )
352        };
353
354        close_channel.run().await?;
355
356        Ok(())
357    }
358
359    /// Send close requests for all channels without waiting for them to
360    /// become inactive. See [`Self::close_all_channels`] for a version
361    /// that polls until closure is confirmed.
362    pub async fn close_all_channels_no_wait(&self, force: bool) -> Result<()> {
363        let channels = self.list_channels().await?;
364
365        for chan in channels {
366            let remote_pubkey = chan.remote_pubkey;
367            self.close_channel(remote_pubkey, force).await?;
368        }
369
370        Ok(())
371    }
372
373    /// Close all channels and poll until none are active.
374    ///
375    /// Only waits for channels that existed at the time of the call, so
376    /// channels opened while polling are ignored.
377    pub async fn close_all_channels(&self, force: bool, timeout: Duration) -> Result<()> {
378        let channels = self.list_channels().await?;
379        let closing_peers: HashSet<_> = channels.iter().map(|chan| chan.remote_pubkey).collect();
380
381        for chan in channels {
382            self.close_channel(chan.remote_pubkey, force).await?;
383        }
384
385        poll_with_timeout("waiting for channels to close", timeout, || async {
386            let channels = self.list_channels().await.map_err(ControlFlow::Continue)?;
387            if channels
388                .iter()
389                .any(|chan| closing_peers.contains(&chan.remote_pubkey) && chan.is_active)
390            {
391                return Err(ControlFlow::Continue(anyhow::anyhow!(
392                    "Some channels are still active"
393                )));
394            }
395            Ok(())
396        })
397        .await
398    }
399
400    /// Open a channel with the gateway's lightning node, returning the funding
401    /// transaction txid.
402    pub async fn open_channel(
403        &self,
404        gw: &Gatewayd,
405        channel_size_sats: u64,
406        push_amount_sats: Option<u64>,
407    ) -> Result<Txid> {
408        let pubkey = gw.client().lightning_pubkey().await?;
409        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
410
411        let txid_str = if gateway_cli_version >= *VERSION_0_11_0_ALPHA {
412            // New format: JSON object with "funding_txid" field
413            let value = cmd!(
414                self,
415                "lightning",
416                "open-channel",
417                "--pubkey",
418                pubkey,
419                "--host",
420                gw.lightning_node_addr,
421                "--channel-size-sats",
422                channel_size_sats,
423                "--push-amount-sats",
424                push_amount_sats.unwrap_or(0)
425            )
426            .out_json()
427            .await?;
428
429            value["funding_txid"]
430                .as_str()
431                .context("funding_txid must be a string")?
432                .to_owned()
433        } else {
434            // Old format: raw txid string
435            cmd!(
436                self,
437                "lightning",
438                "open-channel",
439                "--pubkey",
440                pubkey,
441                "--host",
442                gw.lightning_node_addr,
443                "--channel-size-sats",
444                channel_size_sats,
445                "--push-amount-sats",
446                push_amount_sats.unwrap_or(0)
447            )
448            .out_string()
449            .await?
450        };
451
452        Ok(Txid::from_str(&txid_str)?)
453    }
454
455    pub async fn set_channel_fees(
456        &self,
457        funding_outpoint: bitcoin::OutPoint,
458        base_fee_msat: u64,
459        parts_per_million: u64,
460    ) -> Result<()> {
461        cmd!(
462            self,
463            "lightning",
464            "set-channel-fees",
465            "--funding-outpoint",
466            funding_outpoint,
467            "--base-fee-msat",
468            base_fee_msat,
469            "--parts-per-million",
470            parts_per_million,
471        )
472        .run()
473        .await?;
474        Ok(())
475    }
476
477    pub async fn list_channels(&self) -> Result<Vec<ChannelInfo>> {
478        let channels = cmd!(self, "lightning", "list-channels").out_json().await?;
479
480        let channels = channels
481            .as_array()
482            .context("channels must be an array")?
483            .iter()
484            .map(|channel| {
485                let remote_pubkey = channel["remote_pubkey"]
486                    .as_str()
487                    .context("remote_pubkey must be a string")?
488                    .to_owned();
489                let channel_size_sats = channel["channel_size_sats"]
490                    .as_u64()
491                    .context("channel_size_sats must be a u64")?;
492                let outbound_liquidity_sats = channel["outbound_liquidity_sats"]
493                    .as_u64()
494                    .context("outbound_liquidity_sats must be a u64")?;
495                let inbound_liquidity_sats = channel["inbound_liquidity_sats"]
496                    .as_u64()
497                    .context("inbound_liquidity_sats must be a u64")?;
498                let is_active = channel["is_active"].as_bool().unwrap_or(true);
499                let funding_outpoint = channel.get("funding_outpoint").map(|v| {
500                    serde_json::from_value::<bitcoin::OutPoint>(v.clone())
501                        .expect("Could not deserialize outpoint")
502                });
503                let remote_node_alias = channel
504                    .get("remote_node_alias")
505                    .map(std::string::ToString::to_string);
506                let remote_address = channel
507                    .get("remote_address")
508                    .map(std::string::ToString::to_string);
509                let base_fee_msat = channel
510                    .get("base_fee_msat")
511                    .and_then(serde_json::Value::as_u64);
512                let parts_per_million = channel
513                    .get("parts_per_million")
514                    .and_then(serde_json::Value::as_u64);
515                Ok(ChannelInfo {
516                    remote_pubkey: remote_pubkey
517                        .parse()
518                        .expect("Lightning node returned invalid remote channel pubkey"),
519                    channel_size_sats,
520                    outbound_liquidity_sats,
521                    inbound_liquidity_sats,
522                    is_active,
523                    funding_outpoint,
524                    remote_node_alias,
525                    remote_address,
526                    base_fee_msat,
527                    parts_per_million,
528                })
529            })
530            .collect::<Result<Vec<ChannelInfo>>>()?;
531        Ok(channels)
532    }
533
534    pub async fn wait_for_block_height(&self, target_block_height: u64) -> Result<()> {
535        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
536        poll("waiting for block height", || async {
537            let info = self.get_info().await.map_err(ControlFlow::Continue)?;
538
539            let height_value = if gateway_cli_version < *VERSION_0_10_0_ALPHA {
540                info["block_height"].clone()
541            } else {
542                info["lightning_info"]["connected"]["block_height"].clone()
543            };
544
545            let block_height: Option<u32> = serde_json::from_value(height_value)
546                .context("Could not parse block height")
547                .map_err(ControlFlow::Continue)?;
548            let Some(block_height) = block_height else {
549                return Err(ControlFlow::Continue(anyhow!("Not synced any blocks yet")));
550            };
551
552            let synced_value = if gateway_cli_version < *VERSION_0_10_0_ALPHA {
553                info["synced_to_chain"].clone()
554            } else {
555                info["lightning_info"]["connected"]["synced_to_chain"].clone()
556            };
557            let synced = synced_value
558                .as_bool()
559                .expect("Could not get synced_to_chain");
560            if block_height >= target_block_height as u32 && synced {
561                return Ok(());
562            }
563
564            Err(ControlFlow::Continue(anyhow!("Not synced to block")))
565        })
566        .await?;
567        Ok(())
568    }
569
570    pub async fn get_lightning_fee(&self, fed_id: String) -> Result<PaymentFee> {
571        let info_value = self.get_info().await?;
572        let federations = info_value["federations"]
573            .as_array()
574            .expect("federations is an array");
575
576        let fed = federations
577            .iter()
578            .find(|fed| {
579                serde_json::from_value::<String>(fed["federation_id"].clone())
580                    .expect("could not deserialize federation_id")
581                    == fed_id
582            })
583            .ok_or_else(|| anyhow!("Federation not found"))?;
584
585        let lightning_fee = fed["config"]["lightning_fee"].clone();
586        let base: Amount = serde_json::from_value(lightning_fee["base"].clone())
587            .map_err(|e| anyhow!("Couldnt parse base: {e}"))?;
588        let parts_per_million: u64 =
589            serde_json::from_value(lightning_fee["parts_per_million"].clone())
590                .map_err(|e| anyhow!("Couldnt parse parts_per_million: {e}"))?;
591
592        Ok(PaymentFee {
593            base,
594            parts_per_million,
595        })
596    }
597
598    pub async fn set_federation_routing_fee(
599        &self,
600        fed_id: String,
601        base: u64,
602        ppm: u64,
603    ) -> Result<()> {
604        cmd!(
605            self,
606            "cfg",
607            "set-fees",
608            "--federation-id",
609            fed_id,
610            "--ln-base",
611            base,
612            "--ln-ppm",
613            ppm
614        )
615        .run()
616        .await?;
617
618        Ok(())
619    }
620
621    pub async fn set_federation_transaction_fee(
622        &self,
623        fed_id: String,
624        base: u64,
625        ppm: u64,
626    ) -> Result<()> {
627        cmd!(
628            self,
629            "cfg",
630            "set-fees",
631            "--federation-id",
632            fed_id,
633            "--tx-base",
634            base,
635            "--tx-ppm",
636            ppm
637        )
638        .run()
639        .await?;
640
641        Ok(())
642    }
643
644    pub async fn payment_summary(&self) -> Result<PaymentSummaryResponse> {
645        let out_json = cmd!(self, "payment-summary").out_json().await?;
646        Ok(serde_json::from_value(out_json).expect("Could not deserialize PaymentSummaryResponse"))
647    }
648
649    pub async fn wait_bolt11_invoice(&self, payment_hash: Vec<u8>) -> Result<()> {
650        let payment_hash =
651            sha256::Hash::from_slice(&payment_hash).expect("Could not parse payment hash");
652        let invoice_val = cmd!(
653            self,
654            "lightning",
655            "get-invoice",
656            "--payment-hash",
657            payment_hash
658        )
659        .out_json()
660        .await?;
661        let invoice: GetInvoiceResponse =
662            serde_json::from_value(invoice_val).expect("Could not parse GetInvoiceResponse");
663        anyhow::ensure!(invoice.status == PaymentStatus::Succeeded);
664
665        Ok(())
666    }
667
668    pub async fn list_transactions(
669        &self,
670        start: SystemTime,
671        end: SystemTime,
672    ) -> Result<Vec<PaymentDetails>> {
673        let start_datetime: DateTime<Utc> = start.into();
674        let end_datetime: DateTime<Utc> = end.into();
675        let response = cmd!(
676            self,
677            "lightning",
678            "list-transactions",
679            "--start-time",
680            start_datetime.to_rfc3339(),
681            "--end-time",
682            end_datetime.to_rfc3339()
683        )
684        .out_json()
685        .await?;
686        let transactions = serde_json::from_value::<ListTransactionsResponse>(response)?;
687        Ok(transactions.transactions)
688    }
689
690    pub async fn create_offer(&self, amount: Option<Amount>) -> Result<String> {
691        let offer_value = if let Some(amount) = amount {
692            cmd!(
693                self,
694                "lightning",
695                "create-offer",
696                "--amount-msat",
697                amount.msats
698            )
699            .out_json()
700            .await?
701        } else {
702            cmd!(self, "lightning", "create-offer").out_json().await?
703        };
704        let offer_response = serde_json::from_value::<CreateOfferResponse>(offer_value)
705            .expect("Could not parse offer response");
706        Ok(offer_response.offer)
707    }
708
709    pub async fn pay_offer(&self, offer: String, amount: Option<Amount>) -> Result<()> {
710        if let Some(amount) = amount {
711            cmd!(
712                self,
713                "lightning",
714                "pay-offer",
715                "--offer",
716                offer,
717                "--amount-msat",
718                amount.msats
719            )
720            .run()
721            .await?;
722        } else {
723            cmd!(self, "lightning", "pay-offer", "--offer", offer)
724                .run()
725                .await?;
726        }
727
728        Ok(())
729    }
730
731    pub async fn send_onchain(
732        &self,
733        bitcoind: &Bitcoind,
734        amount: BitcoinAmountOrAll,
735        fee_rate: u64,
736    ) -> Result<bitcoin::Txid> {
737        let withdraw_address = bitcoind.get_new_address().await?;
738        let value = cmd!(
739            self,
740            "onchain",
741            "send",
742            "--address",
743            withdraw_address,
744            "--amount",
745            amount,
746            "--fee-rate-sats-per-vbyte",
747            fee_rate
748        )
749        .out_json()
750        .await?;
751
752        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
753        let txid: bitcoin::Txid = if gateway_cli_version >= *VERSION_0_11_0_ALPHA {
754            // New format: JSON object with "txid" field
755            serde_json::from_value(value["txid"].clone())?
756        } else {
757            // Old format: raw txid string
758            serde_json::from_value(value)?
759        };
760        Ok(txid)
761    }
762
763    pub async fn pegout(
764        &self,
765        fed_id: String,
766        amount: u64,
767        address: Address,
768    ) -> Result<WithdrawResponse> {
769        let value = cmd!(
770            self,
771            "ecash",
772            "pegout",
773            "--federation-id",
774            fed_id,
775            "--amount",
776            amount,
777            "--address",
778            address
779        )
780        .out_json()
781        .await?;
782        Ok(serde_json::from_value(value)?)
783    }
784
785    /// Sweeps the gateway's entire ecash balance for a federation on chain via
786    /// `--amount all`.
787    pub async fn pegout_all(&self, fed_id: String, address: Address) -> Result<WithdrawResponse> {
788        let value = cmd!(
789            self,
790            "ecash",
791            "pegout",
792            "--federation-id",
793            fed_id,
794            "--amount",
795            "all",
796            "--address",
797            address
798        )
799        .out_json()
800        .await?;
801        Ok(serde_json::from_value(value)?)
802    }
803}
804
805#[derive(Clone)]
806pub struct Gatewayd {
807    pub(crate) process: ProcessHandle,
808    pub ln: LightningNode,
809    pub addr: String,
810    pub(crate) lightning_node_addr: String,
811    pub gatewayd_version: Version,
812    pub gw_name: String,
813    pub log_path: PathBuf,
814    pub gw_port: u16,
815    pub ldk_port: u16,
816    pub metrics_port: u16,
817    pub gateway_id: String,
818    pub iroh_gateway_id: Option<String>,
819    pub iroh_port: u16,
820    pub node_id: iroh_base::NodeId,
821    pub gateway_index: usize,
822}
823
824impl Gatewayd {
825    pub async fn new(
826        process_mgr: &ProcessManager,
827        ln: LightningNode,
828        gateway_index: usize,
829    ) -> Result<Self> {
830        let ln_type = ln.ln_type();
831        let (gw_name, port, lightning_node_port, metrics_port) = match &ln {
832            LightningNode::Lnd(_) => (
833                "gatewayd-lnd".to_string(),
834                process_mgr.globals.FM_PORT_GW_LND,
835                process_mgr.globals.FM_PORT_LND_LISTEN,
836                process_mgr.globals.FM_PORT_GW_LND_METRICS,
837            ),
838            LightningNode::Ldk {
839                name,
840                gw_port,
841                ldk_port,
842                metrics_port,
843            } => (
844                name.to_owned(),
845                gw_port.to_owned(),
846                ldk_port.to_owned(),
847                metrics_port.to_owned(),
848            ),
849        };
850        let test_dir = &process_mgr.globals.FM_TEST_DIR;
851        let addr = format!("http://127.0.0.1:{port}/{V1_API_ENDPOINT}");
852        let lightning_node_addr = format!("127.0.0.1:{lightning_node_port}");
853        let iroh_endpoint = process_mgr
854            .globals
855            .gatewayd_overrides
856            .gateway_iroh_endpoints
857            .get(gateway_index)
858            .expect("No gateway for index");
859
860        let mut gateway_env: HashMap<String, String> = HashMap::from_iter([
861            (
862                FM_GATEWAY_DATA_DIR_ENV.to_owned(),
863                format!("{}/{gw_name}", utf8(test_dir)),
864            ),
865            (
866                FM_GATEWAY_LISTEN_ADDR_ENV.to_owned(),
867                format!("127.0.0.1:{port}"),
868            ),
869            (FM_GATEWAY_API_ADDR_ENV.to_owned(), addr.clone()),
870            (FM_PORT_LDK_ENV.to_owned(), lightning_node_port.to_string()),
871            (
872                FM_GATEWAY_IROH_LISTEN_ADDR_ENV.to_owned(),
873                format!("127.0.0.1:{}", iroh_endpoint.port()),
874            ),
875            (
876                FM_GATEWAY_IROH_SECRET_KEY_OVERRIDE_ENV.to_owned(),
877                iroh_endpoint.secret_key(),
878            ),
879            (
880                FM_GATEWAY_METRICS_LISTEN_ADDR_ENV.to_owned(),
881                format!("127.0.0.1:{metrics_port}"),
882            ),
883        ]);
884
885        let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
886
887        if ln_type == LightningNodeType::Ldk {
888            gateway_env.insert("FM_LDK_ALIAS".to_owned(), gw_name.clone());
889        }
890
891        // Both the plain and legacy `NodeTicket` override formats are exported
892        // globally under separate env vars (see `net_overrides`), so gateways of
893        // any version read the one they understand without a per-version branch
894        // here.
895
896        let process = process_mgr
897            .spawn_daemon(
898                &gw_name,
899                cmd!(crate::util::Gatewayd, ln_type).envs(gateway_env),
900            )
901            .await?;
902
903        let timeout = if is_env_var_set(FM_PRE_DKG_ENV) {
904            Duration::from_secs(300)
905        } else {
906            Duration::from_secs(60)
907        };
908        let (gateway_id, iroh_gateway_id) = poll_with_timeout(
909            "waiting for gateway to be ready to respond to rpc",
910            timeout,
911            || async {
912                // Once the gateway id is available via RPC, the gateway is ready
913                let info = cmd!(
914                    crate::util::get_gateway_cli_path(),
915                    "--rpcpassword",
916                    "theresnosecondbest",
917                    "-a",
918                    addr,
919                    "info"
920                )
921                .out_json()
922                .await
923                .map_err(ControlFlow::Continue)?;
924                let (gateway_id, iroh_gateway_id) = if gatewayd_version < *VERSION_0_10_0_ALPHA {
925                    let gateway_id = info["gateway_id"]
926                        .as_str()
927                        .context("gateway_id must be a string")
928                        .map_err(ControlFlow::Break)?
929                        .to_owned();
930                    (gateway_id, None)
931                } else {
932                    let gateway_id = info["registrations"]["http"][1]
933                        .as_str()
934                        .context("gateway id must be a string")
935                        .map_err(ControlFlow::Break)?
936                        .to_owned();
937                    let iroh_gateway_id = info["registrations"]["iroh"][1]
938                        .as_str()
939                        .context("gateway id must be a string")
940                        .map_err(ControlFlow::Break)?
941                        .to_owned();
942                    (gateway_id, Some(iroh_gateway_id))
943                };
944
945                Ok((gateway_id, iroh_gateway_id))
946            },
947        )
948        .await?;
949
950        let log_path = process_mgr
951            .globals
952            .FM_LOGS_DIR
953            .join(format!("{gw_name}.log"));
954        let gatewayd = Self {
955            process,
956            ln,
957            addr,
958            lightning_node_addr,
959            gatewayd_version,
960            gw_name,
961            log_path,
962            gw_port: port,
963            ldk_port: lightning_node_port,
964            metrics_port,
965            gateway_id,
966            iroh_gateway_id,
967            iroh_port: iroh_endpoint.port(),
968            node_id: iroh_endpoint.node_id(),
969            gateway_index,
970        };
971
972        Ok(gatewayd)
973    }
974
975    pub async fn terminate(self) -> Result<()> {
976        self.process.terminate().await
977    }
978
979    pub fn set_lightning_node(&mut self, ln_node: LightningNode) {
980        self.ln = ln_node;
981    }
982
983    pub async fn stop_lightning_node(&mut self) -> Result<()> {
984        info!(target: LOG_DEVIMINT, "Stopping lightning node");
985        match self.ln.clone() {
986            LightningNode::Lnd(lnd) => lnd.terminate().await,
987            LightningNode::Ldk {
988                name: _,
989                gw_port: _,
990                ldk_port: _,
991                metrics_port: _,
992            } => {
993                // This is not implemented because the LDK node lives in
994                // the gateway process and cannot be stopped independently.
995                unimplemented!("LDK node termination not implemented")
996            }
997        }
998    }
999
1000    /// Restarts the gateway using the provided `bin_path`, which is useful for
1001    /// testing upgrades.
1002    pub async fn restart_with_bin(
1003        &mut self,
1004        process_mgr: &ProcessManager,
1005        gatewayd_path: &PathBuf,
1006        gateway_cli_path: &PathBuf,
1007    ) -> Result<()> {
1008        let ln = self.ln.clone();
1009
1010        self.process.terminate().await?;
1011        // TODO: Audit that the environment access only happens in single-threaded code.
1012        unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", gatewayd_path) };
1013        // TODO: Audit that the environment access only happens in single-threaded code.
1014        unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", gateway_cli_path) };
1015
1016        let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
1017        let new_ln = ln;
1018        let new_gw = Self::new(process_mgr, new_ln.clone(), self.gateway_index).await?;
1019        self.process = new_gw.process;
1020        self.set_lightning_node(new_ln);
1021        let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
1022        info!(
1023            target: LOG_DEVIMINT,
1024            ?gatewayd_version,
1025            ?gateway_cli_version,
1026            "upgraded gatewayd and gateway-cli"
1027        );
1028        Ok(())
1029    }
1030
1031    pub fn client(&self) -> GatewayClient {
1032        GatewayClient::new(self)
1033    }
1034}