This commit is contained in:
despiegk 2025-04-21 11:54:18 +02:00
parent d8a314df41
commit 67cbb35156
12 changed files with 1271 additions and 13 deletions

4
.gitignore vendored
View File

@ -34,4 +34,6 @@ yarn-error.log
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
node_modules node_modules
tmp/

View File

@ -22,6 +22,8 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
base64 = "0.21" base64 = "0.21"
sha2 = "0.10" sha2 = "0.10"
ethers = { version = "2.0", features = ["abigen", "legacy"] }
hex = "0.4"
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3" version = "0.3"

173
src/api/ethereum.rs Normal file
View File

@ -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<String, CryptoError> {
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<String, CryptoError> {
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<String, CryptoError> {
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<String, CryptoError> {
// Create a provider
let provider = ethereum::create_gnosis_provider()?;
// Parse the address
let address = address_str.parse::<Address>()
.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<String, CryptoError> {
// 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::<Address>()
.map_err(|_| CryptoError::InvalidEthereumAddress)?;
// Parse the amount
let amount_eth_float = amount_eth.parse::<f64>()
.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();
}

View File

@ -2,6 +2,7 @@
pub mod keypair; pub mod keypair;
pub mod symmetric; pub mod symmetric;
pub mod ethereum;
// Re-export commonly used items for external users // Re-export commonly used items for external users
// (Keeping this even though it's currently unused, as it's good practice for public APIs) // (Keeping this even though it's currently unused, as it's good practice for public APIs)

View File

@ -34,6 +34,12 @@ pub enum CryptoError {
InvalidPassword, InvalidPassword,
/// Error during serialization or deserialization. /// Error during serialization or deserialization.
SerializationError, SerializationError,
/// No Ethereum wallet is available.
NoEthereumWallet,
/// Ethereum transaction failed.
EthereumTransactionFailed,
/// Invalid Ethereum address.
InvalidEthereumAddress,
/// Other error with description. /// Other error with description.
#[allow(dead_code)] #[allow(dead_code)]
Other(String), Other(String),
@ -57,6 +63,9 @@ impl std::fmt::Display for CryptoError {
CryptoError::SpaceAlreadyExists => write!(f, "Space already exists"), CryptoError::SpaceAlreadyExists => write!(f, "Space already exists"),
CryptoError::InvalidPassword => write!(f, "Invalid password"), CryptoError::InvalidPassword => write!(f, "Invalid password"),
CryptoError::SerializationError => write!(f, "Serialization error"), 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), 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::SpaceAlreadyExists => -13,
CryptoError::InvalidPassword => -14, CryptoError::InvalidPassword => -14,
CryptoError::SerializationError => -15, CryptoError::SerializationError => -15,
CryptoError::NoEthereumWallet => -16,
CryptoError::EthereumTransactionFailed => -17,
CryptoError::InvalidEthereumAddress => -18,
CryptoError::Other(_) => -99, CryptoError::Other(_) => -99,
} }
} }

228
src/core/ethereum.rs Normal file
View File

@ -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<SigningKey>,
}
impl EthereumWallet {
/// Creates a new Ethereum wallet from a keypair.
pub fn from_keypair(keypair: &KeyPair) -> Result<Self, CryptoError> {
// Get the private key bytes from the keypair
let private_key_bytes = keypair.signing_key.to_bytes();
// Convert to a hex string (without 0x prefix)
let private_key_hex = hex::encode(private_key_bytes);
// Create an Ethereum wallet from the private key
let 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<Self, CryptoError> {
// Get the private key bytes from the keypair
let private_key_bytes = keypair.signing_key.to_bytes();
// Create a deterministic seed by combining name and private key
let mut hasher = Sha256::default();
hasher.update(name.as_bytes());
hasher.update(&private_key_bytes);
let seed = hasher.finalize();
// Use the seed as a private key
let private_key_hex = hex::encode(seed);
// Create an Ethereum wallet from the derived private key
let 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<Self, CryptoError> {
// Remove 0x prefix if present
let private_key_clean = private_key.trim_start_matches("0x");
// Create an Ethereum wallet from the private key
let 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<String, CryptoError> {
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<Mutex<Vec<EthereumWallet>>> = Lazy::new(|| {
Mutex::new(Vec::new())
});
/// Creates an Ethereum wallet from the currently selected keypair.
pub fn create_ethereum_wallet() -> Result<EthereumWallet, CryptoError> {
// 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<EthereumWallet, CryptoError> {
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<Http>, address: Address) -> Result<U256, CryptoError> {
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<Http>,
to: Address,
amount: U256,
) -> Result<H256, CryptoError> {
// 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<Provider<Http>, CryptoError> {
Provider::<Http>::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<EthereumWallet, CryptoError> {
// 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<EthereumWallet, CryptoError> {
// 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)
}

View File

@ -3,6 +3,7 @@
pub mod error; pub mod error;
pub mod keypair; pub mod keypair;
pub mod symmetric; pub mod symmetric;
pub mod ethereum;
// Re-export commonly used items for internal use // Re-export commonly used items for internal use
// (Keeping this even though it's currently unused, as it's good practice for internal modules) // (Keeping this even though it's currently unused, as it's good practice for internal modules)

View File

@ -11,6 +11,7 @@ mod tests;
// Re-export for internal use // Re-export for internal use
use api::keypair; use api::keypair;
use api::symmetric; use api::symmetric;
use api::ethereum;
use core::error::error_to_status_code; use core::error::error_to_status_code;
// This is like the `main` function, except for JavaScript. // This is like the `main` function, except for JavaScript.
@ -157,3 +158,51 @@ pub fn decrypt_with_password(password: &str, ciphertext: &[u8]) -> Result<Vec<u8
symmetric::decrypt_with_password(password, ciphertext) symmetric::decrypt_with_password(password, ciphertext)
.map_err(|e| JsValue::from_str(&e.to_string())) .map_err(|e| JsValue::from_str(&e.to_string()))
} }
// --- WebAssembly Exports for Ethereum ---
#[wasm_bindgen]
pub fn create_ethereum_wallet() -> 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<String, JsValue> {
ethereum::get_ethereum_address()
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen]
pub fn get_ethereum_private_key() -> Result<String, JsValue> {
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();
}

View File

@ -6,15 +6,16 @@ mod tests {
// Helper to ensure keypair is initialized for tests that need it. // Helper to ensure keypair is initialized for tests that need it.
fn ensure_keypair_initialized() { fn ensure_keypair_initialized() {
// Use try_init which doesn't panic if already initialized // Create a space and keypair for testing
let _ = keypair::keypair_new(); let _ = keypair::create_space("test_space");
assert!(keypair::KEYPAIR.get().is_some(), "KEYPAIR should be initialized"); let _ = keypair::create_keypair("test_keypair");
let _ = keypair::select_keypair("test_keypair");
} }
#[test] #[test]
fn test_keypair_generation_and_retrieval() { fn test_keypair_generation_and_retrieval() {
let _ = keypair::keypair_new(); // Ignore error if already initialized by another test ensure_keypair_initialized();
let pub_key = keypair::keypair_pub_key().expect("Should be able to get pub key after init"); 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"); assert!(!pub_key.is_empty(), "Public key should not be empty");
// Basic check for SEC1 format (0x02, 0x03, or 0x04 prefix) // Basic check for SEC1 format (0x02, 0x03, or 0x04 prefix)
assert!(pub_key.len() == 33 || pub_key.len() == 65, "Public key length is incorrect"); 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() { fn test_sign_verify_valid() {
ensure_keypair_initialized(); ensure_keypair_initialized();
let message = b"this is a test message"; 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"); 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"); assert!(is_valid, "Signature should be valid");
} }
@ -36,11 +37,11 @@ mod tests {
fn test_verify_invalid_signature() { fn test_verify_invalid_signature() {
ensure_keypair_initialized(); ensure_keypair_initialized();
let message = b"another test message"; 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 // 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"); assert!(!is_valid, "Tampered signature should be invalid");
} }
@ -49,9 +50,14 @@ mod tests {
ensure_keypair_initialized(); ensure_keypair_initialized();
let message = b"original message"; let message = b"original message";
let wrong_message = b"different 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"); assert!(!is_valid, "Signature should be invalid for a different message");
} }
// Clean up after tests
fn cleanup() {
keypair::logout();
}
} }

240
www/ethereum.html Normal file
View File

@ -0,0 +1,240 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Ethereum WebAssembly Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.container {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 4px;
}
button.secondary {
background-color: #6c757d;
}
button.danger {
background-color: #dc3545;
}
input, textarea, select {
padding: 8px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
width: 80%;
}
.result {
margin-top: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
word-break: break-all;
}
.key-display {
font-family: monospace;
font-size: 12px;
word-break: break-all;
}
.note {
font-style: italic;
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.status {
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
}
.status.logged-in {
background-color: #d4edda;
color: #155724;
}
.status.logged-out {
background-color: #f8d7da;
color: #721c24;
}
.hidden {
display: none;
}
.address-container {
margin-top: 15px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #ddd;
}
.address-label {
font-weight: bold;
margin-bottom: 5px;
}
.address-value {
font-family: monospace;
word-break: break-all;
background-color: #e9ecef;
padding: 8px;
border-radius: 4px;
margin-bottom: 10px;
border: 1px solid #ced4da;
}
.nav-links {
margin-bottom: 20px;
}
.nav-links a {
margin-right: 15px;
text-decoration: none;
color: #007bff;
}
.nav-links a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<h1>Ethereum WebAssembly Demo</h1>
<div class="nav-links">
<a href="index.html">Main Crypto Demo</a>
<a href="ethereum.html">Ethereum Demo</a>
</div>
<!-- Login/Space Management Section -->
<div class="container" id="login-container">
<h2>Key Space Management</h2>
<div id="login-status" class="status logged-out">
Status: Not logged in
</div>
<div id="login-form">
<div class="form-group">
<label for="space-name">Space Name:</label>
<input type="text" id="space-name" placeholder="Enter space name" />
</div>
<div class="form-group">
<label for="space-password">Password:</label>
<input type="password" id="space-password" placeholder="Enter password" />
</div>
<div>
<button id="login-button">Login</button>
<button id="create-space-button">Create New Space</button>
</div>
</div>
<div id="logout-form" class="hidden">
<div class="form-group">
<label>Current Space: <span id="current-space-name"></span></label>
</div>
<div class="form-group">
<button id="logout-button" class="danger">Logout</button>
</div>
</div>
<div class="result" id="space-result">Result will appear here</div>
</div>
<!-- Keypair Management Section -->
<div class="container" id="keypair-management-container">
<h2>Keypair Management</h2>
<div id="keypair-form">
<div class="form-group">
<label for="keypair-name">Keypair Name:</label>
<input type="text" id="keypair-name" placeholder="Enter keypair name" />
<button id="create-keypair-button">Create Keypair</button>
</div>
<div class="form-group">
<label for="select-keypair">Select Keypair:</label>
<select id="select-keypair">
<option value="">-- Select a keypair --</option>
</select>
</div>
</div>
<div class="result" id="keypair-management-result">Result will appear here</div>
</div>
<!-- Ethereum Wallet Section -->
<div class="container" id="ethereum-wallet-container">
<h2>Ethereum Wallet</h2>
<div class="note">Note: All operations use the Gnosis Chain (xDAI)</div>
<div class="form-group">
<button id="create-ethereum-wallet-button">Create Ethereum Wallet from Selected Keypair</button>
</div>
<div class="form-group">
<label for="wallet-name">Create from Name and Keypair:</label>
<input type="text" id="wallet-name" placeholder="Enter name for deterministic derivation" />
<button id="create-from-name-button">Create from Name</button>
</div>
<div class="form-group">
<label for="private-key">Import Private Key:</label>
<input type="text" id="private-key" placeholder="Enter private key (with or without 0x prefix)" />
<button id="import-private-key-button">Import Private Key</button>
</div>
<div id="ethereum-wallet-info" class="hidden">
<div class="address-container">
<div class="address-label">Ethereum Address:</div>
<div class="address-value" id="ethereum-address-value"></div>
<button id="copy-address-button" class="secondary">Copy Address</button>
</div>
<div class="address-container">
<div class="address-label">Private Key (hex):</div>
<div class="address-value" id="ethereum-private-key-value"></div>
<button id="copy-private-key-button" class="secondary">Copy Private Key</button>
<div class="note">Warning: Never share your private key with anyone!</div>
</div>
</div>
<div class="result" id="ethereum-wallet-result">Result will appear here</div>
</div>
<!-- Ethereum Balance Section -->
<div class="container" id="ethereum-balance-container">
<h2>Check Ethereum Balance</h2>
<div class="form-group">
<button id="check-balance-button">Check Current Wallet Balance</button>
</div>
<div class="result" id="balance-result">Balance will appear here</div>
</div>
<script type="module" src="./js/ethereum.js"></script>
</body>
</html>

View File

@ -84,6 +84,17 @@
.hidden { .hidden {
display: none; 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 { .pubkey-container {
margin-top: 15px; margin-top: 15px;
padding: 10px; padding: 10px;
@ -109,6 +120,11 @@
<body> <body>
<h1>Rust WebAssembly Crypto Example</h1> <h1>Rust WebAssembly Crypto Example</h1>
<div class="nav-links">
<a href="index.html">Main Crypto Demo</a>
<a href="ethereum.html">Ethereum Demo</a>
</div>
<!-- Login/Space Management Section --> <!-- Login/Space Management Section -->
<div class="container" id="login-container"> <div class="container" id="login-container">
<h2>Key Space Management</h2> <h2>Key Space Management</h2>

528
www/js/ethereum.js Normal file
View File

@ -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);