Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GSW-1668 feat: use grc20reg to support multiple grc20 tokens #452

Merged
merged 7 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions _deploy/r/gnoswap/common/grc20reg_helper.gno
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package common

import (
"regexp"
"std"
"strings"

"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/ufmt"
"gno.land/r/demo/grc20reg"
)

var (
re = regexp.MustCompile(`\[gno\.land/r/[^\]]+\]`)
)

// GetToken returns a grc20.Token instance
// if token is not registered, it will panic
// token instance supports following methods:
Expand Down Expand Up @@ -58,6 +64,26 @@ func MustRegistered(path string) {
}
}

// ListRegisteredTokens returns the list of registered tokens
// NOTE:
// - Unfortunate, grc20reg doesn't support this.
// - We need to parse the rendered grc20reg page to get the list of registered tokens.
func ListRegisteredTokens() []string {
render := grc20reg.Render("")
return extractTokenPathsFromRender(render)
}

func extractTokenPathsFromRender(render string) []string {
matches := re.FindAllString(render, -1)

tokenPaths := make([]string, 0, len(matches))
for _, match := range matches {
tokenPath := strings.Trim(match, "[]") // Remove the brackets
tokenPaths = append(tokenPaths, tokenPath)
}
return tokenPaths
}

// TotalSupply returns the total supply of the token
func TotalSupply(path string) uint64 {
return GetToken(path).TotalSupply()
Expand Down
87 changes: 87 additions & 0 deletions _deploy/r/gnoswap/common/grc20reg_helper_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,74 @@ func TestMustRegistered(t *testing.T) {
})
}

func TestListRegisteredTokens(t *testing.T) {
t.Skip("skipping tests -> can not mock grc20reg.Render() || testing extractTokenPathsFromRender() does cover this")
}

func TestExtractTokenPathsFromRender(t *testing.T) {
var (
wugnotPath = "gno.land/r/demo/wugnot"
gnsPath = "gno.land/r/gnoswap/v1/gns"
fooPath = "gno.land/r/onbloc/foo"
quxPath = "gno.land/r/onbloc/qux"
)

// NOTE: following strings are return from grc20reg.Render()
renderList := []string{
// no registered token
`No registered token.`,

// 1 token
`- **wrapped GNOT** - [gno.land/r/demo/wugnot](/r/demo/wugnot) - [info](/r/demo/grc20reg:gno.land/r/demo/wugnot)`,

// 2 tokens
`- **wrapped GNOT** - [gno.land/r/demo/wugnot](/r/demo/wugnot) - [info](/r/demo/grc20reg:gno.land/r/demo/wugnot)
- **Gnoswap** - [gno.land/r/gnoswap/v1/gns](/r/gnoswap/v1/gns) - [info](/r/demo/grc20reg:gno.land/r/gnoswap/v1/gns)
`,

// 4 tokens
`- **wrapped GNOT** - [gno.land/r/demo/wugnot](/r/demo/wugnot) - [info](/r/demo/grc20reg:gno.land/r/demo/wugnot)
- **Gnoswap** - [gno.land/r/gnoswap/v1/gns](/r/gnoswap/v1/gns) - [info](/r/demo/grc20reg:gno.land/r/gnoswap/v1/gns)
- **Baz** - [gno.land/r/onbloc/foo](/r/onbloc/foo) - [info](/r/demo/grc20reg:gno.land/r/onbloc/foo)
- **Qux** - [gno.land/r/onbloc/qux](/r/onbloc/qux) - [info](/r/demo/grc20reg:gno.land/r/onbloc/qux)
`,
}

tests := []struct {
name string
render string
expected []string
}{
{
name: "no registered token",
render: renderList[0],
expected: []string{},
},
{
name: "1 registered token",
render: renderList[1],
expected: []string{wugnotPath},
},
{
name: "2 registered tokens",
render: renderList[2],
expected: []string{wugnotPath, gnsPath},
},
{
name: "4 registered tokens",
render: renderList[3],
expected: []string{wugnotPath, gnsPath, fooPath, quxPath},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
extracted := extractTokenPathsFromRender(tt.render)
uassert.True(t, areSlicesEqual(t, tt.expected, extracted))
})
}
}

func TestTotalSupply(t *testing.T) {
// result from grc2reg and (direct import/call) should be the same
uassert.Equal(t, foo20.TotalSupply(), TotalSupply(tokenPath))
Expand All @@ -172,3 +240,22 @@ func TestAllowance(t *testing.T) {
// result from grc2reg and (direct import/call) should be the same
uassert.Equal(t, foo20.Allowance(AddrToUser(owner), AddrToUser(spender)), Allowance(tokenPath, owner, spender))
}

// areSlicesEqual compares two slices of strings
func areSlicesEqual(t *testing.T, a, b []string) bool {
t.Helper()

// Check if lengths are different
if len(a) != len(b) {
return false
}

// Compare each element
for i := range a {
if a[i] != b[i] {
return false
}
}

return true
}
82 changes: 49 additions & 33 deletions community_pool/community_pool.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,76 @@ import (
"gno.land/p/demo/ufmt"

"gno.land/r/gnoswap/v1/common"
"gno.land/r/gnoswap/v1/consts"
)

// TransferTokenByAdmin transfers token to the given address.
func TransferTokenByAdmin(pkgPath string, to std.Address, amount uint64) {
caller := std.PrevRealm().Addr()
if err := common.AdminOnly(caller); err != nil {
panic(err)
}

transferToken(pkgPath, to, amount)
func TransferTokenByAdmin(tokenPath string, to std.Address, amount uint64) {
assertOnlyNotHalted()
assertOnlyAdmin()

prevAddr, prevRealm := getPrev()
std.Emit(
"TransferTokenByAdmin",
"prevAddr", prevAddr,
"prevRealm", prevRealm,
"pkgPath", pkgPath,
"to", to.String(),
"amount", ufmt.Sprintf("%d", amount),
)
transferToken(tokenPath, to, amount)
}

// TransferToken transfers token to the given address.
// Only governance contract can execute this function via proposal
func TransferToken(pkgPath string, to std.Address, amount uint64) {
caller := std.PrevRealm().Addr()
if err := common.GovernanceOnly(caller); err != nil {
panic(err)
}
func TransferToken(tokenPath string, to std.Address, amount uint64) {
assertOnlyNotHalted()
assertOnlyGovernance()

transferToken(tokenPath, to, amount)
}

transferToken(pkgPath, to, amount)
// transferToken transfers token to the given address.
func transferToken(tokenPath string, to std.Address, amount uint64) {
teller := common.GetTokenTeller(tokenPath)
checkErr(teller.Transfer(to, amount))

prevAddr, prevRealm := getPrev()
prevAddr, prevRealm := getPrevAsString()
std.Emit(
"TransferToken",
"prevAddr", prevAddr,
"prevRealm", prevRealm,
"pkgPath", pkgPath,
"tokenPath", tokenPath,
"to", to.String(),
"amount", ufmt.Sprintf("%d", amount),
)
}

func transferToken(pkgPath string, to std.Address, amount uint64) {
// checkErr panics if the error is not nil.
func checkErr(err error) {
if err != nil {
panic(err.Error())
}
}

// assertOnlyNotHalted panics if the contract is halted.
func assertOnlyNotHalted() {
common.IsHalted()
}

// assertOnlyAdmin panics if the caller is not the admin.
func assertOnlyAdmin() {
caller := getPrevAddr()
if err := common.AdminOnly(caller); err != nil {
panic(err)
}
}

_, found := registered[pkgPath]
if !found {
panic(addDetailToError(
errNotRegistered,
ufmt.Sprintf("community_pool.gno__transferToken() || token(%s) not registered", pkgPath),
))
// assertOnlyGovernance panics if the caller is not the governance.
func assertOnlyGovernance() {
caller := getPrevAddr()
if err := common.GovernanceOnly(caller); err != nil {
panic(err)
}
}

// getPrevAddr returns the address of the caller.
func getPrevAddr() std.Address {
return std.PrevRealm().Addr()
}

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