//! evm_client: EVM RPC client and integration //! evm_client: Minimal EVM JSON-RPC client abstraction //! evm_client: Minimal EVM JSON-RPC client abstraction //! evm_client: Minimal EVM JSON-RPC client abstraction //! evm_client: Minimal EVM JSON-RPC client abstraction //! evm_client: Minimal EVM JSON-RPC client abstraction 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, } pub struct EvmClient { pub provider: Provider, } impl EvmClient { pub fn new(provider: Provider) -> Self { Self { provider } } /// 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 { // 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 { use ethers_core::types::{U256, H256}; 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) } }