Tokenized Comments Example
This chapters demonstrates how one can reward user interaction with virtual tokens. Its a simple comment section where users (after logging in) can leave a comment and like other comments.
The purpose is to demonstrate how to use common programming language features in Motoko.
We use a virtual token with a capped supply, but we intentionally don't allow transfers. This way, the token can be used to reward users, but it can't be traded on exchanges.
WARNING
This is NOT a digital crypto currency. Tokens can not be sent. The tokenomics model is not economically sound. The model is for code writing demonstrations only.
NOTE
The token is not an ICRC1 compliant token
Rules are simple:
- You can only comment and like interact after logging in with Internet Identity.
- You may only comment once per 10 minutes and like once per minute.
- You earn 10 tokens for a posted comment
- You earn 1 token for every like you receive.
We start with a total supply of 10 000 tokens. When all tokens are given out, no more tokens can be earned.
Project setup
The source code is available here. The project consists of two canisters:
- a backend canister written in Motoko
- a frontend UI built with Vite-SvelteKit-Tailwind.
The frontend is uploaded to a separate frontend assets canister that can serve the website to a browser. The frontend canister calls the backend canister's shared functions to interact with the backend.
NOTE
The frontend, the authentication with Internet Identity and the interaction with the backend from within Javascript code are outside the scope of this book.
Backend canister source code
The backend canister source code is setup with the following files:
main.mo
contains the main actor, datastores, shared functions and system upgrade functionsConstants.mo
contains the constants used in the projectTypes.mo
contains the types used in the projectUtils.mo
contains utility functions used in the projectComments.mo
contains the the implementations of the main shared functions
Datastores
We use a state
object of type State
to hold the datastores:
public type State = object {
users : Users;
commentStore : CommentStore;
var commentHistory : CommentHistory;
var treasury : Treasury;
};
The datastore types are:
public type Users = HashMap.HashMap<Principal, User>;
public type CommentStore = HashMap.HashMap<CommentHash, Comment>;
public type CommentHistory = List.List<CommentHash>;
public type Treasury = Nat;
The hashmaps Users
and CommentStore
are immutable variables in our state
object. This means they hold a hashmap object with callable methods, but they cannot be replaced.
CommentHistory
and Treasury
on the other hand are mutable variables. This means they hold a mutable variable that can be replaced with a new value. This happens when we deduct from the treasury or when we add a new comment to the comment history.
Shared functions
The main.mo
contains the public API of the actor. Most of the logic implementation for the shared functions is factored out to Comments.mo
for clear code organization.
We only perform checks for the identity of the caller and pass the datastores as arguments to the functions in Comments.mo
.
State mutations
The state
object and its stores are only updated by functions in Comments.mo
. The updates always occur within an atomically executed functions. The functions that update the state are register
, postComment
and likeComment
.
Note that register
always returns QueryUser
, but postComment
and likeComment
return PostResult
and LikeResult
, which may return an error, see Result
.
postResult
performs all the checks before any state is updated. If any check fails, the function returns an error and the state is not updated. If all checks pass, the function updates the state and returns the result. The response tells us whether the state was successfully updated or not.
likeComment
returns async* LikeResult
. The reason for this is that the function may throw
an Error
if any of its checks fail. (Errors can only be thrown in an asynchronous context).
Errors are only thrown in likeComment
if something unexpected happens, like when a like is submitted to a comment which doesn't exist. Since the frontend is programmed to be able to do that, a call like that should never happen.
Recall that state updates up until an error are persisted in async
and async*
functions. In the case of likeComment
the users
hashmap may be updated before an Error
but balances are only updated if all checks are met.
Upgrading
The state
object initialized in main.mo
from stable var arrays which are initially empty. The state
object is used in working memory and filled with data when posts are made. The state
object is not persisted during upgrades, so when the canister is upgraded, the data is lost.
Therefore, we use the preupgrade
system function to copy all the data to the stable
array variables before an upgrade. The datastores then are initialized again from the stable
variables.
In the postupgrade
system function, we empty out the stable
arrays to save memory.
NOTE
preupgrade
andpostupgrade
system functions may trap during execution and data may be lost in the process. Work is underway to improve canister upgrades by working with stable storage.
Constants
All constants used in the project are defined in Constants.mo
. This way, we can easily change the constants in one place and the changes are reflected throughout the project.
DEMO
The comments canister is live on mainnet.