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::{PeerServerParams, ServerStatus};
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_LND_ENV, FM_LIGHTNING_CLI_BASE_EXECUTABLE_ENV,
39    FM_LIGHTNING_CLI_ENV, FM_LIGHTNINGD_BASE_EXECUTABLE_ENV, FM_LNCLI_BASE_EXECUTABLE_ENV,
40    FM_LNCLI_ENV, FM_LND_BASE_EXECUTABLE_ENV, FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV,
41    FM_LOGS_DIR_ENV, FM_MINT_CLIENT_ENV, FM_RECOVERYTOOL_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 LIGHTNING_CLI_FALLBACK: &str = "lightning-cli";
556
557pub fn get_lightning_cli_path() -> Vec<String> {
558    get_command_str_for_alias(
559        &[FM_LIGHTNING_CLI_BASE_EXECUTABLE_ENV],
560        &[LIGHTNING_CLI_FALLBACK],
561    )
562}
563
564const LNCLI_FALLBACK: &str = "lncli";
565
566pub fn get_lncli_path() -> Vec<String> {
567    get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
568}
569
570const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
571
572pub fn get_bitcoin_cli_path() -> Vec<String> {
573    get_command_str_for_alias(
574        &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
575        &[BITCOIN_CLI_FALLBACK],
576    )
577}
578
579const BITCOIND_FALLBACK: &str = "bitcoind";
580
581const LIGHTNINGD_FALLBACK: &str = "lightningd";
582
583const LND_FALLBACK: &str = "lnd";
584
585const ELECTRS_FALLBACK: &str = "electrs";
586
587const ESPLORA_FALLBACK: &str = "esplora";
588
589const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
590
591const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
592
593const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
594
595pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
596    get_command_str_for_alias(
597        &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
598        &[FEDIMINT_DBTOOL_FALLBACK],
599    )
600}
601
602/// Maps a version hash to a release version
603fn version_hash_to_version(version_hash: &str) -> Result<Version> {
604    match version_hash {
605        "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
606        "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
607        _ => Err(anyhow!("no version known for version hash: {version_hash}")),
608    }
609}
610
611pub struct FedimintdCmd;
612impl FedimintdCmd {
613    pub fn cmd(self) -> Command {
614        to_command(get_command_str_for_alias(
615            &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
616            &[FEDIMINTD_FALLBACK],
617        ))
618    }
619
620    /// Returns the fedimintd version from clap or default min version
621    pub async fn version_or_default() -> Version {
622        match cmd!(FedimintdCmd, "--version").out_string().await {
623            Ok(version) => parse_clap_version(&version),
624            Err(_) => cmd!(FedimintdCmd, "version-hash")
625                .out_string()
626                .await
627                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
628                .unwrap_or(DEFAULT_VERSION),
629        }
630    }
631}
632
633pub struct Gatewayd;
634impl Gatewayd {
635    pub fn cmd(self) -> Command {
636        to_command(get_command_str_for_alias(
637            &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
638            &[GATEWAYD_FALLBACK],
639        ))
640    }
641
642    /// Returns the gatewayd version from clap or default min version
643    pub async fn version_or_default() -> Version {
644        match cmd!(Gatewayd, "--version").out_string().await {
645            Ok(version) => parse_clap_version(&version),
646            Err(_) => cmd!(Gatewayd, "version-hash")
647                .out_string()
648                .await
649                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
650                .unwrap_or(DEFAULT_VERSION),
651        }
652    }
653}
654
655pub struct FedimintCli;
656impl FedimintCli {
657    pub fn cmd(self) -> Command {
658        to_command(get_command_str_for_alias(
659            &[FM_MINT_CLIENT_ENV],
660            &get_fedimint_cli_path()
661                .iter()
662                .map(String::as_str)
663                .collect::<Vec<_>>(),
664        ))
665    }
666
667    /// Returns the fedimint-cli version from clap or default min version
668    pub async fn version_or_default() -> Version {
669        match cmd!(FedimintCli, "--version").out_string().await {
670            Ok(version) => parse_clap_version(&version),
671            Err(_) => DEFAULT_VERSION,
672        }
673    }
674
675    pub async fn ws_status(self, endpoint: &str) -> Result<StatusResponse> {
676        let status = cmd!(self, "admin", "dkg", "--ws", endpoint, "ws-status")
677            .out_json()
678            .await?;
679        Ok(serde_json::from_value(status)?)
680    }
681
682    pub async fn set_password(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
683        cmd!(
684            self,
685            "--password",
686            &auth.0,
687            "admin",
688            "dkg",
689            "--ws",
690            endpoint,
691            "set-password",
692        )
693        .run()
694        .await
695    }
696
697    pub async fn set_local_params_leader(
698        self,
699        peer: &PeerId,
700        auth: &ApiAuth,
701        endpoint: &str,
702    ) -> Result<String> {
703        let json = cmd!(
704            self,
705            "--password",
706            &auth.0,
707            "admin",
708            "config-gen",
709            "--ws",
710            endpoint,
711            "set-local-params",
712            format!("Devimint Guardian {peer}"),
713            "--federation-name",
714            "Devimint Federation"
715        )
716        .out_json()
717        .await?;
718
719        Ok(serde_json::from_value(json)?)
720    }
721
722    pub async fn set_local_params_follower(
723        self,
724        peer: &PeerId,
725        auth: &ApiAuth,
726        endpoint: &str,
727    ) -> Result<String> {
728        let json = cmd!(
729            self,
730            "--password",
731            &auth.0,
732            "admin",
733            "config-gen",
734            "--ws",
735            endpoint,
736            "set-local-params",
737            format!("Devimint Guardian {peer}")
738        )
739        .out_json()
740        .await?;
741
742        Ok(serde_json::from_value(json)?)
743    }
744
745    pub async fn add_peer_connection_info(
746        self,
747        params: &str,
748        auth: &ApiAuth,
749        endpoint: &str,
750    ) -> Result<()> {
751        cmd!(
752            self,
753            "--password",
754            &auth.0,
755            "admin",
756            "config-gen",
757            "--ws",
758            endpoint,
759            "add-peer-connection-info",
760            params
761        )
762        .run()
763        .await
764    }
765
766    pub async fn server_status(self, auth: &ApiAuth, endpoint: &str) -> Result<ServerStatus> {
767        let json = cmd!(
768            self,
769            "--password",
770            &auth.0,
771            "admin",
772            "config-gen",
773            "--ws",
774            endpoint,
775            "server-status",
776        )
777        .out_json()
778        .await?;
779
780        Ok(serde_json::from_value(json)?)
781    }
782
783    pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
784        cmd!(
785            self,
786            "--password",
787            &auth.0,
788            "admin",
789            "config-gen",
790            "--ws",
791            endpoint,
792            "start-dkg"
793        )
794        .run()
795        .await
796    }
797
798    pub async fn set_config_gen_params(
799        self,
800        auth: &ApiAuth,
801        endpoint: &str,
802        meta: BTreeMap<String, String>,
803        server_gen_params: ServerModuleConfigGenParamsRegistry,
804    ) -> Result<()> {
805        cmd!(
806            self,
807            "--password",
808            &auth.0,
809            "admin",
810            "dkg",
811            "--ws",
812            endpoint,
813            "set-config-gen-params",
814            "--meta-json",
815            serde_json::to_string(&meta)?,
816            "--modules-json",
817            serde_json::to_string(&server_gen_params)?
818        )
819        .run()
820        .await
821    }
822
823    pub async fn consensus_config_gen_params_legacy(
824        self,
825        endpoint: &str,
826    ) -> Result<ConfigGenParamsResponseLegacy> {
827        let result = cmd!(
828            self,
829            "admin",
830            "dkg",
831            "--ws",
832            endpoint,
833            "consensus-config-gen-params"
834        )
835        .out_json()
836        .await
837        .context("non-json returned for consensus_config_gen_params")?;
838        Ok(serde_json::from_value(result)?)
839    }
840
841    pub async fn set_config_gen_connections(
842        self,
843        auth: &ApiAuth,
844        endpoint: &str,
845        our_name: &str,
846        leader_api_url: Option<&str>,
847    ) -> Result<()> {
848        // FIXME: this should be a single command
849        if let Some(leader_api_url) = leader_api_url {
850            cmd!(
851                self,
852                "--password",
853                &auth.0,
854                "admin",
855                "dkg",
856                "--ws",
857                endpoint,
858                "set-config-gen-connections",
859                "--our-name",
860                our_name,
861                "--leader-api-url",
862                leader_api_url,
863            )
864            .run()
865            .await
866        } else {
867            cmd!(
868                self,
869                "--password",
870                &auth.0,
871                "admin",
872                "dkg",
873                "--ws",
874                endpoint,
875                "set-config-gen-connections",
876                "--our-name",
877                our_name,
878            )
879            .run()
880            .await
881        }
882    }
883
884    pub async fn get_config_gen_peers(self, endpoint: &str) -> Result<Vec<PeerServerParams>> {
885        let result = cmd!(
886            self,
887            "admin",
888            "dkg",
889            "--ws",
890            endpoint,
891            "get-config-gen-peers"
892        )
893        .out_json()
894        .await
895        .context("non-json returned for get_config_gen_peers")?;
896        Ok(serde_json::from_value(result)?)
897    }
898
899    pub async fn run_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
900        cmd!(
901            self,
902            "--password",
903            &auth.0,
904            "admin",
905            "dkg",
906            "--ws",
907            endpoint,
908            "run-dkg"
909        )
910        .run()
911        .await
912    }
913
914    pub async fn get_verify_config_hash(
915        self,
916        auth: &ApiAuth,
917        endpoint: &str,
918    ) -> Result<BTreeMap<PeerId, bitcoincore_rpc::bitcoin::hashes::sha256::Hash>> {
919        let result = cmd!(
920            self,
921            "--password",
922            &auth.0,
923            "admin",
924            "dkg",
925            "--ws",
926            endpoint,
927            "get-verify-config-hash"
928        )
929        .out_json()
930        .await
931        .context("non-json returned for get_verify_config_hash")?;
932        Ok(serde_json::from_value(result)?)
933    }
934
935    pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
936        cmd!(
937            self,
938            "--password",
939            &auth.0,
940            "--our-id",
941            our_id,
942            "admin",
943            "shutdown",
944            session_count,
945        )
946        .run()
947        .await
948    }
949
950    pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
951        cmd!(
952            self,
953            "--password",
954            &auth.0,
955            "--our-id",
956            our_id,
957            "admin",
958            "status",
959        )
960        .run()
961        .await
962    }
963
964    pub async fn start_consensus(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
965        cmd!(
966            self,
967            "--password",
968            &auth.0,
969            "admin",
970            "dkg",
971            "--ws",
972            endpoint,
973            "start-consensus"
974        )
975        .run()
976        .await
977    }
978}
979
980pub struct LoadTestTool;
981impl LoadTestTool {
982    pub fn cmd(self) -> Command {
983        to_command(get_command_str_for_alias(
984            &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
985            &[LOAD_TEST_TOOL_FALLBACK],
986        ))
987    }
988}
989
990pub struct GatewayCli;
991impl GatewayCli {
992    pub fn cmd(self) -> Command {
993        to_command(get_command_str_for_alias(
994            &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
995            &get_gateway_cli_path()
996                .iter()
997                .map(String::as_str)
998                .collect::<Vec<_>>(),
999        ))
1000    }
1001
1002    /// Returns the gateway-cli version from clap or default min version
1003    pub async fn version_or_default() -> Version {
1004        match cmd!(GatewayCli, "--version").out_string().await {
1005            Ok(version) => parse_clap_version(&version),
1006            Err(_) => DEFAULT_VERSION,
1007        }
1008    }
1009}
1010
1011pub struct GatewayLndCli;
1012impl GatewayLndCli {
1013    pub fn cmd(self) -> Command {
1014        to_command(get_command_str_for_alias(
1015            &[FM_GWCLI_LND_ENV],
1016            &["gateway-lnd"],
1017        ))
1018    }
1019}
1020
1021pub struct LnCli;
1022impl LnCli {
1023    pub fn cmd(self) -> Command {
1024        to_command(get_command_str_for_alias(
1025            &[FM_LNCLI_ENV],
1026            &get_lncli_path()
1027                .iter()
1028                .map(String::as_str)
1029                .collect::<Vec<_>>(),
1030        ))
1031    }
1032}
1033
1034pub struct ClnLightningCli;
1035impl ClnLightningCli {
1036    pub fn cmd(self) -> Command {
1037        to_command(get_command_str_for_alias(
1038            &[FM_LIGHTNING_CLI_ENV],
1039            &get_lightning_cli_path()
1040                .iter()
1041                .map(String::as_str)
1042                .collect::<Vec<_>>(),
1043        ))
1044    }
1045}
1046
1047pub struct BitcoinCli;
1048impl BitcoinCli {
1049    pub fn cmd(self) -> Command {
1050        to_command(get_command_str_for_alias(
1051            &[FM_BTC_CLIENT_ENV],
1052            &get_bitcoin_cli_path()
1053                .iter()
1054                .map(String::as_str)
1055                .collect::<Vec<_>>(),
1056        ))
1057    }
1058}
1059
1060pub struct Bitcoind;
1061impl Bitcoind {
1062    pub fn cmd(self) -> Command {
1063        to_command(get_command_str_for_alias(
1064            &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
1065            &[BITCOIND_FALLBACK],
1066        ))
1067    }
1068}
1069
1070pub struct Lightningd;
1071impl Lightningd {
1072    pub fn cmd(self) -> Command {
1073        to_command(get_command_str_for_alias(
1074            &[FM_LIGHTNINGD_BASE_EXECUTABLE_ENV],
1075            &[LIGHTNINGD_FALLBACK],
1076        ))
1077    }
1078}
1079
1080pub struct Lnd;
1081impl Lnd {
1082    pub fn cmd(self) -> Command {
1083        to_command(get_command_str_for_alias(
1084            &[FM_LND_BASE_EXECUTABLE_ENV],
1085            &[LND_FALLBACK],
1086        ))
1087    }
1088}
1089
1090pub struct Electrs;
1091impl Electrs {
1092    pub fn cmd(self) -> Command {
1093        to_command(get_command_str_for_alias(
1094            &[FM_ELECTRS_BASE_EXECUTABLE_ENV],
1095            &[ELECTRS_FALLBACK],
1096        ))
1097    }
1098}
1099
1100pub struct Esplora;
1101impl Esplora {
1102    pub fn cmd(self) -> Command {
1103        to_command(get_command_str_for_alias(
1104            &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
1105            &[ESPLORA_FALLBACK],
1106        ))
1107    }
1108}
1109
1110pub struct Recoverytool;
1111impl Recoverytool {
1112    pub fn cmd(self) -> Command {
1113        to_command(get_command_str_for_alias(
1114            &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
1115            &[RECOVERYTOOL_FALLBACK],
1116        ))
1117    }
1118}
1119
1120pub struct DevimintFaucet;
1121impl DevimintFaucet {
1122    pub fn cmd(self) -> Command {
1123        to_command(get_command_str_for_alias(
1124            &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
1125            &[DEVIMINT_FAUCET_FALLBACK],
1126        ))
1127    }
1128}
1129
1130fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
1131    // try to use one of the aliases if set
1132    for alias in aliases {
1133        if let Ok(cmd) = std::env::var(alias) {
1134            return cmd.split_whitespace().map(ToOwned::to_owned).collect();
1135        }
1136    }
1137    // otherwise return the default value
1138    default.iter().map(ToString::to_string).collect()
1139}
1140
1141fn to_command(cli: Vec<String>) -> Command {
1142    let mut cmd = tokio::process::Command::new(&cli[0]);
1143    cmd.args(&cli[1..]);
1144    Command {
1145        cmd,
1146        args_debug: cli,
1147    }
1148}
1149
1150pub fn supports_lnv2() -> bool {
1151    is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
1152}
1153
1154/// Returns true if running backwards-compatibility tests
1155pub fn is_backwards_compatibility_test() -> bool {
1156    is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1157}
1158
1159/// Sets the fedimint-cli binary to match the fedimintd's version, which is
1160/// needed for running DKG. Returns the original fedimint-cli path and mint
1161/// client alias so the caller can reset the fedimint-cli version after DKG
1162pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1163    let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1164    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1165    let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1166
1167    if pkg_version == fedimintd_version {
1168        // we're on the current version if the fedimintd version is the same as the
1169        // package version. to use the current version of `fedimint-cli` built by cargo,
1170        // we need to unset FM_FEDIMINT_CLI_BASE_EXECUTABLE
1171        // TODO: Audit that the environment access only happens in single-threaded code.
1172        unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1173    } else {
1174        let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1175
1176        // matches format defined by nix_binary_version_var_name in scripts/_common.sh
1177        let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1178        let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1179        // TODO: Audit that the environment access only happens in single-threaded code.
1180        unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1181    }
1182
1183    let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1184    let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1185    let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1186
1187    let fm_mint_client: String = format!(
1188        "{fedimint_cli} --data-dir {datadir}",
1189        fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1190        datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1191    );
1192    // TODO: Audit that the environment access only happens in single-threaded code.
1193    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1194
1195    Ok((original_fedimint_cli_path, original_fm_mint_client))
1196}
1197
1198/// Sets the fedimint-cli and mint client alias
1199pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1200    // TODO: Audit that the environment access only happens in single-threaded code.
1201    unsafe {
1202        std::env::set_var(
1203            FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1204            original_fedimint_cli_path,
1205        );
1206    };
1207
1208    // TODO: Audit that the environment access only happens in single-threaded code.
1209    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1210}
1211
1212/// Parses a version string returned from clap
1213/// ex: fedimintd 0.3.0-alpha -> 0.3.0-alpha
1214fn parse_clap_version(res: &str) -> Version {
1215    match res.split(' ').collect::<Vec<&str>>().as_slice() {
1216        [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1217        _ => DEFAULT_VERSION,
1218    }
1219}
1220
1221#[test]
1222fn test_parse_clap_version() -> Result<()> {
1223    let version_str = "fedimintd 0.3.0-alpha";
1224    let expected_version = Version::parse("0.3.0-alpha")?;
1225    assert_eq!(expected_version, parse_clap_version(version_str));
1226
1227    let version_str = "fedimintd 0.3.12";
1228    let expected_version = Version::parse("0.3.12")?;
1229    assert_eq!(expected_version, parse_clap_version(version_str));
1230
1231    let version_str = "fedimint-cli 2.12.2-rc22";
1232    let expected_version = Version::parse("2.12.2-rc22")?;
1233    assert_eq!(expected_version, parse_clap_version(version_str));
1234
1235    let version_str = "bad version";
1236    let expected_version = DEFAULT_VERSION;
1237    assert_eq!(expected_version, parse_clap_version(version_str));
1238
1239    Ok(())
1240}
1241
1242mod legacy_types {
1243    use std::collections::BTreeMap;
1244
1245    use fedimint_core::PeerId;
1246    use fedimint_core::admin_client::PeerServerParams;
1247    use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
1248    use serde::{Deserialize, Serialize};
1249
1250    /// The config gen params that need to be in consensus, sent by the config
1251    /// gen leader to all the other guardians
1252    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1253    pub struct ConfigGenParamsConsensusLegacy {
1254        /// Endpoints of all servers
1255        pub peers: BTreeMap<PeerId, PeerServerParams>,
1256        /// Guardian-defined key-value pairs that will be passed to the client
1257        pub meta: BTreeMap<String, String>,
1258        /// Module init params (also contains local params from us)
1259        pub modules: ServerModuleConfigGenParamsRegistry,
1260    }
1261
1262    /// The config gen params response which includes our peer id
1263    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1264    pub struct ConfigGenParamsResponseLegacy {
1265        /// The same for all peers
1266        pub consensus: ConfigGenParamsConsensusLegacy,
1267        /// Our id (might change if new peers join)
1268        pub our_current_id: PeerId,
1269    }
1270}