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