Gno.land From Main Branch: Refactor From Test3 & Toy Registrar Realm

Gno.land From Main Branch: Refactor From Test3 & Toy Registrar Realm

ยท

12 min read

I decided to start fresh from my last couple of explorations and given the test3 faucet is drained, rather than committing to that build I'll aim to start working from HEAD to keep up with changes as they come.

Project Refactor Since Test3

As per this thread the project has been refactored since the test3 release, which means most of the past walkthroughs can be ignored other than initial introduction to the project.

Now rather than flail aimlessly in confusion as per the gonzo take, this time around I spent time catching up on all the intentionally skipped resources and will cite awesome-gno and OnBloc as worth reading.

Installing On Ubuntu From Main Branch

This process works as you'd expect for a Golang project. If you run into dependency issues, one of the previous articles may bail you out.

If you imagine you'll experiment enough with the project to maybe want to contribute, ideally you'll work from your own fork, but these instructions will presume you're instead working straight from the official repo. First I'll clone the main branch to a new folder with and run make install.

cd ~/gno
git clone https://github.com/gnolang/gno.git
cd ./gno
make install

This runs the install_gnokey and install_gno make tasks. Worth noting here is that in the refactor, install_gno refers to the .gnovm path, while the gnodev command from previous versions has now been renamed to gno (brevity, me gusta).

If you've worked with the test3 version, you'll immediately notice no ./build folders exist and you'll need to build your project first. The older structure contained a Makefile with all tasks at the root, but this refactored structure now contains the root level Makefile, and one in both ./gno.land and ./gnovm.

To build this new structure, from the root directory you can run:

cd ./gnovm
make install
make build
cd ../gno.land
make install
make build

This should have installed and built gno in the ./gnovm directory, and gnoland, gnokey, gnoweb and gnofaucet in ./gno.land. If each now has a build directory populated as expected, we can move on to the rest of the localnet setup.

Interactions With Localnet Post-Refactor

Fortunately, once you're familiar with why/how the refactor happened, it's fairly straight forward to adapt what worked with test3 to the new architecture. To recap, the process will be to generate an account (wallet) and add it to your keybase, add its address to the genesis_balances.txt file, startup your node, register your user with the r/demo/user realm, and then you can test all has worked by interacting with the r/demo/boards realm. That process is the same, with some minor changes in the commands used to do it.

Worth noting here, if you want to reset your local node's state to genesis, you can achieve the same as make reset from the test3 build with make fclean from the ./gno.land dir.

First I'll jump into `./gno.land` and use gnokey to generate my account mnemonic and add it to the keybase with a key I'll remember. I've just opted to use my existing local-test to keep it simple.

./build/gnokey generate 
<mnemonic phrase, remember to backup>

./build/gnokey add KEYNAME --recover

Enter a passphrase to encrypt your key to disk:
<make a password>

Repeat the passphrase:
<repeat the password>

Enter your bip39 mnemonic
<enter mnemonic generated previously>

Now when you run ./build/gnokey list again you'll see something like the following:

0. local-test (local) - addr: g138j0um48nm4ppcz0tr6az4qu3yjwjl3l6ntqdx

With your address in hand, you can now head over to ./gno.land/genesis/gensis_balances.txt and append the address to the list to seed it with $GNOT.

From the ./gno.land dir, you can now run ./build/gnoland to spin up your local node, complete with your seeded account. Remember make fclean will reset this to genesis state if you need.

This is the first point I run into a noticeable change between test3 and the current main. Past commands seemed to append flags at the end, while the response I get back indicates it now expects flags to be set before the path. The expected format is query [flags] <path>.

./build/gnokey query -remote localhost:26657 auth/accounts/g138j0um48nm4ppcz0tr6az4qu3yjwjl3l6ntqdx

With that minor tweak and the revised command run I get back the following as expected:

height: 0
data: {
  "BaseAccount": {
    "address": "g138j0um48nm4ppcz0tr6az4qu3yjwjl3l6ntqdx",
    "coins": "10000000000ugnot",
    "public_key": null,
    "account_number": "53",
    "sequence": "0"
  }
}

The second point I run into a change from test3 is related to the first and happens with the ./build/gnokey maketx call command. I determine the format expected is <subcommand> [flags] [<args>...]. This required a bit of tinkering to sort through the pkgpath not specified error I get back at first. This thread helped me find some changes worth making, which included switching from -- to - for flags/args (largely just to comform), and dropping the "true" from the -broadcast flag. The real change needed though was simply moving the keybase identifier/address to the end.

./build/gnokey maketx call -pkgpath "gno.land/r/demo/users" -func "Register" -gas-fee "10000000ugnot" -gas-wanted "2000000" -send "200000000ugnot" -chainid dev -remote 127.0.0.1:26657 -broadcast -args "" -args "kanyetest" -args "OG tinkerer; hack the planet!" local-test

OK!
GAS WANTED: 2000000
GAS USED:   1361117

Worth noting the gas used seems to have ticked up since test3, which had used 1327073 for this step instead of the 1361117 used in the current build. Would need more than a sample from each to glean any meaning.

You should now be able to run ./build/gnoweb and fire up the browser to confirm r/demo/users now contains mister kanyetest. From here you can follow the same pattern of changes in the commands to burn through the remaining steps of creating a board, creating a post, and creating a reply.

./build/gnokey maketx call -pkgpath "gno.land/r/demo/boards" -func "CreateBoard" -args "kanyetest" -gas-fee "1000000ugnot" -gas-wanted "10000000" -broadcast -chainid dev -remote 127.0.0.1:26657 local-test


(2 gno.land/r/demo/boards.BoardID)
OK!
GAS WANTED: 10000000
GAS USED:   2314226


./build/gnokey query -data "gno.land/r/demo/boards
GetBoardIDFromName(\"kanyetest\")" -remote 127.0.0.1:26657 "vm/qeval"

height: 0
data: (2 gno.land/r/demo/boards.BoardID)
(true bool)


./build/gnokey maketx call -pkgpath "gno.land/r/demo/boards" -func "CreateThread" -args 2 -args "Hello gno.land" -args "Hey all! ๐Ÿ‘‹\n\nThis is my first post in this land!" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote 127.0.0.1:26657 local-test

OK!
GAS WANTED: 2000000
GAS USED:   904869

./build/gnokey query -data "gno.land/r/demo/boards
kanyetest/1" -remote 127.0.0.1:26657 "vm/qrender"

height: 0
data: # Hello gno.land

Hey all! ๐Ÿ‘‹\n\nThis is my first post in this land!
\- [@kanyetest](/r/users:kanyetest), [2023-05-11 5:06pm (UTC)](/r/demo/boards:kanyetest/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=2&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=2&threadid=1&postid=1)]


./build/gnokey maketx call -pkgpath "gno.land/r/demo/boards" -func "CreateReply" -args "2" -args "1" -args "1" -args "Neat post!" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote 127.0.0.1:26657 local-test

(2 gno.land/r/demo/boards.PostID)
OK!
GAS WANTED: 2000000
GAS USED:   959322


./build/gnokey query -data "gno.land/r/demo/boards
kanyetest/1" -remote 127.0.0.1:26657 "vm/qrender"


height: 0
data: # Hello gno.land

Hey all! ๐Ÿ‘‹\n\nThis is my first post in this land!
\- [@kanyetest](/r/users:kanyetest), [2023-05-11 5:06pm (UTC)](/r/demo/boards:kanyetest/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=2&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=2&threadid=1&postid=1)]

> Neat post!
> \- [@kanyetest](/r/users:kanyetest), [2023-05-11 5:10pm (UTC)](/r/demo/boards:kanyetest/1/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=2&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=2&threadid=1&postid=2)]

Those commands include some calls to vm/qrender and vm/eval to print to terminal which have also been adapted to conform to the current expected pattern that places those at the end. The other command that's changed worth noting is the file arg on the CreateThread call which was tripping me up trying to leverage the example_post.md file so it's embedded in the command. Will return to prodding at that later since \n definitely just prints/looks bad, but this command didn't seem to work, even with the relative path being correct.

Through the browser you should now also be able to click into r/demo/boards and find your freshly created content.

Toy Project: A Simple Registrar

Now the fun part: new package/realm dev!

Since I already had some simple toy Registrar cooked as part of my zany/ethereally defined CMesh project that was written in Go I decided it seemed worth plucking out to play with in Gno. Then as luck would have it not long after deciding it would be my initial stab at writing something, this thread emerged proposing a registry for GnoVM chains. Sweet. This won't be that per se, but could always be adapted later.

The model doesn't aim to conform to any standard like ENS with NFT, and will simply focus on maintaining the state within the registrar.

The model we're going for is pretty straightforward. The goal is to adapt the following interface and implementation from another project and then use it from a realm.

type IRegistrar interface{
  Register(string,string) bool
  Resolve(string) string
}

type Registrar struct{
  domainMappings map[string]string
}

func (r *Registrar) Init() Registrar{
  r.domainMappings = map[string]string{}
  return *r
}

func (r *Registrar) Register(domain string, fqmn string) bool{

  if r.domainMappings[domain] != "" {
    return false
  }
  r.domainMappings[domain] = fqmn
  return true
}

func (r *Registrar) Resolve(domain string) string{
  fqmn := "DOMAIN UNREGISTERED"
  if(r.domainMappings[domain] != ""){
    fqmn = r.domainMappings[domain]
  }
  return fqmn
}

Package Structure & Example

To break up the code how we want, first the IRegister interface can be moved to p/demo/registrar/iregistrar.gno, and the remaining implementation can be moved to registrar.gno in the same directory. Each will need to be part of the registrar package.

This gives the following for iregistrar.gno:

package registrar

import(
    "std"
    "strings"
)    

type IRegistrar interface{
    Register(string,string) bool
    Resolve(string) string
}

And for registrar.gno it should look like:

package registrar

import (
    "std"
    "strings"
    "gno.land/p/demo/avl"
    "gno.land/p/demo/ufmt"
)

type Registrar struct{
    domainMappings avl.Tree
}

func Init() *Registrar {
    return &Registrar{
        domainMappings: avl.Tree{},
    }
}

func (r *Registrar) Register(domain string, dest string) bool{
    if r.domainMappings.Has(domain) {
        return false
    }
    r.domainMappings.Set(domain,dest)
    return true
}

func (r *Registrar) Resolve(domain string) string{
    dest, found := r.domainMappings.Get(domain)
    if !found {
        return "DOMAIN UNREGISTERED"
    }
    return dest.(string)
}

Given this example is relatively simple, that's all that's needed for the package development. Each file can now have tests written for it, which for now I'll skip for brevity but would live in iregistrar_test.gno and registrar_test.gno, and can be tested with ./gnovm/build/gno test.

Worth noting here, in the original implementation domainMappings was a map, which burned some time while I assumed I'd done something wrong. Instead, it turns out map persistence is not supported and avl.Tree should be used instead. If you get the invalid memory address or nil pointer dereference error and are using a map, this is likely why.

Realm Development & Deployment

In my last Gno related post, I mentioned the grc20 implementation makes for a good place to understand how to work with Gno development and architecture. Returning to it, I'll start simply by grabbing the top portion of the code down to/including init and pasting it into a new file located at r/demo/reg/reg.gno. Change the package to reg, and swap the grc20 package import for your registrar package instead. Then change the foo/AdminToken var to reference this Registrar struct instead, update admin to your address, and replace the existing init code for a call to Init on the registry. From there, some simple delegate functions for Register and Resolve will wrap up the basics. All together reg.gno will look something like this.

package reg

import (
    "std"
    "strings"

    "gno.land/p/demo/registrar"
    "gno.land/p/demo/ufmt"
    "gno.land/r/demo/users"
)

var (
    registry   *registrar.Registrar
    admin std.Address = "g138j0um48nm4ppcz0tr6az4qu3yjwjl3l6ntqdx" // TODO: helper to change admin
)

func init() {
    registry = registrar.Init()
}


func Register(name string, dest string) bool {
    return registry.Register(name, dest)
}

func Resolve(name string) string {
    return registry.Resolve(name)
}

Now back to the foo20 implementation, I'll skip the ERC20 related functions and grab the following from the bottom, adapting it to our needs.

func Render(path string) string {
    parts := strings.Split(path, "/")
    c := len(parts)

    switch {
    case path == "":
        return registry.RenderHome()
    case c == 2 && parts[0] == "resolve":
        dest := registry.Resolve(parts[1])
        return ufmt.Sprintf("%s\n", dest)
    default:
        return "404\n"
    }
}

func assertIsAdmin(address std.Address) {
    if address != admin {
        panic("restricted access")
    }
}

This is where we'll now need to dig into the grc20 package, specifically to grab the RenderHome function from the admin_token implementation and copy it into our registrar.gno package implementation. It should look something like this:

func (r *Registrar) RenderHome() string {
    str := ""
    str += ufmt.Sprintf("* **Known registrations**: %d\n", r.domainMappings.Size())
    return str
}

And, that should be it! Once deployed, when we hit r/demo/reg we'll see the number of registrations and can make calls to Register and Resolve to interact with the new realm.

Before we can see it work though, first you'll need to build your new package and realm, and then add the package to your local node. The following lines should cover you (from root):

cd ./examples

make precompile

cd ../gno.land

./build/gnokey maketx addpkg -deposit "1ugnot" -gas-fee "1ugnot" -gas-wanted "5000000"  -remote "127.0.0.1:26657" -chainid "dev" -broadcast -pkgdir "../examples/gno.land/p/demo/registrar" -pkgpath "gno.land/p/demo/registrar" local-test

OK!
GAS WANTED: 5000000
GAS USED:   326095


./build/gnokey maketx addpkg -deposit "1ugnot" -gas-fee "1ugnot" -gas-wanted "5000000"  -remote "127.0.0.1:26657" -chainid "dev" -broadcast -pkgdir "../examples/gno.land/r/demo/reg" -pkgpath "gno.land/r/demo/reg" local-test

OK!
GAS WANTED: 5000000
GAS USED:   472183

If you run the gnokey commands above an cannot create package with invalid name "" error, check that your file extensions are .gno and not .go... like I had /facepalm. If you forget the -broadcast flag, you'll get a JSON response of your transaction.

Now, from ./gno.land, you can run ./build/gnoweb to see the magical website renderer fire, and navigate your way over to /r/demo/reg to take a look at it. The Render will provide us with a count of the registrations, but as expected none currently exist so it returns 0.

The following will call Register on r/demo/reg, passing it the key=>value as args, and then will try to register the same key again and be rejected. Then it will call Resolve with the good key getting back the value, and fail to resolve a fake key. Finally checking through the browser, the reg index page should now have 1 registration, while going to r/demo/reg/resolve/<key> will return the value.

./build/gnokey maketx call -pkgpath "gno.land/r/demo/reg" -func "Register" -args "example.reg" -args "g138j0um48nm4ppcz0tr6az4qu3yjwjl3l6ntqdx" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote 127.0.0.1:26657 local-test

./build/gnokey maketx call -pkgpath "gno.land/r/demo/reg" -func "Register" -args "example.reg" -args "hijackeddestination" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote 127.0.0.1:26657 local-test

./build/gnokey maketx call -pkgpath "gno.land/r/demo/reg" -func "Resolve" -args "example.reg" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote 127.0.0.1:26657 local-test

./build/gnokey maketx call -pkgpath "gno.land/r/demo/reg" -func "Resolve" -args "fake.reg" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote 127.0.0.1:26657 local-test

Success! Navigating back to the browser we can now see the number of registrations reads 1. Worth noting, the current code does not resolve via browser, throwing 404 for a good key, and a 500 error for an invalid one at the moment. I'll circle back to resolve this, likely aiming to find a better means for path resolution, and likely templating/web rendering in general as I do (I touched on it in an earlier post, but I really am interested in the idea of rolling AstroJS support... that could be fun :P).

Wrap-Up & Next Steps

Other than the remaining web renderer bug, we've now cloned the current main branch, built the project, wrote a new package and realm to use it, deployed them both to our local node and interacted with the realm, confirming Register and Resolve both work (at least from CLI).

I'll aim to resolve the remaining web render issue to successfully Resolve through the browser and will update the example code here once done. I'll also create a branch (edit:done) for this and update with a link to the full code for anyone to take a look at. In the meantime, hopefully this helps devs moving from the test3 build over to the post-refactored project catch up and get back to hacking :)

ย