Skip to content

Commit 47d4498

Browse files
authored
feat(examples): make /r/gnoland/coins better (#4306)
## Description Adds more features to the realm, such as a full balance page, an option to search with a cosmos address that gets converted to a `g1..` automatically. dep on: #4325, #4326 todo: - [x] fix realm-issued coin denom case ie `/gno.land/r/gnoland/coins:zeoncoin` - [x] add tests - [ ] when #4061 is merged, add the form to the convert page
1 parent a48cf5d commit 47d4498

File tree

2 files changed

+213
-47
lines changed

2 files changed

+213
-47
lines changed

examples/gno.land/r/gnoland/coins/coins.gno

Lines changed: 175 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,75 +14,218 @@
1414
// interface and doesn't maintain any state of its own. This pattern allows for
1515
// simple, stateless information retrieval directly through the blockchain's
1616
// 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
2217
package coins
2318

2419
import (
20+
"net/url"
2521
"std"
22+
"strconv"
23+
"strings"
2624

2725
"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"
2834
)
2935

3036
var router *mux.Router
3137

3238
func init() {
3339
router = mux.NewRouter()
3440

35-
// homepage
3641
router.HandleFunc("", func(res *mux.ResponseWriter, req *mux.Request) {
3742
res.Write(renderHomepage())
3843
})
3944

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) {
4355
// banker := std.NewBanker(std.BankerTypeReadonly)
4456
// 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.")
4658
})
4759

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+
}
5563
}
5664

5765
func Render(path string) string {
5866
return router.Render(path)
5967
}
6068

6169
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"
63130

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+
}
65145

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+
}
68151

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+
}
70162

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))
73164

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"
75203
}
76204

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)
80221

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 += " ↓"
84227
}
85228
}
86229

87-
return "Balance: 0 " + denom
230+
return md.Link(label, "?"+query.Encode())
88231
}

examples/gno.land/r/gnoland/coins/coins_test.gno

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,73 @@ import (
77

88
"gno.land/p/demo/testutils"
99
"gno.land/p/demo/ufmt"
10+
"gno.land/p/leon/ctg"
1011
)
1112

1213
func TestBalanceChecker(t *testing.T) {
13-
denom := "testtoken"
14+
denom1 := "testtoken1"
15+
denom2 := "testtoken2"
1416
addr1 := testutils.TestAddress("user1")
1517
addr2 := testutils.TestAddress("user2")
1618

1719
coinsRealm := std.NewCodeRealm("gno.land/r/gnoland/coins")
1820
testing.SetRealm(coinsRealm)
1921

20-
testing.IssueCoins(addr1, std.Coins{{denom, 1000000}})
21-
testing.IssueCoins(addr2, std.Coins{{denom, 500000}})
22+
testing.IssueCoins(addr1, std.NewCoins(std.NewCoin(denom1, 1000000)))
23+
testing.IssueCoins(addr2, std.NewCoins(std.NewCoin(denom1, 501)))
24+
25+
testing.IssueCoins(addr2, std.NewCoins(std.NewCoin(denom2, 12345)))
26+
27+
gnoAddr, _ := ctg.ConvertCosmosToGno("cosmos1s2v4tdskccx2p3yyvzem4mw5nn5fprwcku77hr")
2228

2329
tests := []struct {
2430
name string
2531
path string
26-
expected string
32+
contains string
2733
wantPanic bool
2834
}{
2935
{
3036
name: "homepage",
3137
path: "",
32-
expected: "# gno.land Coins Explorer",
38+
contains: "# Gno.land Coins Explorer",
3339
},
3440
// TODO: not supported yet
3541
// {
3642
// name: "total supply",
3743
// path: denom,
3844
// expected: "Balance: 1500000testtoken",
3945
// },
46+
47+
{
48+
name: "addr1's coin balance",
49+
path: ufmt.Sprintf("balances/%s?coin=%s", addr1.String(), denom1),
50+
contains: ufmt.Sprintf("`%s` has `%d%s`", addr1.String(), 1000000, denom1),
51+
},
52+
{
53+
name: "addr2's full balances",
54+
path: ufmt.Sprintf("balances/%s", addr2.String()),
55+
contains: ufmt.Sprintf("This page shows full coin balances of `%s` at block", addr2.String()),
56+
},
57+
{
58+
name: "addr2's full balances",
59+
path: ufmt.Sprintf("balances/%s", addr2.String()),
60+
contains: `| testtoken1 | 501 |
61+
| testtoken2 | 12345 |`,
62+
},
4063
{
41-
name: "addr1's balance",
42-
path: ufmt.Sprintf("%s/%s", denom, addr1.String()),
43-
expected: "Balance: 1000000testtoken",
64+
name: "addr2's coin balance",
65+
path: ufmt.Sprintf("balances/%s?coin=%s", addr2.String(), denom1),
66+
contains: ufmt.Sprintf("`%s` has `%d%s`", addr2.String(), 501, denom1),
4467
},
4568
{
46-
name: "addr2's balance",
47-
path: ufmt.Sprintf("%s/%s", denom, addr2.String()),
48-
expected: "Balance: 500000testtoken",
69+
name: "cosmos addr conversion",
70+
path: "convert/cosmos1s2v4tdskccx2p3yyvzem4mw5nn5fprwcku77hr",
71+
contains: ufmt.Sprintf("`cosmos1s2v4tdskccx2p3yyvzem4mw5nn5fprwcku77hr` on Cosmos matches `%s`", gnoAddr),
4972
},
5073
{
5174
name: "invalid path",
52-
path: ufmt.Sprintf("%s/invalid/extra", denom),
53-
expected: "404",
75+
path: "invalid",
76+
contains: "404",
5477
wantPanic: false,
5578
},
5679
}
@@ -67,8 +90,8 @@ func TestBalanceChecker(t *testing.T) {
6790

6891
result := Render(tt.path)
6992
if !tt.wantPanic {
70-
if !strings.Contains(result, tt.expected) {
71-
t.Errorf("expected %s to contain %s", result, tt.expected)
93+
if !strings.Contains(result, tt.contains) {
94+
t.Errorf("expected %s to contain %s", result, tt.contains)
7295
}
7396
}
7497
})

0 commit comments

Comments
 (0)