tests: Add browser WASM tests for evm_client
This commit is contained in:
		| @@ -65,6 +65,19 @@ This document outlines the steps and requirements to guarantee that both native | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Browser (WASM) Testing for evm_client | ||||
|  | ||||
| To run browser-based tests for `evm_client`: | ||||
|  | ||||
| ```sh | ||||
| cd evm_client | ||||
| wasm-pack test --headless --firefox | ||||
| # or | ||||
| wasm-pack test --headless --chrome | ||||
| ``` | ||||
|  | ||||
| This will compile your crate to WASM and run the tests in a real browser environment. | ||||
|  | ||||
| ## 6. Checklist for Compliance | ||||
| - [ ] No unconditional `tokio` usage in library code | ||||
| - [ ] All dependencies are WASM-compatible (where needed) | ||||
|   | ||||
| @@ -9,5 +9,19 @@ path = "src/lib.rs" | ||||
| [dependencies] | ||||
| vault = { path = "../vault" } | ||||
| async-trait = "0.1" | ||||
| alloy = "0.6" | ||||
| thiserror = "1" | ||||
| alloy-rlp = { version = "0.3.11", features = ["derive"] } | ||||
| alloy-primitives = "1.1.0" | ||||
| serde = { version = "1", features = ["derive"] } | ||||
| serde_json = "1" | ||||
| log = "0.4" | ||||
| hex = "0.4" | ||||
| reqwest = { version = "0.11", features = ["json"] } | ||||
| k256 = { version = "0.13", features = ["ecdsa"] } | ||||
| gloo-net = { version = "0.5", features = ["http"] } | ||||
|  | ||||
| [dev-dependencies] | ||||
| wasm-bindgen-test = "0.3" | ||||
|  | ||||
| [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] | ||||
| tokio = { version = "1", features = ["macros", "rt-multi-thread"] } | ||||
|   | ||||
| @@ -2,6 +2,12 @@ | ||||
|  | ||||
|  | ||||
|  | ||||
| pub mod signer; | ||||
| pub mod provider; | ||||
| pub mod utils; | ||||
|  | ||||
| pub use signer::Signer; | ||||
| pub use provider::EvmProvider; | ||||
|  | ||||
| #[derive(Debug, thiserror::Error)] | ||||
| pub enum EvmError { | ||||
| @@ -9,15 +15,120 @@ pub enum EvmError { | ||||
|     Rpc(String), | ||||
|     #[error("Vault error: {0}")] | ||||
|     Vault(String), | ||||
|     #[error("Unknown network")]  | ||||
|     UnknownNetwork, | ||||
|     #[error("No provider selected")]  | ||||
|     NoNetwork, | ||||
| } | ||||
|  | ||||
| pub struct EvmClient { | ||||
|     // ... fields for RPC, vault, etc. | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| pub struct EvmClient<S: Signer> { | ||||
|     providers: HashMap<String, EvmProvider>, | ||||
|     current: Option<String>, | ||||
|     signer: S, | ||||
| } | ||||
|  | ||||
| impl EvmClient { | ||||
|     pub async fn connect(_rpc_url: &str) -> Result<Self, EvmError> { | ||||
|         todo!("Implement connect") | ||||
| impl<S: Signer> EvmClient<S> { | ||||
|     pub fn new(signer: S) -> Self { | ||||
|         Self { | ||||
|             providers: HashMap::new(), | ||||
|             current: None, | ||||
|             signer, | ||||
|         } | ||||
|     } | ||||
|     pub fn add_provider(&mut self, key: String, provider: EvmProvider) { | ||||
|         self.providers.insert(key, provider); | ||||
|     } | ||||
|     pub fn set_current(&mut self, key: &str) -> Result<(), EvmError> { | ||||
|         if self.providers.contains_key(key) { | ||||
|             self.current = Some(key.to_string()); | ||||
|             Ok(()) | ||||
|         } else { | ||||
|             Err(EvmError::UnknownNetwork) | ||||
|         } | ||||
|     } | ||||
|     pub fn current_provider(&self) -> Result<&EvmProvider, EvmError> { | ||||
|         self.current | ||||
|             .as_ref() | ||||
|             .and_then(|k| self.providers.get(k)) | ||||
|             .ok_or(EvmError::NoNetwork) | ||||
|     } | ||||
|     pub async fn get_balance(&self, address: &str) -> Result<u128, EvmError> { | ||||
|         let provider = self.current_provider()?; | ||||
|         provider.get_balance(address).await | ||||
|     } | ||||
|     pub async fn transfer(&self, to: &str, amount: u128) -> Result<String, EvmError> { | ||||
|         use crate::provider::{Transaction, parse_signature_rs_v}; | ||||
| use std::str::FromStr; | ||||
|         use alloy_primitives::{Address, U256, Bytes}; | ||||
|         use log::debug; | ||||
|  | ||||
|         let provider = self.current_provider()?; | ||||
|         let chain_id = match provider { | ||||
|             crate::provider::EvmProvider::Http { chain_id, .. } => *chain_id, | ||||
|         }; | ||||
|         // 1. Fetch nonce for sender | ||||
|         let from = self.signer.address(); | ||||
|         let nonce = provider.get_nonce(&from).await?; | ||||
|         // 2. Build tx struct | ||||
|         let tx = Transaction { | ||||
|             nonce, | ||||
|             to: Address::from_str(to).map_err(|e| EvmError::Rpc(format!("Invalid to address: {e}")))?, | ||||
|             value: U256::from(amount), | ||||
|             gas: U256::from(21000), | ||||
|             gas_price: U256::from(1_000_000_000u64), // 1 gwei | ||||
|             data: Bytes::default(), | ||||
|             chain_id, | ||||
|         }; | ||||
|         debug!("transfer: tx={:?}", tx); | ||||
|         // 3. RLP-encode unsigned | ||||
|         let unsigned = tx.rlp_encode_unsigned(); | ||||
|         // 4. Sign | ||||
|         let signature = self.signer.sign(&unsigned).await?; | ||||
|         let (r, s, v) = parse_signature_rs_v(&signature, chain_id).ok_or_else(|| EvmError::Rpc("Invalid signature length".into()))?; | ||||
|         // 5. RLP-encode signed | ||||
|         // Define a tuple for the signed transaction fields in the correct order | ||||
|         let nonce_bytes = tx.nonce.to_be_bytes::<32>(); | ||||
|         let gas_price_bytes = tx.gas_price.to_be_bytes::<32>(); | ||||
|         let gas_bytes = tx.gas.to_be_bytes::<32>(); | ||||
|         let value_bytes = tx.value.to_be_bytes::<32>(); | ||||
|         let v_bytes = U256::from(v).to_be_bytes::<32>(); | ||||
|         let r_bytes = r.to_be_bytes::<32>(); | ||||
|         let s_bytes = s.to_be_bytes::<32>(); | ||||
|         let fields: Vec<&[u8]> = vec![ | ||||
|             &nonce_bytes, | ||||
|             &gas_price_bytes, | ||||
|             &gas_bytes, | ||||
|             tx.to.as_slice(), | ||||
|             &value_bytes, | ||||
|             tx.data.as_ref(), | ||||
|             &v_bytes, | ||||
|             &r_bytes, | ||||
|             &s_bytes, | ||||
|         ]; | ||||
|         let mut raw_tx = Vec::new(); | ||||
|         alloy_rlp::encode_list::<&[u8], [u8]>(&fields, &mut raw_tx); | ||||
|         // 6. Send | ||||
|         let raw_hex = format!("0x{}", hex::encode(&raw_tx)); | ||||
|         let data = serde_json::json!({ | ||||
|             "jsonrpc": "2.0", | ||||
|             "method": "eth_sendRawTransaction", | ||||
|             "params": [raw_hex], | ||||
|             "id": 1 | ||||
|         }); | ||||
|         let url = match provider { | ||||
|             crate::provider::EvmProvider::Http { url, .. } => url, | ||||
|         }; | ||||
|         let resp = crate::utils::http_post(url, &data.to_string()).await?; | ||||
|         if let Some(err) = resp.get("error") { | ||||
|             return Err(EvmError::Rpc(format!("eth_sendRawTransaction error: {err:?}"))); | ||||
|         } | ||||
|         if let Some(result) = resp.get("result") { | ||||
|             if let Some(tx_hash) = result.as_str() { | ||||
|                 return Ok(tx_hash.to_string()); | ||||
|             } | ||||
|         } | ||||
|         Err(EvmError::Rpc("eth_sendRawTransaction: No result field in response".to_string())) | ||||
|     } | ||||
|     // ... other API stubs | ||||
| } | ||||
|   | ||||
							
								
								
									
										111
									
								
								evm_client/src/provider.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								evm_client/src/provider.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| use crate::{EvmError, Signer}; | ||||
|  | ||||
| pub enum EvmProvider { | ||||
|     Http { name: String, url: String, chain_id: u64 }, | ||||
| } | ||||
|  | ||||
| use log::{debug, error}; | ||||
| use alloy_primitives::{Address, U256, Bytes}; | ||||
|  | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| 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, | ||||
| } | ||||
|  | ||||
| impl Transaction { | ||||
|     pub fn rlp_encode_unsigned(&self) -> Vec<u8> { | ||||
|         let nonce_bytes = self.nonce.to_be_bytes::<32>(); | ||||
|         let gas_price_bytes = self.gas_price.to_be_bytes::<32>(); | ||||
|         let gas_bytes = self.gas.to_be_bytes::<32>(); | ||||
|         let value_bytes = self.value.to_be_bytes::<32>(); | ||||
|         let chain_id_bytes = self.chain_id.to_be_bytes(); | ||||
|         let fields: Vec<&[u8]> = vec![ | ||||
|             &nonce_bytes, | ||||
|             &gas_price_bytes, | ||||
|             &gas_bytes, | ||||
|             self.to.as_slice(), | ||||
|             &value_bytes, | ||||
|             self.data.as_ref(), | ||||
|             &chain_id_bytes, | ||||
|             &[0u8][..], | ||||
|             &[0u8][..], | ||||
|         ]; | ||||
|         let mut out = Vec::new(); | ||||
|         alloy_rlp::encode_list::<&[u8], [u8]>(&fields, &mut out); | ||||
|         out | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl EvmProvider { | ||||
|     pub async fn get_nonce(&self, address: &str) -> Result<U256, EvmError> { | ||||
|         debug!("get_nonce: address={}", address); | ||||
|         let data = serde_json::json!({ | ||||
|             "jsonrpc": "2.0", | ||||
|             "method": "eth_getTransactionCount", | ||||
|             "params": [address, "pending"], | ||||
|             "id": 1 | ||||
|         }); | ||||
|         let url = match self { | ||||
|             EvmProvider::Http { url, .. } => url, | ||||
|         }; | ||||
|         let resp = crate::utils::http_post(url, &data.to_string()).await?; | ||||
|         let nonce_hex = resp["result"].as_str().ok_or_else(|| { | ||||
|             error!("No result in eth_getTransactionCount response: {:?}", resp); | ||||
|             EvmError::Rpc("No result".into()) | ||||
|         })?; | ||||
|         Ok(U256::from_str_radix(nonce_hex.trim_start_matches("0x"), 16).unwrap_or(U256::ZERO)) | ||||
|     } | ||||
|  | ||||
|     pub async fn get_balance(&self, address: &str) -> Result<u128, EvmError> { | ||||
|         debug!("get_balance: address={}", address); | ||||
|         let data = serde_json::json!({ | ||||
|             "jsonrpc": "2.0", | ||||
|             "method": "eth_getBalance", | ||||
|             "params": [address, "latest"], | ||||
|             "id": 1 | ||||
|         }); | ||||
|         let url = match self { | ||||
|             EvmProvider::Http { url, .. } => url, | ||||
|         }; | ||||
|         let resp = crate::utils::http_post(url, &data.to_string()).await?; | ||||
|         let balance_hex = resp["result"].as_str().ok_or_else(|| { | ||||
|             error!("No result in eth_getBalance response: {:?}", resp); | ||||
|             EvmError::Rpc("No result".into()) | ||||
|         })?; | ||||
|         Ok(u128::from_str_radix(balance_hex.trim_start_matches("0x"), 16).unwrap_or(0)) | ||||
|     } | ||||
|  | ||||
|     /// Deprecated: Use EvmClient::transfer instead. | ||||
|     pub async fn send_transaction<S: Signer>(&self, _tx: &Transaction, _signer: &S) -> Result<String, EvmError> { | ||||
|         panic!("send_transaction is deprecated. Use EvmClient::transfer instead."); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Helper to parse a 65-byte secp256k1 signature into (r, s, v) for EVM. | ||||
| /// Assumes signature is [r (32 bytes) | s (32 bytes) | v (1 byte)] | ||||
| pub fn parse_signature_rs_v(sig: &[u8], chain_id: u64) -> Option<(U256, U256, u64)> { | ||||
|     if sig.len() != 65 { | ||||
|         return None; | ||||
|     } | ||||
|     let r = U256::from_be_bytes::<32>(sig[0..32].try_into().unwrap()); | ||||
|     let s = U256::from_be_bytes::<32>(sig[32..64].try_into().unwrap()); | ||||
|     let mut v = sig[64] as u64; | ||||
|     // EIP-155: v = recid + 35 + chain_id * 2 | ||||
|     if v < 27 { v += 27; } | ||||
|     v = v + chain_id * 2 + 8; | ||||
|     Some((r, s, v)) | ||||
| } | ||||
|  | ||||
| // Example usage: | ||||
| // let (r, s, v) = parse_signature_rs_v(&signature, tx.chain_id).unwrap(); | ||||
| // Use these for EVM transaction serialization. | ||||
|  | ||||
| // (Remove old sign_and_serialize placeholder) | ||||
|  | ||||
							
								
								
									
										81
									
								
								evm_client/src/signer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								evm_client/src/signer.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| use async_trait::async_trait; | ||||
| use crate::EvmError; | ||||
|  | ||||
| // Native: Only compile for non-WASM | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| #[async_trait] | ||||
| 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() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										25
									
								
								evm_client/src/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								evm_client/src/utils.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| use crate::EvmError; | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| pub async fn http_post(url: &str, body: &str) -> Result<serde_json::Value, EvmError> { | ||||
|     let resp = reqwest::Client::new() | ||||
|         .post(url) | ||||
|         .header("content-type", "application/json") | ||||
|         .body(body.to_owned()) | ||||
|         .send() | ||||
|         .await | ||||
|         .map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|     let json = resp.json().await.map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|     Ok(json) | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| pub async fn http_post(url: &str, body: &str) -> Result<serde_json::Value, EvmError> { | ||||
|     use gloo_net::http::Request; | ||||
|     let resp = Request::post(url) | ||||
|         .header("content-type", "application/json") | ||||
|         .body(body).map_err(|e| EvmError::Rpc(e.to_string()))? | ||||
|         .send().await.map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|     let json = resp.json().await.map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|     Ok(json) | ||||
| } | ||||
							
								
								
									
										69
									
								
								evm_client/tests/balance.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								evm_client/tests/balance.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| use evm_client::{EvmClient, EvmProvider, Signer}; | ||||
|  | ||||
| // Dummy signer that returns a known Ethereum address (Vitalik's address) | ||||
| struct DummySigner; | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| #[async_trait::async_trait] | ||||
| impl Signer for DummySigner { | ||||
|     async fn sign(&self, _message: &[u8]) -> Result<Vec<u8>, evm_client::EvmError> { | ||||
|         Err(evm_client::EvmError::Vault("sign not implemented".to_string())) | ||||
|     } | ||||
|     fn address(&self) -> String { | ||||
|         // Vitalik's main address (has funds on mainnet) | ||||
|         "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| #[tokio::test] | ||||
| async fn test_get_balance_vitalik() { | ||||
|     // Use a public Ethereum mainnet RPC | ||||
|     let provider = EvmProvider::Http { | ||||
|         name: "mainnet".to_string(), | ||||
|         url: "https://eth.drpc.org".to_string(), | ||||
|         chain_id: 1, | ||||
|     }; | ||||
|     let signer = DummySigner; | ||||
|     let mut client = EvmClient::new(signer); | ||||
|     client.add_provider("mainnet".to_string(), provider); | ||||
|     client.set_current("mainnet").unwrap(); | ||||
|     let balance = client.get_balance("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").await.unwrap(); | ||||
|     assert!(balance > 0, "Vitalik's balance should be greater than zero"); | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use wasm_bindgen_test::*; | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| #[async_trait::async_trait(?Send)] | ||||
| impl Signer for DummySigner { | ||||
|     async fn sign(&self, _message: &[u8]) -> Result<Vec<u8>, evm_client::EvmError> { | ||||
|         Err(evm_client::EvmError::Vault("sign not implemented".to_string())) | ||||
|     } | ||||
|     fn address(&self) -> String { | ||||
|         // Vitalik's main address (has funds on mainnet) | ||||
|         "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| #[wasm_bindgen_test(async)] | ||||
| async fn test_get_balance_vitalik_browser() { | ||||
|     let provider = EvmProvider::Http { | ||||
|         name: "mainnet".to_string(), | ||||
|         url: "https://eth.drpc.org".to_string(), | ||||
|         chain_id: 1, | ||||
|     }; | ||||
|     let signer = DummySigner; | ||||
|     let mut client = EvmClient::new(signer); | ||||
|     client.add_provider("mainnet".to_string(), provider); | ||||
|     client.set_current("mainnet").unwrap(); | ||||
|     let balance = client.get_balance("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").await; | ||||
|     assert!(balance.is_ok(), "Balance query should succeed in browser"); | ||||
|     let balance = balance.unwrap(); | ||||
|     assert!(balance > 0u128, "Vitalik's balance should be greater than zero"); | ||||
| } | ||||
							
								
								
									
										59
									
								
								evm_client/tests/evm_client.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								evm_client/tests/evm_client.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| #![cfg(not(target_arch = "wasm32"))] | ||||
| // tests/evm_client.rs | ||||
| use evm_client::{EvmClient, EvmProvider, Signer, EvmError}; | ||||
|  | ||||
| struct DummySigner; | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| #[async_trait::async_trait(?Send)] | ||||
| impl Signer for DummySigner { | ||||
|     async fn sign(&self, _message: &[u8]) -> Result<Vec<u8>, EvmError> { | ||||
|         Ok(vec![0u8; 65]) // dummy signature | ||||
|     } | ||||
|     fn address(&self) -> String { | ||||
|         "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| #[async_trait::async_trait] | ||||
| impl Signer for DummySigner { | ||||
|     async fn sign(&self, _message: &[u8]) -> Result<Vec<u8>, EvmError> { | ||||
|         Ok(vec![0u8; 65]) // dummy signature | ||||
|     } | ||||
|     fn address(&self) -> String { | ||||
|         "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_transfer_rlp_encoding() { | ||||
|     let provider = EvmProvider::Http { | ||||
|         name: "mainnet".to_string(), | ||||
|         url: "https://rpc.ankr.com/eth".to_string(), | ||||
|         chain_id: 1, | ||||
|     }; | ||||
|     let signer = DummySigner; | ||||
|     let mut client = EvmClient::new(signer); | ||||
|     client.add_provider("mainnet".to_string(), provider); | ||||
|     client.set_current("mainnet").unwrap(); | ||||
|     // Use a dummy transfer (will fail to send, but will test RLP logic) | ||||
|     let result = client.transfer("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 1u128).await; | ||||
|     // Should fail due to dummy signature, but should not panic or error at RLP encoding | ||||
|     assert!(matches!(result, Err(EvmError::Rpc(_)) | Err(EvmError::Vault(_)))); | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_transfer_invalid_address() { | ||||
|     let provider = EvmProvider::Http { | ||||
|         name: "mainnet".to_string(), | ||||
|         url: "https://rpc.ankr.com/eth".to_string(), | ||||
|         chain_id: 1, | ||||
|     }; | ||||
|     let signer = DummySigner; | ||||
|     let mut client = EvmClient::new(signer); | ||||
|     client.add_provider("mainnet".to_string(), provider); | ||||
|     client.set_current("mainnet").unwrap(); | ||||
|     let result = client.transfer("invalid_address", 1u128).await; | ||||
|     assert!(matches!(result, Err(EvmError::Rpc(_)))); | ||||
| } | ||||
							
								
								
									
										50
									
								
								evm_client/tests/wasm.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								evm_client/tests/wasm.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| #![cfg(target_arch = "wasm32")] | ||||
| use wasm_bindgen_test::*; | ||||
| wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); | ||||
|  | ||||
| use evm_client::{EvmClient, EvmProvider, Signer, EvmError}; | ||||
|  | ||||
| struct DummySigner; | ||||
|  | ||||
| #[async_trait::async_trait(?Send)] | ||||
| impl Signer for DummySigner { | ||||
|     async fn sign(&self, _message: &[u8]) -> Result<Vec<u8>, EvmError> { | ||||
|         Ok(vec![0u8; 65]) // dummy signature | ||||
|     } | ||||
|     fn address(&self) -> String { | ||||
|         "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen_test(async)] | ||||
| async fn test_get_balance_vitalik_browser() { | ||||
|     let provider = EvmProvider::Http { | ||||
|         name: "mainnet".to_string(), | ||||
|         url: "https://eth.drpc.org".to_string(), | ||||
|         chain_id: 1, | ||||
|     }; | ||||
|     let signer = DummySigner; | ||||
|     let mut client = EvmClient::new(signer); | ||||
|     client.add_provider("mainnet".to_string(), provider); | ||||
|     client.set_current("mainnet").unwrap(); | ||||
|     let balance = client.get_balance("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").await; | ||||
|     assert!(balance.is_ok(), "Balance query should succeed in browser"); | ||||
|     let balance = balance.unwrap(); | ||||
|     assert!(balance > 0u128, "Vitalik's balance should be greater than zero"); | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen_test(async)] | ||||
| async fn test_transfer_rlp_encoding_browser() { | ||||
|     let provider = EvmProvider::Http { | ||||
|         name: "mainnet".to_string(), | ||||
|         url: "https://eth.drpc.org".to_string(), | ||||
|         chain_id: 1, | ||||
|     }; | ||||
|     let signer = DummySigner; | ||||
|     let mut client = EvmClient::new(signer); | ||||
|     client.add_provider("mainnet".to_string(), provider); | ||||
|     client.set_current("mainnet").unwrap(); | ||||
|     let result = client.transfer("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 1u128).await; | ||||
|     assert!(matches!(result, Err(EvmError::Rpc(_)) | Err(EvmError::Vault(_)))); | ||||
| } | ||||
|  | ||||
| @@ -32,7 +32,7 @@ pub struct KeyEntry { | ||||
|     pub metadata: Option<KeyMetadata>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] | ||||
| pub enum KeyType { | ||||
|     Secp256k1, | ||||
|     Ed25519, | ||||
|   | ||||
| @@ -12,7 +12,7 @@ mod session; | ||||
|  | ||||
| mod utils; | ||||
|  | ||||
| use kvstore::KVStore; | ||||
| pub use kvstore::traits::KVStore; | ||||
| use data::*; | ||||
| use error::VaultError; | ||||
| use crate::crypto::random_salt; | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| tempfile = "3.10" | ||||
		Reference in New Issue
	
	Block a user