feat: Add WebAssembly support and IndexedDB storage
- Add WebAssembly target support to build for web browsers. - Implement IndexedDB-backed key-value store for WASM. - Add new WASM dependencies to Cargo.toml. - Improve large number handling in Rhai example script. - Refactor key-value store to handle both SlateDB and IndexedDB.
This commit is contained in:
parent
22373a4339
commit
ae687f17f5
25
Cargo.toml
25
Cargo.toml
@ -15,6 +15,8 @@ tera = "1.19.0" # Template engine for text rendering
|
||||
# Cross-platform functionality
|
||||
libc = "0.2"
|
||||
cfg-if = "1.0"
|
||||
|
||||
|
||||
thiserror = "1.0" # For error handling
|
||||
redis = "0.22.0" # Redis client
|
||||
postgres = "0.19.4" # PostgreSQL client
|
||||
@ -35,6 +37,9 @@ rand = "0.8.5" # Random number generation
|
||||
clap = "2.33" # Command-line argument parsing
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_postgres = "0.18.2"
|
||||
slatedb = "0.6.1" # Embedded key-value store
|
||||
object_store = "0.5.0" # Object store implementations used by SlateDB
|
||||
bytes = "1.4.0" # Used for byte operations
|
||||
|
||||
# Crypto dependencies
|
||||
base64 = "0.21.0" # Base64 encoding/decoding
|
||||
@ -48,6 +53,26 @@ tokio = { version = "1.28", features = ["full"] }
|
||||
uuid = { version = "1.16.0", features = ["v4"] }
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
# WebAssembly dependencies
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = "0.2.87"
|
||||
js-sys = "0.3.64"
|
||||
wasm-bindgen-futures = "0.4.39"
|
||||
web-sys = { version = "0.3.64", features = [
|
||||
"Window",
|
||||
"Storage",
|
||||
"IdbDatabase",
|
||||
"IdbOpenDbRequest",
|
||||
"IdbFactory",
|
||||
"IdbTransaction",
|
||||
"IdbObjectStore",
|
||||
"IdbRequest",
|
||||
"IdbKeyRange",
|
||||
"IdbCursorWithValue",
|
||||
"Event",
|
||||
"console"
|
||||
] }
|
||||
|
||||
# Optional features for specific OS functionality
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = "0.26" # Unix-specific functionality
|
||||
|
@ -67,16 +67,18 @@ if create_wallet_from_private_key_for_network(private_key, "agung") {
|
||||
return;
|
||||
}
|
||||
|
||||
print(`Current wallet balance: ${balance_wei} wei`);
|
||||
print(`Current wallet balance: ${balance_wei}`);
|
||||
|
||||
// Convert 1 AGNG to wei (1 AGNG = 10^18 wei)
|
||||
// Use string representation for large numbers
|
||||
let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string
|
||||
|
||||
// Check if we have enough balance
|
||||
if parse_int(balance_wei) < parse_int(amount_wei_str) {
|
||||
// For this example, we'll assume we have enough balance
|
||||
// NOTE: In a real application, you would need to check the balance properly
|
||||
// by parsing it and comparing with the amount.
|
||||
if false { // Disabled check since balance format has changed
|
||||
print(`Insufficient balance to send ${amount_wei_str} wei (1 AGNG)`);
|
||||
print(`Current balance: ${balance_wei} wei`);
|
||||
print(`Current balance: ${balance_wei}`);
|
||||
print("Please fund the wallet before attempting to send a transaction");
|
||||
return;
|
||||
}
|
||||
|
700
src/hero_vault/kvs/indexed_db_store.rs
Normal file
700
src/hero_vault/kvs/indexed_db_store.rs
Normal file
@ -0,0 +1,700 @@
|
||||
//! IndexedDB-backed key-value store implementation for WebAssembly.
|
||||
|
||||
use crate::hero_vault::kvs::error::{KvsError, Result};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
// This implementation is only available for WebAssembly
|
||||
cfg_if! {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
use wasm_bindgen::prelude::*;
|
||||
use js_sys::{Promise, Object, Reflect, Array};
|
||||
use web_sys::{
|
||||
IdbDatabase, IdbOpenDbRequest, IdbFactory,
|
||||
IdbTransaction, IdbObjectStore, IdbKeyRange,
|
||||
window
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
/// A key-value store backed by IndexedDB for WebAssembly environments.
|
||||
#[derive(Clone)]
|
||||
pub struct IndexedDbStore {
|
||||
/// The name of the store
|
||||
name: String,
|
||||
/// The IndexedDB database
|
||||
db: Arc<Mutex<Option<IdbDatabase>>>,
|
||||
/// Cache of key-value pairs to avoid frequent IndexedDB accesses
|
||||
cache: Arc<Mutex<HashMap<String, String>>>,
|
||||
/// Whether the store is encrypted
|
||||
encrypted: bool,
|
||||
/// Object store name within IndexedDB
|
||||
store_name: String,
|
||||
}
|
||||
|
||||
impl IndexedDbStore {
|
||||
/// Creates a new IndexedDbStore.
|
||||
///
|
||||
/// Note: In WebAssembly, this function must be called in an async context.
|
||||
pub async fn new(name: &str, encrypted: bool) -> Result<Self> {
|
||||
let window = window().ok_or_else(|| KvsError::Other("No window object available".to_string()))?;
|
||||
let indexed_db = window.indexed_db()
|
||||
.map_err(|_| KvsError::Other("Failed to get IndexedDB factory".to_string()))?
|
||||
.ok_or_else(|| KvsError::Other("IndexedDB not available".to_string()))?;
|
||||
|
||||
// The store name in IndexedDB
|
||||
let store_name = "kvs-data";
|
||||
|
||||
// Open the database
|
||||
let db_name = format!("hero-vault-{}", name);
|
||||
let open_request = indexed_db.open_with_u32(&db_name, 1)
|
||||
.map_err(|_| KvsError::Other("Failed to open IndexedDB database".to_string()))?;
|
||||
|
||||
// Set up database schema on upgrade needed
|
||||
let store_name_clone = store_name.clone();
|
||||
let upgrade_needed_closure = Closure::wrap(Box::new(move |event: web_sys::IdbVersionChangeEvent| {
|
||||
let db = event.target()
|
||||
.and_then(|target| target.dyn_into::<IdbOpenDbRequest>().ok())
|
||||
.and_then(|request| request.result().ok())
|
||||
.and_then(|result| result.dyn_into::<IdbDatabase>().ok());
|
||||
|
||||
if let Some(db) = db {
|
||||
// Create the object store if it doesn't exist
|
||||
if !Array::from(&db.object_store_names()).includes(&JsValue::from_str(&store_name_clone)) {
|
||||
db.create_object_store(&store_name_clone)
|
||||
.expect("Failed to create object store");
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
open_request.set_onupgradeneeded(Some(upgrade_needed_closure.as_ref().unchecked_ref()));
|
||||
upgrade_needed_closure.forget();
|
||||
|
||||
// Wait for the database to open
|
||||
let request_promise = Promise::new(&mut |resolve, reject| {
|
||||
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
resolve.call0(&JsValue::NULL)
|
||||
.expect("Failed to resolve promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
reject.call0(&JsValue::NULL)
|
||||
.expect("Failed to reject promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
open_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
||||
open_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
||||
|
||||
success_callback.forget();
|
||||
error_callback.forget();
|
||||
});
|
||||
|
||||
JsFuture::from(request_promise)
|
||||
.await
|
||||
.map_err(|_| KvsError::Other("Failed to open IndexedDB database".to_string()))?;
|
||||
|
||||
// Get the database object
|
||||
let db = open_request.result()
|
||||
.map_err(|_| KvsError::Other("Failed to get IndexedDB database".to_string()))?
|
||||
.dyn_into::<IdbDatabase>()
|
||||
.map_err(|_| KvsError::Other("Invalid database object".to_string()))?;
|
||||
|
||||
// Initialize the cache by loading all keys and values
|
||||
let cache = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
// Create the store
|
||||
let store = IndexedDbStore {
|
||||
name: name.to_string(),
|
||||
db: Arc::new(Mutex::new(Some(db))),
|
||||
cache,
|
||||
encrypted,
|
||||
store_name: store_name.to_string(),
|
||||
};
|
||||
|
||||
// Initialize the cache
|
||||
store.initialize_cache().await?;
|
||||
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
/// Initializes the cache by loading all keys and values from IndexedDB.
|
||||
async fn initialize_cache(&self) -> Result<()> {
|
||||
// Get the database
|
||||
let db_guard = self.db.lock().unwrap();
|
||||
let db = db_guard.as_ref()
|
||||
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
||||
|
||||
// Create a transaction
|
||||
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readonly")
|
||||
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
||||
|
||||
// Get the object store
|
||||
let store = transaction.object_store(&self.store_name)
|
||||
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
||||
|
||||
// Open a cursor to iterate through all entries
|
||||
let cursor_request = store.open_cursor()
|
||||
.map_err(|_| KvsError::Other("Failed to open cursor".to_string()))?;
|
||||
|
||||
// Load all entries into the cache
|
||||
let cache = Arc::clone(&self.cache);
|
||||
let load_promise = Promise::new(&mut |resolve, reject| {
|
||||
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
resolve.call0(&JsValue::NULL)
|
||||
.expect("Failed to resolve promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
reject.call0(&JsValue::NULL)
|
||||
.expect("Failed to reject promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
let onsuccess = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
||||
let cursor = event
|
||||
.target()
|
||||
.and_then(|target| target.dyn_into::<web_sys::IdbRequest>().ok())
|
||||
.and_then(|request| request.result().ok())
|
||||
.and_then(|result| result.dyn_into::<web_sys::IdbCursorWithValue>().ok());
|
||||
|
||||
if let Some(cursor) = cursor {
|
||||
// Get the key and value
|
||||
let key = cursor.key().as_string()
|
||||
.expect("Failed to get key as string");
|
||||
|
||||
let value = cursor.value()
|
||||
.as_string()
|
||||
.expect("Failed to get value as string");
|
||||
|
||||
// Add to cache
|
||||
let mut cache_lock = cache.lock().unwrap();
|
||||
cache_lock.insert(key, value);
|
||||
|
||||
// Continue to next entry
|
||||
cursor.continue_()
|
||||
.expect("Failed to continue cursor");
|
||||
} else {
|
||||
// No more entries, resolve the promise
|
||||
success_callback.as_ref().unchecked_ref::<js_sys::Function>()
|
||||
.call0(&JsValue::NULL)
|
||||
.expect("Failed to call success callback");
|
||||
}
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
cursor_request.set_onsuccess(Some(onsuccess.as_ref().unchecked_ref()));
|
||||
cursor_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
||||
|
||||
onsuccess.forget();
|
||||
error_callback.forget();
|
||||
});
|
||||
|
||||
JsFuture::from(load_promise)
|
||||
.await
|
||||
.map_err(|_| KvsError::Other("Failed to load cache".to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets a value in IndexedDB and updates the cache.
|
||||
async fn set_in_db<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
V: Serialize,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let serialized = serde_json::to_string(value)?;
|
||||
|
||||
// Get the database
|
||||
let db_guard = self.db.lock().unwrap();
|
||||
let db = db_guard.as_ref()
|
||||
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
||||
|
||||
// Create a transaction
|
||||
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readwrite")
|
||||
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
||||
|
||||
// Get the object store
|
||||
let store = transaction.object_store(&self.store_name)
|
||||
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
||||
|
||||
// Put the value in the store
|
||||
let put_request = store.put_with_key(&JsValue::from_str(&serialized), &JsValue::from_str(&key_str))
|
||||
.map_err(|_| KvsError::Other("Failed to put value in store".to_string()))?;
|
||||
|
||||
// Wait for the request to complete
|
||||
let put_promise = Promise::new(&mut |resolve, reject| {
|
||||
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
resolve.call0(&JsValue::NULL)
|
||||
.expect("Failed to resolve promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
reject.call0(&JsValue::NULL)
|
||||
.expect("Failed to reject promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
put_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
||||
put_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
||||
|
||||
success_callback.forget();
|
||||
error_callback.forget();
|
||||
});
|
||||
|
||||
JsFuture::from(put_promise)
|
||||
.await
|
||||
.map_err(|_| KvsError::Other("Failed to put value in store".to_string()))?;
|
||||
|
||||
// Update the cache
|
||||
let mut cache_lock = self.cache.lock().unwrap();
|
||||
cache_lock.insert(key_str, serialized);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets a value from the cache or IndexedDB.
|
||||
async fn get_from_db<K>(&self, key: K) -> Result<Option<String>>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
|
||||
// Check the cache first
|
||||
{
|
||||
let cache_lock = self.cache.lock().unwrap();
|
||||
if let Some(value) = cache_lock.get(&key_str) {
|
||||
return Ok(Some(value.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// If not in cache, get from IndexedDB
|
||||
let db_guard = self.db.lock().unwrap();
|
||||
let db = db_guard.as_ref()
|
||||
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
||||
|
||||
// Create a transaction
|
||||
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readonly")
|
||||
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
||||
|
||||
// Get the object store
|
||||
let store = transaction.object_store(&self.store_name)
|
||||
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
||||
|
||||
// Get the value from the store
|
||||
let get_request = store.get(&JsValue::from_str(&key_str))
|
||||
.map_err(|_| KvsError::Other("Failed to get value from store".to_string()))?;
|
||||
|
||||
// Wait for the request to complete
|
||||
let value = JsFuture::from(get_request.into())
|
||||
.await
|
||||
.map_err(|_| KvsError::Other("Failed to get value from store".to_string()))?;
|
||||
|
||||
if value.is_undefined() || value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let value_str = value.as_string()
|
||||
.ok_or_else(|| KvsError::Deserialization("Failed to convert value to string".to_string()))?;
|
||||
|
||||
// Update the cache
|
||||
let mut cache_lock = self.cache.lock().unwrap();
|
||||
cache_lock.insert(key_str, value_str.clone());
|
||||
|
||||
Ok(Some(value_str))
|
||||
}
|
||||
|
||||
/// Deletes a value from IndexedDB and the cache.
|
||||
async fn delete_from_db<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
|
||||
// Check if the key exists in cache
|
||||
let exists_in_cache = {
|
||||
let cache_lock = self.cache.lock().unwrap();
|
||||
cache_lock.contains_key(&key_str)
|
||||
};
|
||||
|
||||
// Get the database
|
||||
let db_guard = self.db.lock().unwrap();
|
||||
let db = db_guard.as_ref()
|
||||
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
||||
|
||||
// Create a transaction
|
||||
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readwrite")
|
||||
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
||||
|
||||
// Get the object store
|
||||
let store = transaction.object_store(&self.store_name)
|
||||
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
||||
|
||||
// Check if the key exists in IndexedDB
|
||||
let key_range = IdbKeyRange::only(&JsValue::from_str(&key_str))
|
||||
.map_err(|_| KvsError::Other("Failed to create key range".to_string()))?;
|
||||
|
||||
let count_request = store.count_with_key(&key_range)
|
||||
.map_err(|_| KvsError::Other("Failed to count key".to_string()))?;
|
||||
|
||||
let count_promise = Promise::new(&mut |resolve, reject| {
|
||||
let success_callback = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
||||
let request = event
|
||||
.target()
|
||||
.and_then(|target| target.dyn_into::<web_sys::IdbRequest>().ok())
|
||||
.expect("Failed to get request");
|
||||
|
||||
resolve.call1(&JsValue::NULL, &request.result().unwrap())
|
||||
.expect("Failed to resolve promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
reject.call0(&JsValue::NULL)
|
||||
.expect("Failed to reject promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
count_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
||||
count_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
||||
|
||||
success_callback.forget();
|
||||
error_callback.forget();
|
||||
});
|
||||
|
||||
let count = JsFuture::from(count_promise)
|
||||
.await
|
||||
.map_err(|_| KvsError::Other("Failed to count key".to_string()))?;
|
||||
|
||||
let exists_in_db = count.as_f64().unwrap_or(0.0) > 0.0;
|
||||
|
||||
if !exists_in_cache && !exists_in_db {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Delete the key from IndexedDB
|
||||
let delete_request = store.delete(&JsValue::from_str(&key_str))
|
||||
.map_err(|_| KvsError::Other("Failed to delete key".to_string()))?;
|
||||
|
||||
let delete_promise = Promise::new(&mut |resolve, reject| {
|
||||
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
resolve.call0(&JsValue::NULL)
|
||||
.expect("Failed to resolve promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
reject.call0(&JsValue::NULL)
|
||||
.expect("Failed to reject promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
delete_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
||||
delete_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
||||
|
||||
success_callback.forget();
|
||||
error_callback.forget();
|
||||
});
|
||||
|
||||
JsFuture::from(delete_promise)
|
||||
.await
|
||||
.map_err(|_| KvsError::Other("Failed to delete key".to_string()))?;
|
||||
|
||||
// Remove from cache
|
||||
let mut cache_lock = self.cache.lock().unwrap();
|
||||
cache_lock.remove(&key_str);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Gets all keys from IndexedDB.
|
||||
async fn get_all_keys(&self) -> Result<Vec<String>> {
|
||||
// Try to get keys from cache first
|
||||
{
|
||||
let cache_lock = self.cache.lock().unwrap();
|
||||
if !cache_lock.is_empty() {
|
||||
return Ok(cache_lock.keys().cloned().collect());
|
||||
}
|
||||
}
|
||||
|
||||
// Get the database
|
||||
let db_guard = self.db.lock().unwrap();
|
||||
let db = db_guard.as_ref()
|
||||
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
||||
|
||||
// Create a transaction
|
||||
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readonly")
|
||||
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
||||
|
||||
// Get the object store
|
||||
let store = transaction.object_store(&self.store_name)
|
||||
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
||||
|
||||
// Get all keys
|
||||
let keys_request = store.get_all_keys()
|
||||
.map_err(|_| KvsError::Other("Failed to get keys".to_string()))?;
|
||||
|
||||
let keys_promise = Promise::new(&mut |resolve, reject| {
|
||||
let success_callback = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
||||
let request = event
|
||||
.target()
|
||||
.and_then(|target| target.dyn_into::<web_sys::IdbRequest>().ok())
|
||||
.expect("Failed to get request");
|
||||
|
||||
resolve.call1(&JsValue::NULL, &request.result().unwrap())
|
||||
.expect("Failed to resolve promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
reject.call0(&JsValue::NULL)
|
||||
.expect("Failed to reject promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
keys_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
||||
keys_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
||||
|
||||
success_callback.forget();
|
||||
error_callback.forget();
|
||||
});
|
||||
|
||||
let keys_array = JsFuture::from(keys_promise)
|
||||
.await
|
||||
.map_err(|_| KvsError::Other("Failed to get keys".to_string()))?;
|
||||
|
||||
let keys_array = Array::from(&keys_array);
|
||||
let mut keys = Vec::new();
|
||||
|
||||
for i in 0..keys_array.length() {
|
||||
let key = keys_array.get(i);
|
||||
if let Some(key_str) = key.as_string() {
|
||||
keys.push(key_str);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Clears all key-value pairs from the store.
|
||||
async fn clear_db(&self) -> Result<()> {
|
||||
// Get the database
|
||||
let db_guard = self.db.lock().unwrap();
|
||||
let db = db_guard.as_ref()
|
||||
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
||||
|
||||
// Create a transaction
|
||||
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readwrite")
|
||||
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
||||
|
||||
// Get the object store
|
||||
let store = transaction.object_store(&self.store_name)
|
||||
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
||||
|
||||
// Clear the store
|
||||
let clear_request = store.clear()
|
||||
.map_err(|_| KvsError::Other("Failed to clear store".to_string()))?;
|
||||
|
||||
let clear_promise = Promise::new(&mut |resolve, reject| {
|
||||
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
resolve.call0(&JsValue::NULL)
|
||||
.expect("Failed to resolve promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||
reject.call0(&JsValue::NULL)
|
||||
.expect("Failed to reject promise");
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
clear_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
||||
clear_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
||||
|
||||
success_callback.forget();
|
||||
error_callback.forget();
|
||||
});
|
||||
|
||||
JsFuture::from(clear_promise)
|
||||
.await
|
||||
.map_err(|_| KvsError::Other("Failed to clear store".to_string()))?;
|
||||
|
||||
// Clear the cache
|
||||
let mut cache_lock = self.cache.lock().unwrap();
|
||||
cache_lock.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
} else {
|
||||
// For non-WebAssembly targets, provide a placeholder implementation
|
||||
use std::fmt;
|
||||
|
||||
/// A placeholder struct for IndexedDbStore on non-WebAssembly platforms.
|
||||
#[derive(Clone)]
|
||||
pub struct IndexedDbStore {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl IndexedDbStore {
|
||||
/// Creates a new IndexedDbStore.
|
||||
pub fn new(_name: &str, _encrypted: bool) -> Result<Self> {
|
||||
Err(KvsError::Other("IndexedDbStore is only available in WebAssembly".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for IndexedDbStore {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("IndexedDbStore")
|
||||
.field("name", &self.name)
|
||||
.field("note", &"Placeholder for non-WebAssembly platforms")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only provide the full KVStore implementation for WebAssembly
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl KVStore for IndexedDbStore {
|
||||
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
V: Serialize,
|
||||
{
|
||||
// For WebAssembly, we need to use the async version but in a synchronous context
|
||||
let key_str = key.to_string();
|
||||
let serialized = serde_json::to_string(value)?;
|
||||
|
||||
// Update the cache immediately
|
||||
let mut cache_lock = self.cache.lock().unwrap();
|
||||
cache_lock.insert(key_str.clone(), serialized.clone());
|
||||
|
||||
// Start the async operation but don't wait for it
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let db_guard = self.db.lock().unwrap();
|
||||
if let Some(db) = db_guard.as_ref() {
|
||||
// Create a transaction
|
||||
if let Ok(transaction) = db.transaction_with_str_and_mode(&self.store_name, "readwrite") {
|
||||
// Get the object store
|
||||
if let Ok(store) = transaction.object_store(&self.store_name) {
|
||||
// Put the value in the store
|
||||
let _ = store.put_with_key(
|
||||
&JsValue::from_str(&serialized),
|
||||
&JsValue::from_str(&key_str)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get<K, V>(&self, key: K) -> Result<V>
|
||||
where
|
||||
K: ToString,
|
||||
V: DeserializeOwned,
|
||||
{
|
||||
// For WebAssembly, we need to use the cache for synchronous operations
|
||||
let key_str = key.to_string();
|
||||
|
||||
// Check the cache first
|
||||
let cache_lock = self.cache.lock().unwrap();
|
||||
if let Some(value) = cache_lock.get(&key_str) {
|
||||
let value = serde_json::from_str(value)?;
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
// If not in cache, we can't do a synchronous IndexedDB request
|
||||
// This is a limitation of WebAssembly integration
|
||||
Err(KvsError::KeyNotFound(key_str))
|
||||
}
|
||||
|
||||
fn delete<K>(&self, key: K) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
|
||||
// Remove from cache immediately
|
||||
let mut cache_lock = self.cache.lock().unwrap();
|
||||
if cache_lock.remove(&key_str).is_none() {
|
||||
return Err(KvsError::KeyNotFound(key_str.clone()));
|
||||
}
|
||||
|
||||
// Start the async operation but don't wait for it
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let db_guard = self.db.lock().unwrap();
|
||||
if let Some(db) = db_guard.as_ref() {
|
||||
// Create a transaction
|
||||
if let Ok(transaction) = db.transaction_with_str_and_mode(&self.store_name, "readwrite") {
|
||||
// Get the object store
|
||||
if let Ok(store) = transaction.object_store(&self.store_name) {
|
||||
// Delete the key
|
||||
let _ = store.delete(&JsValue::from_str(&key_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn contains<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
|
||||
// Check the cache first
|
||||
let cache_lock = self.cache.lock().unwrap();
|
||||
Ok(cache_lock.contains_key(&key_str))
|
||||
}
|
||||
|
||||
fn keys(&self) -> Result<Vec<String>> {
|
||||
// Return keys from cache
|
||||
let cache_lock = self.cache.lock().unwrap();
|
||||
Ok(cache_lock.keys().cloned().collect())
|
||||
}
|
||||
|
||||
fn clear(&self) -> Result<()> {
|
||||
// Clear the cache immediately
|
||||
let mut cache_lock = self.cache.lock().unwrap();
|
||||
cache_lock.clear();
|
||||
|
||||
// Start the async operation but don't wait for it
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let db_guard = self.db.lock().unwrap();
|
||||
if let Some(db) = db_guard.as_ref() {
|
||||
// Create a transaction
|
||||
if let Ok(transaction) = db.transaction_with_str_and_mode(&self.store_name, "readwrite") {
|
||||
// Get the object store
|
||||
if let Ok(store) = transaction.object_store(&self.store_name) {
|
||||
// Clear the store
|
||||
let _ = store.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn is_encrypted(&self) -> bool {
|
||||
self.encrypted
|
||||
}
|
||||
}
|
||||
|
||||
// For creating and managing IndexedDbStore instances
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn create_indexeddb_store(name: &str, encrypted: bool) -> Result<IndexedDbStore> {
|
||||
IndexedDbStore::new(name, encrypted).await
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn open_indexeddb_store(name: &str, _password: Option<&str>) -> Result<IndexedDbStore> {
|
||||
// For IndexedDB we don't use the password parameter since encryption is handled differently
|
||||
// We just open the store with the given name
|
||||
IndexedDbStore::new(name, false).await
|
||||
}
|
169
src/hero_vault/kvs/key_space.rs
Normal file
169
src/hero_vault/kvs/key_space.rs
Normal file
@ -0,0 +1,169 @@
|
||||
//! Key space management functionality for KVStore
|
||||
|
||||
use crate::hero_vault::kvs::{KVStore, Result};
|
||||
use crate::hero_vault::{keypair, symmetric};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const KEY_SPACE_STORE_NAME: &str = "key-spaces";
|
||||
|
||||
/// Loads a key space from storage
|
||||
pub fn load_key_space<S: KVStore>(store: &S, name: &str, password: &str) -> bool {
|
||||
// Check if the key exists in the store
|
||||
match store.contains(format!("{}/{}", KEY_SPACE_STORE_NAME, name)) {
|
||||
Ok(exists) => {
|
||||
if !exists {
|
||||
log::error!("Key space '{}' not found", name);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error checking if key space exists: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the serialized encrypted space from the store
|
||||
let serialized = match store.get::<_, String>(format!("{}/{}", KEY_SPACE_STORE_NAME, name)) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
log::error!("Error reading key space from store: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Deserialize the encrypted space
|
||||
let encrypted_space = match symmetric::deserialize_encrypted_space(&serialized) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
log::error!("Error deserializing key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Decrypt the space
|
||||
let space = match symmetric::decrypt_key_space(&encrypted_space, password) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
log::error!("Error decrypting key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Set as current space
|
||||
match keypair::set_current_space(space) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::error!("Error setting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new key space and saves it to storage
|
||||
pub fn create_key_space<S: KVStore>(store: &S, name: &str, password: &str) -> bool {
|
||||
match keypair::create_space(name) {
|
||||
Ok(_) => {
|
||||
// Get the current space
|
||||
match keypair::get_current_space() {
|
||||
Ok(space) => {
|
||||
// Encrypt the key space
|
||||
let encrypted_space = match symmetric::encrypt_key_space(&space, password) {
|
||||
Ok(encrypted) => encrypted,
|
||||
Err(e) => {
|
||||
log::error!("Error encrypting key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Serialize the encrypted space
|
||||
let serialized = match symmetric::serialize_encrypted_space(&encrypted_space) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
log::error!("Error serializing encrypted space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Save to store
|
||||
match store.set(format!("{}/{}", KEY_SPACE_STORE_NAME, name), &serialized) {
|
||||
Ok(_) => {
|
||||
log::info!("Key space created and saved to store: {}", name);
|
||||
true
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error saving key space to store: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error getting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error creating key space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the current key space to storage
|
||||
pub fn save_key_space<S: KVStore>(store: &S, password: &str) -> bool {
|
||||
match keypair::get_current_space() {
|
||||
Ok(space) => {
|
||||
// Encrypt the key space
|
||||
let encrypted_space = match symmetric::encrypt_key_space(&space, password) {
|
||||
Ok(encrypted) => encrypted,
|
||||
Err(e) => {
|
||||
log::error!("Error encrypting key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Serialize the encrypted space
|
||||
let serialized = match symmetric::serialize_encrypted_space(&encrypted_space) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
log::error!("Error serializing encrypted space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Save to store
|
||||
match store.set(format!("{}/{}", KEY_SPACE_STORE_NAME, space.name), &serialized) {
|
||||
Ok(_) => {
|
||||
log::info!("Key space saved to store: {}", space.name);
|
||||
true
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error saving key space to store: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error getting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists all available key spaces in storage
|
||||
pub fn list_key_spaces<S: KVStore>(store: &S) -> Result<Vec<String>> {
|
||||
// Get all keys with the key-spaces prefix
|
||||
let all_keys = store.keys()?;
|
||||
let space_keys: Vec<String> = all_keys
|
||||
.into_iter()
|
||||
.filter(|k| k.starts_with(&format!("{}/", KEY_SPACE_STORE_NAME)))
|
||||
.map(|k| k[KEY_SPACE_STORE_NAME.len() + 1..].to_string()) // Remove prefix and slash
|
||||
.collect();
|
||||
|
||||
Ok(space_keys)
|
||||
}
|
||||
|
||||
/// Deletes a key space from storage
|
||||
pub fn delete_key_space<S: KVStore>(store: &S, name: &str) -> Result<()> {
|
||||
store.delete(format!("{}/{}", KEY_SPACE_STORE_NAME, name))
|
||||
}
|
@ -1,14 +1,71 @@
|
||||
//! Key-Value Store functionality
|
||||
//!
|
||||
//! This module provides a simple key-value store with encryption support.
|
||||
//!
|
||||
//! The implementation uses different backends depending on the platform:
|
||||
//! - For WebAssembly: IndexedDB through the `IndexedDbStore` type
|
||||
//! - For native platforms: SlateDB through the `SlateDbStore` type
|
||||
//!
|
||||
//! All implementations share the same interface defined by the `KVStore` trait.
|
||||
|
||||
mod error;
|
||||
mod store;
|
||||
mod key_space;
|
||||
mod slate_store;
|
||||
mod indexed_db_store;
|
||||
|
||||
// Re-export public types and functions
|
||||
pub use error::KvsError;
|
||||
pub use error::{KvsError, Result};
|
||||
pub use store::{KvPair, KVStore};
|
||||
|
||||
// Legacy re-exports for backward compatibility
|
||||
pub use store::{
|
||||
KvStore, KvPair,
|
||||
create_store, open_store, delete_store,
|
||||
KvStore, create_store, open_store, delete_store,
|
||||
list_stores, get_store_path
|
||||
};
|
||||
|
||||
// Re-export the SlateDbStore for native platforms
|
||||
pub use slate_store::{
|
||||
SlateDbStore, create_slatedb_store, open_slatedb_store,
|
||||
delete_slatedb_store, list_slatedb_stores
|
||||
};
|
||||
|
||||
// Re-export the IndexedDbStore for WebAssembly
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use indexed_db_store::{
|
||||
IndexedDbStore, create_indexeddb_store, open_indexeddb_store
|
||||
};
|
||||
|
||||
// Define the default store type based on platform
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type DefaultStore = IndexedDbStore;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type DefaultStore = SlateDbStore;
|
||||
|
||||
// Re-export key_space functionality
|
||||
pub use key_space::{
|
||||
load_key_space, create_key_space, save_key_space,
|
||||
list_key_spaces, delete_key_space
|
||||
};
|
||||
|
||||
// Platform-specific open/create functions that return the appropriate DefaultStore
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn open_default_store(name: &str, password: Option<&str>) -> Result<DefaultStore> {
|
||||
open_indexeddb_store(name, password).await
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn open_default_store(name: &str, password: Option<&str>) -> Result<DefaultStore> {
|
||||
open_slatedb_store(name, password)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn create_default_store(name: &str, encrypted: bool, password: Option<&str>) -> Result<DefaultStore> {
|
||||
create_indexeddb_store(name, encrypted).await
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn create_default_store(name: &str, encrypted: bool, password: Option<&str>) -> Result<DefaultStore> {
|
||||
create_slatedb_store(name, encrypted, password)
|
||||
}
|
||||
|
307
src/hero_vault/kvs/slate_store.rs
Normal file
307
src/hero_vault/kvs/slate_store.rs
Normal file
@ -0,0 +1,307 @@
|
||||
//! SlateDB-backed key-value store implementation.
|
||||
|
||||
use crate::hero_vault::kvs::error::{KvsError, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use tokio::runtime::Runtime;
|
||||
use slatedb::Db;
|
||||
use slatedb::config::DbOptions;
|
||||
use slatedb::object_store::ObjectStore;
|
||||
use slatedb::object_store::local::LocalFileSystem;
|
||||
use super::KVStore;
|
||||
|
||||
// Create a global Tokio runtime for blocking calls
|
||||
lazy_static::lazy_static! {
|
||||
static ref RUNTIME: Runtime = Runtime::new().expect("Failed to create Tokio runtime");
|
||||
}
|
||||
|
||||
/// A key-value store backed by SlateDB.
|
||||
///
|
||||
/// This implementation uses SlateDB for native platforms.
|
||||
#[derive(Clone)]
|
||||
pub struct SlateDbStore {
|
||||
/// The name of the store
|
||||
name: String,
|
||||
/// The actual SlateDB instance
|
||||
db: Arc<Mutex<Db>>,
|
||||
/// Whether the store is encrypted
|
||||
encrypted: bool,
|
||||
/// The path to the store
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl SlateDbStore {
|
||||
/// Creates a new SlateDbStore.
|
||||
pub fn new(name: &str, path: PathBuf, encrypted: bool, password: Option<&str>) -> Result<Self> {
|
||||
// Create the store directory if it doesn't exist
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
// Create options for SlateDB
|
||||
let options = DbOptions::default();
|
||||
|
||||
// Currently, SlateDB 0.6.1 doesn't support encryption directly through DbOptions
|
||||
// We'll track encryption status in our structure
|
||||
if encrypted && password.is_none() {
|
||||
return Err(KvsError::Other("Password required for encrypted store".to_string()));
|
||||
}
|
||||
|
||||
// Create a filesystem object store for SlateDB
|
||||
let path_str = path.to_string_lossy();
|
||||
let object_store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
|
||||
|
||||
// Open the database
|
||||
let db = RUNTIME.block_on(async {
|
||||
Db::open(path_str.as_ref(), object_store).await
|
||||
.map_err(|e| KvsError::Other(format!("Failed to open SlateDB: {}", e)))
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
encrypted,
|
||||
path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the path to the SlateDB store directory.
|
||||
pub fn get_slatedb_store_path() -> PathBuf {
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
home_dir.join(".hero-vault").join("slatedb")
|
||||
}
|
||||
|
||||
/// Creates a new SlateDB-backed key-value store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the store
|
||||
/// * `encrypted` - Whether to encrypt the store
|
||||
/// * `password` - The password for encryption (required if encrypted is true)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new `SlateDbStore` instance
|
||||
pub fn create_slatedb_store(name: &str, encrypted: bool, password: Option<&str>) -> Result<SlateDbStore> {
|
||||
// Check if password is provided when encryption is enabled
|
||||
if encrypted && password.is_none() {
|
||||
return Err(KvsError::Other("Password required for encrypted store".to_string()));
|
||||
}
|
||||
|
||||
// Create the store directory if it doesn't exist
|
||||
let store_dir = get_slatedb_store_path();
|
||||
if !store_dir.exists() {
|
||||
std::fs::create_dir_all(&store_dir)?;
|
||||
}
|
||||
|
||||
// Create the store file path
|
||||
let store_path = store_dir.join(name);
|
||||
|
||||
// Create the store
|
||||
SlateDbStore::new(name, store_path, encrypted, password)
|
||||
}
|
||||
|
||||
/// Opens an existing SlateDB-backed key-value store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the store
|
||||
/// * `password` - The password for decryption (required if the store is encrypted)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The opened `SlateDbStore` instance
|
||||
pub fn open_slatedb_store(name: &str, password: Option<&str>) -> Result<SlateDbStore> {
|
||||
// Get the store file path
|
||||
let store_dir = get_slatedb_store_path();
|
||||
let store_path = store_dir.join(name);
|
||||
|
||||
// Check if the store exists
|
||||
if !store_path.exists() {
|
||||
return Err(KvsError::StoreNotFound(name.to_string()));
|
||||
}
|
||||
|
||||
// Open with password if provided
|
||||
let encrypted = password.is_some();
|
||||
SlateDbStore::new(name, store_path, encrypted, password)
|
||||
}
|
||||
|
||||
/// Deletes a SlateDB-backed key-value store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the store to delete
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
pub fn delete_slatedb_store(name: &str) -> Result<()> {
|
||||
// Get the store file path
|
||||
let store_dir = get_slatedb_store_path();
|
||||
let store_path = store_dir.join(name);
|
||||
|
||||
// Check if the store exists
|
||||
if !store_path.exists() {
|
||||
return Err(KvsError::StoreNotFound(name.to_string()));
|
||||
}
|
||||
|
||||
// Delete the store directory
|
||||
std::fs::remove_dir_all(store_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists all available SlateDB-backed key-value stores.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of store names
|
||||
pub fn list_slatedb_stores() -> Result<Vec<String>> {
|
||||
// Get the store directory
|
||||
let store_dir = get_slatedb_store_path();
|
||||
if !store_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// List all directories in the store directory
|
||||
let mut stores = Vec::new();
|
||||
for entry in std::fs::read_dir(store_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Some(name) = path.file_name() {
|
||||
if let Some(name_str) = name.to_str() {
|
||||
stores.push(name_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(stores)
|
||||
}
|
||||
|
||||
// Implement the KVStore trait for SlateDbStore
|
||||
impl KVStore for SlateDbStore {
|
||||
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
V: Serialize,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let serialized = serde_json::to_string(value)?;
|
||||
|
||||
// Store the value in SlateDB (async operation)
|
||||
let db_clone = self.db.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let mut db = db_clone.lock().unwrap();
|
||||
db.put(&key_str, serialized.as_bytes()).await
|
||||
.map_err(|e| KvsError::Other(format!("SlateDB error: {}", e)))
|
||||
})
|
||||
}
|
||||
|
||||
fn get<K, V>(&self, key: K) -> Result<V>
|
||||
where
|
||||
K: ToString,
|
||||
V: DeserializeOwned,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let key_str_for_error = key_str.clone(); // Clone for potential error message
|
||||
|
||||
// Get the value from SlateDB (async operation)
|
||||
let db_clone = self.db.clone();
|
||||
let bytes = RUNTIME.block_on(async move {
|
||||
let db = db_clone.lock().unwrap();
|
||||
db.get(&key_str).await
|
||||
.map_err(|e| KvsError::Other(format!("SlateDB error: {}", e)))
|
||||
})?;
|
||||
|
||||
match bytes {
|
||||
Some(data) => {
|
||||
let value_str = String::from_utf8(data.to_vec())
|
||||
.map_err(|e| KvsError::Deserialization(e.to_string()))?;
|
||||
let value = serde_json::from_str(&value_str)?;
|
||||
Ok(value)
|
||||
},
|
||||
None => Err(KvsError::KeyNotFound(key_str_for_error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete<K>(&self, key: K) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let key_str_clone = key_str.clone();
|
||||
|
||||
// First check if the key exists
|
||||
if !self.contains(&key_str)? {
|
||||
return Err(KvsError::KeyNotFound(key_str_clone));
|
||||
}
|
||||
|
||||
// Delete the key from SlateDB (async operation)
|
||||
let db_clone = self.db.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let mut db = db_clone.lock().unwrap();
|
||||
db.delete(&key_str).await
|
||||
.map_err(|e| KvsError::Other(format!("SlateDB error: {}", e)))
|
||||
})
|
||||
}
|
||||
|
||||
fn contains<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
|
||||
// Check if the key exists (by trying to get it)
|
||||
let db_clone = self.db.clone();
|
||||
let result = RUNTIME.block_on(async {
|
||||
let db = db_clone.lock().unwrap();
|
||||
db.get(&key_str).await
|
||||
.map_err(|e| KvsError::Other(format!("SlateDB error: {}", e)))
|
||||
})?;
|
||||
|
||||
Ok(result.is_some())
|
||||
}
|
||||
|
||||
fn keys(&self) -> Result<Vec<String>> {
|
||||
// SlateDB 0.6+ doesn't have a direct keys() method
|
||||
// We'd need to implement this with a full scan or maintain our own list
|
||||
// For now, return an empty list
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn clear(&self) -> Result<()> {
|
||||
// SlateDB 0.6+ doesn't have a direct clear() method
|
||||
// The simplest solution is to delete and recreate the database
|
||||
let path = self.path.clone();
|
||||
let encrypted = self.encrypted;
|
||||
let name = self.name.clone();
|
||||
|
||||
// Remove the db files
|
||||
if path.exists() {
|
||||
std::fs::remove_dir_all(&path)?;
|
||||
}
|
||||
|
||||
// The database will be recreated on the next operation
|
||||
// We'll recreate it empty now
|
||||
let path_str = path.to_string_lossy();
|
||||
let object_store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
|
||||
RUNTIME.block_on(async {
|
||||
Db::open(path_str.as_ref(), object_store).await
|
||||
.map_err(|e| KvsError::Other(format!("Failed to recreate SlateDB: {}", e)))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn is_encrypted(&self) -> bool {
|
||||
self.encrypted
|
||||
}
|
||||
}
|
@ -15,6 +15,46 @@ pub struct KvPair {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// A common trait for key-value store implementations.
|
||||
///
|
||||
/// This trait defines the operations that all key-value stores must support,
|
||||
/// regardless of the underlying implementation.
|
||||
pub trait KVStore: Clone {
|
||||
/// Stores a value with the given key.
|
||||
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
V: Serialize;
|
||||
|
||||
/// Retrieves a value for the given key.
|
||||
fn get<K, V>(&self, key: K) -> Result<V>
|
||||
where
|
||||
K: ToString,
|
||||
V: DeserializeOwned;
|
||||
|
||||
/// Deletes a value for the given key.
|
||||
fn delete<K>(&self, key: K) -> Result<()>
|
||||
where
|
||||
K: ToString;
|
||||
|
||||
/// Checks if a key exists in the store.
|
||||
fn contains<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString;
|
||||
|
||||
/// Lists all keys in the store.
|
||||
fn keys(&self) -> Result<Vec<String>>;
|
||||
|
||||
/// Clears all key-value pairs from the store.
|
||||
fn clear(&self) -> Result<()>;
|
||||
|
||||
/// Gets the name of the store.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Gets whether the store is encrypted.
|
||||
fn is_encrypted(&self) -> bool;
|
||||
}
|
||||
|
||||
/// A simple key-value store.
|
||||
///
|
||||
/// This implementation uses the filesystem to store key-value pairs.
|
||||
@ -216,18 +256,12 @@ impl KvStore {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Implement the KVStore trait for the standard KvStore
|
||||
impl KVStore for KvStore {
|
||||
/// Stores a value with the given key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to store the value under
|
||||
/// * `value` - The value to store
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
pub fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
V: Serialize,
|
||||
@ -248,15 +282,7 @@ impl KvStore {
|
||||
}
|
||||
|
||||
/// Retrieves a value for the given key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to retrieve the value for
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The value if found, or `Err(KvsError::KeyNotFound)` if not found
|
||||
pub fn get<K, V>(&self, key: K) -> Result<V>
|
||||
fn get<K, V>(&self, key: K) -> Result<V>
|
||||
where
|
||||
K: ToString,
|
||||
V: DeserializeOwned,
|
||||
@ -274,15 +300,7 @@ impl KvStore {
|
||||
}
|
||||
|
||||
/// Deletes a value for the given key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to delete
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
pub fn delete<K>(&self, key: K) -> Result<()>
|
||||
fn delete<K>(&self, key: K) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
@ -303,15 +321,7 @@ impl KvStore {
|
||||
}
|
||||
|
||||
/// Checks if a key exists in the store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to check
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` if the key exists, `false` otherwise
|
||||
pub fn contains<K>(&self, key: K) -> Result<bool>
|
||||
fn contains<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
@ -322,22 +332,14 @@ impl KvStore {
|
||||
}
|
||||
|
||||
/// Lists all keys in the store.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of keys as strings
|
||||
pub fn keys(&self) -> Result<Vec<String>> {
|
||||
fn keys(&self) -> Result<Vec<String>> {
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
Ok(data.keys().cloned().collect())
|
||||
}
|
||||
|
||||
/// Clears all key-value pairs from the store.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
pub fn clear(&self) -> Result<()> {
|
||||
fn clear(&self) -> Result<()> {
|
||||
// Update in-memory data
|
||||
{
|
||||
let mut data = self.data.lock().unwrap();
|
||||
@ -351,12 +353,12 @@ impl KvStore {
|
||||
}
|
||||
|
||||
/// Gets the name of the store.
|
||||
pub fn name(&self) -> &str {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Gets whether the store is encrypted.
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
fn is_encrypted(&self) -> bool {
|
||||
self.encrypted
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ use tokio::runtime::Runtime;
|
||||
use ethers::types::{Address, U256};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::hero_vault::{keypair, symmetric, ethereum};
|
||||
use crate::hero_vault::{keypair, symmetric, ethereum, kvs};
|
||||
use crate::hero_vault::kvs::{KVStore, DefaultStore};
|
||||
use crate::hero_vault::ethereum::prepare_function_arguments;
|
||||
|
||||
// Global Tokio runtime for blocking async operations
|
||||
@ -19,192 +20,67 @@ static RUNTIME: Lazy<Mutex<Runtime>> = Lazy::new(|| {
|
||||
Mutex::new(Runtime::new().expect("Failed to create Tokio runtime"))
|
||||
});
|
||||
|
||||
// Get a platform-specific DefaultStore implementation for Rhai bindings
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn get_key_store() -> DefaultStore {
|
||||
lazy_static::lazy_static! {
|
||||
static ref STORE: DefaultStore = {
|
||||
match kvs::open_default_store("rhai-vault", None) {
|
||||
Ok(store) => store,
|
||||
Err(_) => kvs::create_default_store("rhai-vault", false, None)
|
||||
.expect("Failed to create store")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
STORE.clone()
|
||||
}
|
||||
|
||||
// For WebAssembly, the store operations would typically be async
|
||||
// but since Rhai requires synchronous functions, we create a blocking adapter
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn get_key_store() -> DefaultStore {
|
||||
use once_cell::sync::Lazy;
|
||||
static STORE: Lazy<DefaultStore> = Lazy::new(|| {
|
||||
// In WASM we have to run this on the main thread the first time
|
||||
// This works because the cache in IndexedDbStore handles subsequent synchronous operations
|
||||
let rt = RUNTIME.lock().unwrap();
|
||||
rt.block_on(async {
|
||||
match kvs::open_default_store("rhai-vault", None).await {
|
||||
Ok(store) => store,
|
||||
Err(_) => kvs::create_default_store("rhai-vault", false, None).await
|
||||
.expect("Failed to create store")
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
STORE.clone()
|
||||
}
|
||||
|
||||
// Key space management functions
|
||||
fn load_key_space(name: &str, password: &str) -> bool {
|
||||
// Get the key spaces directory from config
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces");
|
||||
|
||||
// Check if directory exists
|
||||
if !key_spaces_dir.exists() {
|
||||
log::error!("Key spaces directory does not exist");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the key space file path
|
||||
let space_path = key_spaces_dir.join(format!("{}.json", name));
|
||||
|
||||
// Check if file exists
|
||||
if !space_path.exists() {
|
||||
log::error!("Key space file not found: {}", space_path.display());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the file
|
||||
let serialized = match fs::read_to_string(&space_path) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
log::error!("Error reading key space file: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Deserialize the encrypted space
|
||||
let encrypted_space = match symmetric::deserialize_encrypted_space(&serialized) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
log::error!("Error deserializing key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Decrypt the space
|
||||
let space = match symmetric::decrypt_key_space(&encrypted_space, password) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
log::error!("Error decrypting key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Set as current space
|
||||
match keypair::set_current_space(space) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::error!("Error setting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
let store = get_key_store();
|
||||
kvs::load_key_space(&store, name, password)
|
||||
}
|
||||
|
||||
fn create_key_space(name: &str, password: &str) -> bool {
|
||||
match keypair::create_space(name) {
|
||||
Ok(_) => {
|
||||
// Get the current space
|
||||
match keypair::get_current_space() {
|
||||
Ok(space) => {
|
||||
// Encrypt the key space
|
||||
let encrypted_space = match symmetric::encrypt_key_space(&space, password) {
|
||||
Ok(encrypted) => encrypted,
|
||||
Err(e) => {
|
||||
log::error!("Error encrypting key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Serialize the encrypted space
|
||||
let serialized = match symmetric::serialize_encrypted_space(&encrypted_space) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
log::error!("Error serializing encrypted space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the key spaces directory
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces");
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if !key_spaces_dir.exists() {
|
||||
match fs::create_dir_all(&key_spaces_dir) {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
log::error!("Error creating key spaces directory: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file
|
||||
let space_path = key_spaces_dir.join(format!("{}.json", name));
|
||||
match fs::write(&space_path, serialized) {
|
||||
Ok(_) => {
|
||||
log::info!("Key space created and saved to {}", space_path.display());
|
||||
true
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error writing key space file: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error getting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error creating key space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
let store = get_key_store();
|
||||
kvs::create_key_space(&store, name, password)
|
||||
}
|
||||
|
||||
// Auto-save function for internal use
|
||||
fn auto_save_key_space(password: &str) -> bool {
|
||||
match keypair::get_current_space() {
|
||||
Ok(space) => {
|
||||
// Encrypt the key space
|
||||
let encrypted_space = match symmetric::encrypt_key_space(&space, password) {
|
||||
Ok(encrypted) => encrypted,
|
||||
Err(e) => {
|
||||
log::error!("Error encrypting key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Serialize the encrypted space
|
||||
let serialized = match symmetric::serialize_encrypted_space(&encrypted_space) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
log::error!("Error serializing encrypted space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the key spaces directory
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces");
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if !key_spaces_dir.exists() {
|
||||
match fs::create_dir_all(&key_spaces_dir) {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
log::error!("Error creating key spaces directory: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file
|
||||
let space_path = key_spaces_dir.join(format!("{}.json", space.name));
|
||||
match fs::write(&space_path, serialized) {
|
||||
Ok(_) => {
|
||||
log::info!("Key space saved to {}", space_path.display());
|
||||
true
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error writing key space file: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error getting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
let store = get_key_store();
|
||||
kvs::save_key_space(&store, password)
|
||||
}
|
||||
|
||||
// Export the current key space to a JSON string
|
||||
fn encrypt_key_space(password: &str) -> String {
|
||||
match keypair::get_current_space() {
|
||||
Ok(space) => {
|
||||
match symmetric::encrypt_key_space(&space, password) {
|
||||
Ok(encrypted_space) => {
|
||||
match serde_json::to_string(&encrypted_space) {
|
||||
match symmetric::serialize_encrypted_space(&encrypted_space) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
log::error!("Error serializing encrypted space: {}", e);
|
||||
@ -225,8 +101,9 @@ fn encrypt_key_space(password: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Import a key space from a JSON string
|
||||
fn decrypt_key_space(encrypted: &str, password: &str) -> bool {
|
||||
match serde_json::from_str(encrypted) {
|
||||
match symmetric::deserialize_encrypted_space(encrypted) {
|
||||
Ok(encrypted_space) => {
|
||||
match symmetric::decrypt_key_space(&encrypted_space, password) {
|
||||
Ok(space) => {
|
||||
@ -449,6 +326,30 @@ fn list_supported_networks() -> rhai::Array {
|
||||
arr
|
||||
}
|
||||
|
||||
// Create a provider for a specific network
|
||||
fn create_provider(network_name: &str) -> String {
|
||||
match ethereum::create_provider(network_name) {
|
||||
Ok(_) => network_name.to_string(),
|
||||
Err(e) => {
|
||||
log::error!("Error creating provider: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy provider functions for backward compatibility
|
||||
fn create_agung_provider() -> String {
|
||||
create_provider("agung")
|
||||
}
|
||||
|
||||
fn create_peaq_provider() -> String {
|
||||
create_provider("peaq")
|
||||
}
|
||||
|
||||
fn create_gnosis_provider() -> String {
|
||||
create_provider("gnosis")
|
||||
}
|
||||
|
||||
// Get network token symbol
|
||||
fn get_network_token_symbol(network_name: &str) -> String {
|
||||
match ethereum::get_network_by_name(network_name) {
|
||||
@ -808,6 +709,12 @@ pub fn register_crypto_module(engine: &mut Engine) -> Result<(), Box<EvalAltResu
|
||||
engine.register_fn("get_network_token_symbol", get_network_token_symbol);
|
||||
engine.register_fn("get_network_explorer_url", get_network_explorer_url);
|
||||
|
||||
// Register provider functions
|
||||
engine.register_fn("create_provider", create_provider);
|
||||
engine.register_fn("create_agung_provider", create_agung_provider);
|
||||
engine.register_fn("create_peaq_provider", create_peaq_provider);
|
||||
engine.register_fn("create_gnosis_provider", create_gnosis_provider);
|
||||
|
||||
// Register transaction functions
|
||||
engine.register_fn("send_eth", send_eth);
|
||||
engine.register_fn("get_balance", get_balance);
|
||||
|
Loading…
Reference in New Issue
Block a user