Skip to main content

Creating a Non-Fungible Token Contract

In this tutorial we'll implement a Non-Fungible Token.

info

To know what a non-fungible (and a fungible) token is, we recommend you to checkout Weil Tokens

warning

The steps are slightly different depending on the language you are using, so make sure go chose the right language now.

You have chosen Rust!

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.

cargo new asciiart --lib
cd asciiart

Contract Specification

The token is defined by the following WIDL interface.

asciiart.widl
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.

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

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.

src/lib.rs
#[derive(Serialize, Deserialize, WeilType)]
pub struct AsciiArtContractState {
/// Controllers allowed to mint new tokens.
/// TODO: How is the set updated?
controllers: WeilMap<String, ()>,
inner: NonFungibleToken,
}

Contract Initialization

The constructor for the AsciiArt executes three steps:

  1. 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".
  2. Initialize the controllers set with the address of the account being used to deploy this contract, retrieved using the sender() function.
  3. Create an initial set of tokens.
src/lib.rs
    #[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)
}

Basic Methods

The contract provides methods to retrieve data related to the inner NFT, as follows:

src/lib.rs
    #[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 Methods

These methods are used to get retrieve information based on the current state of the contract.

src/lib.rs

#[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)
}


Mutation Methods

These methods are used to modify the current state of the contract. Most are pretty straightforward, just delegating to the inner token.

src/lib.rs
    #[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())
}

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.

src/lib.rs
impl AsciiArtContractState {
fn is_controller(&self, addr: &String) -> bool {
match self.controllers.get(addr) {
Some(_) => true,
None => false,
}
}
}

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.