smart-contract-sdk
- Rust
- Go
- AssemblyScript
- CPP
Proc Macros
- smart_contract: This macro is used to annotate the
impl
block of generated trait implementation by the contract state. This tell the SDK to capturequery
andmutate
inside theimpl
block and convert them into WASM exported functions which can be invoked externally. - constructor: This macro tell the SDK to call the method at the time of deployment. Usually this method is used to define default initial contract state.
- query and mutate: These macros are for annotating the methods which tell the SDK to insert appropriate runtime specific persistence wrappers around the exported methods for example: for mutate annotated methods, there are possible changes to the contract state which should be persisted back to the storage.
So a complete counter contract would look something like: A complete counter smart contract would look like:
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 set_counter(&mut self, val: usize);
async fn increment(&mut self);
async fn decrement(&mut self);
async fn reset(&mut self);
}
#[derive(Serialize, Deserialize, WeilType)]
pub struct CounterContractState {
counter: usize
}
#[smart_contract]
impl Counter for CounterContractState {
#[constructor]
fn new() -> Result<Self, String>
where
Self: Sized,
{
Ok(CounterContractState { counter: 0 })
}
#[query]
async fn get_count(&self) -> usize {
self.counter
}
#[mutate]
async fn set_counter(&mut self, val: usize) {
self.counter = val;
}
#[mutate]
async fn increment(&mut self) {
self.counter += 1;
}
#[mutate]
async fn decrement(&mut self) {
self.counter -= 1;
}
#[mutate]
async fn reset(&mut self) {
self.counter = 0;
}
}
For most part of the smart contract development, it is sufficient to have the mental model of a struct based contract state and exported functions as its bounded methods. One has to remember that any mutations in contract state survives across multiple method execution. This mental model is almost correct and simple enough to let developers focus on business logic rather than worry about runtime specific details.
TODO
TODO
A simple smart contract using the C++ SDK would look something like this
#include "external/nlohmann.hpp"
class Counter {
public:
int value;
Counter(int initialValue) : value(initialValue) {}
Counter() : value(0) {}
int getValue() const {
return value;
}
void increment() {
value += 1;
}
void decrement() {
if (value > 0) {
value -= 1;
}
}
void setValue(int newValue) {
value = newValue;
}
};
// 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;
}
Collections
As noted in the Collections, collections are used to keep contract state light-weight so that it can be serialized-deserialized quickly and fit conveniently inside the WASM memory. Following are the SDK provided Weil-Collections
:
- Rust
- Go
- AssemblyScript
- CPP
WeilVec<T>
WeilMap<K, V>
WeilSet<T>
WeilTrieMap<T>
WeilMemory
TODO
TODO
WeilVec<T>
WeilMap<K, V>
General Structure
The general structure of all the above collections is that they just have a state_id
with type WeilId
which points to the actual persistently stored collection items. All the collections are implemented using K-V model where state_id + serialized(K) -> serialized(V)
. This makes the serialized version of a collection object light-weight consisting only the state_id
and when get
or set
is called on that collection object, it forms the above key and gets the value or sets the value from storage. So whenever a collection type is part of contract state, it justs contains the state_id
hence making the overall contract state light-weight in memory and lazy loads the specific value for the requested key in get
or set
queries.
Description
WeilVec<T>
: A growable array type. The values are sharded in memory and can be used for iterable and indexable values that are dynamically sized.
- Rust
- Go
- AssemblyScript
- CPP
fn push(&mut self, item: T);
fn get(&self, index: usize) -> Option<T>;
fn set(&mut self, index: usize, item: T) -> Result<(), IndexOutOfBoundsError>;
fn pop(&mut self) -> Option<T>;
fn iter(&self) -> WeilVecIter<T>;
TODO
TODO
int size(); //returns the length of the vector
void push(const T &item); //inserts an element to the end of the list
T get(int idx); //returns the element at the index 'idx'
void set(int idx, const T &item); //sets the value at the 'idx' index to 'item'.
void pop(); //removes the last element from the list
WeilMap<K, V>
: This structure behaves as a thin wrapper around the key-value storage available to contracts. This structure does not contain any metadata about the elements in the map, so it is not iterable.
- Rust
- Go
- AssemblyScript
- CPP
fn insert(&mut self, key: K, val: V);
fn get<Q>(&self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + Serialize;
fn remove<Q>(&self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + Serialize;
TODO
TODO
void insert(const K &key, const V &value); //adds the key, value pair to the map
bool contains(const K &key); //return whether the key was present in the map
V get(const K &key); //returns the value for the given key. If the key is not present, returns a default value of V
V remove(const K &key); //removes the key from the map
WeilSet<T>
: equals WeilMap<T, ()>
- Rust
- Go
- AssemblyScript
- CPP
fn insert(&mut self, value: T);
fn contains<Q>(&self, value: &Q) -> bool
where
T: Borrow<Q>,
Q: Hash + Eq + Serialize;
TODO
TODO
TODO
- Rust
- Go
- AssemblyScript
- CPP
WeilTrieMap<T>
: A string-keyed map like collection which supports efficient prefix queries along with standard get
and set
queries of WeilMap<K, V>
fn insert(&mut self, key: String, val: T);
fn get(&self, key: &str) -> Option<T>;
fn remove(&self, key: &str) -> Option<T>;
fn get_with_prefix(&self, prefix: &str) -> Option<WeilTriePrefixMap<T>>;
TODO
TODO
TODO
WeilMemory
(WeilVec<Chunk>
): A Vec<u8>
like contiguous array which internally divides into 64KB
chunks. This is more optimized than WeilVec<T>
for arbitrary offset reads and writes. In WeilVec<T>
reads and writes happen per element basis whereas WeilMemory
reads and writes happened per chunk (64KB) basis thus balancing the performance and in-memory data for better extended offset reads and writes.
- Rust
- Go
- AssemblyScript
- CPP
fn read(&self, offset: usize, dst: &mut [u8]);
fn write(&mut self, offset: usize, src: &[u8]);
fn grow(&mut self, num_chunks: u32);
fn size(&self) -> usize;
TODO
TODO
TODO
One should never ever use the same WeilId
for two different collections even if the previous one is deleted! Using same WeilId
is an undefined behavior and can have catastrophic effects on the contract state.
General Instructions
Generally Weil-Collections
are used in the outer most contract state as that's where it spans over the scale of data that might not fit in memory. The inner attributes can use standard collections. This balances the trade-off between space and performance well. For example a smart contract might have a map containing key to be wallet address and value to be another map containing tokens vs balance data. Following are the ways one can implement such data structure
- Rust
- Go
- AssemblyScript
- CPP
// Both outer and inner map is using `WeilMap<K, V>`
struct ContractState {
balances: WeilMap<Address, TokenBalances>
}
struct TokenBalances(WeilMap<Token, uint>);
// Outer map using `WeilMap<K, V>` and inner map using `BTreeMap<K, V>`
struct ContractState {
balances: WeilMap<Address, TokenBalances>
}
struct TokenBalances(BTreeMap<Token, uint>);
TODO
TODO
// Both outer and inner map is using `WeilMap<K, V>`
struct ContractState {
collections::WeilMap<Address, TokenBalances> balances;
};
struct TokenBalances{
collections::WeilMap<Token, int> mp;
};
// Outer map using `WeilMap<K, V>` and inner map using `map<K, V>`
struct ContractState {
collections::WeilMap<Address, TokenBalances> balances;
};
struct TokenBalances{
std::map<Token, int> mp;
};
Both are correct! but the second one optimizes the trade-off between in-memory and performance. You need to remember that each lazy get
or set
operation on a Weil-Collection
is potentially a call to persistent storage, while a standard collection is loaded all at ones in memory. So the outer map can be Weil-Collection
which might scale with the number of wallets the blockchain platform is hosting which could be potentially in millions or billions however the inner map can be stored as standard BTreeMap
as it just stores all the tokens owned by that wallet which might be few hundreds or thousands at max.
So by careful inspection about the scale various attributes can attain inside contract state, we can implement quite efficient collection based data-structures.
Runtime
The SDK provides following runtime specific bounded methods on struct Runtime
- Rust
- Go
- AssemblyScript
- CPP
fn get_contract_id() -> String;
fn get_ledger_contract_id() -> String;
fn get_sender() -> String;
fn call_contract<R: DeserializeOwned>(
contract_id: String,
method_name: String,
method_args: Option<String>,
) -> anyhow::Result<R>;
fn debug_log(log: &str);
TODO
TODO
std::string contractId();
std::string ledgerContractId();
std::string sender();
std::pair<int,std::string> callContract(const std::string contractId,
const std::string methodName,
const std::string methodArgs);
void debugLog(std::string log);
Ledger
The SDK provides following ledger specific bounded methods on struct Ledger
- Rust
- Go
- AssemblyScript
- CPP
fn balance_for(addr: String, symbol: String) -> Result<usize>;
fn balances_for(addr: String) -> Result<WeilTriePrefixMap<usize>>;
fn transfer(
symbol: String,
from_addr: String,
to_addr: String,
amount: usize,
) -> Result<()>;
TODO
TODO
int balanceFor(std::string addr, std::string symbol);
std::pair<bool, std::string> transfer(
std::string symbol,
std::string fromAddr,
std::string toAddr,
int amount);
DApp Development: WIDL Client Bindings
The above sections described how to implement a smart contract from the server bindings generated by the WIDL compiler. One can also generate client side bindings which then can be used along side weil-wallet-sdk
js library to write a DApp which interacts with the smart contract.