diff --git a/cli/smartcontract/generate_test.go b/cli/smartcontract/generate_test.go index 80a33aa893..ed0e321575 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 0000000000..52ca5bbfca --- /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 0000000000..3f7cbb48dd --- /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 0000000000..dcd52ae900 --- /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 78e6d0416c..3d7edd638d 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/rpcclient/unwrap/unwrap.go b/pkg/rpcclient/unwrap/unwrap.go index fad8bcffad..f4c2243082 100644 --- a/pkg/rpcclient/unwrap/unwrap.go +++ b/pkg/rpcclient/unwrap/unwrap.go @@ -196,6 +196,42 @@ func Array(r *result.Invoke, err error) ([]stackitem.Item, error) { return arr, nil } +// ArrayOfBools checks the result for correct state (HALT) and then extracts a +// slice of boolean values from the returned stack item. +func ArrayOfBools(r *result.Invoke, err error) ([]bool, error) { + a, err := Array(r, err) + if err != nil { + return nil, err + } + res := make([]bool, len(a)) + for i := range a { + b, err := a[i].TryBool() + if err != nil { + return nil, fmt.Errorf("element %d is not a boolean: %w", i, err) + } + res[i] = b + } + return res, nil +} + +// ArrayOfBigInts checks the result for correct state (HALT) and then extracts a +// slice of (big) integer values from the returned stack item. +func ArrayOfBigInts(r *result.Invoke, err error) ([]*big.Int, error) { + a, err := Array(r, err) + if err != nil { + return nil, err + } + res := make([]*big.Int, len(a)) + for i := range a { + v, err := a[i].TryInteger() + if err != nil { + return nil, fmt.Errorf("element %d is not an integer: %w", i, err) + } + res[i] = v + } + return res, nil +} + // ArrayOfBytes checks the result for correct state (HALT) and then extracts a // slice of byte slices from the returned stack item. func ArrayOfBytes(r *result.Invoke, err error) ([][]byte, error) { @@ -214,6 +250,27 @@ func ArrayOfBytes(r *result.Invoke, err error) ([][]byte, error) { return res, nil } +// ArrayOfUTB8Strings checks the result for correct state (HALT) and then extracts a +// slice of UTF-8 strings from the returned stack item. +func ArrayOfUTF8Strings(r *result.Invoke, err error) ([]string, error) { + a, err := Array(r, err) + if err != nil { + return nil, err + } + res := make([]string, len(a)) + for i := range a { + b, err := a[i].TryBytes() + if err != nil { + return nil, fmt.Errorf("element %d is not a byte string: %w", i, err) + } + if !utf8.Valid(b) { + return nil, fmt.Errorf("element %d is not a UTF-8 string", i) + } + res[i] = string(b) + } + return res, nil +} + // ArrayOfUint160 checks the result for correct state (HALT) and then extracts a // slice of util.Uint160 from the returned stack item. func ArrayOfUint160(r *result.Invoke, err error) ([]util.Uint160, error) { @@ -236,6 +293,28 @@ func ArrayOfUint160(r *result.Invoke, err error) ([]util.Uint160, error) { return res, nil } +// ArrayOfUint256 checks the result for correct state (HALT) and then extracts a +// slice of util.Uint256 from the returned stack item. +func ArrayOfUint256(r *result.Invoke, err error) ([]util.Uint256, error) { + a, err := Array(r, err) + if err != nil { + return nil, err + } + res := make([]util.Uint256, len(a)) + for i := range a { + b, err := a[i].TryBytes() + if err != nil { + return nil, fmt.Errorf("element %d is not a byte string: %w", i, err) + } + u, err := util.Uint256DecodeBytesBE(b) + if err != nil { + return nil, fmt.Errorf("element %d is not a uint256: %w", i, err) + } + res[i] = u + } + return res, nil +} + // ArrayOfPublicKeys checks the result for correct state (HALT) and then // extracts a slice of public keys from the returned stack item. func ArrayOfPublicKeys(r *result.Invoke, err error) (keys.PublicKeys, error) { diff --git a/pkg/rpcclient/unwrap/unwrap_test.go b/pkg/rpcclient/unwrap/unwrap_test.go index 5544216ccf..b26876a58f 100644 --- a/pkg/rpcclient/unwrap/unwrap_test.go +++ b/pkg/rpcclient/unwrap/unwrap_test.go @@ -53,9 +53,24 @@ func TestStdErrors(t *testing.T) { func(r *result.Invoke, err error) (interface{}, error) { return Array(r, err) }, + func(r *result.Invoke, err error) (interface{}, error) { + return ArrayOfBools(r, err) + }, + func(r *result.Invoke, err error) (interface{}, error) { + return ArrayOfBigInts(r, err) + }, func(r *result.Invoke, err error) (interface{}, error) { return ArrayOfBytes(r, err) }, + func(r *result.Invoke, err error) (interface{}, error) { + return ArrayOfUTF8Strings(r, err) + }, + func(r *result.Invoke, err error) (interface{}, error) { + return ArrayOfUint160(r, err) + }, + func(r *result.Invoke, err error) (interface{}, error) { + return ArrayOfUint256(r, err) + }, func(r *result.Invoke, err error) (interface{}, error) { return ArrayOfPublicKeys(r, err) }, @@ -233,6 +248,32 @@ func TestArray(t *testing.T) { require.Equal(t, stackitem.Make(42), a[0]) } +func TestArrayOfBools(t *testing.T) { + _, err := ArrayOfBools(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) + require.Error(t, err) + + _, err = ArrayOfBools(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make("reallybigstringthatcantbeanumberandthuscantbeconvertedtobool")})}}, nil) + require.Error(t, err) + + a, err := ArrayOfBools(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make(true)})}}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(a)) + require.Equal(t, true, a[0]) +} + +func TestArrayOfBigInts(t *testing.T) { + _, err := ArrayOfBigInts(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) + require.Error(t, err) + + _, err = ArrayOfBigInts(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make([]stackitem.Item{})})}}, nil) + require.Error(t, err) + + a, err := ArrayOfBigInts(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make(42)})}}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(a)) + require.Equal(t, big.NewInt(42), a[0]) +} + func TestArrayOfBytes(t *testing.T) { _, err := ArrayOfBytes(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) require.Error(t, err) @@ -246,6 +287,22 @@ func TestArrayOfBytes(t *testing.T) { require.Equal(t, []byte("some"), a[0]) } +func TestArrayOfUTF8Strings(t *testing.T) { + _, err := ArrayOfUTF8Strings(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) + require.Error(t, err) + + _, err = ArrayOfUTF8Strings(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make([]stackitem.Item{})})}}, nil) + require.Error(t, err) + + _, err = ArrayOfUTF8Strings(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make([]byte{0, 0xff})})}}, nil) + require.Error(t, err) + + a, err := ArrayOfUTF8Strings(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make("some")})}}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(a)) + require.Equal(t, "some", a[0]) +} + func TestArrayOfUint160(t *testing.T) { _, err := ArrayOfUint160(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) require.Error(t, err) @@ -263,6 +320,23 @@ func TestArrayOfUint160(t *testing.T) { require.Equal(t, u160, uints[0]) } +func TestArrayOfUint256(t *testing.T) { + _, err := ArrayOfUint256(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) + require.Error(t, err) + + _, err = ArrayOfUint256(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make([]stackitem.Item{})})}}, nil) + require.Error(t, err) + + _, err = ArrayOfUint256(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make([]byte("some"))})}}, nil) + require.Error(t, err) + + u256 := util.Uint256{1, 2, 3} + uints, err := ArrayOfUint256(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make(u256.BytesBE())})}}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(uints)) + require.Equal(t, u256, uints[0]) +} + func TestArrayOfPublicKeys(t *testing.T) { _, err := ArrayOfPublicKeys(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) require.Error(t, err) diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go index be121c828b..6c330cab30 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" } }