This utility creates Metamask Snaps for Sovereign SDK modules.
cargo install --git https://github.com/Sovereign-Labs/sov-snap-generator --tag "v0.1.2"
Also, check if the wasm32-wasi
target is installed:
rustup target list --installed | grep wasm32-wasi
If the command above yields no output, proceed with the target installation:
rustup target add wasm32-wasi
This example will re-export the RuntimeCall
from demo-stf
.
First, we create the sample project:
cargo new --lib sov-runtime
cd sov-runtime
Next, update the Cargo.toml
with the following:
[package]
name = "sov-runtime"
version = "0.1.0"
edition = "2021"
[dependencies]
## Required dependencies for the Snap
borsh = "0.10.3"
serde_json = "1.0"
sov-modules-api = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", rev = "df169be", features = ["serde"] }
## Example definition of a module `RuntimeCall`
## Will be replaced by the user module implementation
demo-stf = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", rev = "df169be", features = ["serde"] }
sov-mock-da = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", rev = "df169be" }
Then, update the src/lib.rs
with the following:
/// The `Context` will be used to define the asymmetric key pair.
pub use sov_modules_api::default_context::ZkDefaultContext as Context;
/// The `DaSpec` will be used to define the runtime specification.
pub use sov_mock_da::MockDaSpec as DaSpec;
/// The `RuntimeCall` will be the call message of the transaction to be signed. This is normally generated automatically by the SDK via the `DispatchCall` derive macro.
pub use demo_stf::runtime::RuntimeCall;
Finally, fetch the default constants.json
required for module compilation.
wget https://raw.githubusercontent.com/Sovereign-Labs/sovereign-sdk/d42e289f26b9824b5ed54dbfbda94007dee305b2/constants.json
The utility defaults to searching for Context
, DaSpec
, and RuntimeCall
definitions at the project root. However, these can be replaced with other paths.
For a sanity check, run the following:
cargo check
Some prompts can be specified through CLI arguments. For more information, run:
sov-snap-generator --help
We use the options --defaults
and --force
to skip prompt confirmations and checks.
First, we generate the target project. This will create the Snap project under ../sov-runtime-snap
(i.e. --target
argument).
cargo sov-snap-generator init --defaults --force --target ../sov-runtime-snap
We can perform a sanity check on the generated WASM project. This project is editable by the user to adhere to the WASM file specification. Nevertheless, functions designated with the directive #[no_mangle]
will be consumed by the Snap and will typically remain unchanged.
cargo check --manifest-path ../sov-runtime-snap/external/sov-wasm/Cargo.toml
To compile the Snap, run:
cargo sov-snap-generator build --target ../sov-runtime-snap
Finally, you can run the local development environment.
Requirements: Metamask Flask
To initiate a web development environment with your Snap, execute the following:
cd ../sov-runtime-snap
yarn start
Your Snap is accessible by default at http://localhost:8000
. To load the Snap into your Metamask Flask and enable signing of transactions, click on Connect/Reconnect.
This development environment allows you to submit a signed transaction to a sov-sequencer. However, most modern browsers query external services for a CORS policy. Normally, a Sequencer will be served behind a layer that handles authentication.
To bypass this issue, you can either disable CORS in your browser or set up a proxy to handle CORS requests, forwarding the payload to the sequencer. Here is a minimalistic Python script that will run a CORS proxy on port 9000, redirecting all requests to 127.0.0.1:12345
:
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
# Define the target URL (the URL you want to proxy to)
TARGET_URL = "http://127.0.0.1:12345"
# Enable CORS for all routes
@app.after_request
def add_cors_headers(response):
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
return response
@app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'OPTIONS'])
@app.route('/<path:path>', methods=['GET', 'POST', 'OPTIONS'])
def proxy(path):
target_url = f"{TARGET_URL}/{path}"
headers = {key: value for (key, value) in request.headers if key != 'Host'}
if request.method == 'OPTIONS':
# Handle preflight requests
return jsonify({'status': 'ok'})
if request.method == 'POST':
# Forward POST request
response = requests.post(target_url, data=request.get_data(), headers=headers)
else:
# Forward GET request
response = requests.get(target_url, headers=headers)
# Forward the received headers and content to the client
headers = [(key, value) for (key, value) in response.headers.items()]
return response.content, response.status_code, headers
if __name__ == '__main__':
app.run(port=9000) # Change the port if needed