devimint/
util.rs

1use std::collections::BTreeMap;
2use std::ffi::OsStr;
3use std::future::Future;
4use std::ops::ControlFlow;
5use std::path::PathBuf;
6use std::process::Stdio;
7use std::sync::Arc;
8use std::time::Duration;
9use std::{env, unreachable};
10
11use anyhow::{Context, Result, anyhow, bail, format_err};
12use fedimint_api_client::api::StatusResponse;
13use fedimint_core::PeerId;
14use fedimint_core::admin_client::{PeerServerParamsLegacy, SetupStatus};
15use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
16use fedimint_core::envs::{FM_ENABLE_MODULE_LNV2_ENV, is_env_var_set};
17use fedimint_core::module::ApiAuth;
18use fedimint_core::task::{self, block_in_place, block_on};
19use fedimint_core::time::now;
20use fedimint_core::util::FmtCompactAnyhow as _;
21use fedimint_core::util::backoff_util::custom_backoff;
22use fedimint_logging::LOG_DEVIMINT;
23use legacy_types::ConfigGenParamsResponseLegacy;
24use semver::Version;
25use serde::de::DeserializeOwned;
26use tokio::fs::OpenOptions;
27use tokio::process::Child;
28use tokio::sync::Mutex;
29use tracing::{debug, warn};
30
31use crate::envs::{
32    FM_BACKWARDS_COMPATIBILITY_TEST_ENV, FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV,
33    FM_BITCOIND_BASE_EXECUTABLE_ENV, FM_BTC_CLIENT_ENV, FM_CLIENT_DIR_ENV,
34    FM_DEVIMINT_CMD_INHERIT_STDERR_ENV, FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV,
35    FM_ELECTRS_BASE_EXECUTABLE_ENV, FM_ESPLORA_BASE_EXECUTABLE_ENV,
36    FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV,
37    FM_FEDIMINTD_BASE_EXECUTABLE_ENV, FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV,
38    FM_GATEWAYD_BASE_EXECUTABLE_ENV, FM_GWCLI_LDK_ENV, FM_GWCLI_LND_ENV,
39    FM_LNCLI_BASE_EXECUTABLE_ENV, FM_LNCLI_ENV, FM_LND_BASE_EXECUTABLE_ENV,
40    FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV, FM_LOGS_DIR_ENV, FM_MINT_CLIENT_ENV,
41    FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV, FM_RECURRINGD_BASE_EXECUTABLE_ENV,
42};
43
44// If a binary doesn't provide a clap version, default to the first stable
45// release (v0.2.1)
46const DEFAULT_VERSION: Version = Version::new(0, 2, 1);
47
48pub fn parse_map(s: &str) -> Result<BTreeMap<String, String>> {
49    let mut map = BTreeMap::new();
50
51    if s.is_empty() {
52        return Ok(map);
53    }
54
55    for pair in s.split(',') {
56        let parts: Vec<&str> = pair.split('=').collect();
57        if parts.len() == 2 {
58            map.insert(parts[0].to_string(), parts[1].to_string());
59        } else {
60            return Err(format_err!("Invalid pair in map: {}", pair));
61        }
62    }
63    Ok(map)
64}
65
66fn send_sigterm(child: &Child) {
67    send_signal(child, nix::sys::signal::Signal::SIGTERM);
68}
69
70fn send_sigkill(child: &Child) {
71    send_signal(child, nix::sys::signal::Signal::SIGKILL);
72}
73
74fn send_signal(child: &Child, signal: nix::sys::signal::Signal) {
75    let _ = nix::sys::signal::kill(
76        nix::unistd::Pid::from_raw(child.id().expect("pid should be present") as _),
77        signal,
78    );
79}
80
81/// Kills process when all references to ProcessHandle are dropped.
82///
83/// NOTE: drop order is significant make sure fields in struct are declared in
84/// correct order it is generally clients, process handle, deps
85#[derive(Debug, Clone)]
86pub struct ProcessHandle(Arc<Mutex<ProcessHandleInner>>);
87
88impl ProcessHandle {
89    pub async fn terminate(&self) -> Result<()> {
90        let mut inner = self.0.lock().await;
91        inner.terminate().await?;
92        Ok(())
93    }
94    pub async fn is_running(&self) -> bool {
95        self.0.lock().await.child.is_some()
96    }
97}
98
99#[derive(Debug)]
100pub struct ProcessHandleInner {
101    name: String,
102    child: Option<Child>,
103}
104
105impl ProcessHandleInner {
106    async fn terminate(&mut self) -> anyhow::Result<()> {
107        if let Some(child) = self.child.as_mut() {
108            debug!(
109                target: LOG_DEVIMINT,
110                name=%self.name,
111                signal="SIGTERM",
112                "sending signal to terminate child process"
113            );
114
115            send_sigterm(child);
116
117            if (fedimint_core::runtime::timeout(Duration::from_secs(2), child.wait()).await)
118                .is_err()
119            {
120                debug!(
121                    target: LOG_DEVIMINT,
122                    name=%self.name,
123                    signal="SIGKILL",
124                    "sending signal to terminate child process"
125                );
126
127                send_sigkill(child);
128
129                match fedimint_core::runtime::timeout(Duration::from_secs(5), child.wait()).await {
130                    Ok(Ok(_)) => {}
131                    Ok(Err(err)) => {
132                        bail!("Failed to terminate child process {}: {}", self.name, err);
133                    }
134                    Err(_) => {
135                        bail!("Failed to terminate child process {}: timeout", self.name);
136                    }
137                }
138            }
139        }
140        // only drop the child handle if succeeded to terminate
141        self.child.take();
142        Ok(())
143    }
144}
145
146impl Drop for ProcessHandleInner {
147    fn drop(&mut self) {
148        if self.child.is_none() {
149            return;
150        }
151
152        block_in_place(|| {
153            if let Err(err) = block_on(self.terminate()) {
154                warn!(target: LOG_DEVIMINT,
155                        name=%self.name,
156                        err = %err.fmt_compact_anyhow(),
157                        "Error terminating process on drop");
158            }
159        });
160    }
161}
162
163#[derive(Clone)]
164pub struct ProcessManager {
165    pub globals: super::vars::Global,
166}
167
168impl ProcessManager {
169    pub fn new(globals: super::vars::Global) -> Self {
170        Self { globals }
171    }
172
173    /// Logs to $FM_LOGS_DIR/{name}.{out,err}
174    pub async fn spawn_daemon(&self, name: &str, mut cmd: Command) -> Result<ProcessHandle> {
175        debug!(target: LOG_DEVIMINT, %name, "Spawning daemon");
176        let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
177        let path = format!("{logs_dir}/{name}.log");
178        let log = OpenOptions::new()
179            .append(true)
180            .create(true)
181            .open(path)
182            .await?
183            .into_std()
184            .await;
185        cmd.cmd.kill_on_drop(false); // we handle killing ourself
186        cmd.cmd.stdout(log.try_clone()?);
187        cmd.cmd.stderr(log);
188        let child = cmd
189            .cmd
190            .spawn()
191            .with_context(|| format!("Could not spawn: {name}"))?;
192        let handle = ProcessHandle(Arc::new(Mutex::new(ProcessHandleInner {
193            name: name.to_owned(),
194            child: Some(child),
195        })));
196        Ok(handle)
197    }
198}
199
200pub struct Command {
201    pub cmd: tokio::process::Command,
202    pub args_debug: Vec<String>,
203}
204
205impl Command {
206    pub fn arg<T: ToString>(mut self, arg: &T) -> Self {
207        let string = arg.to_string();
208        self.cmd.arg(string.clone());
209        self.args_debug.push(string);
210        self
211    }
212
213    pub fn args<T: ToString>(mut self, args: impl IntoIterator<Item = T>) -> Self {
214        for arg in args {
215            self = self.arg(&arg);
216        }
217        self
218    }
219
220    pub fn env<K, V>(mut self, key: K, val: V) -> Self
221    where
222        K: AsRef<OsStr>,
223        V: AsRef<OsStr>,
224    {
225        self.cmd.env(key, val);
226        self
227    }
228
229    pub fn envs<I, K, V>(mut self, env: I) -> Self
230    where
231        I: IntoIterator<Item = (K, V)>,
232        K: AsRef<OsStr>,
233        V: AsRef<OsStr>,
234    {
235        self.cmd.envs(env);
236        self
237    }
238
239    pub fn kill_on_drop(mut self, kill: bool) -> Self {
240        self.cmd.kill_on_drop(kill);
241        self
242    }
243
244    /// Run the command and get its output as json.
245    pub async fn out_json(&mut self) -> Result<serde_json::Value> {
246        Ok(serde_json::from_str(&self.out_string().await?)?)
247    }
248
249    fn command_debug(&self) -> String {
250        self.args_debug
251            .iter()
252            .map(|x| x.replace(' ', "␣"))
253            .collect::<Vec<_>>()
254            .join(" ")
255    }
256
257    /// Run the command and get its output as string.
258    pub async fn out_string(&mut self) -> Result<String> {
259        let output = self
260            .run_inner(true)
261            .await
262            .with_context(|| format!("command: {}", self.command_debug()))?;
263        let output = String::from_utf8(output.stdout)?;
264        Ok(output.trim().to_owned())
265    }
266
267    /// Returns the json error if the command has a non-zero exit code.
268    pub async fn expect_err_json(&mut self) -> Result<serde_json::Value> {
269        let output = self
270            .run_inner(false)
271            .await
272            .with_context(|| format!("command: {}", self.command_debug()))?;
273        let output = String::from_utf8(output.stdout)?;
274        Ok(serde_json::from_str(output.trim())?)
275    }
276
277    /// Run the command expecting an error, which is parsed using a closure.
278    /// Returns an Err if the closure returns false.
279    pub async fn assert_error(
280        &mut self,
281        predicate: impl Fn(serde_json::Value) -> bool,
282    ) -> Result<()> {
283        let parsed_error = self.expect_err_json().await?;
284        anyhow::ensure!(predicate(parsed_error));
285        Ok(())
286    }
287
288    /// Returns an Err if the command doesn't return an error containing the
289    /// provided error string.
290    pub async fn assert_error_contains(&mut self, error: &str) -> Result<()> {
291        self.assert_error(|err_json| {
292            let error_string = err_json
293                .get("error")
294                .expect("json error contains error field")
295                .as_str()
296                .expect("not a string")
297                .to_owned();
298
299            error_string.contains(error)
300        })
301        .await
302    }
303
304    pub async fn run_inner(&mut self, expect_success: bool) -> Result<std::process::Output> {
305        debug!(target: LOG_DEVIMINT, "> {}", self.command_debug());
306        let output = self
307            .cmd
308            .stdout(Stdio::piped())
309            .stderr(if is_env_var_set(FM_DEVIMINT_CMD_INHERIT_STDERR_ENV) {
310                Stdio::inherit()
311            } else {
312                Stdio::piped()
313            })
314            .spawn()?
315            .wait_with_output()
316            .await?;
317
318        if output.status.success() != expect_success {
319            bail!(
320                "{}\nstdout:\n{}\nstderr:\n{}\n",
321                output.status,
322                String::from_utf8_lossy(&output.stdout),
323                String::from_utf8_lossy(&output.stderr),
324            );
325        }
326        Ok(output)
327    }
328
329    /// Run the command ignoring its output.
330    pub async fn run(&mut self) -> Result<()> {
331        let _ = self
332            .run_inner(true)
333            .await
334            .with_context(|| format!("command: {}", self.command_debug()))?;
335        Ok(())
336    }
337
338    /// Run the command logging the output and error
339    pub async fn run_with_logging(&mut self, name: String) -> Result<()> {
340        let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
341        let path = format!("{logs_dir}/{name}.log");
342        let log = OpenOptions::new()
343            .append(true)
344            .create(true)
345            .open(&path)
346            .await
347            .with_context(|| format!("path: {path} cmd: {name}"))?
348            .into_std()
349            .await;
350        self.cmd.stdout(log.try_clone()?);
351        self.cmd.stderr(log);
352        let status = self
353            .cmd
354            .spawn()
355            .with_context(|| format!("cmd: {name}"))?
356            .wait()
357            .await?;
358        if !status.success() {
359            bail!("{}", status);
360        }
361        Ok(())
362    }
363}
364
365/// easy syntax to create a Command
366///
367/// `(A1, A2, A3)` expands to
368/// ```ignore
369/// A1.cmd().await?
370///     .arg(A2)
371///     .arg(A3)
372///     .kill_on_drop(true)
373/// ```
374///
375/// If `An` is a string literal, it is replaced with `format!(a)`
376#[macro_export]
377macro_rules! cmd {
378    ($(@head ($($head:tt)* ))? $curr:literal $(, $($tail:tt)*)?) => {
379        cmd! {
380            @head ($($($head)*)? format!($curr),)
381            $($($tail)*)?
382        }
383    };
384    ($(@head ($($head:tt)* ))? $curr:expr_2021 $(, $($tail:tt)*)?) => {
385        cmd! {
386            @head ($($($head)*)? $curr,)
387            $($($tail)*)?
388        }
389    };
390    (@head ($($head:tt)* )) => {
391        cmd! {
392            @last
393            $($head)*
394        }
395    };
396    // last matcher
397    (@last $this:expr_2021, $($arg:expr_2021),* $(,)?) => {
398        {
399            #[allow(unused)]
400            use $crate::util::ToCmdExt;
401            $this.cmd()
402                $(.arg(&$arg))*
403                .kill_on_drop(true)
404                .env("RUST_BACKTRACE", "1")
405        }
406    };
407}
408
409#[macro_export]
410macro_rules! poll_eq {
411    ($left:expr_2021, $right:expr_2021) => {
412        match ($left, $right) {
413            (left, right) => {
414                if left == right {
415                    Ok(())
416                } else {
417                    Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
418                        "assertion failed, left: {left:?} right: {right:?}"
419                    )))
420                }
421            }
422        }
423    };
424}
425
426// Allow macro to be used within the crate. See https://stackoverflow.com/a/31749071.
427pub(crate) use cmd;
428
429/// Retry until `f` succeeds or timeout is reached
430///
431/// - if `f` return Ok(val), this returns with Ok(val).
432/// - if `f` return Err(Control::Break(err)), this returns Err(err)
433/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
434///   reached
435pub async fn poll_with_timeout<Fut, R>(
436    name: &str,
437    timeout: Duration,
438    f: impl Fn() -> Fut,
439) -> Result<R>
440where
441    Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
442{
443    const MIN_BACKOFF: Duration = Duration::from_millis(50);
444    const MAX_BACKOFF: Duration = Duration::from_secs(1);
445
446    let mut backoff = custom_backoff(MIN_BACKOFF, MAX_BACKOFF, None);
447    let start = now();
448    for attempt in 0u64.. {
449        let attempt_start = now();
450        match f().await {
451            Ok(value) => return Ok(value),
452            Err(ControlFlow::Break(err)) => {
453                return Err(err).with_context(|| format!("polling {name}"));
454            }
455            Err(ControlFlow::Continue(err))
456                if attempt_start
457                    .duration_since(start)
458                    .expect("time goes forward")
459                    < timeout =>
460            {
461                debug!(target: LOG_DEVIMINT, %attempt, err = %err.fmt_compact_anyhow(), "Polling {name} failed, will retry...");
462                task::sleep(backoff.next().unwrap_or(MAX_BACKOFF)).await;
463            }
464            Err(ControlFlow::Continue(err)) => {
465                return Err(err).with_context(|| {
466                    format!(
467                        "Polling {name} failed after {attempt} retries (timeout: {}s)",
468                        timeout.as_secs()
469                    )
470                });
471            }
472        }
473    }
474
475    unreachable!();
476}
477
478const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_secs(60);
479
480/// Retry until `f` succeeds or default timeout is reached
481///
482/// - if `f` return Ok(val), this returns with Ok(val).
483/// - if `f` return Err(Control::Break(err)), this returns Err(err)
484/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
485///   reached
486pub async fn poll<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
487where
488    Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
489{
490    poll_with_timeout(name, DEFAULT_POLL_TIMEOUT, f).await
491}
492
493pub async fn poll_simple<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
494where
495    Fut: Future<Output = Result<R, anyhow::Error>>,
496{
497    poll(name, || async { f().await.map_err(ControlFlow::Continue) }).await
498}
499
500// used to add `cmd` method.
501pub trait ToCmdExt {
502    fn cmd(self) -> Command;
503}
504
505// a command that uses self as program name
506impl ToCmdExt for &'_ str {
507    fn cmd(self) -> Command {
508        Command {
509            cmd: tokio::process::Command::new(self),
510            args_debug: vec![self.to_owned()],
511        }
512    }
513}
514
515impl ToCmdExt for Vec<String> {
516    fn cmd(self) -> Command {
517        to_command(self)
518    }
519}
520
521pub trait JsonValueExt {
522    fn to_typed<T: DeserializeOwned>(self) -> Result<T>;
523}
524
525impl JsonValueExt for serde_json::Value {
526    fn to_typed<T: DeserializeOwned>(self) -> Result<T> {
527        Ok(serde_json::from_value(self)?)
528    }
529}
530
531const GATEWAYD_FALLBACK: &str = "gatewayd";
532
533const FEDIMINTD_FALLBACK: &str = "fedimintd";
534
535const FEDIMINT_CLI_FALLBACK: &str = "fedimint-cli";
536
537pub fn get_fedimint_cli_path() -> Vec<String> {
538    get_command_str_for_alias(
539        &[FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV],
540        &[FEDIMINT_CLI_FALLBACK],
541    )
542}
543
544const GATEWAY_CLI_FALLBACK: &str = "gateway-cli";
545
546pub fn get_gateway_cli_path() -> Vec<String> {
547    get_command_str_for_alias(
548        &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
549        &[GATEWAY_CLI_FALLBACK],
550    )
551}
552
553const LOAD_TEST_TOOL_FALLBACK: &str = "fedimint-load-test-tool";
554
555const LNCLI_FALLBACK: &str = "lncli";
556
557pub fn get_lncli_path() -> Vec<String> {
558    get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
559}
560
561const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
562
563pub fn get_bitcoin_cli_path() -> Vec<String> {
564    get_command_str_for_alias(
565        &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
566        &[BITCOIN_CLI_FALLBACK],
567    )
568}
569
570const BITCOIND_FALLBACK: &str = "bitcoind";
571
572const LND_FALLBACK: &str = "lnd";
573
574const ELECTRS_FALLBACK: &str = "electrs";
575
576const ESPLORA_FALLBACK: &str = "esplora";
577
578const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
579
580const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
581
582const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
583
584pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
585    get_command_str_for_alias(
586        &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
587        &[FEDIMINT_DBTOOL_FALLBACK],
588    )
589}
590
591/// Maps a version hash to a release version
592fn version_hash_to_version(version_hash: &str) -> Result<Version> {
593    match version_hash {
594        "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
595        "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
596        _ => Err(anyhow!("no version known for version hash: {version_hash}")),
597    }
598}
599
600pub struct FedimintdCmd;
601impl FedimintdCmd {
602    pub fn cmd(self) -> Command {
603        to_command(get_command_str_for_alias(
604            &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
605            &[FEDIMINTD_FALLBACK],
606        ))
607    }
608
609    /// Returns the fedimintd version from clap or default min version
610    pub async fn version_or_default() -> Version {
611        match cmd!(FedimintdCmd, "--version").out_string().await {
612            Ok(version) => parse_clap_version(&version),
613            Err(_) => cmd!(FedimintdCmd, "version-hash")
614                .out_string()
615                .await
616                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
617                .unwrap_or(DEFAULT_VERSION),
618        }
619    }
620}
621
622pub struct Gatewayd;
623impl Gatewayd {
624    pub fn cmd(self) -> Command {
625        to_command(get_command_str_for_alias(
626            &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
627            &[GATEWAYD_FALLBACK],
628        ))
629    }
630
631    /// Returns the gatewayd version from clap or default min version
632    pub async fn version_or_default() -> Version {
633        match cmd!(Gatewayd, "--version").out_string().await {
634            Ok(version) => parse_clap_version(&version),
635            Err(_) => cmd!(Gatewayd, "version-hash")
636                .out_string()
637                .await
638                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
639                .unwrap_or(DEFAULT_VERSION),
640        }
641    }
642}
643
644pub struct FedimintCli;
645impl FedimintCli {
646    pub fn cmd(self) -> Command {
647        to_command(get_command_str_for_alias(
648            &[FM_MINT_CLIENT_ENV],
649            &get_fedimint_cli_path()
650                .iter()
651                .map(String::as_str)
652                .collect::<Vec<_>>(),
653        ))
654    }
655
656    /// Returns the fedimint-cli version from clap or default min version
657    pub async fn version_or_default() -> Version {
658        match cmd!(FedimintCli, "--version").out_string().await {
659            Ok(version) => parse_clap_version(&version),
660            Err(_) => DEFAULT_VERSION,
661        }
662    }
663
664    pub async fn ws_status(self, endpoint: &str) -> Result<StatusResponse> {
665        let status = cmd!(self, "admin", "dkg", "--ws", endpoint, "ws-status")
666            .out_json()
667            .await?;
668        Ok(serde_json::from_value(status)?)
669    }
670
671    pub async fn set_password(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
672        cmd!(
673            self,
674            "--password",
675            &auth.0,
676            "admin",
677            "dkg",
678            "--ws",
679            endpoint,
680            "set-password",
681        )
682        .run()
683        .await
684    }
685
686    pub async fn set_local_params_leader(
687        self,
688        peer: &PeerId,
689        auth: &ApiAuth,
690        endpoint: &str,
691    ) -> Result<String> {
692        let json = cmd!(
693            self,
694            "--password",
695            &auth.0,
696            "admin",
697            "setup",
698            endpoint,
699            "set-local-params",
700            format!("Devimint Guardian {peer}"),
701            "--federation-name",
702            "Devimint Federation"
703        )
704        .out_json()
705        .await?;
706
707        Ok(serde_json::from_value(json)?)
708    }
709
710    pub async fn set_local_params_follower(
711        self,
712        peer: &PeerId,
713        auth: &ApiAuth,
714        endpoint: &str,
715    ) -> Result<String> {
716        let json = cmd!(
717            self,
718            "--password",
719            &auth.0,
720            "admin",
721            "setup",
722            endpoint,
723            "set-local-params",
724            format!("Devimint Guardian {peer}")
725        )
726        .out_json()
727        .await?;
728
729        Ok(serde_json::from_value(json)?)
730    }
731
732    pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
733        cmd!(
734            self,
735            "--password",
736            &auth.0,
737            "admin",
738            "setup",
739            endpoint,
740            "add-peer",
741            params
742        )
743        .run()
744        .await
745    }
746
747    pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
748        let json = cmd!(
749            self,
750            "--password",
751            &auth.0,
752            "admin",
753            "setup",
754            endpoint,
755            "status",
756        )
757        .out_json()
758        .await?;
759
760        Ok(serde_json::from_value(json)?)
761    }
762
763    pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
764        cmd!(
765            self,
766            "--password",
767            &auth.0,
768            "admin",
769            "setup",
770            endpoint,
771            "start-dkg"
772        )
773        .run()
774        .await
775    }
776
777    pub async fn set_config_gen_params(
778        self,
779        auth: &ApiAuth,
780        endpoint: &str,
781        meta: BTreeMap<String, String>,
782        server_gen_params: ServerModuleConfigGenParamsRegistry,
783    ) -> Result<()> {
784        cmd!(
785            self,
786            "--password",
787            &auth.0,
788            "admin",
789            "dkg",
790            "--ws",
791            endpoint,
792            "set-config-gen-params",
793            "--meta-json",
794            serde_json::to_string(&meta)?,
795            "--modules-json",
796            serde_json::to_string(&server_gen_params)?
797        )
798        .run()
799        .await
800    }
801
802    pub async fn consensus_config_gen_params_legacy(
803        self,
804        endpoint: &str,
805    ) -> Result<ConfigGenParamsResponseLegacy> {
806        let result = cmd!(
807            self,
808            "admin",
809            "dkg",
810            "--ws",
811            endpoint,
812            "consensus-config-gen-params"
813        )
814        .out_json()
815        .await
816        .context("non-json returned for consensus_config_gen_params")?;
817        Ok(serde_json::from_value(result)?)
818    }
819
820    pub async fn set_config_gen_connections(
821        self,
822        auth: &ApiAuth,
823        endpoint: &str,
824        our_name: &str,
825        leader_api_url: Option<&str>,
826    ) -> Result<()> {
827        // FIXME: this should be a single command
828        if let Some(leader_api_url) = leader_api_url {
829            cmd!(
830                self,
831                "--password",
832                &auth.0,
833                "admin",
834                "dkg",
835                "--ws",
836                endpoint,
837                "set-config-gen-connections",
838                "--our-name",
839                our_name,
840                "--leader-api-url",
841                leader_api_url,
842            )
843            .run()
844            .await
845        } else {
846            cmd!(
847                self,
848                "--password",
849                &auth.0,
850                "admin",
851                "dkg",
852                "--ws",
853                endpoint,
854                "set-config-gen-connections",
855                "--our-name",
856                our_name,
857            )
858            .run()
859            .await
860        }
861    }
862
863    pub async fn get_config_gen_peers(self, endpoint: &str) -> Result<Vec<PeerServerParamsLegacy>> {
864        let result = cmd!(
865            self,
866            "admin",
867            "dkg",
868            "--ws",
869            endpoint,
870            "get-config-gen-peers"
871        )
872        .out_json()
873        .await
874        .context("non-json returned for get_config_gen_peers")?;
875        Ok(serde_json::from_value(result)?)
876    }
877
878    pub async fn run_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
879        cmd!(
880            self,
881            "--password",
882            &auth.0,
883            "admin",
884            "dkg",
885            "--ws",
886            endpoint,
887            "run-dkg"
888        )
889        .run()
890        .await
891    }
892
893    pub async fn get_verify_config_hash(
894        self,
895        auth: &ApiAuth,
896        endpoint: &str,
897    ) -> Result<BTreeMap<PeerId, bitcoincore_rpc::bitcoin::hashes::sha256::Hash>> {
898        let result = cmd!(
899            self,
900            "--password",
901            &auth.0,
902            "admin",
903            "dkg",
904            "--ws",
905            endpoint,
906            "get-verify-config-hash"
907        )
908        .out_json()
909        .await
910        .context("non-json returned for get_verify_config_hash")?;
911        Ok(serde_json::from_value(result)?)
912    }
913
914    pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
915        cmd!(
916            self,
917            "--password",
918            &auth.0,
919            "--our-id",
920            our_id,
921            "admin",
922            "shutdown",
923            session_count,
924        )
925        .run()
926        .await
927    }
928
929    pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
930        cmd!(
931            self,
932            "--password",
933            &auth.0,
934            "--our-id",
935            our_id,
936            "admin",
937            "status",
938        )
939        .run()
940        .await
941    }
942
943    pub async fn start_consensus(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
944        cmd!(
945            self,
946            "--password",
947            &auth.0,
948            "admin",
949            "dkg",
950            "--ws",
951            endpoint,
952            "start-consensus"
953        )
954        .run()
955        .await
956    }
957}
958
959pub struct LoadTestTool;
960impl LoadTestTool {
961    pub fn cmd(self) -> Command {
962        to_command(get_command_str_for_alias(
963            &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
964            &[LOAD_TEST_TOOL_FALLBACK],
965        ))
966    }
967}
968
969pub struct GatewayCli;
970impl GatewayCli {
971    pub fn cmd(self) -> Command {
972        to_command(get_command_str_for_alias(
973            &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
974            &get_gateway_cli_path()
975                .iter()
976                .map(String::as_str)
977                .collect::<Vec<_>>(),
978        ))
979    }
980
981    /// Returns the gateway-cli version from clap or default min version
982    pub async fn version_or_default() -> Version {
983        match cmd!(GatewayCli, "--version").out_string().await {
984            Ok(version) => parse_clap_version(&version),
985            Err(_) => DEFAULT_VERSION,
986        }
987    }
988}
989
990pub struct GatewayLndCli;
991impl GatewayLndCli {
992    pub fn cmd(self) -> Command {
993        to_command(get_command_str_for_alias(
994            &[FM_GWCLI_LND_ENV],
995            &["gateway-lnd"],
996        ))
997    }
998}
999
1000pub struct GatewayLdkCli;
1001impl GatewayLdkCli {
1002    pub fn cmd(self) -> Command {
1003        to_command(get_command_str_for_alias(
1004            &[FM_GWCLI_LDK_ENV],
1005            &["gateway-ldk"],
1006        ))
1007    }
1008}
1009
1010pub struct LnCli;
1011impl LnCli {
1012    pub fn cmd(self) -> Command {
1013        to_command(get_command_str_for_alias(
1014            &[FM_LNCLI_ENV],
1015            &get_lncli_path()
1016                .iter()
1017                .map(String::as_str)
1018                .collect::<Vec<_>>(),
1019        ))
1020    }
1021}
1022
1023pub struct BitcoinCli;
1024impl BitcoinCli {
1025    pub fn cmd(self) -> Command {
1026        to_command(get_command_str_for_alias(
1027            &[FM_BTC_CLIENT_ENV],
1028            &get_bitcoin_cli_path()
1029                .iter()
1030                .map(String::as_str)
1031                .collect::<Vec<_>>(),
1032        ))
1033    }
1034}
1035
1036pub struct Bitcoind;
1037impl Bitcoind {
1038    pub fn cmd(self) -> Command {
1039        to_command(get_command_str_for_alias(
1040            &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
1041            &[BITCOIND_FALLBACK],
1042        ))
1043    }
1044}
1045
1046pub struct Lnd;
1047impl Lnd {
1048    pub fn cmd(self) -> Command {
1049        to_command(get_command_str_for_alias(
1050            &[FM_LND_BASE_EXECUTABLE_ENV],
1051            &[LND_FALLBACK],
1052        ))
1053    }
1054}
1055
1056pub struct Electrs;
1057impl Electrs {
1058    pub fn cmd(self) -> Command {
1059        to_command(get_command_str_for_alias(
1060            &[FM_ELECTRS_BASE_EXECUTABLE_ENV],
1061            &[ELECTRS_FALLBACK],
1062        ))
1063    }
1064}
1065
1066pub struct Esplora;
1067impl Esplora {
1068    pub fn cmd(self) -> Command {
1069        to_command(get_command_str_for_alias(
1070            &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
1071            &[ESPLORA_FALLBACK],
1072        ))
1073    }
1074}
1075
1076pub struct Recoverytool;
1077impl Recoverytool {
1078    pub fn cmd(self) -> Command {
1079        to_command(get_command_str_for_alias(
1080            &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
1081            &[RECOVERYTOOL_FALLBACK],
1082        ))
1083    }
1084}
1085
1086pub struct DevimintFaucet;
1087impl DevimintFaucet {
1088    pub fn cmd(self) -> Command {
1089        to_command(get_command_str_for_alias(
1090            &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
1091            &[DEVIMINT_FAUCET_FALLBACK],
1092        ))
1093    }
1094}
1095
1096pub struct Recurringd;
1097impl Recurringd {
1098    pub fn cmd(self) -> Command {
1099        to_command(get_command_str_for_alias(
1100            &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
1101            &["fedimint-recurringd"],
1102        ))
1103    }
1104}
1105
1106fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
1107    // try to use one of the aliases if set
1108    for alias in aliases {
1109        if let Ok(cmd) = std::env::var(alias) {
1110            return cmd.split_whitespace().map(ToOwned::to_owned).collect();
1111        }
1112    }
1113    // otherwise return the default value
1114    default.iter().map(ToString::to_string).collect()
1115}
1116
1117fn to_command(cli: Vec<String>) -> Command {
1118    let mut cmd = tokio::process::Command::new(&cli[0]);
1119    cmd.args(&cli[1..]);
1120    Command {
1121        cmd,
1122        args_debug: cli,
1123    }
1124}
1125
1126pub fn supports_lnv2() -> bool {
1127    std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
1128        || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
1129}
1130
1131/// Returns true if running backwards-compatibility tests
1132pub fn is_backwards_compatibility_test() -> bool {
1133    is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1134}
1135
1136/// Sets the fedimint-cli binary to match the fedimintd's version, which is
1137/// needed for running DKG. Returns the original fedimint-cli path and mint
1138/// client alias so the caller can reset the fedimint-cli version after DKG
1139pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1140    let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1141    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1142    let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1143
1144    if pkg_version == fedimintd_version {
1145        // we're on the current version if the fedimintd version is the same as the
1146        // package version. to use the current version of `fedimint-cli` built by cargo,
1147        // we need to unset FM_FEDIMINT_CLI_BASE_EXECUTABLE
1148        // TODO: Audit that the environment access only happens in single-threaded code.
1149        unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1150    } else {
1151        let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1152
1153        // matches format defined by nix_binary_version_var_name in scripts/_common.sh
1154        let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1155        let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1156        // TODO: Audit that the environment access only happens in single-threaded code.
1157        unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1158    }
1159
1160    let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1161    let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1162    let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1163
1164    let fm_mint_client: String = format!(
1165        "{fedimint_cli} --data-dir {datadir}",
1166        fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1167        datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1168    );
1169    // TODO: Audit that the environment access only happens in single-threaded code.
1170    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1171
1172    Ok((original_fedimint_cli_path, original_fm_mint_client))
1173}
1174
1175/// Sets the fedimint-cli and mint client alias
1176pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1177    // TODO: Audit that the environment access only happens in single-threaded code.
1178    unsafe {
1179        std::env::set_var(
1180            FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1181            original_fedimint_cli_path,
1182        );
1183    };
1184
1185    // TODO: Audit that the environment access only happens in single-threaded code.
1186    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1187}
1188
1189/// Parses a version string returned from clap
1190/// ex: fedimintd 0.3.0-alpha -> 0.3.0-alpha
1191fn parse_clap_version(res: &str) -> Version {
1192    match res.split(' ').collect::<Vec<&str>>().as_slice() {
1193        [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1194        _ => DEFAULT_VERSION,
1195    }
1196}
1197
1198#[test]
1199fn test_parse_clap_version() -> Result<()> {
1200    let version_str = "fedimintd 0.3.0-alpha";
1201    let expected_version = Version::parse("0.3.0-alpha")?;
1202    assert_eq!(expected_version, parse_clap_version(version_str));
1203
1204    let version_str = "fedimintd 0.3.12";
1205    let expected_version = Version::parse("0.3.12")?;
1206    assert_eq!(expected_version, parse_clap_version(version_str));
1207
1208    let version_str = "fedimint-cli 2.12.2-rc22";
1209    let expected_version = Version::parse("2.12.2-rc22")?;
1210    assert_eq!(expected_version, parse_clap_version(version_str));
1211
1212    let version_str = "bad version";
1213    let expected_version = DEFAULT_VERSION;
1214    assert_eq!(expected_version, parse_clap_version(version_str));
1215
1216    Ok(())
1217}
1218
1219mod legacy_types {
1220    use std::collections::BTreeMap;
1221
1222    use fedimint_core::PeerId;
1223    use fedimint_core::admin_client::PeerServerParamsLegacy;
1224    use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
1225    use serde::{Deserialize, Serialize};
1226
1227    /// The config gen params that need to be in consensus, sent by the config
1228    /// gen leader to all the other guardians
1229    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1230    pub struct ConfigGenParamsConsensusLegacy {
1231        /// Endpoints of all servers
1232        pub peers: BTreeMap<PeerId, PeerServerParamsLegacy>,
1233        /// Guardian-defined key-value pairs that will be passed to the client
1234        pub meta: BTreeMap<String, String>,
1235        /// Module init params (also contains local params from us)
1236        pub modules: ServerModuleConfigGenParamsRegistry,
1237    }
1238
1239    /// The config gen params response which includes our peer id
1240    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1241    pub struct ConfigGenParamsResponseLegacy {
1242        /// The same for all peers
1243        pub consensus: ConfigGenParamsConsensusLegacy,
1244        /// Our id (might change if new peers join)
1245        pub our_current_id: PeerId,
1246    }
1247}