smartcontract: add user-facing testinvokefunction command

With a very special syntax.
This commit is contained in:
Roman Khimov 2019-11-27 12:52:15 +03:00
parent 05f3329ec0
commit e63b25d5ad
3 changed files with 670 additions and 0 deletions

View file

@ -14,6 +14,7 @@ import (
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/rpc"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm"
"github.com/CityOfZion/neo-go/pkg/vm/compiler"
@ -27,6 +28,7 @@ var (
errNoInput = errors.New("no input file was found, specify an input file with the '--in or -i' flag")
errNoConfFile = errors.New("no config file was found, specify a config file with the '--config' or '-c' flag")
errNoWIF = errors.New("no WIF parameter found, specify it with the '--wif or -w' flag")
errNoScriptHash = errors.New("no smart contract hash was provided, specify one as the first argument")
errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag")
errFileExist = errors.New("A file with given smart-contract name already exists")
)
@ -95,6 +97,80 @@ func NewCommands() []cli.Command {
},
},
},
{
Name: "testinvokefunction",
Usage: "invoke deployed contract on the blockchain (test mode)",
UsageText: "neo-go contract testinvokefunction -e endpoint scripthash [method] [arguments...]",
Description: `Executes given (as a script hash) deployed script with the given method and
arguments. If no method is given "" is passed to the script, if no arguments
are given, an empty array is passed. All of the given arguments are
encapsulated into array before invoking the script. The script thus should
follow the regular convention of smart contract arguments (method string and
an array of other arguments).
Arguments always do have regular Neo smart contract parameter types, either
specified explicitly or being inferred from the value. To specify the type
manually use "type:value" syntax where the type is one of the following:
'signature', 'bool', 'int', 'hash160', 'hash256', 'bytes', 'key' or 'string'.
Array types are not currently supported.
Given values are type-checked against given types with the following
restrictions applied:
* 'signature' type values should be hex-encoded and have a (decoded)
length of 64 bytes.
* 'bool' type values are 'true' and 'false'.
* 'int' values are decimal integers that can be successfully converted
from the string.
* 'hash160' values are Neo addresses and hex-encoded 20-bytes long (after
decoding) strings.
* 'hash256' type values should be hex-encoded and have a (decoded)
length of 32 bytes.
* 'bytes' type values are any hex-encoded things.
* 'key' type values are hex-encoded marshalled public keys.
* 'string' type values are any valid UTF-8 strings. In the value's part of
the string the colon looses it's special meaning as a separator between
type and value and is taken literally.
If no type is explicitly specified, it is inferred from the value using the
following logic:
- anything that can be interpreted as a decimal integer gets
an 'int' type
- 'true' and 'false' strings get 'bool' type
- valid Neo addresses and 20 bytes long hex-encoded strings get 'hash160'
type
- valid hex-encoded public keys get 'key' type
- 32 bytes long hex-encoded values get 'hash256' type
- 64 bytes long hex-encoded values get 'signature' type
- any other valid hex-encoded values get 'bytes' type
- anything else is a 'string'
Backslash character is used as an escape character and allows to use colon in
an implicitly typed string. For any other characters it has no special
meaning, to get a literal backslash in the string use the '\\' sequence.
Examples:
* 'int:42' is an integer with a value of 42
* '42' is an integer with a value of 42
* 'bad' is a string with a value of 'bad'
* 'dead' is a byte array with a value of 'dead'
* 'string:dead' is a string with a value of 'dead'
* 'AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y' is a hash160 with a value
of '23ba2703c53263e8d6e522dc32203339dcd8eee9'
* '\4\2' is an integer with a value of 42
* '\\4\2' is a string with a value of '\42'
* 'string:string' is a string with a value of 'string'
* 'string\:string' is a string with a value of 'string:string'
* '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a
key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c'
`,
Action: testInvokeFunction,
Flags: []cli.Flag{
cli.StringFlag{
Name: "endpoint, e",
Usage: "RPC endpoint address (like 'http://seed4.ngd.network:20332')",
},
},
},
{
Name: "testinvokescript",
Usage: "Invoke compiled AVM code on the blockchain (test mode, not creating a transaction for it)",
@ -211,6 +287,52 @@ func contractCompile(ctx *cli.Context) error {
return nil
}
func testInvokeFunction(ctx *cli.Context) error {
endpoint := ctx.String("endpoint")
if len(endpoint) == 0 {
return cli.NewExitError(errNoEndpoint, 1)
}
args := ctx.Args()
if !args.Present() {
return cli.NewExitError(errNoScriptHash, 1)
}
script := args[0]
operation := ""
if len(args) > 1 {
operation = args[1]
}
params := make([]smartcontract.Parameter, 0)
if len(args) > 2 {
for k, s := range args[2:] {
param, err := smartcontract.NewParameterFromString(s)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to parse argument #%d: %v", k+2+1, err), 1)
}
params = append(params, *param)
}
}
client, err := rpc.NewClient(context.TODO(), endpoint, rpc.ClientOptions{})
if err != nil {
return cli.NewExitError(err, 1)
}
resp, err := client.InvokeFunction(script, operation, params)
if err != nil {
return cli.NewExitError(err, 1)
}
b, err := json.MarshalIndent(resp.Result, "", " ")
if err != nil {
return cli.NewExitError(err, 1)
}
fmt.Println(string(b))
return nil
}
func testInvokeScript(ctx *cli.Context) error {
src := ctx.String("in")
if len(src) == 0 {

View file

@ -1,6 +1,14 @@
package smartcontract
import (
"encoding/hex"
"errors"
"strconv"
"strings"
"unicode/utf8"
"github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/util"
)
@ -89,6 +97,203 @@ func NewParameter(t ParamType) Parameter {
}
}
// parseParamType is a user-friendly string to ParamType converter, it's
// case-insensitive and makes the following conversions:
// signature -> SignatureType
// bool -> BoolType
// int -> IntegerType
// hash160 -> Hash160Type
// hash256 -> Hash256Type
// bytes -> ByteArrayType
// key -> PublicKeyType
// string -> StringType
// anything else generates an error.
func parseParamType(typ string) (ParamType, error) {
switch strings.ToLower(typ) {
case "signature":
return SignatureType, nil
case "bool":
return BoolType, nil
case "int":
return IntegerType, nil
case "hash160":
return Hash160Type, nil
case "hash256":
return Hash256Type, nil
case "bytes":
return ByteArrayType, nil
case "key":
return PublicKeyType, nil
case "string":
return StringType, nil
default:
// We deliberately don't support array here.
return 0, errors.New("wrong or unsupported parameter type")
}
}
// adjustValToType is a value type-checker and converter.
func adjustValToType(typ ParamType, val string) (interface{}, error) {
switch typ {
case SignatureType:
b, err := hex.DecodeString(val)
if err != nil {
return nil, err
}
if len(b) != 64 {
return nil, errors.New("not a signature")
}
return val, nil
case BoolType:
switch val {
case "true":
return true, nil
case "false":
return false, nil
default:
return nil, errors.New("invalid boolean value")
}
case IntegerType:
return strconv.Atoi(val)
case Hash160Type:
u, err := crypto.Uint160DecodeAddress(val)
if err == nil {
return hex.EncodeToString(u.Bytes()), nil
}
b, err := hex.DecodeString(val)
if err != nil {
return nil, err
}
if len(b) != 20 {
return nil, errors.New("not a hash160")
}
return val, nil
case Hash256Type:
b, err := hex.DecodeString(val)
if err != nil {
return nil, err
}
if len(b) != 32 {
return nil, errors.New("not a hash256")
}
return val, nil
case ByteArrayType:
_, err := hex.DecodeString(val)
if err != nil {
return nil, err
}
return val, nil
case PublicKeyType:
_, err := keys.NewPublicKeyFromString(val)
if err != nil {
return nil, err
}
return val, nil
case StringType:
return val, nil
default:
return nil, errors.New("unsupported parameter type")
}
}
// inferParamType tries to infer the value type from its contents. It returns
// IntegerType for anything that looks like decimal integer (can be converted
// with strconv.Atoi), BoolType for true and false values, Hash160Type for
// addresses and hex strings encoding 20 bytes long values, PublicKeyType for
// valid hex-encoded public keys, Hash256Type for hex-encoded 32 bytes values,
// SignatureType for hex-encoded 64 bytes values, ByteArrayType for any other
// valid hex-encoded values and StringType for anything else.
func inferParamType(val string) ParamType {
var err error
_, err = strconv.Atoi(val)
if err == nil {
return IntegerType
}
if val == "true" || val == "false" {
return BoolType
}
_, err = crypto.Uint160DecodeAddress(val)
if err == nil {
return Hash160Type
}
_, err = keys.NewPublicKeyFromString(val)
if err == nil {
return PublicKeyType
}
unhexed, err := hex.DecodeString(val)
if err == nil {
switch len(unhexed) {
case 20:
return Hash160Type
case 32:
return Hash256Type
case 64:
return SignatureType
default:
return ByteArrayType
}
}
// Anything can be a string.
return StringType
}
// NewParameterFromString returns a new Parameter initialized from the given
// string in neo-go-specific format. It is intended to be used in user-facing
// interfaces and has some heuristics in it to simplify parameter passing. Exact
// syntax is documented in the cli documentation.
func NewParameterFromString(in string) (*Parameter, error) {
var (
char rune
val string
err error
r *strings.Reader
buf strings.Builder
escaped bool
hadType bool
res = &Parameter{}
)
r = strings.NewReader(in)
for char, _, err = r.ReadRune(); err == nil && char != utf8.RuneError; char, _, err = r.ReadRune() {
if char == '\\' && !escaped {
escaped = true
continue
}
if char == ':' && !escaped && !hadType {
typStr := buf.String()
res.Type, err = parseParamType(typStr)
if err != nil {
return nil, err
}
buf.Reset()
hadType = true
continue
}
escaped = false
// We don't care about length and it never fails.
_, _ = buf.WriteRune(char)
}
if char == utf8.RuneError {
return nil, errors.New("bad UTF-8 string")
}
// The only other error `ReadRune` returns is io.EOF, which is fine and
// expected, so we don't check err here.
val = buf.String()
if !hadType {
res.Type = inferParamType(val)
}
res.Value, err = adjustValToType(res.Type, val)
if err != nil {
return nil, err
}
return res, nil
}
// ContextItem represents a transaction context item.
type ContextItem struct {
Script util.Uint160

View file

@ -0,0 +1,343 @@
package smartcontract
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseParamType(t *testing.T) {
var inouts = []struct {
in string
out ParamType
err bool
}{{
in: "signature",
out: SignatureType,
}, {
in: "Signature",
out: SignatureType,
}, {
in: "SiGnAtUrE",
out: SignatureType,
}, {
in: "bool",
out: BoolType,
}, {
in: "int",
out: IntegerType,
}, {
in: "hash160",
out: Hash160Type,
}, {
in: "hash256",
out: Hash256Type,
}, {
in: "bytes",
out: ByteArrayType,
}, {
in: "key",
out: PublicKeyType,
}, {
in: "string",
out: StringType,
}, {
in: "array",
err: true,
}, {
in: "qwerty",
err: true,
}}
for _, inout := range inouts {
out, err := parseParamType(inout.in)
if inout.err {
assert.NotNil(t, err, "should error on '%s' input", inout.in)
} else {
assert.Nil(t, err, "shouldn't error on '%s' input", inout.in)
assert.Equal(t, inout.out, out, "bad output for '%s' input", inout.in)
}
}
}
func TestInferParamType(t *testing.T) {
var inouts = []struct {
in string
out ParamType
}{{
in: "42",
out: IntegerType,
}, {
in: "-42",
out: IntegerType,
}, {
in: "0",
out: IntegerType,
}, {
in: "2e10",
out: ByteArrayType,
}, {
in: "true",
out: BoolType,
}, {
in: "false",
out: BoolType,
}, {
in: "truee",
out: StringType,
}, {
in: "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y",
out: Hash160Type,
}, {
in: "ZK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y",
out: StringType,
}, {
in: "50befd26fdf6e4d957c11e078b24ebce6291456f",
out: Hash160Type,
}, {
in: "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
out: PublicKeyType,
}, {
in: "30b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
out: ByteArrayType,
}, {
in: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7",
out: Hash256Type,
}, {
in: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7da",
out: ByteArrayType,
}, {
in: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b",
out: SignatureType,
}, {
in: "qwerty",
out: StringType,
}, {
in: "ab",
out: ByteArrayType,
}, {
in: "az",
out: StringType,
}, {
in: "bad",
out: StringType,
}, {
in: "фыва",
out: StringType,
}, {
in: "dead",
out: ByteArrayType,
}}
for _, inout := range inouts {
out := inferParamType(inout.in)
assert.Equal(t, inout.out, out, "bad output for '%s' input", inout.in)
}
}
func TestAdjustValToType(t *testing.T) {
var inouts = []struct {
typ ParamType
val string
out interface{}
err bool
}{{
typ: SignatureType,
val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b",
out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b",
}, {
typ: SignatureType,
val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c",
err: true,
}, {
typ: SignatureType,
val: "qwerty",
err: true,
}, {
typ: BoolType,
val: "false",
out: false,
}, {
typ: BoolType,
val: "true",
out: true,
}, {
typ: BoolType,
val: "qwerty",
err: true,
}, {
typ: BoolType,
val: "42",
err: true,
}, {
typ: BoolType,
val: "0",
err: true,
}, {
typ: IntegerType,
val: "0",
out: 0,
}, {
typ: IntegerType,
val: "42",
out: 42,
}, {
typ: IntegerType,
val: "-42",
out: -42,
}, {
typ: IntegerType,
val: "q",
err: true,
}, {
typ: Hash160Type,
val: "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y",
out: "23ba2703c53263e8d6e522dc32203339dcd8eee9",
}, {
typ: Hash160Type,
val: "50befd26fdf6e4d957c11e078b24ebce6291456f",
out: "50befd26fdf6e4d957c11e078b24ebce6291456f",
}, {
typ: Hash160Type,
val: "befd26fdf6e4d957c11e078b24ebce6291456f",
err: true,
}, {
typ: Hash160Type,
val: "q",
err: true,
}, {
typ: Hash256Type,
val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7",
out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7",
}, {
typ: Hash256Type,
val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282d",
err: true,
}, {
typ: Hash256Type,
val: "q",
err: true,
}, {
typ: ByteArrayType,
val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282d",
out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282d",
}, {
typ: ByteArrayType,
val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7",
out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7",
}, {
typ: ByteArrayType,
val: "50befd26fdf6e4d957c11e078b24ebce6291456f",
out: "50befd26fdf6e4d957c11e078b24ebce6291456f",
}, {
typ: ByteArrayType,
val: "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y",
err: true,
}, {
typ: ByteArrayType,
val: "q",
err: true,
}, {
typ: ByteArrayType,
val: "ab",
out: "ab",
}, {
typ: PublicKeyType,
val: "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
out: "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
}, {
typ: PublicKeyType,
val: "01b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
err: true,
}, {
typ: PublicKeyType,
val: "q",
err: true,
}, {
typ: StringType,
val: "q",
out: "q",
}, {
typ: StringType,
val: "dead",
out: "dead",
}, {
typ: StringType,
val: "йцукен",
out: "йцукен",
}, {
typ: ArrayType,
val: "",
err: true,
}}
for _, inout := range inouts {
out, err := adjustValToType(inout.typ, inout.val)
if inout.err {
assert.NotNil(t, err, "should error on '%s/%s' input", inout.typ, inout.val)
} else {
assert.Nil(t, err, "shouldn't error on '%s/%s' input", inout.typ, inout.val)
assert.Equal(t, inout.out, out, "bad output for '%s/%s' input", inout.typ, inout.val)
}
}
}
func TestNewParameterFromString(t *testing.T) {
var inouts = []struct {
in string
out Parameter
err bool
}{{
in: "qwerty",
out: Parameter{StringType, "qwerty"},
}, {
in: "42",
out: Parameter{IntegerType, 42},
}, {
in: "Hello, 世界",
out: Parameter{StringType, "Hello, 世界"},
}, {
in: `\4\2`,
out: Parameter{IntegerType, 42},
}, {
in: `\\4\2`,
out: Parameter{StringType, `\42`},
}, {
in: `\\\4\2`,
out: Parameter{StringType, `\42`},
}, {
in: "int:42",
out: Parameter{IntegerType, 42},
}, {
in: "true",
out: Parameter{BoolType, true},
}, {
in: "string:true",
out: Parameter{StringType, "true"},
}, {
in: "\xfe\xff",
err: true,
}, {
in: `string\:true`,
out: Parameter{StringType, "string:true"},
}, {
in: "string:true:true",
out: Parameter{StringType, "true:true"},
}, {
in: `string\\:true`,
err: true,
}, {
in: `qwerty:asdf`,
err: true,
}, {
in: `bool:asdf`,
err: true,
}}
for _, inout := range inouts {
out, err := NewParameterFromString(inout.in)
if inout.err {
assert.NotNil(t, err, "should error on '%s' input", inout.in)
} else {
assert.Nil(t, err, "shouldn't error on '%s' input", inout.in)
assert.Equal(t, inout.out, *out, "bad output for '%s' input", inout.in)
}
}
}