|
14 | 14 | // interface and doesn't maintain any state of its own. This pattern allows for
|
15 | 15 | // simple, stateless information retrieval directly through the blockchain's
|
16 | 16 | // rendering capabilities.
|
17 |
| -// |
18 |
| -// Example usage: |
19 |
| -// |
20 |
| -// /r/gnoland/coins:ugnot - shows the total supply of ugnot |
21 |
| -// /r/gnoland/coins:ugnot/g1... - shows the ugnot balance of a specific address |
22 | 17 | package coins
|
23 | 18 |
|
24 | 19 | import (
|
| 20 | + "net/url" |
25 | 21 | "std"
|
| 22 | + "strconv" |
| 23 | + "strings" |
26 | 24 |
|
27 | 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" |
28 | 34 | )
|
29 | 35 |
|
30 | 36 | var router *mux.Router
|
31 | 37 |
|
32 | 38 | func init() {
|
33 | 39 | router = mux.NewRouter()
|
34 | 40 |
|
35 |
| - // homepage |
36 | 41 | router.HandleFunc("", func(res *mux.ResponseWriter, req *mux.Request) {
|
37 | 42 | res.Write(renderHomepage())
|
38 | 43 | })
|
39 | 44 |
|
40 |
| - // coin info |
41 |
| - router.HandleFunc("{denom}", func(res *mux.ResponseWriter, req *mux.Request) { |
42 |
| - // denom := req.GetVar("denom") |
| 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) { |
43 | 55 | // banker := std.NewBanker(std.BankerTypeReadonly)
|
44 | 56 | // res.Write(renderAddressBalance(banker, denom, denom))
|
45 |
| - res.Write("Total supply feature is coming soon. Please check back later!") |
| 57 | + res.Write("The total supply feature is coming soon.") |
46 | 58 | })
|
47 | 59 |
|
48 |
| - // address balance |
49 |
| - router.HandleFunc("{denom}/{address}", func(res *mux.ResponseWriter, req *mux.Request) { |
50 |
| - denom := req.GetVar("denom") |
51 |
| - addr := req.GetVar("address") |
52 |
| - banker := std.NewBanker(std.BankerTypeReadonly) |
53 |
| - res.Write(renderAddressBalance(banker, denom, addr)) |
54 |
| - }) |
| 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 | + } |
55 | 63 | }
|
56 | 64 |
|
57 | 65 | func Render(path string) string {
|
58 | 66 | return router.Render(path)
|
59 | 67 | }
|
60 | 68 |
|
61 | 69 | func renderHomepage() string {
|
62 |
| - return `# gno.land Coins Explorer |
| 70 | + return strings.Replace(`# Gno.land Coins Explorer |
| 71 | + |
| 72 | +This is a simple, readonly realm that allows users to browse native coin balances. |
| 73 | +Here 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 | + |
| 86 | +func 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 | + |
| 107 | +func 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 | + |
| 128 | +func renderAllBalances(rawpath, input string) string { |
| 129 | + out := "# Balances\n\n" |
63 | 130 |
|
64 |
| -## Usage |
| 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 | + } |
65 | 145 |
|
66 |
| -- /r/gnoland/coins:<denom> - shows the total supply of denom (coming soon) |
67 |
| -- /r/gnoland/coins:<denom>/<address> - shows the denom balance of a specific address |
| 146 | + user, _ := users.ResolveAny(input) |
| 147 | + name := "`" + input + "`" |
| 148 | + if user != nil { |
| 149 | + name = user.RenderLink("") |
| 150 | + } |
68 | 151 |
|
69 |
| -Examples: |
| 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 | + } |
70 | 162 |
|
71 |
| -- /r/gnoland/coins:ugnot - shows the total supply of ` + "`ugnot`" + ` (coming soon) |
72 |
| -- /r/gnoland/coins:ugnot/g1... - shows the ` + "`ugnot`" + ` balance of a specific address |
| 163 | + balances := banker.GetCoins(std.Address(input)) |
73 | 164 |
|
74 |
| -` |
| 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 |
| 192 | +func 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 | + |
| 201 | +func isSortReversed(req *realmpath.Request) bool { |
| 202 | + return req.Query.Get("order") != "asc" |
75 | 203 | }
|
76 | 204 |
|
77 |
| -func renderAddressBalance(banker std.Banker, denom string, addr string) string { |
78 |
| - address := std.Address(addr) |
79 |
| - coins := banker.GetCoins(address) |
| 205 | +func 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) |
80 | 221 |
|
81 |
| - for _, coin := range coins { |
82 |
| - if coin.Denom == denom { |
83 |
| - return "Balance: " + coin.String() |
| 222 | + if field == currentField { |
| 223 | + if currentOrder == "asc" { |
| 224 | + label += " ↑" |
| 225 | + } else { |
| 226 | + label += " ↓" |
84 | 227 | }
|
85 | 228 | }
|
86 | 229 |
|
87 |
| - return "Balance: 0 " + denom |
| 230 | + return md.Link(label, "?"+query.Encode()) |
88 | 231 | }
|
0 commit comments