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

feat: token hub #3479

Open
wants to merge 62 commits into
base: master
Choose a base branch
from

Conversation

matijamarjanovic
Copy link
Contributor

@matijamarjanovic matijamarjanovic commented Jan 10, 2025

Description

Token hub is a registry for tokens and NFTs created on gno.land. Since all the tokens are visible in each their own realms, as it is right now there is no easy way that a user can check which tokens belong to him by his address. Token hub bypasses this by concentrating each each token in it's own realm and allows operations over these tokens providing a easier way to communicate with the realms in which the tokens are originally made. Since grc20 registry already exists the idea is to combine it's functionality with similar concept used for other tokens to create a realm that will contain information about all tokens, this way you will be able to get all your token balances in one place.

How it works

In the realm where token is being made, tokenhub needs to be imported and the newly made token has to be registered to the token hub by using RegisterToken function and passing the token as an argument. This is everything token hub needs to be able to operate. Simiraly how it is done here for GRC20 token hub registers a pointer to the token in it's registry.

  • For GRC20 the mentioned registry is used in Token Hub
  • For GRC721 similar approach as in that registry is implemented; note: each NFT has to be registered after minting because each NFT has his own ID. Although nft collection could be registered without keeping track of IDs, this would prove to be useless for anything besides statistics (how many NFTs are minted in that collection)
  • For GRC1155 the method is a bit different - since multiple fungible and non-fungible tokens are a part of the same collection, to keep track of balances one would need both collection (getter) and the token ID, this is why in this case tokens of the same collection are registered separately, which at the end of the day makes sence, but does represent an extra step both in registration and in getting the balance

More Details

In order to make this work I had to make minor additions to other realms as one would probably notice in the code - I had to add Getters (pointers) in the grc721 and grc1155 so storing the token instance itself is avoided, like it is done with GRC20 for grc20reg.gno purposes. I also needed to add Iterate and IterateN functions to grc20 only to be able to get the tokens of that registry which shouldn't undermine any of it's functionality or security.

Contributor's checklist

  • Implement GRC721 basic_nft registration
  • Use grc20reg.gno to combine all tokens in one place
  • Implement GRC1155 token registration
  • Divide different tokens into different pages using path string in Render()
  • Implement pagination in token pages
  • Add support for namespaces, not just addresses (checking balances)
  • GetAllTokens func
  • Remove test.gno file & package and add real tests

Dillema

Since the hub uses existing grc20reg and extends it's usage, if a token is registered through the hub it would mean an incorrect realm path would be written in the grc20reg since the prevrealm would now point to the hub. I have not thought of an ideal solution to this, the best I got is transfering the whole realm to the /r/demo/grc20reg package, if I got the author's permission (@moul)

(because of this probelm this is not yet ready to be merged but I would like an opinion)

All feedback is welcome

@github-actions github-actions bot added the 🧾 package/realm Tag used for new Realms or Packages. label Jan 10, 2025
@Gno2D2
Copy link
Collaborator

Gno2D2 commented Jan 10, 2025

🛠 PR Checks Summary

🔴 Pending initial approval by a review team member (and label matches review triage state)

Manual Checks (for Reviewers):
  • IGNORE the bot requirements for this PR (force green CI check)
  • The pull request description provides enough details
Read More

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

✅ Automated Checks (for Contributors):

🟢 Maintainers must be able to edit this pull request (more info)
🔴 Pending initial approval by a review team member (and label matches review triage state)

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Automated Checks
Maintainers must be able to edit this pull request (more info)

If

🟢 Condition met
└── 🟢 The pull request was created from a fork (head branch repo: matijamarjanovic/gno)

Then

🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

Pending initial approval by a review team member (and label matches review triage state)

If

🟢 Condition met
└── 🟢 Not (🔴 Pull request author is a member of the team: tech-staff)

Then

🔴 Requirement not satisfied
└── 🔴 If
    ├── 🔴 Condition
    │   └── 🔴 Or
    │       ├── 🔴 At least 1 user(s) of the organization approved the pull request
    │       └── 🔴 This pull request is a draft
    └── 🔴 Else
        └── 🔴 And
            ├── 🟢 This label is applied to pull request: review/triage-pending
            └── 🔴 On no pull request

Manual Checks
**IGNORE** the bot requirements for this PR (force green CI check)

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission
The pull request description provides enough details

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 Not (🔴 Pull request author is a member of the team: core-contributors)
    └── 🟢 Not (🔴 Pull request author is user: dependabot[bot])

Can be checked by

  • team core-contributors

@leohhhn
Copy link
Contributor

leohhhn commented Jan 10, 2025

Check this out, it's a similar idea: https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/grc20reg/grc20reg.gno

@matijamarjanovic
Copy link
Contributor Author

Check this out, it's a similar idea

Thanks for pointing out, I will consider implementation for different token types

-change tokenhub to be a hub for nfts rather than a hub for fungible tokens
@matijamarjanovic matijamarjanovic changed the title feat : token hub feat: token hub Jan 10, 2025
Copy link
Contributor

@leohhhn leohhhn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start but there's more to be worked on. Check out the comments I left below.

When resolving comments, try to make commits that correspond to each comment, where it makes sense. This way the reviewer can see that you solved their exact concern, instead of making them read the whole code again.

Comment on lines 37 to 38
pageSize = 2
test = ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pageSize can be const, test should be removed i assume

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed this in 8c09725

Comment on lines 31 to 33
ErrMTAlreadyRegistered = errors.New("Multi-token already registered")
ErrMTNotFound = errors.New("Multi-token not found")
ErrMTInfoNotFound = errors.New("Multi-token info not found")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in case the error msg does not start with some abbreviation, line "NFT", that is usually in uppercase, err msgs should start in lowercase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noted. see 88b0a7e for the fix


// registering a collection of NFTs
func RegisterNFT(nftGetter grc721.NFTGetter, collection string, tokenId string) error {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random newline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed it in 1c4b72a

Comment on lines 58 to 61
nftOwner, err := nft.OwnerOf(grc721.TokenID(tokenId))
if string(nftOwner) == "" || err != nil {
return ErrNFTtokIDNotExists
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if nftOwner is std.Address, and I assume it is, use address.IsValid()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, fixed this in : 005e64b

Comment on lines 41 to 47
func RegisterToken(tokenGetter grc20.TokenGetter, slugs ...string) {
slug := ""
if len(slugs) > 0 {
slug = slugs[0]
}
grc20reg.Register(tokenGetter, slug)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get why there can be multiple slugs. Can you elaborate?

If there is a reason for them to exist, you're basically ignoring all but the first one, for some reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I realize this might be an overkill, but what I was going for is this: slugs should be either 0 length or 1, if it is more the rest is ignored. I changed this to just be one string in : 005e64b

Comment on lines +20 to +23
type GRC1155TokenInfo struct {
Collection grc1155.MultiTokenGetter
TokenID string
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is the right place for this. Is it specific to your hub, or maybe should it be something that exists in the 1155 package itself? Put some godoc as to why you need this.

Copy link
Contributor Author

@matijamarjanovic matijamarjanovic Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think grc1155 need pretty big changes as I was able to transfer other people's tokens using this getter so I think adding this to it wouldn't make much difference.
I think tokenhub should keep this info for now as I expect having to change it in the future with the GRC1155 changes. Please correct me if this is a wrong approach

}
sb.WriteString(md.BulletList(links))

case strings.HasPrefix(path, "token"): // grc20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a string need a comment to explain it like this, maybe it's better to define it as a constant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 43b4b52

}

func getBalances(input string) string {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random newline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed in 1c4b72a

Comment on lines 410 to 436
registeredNFTs.Iterate("", "", func(key string, value interface{}) bool {
nftGetter := value.(grc721.NFTGetter)
nft := nftGetter()
key_parts := strings.Split(key, ".")
owner, err := nft.OwnerOf(grc721.TokenID(key_parts[len(key_parts)-1]))
if err == nil && addr == owner { // show only the nfts owner owns
balances.Set("nft:"+key, 1)
}
return false
})

grc20reg.Iterate(func(key string, tokenGetter grc20.TokenGetter) bool {
token := tokenGetter()
balance := token.BalanceOf(addr)
balances.Set("token:"+key, balance)
return false
})

registeredMTs.Iterate("", "", func(key string, value interface{}) bool {
info := value.(GRC1155TokenInfo)
mt := info.Collection()
balance, err := mt.BalanceOf(addr, grc1155.TokenID(info.TokenID))
if err == nil {
balances.Set("mt:"+key, balance)
}
return false
})
Copy link
Contributor

@leohhhn leohhhn Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be a pretty bad case when your hub gains popularity. You will most likely break gas limits.

Try to optimize this somehow, split it up, not sure exactly how. Iterating trees (especially 3 of them), should be avoided wherever possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we agreed in person, the only thing I could to to optimize this is iterate the lists separately instead of all at once. This was done in 9b9767f

Comment on lines 523 to 534
func getAddressForUsername(addrOrName string) std.Address {
addr := std.Address(addrOrName)

if addr.IsValid() {
return addr
}
if user := users.GetUserByName(addrOrName); user != nil {
return user.Address
}

return ""
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func getAddressForUsername(addrOrName string) std.Address {
addr := std.Address(addrOrName)
if addr.IsValid() {
return addr
}
if user := users.GetUserByName(addrOrName); user != nil {
return user.Address
}
return ""
}
func getAddressForUsername(addrOrName string) std.Address {
addr := std.Address(addrOrName)
if addr.IsValid() {
return addr
}
if user := users.GetUserByName(addrOrName); user != nil {
return user.Address
}
return ""
}

Copy link
Contributor Author

@matijamarjanovic matijamarjanovic Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just a suggestion to move \n? If so - fixed in 387ebd9

@Gno2D2 Gno2D2 added the review/triage-pending PRs opened by external contributors that are waiting for the 1st review label Jan 21, 2025
@matijamarjanovic
Copy link
Contributor Author

Good start but there's more to be worked on. Check out the comments I left below.

All comments have been addressed and changes made. I have also made tests for new funcs. Can you please check out the "Question" section of the PR description and maybe provide an opinion 🙏 Thanks!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🧾 package/realm Tag used for new Realms or Packages. review/triage-pending PRs opened by external contributors that are waiting for the 1st review
Projects
Status: In Progress
Status: Triage
Development

Successfully merging this pull request may close these issues.

3 participants