-
Notifications
You must be signed in to change notification settings - Fork 379
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into docs/gnodev-gs
- Loading branch information
Showing
121 changed files
with
1,390 additions
and
352 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
133
examples/gno.land/p/demo/grc/grc721/grc721_metadata_test.gno
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.