Demo: A Decentralized Forum
In this tutorial, I will guide you through the process of building a decentralized forum on Verisense.
To develop a decentralized application (dApp) on Verisense, you'll need to implement four main components:
-
AVS: This is a compiled WASM file created using the
vrs-core-sdk
. The front-end interacts with this component to write data. -
Surrogate: This is a proxy program responsible for syncing the latest data from the AVS and pushing it to MeiliSearch for efficient searching.
-
MeiliSearch: A fast and powerful search engine that will handle data queries from the front-end, enabling a seamless search experience.
-
Front-end app: This is the user-facing interface that allows interaction with the forum.
The AVS will be deployed on Verisense, while the Surrogate and MeiliSearch instances will be deployed on the same server node where Verisense is running.
Before we begin, please ensure that you have all the required tools installed.
AVS
Create an empty project
First, create a new Rust project,
cargo new --lib veavs
Put these into the Cargo.toml
.
[package]
name = "veavs"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
vrs-core-sdk = { git = "https://github.com/verisense-network/verisense.git", package = "vrs-core-sdk" }
parity-scale-codec = { version = "3.6", features = ["derive"] }
vemodel = { path = "../vemodel" }
You can refer to the original file content here.
Define models
For a decentralized forum, we need to define the following models:
use parity_scale_codec::{Decode, Encode};
use serde::{Deserialize, Serialize};
#[derive(Debug, Decode, Encode, Deserialize, Serialize)]
pub enum Method {
Create,
Update,
Delete,
}
#[derive(Debug, Decode, Encode, Deserialize, Serialize)]
pub struct VeSubspace {
pub id: u64,
pub title: String,
pub slug: String,
pub description: String,
pub banner: String,
pub status: i16,
pub weight: i16,
pub created_time: i64,
}
#[derive(Debug, Decode, Encode, Deserialize, Serialize)]
pub struct VeArticle {
pub id: u64,
pub title: String,
pub content: String,
pub author_id: u64,
pub author_nickname: String,
pub subspace_id: u64,
pub ext_link: String,
pub status: i16,
pub weight: i16,
pub created_time: i64,
pub updated_time: i64,
}
#[derive(Debug, Decode, Encode, Deserialize, Serialize)]
pub struct VeComment {
pub id: u64,
pub content: String,
pub author_id: u64,
pub author_nickname: String,
pub post_id: u64,
pub status: i16,
pub weight: i16,
pub created_time: i64,
}
You can refer to the original file content here.
Implement business
We will implement CRUD actions on each model.
// subspace
#[post]
pub fn add_subspace(mut sb: VeSubspace) -> Result<(), String> {
let max_id = get_max_id(PREFIX_SUBSPACE_KEY);
// update the id field from the avs
sb.id = max_id;
let key = build_key(PREFIX_SUBSPACE_KEY, max_id);
storage::put(&key, sb.encode()).map_err(|e| e.to_string())?;
add_to_common_key(Method::Create, key)?;
Ok(())
}
#[post]
pub fn update_subspace(sb: VeSubspace) -> Result<(), String> {
let id = sb.id;
let key = build_key(PREFIX_SUBSPACE_KEY, id);
storage::put(&key, sb.encode()).map_err(|e| e.to_string())?;
add_to_common_key(Method::Update, key)?;
Ok(())
}
#[post]
pub fn delete_subspace(id: u64) -> Result<(), String> {
let key = build_key(PREFIX_SUBSPACE_KEY, id);
storage::del(&key).map_err(|e| e.to_string())?;
add_to_common_key(Method::Delete, key)?;
Ok(())
}
#[get]
pub fn get_subspace(id: u64) -> Result<Option<VeSubspace>, String> {
let key = build_key(PREFIX_SUBSPACE_KEY, id);
let r = storage::get(&key).map_err(|e| e.to_string())?;
let instance = r.map(|d| VeSubspace::decode(&mut &d[..]).unwrap());
Ok(instance)
}
// article
#[post]
pub fn add_article(mut sb: VeArticle) -> Result<(), String> {
let max_id = get_max_id(PREFIX_ARTICLE_KEY);
// update the id field from the avs
sb.id = max_id;
let key = build_key(PREFIX_ARTICLE_KEY, max_id);
storage::put(&key, sb.encode()).map_err(|e| e.to_string())?;
add_to_common_key(Method::Create, key)?;
Ok(())
}
#[post]
pub fn update_article(sb: VeArticle) -> Result<(), String> {
let id = sb.id;
let key = build_key(PREFIX_ARTICLE_KEY, id);
storage::put(&key, sb.encode()).map_err(|e| e.to_string())?;
add_to_common_key(Method::Update, key)?;
Ok(())
}
#[post]
pub fn delete_article(id: u64) -> Result<(), String> {
let key = build_key(PREFIX_ARTICLE_KEY, id);
storage::del(&key).map_err(|e| e.to_string())?;
add_to_common_key(Method::Delete, key)?;
Ok(())
}
#[get]
pub fn get_article(id: u64) -> Result<Option<VeArticle>, String> {
let key = build_key(PREFIX_ARTICLE_KEY, id);
let r = storage::get(&key).map_err(|e| e.to_string())?;
let instance = r.map(|d| VeArticle::decode(&mut &d[..]).unwrap());
Ok(instance)
}
// comment
#[post]
pub fn add_comment(mut sb: VeComment) -> Result<(), String> {
let max_id = get_max_id(PREFIX_COMMENT_KEY);
// update the id field from the avs
sb.id = max_id;
let key = build_key(PREFIX_COMMENT_KEY, max_id);
storage::put(&key, sb.encode()).map_err(|e| e.to_string())?;
add_to_common_key(Method::Create, key)?;
Ok(())
}
#[post]
pub fn update_comment(sb: VeComment) -> Result<(), String> {
let id = sb.id;
let key = build_key(PREFIX_COMMENT_KEY, id);
storage::put(&key, sb.encode()).map_err(|e| e.to_string())?;
add_to_common_key(Method::Update, key)?;
Ok(())
}
#[post]
pub fn delete_comment(id: u64) -> Result<(), String> {
let key = build_key(PREFIX_COMMENT_KEY, id);
storage::del(&key).map_err(|e| e.to_string())?;
add_to_common_key(Method::Delete, key)?;
Ok(())
}
#[get]
pub fn get_comment(id: u64) -> Result<Option<VeComment>, String> {
let key = build_key(PREFIX_COMMENT_KEY, id);
let r = storage::get(&key).map_err(|e| e.to_string())?;
let instance = r.map(|d| VeComment::decode(&mut &d[..]).unwrap());
Ok(instance)
}
You can find the full code here.
Compile to wasm
In the root of this project, run:
cargo build --release --target wasm32-unknown-unknown
You can find the compiled wasm file located at target/wasm32-unknown-unknown/release/veavs.wasm
.
Deploy it to the Verisense
Register a new AVS protocol on Verisense.
vrx create-nucleus --name veavs --capacity 1
This command will return the registered AVS (nucleus) ID like:
Nucleus created.
id: 5FsXfPrUDqq6abYccExCTUxyzjYaaYTr5utLx2wwdBv1m8R8
name: hello_avs
capacity: 1
Deply the generated wasm file to Verisense using the generated Nucleus ID.
vrx deploy --name veavs --wasm-path ../target/wasm32-unknown-unknown/release/veavs.wasm --nucleus-id 5FsXfPrUDqq6abYccExCTUxyzjYaaYTr5utLx2wwdBv1m8R8 --version 1
Wait for the process to complete successfully.
At this point, we have successfully deployed a new AVS onto Verisense.
Surrogate
The AVS functions as a raw database, but to make use of this data, we need to create a proxy that will index the data into the MeiliSearch engine.
You can check out the surrogate implementation here.
The basic concept behind the surrogate is to retrieve data from the AVS and inject it into MeiliSearch for efficient indexing and searching.
MeiliSearch
MeiliSearch provides a standardized approach for handling data queries.
You can find the API documentation here.
Front-end
You can find the reference code for the front-end here.
In the front-end application, the logic involves writing data to the AVS and querying data from MeiliSearch to display the results.
What It Looks Like
For a preview of the app in action, check out this video.