342 lines
10 KiB
Markdown
342 lines
10 KiB
Markdown
# 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<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`):**
|
||
```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::<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
|
||
```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<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)
|
||
```rust
|
||
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:
|
||
```rust
|
||
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
|
||
- [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*
|