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, FM_PASSWORD_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 no_modules: bool,
46
47 #[command(subcommand)]
48 command: DbCommand,
49}
50
51#[derive(Debug, Clone, Subcommand)]
54enum DbCommand {
55 List {
57 #[arg(long, value_parser = hex_parser)]
58 prefix: Bytes,
59 },
60 Write {
63 #[arg(long, value_parser = hex_parser)]
64 key: Bytes,
65 #[arg(long, value_parser = hex_parser)]
66 value: Bytes,
67 },
68 Delete {
70 #[arg(long, value_parser = hex_parser)]
71 key: Bytes,
72 },
73 DeletePrefix {
75 #[arg(long, value_parser = hex_parser)]
76 prefix: Bytes,
77 },
78 Dump {
84 #[clap(long, env = FM_DBTOOL_CONFIG_DIR_ENV)]
85 cfg_dir: PathBuf,
86 #[arg(long, env = FM_PASSWORD_ENV)]
87 password: String,
88 #[arg(long, required = false)]
89 modules: Option<String>,
90 #[arg(long, required = false)]
91 prefixes: Option<String>,
92 },
93}
94
95fn hex_parser(hex: &str) -> Result<Bytes> {
96 let bytes: Vec<u8> = hex::FromHex::from_hex(hex)?;
97 Ok(bytes.into())
98}
99
100fn print_kv(key: &[u8], value: &[u8]) {
101 println!(
102 "{} {}",
103 key.encode_hex::<String>(),
104 value.encode_hex::<String>()
105 );
106}
107
108pub struct FedimintDBTool {
109 server_module_inits: ServerModuleInitRegistry,
110 client_module_inits: ClientModuleInitRegistry,
111 cli_args: Options,
112}
113
114impl FedimintDBTool {
115 pub fn new(version_hash: &str) -> anyhow::Result<Self> {
117 handle_version_hash_command(version_hash);
118 TracingSetup::default().init()?;
119
120 Ok(Self {
121 server_module_inits: ServerModuleInitRegistry::new(),
122 client_module_inits: ClientModuleInitRegistry::new(),
123 cli_args: Options::parse(),
124 })
125 }
126
127 pub fn with_server_module_init<T>(mut self, r#gen: T) -> Self
128 where
129 T: ServerModuleInit + 'static + Send + Sync,
130 {
131 self.server_module_inits.attach(r#gen);
132 self
133 }
134
135 pub fn with_client_module_init<T>(mut self, r#gen: T) -> Self
136 where
137 T: ClientModuleInit + 'static + Send + Sync,
138 {
139 self.client_module_inits.attach(r#gen);
140 self
141 }
142
143 pub fn with_default_modules_inits(self) -> Self {
144 self.with_server_module_init(WalletInit)
145 .with_server_module_init(MintInit)
146 .with_server_module_init(LightningInit)
147 .with_server_module_init(fedimint_lnv2_server::LightningInit)
148 .with_server_module_init(MetaInit)
149 .with_client_module_init(WalletClientInit::default())
150 .with_client_module_init(MintClientInit)
151 .with_client_module_init(LightningClientInit::default())
152 .with_client_module_init(fedimint_lnv2_client::LightningClientInit::default())
153 .with_client_module_init(MetaClientInit)
154 }
155
156 pub async fn run(&self) -> anyhow::Result<()> {
157 let options = &self.cli_args;
158 match &options.command {
159 DbCommand::List { prefix } => {
160 let rocksdb = fedimint_rocksdb::RocksDb::open(&options.database_dir)
161 .unwrap()
162 .into_database();
163 let mut dbtx = rocksdb.begin_transaction().await;
164 let prefix_iter = dbtx
165 .raw_find_by_prefix(prefix)
166 .await?
167 .collect::<Vec<_>>()
168 .await;
169 for (key, value) in prefix_iter {
170 print_kv(&key, &value);
171 }
172 dbtx.commit_tx().await;
173 }
174 DbCommand::Write { key, value } => {
175 let rocksdb = fedimint_rocksdb::RocksDb::open(&options.database_dir)
176 .unwrap()
177 .into_database();
178 let mut dbtx = rocksdb.begin_transaction().await;
179 dbtx.raw_insert_bytes(key, value)
180 .await
181 .expect("Error inserting entry into RocksDb");
182 dbtx.commit_tx().await;
183 }
184 DbCommand::Delete { key } => {
185 let rocksdb = fedimint_rocksdb::RocksDb::open(&options.database_dir)
186 .unwrap()
187 .into_database();
188 let mut dbtx = rocksdb.begin_transaction().await;
189 dbtx.raw_remove_entry(key)
190 .await
191 .expect("Error removing entry from RocksDb");
192 dbtx.commit_tx().await;
193 }
194 DbCommand::Dump {
195 cfg_dir,
196 modules,
197 prefixes,
198 password,
199 } => {
200 let modules = match modules {
201 Some(mods) => mods
202 .split(',')
203 .map(|s| s.to_string().to_lowercase())
204 .collect::<Vec<String>>(),
205 None => Vec::new(),
206 };
207
208 let prefix_names = match prefixes {
209 Some(db_prefixes) => db_prefixes
210 .split(',')
211 .map(|s| s.to_string().to_lowercase())
212 .collect::<Vec<String>>(),
213 None => Vec::new(),
214 };
215
216 let (module_inits, client_module_inits) = if options.no_modules {
217 (
218 ServerModuleInitRegistry::new(),
219 ClientModuleInitRegistry::new(),
220 )
221 } else {
222 (
223 self.server_module_inits.clone(),
224 self.client_module_inits.clone(),
225 )
226 };
227
228 let mut dbdump = DatabaseDump::new(
229 cfg_dir.clone(),
230 options.database_dir.clone(),
231 password.to_string(),
232 module_inits,
233 client_module_inits,
234 modules,
235 prefix_names,
236 )
237 .await?;
238 dbdump.dump_database().await?;
239 }
240 DbCommand::DeletePrefix { prefix } => {
241 let rocksdb = fedimint_rocksdb::RocksDb::open(&options.database_dir)
242 .unwrap()
243 .into_database();
244 let mut dbtx = rocksdb.begin_transaction().await;
245 dbtx.raw_remove_by_prefix(prefix).await?;
246 dbtx.commit_tx().await;
247 }
248 }
249
250 Ok(())
251 }
252}