use bdk_chain::Merge;
use bdk_wallet::{AsyncWalletPersister, ChangeSet, WalletPersister};
use redb::{Database, ReadableTableMetadata, TableDefinition};
use std::future::Future;
use std::path::Path;
use std::pin::Pin;
const WALLET_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("wallet_data");
const CHANGESET_KEY: &str = "wallet_changeset";
#[derive(Debug)]
pub struct RedbStore {
db: Database,
}
impl RedbStore {
pub fn create<P>(file_path: P) -> Result<Self, RedbError>
where
P: AsRef<Path>,
{
let db = Database::create(file_path)?;
let write_txn = db.begin_write()?;
{
let _table = write_txn.open_table(WALLET_TABLE)?;
}
write_txn.commit()?;
Ok(Self { db })
}
pub fn create_with_config<P>(
file_path: P,
config: &mut redb::Builder,
) -> Result<Self, RedbError>
where
P: AsRef<Path>,
{
let db = config.create(file_path)?;
let write_txn = db.begin_write()?;
{
let _table = write_txn.open_table(WALLET_TABLE)?;
}
write_txn.commit()?;
Ok(Self { db })
}
pub fn open<P>(file_path: P) -> Result<Self, RedbError>
where
P: AsRef<Path>,
{
let db = Database::open(file_path)?;
Ok(Self { db })
}
pub fn open_with_config<P>(file_path: P, config: redb::Builder) -> Result<Self, RedbError>
where
P: AsRef<Path>,
{
let db = config.open(file_path)?;
Ok(Self { db })
}
pub fn open_or_create<P>(file_path: P) -> Result<Self, RedbError>
where
P: AsRef<Path>,
{
if file_path.as_ref().exists() {
Self::open(file_path)
} else {
Self::create(file_path)
}
}
pub fn table_stats(&self) -> Result<redb::TableStats, RedbError> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(WALLET_TABLE)?;
Ok(table.stats()?)
}
fn get_changeset(&self) -> Result<Option<ChangeSet>, RedbError> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(WALLET_TABLE)?;
match table.get(CHANGESET_KEY)? {
Some(value) => {
let changeset_bytes = value.value();
let changeset: ChangeSet =
serde_json::from_slice(changeset_bytes).map_err(RedbError::Deserialization)?;
Ok(Some(changeset))
}
None => Ok(None),
}
}
fn store_changeset(&self, changeset: &ChangeSet) -> Result<(), RedbError> {
if changeset.is_empty() {
return Ok(());
}
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(WALLET_TABLE)?;
let changeset_bytes =
serde_json::to_vec(changeset).map_err(RedbError::Serialization)?;
table.insert(CHANGESET_KEY, changeset_bytes.as_slice())?;
}
write_txn.commit()?;
Ok(())
}
}
#[derive(Debug)]
pub enum RedbError {
Database(redb::Error),
Serialization(serde_json::Error),
Deserialization(serde_json::Error),
Io(std::io::Error),
Commit(redb::CommitError),
Table(redb::TableError),
Transaction(redb::TransactionError),
}
impl std::fmt::Display for RedbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Database(e) => write!(f, "Database error: {}", e),
Self::Serialization(e) => write!(f, "Serialization error: {}", e),
Self::Deserialization(e) => write!(f, "Deserialization error: {}", e),
Self::Io(e) => write!(f, "I/O error: {}", e),
Self::Commit(e) => write!(f, "Commit error: {}", e),
Self::Table(e) => write!(f, "Table error: {}", e),
Self::Transaction(e) => write!(f, "Transaction error: {}", e),
}
}
}
impl std::error::Error for RedbError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Database(e) => Some(e),
Self::Serialization(e) => Some(e),
Self::Deserialization(e) => Some(e),
Self::Io(e) => Some(e),
Self::Commit(e) => Some(e),
Self::Table(e) => Some(e),
Self::Transaction(e) => Some(e),
}
}
}
impl From<redb::DatabaseError> for RedbError {
fn from(e: redb::DatabaseError) -> Self {
Self::Database(e.into())
}
}
impl From<redb::StorageError> for RedbError {
fn from(e: redb::StorageError) -> Self {
Self::Database(e.into())
}
}
impl From<redb::Error> for RedbError {
fn from(e: redb::Error) -> Self {
Self::Database(e)
}
}
impl From<serde_json::Error> for RedbError {
fn from(e: serde_json::Error) -> Self {
Self::Serialization(e)
}
}
impl From<std::io::Error> for RedbError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<redb::CommitError> for RedbError {
fn from(e: redb::CommitError) -> Self {
Self::Commit(e)
}
}
impl From<redb::TableError> for RedbError {
fn from(e: redb::TableError) -> Self {
Self::Table(e)
}
}
impl From<redb::TransactionError> for RedbError {
fn from(e: redb::TransactionError) -> Self {
Self::Transaction(e)
}
}
type FutureResult<'a, T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>;
impl WalletPersister for RedbStore {
type Error = RedbError;
fn initialize(persister: &mut Self) -> Result<ChangeSet, Self::Error> {
persister.get_changeset().map(|opt| opt.unwrap_or_default())
}
fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> {
let existing_changeset = persister.get_changeset()?;
let final_changeset = match existing_changeset {
Some(mut existing) => {
existing.merge(changeset.clone());
existing
}
None => changeset.clone(),
};
persister.store_changeset(&final_changeset)
}
}
impl AsyncWalletPersister for RedbStore {
type Error = RedbError;
fn initialize<'a>(persister: &'a mut Self) -> FutureResult<'a, ChangeSet, Self::Error>
where
Self: 'a,
{
Box::pin(async move {
persister.get_changeset().map(|opt| opt.unwrap_or_default())
})
}
fn persist<'a>(
persister: &'a mut Self,
changeset: &'a ChangeSet,
) -> FutureResult<'a, (), Self::Error>
where
Self: 'a,
{
Box::pin(async move {
let existing_changeset = persister.get_changeset()?;
let final_changeset = match existing_changeset {
Some(mut existing) => {
existing.merge(changeset.clone());
existing
}
None => changeset.clone(),
};
persister.store_changeset(&final_changeset)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use bdk_wallet::{CreateParams, KeychainKind, LoadParams, PersistedWallet};
use bitcoin::Network;
use futures::future::join_all;
use std::fs;
use std::fs::OpenOptions;
use std::sync::Arc;
use tempfile::tempdir;
use tokio::sync::Mutex;
const TEST_DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
const TEST_CHANGE_DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
#[test]
fn test_create_and_persist() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("wallet.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create(&mut store, create_params).unwrap();
let _address = wallet.reveal_next_address(KeychainKind::External);
let persisted = wallet.persist(&mut store).unwrap();
assert!(persisted);
let load_params = LoadParams::default();
let loaded_wallet = PersistedWallet::load(&mut store, load_params).unwrap();
assert!(loaded_wallet.is_some());
}
#[test]
fn test_empty_store() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("empty.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let changeset = WalletPersister::initialize(&mut store).unwrap();
assert!(changeset.is_empty());
}
#[test]
fn test_open_nonexistent_file() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("nonexistent.redb");
let result = RedbStore::open(&db_path);
assert!(result.is_err());
}
#[test]
fn test_open_or_create() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("open_or_create.redb");
let store = RedbStore::open_or_create(&db_path).unwrap();
drop(store);
let store = RedbStore::open_or_create(&db_path).unwrap();
drop(store);
assert!(db_path.exists());
}
#[test]
fn test_empty_changeset() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("empty_changeset.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let empty_changeset = ChangeSet::default();
WalletPersister::persist(&mut store, &empty_changeset).unwrap();
let retrieved = WalletPersister::initialize(&mut store).unwrap();
assert!(retrieved.is_empty());
}
#[test]
fn test_persist_and_retrieve() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("persist_retrieve.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create(&mut store, create_params).unwrap();
for _ in 0..5 {
let _address = wallet.reveal_next_address(KeychainKind::External);
}
wallet.persist(&mut store).unwrap();
drop(store);
let mut store = RedbStore::open(&db_path).unwrap();
let loaded_wallet = PersistedWallet::load(&mut store, LoadParams::default())
.unwrap()
.unwrap();
let original_address = wallet.peek_address(KeychainKind::External, 4);
let loaded_address = loaded_wallet.peek_address(KeychainKind::External, 4);
assert_eq!(
original_address.address.to_string(),
loaded_address.address.to_string()
);
}
#[test]
fn test_update_existing_data() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("update.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create(&mut store, create_params).unwrap();
for _ in 0..3 {
let _address = wallet.reveal_next_address(KeychainKind::External);
}
wallet.persist(&mut store).unwrap();
for _ in 0..3 {
let _address = wallet.reveal_next_address(KeychainKind::External);
}
wallet.persist(&mut store).unwrap();
drop(store);
let mut store = RedbStore::open(&db_path).unwrap();
let loaded_wallet = PersistedWallet::load(&mut store, LoadParams::default())
.unwrap()
.unwrap();
let last_address = loaded_wallet.peek_address(KeychainKind::External, 5);
assert_eq!(last_address.index, 5);
}
#[test]
fn test_multiple_stores_same_file() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("multiple.redb");
let _store1 = RedbStore::create(&db_path).unwrap();
let result = RedbStore::open(&db_path);
assert!(result.is_err());
}
#[test]
fn test_corrupted_data_recovery() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("corrupt.redb");
{
let mut store = RedbStore::create(&db_path).unwrap();
let create_params = CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR)
.network(Network::Testnet);
let mut wallet = PersistedWallet::create(&mut store, create_params).unwrap();
wallet.reveal_next_address(KeychainKind::External);
wallet.persist(&mut store).unwrap();
}
fs::remove_file(&db_path).unwrap();
let mut store = RedbStore::create(&db_path).unwrap();
let changeset = WalletPersister::initialize(&mut store).unwrap();
assert!(changeset.is_empty());
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let _wallet = PersistedWallet::create(&mut store, create_params).unwrap();
}
#[tokio::test]
async fn test_async_create_and_persist() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("wallet.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create_async(&mut store, create_params)
.await
.unwrap();
let _address = wallet.reveal_next_address(KeychainKind::External);
let persisted = wallet.persist_async(&mut store).await.unwrap();
assert!(persisted);
let load_params = LoadParams::default();
let loaded_wallet = PersistedWallet::load_async(&mut store, load_params)
.await
.unwrap();
assert!(loaded_wallet.is_some());
}
#[tokio::test]
async fn test_async_empty_store() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_empty.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let changeset = AsyncWalletPersister::initialize(&mut store).await.unwrap();
assert!(changeset.is_empty());
}
#[tokio::test]
async fn test_async_empty_changeset() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_empty_changeset.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let empty_changeset = ChangeSet::default();
AsyncWalletPersister::persist(&mut store, &empty_changeset)
.await
.unwrap();
let retrieved = AsyncWalletPersister::initialize(&mut store).await.unwrap();
assert!(retrieved.is_empty());
}
#[tokio::test]
async fn test_async_persist_and_retrieve() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_persist_retrieve.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create_async(&mut store, create_params)
.await
.unwrap();
for _ in 0..5 {
let _address = wallet.reveal_next_address(KeychainKind::External);
}
wallet.persist_async(&mut store).await.unwrap();
drop(wallet);
drop(store);
let mut store = RedbStore::open(&db_path).unwrap();
let loaded_wallet = PersistedWallet::load_async(&mut store, LoadParams::default())
.await
.unwrap()
.unwrap();
assert_eq!(
loaded_wallet.peek_address(KeychainKind::External, 4).index,
4
);
}
#[tokio::test]
async fn test_async_update_existing_data() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_update.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create_async(&mut store, create_params)
.await
.unwrap();
for _ in 0..3 {
let _address = wallet.reveal_next_address(KeychainKind::External);
}
wallet.persist_async(&mut store).await.unwrap();
for _ in 0..3 {
let _address = wallet.reveal_next_address(KeychainKind::External);
}
wallet.persist_async(&mut store).await.unwrap();
drop(wallet);
drop(store);
let mut store = RedbStore::open(&db_path).unwrap();
let loaded_wallet = PersistedWallet::load_async(&mut store, LoadParams::default())
.await
.unwrap()
.unwrap();
let last_address = loaded_wallet.peek_address(KeychainKind::External, 5);
assert_eq!(last_address.index, 5);
}
#[tokio::test]
async fn test_async_concurrent_operations() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_concurrent.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let wallet = PersistedWallet::create_async(&mut store, create_params)
.await
.unwrap();
let shared_wallet = Arc::new(Mutex::new(wallet));
let shared_store = Arc::new(Mutex::new(store));
let mut tasks = vec![];
for _ in 0..5 {
let wallet_clone = Arc::clone(&shared_wallet);
let store_clone = Arc::clone(&shared_store);
let task = tokio::spawn(async move {
let mut wallet_guard = wallet_clone.lock().await;
let address = wallet_guard.reveal_next_address(KeychainKind::External);
let mut store_guard = store_clone.lock().await;
wallet_guard.persist_async(&mut *store_guard).await.unwrap();
address
});
tasks.push(task);
}
let results = join_all(tasks).await;
for result in results {
assert!(result.is_ok());
}
let wallet_guard = shared_wallet.lock().await;
let last_address = wallet_guard.peek_address(KeychainKind::External, 4);
assert_eq!(last_address.index, 4);
drop(wallet_guard);
let mut store_guard = shared_store.lock().await;
let loaded_wallet = PersistedWallet::load_async(&mut *store_guard, LoadParams::default())
.await
.unwrap()
.unwrap();
let last_address = loaded_wallet.peek_address(KeychainKind::External, 4);
assert_eq!(last_address.index, 4);
}
#[tokio::test]
async fn test_async_reopen_and_modify() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_reopen.redb");
{
let mut store = RedbStore::create(&db_path).unwrap();
let create_params = CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR)
.network(Network::Testnet);
let mut wallet = PersistedWallet::create_async(&mut store, create_params)
.await
.unwrap();
for _ in 0..3 {
let _address = wallet.reveal_next_address(KeychainKind::External);
}
wallet.persist_async(&mut store).await.unwrap();
}
{
let mut store = RedbStore::open(&db_path).unwrap();
let load_params = LoadParams::default();
let mut wallet = PersistedWallet::load_async(&mut store, load_params)
.await
.unwrap()
.unwrap();
assert_eq!(wallet.peek_address(KeychainKind::External, 2).index, 2);
for _ in 0..2 {
let _address = wallet.reveal_next_address(KeychainKind::External);
}
wallet.persist_async(&mut store).await.unwrap();
}
{
let mut store = RedbStore::open(&db_path).unwrap();
let load_params = LoadParams::default();
let wallet = PersistedWallet::load_async(&mut store, load_params)
.await
.unwrap()
.unwrap();
assert_eq!(wallet.peek_address(KeychainKind::External, 4).index, 4);
}
}
#[tokio::test]
async fn test_async_change_addresses() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_change.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create_async(&mut store, create_params)
.await
.unwrap();
for _ in 0..3 {
let _address = wallet.reveal_next_address(KeychainKind::External);
}
for _ in 0..2 {
let _address = wallet.reveal_next_address(KeychainKind::Internal);
}
wallet.persist_async(&mut store).await.unwrap();
let loaded_wallet = PersistedWallet::load_async(&mut store, LoadParams::default())
.await
.unwrap()
.unwrap();
assert_eq!(
loaded_wallet.peek_address(KeychainKind::External, 2).index,
2
);
assert_eq!(
loaded_wallet.peek_address(KeychainKind::Internal, 1).index,
1
);
}
#[tokio::test]
async fn test_async_multiple_persists() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_multiple_persists.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create_async(&mut store, create_params)
.await
.unwrap();
for i in 0..5 {
let _address = wallet.reveal_next_address(KeychainKind::External);
let persisted = wallet.persist_async(&mut store).await.unwrap();
if i == 0 {
assert!(persisted);
}
}
let loaded_wallet = PersistedWallet::load_async(&mut store, LoadParams::default())
.await
.unwrap()
.unwrap();
assert_eq!(
loaded_wallet.peek_address(KeychainKind::External, 4).index,
4
);
}
#[tokio::test]
async fn test_async_error_handling() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_errors.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create_async(&mut store, create_params)
.await
.unwrap();
wallet.persist_async(&mut store).await.unwrap();
drop(wallet);
drop(store);
{
let file = OpenOptions::new().write(true).open(&db_path).unwrap();
file.set_len(100).unwrap();
}
let result = RedbStore::open(&db_path);
assert!(result.is_err());
match result {
Err(RedbError::Database(_)) => {
}
Err(e) => {
panic!("Unexpected error type: {:?}", e);
}
Ok(_) => {
panic!("Expected an error, but got Ok");
}
}
let db_path2 = temp_dir.path().join("async_errors2.redb");
let mut store = RedbStore::create(&db_path2).unwrap();
let load_result = PersistedWallet::load_async(&mut store, LoadParams::default()).await;
assert!(load_result.is_ok());
assert!(load_result.unwrap().is_none());
let invalid_descriptor = "invalid_descriptor";
let invalid_params =
CreateParams::new(invalid_descriptor, invalid_descriptor).network(Network::Testnet);
let create_result = PersistedWallet::create_async(&mut store, invalid_params).await;
assert!(create_result.is_err());
if cfg!(not(target_os = "windows")) {
let db_path3 = temp_dir.path().join("async_errors3.redb");
let _store1 = RedbStore::create(&db_path3).unwrap();
let result = RedbStore::open(&db_path3);
assert!(result.is_err());
}
}
#[tokio::test]
async fn test_async_load_with_network() {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("async_network.redb");
let mut store = RedbStore::create(&db_path).unwrap();
let create_params =
CreateParams::new(TEST_DESCRIPTOR, TEST_CHANGE_DESCRIPTOR).network(Network::Testnet);
let mut wallet = PersistedWallet::create_async(&mut store, create_params)
.await
.unwrap();
assert_eq!(wallet.network(), Network::Testnet);
wallet.persist_async(&mut store).await.unwrap();
let load_params = LoadParams::default().check_network(Network::Testnet);
let loaded_wallet = PersistedWallet::load_async(&mut store, load_params)
.await
.unwrap()
.unwrap();
assert_eq!(loaded_wallet.network(), Network::Testnet);
let load_params = LoadParams::default().check_network(Network::Bitcoin);
let result = PersistedWallet::load_async(&mut store, load_params).await;
match result {
Ok(Some(wallet)) => {
assert_eq!(wallet.network(), Network::Testnet);
}
Ok(None) => {
panic!("Wallet was not found, but should exist");
}
Err(_) => {
}
}
}
}