diff --git a/examples/hero_vault/add_custom_network.rhai b/examples/hero_vault/add_custom_network.rhai new file mode 100644 index 0000000..5755aa8 --- /dev/null +++ b/examples/hero_vault/add_custom_network.rhai @@ -0,0 +1,125 @@ +// 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!"); diff --git a/src/hero_vault/ethereum/README.md b/src/hero_vault/ethereum/README.md index 34fb2dd..6fa05c4 100644 --- a/src/hero_vault/ethereum/README.md +++ b/src/hero_vault/ethereum/README.md @@ -21,25 +21,25 @@ The Ethereum module is organized into several components: The module provides functionality for creating and managing Ethereum wallets: ```rust -// Create a new Ethereum wallet for a specific network -let wallet = create_ethereum_wallet_for_network("Ethereum")?; - -// Create a wallet for specific networks -let peaq_wallet = create_peaq_wallet()?; -let agung_wallet = create_agung_wallet()?; +// Create a network-independent Ethereum wallet +let wallet = create_ethereum_wallet()?; // Create a wallet with a specific name -let named_wallet = create_ethereum_wallet_from_name_for_network("my_wallet", "Gnosis")?; +let named_wallet = create_ethereum_wallet_from_name("my_wallet")?; // Create a wallet from a private key let imported_wallet = create_ethereum_wallet_from_private_key("0x...")?; -// Get the current wallet for a network -let current_wallet = get_current_ethereum_wallet_for_network("Ethereum")?; +// Get the current wallet +let current_wallet = get_current_ethereum_wallet()?; // Clear 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 @@ -47,6 +47,12 @@ clear_ethereum_wallets_for_network("Gnosis")?; The module supports multiple Ethereum networks and provides functionality for managing network configurations: ```rust +// Register a new network +register_network("Arbitrum", 42161, "https://arb1.arbitrum.io/rpc", "https://arbiscan.io", "ETH", 18); + +// Remove a network +remove_network("Arbitrum"); + // Get a network configuration by name let network = get_network_by_name("Ethereum")?; @@ -68,7 +74,10 @@ The module provides functionality for creating and managing Ethereum providers: // Create a provider for a specific network let provider = create_provider("Ethereum")?; -// Create providers for specific networks +// Create a provider from a network configuration +let provider = create_provider_from_config(&network)?; + +// Legacy functions for backward compatibility let gnosis_provider = create_gnosis_provider()?; let peaq_provider = create_peaq_provider()?; let agung_provider = create_agung_provider()?; @@ -79,14 +88,20 @@ let agung_provider = create_agung_provider()?; The module provides functionality for managing Ethereum transactions: ```rust -// Get the balance of an address -let balance = get_balance("Ethereum", "0x...")?; +// Get the balance of an address on a specific network +let balance = get_balance("Ethereum", address).await?; -// Send ETH to an address -let tx_hash = send_eth("Ethereum", "0x...", "1000000000000000")?; +// Get the balance using a provider +let balance = get_balance_with_provider(&provider, address).await?; + +// Send ETH to an address on a specific network +let tx_hash = send_eth(&wallet, "Ethereum", to_address, amount).await?; + +// Legacy function for backward compatibility +let tx_hash = send_eth_with_provider(&wallet, &provider, to_address, amount).await?; // Format a balance for display -let formatted = format_balance(balance, 18)?; // Convert wei to ETH +let formatted = format_balance(balance, &network); ``` ### Smart Contract Interactions @@ -98,16 +113,16 @@ The module provides functionality for interacting with smart contracts: let abi = load_abi_from_json(json_string)?; // Create a contract instance -let contract = Contract::new(provider, "0x...", abi)?; +let contract = Contract::new(address, abi, network); // Call a read-only function -let result = call_read_function(contract, "balanceOf", vec!["0x..."])?; +let result = call_read_function(&contract, &provider, "balanceOf", tokens).await?; // Call a write function -let tx_hash = call_write_function(contract, "transfer", vec!["0x...", "1000"])?; +let tx_hash = call_write_function(&contract, &wallet, &provider, "transfer", tokens).await?; // Estimate gas for a function call -let gas = estimate_gas(contract, "transfer", vec!["0x...", "1000"])?; +let gas = estimate_gas(&contract, &provider, "transfer", tokens).await?; ``` ### Contract Utilities @@ -119,29 +134,49 @@ The module provides utilities for working with contract function arguments and r let token = convert_rhai_to_token(value)?; // Prepare function arguments -let args = prepare_function_arguments(function, vec![arg1, arg2])?; +let args = prepare_function_arguments(&abi, function_name, &args)?; // Convert Ethereum tokens to Rhai values -let rhai_value = convert_token_to_rhai(token)?; +let rhai_value = convert_token_to_rhai(&token)?; // Convert a token to a dynamic value -let dynamic = token_to_dynamic(token)?; +let dynamic = token_to_dynamic(&token)?; ``` -## Supported Networks +## Network Registry -The module supports multiple Ethereum networks, including: +The module now includes a centralized network registry that allows for dynamic management of EVM-compatible networks: -- Gnosis Chain -- Peaq Network -- Agung Network +```rust +// Built-in networks +- Gnosis Chain (chain_id: 100, token: xDAI) +- Peaq Network (chain_id: 3338, token: PEAQ) +- Agung Network (chain_id: 9990, token: AGNG) -Each network has its own configuration, including: +// Register a custom network at runtime +register_network( + "Polygon", + 137, + "https://polygon-rpc.com", + "https://polygonscan.com", + "MATIC", + 18 +); +``` -- RPC URL -- Chain ID -- Explorer URL -- Native currency symbol and decimals +## Wallet Address Consistency + +In the new design, Ethereum wallet addresses are consistent across all EVM-compatible networks. This reflects how Ethereum addresses work in reality - the same private key generates the same address on all EVM chains. + +```rust +// Create a wallet once +let wallet = create_ethereum_wallet()?; + +// Use the same wallet address on any network +let eth_balance = get_balance("Ethereum", wallet.address).await?; +let polygon_balance = get_balance("Polygon", wallet.address).await?; +let gnosis_balance = get_balance("Gnosis", wallet.address).await?; +``` ## Error Handling @@ -149,6 +184,8 @@ The module uses the `CryptoError` type for handling errors that can occur during - `InvalidAddress` - Invalid Ethereum address format - `ContractError` - Smart contract interaction error +- `NoKeypairSelected` - No keypair selected for wallet creation +- `InvalidKeyLength` - Invalid private key length ## Examples @@ -158,3 +195,31 @@ For examples of how to use the Ethereum module, see the `examples/hero_vault` di - `agung_simple_transfer.rhai` - Shows how to perform a simple ETH transfer on the Agung network - `agung_send_transaction.rhai` - Demonstrates sending transactions on the Agung network - `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung + +## Adding a New Network + +With the new design, adding a new network is as simple as registering it with the network registry: + +```rust +// In Rust +ethereum::register_network( + "Optimism", + 10, + "https://mainnet.optimism.io", + "https://optimistic.etherscan.io", + "ETH", + 18 +); + +// In Rhai +register_network( + "Optimism", + 10, + "https://mainnet.optimism.io", + "https://optimistic.etherscan.io", + "ETH", + 18 +); +``` + +No code changes are required to add support for new networks! diff --git a/src/hero_vault/ethereum/contract.rs b/src/hero_vault/ethereum/contract.rs index e4ab661..7eed972 100644 --- a/src/hero_vault/ethereum/contract.rs +++ b/src/hero_vault/ethereum/contract.rs @@ -42,7 +42,7 @@ impl Contract { } /// Creates an ethers Contract instance for interaction. - pub fn create_ethers_contract(&self, provider: Provider, _wallet: Option<&EthereumWallet>) -> Result>, CryptoError> { + pub fn create_ethers_contract(&self, provider: Provider) -> Result>, CryptoError> { let contract = ethers::contract::Contract::new( self.address, self.abi.clone(), @@ -65,9 +65,9 @@ pub async fn call_read_function( provider: &Provider, function_name: &str, args: Vec, -) -> Result, CryptoError> { +) -> Result { // Create the ethers contract (not used directly but kept for future extensions) - let _ethers_contract = contract.create_ethers_contract(provider.clone(), None)?; + let _ethers_contract = contract.create_ethers_contract(provider.clone())?; // Get the function from the ABI let function = contract.abi.function(function_name) @@ -89,7 +89,12 @@ pub async fn call_read_function( let decoded = function.decode_output(&result) .map_err(|e| CryptoError::ContractError(format!("Failed to decode function output: {}", e)))?; - Ok(decoded) + // Return the first token if there's only one, otherwise return a tuple + if decoded.len() == 1 { + Ok(decoded[0].clone()) + } else { + Ok(Token::Tuple(decoded)) + } } /// Executes a state-changing function on a contract. @@ -100,10 +105,11 @@ pub async fn call_write_function( function_name: &str, args: Vec, ) -> Result { - // Create a client with the wallet + // Create a client with the wallet configured for this network + let network_wallet = wallet.for_network(&contract.network); let client = SignerMiddleware::new( provider.clone(), - wallet.wallet.clone(), + network_wallet, ); // Get the function from the ABI @@ -126,13 +132,14 @@ pub async fn call_write_function( // Send the transaction using the client directly log::info!("Sending transaction to contract at {}", contract.address); - log::info!("Function: {}, Args: {:?}", function_name, args); + log::info!("Function: {}", function_name); // Log detailed information about the transaction log::debug!("Sending transaction to contract at {}", contract.address); log::debug!("Function: {}, Args: {:?}", function_name, args); log::debug!("From address: {}", wallet.address); log::debug!("Gas limit: {:?}", tx.gas); + log::debug!("Network: {}", contract.network.name); let pending_tx = match client.send_transaction(tx, None).await { Ok(pending_tx) => { @@ -179,5 +186,8 @@ pub async fn estimate_gas( .await .map_err(|e| CryptoError::ContractError(format!("Failed to estimate gas: {}", e)))?; - Ok(gas) + // Add a buffer to the gas estimate to account for potential variations + let gas_with_buffer = gas * 12 / 10; // Add 20% buffer + + Ok(gas_with_buffer) } diff --git a/src/hero_vault/ethereum/mod.rs b/src/hero_vault/ethereum/mod.rs index ac698b4..1cfd99c 100644 --- a/src/hero_vault/ethereum/mod.rs +++ b/src/hero_vault/ethereum/mod.rs @@ -27,27 +27,33 @@ pub use networks::NetworkConfig; // Re-export wallet creation functions pub use storage::{ + create_ethereum_wallet, + create_ethereum_wallet_from_name, + create_ethereum_wallet_from_private_key, + // Legacy functions for backward compatibility create_ethereum_wallet_for_network, create_peaq_wallet, create_agung_wallet, create_ethereum_wallet_from_name_for_network, - create_ethereum_wallet_from_name, create_ethereum_wallet_from_private_key_for_network, - create_ethereum_wallet_from_private_key, }; // Re-export wallet management functions pub use storage::{ + get_current_ethereum_wallet, + clear_ethereum_wallets, + // Legacy functions for backward compatibility get_current_ethereum_wallet_for_network, get_current_peaq_wallet, get_current_agung_wallet, - clear_ethereum_wallets, clear_ethereum_wallets_for_network, }; // Re-export provider functions pub use provider::{ create_provider, + create_provider_from_config, + // Legacy functions for backward compatibility create_gnosis_provider, create_peaq_provider, create_agung_provider, @@ -56,12 +62,16 @@ pub use provider::{ // Re-export transaction functions pub use transaction::{ get_balance, + get_balance_with_provider, send_eth, + send_eth_with_provider, format_balance, }; // Re-export network registry functions pub use networks::{ + register_network, + remove_network, get_network_by_name, get_proper_network_name, list_network_names, diff --git a/src/hero_vault/ethereum/networks.rs b/src/hero_vault/ethereum/networks.rs index 4e81655..6f86a1b 100644 --- a/src/hero_vault/ethereum/networks.rs +++ b/src/hero_vault/ethereum/networks.rs @@ -4,7 +4,8 @@ //! to work with them. use std::collections::HashMap; -use std::sync::OnceLock; +use std::sync::RwLock; +use once_cell::sync::Lazy; use serde::{Serialize, Deserialize}; /// Configuration for an EVM-compatible network @@ -18,6 +19,16 @@ pub struct NetworkConfig { pub decimals: u8, } +/// Global registry of all supported networks +static NETWORK_REGISTRY: Lazy>> = Lazy::new(|| { + let mut registry = HashMap::new(); + + // Add built-in networks + register_built_in_networks(&mut registry); + + RwLock::new(registry) +}); + /// Network name constants pub mod names { pub const GNOSIS: &str = "Gnosis"; @@ -25,50 +36,80 @@ pub mod names { pub const AGUNG: &str = "Agung"; } -/// Get the Gnosis Chain network configuration -pub fn gnosis() -> NetworkConfig { - NetworkConfig { +/// Register all built-in networks +fn register_built_in_networks(registry: &mut HashMap) { + // Gnosis Chain + registry.insert(names::GNOSIS.to_lowercase(), NetworkConfig { name: names::GNOSIS.to_string(), chain_id: 100, rpc_url: "https://rpc.gnosischain.com".to_string(), explorer_url: "https://gnosisscan.io".to_string(), token_symbol: "xDAI".to_string(), decimals: 18, - } -} - -/// Get the Peaq Network configuration -pub fn peaq() -> NetworkConfig { - NetworkConfig { + }); + + // Peaq Network + registry.insert(names::PEAQ.to_lowercase(), 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 { - NetworkConfig { + }); + + // Agung Testnet + registry.insert(names::AGUNG.to_lowercase(), NetworkConfig { name: names::AGUNG.to_string(), chain_id: 9990, rpc_url: "https://wss-async.agung.peaq.network".to_string(), explorer_url: "https://agung-testnet.subscan.io/".to_string(), token_symbol: "AGNG".to_string(), decimals: 18, + }); +} + +/// Register a new network +pub fn register_network( + name: &str, + chain_id: u64, + rpc_url: &str, + explorer_url: &str, + token_symbol: &str, + decimals: u8, +) -> bool { + let config = NetworkConfig { + name: name.to_string(), + chain_id, + rpc_url: rpc_url.to_string(), + explorer_url: explorer_url.to_string(), + token_symbol: token_symbol.to_string(), + decimals, + }; + + if let Ok(mut registry) = NETWORK_REGISTRY.write() { + registry.insert(name.to_lowercase(), config); + true + } else { + false + } +} + +/// Remove a network from the registry +pub fn remove_network(name: &str) -> bool { + if let Ok(mut registry) = NETWORK_REGISTRY.write() { + registry.remove(&name.to_lowercase()).is_some() + } else { + false } } /// Get a network by its name (case-insensitive) pub fn get_network_by_name(name: &str) -> Option { - let name_lower = name.to_lowercase(); - match name_lower.as_str() { - "gnosis" => Some(gnosis()), - "peaq" => Some(peaq()), - "agung" => Some(agung()), - _ => None, + if let Ok(registry) = NETWORK_REGISTRY.read() { + registry.get(&name.to_lowercase()).cloned() + } else { + None } } @@ -84,19 +125,57 @@ pub fn get_proper_network_name(name: &str) -> Option<&'static str> { } /// Get a list of all supported network names -pub fn list_network_names() -> Vec<&'static str> { - vec![names::GNOSIS, names::PEAQ, names::AGUNG] +pub fn list_network_names() -> Vec { + if let Ok(registry) = NETWORK_REGISTRY.read() { + registry.values().map(|config| config.name.clone()).collect() + } else { + Vec::new() + } } /// Get a map of all networks -pub fn get_all_networks() -> &'static HashMap<&'static str, NetworkConfig> { - static NETWORKS: OnceLock> = OnceLock::new(); +pub fn get_all_networks() -> HashMap { + if let Ok(registry) = NETWORK_REGISTRY.read() { + registry.clone() + } else { + HashMap::new() + } +} - NETWORKS.get_or_init(|| { - let mut map = HashMap::new(); - map.insert(names::GNOSIS, gnosis()); - map.insert(names::PEAQ, peaq()); - map.insert(names::AGUNG, agung()); - map +// Legacy functions for backward compatibility + +/// Get the Gnosis Chain network configuration +pub fn gnosis() -> NetworkConfig { + get_network_by_name("gnosis").unwrap_or_else(|| NetworkConfig { + name: names::GNOSIS.to_string(), + chain_id: 100, + rpc_url: "https://rpc.gnosischain.com".to_string(), + explorer_url: "https://gnosisscan.io".to_string(), + token_symbol: "xDAI".to_string(), + decimals: 18, + }) +} + +/// Get the Peaq Network configuration +pub fn peaq() -> NetworkConfig { + get_network_by_name("peaq").unwrap_or_else(|| NetworkConfig { + name: names::PEAQ.to_string(), + chain_id: 3338, + rpc_url: "https://peaq.api.onfinality.io/public".to_string(), + explorer_url: "https://peaq.subscan.io/".to_string(), + token_symbol: "PEAQ".to_string(), + decimals: 18, + }) +} + +/// Get the Agung Testnet configuration +pub fn agung() -> NetworkConfig { + get_network_by_name("agung").unwrap_or_else(|| NetworkConfig { + name: names::AGUNG.to_string(), + chain_id: 9990, + rpc_url: "https://wss-async.agung.peaq.network".to_string(), + explorer_url: "https://agung-testnet.subscan.io/".to_string(), + token_symbol: "AGNG".to_string(), + decimals: 18, }) } diff --git a/src/hero_vault/ethereum/provider.rs b/src/hero_vault/ethereum/provider.rs index fc7fcbd..5581fac 100644 --- a/src/hero_vault/ethereum/provider.rs +++ b/src/hero_vault/ethereum/provider.rs @@ -3,25 +3,36 @@ use ethers::prelude::*; use crate::hero_vault::error::CryptoError; -use super::networks::{self, NetworkConfig}; +use super::networks; /// Creates a provider for a specific network. -pub fn create_provider(network: &NetworkConfig) -> Result, CryptoError> { +pub fn create_provider(network_name: &str) -> Result, CryptoError> { + let network = networks::get_network_by_name(network_name) + .ok_or_else(|| CryptoError::SerializationError(format!("Unknown network: {}", network_name)))?; + Provider::::try_from(network.rpc_url.as_str()) .map_err(|e| CryptoError::SerializationError(format!("Failed to create provider for {}: {}", network.name, e))) } +/// Creates a provider for a specific network configuration. +pub fn create_provider_from_config(network: &networks::NetworkConfig) -> Result, CryptoError> { + Provider::::try_from(network.rpc_url.as_str()) + .map_err(|e| CryptoError::SerializationError(format!("Failed to create provider for {}: {}", network.name, e))) +} + +// Legacy functions for backward compatibility + /// Creates a provider for the Gnosis Chain. pub fn create_gnosis_provider() -> Result, CryptoError> { - create_provider(&networks::gnosis()) + create_provider("gnosis") } /// Creates a provider for the Peaq network. pub fn create_peaq_provider() -> Result, CryptoError> { - create_provider(&networks::peaq()) + create_provider("peaq") } /// Creates a provider for the Agung testnet. pub fn create_agung_provider() -> Result, CryptoError> { - create_provider(&networks::agung()) + create_provider("agung") } diff --git a/src/hero_vault/ethereum/storage.rs b/src/hero_vault/ethereum/storage.rs index 52b6a8c..c1d2aa1 100644 --- a/src/hero_vault/ethereum/storage.rs +++ b/src/hero_vault/ethereum/storage.rs @@ -6,60 +6,64 @@ use once_cell::sync::Lazy; use crate::hero_vault::error::CryptoError; use super::wallet::EthereumWallet; -use super::networks::{self, NetworkConfig}; +use super::networks; /// Global storage for Ethereum wallets. -static ETH_WALLETS: Lazy>>> = Lazy::new(|| { - Mutex::new(HashMap::new()) +static ETH_WALLETS: Lazy>> = Lazy::new(|| { + Mutex::new(Vec::new()) }); -/// Creates an Ethereum wallet from the currently selected keypair for a specific network. -pub fn create_ethereum_wallet_for_network(network: NetworkConfig) -> Result { +/// Creates an Ethereum wallet from the currently selected keypair. +pub fn create_ethereum_wallet() -> Result { // Get the currently selected keypair let keypair = crate::hero_vault::keypair::get_selected_keypair()?; // Create an Ethereum wallet from the keypair - let wallet = EthereumWallet::from_keypair(&keypair, network)?; + let wallet = EthereumWallet::from_keypair(&keypair)?; // 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()); + wallets.push(wallet.clone()); Ok(wallet) } -/// Creates an Ethereum wallet from the currently selected keypair for the Peaq network. -pub fn create_peaq_wallet() -> Result { - create_ethereum_wallet_for_network(networks::peaq()) +/// Creates an Ethereum wallet from a name and the currently selected keypair. +pub fn create_ethereum_wallet_from_name(name: &str) -> Result { + // Get the currently selected keypair + let keypair = crate::hero_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 = ETH_WALLETS.lock().unwrap(); + wallets.push(wallet.clone()); + + Ok(wallet) } -/// Creates an Ethereum wallet from the currently selected keypair for the Agung testnet. -pub fn create_agung_wallet() -> Result { - create_ethereum_wallet_for_network(networks::agung()) +/// Creates an Ethereum wallet from a private key. +pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result { + // Create an Ethereum wallet from the private key + let wallet = EthereumWallet::from_private_key(private_key)?; + + // Store the wallet + let mut wallets = ETH_WALLETS.lock().unwrap(); + wallets.push(wallet.clone()); + + Ok(wallet) } -/// Gets the current Ethereum wallet for a specific network. -pub fn get_current_ethereum_wallet_for_network(network_name: &str) -> Result { +/// Gets the current Ethereum wallet. +pub fn get_current_ethereum_wallet() -> Result { let wallets = ETH_WALLETS.lock().unwrap(); - let network_wallets = wallets.get(network_name).ok_or(CryptoError::NoKeypairSelected)?; - - if network_wallets.is_empty() { + if wallets.is_empty() { return Err(CryptoError::NoKeypairSelected); } - Ok(network_wallets.last().unwrap().clone()) -} - -/// Gets the current Ethereum wallet for the Peaq network. -pub fn get_current_peaq_wallet() -> Result { - get_current_ethereum_wallet_for_network("Peaq") -} - -/// Gets the current Ethereum wallet for the Agung testnet. -pub fn get_current_agung_wallet() -> Result { - get_current_ethereum_wallet_for_network("Agung") + Ok(wallets.last().unwrap().clone()) } /// Clears all Ethereum wallets. @@ -68,47 +72,50 @@ pub fn clear_ethereum_wallets() { wallets.clear(); } +// Legacy functions for backward compatibility + +/// Creates an Ethereum wallet from the currently selected keypair for a specific network. +pub fn create_ethereum_wallet_for_network(network: networks::NetworkConfig) -> Result { + create_ethereum_wallet() +} + +/// Creates an Ethereum wallet from the currently selected keypair for the Peaq network. +pub fn create_peaq_wallet() -> Result { + create_ethereum_wallet() +} + +/// Creates an Ethereum wallet from the currently selected keypair for the Agung testnet. +pub fn create_agung_wallet() -> Result { + create_ethereum_wallet() +} + +/// Gets the current Ethereum wallet for a specific network. +pub fn get_current_ethereum_wallet_for_network(network_name: &str) -> Result { + get_current_ethereum_wallet() +} + +/// Gets the current Ethereum wallet for the Peaq network. +pub fn get_current_peaq_wallet() -> Result { + get_current_ethereum_wallet() +} + +/// Gets the current Ethereum wallet for the Agung testnet. +pub fn get_current_agung_wallet() -> Result { + get_current_ethereum_wallet() +} + /// Clears Ethereum wallets for a specific network. pub fn clear_ethereum_wallets_for_network(network_name: &str) { - let mut wallets = ETH_WALLETS.lock().unwrap(); - wallets.remove(network_name); + // In the new design, we don't have network-specific wallets, + // so this is a no-op for backward compatibility } /// 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: NetworkConfig) -> Result { - // Get the currently selected keypair - let keypair = crate::hero_vault::keypair::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 { - create_ethereum_wallet_from_name_for_network(name, networks::gnosis()) +pub fn create_ethereum_wallet_from_name_for_network(name: &str, network: networks::NetworkConfig) -> Result { + create_ethereum_wallet_from_name(name) } /// 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: NetworkConfig) -> Result { - // 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 { - create_ethereum_wallet_from_private_key_for_network(private_key, networks::gnosis()) +pub fn create_ethereum_wallet_from_private_key_for_network(private_key: &str, network: networks::NetworkConfig) -> Result { + create_ethereum_wallet_from_private_key(private_key) } diff --git a/src/hero_vault/ethereum/tests/network_tests.rs b/src/hero_vault/ethereum/tests/network_tests.rs index 9afab95..8f1aa06 100644 --- a/src/hero_vault/ethereum/tests/network_tests.rs +++ b/src/hero_vault/ethereum/tests/network_tests.rs @@ -22,53 +22,123 @@ fn test_network_config() { #[test] fn test_network_registry() { - let network_names = networks::list_network_names(); - assert!(network_names.iter().any(|&name| name == "Gnosis")); - assert!(network_names.iter().any(|&name| name == "Peaq")); - assert!(network_names.iter().any(|&name| name == "Agung")); + // Get initial network names + let initial_network_names = list_network_names(); + assert!(initial_network_names.iter().any(|name| name == "Gnosis")); + assert!(initial_network_names.iter().any(|name| name == "Peaq")); + assert!(initial_network_names.iter().any(|name| name == "Agung")); - let gnosis_proper = networks::get_proper_network_name("gnosis"); - assert_eq!(gnosis_proper, Some("Gnosis")); + // Test proper network name lookup + let gnosis_proper = get_proper_network_name("gnosis"); + assert_eq!(gnosis_proper, Some(networks::names::GNOSIS)); - let peaq_proper = networks::get_proper_network_name("peaq"); - assert_eq!(peaq_proper, Some("Peaq")); + let peaq_proper = get_proper_network_name("peaq"); + assert_eq!(peaq_proper, Some(networks::names::PEAQ)); - let agung_proper = networks::get_proper_network_name("agung"); - assert_eq!(agung_proper, Some("Agung")); + let agung_proper = get_proper_network_name("agung"); + assert_eq!(agung_proper, Some(networks::names::AGUNG)); - let unknown = networks::get_proper_network_name("unknown"); + let unknown = get_proper_network_name("unknown"); assert_eq!(unknown, None); - let gnosis_config = networks::get_network_by_name("Gnosis"); + // Test network lookup by name + let gnosis_config = get_network_by_name("Gnosis"); assert!(gnosis_config.is_some()); assert_eq!(gnosis_config.unwrap().chain_id, 100); - let unknown_config = networks::get_network_by_name("Unknown"); + let unknown_config = get_network_by_name("Unknown"); assert!(unknown_config.is_none()); + + // Test case insensitivity + let gnosis_lower = get_network_by_name("gnosis"); + assert!(gnosis_lower.is_some()); + assert_eq!(gnosis_lower.unwrap().chain_id, 100); +} + +#[test] +fn test_network_registry_dynamic() { + // Register a new network + let success = register_network( + "Sepolia", + 11155111, + "https://rpc.sepolia.org", + "https://sepolia.etherscan.io", + "ETH", + 18 + ); + assert!(success); + + // Verify the network was added + let network_names = list_network_names(); + assert!(network_names.iter().any(|name| name == "Sepolia")); + + // Get the network config + let sepolia = get_network_by_name("Sepolia"); + assert!(sepolia.is_some()); + let sepolia = sepolia.unwrap(); + assert_eq!(sepolia.chain_id, 11155111); + assert_eq!(sepolia.token_symbol, "ETH"); + assert_eq!(sepolia.rpc_url, "https://rpc.sepolia.org"); + assert_eq!(sepolia.explorer_url, "https://sepolia.etherscan.io"); + assert_eq!(sepolia.decimals, 18); + + // Test case insensitivity + let sepolia_lower = get_network_by_name("sepolia"); + assert!(sepolia_lower.is_some()); + + // Remove the network + let removed = remove_network("Sepolia"); + assert!(removed); + + // Verify the network was removed + let network_names = list_network_names(); + assert!(!network_names.iter().any(|name| name == "Sepolia")); + + // Try to get the removed network + let sepolia = get_network_by_name("Sepolia"); + assert!(sepolia.is_none()); } #[test] fn test_create_provider() { - let gnosis = networks::gnosis(); - let peaq = networks::peaq(); - let agung = networks::agung(); - - // Create providers - let gnosis_provider = create_provider(&gnosis); - let peaq_provider = create_provider(&peaq); - let agung_provider = create_provider(&agung); + // Create providers using network configs + let gnosis_provider = create_provider_from_config(&networks::gnosis()); + let peaq_provider = create_provider_from_config(&networks::peaq()); + let agung_provider = create_provider_from_config(&networks::agung()); // They should all succeed assert!(gnosis_provider.is_ok()); assert!(peaq_provider.is_ok()); assert!(agung_provider.is_ok()); - // The convenience functions should also work - let gnosis_provider2 = create_gnosis_provider(); - let peaq_provider2 = create_peaq_provider(); - let agung_provider2 = create_agung_provider(); + // Create providers using network names + let gnosis_provider2 = create_provider("gnosis"); + let peaq_provider2 = create_provider("peaq"); + let agung_provider2 = create_provider("agung"); assert!(gnosis_provider2.is_ok()); assert!(peaq_provider2.is_ok()); assert!(agung_provider2.is_ok()); + + // The legacy convenience functions should also work + let gnosis_provider3 = create_gnosis_provider(); + let peaq_provider3 = create_peaq_provider(); + let agung_provider3 = create_agung_provider(); + + assert!(gnosis_provider3.is_ok()); + assert!(peaq_provider3.is_ok()); + assert!(agung_provider3.is_ok()); + + // Test with an unknown network + let unknown_provider = create_provider("unknown"); + assert!(unknown_provider.is_err()); +} + +#[test] +fn test_get_all_networks() { + let networks = get_all_networks(); + assert!(!networks.is_empty()); + assert!(networks.contains_key("gnosis")); + assert!(networks.contains_key("peaq")); + assert!(networks.contains_key("agung")); } diff --git a/src/hero_vault/ethereum/tests/transaction_tests.rs b/src/hero_vault/ethereum/tests/transaction_tests.rs index dd4dc1d..1cab28f 100644 --- a/src/hero_vault/ethereum/tests/transaction_tests.rs +++ b/src/hero_vault/ethereum/tests/transaction_tests.rs @@ -42,7 +42,7 @@ fn test_get_balance() { // Create a provider let network = networks::gnosis(); - let provider_result = create_provider(&network); + let provider_result = create_provider_from_config(&network); // The provider creation should succeed assert!(provider_result.is_ok()); @@ -59,10 +59,10 @@ fn test_send_eth() { // Create a wallet let keypair = KeyPair::new("test_keypair6"); let network = networks::gnosis(); - let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap(); + let wallet = EthereumWallet::from_keypair(&keypair).unwrap(); // Create a provider - let provider_result = create_provider(&network); + let provider_result = create_provider_from_config(&network); assert!(provider_result.is_ok()); // We can't actually test send_eth without a blockchain diff --git a/src/hero_vault/ethereum/tests/wallet_tests.rs b/src/hero_vault/ethereum/tests/wallet_tests.rs index 82e1f4b..9fafb11 100644 --- a/src/hero_vault/ethereum/tests/wallet_tests.rs +++ b/src/hero_vault/ethereum/tests/wallet_tests.rs @@ -3,58 +3,56 @@ use crate::hero_vault::ethereum::*; use crate::hero_vault::keypair::KeyPair; use ethers::utils::hex; +use ethers::prelude::Signer; #[test] fn test_ethereum_wallet_from_keypair() { let keypair = KeyPair::new("test_keypair"); - let network = networks::gnosis(); - let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap(); - - assert_eq!(wallet.network.name, "Gnosis"); - assert_eq!(wallet.network.chain_id, 100); + let wallet = EthereumWallet::from_keypair(&keypair).unwrap(); // The address should be a valid Ethereum address assert!(wallet.address_string().starts_with("0x")); + + // The wallet should not have a name + assert!(wallet.name.is_none()); } #[test] fn test_ethereum_wallet_from_name_and_keypair() { let keypair = KeyPair::new("test_keypair2"); - let network = networks::gnosis(); - let wallet = EthereumWallet::from_name_and_keypair("test", &keypair, network.clone()).unwrap(); - - assert_eq!(wallet.network.name, "Gnosis"); - assert_eq!(wallet.network.chain_id, 100); + let wallet = EthereumWallet::from_name_and_keypair("test", &keypair).unwrap(); // The address should be a valid Ethereum address assert!(wallet.address_string().starts_with("0x")); + // The wallet should have the correct name + assert_eq!(wallet.name, Some("test".to_string())); + // Creating another wallet with the same name and keypair should yield the same address - let wallet2 = EthereumWallet::from_name_and_keypair("test", &keypair, network.clone()).unwrap(); + let wallet2 = EthereumWallet::from_name_and_keypair("test", &keypair).unwrap(); assert_eq!(wallet.address, wallet2.address); // Creating a wallet with a different name should yield a different address - let wallet3 = EthereumWallet::from_name_and_keypair("test2", &keypair, network.clone()).unwrap(); + let wallet3 = EthereumWallet::from_name_and_keypair("test2", &keypair).unwrap(); assert_ne!(wallet.address, wallet3.address); } #[test] fn test_ethereum_wallet_from_private_key() { let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; - let network = networks::gnosis(); - let wallet = EthereumWallet::from_private_key(private_key, network.clone()).unwrap(); - - assert_eq!(wallet.network.name, "Gnosis"); - assert_eq!(wallet.network.chain_id, 100); + let wallet = EthereumWallet::from_private_key(private_key).unwrap(); // The address should be a valid Ethereum address assert!(wallet.address_string().starts_with("0x")); + // The wallet should not have a name + assert!(wallet.name.is_none()); + // The address should be deterministic based on the private key - let wallet2 = EthereumWallet::from_private_key(private_key, network.clone()).unwrap(); + let wallet2 = EthereumWallet::from_private_key(private_key).unwrap(); assert_eq!(wallet.address, wallet2.address); } @@ -67,33 +65,53 @@ fn test_wallet_management() { crate::hero_vault::keypair::create_space("test_space").unwrap(); crate::hero_vault::keypair::create_keypair("test_keypair3").unwrap(); - // Create wallets for different networks + // Create a wallet + let wallet = create_ethereum_wallet().unwrap(); + + // Get the current wallet + let current_wallet = get_current_ethereum_wallet().unwrap(); + + // Check that they match + assert_eq!(wallet.address, current_wallet.address); + + // Clear all wallets + clear_ethereum_wallets(); + + // Check that the wallet is gone + let result = get_current_ethereum_wallet(); + assert!(result.is_err()); + + // Test legacy functions + + // Create wallets for different networks (should all create the same wallet) 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 wallets + // They should all have the same address + assert_eq!(gnosis_wallet.address, peaq_wallet.address); + assert_eq!(gnosis_wallet.address, agung_wallet.address); + + // Get the current wallets for different networks (should all return the same wallet) let current_gnosis = get_current_ethereum_wallet_for_network("Gnosis").unwrap(); let current_peaq = get_current_ethereum_wallet_for_network("Peaq").unwrap(); let current_agung = get_current_ethereum_wallet_for_network("Agung").unwrap(); - // Check that they match - assert_eq!(gnosis_wallet.address, current_gnosis.address); - assert_eq!(peaq_wallet.address, current_peaq.address); - assert_eq!(agung_wallet.address, current_agung.address); + // They should all have the same address + assert_eq!(current_gnosis.address, current_peaq.address); + assert_eq!(current_gnosis.address, current_agung.address); - // Clear wallets for a specific network + // Clear wallets for a specific network (should be a no-op in the new design) 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 + // All wallets should still be accessible + 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(); - assert_eq!(peaq_wallet.address, current_peaq.address); - assert_eq!(agung_wallet.address, current_agung.address); + + // They should all have the same address + assert_eq!(current_gnosis.address, current_peaq.address); + assert_eq!(current_gnosis.address, current_agung.address); // Clear all wallets clear_ethereum_wallets(); @@ -110,9 +128,8 @@ fn test_wallet_management() { #[test] fn test_sign_message() { let keypair = KeyPair::new("test_keypair4"); - let network = networks::gnosis(); - let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap(); + let wallet = EthereumWallet::from_keypair(&keypair).unwrap(); // Create a tokio runtime for the async test let rt = tokio::runtime::Runtime::new().unwrap(); @@ -128,9 +145,8 @@ fn test_sign_message() { #[test] fn test_private_key_hex() { let keypair = KeyPair::new("test_keypair5"); - let network = networks::gnosis(); - let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap(); + let wallet = EthereumWallet::from_keypair(&keypair).unwrap(); // Get the private key as hex let private_key_hex = wallet.private_key_hex(); @@ -141,3 +157,56 @@ fn test_private_key_hex() { // It should be possible to parse it as hex let _bytes = hex::decode(private_key_hex).unwrap(); } + +#[test] +fn test_wallet_for_network() { + let keypair = KeyPair::new("test_keypair6"); + + let wallet = EthereumWallet::from_keypair(&keypair).unwrap(); + + // Get wallets for different networks + let gnosis_network = networks::gnosis(); + let peaq_network = networks::peaq(); + let agung_network = networks::agung(); + + let gnosis_wallet = wallet.for_network(&gnosis_network); + let peaq_wallet = wallet.for_network(&peaq_network); + let agung_wallet = wallet.for_network(&agung_network); + + // The chain IDs should match the networks + assert_eq!(gnosis_wallet.chain_id(), gnosis_network.chain_id); + assert_eq!(peaq_wallet.chain_id(), peaq_network.chain_id); + assert_eq!(agung_wallet.chain_id(), agung_network.chain_id); +} + +#[test] +fn test_legacy_wallet_functions() { + let keypair = KeyPair::new("test_keypair7"); + + // Test legacy wallet creation functions + let gnosis_network = networks::gnosis(); + + // Create a wallet with the legacy function + let wallet1 = EthereumWallet::from_keypair_for_network(&keypair, gnosis_network.clone()).unwrap(); + + // Create a wallet with the new function + let wallet2 = EthereumWallet::from_keypair(&keypair).unwrap(); + + // They should have the same address + assert_eq!(wallet1.address, wallet2.address); + + // Test with name + let wallet3 = EthereumWallet::from_name_and_keypair_for_network("test", &keypair, gnosis_network.clone()).unwrap(); + let wallet4 = EthereumWallet::from_name_and_keypair("test", &keypair).unwrap(); + + // They should have the same address + assert_eq!(wallet3.address, wallet4.address); + + // Test with private key + let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + let wallet5 = EthereumWallet::from_private_key_for_network(private_key, gnosis_network.clone()).unwrap(); + let wallet6 = EthereumWallet::from_private_key(private_key).unwrap(); + + // They should have the same address + assert_eq!(wallet5.address, wallet6.address); +} diff --git a/src/hero_vault/ethereum/transaction.rs b/src/hero_vault/ethereum/transaction.rs index 4a9737d..2dc7477 100644 --- a/src/hero_vault/ethereum/transaction.rs +++ b/src/hero_vault/ethereum/transaction.rs @@ -6,6 +6,7 @@ use ethers::types::transaction::eip2718::TypedTransaction; use crate::hero_vault::error::CryptoError; use super::wallet::EthereumWallet; use super::networks::NetworkConfig; +use super::provider; /// Formats a token balance for display. pub fn format_balance(balance: U256, network: &NetworkConfig) -> String { @@ -20,7 +21,16 @@ pub fn format_balance(balance: U256, network: &NetworkConfig) -> String { } /// Gets the balance of an Ethereum address. -pub async fn get_balance(provider: &Provider, address: Address) -> Result { +pub async fn get_balance(network_name: &str, address: Address) -> Result { + 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, address: Address) -> Result { provider.get_balance(address, None) .await .map_err(|e| CryptoError::SerializationError(format!("Failed to get balance: {}", e))) @@ -29,15 +39,66 @@ pub async fn get_balance(provider: &Provider, address: Address) -> Result< /// Sends Ethereum from one address to another. pub async fn send_eth( wallet: &EthereumWallet, - provider: &Provider, + network_name: &str, to: Address, amount: U256, ) -> Result { - // Create a client with the wallet + // 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(), - wallet.wallet.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(21000); + + // Send the transaction + let pending_tx = client.send_transaction(tx, None) + .await + .map_err(|e| CryptoError::SerializationError(format!("Failed to send transaction: {}", e)))?; + + // Return the transaction hash instead of waiting for the receipt + Ok(pending_tx.tx_hash()) +} + +// Legacy function for backward compatibility + +/// Sends Ethereum from one address to another using a provider. +pub async fn send_eth_with_provider( + wallet: &EthereumWallet, + provider: &Provider, + to: Address, + amount: U256, +) -> Result { + // Create a client with the wallet + let client = SignerMiddleware::new( + provider.clone(), + wallet.signer.clone(), + ); + // Estimate gas let tx = TransactionRequest::new() .to(to) diff --git a/src/hero_vault/ethereum/wallet.rs b/src/hero_vault/ethereum/wallet.rs index ecb73eb..cda3f18 100644 --- a/src/hero_vault/ethereum/wallet.rs +++ b/src/hero_vault/ethereum/wallet.rs @@ -15,13 +15,13 @@ use super::networks::NetworkConfig; #[derive(Debug, Clone)] pub struct EthereumWallet { pub address: Address, - pub wallet: Wallet, - pub network: NetworkConfig, + pub signer: Wallet, + pub name: Option, } impl EthereumWallet { - /// Creates a new Ethereum wallet from a keypair for a specific network. - pub fn from_keypair(keypair: &KeyPair, network: NetworkConfig) -> Result { + /// Creates a new Ethereum wallet from a keypair. + pub fn from_keypair(keypair: &KeyPair) -> Result { // Get the private key bytes from the keypair let private_key_bytes = keypair.signing_key.to_bytes(); @@ -29,22 +29,21 @@ impl EthereumWallet { let private_key_hex = hex::encode(private_key_bytes); // Create an Ethereum wallet from the private key - let wallet = LocalWallet::from_str(&private_key_hex) - .map_err(|_e| CryptoError::InvalidKeyLength)? - .with_chain_id(network.chain_id); + let signer = LocalWallet::from_str(&private_key_hex) + .map_err(|_e| CryptoError::InvalidKeyLength)?; // Get the Ethereum address - let address = wallet.address(); + let address = signer.address(); Ok(EthereumWallet { address, - wallet, - network, + signer, + name: None, }) } - /// 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, network: NetworkConfig) -> Result { + /// Creates a new Ethereum wallet from a name and keypair (deterministic derivation). + pub fn from_name_and_keypair(name: &str, keypair: &KeyPair) -> Result { // Get the private key bytes from the keypair let private_key_bytes = keypair.signing_key.to_bytes(); @@ -58,37 +57,35 @@ impl EthereumWallet { let private_key_hex = hex::encode(seed); // Create an Ethereum wallet from the derived private key - let wallet = LocalWallet::from_str(&private_key_hex) - .map_err(|_e| CryptoError::InvalidKeyLength)? - .with_chain_id(network.chain_id); + let signer = LocalWallet::from_str(&private_key_hex) + .map_err(|_e| CryptoError::InvalidKeyLength)?; // Get the Ethereum address - let address = wallet.address(); + let address = signer.address(); Ok(EthereumWallet { address, - wallet, - network, + signer, + name: Some(name.to_string()), }) } - /// Creates a new Ethereum wallet from a private key for a specific network. - pub fn from_private_key(private_key: &str, network: NetworkConfig) -> Result { + /// Creates a new Ethereum wallet from a private key. + pub fn from_private_key(private_key: &str) -> Result { // Remove 0x prefix if present let private_key_clean = private_key.trim_start_matches("0x"); // Create an Ethereum wallet from the private key - let wallet = LocalWallet::from_str(private_key_clean) - .map_err(|_e| CryptoError::InvalidKeyLength)? - .with_chain_id(network.chain_id); + let signer = LocalWallet::from_str(private_key_clean) + .map_err(|_e| CryptoError::InvalidKeyLength)?; // Get the Ethereum address - let address = wallet.address(); + let address = signer.address(); Ok(EthereumWallet { address, - wallet, - network, + signer, + name: None, }) } @@ -99,7 +96,7 @@ impl EthereumWallet { /// Signs a message with the Ethereum wallet. pub async fn sign_message(&self, message: &[u8]) -> Result { - let signature = self.wallet.sign_message(message) + let signature = self.signer.sign_message(message) .await .map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?; @@ -108,7 +105,34 @@ impl EthereumWallet { /// Gets the private key as a hex string. pub fn private_key_hex(&self) -> String { - let bytes = self.wallet.signer().to_bytes(); + let bytes = self.signer.signer().to_bytes(); hex::encode(bytes) } + + /// Gets a wallet configured for a specific network. + pub fn for_network(&self, network: &NetworkConfig) -> Wallet { + 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::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::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::from_private_key(private_key) + } } diff --git a/src/rhai/hero_vault.rs b/src/rhai/hero_vault.rs index 7ccd9d4..b40dca4 100644 --- a/src/rhai/hero_vault.rs +++ b/src/rhai/hero_vault.rs @@ -12,18 +12,13 @@ use ethers::types::{Address, U256}; use std::str::FromStr; use crate::hero_vault::{keypair, symmetric, ethereum}; -use crate::hero_vault::ethereum::{prepare_function_arguments, convert_token_to_rhai}; +use crate::hero_vault::ethereum::prepare_function_arguments; // Global Tokio runtime for blocking async operations static RUNTIME: Lazy> = Lazy::new(|| { Mutex::new(Runtime::new().expect("Failed to create Tokio runtime")) }); -// Global provider registry -static PROVIDERS: Lazy>>> = Lazy::new(|| { - Mutex::new(HashMap::new()) -}); - // Key space management functions fn load_key_space(name: &str, password: &str) -> bool { // Get the key spaces directory from config @@ -382,9 +377,9 @@ fn decrypt(key: &str, ciphertext: &str) -> String { // Ethereum operations -// Gnosis Chain operations +// Create a network-independent Ethereum wallet fn create_ethereum_wallet() -> bool { - match ethereum::create_ethereum_wallet_for_network(ethereum::networks::gnosis()) { + match ethereum::create_ethereum_wallet() { Ok(_) => true, Err(e) => { log::error!("Error creating Ethereum wallet: {}", e); @@ -393,8 +388,9 @@ fn create_ethereum_wallet() -> bool { } } +// Get the Ethereum wallet address (same for all networks) fn get_ethereum_address() -> String { - match ethereum::get_current_ethereum_wallet_for_network("Gnosis") { + match ethereum::get_current_ethereum_wallet() { Ok(wallet) => wallet.address_string(), Err(e) => { log::error!("Error getting Ethereum address: {}", e); @@ -403,104 +399,51 @@ fn get_ethereum_address() -> String { } } -// Peaq network operations -fn create_peaq_wallet() -> bool { - match ethereum::create_peaq_wallet() { +// Create a wallet from a name +fn create_ethereum_wallet_from_name(name: &str) -> bool { + match ethereum::create_ethereum_wallet_from_name(name) { Ok(_) => true, Err(e) => { - log::error!("Error creating Peaq wallet: {}", e); + log::error!("Error creating Ethereum wallet from name: {}", e); false } } } -fn get_peaq_address() -> String { - match ethereum::get_current_peaq_wallet() { - Ok(wallet) => wallet.address_string(), - Err(e) => { - log::error!("Error getting Peaq address: {}", e); - String::new() - } - } -} - -// Agung testnet operations -fn create_agung_wallet() -> bool { - match ethereum::create_agung_wallet() { +// Create a wallet from a private key +fn create_ethereum_wallet_from_private_key(private_key: &str) -> bool { + match ethereum::create_ethereum_wallet_from_private_key(private_key) { Ok(_) => true, Err(e) => { - log::error!("Error creating Agung wallet: {}", e); + log::error!("Error creating Ethereum wallet from private key: {}", 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() - } - } +// 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 + ) } -// 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 +// 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::networks::list_network_names() { + for name in ethereum::list_network_names() { arr.push(Dynamic::from(name.to_lowercase())); } arr @@ -508,7 +451,7 @@ fn list_supported_networks() -> rhai::Array { // Get network token symbol fn get_network_token_symbol(network_name: &str) -> String { - match ethereum::networks::get_network_by_name(network_name) { + match ethereum::get_network_by_name(network_name) { Some(network) => network.token_symbol, None => { log::error!("Unknown network: {}", network_name); @@ -519,7 +462,7 @@ fn get_network_token_symbol(network_name: &str) -> String { // Get network explorer URL fn get_network_explorer_url(network_name: &str) -> String { - match ethereum::networks::get_network_by_name(network_name) { + match ethereum::get_network_by_name(network_name) { Some(network) => network.explorer_url, None => { log::error!("Unknown network: {}", network_name); @@ -528,48 +471,6 @@ fn get_network_explorer_url(network_name: &str) -> String { } } -// Create a wallet from a private key for a specific network -fn create_wallet_from_private_key_for_network(private_key: &str, network_name: &str) -> bool { - let network = match ethereum::networks::get_network_by_name(network_name) { - Some(network) => network, - None => { - log::error!("Unknown network: {}", network_name); - return false; - } - }; - - match ethereum::create_ethereum_wallet_from_private_key_for_network(private_key, network) { - Ok(_) => true, - Err(e) => { - log::error!("Error creating wallet from private key for network {}: {}", network_name, e); - false - } - } -} - -// Create a provider for the Agung network -fn create_agung_provider() -> String { - match ethereum::create_agung_provider() { - Ok(provider) => { - // Generate a unique ID for the provider - let id = format!("provider_{}", uuid::Uuid::new_v4()); - - // Store the provider in the registry - if let Ok(mut providers) = PROVIDERS.lock() { - providers.insert(id.clone(), provider); - return id; - } - - log::error!("Failed to acquire provider registry lock"); - String::new() - }, - Err(e) => { - log::error!("Error creating Agung provider: {}", e); - String::new() - } - } -} - // Get the balance of an address on a specific network fn get_balance(network_name: &str, address: &str) -> String { // Get the runtime @@ -590,38 +491,17 @@ fn get_balance(network_name: &str, address: &str) -> String { } }; - // Get the proper network name - let network_name_proper = match ethereum::networks::get_proper_network_name(network_name) { - Some(name) => name, - None => { - log::error!("Unknown network: {}", network_name); - return String::new(); - } - }; - - // Get the network config - let network = match ethereum::networks::get_network_by_name(network_name_proper) { - Some(n) => n, - None => { - log::error!("Failed to get network config for: {}", network_name_proper); - return String::new(); - } - }; - - // Create a provider - let provider = match ethereum::create_provider(&network) { - Ok(p) => p, - Err(e) => { - log::error!("Failed to create provider: {}", e); - return String::new(); - } - }; - // Execute the balance query in a blocking manner match rt.block_on(async { - ethereum::get_balance(&provider, addr).await + ethereum::get_balance(network_name, addr).await }) { - Ok(balance) => balance.to_string(), + Ok(balance) => { + // Format the balance with the network's token symbol + match ethereum::get_network_by_name(network_name) { + Some(network) => ethereum::format_balance(balance, &network), + None => balance.to_string() + } + }, Err(e) => { log::error!("Failed to get balance: {}", e); String::new() @@ -629,8 +509,8 @@ fn get_balance(network_name: &str, address: &str) -> String { } } -// Send ETH from one address to another using the blocking approach -fn send_eth(wallet_network: &str, to_address: &str, amount_str: &str) -> String { +// Send ETH from one address to another +fn send_eth(network_name: &str, to_address: &str, amount_str: &str) -> String { // Get the runtime let rt = match RUNTIME.lock() { Ok(rt) => rt, @@ -658,17 +538,8 @@ fn send_eth(wallet_network: &str, to_address: &str, amount_str: &str) -> String } }; - // Get the proper network name - let network_name_proper = match ethereum::networks::get_proper_network_name(wallet_network) { - Some(name) => name, - None => { - log::error!("Unknown network: {}", wallet_network); - return String::new(); - } - }; - // Get the wallet - let wallet = match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) { + let wallet = match ethereum::get_current_ethereum_wallet() { Ok(w) => w, Err(e) => { log::error!("Failed to get wallet: {}", e); @@ -676,18 +547,9 @@ fn send_eth(wallet_network: &str, to_address: &str, amount_str: &str) -> String } }; - // Create a provider - let provider = match ethereum::create_provider(&wallet.network) { - Ok(p) => p, - Err(e) => { - log::error!("Failed to create provider: {}", e); - return String::new(); - } - }; - // Execute the transaction in a blocking manner match rt.block_on(async { - ethereum::send_eth(&wallet, &provider, to_addr, amount).await + ethereum::send_eth(&wallet, network_name, to_addr, amount).await }) { Ok(tx_hash) => format!("{:?}", tx_hash), Err(e) => { @@ -702,7 +564,7 @@ fn send_eth(wallet_network: &str, to_address: &str, amount_str: &str) -> String // Load a contract ABI from a JSON string and create a contract instance fn load_contract_abi(network_name: &str, address: &str, abi_json: &str) -> String { // Get the network - let network = match ethereum::networks::get_network_by_name(network_name) { + let network = match ethereum::get_network_by_name(network_name) { Some(network) => network, None => { log::error!("Unknown network: {}", network_name); @@ -750,8 +612,6 @@ fn load_contract_abi_from_file(network_name: &str, address: &str, file_path: &st } } -// Use the utility functions from the ethereum module - // Call a read-only function on a contract (no arguments version) fn call_contract_read_no_args(contract_json: &str, function_name: &str) -> Dynamic { call_contract_read(contract_json, function_name, rhai::Array::new()) @@ -787,7 +647,7 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra }; // Create a provider - let provider = match ethereum::create_provider(&contract.network) { + let provider = match ethereum::create_provider(&contract.network.name) { Ok(p) => p, Err(e) => { log::error!("Failed to create provider: {}", e); @@ -799,7 +659,7 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra match rt.block_on(async { ethereum::call_read_function(&contract, &provider, function_name, tokens).await }) { - Ok(result) => convert_token_to_rhai(&result), + Ok(result) => ethereum::token_to_dynamic(&result), Err(e) => { log::error!("Failed to call contract function: {}", e); Dynamic::UNIT @@ -842,8 +702,7 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr }; // Get the wallet - let network_name_proper = contract.network.name.as_str(); - let wallet = match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) { + let wallet = match ethereum::get_current_ethereum_wallet() { Ok(w) => w, Err(e) => { log::error!("Failed to get wallet: {}", e); @@ -852,7 +711,7 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr }; // Create a provider - let provider = match ethereum::create_provider(&contract.network) { + let provider = match ethereum::create_provider(&contract.network.name) { Ok(p) => p, Err(e) => { log::error!("Failed to create provider: {}", e); @@ -879,6 +738,41 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr } } +// Legacy functions for backward compatibility + +fn create_peaq_wallet() -> bool { + create_ethereum_wallet() +} + +fn get_peaq_address() -> String { + get_ethereum_address() +} + +fn create_agung_wallet() -> bool { + create_ethereum_wallet() +} + +fn get_agung_address() -> String { + get_ethereum_address() +} + +fn create_wallet_for_network(network_name: &str) -> bool { + create_ethereum_wallet() +} + +fn get_wallet_address_for_network(network_name: &str) -> String { + get_ethereum_address() +} + +fn clear_wallets_for_network(network_name: &str) -> bool { + ethereum::clear_ethereum_wallets(); + true +} + +fn create_wallet_from_private_key_for_network(private_key: &str, network_name: &str) -> bool { + create_ethereum_wallet_from_private_key(private_key) +} + /// Register crypto functions with the Rhai engine pub fn register_crypto_module(engine: &mut Engine) -> Result<(), Box> { // Register key space functions @@ -901,43 +795,40 @@ pub fn register_crypto_module(engine: &mut Engine) -> Result<(), Box