Create a Basic Applet
Generally the life-cycle of a Weilliptic Applet, a WeilChain smart-contract, consists of the following steps:
- Specifying the Applet's interface using WIDL.
- Generate server bindings for the language of your choice using the WIDL compiler.
- Fill in the bindings with business logic.
- Compile the applet into a WASM module and deploy on the WeilChain platform.
- Use the exposed methods of the Applet using either the Weilliptic CLI, or Wallet, or DApp.
In this tutorial we'll implement a basic Counter Applet.
If you prefer a video explanation, Here is a complete end-to-end walkthrough of writing, deploying and interacting smart contracts on WeilChain.
The steps are slightly different depending on the language you are using, so make sure to chose the right language now.
- Rust
- Go
- AssemblyScript
- CPP
You have chosen Rust!
You have chosen Go!
You have chosen AssemblyScript!
You have chosen C++!
Preparations
- Rust
- Go
- AssemblyScript
- C++
wadk
and wadk-utils
will be officially available on crates.io as part of the testnet launch.
This tutorial assumes that
- you have completed the How-to install the WIDL compiler and extension.
- you have installed the tools for the supported language, i.e., Rust, Golang, AssemblyScript, or C++.
To start, create a new project:
- Rust
- Go
- AssemblyScript
- CPP
cargo new counter_rust --lib
cd counter_rust
mkdir counter_go
cd counter_go
go mod init main
go mod tidy
mkdir contract
mkdir counter_as
cd counter_as
npm init
npm install --save-dev assemblyscript
npx asinit .
npm install assemblyscript-json json-as visitor-as --force
After that, add the following to your asconfig.json
.
{
"options": {
"transform": ["json-as/transform"]
}
}
Refer to the json-as
documentation for more information on what this does.
mkdir counterCpp
cd CounterCPP
touch CMakeLists.txt
Prerequisites:
- Add an include folder in the root of your project, which contains all the required headers to work with the C++ SDK.
- Add the statically compiled library code (libweilsdk_static.a) in your project in the lib folder.
- You need to have emscripten installed.
Contract Specification
Create file counter.widl
, with the following contents.
interface Counter {
query func get_count() -> uint;
mutate func increment()
}
This file defines a service called Counter
with two methods, get_count
and increment()
.
Observe that the definition of get_count()
starts with query
but the definition of increment()
starts with mutate
. These indicate if the operation will merely consult the state or can potentially update it.
query
query
methods will not update the state of the Applet. Even if your method does update the state, the change will not be persisted.
mutate
mutate
methods may update the Applet's state. Any changes to the state will be made durable at the end of the execution.
Observe as well that the definition of the last method is not terminated by a ;
.
Server-Side Bindings
Server-side bindings act as a proxy between the host and the Applet.
Server-side bindings generation may be performed manually, but this approach is time-consuming and error prone.
Hence we use the WIDL
compiler to generate the bindings.
- Rust
- Go
- AssemblyScript
- CPP
widl generate counter.widl server rust
A file named bindings.rs
should have been created.
It contains a skeleton for the Applet being developed, annotated with macros that will be expanded to the actual bindings during the compilation.
use serde::{Deserialize, Serialize};
use weil_macros::{constructor, mutate, query, smart_contract, WeilType};
pub trait Counter {
fn new() -> Result<Self, String>
where
Self: Sized;
async fn get_count(&self) -> usize;
async fn increment(&mut self);
}
#[derive(Serialize, Deserialize, WeilType)]
pub struct CounterContractState {
// define your contract state here!
}
#[smart_contract]
impl Counter for CounterContractState {
#[constructor]
fn new() -> Result<Self, String>
where
Self: Sized,
{
unimplemented!();
}
#[query]
async fn get_count(&self) -> usize {
unimplemented!();
}
#[mutate]
async fn increment(&mut self) {
unimplemented!();
}
}
Observe that a trait with the name of the service, Counter
, has been defined and that it defines three methods, two of which match the methods defined in the WIDL file, get_count
and increment
.
The third method is a constructor for the service.
We'll fill in the logic for these methods later. For now, let's compile the contract.
widl generate counter.widl server go
You will notice that a file named contract.go
got created.
It contains a skeleton for the Applet being developed.
package contract
import (
"github.com/weilliptic-inc/wadk/go/weil_go/collections"
"github.com/weilliptic-inc/wadk/go/weil_go/types"
)
type CounterContractState struct {
// implement your contract state here!
}
func NewCounterContractState() (*CounterContractState, error) {
return &CounterContractState {}, nil
}
// query
func (obj *CounterContractState) GetCount() uint32 {
// TODO: implement this!
}
// mutate
func (obj *CounterContractState) Increment() {
// TODO: implement this!
}
Observe that a struct with the name of the service, appended with ContractState
, that is, CounterContractState
, has been defined and that it implements three methods, two of which match the methods defined in the WIDL file, get_count
and increment
(except for the casing).
The third method, NewCounterContractState
is a constructor for the struct.
We'll fill in the logic for these methods later. For now, let's compile the contract.
You will notice that other files were also created:
main.go
exports.go
types.go
Files main.go
and exports.go
contain the actual bindings, which that will call into your implementation of the contract.
File types.go
would contain any types defined in the specification, which are none in this example.
In the assembly
folder create a new file counter.ts
touch assembly/counter.ts
Write a contract state class in this file
@json
export class CounterContractState {
counter: u64
constructor() {
this.counter = 0
}
increment(): void {
this.counter += 1
}
getCount(): u64 {
return this.counter
}
}
@json
decorator above is important if you want to benefit from state serialization and deserialization provided by
json-as
library.
Next, you update assembly/index.ts
to write your code that will interact with runtime as well as use
CounterContractState
methods.
export function init(): void {
}
export function get_count(): void {
}
export function increment(): void {
}
Server-side binding generation is not yet supported. Copy the following file manually for now.
widl generate counter.widl server cpp
A file named bindings.hpp
should have been created.
It contain a skeleton for the Applet being developed.
#include "external/nlohmann.hpp"
//define your counter state
class Counter {
public:
int value;
Counter(int initialValue) {}
Counter() : {}
int getCount() const {
return 0;
}
void increment() {
return;
}
};
// Serialization functions for Counter
inline void to_json(nlohmann::json& j, const Counter& c) {
return;
}
inline void from_json(const nlohmann::json& j, Counter& c) {
return;
}
Notice that a class with the name of the service, Counter
, has been defined; it will hold the state of the applet.
The class defines two methods, which retrieve or manipulate the data inside it, getCount
and increment
; these match the methods in the specification.
The other two methods are default and parameterized constructors for the service.
The to_json and from_json methods are extremely important here. They will be called to serialize and deserialize your contract state. Make sure the logic to convert your state to and from JSON is correctly implemented to not risk losing data while serializing and deserializing.
You will notice that another file, main.cpp
, was also created.
This file contains the stubs that will call into your implementation of the contracts
#include "bindings.hpp"
#include "weilsdk/runtime.h"
#include "external/nlohmann.hpp"
#include "weilsdk/error.h"
#include <map>
extern "C" int __new(size_t len, unsigned char _id) __attribute__((export_name("__new")));
extern "C" void init() __attribute__((export_name("init")));
extern "C" void get_value() __attribute__((export_name("get_value")));
extern "C" void increment() __attribute__((export_name("increment")));
Counter smart_contract_state;
extern "C" {
//export __new
int __new(size_t len, unsigned char _id) {
void* ptr = weilsdk::Runtime::allocate(len);
return reinterpret_cast<int>(ptr); // Return the pointer as an integer to track the memory location
}
//export method_kind_data
void method_kind_data() {
std::map<std::string, std::string> method_kind_mapping;
method_kind_mapping["get_count"]= "query";
method_kind_mapping["increment"]= "mutate";
nlohmann::json json_object = method_kind_mapping;
std::string serialized_string = json_object.dump();
weilsdk::Runtime::setResult(serialized_string,0);
}
//export init
void init() {
nlohmann::json j;
to_json(j,smart_contract_state);
std::string serializedPayload = j.dump();
weilsdk::WeilValue wv;
wv.new_with_state_and_ok_value(serializedPayload, "Ok");
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {wv});
}
void get_count() {
std::string serializedState = weilsdk::Runtime::state();
nlohmann::json j = nlohmann::json::parse(serializedState);
from_json(j,smart_contract_state);
int result = smart_contract_state.getCount();
std::string serialized_result = std::to_string(result);
weilsdk::Runtime::setResult(serialized_result, 0);
}
void increment() {
std::string serializedState = weilsdk::Runtime::state();
nlohmann::json j = nlohmann::json::parse(serializedState);
from_json(j,smart_contract_state);
smart_contract_state.increment();
int incremented_count = smart_contract_state.getCount();
nlohmann::json j1;
to_json(j1,smart_contract_state);
std::string serializedPayload = j1.dump();
weilsdk::WeilValue wv;
wv.new_with_state_and_ok_value(serializedPayload,std::to_string(incremented_count));
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {wv});
}
}
int main(){
return 0;
}
Make sure your handle errors in the implementations of functions "getCount" , "increment", etc. gracefully and deterministically.
You should not use try
, catch
as they may lead to unexpected behaviour due to cross-language compatibility issues.
We suggest using std::pair<int, type>
as return types to check errors at intermediate steps.
Compiling the contract
- Rust
- Go
- AssemblyScript
- CPP
In order to compile the skeleton, first, copy bindings.rs
its contents to file src/lib.rs
.
cat bindings.rs > src/lib.rc
rm bindings.rs
Next, update the Cargo.toml
file to be as follows and include needed dependencies.
[package]
name = "counter"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1.0.219"
serde_json = "1.0.140"
anyhow = "1.0.97"
weil_macros = "0.1" # as a subcrate of wadk on crates.io
weil_rs = "0.1" # as a subcrate of wadk on crates.io
weil_contracts = "0.1" # as a subcrate of wadk on crates.io
wadk-utils = "0.1" # Separate public crate on crates.io
[lib]
crate-type = ["cdylib"]
Finally compile the contract into a WASM module.
cargo build --target wasm32-unknown-unknown --release
You should now have file target/wasm32-unknown-unknown/release/counter.wasm
, which is the body of the Applet.
However, it is not useful in its current state and until we fill in the logic of the methods.
To compile, create folder contract
and move contract.go
, exports.go
and types.go
to the contract
folder.
mkdir contract
mv types.go exports.go contract.go contract
Next download needed dependencies to go.mod
by executing the following commands
go get
While the repository is not publicly available, use the following go.mod
module main
go 1.22.2
require (
github.com/weilliptic-inc/jsonmap
github.com/weilliptic-inc/wadk/go/weil_go
)
replace github.com/weilliptic-inc/wadk/go/weil_go => /root/code/wadk/go/weil_go
Finally try to compile the contract into a WASM module.
mkdir target
mkdir target/wasi
tinygo build -target wasi -o target/wasi/counter_go.wasm
You will see a few error messages, but don't worry. These errors will go away once we fill in the logic of the Applet methods.
npm run asbuild
Add the dependencies to include
folder
Create a CMakeLists.txt file in the root of your project.
touch CMakeLists.txt
Here's a working file.
cmake_minimum_required(VERSION 3.10)
project(counter)
# Specify C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(LIBWEIL_DIR "${CMAKE_SOURCE_DIR}/lib")
include_directories(${CMAKE_SOURCE_DIR}/include)
add_executable(counter src/main.cpp)
target_link_libraries(counter "${LIBWEIL_DIR}/libweilsdk_static.a")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s STANDALONE_WASM --no-entry -O3 -s ERROR_ON_UNDEFINED_SYMBOLS=0")
Finally compile the contract into a WASM module.
mkdir build
cd build
emcmake cmake ..
make
You should now have file build/counter.wasm
, which is the body of the Applet.
However, it is not useful in its current state and until we fill in the logic of the methods.
Filling in the logic
Open contract file and go to the definition of struct/class with the state.
This is struct/class, which will contain the Applet's state, is still empty.
Modify it to include a single field with name count
and integer type.
- Rust
- Go
- AssemblyScript
- CPP
...
#[derive(Serialize, Deserialize, WeilType)]
pub struct CounterContractState {
count: usize;
}
...
...
type CounterContractState struct {
Val uint32 `json:"inner"`
}
...
It is paramount that the fields of the state be named with an initial capital letter, so the state will be properly (de)serialized.
class Counter {
public:
int value;
}
Also fill the (de)serialization methods, which will convert the state to and from JSON.
// Serialization functions for Counter
inline void to_json(nlohmann::json& j, const Counter& c) {
j = nlohmann::json{{"value", c.value}};
}
inline void from_json(const nlohmann::json& j, Counter& c) {
int val = j.at("value");
c.value = val;
}
Now locate the constructor and change its contents as follows, so it sets the initial state of the Applet, when it is deployed.
- Rust
- Go
- AssemblyScript
- CPP
...
#[constructor]
fn new() -> Result<Self, String>
where
Self: Sized,
{
Ok(CounterContractState { counter: 0 })
}
...
func NewCounterContractState() (*CounterContractState, error) {
return &CounterContractState {
Val: 0,
}, nil
}
export function init(): void {
const state = new CounterContractState();
const resultValue = "true";
const weilValue = WeilValue.newWithStateAndOkValue(state, resultValue);
const result = Result.Ok<WeilValue<CounterContractState,string>, WeilError>(weilValue);
Runtime.setStateAndResult(result);
}
Counter(int initialValue) : value(initialValue) {}
Counter() : value(0) {}
Finally, locate methods to get the counter's value and to increment it, and add the corresponding logic :
- Rust
- Go
- AssemblyScript
- CPP
...
#[query]
async fn get_count(&self) -> usize {
self.count
}
#[mutate]
async fn increment(&mut self) {
self.count += 1
}
...
Observe that the query
and mutate
keywords used in the WIDL file became macros in the Rust definition and that the query
method receives a reference to the Applet state, while the mutate
method receives a mutable reference.
// query
func (obj *CounterContractState) GetCount() uint32 {
return obj.Val
}
// mutate
func (obj *CounterContractState) Increment() {
s.Val++
}
Observe that the query
and mutate
keywords used in the WIDL file are mere comments here, that only help you identify the methods that implement the WIDL service.
export function get_count(): void {
const state = Runtime.state<CounterContractState>()
const result = state.getCount()
Runtime.setOkResult(result)
}
export function increment(): void {
const state = Runtime.state<CounterContractState>()
state.increment()
const resultValue = "ok"
const weilvalue = WeilValue.newWithStateAndOkValue(state, resultValue)
const result = Result.Ok<WeilValue<CounterContractState,string>, WeilError>(weilvalue);
Runtime.setStateAndResult(result);
}
int getCount() const {
return value;
}
void increment() {
value += 1;
}
Now compile the Applet again to ensure that your file is correct.
- Rust
- Go
- AssemblyScript
- CPP
cargo build --target wasm32-unknown-unknown --release
Edit contract/contract.go
and remove the import clauses, so it looks exactly like the following.
package contract
type CounterContractState struct {
Val uint32 `json:"inner"`
}
func NewCounterContractState() (*CounterContractState, error) {
return &CounterContractState {
Val: 0,
}, nil
}
// query
func (obj *CounterContractState) GetCount() uint32 {
return obj.Val
}
// mutate
func (obj *CounterContractState) Increment() {
obj.Val++
}
Edit contract/types.go
and remove the import clauses, so it looks exactly like the following.
package contract
tinygo build -target wasi -o target/wasi/counter_go.wasm
npx run asbuld
make
Next steps
Congratulations! You should have your Applet ready to be deployed, which you can do by following the tutorial Deploy and use a WeilChain Applet.