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