feat: Add basic project structure and initial crates
- Added basic project structure with workspace and crates: `kvstore`, `vault`, `evm_client`, `cli_app`, `web_app`. - Created initial `Cargo.toml` files for each crate. - Added placeholder implementations for key components. - Included initial documentation files (`README.md`, architecture docs, repo structure). - Included initial implementaion for kvstore crate(async API, backend abstraction, separation of concerns, WASM/native support, testability) - Included native and browser tests for the kvstore crate
This commit is contained in:
		
							
								
								
									
										24
									
								
								kvstore/src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								kvstore/src/error.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
//! Error types for the kvstore crate
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Error)]
 | 
			
		||||
pub enum KVError {
 | 
			
		||||
    #[error("I/O error: {0}")]
 | 
			
		||||
    Io(#[from] std::io::Error),
 | 
			
		||||
    #[error("Key not found: {0}")]
 | 
			
		||||
    KeyNotFound(String),
 | 
			
		||||
    #[error("Store not found: {0}")]
 | 
			
		||||
    StoreNotFound(String),
 | 
			
		||||
    #[error("Serialization error: {0}")]
 | 
			
		||||
    Serialization(String),
 | 
			
		||||
    #[error("Deserialization error: {0}")]
 | 
			
		||||
    Deserialization(String),
 | 
			
		||||
    #[error("Encryption error: {0}")]
 | 
			
		||||
    Encryption(String),
 | 
			
		||||
    #[error("Decryption error: {0}")]
 | 
			
		||||
    Decryption(String),
 | 
			
		||||
    #[error("Other error: {0}")]
 | 
			
		||||
    Other(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub type Result<T> = std::result::Result<T, KVError>;
 | 
			
		||||
							
								
								
									
										18
									
								
								kvstore/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								kvstore/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
/// Error types for the kvstore crate
 | 
			
		||||
pub mod error;
 | 
			
		||||
/// Async trait for key-value storage
 | 
			
		||||
pub mod traits;
 | 
			
		||||
/// Native backend (e.g., sled or slatedb)
 | 
			
		||||
#[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
pub mod native;
 | 
			
		||||
/// WASM backend (IndexedDB)
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
pub mod wasm;
 | 
			
		||||
 | 
			
		||||
pub use error::{KVError, Result};
 | 
			
		||||
pub use traits::KVStore;
 | 
			
		||||
 | 
			
		||||
#[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
pub use native::NativeStore;
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
pub use wasm::WasmStore;
 | 
			
		||||
							
								
								
									
										106
									
								
								kvstore/src/native.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								kvstore/src/native.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
//! Native backend for kvstore using sled
 | 
			
		||||
//!
 | 
			
		||||
//! # Runtime Requirement
 | 
			
		||||
//!
 | 
			
		||||
//! **A Tokio runtime must be running to use this backend.**
 | 
			
		||||
//! This library does not start or manage a runtime; it assumes that all async methods are called from within an existing Tokio runtime context (e.g., via `#[tokio::main]` or `tokio::test`).
 | 
			
		||||
//!
 | 
			
		||||
//! All blocking I/O is offloaded using `tokio::task::spawn_blocking`.
 | 
			
		||||
//!
 | 
			
		||||
//! # Example
 | 
			
		||||
//!
 | 
			
		||||
 | 
			
		||||
use crate::traits::KVStore;
 | 
			
		||||
use crate::error::{KVError, Result};
 | 
			
		||||
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
use sled::Db;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct NativeStore {
 | 
			
		||||
    db: Arc<Db>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl NativeStore {
 | 
			
		||||
    pub fn open(path: &str) -> Result<Self> {
 | 
			
		||||
        let db = sled::open(path).map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
 | 
			
		||||
        Ok(Self { db: Arc::new(db) })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
#[async_trait]
 | 
			
		||||
impl KVStore for NativeStore {
 | 
			
		||||
    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
 | 
			
		||||
        let db = self.db.clone();
 | 
			
		||||
        let key = key.to_owned();
 | 
			
		||||
        tokio::task::spawn_blocking(move || {
 | 
			
		||||
            db.get(&key)
 | 
			
		||||
                .map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))
 | 
			
		||||
                .map(|opt| opt.map(|ivec| ivec.to_vec()))
 | 
			
		||||
        })
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|e| KVError::Other(format!("Join error: {e}")))?
 | 
			
		||||
    }
 | 
			
		||||
    async fn set(&self, key: &str, value: &[u8]) -> Result<()> {
 | 
			
		||||
        let db = self.db.clone();
 | 
			
		||||
        let key = key.to_owned();
 | 
			
		||||
        let value = value.to_vec();
 | 
			
		||||
        tokio::task::spawn_blocking(move || {
 | 
			
		||||
            db.insert(&key, value)
 | 
			
		||||
                .map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
 | 
			
		||||
            db.flush().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
 | 
			
		||||
            Ok(())
 | 
			
		||||
        })
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|e| KVError::Other(format!("Join error: {e}")))?
 | 
			
		||||
    }
 | 
			
		||||
    async fn remove(&self, key: &str) -> Result<()> {
 | 
			
		||||
        let db = self.db.clone();
 | 
			
		||||
        let key = key.to_owned();
 | 
			
		||||
        tokio::task::spawn_blocking(move || {
 | 
			
		||||
            db.remove(&key)
 | 
			
		||||
                .map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
 | 
			
		||||
            db.flush().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
 | 
			
		||||
            Ok(())
 | 
			
		||||
        })
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|e| KVError::Other(format!("Join error: {e}")))?
 | 
			
		||||
    }
 | 
			
		||||
    async fn contains_key(&self, key: &str) -> Result<bool> {
 | 
			
		||||
        let db = self.db.clone();
 | 
			
		||||
        let key = key.to_owned();
 | 
			
		||||
        tokio::task::spawn_blocking(move || {
 | 
			
		||||
            Ok(db.contains_key(&key)
 | 
			
		||||
                .map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?)
 | 
			
		||||
        })
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|e| KVError::Other(format!("Join error: {e}")))?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn keys(&self) -> Result<Vec<String>> {
 | 
			
		||||
        let db = self.db.clone();
 | 
			
		||||
        tokio::task::spawn_blocking(move || {
 | 
			
		||||
            let mut keys = Vec::new();
 | 
			
		||||
            for result in db.iter() {
 | 
			
		||||
                let (key, _) = result.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
 | 
			
		||||
                keys.push(String::from_utf8_lossy(&key).to_string());
 | 
			
		||||
            }
 | 
			
		||||
            Ok(keys)
 | 
			
		||||
        })
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|e| KVError::Other(format!("Join error: {e}")))?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn clear(&self) -> Result<()> {
 | 
			
		||||
        let db = self.db.clone();
 | 
			
		||||
        tokio::task::spawn_blocking(move || {
 | 
			
		||||
            db.clear().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
 | 
			
		||||
            db.flush().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
 | 
			
		||||
            Ok(())
 | 
			
		||||
        })
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|e| KVError::Other(format!("Join error: {e}")))?
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								kvstore/src/traits.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								kvstore/src/traits.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
//! Async trait for key-value storage
 | 
			
		||||
 | 
			
		||||
use crate::error::Result;
 | 
			
		||||
 | 
			
		||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
 | 
			
		||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
 | 
			
		||||
/// Async key-value store interface.
 | 
			
		||||
///
 | 
			
		||||
/// For native (non-wasm32) backends, implementers should be `Send + Sync` to support runtime-agnostic async usage.
 | 
			
		||||
/// For WASM (wasm32) backends, `Send + Sync` is not required and types may not implement them.
 | 
			
		||||
///
 | 
			
		||||
/// Methods:
 | 
			
		||||
/// - get
 | 
			
		||||
/// - set
 | 
			
		||||
/// - remove (was delete)
 | 
			
		||||
/// - contains_key (was exists)
 | 
			
		||||
/// - keys
 | 
			
		||||
/// - clear
 | 
			
		||||
pub trait KVStore {
 | 
			
		||||
    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
 | 
			
		||||
    async fn set(&self, key: &str, value: &[u8]) -> Result<()>;
 | 
			
		||||
    async fn remove(&self, key: &str) -> Result<()>;
 | 
			
		||||
    async fn contains_key(&self, key: &str) -> Result<bool>;
 | 
			
		||||
    async fn keys(&self) -> Result<Vec<String>>;
 | 
			
		||||
    async fn clear(&self) -> Result<()>;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								kvstore/src/wasm.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								kvstore/src/wasm.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
//! WASM backend for kvstore using IndexedDB (idb crate)
 | 
			
		||||
//!
 | 
			
		||||
//! # Platform
 | 
			
		||||
//!
 | 
			
		||||
//! This backend is only available when compiling for `wasm32` (browser/WebAssembly).
 | 
			
		||||
//! It uses the `idb` crate for async IndexedDB operations.
 | 
			
		||||
//!
 | 
			
		||||
//! # Usage
 | 
			
		||||
//!
 | 
			
		||||
//! This implementation is designed to run inside a browser environment and is not supported on native targets.
 | 
			
		||||
//!
 | 
			
		||||
//! # Example
 | 
			
		||||
//!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
use crate::traits::KVStore;
 | 
			
		||||
use crate::error::{KVError, Result};
 | 
			
		||||
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
use idb::{Database, TransactionMode, Factory};
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
use wasm_bindgen::JsValue;
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
use js_sys::Uint8Array;
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
const STORE_NAME: &str = "kv";
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
pub struct WasmStore {
 | 
			
		||||
    db: Database,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
impl WasmStore {
 | 
			
		||||
    pub async fn open(name: &str) -> Result<Self> {
 | 
			
		||||
        let factory = Factory::new().map_err(|e| KVError::Other(format!("IndexedDB factory error: {e:?}")))?;
 | 
			
		||||
        let mut open_req = factory.open(name, None)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("IndexedDB factory open error: {e:?}")))?;
 | 
			
		||||
        open_req.on_upgrade_needed(|event| {
 | 
			
		||||
            let db = event.database().expect("Failed to get database in upgrade event");
 | 
			
		||||
            if !db.store_names().iter().any(|n| n == STORE_NAME) {
 | 
			
		||||
                db.create_object_store(STORE_NAME, Default::default()).unwrap();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        let db = open_req.await.map_err(|e| KVError::Other(format!("IndexedDB open error: {e:?}")))?;
 | 
			
		||||
        Ok(Self { db })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
#[async_trait(?Send)]
 | 
			
		||||
impl KVStore for WasmStore {
 | 
			
		||||
    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
 | 
			
		||||
        let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadOnly)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
 | 
			
		||||
        let store = tx.object_store(STORE_NAME)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
 | 
			
		||||
        use idb::Query;
 | 
			
		||||
        let val = store.get(Query::from(JsValue::from_str(key))).await
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb get await error: {e:?}")))?;
 | 
			
		||||
        if let Some(jsval) = val {
 | 
			
		||||
            let arr = Uint8Array::new(&jsval);
 | 
			
		||||
            Ok(Some(arr.to_vec()))
 | 
			
		||||
        } else {
 | 
			
		||||
            Ok(None)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    async fn set(&self, key: &str, value: &[u8]) -> Result<()> {
 | 
			
		||||
        let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
 | 
			
		||||
        let store = tx.object_store(STORE_NAME)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
 | 
			
		||||
        store.put(&Uint8Array::from(value).into(), Some(&JsValue::from_str(key))).await
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb put await error: {e:?}")))?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    async fn remove(&self, key: &str) -> Result<()> {
 | 
			
		||||
        let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
 | 
			
		||||
        let store = tx.object_store(STORE_NAME)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
 | 
			
		||||
        use idb::Query;
 | 
			
		||||
        store.delete(Query::from(JsValue::from_str(key))).await
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb delete await error: {e:?}")))?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    async fn contains_key(&self, key: &str) -> Result<bool> {
 | 
			
		||||
        Ok(self.get(key).await?.is_some())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn keys(&self) -> Result<Vec<String>> {
 | 
			
		||||
        let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadOnly)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
 | 
			
		||||
        let store = tx.object_store(STORE_NAME)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
 | 
			
		||||
        let js_keys = store.get_all_keys(None, None).await
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb get_all_keys error: {e:?}")))?;
 | 
			
		||||
        let arr = js_sys::Array::from(&JsValue::from(js_keys));
 | 
			
		||||
        let mut keys = Vec::new();
 | 
			
		||||
        for i in 0..arr.length() {
 | 
			
		||||
            if let Some(s) = arr.get(i).as_string() {
 | 
			
		||||
                keys.push(s);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(keys)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn clear(&self) -> Result<()> {
 | 
			
		||||
        let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
 | 
			
		||||
        let store = tx.object_store(STORE_NAME)
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
 | 
			
		||||
        store.clear().await
 | 
			
		||||
            .map_err(|e| KVError::Other(format!("idb clear error: {e:?}")))?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
pub struct WasmStore;
 | 
			
		||||
#[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
#[async_trait]
 | 
			
		||||
impl KVStore for WasmStore {
 | 
			
		||||
    async fn get(&self, _key: &str) -> Result<Option<Vec<u8>>> {
 | 
			
		||||
        Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
 | 
			
		||||
    }
 | 
			
		||||
    async fn set(&self, _key: &str, _value: &[u8]) -> Result<()> {
 | 
			
		||||
        Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
 | 
			
		||||
    }
 | 
			
		||||
    async fn delete(&self, _key: &str) -> Result<()> {
 | 
			
		||||
        Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
 | 
			
		||||
    }
 | 
			
		||||
    async fn exists(&self, _key: &str) -> Result<bool> {
 | 
			
		||||
        Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user