Creating a Non-Fungible Token Contract
In this tutorial we'll implement a Non-Fungible Token.
To know what a non-fungible (and a fungible) token is, we recommend you to checkout Weil Tokens
The steps are slightly different depending on the language you are using, so make sure go 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
This tutorial assumes that you have completed the Cross-Contract tutorial.
To start, create a new project. We'll name our example token AsciiArt, for it will hold a collection of some simple ASCII drawings.
- Rust
- Go
- AssemblyScript
- CPP
cargo new asciiart --lib
cd asciiart
mkdir asciiart
cd asciiart
go mod init main
go mod tidy
mkdir contract
mkdir asciiart
cd asciiart
npm init
npm install --save-dev assemblyscript
npx asinit .
npm install assemblyscript-json json-as visitor-as --force
mkdir asciiart
cd asciiart
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
The token is defined by the following WIDL interface.
record TokenDetails {
title: string,
name: string,
description: string,
payload: string
}
interface AsciiArt {
query func name() -> string;
query func balance_of(addr: string) -> uint;
query func owner_of(token_id: string) -> result<string, string>;
query func details(token_id: string) -> result<TokenDetails, string>;
mutate func approve(spender: string, token_id: string) -> result<(), string>;
mutate func set_approve_for_all(spender: string, approval: bool);
mutate func transfer(to_addr: string, token_id: string) -> result<(), string>;
mutate func transfer_from(from_addr: string, to_addr: string, token_id: string) -> result<(), string>;
query func get_approved(token_id: string) -> result<list<string>, string>;
query func is_approved_for_all(owner: string, spender: string) -> bool;
mutate func mint(
token_id: string,
title: string,
name: string,
description: string,
payload: string
) -> result<(), string>
}
This is is the minimal interface any Non-Fungible Token must implement in a WeilChain
to be compatible with our Wallet.
You could extend it for your own purposes, though, but we will not do this in this tutorial.
Server-Side Bindings
As usual, we need to create the server-side bindings and the contract skeleton.
- Rust
- Go
- AssemblyScript
- CPP
widl generate asciiart.widl server rust
This will have generated the bindings.rs
file, with the Applet skeleton.
Let's fill in the skeleton with the contract logic.
First, lets move the file to its final location.
cat bindings.rs > src/lib.rs
rm bindings.rs
widl generate asciiart.widl server go
This will have generated the contract.go
file, with the Applet skeleton.
Let's fill in the skeleton with the contract logic.
First, lets move the file to its final location.
mkdir contract
mv types.go exports.go contract.go contract
AssemblyScript SDK doesn't support binding generation yet. Please copy the file manually for now.
Create the file assembly/asciiart.ts
with the following content
@json
export class AsciiArtContractState {
inner: NonFungibleToken
controllers: WeilMap<string, Box<boolean>>
constructor(
controllers: WeilMap<string, Box<boolean>>,
inner: NonFungibleToken,
) {
this.controllers = controllers
this.inner = inner
}
}
Update assembly/index.ts
like this
export function init(): void {}
export function name(): void {}
export function balance_of(): void {}
export function owner_of(): void {}
export function details(): void {}
export function approve(): void {}
export function set_approve_for_all(): void {}
export function transfer(): void {}
export function transfer_from(): void {}
export function get_approved(): void {}
export function is_approved_for_all(): void {}
export function mint(): void {}
export function yo(a: string): void {}
export function method_kind_data(): void {
const result: Map<string, string> = new Map<string, string>()
result.set('name', 'query')
result.set('balance_of', 'query')
result.set('owner_of', 'query')
result.set('details', 'query')
result.set('approve', 'mutate')
result.set('set_approve_for_all', 'mutate')
result.set('transfer', 'mutate')
result.set('transfer_from', 'mutate')
result.set('get_approved', 'query')
result.set('is_approved_for_all', 'query')
result.set('mint', 'mutate')
Runtime.setOkResult(result)
}
export function method_kind(): void {
method_kind_data()
}
C++ SDK doesn't support binding generation yet. Please copy the file manually for now.
This file contains the Applet skeleton. Let's fill in the skeleton with the contract logic.
extern "C" {
int __new(size_t len, unsigned char _id) {
void *ptr = weilsdk::Runtime::allocate(len);
return reinterpret_cast<int>(ptr);
}
void method_kind_data() {
std::map<std::string, std::string> method_kind_mapping;
// method_kind_mapping["new"] = "mutate";
method_kind_mapping["name"]= "query";
method_kind_mapping["balance_of"]= "query";
method_kind_mapping["is_controller"]= "query";
method_kind_mapping["owner_of"]= "query";
method_kind_mapping["details"]= "query";
method_kind_mapping["approve"]= "mutate";
method_kind_mapping["set_approve_for_all"]= "mutate";
method_kind_mapping["transfer"]= "mutate";
method_kind_mapping["transfer_from"]= "mutate";
method_kind_mapping["get_approved"]= "query";
method_kind_mapping["is_approved_for_all"]= "query";
method_kind_mapping["mint"]= "mutate";
nlohmann::json json_object = method_kind_mapping;
std::string serialized_string = json_object.dump();
weilsdk::Runtime::setResult(serialized_string,0);
}
weilcontracts::AsciiArtContractState ascii_instance;
void init(){
weilcontracts::AsciiArtContractState ascii_instance("AsciiArt");
// Add the contract creator as a controller
std::string creator = weilsdk::Runtime::sender();
collections::WeilMap<std::string, bool> controllers(0);
controllers.insert(creator,true);
// Mint initial tokens
std::vector<std::pair<std::string, weilcontracts::Token>> initial_tokens = {
{"1", weilcontracts::Token("A fish going left!", "fish 1", "A one line ASCII drawing of a fish", "<><")},
{"2", weilcontracts::Token("A fish going right!", "fish 2", "A one line ASCII drawing of a fish swimming to the right", "><>")},
{"3", weilcontracts::Token("A big fish going left!", "fish 3", "A one line ASCII drawing of a fish swimming to the left", "<'))><")},
{"4", weilcontracts::Token("A big fish going right!", "fish 4", "A one line ASCII drawing of a fish swimming to the right", "><(('>")},
{"5", weilcontracts::Token("A Face", "face 1", "A one line ASCII drawing of a face", "(-_-)")},
{"6", weilcontracts::Token("Arms raised", "arms 1", "A one line ASCII drawing of a person with arms raised", "\\o/")}
};
ascii_instance.setControllers(controllers);
for (const auto& [id, token] : initial_tokens) {
auto result = ascii_instance.inner.mint(id, token);
if(result.first){
std::string error = result.second;
weilsdk::MethodError me = weilsdk::MethodError("init",error);
weilsdk::Runtime::setStateAndResult(weilsdk::WeilError::FunctionReturnedWithError(me));
return;
}
}
nlohmann::json j;
to_json(j,ascii_instance);
std::string serialized_state = j.dump();
weilsdk::WeilValue wv;
wv.new_with_state_and_ok_value(serialized_state, "Ok");
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {wv});
}
struct isControllerArgs{
std::string addr;
};
inline void to_json_0(nlohmann::json &j, const isControllerArgs &k)
{
j = nlohmann::json{{"addr", k.addr}};
}
inline void from_json_0(const nlohmann::json &j, isControllerArgs &k)
{
j.at("addr").get_to(k.addr);
}
void is_controller(){
isControllerArgs args;
std::string raw_args = weilsdk::Runtime::args();
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("addr"))
{
weilsdk::MethodError me = weilsdk::MethodError("is_controller", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 0);
return;
}
from_json_0(j,args);
std::string stateString = weilsdk::Runtime::state();
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
bool result = ascii_instance.is_controller(args.addr);
if(result){
weilsdk::Runtime::setResult("True", 0);
}
else{
weilsdk::Runtime::setResult("False", 0);
}
}
void name(){
std::string stateString = weilsdk::Runtime::state();
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
std::string name = ascii_instance.name();
weilsdk::Runtime::setResult(name,0);
}
struct balanceOfArgs{
std::string addr;
};
inline void to_json_1(nlohmann::json &j, const balanceOfArgs &k)
{
j = nlohmann::json{{"addr", k.addr}};
}
inline void from_json_1(const nlohmann::json &j, balanceOfArgs &k)
{
j.at("addr").get_to(k.addr);
}
void balance_of(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("addr"))
{
weilsdk::MethodError me = weilsdk::MethodError("balance_of", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 1);
return;
}
balanceOfArgs args;
from_json_1(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
int result = ascii_instance.balance_of(args.addr);
weilsdk::Runtime::setResult(std::to_string(result),0);
}
struct ownerOfArgs{
std::string token_id;
};
inline void to_json_2(nlohmann::json &j, const ownerOfArgs &k)
{
j = nlohmann::json{{"token_id", k.token_id}};
}
inline void from_json_2(const nlohmann::json &j, ownerOfArgs &k)
{
j.at("token_id").get_to(k.token_id);
}
void owner_of(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("token_id"))
{
weilsdk::MethodError me = weilsdk::MethodError("owner_of", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 1);
return;
}
ownerOfArgs args;
from_json_2(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
std::string result = ascii_instance.owner_of(args.token_id);
weilsdk::Runtime::setResult(result,0);
}
struct detailsArgs{
std::string token_id;
};
inline void to_json_3(nlohmann::json &j, const detailsArgs &k)
{
j = nlohmann::json{{"token_id", k.token_id}};
}
inline void from_json_3(const nlohmann::json &j, detailsArgs &k)
{
j.at("token_id").get_to(k.token_id);
}
void details(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("token_id"))
{
weilsdk::MethodError me = weilsdk::MethodError("details", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 0);
return;
}
detailsArgs args;
from_json_3(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
std::variant<weilcontracts::TokenDetails, std::string> result = ascii_instance.details(args.token_id);
if (std::holds_alternative<std::string>(result)) {
std::string error = std::get<std::string>(result);
weilsdk::MethodError me = weilsdk::MethodError("details",error);
weilsdk::Runtime::setResult(weilsdk::WeilError::FunctionReturnedWithError(me),1);
}
else if (std::holds_alternative<weilcontracts::TokenDetails>(result)) {
weilcontracts::TokenDetails token_details = std::get<weilcontracts::TokenDetails>(result);
nlohmann::json j1;
to_json(j1,token_details);
std::string serialized_result = j1.dump();
weilsdk::Runtime::setResult(serialized_result,0);
}
}
struct approveArgs{
std::string spender;
std::string token_id;
};
inline void to_json_4(nlohmann::json &j, const approveArgs &k)
{
j = nlohmann::json{{"spender", k.spender},{"token_id", k.token_id}};
}
inline void from_json_4(const nlohmann::json &j, approveArgs &k)
{
j.at("spender").get_to(k.spender);
j.at("token_id").get_to(k.token_id);
}
void approve(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("spender") || !j.contains("token_id"))
{
weilsdk::MethodError me = weilsdk::MethodError("approve", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 1);
return;
}
approveArgs args;
from_json_4(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
ascii_instance.approve(args.spender, args.token_id);
nlohmann::json j2;
to_json(j2,ascii_instance);
weilsdk::WeilValue wv;
wv.new_with_state_and_ok_value(j2.dump(), "Ok");
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {wv});
}
struct approveAllArgs{
std::string spender;
bool approval;
};
inline void to_json_5(nlohmann::json &j, const approveAllArgs &k)
{
j = nlohmann::json{{"spender", k.spender},{"approval", k.approval}};
}
inline void from_json_5(const nlohmann::json &j, approveAllArgs &k)
{
j.at("spender").get_to(k.spender);
j.at("approval").get_to(k.approval);
}
void set_approve_for_all(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("spender") || !j.contains("approval"))
{
weilsdk::MethodError me = weilsdk::MethodError("set_approve_for_all", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 1);
return;
}
approveAllArgs args;
from_json_5(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
ascii_instance.set_approve_for_all(args.spender, args.approval);
nlohmann::json j2;
to_json(j2,ascii_instance);
weilsdk::WeilValue wv;
wv.new_with_state_and_ok_value(j2.dump(), "Ok");
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {wv});
}
struct transferArgs{
std::string to_addr;
std::string token_id;
};
inline void to_json_6(nlohmann::json &j, const transferArgs &k)
{
j = nlohmann::json{{"to_addr", k.to_addr},{"token_id", k.token_id}};
}
inline void from_json_6(const nlohmann::json &j, transferArgs &k)
{
j.at("to_addr").get_to(k.to_addr);
j.at("token_id").get_to(k.token_id);
}
void transfer(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("to_addr") || !j.contains("token_id"))
{
weilsdk::MethodError me = weilsdk::MethodError("transfer", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 1);
return;
}
transferArgs args;
from_json_6(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
//error, result
auto result = ascii_instance.transfer(args.to_addr, args.token_id).first;
if(!result){
nlohmann::json j2;
to_json(j2,ascii_instance);
weilsdk::WeilValue wv;
wv.new_with_state_and_ok_value(j2.dump(), "Ok");
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {wv});
}
else{
weilsdk::MethodError me = weilsdk::MethodError("transfer","could not transfer");
std::string err = weilsdk::WeilError::FunctionReturnedWithError(me);
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {err});
}
}
struct transferFromArgs{
std::string from_addr;
std::string to_addr;
std::string token_id;
};
inline void to_json_7(nlohmann::json &j, const transferFromArgs &k)
{
j = nlohmann::json{{"from_addr", k.from_addr},{"to_addr", k.to_addr},{"token_id", k.token_id}};
}
inline void from_json_7(const nlohmann::json &j, transferFromArgs &k)
{
j.at("from_addr").get_to(k.from_addr);
j.at("to_addr").get_to(k.to_addr);
j.at("token_id").get_to(k.token_id);
}
void transfer_from(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("from_addr") || !j.contains("to_addr") || !j.contains("token_id"))
{
weilsdk::MethodError me = weilsdk::MethodError("transfer_from", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 0);
return;
}
transferFromArgs args;
from_json_7(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
bool result = ascii_instance.transfer_from(args.from_addr, args.to_addr, args.token_id).first;
if(result){
nlohmann::json j2;
to_json(j2,ascii_instance);
weilsdk::WeilValue wv;
wv.new_with_state_and_ok_value(j2.dump(), "Ok");
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {wv});
}
else{
weilsdk::MethodError me = weilsdk::MethodError("transfer_from","could not transfer_from");
std::string err = weilsdk::WeilError::FunctionReturnedWithError(me);
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {err});
}
}
struct getApprovedArgs{
std::string token_id;
};
inline void to_json_8(nlohmann::json &j, const getApprovedArgs &k)
{
j = nlohmann::json{{"token_id", k.token_id}};
}
inline void from_json_8(const nlohmann::json &j, getApprovedArgs &k)
{
j.at("token_id").get_to(k.token_id);
}
void get_approved(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("token_id"))
{
weilsdk::MethodError me = weilsdk::MethodError("get_approved", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 1);
return;
}
getApprovedArgs args;
from_json_8(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
std::pair<bool, std::variant<std::string, std::vector<std::string>>> approval = ascii_instance.get_approved(args.token_id);
std::variant<std::string, std::vector<std::string>> result = approval.second;
if(approval.first){
std::string error = std::get<std::string>(result);
weilsdk::MethodError me = weilsdk::MethodError("get_approved",error);
weilsdk::Runtime::setResult(weilsdk::WeilError::FunctionReturnedWithError(me),1);
}
else{
std::vector<std::string> approved = std::get< std::vector<std::string>>(result);
nlohmann::json j2= approved;
weilsdk::Runtime::setResult(j2.dump(),0);
}
}
struct isApprovedAllArgs{
std::string owner;
std::string spender;
};
inline void to_json_9(nlohmann::json &j, const isApprovedAllArgs &k)
{
j = nlohmann::json{{"owner", k.owner}, {"spender", k.spender}};
}
inline void from_json_9(const nlohmann::json &j, isApprovedAllArgs &k)
{
j.at("owner").get_to(k.owner);
j.at("spender").get_to(k.spender);
}
void is_approved_for_all(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("owner") || !j.contains("spender"))
{
weilsdk::MethodError me = weilsdk::MethodError("is_approved_for_all", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 1);
return;
}
isApprovedAllArgs args;
from_json_9(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
bool result = ascii_instance.is_approved_for_all(args.owner, args.spender);
weilsdk::Runtime::setResult(std::to_string(result),0);
}
struct mintArgs{
std::string token_id;
std::string title;
std::string name;
std::string description;
std::string payload;
};
inline void to_json_10(nlohmann::json &j, const mintArgs &k)
{
j = nlohmann::json{{"token_id", k.token_id}, {"title", k.title}, {"name", k.name}, {"description", k.description},{"payload", k.payload}};
}
inline void from_json_10(const nlohmann::json &j, mintArgs &k)
{
j.at("token_id").get_to(k.token_id);
j.at("title").get_to(k.title);
j.at("name").get_to(k.name);
j.at("description").get_to(k.description);
j.at("payload").get_to(k.payload);
}
void mint(){
std::pair<std::string, std::string> p = weilsdk::Runtime::stateAndArgs();
std::string raw_args = p.second;
nlohmann::json j = nlohmann::json::parse(raw_args);
if (j.is_discarded() || !j.contains("token_id") || !j.contains("title") || !j.contains("name") || !j.contains("description") || !j.contains("payload"))
{
weilsdk::MethodError me = weilsdk::MethodError("mint", "invalid_args");
weilsdk::Runtime::setResult(weilsdk::WeilError::MethodArgumentDeserializationError(me), 1);
return;
}
mintArgs args;
from_json_10(j,args);
std::string stateString = p.first;
nlohmann::json j1 = nlohmann::json::parse(stateString);
from_json(j1,ascii_instance);
std::pair<int,std::string> result = ascii_instance.mint(args.token_id, args.title, args.name, args.description, args.payload);
nlohmann::json j2;
to_json(j2,ascii_instance);
if(result.first){
std::string error = result.second;
weilsdk::MethodError me = weilsdk::MethodError("mint",error);
std::string err = weilsdk::WeilError::FunctionReturnedWithError(me);
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {err});
}
else{
weilsdk::WeilValue wv;
wv.new_with_state_and_ok_value(j2.dump(), "Ok");
weilsdk::Runtime::setStateAndResult(std::variant<weilsdk::WeilValue,weilsdk::WeilError> {wv});
}
}
}
Filling in the Logic
Contract State
The AsciiArt contract has an inner of type Non-Fungible token, to which most operations will be delegated. It also contains a set of controllers, which are allowed to mint new tokens.
- Rust
- Go
- AssemblyScript
- CPP
#[derive(Serialize, Deserialize, WeilType)]
pub struct AsciiArtContractState {
/// Controllers allowed to mint new tokens.
/// TODO: How is the set updated?
controllers: WeilMap<String, ()>,
inner: NonFungibleToken,
}
type AsciiArtContractState struct {
/// Controllers allowed to mint new tokens.
/// TODO: How is the set updated?
Controllers collections.WeilSet[string]
Inner non_fungible.NonFungibleToken
}
export function init(): void {
const controllers = new WeilMap<string, Box<boolean>>(new WeilId(0))
controllers.set(Runtime.sender(), new Box(true))
const contract = new AsciiArtContractState(
controllers,
new NonFungibleToken('AsciiArt'),
)
...
}
namespace weilcontracts {
class AsciiArtContractState {
public:
collections::WeilMap<std::string, bool> controllers;
NonFungibleToken inner;
AsciiArtContractState(std::string _name) : inner(_name) {}
AsciiArtContractState() : inner("") {}
}
}
Contract Initialization
The constructor for the AsciiArt executes three steps:
- Initialize the inner Non-Fungible token, to which most operation will be delegated. Initializing it requires just 1 parameter, the name for the NFT, for which we will use "AsciiArt".
- Initialize the controllers set with the address of the account being used to deploy this contract, retrieved using the
sender()
function. - Create an initial set of tokens.
- Rust
- Go
- AssemblyScript
- CPP
#[constructor]
fn new() -> Result<Self, String>
where
Self: Sized,
{
let creator: String = Runtime::sender();
let mut controllers = WeilMap::new(WeilId(0));
controllers.insert(creator.clone(), ());
let mut token = AsciiArtContractState {
controllers,
inner: NonFungibleToken::new("AsciiArt".to_string()),
};
// This contract mints some tokens at the start, but others might mint later.
let initial_tokens = vec![
(
"0",
Token::new(
"A fish going left!".to_string(),
"fish 1".to_string(),
"A one line ASCII drawing of a fish".to_string(),
"<><".to_string(),
),
),
(
"1",
Token::new(
"A fish going right!".to_string(),
"fish 2".to_string(),
"A one line ASCII drawing of a fish swimming to the right".to_string(),
"><>".to_string(),
),
),
(
"2",
Token::new(
"A big fish going left!".to_string(),
"fish 3".to_string(),
"A one line ASCII drawing of a fish swimming to the left".to_string(),
"<'))><".to_string(),
),
),
(
"3",
Token::new(
"A big fish going right!".to_string(),
"fish 4".to_string(),
"A one line ASCII drawing of a fish swimming to the right".to_string(),
"><(('>".to_string(),
),
),
(
"4",
Token::new(
"A Face".to_string(),
"face 1".to_string(),
"A one line ASCII drawing of a face".to_string(),
"(-_-)".to_string(),
),
),
(
"5",
Token::new(
"Arms raised".to_string(),
"arms 1".to_string(),
"A one line ASCII drawing of a person with arms raised".to_string(),
"\\o/".to_string(),
),
),
];
for (i, t) in initial_tokens {
token
.inner
.mint(i.to_string(), t)
.map_err(|err| err.to_string())?;
}
Ok(token)
}
func NewAsciiArtContractState() (*AsciiArtContractState, error) {
creator := runtime.Sender()
controllers := *collections.NewWeilSet[string](*collections.NewWeilId(0))
controllers.Insert(&creator)
token := AsciiArtContractState{
Controllers: controllers,
Inner: *non_fungible.NewNonFungibleToken("AsciiArt"),
}
initialTokens := [6]non_fungible.Token{
{
Title: "A fish going left!",
Name: "fish 1",
Description: "A one line ASCII drawing of a fish",
Payload: "<><",
},
{
Title: "A fish going right!",
Name: "fish 2",
Description: "A one line ASCII drawing of a fish swimming to the right",
Payload: "><>",
},
{
Title: "A big fish going left!",
Name: "fish 3",
Description: "A one line ASCII drawing of a fish swimming to the left",
Payload: "<'))><",
},
{
Title: "A big fish going right!",
Name: "fish 4",
Description: "A one line ASCII drawing of a fish swimming to the right",
Payload: "><(('>",
},
{
Title: "A Face",
Name: "face 1",
Description: "A one line ASCII drawing of a face",
Payload: "(-_-)",
},
{
Title: "Arms raised",
Name: "arms 1",
Description: "A one line ASCII drawing of a person with arms raised",
Payload: "\\o/",
},
}
for i, t := range initialTokens {
err := token.Inner.Mint(strconv.Itoa(i), t)
if err != nil {
return nil, err
}
}
return &token, nil
}
export function init(): void {
const controllers = new WeilMap<string, Box<boolean>>(new WeilId(0))
controllers.set(Runtime.sender(), new Box(true))
const contract = new AsciiArtContractState(
controllers,
new NonFungibleToken('AsciiArt'),
)
const initialTokens: Array<Token> = [
new Token(
'A fish going left!',
'fish 1',
'A one line ASCII drawing of a fish',
'<><',
),
new Token(
'A fish going right!',
'fish 2',
'A one line ASCII drawing of a fish swimming to the right',
'><>',
),
new Token(
'A big fish going left!',
'fish 3',
'A one line ASCII drawing of a fish swimming to the left',
"<'))><",
),
new Token(
'A big fish going right!',
'fish 4',
'A one line ASCII drawing of a fish swimming to the right',
"><(('>",
),
new Token(
'A Face',
'face 1',
'A one line ASCII drawing of a face',
'(-_-)',
),
new Token(
'Arms raised',
'arms 1',
'A one line ASCII drawing of a person with arms raised',
'\\o/',
),
]
for (let index = 0; index < initialTokens.length; index += 1) {
contract.inner.mint(index.toString(), initialTokens[index])
}
Runtime.setState(contract)
Runtime.setOkResult('true')
}
std::vector<std::pair<std::string, weilcontracts::Token>> initial_tokens = {
{"1", weilcontracts::Token("A fish going left!", "fish 1", "A one line ASCII drawing of a fish", "<><")},
{"2", weilcontracts::Token("A fish going right!", "fish 2", "A one line ASCII drawing of a fish swimming to the right", "><>")},
{"3", weilcontracts::Token("A big fish going left!", "fish 3", "A one line ASCII drawing of a fish swimming to the left", "<'))><")},
{"4", weilcontracts::Token("A big fish going right!", "fish 4", "A one line ASCII drawing of a fish swimming to the right", "><(('>")},
{"5", weilcontracts::Token("A Face", "face 1", "A one line ASCII drawing of a face", "(-_-)")},
{"6", weilcontracts::Token("Arms raised", "arms 1", "A one line ASCII drawing of a person with arms raised", "\\o/")}
};
for (const auto& [id, token] : initial_tokens) {
auto result = ascii_instance.inner.mint(id, token);
if(result.first){
std::string error = result.second;
weilsdk::MethodError me = weilsdk::MethodError("init",error);
weilsdk::Runtime::setResult(weilsdk::WeilError::FunctionReturnedWithError(me),0);
return;
}
}
We also need to construct serialization and deserialization methods for a AsciiArt instance. To achieve this, we define four additional functions in the AsciiArt class
collections::WeilMap<std::string, bool> getControllers() const{
return controllers;
}
void setControllers(collections::WeilMap<std::string, bool> _controllers) {
controllers = _controllers;
}
const NonFungibleToken& getInner() const{
return inner;
}
void setInner(const NonFungibleToken& _inner) {inner = _inner;}
Finally we create our serialization and deserialization methods for our AsciiArt instance.
inline void to_json(nlohmann::json &j, const AsciiArtContractState &asciiart){
nlohmann::json j1;
to_json(j1, asciiart.getControllers());
std::string serialized_controllers_state = j1.dump();
j = nlohmann::json{
{"inner", asciiart.getInner()},
{"controllers", serialized_controllers_state}
};
}
inline void from_json(const nlohmann::json &j, AsciiArtContractState &asciiart) {
std::string serialized_controllers = j.at("controllers");
nlohmann::json j1 = nlohmann::json::parse(serialized_controllers);
collections::WeilMap<std::string, bool> mp;
from_json(j1,mp);
asciiart.setControllers(mp);
std::string name = j.at("inner").at("name");
NonFungibleToken nft(name);
asciiart.setInner(nft);
}
Basic Methods
The contract provides methods to retrieve data related to the inner NFT, as follows:
- Rust
- Go
- AssemblyScript
- CPP
#[query]
async fn name(&self) -> String {
self.inner.name()
}
#[query]
async fn details(&self, token_id: String) -> Result<TokenDetails, String> {
let token = self
.inner
.details(token_id)
.map_err(|err| err.to_string())?;
Ok(TokenDetails {
name: token.name,
title: token.title,
description: token.description,
payload: token.payload,
})
}
// query
func (t *AsciiArtContractState) Name() string {
return t.Inner.Name()
}
// query
func (t *AsciiArtContractState) Details(tokenId string) (*TokenDetails, error) {
result, err := t.Inner.Details(tokenId)
if err != nil {
return nil, err
}
return &TokenDetails{
Name: result.Name,
Title: result.Title,
Description: result.Description,
Payload: result.Payload,
}, nil
}
Observe that the last method, Details
, returns TokenDetails
, a type defined in the WIDL specification and generated in the types.go
file.
package contract
type TokenDetails struct {
Title string `json:"title"`
Name string `json:"name"`
Description string `json:"description"`
Payload string `json:"payload"`
}
export function details(): void {
const stateAndArgs = Runtime.stateAndArgs<AsciiArtContractState, TokenIdArgs>()
const state = (stateAndArgs.elements[0] as JSONWrapper<AsciiArtContractState>).inner;
const args = (stateAndArgs.elements[1] as JSONWrapper<TokenIdArgs>).inner;
if (args.token_id === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'details',
err_msg: `field "token_id" must be provided`,
}),
)
return
}
const result = state.inner.details(args.token_id as string)
if (result.isOk()) {
Runtime.setOkResult(result.tryValue())
} else {
Runtime.setErrorResult(result.tryError())
}
}
std::string name() const {
return inner.getName();
}
std::variant<TokenDetails, std::string> details(const std::string& tokenId){
auto result = inner.details(tokenId);
if(result.first){
//there was some error
return std::get<std::string>(result.second);
}
weilcontracts::Token token = std::get<weilcontracts::Token>(result.second);
TokenDetails tokenDetails {
token.name,
token.title,
token.description,
token.payload
};
return tokenDetails;
}
Query Methods
These methods are used to get retrieve information based on the current state of the contract.
- Rust
- Go
- AssemblyScript
- CPP
#[query]
async fn balance_of(&self, addr: String) -> usize {
self.inner.balance_of(addr)
}
#[query]
async fn owner_of(&self, token_id: String) -> Result<String, String> {
self.inner.owner_of(token_id).map_err(|err| err.to_string())
}
#[query]
async fn get_approved(&self, token_id: String) -> Result<Vec<String>, String> {
self.inner
.get_approved(token_id)
.map_err(|err| err.to_string())
}
#[query]
async fn is_approved_for_all(&self, owner: String, spender: String) -> bool {
self.inner.is_approved_for_all(owner, spender)
}
// query
func (t *AsciiArtContractState) BalanceOf(addr string) uint64 {
return t.Inner.BalanceOf(addr)
}
// query
func (t *AsciiArtContractState) OwnerOf(tokenId string) (*string, error) {
return t.Inner.OwnerOf(tokenId)
}
// query
func (t *AsciiArtContractState) GetApproved(tokenId string) (*[]string, error) {
return t.Inner.GetApproved(tokenId)
}
// query
func (t *AsciiArtContractState) IsApprovedForAll(owner string, spender string) bool {
return t.Inner.IsApprovedForAll(owner, spender)
}
export function balance_of(): void {
const stateAndArgs = Runtime.stateAndArgs<AsciiArtContractState, BalanceOfArgs>()
const state = (stateAndArgs.elements[0] as JSONWrapper<AsciiArtContractState>).inner;
const args = (stateAndArgs.elements[1] as JSONWrapper<BalanceOfArgs>).inner;
if (args.addr === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'balance_of',
err_msg: `field "addr" must be provided`,
}),
)
return
}
Runtime.setOkResult(state.inner.balanceOf(args.addr))
}
export function owner_of(): void {
const stateAndArgs = Runtime.stateAndArgs<AsciiArtContractState, TokenIdArgs>()
const state = (stateAndArgs.elements[0] as JSONWrapper<AsciiArtContractState>).inner;
const args = (stateAndArgs.elements[1] as JSONWrapper<TokenIdArgs>).inner;
if (args.token_id === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'owner_of',
err_msg: `field "token_id" must be provided`,
}),
)
return
}
const result = state.inner.ownerOf(args.token_id as string)
if (result.isOk()) {
Runtime.setOkResult(result.tryValue())
} else {
Runtime.setErrorResult(result.tryError())
}
}
export function get_approved(): void {
const stateAndArgs = Runtime.stateAndArgs<AsciiArtContractState, TokenIdArgs>()
const state = (stateAndArgs.elements[0] as JSONWrapper<AsciiArtContractState>).inner;
const args = (stateAndArgs.elements[1] as JSONWrapper<TokenIdArgs>).inner;
if (args.token_id === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'get_approved',
err_msg: `field "token_id" must be provided`,
}),
)
return
}
Runtime.setOkResult(state.inner.getApproved(args.token_id as string))
}
export function is_approved_for_all(): void {
// const nonNullableArgs = Runtime.args<NonNullable<IsApprovedForAllArgs>>()
// why did we need non-nullable args also??
const stateAndArgs = Runtime.stateAndArgs<AsciiArtContractState, IsApprovedForAllArgs>()
const state = (stateAndArgs.elements[0] as JSONWrapper<AsciiArtContractState>).inner;
const args = (stateAndArgs.elements[1] as JSONWrapper<IsApprovedForAllArgs>).inner;
if (args.spender === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'is_approved_for_all',
err_msg: `field "spender" must be provided`,
}),
)
return
}
if (args.owner === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'is_approved_for_all',
err_msg: `field "owner" must be provided`,
}),
)
return
}
Runtime.setOkResult(
state.inner.isApprovedForAll(
args.owner as string,
args.spender as string,
),
)
}
size_t balance_of(const std::string& addr) {
return inner.balanceOf(addr);
}
std::string owner_of(const std::string& token_id) {
return inner.ownerOf(token_id).second;
}
std::pair<bool, std::variant<std::string, std::vector<Address>>> get_approved(const TokenId tokenId){
return inner.getApproved(tokenId);
}
bool is_approved_for_all(std::string owner, std::string spender){
return inner.isApprovedForAll(owner, spender);
}
Mutation Methods
These methods are used to modify the current state of the contract. Most are pretty straightforward, just delegating to the inner token.
- Rust
- Go
- AssemblyScript
- CPP
#[mutate]
async fn approve(&mut self, spender: String, token_id: String) -> Result<(), String> {
self.inner
.approve(spender, token_id)
.map_err(|err| err.to_string())
}
#[mutate]
async fn set_approve_for_all(&mut self, spender: String, approval: bool) {
self.inner.set_approve_for_all(spender, approval)
}
#[mutate]
async fn transfer(&mut self, to_addr: String, token_id: String) -> Result<(), String> {
self.inner
.transfer(to_addr, token_id)
.map_err(|err| err.to_string())
}
#[mutate]
async fn transfer_from(
&mut self,
from_addr: String,
to_addr: String,
token_id: String,
) -> Result<(), String> {
self.inner
.transfer_from(from_addr, to_addr, token_id)
.map_err(|err| err.to_string())
}
#[mutate]
async fn mint(
&mut self,
token_id: String,
title: String,
name: String,
description: String,
payload: String,
) -> Result<(), String> {
let token = Token::new(title, name, description, payload);
let from_addr = Runtime::sender();
if !self.is_controller(&from_addr) {
return Err(format!("Only controllers can mint"));
}
self.inner
.mint(token_id, token)
.map_err(|err| err.to_string())
}
// mutate
func (t *AsciiArtContractState) Approve(spender string, tokenId string) error {
return t.Inner.Approve(spender, tokenId)
}
// mutate
func (t *AsciiArtContractState) SetApproveForAll(spender string, approval bool) {
t.Inner.SetApproveForAll(spender, approval)
}
// mutate
func (t *AsciiArtContractState) Transfer(toAddr string, tokenId string) error {
return t.Inner.Transfer(toAddr, tokenId)
}
// mutate
func (t *AsciiArtContractState) TransferFrom(fromAddr string, toAddr string, tokenId string) error {
return t.Inner.TransferFrom(fromAddr, toAddr, tokenId)
}
// mutate
func (t *AsciiArtContractState) Mint(tokenId string, title string, name string, description string, payload string) error {
fromAddr := runtime.Sender()
token := non_fungible.Token{
Title: title,
Name: name,
Description: description,
Payload: payload,
}
if !t.isController(fromAddr) {
return fmt.Errorf("only controllers can mint")
}
return t.Inner.Mint(tokenId, token)
}
export function approve(): void {
const stateAndArgs = Runtime.stateAndArgs<AsciiArtContractState, ApproveArgs>()
const state = (stateAndArgs.elements[0] as JSONWrapper<AsciiArtContractState>).inner;
const args = (stateAndArgs.elements[1] as JSONWrapper<ApproveArgs>).inner;
if (args.token_id === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'approve',
err_msg: `field "token_id" must be provided`,
}),
)
return
}
if (args.spender === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'approve',
err_msg: `field "spender" must be provided`,
}),
)
return
}
const res = state.inner.approve(
args.spender as string,
args.token_id as string,
)
if (res.isOk()) {
const weilValue = WeilValue.newWithStateAndOkValue(state, res.tryValue());
const result = Result.Ok<WeilValue<AsciiArtContractState,string>, WeilError>(weilValue);
Runtime.setStateAndResult<AsciiArtContractState,string>(result);
} else {
const error = res.tryError();
const result = Result.Err<WeilValue<AsciiArtContractState,string>, WeilError>(
WeilError.FunctionReturnedWithError({
method_name: 'approve',
err_msg: error.message
})
);
Runtime.setStateAndResult<AsciiArtContractState,string>(result);
}
}
export function set_approve_for_all(): void {
const stateAndArgs = Runtime.stateAndArgs<AsciiArtContractState, SetApproveForAllArgs>()
const state = (stateAndArgs.elements[0] as JSONWrapper<AsciiArtContractState>).inner;
const args = (stateAndArgs.elements[1] as JSONWrapper<SetApproveForAllArgs>).inner;
if (args.spender == null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'set_approve_for_all',
err_msg: `field "spender" must be provided`,
}),
)
return
}
state.inner.setApproveForAll(
args.spender as string,
args.approval as boolean,
)
const weilValue = WeilValue.newWithStateAndOkValue(state, "true");
const result = Result.Ok<WeilValue<AsciiArtContractState,string>, WeilError>(weilValue);
Runtime.setStateAndResult<AsciiArtContractState,string>(result);
}
export function transfer(): void {
const stateAndArgs = Runtime.stateAndArgs<AsciiArtContractState, TransferArgs>()
const state = (stateAndArgs.elements[0] as JSONWrapper<AsciiArtContractState>).inner;
const args = (stateAndArgs.elements[1] as JSONWrapper<TransferArgs>).inner;
if (args.token_id === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'transfer',
err_msg: `field "token_id" must be provided`,
}),
)
return
}
if (args.to_addr === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'transfer',
err_msg: `field "to_addr" must be provided`,
}),
)
return
}
const res = state.inner.transfer(
args.to_addr as string,
args.token_id as string,
)
if (res.isOk()) {
const weilValue = WeilValue.newWithStateAndOkValue(state, res.tryValue());
const result = Result.Ok<WeilValue<AsciiArtContractState,string>, WeilError>(weilValue);
Runtime.setStateAndResult<AsciiArtContractState,string>(result);
} else {
const error = res.tryError();
const result = Result.Err<WeilValue<AsciiArtContractState,string>, WeilError>(
WeilError.FunctionReturnedWithError({
method_name: 'transfer',
err_msg: error.message
})
);
Runtime.setStateAndResult<AsciiArtContractState,string>(result);
}
}
export function transfer_from(): void {
const stateAndArgs = Runtime.stateAndArgs<AsciiArtContractState, TransferFromArgs>()
const state = (stateAndArgs.elements[0] as JSONWrapper<AsciiArtContractState>).inner;
const args = (stateAndArgs.elements[1] as JSONWrapper<TransferFromArgs>).inner;
if (args.token_id === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'transfer_from',
err_msg: `field "token_id" must be provided`,
}),
)
return
}
if (args.from_addr === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'transfer_from',
err_msg: `field "from_addr" must be provided`,
}),
)
return
}
if (args.to_addr === null) {
Runtime.setErrorResult(
WeilError.MethodArgumentDeserializationError({
method_name: 'transfer_from',
err_msg: `field "to_addr" must be provided`,
}),
)
return
}
const res = state.inner.transferFrom(
args.from_addr as string,
args.to_addr as string,
args.token_id as string,
)
if (res.isOk()) {
const weilValue = WeilValue.newWithStateAndOkValue(state, res.tryValue());
const result = Result.Ok<WeilValue<AsciiArtContractState,string>, WeilError>(weilValue);
Runtime.setStateAndResult<AsciiArtContractState,string>(result);
} else {
const error = res.tryError();
const result = Result.Err<WeilValue<AsciiArtContractState,string>, WeilError>(
WeilError.FunctionReturnedWithError({
method_name: 'approve',
err_msg: error.message
})
);
Runtime.setStateAndResult<AsciiArtContractState,string>(result);
}
}
void approve(const std::string& spender, const std::string& tokenId) {
inner.approve(spender, tokenId);
}
void set_approve_for_all(const std::string& spender, bool approval) {
inner.setApproveForAll(spender, approval);
}
void transfer(const std::string& toAddr, const std::string& tokenId) {
inner.transfer(toAddr, tokenId);
}
void transfer_from(const std::string& fromAddr, const std::string& toAddr, const std::string& tokenId) {
inner.transferFrom(fromAddr, toAddr, tokenId);
}
The mint
method, though, is more interesting. Before minting the token, it checks if the address that invoked this operation is contained in the controllers set, which contains the address of whoever deployed the contract.
For that an ancillary function is used.
- Rust
- Go
- AssemblyScript
- CPP
impl AsciiArtContractState {
fn is_controller(&self, addr: &String) -> bool {
match self.controllers.get(addr) {
Some(_) => true,
None => false,
}
}
}
func (t *AsciiArtContractState) isController(addr string) bool {
return t.Controllers.Contains(&addr)
}
isController(addr: string): boolean {
return this.controllers.get(addr) !== null
}
bool is_controller(const std::string& addr) {
if(!this->getControllers().contains(addr)){
return false;
}
else return this->getControllers().get(addr);
}
You now have all pieces in place. Go ahead, build the contract and deploy it. Then follow the WebWallet tutorial to see and operate your tokens.