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