From 6caff9c441b83fd446d481c03e1ceba4e83160f3 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Thu, 27 Oct 2022 01:57:11 -0500 Subject: [PATCH] Add wasm compatible database using gloo_storage::LocalStorage --- Cargo.toml | 2 + src/database/any.rs | 28 +++ src/database/localstorage.rs | 469 +++++++++++++++++++++++++++++++++++ src/database/mod.rs | 5 + 4 files changed, 504 insertions(+) create mode 100644 src/database/localstorage.rs diff --git a/Cargo.toml b/Cargo.toml index 6dcfead1f..9f1b05af2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ rocksdb = { version = "0.14", default-features = false, features = ["snappy"], o cc = { version = ">=1.0.64", optional = true } socks = { version = "0.3", optional = true } hwi = { version = "0.3.0", optional = true } +gloo-storage = { version = "0.2.2", optional = true } bip39 = { version = "1.0.1", optional = true } bitcoinconsensus = { version = "0.19.0-3", optional = true } @@ -61,6 +62,7 @@ all-keys = ["keys-bip39"] keys-bip39 = ["bip39"] rpc = ["bitcoincore-rpc"] hardware-signer = ["hwi"] +wasm-db = ["gloo-storage"] # We currently provide mulitple implementations of `Blockchain`, all are # blocking except for the `EsploraBlockchain` which can be either async or diff --git a/src/database/any.rs b/src/database/any.rs index bbd9d41a5..94500fdbd 100644 --- a/src/database/any.rs +++ b/src/database/any.rs @@ -68,6 +68,8 @@ macro_rules! impl_inner_method { $enum_name::Sled(inner) => inner.$name( $($args, )* ), #[cfg(feature = "sqlite")] $enum_name::Sqlite(inner) => inner.$name( $($args, )* ), + #[cfg(feature = "wasm-db")] + $enum_name::LocalStorage(inner) => inner.$name( $($args, )* ), } } } @@ -89,11 +91,16 @@ pub enum AnyDatabase { #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] /// Sqlite embedded database using [`rusqlite`] Sqlite(sqlite::SqliteDatabase), + #[cfg(feature = "wasm-db")] + #[cfg_attr(docsrs, doc(cfg(feature = "wasm-db")))] + /// Browser LocalStorage database based on [`gloo_storage`] + LocalStorage(localstorage::LocalStorageDatabase), } impl_from!(memory::MemoryDatabase, AnyDatabase, Memory,); impl_from!(sled::Tree, AnyDatabase, Sled, #[cfg(feature = "key-value-db")]); impl_from!(sqlite::SqliteDatabase, AnyDatabase, Sqlite, #[cfg(feature = "sqlite")]); +impl_from!(localstorage::LocalStorageDatabase, AnyDatabase, LocalStorage, #[cfg(feature = "wasm-db")]); /// Type that contains any of the [`BatchDatabase::Batch`] types defined by the library pub enum AnyBatch { @@ -107,6 +114,10 @@ pub enum AnyBatch { #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] /// Sqlite embedded database using [`rusqlite`] Sqlite(::Batch), + #[cfg(feature = "wasm-db")] + #[cfg_attr(docsrs, doc(cfg(feature = "wasm-db")))] + /// Browser LocalStorage database based on [`gloo_storage`] + LocalStorage(::Batch), } impl_from!( @@ -116,6 +127,7 @@ impl_from!( ); impl_from!(::Batch, AnyBatch, Sled, #[cfg(feature = "key-value-db")]); impl_from!(::Batch, AnyBatch, Sqlite, #[cfg(feature = "sqlite")]); +impl_from!(::Batch, AnyBatch, LocalStorage, #[cfg(feature = "wasm-db")]); impl BatchOperations for AnyDatabase { fn set_script_pubkey( @@ -326,6 +338,8 @@ impl BatchDatabase for AnyDatabase { AnyDatabase::Sled(inner) => inner.begin_batch().into(), #[cfg(feature = "sqlite")] AnyDatabase::Sqlite(inner) => inner.begin_batch().into(), + #[cfg(feature = "wasm-db")] + AnyDatabase::LocalStorage(inner) => inner.begin_batch().into(), } } fn commit_batch(&mut self, batch: Self::Batch) -> Result<(), Error> { @@ -345,6 +359,11 @@ impl BatchDatabase for AnyDatabase { AnyBatch::Sqlite(batch) => db.commit_batch(batch), _ => unimplemented!("Other batch shouldn't be used with Sqlite db."), }, + #[cfg(feature = "wasm-db")] + AnyDatabase::LocalStorage(db) => match batch { + AnyBatch::LocalStorage(batch) => db.commit_batch(batch), + _ => unimplemented!("Other batch shouldn't be used with LocalStorage db."), + }, } } } @@ -402,6 +421,10 @@ pub enum AnyDatabaseConfig { #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] /// Sqlite embedded database using [`rusqlite`] Sqlite(SqliteDbConfiguration), + #[cfg(feature = "wasm-db")] + #[cfg_attr(docsrs, doc(cfg(feature = "wasm-db")))] + /// Browser LocalStorage database based on [`gloo_storage`] + LocalStorage(()), } impl ConfigurableDatabase for AnyDatabase { @@ -418,6 +441,10 @@ impl ConfigurableDatabase for AnyDatabase { AnyDatabaseConfig::Sqlite(inner) => { AnyDatabase::Sqlite(sqlite::SqliteDatabase::from_config(inner)?) } + #[cfg(feature = "wasm-db")] + AnyDatabaseConfig::LocalStorage(inner) => { + AnyDatabase::LocalStorage(localstorage::LocalStorageDatabase::from_config(inner)?) + } }) } } @@ -425,3 +452,4 @@ impl ConfigurableDatabase for AnyDatabase { impl_from!((), AnyDatabaseConfig, Memory,); impl_from!(SledDbConfiguration, AnyDatabaseConfig, Sled, #[cfg(feature = "key-value-db")]); impl_from!(SqliteDbConfiguration, AnyDatabaseConfig, Sqlite, #[cfg(feature = "sqlite")]); +impl_from!((), AnyDatabaseConfig, LocalStorage, #[cfg(feature = "wasm-db")]); diff --git a/src/database/localstorage.rs b/src/database/localstorage.rs new file mode 100644 index 000000000..4ffb4dd61 --- /dev/null +++ b/src/database/localstorage.rs @@ -0,0 +1,469 @@ +use std::collections::HashMap; + +use bitcoin::consensus::deserialize; +use bitcoin::consensus::encode::serialize; +use bitcoin::hash_types::Txid; +use bitcoin::hashes::hex::{FromHex, ToHex}; +use bitcoin::{OutPoint, Script, Transaction}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +use gloo_storage::{LocalStorage, Storage}; + +use crate::database::memory::MapKey; +use crate::database::{BatchDatabase, BatchOperations, ConfigurableDatabase, Database, SyncTime}; +use crate::{Error, KeychainKind, LocalUtxo, TransactionDetails}; + +#[derive(Debug, Default)] +pub struct LocalStorageDatabase {} + +impl ConfigurableDatabase for LocalStorageDatabase { + type Config = (); + + fn from_config(_config: &Self::Config) -> Result { + Ok(LocalStorageDatabase::default()) + } +} + +impl LocalStorageDatabase { + // A wrapper for LocalStorage::set that converts the error to Error + fn set(&self, key: impl AsRef, value: T) -> Result<(), Error> + where + T: Serialize, + { + LocalStorage::set(key, value).map_err(|_| Error::Generic("Storage error".to_string())) + } + + // mostly a copy of LocalStorage::get_all() + fn scan_prefix(&self, prefix: Vec) -> Map { + let local_storage = LocalStorage::raw(); + let length = LocalStorage::length(); + let mut map = Map::with_capacity(length as usize); + for index in 0..length { + let key_opt: Option = local_storage.key(index).unwrap(); + + if let Some(key) = key_opt { + if key.starts_with(&prefix.to_hex()) { + let value: Value = LocalStorage::get(&key).unwrap(); + map.insert(key, value); + } + } + } + + map + } +} + +impl BatchOperations for LocalStorageDatabase { + fn set_script_pubkey( + &mut self, + script: &Script, + keychain: KeychainKind, + path: u32, + ) -> Result<(), Error> { + let key = MapKey::Path((Some(keychain), Some(path))).as_map_key(); + self.set(key, script.clone())?; + + let key = MapKey::Script(Some(script)).as_map_key(); + let spk_info = ScriptPubKeyInfo { keychain, path }; + self.set(key, spk_info)?; + + Ok(()) + } + + fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> { + let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key(); + self.set(key, utxo)?; + + Ok(()) + } + fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> { + let key = MapKey::RawTx(Some(&transaction.txid())).as_map_key(); + self.set(key, transaction.clone())?; + + Ok(()) + } + fn set_tx(&mut self, transaction: &TransactionDetails) -> Result<(), Error> { + let key = MapKey::Transaction(Some(&transaction.txid)).as_map_key(); + + // insert the raw_tx if present + if let Some(ref tx) = transaction.transaction { + self.set_raw_tx(tx)?; + } + + // remove the raw tx from the serialized version + let mut transaction = transaction.clone(); + transaction.transaction = None; + + self.set(key, transaction)?; + + Ok(()) + } + fn set_last_index(&mut self, keychain: KeychainKind, value: u32) -> Result<(), Error> { + let key = MapKey::LastIndex(keychain).as_map_key(); + self.set(key, value)?; + + Ok(()) + } + fn set_sync_time(&mut self, data: SyncTime) -> Result<(), Error> { + let key = MapKey::SyncTime.as_map_key(); + self.set(key, data)?; + + Ok(()) + } + + fn del_script_pubkey_from_path( + &mut self, + keychain: KeychainKind, + path: u32, + ) -> Result, Error> { + let key = MapKey::Path((Some(keychain), Some(path))).as_map_key(); + let res: Option