Skip to content
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
4 changes: 2 additions & 2 deletions examples/gno.land/p/demo/tokens/grc20/token.gno
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import (
//
// Token, ledger := grc20.NewToken(0, cur, "Foo", "FOO", 4)
//
// If the Token should be discoverable, follow up with
// grc20reg.Register(cross, Token, slug).
// If the Token should be discoverable, follow up with grc20reg.Register.
// The registry key is Token.ID().
func NewToken(_ int, rlm realm, name, symbol string, decimals int) (*Token, *PrivateLedger) {
if !rlm.IsCurrent() {
panic(ErrSpoofedRealm)
Expand Down
2 changes: 1 addition & 1 deletion examples/gno.land/p/demo/tokens/grc20/types.gno
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ var (

// Construction limits. Symbol is restricted to the same charset as
// grc20reg.validateSlug because Token.ID() = origRealm + "." + symbol is
// emitted in events and frequently used as a registry slug; banning `.`
// emitted in events and used as the canonical registry key; banning `.`
// `/` and whitespace here prevents downstream parsers from being fooled
// by ambiguous IDs (e.g. an attacker emitting tokens whose ID parses as
// belonging to a victim realm). Name is for display only and allows
Expand Down
2 changes: 1 addition & 1 deletion examples/gno.land/r/demo/defi/atomicswap/atomicswap.gno
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
// token.Approve(swapAddr, 1000)
//
// // Alice creates a swap with 1000 tokens for Bob
// id, _ := atomicswap.NewGRC20Swap(bobAddr, hashlock, "gno.land/r/demo/token")
// id, _ := atomicswap.NewGRC20Swap(bobAddr, hashlock, "gno.land/r/demo/token.TKN")
//
// // Bob claims the swap by providing the secret
// atomicswap.Claim(id, "mysecret")
Expand Down
4 changes: 2 additions & 2 deletions examples/gno.land/r/demo/defi/atomicswap/atomicswap_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ func TestNewGRC20Swap_Claim(cur realm, t *testing.T) {

// Create a new swap
testing.SetRealm(testing.NewUserRealm(sender))
id, swap := NewGRC20Swap(cross(cur), recipient, hashlockHex, "gno.land/r/tests/vm/test20")
id, swap := NewGRC20Swap(cross(cur), recipient, hashlockHex, "gno.land/r/tests/vm/test20.TST")
uassert.Equal(t, 1, id)

expected := `- status: active
Expand Down Expand Up @@ -345,7 +345,7 @@ func TestNewGRC20Swap_Refund(cur realm, t *testing.T) {

// Create a new swap
testing.SetRealm(testing.NewUserRealm(sender))
id, swap := NewGRC20Swap(cross(cur), recipient, hashlockHex, "gno.land/r/tests/vm/test20")
id, swap := NewGRC20Swap(cross(cur), recipient, hashlockHex, "gno.land/r/tests/vm/test20.TST")
uassert.Equal(t, 1, id)

expected := `- status: active
Expand Down
25 changes: 18 additions & 7 deletions examples/gno.land/r/demo/defi/grc20reg/grc20reg.gno
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,37 @@ import (
"gno.land/p/nt/ufmt/v0"
)

var registry = avl.NewTree() // rlmPath[.slug] -> *Token (slug is optional)
var registry = avl.NewTree() // rlmPath.symbol -> *Token

// Construction lives in grc20.NewToken — it now takes (_ int, rlm realm)
// and binds origRealm from rlm.PkgPath() under an IsCurrent assertion.
// Use this package's Register to make a token discoverable:
// Register stores tokens under their canonical Token.ID():
//
// Token, ledger := grc20.NewToken(0, cur, name, symbol, decimals)
// grc20reg.Register(cross, Token, "")

func Register(cur realm, token *grc20.Token, slug string) {
if token == nil {
Comment thread
moul marked this conversation as resolved.
panic("grc20reg: nil token")
}
if slug != "" {
validateSlug(slug)
}
rlmPath := cur.Previous().PkgPath()
key := fqname.Construct(rlmPath, slug)
key := fqname.Construct(rlmPath, token.GetSymbol())
if key != token.ID() {
panic("grc20reg: token must be registered from its own realm")
}
if registry.Has(key) {
panic("grc20reg: token already registered")
}
registry.Set(key, token)
chain.Emit(
registerEvent,
"token_path", key,
"pkgpath", rlmPath,
"slug", slug,
"symbol", token.GetSymbol(),
)
}

Expand Down Expand Up @@ -59,8 +70,8 @@ func Render(path string) string {
registry.Iterate("", "", func(key string, tokenI any) bool {
count++
token := tokenI.(*grc20.Token)
rlmPath, slug := fqname.Parse(key)
rlmLink := fqname.RenderLink(rlmPath, slug)
rlmPath, symbol := fqname.Parse(key)
rlmLink := fqname.RenderLink(rlmPath, symbol)
infoLink := "/r/demo/grc20reg:" + key
s += "- " + md.Bold(md.EscapeText(token.GetName())) + " - " + rlmLink + " - " + md.Link("info", infoLink) + "\n"
return false
Expand All @@ -72,8 +83,8 @@ func Render(path string) string {
default: // specific token
key := path
token := MustGet(key)
rlmPath, slug := fqname.Parse(key)
rlmLink := fqname.RenderLink(rlmPath, slug)
rlmPath, symbol := fqname.Parse(key)
rlmLink := fqname.RenderLink(rlmPath, symbol)
s := ufmt.Sprintf("# %s\n", md.EscapeText(token.GetName()))
s += "- symbol: " + md.Bold(md.EscapeText(token.GetSymbol())) + "\n"
s += ufmt.Sprintf("- realm: %s\n", rlmLink)
Expand Down
68 changes: 49 additions & 19 deletions examples/gno.land/r/demo/defi/grc20reg/grc20reg_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,68 @@ func TestRegistry(cur realm, t *testing.T) {
token, ledger := grc20.NewToken(0, cur, "TestToken", "TST", 4)
ledger.Mint(cur.Address(), 1234567)
// register
Register(cross(cur), token, "")
regToken := Get(realmAddr)
Register(cross(cur), token, "mySlug")
key := realmAddr + ".TST"
regToken := Get(key)
urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil
urequire.Equal(t, regToken.GetSymbol(), "TST")

expected := `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo) - [info](/r/demo/grc20reg:gno.land/r/demo/foo)
expected := `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo).TST - [info](/r/demo/grc20reg:gno.land/r/demo/foo.TST)
`
got := Render("")
urequire.True(t, strings.Contains(got, expected))
// 404
invalidToken := Get("0xdeadbeef")
urequire.True(t, invalidToken == nil)

// register with a slug
Register(cross(cur), token, "mySlug")
regToken = Get(realmAddr + ".mySlug")
urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil
urequire.Equal(t, regToken.GetSymbol(), "TST")

// override
Register(cross(cur), token, "")
regToken = Get(realmAddr + "")
urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil
urequire.Equal(t, regToken.GetSymbol(), "TST")

got = Render("")
urequire.True(t, strings.Contains(got, `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo) - [info](/r/demo/grc20reg:gno.land/r/demo/foo)`))
urequire.True(t, strings.Contains(got, `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo).mySlug - [info](/r/demo/grc20reg:gno.land/r/demo/foo.mySlug)`))
urequire.True(t, strings.Contains(got, expected))

expected = `# TestToken
- symbol: **TST**
- realm: [gno.land/r/demo/foo](/r/demo/foo).mySlug
- realm: [gno.land/r/demo/foo](/r/demo/foo).TST
- decimals: 4
- total supply: 1234567
`
got = Render("gno.land/r/demo/foo.mySlug")
got = Render(key)
urequire.Equal(t, expected, got)
}

func TestRegisterRejectsOverwrite(cur realm, t *testing.T) {
testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_overwrite"))
token, ledger := grc20.NewToken(0, cur, "Bar", "BAR", 4)
ledger.Mint(cur.Address(), 11)
Register(cross(cur), token, "")

urequire.Equal(t, "BAR", Get(token.ID()).GetSymbol())
urequire.Equal(t, int64(11), Get(token.ID()).BalanceOf(cur.Address()))

replacement, _ := grc20.NewToken(0, cur, "Replacement", "BAR", 6)
urequire.AbortsContains(t, cur, "token already registered", func() {
Register(cross(cur), replacement, "")
})
}

func TestRegisterRejectsAliasedTokenPaths(cur realm, t *testing.T) {
testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_alias"))
token, _ := grc20.NewToken(0, cur, "Aliased Token", "ALIAS", 4)
Register(cross(cur), token, "first")

urequire.AbortsContains(t, cur, "token already registered", func() {
Register(cross(cur), token, "second")
})
}

func TestRegisterRejectsTokenFromDifferentRealm(cur realm, t *testing.T) {
testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_id_source"))
token, _ := grc20.NewToken(0, cur, "Mismatch Token", "MISMATCH", 4)

testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_id_target"))
urequire.AbortsContains(t, cur, "token must be registered from its own realm", func() {
Register(cross(cur), token, "")
})
}

func TestValidateSlug(cur realm, t *testing.T) {
// Valid slugs — should not panic
valid := []string{"mytoken", "my-token", "my_token", "Token123", "a", "A-B_c", strings.Repeat("a", maxSlugLen)}
Expand Down Expand Up @@ -102,3 +125,10 @@ func TestValidateSlugPanicsOnInjection(cur realm, t *testing.T) {
validateSlug(`) [Claim](https://evil.com`)
t.Errorf("should have panicked")
}

func TestRegisterRejectsNilToken(cur realm, t *testing.T) {
testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_nil"))
urequire.AbortsContains(t, cur, "nil token", func() {
Register(cross(cur), nil, "")
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package tokenhub

import (
"gno.land/p/demo/tokens/grc20"
"gno.land/p/nt/fqname/v0"
"gno.land/r/demo/defi/grc20reg"

"gno.land/p/demo/tokens/grc1155"
Expand All @@ -29,12 +28,11 @@ func init(cur realm) {
hor.Register(cross(cur), "Token Hub", "Registry for tokens and NFTs on gno.land")
}

// RegisterToken is a function that uses gno.land/r/demo/defi/grc20reg to register a token
// RegisterToken registers a token in grc20reg with the given slug.
// RegisterToken registers a token in grc20reg.
// Returns the registry key that can be used to retrieve the token.
func RegisterToken(cur realm, token *grc20.Token, slug string) string {
grc20reg.Register(cross(cur), token, slug)
return fqname.Construct(cur.PkgPath(), slug)
return token.ID()
}

// RegisterNFT is a function that registers an NFT in an avl.Tree
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import (
"gno.land/p/nt/urequire/v0"
)

const testRealmPkgPath = "gno.land/r/matijamarjanovic/testrealm"
const (
tokenhubPkgPath = "gno.land/r/matijamarjanovic/tokenhub"
testRealmPkgPath = "gno.land/r/matijamarjanovic/testrealm"
)

// newTestNFT constructs an NFT through a same-realm cross so NewBasicNFT's
// IsCurrent + non-empty PkgPath checks pass.
Expand All @@ -27,12 +30,13 @@ func newTestNFT(_ int, rlm realm, name, symbol string) *grc721.BasicNFT {

func TestTokenRegistration(cur realm, t *testing.T) {
resetState(t)
testing.SetRealm(testing.NewCodeRealm(testRealmPkgPath))

token, _ := grc20.NewToken(0, cur, "Test Token", "TEST", 6)
RegisterToken(cross(cur), token, "test_token")
key := RegisterToken(cross(cur), token, "test_token")

retrievedToken := GetToken("gno.land/r/matijamarjanovic/tokenhub.test_token")
uassert.Equal(t, tokenhubPkgPath+".TEST", key)

retrievedToken := GetToken(tokenhubPkgPath + ".TEST")

urequire.True(t, retrievedToken != nil, "Should retrieve registered token")
uassert.Equal(t, "Test Token", retrievedToken.GetName(), "Token name should match")
Expand Down Expand Up @@ -78,15 +82,17 @@ func TestGRC1155Registration(cur realm, t *testing.T) {

func TestBalanceRetrieval(cur realm, t *testing.T) {
resetState(t)
testing.SetRealm(testing.NewCodeRealm(testRealmPkgPath))

token, ledger := grc20.NewToken(0, cur, "Test Token", "TEST", 6)
RegisterToken(cross(cur), token, "test_token")
ledger.Mint(unsafe.CurrentRealm().Address(), 1000)
token, ledger := grc20.NewToken(0, cur, "Balance Token", "BAL", 6)
RegisterToken(cross(cur), token, "balance_token")
holder := unsafe.CurrentRealm().Address()
ledger.Mint(holder, 1000)

balances := GetUserTokenBalances(unsafe.CurrentRealm().Address().String())
balances := GetUserTokenBalances(holder.String())
uassert.True(t, strings.Contains(balances,
"Token:gno.land/r/matijamarjanovic/tokenhub.test_token:1000"), "Should show correct GRC20 balance")
"Token:"+tokenhubPkgPath+".BAL:1000"), "Should show correct GRC20 balance")

testing.SetRealm(testing.NewCodeRealm(testRealmPkgPath))

nft := newTestNFT(0, cur, "Test NFT", "TNFT")
nft.Mint(unsafe.CurrentRealm().Address(), grc721.TokenID("1"))
Expand All @@ -102,9 +108,9 @@ func TestBalanceRetrieval(cur realm, t *testing.T) {
balances = GetUserMultiTokenBalances(unsafe.CurrentRealm().Address().String())
uassert.True(t, strings.Contains(balances, "MultiToken:"+testRealmPkgPath+".1:0"), "Should show multi-token balance")

nonZeroBalances := GetUserTokenBalancesNonZero(unsafe.CurrentRealm().Address().String())
nonZeroBalances := GetUserTokenBalancesNonZero(holder.String())
uassert.True(t, strings.Contains(nonZeroBalances,
"Token:gno.land/r/matijamarjanovic/tokenhub.test_token:1000"), "Should show non-zero GRC20 balance")
"Token:"+tokenhubPkgPath+".BAL:1000"), "Should show non-zero GRC20 balance")
}

func TestErrorCases(cur realm, t *testing.T) {
Expand All @@ -126,15 +132,16 @@ func TestErrorCases(cur realm, t *testing.T) {

func TestTokenListingFunctions(cur realm, t *testing.T) {
resetState(t)
testing.SetRealm(testing.NewCodeRealm(testRealmPkgPath))

grc20Token, _ := grc20.NewToken(0, cur, "Test Token", "TEST", 6)
grc20Token, _ := grc20.NewToken(0, cur, "Listing Token", "LST", 6)
RegisterToken(cross(cur), grc20Token, "listing_token")

grc20List := GetAllTokens()
uassert.True(t, strings.Contains(grc20List, "Token:gno.land/r/matijamarjanovic/tokenhub.listing_token"),
uassert.True(t, strings.Contains(grc20List, "Token:"+tokenhubPkgPath+".LST"),
"GetAllGRC20Tokens should list registered token")

testing.SetRealm(testing.NewCodeRealm(testRealmPkgPath))

nftToken := newTestNFT(0, cur, "Listing NFT", "LNFT")
nftToken.Mint(address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y"), grc721.TokenID("1"))
RegisterNFT(cross(cur), nftToken.Getter(), "listing_nft", "1")
Expand All @@ -153,19 +160,19 @@ func TestTokenListingFunctions(cur realm, t *testing.T) {
completeList := GetAllRegistered()
uassert.True(t, strings.Contains(completeList, "NFT:"+testRealmPkgPath+".listing_nft.1"),
"GetAllTokens should list NFTs")
uassert.True(t, strings.Contains(completeList, "Token:gno.land/r/matijamarjanovic/tokenhub.listing_token"),
uassert.True(t, strings.Contains(completeList, "Token:"+tokenhubPkgPath+".LST"),
"GetAllTokens should list GRC20 tokens")
uassert.True(t, strings.Contains(completeList, "MultiToken:"+testRealmPkgPath+".listing_mt"),
"GetAllTokens should list multi-tokens")
}

func TestMustGetFunctions(cur realm, t *testing.T) {
testing.SetRealm(testing.NewCodeRealm(testRealmPkgPath))
resetState(t)

token, _ := grc20.NewToken(0, cur, "Must Token", "MUST", 6)
RegisterToken(cross(cur), token, "must_token")

retrievedToken := MustGetToken("gno.land/r/matijamarjanovic/tokenhub.must_token")
retrievedToken := MustGetToken(tokenhubPkgPath + ".MUST")
uassert.Equal(t, "Must Token", retrievedToken.GetName(), "Token name should match")

// Under the unified declaring-realm borrow, calling MustGetToken
Expand All @@ -176,6 +183,8 @@ func TestMustGetFunctions(cur realm, t *testing.T) {
uassert.True(t, r != nil, "MustGetToken should panic for non-existent token")
uassert.True(t, strings.Contains(r.(string), "unknown token"), "Panic message should mention unknown token")

testing.SetRealm(testing.NewCodeRealm(testRealmPkgPath))

nft := newTestNFT(0, cur, "Must NFT", "MNFT")
nft.Mint(unsafe.CurrentRealm().Address(), grc721.TokenID("1"))
RegisterNFT(cross(cur), nft.Getter(), "must_nft", "1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ gnoland start
gnokey maketx call -pkgpath gno.land/r/demo/defi/foo20 -func Faucet -gas-fee 740001ugnot -gas-wanted 7_400_000 -chainid=tendermint_test test1

# check the event after transferring the grc20 token
gnokey maketx call -pkgpath gno.land/r/grc20transfer -func TransferBy -args 'gno.land/r/demo/defi/foo20' -args 'g1c8cf99clyyjr2kh9awxnfyt36cvmr703tsmazp' -args '1000000' -gas-fee 940001ugnot -gas-wanted 9_400_000 -chainid=tendermint_test test1
gnokey maketx call -pkgpath gno.land/r/grc20transfer -func TransferBy -args 'gno.land/r/demo/defi/foo20.FOO' -args 'g1c8cf99clyyjr2kh9awxnfyt36cvmr703tsmazp' -args '1000000' -gas-fee 940001ugnot -gas-wanted 9_400_000 -chainid=tendermint_test test1
stdout OK!
stdout 'GAS WANTED: [0-9]+'
stdout 'GAS USED: [0-9]+'
Expand Down
Loading