From 7bcbcf05d38f95f8a2b047fdd57eb320a065339e Mon Sep 17 00:00:00 2001 From: codingp110 Date: Mon, 14 Jul 2025 22:54:11 +0530 Subject: [PATCH 1/2] feat: add redb as an alternative to sqlite --- Cargo.lock | 70 +++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 4 ++- src/commands.rs | 5 +++- src/error.rs | 9 ++++++ src/handlers.rs | 74 +++++++++++++++++++++++++++++++++++++----------- src/main.rs | 2 ++ src/persister.rs | 39 +++++++++++++++++++++++++ src/utils.rs | 6 ++-- 8 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 src/persister.rs diff --git a/Cargo.lock b/Cargo.lock index 365b16c6..6daa9c64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -185,6 +185,7 @@ dependencies = [ "bdk_electrum", "bdk_esplora", "bdk_kyoto", + "bdk_redb", "bdk_wallet", "clap", "dirs", @@ -265,6 +266,19 @@ dependencies = [ "kyoto-cbf", ] +[[package]] +name = "bdk_redb" +version = "0.1.0" +source = "git+https://github.com/110CodingP/bdk_redb#9985249c3f22489ccaf4ae09138fae950eca774b" +dependencies = [ + "bdk_chain", + "bdk_wallet", + "ciborium", + "redb", + "tempfile", + "thiserror", +] + [[package]] name = "bdk_wallet" version = "2.1.0" @@ -509,6 +523,33 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b4b0fc281743d80256607bd65e8beedc42cb0787ea119c85b81b4c0eab85e5f" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -591,6 +632,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "dirs" version = "6.0.0" @@ -878,6 +925,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1663,6 +1720,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "redb" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef6a6d3a65ea334d6cdfb31fa2525c20184b7aa7bd1ad1e2e37502610d4609f" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -1798,7 +1864,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c8695318..a72b7af3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ bdk_bitcoind_rpc = { version = "0.20.0", optional = true } bdk_electrum = { version = "0.23.0", optional = true } bdk_esplora = { version = "0.22.0", features = ["async-https", "tokio"], optional = true } bdk_kyoto = { version = "0.11.0", optional = true } +bdk_redb = { git = "https://github.com/110CodingP/bdk_redb", optional = true } shlex = { version = "1.3.0", optional = true } tracing = "0.1.41" tracing-subscriber = "0.3.19" @@ -38,12 +39,13 @@ repl = ["shlex"] # Available database options sqlite = ["bdk_wallet/rusqlite"] +redb = ["bdk_redb"] # Available blockchain client options cbf = ["bdk_kyoto"] electrum = ["bdk_electrum"] esplora = ["bdk_esplora"] -rpc = ["bdk_bitcoind_rpc"] +rpc = ["bdk_bitcoind_rpc"] # Use this to consensus verify transactions at sync time verify = [] diff --git a/src/commands.rs b/src/commands.rs index dbc27b68..372b0340 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -126,6 +126,9 @@ pub enum DatabaseType { /// Sqlite database #[cfg(feature = "sqlite")] Sqlite, + /// Redb database + #[cfg(feature = "redb")] + Redb, } #[cfg(any( @@ -169,7 +172,7 @@ pub struct WalletOpts { ))] #[arg(env = "CLIENT_TYPE", short = 'c', long, value_enum, required = true)] pub client_type: ClientType, - #[cfg(feature = "sqlite")] + #[cfg(any(feature = "sqlite", feature = "redb"))] #[arg(env = "DATABASE_TYPE", short = 'd', long, value_enum, required = true)] pub database_type: DatabaseType, /// Sets the server url. diff --git a/src/error.rs b/src/error.rs index cb757156..5f548d91 100644 --- a/src/error.rs +++ b/src/error.rs @@ -57,9 +57,18 @@ pub enum BDKCliError { #[error("PsbtError: {0}")] PsbtError(#[from] bdk_wallet::bitcoin::psbt::Error), + #[cfg(feature = "sqlite")] #[error("Rusqlite error: {0}")] RusqliteError(#[from] bdk_wallet::rusqlite::Error), + #[cfg(feature = "redb")] + #[error("Redb StoreError: {0}")] + RedbStoreError(#[from] bdk_redb::error::StoreError), + + #[cfg(feature = "redb")] + #[error("Redb dabtabase error: {0}")] + RedbDatabaseError(#[from] bdk_redb::redb::DatabaseError), + #[error("Serde json error: {0}")] SerdeJson(#[from] serde_json::Error), diff --git a/src/handlers.rs b/src/handlers.rs index 8be2e570..71ca5b42 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -13,9 +13,13 @@ use crate::commands::OfflineWalletSubCommand::*; use crate::commands::*; use crate::error::BDKCliError as Error; +#[cfg(any(feature = "sqlite", feature = "redb"))] +use crate::persister::Persister; #[cfg(feature = "cbf")] use crate::utils::BlockchainClient::KyotoClient; use crate::utils::*; +#[cfg(feature = "redb")] +use bdk_redb::Store as RedbStore; use bdk_wallet::bip39::{Language, Mnemonic}; use bdk_wallet::bitcoin::Network; use bdk_wallet::bitcoin::bip32::{DerivationPath, KeySource}; @@ -51,6 +55,8 @@ use crate::utils::BlockchainClient::Electrum; #[cfg(feature = "cbf")] use bdk_kyoto::{Info, LightClient}; use bdk_wallet::bitcoin::base64::prelude::*; +#[cfg(feature = "redb")] +use std::sync::Arc; #[cfg(feature = "cbf")] use tokio::select; #[cfg(any( @@ -751,15 +757,28 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { let home_dir = prepare_home_dir(cli_opts.datadir)?; let wallet_name = &wallet_opts.wallet; let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?; - #[cfg(feature = "sqlite")] + + #[cfg(any(feature = "sqlite", feature = "redb"))] let result = { - let mut persister = match &wallet_opts.database_type { + let mut persister: Persister = match &wallet_opts.database_type { #[cfg(feature = "sqlite")] DatabaseType::Sqlite => { let db_file = database_path.join("wallet.sqlite"); let connection = Connection::open(db_file)?; log::debug!("Sqlite database opened successfully"); - connection + Persister::Connection(connection) + } + #[cfg(feature = "redb")] + DatabaseType::Redb => { + let db = Arc::new(bdk_redb::redb::Database::create( + home_dir.join("wallet.redb"), + )?); + let store = RedbStore::new( + db, + wallet_name.as_deref().unwrap_or("wallet").to_string(), + )?; + log::debug!("Redb database opened successfully"); + Persister::RedbStore(store) } }; @@ -776,7 +795,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { wallet.persist(&mut persister)?; result }; - #[cfg(not(any(feature = "sqlite")))] + #[cfg(not(any(feature = "sqlite", feature = "redb")))] let result = { let wallet = new_wallet(network, &wallet_opts)?; let blockchain_client = @@ -792,18 +811,30 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), } => { let network = cli_opts.network; - #[cfg(feature = "sqlite")] + #[cfg(any(feature = "sqlite", feature = "redb"))] let result = { let home_dir = prepare_home_dir(cli_opts.datadir)?; let wallet_name = &wallet_opts.wallet; - let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?; - let mut persister = match &wallet_opts.database_type { + let mut persister: Persister = match &wallet_opts.database_type { #[cfg(feature = "sqlite")] DatabaseType::Sqlite => { + let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?; let db_file = database_path.join("wallet.sqlite"); let connection = Connection::open(db_file)?; log::debug!("Sqlite database opened successfully"); - connection + Persister::Connection(connection) + } + #[cfg(feature = "redb")] + DatabaseType::Redb => { + let db = Arc::new(bdk_redb::redb::Database::create( + home_dir.join("wallet.redb"), + )?); + let store = RedbStore::new( + db, + wallet_name.as_deref().unwrap_or("wallet").to_string(), + )?; + log::debug!("Redb database opened successfully"); + Persister::RedbStore(store) } }; @@ -816,7 +847,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { wallet.persist(&mut persister)?; result }; - #[cfg(not(any(feature = "sqlite")))] + #[cfg(not(any(feature = "sqlite", feature = "redb")))] let result = { let mut wallet = new_wallet(network, &wallet_opts)?; handle_offline_wallet_subcommand(&mut wallet, &wallet_opts, offline_subcommand)? @@ -840,27 +871,38 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { #[cfg(feature = "repl")] CliSubCommand::Repl { wallet_opts } => { let network = cli_opts.network; - #[cfg(feature = "sqlite")] + #[cfg(any(feature = "sqlite", feature = "redb"))] let (mut wallet, mut persister) = { let wallet_name = &wallet_opts.wallet; let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; - let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?; - - let mut persister = match &wallet_opts.database_type { + let mut persister: Persister = match &wallet_opts.database_type { #[cfg(feature = "sqlite")] DatabaseType::Sqlite => { + let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?; let db_file = database_path.join("wallet.sqlite"); let connection = Connection::open(db_file)?; log::debug!("Sqlite database opened successfully"); - connection + Persister::Connection(connection) + } + #[cfg(feature = "redb")] + DatabaseType::Redb => { + let db = Arc::new(bdk_redb::redb::Database::create( + home_dir.join("wallet.redb"), + )?); + let store = RedbStore::new( + db, + wallet_name.as_deref().unwrap_or("wallet").to_string(), + )?; + log::debug!("Redb database opened successfully"); + Persister::RedbStore(store) } }; let wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; (wallet, persister) }; - #[cfg(not(any(feature = "sqlite")))] + #[cfg(not(any(feature = "sqlite", feature = "redb")))] let mut wallet = new_wallet(network, &wallet_opts)?; let home_dir = prepare_home_dir(cli_opts.datadir.clone())?; let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?; @@ -880,7 +922,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { database_path.clone(), ) .await; - #[cfg(feature = "sqlite")] + #[cfg(any(feature = "sqlite", feature = "redb"))] wallet.persist(&mut persister)?; match result { diff --git a/src/main.rs b/src/main.rs index 6b3db95e..c69aecc7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,8 @@ mod commands; mod error; mod handlers; +#[cfg(any(feature = "sqlite", feature = "redb"))] +mod persister; mod utils; use bdk_wallet::bitcoin::Network; diff --git a/src/persister.rs b/src/persister.rs new file mode 100644 index 00000000..450af5c5 --- /dev/null +++ b/src/persister.rs @@ -0,0 +1,39 @@ +use crate::error::BDKCliError; +use bdk_wallet::WalletPersister; + +pub enum Persister { + #[cfg(feature = "sqlite")] + Connection(bdk_wallet::rusqlite::Connection), + #[cfg(feature = "redb")] + RedbStore(bdk_redb::Store), +} + +impl WalletPersister for Persister { + type Error = BDKCliError; + + fn initialize(persister: &mut Self) -> Result { + match persister { + #[cfg(feature = "sqlite")] + Persister::Connection(connection) => { + WalletPersister::initialize(connection).map_err(BDKCliError::from) + } + #[cfg(feature = "redb")] + Persister::RedbStore(store) => { + WalletPersister::initialize(store).map_err(BDKCliError::from) + } + } + } + + fn persist(persister: &mut Self, changeset: &bdk_wallet::ChangeSet) -> Result<(), Self::Error> { + match persister { + #[cfg(feature = "sqlite")] + Persister::Connection(connection) => { + WalletPersister::persist(connection, changeset).map_err(BDKCliError::from) + } + #[cfg(feature = "redb")] + Persister::RedbStore(store) => { + WalletPersister::persist(store, changeset).map_err(BDKCliError::from) + } + } + } +} diff --git a/src/utils.rs b/src/utils.rs index 7acf8521..de05c665 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -33,7 +33,7 @@ use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; use crate::commands::ClientType; use bdk_wallet::Wallet; -#[cfg(feature = "sqlite")] +#[cfg(any(feature = "sqlite", feature = "redb"))] use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; /// Parse the recipient (Address,Amount) argument from cli input. @@ -214,7 +214,7 @@ pub(crate) fn new_blockchain_client( Ok(client) } -#[cfg(feature = "sqlite")] +#[cfg(any(feature = "sqlite", feature = "redb"))] /// Create a new persisted wallet from given wallet configuration options. pub(crate) fn new_persisted_wallet( network: Network, @@ -271,7 +271,7 @@ where Ok(wallet) } -#[cfg(not(any(feature = "sqlite",)))] +#[cfg(not(any(feature = "sqlite", feature = "redb")))] /// Create a new non-persisted wallet from given wallet configuration options. pub(crate) fn new_wallet(network: Network, wallet_opts: &WalletOpts) -> Result { let ext_descriptor = wallet_opts.ext_descriptor.clone(); From 83e6dd6e9ccd9c9cb5414f02da505a4e4810b41d Mon Sep 17 00:00:00 2001 From: codingp110 Date: Tue, 15 Jul 2025 12:51:18 +0530 Subject: [PATCH 2/2] refactor: add docs,version and change visibility Since the user does not need to know about how commands are being handled. --- Cargo.lock | 3 ++- Cargo.toml | 2 +- src/persister.rs | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6daa9c64..58cb3f0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,7 +269,8 @@ dependencies = [ [[package]] name = "bdk_redb" version = "0.1.0" -source = "git+https://github.com/110CodingP/bdk_redb#9985249c3f22489ccaf4ae09138fae950eca774b" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f69a501c17da6d90243c112a6ad07955eadd5cd321da663283040b29e724c70d" dependencies = [ "bdk_chain", "bdk_wallet", diff --git a/Cargo.toml b/Cargo.toml index a72b7af3..06d9f665 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ bdk_bitcoind_rpc = { version = "0.20.0", optional = true } bdk_electrum = { version = "0.23.0", optional = true } bdk_esplora = { version = "0.22.0", features = ["async-https", "tokio"], optional = true } bdk_kyoto = { version = "0.11.0", optional = true } -bdk_redb = { git = "https://github.com/110CodingP/bdk_redb", optional = true } +bdk_redb = { version = "0.1.0", optional = true } shlex = { version = "1.3.0", optional = true } tracing = "0.1.41" tracing-subscriber = "0.3.19" diff --git a/src/persister.rs b/src/persister.rs index 450af5c5..1f9a742c 100644 --- a/src/persister.rs +++ b/src/persister.rs @@ -1,7 +1,8 @@ use crate::error::BDKCliError; use bdk_wallet::WalletPersister; -pub enum Persister { +// Types of Persistence backends supported by bdk-cli +pub(crate) enum Persister { #[cfg(feature = "sqlite")] Connection(bdk_wallet::rusqlite::Connection), #[cfg(feature = "redb")]