165 lines
6.6 KiB
Rust
165 lines
6.6 KiB
Rust
//! 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<String>,
|
|
}
|
|
|
|
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<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};
|
|
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)
|
|
}
|
|
}
|