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    JoinFederation {
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    JoinFederation {
397        invite_code: String,
398    },
399
400    Completion {
401        shell: clap_complete::Shell,
402    },
403}
404
405#[allow(clippy::large_enum_variant)]
406#[derive(Debug, Clone, Subcommand)]
407enum AdminCmd {
408    /// Show the status according to the `status` endpoint
409    Status,
410
411    /// Show an audit across all modules
412    Audit,
413
414    /// Download guardian config to back it up
415    GuardianConfigBackup,
416
417    Setup(SetupAdminArgs),
418    /// Sign and announce a new API endpoint. The previous one will be
419    /// invalidated
420    SignApiAnnouncement {
421        /// New API URL to announce
422        api_url: SafeUrl,
423        /// Provide the API url for the guardian directly in case the old one
424        /// isn't reachable anymore
425        #[clap(long)]
426        override_url: Option<SafeUrl>,
427    },
428    /// Stop fedimintd after the specified session to do a coordinated upgrade
429    Shutdown {
430        /// Session index to stop after
431        session_idx: u64,
432    },
433    /// Show statistics about client backups stored by the federation
434    BackupStatistics,
435    /// Change guardian password, will shut down fedimintd and require manual
436    /// restart
437    ChangePassword {
438        /// New password to set
439        new_password: String,
440    },
441}
442
443#[derive(Debug, Clone, Args)]
444struct SetupAdminArgs {
445    endpoint: SafeUrl,
446
447    #[clap(subcommand)]
448    subcommand: SetupAdminCmd,
449}
450
451#[derive(Debug, Clone, Subcommand)]
452enum SetupAdminCmd {
453    Status,
454    SetLocalParams {
455        name: String,
456        #[clap(long)]
457        federation_name: Option<String>,
458    },
459    AddPeer {
460        info: String,
461    },
462    StartDkg,
463}
464
465#[derive(Debug, Clone, Subcommand)]
466enum DecodeType {
467    /// Decode an invite code string into a JSON representation
468    InviteCode { invite_code: InviteCode },
469    /// Decode a string of ecash notes into a JSON representation
470    #[group(required = true, multiple = false)]
471    Notes {
472        /// Base64 e-cash notes to be decoded
473        notes: Option<OOBNotes>,
474        /// File containing base64 e-cash notes to be decoded
475        #[arg(long)]
476        file: Option<PathBuf>,
477    },
478    /// Decode a transaction hex string and print it to stdout
479    Transaction { hex_string: String },
480    /// Decode a setup code (as shared during a federation setup ceremony)
481    /// string into a JSON representation
482    SetupCode { setup_code: String },
483}
484
485#[derive(Debug, Clone, Deserialize, Serialize)]
486struct OOBNotesJson {
487    federation_id_prefix: String,
488    notes: TieredMulti<SpendableNote>,
489}
490
491#[derive(Debug, Clone, Subcommand)]
492enum EncodeType {
493    /// Encode connection info from its constituent parts
494    InviteCode {
495        #[clap(long)]
496        url: SafeUrl,
497        #[clap(long = "federation_id")]
498        federation_id: FederationId,
499        #[clap(long = "peer")]
500        peer: PeerId,
501        #[arg(env = FM_API_SECRET_ENV)]
502        api_secret: Option<String>,
503    },
504
505    /// Encode a JSON string of notes to an ecash string
506    Notes { notes_json: String },
507}
508
509#[derive(Debug, Clone, Subcommand)]
510enum DevCmd {
511    /// Send direct method call to the API. If you specify --peer-id, it will
512    /// just ask one server, otherwise it will try to get consensus from all
513    /// servers.
514    #[command(after_long_help = r#"
515Examples:
516
517  fedimint-cli dev api --peer-id 0 config '"fed114znk7uk7ppugdjuytr8venqf2tkywd65cqvg3u93um64tu5cw4yr0n3fvn7qmwvm4g48cpndgnm4gqq4waen5te0xyerwt3s9cczuvf6xyurzde597s7crdvsk2vmyarjw9gwyqjdzj"'
518    "#)]
519    Api {
520        /// JSON-RPC method to call
521        method: String,
522        /// JSON-RPC parameters for the request
523        ///
524        /// Note: single jsonrpc argument params string, which might require
525        /// double-quotes (see example above).
526        #[clap(default_value = "null")]
527        params: String,
528        /// Which server to send request to
529        #[clap(long = "peer-id")]
530        peer_id: Option<u16>,
531
532        /// Module selector (either module id or module kind)
533        #[clap(long = "module")]
534        module: Option<ModuleSelector>,
535
536        /// Guardian password in case authenticated API endpoints are being
537        /// called. Only use together with --peer-id.
538        #[clap(long, requires = "peer_id")]
539        password: Option<String>,
540    },
541
542    ApiAnnouncements,
543
544    /// Advance the note_idx
545    AdvanceNoteIdx {
546        #[clap(long, default_value = "1")]
547        count: usize,
548
549        #[clap(long)]
550        amount: Amount,
551    },
552
553    /// Wait for the fed to reach a consensus block count
554    WaitBlockCount {
555        count: u64,
556    },
557
558    /// Just start the `Client` and wait
559    Wait {
560        /// Limit the wait time
561        seconds: Option<f32>,
562    },
563
564    /// Wait for all state machines to complete
565    WaitComplete,
566
567    /// Decode invite code or ecash notes string into a JSON representation
568    Decode {
569        #[clap(subcommand)]
570        decode_type: DecodeType,
571    },
572
573    /// Encode an invite code or ecash notes into binary
574    Encode {
575        #[clap(subcommand)]
576        encode_type: EncodeType,
577    },
578
579    /// Gets the current fedimint AlephBFT block count
580    SessionCount,
581
582    ConfigDecrypt {
583        /// Encrypted config file
584        #[arg(long = "in-file")]
585        in_file: PathBuf,
586        /// Plaintext config file output
587        #[arg(long = "out-file")]
588        out_file: PathBuf,
589        /// Encryption salt file, otherwise defaults to the salt file from the
590        /// `in_file` directory
591        #[arg(long = "salt-file")]
592        salt_file: Option<PathBuf>,
593        /// The password that encrypts the configs
594        #[arg(env = FM_PASSWORD_ENV)]
595        password: String,
596    },
597
598    ConfigEncrypt {
599        /// Plaintext config file
600        #[arg(long = "in-file")]
601        in_file: PathBuf,
602        /// Encrypted config file output
603        #[arg(long = "out-file")]
604        out_file: PathBuf,
605        /// Encryption salt file, otherwise defaults to the salt file from the
606        /// `out_file` directory
607        #[arg(long = "salt-file")]
608        salt_file: Option<PathBuf>,
609        /// The password that encrypts the configs
610        #[arg(env = FM_PASSWORD_ENV)]
611        password: String,
612    },
613
614    /// Lists active and inactive state machine states of the operation
615    /// chronologically
616    ListOperationStates {
617        operation_id: OperationId,
618    },
619    /// Returns the federation's meta fields. If they are set correctly via the
620    /// meta module these are returned, otherwise the legacy mechanism
621    /// (config+override file) is used.
622    MetaFields,
623    /// Gets the tagged fedimintd version for a peer
624    PeerVersion {
625        #[clap(long)]
626        peer_id: u16,
627    },
628    /// Dump Client's Event Log
629    ShowEventLog {
630        #[arg(long)]
631        pos: Option<EventLogId>,
632        #[arg(long, default_value = "10")]
633        limit: u64,
634    },
635    /// Dump Client's Trimable Event Log
636    ShowEventLogTrimable {
637        #[arg(long)]
638        pos: Option<EventLogId>,
639        #[arg(long, default_value = "10")]
640        limit: u64,
641    },
642    /// Test the built-in event handling and tracking by printing events to
643    /// console
644    TestEventLogHandling,
645    /// Manually submit a fedimint transaction to guardians
646    ///
647    /// This can be useful to check why a transaction may have been rejected
648    /// when debugging client issues.
649    SubmitTransaction {
650        /// Hex-encoded fedimint transaction
651        transaction: String,
652    },
653}
654
655#[derive(Debug, Serialize, Deserialize)]
656#[serde(rename_all = "snake_case")]
657struct PayRequest {
658    notes: TieredMulti<SpendableNote>,
659    invoice: lightning_invoice::Bolt11Invoice,
660}
661
662pub struct FedimintCli {
663    module_inits: ClientModuleInitRegistry,
664    cli_args: Opts,
665}
666
667impl FedimintCli {
668    /// Build a new `fedimintd` with a custom version hash
669    pub fn new(version_hash: &str) -> anyhow::Result<FedimintCli> {
670        assert_eq!(
671            fedimint_build_code_version_env!().len(),
672            version_hash.len(),
673            "version_hash must have an expected length"
674        );
675
676        handle_version_hash_command(version_hash);
677
678        let cli_args = Opts::parse();
679        let base_level = if cli_args.verbose { "debug" } else { "info" };
680        TracingSetup::default()
681            .with_base_level(base_level)
682            .init()
683            .expect("tracing initializes");
684
685        let version = env!("CARGO_PKG_VERSION");
686        debug!(target: LOG_CLIENT, "Starting fedimint-cli (version: {version} version_hash: {version_hash})");
687
688        Ok(Self {
689            module_inits: ClientModuleInitRegistry::new(),
690            cli_args,
691        })
692    }
693
694    pub fn with_module<T>(mut self, r#gen: T) -> Self
695    where
696        T: ClientModuleInit + 'static + Send + Sync,
697    {
698        self.module_inits.attach(r#gen);
699        self
700    }
701
702    pub fn with_default_modules(self) -> Self {
703        self.with_module(LightningClientInit::default())
704            .with_module(MintClientInit)
705            .with_module(WalletClientInit::default())
706            .with_module(MetaClientInit)
707            .with_module(fedimint_lnv2_client::LightningClientInit::default())
708    }
709
710    pub async fn run(&mut self) {
711        match self.handle_command(self.cli_args.clone()).await {
712            Ok(output) => {
713                // ignore if there's anyone reading the stuff we're writing out
714                let _ = writeln!(std::io::stdout(), "{output}");
715            }
716            Err(err) => {
717                debug!(target: LOG_CLIENT, err = %err.error.as_str(), "Command failed");
718                let _ = writeln!(std::io::stdout(), "{err}");
719                exit(1);
720            }
721        }
722    }
723
724    async fn make_client_builder(&self, cli: &Opts) -> CliResult<(ClientBuilder, Database)> {
725        let mut client_builder = Client::builder()
726            .await
727            .map_err_cli()?
728            .with_iroh_enable_dht(cli.iroh_enable_dht())
729            .with_iroh_enable_next(cli.iroh_enable_next());
730        client_builder.with_module_inits(self.module_inits.clone());
731
732        let db = cli.load_database().await?;
733        Ok((client_builder, db))
734    }
735
736    async fn client_join(
737        &mut self,
738        cli: &Opts,
739        invite_code: InviteCode,
740    ) -> CliResult<ClientHandleArc> {
741        let (client_builder, db) = self.make_client_builder(cli).await?;
742
743        let mnemonic = load_or_generate_mnemonic(&db).await?;
744
745        let client = client_builder
746            .preview(cli.make_endpoints().await.map_err_cli()?, &invite_code)
747            .await
748            .map_err_cli()?
749            .join(
750                db,
751                RootSecret::StandardDoubleDerive(Bip39RootSecretStrategy::<12>::to_root_secret(
752                    &mnemonic,
753                )),
754            )
755            .await
756            .map(Arc::new)
757            .map_err_cli()?;
758
759        print_welcome_message(&client).await;
760        log_expiration_notice(&client).await;
761
762        Ok(client)
763    }
764
765    async fn client_open(&self, cli: &Opts) -> CliResult<ClientHandleArc> {
766        let (mut client_builder, db) = self.make_client_builder(cli).await?;
767
768        if let Some(our_id) = cli.our_id {
769            client_builder.set_admin_creds(AdminCreds {
770                peer_id: our_id,
771                auth: cli.auth()?,
772            });
773        }
774
775        let mnemonic = Mnemonic::from_entropy(
776            &Client::load_decodable_client_secret::<Vec<u8>>(&db)
777                .await
778                .map_err_cli()?,
779        )
780        .map_err_cli()?;
781
782        let client = client_builder
783            .open(
784                cli.make_endpoints().await.map_err_cli()?,
785                db,
786                RootSecret::StandardDoubleDerive(Bip39RootSecretStrategy::<12>::to_root_secret(
787                    &mnemonic,
788                )),
789            )
790            .await
791            .map(Arc::new)
792            .map_err_cli()?;
793
794        log_expiration_notice(&client).await;
795
796        Ok(client)
797    }
798
799    async fn client_recover(
800        &mut self,
801        cli: &Opts,
802        mnemonic: Mnemonic,
803        invite_code: InviteCode,
804    ) -> CliResult<ClientHandleArc> {
805        let (builder, db) = self.make_client_builder(cli).await?;
806        match Client::load_decodable_client_secret_opt::<Vec<u8>>(&db)
807            .await
808            .map_err_cli()?
809        {
810            Some(existing) => {
811                if existing != mnemonic.to_entropy() {
812                    Err(anyhow::anyhow!("Previously set mnemonic does not match")).map_err_cli()?;
813                }
814            }
815            None => {
816                Client::store_encodable_client_secret(&db, mnemonic.to_entropy())
817                    .await
818                    .map_err_cli()?;
819            }
820        }
821
822        let root_secret = RootSecret::StandardDoubleDerive(
823            Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic),
824        );
825
826        let preview = builder
827            .preview(cli.make_endpoints().await.map_err_cli()?, &invite_code)
828            .await
829            .map_err_cli()?;
830
831        let backup = preview
832            .download_backup_from_federation(root_secret.clone())
833            .await
834            .map_err_cli()?;
835
836        let client = preview
837            .recover(db, root_secret, backup)
838            .await
839            .map(Arc::new)
840            .map_err_cli()?;
841
842        print_welcome_message(&client).await;
843        log_expiration_notice(&client).await;
844
845        Ok(client)
846    }
847
848    async fn handle_command(&mut self, cli: Opts) -> CliOutputResult {
849        match cli.command.clone() {
850            Command::InviteCode { peer } => {
851                let client = self.client_open(&cli).await?;
852
853                let invite_code = client
854                    .invite_code(peer)
855                    .await
856                    .ok_or_cli_msg("peer not found")?;
857
858                Ok(CliOutput::InviteCode { invite_code })
859            }
860            Command::JoinFederation { invite_code } => {
861                {
862                    let invite_code: InviteCode = InviteCode::from_str(&invite_code)
863                        .map_err_cli_msg("invalid invite code")?;
864
865                    // Build client and store config in DB
866                    let _client = self.client_join(&cli, invite_code).await?;
867                }
868
869                Ok(CliOutput::JoinFederation {
870                    joined: invite_code,
871                })
872            }
873            Command::VersionHash => Ok(CliOutput::VersionHash {
874                hash: fedimint_build_code_version_env!().to_string(),
875            }),
876            Command::Client(ClientCmd::Restore {
877                mnemonic,
878                invite_code,
879            }) => {
880                let invite_code: InviteCode =
881                    InviteCode::from_str(&invite_code).map_err_cli_msg("invalid invite code")?;
882                let mnemonic = Mnemonic::from_str(&mnemonic).map_err_cli()?;
883                let client = self.client_recover(&cli, mnemonic, invite_code).await?;
884
885                // TODO: until we implement recovery for other modules we can't really wait
886                // for more than this one
887                debug!(target: LOG_CLIENT, "Waiting for mint module recovery to finish");
888                client.wait_for_all_recoveries().await.map_err_cli()?;
889
890                debug!(target: LOG_CLIENT, "Recovery complete");
891
892                Ok(CliOutput::Raw(serde_json::to_value(()).unwrap()))
893            }
894            Command::Client(command) => {
895                let client = self.client_open(&cli).await?;
896                Ok(CliOutput::Raw(
897                    client::handle_command(command, client)
898                        .await
899                        .map_err_cli()?,
900                ))
901            }
902            Command::Admin(AdminCmd::Audit) => {
903                let client = self.client_open(&cli).await?;
904
905                let audit = cli
906                    .admin_client(
907                        &client.get_peer_urls().await,
908                        client.api_secret().as_deref(),
909                    )
910                    .await?
911                    .audit(cli.auth()?)
912                    .await?;
913                Ok(CliOutput::Raw(
914                    serde_json::to_value(audit).map_err_cli_msg("invalid response")?,
915                ))
916            }
917            Command::Admin(AdminCmd::Status) => {
918                let client = self.client_open(&cli).await?;
919
920                let status = cli
921                    .admin_client(
922                        &client.get_peer_urls().await,
923                        client.api_secret().as_deref(),
924                    )
925                    .await?
926                    .status()
927                    .await?;
928                Ok(CliOutput::Raw(
929                    serde_json::to_value(status).map_err_cli_msg("invalid response")?,
930                ))
931            }
932            Command::Admin(AdminCmd::GuardianConfigBackup) => {
933                let client = self.client_open(&cli).await?;
934
935                let guardian_config_backup = cli
936                    .admin_client(
937                        &client.get_peer_urls().await,
938                        client.api_secret().as_deref(),
939                    )
940                    .await?
941                    .guardian_config_backup(cli.auth()?)
942                    .await?;
943                Ok(CliOutput::Raw(
944                    serde_json::to_value(guardian_config_backup)
945                        .map_err_cli_msg("invalid response")?,
946                ))
947            }
948            Command::Admin(AdminCmd::Setup(dkg_args)) => self
949                .handle_admin_setup_command(cli, dkg_args)
950                .await
951                .map(CliOutput::Raw)
952                .map_err_cli_msg("Config Gen Error"),
953            Command::Admin(AdminCmd::SignApiAnnouncement {
954                api_url,
955                override_url,
956            }) => {
957                let client = self.client_open(&cli).await?;
958
959                if !["ws", "wss"].contains(&api_url.scheme()) {
960                    return Err(CliError {
961                        error: format!(
962                            "Unsupported URL scheme {}, use ws:// or wss://",
963                            api_url.scheme()
964                        ),
965                    });
966                }
967
968                let announcement = cli
969                    .admin_client(
970                        &override_url
971                            .and_then(|url| Some(vec![(cli.our_id?, url)].into_iter().collect()))
972                            .unwrap_or(client.get_peer_urls().await),
973                        client.api_secret().as_deref(),
974                    )
975                    .await?
976                    .sign_api_announcement(api_url, cli.auth()?)
977                    .await?;
978
979                Ok(CliOutput::Raw(
980                    serde_json::to_value(announcement).map_err_cli_msg("invalid response")?,
981                ))
982            }
983            Command::Admin(AdminCmd::Shutdown { session_idx }) => {
984                let client = self.client_open(&cli).await?;
985
986                cli.admin_client(
987                    &client.get_peer_urls().await,
988                    client.api_secret().as_deref(),
989                )
990                .await?
991                .shutdown(Some(session_idx), cli.auth()?)
992                .await?;
993
994                Ok(CliOutput::Raw(json!(null)))
995            }
996            Command::Admin(AdminCmd::BackupStatistics) => {
997                let client = self.client_open(&cli).await?;
998
999                let backup_statistics = cli
1000                    .admin_client(
1001                        &client.get_peer_urls().await,
1002                        client.api_secret().as_deref(),
1003                    )
1004                    .await?
1005                    .backup_statistics(cli.auth()?)
1006                    .await?;
1007
1008                Ok(CliOutput::Raw(
1009                    serde_json::to_value(backup_statistics).expect("Can be encoded"),
1010                ))
1011            }
1012            Command::Admin(AdminCmd::ChangePassword { new_password }) => {
1013                let client = self.client_open(&cli).await?;
1014
1015                cli.admin_client(
1016                    &client.get_peer_urls().await,
1017                    client.api_secret().as_deref(),
1018                )
1019                .await?
1020                .change_password(cli.auth()?, &new_password)
1021                .await?;
1022
1023                warn!(target: LOG_CLIENT, "Password changed, please restart fedimintd manually");
1024
1025                Ok(CliOutput::Raw(json!(null)))
1026            }
1027            Command::Dev(DevCmd::Api {
1028                method,
1029                params,
1030                peer_id,
1031                password: auth,
1032                module,
1033            }) => {
1034                //Parse params to JSON.
1035                //If fails, convert to JSON string.
1036                let params = serde_json::from_str::<Value>(&params).unwrap_or_else(|err| {
1037                    debug!(
1038                        target: LOG_CLIENT,
1039                        "Failed to serialize params:{}. Converting it to JSON string",
1040                        err
1041                    );
1042
1043                    serde_json::Value::String(params)
1044                });
1045
1046                let mut params = ApiRequestErased::new(params);
1047                if let Some(auth) = auth {
1048                    params = params.with_auth(ApiAuth(auth));
1049                }
1050                let client = self.client_open(&cli).await?;
1051
1052                let api = client.api_clone();
1053
1054                let module_api = match module {
1055                    Some(selector) => {
1056                        Some(api.with_module(selector.resolve(&client).map_err_cli()?))
1057                    }
1058                    None => None,
1059                };
1060
1061                let response: Value = match (peer_id, module_api) {
1062                    (Some(peer_id), Some(module_api)) => module_api
1063                        .request_raw(peer_id.into(), &method, &params)
1064                        .await
1065                        .map_err_cli()?,
1066                    (Some(peer_id), None) => api
1067                        .request_raw(peer_id.into(), &method, &params)
1068                        .await
1069                        .map_err_cli()?,
1070                    (None, Some(module_api)) => module_api
1071                        .request_current_consensus(method, params)
1072                        .await
1073                        .map_err_cli()?,
1074                    (None, None) => api
1075                        .request_current_consensus(method, params)
1076                        .await
1077                        .map_err_cli()?,
1078                };
1079
1080                Ok(CliOutput::UntypedApiOutput { value: response })
1081            }
1082            Command::Dev(DevCmd::AdvanceNoteIdx { count, amount }) => {
1083                let client = self.client_open(&cli).await?;
1084
1085                let mint = client
1086                    .get_first_module::<MintClientModule>()
1087                    .map_err_cli_msg("can't get mint module")?;
1088
1089                for _ in 0..count {
1090                    mint.advance_note_idx(amount)
1091                        .await
1092                        .map_err_cli_msg("failed to advance the note_idx")?;
1093                }
1094
1095                Ok(CliOutput::Raw(serde_json::Value::Null))
1096            }
1097            Command::Dev(DevCmd::ApiAnnouncements) => {
1098                let client = self.client_open(&cli).await?;
1099                let announcements = client.get_peer_url_announcements().await;
1100                Ok(CliOutput::Raw(
1101                    serde_json::to_value(announcements).expect("Can be encoded"),
1102                ))
1103            }
1104            Command::Dev(DevCmd::WaitBlockCount { count: target }) => retry(
1105                "wait_block_count",
1106                backoff_util::custom_backoff(
1107                    Duration::from_millis(100),
1108                    Duration::from_secs(5),
1109                    None,
1110                ),
1111                || async {
1112                    let client = self.client_open(&cli).await?;
1113                    let wallet = client.get_first_module::<WalletClientModule>()?;
1114                    let count = client
1115                        .api()
1116                        .with_module(wallet.id)
1117                        .fetch_consensus_block_count()
1118                        .await?;
1119                    if count >= target {
1120                        Ok(CliOutput::WaitBlockCount { reached: count })
1121                    } else {
1122                        info!(target: LOG_CLIENT, current=count, target, "Block count not reached");
1123                        Err(format_err!("target not reached"))
1124                    }
1125                },
1126            )
1127            .await
1128            .map_err_cli(),
1129
1130            Command::Dev(DevCmd::WaitComplete) => {
1131                let client = self.client_open(&cli).await?;
1132                client
1133                    .wait_for_all_active_state_machines()
1134                    .await
1135                    .map_err_cli_msg("failed to wait for all active state machines")?;
1136                Ok(CliOutput::Raw(serde_json::Value::Null))
1137            }
1138            Command::Dev(DevCmd::Wait { seconds }) => {
1139                let client = self.client_open(&cli).await?;
1140                // Since most callers are `wait`ing for something to happen,
1141                // let's trigger a network call, so any background threads
1142                // waiting for it starts doing their job.
1143                client
1144                    .task_group()
1145                    .spawn_cancellable("fedimint-cli dev wait: init networking", {
1146                        let client = client.clone();
1147                        async move {
1148                            let _ = client.api().session_count().await;
1149                        }
1150                    });
1151
1152                if let Some(secs) = seconds {
1153                    runtime::sleep(Duration::from_secs_f32(secs)).await;
1154                } else {
1155                    pending::<()>().await;
1156                }
1157                Ok(CliOutput::Raw(serde_json::Value::Null))
1158            }
1159            Command::Dev(DevCmd::Decode { decode_type }) => match decode_type {
1160                DecodeType::InviteCode { invite_code } => Ok(CliOutput::DecodeInviteCode {
1161                    url: invite_code.url(),
1162                    federation_id: invite_code.federation_id(),
1163                }),
1164                DecodeType::Notes { notes, file } => {
1165                    let notes = if let Some(notes) = notes {
1166                        notes
1167                    } else if let Some(file) = file {
1168                        let notes_str =
1169                            fs::read_to_string(file).map_err_cli_msg("failed to read file")?;
1170                        OOBNotes::from_str(&notes_str).map_err_cli_msg("failed to decode notes")?
1171                    } else {
1172                        unreachable!("Clap enforces either notes or file being set");
1173                    };
1174
1175                    let notes_json = notes
1176                        .notes_json()
1177                        .map_err_cli_msg("failed to decode notes")?;
1178                    Ok(CliOutput::Raw(notes_json))
1179                }
1180                DecodeType::Transaction { hex_string } => {
1181                    let bytes: Vec<u8> = hex::FromHex::from_hex(&hex_string)
1182                        .map_err_cli_msg("failed to decode transaction")?;
1183
1184                    let client = self.client_open(&cli).await?;
1185                    let tx = fedimint_core::transaction::Transaction::from_bytes(
1186                        &bytes,
1187                        client.decoders(),
1188                    )
1189                    .map_err_cli_msg("failed to decode transaction")?;
1190
1191                    Ok(CliOutput::DecodeTransaction {
1192                        transaction: (format!("{tx:?}")),
1193                    })
1194                }
1195                DecodeType::SetupCode { setup_code } => {
1196                    let setup_code = base32::decode_prefixed(FEDIMINT_PREFIX, &setup_code)
1197                        .map_err_cli_msg("failed to decode setup code")?;
1198
1199                    Ok(CliOutput::SetupCode { setup_code })
1200                }
1201            },
1202            Command::Dev(DevCmd::Encode { encode_type }) => match encode_type {
1203                EncodeType::InviteCode {
1204                    url,
1205                    federation_id,
1206                    peer,
1207                    api_secret,
1208                } => Ok(CliOutput::InviteCode {
1209                    invite_code: InviteCode::new(url, peer, federation_id, api_secret),
1210                }),
1211                EncodeType::Notes { notes_json } => {
1212                    let notes = serde_json::from_str::<OOBNotesJson>(&notes_json)
1213                        .map_err_cli_msg("invalid JSON for notes")?;
1214                    let prefix =
1215                        FederationIdPrefix::from_str(&notes.federation_id_prefix).map_err_cli()?;
1216                    let notes = OOBNotes::new(prefix, notes.notes);
1217                    Ok(CliOutput::Raw(notes.to_string().into()))
1218                }
1219            },
1220            Command::Dev(DevCmd::SessionCount) => {
1221                let client = self.client_open(&cli).await?;
1222                let count = client.api().session_count().await?;
1223                Ok(CliOutput::EpochCount { count })
1224            }
1225            Command::Dev(DevCmd::ConfigDecrypt {
1226                in_file,
1227                out_file,
1228                salt_file,
1229                password,
1230            }) => {
1231                let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&in_file));
1232                let salt = fs::read_to_string(salt_file).map_err_cli()?;
1233                let key = get_encryption_key(&password, &salt).map_err_cli()?;
1234                let decrypted_bytes = encrypted_read(&key, in_file).map_err_cli()?;
1235
1236                let mut out_file_handle = fs::File::options()
1237                    .create_new(true)
1238                    .write(true)
1239                    .open(out_file)
1240                    .expect("Could not create output cfg file");
1241                out_file_handle.write_all(&decrypted_bytes).map_err_cli()?;
1242                Ok(CliOutput::ConfigDecrypt)
1243            }
1244            Command::Dev(DevCmd::ConfigEncrypt {
1245                in_file,
1246                out_file,
1247                salt_file,
1248                password,
1249            }) => {
1250                let mut in_file_handle =
1251                    fs::File::open(in_file).expect("Could not create output cfg file");
1252                let mut plaintext_bytes = vec![];
1253                in_file_handle.read_to_end(&mut plaintext_bytes).unwrap();
1254
1255                let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&out_file));
1256                let salt = fs::read_to_string(salt_file).map_err_cli()?;
1257                let key = get_encryption_key(&password, &salt).map_err_cli()?;
1258                encrypted_write(plaintext_bytes, &key, out_file).map_err_cli()?;
1259                Ok(CliOutput::ConfigEncrypt)
1260            }
1261            Command::Dev(DevCmd::ListOperationStates { operation_id }) => {
1262                #[derive(Serialize)]
1263                struct ReactorLogState {
1264                    active: bool,
1265                    module_instance: ModuleInstanceId,
1266                    creation_time: String,
1267                    #[serde(skip_serializing_if = "Option::is_none")]
1268                    end_time: Option<String>,
1269                    state: String,
1270                }
1271
1272                let client = self.client_open(&cli).await?;
1273
1274                let (active_states, inactive_states) =
1275                    client.executor().get_operation_states(operation_id).await;
1276                let all_states =
1277                    active_states
1278                        .into_iter()
1279                        .map(|(active_state, active_meta)| ReactorLogState {
1280                            active: true,
1281                            module_instance: active_state.module_instance_id(),
1282                            creation_time: crate::client::time_to_iso8601(&active_meta.created_at),
1283                            end_time: None,
1284                            state: format!("{active_state:?}",),
1285                        })
1286                        .chain(inactive_states.into_iter().map(
1287                            |(inactive_state, inactive_meta)| ReactorLogState {
1288                                active: false,
1289                                module_instance: inactive_state.module_instance_id(),
1290                                creation_time: crate::client::time_to_iso8601(
1291                                    &inactive_meta.created_at,
1292                                ),
1293                                end_time: Some(crate::client::time_to_iso8601(
1294                                    &inactive_meta.exited_at,
1295                                )),
1296                                state: format!("{inactive_state:?}",),
1297                            },
1298                        ))
1299                        .sorted_by(|a, b| a.creation_time.cmp(&b.creation_time))
1300                        .collect::<Vec<_>>();
1301
1302                Ok(CliOutput::Raw(json!({
1303                    "states": all_states
1304                })))
1305            }
1306            Command::Dev(DevCmd::MetaFields) => {
1307                let client = self.client_open(&cli).await?;
1308                let source = MetaModuleMetaSourceWithFallback::<LegacyMetaSource>::default();
1309
1310                let meta_fields = source
1311                    .fetch(
1312                        &client.config().await,
1313                        &client.api_clone(),
1314                        FetchKind::Initial,
1315                        None,
1316                    )
1317                    .await
1318                    .map_err_cli()?;
1319
1320                Ok(CliOutput::Raw(
1321                    serde_json::to_value(meta_fields).expect("Can be encoded"),
1322                ))
1323            }
1324            Command::Dev(DevCmd::PeerVersion { peer_id }) => {
1325                let client = self.client_open(&cli).await?;
1326                let version = client
1327                    .api()
1328                    .fedimintd_version(peer_id.into())
1329                    .await
1330                    .map_err_cli()?;
1331
1332                Ok(CliOutput::Raw(json!({ "version": version })))
1333            }
1334            Command::Dev(DevCmd::ShowEventLog { pos, limit }) => {
1335                let client = self.client_open(&cli).await?;
1336
1337                let events: Vec<_> = client
1338                    .get_event_log(pos, limit)
1339                    .await
1340                    .into_iter()
1341                    .map(|v| {
1342                        let id = v.id();
1343                        let v = v.as_raw();
1344                        let module_id = v.module.as_ref().map(|m| m.1);
1345                        let module_kind = v.module.as_ref().map(|m| m.0.clone());
1346                        serde_json::json!({
1347                            "id": id,
1348                            "kind": v.kind,
1349                            "module_kind": module_kind,
1350                            "module_id": module_id,
1351                            "ts": v.ts_usecs,
1352                            "payload": serde_json::from_slice(&v.payload).unwrap_or_else(|_| hex::encode(&v.payload)),
1353                        })
1354                    })
1355                    .collect();
1356
1357                Ok(CliOutput::Raw(
1358                    serde_json::to_value(events).expect("Can be encoded"),
1359                ))
1360            }
1361            Command::Dev(DevCmd::ShowEventLogTrimable { pos, limit }) => {
1362                let client = self.client_open(&cli).await?;
1363
1364                let events: Vec<_> = client
1365                    .get_event_log_trimable(
1366                        pos.map(|id| EventLogTrimableId::from(u64::from(id))),
1367                        limit,
1368                    )
1369                    .await
1370                    .into_iter()
1371                    .map(|v| {
1372                        let id = v.id();
1373                        let v = v.as_raw();
1374                        let module_id = v.module.as_ref().map(|m| m.1);
1375                        let module_kind = v.module.as_ref().map(|m| m.0.clone());
1376                        serde_json::json!({
1377                            "id": id,
1378                            "kind": v.kind,
1379                            "module_kind": module_kind,
1380                            "module_id": module_id,
1381                            "ts": v.ts_usecs,
1382                            "payload": serde_json::from_slice(&v.payload).unwrap_or_else(|_| hex::encode(&v.payload)),
1383                        })
1384                    })
1385                    .collect();
1386
1387                Ok(CliOutput::Raw(
1388                    serde_json::to_value(events).expect("Can be encoded"),
1389                ))
1390            }
1391            Command::Dev(DevCmd::SubmitTransaction { transaction }) => {
1392                let client = self.client_open(&cli).await?;
1393                let tx = Transaction::consensus_decode_hex(&transaction, client.decoders())
1394                    .map_err_cli()?;
1395                let tx_outcome = client
1396                    .api()
1397                    .submit_transaction(tx)
1398                    .await
1399                    .try_into_inner(client.decoders())
1400                    .map_err_cli()?;
1401
1402                Ok(CliOutput::Raw(
1403                    serde_json::to_value(tx_outcome.0.map_err_cli()?).expect("Can be encoded"),
1404                ))
1405            }
1406            Command::Dev(DevCmd::TestEventLogHandling) => {
1407                let client = self.client_open(&cli).await?;
1408
1409                client
1410                    .handle_events(
1411                        client.built_in_application_event_log_tracker(),
1412                        move |_dbtx, event| {
1413                            Box::pin(async move {
1414                                info!(target: LOG_CLIENT, "{event:?}");
1415
1416                                Ok(())
1417                            })
1418                        },
1419                    )
1420                    .await
1421                    .map_err_cli()?;
1422                unreachable!(
1423                    "handle_events exits only if client shuts down, which we don't do here"
1424                )
1425            }
1426            Command::Completion { shell } => {
1427                let bin_path = PathBuf::from(
1428                    std::env::args_os()
1429                        .next()
1430                        .expect("Binary name is always provided if we get this far"),
1431                );
1432                let bin_name = bin_path
1433                    .file_name()
1434                    .expect("path has file name")
1435                    .to_string_lossy();
1436                clap_complete::generate(
1437                    shell,
1438                    &mut Opts::command(),
1439                    bin_name.as_ref(),
1440                    &mut std::io::stdout(),
1441                );
1442                // HACK: prints true to stdout which is fine for shells
1443                Ok(CliOutput::Raw(serde_json::Value::Bool(true)))
1444            }
1445        }
1446    }
1447
1448    async fn handle_admin_setup_command(
1449        &self,
1450        cli: Opts,
1451        args: SetupAdminArgs,
1452    ) -> anyhow::Result<Value> {
1453        let client =
1454            DynGlobalApi::new_admin_setup(cli.make_endpoints().await?, args.endpoint.clone())?;
1455
1456        match &args.subcommand {
1457            SetupAdminCmd::Status => {
1458                let status = client.setup_status(cli.auth()?).await?;
1459
1460                Ok(serde_json::to_value(status).expect("JSON serialization failed"))
1461            }
1462            SetupAdminCmd::SetLocalParams {
1463                name,
1464                federation_name,
1465            } => {
1466                let info = client
1467                    .set_local_params(
1468                        name.clone(),
1469                        federation_name.clone(),
1470                        None,
1471                        None,
1472                        cli.auth()?,
1473                    )
1474                    .await?;
1475
1476                Ok(serde_json::to_value(info).expect("JSON serialization failed"))
1477            }
1478            SetupAdminCmd::AddPeer { info } => {
1479                let name = client
1480                    .add_peer_connection_info(info.clone(), cli.auth()?)
1481                    .await?;
1482
1483                Ok(serde_json::to_value(name).expect("JSON serialization failed"))
1484            }
1485            SetupAdminCmd::StartDkg => {
1486                client.start_dkg(cli.auth()?).await?;
1487
1488                Ok(Value::Null)
1489            }
1490        }
1491    }
1492}
1493
1494async fn log_expiration_notice(client: &Client) {
1495    client.get_meta_expiration_timestamp().await;
1496    if let Some(expiration_time) = client.get_meta_expiration_timestamp().await {
1497        match expiration_time.duration_since(fedimint_core::time::now()) {
1498            Ok(until_expiration) => {
1499                let days = until_expiration.as_secs() / (60 * 60 * 24);
1500
1501                if 90 < days {
1502                    debug!(target: LOG_CLIENT, %days, "This federation will expire");
1503                } else if 30 < days {
1504                    info!(target: LOG_CLIENT, %days, "This federation will expire");
1505                } else {
1506                    warn!(target: LOG_CLIENT, %days, "This federation will expire soon");
1507                }
1508            }
1509            Err(_) => {
1510                tracing::error!(target: LOG_CLIENT, "This federation has expired and might not be safe to use");
1511            }
1512        }
1513    }
1514}
1515async fn print_welcome_message(client: &Client) {
1516    if let Some(welcome_message) = client
1517        .meta_service()
1518        .get_field::<String>(client.db(), "welcome_message")
1519        .await
1520        .and_then(|v| v.value)
1521    {
1522        eprintln!("{welcome_message}");
1523    }
1524}
1525
1526fn salt_from_file_path(file_path: &Path) -> PathBuf {
1527    file_path
1528        .parent()
1529        .expect("File has no parent?!")
1530        .join(SALT_FILE)
1531}
1532
1533/// Convert clap arguments to backup metadata
1534fn metadata_from_clap_cli(metadata: Vec<String>) -> Result<BTreeMap<String, String>, CliError> {
1535    let metadata: BTreeMap<String, String> = metadata
1536        .into_iter()
1537        .map(|item| {
1538            match &item
1539                .splitn(2, '=')
1540                .map(ToString::to_string)
1541                .collect::<Vec<String>>()[..]
1542            {
1543                [] => Err(format_err!("Empty metadata argument not allowed")),
1544                [key] => Err(format_err!("Metadata {key} is missing a value")),
1545                [key, val] => Ok((key.clone(), val.clone())),
1546                [..] => unreachable!(),
1547            }
1548        })
1549        .collect::<anyhow::Result<_>>()
1550        .map_err_cli_msg("invalid metadata")?;
1551    Ok(metadata)
1552}
1553
1554#[test]
1555fn metadata_from_clap_cli_test() {
1556    for (args, expected) in [
1557        (
1558            vec!["a=b".to_string()],
1559            BTreeMap::from([("a".into(), "b".into())]),
1560        ),
1561        (
1562            vec!["a=b".to_string(), "c=d".to_string()],
1563            BTreeMap::from([("a".into(), "b".into()), ("c".into(), "d".into())]),
1564        ),
1565    ] {
1566        assert_eq!(metadata_from_clap_cli(args).unwrap(), expected);
1567    }
1568}