10 KiB
10 KiB
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 managementevm_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
:
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 viawasm_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---
- Only allows script input from:
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
, andsend_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 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
Last updated: 2025-05-15