Repository navigation
Add command to delete saved wallet config #312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -7,15 +7,18 @@ use std::collections::HashMap; | |||||
| feature = "cbf" | ||||||
| ))] | ||||||
| use crate::client::ClientType; | ||||||
| use crate::commands::WalletOpts; | ||||||
| use crate::commands::{WalletOpts, WalletsSubCommand}; | ||||||
| use crate::config::{WalletConfig, WalletConfigInner}; | ||||||
| use crate::error::BDKCliError as Error; | ||||||
| use crate::handlers::Init; | ||||||
| use crate::handlers::{AppCommand, AppContext}; | ||||||
| #[cfg(any(feature = "sqlite", feature = "redb"))] | ||||||
| use crate::persister::DatabaseType; | ||||||
| use crate::utils::descriptors::validate_descriptor_pair; | ||||||
| use crate::utils::output::FormatOutput; | ||||||
| use crate::utils::types::{StatusResult, WalletsListResult}; | ||||||
| #[cfg(feature = "redb")] | ||||||
| use bdk_redb::redb::TableHandle; | ||||||
| use bdk_wallet::bitcoin::Network; | ||||||
| use clap::Args; | ||||||
|
|
||||||
|
|
@@ -175,3 +178,215 @@ impl AppCommand<AppContext<Init>> for ListWalletsCommand { | |||||
| Ok(WalletsListResult(config.wallets)) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| fn wallet_data_path_exists(path: &std::path::Path) -> Result<bool, Error> { | ||||||
| match std::fs::symlink_metadata(path) { | ||||||
| Ok(_) => Ok(true), | ||||||
| Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), | ||||||
| Err(error) => Err(Error::Generic(format!( | ||||||
| "Failed to inspect wallet data at {path:?}: {error}" | ||||||
| ))), | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| #[cfg(any(feature = "sqlite", feature = "redb"))] | ||||||
| fn wallet_data_exists( | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fn should live in the |
||||||
| datadir: &std::path::Path, | ||||||
| wallet_name: &str, | ||||||
| database_type: &str, | ||||||
| ) -> Result<bool, Error> { | ||||||
| // Validate the persisted configuration before inspecting any data. In particular, an | ||||||
| // unknown database type must never turn a delete into a silent success. | ||||||
| match database_type { | ||||||
| // Both markers are valid persisted formats even when this binary only | ||||||
| // supports one of them; the opposite backend is checked conservatively below. | ||||||
| "sqlite" | "redb" => {} | ||||||
| _ => { | ||||||
| return Err(Error::Generic(format!( | ||||||
| "Unsupported database type: {database_type}" | ||||||
| ))); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // A configuration can be changed to another backend without moving the original data, so | ||||||
| // inspect every persistence backend marker rather than only the configured backend. | ||||||
| let sqlite_path = datadir.join(wallet_name).join("wallet.sqlite"); | ||||||
| let sqlite_data_exists = wallet_data_path_exists(&sqlite_path)?; | ||||||
|
|
||||||
| #[cfg(feature = "redb")] | ||||||
| let redb_data_exists = { | ||||||
| // Redb is shared by wallets, so only block deletion when this wallet has its sentinel | ||||||
| // table. | ||||||
| redb_wallet_data_exists(datadir, wallet_name)? | ||||||
| }; | ||||||
|
|
||||||
| #[cfg(not(feature = "redb"))] | ||||||
| let redb_data_exists = { | ||||||
| // Without Redb support, conservatively treat the shared marker as data because this | ||||||
| // build cannot inspect its wallet-specific tables. | ||||||
| wallet_data_path_exists(&datadir.join("wallet.redb"))? | ||||||
| }; | ||||||
|
|
||||||
| Ok(sqlite_data_exists || redb_data_exists) | ||||||
| } | ||||||
|
|
||||||
| #[cfg(feature = "redb")] | ||||||
| fn redb_inspection_error<E: std::fmt::Display>( | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should rather implement BDKCliError. So the right place should be in the Error module. |
||||||
| db_path: &std::path::Path, | ||||||
| wallet_name: &str, | ||||||
| error: E, | ||||||
| ) -> Error { | ||||||
| Error::Generic(format!( | ||||||
| "Failed to inspect shared Redb store while determining whether wallet '{wallet_name}' has persisted data at {db_path:?}: {error}" | ||||||
| )) | ||||||
| } | ||||||
|
|
||||||
| #[cfg(feature = "redb")] | ||||||
| fn redb_wallet_data_exists(datadir: &std::path::Path, wallet_name: &str) -> Result<bool, Error> { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same comment for |
||||||
| let db_path = datadir.join("wallet.redb"); | ||||||
| if !wallet_data_path_exists(&db_path)? { | ||||||
| return Ok(false); | ||||||
| } | ||||||
|
|
||||||
| let database = bdk_redb::redb::Database::open(&db_path) | ||||||
| .map_err(|error| redb_inspection_error(&db_path, wallet_name, error))?; | ||||||
|
|
||||||
| let read_transactions = database | ||||||
| .begin_read() | ||||||
| .map_err(|error| redb_inspection_error(&db_path, wallet_name, error))?; | ||||||
|
|
||||||
| let mut tables = read_transactions | ||||||
| .list_tables() | ||||||
| .map_err(|error| redb_inspection_error(&db_path, wallet_name, error))?; | ||||||
|
|
||||||
| // bdk_redb creates this per-wallet table in the first committed table batch for a | ||||||
| // persisted wallet, so it is the sentinel for an initialized wallet store. | ||||||
| let keychain_table_name = format!("{wallet_name}_keychain"); | ||||||
|
|
||||||
| Ok(tables.any(|table| table.name() == keychain_table_name.as_str())) | ||||||
| } | ||||||
|
|
||||||
| #[cfg(not(any(feature = "sqlite", feature = "redb")))] | ||||||
| fn wallet_data_exists(datadir: &std::path::Path, wallet_name: &str) -> Result<bool, Error> { | ||||||
| // The typed config intentionally omits `database_type` in this build, so inspect the raw | ||||||
| // wallet entry as well. A config carrying that field may have been created by a build with a | ||||||
| // database backend and must not be silently deleted just because its marker is absent. | ||||||
| let config_path = datadir.join("config.toml"); | ||||||
| let config_content = std::fs::read_to_string(&config_path) | ||||||
| .map_err(|error| Error::Generic(format!("Failed to read config file: {error}")))?; | ||||||
| let raw_config: toml::Table = toml::from_str(&config_content) | ||||||
| .map_err(|error| Error::Generic(format!("Failed to parse config file: {error}")))?; | ||||||
| let database_type_present = raw_config | ||||||
| .get("wallets") | ||||||
| .and_then(toml::Value::as_table) | ||||||
| .and_then(|wallets| wallets.get(wallet_name)) | ||||||
| .and_then(toml::Value::as_table) | ||||||
| .is_some_and(|wallet| wallet.contains_key("database_type")); | ||||||
|
|
||||||
| Ok(database_type_present | ||||||
| || wallet_data_path_exists(&datadir.join(wallet_name).join("wallet.sqlite"))? | ||||||
| || wallet_data_path_exists(&datadir.join("wallet.redb"))?) | ||||||
| } | ||||||
|
|
||||||
| fn remove_wallet_from_config_file( | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be moved to |
||||||
| datadir: &std::path::Path, | ||||||
| wallet_name: &str, | ||||||
| ) -> Result<(), Error> { | ||||||
| let config_path = datadir.join("config.toml"); | ||||||
| let config_content = std::fs::read_to_string(&config_path) | ||||||
| .map_err(|error| Error::Generic(format!("Failed to read config file: {error}")))?; | ||||||
| let mut raw_config: toml::Table = toml::from_str(&config_content) | ||||||
| .map_err(|error| Error::Generic(format!("Failed to parse config file: {error}")))?; | ||||||
| let wallets = raw_config | ||||||
| .get_mut("wallets") | ||||||
| .and_then(toml::Value::as_table_mut) | ||||||
| .ok_or_else(|| Error::Generic("Config does not contain a wallets table".into()))?; | ||||||
|
|
||||||
| if wallets.remove(wallet_name).is_none() { | ||||||
| return Err(Error::Generic(format!( | ||||||
| "Wallet '{wallet_name}' not found in config" | ||||||
| ))); | ||||||
| } | ||||||
|
|
||||||
| let updated_config = toml::to_string_pretty(&raw_config) | ||||||
| .map_err(|error| Error::Generic(format!("Failed to serialize config: {error}")))?; | ||||||
| std::fs::write(&config_path, updated_config) | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should use
Suggested change
|
||||||
| .map_err(|error| Error::Generic(format!("Failed to write config file: {error}"))) | ||||||
| } | ||||||
|
|
||||||
| #[derive(Args, Debug, Clone, PartialEq)] | ||||||
| pub struct DeleteWalletConfigCommand { | ||||||
| /// Name of the saved wallet configuration to delete. | ||||||
| #[arg(value_name = "WALLET_NAME")] | ||||||
| pub(crate) wallet_name: String, | ||||||
| } | ||||||
|
|
||||||
| impl AppCommand<AppContext<Init>> for DeleteWalletConfigCommand { | ||||||
| type Output = StatusResult; | ||||||
|
|
||||||
| fn execute(&self, ctx: &mut AppContext<Init>) -> Result<Self::Output, Error> { | ||||||
| let config = match WalletConfig::load(&ctx.datadir)? { | ||||||
| Some(config) => config, | ||||||
| None => return Err(Error::Generic("No wallets configured yet.".into())), | ||||||
| }; | ||||||
|
|
||||||
| #[cfg(any(feature = "sqlite", feature = "redb"))] | ||||||
| let data_exists = { | ||||||
| let wallet_config = config.wallets.get(&self.wallet_name).ok_or_else(|| { | ||||||
| Error::Generic(format!("Wallet '{}' not found in config", self.wallet_name)) | ||||||
| })?; | ||||||
|
|
||||||
| wallet_data_exists( | ||||||
| &ctx.datadir, | ||||||
| &self.wallet_name, | ||||||
| &wallet_config.database_type, | ||||||
| )? | ||||||
| }; | ||||||
|
|
||||||
| #[cfg(not(any(feature = "sqlite", feature = "redb")))] | ||||||
| let data_exists = { | ||||||
| if !config.wallets.contains_key(&self.wallet_name) { | ||||||
| return Err(Error::Generic(format!( | ||||||
| "Wallet '{}' not found in config", | ||||||
| self.wallet_name | ||||||
| ))); | ||||||
| } | ||||||
|
|
||||||
| wallet_data_exists(&ctx.datadir, &self.wallet_name)? | ||||||
| }; | ||||||
|
|
||||||
| if data_exists { | ||||||
| return Err(Error::Generic(format!( | ||||||
| "Wallet data exists for configuration '{}'; the saved configuration was not deleted", | ||||||
| self.wallet_name | ||||||
| ))); | ||||||
| } | ||||||
|
|
||||||
| if config.wallets.len() == 1 { | ||||||
| let config_path = ctx.datadir.join("config.toml"); | ||||||
| std::fs::remove_file(&config_path).map_err(|error| { | ||||||
| Error::Generic(format!( | ||||||
| "Failed to remove config at {config_path:?}: {error}" | ||||||
| )) | ||||||
| })?; | ||||||
| } else { | ||||||
| remove_wallet_from_config_file(&ctx.datadir, &self.wallet_name)?; | ||||||
| } | ||||||
|
|
||||||
| Ok(StatusResult { | ||||||
| message: format!( | ||||||
| "Wallet configuration '{}' deleted successfully", | ||||||
| self.wallet_name | ||||||
| ), | ||||||
| }) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl WalletsSubCommand { | ||||||
| pub fn execute(&self, ctx: &mut AppContext<Init>) -> Result<(), Error> { | ||||||
| match self { | ||||||
| Self::List(command) => command.execute(ctx)?.write_out(std::io::stdout()), | ||||||
| Self::Delete(command) => command.execute(ctx)?.write_out(std::io::stdout()), | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this fn should be moved to the
utilsdir and should be something likepath_existsas it may be useful outside of wallet data.