Search Apps Documentation Source Content File Folder Download Copy Actions Download

coins.gno

7.34 Kb · 256 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"
 27	"gno.land/p/leon/ctg"
 28	"gno.land/p/moul/md"
 29	"gno.land/p/moul/mdtable"
 30	"gno.land/p/nt/mux"
 31	"gno.land/p/nt/ufmt"
 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 := 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. 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.NewBanker(banker.BankerTypeReadonly)
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)
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
173func renderSingleCoinBalance(denom, addr, origInput string, wasConverted bool) string {
174	out := "# Coin balance\n\n"
175	banker_ := banker.NewBanker(banker.BankerTypeReadonly)
176
177	if wasConverted {
178		out += ufmt.Sprintf("> [!NOTE]\n>  Automatically converted `%s` to its Gno equivalent.\n\n", origInput)
179	}
180
181	user, _ := users.ResolveAny(addr)
182	name := "`" + addr + "`"
183	if user != nil {
184		name = user.RenderLink("")
185	}
186
187	out += ufmt.Sprintf("%s has `%d%s` at block #%d\n\n",
188		name, banker_.GetCoins(address(addr)).AmountOf(denom), denom, runtime.ChainHeight())
189
190	out += "[View full balance list for this address](/r/gnoland/coins:balances?address=" + addr + ")"
191
192	return out
193}
194
195func renderConvertedAddress(addr string) string {
196	out := "# Address converter\n\n"
197
198	gnoAddress, err := ctg.ConvertAnyToGno(addr)
199	if err != nil {
200		out += err.Error()
201		return out
202	}
203
204	user, _ := users.ResolveAny(gnoAddress.String())
205	name := "`" + gnoAddress.String() + "`"
206	if user != nil {
207		name = user.RenderLink("")
208	}
209
210	out += ufmt.Sprintf("`%s` on Cosmos matches %s on gno.land.\n\n", addr, name)
211	out += "[[View `ugnot` balance for this address]](/r/gnoland/coins:balances?address=" + gnoAddress.String() + "&coin=ugnot) - "
212	out += "[[View full balance list for this address]](/r/gnoland/coins:balances?address=" + gnoAddress.String() + ")"
213	return out
214}
215
216// Helper functions for sorting and pagination
217func getSortField(req *mux.Request) string {
218	field := req.Query.Get("sort")
219	switch field {
220	case "denom", "balance":
221		return field
222	}
223	return "denom"
224}
225
226func isSortReversed(req *mux.Request) bool {
227	return req.Query.Get("order") != "asc"
228}
229
230func renderSortLink(req *mux.Request, field, label string) string {
231	currentField := getSortField(req)
232	currentOrder := req.Query.Get("order")
233
234	newOrder := "desc"
235	if field == currentField && currentOrder != "asc" {
236		newOrder = "asc"
237	}
238
239	query := make(url.Values)
240	for k, vs := range req.Query {
241		query[k] = append([]string(nil), vs...)
242	}
243
244	query.Set("sort", field)
245	query.Set("order", newOrder)
246
247	if field == currentField {
248		if currentOrder == "asc" {
249			label += " ↑"
250		} else {
251			label += " ↓"
252		}
253	}
254
255	return md.Link(label, "?"+query.Encode())
256}