Skip to content

Commit 67011b3

Browse files
committed
Merge #206: feat: add redb as an alternative to sqlite
83e6dd6 refactor: add docs,version and change visibility (codingp110) 7bcbcf0 feat: add redb as an alternative to sqlite (codingp110) Pull request description: <!-- You can erase any parts of this template not applicable to your Pull Request. --> ### Description We currently only support `sqlite` persistence. This PR adds an alternative `redb` persistence by leveraging Summer Of Bitcoin work on [`bdk_redb`](https://github.com/110CodingP/bdk_redb). <!-- Describe the purpose of this PR, what's being adding and/or fixed --> ### Notes to the reviewers The CI does not pass since `bdk_redb` has a MSRV of 1.85.0 which is in turn due to `redb`. Also `bdk_redb` is yet to published as a crate so currently we use the GitHub version of the same. The following script tests a simple scenario of creating a wallet with bitcoind-rpc as chain source , sending funds to an address controlled by the wallet and creating a transaction using the wallet: ``` rm -rf ~/.bitcoin/regtest rm -rf ~/.bdk-bitcoin bitcoin-cli -rpcuser=alice -rpcpassword=password createwallet alice export NETWORK=regtest export EXT_DESCRIPTOR=$(bitcoin-cli -rpcwallet=alice -rpcuser=alice -rpcpassword=password listdescriptors true | jq -r '.descriptors | .[0] | .desc') export INT_DESCRIPTOR=$(bitcoin-cli -rpcwallet=alice -rpcuser=alice -rpcpassword=password listdescriptors true | jq -r '.descriptors | .[1] | .desc') export DATABASE_TYPE=redb export CLIENT_TYPE=rpc export SERVER_URL=127.0.0.1:18443 addr=$(bdk-cli wallet -w alice -a alice:password new_address | jq -r '.address') bitcoin-cli -rpcuser=alice -rpcpassword=password -rpcwallet=alice generatetoaddress 102 $addr bdk-cli wallet -w alice -a alice:password full_scan bdk-cli wallet -w alice -a alice:password balance recipient=$(bitcoin-cli -rpcwallet=alice getrawchangeaddress) psbt=$(bdk-cli wallet -w alice -a alice:password create_tx --to "$recipient:5000" | jq -r '.psbt') signed_psbt=$(bdk-cli wallet -w alice -a alice:password sign $psbt | jq -r '.psbt') bdk-cli wallet -w alice -a alice:password broadcast --psbt $signed_psbt unset $addr addr=$(bdk-cli wallet -w alice -a alice:password new_address | jq -r '.address') bitcoin-cli -rpcuser=alice -rpcpassword=password -rpcwallet=alice generatetoaddress 1 $addr bdk-cli wallet -w alice -a alice:password sync bdk-cli wallet -w alice -a alice:password balance ``` the conf is as follows: ``` regtest=1 [regtest] server=1 rpcuser=alice rpcpassword=password ``` ~~Also removed `sqlite` from default features since we now have an alternative.~~ <!-- In this section you can include notes directed to the reviewers, like explaining why some parts of the PR were done in a specific way --> ## Changelog notice <!-- Notice the release manager should include in the release tag message changelog --> <!-- See https://keepachangelog.com/en/1.0.0/ for examples --> ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk-cli/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature (test is in the PR description) * [x] I've added docs for the new feature * [x] I've updated `CHANGELOG.md` ACKs for top commit: notmandatory: utACK 83e6dd6 Tree-SHA512: 50481b0cc293437145c5b1c3671e21b4a2bed32733e86a05a3404e13b70c77bd04754c53411656c7e84a4fc6efcf92659949f2c02f2135fabb0612b010ef4861
2 parents a46aa9d + 83e6dd6 commit 67011b3

File tree

8 files changed

+188
-23
lines changed

8 files changed

+188
-23
lines changed

Cargo.lock

Lines changed: 69 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ bdk_bitcoind_rpc = { version = "0.20.0", optional = true }
2626
bdk_electrum = { version = "0.23.0", optional = true }
2727
bdk_esplora = { version = "0.22.0", features = ["async-https", "tokio"], optional = true }
2828
bdk_kyoto = { version = "0.11.0", optional = true }
29+
bdk_redb = { version = "0.1.0", optional = true }
2930
shlex = { version = "1.3.0", optional = true }
3031
tracing = "0.1.41"
3132
tracing-subscriber = "0.3.19"
@@ -38,12 +39,13 @@ repl = ["shlex"]
3839

3940
# Available database options
4041
sqlite = ["bdk_wallet/rusqlite"]
42+
redb = ["bdk_redb"]
4143

4244
# Available blockchain client options
4345
cbf = ["bdk_kyoto"]
4446
electrum = ["bdk_electrum"]
4547
esplora = ["bdk_esplora"]
46-
rpc = ["bdk_bitcoind_rpc"]
48+
rpc = ["bdk_bitcoind_rpc"]
4749

4850
# Use this to consensus verify transactions at sync time
4951
verify = []

src/commands.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ pub enum DatabaseType {
126126
/// Sqlite database
127127
#[cfg(feature = "sqlite")]
128128
Sqlite,
129+
/// Redb database
130+
#[cfg(feature = "redb")]
131+
Redb,
129132
}
130133

131134
#[cfg(any(
@@ -169,7 +172,7 @@ pub struct WalletOpts {
169172
))]
170173
#[arg(env = "CLIENT_TYPE", short = 'c', long, value_enum, required = true)]
171174
pub client_type: ClientType,
172-
#[cfg(feature = "sqlite")]
175+
#[cfg(any(feature = "sqlite", feature = "redb"))]
173176
#[arg(env = "DATABASE_TYPE", short = 'd', long, value_enum, required = true)]
174177
pub database_type: DatabaseType,
175178
/// Sets the server url.

src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,18 @@ pub enum BDKCliError {
5757
#[error("PsbtError: {0}")]
5858
PsbtError(#[from] bdk_wallet::bitcoin::psbt::Error),
5959

60+
#[cfg(feature = "sqlite")]
6061
#[error("Rusqlite error: {0}")]
6162
RusqliteError(#[from] bdk_wallet::rusqlite::Error),
6263

64+
#[cfg(feature = "redb")]
65+
#[error("Redb StoreError: {0}")]
66+
RedbStoreError(#[from] bdk_redb::error::StoreError),
67+
68+
#[cfg(feature = "redb")]
69+
#[error("Redb dabtabase error: {0}")]
70+
RedbDatabaseError(#[from] bdk_redb::redb::DatabaseError),
71+
6372
#[error("Serde json error: {0}")]
6473
SerdeJson(#[from] serde_json::Error),
6574

src/handlers.rs

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313
use crate::commands::OfflineWalletSubCommand::*;
1414
use crate::commands::*;
1515
use crate::error::BDKCliError as Error;
16+
#[cfg(any(feature = "sqlite", feature = "redb"))]
17+
use crate::persister::Persister;
1618
#[cfg(feature = "cbf")]
1719
use crate::utils::BlockchainClient::KyotoClient;
1820
use crate::utils::*;
21+
#[cfg(feature = "redb")]
22+
use bdk_redb::Store as RedbStore;
1923
use bdk_wallet::bip39::{Language, Mnemonic};
2024
use bdk_wallet::bitcoin::Network;
2125
use bdk_wallet::bitcoin::bip32::{DerivationPath, KeySource};
@@ -51,6 +55,8 @@ use crate::utils::BlockchainClient::Electrum;
5155
#[cfg(feature = "cbf")]
5256
use bdk_kyoto::{Info, LightClient};
5357
use bdk_wallet::bitcoin::base64::prelude::*;
58+
#[cfg(feature = "redb")]
59+
use std::sync::Arc;
5460
#[cfg(feature = "cbf")]
5561
use tokio::select;
5662
#[cfg(any(
@@ -751,15 +757,28 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
751757
let home_dir = prepare_home_dir(cli_opts.datadir)?;
752758
let wallet_name = &wallet_opts.wallet;
753759
let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?;
754-
#[cfg(feature = "sqlite")]
760+
761+
#[cfg(any(feature = "sqlite", feature = "redb"))]
755762
let result = {
756-
let mut persister = match &wallet_opts.database_type {
763+
let mut persister: Persister = match &wallet_opts.database_type {
757764
#[cfg(feature = "sqlite")]
758765
DatabaseType::Sqlite => {
759766
let db_file = database_path.join("wallet.sqlite");
760767
let connection = Connection::open(db_file)?;
761768
log::debug!("Sqlite database opened successfully");
762-
connection
769+
Persister::Connection(connection)
770+
}
771+
#[cfg(feature = "redb")]
772+
DatabaseType::Redb => {
773+
let db = Arc::new(bdk_redb::redb::Database::create(
774+
home_dir.join("wallet.redb"),
775+
)?);
776+
let store = RedbStore::new(
777+
db,
778+
wallet_name.as_deref().unwrap_or("wallet").to_string(),
779+
)?;
780+
log::debug!("Redb database opened successfully");
781+
Persister::RedbStore(store)
763782
}
764783
};
765784

@@ -776,7 +795,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
776795
wallet.persist(&mut persister)?;
777796
result
778797
};
779-
#[cfg(not(any(feature = "sqlite")))]
798+
#[cfg(not(any(feature = "sqlite", feature = "redb")))]
780799
let result = {
781800
let wallet = new_wallet(network, &wallet_opts)?;
782801
let blockchain_client =
@@ -792,18 +811,30 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
792811
subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
793812
} => {
794813
let network = cli_opts.network;
795-
#[cfg(feature = "sqlite")]
814+
#[cfg(any(feature = "sqlite", feature = "redb"))]
796815
let result = {
797816
let home_dir = prepare_home_dir(cli_opts.datadir)?;
798817
let wallet_name = &wallet_opts.wallet;
799-
let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?;
800-
let mut persister = match &wallet_opts.database_type {
818+
let mut persister: Persister = match &wallet_opts.database_type {
801819
#[cfg(feature = "sqlite")]
802820
DatabaseType::Sqlite => {
821+
let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?;
803822
let db_file = database_path.join("wallet.sqlite");
804823
let connection = Connection::open(db_file)?;
805824
log::debug!("Sqlite database opened successfully");
806-
connection
825+
Persister::Connection(connection)
826+
}
827+
#[cfg(feature = "redb")]
828+
DatabaseType::Redb => {
829+
let db = Arc::new(bdk_redb::redb::Database::create(
830+
home_dir.join("wallet.redb"),
831+
)?);
832+
let store = RedbStore::new(
833+
db,
834+
wallet_name.as_deref().unwrap_or("wallet").to_string(),
835+
)?;
836+
log::debug!("Redb database opened successfully");
837+
Persister::RedbStore(store)
807838
}
808839
};
809840

@@ -816,7 +847,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
816847
wallet.persist(&mut persister)?;
817848
result
818849
};
819-
#[cfg(not(any(feature = "sqlite")))]
850+
#[cfg(not(any(feature = "sqlite", feature = "redb")))]
820851
let result = {
821852
let mut wallet = new_wallet(network, &wallet_opts)?;
822853
handle_offline_wallet_subcommand(&mut wallet, &wallet_opts, offline_subcommand)?
@@ -840,27 +871,38 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
840871
#[cfg(feature = "repl")]
841872
CliSubCommand::Repl { wallet_opts } => {
842873
let network = cli_opts.network;
843-
#[cfg(feature = "sqlite")]
874+
#[cfg(any(feature = "sqlite", feature = "redb"))]
844875
let (mut wallet, mut persister) = {
845876
let wallet_name = &wallet_opts.wallet;
846877

847878
let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
848879

849-
let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?;
850-
851-
let mut persister = match &wallet_opts.database_type {
880+
let mut persister: Persister = match &wallet_opts.database_type {
852881
#[cfg(feature = "sqlite")]
853882
DatabaseType::Sqlite => {
883+
let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?;
854884
let db_file = database_path.join("wallet.sqlite");
855885
let connection = Connection::open(db_file)?;
856886
log::debug!("Sqlite database opened successfully");
857-
connection
887+
Persister::Connection(connection)
888+
}
889+
#[cfg(feature = "redb")]
890+
DatabaseType::Redb => {
891+
let db = Arc::new(bdk_redb::redb::Database::create(
892+
home_dir.join("wallet.redb"),
893+
)?);
894+
let store = RedbStore::new(
895+
db,
896+
wallet_name.as_deref().unwrap_or("wallet").to_string(),
897+
)?;
898+
log::debug!("Redb database opened successfully");
899+
Persister::RedbStore(store)
858900
}
859901
};
860902
let wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
861903
(wallet, persister)
862904
};
863-
#[cfg(not(any(feature = "sqlite")))]
905+
#[cfg(not(any(feature = "sqlite", feature = "redb")))]
864906
let mut wallet = new_wallet(network, &wallet_opts)?;
865907
let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
866908
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<String, Error> {
880922
database_path.clone(),
881923
)
882924
.await;
883-
#[cfg(feature = "sqlite")]
925+
#[cfg(any(feature = "sqlite", feature = "redb"))]
884926
wallet.persist(&mut persister)?;
885927

886928
match result {

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
mod commands;
1414
mod error;
1515
mod handlers;
16+
#[cfg(any(feature = "sqlite", feature = "redb"))]
17+
mod persister;
1618
mod utils;
1719

1820
use bdk_wallet::bitcoin::Network;

src/persister.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use crate::error::BDKCliError;
2+
use bdk_wallet::WalletPersister;
3+
4+
// Types of Persistence backends supported by bdk-cli
5+
pub(crate) enum Persister {
6+
#[cfg(feature = "sqlite")]
7+
Connection(bdk_wallet::rusqlite::Connection),
8+
#[cfg(feature = "redb")]
9+
RedbStore(bdk_redb::Store),
10+
}
11+
12+
impl WalletPersister for Persister {
13+
type Error = BDKCliError;
14+
15+
fn initialize(persister: &mut Self) -> Result<bdk_wallet::ChangeSet, Self::Error> {
16+
match persister {
17+
#[cfg(feature = "sqlite")]
18+
Persister::Connection(connection) => {
19+
WalletPersister::initialize(connection).map_err(BDKCliError::from)
20+
}
21+
#[cfg(feature = "redb")]
22+
Persister::RedbStore(store) => {
23+
WalletPersister::initialize(store).map_err(BDKCliError::from)
24+
}
25+
}
26+
}
27+
28+
fn persist(persister: &mut Self, changeset: &bdk_wallet::ChangeSet) -> Result<(), Self::Error> {
29+
match persister {
30+
#[cfg(feature = "sqlite")]
31+
Persister::Connection(connection) => {
32+
WalletPersister::persist(connection, changeset).map_err(BDKCliError::from)
33+
}
34+
#[cfg(feature = "redb")]
35+
Persister::RedbStore(store) => {
36+
WalletPersister::persist(store, changeset).map_err(BDKCliError::from)
37+
}
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)