Creating a Simple Nucleus
In this chapter, we will guide you step-by-step through building a simple Nucleus (AVS).
Let's begin by setting up the code structure directly:
Step 1: Create a New Rust Project
Navigate to your desired project directory and create a new Rust project using cargo
:
cargo new --lib hello-avs
cd hello-avs
Step 2: Update the Cargo.toml
File
Replace the contents of your Cargo.toml
with the following:
[package]
name = "hello-avs"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
vrs-core-sdk = { version = "0.2.0" }
parity-scale-codec = { version = "3.6", features = ["derive"] }
scale-info = { version = "2.11.6", features = ["derive"] }
Step 3: Implement the Core Logic
Edit the file src/lib.rs
and insert the following code:
#![allow(unused)] fn main() { use scale_info::TypeInfo; use vrs_core_sdk::codec::{Decode, Encode}; use vrs_core_sdk::nucleus; #[derive(Debug, Decode, Encode, TypeInfo)] pub struct User { pub id: u64, pub name: String, } #[nucleus] pub mod nucleus { use crate::User; use vrs_core_sdk::codec::{Decode, Encode}; use vrs_core_sdk::{get, post, storage}; #[post] pub fn add_user(user: User) -> Result<u64, String> { let key = [&b"user:"[..], &user.id.to_be_bytes()[..]].concat(); println!("{:?}", key); storage::put(&key, &user.encode()).map_err(|e| e.to_string())?; Ok(user.id) } #[get] pub fn get_user(id: u64) -> Result<Option<User>, String> { let key = [&b"user:"[..], &id.to_be_bytes()[..]].concat(); println!("{:?}", key); let result = storage::get(&key).map_err(|e| e.to_string())?; let user = result.map(|data| User::decode(&mut &data[..]).unwrap()); Ok(user) } } }
Step 4: Compile the Project to WebAssembly
Finally, compile your project to WebAssembly by running the following command in your project's root directory (hello-avs
):
cargo build --release --target wasm32-unknown-unknown
Upon successful compilation, you'll see a message similar to:
Finished `release` profile [optimized] target(s) in 0.09s
You have successfully created a simple Nucleus (AVS). The compiled WebAssembly executable (hello_avs.wasm
) will be available in the directory target/wasm32-unknown-unknown/release
.
What Does hello-avs
Do?
Let's break down the functionality provided by this simple Nucleus step-by-step:
First, a struct named User
is defined:
#![allow(unused)] fn main() { #[derive(Debug, Decode, Encode, TypeInfo)] pub struct User { pub id: u64, pub name: String, } }
Nucleus interfaces are exposed externally using macros such as #[post]
and #[get]
.
- The
#[post]
macro is used for interfaces that modify the blockchain's storage, as modifications consume gas. - The
#[get]
macro is used for interfaces that do not modify the storage.
The storage interaction APIs are as follows:
storage::put
writes data to storage.storage::get
reads data from storage.
Purpose of TypeInfo
and #[nucleus]
TypeInfo
#![allow(unused)] fn main() { #[derive(Debug, Decode, Encode, TypeInfo)] pub struct User { pub id: u64, pub name: String, } }
The TypeInfo
derive macro comes from the scale-info
crate. It plays a crucial role in generating type metadata at compile time, which is then embedded into your compiled WebAssembly module.
This metadata serves several important purposes:
-
ABI Generation:
TypeInfo
enables automatic generation of an Application Binary Interface (ABI). This allows external clients (such as frontends, other chain modules, or the Verisense dashboard) to understand the precise structure of types likeUser
, including their field names and data types. -
Cross-language Compatibility: Since the type descriptions are included, tools written in JavaScript, Python, or other languages can introspect your WebAssembly module and correctly encode/decode the data to call your functions.
-
Auto-generated UI: The Verisense dashboard, for example, uses this type metadata to automatically generate input forms. It knows that when it needs a
User
, it should display an input forid: u64
andname: String
.
In short:
TypeInfo
ensures that your data structures are fully described for ABI export, enabling automated tooling, documentation, and seamless cross-language integration.
#[nucleus]
#![allow(unused)] fn main() { #[nucleus] pub mod nucleus { ... } }
The #[nucleus]
attribute macro comes from the vrs-core-sdk
and is fundamental to marking this Rust module as a Nucleus.
Its primary responsibilities are:
-
Export ABI: It collects all functions inside this module that are marked with
#[get]
,#[post]
, or#[init]
and registers them in the Nucleus ABI. This means when your.wasm
is deployed, the blockchain runtime or the Verisense dashboard knows exactly which functions are exposed, along with their input/output types. -
Generate Glue Code: It automatically generates the necessary export functions (such as
_invoke
) that the blockchain’s Wasm executor will call. This abstracts away low-level host bindings, so you only need to focus on your Rust functions. -
Provide a Clean Namespace: By wrapping your interfaces inside the
#[nucleus]
macro, you avoid polluting the global scope. All public functions intended to be callable externally are neatly contained and registered.
In short:
#[nucleus]
transforms your Rust module into a deployable AVS, automatically exporting its ABI and wiring up the execution glue so it can run inside the Verisense runtime.
Add a New User with an ID
#![allow(unused)] fn main() { #[post] pub fn add_user(user: User) -> Result<u64, String> { // Construct the user's key let key = [&b"user:"[..], &user.id.to_be_bytes()[..]].concat(); // Write data to storage storage::put(&key, &user.encode()).map_err(|e| e.to_string())?; Ok(user.id) } }
Retrieve User by ID
#![allow(unused)] fn main() { #[get] pub fn get_user(id: u64) -> Result<Option<User>, String> { // Construct the user's key let key = [&b"user:"[..], &id.to_be_bytes()[..]].concat(); // Retrieve data from storage let result = storage::get(&key).map_err(|e| e.to_string())?; // Decode the data into a User struct let user = result.map(|data| User::decode(&mut &data[..]).unwrap()); Ok(user) } }