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 :)