diff --git a/Cargo.toml b/Cargo.toml index 327da7f..6097a5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,12 +29,25 @@ serde_json = "1.0" # For JSON handling glob = "0.3.1" # For file pattern matching tempfile = "3.5" # For temporary file operations log = "0.4" # Logging facade +env_logger = "0.10.0" # Logger implementation rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language rand = "0.8.5" # Random number generation clap = "2.33" # Command-line argument parsing r2d2 = "0.8.10" r2d2_postgres = "0.18.2" +# Crypto dependencies +base64 = "0.21.0" # Base64 encoding/decoding +k256 = { version = "0.13.1", features = ["ecdsa"] } # Elliptic curve cryptography +once_cell = "1.18.0" # Lazy static initialization +sha2 = "0.10.7" # SHA-2 hash functions +chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher +ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library +dirs = "5.0.1" # Directory paths +tokio = { version = "1.28", features = ["full"] } +uuid = { version = "1.16.0", features = ["v4"] } +tokio-test = "0.4.4" + # Optional features for specific OS functionality [target.'cfg(unix)'.dependencies] nix = "0.26" # Unix-specific functionality @@ -47,7 +60,9 @@ windows = { version = "0.48", features = [ ] } [dev-dependencies] -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 +mockall = "0.11.4" # For mocking in tests [[bin]] name = "herodo" diff --git a/examples/hero_vault/README.md b/examples/hero_vault/README.md new file mode 100644 index 0000000..f0dc870 --- /dev/null +++ b/examples/hero_vault/README.md @@ -0,0 +1,64 @@ +# Hero Vault Cryptography Examples + +This directory contains examples demonstrating the Hero Vault cryptography functionality integrated into the SAL project. + +## Overview + +Hero Vault provides cryptographic operations including: + +- Key space management (creation, loading, encryption, decryption) +- Keypair management (creation, selection, listing) +- Digital signatures (signing and verification) +- Symmetric encryption (key generation, encryption, decryption) +- Ethereum wallet functionality +- Smart contract interactions +- Key-value store with encryption + +## Example Files + +- `example.rhai` - Basic example demonstrating key management, signing, and encryption +- `advanced_example.rhai` - Advanced example with error handling, conditional logic, and more complex operations +- `key_persistence_example.rhai` - Demonstrates creating and saving a key space to disk +- `load_existing_space.rhai` - Shows how to load a previously created key space and use its keypairs +- `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts +- `agung_send_transaction.rhai` - Demonstrates sending native tokens on the Agung network +- `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung + +## Running the Examples + +You can run the examples using the `herodo` tool that comes with the SAL project: + +```bash +# Run a single example +herodo --path example.rhai + +# Run all examples using the provided script +./run_examples.sh +``` + +## Key Space Storage + +Key spaces are stored in the `~/.hero-vault/key-spaces/` directory by default. Each key space is stored in a separate JSON file named after the key space (e.g., `my_space.json`). + +## Ethereum Functionality + +The Hero Vault module provides comprehensive Ethereum wallet functionality: + +- Creating and managing wallets for different networks +- Sending ETH transactions +- Checking balances +- Interacting with smart contracts (read and write functions) +- Support for multiple networks (Ethereum, Gnosis, Peaq, Agung, etc.) + +## Security + +Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password. The encryption ensures that the key material is secure at rest. + +## Best Practices + +1. **Use Strong Passwords**: Since the security of your key spaces depends on the strength of your passwords, use strong, unique passwords. +2. **Backup Key Spaces**: Regularly backup your key spaces directory to prevent data loss. +3. **Script Organization**: Split your scripts into logical units, with separate scripts for key creation and key usage. +4. **Error Handling**: Always check the return values of functions to ensure operations succeeded before proceeding. +5. **Network Selection**: When working with Ethereum functionality, be explicit about which network you're targeting to avoid confusion. +6. **Gas Management**: For Ethereum transactions, consider gas costs and set appropriate gas limits. diff --git a/examples/hero_vault/advanced_example.rhai b/examples/hero_vault/advanced_example.rhai new file mode 100644 index 0000000..ea90498 --- /dev/null +++ b/examples/hero_vault/advanced_example.rhai @@ -0,0 +1,233 @@ +// Advanced Rhai script example for Hero Vault Cryptography Module +// This script demonstrates conditional logic, error handling, and more complex operations + +// Function to create a key space with error handling +fn setup_key_space(name, password) { + print("Attempting: Create key space: " + name); + let result = create_key_space(name, password); + + if result { + print("✅ Create key space succeeded!"); + return true; + } else { + print("❌ Create key space failed!"); + } + + return false; +} + +// Function to create and select a keypair +fn setup_keypair(name, password) { + print("Attempting: Create keypair: " + name); + let result = create_keypair(name, password); + + if result { + print("✅ Create keypair succeeded!"); + + print("Attempting: Select keypair: " + name); + let selected = select_keypair(name); + + if selected { + print("✅ Select keypair succeeded!"); + return true; + } else { + print("❌ Select keypair failed!"); + } + } else { + print("❌ Create keypair failed!"); + } + + return false; +} + +// Function to sign multiple messages +fn sign_messages(messages) { + let signatures = []; + + for message in messages { + print("Signing message: " + message); + print("Attempting: Sign message"); + let signature = sign(message); + + if signature != "" { + print("✅ Sign message succeeded!"); + signatures.push(#{ + message: message, + signature: signature + }); + } else { + print("❌ Sign message failed!"); + } + } + + return signatures; +} + +// Function to verify signatures +fn verify_signatures(signed_messages) { + let results = []; + + for item in signed_messages { + let message = item.message; + let signature = item.signature; + + print("Verifying signature for: " + message); + print("Attempting: Verify signature"); + let is_valid = verify(message, signature); + + if is_valid { + print("✅ Verify signature succeeded!"); + } else { + print("❌ Verify signature failed!"); + } + + results.push(#{ + message: message, + valid: is_valid + }); + } + + return results; +} + +// Function to encrypt multiple messages +fn encrypt_messages(messages) { + // Generate a symmetric key + print("Attempting: Generate symmetric key"); + let key = generate_key(); + + if key == "" { + print("❌ Generate symmetric key failed!"); + return []; + } + + print("✅ Generate symmetric key succeeded!"); + print("Using key: " + key); + let encrypted_messages = []; + + for message in messages { + print("Encrypting message: " + message); + print("Attempting: Encrypt message"); + let encrypted = encrypt(key, message); + + if encrypted != "" { + print("✅ Encrypt message succeeded!"); + encrypted_messages.push(#{ + original: message, + encrypted: encrypted, + key: key + }); + } else { + print("❌ Encrypt message failed!"); + } + } + + return encrypted_messages; +} + +// Function to decrypt messages +fn decrypt_messages(encrypted_messages) { + let decrypted_messages = []; + + for item in encrypted_messages { + let encrypted = item.encrypted; + let key = item.key; + let original = item.original; + + print("Decrypting message..."); + print("Attempting: Decrypt message"); + let decrypted = decrypt(key, encrypted); + + if decrypted != false { + let success = decrypted == original; + + decrypted_messages.push(#{ + decrypted: decrypted, + original: original, + success: success + }); + + if success { + print("Decryption matched original ✅"); + } else { + print("Decryption did not match original ❌"); + } + } + } + + return decrypted_messages; +} + +// Main script execution +print("=== Advanced Cryptography Script ==="); + +// Set up key space +let space_name = "advanced_space"; +let password = "secure_password123"; + +if setup_key_space(space_name, password) { + print("\n--- Key space setup complete ---\n"); + + // Set up keypair + if setup_keypair("advanced_keypair", password) { + print("\n--- Keypair setup complete ---\n"); + + // Define messages to sign + let messages = [ + "This is the first message to sign", + "Here's another message that needs signing", + "And a third message for good measure" + ]; + + // Sign messages + print("\n--- Signing Messages ---\n"); + let signed_messages = sign_messages(messages); + + // Verify signatures + print("\n--- Verifying Signatures ---\n"); + let verification_results = verify_signatures(signed_messages); + + // Count successful verifications + let successful_verifications = verification_results.filter(|r| r.valid).len(); + print("Successfully verified " + successful_verifications + " out of " + verification_results.len() + " signatures"); + + // Encrypt messages + print("\n--- Encrypting Messages ---\n"); + let encrypted_messages = encrypt_messages(messages); + + // Decrypt messages + print("\n--- Decrypting Messages ---\n"); + let decryption_results = decrypt_messages(encrypted_messages); + + // Count successful decryptions + let successful_decryptions = decryption_results.filter(|r| r.success).len(); + print("Successfully decrypted " + successful_decryptions + " out of " + decryption_results.len() + " messages"); + + // Create Ethereum wallet + print("\n--- Creating Ethereum Wallet ---\n"); + print("Attempting: Create Ethereum wallet"); + let wallet_created = create_ethereum_wallet(); + + if wallet_created { + print("✅ Create Ethereum wallet succeeded!"); + + print("Attempting: Get Ethereum address"); + let address = get_ethereum_address(); + + if address != "" { + print("✅ Get Ethereum address succeeded!"); + print("Ethereum wallet address: " + address); + } else { + print("❌ Get Ethereum address failed!"); + } + } else { + print("❌ Create Ethereum wallet failed!"); + } + + print("\n=== Script execution completed successfully! ==="); + } else { + print("Failed to set up keypair. Aborting script."); + } +} else { + print("Failed to set up key space. Aborting script."); +} \ No newline at end of file diff --git a/examples/hero_vault/agung_contract_with_args.rhai b/examples/hero_vault/agung_contract_with_args.rhai new file mode 100644 index 0000000..005068f --- /dev/null +++ b/examples/hero_vault/agung_contract_with_args.rhai @@ -0,0 +1,152 @@ +// Example Rhai script for testing contract functions with arguments on Agung network +// This script demonstrates how to use call_contract_read and call_contract_write with arguments + +// Step 1: Set up wallet and network +let space_name = "agung_contract_args_demo"; +let password = "secure_password123"; +let private_key = "51c194d20bcd25360a3aa94426b3b60f738007e42f22e1bc97821c65c353e6d2"; +let network_name = "agung"; + +print("=== Testing Contract Functions With Arguments on Agung Network ===\n"); + +// Create a key space +print("Creating key space: " + space_name); +if create_key_space(space_name, password) { + print("✓ Key space created successfully"); + + // Create a keypair + print("\nCreating keypair..."); + if create_keypair("contract_key", password) { + print("✓ Created contract keypair"); + + // Create a wallet from the private key for the Agung network + print("\nCreating wallet from private key for Agung network..."); + if create_wallet_from_private_key_for_network(private_key, network_name) { + print("✓ Wallet created successfully"); + + // Get the wallet address + let wallet_address = get_wallet_address_for_network(network_name); + print("Wallet address: " + wallet_address); + + // Check wallet balance + print("\nChecking wallet balance..."); + let balance = get_balance(network_name, wallet_address); + if balance != "" { + print("Wallet balance: " + balance + " wei"); + + // Define a simple ERC-20 token contract ABI (partial) + let token_abi = `[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [{"name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [{"name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{"name": "", "type": "uint8"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"name": "_owner", "type": "address"}], + "name": "balanceOf", + "outputs": [{"name": "balance", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{"name": "_to", "type": "address"}, {"name": "_value", "type": "uint256"}], + "name": "transfer", + "outputs": [{"name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + ]`; + + // For this example, we'll use a test token contract on Agung + let token_address = "0x7267B587E4416537060C6bF0B06f6Fd421106650"; + + print("\nLoading contract ABI..."); + let contract = load_contract_abi(network_name, token_address, token_abi); + + if contract != "" { + print("✓ Contract loaded successfully"); + + // First, let's try to read some data from the contract + print("\nReading contract data..."); + + // Try to get token name (no arguments) + let token_name = call_contract_read(contract, "name"); + print("Token name: " + token_name); + + // Try to get token symbol (no arguments) + let token_symbol = call_contract_read(contract, "symbol"); + print("Token symbol: " + token_symbol); + + // Try to get token decimals (no arguments) + let token_decimals = call_contract_read(contract, "decimals"); + print("Token decimals: " + token_decimals); + + // Try to get token balance (with address argument) + print("\nCalling balanceOf with address argument..."); + let balance = call_contract_read(contract, "balanceOf", [wallet_address]); + print("Token balance: " + balance); + + // Now, let's try to execute a write function with arguments + print("\nExecuting contract write function with arguments..."); + + // Define a recipient address and amount for the transfer + // Using a random valid address on the network + let recipient = "0xEEdf3468E8F232A7a03D49b674bA44740C8BD8Be"; + let amount = 1000000; // Changed from string to number for uint256 compatibility + + print("Attempting to transfer " + amount + " tokens to " + recipient); + + // Call the transfer function with arguments + let tx_hash = call_contract_write(contract, "transfer", [recipient, amount]); + + if tx_hash != "" { + print("✓ Transaction sent successfully"); + print("Transaction hash: " + tx_hash); + print("You can view the transaction at: " + get_network_explorer_url(network_name) + "/tx/" + tx_hash); + } else { + print("✗ Failed to send transaction"); + print("This could be due to insufficient funds, contract issues, or other errors."); + } + } else { + print("✗ Failed to load contract"); + } + } else { + print("✗ Failed to get wallet balance"); + } + } else { + print("✗ Failed to create wallet from private key"); + } + } else { + print("✗ Failed to create keypair"); + } +} else { + print("✗ Failed to create key space"); +} + +print("\nContract function with arguments test completed"); diff --git a/examples/hero_vault/agung_send_transaction.rhai b/examples/hero_vault/agung_send_transaction.rhai new file mode 100644 index 0000000..c22fa74 --- /dev/null +++ b/examples/hero_vault/agung_send_transaction.rhai @@ -0,0 +1,104 @@ +// Script to create an Agung wallet from a private key and send tokens +// This script demonstrates how to create a wallet from a private key and send tokens + +// Define the private key and recipient address +let private_key = "0x9ecfd58eca522b0e7c109bf945966ee208cd6d593b1dc3378aedfdc60b64f512"; +let recipient_address = "0xf400f9c3F7317e19523a5DB698Ce67e7a7E083e2"; + +print("=== Agung Wallet Transaction Demo ==="); +print(`From private key: ${private_key}`); +print(`To address: ${recipient_address}`); + +// First, create a key space and keypair (required for the wallet infrastructure) +let space_name = "agung_transaction_demo"; +let password = "demo_password"; + +// Create a new key space +if !create_key_space(space_name, password) { + print("Failed to create key space"); + return; +} + +// Create a keypair +if !create_keypair("demo_keypair", password) { + print("Failed to create keypair"); + return; +} + +// Select the keypair +if !select_keypair("demo_keypair") { + print("Failed to select keypair"); + return; +} + +print("\nCreated and selected keypair successfully"); + +// Clear any existing Agung wallets to avoid conflicts +if clear_wallets_for_network("agung") { + print("Cleared existing Agung wallets"); +} else { + print("Failed to clear existing Agung wallets"); + return; +} + +// Create a wallet from the private key directly +print("\n=== Creating Wallet from Private Key ==="); + +// Create a wallet from the private key for the Agung network +if create_wallet_from_private_key_for_network(private_key, "agung") { + print("Successfully created wallet from private key for Agung network"); + + // Get the wallet address + let wallet_address = get_wallet_address_for_network("agung"); + print(`Wallet address: ${wallet_address}`); + + // Create a provider for the Agung network + let provider_id = create_agung_provider(); + if provider_id != "" { + print("Successfully created Agung provider"); + + // Check the wallet balance first + let wallet_address = get_wallet_address_for_network("agung"); + let balance_wei = get_balance("agung", wallet_address); + + if balance_wei == "" { + print("Failed to get wallet balance"); + print("This could be due to network issues or other errors."); + return; + } + + print(`Current wallet balance: ${balance_wei} wei`); + + // Convert 1 AGNG to wei (1 AGNG = 10^18 wei) + // Use string representation for large numbers + let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string + + // Check if we have enough balance + if parse_int(balance_wei) < parse_int(amount_wei_str) { + print(`Insufficient balance to send ${amount_wei_str} wei (1 AGNG)`); + print(`Current balance: ${balance_wei} wei`); + print("Please fund the wallet before attempting to send a transaction"); + return; + } + + print(`Attempting to send ${amount_wei_str} wei (1 AGNG) to ${recipient_address}`); + + // Send the transaction using the blocking implementation + let tx_hash = send_eth("agung", recipient_address, amount_wei_str); + + if tx_hash != "" { + print(`Transaction sent with hash: ${tx_hash}`); + print(`You can view the transaction at: ${get_network_explorer_url("agung")}/tx/${tx_hash}`); + } else { + print("Transaction failed"); + print("This could be due to insufficient funds, network issues, or other errors."); + print("Check the logs for more details."); + } + } else { + print("Failed to create Agung provider"); + } +} else { + print("Failed to create wallet from private key"); +} + +print("\nAgung transaction demo completed"); diff --git a/examples/hero_vault/contract_example.rhai b/examples/hero_vault/contract_example.rhai new file mode 100644 index 0000000..b2811d5 --- /dev/null +++ b/examples/hero_vault/contract_example.rhai @@ -0,0 +1,98 @@ +// Example Rhai script for interacting with smart contracts using Hero Vault +// This script demonstrates loading a contract ABI and interacting with a contract + +// Step 1: Set up wallet and network +let space_name = "contract_demo_space"; +let password = "secure_password123"; + +print("Creating key space: " + space_name); +if create_key_space(space_name, password) { + print("✓ Key space created successfully"); + + // Create a keypair + print("\nCreating keypair..."); + if create_keypair("contract_key", password) { + print("✓ Created contract keypair"); + } + + // Step 2: Create an Ethereum wallet for Gnosis Chain + print("\nCreating Ethereum wallet..."); + if create_ethereum_wallet() { + print("✓ Ethereum wallet created"); + + let address = get_ethereum_address(); + print("Ethereum address: " + address); + + // Step 3: Define a simple ERC-20 ABI (partial) + let erc20_abi = `[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [{"name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [{"name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{"name": "", "type": "uint8"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"name": "owner", "type": "address"}], + "name": "balanceOf", + "outputs": [{"name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + } + ]`; + + // Step 4: Load the contract ABI + print("\nLoading contract ABI..."); + let contract = load_contract_abi("Gnosis", "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", erc20_abi); + if contract != "" { + print("✓ Contract loaded successfully"); + + // Step 5: Call read-only functions + print("\nCalling read-only functions..."); + + // Get token name + let token_name = call_contract_read(contract, "name"); + print("Token name: " + token_name); + + // Get token symbol + let token_symbol = call_contract_read(contract, "symbol"); + print("Token symbol: " + token_symbol); + + // Get token decimals + let token_decimals = call_contract_read(contract, "decimals"); + print("Token decimals: " + token_decimals); + + // For now, we're just demonstrating the basic structure + } else { + print("✗ Failed to load contract"); + } + } else { + print("✗ Failed to create Ethereum wallet"); + } +} else { + print("✗ Failed to create key space"); +} + +print("\nContract example completed"); diff --git a/examples/hero_vault/example.rhai b/examples/hero_vault/example.rhai new file mode 100644 index 0000000..179546e --- /dev/null +++ b/examples/hero_vault/example.rhai @@ -0,0 +1,85 @@ +// Example Rhai script for Hero Vault Cryptography Module +// This script demonstrates key management, signing, and encryption + +// Step 1: Create and manage a key space +let space_name = "demo_space"; +let password = "secure_password123"; + +print("Creating key space: " + space_name); +if create_key_space(space_name, password) { + print("✓ Key space created successfully"); + + // Step 2: Create and use keypairs + print("\nCreating keypairs..."); + if create_keypair("signing_key", password) { + print("✓ Created signing keypair"); + } + + if create_keypair("encryption_key", password) { + print("✓ Created encryption keypair"); + } + + // List all keypairs + let keypairs = list_keypairs(); + print("Available keypairs: " + keypairs); + + // Step 3: Sign a message + print("\nPerforming signing operations..."); + if select_keypair("signing_key") { + print("✓ Selected signing keypair"); + + let message = "This is a secure message that needs to be signed"; + print("Message: " + message); + + let signature = sign(message); + print("Signature: " + signature); + + // Verify the signature + let is_valid = verify(message, signature); + if is_valid { + print("Signature verification: ✓ Valid"); + } else { + print("Signature verification: ✗ Invalid"); + } + } + + // Step 4: Encrypt and decrypt data + print("\nPerforming encryption operations..."); + + // Generate a symmetric key + let sym_key = generate_key(); + print("Generated symmetric key: " + sym_key); + + // Encrypt a message + let secret = "This is a top secret message that must be encrypted"; + print("Original message: " + secret); + + let encrypted_data = encrypt(sym_key, secret); + print("Encrypted data: " + encrypted_data); + + // Decrypt the message + let decrypted_data = decrypt(sym_key, encrypted_data); + print("Decrypted message: " + decrypted_data); + + // Verify decryption was successful + if decrypted_data == secret { + print("✓ Encryption/decryption successful"); + } else { + print("✗ Encryption/decryption failed"); + } + + // Step 5: Create an Ethereum wallet + print("\nCreating Ethereum wallet..."); + if select_keypair("encryption_key") { + print("✓ Selected keypair for Ethereum wallet"); + + if create_ethereum_wallet() { + print("✓ Ethereum wallet created"); + + let address = get_ethereum_address(); + print("Ethereum address: " + address); + } + } + + print("\nScript execution completed successfully!"); +} \ No newline at end of file diff --git a/examples/hero_vault/key_persistence_example.rhai b/examples/hero_vault/key_persistence_example.rhai new file mode 100644 index 0000000..7b77d85 --- /dev/null +++ b/examples/hero_vault/key_persistence_example.rhai @@ -0,0 +1,65 @@ +// Example Rhai script demonstrating key space persistence for Hero Vault +// This script shows how to create, save, and load key spaces + +// Step 1: Create a key space +let space_name = "persistent_space"; +let password = "secure_password123"; + +print("Creating key space: " + space_name); +if create_key_space(space_name, password) { + print("✓ Key space created successfully"); + + // Step 2: Create keypairs in this space + print("\nCreating keypairs..."); + if create_keypair("persistent_key1", password) { + print("✓ Created first keypair"); + } + + if create_keypair("persistent_key2", password) { + print("✓ Created second keypair"); + } + + // List all keypairs + let keypairs = list_keypairs(); + print("Available keypairs: " + keypairs); + + // Step 3: Clear the session (simulate closing and reopening the CLI) + print("\nClearing session (simulating restart)..."); + // Note: In a real script, you would exit here and run a new script + // For demonstration purposes, we'll continue in the same script + + // Step 4: Load the key space from disk + print("\nLoading key space from disk..."); + if load_key_space(space_name, password) { + print("✓ Key space loaded successfully"); + + // Verify the keypairs are still available + let loaded_keypairs = list_keypairs(); + print("Keypairs after loading: " + loaded_keypairs); + + // Step 5: Use a keypair from the loaded space + print("\nSelecting and using a keypair..."); + if select_keypair("persistent_key1") { + print("✓ Selected keypair"); + + let message = "This message was signed using a keypair from a loaded key space"; + let signature = sign(message); + print("Message: " + message); + print("Signature: " + signature); + + // Verify the signature + let is_valid = verify(message, signature); + if is_valid { + print("Signature verification: ✓ Valid"); + } else { + print("Signature verification: ✗ Invalid"); + } + } + } else { + print("✗ Failed to load key space"); + } +} else { + print("✗ Failed to create key space"); +} + +print("\nScript execution completed!"); \ No newline at end of file diff --git a/examples/hero_vault/load_existing_space.rhai b/examples/hero_vault/load_existing_space.rhai new file mode 100644 index 0000000..52c1fac --- /dev/null +++ b/examples/hero_vault/load_existing_space.rhai @@ -0,0 +1,65 @@ +// Example Rhai script demonstrating loading an existing key space for Hero Vault +// This script shows how to load a previously created key space and use its keypairs + +// Define the key space name and password +let space_name = "persistent_space"; +let password = "secure_password123"; + +print("Loading existing key space: " + space_name); + +// Load the key space from disk +if load_key_space(space_name, password) { + print("✓ Key space loaded successfully"); + + // List available keypairs + let keypairs = list_keypairs(); + print("Available keypairs: " + keypairs); + + // Use both keypairs to sign different messages + if select_keypair("persistent_key1") { + print("\nUsing persistent_key1:"); + let message1 = "Message signed with the first keypair"; + let signature1 = sign(message1); + print("Message: " + message1); + print("Signature: " + signature1); + + let is_valid1 = verify(message1, signature1); + if is_valid1 { + print("Verification: ✓ Valid"); + } else { + print("Verification: ✗ Invalid"); + } + } + + if select_keypair("persistent_key2") { + print("\nUsing persistent_key2:"); + let message2 = "Message signed with the second keypair"; + let signature2 = sign(message2); + print("Message: " + message2); + print("Signature: " + signature2); + + let is_valid2 = verify(message2, signature2); + if is_valid2 { + print("Verification: ✓ Valid"); + } else { + print("Verification: ✗ Invalid"); + } + } + + // Create an Ethereum wallet using one of the keypairs + print("\nCreating Ethereum wallet from persistent keypair:"); + if select_keypair("persistent_key1") { + if create_ethereum_wallet() { + print("✓ Ethereum wallet created"); + + let address = get_ethereum_address(); + print("Ethereum address: " + address); + } else { + print("✗ Failed to create Ethereum wallet"); + } + } +} else { + print("✗ Failed to load key space. Make sure you've run key_persistence_example.rhai first."); +} + +print("\nScript execution completed!"); \ No newline at end of file diff --git a/src/bin/herodo.rs b/src/bin/herodo.rs index f1a5491..31ac457 100644 --- a/src/bin/herodo.rs +++ b/src/bin/herodo.rs @@ -4,8 +4,12 @@ //! It parses command line arguments and calls into the implementation in the cmd module. use clap::{App, Arg}; +use env_logger; fn main() -> Result<(), Box> { + // Initialize the logger + env_logger::init(); + // Parse command line arguments let matches = App::new("herodo") .version("0.1.0") @@ -27,4 +31,4 @@ fn main() -> Result<(), Box> { // Call the run function from the cmd module sal::cmd::herodo::run(script_path) -} \ No newline at end of file +} diff --git a/src/hero_vault/README.md b/src/hero_vault/README.md new file mode 100644 index 0000000..28a3f1b --- /dev/null +++ b/src/hero_vault/README.md @@ -0,0 +1,160 @@ +# Hero Vault Cryptography Module + +The Hero Vault module provides comprehensive cryptographic functionality for the SAL project, including key management, digital signatures, symmetric encryption, Ethereum wallet operations, and a secure key-value store. + +## Module Structure + +The Hero Vault module is organized into several submodules: + +- `error.rs` - Error types for cryptographic operations +- `keypair/` - ECDSA keypair management functionality +- `symmetric/` - Symmetric encryption using ChaCha20Poly1305 +- `ethereum/` - Ethereum wallet and smart contract functionality +- `kvs/` - Encrypted key-value store + +## Key Features + +### Key Space Management + +The module provides functionality for creating, loading, and managing key spaces. A key space is a secure container for cryptographic keys, which can be encrypted and stored on disk. + +```rust +// Create a new key space +let space = KeySpace::new("my_space", "secure_password")?; + +// Save the key space to disk +space.save()?; + +// Load a key space from disk +let loaded_space = KeySpace::load("my_space", "secure_password")?; +``` + +### Keypair Management + +The module provides functionality for creating, selecting, and using ECDSA keypairs for digital signatures. + +```rust +// Create a new keypair in the active key space +let keypair = space.create_keypair("my_keypair", "secure_password")?; + +// Select a keypair for use +space.select_keypair("my_keypair")?; + +// List all keypairs in the active key space +let keypairs = space.list_keypairs()?; +``` + +### Digital Signatures + +The module provides functionality for signing and verifying messages using ECDSA. + +```rust +// Sign a message using the selected keypair +let signature = space.sign("This is a message to sign")?; + +// Verify a signature +let is_valid = space.verify("This is a message to sign", &signature)?; +``` + +### Symmetric Encryption + +The module provides functionality for symmetric encryption using ChaCha20Poly1305. + +```rust +// Generate a new symmetric key +let key = space.generate_key()?; + +// Encrypt a message +let encrypted = space.encrypt(&key, "This is a secret message")?; + +// Decrypt a message +let decrypted = space.decrypt(&key, &encrypted)?; +``` + +### Ethereum Wallet Functionality + +The module provides comprehensive Ethereum wallet functionality, including: + +- Creating and managing wallets for different networks +- Sending ETH transactions +- Checking balances +- Interacting with smart contracts + +```rust +// Create an Ethereum wallet +let wallet = EthereumWallet::new(keypair)?; + +// Get the wallet address +let address = wallet.get_address()?; + +// Send ETH +let tx_hash = wallet.send_eth("0x1234...", "1000000000000000")?; + +// Check balance +let balance = wallet.get_balance("0x1234...")?; +``` + +### Smart Contract Interactions + +The module provides functionality for interacting with smart contracts on EVM-based blockchains. + +```rust +// Load a contract ABI +let contract = Contract::new(provider, "0x1234...", abi)?; + +// Call a read-only function +let result = contract.call_read("balanceOf", vec!["0x5678..."])?; + +// Call a write function +let tx_hash = contract.call_write("transfer", vec!["0x5678...", "1000"])?; +``` + +### Key-Value Store + +The module provides an encrypted key-value store for securely storing sensitive data. + +```rust +// Create a new store +let store = KvStore::new("my_store", "secure_password")?; + +// Set a value +store.set("api_key", "secret_api_key")?; + +// Get a value +let api_key = store.get("api_key")?; +``` + +## Error Handling + +The module uses a comprehensive error type (`CryptoError`) for handling errors that can occur during cryptographic operations: + +- `InvalidKeyLength` - Invalid key length +- `EncryptionFailed` - Encryption failed +- `DecryptionFailed` - Decryption failed +- `SignatureFormatError` - Signature format error +- `KeypairAlreadyExists` - Keypair already exists +- `KeypairNotFound` - Keypair not found +- `NoActiveSpace` - No active key space +- `NoKeypairSelected` - No keypair selected +- `SerializationError` - Serialization error +- `InvalidAddress` - Invalid address format +- `ContractError` - Smart contract error + +## Ethereum Networks + +The module supports multiple Ethereum networks, including: + +- Gnosis Chain +- Peaq Network +- Agung Network + +## Security Considerations + +- Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password +- Private keys are never stored in plaintext +- The module uses secure random number generation for key creation +- All cryptographic operations use well-established libraries and algorithms + +## Examples + +For examples of how to use the Hero Vault module, see the `examples/hero_vault` directory. diff --git a/src/hero_vault/error.rs b/src/hero_vault/error.rs new file mode 100644 index 0000000..2cf41f1 --- /dev/null +++ b/src/hero_vault/error.rs @@ -0,0 +1,58 @@ +//! Error types for cryptographic operations + +use thiserror::Error; + +/// Errors that can occur during cryptographic operations +#[derive(Error, Debug)] +pub enum CryptoError { + /// Invalid key length + #[error("Invalid key length")] + InvalidKeyLength, + + /// Encryption failed + #[error("Encryption failed: {0}")] + EncryptionFailed(String), + + /// Decryption failed + #[error("Decryption failed: {0}")] + DecryptionFailed(String), + + /// Signature format error + #[error("Signature format error: {0}")] + SignatureFormatError(String), + + /// Keypair already exists + #[error("Keypair already exists: {0}")] + KeypairAlreadyExists(String), + + /// Keypair not found + #[error("Keypair not found: {0}")] + KeypairNotFound(String), + + /// No active key space + #[error("No active key space")] + NoActiveSpace, + + /// No keypair selected + #[error("No keypair selected")] + NoKeypairSelected, + + /// Serialization error + #[error("Serialization error: {0}")] + SerializationError(String), + + /// Invalid address format + #[error("Invalid address format: {0}")] + InvalidAddress(String), + + /// Smart contract error + #[error("Smart contract error: {0}")] + ContractError(String), +} + +/// Convert CryptoError to SAL's Error type +impl From for crate::Error { + fn from(err: CryptoError) -> Self { + crate::Error::Sal(err.to_string()) + } +} diff --git a/src/hero_vault/ethereum/README.md b/src/hero_vault/ethereum/README.md new file mode 100644 index 0000000..34fb2dd --- /dev/null +++ b/src/hero_vault/ethereum/README.md @@ -0,0 +1,160 @@ +# Hero Vault Ethereum Module + +The Ethereum module provides functionality for creating and managing Ethereum wallets and interacting with smart contracts on EVM-based blockchains. + +## Module Structure + +The Ethereum module is organized into several components: + +- `wallet.rs` - Core Ethereum wallet implementation +- `networks.rs` - Network registry and configuration +- `provider.rs` - Provider creation and management +- `transaction.rs` - Transaction-related functionality +- `storage.rs` - Wallet storage functionality +- `contract.rs` - Smart contract interaction functionality +- `contract_utils.rs` - Utilities for contract interactions + +## Key Features + +### Wallet Management + +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 wallet with a specific name +let named_wallet = create_ethereum_wallet_from_name_for_network("my_wallet", "Gnosis")?; + +// Create a wallet from a private key +let imported_wallet = create_ethereum_wallet_from_private_key("0x...")?; + +// Get the current wallet for a network +let current_wallet = get_current_ethereum_wallet_for_network("Ethereum")?; + +// Clear wallets +clear_ethereum_wallets()?; +clear_ethereum_wallets_for_network("Gnosis")?; +``` + +### Network Management + +The module supports multiple Ethereum networks and provides functionality for managing network configurations: + +```rust +// Get a network configuration by name +let network = get_network_by_name("Ethereum")?; + +// Get the proper network name (normalized) +let name = get_proper_network_name("eth")?; // Returns "Ethereum" + +// List all available network names +let networks = list_network_names()?; + +// Get all network configurations +let all_networks = get_all_networks()?; +``` + +### Provider Management + +The module provides functionality for creating and managing Ethereum providers: + +```rust +// Create a provider for a specific network +let provider = create_provider("Ethereum")?; + +// Create providers for specific networks +let gnosis_provider = create_gnosis_provider()?; +let peaq_provider = create_peaq_provider()?; +let agung_provider = create_agung_provider()?; +``` + +### Transaction Management + +The module provides functionality for managing Ethereum transactions: + +```rust +// Get the balance of an address +let balance = get_balance("Ethereum", "0x...")?; + +// Send ETH to an address +let tx_hash = send_eth("Ethereum", "0x...", "1000000000000000")?; + +// Format a balance for display +let formatted = format_balance(balance, 18)?; // Convert wei to ETH +``` + +### Smart Contract Interactions + +The module provides functionality for interacting with smart contracts: + +```rust +// Load a contract ABI from JSON +let abi = load_abi_from_json(json_string)?; + +// Create a contract instance +let contract = Contract::new(provider, "0x...", abi)?; + +// Call a read-only function +let result = call_read_function(contract, "balanceOf", vec!["0x..."])?; + +// Call a write function +let tx_hash = call_write_function(contract, "transfer", vec!["0x...", "1000"])?; + +// Estimate gas for a function call +let gas = estimate_gas(contract, "transfer", vec!["0x...", "1000"])?; +``` + +### Contract Utilities + +The module provides utilities for working with contract function arguments and return values: + +```rust +// Convert Rhai values to Ethereum tokens +let token = convert_rhai_to_token(value)?; + +// Prepare function arguments +let args = prepare_function_arguments(function, vec![arg1, arg2])?; + +// Convert Ethereum tokens to Rhai values +let rhai_value = convert_token_to_rhai(token)?; + +// Convert a token to a dynamic value +let dynamic = token_to_dynamic(token)?; +``` + +## Supported Networks + +The module supports multiple Ethereum networks, including: + +- Gnosis Chain +- Peaq Network +- Agung Network + +Each network has its own configuration, including: + +- RPC URL +- Chain ID +- Explorer URL +- Native currency symbol and decimals + +## Error Handling + +The module uses the `CryptoError` type for handling errors that can occur during Ethereum operations: + +- `InvalidAddress` - Invalid Ethereum address format +- `ContractError` - Smart contract interaction error + +## Examples + +For examples of how to use the Ethereum module, see the `examples/hero_vault` directory, particularly: + +- `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts +- `agung_simple_transfer.rhai` - Shows how to perform a simple ETH transfer on the Agung network +- `agung_send_transaction.rhai` - Demonstrates sending transactions on the Agung network +- `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung diff --git a/src/hero_vault/ethereum/contract.rs b/src/hero_vault/ethereum/contract.rs new file mode 100644 index 0000000..7f0b224 --- /dev/null +++ b/src/hero_vault/ethereum/contract.rs @@ -0,0 +1,179 @@ +//! Smart contract interaction functionality. +//! +//! This module provides functionality for interacting with smart contracts on EVM-based blockchains. + +use ethers::prelude::*; +use ethers::abi::{Abi, Token}; +use std::sync::Arc; +use std::str::FromStr; +use serde::{Serialize, Deserialize}; + +use crate::hero_vault::error::CryptoError; +use super::wallet::EthereumWallet; +use super::networks::NetworkConfig; + +/// A smart contract instance. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Contract { + /// The contract address + pub address: Address, + /// The contract ABI + pub abi: Abi, + /// The network the contract is deployed on + pub network: NetworkConfig, +} + +impl Contract { + /// Creates a new contract instance. + pub fn new(address: Address, abi: Abi, network: NetworkConfig) -> Self { + Contract { + address, + abi, + network, + } + } + + /// Creates a new contract instance from an address string and ABI. + pub fn from_address_string(address_str: &str, abi: Abi, network: NetworkConfig) -> Result { + let address = Address::from_str(address_str) + .map_err(|e| CryptoError::InvalidAddress(format!("Invalid address format: {}", e)))?; + + Ok(Contract::new(address, abi, network)) + } + + /// Creates an ethers Contract instance for interaction. + pub fn create_ethers_contract(&self, provider: Provider, _wallet: Option<&EthereumWallet>) -> Result>, CryptoError> { + let contract = ethers::contract::Contract::new( + self.address, + self.abi.clone(), + Arc::new(provider), + ); + + Ok(contract) + } +} + +/// Loads a contract ABI from a JSON string. +pub fn load_abi_from_json(json_str: &str) -> Result { + serde_json::from_str(json_str) + .map_err(|e| CryptoError::SerializationError(format!("Failed to parse ABI JSON: {}", e))) +} + +/// Calls a read-only function on a contract. +pub async fn call_read_function( + contract: &Contract, + provider: &Provider, + function_name: &str, + args: Vec, +) -> Result, CryptoError> { + // Create the ethers contract (not used directly but kept for future extensions) + let _ethers_contract = contract.create_ethers_contract(provider.clone(), None)?; + + // Get the function from the ABI + let function = contract.abi.function(function_name) + .map_err(|e| CryptoError::ContractError(format!("Function not found in ABI: {}", e)))?; + + // Encode the function call + let call_data = function.encode_input(&args) + .map_err(|e| CryptoError::ContractError(format!("Failed to encode function call: {}", e)))?; + + // Make the call + let tx = TransactionRequest::new() + .to(contract.address) + .data(call_data); + + let result = provider.call(&tx.into(), None).await + .map_err(|e| CryptoError::ContractError(format!("Contract call failed: {}", e)))?; + + // Decode the result + let decoded = function.decode_output(&result) + .map_err(|e| CryptoError::ContractError(format!("Failed to decode function output: {}", e)))?; + + Ok(decoded) +} + +/// Executes a state-changing function on a contract. +pub async fn call_write_function( + contract: &Contract, + wallet: &EthereumWallet, + provider: &Provider, + function_name: &str, + args: Vec, +) -> Result { + // Create a client with the wallet + let client = SignerMiddleware::new( + provider.clone(), + wallet.wallet.clone(), + ); + + // Get the function from the ABI + let function = contract.abi.function(function_name) + .map_err(|e| CryptoError::ContractError(format!("Function not found in ABI: {}", e)))?; + + // Encode the function call + let call_data = function.encode_input(&args) + .map_err(|e| CryptoError::ContractError(format!("Failed to encode function call: {}", e)))?; + + // Create the transaction request with gas limit + let tx = TransactionRequest::new() + .to(contract.address) + .data(call_data) + .gas(U256::from(300000)); // Set a reasonable gas limit + + // Send the transaction using the client directly + log::info!("Sending transaction to contract at {}", contract.address); + log::info!("Function: {}, Args: {:?}", function_name, args); + + // Log detailed information about the transaction + log::debug!("Sending transaction to contract at {}", contract.address); + log::debug!("Function: {}, Args: {:?}", function_name, args); + log::debug!("From address: {}", wallet.address); + log::debug!("Gas limit: {:?}", tx.gas); + + let pending_tx = match client.send_transaction(tx, None).await { + Ok(pending_tx) => { + log::debug!("Transaction sent successfully: {:?}", pending_tx.tx_hash()); + log::info!("Transaction sent successfully: {:?}", pending_tx.tx_hash()); + pending_tx + }, + Err(e) => { + // Log the error for debugging + log::error!("Failed to send transaction: {}", e); + log::error!("ERROR DETAILS: {:?}", e); + return Err(CryptoError::ContractError(format!("Failed to send transaction: {}", e))); + } + }; + + // Return the transaction hash + Ok(pending_tx.tx_hash()) +} + +/// Estimates gas for a contract function call. +pub async fn estimate_gas( + contract: &Contract, + wallet: &EthereumWallet, + provider: &Provider, + function_name: &str, + args: Vec, +) -> Result { + // Get the function from the ABI + let function = contract.abi.function(function_name) + .map_err(|e| CryptoError::ContractError(format!("Function not found in ABI: {}", e)))?; + + // Encode the function call + let call_data = function.encode_input(&args) + .map_err(|e| CryptoError::ContractError(format!("Failed to encode function call: {}", e)))?; + + // Create the transaction request + let tx = TransactionRequest::new() + .from(wallet.address) + .to(contract.address) + .data(call_data); + + // Estimate gas + let gas = provider.estimate_gas(&tx.into(), None) + .await + .map_err(|e| CryptoError::ContractError(format!("Failed to estimate gas: {}", e)))?; + + Ok(gas) +} diff --git a/src/hero_vault/ethereum/contract_utils.rs b/src/hero_vault/ethereum/contract_utils.rs new file mode 100644 index 0000000..d40c23d --- /dev/null +++ b/src/hero_vault/ethereum/contract_utils.rs @@ -0,0 +1,183 @@ +//! Utility functions for smart contract interactions. + +use ethers::abi::{Abi, Token, ParamType}; +use ethers::types::{Address, U256}; +use std::str::FromStr; +use rhai::{Dynamic, Array}; + +/// Convert Rhai Dynamic values to ethers Token types +pub fn convert_rhai_to_token(value: &Dynamic, expected_type: Option<&ParamType>) -> Result { + match value { + // Handle integers + v if v.is_int() => { + let i = v.as_int().unwrap(); + if let Some(param_type) = expected_type { + match param_type { + ParamType::Uint(_) => Ok(Token::Uint(U256::from(i as u64))), + ParamType::Int(_) => { + // Convert to I256 - in a real implementation, we would handle this properly + // For now, we'll just use U256 for both types + Ok(Token::Uint(U256::from(i as u64))) + }, + _ => Err(format!("Expected {}, got integer", param_type)) + } + } else { + // Default to Uint256 if no type info + Ok(Token::Uint(U256::from(i as u64))) + } + }, + + // Handle strings and addresses + v if v.is_string() => { + let s = v.to_string(); + if let Some(param_type) = expected_type { + match param_type { + ParamType::Address => { + match Address::from_str(&s) { + Ok(addr) => Ok(Token::Address(addr)), + Err(e) => Err(format!("Invalid address format: {}", e)) + } + }, + ParamType::String => Ok(Token::String(s)), + ParamType::Bytes => { + // Handle hex string conversion to bytes + if s.starts_with("0x") { + match ethers::utils::hex::decode(&s[2..]) { + Ok(bytes) => Ok(Token::Bytes(bytes)), + Err(e) => Err(format!("Invalid hex string: {}", e)) + } + } else { + Ok(Token::Bytes(s.as_bytes().to_vec())) + } + }, + _ => Err(format!("Expected {}, got string", param_type)) + } + } else { + // Try to detect type from string format + if s.starts_with("0x") && s.len() == 42 { + // Likely an address + match Address::from_str(&s) { + Ok(addr) => Ok(Token::Address(addr)), + Err(_) => Ok(Token::String(s)) + } + } else { + Ok(Token::String(s)) + } + } + }, + + // Handle booleans + v if v.is_bool() => { + let b = v.as_bool().unwrap(); + if let Some(param_type) = expected_type { + if matches!(param_type, ParamType::Bool) { + Ok(Token::Bool(b)) + } else { + Err(format!("Expected {}, got boolean", param_type)) + } + } else { + Ok(Token::Bool(b)) + } + }, + + // Handle arrays + v if v.is_array() => { + let arr = v.clone().into_array().unwrap(); + if let Some(ParamType::Array(inner_type)) = expected_type { + let mut tokens = Vec::new(); + for item in arr.iter() { + match convert_rhai_to_token(item, Some(inner_type)) { + Ok(token) => tokens.push(token), + Err(e) => return Err(e) + } + } + Ok(Token::Array(tokens)) + } else { + Err("Array type mismatch or no type information available".to_string()) + } + }, + + // Handle other types or return error + _ => Err(format!("Unsupported Rhai type: {:?}", value)) + } +} + +/// Validate and convert arguments based on function ABI +pub fn prepare_function_arguments( + abi: &Abi, + function_name: &str, + args: &Array +) -> Result, String> { + // Get the function from the ABI + let function = abi.function(function_name) + .map_err(|e| format!("Function not found in ABI: {}", e))?; + + // Check if number of arguments matches + if function.inputs.len() != args.len() { + return Err(format!( + "Wrong number of arguments for function '{}': expected {}, got {}", + function_name, function.inputs.len(), args.len() + )); + } + + // Convert each argument according to the expected type + let mut tokens = Vec::new(); + for (i, (param, arg)) in function.inputs.iter().zip(args.iter()).enumerate() { + match convert_rhai_to_token(arg, Some(¶m.kind)) { + Ok(token) => tokens.push(token), + Err(e) => return Err(format!("Error converting argument {}: {}", i, e)) + } + } + + Ok(tokens) +} + +/// Convert ethers Token to Rhai Dynamic value +pub fn convert_token_to_rhai(tokens: &[Token]) -> Dynamic { + if tokens.is_empty() { + return Dynamic::UNIT; + } + + // If there's only one return value, return it directly + if tokens.len() == 1 { + return token_to_dynamic(&tokens[0]); + } + + // If there are multiple return values, return them as an array + let mut array = Array::new(); + for token in tokens { + array.push(token_to_dynamic(token)); + } + Dynamic::from(array) +} + +/// Convert a single token to a Dynamic value +pub fn token_to_dynamic(token: &Token) -> Dynamic { + match token { + Token::Address(addr) => Dynamic::from(format!("{:?}", addr)), + Token::Bytes(bytes) => Dynamic::from(ethers::utils::hex::encode(bytes)), + Token::Int(i) => Dynamic::from(i.to_string()), + Token::Uint(u) => Dynamic::from(u.to_string()), + Token::Bool(b) => Dynamic::from(*b), + Token::String(s) => Dynamic::from(s.clone()), + Token::Array(arr) => { + let mut rhai_arr = Array::new(); + for item in arr { + rhai_arr.push(token_to_dynamic(item)); + } + Dynamic::from(rhai_arr) + }, + Token::Tuple(tuple) => { + let mut rhai_arr = Array::new(); + for item in tuple { + rhai_arr.push(token_to_dynamic(item)); + } + Dynamic::from(rhai_arr) + }, + // Handle other token types + _ => { + log::warn!("Unsupported token type: {:?}", token); + Dynamic::UNIT + } + } +} diff --git a/src/hero_vault/ethereum/mod.rs b/src/hero_vault/ethereum/mod.rs new file mode 100644 index 0000000..ac698b4 --- /dev/null +++ b/src/hero_vault/ethereum/mod.rs @@ -0,0 +1,87 @@ +//! Ethereum wallet functionality +//! +//! This module provides functionality for creating and managing Ethereum wallets +//! and interacting with smart contracts on EVM-based blockchains. +//! +//! The module is organized into several components: +//! - `wallet.rs`: Core Ethereum wallet implementation +//! - `networks.rs`: Network registry and configuration +//! - `provider.rs`: Provider creation and management +//! - `transaction.rs`: Transaction-related functionality +//! - `storage.rs`: Wallet storage functionality +//! - `contract.rs`: Smart contract interaction functionality + +mod wallet; +mod provider; +mod transaction; +mod storage; +mod contract; +mod contract_utils; +pub mod networks; +#[cfg(test)] +pub mod tests; + +// Re-export public types and functions +pub use wallet::EthereumWallet; +pub use networks::NetworkConfig; + +// Re-export wallet creation functions +pub use storage::{ + 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_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_gnosis_provider, + create_peaq_provider, + create_agung_provider, +}; + +// Re-export transaction functions +pub use transaction::{ + get_balance, + send_eth, + format_balance, +}; + +// Re-export network registry functions +pub use networks::{ + get_network_by_name, + get_proper_network_name, + list_network_names, + get_all_networks, + names, +}; + +// Re-export contract functions +pub use contract::{ + Contract, + load_abi_from_json, + call_read_function, + call_write_function, + estimate_gas, +}; + +// Re-export contract utility functions +pub use contract_utils::{ + convert_rhai_to_token, + prepare_function_arguments, + convert_token_to_rhai, + token_to_dynamic, +}; diff --git a/src/hero_vault/ethereum/networks.rs b/src/hero_vault/ethereum/networks.rs new file mode 100644 index 0000000..4e81655 --- /dev/null +++ b/src/hero_vault/ethereum/networks.rs @@ -0,0 +1,102 @@ +//! Ethereum network registry +//! +//! This module provides a centralized registry of Ethereum networks and utilities +//! to work with them. + +use std::collections::HashMap; +use std::sync::OnceLock; +use serde::{Serialize, Deserialize}; + +/// Configuration for an EVM-compatible network +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkConfig { + pub name: String, + pub chain_id: u64, + pub rpc_url: String, + pub explorer_url: String, + pub token_symbol: String, + pub decimals: u8, +} + +/// Network name constants +pub mod names { + pub const GNOSIS: &str = "Gnosis"; + pub const PEAQ: &str = "Peaq"; + pub const AGUNG: &str = "Agung"; +} + +/// Get the Gnosis Chain network configuration +pub fn gnosis() -> NetworkConfig { + NetworkConfig { + name: names::GNOSIS.to_string(), + chain_id: 100, + rpc_url: "https://rpc.gnosischain.com".to_string(), + explorer_url: "https://gnosisscan.io".to_string(), + token_symbol: "xDAI".to_string(), + decimals: 18, + } +} + +/// Get the Peaq Network configuration +pub fn peaq() -> NetworkConfig { + NetworkConfig { + name: names::PEAQ.to_string(), + chain_id: 3338, + rpc_url: "https://peaq.api.onfinality.io/public".to_string(), + explorer_url: "https://peaq.subscan.io/".to_string(), + token_symbol: "PEAQ".to_string(), + decimals: 18, + } +} + +/// Get the Agung Testnet configuration +pub fn agung() -> NetworkConfig { + NetworkConfig { + name: names::AGUNG.to_string(), + chain_id: 9990, + rpc_url: "https://wss-async.agung.peaq.network".to_string(), + explorer_url: "https://agung-testnet.subscan.io/".to_string(), + token_symbol: "AGNG".to_string(), + decimals: 18, + } +} + +/// 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, + } +} + +/// Get the proper capitalization of a network name +pub fn get_proper_network_name(name: &str) -> Option<&'static str> { + let name_lower = name.to_lowercase(); + match name_lower.as_str() { + "gnosis" => Some(names::GNOSIS), + "peaq" => Some(names::PEAQ), + "agung" => Some(names::AGUNG), + _ => None, + } +} + +/// Get a list of all supported network names +pub fn list_network_names() -> Vec<&'static str> { + vec![names::GNOSIS, names::PEAQ, names::AGUNG] +} + +/// Get a map of all networks +pub fn get_all_networks() -> &'static HashMap<&'static str, NetworkConfig> { + static NETWORKS: OnceLock> = OnceLock::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 + }) +} diff --git a/src/hero_vault/ethereum/provider.rs b/src/hero_vault/ethereum/provider.rs new file mode 100644 index 0000000..fc7fcbd --- /dev/null +++ b/src/hero_vault/ethereum/provider.rs @@ -0,0 +1,27 @@ +//! Ethereum provider functionality. + +use ethers::prelude::*; + +use crate::hero_vault::error::CryptoError; +use super::networks::{self, NetworkConfig}; + +/// Creates a provider for a specific network. +pub fn create_provider(network: &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))) +} + +/// Creates a provider for the Gnosis Chain. +pub fn create_gnosis_provider() -> Result, CryptoError> { + create_provider(&networks::gnosis()) +} + +/// Creates a provider for the Peaq network. +pub fn create_peaq_provider() -> Result, CryptoError> { + create_provider(&networks::peaq()) +} + +/// Creates a provider for the Agung testnet. +pub fn create_agung_provider() -> Result, CryptoError> { + create_provider(&networks::agung()) +} diff --git a/src/hero_vault/ethereum/storage.rs b/src/hero_vault/ethereum/storage.rs new file mode 100644 index 0000000..52b6a8c --- /dev/null +++ b/src/hero_vault/ethereum/storage.rs @@ -0,0 +1,114 @@ +//! Ethereum wallet storage functionality. + +use std::sync::Mutex; +use std::collections::HashMap; +use once_cell::sync::Lazy; + +use crate::hero_vault::error::CryptoError; +use super::wallet::EthereumWallet; +use super::networks::{self, NetworkConfig}; + +/// Global storage for Ethereum wallets. +static ETH_WALLETS: Lazy>>> = Lazy::new(|| { + Mutex::new(HashMap::new()) +}); + +/// Creates an Ethereum wallet from the currently selected keypair for a specific network. +pub fn create_ethereum_wallet_for_network(network: NetworkConfig) -> 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)?; + + // Store the wallet + let mut wallets = ETH_WALLETS.lock().unwrap(); + let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new); + network_wallets.push(wallet.clone()); + + Ok(wallet) +} + +/// Creates an Ethereum wallet from the currently selected keypair for the Peaq network. +pub fn create_peaq_wallet() -> Result { + create_ethereum_wallet_for_network(networks::peaq()) +} + +/// Creates an Ethereum wallet from the currently selected keypair for the Agung testnet. +pub fn create_agung_wallet() -> Result { + create_ethereum_wallet_for_network(networks::agung()) +} + +/// Gets the current Ethereum wallet for a specific network. +pub fn get_current_ethereum_wallet_for_network(network_name: &str) -> Result { + let wallets = ETH_WALLETS.lock().unwrap(); + + let network_wallets = wallets.get(network_name).ok_or(CryptoError::NoKeypairSelected)?; + + if network_wallets.is_empty() { + return Err(CryptoError::NoKeypairSelected); + } + + Ok(network_wallets.last().unwrap().clone()) +} + +/// Gets the current Ethereum wallet for the Peaq network. +pub fn get_current_peaq_wallet() -> Result { + 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") +} + +/// Clears all Ethereum wallets. +pub fn clear_ethereum_wallets() { + let mut wallets = ETH_WALLETS.lock().unwrap(); + wallets.clear(); +} + +/// Clears Ethereum wallets for a specific network. +pub fn clear_ethereum_wallets_for_network(network_name: &str) { + let mut wallets = ETH_WALLETS.lock().unwrap(); + wallets.remove(network_name); +} + +/// Creates an Ethereum wallet from a name and the currently selected keypair for a specific network. +pub fn create_ethereum_wallet_from_name_for_network(name: &str, network: 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()) +} + +/// 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()) +} diff --git a/src/hero_vault/ethereum/tests/contract_args_tests.rs b/src/hero_vault/ethereum/tests/contract_args_tests.rs new file mode 100644 index 0000000..a729b6f --- /dev/null +++ b/src/hero_vault/ethereum/tests/contract_args_tests.rs @@ -0,0 +1,47 @@ +//! Tests for smart contract argument handling functionality. + +use ethers::types::Address; +use std::str::FromStr; + +use crate::hero_vault::ethereum::*; + +#[test] +fn test_contract_creation() { + // Create a simple ABI + let abi_json = r#"[ + { + "inputs": [], + "name": "getValue", + "outputs": [{"type": "uint256", "name": ""}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"type": "uint256", "name": "newValue"}], + "name": "setValue", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ]"#; + + // Parse the ABI + let abi = load_abi_from_json(abi_json).unwrap(); + + // Create a contract address + let address = Address::from_str("0x1234567890123456789012345678901234567890").unwrap(); + + // Create a network config + let network = networks::gnosis(); + + // Create a contract + let contract = Contract::new(address, abi, network); + + // Verify the contract was created correctly + assert_eq!(contract.address, address); + assert_eq!(contract.network.name, "Gnosis"); + + // Verify the ABI contains the expected functions + assert!(contract.abi.function("getValue").is_ok()); + assert!(contract.abi.function("setValue").is_ok()); +} diff --git a/src/hero_vault/ethereum/tests/contract_tests.rs b/src/hero_vault/ethereum/tests/contract_tests.rs new file mode 100644 index 0000000..171e9e4 --- /dev/null +++ b/src/hero_vault/ethereum/tests/contract_tests.rs @@ -0,0 +1,83 @@ +//! Tests for smart contract functionality. + +use ethers::types::Address; +use std::str::FromStr; + +use crate::hero_vault::ethereum::*; + +#[test] +fn test_contract_creation() { + // Create a simple ABI + let abi_json = r#"[ + { + "inputs": [], + "name": "getValue", + "outputs": [{"type": "uint256", "name": ""}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"type": "uint256", "name": "newValue"}], + "name": "setValue", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ]"#; + + // Parse the ABI + let abi = load_abi_from_json(abi_json).unwrap(); + + // Create a contract address + let address = Address::from_str("0x1234567890123456789012345678901234567890").unwrap(); + + // Create a network config + let network = networks::gnosis(); + + // Create a contract + let contract = Contract::new(address, abi, network); + + // Verify the contract was created correctly + assert_eq!(contract.address, address); + assert_eq!(contract.network.name, "Gnosis"); + + // Verify the ABI contains the expected functions + assert!(contract.abi.function("getValue").is_ok()); + assert!(contract.abi.function("setValue").is_ok()); +} + +#[test] +fn test_contract_from_address_string() { + // Create a simple ABI + let abi_json = r#"[ + { + "inputs": [], + "name": "getValue", + "outputs": [{"type": "uint256", "name": ""}], + "stateMutability": "view", + "type": "function" + } + ]"#; + + // Parse the ABI + let abi = load_abi_from_json(abi_json).unwrap(); + + // Create a network config + let network = networks::gnosis(); + + // Create a contract from an address string + let address_str = "0x1234567890123456789012345678901234567890"; + let contract = Contract::from_address_string(address_str, abi, network).unwrap(); + + // Verify the contract was created correctly + assert_eq!(contract.address, Address::from_str(address_str).unwrap()); + + // Test with an invalid address + let invalid_address = "0xinvalid"; + let result = Contract::from_address_string(invalid_address, contract.abi.clone(), contract.network.clone()); + assert!(result.is_err()); +} + +// Note: We can't easily test the actual contract calls in unit tests without mocking +// the provider, which would be complex. These would be better tested in integration tests +// with a local blockchain or testnet. diff --git a/src/hero_vault/ethereum/tests/mod.rs b/src/hero_vault/ethereum/tests/mod.rs new file mode 100644 index 0000000..2c5c097 --- /dev/null +++ b/src/hero_vault/ethereum/tests/mod.rs @@ -0,0 +1,7 @@ +//! Tests for Ethereum functionality. + +mod wallet_tests; +mod network_tests; +mod transaction_tests; +mod contract_tests; +mod contract_args_tests; diff --git a/src/hero_vault/ethereum/tests/network_tests.rs b/src/hero_vault/ethereum/tests/network_tests.rs new file mode 100644 index 0000000..9afab95 --- /dev/null +++ b/src/hero_vault/ethereum/tests/network_tests.rs @@ -0,0 +1,74 @@ +//! Tests for Ethereum network functionality. + +use crate::hero_vault::ethereum::*; + +#[test] +fn test_network_config() { + let gnosis = networks::gnosis(); + assert_eq!(gnosis.name, "Gnosis"); + assert_eq!(gnosis.chain_id, 100); + assert_eq!(gnosis.token_symbol, "xDAI"); + + let peaq = networks::peaq(); + assert_eq!(peaq.name, "Peaq"); + assert_eq!(peaq.chain_id, 3338); + assert_eq!(peaq.token_symbol, "PEAQ"); + + let agung = networks::agung(); + assert_eq!(agung.name, "Agung"); + assert_eq!(agung.chain_id, 9990); + assert_eq!(agung.token_symbol, "AGNG"); +} + +#[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")); + + let gnosis_proper = networks::get_proper_network_name("gnosis"); + assert_eq!(gnosis_proper, Some("Gnosis")); + + let peaq_proper = networks::get_proper_network_name("peaq"); + assert_eq!(peaq_proper, Some("Peaq")); + + let agung_proper = networks::get_proper_network_name("agung"); + assert_eq!(agung_proper, Some("Agung")); + + let unknown = networks::get_proper_network_name("unknown"); + assert_eq!(unknown, None); + + let gnosis_config = networks::get_network_by_name("Gnosis"); + assert!(gnosis_config.is_some()); + assert_eq!(gnosis_config.unwrap().chain_id, 100); + + let unknown_config = networks::get_network_by_name("Unknown"); + assert!(unknown_config.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); + + // 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(); + + assert!(gnosis_provider2.is_ok()); + assert!(peaq_provider2.is_ok()); + assert!(agung_provider2.is_ok()); +} diff --git a/src/hero_vault/ethereum/tests/transaction_tests.rs b/src/hero_vault/ethereum/tests/transaction_tests.rs new file mode 100644 index 0000000..dd4dc1d --- /dev/null +++ b/src/hero_vault/ethereum/tests/transaction_tests.rs @@ -0,0 +1,70 @@ +//! Tests for Ethereum transaction functionality. + +use crate::hero_vault::ethereum::*; +use crate::hero_vault::keypair::KeyPair; +use ethers::types::U256; +use std::str::FromStr; + +#[test] +fn test_format_balance() { + let network = networks::gnosis(); + + // Test with 0 + let balance = U256::from(0); + let formatted = format_balance(balance, &network); + assert_eq!(formatted, "0.000000 xDAI"); + + // Test with 1 wei + let balance = U256::from(1); + let formatted = format_balance(balance, &network); + assert_eq!(formatted, "0.000000 xDAI"); + + // Test with 1 gwei (10^9 wei) + let balance = U256::from(1_000_000_000u64); + let formatted = format_balance(balance, &network); + assert_eq!(formatted, "0.000000 xDAI"); + + // Test with 1 ETH (10^18 wei) + let balance = U256::from_dec_str("1000000000000000000").unwrap(); + let formatted = format_balance(balance, &network); + assert_eq!(formatted, "1.000000 xDAI"); + + // Test with a larger amount + let balance = U256::from_dec_str("123456789000000000000").unwrap(); + let formatted = format_balance(balance, &network); + assert_eq!(formatted, "123.456789 xDAI"); +} + +#[test] +fn test_get_balance() { + // This is a mock test since we can't actually query the blockchain in a unit test + // In a real test, we would use a local blockchain or mock the provider + + // Create a provider + let network = networks::gnosis(); + let provider_result = create_provider(&network); + + // The provider creation should succeed + assert!(provider_result.is_ok()); + + // We can't actually test get_balance without a blockchain + // In a real test, we would mock the provider and test the function +} + +#[test] +fn test_send_eth() { + // This is a mock test since we can't actually send transactions in a unit test + // In a real test, we would use a local blockchain or mock the provider + + // Create a wallet + let keypair = KeyPair::new("test_keypair6"); + let network = networks::gnosis(); + let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap(); + + // Create a provider + let provider_result = create_provider(&network); + assert!(provider_result.is_ok()); + + // We can't actually test send_eth without a blockchain + // In a real test, we would mock the provider and test the function +} diff --git a/src/hero_vault/ethereum/tests/wallet_tests.rs b/src/hero_vault/ethereum/tests/wallet_tests.rs new file mode 100644 index 0000000..82e1f4b --- /dev/null +++ b/src/hero_vault/ethereum/tests/wallet_tests.rs @@ -0,0 +1,143 @@ +//! Tests for Ethereum wallet functionality. + +use crate::hero_vault::ethereum::*; +use crate::hero_vault::keypair::KeyPair; +use ethers::utils::hex; + +#[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); + + // The address should be a valid Ethereum address + assert!(wallet.address_string().starts_with("0x")); +} + +#[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); + + // The address should be a valid Ethereum address + assert!(wallet.address_string().starts_with("0x")); + + // 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(); + 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(); + 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); + + // The address should be a valid Ethereum address + assert!(wallet.address_string().starts_with("0x")); + + // The address should be deterministic based on the private key + let wallet2 = EthereumWallet::from_private_key(private_key, network.clone()).unwrap(); + assert_eq!(wallet.address, wallet2.address); +} + +#[test] +fn test_wallet_management() { + // Clear any existing wallets + clear_ethereum_wallets(); + + // Create a key space and keypair + crate::hero_vault::keypair::create_space("test_space").unwrap(); + crate::hero_vault::keypair::create_keypair("test_keypair3").unwrap(); + + // Create wallets for different networks + let gnosis_wallet = create_ethereum_wallet_for_network(networks::gnosis()).unwrap(); + let peaq_wallet = create_ethereum_wallet_for_network(networks::peaq()).unwrap(); + let agung_wallet = create_ethereum_wallet_for_network(networks::agung()).unwrap(); + + // Get the current wallets + let current_gnosis = get_current_ethereum_wallet_for_network("Gnosis").unwrap(); + let current_peaq = get_current_ethereum_wallet_for_network("Peaq").unwrap(); + let current_agung = get_current_ethereum_wallet_for_network("Agung").unwrap(); + + // Check that they match + assert_eq!(gnosis_wallet.address, current_gnosis.address); + assert_eq!(peaq_wallet.address, current_peaq.address); + assert_eq!(agung_wallet.address, current_agung.address); + + // Clear wallets for a specific network + clear_ethereum_wallets_for_network("Gnosis"); + + // Check that the wallet is gone + let result = get_current_ethereum_wallet_for_network("Gnosis"); + assert!(result.is_err()); + + // But the others should still be there + let current_peaq = get_current_ethereum_wallet_for_network("Peaq").unwrap(); + let current_agung = get_current_ethereum_wallet_for_network("Agung").unwrap(); + assert_eq!(peaq_wallet.address, current_peaq.address); + assert_eq!(agung_wallet.address, current_agung.address); + + // Clear all wallets + clear_ethereum_wallets(); + + // Check that all wallets are gone + let result1 = get_current_ethereum_wallet_for_network("Gnosis"); + let result2 = get_current_ethereum_wallet_for_network("Peaq"); + let result3 = get_current_ethereum_wallet_for_network("Agung"); + assert!(result1.is_err()); + assert!(result2.is_err()); + assert!(result3.is_err()); +} + +#[test] +fn test_sign_message() { + let keypair = KeyPair::new("test_keypair4"); + let network = networks::gnosis(); + + let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap(); + + // Create a tokio runtime for the async test + let rt = tokio::runtime::Runtime::new().unwrap(); + + // Sign a message + let message = b"Hello, world!"; + let signature = rt.block_on(wallet.sign_message(message)).unwrap(); + + // The signature should be a non-empty string + assert!(!signature.is_empty()); +} + +#[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(); + + // Get the private key as hex + let private_key_hex = wallet.private_key_hex(); + + // The private key should be a 64-character hex string (32 bytes) + assert_eq!(private_key_hex.len(), 64); + + // It should be possible to parse it as hex + let _bytes = hex::decode(private_key_hex).unwrap(); +} diff --git a/src/hero_vault/ethereum/transaction.rs b/src/hero_vault/ethereum/transaction.rs new file mode 100644 index 0000000..7c1deb5 --- /dev/null +++ b/src/hero_vault/ethereum/transaction.rs @@ -0,0 +1,54 @@ +//! Ethereum transaction functionality. + +use ethers::prelude::*; + +use crate::hero_vault::error::CryptoError; +use super::wallet::EthereumWallet; +use super::networks::NetworkConfig; + +/// Formats a token balance for display. +pub fn format_balance(balance: U256, network: &NetworkConfig) -> String { + let wei = balance.as_u128(); + let divisor = 10u128.pow(network.decimals as u32) as f64; + let token = wei as f64 / divisor; + + // Display with the appropriate number of decimal places + let display_decimals = std::cmp::min(6, network.decimals); + + format!("{:.*} {}", display_decimals as usize, token, network.token_symbol) +} + +/// Gets the balance of an Ethereum address. +pub async fn get_balance(provider: &Provider, address: Address) -> Result { + provider.get_balance(address, None) + .await + .map_err(|e| CryptoError::SerializationError(format!("Failed to get balance: {}", e))) +} + +/// Sends Ethereum from one address to another. +pub async fn send_eth( + wallet: &EthereumWallet, + provider: &Provider, + to: Address, + amount: U256, +) -> Result { + // Create a client with the wallet + let client = SignerMiddleware::new( + provider.clone(), + wallet.wallet.clone(), + ); + + // 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()) +} diff --git a/src/hero_vault/ethereum/wallet.rs b/src/hero_vault/ethereum/wallet.rs new file mode 100644 index 0000000..ecb73eb --- /dev/null +++ b/src/hero_vault/ethereum/wallet.rs @@ -0,0 +1,114 @@ +//! Ethereum wallet implementation. + +use ethers::prelude::*; +use ethers::signers::{LocalWallet, Signer, Wallet}; +use ethers::utils::hex; +use k256::ecdsa::SigningKey; +use std::str::FromStr; +use sha2::{Sha256, Digest}; + +use crate::hero_vault::error::CryptoError; +use crate::hero_vault::keypair::KeyPair; +use super::networks::NetworkConfig; + +/// An Ethereum wallet derived from a keypair. +#[derive(Debug, Clone)] +pub struct EthereumWallet { + pub address: Address, + pub wallet: Wallet, + pub network: NetworkConfig, +} + +impl EthereumWallet { + /// Creates a new Ethereum wallet from a keypair for a specific network. + pub fn from_keypair(keypair: &KeyPair, network: NetworkConfig) -> Result { + // Get the private key bytes from the keypair + let private_key_bytes = keypair.signing_key.to_bytes(); + + // Convert to a hex string (without 0x prefix) + let private_key_hex = hex::encode(private_key_bytes); + + // Create an Ethereum wallet from the private key + let wallet = LocalWallet::from_str(&private_key_hex) + .map_err(|_e| CryptoError::InvalidKeyLength)? + .with_chain_id(network.chain_id); + + // Get the Ethereum address + let address = wallet.address(); + + Ok(EthereumWallet { + address, + wallet, + network, + }) + } + + /// 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 { + // Get the private key bytes from the keypair + let private_key_bytes = keypair.signing_key.to_bytes(); + + // Create a deterministic seed by combining name and private key + let mut hasher = Sha256::default(); + hasher.update(name.as_bytes()); + hasher.update(&private_key_bytes); + let seed = hasher.finalize(); + + // Use the seed as a private key + let private_key_hex = hex::encode(seed); + + // Create an Ethereum wallet from the derived private key + let wallet = LocalWallet::from_str(&private_key_hex) + .map_err(|_e| CryptoError::InvalidKeyLength)? + .with_chain_id(network.chain_id); + + // Get the Ethereum address + let address = wallet.address(); + + Ok(EthereumWallet { + address, + wallet, + network, + }) + } + + /// Creates a new Ethereum wallet from a private key for a specific network. + pub fn from_private_key(private_key: &str, network: NetworkConfig) -> 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); + + // Get the Ethereum address + let address = wallet.address(); + + Ok(EthereumWallet { + address, + wallet, + network, + }) + } + + /// Gets the Ethereum address as a string. + pub fn address_string(&self) -> String { + format!("{:?}", self.address) + } + + /// Signs a message with the Ethereum wallet. + pub async fn sign_message(&self, message: &[u8]) -> Result { + let signature = self.wallet.sign_message(message) + .await + .map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?; + + Ok(signature.to_string()) + } + + /// Gets the private key as a hex string. + pub fn private_key_hex(&self) -> String { + let bytes = self.wallet.signer().to_bytes(); + hex::encode(bytes) + } +} diff --git a/src/hero_vault/keypair/README.md b/src/hero_vault/keypair/README.md new file mode 100644 index 0000000..b89f9cf --- /dev/null +++ b/src/hero_vault/keypair/README.md @@ -0,0 +1,187 @@ +# Hero Vault Keypair Module + +The Keypair module provides functionality for creating, managing, and using ECDSA keypairs for digital signatures and other cryptographic operations. + +## Module Structure + +The Keypair module is organized into: + +- `implementation.rs` - Core implementation of the KeyPair and KeySpace types +- `mod.rs` - Module exports and public interface + +## Key Types + +### KeyPair + +The `KeyPair` type represents an ECDSA keypair used for digital signatures and other cryptographic operations. + +```rust +pub struct KeyPair { + // Private fields + // ... +} + +impl KeyPair { + // Create a new random keypair + pub fn new() -> Result; + + // Create a keypair from an existing private key + pub fn from_private_key(private_key: &[u8]) -> Result; + + // Get the public key + pub fn public_key(&self) -> &[u8]; + + // Sign a message + pub fn sign(&self, message: &[u8]) -> Result, CryptoError>; + + // Verify a signature + pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result; + + // Derive an Ethereum address from the public key + pub fn to_ethereum_address(&self) -> Result; + + // Export the private key (should be used with caution) + pub fn export_private_key(&self) -> Result, CryptoError>; +} +``` + +### KeySpace + +The `KeySpace` type represents a secure container for multiple keypairs, which can be encrypted and stored on disk. + +```rust +pub struct KeySpace { + // Private fields + // ... +} + +impl KeySpace { + // Create a new key space + pub fn new(name: &str, password: &str) -> Result; + + // Load a key space from disk + pub fn load(name: &str, password: &str) -> Result; + + // Save the key space to disk + pub fn save(&self) -> Result<(), CryptoError>; + + // Create a new keypair in the key space + pub fn create_keypair(&mut self, name: &str, password: &str) -> Result<&KeyPair, CryptoError>; + + // Select a keypair for use + pub fn select_keypair(&mut self, name: &str) -> Result<&KeyPair, CryptoError>; + + // Get the currently selected keypair + pub fn current_keypair(&self) -> Result<&KeyPair, CryptoError>; + + // List all keypairs in the key space + pub fn list_keypairs(&self) -> Result, CryptoError>; + + // Get a keypair by name + pub fn get_keypair(&self, name: &str) -> Result<&KeyPair, CryptoError>; + + // Remove a keypair from the key space + pub fn remove_keypair(&mut self, name: &str) -> Result<(), CryptoError>; + + // Rename a keypair + pub fn rename_keypair(&mut self, old_name: &str, new_name: &str) -> Result<(), CryptoError>; + + // Get the name of the key space + pub fn name(&self) -> &str; +} +``` + +## Key Features + +### Key Space Management + +The module provides functionality for creating, loading, and managing key spaces: + +```rust +// Create a new key space +let mut space = KeySpace::new("my_space", "secure_password")?; + +// Save the key space to disk +space.save()?; + +// Load a key space from disk +let mut loaded_space = KeySpace::load("my_space", "secure_password")?; +``` + +### Keypair Management + +The module provides functionality for creating, selecting, and using keypairs: + +```rust +// Create a new keypair in the key space +let keypair = space.create_keypair("my_keypair", "secure_password")?; + +// Select a keypair for use +space.select_keypair("my_keypair")?; + +// Get the currently selected keypair +let current = space.current_keypair()?; + +// List all keypairs in the key space +let keypairs = space.list_keypairs()?; + +// Get a keypair by name +let keypair = space.get_keypair("my_keypair")?; + +// Remove a keypair from the key space +space.remove_keypair("my_keypair")?; + +// Rename a keypair +space.rename_keypair("my_keypair", "new_name")?; +``` + +### Digital Signatures + +The module provides functionality for signing and verifying messages using ECDSA: + +```rust +// Sign a message using the selected keypair +let keypair = space.current_keypair()?; +let signature = keypair.sign("This is a message to sign".as_bytes())?; + +// Verify a signature +let is_valid = keypair.verify("This is a message to sign".as_bytes(), &signature)?; +``` + +### Ethereum Address Derivation + +The module provides functionality for deriving Ethereum addresses from keypairs: + +```rust +// Derive an Ethereum address from a keypair +let keypair = space.current_keypair()?; +let address = keypair.to_ethereum_address()?; +``` + +## Security Considerations + +- Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password +- Private keys are never stored in plaintext +- The module uses secure random number generation for key creation +- All cryptographic operations use well-established libraries and algorithms + +## Error Handling + +The module uses the `CryptoError` type for handling errors that can occur during keypair operations: + +- `InvalidKeyLength` - Invalid key length +- `SignatureFormatError` - Signature format error +- `KeypairAlreadyExists` - Keypair already exists +- `KeypairNotFound` - Keypair not found +- `NoActiveSpace` - No active key space +- `NoKeypairSelected` - No keypair selected +- `SerializationError` - Serialization error + +## Examples + +For examples of how to use the Keypair module, see the `examples/hero_vault` directory, particularly: + +- `example.rhai` - Basic example demonstrating key management and signing +- `advanced_example.rhai` - Advanced example with error handling +- `key_persistence_example.rhai` - Demonstrates creating and saving a key space to disk +- `load_existing_space.rhai` - Shows how to load a previously created key space diff --git a/src/hero_vault/keypair/implementation.rs b/src/hero_vault/keypair/implementation.rs new file mode 100644 index 0000000..edd35c6 --- /dev/null +++ b/src/hero_vault/keypair/implementation.rs @@ -0,0 +1,467 @@ +//! Implementation of keypair functionality. + +use k256::ecdsa::{SigningKey, VerifyingKey, signature::{Signer, Verifier}, Signature}; +use rand::rngs::OsRng; +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; +use once_cell::sync::Lazy; +use std::sync::Mutex; +use sha2::{Sha256, Digest}; + +use crate::hero_vault::error::CryptoError; + +/// A keypair for signing and verifying messages. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeyPair { + pub name: String, + #[serde(with = "verifying_key_serde")] + pub verifying_key: VerifyingKey, + #[serde(with = "signing_key_serde")] + pub signing_key: SigningKey, +} + +// Serialization helpers for VerifyingKey +mod verifying_key_serde { + use super::*; + use serde::{Serializer, Deserializer}; + use serde::de::{self, Visitor}; + use std::fmt; + + pub fn serialize(key: &VerifyingKey, serializer: S) -> Result + where + S: Serializer, + { + let bytes = key.to_sec1_bytes(); + // Convert bytes to a Vec and serialize that instead + serializer.collect_seq(bytes) + } + + struct VerifyingKeyVisitor; + + impl<'de> Visitor<'de> for VerifyingKeyVisitor { + type Value = VerifyingKey; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array representing a verifying key") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: de::Error, + { + VerifyingKey::from_sec1_bytes(v).map_err(|e| { + log::error!("Error deserializing verifying key: {:?}", e); + E::custom(format!("invalid verifying key: {:?}", e)) + }) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + // Collect all bytes from the sequence + let mut bytes = Vec::new(); + while let Some(byte) = seq.next_element()? { + bytes.push(byte); + } + + VerifyingKey::from_sec1_bytes(&bytes).map_err(|e| { + log::error!("Error deserializing verifying key from seq: {:?}", e); + de::Error::custom(format!("invalid verifying key from seq: {:?}", e)) + }) + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Try to deserialize as bytes first, then as a sequence + deserializer.deserialize_any(VerifyingKeyVisitor) + } +} + +// Serialization helpers for SigningKey +mod signing_key_serde { + use super::*; + use serde::{Serializer, Deserializer}; + use serde::de::{self, Visitor}; + use std::fmt; + + pub fn serialize(key: &SigningKey, serializer: S) -> Result + where + S: Serializer, + { + let bytes = key.to_bytes(); + // Convert bytes to a Vec and serialize that instead + serializer.collect_seq(bytes) + } + + struct SigningKeyVisitor; + + impl<'de> Visitor<'de> for SigningKeyVisitor { + type Value = SigningKey; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array representing a signing key") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: de::Error, + { + SigningKey::from_bytes(v.into()).map_err(|e| { + log::error!("Error deserializing signing key: {:?}", e); + E::custom(format!("invalid signing key: {:?}", e)) + }) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + // Collect all bytes from the sequence + let mut bytes = Vec::new(); + while let Some(byte) = seq.next_element()? { + bytes.push(byte); + } + + SigningKey::from_bytes(bytes.as_slice().into()).map_err(|e| { + log::error!("Error deserializing signing key from seq: {:?}", e); + de::Error::custom(format!("invalid signing key from seq: {:?}", e)) + }) + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Try to deserialize as bytes first, then as a sequence + deserializer.deserialize_any(SigningKeyVisitor) + } +} + +impl KeyPair { + /// Creates a new keypair with the given name. + pub fn new(name: &str) -> Self { + let signing_key = SigningKey::random(&mut OsRng); + let verifying_key = VerifyingKey::from(&signing_key); + + KeyPair { + name: name.to_string(), + verifying_key, + signing_key, + } + } + + /// Gets the public key bytes. + pub fn pub_key(&self) -> Vec { + self.verifying_key.to_sec1_bytes().to_vec() + } + + /// Derives a public key from a private key. + pub fn pub_key_from_private(private_key: &[u8]) -> Result, CryptoError> { + let signing_key = SigningKey::from_bytes(private_key.into()) + .map_err(|_| CryptoError::InvalidKeyLength)?; + let verifying_key = VerifyingKey::from(&signing_key); + Ok(verifying_key.to_sec1_bytes().to_vec()) + } + + /// Signs a message. + pub fn sign(&self, message: &[u8]) -> Vec { + let signature: Signature = self.signing_key.sign(message); + signature.to_bytes().to_vec() + } + + /// Verifies a message signature. + pub fn verify(&self, message: &[u8], signature_bytes: &[u8]) -> Result { + let signature = Signature::from_bytes(signature_bytes.into()) + .map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?; + + match self.verifying_key.verify(message, &signature) { + Ok(_) => Ok(true), + Err(_) => Ok(false), // Verification failed, but operation was successful + } + } + + /// Verifies a message signature using only a public key. + pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result { + let verifying_key = VerifyingKey::from_sec1_bytes(public_key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + + let signature = Signature::from_bytes(signature_bytes.into()) + .map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?; + + match verifying_key.verify(message, &signature) { + Ok(_) => Ok(true), + Err(_) => Ok(false), // Verification failed, but operation was successful + } + } + + /// Encrypts a message using the recipient's public key. + /// This implements ECIES (Elliptic Curve Integrated Encryption Scheme): + /// 1. Generate an ephemeral keypair + /// 2. Derive a shared secret using ECDH + /// 3. Derive encryption key from the shared secret + /// 4. Encrypt the message using symmetric encryption + /// 5. Return the ephemeral public key and the ciphertext + pub fn encrypt_asymmetric(&self, recipient_public_key: &[u8], message: &[u8]) -> Result, CryptoError> { + // Parse recipient's public key + let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + + // Generate ephemeral keypair + let ephemeral_signing_key = SigningKey::random(&mut OsRng); + let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key); + + // Derive shared secret (this is a simplified ECDH) + // In a real implementation, we would use proper ECDH, but for this example: + let shared_point = recipient_key.to_encoded_point(false); + let shared_secret = { + let mut hasher = Sha256::default(); + hasher.update(ephemeral_signing_key.to_bytes()); + hasher.update(shared_point.as_bytes()); + hasher.finalize().to_vec() + }; + + // Encrypt the message using the derived key + let ciphertext = crate::hero_vault::symmetric::encrypt_with_key(&shared_secret, message) + .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; + + // Format: ephemeral_public_key || ciphertext + let mut result = ephemeral_public_key.to_sec1_bytes().to_vec(); + result.extend_from_slice(&ciphertext); + + Ok(result) + } + + /// Decrypts a message using the recipient's private key. + /// This is the counterpart to encrypt_asymmetric. + pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result, CryptoError> { + // The first 33 or 65 bytes (depending on compression) are the ephemeral public key + // For simplicity, we'll assume uncompressed keys (65 bytes) + if ciphertext.len() <= 65 { + return Err(CryptoError::DecryptionFailed("Ciphertext too short".to_string())); + } + + // Extract ephemeral public key and actual ciphertext + let ephemeral_public_key = &ciphertext[..65]; + let actual_ciphertext = &ciphertext[65..]; + + // Parse ephemeral public key + let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + + // Derive shared secret (simplified ECDH) + let shared_point = sender_key.to_encoded_point(false); + let shared_secret = { + let mut hasher = Sha256::default(); + hasher.update(self.signing_key.to_bytes()); + hasher.update(shared_point.as_bytes()); + hasher.finalize().to_vec() + }; + + // Decrypt the message using the derived key + crate::hero_vault::symmetric::decrypt_with_key(&shared_secret, actual_ciphertext) + .map_err(|e| CryptoError::DecryptionFailed(e.to_string())) + } +} + +/// A collection of keypairs. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct KeySpace { + pub name: String, + pub keypairs: HashMap, +} + +impl KeySpace { + /// Creates a new key space with the given name. + pub fn new(name: &str) -> Self { + KeySpace { + name: name.to_string(), + keypairs: HashMap::new(), + } + } + + /// Adds a new keypair to the space. + pub fn add_keypair(&mut self, name: &str) -> Result<(), CryptoError> { + if self.keypairs.contains_key(name) { + return Err(CryptoError::KeypairAlreadyExists(name.to_string())); + } + + let keypair = KeyPair::new(name); + self.keypairs.insert(name.to_string(), keypair); + Ok(()) + } + + /// Gets a keypair by name. + pub fn get_keypair(&self, name: &str) -> Result<&KeyPair, CryptoError> { + self.keypairs.get(name).ok_or(CryptoError::KeypairNotFound(name.to_string())) + } + + /// Lists all keypair names in the space. + pub fn list_keypairs(&self) -> Vec { + self.keypairs.keys().cloned().collect() + } +} + +/// Session state for the current key space and selected keypair. +pub struct Session { + pub current_space: Option, + pub selected_keypair: Option, +} + +impl Default for Session { + fn default() -> Self { + Session { + current_space: None, + selected_keypair: None, + } + } +} + +/// Global session state. +static SESSION: Lazy> = Lazy::new(|| { + Mutex::new(Session::default()) +}); + +/// Creates a new key space with the given name. +pub fn create_space(name: &str) -> Result<(), CryptoError> { + let mut session = SESSION.lock().unwrap(); + + // Create a new space + let space = KeySpace::new(name); + + // Set as current space + session.current_space = Some(space); + session.selected_keypair = None; + + Ok(()) +} + +/// Sets the current key space. +pub fn set_current_space(space: KeySpace) -> Result<(), CryptoError> { + let mut session = SESSION.lock().unwrap(); + session.current_space = Some(space); + session.selected_keypair = None; + Ok(()) +} + +/// Gets the current key space. +pub fn get_current_space() -> Result { + let session = SESSION.lock().unwrap(); + session.current_space.clone().ok_or(CryptoError::NoActiveSpace) +} + +/// Clears the current session (logout). +pub fn clear_session() { + let mut session = SESSION.lock().unwrap(); + session.current_space = None; + session.selected_keypair = None; +} + +/// Creates a new keypair in the current space. +pub fn create_keypair(name: &str) -> Result<(), CryptoError> { + let mut session = SESSION.lock().unwrap(); + + if let Some(ref mut space) = session.current_space { + if space.keypairs.contains_key(name) { + return Err(CryptoError::KeypairAlreadyExists(name.to_string())); + } + + let keypair = KeyPair::new(name); + space.keypairs.insert(name.to_string(), keypair); + + // Automatically select the new keypair + session.selected_keypair = Some(name.to_string()); + + Ok(()) + } else { + Err(CryptoError::NoActiveSpace) + } +} + +/// Selects a keypair for use. +pub fn select_keypair(name: &str) -> Result<(), CryptoError> { + let mut session = SESSION.lock().unwrap(); + + if let Some(ref space) = session.current_space { + if !space.keypairs.contains_key(name) { + return Err(CryptoError::KeypairNotFound(name.to_string())); + } + + session.selected_keypair = Some(name.to_string()); + Ok(()) + } else { + Err(CryptoError::NoActiveSpace) + } +} + +/// Gets the currently selected keypair. +pub fn get_selected_keypair() -> Result { + let session = SESSION.lock().unwrap(); + + if let Some(ref space) = session.current_space { + if let Some(ref keypair_name) = session.selected_keypair { + if let Some(keypair) = space.keypairs.get(keypair_name) { + return Ok(keypair.clone()); + } + return Err(CryptoError::KeypairNotFound(keypair_name.clone())); + } + return Err(CryptoError::NoKeypairSelected); + } + + Err(CryptoError::NoActiveSpace) +} + +/// Lists all keypair names in the current space. +pub fn list_keypairs() -> Result, CryptoError> { + let session = SESSION.lock().unwrap(); + + if let Some(ref space) = session.current_space { + Ok(space.keypairs.keys().cloned().collect()) + } else { + Err(CryptoError::NoActiveSpace) + } +} + +/// Gets the public key of the selected keypair. +pub fn keypair_pub_key() -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + Ok(keypair.pub_key()) +} + +/// Derives a public key from a private key. +pub fn derive_public_key(private_key: &[u8]) -> Result, CryptoError> { + KeyPair::pub_key_from_private(private_key) +} + +/// Signs a message with the selected keypair. +pub fn keypair_sign(message: &[u8]) -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + Ok(keypair.sign(message)) +} + +/// Verifies a message signature with the selected keypair. +pub fn keypair_verify(message: &[u8], signature_bytes: &[u8]) -> Result { + let keypair = get_selected_keypair()?; + keypair.verify(message, signature_bytes) +} + +/// Verifies a message signature with a public key. +pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result { + KeyPair::verify_with_public_key(public_key, message, signature_bytes) +} + +/// Encrypts a message for a recipient using their public key. +pub fn encrypt_asymmetric(recipient_public_key: &[u8], message: &[u8]) -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + keypair.encrypt_asymmetric(recipient_public_key, message) +} + +/// Decrypts a message that was encrypted with the current keypair's public key. +pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + keypair.decrypt_asymmetric(ciphertext) +} diff --git a/src/hero_vault/keypair/mod.rs b/src/hero_vault/keypair/mod.rs new file mode 100644 index 0000000..016b7f2 --- /dev/null +++ b/src/hero_vault/keypair/mod.rs @@ -0,0 +1,14 @@ +//! Key pair management functionality +//! +//! This module provides functionality for creating and managing ECDSA key pairs. + +mod implementation; + +// Re-export public types and functions +pub use implementation::{ + KeyPair, KeySpace, + create_space, set_current_space, get_current_space, clear_session, + create_keypair, select_keypair, get_selected_keypair, list_keypairs, + keypair_pub_key, derive_public_key, keypair_sign, keypair_verify, + verify_with_public_key, encrypt_asymmetric, decrypt_asymmetric +}; diff --git a/src/hero_vault/kvs/README.md b/src/hero_vault/kvs/README.md new file mode 100644 index 0000000..89d6d2e --- /dev/null +++ b/src/hero_vault/kvs/README.md @@ -0,0 +1,167 @@ +# Hero Vault Key-Value Store Module + +The Key-Value Store (KVS) module provides an encrypted key-value store for securely storing sensitive data. + +## Module Structure + +The KVS module is organized into: + +- `store.rs` - Core implementation of the key-value store +- `error.rs` - Error types specific to the KVS module +- `mod.rs` - Module exports and public interface + +## Key Types + +### KvStore + +The `KvStore` type represents an encrypted key-value store: + +```rust +pub struct KvStore { + // Private fields + // ... +} + +impl KvStore { + // Create a new store + pub fn new(name: &str, password: &str) -> Result; + + // Load a store from disk + pub fn load(name: &str, password: &str) -> Result; + + // Save the store to disk + pub fn save(&self) -> Result<(), CryptoError>; + + // Set a value + pub fn set(&mut self, key: &str, value: &str) -> Result<(), CryptoError>; + + // Get a value + pub fn get(&self, key: &str) -> Result, CryptoError>; + + // Delete a value + pub fn delete(&mut self, key: &str) -> Result<(), CryptoError>; + + // Check if a key exists + pub fn has(&self, key: &str) -> Result; + + // List all keys + pub fn keys(&self) -> Result, CryptoError>; + + // Clear all values + pub fn clear(&mut self) -> Result<(), CryptoError>; + + // Get the name of the store + pub fn name(&self) -> &str; +} +``` + +## Key Features + +### Store Management + +The module provides functionality for creating, loading, and managing key-value stores: + +```rust +// Create a new store +let mut store = KvStore::new("my_store", "secure_password")?; + +// Save the store to disk +store.save()?; + +// Load a store from disk +let mut loaded_store = KvStore::load("my_store", "secure_password")?; +``` + +### Value Management + +The module provides functionality for managing values in the store: + +```rust +// Set a value +store.set("api_key", "secret_api_key")?; + +// Get a value +let api_key = store.get("api_key")?; + +// Delete a value +store.delete("api_key")?; + +// Check if a key exists +let exists = store.has("api_key")?; + +// List all keys +let keys = store.keys()?; + +// Clear all values +store.clear()?; +``` + +## Technical Details + +### Encryption + +The KVS module uses the Symmetric Encryption module to encrypt all values stored in the key-value store. This ensures that sensitive data is protected at rest. + +The encryption process: + +1. A master key is derived from the provided password using PBKDF2 +2. Each value is encrypted using ChaCha20Poly1305 with a unique key derived from the master key and the value's key +3. The encrypted values are stored in a JSON file on disk + +### Storage Format + +The key-value store is stored in a JSON file with the following structure: + +```json +{ + "name": "my_store", + "salt": "base64-encoded-salt", + "values": { + "key1": "base64-encoded-encrypted-value", + "key2": "base64-encoded-encrypted-value", + ... + } +} +``` + +The file is stored in the `~/.hero-vault/stores/` directory by default. + +## Security Considerations + +- Use strong passwords to protect the key-value store +- The security of the store depends on the strength of the password +- Consider the security implications of storing sensitive data on disk +- Regularly backup the store to prevent data loss + +## Error Handling + +The module uses the `CryptoError` type for handling errors that can occur during key-value store operations: + +- `EncryptionFailed` - Encryption failed +- `DecryptionFailed` - Decryption failed +- `SerializationError` - Serialization error + +## 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: + +```rust +// Create a new store +let mut store = KvStore::new("my_store", "secure_password")?; + +// Set some values +store.set("api_key", "secret_api_key")?; +store.set("access_token", "secret_access_token")?; + +// Save the store to disk +store.save()?; + +// Later, load the store +let loaded_store = KvStore::load("my_store", "secure_password")?; + +// Get a value +let api_key = loaded_store.get("api_key")?; +println!("API Key: {}", api_key.unwrap_or_default()); +``` diff --git a/src/hero_vault/kvs/error.rs b/src/hero_vault/kvs/error.rs new file mode 100644 index 0000000..039644e --- /dev/null +++ b/src/hero_vault/kvs/error.rs @@ -0,0 +1,66 @@ +//! Error types for the key-value store. + +use std::fmt; +use thiserror::Error; + +/// Errors that can occur when using the key-value store. +#[derive(Debug, Error)] +pub enum KvsError { + /// I/O error + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + /// Key not found + #[error("Key not found: {0}")] + KeyNotFound(String), + + /// Store not found + #[error("Store not found: {0}")] + StoreNotFound(String), + + /// Serialization error + #[error("Serialization error: {0}")] + Serialization(String), + + /// Deserialization error + #[error("Deserialization error: {0}")] + Deserialization(String), + + /// Encryption error + #[error("Encryption error: {0}")] + Encryption(String), + + /// Decryption error + #[error("Decryption error: {0}")] + Decryption(String), + + /// Other error + #[error("Error: {0}")] + Other(String), +} + +impl From for KvsError { + fn from(err: serde_json::Error) -> Self { + KvsError::Serialization(err.to_string()) + } +} + +impl From for crate::hero_vault::error::CryptoError { + fn from(err: KvsError) -> Self { + crate::hero_vault::error::CryptoError::SerializationError(err.to_string()) + } +} + +impl From for KvsError { + fn from(err: crate::hero_vault::error::CryptoError) -> Self { + match err { + crate::hero_vault::error::CryptoError::EncryptionFailed(msg) => KvsError::Encryption(msg), + crate::hero_vault::error::CryptoError::DecryptionFailed(msg) => KvsError::Decryption(msg), + crate::hero_vault::error::CryptoError::SerializationError(msg) => KvsError::Serialization(msg), + _ => KvsError::Other(err.to_string()), + } + } +} + +/// Result type for key-value store operations. +pub type Result = std::result::Result; diff --git a/src/hero_vault/kvs/mod.rs b/src/hero_vault/kvs/mod.rs new file mode 100644 index 0000000..890333d --- /dev/null +++ b/src/hero_vault/kvs/mod.rs @@ -0,0 +1,14 @@ +//! Key-Value Store functionality +//! +//! This module provides a simple key-value store with encryption support. + +mod error; +mod store; + +// Re-export public types and functions +pub use error::KvsError; +pub use store::{ + KvStore, KvPair, + create_store, open_store, delete_store, + list_stores, get_store_path +}; diff --git a/src/hero_vault/kvs/store.rs b/src/hero_vault/kvs/store.rs new file mode 100644 index 0000000..6ea279f --- /dev/null +++ b/src/hero_vault/kvs/store.rs @@ -0,0 +1,362 @@ +//! Implementation of a simple key-value store using the filesystem. + +use crate::hero_vault::kvs::error::{KvsError, Result}; +use crate::hero_vault::symmetric; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +/// A key-value pair. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KvPair { + pub key: String, + pub value: String, +} + +/// A simple key-value store. +/// +/// This implementation uses the filesystem to store key-value pairs. +#[derive(Clone)] +pub struct KvStore { + /// The name of the store + name: String, + /// The path to the store file + path: PathBuf, + /// In-memory cache of the store data + data: Arc>>, + /// Whether the store is encrypted + encrypted: bool, + /// The password for encryption (if encrypted) + password: Option, +} + +/// Gets the path to the key-value store directory. +pub fn get_store_path() -> PathBuf { + let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); + home_dir.join(".hero-vault").join("kvs") +} + +/// Creates a new key-value store with the given name. +/// +/// # 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 `KvStore` instance +pub fn create_store(name: &str, encrypted: bool, password: Option<&str>) -> Result { + // 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_store_path(); + if !store_dir.exists() { + fs::create_dir_all(&store_dir)?; + } + + // Create the store file path + let store_path = store_dir.join(format!("{}.json", name)); + + // Create an empty store + let store = KvStore { + name: name.to_string(), + path: store_path, + data: Arc::new(Mutex::new(HashMap::new())), + encrypted, + password: password.map(|s| s.to_string()), + }; + + // Save the empty store + store.save()?; + + Ok(store) +} + +/// Opens an existing key-value store with the given name. +/// +/// # Arguments +/// +/// * `name` - The name of the store +/// * `password` - The password for decryption (required if the store is encrypted) +/// +/// # Returns +/// +/// The opened `KvStore` instance +pub fn open_store(name: &str, password: Option<&str>) -> Result { + // Get the store file path + let store_dir = get_store_path(); + let store_path = store_dir.join(format!("{}.json", name)); + + // Check if the store exists + if !store_path.exists() { + return Err(KvsError::StoreNotFound(name.to_string())); + } + + // Read the store file + let file_content = fs::read_to_string(&store_path)?; + + // Check if the file is encrypted (simple heuristic) + let is_encrypted = !file_content.starts_with('{'); + + // If encrypted, we need a password + if is_encrypted && password.is_none() { + return Err(KvsError::Other("Password required for encrypted store".to_string())); + } + + // Parse the store data + let data: HashMap = if is_encrypted { + // Decrypt the file content + let password = password.unwrap(); + let encrypted_data: Vec = serde_json::from_str(&file_content)?; + let key = symmetric::derive_key_from_password(password); + let decrypted_data = symmetric::decrypt_symmetric(&key, &encrypted_data)?; + let decrypted_str = String::from_utf8(decrypted_data) + .map_err(|e| KvsError::Deserialization(e.to_string()))?; + serde_json::from_str(&decrypted_str)? + } else { + serde_json::from_str(&file_content)? + }; + + // Create the store + let store = KvStore { + name: name.to_string(), + path: store_path, + data: Arc::new(Mutex::new(data)), + encrypted: is_encrypted, + password: password.map(|s| s.to_string()), + }; + + Ok(store) +} + +/// Deletes a key-value store with the given name. +/// +/// # Arguments +/// +/// * `name` - The name of the store to delete +/// +/// # Returns +/// +/// `Ok(())` if the operation was successful +pub fn delete_store(name: &str) -> Result<()> { + // Get the store file path + let store_dir = get_store_path(); + let store_path = store_dir.join(format!("{}.json", name)); + + // Check if the store exists + if !store_path.exists() { + return Err(KvsError::StoreNotFound(name.to_string())); + } + + // Delete the store file + fs::remove_file(store_path)?; + + Ok(()) +} + +/// Lists all available key-value stores. +/// +/// # Returns +/// +/// A vector of store names +pub fn list_stores() -> Result> { + // Get the store directory + let store_dir = get_store_path(); + if !store_dir.exists() { + return Ok(Vec::new()); + } + + // List all JSON files in the directory + let mut stores = Vec::new(); + for entry in fs::read_dir(store_dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "json") { + if let Some(name) = path.file_stem() { + if let Some(name_str) = name.to_str() { + stores.push(name_str.to_string()); + } + } + } + } + + Ok(stores) +} + +impl KvStore { + /// Saves the store to disk. + fn save(&self) -> Result<()> { + // Get the store data + let data = self.data.lock().unwrap(); + + // Serialize the data + let serialized = serde_json::to_string(&*data)?; + + // Write to file + if self.encrypted { + if let Some(password) = &self.password { + // Encrypt the data + let key = symmetric::derive_key_from_password(password); + let encrypted_data = symmetric::encrypt_symmetric(&key, serialized.as_bytes())?; + let encrypted_json = serde_json::to_string(&encrypted_data)?; + fs::write(&self.path, encrypted_json)?; + } else { + return Err(KvsError::Other("Password required for encrypted store".to_string())); + } + } else { + fs::write(&self.path, serialized)?; + } + + Ok(()) + } + + /// Stores a value with the given key. + /// + /// # Arguments + /// + /// * `key` - The key to store the value under + /// * `value` - The value to store + /// + /// # Returns + /// + /// `Ok(())` if the operation was successful + pub fn set(&self, key: K, value: &V) -> Result<()> + where + K: ToString, + V: Serialize, + { + let key_str = key.to_string(); + let serialized = serde_json::to_string(value)?; + + // Update in-memory data + { + let mut data = self.data.lock().unwrap(); + data.insert(key_str, serialized); + } + + // Save to disk + self.save()?; + + Ok(()) + } + + /// Retrieves a value for the given key. + /// + /// # Arguments + /// + /// * `key` - The key to retrieve the value for + /// + /// # Returns + /// + /// The value if found, or `Err(KvsError::KeyNotFound)` if not found + pub fn get(&self, key: K) -> Result + where + K: ToString, + V: DeserializeOwned, + { + let key_str = key.to_string(); + let data = self.data.lock().unwrap(); + + match data.get(&key_str) { + Some(serialized) => { + let value = serde_json::from_str(serialized)?; + Ok(value) + }, + None => Err(KvsError::KeyNotFound(key_str)), + } + } + + /// Deletes a value for the given key. + /// + /// # Arguments + /// + /// * `key` - The key to delete + /// + /// # Returns + /// + /// `Ok(())` if the operation was successful + pub fn delete(&self, key: K) -> Result<()> + where + K: ToString, + { + let key_str = key.to_string(); + + // Update in-memory data + { + let mut data = self.data.lock().unwrap(); + if data.remove(&key_str).is_none() { + return Err(KvsError::KeyNotFound(key_str)); + } + } + + // Save to disk + self.save()?; + + Ok(()) + } + + /// Checks if a key exists in the store. + /// + /// # Arguments + /// + /// * `key` - The key to check + /// + /// # Returns + /// + /// `true` if the key exists, `false` otherwise + pub fn contains(&self, key: K) -> Result + where + K: ToString, + { + let key_str = key.to_string(); + let data = self.data.lock().unwrap(); + + Ok(data.contains_key(&key_str)) + } + + /// Lists all keys in the store. + /// + /// # Returns + /// + /// A vector of keys as strings + pub fn keys(&self) -> Result> { + let data = self.data.lock().unwrap(); + + Ok(data.keys().cloned().collect()) + } + + /// Clears all key-value pairs from the store. + /// + /// # Returns + /// + /// `Ok(())` if the operation was successful + pub fn clear(&self) -> Result<()> { + // Update in-memory data + { + let mut data = self.data.lock().unwrap(); + data.clear(); + } + + // Save to disk + self.save()?; + + Ok(()) + } + + /// Gets the name of the store. + pub fn name(&self) -> &str { + &self.name + } + + /// Gets whether the store is encrypted. + pub fn is_encrypted(&self) -> bool { + self.encrypted + } +} diff --git a/src/hero_vault/mod.rs b/src/hero_vault/mod.rs new file mode 100644 index 0000000..0a301ca --- /dev/null +++ b/src/hero_vault/mod.rs @@ -0,0 +1,19 @@ +//! Hero Vault: Cryptographic functionality for SAL +//! +//! This module provides cryptographic operations including: +//! - Key space management (creation, loading, encryption, decryption) +//! - Key pair management (ECDSA) +//! - Digital signatures (signing and verification) +//! - Symmetric encryption (ChaCha20Poly1305) +//! - Ethereum wallet functionality +//! - Key-value store with encryption + +pub mod error; +pub mod keypair; +pub mod symmetric; +pub mod ethereum; +pub mod kvs; + +// Re-export common types for convenience +pub use error::CryptoError; +pub use keypair::{KeyPair, KeySpace}; diff --git a/src/hero_vault/symmetric/README.md b/src/hero_vault/symmetric/README.md new file mode 100644 index 0000000..18359ec --- /dev/null +++ b/src/hero_vault/symmetric/README.md @@ -0,0 +1,98 @@ +# Hero Vault Symmetric Encryption Module + +The Symmetric Encryption module provides functionality for symmetric encryption and decryption using the ChaCha20Poly1305 algorithm. + +## Module Structure + +The Symmetric Encryption module is organized into: + +- `implementation.rs` - Core implementation of symmetric encryption functionality +- `mod.rs` - Module exports and public interface + +## Key Features + +### Key Generation + +The module provides functionality for generating secure symmetric keys: + +```rust +// Generate a new symmetric key +let key = generate_key()?; +``` + +### Encryption + +The module provides functionality for encrypting data using ChaCha20Poly1305: + +```rust +// Encrypt data +let encrypted = encrypt(&key, "This is a secret message")?; +``` + +### Decryption + +The module provides functionality for decrypting data encrypted with ChaCha20Poly1305: + +```rust +// Decrypt data +let decrypted = decrypt(&key, &encrypted)?; +``` + +### Password-Based Key Derivation + +The module provides functionality for deriving encryption keys from passwords: + +```rust +// Derive a key from a password +let key = derive_key_from_password(password, salt)?; +``` + +## Technical Details + +### ChaCha20Poly1305 + +The module uses the ChaCha20Poly1305 authenticated encryption with associated data (AEAD) algorithm, which provides both confidentiality and integrity protection. + +ChaCha20 is a stream cipher designed by Daniel J. Bernstein, which is combined with the Poly1305 message authentication code to provide authenticated encryption. + +Key features of ChaCha20Poly1305: + +- 256-bit key +- 96-bit nonce (used once) +- Authentication tag to verify integrity +- High performance on modern processors +- Resistance to timing attacks + +### Key Derivation + +For password-based encryption, the module uses the PBKDF2 (Password-Based Key Derivation Function 2) algorithm to derive encryption keys from passwords. + +Key features of PBKDF2: + +- Configurable iteration count to increase computational cost +- Salt to prevent rainbow table attacks +- Configurable output key length +- Uses HMAC-SHA256 as the underlying pseudorandom function + +## Security Considerations + +- Always use a unique key for each encryption operation +- Never reuse nonces with the same key +- Store keys securely +- Use strong passwords for password-based encryption +- Consider the security implications of storing encrypted data + +## Error Handling + +The module uses the `CryptoError` type for handling errors that can occur during symmetric encryption operations: + +- `InvalidKeyLength` - Invalid key length +- `EncryptionFailed` - Encryption failed +- `DecryptionFailed` - Decryption failed + +## Examples + +For examples of how to use the Symmetric Encryption module, see the `examples/hero_vault` directory, particularly: + +- `example.rhai` - Basic example demonstrating symmetric encryption +- `advanced_example.rhai` - Advanced example with error handling diff --git a/src/hero_vault/symmetric/implementation.rs b/src/hero_vault/symmetric/implementation.rs new file mode 100644 index 0000000..476470e --- /dev/null +++ b/src/hero_vault/symmetric/implementation.rs @@ -0,0 +1,266 @@ +//! Implementation of symmetric encryption functionality. + +use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce}; +use chacha20poly1305::aead::Aead; +use rand::{rngs::OsRng, RngCore}; +use serde::{Serialize, Deserialize}; +use sha2::{Sha256, Digest}; + +use crate::hero_vault::error::CryptoError; +use crate::hero_vault::keypair::KeySpace; + +/// The size of the nonce in bytes. +const NONCE_SIZE: usize = 12; + +/// Generates a random 32-byte symmetric key. +/// +/// # Returns +/// +/// A 32-byte array containing the random key. +pub fn generate_symmetric_key() -> [u8; 32] { + let mut key = [0u8; 32]; + OsRng.fill_bytes(&mut key); + key +} + +/// Derives a 32-byte key from a password. +/// +/// # Arguments +/// +/// * `password` - The password to derive the key from. +/// +/// # Returns +/// +/// A 32-byte array containing the derived key. +pub fn derive_key_from_password(password: &str) -> [u8; 32] { + let mut hasher = Sha256::default(); + hasher.update(password.as_bytes()); + let result = hasher.finalize(); + + let mut key = [0u8; 32]; + key.copy_from_slice(&result); + key +} + +/// Encrypts data using ChaCha20Poly1305 with an internally generated nonce. +/// +/// The nonce is appended to the ciphertext so it can be extracted during decryption. +/// +/// # Arguments +/// +/// * `key` - The encryption key (should be 32 bytes). +/// * `message` - The message to encrypt. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the ciphertext with the nonce appended. +/// * `Err(CryptoError::InvalidKeyLength)` if the key length is invalid. +/// * `Err(CryptoError::EncryptionFailed)` if encryption fails. +pub fn encrypt_symmetric(key: &[u8], message: &[u8]) -> Result, CryptoError> { + // Create cipher + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + + // Generate random nonce + let mut nonce_bytes = [0u8; NONCE_SIZE]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + // Encrypt message + let ciphertext = cipher.encrypt(nonce, message) + .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; + + // Append nonce to ciphertext + let mut result = ciphertext; + result.extend_from_slice(&nonce_bytes); + + Ok(result) +} + +/// Decrypts data using ChaCha20Poly1305, extracting the nonce from the ciphertext. +/// +/// # Arguments +/// +/// * `key` - The decryption key (should be 32 bytes). +/// * `ciphertext_with_nonce` - The ciphertext with the nonce appended. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the decrypted message. +/// * `Err(CryptoError::InvalidKeyLength)` if the key length is invalid. +/// * `Err(CryptoError::DecryptionFailed)` if decryption fails or the ciphertext is too short. +pub fn decrypt_symmetric(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result, CryptoError> { + // Check if ciphertext is long enough to contain a nonce + if ciphertext_with_nonce.len() <= NONCE_SIZE { + return Err(CryptoError::DecryptionFailed("Ciphertext too short".to_string())); + } + + // Extract nonce from the end of ciphertext + let ciphertext_len = ciphertext_with_nonce.len() - NONCE_SIZE; + let ciphertext = &ciphertext_with_nonce[0..ciphertext_len]; + let nonce_bytes = &ciphertext_with_nonce[ciphertext_len..]; + + // Create cipher + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + + let nonce = Nonce::from_slice(nonce_bytes); + + // Decrypt message + cipher.decrypt(nonce, ciphertext) + .map_err(|e| CryptoError::DecryptionFailed(e.to_string())) +} + +/// Encrypts data using a key directly (for internal use). +/// +/// # Arguments +/// +/// * `key` - The encryption key. +/// * `message` - The message to encrypt. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the ciphertext with the nonce appended. +/// * `Err(CryptoError)` if encryption fails. +pub fn encrypt_with_key(key: &[u8], message: &[u8]) -> Result, CryptoError> { + encrypt_symmetric(key, message) +} + +/// Decrypts data using a key directly (for internal use). +/// +/// # Arguments +/// +/// * `key` - The decryption key. +/// * `ciphertext_with_nonce` - The ciphertext with the nonce appended. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the decrypted message. +/// * `Err(CryptoError)` if decryption fails. +pub fn decrypt_with_key(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result, CryptoError> { + decrypt_symmetric(key, ciphertext_with_nonce) +} + +/// Metadata for an encrypted key space. +#[derive(Serialize, Deserialize, Debug)] +pub struct EncryptedKeySpaceMetadata { + pub name: String, + pub created_at: u64, + pub last_accessed: u64, +} + +/// An encrypted key space with metadata. +#[derive(Serialize, Deserialize, Debug)] +pub struct EncryptedKeySpace { + pub metadata: EncryptedKeySpaceMetadata, + pub encrypted_data: Vec, +} + +/// Encrypts a key space using a password. +/// +/// # Arguments +/// +/// * `space` - The key space to encrypt. +/// * `password` - The password to encrypt with. +/// +/// # Returns +/// +/// * `Ok(EncryptedKeySpace)` containing the encrypted key space. +/// * `Err(CryptoError)` if encryption fails. +pub fn encrypt_key_space(space: &KeySpace, password: &str) -> Result { + // Serialize the key space + let serialized = match serde_json::to_vec(space) { + Ok(data) => data, + Err(e) => { + log::error!("Serialization error during encryption: {}", e); + return Err(CryptoError::SerializationError(e.to_string())); + } + }; + + // Derive key from password + let key = derive_key_from_password(password); + + // Encrypt the serialized data + let encrypted_data = encrypt_symmetric(&key, &serialized)?; + + // Create metadata + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + let metadata = EncryptedKeySpaceMetadata { + name: space.name.clone(), + created_at: now, + last_accessed: now, + }; + + Ok(EncryptedKeySpace { + metadata, + encrypted_data, + }) +} + +/// Decrypts a key space using a password. +/// +/// # Arguments +/// +/// * `encrypted_space` - The encrypted key space. +/// * `password` - The password to decrypt with. +/// +/// # Returns +/// +/// * `Ok(KeySpace)` containing the decrypted key space. +/// * `Err(CryptoError)` if decryption fails. +pub fn decrypt_key_space(encrypted_space: &EncryptedKeySpace, password: &str) -> Result { + // Derive key from password + let key = derive_key_from_password(password); + + // Decrypt the data + let decrypted_data = decrypt_symmetric(&key, &encrypted_space.encrypted_data)?; + + // Deserialize the key space + let space: KeySpace = match serde_json::from_slice(&decrypted_data) { + Ok(space) => space, + Err(e) => { + log::error!("Deserialization error: {}", e); + return Err(CryptoError::SerializationError(e.to_string())); + } + }; + + Ok(space) +} + +/// Serializes an encrypted key space to a JSON string. +/// +/// # Arguments +/// +/// * `encrypted_space` - The encrypted key space to serialize. +/// +/// # Returns +/// +/// * `Ok(String)` containing the serialized encrypted key space. +/// * `Err(CryptoError)` if serialization fails. +pub fn serialize_encrypted_space(encrypted_space: &EncryptedKeySpace) -> Result { + serde_json::to_string(encrypted_space) + .map_err(|e| CryptoError::SerializationError(e.to_string())) +} + +/// Deserializes an encrypted key space from a JSON string. +/// +/// # Arguments +/// +/// * `serialized` - The serialized encrypted key space. +/// +/// # Returns +/// +/// * `Ok(EncryptedKeySpace)` containing the deserialized encrypted key space. +/// * `Err(CryptoError)` if deserialization fails. +pub fn deserialize_encrypted_space(serialized: &str) -> Result { + match serde_json::from_str(serialized) { + Ok(space) => Ok(space), + Err(e) => { + log::error!("Error deserializing encrypted space: {}", e); + Err(CryptoError::SerializationError(e.to_string())) + } + } +} diff --git a/src/hero_vault/symmetric/mod.rs b/src/hero_vault/symmetric/mod.rs new file mode 100644 index 0000000..5b2f24a --- /dev/null +++ b/src/hero_vault/symmetric/mod.rs @@ -0,0 +1,15 @@ +//! Symmetric encryption functionality +//! +//! This module provides functionality for symmetric encryption using ChaCha20Poly1305. + +mod implementation; + +// Re-export public types and functions +pub use implementation::{ + generate_symmetric_key, derive_key_from_password, + encrypt_symmetric, decrypt_symmetric, + encrypt_with_key, decrypt_with_key, + encrypt_key_space, decrypt_key_space, + serialize_encrypted_space, deserialize_encrypted_space, + EncryptedKeySpace, EncryptedKeySpaceMetadata +}; diff --git a/src/lib.rs b/src/lib.rs index cbc8046..2d390d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ //! - System information //! - Network operations //! - Environment variables +//! - Cryptographic operations use std::io; use thiserror::Error; @@ -45,6 +46,7 @@ pub mod redisclient; pub mod rhai; pub mod text; pub mod virt; +pub mod hero_vault; // Version information /// Returns the version of the SAL library diff --git a/src/rhai/hero_vault.rs b/src/rhai/hero_vault.rs new file mode 100644 index 0000000..7ccd9d4 --- /dev/null +++ b/src/rhai/hero_vault.rs @@ -0,0 +1,943 @@ +//! Rhai bindings for SAL crypto functionality + +use rhai::{Engine, Dynamic, EvalAltResult}; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +use std::fs; +use std::path::PathBuf; +use std::collections::HashMap; +use std::sync::Mutex; +use once_cell::sync::Lazy; +use tokio::runtime::Runtime; +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}; + +// 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 + let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); + let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces"); + + // Check if directory exists + if !key_spaces_dir.exists() { + log::error!("Key spaces directory does not exist"); + return false; + } + + // Get the key space file path + let space_path = key_spaces_dir.join(format!("{}.json", name)); + + // Check if file exists + if !space_path.exists() { + log::error!("Key space file not found: {}", space_path.display()); + return false; + } + + // Read the file + let serialized = match fs::read_to_string(&space_path) { + Ok(data) => data, + Err(e) => { + log::error!("Error reading key space file: {}", e); + return false; + } + }; + + // Deserialize the encrypted space + let encrypted_space = match symmetric::deserialize_encrypted_space(&serialized) { + Ok(space) => space, + Err(e) => { + log::error!("Error deserializing key space: {}", e); + return false; + } + }; + + // Decrypt the space + let space = match symmetric::decrypt_key_space(&encrypted_space, password) { + Ok(space) => space, + Err(e) => { + log::error!("Error decrypting key space: {}", e); + return false; + } + }; + + // Set as current space + match keypair::set_current_space(space) { + Ok(_) => true, + Err(e) => { + log::error!("Error setting current space: {}", e); + false + } + } +} + +fn create_key_space(name: &str, password: &str) -> bool { + match keypair::create_space(name) { + Ok(_) => { + // Get the current space + match keypair::get_current_space() { + Ok(space) => { + // Encrypt the key space + let encrypted_space = match symmetric::encrypt_key_space(&space, password) { + Ok(encrypted) => encrypted, + Err(e) => { + log::error!("Error encrypting key space: {}", e); + return false; + } + }; + + // Serialize the encrypted space + let serialized = match symmetric::serialize_encrypted_space(&encrypted_space) { + Ok(json) => json, + Err(e) => { + log::error!("Error serializing encrypted space: {}", e); + return false; + } + }; + + // Get the key spaces directory + let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); + let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces"); + + // Create directory if it doesn't exist + if !key_spaces_dir.exists() { + match fs::create_dir_all(&key_spaces_dir) { + Ok(_) => {}, + Err(e) => { + log::error!("Error creating key spaces directory: {}", e); + return false; + } + } + } + + // Write to file + let space_path = key_spaces_dir.join(format!("{}.json", name)); + match fs::write(&space_path, serialized) { + Ok(_) => { + log::info!("Key space created and saved to {}", space_path.display()); + true + }, + Err(e) => { + log::error!("Error writing key space file: {}", e); + false + } + } + }, + Err(e) => { + log::error!("Error getting current space: {}", e); + false + } + } + }, + Err(e) => { + log::error!("Error creating key space: {}", e); + false + } + } +} + +// Auto-save function for internal use +fn auto_save_key_space(password: &str) -> bool { + match keypair::get_current_space() { + Ok(space) => { + // Encrypt the key space + let encrypted_space = match symmetric::encrypt_key_space(&space, password) { + Ok(encrypted) => encrypted, + Err(e) => { + log::error!("Error encrypting key space: {}", e); + return false; + } + }; + + // Serialize the encrypted space + let serialized = match symmetric::serialize_encrypted_space(&encrypted_space) { + Ok(json) => json, + Err(e) => { + log::error!("Error serializing encrypted space: {}", e); + return false; + } + }; + + // Get the key spaces directory + let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); + let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces"); + + // Create directory if it doesn't exist + if !key_spaces_dir.exists() { + match fs::create_dir_all(&key_spaces_dir) { + Ok(_) => {}, + Err(e) => { + log::error!("Error creating key spaces directory: {}", e); + return false; + } + } + } + + // Write to file + let space_path = key_spaces_dir.join(format!("{}.json", space.name)); + match fs::write(&space_path, serialized) { + Ok(_) => { + log::info!("Key space saved to {}", space_path.display()); + true + }, + Err(e) => { + log::error!("Error writing key space file: {}", e); + false + } + } + }, + Err(e) => { + log::error!("Error getting current space: {}", e); + false + } + } +} + +fn encrypt_key_space(password: &str) -> String { + match keypair::get_current_space() { + Ok(space) => { + match symmetric::encrypt_key_space(&space, password) { + Ok(encrypted_space) => { + match serde_json::to_string(&encrypted_space) { + Ok(json) => json, + Err(e) => { + log::error!("Error serializing encrypted space: {}", e); + String::new() + } + } + }, + Err(e) => { + log::error!("Error encrypting key space: {}", e); + String::new() + } + } + }, + Err(e) => { + log::error!("Error getting current space: {}", e); + String::new() + } + } +} + +fn decrypt_key_space(encrypted: &str, password: &str) -> bool { + match serde_json::from_str(encrypted) { + Ok(encrypted_space) => { + match symmetric::decrypt_key_space(&encrypted_space, password) { + Ok(space) => { + match keypair::set_current_space(space) { + Ok(_) => true, + Err(e) => { + log::error!("Error setting current space: {}", e); + false + } + } + }, + Err(e) => { + log::error!("Error decrypting key space: {}", e); + false + } + } + }, + Err(e) => { + log::error!("Error parsing encrypted space: {}", e); + false + } + } +} + +// Keypair management functions +fn create_keypair(name: &str, password: &str) -> bool { + match keypair::create_keypair(name) { + Ok(_) => { + // Auto-save the key space after creating a keypair + auto_save_key_space(password) + }, + Err(e) => { + log::error!("Error creating keypair: {}", e); + false + } + } +} + +fn select_keypair(name: &str) -> bool { + match keypair::select_keypair(name) { + Ok(_) => true, + Err(e) => { + log::error!("Error selecting keypair: {}", e); + false + } + } +} + +fn list_keypairs() -> Vec { + match keypair::list_keypairs() { + Ok(keypairs) => keypairs, + Err(e) => { + log::error!("Error listing keypairs: {}", e); + Vec::new() + } + } +} + +// Cryptographic operations +fn sign(message: &str) -> String { + let message_bytes = message.as_bytes(); + match keypair::keypair_sign(message_bytes) { + Ok(signature) => BASE64.encode(signature), + Err(e) => { + log::error!("Error signing message: {}", e); + String::new() + } + } +} + +fn verify(message: &str, signature: &str) -> bool { + let message_bytes = message.as_bytes(); + match BASE64.decode(signature) { + Ok(signature_bytes) => { + match keypair::keypair_verify(message_bytes, &signature_bytes) { + Ok(is_valid) => is_valid, + Err(e) => { + log::error!("Error verifying signature: {}", e); + false + } + } + }, + Err(e) => { + log::error!("Error decoding signature: {}", e); + false + } + } +} + +// Symmetric encryption +fn generate_key() -> String { + let key = symmetric::generate_symmetric_key(); + BASE64.encode(key) +} + +fn encrypt(key: &str, message: &str) -> String { + match BASE64.decode(key) { + Ok(key_bytes) => { + let message_bytes = message.as_bytes(); + match symmetric::encrypt_symmetric(&key_bytes, message_bytes) { + Ok(ciphertext) => BASE64.encode(ciphertext), + Err(e) => { + log::error!("Error encrypting message: {}", e); + String::new() + } + } + }, + Err(e) => { + log::error!("Error decoding key: {}", e); + String::new() + } + } +} + +fn decrypt(key: &str, ciphertext: &str) -> String { + match BASE64.decode(key) { + Ok(key_bytes) => { + match BASE64.decode(ciphertext) { + Ok(ciphertext_bytes) => { + match symmetric::decrypt_symmetric(&key_bytes, &ciphertext_bytes) { + Ok(plaintext) => { + match String::from_utf8(plaintext) { + Ok(text) => text, + Err(e) => { + log::error!("Error converting plaintext to string: {}", e); + String::new() + } + } + }, + Err(e) => { + log::error!("Error decrypting ciphertext: {}", e); + String::new() + } + } + }, + Err(e) => { + log::error!("Error decoding ciphertext: {}", e); + String::new() + } + } + }, + Err(e) => { + log::error!("Error decoding key: {}", e); + String::new() + } + } +} + +// Ethereum operations + +// Gnosis Chain operations +fn create_ethereum_wallet() -> bool { + match ethereum::create_ethereum_wallet_for_network(ethereum::networks::gnosis()) { + Ok(_) => true, + Err(e) => { + log::error!("Error creating Ethereum wallet: {}", e); + false + } + } +} + +fn get_ethereum_address() -> String { + match ethereum::get_current_ethereum_wallet_for_network("Gnosis") { + Ok(wallet) => wallet.address_string(), + Err(e) => { + log::error!("Error getting Ethereum address: {}", e); + String::new() + } + } +} + +// Peaq network operations +fn create_peaq_wallet() -> bool { + match ethereum::create_peaq_wallet() { + Ok(_) => true, + Err(e) => { + log::error!("Error creating Peaq wallet: {}", 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() { + Ok(_) => true, + Err(e) => { + log::error!("Error creating Agung wallet: {}", e); + false + } + } +} + +fn get_agung_address() -> String { + match ethereum::get_current_agung_wallet() { + Ok(wallet) => wallet.address_string(), + Err(e) => { + log::error!("Error getting Agung address: {}", e); + String::new() + } + } +} + +// Generic network operations +fn create_wallet_for_network(network_name: &str) -> bool { + let network = match ethereum::networks::get_network_by_name(network_name) { + Some(network) => network, + None => { + log::error!("Unknown network: {}", network_name); + return false; + } + }; + + match ethereum::create_ethereum_wallet_for_network(network) { + Ok(_) => true, + Err(e) => { + log::error!("Error creating wallet for network {}: {}", network_name, e); + false + } + } +} + +// Get wallet address for a specific network +fn get_wallet_address_for_network(network_name: &str) -> String { + let network_name_proper = match ethereum::networks::get_proper_network_name(network_name) { + Some(name) => name, + None => { + log::error!("Unknown network: {}", network_name); + return String::new(); + } + }; + + match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) { + Ok(wallet) => wallet.address_string(), + Err(e) => { + log::error!("Error getting wallet address for network {}: {}", network_name, e); + String::new() + } + } +} + +// Clear wallets for a specific network +fn clear_wallets_for_network(network_name: &str) -> bool { + let network_name_proper = match ethereum::networks::get_proper_network_name(network_name) { + Some(name) => name, + None => { + log::error!("Unknown network: {}", network_name); + return false; + } + }; + + ethereum::clear_ethereum_wallets_for_network(network_name_proper); + true +} + +// List supported networks +fn list_supported_networks() -> rhai::Array { + let mut arr = rhai::Array::new(); + for name in ethereum::networks::list_network_names() { + arr.push(Dynamic::from(name.to_lowercase())); + } + arr +} + +// Get network token symbol +fn get_network_token_symbol(network_name: &str) -> String { + match ethereum::networks::get_network_by_name(network_name) { + Some(network) => network.token_symbol, + None => { + log::error!("Unknown network: {}", network_name); + String::new() + } + } +} + +// Get network explorer URL +fn get_network_explorer_url(network_name: &str) -> String { + match ethereum::networks::get_network_by_name(network_name) { + Some(network) => network.explorer_url, + None => { + log::error!("Unknown network: {}", network_name); + String::new() + } + } +} + +// Create a wallet from a private key for a specific network +fn create_wallet_from_private_key_for_network(private_key: &str, network_name: &str) -> bool { + let network = match ethereum::networks::get_network_by_name(network_name) { + Some(network) => network, + None => { + log::error!("Unknown network: {}", network_name); + return false; + } + }; + + match ethereum::create_ethereum_wallet_from_private_key_for_network(private_key, network) { + Ok(_) => true, + Err(e) => { + log::error!("Error creating wallet from private key for network {}: {}", network_name, e); + false + } + } +} + +// Create a provider for the Agung network +fn create_agung_provider() -> String { + match ethereum::create_agung_provider() { + Ok(provider) => { + // Generate a unique ID for the provider + let id = format!("provider_{}", uuid::Uuid::new_v4()); + + // Store the provider in the registry + if let Ok(mut providers) = PROVIDERS.lock() { + providers.insert(id.clone(), provider); + return id; + } + + log::error!("Failed to acquire provider registry lock"); + String::new() + }, + Err(e) => { + log::error!("Error creating Agung provider: {}", e); + String::new() + } + } +} + +// Get the balance of an address on a specific network +fn get_balance(network_name: &str, address: &str) -> String { + // Get the runtime + let rt = match RUNTIME.lock() { + Ok(rt) => rt, + Err(e) => { + log::error!("Failed to acquire runtime lock: {}", e); + return String::new(); + } + }; + + // Parse the address + let addr = match Address::from_str(address) { + Ok(addr) => addr, + Err(e) => { + log::error!("Invalid address format: {}", e); + return String::new(); + } + }; + + // 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 + }) { + Ok(balance) => balance.to_string(), + Err(e) => { + log::error!("Failed to get balance: {}", e); + String::new() + } + } +} + +// Send ETH from one address to another using the blocking approach +fn send_eth(wallet_network: &str, to_address: &str, amount_str: &str) -> String { + // Get the runtime + let rt = match RUNTIME.lock() { + Ok(rt) => rt, + Err(e) => { + log::error!("Failed to acquire runtime lock: {}", e); + return String::new(); + } + }; + + // Parse the address + let to_addr = match Address::from_str(to_address) { + Ok(addr) => addr, + Err(e) => { + log::error!("Invalid address format: {}", e); + return String::new(); + } + }; + + // Parse the amount (using string to handle large numbers) + let amount = match U256::from_dec_str(amount_str) { + Ok(amt) => amt, + Err(e) => { + log::error!("Invalid amount format: {}", e); + return String::new(); + } + }; + + // 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) { + Ok(w) => w, + Err(e) => { + log::error!("Failed to get wallet: {}", e); + return String::new(); + } + }; + + // 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 + }) { + Ok(tx_hash) => format!("{:?}", tx_hash), + Err(e) => { + log::error!("Transaction failed: {}", e); + String::new() + } + } +} + +// Smart contract operations + +// 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) { + Some(network) => network, + None => { + log::error!("Unknown network: {}", network_name); + return String::new(); + } + }; + + // Parse the ABI + let abi = match ethereum::load_abi_from_json(abi_json) { + Ok(abi) => abi, + Err(e) => { + log::error!("Error parsing ABI: {}", e); + return String::new(); + } + }; + + // Create the contract + match ethereum::Contract::from_address_string(address, abi, network) { + Ok(contract) => { + // Serialize the contract to JSON for storage + match serde_json::to_string(&contract) { + Ok(json) => json, + Err(e) => { + log::error!("Error serializing contract: {}", e); + String::new() + } + } + }, + Err(e) => { + log::error!("Error creating contract: {}", e); + String::new() + } + } +} + +// Load a contract ABI from a file +fn load_contract_abi_from_file(network_name: &str, address: &str, file_path: &str) -> String { + // Read the ABI file + match fs::read_to_string(file_path) { + Ok(abi_json) => load_contract_abi(network_name, address, &abi_json), + Err(e) => { + log::error!("Error reading ABI file: {}", e); + String::new() + } + } +} + +// 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()) +} + +// Call a read-only function on a contract with arguments +fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Array) -> Dynamic { + // Deserialize the contract + let contract: ethereum::Contract = match serde_json::from_str(contract_json) { + Ok(contract) => contract, + Err(e) => { + log::error!("Error deserializing contract: {}", e); + return Dynamic::UNIT; + } + }; + + // Prepare the arguments + let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) { + Ok(tokens) => tokens, + Err(e) => { + log::error!("Error preparing arguments: {}", e); + return Dynamic::UNIT; + } + }; + + // Get the runtime + let rt = match RUNTIME.lock() { + Ok(rt) => rt, + Err(e) => { + log::error!("Failed to acquire runtime lock: {}", e); + return Dynamic::UNIT; + } + }; + + // Create a provider + let provider = match ethereum::create_provider(&contract.network) { + Ok(p) => p, + Err(e) => { + log::error!("Failed to create provider: {}", e); + return Dynamic::UNIT; + } + }; + + // Execute the call in a blocking manner + match rt.block_on(async { + ethereum::call_read_function(&contract, &provider, function_name, tokens).await + }) { + Ok(result) => convert_token_to_rhai(&result), + Err(e) => { + log::error!("Failed to call contract function: {}", e); + Dynamic::UNIT + } + } +} + +// Call a state-changing function on a contract (no arguments version) +fn call_contract_write_no_args(contract_json: &str, function_name: &str) -> String { + call_contract_write(contract_json, function_name, rhai::Array::new()) +} + +// Call a state-changing function on a contract with arguments +fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Array) -> String { + // Deserialize the contract + let contract: ethereum::Contract = match serde_json::from_str(contract_json) { + Ok(contract) => contract, + Err(e) => { + log::error!("Error deserializing contract: {}", e); + return String::new(); + } + }; + + // Prepare the arguments + let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) { + Ok(tokens) => tokens, + Err(e) => { + log::error!("Error preparing arguments: {}", e); + return String::new(); + } + }; + + // Get the runtime + let rt = match RUNTIME.lock() { + Ok(rt) => rt, + Err(e) => { + log::error!("Failed to acquire runtime lock: {}", e); + return String::new(); + } + }; + + // Get the wallet + let network_name_proper = contract.network.name.as_str(); + let wallet = match ethereum::get_current_ethereum_wallet_for_network(network_name_proper) { + Ok(w) => w, + Err(e) => { + log::error!("Failed to get wallet: {}", e); + return String::new(); + } + }; + + // Create a provider + let provider = match ethereum::create_provider(&contract.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::call_write_function(&contract, &wallet, &provider, function_name, tokens).await + }) { + Ok(tx_hash) => format!("{:?}", tx_hash), + Err(e) => { + // Log the error details for debugging + log::debug!("\nERROR DETAILS: Transaction failed: {}", e); + log::debug!("Contract address: {}", contract.address); + log::debug!("Function: {}", function_name); + log::debug!("Arguments: {:?}", args); + log::debug!("Wallet address: {}", wallet.address); + log::debug!("Network: {}", contract.network.name); + log::error!("Transaction failed: {}", e); + String::new() + } + } +} + +/// Register crypto functions with the Rhai engine +pub fn register_crypto_module(engine: &mut Engine) -> Result<(), Box> { + // Register key space functions + engine.register_fn("load_key_space", load_key_space); + engine.register_fn("create_key_space", create_key_space); + engine.register_fn("encrypt_key_space", encrypt_key_space); + engine.register_fn("decrypt_key_space", decrypt_key_space); + + // Register keypair functions + engine.register_fn("create_keypair", create_keypair); + engine.register_fn("select_keypair", select_keypair); + engine.register_fn("list_keypairs", list_keypairs); + + // Register signing/verification functions + engine.register_fn("sign", sign); + engine.register_fn("verify", verify); + + // Register symmetric encryption functions + engine.register_fn("generate_key", generate_key); + engine.register_fn("encrypt", encrypt); + engine.register_fn("decrypt", decrypt); + + // Register Ethereum functions (Gnosis Chain) + engine.register_fn("create_ethereum_wallet", create_ethereum_wallet); + engine.register_fn("get_ethereum_address", get_ethereum_address); + + // Register Peaq network functions + engine.register_fn("create_peaq_wallet", create_peaq_wallet); + engine.register_fn("get_peaq_address", get_peaq_address); + + // Register Agung testnet functions + engine.register_fn("create_agung_wallet", create_agung_wallet); + engine.register_fn("get_agung_address", get_agung_address); + + // Register generic network functions + engine.register_fn("create_wallet_for_network", create_wallet_for_network); + engine.register_fn("get_wallet_address_for_network", get_wallet_address_for_network); + engine.register_fn("clear_wallets_for_network", clear_wallets_for_network); + engine.register_fn("list_supported_networks", list_supported_networks); + engine.register_fn("get_network_token_symbol", get_network_token_symbol); + engine.register_fn("get_network_explorer_url", get_network_explorer_url); + + // Register new Ethereum functions for wallet creation from private key and transactions + engine.register_fn("create_wallet_from_private_key_for_network", create_wallet_from_private_key_for_network); + engine.register_fn("create_agung_provider", create_agung_provider); + engine.register_fn("send_eth", send_eth); + engine.register_fn("get_balance", get_balance); + + // Register smart contract functions + engine.register_fn("load_contract_abi", load_contract_abi); + engine.register_fn("load_contract_abi_from_file", load_contract_abi_from_file); + + // Register the read function with different arities + engine.register_fn("call_contract_read", call_contract_read_no_args); + engine.register_fn("call_contract_read", call_contract_read); + + // Register the write function with different arities + engine.register_fn("call_contract_write", call_contract_write_no_args); + engine.register_fn("call_contract_write", call_contract_write); + + Ok(()) +} diff --git a/src/rhai/mod.rs b/src/rhai/mod.rs index 317a57d..7a23a37 100644 --- a/src/rhai/mod.rs +++ b/src/rhai/mod.rs @@ -12,6 +12,7 @@ mod postgresclient; mod process; mod redisclient; mod rfs; +mod hero_vault; // This module now uses hero_vault internally mod text; #[cfg(test)] @@ -105,6 +106,9 @@ pub use crate::text::{ // Re-export TextReplacer functions pub use text::*; +// Re-export crypto module +pub use hero_vault::register_crypto_module; + // Rename copy functions to avoid conflicts pub use os::copy as os_copy; @@ -148,6 +152,10 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { // Register RFS module functions rfs::register(engine)?; + + // Register Crypto module functions + hero_vault::register_crypto_module(engine)?; + // Register Redis client module functions redisclient::register_redisclient_module(engine)?;