diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go index 9993466e3..c1829a877 100644 --- a/cli/cmdargs/parser.go +++ b/cli/cmdargs/parser.go @@ -1,14 +1,25 @@ package cmdargs import ( + "errors" "fmt" "strings" "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/urfave/cli" ) +const ( + // CosignersSeparator marks the start of cosigners cli args. + CosignersSeparator = "--" + // ArrayStartSeparator marks the start of array cli arg. + ArrayStartSeparator = "[" + // ArrayEndSeparator marks the end of array cli arg. + ArrayEndSeparator = "]" +) + // GetSignersFromContext returns signers parsed from context args starting // from the specified offset. func GetSignersFromContext(ctx *cli.Context, offset int) ([]transaction.Signer, *cli.ExitError) { @@ -47,3 +58,73 @@ func parseCosigner(c string) (transaction.Signer, error) { } return res, nil } + +// GetDataFromContext returns data parameter from context args. +func GetDataFromContext(ctx *cli.Context) (int, interface{}, *cli.ExitError) { + var ( + data interface{} + offset int + params []smartcontract.Parameter + err error + ) + args := ctx.Args() + if args.Present() { + offset, params, err = ParseParams(args, true) + if err != nil { + return offset, nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1) + } + if len(params) != 1 { + return offset, nil, cli.NewExitError("'data' should be represented as a single parameter", 1) + } + data, err = smartcontract.ExpandParameterToEmitable(params[0]) + if err != nil { + return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1) + } + } + return offset, data, nil +} + +// ParseParams extracts array of smartcontract.Parameter from the given args and +// returns the number of handled words, the array itself and an error. +// `calledFromMain` denotes whether the method was called from the outside or +// recursively and used to check if CosignersSeparator and ArrayEndSeparator are +// allowed to be in `args` sequence. +func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Parameter, error) { + res := []smartcontract.Parameter{} + for k := 0; k < len(args); { + s := args[k] + switch s { + case CosignersSeparator: + if calledFromMain { + return k + 1, res, nil // `1` to convert index to numWordsRead + } + return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket") + case ArrayStartSeparator: + numWordsRead, array, err := ParseParams(args[k+1:], false) + if err != nil { + return 0, nil, fmt.Errorf("failed to parse array: %w", err) + } + res = append(res, smartcontract.Parameter{ + Type: smartcontract.ArrayType, + Value: array, + }) + k += 1 + numWordsRead // `1` for opening bracket + case ArrayEndSeparator: + if calledFromMain { + return 0, nil, errors.New("invalid array syntax: missing opening bracket") + } + return k + 1, res, nil // `1`to convert index to numWordsRead + default: + param, err := smartcontract.NewParameterFromString(s) + if err != nil { + return 0, nil, fmt.Errorf("failed to parse argument #%d: %w", k+1, err) + } + res = append(res, *param) + k++ + } + } + if calledFromMain { + return len(args), res, nil + } + return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket") +} diff --git a/cli/cmdargs/parser_test.go b/cli/cmdargs/parser_test.go index 31cfc7f6b..7b09aa05d 100644 --- a/cli/cmdargs/parser_test.go +++ b/cli/cmdargs/parser_test.go @@ -1,9 +1,11 @@ package cmdargs import ( + "strings" "testing" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" ) @@ -52,3 +54,309 @@ func TestParseCosigner(t *testing.T) { require.Error(t, err) } } + +func TestParseParams_CalledFromItself(t *testing.T) { + testCases := map[string]struct { + WordsRead int + Value []smartcontract.Parameter + }{ + "]": { + WordsRead: 1, + Value: []smartcontract.Parameter{}, + }, + "[ [ ] ] ]": { + WordsRead: 5, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{}, + }, + }, + }, + }, + }, + "a b c ]": { + WordsRead: 4, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "a", + }, + { + Type: smartcontract.StringType, + Value: "b", + }, + { + Type: smartcontract.StringType, + Value: "c", + }, + }, + }, + "a [ b [ [ c d ] e ] ] f ] extra items": { + WordsRead: 13, // the method should return right after the last bracket, as calledFromMain == false + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "a", + }, + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "b", + }, + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "c", + }, + { + Type: smartcontract.StringType, + Value: "d", + }, + }, + }, + { + Type: smartcontract.StringType, + Value: "e", + }, + }, + }, + }, + }, + { + Type: smartcontract.StringType, + Value: "f", + }, + }, + }, + } + + for str, expected := range testCases { + input := strings.Split(str, " ") + offset, actual, err := ParseParams(input, false) + require.NoError(t, err) + require.Equal(t, expected.WordsRead, offset) + require.Equal(t, expected.Value, actual) + } + + errorCases := []string{ + "[ ]", + "[ a b [ c ] d ]", + "[ ] --", + "--", + "not-int:integer ]", + } + + for _, str := range errorCases { + input := strings.Split(str, " ") + _, _, err := ParseParams(input, false) + require.Error(t, err) + } +} + +func TestParseParams_CalledFromOutside(t *testing.T) { + testCases := map[string]struct { + WordsRead int + Parameters []smartcontract.Parameter + }{ + "-- cosigner1": { + WordsRead: 1, // the `--` only + Parameters: []smartcontract.Parameter{}, + }, + "a b c": { + WordsRead: 3, + Parameters: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "a", + }, + { + Type: smartcontract.StringType, + Value: "b", + }, + { + Type: smartcontract.StringType, + Value: "c", + }, + }, + }, + "a b c -- cosigner1": { + WordsRead: 4, + Parameters: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "a", + }, + { + Type: smartcontract.StringType, + Value: "b", + }, + { + Type: smartcontract.StringType, + Value: "c", + }, + }, + }, + "a [ b [ [ c d ] e ] ] f": { + WordsRead: 12, + Parameters: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "a", + }, + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "b", + }, + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "c", + }, + { + Type: smartcontract.StringType, + Value: "d", + }, + }, + }, + { + Type: smartcontract.StringType, + Value: "e", + }, + }, + }, + }, + }, + { + Type: smartcontract.StringType, + Value: "f", + }, + }, + }, + "a [ b ] -- cosigner1 cosigner2": { + WordsRead: 5, + Parameters: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "a", + }, + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "b", + }, + }, + }, + }, + }, + "a [ b ]": { + WordsRead: 4, + Parameters: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "a", + }, + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "b", + }, + }, + }, + }, + }, + "a [ b ] [ [ c ] ] [ [ [ d ] ] ]": { + WordsRead: 16, + Parameters: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "a", + }, + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "b", + }, + }, + }, + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "c", + }, + }, + }, + }, + }, + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: "d", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + for str, expected := range testCases { + input := strings.Split(str, " ") + offset, arr, err := ParseParams(input, true) + require.NoError(t, err) + require.Equal(t, expected.WordsRead, offset) + require.Equal(t, expected.Parameters, arr) + } + + errorCases := []string{ + "[", + "]", + "[ [ ]", + "[ [ ] --", + "[ -- ]", + } + for _, str := range errorCases { + input := strings.Split(str, " ") + _, _, err := ParseParams(input, true) + require.Error(t, err) + } +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index b935faddb..3fc97f41d 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -87,11 +87,6 @@ func init() { func RuntimeNotify(args []interface{}) { runtime.Notify(notificationName, args) }` - // cosignersSeparator is a special value which is used to distinguish - // parameters and cosigners for invoke* commands - cosignersSeparator = "--" - arrayStartSeparator = "[" - arrayEndSeparator = "]" ) // NewCommands returns 'contract' command. @@ -542,7 +537,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { paramsStart++ if len(args) > paramsStart { - cosignersOffset, params, err = ParseParams(args[paramsStart:], true) + cosignersOffset, params, err = cmdargs.ParseParams(args[paramsStart:], true) if err != nil { return cli.NewExitError(err, 1) } @@ -622,52 +617,6 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { return nil } -// ParseParams extracts array of smartcontract.Parameter from the given args and -// returns the number of handled words, the array itself and an error. -// `calledFromMain` denotes whether the method was called from the outside or -// recursively and used to check if cosignersSeparator and closing bracket are -// allowed to be in `args` sequence. -func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Parameter, error) { - res := []smartcontract.Parameter{} - for k := 0; k < len(args); { - s := args[k] - switch s { - case cosignersSeparator: - if calledFromMain { - return k + 1, res, nil // `1` to convert index to numWordsRead - } - return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket") - case arrayStartSeparator: - numWordsRead, array, err := ParseParams(args[k+1:], false) - if err != nil { - return 0, nil, fmt.Errorf("failed to parse array: %w", err) - } - res = append(res, smartcontract.Parameter{ - Type: smartcontract.ArrayType, - Value: array, - }) - k += 1 + numWordsRead // `1` for opening bracket - case arrayEndSeparator: - if calledFromMain { - return 0, nil, errors.New("invalid array syntax: missing opening bracket") - } - return k + 1, res, nil // `1`to convert index to numWordsRead - default: - param, err := smartcontract.NewParameterFromString(s) - if err != nil { - return 0, nil, fmt.Errorf("failed to parse argument #%d: %w", k+1, err) - } - res = append(res, *param) - k++ - } - } - if calledFromMain { - return len(args), res, nil - } - return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket") - -} - func testInvokeScript(ctx *cli.Context) error { src := ctx.String("in") if len(src) == 0 { @@ -828,7 +777,7 @@ func contractDeploy(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("failed to restore manifest file: %w", err), 1) } - _, data, extErr := GetDataFromContext(ctx) + _, data, extErr := cmdargs.GetDataFromContext(ctx) if extErr != nil { return extErr } @@ -888,31 +837,6 @@ func contractDeploy(ctx *cli.Context) error { return nil } -// GetDataFromContext returns data parameter from context args. -func GetDataFromContext(ctx *cli.Context) (int, interface{}, *cli.ExitError) { - var ( - data interface{} - offset int - params []smartcontract.Parameter - err error - ) - args := ctx.Args() - if args.Present() { - offset, params, err = ParseParams(args, true) - if err != nil { - return offset, nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1) - } - if len(params) != 1 { - return offset, nil, cli.NewExitError("'data' should be represented as a single parameter", 1) - } - data, err = smartcontract.ExpandParameterToEmitable(params[0]) - if err != nil { - return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1) - } - } - return offset, data, nil -} - // ParseContractConfig reads contract configuration file (.yaml) and returns unmarshalled ProjectConfig. func ParseContractConfig(confFile string) (ProjectConfig, error) { conf := ProjectConfig{} diff --git a/cli/smartcontract/smart_contract_test.go b/cli/smartcontract/smart_contract_test.go index 54e70f340..3dfbb298c 100644 --- a/cli/smartcontract/smart_contract_test.go +++ b/cli/smartcontract/smart_contract_test.go @@ -4,10 +4,8 @@ import ( "flag" "io/ioutil" "os" - "strings" "testing" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/stretchr/testify/require" "github.com/urfave/cli" ) @@ -66,309 +64,3 @@ events: type: Array `, string(manifest)) } - -func TestParseParams_CalledFromItself(t *testing.T) { - testCases := map[string]struct { - WordsRead int - Value []smartcontract.Parameter - }{ - "]": { - WordsRead: 1, - Value: []smartcontract.Parameter{}, - }, - "[ [ ] ] ]": { - WordsRead: 5, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{}, - }, - }, - }, - }, - }, - "a b c ]": { - WordsRead: 4, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "a", - }, - { - Type: smartcontract.StringType, - Value: "b", - }, - { - Type: smartcontract.StringType, - Value: "c", - }, - }, - }, - "a [ b [ [ c d ] e ] ] f ] extra items": { - WordsRead: 13, // the method should return right after the last bracket, as calledFromMain == false - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "a", - }, - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "b", - }, - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "c", - }, - { - Type: smartcontract.StringType, - Value: "d", - }, - }, - }, - { - Type: smartcontract.StringType, - Value: "e", - }, - }, - }, - }, - }, - { - Type: smartcontract.StringType, - Value: "f", - }, - }, - }, - } - - for str, expected := range testCases { - input := strings.Split(str, " ") - offset, actual, err := ParseParams(input, false) - require.NoError(t, err) - require.Equal(t, expected.WordsRead, offset) - require.Equal(t, expected.Value, actual) - } - - errorCases := []string{ - "[ ]", - "[ a b [ c ] d ]", - "[ ] --", - "--", - "not-int:integer ]", - } - - for _, str := range errorCases { - input := strings.Split(str, " ") - _, _, err := ParseParams(input, false) - require.Error(t, err) - } -} - -func TestParseParams_CalledFromOutside(t *testing.T) { - testCases := map[string]struct { - WordsRead int - Parameters []smartcontract.Parameter - }{ - "-- cosigner1": { - WordsRead: 1, // the `--` only - Parameters: []smartcontract.Parameter{}, - }, - "a b c": { - WordsRead: 3, - Parameters: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "a", - }, - { - Type: smartcontract.StringType, - Value: "b", - }, - { - Type: smartcontract.StringType, - Value: "c", - }, - }, - }, - "a b c -- cosigner1": { - WordsRead: 4, - Parameters: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "a", - }, - { - Type: smartcontract.StringType, - Value: "b", - }, - { - Type: smartcontract.StringType, - Value: "c", - }, - }, - }, - "a [ b [ [ c d ] e ] ] f": { - WordsRead: 12, - Parameters: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "a", - }, - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "b", - }, - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "c", - }, - { - Type: smartcontract.StringType, - Value: "d", - }, - }, - }, - { - Type: smartcontract.StringType, - Value: "e", - }, - }, - }, - }, - }, - { - Type: smartcontract.StringType, - Value: "f", - }, - }, - }, - "a [ b ] -- cosigner1 cosigner2": { - WordsRead: 5, - Parameters: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "a", - }, - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "b", - }, - }, - }, - }, - }, - "a [ b ]": { - WordsRead: 4, - Parameters: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "a", - }, - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "b", - }, - }, - }, - }, - }, - "a [ b ] [ [ c ] ] [ [ [ d ] ] ]": { - WordsRead: 16, - Parameters: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "a", - }, - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "b", - }, - }, - }, - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "c", - }, - }, - }, - }, - }, - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: "d", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - for str, expected := range testCases { - input := strings.Split(str, " ") - offset, arr, err := ParseParams(input, true) - require.NoError(t, err) - require.Equal(t, expected.WordsRead, offset) - require.Equal(t, expected.Parameters, arr) - } - - errorCases := []string{ - "[", - "]", - "[ [ ]", - "[ [ ] --", - "[ -- ]", - } - for _, str := range errorCases { - input := strings.Split(str, " ") - _, _, err := ParseParams(input, true) - require.Error(t, err) - } -} diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index 802e28ace..1836ce168 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -6,10 +6,10 @@ import ( "math/big" "strings" + "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" - smartcontractcli "github.com/nspcc-dev/neo-go/cli/smartcontract" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/rpc/client" @@ -461,7 +461,7 @@ func transferNEP17(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1) } - _, data, extErr := smartcontractcli.GetDataFromContext(ctx) + _, data, extErr := cmdargs.GetDataFromContext(ctx) if extErr != nil { return extErr }