// Package coins provides simple helpers to retrieve information about coins
// on the Gno.land blockchain.
//
// The primary goal of this realm is to allow users to check their token balances without
// relying on external tools or services. This is particularly valuable for new networks
// that aren't yet widely supported by public explorers or wallets. By using this realm,
// users can always access their balance information directly through the gnodev.
//
// While currently focused on basic balance checking functionality, this realm could
// potentially be extended to support other banker-related workflows in the future.
// However, we aim to keep it minimal and focused on its core purpose.
//
// This is a "Render-only realm" - it exposes only a Render function as its public
// interface and doesn't maintain any state of its own. This pattern allows for
// simple, stateless information retrieval directly through the blockchain's
// rendering capabilities.
package coins
import (
"net/url"
"std"
"strconv"
"strings"
"gno.land/p/demo/mux"
"gno.land/p/demo/ufmt"
"gno.land/p/leon/coinsort"
"gno.land/p/leon/ctg"
"gno.land/p/moul/md"
"gno.land/p/moul/mdtable"
"gno.land/r/sys/users"
)
var router *mux.Router
func init() {
router = mux.NewRouter()
router.HandleFunc("", func(res *mux.ResponseWriter, req *mux.Request) {
res.Write(renderHomepage())
})
router.HandleFunc("balances", func(res *mux.ResponseWriter, req *mux.Request) {
res.Write(renderBalances(req))
})
router.HandleFunc("convert/{address}", func(res *mux.ResponseWriter, req *mux.Request) {
res.Write(renderConvertedAddress(req.GetVar("address")))
})
// Coin info
router.HandleFunc("supply/{denom}", func(res *mux.ResponseWriter, req *mux.Request) {
// banker := std.NewBanker(std.BankerTypeReadonly)
// res.Write(renderAddressBalance(banker, denom, denom))
res.Write("The total supply feature is coming soon.")
})
router.NotFoundHandler = func(res *mux.ResponseWriter, req *mux.Request) {
res.Write("# 404\n\nThat page was not found. Would you like to [**go home**?](/r/gnoland/coins)")
}
}
func Render(path string) string {
return router.Render(path)
}
func renderHomepage() string {
return strings.Replace(`# Gno.land Coins Explorer
This is a simple, readonly realm that allows users to browse native coin balances. Check your coin balance below!
Here are a few more ways to use this app:
- ~/r/gnoland/coins:balances?address=g1...~ - show full list of coin balances of an address
- [Example](/r/gnoland/coins:balances?address=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5)
- ~/r/gnoland/coins:balances?address=g1...&coin=ugnot~ - shows the balance of an address for a specific coin
- [Example](/r/gnoland/coins:balances?address=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5&coin=ugnot)
- ~/r/gnoland/coins:convert/~ - convert a bech32 address to a Gno address
- [Example](/r/gnoland/coins:convert/cosmos1jg8mtutu9khhfwc4nxmuhcpftf0pajdh6svrgs)
- ~/r/gnoland/coins:supply/~ - shows the total supply of denom
- Coming soon!
`, "~", "`", -1)
}
func renderBalances(req *mux.Request) string {
out := "# Balances\n\n"
input := req.Query.Get("address")
coin := req.Query.Get("coin")
if input == "" && coin == "" {
out += "Please input a valid address and coin denomination.\n\n"
return out
}
if input == "" {
out += "Please input a valid bech32 address.\n\n"
return out
}
originalInput := input
var wasConverted bool
// Try to validate or convert
if !std.Address(input).IsValid() {
addr, err := ctg.ConvertAnyToGno(input)
if err != nil {
return out + ufmt.Sprintf("Tried converting `%s` to a Gno address but failed. Please try with a valid bech32 address.\n\n", input)
}
input = addr.String()
wasConverted = true
}
if wasConverted {
out += ufmt.Sprintf("> [!NOTE]\n> Automatically converted `%s` to its Gno equivalent.\n\n", originalInput)
}
banker := std.NewBanker(std.BankerTypeReadonly)
balances := banker.GetCoins(std.Address(input))
if len(balances) == 0 {
out += "This address currently has no coins."
return out
}
if coin != "" {
return renderSingleCoinBalance(coin, input, originalInput, wasConverted)
}
user, _ := users.ResolveAny(input)
name := "`" + input + "`"
if user != nil {
name = user.RenderLink("")
}
out += ufmt.Sprintf("This page shows full coin balances of %s at block #%d\n\n",
name, std.ChainHeight())
// Determine sorting
if getSortField(req) == "balance" {
coinsort.SortByBalance(balances)
}
// Create table
denomColumn := renderSortLink(req, "denom", "Denomination")
balanceColumn := renderSortLink(req, "balance", "Balance")
table := mdtable.Table{
Headers: []string{denomColumn, balanceColumn},
}
if isSortReversed(req) {
for _, b := range balances {
table.Append([]string{b.Denom, strconv.Itoa(int(b.Amount))})
}
} else {
for i := len(balances) - 1; i >= 0; i-- {
table.Append([]string{balances[i].Denom, strconv.Itoa(int(balances[i].Amount))})
}
}
out += table.String() + "\n\n"
return out
}
func renderSingleCoinBalance(denom, addr, origInput string, wasConverted bool) string {
out := "# Coin balance\n\n"
banker := std.NewBanker(std.BankerTypeReadonly)
if wasConverted {
out += ufmt.Sprintf("> [!NOTE]\n> Automatically converted `%s` to its Gno equivalent.\n\n", origInput)
}
user, _ := users.ResolveAny(addr)
name := "`" + addr + "`"
if user != nil {
name = user.RenderLink("")
}
out += ufmt.Sprintf("%s has `%d%s` at block #%d\n\n",
name, banker.GetCoins(std.Address(addr)).AmountOf(denom), denom, std.ChainHeight())
out += "[View full balance list for this address](/r/gnoland/coins:balances?address=" + addr + ")"
return out
}
func renderConvertedAddress(addr string) string {
out := "# Address converter\n\n"
gnoAddress, err := ctg.ConvertAnyToGno(addr)
if err != nil {
out += err.Error()
return out
}
user, _ := users.ResolveAny(gnoAddress.String())
name := "`" + gnoAddress.String() + "`"
if user != nil {
name = user.RenderLink("")
}
out += ufmt.Sprintf("`%s` on Cosmos matches %s on gno.land.\n\n", addr, name)
out += "[[View `ugnot` balance for this address]](/r/gnoland/coins:balances?address=" + gnoAddress.String() + "&coin=ugnot) - "
out += "[[View full balance list for this address]](/r/gnoland/coins:balances?address=" + gnoAddress.String() + ")"
return out
}
// Helper functions for sorting and pagination
func getSortField(req *mux.Request) string {
field := req.Query.Get("sort")
switch field {
case "denom", "balance":
return field
}
return "denom"
}
func isSortReversed(req *mux.Request) bool {
return req.Query.Get("order") != "asc"
}
func renderSortLink(req *mux.Request, field, label string) string {
currentField := getSortField(req)
currentOrder := req.Query.Get("order")
newOrder := "desc"
if field == currentField && currentOrder != "asc" {
newOrder = "asc"
}
query := make(url.Values)
for k, vs := range req.Query {
query[k] = append([]string(nil), vs...)
}
query.Set("sort", field)
query.Set("order", newOrder)
if field == currentField {
if currentOrder == "asc" {
label += " ↑"
} else {
label += " ↓"
}
}
return md.Link(label, "?"+query.Encode())
}