Skip to main content

fedimint_dbtool/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::return_self_not_must_use)]
6
7pub mod envs;
8
9use std::path::PathBuf;
10
11use anyhow::Result;
12use bytes::Bytes;
13use clap::{Parser, Subcommand};
14use fedimint_client::module_init::ClientModuleInitRegistry;
15use fedimint_client_module::module::init::ClientModuleInit;
16use fedimint_core::db::{IDatabaseTransactionOpsCore, IRawDatabaseExt};
17use fedimint_core::util::handle_version_hash_command;
18use fedimint_ln_client::LightningClientInit;
19use fedimint_ln_server::LightningInit;
20use fedimint_logging::TracingSetup;
21use fedimint_meta_client::MetaClientInit;
22use fedimint_meta_server::MetaInit;
23use fedimint_mint_client::MintClientInit;
24use fedimint_mint_server::MintInit;
25use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
26use fedimint_wallet_client::WalletClientInit;
27use fedimint_wallet_server::WalletInit;
28use futures::StreamExt;
29use hex::ToHex;
30
31use crate::dump::DatabaseDump;
32use crate::envs::{FM_DBTOOL_CONFIG_DIR_ENV, FM_DBTOOL_DATABASE_ENV};
33
34mod dump;
35
36#[derive(Debug, Clone, Parser)]
37#[command(version)]
38struct Options {
39    #[clap(long, env = FM_DBTOOL_DATABASE_ENV)]
40    database_dir: String,
41
42    #[clap(long, hide = true)]
43    /// Run dbtool like it doesn't know about any module kind. This is a
44    /// internal option for testing.
45    no_modules: bool,
46
47    #[command(subcommand)]
48    command: DbCommand,
49}
50
51/// Tool to inspect and manipulate rocksdb databases. All binary arguments
52/// (keys, values) have to be hex encoded.
53#[derive(Debug, Clone, Subcommand)]
54enum DbCommand {
55    /// List all key-value pairs where the key begins with `prefix`
56    List {
57        #[arg(long, value_parser = hex_parser)]
58        prefix: Bytes,
59    },
60    /// Write a key-value pair to the database, overwriting the previous value
61    /// if present
62    Write {
63        #[arg(long, value_parser = hex_parser)]
64        key: Bytes,
65        #[arg(long, value_parser = hex_parser)]
66        value: Bytes,
67    },
68    /// Delete a single entry from the database identified by `key`
69    Delete {
70        #[arg(long, value_parser = hex_parser)]
71        key: Bytes,
72    },
73    /// Deletes all keys starting
74    DeletePrefix {
75        #[arg(long, value_parser = hex_parser)]
76        prefix: Bytes,
77    },
78    /// Dump a subset of the specified database and serialize the retrieved data
79    /// to JSON. Module and prefix are used to specify which subset of the
80    /// database to dump.
81    Dump {
82        #[clap(long, env = FM_DBTOOL_CONFIG_DIR_ENV)]
83        cfg_dir: PathBuf,
84        #[arg(long, required = false)]
85        modules: Option<String>,
86        #[arg(long, required = false)]
87        prefixes: Option<String>,
88    },
89}
90
91fn hex_parser(hex: &str) -> Result<Bytes> {
92    let bytes: Vec<u8> = hex::FromHex::from_hex(hex)?;
93    Ok(bytes.into())
94}
95
96fn print_kv(key: &[u8], value: &[u8]) {
97    println!(
98        "{} {}",
99        key.encode_hex::<String>(),
100        value.encode_hex::<String>()
101    );
102}
103
104pub struct FedimintDBTool {
105    server_module_inits: ServerModuleInitRegistry,
106    client_module_inits: ClientModuleInitRegistry,
107    cli_args: Options,
108}
109
110impl FedimintDBTool {
111    /// Build a new `fedimintdb-tool` with a custom version hash
112    pub fn new(version_hash: &str) -> anyhow::Result<Self> {
113        handle_version_hash_command(version_hash);
114        TracingSetup::default().init()?;
115
116        Ok(Self {
117            server_module_inits: ServerModuleInitRegistry::new(),
118            client_module_inits: ClientModuleInitRegistry::new(),
119            cli_args: Options::parse(),
120        })
121    }
122
123    pub fn with_server_module_init<T>(mut self, r#gen: T) -> Self
124    where
125        T: ServerModuleInit + 'static + Send + Sync,
126    {
127        self.server_module_inits.attach(r#gen);
128        self
129    }
130
131    pub fn with_client_module_init<T>(mut self, r#gen: T) -> Self
132    where
133        T: ClientModuleInit + 'static + Send + Sync,
134    {
135        self.client_module_inits.attach(r#gen);
136        self
137    }
138
139    pub fn with_default_modules_inits(self) -> Self {
140        self.with_server_module_init(WalletInit)
141            .with_server_module_init(MintInit)
142            .with_server_module_init(LightningInit)
143            .with_server_module_init(fedimint_lnv2_server::LightningInit)
144            .with_server_module_init(MetaInit)
145            .with_client_module_init(WalletClientInit::default())
146            .with_client_module_init(MintClientInit)
147            .with_client_module_init(LightningClientInit::default())
148            .with_client_module_init(fedimint_lnv2_client::LightningClientInit::default())
149            .with_client_module_init(fedimint_walletv2_client::WalletClientInit)
150            .with_client_module_init(MetaClientInit)
151    }
152
153    pub async fn run(&self) -> anyhow::Result<()> {
154        let options = &self.cli_args;
155        match &options.command {
156            DbCommand::List { prefix } => {
157                let rocksdb = open_db(options).await;
158                let mut dbtx = rocksdb.begin_transaction().await;
159                let prefix_iter = dbtx
160                    .raw_find_by_prefix(prefix)
161                    .await?
162                    .collect::<Vec<_>>()
163                    .await;
164                for (key, value) in prefix_iter {
165                    print_kv(&key, &value);
166                }
167                dbtx.commit_tx().await;
168            }
169            DbCommand::Write { key, value } => {
170                let rocksdb = open_db(options).await;
171                let mut dbtx = rocksdb.begin_transaction().await;
172                dbtx.raw_insert_bytes(key, value)
173                    .await
174                    .expect("Error inserting entry into RocksDb");
175                dbtx.commit_tx().await;
176            }
177            DbCommand::Delete { key } => {
178                let rocksdb = open_db(options).await;
179                let mut dbtx = rocksdb.begin_transaction().await;
180                dbtx.raw_remove_entry(key)
181                    .await
182                    .expect("Error removing entry from RocksDb");
183                dbtx.commit_tx().await;
184            }
185            DbCommand::Dump {
186                cfg_dir,
187                modules,
188                prefixes,
189            } => {
190                let modules = match modules {
191                    Some(mods) => mods
192                        .split(',')
193                        .map(|s| s.to_string().to_lowercase())
194                        .collect::<Vec<String>>(),
195                    None => Vec::new(),
196                };
197
198                let prefix_names = match prefixes {
199                    Some(db_prefixes) => db_prefixes
200                        .split(',')
201                        .map(|s| s.to_string().to_lowercase())
202                        .collect::<Vec<String>>(),
203                    None => Vec::new(),
204                };
205
206                let (module_inits, client_module_inits) = if options.no_modules {
207                    (
208                        ServerModuleInitRegistry::new(),
209                        ClientModuleInitRegistry::new(),
210                    )
211                } else {
212                    (
213                        self.server_module_inits.clone(),
214                        self.client_module_inits.clone(),
215                    )
216                };
217
218                let mut dbdump = DatabaseDump::new(
219                    cfg_dir.clone(),
220                    options.database_dir.clone(),
221                    module_inits,
222                    client_module_inits,
223                    modules,
224                    prefix_names,
225                )
226                .await?;
227                dbdump.dump_database().await?;
228            }
229            DbCommand::DeletePrefix { prefix } => {
230                let rocksdb = open_db(options).await;
231                let mut dbtx = rocksdb.begin_transaction().await;
232                dbtx.raw_remove_by_prefix(prefix).await?;
233                dbtx.commit_tx().await;
234            }
235        }
236
237        Ok(())
238    }
239}
240
241async fn open_db(options: &Options) -> fedimint_core::db::Database {
242    fedimint_rocksdb::RocksDb::build(&options.database_dir)
243        .open()
244        .await
245        .unwrap()
246        .into_database()
247}