diff --git a/.gitignore b/.gitignore index 6a661f8..5394849 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ yarn-error.log .env.development.local .env.test.local .env.production.local -node_modules \ No newline at end of file +node_modules + +tmp/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a63861e..7552dcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" base64 = "0.21" sha2 = "0.10" +ethers = { version = "2.0", features = ["abigen", "legacy"] } +hex = "0.4" [dependencies.web-sys] version = "0.3" diff --git a/src/api/ethereum.rs b/src/api/ethereum.rs new file mode 100644 index 0000000..ce0d4af --- /dev/null +++ b/src/api/ethereum.rs @@ -0,0 +1,173 @@ +//! Public API for Ethereum operations. + +use crate::core::ethereum; +use crate::core::error::CryptoError; +use ethers::prelude::*; +use wasm_bindgen::prelude::*; + +/// Creates an Ethereum wallet from the currently selected keypair. +/// +/// # Returns +/// +/// * `Ok(())` if the wallet was created successfully. +/// * `Err(CryptoError::NoActiveSpace)` if no space is active. +/// * `Err(CryptoError::NoKeypairSelected)` if no keypair is selected. +/// * `Err(CryptoError::KeypairNotFound)` if the selected keypair was not found. +/// * `Err(CryptoError::InvalidKeyLength)` if the keypair's private key is invalid for Ethereum. +pub fn create_ethereum_wallet() -> Result<(), CryptoError> { + ethereum::create_ethereum_wallet()?; + Ok(()) +} + +/// Creates an Ethereum wallet from a name and the currently selected keypair. +/// +/// # Arguments +/// +/// * `name` - The name to use for deterministic derivation. +/// +/// # Returns +/// +/// * `Ok(())` if the wallet was created successfully. +/// * `Err(CryptoError)` if an error occurred. +pub fn create_ethereum_wallet_from_name(name: &str) -> Result<(), CryptoError> { + ethereum::create_ethereum_wallet_from_name(name)?; + Ok(()) +} + +/// Creates an Ethereum wallet from a private key. +/// +/// # Arguments +/// +/// * `private_key` - The private key as a hex string (with or without 0x prefix). +/// +/// # Returns +/// +/// * `Ok(())` if the wallet was created successfully. +/// * `Err(CryptoError)` if an error occurred. +pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<(), CryptoError> { + ethereum::create_ethereum_wallet_from_private_key(private_key)?; + Ok(()) +} + +/// Gets the Ethereum address of the current wallet. +/// +/// # Returns +/// +/// * `Ok(String)` containing the Ethereum address. +/// * `Err(CryptoError::NoEthereumWallet)` if no Ethereum wallet is available. +pub fn get_ethereum_address() -> Result { + let wallet = ethereum::get_current_ethereum_wallet()?; + Ok(wallet.address_string()) +} + +/// Gets the Ethereum private key as a hex string. +/// +/// # Returns +/// +/// * `Ok(String)` containing the Ethereum private key as a hex string. +/// * `Err(CryptoError::NoEthereumWallet)` if no Ethereum wallet is available. +pub fn get_ethereum_private_key() -> Result { + let wallet = ethereum::get_current_ethereum_wallet()?; + Ok(wallet.private_key_hex()) +} + +/// Signs a message with the Ethereum wallet. +/// +/// # Arguments +/// +/// * `message` - The message to sign. +/// +/// # Returns +/// +/// * `Ok(String)` containing the signature. +/// * `Err(CryptoError::NoEthereumWallet)` if no Ethereum wallet is available. +/// * `Err(CryptoError::SignatureFormatError)` if signing fails. +pub async fn sign_ethereum_message(message: &[u8]) -> Result { + let wallet = ethereum::get_current_ethereum_wallet()?; + wallet.sign_message(message).await +} + +/// Formats an Ethereum balance for display. +/// +/// # Arguments +/// +/// * `balance_hex` - The balance as a hex string. +/// +/// # Returns +/// +/// * `String` containing the formatted balance. +pub fn format_eth_balance(balance_hex: &str) -> String { + let balance = U256::from_str_radix(balance_hex.trim_start_matches("0x"), 16) + .unwrap_or_default(); + ethereum::format_eth_balance(balance) +} + +/// Gets the balance of an Ethereum address. +/// +/// # Arguments +/// +/// * `address_str` - The Ethereum address as a string. +/// +/// # Returns +/// +/// * `Ok(String)` containing the balance as a hex string. +/// * `Err(CryptoError)` if getting the balance fails. +pub async fn get_ethereum_balance(address_str: &str) -> Result { + // Create a provider + let provider = ethereum::create_gnosis_provider()?; + + // Parse the address + let address = address_str.parse::
() + .map_err(|_| CryptoError::InvalidEthereumAddress)?; + + // Get the balance + let balance = ethereum::get_balance(&provider, address).await?; + + // Return the balance as a hex string + Ok(format!("0x{:x}", balance)) +} + +/// Sends Ethereum from the current wallet to another address. +/// +/// # Arguments +/// +/// * `to_address` - The recipient's Ethereum address as a string. +/// * `amount_eth` - The amount to send in ETH (as a string). +/// +/// # Returns +/// +/// * `Ok(String)` containing the transaction hash. +/// * `Err(CryptoError)` if sending fails. +pub async fn send_ethereum( + to_address: &str, + amount_eth: &str, +) -> Result { + // Create a provider + let provider = ethereum::create_gnosis_provider()?; + + // Get the current wallet + let wallet = ethereum::get_current_ethereum_wallet()?; + + // Parse the recipient address + let to = to_address.parse::
() + .map_err(|_| CryptoError::InvalidEthereumAddress)?; + + // Parse the amount + let amount_eth_float = amount_eth.parse::() + .map_err(|_| CryptoError::Other("Invalid amount".to_string()))?; + + // Convert ETH to Wei + let amount_wei = (amount_eth_float * 1_000_000_000_000_000_000.0) as u128; + let amount = U256::from(amount_wei); + + // Send the transaction + let tx_hash = ethereum::send_eth(&wallet, &provider, to, amount).await?; + + // Return the transaction hash + Ok(format!("0x{:x}", tx_hash)) +} + +/// Clears all Ethereum wallets. +pub fn clear_ethereum_wallets() { + ethereum::clear_ethereum_wallets(); +} \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 731a3ea..9246991 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,6 +2,7 @@ pub mod keypair; pub mod symmetric; +pub mod ethereum; // Re-export commonly used items for external users // (Keeping this even though it's currently unused, as it's good practice for public APIs) diff --git a/src/core/error.rs b/src/core/error.rs index b070230..4838e8e 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -34,6 +34,12 @@ pub enum CryptoError { InvalidPassword, /// Error during serialization or deserialization. SerializationError, + /// No Ethereum wallet is available. + NoEthereumWallet, + /// Ethereum transaction failed. + EthereumTransactionFailed, + /// Invalid Ethereum address. + InvalidEthereumAddress, /// Other error with description. #[allow(dead_code)] Other(String), @@ -57,6 +63,9 @@ impl std::fmt::Display for CryptoError { CryptoError::SpaceAlreadyExists => write!(f, "Space already exists"), CryptoError::InvalidPassword => write!(f, "Invalid password"), CryptoError::SerializationError => write!(f, "Serialization error"), + CryptoError::NoEthereumWallet => write!(f, "No Ethereum wallet available"), + CryptoError::EthereumTransactionFailed => write!(f, "Ethereum transaction failed"), + CryptoError::InvalidEthereumAddress => write!(f, "Invalid Ethereum address"), CryptoError::Other(s) => write!(f, "Crypto error: {}", s), } } @@ -82,6 +91,9 @@ pub fn error_to_status_code(err: CryptoError) -> i32 { CryptoError::SpaceAlreadyExists => -13, CryptoError::InvalidPassword => -14, CryptoError::SerializationError => -15, + CryptoError::NoEthereumWallet => -16, + CryptoError::EthereumTransactionFailed => -17, + CryptoError::InvalidEthereumAddress => -18, CryptoError::Other(_) => -99, } } \ No newline at end of file diff --git a/src/core/ethereum.rs b/src/core/ethereum.rs new file mode 100644 index 0000000..5f5a6bb --- /dev/null +++ b/src/core/ethereum.rs @@ -0,0 +1,228 @@ +//! Core implementation of Ethereum functionality. + +use ethers::prelude::*; +use ethers::signers::{LocalWallet, Signer, Wallet}; +use ethers::utils::hex; +use k256::ecdsa::SigningKey; +use std::str::FromStr; +use std::sync::Mutex; +use once_cell::sync::Lazy; +use sha2::{Sha256, Digest}; + +use super::error::CryptoError; +use super::keypair::KeyPair; + +// Gnosis Chain configuration +pub const GNOSIS_CHAIN_ID: u64 = 100; +pub const GNOSIS_RPC_URL: &str = "https://rpc.gnosis.gateway.fm"; +pub const GNOSIS_EXPLORER: &str = "https://gnosisscan.io"; + +/// An Ethereum wallet derived from a keypair. +#[derive(Debug, Clone)] +pub struct EthereumWallet { + pub address: Address, + pub wallet: Wallet, +} + +impl EthereumWallet { + /// Creates a new Ethereum wallet from a keypair. + pub fn from_keypair(keypair: &KeyPair) -> Result { + // Get the private key bytes from the keypair + let private_key_bytes = keypair.signing_key.to_bytes(); + + // 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(|_| CryptoError::InvalidKeyLength)? + .with_chain_id(GNOSIS_CHAIN_ID); + + // Get the Ethereum address + let address = wallet.address(); + + Ok(EthereumWallet { + address, + wallet, + }) + } + + /// Creates a new Ethereum wallet from a name and keypair (deterministic derivation). + pub fn from_name_and_keypair(name: &str, keypair: &KeyPair) -> Result { + // Get the private key bytes from the keypair + let private_key_bytes = keypair.signing_key.to_bytes(); + + // 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(|_| CryptoError::InvalidKeyLength)? + .with_chain_id(GNOSIS_CHAIN_ID); + + // Get the Ethereum address + let address = wallet.address(); + + Ok(EthereumWallet { + address, + wallet, + }) + } + + /// Creates a new Ethereum wallet from a private key. + pub fn from_private_key(private_key: &str) -> Result { + // Remove 0x prefix if present + let private_key_clean = private_key.trim_start_matches("0x"); + + // Create an Ethereum wallet from the private key + let wallet = LocalWallet::from_str(private_key_clean) + .map_err(|_| CryptoError::InvalidKeyLength)? + .with_chain_id(GNOSIS_CHAIN_ID); + + // Get the Ethereum address + let address = wallet.address(); + + Ok(EthereumWallet { + address, + wallet, + }) + } + + /// 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(|_| CryptoError::SignatureFormatError)?; + + 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) + } +} + +/// Global storage for Ethereum wallets. +static ETH_WALLETS: Lazy>> = Lazy::new(|| { + Mutex::new(Vec::new()) +}); + +/// Creates an Ethereum wallet from the currently selected keypair. +pub fn create_ethereum_wallet() -> Result { + // Get the currently selected keypair + let keypair = super::keypair::get_selected_keypair()?; + + // Create an Ethereum wallet from the keypair + let wallet = EthereumWallet::from_keypair(&keypair)?; + + // Store the wallet + let mut wallets = ETH_WALLETS.lock().unwrap(); + wallets.push(wallet.clone()); + + Ok(wallet) +} + +/// Gets the current Ethereum wallet. +pub fn get_current_ethereum_wallet() -> Result { + let wallets = ETH_WALLETS.lock().unwrap(); + + if wallets.is_empty() { + return Err(CryptoError::NoKeypairSelected); + } + + Ok(wallets.last().unwrap().clone()) +} + +/// Clears all Ethereum wallets. +pub fn clear_ethereum_wallets() { + let mut wallets = ETH_WALLETS.lock().unwrap(); + wallets.clear(); +} + +/// Formats an Ethereum balance for display. +pub fn format_eth_balance(balance: U256) -> String { + let wei = balance.as_u128(); + let eth = wei as f64 / 1_000_000_000_000_000_000.0; + format!("{:.6} ETH", eth) +} + +/// 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(|_| CryptoError::Other("Failed to get balance".to_string())) +} + +/// 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(|_| CryptoError::Other("Failed to send transaction".to_string()))?; + + // Return the transaction hash instead of waiting for the receipt + Ok(pending_tx.tx_hash()) +} + +/// Creates a provider for the Gnosis Chain. +pub fn create_gnosis_provider() -> Result, CryptoError> { + Provider::::try_from(GNOSIS_RPC_URL) + .map_err(|_| CryptoError::Other("Failed to create Gnosis provider".to_string())) +} + +/// Creates an Ethereum wallet from a name and the currently selected keypair. +pub fn create_ethereum_wallet_from_name(name: &str) -> Result { + // Get the currently selected keypair + let keypair = super::keypair::get_selected_keypair()?; + + // Create an Ethereum wallet from the name and keypair + let wallet = EthereumWallet::from_name_and_keypair(name, &keypair)?; + + // Store the wallet + let mut wallets = ETH_WALLETS.lock().unwrap(); + wallets.push(wallet.clone()); + + Ok(wallet) +} + +/// Creates an Ethereum wallet from a private key. +pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result { + // Create an Ethereum wallet from the private key + let wallet = EthereumWallet::from_private_key(private_key)?; + + // Store the wallet + let mut wallets = ETH_WALLETS.lock().unwrap(); + wallets.push(wallet.clone()); + + Ok(wallet) +} \ No newline at end of file diff --git a/src/core/mod.rs b/src/core/mod.rs index f676771..fed07b4 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -3,6 +3,7 @@ pub mod error; pub mod keypair; pub mod symmetric; +pub mod ethereum; // Re-export commonly used items for internal use // (Keeping this even though it's currently unused, as it's good practice for internal modules) diff --git a/src/lib.rs b/src/lib.rs index 909da6d..526de67 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ mod tests; // Re-export for internal use use api::keypair; use api::symmetric; +use api::ethereum; use core::error::error_to_status_code; // This is like the `main` function, except for JavaScript. @@ -157,3 +158,51 @@ pub fn decrypt_with_password(password: &str, ciphertext: &[u8]) -> Result i32 { + match ethereum::create_ethereum_wallet() { + Ok(_) => 0, // Success + Err(e) => error_to_status_code(e), + } +} + +#[wasm_bindgen] +pub fn create_ethereum_wallet_from_name(name: &str) -> i32 { + match ethereum::create_ethereum_wallet_from_name(name) { + Ok(_) => 0, // Success + Err(e) => error_to_status_code(e), + } +} + +#[wasm_bindgen] +pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> i32 { + match ethereum::create_ethereum_wallet_from_private_key(private_key) { + Ok(_) => 0, // Success + Err(e) => error_to_status_code(e), + } +} + +#[wasm_bindgen] +pub fn get_ethereum_address() -> Result { + ethereum::get_ethereum_address() + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn get_ethereum_private_key() -> Result { + ethereum::get_ethereum_private_key() + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn format_eth_balance(balance_hex: &str) -> String { + ethereum::format_eth_balance(balance_hex) +} + +#[wasm_bindgen] +pub fn clear_ethereum_wallets() { + ethereum::clear_ethereum_wallets(); +} diff --git a/src/tests/keypair_tests.rs b/src/tests/keypair_tests.rs index 8e00122..4b42535 100644 --- a/src/tests/keypair_tests.rs +++ b/src/tests/keypair_tests.rs @@ -6,15 +6,16 @@ mod tests { // Helper to ensure keypair is initialized for tests that need it. fn ensure_keypair_initialized() { - // Use try_init which doesn't panic if already initialized - let _ = keypair::keypair_new(); - assert!(keypair::KEYPAIR.get().is_some(), "KEYPAIR should be initialized"); + // Create a space and keypair for testing + let _ = keypair::create_space("test_space"); + let _ = keypair::create_keypair("test_keypair"); + let _ = keypair::select_keypair("test_keypair"); } #[test] fn test_keypair_generation_and_retrieval() { - let _ = keypair::keypair_new(); // Ignore error if already initialized by another test - let pub_key = keypair::keypair_pub_key().expect("Should be able to get pub key after init"); + ensure_keypair_initialized(); + let pub_key = keypair::pub_key().expect("Should be able to get pub key after init"); assert!(!pub_key.is_empty(), "Public key should not be empty"); // Basic check for SEC1 format (0x02, 0x03, or 0x04 prefix) assert!(pub_key.len() == 33 || pub_key.len() == 65, "Public key length is incorrect"); @@ -25,10 +26,10 @@ mod tests { fn test_sign_verify_valid() { ensure_keypair_initialized(); let message = b"this is a test message"; - let signature = keypair::keypair_sign(message).expect("Signing failed"); + let signature = keypair::sign(message).expect("Signing failed"); assert!(!signature.is_empty(), "Signature should not be empty"); - let is_valid = keypair::keypair_verify(message, &signature).expect("Verification failed"); + let is_valid = keypair::verify(message, &signature).expect("Verification failed"); assert!(is_valid, "Signature should be valid"); } @@ -36,11 +37,11 @@ mod tests { fn test_verify_invalid_signature() { ensure_keypair_initialized(); let message = b"another test message"; - let mut invalid_signature = keypair::keypair_sign(message).expect("Signing failed"); + let mut invalid_signature = keypair::sign(message).expect("Signing failed"); // Tamper with the signature - invalid_signature[0] = invalid_signature[0].wrapping_add(1); + invalid_signature[0] = invalid_signature[0].wrapping_add(1); - let is_valid = keypair::keypair_verify(message, &invalid_signature).expect("Verification process failed"); + let is_valid = keypair::verify(message, &invalid_signature).expect("Verification process failed"); assert!(!is_valid, "Tampered signature should be invalid"); } @@ -49,9 +50,14 @@ mod tests { ensure_keypair_initialized(); let message = b"original message"; let wrong_message = b"different message"; - let signature = keypair::keypair_sign(message).expect("Signing failed"); + let signature = keypair::sign(message).expect("Signing failed"); - let is_valid = keypair::keypair_verify(wrong_message, &signature).expect("Verification process failed"); + let is_valid = keypair::verify(wrong_message, &signature).expect("Verification process failed"); assert!(!is_valid, "Signature should be invalid for a different message"); } + + // Clean up after tests + fn cleanup() { + keypair::logout(); + } } \ No newline at end of file diff --git a/www/ethereum.html b/www/ethereum.html new file mode 100644 index 0000000..ce33fdf --- /dev/null +++ b/www/ethereum.html @@ -0,0 +1,240 @@ + + + + + Ethereum WebAssembly Demo + + + +

Ethereum WebAssembly Demo

+ + + + +
+

Key Space Management

+ +
+ Status: Not logged in +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + +
Result will appear here
+
+ + +
+

Keypair Management

+ +
+
+ + + +
+ +
+ + +
+
+ +
Result will appear here
+
+ + +
+

Ethereum Wallet

+ +
Note: All operations use the Gnosis Chain (xDAI)
+ +
+ +
+ +
+ + + +
+ +
+ + + +
+ + + +
Result will appear here
+
+ + +
+

Check Ethereum Balance

+ +
+ +
+ +
Balance will appear here
+
+ + + + \ No newline at end of file diff --git a/www/index.html b/www/index.html index 49e1ae2..37cafde 100644 --- a/www/index.html +++ b/www/index.html @@ -84,6 +84,17 @@ .hidden { display: none; } + .nav-links { + margin-bottom: 20px; + } + .nav-links a { + margin-right: 15px; + text-decoration: none; + color: #007bff; + } + .nav-links a:hover { + text-decoration: underline; + } .pubkey-container { margin-top: 15px; padding: 10px; @@ -109,6 +120,11 @@

Rust WebAssembly Crypto Example

+ +

Key Space Management

diff --git a/www/js/ethereum.js b/www/js/ethereum.js new file mode 100644 index 0000000..ad6bad3 --- /dev/null +++ b/www/js/ethereum.js @@ -0,0 +1,528 @@ +// Import our WebAssembly module +import init, { + create_key_space, + encrypt_key_space, + decrypt_key_space, + logout, + create_keypair, + select_keypair, + list_keypairs, + keypair_pub_key, + create_ethereum_wallet, + create_ethereum_wallet_from_name, + create_ethereum_wallet_from_private_key, + get_ethereum_address, + get_ethereum_private_key, + format_eth_balance, + clear_ethereum_wallets +} from '../../pkg/webassembly.js'; + +// Helper function to convert ArrayBuffer to hex string +function bufferToHex(buffer) { + return Array.from(new Uint8Array(buffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +} + +// Helper function to convert hex string to Uint8Array +function hexToBuffer(hex) { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return bytes; +} + +// LocalStorage functions for key spaces +const STORAGE_PREFIX = 'crypto_space_'; +const ETH_WALLET_PREFIX = 'eth_wallet_'; + +// Save encrypted space to localStorage +function saveSpaceToStorage(spaceName, encryptedData) { + localStorage.setItem(`${STORAGE_PREFIX}${spaceName}`, encryptedData); +} + +// Get encrypted space from localStorage +function getSpaceFromStorage(spaceName) { + return localStorage.getItem(`${STORAGE_PREFIX}${spaceName}`); +} + +// Save Ethereum wallet to localStorage +function saveEthWalletToStorage(address, privateKey) { + localStorage.setItem(`${ETH_WALLET_PREFIX}${address}`, privateKey); +} + +// Get Ethereum wallet from localStorage +function getEthWalletFromStorage(address) { + return localStorage.getItem(`${ETH_WALLET_PREFIX}${address}`); +} + +// Session state +let isLoggedIn = false; +let currentSpace = null; +let selectedKeypair = null; +let hasEthereumWallet = false; + +// Update UI based on login state +function updateLoginUI() { + const loginForm = document.getElementById('login-form'); + const logoutForm = document.getElementById('logout-form'); + const loginStatus = document.getElementById('login-status'); + const currentSpaceName = document.getElementById('current-space-name'); + + if (isLoggedIn) { + loginForm.classList.add('hidden'); + logoutForm.classList.remove('hidden'); + loginStatus.textContent = 'Status: Logged in'; + loginStatus.className = 'status logged-in'; + currentSpaceName.textContent = currentSpace; + } else { + loginForm.classList.remove('hidden'); + logoutForm.classList.add('hidden'); + loginStatus.textContent = 'Status: Not logged in'; + loginStatus.className = 'status logged-out'; + currentSpaceName.textContent = ''; + + // Hide Ethereum wallet info when logged out + document.getElementById('ethereum-wallet-info').classList.add('hidden'); + hasEthereumWallet = false; + } +} + +// Update the keypairs dropdown list +function updateKeypairsList() { + const selectKeypair = document.getElementById('select-keypair'); + + // Clear existing options + while (selectKeypair.options.length > 1) { + selectKeypair.remove(1); + } + + try { + // Get keypairs list + const keypairs = list_keypairs(); + + // Add options for each keypair + keypairs.forEach(keypairName => { + const option = document.createElement('option'); + option.value = keypairName; + option.textContent = keypairName; + selectKeypair.appendChild(option); + }); + + // If there's a selected keypair, select it in the dropdown + if (selectedKeypair) { + selectKeypair.value = selectedKeypair; + } + } catch (e) { + console.error('Error updating keypairs list:', e); + } +} + +// Login to a space +async function performLogin() { + const spaceName = document.getElementById('space-name').value.trim(); + const password = document.getElementById('space-password').value; + + if (!spaceName || !password) { + document.getElementById('space-result').textContent = 'Please enter both space name and password'; + return; + } + + try { + // Get encrypted space from localStorage + const encryptedSpace = getSpaceFromStorage(spaceName); + if (!encryptedSpace) { + document.getElementById('space-result').textContent = `Space "${spaceName}" not found`; + return; + } + + // Decrypt the space + const result = decrypt_key_space(encryptedSpace, password); + if (result === 0) { + isLoggedIn = true; + currentSpace = spaceName; + updateLoginUI(); + updateKeypairsList(); + document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`; + } else { + document.getElementById('space-result').textContent = `Error logging in: ${result}`; + } + } catch (e) { + document.getElementById('space-result').textContent = `Error: ${e}`; + } +} + +// Create a new space +async function performCreateSpace() { + const spaceName = document.getElementById('space-name').value.trim(); + const password = document.getElementById('space-password').value; + + if (!spaceName || !password) { + document.getElementById('space-result').textContent = 'Please enter both space name and password'; + return; + } + + // Check if space already exists + if (getSpaceFromStorage(spaceName)) { + document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`; + return; + } + + try { + // Create new space + const result = create_key_space(spaceName); + if (result === 0) { + // Encrypt and save the space + const encryptedSpace = encrypt_key_space(password); + saveSpaceToStorage(spaceName, encryptedSpace); + + isLoggedIn = true; + currentSpace = spaceName; + updateLoginUI(); + updateKeypairsList(); + document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`; + } else { + document.getElementById('space-result').textContent = `Error creating space: ${result}`; + } + } catch (e) { + document.getElementById('space-result').textContent = `Error: ${e}`; + } +} + +// Logout from current space +function performLogout() { + logout(); + clear_ethereum_wallets(); + isLoggedIn = false; + currentSpace = null; + selectedKeypair = null; + hasEthereumWallet = false; + updateLoginUI(); + document.getElementById('space-result').textContent = 'Logged out successfully'; +} + +// Create a new keypair +async function performCreateKeypair() { + if (!isLoggedIn) { + document.getElementById('keypair-management-result').textContent = 'Please login first'; + return; + } + + const keypairName = document.getElementById('keypair-name').value.trim(); + + if (!keypairName) { + document.getElementById('keypair-management-result').textContent = 'Please enter a keypair name'; + return; + } + + try { + // Create new keypair + const result = create_keypair(keypairName); + if (result === 0) { + document.getElementById('keypair-management-result').textContent = `Successfully created keypair "${keypairName}"`; + + // Update keypairs list + updateKeypairsList(); + + // Select the new keypair + selectedKeypair = keypairName; + document.getElementById('select-keypair').value = keypairName; + + // Save the updated space to localStorage + saveCurrentSpace(); + } else { + document.getElementById('keypair-management-result').textContent = `Error creating keypair: ${result}`; + } + } catch (e) { + document.getElementById('keypair-management-result').textContent = `Error: ${e}`; + } +} + +// Select a keypair +async function performSelectKeypair() { + if (!isLoggedIn) { + document.getElementById('keypair-management-result').textContent = 'Please login first'; + return; + } + + const keypairName = document.getElementById('select-keypair').value; + + if (!keypairName) { + document.getElementById('keypair-management-result').textContent = 'Please select a keypair'; + return; + } + + try { + // Select keypair + const result = select_keypair(keypairName); + if (result === 0) { + selectedKeypair = keypairName; + document.getElementById('keypair-management-result').textContent = `Selected keypair "${keypairName}"`; + + // Hide Ethereum wallet info when changing keypairs + document.getElementById('ethereum-wallet-info').classList.add('hidden'); + hasEthereumWallet = false; + } else { + document.getElementById('keypair-management-result').textContent = `Error selecting keypair: ${result}`; + } + } catch (e) { + document.getElementById('keypair-management-result').textContent = `Error: ${e}`; + } +} + +// Save the current space to localStorage +function saveCurrentSpace() { + if (!isLoggedIn || !currentSpace) return; + + try { + const password = document.getElementById('space-password').value; + if (!password) { + console.error('Password not available for saving space'); + return; + } + + const encryptedSpace = encrypt_key_space(password); + saveSpaceToStorage(currentSpace, encryptedSpace); + } catch (e) { + console.error('Error saving space:', e); + } +} + +// Create an Ethereum wallet from the selected keypair +async function performCreateEthereumWallet() { + if (!isLoggedIn) { + document.getElementById('ethereum-wallet-result').textContent = 'Please login first'; + return; + } + + if (!selectedKeypair) { + document.getElementById('ethereum-wallet-result').textContent = 'Please select a keypair first'; + return; + } + + try { + // Create Ethereum wallet + const result = create_ethereum_wallet(); + if (result === 0) { + hasEthereumWallet = true; + + // Get and display Ethereum address + const address = get_ethereum_address(); + document.getElementById('ethereum-address-value').textContent = address; + + // Get and display private key + const privateKey = get_ethereum_private_key(); + document.getElementById('ethereum-private-key-value').textContent = privateKey; + + // Show the wallet info + document.getElementById('ethereum-wallet-info').classList.remove('hidden'); + + // Save the wallet to localStorage + saveEthWalletToStorage(address, privateKey); + + document.getElementById('ethereum-wallet-result').textContent = 'Successfully created Ethereum wallet'; + + // Save the updated space to localStorage + saveCurrentSpace(); + } else { + document.getElementById('ethereum-wallet-result').textContent = `Error creating Ethereum wallet: ${result}`; + } + } catch (e) { + document.getElementById('ethereum-wallet-result').textContent = `Error: ${e}`; + } +} + +// Create an Ethereum wallet from a name and the selected keypair +async function performCreateEthereumWalletFromName() { + if (!isLoggedIn) { + document.getElementById('ethereum-wallet-result').textContent = 'Please login first'; + return; + } + + if (!selectedKeypair) { + document.getElementById('ethereum-wallet-result').textContent = 'Please select a keypair first'; + return; + } + + const name = document.getElementById('wallet-name').value.trim(); + + if (!name) { + document.getElementById('ethereum-wallet-result').textContent = 'Please enter a name for derivation'; + return; + } + + try { + // Create Ethereum wallet from name + const result = create_ethereum_wallet_from_name(name); + if (result === 0) { + hasEthereumWallet = true; + + // Get and display Ethereum address + const address = get_ethereum_address(); + document.getElementById('ethereum-address-value').textContent = address; + + // Get and display private key + const privateKey = get_ethereum_private_key(); + document.getElementById('ethereum-private-key-value').textContent = privateKey; + + // Show the wallet info + document.getElementById('ethereum-wallet-info').classList.remove('hidden'); + + // Save the wallet to localStorage + saveEthWalletToStorage(address, privateKey); + + document.getElementById('ethereum-wallet-result').textContent = `Successfully created Ethereum wallet from name "${name}"`; + + // Save the updated space to localStorage + saveCurrentSpace(); + } else { + document.getElementById('ethereum-wallet-result').textContent = `Error creating Ethereum wallet: ${result}`; + } + } catch (e) { + document.getElementById('ethereum-wallet-result').textContent = `Error: ${e}`; + } +} + +// Create an Ethereum wallet from a private key +async function performCreateEthereumWalletFromPrivateKey() { + if (!isLoggedIn) { + document.getElementById('ethereum-wallet-result').textContent = 'Please login first'; + return; + } + + const privateKey = document.getElementById('private-key').value.trim(); + + if (!privateKey) { + document.getElementById('ethereum-wallet-result').textContent = 'Please enter a private key'; + return; + } + + try { + // Create Ethereum wallet from private key + const result = create_ethereum_wallet_from_private_key(privateKey); + if (result === 0) { + hasEthereumWallet = true; + + // Get and display Ethereum address + const address = get_ethereum_address(); + document.getElementById('ethereum-address-value').textContent = address; + + // Get and display private key + const displayPrivateKey = get_ethereum_private_key(); + document.getElementById('ethereum-private-key-value').textContent = displayPrivateKey; + + // Show the wallet info + document.getElementById('ethereum-wallet-info').classList.remove('hidden'); + + // Save the wallet to localStorage + saveEthWalletToStorage(address, displayPrivateKey); + + document.getElementById('ethereum-wallet-result').textContent = 'Successfully imported Ethereum wallet from private key'; + + // Save the updated space to localStorage + saveCurrentSpace(); + } else { + document.getElementById('ethereum-wallet-result').textContent = `Error importing Ethereum wallet: ${result}`; + } + } catch (e) { + document.getElementById('ethereum-wallet-result').textContent = `Error: ${e}`; + } +} + +// Check the balance of an Ethereum address +async function checkBalance() { + if (!hasEthereumWallet) { + document.getElementById('balance-result').textContent = 'Please create an Ethereum wallet first'; + return; + } + + try { + const address = get_ethereum_address(); + + document.getElementById('balance-result').textContent = 'Checking balance...'; + + // Use the Ethereum Web3 API directly from JavaScript + const response = await fetch(GNOSIS_RPC_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_getBalance', + params: [address, 'latest'], + id: 1, + }), + }); + + const data = await response.json(); + + if (data.error) { + document.getElementById('balance-result').textContent = `Error: ${data.error.message}`; + return; + } + + const balanceHex = data.result; + const formattedBalance = format_eth_balance(balanceHex); + + document.getElementById('balance-result').textContent = `Balance: ${formattedBalance}`; + } catch (e) { + document.getElementById('balance-result').textContent = `Error: ${e}`; + } +} + +// Copy text to clipboard +function copyToClipboard(text, successMessage) { + navigator.clipboard.writeText(text) + .then(() => { + alert(successMessage); + }) + .catch(err => { + console.error('Could not copy text: ', err); + }); +} + +// Constants +const GNOSIS_RPC_URL = "https://rpc.gnosis.gateway.fm"; +const GNOSIS_EXPLORER = "https://gnosisscan.io"; + +async function run() { + // Initialize the WebAssembly module + await init(); + + console.log('WebAssembly crypto module initialized!'); + + // Set up the login/space management + document.getElementById('login-button').addEventListener('click', performLogin); + document.getElementById('create-space-button').addEventListener('click', performCreateSpace); + document.getElementById('logout-button').addEventListener('click', performLogout); + + // Set up the keypair management + document.getElementById('create-keypair-button').addEventListener('click', performCreateKeypair); + document.getElementById('select-keypair').addEventListener('change', performSelectKeypair); + + // Set up the Ethereum wallet management + document.getElementById('create-ethereum-wallet-button').addEventListener('click', performCreateEthereumWallet); + document.getElementById('create-from-name-button').addEventListener('click', performCreateEthereumWalletFromName); + document.getElementById('import-private-key-button').addEventListener('click', performCreateEthereumWalletFromPrivateKey); + + // Set up the copy buttons + document.getElementById('copy-address-button').addEventListener('click', () => { + const address = document.getElementById('ethereum-address-value').textContent; + copyToClipboard(address, 'Ethereum address copied to clipboard!'); + }); + + document.getElementById('copy-private-key-button').addEventListener('click', () => { + const privateKey = document.getElementById('ethereum-private-key-value').textContent; + copyToClipboard(privateKey, 'Private key copied to clipboard!'); + }); + + // Set up the balance check + document.getElementById('check-balance-button').addEventListener('click', checkBalance); + + // Initialize UI + updateLoginUI(); +} + +run().catch(console.error); \ No newline at end of file