// 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/p/moul/realmpath"
"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/{address}", func(res *mux.ResponseWriter, req *mux.Request) {
res.Write(renderAllBalances(req.RawPath, req.GetVar("address")))
})
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.
Here are a few examples on how to use it:
- ~/r/gnoland/coins:balances/
~ - show full list of coin balances of an address
- [Example](/r/gnoland/coins:balances/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5)
- ~/r/gnoland/coins:balances/?coin=ugnot~ - shows the balance of an address for a specific coin
- [Example](/r/gnoland/coins:balances/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5?coin=ugnot)
- ~/r/gnoland/coins:convert/~ - convert Cosmos address to Gno address
- [Example](/r/gnoland/coins:convert/cosmos1jg8mtutu9khhfwc4nxmuhcpftf0pajdh6svrgs)
- ~/r/gnoland/coins:supply/~ - shows the total supply of denom
- Coming soon!
`, "~", "`", -1)
}
func renderConvertedAddress(addr string) string {
out := "# Address converter\n\n"
gnoAddress, err := ctg.ConvertCosmosToGno(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/" + gnoAddress.String() + "?coin=ugnot)\n\n"
out += "[View full balance list for this address](/r/gnoland/coins:balances/" + gnoAddress.String() + ")"
return out
}
func renderSingleCoinBalance(banker std.Banker, denom string, addr string) string {
out := "# Single coin balance\n\n"
if !std.Address(addr).IsValid() {
out += "Invalid address."
return out
}
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/" + addr + ")"
return out
}
func renderAllBalances(rawpath, input string) string {
out := "# Balances\n\n"
if strings.HasPrefix(input, "cosmos") {
addr, err := ctg.ConvertCosmosToGno(input)
if err != nil {
out += "Tried converting a Cosmos address to a Gno address but failed. Please double-scheck your input."
return out
}
out += ufmt.Sprintf("> [!NOTE]\n> Automatically converted `%s` to its Gno equivalent.\n\n", input)
input = addr.String()
} else {
if !std.Address(input).IsValid() {
out += "Invalid address."
return out
}
}
user, _ := users.ResolveAny(input)
name := "`" + input + "`"
if user != nil {
name = user.RenderLink("")
}
banker := std.NewBanker(std.BankerTypeReadonly)
out += ufmt.Sprintf("This page shows full coin balances of %s at block #%d\n\n",
name, std.ChainHeight())
req := realmpath.Parse(rawpath)
coin := req.Query.Get("coin")
if coin != "" {
return renderSingleCoinBalance(banker, coin, input)
}
balances := banker.GetCoins(std.Address(input))
// 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
}
// Helper functions for sorting and pagination
func getSortField(req *realmpath.Request) string {
field := req.Query.Get("sort")
switch field {
case "denom", "balance": // XXX: add Coins.SortBy{denom,bal} methods
return field
}
return "denom"
}
func isSortReversed(req *realmpath.Request) bool {
return req.Query.Get("order") != "asc"
}
func renderSortLink(req *realmpath.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())
}