Exploring Gno.land: Building Realms w/ Gnolang
A smooth introduction to Gno.land
Before starting, it is worth noting that this is NOT a paid content.
What is Gno.land?
Gno.land is a blockchain L1 solution concentrated on making smart contract development as easy as possible.
Why does it matter?
Because this way, a lot of web2 developers can easily contribute to web3 and build smart contracts.
Gnolang
Gnolang serves as the programming language for creating Realms, which are Smart Contracts on the Gnoland platform.
It's essentially an interpreted version of Golang, where developers upload their source code onto the blockchain and GnoVM executes the AST interpretation. By doing so, Gnoland emphasizes transparency since developers must share their source code instead of compiled bytecode. Furthermore, Gnolang is designed to support multi-threading, similar to Go routines and channels, for developing smart contracts.
EVM vs GnoVM
EVM
Program code - Solidity
EVM bytecode - another low-level code
EVM implementation - C++, Go, or Rust
GnoVM
Program code - Go
Go AST (abstract syntax tree) - Go
GnoVM implementation - Go
So, you can learn Go once, and then do everything :).
As I mentioned, Realms (smart contracts) are written in Gno.
A public function called Render(path string) string
can be implemented by each Realm, which is responsible for generating valid markdown for the given path parameter. This function is aimed at enhancing the interactivity of Realms and simplifying their rendering process.
The gnodev cli toolsuite is provided to developers for developing Realms, which includes user-friendly commands for testing and building Realms.
Realms can employ Gno packages from the Gno standard library such as random, maths, avl, etc. or even from the community.
The variables of the Realm's packages are used to store the Realm's state.
Packages
In essence, a gno.land Package is a collection of features and capabilities that does not possess any state, which makes it akin to a library. Another significant aspect is that packages can be imported from various Realms and Packages, using the syntax:
import "gno.land/p/..."
.
Realms
A Realm, written in Gno, is a Smart Contract located on gno.land. Unlike Packages, Realms have the ability to store values or state. To import a Realm, one can use the syntax:
import "gno.land/r/..."
.
Now, it is time to say "Hello!" to the entire world (or, Cosmos 😄)
package demo
// As our function returns a string value, we indicate it while defining the function.
func Hello() string {
return "Hello, World!"
}
Realm Development
State
As mentioned earlier, the main difference between packages /p
and realms /r
is that realms can manage a state.
States are basically defined as global variables (outside of functions) inside the Realm:
package updatename
var myName string = "Furkan"
func UpdateName(name string) {
myName = name
}
Init
The Realm has the ability to define an init()
function, which gets executed when the Realm is first called. This can be viewed as a kind of constructor for the Realm, allowing you to establish initial values and logic for the Realm.
Let's write a basic staking Realm!
package staking
import (
"std"
)
var balance_of map[std.Address]uint64
var staked_amount map[std.Address]uint64
func Stake(pool std.Address, staking_amount uint64) {
caller := std.GetOrigCaller()
TransferFrom(caller, pool, amount)
staked_amount[caller] += stkaing_amount
}
func TransferFrom(from std.Address, to std.Address, amount uint64) {
if (balance_of[from] < amount) {
panic("No sufficient balance!")
}
balance_of[from] -= amount
balance_of[to] += amount
}
func assertAdmin() {
if std.GetOrigCaller() != admin {
panic("Nobody can call this function, but the admin!")
}
}
Render()
The Render
is a method on a Realm which can return some sort of data to a user browsing the specific Realm using Gno.land website.
When you try to view a Package, you see that the source code is displayed; but when you try to view a Realm, the thing being displayed is the output of its Render
method. This is so natural, because recall that Packages cannot hold any state to display.
It does look like as follows:
func Render(path string) string
Now, let us write an example for a HelloWorld contract:
package hello
import (
"strings"
)
func Hello() string {
return "Hello World!"
}
func enterName(name string) string {
return "Welcome, " + name + "!"
}
// Here it comes!
func Render(path string) string {
if (path == "hello") {
return Hello()
} else {
return "Nothing found!"
}
}
ToDo App | Preliminaries
Let us clone the official Gno repository and run the build command:
git clone https://github.com/gnolang/gno.git
cd gno
make build
Now, we can create the project directories of our Package and Realm:
mkdir examples/gno.land/p/demo/todo
mkdir examples/gno.land/r/demo/todo
Let us start by building the Package and the test files 'task.gno', 'task_test.gno':
touch examples/gno.land/p/demo/todo/task.gno
touch examples/gno.land/p/demo/todo/task_test.gno
ToDo App | Package & Test
It is wise to use a struct to introduce the notion of task. 3 variables id, task, and isDone will represent any task. Let's define Task struct to make things easier:
package todo
import (
"gno.land/p/demo/ufmt"
)
type Task struct {
id uint
task string
isDone bool
}
It's time to define some functions! Let's define NewTask() that users will call when they want to create new tasks for their ToDo:
func NewTask(
id uint,
task string,
isDone bool,
) *Task {
return &Task{
id: id,
task: task,
isDone: isDone,
}
}
Now, we can define some useful functions that we can call inside our Realm:
func (t Task) GetId() uint {
return t.id
}
func (t Task) GetIdString() uint {
return ufmt.Sprintf("%d", t.GetId())
}
func (t Task) GetTask() string {
return t.task
}
func (t Task) GetIsDone() bool {
return t.isDone
}
func (t Task) GetInfo() string {
return ufmt.Sprintf(
"id: %d\nTask: %d\nisDone",
t.id, t.Task, t.isDone,
)
}
Our task.gno
Package is ready! Let us write a test for it now and see if it is working properly:
package todo
import (
"testing"
)
func TestNewTask(t *testing.T) {
const (
id uint = 1
task string = "Debug the code!"
isDone bool = false
)
j := NewTask(id, task, isDone)
if (j.GetId() != id) {
t.Fatalf("Wrong ID!")
}
if (j.GetTask() != task) {
t.Fatalf("Wrong Task!")
}
if (j.GetIsDone() != isDone) {
t.Fatalf("Wrong Status!")
}
}
Before running the test command, we need to execute some commands:
make install gnodev
gnodev precompile examples/gno.land/p/demo/todo/task.gno
gnodev test examples/gno.land/p/demo/todo/task_test.gno
The terminal output:
=== RUN TestNewTask
--- PASS: TestNewItem (0.00s)
ok ./examples/gno.land/p/demo/todo/ 2.01s
ToDo App | Realm
Let us start by creating the Realm:
touch examples/gno.land/r/demo/todo/todo.gno
As our contract will have many functions and we want to log if functions will work properly, it is useful to define some messages after importing relevant libraries and also we can initially define a bunch of constants:
package todo
import (
"bytes"
"strings"
"std"
"gno.land/p/demo/todo"
"gno.land/p/demo/avl"
)
const (
successMessage = "New task is added successfully!"
markMessage = "Good job!"
failMessage = "Unable to add the new task!"
noTaskMessage = "There is no task listed!"
notFoundMessage = "Not found!"
)
var (
admin std.Address = "?"
tasks avl.Tree
idCounter uint
)
We will add the admin address later when we generate it :).
Now, let's add some useful functions:
func isAdmin(address std.Address) bool {
return address == admin
}
func getTaskById(id string) *todo.Task {
task, found := task.Get(id)
if !found {
return nil
}
return task.(*todo.Task)
}
You can think of isAdmin()
as similar as the onlyOwner
modifier in Solidity.
It's time to add our Render() method:
func Render(path string) string {
parts: = strings.Split(path, "/")
switch {
case path == "":
return renderTasks()
case len(parts) == 2 && parts[0] == "task":
task: = getTaskById(parts[1])
if task == nil {
return notFoundMessage
}
return task.GetInfo()
default:
return notFoundMessage
}
}
func renderTasks() string {
if tasks.Size() < 1 {
return noTaskMessage
}
var buffer bytes.Buffer
tasks.Iterate("", "", func(t * avl.Node) bool {
task,
_: = t.Value().( * todo.Task)
buffer.WriteString(task.GetInfo())
buffer.WriteString("\n\n")
return false
})
return buffer.String()
}
Let's define our final two functions: AddTask() and MarkDone(). The first one will be used to add new tasks to the ToDo. The latter will be used to mark tasks to be done.
func AddTask(
task string,
isDone bool,
) string {
if !isAdmin(std.GetOrigCaller()) {
return failMessage
}
newTask: = todo.NewTask(
idCounter,
task,
isDone,
)
tasks.Set(
newTask.GetIdString(),
newTask,
)
idCounter++
return successMessage
}
func MarkDone(id string) string {
task: = getTaskById(id)
if task == nil {
return notFoundMessage
}
task.isDone = true
return markMessage
}
ToDo App | CLI Operations
In this page, we will create an account, deploy our code, and finally do some operations. We start by generating a Gno.land account:
./build/gnokey generate
The terminal output:
pupil great rule input annual sunny west quick bunker borrow tomorrow exact couple vital air cluster glare fire nest timber shadow point dilemma sight
This is the mnemonics that we will use to obtain our account!
Let 's do it:
./build/gnokey add--recover Admin
After entering our mnemonics, the terminal output is as follows:
Admin(local) - addr: g1m2znmyqgd32r9zcts6lp3yapmj4h832fn73n2n pub: gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqgfsdc7vfka27rget2qxxxrdxs8we3zfu0qm7glkq2lu3hqrt5u6kk5xcst, path: < nil >
This is the address that we 'd replace in our contract:
var (
admin std.Address = "g1m2znmyqgd32r9zcts6lp3yapmj4h832fn73n2n"
tasks avl.Tree idCounter uint
)
After getting some test tokens from the faucet, we are ready to deploy our code. But first, let's start the local blockchain:
./build/gnoland
Now, in another terminal, we can deploy our code:
./build/gnokey maketx addpkg - deposit "100000000ugnot" - gas - fee "1000000ugnot" - gas - wanted "5000000" - remote "localhost:26657" - chainid "dev" - pkgdir "examples/gno.land/p/demo/todo" - pkgpath "gno.land/p/demo/todo" Admin
./build/gnokey maketx addpkg - deposit "100000000ugnot" - gas - fee "1000000ugnot" - gas - wanted "5000000" - remote "localhost:26657" - chainid "dev" - pkgdir "examples/gno.land/r/demo/todo" - pkgpath "gno.land/r/demo/todo" Admin
The terminal outputs:
OK!
OK!
The Realm and the Package have been successfully deployed! Now, let's make a contract call:
./build/gnokey maketx call - func AddTask - args "Deploy your Realm" - args false - deposit "100000000ugnot" - gas - fee "1000000ugnot" - gas - wanted "5000000" - remote "localhost:26657" - chainid "dev" - pkgpath "gno.land/r/demo/todo" Admin
The terminal output:
("New task is added successfully!", string)
OK!
Thanks for reading Furkan’s Substack! Subscribe for free to receive new posts and support my work.