sal-modular/docs/rhai_architecture_plan.md

10 KiB
Raw Blame History

Rhai Scripting System Architecture & Implementation Plan

Project Goal

Build a system that allows users to write and execute Rhai scripts both:

  • Locally via a CLI, and
  • In the browser via a browser extension

using the same core logic (the vault and evm_client crates), powered by a Rust WebAssembly module.


Requirements & Architecture

1. Shared Rust Libraries

  • Core Libraries:
    • vault/: Cryptographic vault and session management
    • evm_client/: EVM RPC client
  • Responsibilities:
    • Implement business logic
    • Expose functions to the Rhai scripting engine
    • Reusable in both native CLI and WebAssembly builds
rhai_sandbox_workspace/
├── Cargo.toml              # Workspace manifest
├── vault/                  # Shared logic + Rhai bindings
│   ├── Cargo.toml
│   └── src/
│       ├── lib.rs          # Public API (all core logic)
│       ├── rhai_bindings.rs# Rhai registration (shared)
│       └── utils.rs        # Any helpers
├── cli/                    # CLI runner
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
├── wasm/                   # Wasm runner using same API
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── browser-extension/      # (optional) Extension frontend
│   ├── manifest.json
│   ├── index.html
│   └── index.js

3. Code Organization for Shared Rhai Bindings

In vault/src/lib.rs:

pub mod rhai_bindings;
pub use rhai_bindings::register_rhai_api;

pub fn fib(n: i64) -> i64 {
    if n < 2 { n } else { fib(n - 1) + fib(n - 2) }
}

In vault/src/rhai_bindings.rs:

use rhai::{Engine, RegisterFn};
use crate::fib;

pub fn register_rhai_api(engine: &mut Engine) {
    engine.register_fn("fib", fib);
}

Using in CLI (cli/src/main.rs):

use rhai::Engine;
use vault::register_rhai_api;
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        eprintln!("Usage: cli <script.rhai>");
        return;
    }
    let script = fs::read_to_string(&args[1]).expect("Failed to read script");
    let mut engine = Engine::new();
    register_rhai_api(&mut engine);
    match engine.eval::<i64>(&script) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Using in WASM (wasm/src/lib.rs):

use wasm_bindgen::prelude::*;
use rhai::Engine;
use vault::register_rhai_api;

#[wasm_bindgen]
pub fn run_rhai(script: &str) -> JsValue {
    let mut engine = Engine::new();
    register_rhai_api(&mut engine);
    match engine.eval_expression::<i64>(script) {
        Ok(res) => JsValue::from_f64(res as f64),
        Err(e) => JsValue::from_str(&format!("Error: {}", e)),
    }
}

Benefits:

  • Single source of truth for Rhai bindings (register_rhai_api)
  • Easy to expand: add more Rust functions and register in one place
  • Works seamlessly across CLI and WASM
  • Encourages code reuse and maintainability

This approach fully adheres to the principles in architecture.md:

  • Modular, layered design
  • Code reuse across targets
  • Shared business logic for both native and WASM
  • Clean separation of platform-specific code

4. Native CLI Tool (cli/)

  • Accepts .rhai script file or stdin
  • Uses shared libraries to run the script via the Rhai engine
  • Outputs the result to the terminal

5. WebAssembly Module (wasm/)

  • Uses the same core library for Rhai logic
  • Exposes a run_rhai(script: &str) -> String function via wasm_bindgen
  • Usable from browser-based JS (e.g., import { run_rhai })

4. Browser Extension (browser_extension/)

  • UI for user to enter Rhai code after loading keyspace and selecting keypair (using SessionManager)
  • Loads the WebAssembly module
  • Runs user input through run_rhai(script)
  • Displays the result or error
  • Security:
    • Only allows script input from:
      • Trusted websites (via content script injection)
      • Extension popup UI---

EVM Client Integration: Pluggable Signer Pattern


Cross-Platform, Multi-Network EvmClient Design

To be consistent with the rest of the project and adhere to the architecture and modularity principles, the evm_client crate should:

  • Use async APIs and traits for all network and signing operations
  • Support both native and WASM (browser) environments via conditional compilation
  • Allow dynamic switching between multiple EVM networks/providers at runtime
  • Avoid direct dependencies on vault/session, using the pluggable Signer trait
  • Expose a clear, ergonomic API for both Rust and Rhai scripting

1. EvmProvider Abstraction

pub enum EvmProvider {
    Http { name: String, url: String, chain_id: u64 },
    // Future: WebSocket, Infura, etc.
}

2. EvmClient Struct

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

3. Provider Networking (Native + WASM)

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

// Cross-platform HTTP POST
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?))
    }
}

4. Rhai Scripting Integration

  • Expose add_network, switch_network, and send_tx functions to the Rhai engine via the shared bindings pattern.
  • Example:
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);
}

5. Consistency and Compliance

  • Async, modular, and testable: All APIs are async and trait-based, just like kvstore/vault.
  • No direct dependencies: EvmClient is generic over signing backend, like other crates.
  • Cross-platform: Uses conditional compilation for networking, ensuring WASM and native support.
  • Clear separation: Network and signing logic are independent, allowing easy extension and testing.

This design fits seamlessly with your projects architecture and modularity goals.

5. Web App Integration

  • Enable trusted web apps to send Rhai scripts to the extension, using one or both of:

Option A: Direct (Client-side)

  • Web apps use window.postMessage() or DOM events
  • Extension listens via content script
  • Validates origin before running the script

Option B: Server-based (WebSocket)

  • Both extension and web app connect to a backend WebSocket server
  • Web app sends script to server
  • Server routes it to the right connected extension client
  • Extension executes the script and returns the result

6. Security Considerations

  • All script execution is sandboxed via Rhai + Wasm
  • Only allow input from:
    • Extension popup
    • Approved websites or servers
  • Validate origins and inputs strictly
  • Do not expose internal APIs beyond run_rhai(script)

High-Level Component Diagram

+-------------------+     +-------------------+
|   CLI Tool        |     |  Browser Extension|
|  (cli/)           |     |  (browser_ext/)   |
+---------+---------+     +---------+---------+
          |                         |
          |        +----------------+
          |        |
+---------v---------+
|   WASM Module     |   <--- Shared Rust Core (vault, evm_client)
|   (wasm/)         |
+---------+---------+
          |
+---------v---------+
|  Rhai Engine      |
+-------------------+

Implementation Phases

Phase 1: Core Library Integration

  • Ensure all business logic (vault & evm_client) is accessible from both native and WASM targets.
  • Expose required functions to Rhai engine.

Phase 2: CLI Tool

  • Implement CLI that loads and runs Rhai scripts using the shared core.
  • Add support for stdin and file input.

Phase 3: WASM Module

  • Build WASM module exposing run_rhai.
  • Integrate with browser JS via wasm_bindgen.

Phase 4: Browser Extension

  • UI for script input and result display.
  • Integrate WASM module and SessionManager.
  • Secure script input (popup and trusted sites only).

Phase 5: Web App Integration

  • Implement postMessage and/or WebSocket protocol for trusted web apps to send scripts.
  • Validate origins and handle results.

Open Questions / TODOs

  • What subset of the vault/evm_client API should be exposed to Rhai?
  • What are the best practices for sandboxing Rhai in WASM?
  • How will user authentication/session be handled between extension and web app?
  • How will error reporting and logging work across boundaries?

References


Last updated: 2025-05-15