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}