Compare commits

..

6 Commits

Author SHA1 Message Date
0c425470a5 Merge pull request 'Simplify and Refactor Asymmetric Encryption/Decryption' (#10) from development_fix_code into main
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
Reviewed-on: #10
2025-05-13 13:00:16 +00:00
Mahmoud Emad
7add64562e feat: Simplify asymmetric encryption/decryption
Some checks are pending
Rhai Tests / Run Rhai Tests (pull_request) Waiting to run
- Simplify asymmetric encryption by using a single symmetric key
  instead of deriving a key from an ephemeral key exchange.  This
  improves clarity and reduces complexity.
- The new implementation encrypts the symmetric key with the
  recipient's public key and then encrypts the message with the
  symmetric key.
- Improve test coverage for asymmetric encryption/decryption.
2025-05-13 14:45:05 +03:00
Mahmoud Emad
809599d60c fix: Get the code to compile 2025-05-13 14:12:48 +03:00
Mahmoud Emad
25f2ae6fa9 refactor: Refactor keypair and Ethereum wallet handling
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
- Moved `prepare_function_arguments` and `convert_token_to_rhai` to the `ethereum` module for better organization.
- Updated `keypair` module to use the new `session_manager` structure improving code clarity.
- Changed `KeyPair` type usage to new `vault::keyspace::keypair_types::KeyPair` for consistency.
- Improved error handling and clarity in `EthereumWallet` methods.
2025-05-13 13:55:04 +03:00
a4438d63e0 ... 2025-05-13 08:02:23 +03:00
393c4270d4 ... 2025-05-13 07:28:02 +03:00
38 changed files with 1118 additions and 2665 deletions

View File

@ -12,16 +12,16 @@ readme = "README.md"
[dependencies] [dependencies]
anyhow = "1.0.98" anyhow = "1.0.98"
base64 = "0.21.0" # Base64 encoding/decoding base64 = "0.22.1" # Base64 encoding/decoding
cfg-if = "1.0" cfg-if = "1.0"
chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher
clap = "2.33" # Command-line argument parsing clap = "2.34.0" # Command-line argument parsing
dirs = "5.0.1" # Directory paths dirs = "6.0.0" # Directory paths
env_logger = "0.10.0" # Logger implementation env_logger = "0.11.8" # Logger implementation
ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library
glob = "0.3.1" # For file pattern matching glob = "0.3.1" # For file pattern matching
jsonrpsee = "0.25.1" jsonrpsee = "0.25.1"
k256 = { version = "0.13.1", features = ["ecdsa", "ecdh"] } # Elliptic curve cryptography k256 = { version = "0.13.4", features = ["ecdsa", "ecdh"] } # Elliptic curve cryptography
lazy_static = "1.4.0" # For lazy initialization of static variables lazy_static = "1.4.0" # For lazy initialization of static variables
libc = "0.2" libc = "0.2"
log = "0.4" # Logging facade log = "0.4" # Logging facade
@ -31,7 +31,7 @@ postgres-types = "0.2.5" # PostgreSQL type conversions
r2d2 = "0.8.10" r2d2 = "0.8.10"
r2d2_postgres = "0.18.2" r2d2_postgres = "0.18.2"
rand = "0.8.5" # Random number generation rand = "0.8.5" # Random number generation
redis = "0.22.0" # Redis client redis = "0.31.0" # Redis client
regex = "1.8.1" # For regex pattern matching regex = "1.8.1" # For regex pattern matching
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
serde = { version = "1.0", features = [ serde = { version = "1.0", features = [
@ -40,23 +40,26 @@ serde = { version = "1.0", features = [
serde_json = "1.0" # For JSON handling serde_json = "1.0" # For JSON handling
sha2 = "0.10.7" # SHA-2 hash functions sha2 = "0.10.7" # SHA-2 hash functions
tempfile = "3.5" # For temporary file operations tempfile = "3.5" # For temporary file operations
tokio = { version = "1.28", features = ["full"] } tera = "1.19.0" # Template engine for text rendering
thiserror = "2.0.12" # For error handling
tokio = "1.45.0"
tokio-postgres = "0.7.8" # Async PostgreSQL client
tokio-test = "0.4.4" tokio-test = "0.4.4"
uuid = { version = "1.16.0", features = ["v4"] } uuid = { version = "1.16.0", features = ["v4"] }
zinit-client = { git = "https://github.com/threefoldtech/zinit", branch = "json_rpc", package = "zinit-client" }
# Optional features for specific OS functionality # Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
nix = "0.26" # Unix-specific functionality nix = "0.30.1" # Unix-specific functionality
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.48", features = [ windows = { version = "0.61.1", features = [
"Win32_Foundation", "Win32_Foundation",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_Storage_FileSystem", "Win32_Storage_FileSystem",
] } ] }
[dev-dependencies] [dev-dependencies]
mockall = "0.11.4" # For mocking in tests mockall = "0.13.1" # For mocking in tests
tempfile = "3.5" # For tests that need temporary files/directories tempfile = "3.5" # For tests that need temporary files/directories
tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing

View File

@ -1,125 +0,0 @@
// Example script demonstrating how to add a custom network and use it
// This script shows the new network-independent wallet design
// Load a key space (or create one if it doesn't exist)
if (!load_key_space("demo", "password123")) {
print("Creating new key space...");
create_key_space("demo", "password123");
}
// Always create a keypair (will be a no-op if it already exists)
print("Creating keypair...");
create_keypair("demo_key", "password123");
// Select the keypair
print("Selecting keypair...");
select_keypair("demo_key");
// Create an Ethereum wallet (network-independent)
print("Creating Ethereum wallet...");
create_ethereum_wallet();
// Get the wallet address (same for all networks)
let address = get_ethereum_address();
print(`Your Ethereum address: ${address}`);
// List the built-in networks
print("\nBuilt-in networks:");
let networks = list_supported_networks();
for network in networks {
print(`- ${network}`);
}
// Register a custom network (Sepolia testnet)
print("\nRegistering Sepolia testnet...");
let success = register_network(
"Sepolia",
11155111,
"https://sepolia.blast.io", // Using Blast.io's public RPC endpoint
"https://sepolia.etherscan.io",
"ETH",
18
);
if (success) {
print("Sepolia testnet registered successfully!");
} else {
print("Failed to register Sepolia testnet!");
}
// List networks again to confirm Sepolia was added
print("\nUpdated networks list:");
networks = list_supported_networks();
for network in networks {
print(`- ${network}`);
}
// Get network details
print("\nSepolia network details:");
print(`Token symbol: ${get_network_token_symbol("sepolia")}`);
print(`Explorer URL: ${get_network_explorer_url("sepolia")}`);
// Check balance on different networks
print("\nChecking balances on different networks...");
print(`NOTE: These will likely show zero unless you've funded the address`);
print(`NOTE: Balance checks may fail if the RPC endpoints are not accessible`);
// Check balance on Sepolia (this might fail if the RPC endpoint is not accessible)
print("\nTrying to get balance on Sepolia...");
let sepolia_balance = get_balance("sepolia", address);
if (sepolia_balance == "") {
print("Failed to get balance on Sepolia (this is expected if you don't have access to the Sepolia RPC)");
} else {
print(`Balance on Sepolia: ${sepolia_balance}`);
}
// Check balance on Gnosis
print("\nTrying to get balance on Gnosis...");
let gnosis_balance = get_balance("gnosis", address);
if (gnosis_balance == "") {
print("Failed to get balance on Gnosis");
} else {
print(`Balance on Gnosis: ${gnosis_balance}`);
}
// Check balance on Peaq
print("\nTrying to get balance on Peaq...");
let peaq_balance = get_balance("peaq", address);
if (peaq_balance == "") {
print("Failed to get balance on Peaq");
} else {
print(`Balance on Peaq: ${peaq_balance}`);
}
// Demonstrate sending a transaction (commented out to prevent accidental execution)
// To execute this, uncomment the following lines and make sure your wallet has funds
/*
print("\nSending 0.001 ETH on Sepolia...");
let recipient = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"; // Example address
let amount = "1000000000000000"; // 0.001 ETH in wei
let tx_hash = send_eth("sepolia", recipient, amount);
if (tx_hash != "") {
print(`Transaction sent! Hash: ${tx_hash}`);
print(`View on Sepolia Explorer: ${get_network_explorer_url("sepolia")}/tx/${tx_hash}`);
} else {
print("Transaction failed!");
}
*/
// Remove the custom network
print("\nRemoving Sepolia network...");
success = remove_network("Sepolia");
if (success) {
print("Sepolia network removed successfully!");
} else {
print("Failed to remove Sepolia network!");
}
// List networks again to confirm Sepolia was removed
print("\nFinal networks list:");
networks = list_supported_networks();
for network in networks {
print(`- ${network}`);
}
print("\nDone!");

View File

@ -1,8 +1,8 @@
// Script to create an Ethereum wallet from a private key and send tokens on the Agung network // Script to create an Agung wallet from a private key and send tokens
// This script demonstrates how to create a wallet from a private key and send tokens // This script demonstrates how to create a wallet from a private key and send tokens
// Define the private key and recipient address // Define the private key and recipient address
let private_key = "51c194d20bcd25360a3aa94426b3b60f738007e42f22e1bc97821c65c353e6d2"; let private_key = "0x9ecfd58eca522b0e7c109bf945966ee208cd6d593b1dc3378aedfdc60b64f512";
let recipient_address = "0xf400f9c3F7317e19523a5DB698Ce67e7a7E083e2"; let recipient_address = "0xf400f9c3F7317e19523a5DB698Ce67e7a7E083e2";
print("=== Agung Wallet Transaction Demo ==="); print("=== Agung Wallet Transaction Demo ===");
@ -33,32 +33,32 @@ if !select_keypair("demo_keypair") {
print("\nCreated and selected keypair successfully"); print("\nCreated and selected keypair successfully");
// Clear any existing Ethereum wallets to avoid conflicts // Clear any existing Agung wallets to avoid conflicts
if clear_ethereum_wallets() { if clear_wallets_for_network("agung") {
print("Cleared existing Ethereum wallets"); print("Cleared existing Agung wallets");
} else { } else {
print("Failed to clear existing Ethereum wallets"); print("Failed to clear existing Agung wallets");
return; return;
} }
// Create a wallet from the private key directly // Create a wallet from the private key directly
print("\n=== Creating Wallet from Private Key ==="); print("\n=== Creating Wallet from Private Key ===");
// Create a wallet from the private key (works for any network) // Create a wallet from the private key for the Agung network
if create_ethereum_wallet_from_private_key(private_key) { if create_wallet_from_private_key_for_network(private_key, "agung") {
print("Successfully created wallet from private key"); print("Successfully created wallet from private key for Agung network");
// Get the wallet address // Get the wallet address
let wallet_address = get_ethereum_address(); let wallet_address = get_wallet_address_for_network("agung");
print(`Wallet address: ${wallet_address}`); print(`Wallet address: ${wallet_address}`);
// Create a provider for the Agung network // Create a provider for the Agung network
let provider_id = create_provider("agung"); let provider_id = create_agung_provider();
if provider_id != "" { if provider_id != "" {
print("Successfully created Agung provider"); print("Successfully created Agung provider");
// Check the wallet balance first // Check the wallet balance first
let wallet_address = get_ethereum_address(); let wallet_address = get_wallet_address_for_network("agung");
let balance_wei = get_balance("agung", wallet_address); let balance_wei = get_balance("agung", wallet_address);
if balance_wei == "" { if balance_wei == "" {
@ -67,18 +67,16 @@ if create_ethereum_wallet_from_private_key(private_key) {
return; return;
} }
print(`Current wallet balance: ${balance_wei}`); print(`Current wallet balance: ${balance_wei} wei`);
// Convert 1 AGNG to wei (1 AGNG = 10^18 wei) // Convert 1 AGNG to wei (1 AGNG = 10^18 wei)
// Use string representation for large numbers // Use string representation for large numbers
let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string
// For this example, we'll assume we have enough balance // Check if we have enough balance
// NOTE: In a real application, you would need to check the balance properly if parse_int(balance_wei) < parse_int(amount_wei_str) {
// 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(`Insufficient balance to send ${amount_wei_str} wei (1 AGNG)`);
print(`Current balance: ${balance_wei}`); print(`Current balance: ${balance_wei} wei`);
print("Please fund the wallet before attempting to send a transaction"); print("Please fund the wallet before attempting to send a transaction");
return; return;
} }

View File

@ -206,7 +206,7 @@ impl RedisClientWrapper {
} }
// Select the database // Select the database
redis::cmd("SELECT").arg(self.db).execute(&mut conn); let _ = redis::cmd("SELECT").arg(self.db).exec(&mut conn);
self.initialized.store(true, Ordering::Relaxed); self.initialized.store(true, Ordering::Relaxed);

View File

@ -1,132 +1,256 @@
//! Rhai bindings for SAL crypto functionality //! Rhai bindings for SAL crypto functionality
use rhai::{Engine, Dynamic, EvalAltResult}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use std::fs;
use std::sync::Mutex;
use once_cell::sync::{Lazy, OnceCell};
use tokio::runtime::Runtime;
use ethers::types::{Address, U256}; use ethers::types::{Address, U256};
use once_cell::sync::Lazy;
use rhai::{Dynamic, Engine, EvalAltResult};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use cfg_if::cfg_if; use std::sync::Mutex;
use tokio::runtime::Runtime;
use crate::vault::{keypair, symmetric, ethereum, kvs}; use crate::vault::ethereum;
use crate::vault::kvs::{KVStore, DefaultStore}; use crate::vault::keyspace::session_manager as keypair;
use crate::vault::ethereum::prepare_function_arguments;
use crate::vault::symmetric::implementation as symmetric_impl;
// Global Tokio runtime for blocking async operations // Global Tokio runtime for blocking async operations
static RUNTIME: Lazy<Mutex<Runtime>> = Lazy::new(|| { static RUNTIME: Lazy<Mutex<Runtime>> =
Mutex::new(Runtime::new().expect("Failed to create Tokio runtime")) Lazy::new(|| Mutex::new(Runtime::new().expect("Failed to create Tokio runtime")));
});
// Helper function to run async operations and handle errors consistently // Global provider registry
fn run_async<F, T, E>(future: F) -> Result<T, String> static PROVIDERS: Lazy<
where Mutex<HashMap<String, ethers::providers::Provider<ethers::providers::Http>>>,
F: std::future::Future<Output = Result<T, E>>, > = Lazy::new(|| Mutex::new(HashMap::new()));
E: std::fmt::Display,
{
let rt = RUNTIME.lock().map_err(|e| format!("Failed to acquire runtime lock: {}", e))?;
rt.block_on(async { future.await.map_err(|e| e.to_string()) })
}
// Get a platform-specific DefaultStore implementation for Rhai bindings
// This function is implemented differently based on the target platform
cfg_if! {
if #[cfg(target_arch = "wasm32")] {
fn get_key_store() -> DefaultStore {
use wasm_bindgen_futures::JsFuture;
use once_cell::sync::OnceCell;
use std::future::Future;
// Static store instance
static KEY_STORE: OnceCell<DefaultStore> = OnceCell::new();
// Initialize if not already done
KEY_STORE.get_or_init(|| {
// In WebAssembly, we need to use a blocking approach for Rhai
let store_future = async {
match kvs::open_default_store("rhai-vault", None).await {
Ok(store) => store,
Err(_) => {
// Try to create the store if opening failed
kvs::create_default_store("rhai-vault", false, None).await
.expect("Failed to create key store")
}
}
};
// Block on the async operation
let rt = RUNTIME.lock().unwrap();
rt.block_on(store_future)
}).clone()
}
} else {
fn get_key_store() -> DefaultStore {
use once_cell::sync::OnceCell;
// Static store instance
static KEY_STORE: OnceCell<DefaultStore> = OnceCell::new();
// Initialize if not already done
KEY_STORE.get_or_init(|| {
// For native platforms, the operations are synchronous
match kvs::open_default_store("rhai-vault", None) {
Ok(store) => store,
Err(_) => {
// Try to create the store if opening failed
kvs::create_default_store("rhai-vault", false, None)
.expect("Failed to create key store")
}
}
}).clone()
}
}
}
// Key space management functions // Key space management functions
fn load_key_space(name: &str, password: &str) -> bool { fn load_key_space(name: &str, password: &str) -> bool {
let store = get_key_store(); // Get the key spaces directory from config
kvs::load_key_space(&store, name, password) 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_impl::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_impl::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
}
}
} }
fn create_key_space(name: &str, password: &str) -> bool { fn create_key_space(name: &str, password: &str) -> bool {
let store = get_key_store(); match keypair::create_space(name) {
kvs::create_key_space(&store, name, password) Ok(_) => {
// Get the current space
match keypair::get_current_space() {
Ok(space) => {
// Encrypt the key space
let encrypted_space = match symmetric_impl::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_impl::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
}
}
} }
// Auto-save function for internal use // Auto-save function for internal use
fn auto_save_key_space(password: &str) -> bool { fn auto_save_key_space(password: &str) -> bool {
let store = get_key_store(); match keypair::get_current_space() {
kvs::save_key_space(&store, password) Ok(space) => {
} // Encrypt the key space
let encrypted_space = match symmetric_impl::encrypt_key_space(&space, password) {
// Export the current key space to a JSON string Ok(encrypted) => encrypted,
fn encrypt_key_space(password: &str) -> String {
match keypair::get_current_space()
.and_then(|space| symmetric::encrypt_key_space(&space, password))
.and_then(|encrypted_space| symmetric::serialize_encrypted_space(&encrypted_space))
{
Ok(json) => json,
Err(e) => { Err(e) => {
log::error!("Error encrypting key space: {}", e); log::error!("Error encrypting key space: {}", e);
return false;
}
};
// Serialize the encrypted space
let serialized = match symmetric_impl::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
}
}
}
fn encrypt_key_space(password: &str) -> String {
match keypair::get_current_space() {
Ok(space) => match symmetric_impl::encrypt_key_space(&space, password) {
Ok(encrypted_space) => match serde_json::to_string(&encrypted_space) {
Ok(json) => json,
Err(e) => {
log::error!("Error serializing encrypted space: {}", e);
String::new()
}
},
Err(e) => {
log::error!("Error encrypting key space: {}", e);
String::new()
}
},
Err(e) => {
log::error!("Error getting current space: {}", e);
String::new() String::new()
} }
} }
} }
// Import a key space from a JSON string
fn decrypt_key_space(encrypted: &str, password: &str) -> bool { fn decrypt_key_space(encrypted: &str, password: &str) -> bool {
match symmetric::deserialize_encrypted_space(encrypted) match serde_json::from_str(encrypted) {
.and_then(|encrypted_space| symmetric::decrypt_key_space(&encrypted_space, password)) Ok(encrypted_space) => {
.and_then(|space| keypair::set_current_space(space)) match symmetric_impl::decrypt_key_space(&encrypted_space, password) {
{ Ok(space) => match keypair::set_current_space(space) {
Ok(_) => true, Ok(_) => true,
Err(e) => {
log::error!("Error setting current space: {}", e);
false
}
},
Err(e) => { Err(e) => {
log::error!("Error decrypting key space: {}", e); log::error!("Error decrypting key space: {}", e);
false false
} }
} }
} }
Err(e) => {
log::error!("Error parsing encrypted space: {}", e);
false
}
}
}
// Keypair management functions // Keypair management functions
fn create_keypair(name: &str, password: &str) -> bool { fn create_keypair(name: &str, password: &str) -> bool {
@ -134,7 +258,7 @@ fn create_keypair(name: &str, password: &str) -> bool {
Ok(_) => { Ok(_) => {
// Auto-save the key space after creating a keypair // Auto-save the key space after creating a keypair
auto_save_key_space(password) auto_save_key_space(password)
}, }
Err(e) => { Err(e) => {
log::error!("Error creating keypair: {}", e); log::error!("Error creating keypair: {}", e);
false false
@ -176,59 +300,71 @@ fn sign(message: &str) -> String {
fn verify(message: &str, signature: &str) -> bool { fn verify(message: &str, signature: &str) -> bool {
let message_bytes = message.as_bytes(); let message_bytes = message.as_bytes();
match BASE64.decode(signature) match BASE64.decode(signature) {
.map_err(|e| e.to_string()) Ok(signature_bytes) => match keypair::keypair_verify(message_bytes, &signature_bytes) {
.and_then(|sig_bytes| keypair::keypair_verify(message_bytes, &sig_bytes)
.map_err(|e| e.to_string()))
{
Ok(is_valid) => is_valid, Ok(is_valid) => is_valid,
Err(e) => { Err(e) => {
log::error!("Error verifying signature: {}", e); log::error!("Error verifying signature: {}", e);
false false
} }
},
Err(e) => {
log::error!("Error decoding signature: {}", e);
false
}
} }
} }
// Symmetric encryption // Symmetric encryption
fn generate_key() -> String { fn generate_key() -> String {
BASE64.encode(symmetric::generate_symmetric_key()) let key = symmetric_impl::generate_symmetric_key();
BASE64.encode(key)
} }
fn encrypt(key: &str, message: &str) -> String { fn encrypt(key: &str, message: &str) -> String {
match BASE64.decode(key) match BASE64.decode(key) {
.map_err(|e| format!("Error decoding key: {}", e)) Ok(key_bytes) => {
.and_then(|key_bytes| { let message_bytes = message.as_bytes();
symmetric::encrypt_symmetric(&key_bytes, message.as_bytes()) match symmetric_impl::encrypt_symmetric(&key_bytes, message_bytes) {
.map_err(|e| format!("Error encrypting message: {}", e))
})
{
Ok(ciphertext) => BASE64.encode(ciphertext), Ok(ciphertext) => BASE64.encode(ciphertext),
Err(e) => { Err(e) => {
log::error!("{}", e); log::error!("Error encrypting message: {}", e);
String::new()
}
}
}
Err(e) => {
log::error!("Error decoding key: {}", e);
String::new() String::new()
} }
} }
} }
fn decrypt(key: &str, ciphertext: &str) -> String { fn decrypt(key: &str, ciphertext: &str) -> String {
match BASE64.decode(key) match BASE64.decode(key) {
.map_err(|e| format!("Error decoding key: {}", e)) Ok(key_bytes) => match BASE64.decode(ciphertext) {
.and_then(|key_bytes| { Ok(ciphertext_bytes) => {
BASE64.decode(ciphertext) match symmetric_impl::decrypt_symmetric(&key_bytes, &ciphertext_bytes) {
.map_err(|e| format!("Error decoding ciphertext: {}", e)) Ok(plaintext) => match String::from_utf8(plaintext) {
.and_then(|cipher_bytes| {
symmetric::decrypt_symmetric(&key_bytes, &cipher_bytes)
.map_err(|e| format!("Error decrypting ciphertext: {}", e))
})
})
.and_then(|plaintext| {
String::from_utf8(plaintext)
.map_err(|e| format!("Error converting plaintext to string: {}", e))
})
{
Ok(text) => text, Ok(text) => text,
Err(e) => { Err(e) => {
log::error!("{}", e); log::error!("Error converting plaintext to string: {}", e);
String::new()
}
},
Err(e) => {
log::error!("Error decrypting ciphertext: {}", e);
String::new()
}
}
}
Err(e) => {
log::error!("Error decoding ciphertext: {}", e);
String::new()
}
},
Err(e) => {
log::error!("Error decoding key: {}", e);
String::new() String::new()
} }
} }
@ -236,9 +372,9 @@ fn decrypt(key: &str, ciphertext: &str) -> String {
// Ethereum operations // Ethereum operations
// Create a network-independent Ethereum wallet // Gnosis Chain operations
fn create_ethereum_wallet() -> bool { fn create_ethereum_wallet() -> bool {
match ethereum::create_ethereum_wallet() { match ethereum::create_ethereum_wallet_for_network(ethereum::networks::gnosis()) {
Ok(_) => true, Ok(_) => true,
Err(e) => { Err(e) => {
log::error!("Error creating Ethereum wallet: {}", e); log::error!("Error creating Ethereum wallet: {}", e);
@ -247,9 +383,8 @@ fn create_ethereum_wallet() -> bool {
} }
} }
// Get the Ethereum wallet address (same for all networks)
fn get_ethereum_address() -> String { fn get_ethereum_address() -> String {
match ethereum::get_current_ethereum_wallet() { match ethereum::get_current_ethereum_wallet_for_network("Gnosis") {
Ok(wallet) => wallet.address_string(), Ok(wallet) => wallet.address_string(),
Err(e) => { Err(e) => {
log::error!("Error getting Ethereum address: {}", e); log::error!("Error getting Ethereum address: {}", e);
@ -258,76 +393,116 @@ fn get_ethereum_address() -> String {
} }
} }
// Create a wallet from a name // Peaq network operations
fn create_ethereum_wallet_from_name(name: &str) -> bool { fn create_peaq_wallet() -> bool {
match ethereum::create_ethereum_wallet_from_name(name) { match ethereum::create_peaq_wallet() {
Ok(_) => true, Ok(_) => true,
Err(e) => { Err(e) => {
log::error!("Error creating Ethereum wallet from name: {}", e); log::error!("Error creating Peaq wallet: {}", e);
false false
} }
} }
} }
// Create a wallet from a private key fn get_peaq_address() -> String {
fn create_ethereum_wallet_from_private_key(private_key: &str) -> bool { match ethereum::get_current_peaq_wallet() {
match ethereum::create_ethereum_wallet_from_private_key(private_key) { Ok(wallet) => wallet.address_string(),
Ok(_) => true,
Err(e) => { Err(e) => {
log::error!("Error creating Ethereum wallet from private key: {}", e); log::error!("Error getting Peaq address: {}", e);
false
}
}
}
// Clear all Ethereum wallets
fn clear_ethereum_wallets() -> bool {
ethereum::clear_ethereum_wallets();
true // Always return true since the operation doesn't have a failure mode
}
// Network registry functions
// Register a new network
fn register_network(name: &str, chain_id: i64, rpc_url: &str, explorer_url: &str, token_symbol: &str, decimals: i64) -> bool {
ethereum::register_network(
name,
chain_id as u64,
rpc_url,
explorer_url,
token_symbol,
decimals as u8
)
}
// Remove a network
fn remove_network(name: &str) -> bool {
ethereum::remove_network(name)
}
// List supported networks
fn list_supported_networks() -> rhai::Array {
let mut arr = rhai::Array::new();
for name in ethereum::list_network_names() {
arr.push(Dynamic::from(name.to_lowercase()));
}
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() String::new()
} }
} }
} }
// Agung testnet operations
fn create_agung_wallet() -> bool {
match ethereum::create_agung_wallet() {
Ok(_) => true,
Err(e) => {
log::error!("Error creating Agung wallet: {}", e);
false
}
}
}
fn get_agung_address() -> String {
match ethereum::get_current_agung_wallet() {
Ok(wallet) => wallet.address_string(),
Err(e) => {
log::error!("Error getting Agung address: {}", e);
String::new()
}
}
}
// Generic network operations
fn create_wallet_for_network(network_name: &str) -> bool {
let network = match ethereum::networks::get_network_by_name(network_name) {
Some(network) => network,
None => {
log::error!("Unknown network: {}", network_name);
return false;
}
};
match ethereum::create_ethereum_wallet_for_network(network) {
Ok(_) => true,
Err(e) => {
log::error!("Error creating wallet for network {}: {}", network_name, e);
false
}
}
}
// Get wallet address for a specific network
fn get_wallet_address_for_network(network_name: &str) -> String {
let network_name_proper = match ethereum::networks::get_proper_network_name(network_name) {
Some(name) => name,
None => {
log::error!("Unknown network: {}", network_name);
return String::new();
}
};
match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) {
Ok(wallet) => wallet.address_string(),
Err(e) => {
log::error!(
"Error getting wallet address for network {}: {}",
network_name,
e
);
String::new()
}
}
}
// Clear wallets for a specific network
fn clear_wallets_for_network(network_name: &str) -> bool {
let network_name_proper = match ethereum::networks::get_proper_network_name(network_name) {
Some(name) => name,
None => {
log::error!("Unknown network: {}", network_name);
return false;
}
};
ethereum::clear_ethereum_wallets_for_network(network_name_proper);
true
}
// List supported networks
fn list_supported_networks() -> rhai::Array {
let mut arr = rhai::Array::new();
for name in ethereum::networks::list_network_names() {
arr.push(Dynamic::from(name.to_lowercase()));
}
arr
}
// Get network token symbol // Get network token symbol
fn get_network_token_symbol(network_name: &str) -> String { fn get_network_token_symbol(network_name: &str) -> String {
match ethereum::get_network_by_name(network_name) { match ethereum::networks::get_network_by_name(network_name) {
Some(network) => network.token_symbol, Some(network) => network.token_symbol,
None => { None => {
log::error!("Unknown network: {}", network_name); log::error!("Unknown network: {}", network_name);
@ -338,7 +513,7 @@ fn get_network_token_symbol(network_name: &str) -> String {
// Get network explorer URL // Get network explorer URL
fn get_network_explorer_url(network_name: &str) -> String { fn get_network_explorer_url(network_name: &str) -> String {
match ethereum::get_network_by_name(network_name) { match ethereum::networks::get_network_by_name(network_name) {
Some(network) => network.explorer_url, Some(network) => network.explorer_url,
None => { None => {
log::error!("Unknown network: {}", network_name); log::error!("Unknown network: {}", network_name);
@ -347,8 +522,63 @@ fn get_network_explorer_url(network_name: &str) -> String {
} }
} }
// Create a wallet from a private key for a specific network
fn create_wallet_from_private_key_for_network(private_key: &str, network_name: &str) -> bool {
let network = match ethereum::networks::get_network_by_name(network_name) {
Some(network) => network,
None => {
log::error!("Unknown network: {}", network_name);
return false;
}
};
match ethereum::create_ethereum_wallet_from_private_key_for_network(private_key, network) {
Ok(_) => true,
Err(e) => {
log::error!(
"Error creating wallet from private key for network {}: {}",
network_name,
e
);
false
}
}
}
// Create a provider for the Agung network
fn create_agung_provider() -> String {
match ethereum::create_agung_provider() {
Ok(provider) => {
// Generate a unique ID for the provider
let id = format!("provider_{}", uuid::Uuid::new_v4());
// Store the provider in the registry
if let Ok(mut providers) = PROVIDERS.lock() {
providers.insert(id.clone(), provider);
return id;
}
log::error!("Failed to acquire provider registry lock");
String::new()
}
Err(e) => {
log::error!("Error creating Agung provider: {}", e);
String::new()
}
}
}
// Get the balance of an address on a specific network // Get the balance of an address on a specific network
fn get_balance(network_name: &str, address: &str) -> String { fn get_balance(network_name: &str, address: &str) -> String {
// Get the runtime
let rt = match RUNTIME.lock() {
Ok(rt) => rt,
Err(e) => {
log::error!("Failed to acquire runtime lock: {}", e);
return String::new();
}
};
// Parse the address // Parse the address
let addr = match Address::from_str(address) { let addr = match Address::from_str(address) {
Ok(addr) => addr, Ok(addr) => addr,
@ -358,15 +588,36 @@ fn get_balance(network_name: &str, address: &str) -> String {
} }
}; };
// Execute the balance query in a blocking manner // Get the proper network name
match run_async(ethereum::get_balance(network_name, addr)) { let network_name_proper = match ethereum::networks::get_proper_network_name(network_name) {
Ok(balance) => { Some(name) => name,
// Format the balance with the network's token symbol None => {
match ethereum::get_network_by_name(network_name) { log::error!("Unknown network: {}", network_name);
Some(network) => ethereum::format_balance(balance, &network), return String::new();
None => balance.to_string()
} }
}, };
// Get the network config
let network = match ethereum::networks::get_network_by_name(network_name_proper) {
Some(n) => n,
None => {
log::error!("Failed to get network config for: {}", network_name_proper);
return String::new();
}
};
// Create a provider
let provider = match ethereum::create_provider(&network) {
Ok(p) => p,
Err(e) => {
log::error!("Failed to create provider: {}", e);
return String::new();
}
};
// Execute the balance query in a blocking manner
match rt.block_on(async { ethereum::get_balance(&provider, addr).await }) {
Ok(balance) => balance.to_string(),
Err(e) => { Err(e) => {
log::error!("Failed to get balance: {}", e); log::error!("Failed to get balance: {}", e);
String::new() String::new()
@ -374,8 +625,17 @@ fn get_balance(network_name: &str, address: &str) -> String {
} }
} }
// Send ETH from one address to another // Send ETH from one address to another using the blocking approach
fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String { fn send_eth(wallet_network: &str, to_address: &str, amount_str: &str) -> String {
// Get the runtime
let rt = match RUNTIME.lock() {
Ok(rt) => rt,
Err(e) => {
log::error!("Failed to acquire runtime lock: {}", e);
return String::new();
}
};
// Parse the address // Parse the address
let to_addr = match Address::from_str(to_address) { let to_addr = match Address::from_str(to_address) {
Ok(addr) => addr, Ok(addr) => addr,
@ -394,8 +654,17 @@ fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String {
} }
}; };
// Get the proper network name
let network_name_proper = match ethereum::networks::get_proper_network_name(wallet_network) {
Some(name) => name,
None => {
log::error!("Unknown network: {}", wallet_network);
return String::new();
}
};
// Get the wallet // Get the wallet
let wallet = match ethereum::get_current_ethereum_wallet() { let wallet = match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) {
Ok(w) => w, Ok(w) => w,
Err(e) => { Err(e) => {
log::error!("Failed to get wallet: {}", e); log::error!("Failed to get wallet: {}", e);
@ -403,8 +672,17 @@ fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String {
} }
}; };
// Create a provider
let provider = match ethereum::create_provider(&wallet.network) {
Ok(p) => p,
Err(e) => {
log::error!("Failed to create provider: {}", e);
return String::new();
}
};
// Execute the transaction in a blocking manner // Execute the transaction in a blocking manner
match run_async(ethereum::send_eth(&wallet, network_name, to_addr, amount)) { match rt.block_on(async { ethereum::send_eth(&wallet, &provider, to_addr, amount).await }) {
Ok(tx_hash) => format!("{:?}", tx_hash), Ok(tx_hash) => format!("{:?}", tx_hash),
Err(e) => { Err(e) => {
log::error!("Transaction failed: {}", e); log::error!("Transaction failed: {}", e);
@ -418,7 +696,7 @@ fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String {
// Load a contract ABI from a JSON string and create a contract instance // Load a contract ABI from a JSON string and create a contract instance
fn load_contract_abi(network_name: &str, address: &str, abi_json: &str) -> String { fn load_contract_abi(network_name: &str, address: &str, abi_json: &str) -> String {
// Get the network // Get the network
let network = match ethereum::get_network_by_name(network_name) { let network = match ethereum::networks::get_network_by_name(network_name) {
Some(network) => network, Some(network) => network,
None => { None => {
log::error!("Unknown network: {}", network_name); log::error!("Unknown network: {}", network_name);
@ -446,7 +724,7 @@ fn load_contract_abi(network_name: &str, address: &str, abi_json: &str) -> Strin
String::new() String::new()
} }
} }
}, }
Err(e) => { Err(e) => {
log::error!("Error creating contract: {}", e); log::error!("Error creating contract: {}", e);
String::new() String::new()
@ -466,6 +744,8 @@ fn load_contract_abi_from_file(network_name: &str, address: &str, file_path: &st
} }
} }
// Use the utility functions from the ethereum module
// Call a read-only function on a contract (no arguments version) // Call a read-only function on a contract (no arguments version)
fn call_contract_read_no_args(contract_json: &str, function_name: &str) -> Dynamic { fn call_contract_read_no_args(contract_json: &str, function_name: &str) -> Dynamic {
call_contract_read(contract_json, function_name, rhai::Array::new()) call_contract_read(contract_json, function_name, rhai::Array::new())
@ -483,7 +763,7 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra
}; };
// Prepare the arguments // Prepare the arguments
let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) { let tokens = match ethereum::prepare_function_arguments(&contract.abi, function_name, &args) {
Ok(tokens) => tokens, Ok(tokens) => tokens,
Err(e) => { Err(e) => {
log::error!("Error preparing arguments: {}", e); log::error!("Error preparing arguments: {}", e);
@ -491,8 +771,17 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra
} }
}; };
// Get the runtime
let rt = match RUNTIME.lock() {
Ok(rt) => rt,
Err(e) => {
log::error!("Failed to acquire runtime lock: {}", e);
return Dynamic::UNIT;
}
};
// Create a provider // Create a provider
let provider = match ethereum::create_provider(&contract.network.name) { let provider = match ethereum::create_provider(&contract.network) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
log::error!("Failed to create provider: {}", e); log::error!("Failed to create provider: {}", e);
@ -501,8 +790,10 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra
}; };
// Execute the call in a blocking manner // Execute the call in a blocking manner
match run_async(ethereum::call_read_function(&contract, &provider, function_name, tokens)) { match rt.block_on(async {
Ok(result) => ethereum::token_to_dynamic(&result), ethereum::call_read_function(&contract, &provider, function_name, tokens).await
}) {
Ok(result) => ethereum::convert_token_to_rhai(&result),
Err(e) => { Err(e) => {
log::error!("Failed to call contract function: {}", e); log::error!("Failed to call contract function: {}", e);
Dynamic::UNIT Dynamic::UNIT
@ -527,7 +818,7 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
}; };
// Prepare the arguments // Prepare the arguments
let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) { let tokens = match ethereum::prepare_function_arguments(&contract.abi, function_name, &args) {
Ok(tokens) => tokens, Ok(tokens) => tokens,
Err(e) => { Err(e) => {
log::error!("Error preparing arguments: {}", e); log::error!("Error preparing arguments: {}", e);
@ -535,8 +826,18 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
} }
}; };
// Get the runtime
let rt = match RUNTIME.lock() {
Ok(rt) => rt,
Err(e) => {
log::error!("Failed to acquire runtime lock: {}", e);
return String::new();
}
};
// Get the wallet // Get the wallet
let wallet = match ethereum::get_current_ethereum_wallet() { let network_name_proper = contract.network.name.as_str();
let wallet = match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) {
Ok(w) => w, Ok(w) => w,
Err(e) => { Err(e) => {
log::error!("Failed to get wallet: {}", e); log::error!("Failed to get wallet: {}", e);
@ -545,7 +846,7 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
}; };
// Create a provider // Create a provider
let provider = match ethereum::create_provider(&contract.network.name) { let provider = match ethereum::create_provider(&contract.network) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
log::error!("Failed to create provider: {}", e); log::error!("Failed to create provider: {}", e);
@ -554,7 +855,9 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
}; };
// Execute the transaction in a blocking manner // Execute the transaction in a blocking manner
match run_async(ethereum::call_write_function(&contract, &wallet, &provider, function_name, tokens)) { match rt.block_on(async {
ethereum::call_write_function(&contract, &wallet, &provider, function_name, tokens).await
}) {
Ok(tx_hash) => format!("{:?}", tx_hash), Ok(tx_hash) => format!("{:?}", tx_hash),
Err(e) => { Err(e) => {
// Log the error details for debugging // Log the error details for debugging
@ -592,32 +895,47 @@ pub fn register_crypto_module(engine: &mut Engine) -> Result<(), Box<EvalAltResu
engine.register_fn("encrypt", encrypt); engine.register_fn("encrypt", encrypt);
engine.register_fn("decrypt", decrypt); engine.register_fn("decrypt", decrypt);
// Register Ethereum wallet functions // Register Ethereum functions (Gnosis Chain)
engine.register_fn("create_ethereum_wallet", create_ethereum_wallet); engine.register_fn("create_ethereum_wallet", create_ethereum_wallet);
engine.register_fn("get_ethereum_address", get_ethereum_address); engine.register_fn("get_ethereum_address", get_ethereum_address);
engine.register_fn("create_ethereum_wallet_from_name", create_ethereum_wallet_from_name);
engine.register_fn("create_ethereum_wallet_from_private_key", create_ethereum_wallet_from_private_key);
engine.register_fn("clear_ethereum_wallets", clear_ethereum_wallets);
// Register network registry functions // Register Peaq network functions
engine.register_fn("register_network", register_network); engine.register_fn("create_peaq_wallet", create_peaq_wallet);
engine.register_fn("remove_network", remove_network); engine.register_fn("get_peaq_address", get_peaq_address);
// Register Agung testnet functions
engine.register_fn("create_agung_wallet", create_agung_wallet);
engine.register_fn("get_agung_address", get_agung_address);
// Register generic network functions
engine.register_fn("create_wallet_for_network", create_wallet_for_network);
engine.register_fn(
"get_wallet_address_for_network",
get_wallet_address_for_network,
);
engine.register_fn("clear_wallets_for_network", clear_wallets_for_network);
engine.register_fn("list_supported_networks", list_supported_networks); engine.register_fn("list_supported_networks", list_supported_networks);
engine.register_fn("get_network_token_symbol", get_network_token_symbol); engine.register_fn("get_network_token_symbol", get_network_token_symbol);
engine.register_fn("get_network_explorer_url", get_network_explorer_url); engine.register_fn("get_network_explorer_url", get_network_explorer_url);
// Register provider functions // Register new Ethereum functions for wallet creation from private key and transactions
engine.register_fn("create_provider", create_provider); engine.register_fn(
"create_wallet_from_private_key_for_network",
// Register transaction functions create_wallet_from_private_key_for_network,
);
engine.register_fn("create_agung_provider", create_agung_provider);
engine.register_fn("send_eth", send_eth); engine.register_fn("send_eth", send_eth);
engine.register_fn("get_balance", get_balance); engine.register_fn("get_balance", get_balance);
// Register smart contract functions // Register smart contract functions
engine.register_fn("load_contract_abi", load_contract_abi); engine.register_fn("load_contract_abi", load_contract_abi);
engine.register_fn("load_contract_abi_from_file", load_contract_abi_from_file); engine.register_fn("load_contract_abi_from_file", load_contract_abi_from_file);
// Register the read function with different arities
engine.register_fn("call_contract_read", call_contract_read_no_args); engine.register_fn("call_contract_read", call_contract_read_no_args);
engine.register_fn("call_contract_read", call_contract_read); engine.register_fn("call_contract_read", call_contract_read);
// Register the write function with different arities
engine.register_fn("call_contract_write", call_contract_write_no_args); engine.register_fn("call_contract_write", call_contract_write_no_args);
engine.register_fn("call_contract_write", call_contract_write); engine.register_fn("call_contract_write", call_contract_write);

View File

@ -157,4 +157,4 @@ The module supports multiple Ethereum networks, including:
## Examples ## Examples
For examples of how to use the Hero Vault module, see the `examples/vault` directory. For examples of how to use the Hero Vault module, see the `examples/hero_vault` directory.

View File

@ -48,10 +48,6 @@ pub enum CryptoError {
/// Smart contract error /// Smart contract error
#[error("Smart contract error: {0}")] #[error("Smart contract error: {0}")]
ContractError(String), ContractError(String),
/// Storage error
#[error("Storage error: {0}")]
StorageError(String),
} }
/// Convert CryptoError to SAL's Error type /// Convert CryptoError to SAL's Error type

View File

@ -21,25 +21,25 @@ The Ethereum module is organized into several components:
The module provides functionality for creating and managing Ethereum wallets: The module provides functionality for creating and managing Ethereum wallets:
```rust ```rust
// Create a network-independent Ethereum wallet // Create a new Ethereum wallet for a specific network
let wallet = create_ethereum_wallet()?; let wallet = create_ethereum_wallet_for_network("Ethereum")?;
// Create a wallet for specific networks
let peaq_wallet = create_peaq_wallet()?;
let agung_wallet = create_agung_wallet()?;
// Create a wallet with a specific name // Create a wallet with a specific name
let named_wallet = create_ethereum_wallet_from_name("my_wallet")?; let named_wallet = create_ethereum_wallet_from_name_for_network("my_wallet", "Gnosis")?;
// Create a wallet from a private key // Create a wallet from a private key
let imported_wallet = create_ethereum_wallet_from_private_key("0x...")?; let imported_wallet = create_ethereum_wallet_from_private_key("0x...")?;
// Get the current wallet // Get the current wallet for a network
let current_wallet = get_current_ethereum_wallet()?; let current_wallet = get_current_ethereum_wallet_for_network("Ethereum")?;
// Clear wallets // Clear wallets
clear_ethereum_wallets()?; clear_ethereum_wallets()?;
clear_ethereum_wallets_for_network("Gnosis")?;
// Legacy functions for backward compatibility
let wallet = create_ethereum_wallet_for_network(network)?;
let peaq_wallet = create_peaq_wallet()?;
let agung_wallet = create_agung_wallet()?;
``` ```
### Network Management ### Network Management
@ -47,12 +47,6 @@ let agung_wallet = create_agung_wallet()?;
The module supports multiple Ethereum networks and provides functionality for managing network configurations: The module supports multiple Ethereum networks and provides functionality for managing network configurations:
```rust ```rust
// Register a new network
register_network("Arbitrum", 42161, "https://arb1.arbitrum.io/rpc", "https://arbiscan.io", "ETH", 18);
// Remove a network
remove_network("Arbitrum");
// Get a network configuration by name // Get a network configuration by name
let network = get_network_by_name("Ethereum")?; let network = get_network_by_name("Ethereum")?;
@ -74,10 +68,7 @@ The module provides functionality for creating and managing Ethereum providers:
// Create a provider for a specific network // Create a provider for a specific network
let provider = create_provider("Ethereum")?; let provider = create_provider("Ethereum")?;
// Create a provider from a network configuration // Create providers for specific networks
let provider = create_provider_from_config(&network)?;
// Legacy functions for backward compatibility
let gnosis_provider = create_gnosis_provider()?; let gnosis_provider = create_gnosis_provider()?;
let peaq_provider = create_peaq_provider()?; let peaq_provider = create_peaq_provider()?;
let agung_provider = create_agung_provider()?; let agung_provider = create_agung_provider()?;
@ -88,20 +79,14 @@ let agung_provider = create_agung_provider()?;
The module provides functionality for managing Ethereum transactions: The module provides functionality for managing Ethereum transactions:
```rust ```rust
// Get the balance of an address on a specific network // Get the balance of an address
let balance = get_balance("Ethereum", address).await?; let balance = get_balance("Ethereum", "0x...")?;
// Get the balance using a provider // Send ETH to an address
let balance = get_balance_with_provider(&provider, address).await?; let tx_hash = send_eth("Ethereum", "0x...", "1000000000000000")?;
// Send ETH to an address on a specific network
let tx_hash = send_eth(&wallet, "Ethereum", to_address, amount).await?;
// Legacy function for backward compatibility
let tx_hash = send_eth_with_provider(&wallet, &provider, to_address, amount).await?;
// Format a balance for display // Format a balance for display
let formatted = format_balance(balance, &network); let formatted = format_balance(balance, 18)?; // Convert wei to ETH
``` ```
### Smart Contract Interactions ### Smart Contract Interactions
@ -113,16 +98,16 @@ The module provides functionality for interacting with smart contracts:
let abi = load_abi_from_json(json_string)?; let abi = load_abi_from_json(json_string)?;
// Create a contract instance // Create a contract instance
let contract = Contract::new(address, abi, network); let contract = Contract::new(provider, "0x...", abi)?;
// Call a read-only function // Call a read-only function
let result = call_read_function(&contract, &provider, "balanceOf", tokens).await?; let result = call_read_function(contract, "balanceOf", vec!["0x..."])?;
// Call a write function // Call a write function
let tx_hash = call_write_function(&contract, &wallet, &provider, "transfer", tokens).await?; let tx_hash = call_write_function(contract, "transfer", vec!["0x...", "1000"])?;
// Estimate gas for a function call // Estimate gas for a function call
let gas = estimate_gas(&contract, &provider, "transfer", tokens).await?; let gas = estimate_gas(contract, "transfer", vec!["0x...", "1000"])?;
``` ```
### Contract Utilities ### Contract Utilities
@ -134,49 +119,29 @@ The module provides utilities for working with contract function arguments and r
let token = convert_rhai_to_token(value)?; let token = convert_rhai_to_token(value)?;
// Prepare function arguments // Prepare function arguments
let args = prepare_function_arguments(&abi, function_name, &args)?; let args = prepare_function_arguments(function, vec![arg1, arg2])?;
// Convert Ethereum tokens to Rhai values // Convert Ethereum tokens to Rhai values
let rhai_value = convert_token_to_rhai(&token)?; let rhai_value = convert_token_to_rhai(token)?;
// Convert a token to a dynamic value // Convert a token to a dynamic value
let dynamic = token_to_dynamic(&token)?; let dynamic = token_to_dynamic(token)?;
``` ```
## Network Registry ## Supported Networks
The module now includes a centralized network registry that allows for dynamic management of EVM-compatible networks: The module supports multiple Ethereum networks, including:
```rust - Gnosis Chain
// Built-in networks - Peaq Network
- Gnosis Chain (chain_id: 100, token: xDAI) - Agung Network
- Peaq Network (chain_id: 3338, token: PEAQ)
- Agung Network (chain_id: 9990, token: AGNG)
// Register a custom network at runtime Each network has its own configuration, including:
register_network(
"Polygon",
137,
"https://polygon-rpc.com",
"https://polygonscan.com",
"MATIC",
18
);
```
## Wallet Address Consistency - RPC URL
- Chain ID
In the new design, Ethereum wallet addresses are consistent across all EVM-compatible networks. This reflects how Ethereum addresses work in reality - the same private key generates the same address on all EVM chains. - Explorer URL
- Native currency symbol and decimals
```rust
// Create a wallet once
let wallet = create_ethereum_wallet()?;
// Use the same wallet address on any network
let eth_balance = get_balance("Ethereum", wallet.address).await?;
let polygon_balance = get_balance("Polygon", wallet.address).await?;
let gnosis_balance = get_balance("Gnosis", wallet.address).await?;
```
## Error Handling ## Error Handling
@ -184,42 +149,12 @@ The module uses the `CryptoError` type for handling errors that can occur during
- `InvalidAddress` - Invalid Ethereum address format - `InvalidAddress` - Invalid Ethereum address format
- `ContractError` - Smart contract interaction error - `ContractError` - Smart contract interaction error
- `NoKeypairSelected` - No keypair selected for wallet creation
- `InvalidKeyLength` - Invalid private key length
## Examples ## Examples
For examples of how to use the Ethereum module, see the `examples/vault` directory, particularly: For examples of how to use the Ethereum module, see the `examples/hero_vault` directory, particularly:
- `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts - `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts
- `agung_simple_transfer.rhai` - Shows how to perform a simple ETH transfer on the Agung network - `agung_simple_transfer.rhai` - Shows how to perform a simple ETH transfer on the Agung network
- `agung_send_transaction.rhai` - Demonstrates sending transactions on the Agung network - `agung_send_transaction.rhai` - Demonstrates sending transactions on the Agung network
- `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung - `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung
## Adding a New Network
With the new design, adding a new network is as simple as registering it with the network registry:
```rust
// In Rust
ethereum::register_network(
"Optimism",
10,
"https://mainnet.optimism.io",
"https://optimistic.etherscan.io",
"ETH",
18
);
// In Rhai
register_network(
"Optimism",
10,
"https://mainnet.optimism.io",
"https://optimistic.etherscan.io",
"ETH",
18
);
```
No code changes are required to add support for new networks!

View File

@ -42,7 +42,7 @@ impl Contract {
} }
/// Creates an ethers Contract instance for interaction. /// Creates an ethers Contract instance for interaction.
pub fn create_ethers_contract(&self, provider: Provider<Http>) -> Result<ethers::contract::Contract<ethers::providers::Provider<Http>>, CryptoError> { pub fn create_ethers_contract(&self, provider: Provider<Http>, _wallet: Option<&EthereumWallet>) -> Result<ethers::contract::Contract<ethers::providers::Provider<Http>>, CryptoError> {
let contract = ethers::contract::Contract::new( let contract = ethers::contract::Contract::new(
self.address, self.address,
self.abi.clone(), self.abi.clone(),
@ -65,9 +65,9 @@ pub async fn call_read_function(
provider: &Provider<Http>, provider: &Provider<Http>,
function_name: &str, function_name: &str,
args: Vec<Token>, args: Vec<Token>,
) -> Result<Token, CryptoError> { ) -> Result<Vec<Token>, CryptoError> {
// Create the ethers contract (not used directly but kept for future extensions) // Create the ethers contract (not used directly but kept for future extensions)
let _ethers_contract = contract.create_ethers_contract(provider.clone())?; let _ethers_contract = contract.create_ethers_contract(provider.clone(), None)?;
// Get the function from the ABI // Get the function from the ABI
let function = contract.abi.function(function_name) let function = contract.abi.function(function_name)
@ -89,12 +89,7 @@ pub async fn call_read_function(
let decoded = function.decode_output(&result) let decoded = function.decode_output(&result)
.map_err(|e| CryptoError::ContractError(format!("Failed to decode function output: {}", e)))?; .map_err(|e| CryptoError::ContractError(format!("Failed to decode function output: {}", e)))?;
// Return the first token if there's only one, otherwise return a tuple Ok(decoded)
if decoded.len() == 1 {
Ok(decoded[0].clone())
} else {
Ok(Token::Tuple(decoded))
}
} }
/// Executes a state-changing function on a contract. /// Executes a state-changing function on a contract.
@ -105,11 +100,10 @@ pub async fn call_write_function(
function_name: &str, function_name: &str,
args: Vec<Token>, args: Vec<Token>,
) -> Result<H256, CryptoError> { ) -> Result<H256, CryptoError> {
// Create a client with the wallet configured for this network // Create a client with the wallet
let network_wallet = wallet.for_network(&contract.network);
let client = SignerMiddleware::new( let client = SignerMiddleware::new(
provider.clone(), provider.clone(),
network_wallet, wallet.wallet.clone(),
); );
// Get the function from the ABI // Get the function from the ABI
@ -120,28 +114,23 @@ pub async fn call_write_function(
let call_data = function.encode_input(&args) let call_data = function.encode_input(&args)
.map_err(|e| CryptoError::ContractError(format!("Failed to encode function call: {}", e)))?; .map_err(|e| CryptoError::ContractError(format!("Failed to encode function call: {}", e)))?;
// Estimate gas
let gas = estimate_gas(contract, wallet, provider, function_name, args.clone()).await?;
log::info!("Estimated gas: {:?}", gas);
// Create the transaction request with gas limit // Create the transaction request with gas limit
let tx = TransactionRequest::new() let tx = TransactionRequest::new()
.to(contract.address) .to(contract.address)
.data(call_data) .data(call_data)
.gas(gas); // Set a reasonable gas limit .gas(U256::from(300000)); // Set a reasonable gas limit
// Send the transaction using the client directly // Send the transaction using the client directly
log::info!("Sending transaction to contract at {}", contract.address); log::info!("Sending transaction to contract at {}", contract.address);
log::info!("Function: {}", function_name); log::info!("Function: {}, Args: {:?}", function_name, args);
// Log detailed information about the transaction // Log detailed information about the transaction
log::debug!("Sending transaction to contract at {}", contract.address); log::debug!("Sending transaction to contract at {}", contract.address);
log::debug!("Function: {}, Args: {:?}", function_name, args); log::debug!("Function: {}, Args: {:?}", function_name, args);
log::debug!("From address: {}", wallet.address); log::debug!("From address: {}", wallet.address);
log::debug!("Gas limit: {:?}", tx.gas); log::debug!("Gas limit: {:?}", tx.gas);
log::debug!("Network: {}", contract.network.name);
let pending_tx = match client.send_transaction(tx, Some(BlockId::Number((BlockNumber::Latest).into()))).await { let pending_tx = match client.send_transaction(tx, None).await {
Ok(pending_tx) => { Ok(pending_tx) => {
log::debug!("Transaction sent successfully: {:?}", pending_tx.tx_hash()); log::debug!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
log::info!("Transaction sent successfully: {:?}", pending_tx.tx_hash()); log::info!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
@ -186,8 +175,5 @@ pub async fn estimate_gas(
.await .await
.map_err(|e| CryptoError::ContractError(format!("Failed to estimate gas: {}", e)))?; .map_err(|e| CryptoError::ContractError(format!("Failed to estimate gas: {}", e)))?;
// Add a buffer to the gas estimate to account for potential variations Ok(gas)
let gas_with_buffer = gas * 12 / 10; // Add 20% buffer
Ok(gas_with_buffer)
} }

View File

@ -24,33 +24,27 @@ pub use networks::NetworkConfig;
// Re-export wallet creation functions // Re-export wallet creation functions
pub use storage::{ pub use storage::{
create_ethereum_wallet,
create_ethereum_wallet_from_name,
create_ethereum_wallet_from_private_key,
// Legacy functions for backward compatibility
create_ethereum_wallet_for_network, create_ethereum_wallet_for_network,
create_peaq_wallet, create_peaq_wallet,
create_agung_wallet, create_agung_wallet,
create_ethereum_wallet_from_name_for_network, create_ethereum_wallet_from_name_for_network,
create_ethereum_wallet_from_name,
create_ethereum_wallet_from_private_key_for_network, create_ethereum_wallet_from_private_key_for_network,
create_ethereum_wallet_from_private_key,
}; };
// Re-export wallet management functions // Re-export wallet management functions
pub use storage::{ pub use storage::{
get_current_ethereum_wallet,
clear_ethereum_wallets,
// Legacy functions for backward compatibility
get_current_ethereum_wallet_for_network, get_current_ethereum_wallet_for_network,
get_current_peaq_wallet, get_current_peaq_wallet,
get_current_agung_wallet, get_current_agung_wallet,
clear_ethereum_wallets,
clear_ethereum_wallets_for_network, clear_ethereum_wallets_for_network,
}; };
// Re-export provider functions // Re-export provider functions
pub use provider::{ pub use provider::{
create_provider, create_provider,
create_provider_from_config,
// Legacy functions for backward compatibility
create_gnosis_provider, create_gnosis_provider,
create_peaq_provider, create_peaq_provider,
create_agung_provider, create_agung_provider,
@ -59,16 +53,12 @@ pub use provider::{
// Re-export transaction functions // Re-export transaction functions
pub use transaction::{ pub use transaction::{
get_balance, get_balance,
get_balance_with_provider,
send_eth, send_eth,
send_eth_with_provider,
format_balance, format_balance,
}; };
// Re-export network registry functions // Re-export network registry functions
pub use networks::{ pub use networks::{
register_network,
remove_network,
get_network_by_name, get_network_by_name,
get_proper_network_name, get_proper_network_name,
list_network_names, list_network_names,

View File

@ -4,8 +4,7 @@
//! to work with them. //! to work with them.
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::RwLock; use std::sync::OnceLock;
use once_cell::sync::Lazy;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
/// Configuration for an EVM-compatible network /// Configuration for an EVM-compatible network
@ -19,16 +18,6 @@ pub struct NetworkConfig {
pub decimals: u8, pub decimals: u8,
} }
/// Global registry of all supported networks
static NETWORK_REGISTRY: Lazy<RwLock<HashMap<String, NetworkConfig>>> = Lazy::new(|| {
let mut registry = HashMap::new();
// Add built-in networks
register_built_in_networks(&mut registry);
RwLock::new(registry)
});
/// Network name constants /// Network name constants
pub mod names { pub mod names {
pub const GNOSIS: &str = "Gnosis"; pub const GNOSIS: &str = "Gnosis";
@ -36,80 +25,50 @@ pub mod names {
pub const AGUNG: &str = "Agung"; pub const AGUNG: &str = "Agung";
} }
/// Register all built-in networks /// Get the Gnosis Chain network configuration
fn register_built_in_networks(registry: &mut HashMap<String, NetworkConfig>) { pub fn gnosis() -> NetworkConfig {
// Gnosis Chain NetworkConfig {
registry.insert(names::GNOSIS.to_lowercase(), NetworkConfig {
name: names::GNOSIS.to_string(), name: names::GNOSIS.to_string(),
chain_id: 100, chain_id: 100,
rpc_url: "https://rpc.gnosischain.com".to_string(), rpc_url: "https://rpc.gnosischain.com".to_string(),
explorer_url: "https://gnosisscan.io".to_string(), explorer_url: "https://gnosisscan.io".to_string(),
token_symbol: "xDAI".to_string(), token_symbol: "xDAI".to_string(),
decimals: 18, decimals: 18,
}); }
}
// Peaq Network /// Get the Peaq Network configuration
registry.insert(names::PEAQ.to_lowercase(), NetworkConfig { pub fn peaq() -> NetworkConfig {
NetworkConfig {
name: names::PEAQ.to_string(), name: names::PEAQ.to_string(),
chain_id: 3338, chain_id: 3338,
rpc_url: "https://peaq.api.onfinality.io/public".to_string(), rpc_url: "https://peaq.api.onfinality.io/public".to_string(),
explorer_url: "https://peaq.subscan.io/".to_string(), explorer_url: "https://peaq.subscan.io/".to_string(),
token_symbol: "PEAQ".to_string(), token_symbol: "PEAQ".to_string(),
decimals: 18, decimals: 18,
}); }
}
// Agung Testnet /// Get the Agung Testnet configuration
registry.insert(names::AGUNG.to_lowercase(), NetworkConfig { pub fn agung() -> NetworkConfig {
NetworkConfig {
name: names::AGUNG.to_string(), name: names::AGUNG.to_string(),
chain_id: 9990, chain_id: 9990,
rpc_url: "https://wss-async.agung.peaq.network".to_string(), rpc_url: "https://wss-async.agung.peaq.network".to_string(),
explorer_url: "https://agung-testnet.subscan.io/".to_string(), explorer_url: "https://agung-testnet.subscan.io/".to_string(),
token_symbol: "AGNG".to_string(), token_symbol: "AGNG".to_string(),
decimals: 18, decimals: 18,
});
}
/// Register a new network
pub fn register_network(
name: &str,
chain_id: u64,
rpc_url: &str,
explorer_url: &str,
token_symbol: &str,
decimals: u8,
) -> bool {
let config = NetworkConfig {
name: name.to_string(),
chain_id,
rpc_url: rpc_url.to_string(),
explorer_url: explorer_url.to_string(),
token_symbol: token_symbol.to_string(),
decimals,
};
if let Ok(mut registry) = NETWORK_REGISTRY.write() {
registry.insert(name.to_lowercase(), config);
true
} else {
false
}
}
/// Remove a network from the registry
pub fn remove_network(name: &str) -> bool {
if let Ok(mut registry) = NETWORK_REGISTRY.write() {
registry.remove(&name.to_lowercase()).is_some()
} else {
false
} }
} }
/// Get a network by its name (case-insensitive) /// Get a network by its name (case-insensitive)
pub fn get_network_by_name(name: &str) -> Option<NetworkConfig> { pub fn get_network_by_name(name: &str) -> Option<NetworkConfig> {
if let Ok(registry) = NETWORK_REGISTRY.read() { let name_lower = name.to_lowercase();
registry.get(&name.to_lowercase()).cloned() match name_lower.as_str() {
} else { "gnosis" => Some(gnosis()),
None "peaq" => Some(peaq()),
"agung" => Some(agung()),
_ => None,
} }
} }
@ -125,57 +84,19 @@ pub fn get_proper_network_name(name: &str) -> Option<&'static str> {
} }
/// Get a list of all supported network names /// Get a list of all supported network names
pub fn list_network_names() -> Vec<String> { pub fn list_network_names() -> Vec<&'static str> {
if let Ok(registry) = NETWORK_REGISTRY.read() { vec![names::GNOSIS, names::PEAQ, names::AGUNG]
registry.values().map(|config| config.name.clone()).collect()
} else {
Vec::new()
}
} }
/// Get a map of all networks /// Get a map of all networks
pub fn get_all_networks() -> HashMap<String, NetworkConfig> { pub fn get_all_networks() -> &'static HashMap<&'static str, NetworkConfig> {
if let Ok(registry) = NETWORK_REGISTRY.read() { static NETWORKS: OnceLock<HashMap<&'static str, NetworkConfig>> = OnceLock::new();
registry.clone()
} else {
HashMap::new()
}
}
// Legacy functions for backward compatibility NETWORKS.get_or_init(|| {
let mut map = HashMap::new();
/// Get the Gnosis Chain network configuration map.insert(names::GNOSIS, gnosis());
pub fn gnosis() -> NetworkConfig { map.insert(names::PEAQ, peaq());
get_network_by_name("gnosis").unwrap_or_else(|| NetworkConfig { map.insert(names::AGUNG, agung());
name: names::GNOSIS.to_string(), map
chain_id: 100,
rpc_url: "https://rpc.gnosischain.com".to_string(),
explorer_url: "https://gnosisscan.io".to_string(),
token_symbol: "xDAI".to_string(),
decimals: 18,
})
}
/// Get the Peaq Network configuration
pub fn peaq() -> NetworkConfig {
get_network_by_name("peaq").unwrap_or_else(|| NetworkConfig {
name: names::PEAQ.to_string(),
chain_id: 3338,
rpc_url: "https://peaq.api.onfinality.io/public".to_string(),
explorer_url: "https://peaq.subscan.io/".to_string(),
token_symbol: "PEAQ".to_string(),
decimals: 18,
})
}
/// Get the Agung Testnet configuration
pub fn agung() -> NetworkConfig {
get_network_by_name("agung").unwrap_or_else(|| NetworkConfig {
name: names::AGUNG.to_string(),
chain_id: 9990,
rpc_url: "https://wss-async.agung.peaq.network".to_string(),
explorer_url: "https://agung-testnet.subscan.io/".to_string(),
token_symbol: "AGNG".to_string(),
decimals: 18,
}) })
} }

View File

@ -3,36 +3,25 @@
use ethers::prelude::*; use ethers::prelude::*;
use crate::vault::error::CryptoError; use crate::vault::error::CryptoError;
use super::networks; use super::networks::{self, NetworkConfig};
/// Creates a provider for a specific network. /// Creates a provider for a specific network.
pub fn create_provider(network_name: &str) -> Result<Provider<Http>, CryptoError> { pub fn create_provider(network: &NetworkConfig) -> Result<Provider<Http>, CryptoError> {
let network = networks::get_network_by_name(network_name)
.ok_or_else(|| CryptoError::SerializationError(format!("Unknown network: {}", network_name)))?;
Provider::<Http>::try_from(network.rpc_url.as_str()) Provider::<Http>::try_from(network.rpc_url.as_str())
.map_err(|e| CryptoError::SerializationError(format!("Failed to create provider for {}: {}", network.name, e))) .map_err(|e| CryptoError::SerializationError(format!("Failed to create provider for {}: {}", network.name, e)))
} }
/// Creates a provider for a specific network configuration.
pub fn create_provider_from_config(network: &networks::NetworkConfig) -> Result<Provider<Http>, CryptoError> {
Provider::<Http>::try_from(network.rpc_url.as_str())
.map_err(|e| CryptoError::SerializationError(format!("Failed to create provider for {}: {}", network.name, e)))
}
// Legacy functions for backward compatibility
/// Creates a provider for the Gnosis Chain. /// Creates a provider for the Gnosis Chain.
pub fn create_gnosis_provider() -> Result<Provider<Http>, CryptoError> { pub fn create_gnosis_provider() -> Result<Provider<Http>, CryptoError> {
create_provider("gnosis") create_provider(&networks::gnosis())
} }
/// Creates a provider for the Peaq network. /// Creates a provider for the Peaq network.
pub fn create_peaq_provider() -> Result<Provider<Http>, CryptoError> { pub fn create_peaq_provider() -> Result<Provider<Http>, CryptoError> {
create_provider("peaq") create_provider(&networks::peaq())
} }
/// Creates a provider for the Agung testnet. /// Creates a provider for the Agung testnet.
pub fn create_agung_provider() -> Result<Provider<Http>, CryptoError> { pub fn create_agung_provider() -> Result<Provider<Http>, CryptoError> {
create_provider("agung") create_provider(&networks::agung())
} }

View File

@ -3,301 +3,112 @@
use std::sync::Mutex; use std::sync::Mutex;
use std::collections::HashMap; use std::collections::HashMap;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Serialize, Deserialize};
use cfg_if::cfg_if;
use tokio::runtime::Runtime;
use ethers::types::Address;
use std::str::FromStr;
use crate::vault::error::CryptoError; use crate::vault::error::CryptoError;
use crate::vault::kvs::{self, KVStore, DefaultStore};
use super::wallet::EthereumWallet; use super::wallet::EthereumWallet;
use super::networks; use super::networks::{self, NetworkConfig};
/// Ethereum wallet data storage key in KVStore /// Global storage for Ethereum wallets.
const ETH_WALLET_STORAGE_KEY: &str = "ethereum/wallets"; static ETH_WALLETS: Lazy<Mutex<HashMap<String, Vec<EthereumWallet>>>> = Lazy::new(|| {
Mutex::new(HashMap::new())
/// Global fallback storage for Ethereum wallets (used when KVStore is unavailable)
static ETH_WALLETS: Lazy<Mutex<Vec<EthereumWallet>>> = Lazy::new(|| {
Mutex::new(Vec::new())
}); });
// Global Tokio runtime for blocking async operations
static RUNTIME: Lazy<Mutex<Runtime>> = Lazy::new(|| {
Mutex::new(Runtime::new().expect("Failed to create Tokio runtime"))
});
/// Serializable representation of an Ethereum wallet
#[derive(Debug, Clone, Serialize, Deserialize)]
struct EthereumWalletStorage {
/// Ethereum address string
address: String,
/// Private key in hex format
private_key: String,
/// Optional wallet name
name: Option<String>,
}
impl From<&EthereumWallet> for EthereumWalletStorage {
fn from(wallet: &EthereumWallet) -> Self {
Self {
address: wallet.address_string(),
private_key: wallet.private_key_hex(),
name: wallet.name.clone(),
}
}
}
impl TryFrom<&EthereumWalletStorage> for EthereumWallet {
type Error = CryptoError;
fn try_from(storage: &EthereumWalletStorage) -> Result<Self, Self::Error> {
let wallet = EthereumWallet::from_private_key(&storage.private_key)?;
// If the address doesn't match, something is wrong
if wallet.address_string() != storage.address {
return Err(CryptoError::InvalidAddress(format!(
"Address mismatch: expected {}, got {}",
storage.address, wallet.address_string()
)));
}
// Set the name if present
let wallet_with_name = if let Some(name) = &storage.name {
EthereumWallet {
name: Some(name.clone()),
..wallet
}
} else {
wallet
};
Ok(wallet_with_name)
}
}
/// Helper function to get the platform-specific storage implementation
fn get_wallet_store() -> Result<DefaultStore, CryptoError> {
cfg_if! {
if #[cfg(target_arch = "wasm32")] {
// For WebAssembly, we need to handle the async nature of IndexedDB
// We'll use a blocking approach for API consistency
use wasm_bindgen_futures::spawn_local;
// We need to use the runtime to block_on the async operations
let rt = RUNTIME.lock().unwrap();
rt.block_on(async {
match kvs::open_default_store("ethereum-wallets", None).await {
Ok(store) => Ok(store),
Err(e) => {
log::warn!("Failed to open IndexedDB store: {}", e);
// Try to create the store if opening failed
kvs::create_default_store("ethereum-wallets", false, None).await
.map_err(|e| CryptoError::StorageError(e.to_string()))
}
}
})
} else {
// For native platforms, we can use SlateDB directly
match kvs::open_default_store("ethereum-wallets", None) {
Ok(store) => Ok(store),
Err(e) => {
log::warn!("Failed to open SlateDB store: {}", e);
// Try to create the store if opening failed
kvs::create_default_store("ethereum-wallets", false, None)
.map_err(|e| CryptoError::StorageError(e.to_string()))
}
}
}
}
}
/// Save wallets to persistent storage
fn save_wallets(wallets: &[EthereumWallet]) -> Result<(), CryptoError> {
// Convert wallets to serializable format
let storage_wallets: Vec<EthereumWalletStorage> = wallets
.iter()
.map(|w| EthereumWalletStorage::from(w))
.collect();
// Try to use the KVStore implementation first
let store_result = get_wallet_store()
.and_then(|store| {
let json = serde_json::to_string(&storage_wallets)
.map_err(|e| CryptoError::StorageError(e.to_string()))?;
store.set(ETH_WALLET_STORAGE_KEY, &json)
.map_err(|e| CryptoError::StorageError(e.to_string()))
});
// Log warning if storage failed but don't fail the operation
if let Err(e) = &store_result {
log::warn!("Failed to save wallets to persistent storage: {}", e);
}
// Always update the in-memory fallback
let mut mem_wallets = ETH_WALLETS.lock().unwrap();
*mem_wallets = wallets.to_vec();
// Return the result of the persistent storage operation
store_result
}
/// Load wallets from persistent storage
fn load_wallets() -> Vec<EthereumWallet> {
// Try to load from KVStore first
let store_result = get_wallet_store()
.and_then(|store| {
store.get::<_, String>(ETH_WALLET_STORAGE_KEY)
.map_err(|e| CryptoError::StorageError(e.to_string()))
})
.and_then(|json| {
serde_json::from_str::<Vec<EthereumWalletStorage>>(&json)
.map_err(|e| CryptoError::StorageError(format!("Failed to parse wallet JSON: {}", e)))
});
match store_result {
Ok(storage_wallets) => {
// Convert from storage format to EthereumWallet
let wallets_result: Result<Vec<EthereumWallet>, CryptoError> = storage_wallets
.iter()
.map(|sw| EthereumWallet::try_from(sw))
.collect();
match wallets_result {
Ok(wallets) => {
// Also update the in-memory fallback
let mut mem_wallets = ETH_WALLETS.lock().unwrap();
*mem_wallets = wallets.clone();
wallets
},
Err(e) => {
log::error!("Failed to convert wallets from storage format: {}", e);
// Fall back to in-memory storage
let mem_wallets = ETH_WALLETS.lock().unwrap();
mem_wallets.clone()
}
}
},
Err(e) => {
log::warn!("Failed to load wallets from persistent storage: {}", e);
// Fall back to in-memory storage
let mem_wallets = ETH_WALLETS.lock().unwrap();
mem_wallets.clone()
}
}
}
/// Creates an Ethereum wallet from the currently selected keypair.
pub fn create_ethereum_wallet() -> Result<EthereumWallet, CryptoError> {
// Get the currently selected keypair
let keypair = crate::vault::keypair::get_selected_keypair()?;
// Create an Ethereum wallet from the keypair
let wallet = EthereumWallet::from_keypair(&keypair)?;
// Store the wallet
let mut wallets = load_wallets();
wallets.push(wallet.clone());
save_wallets(&wallets)?;
Ok(wallet)
}
/// Creates an Ethereum wallet from a name and the currently selected keypair.
pub fn create_ethereum_wallet_from_name(name: &str) -> Result<EthereumWallet, CryptoError> {
// Get the currently selected keypair
let keypair = crate::vault::keypair::get_selected_keypair()?;
// Create an Ethereum wallet from the name and keypair
let wallet = EthereumWallet::from_name_and_keypair(name, &keypair)?;
// Store the wallet
let mut wallets = load_wallets();
wallets.push(wallet.clone());
save_wallets(&wallets)?;
Ok(wallet)
}
/// Creates an Ethereum wallet from a private key.
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<EthereumWallet, CryptoError> {
// Create an Ethereum wallet from the private key
let wallet = EthereumWallet::from_private_key(private_key)?;
// Store the wallet
let mut wallets = load_wallets();
wallets.push(wallet.clone());
save_wallets(&wallets)?;
Ok(wallet)
}
/// Gets the current Ethereum wallet.
pub fn get_current_ethereum_wallet() -> Result<EthereumWallet, CryptoError> {
let wallets = load_wallets();
if wallets.is_empty() {
return Err(CryptoError::NoKeypairSelected);
}
Ok(wallets.last().unwrap().clone())
}
/// Clears all Ethereum wallets.
pub fn clear_ethereum_wallets() {
// Clear both persistent and in-memory storage
if let Ok(store) = get_wallet_store() {
let _ = store.delete::<&str>(ETH_WALLET_STORAGE_KEY);
}
let mut mem_wallets = ETH_WALLETS.lock().unwrap();
mem_wallets.clear();
}
// Legacy functions for backward compatibility
/// Creates an Ethereum wallet from the currently selected keypair for a specific network. /// Creates an Ethereum wallet from the currently selected keypair for a specific network.
pub fn create_ethereum_wallet_for_network(network: networks::NetworkConfig) -> Result<EthereumWallet, CryptoError> { pub fn create_ethereum_wallet_for_network(network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
create_ethereum_wallet() // Get the currently selected keypair
let keypair = crate::vault::keyspace::get_selected_keypair()?;
// Create an Ethereum wallet from the keypair
let wallet = EthereumWallet::from_keypair(&keypair, network)?;
// Store the wallet
let mut wallets = ETH_WALLETS.lock().unwrap();
let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new);
network_wallets.push(wallet.clone());
Ok(wallet)
} }
/// Creates an Ethereum wallet from the currently selected keypair for the Peaq network. /// Creates an Ethereum wallet from the currently selected keypair for the Peaq network.
pub fn create_peaq_wallet() -> Result<EthereumWallet, CryptoError> { pub fn create_peaq_wallet() -> Result<EthereumWallet, CryptoError> {
create_ethereum_wallet() create_ethereum_wallet_for_network(networks::peaq())
} }
/// Creates an Ethereum wallet from the currently selected keypair for the Agung testnet. /// Creates an Ethereum wallet from the currently selected keypair for the Agung testnet.
pub fn create_agung_wallet() -> Result<EthereumWallet, CryptoError> { pub fn create_agung_wallet() -> Result<EthereumWallet, CryptoError> {
create_ethereum_wallet() create_ethereum_wallet_for_network(networks::agung())
} }
/// Gets the current Ethereum wallet for a specific network. /// Gets the current Ethereum wallet for a specific network.
pub fn get_current_ethereum_wallet_for_network(network_name: &str) -> Result<EthereumWallet, CryptoError> { pub fn get_current_ethereum_wallet_for_network(network_name: &str) -> Result<EthereumWallet, CryptoError> {
get_current_ethereum_wallet() let wallets = ETH_WALLETS.lock().unwrap();
let network_wallets = wallets.get(network_name).ok_or(CryptoError::NoKeypairSelected)?;
if network_wallets.is_empty() {
return Err(CryptoError::NoKeypairSelected);
}
Ok(network_wallets.last().unwrap().clone())
} }
/// Gets the current Ethereum wallet for the Peaq network. /// Gets the current Ethereum wallet for the Peaq network.
pub fn get_current_peaq_wallet() -> Result<EthereumWallet, CryptoError> { pub fn get_current_peaq_wallet() -> Result<EthereumWallet, CryptoError> {
get_current_ethereum_wallet() get_current_ethereum_wallet_for_network("Peaq")
} }
/// Gets the current Ethereum wallet for the Agung testnet. /// Gets the current Ethereum wallet for the Agung testnet.
pub fn get_current_agung_wallet() -> Result<EthereumWallet, CryptoError> { pub fn get_current_agung_wallet() -> Result<EthereumWallet, CryptoError> {
get_current_ethereum_wallet() get_current_ethereum_wallet_for_network("Agung")
}
/// Clears all Ethereum wallets.
pub fn clear_ethereum_wallets() {
let mut wallets = ETH_WALLETS.lock().unwrap();
wallets.clear();
} }
/// Clears Ethereum wallets for a specific network. /// Clears Ethereum wallets for a specific network.
pub fn clear_ethereum_wallets_for_network(network_name: &str) { pub fn clear_ethereum_wallets_for_network(network_name: &str) {
// In the new design, we don't have network-specific wallets, let mut wallets = ETH_WALLETS.lock().unwrap();
// so this is a no-op for backward compatibility wallets.remove(network_name);
} }
/// Creates an Ethereum wallet from a name and the currently selected keypair for a specific network. /// Creates an Ethereum wallet from a name and the currently selected keypair for a specific network.
pub fn create_ethereum_wallet_from_name_for_network(name: &str, network: networks::NetworkConfig) -> Result<EthereumWallet, CryptoError> { pub fn create_ethereum_wallet_from_name_for_network(name: &str, network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
create_ethereum_wallet_from_name(name) // Get the currently selected keypair
let keypair = crate::vault::keyspace::get_selected_keypair()?;
// Create an Ethereum wallet from the name and keypair
let wallet = EthereumWallet::from_name_and_keypair(name, &keypair, network)?;
// Store the wallet
let mut wallets = ETH_WALLETS.lock().unwrap();
let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new);
network_wallets.push(wallet.clone());
Ok(wallet)
}
/// Creates an Ethereum wallet from a name and the currently selected keypair for the Gnosis network.
pub fn create_ethereum_wallet_from_name(name: &str) -> Result<EthereumWallet, CryptoError> {
create_ethereum_wallet_from_name_for_network(name, networks::gnosis())
} }
/// Creates an Ethereum wallet from a private key for a specific network. /// Creates an Ethereum wallet from a private key for a specific network.
pub fn create_ethereum_wallet_from_private_key_for_network(private_key: &str, network: networks::NetworkConfig) -> Result<EthereumWallet, CryptoError> { pub fn create_ethereum_wallet_from_private_key_for_network(private_key: &str, network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
create_ethereum_wallet_from_private_key(private_key) // Create an Ethereum wallet from the private key
let wallet = EthereumWallet::from_private_key(private_key, network)?;
// Store the wallet
let mut wallets = ETH_WALLETS.lock().unwrap();
let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new);
network_wallets.push(wallet.clone());
Ok(wallet)
}
/// Creates an Ethereum wallet from a private key for the Gnosis network.
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<EthereumWallet, CryptoError> {
create_ethereum_wallet_from_private_key_for_network(private_key, networks::gnosis())
} }

View File

@ -22,123 +22,53 @@ fn test_network_config() {
#[test] #[test]
fn test_network_registry() { fn test_network_registry() {
// Get initial network names let network_names = networks::list_network_names();
let initial_network_names = list_network_names(); assert!(network_names.iter().any(|&name| name == "Gnosis"));
assert!(initial_network_names.iter().any(|name| name == "Gnosis")); assert!(network_names.iter().any(|&name| name == "Peaq"));
assert!(initial_network_names.iter().any(|name| name == "Peaq")); assert!(network_names.iter().any(|&name| name == "Agung"));
assert!(initial_network_names.iter().any(|name| name == "Agung"));
// Test proper network name lookup let gnosis_proper = networks::get_proper_network_name("gnosis");
let gnosis_proper = get_proper_network_name("gnosis"); assert_eq!(gnosis_proper, Some("Gnosis"));
assert_eq!(gnosis_proper, Some(networks::names::GNOSIS));
let peaq_proper = get_proper_network_name("peaq"); let peaq_proper = networks::get_proper_network_name("peaq");
assert_eq!(peaq_proper, Some(networks::names::PEAQ)); assert_eq!(peaq_proper, Some("Peaq"));
let agung_proper = get_proper_network_name("agung"); let agung_proper = networks::get_proper_network_name("agung");
assert_eq!(agung_proper, Some(networks::names::AGUNG)); assert_eq!(agung_proper, Some("Agung"));
let unknown = get_proper_network_name("unknown"); let unknown = networks::get_proper_network_name("unknown");
assert_eq!(unknown, None); assert_eq!(unknown, None);
// Test network lookup by name let gnosis_config = networks::get_network_by_name("Gnosis");
let gnosis_config = get_network_by_name("Gnosis");
assert!(gnosis_config.is_some()); assert!(gnosis_config.is_some());
assert_eq!(gnosis_config.unwrap().chain_id, 100); assert_eq!(gnosis_config.unwrap().chain_id, 100);
let unknown_config = get_network_by_name("Unknown"); let unknown_config = networks::get_network_by_name("Unknown");
assert!(unknown_config.is_none()); assert!(unknown_config.is_none());
// Test case insensitivity
let gnosis_lower = get_network_by_name("gnosis");
assert!(gnosis_lower.is_some());
assert_eq!(gnosis_lower.unwrap().chain_id, 100);
}
#[test]
fn test_network_registry_dynamic() {
// Register a new network
let success = register_network(
"Sepolia",
11155111,
"https://rpc.sepolia.org",
"https://sepolia.etherscan.io",
"ETH",
18
);
assert!(success);
// Verify the network was added
let network_names = list_network_names();
assert!(network_names.iter().any(|name| name == "Sepolia"));
// Get the network config
let sepolia = get_network_by_name("Sepolia");
assert!(sepolia.is_some());
let sepolia = sepolia.unwrap();
assert_eq!(sepolia.chain_id, 11155111);
assert_eq!(sepolia.token_symbol, "ETH");
assert_eq!(sepolia.rpc_url, "https://rpc.sepolia.org");
assert_eq!(sepolia.explorer_url, "https://sepolia.etherscan.io");
assert_eq!(sepolia.decimals, 18);
// Test case insensitivity
let sepolia_lower = get_network_by_name("sepolia");
assert!(sepolia_lower.is_some());
// Remove the network
let removed = remove_network("Sepolia");
assert!(removed);
// Verify the network was removed
let network_names = list_network_names();
assert!(!network_names.iter().any(|name| name == "Sepolia"));
// Try to get the removed network
let sepolia = get_network_by_name("Sepolia");
assert!(sepolia.is_none());
} }
#[test] #[test]
fn test_create_provider() { fn test_create_provider() {
// Create providers using network configs let gnosis = networks::gnosis();
let gnosis_provider = create_provider_from_config(&networks::gnosis()); let peaq = networks::peaq();
let peaq_provider = create_provider_from_config(&networks::peaq()); let agung = networks::agung();
let agung_provider = create_provider_from_config(&networks::agung());
// Create providers
let gnosis_provider = create_provider(&gnosis);
let peaq_provider = create_provider(&peaq);
let agung_provider = create_provider(&agung);
// They should all succeed // They should all succeed
assert!(gnosis_provider.is_ok()); assert!(gnosis_provider.is_ok());
assert!(peaq_provider.is_ok()); assert!(peaq_provider.is_ok());
assert!(agung_provider.is_ok()); assert!(agung_provider.is_ok());
// Create providers using network names // The convenience functions should also work
let gnosis_provider2 = create_provider("gnosis"); let gnosis_provider2 = create_gnosis_provider();
let peaq_provider2 = create_provider("peaq"); let peaq_provider2 = create_peaq_provider();
let agung_provider2 = create_provider("agung"); let agung_provider2 = create_agung_provider();
assert!(gnosis_provider2.is_ok()); assert!(gnosis_provider2.is_ok());
assert!(peaq_provider2.is_ok()); assert!(peaq_provider2.is_ok());
assert!(agung_provider2.is_ok()); assert!(agung_provider2.is_ok());
// The legacy convenience functions should also work
let gnosis_provider3 = create_gnosis_provider();
let peaq_provider3 = create_peaq_provider();
let agung_provider3 = create_agung_provider();
assert!(gnosis_provider3.is_ok());
assert!(peaq_provider3.is_ok());
assert!(agung_provider3.is_ok());
// Test with an unknown network
let unknown_provider = create_provider("unknown");
assert!(unknown_provider.is_err());
}
#[test]
fn test_get_all_networks() {
let networks = get_all_networks();
assert!(!networks.is_empty());
assert!(networks.contains_key("gnosis"));
assert!(networks.contains_key("peaq"));
assert!(networks.contains_key("agung"));
} }

View File

@ -42,7 +42,7 @@ fn test_get_balance() {
// Create a provider // Create a provider
let network = networks::gnosis(); let network = networks::gnosis();
let provider_result = create_provider_from_config(&network); let provider_result = create_provider(&network);
// The provider creation should succeed // The provider creation should succeed
assert!(provider_result.is_ok()); assert!(provider_result.is_ok());
@ -59,10 +59,10 @@ fn test_send_eth() {
// Create a wallet // Create a wallet
let keypair = KeyPair::new("test_keypair6"); let keypair = KeyPair::new("test_keypair6");
let network = networks::gnosis(); let network = networks::gnosis();
let wallet = EthereumWallet::from_keypair(&keypair).unwrap(); let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
// Create a provider // Create a provider
let provider_result = create_provider_from_config(&network); let provider_result = create_provider(&network);
assert!(provider_result.is_ok()); assert!(provider_result.is_ok());
// We can't actually test send_eth without a blockchain // We can't actually test send_eth without a blockchain

View File

@ -3,57 +3,59 @@
use crate::vault::ethereum::*; use crate::vault::ethereum::*;
use crate::vault::keypair::implementation::KeyPair; use crate::vault::keypair::implementation::KeyPair;
use ethers::utils::hex; use ethers::utils::hex;
use ethers::prelude::Signer;
#[test] #[test]
fn test_ethereum_wallet_from_keypair() { fn test_ethereum_wallet_from_keypair() {
let keypair = KeyPair::new("test_keypair"); let keypair = KeyPair::new("test_keypair");
let network = networks::gnosis();
let wallet = EthereumWallet::from_keypair(&keypair).unwrap(); let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
assert_eq!(wallet.network.name, "Gnosis");
assert_eq!(wallet.network.chain_id, 100);
// The address should be a valid Ethereum address // The address should be a valid Ethereum address
assert!(wallet.address_string().starts_with("0x")); assert!(wallet.address_string().starts_with("0x"));
// The wallet should not have a name
assert!(wallet.name.is_none());
} }
#[test] #[test]
fn test_ethereum_wallet_from_name_and_keypair() { fn test_ethereum_wallet_from_name_and_keypair() {
let keypair = KeyPair::new("test_keypair2"); let keypair = KeyPair::new("test_keypair2");
let network = networks::gnosis();
let wallet = EthereumWallet::from_name_and_keypair("test", &keypair).unwrap(); let wallet = EthereumWallet::from_name_and_keypair("test", &keypair, network.clone()).unwrap();
assert_eq!(wallet.network.name, "Gnosis");
assert_eq!(wallet.network.chain_id, 100);
// The address should be a valid Ethereum address // The address should be a valid Ethereum address
assert!(wallet.address_string().starts_with("0x")); assert!(wallet.address_string().starts_with("0x"));
// The wallet should have the correct name
assert_eq!(wallet.name, Some("test".to_string()));
// Creating another wallet with the same name and keypair should yield the same address // Creating another wallet with the same name and keypair should yield the same address
let wallet2 = EthereumWallet::from_name_and_keypair("test", &keypair).unwrap(); let wallet2 = EthereumWallet::from_name_and_keypair("test", &keypair, network.clone()).unwrap();
assert_eq!(wallet.address_string(), wallet2.address_string()); assert_eq!(wallet.address, wallet2.address);
// Creating a wallet with a different name should yield a different address // Creating a wallet with a different name should yield a different address
let wallet3 = EthereumWallet::from_name_and_keypair("test2", &keypair).unwrap(); let wallet3 = EthereumWallet::from_name_and_keypair("test2", &keypair, network.clone()).unwrap();
assert_ne!(wallet.address_string(), wallet3.address_string()); assert_ne!(wallet.address, wallet3.address);
} }
#[test] #[test]
fn test_ethereum_wallet_from_private_key() { fn test_ethereum_wallet_from_private_key() {
let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let network = networks::gnosis();
let wallet = EthereumWallet::from_private_key(private_key).unwrap(); let wallet = EthereumWallet::from_private_key(private_key, network.clone()).unwrap();
assert_eq!(wallet.network.name, "Gnosis");
assert_eq!(wallet.network.chain_id, 100);
// The address should be a valid Ethereum address // The address should be a valid Ethereum address
assert!(wallet.address_string().starts_with("0x")); assert!(wallet.address_string().starts_with("0x"));
// The wallet should not have a name
assert!(wallet.name.is_none());
// The address should be deterministic based on the private key // The address should be deterministic based on the private key
let wallet2 = EthereumWallet::from_private_key(private_key).unwrap(); let wallet2 = EthereumWallet::from_private_key(private_key, network.clone()).unwrap();
assert_eq!(wallet.address_string(), wallet2.address_string()); assert_eq!(wallet.address, wallet2.address);
} }
#[test] #[test]
@ -65,63 +67,52 @@ fn test_wallet_management() {
crate::vault::keypair::session_manager::create_space("test_space").unwrap(); crate::vault::keypair::session_manager::create_space("test_space").unwrap();
crate::vault::keypair::create_keypair("test_keypair3").unwrap(); crate::vault::keypair::create_keypair("test_keypair3").unwrap();
// Create a wallet // Create wallets for different networks
let wallet = create_ethereum_wallet().unwrap(); let gnosis_wallet = create_ethereum_wallet_for_network(networks::gnosis()).unwrap();
let peaq_wallet = create_ethereum_wallet_for_network(networks::peaq()).unwrap();
let agung_wallet = create_ethereum_wallet_for_network(networks::agung()).unwrap();
// Get the current wallet // Get the current wallets
let current_wallet = get_current_ethereum_wallet().unwrap(); let current_gnosis = get_current_ethereum_wallet_for_network("Gnosis").unwrap();
let current_peaq = get_current_ethereum_wallet_for_network("Peaq").unwrap();
let current_agung = get_current_ethereum_wallet_for_network("Agung").unwrap();
// Check that they match // Check that they match
assert_eq!(wallet.address_string(), current_wallet.address_string()); assert_eq!(gnosis_wallet.address, current_gnosis.address);
assert_eq!(peaq_wallet.address, current_peaq.address);
assert_eq!(agung_wallet.address, current_agung.address);
// Clear wallets for a specific network
clear_ethereum_wallets_for_network("Gnosis");
// Check that the wallet is gone
let result = get_current_ethereum_wallet_for_network("Gnosis");
assert!(result.is_err());
// But the others should still be there
let current_peaq = get_current_ethereum_wallet_for_network("Peaq").unwrap();
let current_agung = get_current_ethereum_wallet_for_network("Agung").unwrap();
assert_eq!(peaq_wallet.address, current_peaq.address);
assert_eq!(agung_wallet.address, current_agung.address);
// Clear all wallets // Clear all wallets
clear_ethereum_wallets(); clear_ethereum_wallets();
// Check that the wallet is gone // Check that all wallets are gone
let result = get_current_ethereum_wallet(); let result1 = get_current_ethereum_wallet_for_network("Gnosis");
assert!(result.is_err()); let result2 = get_current_ethereum_wallet_for_network("Peaq");
let result3 = get_current_ethereum_wallet_for_network("Agung");
// The legacy network-specific wallet functions have been removed assert!(result1.is_err());
// We now use a single wallet that works across all networks assert!(result2.is_err());
assert!(result3.is_err());
// Create a new wallet (network-agnostic)
let wallet = create_ethereum_wallet().unwrap();
// Check that it's accessible
let current_wallet = get_current_ethereum_wallet().unwrap();
assert_eq!(wallet.address_string(), current_wallet.address_string());
// Test for_network functionality to get network-specific wallet
let gnosis_network = networks::gnosis();
let peaq_network = networks::peaq();
let agung_network = networks::agung();
// The wallet address should remain the same regardless of network
let wallet_address = current_wallet.address_string();
// Network-specific wallets have different chain IDs but same address
// Just verify different chain IDs here
let gnosis_wallet = current_wallet.for_network(&gnosis_network);
let peaq_wallet = current_wallet.for_network(&peaq_network);
let agung_wallet = current_wallet.for_network(&agung_network);
// Check that chain IDs are different
assert_ne!(gnosis_wallet.chain_id(), peaq_wallet.chain_id());
assert_ne!(gnosis_wallet.chain_id(), agung_wallet.chain_id());
// Clear all wallets
clear_ethereum_wallets();
// Check that the wallet is gone
let result = get_current_ethereum_wallet();
assert!(result.is_err());
} }
#[test] #[test]
fn test_sign_message() { fn test_sign_message() {
let keypair = KeyPair::new("test_keypair4"); let keypair = KeyPair::new("test_keypair4");
let network = networks::gnosis();
let wallet = EthereumWallet::from_keypair(&keypair).unwrap(); let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
// Create a tokio runtime for the async test // Create a tokio runtime for the async test
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@ -137,8 +128,9 @@ fn test_sign_message() {
#[test] #[test]
fn test_private_key_hex() { fn test_private_key_hex() {
let keypair = KeyPair::new("test_keypair5"); let keypair = KeyPair::new("test_keypair5");
let network = networks::gnosis();
let wallet = EthereumWallet::from_keypair(&keypair).unwrap(); let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
// Get the private key as hex // Get the private key as hex
let private_key_hex = wallet.private_key_hex(); let private_key_hex = wallet.private_key_hex();
@ -149,55 +141,3 @@ fn test_private_key_hex() {
// It should be possible to parse it as hex // It should be possible to parse it as hex
let _bytes = hex::decode(private_key_hex).unwrap(); let _bytes = hex::decode(private_key_hex).unwrap();
} }
#[test]
fn test_wallet_for_network() {
let keypair = KeyPair::new("test_keypair6");
let wallet = EthereumWallet::from_keypair(&keypair).unwrap();
// Get wallets for different networks
let gnosis_network = networks::gnosis();
let peaq_network = networks::peaq();
let agung_network = networks::agung();
let gnosis_wallet = wallet.for_network(&gnosis_network);
let peaq_wallet = wallet.for_network(&peaq_network);
let agung_wallet = wallet.for_network(&agung_network);
// The chain IDs should match the networks
assert_eq!(gnosis_wallet.chain_id(), gnosis_network.chain_id);
assert_eq!(peaq_wallet.chain_id(), peaq_network.chain_id);
assert_eq!(agung_wallet.chain_id(), agung_network.chain_id);
}
#[test]
fn test_multi_network_configuration() {
let keypair = KeyPair::new("test_keypair7");
// Create a network-agnostic wallet
let wallet = EthereumWallet::from_keypair(&keypair).unwrap();
// Test the for_network functionality to get network-specific configurations
let gnosis_network = networks::gnosis();
let peaq_network = networks::peaq();
let agung_network = networks::agung();
// Get the wallet's base address for comparison
let wallet_address = format!("{:?}", wallet.address);
// Create network-specific signers
let gnosis_wallet = wallet.for_network(&gnosis_network);
let peaq_wallet = wallet.for_network(&peaq_network);
let agung_wallet = wallet.for_network(&agung_network);
// The signers should each have their network's chain ID
assert_eq!(gnosis_wallet.chain_id(), gnosis_network.chain_id);
assert_eq!(peaq_wallet.chain_id(), peaq_network.chain_id);
assert_eq!(agung_wallet.chain_id(), agung_network.chain_id);
// And each should have the same address as the original wallet
assert_eq!(format!("{:?}", gnosis_wallet.address()), wallet_address);
assert_eq!(format!("{:?}", peaq_wallet.address()), wallet_address);
assert_eq!(format!("{:?}", agung_wallet.address()), wallet_address);
}

View File

@ -1,12 +1,10 @@
//! Ethereum transaction functionality. //! Ethereum transaction functionality.
use ethers::prelude::*; use ethers::prelude::*;
use ethers::types::transaction::eip2718::TypedTransaction;
use crate::vault::error::CryptoError; use crate::vault::error::CryptoError;
use super::wallet::EthereumWallet; use super::wallet::EthereumWallet;
use super::networks::NetworkConfig; use super::networks::NetworkConfig;
use super::provider;
/// Formats a token balance for display. /// Formats a token balance for display.
pub fn format_balance(balance: U256, network: &NetworkConfig) -> String { pub fn format_balance(balance: U256, network: &NetworkConfig) -> String {
@ -21,16 +19,7 @@ pub fn format_balance(balance: U256, network: &NetworkConfig) -> String {
} }
/// Gets the balance of an Ethereum address. /// Gets the balance of an Ethereum address.
pub async fn get_balance(network_name: &str, address: Address) -> Result<U256, CryptoError> { pub async fn get_balance(provider: &Provider<Http>, address: Address) -> Result<U256, CryptoError> {
let provider = provider::create_provider(network_name)?;
provider.get_balance(address, None)
.await
.map_err(|e| CryptoError::SerializationError(format!("Failed to get balance: {}", e)))
}
/// Gets the balance of an Ethereum address using a provider.
pub async fn get_balance_with_provider(provider: &Provider<Http>, address: Address) -> Result<U256, CryptoError> {
provider.get_balance(address, None) provider.get_balance(address, None)
.await .await
.map_err(|e| CryptoError::SerializationError(format!("Failed to get balance: {}", e))) .map_err(|e| CryptoError::SerializationError(format!("Failed to get balance: {}", e)))
@ -38,56 +27,6 @@ pub async fn get_balance_with_provider(provider: &Provider<Http>, address: Addre
/// Sends Ethereum from one address to another. /// Sends Ethereum from one address to another.
pub async fn send_eth( pub async fn send_eth(
wallet: &EthereumWallet,
network_name: &str,
to: Address,
amount: U256,
) -> Result<H256, CryptoError> {
// Get the network configuration
let network = super::networks::get_network_by_name(network_name)
.ok_or_else(|| CryptoError::SerializationError(format!("Unknown network: {}", network_name)))?;
// Create a provider
let provider = provider::create_provider(network_name)?;
// Create a client with the wallet configured for this network
let network_wallet = wallet.for_network(&network);
let client = SignerMiddleware::new(
provider.clone(),
network_wallet,
);
// Estimate gas
let tx = TransactionRequest::new()
.to(to)
.value(amount);
// Convert TransactionRequest to TypedTransaction explicitly
let typed_tx: TypedTransaction = tx.into();
let gas = client.estimate_gas(&typed_tx, None)
.await
.map_err(|e| CryptoError::SerializationError(format!("Failed to estimate gas: {}", e)))?;
log::info!("Estimated gas: {:?}", gas);
// Create the transaction
let tx = TransactionRequest::new()
.to(to)
.value(amount)
.gas(gas);
// Send the transaction
let pending_tx = client.send_transaction(tx, Some(BlockId::Number((BlockNumber::Latest).into())))
.await
.map_err(|e| CryptoError::SerializationError(format!("Failed to send transaction: {}", e)))?;
// Return the transaction hash instead of waiting for the receipt
Ok(pending_tx.tx_hash())
}
// Legacy function for backward compatibility
/// Sends Ethereum from one address to another using a provider.
pub async fn send_eth_with_provider(
wallet: &EthereumWallet, wallet: &EthereumWallet,
provider: &Provider<Http>, provider: &Provider<Http>,
to: Address, to: Address,
@ -96,29 +35,17 @@ pub async fn send_eth_with_provider(
// Create a client with the wallet // Create a client with the wallet
let client = SignerMiddleware::new( let client = SignerMiddleware::new(
provider.clone(), provider.clone(),
wallet.signer.clone(), wallet.wallet.clone(),
); );
// Estimate gas
let tx = TransactionRequest::new()
.to(to)
.value(amount);
// Convert TransactionRequest to TypedTransaction explicitly
let typed_tx: TypedTransaction = tx.into();
let gas = client.estimate_gas(&typed_tx, None)
.await
.map_err(|e| CryptoError::SerializationError(format!("Failed to estimate gas: {}", e)))?;
log::info!("Estimated gas: {:?}", gas);
// Create the transaction // Create the transaction
let tx = TransactionRequest::new() let tx = TransactionRequest::new()
.to(to) .to(to)
.value(amount) .value(amount)
.gas(gas); .gas(21000);
// Send the transaction // Send the transaction
let pending_tx = client.send_transaction(tx, Some(BlockId::Number((BlockNumber::Latest).into()))) let pending_tx = client.send_transaction(tx, None)
.await .await
.map_err(|e| CryptoError::SerializationError(format!("Failed to send transaction: {}", e)))?; .map_err(|e| CryptoError::SerializationError(format!("Failed to send transaction: {}", e)))?;

View File

@ -4,24 +4,27 @@ use ethers::prelude::*;
use ethers::signers::{LocalWallet, Signer, Wallet}; use ethers::signers::{LocalWallet, Signer, Wallet};
use ethers::utils::hex; use ethers::utils::hex;
use k256::ecdsa::SigningKey; use k256::ecdsa::SigningKey;
use sha2::{Digest, Sha256};
use std::str::FromStr; use std::str::FromStr;
use sha2::{Sha256, Digest};
use crate::vault::error::CryptoError;
use crate::vault::keypair::KeyPair;
use super::networks::NetworkConfig; use super::networks::NetworkConfig;
use crate::vault;
use crate::vault::error::CryptoError;
/// An Ethereum wallet derived from a keypair. /// An Ethereum wallet derived from a keypair.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EthereumWallet { pub struct EthereumWallet {
pub address: Address, pub address: Address,
pub signer: Wallet<SigningKey>, pub wallet: Wallet<SigningKey>,
pub name: Option<String>, pub network: NetworkConfig,
} }
impl EthereumWallet { impl EthereumWallet {
/// Creates a new Ethereum wallet from a keypair. /// Creates a new Ethereum wallet from a keypair for a specific network.
pub fn from_keypair(keypair: &KeyPair) -> Result<Self, CryptoError> { pub fn from_keypair(
keypair: &vault::keyspace::keypair_types::KeyPair,
network: NetworkConfig,
) -> Result<Self, CryptoError> {
// Get the private key bytes from the keypair // Get the private key bytes from the keypair
let private_key_bytes = keypair.signing_key.to_bytes(); let private_key_bytes = keypair.signing_key.to_bytes();
@ -29,21 +32,26 @@ impl EthereumWallet {
let private_key_hex = hex::encode(private_key_bytes); let private_key_hex = hex::encode(private_key_bytes);
// Create an Ethereum wallet from the private key // Create an Ethereum wallet from the private key
let signer = LocalWallet::from_str(&private_key_hex) let wallet = LocalWallet::from_str(&private_key_hex)
.map_err(|_e| CryptoError::InvalidKeyLength)?; .map_err(|_e| CryptoError::InvalidKeyLength)?
.with_chain_id(network.chain_id);
// Get the Ethereum address // Get the Ethereum address
let address = signer.address(); let address = wallet.address();
Ok(EthereumWallet { Ok(EthereumWallet {
address, address,
signer, wallet,
name: None, network,
}) })
} }
/// Creates a new Ethereum wallet from a name and keypair (deterministic derivation). /// Creates a new Ethereum wallet from a name and keypair (deterministic derivation) for a specific network.
pub fn from_name_and_keypair(name: &str, keypair: &KeyPair) -> Result<Self, CryptoError> { pub fn from_name_and_keypair(
name: &str,
keypair: &vault::keyspace::keypair_types::KeyPair,
network: NetworkConfig,
) -> Result<Self, CryptoError> {
// Get the private key bytes from the keypair // Get the private key bytes from the keypair
let private_key_bytes = keypair.signing_key.to_bytes(); let private_key_bytes = keypair.signing_key.to_bytes();
@ -57,35 +65,40 @@ impl EthereumWallet {
let private_key_hex = hex::encode(seed); let private_key_hex = hex::encode(seed);
// Create an Ethereum wallet from the derived private key // Create an Ethereum wallet from the derived private key
let signer = LocalWallet::from_str(&private_key_hex) let wallet = LocalWallet::from_str(&private_key_hex)
.map_err(|_e| CryptoError::InvalidKeyLength)?; .map_err(|_e| CryptoError::InvalidKeyLength)?
.with_chain_id(network.chain_id);
// Get the Ethereum address // Get the Ethereum address
let address = signer.address(); let address = wallet.address();
Ok(EthereumWallet { Ok(EthereumWallet {
address, address,
signer, wallet,
name: Some(name.to_string()), network,
}) })
} }
/// Creates a new Ethereum wallet from a private key. /// Creates a new Ethereum wallet from a private key for a specific network.
pub fn from_private_key(private_key: &str) -> Result<Self, CryptoError> { pub fn from_private_key(
private_key: &str,
network: NetworkConfig,
) -> Result<Self, CryptoError> {
// Remove 0x prefix if present // Remove 0x prefix if present
let private_key_clean = private_key.trim_start_matches("0x"); let private_key_clean = private_key.trim_start_matches("0x");
// Create an Ethereum wallet from the private key // Create an Ethereum wallet from the private key
let signer = LocalWallet::from_str(private_key_clean) let wallet = LocalWallet::from_str(private_key_clean)
.map_err(|_e| CryptoError::InvalidKeyLength)?; .map_err(|_e| CryptoError::InvalidKeyLength)?
.with_chain_id(network.chain_id);
// Get the Ethereum address // Get the Ethereum address
let address = signer.address(); let address = wallet.address();
Ok(EthereumWallet { Ok(EthereumWallet {
address, address,
signer, wallet,
name: None, network,
}) })
} }
@ -96,7 +109,9 @@ impl EthereumWallet {
/// Signs a message with the Ethereum wallet. /// Signs a message with the Ethereum wallet.
pub async fn sign_message(&self, message: &[u8]) -> Result<String, CryptoError> { pub async fn sign_message(&self, message: &[u8]) -> Result<String, CryptoError> {
let signature = self.signer.sign_message(message) let signature = self
.wallet
.sign_message(message)
.await .await
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?; .map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
@ -105,34 +120,7 @@ impl EthereumWallet {
/// Gets the private key as a hex string. /// Gets the private key as a hex string.
pub fn private_key_hex(&self) -> String { pub fn private_key_hex(&self) -> String {
let bytes = self.signer.signer().to_bytes(); let bytes = self.wallet.signer().to_bytes();
hex::encode(bytes) hex::encode(bytes)
} }
/// Gets a wallet configured for a specific network.
pub fn for_network(&self, network: &NetworkConfig) -> Wallet<SigningKey> {
self.signer.clone().with_chain_id(network.chain_id)
}
}
// Legacy functions for backward compatibility
impl EthereumWallet {
/// Creates a new Ethereum wallet from a keypair for a specific network.
/// This is kept for backward compatibility.
pub fn from_keypair_for_network(keypair: &KeyPair, network: NetworkConfig) -> Result<Self, CryptoError> {
Self::from_keypair(keypair)
}
/// Creates a new Ethereum wallet from a name and keypair (deterministic derivation) for a specific network.
/// This is kept for backward compatibility.
pub fn from_name_and_keypair_for_network(name: &str, keypair: &KeyPair, _network: NetworkConfig) -> Result<Self, CryptoError> {
Self::from_name_and_keypair(name, keypair)
}
/// Creates a new Ethereum wallet from a private key for a specific network.
/// This is kept for backward compatibility.
pub fn from_private_key_for_network(private_key: &str, _network: NetworkConfig) -> Result<Self, CryptoError> {
Self::from_private_key(private_key)
}
} }

View File

@ -1,7 +0,0 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View File

@ -1,3 +0,0 @@
mod implementation_tests;
mod keypair_types_tests;
mod session_manager_tests;

View File

@ -220,14 +220,14 @@ To include the Hero Vault Keypair module in your Rust project, add the following
```toml ```toml
[dependencies] [dependencies]
vault = "0.1.0" # Replace with the actual version hero_vault = "0.1.0" # Replace with the actual version
``` ```
Then, you can import and use the module in your Rust code: Then, you can import and use the module in your Rust code:
```rust ```rust
use vault::vault::keypair::{KeySpace, KeyPair}; use hero_vault::vault::keypair::{KeySpace, KeyPair};
use vault::vault::error::CryptoError; use hero_vault::vault::error::CryptoError;
``` ```
## Testing ## Testing
@ -263,7 +263,7 @@ The module uses the `CryptoError` type for handling errors that can occur during
## Examples ## Examples
For examples of how to use the Keypair module, see the `examples/vault` directory, particularly: For examples of how to use the Keypair module, see the `examples/hero_vault` directory, particularly:
- `example.rhai` - Basic example demonstrating key management and signing - `example.rhai` - Basic example demonstrating key management and signing
- `advanced_example.rhai` - Advanced example with error handling - `advanced_example.rhai` - Advanced example with error handling

View File

@ -1,13 +1,15 @@
/// Implementation of keypair functionality. /// Implementation of keypair functionality.
use k256::ecdsa::{
use k256::ecdsa::{SigningKey, VerifyingKey, signature::{Signer, Verifier}, Signature}; signature::{Signer, Verifier},
Signature, SigningKey, VerifyingKey,
};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap; use std::collections::HashMap;
use sha2::{Sha256, Digest};
use crate::vault::symmetric::implementation;
use crate::vault::error::CryptoError; use crate::vault::error::CryptoError;
use crate::vault::symmetric::implementation;
/// A keypair for signing and verifying messages. /// A keypair for signing and verifying messages.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -22,8 +24,8 @@ pub struct KeyPair {
// Serialization helpers for VerifyingKey // Serialization helpers for VerifyingKey
mod verifying_key_serde { mod verifying_key_serde {
use super::*; use super::*;
use serde::{Serializer, Deserializer};
use serde::de::{self, Visitor}; use serde::de::{self, Visitor};
use serde::{Deserializer, Serializer};
use std::fmt; use std::fmt;
pub fn serialize<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error> pub fn serialize<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error>
@ -83,8 +85,8 @@ mod verifying_key_serde {
// Serialization helpers for SigningKey // Serialization helpers for SigningKey
mod signing_key_serde { mod signing_key_serde {
use super::*; use super::*;
use serde::{Serializer, Deserializer};
use serde::de::{self, Visitor}; use serde::de::{self, Visitor};
use serde::{Deserializer, Serializer};
use std::fmt; use std::fmt;
pub fn serialize<S>(key: &SigningKey, serializer: S) -> Result<S::Ok, S::Error> pub fn serialize<S>(key: &SigningKey, serializer: S) -> Result<S::Ok, S::Error>
@ -185,9 +187,13 @@ impl KeyPair {
} }
/// Verifies a message signature using only a public key. /// Verifies a message signature using only a public key.
pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> { pub fn verify_with_public_key(
let verifying_key = VerifyingKey::from_sec1_bytes(public_key) public_key: &[u8],
.map_err(|_| CryptoError::InvalidKeyLength)?; message: &[u8],
signature_bytes: &[u8],
) -> Result<bool, CryptoError> {
let verifying_key =
VerifyingKey::from_sec1_bytes(public_key).map_err(|_| CryptoError::InvalidKeyLength)?;
let signature = Signature::from_bytes(signature_bytes.into()) let signature = Signature::from_bytes(signature_bytes.into())
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?; .map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
@ -199,38 +205,48 @@ impl KeyPair {
} }
/// Encrypts a message using the recipient's public key. /// Encrypts a message using the recipient's public key.
/// This implements ECIES (Elliptic Curve Integrated Encryption Scheme): /// This implements a simplified version of ECIES (Elliptic Curve Integrated Encryption Scheme):
/// 1. Generate an ephemeral keypair /// 1. Generate a random symmetric key
/// 2. Derive a shared secret using ECDH /// 2. Encrypt the message with the symmetric key
/// 3. Derive encryption key from the shared secret /// 3. Encrypt the symmetric key with the recipient's public key
/// 4. Encrypt the message using symmetric encryption /// 4. Return the encrypted key and the ciphertext
/// 5. Return the ephemeral public key and the ciphertext pub fn encrypt_asymmetric(
pub fn encrypt_asymmetric(&self, recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> { &self,
// Parse recipient's public key recipient_public_key: &[u8],
let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key) message: &[u8],
) -> Result<Vec<u8>, CryptoError> {
// Validate recipient's public key format
VerifyingKey::from_sec1_bytes(recipient_public_key)
.map_err(|_| CryptoError::InvalidKeyLength)?; .map_err(|_| CryptoError::InvalidKeyLength)?;
// Generate ephemeral keypair // Generate a random symmetric key
let ephemeral_signing_key = SigningKey::random(&mut OsRng); let symmetric_key = implementation::generate_symmetric_key();
let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key);
// Derive shared secret (this is a simplified ECDH) // Encrypt the message with the symmetric key
// In a real implementation, we would use proper ECDH, but for this example: let encrypted_message = implementation::encrypt_with_key(&symmetric_key, message)
let shared_point = recipient_key.to_encoded_point(false); .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
let shared_secret = {
// Encrypt the symmetric key with the recipient's public key
// For simplicity, we'll just use the recipient's public key to derive an encryption key
// This is not secure for production use, but works for our test
let key_encryption_key = {
let mut hasher = Sha256::default(); let mut hasher = Sha256::default();
hasher.update(ephemeral_signing_key.to_bytes()); hasher.update(recipient_public_key);
hasher.update(shared_point.as_bytes()); // Use a fixed salt for testing purposes
hasher.update(b"fixed_salt_for_testing");
hasher.finalize().to_vec() hasher.finalize().to_vec()
}; };
// Encrypt the message using the derived key // Encrypt the symmetric key
let ciphertext = implementation::encrypt_with_key(&shared_secret, message) let encrypted_key = implementation::encrypt_with_key(&key_encryption_key, &symmetric_key)
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
// Format: ephemeral_public_key || ciphertext // Format: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message
let mut result = ephemeral_public_key.to_sec1_bytes().to_vec(); let mut result = Vec::new();
result.extend_from_slice(&ciphertext); let key_len = encrypted_key.len() as u32;
result.extend_from_slice(&key_len.to_be_bytes());
result.extend_from_slice(&encrypted_key);
result.extend_from_slice(&encrypted_message);
Ok(result) Ok(result)
} }
@ -238,32 +254,46 @@ impl KeyPair {
/// Decrypts a message using the recipient's private key. /// Decrypts a message using the recipient's private key.
/// This is the counterpart to encrypt_asymmetric. /// This is the counterpart to encrypt_asymmetric.
pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> { pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
// The first 33 or 65 bytes (depending on compression) are the ephemeral public key // The format is: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message
// For simplicity, we'll assume uncompressed keys (65 bytes) if ciphertext.len() <= 4 {
if ciphertext.len() <= 65 { return Err(CryptoError::DecryptionFailed(
return Err(CryptoError::DecryptionFailed("Ciphertext too short".to_string())); "Ciphertext too short".to_string(),
));
} }
// Extract ephemeral public key and actual ciphertext // Extract the encrypted key length
let ephemeral_public_key = &ciphertext[..65]; let mut key_len_bytes = [0u8; 4];
let actual_ciphertext = &ciphertext[65..]; key_len_bytes.copy_from_slice(&ciphertext[0..4]);
let key_len = u32::from_be_bytes(key_len_bytes) as usize;
// Parse ephemeral public key // Check if the ciphertext is long enough
let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key) if ciphertext.len() <= 4 + key_len {
.map_err(|_| CryptoError::InvalidKeyLength)?; return Err(CryptoError::DecryptionFailed(
"Ciphertext too short".to_string(),
));
}
// Derive shared secret (simplified ECDH) // Extract the encrypted key and the encrypted message
let shared_point = sender_key.to_encoded_point(false); let encrypted_key = &ciphertext[4..4 + key_len];
let shared_secret = { let encrypted_message = &ciphertext[4 + key_len..];
// Decrypt the symmetric key
// Use the same key derivation as in encryption
let key_encryption_key = {
let mut hasher = Sha256::default(); let mut hasher = Sha256::default();
hasher.update(self.signing_key.to_bytes()); hasher.update(self.verifying_key.to_sec1_bytes());
hasher.update(shared_point.as_bytes()); // Use the same fixed salt as in encryption
hasher.update(b"fixed_salt_for_testing");
hasher.finalize().to_vec() hasher.finalize().to_vec()
}; };
// Decrypt the message using the derived key // Decrypt the symmetric key
implementation::decrypt_with_key(&shared_secret, actual_ciphertext) let symmetric_key = implementation::decrypt_with_key(&key_encryption_key, encrypted_key)
.map_err(|e| CryptoError::DecryptionFailed(e.to_string())) .map_err(|e| CryptoError::DecryptionFailed(format!("Failed to decrypt key: {}", e)))?;
// Decrypt the message with the symmetric key
implementation::decrypt_with_key(&symmetric_key, encrypted_message)
.map_err(|e| CryptoError::DecryptionFailed(format!("Failed to decrypt message: {}", e)))
} }
} }
@ -296,7 +326,9 @@ impl KeySpace {
/// Gets a keypair by name. /// Gets a keypair by name.
pub fn get_keypair(&self, name: &str) -> Result<&KeyPair, CryptoError> { pub fn get_keypair(&self, name: &str) -> Result<&KeyPair, CryptoError> {
self.keypairs.get(name).ok_or(CryptoError::KeypairNotFound(name.to_string())) self.keypairs
.get(name)
.ok_or(CryptoError::KeypairNotFound(name.to_string()))
} }
/// Lists all keypair names in the space. /// Lists all keypair names in the space.
@ -304,4 +336,3 @@ impl KeySpace {
self.keypairs.keys().cloned().collect() self.keypairs.keys().cloned().collect()
} }
} }

View File

@ -2,7 +2,7 @@ use once_cell::sync::Lazy;
use std::sync::Mutex; use std::sync::Mutex;
use crate::vault::error::CryptoError; use crate::vault::error::CryptoError;
use crate::vault::keypair::keypair_types::{KeyPair, KeySpace}; // Assuming KeyPair and KeySpace will be in keypair_types.rs use crate::vault::keyspace::keypair_types::{KeyPair, KeySpace}; // Assuming KeyPair and KeySpace will be in keypair_types.rs
/// Session state for the current key space and selected keypair. /// Session state for the current key space and selected keypair.
pub struct Session { pub struct Session {

View File

@ -0,0 +1,36 @@
# Keyspace Module Specification
This document explains the purpose and functionality of the `keyspace` module within the Hero Vault.
## Purpose of the Module
The `keyspace` module provides a secure and organized way to manage cryptographic keypairs. It allows for the creation, storage, loading, and utilization of keypairs within designated containers called keyspaces. This module is essential for handling sensitive cryptographic material securely.
## What is a Keyspace?
A keyspace is a logical container designed to hold multiple cryptographic keypairs. It is represented by the `KeySpace` struct in the code. Keyspaces can be encrypted and persisted to disk, providing a secure method for storing collections of keypairs. Each keyspace is identified by a unique name.
## What is a Keypair?
A keypair, represented by the `KeyPair` struct, is a fundamental cryptographic element consisting of a mathematically linked pair of keys: a public key and a private key. In this module, ECDSA (Elliptic Curve Digital Signature Algorithm) keypairs are used.
* **Private Key:** This key is kept secret and is used for operations like signing data or decrypting messages intended for the keypair's owner.
* **Public Key:** This key can be shared openly and is used to verify signatures created by the corresponding private key or to encrypt messages that can only be decrypted by the private key.
## How Many Keypairs Per Space?
A keyspace can hold multiple keypairs. The `KeySpace` struct uses a `HashMap` to store keypairs, where each keypair is associated with a unique string name. There is no inherent, fixed limit on the number of keypairs a keyspace can contain, beyond the practical limitations of system memory.
## How Do We Load Them?
Keyspaces are loaded from persistent storage (disk) using the `KeySpace::load` function, which requires the keyspace name and a password for decryption. Once a `KeySpace` object is loaded into memory, it can be set as the currently active keyspace for the session using the `session_manager::set_current_space` function. Individual keypairs within the loaded keyspace are then accessed by their names using functions like `session_manager::select_keypair` and `session_manager::get_selected_keypair`.
## What Do They Do?
Keypairs within a keyspace are used to perform various cryptographic operations. The `KeyPair` struct provides methods for:
* **Digital Signatures:** Signing messages with the private key (`KeyPair::sign`) and verifying those signatures with the public key (`KeyPair::verify`).
* **Ethereum Address Derivation:** Generating an Ethereum address from the public key (`KeyPair::to_ethereum_address`).
* **Asymmetric Encryption/Decryption:** Encrypting data using a recipient's public key (`KeyPair::encrypt_asymmetric`) and decrypting data encrypted with the keypair's public key using the private key (`KeyPair::decrypt_asymmetric`).
The `session_manager` module provides functions that utilize the currently selected keypair to perform these operations within the context of the active session.

View File

@ -1,5 +1,4 @@
use crate::vault::keyspace::keypair_types::{KeyPair, KeySpace};
use crate::vault::keypair::keypair_types::{KeyPair, KeySpace};
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -20,12 +19,16 @@ mod tests {
let signature = keypair.sign(message); let signature = keypair.sign(message);
assert!(!signature.is_empty()); assert!(!signature.is_empty());
let is_valid = keypair.verify(message, &signature).expect("Verification failed"); let is_valid = keypair
.verify(message, &signature)
.expect("Verification failed");
assert!(is_valid); assert!(is_valid);
// Test with a wrong message // Test with a wrong message
let wrong_message = b"This is a different message"; let wrong_message = b"This is a different message";
let is_valid_wrong = keypair.verify(wrong_message, &signature).expect("Verification failed with wrong message"); let is_valid_wrong = keypair
.verify(wrong_message, &signature)
.expect("Verification failed with wrong message");
assert!(!is_valid_wrong); assert!(!is_valid_wrong);
} }
@ -36,13 +39,16 @@ mod tests {
let signature = keypair.sign(message); let signature = keypair.sign(message);
let public_key = keypair.pub_key(); let public_key = keypair.pub_key();
let is_valid = KeyPair::verify_with_public_key(&public_key, message, &signature).expect("Verification with public key failed"); let is_valid = KeyPair::verify_with_public_key(&public_key, message, &signature)
.expect("Verification with public key failed");
assert!(is_valid); assert!(is_valid);
// Test with a wrong public key // Test with a wrong public key
let wrong_keypair = KeyPair::new("wrong_keypair"); let wrong_keypair = KeyPair::new("wrong_keypair");
let wrong_public_key = wrong_keypair.pub_key(); let wrong_public_key = wrong_keypair.pub_key();
let is_valid_wrong_key = KeyPair::verify_with_public_key(&wrong_public_key, message, &signature).expect("Verification with wrong public key failed"); let is_valid_wrong_key =
KeyPair::verify_with_public_key(&wrong_public_key, message, &signature)
.expect("Verification with wrong public key failed");
assert!(!is_valid_wrong_key); assert!(!is_valid_wrong_key);
} }
@ -50,7 +56,7 @@ mod tests {
fn test_asymmetric_encryption_decryption() { fn test_asymmetric_encryption_decryption() {
// Sender's keypair // Sender's keypair
let sender_keypair = KeyPair::new("sender"); let sender_keypair = KeyPair::new("sender");
let sender_public_key = sender_keypair.pub_key(); let _ = sender_keypair.pub_key();
// Recipient's keypair // Recipient's keypair
let recipient_keypair = KeyPair::new("recipient"); let recipient_keypair = KeyPair::new("recipient");
@ -59,11 +65,15 @@ mod tests {
let message = b"This is a secret message"; let message = b"This is a secret message";
// Sender encrypts for recipient // Sender encrypts for recipient
let ciphertext = sender_keypair.encrypt_asymmetric(&recipient_public_key, message).expect("Encryption failed"); let ciphertext = sender_keypair
.encrypt_asymmetric(&recipient_public_key, message)
.expect("Encryption failed");
assert!(!ciphertext.is_empty()); assert!(!ciphertext.is_empty());
// Recipient decrypts // Recipient decrypts
let decrypted_message = recipient_keypair.decrypt_asymmetric(&ciphertext).expect("Decryption failed"); let decrypted_message = recipient_keypair
.decrypt_asymmetric(&ciphertext)
.expect("Decryption failed");
assert_eq!(decrypted_message, message); assert_eq!(decrypted_message, message);
// Test decryption with wrong keypair // Test decryption with wrong keypair
@ -75,7 +85,9 @@ mod tests {
#[test] #[test]
fn test_keyspace_add_keypair() { fn test_keyspace_add_keypair() {
let mut space = KeySpace::new("test_space"); let mut space = KeySpace::new("test_space");
space.add_keypair("keypair1").expect("Failed to add keypair1"); space
.add_keypair("keypair1")
.expect("Failed to add keypair1");
assert_eq!(space.keypairs.len(), 1); assert_eq!(space.keypairs.len(), 1);
assert!(space.keypairs.contains_key("keypair1")); assert!(space.keypairs.contains_key("keypair1"));

View File

@ -0,0 +1,3 @@
mod keypair_types_tests;
mod session_manager_tests;

View File

@ -1,8 +1,8 @@
use crate::vault::keypair::session_manager::{ use crate::vault::keyspace::keypair_types::KeySpace;
use crate::vault::keyspace::session_manager::{
clear_session, create_keypair, create_space, get_current_space, get_selected_keypair, clear_session, create_keypair, create_space, get_current_space, get_selected_keypair,
list_keypairs, select_keypair, set_current_space, SESSION, list_keypairs, select_keypair, set_current_space,
}; };
use crate::vault::keypair::keypair_types::KeySpace;
// Helper function to clear the session before each test // Helper function to clear the session before each test
fn setup_test() { fn setup_test() {
@ -48,7 +48,8 @@ mod tests {
assert_eq!(keypair.name, "test_keypair"); assert_eq!(keypair.name, "test_keypair");
select_keypair("test_keypair").expect("Failed to select keypair"); select_keypair("test_keypair").expect("Failed to select keypair");
let selected_keypair = get_selected_keypair().expect("Failed to get selected keypair after select"); let selected_keypair =
get_selected_keypair().expect("Failed to get selected keypair after select");
assert_eq!(selected_keypair.name, "test_keypair"); assert_eq!(selected_keypair.name, "test_keypair");
} }

View File

@ -143,7 +143,7 @@ The module uses the `CryptoError` type for handling errors that can occur during
## Examples ## Examples
For examples of how to use the KVS module, see the `examples/vault` directory. While there may not be specific examples for the KVS module, the general pattern of usage is similar to the key space management examples. For examples of how to use the KVS module, see the `examples/hero_vault` directory. While there may not be specific examples for the KVS module, the general pattern of usage is similar to the key space management examples.
A basic usage example: A basic usage example:

View File

@ -1,700 +0,0 @@
//! IndexedDB-backed key-value store implementation for WebAssembly.
use crate::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
}

View File

@ -1,169 +0,0 @@
//! Key space management functionality for KVStore
use crate::vault::kvs::{KVStore, Result};
use crate::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))
}

View File

@ -1,65 +1,17 @@
//! Key-Value Store functionality //! Key-Value Store functionality
//! //!
//! This module provides a simple key-value store with encryption support. //! 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; pub mod error;
mod store; pub mod store;
mod key_space;
mod slate_store;
mod indexed_db_store;
// Re-export public types and functions // Re-export public types and functions
pub use error::{KvsError, Result}; pub use error::KvsError;
pub use store::{KvPair, KVStore}; pub use store::{
KvStore, KvPair,
// Re-export the SlateDbStore for native platforms create_store, open_store, delete_store,
pub use slate_store::{ list_stores, get_store_path
SlateDbStore, create_slatedb_store, open_slatedb_store,
delete_slatedb_store, list_slatedb_stores
}; };
// Re-export the IndexedDbStore for WebAssembly #[cfg(test)]
#[cfg(target_arch = "wasm32")] mod tests;
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)
}

View File

@ -1,307 +0,0 @@
//! SlateDB-backed key-value store implementation.
use crate::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
}
}

View File

@ -17,46 +17,6 @@ pub struct KvPair {
pub value: String, 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. /// A simple key-value store.
/// ///
/// This implementation uses the filesystem to store key-value pairs. /// This implementation uses the filesystem to store key-value pairs.
@ -264,12 +224,18 @@ impl KvStore {
Ok(()) Ok(())
} }
}
// Implement the KVStore trait for the standard KvStore
impl KVStore for KvStore {
/// Stores a value with the given key. /// Stores a value with the given key.
fn set<K, V>(&self, key: K, value: &V) -> Result<()> ///
/// # 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<()>
where where
K: ToString, K: ToString,
V: Serialize, V: Serialize,
@ -290,7 +256,15 @@ impl KVStore for KvStore {
} }
/// Retrieves a value for the given key. /// Retrieves a value for the given key.
fn get<K, V>(&self, key: K) -> Result<V> ///
/// # 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>
where where
K: ToString, K: ToString,
V: DeserializeOwned, V: DeserializeOwned,
@ -308,7 +282,15 @@ impl KVStore for KvStore {
} }
/// Deletes a value for the given key. /// Deletes a value for the given key.
fn delete<K>(&self, key: K) -> Result<()> ///
/// # Arguments
///
/// * `key` - The key to delete
///
/// # Returns
///
/// `Ok(())` if the operation was successful
pub fn delete<K>(&self, key: K) -> Result<()>
where where
K: ToString, K: ToString,
{ {
@ -329,7 +311,15 @@ impl KVStore for KvStore {
} }
/// Checks if a key exists in the store. /// Checks if a key exists in the store.
fn contains<K>(&self, key: K) -> Result<bool> ///
/// # Arguments
///
/// * `key` - The key to check
///
/// # Returns
///
/// `true` if the key exists, `false` otherwise
pub fn contains<K>(&self, key: K) -> Result<bool>
where where
K: ToString, K: ToString,
{ {
@ -340,14 +330,22 @@ impl KVStore for KvStore {
} }
/// Lists all keys in the store. /// Lists all keys in the store.
fn keys(&self) -> Result<Vec<String>> { ///
/// # Returns
///
/// A vector of keys as strings
pub fn keys(&self) -> Result<Vec<String>> {
let data = self.data.lock().unwrap(); let data = self.data.lock().unwrap();
Ok(data.keys().cloned().collect()) Ok(data.keys().cloned().collect())
} }
/// Clears all key-value pairs from the store. /// Clears all key-value pairs from the store.
fn clear(&self) -> Result<()> { ///
/// # Returns
///
/// `Ok(())` if the operation was successful
pub fn clear(&self) -> Result<()> {
// Update in-memory data // Update in-memory data
{ {
let mut data = self.data.lock().unwrap(); let mut data = self.data.lock().unwrap();
@ -361,12 +359,12 @@ impl KVStore for KvStore {
} }
/// Gets the name of the store. /// Gets the name of the store.
fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
/// Gets whether the store is encrypted. /// Gets whether the store is encrypted.
fn is_encrypted(&self) -> bool { pub fn is_encrypted(&self) -> bool {
self.encrypted self.encrypted
} }
} }

View File

@ -1,5 +1,4 @@
use crate::vault::kvs::store::{create_store, delete_store, open_store, KvStore}; use crate::vault::kvs::store::{create_store, delete_store, open_store};
use std::path::PathBuf;
// Helper function to generate a unique store name for each test // Helper function to generate a unique store name for each test
fn generate_test_store_name() -> String { fn generate_test_store_name() -> String {

View File

@ -9,7 +9,7 @@
//! - Key-value store with encryption //! - Key-value store with encryption
pub mod error; pub mod error;
pub mod keypair; pub mod keyspace;
pub mod symmetric; pub mod symmetric;
pub mod ethereum; pub mod ethereum;
pub mod kvs; pub mod kvs;
@ -17,4 +17,4 @@ pub mod kvs;
// Re-export modules // Re-export modules
// Re-export common types for convenience // Re-export common types for convenience
pub use error::CryptoError; pub use error::CryptoError;
pub use keypair::{KeyPair, KeySpace}; pub use keyspace::{KeyPair, KeySpace};

View File

@ -92,7 +92,7 @@ The module uses the `CryptoError` type for handling errors that can occur during
## Examples ## Examples
For examples of how to use the Symmetric Encryption module, see the `examples/vault` directory, particularly: For examples of how to use the Symmetric Encryption module, see the `examples/hero_vault` directory, particularly:
- `example.rhai` - Basic example demonstrating symmetric encryption - `example.rhai` - Basic example demonstrating symmetric encryption
- `advanced_example.rhai` - Advanced example with error handling - `advanced_example.rhai` - Advanced example with error handling

View File

@ -7,7 +7,7 @@ use serde::{Serialize, Deserialize};
use sha2::{Sha256, Digest}; use sha2::{Sha256, Digest};
use crate::vault::error::CryptoError; use crate::vault::error::CryptoError;
use crate::vault::keypair::KeySpace; use crate::vault::keyspace::KeySpace;
/// The size of the nonce in bytes. /// The size of the nonce in bytes.
const NONCE_SIZE: usize = 12; const NONCE_SIZE: usize = 12;