Skip to content

Commit

Permalink
feat(examples): define metadata & royalty info for GRC721 realm (#1962)
Browse files Browse the repository at this point in the history
<!-- please provide a detailed description of the changes made in this
pull request. -->

<details><summary>Contributors' checklist...</summary>

- [x] Added new tests, or not needed, or not feasible
- [ ] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [ ] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>
  • Loading branch information
linhpn99 committed May 10, 2024
1 parent abaf103 commit a03eeb3
Show file tree
Hide file tree
Showing 7 changed files with 460 additions and 0 deletions.
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) {
// 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

0 comments on commit a03eeb3

Please sign in to comment.