From 656854822630edac2bf2b5100fa97bbea4a548c1 Mon Sep 17 00:00:00 2001 From: Markus Levonyak Date: Thu, 25 Jun 2026 16:44:58 +0000 Subject: [PATCH] feat: use bdk_sqlite for BDK wallet persistence --- Cargo.lock | 23 ++++++-------------- Cargo.toml | 4 +++- src/error.rs | 42 +++++++++++------------------------- src/lib.rs | 4 ++-- src/wallet/core.rs | 36 +++++++++++++++---------------- src/wallet/indexer.rs | 4 +--- src/wallet/multisig.rs | 4 ++-- src/wallet/offline.rs | 2 +- src/wallet/online.rs | 2 +- src/wallet/test/go_online.rs | 4 ++-- 10 files changed, 48 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3810b4f8..dc50f87b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,14 +383,14 @@ dependencies = [ ] [[package]] -name = "bdk_file_store" -version = "0.22.0" +name = "bdk_sqlite" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b9a98edb58d02b601239832674a9ca9f1465e08e1dd1fac3a0e34004372406" +checksum = "8f2f05ffd2ec7af55c049fc4f2b5d55fdfc51f0e04a0f445f08409b0a3f4efcd" dependencies = [ - "bdk_core", - "bincode", - "serde", + "bdk_chain", + "bdk_wallet", + "sqlx", ] [[package]] @@ -400,7 +400,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67f3c4f9526d22374fca5b7ff1d6bf8d921ab56db2dac8df66a2c5561b31d4ef" dependencies = [ "bdk_chain", - "bdk_file_store", "bip39", "bitcoin", "miniscript", @@ -429,15 +428,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bip39" version = "2.2.2" @@ -3162,6 +3152,7 @@ dependencies = [ "base64 0.22.1", "bdk_electrum", "bdk_esplora", + "bdk_sqlite", "bdk_wallet", "biscuit-auth", "chacha20poly1305", diff --git a/Cargo.toml b/Cargo.toml index ec086555..791572e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,10 @@ members = [".", "migration"] base64 = { version = "0.22.1", default-features = false, features = [ "std", ] } +bdk_sqlite = { version = "0.6.0", default-features = false, features = [ + "wallet", +] } bdk_wallet = { version = "=3.0.0", default-features = false, features = [ - "file_store", "keys-bip39", "std", ] } diff --git a/src/error.rs b/src/error.rs index 494e43ed..fa297044 100644 --- a/src/error.rs +++ b/src/error.rs @@ -856,32 +856,24 @@ impl From for Error { } } -impl From> for Error { - fn from(e: bdk_wallet::file_store::StoreErrorWithDump) -> Self { +impl From for Error { + fn from(e: bdk_sqlite::Error) -> Self { Error::IO { details: e.to_string(), } } } -impl From for Error { - fn from(e: bdk_wallet::FileStoreError) -> Self { +impl From> for Error { + fn from(e: bdk_wallet::CreateWithPersistError) -> Self { Error::IO { details: e.to_string(), } } } -impl From> for Error { - fn from(e: bdk_wallet::CreateWithPersistError) -> Self { - Error::IO { - details: e.to_string(), - } - } -} - -impl From> for Error { - fn from(e: bdk_wallet::LoadWithPersistError) -> Self { +impl From> for Error { + fn from(e: bdk_wallet::LoadWithPersistError) -> Self { match e { bdk_wallet::LoadWithPersistError::InvalidChangeSet( bdk_wallet::LoadError::Mismatch(bdk_wallet::LoadMismatch::Genesis { .. }), @@ -959,31 +951,21 @@ mod tests { assert_matches!(err, Error::Internal { details } if !details.is_empty()); // LoadWithPersistError error - let err = bdk_wallet::LoadWithPersistError::Persist(bdk_wallet::FileStoreError::Write( - std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no access"), + let err = bdk_wallet::LoadWithPersistError::Persist(bdk_sqlite::Error::FromInt( + u8::try_from(300u32).unwrap_err(), )); let err = Error::from(err); assert_matches!(err, Error::IO { details } if !details.is_empty()); // CreateWithPersistError error - let err = bdk_wallet::CreateWithPersistError::Persist(bdk_wallet::FileStoreError::Write( - std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no access"), + let err = bdk_wallet::CreateWithPersistError::Persist(bdk_sqlite::Error::FromInt( + u8::try_from(300u32).unwrap_err(), )); let err = Error::from(err); assert_matches!(err, Error::IO { details } if !details.is_empty()); - // FileStoreError error - let err = bdk_wallet::FileStoreError::Write(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "no access", - )); - let err = Error::from(err); - assert_matches!(err, Error::IO { details } if !details.is_empty()); - - // StoreErrorWithDump error - let err = bdk_wallet::file_store::StoreErrorWithDump::::from( - std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no access"), - ); + // bdk_sqlite::Error error + let err = bdk_sqlite::Error::FromInt(u8::try_from(300u32).unwrap_err()); let err = Error::from(err); assert_matches!(err, Error::IO { details } if !details.is_empty()); diff --git a/src/lib.rs b/src/lib.rs index 802a9a00..5079ae28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,10 +126,11 @@ use bdk_esplora::{ BlockingClient as EsploraClient, Builder as EsploraBuilder, Error as EsploraError, }, }; +use bdk_sqlite::Store as BdkStore; #[cfg(feature = "esplora")] use bdk_wallet::bitcoin::Txid; use bdk_wallet::{ - ChangeSet, KeychainKind, LocalOutput, PersistedWallet, SignOptions, Wallet as BdkWallet, + KeychainKind, LocalOutput, PersistedWallet, SignOptions, Wallet as BdkWallet, bitcoin::{ Address as BdkAddress, Amount as BdkAmount, BlockHash, Network as BdkNetwork, NetworkKind, OutPoint, OutPoint as BdkOutPoint, ScriptBuf, TxOut, @@ -140,7 +141,6 @@ use bdk_wallet::{ }, chain::{CanonicalizationParams, ChainPosition}, descriptor::Segwitv0, - file_store::Store, keys::{ DerivableKey, DescriptorKey, DescriptorKey::{Public, Secret}, diff --git a/src/wallet/core.rs b/src/wallet/core.rs index 7148df52..2cdd61c6 100644 --- a/src/wallet/core.rs +++ b/src/wallet/core.rs @@ -4,7 +4,7 @@ use super::*; -const BDK_DB_NAME: &str = "bdk_db"; +const BDK_DB_NAME: &str = "bdk_db_sqlite"; pub(crate) const NUM_KNOWN_SCHEMAS: usize = 4; @@ -67,8 +67,8 @@ pub struct WalletInternals { pub(crate) _logger_guard: AsyncGuard, pub(crate) database: Arc, pub(crate) wallet_dir: PathBuf, - pub(crate) bdk_wallet: PersistedWallet>, - pub(crate) bdk_database: Store, + pub(crate) bdk_wallet: PersistedWallet, + pub(crate) bdk_database: BdkStore, #[cfg(any(feature = "electrum", feature = "esplora"))] pub(crate) online_data: Option, } @@ -120,7 +120,7 @@ pub(crate) fn setup_bdk>( desc_vanilla: String, watch_only: bool, bdk_network: BdkNetwork, -) -> Result<(PersistedWallet>, Store), Error> { +) -> Result<(PersistedWallet, BdkStore), Error> { let chain_net: ChainNet = wallet_data.bitcoin_network.into(); let mut wallet_params = BdkWallet::load() .descriptor(KeychainKind::External, Some(desc_colored.clone())) @@ -135,13 +135,16 @@ pub(crate) fn setup_bdk>( BDK_DB_NAME.to_string() }; let bdk_db_path = wallet_dir.as_ref().join(bdk_db_name); - let (mut bdk_database, _) = - Store::::load_or_create(BDK_DB_NAME.as_bytes(), bdk_db_path)?; - let bdk_wallet = match wallet_params.load_wallet(&mut bdk_database)? { + let bdk_db_url = format!("sqlite:{}", adjust_canonicalization(bdk_db_path)); + let mut bdk_database = block_on(BdkStore::new(&bdk_db_url))?; + // schema migrations are applied automatically when the wallet is loaded + let bdk_wallet = match block_on(wallet_params.load_wallet_async(&mut bdk_database))? { Some(wallet) => wallet, - None => BdkWallet::create(desc_colored, desc_vanilla) - .network(bdk_network) - .create_wallet(&mut bdk_database)?, + None => block_on( + BdkWallet::create(desc_colored, desc_vanilla) + .network(bdk_network) + .create_wallet_async(&mut bdk_database), + )?, }; Ok((bdk_wallet, bdk_database)) } @@ -180,20 +183,15 @@ pub trait WalletCore { fn internals_mut(&mut self) -> &mut WalletInternals; - fn bdk_wallet(&self) -> &PersistedWallet> { + fn bdk_wallet(&self) -> &PersistedWallet { &self.internals().bdk_wallet } - fn bdk_wallet_mut(&mut self) -> &mut PersistedWallet> { + fn bdk_wallet_mut(&mut self) -> &mut PersistedWallet { &mut self.internals_mut().bdk_wallet } - fn bdk_wallet_db_mut( - &mut self, - ) -> ( - &mut PersistedWallet>, - &mut Store, - ) { + fn bdk_wallet_db_mut(&mut self) -> (&mut PersistedWallet, &mut BdkStore) { let internals_mut = self.internals_mut(); ( &mut internals_mut.bdk_wallet, @@ -354,7 +352,7 @@ pub trait WalletCore { .map_err(|e| Error::FailedBdkSync { details: e.to_string(), })?; - bdk_wallet.persist(bdk_db)?; + block_on(bdk_wallet.persist_async(bdk_db))?; if matches!(options.keychain, SyncKeychain::Colored) { self.update_db_colored_txos_from_bdk(txn, include_spent)?; diff --git a/src/wallet/indexer.rs b/src/wallet/indexer.rs index 94b4693d..2f468a61 100644 --- a/src/wallet/indexer.rs +++ b/src/wallet/indexer.rs @@ -196,9 +196,7 @@ impl Indexer { pub(crate) fn populate_tx_cache( &self, - #[cfg_attr(feature = "esplora", allow(unused))] bdk_wallet: &PersistedWallet< - Store, - >, + #[cfg_attr(feature = "esplora", allow(unused))] bdk_wallet: &PersistedWallet, ) { match self { #[cfg(feature = "electrum")] diff --git a/src/wallet/multisig.rs b/src/wallet/multisig.rs index 2a539e6d..d081445f 100644 --- a/src/wallet/multisig.rs +++ b/src/wallet/multisig.rs @@ -240,7 +240,7 @@ impl WalletCore for MultisigWallet { reveal(KeychainKind::Internal, response.internal); reveal(KeychainKind::External, response.external); if persist { - bdk_wallet.persist(bdk_database)?; + block_on(bdk_wallet.persist_async(bdk_database))?; } // sync UTXOs self.sync_bdk_and_db_txos(txn, options, include_spent) @@ -267,7 +267,7 @@ impl WalletOffline for MultisigWallet { bdk_wallet.reveal_next_address(keychain); } let first_address = bdk_wallet.peek_address(keychain, start_index).address; - bdk_wallet.persist(bdk_database)?; + block_on(bdk_wallet.persist_async(bdk_database))?; Ok(first_address) } } diff --git a/src/wallet/offline.rs b/src/wallet/offline.rs index b5b81a34..d7332a8b 100644 --- a/src/wallet/offline.rs +++ b/src/wallet/offline.rs @@ -1243,7 +1243,7 @@ pub trait WalletOffline: WalletBackup { ) -> Result { let (bdk_wallet, bdk_db) = self.bdk_wallet_db_mut(); let address = bdk_wallet.reveal_next_address(keychain).address; - bdk_wallet.persist(bdk_db)?; + block_on(bdk_wallet.persist_async(bdk_db))?; Ok(address) } diff --git a/src/wallet/online.rs b/src/wallet/online.rs index edbe8afd..7efe6232 100644 --- a/src/wallet/online.rs +++ b/src/wallet/online.rs @@ -99,7 +99,7 @@ pub trait WalletOnline: WalletOffline { let seen_at = now().unix_timestamp() as u64; let (bdk_wallet, bdk_db) = self.bdk_wallet_db_mut(); bdk_wallet.apply_unconfirmed_txs([(tx.clone(), seen_at)]); - bdk_wallet.persist(bdk_db)?; + block_on(bdk_wallet.persist_async(bdk_db))?; // promote any newly-known colored UTXOs (e.g. the change output) from // exists=false to exists=true in the rgb_lib DB diff --git a/src/wallet/test/go_online.rs b/src/wallet/test/go_online.rs index 727d16b4..79efecb6 100644 --- a/src/wallet/test/go_online.rs +++ b/src/wallet/test/go_online.rs @@ -175,7 +175,7 @@ fn consistency_check_fail_bitcoins() { .unwrap(); { let (bdk_wallet, bdk_database) = party_empty.wallet.bdk_wallet_db_mut(); - bdk_wallet.persist(bdk_database).unwrap(); + block_on(bdk_wallet.persist_async(bdk_database)).unwrap(); } let mut rcv_party = get_funded_party!(); party_empty.drain_to(&rcv_party.get_address()); @@ -267,7 +267,7 @@ fn consistency_check_fail_utxos() { .unwrap(); { let (bdk_wallet, bdk_database) = party_empty.wallet.bdk_wallet_db_mut(); - bdk_wallet.persist(bdk_database).unwrap(); + block_on(bdk_wallet.persist_async(bdk_database)).unwrap(); } let mut rcv_party = get_funded_party!(); party_empty.drain_to(&rcv_party.get_address());