feat: implement browser extension UI with WebAssembly integration

This commit is contained in:
Sameh Abouel-saad
2025-05-22 11:53:32 +03:00
parent 13945a8725
commit ed76ba3d8d
74 changed files with 7054 additions and 577 deletions

9
evm_client/src/error.rs Normal file
View File

@@ -0,0 +1,9 @@
#[derive(Debug, thiserror::Error)]
pub enum EvmError {
#[error("RPC error: {0}")]
Rpc(String),
#[error("Signing error: {0}")]
Signing(String),
#[error("Other error: {0}")]
Other(String),
}

View File

@@ -14,22 +14,151 @@
pub use ethers_core::types::*;
pub mod provider;
pub mod signer;
pub mod rhai_bindings;
pub mod rhai_sync_helpers;
pub mod error;
pub use provider::send_rpc;
pub use error::EvmError;
/// Public EVM client struct for use in bindings and sync helpers
pub struct Provider {
pub rpc_url: String,
pub chain_id: u64,
pub explorer_url: Option<String>,
}
pub struct EvmClient {
// Add fields as needed for your implementation
pub provider: Provider,
}
impl EvmClient {
pub async fn get_balance(&self, provider_url: &str, public_key: &[u8]) -> Result<u64, String> {
// TODO: Implement actual logic
Ok(0)
pub fn new(provider: Provider) -> Self {
Self { provider }
}
pub async fn send_transaction(&self, provider_url: &str, key_id: &str, password: &[u8], tx_data: rhai::Map) -> Result<String, String> {
// TODO: Implement actual logic
Ok("tx_hash_placeholder".to_string())
/// Initialize logging for the current target (native: env_logger, WASM: console_log)
/// Call this before using any log macros.
pub fn init_logging() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
#[cfg(not(target_arch = "wasm32"))]
{
use env_logger;
let _ = env_logger::builder().is_test(false).try_init();
}
#[cfg(target_arch = "wasm32")]
{
use console_log;
let _ = console_log::init_with_level(log::Level::Debug);
}
});
}
pub async fn get_balance(&self, address: Address) -> Result<U256, EvmError> {
// TODO: Use provider info
provider::get_balance(&self.provider.rpc_url, address)
.await
.map_err(|e| EvmError::Rpc(e.to_string()))
}
pub async fn send_transaction(
&self,
mut tx: provider::Transaction,
signer: &dyn crate::signer::Signer,
) -> Result<ethers_core::types::H256, EvmError> {
use ethers_core::types::{U256, H256, Bytes, Address};
use std::str::FromStr;
use serde_json::json;
use crate::provider::{send_rpc, parse_signature_rs_v};
// 1. Fill in missing fields via JSON-RPC if needed
// Parse signer address as H160
let signer_addr = ethers_core::types::Address::from_str(&signer.address())
.map_err(|e| EvmError::Rpc(format!("Invalid signer address: {}", e)))?;
// Nonce
if tx.nonce.is_none() {
let body = json!({
"jsonrpc": "2.0",
"method": "eth_getTransactionCount",
"params": [format!("0x{:x}", signer_addr), "pending"],
"id": 1
}).to_string();
let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?;
let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?;
let hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_getTransactionCount".to_string()))?;
tx.nonce = Some(U256::from_str_radix(hex.trim_start_matches("0x"), 16).map_err(|e| EvmError::Rpc(e.to_string()))?);
}
// Gas Price
if tx.gas_price.is_none() {
let body = json!({
"jsonrpc": "2.0",
"method": "eth_gasPrice",
"params": [],
"id": 1
}).to_string();
let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?;
let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?;
let hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_gasPrice".to_string()))?;
tx.gas_price = Some(U256::from_str_radix(hex.trim_start_matches("0x"), 16).map_err(|e| EvmError::Rpc(e.to_string()))?);
}
// Chain ID
if tx.chain_id.is_none() {
tx.chain_id = Some(self.provider.chain_id);
}
// Gas (optional: estimate if missing)
if tx.gas.is_none() {
let body = json!({
"jsonrpc": "2.0",
"method": "eth_estimateGas",
"params": [{
"to": format!("0x{:x}", tx.to),
"from": format!("0x{:x}", signer_addr),
"value": format!("0x{:x}", tx.value),
"data": format!("0x{}", hex::encode(&tx.data)),
}],
"id": 1
}).to_string();
let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?;
let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?;
let hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_estimateGas".to_string()))?;
tx.gas = Some(U256::from_str_radix(hex.trim_start_matches("0x"), 16).map_err(|e| EvmError::Rpc(e.to_string()))?);
}
// 2. RLP encode unsigned transaction
let rlp_unsigned = tx.rlp_encode_unsigned();
// 3. Sign the RLP-encoded unsigned transaction
let sig = signer.sign(&rlp_unsigned).await?;
let (r, s, v) = parse_signature_rs_v(&sig, tx.chain_id.unwrap()).ok_or_else(|| EvmError::Signing("Invalid signature format".to_string()))?;
// 4. RLP encode signed transaction (EIP-155)
use rlp::RlpStream;
let mut rlp_stream = RlpStream::new_list(9);
rlp_stream.append(&tx.nonce.unwrap());
rlp_stream.append(&tx.gas_price.unwrap());
rlp_stream.append(&tx.gas.unwrap());
rlp_stream.append(&tx.to);
rlp_stream.append(&tx.value);
rlp_stream.append(&tx.data.to_vec());
rlp_stream.append(&tx.chain_id.unwrap());
rlp_stream.append(&r);
rlp_stream.append(&s);
let raw_tx = rlp_stream.out().to_vec();
// 5. Broadcast the raw transaction
let raw_hex = format!("0x{}", hex::encode(&raw_tx));
let body = json!({
"jsonrpc": "2.0",
"method": "eth_sendRawTransaction",
"params": [raw_hex],
"id": 1
}).to_string();
let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?;
let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?;
let tx_hash_hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_sendRawTransaction".to_string()))?;
let tx_hash = H256::from_slice(&hex::decode(tx_hash_hex.trim_start_matches("0x")).map_err(|e| EvmError::Rpc(e.to_string()))?);
Ok(tx_hash)
}
}

View File

@@ -30,13 +30,13 @@ pub async fn send_rpc(url: &str, body: &str) -> Result<String, Box<dyn Error>> {
}
}
pub struct Transaction {
pub nonce: U256,
pub to: Address,
pub value: U256,
pub gas: U256,
pub gas_price: U256,
pub data: Bytes,
pub chain_id: u64,
pub gas: Option<U256>,
pub gas_price: Option<U256>,
pub nonce: Option<U256>,
pub chain_id: Option<u64>,
}
impl Transaction {
@@ -94,5 +94,4 @@ pub async fn get_balance(url: &str, address: Address) -> Result<U256, Box<dyn st
Ok(balance)
}
// (Remove old sign_and_serialize placeholder)

View File

@@ -14,20 +14,16 @@ pub fn register_rhai_api(engine: &mut Engine, evm_client: std::sync::Arc<EvmClie
}
impl RhaiEvmClient {
/// Get balance using the EVM client.
pub fn get_balance(&self, provider_url: String, public_key: rhai::Blob) -> Result<String, String> {
// Use the sync helper from crate::rhai_sync_helpers
crate::rhai_sync_helpers::get_balance_sync(&self.inner, &provider_url, &public_key)
pub fn get_balance(&self, address_hex: String) -> Result<String, String> {
use ethers_core::types::Address;
let address = Address::from_slice(&hex::decode(address_hex.trim_start_matches("0x")).map_err(|e| format!("hex decode error: {e}"))?);
crate::rhai_sync_helpers::get_balance_sync(&self.inner, address)
}
/// Send transaction using the EVM client.
pub fn send_transaction(&self, provider_url: String, key_id: String, password: rhai::Blob, tx_data: rhai::Map) -> Result<String, String> {
// Use the sync helper from crate::rhai_sync_helpers
crate::rhai_sync_helpers::send_transaction_sync(&self.inner, &provider_url, &key_id, &password, tx_data)
}
}
engine.register_type::<RhaiEvmClient>();
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
engine.register_fn("send_transaction", RhaiEvmClient::send_transaction);
// Register instance for scripts
let rhai_ec = RhaiEvmClient { inner: evm_client.clone() };
// Rhai does not support register_global_constant; pass the client as a parameter or use module scope.

View File

@@ -11,11 +11,10 @@ use tokio::runtime::Handle;
#[cfg(not(target_arch = "wasm32"))]
pub fn get_balance_sync(
evm_client: &EvmClient,
provider_url: &str,
public_key: &[u8],
address: ethers_core::types::Address,
) -> Result<String, String> {
Handle::current().block_on(async {
evm_client.get_balance(provider_url, public_key)
evm_client.get_balance(address)
.await
.map(|b| b.to_string())
.map_err(|e| format!("get_balance error: {e}"))
@@ -26,15 +25,13 @@ pub fn get_balance_sync(
#[cfg(not(target_arch = "wasm32"))]
pub fn send_transaction_sync(
evm_client: &EvmClient,
provider_url: &str,
key_id: &str,
password: &[u8],
tx_data: Map,
tx: crate::provider::Transaction,
signer: &dyn crate::signer::Signer,
) -> Result<String, String> {
Handle::current().block_on(async {
evm_client.send_transaction(provider_url, key_id, password, tx_data)
evm_client.send_transaction(tx, signer)
.await
.map(|tx| tx.to_string())
.map(|tx| format!("0x{:x}", tx))
.map_err(|e| format!("send_transaction error: {e}"))
})
}

View File

@@ -1,80 +1,8 @@
// Signing should be done using ethers-core utilities directly. This file is now empty.
use super::error::EvmError;
// Native: Only compile for non-WASM
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait Signer: Send + Sync {
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>;
fn address(&self) -> String;
}
// WASM: Only compile for WASM
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
pub trait Signer {
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>;
fn address(&self) -> String;
}
// --- Implementation for vault::SessionManager ---
#[cfg(target_arch = "wasm32")]
#[async_trait::async_trait(?Send)]
impl<S: vault::KVStore> Signer for vault::SessionManager<S> {
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError> {
log::debug!("SessionManager::sign called");
self.sign(message)
.await
.map_err(|e| {
log::error!("Vault signing error: {}", e);
EvmError::Vault(e.to_string())
})
}
fn address(&self) -> String {
log::debug!("SessionManager::address called");
self.current_keypair()
.map(|k| {
if k.key_type == vault::KeyType::Secp256k1 {
let pubkey = &k.public_key;
use alloy_primitives::keccak256;
let hash = keccak256(&pubkey[1..]);
format!("0x{}", hex::encode(&hash[12..]))
} else {
format!("0x{}", hex::encode(&k.public_key))
}
})
.unwrap_or_default()
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait::async_trait]
impl<S: vault::KVStore + Send + Sync> Signer for vault::SessionManager<S> {
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError> {
log::debug!("SessionManager::sign called");
self.sign(message)
.await
.map_err(|e| {
log::error!("Vault signing error: {}", e);
EvmError::Vault(e.to_string())
})
}
fn address(&self) -> String {
log::debug!("SessionManager::address called");
self.current_keypair()
.map(|k| {
if k.key_type == vault::KeyType::Secp256k1 {
let pubkey = &k.public_key;
use alloy_primitives::keccak256;
let hash = keccak256(&pubkey[1..]);
format!("0x{}", hex::encode(&hash[12..]))
} else {
format!("0x{}", hex::encode(&k.public_key))
}
})
.unwrap_or_default()
}
}