From 3a55edf69ea8590529b203bb1263b231869bd901 Mon Sep 17 00:00:00 2001 From: Ekaterina Pavlova Date: Tue, 5 Nov 2024 11:41:24 +0300 Subject: [PATCH 1/2] manifest: add `Required` field to `Standard` `Required` contains standards that are required for this standard. Signed-off-by: Ekaterina Pavlova --- pkg/smartcontract/manifest/standard/check.go | 2 ++ pkg/smartcontract/manifest/standard/comply.go | 5 +++++ 2 files changed, 7 insertions(+) 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..ec29fff43e 100644 --- a/pkg/smartcontract/manifest/standard/comply.go +++ b/pkg/smartcontract/manifest/standard/comply.go @@ -66,6 +66,11 @@ func ComplyABI(m *manifest.Manifest, st *Standard) error { } func comply(m *manifest.Manifest, checkNames bool, st *Standard) error { + if len(st.Required) > 0 { + if err := check(m, checkNames, st.Required...); err != nil { + return fmt.Errorf("required standard '%s' is not supported: %w", st.Name, err) + } + } if st.Base != nil { if err := comply(m, checkNames, st.Base); err != nil { return err From cbfda6a147331c09b359aa55423cbc8c3bb90d7c Mon Sep 17 00:00:00 2001 From: Ekaterina Pavlova Date: Tue, 5 Nov 2024 11:42:12 +0300 Subject: [PATCH 2/2] manifest: support NEP-24 Close #3451 Signed-off-by: Ekaterina Pavlova --- cli/smartcontract/generate_test.go | 38 ++- .../rpcbindings/nft-d/rpcbindings.out | 145 +++++++++ .../nft-d/rpcbindings_dynamic_hash.out | 140 ++++++++ .../rpcbindings/nft-nd/rpcbindings.out | 248 ++++++++++++++ .../nft-nd/rpcbindings_dynamic_hash.out | 243 ++++++++++++++ examples/nft-d/nft.go | 37 +++ examples/nft-d/nft.yml | 4 +- examples/nft-nd/nft.go | 62 ++++ examples/nft-nd/nft.yml | 6 +- go.mod | 2 +- pkg/rpcclient/nep24/doc_test.go | 32 ++ pkg/rpcclient/nep24/royalty.go | 177 ++++++++++ pkg/rpcclient/nep24/royalty_test.go | 303 ++++++++++++++++++ pkg/smartcontract/manifest/manifest.go | 4 + pkg/smartcontract/manifest/standard/comply.go | 2 + pkg/smartcontract/manifest/standard/nep24.go | 48 +++ pkg/smartcontract/rpcbinding/binding.go | 78 ++++- 17 files changed, 1536 insertions(+), 33 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 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 19e1cdc1d5..88e1963d08 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,17 @@ 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", "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 0000000000..9b06809c60 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings.out @@ -0,0 +1,145 @@ +// 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.RoyaltyReader{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) + return &Contract{ContractReader{nep11dt.DivisibleReader, nep24.RoyaltyReader{}, 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 0000000000..1ee2eed486 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/nft-d/rpcbindings_dynamic_hash.out @@ -0,0 +1,140 @@ +// 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.RoyaltyReader{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) + return &Contract{ContractReader{nep11dt.DivisibleReader, nep24.RoyaltyReader{}, 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 0000000000..8f25846448 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings.out @@ -0,0 +1,248 @@ +// 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} + +// NftRoyaltyRecipient is a contract-specific nft.RoyaltyRecipient type used by its methods. +type NftRoyaltyRecipient struct { + Address util.Uint160 + Amount *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.RoyaltyReader{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) + return &Contract{ContractReader{nep11ndt.NonDivisibleReader, nep24.RoyaltyReader{}, 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 []*NftRoyaltyRecipient) ([]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 []*NftRoyaltyRecipient) (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 []*NftRoyaltyRecipient) (*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 []*NftRoyaltyRecipient) (*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) +} + +// itemToNftRoyaltyRecipient converts stack item into *NftRoyaltyRecipient. +// NULL item is returned as nil pointer without error. +func itemToNftRoyaltyRecipient(item stackitem.Item, err error) (*NftRoyaltyRecipient, error) { + if err != nil { + return nil, err + } + _, null := item.(stackitem.Null) + if null { + return nil, nil + } + var res = new(NftRoyaltyRecipient) + err = res.FromStackItem(item) + return res, err +} + +// FromStackItem retrieves fields of NftRoyaltyRecipient from the given +// [stackitem.Item] or returns an error if it's not possible to do to so. +func (res *NftRoyaltyRecipient) 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.Amount, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Amount: %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 0000000000..1877d35f1f --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/nft-nd/rpcbindings_dynamic_hash.out @@ -0,0 +1,243 @@ +// 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" +) + +// NftRoyaltyRecipient is a contract-specific nft.RoyaltyRecipient type used by its methods. +type NftRoyaltyRecipient struct { + Address util.Uint160 + Amount *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.RoyaltyReader{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) + return &Contract{ContractReader{nep11ndt.NonDivisibleReader, nep24.RoyaltyReader{}, 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 []*NftRoyaltyRecipient) ([]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 []*NftRoyaltyRecipient) (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 []*NftRoyaltyRecipient) (*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 []*NftRoyaltyRecipient) (*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) +} + +// itemToNftRoyaltyRecipient converts stack item into *NftRoyaltyRecipient. +// NULL item is returned as nil pointer without error. +func itemToNftRoyaltyRecipient(item stackitem.Item, err error) (*NftRoyaltyRecipient, error) { + if err != nil { + return nil, err + } + _, null := item.(stackitem.Null) + if null { + return nil, nil + } + var res = new(NftRoyaltyRecipient) + err = res.FromStackItem(item) + return res, err +} + +// FromStackItem retrieves fields of NftRoyaltyRecipient from the given +// [stackitem.Item] or returns an error if it's not possible to do to so. +func (res *NftRoyaltyRecipient) 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.Amount, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Amount: %w", err) + } + + return nil +} diff --git a/examples/nft-d/nft.go b/examples/nft-d/nft.go index 0970c56da0..6f0621d1d7 100644 --- a/examples/nft-d/nft.go +++ b/examples/nft-d/nft.go @@ -34,6 +34,8 @@ const ( tokenOwnerPrefix = "t" // tokenPrefix contains map from token id to its properties (serialised containerID + objectID). tokenPrefix = "i" + // royaltyInfoPrefix contains map from token id to its royalty information. + royaltyInfoPrefix = "r" ) var ( @@ -425,3 +427,38 @@ 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") + } + + callingHash := runtime.GetExecutingScriptHash() + if !royaltyToken.Equals(callingHash) { + panic("invalid royalty token") + } + + ctx := storage.GetReadOnlyContext() + key := mkTokenKey(tokenID) + ownerIter := ownersOf(ctx, key) + var owners []interop.Hash160 + for iterator.Next(ownerIter) { + owners = append(owners, iterator.Value(ownerIter).(interop.Hash160)) + } + + var recipients []RoyaltyRecipient + for _, owner := range owners { + recipients = append(recipients, RoyaltyRecipient{ + Address: owner, + Amount: salePrice / 10, + }) + } + return recipients +} diff --git a/examples/nft-d/nft.yml b/examples/nft-d/nft.yml index 0253f6705b..835eb8c8c1 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 78a8bc29ec..e8e824ab91 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,63 @@ 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") + } + + callingHash := runtime.GetExecutingScriptHash() + if !royaltyToken.Equals(callingHash) { + 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) / 100, + }, + } + } else { + storedRecipients := val.([]any) + for _, r := range storedRecipients { + recipient := r.([]any) + recipients = append(recipients, RoyaltyRecipient{ + Address: recipient[0].(interop.Hash160), + Amount: recipient[1].(int), + }) + } + } + return recipients +} + +// SetRoyaltyInfo sets the royalty information for the given token ID. +func SetRoyaltyInfo(ctx storage.Context, tokenID []byte, recipients []RoyaltyRecipient) bool { + if !Verify() { + return false + } + var serialized []any + for _, recipient := range recipients { + serialized = append(serialized, []any{recipient.Address, recipient.Amount}) + } + storage.Put(ctx, append([]byte(royaltyInfoPrefix), tokenID...), serialized) + return true +} diff --git a/examples/nft-nd/nft.yml b/examples/nft-nd/nft.yml index ff63571505..4fc3e7aa64 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: @@ -15,5 +15,5 @@ events: type: ByteArray permissions: - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd - methods: ["update", "destroy"] + methods: ["update", "destroy","setRoyaltyInfo"] - methods: ["onNEP11Payment"] diff --git a/go.mod b/go.mod index 4407ba9eb6..6f04719c21 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( go.etcd.io/bbolt v1.3.11 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.26.0 + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 golang.org/x/term v0.23.0 golang.org/x/text v0.17.0 golang.org/x/tools v0.24.0 @@ -68,7 +69,6 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sync v0.8.0 // indirect 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..6211c93a48 --- /dev/null +++ b/pkg/rpcclient/nep24/royalty.go @@ -0,0 +1,177 @@ +/* +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 +} + +// 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 0000000000..f8aea9dc94 --- /dev/null +++ b/pkg/rpcclient/nep24/royalty_test.go @@ -0,0 +1,303 @@ +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 := &RoyaltyReader{ + Invoker: ta, + Hash: 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 f709c21ae0..e42274f63b 100644 --- a/pkg/smartcontract/manifest/manifest.go +++ b/pkg/smartcontract/manifest/manifest.go @@ -26,6 +26,10 @@ 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 which should be triggered after NEP-24 tokens. + NEP24Payable = "NEP-24-Payable" emptyFeatures = "{}" ) diff --git a/pkg/smartcontract/manifest/standard/comply.go b/pkg/smartcontract/manifest/standard/comply.go index ec29fff43e..caddcc6223 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 0000000000..357cb31205 --- /dev/null +++ b/pkg/smartcontract/manifest/standard/nep24.go @@ -0,0 +1,48 @@ +package standard + +import ( + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" +) + +// Nep24 is a NEP-24 Standard for NFT royalties. +var Nep24 = &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, + }, + }, + }, + }, + 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 de8fb86683..986508ca36 100644 --- a/pkg/smartcontract/rpcbinding/binding.go +++ b/pkg/smartcontract/rpcbinding/binding.go @@ -13,6 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard" "github.com/nspcc-dev/neo-go/pkg/util" + "golang.org/x/exp/maps" ) // The set of constants containing parts of RPC binding template. Each block of code @@ -179,6 +180,8 @@ type ContractReader struct { {{end -}} {{if .IsNep17}}nep17.TokenReader {{end -}} + {{if .IsNep24}}nep24.RoyaltyReader + {{end -}} invoker Invoker hash util.Uint160 } @@ -208,6 +211,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.RoyaltyReader{invoker, hash}, {{end -}} invoker, hash} } {{end -}} @@ -228,6 +232,7 @@ func New(actor Actor{{- if not (len .Hash) -}}, hash util.Uint160{{- end -}}) *C {{- if .IsNep11D}}nep11dt.DivisibleReader, {{end -}} {{- if .IsNep11ND}}nep11ndt.NonDivisibleReader, {{end -}} {{- if .IsNep17}}nep17t.TokenReader, {{end -}} + {{- if .IsNep24}}nep24.RoyaltyReader{}, {{end -}} actor, hash}, {{end -}} {{- if .IsNep11D}}nep11dt.DivisibleWriter, {{end -}} {{- if .IsNep11ND}}nep11ndt.BaseWriter, {{end -}} @@ -352,6 +357,7 @@ type ( IsNep11D bool IsNep11ND bool IsNep17 bool + IsNep24 bool HasReader bool HasWriter bool @@ -404,27 +410,47 @@ 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, manifest.NEP24Payable: + if standard.ComplyABI(cfg.Manifest, standard.Nep24) == nil { + imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep24"] = struct{}{} + ctr.IsNep24 = 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) + mfst.ABI.Events = dropStdEvents(mfst.ABI.Events, standard.Nep24Payable) + cfg = dropTypes(cfg, standard.Nep24) + ctr.IsNep24 = true + } + // 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 +545,30 @@ func dropStdEvents(events []manifest.Event, std *standard.Standard) []manifest.E return events } +// dropTypes removes NamedTypes of NEP-24 from the config if they are used only once. +func dropTypes(cfg binding.Config, std *standard.Standard) binding.Config { + var targetTypeName string + if royaltyInfo, ok := cfg.Types[std.ABI.Methods[0].Name]; ok && royaltyInfo.Value != nil { + targetTypeName = royaltyInfo.Value.Name + } else { + return cfg + } + count := 0 + for _, typeDef := range cfg.Types { + if typeDef.Value != nil && typeDef.Value.Name == targetTypeName { + count++ + } + } + if count == 1 { + filteredNamedTypes := maps.Clone(cfg.NamedTypes) + if _, exists := cfg.NamedTypes[targetTypeName]; exists { + delete(filteredNamedTypes, targetTypeName) + } + cfg.NamedTypes = filteredNamedTypes + } + return cfg +} + func extendedTypeToGo(et binding.ExtendedType, named map[string]binding.ExtendedType) (string, string) { switch et.Base { case smartcontract.AnyType: @@ -834,7 +884,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 +894,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]