From 8cf1b906f89cd08040eaf9a6396f08f44e13b563 Mon Sep 17 00:00:00 2001 From: Ekaterina Pavlova Date: Tue, 15 Oct 2024 19:59:21 +0300 Subject: [PATCH] manifest: support NEP-24 Close #3451 Signed-off-by: Ekaterina Pavlova --- examples/nft-nd/nft.go | 22 ++ examples/nft-nd/nft.yml | 18 +- pkg/rpcclient/nep24/doc_test.go | 32 ++ pkg/rpcclient/nep24/royalty.go | 163 +++++++++ pkg/rpcclient/nep24/royalty_test.go | 314 ++++++++++++++++++ pkg/smartcontract/manifest/manifest.go | 2 + pkg/smartcontract/manifest/standard/check.go | 2 + pkg/smartcontract/manifest/standard/comply.go | 5 + pkg/smartcontract/manifest/standard/nep11.go | 3 + pkg/smartcontract/manifest/standard/nep17.go | 1 + pkg/smartcontract/manifest/standard/nep24.go | 39 +++ .../manifest/standard/payable.go | 2 + pkg/smartcontract/rpcbinding/binding.go | 18 +- 13 files changed, 616 insertions(+), 5 deletions(-) create mode 100644 pkg/rpcclient/nep24/doc_test.go create mode 100644 pkg/rpcclient/nep24/royalty.go create mode 100644 pkg/rpcclient/nep24/royalty_test.go create mode 100644 pkg/smartcontract/manifest/standard/nep24.go diff --git a/examples/nft-nd/nft.go b/examples/nft-nd/nft.go index 78a8bc29ec..23b8a2ab63 100644 --- a/examples/nft-nd/nft.go +++ b/examples/nft-nd/nft.go @@ -8,6 +8,8 @@ you own a hash it's HASHY. package nft import ( + "math/big" + "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/contract" "github.com/nspcc-dev/neo-go/pkg/interop/iterator" @@ -285,3 +287,23 @@ func Properties(id []byte) map[string]string { } return result } + +// RoyaltyRecipient contains information about the recipient and the royalty amount. +type RoyaltyRecipient struct { + Address interop.Hash160 + Amount *big.Int +} + +func RoyaltyInfo(tokenID []byte, royaltyToken interop.Hash160, salePrice int) []RoyaltyRecipient { + ctx := storage.GetReadOnlyContext() + owner := getOwnerOf(ctx, tokenID) + if !runtime.CheckWitness(owner) { + panic("invalid owner") + } + return []RoyaltyRecipient{ + { + Address: owner, + Amount: big.NewInt(int64(salePrice / 10)), + }, + } +} diff --git a/examples/nft-nd/nft.yml b/examples/nft-nd/nft.yml index ff63571505..6d16fc7229 100644 --- a/examples/nft-nd/nft.yml +++ b/examples/nft-nd/nft.yml @@ -1,7 +1,7 @@ name: "HASHY NFT" sourceurl: https://github.com/nspcc-dev/neo-go/ -supportedstandards: ["NEP-11"] -safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "tokens", "properties"] +supportedstandards: ["NEP-11","NEP-24"] +safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "tokens", "properties", "royaltyInfo"] events: - name: Transfer parameters: @@ -13,7 +13,19 @@ events: type: Integer - name: tokenId type: ByteArray + - name: RoyaltiesTransferred + parameters: + - name: royaltyToken + type: Hash160 + - name: royaltyRecipient + type: Hash160 + - name: buyer + type: Hash160 + - name: tokenId + type: ByteArray + - name: amount + type: Integer permissions: - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd methods: ["update", "destroy"] - - methods: ["onNEP11Payment"] + - methods: ["onNEP11Payment"] \ No newline at end of file diff --git a/pkg/rpcclient/nep24/doc_test.go b/pkg/rpcclient/nep24/doc_test.go new file mode 100644 index 0000000000..e1ffb104c5 --- /dev/null +++ b/pkg/rpcclient/nep24/doc_test.go @@ -0,0 +1,32 @@ +package nep24_test + +import ( + "context" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep24" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +func ExampleRoyaltyReader() { + // No error checking done at all, intentionally. + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Safe methods are reachable with just an invoker, no need for an account there. + inv := invoker.New(c, nil) + + // NEP-24 contract hash. + nep24Hash := util.Uint160{9, 8, 7} + + // And a reader interface. + n24 := nep24.RoyaltyReader{Invoker: inv, Hash: nep24Hash} + + // Get the royalty information for a token. + tokenID := []byte("someTokenID") + royaltyToken := util.Uint160{1, 2, 3} + salePrice := big.NewInt(1000) + royaltyInfo, _ := n24.RoyaltyInfo(tokenID, royaltyToken, salePrice) + _ = royaltyInfo +} diff --git a/pkg/rpcclient/nep24/royalty.go b/pkg/rpcclient/nep24/royalty.go new file mode 100644 index 0000000000..524e267a03 --- /dev/null +++ b/pkg/rpcclient/nep24/royalty.go @@ -0,0 +1,163 @@ +/* +Package nep24 contains RPC wrappers to work with NEP-24 contracts. + +Safe methods are encapsulated into RoyaltyReader structure. +*/ +package nep24 + +import ( + "errors" + "fmt" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neptoken" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// RoyaltyRecipient contains information about the recipient and the royalty amount. +type RoyaltyRecipient struct { + Address util.Uint160 + Amount *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 represents safe (read-only) methods of NEP-24 token. It can be +// used to query data about royalties. +type RoyaltyReader struct { + Invoker neptoken.Invoker + Hash util.Uint160 +} + +// RoyaltyInfo returns the royalty information for the given tokenID, royaltyToken, +// and salePrice. +func (c *RoyaltyReader) RoyaltyInfo(tokenID []byte, royaltyToken util.Uint160, salePrice *big.Int) ([]RoyaltyRecipient, error) { + res, err := c.Invoker.Call(c.Hash, "royaltyInfo", tokenID, royaltyToken, salePrice) + if err != nil { + return nil, err + } + var royalties []RoyaltyRecipient + for _, item := range res.Stack { + royalty, ok := item.Value().([]stackitem.Item) + if !ok || len(royalty) != 2 { + return nil, fmt.Errorf("invalid royalty structure: expected array of 2 items, got %d", len(royalty)) + } + var recipient RoyaltyRecipient + err = recipient.FromStackItem(royalty) + if err != nil { + return nil, fmt.Errorf("failed to decode royalty detail: %w", err) + } + royalties = append(royalties, recipient) + } + + return royalties, nil +} + +// FromStackItem converts a stack item into a RoyaltyRecipient struct. +func (r *RoyaltyRecipient) FromStackItem(item []stackitem.Item) error { + if len(item) != 2 { + return fmt.Errorf("invalid royalty structure: expected 2 items, got %d", len(item)) + } + + recipientBytes, err := item[0].TryBytes() + if err != nil { + return fmt.Errorf("failed to decode recipient address: %w", err) + } + + recipient, err := util.Uint160DecodeBytesBE(recipientBytes) + if err != nil { + return fmt.Errorf("invalid recipient address: %w", err) + } + + amountBigInt, err := item[1].TryInteger() + if err != nil { + return fmt.Errorf("failed to decode royalty amount: %w", err) + } + amount := big.NewInt(0).Set(amountBigInt) + r.Amount = amount + r.Address = recipient + return 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 || len(arr) != 5 { + return errors.New("invalid event structure: expected array of 5 items") + } + + b, err := arr[0].TryBytes() + if err != nil { + return fmt.Errorf("failed to decode RoyaltyToken: %w", err) + } + e.RoyaltyToken, err = util.Uint160DecodeBytesBE(b) + if err != nil { + return fmt.Errorf("invalid RoyaltyToken: %w", err) + } + + b, err = arr[1].TryBytes() + if err != nil { + return fmt.Errorf("failed to decode RoyaltyRecipient: %w", err) + } + e.RoyaltyRecipient, err = util.Uint160DecodeBytesBE(b) + if err != nil { + return fmt.Errorf("invalid RoyaltyRecipient: %w", err) + } + + b, err = arr[2].TryBytes() + if err != nil { + return fmt.Errorf("failed to decode Buyer: %w", err) + } + e.Buyer, err = util.Uint160DecodeBytesBE(b) + if err != nil { + return fmt.Errorf("invalid Buyer: %w", err) + } + + e.TokenID, err = arr[3].TryBytes() + if err != nil { + return fmt.Errorf("failed to decode TokenID: %w", err) + } + + e.Amount, err = arr[4].TryInteger() + if err != nil { + return fmt.Errorf("failed to decode Amount: %w", err) + } + + return nil +} diff --git a/pkg/rpcclient/nep24/royalty_test.go b/pkg/rpcclient/nep24/royalty_test.go new file mode 100644 index 0000000000..dfdc2c8e6e --- /dev/null +++ b/pkg/rpcclient/nep24/royalty_test.go @@ -0,0 +1,314 @@ +package nep24 + +import ( + "errors" + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/state" + "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" +) + +type testAct struct { + err error + res *result.Invoke +} + +func (t *testAct) Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) { + return t.res, t.err +} + +func TestRoyaltyReaderRoyaltyInfo(t *testing.T) { + ta := new(testAct) + rr := &RoyaltyReader{ + Invoker: ta, + Hash: 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 []RoyaltyRecipient + }{ + { + 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), + }), stackitem.Make([]stackitem.Item{ + stackitem.Make(recipient.BytesBE()), + stackitem.Make(amount), + })}, + } + }, + expectErr: false, + expectedRI: []RoyaltyRecipient{ + { + Address: util.Uint160{7, 8, 9}, + Amount: big.NewInt(100), + }, + { + Address: util.Uint160{7, 8, 9}, + Amount: 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) + } + }) + } +} + +func TestItemToRoyaltyRecipient(t *testing.T) { + tests := []struct { + name string + items []stackitem.Item + expectErr bool + expected RoyaltyRecipient + }{ + { + name: "valid input", + items: []stackitem.Item{ + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), + stackitem.Make(big.NewInt(100)), + }, + expectErr: false, + expected: RoyaltyRecipient{ + Address: util.Uint160{7, 8, 9}, + Amount: big.NewInt(100), + }, + }, + { + name: "invalid number of items", + items: []stackitem.Item{ + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), + }, + expectErr: true, + }, + { + name: "invalid recipient size", + items: []stackitem.Item{ + stackitem.Make([]byte{1, 2}), + stackitem.Make(big.NewInt(100)), + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ri RoyaltyRecipient + err := ri.FromStackItem(tt.items) + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, ri) + } + }) + } +} + +func TestRoyaltiesTransferredEventFromStackitem(t *testing.T) { + tests := []struct { + name string + item *stackitem.Array + expectErr bool + expected *RoyaltiesTransferredEvent + }{ + { + name: "valid stack item", + item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(util.Uint160{1, 2, 3}.BytesBE()), // RoyaltyToken + stackitem.Make(util.Uint160{4, 5, 6}.BytesBE()), // RoyaltyRecipient + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), // Buyer + stackitem.Make([]byte{1, 2, 3}), // TokenID + stackitem.Make(big.NewInt(100)), // Amount + }), + expectErr: false, + expected: &RoyaltiesTransferredEvent{ + RoyaltyToken: util.Uint160{1, 2, 3}, + RoyaltyRecipient: util.Uint160{4, 5, 6}, + Buyer: util.Uint160{7, 8, 9}, + TokenID: []byte{1, 2, 3}, + Amount: big.NewInt(100), + }, + }, + { + name: "invalid number of items", + item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(util.Uint160{1, 2, 3}.BytesBE()), // Only one item + }), + expectErr: true, + }, + { + name: "invalid recipient size", + item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(util.Uint160{1, 2, 3}.BytesBE()), // RoyaltyToken + stackitem.Make([]byte{1, 2}), // Invalid RoyaltyRecipient + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), // Buyer + stackitem.Make([]byte{1, 2, 3}), // TokenID + stackitem.Make(big.NewInt(100)), // Amount + }), + expectErr: true, + }, + { + name: "invalid integer amount", + item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(util.Uint160{1, 2, 3}.BytesBE()), // RoyaltyToken + stackitem.Make(util.Uint160{4, 5, 6}.BytesBE()), // RoyaltyRecipient + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), // Buyer + stackitem.Make([]byte{1, 2, 3}), // TokenID + stackitem.Make(stackitem.NewStruct(nil)), // Invalid integer for Amount + }), + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := new(RoyaltiesTransferredEvent) + err := event.FromStackItem(tt.item) + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, event) + } + }) + } +} + +func TestRoyaltiesTransferredEventsFromApplicationLog(t *testing.T) { + createEvent := func(token, recipient, buyer util.Uint160, tokenID []byte, amount *big.Int) state.NotificationEvent { + return state.NotificationEvent{ + ScriptHash: util.Uint160{1, 2, 3}, // Any contract address. + Name: "RoyaltiesTransferred", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(token.BytesBE()), // RoyaltyToken + stackitem.Make(recipient.BytesBE()), // RoyaltyRecipient + stackitem.Make(buyer.BytesBE()), // Buyer + stackitem.Make(tokenID), // TokenID + stackitem.Make(amount), // Amount + }), + } + } + + tests := []struct { + name string + log *result.ApplicationLog + expectErr bool + expected []*RoyaltiesTransferredEvent + }{ + { + name: "valid log with one event", + log: &result.ApplicationLog{ + Executions: []state.Execution{ + { + Events: []state.NotificationEvent{ + createEvent( + util.Uint160{1, 2, 3}, // RoyaltyToken + util.Uint160{4, 5, 6}, // RoyaltyRecipient + util.Uint160{7, 8, 9}, // Buyer + []byte{1, 2, 3}, // TokenID + big.NewInt(100), // Amount + ), + }, + }, + }, + }, + expectErr: false, + expected: []*RoyaltiesTransferredEvent{ + { + RoyaltyToken: util.Uint160{1, 2, 3}, + RoyaltyRecipient: util.Uint160{4, 5, 6}, + Buyer: util.Uint160{7, 8, 9}, + TokenID: []byte{1, 2, 3}, + Amount: big.NewInt(100), + }, + }, + }, + { + name: "invalid event structure (missing fields)", + log: &result.ApplicationLog{ + Executions: []state.Execution{ + { + Events: []state.NotificationEvent{ + { + Name: "RoyaltiesTransferred", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(util.Uint160{1, 2, 3}.BytesBE()), // RoyaltyToken + // Missing other fields + }), + }, + }, + }, + }, + }, + expectErr: true, + }, + { + name: "empty log", + log: &result.ApplicationLog{}, + expectErr: false, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + events, err := RoyaltiesTransferredEventsFromApplicationLog(tt.log) + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, events) + } + }) + } +} diff --git a/pkg/smartcontract/manifest/manifest.go b/pkg/smartcontract/manifest/manifest.go index f709c21ae0..ff9e624df9 100644 --- a/pkg/smartcontract/manifest/manifest.go +++ b/pkg/smartcontract/manifest/manifest.go @@ -26,6 +26,8 @@ const ( NEP11Payable = "NEP-11-Payable" // NEP17Payable represents the name of contract interface which can receive NEP-17 tokens. NEP17Payable = "NEP-17-Payable" + // NEP24StandardName represents the name of the NEP-24 smart contract standard for NFT royalties. + NEP24StandardName = "NEP-24" emptyFeatures = "{}" ) diff --git a/pkg/smartcontract/manifest/standard/check.go b/pkg/smartcontract/manifest/standard/check.go index 2957c265df..9fb9a14fee 100644 --- a/pkg/smartcontract/manifest/standard/check.go +++ b/pkg/smartcontract/manifest/standard/check.go @@ -12,4 +12,6 @@ type Standard struct { // If contract contains method with the same name and parameter count, // it must have signature declared by this contract. Optional []manifest.Method + // Required contains standards that are required for this standard. + Required []string } diff --git a/pkg/smartcontract/manifest/standard/comply.go b/pkg/smartcontract/manifest/standard/comply.go index f8991c7933..a8abbb8b59 100644 --- a/pkg/smartcontract/manifest/standard/comply.go +++ b/pkg/smartcontract/manifest/standard/comply.go @@ -23,6 +23,7 @@ var checks = map[string][]*Standard{ manifest.NEP17StandardName: {Nep17}, manifest.NEP11Payable: {Nep11Payable}, manifest.NEP17Payable: {Nep17Payable}, + manifest.NEP24StandardName: {Nep24Royalty}, } // Check checks if the manifest complies with all provided standards. @@ -66,6 +67,10 @@ func ComplyABI(m *manifest.Manifest, st *Standard) error { } func comply(m *manifest.Manifest, checkNames bool, st *Standard) error { + if err := check(m, checkNames, st.Required...); err != nil { + return err + } + if st.Base != nil { if err := comply(m, checkNames, st.Base); err != nil { return err diff --git a/pkg/smartcontract/manifest/standard/nep11.go b/pkg/smartcontract/manifest/standard/nep11.go index 7f94173fa4..cca00cebe6 100644 --- a/pkg/smartcontract/manifest/standard/nep11.go +++ b/pkg/smartcontract/manifest/standard/nep11.go @@ -65,6 +65,7 @@ var Nep11Base = &Standard{ Safe: true, }, }, + Required: []string{manifest.NEP11StandardName}, } // Nep11NonDivisible is a NEP-11 non-divisible Standard. @@ -84,6 +85,7 @@ var Nep11NonDivisible = &Standard{ }, }, }, + Required: []string{manifest.NEP11StandardName}, } // Nep11Divisible is a NEP-11 divisible Standard. @@ -123,4 +125,5 @@ var Nep11Divisible = &Standard{ }, }, }, + Required: []string{manifest.NEP11StandardName}, } diff --git a/pkg/smartcontract/manifest/standard/nep17.go b/pkg/smartcontract/manifest/standard/nep17.go index 4d4422aab4..d4d0e2d40e 100644 --- a/pkg/smartcontract/manifest/standard/nep17.go +++ b/pkg/smartcontract/manifest/standard/nep17.go @@ -42,4 +42,5 @@ var Nep17 = &Standard{ }, }, }, + Required: []string{manifest.NEP17StandardName}, } diff --git a/pkg/smartcontract/manifest/standard/nep24.go b/pkg/smartcontract/manifest/standard/nep24.go new file mode 100644 index 0000000000..54c6ae9a9d --- /dev/null +++ b/pkg/smartcontract/manifest/standard/nep24.go @@ -0,0 +1,39 @@ +package standard + +import ( + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" +) + +// Nep24Royalty is a NEP-24 Standard for NFT royalties. +var Nep24Royalty = &Standard{ + 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}, + }, + }, + }, + }, + }, + Required: []string{manifest.NEP24StandardName}, +} diff --git a/pkg/smartcontract/manifest/standard/payable.go b/pkg/smartcontract/manifest/standard/payable.go index d0e3de6fad..cdc68de6c3 100644 --- a/pkg/smartcontract/manifest/standard/payable.go +++ b/pkg/smartcontract/manifest/standard/payable.go @@ -21,6 +21,7 @@ var Nep11Payable = &Standard{ }}, }, }, + Required: []string{manifest.NEP11Payable}, } // Nep17Payable contains NEP-17's onNEP17Payment method definition. @@ -38,4 +39,5 @@ var Nep17Payable = &Standard{ }}, }, }, + Required: []string{manifest.NEP17Payable}, } diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go index de8fb86683..c172667709 100644 --- a/pkg/smartcontract/rpcbinding/binding.go +++ b/pkg/smartcontract/rpcbinding/binding.go @@ -179,6 +179,8 @@ type ContractReader struct { {{end -}} {{if .IsNep17}}nep17.TokenReader {{end -}} + {{if .IsNep24}}nep24.RoyaltyReader + {{end -}} invoker Invoker hash util.Uint160 } @@ -194,6 +196,8 @@ type Contract struct { {{end -}} {{if .IsNep17}}nep17.TokenWriter {{end -}} + {{if .IsNep24}}nep24.RoyaltyWriter + {{end -}} actor Actor hash util.Uint160 } @@ -208,6 +212,7 @@ func NewReader(invoker Invoker{{- if not (len .Hash) -}}, hash util.Uint160{{- e {{- if .IsNep11D}}*nep11.NewDivisibleReader(invoker, hash), {{end}} {{- if .IsNep11ND}}*nep11.NewNonDivisibleReader(invoker, hash), {{end}} {{- if .IsNep17}}*nep17.NewReader(invoker, hash), {{end -}} + {{- if .IsNep24}}*nep24.NewRoyaltyReader(invoker, hash), {{end -}} invoker, hash} } {{end -}} @@ -223,15 +228,19 @@ func New(actor Actor{{- if not (len .Hash) -}}, hash util.Uint160{{- end -}}) *C {{end -}} {{if .IsNep17}}var nep17t = nep17.New(actor, hash) {{end -}} + {{if .IsNep24}}var nep24t = nep24.NewRoyalty(actor, hash) + {{end -}} return &Contract{ {{- if .HasReader}}ContractReader{ {{- if .IsNep11D}}nep11dt.DivisibleReader, {{end -}} {{- if .IsNep11ND}}nep11ndt.NonDivisibleReader, {{end -}} {{- if .IsNep17}}nep17t.TokenReader, {{end -}} + {{- if .IsNep24}}nep24t.RoyaltyReader, {{end -}} actor, hash}, {{end -}} {{- if .IsNep11D}}nep11dt.DivisibleWriter, {{end -}} {{- if .IsNep11ND}}nep11ndt.BaseWriter, {{end -}} {{- if .IsNep17}}nep17t.TokenWriter, {{end -}} + {{- if .IsNep24}}nep24t.RoyaltyWriter, {{end -}} actor, hash} } {{end -}} @@ -352,6 +361,7 @@ type ( IsNep11D bool IsNep11ND bool IsNep17 bool + IsNep24 bool HasReader bool HasWriter bool @@ -413,6 +423,10 @@ func Generate(cfg binding.Config) error { mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11NonDivisible) ctr.IsNep11ND = true } + if standard.ComplyABI(cfg.Manifest, standard.Nep24Royalty) == nil { + mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep24Royalty) + ctr.IsNep24 = true + } mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep11Base) break // Can't be NEP-17 at the same time. } @@ -834,7 +848,7 @@ func scTemplateToRPC(cfg binding.Config, ctr ContractTmpl, imports map[string]st imports["github.com/nspcc-dev/neo-go/pkg/util"] = struct{}{} if len(ctr.SafeMethods) > 0 { imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"] = struct{}{} - if !(ctr.IsNep17 || ctr.IsNep11D || ctr.IsNep11ND) { + if !(ctr.IsNep17 || ctr.IsNep11D || ctr.IsNep11ND || ctr.IsNep24) { imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{} } } @@ -844,7 +858,7 @@ func scTemplateToRPC(cfg binding.Config, ctr ContractTmpl, imports map[string]st if len(ctr.Methods) > 0 || ctr.IsNep17 || ctr.IsNep11D || ctr.IsNep11ND { ctr.HasWriter = true } - if len(ctr.SafeMethods) > 0 || ctr.IsNep17 || ctr.IsNep11D || ctr.IsNep11ND { + if len(ctr.SafeMethods) > 0 || ctr.IsNep17 || ctr.IsNep11D || ctr.IsNep11ND || ctr.IsNep24 { ctr.HasReader = true } ctr.Imports = ctr.Imports[:0]