From 82c6ce218ba09d769e037326f6ea4916b51d24a8 Mon Sep 17 00:00:00 2001
From: Roman Khimov <roman@nspcc.ru>
Date: Fri, 11 Nov 2022 14:59:01 +0300
Subject: [PATCH] rpcbinding: use binding condig to generate code for simple
 arrays

Part of #2767.
---
 cli/smartcontract/generate_test.go            |  38 ++++++
 cli/smartcontract/testdata/types/config.yml   |   3 +
 .../testdata/types/rpcbindings.out            | 109 ++++++++++++++++++
 cli/smartcontract/testdata/types/types.go     |  69 +++++++++++
 docs/compiler.md                              |  11 +-
 pkg/smartcontract/rpcbinding/binding.go       |  37 ++++++
 6 files changed, 266 insertions(+), 1 deletion(-)
 create mode 100644 cli/smartcontract/testdata/types/config.yml
 create mode 100644 cli/smartcontract/testdata/types/rpcbindings.out
 create mode 100644 cli/smartcontract/testdata/types/types.go

diff --git a/cli/smartcontract/generate_test.go b/cli/smartcontract/generate_test.go
index 80a33aa89..ed0e32157 100644
--- a/cli/smartcontract/generate_test.go
+++ b/cli/smartcontract/generate_test.go
@@ -365,6 +365,44 @@ func TestGenerateRPCBindings(t *testing.T) {
 		filepath.Join("testdata", "nonepiter", "iter.go"))
 }
 
+func TestAssistedRPCBindings(t *testing.T) {
+	tmpDir := t.TempDir()
+	app := cli.NewApp()
+	app.Commands = NewCommands()
+
+	var checkBinding = func(source string) {
+		t.Run(source, func(t *testing.T) {
+			manifestF := filepath.Join(tmpDir, "manifest.json")
+			bindingF := filepath.Join(tmpDir, "binding.yml")
+			nefF := filepath.Join(tmpDir, "out.nef")
+			require.NoError(t, app.Run([]string{"", "contract", "compile",
+				"--in", source,
+				"--config", filepath.Join(source, "config.yml"),
+				"--manifest", manifestF,
+				"--bindings", bindingF,
+				"--out", nefF,
+			}))
+			outFile := filepath.Join(tmpDir, "out.go")
+			require.NoError(t, app.Run([]string{"", "contract", "generate-rpcwrapper",
+				"--config", bindingF,
+				"--manifest", manifestF,
+				"--out", outFile,
+				"--hash", "0x00112233445566778899aabbccddeeff00112233",
+			}))
+
+			data, err := os.ReadFile(outFile)
+			require.NoError(t, err)
+			data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
+			expected, err := os.ReadFile(filepath.Join(source, "rpcbindings.out"))
+			require.NoError(t, err)
+			expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows.
+			require.Equal(t, string(expected), string(data))
+		})
+	}
+
+	checkBinding(filepath.Join("testdata", "types"))
+}
+
 func TestGenerate_Errors(t *testing.T) {
 	app := cli.NewApp()
 	app.Commands = []cli.Command{generateWrapperCmd}
diff --git a/cli/smartcontract/testdata/types/config.yml b/cli/smartcontract/testdata/types/config.yml
new file mode 100644
index 000000000..52ca5bbfc
--- /dev/null
+++ b/cli/smartcontract/testdata/types/config.yml
@@ -0,0 +1,3 @@
+name: "Types"
+sourceurl: https://github.com/nspcc-dev/neo-go/
+safemethods: ["bool", "int", "bytes", "string", "hash160", "hash256", "publicKey", "signature", "bools", "ints", "bytess", "strings", "hash160s", "hash256s", "publicKeys", "signatures"]
diff --git a/cli/smartcontract/testdata/types/rpcbindings.out b/cli/smartcontract/testdata/types/rpcbindings.out
new file mode 100644
index 000000000..3f7cbb48d
--- /dev/null
+++ b/cli/smartcontract/testdata/types/rpcbindings.out
@@ -0,0 +1,109 @@
+// Package types contains RPC wrappers for Types contract.
+package types
+
+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/unwrap"
+	"github.com/nspcc-dev/neo-go/pkg/util"
+	"math/big"
+)
+
+// Hash contains contract hash.
+var Hash = util.Uint160{0x33, 0x22, 0x11, 0x0, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x0}
+
+// 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}
+}
+
+
+// Bool invokes `bool` method of contract.
+func (c *ContractReader) Bool(b bool) (bool, error) {
+	return unwrap.Bool(c.invoker.Call(Hash, "bool", b))
+}
+
+// Bools invokes `bools` method of contract.
+func (c *ContractReader) Bools(b []bool) ([]bool, error) {
+	return unwrap.ArrayOfBools(c.invoker.Call(Hash, "bools", b))
+}
+
+// Bytes invokes `bytes` method of contract.
+func (c *ContractReader) Bytes(b []byte) ([]byte, error) {
+	return unwrap.Bytes(c.invoker.Call(Hash, "bytes", b))
+}
+
+// Bytess invokes `bytess` method of contract.
+func (c *ContractReader) Bytess(b [][]byte) ([][]byte, error) {
+	return unwrap.ArrayOfBytes(c.invoker.Call(Hash, "bytess", b))
+}
+
+// Hash160 invokes `hash160` method of contract.
+func (c *ContractReader) Hash160(h util.Uint160) (util.Uint160, error) {
+	return unwrap.Uint160(c.invoker.Call(Hash, "hash160", h))
+}
+
+// Hash160s invokes `hash160s` method of contract.
+func (c *ContractReader) Hash160s(h []util.Uint160) ([]util.Uint160, error) {
+	return unwrap.ArrayOfUint160(c.invoker.Call(Hash, "hash160s", h))
+}
+
+// Hash256 invokes `hash256` method of contract.
+func (c *ContractReader) Hash256(h util.Uint256) (util.Uint256, error) {
+	return unwrap.Uint256(c.invoker.Call(Hash, "hash256", h))
+}
+
+// Hash256s invokes `hash256s` method of contract.
+func (c *ContractReader) Hash256s(h []util.Uint256) ([]util.Uint256, error) {
+	return unwrap.ArrayOfUint256(c.invoker.Call(Hash, "hash256s", h))
+}
+
+// Int invokes `int` method of contract.
+func (c *ContractReader) Int(i *big.Int) (*big.Int, error) {
+	return unwrap.BigInt(c.invoker.Call(Hash, "int", i))
+}
+
+// Ints invokes `ints` method of contract.
+func (c *ContractReader) Ints(i []*big.Int) ([]*big.Int, error) {
+	return unwrap.ArrayOfBigInts(c.invoker.Call(Hash, "ints", i))
+}
+
+// PublicKey invokes `publicKey` method of contract.
+func (c *ContractReader) PublicKey(k *keys.PublicKey) (*keys.PublicKey, error) {
+	return unwrap.PublicKey(c.invoker.Call(Hash, "publicKey", k))
+}
+
+// PublicKeys invokes `publicKeys` method of contract.
+func (c *ContractReader) PublicKeys(k keys.PublicKeys) (keys.PublicKeys, error) {
+	return unwrap.ArrayOfPublicKeys(c.invoker.Call(Hash, "publicKeys", k))
+}
+
+// Signature invokes `signature` method of contract.
+func (c *ContractReader) Signature(s []byte) ([]byte, error) {
+	return unwrap.Bytes(c.invoker.Call(Hash, "signature", s))
+}
+
+// Signatures invokes `signatures` method of contract.
+func (c *ContractReader) Signatures(s [][]byte) ([][]byte, error) {
+	return unwrap.ArrayOfBytes(c.invoker.Call(Hash, "signatures", s))
+}
+
+// String invokes `string` method of contract.
+func (c *ContractReader) String(s string) (string, error) {
+	return unwrap.UTF8String(c.invoker.Call(Hash, "string", s))
+}
+
+// Strings invokes `strings` method of contract.
+func (c *ContractReader) Strings(s []string) ([]string, error) {
+	return unwrap.ArrayOfUTF8Strings(c.invoker.Call(Hash, "strings", s))
+}
diff --git a/cli/smartcontract/testdata/types/types.go b/cli/smartcontract/testdata/types/types.go
new file mode 100644
index 000000000..dcd52ae90
--- /dev/null
+++ b/cli/smartcontract/testdata/types/types.go
@@ -0,0 +1,69 @@
+package types
+
+import (
+	"github.com/nspcc-dev/neo-go/pkg/interop"
+)
+
+func Bool(b bool) bool {
+	return false
+}
+
+func Int(i int) int {
+	return 0
+}
+
+func Bytes(b []byte) []byte {
+	return nil
+}
+
+func String(s string) string {
+	return ""
+}
+
+func Hash160(h interop.Hash160) interop.Hash160 {
+	return nil
+}
+
+func Hash256(h interop.Hash256) interop.Hash256 {
+	return nil
+}
+
+func PublicKey(k interop.PublicKey) interop.PublicKey {
+	return nil
+}
+
+func Signature(s interop.Signature) interop.Signature {
+	return nil
+}
+
+func Bools(b []bool) []bool {
+	return nil
+}
+
+func Ints(i []int) []int {
+	return nil
+}
+
+func Bytess(b [][]byte) [][]byte {
+	return nil
+}
+
+func Strings(s []string) []string {
+	return nil
+}
+
+func Hash160s(h []interop.Hash160) []interop.Hash160 {
+	return nil
+}
+
+func Hash256s(h []interop.Hash256) []interop.Hash256 {
+	return nil
+}
+
+func PublicKeys(k []interop.PublicKey) []interop.PublicKey {
+	return nil
+}
+
+func Signatures(s []interop.Signature) []interop.Signature {
+	return nil
+}
diff --git a/docs/compiler.md b/docs/compiler.md
index 78e6d0416..3d7edd638 100644
--- a/docs/compiler.md
+++ b/docs/compiler.md
@@ -446,6 +446,10 @@ does not do anything else unless the method's returned value is of a boolean
 type, in this case an ASSERT is added to script making it fail when the method
 returns false.
 
+```
+$ ./bin/neo-go contract generate-rpcwrapper --manifest manifest.json --out rpcwrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
+```
+
 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
@@ -454,8 +458,13 @@ iterator and an appropriate unwrapper is used with UUID and iterator structure
 result. This pair can then be used in Invoker `TraverseIterator` method to
 retrieve actual resulting items.
 
+Go contracts can also make use of additional type data from bindings
+configuration file generated during compilation. At the moment it allows to
+generate proper wrappers for simple array types, but doesn't cover structures:
+
 ```
-$ ./bin/neo-go contract generate-rpcwrapper --manifest manifest.json --out rpcwrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
+$ ./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-rpcwrapper --manifest manifest.json --config contract.bindings.yml --out rpcwrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
 ```
 
 ## Smart contract examples
diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go
index be121c828..6c330cab3 100644
--- a/pkg/smartcontract/rpcbinding/binding.go
+++ b/pkg/smartcontract/rpcbinding/binding.go
@@ -296,6 +296,29 @@ func dropStdMethods(meths []manifest.Method, std *standard.Standard) []manifest.
 }
 
 func scTypeToGo(name string, typ smartcontract.ParamType, overrides map[string]binding.Override) (string, string) {
+	over, ok := overrides[name]
+	if ok {
+		switch over.TypeName {
+		case "[]bool":
+			return "[]bool", ""
+		case "[]int", "[]uint", "[]int8", "[]uint8", "[]int16",
+			"[]uint16", "[]int32", "[]uint32", "[]int64", "[]uint64":
+			return "[]*big.Int", "math/big"
+		case "[][]byte":
+			return "[][]byte", ""
+		case "[]string":
+			return "[]string", ""
+		case "[]interop.Hash160":
+			return "[]util.Uint160", "github.com/nspcc-dev/neo-go/pkg/util"
+		case "[]interop.Hash256":
+			return "[]util.Uint256", "github.com/nspcc-dev/neo-go/pkg/util"
+		case "[]interop.PublicKey":
+			return "keys.PublicKeys", "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+		case "[]interop.Signature":
+			return "[][]byte", ""
+		}
+	}
+
 	switch typ {
 	case smartcontract.AnyType:
 		return "interface{}", ""
@@ -383,6 +406,20 @@ func scTemplateToRPC(cfg binding.Config, ctr ContractTmpl, imports map[string]st
 			ctr.SafeMethods[i].CallFlag = "Array"
 		case "*stackitem.Map":
 			ctr.SafeMethods[i].CallFlag = "Map"
+		case "[]bool":
+			ctr.SafeMethods[i].CallFlag = "ArrayOfBools"
+		case "[]*big.Int":
+			ctr.SafeMethods[i].CallFlag = "ArrayOfBigInts"
+		case "[][]byte":
+			ctr.SafeMethods[i].CallFlag = "ArrayOfBytes"
+		case "[]string":
+			ctr.SafeMethods[i].CallFlag = "ArrayOfUTF8Strings"
+		case "[]util.Uint160":
+			ctr.SafeMethods[i].CallFlag = "ArrayOfUint160"
+		case "[]util.Uint256":
+			ctr.SafeMethods[i].CallFlag = "ArrayOfUint256"
+		case "keys.PublicKeys":
+			ctr.SafeMethods[i].CallFlag = "ArrayOfPublicKeys"
 		}
 	}