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_core::PeerId;
13use fedimint_core::admin_client::SetupStatus;
14use fedimint_core::envs::{
15 FM_ENABLE_MODULE_LNV2_ENV, FM_ENABLE_MODULE_MINTV2_ENV, FM_ENABLE_MODULE_WALLETV2_ENV,
16 is_env_var_set,
17};
18use fedimint_core::module::ApiAuth;
19use fedimint_core::task::{self};
20use fedimint_core::time::now;
21use fedimint_core::util::FmtCompactAnyhow as _;
22use fedimint_core::util::backoff_util::custom_backoff;
23use fedimint_logging::LOG_DEVIMINT;
24use semver::Version;
25use serde::de::DeserializeOwned;
26use tokio::fs::OpenOptions;
27use tokio::process::Child;
28use tokio::sync::Mutex;
29use tracing::debug;
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_ESPLORA_BASE_EXECUTABLE_ENV, FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
36 FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV, FM_FEDIMINTD_BASE_EXECUTABLE_ENV,
37 FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV, FM_GATEWAYD_BASE_EXECUTABLE_ENV, FM_GWCLI_LDK_ENV,
38 FM_GWCLI_LND_ENV, FM_LNCLI_BASE_EXECUTABLE_ENV, FM_LNCLI_ENV, FM_LND_BASE_EXECUTABLE_ENV,
39 FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV, FM_LOGS_DIR_ENV, FM_MINT_CLIENT_ENV,
40 FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV, FM_RECURRINGD_BASE_EXECUTABLE_ENV,
41};
42
43const DEFAULT_VERSION: Version = Version::new(0, 2, 1);
46
47pub fn parse_map(s: &str) -> Result<BTreeMap<String, String>> {
48 let mut map = BTreeMap::new();
49
50 if s.is_empty() {
51 return Ok(map);
52 }
53
54 for pair in s.split(',') {
55 let parts: Vec<&str> = pair.split('=').collect();
56 if parts.len() == 2 {
57 map.insert(parts[0].to_string(), parts[1].to_string());
58 } else {
59 return Err(format_err!("Invalid pair in map: {pair}"));
60 }
61 }
62 Ok(map)
63}
64
65use crate::process_reaper;
66
67#[derive(Debug, Clone)]
72pub struct ProcessHandle(Arc<Mutex<ProcessHandleInner>>);
73
74impl ProcessHandle {
75 pub async fn terminate(&self) -> Result<()> {
76 let mut inner = self.0.lock().await;
77 inner.terminate();
78 Ok(())
79 }
80
81 pub async fn await_terminated(&self) -> Result<()> {
82 let mut inner = self.0.lock().await;
83 inner.await_terminated().await?;
84 Ok(())
85 }
86
87 pub async fn is_running(&self) -> bool {
88 self.0.lock().await.child.is_some()
89 }
90
91 pub async fn id(&self) -> Option<u32> {
92 self.0.lock().await.child.as_ref().and_then(Child::id)
93 }
94}
95
96#[derive(Debug)]
97pub struct ProcessHandleInner {
98 name: String,
99 child: Option<Child>,
100}
101
102impl ProcessHandleInner {
103 fn terminate(&mut self) {
115 if let Some(child) = self.child.take() {
116 debug!(
117 target: LOG_DEVIMINT,
118 name = %self.name,
119 "killing child process"
120 );
121
122 process_reaper::kill_process(&child);
123 process_reaper::reap_killed_processes();
124 }
125 }
126
127 async fn await_terminated(&mut self) -> anyhow::Result<()> {
128 match self
129 .child
130 .as_mut()
131 .expect("Process not running")
132 .wait()
133 .await
134 {
135 Ok(_status) => {
136 debug!(
137 target: LOG_DEVIMINT,
138 name=%self.name,
139 "child process terminated"
140 );
141 }
142 Err(err) => {
143 bail!("Failed to wait for child process {}: {}", self.name, err);
144 }
145 }
146
147 self.child.take();
149 Ok(())
150 }
151}
152
153impl Drop for ProcessHandleInner {
154 fn drop(&mut self) {
155 if self.child.is_none() {
156 return;
157 }
158
159 self.terminate();
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 pub async fn spawn_daemon(&self, name: &str, mut cmd: Command) -> Result<ProcessHandle> {
175 process_reaper::reap_killed_processes();
178 debug!(target: LOG_DEVIMINT, %name, "Spawning daemon");
179 let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
180 let path = format!("{logs_dir}/{name}.log");
181 let log = OpenOptions::new()
182 .append(true)
183 .create(true)
184 .open(path)
185 .await?
186 .into_std()
187 .await;
188 cmd.cmd.kill_on_drop(false); cmd.cmd.stdout(log.try_clone()?);
190 cmd.cmd.stderr(log);
191 let child = cmd
192 .cmd
193 .spawn()
194 .with_context(|| format!("Could not spawn: {name}"))?;
195 let handle = ProcessHandle(Arc::new(Mutex::new(ProcessHandleInner {
196 name: name.to_owned(),
197 child: Some(child),
198 })));
199 Ok(handle)
200 }
201}
202
203pub struct Command {
204 pub cmd: tokio::process::Command,
205 pub args_debug: Vec<String>,
206}
207
208impl Command {
209 pub fn arg<T: ToString>(mut self, arg: &T) -> Self {
210 let string = arg.to_string();
211 self.cmd.arg(string.clone());
212 self.args_debug.push(string);
213 self
214 }
215
216 pub fn args<T: ToString>(mut self, args: impl IntoIterator<Item = T>) -> Self {
217 for arg in args {
218 self = self.arg(&arg);
219 }
220 self
221 }
222
223 pub fn env<K, V>(mut self, key: K, val: V) -> Self
224 where
225 K: AsRef<OsStr>,
226 V: AsRef<OsStr>,
227 {
228 self.cmd.env(key, val);
229 self
230 }
231
232 pub fn envs<I, K, V>(mut self, env: I) -> Self
233 where
234 I: IntoIterator<Item = (K, V)>,
235 K: AsRef<OsStr>,
236 V: AsRef<OsStr>,
237 {
238 self.cmd.envs(env);
239 self
240 }
241
242 pub fn kill_on_drop(mut self, kill: bool) -> Self {
243 self.cmd.kill_on_drop(kill);
244 self
245 }
246
247 pub async fn out_json(&mut self) -> Result<serde_json::Value> {
249 Ok(serde_json::from_str(&self.out_string().await?)?)
250 }
251
252 fn command_debug(&self) -> String {
253 self.args_debug
254 .iter()
255 .map(|x| x.replace(' ', "␣"))
256 .collect::<Vec<_>>()
257 .join(" ")
258 }
259
260 pub async fn out_string(&mut self) -> Result<String> {
262 let output = self
263 .run_inner(true)
264 .await
265 .with_context(|| format!("command: {}", self.command_debug()))?;
266 let output = String::from_utf8(output.stdout)?;
267 Ok(output.trim().to_owned())
268 }
269
270 pub async fn expect_err_json(&mut self) -> Result<serde_json::Value> {
272 let output = self
273 .run_inner(false)
274 .await
275 .with_context(|| format!("command: {}", self.command_debug()))?;
276 let output = String::from_utf8(output.stdout)?;
277 Ok(serde_json::from_str(output.trim())?)
278 }
279
280 pub async fn assert_error(
283 &mut self,
284 predicate: impl Fn(serde_json::Value) -> bool,
285 ) -> Result<()> {
286 let parsed_error = self.expect_err_json().await?;
287 anyhow::ensure!(predicate(parsed_error));
288 Ok(())
289 }
290
291 pub async fn assert_error_contains(&mut self, error: &str) -> Result<()> {
294 self.assert_error(|err_json| {
295 let error_string = err_json
296 .get("error")
297 .expect("json error contains error field")
298 .as_str()
299 .expect("not a string")
300 .to_owned();
301
302 error_string.contains(error)
303 })
304 .await
305 }
306
307 pub async fn run_inner(&mut self, expect_success: bool) -> Result<std::process::Output> {
308 debug!(target: LOG_DEVIMINT, "> {}", self.command_debug());
309 let output = self
310 .cmd
311 .stdout(Stdio::piped())
312 .stderr(if is_env_var_set(FM_DEVIMINT_CMD_INHERIT_STDERR_ENV) {
313 Stdio::inherit()
314 } else {
315 Stdio::piped()
316 })
317 .spawn()?
318 .wait_with_output()
319 .await?;
320
321 if output.status.success() != expect_success {
322 bail!(
323 "{}\nstdout:\n{}\nstderr:\n{}\n",
324 output.status,
325 String::from_utf8_lossy(&output.stdout),
326 String::from_utf8_lossy(&output.stderr),
327 );
328 }
329 Ok(output)
330 }
331
332 pub async fn run(&mut self) -> Result<()> {
334 let _ = self
335 .run_inner(true)
336 .await
337 .with_context(|| format!("command: {}", self.command_debug()))?;
338 Ok(())
339 }
340
341 pub async fn run_with_logging(&mut self, name: String) -> Result<()> {
343 let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
344 let path = format!("{logs_dir}/{name}.log");
345 let log = OpenOptions::new()
346 .append(true)
347 .create(true)
348 .open(&path)
349 .await
350 .with_context(|| format!("path: {path} cmd: {name}"))?
351 .into_std()
352 .await;
353 self.cmd.stdout(log.try_clone()?);
354 self.cmd.stderr(log);
355 let status = self
356 .cmd
357 .spawn()
358 .with_context(|| format!("cmd: {name}"))?
359 .wait()
360 .await?;
361 if !status.success() {
362 bail!("{status}");
363 }
364 Ok(())
365 }
366}
367
368#[macro_export]
380macro_rules! cmd {
381 ($(@head ($($head:tt)* ))? $curr:literal $(, $($tail:tt)*)?) => {
382 cmd! {
383 @head ($($($head)*)? format!($curr),)
384 $($($tail)*)?
385 }
386 };
387 ($(@head ($($head:tt)* ))? $curr:expr_2021 $(, $($tail:tt)*)?) => {
388 cmd! {
389 @head ($($($head)*)? $curr,)
390 $($($tail)*)?
391 }
392 };
393 (@head ($($head:tt)* )) => {
394 cmd! {
395 @last
396 $($head)*
397 }
398 };
399 (@last $this:expr_2021, $($arg:expr_2021),* $(,)?) => {
401 {
402 #[allow(unused)]
403 use $crate::util::ToCmdExt;
404 $this.cmd()
405 $(.arg(&$arg))*
406 .kill_on_drop(true)
407 .env("RUST_BACKTRACE", "1")
408 .env("RUST_LIB_BACKTRACE", "0")
409 }
410 };
411}
412
413#[macro_export]
414macro_rules! poll_eq {
415 ($left:expr_2021, $right:expr_2021) => {
416 match ($left, $right) {
417 (left, right) => {
418 if left == right {
419 Ok(())
420 } else {
421 Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
422 "assertion failed, left: {left:?} right: {right:?}"
423 )))
424 }
425 }
426 }
427 };
428}
429
430#[macro_export]
431macro_rules! poll_almost_equal {
432 ($left:expr_2021, $right:expr_2021) => {
433 match ($left, $right) {
434 (left, right) => $crate::util::almost_equal(left, right, 1_000_000)
435 .map_err(|e| std::ops::ControlFlow::Continue(anyhow::anyhow!(e))),
436 }
437 };
438}
439
440pub fn almost_equal(a: u64, b: u64, max: u64) -> Result<(), String> {
441 if a.abs_diff(b) <= max {
442 Ok(())
443 } else {
444 Err(format!(
445 "Expected difference is {max} but we found {}",
446 a.abs_diff(b)
447 ))
448 }
449}
450
451pub(crate) use cmd;
453
454pub async fn poll_with_timeout<Fut, R>(
461 name: &str,
462 timeout: Duration,
463 f: impl Fn() -> Fut,
464) -> Result<R>
465where
466 Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
467{
468 const MIN_BACKOFF: Duration = Duration::from_millis(50);
469 const MAX_BACKOFF: Duration = Duration::from_secs(1);
470
471 let mut backoff = custom_backoff(MIN_BACKOFF, MAX_BACKOFF, None);
472 let start = now();
473 for attempt in 0u64.. {
474 let attempt_start = now();
475 match f().await {
476 Ok(value) => return Ok(value),
477 Err(ControlFlow::Break(err)) => {
478 return Err(err).with_context(|| format!("polling {name}"));
479 }
480 Err(ControlFlow::Continue(err))
481 if attempt_start
482 .duration_since(start)
483 .expect("time goes forward")
484 < timeout =>
485 {
486 debug!(target: LOG_DEVIMINT, %attempt, err = %err.fmt_compact_anyhow(), "Polling {name} failed, will retry...");
487 task::sleep(backoff.next().unwrap_or(MAX_BACKOFF)).await;
488 }
489 Err(ControlFlow::Continue(err)) => {
490 return Err(err).with_context(|| {
491 format!(
492 "Polling {name} failed after {attempt} retries (timeout: {}s)",
493 timeout.as_secs()
494 )
495 });
496 }
497 }
498 }
499
500 unreachable!();
501}
502
503const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_mins(1);
504const EXTRA_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(90);
505
506pub async fn poll<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
513where
514 Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
515{
516 poll_with_timeout(
517 name,
518 if is_env_var_set("FM_EXTRA_LONG_POLL") {
519 EXTRA_LONG_POLL_TIMEOUT
520 } else {
521 DEFAULT_POLL_TIMEOUT
522 },
523 f,
524 )
525 .await
526}
527
528pub async fn poll_simple<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
529where
530 Fut: Future<Output = Result<R, anyhow::Error>>,
531{
532 poll(name, || async { f().await.map_err(ControlFlow::Continue) }).await
533}
534
535pub trait ToCmdExt {
537 fn cmd(self) -> Command;
538}
539
540impl ToCmdExt for &'_ str {
542 fn cmd(self) -> Command {
543 Command {
544 cmd: tokio::process::Command::new(self),
545 args_debug: vec![self.to_owned()],
546 }
547 }
548}
549
550impl ToCmdExt for Vec<String> {
551 fn cmd(self) -> Command {
552 to_command(self)
553 }
554}
555
556pub trait JsonValueExt {
557 fn to_typed<T: DeserializeOwned>(self) -> Result<T>;
558}
559
560impl JsonValueExt for serde_json::Value {
561 fn to_typed<T: DeserializeOwned>(self) -> Result<T> {
562 Ok(serde_json::from_value(self)?)
563 }
564}
565
566const GATEWAYD_FALLBACK: &str = "gatewayd";
567
568const FEDIMINTD_FALLBACK: &str = "fedimintd";
569
570const FEDIMINT_CLI_FALLBACK: &str = "fedimint-cli";
571
572pub fn get_fedimint_cli_path() -> Vec<String> {
573 get_command_str_for_alias(
574 &[FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV],
575 &[FEDIMINT_CLI_FALLBACK],
576 )
577}
578
579const GATEWAY_CLI_FALLBACK: &str = "gateway-cli";
580
581pub fn get_gateway_cli_path() -> Vec<String> {
582 get_command_str_for_alias(
583 &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
584 &[GATEWAY_CLI_FALLBACK],
585 )
586}
587
588const LOAD_TEST_TOOL_FALLBACK: &str = "fedimint-load-test-tool";
589
590const LNCLI_FALLBACK: &str = "lncli";
591
592pub fn get_lncli_path() -> Vec<String> {
593 get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
594}
595
596const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
597
598pub fn get_bitcoin_cli_path() -> Vec<String> {
599 get_command_str_for_alias(
600 &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
601 &[BITCOIN_CLI_FALLBACK],
602 )
603}
604
605const BITCOIND_FALLBACK: &str = "bitcoind";
606
607const LND_FALLBACK: &str = "lnd";
608
609const ESPLORA_FALLBACK: &str = "esplora";
610
611const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
612
613const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
614
615const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
616
617pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
618 get_command_str_for_alias(
619 &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
620 &[FEDIMINT_DBTOOL_FALLBACK],
621 )
622}
623
624fn version_hash_to_version(version_hash: &str) -> Result<Version> {
626 match version_hash {
627 "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
628 "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
629 _ => Err(anyhow!("no version known for version hash: {version_hash}")),
630 }
631}
632
633pub struct FedimintdCmd;
634impl FedimintdCmd {
635 pub fn cmd(self) -> Command {
636 to_command(get_command_str_for_alias(
637 &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
638 &[FEDIMINTD_FALLBACK],
639 ))
640 }
641
642 pub async fn version_or_default() -> Version {
644 match cmd!(FedimintdCmd, "--version").out_string().await {
645 Ok(version) => parse_clap_version(&version),
646 Err(_) => cmd!(FedimintdCmd, "version-hash")
647 .out_string()
648 .await
649 .map_or(DEFAULT_VERSION, |v| {
650 version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION)
651 }),
652 }
653 }
654}
655
656pub struct Gatewayd;
657impl Gatewayd {
658 pub fn cmd(self) -> Command {
659 to_command(get_command_str_for_alias(
660 &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
661 &[GATEWAYD_FALLBACK],
662 ))
663 }
664
665 pub async fn version_or_default() -> Version {
667 match cmd!(Gatewayd, "--version").out_string().await {
668 Ok(version) => parse_clap_version(&version),
669 Err(_) => cmd!(Gatewayd, "version-hash")
670 .out_string()
671 .await
672 .map_or(DEFAULT_VERSION, |v| {
673 version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION)
674 }),
675 }
676 }
677}
678
679pub struct FedimintCli;
680impl FedimintCli {
681 pub fn cmd(self) -> Command {
682 to_command(get_command_str_for_alias(
683 &[FM_MINT_CLIENT_ENV],
684 &get_fedimint_cli_path()
685 .iter()
686 .map(String::as_str)
687 .collect::<Vec<_>>(),
688 ))
689 }
690
691 pub async fn version_or_default() -> Version {
693 match cmd!(FedimintCli, "--version").out_string().await {
694 Ok(version) => parse_clap_version(&version),
695 Err(_) => DEFAULT_VERSION,
696 }
697 }
698
699 pub async fn set_password(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
700 cmd!(
701 self,
702 "--password",
703 auth.as_str(),
704 "admin",
705 "dkg",
706 "--ws",
707 endpoint,
708 "set-password",
709 )
710 .run()
711 .await
712 }
713
714 pub async fn set_local_params_leader(
715 self,
716 peer: &PeerId,
717 auth: &ApiAuth,
718 endpoint: &str,
719 federation_size: Option<usize>,
720 ) -> Result<String> {
721 let mut command = cmd!(
722 self,
723 "--password",
724 auth.as_str(),
725 "admin",
726 "setup",
727 endpoint,
728 "set-local-params",
729 format!("Devimint Guardian {peer}"),
730 "--federation-name",
731 "Devimint Federation",
732 );
733
734 if let Some(size) = federation_size {
735 command = command.args(["--federation-size", &size.to_string()]);
736 }
737
738 let json = command.out_json().await?;
739
740 Ok(serde_json::from_value(json)?)
741 }
742
743 pub async fn set_local_params_follower(
744 self,
745 peer: &PeerId,
746 auth: &ApiAuth,
747 endpoint: &str,
748 ) -> Result<String> {
749 let json = cmd!(
750 self,
751 "--password",
752 auth.as_str(),
753 "admin",
754 "setup",
755 endpoint,
756 "set-local-params",
757 format!("Devimint Guardian {peer}")
758 )
759 .out_json()
760 .await?;
761
762 Ok(serde_json::from_value(json)?)
763 }
764
765 pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
766 cmd!(
767 self,
768 "--password",
769 auth.as_str(),
770 "admin",
771 "setup",
772 endpoint,
773 "add-peer",
774 params
775 )
776 .run()
777 .await
778 }
779
780 pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
781 let json = cmd!(
782 self,
783 "--password",
784 auth.as_str(),
785 "admin",
786 "setup",
787 endpoint,
788 "status",
789 )
790 .out_json()
791 .await?;
792
793 Ok(serde_json::from_value(json)?)
794 }
795
796 pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
797 cmd!(
798 self,
799 "--password",
800 auth.as_str(),
801 "admin",
802 "setup",
803 endpoint,
804 "start-dkg"
805 )
806 .run()
807 .await
808 }
809
810 pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
811 cmd!(
812 self,
813 "--password",
814 auth.as_str(),
815 "--our-id",
816 our_id,
817 "admin",
818 "shutdown",
819 session_count,
820 )
821 .run()
822 .await
823 }
824
825 pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
826 cmd!(
827 self,
828 "--password",
829 auth.as_str(),
830 "--our-id",
831 our_id,
832 "admin",
833 "status",
834 )
835 .run()
836 .await
837 }
838}
839
840pub struct LoadTestTool;
841impl LoadTestTool {
842 pub fn cmd(self) -> Command {
843 to_command(get_command_str_for_alias(
844 &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
845 &[LOAD_TEST_TOOL_FALLBACK],
846 ))
847 }
848}
849
850pub struct GatewayCli;
851impl GatewayCli {
852 pub fn cmd(self) -> Command {
853 to_command(get_command_str_for_alias(
854 &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
855 &get_gateway_cli_path()
856 .iter()
857 .map(String::as_str)
858 .collect::<Vec<_>>(),
859 ))
860 }
861
862 pub async fn version_or_default() -> Version {
864 match cmd!(GatewayCli, "--version").out_string().await {
865 Ok(version) => parse_clap_version(&version),
866 Err(_) => DEFAULT_VERSION,
867 }
868 }
869}
870
871pub struct GatewayLndCli;
872impl GatewayLndCli {
873 pub fn cmd(self) -> Command {
874 to_command(get_command_str_for_alias(
875 &[FM_GWCLI_LND_ENV],
876 &["gateway-lnd"],
877 ))
878 }
879}
880
881pub struct GatewayLdkCli;
882impl GatewayLdkCli {
883 pub fn cmd(self) -> Command {
884 to_command(get_command_str_for_alias(
885 &[FM_GWCLI_LDK_ENV],
886 &["gateway-ldk"],
887 ))
888 }
889}
890
891pub struct LnCli;
892impl LnCli {
893 pub fn cmd(self) -> Command {
894 to_command(get_command_str_for_alias(
895 &[FM_LNCLI_ENV],
896 &get_lncli_path()
897 .iter()
898 .map(String::as_str)
899 .collect::<Vec<_>>(),
900 ))
901 }
902}
903
904pub struct BitcoinCli;
905impl BitcoinCli {
906 pub fn cmd(self) -> Command {
907 to_command(get_command_str_for_alias(
908 &[FM_BTC_CLIENT_ENV],
909 &get_bitcoin_cli_path()
910 .iter()
911 .map(String::as_str)
912 .collect::<Vec<_>>(),
913 ))
914 }
915}
916
917pub struct Bitcoind;
918impl Bitcoind {
919 pub fn cmd(self) -> Command {
920 to_command(get_command_str_for_alias(
921 &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
922 &[BITCOIND_FALLBACK],
923 ))
924 }
925}
926
927pub struct Lnd;
928impl Lnd {
929 pub fn cmd(self) -> Command {
930 to_command(get_command_str_for_alias(
931 &[FM_LND_BASE_EXECUTABLE_ENV],
932 &[LND_FALLBACK],
933 ))
934 }
935}
936
937pub struct Esplora;
938impl Esplora {
939 pub fn cmd(self) -> Command {
940 to_command(get_command_str_for_alias(
941 &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
942 &[ESPLORA_FALLBACK],
943 ))
944 }
945}
946
947pub struct Recoverytool;
948impl Recoverytool {
949 pub fn cmd(self) -> Command {
950 to_command(get_command_str_for_alias(
951 &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
952 &[RECOVERYTOOL_FALLBACK],
953 ))
954 }
955}
956
957pub struct DevimintFaucet;
958impl DevimintFaucet {
959 pub fn cmd(self) -> Command {
960 to_command(get_command_str_for_alias(
961 &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
962 &[DEVIMINT_FAUCET_FALLBACK],
963 ))
964 }
965}
966
967pub struct Recurringd;
968impl Recurringd {
969 pub fn cmd(self) -> Command {
970 to_command(get_command_str_for_alias(
971 &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
972 &["fedimint-recurringd"],
973 ))
974 }
975}
976
977fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
978 for alias in aliases {
980 if let Ok(cmd) = std::env::var(alias) {
981 return cmd.split_whitespace().map(ToOwned::to_owned).collect();
982 }
983 }
984 default.iter().map(ToString::to_string).collect()
986}
987
988fn to_command(cli: Vec<String>) -> Command {
989 let mut cmd = tokio::process::Command::new(&cli[0]);
990 cmd.args(&cli[1..]);
991 Command {
992 cmd,
993 args_debug: cli,
994 }
995}
996
997pub fn supports_lnv2() -> bool {
998 std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
999 || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
1000}
1001
1002pub fn supports_wallet_v2() -> bool {
1003 is_env_var_set(FM_ENABLE_MODULE_WALLETV2_ENV)
1004}
1005
1006pub fn supports_mint_v2() -> bool {
1007 is_env_var_set(FM_ENABLE_MODULE_MINTV2_ENV)
1008}
1009
1010pub fn is_backwards_compatibility_test() -> bool {
1012 is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1013}
1014
1015pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1019 let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1020 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1021 let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1022
1023 if pkg_version == fedimintd_version {
1024 unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1029 } else {
1030 let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1031
1032 let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1034 let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1035 unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1037 }
1038
1039 let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1040 let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1041 let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1042
1043 let fm_mint_client: String = format!(
1044 "{fedimint_cli} --data-dir {datadir}",
1045 fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1046 datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1047 );
1048 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1050
1051 Ok((original_fedimint_cli_path, original_fm_mint_client))
1052}
1053
1054pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1056 unsafe {
1058 std::env::set_var(
1059 FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1060 original_fedimint_cli_path,
1061 );
1062 };
1063
1064 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1066}
1067
1068fn parse_clap_version(res: &str) -> Version {
1071 match res.split(' ').collect::<Vec<&str>>().as_slice() {
1072 [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1073 _ => DEFAULT_VERSION,
1074 }
1075}
1076
1077#[test]
1078fn test_parse_clap_version() -> Result<()> {
1079 let version_str = "fedimintd 0.3.0-alpha";
1080 let expected_version = Version::parse("0.3.0-alpha")?;
1081 assert_eq!(expected_version, parse_clap_version(version_str));
1082
1083 let version_str = "fedimintd 0.3.12";
1084 let expected_version = Version::parse("0.3.12")?;
1085 assert_eq!(expected_version, parse_clap_version(version_str));
1086
1087 let version_str = "fedimint-cli 2.12.2-rc22";
1088 let expected_version = Version::parse("2.12.2-rc22")?;
1089 assert_eq!(expected_version, parse_clap_version(version_str));
1090
1091 let version_str = "bad version";
1092 let expected_version = DEFAULT_VERSION;
1093 assert_eq!(expected_version, parse_clap_version(version_str));
1094
1095 Ok(())
1096}