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 5 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")
)
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 {
IGRC721

SetExtension(tid TokenID, extension Metadata) error
Extension(tid TokenID) (Metadata, error)
}
linhpn99 marked this conversation as resolved.
Show resolved Hide resolved

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
}
22 changes: 22 additions & 0 deletions examples/gno.land/p/demo/grc/grc721/igrc721_royalty.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package grc721

import (
"std"
)

type IGRC721Royalty interface {
IGRC721Metadata

SetTokenRoyalty(tokenID TokenID, royaltyInfo RoyaltyInfo) (bool, error)
RoyaltyInfo(tokenID TokenID, salePrice uint64) (std.Address, uint64, error)
linhpn99 marked this conversation as resolved.
Show resolved Hide resolved
}

// royaltyCalculator is an interface for calculating royalty amounts.
type RoyaltyCalculator interface {
RoyaltyAmount(salePrice, percentage uint64) (uint64, error)
}

type RoyaltyInfo struct {
PaymentAddress std.Address
Percentage uint64
}
linhpn99 marked this conversation as resolved.
Show resolved Hide resolved
85 changes: 85 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,85 @@
package grc721

import (
"std"

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

// metadataNFT represents an NFT with metadata extensions.
type metadataNFT struct {
*basicNFT

extensions avl.Tree
}

// 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 {

nft := NewBasicNFT(name, symbol)

return &metadataNFT{
basicNFT: nft,
extensions: avl.Tree{},
}
}

// SetExtension sets the metadata extension for the given token ID.
func (s *metadataNFT) SetExtension(tid TokenID, extension Metadata) (bool, error) {
// Check for the right owner
owner, err := s.basicNFT.OwnerOf(tid)
if err != nil {
return false, err
}
caller := std.PrevRealm().Addr()
if caller != owner {
return false, ErrCallerIsNotOwner
}
s.extensions.Set(string(tid), extension)
return true, nil
}

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

return extension.(Metadata), nil
}

func (s *metadataNFT) mint(to std.Address, tid TokenID) error {
if err := isValidAddress(to); err != nil {
return err
}

if s.basicNFT.exists(tid) {
return ErrTokenIdAlreadyExists
}

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

// Check that tokenId was not minted by `beforeTokenTransfer`
if s.basicNFT.exists(tid) {
return ErrTokenIdAlreadyExists
}

toBalance, err := s.basicNFT.BalanceOf(to)
if err != nil {
return err
}
toBalance += 1
s.basicNFT.balances.Set(to.String(), toBalance)
s.basicNFT.owners.Set(string(tid), to)

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

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

return nil
}
130 changes: 130 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,130 @@
package grc721

import (
"std"
"testing"

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

func TestSetExtension(t *testing.T) {
dummy := NewNFTWithMetadata(dummyNFTName, dummyNFTSymbol)
if dummy == nil {
t.Errorf("should not be nil")
}

addr1 := std.Address("g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm")
addr2 := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj")

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

std.TestSetOrigCaller(std.Address(addr1)) // addr1

dummy.mint(addr1, TokenID("1"))

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

if derr != nil {
t.Errorf("Should not result in error ", derr.Error())
}

// Test case: Invalid token ID
_, err := dummy.SetExtension(TokenID("3"), Metadata{
Name : name,
Description: description,
Image: image,
ImageData: imageData,
ExternalURL: externalURL,
Attributes: attributes,
BackgroundColor: backgroundColor,
AnimationURL: animationURL,
YoutubeURL: youtubeURL,
})
if err != ErrInvalidTokenId {
t.Errorf("Expected error %v, got %v", ErrInvalidTokenId, err)
}

std.TestSetOrigCaller(std.Address(addr2)) // addr2

_, cerr := dummy.SetExtension(TokenID("1"), Metadata{
Name : name,
Description: description,
Image: image,
ImageData: imageData,
ExternalURL: externalURL,
Attributes: attributes,
BackgroundColor: backgroundColor,
AnimationURL: animationURL,
YoutubeURL: youtubeURL,
}) // addr2 trying to set Extension for token 1
if cerr != ErrCallerIsNotOwner {
t.Errorf("Expected error %v, got %v", ErrCallerIsNotOwner, cerr)
}

// Test case: Retrieving Extension
std.TestSetOrigCaller(std.Address(addr1)) // addr1

dummyExtension, err := dummy.Extension(TokenID("1"))
if err != nil {
t.Errorf("Extension error: %v", err.Error())

} else {
if dummyExtension.Image != image {
t.Errorf("Expected Extension's image %v, got %v", image, dummyExtension.Image)
}

if dummyExtension.ImageData != imageData {
t.Errorf("Expected Extension's imageData %v, got %v", imageData, dummyExtension.ImageData)
}

if dummyExtension.ExternalURL != externalURL {
t.Errorf("Expected Extension's externalURL %v, got %v", externalURL, dummyExtension.ExternalURL)
}

if dummyExtension.Description != description {
t.Errorf("Expected Extension's description %v, got %v", description, dummyExtension.Description)
}

if dummyExtension.Name != name {
t.Errorf("Expected Extension's name %v, got %v", name, dummyExtension.Name)
}

if len(dummyExtension.Attributes) != len(attributes) {
t.Errorf("Expected %d Extension's attributes %v, got %v", len(attributes), len(dummyExtension.Attributes))
}

if dummyExtension.BackgroundColor != backgroundColor {
t.Errorf("Expected Extension's backgroundColor %v, got %v", backgroundColor, dummyExtension.BackgroundColor)
}

if dummyExtension.AnimationURL != animationURL {
t.Errorf("Expected Extension's animationURL %v, got %v", animationURL, dummyExtension.AnimationURL)
}

if dummyExtension.YoutubeURL != youtubeURL {
t.Errorf("Expected Extension's youtubeURL %v, got %v", youtubeURL, dummyExtension.YoutubeURL)
}
}


}

99 changes: 99 additions & 0 deletions examples/gno.land/p/demo/grc/grc721/nft_royalty.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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
calculator RoyaltyCalculator // Interface for calculating royalty amount
}

type royaltyCalculator struct {}

func NewDefaultRoyaltyCalculator() *royaltyCalculator {
return &royaltyCalculator{}
}

func (r *royaltyCalculator) RoyaltyAmount(salePrice, percentage uint64) (uint64, error) {
royaltyAmount := (salePrice * percentage) / 100
return royaltyAmount, nil
}

type Option func(*royaltyNFT)

func WithCalculator(c RoyaltyCalculator) Option {
return func(r *royaltyNFT) {
r.calculator = c
}
}

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

r := &royaltyNFT{
metadataNFT: nft,
tokenRoyaltyInfo: avl.Tree{},
calculator: NewDefaultRoyaltyCalculator(),
}

for _, opt := range opts {
if opt != nil {
opt(r)
}
}

return r
}

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

// Check if royalty percentage exceeds 100%
if royaltyInfo.Percentage > 100 {
return false, ErrInvalidRoyaltyPercentage
}

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

// Set royalty information for the token
r.tokenRoyaltyInfo.Set(string(tid), royaltyInfo)
return true, 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 using the provided calculator
royaltyAmount, err := r.calculator.RoyaltyAmount(salePrice, royaltyInfo.Percentage)
if err != nil {
return "", 0, ErrCannotCalculateRoyaltyAmount
}

return royaltyInfo.PaymentAddress, royaltyAmount, nil
}
Loading