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

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

View File

@@ -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,
}
}

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

View File

@@ -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<Vec<u8
symmetric::decrypt_with_password(password, ciphertext)
.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.
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();
}
}