Skip to content

Commit e138837

Browse files
authored
GSW-1668 feat: use grc20reg to support multiple grc20 tokens (#452)
* feat: ListRegisteredTokens in common realm * feat: use grc20reg in `protocol_fee` * feat: use grc20reg in `community_pool` * feat: use grc20reg in `launchpad` * feat: use grc20reg in `gov/staker`
1 parent cc944ee commit e138837

20 files changed

+547
-983
lines changed

_deploy/r/gnoswap/common/grc20reg_helper.gno

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package common
22

33
import (
4+
"regexp"
45
"std"
6+
"strings"
57

68
"gno.land/p/demo/grc/grc20"
79
"gno.land/p/demo/ufmt"
810
"gno.land/r/demo/grc20reg"
911
)
1012

13+
var (
14+
re = regexp.MustCompile(`\[gno\.land/r/[^\]]+\]`)
15+
)
16+
1117
// GetToken returns a grc20.Token instance
1218
// if token is not registered, it will panic
1319
// token instance supports following methods:
@@ -58,6 +64,26 @@ func MustRegistered(path string) {
5864
}
5965
}
6066

67+
// ListRegisteredTokens returns the list of registered tokens
68+
// NOTE:
69+
// - Unfortunate, grc20reg doesn't support this.
70+
// - We need to parse the rendered grc20reg page to get the list of registered tokens.
71+
func ListRegisteredTokens() []string {
72+
render := grc20reg.Render("")
73+
return extractTokenPathsFromRender(render)
74+
}
75+
76+
func extractTokenPathsFromRender(render string) []string {
77+
matches := re.FindAllString(render, -1)
78+
79+
tokenPaths := make([]string, 0, len(matches))
80+
for _, match := range matches {
81+
tokenPath := strings.Trim(match, "[]") // Remove the brackets
82+
tokenPaths = append(tokenPaths, tokenPath)
83+
}
84+
return tokenPaths
85+
}
86+
6187
// TotalSupply returns the total supply of the token
6288
func TotalSupply(path string) uint64 {
6389
return GetToken(path).TotalSupply()

_deploy/r/gnoswap/common/grc20reg_helper_test.gno

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,74 @@ func TestMustRegistered(t *testing.T) {
153153
})
154154
}
155155

156+
func TestListRegisteredTokens(t *testing.T) {
157+
t.Skip("skipping tests -> can not mock grc20reg.Render() || testing extractTokenPathsFromRender() does cover this")
158+
}
159+
160+
func TestExtractTokenPathsFromRender(t *testing.T) {
161+
var (
162+
wugnotPath = "gno.land/r/demo/wugnot"
163+
gnsPath = "gno.land/r/gnoswap/v1/gns"
164+
fooPath = "gno.land/r/onbloc/foo"
165+
quxPath = "gno.land/r/onbloc/qux"
166+
)
167+
168+
// NOTE: following strings are return from grc20reg.Render()
169+
renderList := []string{
170+
// no registered token
171+
`No registered token.`,
172+
173+
// 1 token
174+
`- **wrapped GNOT** - [gno.land/r/demo/wugnot](/r/demo/wugnot) - [info](/r/demo/grc20reg:gno.land/r/demo/wugnot)`,
175+
176+
// 2 tokens
177+
`- **wrapped GNOT** - [gno.land/r/demo/wugnot](/r/demo/wugnot) - [info](/r/demo/grc20reg:gno.land/r/demo/wugnot)
178+
- **Gnoswap** - [gno.land/r/gnoswap/v1/gns](/r/gnoswap/v1/gns) - [info](/r/demo/grc20reg:gno.land/r/gnoswap/v1/gns)
179+
`,
180+
181+
// 4 tokens
182+
`- **wrapped GNOT** - [gno.land/r/demo/wugnot](/r/demo/wugnot) - [info](/r/demo/grc20reg:gno.land/r/demo/wugnot)
183+
- **Gnoswap** - [gno.land/r/gnoswap/v1/gns](/r/gnoswap/v1/gns) - [info](/r/demo/grc20reg:gno.land/r/gnoswap/v1/gns)
184+
- **Baz** - [gno.land/r/onbloc/foo](/r/onbloc/foo) - [info](/r/demo/grc20reg:gno.land/r/onbloc/foo)
185+
- **Qux** - [gno.land/r/onbloc/qux](/r/onbloc/qux) - [info](/r/demo/grc20reg:gno.land/r/onbloc/qux)
186+
`,
187+
}
188+
189+
tests := []struct {
190+
name string
191+
render string
192+
expected []string
193+
}{
194+
{
195+
name: "no registered token",
196+
render: renderList[0],
197+
expected: []string{},
198+
},
199+
{
200+
name: "1 registered token",
201+
render: renderList[1],
202+
expected: []string{wugnotPath},
203+
},
204+
{
205+
name: "2 registered tokens",
206+
render: renderList[2],
207+
expected: []string{wugnotPath, gnsPath},
208+
},
209+
{
210+
name: "4 registered tokens",
211+
render: renderList[3],
212+
expected: []string{wugnotPath, gnsPath, fooPath, quxPath},
213+
},
214+
}
215+
216+
for _, tt := range tests {
217+
t.Run(tt.name, func(t *testing.T) {
218+
extracted := extractTokenPathsFromRender(tt.render)
219+
uassert.True(t, areSlicesEqual(t, tt.expected, extracted))
220+
})
221+
}
222+
}
223+
156224
func TestTotalSupply(t *testing.T) {
157225
// result from grc2reg and (direct import/call) should be the same
158226
uassert.Equal(t, foo20.TotalSupply(), TotalSupply(tokenPath))
@@ -172,3 +240,22 @@ func TestAllowance(t *testing.T) {
172240
// result from grc2reg and (direct import/call) should be the same
173241
uassert.Equal(t, foo20.Allowance(AddrToUser(owner), AddrToUser(spender)), Allowance(tokenPath, owner, spender))
174242
}
243+
244+
// areSlicesEqual compares two slices of strings
245+
func areSlicesEqual(t *testing.T, a, b []string) bool {
246+
t.Helper()
247+
248+
// Check if lengths are different
249+
if len(a) != len(b) {
250+
return false
251+
}
252+
253+
// Compare each element
254+
for i := range a {
255+
if a[i] != b[i] {
256+
return false
257+
}
258+
}
259+
260+
return true
261+
}

community_pool/community_pool.gno

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,60 +6,76 @@ import (
66
"gno.land/p/demo/ufmt"
77

88
"gno.land/r/gnoswap/v1/common"
9-
"gno.land/r/gnoswap/v1/consts"
109
)
1110

1211
// TransferTokenByAdmin transfers token to the given address.
13-
func TransferTokenByAdmin(pkgPath string, to std.Address, amount uint64) {
14-
caller := std.PrevRealm().Addr()
15-
if err := common.AdminOnly(caller); err != nil {
16-
panic(err)
17-
}
18-
19-
transferToken(pkgPath, to, amount)
12+
func TransferTokenByAdmin(tokenPath string, to std.Address, amount uint64) {
13+
assertOnlyNotHalted()
14+
assertOnlyAdmin()
2015

21-
prevAddr, prevRealm := getPrev()
22-
std.Emit(
23-
"TransferTokenByAdmin",
24-
"prevAddr", prevAddr,
25-
"prevRealm", prevRealm,
26-
"pkgPath", pkgPath,
27-
"to", to.String(),
28-
"amount", ufmt.Sprintf("%d", amount),
29-
)
16+
transferToken(tokenPath, to, amount)
3017
}
3118

3219
// TransferToken transfers token to the given address.
3320
// Only governance contract can execute this function via proposal
34-
func TransferToken(pkgPath string, to std.Address, amount uint64) {
35-
caller := std.PrevRealm().Addr()
36-
if err := common.GovernanceOnly(caller); err != nil {
37-
panic(err)
38-
}
21+
func TransferToken(tokenPath string, to std.Address, amount uint64) {
22+
assertOnlyNotHalted()
23+
assertOnlyGovernance()
24+
25+
transferToken(tokenPath, to, amount)
26+
}
3927

40-
transferToken(pkgPath, to, amount)
28+
// transferToken transfers token to the given address.
29+
func transferToken(tokenPath string, to std.Address, amount uint64) {
30+
teller := common.GetTokenTeller(tokenPath)
31+
checkErr(teller.Transfer(to, amount))
4132

42-
prevAddr, prevRealm := getPrev()
33+
prevAddr, prevRealm := getPrevAsString()
4334
std.Emit(
4435
"TransferToken",
4536
"prevAddr", prevAddr,
4637
"prevRealm", prevRealm,
47-
"pkgPath", pkgPath,
38+
"tokenPath", tokenPath,
4839
"to", to.String(),
4940
"amount", ufmt.Sprintf("%d", amount),
5041
)
5142
}
5243

53-
func transferToken(pkgPath string, to std.Address, amount uint64) {
44+
// checkErr panics if the error is not nil.
45+
func checkErr(err error) {
46+
if err != nil {
47+
panic(err.Error())
48+
}
49+
}
50+
51+
// assertOnlyNotHalted panics if the contract is halted.
52+
func assertOnlyNotHalted() {
5453
common.IsHalted()
54+
}
55+
56+
// assertOnlyAdmin panics if the caller is not the admin.
57+
func assertOnlyAdmin() {
58+
caller := getPrevAddr()
59+
if err := common.AdminOnly(caller); err != nil {
60+
panic(err)
61+
}
62+
}
5563

56-
_, found := registered[pkgPath]
57-
if !found {
58-
panic(addDetailToError(
59-
errNotRegistered,
60-
ufmt.Sprintf("community_pool.gno__transferToken() || token(%s) not registered", pkgPath),
61-
))
64+
// assertOnlyGovernance panics if the caller is not the governance.
65+
func assertOnlyGovernance() {
66+
caller := getPrevAddr()
67+
if err := common.GovernanceOnly(caller); err != nil {
68+
panic(err)
6269
}
70+
}
71+
72+
// getPrevAddr returns the address of the caller.
73+
func getPrevAddr() std.Address {
74+
return std.PrevRealm().Addr()
75+
}
6376

64-
registered[pkgPath].Transfer()(a2u(to), amount)
77+
// getPrevAsString returns the address and realm of the caller as a string.
78+
func getPrevAsString() (string, string) {
79+
prev := std.PrevRealm()
80+
return prev.Addr().String(), prev.PkgPath()
6581
}

0 commit comments

Comments
 (0)