diff --git a/src/database/any.rs b/src/database/any.rs new file mode 100644 index 00000000..aea44eea --- /dev/null +++ b/src/database/any.rs @@ -0,0 +1,390 @@ +// Magical Bitcoin Library +// Written in 2020 by +// Alekos Filini +// +// Copyright (c) 2020 Magical Bitcoin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//! Runtime-checked database types +//! +//! This module provides the implementation of [`AnyDatabase`] which allows switching the +//! inner [`Database`] type at runtime. +//! +//! ## Example +//! +//! In this example, `wallet_memory` and `wallet_sled` have the same type of `Wallet`. +//! +//! ```no_run +//! # use bitcoin::Network; +//! # use bdk::database::{AnyDatabase, MemoryDatabase}; +//! # use bdk::{Wallet, OfflineWallet}; +//! let memory = MemoryDatabase::default().into(); +//! let wallet_memory: OfflineWallet = Wallet::new_offline("...", None, Network::Testnet, memory)?; +//! +//! # #[cfg(feature = "key-value-db")] +//! # { +//! let sled = sled::open("my-database")?.open_tree("default_tree")?.into(); +//! let wallet_sled: OfflineWallet = Wallet::new_offline("...", None, Network::Testnet, sled)?; +//! # } +//! # Ok::<(), bdk::Error>(()) +//! ``` +//! +//! When paired with the use of [`ConfigurableDatabase`], it allows creating wallets with any +//! database supported using a single line of code: +//! +//! ```no_run +//! # use bitcoin::Network; +//! # use bdk::database::*; +//! # use bdk::{Wallet, OfflineWallet}; +//! let config = serde_json::from_str("...")?; +//! let database = AnyDatabase::from_config(&config)?; +//! let wallet: OfflineWallet<_> = Wallet::new_offline("...", None, Network::Testnet, database)?; +//! # Ok::<(), bdk::Error>(()) +//! ``` + +use super::*; + +macro_rules! impl_from { + ( $from:ty, $to:ty, $variant:ident, $( $cfg:tt )* ) => { + $( $cfg )* + impl From<$from> for $to { + fn from(inner: $from) -> Self { + <$to>::$variant(inner) + } + } + }; +} + +macro_rules! impl_inner_method { + ( $enum_name:ident, $self:expr, $name:ident $(, $args:expr)* ) => { + match $self { + $enum_name::Memory(inner) => inner.$name( $($args, )* ), + #[cfg(feature = "key-value-db")] + $enum_name::Sled(inner) => inner.$name( $($args, )* ), + } + } +} + +/// Type that can contain any of the [`Database`] types defined by the library +/// +/// It allows switching database type at runtime. +/// +/// See [this module](crate::database::any)'s documentation for a usage example. +pub enum AnyDatabase { + Memory(memory::MemoryDatabase), + #[cfg(feature = "key-value-db")] + #[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))] + Sled(sled::Tree), +} + +impl_from!(memory::MemoryDatabase, AnyDatabase, Memory,); +impl_from!(sled::Tree, AnyDatabase, Sled, #[cfg(feature = "key-value-db")]); + +/// Type that contains any of the [`BatchDatabase::Batch`] types defined by the library +pub enum AnyBatch { + Memory(::Batch), + #[cfg(feature = "key-value-db")] + #[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))] + Sled(::Batch), +} + +impl_from!( + ::Batch, + AnyBatch, + Memory, +); +impl_from!(::Batch, AnyBatch, Sled, #[cfg(feature = "key-value-db")]); + +impl BatchOperations for AnyDatabase { + fn set_script_pubkey( + &mut self, + script: &Script, + script_type: ScriptType, + child: u32, + ) -> Result<(), Error> { + impl_inner_method!( + AnyDatabase, + self, + set_script_pubkey, + script, + script_type, + child + ) + } + fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> { + impl_inner_method!(AnyDatabase, self, set_utxo, utxo) + } + fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> { + impl_inner_method!(AnyDatabase, self, set_raw_tx, transaction) + } + fn set_tx(&mut self, transaction: &TransactionDetails) -> Result<(), Error> { + impl_inner_method!(AnyDatabase, self, set_tx, transaction) + } + fn set_last_index(&mut self, script_type: ScriptType, value: u32) -> Result<(), Error> { + impl_inner_method!(AnyDatabase, self, set_last_index, script_type, value) + } + + fn del_script_pubkey_from_path( + &mut self, + script_type: ScriptType, + child: u32, + ) -> Result, Error> { + impl_inner_method!( + AnyDatabase, + self, + del_script_pubkey_from_path, + script_type, + child + ) + } + fn del_path_from_script_pubkey( + &mut self, + script: &Script, + ) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, del_path_from_script_pubkey, script) + } + fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, del_utxo, outpoint) + } + fn del_raw_tx(&mut self, txid: &Txid) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, del_raw_tx, txid) + } + fn del_tx( + &mut self, + txid: &Txid, + include_raw: bool, + ) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, del_tx, txid, include_raw) + } + fn del_last_index(&mut self, script_type: ScriptType) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, del_last_index, script_type) + } +} + +impl Database for AnyDatabase { + fn check_descriptor_checksum>( + &mut self, + script_type: ScriptType, + bytes: B, + ) -> Result<(), Error> { + impl_inner_method!( + AnyDatabase, + self, + check_descriptor_checksum, + script_type, + bytes + ) + } + + fn iter_script_pubkeys(&self, script_type: Option) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, iter_script_pubkeys, script_type) + } + fn iter_utxos(&self) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, iter_utxos) + } + fn iter_raw_txs(&self) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, iter_raw_txs) + } + fn iter_txs(&self, include_raw: bool) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, iter_txs, include_raw) + } + + fn get_script_pubkey_from_path( + &self, + script_type: ScriptType, + child: u32, + ) -> Result, Error> { + impl_inner_method!( + AnyDatabase, + self, + get_script_pubkey_from_path, + script_type, + child + ) + } + fn get_path_from_script_pubkey( + &self, + script: &Script, + ) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, get_path_from_script_pubkey, script) + } + fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, get_utxo, outpoint) + } + fn get_raw_tx(&self, txid: &Txid) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, get_raw_tx, txid) + } + fn get_tx(&self, txid: &Txid, include_raw: bool) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, get_tx, txid, include_raw) + } + fn get_last_index(&self, script_type: ScriptType) -> Result, Error> { + impl_inner_method!(AnyDatabase, self, get_last_index, script_type) + } + + fn increment_last_index(&mut self, script_type: ScriptType) -> Result { + impl_inner_method!(AnyDatabase, self, increment_last_index, script_type) + } +} + +impl BatchOperations for AnyBatch { + fn set_script_pubkey( + &mut self, + script: &Script, + script_type: ScriptType, + child: u32, + ) -> Result<(), Error> { + impl_inner_method!( + AnyBatch, + self, + set_script_pubkey, + script, + script_type, + child + ) + } + fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> { + impl_inner_method!(AnyBatch, self, set_utxo, utxo) + } + fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> { + impl_inner_method!(AnyBatch, self, set_raw_tx, transaction) + } + fn set_tx(&mut self, transaction: &TransactionDetails) -> Result<(), Error> { + impl_inner_method!(AnyBatch, self, set_tx, transaction) + } + fn set_last_index(&mut self, script_type: ScriptType, value: u32) -> Result<(), Error> { + impl_inner_method!(AnyBatch, self, set_last_index, script_type, value) + } + + fn del_script_pubkey_from_path( + &mut self, + script_type: ScriptType, + child: u32, + ) -> Result, Error> { + impl_inner_method!( + AnyBatch, + self, + del_script_pubkey_from_path, + script_type, + child + ) + } + fn del_path_from_script_pubkey( + &mut self, + script: &Script, + ) -> Result, Error> { + impl_inner_method!(AnyBatch, self, del_path_from_script_pubkey, script) + } + fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { + impl_inner_method!(AnyBatch, self, del_utxo, outpoint) + } + fn del_raw_tx(&mut self, txid: &Txid) -> Result, Error> { + impl_inner_method!(AnyBatch, self, del_raw_tx, txid) + } + fn del_tx( + &mut self, + txid: &Txid, + include_raw: bool, + ) -> Result, Error> { + impl_inner_method!(AnyBatch, self, del_tx, txid, include_raw) + } + fn del_last_index(&mut self, script_type: ScriptType) -> Result, Error> { + impl_inner_method!(AnyBatch, self, del_last_index, script_type) + } +} + +impl BatchDatabase for AnyDatabase { + type Batch = AnyBatch; + + fn begin_batch(&self) -> Self::Batch { + match self { + AnyDatabase::Memory(inner) => inner.begin_batch().into(), + #[cfg(feature = "key-value-db")] + AnyDatabase::Sled(inner) => inner.begin_batch().into(), + } + } + fn commit_batch(&mut self, batch: Self::Batch) -> Result<(), Error> { + // TODO: refactor once `move_ref_pattern` is stable + + match self { + AnyDatabase::Memory(db) => { + if let AnyBatch::Memory(batch) = batch { + db.commit_batch(batch) + } else { + unimplemented!() + } + } + #[cfg(feature = "key-value-db")] + AnyDatabase::Sled(db) => { + if let AnyBatch::Sled(batch) = batch { + db.commit_batch(batch) + } else { + unimplemented!() + } + } + } + } +} + +/// Configuration type for a [`sled::Tree`] database +#[cfg(feature = "key-value-db")] +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SledDbConfiguration { + pub path: String, + pub tree_name: String, +} + +#[cfg(feature = "key-value-db")] +impl ConfigurableDatabase for sled::Tree { + type Config = SledDbConfiguration; + + fn from_config(config: &Self::Config) -> Result { + Ok(sled::open(&config.path)?.open_tree(&config.tree_name)?) + } +} + +/// Type that can contain any of the database configurations defined by the library +/// +/// This allows storing a single configuration that can be loaded into an [`AnyDatabase`] +/// instance. Wallets that plan to offer users the ability to switch blockchain backend at runtime +/// will find this particularly useful. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum AnyDatabaseConfig { + Memory(()), + #[cfg(feature = "key-value-db")] + #[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))] + Sled(SledDbConfiguration), +} + +impl ConfigurableDatabase for AnyDatabase { + type Config = AnyDatabaseConfig; + + fn from_config(config: &Self::Config) -> Result { + Ok(match config { + AnyDatabaseConfig::Memory(inner) => { + AnyDatabase::Memory(memory::MemoryDatabase::from_config(inner)?) + } + #[cfg(feature = "key-value-db")] + AnyDatabaseConfig::Sled(inner) => AnyDatabase::Sled(sled::Tree::from_config(inner)?), + }) + } +} + +impl_from!((), AnyDatabaseConfig, Memory,); +impl_from!(SledDbConfiguration, AnyDatabaseConfig, Sled, #[cfg(feature = "key-value-db")]); diff --git a/src/database/memory.rs b/src/database/memory.rs index d1ecd645..004ab585 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -34,7 +34,7 @@ use bitcoin::consensus::encode::{deserialize, serialize}; use bitcoin::hash_types::Txid; use bitcoin::{OutPoint, Script, Transaction}; -use crate::database::{BatchDatabase, BatchOperations, Database}; +use crate::database::{BatchDatabase, BatchOperations, ConfigurableDatabase, Database}; use crate::error::Error; use crate::types::*; @@ -450,6 +450,14 @@ impl BatchDatabase for MemoryDatabase { } } +impl ConfigurableDatabase for MemoryDatabase { + type Config = (); + + fn from_config(_config: &Self::Config) -> Result { + Ok(MemoryDatabase::default()) + } +} + #[cfg(test)] impl MemoryDatabase { // Artificially insert a tx in the database, as if we had found it with a `sync` diff --git a/src/database/mod.rs b/src/database/mod.rs index 45cedcd1..090e1ae4 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -43,6 +43,9 @@ use bitcoin::{OutPoint, Script, Transaction, TxOut}; use crate::error::Error; use crate::types::*; +pub mod any; +pub use any::{AnyDatabase, AnyDatabaseConfig}; + #[cfg(feature = "key-value-db")] pub(crate) mod keyvalue; @@ -159,6 +162,15 @@ pub trait BatchDatabase: Database { fn commit_batch(&mut self, batch: Self::Batch) -> Result<(), Error>; } +/// Trait for [`Database`] types that can be created given a configuration +pub trait ConfigurableDatabase: Database + Sized { + /// Type that contains the configuration + type Config: std::fmt::Debug; + + /// Create a new instance given a configuration + fn from_config(config: &Self::Config) -> Result; +} + pub(crate) trait DatabaseUtils: Database { fn is_mine(&self, script: &Script) -> Result { self.get_path_from_script_pubkey(script)