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 for grc721 #1960

Closed
wants to merge 9 commits into from
27 changes: 27 additions & 0 deletions examples/gno.land/p/demo/grc/grc721/igrc721_metadata.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

package grc721

type IGRC721Metadata interface {
Name() string
Symbol() string
TokenURI(tid TokenID) (string, error)
TokenMetadata(tid TokenID) (Metadata, error)
}

type Trait struct{
DisplayType string
TraitType string
Value string
}

type Metadata struct {
Image string
ImageData string
ExternalURL string
Description string
Name string
Attributes []Trait
BackgroundColor string
AnimationURL string
YoutubeURL string
}
98 changes: 98 additions & 0 deletions examples/gno.land/p/demo/grc/grc721/nft_metadata.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 IGRC721Metadata interface.
var _ IGRC721Metadata = (*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.Tree{},
}
}

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

// Set the metadata for the token ID in the extensions AVL tree
s.extensions.Set(string(tid), metadata)
return true, 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
}
134 changes: 134 additions & 0 deletions examples/gno.land/p/demo/grc/grc721/nft_metadata_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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 := std.Address("g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm")
addr2 := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj")

// 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(std.Address(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(std.Address(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(std.Address(addr1)) // addr1

// Retrieve metadata for token 1
dummyMetadata, err := dummy.TokenMetadata(TokenID("1"))
if err != nil {
t.Errorf("Metadata error: %s", err.Error())
} else {
// 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)
}
}
}