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}