Skip to main content

How to connect a Go app to gno.land

This guide will show you how to connect to a gno.land network from your Go application, using the gnoclient package.

For this guide, we will build a small Go app that will:

  • Get account information from the chain
  • Broadcast a state-changing transaction
  • Read on-chain state

Prerequisites

  • A local gno.land keypair generated using gnokey

Setup

To get started, create a new Go project. In a clean directory, run the following:

go mod init example

After this, create a new main.go file:

touch main.go

Set up your main file with the code below:

package main

func main() {}

Finally, add the gnoclient package by running the following command:

go get github.com/gnolang/gno/gno.land/pkg/gnoclient

Main components

The gnoclient package exposes a Client struct containing a Signer and RPCClient connector. Client exposes all available functionality for talking to a gno.land chain.

type Client struct {
Signer Signer // Signer for transaction authentication
RPCClient rpcclient.Client // gnolang/gno/tm2/pkg/bft/rpc/client
}

Signer

The Signer provides functionality to sign transactions with a gno.land keypair. The keypair can be accessed from a local keybase, or it can be generated in-memory from a BIP39 mnemonic.

info

The keybase directory path is set with the gnokey --home flag.

RPCClient

The RPCCLient provides connectivity to a gno.land network via HTTP or WebSockets.

Initialize the Signer

For this example, we will initialize the Signer from a local keybase:

package main

import (
"github.com/gnolang/gno/gno.land/pkg/gnoclient"
"github.com/gnolang/gno/tm2/pkg/crypto/keys"
)

func main() {
// Initialize keybase from a directory
keybase, _ := keys.NewKeyBaseFromDir("path/to/keybase/dir")

// Create signer
signer := gnoclient.SignerFromKeybase{
Keybase: keybase,
Account: "<keypair_name>", // Name of your keypair in keybase
Password: "<keypair_password>", // Password to decrypt your keypair
ChainID: "<gno_chainID>", // id of gno.land chain
}
}

A few things to note:

  • You can view keys in your local keybase by running gnokey list.
  • You can get the password from a user input using the IO package.
  • Signer can also be initialized in-memory from a BIP39 mnemonic, using the SignerFromBip39 function.

Initialize the RPC connection & Client

You can initialize the RPC Client used to connect to the gno.land network with the following line:

rpc, err := rpcclient.NewHTTPClient("<gno.land_remote_endpoint>")
if err != nil {
panic(err)
}

A list of gno.land network endpoints & chain IDs can be found in the Gno RPC endpoints page.

With this, we can initialize the gnoclient.Client struct:

package main

import (
"github.com/gnolang/gno/gno.land/pkg/gnoclient"
"github.com/gnolang/gno/tm2/pkg/crypto/keys"
rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
)

func main() {
// Initialize keybase from a directory
keybase, _ := keys.NewKeyBaseFromDir("path/to/keybase/dir")

// Create signer
signer := gnoclient.SignerFromKeybase{
Keybase: keybase,
Account: "<keypair_name>", // Name of your keypair in keybase
Password: "<keypair_password>", // Password to decrypt your keypair
ChainID: "<gno_chainID>", // id of gno.land chain
}

// Initialize the RPC client
rpc, err := rpcclient.NewHTTPClient("<gno.land_remote_endpoint>")
if err != nil {
panic(err)
}

// Initialize the gnoclient
client := gnoclient.Client{
Signer: signer,
RPCClient: rpc,
}
}

We can now communicate with the gno.land chain. Let's explore some of the functionality gnoclient provides.

Query account info from a chain

To send transactions to the chain, we need to know the account number (ID) and sequence (nonce). We can get this information by querying the chain with the QueryAccount function:

import (
...
"github.com/gnolang/gno/tm2/pkg/crypto"
)
// Convert Gno address string to `crypto.Address`
addr, err := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // your Gno address
if err != nil {
panic(err)
}

accountRes, _, err := client.QueryAccount(addr)
if err != nil {
panic(err)
}

An example result would be as follows:

fmt.Println(accountRes)
// Output:
// Account:
// Address: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5
// Pubkey:
// Coins: 9999862000000ugnot
// AccountNumber: 0
// Sequence: 0

We are now ready to send a transaction to the chain.

Sending a transaction

A gno.land transaction consists of two main parts:

  • A set of base transaction fields, such as a gas price, gas limit, account & sequence number,
  • An array of messages to be executed on the chain.

To construct the base set of transaction fields, we can use the BaseTxCfg type:

txCfg := gnoclient.BaseTxCfg{
GasFee: "1000000ugnot", // gas price
GasWanted: 1000000, // gas limit
AccountNumber: accountRes.GetAccountNumber(), // account ID
SequenceNumber: accountRes.GetSequence(), // account nonce
Memo: "This is a cool how-to guide!", // transaction memo
}

For calling an exported (public) function in a Gno realm, we can use the MsgCall message type. We will use the wrapped ugnot realm for this example, wrapping 1000000ugnot (1 $GNOT) for demonstration purposes.

import (
...
"github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
"github.com/gnolang/gno/tm2/pkg/std"
)
msg := vm.MsgCall{
Caller: addr, // address of the caller (signer)
PkgPath: "gno.land/r/demo/wugnot", // wrapped ugnot realm path
Func: "Deposit", // function to call
Args: nil, // arguments in string format
Send: std.Coins{{Denom: ugnot.Denom, Amount: int64(1000000)}}, // coins to send along with transaction
}

Finally, to actually call the function, we can use Call:

res, err := client.Call(txCfg, msg)
if err != nil {
panic(err)
}

Before running your code, make sure your keypair has enough funds to send the transaction.

If everything went well, you've just sent a state-changing transaction to a gno.land chain!

Reading on-chain state

To read on-chain state, you can use the QEval() function. This functionality allows you to evaluate a query expression on a realm, without having to spend gas.

Let's fetch the balance of wrapped ugnot for our address:

// Evaluate expression
qevalRes, _, err := client.QEval("gno.land/r/demo/wugnot", "BalanceOf(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")")
if err != nil {
panic(err)
}

Printing out the result should output:

fmt.Println(qevalRes)
// Output:
// (1000000 uint64)

To see all functionality the gnoclient package provides, see the gnoclient reference page.

Conclusion

Congratulations 🎉

You've just built a small demo app in Go that connects to a gno.land chain to query account info, send a transaction, and read on-chain state.

Check out the full example app code here.

To see a real-world example CLI tool use gnoclient, check out gnoblog-cli.