cli: unify parameters parsing
Share parameters parsing code between 'contract invokefunction' and 'vm run' commands. It allows VM CLI to parse more complicated parameter types including arrays and file-backed bytestrings.
This commit is contained in:
parent
3fba4e4f17
commit
7eb87afab8
8 changed files with 167 additions and 147 deletions
|
@ -24,6 +24,76 @@ const (
|
|||
ArrayEndSeparator = "]"
|
||||
)
|
||||
|
||||
const (
|
||||
// ParamsParsingDoc is a documentation for parameters parsing.
|
||||
ParamsParsingDoc = ` 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 also supported: use special space-separated '[' and ']'
|
||||
symbols around array values to denote array bounds. Nested arrays are also
|
||||
supported.
|
||||
|
||||
There is ability to provide an argument of 'bytearray' type via file. Use a
|
||||
special 'filebytes' argument type for this with a filepath specified after
|
||||
the colon, e.g. 'filebytes:my_file.txt'.
|
||||
|
||||
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.
|
||||
* 'filebytes' type values are filenames with the argument value inside.
|
||||
* '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'
|
||||
* 'filebytes:my_data.txt' is bytes decoded from a content of my_data.txt
|
||||
* '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'
|
||||
* '[ a b c ]' is an array with strings values 'a', 'b' and 'c'
|
||||
* '[ a b [ c d ] e ]' is an array with 4 values: string 'a', string 'b',
|
||||
array of two strings 'c' and 'd', string 'e'
|
||||
* '[ ]' is an empty array`
|
||||
)
|
||||
|
||||
// GetSignersFromContext returns signers parsed from context args starting
|
||||
// from the specified offset.
|
||||
func GetSignersFromContext(ctx *cli.Context, offset int) ([]transaction.Signer, *cli.ExitError) {
|
||||
|
|
|
@ -209,72 +209,7 @@ func NewCommands() []cli.Command {
|
|||
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 also supported: use special space-separated '[' and ']'
|
||||
symbols around array values to denote array bounds. Nested arrays are also
|
||||
supported.
|
||||
|
||||
There is ability to provide an argument of 'bytearray' type via file. Use a
|
||||
special 'filebytes' argument type for this with a filepath specified after
|
||||
the colon, e.g. 'filebytes:my_file.txt'.
|
||||
|
||||
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.
|
||||
* 'filebytes' type values are filenames with the argument value inside.
|
||||
* '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'
|
||||
* 'filebytes:my_data.txt' is bytes decoded from a content of my_data.txt
|
||||
* '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'
|
||||
* '[ a b c ]' is an array with strings values 'a', 'b' and 'c'
|
||||
* '[ a b [ c d ] e ]' is an array with 4 values: string 'a', string 'b',
|
||||
array of two strings 'c' and 'd', string 'e'
|
||||
* '[ ]' is an empty array
|
||||
` + cmdargs.ParamsParsingDoc + `
|
||||
|
||||
Signers represent a set of Uint160 hashes with witness scopes and are used
|
||||
to verify hashes in System.Runtime.CheckWitness syscall. First signer is treated
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/nspcc-dev/neo-go/cli/cmdargs"
|
||||
"github.com/nspcc-dev/neo-go/cli/flags"
|
||||
"github.com/nspcc-dev/neo-go/cli/options"
|
||||
"github.com/nspcc-dev/neo-go/cli/paramcontext"
|
||||
|
@ -52,11 +53,6 @@ const (
|
|||
exitFuncKey = "exitFunc"
|
||||
readlineInstanceKey = "readlineKey"
|
||||
printLogoKey = "printLogoKey"
|
||||
boolType = "bool"
|
||||
boolFalse = "false"
|
||||
boolTrue = "true"
|
||||
intType = "int"
|
||||
stringType = "string"
|
||||
)
|
||||
|
||||
// Various flag names.
|
||||
|
@ -221,18 +217,12 @@ and converted to other formats. Strings are escaped and output in quotes.`,
|
|||
<method> is a contract method, specified in manifest. It can be '_' which will push
|
||||
parameters onto the stack and execute from the current offset.
|
||||
<parameter> is a parameter (can be repeated multiple times) that can be specified
|
||||
as <type>:<value>, where type can be:
|
||||
'` + boolType + `': supports '` + boolFalse + `' and '` + boolTrue + `' values
|
||||
'` + intType + `': supports integers as values
|
||||
'` + stringType + `': supports strings as values (that are pushed as a byte array
|
||||
values to the stack)
|
||||
or can be just <value>, for which the type will be detected automatically
|
||||
following these rules: '` + boolTrue + `' and '` + boolFalse + `' are treated as respective
|
||||
boolean values, everything that can be converted to integer is treated as
|
||||
integer and everything else is treated like a string.
|
||||
using the same rules as for 'contract testinvokefunction' command:
|
||||
|
||||
` + cmdargs.ParamsParsingDoc + `
|
||||
|
||||
Example:
|
||||
> run put ` + stringType + `:"Something to put"`,
|
||||
> run put string:"Something to put"`,
|
||||
Action: handleRun,
|
||||
},
|
||||
{
|
||||
|
@ -862,9 +852,16 @@ func handleRun(c *cli.Context) error {
|
|||
runCurrent = args[0] != "_"
|
||||
)
|
||||
|
||||
params, err = parseArgs(args[1:])
|
||||
_, scParams, err := cmdargs.ParseParams(args[1:], true)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w: %v", ErrInvalidParameter, err)
|
||||
}
|
||||
params = make([]stackitem.Item, len(scParams))
|
||||
for i := range scParams {
|
||||
params[i], err = scParams[i].ToStackItem()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert parameter #%d to stackitem: %w", i, err)
|
||||
}
|
||||
}
|
||||
if runCurrent {
|
||||
if m == nil {
|
||||
|
@ -1265,48 +1262,6 @@ func Parse(args []string) (string, error) {
|
|||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func parseArgs(args []string) ([]stackitem.Item, error) {
|
||||
items := make([]stackitem.Item, len(args))
|
||||
for i, arg := range args {
|
||||
var typ, value string
|
||||
typeAndVal := strings.Split(arg, ":")
|
||||
if len(typeAndVal) < 2 {
|
||||
if typeAndVal[0] == boolFalse || typeAndVal[0] == boolTrue {
|
||||
typ = boolType
|
||||
} else if _, err := strconv.Atoi(typeAndVal[0]); err == nil {
|
||||
typ = intType
|
||||
} else {
|
||||
typ = stringType
|
||||
}
|
||||
value = typeAndVal[0]
|
||||
} else {
|
||||
typ = typeAndVal[0]
|
||||
value = typeAndVal[1]
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case boolType:
|
||||
if value == boolFalse {
|
||||
items[i] = stackitem.NewBool(false)
|
||||
} else if value == boolTrue {
|
||||
items[i] = stackitem.NewBool(true)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%w: invalid bool value", ErrInvalidParameter)
|
||||
}
|
||||
case intType:
|
||||
val, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid integer value", ErrInvalidParameter)
|
||||
}
|
||||
items[i] = stackitem.NewBigInteger(big.NewInt(val))
|
||||
case stringType:
|
||||
items[i] = stackitem.NewByteArray([]byte(value))
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const logo = `
|
||||
_ ____________ __________ _ ____ ___
|
||||
/ | / / ____/ __ \ / ____/ __ \ | | / / |/ /
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
gio "io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -428,6 +429,9 @@ func TestRunWithDifferentArguments(t *testing.T) {
|
|||
}
|
||||
func GetString(arg string) string {
|
||||
return arg
|
||||
}
|
||||
func GetArr(arg []interface{}) []interface{}{
|
||||
return arg
|
||||
}`
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
@ -449,6 +453,7 @@ func TestRunWithDifferentArguments(t *testing.T) {
|
|||
"run _ 1 2",
|
||||
"loadbase64 "+base64.StdEncoding.EncodeToString([]byte{byte(opcode.MUL)}),
|
||||
"run _ 21 2",
|
||||
"loadgo "+filename, "run getArr [ 1 2 3 ]",
|
||||
)
|
||||
|
||||
e.checkNextLine(t, "READY: loaded \\d.* instructions")
|
||||
|
@ -480,6 +485,13 @@ func TestRunWithDifferentArguments(t *testing.T) {
|
|||
|
||||
e.checkNextLine(t, "READY: loaded \\d.* instructions")
|
||||
e.checkStack(t, 42)
|
||||
|
||||
e.checkNextLine(t, "READY: loaded \\d.* instructions")
|
||||
e.checkStack(t, []stackitem.Item{
|
||||
stackitem.NewBigInteger(big.NewInt(1)),
|
||||
stackitem.NewBigInteger(big.NewInt(2)),
|
||||
stackitem.NewBigInteger(big.NewInt(3)),
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrintOps(t *testing.T) {
|
||||
|
|
|
@ -403,3 +403,12 @@ func ExpandParameterToEmitable(param Parameter) (interface{}, error) {
|
|||
return param.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ToStackItem converts smartcontract parameter to stackitem.Item.
|
||||
func (p *Parameter) ToStackItem() (stackitem.Item, error) {
|
||||
e, err := ExpandParameterToEmitable(*p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stackitem.Make(e), nil
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
|
@ -446,47 +448,57 @@ func hexToBase64(s string) string {
|
|||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func TestExpandParameterToEmitable(t *testing.T) {
|
||||
func TestExpandParameterToEmitableToStackitem(t *testing.T) {
|
||||
pk, _ := keys.NewPrivateKey()
|
||||
testCases := []struct {
|
||||
In Parameter
|
||||
Expected interface{}
|
||||
In Parameter
|
||||
Expected interface{}
|
||||
ExpectedStackitem stackitem.Item
|
||||
}{
|
||||
{
|
||||
In: Parameter{Type: BoolType, Value: true},
|
||||
Expected: true,
|
||||
In: Parameter{Type: BoolType, Value: true},
|
||||
Expected: true,
|
||||
ExpectedStackitem: stackitem.NewBool(true),
|
||||
},
|
||||
{
|
||||
In: Parameter{Type: IntegerType, Value: big.NewInt(123)},
|
||||
Expected: big.NewInt(123),
|
||||
In: Parameter{Type: IntegerType, Value: big.NewInt(123)},
|
||||
Expected: big.NewInt(123),
|
||||
ExpectedStackitem: stackitem.NewBigInteger(big.NewInt(123)),
|
||||
},
|
||||
{
|
||||
In: Parameter{Type: ByteArrayType, Value: []byte{1, 2, 3}},
|
||||
Expected: []byte{1, 2, 3},
|
||||
In: Parameter{Type: ByteArrayType, Value: []byte{1, 2, 3}},
|
||||
Expected: []byte{1, 2, 3},
|
||||
ExpectedStackitem: stackitem.NewByteArray([]byte{1, 2, 3}),
|
||||
},
|
||||
{
|
||||
In: Parameter{Type: StringType, Value: "writing's on the wall"},
|
||||
Expected: "writing's on the wall",
|
||||
In: Parameter{Type: StringType, Value: "writing's on the wall"},
|
||||
Expected: "writing's on the wall",
|
||||
ExpectedStackitem: stackitem.NewByteArray([]byte("writing's on the wall")),
|
||||
},
|
||||
{
|
||||
In: Parameter{Type: Hash160Type, Value: util.Uint160{1, 2, 3}},
|
||||
Expected: util.Uint160{1, 2, 3},
|
||||
In: Parameter{Type: Hash160Type, Value: util.Uint160{1, 2, 3}},
|
||||
Expected: util.Uint160{1, 2, 3},
|
||||
ExpectedStackitem: stackitem.NewByteArray(util.Uint160{1, 2, 3}.BytesBE()),
|
||||
},
|
||||
{
|
||||
In: Parameter{Type: Hash256Type, Value: util.Uint256{1, 2, 3}},
|
||||
Expected: util.Uint256{1, 2, 3},
|
||||
In: Parameter{Type: Hash256Type, Value: util.Uint256{1, 2, 3}},
|
||||
Expected: util.Uint256{1, 2, 3},
|
||||
ExpectedStackitem: stackitem.NewByteArray(util.Uint256{1, 2, 3}.BytesBE()),
|
||||
},
|
||||
{
|
||||
In: Parameter{Type: PublicKeyType, Value: pk.PublicKey().Bytes()},
|
||||
Expected: pk.PublicKey().Bytes(),
|
||||
In: Parameter{Type: PublicKeyType, Value: pk.PublicKey().Bytes()},
|
||||
Expected: pk.PublicKey().Bytes(),
|
||||
ExpectedStackitem: stackitem.NewByteArray(pk.PublicKey().Bytes()),
|
||||
},
|
||||
{
|
||||
In: Parameter{Type: SignatureType, Value: []byte{1, 2, 3}},
|
||||
Expected: []byte{1, 2, 3},
|
||||
In: Parameter{Type: SignatureType, Value: []byte{1, 2, 3}},
|
||||
Expected: []byte{1, 2, 3},
|
||||
ExpectedStackitem: stackitem.NewByteArray([]byte{1, 2, 3}),
|
||||
},
|
||||
{
|
||||
In: Parameter{Type: AnyType},
|
||||
Expected: nil,
|
||||
In: Parameter{Type: AnyType},
|
||||
Expected: nil,
|
||||
ExpectedStackitem: stackitem.Null{},
|
||||
},
|
||||
{
|
||||
In: Parameter{Type: ArrayType, Value: []Parameter{
|
||||
|
@ -509,6 +521,13 @@ func TestExpandParameterToEmitable(t *testing.T) {
|
|||
},
|
||||
}},
|
||||
Expected: []interface{}{big.NewInt(123), []byte{1, 2, 3}, []interface{}{true}},
|
||||
ExpectedStackitem: stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewBigInteger(big.NewInt(123)),
|
||||
stackitem.NewByteArray([]byte{1, 2, 3}),
|
||||
stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewBool(true),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}
|
||||
bw := io.NewBufBinWriter()
|
||||
|
@ -519,6 +538,10 @@ func TestExpandParameterToEmitable(t *testing.T) {
|
|||
|
||||
emit.Array(bw.BinWriter, actual)
|
||||
require.NoError(t, bw.Err)
|
||||
|
||||
actualSI, err := testCase.In.ToStackItem()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testCase.ExpectedStackitem, actualSI)
|
||||
}
|
||||
errCases := []Parameter{
|
||||
{Type: UnknownType},
|
||||
|
|
|
@ -122,6 +122,18 @@ func Make(v interface{}) Item {
|
|||
a = append(a, Make(i))
|
||||
}
|
||||
return Make(a)
|
||||
case []interface{}:
|
||||
res := make([]Item, len(val))
|
||||
for i := range val {
|
||||
res[i] = Make(val[i])
|
||||
}
|
||||
return Make(res)
|
||||
case util.Uint160:
|
||||
return Make(val.BytesBE())
|
||||
case util.Uint256:
|
||||
return Make(val.BytesBE())
|
||||
case nil:
|
||||
return Null{}
|
||||
default:
|
||||
i64T := reflect.TypeOf(int64(0))
|
||||
if reflect.TypeOf(val).ConvertibleTo(i64T) {
|
||||
|
|
|
@ -77,13 +77,17 @@ var makeStackItemTestCases = []struct {
|
|||
input: []int{1, 2, 3},
|
||||
result: &Array{value: []Item{(*BigInteger)(big.NewInt(1)), (*BigInteger)(big.NewInt(2)), (*BigInteger)(big.NewInt(3))}},
|
||||
},
|
||||
{
|
||||
input: nil,
|
||||
result: Null{},
|
||||
},
|
||||
}
|
||||
|
||||
var makeStackItemErrorCases = []struct {
|
||||
input interface{}
|
||||
}{
|
||||
{
|
||||
input: nil,
|
||||
input: map[int]int{1: 2},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue