1// Package coins provides simple helpers to retrieve information about coins2// on the Gno.land blockchain.3//4// The primary goal of this realm is to allow users to check their token balances without5// relying on external tools or services. This is particularly valuable for new networks6// that aren't yet widely supported by public explorers or wallets. By using this realm,7// users can always access their balance information directly through the gnodev.8//9// While currently focused on basic balance checking functionality, this realm could10// potentially be extended to support other banker-related workflows in the future.11// However, we aim to keep it minimal and focused on its core purpose.12//13// This is a "Render-only realm" - it exposes only a Render function as its public14// interface and doesn't maintain any state of its own. This pattern allows for15// simple, stateless information retrieval directly through the blockchain's16// rendering capabilities.17package coins1819import (20 "chain/banker"21 "chain/runtime"22 "net/url"23 "strconv"24 "strings"2526 "gno.land/p/leon/coinsort/v0"27 "gno.land/p/leon/ctg/v0"28 "gno.land/p/moul/md/v0"29 "gno.land/p/moul/mdtable/v0"30 "gno.land/p/nt/mux/v0"31 "gno.land/p/nt/ufmt/v0"3233 "gno.land/r/sys/users"34)3536var router *mux.Router3738func init() {39 router = mux.NewRouter()4041 router.HandleFunc("", func(res *mux.ResponseWriter, req *mux.Request) {42 res.Write(renderHomepage())43 })4445 router.HandleFunc("balances", func(res *mux.ResponseWriter, req *mux.Request) {46 res.Write(renderBalances(req))47 })4849 router.HandleFunc("convert/{address}", func(res *mux.ResponseWriter, req *mux.Request) {50 res.Write(renderConvertedAddress(req.GetVar("address")))51 })5253 // Coin info54 router.HandleFunc("supply/{denom}", func(res *mux.ResponseWriter, req *mux.Request) {55 // banker := banker.NewReadonlyBanker()56 // res.Write(renderAddressBalance(banker, denom, denom))57 res.Write("The total supply feature is coming soon.")58 })5960 router.NotFoundHandler = func(res *mux.ResponseWriter, req *mux.Request) {61 res.Write("# 404\n\nThat page was not found. Would you like to [**go home**?](/r/gnoland/coins)")62 }63}6465func Render(path string) string {66 return router.Render(path)67}6869func renderHomepage() string {70 return strings.Replace(`# Gno.land Coins Explorer7172This is a simple, readonly realm that allows users to browse native coin balances. Check your coin balance below!7374<gno-form path="balances">75 <gno-input name="address" type="text" placeholder="Valid bech32 address (e.g. g1..., cosmos1..., osmo1...)" />76 <gno-input name="coin" type="text" placeholder="Coin (e.g. ugnot)"" />77</gno-form>7879Here are a few more ways to use this app:8081- ~/r/gnoland/coins:balances?address=g1...~ - show full list of coin balances of an address82 - [Example](/r/gnoland/coins:balances?address=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5)83- ~/r/gnoland/coins:balances?address=g1...&coin=ugnot~ - shows the balance of an address for a specific coin84 - [Example](/r/gnoland/coins:balances?address=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5&coin=ugnot)85- ~/r/gnoland/coins:convert/<bech32_addr>~ - convert a bech32 address to a Gno address86 - [Example](/r/gnoland/coins:convert/cosmos1jg8mtutu9khhfwc4nxmuhcpftf0pajdh6svrgs)87- ~/r/gnoland/coins:supply/<denom>~ - shows the total supply of denom88 - Coming soon!8990`, "~", "`", -1)91}9293func renderBalances(req *mux.Request) string {94 out := "# Balances\n\n"9596 input := req.Query.Get("address")97 coin := req.Query.Get("coin")9899 if input == "" && coin == "" {100 out += "Please input a valid address and coin denomination.\n\n"101 return out102 }103104 if input == "" {105 out += "Please input a valid bech32 address.\n\n"106 return out107 }108109 originalInput := input110 var wasConverted bool111112 // Try to validate or convert113 if !address(input).IsValid() {114 addr, err := ctg.ConvertAnyToGno(input)115 if err != nil {116 return out + ufmt.Sprintf("Tried converting `%s` to a Gno address but failed. Please try with a valid bech32 address.\n\n", input)117 }118 input = addr.String()119 wasConverted = true120 }121122 if wasConverted {123 out += ufmt.Sprintf("> [!NOTE]\n> Automatically converted `%s` to its Gno equivalent.\n\n", originalInput)124 }125126 banker_ := banker.NewReadonlyBanker()127 balances := banker_.GetCoins(address(input))128129 if len(balances) == 0 {130 out += "This address currently has no coins."131 return out132 }133134 if coin != "" {135 return renderSingleCoinBalance(coin, input, originalInput, wasConverted, balances.AmountOf(coin))136 }137138 user, _ := users.ResolveAny(input)139 name := "`" + input + "`"140 if user != nil {141 name = user.RenderLink("")142 }143144 out += ufmt.Sprintf("This page shows full coin balances of %s at block #%d\n\n",145 name, runtime.ChainHeight())146147 // Determine sorting148 if getSortField(req) == "balance" {149 coinsort.SortByBalance(balances)150 }151152 // Create table153 denomColumn := renderSortLink(req, "denom", "Denomination")154 balanceColumn := renderSortLink(req, "balance", "Balance")155 table := mdtable.Table{156 Headers: []string{denomColumn, balanceColumn},157 }158159 if isSortReversed(req) {160 for _, b := range balances {161 table.Append([]string{b.Denom, strconv.Itoa(int(b.Amount))})162 }163 } else {164 for i := len(balances) - 1; i >= 0; i-- {165 table.Append([]string{balances[i].Denom, strconv.Itoa(int(balances[i].Amount))})166 }167 }168169 out += table.String() + "\n\n"170 return out171}172173// amount is taken from the balances the caller already read, rather than read again.174// Beyond saving the read, it keeps an unvalidated denom out of the banker: denom is175// the "coin" query parameter, and GetCoin panics on a malformed one, so on a render176// path any URL could otherwise break the page.177func renderSingleCoinBalance(denom, addr, origInput string, wasConverted bool, amount int64) string {178 out := "# Coin balance\n\n"179180 if wasConverted {181 out += ufmt.Sprintf("> [!NOTE]\n> Automatically converted `%s` to its Gno equivalent.\n\n", origInput)182 }183184 user, _ := users.ResolveAny(addr)185 name := "`" + addr + "`"186 if user != nil {187 name = user.RenderLink("")188 }189190 out += ufmt.Sprintf("%s has `%d%s` at block #%d\n\n",191 name, amount, denom, runtime.ChainHeight())192193 out += "[View full balance list for this address](/r/gnoland/coins:balances?address=" + addr + ")"194195 return out196}197198func renderConvertedAddress(addr string) string {199 out := "# Address converter\n\n"200201 gnoAddress, err := ctg.ConvertAnyToGno(addr)202 if err != nil {203 out += err.Error()204 return out205 }206207 user, _ := users.ResolveAny(gnoAddress.String())208 name := "`" + gnoAddress.String() + "`"209 if user != nil {210 name = user.RenderLink("")211 }212213 out += ufmt.Sprintf("`%s` on Cosmos matches %s on gno.land.\n\n", addr, name)214 out += "[[View `ugnot` balance for this address]](/r/gnoland/coins:balances?address=" + gnoAddress.String() + "&coin=ugnot) - "215 out += "[[View full balance list for this address]](/r/gnoland/coins:balances?address=" + gnoAddress.String() + ")"216 return out217}218219// Helper functions for sorting and pagination220func getSortField(req *mux.Request) string {221 field := req.Query.Get("sort")222 switch field {223 case "denom", "balance":224 return field225 }226 return "denom"227}228229func isSortReversed(req *mux.Request) bool {230 return req.Query.Get("order") != "asc"231}232233func renderSortLink(req *mux.Request, field, label string) string {234 currentField := getSortField(req)235 currentOrder := req.Query.Get("order")236237 newOrder := "desc"238 if field == currentField && currentOrder != "asc" {239 newOrder = "asc"240 }241242 query := make(url.Values)243 for k, vs := range req.Query {244 query[k] = append([]string(nil), vs...)245 }246247 query.Set("sort", field)248 query.Set("order", newOrder)249250 if field == currentField {251 if currentOrder == "asc" {252 label += " ↑"253 } else {254 label += " ↓"255 }256 }257258 return md.Link(label, "?"+query.Encode())259}260Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.