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(examples): define metadata & royalty info for GRC721 realm #1962

Merged
merged 27 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d943b63
add royalty info for grc721 realm
linhpn99 Apr 20, 2024
297564e
chore(examlple): add royaltyInfo on the top of grc721Metadata
linhpn99 Apr 20, 2024
49169b3
Merge branch 'master' into grc721Royalty
linhpn99 Apr 20, 2024
33f421a
define RoyaltyCalculator to support clients to customize the calculat…
linhpn99 Apr 22, 2024
b955922
Merge branch 'grc721Royalty' of https://github.com/linhpn99/gno into …
linhpn99 Apr 22, 2024
3acadc1
redefine GRC721Royalty interface
linhpn99 Apr 22, 2024
04f5778
Merge branch 'master' into grc721Royalty
linhpn99 Apr 22, 2024
9eb715e
redefine IGRC721Metadata interface
linhpn99 Apr 22, 2024
b995152
add needed functions for IRC721Metadata
linhpn99 Apr 22, 2024
8fb8341
format error with %s
linhpn99 Apr 22, 2024
3945bef
rename file + split interface
linhpn99 Apr 22, 2024
2d260bc
add more comments
linhpn99 Apr 22, 2024
7e0c609
Merge branch 'master' into grc721Royalty
linhpn99 May 7, 2024
5831669
tmp
linhpn99 May 8, 2024
fcef7d0
Merge branch 'master' into grc721Royalty
linhpn99 May 8, 2024
2bc03b5
remove bool param from return
linhpn99 May 8, 2024
9067b07
add comment for RoyaltyInfo model
linhpn99 May 8, 2024
aebba74
define maxRoyaltyPercentage
linhpn99 May 8, 2024
fd49c8b
update
linhpn99 May 8, 2024
b59c378
update
linhpn99 May 8, 2024
c035de8
NewTree
linhpn99 May 8, 2024
01cdea5
update testcases
linhpn99 May 8, 2024
e1e10e8
remove redundant else
linhpn99 May 8, 2024
5f0fbaa
Merge branch 'master' into grc721Royalty
linhpn99 May 9, 2024
6cd4caf
Merge branch 'master' into grc721Royalty
linhpn99 May 10, 2024
ea00ba1
format code
linhpn99 May 10, 2024
b811cd5
Merge branch 'grc721Royalty' of https://github.com/linhpn99/gno into …
linhpn99 May 10, 2024
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
5 changes: 5 additions & 0 deletions examples/gno.land/p/demo/grc/grc721/errors.gno
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ var (
ErrTransferToNonGRC721Receiver = errors.New("transfer to non GRC721Receiver implementer")
ErrCallerIsNotOwnerOrApproved = errors.New("caller is not token owner or approved")
ErrTokenIdAlreadyExists = errors.New("token id already exists")

// ERC721Royalty
ErrInvalidRoyaltyPercentage = errors.New("invalid royalty percentage")
ErrInvalidRoyaltyPaymentAddress = errors.New("invalid royalty paymentAddress")
ErrCannotCalculateRoyaltyAmount = errors.New("cannot calculate royalty amount")
)
95 changes: 95 additions & 0 deletions examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package grc721

import (
"std"

"gno.land/p/demo/avl"
)

// metadataNFT represents an NFT with metadata extensions.
type metadataNFT struct {
*basicNFT // Embedded basicNFT struct for basic NFT functionality
extensions *avl.Tree // AVL tree for storing metadata extensions
}

// Ensure that metadataNFT implements the IGRC721MetadataOnchain interface.
var _ IGRC721MetadataOnchain = (*metadataNFT)(nil)

// NewNFTWithMetadata creates a new basic NFT with metadata extensions.
func NewNFTWithMetadata(name string, symbol string) *metadataNFT {
// Create a new basic NFT
nft := NewBasicNFT(name, symbol)

// Return a metadataNFT with basicNFT embedded and an empty AVL tree for extensions
return &metadataNFT{
basicNFT: nft,
extensions: avl.NewTree(),
}
}

// SetTokenMetadata sets metadata for a given token ID.
func (s *metadataNFT) SetTokenMetadata(tid TokenID, metadata Metadata) error {
// Check if the caller is the owner of the token
owner, err := s.basicNFT.OwnerOf(tid)
if err != nil {
return err
}
caller := std.PrevRealm().Addr()
if caller != owner {
return ErrCallerIsNotOwner
}

// Set the metadata for the token ID in the extensions AVL tree
s.extensions.Set(string(tid), metadata)
return nil
}

// TokenMetadata retrieves metadata for a given token ID.
func (s *metadataNFT) TokenMetadata(tid TokenID) (Metadata, error) {
// Retrieve metadata from the extensions AVL tree
metadata, found := s.extensions.Get(string(tid))
if !found {
return Metadata{}, ErrInvalidTokenId
}

return metadata.(Metadata), nil
}

// mint mints a new token and assigns it to the specified address.
func (s *metadataNFT) mint(to std.Address, tid TokenID) error {
// Check if the address is valid
if err := isValidAddress(to); err != nil {
return err
}

// Check if the token ID already exists
if s.basicNFT.exists(tid) {
return ErrTokenIdAlreadyExists
}

s.basicNFT.beforeTokenTransfer(zeroAddress, to, tid, 1)

// Check if the token ID was minted by beforeTokenTransfer
if s.basicNFT.exists(tid) {
return ErrTokenIdAlreadyExists
}

// Increment balance of the recipient address
toBalance, err := s.basicNFT.BalanceOf(to)
if err != nil {
return err
}
toBalance += 1
s.basicNFT.balances.Set(to.String(), toBalance)

// Set owner of the token ID to the recipient address
s.basicNFT.owners.Set(string(tid), to)

// Emit transfer event
event := TransferEvent{zeroAddress, to, tid}
emit(&event)

s.basicNFT.afterTokenTransfer(zeroAddress, to, tid, 1)

return nil
}
133 changes: 133 additions & 0 deletions examples/gno.land/p/demo/grc/grc721/grc721_metadata_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package grc721

import (
"std"
"testing"

"gno.land/p/demo/testutils"
"gno.land/p/demo/users"
)

func TestSetMetadata(t *testing.T) {
// Create a new dummy NFT with metadata
dummy := NewNFTWithMetadata(dummyNFTName, dummyNFTSymbol)
if dummy == nil {
t.Errorf("should not be nil")
}

// Define addresses for testing purposes
addr1 := testutils.TestAddress("alice")
addr2 := testutils.TestAddress("bob")

// Define metadata attributes
name := "test"
description := "test"
image := "test"
imageData := "test"
externalURL := "test"
attributes := []Trait{}
backgroundColor := "test"
animationURL := "test"
youtubeURL := "test"

// Set the original caller to addr1
std.TestSetOrigCaller(addr1) // addr1

// Mint a new token for addr1
dummy.mint(addr1, TokenID("1"))

// Set metadata for token 1
derr := dummy.SetTokenMetadata(TokenID("1"), Metadata{
Name: name,
Description: description,
Image: image,
ImageData: imageData,
ExternalURL: externalURL,
Attributes: attributes,
BackgroundColor: backgroundColor,
AnimationURL: animationURL,
YoutubeURL: youtubeURL,
})

// Check if there was an error setting metadata
if derr != nil {
t.Errorf("Should not result in error : %s", derr.Error())
}

// Test case: Invalid token ID
err := dummy.SetTokenMetadata(TokenID("3"), Metadata{
Name: name,
Description: description,
Image: image,
ImageData: imageData,
ExternalURL: externalURL,
Attributes: attributes,
BackgroundColor: backgroundColor,
AnimationURL: animationURL,
YoutubeURL: youtubeURL,
})

// Check if the error returned matches the expected error
if err != ErrInvalidTokenId {
t.Errorf("Expected error %s, got %s", ErrInvalidTokenId, err)
}

// Set the original caller to addr2
std.TestSetOrigCaller(addr2) // addr2

// Try to set metadata for token 1 from addr2 (should fail)
cerr := dummy.SetTokenMetadata(TokenID("1"), Metadata{
Name: name,
Description: description,
Image: image,
ImageData: imageData,
ExternalURL: externalURL,
Attributes: attributes,
BackgroundColor: backgroundColor,
AnimationURL: animationURL,
YoutubeURL: youtubeURL,
})

// Check if the error returned matches the expected error
if cerr != ErrCallerIsNotOwner {
t.Errorf("Expected error %s, got %s", ErrCallerIsNotOwner, cerr)
}

// Set the original caller back to addr1
std.TestSetOrigCaller(addr1) // addr1

// Retrieve metadata for token 1
dummyMetadata, err := dummy.TokenMetadata(TokenID("1"))
if err != nil {
t.Errorf("Metadata error: %s", err.Error())
}

// Check if metadata attributes match expected values
if dummyMetadata.Image != image {
t.Errorf("Expected Metadata's image %s, got %s", image, dummyMetadata.Image)
}
if dummyMetadata.ImageData != imageData {
t.Errorf("Expected Metadata's imageData %s, got %s", imageData, dummyMetadata.ImageData)
}
if dummyMetadata.ExternalURL != externalURL {
t.Errorf("Expected Metadata's externalURL %s, got %s", externalURL, dummyMetadata.ExternalURL)
}
if dummyMetadata.Description != description {
t.Errorf("Expected Metadata's description %s, got %s", description, dummyMetadata.Description)
}
if dummyMetadata.Name != name {
t.Errorf("Expected Metadata's name %s, got %s", name, dummyMetadata.Name)
}
if len(dummyMetadata.Attributes) != len(attributes) {
t.Errorf("Expected %d Metadata's attributes, got %d", len(attributes), len(dummyMetadata.Attributes))
}
if dummyMetadata.BackgroundColor != backgroundColor {
t.Errorf("Expected Metadata's backgroundColor %s, got %s", backgroundColor, dummyMetadata.BackgroundColor)
}
if dummyMetadata.AnimationURL != animationURL {
t.Errorf("Expected Metadata's animationURL %s, got %s", animationURL, dummyMetadata.AnimationURL)
}
if dummyMetadata.YoutubeURL != youtubeURL {
t.Errorf("Expected Metadata's youtubeURL %s, got %s", youtubeURL, dummyMetadata.YoutubeURL)
}
}
78 changes: 78 additions & 0 deletions examples/gno.land/p/demo/grc/grc721/grc721_royalty.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package grc721

import (
"std"

"gno.land/p/demo/avl"
)

// royaltyNFT represents a non-fungible token (NFT) with royalty functionality.
type royaltyNFT struct {
*metadataNFT // Embedding metadataNFT for NFT functionality
tokenRoyaltyInfo *avl.Tree // AVL tree to store royalty information for each token
maxRoyaltyPercentage uint64 // maxRoyaltyPercentage represents the maximum royalty percentage that can be charged every sale
}

// Ensure that royaltyNFT implements the IGRC2981 interface.
var _ IGRC2981 = (*royaltyNFT)(nil)

// NewNFTWithRoyalty creates a new royalty NFT with the specified name, symbol, and royalty calculator.
func NewNFTWithRoyalty(name string, symbol string) *royaltyNFT {
// Create a new NFT with metadata
nft := NewNFTWithMetadata(name, symbol)

return &royaltyNFT{
metadataNFT: nft,
tokenRoyaltyInfo: avl.NewTree(),
maxRoyaltyPercentage: 100,
}
}

// SetTokenRoyalty sets the royalty information for a specific token ID.
func (r *royaltyNFT) SetTokenRoyalty(tid TokenID, royaltyInfo RoyaltyInfo) error {
// Validate the payment address
if err := isValidAddress(royaltyInfo.PaymentAddress); err != nil {
return ErrInvalidRoyaltyPaymentAddress
}

// Check if royalty percentage exceeds maxRoyaltyPercentage
if royaltyInfo.Percentage > r.maxRoyaltyPercentage {
return ErrInvalidRoyaltyPercentage
}

// Check if the caller is the owner of the token
owner, err := r.metadataNFT.OwnerOf(tid)
if err != nil {
return err
}
caller := std.PrevRealm().Addr()
if caller != owner {
return ErrCallerIsNotOwner
}

// Set royalty information for the token
r.tokenRoyaltyInfo.Set(string(tid), royaltyInfo)

return nil
}

// RoyaltyInfo returns the royalty information for the given token ID and sale price.
func (r *royaltyNFT) RoyaltyInfo(tid TokenID, salePrice uint64) (std.Address, uint64, error) {
linhpn99 marked this conversation as resolved.
Show resolved Hide resolved
// Retrieve royalty information for the token
val, found := r.tokenRoyaltyInfo.Get(string(tid))
if !found {
return "", 0, ErrInvalidTokenId
}

royaltyInfo := val.(RoyaltyInfo)

// Calculate royalty amount
royaltyAmount, _ := r.calculateRoyaltyAmount(salePrice, royaltyInfo.Percentage)

return royaltyInfo.PaymentAddress, royaltyAmount, nil
}

func (r *royaltyNFT) calculateRoyaltyAmount(salePrice, percentage uint64) (uint64, error) {
royaltyAmount := (salePrice * percentage) / 100
return royaltyAmount, nil
}
Loading
Loading