Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

coins.gno

7.57 Kb · 259 lines
  1// Package coins provides simple helpers to retrieve information about coins
  2// on the Gno.land blockchain.
  3//
  4// The primary goal of this realm is to allow users to check their token balances without
  5// relying on external tools or services. This is particularly valuable for new networks
  6// 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 could
 10// 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 public
 14// interface and doesn't maintain any state of its own. This pattern allows for
 15// simple, stateless information retrieval directly through the blockchain's
 16// rendering capabilities.
 17package coins
 18
 19import (
 20	"chain/banker"
 21	"chain/runtime"
 22	"net/url"
 23	"strconv"
 24	"strings"
 25
 26	"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"
 32
 33	"gno.land/r/sys/users"
 34)
 35
 36var router *mux.Router
 37
 38func init() {
 39	router = mux.NewRouter()
 40
 41	router.HandleFunc("", func(res *mux.ResponseWriter, req *mux.Request) {
 42		res.Write(renderHomepage())
 43	})
 44
 45	router.HandleFunc("balances", func(res *mux.ResponseWriter, req *mux.Request) {
 46		res.Write(renderBalances(req))
 47	})
 48
 49	router.HandleFunc("convert/{address}", func(res *mux.ResponseWriter, req *mux.Request) {
 50		res.Write(renderConvertedAddress(req.GetVar("address")))
 51	})
 52
 53	// Coin info
 54	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	})
 59
 60	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}
 64
 65func Render(path string) string {
 66	return router.Render(path)
 67}
 68
 69func renderHomepage() string {
 70	return strings.Replace(`# Gno.land Coins Explorer
 71
 72This is a simple, readonly realm that allows users to browse native coin balances. Check your coin balance below!
 73
 74<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>
 78
 79Here are a few more ways to use this app:
 80
 81- ~/r/gnoland/coins:balances?address=g1...~ - show full list of coin balances of an address
 82	- [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 coin
 84	- [Example](/r/gnoland/coins:balances?address=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5&coin=ugnot)
 85- ~/r/gnoland/coins:convert/<bech32_addr>~ - convert a bech32 address to a Gno address
 86	- [Example](/r/gnoland/coins:convert/cosmos1jg8mtutu9khhfwc4nxmuhcpftf0pajdh6svrgs)
 87- ~/r/gnoland/coins:supply/<denom>~ - shows the total supply of denom
 88	- Coming soon!
 89
 90`, "~", "`", -1)
 91}
 92
 93func renderBalances(req *mux.Request) string {
 94	out := "# Balances\n\n"
 95
 96	input := req.Query.Get("address")
 97	coin := req.Query.Get("coin")
 98
 99	if input == "" && coin == "" {
100		out += "Please input a valid address and coin denomination.\n\n"
101		return out
102	}
103
104	if input == "" {
105		out += "Please input a valid bech32 address.\n\n"
106		return out
107	}
108
109	originalInput := input
110	var wasConverted bool
111
112	// Try to validate or convert
113	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 = true
120	}
121
122	if wasConverted {
123		out += ufmt.Sprintf("> [!NOTE]\n>  Automatically converted `%s` to its Gno equivalent.\n\n", originalInput)
124	}
125
126	banker_ := banker.NewReadonlyBanker()
127	balances := banker_.GetCoins(address(input))
128
129	if len(balances) == 0 {
130		out += "This address currently has no coins."
131		return out
132	}
133
134	if coin != "" {
135		return renderSingleCoinBalance(coin, input, originalInput, wasConverted, balances.AmountOf(coin))
136	}
137
138	user, _ := users.ResolveAny(input)
139	name := "`" + input + "`"
140	if user != nil {
141		name = user.RenderLink("")
142	}
143
144	out += ufmt.Sprintf("This page shows full coin balances of %s at block #%d\n\n",
145		name, runtime.ChainHeight())
146
147	// Determine sorting
148	if getSortField(req) == "balance" {
149		coinsort.SortByBalance(balances)
150	}
151
152	// Create table
153	denomColumn := renderSortLink(req, "denom", "Denomination")
154	balanceColumn := renderSortLink(req, "balance", "Balance")
155	table := mdtable.Table{
156		Headers: []string{denomColumn, balanceColumn},
157	}
158
159	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	}
168
169	out += table.String() + "\n\n"
170	return out
171}
172
173// 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 is
175// the "coin" query parameter, and GetCoin panics on a malformed one, so on a render
176// 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"
179
180	if wasConverted {
181		out += ufmt.Sprintf("> [!NOTE]\n>  Automatically converted `%s` to its Gno equivalent.\n\n", origInput)
182	}
183
184	user, _ := users.ResolveAny(addr)
185	name := "`" + addr + "`"
186	if user != nil {
187		name = user.RenderLink("")
188	}
189
190	out += ufmt.Sprintf("%s has `%d%s` at block #%d\n\n",
191		name, amount, denom, runtime.ChainHeight())
192
193	out += "[View full balance list for this address](/r/gnoland/coins:balances?address=" + addr + ")"
194
195	return out
196}
197
198func renderConvertedAddress(addr string) string {
199	out := "# Address converter\n\n"
200
201	gnoAddress, err := ctg.ConvertAnyToGno(addr)
202	if err != nil {
203		out += err.Error()
204		return out
205	}
206
207	user, _ := users.ResolveAny(gnoAddress.String())
208	name := "`" + gnoAddress.String() + "`"
209	if user != nil {
210		name = user.RenderLink("")
211	}
212
213	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 out
217}
218
219// Helper functions for sorting and pagination
220func getSortField(req *mux.Request) string {
221	field := req.Query.Get("sort")
222	switch field {
223	case "denom", "balance":
224		return field
225	}
226	return "denom"
227}
228
229func isSortReversed(req *mux.Request) bool {
230	return req.Query.Get("order") != "asc"
231}
232
233func renderSortLink(req *mux.Request, field, label string) string {
234	currentField := getSortField(req)
235	currentOrder := req.Query.Get("order")
236
237	newOrder := "desc"
238	if field == currentField && currentOrder != "asc" {
239		newOrder = "asc"
240	}
241
242	query := make(url.Values)
243	for k, vs := range req.Query {
244		query[k] = append([]string(nil), vs...)
245	}
246
247	query.Set("sort", field)
248	query.Set("order", newOrder)
249
250	if field == currentField {
251		if currentOrder == "asc" {
252			label += " ↑"
253		} else {
254			label += " ↓"
255		}
256	}
257
258	return md.Link(label, "?"+query.Encode())
259}