Skip to main content

fedimint_cli/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::doc_markdown)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::module_name_repetitions)]
6#![allow(clippy::must_use_candidate)]
7#![allow(clippy::ref_option)]
8#![allow(clippy::return_self_not_must_use)]
9#![allow(clippy::too_many_lines)]
10#![allow(clippy::large_futures)]
11
12mod client;
13pub mod envs;
14mod utils;
15
16use core::fmt;
17use std::collections::BTreeMap;
18use std::fmt::Debug;
19use std::io::{Read, Write};
20use std::path::{Path, PathBuf};
21use std::process::exit;
22use std::str::FromStr;
23use std::sync::Arc;
24use std::time::Duration;
25use std::{fs, result};
26
27use anyhow::{Context, format_err};
28use clap::{Args, CommandFactory, Parser, Subcommand};
29use client::ModuleSelector;
30#[cfg(feature = "tor")]
31use envs::FM_USE_TOR_ENV;
32use envs::{FM_API_SECRET_ENV, FM_DB_BACKEND_ENV, FM_IROH_ENABLE_DHT_ENV, SALT_FILE};
33use fedimint_aead::{encrypted_read, encrypted_write, get_encryption_key};
34use fedimint_api_client::api::{DynGlobalApi, FederationApiExt, FederationError};
35use fedimint_bip39::{Bip39RootSecretStrategy, Mnemonic};
36use fedimint_client::module::meta::{FetchKind, LegacyMetaSource, MetaSource};
37use fedimint_client::module::module::init::ClientModuleInit;
38use fedimint_client::module_init::ClientModuleInitRegistry;
39use fedimint_client::secret::RootSecretStrategy;
40use fedimint_client::{AdminCreds, Client, ClientBuilder, ClientHandleArc, RootSecret};
41use fedimint_connectors::ConnectorRegistry;
42use fedimint_core::base32::FEDIMINT_PREFIX;
43use fedimint_core::config::{FederationId, FederationIdPrefix};
44use fedimint_core::core::{ModuleInstanceId, OperationId};
45use fedimint_core::db::{Database, DatabaseValue};
46use fedimint_core::encoding::Decodable;
47use fedimint_core::invite_code::InviteCode;
48use fedimint_core::module::{ApiAuth, ApiRequestErased};
49use fedimint_core::setup_code::PeerSetupCode;
50use fedimint_core::transaction::Transaction;
51use fedimint_core::util::{SafeUrl, backoff_util, handle_version_hash_command, retry};
52use fedimint_core::{
53    Amount, PeerId, TieredMulti, base32, fedimint_build_code_version_env, runtime,
54};
55use fedimint_eventlog::{EventLogId, EventLogTrimableId};
56use fedimint_ln_client::LightningClientInit;
57use fedimint_logging::{LOG_CLIENT, TracingSetup};
58use fedimint_meta_client::{MetaClientInit, MetaModuleMetaSourceWithFallback};
59use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, SpendableNote};
60use fedimint_wallet_client::api::WalletFederationApi;
61use fedimint_wallet_client::{WalletClientInit, WalletClientModule};
62use futures::future::pending;
63use itertools::Itertools;
64use rand::thread_rng;
65use serde::{Deserialize, Serialize};
66use serde_json::{Value, json};
67use thiserror::Error;
68use tracing::{debug, info, warn};
69use utils::parse_peer_id;
70
71use crate::client::ClientCmd;
72use crate::envs::{FM_CLIENT_DIR_ENV, FM_IROH_ENABLE_NEXT_ENV, FM_OUR_ID_ENV, FM_PASSWORD_ENV};
73
74/// Type of output the cli produces
75#[derive(Serialize)]
76#[serde(rename_all = "snake_case")]
77#[serde(untagged)]
78enum CliOutput {
79    VersionHash {
80        hash: String,
81    },
82
83    UntypedApiOutput {
84        value: Value,
85    },
86
87    WaitBlockCount {
88        reached: u64,
89    },
90
91    InviteCode {
92        invite_code: InviteCode,
93    },
94
95    DecodeInviteCode {
96        url: SafeUrl,
97        federation_id: FederationId,
98    },
99
100    Join {
101        joined: String,
102    },
103
104    DecodeTransaction {
105        transaction: String,
106    },
107
108    EpochCount {
109        count: u64,
110    },
111
112    ConfigDecrypt,
113
114    ConfigEncrypt,
115
116    SetupCode {
117        setup_code: PeerSetupCode,
118    },
119
120    Raw(serde_json::Value),
121}
122
123impl fmt::Display for CliOutput {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "{}", serde_json::to_string_pretty(self).unwrap())
126    }
127}
128
129/// `Result` with `CliError` as `Error`
130type CliResult<E> = Result<E, CliError>;
131
132/// `Result` with `CliError` as `Error` and `CliOutput` as `Ok`
133type CliOutputResult = Result<CliOutput, CliError>;
134
135/// Cli error
136#[derive(Serialize, Error)]
137#[serde(tag = "error", rename_all(serialize = "snake_case"))]
138struct CliError {
139    error: String,
140}
141
142/// Extension trait making turning Results/Errors into
143/// [`CliError`]/[`CliOutputResult`] easier
144trait CliResultExt<O, E> {
145    /// Map error into `CliError` wrapping the original error message
146    fn map_err_cli(self) -> Result<O, CliError>;
147    /// Map error into `CliError` using custom error message `msg`
148    fn map_err_cli_msg(self, msg: impl fmt::Display + Send + Sync + 'static)
149    -> Result<O, CliError>;
150}
151
152impl<O, E> CliResultExt<O, E> for result::Result<O, E>
153where
154    E: Into<anyhow::Error>,
155{
156    fn map_err_cli(self) -> Result<O, CliError> {
157        self.map_err(|e| {
158            let e = e.into();
159            CliError {
160                error: format!("{e:#}"),
161            }
162        })
163    }
164
165    fn map_err_cli_msg(
166        self,
167        msg: impl fmt::Display + Send + Sync + 'static,
168    ) -> Result<O, CliError> {
169        self.map_err(|e| Into::<anyhow::Error>::into(e))
170            .context(msg)
171            .map_err(|e| CliError {
172                error: format!("{e:#}"),
173            })
174    }
175}
176
177/// Extension trait to make turning `Option`s into
178/// [`CliError`]/[`CliOutputResult`] easier
179trait CliOptionExt<O> {
180    fn ok_or_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError>;
181}
182
183impl<O> CliOptionExt<O> for Option<O> {
184    fn ok_or_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError> {
185        self.ok_or_else(|| CliError { error: msg.into() })
186    }
187}
188
189// TODO: Refactor federation API errors to just delegate to this
190impl From<FederationError> for CliError {
191    fn from(e: FederationError) -> Self {
192        CliError {
193            error: e.to_string(),
194        }
195    }
196}
197
198impl Debug for CliError {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        f.debug_struct("CliError")
201            .field("error", &self.error)
202            .finish()
203    }
204}
205
206impl fmt::Display for CliError {
207    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
208        let json = serde_json::to_value(self).expect("CliError is valid json");
209        let json_as_string =
210            serde_json::to_string_pretty(&json).expect("valid json is serializable");
211        write!(f, "{json_as_string}")
212    }
213}
214
215#[derive(Debug, Clone, Copy, clap::ValueEnum)]
216enum DatabaseBackend {
217    /// Use RocksDB database backend
218    #[value(name = "rocksdb")]
219    RocksDb,
220    /// Use CursedRedb database backend (hybrid memory/redb)
221    #[value(name = "cursed-redb")]
222    CursedRedb,
223}
224
225#[derive(Parser, Clone)]
226#[command(version)]
227struct Opts {
228    /// The working directory of the client containing the config and db
229    #[arg(long = "data-dir", env = FM_CLIENT_DIR_ENV)]
230    data_dir: Option<PathBuf>,
231
232    /// Peer id of the guardian
233    #[arg(env = FM_OUR_ID_ENV, long, value_parser = parse_peer_id)]
234    our_id: Option<PeerId>,
235
236    /// Guardian password for authentication
237    #[arg(long, env = FM_PASSWORD_ENV)]
238    password: Option<String>,
239
240    #[cfg(feature = "tor")]
241    /// Activate usage of Tor as the Connector when building the Client
242    #[arg(long, env = FM_USE_TOR_ENV)]
243    use_tor: bool,
244
245    // Enable using DHT name resolution in Iroh
246    #[arg(long, env = FM_IROH_ENABLE_DHT_ENV)]
247    iroh_enable_dht: Option<bool>,
248
249    // Enable using (in parallel) unstable/next Iroh stack
250    #[arg(long, env = FM_IROH_ENABLE_NEXT_ENV)]
251    iroh_enable_next: Option<bool>,
252
253    /// Database backend to use.
254    #[arg(long, env = FM_DB_BACKEND_ENV, value_enum, default_value = "rocksdb")]
255    db_backend: DatabaseBackend,
256
257    /// Activate more verbose logging, for full control use the RUST_LOG env
258    /// variable
259    #[arg(short = 'v', long)]
260    verbose: bool,
261
262    #[clap(subcommand)]
263    command: Command,
264}
265
266impl Opts {
267    fn data_dir(&self) -> CliResult<&PathBuf> {
268        self.data_dir
269            .as_ref()
270            .ok_or_cli_msg("`--data-dir=` argument not set.")
271    }
272
273    /// Get and create if doesn't exist the data dir
274    async fn data_dir_create(&self) -> CliResult<&PathBuf> {
275        let dir = self.data_dir()?;
276
277        tokio::fs::create_dir_all(&dir).await.map_err_cli()?;
278
279        Ok(dir)
280    }
281    fn iroh_enable_dht(&self) -> bool {
282        self.iroh_enable_dht.unwrap_or(true)
283    }
284
285    fn iroh_enable_next(&self) -> bool {
286        self.iroh_enable_next.unwrap_or(true)
287    }
288
289    fn use_tor(&self) -> bool {
290        #[cfg(feature = "tor")]
291        return self.use_tor;
292        #[cfg(not(feature = "tor"))]
293        false
294    }
295
296    async fn admin_client(
297        &self,
298        peer_urls: &BTreeMap<PeerId, SafeUrl>,
299        api_secret: Option<&str>,
300    ) -> CliResult<DynGlobalApi> {
301        let our_id = self.our_id.ok_or_cli_msg("Admin client needs our-id set")?;
302
303        DynGlobalApi::new_admin(
304            self.make_endpoints().await.map_err(|e| CliError {
305                error: e.to_string(),
306            })?,
307            our_id,
308            peer_urls
309                .get(&our_id)
310                .cloned()
311                .context("Our peer URL not found in config")
312                .map_err_cli()?,
313            api_secret,
314        )
315        .map_err_cli()
316    }
317
318    async fn make_endpoints(&self) -> Result<ConnectorRegistry, anyhow::Error> {
319        ConnectorRegistry::build_from_client_defaults()
320            .iroh_next(self.iroh_enable_next())
321            .iroh_pkarr_dht(self.iroh_enable_dht())
322            .ws_force_tor(self.use_tor())
323            .bind()
324            .await
325    }
326
327    fn auth(&self) -> CliResult<ApiAuth> {
328        let password = self
329            .password
330            .clone()
331            .ok_or_cli_msg("CLI needs password set")?;
332        Ok(ApiAuth(password))
333    }
334
335    async fn load_database(&self) -> CliResult<Database> {
336        debug!(target: LOG_CLIENT, "Loading client database");
337        let db_path = self.data_dir_create().await?.join("client.db");
338        match self.db_backend {
339            DatabaseBackend::RocksDb => {
340                debug!(target: LOG_CLIENT, "Using RocksDB database backend");
341                Ok(fedimint_rocksdb::RocksDb::build(db_path)
342                    .open()
343                    .await
344                    .map_err_cli_msg("could not open rocksdb database")?
345                    .into())
346            }
347            DatabaseBackend::CursedRedb => {
348                debug!(target: LOG_CLIENT, "Using CursedRedb database backend");
349                Ok(fedimint_cursed_redb::MemAndRedb::new(db_path)
350                    .await
351                    .map_err_cli_msg("could not open cursed redb database")?
352                    .into())
353            }
354        }
355    }
356}
357
358async fn load_or_generate_mnemonic(db: &Database) -> Result<Mnemonic, CliError> {
359    Ok(
360        if let Ok(entropy) = Client::load_decodable_client_secret::<Vec<u8>>(db).await {
361            Mnemonic::from_entropy(&entropy).map_err_cli()?
362        } else {
363            debug!(
364                target: LOG_CLIENT,
365                "Generating mnemonic and writing entropy to client storage"
366            );
367            let mnemonic = Bip39RootSecretStrategy::<12>::random(&mut thread_rng());
368            Client::store_encodable_client_secret(db, mnemonic.to_entropy())
369                .await
370                .map_err_cli()?;
371            mnemonic
372        },
373    )
374}
375
376#[derive(Subcommand, Clone)]
377enum Command {
378    /// Print the latest Git commit hash this bin. was built with.
379    VersionHash,
380
381    #[clap(flatten)]
382    Client(client::ClientCmd),
383
384    #[clap(subcommand)]
385    Admin(AdminCmd),
386
387    #[clap(subcommand)]
388    Dev(DevCmd),
389
390    /// Config enabling client to establish websocket connection to federation
391    InviteCode {
392        peer: PeerId,
393    },
394
395    /// Join a federation using its InviteCode
396    #[clap(alias = "join-federation")]
397    Join {
398        invite_code: String,
399    },
400
401    Completion {
402        shell: clap_complete::Shell,
403    },
404}
405
406#[allow(clippy::large_enum_variant)]
407#[derive(Debug, Clone, Subcommand)]
408enum AdminCmd {
409    /// Show the status according to the `status` endpoint
410    Status,
411
412    /// Show an audit across all modules
413    Audit,
414
415    /// Download guardian config to back it up
416    GuardianConfigBackup,
417
418    Setup(SetupAdminArgs),
419    /// Sign and announce a new API endpoint. The previous one will be
420    /// invalidated
421    SignApiAnnouncement {
422        /// New API URL to announce
423        api_url: SafeUrl,
424        /// Provide the API url for the guardian directly in case the old one
425        /// isn't reachable anymore
426        #[clap(long)]
427        override_url: Option<SafeUrl>,
428    },
429    /// Stop fedimintd after the specified session to do a coordinated upgrade
430    Shutdown {
431        /// Session index to stop after
432        session_idx: u64,
433    },
434    /// Show statistics about client backups stored by the federation
435    BackupStatistics,
436    /// Change guardian password, will shut down fedimintd and require manual
437    /// restart
438    ChangePassword {
439        /// New password to set
440        new_password: String,
441    },
442}
443
444#[derive(Debug, Clone, Args)]
445struct SetupAdminArgs {
446    endpoint: SafeUrl,
447
448    #[clap(subcommand)]
449    subcommand: SetupAdminCmd,
450}
451
452#[derive(Debug, Clone, Subcommand)]
453enum SetupAdminCmd {
454    Status,
455    SetLocalParams {
456        name: String,
457        #[clap(long)]
458        federation_name: Option<String>,
459    },
460    AddPeer {
461        info: String,
462    },
463    StartDkg,
464}
465
466#[derive(Debug, Clone, Subcommand)]
467enum DecodeType {
468    /// Decode an invite code string into a JSON representation
469    InviteCode { invite_code: InviteCode },
470    /// Decode a string of ecash notes into a JSON representation
471    #[group(required = true, multiple = false)]
472    Notes {
473        /// Base64 e-cash notes to be decoded
474        notes: Option<OOBNotes>,
475        /// File containing base64 e-cash notes to be decoded
476        #[arg(long)]
477        file: Option<PathBuf>,
478    },
479    /// Decode a transaction hex string and print it to stdout
480    Transaction { hex_string: String },
481    /// Decode a setup code (as shared during a federation setup ceremony)
482    /// string into a JSON representation
483    SetupCode { setup_code: String },
484}
485
486#[derive(Debug, Clone, Deserialize, Serialize)]
487struct OOBNotesJson {
488    federation_id_prefix: String,
489    notes: TieredMulti<SpendableNote>,
490}
491
492#[derive(Debug, Clone, Subcommand)]
493enum EncodeType {
494    /// Encode connection info from its constituent parts
495    InviteCode {
496        #[clap(long)]
497        url: SafeUrl,
498        #[clap(long = "federation_id")]
499        federation_id: FederationId,
500        #[clap(long = "peer")]
501        peer: PeerId,
502        #[arg(env = FM_API_SECRET_ENV)]
503        api_secret: Option<String>,
504    },
505
506    /// Encode a JSON string of notes to an ecash string
507    Notes { notes_json: String },
508}
509
510#[derive(Debug, Clone, Subcommand)]
511enum DevCmd {
512    /// Send direct method call to the API. If you specify --peer-id, it will
513    /// just ask one server, otherwise it will try to get consensus from all
514    /// servers.
515    #[command(after_long_help = r#"
516Examples:
517
518  fedimint-cli dev api --peer-id 0 config '"fed114znk7uk7ppugdjuytr8venqf2tkywd65cqvg3u93um64tu5cw4yr0n3fvn7qmwvm4g48cpndgnm4gqq4waen5te0xyerwt3s9cczuvf6xyurzde597s7crdvsk2vmyarjw9gwyqjdzj"'
519    "#)]
520    Api {
521        /// JSON-RPC method to call
522        method: String,
523        /// JSON-RPC parameters for the request
524        ///
525        /// Note: single jsonrpc argument params string, which might require
526        /// double-quotes (see example above).
527        #[clap(default_value = "null")]
528        params: String,
529        /// Which server to send request to
530        #[clap(long = "peer-id")]
531        peer_id: Option<u16>,
532
533        /// Module selector (either module id or module kind)
534        #[clap(long = "module")]
535        module: Option<ModuleSelector>,
536
537        /// Guardian password in case authenticated API endpoints are being
538        /// called. Only use together with --peer-id.
539        #[clap(long, requires = "peer_id")]
540        password: Option<String>,
541    },
542
543    ApiAnnouncements,
544
545    /// Advance the note_idx
546    AdvanceNoteIdx {
547        #[clap(long, default_value = "1")]
548        count: usize,
549
550        #[clap(long)]
551        amount: Amount,
552    },
553
554    /// Wait for the fed to reach a consensus block count
555    WaitBlockCount {
556        count: u64,
557    },
558
559    /// Just start the `Client` and wait
560    Wait {
561        /// Limit the wait time
562        seconds: Option<f32>,
563    },
564
565    /// Wait for all state machines to complete
566    WaitComplete,
567
568    /// Decode invite code or ecash notes string into a JSON representation
569    Decode {
570        #[clap(subcommand)]
571        decode_type: DecodeType,
572    },
573
574    /// Encode an invite code or ecash notes into binary
575    Encode {
576        #[clap(subcommand)]
577        encode_type: EncodeType,
578    },
579
580    /// Gets the current fedimint AlephBFT block count
581    SessionCount,
582
583    ConfigDecrypt {
584        /// Encrypted config file
585        #[arg(long = "in-file")]
586        in_file: PathBuf,
587        /// Plaintext config file output
588        #[arg(long = "out-file")]
589        out_file: PathBuf,
590        /// Encryption salt file, otherwise defaults to the salt file from the
591        /// `in_file` directory
592        #[arg(long = "salt-file")]
593        salt_file: Option<PathBuf>,
594        /// The password that encrypts the configs
595        #[arg(env = FM_PASSWORD_ENV)]
596        password: String,
597    },
598
599    ConfigEncrypt {
600        /// Plaintext config file
601        #[arg(long = "in-file")]
602        in_file: PathBuf,
603        /// Encrypted config file output
604        #[arg(long = "out-file")]
605        out_file: PathBuf,
606        /// Encryption salt file, otherwise defaults to the salt file from the
607        /// `out_file` directory
608        #[arg(long = "salt-file")]
609        salt_file: Option<PathBuf>,
610        /// The password that encrypts the configs
611        #[arg(env = FM_PASSWORD_ENV)]
612        password: String,
613    },
614
615    /// Lists active and inactive state machine states of the operation
616    /// chronologically
617    ListOperationStates {
618        operation_id: OperationId,
619    },
620    /// Returns the federation's meta fields. If they are set correctly via the
621    /// meta module these are returned, otherwise the legacy mechanism
622    /// (config+override file) is used.
623    MetaFields,
624    /// Gets the tagged fedimintd version for a peer
625    PeerVersion {
626        #[clap(long)]
627        peer_id: u16,
628    },
629    /// Dump Client's Event Log
630    ShowEventLog {
631        #[arg(long)]
632        pos: Option<EventLogId>,
633        #[arg(long, default_value = "10")]
634        limit: u64,
635    },
636    /// Dump Client's Trimable Event Log
637    ShowEventLogTrimable {
638        #[arg(long)]
639        pos: Option<EventLogId>,
640        #[arg(long, default_value = "10")]
641        limit: u64,
642    },
643    /// Test the built-in event handling and tracking by printing events to
644    /// console
645    TestEventLogHandling,
646    /// Manually submit a fedimint transaction to guardians
647    ///
648    /// This can be useful to check why a transaction may have been rejected
649    /// when debugging client issues.
650    SubmitTransaction {
651        /// Hex-encoded fedimint transaction
652        transaction: String,
653    },
654}
655
656#[derive(Debug, Serialize, Deserialize)]
657#[serde(rename_all = "snake_case")]
658struct PayRequest {
659    notes: TieredMulti<SpendableNote>,
660    invoice: lightning_invoice::Bolt11Invoice,
661}
662
663pub struct FedimintCli {
664    module_inits: ClientModuleInitRegistry,
665    cli_args: Opts,
666}
667
668impl FedimintCli {
669    /// Build a new `fedimintd` with a custom version hash
670    pub fn new(version_hash: &str) -> anyhow::Result<FedimintCli> {
671        assert_eq!(
672            fedimint_build_code_version_env!().len(),
673            version_hash.len(),
674            "version_hash must have an expected length"
675        );
676
677        handle_version_hash_command(version_hash);
678
679        let cli_args = Opts::parse();
680        let base_level = if cli_args.verbose { "debug" } else { "info" };
681        TracingSetup::default()
682            .with_base_level(base_level)
683            .init()
684            .expect("tracing initializes");
685
686        let version = env!("CARGO_PKG_VERSION");
687        debug!(target: LOG_CLIENT, "Starting fedimint-cli (version: {version} version_hash: {version_hash})");
688
689        Ok(Self {
690            module_inits: ClientModuleInitRegistry::new(),
691            cli_args,
692        })
693    }
694
695    pub fn with_module<T>(mut self, r#gen: T) -> Self
696    where
697        T: ClientModuleInit + 'static + Send + Sync,
698    {
699        self.module_inits.attach(r#gen);
700        self
701    }
702
703    pub fn with_default_modules(self) -> Self {
704        self.with_module(LightningClientInit::default())
705            .with_module(MintClientInit)
706            .with_module(WalletClientInit::default())
707            .with_module(MetaClientInit)
708            .with_module(fedimint_lnv2_client::LightningClientInit::default())
709    }
710
711    pub async fn run(&mut self) {
712        match self.handle_command(self.cli_args.clone()).await {
713            Ok(output) => {
714                // ignore if there's anyone reading the stuff we're writing out
715                let _ = writeln!(std::io::stdout(), "{output}");
716            }
717            Err(err) => {
718                debug!(target: LOG_CLIENT, err = %err.error.as_str(), "Command failed");
719                let _ = writeln!(std::io::stdout(), "{err}");
720                exit(1);
721            }
722        }
723    }
724
725    async fn make_client_builder(&self, cli: &Opts) -> CliResult<(ClientBuilder, Database)> {
726        let mut client_builder = Client::builder()
727            .await
728            .map_err_cli()?
729            .with_iroh_enable_dht(cli.iroh_enable_dht())
730            .with_iroh_enable_next(cli.iroh_enable_next());
731        client_builder.with_module_inits(self.module_inits.clone());
732
733        let db = cli.load_database().await?;
734        Ok((client_builder, db))
735    }
736
737    async fn client_join(
738        &mut self,
739        cli: &Opts,
740        invite_code: InviteCode,
741    ) -> CliResult<ClientHandleArc> {
742        let (client_builder, db) = self.make_client_builder(cli).await?;
743
744        let mnemonic = load_or_generate_mnemonic(&db).await?;
745
746        let client = client_builder
747            .preview(cli.make_endpoints().await.map_err_cli()?, &invite_code)
748            .await
749            .map_err_cli()?
750            .join(
751                db,
752                RootSecret::StandardDoubleDerive(Bip39RootSecretStrategy::<12>::to_root_secret(
753                    &mnemonic,
754                )),
755            )
756            .await
757            .map(Arc::new)
758            .map_err_cli()?;
759
760        print_welcome_message(&client).await;
761        log_expiration_notice(&client).await;
762
763        Ok(client)
764    }
765
766    async fn client_open(&self, cli: &Opts) -> CliResult<ClientHandleArc> {
767        let (mut client_builder, db) = self.make_client_builder(cli).await?;
768
769        if let Some(our_id) = cli.our_id {
770            client_builder.set_admin_creds(AdminCreds {
771                peer_id: our_id,
772                auth: cli.auth()?,
773            });
774        }
775
776        let mnemonic = Mnemonic::from_entropy(
777            &Client::load_decodable_client_secret::<Vec<u8>>(&db)
778                .await
779                .map_err_cli()?,
780        )
781        .map_err_cli()?;
782
783        let client = client_builder
784            .open(
785                cli.make_endpoints().await.map_err_cli()?,
786                db,
787                RootSecret::StandardDoubleDerive(Bip39RootSecretStrategy::<12>::to_root_secret(
788                    &mnemonic,
789                )),
790            )
791            .await
792            .map(Arc::new)
793            .map_err_cli()?;
794
795        log_expiration_notice(&client).await;
796
797        Ok(client)
798    }
799
800    async fn client_recover(
801        &mut self,
802        cli: &Opts,
803        mnemonic: Mnemonic,
804        invite_code: InviteCode,
805    ) -> CliResult<ClientHandleArc> {
806        let (builder, db) = self.make_client_builder(cli).await?;
807        match Client::load_decodable_client_secret_opt::<Vec<u8>>(&db)
808            .await
809            .map_err_cli()?
810        {
811            Some(existing) => {
812                if existing != mnemonic.to_entropy() {
813                    Err(anyhow::anyhow!("Previously set mnemonic does not match")).map_err_cli()?;
814                }
815            }
816            None => {
817                Client::store_encodable_client_secret(&db, mnemonic.to_entropy())
818                    .await
819                    .map_err_cli()?;
820            }
821        }
822
823        let root_secret = RootSecret::StandardDoubleDerive(
824            Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic),
825        );
826
827        let preview = builder
828            .preview(cli.make_endpoints().await.map_err_cli()?, &invite_code)
829            .await
830            .map_err_cli()?;
831
832        let backup = preview
833            .download_backup_from_federation(root_secret.clone())
834            .await
835            .map_err_cli()?;
836
837        let client = preview
838            .recover(db, root_secret, backup)
839            .await
840            .map(Arc::new)
841            .map_err_cli()?;
842
843        print_welcome_message(&client).await;
844        log_expiration_notice(&client).await;
845
846        Ok(client)
847    }
848
849    async fn handle_command(&mut self, cli: Opts) -> CliOutputResult {
850        match cli.command.clone() {
851            Command::InviteCode { peer } => {
852                let client = self.client_open(&cli).await?;
853
854                let invite_code = client
855                    .invite_code(peer)
856                    .await
857                    .ok_or_cli_msg("peer not found")?;
858
859                Ok(CliOutput::InviteCode { invite_code })
860            }
861            Command::Join { invite_code } => {
862                {
863                    let invite_code: InviteCode = InviteCode::from_str(&invite_code)
864                        .map_err_cli_msg("invalid invite code")?;
865
866                    // Build client and store config in DB
867                    let _client = self.client_join(&cli, invite_code).await?;
868                }
869
870                Ok(CliOutput::Join {
871                    joined: invite_code,
872                })
873            }
874            Command::VersionHash => Ok(CliOutput::VersionHash {
875                hash: fedimint_build_code_version_env!().to_string(),
876            }),
877            Command::Client(ClientCmd::Restore {
878                mnemonic,
879                invite_code,
880            }) => {
881                let invite_code: InviteCode =
882                    InviteCode::from_str(&invite_code).map_err_cli_msg("invalid invite code")?;
883                let mnemonic = Mnemonic::from_str(&mnemonic).map_err_cli()?;
884                let client = self.client_recover(&cli, mnemonic, invite_code).await?;
885
886                // TODO: until we implement recovery for other modules we can't really wait
887                // for more than this one
888                debug!(target: LOG_CLIENT, "Waiting for mint module recovery to finish");
889                client.wait_for_all_recoveries().await.map_err_cli()?;
890
891                debug!(target: LOG_CLIENT, "Recovery complete");
892
893                Ok(CliOutput::Raw(serde_json::to_value(()).unwrap()))
894            }
895            Command::Client(command) => {
896                let client = self.client_open(&cli).await?;
897                Ok(CliOutput::Raw(
898                    client::handle_command(command, client)
899                        .await
900                        .map_err_cli()?,
901                ))
902            }
903            Command::Admin(AdminCmd::Audit) => {
904                let client = self.client_open(&cli).await?;
905
906                let audit = cli
907                    .admin_client(
908                        &client.get_peer_urls().await,
909                        client.api_secret().as_deref(),
910                    )
911                    .await?
912                    .audit(cli.auth()?)
913                    .await?;
914                Ok(CliOutput::Raw(
915                    serde_json::to_value(audit).map_err_cli_msg("invalid response")?,
916                ))
917            }
918            Command::Admin(AdminCmd::Status) => {
919                let client = self.client_open(&cli).await?;
920
921                let status = cli
922                    .admin_client(
923                        &client.get_peer_urls().await,
924                        client.api_secret().as_deref(),
925                    )
926                    .await?
927                    .status()
928                    .await?;
929                Ok(CliOutput::Raw(
930                    serde_json::to_value(status).map_err_cli_msg("invalid response")?,
931                ))
932            }
933            Command::Admin(AdminCmd::GuardianConfigBackup) => {
934                let client = self.client_open(&cli).await?;
935
936                let guardian_config_backup = cli
937                    .admin_client(
938                        &client.get_peer_urls().await,
939                        client.api_secret().as_deref(),
940                    )
941                    .await?
942                    .guardian_config_backup(cli.auth()?)
943                    .await?;
944                Ok(CliOutput::Raw(
945                    serde_json::to_value(guardian_config_backup)
946                        .map_err_cli_msg("invalid response")?,
947                ))
948            }
949            Command::Admin(AdminCmd::Setup(dkg_args)) => self
950                .handle_admin_setup_command(cli, dkg_args)
951                .await
952                .map(CliOutput::Raw)
953                .map_err_cli_msg("Config Gen Error"),
954            Command::Admin(AdminCmd::SignApiAnnouncement {
955                api_url,
956                override_url,
957            }) => {
958                let client = self.client_open(&cli).await?;
959
960                if !["ws", "wss"].contains(&api_url.scheme()) {
961                    return Err(CliError {
962                        error: format!(
963                            "Unsupported URL scheme {}, use ws:// or wss://",
964                            api_url.scheme()
965                        ),
966                    });
967                }
968
969                let announcement = cli
970                    .admin_client(
971                        &override_url
972                            .and_then(|url| Some(vec![(cli.our_id?, url)].into_iter().collect()))
973                            .unwrap_or(client.get_peer_urls().await),
974                        client.api_secret().as_deref(),
975                    )
976                    .await?
977                    .sign_api_announcement(api_url, cli.auth()?)
978                    .await?;
979
980                Ok(CliOutput::Raw(
981                    serde_json::to_value(announcement).map_err_cli_msg("invalid response")?,
982                ))
983            }
984            Command::Admin(AdminCmd::Shutdown { session_idx }) => {
985                let client = self.client_open(&cli).await?;
986
987                cli.admin_client(
988                    &client.get_peer_urls().await,
989                    client.api_secret().as_deref(),
990                )
991                .await?
992                .shutdown(Some(session_idx), cli.auth()?)
993                .await?;
994
995                Ok(CliOutput::Raw(json!(null)))
996            }
997            Command::Admin(AdminCmd::BackupStatistics) => {
998                let client = self.client_open(&cli).await?;
999
1000                let backup_statistics = cli
1001                    .admin_client(
1002                        &client.get_peer_urls().await,
1003                        client.api_secret().as_deref(),
1004                    )
1005                    .await?
1006                    .backup_statistics(cli.auth()?)
1007                    .await?;
1008
1009                Ok(CliOutput::Raw(
1010                    serde_json::to_value(backup_statistics).expect("Can be encoded"),
1011                ))
1012            }
1013            Command::Admin(AdminCmd::ChangePassword { new_password }) => {
1014                let client = self.client_open(&cli).await?;
1015
1016                cli.admin_client(
1017                    &client.get_peer_urls().await,
1018                    client.api_secret().as_deref(),
1019                )
1020                .await?
1021                .change_password(cli.auth()?, &new_password)
1022                .await?;
1023
1024                warn!(target: LOG_CLIENT, "Password changed, please restart fedimintd manually");
1025
1026                Ok(CliOutput::Raw(json!(null)))
1027            }
1028            Command::Dev(DevCmd::Api {
1029                method,
1030                params,
1031                peer_id,
1032                password: auth,
1033                module,
1034            }) => {
1035                //Parse params to JSON.
1036                //If fails, convert to JSON string.
1037                let params = serde_json::from_str::<Value>(&params).unwrap_or_else(|err| {
1038                    debug!(
1039                        target: LOG_CLIENT,
1040                        "Failed to serialize params:{}. Converting it to JSON string",
1041                        err
1042                    );
1043
1044                    serde_json::Value::String(params)
1045                });
1046
1047                let mut params = ApiRequestErased::new(params);
1048                if let Some(auth) = auth {
1049                    params = params.with_auth(ApiAuth(auth));
1050                }
1051                let client = self.client_open(&cli).await?;
1052
1053                let api = client.api_clone();
1054
1055                let module_api = match module {
1056                    Some(selector) => {
1057                        Some(api.with_module(selector.resolve(&client).map_err_cli()?))
1058                    }
1059                    None => None,
1060                };
1061
1062                let response: Value = match (peer_id, module_api) {
1063                    (Some(peer_id), Some(module_api)) => module_api
1064                        .request_raw(peer_id.into(), &method, &params)
1065                        .await
1066                        .map_err_cli()?,
1067                    (Some(peer_id), None) => api
1068                        .request_raw(peer_id.into(), &method, &params)
1069                        .await
1070                        .map_err_cli()?,
1071                    (None, Some(module_api)) => module_api
1072                        .request_current_consensus(method, params)
1073                        .await
1074                        .map_err_cli()?,
1075                    (None, None) => api
1076                        .request_current_consensus(method, params)
1077                        .await
1078                        .map_err_cli()?,
1079                };
1080
1081                Ok(CliOutput::UntypedApiOutput { value: response })
1082            }
1083            Command::Dev(DevCmd::AdvanceNoteIdx { count, amount }) => {
1084                let client = self.client_open(&cli).await?;
1085
1086                let mint = client
1087                    .get_first_module::<MintClientModule>()
1088                    .map_err_cli_msg("can't get mint module")?;
1089
1090                for _ in 0..count {
1091                    mint.advance_note_idx(amount)
1092                        .await
1093                        .map_err_cli_msg("failed to advance the note_idx")?;
1094                }
1095
1096                Ok(CliOutput::Raw(serde_json::Value::Null))
1097            }
1098            Command::Dev(DevCmd::ApiAnnouncements) => {
1099                let client = self.client_open(&cli).await?;
1100                let announcements = client.get_peer_url_announcements().await;
1101                Ok(CliOutput::Raw(
1102                    serde_json::to_value(announcements).expect("Can be encoded"),
1103                ))
1104            }
1105            Command::Dev(DevCmd::WaitBlockCount { count: target }) => retry(
1106                "wait_block_count",
1107                backoff_util::custom_backoff(
1108                    Duration::from_millis(100),
1109                    Duration::from_secs(5),
1110                    None,
1111                ),
1112                || async {
1113                    let client = self.client_open(&cli).await?;
1114                    let wallet = client.get_first_module::<WalletClientModule>()?;
1115                    let count = client
1116                        .api()
1117                        .with_module(wallet.id)
1118                        .fetch_consensus_block_count()
1119                        .await?;
1120                    if count >= target {
1121                        Ok(CliOutput::WaitBlockCount { reached: count })
1122                    } else {
1123                        info!(target: LOG_CLIENT, current=count, target, "Block count not reached");
1124                        Err(format_err!("target not reached"))
1125                    }
1126                },
1127            )
1128            .await
1129            .map_err_cli(),
1130
1131            Command::Dev(DevCmd::WaitComplete) => {
1132                let client = self.client_open(&cli).await?;
1133                client
1134                    .wait_for_all_active_state_machines()
1135                    .await
1136                    .map_err_cli_msg("failed to wait for all active state machines")?;
1137                Ok(CliOutput::Raw(serde_json::Value::Null))
1138            }
1139            Command::Dev(DevCmd::Wait { seconds }) => {
1140                let client = self.client_open(&cli).await?;
1141                // Since most callers are `wait`ing for something to happen,
1142                // let's trigger a network call, so any background threads
1143                // waiting for it starts doing their job.
1144                client
1145                    .task_group()
1146                    .spawn_cancellable("fedimint-cli dev wait: init networking", {
1147                        let client = client.clone();
1148                        async move {
1149                            let _ = client.api().session_count().await;
1150                        }
1151                    });
1152
1153                if let Some(secs) = seconds {
1154                    runtime::sleep(Duration::from_secs_f32(secs)).await;
1155                } else {
1156                    pending::<()>().await;
1157                }
1158                Ok(CliOutput::Raw(serde_json::Value::Null))
1159            }
1160            Command::Dev(DevCmd::Decode { decode_type }) => match decode_type {
1161                DecodeType::InviteCode { invite_code } => Ok(CliOutput::DecodeInviteCode {
1162                    url: invite_code.url(),
1163                    federation_id: invite_code.federation_id(),
1164                }),
1165                DecodeType::Notes { notes, file } => {
1166                    let notes = if let Some(notes) = notes {
1167                        notes
1168                    } else if let Some(file) = file {
1169                        let notes_str =
1170                            fs::read_to_string(file).map_err_cli_msg("failed to read file")?;
1171                        OOBNotes::from_str(&notes_str).map_err_cli_msg("failed to decode notes")?
1172                    } else {
1173                        unreachable!("Clap enforces either notes or file being set");
1174                    };
1175
1176                    let notes_json = notes
1177                        .notes_json()
1178                        .map_err_cli_msg("failed to decode notes")?;
1179                    Ok(CliOutput::Raw(notes_json))
1180                }
1181                DecodeType::Transaction { hex_string } => {
1182                    let bytes: Vec<u8> = hex::FromHex::from_hex(&hex_string)
1183                        .map_err_cli_msg("failed to decode transaction")?;
1184
1185                    let client = self.client_open(&cli).await?;
1186                    let tx = fedimint_core::transaction::Transaction::from_bytes(
1187                        &bytes,
1188                        client.decoders(),
1189                    )
1190                    .map_err_cli_msg("failed to decode transaction")?;
1191
1192                    Ok(CliOutput::DecodeTransaction {
1193                        transaction: (format!("{tx:?}")),
1194                    })
1195                }
1196                DecodeType::SetupCode { setup_code } => {
1197                    let setup_code = base32::decode_prefixed(FEDIMINT_PREFIX, &setup_code)
1198                        .map_err_cli_msg("failed to decode setup code")?;
1199
1200                    Ok(CliOutput::SetupCode { setup_code })
1201                }
1202            },
1203            Command::Dev(DevCmd::Encode { encode_type }) => match encode_type {
1204                EncodeType::InviteCode {
1205                    url,
1206                    federation_id,
1207                    peer,
1208                    api_secret,
1209                } => Ok(CliOutput::InviteCode {
1210                    invite_code: InviteCode::new(url, peer, federation_id, api_secret),
1211                }),
1212                EncodeType::Notes { notes_json } => {
1213                    let notes = serde_json::from_str::<OOBNotesJson>(&notes_json)
1214                        .map_err_cli_msg("invalid JSON for notes")?;
1215                    let prefix =
1216                        FederationIdPrefix::from_str(&notes.federation_id_prefix).map_err_cli()?;
1217                    let notes = OOBNotes::new(prefix, notes.notes);
1218                    Ok(CliOutput::Raw(notes.to_string().into()))
1219                }
1220            },
1221            Command::Dev(DevCmd::SessionCount) => {
1222                let client = self.client_open(&cli).await?;
1223                let count = client.api().session_count().await?;
1224                Ok(CliOutput::EpochCount { count })
1225            }
1226            Command::Dev(DevCmd::ConfigDecrypt {
1227                in_file,
1228                out_file,
1229                salt_file,
1230                password,
1231            }) => {
1232                let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&in_file));
1233                let salt = fs::read_to_string(salt_file).map_err_cli()?;
1234                let key = get_encryption_key(&password, &salt).map_err_cli()?;
1235                let decrypted_bytes = encrypted_read(&key, in_file).map_err_cli()?;
1236
1237                let mut out_file_handle = fs::File::options()
1238                    .create_new(true)
1239                    .write(true)
1240                    .open(out_file)
1241                    .expect("Could not create output cfg file");
1242                out_file_handle.write_all(&decrypted_bytes).map_err_cli()?;
1243                Ok(CliOutput::ConfigDecrypt)
1244            }
1245            Command::Dev(DevCmd::ConfigEncrypt {
1246                in_file,
1247                out_file,
1248                salt_file,
1249                password,
1250            }) => {
1251                let mut in_file_handle =
1252                    fs::File::open(in_file).expect("Could not create output cfg file");
1253                let mut plaintext_bytes = vec![];
1254                in_file_handle.read_to_end(&mut plaintext_bytes).unwrap();
1255
1256                let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&out_file));
1257                let salt = fs::read_to_string(salt_file).map_err_cli()?;
1258                let key = get_encryption_key(&password, &salt).map_err_cli()?;
1259                encrypted_write(plaintext_bytes, &key, out_file).map_err_cli()?;
1260                Ok(CliOutput::ConfigEncrypt)
1261            }
1262            Command::Dev(DevCmd::ListOperationStates { operation_id }) => {
1263                #[derive(Serialize)]
1264                struct ReactorLogState {
1265                    active: bool,
1266                    module_instance: ModuleInstanceId,
1267                    creation_time: String,
1268                    #[serde(skip_serializing_if = "Option::is_none")]
1269                    end_time: Option<String>,
1270                    state: String,
1271                }
1272
1273                let client = self.client_open(&cli).await?;
1274
1275                let (active_states, inactive_states) =
1276                    client.executor().get_operation_states(operation_id).await;
1277                let all_states =
1278                    active_states
1279                        .into_iter()
1280                        .map(|(active_state, active_meta)| ReactorLogState {
1281                            active: true,
1282                            module_instance: active_state.module_instance_id(),
1283                            creation_time: crate::client::time_to_iso8601(&active_meta.created_at),
1284                            end_time: None,
1285                            state: format!("{active_state:?}",),
1286                        })
1287                        .chain(inactive_states.into_iter().map(
1288                            |(inactive_state, inactive_meta)| ReactorLogState {
1289                                active: false,
1290                                module_instance: inactive_state.module_instance_id(),
1291                                creation_time: crate::client::time_to_iso8601(
1292                                    &inactive_meta.created_at,
1293                                ),
1294                                end_time: Some(crate::client::time_to_iso8601(
1295                                    &inactive_meta.exited_at,
1296                                )),
1297                                state: format!("{inactive_state:?}",),
1298                            },
1299                        ))
1300                        .sorted_by(|a, b| a.creation_time.cmp(&b.creation_time))
1301                        .collect::<Vec<_>>();
1302
1303                Ok(CliOutput::Raw(json!({
1304                    "states": all_states
1305                })))
1306            }
1307            Command::Dev(DevCmd::MetaFields) => {
1308                let client = self.client_open(&cli).await?;
1309                let source = MetaModuleMetaSourceWithFallback::<LegacyMetaSource>::default();
1310
1311                let meta_fields = source
1312                    .fetch(
1313                        &client.config().await,
1314                        &client.api_clone(),
1315                        FetchKind::Initial,
1316                        None,
1317                    )
1318                    .await
1319                    .map_err_cli()?;
1320
1321                Ok(CliOutput::Raw(
1322                    serde_json::to_value(meta_fields).expect("Can be encoded"),
1323                ))
1324            }
1325            Command::Dev(DevCmd::PeerVersion { peer_id }) => {
1326                let client = self.client_open(&cli).await?;
1327                let version = client
1328                    .api()
1329                    .fedimintd_version(peer_id.into())
1330                    .await
1331                    .map_err_cli()?;
1332
1333                Ok(CliOutput::Raw(json!({ "version": version })))
1334            }
1335            Command::Dev(DevCmd::ShowEventLog { pos, limit }) => {
1336                let client = self.client_open(&cli).await?;
1337
1338                let events: Vec<_> = client
1339                    .get_event_log(pos, limit)
1340                    .await
1341                    .into_iter()
1342                    .map(|v| {
1343                        let id = v.id();
1344                        let v = v.as_raw();
1345                        let module_id = v.module.as_ref().map(|m| m.1);
1346                        let module_kind = v.module.as_ref().map(|m| m.0.clone());
1347                        serde_json::json!({
1348                            "id": id,
1349                            "kind": v.kind,
1350                            "module_kind": module_kind,
1351                            "module_id": module_id,
1352                            "ts": v.ts_usecs,
1353                            "payload": serde_json::from_slice(&v.payload).unwrap_or_else(|_| hex::encode(&v.payload)),
1354                        })
1355                    })
1356                    .collect();
1357
1358                Ok(CliOutput::Raw(
1359                    serde_json::to_value(events).expect("Can be encoded"),
1360                ))
1361            }
1362            Command::Dev(DevCmd::ShowEventLogTrimable { pos, limit }) => {
1363                let client = self.client_open(&cli).await?;
1364
1365                let events: Vec<_> = client
1366                    .get_event_log_trimable(
1367                        pos.map(|id| EventLogTrimableId::from(u64::from(id))),
1368                        limit,
1369                    )
1370                    .await
1371                    .into_iter()
1372                    .map(|v| {
1373                        let id = v.id();
1374                        let v = v.as_raw();
1375                        let module_id = v.module.as_ref().map(|m| m.1);
1376                        let module_kind = v.module.as_ref().map(|m| m.0.clone());
1377                        serde_json::json!({
1378                            "id": id,
1379                            "kind": v.kind,
1380                            "module_kind": module_kind,
1381                            "module_id": module_id,
1382                            "ts": v.ts_usecs,
1383                            "payload": serde_json::from_slice(&v.payload).unwrap_or_else(|_| hex::encode(&v.payload)),
1384                        })
1385                    })
1386                    .collect();
1387
1388                Ok(CliOutput::Raw(
1389                    serde_json::to_value(events).expect("Can be encoded"),
1390                ))
1391            }
1392            Command::Dev(DevCmd::SubmitTransaction { transaction }) => {
1393                let client = self.client_open(&cli).await?;
1394                let tx = Transaction::consensus_decode_hex(&transaction, client.decoders())
1395                    .map_err_cli()?;
1396                let tx_outcome = client
1397                    .api()
1398                    .submit_transaction(tx)
1399                    .await
1400                    .try_into_inner(client.decoders())
1401                    .map_err_cli()?;
1402
1403                Ok(CliOutput::Raw(
1404                    serde_json::to_value(tx_outcome.0.map_err_cli()?).expect("Can be encoded"),
1405                ))
1406            }
1407            Command::Dev(DevCmd::TestEventLogHandling) => {
1408                let client = self.client_open(&cli).await?;
1409
1410                client
1411                    .handle_events(
1412                        client.built_in_application_event_log_tracker(),
1413                        move |_dbtx, event| {
1414                            Box::pin(async move {
1415                                info!(target: LOG_CLIENT, "{event:?}");
1416
1417                                Ok(())
1418                            })
1419                        },
1420                    )
1421                    .await
1422                    .map_err_cli()?;
1423                unreachable!(
1424                    "handle_events exits only if client shuts down, which we don't do here"
1425                )
1426            }
1427            Command::Completion { shell } => {
1428                let bin_path = PathBuf::from(
1429                    std::env::args_os()
1430                        .next()
1431                        .expect("Binary name is always provided if we get this far"),
1432                );
1433                let bin_name = bin_path
1434                    .file_name()
1435                    .expect("path has file name")
1436                    .to_string_lossy();
1437                clap_complete::generate(
1438                    shell,
1439                    &mut Opts::command(),
1440                    bin_name.as_ref(),
1441                    &mut std::io::stdout(),
1442                );
1443                // HACK: prints true to stdout which is fine for shells
1444                Ok(CliOutput::Raw(serde_json::Value::Bool(true)))
1445            }
1446        }
1447    }
1448
1449    async fn handle_admin_setup_command(
1450        &self,
1451        cli: Opts,
1452        args: SetupAdminArgs,
1453    ) -> anyhow::Result<Value> {
1454        let client =
1455            DynGlobalApi::new_admin_setup(cli.make_endpoints().await?, args.endpoint.clone())?;
1456
1457        match &args.subcommand {
1458            SetupAdminCmd::Status => {
1459                let status = client.setup_status(cli.auth()?).await?;
1460
1461                Ok(serde_json::to_value(status).expect("JSON serialization failed"))
1462            }
1463            SetupAdminCmd::SetLocalParams {
1464                name,
1465                federation_name,
1466            } => {
1467                let info = client
1468                    .set_local_params(
1469                        name.clone(),
1470                        federation_name.clone(),
1471                        None,
1472                        None,
1473                        cli.auth()?,
1474                    )
1475                    .await?;
1476
1477                Ok(serde_json::to_value(info).expect("JSON serialization failed"))
1478            }
1479            SetupAdminCmd::AddPeer { info } => {
1480                let name = client
1481                    .add_peer_connection_info(info.clone(), cli.auth()?)
1482                    .await?;
1483
1484                Ok(serde_json::to_value(name).expect("JSON serialization failed"))
1485            }
1486            SetupAdminCmd::StartDkg => {
1487                client.start_dkg(cli.auth()?).await?;
1488
1489                Ok(Value::Null)
1490            }
1491        }
1492    }
1493}
1494
1495async fn log_expiration_notice(client: &Client) {
1496    client.get_meta_expiration_timestamp().await;
1497    if let Some(expiration_time) = client.get_meta_expiration_timestamp().await {
1498        match expiration_time.duration_since(fedimint_core::time::now()) {
1499            Ok(until_expiration) => {
1500                let days = until_expiration.as_secs() / (60 * 60 * 24);
1501
1502                if 90 < days {
1503                    debug!(target: LOG_CLIENT, %days, "This federation will expire");
1504                } else if 30 < days {
1505                    info!(target: LOG_CLIENT, %days, "This federation will expire");
1506                } else {
1507                    warn!(target: LOG_CLIENT, %days, "This federation will expire soon");
1508                }
1509            }
1510            Err(_) => {
1511                tracing::error!(target: LOG_CLIENT, "This federation has expired and might not be safe to use");
1512            }
1513        }
1514    }
1515}
1516async fn print_welcome_message(client: &Client) {
1517    if let Some(welcome_message) = client
1518        .meta_service()
1519        .get_field::<String>(client.db(), "welcome_message")
1520        .await
1521        .and_then(|v| v.value)
1522    {
1523        eprintln!("{welcome_message}");
1524    }
1525}
1526
1527fn salt_from_file_path(file_path: &Path) -> PathBuf {
1528    file_path
1529        .parent()
1530        .expect("File has no parent?!")
1531        .join(SALT_FILE)
1532}
1533
1534/// Convert clap arguments to backup metadata
1535fn metadata_from_clap_cli(metadata: Vec<String>) -> Result<BTreeMap<String, String>, CliError> {
1536    let metadata: BTreeMap<String, String> = metadata
1537        .into_iter()
1538        .map(|item| {
1539            match &item
1540                .splitn(2, '=')
1541                .map(ToString::to_string)
1542                .collect::<Vec<String>>()[..]
1543            {
1544                [] => Err(format_err!("Empty metadata argument not allowed")),
1545                [key] => Err(format_err!("Metadata {key} is missing a value")),
1546                [key, val] => Ok((key.clone(), val.clone())),
1547                [..] => unreachable!(),
1548            }
1549        })
1550        .collect::<anyhow::Result<_>>()
1551        .map_err_cli_msg("invalid metadata")?;
1552    Ok(metadata)
1553}
1554
1555#[test]
1556fn metadata_from_clap_cli_test() {
1557    for (args, expected) in [
1558        (
1559            vec!["a=b".to_string()],
1560            BTreeMap::from([("a".into(), "b".into())]),
1561        ),
1562        (
1563            vec!["a=b".to_string(), "c=d".to_string()],
1564            BTreeMap::from([("a".into(), "b".into()), ("c".into(), "d".into())]),
1565        ),
1566    ] {
1567        assert_eq!(metadata_from_clap_cli(args).unwrap(), expected);
1568    }
1569}