Compare commits
6 Commits
developmen
...
main
Author | SHA1 | Date | |
---|---|---|---|
0c425470a5 | |||
|
7add64562e | ||
|
809599d60c | ||
|
25f2ae6fa9 | ||
a4438d63e0 | |||
393c4270d4 |
25
Cargo.toml
25
Cargo.toml
@ -12,16 +12,16 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.98"
|
||||
base64 = "0.21.0" # Base64 encoding/decoding
|
||||
base64 = "0.22.1" # Base64 encoding/decoding
|
||||
cfg-if = "1.0"
|
||||
chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher
|
||||
clap = "2.33" # Command-line argument parsing
|
||||
dirs = "5.0.1" # Directory paths
|
||||
env_logger = "0.10.0" # Logger implementation
|
||||
clap = "2.34.0" # Command-line argument parsing
|
||||
dirs = "6.0.0" # Directory paths
|
||||
env_logger = "0.11.8" # Logger implementation
|
||||
ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library
|
||||
glob = "0.3.1" # For file pattern matching
|
||||
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
|
||||
libc = "0.2"
|
||||
log = "0.4" # Logging facade
|
||||
@ -31,7 +31,7 @@ postgres-types = "0.2.5" # PostgreSQL type conversions
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_postgres = "0.18.2"
|
||||
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
|
||||
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
|
||||
serde = { version = "1.0", features = [
|
||||
@ -40,23 +40,26 @@ serde = { version = "1.0", features = [
|
||||
serde_json = "1.0" # For JSON handling
|
||||
sha2 = "0.10.7" # SHA-2 hash functions
|
||||
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"
|
||||
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
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = "0.26" # Unix-specific functionality
|
||||
nix = "0.30.1" # Unix-specific functionality
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.48", features = [
|
||||
windows = { version = "0.61.1", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Threading",
|
||||
"Win32_Storage_FileSystem",
|
||||
] }
|
||||
|
||||
[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
|
||||
tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing
|
||||
|
||||
|
@ -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!");
|
@ -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
|
||||
|
||||
// Define the private key and recipient address
|
||||
let private_key = "51c194d20bcd25360a3aa94426b3b60f738007e42f22e1bc97821c65c353e6d2";
|
||||
let private_key = "0x9ecfd58eca522b0e7c109bf945966ee208cd6d593b1dc3378aedfdc60b64f512";
|
||||
let recipient_address = "0xf400f9c3F7317e19523a5DB698Ce67e7a7E083e2";
|
||||
|
||||
print("=== Agung Wallet Transaction Demo ===");
|
||||
@ -33,32 +33,32 @@ if !select_keypair("demo_keypair") {
|
||||
|
||||
print("\nCreated and selected keypair successfully");
|
||||
|
||||
// Clear any existing Ethereum wallets to avoid conflicts
|
||||
if clear_ethereum_wallets() {
|
||||
print("Cleared existing Ethereum wallets");
|
||||
// Clear any existing Agung wallets to avoid conflicts
|
||||
if clear_wallets_for_network("agung") {
|
||||
print("Cleared existing Agung wallets");
|
||||
} else {
|
||||
print("Failed to clear existing Ethereum wallets");
|
||||
print("Failed to clear existing Agung wallets");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a wallet from the private key directly
|
||||
print("\n=== Creating Wallet from Private Key ===");
|
||||
|
||||
// Create a wallet from the private key (works for any network)
|
||||
if create_ethereum_wallet_from_private_key(private_key) {
|
||||
print("Successfully created wallet from private key");
|
||||
// Create a wallet from the private key for the Agung network
|
||||
if create_wallet_from_private_key_for_network(private_key, "agung") {
|
||||
print("Successfully created wallet from private key for Agung network");
|
||||
|
||||
// Get the wallet address
|
||||
let wallet_address = get_ethereum_address();
|
||||
let wallet_address = get_wallet_address_for_network("agung");
|
||||
print(`Wallet address: ${wallet_address}`);
|
||||
|
||||
// Create a provider for the Agung network
|
||||
let provider_id = create_provider("agung");
|
||||
let provider_id = create_agung_provider();
|
||||
if provider_id != "" {
|
||||
print("Successfully created Agung provider");
|
||||
|
||||
// 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);
|
||||
|
||||
if balance_wei == "" {
|
||||
@ -67,18 +67,16 @@ if create_ethereum_wallet_from_private_key(private_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
print(`Current wallet balance: ${balance_wei}`);
|
||||
print(`Current wallet balance: ${balance_wei} wei`);
|
||||
|
||||
// Convert 1 AGNG to wei (1 AGNG = 10^18 wei)
|
||||
// Use string representation for large numbers
|
||||
let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string
|
||||
|
||||
// For this example, we'll assume we have enough balance
|
||||
// NOTE: In a real application, you would need to check the balance properly
|
||||
// by parsing it and comparing with the amount.
|
||||
if false { // Disabled check since balance format has changed
|
||||
// Check if we have enough balance
|
||||
if parse_int(balance_wei) < parse_int(amount_wei_str) {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
@ -206,7 +206,7 @@ impl RedisClientWrapper {
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
@ -1,128 +1,252 @@
|
||||
//! Rhai bindings for SAL crypto functionality
|
||||
|
||||
use rhai::{Engine, Dynamic, EvalAltResult};
|
||||
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 base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
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 cfg_if::cfg_if;
|
||||
use std::sync::Mutex;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::vault::{keypair, symmetric, ethereum, kvs};
|
||||
use crate::vault::kvs::{KVStore, DefaultStore};
|
||||
use crate::vault::ethereum::prepare_function_arguments;
|
||||
use crate::vault::ethereum;
|
||||
use crate::vault::keyspace::session_manager as keypair;
|
||||
|
||||
use crate::vault::symmetric::implementation as symmetric_impl;
|
||||
// Global Tokio runtime for blocking async operations
|
||||
static RUNTIME: Lazy<Mutex<Runtime>> = Lazy::new(|| {
|
||||
Mutex::new(Runtime::new().expect("Failed to create Tokio runtime"))
|
||||
});
|
||||
static RUNTIME: Lazy<Mutex<Runtime>> =
|
||||
Lazy::new(|| Mutex::new(Runtime::new().expect("Failed to create Tokio runtime")));
|
||||
|
||||
// Helper function to run async operations and handle errors consistently
|
||||
fn run_async<F, T, E>(future: F) -> Result<T, String>
|
||||
where
|
||||
F: std::future::Future<Output = Result<T, E>>,
|
||||
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()) })
|
||||
}
|
||||
// Global provider registry
|
||||
static PROVIDERS: Lazy<
|
||||
Mutex<HashMap<String, ethers::providers::Provider<ethers::providers::Http>>>,
|
||||
> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
// 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()
|
||||
// Key space management functions
|
||||
fn load_key_space(name: &str, password: &str) -> bool {
|
||||
// Get the key spaces directory from config
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces");
|
||||
|
||||
// Check if directory exists
|
||||
if !key_spaces_dir.exists() {
|
||||
log::error!("Key spaces directory does not exist");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the key space file path
|
||||
let space_path = key_spaces_dir.join(format!("{}.json", name));
|
||||
|
||||
// Check if file exists
|
||||
if !space_path.exists() {
|
||||
log::error!("Key space file not found: {}", space_path.display());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the file
|
||||
let serialized = match fs::read_to_string(&space_path) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
log::error!("Error reading key space file: {}", e);
|
||||
return false;
|
||||
}
|
||||
} 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()
|
||||
};
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Key space management functions
|
||||
fn load_key_space(name: &str, password: &str) -> bool {
|
||||
let store = get_key_store();
|
||||
kvs::load_key_space(&store, name, password)
|
||||
}
|
||||
|
||||
fn create_key_space(name: &str, password: &str) -> bool {
|
||||
let store = get_key_store();
|
||||
kvs::create_key_space(&store, name, password)
|
||||
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_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
|
||||
fn auto_save_key_space(password: &str) -> bool {
|
||||
let store = get_key_store();
|
||||
kvs::save_key_space(&store, password)
|
||||
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", 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export the current key space to a JSON string
|
||||
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,
|
||||
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 encrypting key space: {}", e);
|
||||
log::error!("Error getting current space: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import a key space from a JSON string
|
||||
fn decrypt_key_space(encrypted: &str, password: &str) -> bool {
|
||||
match symmetric::deserialize_encrypted_space(encrypted)
|
||||
.and_then(|encrypted_space| symmetric::decrypt_key_space(&encrypted_space, password))
|
||||
.and_then(|space| keypair::set_current_space(space))
|
||||
{
|
||||
Ok(_) => true,
|
||||
match serde_json::from_str(encrypted) {
|
||||
Ok(encrypted_space) => {
|
||||
match symmetric_impl::decrypt_key_space(&encrypted_space, password) {
|
||||
Ok(space) => match keypair::set_current_space(space) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::error!("Error setting current space: {}", e);
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error decrypting key space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error decrypting key space: {}", e);
|
||||
log::error!("Error parsing encrypted space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -134,7 +258,7 @@ fn create_keypair(name: &str, password: &str) -> bool {
|
||||
Ok(_) => {
|
||||
// Auto-save the key space after creating a keypair
|
||||
auto_save_key_space(password)
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error creating keypair: {}", e);
|
||||
false
|
||||
@ -176,14 +300,16 @@ fn sign(message: &str) -> String {
|
||||
|
||||
fn verify(message: &str, signature: &str) -> bool {
|
||||
let message_bytes = message.as_bytes();
|
||||
match BASE64.decode(signature)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|sig_bytes| keypair::keypair_verify(message_bytes, &sig_bytes)
|
||||
.map_err(|e| e.to_string()))
|
||||
{
|
||||
Ok(is_valid) => is_valid,
|
||||
match BASE64.decode(signature) {
|
||||
Ok(signature_bytes) => match keypair::keypair_verify(message_bytes, &signature_bytes) {
|
||||
Ok(is_valid) => is_valid,
|
||||
Err(e) => {
|
||||
log::error!("Error verifying signature: {}", e);
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error verifying signature: {}", e);
|
||||
log::error!("Error decoding signature: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -191,44 +317,54 @@ fn verify(message: &str, signature: &str) -> bool {
|
||||
|
||||
// Symmetric encryption
|
||||
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 {
|
||||
match BASE64.decode(key)
|
||||
.map_err(|e| format!("Error decoding key: {}", e))
|
||||
.and_then(|key_bytes| {
|
||||
symmetric::encrypt_symmetric(&key_bytes, message.as_bytes())
|
||||
.map_err(|e| format!("Error encrypting message: {}", e))
|
||||
})
|
||||
{
|
||||
Ok(ciphertext) => BASE64.encode(ciphertext),
|
||||
match BASE64.decode(key) {
|
||||
Ok(key_bytes) => {
|
||||
let message_bytes = message.as_bytes();
|
||||
match symmetric_impl::encrypt_symmetric(&key_bytes, message_bytes) {
|
||||
Ok(ciphertext) => BASE64.encode(ciphertext),
|
||||
Err(e) => {
|
||||
log::error!("Error encrypting message: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
log::error!("Error decoding key: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt(key: &str, ciphertext: &str) -> String {
|
||||
match BASE64.decode(key)
|
||||
.map_err(|e| format!("Error decoding key: {}", e))
|
||||
.and_then(|key_bytes| {
|
||||
BASE64.decode(ciphertext)
|
||||
.map_err(|e| format!("Error decoding ciphertext: {}", e))
|
||||
.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,
|
||||
match BASE64.decode(key) {
|
||||
Ok(key_bytes) => match BASE64.decode(ciphertext) {
|
||||
Ok(ciphertext_bytes) => {
|
||||
match symmetric_impl::decrypt_symmetric(&key_bytes, &ciphertext_bytes) {
|
||||
Ok(plaintext) => match String::from_utf8(plaintext) {
|
||||
Ok(text) => text,
|
||||
Err(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!("{}", e);
|
||||
log::error!("Error decoding key: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
@ -236,9 +372,9 @@ fn decrypt(key: &str, ciphertext: &str) -> String {
|
||||
|
||||
// Ethereum operations
|
||||
|
||||
// Create a network-independent Ethereum wallet
|
||||
// Gnosis Chain operations
|
||||
fn create_ethereum_wallet() -> bool {
|
||||
match ethereum::create_ethereum_wallet() {
|
||||
match ethereum::create_ethereum_wallet_for_network(ethereum::networks::gnosis()) {
|
||||
Ok(_) => true,
|
||||
Err(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 {
|
||||
match ethereum::get_current_ethereum_wallet() {
|
||||
match ethereum::get_current_ethereum_wallet_for_network("Gnosis") {
|
||||
Ok(wallet) => wallet.address_string(),
|
||||
Err(e) => {
|
||||
log::error!("Error getting Ethereum address: {}", e);
|
||||
@ -258,76 +393,116 @@ fn get_ethereum_address() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Create a wallet from a name
|
||||
fn create_ethereum_wallet_from_name(name: &str) -> bool {
|
||||
match ethereum::create_ethereum_wallet_from_name(name) {
|
||||
// Peaq network operations
|
||||
fn create_peaq_wallet() -> bool {
|
||||
match ethereum::create_peaq_wallet() {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::error!("Error creating Ethereum wallet from name: {}", e);
|
||||
log::error!("Error creating Peaq wallet: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a wallet from a private key
|
||||
fn create_ethereum_wallet_from_private_key(private_key: &str) -> bool {
|
||||
match ethereum::create_ethereum_wallet_from_private_key(private_key) {
|
||||
Ok(_) => true,
|
||||
fn get_peaq_address() -> String {
|
||||
match ethereum::get_current_peaq_wallet() {
|
||||
Ok(wallet) => wallet.address_string(),
|
||||
Err(e) => {
|
||||
log::error!("Error creating Ethereum wallet from private key: {}", 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);
|
||||
log::error!("Error getting Peaq address: {}", e);
|
||||
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
|
||||
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,
|
||||
None => {
|
||||
log::error!("Unknown network: {}", network_name);
|
||||
@ -338,7 +513,7 @@ fn get_network_token_symbol(network_name: &str) -> String {
|
||||
|
||||
// Get network explorer URL
|
||||
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,
|
||||
None => {
|
||||
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
|
||||
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
|
||||
let addr = match Address::from_str(address) {
|
||||
Ok(addr) => addr,
|
||||
@ -358,15 +588,36 @@ fn get_balance(network_name: &str, address: &str) -> String {
|
||||
}
|
||||
};
|
||||
|
||||
// Get the proper network name
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
// 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 run_async(ethereum::get_balance(network_name, addr)) {
|
||||
Ok(balance) => {
|
||||
// Format the balance with the network's token symbol
|
||||
match ethereum::get_network_by_name(network_name) {
|
||||
Some(network) => ethereum::format_balance(balance, &network),
|
||||
None => balance.to_string()
|
||||
}
|
||||
},
|
||||
match rt.block_on(async { ethereum::get_balance(&provider, addr).await }) {
|
||||
Ok(balance) => balance.to_string(),
|
||||
Err(e) => {
|
||||
log::error!("Failed to get balance: {}", e);
|
||||
String::new()
|
||||
@ -374,8 +625,17 @@ fn get_balance(network_name: &str, address: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Send ETH from one address to another
|
||||
fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String {
|
||||
// Send ETH from one address to another using the blocking approach
|
||||
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
|
||||
let to_addr = match Address::from_str(to_address) {
|
||||
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
|
||||
let wallet = match ethereum::get_current_ethereum_wallet() {
|
||||
let wallet = match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) {
|
||||
Ok(w) => w,
|
||||
Err(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
|
||||
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),
|
||||
Err(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
|
||||
fn load_contract_abi(network_name: &str, address: &str, abi_json: &str) -> String {
|
||||
// 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,
|
||||
None => {
|
||||
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()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error creating contract: {}", e);
|
||||
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)
|
||||
fn call_contract_read_no_args(contract_json: &str, function_name: &str) -> Dynamic {
|
||||
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
|
||||
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,
|
||||
Err(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
|
||||
let provider = match ethereum::create_provider(&contract.network.name) {
|
||||
let provider = match ethereum::create_provider(&contract.network) {
|
||||
Ok(p) => p,
|
||||
Err(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
|
||||
match run_async(ethereum::call_read_function(&contract, &provider, function_name, tokens)) {
|
||||
Ok(result) => ethereum::token_to_dynamic(&result),
|
||||
match rt.block_on(async {
|
||||
ethereum::call_read_function(&contract, &provider, function_name, tokens).await
|
||||
}) {
|
||||
Ok(result) => ethereum::convert_token_to_rhai(&result),
|
||||
Err(e) => {
|
||||
log::error!("Failed to call contract function: {}", e);
|
||||
Dynamic::UNIT
|
||||
@ -527,7 +818,7 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
|
||||
};
|
||||
|
||||
// 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,
|
||||
Err(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
|
||||
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,
|
||||
Err(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
|
||||
let provider = match ethereum::create_provider(&contract.network.name) {
|
||||
let provider = match ethereum::create_provider(&contract.network) {
|
||||
Ok(p) => p,
|
||||
Err(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
|
||||
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),
|
||||
Err(e) => {
|
||||
// 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("decrypt", decrypt);
|
||||
|
||||
// Register Ethereum wallet functions
|
||||
// Register Ethereum functions (Gnosis Chain)
|
||||
engine.register_fn("create_ethereum_wallet", create_ethereum_wallet);
|
||||
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
|
||||
engine.register_fn("register_network", register_network);
|
||||
engine.register_fn("remove_network", remove_network);
|
||||
// Register Peaq network functions
|
||||
engine.register_fn("create_peaq_wallet", create_peaq_wallet);
|
||||
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("get_network_token_symbol", get_network_token_symbol);
|
||||
engine.register_fn("get_network_explorer_url", get_network_explorer_url);
|
||||
|
||||
// Register provider functions
|
||||
engine.register_fn("create_provider", create_provider);
|
||||
|
||||
// Register transaction functions
|
||||
// Register new Ethereum functions for wallet creation from private key and transactions
|
||||
engine.register_fn(
|
||||
"create_wallet_from_private_key_for_network",
|
||||
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("get_balance", get_balance);
|
||||
|
||||
// Register smart contract functions
|
||||
engine.register_fn("load_contract_abi", load_contract_abi);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
|
@ -157,4 +157,4 @@ The module supports multiple Ethereum networks, including:
|
||||
|
||||
## 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.
|
||||
|
@ -48,10 +48,6 @@ pub enum CryptoError {
|
||||
/// Smart contract error
|
||||
#[error("Smart contract error: {0}")]
|
||||
ContractError(String),
|
||||
|
||||
/// Storage error
|
||||
#[error("Storage error: {0}")]
|
||||
StorageError(String),
|
||||
}
|
||||
|
||||
/// Convert CryptoError to SAL's Error type
|
||||
|
@ -21,25 +21,25 @@ The Ethereum module is organized into several components:
|
||||
The module provides functionality for creating and managing Ethereum wallets:
|
||||
|
||||
```rust
|
||||
// Create a network-independent Ethereum wallet
|
||||
let wallet = create_ethereum_wallet()?;
|
||||
// Create a new Ethereum wallet for a specific network
|
||||
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
|
||||
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
|
||||
let imported_wallet = create_ethereum_wallet_from_private_key("0x...")?;
|
||||
|
||||
// Get the current wallet
|
||||
let current_wallet = get_current_ethereum_wallet()?;
|
||||
// Get the current wallet for a network
|
||||
let current_wallet = get_current_ethereum_wallet_for_network("Ethereum")?;
|
||||
|
||||
// Clear wallets
|
||||
clear_ethereum_wallets()?;
|
||||
|
||||
// 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()?;
|
||||
clear_ethereum_wallets_for_network("Gnosis")?;
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```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
|
||||
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
|
||||
let provider = create_provider("Ethereum")?;
|
||||
|
||||
// Create a provider from a network configuration
|
||||
let provider = create_provider_from_config(&network)?;
|
||||
|
||||
// Legacy functions for backward compatibility
|
||||
// Create providers for specific networks
|
||||
let gnosis_provider = create_gnosis_provider()?;
|
||||
let peaq_provider = create_peaq_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:
|
||||
|
||||
```rust
|
||||
// Get the balance of an address on a specific network
|
||||
let balance = get_balance("Ethereum", address).await?;
|
||||
// Get the balance of an address
|
||||
let balance = get_balance("Ethereum", "0x...")?;
|
||||
|
||||
// Get the balance using a provider
|
||||
let balance = get_balance_with_provider(&provider, address).await?;
|
||||
|
||||
// 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?;
|
||||
// Send ETH to an address
|
||||
let tx_hash = send_eth("Ethereum", "0x...", "1000000000000000")?;
|
||||
|
||||
// Format a balance for display
|
||||
let formatted = format_balance(balance, &network);
|
||||
let formatted = format_balance(balance, 18)?; // Convert wei to ETH
|
||||
```
|
||||
|
||||
### Smart Contract Interactions
|
||||
@ -113,16 +98,16 @@ The module provides functionality for interacting with smart contracts:
|
||||
let abi = load_abi_from_json(json_string)?;
|
||||
|
||||
// Create a contract instance
|
||||
let contract = Contract::new(address, abi, network);
|
||||
let contract = Contract::new(provider, "0x...", abi)?;
|
||||
|
||||
// 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
|
||||
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
|
||||
let gas = estimate_gas(&contract, &provider, "transfer", tokens).await?;
|
||||
let gas = estimate_gas(contract, "transfer", vec!["0x...", "1000"])?;
|
||||
```
|
||||
|
||||
### 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)?;
|
||||
|
||||
// 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
|
||||
let rhai_value = convert_token_to_rhai(&token)?;
|
||||
let rhai_value = convert_token_to_rhai(token)?;
|
||||
|
||||
// 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
|
||||
// Built-in networks
|
||||
- Gnosis Chain (chain_id: 100, token: xDAI)
|
||||
- Peaq Network (chain_id: 3338, token: PEAQ)
|
||||
- Agung Network (chain_id: 9990, token: AGNG)
|
||||
- Gnosis Chain
|
||||
- Peaq Network
|
||||
- Agung Network
|
||||
|
||||
// Register a custom network at runtime
|
||||
register_network(
|
||||
"Polygon",
|
||||
137,
|
||||
"https://polygon-rpc.com",
|
||||
"https://polygonscan.com",
|
||||
"MATIC",
|
||||
18
|
||||
);
|
||||
```
|
||||
Each network has its own configuration, including:
|
||||
|
||||
## Wallet Address Consistency
|
||||
|
||||
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.
|
||||
|
||||
```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?;
|
||||
```
|
||||
- RPC URL
|
||||
- Chain ID
|
||||
- Explorer URL
|
||||
- Native currency symbol and decimals
|
||||
|
||||
## Error Handling
|
||||
|
||||
@ -184,42 +149,12 @@ The module uses the `CryptoError` type for handling errors that can occur during
|
||||
|
||||
- `InvalidAddress` - Invalid Ethereum address format
|
||||
- `ContractError` - Smart contract interaction error
|
||||
- `NoKeypairSelected` - No keypair selected for wallet creation
|
||||
- `InvalidKeyLength` - Invalid private key length
|
||||
|
||||
## 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
|
||||
- `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_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!
|
||||
|
@ -42,7 +42,7 @@ impl Contract {
|
||||
}
|
||||
|
||||
/// 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(
|
||||
self.address,
|
||||
self.abi.clone(),
|
||||
@ -65,9 +65,9 @@ pub async fn call_read_function(
|
||||
provider: &Provider<Http>,
|
||||
function_name: &str,
|
||||
args: Vec<Token>,
|
||||
) -> Result<Token, CryptoError> {
|
||||
) -> Result<Vec<Token>, CryptoError> {
|
||||
// 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
|
||||
let function = contract.abi.function(function_name)
|
||||
@ -89,12 +89,7 @@ pub async fn call_read_function(
|
||||
let decoded = function.decode_output(&result)
|
||||
.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
|
||||
if decoded.len() == 1 {
|
||||
Ok(decoded[0].clone())
|
||||
} else {
|
||||
Ok(Token::Tuple(decoded))
|
||||
}
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
/// Executes a state-changing function on a contract.
|
||||
@ -105,11 +100,10 @@ pub async fn call_write_function(
|
||||
function_name: &str,
|
||||
args: Vec<Token>,
|
||||
) -> Result<H256, CryptoError> {
|
||||
// Create a client with the wallet configured for this network
|
||||
let network_wallet = wallet.for_network(&contract.network);
|
||||
// Create a client with the wallet
|
||||
let client = SignerMiddleware::new(
|
||||
provider.clone(),
|
||||
network_wallet,
|
||||
wallet.wallet.clone(),
|
||||
);
|
||||
|
||||
// Get the function from the ABI
|
||||
@ -120,28 +114,23 @@ pub async fn call_write_function(
|
||||
let call_data = function.encode_input(&args)
|
||||
.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
|
||||
let tx = TransactionRequest::new()
|
||||
.to(contract.address)
|
||||
.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
|
||||
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::debug!("Sending transaction to contract at {}", contract.address);
|
||||
log::debug!("Function: {}, Args: {:?}", function_name, args);
|
||||
log::debug!("From address: {}", wallet.address);
|
||||
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) => {
|
||||
log::debug!("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
|
||||
.map_err(|e| CryptoError::ContractError(format!("Failed to estimate gas: {}", e)))?;
|
||||
|
||||
// Add a buffer to the gas estimate to account for potential variations
|
||||
let gas_with_buffer = gas * 12 / 10; // Add 20% buffer
|
||||
|
||||
Ok(gas_with_buffer)
|
||||
Ok(gas)
|
||||
}
|
||||
|
@ -24,33 +24,27 @@ pub use networks::NetworkConfig;
|
||||
|
||||
// Re-export wallet creation functions
|
||||
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_peaq_wallet,
|
||||
create_agung_wallet,
|
||||
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,
|
||||
};
|
||||
|
||||
// Re-export wallet management functions
|
||||
pub use storage::{
|
||||
get_current_ethereum_wallet,
|
||||
clear_ethereum_wallets,
|
||||
// Legacy functions for backward compatibility
|
||||
get_current_ethereum_wallet_for_network,
|
||||
get_current_peaq_wallet,
|
||||
get_current_agung_wallet,
|
||||
clear_ethereum_wallets,
|
||||
clear_ethereum_wallets_for_network,
|
||||
};
|
||||
|
||||
// Re-export provider functions
|
||||
pub use provider::{
|
||||
create_provider,
|
||||
create_provider_from_config,
|
||||
// Legacy functions for backward compatibility
|
||||
create_gnosis_provider,
|
||||
create_peaq_provider,
|
||||
create_agung_provider,
|
||||
@ -59,16 +53,12 @@ pub use provider::{
|
||||
// Re-export transaction functions
|
||||
pub use transaction::{
|
||||
get_balance,
|
||||
get_balance_with_provider,
|
||||
send_eth,
|
||||
send_eth_with_provider,
|
||||
format_balance,
|
||||
};
|
||||
|
||||
// Re-export network registry functions
|
||||
pub use networks::{
|
||||
register_network,
|
||||
remove_network,
|
||||
get_network_by_name,
|
||||
get_proper_network_name,
|
||||
list_network_names,
|
||||
|
@ -4,8 +4,7 @@
|
||||
//! to work with them.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::OnceLock;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// Configuration for an EVM-compatible network
|
||||
@ -19,16 +18,6 @@ pub struct NetworkConfig {
|
||||
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
|
||||
pub mod names {
|
||||
pub const GNOSIS: &str = "Gnosis";
|
||||
@ -36,80 +25,50 @@ pub mod names {
|
||||
pub const AGUNG: &str = "Agung";
|
||||
}
|
||||
|
||||
/// Register all built-in networks
|
||||
fn register_built_in_networks(registry: &mut HashMap<String, NetworkConfig>) {
|
||||
// Gnosis Chain
|
||||
registry.insert(names::GNOSIS.to_lowercase(), NetworkConfig {
|
||||
/// Get the Gnosis Chain network configuration
|
||||
pub fn gnosis() -> NetworkConfig {
|
||||
NetworkConfig {
|
||||
name: names::GNOSIS.to_string(),
|
||||
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,
|
||||
});
|
||||
|
||||
// Peaq Network
|
||||
registry.insert(names::PEAQ.to_lowercase(), NetworkConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Peaq Network configuration
|
||||
pub fn peaq() -> NetworkConfig {
|
||||
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,
|
||||
});
|
||||
|
||||
// Agung Testnet
|
||||
registry.insert(names::AGUNG.to_lowercase(), NetworkConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Agung Testnet configuration
|
||||
pub fn agung() -> NetworkConfig {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
/// 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)
|
||||
pub fn get_network_by_name(name: &str) -> Option<NetworkConfig> {
|
||||
if let Ok(registry) = NETWORK_REGISTRY.read() {
|
||||
registry.get(&name.to_lowercase()).cloned()
|
||||
} else {
|
||||
None
|
||||
let name_lower = name.to_lowercase();
|
||||
match name_lower.as_str() {
|
||||
"gnosis" => Some(gnosis()),
|
||||
"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
|
||||
pub fn list_network_names() -> Vec<String> {
|
||||
if let Ok(registry) = NETWORK_REGISTRY.read() {
|
||||
registry.values().map(|config| config.name.clone()).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
pub fn list_network_names() -> Vec<&'static str> {
|
||||
vec![names::GNOSIS, names::PEAQ, names::AGUNG]
|
||||
}
|
||||
|
||||
/// Get a map of all networks
|
||||
pub fn get_all_networks() -> HashMap<String, NetworkConfig> {
|
||||
if let Ok(registry) = NETWORK_REGISTRY.read() {
|
||||
registry.clone()
|
||||
} else {
|
||||
HashMap::new()
|
||||
}
|
||||
}
|
||||
pub fn get_all_networks() -> &'static HashMap<&'static str, NetworkConfig> {
|
||||
static NETWORKS: OnceLock<HashMap<&'static str, NetworkConfig>> = OnceLock::new();
|
||||
|
||||
// Legacy functions for backward compatibility
|
||||
|
||||
/// Get the Gnosis Chain network configuration
|
||||
pub fn gnosis() -> NetworkConfig {
|
||||
get_network_by_name("gnosis").unwrap_or_else(|| NetworkConfig {
|
||||
name: names::GNOSIS.to_string(),
|
||||
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,
|
||||
NETWORKS.get_or_init(|| {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(names::GNOSIS, gnosis());
|
||||
map.insert(names::PEAQ, peaq());
|
||||
map.insert(names::AGUNG, agung());
|
||||
map
|
||||
})
|
||||
}
|
||||
|
@ -3,36 +3,25 @@
|
||||
use ethers::prelude::*;
|
||||
|
||||
use crate::vault::error::CryptoError;
|
||||
use super::networks;
|
||||
use super::networks::{self, NetworkConfig};
|
||||
|
||||
/// Creates a provider for a specific network.
|
||||
pub fn create_provider(network_name: &str) -> Result<Provider<Http>, CryptoError> {
|
||||
let network = networks::get_network_by_name(network_name)
|
||||
.ok_or_else(|| CryptoError::SerializationError(format!("Unknown network: {}", network_name)))?;
|
||||
|
||||
pub fn create_provider(network: &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)))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn create_gnosis_provider() -> Result<Provider<Http>, CryptoError> {
|
||||
create_provider("gnosis")
|
||||
create_provider(&networks::gnosis())
|
||||
}
|
||||
|
||||
/// Creates a provider for the Peaq network.
|
||||
pub fn create_peaq_provider() -> Result<Provider<Http>, CryptoError> {
|
||||
create_provider("peaq")
|
||||
create_provider(&networks::peaq())
|
||||
}
|
||||
|
||||
/// Creates a provider for the Agung testnet.
|
||||
pub fn create_agung_provider() -> Result<Provider<Http>, CryptoError> {
|
||||
create_provider("agung")
|
||||
create_provider(&networks::agung())
|
||||
}
|
||||
|
@ -3,301 +3,112 @@
|
||||
use std::sync::Mutex;
|
||||
use std::collections::HashMap;
|
||||
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::kvs::{self, KVStore, DefaultStore};
|
||||
use super::wallet::EthereumWallet;
|
||||
use super::networks;
|
||||
use super::networks::{self, NetworkConfig};
|
||||
|
||||
/// Ethereum wallet data storage key in KVStore
|
||||
const ETH_WALLET_STORAGE_KEY: &str = "ethereum/wallets";
|
||||
|
||||
/// 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 storage for Ethereum wallets.
|
||||
static ETH_WALLETS: Lazy<Mutex<HashMap<String, Vec<EthereumWallet>>>> = Lazy::new(|| {
|
||||
Mutex::new(HashMap::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.
|
||||
pub fn create_ethereum_wallet_for_network(network: networks::NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
||||
create_ethereum_wallet()
|
||||
pub fn create_ethereum_wallet_for_network(network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
||||
// 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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
pub fn clear_ethereum_wallets_for_network(network_name: &str) {
|
||||
// In the new design, we don't have network-specific wallets,
|
||||
// so this is a no-op for backward compatibility
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
wallets.remove(network_name);
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
create_ethereum_wallet_from_name(name)
|
||||
pub fn create_ethereum_wallet_from_name_for_network(name: &str, network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
||||
// 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.
|
||||
pub fn create_ethereum_wallet_from_private_key_for_network(private_key: &str, network: networks::NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
||||
create_ethereum_wallet_from_private_key(private_key)
|
||||
pub fn create_ethereum_wallet_from_private_key_for_network(private_key: &str, network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
||||
// 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())
|
||||
}
|
||||
|
@ -22,123 +22,53 @@ fn test_network_config() {
|
||||
|
||||
#[test]
|
||||
fn test_network_registry() {
|
||||
// Get initial network names
|
||||
let initial_network_names = list_network_names();
|
||||
assert!(initial_network_names.iter().any(|name| name == "Gnosis"));
|
||||
assert!(initial_network_names.iter().any(|name| name == "Peaq"));
|
||||
assert!(initial_network_names.iter().any(|name| name == "Agung"));
|
||||
let network_names = networks::list_network_names();
|
||||
assert!(network_names.iter().any(|&name| name == "Gnosis"));
|
||||
assert!(network_names.iter().any(|&name| name == "Peaq"));
|
||||
assert!(network_names.iter().any(|&name| name == "Agung"));
|
||||
|
||||
// Test proper network name lookup
|
||||
let gnosis_proper = get_proper_network_name("gnosis");
|
||||
assert_eq!(gnosis_proper, Some(networks::names::GNOSIS));
|
||||
let gnosis_proper = networks::get_proper_network_name("gnosis");
|
||||
assert_eq!(gnosis_proper, Some("Gnosis"));
|
||||
|
||||
let peaq_proper = get_proper_network_name("peaq");
|
||||
assert_eq!(peaq_proper, Some(networks::names::PEAQ));
|
||||
let peaq_proper = networks::get_proper_network_name("peaq");
|
||||
assert_eq!(peaq_proper, Some("Peaq"));
|
||||
|
||||
let agung_proper = get_proper_network_name("agung");
|
||||
assert_eq!(agung_proper, Some(networks::names::AGUNG));
|
||||
let agung_proper = networks::get_proper_network_name("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);
|
||||
|
||||
// Test network lookup by name
|
||||
let gnosis_config = get_network_by_name("Gnosis");
|
||||
let gnosis_config = networks::get_network_by_name("Gnosis");
|
||||
assert!(gnosis_config.is_some());
|
||||
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());
|
||||
|
||||
// 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]
|
||||
fn test_create_provider() {
|
||||
// Create providers using network configs
|
||||
let gnosis_provider = create_provider_from_config(&networks::gnosis());
|
||||
let peaq_provider = create_provider_from_config(&networks::peaq());
|
||||
let agung_provider = create_provider_from_config(&networks::agung());
|
||||
let gnosis = networks::gnosis();
|
||||
let peaq = networks::peaq();
|
||||
let agung = 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
|
||||
assert!(gnosis_provider.is_ok());
|
||||
assert!(peaq_provider.is_ok());
|
||||
assert!(agung_provider.is_ok());
|
||||
|
||||
// Create providers using network names
|
||||
let gnosis_provider2 = create_provider("gnosis");
|
||||
let peaq_provider2 = create_provider("peaq");
|
||||
let agung_provider2 = create_provider("agung");
|
||||
// The convenience functions should also work
|
||||
let gnosis_provider2 = create_gnosis_provider();
|
||||
let peaq_provider2 = create_peaq_provider();
|
||||
let agung_provider2 = create_agung_provider();
|
||||
|
||||
assert!(gnosis_provider2.is_ok());
|
||||
assert!(peaq_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"));
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ fn test_get_balance() {
|
||||
|
||||
// Create a provider
|
||||
let network = networks::gnosis();
|
||||
let provider_result = create_provider_from_config(&network);
|
||||
let provider_result = create_provider(&network);
|
||||
|
||||
// The provider creation should succeed
|
||||
assert!(provider_result.is_ok());
|
||||
@ -59,10 +59,10 @@ fn test_send_eth() {
|
||||
// Create a wallet
|
||||
let keypair = KeyPair::new("test_keypair6");
|
||||
let network = networks::gnosis();
|
||||
let wallet = EthereumWallet::from_keypair(&keypair).unwrap();
|
||||
let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
|
||||
|
||||
// Create a provider
|
||||
let provider_result = create_provider_from_config(&network);
|
||||
let provider_result = create_provider(&network);
|
||||
assert!(provider_result.is_ok());
|
||||
|
||||
// We can't actually test send_eth without a blockchain
|
||||
|
@ -3,57 +3,59 @@
|
||||
use crate::vault::ethereum::*;
|
||||
use crate::vault::keypair::implementation::KeyPair;
|
||||
use ethers::utils::hex;
|
||||
use ethers::prelude::Signer;
|
||||
|
||||
#[test]
|
||||
fn test_ethereum_wallet_from_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
|
||||
assert!(wallet.address_string().starts_with("0x"));
|
||||
|
||||
// The wallet should not have a name
|
||||
assert!(wallet.name.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ethereum_wallet_from_name_and_keypair() {
|
||||
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
|
||||
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
|
||||
let wallet2 = EthereumWallet::from_name_and_keypair("test", &keypair).unwrap();
|
||||
assert_eq!(wallet.address_string(), wallet2.address_string());
|
||||
let wallet2 = EthereumWallet::from_name_and_keypair("test", &keypair, network.clone()).unwrap();
|
||||
assert_eq!(wallet.address, wallet2.address);
|
||||
|
||||
// Creating a wallet with a different name should yield a different address
|
||||
let wallet3 = EthereumWallet::from_name_and_keypair("test2", &keypair).unwrap();
|
||||
assert_ne!(wallet.address_string(), wallet3.address_string());
|
||||
let wallet3 = EthereumWallet::from_name_and_keypair("test2", &keypair, network.clone()).unwrap();
|
||||
assert_ne!(wallet.address, wallet3.address);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ethereum_wallet_from_private_key() {
|
||||
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
|
||||
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
|
||||
let wallet2 = EthereumWallet::from_private_key(private_key).unwrap();
|
||||
assert_eq!(wallet.address_string(), wallet2.address_string());
|
||||
let wallet2 = EthereumWallet::from_private_key(private_key, network.clone()).unwrap();
|
||||
assert_eq!(wallet.address, wallet2.address);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -65,63 +67,52 @@ fn test_wallet_management() {
|
||||
crate::vault::keypair::session_manager::create_space("test_space").unwrap();
|
||||
crate::vault::keypair::create_keypair("test_keypair3").unwrap();
|
||||
|
||||
// Create a wallet
|
||||
let wallet = create_ethereum_wallet().unwrap();
|
||||
// Create wallets for different networks
|
||||
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
|
||||
let current_wallet = get_current_ethereum_wallet().unwrap();
|
||||
// Get the current wallets
|
||||
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
|
||||
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_ethereum_wallets();
|
||||
|
||||
// Check that the wallet is gone
|
||||
let result = get_current_ethereum_wallet();
|
||||
assert!(result.is_err());
|
||||
|
||||
// The legacy network-specific wallet functions have been removed
|
||||
// We now use a single wallet that works across all networks
|
||||
|
||||
// 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());
|
||||
// Check that all wallets are gone
|
||||
let result1 = get_current_ethereum_wallet_for_network("Gnosis");
|
||||
let result2 = get_current_ethereum_wallet_for_network("Peaq");
|
||||
let result3 = get_current_ethereum_wallet_for_network("Agung");
|
||||
assert!(result1.is_err());
|
||||
assert!(result2.is_err());
|
||||
assert!(result3.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_message() {
|
||||
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
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
@ -137,8 +128,9 @@ fn test_sign_message() {
|
||||
#[test]
|
||||
fn test_private_key_hex() {
|
||||
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
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
//! Ethereum transaction functionality.
|
||||
|
||||
use ethers::prelude::*;
|
||||
use ethers::types::transaction::eip2718::TypedTransaction;
|
||||
|
||||
use crate::vault::error::CryptoError;
|
||||
use super::wallet::EthereumWallet;
|
||||
use super::networks::NetworkConfig;
|
||||
use super::provider;
|
||||
|
||||
/// Formats a token balance for display.
|
||||
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.
|
||||
pub async fn get_balance(network_name: &str, 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> {
|
||||
pub async fn get_balance(provider: &Provider<Http>, address: Address) -> Result<U256, CryptoError> {
|
||||
provider.get_balance(address, None)
|
||||
.await
|
||||
.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.
|
||||
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,
|
||||
provider: &Provider<Http>,
|
||||
to: Address,
|
||||
@ -96,29 +35,17 @@ pub async fn send_eth_with_provider(
|
||||
// Create a client with the wallet
|
||||
let client = SignerMiddleware::new(
|
||||
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
|
||||
let tx = TransactionRequest::new()
|
||||
.to(to)
|
||||
.value(amount)
|
||||
.gas(gas);
|
||||
.gas(21000);
|
||||
|
||||
// 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
|
||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to send transaction: {}", e)))?;
|
||||
|
||||
|
@ -4,135 +4,123 @@ use ethers::prelude::*;
|
||||
use ethers::signers::{LocalWallet, Signer, Wallet};
|
||||
use ethers::utils::hex;
|
||||
use k256::ecdsa::SigningKey;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::str::FromStr;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use crate::vault::error::CryptoError;
|
||||
use crate::vault::keypair::KeyPair;
|
||||
use super::networks::NetworkConfig;
|
||||
use crate::vault;
|
||||
use crate::vault::error::CryptoError;
|
||||
|
||||
/// An Ethereum wallet derived from a keypair.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EthereumWallet {
|
||||
pub address: Address,
|
||||
pub signer: Wallet<SigningKey>,
|
||||
pub name: Option<String>,
|
||||
pub wallet: Wallet<SigningKey>,
|
||||
pub network: NetworkConfig,
|
||||
}
|
||||
|
||||
impl EthereumWallet {
|
||||
/// Creates a new Ethereum wallet from a keypair.
|
||||
pub fn from_keypair(keypair: &KeyPair) -> Result<Self, CryptoError> {
|
||||
/// Creates a new Ethereum wallet from a keypair for a specific network.
|
||||
pub fn from_keypair(
|
||||
keypair: &vault::keyspace::keypair_types::KeyPair,
|
||||
network: NetworkConfig,
|
||||
) -> Result<Self, CryptoError> {
|
||||
// Get the private key bytes from the keypair
|
||||
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||
|
||||
|
||||
// Convert to a hex string (without 0x prefix)
|
||||
let private_key_hex = hex::encode(private_key_bytes);
|
||||
|
||||
|
||||
// Create an Ethereum wallet from the private key
|
||||
let signer = LocalWallet::from_str(&private_key_hex)
|
||||
.map_err(|_e| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||
.map_err(|_e| CryptoError::InvalidKeyLength)?
|
||||
.with_chain_id(network.chain_id);
|
||||
|
||||
// Get the Ethereum address
|
||||
let address = signer.address();
|
||||
|
||||
let address = wallet.address();
|
||||
|
||||
Ok(EthereumWallet {
|
||||
address,
|
||||
signer,
|
||||
name: None,
|
||||
wallet,
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new Ethereum wallet from a name and keypair (deterministic derivation).
|
||||
pub fn from_name_and_keypair(name: &str, keypair: &KeyPair) -> Result<Self, CryptoError> {
|
||||
|
||||
/// 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: &vault::keyspace::keypair_types::KeyPair,
|
||||
network: NetworkConfig,
|
||||
) -> Result<Self, CryptoError> {
|
||||
// Get the private key bytes from the keypair
|
||||
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||
|
||||
|
||||
// Create a deterministic seed by combining name and private key
|
||||
let mut hasher = Sha256::default();
|
||||
hasher.update(name.as_bytes());
|
||||
hasher.update(&private_key_bytes);
|
||||
let seed = hasher.finalize();
|
||||
|
||||
|
||||
// Use the seed as a private key
|
||||
let private_key_hex = hex::encode(seed);
|
||||
|
||||
|
||||
// Create an Ethereum wallet from the derived private key
|
||||
let signer = LocalWallet::from_str(&private_key_hex)
|
||||
.map_err(|_e| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||
.map_err(|_e| CryptoError::InvalidKeyLength)?
|
||||
.with_chain_id(network.chain_id);
|
||||
|
||||
// Get the Ethereum address
|
||||
let address = signer.address();
|
||||
|
||||
let address = wallet.address();
|
||||
|
||||
Ok(EthereumWallet {
|
||||
address,
|
||||
signer,
|
||||
name: Some(name.to_string()),
|
||||
wallet,
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new Ethereum wallet from a private key.
|
||||
pub fn from_private_key(private_key: &str) -> Result<Self, CryptoError> {
|
||||
|
||||
/// Creates a new Ethereum wallet from a private key for a specific network.
|
||||
pub fn from_private_key(
|
||||
private_key: &str,
|
||||
network: NetworkConfig,
|
||||
) -> Result<Self, CryptoError> {
|
||||
// Remove 0x prefix if present
|
||||
let private_key_clean = private_key.trim_start_matches("0x");
|
||||
|
||||
|
||||
// Create an Ethereum wallet from the private key
|
||||
let signer = LocalWallet::from_str(private_key_clean)
|
||||
.map_err(|_e| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
let wallet = LocalWallet::from_str(private_key_clean)
|
||||
.map_err(|_e| CryptoError::InvalidKeyLength)?
|
||||
.with_chain_id(network.chain_id);
|
||||
|
||||
// Get the Ethereum address
|
||||
let address = signer.address();
|
||||
|
||||
let address = wallet.address();
|
||||
|
||||
Ok(EthereumWallet {
|
||||
address,
|
||||
signer,
|
||||
name: None,
|
||||
wallet,
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Gets the Ethereum address as a string.
|
||||
pub fn address_string(&self) -> String {
|
||||
format!("{:?}", self.address)
|
||||
}
|
||||
|
||||
|
||||
/// Signs a message with the Ethereum wallet.
|
||||
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
|
||||
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
||||
|
||||
|
||||
Ok(signature.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Gets the private key as a hex 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)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
mod implementation_tests;
|
||||
mod keypair_types_tests;
|
||||
mod session_manager_tests;
|
@ -220,14 +220,14 @@ To include the Hero Vault Keypair module in your Rust project, add the following
|
||||
|
||||
```toml
|
||||
[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:
|
||||
|
||||
```rust
|
||||
use vault::vault::keypair::{KeySpace, KeyPair};
|
||||
use vault::vault::error::CryptoError;
|
||||
use hero_vault::vault::keypair::{KeySpace, KeyPair};
|
||||
use hero_vault::vault::error::CryptoError;
|
||||
```
|
||||
|
||||
## Testing
|
||||
@ -263,7 +263,7 @@ The module uses the `CryptoError` type for handling errors that can occur during
|
||||
|
||||
## 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
|
||||
- `advanced_example.rhai` - Advanced example with error handling
|
@ -1,13 +1,15 @@
|
||||
/// Implementation of keypair functionality.
|
||||
|
||||
use k256::ecdsa::{SigningKey, VerifyingKey, signature::{Signer, Verifier}, Signature};
|
||||
use k256::ecdsa::{
|
||||
signature::{Signer, Verifier},
|
||||
Signature, SigningKey, VerifyingKey,
|
||||
};
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use crate::vault::symmetric::implementation;
|
||||
use crate::vault::error::CryptoError;
|
||||
use crate::vault::symmetric::implementation;
|
||||
|
||||
/// A keypair for signing and verifying messages.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -22,8 +24,8 @@ pub struct KeyPair {
|
||||
// Serialization helpers for VerifyingKey
|
||||
mod verifying_key_serde {
|
||||
use super::*;
|
||||
use serde::{Serializer, Deserializer};
|
||||
use serde::de::{self, Visitor};
|
||||
use serde::{Deserializer, Serializer};
|
||||
use std::fmt;
|
||||
|
||||
pub fn serialize<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error>
|
||||
@ -63,7 +65,7 @@ mod verifying_key_serde {
|
||||
while let Some(byte) = seq.next_element()? {
|
||||
bytes.push(byte);
|
||||
}
|
||||
|
||||
|
||||
VerifyingKey::from_sec1_bytes(&bytes).map_err(|e| {
|
||||
log::error!("Error deserializing verifying key from seq: {:?}", e);
|
||||
de::Error::custom(format!("invalid verifying key from seq: {:?}", e))
|
||||
@ -83,8 +85,8 @@ mod verifying_key_serde {
|
||||
// Serialization helpers for SigningKey
|
||||
mod signing_key_serde {
|
||||
use super::*;
|
||||
use serde::{Serializer, Deserializer};
|
||||
use serde::de::{self, Visitor};
|
||||
use serde::{Deserializer, Serializer};
|
||||
use std::fmt;
|
||||
|
||||
pub fn serialize<S>(key: &SigningKey, serializer: S) -> Result<S::Ok, S::Error>
|
||||
@ -124,7 +126,7 @@ mod signing_key_serde {
|
||||
while let Some(byte) = seq.next_element()? {
|
||||
bytes.push(byte);
|
||||
}
|
||||
|
||||
|
||||
SigningKey::from_bytes(bytes.as_slice().into()).map_err(|e| {
|
||||
log::error!("Error deserializing signing key from seq: {:?}", e);
|
||||
de::Error::custom(format!("invalid signing key from seq: {:?}", e))
|
||||
@ -146,7 +148,7 @@ impl KeyPair {
|
||||
pub fn new(name: &str) -> Self {
|
||||
let signing_key = SigningKey::random(&mut OsRng);
|
||||
let verifying_key = VerifyingKey::from(&signing_key);
|
||||
|
||||
|
||||
KeyPair {
|
||||
name: name.to_string(),
|
||||
verifying_key,
|
||||
@ -158,7 +160,7 @@ impl KeyPair {
|
||||
pub fn pub_key(&self) -> Vec<u8> {
|
||||
self.verifying_key.to_sec1_bytes().to_vec()
|
||||
}
|
||||
|
||||
|
||||
/// Derives a public key from a private key.
|
||||
pub fn pub_key_from_private(private_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
let signing_key = SigningKey::from_bytes(private_key.into())
|
||||
@ -177,93 +179,121 @@ impl KeyPair {
|
||||
pub fn verify(&self, message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
||||
let signature = Signature::from_bytes(signature_bytes.into())
|
||||
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
||||
|
||||
|
||||
match self.verifying_key.verify(message, &signature) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false), // Verification failed, but operation was successful
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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> {
|
||||
let verifying_key = VerifyingKey::from_sec1_bytes(public_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
pub fn verify_with_public_key(
|
||||
public_key: &[u8],
|
||||
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())
|
||||
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
||||
|
||||
|
||||
match verifying_key.verify(message, &signature) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false), // Verification failed, but operation was successful
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Encrypts a message using the recipient's public key.
|
||||
/// This implements ECIES (Elliptic Curve Integrated Encryption Scheme):
|
||||
/// 1. Generate an ephemeral keypair
|
||||
/// 2. Derive a shared secret using ECDH
|
||||
/// 3. Derive encryption key from the shared secret
|
||||
/// 4. Encrypt the message using symmetric encryption
|
||||
/// 5. Return the ephemeral public key and the ciphertext
|
||||
pub fn encrypt_asymmetric(&self, recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
// Parse recipient's public key
|
||||
let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key)
|
||||
/// This implements a simplified version of ECIES (Elliptic Curve Integrated Encryption Scheme):
|
||||
/// 1. Generate a random symmetric key
|
||||
/// 2. Encrypt the message with the symmetric key
|
||||
/// 3. Encrypt the symmetric key with the recipient's public key
|
||||
/// 4. Return the encrypted key and the ciphertext
|
||||
pub fn encrypt_asymmetric(
|
||||
&self,
|
||||
recipient_public_key: &[u8],
|
||||
message: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
// Validate recipient's public key format
|
||||
VerifyingKey::from_sec1_bytes(recipient_public_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
// Generate ephemeral keypair
|
||||
let ephemeral_signing_key = SigningKey::random(&mut OsRng);
|
||||
let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key);
|
||||
|
||||
// Derive shared secret (this is a simplified ECDH)
|
||||
// In a real implementation, we would use proper ECDH, but for this example:
|
||||
let shared_point = recipient_key.to_encoded_point(false);
|
||||
let shared_secret = {
|
||||
|
||||
// Generate a random symmetric key
|
||||
let symmetric_key = implementation::generate_symmetric_key();
|
||||
|
||||
// Encrypt the message with the symmetric key
|
||||
let encrypted_message = implementation::encrypt_with_key(&symmetric_key, message)
|
||||
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
|
||||
|
||||
// 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();
|
||||
hasher.update(ephemeral_signing_key.to_bytes());
|
||||
hasher.update(shared_point.as_bytes());
|
||||
hasher.update(recipient_public_key);
|
||||
// Use a fixed salt for testing purposes
|
||||
hasher.update(b"fixed_salt_for_testing");
|
||||
hasher.finalize().to_vec()
|
||||
};
|
||||
|
||||
// Encrypt the message using the derived key
|
||||
let ciphertext = implementation::encrypt_with_key(&shared_secret, message)
|
||||
|
||||
// Encrypt the symmetric key
|
||||
let encrypted_key = implementation::encrypt_with_key(&key_encryption_key, &symmetric_key)
|
||||
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
|
||||
|
||||
// Format: ephemeral_public_key || ciphertext
|
||||
let mut result = ephemeral_public_key.to_sec1_bytes().to_vec();
|
||||
result.extend_from_slice(&ciphertext);
|
||||
|
||||
|
||||
// Format: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message
|
||||
let mut result = Vec::new();
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
/// Decrypts a message using the recipient's private key.
|
||||
/// This is the counterpart to encrypt_asymmetric.
|
||||
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
|
||||
// For simplicity, we'll assume uncompressed keys (65 bytes)
|
||||
if ciphertext.len() <= 65 {
|
||||
return Err(CryptoError::DecryptionFailed("Ciphertext too short".to_string()));
|
||||
// The format is: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message
|
||||
if ciphertext.len() <= 4 {
|
||||
return Err(CryptoError::DecryptionFailed(
|
||||
"Ciphertext too short".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Extract ephemeral public key and actual ciphertext
|
||||
let ephemeral_public_key = &ciphertext[..65];
|
||||
let actual_ciphertext = &ciphertext[65..];
|
||||
|
||||
// Parse ephemeral public key
|
||||
let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
// Derive shared secret (simplified ECDH)
|
||||
let shared_point = sender_key.to_encoded_point(false);
|
||||
let shared_secret = {
|
||||
|
||||
// Extract the encrypted key length
|
||||
let mut key_len_bytes = [0u8; 4];
|
||||
key_len_bytes.copy_from_slice(&ciphertext[0..4]);
|
||||
let key_len = u32::from_be_bytes(key_len_bytes) as usize;
|
||||
|
||||
// Check if the ciphertext is long enough
|
||||
if ciphertext.len() <= 4 + key_len {
|
||||
return Err(CryptoError::DecryptionFailed(
|
||||
"Ciphertext too short".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Extract the encrypted key and the encrypted message
|
||||
let encrypted_key = &ciphertext[4..4 + key_len];
|
||||
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();
|
||||
hasher.update(self.signing_key.to_bytes());
|
||||
hasher.update(shared_point.as_bytes());
|
||||
hasher.update(self.verifying_key.to_sec1_bytes());
|
||||
// Use the same fixed salt as in encryption
|
||||
hasher.update(b"fixed_salt_for_testing");
|
||||
hasher.finalize().to_vec()
|
||||
};
|
||||
|
||||
// Decrypt the message using the derived key
|
||||
implementation::decrypt_with_key(&shared_secret, actual_ciphertext)
|
||||
.map_err(|e| CryptoError::DecryptionFailed(e.to_string()))
|
||||
|
||||
// Decrypt the symmetric key
|
||||
let symmetric_key = implementation::decrypt_with_key(&key_encryption_key, encrypted_key)
|
||||
.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)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,7 +318,7 @@ impl KeySpace {
|
||||
if self.keypairs.contains_key(name) {
|
||||
return Err(CryptoError::KeypairAlreadyExists(name.to_string()));
|
||||
}
|
||||
|
||||
|
||||
let keypair = KeyPair::new(name);
|
||||
self.keypairs.insert(name.to_string(), keypair);
|
||||
Ok(())
|
||||
@ -296,7 +326,9 @@ impl KeySpace {
|
||||
|
||||
/// Gets a keypair by name.
|
||||
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.
|
||||
@ -304,4 +336,3 @@ impl KeySpace {
|
||||
self.keypairs.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
|
||||
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.
|
||||
pub struct Session {
|
36
src/vault/keyspace/spec.md
Normal file
36
src/vault/keyspace/spec.md
Normal 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.
|
@ -1,5 +1,4 @@
|
||||
|
||||
use crate::vault::keypair::keypair_types::{KeyPair, KeySpace};
|
||||
use crate::vault::keyspace::keypair_types::{KeyPair, KeySpace};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@ -20,12 +19,16 @@ mod tests {
|
||||
let signature = keypair.sign(message);
|
||||
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);
|
||||
|
||||
// Test with a wrong 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);
|
||||
}
|
||||
|
||||
@ -36,13 +39,16 @@ mod tests {
|
||||
let signature = keypair.sign(message);
|
||||
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);
|
||||
|
||||
// Test with a wrong public key
|
||||
let wrong_keypair = KeyPair::new("wrong_keypair");
|
||||
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);
|
||||
}
|
||||
|
||||
@ -50,7 +56,7 @@ mod tests {
|
||||
fn test_asymmetric_encryption_decryption() {
|
||||
// Sender's keypair
|
||||
let sender_keypair = KeyPair::new("sender");
|
||||
let sender_public_key = sender_keypair.pub_key();
|
||||
let _ = sender_keypair.pub_key();
|
||||
|
||||
// Recipient's keypair
|
||||
let recipient_keypair = KeyPair::new("recipient");
|
||||
@ -59,11 +65,15 @@ mod tests {
|
||||
let message = b"This is a secret message";
|
||||
|
||||
// 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());
|
||||
|
||||
// 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);
|
||||
|
||||
// Test decryption with wrong keypair
|
||||
@ -75,7 +85,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_keyspace_add_keypair() {
|
||||
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!(space.keypairs.contains_key("keypair1"));
|
||||
|
||||
@ -83,4 +95,4 @@ mod tests {
|
||||
let result = space.add_keypair("keypair1");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
}
|
3
src/vault/keyspace/tests/mod.rs
Normal file
3
src/vault/keyspace/tests/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
mod keypair_types_tests;
|
||||
mod session_manager_tests;
|
@ -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,
|
||||
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
|
||||
fn setup_test() {
|
||||
@ -48,7 +48,8 @@ mod tests {
|
||||
assert_eq!(keypair.name, "test_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");
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ The module uses the `CryptoError` type for handling errors that can occur during
|
||||
|
||||
## 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:
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
@ -1,65 +1,17 @@
|
||||
//! Key-Value Store functionality
|
||||
//!
|
||||
//! This module provides a simple key-value store with encryption support.
|
||||
//!
|
||||
//! The implementation uses different backends depending on the platform:
|
||||
//! - For WebAssembly: IndexedDB through the `IndexedDbStore` type
|
||||
//! - For native platforms: SlateDB through the `SlateDbStore` type
|
||||
//!
|
||||
//! All implementations share the same interface defined by the `KVStore` trait.
|
||||
|
||||
mod error;
|
||||
mod store;
|
||||
mod key_space;
|
||||
mod slate_store;
|
||||
mod indexed_db_store;
|
||||
pub mod error;
|
||||
pub mod store;
|
||||
|
||||
// Re-export public types and functions
|
||||
pub use error::{KvsError, Result};
|
||||
pub use store::{KvPair, KVStore};
|
||||
|
||||
// Re-export the SlateDbStore for native platforms
|
||||
pub use slate_store::{
|
||||
SlateDbStore, create_slatedb_store, open_slatedb_store,
|
||||
delete_slatedb_store, list_slatedb_stores
|
||||
pub use error::KvsError;
|
||||
pub use store::{
|
||||
KvStore, KvPair,
|
||||
create_store, open_store, delete_store,
|
||||
list_stores, get_store_path
|
||||
};
|
||||
|
||||
// Re-export the IndexedDbStore for WebAssembly
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use indexed_db_store::{
|
||||
IndexedDbStore, create_indexeddb_store, open_indexeddb_store
|
||||
};
|
||||
|
||||
// Define the default store type based on platform
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type DefaultStore = IndexedDbStore;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type DefaultStore = SlateDbStore;
|
||||
|
||||
// Re-export key_space functionality
|
||||
pub use key_space::{
|
||||
load_key_space, create_key_space, save_key_space,
|
||||
list_key_spaces, delete_key_space
|
||||
};
|
||||
|
||||
// Platform-specific open/create functions that return the appropriate DefaultStore
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn open_default_store(name: &str, password: Option<&str>) -> Result<DefaultStore> {
|
||||
open_indexeddb_store(name, password).await
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn open_default_store(name: &str, password: Option<&str>) -> Result<DefaultStore> {
|
||||
open_slatedb_store(name, password)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn create_default_store(name: &str, encrypted: bool, password: Option<&str>) -> Result<DefaultStore> {
|
||||
create_indexeddb_store(name, encrypted).await
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn create_default_store(name: &str, encrypted: bool, password: Option<&str>) -> Result<DefaultStore> {
|
||||
create_slatedb_store(name, encrypted, password)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -17,46 +17,6 @@ pub struct KvPair {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// A common trait for key-value store implementations.
|
||||
///
|
||||
/// This trait defines the operations that all key-value stores must support,
|
||||
/// regardless of the underlying implementation.
|
||||
pub trait KVStore: Clone {
|
||||
/// Stores a value with the given key.
|
||||
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
V: Serialize;
|
||||
|
||||
/// Retrieves a value for the given key.
|
||||
fn get<K, V>(&self, key: K) -> Result<V>
|
||||
where
|
||||
K: ToString,
|
||||
V: DeserializeOwned;
|
||||
|
||||
/// Deletes a value for the given key.
|
||||
fn delete<K>(&self, key: K) -> Result<()>
|
||||
where
|
||||
K: ToString;
|
||||
|
||||
/// Checks if a key exists in the store.
|
||||
fn contains<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString;
|
||||
|
||||
/// Lists all keys in the store.
|
||||
fn keys(&self) -> Result<Vec<String>>;
|
||||
|
||||
/// Clears all key-value pairs from the store.
|
||||
fn clear(&self) -> Result<()>;
|
||||
|
||||
/// Gets the name of the store.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Gets whether the store is encrypted.
|
||||
fn is_encrypted(&self) -> bool;
|
||||
}
|
||||
|
||||
/// A simple key-value store.
|
||||
///
|
||||
/// This implementation uses the filesystem to store key-value pairs.
|
||||
@ -264,12 +224,18 @@ impl KvStore {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Implement the KVStore trait for the standard KvStore
|
||||
impl KVStore for KvStore {
|
||||
/// 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
|
||||
K: ToString,
|
||||
V: Serialize,
|
||||
@ -290,7 +256,15 @@ impl KVStore for KvStore {
|
||||
}
|
||||
|
||||
/// 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
|
||||
K: ToString,
|
||||
V: DeserializeOwned,
|
||||
@ -308,7 +282,15 @@ impl KVStore for KvStore {
|
||||
}
|
||||
|
||||
/// 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
|
||||
K: ToString,
|
||||
{
|
||||
@ -329,7 +311,15 @@ impl KVStore for KvStore {
|
||||
}
|
||||
|
||||
/// 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
|
||||
K: ToString,
|
||||
{
|
||||
@ -340,14 +330,22 @@ impl KVStore for KvStore {
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
Ok(data.keys().cloned().collect())
|
||||
}
|
||||
|
||||
/// 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
|
||||
{
|
||||
let mut data = self.data.lock().unwrap();
|
||||
@ -361,12 +359,12 @@ impl KVStore for KvStore {
|
||||
}
|
||||
|
||||
/// Gets the name of the store.
|
||||
fn name(&self) -> &str {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Gets whether the store is encrypted.
|
||||
fn is_encrypted(&self) -> bool {
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.encrypted
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
use crate::vault::kvs::store::{create_store, delete_store, open_store, KvStore};
|
||||
use std::path::PathBuf;
|
||||
use crate::vault::kvs::store::{create_store, delete_store, open_store};
|
||||
|
||||
// Helper function to generate a unique store name for each test
|
||||
fn generate_test_store_name() -> String {
|
||||
@ -102,4 +101,4 @@ mod tests {
|
||||
|
||||
cleanup_test_store(&store_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
//! - Key-value store with encryption
|
||||
|
||||
pub mod error;
|
||||
pub mod keypair;
|
||||
pub mod keyspace;
|
||||
pub mod symmetric;
|
||||
pub mod ethereum;
|
||||
pub mod kvs;
|
||||
@ -17,4 +17,4 @@ pub mod kvs;
|
||||
// Re-export modules
|
||||
// Re-export common types for convenience
|
||||
pub use error::CryptoError;
|
||||
pub use keypair::{KeyPair, KeySpace};
|
||||
pub use keyspace::{KeyPair, KeySpace};
|
||||
|
@ -92,7 +92,7 @@ The module uses the `CryptoError` type for handling errors that can occur during
|
||||
|
||||
## 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
|
||||
- `advanced_example.rhai` - Advanced example with error handling
|
||||
|
@ -7,7 +7,7 @@ use serde::{Serialize, Deserialize};
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use crate::vault::error::CryptoError;
|
||||
use crate::vault::keypair::KeySpace;
|
||||
use crate::vault::keyspace::KeySpace;
|
||||
|
||||
/// The size of the nonce in bytes.
|
||||
const NONCE_SIZE: usize = 12;
|
||||
|
Loading…
Reference in New Issue
Block a user