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 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 {
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 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}