From b2bd8e4a0a7c016e6066b80f7f0891e86bbb334c Mon Sep 17 00:00:00 2001 From: Ekaterina Pavlova Date: Fri, 8 Nov 2024 10:21:28 +0300 Subject: [PATCH] manifest: support NEP-24 Close #3451 Signed-off-by: Ekaterina Pavlova --- cli/smartcontract/generate_test.go | 39 ++- .../rpcbindings/nft-d/rpcbindings.out | 146 +++++++++ .../nft-d/rpcbindings_dynamic_hash.out | 141 ++++++++ .../rpcbindings/nft-nd/rpcbindings.out | 249 +++++++++++++++ .../nft-nd/rpcbindings_dynamic_hash.out | 244 ++++++++++++++ .../testdata/rpcbindings/royalty/config.yml | 16 + .../testdata/rpcbindings/royalty/royalty.go | 13 + .../rpcbindings/royalty/rpcbindings.out | 57 ++++ .../royalty/rpcbindings_dynamic_hash.out | 53 ++++ examples/README.md | 4 +- examples/nft-d/nft.go | 40 +++ examples/nft-d/nft.yml | 4 +- examples/nft-nd/nft.go | 67 ++++ examples/nft-nd/nft.yml | 4 +- pkg/rpcclient/nep24/doc_test.go | 32 ++ pkg/rpcclient/nep24/royalty.go | 185 +++++++++++ pkg/rpcclient/nep24/royalty_test.go | 300 ++++++++++++++++++ pkg/smartcontract/manifest/manifest.go | 5 + pkg/smartcontract/manifest/standard/comply.go | 2 + pkg/smartcontract/manifest/standard/nep24.go | 51 +++ pkg/smartcontract/rpcbinding/binding.go | 99 +++++- 21 files changed, 1715 insertions(+), 36 deletions(-) create mode 100644 cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings.out create mode 100644 cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings_dynamic_hash.out create mode 100644 cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings.out create mode 100644 cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings_dynamic_hash.out create mode 100644 cli/smartcontract/testdata/rpcbindings/royalty/config.yml create mode 100644 cli/smartcontract/testdata/rpcbindings/royalty/royalty.go create mode 100644 cli/smartcontract/testdata/rpcbindings/royalty/rpcbindings.out create mode 100644 cli/smartcontract/testdata/rpcbindings/royalty/rpcbindings_dynamic_hash.out 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/cli/smartcontract/generate_test.go b/cli/smartcontract/generate_test.go index 19e1cdc1d..20209f070 100644 --- a/cli/smartcontract/generate_test.go +++ b/cli/smartcontract/generate_test.go @@ -476,7 +476,7 @@ func TestAssistedRPCBindings(t *testing.T) { tmpDir := t.TempDir() e := testcli.NewExecutor(t, false) - var checkBinding = func(source string, hasDefinedHash bool, guessEventTypes bool, suffix ...string) { + var checkBinding = func(source, configFile, expectedFile string, hasDefinedHash, guessEventTypes bool, suffix ...string) { testName := source if len(suffix) != 0 { testName += suffix[0] @@ -484,13 +484,20 @@ func TestAssistedRPCBindings(t *testing.T) { testName += fmt.Sprintf(", predefined hash: %t", hasDefinedHash) t.Run(testName, func(t *testing.T) { outFile := filepath.Join(tmpDir, "out.go") - configFile := filepath.Join(source, "config.yml") - expectedFile := filepath.Join(source, "rpcbindings.out") - if len(suffix) != 0 { - configFile = filepath.Join(source, "config"+suffix[0]+".yml") - expectedFile = filepath.Join(source, "rpcbindings"+suffix[0]+".out") - } else if !hasDefinedHash { - expectedFile = filepath.Join(source, "rpcbindings_dynamic_hash.out") + if configFile == "" { + if len(suffix) != 0 { + configFile = filepath.Join(source, "config"+suffix[0]+".yml") + } else { + configFile = filepath.Join(source, "config.yml") + } + } + if expectedFile == "" { + expectedFile = filepath.Join(source, "rpcbindings.out") + if len(suffix) != 0 { + expectedFile = filepath.Join(source, "rpcbindings"+suffix[0]+".out") + } else if !hasDefinedHash { + expectedFile = filepath.Join(source, "rpcbindings_dynamic_hash.out") + } } manifestF := filepath.Join(tmpDir, "manifest.json") bindingF := filepath.Join(tmpDir, "binding.yml") @@ -532,12 +539,18 @@ func TestAssistedRPCBindings(t *testing.T) { } for _, hasDefinedHash := range []bool{true, false} { - checkBinding(filepath.Join("testdata", "rpcbindings", "types"), hasDefinedHash, false) - checkBinding(filepath.Join("testdata", "rpcbindings", "structs"), hasDefinedHash, false) + checkBinding(filepath.Join("testdata", "rpcbindings", "types"), "", "", hasDefinedHash, false) + checkBinding(filepath.Join("testdata", "rpcbindings", "structs"), "", "", hasDefinedHash, false) + checkBinding(filepath.Join("testdata", "rpcbindings", "royalty"), "", "", hasDefinedHash, false) } - checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), true, false) - checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), true, false, "_extended") - checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), true, true, "_guessed") + checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), "", "", true, false) + checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), "", "", true, false, "_extended") + checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), "", "", true, true, "_guessed") + + checkBinding(filepath.Join("..", "..", "examples", "nft-d"), filepath.Join("..", "..", "examples", "nft-d", "nft.yml"), filepath.Join("testdata", "rpcbindings", "nft-d", "rpcbindings_dynamic_hash.out"), false, false) + checkBinding(filepath.Join("..", "..", "examples", "nft-d"), filepath.Join("..", "..", "examples", "nft-d", "nft.yml"), filepath.Join("testdata", "rpcbindings", "nft-d", "rpcbindings.out"), true, true) + checkBinding(filepath.Join("..", "..", "examples", "nft-nd"), filepath.Join("..", "..", "examples", "nft-nd", "nft.yml"), filepath.Join("testdata", "rpcbindings", "nft-nd", "rpcbindings_dynamic_hash.out"), false, false) + checkBinding(filepath.Join("..", "..", "examples", "nft-nd"), filepath.Join("..", "..", "examples", "nft-nd", "nft.yml"), filepath.Join("testdata", "rpcbindings", "nft-nd", "rpcbindings.out"), true, true) require.False(t, rewriteExpectedOutputs) } diff --git a/cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings.out b/cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings.out new file mode 100644 index 000000000..ae0262fcf --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings.out @@ -0,0 +1,146 @@ +// Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. + +// Package nft contains RPC wrappers for NeoFS Object NFT contract. +package nft + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep24" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// Hash contains contract hash. +var Hash = util.Uint160{0x33, 0x22, 0x11, 0x0, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x0} + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + nep11.Invoker +} + +// Actor is used by Contract to call state-changing methods. +type Actor interface { + Invoker + + nep11.Actor + + MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// ContractReader implements safe contract methods. +type ContractReader struct { + nep11.DivisibleReader + nep24.RoyaltyReader + invoker Invoker + hash util.Uint160 +} + +// Contract implements all contract methods. +type Contract struct { + ContractReader + nep11.DivisibleWriter + actor Actor + hash util.Uint160 +} + +// NewReader creates an instance of ContractReader using Hash and the given Invoker. +func NewReader(invoker Invoker) *ContractReader { + var hash = Hash + return &ContractReader{*nep11.NewDivisibleReader(invoker, hash), *nep24.NewRoyaltyReader(invoker, hash), invoker, hash} +} + +// New creates an instance of Contract using Hash and the given Actor. +func New(actor Actor) *Contract { + var hash = Hash + var nep11dt = nep11.NewDivisible(actor, hash) + var nep24t = nep24.NewRoyaltyReader(actor, hash) + return &Contract{ContractReader{nep11dt.DivisibleReader, *nep24t, actor, hash}, nep11dt.DivisibleWriter, actor, hash} +} + +// Destroy creates a transaction invoking `destroy` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Destroy() (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "destroy") +} + +// DestroyTransaction creates a transaction invoking `destroy` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) DestroyTransaction() (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "destroy") +} + +// DestroyUnsigned creates a transaction invoking `destroy` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) DestroyUnsigned() (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "destroy", nil) +} + +// Update creates a transaction invoking `update` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Update(nef []byte, manifest []byte) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "update", nef, manifest) +} + +// UpdateTransaction creates a transaction invoking `update` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) UpdateTransaction(nef []byte, manifest []byte) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "update", nef, manifest) +} + +// UpdateUnsigned creates a transaction invoking `update` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) UpdateUnsigned(nef []byte, manifest []byte) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "update", nil, nef, manifest) +} + +func (c *Contract) scriptForVerify() ([]byte, error) { + return smartcontract.CreateCallWithAssertScript(c.hash, "verify") +} + +// Verify creates a transaction invoking `verify` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Verify() (util.Uint256, uint32, error) { + script, err := c.scriptForVerify() + if err != nil { + return util.Uint256{}, 0, err + } + return c.actor.SendRun(script) +} + +// VerifyTransaction creates a transaction invoking `verify` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) VerifyTransaction() (*transaction.Transaction, error) { + script, err := c.scriptForVerify() + if err != nil { + return nil, err + } + return c.actor.MakeRun(script) +} + +// VerifyUnsigned creates a transaction invoking `verify` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) VerifyUnsigned() (*transaction.Transaction, error) { + script, err := c.scriptForVerify() + if err != nil { + return nil, err + } + return c.actor.MakeUnsignedRun(script, nil) +} diff --git a/cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings_dynamic_hash.out b/cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings_dynamic_hash.out new file mode 100644 index 000000000..ee706d5b9 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings_dynamic_hash.out @@ -0,0 +1,141 @@ +// Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. + +// Package nft contains RPC wrappers for NeoFS Object NFT contract. +package nft + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep24" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + nep11.Invoker +} + +// Actor is used by Contract to call state-changing methods. +type Actor interface { + Invoker + + nep11.Actor + + MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// ContractReader implements safe contract methods. +type ContractReader struct { + nep11.DivisibleReader + nep24.RoyaltyReader + invoker Invoker + hash util.Uint160 +} + +// Contract implements all contract methods. +type Contract struct { + ContractReader + nep11.DivisibleWriter + actor Actor + hash util.Uint160 +} + +// NewReader creates an instance of ContractReader using provided contract hash and the given Invoker. +func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { + return &ContractReader{*nep11.NewDivisibleReader(invoker, hash), *nep24.NewRoyaltyReader(invoker, hash), invoker, hash} +} + +// New creates an instance of Contract using provided contract hash and the given Actor. +func New(actor Actor, hash util.Uint160) *Contract { + var nep11dt = nep11.NewDivisible(actor, hash) + var nep24t = nep24.NewRoyaltyReader(actor, hash) + return &Contract{ContractReader{nep11dt.DivisibleReader, *nep24t, actor, hash}, nep11dt.DivisibleWriter, actor, hash} +} + +// Destroy creates a transaction invoking `destroy` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Destroy() (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "destroy") +} + +// DestroyTransaction creates a transaction invoking `destroy` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) DestroyTransaction() (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "destroy") +} + +// DestroyUnsigned creates a transaction invoking `destroy` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) DestroyUnsigned() (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "destroy", nil) +} + +// Update creates a transaction invoking `update` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Update(nef []byte, manifest []byte) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "update", nef, manifest) +} + +// UpdateTransaction creates a transaction invoking `update` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) UpdateTransaction(nef []byte, manifest []byte) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "update", nef, manifest) +} + +// UpdateUnsigned creates a transaction invoking `update` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) UpdateUnsigned(nef []byte, manifest []byte) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "update", nil, nef, manifest) +} + +func (c *Contract) scriptForVerify() ([]byte, error) { + return smartcontract.CreateCallWithAssertScript(c.hash, "verify") +} + +// Verify creates a transaction invoking `verify` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Verify() (util.Uint256, uint32, error) { + script, err := c.scriptForVerify() + if err != nil { + return util.Uint256{}, 0, err + } + return c.actor.SendRun(script) +} + +// VerifyTransaction creates a transaction invoking `verify` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) VerifyTransaction() (*transaction.Transaction, error) { + script, err := c.scriptForVerify() + if err != nil { + return nil, err + } + return c.actor.MakeRun(script) +} + +// VerifyUnsigned creates a transaction invoking `verify` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) VerifyUnsigned() (*transaction.Transaction, error) { + script, err := c.scriptForVerify() + if err != nil { + return nil, err + } + return c.actor.MakeUnsignedRun(script, nil) +} diff --git a/cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings.out b/cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings.out new file mode 100644 index 000000000..4f4623e8e --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings.out @@ -0,0 +1,249 @@ +// Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. + +// Package nft contains RPC wrappers for HASHY NFT contract. +package nft + +import ( + "errors" + "fmt" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep24" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "math/big" +) + +// Hash contains contract hash. +var Hash = util.Uint160{0x33, 0x22, 0x11, 0x0, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x0} + +// NftRoyaltyRecipientShare is a contract-specific nft.RoyaltyRecipientShare type used by its methods. +type NftRoyaltyRecipientShare struct { + Address util.Uint160 + Share *big.Int +} + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + nep11.Invoker +} + +// Actor is used by Contract to call state-changing methods. +type Actor interface { + Invoker + + nep11.Actor + + MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// ContractReader implements safe contract methods. +type ContractReader struct { + nep11.NonDivisibleReader + nep24.RoyaltyReader + invoker Invoker + hash util.Uint160 +} + +// Contract implements all contract methods. +type Contract struct { + ContractReader + nep11.BaseWriter + actor Actor + hash util.Uint160 +} + +// NewReader creates an instance of ContractReader using Hash and the given Invoker. +func NewReader(invoker Invoker) *ContractReader { + var hash = Hash + return &ContractReader{*nep11.NewNonDivisibleReader(invoker, hash), *nep24.NewRoyaltyReader(invoker, hash), invoker, hash} +} + +// New creates an instance of Contract using Hash and the given Actor. +func New(actor Actor) *Contract { + var hash = Hash + var nep11ndt = nep11.NewNonDivisible(actor, hash) + var nep24t = nep24.NewRoyaltyReader(actor, hash) + return &Contract{ContractReader{nep11ndt.NonDivisibleReader, *nep24t, actor, hash}, nep11ndt.BaseWriter, actor, hash} +} + +// Destroy creates a transaction invoking `destroy` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Destroy() (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "destroy") +} + +// DestroyTransaction creates a transaction invoking `destroy` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) DestroyTransaction() (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "destroy") +} + +// DestroyUnsigned creates a transaction invoking `destroy` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) DestroyUnsigned() (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "destroy", nil) +} + +func (c *Contract) scriptForSetRoyaltyInfo(ctx any, tokenID []byte, recipients []*NftRoyaltyRecipientShare) ([]byte, error) { + return smartcontract.CreateCallWithAssertScript(c.hash, "setRoyaltyInfo", ctx, tokenID, recipients) +} + +// SetRoyaltyInfo creates a transaction invoking `setRoyaltyInfo` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) SetRoyaltyInfo(ctx any, tokenID []byte, recipients []*NftRoyaltyRecipientShare) (util.Uint256, uint32, error) { + script, err := c.scriptForSetRoyaltyInfo(ctx, tokenID, recipients) + if err != nil { + return util.Uint256{}, 0, err + } + return c.actor.SendRun(script) +} + +// SetRoyaltyInfoTransaction creates a transaction invoking `setRoyaltyInfo` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) SetRoyaltyInfoTransaction(ctx any, tokenID []byte, recipients []*NftRoyaltyRecipientShare) (*transaction.Transaction, error) { + script, err := c.scriptForSetRoyaltyInfo(ctx, tokenID, recipients) + if err != nil { + return nil, err + } + return c.actor.MakeRun(script) +} + +// SetRoyaltyInfoUnsigned creates a transaction invoking `setRoyaltyInfo` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) SetRoyaltyInfoUnsigned(ctx any, tokenID []byte, recipients []*NftRoyaltyRecipientShare) (*transaction.Transaction, error) { + script, err := c.scriptForSetRoyaltyInfo(ctx, tokenID, recipients) + if err != nil { + return nil, err + } + return c.actor.MakeUnsignedRun(script, nil) +} + +// Update creates a transaction invoking `update` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Update(nef []byte, manifest []byte) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "update", nef, manifest) +} + +// UpdateTransaction creates a transaction invoking `update` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) UpdateTransaction(nef []byte, manifest []byte) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "update", nef, manifest) +} + +// UpdateUnsigned creates a transaction invoking `update` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) UpdateUnsigned(nef []byte, manifest []byte) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "update", nil, nef, manifest) +} + +func (c *Contract) scriptForVerify() ([]byte, error) { + return smartcontract.CreateCallWithAssertScript(c.hash, "verify") +} + +// Verify creates a transaction invoking `verify` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Verify() (util.Uint256, uint32, error) { + script, err := c.scriptForVerify() + if err != nil { + return util.Uint256{}, 0, err + } + return c.actor.SendRun(script) +} + +// VerifyTransaction creates a transaction invoking `verify` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) VerifyTransaction() (*transaction.Transaction, error) { + script, err := c.scriptForVerify() + if err != nil { + return nil, err + } + return c.actor.MakeRun(script) +} + +// VerifyUnsigned creates a transaction invoking `verify` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) VerifyUnsigned() (*transaction.Transaction, error) { + script, err := c.scriptForVerify() + if err != nil { + return nil, err + } + return c.actor.MakeUnsignedRun(script, nil) +} + +// itemToNftRoyaltyRecipientShare converts stack item into *NftRoyaltyRecipientShare. +// NULL item is returned as nil pointer without error. +func itemToNftRoyaltyRecipientShare(item stackitem.Item, err error) (*NftRoyaltyRecipientShare, error) { + if err != nil { + return nil, err + } + _, null := item.(stackitem.Null) + if null { + return nil, nil + } + var res = new(NftRoyaltyRecipientShare) + err = res.FromStackItem(item) + return res, err +} + +// FromStackItem retrieves fields of NftRoyaltyRecipientShare from the given +// [stackitem.Item] or returns an error if it's not possible to do to so. +func (res *NftRoyaltyRecipientShare) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 2 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + res.Address, err = func(item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + }(arr[index]) + if err != nil { + return fmt.Errorf("field Address: %w", err) + } + + index++ + res.Share, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Share: %w", err) + } + + return nil +} diff --git a/cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings_dynamic_hash.out b/cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings_dynamic_hash.out new file mode 100644 index 000000000..d582ac114 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings_dynamic_hash.out @@ -0,0 +1,244 @@ +// Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. + +// Package nft contains RPC wrappers for HASHY NFT contract. +package nft + +import ( + "errors" + "fmt" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep24" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "math/big" +) + +// NftRoyaltyRecipientShare is a contract-specific nft.RoyaltyRecipientShare type used by its methods. +type NftRoyaltyRecipientShare struct { + Address util.Uint160 + Share *big.Int +} + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + nep11.Invoker +} + +// Actor is used by Contract to call state-changing methods. +type Actor interface { + Invoker + + nep11.Actor + + MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// ContractReader implements safe contract methods. +type ContractReader struct { + nep11.NonDivisibleReader + nep24.RoyaltyReader + invoker Invoker + hash util.Uint160 +} + +// Contract implements all contract methods. +type Contract struct { + ContractReader + nep11.BaseWriter + actor Actor + hash util.Uint160 +} + +// NewReader creates an instance of ContractReader using provided contract hash and the given Invoker. +func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { + return &ContractReader{*nep11.NewNonDivisibleReader(invoker, hash), *nep24.NewRoyaltyReader(invoker, hash), invoker, hash} +} + +// New creates an instance of Contract using provided contract hash and the given Actor. +func New(actor Actor, hash util.Uint160) *Contract { + var nep11ndt = nep11.NewNonDivisible(actor, hash) + var nep24t = nep24.NewRoyaltyReader(actor, hash) + return &Contract{ContractReader{nep11ndt.NonDivisibleReader, *nep24t, actor, hash}, nep11ndt.BaseWriter, actor, hash} +} + +// Destroy creates a transaction invoking `destroy` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Destroy() (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "destroy") +} + +// DestroyTransaction creates a transaction invoking `destroy` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) DestroyTransaction() (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "destroy") +} + +// DestroyUnsigned creates a transaction invoking `destroy` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) DestroyUnsigned() (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "destroy", nil) +} + +func (c *Contract) scriptForSetRoyaltyInfo(ctx any, tokenID []byte, recipients []*NftRoyaltyRecipientShare) ([]byte, error) { + return smartcontract.CreateCallWithAssertScript(c.hash, "setRoyaltyInfo", ctx, tokenID, recipients) +} + +// SetRoyaltyInfo creates a transaction invoking `setRoyaltyInfo` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) SetRoyaltyInfo(ctx any, tokenID []byte, recipients []*NftRoyaltyRecipientShare) (util.Uint256, uint32, error) { + script, err := c.scriptForSetRoyaltyInfo(ctx, tokenID, recipients) + if err != nil { + return util.Uint256{}, 0, err + } + return c.actor.SendRun(script) +} + +// SetRoyaltyInfoTransaction creates a transaction invoking `setRoyaltyInfo` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) SetRoyaltyInfoTransaction(ctx any, tokenID []byte, recipients []*NftRoyaltyRecipientShare) (*transaction.Transaction, error) { + script, err := c.scriptForSetRoyaltyInfo(ctx, tokenID, recipients) + if err != nil { + return nil, err + } + return c.actor.MakeRun(script) +} + +// SetRoyaltyInfoUnsigned creates a transaction invoking `setRoyaltyInfo` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) SetRoyaltyInfoUnsigned(ctx any, tokenID []byte, recipients []*NftRoyaltyRecipientShare) (*transaction.Transaction, error) { + script, err := c.scriptForSetRoyaltyInfo(ctx, tokenID, recipients) + if err != nil { + return nil, err + } + return c.actor.MakeUnsignedRun(script, nil) +} + +// Update creates a transaction invoking `update` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Update(nef []byte, manifest []byte) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "update", nef, manifest) +} + +// UpdateTransaction creates a transaction invoking `update` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) UpdateTransaction(nef []byte, manifest []byte) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "update", nef, manifest) +} + +// UpdateUnsigned creates a transaction invoking `update` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) UpdateUnsigned(nef []byte, manifest []byte) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "update", nil, nef, manifest) +} + +func (c *Contract) scriptForVerify() ([]byte, error) { + return smartcontract.CreateCallWithAssertScript(c.hash, "verify") +} + +// Verify creates a transaction invoking `verify` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Verify() (util.Uint256, uint32, error) { + script, err := c.scriptForVerify() + if err != nil { + return util.Uint256{}, 0, err + } + return c.actor.SendRun(script) +} + +// VerifyTransaction creates a transaction invoking `verify` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) VerifyTransaction() (*transaction.Transaction, error) { + script, err := c.scriptForVerify() + if err != nil { + return nil, err + } + return c.actor.MakeRun(script) +} + +// VerifyUnsigned creates a transaction invoking `verify` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) VerifyUnsigned() (*transaction.Transaction, error) { + script, err := c.scriptForVerify() + if err != nil { + return nil, err + } + return c.actor.MakeUnsignedRun(script, nil) +} + +// itemToNftRoyaltyRecipientShare converts stack item into *NftRoyaltyRecipientShare. +// NULL item is returned as nil pointer without error. +func itemToNftRoyaltyRecipientShare(item stackitem.Item, err error) (*NftRoyaltyRecipientShare, error) { + if err != nil { + return nil, err + } + _, null := item.(stackitem.Null) + if null { + return nil, nil + } + var res = new(NftRoyaltyRecipientShare) + err = res.FromStackItem(item) + return res, err +} + +// FromStackItem retrieves fields of NftRoyaltyRecipientShare from the given +// [stackitem.Item] or returns an error if it's not possible to do to so. +func (res *NftRoyaltyRecipientShare) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 2 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + res.Address, err = func(item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + }(arr[index]) + if err != nil { + return fmt.Errorf("field Address: %w", err) + } + + index++ + res.Share, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Share: %w", err) + } + + return nil +} diff --git a/cli/smartcontract/testdata/rpcbindings/royalty/config.yml b/cli/smartcontract/testdata/rpcbindings/royalty/config.yml new file mode 100644 index 000000000..a1218455e --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/royalty/config.yml @@ -0,0 +1,16 @@ +name: Test royalty +sourceurl: https://github.com/nspcc-dev/neo-go/ +supportedstandards: ["NEP-24-Payable"] +events: + - name: RoyaltiesTransferred + parameters: + - name: royaltyToken + type: Hash160 + - name: royaltyRecipient + type: Hash160 + - name: buyer + type: Hash160 + - name: tokenId + type: ByteArray + - name: amount + type: Integer diff --git a/cli/smartcontract/testdata/rpcbindings/royalty/royalty.go b/cli/smartcontract/testdata/rpcbindings/royalty/royalty.go new file mode 100644 index 000000000..835a5273e --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/royalty/royalty.go @@ -0,0 +1,13 @@ +package royalty + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/native/std" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" +) + +// RoyaltiesTransferred notifies about royalty payment. This method is called by marketplace +// contract when royalties are transferred. +func RoyaltiesTransferred(royaltyToken, royaltyRecipient, buyer interop.Hash160, tokenId []byte, amount int) { + runtime.Notify("RoyaltiesTransferred", royaltyToken, royaltyRecipient, buyer, std.Deserialize(tokenId), amount) +} diff --git a/cli/smartcontract/testdata/rpcbindings/royalty/rpcbindings.out b/cli/smartcontract/testdata/rpcbindings/royalty/rpcbindings.out new file mode 100644 index 000000000..ad3fe68ee --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/royalty/rpcbindings.out @@ -0,0 +1,57 @@ +// Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. + +// Package royalty contains RPC wrappers for Test royalty contract. +package royalty + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/util" + "math/big" +) + +// Hash contains contract hash. +var Hash = util.Uint160{0x33, 0x22, 0x11, 0x0, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x0} + +// Actor is used by Contract to call state-changing methods. +type Actor interface { + MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// Contract implements all contract methods. +type Contract struct { + actor Actor + hash util.Uint160 +} + +// New creates an instance of Contract using Hash and the given Actor. +func New(actor Actor) *Contract { + var hash = Hash + return &Contract{actor, hash} +} + +// RoyaltiesTransferred creates a transaction invoking `royaltiesTransferred` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) RoyaltiesTransferred(royaltyToken util.Uint160, royaltyRecipient util.Uint160, buyer util.Uint160, tokenId []byte, amount *big.Int) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "royaltiesTransferred", royaltyToken, royaltyRecipient, buyer, tokenId, amount) +} + +// RoyaltiesTransferredTransaction creates a transaction invoking `royaltiesTransferred` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) RoyaltiesTransferredTransaction(royaltyToken util.Uint160, royaltyRecipient util.Uint160, buyer util.Uint160, tokenId []byte, amount *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "royaltiesTransferred", royaltyToken, royaltyRecipient, buyer, tokenId, amount) +} + +// RoyaltiesTransferredUnsigned creates a transaction invoking `royaltiesTransferred` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) RoyaltiesTransferredUnsigned(royaltyToken util.Uint160, royaltyRecipient util.Uint160, buyer util.Uint160, tokenId []byte, amount *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "royaltiesTransferred", nil, royaltyToken, royaltyRecipient, buyer, tokenId, amount) +} diff --git a/cli/smartcontract/testdata/rpcbindings/royalty/rpcbindings_dynamic_hash.out b/cli/smartcontract/testdata/rpcbindings/royalty/rpcbindings_dynamic_hash.out new file mode 100644 index 000000000..9ead45f6e --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/royalty/rpcbindings_dynamic_hash.out @@ -0,0 +1,53 @@ +// Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. + +// Package royalty contains RPC wrappers for Test royalty contract. +package royalty + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/util" + "math/big" +) + +// Actor is used by Contract to call state-changing methods. +type Actor interface { + MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// Contract implements all contract methods. +type Contract struct { + actor Actor + hash util.Uint160 +} + +// New creates an instance of Contract using provided contract hash and the given Actor. +func New(actor Actor, hash util.Uint160) *Contract { + return &Contract{actor, hash} +} + +// RoyaltiesTransferred creates a transaction invoking `royaltiesTransferred` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) RoyaltiesTransferred(royaltyToken util.Uint160, royaltyRecipient util.Uint160, buyer util.Uint160, tokenId []byte, amount *big.Int) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "royaltiesTransferred", royaltyToken, royaltyRecipient, buyer, tokenId, amount) +} + +// RoyaltiesTransferredTransaction creates a transaction invoking `royaltiesTransferred` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) RoyaltiesTransferredTransaction(royaltyToken util.Uint160, royaltyRecipient util.Uint160, buyer util.Uint160, tokenId []byte, amount *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "royaltiesTransferred", royaltyToken, royaltyRecipient, buyer, tokenId, amount) +} + +// RoyaltiesTransferredUnsigned creates a transaction invoking `royaltiesTransferred` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) RoyaltiesTransferredUnsigned(royaltyToken util.Uint160, royaltyRecipient util.Uint160, buyer util.Uint160, tokenId []byte, amount *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "royaltiesTransferred", nil, royaltyToken, royaltyRecipient, buyer, tokenId, amount) +} diff --git a/examples/README.md b/examples/README.md index 3881b32dc..73c1acfe8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -25,8 +25,8 @@ See the table below for the detailed examples description. | [engine](engine) | This contract demonstrates how to use `runtime` interop package which implements an API for `System.Runtime.*` Neo system calls. Please, refer to the `runtime` [package documentation](../pkg/interop/doc.go) for details. | | [events](events) | The contract shows how execution notifications with the different arguments types can be sent with the help of `runtime.Notify` function of the `runtime` interop package. Please, refer to the `runtime.Notify` [function documentation](../pkg/interop/runtime/runtime.go) for details. | | [iterator](iterator) | This example describes a way to work with Neo iterators. Please, refer to the `iterator` [package documentation](../pkg/interop/iterator/iterator.go) for details. | -| [nft-d](nft-d) | NEP-11 divisible NFT. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/blob/master/nep-11.mediawiki) for details. | -| [nft-nd](nft-nd) | NEP-11 non-divisible NFT. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/blob/master/nep-11.mediawiki) for details. | +| [nft-d](nft-d) | NEP-11 divisible NFT. This contract implements the NEP-11 and the NEP-24 token standards. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/blob/master/nep-11.mediawiki) and NEP-24 [specification](https://github.com/neo-project/proposals/blob/master/nep-24.mediawiki) for details. | +| [nft-nd](nft-nd) | NEP-11 non-divisible NFT. This contract implements the NEP-11 and the NEP-24 token standards. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/blob/master/nep-11.mediawiki) and NEP-24 [specification](https://github.com/neo-project/proposals/blob/master/nep-24.mediawiki) for details. | | [nft-nd-nns](nft-nd-nns) | Neo Name Service contract which is NEP-11 non-divisible NFT. The contract implements methods for Neo domain name system managing such as domains registration/transferring, records addition and names resolving. The package also contains tests implemented with [neotest](https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/neotest). | | [oracle](oracle) | Oracle demo contract exposing two methods that you can use to process URLs. It uses oracle native contract, see [interop package documentation](../pkg/interop/native/oracle/oracle.go) also. | | [runtime](runtime) | This contract demonstrates how to use special `_initialize` and `_deploy` methods. See the [compiler documentation](../docs/compiler.md#vm-api-interop-layer ) for methods details. It also shows the pattern for checking owner witness inside the contract with the help of `runtime.CheckWitness` interop [function](../pkg/interop/runtime/runtime.go). | diff --git a/examples/nft-d/nft.go b/examples/nft-d/nft.go index 0970c56da..06f6838a1 100644 --- a/examples/nft-d/nft.go +++ b/examples/nft-d/nft.go @@ -425,3 +425,43 @@ func Update(nef, manifest []byte) { } management.Update(nef, manifest) } + +// RoyaltyRecipient contains information about the recipient and the royalty amount. +type RoyaltyRecipient struct { + Address interop.Hash160 + Amount int +} + +// RoyaltyInfo returns a list of royalty recipients and the corresponding royalty amounts. +func RoyaltyInfo(tokenID []byte, royaltyToken interop.Hash160, salePrice int) []RoyaltyRecipient { + if salePrice <= 0 { + panic("sale price must be positive") + } + + executingHash := runtime.GetExecutingScriptHash() + if !royaltyToken.Equals(executingHash) { + panic("invalid royalty token") + } + + ctx := storage.GetReadOnlyContext() + if !isTokenValid(ctx, tokenID) { + panic("unknown token") + } + ownerIter := ownersOf(ctx, tokenID) + var owners []interop.Hash160 + for iterator.Next(ownerIter) { + owners = append(owners, iterator.Value(ownerIter).(interop.Hash160)) + } + + var ( + recipients []RoyaltyRecipient + amount = salePrice / 10 / len(owners) + ) + for _, owner := range owners { + recipients = append(recipients, RoyaltyRecipient{ + Address: owner, + Amount: amount, + }) + } + return recipients +} diff --git a/examples/nft-d/nft.yml b/examples/nft-d/nft.yml index 0253f6705..decd24a82 100644 --- a/examples/nft-d/nft.yml +++ b/examples/nft-d/nft.yml @@ -1,7 +1,7 @@ name: "NeoFS Object NFT" sourceurl: https://github.com/nspcc-dev/neo-go/ -supportedstandards: ["NEP-11"] -safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "properties", "tokens"] +supportedstandards: ["NEP-11", "NEP-24"] +safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "properties", "tokens", "royaltyInfo"] events: - name: Transfer parameters: diff --git a/examples/nft-nd/nft.go b/examples/nft-nd/nft.go index 78a8bc29e..2a6d84fe2 100644 --- a/examples/nft-nd/nft.go +++ b/examples/nft-nd/nft.go @@ -30,6 +30,8 @@ const ( accountPrefix = "a" // tokenPrefix contains map from token id to it's owner. tokenPrefix = "t" + // royaltyInfoPrefix contains map from token id to its royalty information. + royaltyInfoPrefix = "r" ) var ( @@ -285,3 +287,68 @@ 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 int +} + +// RoyaltyInfo returns a list of royalty recipients and the corresponding royalty amounts. +func RoyaltyInfo(tokenID []byte, royaltyToken interop.Hash160, salePrice int) []RoyaltyRecipient { + if salePrice <= 0 { + panic("sale price must be positive") + } + + executingHash := runtime.GetExecutingScriptHash() + if !royaltyToken.Equals(executingHash) { + panic("invalid royalty token") + } + + ctx := storage.GetReadOnlyContext() + owner := getOwnerOf(ctx, tokenID) + if owner == nil { + panic("invalid token ID") + } + + royaltyInfoKey := append([]byte(royaltyInfoPrefix), tokenID...) + val := storage.Get(ctx, royaltyInfoKey) + + var recipients []RoyaltyRecipient + if val == nil { + recipients = []RoyaltyRecipient{ + { + Address: owner, + Amount: salePrice / 10, + }, + } + } else { + bval := val.([]byte) + storedRecipients := std.Deserialize(bval) + st := storedRecipients.([]RoyaltyRecipientShare) + for _, r := range st { + recipients = append(recipients, RoyaltyRecipient{ + Address: r.Address, + Amount: salePrice * r.Share / 100, + }) + } + } + return recipients +} + +// RoyaltyRecipientShare contains information about the recipient and the royalty share in percents. +type RoyaltyRecipientShare struct { + Address interop.Hash160 + Share int +} + +// SetRoyaltyInfo sets the royalty share for specified recipients on a given token ID. +// Only the token owner can set the royalty information. +func SetRoyaltyInfo(ctx storage.Context, tokenID []byte, recipients []RoyaltyRecipientShare) bool { + if !runtime.CheckWitness(getOwnerOf(ctx, tokenID)) { + return false + } + putKey := append([]byte(royaltyInfoPrefix), tokenID...) + storage.Put(ctx, putKey, std.Serialize(recipients)) + return true +} diff --git a/examples/nft-nd/nft.yml b/examples/nft-nd/nft.yml index ff6357150..497d41514 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: diff --git a/pkg/rpcclient/nep24/doc_test.go b/pkg/rpcclient/nep24/doc_test.go new file mode 100644 index 000000000..4dfb0e2a7 --- /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.NewRoyaltyReader(inv, 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 000000000..4bcc72928 --- /dev/null +++ b/pkg/rpcclient/nep24/royalty.go @@ -0,0 +1,185 @@ +/* +Package nep24 provides RPC wrappers for NEP-24 contracts. + +All methods are safe (read-only) and encapsulated in the RoyaltyReader structure, +designed for managing NFT royalties and retrieving royalty information. +Refer to the nep11 package for basic NFT functionalities, while nep24 handles +royalty-related operations. +*/ +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 +} + +// NewRoyaltyReader returns a new RoyaltyReader instance. +func NewRoyaltyReader(invoker neptoken.Invoker, hash util.Uint160) *RoyaltyReader { + return &RoyaltyReader{ + invoker: invoker, + hash: hash, + } +} + +// 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 + } + if len(res.Stack) != 1 { + return nil, errors.New("invalid response: expected a single item on the stack") + } + rootItem, ok := res.Stack[0].Value().([]stackitem.Item) + if !ok { + return nil, errors.New("invalid response: expected an array of royalties") + } + + var royalties []RoyaltyRecipient + for _, item := range rootItem { + royalty, ok := item.Value().([]stackitem.Item) + if !ok { + 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) + } + if amountBigInt.Sign() < 0 { + return errors.New("negative royalty amount") + } + 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 000000000..b39564d93 --- /dev/null +++ b/pkg/rpcclient/nep24/royalty_test.go @@ -0,0 +1,300 @@ +package nep24 + +import ( + "errors" + "fmt" + "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 := NewRoyaltyReader(ta, util.Uint160{1, 2, 3}) + + tokenID := []byte{1, 2, 3} + royaltyToken := util.Uint160{4, 5, 6} + salePrice := big.NewInt(1000) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), + stackitem.Make(big.NewInt(100)), + }), + stackitem.Make([]stackitem.Item{ + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), + stackitem.Make(big.NewInt(200)), + }), + }), + }, + } + ri, err := rr.RoyaltyInfo(tokenID, royaltyToken, salePrice) + require.NoError(t, err) + require.Equal(t, []RoyaltyRecipient{ + { + Address: util.Uint160{7, 8, 9}, + Amount: big.NewInt(100), + }, + { + Address: util.Uint160{7, 8, 9}, + Amount: big.NewInt(200), + }, + }, ri) + + ta.err = errors.New("") + _, err = rr.RoyaltyInfo(tokenID, royaltyToken, salePrice) + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), + }), + }, + } + _, err = rr.RoyaltyInfo(tokenID, royaltyToken, salePrice) + require.Error(t, err) +} + +func TestRoyaltyRecipient_FromStackItem(t *testing.T) { + tests := map[string]struct { + items []stackitem.Item + err error + expected RoyaltyRecipient + }{ + "good": { + items: []stackitem.Item{ + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), + stackitem.Make(big.NewInt(100)), + }, + err: nil, + expected: RoyaltyRecipient{ + Address: util.Uint160{7, 8, 9}, + Amount: big.NewInt(100), + }, + }, + "invalid number of items": { + items: []stackitem.Item{ + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), + }, + err: fmt.Errorf("invalid royalty structure: expected 2 items, got 1"), + }, + "invalid recipient size": { + items: []stackitem.Item{ + stackitem.Make([]byte{1, 2}), + stackitem.Make(big.NewInt(100)), + }, + err: fmt.Errorf("invalid recipient address: expected byte size of 20 got 2"), + }, + "invalid recipient type": { + items: []stackitem.Item{ + stackitem.Make([]int{7, 8, 9}), + stackitem.Make(big.NewInt(100)), + }, + err: fmt.Errorf("failed to decode recipient address: invalid conversion: Array/ByteString"), + }, + "invalid amount type": { + items: []stackitem.Item{ + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), + stackitem.Make([]int{7, 8, 9}), + }, + err: fmt.Errorf("failed to decode royalty amount: invalid conversion: Array/Integer"), + }, + "negative amount": { + items: []stackitem.Item{ + stackitem.Make(util.Uint160{7, 8, 9}.BytesBE()), + stackitem.Make(big.NewInt(-100)), + }, + err: fmt.Errorf("negative royalty amount"), + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + var ri RoyaltyRecipient + err := ri.FromStackItem(tt.items) + if tt.err != nil { + require.EqualError(t, err, tt.err.Error()) + } 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: "good", + 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 f709c21ae..ae88ab9a7 100644 --- a/pkg/smartcontract/manifest/manifest.go +++ b/pkg/smartcontract/manifest/manifest.go @@ -26,6 +26,11 @@ 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" + // NEP24Payable represents the name of the contract interface for handling royalty payments in accordance + // with the NEP-24 standard. + NEP24Payable = "NEP-24-Payable" emptyFeatures = "{}" ) diff --git a/pkg/smartcontract/manifest/standard/comply.go b/pkg/smartcontract/manifest/standard/comply.go index ec29fff43..caddcc622 100644 --- a/pkg/smartcontract/manifest/standard/comply.go +++ b/pkg/smartcontract/manifest/standard/comply.go @@ -23,6 +23,8 @@ var checks = map[string][]*Standard{ manifest.NEP17StandardName: {Nep17}, manifest.NEP11Payable: {Nep11Payable}, manifest.NEP17Payable: {Nep17Payable}, + manifest.NEP24StandardName: {Nep24}, + manifest.NEP24Payable: {Nep24Payable}, } // Check checks if the manifest complies with all provided standards. diff --git a/pkg/smartcontract/manifest/standard/nep24.go b/pkg/smartcontract/manifest/standard/nep24.go new file mode 100644 index 000000000..3dd076b21 --- /dev/null +++ b/pkg/smartcontract/manifest/standard/nep24.go @@ -0,0 +1,51 @@ +package standard + +import ( + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" +) + +// MethodRoyaltyInfo is the name of the method that returns royalty information. +const MethodRoyaltyInfo = "royaltyInfo" + +// Nep24 is a NEP-24 Standard for NFT royalties. +var Nep24 = &Standard{ + Manifest: manifest.Manifest{ + ABI: manifest.ABI{ + Methods: []manifest.Method{ + { + Name: MethodRoyaltyInfo, + Parameters: []manifest.Parameter{ + {Name: "tokenId", Type: smartcontract.ByteArrayType}, + {Name: "royaltyToken", Type: smartcontract.Hash160Type}, + {Name: "salePrice", Type: smartcontract.IntegerType}, + }, + ReturnType: smartcontract.ArrayType, + Safe: true, + }, + }, + }, + }, + Required: []string{manifest.NEP11StandardName}, +} + +// Nep24Payable contains an event that MUST be triggered after marketplaces +// transferring royalties to the royalty recipient if royaltyInfo method is implemented. +var Nep24Payable = &Standard{ + Manifest: manifest.Manifest{ + ABI: manifest.ABI{ + 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}, + }, + }, + }, + }, + }, +} diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go index de8fb8668..72a3859d5 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 } @@ -208,6 +210,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,11 +226,14 @@ 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.NewRoyaltyReader(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, {{end -}} actor, hash}, {{end -}} {{- if .IsNep11D}}nep11dt.DivisibleWriter, {{end -}} {{- if .IsNep11ND}}nep11ndt.BaseWriter, {{end -}} @@ -349,9 +355,11 @@ type ( CustomEvents []CustomEventTemplate NamedTypes []binding.ExtendedType - IsNep11D bool - IsNep11ND bool - IsNep17 bool + IsNep11D bool + IsNep11ND bool + IsNep17 bool + IsNep24 bool + IsNep24Payable bool HasReader bool HasWriter bool @@ -404,27 +412,52 @@ func Generate(cfg binding.Config) error { // Strip standard methods from NEP-XX packages. for _, std := range cfg.Manifest.SupportedStandards { - if std == manifest.NEP11StandardName { + switch std { + case manifest.NEP11StandardName: imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"] = struct{}{} if standard.ComplyABI(cfg.Manifest, standard.Nep11Divisible) == nil { - mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11Divisible) ctr.IsNep11D = true } else if standard.ComplyABI(cfg.Manifest, standard.Nep11NonDivisible) == nil { - mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11NonDivisible) ctr.IsNep11ND = true } - mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep11Base) - break // Can't be NEP-17 at the same time. - } - if std == manifest.NEP17StandardName && standard.ComplyABI(cfg.Manifest, standard.Nep17) == nil { - mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep17) - imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"] = struct{}{} - ctr.IsNep17 = true - mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep17) - break // Can't be NEP-11 at the same time. + case manifest.NEP17StandardName: + if standard.ComplyABI(cfg.Manifest, standard.Nep17) == nil { + imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"] = struct{}{} + ctr.IsNep17 = true + } + case manifest.NEP24StandardName: + if standard.ComplyABI(cfg.Manifest, standard.Nep24) == nil { + imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep24"] = struct{}{} + ctr.IsNep24 = true + } + case manifest.NEP24Payable: + if standard.ComplyABI(cfg.Manifest, standard.Nep24Payable) == nil { + ctr.IsNep24Payable = true + } } } + if ctr.IsNep11D { + mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11Divisible) + } + if ctr.IsNep11ND { + mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11NonDivisible) + } + if ctr.IsNep11D || ctr.IsNep11ND { + mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep11Base) + } + if ctr.IsNep17 { + mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep17) + mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep17) + } + if ctr.IsNep24 { + mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep24) + cfg = dropNep24Types(cfg) + } + if ctr.IsNep24Payable { + mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep24Payable) + } + // OnNepXXPayment handlers normally can't be called directly. if standard.ComplyABI(cfg.Manifest, standard.Nep11Payable) == nil { mfst.ABI.Methods = dropStdMethods(mfst.ABI.Methods, standard.Nep11Payable) @@ -519,6 +552,38 @@ func dropStdEvents(events []manifest.Event, std *standard.Standard) []manifest.E return events } +// dropNep24Types removes NamedTypes of NEP-24 from the config if they are used only from the methods of the standard. +func dropNep24Types(cfg binding.Config) binding.Config { + var targetTypeName string + // Find structure returned by standard.MethodRoyaltyInfo method + // and remove it from binding.Config.NamedTypes as it will be imported from nep24 package. + if royaltyInfo, ok := cfg.Types[standard.MethodRoyaltyInfo]; ok && royaltyInfo.Value != nil { + returnType, exists := cfg.NamedTypes[royaltyInfo.Value.Name] + if !exists || returnType.Fields == nil || len(returnType.Fields) != 2 || + returnType.Fields[0].ExtendedType.Base != smartcontract.Hash160Type || + returnType.Fields[1].ExtendedType.Base != smartcontract.IntegerType { + return cfg + } + targetTypeName = royaltyInfo.Value.Name + } else { + return cfg + } + found := false + for _, typeDef := range cfg.Types { + if typeDef.Value != nil && typeDef.Value.Name == targetTypeName { + if found { + return cfg + } + found = true + } + } + + if found { + delete(cfg.NamedTypes, targetTypeName) + } + return cfg +} + func extendedTypeToGo(et binding.ExtendedType, named map[string]binding.ExtendedType) (string, string) { switch et.Base { case smartcontract.AnyType: @@ -834,7 +899,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 +909,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]