189 lines
5.3 KiB
Markdown
189 lines
5.3 KiB
Markdown
# EVM Client Architecture & Implementation Plan
|
|
|
|
## Project Goal
|
|
Build a cross-platform (native + WASM) EVM client that can:
|
|
- Interact with multiple EVM-compatible networks/providers
|
|
- Use pluggable signing backends (e.g., SessionManager, hardware wallets, mocks)
|
|
- Integrate seamlessly with Rhai scripting and the rest of the modular Rust workspace
|
|
|
|
---
|
|
|
|
## Requirements & Principles
|
|
- **Async, modular, and testable**: All APIs are async and trait-based
|
|
- **Cross-platform**: Native (Rust) and WASM (browser) support
|
|
- **Multi-network**: Support for multiple EVM networks/providers, switchable at runtime
|
|
- **Pluggable signing**: No direct dependency on vault/session; uses a generic Signer trait
|
|
- **Consistency**: Follows conventions in architecture.md and other project docs
|
|
- **Scripting**: Exposes ergonomic API for both Rust and Rhai scripting
|
|
|
|
---
|
|
|
|
## Recommended File Structure
|
|
|
|
```
|
|
evm_client/
|
|
├── Cargo.toml
|
|
└── src/
|
|
├── lib.rs # Public API
|
|
├── provider.rs # EvmProvider abstraction
|
|
├── client.rs # EvmClient struct
|
|
├── signer.rs # Signer trait
|
|
└── utils.rs # Helpers (e.g., HTTP, WASM glue)
|
|
```
|
|
|
|
---
|
|
|
|
## 1. Pluggable Signer Trait
|
|
|
|
```rust
|
|
// signer.rs
|
|
#[async_trait::async_trait]
|
|
pub trait Signer {
|
|
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>;
|
|
fn address(&self) -> String;
|
|
}
|
|
```
|
|
|
|
- `SessionManager` in vault implements this trait.
|
|
- Any other backend (mock, hardware wallet, etc.) can implement it.
|
|
|
|
---
|
|
|
|
## 2. EvmProvider Abstraction
|
|
|
|
```rust
|
|
// provider.rs
|
|
pub enum EvmProvider {
|
|
Http { name: String, url: String, chain_id: u64 },
|
|
// Future: WebSocket, Infura, etc.
|
|
}
|
|
|
|
impl EvmProvider {
|
|
pub async fn send_raw_transaction<S: Signer>(&self, tx: &Transaction, signer: &S) -> Result<TxHash, EvmError> {
|
|
let raw_tx = tx.sign(signer).await?;
|
|
let body = format!("{{\"raw\":\"{}\"}}", hex::encode(&raw_tx));
|
|
match self {
|
|
EvmProvider::Http { url, .. } => {
|
|
http_post(url, &body).await
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. EvmClient Struct & API
|
|
|
|
```rust
|
|
// client.rs
|
|
use std::collections::HashMap;
|
|
|
|
pub struct EvmClient<S: Signer> {
|
|
providers: HashMap<String, EvmProvider>,
|
|
current: String,
|
|
signer: S,
|
|
}
|
|
|
|
impl<S: Signer> EvmClient<S> {
|
|
pub fn new(signer: S) -> Self {
|
|
Self {
|
|
providers: HashMap::new(),
|
|
current: String::new(),
|
|
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 = key.to_string();
|
|
Ok(())
|
|
} else {
|
|
Err(EvmError::UnknownNetwork)
|
|
}
|
|
}
|
|
pub fn current_provider(&self) -> Option<&EvmProvider> {
|
|
self.providers.get(&self.current)
|
|
}
|
|
pub async fn send_transaction(&self, tx: Transaction) -> Result<TxHash, EvmError> {
|
|
let provider = self.current_provider().ok_or(EvmError::NoNetwork)?;
|
|
provider.send_raw_transaction(&tx, &self.signer).await
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Cross-Platform Networking (Native + WASM)
|
|
|
|
```rust
|
|
// utils.rs
|
|
pub async fn http_post(url: &str, body: &str) -> Result<TxHash, EvmError> {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
let resp = reqwest::Client::new().post(url).body(body.to_owned()).send().await?;
|
|
// parse response...
|
|
Ok(parse_tx_hash(resp.text().await?))
|
|
}
|
|
#[cfg(target_arch = "wasm32")]
|
|
{
|
|
let resp = gloo_net::http::Request::post(url).body(body).send().await?;
|
|
// parse response...
|
|
Ok(parse_tx_hash(resp.text().await?))
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Rhai Scripting Integration
|
|
|
|
```rust
|
|
// rhai_bindings.rs
|
|
pub fn register_rhai_api(engine: &mut Engine) {
|
|
engine.register_type::<EvmClient<MySigner>>();
|
|
engine.register_fn("add_network", EvmClient::add_provider);
|
|
engine.register_fn("switch_network", EvmClient::set_current);
|
|
engine.register_fn("send_tx", EvmClient::send_transaction);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Usage Example
|
|
|
|
```rust
|
|
use evm_client::{EvmClient, EvmProvider, Signer};
|
|
use vault::SessionManager;
|
|
|
|
let mut client = EvmClient::new(session_manager);
|
|
client.add_provider("mainnet".into(), EvmProvider::Http { name: "Ethereum Mainnet".into(), url: "...".into(), chain_id: 1 });
|
|
client.add_provider("polygon".into(), EvmProvider::Http { name: "Polygon".into(), url: "...".into(), chain_id: 137 });
|
|
client.set_current("polygon")?;
|
|
let tx_hash = client.send_transaction(tx).await?;
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Compliance & Consistency
|
|
- **Async/trait-based**: Like kvstore/vault, all APIs are async and trait-based
|
|
- **No direct dependencies**: Uses generic Signer, not vault/session directly
|
|
- **Cross-platform**: Uses conditional networking for native/WASM
|
|
- **Modular/testable**: Clear separation of provider, client, and signer logic
|
|
- **Rhai scripting**: Exposes ergonomic scripting API
|
|
- **Follows architecture.md**: Modular, layered, reusable, and extensible
|
|
|
|
---
|
|
|
|
## 8. Open Questions / TODOs
|
|
- How to handle provider-specific errors and retries?
|
|
- Should we support WebSocket providers in v1?
|
|
- What subset of EVM JSON-RPC should be exposed to Rhai?
|
|
- How to best test WASM networking in CI?
|
|
|
|
---
|
|
|
|
*Last updated: 2025-05-16*
|