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}