Skip to main content

fedimint_cli/
cli.rs

1use std::path::PathBuf;
2
3use clap::builder::BoolishValueParser;
4use clap::{Args, Parser, Subcommand};
5use fedimint_core::config::FederationId;
6use fedimint_core::core::OperationId;
7use fedimint_core::invite_code::InviteCode;
8use fedimint_core::util::SafeUrl;
9use fedimint_core::{Amount, PeerId, TieredMulti};
10use fedimint_eventlog::EventLogId;
11use fedimint_mint_client::{OOBNotes, SpendableNote};
12use serde::{Deserialize, Serialize};
13
14use crate::client::{ClientCmd, ModuleSelector};
15#[cfg(feature = "tor")]
16use crate::envs::FM_USE_TOR_ENV;
17use crate::envs::{
18    FM_API_SECRET_ENV, FM_CLIENT_DIR_ENV, FM_DB_BACKEND_ENV, FM_FEDERATION_SECRET_HEX_ENV,
19    FM_IROH_ENABLE_DHT_ENV, FM_OUR_ID_ENV, FM_PASSWORD_API_ENV,
20};
21use crate::utils::parse_peer_id;
22
23#[derive(Debug, Clone, Copy, clap::ValueEnum)]
24pub(crate) enum DatabaseBackend {
25    /// Use RocksDB database backend
26    #[value(name = "rocksdb")]
27    RocksDb,
28    /// Use CursedRedb database backend (hybrid memory/redb)
29    #[value(name = "cursed-redb")]
30    CursedRedb,
31}
32
33#[derive(Parser, Clone)]
34#[command(version)]
35pub(crate) struct Opts {
36    /// The working directory of the client containing the config and db
37    #[arg(long = "data-dir", env = FM_CLIENT_DIR_ENV)]
38    pub data_dir: Option<PathBuf>,
39
40    /// Peer id of the guardian
41    #[arg(env = FM_OUR_ID_ENV, long, value_parser = parse_peer_id)]
42    pub our_id: Option<PeerId>,
43
44    /// Guardian password for authentication
45    #[arg(long, env = FM_PASSWORD_API_ENV)]
46    pub password: Option<String>,
47
48    /// Federation secret as consensus-encoded hex.
49    #[arg(long, env = FM_FEDERATION_SECRET_HEX_ENV)]
50    pub federation_secret_hex: Option<String>,
51
52    #[cfg(feature = "tor")]
53    /// Activate usage of Tor as the Connector when building the Client
54    #[arg(long, env = FM_USE_TOR_ENV, value_parser = BoolishValueParser::new())]
55    pub use_tor: bool,
56
57    // Enable using DHT name resolution in Iroh
58    #[arg(long, env = FM_IROH_ENABLE_DHT_ENV, value_parser = BoolishValueParser::new())]
59    pub iroh_enable_dht: Option<bool>,
60
61    /// Database backend to use.
62    #[arg(long, env = FM_DB_BACKEND_ENV, value_enum, default_value = "rocksdb")]
63    pub db_backend: DatabaseBackend,
64
65    /// Activate more verbose logging, for full control use the RUST_LOG env
66    /// variable
67    #[arg(short = 'v', long)]
68    pub verbose: bool,
69
70    #[clap(subcommand)]
71    pub command: Command,
72}
73
74#[derive(Subcommand, Clone)]
75pub(crate) enum Command {
76    /// Print the latest Git commit hash this bin. was built with.
77    VersionHash,
78
79    #[clap(flatten)]
80    Client(ClientCmd),
81
82    #[clap(subcommand)]
83    Admin(AdminCmd),
84
85    #[clap(subcommand)]
86    Dev(DevCmd),
87
88    /// Config enabling client to establish websocket connection to federation
89    InviteCode {
90        peer: PeerId,
91    },
92
93    /// Join a federation using its InviteCode
94    #[clap(alias = "join-federation")]
95    Join {
96        invite_code: String,
97    },
98
99    Completion {
100        shell: clap_complete::Shell,
101    },
102}
103
104#[allow(clippy::large_enum_variant)]
105#[derive(Debug, Clone, Subcommand)]
106pub(crate) enum AdminCmd {
107    /// Store admin credentials (peer_id and password) in the client database.
108    ///
109    /// This allows subsequent admin commands to be run without specifying
110    /// `--our-id` and `--password` each time.
111    ///
112    /// The command will verify the credentials by making an authenticated
113    /// API call before storing them.
114    Auth {
115        /// Guardian's peer ID
116        #[arg(long, env = FM_OUR_ID_ENV)]
117        peer_id: u16,
118        /// Guardian password for authentication
119        #[arg(long, env = FM_PASSWORD_API_ENV)]
120        password: String,
121        /// Skip interactive endpoint verification
122        #[arg(long)]
123        no_verify: bool,
124        /// Force overwrite existing stored credentials
125        #[arg(long)]
126        force: bool,
127    },
128
129    /// Show the status according to the `status` endpoint
130    Status,
131
132    /// Show an audit across all modules
133    Audit,
134
135    /// Download guardian config to back it up
136    GuardianConfigBackup,
137
138    Setup(SetupAdminArgs),
139    /// Sign and announce a new API endpoint. The previous one will be
140    /// invalidated
141    SignApiAnnouncement {
142        /// New API URL to announce
143        api_url: SafeUrl,
144        /// Provide the API url for the guardian directly in case the old one
145        /// isn't reachable anymore
146        #[clap(long)]
147        override_url: Option<SafeUrl>,
148    },
149    /// Sign guardian metadata
150    SignGuardianMetadata {
151        /// API URLs (can be specified multiple times or comma-separated)
152        #[clap(long, value_delimiter = ',')]
153        api_urls: Vec<SafeUrl>,
154        /// Pkarr ID (z32 format)
155        #[clap(long)]
156        pkarr_id: String,
157    },
158    /// Stop fedimintd after the specified session to do a coordinated upgrade
159    Shutdown {
160        /// Session index to stop after
161        session_idx: u64,
162    },
163    /// Show statistics about client backups stored by the federation
164    BackupStatistics,
165}
166
167#[derive(Debug, Clone, Args)]
168pub(crate) struct SetupAdminArgs {
169    pub endpoint: SafeUrl,
170
171    #[clap(subcommand)]
172    pub subcommand: SetupAdminCmd,
173}
174
175#[derive(Debug, Clone, Subcommand)]
176pub(crate) enum SetupAdminCmd {
177    Status,
178    SetLocalParams {
179        name: String,
180        #[clap(long)]
181        federation_name: Option<String>,
182        #[clap(long)]
183        federation_size: Option<u32>,
184    },
185    AddPeer {
186        info: String,
187    },
188    StartDkg,
189}
190
191#[derive(Debug, Clone, Subcommand)]
192pub(crate) enum DecodeType {
193    /// Decode an invite code string into a JSON representation
194    InviteCode { invite_code: InviteCode },
195    /// Decode a string of ecash notes into a JSON representation
196    #[group(required = true, multiple = false)]
197    Notes {
198        /// Base64 e-cash notes to be decoded
199        notes: Option<OOBNotes>,
200        /// File containing base64 e-cash notes to be decoded
201        #[arg(long)]
202        file: Option<PathBuf>,
203    },
204    /// Decode a transaction hex string and print it to stdout
205    Transaction { hex_string: String },
206    /// Decode a setup code (as shared during a federation setup ceremony)
207    /// string into a JSON representation
208    SetupCode { setup_code: String },
209}
210
211#[derive(Debug, Clone, Deserialize, Serialize)]
212pub(crate) struct OOBNotesJson {
213    pub federation_id_prefix: String,
214    pub notes: TieredMulti<SpendableNote>,
215}
216
217#[derive(Debug, Clone, Subcommand)]
218pub(crate) enum EncodeType {
219    /// Encode connection info from its constituent parts
220    InviteCode {
221        #[clap(long)]
222        url: SafeUrl,
223        #[clap(long = "federation_id")]
224        federation_id: FederationId,
225        #[clap(long = "peer")]
226        peer: PeerId,
227        #[arg(env = FM_API_SECRET_ENV)]
228        api_secret: Option<String>,
229    },
230
231    /// Encode a JSON string of notes to an ecash string
232    Notes { notes_json: String },
233}
234
235#[derive(Debug, Clone, Subcommand)]
236pub(crate) enum DevCmd {
237    /// Send direct method call to the API. If you specify --peer-id, it will
238    /// just ask one server, otherwise it will try to get consensus from all
239    /// servers.
240    #[command(after_long_help = r#"
241Examples:
242
243  fedimint-cli dev api --peer-id 0 config '"fed114znk7uk7ppugdjuytr8venqf2tkywd65cqvg3u93um64tu5cw4yr0n3fvn7qmwvm4g48cpndgnm4gqq4waen5te0xyerwt3s9cczuvf6xyurzde597s7crdvsk2vmyarjw9gwyqjdzj"'
244    "#)]
245    Api {
246        /// JSON-RPC method to call
247        method: String,
248        /// JSON-RPC parameters for the request
249        ///
250        /// Note: single jsonrpc argument params string, which might require
251        /// double-quotes (see example above).
252        #[clap(default_value = "null")]
253        params: String,
254        /// Which server to send request to
255        #[clap(long = "peer-id")]
256        peer_id: Option<u16>,
257
258        /// Module selector (either module id or module kind)
259        #[clap(long = "module")]
260        module: Option<ModuleSelector>,
261
262        /// Guardian password in case authenticated API endpoints are being
263        /// called. Only use together with --peer-id.
264        #[clap(long, requires = "peer_id")]
265        password: Option<String>,
266    },
267
268    ApiAnnouncements,
269
270    GuardianMetadata,
271
272    /// Advance the note_idx
273    AdvanceNoteIdx {
274        #[clap(long, default_value = "1")]
275        count: usize,
276
277        #[clap(long)]
278        amount: Amount,
279    },
280
281    /// Wait for the fed to reach a consensus block count
282    WaitBlockCount {
283        count: u64,
284    },
285
286    /// Just start the `Client` and wait
287    Wait {
288        /// Limit the wait time
289        seconds: Option<f32>,
290    },
291
292    /// Wait for all state machines to complete
293    WaitComplete,
294
295    /// Decode invite code or ecash notes string into a JSON representation
296    Decode {
297        #[clap(subcommand)]
298        decode_type: DecodeType,
299    },
300
301    /// Encode an invite code or ecash notes into binary
302    Encode {
303        #[clap(subcommand)]
304        encode_type: EncodeType,
305    },
306
307    /// Gets the current fedimint AlephBFT block count
308    SessionCount,
309
310    /// Show public guardian IPs and iroh connection paths from an invite code
311    #[command(
312        name = "query-federation-ips",
313        visible_aliases = ["federation-ip-query", "iroh-ip-query"]
314    )]
315    QueryFederationIps {
316        invite_code: InviteCode,
317        /// Time to wait for iroh to discover a direct path to each guardian
318        #[arg(long, default_value = "5")]
319        path_timeout_seconds: u64,
320        /// Fail unless every iroh guardian reaches a direct or mixed path
321        /// before timeout
322        #[arg(long)]
323        require_direct: bool,
324    },
325
326    /// Returns the client config
327    Config,
328
329    ConfigDecrypt {
330        /// Encrypted config file
331        #[arg(long = "in-file")]
332        in_file: PathBuf,
333        /// Plaintext config file output
334        #[arg(long = "out-file")]
335        out_file: PathBuf,
336        /// Encryption salt file, otherwise defaults to the salt file from the
337        /// `in_file` directory
338        #[arg(long = "salt-file")]
339        salt_file: Option<PathBuf>,
340        /// The password that encrypts the configs
341        #[arg(env = FM_PASSWORD_API_ENV)]
342        password: String,
343    },
344
345    ConfigEncrypt {
346        /// Plaintext config file
347        #[arg(long = "in-file")]
348        in_file: PathBuf,
349        /// Encrypted config file output
350        #[arg(long = "out-file")]
351        out_file: PathBuf,
352        /// Encryption salt file, otherwise defaults to the salt file from the
353        /// `out_file` directory
354        #[arg(long = "salt-file")]
355        salt_file: Option<PathBuf>,
356        /// The password that encrypts the configs
357        #[arg(env = FM_PASSWORD_API_ENV)]
358        password: String,
359    },
360
361    /// Lists active and inactive state machine states of the operation
362    /// chronologically
363    ListOperationStates {
364        operation_id: OperationId,
365    },
366    /// Returns the federation's meta fields. If they are set correctly via the
367    /// meta module these are returned, otherwise the legacy mechanism
368    /// (config+override file) is used.
369    MetaFields,
370    /// Gets the tagged fedimintd version for a peer
371    PeerVersion {
372        #[clap(long)]
373        peer_id: u16,
374    },
375    /// Dump Client's Event Log
376    ShowEventLog {
377        #[arg(long)]
378        pos: Option<EventLogId>,
379        #[arg(long, default_value = "10")]
380        limit: u64,
381    },
382    /// Dump Client's Trimable Event Log
383    ShowEventLogTrimable {
384        #[arg(long)]
385        pos: Option<EventLogId>,
386        #[arg(long, default_value = "10")]
387        limit: u64,
388    },
389    /// Print the id the next entry appended to the client's event log will be
390    /// assigned (the position just past the current end of the log).
391    NextEventLogId,
392    /// Test the built-in event handling and tracking by printing events to
393    /// console
394    TestEventLogHandling,
395    /// Manually submit a fedimint transaction to guardians
396    ///
397    /// This can be useful to check why a transaction may have been rejected
398    /// when debugging client issues.
399    SubmitTransaction {
400        /// Hex-encoded fedimint transaction
401        transaction: String,
402    },
403    /// Show the chain ID (bitcoin block hash at height 1) cached in the client
404    /// database
405    ChainId,
406    /// Force refresh API versions from the federation, bypassing cached values.
407    /// Queries all peers for their supported API versions and updates the
408    /// cache.
409    RefreshApiVersions,
410    /// Trigger a panic to verify backtrace handling
411    Panic,
412    /// Visualize client internals for debugging
413    Visualize {
414        #[clap(subcommand)]
415        visualize_type: VisualizeCmd,
416    },
417}
418
419#[derive(Debug, Clone, Subcommand)]
420pub(crate) enum VisualizeCmd {
421    /// Show every e-cash note with creation/spending provenance
422    Notes {
423        #[arg(long)]
424        limit: Option<usize>,
425    },
426    /// Show transactions with inputs and outputs
427    Transactions {
428        /// Show a specific operation (by full ID)
429        operation_id: Option<OperationId>,
430        /// How many most-recent operations to show (ignored if operation_id is
431        /// given)
432        #[arg(long)]
433        limit: Option<usize>,
434    },
435    /// Show operations with their state machines
436    Operations {
437        /// Show a specific operation (by full ID)
438        operation_id: Option<OperationId>,
439        /// How many most-recent operations to show (ignored if operation_id is
440        /// given)
441        #[arg(long)]
442        limit: Option<usize>,
443    },
444}