From d6d4f072807a26d68b736a3d6361311b53eb60ae Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 26 Oct 2022 12:40:59 +0300 Subject: [PATCH 01/10] binding: always import some packages and do it outside If the contract has no methods, it's probably a broken one. --- pkg/smartcontract/binding/generate.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/smartcontract/binding/generate.go b/pkg/smartcontract/binding/generate.go index b38802a21..aebab9ddf 100644 --- a/pkg/smartcontract/binding/generate.go +++ b/pkg/smartcontract/binding/generate.go @@ -92,6 +92,9 @@ func Generate(cfg Config) error { if err != nil { return err } + ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/contract") + ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal") + sort.Strings(ctr.Imports) tmp, err := template.New("generate").Funcs(template.FuncMap{ "lowerFirst": lowerFirst, @@ -169,9 +172,6 @@ func templateFromManifest(cfg Config) (contractTmpl, error) { continue } - imports["github.com/nspcc-dev/neo-go/pkg/interop/contract"] = struct{}{} - imports["github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"] = struct{}{} - // Consider `perform(a)` and `perform(a, b)` methods. // First, try to export the second method with `Perform2` name. // If `perform2` is already in the manifest, use `perform_2` with as many underscores @@ -240,7 +240,6 @@ func templateFromManifest(cfg Config) (contractTmpl, error) { for imp := range imports { ctr.Imports = append(ctr.Imports, imp) } - sort.Strings(ctr.Imports) return ctr, nil } From 4191b18728f43fb4f9daf69dcac0d98d2613da5f Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 26 Oct 2022 12:43:48 +0300 Subject: [PATCH 02/10] binding: make TemplateFromManifest more reusable Other template generators can make use of it. --- pkg/smartcontract/binding/generate.go | 83 ++++++++++++--------------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/pkg/smartcontract/binding/generate.go b/pkg/smartcontract/binding/generate.go index aebab9ddf..17d5e028d 100644 --- a/pkg/smartcontract/binding/generate.go +++ b/pkg/smartcontract/binding/generate.go @@ -55,24 +55,24 @@ type ( Output io.Writer `yaml:"-"` } - contractTmpl struct { + ContractTmpl struct { PackageName string ContractName string Imports []string Hash string - Methods []methodTmpl + Methods []MethodTmpl } - methodTmpl struct { + MethodTmpl struct { Name string NameABI string CallFlag string Comment string - Arguments []paramTmpl + Arguments []ParamTmpl ReturnType string } - paramTmpl struct { + ParamTmpl struct { Name string Type string } @@ -88,7 +88,7 @@ func NewConfig() Config { // Generate writes Go file containing smartcontract bindings to the `cfg.Output`. func Generate(cfg Config) error { - ctr, err := templateFromManifest(cfg) + ctr, err := TemplateFromManifest(cfg, scTypeToGo) if err != nil { return err } @@ -98,7 +98,6 @@ func Generate(cfg Config) error { tmp, err := template.New("generate").Funcs(template.FuncMap{ "lowerFirst": lowerFirst, - "scTypeToGo": scTypeToGo, }).Parse(srcTmpl) if err != nil { return err @@ -107,46 +106,52 @@ func Generate(cfg Config) error { return tmp.Execute(cfg.Output, ctr) } -func scTypeToGo(typ smartcontract.ParamType) string { +func scTypeToGo(name string, typ smartcontract.ParamType, overrides map[string]Override) (string, string) { + if over, ok := overrides[name]; ok { + return over.TypeName, over.Package + } + switch typ { case smartcontract.AnyType: - return "interface{}" + return "interface{}", "" case smartcontract.BoolType: - return "bool" + return "bool", "" case smartcontract.IntegerType: - return "int" + return "int", "" case smartcontract.ByteArrayType: - return "[]byte" + return "[]byte", "" case smartcontract.StringType: - return "string" + return "string", "" case smartcontract.Hash160Type: - return "interop.Hash160" + return "interop.Hash160", "github.com/nspcc-dev/neo-go/pkg/interop" case smartcontract.Hash256Type: - return "interop.Hash256" + return "interop.Hash256", "github.com/nspcc-dev/neo-go/pkg/interop" case smartcontract.PublicKeyType: - return "interop.PublicKey" + return "interop.PublicKey", "github.com/nspcc-dev/neo-go/pkg/interop" case smartcontract.SignatureType: - return "interop.Signature" + return "interop.Signature", "github.com/nspcc-dev/neo-go/pkg/interop" case smartcontract.ArrayType: - return "[]interface{}" + return "[]interface{}", "" case smartcontract.MapType: - return "map[string]interface{}" + return "map[string]interface{}", "" case smartcontract.InteropInterfaceType: - return "interface{}" + return "interface{}", "" case smartcontract.VoidType: - return "" + return "", "" default: panic("unreachable") } } -func templateFromManifest(cfg Config) (contractTmpl, error) { +// TemplateFromManifest create a contract template using the given configuration +// and type conversion function. +func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract.ParamType, map[string]Override) (string, string)) (ContractTmpl, error) { hStr := "" for _, b := range cfg.Hash.BytesBE() { hStr += fmt.Sprintf("\\x%02x", b) } - ctr := contractTmpl{ + ctr := ContractTmpl{ PackageName: cfg.Package, ContractName: cfg.Manifest.Name, Hash: hStr, @@ -187,7 +192,7 @@ func templateFromManifest(cfg Config) (contractTmpl, error) { } seen[name] = true - mtd := methodTmpl{ + mtd := MethodTmpl{ Name: upperFirst(name), NameABI: m.Name, CallFlag: callflag.All.String(), @@ -204,36 +209,22 @@ func templateFromManifest(cfg Config) (contractTmpl, error) { return ctr, fmt.Errorf("manifest ABI method %q/%d: parameter #%d is unnamed", m.Name, len(m.Parameters), i) } - var typeStr string - if over, ok := cfg.Overrides[m.Name+"."+name]; ok { - typeStr = over.TypeName - if over.Package != "" { - imports[over.Package] = struct{}{} - } - } else { - typeStr = scTypeToGo(m.Parameters[i].Type) + typeStr, pkg := scTypeConverter(m.Name+"."+name, m.Parameters[i].Type, cfg.Overrides) + if pkg != "" { + imports[pkg] = struct{}{} } - mtd.Arguments = append(mtd.Arguments, paramTmpl{ + mtd.Arguments = append(mtd.Arguments, ParamTmpl{ Name: name, Type: typeStr, }) } - if over, ok := cfg.Overrides[m.Name]; ok { - mtd.ReturnType = over.TypeName - if over.Package != "" { - imports[over.Package] = struct{}{} - } - } else { - mtd.ReturnType = scTypeToGo(m.ReturnType) - switch m.ReturnType { - case smartcontract.Hash160Type, smartcontract.Hash256Type, smartcontract.InteropInterfaceType, - smartcontract.SignatureType, smartcontract.PublicKeyType: - imports["github.com/nspcc-dev/neo-go/pkg/interop"] = struct{}{} - } + typeStr, pkg := scTypeConverter(m.Name, m.ReturnType, cfg.Overrides) + if pkg != "" { + imports[pkg] = struct{}{} } - + mtd.ReturnType = typeStr ctr.Methods = append(ctr.Methods, mtd) } From 396f56f792ebac385dc94b70472f60aa22b7ed77 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 26 Oct 2022 12:47:39 +0300 Subject: [PATCH 03/10] binding: drop unused lowerFirst --- pkg/smartcontract/binding/generate.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pkg/smartcontract/binding/generate.go b/pkg/smartcontract/binding/generate.go index 17d5e028d..f9dfb2986 100644 --- a/pkg/smartcontract/binding/generate.go +++ b/pkg/smartcontract/binding/generate.go @@ -96,9 +96,7 @@ func Generate(cfg Config) error { ctr.Imports = append(ctr.Imports, "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal") sort.Strings(ctr.Imports) - tmp, err := template.New("generate").Funcs(template.FuncMap{ - "lowerFirst": lowerFirst, - }).Parse(srcTmpl) + tmp, err := template.New("generate").Parse(srcTmpl) if err != nil { return err } @@ -238,7 +236,3 @@ func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract func upperFirst(s string) string { return strings.ToUpper(s[0:1]) + s[1:] } - -func lowerFirst(s string) string { - return strings.ToLower(s[0:1]) + s[1:] -} From 64b603b05618787971bedd0e15b6fbeeb7a4a607 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 26 Oct 2022 21:23:12 +0300 Subject: [PATCH 04/10] standard: export standard definitions Make Comply* functions useful and expose standard definitions for some reuse. --- pkg/smartcontract/manifest/standard/comply.go | 8 ++++---- .../manifest/standard/comply_test.go | 6 +++--- pkg/smartcontract/manifest/standard/nep11.go | 15 +++++++++------ pkg/smartcontract/manifest/standard/nep17.go | 5 +++-- pkg/smartcontract/manifest/standard/payable.go | 6 ++++-- pkg/smartcontract/manifest/standard/token.go | 3 ++- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/pkg/smartcontract/manifest/standard/comply.go b/pkg/smartcontract/manifest/standard/comply.go index ab7a3a2bd..edf3634b1 100644 --- a/pkg/smartcontract/manifest/standard/comply.go +++ b/pkg/smartcontract/manifest/standard/comply.go @@ -19,10 +19,10 @@ var ( ) var checks = map[string][]*Standard{ - manifest.NEP11StandardName: {nep11NonDivisible, nep11Divisible}, - manifest.NEP17StandardName: {nep17}, - manifest.NEP11Payable: {nep11payable}, - manifest.NEP17Payable: {nep17payable}, + manifest.NEP11StandardName: {Nep11NonDivisible, Nep11Divisible}, + manifest.NEP17StandardName: {Nep17}, + manifest.NEP11Payable: {Nep11Payable}, + manifest.NEP17Payable: {Nep17Payable}, } // Check checks if the manifest complies with all provided standards. diff --git a/pkg/smartcontract/manifest/standard/comply_test.go b/pkg/smartcontract/manifest/standard/comply_test.go index 0d218c536..360a7c1de 100644 --- a/pkg/smartcontract/manifest/standard/comply_test.go +++ b/pkg/smartcontract/manifest/standard/comply_test.go @@ -135,9 +135,9 @@ func TestCheck(t *testing.T) { m := manifest.NewManifest("Test") require.Error(t, Check(m, manifest.NEP17StandardName)) - m.ABI.Methods = append(m.ABI.Methods, decimalTokenBase.ABI.Methods...) - m.ABI.Methods = append(m.ABI.Methods, nep17.ABI.Methods...) - m.ABI.Events = append(m.ABI.Events, nep17.ABI.Events...) + m.ABI.Methods = append(m.ABI.Methods, DecimalTokenBase.ABI.Methods...) + m.ABI.Methods = append(m.ABI.Methods, Nep17.ABI.Methods...) + m.ABI.Events = append(m.ABI.Events, Nep17.ABI.Events...) require.NoError(t, Check(m, manifest.NEP17StandardName)) require.NoError(t, CheckABI(m, manifest.NEP17StandardName)) } diff --git a/pkg/smartcontract/manifest/standard/nep11.go b/pkg/smartcontract/manifest/standard/nep11.go index 3d896a0ff..7f94173fa 100644 --- a/pkg/smartcontract/manifest/standard/nep11.go +++ b/pkg/smartcontract/manifest/standard/nep11.go @@ -5,8 +5,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" ) -var nep11Base = &Standard{ - Base: decimalTokenBase, +// Nep11Base is a Standard containing common NEP-11 methods. +var Nep11Base = &Standard{ + Base: DecimalTokenBase, Manifest: manifest.Manifest{ ABI: manifest.ABI{ Methods: []manifest.Method{ @@ -66,8 +67,9 @@ var nep11Base = &Standard{ }, } -var nep11NonDivisible = &Standard{ - Base: nep11Base, +// Nep11NonDivisible is a NEP-11 non-divisible Standard. +var Nep11NonDivisible = &Standard{ + Base: Nep11Base, Manifest: manifest.Manifest{ ABI: manifest.ABI{ Methods: []manifest.Method{ @@ -84,8 +86,9 @@ var nep11NonDivisible = &Standard{ }, } -var nep11Divisible = &Standard{ - Base: nep11Base, +// Nep11Divisible is a NEP-11 divisible Standard. +var Nep11Divisible = &Standard{ + Base: Nep11Base, Manifest: manifest.Manifest{ ABI: manifest.ABI{ Methods: []manifest.Method{ diff --git a/pkg/smartcontract/manifest/standard/nep17.go b/pkg/smartcontract/manifest/standard/nep17.go index 4e08c4120..4d4422aab 100644 --- a/pkg/smartcontract/manifest/standard/nep17.go +++ b/pkg/smartcontract/manifest/standard/nep17.go @@ -5,8 +5,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" ) -var nep17 = &Standard{ - Base: decimalTokenBase, +// Nep17 is a NEP-17 Standard. +var Nep17 = &Standard{ + Base: DecimalTokenBase, Manifest: manifest.Manifest{ ABI: manifest.ABI{ Methods: []manifest.Method{ diff --git a/pkg/smartcontract/manifest/standard/payable.go b/pkg/smartcontract/manifest/standard/payable.go index 9cb4f39c6..d0e3de6fa 100644 --- a/pkg/smartcontract/manifest/standard/payable.go +++ b/pkg/smartcontract/manifest/standard/payable.go @@ -5,7 +5,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" ) -var nep11payable = &Standard{ +// Nep11Payable contains NEP-11's onNEP11Payment method definition. +var Nep11Payable = &Standard{ Manifest: manifest.Manifest{ ABI: manifest.ABI{ Methods: []manifest.Method{{ @@ -22,7 +23,8 @@ var nep11payable = &Standard{ }, } -var nep17payable = &Standard{ +// Nep17Payable contains NEP-17's onNEP17Payment method definition. +var Nep17Payable = &Standard{ Manifest: manifest.Manifest{ ABI: manifest.ABI{ Methods: []manifest.Method{{ diff --git a/pkg/smartcontract/manifest/standard/token.go b/pkg/smartcontract/manifest/standard/token.go index 877050131..2600fb1d7 100644 --- a/pkg/smartcontract/manifest/standard/token.go +++ b/pkg/smartcontract/manifest/standard/token.go @@ -5,7 +5,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" ) -var decimalTokenBase = &Standard{ +// DecimalTokenBase contains methods common to NEP-11 and NEP-17 token standards. +var DecimalTokenBase = &Standard{ Manifest: manifest.Manifest{ ABI: manifest.ABI{ Methods: []manifest.Method{ From e0eff94094ea8fdcf9942343c4e2544561b5ac43 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 26 Oct 2022 21:24:34 +0300 Subject: [PATCH 05/10] standard: correct Comply* comments --- pkg/smartcontract/manifest/standard/comply.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/smartcontract/manifest/standard/comply.go b/pkg/smartcontract/manifest/standard/comply.go index edf3634b1..f8991c793 100644 --- a/pkg/smartcontract/manifest/standard/comply.go +++ b/pkg/smartcontract/manifest/standard/comply.go @@ -55,12 +55,12 @@ func check(m *manifest.Manifest, checkNames bool, standards ...string) error { } // Comply if m has all methods and event from st manifest and they have the same signature. -// Parameter names are ignored. +// Parameter names are checked to exactly match the ones in the given standard. func Comply(m *manifest.Manifest, st *Standard) error { return comply(m, true, st) } -// ComplyABI is similar to comply but doesn't check parameter names. +// ComplyABI is similar to Comply but doesn't check parameter names. func ComplyABI(m *manifest.Manifest, st *Standard) error { return comply(m, false, st) } From f0abc035aff2558f5173598b3725aeb1ce6562c5 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 27 Oct 2022 14:45:40 +0300 Subject: [PATCH 06/10] unwrap: add PublicKey to unwrap public keys We have this type in NEP-14 directly. --- pkg/rpcclient/unwrap/unwrap.go | 10 ++++++++++ pkg/rpcclient/unwrap/unwrap_test.go | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pkg/rpcclient/unwrap/unwrap.go b/pkg/rpcclient/unwrap/unwrap.go index c31c74558..fad8bcffa 100644 --- a/pkg/rpcclient/unwrap/unwrap.go +++ b/pkg/rpcclient/unwrap/unwrap.go @@ -146,6 +146,16 @@ func Uint256(r *result.Invoke, err error) (util.Uint256, error) { return util.Uint256DecodeBytesBE(b) } +// PublicKey expects correct execution (HALT state) with a single stack item +// returned. A public key is extracted from this item and returned. +func PublicKey(r *result.Invoke, err error) (*keys.PublicKey, error) { + b, err := Bytes(r, err) + if err != nil { + return nil, err + } + return keys.NewPublicKeyFromBytes(b, elliptic.P256()) +} + // SessionIterator expects correct execution (HALT state) with a single stack // item returned. If this item is an iterator it's returned to the caller along // with the session ID. Notice that this function also returns successfully diff --git a/pkg/rpcclient/unwrap/unwrap_test.go b/pkg/rpcclient/unwrap/unwrap_test.go index 29606da5c..5544216cc 100644 --- a/pkg/rpcclient/unwrap/unwrap_test.go +++ b/pkg/rpcclient/unwrap/unwrap_test.go @@ -43,6 +43,9 @@ func TestStdErrors(t *testing.T) { func(r *result.Invoke, err error) (interface{}, error) { return Uint256(r, err) }, + func(r *result.Invoke, err error) (interface{}, error) { + return PublicKey(r, err) + }, func(r *result.Invoke, err error) (interface{}, error) { _, _, err = SessionIterator(r, err) return nil, err @@ -189,6 +192,18 @@ func TestUint256(t *testing.T) { require.Equal(t, util.Uint256{1, 2, 3}, u) } +func TestPublicKey(t *testing.T) { + k, err := keys.NewPrivateKey() + require.NoError(t, err) + + _, err = PublicKey(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(util.Uint160{1, 2, 3}.BytesBE())}}, nil) + require.Error(t, err) + + pk, err := PublicKey(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(k.PublicKey().Bytes())}}, nil) + require.NoError(t, err) + require.Equal(t, k.PublicKey(), pk) +} + func TestSessionIterator(t *testing.T) { _, _, err := SessionIterator(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) require.Error(t, err) From 617c31093fb6d12394d58e6bc8530f4c341157eb Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 26 Oct 2022 23:27:24 +0300 Subject: [PATCH 07/10] 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 +} From 3fba1dd8cfe78d9dfba5ccaa6932adf1dc028660 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 27 Oct 2022 22:17:59 +0300 Subject: [PATCH 08/10] docs: add generate-wrapper explainer --- docs/compiler.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/compiler.md b/docs/compiler.md index 83e0dec0c..9682b45ea 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -410,6 +410,30 @@ given RPC server and wallet and paying 0.00001 extra GAS for this transaction): $ ./bin/neo-go contract invokefunction -r http://localhost:20331 -w my_wallet.json -g 0.00001 f84d6a337fbc3d3a201d41da99e86b479e7a2554 balanceOf AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y ``` +### Generating contract bindings +To be able to use deployed contract from another contract one needs to have +its interface definition (exported methods and hash). While it is possible to +use generic contract.Call interop interface, it's not very convenient and +efficient. NeoGo can autogenerate contract bindings in Go language for any +deployed contract based on its manifest, it creates a Go source file with all +of the contract's methods that then can be imported and used as a regular Go +package. + +``` +$ ./bin/neo-go contract generate-wrapper --manifest manifest.json --out wrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176 +``` + +Notice that some structured types can be omitted this way (when a function +returns some structure it's just an "Array" type in the manifest with no +internal details), but if the contract you're using is written in Go +originally you can create a specific configuration file during compilation +that will add this data for wrapper generator to use: + +``` +$ ./bin/neo-go contract compile -i contract.go --config contract.yml -o contract.nef --manifest manifest.json --bindings contract.bindings.yml +$ ./bin/neo-go contract generate-wrapper --manifest manifest.json --config contract.bindings.yml --out wrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176 +``` + ## Smart contract examples Some examples are provided in the [examples directory](../examples). For more From 3b635164b7c7447df6b184fa62218b4eb5e2efd2 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 27 Oct 2022 22:22:49 +0300 Subject: [PATCH 09/10] docs: initial RPC wrapper generator doc --- docs/compiler.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/compiler.md b/docs/compiler.md index 9682b45ea..1b4f5833d 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -434,6 +434,19 @@ $ ./bin/neo-go contract compile -i contract.go --config contract.yml -o contract $ ./bin/neo-go contract generate-wrapper --manifest manifest.json --config contract.bindings.yml --out wrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176 ``` +### Generating RPC contract bindings +To simplify interacting with the contract via RPC you can generate +contract-specific RPC bindings with the "generate-rpcwrapper" command. At the +moment it only works for safe (read-only) methods. If your contract is NEP-11 +or NEP-17 that's autodetected and an appropriate package is included as +well. Notice that the type data available in the manifest is limited, so in +some cases the interface generated may use generic stackitem types. Iterators +are not supported yet. + +``` +$ ./bin/neo-go contract generate-rpcwrapper --manifest manifest.json --out rpcwrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176 +``` + ## Smart contract examples Some examples are provided in the [examples directory](../examples). For more From 02ce59cfd51f06e0119291e87bfdf55f5e7c9532 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 27 Oct 2022 22:35:06 +0300 Subject: [PATCH 10/10] binding: avoid name conflicts with Go keywords And clashing one name on another after rename. --- cli/smartcontract/generate_test.go | 14 ++++++++++++++ cli/smartcontract/testdata/nameservice/nns.go | 8 ++++---- pkg/smartcontract/binding/generate.go | 11 ++++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cli/smartcontract/generate_test.go b/cli/smartcontract/generate_test.go index 9eada4790..ef753fc60 100644 --- a/cli/smartcontract/generate_test.go +++ b/cli/smartcontract/generate_test.go @@ -45,6 +45,15 @@ func TestGenerate(t *testing.T) { ReturnType: smartcontract.IntegerType, Safe: true, }, + manifest.Method{ + Name: "zum", + Parameters: []manifest.Parameter{ + manifest.NewParameter("type", smartcontract.IntegerType), + manifest.NewParameter("typev", smartcontract.IntegerType), + manifest.NewParameter("func", smartcontract.IntegerType), + }, + ReturnType: smartcontract.IntegerType, + }, manifest.Method{ Name: "justExecute", Parameters: []manifest.Parameter{ @@ -172,6 +181,11 @@ func Sum3() int { return neogointernal.CallWithToken(Hash, "sum3", int(contract.ReadOnly)).(int) } +// Zum invokes ` + "`zum`" + ` method of contract. +func Zum(typev int, typev_ int, funcv int) int { + return neogointernal.CallWithToken(Hash, "zum", int(contract.All), typev, typev_, funcv).(int) +} + // JustExecute invokes ` + "`justExecute`" + ` method of contract. func JustExecute(arr []interface{}) { neogointernal.CallWithTokenNoRet(Hash, "justExecute", int(contract.All), arr) diff --git a/cli/smartcontract/testdata/nameservice/nns.go b/cli/smartcontract/testdata/nameservice/nns.go index 2d9259616..62c59082b 100644 --- a/cli/smartcontract/testdata/nameservice/nns.go +++ b/cli/smartcontract/testdata/nameservice/nns.go @@ -47,8 +47,8 @@ func (c *ContractReader) IsAvailable(name string) (bool, error) { } // 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)) +func (c *ContractReader) GetRecord(name string, typev *big.Int) (string, error) { + return unwrap.UTF8String(c.invoker.Call(Hash, "getRecord", name, typev)) } // GetAllRecords invokes `getAllRecords` method of contract. @@ -57,6 +57,6 @@ func (c *ContractReader) GetAllRecords(name string) (stackitem.Item, error) { } // 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)) +func (c *ContractReader) Resolve(name string, typev *big.Int) (string, error) { + return unwrap.UTF8String(c.invoker.Call(Hash, "resolve", name, typev)) } diff --git a/pkg/smartcontract/binding/generate.go b/pkg/smartcontract/binding/generate.go index f9dfb2986..b3a735917 100644 --- a/pkg/smartcontract/binding/generate.go +++ b/pkg/smartcontract/binding/generate.go @@ -3,6 +3,7 @@ package binding import ( "bytes" "fmt" + "go/token" "io" "sort" "strconv" @@ -201,6 +202,8 @@ func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract } else if m.Safe { mtd.CallFlag = callflag.ReadOnly.String() } + + var varnames = make(map[string]bool) for i := range m.Parameters { name := m.Parameters[i].Name if name == "" { @@ -211,7 +214,13 @@ func TemplateFromManifest(cfg Config, scTypeConverter func(string, smartcontract if pkg != "" { imports[pkg] = struct{}{} } - + if token.IsKeyword(name) { + name = name + "v" + } + for varnames[name] { + name = name + "_" + } + varnames[name] = true mtd.Arguments = append(mtd.Arguments, ParamTmpl{ Name: name, Type: typeStr,