From 617c31093fb6d12394d58e6bc8530f4c341157eb Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 26 Oct 2022 23:27:24 +0300 Subject: [PATCH] smartcontract: initial rpcbinding implementation, fix #2705 It can do some unwrapping and reuse nepXX packages. It only uses manifest data at the moment, see #2767, #2768, #2769. --- cli/smartcontract/generate.go | 59 ++- cli/smartcontract/generate_test.go | 82 +++- cli/smartcontract/smart_contract.go | 1 + cli/smartcontract/testdata/nameservice/nns.go | 62 +++ .../testdata/nameservice/nns.manifest.json | 441 ++++++++++++++++++ cli/smartcontract/testdata/nex/nex.go | 52 +++ .../testdata/nex/nex.manifest.json | 275 +++++++++++ pkg/smartcontract/rpcbinding/binding.go | 226 +++++++++ 8 files changed, 1176 insertions(+), 22 deletions(-) create mode 100644 cli/smartcontract/testdata/nameservice/nns.go create mode 100644 cli/smartcontract/testdata/nameservice/nns.manifest.json create mode 100644 cli/smartcontract/testdata/nex/nex.go create mode 100644 cli/smartcontract/testdata/nex/nex.manifest.json create mode 100644 pkg/smartcontract/rpcbinding/binding.go diff --git a/cli/smartcontract/generate.go b/cli/smartcontract/generate.go index 44d6f6f02..6965d527c 100644 --- a/cli/smartcontract/generate.go +++ b/cli/smartcontract/generate.go @@ -7,39 +7,58 @@ import ( "github.com/nspcc-dev/neo-go/cli/cmdargs" "github.com/nspcc-dev/neo-go/pkg/smartcontract/binding" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/rpcbinding" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/urfave/cli" "gopkg.in/yaml.v3" ) +var generatorFlags = []cli.Flag{ + cli.StringFlag{ + Name: "config, c", + Usage: "Configuration file to use", + }, + cli.StringFlag{ + Name: "manifest, m", + Usage: "Read contract manifest (*.manifest.json) file", + }, + cli.StringFlag{ + Name: "out, o", + Usage: "Output of the compiled contract", + }, + cli.StringFlag{ + Name: "hash", + Usage: "Smart-contract hash", + }, +} + var generateWrapperCmd = cli.Command{ Name: "generate-wrapper", Usage: "generate wrapper to use in other contracts", UsageText: "neo-go contract generate-wrapper --manifest --out --hash ", Description: ``, Action: contractGenerateWrapper, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config, c", - Usage: "Configuration file to use", - }, - cli.StringFlag{ - Name: "manifest, m", - Usage: "Read contract manifest (*.manifest.json) file", - }, - cli.StringFlag{ - Name: "out, o", - Usage: "Output of the compiled contract", - }, - cli.StringFlag{ - Name: "hash", - Usage: "Smart-contract hash", - }, - }, + Flags: generatorFlags, +} + +var generateRPCWrapperCmd = cli.Command{ + Name: "generate-rpcwrapper", + Usage: "generate RPC wrapper to use for data reads", + UsageText: "neo-go contract generate-rpcwrapper --manifest --out --hash ", + Action: contractGenerateRPCWrapper, + Flags: generatorFlags, } -// contractGenerateWrapper deploys contract. func contractGenerateWrapper(ctx *cli.Context) error { + return contractGenerateSomething(ctx, binding.Generate) +} + +func contractGenerateRPCWrapper(ctx *cli.Context) error { + return contractGenerateSomething(ctx, rpcbinding.Generate) +} + +// contractGenerateSomething reads generator parameters and calls the given callback. +func contractGenerateSomething(ctx *cli.Context, cb func(binding.Config) error) error { if err := cmdargs.EnsureNone(ctx); err != nil { return err } @@ -74,7 +93,7 @@ func contractGenerateWrapper(ctx *cli.Context) error { cfg.Output = f - err = binding.Generate(cfg) + err = cb(cfg) if err != nil { return cli.NewExitError(fmt.Errorf("error during generation: %w", err), 1) } diff --git a/cli/smartcontract/generate_test.go b/cli/smartcontract/generate_test.go index e7f57371e..9eada4790 100644 --- a/cli/smartcontract/generate_test.go +++ b/cli/smartcontract/generate_test.go @@ -1,6 +1,7 @@ package smartcontract import ( + "bytes" "encoding/json" "os" "path/filepath" @@ -224,6 +225,7 @@ func TestGenerateValidPackageName(t *testing.T) { Name: "get", Parameters: []manifest.Parameter{}, ReturnType: smartcontract.IntegerType, + Safe: true, }, ) @@ -239,7 +241,7 @@ func TestGenerateValidPackageName(t *testing.T) { 0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xBE, 0xEF, 0x03, 0x04, } app := cli.NewApp() - app.Commands = []cli.Command{generateWrapperCmd} + app.Commands = []cli.Command{generateWrapperCmd, generateRPCWrapperCmd} require.NoError(t, app.Run([]string{"", "generate-wrapper", "--manifest", manifestFile, "--out", outFile, @@ -261,9 +263,85 @@ const Hash = "\x04\x08\x15\x16\x23\x42\x43\x44\x00\x01\xca\xfe\xba\xbe\xde\xad\x // Get invokes `+"`get`"+` method of contract. func Get() int { - return neogointernal.CallWithToken(Hash, "get", int(contract.All)).(int) + return neogointernal.CallWithToken(Hash, "get", int(contract.ReadOnly)).(int) } `, string(data)) + require.NoError(t, app.Run([]string{"", "generate-rpcwrapper", + "--manifest", manifestFile, + "--out", outFile, + "--hash", "0x" + h.StringLE(), + })) + + data, err = os.ReadFile(outFile) + require.NoError(t, err) + require.Equal(t, `// Package myspacecontract contains RPC wrappers for My space contract contract. +package myspacecontract + +import ( + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "math/big" +) + +// Hash contains contract hash. +var Hash = util.Uint160{0x4, 0x8, 0x15, 0x16, 0x23, 0x42, 0x43, 0x44, 0x0, 0x1, 0xca, 0xfe, 0xba, 0xbe, 0xde, 0xad, 0xbe, 0xef, 0x3, 0x4} + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) +} + +// ContractReader implements safe contract methods. +type ContractReader struct { + invoker Invoker +} + +// NewReader creates an instance of ContractReader using Hash and the given Invoker. +func NewReader(invoker Invoker) *ContractReader { + return &ContractReader{invoker} +} + + +// Get invokes `+"`get`"+` method of contract. +func (c *ContractReader) Get() (*big.Int, error) { + return unwrap.BigInt(c.invoker.Call(Hash, "get")) +} +`, string(data)) +} + +func TestGenerateRPCBindings(t *testing.T) { + tmpDir := t.TempDir() + app := cli.NewApp() + app.Commands = []cli.Command{generateWrapperCmd, generateRPCWrapperCmd} + + outFile := filepath.Join(tmpDir, "out.go") + require.NoError(t, app.Run([]string{"", "generate-rpcwrapper", + "--manifest", filepath.Join("testdata", "nex", "nex.manifest.json"), + "--out", outFile, + "--hash", "0xa2a67f09e8cf22c6bfd5cea24adc0f4bf0a11aa8", + })) + + data, err := os.ReadFile(outFile) + require.NoError(t, err) + data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows. + expected, err := os.ReadFile(filepath.Join("testdata", "nex", "nex.go")) + require.NoError(t, err) + expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows. + require.Equal(t, string(expected), string(data)) + + require.NoError(t, app.Run([]string{"", "generate-rpcwrapper", + "--manifest", filepath.Join("testdata", "nameservice", "nns.manifest.json"), + "--out", outFile, + "--hash", "0x50ac1c37690cc2cfc594472833cf57505d5f46de", + })) + + data, err = os.ReadFile(outFile) + require.NoError(t, err) + data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows. + expected, err = os.ReadFile(filepath.Join("testdata", "nameservice", "nns.go")) + require.NoError(t, err) + expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows. + require.Equal(t, string(expected), string(data)) } func TestGenerate_Errors(t *testing.T) { diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 8a468ed1a..61d026bc9 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -182,6 +182,7 @@ func NewCommands() []cli.Command { Flags: deployFlags, }, generateWrapperCmd, + generateRPCWrapperCmd, { Name: "invokefunction", Usage: "invoke deployed contract on the blockchain", diff --git a/cli/smartcontract/testdata/nameservice/nns.go b/cli/smartcontract/testdata/nameservice/nns.go new file mode 100644 index 000000000..2d9259616 --- /dev/null +++ b/cli/smartcontract/testdata/nameservice/nns.go @@ -0,0 +1,62 @@ +// Package nameservice contains RPC wrappers for NameService contract. +package nameservice + +import ( + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "math/big" +) + +// Hash contains contract hash. +var Hash = util.Uint160{0xde, 0x46, 0x5f, 0x5d, 0x50, 0x57, 0xcf, 0x33, 0x28, 0x47, 0x94, 0xc5, 0xcf, 0xc2, 0xc, 0x69, 0x37, 0x1c, 0xac, 0x50} + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + nep11.Invoker + Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) +} + +// ContractReader implements safe contract methods. +type ContractReader struct { + nep11.NonDivisibleReader + invoker Invoker +} + +// NewReader creates an instance of ContractReader using Hash and the given Invoker. +func NewReader(invoker Invoker) *ContractReader { + return &ContractReader{*nep11.NewNonDivisibleReader(invoker, Hash), invoker} +} + + +// Roots invokes `roots` method of contract. +func (c *ContractReader) Roots() (stackitem.Item, error) { + return unwrap.Item(c.invoker.Call(Hash, "roots")) +} + +// GetPrice invokes `getPrice` method of contract. +func (c *ContractReader) GetPrice(length *big.Int) (*big.Int, error) { + return unwrap.BigInt(c.invoker.Call(Hash, "getPrice", length)) +} + +// IsAvailable invokes `isAvailable` method of contract. +func (c *ContractReader) IsAvailable(name string) (bool, error) { + return unwrap.Bool(c.invoker.Call(Hash, "isAvailable", name)) +} + +// GetRecord invokes `getRecord` method of contract. +func (c *ContractReader) GetRecord(name string, type *big.Int) (string, error) { + return unwrap.UTF8String(c.invoker.Call(Hash, "getRecord", name, type)) +} + +// GetAllRecords invokes `getAllRecords` method of contract. +func (c *ContractReader) GetAllRecords(name string) (stackitem.Item, error) { + return unwrap.Item(c.invoker.Call(Hash, "getAllRecords", name)) +} + +// Resolve invokes `resolve` method of contract. +func (c *ContractReader) Resolve(name string, type *big.Int) (string, error) { + return unwrap.UTF8String(c.invoker.Call(Hash, "resolve", name, type)) +} diff --git a/cli/smartcontract/testdata/nameservice/nns.manifest.json b/cli/smartcontract/testdata/nameservice/nns.manifest.json new file mode 100644 index 000000000..555ab898a --- /dev/null +++ b/cli/smartcontract/testdata/nameservice/nns.manifest.json @@ -0,0 +1,441 @@ + { + "abi" : { + "events" : [ + { + "parameters" : [ + { + "name" : "from", + "type" : "Hash160" + }, + { + "name" : "to", + "type" : "Hash160" + }, + { + "name" : "amount", + "type" : "Integer" + }, + { + "type" : "ByteArray", + "name" : "tokenId" + } + ], + "name" : "Transfer" + }, + { + "parameters" : [ + { + "type" : "String", + "name" : "name" + }, + { + "type" : "Hash160", + "name" : "oldAdmin" + }, + { + "type" : "Hash160", + "name" : "newAdmin" + } + ], + "name" : "SetAdmin" + }, + { + "name" : "Renew", + "parameters" : [ + { + "name" : "name", + "type" : "String" + }, + { + "type" : "Integer", + "name" : "oldExpiration" + }, + { + "name" : "newExpiration", + "type" : "Integer" + } + ] + } + ], + "methods" : [ + { + "safe" : true, + "parameters" : [], + "name" : "symbol", + "returntype" : "String", + "offset" : 0 + }, + { + "parameters" : [], + "name" : "decimals", + "returntype" : "Integer", + "safe" : true, + "offset" : 6 + }, + { + "offset" : 8, + "returntype" : "Integer", + "name" : "totalSupply", + "parameters" : [], + "safe" : true + }, + { + "offset" : 53, + "safe" : true, + "parameters" : [ + { + "type" : "ByteArray", + "name" : "tokenId" + } + ], + "name" : "ownerOf", + "returntype" : "Hash160" + }, + { + "safe" : true, + "name" : "properties", + "returntype" : "Map", + "parameters" : [ + { + "type" : "ByteArray", + "name" : "tokenId" + } + ], + "offset" : 184 + }, + { + "safe" : true, + "returntype" : "Integer", + "name" : "balanceOf", + "parameters" : [ + { + "name" : "owner", + "type" : "Hash160" + } + ], + "offset" : 341 + }, + { + "safe" : true, + "returntype" : "InteropInterface", + "name" : "tokens", + "parameters" : [], + "offset" : 453 + }, + { + "safe" : true, + "name" : "tokensOf", + "returntype" : "InteropInterface", + "parameters" : [ + { + "name" : "owner", + "type" : "Hash160" + } + ], + "offset" : 494 + }, + { + "offset" : 600, + "safe" : false, + "parameters" : [ + { + "type" : "Hash160", + "name" : "to" + }, + { + "name" : "tokenId", + "type" : "ByteArray" + }, + { + "name" : "data", + "type" : "Any" + } + ], + "name" : "transfer", + "returntype" : "Boolean" + }, + { + "name" : "update", + "returntype" : "Void", + "parameters" : [ + { + "type" : "ByteArray", + "name" : "nef" + }, + { + "name" : "manifest", + "type" : "String" + } + ], + "safe" : false, + "offset" : 1121 + }, + { + "offset" : 1291, + "returntype" : "Void", + "name" : "addRoot", + "parameters" : [ + { + "name" : "root", + "type" : "String" + } + ], + "safe" : false + }, + { + "offset" : 1725, + "safe" : true, + "parameters" : [], + "returntype" : "InteropInterface", + "name" : "roots" + }, + { + "offset" : 1757, + "parameters" : [ + { + "type" : "Array", + "name" : "priceList" + } + ], + "name" : "setPrice", + "returntype" : "Void", + "safe" : false + }, + { + "offset" : 1952, + "safe" : true, + "parameters" : [ + { + "name" : "length", + "type" : "Integer" + } + ], + "name" : "getPrice", + "returntype" : "Integer" + }, + { + "offset" : 2017, + "safe" : true, + "parameters" : [ + { + "type" : "String", + "name" : "name" + } + ], + "name" : "isAvailable", + "returntype" : "Boolean" + }, + { + "offset" : 2405, + "parameters" : [ + { + "type" : "String", + "name" : "name" + }, + { + "type" : "Hash160", + "name" : "owner" + } + ], + "name" : "register", + "returntype" : "Boolean", + "safe" : false + }, + { + "name" : "renew", + "returntype" : "Integer", + "parameters" : [ + { + "type" : "String", + "name" : "name" + } + ], + "safe" : false, + "offset" : 3113 + }, + { + "offset" : 3123, + "safe" : false, + "parameters" : [ + { + "name" : "name", + "type" : "String" + }, + { + "name" : "years", + "type" : "Integer" + } + ], + "name" : "renew", + "returntype" : "Integer" + }, + { + "offset" : 3697, + "parameters" : [ + { + "type" : "String", + "name" : "name" + }, + { + "name" : "admin", + "type" : "Hash160" + } + ], + "name" : "setAdmin", + "returntype" : "Void", + "safe" : false + }, + { + "safe" : false, + "returntype" : "Void", + "name" : "setRecord", + "parameters" : [ + { + "name" : "name", + "type" : "String" + }, + { + "type" : "Integer", + "name" : "type" + }, + { + "name" : "data", + "type" : "String" + } + ], + "offset" : 3921 + }, + { + "name" : "getRecord", + "returntype" : "String", + "parameters" : [ + { + "name" : "name", + "type" : "String" + }, + { + "type" : "Integer", + "name" : "type" + } + ], + "safe" : true, + "offset" : 5877 + }, + { + "returntype" : "InteropInterface", + "name" : "getAllRecords", + "parameters" : [ + { + "name" : "name", + "type" : "String" + } + ], + "safe" : true, + "offset" : 6201 + }, + { + "safe" : false, + "returntype" : "Void", + "name" : "deleteRecord", + "parameters" : [ + { + "name" : "name", + "type" : "String" + }, + { + "type" : "Integer", + "name" : "type" + } + ], + "offset" : 6281 + }, + { + "offset" : 6565, + "name" : "resolve", + "returntype" : "String", + "parameters" : [ + { + "name" : "name", + "type" : "String" + }, + { + "type" : "Integer", + "name" : "type" + } + ], + "safe" : true + }, + { + "safe" : false, + "parameters" : [ + { + "name" : "data", + "type" : "Any" + }, + { + "name" : "update", + "type" : "Boolean" + } + ], + "name" : "_deploy", + "returntype" : "Void", + "offset" : 7152 + }, + { + "offset" : 7514, + "parameters" : [], + "returntype" : "Void", + "name" : "_initialize", + "safe" : false + } + ] + }, + "supportedstandards" : [ + "NEP-11" + ], + "permissions" : [ + { + "contract" : "0x726cb6e0cd8628a1350a611384688911ab75f51b", + "methods" : [ + "ripemd160" + ] + }, + { + "contract" : "0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0", + "methods" : [ + "atoi", + "deserialize", + "serialize", + "stringSplit" + ] + }, + { + "contract" : "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", + "methods" : [ + "getCommittee" + ] + }, + { + "contract" : "0xfffdc93764dbaddd97c48f252a53ea4643faa3fd", + "methods" : [ + "getContract", + "update" + ] + }, + { + "methods" : [ + "onNEP11Payment" + ], + "contract" : "*" + } + ], + "features" : {}, + "name" : "NameService", + "trusts" : [], + "extra" : { + "Author" : "The Neo Project", + "Description" : "Neo Name Service", + "Email" : "dev@neo.org" + }, + "groups" : [] + } diff --git a/cli/smartcontract/testdata/nex/nex.go b/cli/smartcontract/testdata/nex/nex.go new file mode 100644 index 000000000..90a9b5645 --- /dev/null +++ b/cli/smartcontract/testdata/nex/nex.go @@ -0,0 +1,52 @@ +// Package nextoken contains RPC wrappers for NEX Token contract. +package nextoken + +import ( + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/util" + "math/big" +) + +// Hash contains contract hash. +var Hash = util.Uint160{0xa8, 0x1a, 0xa1, 0xf0, 0x4b, 0xf, 0xdc, 0x4a, 0xa2, 0xce, 0xd5, 0xbf, 0xc6, 0x22, 0xcf, 0xe8, 0x9, 0x7f, 0xa6, 0xa2} + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + nep17.Invoker + Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) +} + +// ContractReader implements safe contract methods. +type ContractReader struct { + nep17.TokenReader + invoker Invoker +} + +// NewReader creates an instance of ContractReader using Hash and the given Invoker. +func NewReader(invoker Invoker) *ContractReader { + return &ContractReader{*nep17.NewReader(invoker, Hash), invoker} +} + + +// Cap invokes `cap` method of contract. +func (c *ContractReader) Cap() (*big.Int, error) { + return unwrap.BigInt(c.invoker.Call(Hash, "cap")) +} + +// GetMinter invokes `getMinter` method of contract. +func (c *ContractReader) GetMinter() (*keys.PublicKey, error) { + return unwrap.PublicKey(c.invoker.Call(Hash, "getMinter")) +} + +// GetOwner invokes `getOwner` method of contract. +func (c *ContractReader) GetOwner() (util.Uint160, error) { + return unwrap.Uint160(c.invoker.Call(Hash, "getOwner")) +} + +// TotalMinted invokes `totalMinted` method of contract. +func (c *ContractReader) TotalMinted() (*big.Int, error) { + return unwrap.BigInt(c.invoker.Call(Hash, "totalMinted")) +} diff --git a/cli/smartcontract/testdata/nex/nex.manifest.json b/cli/smartcontract/testdata/nex/nex.manifest.json new file mode 100644 index 000000000..83201b356 --- /dev/null +++ b/cli/smartcontract/testdata/nex/nex.manifest.json @@ -0,0 +1,275 @@ + { + "name" : "NEX Token", + "abi" : { + "events" : [ + { + "parameters" : [ + { + "type" : "Hash160", + "name" : "from" + }, + { + "name" : "to", + "type" : "Hash160" + }, + { + "name" : "amount", + "type" : "Integer" + } + ], + "name" : "Transfer" + }, + { + "name" : "OnMint", + "parameters" : [ + { + "name" : "from", + "type" : "Hash160" + }, + { + "type" : "Hash160", + "name" : "to" + }, + { + "type" : "Integer", + "name" : "amount" + }, + { + "name" : "swapId", + "type" : "Integer" + } + ] + } + ], + "methods" : [ + { + "parameters" : [], + "offset" : 0, + "name" : "_initialize", + "safe" : false, + "returntype" : "Void" + }, + { + "parameters" : [ + { + "type" : "Any", + "name" : "data" + }, + { + "name" : "isUpdate", + "type" : "Boolean" + } + ], + "offset" : 3, + "name" : "_deploy", + "safe" : false, + "returntype" : "Void" + }, + { + "parameters" : [ + { + "type" : "Hash160", + "name" : "holder" + } + ], + "offset" : 484, + "returntype" : "Integer", + "safe" : true, + "name" : "balanceOf" + }, + { + "safe" : true, + "returntype" : "Integer", + "name" : "cap", + "offset" : 1881, + "parameters" : [] + }, + { + "name" : "changeMinter", + "safe" : false, + "returntype" : "Void", + "parameters" : [ + { + "name" : "newMinter", + "type" : "PublicKey" + } + ], + "offset" : 1678 + }, + { + "parameters" : [ + { + "type" : "Hash160", + "name" : "newOwner" + } + ], + "offset" : 1659, + "name" : "changeOwner", + "safe" : false, + "returntype" : "Void" + }, + { + "offset" : 466, + "parameters" : [], + "safe" : true, + "name" : "decimals", + "returntype" : "Integer" + }, + { + "returntype" : "Void", + "safe" : false, + "name" : "destroy", + "parameters" : [], + "offset" : 1194 + }, + { + "safe" : true, + "returntype" : "PublicKey", + "name" : "getMinter", + "offset" : 1671, + "parameters" : [] + }, + { + "parameters" : [], + "offset" : 1652, + "name" : "getOwner", + "safe" : true, + "returntype" : "Hash160" + }, + { + "name" : "maxSupply", + "safe" : false, + "returntype" : "Integer", + "parameters" : [], + "offset" : 468 + }, + { + "offset" : 1222, + "parameters" : [ + { + "name" : "from", + "type" : "Hash160" + }, + { + "name" : "to", + "type" : "Hash160" + }, + { + "type" : "Integer", + "name" : "amount" + }, + { + "name" : "swapId", + "type" : "Integer" + }, + { + "name" : "signature", + "type" : "Signature" + }, + { + "name" : "data", + "type" : "Any" + } + ], + "safe" : false, + "name" : "mint", + "returntype" : "Void" + }, + { + "safe" : true, + "name" : "symbol", + "returntype" : "String", + "parameters" : [], + "offset" : 460 + }, + { + "offset" : 1854, + "parameters" : [], + "name" : "totalMinted", + "safe" : true, + "returntype" : "Integer" + }, + { + "offset" : 478, + "parameters" : [], + "name" : "totalSupply", + "safe" : true, + "returntype" : "Integer" + }, + { + "offset" : 543, + "parameters" : [ + { + "name" : "from", + "type" : "Hash160" + }, + { + "name" : "to", + "type" : "Hash160" + }, + { + "type" : "Integer", + "name" : "amount" + }, + { + "name" : "data", + "type" : "Any" + } + ], + "name" : "transfer", + "safe" : false, + "returntype" : "Boolean" + }, + { + "offset" : 1205, + "parameters" : [ + { + "type" : "ByteArray", + "name" : "nef" + }, + { + "name" : "manifest", + "type" : "ByteArray" + } + ], + "safe" : false, + "returntype" : "Void", + "name" : "update" + }, + { + "offset" : 1717, + "parameters" : [ + { + "type" : "Integer", + "name" : "newCap" + } + ], + "returntype" : "Void", + "safe" : false, + "name" : "updateCap" + } + ] + }, + "supportedstandards" : [ + "NEP-17" + ], + "extra" : null, + "trusts" : [], + "features" : {}, + "groups" : [], + "permissions" : [ + { + "methods" : [ + "onNEP17Payment" + ], + "contract" : "*" + }, + { + "methods" : [ + "update", + "destroy" + ], + "contract" : "0xfffdc93764dbaddd97c48f252a53ea4643faa3fd" + } + ] + } diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go new file mode 100644 index 000000000..0c8b86c30 --- /dev/null +++ b/pkg/smartcontract/rpcbinding/binding.go @@ -0,0 +1,226 @@ +package rpcbinding + +import ( + "fmt" + "sort" + "text/template" + + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/binding" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard" +) + +const srcTmpl = ` +{{- define "METHOD" -}} +// {{.Name}} {{.Comment}} +func (c *ContractReader) {{.Name}}({{range $index, $arg := .Arguments -}} + {{- if ne $index 0}}, {{end}} + {{- .Name}} {{.Type}} + {{- end}}) {{if .ReturnType }}({{ .ReturnType }}, error) { + return unwrap.{{.CallFlag}}(c.invoker.Call(Hash, "{{ .NameABI }}"{{/* CallFlag field is used for function name */}} + {{- range $arg := .Arguments -}}, {{.Name}}{{end}})) + {{- else -}} (*result.Invoke, error) { + c.invoker.Call(Hash, "{{ .NameABI }}" + {{- range $arg := .Arguments -}}, {{.Name}}{{end}}) + {{- end}} +} +{{- end -}} +// Package {{.PackageName}} contains RPC wrappers for {{.ContractName}} contract. +package {{.PackageName}} + +import ( +{{range $m := .Imports}} "{{ $m }}" +{{end}}) + +// Hash contains contract hash. +var Hash = {{ .Hash }} + +// Invoker is used by ContractReader to call various safe methods. +type Invoker interface { + {{if or .IsNep11D .IsNep11ND}}nep11.Invoker + {{end -}} + {{if .IsNep17}}nep17.Invoker + {{end -}} + Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) +} + +// ContractReader implements safe contract methods. +type ContractReader struct { + {{if .IsNep11D}}nep11.DivisibleReader + {{end -}} + {{if .IsNep11ND}}nep11.NonDivisibleReader + {{end -}} + {{if .IsNep17}}nep17.TokenReader + {{end -}} + invoker Invoker +} + +// NewReader creates an instance of ContractReader using Hash and the given Invoker. +func NewReader(invoker Invoker) *ContractReader { + return &ContractReader{ + {{- if .IsNep11D}}*nep11.NewDivisibleReader(invoker, Hash), {{end}} + {{- if .IsNep11ND}}*nep11.NewNonDivisibleReader(invoker, Hash), {{end}} + {{- if .IsNep17}}*nep17.NewReader(invoker, Hash), {{end -}} + invoker} +} + +{{range $m := .Methods}} +{{template "METHOD" $m }} +{{end}}` + +var srcTemplate = template.Must(template.New("generate").Parse(srcTmpl)) + +type ( + ContractTmpl struct { + binding.ContractTmpl + IsNep11D bool + IsNep11ND bool + IsNep17 bool + } +) + +// NewConfig initializes and returns a new config instance. +func NewConfig() binding.Config { + return binding.NewConfig() +} + +// Generate writes Go file containing smartcontract bindings to the `cfg.Output`. +func Generate(cfg binding.Config) error { + bctr, err := binding.TemplateFromManifest(cfg, scTypeToGo) + if err != nil { + return err + } + ctr := scTemplateToRPC(cfg, bctr) + + return srcTemplate.Execute(cfg.Output, ctr) +} + +func dropManifestMethods(meths []binding.MethodTmpl, manifested []manifest.Method) []binding.MethodTmpl { + for _, m := range manifested { + for i := 0; i < len(meths); i++ { + if meths[i].NameABI == m.Name && len(meths[i].Arguments) == len(m.Parameters) { + meths = append(meths[:i], meths[i+1:]...) + i-- + } + } + } + return meths +} + +func dropStdMethods(meths []binding.MethodTmpl, std *standard.Standard) []binding.MethodTmpl { + meths = dropManifestMethods(meths, std.Manifest.ABI.Methods) + if std.Optional != nil { + meths = dropManifestMethods(meths, std.Optional) + } + if std.Base != nil { + return dropStdMethods(meths, std.Base) + } + return meths +} + +func scTypeToGo(name string, typ smartcontract.ParamType, overrides map[string]binding.Override) (string, string) { + switch typ { + case smartcontract.AnyType: + return "interface{}", "" + case smartcontract.BoolType: + return "bool", "" + case smartcontract.IntegerType: + return "*big.Int", "math/big" + case smartcontract.ByteArrayType: + return "[]byte", "" + case smartcontract.StringType: + return "string", "" + case smartcontract.Hash160Type: + return "util.Uint160", "github.com/nspcc-dev/neo-go/pkg/util" + case smartcontract.Hash256Type: + return "util.Uint256", "github.com/nspcc-dev/neo-go/pkg/util" + case smartcontract.PublicKeyType: + return "*keys.PublicKey", "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + case smartcontract.SignatureType: + return "[]byte", "" + case smartcontract.ArrayType: + return "[]interface{}", "" + case smartcontract.MapType: + return "*stackitem.Map", "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + case smartcontract.InteropInterfaceType: + return "interface{}", "" + case smartcontract.VoidType: + return "", "" + default: + panic("unreachable") + } +} + +func scTemplateToRPC(cfg binding.Config, bctr binding.ContractTmpl) ContractTmpl { + var imports = make(map[string]struct{}) + var ctr = ContractTmpl{ContractTmpl: bctr} + for i := range ctr.Imports { + imports[ctr.Imports[i]] = struct{}{} + } + ctr.Hash = fmt.Sprintf("%#v", cfg.Hash) + for _, std := range cfg.Manifest.SupportedStandards { + if std == manifest.NEP11StandardName { + imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"] = struct{}{} + if standard.ComplyABI(cfg.Manifest, standard.Nep11Divisible) == nil { + ctr.Methods = dropStdMethods(ctr.Methods, standard.Nep11Divisible) + ctr.IsNep11D = true + } else if standard.ComplyABI(cfg.Manifest, standard.Nep11NonDivisible) == nil { + ctr.Methods = dropStdMethods(ctr.Methods, standard.Nep11NonDivisible) + ctr.IsNep11ND = true + } + break // Can't be NEP-17 at the same time. + } + if std == manifest.NEP17StandardName && standard.ComplyABI(cfg.Manifest, standard.Nep17) == nil { + ctr.Methods = dropStdMethods(ctr.Methods, standard.Nep17) + imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"] = struct{}{} + ctr.IsNep17 = true + break // Can't be NEP-11 at the same time. + } + } + for i := 0; i < len(ctr.Methods); i++ { + abim := cfg.Manifest.ABI.GetMethod(ctr.Methods[i].NameABI, len(ctr.Methods[i].Arguments)) + if !abim.Safe { + ctr.Methods = append(ctr.Methods[:i], ctr.Methods[i+1:]...) + i-- + } + } + // We're misusing CallFlag field for function name here. + for i := range ctr.Methods { + switch ctr.Methods[i].ReturnType { + case "interface{}": + imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{} + ctr.Methods[i].ReturnType = "stackitem.Item" + ctr.Methods[i].CallFlag = "Item" + case "bool": + ctr.Methods[i].CallFlag = "Bool" + case "*big.Int": + ctr.Methods[i].CallFlag = "BigInt" + case "string": + ctr.Methods[i].CallFlag = "UTF8String" + case "util.Uint160": + ctr.Methods[i].CallFlag = "Uint160" + case "util.Uint256": + ctr.Methods[i].CallFlag = "Uint256" + case "*keys.PublicKey": + ctr.Methods[i].CallFlag = "PublicKey" + case "[]byte": + ctr.Methods[i].CallFlag = "Bytes" + case "[]interface{}": + imports["github.com/nspcc-dev/neo-go/pkg/vm/stackitem"] = struct{}{} + ctr.Methods[i].ReturnType = "[]stackitem.Item" + ctr.Methods[i].CallFlag = "Array" + case "*stackitem.Map": + ctr.Methods[i].CallFlag = "Map" + } + } + + imports["github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"] = struct{}{} + imports["github.com/nspcc-dev/neo-go/pkg/neorpc/result"] = struct{}{} + ctr.Imports = ctr.Imports[:0] + for imp := range imports { + ctr.Imports = append(ctr.Imports, imp) + } + sort.Strings(ctr.Imports) + return ctr +}