# 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 ### 2. Recommended File Structure ``` 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`:** ```rust 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`:** ```rust 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`):** ```rust use rhai::Engine; use vault::register_rhai_api; use std::env; use std::fs; fn main() { let args: Vec = env::args().collect(); if args.len() != 2 { eprintln!("Usage: cli "); 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::(&script) { Ok(result) => println!("Result: {}", result), Err(e) => eprintln!("Error: {}", e), } } ``` **Using in WASM (`wasm/src/lib.rs`):** ```rust 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::(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 ```rust pub enum EvmProvider { Http { name: String, url: String, chain_id: u64 }, // Future: WebSocket, Infura, etc. } ``` ### 2. EvmClient Struct ```rust use std::collections::HashMap; pub struct EvmClient { providers: HashMap, current: String, signer: S, } impl EvmClient { 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 { let provider = self.current_provider().ok_or(EvmError::NoNetwork)?; provider.send_raw_transaction(&tx, &self.signer).await } } ``` ### 3. Provider Networking (Native + WASM) ```rust impl EvmProvider { pub async fn send_raw_transaction(&self, tx: &Transaction, signer: &S) -> Result { 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 { #[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: ```rust pub fn register_rhai_api(engine: &mut Engine) { engine.register_type::>(); 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 project’s 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 - [Rhai scripting engine](https://rhai.rs/) - [wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/) - [WebExtension APIs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions) - [Rust + WASM Book](https://rustwasm.github.io/book/) --- *Last updated: 2025-05-15*