sal-modular/docs/evm_client_architecture_plan.md

5.3 KiB

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

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

// 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

// 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

// 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)

// 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

// 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

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