Skip to content

Commit

Permalink
manifest: add NEP-24
Browse files Browse the repository at this point in the history
Close #3451

Signed-off-by: Ekaterina Pavlova <[email protected]>
  • Loading branch information
AliceInHunterland committed Aug 19, 2024
1 parent 7766168 commit 9b11088
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 1 deletion.
181 changes: 181 additions & 0 deletions pkg/rpcclient/nep11/royalty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Package nep11 provides RPC wrappers for NEP-11 contracts, including support for NEP-24 NFT royalties.
package nep11

import (
"errors"
"fmt"
"math/big"

"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// RoyaltyInfoDetail contains information about the recipient and the royalty amount.
type RoyaltyInfoDetail struct {
RoyaltyRecipient util.Uint160
RoyaltyAmount *big.Int
}

// RoyaltiesTransferredEvent represents a RoyaltiesTransferred event as defined in the NEP-24 standard.
type RoyaltiesTransferredEvent struct {
RoyaltyToken util.Uint160
RoyaltyRecipient util.Uint160
Buyer util.Uint160
TokenID []byte
Amount *big.Int
}

// RoyaltyReader is an interface for contracts implementing NEP-24 royalties.
type RoyaltyReader struct {
BaseReader
}

// RoyaltyWriter is an interface for state-changing methods related to NEP-24 royalties.
type RoyaltyWriter struct {
BaseWriter
}

// Royalty is a full reader and writer interface for NEP-24 royalties.
type Royalty struct {
RoyaltyReader
RoyaltyWriter
}

// NewRoyaltyReader creates an instance of RoyaltyReader for a contract with the given hash using the given invoker.
func NewRoyaltyReader(invoker Invoker, hash util.Uint160) *RoyaltyReader {
return &RoyaltyReader{*NewBaseReader(invoker, hash)}
}

// NewRoyalty creates an instance of Royalty for a contract with the given hash using the given actor.
func NewRoyalty(actor Actor, hash util.Uint160) *Royalty {
return &Royalty{*NewRoyaltyReader(actor, hash), RoyaltyWriter{BaseWriter{hash, actor}}}
}

// RoyaltyInfo retrieves the royalty information for a given token ID, including the recipient(s) and amount(s).
func (r *RoyaltyReader) RoyaltyInfo(tokenID []byte, royaltyToken util.Uint160, salePrice *big.Int) ([]*RoyaltyInfoDetail, error) {
items, err := unwrap.Array(r.invoker.Call(r.hash, "RoyaltyInfo", tokenID, royaltyToken, salePrice))
fmt.Println(items)
if err != nil {
return nil, err
}

royalties := make([]*RoyaltyInfoDetail, len(items))
for i, item := range items {
royaltyDetail, err := itemToRoyaltyInfoDetail(item, nil)
if err != nil {
return nil, fmt.Errorf("failed to decode royalty detail %d: %w", i, err)
}
royalties[i] = royaltyDetail
}
return royalties, nil
}

// itemToRoyaltyInfoDetail converts a stack item into a RoyaltyInfoDetail struct.
func itemToRoyaltyInfoDetail(item stackitem.Item, err error) (*RoyaltyInfoDetail, error) {
if err != nil {
return nil, err
}
fmt.Println("itemToRoyaltyInfoDetail item", item)

arr, ok := item.Value().([]stackitem.Item)
if !ok || len(arr) != 2 {
return nil, fmt.Errorf("invalid RoyaltyInfoDetail structure: expected array of 2 items, got %T", item.Value())
}

recipientBytes, err := arr[0].TryBytes()
if err != nil {
return nil, fmt.Errorf("failed to decode RoyaltyRecipient: %w", err)
}

recipient, err := util.Uint160DecodeBytesBE(recipientBytes)
if err != nil {
return nil, fmt.Errorf("invalid RoyaltyRecipient: %w", err)
}

amount, err := arr[1].TryInteger()
if err != nil {
return nil, fmt.Errorf("failed to decode RoyaltyAmount: %w", err)
}

return &RoyaltyInfoDetail{
RoyaltyRecipient: recipient,
RoyaltyAmount: amount,
}, nil
}

// RoyaltiesTransferredEventsFromApplicationLog retrieves all emitted RoyaltiesTransferredEvents from the provided [result.ApplicationLog].
func RoyaltiesTransferredEventsFromApplicationLog(log *result.ApplicationLog) ([]*RoyaltiesTransferredEvent, error) {
if log == nil {
return nil, errors.New("nil application log")
}
var res []*RoyaltiesTransferredEvent
for i, ex := range log.Executions {
for j, e := range ex.Events {
if e.Name != "RoyaltiesTransferred" {
continue
}
event := new(RoyaltiesTransferredEvent)
err := event.FromStackItem(e.Item)
if err != nil {
return nil, fmt.Errorf("failed to decode event from stackitem (event #%d, execution #%d): %w", j, i, err)
}
res = append(res, event)
}
}
return res, nil
}

// FromStackItem converts a stack item into a RoyaltiesTransferredEvent struct.
func (e *RoyaltiesTransferredEvent) FromStackItem(item *stackitem.Array) error {
if item == nil {
return errors.New("nil item")
}
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not an array")
}
if len(arr) != 5 {
return errors.New("wrong number of event parameters")
}

b, err := arr[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid RoyaltyToken: %w", err)
}
e.RoyaltyToken, err = util.Uint160DecodeBytesBE(b)
if err != nil {
return fmt.Errorf("failed to decode RoyaltyToken: %w", err)
}

b, err = arr[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid RoyaltyRecipient: %w", err)
}
e.RoyaltyRecipient, err = util.Uint160DecodeBytesBE(b)
if err != nil {
return fmt.Errorf("failed to decode RoyaltyRecipient: %w", err)
}

b, err = arr[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid Buyer: %w", err)
}
e.Buyer, err = util.Uint160DecodeBytesBE(b)
if err != nil {
return fmt.Errorf("failed to decode Buyer: %w", err)
}

e.TokenID, err = arr[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid TokenID: %w", err)
}

e.Amount, err = arr[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid Amount: %w", err)
}

return nil
}
83 changes: 83 additions & 0 deletions pkg/rpcclient/nep11/royalty_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package nep11

import (
"errors"
"math/big"
"testing"

"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)

// TestRoyaltyReaderRoyaltyInfo tests the RoyaltyInfo method in the RoyaltyReader.
func TestRoyaltyReaderRoyaltyInfo(t *testing.T) {
ta := new(testAct)
rr := NewRoyaltyReader(ta, util.Uint160{1, 2, 3})

tokenID := []byte{1, 2, 3}
royaltyToken := util.Uint160{4, 5, 6}
salePrice := big.NewInt(1000)

tests := []struct {
name string
setupFunc func()
expectErr bool
expectedRI []RoyaltyInfoDetail
}{
{
name: "error case",
setupFunc: func() {
ta.err = errors.New("some error")
},
expectErr: true,
},
{
name: "valid response",
setupFunc: func() {
ta.err = nil
recipient := util.Uint160{7, 8, 9}
amount := big.NewInt(100)
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{
stackitem.Make(recipient.BytesBE()),
stackitem.Make(amount),
})},
}
},
expectErr: false,
expectedRI: []RoyaltyInfoDetail{
{RoyaltyRecipient: util.Uint160{7, 8, 9}, RoyaltyAmount: big.NewInt(100)},
},
},
{
name: "invalid data response",
setupFunc: func() {
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{
stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()),
}),
},
}
},
expectErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupFunc()
ri, err := rr.RoyaltyInfo(tokenID, royaltyToken, salePrice)
if tt.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expectedRI, ri)
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/smartcontract/manifest/standard/comply.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var (
)

var checks = map[string][]*Standard{
manifest.NEP11StandardName: {Nep11NonDivisible, Nep11Divisible},
manifest.NEP11StandardName: {Nep11NonDivisible, Nep11Divisible, Nep11WithRoyalty},
manifest.NEP17StandardName: {Nep17},
manifest.NEP11Payable: {Nep11Payable},
manifest.NEP17Payable: {Nep17Payable},
Expand Down
39 changes: 39 additions & 0 deletions pkg/smartcontract/manifest/standard/nep24.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package standard

import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
)

// Nep11WithRoyalty is a NEP-24 Standard for NFT royalties.
var Nep11WithRoyalty = &Standard{
Base: Nep11Base,
Manifest: manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{
{
Name: "RoyaltyInfo",
Parameters: []manifest.Parameter{
{Name: "tokenId", Type: smartcontract.ByteArrayType},
{Name: "royaltyToken", Type: smartcontract.Hash160Type},
{Name: "salePrice", Type: smartcontract.IntegerType},
},
ReturnType: smartcontract.ArrayType,
Safe: true,
},
},
Events: []manifest.Event{
{
Name: "RoyaltiesTransferred",
Parameters: []manifest.Parameter{
{Name: "royaltyToken", Type: smartcontract.Hash160Type},
{Name: "royaltyRecipient", Type: smartcontract.Hash160Type},
{Name: "buyer", Type: smartcontract.Hash160Type},
{Name: "tokenId", Type: smartcontract.ByteArrayType},
{Name: "amount", Type: smartcontract.IntegerType},
},
},
},
},
},
}
3 changes: 3 additions & 0 deletions pkg/smartcontract/rpcbinding/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ func Generate(cfg binding.Config) error {
} else if standard.ComplyABI(cfg.Manifest, standard.Nep11NonDivisible) == nil {
mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11NonDivisible)
ctr.IsNep11ND = true
} else if standard.ComplyABI(cfg.Manifest, standard.Nep11WithRoyalty) == nil {
mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11WithRoyalty)
ctr.IsNep11D = true
}
mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep11Base)
break // Can't be NEP-17 at the same time.
Expand Down

0 comments on commit 9b11088

Please sign in to comment.