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: clean GRC721 implementation #2117

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
14 changes: 14 additions & 0 deletions examples/gno.land/r/x/grc721-by-spec/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# grc721-by-spec

This folder contains 3 main parts:
1. The Go implementation of the ERC721 standard, called GRC721, which was used
for the initial implementation & testing
2. The Gno package implementation ported from the Go implementation mentioned above
3. An example NFT collection realm utilizing the Gno package

To test this out, install `gnodev` and run the following command in this folder:
```shell
gnodev ./grc721-gno ./exampleNFT
```

Then, visit [`localhost:8888/r/example/nft`](http://localhost:8888/r/example/nft).
7 changes: 7 additions & 0 deletions examples/gno.land/r/x/grc721-by-spec/exampleNFT/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module gno.land/r/example/nft

require (
gno.land/p/demo/ownable v0.0.0-latest
gno.land/p/demo/seqid v0.0.0-latest
gno.land/p/demo/xgrc721 v0.0.0-latest
)
102 changes: 102 additions & 0 deletions examples/gno.land/r/x/grc721-by-spec/exampleNFT/nft.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package nft

import (
"bytes"
"std"
"strconv"

"gno.land/p/demo/ownable"
"gno.land/p/demo/ufmt"
"gno.land/p/demo/xgrc721"
)

var token *xgrc721.Token
var o *ownable.Ownable

var idCounter int
var ids []int

func init() {
token = xgrc721.NewGRC721Token("Example NFT", "EX")
o = ownable.New()

Mint(o.Owner(), strconv.Itoa(idCounter))
}

func BalanceOf(owner std.Address) uint64 {
return token.BalanceOf(owner)
}

func OwnerOf(tokenID string) std.Address {
return token.OwnerOf(tokenID)
}

func TransferFrom(from, to std.Address, tokenID string) {
token.TransferFrom(from, to, tokenID)
}

func Approve(to std.Address, tokenID string) {
token.Approve(to, tokenID)
}

func SetApprovalForAll(operator std.Address, approved bool) {
token.SetApprovalForAll(operator, approved)
}

func GetApproved(tokenID string) std.Address {
return token.GetApproved(tokenID)
}

func IsApprovedForAll(owner, operator std.Address) bool {
return token.IsApprovedForAll(owner, operator)
}

func TokenURI(tokenID string) string {
return token.TokenURI(tokenID)
}

func SetTokenURI(tokenID string, uri string) string {
if err := o.CallerIsOwner(); err != nil {
panic("only owner can mint NFTs")
}

return token.SetTokenURI(tokenID, uri)
}

func Name() string {
return token.Name()
}

func Symbol() string {
return token.Symbol()
}

func Mint(to std.Address, tokenID string) {
if err := o.CallerIsOwner(); err != nil {
panic("only owner can mint NFTs")
}

token.Mint(to, tokenID)

ids = append(ids, idCounter)
idCounter += 1
}

func GetCollectionOwner() std.Address {
return o.Owner()
}

// Render renders tokens & their owners
func Render(_ string) string {
var buf bytes.Buffer

buf.WriteString(ufmt.Sprintf("# NFT Collection: \"%s\" $%s\n\n", Name(), Symbol()))

for i := 0; i < len(ids); i++ {
owner := token.OwnerOf(strconv.Itoa(ids[i]))
str := ufmt.Sprintf("#### TokenID #%d - owned by %s\n", ids[i], owner.String())
buf.WriteString(str)
}

return buf.String()
}
7 changes: 7 additions & 0 deletions examples/gno.land/r/x/grc721-by-spec/grc721-gno/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module gno.land/p/demo/xgrc721

require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/testutils v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
)
261 changes: 261 additions & 0 deletions examples/gno.land/r/x/grc721-by-spec/grc721-gno/grc721.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package xgrc721

import (
"gno.land/p/demo/avl"
"gno.land/p/demo/ufmt"
"std"
)

type Token struct {
name string
symbol string
owners *avl.Tree // tokenID > std.Address
balances *avl.Tree // std.Address > # of owned tokens
tokenApprovals *avl.Tree // tokenID > std.Address
operatorApprovals *avl.Tree // "OwnerAddress:OperatorAddress" -> bool
tokenURIs *avl.Tree // tokenID > URI
}

var _ IGRC721 = (*Token)(nil)

const emptyAddress = std.Address("")

func NewGRC721Token(name, symbol string) *Token {
return &Token{
name: name,
symbol: symbol,
owners: avl.NewTree(),
balances: avl.NewTree(),
// give an address permission to a specific tokenID
tokenApprovals: avl.NewTree(),
// give any addresses permissions for all owners' assets
operatorApprovals: avl.NewTree(),
tokenURIs: avl.NewTree(),
}
}

func (nft Token) Name() string { return nft.name }
func (nft Token) Symbol() string { return nft.symbol }

func (nft Token) BalanceOf(owner std.Address) uint64 {
mustBeValid(owner)

balance, found := nft.balances.Get(owner.String())
if !found {
return 0
}

return balance.(uint64)
}

func (nft Token) OwnerOf(tokenId string) std.Address {
return nft.mustBeOwned(tokenId)
}

func (nft Token) TransferFrom(from, to std.Address, tokenId string) {
caller := std.PrevRealm().Addr()
mustBeValid(to)

prevOwner := nft.update(to, tokenId, caller)
if prevOwner != from {
panic("GRC721: incorrect owner")
}
}

func (nft Token) Approve(to std.Address, tokenId string) {
caller := std.PrevRealm().Addr()

if caller == to {
panic("GRC721: cannot approve yourself")
}

mustBeValid(to)
nft.approve(to, tokenId, caller, true)
}

func (nft Token) SetApprovalForAll(operator std.Address, approved bool) {
caller := std.PrevRealm().Addr()
mustBeValid(operator)

if caller == operator {
panic("GRC721: cannot set operator to yourself")
}

nft.operatorApprovals.Set(operatorKey(caller, operator), approved)

if approved {
std.Emit("ApprovalForAll", "owner", caller.String(), "operator", operator.String(), "approved", "true")
} else {
std.Emit("ApprovalForAll", "owner", caller.String(), "operator", operator.String(), "approved", "false")
} // We do not support strconv.FormatBool yet
}

func (nft Token) GetApproved(tokenId string) std.Address {
_ = nft.mustBeOwned(tokenId)
return nft.getApproved(tokenId)
}

func (nft Token) IsApprovedForAll(owner, operator std.Address) bool {
approved, exists := nft.operatorApprovals.Get(operatorKey(owner, operator))
if !exists || approved == false {
return false
}

return true
}

func (nft Token) TokenURI(tokenId string) string {
nft.mustBeOwned(tokenId)
uri, exists := nft.tokenURIs.Get(tokenId)
if !exists {
return ""
}

return uri.(string)
}

func (nft Token) SetTokenURI(tokenId string, tokenURI string) string {
nft.tokenURIs.Set(tokenId, tokenURI)
return tokenURI
}

func (nft Token) Mint(to std.Address, tokenId string) {
mustBeValid(to)
prevOwner := nft.update(to, tokenId, emptyAddress)
if prevOwner != emptyAddress {
str := ufmt.Sprintf("GRC721: token with id %s has already been minted", tokenId)
panic(str)
}
}

func (nft Token) Burn(tokenId string) {
prevOwner := nft.update(emptyAddress, tokenId, emptyAddress)

if prevOwner == emptyAddress {
str := ufmt.Sprintf("GRC721: Token with ID %s does not exist", tokenId)
panic(str)
}
}

// Helpers
func (nft Token) requireOwner(caller std.Address, tokenId string) {
if caller != nft.mustBeOwned(tokenId) {
panic("GRC721: not owner")
}
}

func (nft Token) getApproved(tokenId string) std.Address {
approved, exists := nft.tokenApprovals.Get(tokenId)
if !exists {
return "" // panic instead?
}

return approved.(std.Address)
}

// mustBeValid panics if the given address is not valid
func mustBeValid(address std.Address) {
if !address.IsValid() {
err := ufmt.Sprintf("GRC721: invalid address %s", address)
panic(err)
}
}

// mustBeOwned panics if token is not owned by an address (does not exist)
// If the token is owned, mustBeOwned returns the owner of the token
func (nft Token) mustBeOwned(tokenId string) std.Address {
owner, exists := nft.owners.Get(tokenId)
if !exists {
err := ufmt.Sprintf("GRC721: token with ID %s does not exist", tokenId)
panic(err)
}

return owner.(std.Address)
}

// checkAuthorized checks if spender is authorized to spend specified token on behalf of owner
// Panics if token doesn't exist, or if spender is not authorized in any way
func (nft Token) checkAuthorized(owner, spender std.Address, tokenId string) {
_ = nft.mustBeOwned(tokenId)

if !nft.isAuthorized(owner, spender, tokenId) {
str := ufmt.Sprintf("GRC721: %s is not authorized for %s", spender, tokenId)
panic(str)
}
}

// isAuthorized returns if the spender is authorized to transfer the specified token
// Assumes addresses are valid and the token exists
func (nft Token) isAuthorized(owner, spender std.Address, tokenId string) bool {
return owner == spender ||
nft.IsApprovedForAll(owner, spender) ||
nft.getApproved(tokenId) == owner
}

func (nft Token) update(to std.Address, tokenId string, auth std.Address) std.Address {
from := nft.ownerOf(tokenId)

if auth != emptyAddress {
nft.checkAuthorized(from, auth, tokenId)
}

// If token exists
if from != emptyAddress {
// Clear approval for this token
nft.approve(emptyAddress, tokenId, emptyAddress, false)

// Set new balances
ownerNewBalance, _ := nft.balances.Get(from.String())
nft.balances.Set(from.String(), ownerNewBalance.(uint64)-1)
}

if to != emptyAddress {
toBalance, initialized := nft.balances.Get(to.String())
if !initialized {
nft.balances.Set(to.String(), uint64(1))
} else {
nft.balances.Set(to.String(), toBalance.(uint64)+1)
}
// Set new ownership
nft.owners.Set(tokenId, to)
} else {
// Burn
_, removed := nft.owners.Remove(tokenId)
if !removed {
str := ufmt.Sprintf("GRC721: Cannot burn token with id %s", tokenId)
panic(str)
}
}

std.Emit("Transfer", "from", from.String(), "to", to.String(), "tokenID", tokenId)
return from
}

func (nft Token) approve(to std.Address, tokenId string, auth std.Address, emitEvent bool) {
if emitEvent || auth != emptyAddress {
owner := nft.mustBeOwned(tokenId)

if auth != emptyAddress && owner != auth && !nft.IsApprovedForAll(owner, auth) {
panic("GRC721: invalid approver")
}
if emitEvent {
std.Emit("Approval", "owner", owner.String(), "approved", to.String(), "tokenID", tokenId)
}
}

nft.tokenApprovals.Set(tokenId, to)
}

func (nft Token) ownerOf(tokenId string) std.Address {
owner, exists := nft.owners.Get(tokenId)
if !exists {
return emptyAddress
}

return owner.(std.Address)
}

// operatorKey is a helper to create the key for the operatorApproval tree
func operatorKey(owner, operator std.Address) string {
return owner.String() + ":" + operator.String()
}
Loading
Loading