coins.gno

6.70 Kb · 231 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	"gno.land/p/moul/realmpath"
 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/{address}", func(res *mux.ResponseWriter, req *mux.Request) {
 46		res.Write(renderAllBalances(req.RawPath, req.GetVar("address")))
 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 := std.NewBanker(std.BankerTypeReadonly)
 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.
 73Here are a few examples on how to use it:
 74
 75- ~/r/gnoland/coins:balances/<address>~ - show full list of coin balances of an address
 76	- [Example](/r/gnoland/coins:balances/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5)
 77- ~/r/gnoland/coins:balances/<address>?coin=ugnot~ - shows the balance of an address for a specific coin
 78	- [Example](/r/gnoland/coins:balances/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5?coin=ugnot)
 79- ~/r/gnoland/coins:convert/<cosmos_address>~ - convert Cosmos address to Gno address
 80	- [Example](/r/gnoland/coins:convert/cosmos1jg8mtutu9khhfwc4nxmuhcpftf0pajdh6svrgs)
 81- ~/r/gnoland/coins:supply/<denom>~ - shows the total supply of denom
 82	- Coming soon!
 83`, "~", "`", -1)
 84}
 85
 86func renderConvertedAddress(addr string) string {
 87	out := "# Address converter\n\n"
 88
 89	gnoAddress, err := ctg.ConvertCosmosToGno(addr)
 90	if err != nil {
 91		out += err.Error()
 92		return out
 93	}
 94
 95	user, _ := users.ResolveAny(gnoAddress.String())
 96	name := "`" + gnoAddress.String() + "`"
 97	if user != nil {
 98		name = user.RenderLink("")
 99	}
100
101	out += ufmt.Sprintf("`%s` on Cosmos matches %s on gno.land.\n\n", addr, name)
102	out += "[View `ugnot` balance for this address](/r/gnoland/coins:balances/" + gnoAddress.String() + "?coin=ugnot)\n\n"
103	out += "[View full balance list for this address](/r/gnoland/coins:balances/" + gnoAddress.String() + ")"
104	return out
105}
106
107func renderSingleCoinBalance(banker std.Banker, denom string, addr string) string {
108	out := "# Single coin balance\n\n"
109	if !std.Address(addr).IsValid() {
110		out += "Invalid address."
111		return out
112	}
113
114	user, _ := users.ResolveAny(addr)
115	name := "`" + addr + "`"
116	if user != nil {
117		name = user.RenderLink("")
118	}
119
120	out += ufmt.Sprintf("%s has `%d%s` at block #%d\n\n",
121		name, banker.GetCoins(std.Address(addr)).AmountOf(denom), denom, std.ChainHeight())
122
123	out += "[View full balance list for this address](/r/gnoland/coins:balances/" + addr + ")"
124
125	return out
126}
127
128func renderAllBalances(rawpath, input string) string {
129	out := "# Balances\n\n"
130
131	if strings.HasPrefix(input, "cosmos") {
132		addr, err := ctg.ConvertCosmosToGno(input)
133		if err != nil {
134			out += "Tried converting a Cosmos address to a Gno address but failed. Please double-scheck your input."
135			return out
136		}
137		out += ufmt.Sprintf("> [!NOTE]\n>  Automatically converted `%s` to its Gno equivalent.\n\n", input)
138		input = addr.String()
139	} else {
140		if !std.Address(input).IsValid() {
141			out += "Invalid address."
142			return out
143		}
144	}
145
146	user, _ := users.ResolveAny(input)
147	name := "`" + input + "`"
148	if user != nil {
149		name = user.RenderLink("")
150	}
151
152	banker := std.NewBanker(std.BankerTypeReadonly)
153	out += ufmt.Sprintf("This page shows full coin balances of %s at block #%d\n\n",
154		name, std.ChainHeight())
155
156	req := realmpath.Parse(rawpath)
157
158	coin := req.Query.Get("coin")
159	if coin != "" {
160		return renderSingleCoinBalance(banker, coin, input)
161	}
162
163	balances := banker.GetCoins(std.Address(input))
164
165	// Determine sorting
166	if getSortField(req) == "balance" {
167		coinsort.SortByBalance(balances)
168	}
169
170	// Create table
171	denomColumn := renderSortLink(req, "denom", "Denomination")
172	balanceColumn := renderSortLink(req, "balance", "Balance")
173	table := mdtable.Table{
174		Headers: []string{denomColumn, balanceColumn},
175	}
176
177	if isSortReversed(req) {
178		for _, b := range balances {
179			table.Append([]string{b.Denom, strconv.Itoa(int(b.Amount))})
180		}
181	} else {
182		for i := len(balances) - 1; i >= 0; i-- {
183			table.Append([]string{balances[i].Denom, strconv.Itoa(int(balances[i].Amount))})
184		}
185	}
186
187	out += table.String() + "\n\n"
188	return out
189}
190
191// Helper functions for sorting and pagination
192func getSortField(req *realmpath.Request) string {
193	field := req.Query.Get("sort")
194	switch field {
195	case "denom", "balance": // XXX: add Coins.SortBy{denom,bal} methods
196		return field
197	}
198	return "denom"
199}
200
201func isSortReversed(req *realmpath.Request) bool {
202	return req.Query.Get("order") != "asc"
203}
204
205func renderSortLink(req *realmpath.Request, field, label string) string {
206	currentField := getSortField(req)
207	currentOrder := req.Query.Get("order")
208
209	newOrder := "desc"
210	if field == currentField && currentOrder != "asc" {
211		newOrder = "asc"
212	}
213
214	query := make(url.Values)
215	for k, vs := range req.Query {
216		query[k] = append([]string(nil), vs...)
217	}
218
219	query.Set("sort", field)
220	query.Set("order", newOrder)
221
222	if field == currentField {
223		if currentOrder == "asc" {
224			label += " ↑"
225		} else {
226			label += " ↓"
227		}
228	}
229
230	return md.Link(label, "?"+query.Encode())
231}