coins.gno

7.31 Kb · 255 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	"net/url"
 21	"std"
 22	"strconv"
 23	"strings"
 24
 25	"gno.land/p/demo/mux"
 26	"gno.land/p/demo/ufmt"
 27	"gno.land/p/leon/coinsort"
 28	"gno.land/p/leon/ctg"
 29	"gno.land/p/moul/md"
 30	"gno.land/p/moul/mdtable"
 31
 32	"gno.land/r/sys/users"
 33)
 34
 35var router *mux.Router
 36
 37func init() {
 38	router = mux.NewRouter()
 39
 40	router.HandleFunc("", func(res *mux.ResponseWriter, req *mux.Request) {
 41		res.Write(renderHomepage())
 42	})
 43
 44	router.HandleFunc("balances", func(res *mux.ResponseWriter, req *mux.Request) {
 45		res.Write(renderBalances(req))
 46	})
 47
 48	router.HandleFunc("convert/{address}", func(res *mux.ResponseWriter, req *mux.Request) {
 49		res.Write(renderConvertedAddress(req.GetVar("address")))
 50	})
 51
 52	// Coin info
 53	router.HandleFunc("supply/{denom}", func(res *mux.ResponseWriter, req *mux.Request) {
 54		// banker := std.NewBanker(std.BankerTypeReadonly)
 55		// res.Write(renderAddressBalance(banker, denom, denom))
 56		res.Write("The total supply feature is coming soon.")
 57	})
 58
 59	router.NotFoundHandler = func(res *mux.ResponseWriter, req *mux.Request) {
 60		res.Write("# 404\n\nThat page was not found. Would you like to [**go home**?](/r/gnoland/coins)")
 61	}
 62}
 63
 64func Render(path string) string {
 65	return router.Render(path)
 66}
 67
 68func renderHomepage() string {
 69	return strings.Replace(`# Gno.land Coins Explorer
 70
 71This is a simple, readonly realm that allows users to browse native coin balances. Check your coin balance below!
 72
 73<gno-form path="balances">
 74	<gno-input name="address" type="text" placeholder="Valid bech32 address (e.g. g1..., cosmos1..., osmo1...)" />
 75	<gno-input name="coin" type="text" placeholder="Coin (e.g. ugnot)"" />
 76</gno-form>
 77
 78Here are a few more ways to use this app:
 79
 80- ~/r/gnoland/coins:balances?address=g1...~ - show full list of coin balances of an address
 81	- [Example](/r/gnoland/coins:balances?address=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5)
 82- ~/r/gnoland/coins:balances?address=g1...&coin=ugnot~ - shows the balance of an address for a specific coin
 83	- [Example](/r/gnoland/coins:balances?address=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5&coin=ugnot)
 84- ~/r/gnoland/coins:convert/<bech32_addr>~ - convert a bech32 address to a Gno address
 85	- [Example](/r/gnoland/coins:convert/cosmos1jg8mtutu9khhfwc4nxmuhcpftf0pajdh6svrgs)
 86- ~/r/gnoland/coins:supply/<denom>~ - shows the total supply of denom
 87	- Coming soon!
 88
 89`, "~", "`", -1)
 90}
 91
 92func renderBalances(req *mux.Request) string {
 93	out := "# Balances\n\n"
 94
 95	input := req.Query.Get("address")
 96	coin := req.Query.Get("coin")
 97
 98	if input == "" && coin == "" {
 99		out += "Please input a valid address and coin denomination.\n\n"
100		return out
101	}
102
103	if input == "" {
104		out += "Please input a valid bech32 address.\n\n"
105		return out
106	}
107
108	originalInput := input
109	var wasConverted bool
110
111	// Try to validate or convert
112	if !std.Address(input).IsValid() {
113		addr, err := ctg.ConvertAnyToGno(input)
114		if err != nil {
115			return out + ufmt.Sprintf("Tried converting `%s` to a Gno address but failed. Please try with a valid bech32 address.\n\n", input)
116		}
117		input = addr.String()
118		wasConverted = true
119	}
120
121	if wasConverted {
122		out += ufmt.Sprintf("> [!NOTE]\n>  Automatically converted `%s` to its Gno equivalent.\n\n", originalInput)
123	}
124
125	banker := std.NewBanker(std.BankerTypeReadonly)
126	balances := banker.GetCoins(std.Address(input))
127
128	if len(balances) == 0 {
129		out += "This address currently has no coins."
130		return out
131	}
132
133	if coin != "" {
134		return renderSingleCoinBalance(coin, input, originalInput, wasConverted)
135	}
136
137	user, _ := users.ResolveAny(input)
138	name := "`" + input + "`"
139	if user != nil {
140		name = user.RenderLink("")
141	}
142
143	out += ufmt.Sprintf("This page shows full coin balances of %s at block #%d\n\n",
144		name, std.ChainHeight())
145
146	// Determine sorting
147	if getSortField(req) == "balance" {
148		coinsort.SortByBalance(balances)
149	}
150
151	// Create table
152	denomColumn := renderSortLink(req, "denom", "Denomination")
153	balanceColumn := renderSortLink(req, "balance", "Balance")
154	table := mdtable.Table{
155		Headers: []string{denomColumn, balanceColumn},
156	}
157
158	if isSortReversed(req) {
159		for _, b := range balances {
160			table.Append([]string{b.Denom, strconv.Itoa(int(b.Amount))})
161		}
162	} else {
163		for i := len(balances) - 1; i >= 0; i-- {
164			table.Append([]string{balances[i].Denom, strconv.Itoa(int(balances[i].Amount))})
165		}
166	}
167
168	out += table.String() + "\n\n"
169	return out
170}
171
172func renderSingleCoinBalance(denom, addr, origInput string, wasConverted bool) string {
173	out := "# Coin balance\n\n"
174	banker := std.NewBanker(std.BankerTypeReadonly)
175
176	if wasConverted {
177		out += ufmt.Sprintf("> [!NOTE]\n>  Automatically converted `%s` to its Gno equivalent.\n\n", origInput)
178	}
179
180	user, _ := users.ResolveAny(addr)
181	name := "`" + addr + "`"
182	if user != nil {
183		name = user.RenderLink("")
184	}
185
186	out += ufmt.Sprintf("%s has `%d%s` at block #%d\n\n",
187		name, banker.GetCoins(std.Address(addr)).AmountOf(denom), denom, std.ChainHeight())
188
189	out += "[View full balance list for this address](/r/gnoland/coins:balances?address=" + addr + ")"
190
191	return out
192}
193
194func renderConvertedAddress(addr string) string {
195	out := "# Address converter\n\n"
196
197	gnoAddress, err := ctg.ConvertAnyToGno(addr)
198	if err != nil {
199		out += err.Error()
200		return out
201	}
202
203	user, _ := users.ResolveAny(gnoAddress.String())
204	name := "`" + gnoAddress.String() + "`"
205	if user != nil {
206		name = user.RenderLink("")
207	}
208
209	out += ufmt.Sprintf("`%s` on Cosmos matches %s on gno.land.\n\n", addr, name)
210	out += "[[View `ugnot` balance for this address]](/r/gnoland/coins:balances?address=" + gnoAddress.String() + "&coin=ugnot) - "
211	out += "[[View full balance list for this address]](/r/gnoland/coins:balances?address=" + gnoAddress.String() + ")"
212	return out
213}
214
215// Helper functions for sorting and pagination
216func getSortField(req *mux.Request) string {
217	field := req.Query.Get("sort")
218	switch field {
219	case "denom", "balance":
220		return field
221	}
222	return "denom"
223}
224
225func isSortReversed(req *mux.Request) bool {
226	return req.Query.Get("order") != "asc"
227}
228
229func renderSortLink(req *mux.Request, field, label string) string {
230	currentField := getSortField(req)
231	currentOrder := req.Query.Get("order")
232
233	newOrder := "desc"
234	if field == currentField && currentOrder != "asc" {
235		newOrder = "asc"
236	}
237
238	query := make(url.Values)
239	for k, vs := range req.Query {
240		query[k] = append([]string(nil), vs...)
241	}
242
243	query.Set("sort", field)
244	query.Set("order", newOrder)
245
246	if field == currentField {
247		if currentOrder == "asc" {
248			label += " ↑"
249		} else {
250			label += " ↓"
251		}
252	}
253
254	return md.Link(label, "?"+query.Encode())
255}