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]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
base64 = "0.21.0" # Base64 encoding/decoding
|
base64 = "0.22.1" # Base64 encoding/decoding
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher
|
chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher
|
||||||
clap = "2.33" # Command-line argument parsing
|
clap = "2.34.0" # Command-line argument parsing
|
||||||
dirs = "5.0.1" # Directory paths
|
dirs = "6.0.0" # Directory paths
|
||||||
env_logger = "0.10.0" # Logger implementation
|
env_logger = "0.11.8" # Logger implementation
|
||||||
ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library
|
ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library
|
||||||
glob = "0.3.1" # For file pattern matching
|
glob = "0.3.1" # For file pattern matching
|
||||||
jsonrpsee = "0.25.1"
|
jsonrpsee = "0.25.1"
|
||||||
k256 = { version = "0.13.1", features = ["ecdsa", "ecdh"] } # Elliptic curve cryptography
|
k256 = { version = "0.13.4", features = ["ecdsa", "ecdh"] } # Elliptic curve cryptography
|
||||||
lazy_static = "1.4.0" # For lazy initialization of static variables
|
lazy_static = "1.4.0" # For lazy initialization of static variables
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4" # Logging facade
|
log = "0.4" # Logging facade
|
||||||
@ -31,7 +31,7 @@ postgres-types = "0.2.5" # PostgreSQL type conversions
|
|||||||
r2d2 = "0.8.10"
|
r2d2 = "0.8.10"
|
||||||
r2d2_postgres = "0.18.2"
|
r2d2_postgres = "0.18.2"
|
||||||
rand = "0.8.5" # Random number generation
|
rand = "0.8.5" # Random number generation
|
||||||
redis = "0.22.0" # Redis client
|
redis = "0.31.0" # Redis client
|
||||||
regex = "1.8.1" # For regex pattern matching
|
regex = "1.8.1" # For regex pattern matching
|
||||||
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
|
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
|
||||||
serde = { version = "1.0", features = [
|
serde = { version = "1.0", features = [
|
||||||
@ -40,23 +40,26 @@ serde = { version = "1.0", features = [
|
|||||||
serde_json = "1.0" # For JSON handling
|
serde_json = "1.0" # For JSON handling
|
||||||
sha2 = "0.10.7" # SHA-2 hash functions
|
sha2 = "0.10.7" # SHA-2 hash functions
|
||||||
tempfile = "3.5" # For temporary file operations
|
tempfile = "3.5" # For temporary file operations
|
||||||
tokio = { version = "1.28", features = ["full"] }
|
tera = "1.19.0" # Template engine for text rendering
|
||||||
|
thiserror = "2.0.12" # For error handling
|
||||||
|
tokio = "1.45.0"
|
||||||
|
tokio-postgres = "0.7.8" # Async PostgreSQL client
|
||||||
tokio-test = "0.4.4"
|
tokio-test = "0.4.4"
|
||||||
uuid = { version = "1.16.0", features = ["v4"] }
|
uuid = { version = "1.16.0", features = ["v4"] }
|
||||||
|
zinit-client = { git = "https://github.com/threefoldtech/zinit", branch = "json_rpc", package = "zinit-client" }
|
||||||
# Optional features for specific OS functionality
|
# Optional features for specific OS functionality
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
nix = "0.26" # Unix-specific functionality
|
nix = "0.30.1" # Unix-specific functionality
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.48", features = [
|
windows = { version = "0.61.1", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
"Win32_Storage_FileSystem",
|
"Win32_Storage_FileSystem",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockall = "0.11.4" # For mocking in tests
|
mockall = "0.13.1" # For mocking in tests
|
||||||
tempfile = "3.5" # For tests that need temporary files/directories
|
tempfile = "3.5" # For tests that need temporary files/directories
|
||||||
tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing
|
tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing
|
||||||
|
|
||||||
|
@ -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
|
// This script demonstrates how to create a wallet from a private key and send tokens
|
||||||
|
|
||||||
// Define the private key and recipient address
|
// Define the private key and recipient address
|
||||||
let private_key = "51c194d20bcd25360a3aa94426b3b60f738007e42f22e1bc97821c65c353e6d2";
|
let private_key = "0x9ecfd58eca522b0e7c109bf945966ee208cd6d593b1dc3378aedfdc60b64f512";
|
||||||
let recipient_address = "0xf400f9c3F7317e19523a5DB698Ce67e7a7E083e2";
|
let recipient_address = "0xf400f9c3F7317e19523a5DB698Ce67e7a7E083e2";
|
||||||
|
|
||||||
print("=== Agung Wallet Transaction Demo ===");
|
print("=== Agung Wallet Transaction Demo ===");
|
||||||
@ -33,32 +33,32 @@ if !select_keypair("demo_keypair") {
|
|||||||
|
|
||||||
print("\nCreated and selected keypair successfully");
|
print("\nCreated and selected keypair successfully");
|
||||||
|
|
||||||
// Clear any existing Ethereum wallets to avoid conflicts
|
// Clear any existing Agung wallets to avoid conflicts
|
||||||
if clear_ethereum_wallets() {
|
if clear_wallets_for_network("agung") {
|
||||||
print("Cleared existing Ethereum wallets");
|
print("Cleared existing Agung wallets");
|
||||||
} else {
|
} else {
|
||||||
print("Failed to clear existing Ethereum wallets");
|
print("Failed to clear existing Agung wallets");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a wallet from the private key directly
|
// Create a wallet from the private key directly
|
||||||
print("\n=== Creating Wallet from Private Key ===");
|
print("\n=== Creating Wallet from Private Key ===");
|
||||||
|
|
||||||
// Create a wallet from the private key (works for any network)
|
// Create a wallet from the private key for the Agung network
|
||||||
if create_ethereum_wallet_from_private_key(private_key) {
|
if create_wallet_from_private_key_for_network(private_key, "agung") {
|
||||||
print("Successfully created wallet from private key");
|
print("Successfully created wallet from private key for Agung network");
|
||||||
|
|
||||||
// Get the wallet address
|
// Get the wallet address
|
||||||
let wallet_address = get_ethereum_address();
|
let wallet_address = get_wallet_address_for_network("agung");
|
||||||
print(`Wallet address: ${wallet_address}`);
|
print(`Wallet address: ${wallet_address}`);
|
||||||
|
|
||||||
// Create a provider for the Agung network
|
// Create a provider for the Agung network
|
||||||
let provider_id = create_provider("agung");
|
let provider_id = create_agung_provider();
|
||||||
if provider_id != "" {
|
if provider_id != "" {
|
||||||
print("Successfully created Agung provider");
|
print("Successfully created Agung provider");
|
||||||
|
|
||||||
// Check the wallet balance first
|
// Check the wallet balance first
|
||||||
let wallet_address = get_ethereum_address();
|
let wallet_address = get_wallet_address_for_network("agung");
|
||||||
let balance_wei = get_balance("agung", wallet_address);
|
let balance_wei = get_balance("agung", wallet_address);
|
||||||
|
|
||||||
if balance_wei == "" {
|
if balance_wei == "" {
|
||||||
@ -67,18 +67,16 @@ if create_ethereum_wallet_from_private_key(private_key) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print(`Current wallet balance: ${balance_wei}`);
|
print(`Current wallet balance: ${balance_wei} wei`);
|
||||||
|
|
||||||
// Convert 1 AGNG to wei (1 AGNG = 10^18 wei)
|
// Convert 1 AGNG to wei (1 AGNG = 10^18 wei)
|
||||||
// Use string representation for large numbers
|
// Use string representation for large numbers
|
||||||
let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string
|
let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string
|
||||||
|
|
||||||
// For this example, we'll assume we have enough balance
|
// Check if we have enough balance
|
||||||
// NOTE: In a real application, you would need to check the balance properly
|
if parse_int(balance_wei) < parse_int(amount_wei_str) {
|
||||||
// by parsing it and comparing with the amount.
|
|
||||||
if false { // Disabled check since balance format has changed
|
|
||||||
print(`Insufficient balance to send ${amount_wei_str} wei (1 AGNG)`);
|
print(`Insufficient balance to send ${amount_wei_str} wei (1 AGNG)`);
|
||||||
print(`Current balance: ${balance_wei}`);
|
print(`Current balance: ${balance_wei} wei`);
|
||||||
print("Please fund the wallet before attempting to send a transaction");
|
print("Please fund the wallet before attempting to send a transaction");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -206,7 +206,7 @@ impl RedisClientWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Select the database
|
// Select the database
|
||||||
redis::cmd("SELECT").arg(self.db).execute(&mut conn);
|
let _ = redis::cmd("SELECT").arg(self.db).exec(&mut conn);
|
||||||
|
|
||||||
self.initialized.store(true, Ordering::Relaxed);
|
self.initialized.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
|
@ -1,128 +1,252 @@
|
|||||||
//! Rhai bindings for SAL crypto functionality
|
//! Rhai bindings for SAL crypto functionality
|
||||||
|
|
||||||
use rhai::{Engine, Dynamic, EvalAltResult};
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
|
||||||
use std::fs;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use once_cell::sync::{Lazy, OnceCell};
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use ethers::types::{Address, U256};
|
use ethers::types::{Address, U256};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use rhai::{Dynamic, Engine, EvalAltResult};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use cfg_if::cfg_if;
|
use std::sync::Mutex;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
use crate::vault::{keypair, symmetric, ethereum, kvs};
|
use crate::vault::ethereum;
|
||||||
use crate::vault::kvs::{KVStore, DefaultStore};
|
use crate::vault::keyspace::session_manager as keypair;
|
||||||
use crate::vault::ethereum::prepare_function_arguments;
|
|
||||||
|
|
||||||
|
use crate::vault::symmetric::implementation as symmetric_impl;
|
||||||
// Global Tokio runtime for blocking async operations
|
// Global Tokio runtime for blocking async operations
|
||||||
static RUNTIME: Lazy<Mutex<Runtime>> = Lazy::new(|| {
|
static RUNTIME: Lazy<Mutex<Runtime>> =
|
||||||
Mutex::new(Runtime::new().expect("Failed to create Tokio runtime"))
|
Lazy::new(|| Mutex::new(Runtime::new().expect("Failed to create Tokio runtime")));
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to run async operations and handle errors consistently
|
// Global provider registry
|
||||||
fn run_async<F, T, E>(future: F) -> Result<T, String>
|
static PROVIDERS: Lazy<
|
||||||
where
|
Mutex<HashMap<String, ethers::providers::Provider<ethers::providers::Http>>>,
|
||||||
F: std::future::Future<Output = Result<T, E>>,
|
> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||||
E: std::fmt::Display,
|
|
||||||
{
|
|
||||||
let rt = RUNTIME.lock().map_err(|e| format!("Failed to acquire runtime lock: {}", e))?;
|
|
||||||
rt.block_on(async { future.await.map_err(|e| e.to_string()) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a platform-specific DefaultStore implementation for Rhai bindings
|
// Key space management functions
|
||||||
// This function is implemented differently based on the target platform
|
fn load_key_space(name: &str, password: &str) -> bool {
|
||||||
cfg_if! {
|
// Get the key spaces directory from config
|
||||||
if #[cfg(target_arch = "wasm32")] {
|
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
fn get_key_store() -> DefaultStore {
|
let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces");
|
||||||
use wasm_bindgen_futures::JsFuture;
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use std::future::Future;
|
|
||||||
|
|
||||||
// Static store instance
|
// Check if directory exists
|
||||||
static KEY_STORE: OnceCell<DefaultStore> = OnceCell::new();
|
if !key_spaces_dir.exists() {
|
||||||
|
log::error!("Key spaces directory does not exist");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize if not already done
|
// Get the key space file path
|
||||||
KEY_STORE.get_or_init(|| {
|
let space_path = key_spaces_dir.join(format!("{}.json", name));
|
||||||
// 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
|
// Check if file exists
|
||||||
let rt = RUNTIME.lock().unwrap();
|
if !space_path.exists() {
|
||||||
rt.block_on(store_future)
|
log::error!("Key space file not found: {}", space_path.display());
|
||||||
}).clone()
|
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
|
// Deserialize the encrypted space
|
||||||
static KEY_STORE: OnceCell<DefaultStore> = OnceCell::new();
|
let encrypted_space = match symmetric_impl::deserialize_encrypted_space(&serialized) {
|
||||||
|
Ok(space) => space,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error deserializing key space: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize if not already done
|
// Decrypt the space
|
||||||
KEY_STORE.get_or_init(|| {
|
let space = match symmetric_impl::decrypt_key_space(&encrypted_space, password) {
|
||||||
// For native platforms, the operations are synchronous
|
Ok(space) => space,
|
||||||
match kvs::open_default_store("rhai-vault", None) {
|
Err(e) => {
|
||||||
Ok(store) => store,
|
log::error!("Error decrypting key space: {}", e);
|
||||||
Err(_) => {
|
return false;
|
||||||
// Try to create the store if opening failed
|
}
|
||||||
kvs::create_default_store("rhai-vault", false, None)
|
};
|
||||||
.expect("Failed to create key store")
|
|
||||||
}
|
// Set as current space
|
||||||
}
|
match keypair::set_current_space(space) {
|
||||||
}).clone()
|
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 {
|
fn create_key_space(name: &str, password: &str) -> bool {
|
||||||
let store = get_key_store();
|
match keypair::create_space(name) {
|
||||||
kvs::create_key_space(&store, name, password)
|
Ok(_) => {
|
||||||
|
// Get the current space
|
||||||
|
match keypair::get_current_space() {
|
||||||
|
Ok(space) => {
|
||||||
|
// Encrypt the key space
|
||||||
|
let encrypted_space = match symmetric_impl::encrypt_key_space(&space, password)
|
||||||
|
{
|
||||||
|
Ok(encrypted) => encrypted,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error encrypting key space: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Serialize the encrypted space
|
||||||
|
let serialized =
|
||||||
|
match symmetric_impl::serialize_encrypted_space(&encrypted_space) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error serializing encrypted space: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the key spaces directory
|
||||||
|
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
|
let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces");
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if !key_spaces_dir.exists() {
|
||||||
|
match fs::create_dir_all(&key_spaces_dir) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error creating key spaces directory: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
let space_path = key_spaces_dir.join(format!("{}.json", name));
|
||||||
|
match fs::write(&space_path, serialized) {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("Key space created and saved to {}", space_path.display());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error writing key space file: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error getting current space: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error creating key space: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-save function for internal use
|
// Auto-save function for internal use
|
||||||
fn auto_save_key_space(password: &str) -> bool {
|
fn auto_save_key_space(password: &str) -> bool {
|
||||||
let store = get_key_store();
|
match keypair::get_current_space() {
|
||||||
kvs::save_key_space(&store, password)
|
Ok(space) => {
|
||||||
|
// Encrypt the key space
|
||||||
|
let encrypted_space = match symmetric_impl::encrypt_key_space(&space, password) {
|
||||||
|
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 {
|
fn encrypt_key_space(password: &str) -> String {
|
||||||
match keypair::get_current_space()
|
match keypair::get_current_space() {
|
||||||
.and_then(|space| symmetric::encrypt_key_space(&space, password))
|
Ok(space) => match symmetric_impl::encrypt_key_space(&space, password) {
|
||||||
.and_then(|encrypted_space| symmetric::serialize_encrypted_space(&encrypted_space))
|
Ok(encrypted_space) => match serde_json::to_string(&encrypted_space) {
|
||||||
{
|
Ok(json) => json,
|
||||||
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) => {
|
Err(e) => {
|
||||||
log::error!("Error encrypting key space: {}", e);
|
log::error!("Error getting current space: {}", e);
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import a key space from a JSON string
|
|
||||||
fn decrypt_key_space(encrypted: &str, password: &str) -> bool {
|
fn decrypt_key_space(encrypted: &str, password: &str) -> bool {
|
||||||
match symmetric::deserialize_encrypted_space(encrypted)
|
match serde_json::from_str(encrypted) {
|
||||||
.and_then(|encrypted_space| symmetric::decrypt_key_space(&encrypted_space, password))
|
Ok(encrypted_space) => {
|
||||||
.and_then(|space| keypair::set_current_space(space))
|
match symmetric_impl::decrypt_key_space(&encrypted_space, password) {
|
||||||
{
|
Ok(space) => match keypair::set_current_space(space) {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error setting current space: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error decrypting key space: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error decrypting key space: {}", e);
|
log::error!("Error parsing encrypted space: {}", e);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,7 +258,7 @@ fn create_keypair(name: &str, password: &str) -> bool {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Auto-save the key space after creating a keypair
|
// Auto-save the key space after creating a keypair
|
||||||
auto_save_key_space(password)
|
auto_save_key_space(password)
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error creating keypair: {}", e);
|
log::error!("Error creating keypair: {}", e);
|
||||||
false
|
false
|
||||||
@ -176,14 +300,16 @@ fn sign(message: &str) -> String {
|
|||||||
|
|
||||||
fn verify(message: &str, signature: &str) -> bool {
|
fn verify(message: &str, signature: &str) -> bool {
|
||||||
let message_bytes = message.as_bytes();
|
let message_bytes = message.as_bytes();
|
||||||
match BASE64.decode(signature)
|
match BASE64.decode(signature) {
|
||||||
.map_err(|e| e.to_string())
|
Ok(signature_bytes) => match keypair::keypair_verify(message_bytes, &signature_bytes) {
|
||||||
.and_then(|sig_bytes| keypair::keypair_verify(message_bytes, &sig_bytes)
|
Ok(is_valid) => is_valid,
|
||||||
.map_err(|e| e.to_string()))
|
Err(e) => {
|
||||||
{
|
log::error!("Error verifying signature: {}", e);
|
||||||
Ok(is_valid) => is_valid,
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error verifying signature: {}", e);
|
log::error!("Error decoding signature: {}", e);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,44 +317,54 @@ fn verify(message: &str, signature: &str) -> bool {
|
|||||||
|
|
||||||
// Symmetric encryption
|
// Symmetric encryption
|
||||||
fn generate_key() -> String {
|
fn generate_key() -> String {
|
||||||
BASE64.encode(symmetric::generate_symmetric_key())
|
let key = symmetric_impl::generate_symmetric_key();
|
||||||
|
BASE64.encode(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encrypt(key: &str, message: &str) -> String {
|
fn encrypt(key: &str, message: &str) -> String {
|
||||||
match BASE64.decode(key)
|
match BASE64.decode(key) {
|
||||||
.map_err(|e| format!("Error decoding key: {}", e))
|
Ok(key_bytes) => {
|
||||||
.and_then(|key_bytes| {
|
let message_bytes = message.as_bytes();
|
||||||
symmetric::encrypt_symmetric(&key_bytes, message.as_bytes())
|
match symmetric_impl::encrypt_symmetric(&key_bytes, message_bytes) {
|
||||||
.map_err(|e| format!("Error encrypting message: {}", e))
|
Ok(ciphertext) => BASE64.encode(ciphertext),
|
||||||
})
|
Err(e) => {
|
||||||
{
|
log::error!("Error encrypting message: {}", e);
|
||||||
Ok(ciphertext) => BASE64.encode(ciphertext),
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("{}", e);
|
log::error!("Error decoding key: {}", e);
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrypt(key: &str, ciphertext: &str) -> String {
|
fn decrypt(key: &str, ciphertext: &str) -> String {
|
||||||
match BASE64.decode(key)
|
match BASE64.decode(key) {
|
||||||
.map_err(|e| format!("Error decoding key: {}", e))
|
Ok(key_bytes) => match BASE64.decode(ciphertext) {
|
||||||
.and_then(|key_bytes| {
|
Ok(ciphertext_bytes) => {
|
||||||
BASE64.decode(ciphertext)
|
match symmetric_impl::decrypt_symmetric(&key_bytes, &ciphertext_bytes) {
|
||||||
.map_err(|e| format!("Error decoding ciphertext: {}", e))
|
Ok(plaintext) => match String::from_utf8(plaintext) {
|
||||||
.and_then(|cipher_bytes| {
|
Ok(text) => text,
|
||||||
symmetric::decrypt_symmetric(&key_bytes, &cipher_bytes)
|
Err(e) => {
|
||||||
.map_err(|e| format!("Error decrypting ciphertext: {}", e))
|
log::error!("Error converting plaintext to string: {}", e);
|
||||||
})
|
String::new()
|
||||||
})
|
}
|
||||||
.and_then(|plaintext| {
|
},
|
||||||
String::from_utf8(plaintext)
|
Err(e) => {
|
||||||
.map_err(|e| format!("Error converting plaintext to string: {}", e))
|
log::error!("Error decrypting ciphertext: {}", e);
|
||||||
})
|
String::new()
|
||||||
{
|
}
|
||||||
Ok(text) => text,
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error decoding ciphertext: {}", e);
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("{}", e);
|
log::error!("Error decoding key: {}", e);
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,9 +372,9 @@ fn decrypt(key: &str, ciphertext: &str) -> String {
|
|||||||
|
|
||||||
// Ethereum operations
|
// Ethereum operations
|
||||||
|
|
||||||
// Create a network-independent Ethereum wallet
|
// Gnosis Chain operations
|
||||||
fn create_ethereum_wallet() -> bool {
|
fn create_ethereum_wallet() -> bool {
|
||||||
match ethereum::create_ethereum_wallet() {
|
match ethereum::create_ethereum_wallet_for_network(ethereum::networks::gnosis()) {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error creating Ethereum wallet: {}", e);
|
log::error!("Error creating Ethereum wallet: {}", e);
|
||||||
@ -247,9 +383,8 @@ fn create_ethereum_wallet() -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the Ethereum wallet address (same for all networks)
|
|
||||||
fn get_ethereum_address() -> String {
|
fn get_ethereum_address() -> String {
|
||||||
match ethereum::get_current_ethereum_wallet() {
|
match ethereum::get_current_ethereum_wallet_for_network("Gnosis") {
|
||||||
Ok(wallet) => wallet.address_string(),
|
Ok(wallet) => wallet.address_string(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error getting Ethereum address: {}", e);
|
log::error!("Error getting Ethereum address: {}", e);
|
||||||
@ -258,76 +393,116 @@ fn get_ethereum_address() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a wallet from a name
|
// Peaq network operations
|
||||||
fn create_ethereum_wallet_from_name(name: &str) -> bool {
|
fn create_peaq_wallet() -> bool {
|
||||||
match ethereum::create_ethereum_wallet_from_name(name) {
|
match ethereum::create_peaq_wallet() {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error creating Ethereum wallet from name: {}", e);
|
log::error!("Error creating Peaq wallet: {}", e);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a wallet from a private key
|
fn get_peaq_address() -> String {
|
||||||
fn create_ethereum_wallet_from_private_key(private_key: &str) -> bool {
|
match ethereum::get_current_peaq_wallet() {
|
||||||
match ethereum::create_ethereum_wallet_from_private_key(private_key) {
|
Ok(wallet) => wallet.address_string(),
|
||||||
Ok(_) => true,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error creating Ethereum wallet from private key: {}", e);
|
log::error!("Error getting Peaq address: {}", e);
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear all Ethereum wallets
|
|
||||||
fn clear_ethereum_wallets() -> bool {
|
|
||||||
ethereum::clear_ethereum_wallets();
|
|
||||||
true // Always return true since the operation doesn't have a failure mode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Network registry functions
|
|
||||||
|
|
||||||
// Register a new network
|
|
||||||
fn register_network(name: &str, chain_id: i64, rpc_url: &str, explorer_url: &str, token_symbol: &str, decimals: i64) -> bool {
|
|
||||||
ethereum::register_network(
|
|
||||||
name,
|
|
||||||
chain_id as u64,
|
|
||||||
rpc_url,
|
|
||||||
explorer_url,
|
|
||||||
token_symbol,
|
|
||||||
decimals as u8
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a network
|
|
||||||
fn remove_network(name: &str) -> bool {
|
|
||||||
ethereum::remove_network(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List supported networks
|
|
||||||
fn list_supported_networks() -> rhai::Array {
|
|
||||||
let mut arr = rhai::Array::new();
|
|
||||||
for name in ethereum::list_network_names() {
|
|
||||||
arr.push(Dynamic::from(name.to_lowercase()));
|
|
||||||
}
|
|
||||||
arr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a provider for a specific network
|
|
||||||
fn create_provider(network_name: &str) -> String {
|
|
||||||
match ethereum::create_provider(network_name) {
|
|
||||||
Ok(_) => network_name.to_string(),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Error creating provider: {}", e);
|
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agung testnet operations
|
||||||
|
fn create_agung_wallet() -> bool {
|
||||||
|
match ethereum::create_agung_wallet() {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error creating Agung wallet: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_agung_address() -> String {
|
||||||
|
match ethereum::get_current_agung_wallet() {
|
||||||
|
Ok(wallet) => wallet.address_string(),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error getting Agung address: {}", e);
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic network operations
|
||||||
|
fn create_wallet_for_network(network_name: &str) -> bool {
|
||||||
|
let network = match ethereum::networks::get_network_by_name(network_name) {
|
||||||
|
Some(network) => network,
|
||||||
|
None => {
|
||||||
|
log::error!("Unknown network: {}", network_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match ethereum::create_ethereum_wallet_for_network(network) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error creating wallet for network {}: {}", network_name, e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get wallet address for a specific network
|
||||||
|
fn get_wallet_address_for_network(network_name: &str) -> String {
|
||||||
|
let network_name_proper = match ethereum::networks::get_proper_network_name(network_name) {
|
||||||
|
Some(name) => name,
|
||||||
|
None => {
|
||||||
|
log::error!("Unknown network: {}", network_name);
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) {
|
||||||
|
Ok(wallet) => wallet.address_string(),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Error getting wallet address for network {}: {}",
|
||||||
|
network_name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear wallets for a specific network
|
||||||
|
fn clear_wallets_for_network(network_name: &str) -> bool {
|
||||||
|
let network_name_proper = match ethereum::networks::get_proper_network_name(network_name) {
|
||||||
|
Some(name) => name,
|
||||||
|
None => {
|
||||||
|
log::error!("Unknown network: {}", network_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ethereum::clear_ethereum_wallets_for_network(network_name_proper);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// List supported networks
|
||||||
|
fn list_supported_networks() -> rhai::Array {
|
||||||
|
let mut arr = rhai::Array::new();
|
||||||
|
for name in ethereum::networks::list_network_names() {
|
||||||
|
arr.push(Dynamic::from(name.to_lowercase()));
|
||||||
|
}
|
||||||
|
arr
|
||||||
|
}
|
||||||
|
|
||||||
// Get network token symbol
|
// Get network token symbol
|
||||||
fn get_network_token_symbol(network_name: &str) -> String {
|
fn get_network_token_symbol(network_name: &str) -> String {
|
||||||
match ethereum::get_network_by_name(network_name) {
|
match ethereum::networks::get_network_by_name(network_name) {
|
||||||
Some(network) => network.token_symbol,
|
Some(network) => network.token_symbol,
|
||||||
None => {
|
None => {
|
||||||
log::error!("Unknown network: {}", network_name);
|
log::error!("Unknown network: {}", network_name);
|
||||||
@ -338,7 +513,7 @@ fn get_network_token_symbol(network_name: &str) -> String {
|
|||||||
|
|
||||||
// Get network explorer URL
|
// Get network explorer URL
|
||||||
fn get_network_explorer_url(network_name: &str) -> String {
|
fn get_network_explorer_url(network_name: &str) -> String {
|
||||||
match ethereum::get_network_by_name(network_name) {
|
match ethereum::networks::get_network_by_name(network_name) {
|
||||||
Some(network) => network.explorer_url,
|
Some(network) => network.explorer_url,
|
||||||
None => {
|
None => {
|
||||||
log::error!("Unknown network: {}", network_name);
|
log::error!("Unknown network: {}", network_name);
|
||||||
@ -347,8 +522,63 @@ fn get_network_explorer_url(network_name: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a wallet from a private key for a specific network
|
||||||
|
fn create_wallet_from_private_key_for_network(private_key: &str, network_name: &str) -> bool {
|
||||||
|
let network = match ethereum::networks::get_network_by_name(network_name) {
|
||||||
|
Some(network) => network,
|
||||||
|
None => {
|
||||||
|
log::error!("Unknown network: {}", network_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match ethereum::create_ethereum_wallet_from_private_key_for_network(private_key, network) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Error creating wallet from private key for network {}: {}",
|
||||||
|
network_name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a provider for the Agung network
|
||||||
|
fn create_agung_provider() -> String {
|
||||||
|
match ethereum::create_agung_provider() {
|
||||||
|
Ok(provider) => {
|
||||||
|
// Generate a unique ID for the provider
|
||||||
|
let id = format!("provider_{}", uuid::Uuid::new_v4());
|
||||||
|
|
||||||
|
// Store the provider in the registry
|
||||||
|
if let Ok(mut providers) = PROVIDERS.lock() {
|
||||||
|
providers.insert(id.clone(), provider);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::error!("Failed to acquire provider registry lock");
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error creating Agung provider: {}", e);
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the balance of an address on a specific network
|
// Get the balance of an address on a specific network
|
||||||
fn get_balance(network_name: &str, address: &str) -> String {
|
fn get_balance(network_name: &str, address: &str) -> String {
|
||||||
|
// Get the runtime
|
||||||
|
let rt = match RUNTIME.lock() {
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to acquire runtime lock: {}", e);
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Parse the address
|
// Parse the address
|
||||||
let addr = match Address::from_str(address) {
|
let addr = match Address::from_str(address) {
|
||||||
Ok(addr) => addr,
|
Ok(addr) => addr,
|
||||||
@ -358,15 +588,36 @@ fn get_balance(network_name: &str, address: &str) -> String {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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
|
// Execute the balance query in a blocking manner
|
||||||
match run_async(ethereum::get_balance(network_name, addr)) {
|
match rt.block_on(async { ethereum::get_balance(&provider, addr).await }) {
|
||||||
Ok(balance) => {
|
Ok(balance) => balance.to_string(),
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to get balance: {}", e);
|
log::error!("Failed to get balance: {}", e);
|
||||||
String::new()
|
String::new()
|
||||||
@ -374,8 +625,17 @@ fn get_balance(network_name: &str, address: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send ETH from one address to another
|
// Send ETH from one address to another using the blocking approach
|
||||||
fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String {
|
fn send_eth(wallet_network: &str, to_address: &str, amount_str: &str) -> String {
|
||||||
|
// Get the runtime
|
||||||
|
let rt = match RUNTIME.lock() {
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to acquire runtime lock: {}", e);
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Parse the address
|
// Parse the address
|
||||||
let to_addr = match Address::from_str(to_address) {
|
let to_addr = match Address::from_str(to_address) {
|
||||||
Ok(addr) => addr,
|
Ok(addr) => addr,
|
||||||
@ -394,8 +654,17 @@ fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get the proper network name
|
||||||
|
let network_name_proper = match ethereum::networks::get_proper_network_name(wallet_network) {
|
||||||
|
Some(name) => name,
|
||||||
|
None => {
|
||||||
|
log::error!("Unknown network: {}", wallet_network);
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Get the wallet
|
// Get the wallet
|
||||||
let wallet = match ethereum::get_current_ethereum_wallet() {
|
let wallet = match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) {
|
||||||
Ok(w) => w,
|
Ok(w) => w,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to get wallet: {}", e);
|
log::error!("Failed to get wallet: {}", e);
|
||||||
@ -403,8 +672,17 @@ fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create a provider
|
||||||
|
let provider = match ethereum::create_provider(&wallet.network) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to create provider: {}", e);
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Execute the transaction in a blocking manner
|
// Execute the transaction in a blocking manner
|
||||||
match run_async(ethereum::send_eth(&wallet, network_name, to_addr, amount)) {
|
match rt.block_on(async { ethereum::send_eth(&wallet, &provider, to_addr, amount).await }) {
|
||||||
Ok(tx_hash) => format!("{:?}", tx_hash),
|
Ok(tx_hash) => format!("{:?}", tx_hash),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Transaction failed: {}", e);
|
log::error!("Transaction failed: {}", e);
|
||||||
@ -418,7 +696,7 @@ fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String {
|
|||||||
// Load a contract ABI from a JSON string and create a contract instance
|
// Load a contract ABI from a JSON string and create a contract instance
|
||||||
fn load_contract_abi(network_name: &str, address: &str, abi_json: &str) -> String {
|
fn load_contract_abi(network_name: &str, address: &str, abi_json: &str) -> String {
|
||||||
// Get the network
|
// Get the network
|
||||||
let network = match ethereum::get_network_by_name(network_name) {
|
let network = match ethereum::networks::get_network_by_name(network_name) {
|
||||||
Some(network) => network,
|
Some(network) => network,
|
||||||
None => {
|
None => {
|
||||||
log::error!("Unknown network: {}", network_name);
|
log::error!("Unknown network: {}", network_name);
|
||||||
@ -446,7 +724,7 @@ fn load_contract_abi(network_name: &str, address: &str, abi_json: &str) -> Strin
|
|||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error creating contract: {}", e);
|
log::error!("Error creating contract: {}", e);
|
||||||
String::new()
|
String::new()
|
||||||
@ -466,6 +744,8 @@ fn load_contract_abi_from_file(network_name: &str, address: &str, file_path: &st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the utility functions from the ethereum module
|
||||||
|
|
||||||
// Call a read-only function on a contract (no arguments version)
|
// Call a read-only function on a contract (no arguments version)
|
||||||
fn call_contract_read_no_args(contract_json: &str, function_name: &str) -> Dynamic {
|
fn call_contract_read_no_args(contract_json: &str, function_name: &str) -> Dynamic {
|
||||||
call_contract_read(contract_json, function_name, rhai::Array::new())
|
call_contract_read(contract_json, function_name, rhai::Array::new())
|
||||||
@ -483,7 +763,7 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Prepare the arguments
|
// Prepare the arguments
|
||||||
let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) {
|
let tokens = match ethereum::prepare_function_arguments(&contract.abi, function_name, &args) {
|
||||||
Ok(tokens) => tokens,
|
Ok(tokens) => tokens,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error preparing arguments: {}", e);
|
log::error!("Error preparing arguments: {}", e);
|
||||||
@ -491,8 +771,17 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get the runtime
|
||||||
|
let rt = match RUNTIME.lock() {
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to acquire runtime lock: {}", e);
|
||||||
|
return Dynamic::UNIT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Create a provider
|
// Create a provider
|
||||||
let provider = match ethereum::create_provider(&contract.network.name) {
|
let provider = match ethereum::create_provider(&contract.network) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to create provider: {}", e);
|
log::error!("Failed to create provider: {}", e);
|
||||||
@ -501,8 +790,10 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Execute the call in a blocking manner
|
// Execute the call in a blocking manner
|
||||||
match run_async(ethereum::call_read_function(&contract, &provider, function_name, tokens)) {
|
match rt.block_on(async {
|
||||||
Ok(result) => ethereum::token_to_dynamic(&result),
|
ethereum::call_read_function(&contract, &provider, function_name, tokens).await
|
||||||
|
}) {
|
||||||
|
Ok(result) => ethereum::convert_token_to_rhai(&result),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to call contract function: {}", e);
|
log::error!("Failed to call contract function: {}", e);
|
||||||
Dynamic::UNIT
|
Dynamic::UNIT
|
||||||
@ -527,7 +818,7 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Prepare the arguments
|
// Prepare the arguments
|
||||||
let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) {
|
let tokens = match ethereum::prepare_function_arguments(&contract.abi, function_name, &args) {
|
||||||
Ok(tokens) => tokens,
|
Ok(tokens) => tokens,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error preparing arguments: {}", e);
|
log::error!("Error preparing arguments: {}", e);
|
||||||
@ -535,8 +826,18 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get the runtime
|
||||||
|
let rt = match RUNTIME.lock() {
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to acquire runtime lock: {}", e);
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Get the wallet
|
// Get the wallet
|
||||||
let wallet = match ethereum::get_current_ethereum_wallet() {
|
let network_name_proper = contract.network.name.as_str();
|
||||||
|
let wallet = match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) {
|
||||||
Ok(w) => w,
|
Ok(w) => w,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to get wallet: {}", e);
|
log::error!("Failed to get wallet: {}", e);
|
||||||
@ -545,7 +846,7 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create a provider
|
// Create a provider
|
||||||
let provider = match ethereum::create_provider(&contract.network.name) {
|
let provider = match ethereum::create_provider(&contract.network) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to create provider: {}", e);
|
log::error!("Failed to create provider: {}", e);
|
||||||
@ -554,7 +855,9 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Execute the transaction in a blocking manner
|
// Execute the transaction in a blocking manner
|
||||||
match run_async(ethereum::call_write_function(&contract, &wallet, &provider, function_name, tokens)) {
|
match rt.block_on(async {
|
||||||
|
ethereum::call_write_function(&contract, &wallet, &provider, function_name, tokens).await
|
||||||
|
}) {
|
||||||
Ok(tx_hash) => format!("{:?}", tx_hash),
|
Ok(tx_hash) => format!("{:?}", tx_hash),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Log the error details for debugging
|
// Log the error details for debugging
|
||||||
@ -592,32 +895,47 @@ pub fn register_crypto_module(engine: &mut Engine) -> Result<(), Box<EvalAltResu
|
|||||||
engine.register_fn("encrypt", encrypt);
|
engine.register_fn("encrypt", encrypt);
|
||||||
engine.register_fn("decrypt", decrypt);
|
engine.register_fn("decrypt", decrypt);
|
||||||
|
|
||||||
// Register Ethereum wallet functions
|
// Register Ethereum functions (Gnosis Chain)
|
||||||
engine.register_fn("create_ethereum_wallet", create_ethereum_wallet);
|
engine.register_fn("create_ethereum_wallet", create_ethereum_wallet);
|
||||||
engine.register_fn("get_ethereum_address", get_ethereum_address);
|
engine.register_fn("get_ethereum_address", get_ethereum_address);
|
||||||
engine.register_fn("create_ethereum_wallet_from_name", create_ethereum_wallet_from_name);
|
|
||||||
engine.register_fn("create_ethereum_wallet_from_private_key", create_ethereum_wallet_from_private_key);
|
|
||||||
engine.register_fn("clear_ethereum_wallets", clear_ethereum_wallets);
|
|
||||||
|
|
||||||
// Register network registry functions
|
// Register Peaq network functions
|
||||||
engine.register_fn("register_network", register_network);
|
engine.register_fn("create_peaq_wallet", create_peaq_wallet);
|
||||||
engine.register_fn("remove_network", remove_network);
|
engine.register_fn("get_peaq_address", get_peaq_address);
|
||||||
|
|
||||||
|
// Register Agung testnet functions
|
||||||
|
engine.register_fn("create_agung_wallet", create_agung_wallet);
|
||||||
|
engine.register_fn("get_agung_address", get_agung_address);
|
||||||
|
|
||||||
|
// Register generic network functions
|
||||||
|
engine.register_fn("create_wallet_for_network", create_wallet_for_network);
|
||||||
|
engine.register_fn(
|
||||||
|
"get_wallet_address_for_network",
|
||||||
|
get_wallet_address_for_network,
|
||||||
|
);
|
||||||
|
engine.register_fn("clear_wallets_for_network", clear_wallets_for_network);
|
||||||
engine.register_fn("list_supported_networks", list_supported_networks);
|
engine.register_fn("list_supported_networks", list_supported_networks);
|
||||||
engine.register_fn("get_network_token_symbol", get_network_token_symbol);
|
engine.register_fn("get_network_token_symbol", get_network_token_symbol);
|
||||||
engine.register_fn("get_network_explorer_url", get_network_explorer_url);
|
engine.register_fn("get_network_explorer_url", get_network_explorer_url);
|
||||||
|
|
||||||
// Register provider functions
|
// Register new Ethereum functions for wallet creation from private key and transactions
|
||||||
engine.register_fn("create_provider", create_provider);
|
engine.register_fn(
|
||||||
|
"create_wallet_from_private_key_for_network",
|
||||||
// Register transaction functions
|
create_wallet_from_private_key_for_network,
|
||||||
|
);
|
||||||
|
engine.register_fn("create_agung_provider", create_agung_provider);
|
||||||
engine.register_fn("send_eth", send_eth);
|
engine.register_fn("send_eth", send_eth);
|
||||||
engine.register_fn("get_balance", get_balance);
|
engine.register_fn("get_balance", get_balance);
|
||||||
|
|
||||||
// Register smart contract functions
|
// Register smart contract functions
|
||||||
engine.register_fn("load_contract_abi", load_contract_abi);
|
engine.register_fn("load_contract_abi", load_contract_abi);
|
||||||
engine.register_fn("load_contract_abi_from_file", load_contract_abi_from_file);
|
engine.register_fn("load_contract_abi_from_file", load_contract_abi_from_file);
|
||||||
|
|
||||||
|
// Register the read function with different arities
|
||||||
engine.register_fn("call_contract_read", call_contract_read_no_args);
|
engine.register_fn("call_contract_read", call_contract_read_no_args);
|
||||||
engine.register_fn("call_contract_read", call_contract_read);
|
engine.register_fn("call_contract_read", call_contract_read);
|
||||||
|
|
||||||
|
// Register the write function with different arities
|
||||||
engine.register_fn("call_contract_write", call_contract_write_no_args);
|
engine.register_fn("call_contract_write", call_contract_write_no_args);
|
||||||
engine.register_fn("call_contract_write", call_contract_write);
|
engine.register_fn("call_contract_write", call_contract_write);
|
||||||
|
|
||||||
|
@ -157,4 +157,4 @@ The module supports multiple Ethereum networks, including:
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
For examples of how to use the Hero Vault module, see the `examples/vault` directory.
|
For examples of how to use the Hero Vault module, see the `examples/hero_vault` directory.
|
||||||
|
@ -48,10 +48,6 @@ pub enum CryptoError {
|
|||||||
/// Smart contract error
|
/// Smart contract error
|
||||||
#[error("Smart contract error: {0}")]
|
#[error("Smart contract error: {0}")]
|
||||||
ContractError(String),
|
ContractError(String),
|
||||||
|
|
||||||
/// Storage error
|
|
||||||
#[error("Storage error: {0}")]
|
|
||||||
StorageError(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert CryptoError to SAL's Error type
|
/// Convert CryptoError to SAL's Error type
|
||||||
|
@ -21,25 +21,25 @@ The Ethereum module is organized into several components:
|
|||||||
The module provides functionality for creating and managing Ethereum wallets:
|
The module provides functionality for creating and managing Ethereum wallets:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Create a network-independent Ethereum wallet
|
// Create a new Ethereum wallet for a specific network
|
||||||
let wallet = create_ethereum_wallet()?;
|
let wallet = create_ethereum_wallet_for_network("Ethereum")?;
|
||||||
|
|
||||||
|
// Create a wallet for specific networks
|
||||||
|
let peaq_wallet = create_peaq_wallet()?;
|
||||||
|
let agung_wallet = create_agung_wallet()?;
|
||||||
|
|
||||||
// Create a wallet with a specific name
|
// Create a wallet with a specific name
|
||||||
let named_wallet = create_ethereum_wallet_from_name("my_wallet")?;
|
let named_wallet = create_ethereum_wallet_from_name_for_network("my_wallet", "Gnosis")?;
|
||||||
|
|
||||||
// Create a wallet from a private key
|
// Create a wallet from a private key
|
||||||
let imported_wallet = create_ethereum_wallet_from_private_key("0x...")?;
|
let imported_wallet = create_ethereum_wallet_from_private_key("0x...")?;
|
||||||
|
|
||||||
// Get the current wallet
|
// Get the current wallet for a network
|
||||||
let current_wallet = get_current_ethereum_wallet()?;
|
let current_wallet = get_current_ethereum_wallet_for_network("Ethereum")?;
|
||||||
|
|
||||||
// Clear wallets
|
// Clear wallets
|
||||||
clear_ethereum_wallets()?;
|
clear_ethereum_wallets()?;
|
||||||
|
clear_ethereum_wallets_for_network("Gnosis")?;
|
||||||
// Legacy functions for backward compatibility
|
|
||||||
let wallet = create_ethereum_wallet_for_network(network)?;
|
|
||||||
let peaq_wallet = create_peaq_wallet()?;
|
|
||||||
let agung_wallet = create_agung_wallet()?;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Network Management
|
### Network Management
|
||||||
@ -47,12 +47,6 @@ let agung_wallet = create_agung_wallet()?;
|
|||||||
The module supports multiple Ethereum networks and provides functionality for managing network configurations:
|
The module supports multiple Ethereum networks and provides functionality for managing network configurations:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Register a new network
|
|
||||||
register_network("Arbitrum", 42161, "https://arb1.arbitrum.io/rpc", "https://arbiscan.io", "ETH", 18);
|
|
||||||
|
|
||||||
// Remove a network
|
|
||||||
remove_network("Arbitrum");
|
|
||||||
|
|
||||||
// Get a network configuration by name
|
// Get a network configuration by name
|
||||||
let network = get_network_by_name("Ethereum")?;
|
let network = get_network_by_name("Ethereum")?;
|
||||||
|
|
||||||
@ -74,10 +68,7 @@ The module provides functionality for creating and managing Ethereum providers:
|
|||||||
// Create a provider for a specific network
|
// Create a provider for a specific network
|
||||||
let provider = create_provider("Ethereum")?;
|
let provider = create_provider("Ethereum")?;
|
||||||
|
|
||||||
// Create a provider from a network configuration
|
// Create providers for specific networks
|
||||||
let provider = create_provider_from_config(&network)?;
|
|
||||||
|
|
||||||
// Legacy functions for backward compatibility
|
|
||||||
let gnosis_provider = create_gnosis_provider()?;
|
let gnosis_provider = create_gnosis_provider()?;
|
||||||
let peaq_provider = create_peaq_provider()?;
|
let peaq_provider = create_peaq_provider()?;
|
||||||
let agung_provider = create_agung_provider()?;
|
let agung_provider = create_agung_provider()?;
|
||||||
@ -88,20 +79,14 @@ let agung_provider = create_agung_provider()?;
|
|||||||
The module provides functionality for managing Ethereum transactions:
|
The module provides functionality for managing Ethereum transactions:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Get the balance of an address on a specific network
|
// Get the balance of an address
|
||||||
let balance = get_balance("Ethereum", address).await?;
|
let balance = get_balance("Ethereum", "0x...")?;
|
||||||
|
|
||||||
// Get the balance using a provider
|
// Send ETH to an address
|
||||||
let balance = get_balance_with_provider(&provider, address).await?;
|
let tx_hash = send_eth("Ethereum", "0x...", "1000000000000000")?;
|
||||||
|
|
||||||
// Send ETH to an address on a specific network
|
|
||||||
let tx_hash = send_eth(&wallet, "Ethereum", to_address, amount).await?;
|
|
||||||
|
|
||||||
// Legacy function for backward compatibility
|
|
||||||
let tx_hash = send_eth_with_provider(&wallet, &provider, to_address, amount).await?;
|
|
||||||
|
|
||||||
// Format a balance for display
|
// Format a balance for display
|
||||||
let formatted = format_balance(balance, &network);
|
let formatted = format_balance(balance, 18)?; // Convert wei to ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
### Smart Contract Interactions
|
### Smart Contract Interactions
|
||||||
@ -113,16 +98,16 @@ The module provides functionality for interacting with smart contracts:
|
|||||||
let abi = load_abi_from_json(json_string)?;
|
let abi = load_abi_from_json(json_string)?;
|
||||||
|
|
||||||
// Create a contract instance
|
// Create a contract instance
|
||||||
let contract = Contract::new(address, abi, network);
|
let contract = Contract::new(provider, "0x...", abi)?;
|
||||||
|
|
||||||
// Call a read-only function
|
// Call a read-only function
|
||||||
let result = call_read_function(&contract, &provider, "balanceOf", tokens).await?;
|
let result = call_read_function(contract, "balanceOf", vec!["0x..."])?;
|
||||||
|
|
||||||
// Call a write function
|
// Call a write function
|
||||||
let tx_hash = call_write_function(&contract, &wallet, &provider, "transfer", tokens).await?;
|
let tx_hash = call_write_function(contract, "transfer", vec!["0x...", "1000"])?;
|
||||||
|
|
||||||
// Estimate gas for a function call
|
// Estimate gas for a function call
|
||||||
let gas = estimate_gas(&contract, &provider, "transfer", tokens).await?;
|
let gas = estimate_gas(contract, "transfer", vec!["0x...", "1000"])?;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Contract Utilities
|
### Contract Utilities
|
||||||
@ -134,49 +119,29 @@ The module provides utilities for working with contract function arguments and r
|
|||||||
let token = convert_rhai_to_token(value)?;
|
let token = convert_rhai_to_token(value)?;
|
||||||
|
|
||||||
// Prepare function arguments
|
// Prepare function arguments
|
||||||
let args = prepare_function_arguments(&abi, function_name, &args)?;
|
let args = prepare_function_arguments(function, vec![arg1, arg2])?;
|
||||||
|
|
||||||
// Convert Ethereum tokens to Rhai values
|
// Convert Ethereum tokens to Rhai values
|
||||||
let rhai_value = convert_token_to_rhai(&token)?;
|
let rhai_value = convert_token_to_rhai(token)?;
|
||||||
|
|
||||||
// Convert a token to a dynamic value
|
// Convert a token to a dynamic value
|
||||||
let dynamic = token_to_dynamic(&token)?;
|
let dynamic = token_to_dynamic(token)?;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Network Registry
|
## Supported Networks
|
||||||
|
|
||||||
The module now includes a centralized network registry that allows for dynamic management of EVM-compatible networks:
|
The module supports multiple Ethereum networks, including:
|
||||||
|
|
||||||
```rust
|
- Gnosis Chain
|
||||||
// Built-in networks
|
- Peaq Network
|
||||||
- Gnosis Chain (chain_id: 100, token: xDAI)
|
- Agung Network
|
||||||
- Peaq Network (chain_id: 3338, token: PEAQ)
|
|
||||||
- Agung Network (chain_id: 9990, token: AGNG)
|
|
||||||
|
|
||||||
// Register a custom network at runtime
|
Each network has its own configuration, including:
|
||||||
register_network(
|
|
||||||
"Polygon",
|
|
||||||
137,
|
|
||||||
"https://polygon-rpc.com",
|
|
||||||
"https://polygonscan.com",
|
|
||||||
"MATIC",
|
|
||||||
18
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wallet Address Consistency
|
- RPC URL
|
||||||
|
- Chain ID
|
||||||
In the new design, Ethereum wallet addresses are consistent across all EVM-compatible networks. This reflects how Ethereum addresses work in reality - the same private key generates the same address on all EVM chains.
|
- Explorer URL
|
||||||
|
- Native currency symbol and decimals
|
||||||
```rust
|
|
||||||
// Create a wallet once
|
|
||||||
let wallet = create_ethereum_wallet()?;
|
|
||||||
|
|
||||||
// Use the same wallet address on any network
|
|
||||||
let eth_balance = get_balance("Ethereum", wallet.address).await?;
|
|
||||||
let polygon_balance = get_balance("Polygon", wallet.address).await?;
|
|
||||||
let gnosis_balance = get_balance("Gnosis", wallet.address).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
@ -184,42 +149,12 @@ The module uses the `CryptoError` type for handling errors that can occur during
|
|||||||
|
|
||||||
- `InvalidAddress` - Invalid Ethereum address format
|
- `InvalidAddress` - Invalid Ethereum address format
|
||||||
- `ContractError` - Smart contract interaction error
|
- `ContractError` - Smart contract interaction error
|
||||||
- `NoKeypairSelected` - No keypair selected for wallet creation
|
|
||||||
- `InvalidKeyLength` - Invalid private key length
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
For examples of how to use the Ethereum module, see the `examples/vault` directory, particularly:
|
For examples of how to use the Ethereum module, see the `examples/hero_vault` directory, particularly:
|
||||||
|
|
||||||
- `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts
|
- `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts
|
||||||
- `agung_simple_transfer.rhai` - Shows how to perform a simple ETH transfer on the Agung network
|
- `agung_simple_transfer.rhai` - Shows how to perform a simple ETH transfer on the Agung network
|
||||||
- `agung_send_transaction.rhai` - Demonstrates sending transactions on the Agung network
|
- `agung_send_transaction.rhai` - Demonstrates sending transactions on the Agung network
|
||||||
- `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung
|
- `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung
|
||||||
|
|
||||||
## Adding a New Network
|
|
||||||
|
|
||||||
With the new design, adding a new network is as simple as registering it with the network registry:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// In Rust
|
|
||||||
ethereum::register_network(
|
|
||||||
"Optimism",
|
|
||||||
10,
|
|
||||||
"https://mainnet.optimism.io",
|
|
||||||
"https://optimistic.etherscan.io",
|
|
||||||
"ETH",
|
|
||||||
18
|
|
||||||
);
|
|
||||||
|
|
||||||
// In Rhai
|
|
||||||
register_network(
|
|
||||||
"Optimism",
|
|
||||||
10,
|
|
||||||
"https://mainnet.optimism.io",
|
|
||||||
"https://optimistic.etherscan.io",
|
|
||||||
"ETH",
|
|
||||||
18
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
No code changes are required to add support for new networks!
|
|
||||||
|
@ -42,7 +42,7 @@ impl Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an ethers Contract instance for interaction.
|
/// Creates an ethers Contract instance for interaction.
|
||||||
pub fn create_ethers_contract(&self, provider: Provider<Http>) -> Result<ethers::contract::Contract<ethers::providers::Provider<Http>>, CryptoError> {
|
pub fn create_ethers_contract(&self, provider: Provider<Http>, _wallet: Option<&EthereumWallet>) -> Result<ethers::contract::Contract<ethers::providers::Provider<Http>>, CryptoError> {
|
||||||
let contract = ethers::contract::Contract::new(
|
let contract = ethers::contract::Contract::new(
|
||||||
self.address,
|
self.address,
|
||||||
self.abi.clone(),
|
self.abi.clone(),
|
||||||
@ -65,9 +65,9 @@ pub async fn call_read_function(
|
|||||||
provider: &Provider<Http>,
|
provider: &Provider<Http>,
|
||||||
function_name: &str,
|
function_name: &str,
|
||||||
args: Vec<Token>,
|
args: Vec<Token>,
|
||||||
) -> Result<Token, CryptoError> {
|
) -> Result<Vec<Token>, CryptoError> {
|
||||||
// Create the ethers contract (not used directly but kept for future extensions)
|
// Create the ethers contract (not used directly but kept for future extensions)
|
||||||
let _ethers_contract = contract.create_ethers_contract(provider.clone())?;
|
let _ethers_contract = contract.create_ethers_contract(provider.clone(), None)?;
|
||||||
|
|
||||||
// Get the function from the ABI
|
// Get the function from the ABI
|
||||||
let function = contract.abi.function(function_name)
|
let function = contract.abi.function(function_name)
|
||||||
@ -89,12 +89,7 @@ pub async fn call_read_function(
|
|||||||
let decoded = function.decode_output(&result)
|
let decoded = function.decode_output(&result)
|
||||||
.map_err(|e| CryptoError::ContractError(format!("Failed to decode function output: {}", e)))?;
|
.map_err(|e| CryptoError::ContractError(format!("Failed to decode function output: {}", e)))?;
|
||||||
|
|
||||||
// Return the first token if there's only one, otherwise return a tuple
|
Ok(decoded)
|
||||||
if decoded.len() == 1 {
|
|
||||||
Ok(decoded[0].clone())
|
|
||||||
} else {
|
|
||||||
Ok(Token::Tuple(decoded))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes a state-changing function on a contract.
|
/// Executes a state-changing function on a contract.
|
||||||
@ -105,11 +100,10 @@ pub async fn call_write_function(
|
|||||||
function_name: &str,
|
function_name: &str,
|
||||||
args: Vec<Token>,
|
args: Vec<Token>,
|
||||||
) -> Result<H256, CryptoError> {
|
) -> Result<H256, CryptoError> {
|
||||||
// Create a client with the wallet configured for this network
|
// Create a client with the wallet
|
||||||
let network_wallet = wallet.for_network(&contract.network);
|
|
||||||
let client = SignerMiddleware::new(
|
let client = SignerMiddleware::new(
|
||||||
provider.clone(),
|
provider.clone(),
|
||||||
network_wallet,
|
wallet.wallet.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get the function from the ABI
|
// Get the function from the ABI
|
||||||
@ -120,28 +114,23 @@ pub async fn call_write_function(
|
|||||||
let call_data = function.encode_input(&args)
|
let call_data = function.encode_input(&args)
|
||||||
.map_err(|e| CryptoError::ContractError(format!("Failed to encode function call: {}", e)))?;
|
.map_err(|e| CryptoError::ContractError(format!("Failed to encode function call: {}", e)))?;
|
||||||
|
|
||||||
// Estimate gas
|
|
||||||
let gas = estimate_gas(contract, wallet, provider, function_name, args.clone()).await?;
|
|
||||||
log::info!("Estimated gas: {:?}", gas);
|
|
||||||
|
|
||||||
// Create the transaction request with gas limit
|
// Create the transaction request with gas limit
|
||||||
let tx = TransactionRequest::new()
|
let tx = TransactionRequest::new()
|
||||||
.to(contract.address)
|
.to(contract.address)
|
||||||
.data(call_data)
|
.data(call_data)
|
||||||
.gas(gas); // Set a reasonable gas limit
|
.gas(U256::from(300000)); // Set a reasonable gas limit
|
||||||
|
|
||||||
// Send the transaction using the client directly
|
// Send the transaction using the client directly
|
||||||
log::info!("Sending transaction to contract at {}", contract.address);
|
log::info!("Sending transaction to contract at {}", contract.address);
|
||||||
log::info!("Function: {}", function_name);
|
log::info!("Function: {}, Args: {:?}", function_name, args);
|
||||||
|
|
||||||
// Log detailed information about the transaction
|
// Log detailed information about the transaction
|
||||||
log::debug!("Sending transaction to contract at {}", contract.address);
|
log::debug!("Sending transaction to contract at {}", contract.address);
|
||||||
log::debug!("Function: {}, Args: {:?}", function_name, args);
|
log::debug!("Function: {}, Args: {:?}", function_name, args);
|
||||||
log::debug!("From address: {}", wallet.address);
|
log::debug!("From address: {}", wallet.address);
|
||||||
log::debug!("Gas limit: {:?}", tx.gas);
|
log::debug!("Gas limit: {:?}", tx.gas);
|
||||||
log::debug!("Network: {}", contract.network.name);
|
|
||||||
|
|
||||||
let pending_tx = match client.send_transaction(tx, Some(BlockId::Number((BlockNumber::Latest).into()))).await {
|
let pending_tx = match client.send_transaction(tx, None).await {
|
||||||
Ok(pending_tx) => {
|
Ok(pending_tx) => {
|
||||||
log::debug!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
|
log::debug!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
|
||||||
log::info!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
|
log::info!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
|
||||||
@ -186,8 +175,5 @@ pub async fn estimate_gas(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| CryptoError::ContractError(format!("Failed to estimate gas: {}", e)))?;
|
.map_err(|e| CryptoError::ContractError(format!("Failed to estimate gas: {}", e)))?;
|
||||||
|
|
||||||
// Add a buffer to the gas estimate to account for potential variations
|
Ok(gas)
|
||||||
let gas_with_buffer = gas * 12 / 10; // Add 20% buffer
|
|
||||||
|
|
||||||
Ok(gas_with_buffer)
|
|
||||||
}
|
}
|
||||||
|
@ -24,33 +24,27 @@ pub use networks::NetworkConfig;
|
|||||||
|
|
||||||
// Re-export wallet creation functions
|
// Re-export wallet creation functions
|
||||||
pub use storage::{
|
pub use storage::{
|
||||||
create_ethereum_wallet,
|
|
||||||
create_ethereum_wallet_from_name,
|
|
||||||
create_ethereum_wallet_from_private_key,
|
|
||||||
// Legacy functions for backward compatibility
|
|
||||||
create_ethereum_wallet_for_network,
|
create_ethereum_wallet_for_network,
|
||||||
create_peaq_wallet,
|
create_peaq_wallet,
|
||||||
create_agung_wallet,
|
create_agung_wallet,
|
||||||
create_ethereum_wallet_from_name_for_network,
|
create_ethereum_wallet_from_name_for_network,
|
||||||
|
create_ethereum_wallet_from_name,
|
||||||
create_ethereum_wallet_from_private_key_for_network,
|
create_ethereum_wallet_from_private_key_for_network,
|
||||||
|
create_ethereum_wallet_from_private_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export wallet management functions
|
// Re-export wallet management functions
|
||||||
pub use storage::{
|
pub use storage::{
|
||||||
get_current_ethereum_wallet,
|
|
||||||
clear_ethereum_wallets,
|
|
||||||
// Legacy functions for backward compatibility
|
|
||||||
get_current_ethereum_wallet_for_network,
|
get_current_ethereum_wallet_for_network,
|
||||||
get_current_peaq_wallet,
|
get_current_peaq_wallet,
|
||||||
get_current_agung_wallet,
|
get_current_agung_wallet,
|
||||||
|
clear_ethereum_wallets,
|
||||||
clear_ethereum_wallets_for_network,
|
clear_ethereum_wallets_for_network,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export provider functions
|
// Re-export provider functions
|
||||||
pub use provider::{
|
pub use provider::{
|
||||||
create_provider,
|
create_provider,
|
||||||
create_provider_from_config,
|
|
||||||
// Legacy functions for backward compatibility
|
|
||||||
create_gnosis_provider,
|
create_gnosis_provider,
|
||||||
create_peaq_provider,
|
create_peaq_provider,
|
||||||
create_agung_provider,
|
create_agung_provider,
|
||||||
@ -59,16 +53,12 @@ pub use provider::{
|
|||||||
// Re-export transaction functions
|
// Re-export transaction functions
|
||||||
pub use transaction::{
|
pub use transaction::{
|
||||||
get_balance,
|
get_balance,
|
||||||
get_balance_with_provider,
|
|
||||||
send_eth,
|
send_eth,
|
||||||
send_eth_with_provider,
|
|
||||||
format_balance,
|
format_balance,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export network registry functions
|
// Re-export network registry functions
|
||||||
pub use networks::{
|
pub use networks::{
|
||||||
register_network,
|
|
||||||
remove_network,
|
|
||||||
get_network_by_name,
|
get_network_by_name,
|
||||||
get_proper_network_name,
|
get_proper_network_name,
|
||||||
list_network_names,
|
list_network_names,
|
||||||
|
@ -4,8 +4,7 @@
|
|||||||
//! to work with them.
|
//! to work with them.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::RwLock;
|
use std::sync::OnceLock;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
/// Configuration for an EVM-compatible network
|
/// Configuration for an EVM-compatible network
|
||||||
@ -19,16 +18,6 @@ pub struct NetworkConfig {
|
|||||||
pub decimals: u8,
|
pub decimals: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global registry of all supported networks
|
|
||||||
static NETWORK_REGISTRY: Lazy<RwLock<HashMap<String, NetworkConfig>>> = Lazy::new(|| {
|
|
||||||
let mut registry = HashMap::new();
|
|
||||||
|
|
||||||
// Add built-in networks
|
|
||||||
register_built_in_networks(&mut registry);
|
|
||||||
|
|
||||||
RwLock::new(registry)
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Network name constants
|
/// Network name constants
|
||||||
pub mod names {
|
pub mod names {
|
||||||
pub const GNOSIS: &str = "Gnosis";
|
pub const GNOSIS: &str = "Gnosis";
|
||||||
@ -36,80 +25,50 @@ pub mod names {
|
|||||||
pub const AGUNG: &str = "Agung";
|
pub const AGUNG: &str = "Agung";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register all built-in networks
|
/// Get the Gnosis Chain network configuration
|
||||||
fn register_built_in_networks(registry: &mut HashMap<String, NetworkConfig>) {
|
pub fn gnosis() -> NetworkConfig {
|
||||||
// Gnosis Chain
|
NetworkConfig {
|
||||||
registry.insert(names::GNOSIS.to_lowercase(), NetworkConfig {
|
|
||||||
name: names::GNOSIS.to_string(),
|
name: names::GNOSIS.to_string(),
|
||||||
chain_id: 100,
|
chain_id: 100,
|
||||||
rpc_url: "https://rpc.gnosischain.com".to_string(),
|
rpc_url: "https://rpc.gnosischain.com".to_string(),
|
||||||
explorer_url: "https://gnosisscan.io".to_string(),
|
explorer_url: "https://gnosisscan.io".to_string(),
|
||||||
token_symbol: "xDAI".to_string(),
|
token_symbol: "xDAI".to_string(),
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Peaq Network
|
/// Get the Peaq Network configuration
|
||||||
registry.insert(names::PEAQ.to_lowercase(), NetworkConfig {
|
pub fn peaq() -> NetworkConfig {
|
||||||
|
NetworkConfig {
|
||||||
name: names::PEAQ.to_string(),
|
name: names::PEAQ.to_string(),
|
||||||
chain_id: 3338,
|
chain_id: 3338,
|
||||||
rpc_url: "https://peaq.api.onfinality.io/public".to_string(),
|
rpc_url: "https://peaq.api.onfinality.io/public".to_string(),
|
||||||
explorer_url: "https://peaq.subscan.io/".to_string(),
|
explorer_url: "https://peaq.subscan.io/".to_string(),
|
||||||
token_symbol: "PEAQ".to_string(),
|
token_symbol: "PEAQ".to_string(),
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Agung Testnet
|
/// Get the Agung Testnet configuration
|
||||||
registry.insert(names::AGUNG.to_lowercase(), NetworkConfig {
|
pub fn agung() -> NetworkConfig {
|
||||||
|
NetworkConfig {
|
||||||
name: names::AGUNG.to_string(),
|
name: names::AGUNG.to_string(),
|
||||||
chain_id: 9990,
|
chain_id: 9990,
|
||||||
rpc_url: "https://wss-async.agung.peaq.network".to_string(),
|
rpc_url: "https://wss-async.agung.peaq.network".to_string(),
|
||||||
explorer_url: "https://agung-testnet.subscan.io/".to_string(),
|
explorer_url: "https://agung-testnet.subscan.io/".to_string(),
|
||||||
token_symbol: "AGNG".to_string(),
|
token_symbol: "AGNG".to_string(),
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register a new network
|
|
||||||
pub fn register_network(
|
|
||||||
name: &str,
|
|
||||||
chain_id: u64,
|
|
||||||
rpc_url: &str,
|
|
||||||
explorer_url: &str,
|
|
||||||
token_symbol: &str,
|
|
||||||
decimals: u8,
|
|
||||||
) -> bool {
|
|
||||||
let config = NetworkConfig {
|
|
||||||
name: name.to_string(),
|
|
||||||
chain_id,
|
|
||||||
rpc_url: rpc_url.to_string(),
|
|
||||||
explorer_url: explorer_url.to_string(),
|
|
||||||
token_symbol: token_symbol.to_string(),
|
|
||||||
decimals,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(mut registry) = NETWORK_REGISTRY.write() {
|
|
||||||
registry.insert(name.to_lowercase(), config);
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a network from the registry
|
|
||||||
pub fn remove_network(name: &str) -> bool {
|
|
||||||
if let Ok(mut registry) = NETWORK_REGISTRY.write() {
|
|
||||||
registry.remove(&name.to_lowercase()).is_some()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a network by its name (case-insensitive)
|
/// Get a network by its name (case-insensitive)
|
||||||
pub fn get_network_by_name(name: &str) -> Option<NetworkConfig> {
|
pub fn get_network_by_name(name: &str) -> Option<NetworkConfig> {
|
||||||
if let Ok(registry) = NETWORK_REGISTRY.read() {
|
let name_lower = name.to_lowercase();
|
||||||
registry.get(&name.to_lowercase()).cloned()
|
match name_lower.as_str() {
|
||||||
} else {
|
"gnosis" => Some(gnosis()),
|
||||||
None
|
"peaq" => Some(peaq()),
|
||||||
|
"agung" => Some(agung()),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,57 +84,19 @@ pub fn get_proper_network_name(name: &str) -> Option<&'static str> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get a list of all supported network names
|
/// Get a list of all supported network names
|
||||||
pub fn list_network_names() -> Vec<String> {
|
pub fn list_network_names() -> Vec<&'static str> {
|
||||||
if let Ok(registry) = NETWORK_REGISTRY.read() {
|
vec![names::GNOSIS, names::PEAQ, names::AGUNG]
|
||||||
registry.values().map(|config| config.name.clone()).collect()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a map of all networks
|
/// Get a map of all networks
|
||||||
pub fn get_all_networks() -> HashMap<String, NetworkConfig> {
|
pub fn get_all_networks() -> &'static HashMap<&'static str, NetworkConfig> {
|
||||||
if let Ok(registry) = NETWORK_REGISTRY.read() {
|
static NETWORKS: OnceLock<HashMap<&'static str, NetworkConfig>> = OnceLock::new();
|
||||||
registry.clone()
|
|
||||||
} else {
|
|
||||||
HashMap::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy functions for backward compatibility
|
NETWORKS.get_or_init(|| {
|
||||||
|
let mut map = HashMap::new();
|
||||||
/// Get the Gnosis Chain network configuration
|
map.insert(names::GNOSIS, gnosis());
|
||||||
pub fn gnosis() -> NetworkConfig {
|
map.insert(names::PEAQ, peaq());
|
||||||
get_network_by_name("gnosis").unwrap_or_else(|| NetworkConfig {
|
map.insert(names::AGUNG, agung());
|
||||||
name: names::GNOSIS.to_string(),
|
map
|
||||||
chain_id: 100,
|
|
||||||
rpc_url: "https://rpc.gnosischain.com".to_string(),
|
|
||||||
explorer_url: "https://gnosisscan.io".to_string(),
|
|
||||||
token_symbol: "xDAI".to_string(),
|
|
||||||
decimals: 18,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the Peaq Network configuration
|
|
||||||
pub fn peaq() -> NetworkConfig {
|
|
||||||
get_network_by_name("peaq").unwrap_or_else(|| NetworkConfig {
|
|
||||||
name: names::PEAQ.to_string(),
|
|
||||||
chain_id: 3338,
|
|
||||||
rpc_url: "https://peaq.api.onfinality.io/public".to_string(),
|
|
||||||
explorer_url: "https://peaq.subscan.io/".to_string(),
|
|
||||||
token_symbol: "PEAQ".to_string(),
|
|
||||||
decimals: 18,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the Agung Testnet configuration
|
|
||||||
pub fn agung() -> NetworkConfig {
|
|
||||||
get_network_by_name("agung").unwrap_or_else(|| NetworkConfig {
|
|
||||||
name: names::AGUNG.to_string(),
|
|
||||||
chain_id: 9990,
|
|
||||||
rpc_url: "https://wss-async.agung.peaq.network".to_string(),
|
|
||||||
explorer_url: "https://agung-testnet.subscan.io/".to_string(),
|
|
||||||
token_symbol: "AGNG".to_string(),
|
|
||||||
decimals: 18,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3,36 +3,25 @@
|
|||||||
use ethers::prelude::*;
|
use ethers::prelude::*;
|
||||||
|
|
||||||
use crate::vault::error::CryptoError;
|
use crate::vault::error::CryptoError;
|
||||||
use super::networks;
|
use super::networks::{self, NetworkConfig};
|
||||||
|
|
||||||
/// Creates a provider for a specific network.
|
/// Creates a provider for a specific network.
|
||||||
pub fn create_provider(network_name: &str) -> Result<Provider<Http>, CryptoError> {
|
pub fn create_provider(network: &NetworkConfig) -> Result<Provider<Http>, CryptoError> {
|
||||||
let network = networks::get_network_by_name(network_name)
|
|
||||||
.ok_or_else(|| CryptoError::SerializationError(format!("Unknown network: {}", network_name)))?;
|
|
||||||
|
|
||||||
Provider::<Http>::try_from(network.rpc_url.as_str())
|
Provider::<Http>::try_from(network.rpc_url.as_str())
|
||||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to create provider for {}: {}", network.name, e)))
|
.map_err(|e| CryptoError::SerializationError(format!("Failed to create provider for {}: {}", network.name, e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a provider for a specific network configuration.
|
|
||||||
pub fn create_provider_from_config(network: &networks::NetworkConfig) -> Result<Provider<Http>, CryptoError> {
|
|
||||||
Provider::<Http>::try_from(network.rpc_url.as_str())
|
|
||||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to create provider for {}: {}", network.name, e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy functions for backward compatibility
|
|
||||||
|
|
||||||
/// Creates a provider for the Gnosis Chain.
|
/// Creates a provider for the Gnosis Chain.
|
||||||
pub fn create_gnosis_provider() -> Result<Provider<Http>, CryptoError> {
|
pub fn create_gnosis_provider() -> Result<Provider<Http>, CryptoError> {
|
||||||
create_provider("gnosis")
|
create_provider(&networks::gnosis())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a provider for the Peaq network.
|
/// Creates a provider for the Peaq network.
|
||||||
pub fn create_peaq_provider() -> Result<Provider<Http>, CryptoError> {
|
pub fn create_peaq_provider() -> Result<Provider<Http>, CryptoError> {
|
||||||
create_provider("peaq")
|
create_provider(&networks::peaq())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a provider for the Agung testnet.
|
/// Creates a provider for the Agung testnet.
|
||||||
pub fn create_agung_provider() -> Result<Provider<Http>, CryptoError> {
|
pub fn create_agung_provider() -> Result<Provider<Http>, CryptoError> {
|
||||||
create_provider("agung")
|
create_provider(&networks::agung())
|
||||||
}
|
}
|
||||||
|
@ -3,301 +3,112 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use cfg_if::cfg_if;
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use ethers::types::Address;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::vault::error::CryptoError;
|
use crate::vault::error::CryptoError;
|
||||||
use crate::vault::kvs::{self, KVStore, DefaultStore};
|
|
||||||
use super::wallet::EthereumWallet;
|
use super::wallet::EthereumWallet;
|
||||||
use super::networks;
|
use super::networks::{self, NetworkConfig};
|
||||||
|
|
||||||
/// Ethereum wallet data storage key in KVStore
|
/// Global storage for Ethereum wallets.
|
||||||
const ETH_WALLET_STORAGE_KEY: &str = "ethereum/wallets";
|
static ETH_WALLETS: Lazy<Mutex<HashMap<String, Vec<EthereumWallet>>>> = Lazy::new(|| {
|
||||||
|
Mutex::new(HashMap::new())
|
||||||
/// Global fallback storage for Ethereum wallets (used when KVStore is unavailable)
|
|
||||||
static ETH_WALLETS: Lazy<Mutex<Vec<EthereumWallet>>> = Lazy::new(|| {
|
|
||||||
Mutex::new(Vec::new())
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global Tokio runtime for blocking async operations
|
|
||||||
static RUNTIME: Lazy<Mutex<Runtime>> = Lazy::new(|| {
|
|
||||||
Mutex::new(Runtime::new().expect("Failed to create Tokio runtime"))
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Serializable representation of an Ethereum wallet
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct EthereumWalletStorage {
|
|
||||||
/// Ethereum address string
|
|
||||||
address: String,
|
|
||||||
/// Private key in hex format
|
|
||||||
private_key: String,
|
|
||||||
/// Optional wallet name
|
|
||||||
name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&EthereumWallet> for EthereumWalletStorage {
|
|
||||||
fn from(wallet: &EthereumWallet) -> Self {
|
|
||||||
Self {
|
|
||||||
address: wallet.address_string(),
|
|
||||||
private_key: wallet.private_key_hex(),
|
|
||||||
name: wallet.name.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&EthereumWalletStorage> for EthereumWallet {
|
|
||||||
type Error = CryptoError;
|
|
||||||
|
|
||||||
fn try_from(storage: &EthereumWalletStorage) -> Result<Self, Self::Error> {
|
|
||||||
let wallet = EthereumWallet::from_private_key(&storage.private_key)?;
|
|
||||||
|
|
||||||
// If the address doesn't match, something is wrong
|
|
||||||
if wallet.address_string() != storage.address {
|
|
||||||
return Err(CryptoError::InvalidAddress(format!(
|
|
||||||
"Address mismatch: expected {}, got {}",
|
|
||||||
storage.address, wallet.address_string()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the name if present
|
|
||||||
let wallet_with_name = if let Some(name) = &storage.name {
|
|
||||||
EthereumWallet {
|
|
||||||
name: Some(name.clone()),
|
|
||||||
..wallet
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
wallet
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(wallet_with_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to get the platform-specific storage implementation
|
|
||||||
fn get_wallet_store() -> Result<DefaultStore, CryptoError> {
|
|
||||||
cfg_if! {
|
|
||||||
if #[cfg(target_arch = "wasm32")] {
|
|
||||||
// For WebAssembly, we need to handle the async nature of IndexedDB
|
|
||||||
// We'll use a blocking approach for API consistency
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
|
||||||
|
|
||||||
// We need to use the runtime to block_on the async operations
|
|
||||||
let rt = RUNTIME.lock().unwrap();
|
|
||||||
rt.block_on(async {
|
|
||||||
match kvs::open_default_store("ethereum-wallets", None).await {
|
|
||||||
Ok(store) => Ok(store),
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to open IndexedDB store: {}", e);
|
|
||||||
// Try to create the store if opening failed
|
|
||||||
kvs::create_default_store("ethereum-wallets", false, None).await
|
|
||||||
.map_err(|e| CryptoError::StorageError(e.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// For native platforms, we can use SlateDB directly
|
|
||||||
match kvs::open_default_store("ethereum-wallets", None) {
|
|
||||||
Ok(store) => Ok(store),
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to open SlateDB store: {}", e);
|
|
||||||
// Try to create the store if opening failed
|
|
||||||
kvs::create_default_store("ethereum-wallets", false, None)
|
|
||||||
.map_err(|e| CryptoError::StorageError(e.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save wallets to persistent storage
|
|
||||||
fn save_wallets(wallets: &[EthereumWallet]) -> Result<(), CryptoError> {
|
|
||||||
// Convert wallets to serializable format
|
|
||||||
let storage_wallets: Vec<EthereumWalletStorage> = wallets
|
|
||||||
.iter()
|
|
||||||
.map(|w| EthereumWalletStorage::from(w))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Try to use the KVStore implementation first
|
|
||||||
let store_result = get_wallet_store()
|
|
||||||
.and_then(|store| {
|
|
||||||
let json = serde_json::to_string(&storage_wallets)
|
|
||||||
.map_err(|e| CryptoError::StorageError(e.to_string()))?;
|
|
||||||
store.set(ETH_WALLET_STORAGE_KEY, &json)
|
|
||||||
.map_err(|e| CryptoError::StorageError(e.to_string()))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log warning if storage failed but don't fail the operation
|
|
||||||
if let Err(e) = &store_result {
|
|
||||||
log::warn!("Failed to save wallets to persistent storage: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always update the in-memory fallback
|
|
||||||
let mut mem_wallets = ETH_WALLETS.lock().unwrap();
|
|
||||||
*mem_wallets = wallets.to_vec();
|
|
||||||
|
|
||||||
// Return the result of the persistent storage operation
|
|
||||||
store_result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load wallets from persistent storage
|
|
||||||
fn load_wallets() -> Vec<EthereumWallet> {
|
|
||||||
// Try to load from KVStore first
|
|
||||||
let store_result = get_wallet_store()
|
|
||||||
.and_then(|store| {
|
|
||||||
store.get::<_, String>(ETH_WALLET_STORAGE_KEY)
|
|
||||||
.map_err(|e| CryptoError::StorageError(e.to_string()))
|
|
||||||
})
|
|
||||||
.and_then(|json| {
|
|
||||||
serde_json::from_str::<Vec<EthereumWalletStorage>>(&json)
|
|
||||||
.map_err(|e| CryptoError::StorageError(format!("Failed to parse wallet JSON: {}", e)))
|
|
||||||
});
|
|
||||||
|
|
||||||
match store_result {
|
|
||||||
Ok(storage_wallets) => {
|
|
||||||
// Convert from storage format to EthereumWallet
|
|
||||||
let wallets_result: Result<Vec<EthereumWallet>, CryptoError> = storage_wallets
|
|
||||||
.iter()
|
|
||||||
.map(|sw| EthereumWallet::try_from(sw))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
match wallets_result {
|
|
||||||
Ok(wallets) => {
|
|
||||||
// Also update the in-memory fallback
|
|
||||||
let mut mem_wallets = ETH_WALLETS.lock().unwrap();
|
|
||||||
*mem_wallets = wallets.clone();
|
|
||||||
wallets
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to convert wallets from storage format: {}", e);
|
|
||||||
// Fall back to in-memory storage
|
|
||||||
let mem_wallets = ETH_WALLETS.lock().unwrap();
|
|
||||||
mem_wallets.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to load wallets from persistent storage: {}", e);
|
|
||||||
// Fall back to in-memory storage
|
|
||||||
let mem_wallets = ETH_WALLETS.lock().unwrap();
|
|
||||||
mem_wallets.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an Ethereum wallet from the currently selected keypair.
|
|
||||||
pub fn create_ethereum_wallet() -> Result<EthereumWallet, CryptoError> {
|
|
||||||
// Get the currently selected keypair
|
|
||||||
let keypair = crate::vault::keypair::get_selected_keypair()?;
|
|
||||||
|
|
||||||
// Create an Ethereum wallet from the keypair
|
|
||||||
let wallet = EthereumWallet::from_keypair(&keypair)?;
|
|
||||||
|
|
||||||
// Store the wallet
|
|
||||||
let mut wallets = load_wallets();
|
|
||||||
wallets.push(wallet.clone());
|
|
||||||
save_wallets(&wallets)?;
|
|
||||||
|
|
||||||
Ok(wallet)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an Ethereum wallet from a name and the currently selected keypair.
|
|
||||||
pub fn create_ethereum_wallet_from_name(name: &str) -> Result<EthereumWallet, CryptoError> {
|
|
||||||
// Get the currently selected keypair
|
|
||||||
let keypair = crate::vault::keypair::get_selected_keypair()?;
|
|
||||||
|
|
||||||
// Create an Ethereum wallet from the name and keypair
|
|
||||||
let wallet = EthereumWallet::from_name_and_keypair(name, &keypair)?;
|
|
||||||
|
|
||||||
// Store the wallet
|
|
||||||
let mut wallets = load_wallets();
|
|
||||||
wallets.push(wallet.clone());
|
|
||||||
save_wallets(&wallets)?;
|
|
||||||
|
|
||||||
Ok(wallet)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an Ethereum wallet from a private key.
|
|
||||||
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<EthereumWallet, CryptoError> {
|
|
||||||
// Create an Ethereum wallet from the private key
|
|
||||||
let wallet = EthereumWallet::from_private_key(private_key)?;
|
|
||||||
|
|
||||||
// Store the wallet
|
|
||||||
let mut wallets = load_wallets();
|
|
||||||
wallets.push(wallet.clone());
|
|
||||||
save_wallets(&wallets)?;
|
|
||||||
|
|
||||||
Ok(wallet)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the current Ethereum wallet.
|
|
||||||
pub fn get_current_ethereum_wallet() -> Result<EthereumWallet, CryptoError> {
|
|
||||||
let wallets = load_wallets();
|
|
||||||
|
|
||||||
if wallets.is_empty() {
|
|
||||||
return Err(CryptoError::NoKeypairSelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(wallets.last().unwrap().clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears all Ethereum wallets.
|
|
||||||
pub fn clear_ethereum_wallets() {
|
|
||||||
// Clear both persistent and in-memory storage
|
|
||||||
if let Ok(store) = get_wallet_store() {
|
|
||||||
let _ = store.delete::<&str>(ETH_WALLET_STORAGE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut mem_wallets = ETH_WALLETS.lock().unwrap();
|
|
||||||
mem_wallets.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy functions for backward compatibility
|
|
||||||
|
|
||||||
/// Creates an Ethereum wallet from the currently selected keypair for a specific network.
|
/// Creates an Ethereum wallet from the currently selected keypair for a specific network.
|
||||||
pub fn create_ethereum_wallet_for_network(network: networks::NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
pub fn create_ethereum_wallet_for_network(network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
||||||
create_ethereum_wallet()
|
// Get the currently selected keypair
|
||||||
|
let keypair = crate::vault::keyspace::get_selected_keypair()?;
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from the keypair
|
||||||
|
let wallet = EthereumWallet::from_keypair(&keypair, network)?;
|
||||||
|
|
||||||
|
// Store the wallet
|
||||||
|
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new);
|
||||||
|
network_wallets.push(wallet.clone());
|
||||||
|
|
||||||
|
Ok(wallet)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an Ethereum wallet from the currently selected keypair for the Peaq network.
|
/// Creates an Ethereum wallet from the currently selected keypair for the Peaq network.
|
||||||
pub fn create_peaq_wallet() -> Result<EthereumWallet, CryptoError> {
|
pub fn create_peaq_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||||
create_ethereum_wallet()
|
create_ethereum_wallet_for_network(networks::peaq())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an Ethereum wallet from the currently selected keypair for the Agung testnet.
|
/// Creates an Ethereum wallet from the currently selected keypair for the Agung testnet.
|
||||||
pub fn create_agung_wallet() -> Result<EthereumWallet, CryptoError> {
|
pub fn create_agung_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||||
create_ethereum_wallet()
|
create_ethereum_wallet_for_network(networks::agung())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the current Ethereum wallet for a specific network.
|
/// Gets the current Ethereum wallet for a specific network.
|
||||||
pub fn get_current_ethereum_wallet_for_network(network_name: &str) -> Result<EthereumWallet, CryptoError> {
|
pub fn get_current_ethereum_wallet_for_network(network_name: &str) -> Result<EthereumWallet, CryptoError> {
|
||||||
get_current_ethereum_wallet()
|
let wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
|
||||||
|
let network_wallets = wallets.get(network_name).ok_or(CryptoError::NoKeypairSelected)?;
|
||||||
|
|
||||||
|
if network_wallets.is_empty() {
|
||||||
|
return Err(CryptoError::NoKeypairSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(network_wallets.last().unwrap().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the current Ethereum wallet for the Peaq network.
|
/// Gets the current Ethereum wallet for the Peaq network.
|
||||||
pub fn get_current_peaq_wallet() -> Result<EthereumWallet, CryptoError> {
|
pub fn get_current_peaq_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||||
get_current_ethereum_wallet()
|
get_current_ethereum_wallet_for_network("Peaq")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the current Ethereum wallet for the Agung testnet.
|
/// Gets the current Ethereum wallet for the Agung testnet.
|
||||||
pub fn get_current_agung_wallet() -> Result<EthereumWallet, CryptoError> {
|
pub fn get_current_agung_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||||
get_current_ethereum_wallet()
|
get_current_ethereum_wallet_for_network("Agung")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears all Ethereum wallets.
|
||||||
|
pub fn clear_ethereum_wallets() {
|
||||||
|
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
wallets.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears Ethereum wallets for a specific network.
|
/// Clears Ethereum wallets for a specific network.
|
||||||
pub fn clear_ethereum_wallets_for_network(network_name: &str) {
|
pub fn clear_ethereum_wallets_for_network(network_name: &str) {
|
||||||
// In the new design, we don't have network-specific wallets,
|
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||||
// so this is a no-op for backward compatibility
|
wallets.remove(network_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an Ethereum wallet from a name and the currently selected keypair for a specific network.
|
/// Creates an Ethereum wallet from a name and the currently selected keypair for a specific network.
|
||||||
pub fn create_ethereum_wallet_from_name_for_network(name: &str, network: networks::NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
pub fn create_ethereum_wallet_from_name_for_network(name: &str, network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
||||||
create_ethereum_wallet_from_name(name)
|
// Get the currently selected keypair
|
||||||
|
let keypair = crate::vault::keyspace::get_selected_keypair()?;
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from the name and keypair
|
||||||
|
let wallet = EthereumWallet::from_name_and_keypair(name, &keypair, network)?;
|
||||||
|
|
||||||
|
// Store the wallet
|
||||||
|
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new);
|
||||||
|
network_wallets.push(wallet.clone());
|
||||||
|
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an Ethereum wallet from a name and the currently selected keypair for the Gnosis network.
|
||||||
|
pub fn create_ethereum_wallet_from_name(name: &str) -> Result<EthereumWallet, CryptoError> {
|
||||||
|
create_ethereum_wallet_from_name_for_network(name, networks::gnosis())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an Ethereum wallet from a private key for a specific network.
|
/// Creates an Ethereum wallet from a private key for a specific network.
|
||||||
pub fn create_ethereum_wallet_from_private_key_for_network(private_key: &str, network: networks::NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
pub fn create_ethereum_wallet_from_private_key_for_network(private_key: &str, network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
||||||
create_ethereum_wallet_from_private_key(private_key)
|
// Create an Ethereum wallet from the private key
|
||||||
|
let wallet = EthereumWallet::from_private_key(private_key, network)?;
|
||||||
|
|
||||||
|
// Store the wallet
|
||||||
|
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new);
|
||||||
|
network_wallets.push(wallet.clone());
|
||||||
|
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an Ethereum wallet from a private key for the Gnosis network.
|
||||||
|
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<EthereumWallet, CryptoError> {
|
||||||
|
create_ethereum_wallet_from_private_key_for_network(private_key, networks::gnosis())
|
||||||
}
|
}
|
||||||
|
@ -22,123 +22,53 @@ fn test_network_config() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_network_registry() {
|
fn test_network_registry() {
|
||||||
// Get initial network names
|
let network_names = networks::list_network_names();
|
||||||
let initial_network_names = list_network_names();
|
assert!(network_names.iter().any(|&name| name == "Gnosis"));
|
||||||
assert!(initial_network_names.iter().any(|name| name == "Gnosis"));
|
assert!(network_names.iter().any(|&name| name == "Peaq"));
|
||||||
assert!(initial_network_names.iter().any(|name| name == "Peaq"));
|
assert!(network_names.iter().any(|&name| name == "Agung"));
|
||||||
assert!(initial_network_names.iter().any(|name| name == "Agung"));
|
|
||||||
|
|
||||||
// Test proper network name lookup
|
let gnosis_proper = networks::get_proper_network_name("gnosis");
|
||||||
let gnosis_proper = get_proper_network_name("gnosis");
|
assert_eq!(gnosis_proper, Some("Gnosis"));
|
||||||
assert_eq!(gnosis_proper, Some(networks::names::GNOSIS));
|
|
||||||
|
|
||||||
let peaq_proper = get_proper_network_name("peaq");
|
let peaq_proper = networks::get_proper_network_name("peaq");
|
||||||
assert_eq!(peaq_proper, Some(networks::names::PEAQ));
|
assert_eq!(peaq_proper, Some("Peaq"));
|
||||||
|
|
||||||
let agung_proper = get_proper_network_name("agung");
|
let agung_proper = networks::get_proper_network_name("agung");
|
||||||
assert_eq!(agung_proper, Some(networks::names::AGUNG));
|
assert_eq!(agung_proper, Some("Agung"));
|
||||||
|
|
||||||
let unknown = get_proper_network_name("unknown");
|
let unknown = networks::get_proper_network_name("unknown");
|
||||||
assert_eq!(unknown, None);
|
assert_eq!(unknown, None);
|
||||||
|
|
||||||
// Test network lookup by name
|
let gnosis_config = networks::get_network_by_name("Gnosis");
|
||||||
let gnosis_config = get_network_by_name("Gnosis");
|
|
||||||
assert!(gnosis_config.is_some());
|
assert!(gnosis_config.is_some());
|
||||||
assert_eq!(gnosis_config.unwrap().chain_id, 100);
|
assert_eq!(gnosis_config.unwrap().chain_id, 100);
|
||||||
|
|
||||||
let unknown_config = get_network_by_name("Unknown");
|
let unknown_config = networks::get_network_by_name("Unknown");
|
||||||
assert!(unknown_config.is_none());
|
assert!(unknown_config.is_none());
|
||||||
|
|
||||||
// Test case insensitivity
|
|
||||||
let gnosis_lower = get_network_by_name("gnosis");
|
|
||||||
assert!(gnosis_lower.is_some());
|
|
||||||
assert_eq!(gnosis_lower.unwrap().chain_id, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_network_registry_dynamic() {
|
|
||||||
// Register a new network
|
|
||||||
let success = register_network(
|
|
||||||
"Sepolia",
|
|
||||||
11155111,
|
|
||||||
"https://rpc.sepolia.org",
|
|
||||||
"https://sepolia.etherscan.io",
|
|
||||||
"ETH",
|
|
||||||
18
|
|
||||||
);
|
|
||||||
assert!(success);
|
|
||||||
|
|
||||||
// Verify the network was added
|
|
||||||
let network_names = list_network_names();
|
|
||||||
assert!(network_names.iter().any(|name| name == "Sepolia"));
|
|
||||||
|
|
||||||
// Get the network config
|
|
||||||
let sepolia = get_network_by_name("Sepolia");
|
|
||||||
assert!(sepolia.is_some());
|
|
||||||
let sepolia = sepolia.unwrap();
|
|
||||||
assert_eq!(sepolia.chain_id, 11155111);
|
|
||||||
assert_eq!(sepolia.token_symbol, "ETH");
|
|
||||||
assert_eq!(sepolia.rpc_url, "https://rpc.sepolia.org");
|
|
||||||
assert_eq!(sepolia.explorer_url, "https://sepolia.etherscan.io");
|
|
||||||
assert_eq!(sepolia.decimals, 18);
|
|
||||||
|
|
||||||
// Test case insensitivity
|
|
||||||
let sepolia_lower = get_network_by_name("sepolia");
|
|
||||||
assert!(sepolia_lower.is_some());
|
|
||||||
|
|
||||||
// Remove the network
|
|
||||||
let removed = remove_network("Sepolia");
|
|
||||||
assert!(removed);
|
|
||||||
|
|
||||||
// Verify the network was removed
|
|
||||||
let network_names = list_network_names();
|
|
||||||
assert!(!network_names.iter().any(|name| name == "Sepolia"));
|
|
||||||
|
|
||||||
// Try to get the removed network
|
|
||||||
let sepolia = get_network_by_name("Sepolia");
|
|
||||||
assert!(sepolia.is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_provider() {
|
fn test_create_provider() {
|
||||||
// Create providers using network configs
|
let gnosis = networks::gnosis();
|
||||||
let gnosis_provider = create_provider_from_config(&networks::gnosis());
|
let peaq = networks::peaq();
|
||||||
let peaq_provider = create_provider_from_config(&networks::peaq());
|
let agung = networks::agung();
|
||||||
let agung_provider = create_provider_from_config(&networks::agung());
|
|
||||||
|
// Create providers
|
||||||
|
let gnosis_provider = create_provider(&gnosis);
|
||||||
|
let peaq_provider = create_provider(&peaq);
|
||||||
|
let agung_provider = create_provider(&agung);
|
||||||
|
|
||||||
// They should all succeed
|
// They should all succeed
|
||||||
assert!(gnosis_provider.is_ok());
|
assert!(gnosis_provider.is_ok());
|
||||||
assert!(peaq_provider.is_ok());
|
assert!(peaq_provider.is_ok());
|
||||||
assert!(agung_provider.is_ok());
|
assert!(agung_provider.is_ok());
|
||||||
|
|
||||||
// Create providers using network names
|
// The convenience functions should also work
|
||||||
let gnosis_provider2 = create_provider("gnosis");
|
let gnosis_provider2 = create_gnosis_provider();
|
||||||
let peaq_provider2 = create_provider("peaq");
|
let peaq_provider2 = create_peaq_provider();
|
||||||
let agung_provider2 = create_provider("agung");
|
let agung_provider2 = create_agung_provider();
|
||||||
|
|
||||||
assert!(gnosis_provider2.is_ok());
|
assert!(gnosis_provider2.is_ok());
|
||||||
assert!(peaq_provider2.is_ok());
|
assert!(peaq_provider2.is_ok());
|
||||||
assert!(agung_provider2.is_ok());
|
assert!(agung_provider2.is_ok());
|
||||||
|
|
||||||
// The legacy convenience functions should also work
|
|
||||||
let gnosis_provider3 = create_gnosis_provider();
|
|
||||||
let peaq_provider3 = create_peaq_provider();
|
|
||||||
let agung_provider3 = create_agung_provider();
|
|
||||||
|
|
||||||
assert!(gnosis_provider3.is_ok());
|
|
||||||
assert!(peaq_provider3.is_ok());
|
|
||||||
assert!(agung_provider3.is_ok());
|
|
||||||
|
|
||||||
// Test with an unknown network
|
|
||||||
let unknown_provider = create_provider("unknown");
|
|
||||||
assert!(unknown_provider.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_all_networks() {
|
|
||||||
let networks = get_all_networks();
|
|
||||||
assert!(!networks.is_empty());
|
|
||||||
assert!(networks.contains_key("gnosis"));
|
|
||||||
assert!(networks.contains_key("peaq"));
|
|
||||||
assert!(networks.contains_key("agung"));
|
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ fn test_get_balance() {
|
|||||||
|
|
||||||
// Create a provider
|
// Create a provider
|
||||||
let network = networks::gnosis();
|
let network = networks::gnosis();
|
||||||
let provider_result = create_provider_from_config(&network);
|
let provider_result = create_provider(&network);
|
||||||
|
|
||||||
// The provider creation should succeed
|
// The provider creation should succeed
|
||||||
assert!(provider_result.is_ok());
|
assert!(provider_result.is_ok());
|
||||||
@ -59,10 +59,10 @@ fn test_send_eth() {
|
|||||||
// Create a wallet
|
// Create a wallet
|
||||||
let keypair = KeyPair::new("test_keypair6");
|
let keypair = KeyPair::new("test_keypair6");
|
||||||
let network = networks::gnosis();
|
let network = networks::gnosis();
|
||||||
let wallet = EthereumWallet::from_keypair(&keypair).unwrap();
|
let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
|
||||||
|
|
||||||
// Create a provider
|
// Create a provider
|
||||||
let provider_result = create_provider_from_config(&network);
|
let provider_result = create_provider(&network);
|
||||||
assert!(provider_result.is_ok());
|
assert!(provider_result.is_ok());
|
||||||
|
|
||||||
// We can't actually test send_eth without a blockchain
|
// We can't actually test send_eth without a blockchain
|
||||||
|
@ -3,57 +3,59 @@
|
|||||||
use crate::vault::ethereum::*;
|
use crate::vault::ethereum::*;
|
||||||
use crate::vault::keypair::implementation::KeyPair;
|
use crate::vault::keypair::implementation::KeyPair;
|
||||||
use ethers::utils::hex;
|
use ethers::utils::hex;
|
||||||
use ethers::prelude::Signer;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ethereum_wallet_from_keypair() {
|
fn test_ethereum_wallet_from_keypair() {
|
||||||
let keypair = KeyPair::new("test_keypair");
|
let keypair = KeyPair::new("test_keypair");
|
||||||
|
let network = networks::gnosis();
|
||||||
|
|
||||||
let wallet = EthereumWallet::from_keypair(&keypair).unwrap();
|
let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.network.name, "Gnosis");
|
||||||
|
assert_eq!(wallet.network.chain_id, 100);
|
||||||
|
|
||||||
// The address should be a valid Ethereum address
|
// The address should be a valid Ethereum address
|
||||||
assert!(wallet.address_string().starts_with("0x"));
|
assert!(wallet.address_string().starts_with("0x"));
|
||||||
|
|
||||||
// The wallet should not have a name
|
|
||||||
assert!(wallet.name.is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ethereum_wallet_from_name_and_keypair() {
|
fn test_ethereum_wallet_from_name_and_keypair() {
|
||||||
let keypair = KeyPair::new("test_keypair2");
|
let keypair = KeyPair::new("test_keypair2");
|
||||||
|
let network = networks::gnosis();
|
||||||
|
|
||||||
let wallet = EthereumWallet::from_name_and_keypair("test", &keypair).unwrap();
|
let wallet = EthereumWallet::from_name_and_keypair("test", &keypair, network.clone()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.network.name, "Gnosis");
|
||||||
|
assert_eq!(wallet.network.chain_id, 100);
|
||||||
|
|
||||||
// The address should be a valid Ethereum address
|
// The address should be a valid Ethereum address
|
||||||
assert!(wallet.address_string().starts_with("0x"));
|
assert!(wallet.address_string().starts_with("0x"));
|
||||||
|
|
||||||
// The wallet should have the correct name
|
|
||||||
assert_eq!(wallet.name, Some("test".to_string()));
|
|
||||||
|
|
||||||
// Creating another wallet with the same name and keypair should yield the same address
|
// Creating another wallet with the same name and keypair should yield the same address
|
||||||
let wallet2 = EthereumWallet::from_name_and_keypair("test", &keypair).unwrap();
|
let wallet2 = EthereumWallet::from_name_and_keypair("test", &keypair, network.clone()).unwrap();
|
||||||
assert_eq!(wallet.address_string(), wallet2.address_string());
|
assert_eq!(wallet.address, wallet2.address);
|
||||||
|
|
||||||
// Creating a wallet with a different name should yield a different address
|
// Creating a wallet with a different name should yield a different address
|
||||||
let wallet3 = EthereumWallet::from_name_and_keypair("test2", &keypair).unwrap();
|
let wallet3 = EthereumWallet::from_name_and_keypair("test2", &keypair, network.clone()).unwrap();
|
||||||
assert_ne!(wallet.address_string(), wallet3.address_string());
|
assert_ne!(wallet.address, wallet3.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ethereum_wallet_from_private_key() {
|
fn test_ethereum_wallet_from_private_key() {
|
||||||
let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||||
|
let network = networks::gnosis();
|
||||||
|
|
||||||
let wallet = EthereumWallet::from_private_key(private_key).unwrap();
|
let wallet = EthereumWallet::from_private_key(private_key, network.clone()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.network.name, "Gnosis");
|
||||||
|
assert_eq!(wallet.network.chain_id, 100);
|
||||||
|
|
||||||
// The address should be a valid Ethereum address
|
// The address should be a valid Ethereum address
|
||||||
assert!(wallet.address_string().starts_with("0x"));
|
assert!(wallet.address_string().starts_with("0x"));
|
||||||
|
|
||||||
// The wallet should not have a name
|
|
||||||
assert!(wallet.name.is_none());
|
|
||||||
|
|
||||||
// The address should be deterministic based on the private key
|
// The address should be deterministic based on the private key
|
||||||
let wallet2 = EthereumWallet::from_private_key(private_key).unwrap();
|
let wallet2 = EthereumWallet::from_private_key(private_key, network.clone()).unwrap();
|
||||||
assert_eq!(wallet.address_string(), wallet2.address_string());
|
assert_eq!(wallet.address, wallet2.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -65,63 +67,52 @@ fn test_wallet_management() {
|
|||||||
crate::vault::keypair::session_manager::create_space("test_space").unwrap();
|
crate::vault::keypair::session_manager::create_space("test_space").unwrap();
|
||||||
crate::vault::keypair::create_keypair("test_keypair3").unwrap();
|
crate::vault::keypair::create_keypair("test_keypair3").unwrap();
|
||||||
|
|
||||||
// Create a wallet
|
// Create wallets for different networks
|
||||||
let wallet = create_ethereum_wallet().unwrap();
|
let gnosis_wallet = create_ethereum_wallet_for_network(networks::gnosis()).unwrap();
|
||||||
|
let peaq_wallet = create_ethereum_wallet_for_network(networks::peaq()).unwrap();
|
||||||
|
let agung_wallet = create_ethereum_wallet_for_network(networks::agung()).unwrap();
|
||||||
|
|
||||||
// Get the current wallet
|
// Get the current wallets
|
||||||
let current_wallet = get_current_ethereum_wallet().unwrap();
|
let current_gnosis = get_current_ethereum_wallet_for_network("Gnosis").unwrap();
|
||||||
|
let current_peaq = get_current_ethereum_wallet_for_network("Peaq").unwrap();
|
||||||
|
let current_agung = get_current_ethereum_wallet_for_network("Agung").unwrap();
|
||||||
|
|
||||||
// Check that they match
|
// Check that they match
|
||||||
assert_eq!(wallet.address_string(), current_wallet.address_string());
|
assert_eq!(gnosis_wallet.address, current_gnosis.address);
|
||||||
|
assert_eq!(peaq_wallet.address, current_peaq.address);
|
||||||
|
assert_eq!(agung_wallet.address, current_agung.address);
|
||||||
|
|
||||||
|
// Clear wallets for a specific network
|
||||||
|
clear_ethereum_wallets_for_network("Gnosis");
|
||||||
|
|
||||||
|
// Check that the wallet is gone
|
||||||
|
let result = get_current_ethereum_wallet_for_network("Gnosis");
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// But the others should still be there
|
||||||
|
let current_peaq = get_current_ethereum_wallet_for_network("Peaq").unwrap();
|
||||||
|
let current_agung = get_current_ethereum_wallet_for_network("Agung").unwrap();
|
||||||
|
assert_eq!(peaq_wallet.address, current_peaq.address);
|
||||||
|
assert_eq!(agung_wallet.address, current_agung.address);
|
||||||
|
|
||||||
// Clear all wallets
|
// Clear all wallets
|
||||||
clear_ethereum_wallets();
|
clear_ethereum_wallets();
|
||||||
|
|
||||||
// Check that the wallet is gone
|
// Check that all wallets are gone
|
||||||
let result = get_current_ethereum_wallet();
|
let result1 = get_current_ethereum_wallet_for_network("Gnosis");
|
||||||
assert!(result.is_err());
|
let result2 = get_current_ethereum_wallet_for_network("Peaq");
|
||||||
|
let result3 = get_current_ethereum_wallet_for_network("Agung");
|
||||||
// The legacy network-specific wallet functions have been removed
|
assert!(result1.is_err());
|
||||||
// We now use a single wallet that works across all networks
|
assert!(result2.is_err());
|
||||||
|
assert!(result3.is_err());
|
||||||
// Create a new wallet (network-agnostic)
|
|
||||||
let wallet = create_ethereum_wallet().unwrap();
|
|
||||||
|
|
||||||
// Check that it's accessible
|
|
||||||
let current_wallet = get_current_ethereum_wallet().unwrap();
|
|
||||||
assert_eq!(wallet.address_string(), current_wallet.address_string());
|
|
||||||
|
|
||||||
// Test for_network functionality to get network-specific wallet
|
|
||||||
let gnosis_network = networks::gnosis();
|
|
||||||
let peaq_network = networks::peaq();
|
|
||||||
let agung_network = networks::agung();
|
|
||||||
|
|
||||||
// The wallet address should remain the same regardless of network
|
|
||||||
let wallet_address = current_wallet.address_string();
|
|
||||||
|
|
||||||
// Network-specific wallets have different chain IDs but same address
|
|
||||||
// Just verify different chain IDs here
|
|
||||||
let gnosis_wallet = current_wallet.for_network(&gnosis_network);
|
|
||||||
let peaq_wallet = current_wallet.for_network(&peaq_network);
|
|
||||||
let agung_wallet = current_wallet.for_network(&agung_network);
|
|
||||||
|
|
||||||
// Check that chain IDs are different
|
|
||||||
assert_ne!(gnosis_wallet.chain_id(), peaq_wallet.chain_id());
|
|
||||||
assert_ne!(gnosis_wallet.chain_id(), agung_wallet.chain_id());
|
|
||||||
|
|
||||||
// Clear all wallets
|
|
||||||
clear_ethereum_wallets();
|
|
||||||
|
|
||||||
// Check that the wallet is gone
|
|
||||||
let result = get_current_ethereum_wallet();
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sign_message() {
|
fn test_sign_message() {
|
||||||
let keypair = KeyPair::new("test_keypair4");
|
let keypair = KeyPair::new("test_keypair4");
|
||||||
|
let network = networks::gnosis();
|
||||||
|
|
||||||
let wallet = EthereumWallet::from_keypair(&keypair).unwrap();
|
let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
|
||||||
|
|
||||||
// Create a tokio runtime for the async test
|
// Create a tokio runtime for the async test
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
@ -137,8 +128,9 @@ fn test_sign_message() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_private_key_hex() {
|
fn test_private_key_hex() {
|
||||||
let keypair = KeyPair::new("test_keypair5");
|
let keypair = KeyPair::new("test_keypair5");
|
||||||
|
let network = networks::gnosis();
|
||||||
|
|
||||||
let wallet = EthereumWallet::from_keypair(&keypair).unwrap();
|
let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
|
||||||
|
|
||||||
// Get the private key as hex
|
// Get the private key as hex
|
||||||
let private_key_hex = wallet.private_key_hex();
|
let private_key_hex = wallet.private_key_hex();
|
||||||
@ -149,55 +141,3 @@ fn test_private_key_hex() {
|
|||||||
// It should be possible to parse it as hex
|
// It should be possible to parse it as hex
|
||||||
let _bytes = hex::decode(private_key_hex).unwrap();
|
let _bytes = hex::decode(private_key_hex).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_wallet_for_network() {
|
|
||||||
let keypair = KeyPair::new("test_keypair6");
|
|
||||||
|
|
||||||
let wallet = EthereumWallet::from_keypair(&keypair).unwrap();
|
|
||||||
|
|
||||||
// Get wallets for different networks
|
|
||||||
let gnosis_network = networks::gnosis();
|
|
||||||
let peaq_network = networks::peaq();
|
|
||||||
let agung_network = networks::agung();
|
|
||||||
|
|
||||||
let gnosis_wallet = wallet.for_network(&gnosis_network);
|
|
||||||
let peaq_wallet = wallet.for_network(&peaq_network);
|
|
||||||
let agung_wallet = wallet.for_network(&agung_network);
|
|
||||||
|
|
||||||
// The chain IDs should match the networks
|
|
||||||
assert_eq!(gnosis_wallet.chain_id(), gnosis_network.chain_id);
|
|
||||||
assert_eq!(peaq_wallet.chain_id(), peaq_network.chain_id);
|
|
||||||
assert_eq!(agung_wallet.chain_id(), agung_network.chain_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multi_network_configuration() {
|
|
||||||
let keypair = KeyPair::new("test_keypair7");
|
|
||||||
|
|
||||||
// Create a network-agnostic wallet
|
|
||||||
let wallet = EthereumWallet::from_keypair(&keypair).unwrap();
|
|
||||||
|
|
||||||
// Test the for_network functionality to get network-specific configurations
|
|
||||||
let gnosis_network = networks::gnosis();
|
|
||||||
let peaq_network = networks::peaq();
|
|
||||||
let agung_network = networks::agung();
|
|
||||||
|
|
||||||
// Get the wallet's base address for comparison
|
|
||||||
let wallet_address = format!("{:?}", wallet.address);
|
|
||||||
|
|
||||||
// Create network-specific signers
|
|
||||||
let gnosis_wallet = wallet.for_network(&gnosis_network);
|
|
||||||
let peaq_wallet = wallet.for_network(&peaq_network);
|
|
||||||
let agung_wallet = wallet.for_network(&agung_network);
|
|
||||||
|
|
||||||
// The signers should each have their network's chain ID
|
|
||||||
assert_eq!(gnosis_wallet.chain_id(), gnosis_network.chain_id);
|
|
||||||
assert_eq!(peaq_wallet.chain_id(), peaq_network.chain_id);
|
|
||||||
assert_eq!(agung_wallet.chain_id(), agung_network.chain_id);
|
|
||||||
|
|
||||||
// And each should have the same address as the original wallet
|
|
||||||
assert_eq!(format!("{:?}", gnosis_wallet.address()), wallet_address);
|
|
||||||
assert_eq!(format!("{:?}", peaq_wallet.address()), wallet_address);
|
|
||||||
assert_eq!(format!("{:?}", agung_wallet.address()), wallet_address);
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
//! Ethereum transaction functionality.
|
//! Ethereum transaction functionality.
|
||||||
|
|
||||||
use ethers::prelude::*;
|
use ethers::prelude::*;
|
||||||
use ethers::types::transaction::eip2718::TypedTransaction;
|
|
||||||
|
|
||||||
use crate::vault::error::CryptoError;
|
use crate::vault::error::CryptoError;
|
||||||
use super::wallet::EthereumWallet;
|
use super::wallet::EthereumWallet;
|
||||||
use super::networks::NetworkConfig;
|
use super::networks::NetworkConfig;
|
||||||
use super::provider;
|
|
||||||
|
|
||||||
/// Formats a token balance for display.
|
/// Formats a token balance for display.
|
||||||
pub fn format_balance(balance: U256, network: &NetworkConfig) -> String {
|
pub fn format_balance(balance: U256, network: &NetworkConfig) -> String {
|
||||||
@ -21,16 +19,7 @@ pub fn format_balance(balance: U256, network: &NetworkConfig) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the balance of an Ethereum address.
|
/// Gets the balance of an Ethereum address.
|
||||||
pub async fn get_balance(network_name: &str, address: Address) -> Result<U256, CryptoError> {
|
pub async fn get_balance(provider: &Provider<Http>, address: Address) -> Result<U256, CryptoError> {
|
||||||
let provider = provider::create_provider(network_name)?;
|
|
||||||
|
|
||||||
provider.get_balance(address, None)
|
|
||||||
.await
|
|
||||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to get balance: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the balance of an Ethereum address using a provider.
|
|
||||||
pub async fn get_balance_with_provider(provider: &Provider<Http>, address: Address) -> Result<U256, CryptoError> {
|
|
||||||
provider.get_balance(address, None)
|
provider.get_balance(address, None)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to get balance: {}", e)))
|
.map_err(|e| CryptoError::SerializationError(format!("Failed to get balance: {}", e)))
|
||||||
@ -38,56 +27,6 @@ pub async fn get_balance_with_provider(provider: &Provider<Http>, address: Addre
|
|||||||
|
|
||||||
/// Sends Ethereum from one address to another.
|
/// Sends Ethereum from one address to another.
|
||||||
pub async fn send_eth(
|
pub async fn send_eth(
|
||||||
wallet: &EthereumWallet,
|
|
||||||
network_name: &str,
|
|
||||||
to: Address,
|
|
||||||
amount: U256,
|
|
||||||
) -> Result<H256, CryptoError> {
|
|
||||||
// Get the network configuration
|
|
||||||
let network = super::networks::get_network_by_name(network_name)
|
|
||||||
.ok_or_else(|| CryptoError::SerializationError(format!("Unknown network: {}", network_name)))?;
|
|
||||||
|
|
||||||
// Create a provider
|
|
||||||
let provider = provider::create_provider(network_name)?;
|
|
||||||
|
|
||||||
// Create a client with the wallet configured for this network
|
|
||||||
let network_wallet = wallet.for_network(&network);
|
|
||||||
let client = SignerMiddleware::new(
|
|
||||||
provider.clone(),
|
|
||||||
network_wallet,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Estimate gas
|
|
||||||
let tx = TransactionRequest::new()
|
|
||||||
.to(to)
|
|
||||||
.value(amount);
|
|
||||||
|
|
||||||
// Convert TransactionRequest to TypedTransaction explicitly
|
|
||||||
let typed_tx: TypedTransaction = tx.into();
|
|
||||||
let gas = client.estimate_gas(&typed_tx, None)
|
|
||||||
.await
|
|
||||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to estimate gas: {}", e)))?;
|
|
||||||
log::info!("Estimated gas: {:?}", gas);
|
|
||||||
|
|
||||||
// Create the transaction
|
|
||||||
let tx = TransactionRequest::new()
|
|
||||||
.to(to)
|
|
||||||
.value(amount)
|
|
||||||
.gas(gas);
|
|
||||||
|
|
||||||
// Send the transaction
|
|
||||||
let pending_tx = client.send_transaction(tx, Some(BlockId::Number((BlockNumber::Latest).into())))
|
|
||||||
.await
|
|
||||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to send transaction: {}", e)))?;
|
|
||||||
|
|
||||||
// Return the transaction hash instead of waiting for the receipt
|
|
||||||
Ok(pending_tx.tx_hash())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy function for backward compatibility
|
|
||||||
|
|
||||||
/// Sends Ethereum from one address to another using a provider.
|
|
||||||
pub async fn send_eth_with_provider(
|
|
||||||
wallet: &EthereumWallet,
|
wallet: &EthereumWallet,
|
||||||
provider: &Provider<Http>,
|
provider: &Provider<Http>,
|
||||||
to: Address,
|
to: Address,
|
||||||
@ -96,29 +35,17 @@ pub async fn send_eth_with_provider(
|
|||||||
// Create a client with the wallet
|
// Create a client with the wallet
|
||||||
let client = SignerMiddleware::new(
|
let client = SignerMiddleware::new(
|
||||||
provider.clone(),
|
provider.clone(),
|
||||||
wallet.signer.clone(),
|
wallet.wallet.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Estimate gas
|
|
||||||
let tx = TransactionRequest::new()
|
|
||||||
.to(to)
|
|
||||||
.value(amount);
|
|
||||||
|
|
||||||
// Convert TransactionRequest to TypedTransaction explicitly
|
|
||||||
let typed_tx: TypedTransaction = tx.into();
|
|
||||||
let gas = client.estimate_gas(&typed_tx, None)
|
|
||||||
.await
|
|
||||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to estimate gas: {}", e)))?;
|
|
||||||
log::info!("Estimated gas: {:?}", gas);
|
|
||||||
|
|
||||||
// Create the transaction
|
// Create the transaction
|
||||||
let tx = TransactionRequest::new()
|
let tx = TransactionRequest::new()
|
||||||
.to(to)
|
.to(to)
|
||||||
.value(amount)
|
.value(amount)
|
||||||
.gas(gas);
|
.gas(21000);
|
||||||
|
|
||||||
// Send the transaction
|
// Send the transaction
|
||||||
let pending_tx = client.send_transaction(tx, Some(BlockId::Number((BlockNumber::Latest).into())))
|
let pending_tx = client.send_transaction(tx, None)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to send transaction: {}", e)))?;
|
.map_err(|e| CryptoError::SerializationError(format!("Failed to send transaction: {}", e)))?;
|
||||||
|
|
||||||
|
@ -4,24 +4,27 @@ use ethers::prelude::*;
|
|||||||
use ethers::signers::{LocalWallet, Signer, Wallet};
|
use ethers::signers::{LocalWallet, Signer, Wallet};
|
||||||
use ethers::utils::hex;
|
use ethers::utils::hex;
|
||||||
use k256::ecdsa::SigningKey;
|
use k256::ecdsa::SigningKey;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use sha2::{Sha256, Digest};
|
|
||||||
|
|
||||||
use crate::vault::error::CryptoError;
|
|
||||||
use crate::vault::keypair::KeyPair;
|
|
||||||
use super::networks::NetworkConfig;
|
use super::networks::NetworkConfig;
|
||||||
|
use crate::vault;
|
||||||
|
use crate::vault::error::CryptoError;
|
||||||
|
|
||||||
/// An Ethereum wallet derived from a keypair.
|
/// An Ethereum wallet derived from a keypair.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EthereumWallet {
|
pub struct EthereumWallet {
|
||||||
pub address: Address,
|
pub address: Address,
|
||||||
pub signer: Wallet<SigningKey>,
|
pub wallet: Wallet<SigningKey>,
|
||||||
pub name: Option<String>,
|
pub network: NetworkConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EthereumWallet {
|
impl EthereumWallet {
|
||||||
/// Creates a new Ethereum wallet from a keypair.
|
/// Creates a new Ethereum wallet from a keypair for a specific network.
|
||||||
pub fn from_keypair(keypair: &KeyPair) -> Result<Self, CryptoError> {
|
pub fn from_keypair(
|
||||||
|
keypair: &vault::keyspace::keypair_types::KeyPair,
|
||||||
|
network: NetworkConfig,
|
||||||
|
) -> Result<Self, CryptoError> {
|
||||||
// Get the private key bytes from the keypair
|
// Get the private key bytes from the keypair
|
||||||
let private_key_bytes = keypair.signing_key.to_bytes();
|
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||||
|
|
||||||
@ -29,21 +32,26 @@ impl EthereumWallet {
|
|||||||
let private_key_hex = hex::encode(private_key_bytes);
|
let private_key_hex = hex::encode(private_key_bytes);
|
||||||
|
|
||||||
// Create an Ethereum wallet from the private key
|
// Create an Ethereum wallet from the private key
|
||||||
let signer = LocalWallet::from_str(&private_key_hex)
|
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||||
.map_err(|_e| CryptoError::InvalidKeyLength)?;
|
.map_err(|_e| CryptoError::InvalidKeyLength)?
|
||||||
|
.with_chain_id(network.chain_id);
|
||||||
|
|
||||||
// Get the Ethereum address
|
// Get the Ethereum address
|
||||||
let address = signer.address();
|
let address = wallet.address();
|
||||||
|
|
||||||
Ok(EthereumWallet {
|
Ok(EthereumWallet {
|
||||||
address,
|
address,
|
||||||
signer,
|
wallet,
|
||||||
name: None,
|
network,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new Ethereum wallet from a name and keypair (deterministic derivation).
|
/// Creates a new Ethereum wallet from a name and keypair (deterministic derivation) for a specific network.
|
||||||
pub fn from_name_and_keypair(name: &str, keypair: &KeyPair) -> Result<Self, CryptoError> {
|
pub fn from_name_and_keypair(
|
||||||
|
name: &str,
|
||||||
|
keypair: &vault::keyspace::keypair_types::KeyPair,
|
||||||
|
network: NetworkConfig,
|
||||||
|
) -> Result<Self, CryptoError> {
|
||||||
// Get the private key bytes from the keypair
|
// Get the private key bytes from the keypair
|
||||||
let private_key_bytes = keypair.signing_key.to_bytes();
|
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||||
|
|
||||||
@ -57,35 +65,40 @@ impl EthereumWallet {
|
|||||||
let private_key_hex = hex::encode(seed);
|
let private_key_hex = hex::encode(seed);
|
||||||
|
|
||||||
// Create an Ethereum wallet from the derived private key
|
// Create an Ethereum wallet from the derived private key
|
||||||
let signer = LocalWallet::from_str(&private_key_hex)
|
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||||
.map_err(|_e| CryptoError::InvalidKeyLength)?;
|
.map_err(|_e| CryptoError::InvalidKeyLength)?
|
||||||
|
.with_chain_id(network.chain_id);
|
||||||
|
|
||||||
// Get the Ethereum address
|
// Get the Ethereum address
|
||||||
let address = signer.address();
|
let address = wallet.address();
|
||||||
|
|
||||||
Ok(EthereumWallet {
|
Ok(EthereumWallet {
|
||||||
address,
|
address,
|
||||||
signer,
|
wallet,
|
||||||
name: Some(name.to_string()),
|
network,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new Ethereum wallet from a private key.
|
/// Creates a new Ethereum wallet from a private key for a specific network.
|
||||||
pub fn from_private_key(private_key: &str) -> Result<Self, CryptoError> {
|
pub fn from_private_key(
|
||||||
|
private_key: &str,
|
||||||
|
network: NetworkConfig,
|
||||||
|
) -> Result<Self, CryptoError> {
|
||||||
// Remove 0x prefix if present
|
// Remove 0x prefix if present
|
||||||
let private_key_clean = private_key.trim_start_matches("0x");
|
let private_key_clean = private_key.trim_start_matches("0x");
|
||||||
|
|
||||||
// Create an Ethereum wallet from the private key
|
// Create an Ethereum wallet from the private key
|
||||||
let signer = LocalWallet::from_str(private_key_clean)
|
let wallet = LocalWallet::from_str(private_key_clean)
|
||||||
.map_err(|_e| CryptoError::InvalidKeyLength)?;
|
.map_err(|_e| CryptoError::InvalidKeyLength)?
|
||||||
|
.with_chain_id(network.chain_id);
|
||||||
|
|
||||||
// Get the Ethereum address
|
// Get the Ethereum address
|
||||||
let address = signer.address();
|
let address = wallet.address();
|
||||||
|
|
||||||
Ok(EthereumWallet {
|
Ok(EthereumWallet {
|
||||||
address,
|
address,
|
||||||
signer,
|
wallet,
|
||||||
name: None,
|
network,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +109,9 @@ impl EthereumWallet {
|
|||||||
|
|
||||||
/// Signs a message with the Ethereum wallet.
|
/// Signs a message with the Ethereum wallet.
|
||||||
pub async fn sign_message(&self, message: &[u8]) -> Result<String, CryptoError> {
|
pub async fn sign_message(&self, message: &[u8]) -> Result<String, CryptoError> {
|
||||||
let signature = self.signer.sign_message(message)
|
let signature = self
|
||||||
|
.wallet
|
||||||
|
.sign_message(message)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
||||||
|
|
||||||
@ -105,34 +120,7 @@ impl EthereumWallet {
|
|||||||
|
|
||||||
/// Gets the private key as a hex string.
|
/// Gets the private key as a hex string.
|
||||||
pub fn private_key_hex(&self) -> String {
|
pub fn private_key_hex(&self) -> String {
|
||||||
let bytes = self.signer.signer().to_bytes();
|
let bytes = self.wallet.signer().to_bytes();
|
||||||
hex::encode(bytes)
|
hex::encode(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a wallet configured for a specific network.
|
|
||||||
pub fn for_network(&self, network: &NetworkConfig) -> Wallet<SigningKey> {
|
|
||||||
self.signer.clone().with_chain_id(network.chain_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy functions for backward compatibility
|
|
||||||
|
|
||||||
impl EthereumWallet {
|
|
||||||
/// Creates a new Ethereum wallet from a keypair for a specific network.
|
|
||||||
/// This is kept for backward compatibility.
|
|
||||||
pub fn from_keypair_for_network(keypair: &KeyPair, network: NetworkConfig) -> Result<Self, CryptoError> {
|
|
||||||
Self::from_keypair(keypair)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new Ethereum wallet from a name and keypair (deterministic derivation) for a specific network.
|
|
||||||
/// This is kept for backward compatibility.
|
|
||||||
pub fn from_name_and_keypair_for_network(name: &str, keypair: &KeyPair, _network: NetworkConfig) -> Result<Self, CryptoError> {
|
|
||||||
Self::from_name_and_keypair(name, keypair)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new Ethereum wallet from a private key for a specific network.
|
|
||||||
/// This is kept for backward compatibility.
|
|
||||||
pub fn from_private_key_for_network(private_key: &str, _network: NetworkConfig) -> Result<Self, CryptoError> {
|
|
||||||
Self::from_private_key(private_key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
vault = "0.1.0" # Replace with the actual version
|
hero_vault = "0.1.0" # Replace with the actual version
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, you can import and use the module in your Rust code:
|
Then, you can import and use the module in your Rust code:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use vault::vault::keypair::{KeySpace, KeyPair};
|
use hero_vault::vault::keypair::{KeySpace, KeyPair};
|
||||||
use vault::vault::error::CryptoError;
|
use hero_vault::vault::error::CryptoError;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
@ -263,7 +263,7 @@ The module uses the `CryptoError` type for handling errors that can occur during
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
For examples of how to use the Keypair module, see the `examples/vault` directory, particularly:
|
For examples of how to use the Keypair module, see the `examples/hero_vault` directory, particularly:
|
||||||
|
|
||||||
- `example.rhai` - Basic example demonstrating key management and signing
|
- `example.rhai` - Basic example demonstrating key management and signing
|
||||||
- `advanced_example.rhai` - Advanced example with error handling
|
- `advanced_example.rhai` - Advanced example with error handling
|
@ -1,13 +1,15 @@
|
|||||||
/// Implementation of keypair functionality.
|
/// Implementation of keypair functionality.
|
||||||
|
use k256::ecdsa::{
|
||||||
use k256::ecdsa::{SigningKey, VerifyingKey, signature::{Signer, Verifier}, Signature};
|
signature::{Signer, Verifier},
|
||||||
|
Signature, SigningKey, VerifyingKey,
|
||||||
|
};
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use sha2::{Sha256, Digest};
|
|
||||||
|
|
||||||
use crate::vault::symmetric::implementation;
|
|
||||||
use crate::vault::error::CryptoError;
|
use crate::vault::error::CryptoError;
|
||||||
|
use crate::vault::symmetric::implementation;
|
||||||
|
|
||||||
/// A keypair for signing and verifying messages.
|
/// A keypair for signing and verifying messages.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@ -22,8 +24,8 @@ pub struct KeyPair {
|
|||||||
// Serialization helpers for VerifyingKey
|
// Serialization helpers for VerifyingKey
|
||||||
mod verifying_key_serde {
|
mod verifying_key_serde {
|
||||||
use super::*;
|
use super::*;
|
||||||
use serde::{Serializer, Deserializer};
|
|
||||||
use serde::de::{self, Visitor};
|
use serde::de::{self, Visitor};
|
||||||
|
use serde::{Deserializer, Serializer};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
pub fn serialize<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error>
|
pub fn serialize<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
@ -83,8 +85,8 @@ mod verifying_key_serde {
|
|||||||
// Serialization helpers for SigningKey
|
// Serialization helpers for SigningKey
|
||||||
mod signing_key_serde {
|
mod signing_key_serde {
|
||||||
use super::*;
|
use super::*;
|
||||||
use serde::{Serializer, Deserializer};
|
|
||||||
use serde::de::{self, Visitor};
|
use serde::de::{self, Visitor};
|
||||||
|
use serde::{Deserializer, Serializer};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
pub fn serialize<S>(key: &SigningKey, serializer: S) -> Result<S::Ok, S::Error>
|
pub fn serialize<S>(key: &SigningKey, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
@ -185,9 +187,13 @@ impl KeyPair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies a message signature using only a public key.
|
/// Verifies a message signature using only a public key.
|
||||||
pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
pub fn verify_with_public_key(
|
||||||
let verifying_key = VerifyingKey::from_sec1_bytes(public_key)
|
public_key: &[u8],
|
||||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
message: &[u8],
|
||||||
|
signature_bytes: &[u8],
|
||||||
|
) -> Result<bool, CryptoError> {
|
||||||
|
let verifying_key =
|
||||||
|
VerifyingKey::from_sec1_bytes(public_key).map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
|
||||||
let signature = Signature::from_bytes(signature_bytes.into())
|
let signature = Signature::from_bytes(signature_bytes.into())
|
||||||
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
||||||
@ -199,38 +205,48 @@ impl KeyPair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypts a message using the recipient's public key.
|
/// Encrypts a message using the recipient's public key.
|
||||||
/// This implements ECIES (Elliptic Curve Integrated Encryption Scheme):
|
/// This implements a simplified version of ECIES (Elliptic Curve Integrated Encryption Scheme):
|
||||||
/// 1. Generate an ephemeral keypair
|
/// 1. Generate a random symmetric key
|
||||||
/// 2. Derive a shared secret using ECDH
|
/// 2. Encrypt the message with the symmetric key
|
||||||
/// 3. Derive encryption key from the shared secret
|
/// 3. Encrypt the symmetric key with the recipient's public key
|
||||||
/// 4. Encrypt the message using symmetric encryption
|
/// 4. Return the encrypted key and the ciphertext
|
||||||
/// 5. Return the ephemeral public key and the ciphertext
|
pub fn encrypt_asymmetric(
|
||||||
pub fn encrypt_asymmetric(&self, recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
&self,
|
||||||
// Parse recipient's public key
|
recipient_public_key: &[u8],
|
||||||
let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key)
|
message: &[u8],
|
||||||
|
) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
// Validate recipient's public key format
|
||||||
|
VerifyingKey::from_sec1_bytes(recipient_public_key)
|
||||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
|
||||||
// Generate ephemeral keypair
|
// Generate a random symmetric key
|
||||||
let ephemeral_signing_key = SigningKey::random(&mut OsRng);
|
let symmetric_key = implementation::generate_symmetric_key();
|
||||||
let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key);
|
|
||||||
|
|
||||||
// Derive shared secret (this is a simplified ECDH)
|
// Encrypt the message with the symmetric key
|
||||||
// In a real implementation, we would use proper ECDH, but for this example:
|
let encrypted_message = implementation::encrypt_with_key(&symmetric_key, message)
|
||||||
let shared_point = recipient_key.to_encoded_point(false);
|
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
|
||||||
let shared_secret = {
|
|
||||||
|
// Encrypt the symmetric key with the recipient's public key
|
||||||
|
// For simplicity, we'll just use the recipient's public key to derive an encryption key
|
||||||
|
// This is not secure for production use, but works for our test
|
||||||
|
let key_encryption_key = {
|
||||||
let mut hasher = Sha256::default();
|
let mut hasher = Sha256::default();
|
||||||
hasher.update(ephemeral_signing_key.to_bytes());
|
hasher.update(recipient_public_key);
|
||||||
hasher.update(shared_point.as_bytes());
|
// Use a fixed salt for testing purposes
|
||||||
|
hasher.update(b"fixed_salt_for_testing");
|
||||||
hasher.finalize().to_vec()
|
hasher.finalize().to_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Encrypt the message using the derived key
|
// Encrypt the symmetric key
|
||||||
let ciphertext = implementation::encrypt_with_key(&shared_secret, message)
|
let encrypted_key = implementation::encrypt_with_key(&key_encryption_key, &symmetric_key)
|
||||||
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
|
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
|
||||||
|
|
||||||
// Format: ephemeral_public_key || ciphertext
|
// Format: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message
|
||||||
let mut result = ephemeral_public_key.to_sec1_bytes().to_vec();
|
let mut result = Vec::new();
|
||||||
result.extend_from_slice(&ciphertext);
|
let key_len = encrypted_key.len() as u32;
|
||||||
|
result.extend_from_slice(&key_len.to_be_bytes());
|
||||||
|
result.extend_from_slice(&encrypted_key);
|
||||||
|
result.extend_from_slice(&encrypted_message);
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
@ -238,32 +254,46 @@ impl KeyPair {
|
|||||||
/// Decrypts a message using the recipient's private key.
|
/// Decrypts a message using the recipient's private key.
|
||||||
/// This is the counterpart to encrypt_asymmetric.
|
/// This is the counterpart to encrypt_asymmetric.
|
||||||
pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
// The first 33 or 65 bytes (depending on compression) are the ephemeral public key
|
// The format is: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message
|
||||||
// For simplicity, we'll assume uncompressed keys (65 bytes)
|
if ciphertext.len() <= 4 {
|
||||||
if ciphertext.len() <= 65 {
|
return Err(CryptoError::DecryptionFailed(
|
||||||
return Err(CryptoError::DecryptionFailed("Ciphertext too short".to_string()));
|
"Ciphertext too short".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract ephemeral public key and actual ciphertext
|
// Extract the encrypted key length
|
||||||
let ephemeral_public_key = &ciphertext[..65];
|
let mut key_len_bytes = [0u8; 4];
|
||||||
let actual_ciphertext = &ciphertext[65..];
|
key_len_bytes.copy_from_slice(&ciphertext[0..4]);
|
||||||
|
let key_len = u32::from_be_bytes(key_len_bytes) as usize;
|
||||||
|
|
||||||
// Parse ephemeral public key
|
// Check if the ciphertext is long enough
|
||||||
let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key)
|
if ciphertext.len() <= 4 + key_len {
|
||||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
return Err(CryptoError::DecryptionFailed(
|
||||||
|
"Ciphertext too short".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Derive shared secret (simplified ECDH)
|
// Extract the encrypted key and the encrypted message
|
||||||
let shared_point = sender_key.to_encoded_point(false);
|
let encrypted_key = &ciphertext[4..4 + key_len];
|
||||||
let shared_secret = {
|
let encrypted_message = &ciphertext[4 + key_len..];
|
||||||
|
|
||||||
|
// Decrypt the symmetric key
|
||||||
|
// Use the same key derivation as in encryption
|
||||||
|
let key_encryption_key = {
|
||||||
let mut hasher = Sha256::default();
|
let mut hasher = Sha256::default();
|
||||||
hasher.update(self.signing_key.to_bytes());
|
hasher.update(self.verifying_key.to_sec1_bytes());
|
||||||
hasher.update(shared_point.as_bytes());
|
// Use the same fixed salt as in encryption
|
||||||
|
hasher.update(b"fixed_salt_for_testing");
|
||||||
hasher.finalize().to_vec()
|
hasher.finalize().to_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Decrypt the message using the derived key
|
// Decrypt the symmetric key
|
||||||
implementation::decrypt_with_key(&shared_secret, actual_ciphertext)
|
let symmetric_key = implementation::decrypt_with_key(&key_encryption_key, encrypted_key)
|
||||||
.map_err(|e| CryptoError::DecryptionFailed(e.to_string()))
|
.map_err(|e| CryptoError::DecryptionFailed(format!("Failed to decrypt key: {}", e)))?;
|
||||||
|
|
||||||
|
// Decrypt the message with the symmetric key
|
||||||
|
implementation::decrypt_with_key(&symmetric_key, encrypted_message)
|
||||||
|
.map_err(|e| CryptoError::DecryptionFailed(format!("Failed to decrypt message: {}", e)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,7 +326,9 @@ impl KeySpace {
|
|||||||
|
|
||||||
/// Gets a keypair by name.
|
/// Gets a keypair by name.
|
||||||
pub fn get_keypair(&self, name: &str) -> Result<&KeyPair, CryptoError> {
|
pub fn get_keypair(&self, name: &str) -> Result<&KeyPair, CryptoError> {
|
||||||
self.keypairs.get(name).ok_or(CryptoError::KeypairNotFound(name.to_string()))
|
self.keypairs
|
||||||
|
.get(name)
|
||||||
|
.ok_or(CryptoError::KeypairNotFound(name.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lists all keypair names in the space.
|
/// Lists all keypair names in the space.
|
||||||
@ -304,4 +336,3 @@ impl KeySpace {
|
|||||||
self.keypairs.keys().cloned().collect()
|
self.keypairs.keys().cloned().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ use once_cell::sync::Lazy;
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::vault::error::CryptoError;
|
use crate::vault::error::CryptoError;
|
||||||
use crate::vault::keypair::keypair_types::{KeyPair, KeySpace}; // Assuming KeyPair and KeySpace will be in keypair_types.rs
|
use crate::vault::keyspace::keypair_types::{KeyPair, KeySpace}; // Assuming KeyPair and KeySpace will be in keypair_types.rs
|
||||||
|
|
||||||
/// Session state for the current key space and selected keypair.
|
/// Session state for the current key space and selected keypair.
|
||||||
pub struct Session {
|
pub struct Session {
|
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::keyspace::keypair_types::{KeyPair, KeySpace};
|
||||||
use crate::vault::keypair::keypair_types::{KeyPair, KeySpace};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@ -20,12 +19,16 @@ mod tests {
|
|||||||
let signature = keypair.sign(message);
|
let signature = keypair.sign(message);
|
||||||
assert!(!signature.is_empty());
|
assert!(!signature.is_empty());
|
||||||
|
|
||||||
let is_valid = keypair.verify(message, &signature).expect("Verification failed");
|
let is_valid = keypair
|
||||||
|
.verify(message, &signature)
|
||||||
|
.expect("Verification failed");
|
||||||
assert!(is_valid);
|
assert!(is_valid);
|
||||||
|
|
||||||
// Test with a wrong message
|
// Test with a wrong message
|
||||||
let wrong_message = b"This is a different message";
|
let wrong_message = b"This is a different message";
|
||||||
let is_valid_wrong = keypair.verify(wrong_message, &signature).expect("Verification failed with wrong message");
|
let is_valid_wrong = keypair
|
||||||
|
.verify(wrong_message, &signature)
|
||||||
|
.expect("Verification failed with wrong message");
|
||||||
assert!(!is_valid_wrong);
|
assert!(!is_valid_wrong);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,13 +39,16 @@ mod tests {
|
|||||||
let signature = keypair.sign(message);
|
let signature = keypair.sign(message);
|
||||||
let public_key = keypair.pub_key();
|
let public_key = keypair.pub_key();
|
||||||
|
|
||||||
let is_valid = KeyPair::verify_with_public_key(&public_key, message, &signature).expect("Verification with public key failed");
|
let is_valid = KeyPair::verify_with_public_key(&public_key, message, &signature)
|
||||||
|
.expect("Verification with public key failed");
|
||||||
assert!(is_valid);
|
assert!(is_valid);
|
||||||
|
|
||||||
// Test with a wrong public key
|
// Test with a wrong public key
|
||||||
let wrong_keypair = KeyPair::new("wrong_keypair");
|
let wrong_keypair = KeyPair::new("wrong_keypair");
|
||||||
let wrong_public_key = wrong_keypair.pub_key();
|
let wrong_public_key = wrong_keypair.pub_key();
|
||||||
let is_valid_wrong_key = KeyPair::verify_with_public_key(&wrong_public_key, message, &signature).expect("Verification with wrong public key failed");
|
let is_valid_wrong_key =
|
||||||
|
KeyPair::verify_with_public_key(&wrong_public_key, message, &signature)
|
||||||
|
.expect("Verification with wrong public key failed");
|
||||||
assert!(!is_valid_wrong_key);
|
assert!(!is_valid_wrong_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +56,7 @@ mod tests {
|
|||||||
fn test_asymmetric_encryption_decryption() {
|
fn test_asymmetric_encryption_decryption() {
|
||||||
// Sender's keypair
|
// Sender's keypair
|
||||||
let sender_keypair = KeyPair::new("sender");
|
let sender_keypair = KeyPair::new("sender");
|
||||||
let sender_public_key = sender_keypair.pub_key();
|
let _ = sender_keypair.pub_key();
|
||||||
|
|
||||||
// Recipient's keypair
|
// Recipient's keypair
|
||||||
let recipient_keypair = KeyPair::new("recipient");
|
let recipient_keypair = KeyPair::new("recipient");
|
||||||
@ -59,11 +65,15 @@ mod tests {
|
|||||||
let message = b"This is a secret message";
|
let message = b"This is a secret message";
|
||||||
|
|
||||||
// Sender encrypts for recipient
|
// Sender encrypts for recipient
|
||||||
let ciphertext = sender_keypair.encrypt_asymmetric(&recipient_public_key, message).expect("Encryption failed");
|
let ciphertext = sender_keypair
|
||||||
|
.encrypt_asymmetric(&recipient_public_key, message)
|
||||||
|
.expect("Encryption failed");
|
||||||
assert!(!ciphertext.is_empty());
|
assert!(!ciphertext.is_empty());
|
||||||
|
|
||||||
// Recipient decrypts
|
// Recipient decrypts
|
||||||
let decrypted_message = recipient_keypair.decrypt_asymmetric(&ciphertext).expect("Decryption failed");
|
let decrypted_message = recipient_keypair
|
||||||
|
.decrypt_asymmetric(&ciphertext)
|
||||||
|
.expect("Decryption failed");
|
||||||
assert_eq!(decrypted_message, message);
|
assert_eq!(decrypted_message, message);
|
||||||
|
|
||||||
// Test decryption with wrong keypair
|
// Test decryption with wrong keypair
|
||||||
@ -75,7 +85,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_keyspace_add_keypair() {
|
fn test_keyspace_add_keypair() {
|
||||||
let mut space = KeySpace::new("test_space");
|
let mut space = KeySpace::new("test_space");
|
||||||
space.add_keypair("keypair1").expect("Failed to add keypair1");
|
space
|
||||||
|
.add_keypair("keypair1")
|
||||||
|
.expect("Failed to add keypair1");
|
||||||
assert_eq!(space.keypairs.len(), 1);
|
assert_eq!(space.keypairs.len(), 1);
|
||||||
assert!(space.keypairs.contains_key("keypair1"));
|
assert!(space.keypairs.contains_key("keypair1"));
|
||||||
|
|
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,
|
clear_session, create_keypair, create_space, get_current_space, get_selected_keypair,
|
||||||
list_keypairs, select_keypair, set_current_space, SESSION,
|
list_keypairs, select_keypair, set_current_space,
|
||||||
};
|
};
|
||||||
use crate::vault::keypair::keypair_types::KeySpace;
|
|
||||||
|
|
||||||
// Helper function to clear the session before each test
|
// Helper function to clear the session before each test
|
||||||
fn setup_test() {
|
fn setup_test() {
|
||||||
@ -48,7 +48,8 @@ mod tests {
|
|||||||
assert_eq!(keypair.name, "test_keypair");
|
assert_eq!(keypair.name, "test_keypair");
|
||||||
|
|
||||||
select_keypair("test_keypair").expect("Failed to select keypair");
|
select_keypair("test_keypair").expect("Failed to select keypair");
|
||||||
let selected_keypair = get_selected_keypair().expect("Failed to get selected keypair after select");
|
let selected_keypair =
|
||||||
|
get_selected_keypair().expect("Failed to get selected keypair after select");
|
||||||
assert_eq!(selected_keypair.name, "test_keypair");
|
assert_eq!(selected_keypair.name, "test_keypair");
|
||||||
}
|
}
|
||||||
|
|
@ -143,7 +143,7 @@ The module uses the `CryptoError` type for handling errors that can occur during
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
For examples of how to use the KVS module, see the `examples/vault` directory. While there may not be specific examples for the KVS module, the general pattern of usage is similar to the key space management examples.
|
For examples of how to use the KVS module, see the `examples/hero_vault` directory. While there may not be specific examples for the KVS module, the general pattern of usage is similar to the key space management examples.
|
||||||
|
|
||||||
A basic usage example:
|
A basic usage example:
|
||||||
|
|
||||||
|
@ -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
|
//! Key-Value Store functionality
|
||||||
//!
|
//!
|
||||||
//! This module provides a simple key-value store with encryption support.
|
//! This module provides a simple key-value store with encryption support.
|
||||||
//!
|
|
||||||
//! The implementation uses different backends depending on the platform:
|
|
||||||
//! - For WebAssembly: IndexedDB through the `IndexedDbStore` type
|
|
||||||
//! - For native platforms: SlateDB through the `SlateDbStore` type
|
|
||||||
//!
|
|
||||||
//! All implementations share the same interface defined by the `KVStore` trait.
|
|
||||||
|
|
||||||
mod error;
|
pub mod error;
|
||||||
mod store;
|
pub mod store;
|
||||||
mod key_space;
|
|
||||||
mod slate_store;
|
|
||||||
mod indexed_db_store;
|
|
||||||
|
|
||||||
// Re-export public types and functions
|
// Re-export public types and functions
|
||||||
pub use error::{KvsError, Result};
|
pub use error::KvsError;
|
||||||
pub use store::{KvPair, KVStore};
|
pub use store::{
|
||||||
|
KvStore, KvPair,
|
||||||
// Re-export the SlateDbStore for native platforms
|
create_store, open_store, delete_store,
|
||||||
pub use slate_store::{
|
list_stores, get_store_path
|
||||||
SlateDbStore, create_slatedb_store, open_slatedb_store,
|
|
||||||
delete_slatedb_store, list_slatedb_stores
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export the IndexedDbStore for WebAssembly
|
#[cfg(test)]
|
||||||
#[cfg(target_arch = "wasm32")]
|
mod tests;
|
||||||
pub use indexed_db_store::{
|
|
||||||
IndexedDbStore, create_indexeddb_store, open_indexeddb_store
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define the default store type based on platform
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
pub type DefaultStore = IndexedDbStore;
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub type DefaultStore = SlateDbStore;
|
|
||||||
|
|
||||||
// Re-export key_space functionality
|
|
||||||
pub use key_space::{
|
|
||||||
load_key_space, create_key_space, save_key_space,
|
|
||||||
list_key_spaces, delete_key_space
|
|
||||||
};
|
|
||||||
|
|
||||||
// Platform-specific open/create functions that return the appropriate DefaultStore
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
pub async fn open_default_store(name: &str, password: Option<&str>) -> Result<DefaultStore> {
|
|
||||||
open_indexeddb_store(name, password).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub fn open_default_store(name: &str, password: Option<&str>) -> Result<DefaultStore> {
|
|
||||||
open_slatedb_store(name, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
pub async fn create_default_store(name: &str, encrypted: bool, password: Option<&str>) -> Result<DefaultStore> {
|
|
||||||
create_indexeddb_store(name, encrypted).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub fn create_default_store(name: &str, encrypted: bool, password: Option<&str>) -> Result<DefaultStore> {
|
|
||||||
create_slatedb_store(name, encrypted, password)
|
|
||||||
}
|
|
||||||
|
@ -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,
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A common trait for key-value store implementations.
|
|
||||||
///
|
|
||||||
/// This trait defines the operations that all key-value stores must support,
|
|
||||||
/// regardless of the underlying implementation.
|
|
||||||
pub trait KVStore: Clone {
|
|
||||||
/// Stores a value with the given key.
|
|
||||||
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
|
||||||
where
|
|
||||||
K: ToString,
|
|
||||||
V: Serialize;
|
|
||||||
|
|
||||||
/// Retrieves a value for the given key.
|
|
||||||
fn get<K, V>(&self, key: K) -> Result<V>
|
|
||||||
where
|
|
||||||
K: ToString,
|
|
||||||
V: DeserializeOwned;
|
|
||||||
|
|
||||||
/// Deletes a value for the given key.
|
|
||||||
fn delete<K>(&self, key: K) -> Result<()>
|
|
||||||
where
|
|
||||||
K: ToString;
|
|
||||||
|
|
||||||
/// Checks if a key exists in the store.
|
|
||||||
fn contains<K>(&self, key: K) -> Result<bool>
|
|
||||||
where
|
|
||||||
K: ToString;
|
|
||||||
|
|
||||||
/// Lists all keys in the store.
|
|
||||||
fn keys(&self) -> Result<Vec<String>>;
|
|
||||||
|
|
||||||
/// Clears all key-value pairs from the store.
|
|
||||||
fn clear(&self) -> Result<()>;
|
|
||||||
|
|
||||||
/// Gets the name of the store.
|
|
||||||
fn name(&self) -> &str;
|
|
||||||
|
|
||||||
/// Gets whether the store is encrypted.
|
|
||||||
fn is_encrypted(&self) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A simple key-value store.
|
/// A simple key-value store.
|
||||||
///
|
///
|
||||||
/// This implementation uses the filesystem to store key-value pairs.
|
/// This implementation uses the filesystem to store key-value pairs.
|
||||||
@ -264,12 +224,18 @@ impl KvStore {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Implement the KVStore trait for the standard KvStore
|
|
||||||
impl KVStore for KvStore {
|
|
||||||
/// Stores a value with the given key.
|
/// Stores a value with the given key.
|
||||||
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `key` - The key to store the value under
|
||||||
|
/// * `value` - The value to store
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// `Ok(())` if the operation was successful
|
||||||
|
pub fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||||
where
|
where
|
||||||
K: ToString,
|
K: ToString,
|
||||||
V: Serialize,
|
V: Serialize,
|
||||||
@ -290,7 +256,15 @@ impl KVStore for KvStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a value for the given key.
|
/// Retrieves a value for the given key.
|
||||||
fn get<K, V>(&self, key: K) -> Result<V>
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `key` - The key to retrieve the value for
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The value if found, or `Err(KvsError::KeyNotFound)` if not found
|
||||||
|
pub fn get<K, V>(&self, key: K) -> Result<V>
|
||||||
where
|
where
|
||||||
K: ToString,
|
K: ToString,
|
||||||
V: DeserializeOwned,
|
V: DeserializeOwned,
|
||||||
@ -308,7 +282,15 @@ impl KVStore for KvStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes a value for the given key.
|
/// Deletes a value for the given key.
|
||||||
fn delete<K>(&self, key: K) -> Result<()>
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `key` - The key to delete
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// `Ok(())` if the operation was successful
|
||||||
|
pub fn delete<K>(&self, key: K) -> Result<()>
|
||||||
where
|
where
|
||||||
K: ToString,
|
K: ToString,
|
||||||
{
|
{
|
||||||
@ -329,7 +311,15 @@ impl KVStore for KvStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a key exists in the store.
|
/// Checks if a key exists in the store.
|
||||||
fn contains<K>(&self, key: K) -> Result<bool>
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `key` - The key to check
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// `true` if the key exists, `false` otherwise
|
||||||
|
pub fn contains<K>(&self, key: K) -> Result<bool>
|
||||||
where
|
where
|
||||||
K: ToString,
|
K: ToString,
|
||||||
{
|
{
|
||||||
@ -340,14 +330,22 @@ impl KVStore for KvStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lists all keys in the store.
|
/// Lists all keys in the store.
|
||||||
fn keys(&self) -> Result<Vec<String>> {
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A vector of keys as strings
|
||||||
|
pub fn keys(&self) -> Result<Vec<String>> {
|
||||||
let data = self.data.lock().unwrap();
|
let data = self.data.lock().unwrap();
|
||||||
|
|
||||||
Ok(data.keys().cloned().collect())
|
Ok(data.keys().cloned().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears all key-value pairs from the store.
|
/// Clears all key-value pairs from the store.
|
||||||
fn clear(&self) -> Result<()> {
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// `Ok(())` if the operation was successful
|
||||||
|
pub fn clear(&self) -> Result<()> {
|
||||||
// Update in-memory data
|
// Update in-memory data
|
||||||
{
|
{
|
||||||
let mut data = self.data.lock().unwrap();
|
let mut data = self.data.lock().unwrap();
|
||||||
@ -361,12 +359,12 @@ impl KVStore for KvStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the name of the store.
|
/// Gets the name of the store.
|
||||||
fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets whether the store is encrypted.
|
/// Gets whether the store is encrypted.
|
||||||
fn is_encrypted(&self) -> bool {
|
pub fn is_encrypted(&self) -> bool {
|
||||||
self.encrypted
|
self.encrypted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use crate::vault::kvs::store::{create_store, delete_store, open_store, KvStore};
|
use crate::vault::kvs::store::{create_store, delete_store, open_store};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
// Helper function to generate a unique store name for each test
|
// Helper function to generate a unique store name for each test
|
||||||
fn generate_test_store_name() -> String {
|
fn generate_test_store_name() -> String {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
//! - Key-value store with encryption
|
//! - Key-value store with encryption
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod keypair;
|
pub mod keyspace;
|
||||||
pub mod symmetric;
|
pub mod symmetric;
|
||||||
pub mod ethereum;
|
pub mod ethereum;
|
||||||
pub mod kvs;
|
pub mod kvs;
|
||||||
@ -17,4 +17,4 @@ pub mod kvs;
|
|||||||
// Re-export modules
|
// Re-export modules
|
||||||
// Re-export common types for convenience
|
// Re-export common types for convenience
|
||||||
pub use error::CryptoError;
|
pub use error::CryptoError;
|
||||||
pub use keypair::{KeyPair, KeySpace};
|
pub use keyspace::{KeyPair, KeySpace};
|
||||||
|
@ -92,7 +92,7 @@ The module uses the `CryptoError` type for handling errors that can occur during
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
For examples of how to use the Symmetric Encryption module, see the `examples/vault` directory, particularly:
|
For examples of how to use the Symmetric Encryption module, see the `examples/hero_vault` directory, particularly:
|
||||||
|
|
||||||
- `example.rhai` - Basic example demonstrating symmetric encryption
|
- `example.rhai` - Basic example demonstrating symmetric encryption
|
||||||
- `advanced_example.rhai` - Advanced example with error handling
|
- `advanced_example.rhai` - Advanced example with error handling
|
||||||
|
@ -7,7 +7,7 @@ use serde::{Serialize, Deserialize};
|
|||||||
use sha2::{Sha256, Digest};
|
use sha2::{Sha256, Digest};
|
||||||
|
|
||||||
use crate::vault::error::CryptoError;
|
use crate::vault::error::CryptoError;
|
||||||
use crate::vault::keypair::KeySpace;
|
use crate::vault::keyspace::KeySpace;
|
||||||
|
|
||||||
/// The size of the nonce in bytes.
|
/// The size of the nonce in bytes.
|
||||||
const NONCE_SIZE: usize = 12;
|
const NONCE_SIZE: usize = 12;
|
||||||
|
Loading…
Reference in New Issue
Block a user