Skip to main content

smart-contract-sdk

Proc Macros

  1. smart_contract: This macro is used to annotate the impl block of generated trait implementation by the contract state. This tell the SDK to capture query and mutate inside the impl block and convert them into WASM exported functions which can be invoked externally.
  2. 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.
  3. 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.

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:

WeilVec<T>
WeilMap<K, V>
WeilSet<T>
WeilTrieMap<T>
WeilMemory

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.

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>;

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.

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;

WeilSet<T>: equals WeilMap<T, ()>

fn insert(&mut self, value: T);

fn contains<Q>(&self, value: &Q) -> bool
where
T: Borrow<Q>,
Q: Hash + Eq + Serialize;

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>>;

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.

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;
Caution!

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

// 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>);

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

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);

Ledger

The SDK provides following ledger specific bounded methods on struct Ledger

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<()>;

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.