From 2ab420ed18feb436270f4fe50b93c5dddbabeafe Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 21 Apr 2021 10:54:10 +0300 Subject: [PATCH 01/11] cli: move cosigners parsing to a separate function We have a lot of common code which is shared between `smartcontract` and `wallet` cli packages. It's convinient to keep it in a separate helper package in order to avoid functional cli packages dependencies. --- cli/cmdargs/parser.go | 49 +++++++++++++++++++++ cli/cmdargs/parser_test.go | 54 ++++++++++++++++++++++++ cli/smartcontract/smart_contract.go | 48 ++++----------------- cli/smartcontract/smart_contract_test.go | 47 --------------------- 4 files changed, 111 insertions(+), 87 deletions(-) create mode 100644 cli/cmdargs/parser.go create mode 100644 cli/cmdargs/parser_test.go diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go new file mode 100644 index 000000000..9993466e3 --- /dev/null +++ b/cli/cmdargs/parser.go @@ -0,0 +1,49 @@ +package cmdargs + +import ( + "fmt" + "strings" + + "github.com/nspcc-dev/neo-go/cli/flags" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/urfave/cli" +) + +// GetSignersFromContext returns signers parsed from context args starting +// from the specified offset. +func GetSignersFromContext(ctx *cli.Context, offset int) ([]transaction.Signer, *cli.ExitError) { + args := ctx.Args() + var signers []transaction.Signer + if args.Present() && len(args) > offset { + for i, c := range args[offset:] { + cosigner, err := parseCosigner(c) + if err != nil { + return nil, cli.NewExitError(fmt.Errorf("failed to parse signer #%d: %w", i, err), 1) + } + signers = append(signers, cosigner) + } + } + return signers, nil +} + +func parseCosigner(c string) (transaction.Signer, error) { + var ( + err error + res = transaction.Signer{ + Scopes: transaction.CalledByEntry, + } + ) + data := strings.SplitN(c, ":", 2) + s := data[0] + res.Account, err = flags.ParseAddress(s) + if err != nil { + return res, err + } + if len(data) > 1 { + res.Scopes, err = transaction.ScopesFromString(data[1]) + if err != nil { + return transaction.Signer{}, err + } + } + return res, nil +} diff --git a/cli/cmdargs/parser_test.go b/cli/cmdargs/parser_test.go new file mode 100644 index 000000000..31cfc7f6b --- /dev/null +++ b/cli/cmdargs/parser_test.go @@ -0,0 +1,54 @@ +package cmdargs + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestParseCosigner(t *testing.T) { + acc := util.Uint160{1, 3, 5, 7} + testCases := map[string]transaction.Signer{ + acc.StringLE(): { + Account: acc, + Scopes: transaction.CalledByEntry, + }, + "0x" + acc.StringLE(): { + Account: acc, + Scopes: transaction.CalledByEntry, + }, + acc.StringLE() + ":Global": { + Account: acc, + Scopes: transaction.Global, + }, + acc.StringLE() + ":CalledByEntry": { + Account: acc, + Scopes: transaction.CalledByEntry, + }, + acc.StringLE() + ":None": { + Account: acc, + Scopes: transaction.None, + }, + acc.StringLE() + ":CalledByEntry,CustomContracts": { + Account: acc, + Scopes: transaction.CalledByEntry | transaction.CustomContracts, + }, + } + for s, expected := range testCases { + actual, err := parseCosigner(s) + require.NoError(t, err) + require.Equal(t, expected, actual) + } + errorCases := []string{ + acc.StringLE() + "0", + acc.StringLE() + ":Unknown", + acc.StringLE() + ":Global,CustomContracts", + acc.StringLE() + ":Global,None", + } + for _, s := range errorCases { + _, err := parseCosigner(s) + require.Error(t, err) + } +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index a3306fe0d..ed0bad0c1 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" + "github.com/nspcc-dev/neo-go/cli/cmdargs" "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" @@ -513,6 +514,7 @@ func invokeFunction(ctx *cli.Context) error { func invokeInternal(ctx *cli.Context, signAndPush bool) error { var ( err error + exitErr *cli.ExitError gas fixedn.Fixed8 operation string params = make([]smartcontract.Parameter, 0) @@ -547,14 +549,9 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { } cosignersStart := paramsStart + cosignersOffset - if len(args) > cosignersStart { - for i, c := range args[cosignersStart:] { - cosigner, err := parseCosigner(c) - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to parse cosigner #%d: %w", i+1, err), 1) - } - cosigners = append(cosigners, cosigner) - } + cosigners, exitErr = cmdargs.GetSignersFromContext(ctx, cosignersStart) + if exitErr != nil { + return exitErr } if signAndPush { @@ -686,16 +683,9 @@ func testInvokeScript(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("failed to restore .nef file: %w", err), 1) } - args := ctx.Args() - var signers []transaction.Signer - if args.Present() { - for i, c := range args[:] { - cosigner, err := parseCosigner(c) - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to parse signer #%d: %w", i+1, err), 1) - } - signers = append(signers, cosigner) - } + signers, exitErr := cmdargs.GetSignersFromContext(ctx, 0) + if exitErr != nil { + return exitErr } gctx, cancel := options.GetTimeoutContext(ctx) @@ -932,25 +922,3 @@ func ParseContractConfig(confFile string) (ProjectConfig, error) { } return conf, nil } - -func parseCosigner(c string) (transaction.Signer, error) { - var ( - err error - res = transaction.Signer{ - Scopes: transaction.CalledByEntry, - } - ) - data := strings.SplitN(c, ":", 2) - s := data[0] - res.Account, err = flags.ParseAddress(s) - if err != nil { - return res, err - } - if len(data) > 1 { - res.Scopes, err = transaction.ScopesFromString(data[1]) - if err != nil { - return transaction.Signer{}, err - } - } - return res, nil -} diff --git a/cli/smartcontract/smart_contract_test.go b/cli/smartcontract/smart_contract_test.go index e00ea71ec..54e70f340 100644 --- a/cli/smartcontract/smart_contract_test.go +++ b/cli/smartcontract/smart_contract_test.go @@ -7,9 +7,7 @@ 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" "github.com/urfave/cli" ) @@ -69,51 +67,6 @@ events: `, string(manifest)) } -func TestParseCosigner(t *testing.T) { - acc := util.Uint160{1, 3, 5, 7} - testCases := map[string]transaction.Signer{ - acc.StringLE(): { - Account: acc, - Scopes: transaction.CalledByEntry, - }, - "0x" + acc.StringLE(): { - Account: acc, - Scopes: transaction.CalledByEntry, - }, - acc.StringLE() + ":Global": { - Account: acc, - Scopes: transaction.Global, - }, - acc.StringLE() + ":CalledByEntry": { - Account: acc, - Scopes: transaction.CalledByEntry, - }, - acc.StringLE() + ":None": { - Account: acc, - Scopes: transaction.None, - }, - acc.StringLE() + ":CalledByEntry,CustomContracts": { - Account: acc, - Scopes: transaction.CalledByEntry | transaction.CustomContracts, - }, - } - for s, expected := range testCases { - actual, err := parseCosigner(s) - require.NoError(t, err) - require.Equal(t, expected, actual) - } - errorCases := []string{ - acc.StringLE() + "0", - acc.StringLE() + ":Unknown", - acc.StringLE() + ":Global,CustomContracts", - acc.StringLE() + ":Global,None", - } - for _, s := range errorCases { - _, err := parseCosigner(s) - require.Error(t, err) - } -} - func TestParseParams_CalledFromItself(t *testing.T) { testCases := map[string]struct { WordsRead int From d12ae0998f78d9c3ba01c9deec24a0346091749d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 21 Apr 2021 11:14:56 +0300 Subject: [PATCH 02/11] cli: return numWordsRead from GetDataFromContext It's needed when we have extra args after the 'data' argument. Then these args are started right from numWordsRead offset. --- cli/smartcontract/smart_contract.go | 21 +++++++++++++-------- cli/wallet/nep17.go | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index ed0bad0c1..b935faddb 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -828,7 +828,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 := GetDataFromContext(ctx) if extErr != nil { return extErr } @@ -889,23 +889,28 @@ func contractDeploy(ctx *cli.Context) error { } // GetDataFromContext returns data parameter from context args. -func GetDataFromContext(ctx *cli.Context) (interface{}, *cli.ExitError) { - var data interface{} +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() { - _, params, err := ParseParams(args, true) + offset, params, err = ParseParams(args, true) if err != nil { - return nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1) + return offset, nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1) } if len(params) != 1 { - return nil, cli.NewExitError("'data' should be represented as a single parameter", 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 nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1) + return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1) } } - return data, nil + return offset, data, nil } // ParseContractConfig reads contract configuration file (.yaml) and returns unmarshalled ProjectConfig. diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index 70817a15b..802e28ace 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -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 := smartcontractcli.GetDataFromContext(ctx) if extErr != nil { return extErr } From 94316fa36de6caba37a32d47ad532e223c6b81aa Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 21 Apr 2021 13:14:55 +0300 Subject: [PATCH 03/11] cli: move GetDataFromContext and ParseParams to a `helpers` package --- cli/cmdargs/parser.go | 81 ++++++ cli/cmdargs/parser_test.go | 308 +++++++++++++++++++++++ cli/smartcontract/smart_contract.go | 80 +----- cli/smartcontract/smart_contract_test.go | 308 ----------------------- cli/wallet/nep17.go | 4 +- 5 files changed, 393 insertions(+), 388 deletions(-) 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 } From 16fa191ccb79ef7e0ba053353726b7da8e7def53 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 21 Apr 2021 15:22:13 +0300 Subject: [PATCH 04/11] cli: move combining signers and accounts to a separate package This code will be reused in other packages. --- cli/cmdargs/parser.go | 20 ++++++++++++++++++++ cli/smartcontract/smart_contract.go | 12 +++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go index c1829a877..252ca4cfc 100644 --- a/cli/cmdargs/parser.go +++ b/cli/cmdargs/parser.go @@ -7,7 +7,10 @@ import ( "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" ) @@ -128,3 +131,20 @@ func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Param } return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket") } + +// GetSignersAccounts returns the list of signers combined with the corresponding +// accounts from the provided wallet. +func GetSignersAccounts(wall *wallet.Wallet, signers []transaction.Signer) ([]client.SignerAccount, error) { + signersAccounts := make([]client.SignerAccount, len(signers)) + for i := range signers { + signerAcc := wall.GetAccount(signers[i].Account) + if signerAcc == nil { + return nil, fmt.Errorf("no account was found in the wallet for signer #%d (%s)", i, address.Uint160ToString(signers[i].Account)) + } + signersAccounts[i] = client.SignerAccount{ + Signer: signers[i], + Account: signerAcc, + } + } + return signersAccounts, nil +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 3fc97f41d..cb95e8627 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -555,15 +555,9 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { if err != nil { return err } - for i := range cosigners { - cosignerAcc := wall.GetAccount(cosigners[i].Account) - if cosignerAcc == nil { - return cli.NewExitError(fmt.Errorf("can't calculate network fee: no account was found in the wallet for cosigner #%d", i), 1) - } - cosignersAccounts = append(cosignersAccounts, client.SignerAccount{ - Signer: cosigners[i], - Account: cosignerAcc, - }) + cosignersAccounts, err = cmdargs.GetSignersAccounts(wall, cosigners) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to calculate network fee: %w", err), 1) } } gctx, cancel := options.GetTimeoutContext(ctx) From 1d6ba389c9ef1dad2b166ebfc160cea3e6c0b844 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 21 Apr 2021 13:44:37 +0300 Subject: [PATCH 05/11] rpc: split SignAndPushInvocationTx in two parts There are several places where constructed transaction need to be signed and sent. Thus, we definitely need a separate method for signing and sending constructed transaction to avoid code duplication. --- pkg/rpc/client/rpc.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 97deedfc1..db88ff0df 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -535,13 +535,22 @@ func (c *Client) SubmitRawOracleResponse(ps request.RawParams) error { // invocation transaction and an error. If one of the cosigners accounts is // neither contract-based nor unlocked an error is returned. func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sysfee int64, netfee fixedn.Fixed8, cosigners []SignerAccount) (util.Uint256, error) { - var txHash util.Uint256 - var err error - tx, err := c.CreateTxFromScript(script, acc, sysfee, int64(netfee), cosigners) if err != nil { - return txHash, fmt.Errorf("failed to create tx: %w", err) + return util.Uint256{}, fmt.Errorf("failed to create tx: %w", err) } + return c.SignAndPushTx(tx, acc, cosigners) +} + +// SignAndPushTx signs given transaction using given wif and cosigners and pushes +// it to the chain. It returns a hash of the transaction and an error. If one of +// the cosigners accounts is neither contract-based nor unlocked an error is +// returned. +func (c *Client) SignAndPushTx(tx *transaction.Transaction, acc *wallet.Account, cosigners []SignerAccount) (util.Uint256, error) { + var ( + txHash util.Uint256 + err error + ) if err = acc.SignTx(c.GetNetwork(), tx); err != nil { return txHash, fmt.Errorf("failed to sign tx: %w", err) } From 49c35dec2059ecfb0ad2d3fa64c25f1a5b86577c Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 21 Apr 2021 12:43:30 +0300 Subject: [PATCH 06/11] rpc: allow to provide cosigners for NEP17-transfer-related commands --- cli/wallet/nep17.go | 2 +- cli/wallet/wallet.go | 2 +- pkg/rpc/client/nep17.go | 44 +++++++++++++++++------------------ pkg/rpc/server/client_test.go | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index 1836ce168..f23b89f5a 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -477,7 +477,7 @@ func transferNEP17(ctx *cli.Context) error { func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget) error { gas := flags.Fixed8FromContext(ctx, "gas") - tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients) + tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients, nil) if err != nil { return cli.NewExitError(err, 1) } diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 0913d5a1c..9b2f3eee7 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -272,7 +272,7 @@ func claimGas(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - hash, err := c.TransferNEP17(acc, scriptHash, neoContractHash, 0, 0, nil) + hash, err := c.TransferNEP17(acc, scriptHash, neoContractHash, 0, 0, nil, nil) if err != nil { return cli.NewExitError(err, 1) } diff --git a/pkg/rpc/client/nep17.go b/pkg/rpc/client/nep17.go index 3b27ebf54..470de9380 100644 --- a/pkg/rpc/client/nep17.go +++ b/pkg/rpc/client/nep17.go @@ -69,19 +69,23 @@ func (c *Client) NEP17TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) { // method of a given contract (token) to move specified amount of NEP17 assets // (in FixedN format using contract's number of decimals) to given account and // returns it. The returned transaction is not signed. -func (c *Client) CreateNEP17TransferTx(acc *wallet.Account, to util.Uint160, token util.Uint160, amount int64, gas int64, data interface{}) (*transaction.Transaction, error) { +func (c *Client) CreateNEP17TransferTx(acc *wallet.Account, to util.Uint160, + token util.Uint160, amount int64, gas int64, data interface{}, cosigners []SignerAccount) (*transaction.Transaction, error) { return c.CreateNEP17MultiTransferTx(acc, gas, []TransferTarget{ {Token: token, Address: to, Amount: amount, Data: data, }, - }) + }, cosigners) } -// CreateNEP17MultiTransferTx creates an invocation transaction for performing NEP17 transfers -// from a single sender to multiple recipients with the given data. -func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, recipients []TransferTarget) (*transaction.Transaction, error) { +// CreateNEP17MultiTransferTx creates an invocation transaction for performing +// NEP17 transfers from a single sender to multiple recipients with the given +// data and cosigners. Transaction's sender is included with the CalledByEntry +// scope by default. +func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, + recipients []TransferTarget, cosigners []SignerAccount) (*transaction.Transaction, error) { from, err := address.StringToUint160(acc.Address) if err != nil { return nil, fmt.Errorf("bad account address: %w", err) @@ -95,13 +99,13 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, reci if w.Err != nil { return nil, fmt.Errorf("failed to create transfer script: %w", w.Err) } - return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []SignerAccount{{ + return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, append([]SignerAccount{{ Signer: transaction.Signer{ Account: from, Scopes: transaction.CalledByEntry, }, Account: acc, - }}) + }}, cosigners...)) } // CreateTxFromScript creates transaction and properly sets cosigners and NetworkFee. @@ -143,38 +147,34 @@ func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, // TransferNEP17 creates an invocation transaction that invokes 'transfer' method // on a given token to move specified amount of NEP17 assets (in FixedN format // using contract's number of decimals) to given account with data specified and -// sends it to the network returning just a hash of it. -func (c *Client) TransferNEP17(acc *wallet.Account, to util.Uint160, token util.Uint160, amount int64, gas int64, data interface{}) (util.Uint256, error) { +// sends it to the network returning just a hash of it. Cosigners argument +// specifies a set of the transaction cosigners (may be nil or may include sender) +// with proper scope and accounts to cosign the transaction. If cosigning is +// impossible (e.g. due to locked cosigner's account) an error is returned. +func (c *Client) TransferNEP17(acc *wallet.Account, to util.Uint160, token util.Uint160, + amount int64, gas int64, data interface{}, cosigners []SignerAccount) (util.Uint256, error) { if !c.initDone { return util.Uint256{}, errNetworkNotInitialized } - tx, err := c.CreateNEP17TransferTx(acc, to, token, amount, gas, data) + tx, err := c.CreateNEP17TransferTx(acc, to, token, amount, gas, data, cosigners) if err != nil { return util.Uint256{}, err } - if err := acc.SignTx(c.GetNetwork(), tx); err != nil { - return util.Uint256{}, fmt.Errorf("can't sign tx: %w", err) - } - - return c.SendRawTransaction(tx) + return c.SignAndPushTx(tx, acc, cosigners) } // MultiTransferNEP17 is similar to TransferNEP17, buf allows to have multiple recipients. -func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients []TransferTarget) (util.Uint256, error) { +func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients []TransferTarget, cosigners []SignerAccount) (util.Uint256, error) { if !c.initDone { return util.Uint256{}, errNetworkNotInitialized } - tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients) + tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients, cosigners) if err != nil { return util.Uint256{}, err } - if err := acc.SignTx(c.GetNetwork(), tx); err != nil { - return util.Uint256{}, fmt.Errorf("can't sign tx: %w", err) - } - - return c.SendRawTransaction(tx) + return c.SignAndPushTx(tx, acc, cosigners) } diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index e6014afd4..b71e32ab2 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -707,7 +707,7 @@ func TestCreateNEP17TransferTx(t *testing.T) { gasContractHash, err := c.GetNativeContractHash(nativenames.Gas) require.NoError(t, err) - tx, err := c.CreateNEP17TransferTx(acc, util.Uint160{}, gasContractHash, 1000, 0, nil) + tx, err := c.CreateNEP17TransferTx(acc, util.Uint160{}, gasContractHash, 1000, 0, nil, nil) require.NoError(t, err) require.NoError(t, acc.SignTx(testchain.Network(), tx)) require.NoError(t, chain.VerifyTx(tx)) From 127d0ad2baeadccc3e552c1ae2b79d5db7b6951c Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 21 Apr 2021 17:39:18 +0300 Subject: [PATCH 07/11] rpc: allow to provide cosigners for NEP11-transfer-related commands --- pkg/rpc/client/nep11.go | 26 +++++++++----------------- pkg/rpc/server/client_test.go | 2 +- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/pkg/rpc/client/nep11.go b/pkg/rpc/client/nep11.go index f313815b3..664430cd5 100644 --- a/pkg/rpc/client/nep11.go +++ b/pkg/rpc/client/nep11.go @@ -39,20 +39,16 @@ func (c *Client) NEP11BalanceOf(tokenHash, owner util.Uint160) (int64, error) { // on a given token to move the whole NEP11 token with the specified token ID to // given account and sends it to the network returning just a hash of it. func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160, - tokenHash util.Uint160, tokenID string, gas int64) (util.Uint256, error) { + tokenHash util.Uint160, tokenID string, gas int64, cosigners []SignerAccount) (util.Uint256, error) { if !c.initDone { return util.Uint256{}, errNetworkNotInitialized } - tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, to, tokenID) + tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, cosigners, to, tokenID) if err != nil { return util.Uint256{}, err } - if err := acc.SignTx(c.GetNetwork(), tx); err != nil { - return util.Uint256{}, fmt.Errorf("can't sign NEP11 transfer tx: %w", err) - } - - return c.SendRawTransaction(tx) + return c.SignAndPushTx(tx, acc, cosigners) } // createNEP11TransferTx is an internal helper for TransferNEP11 and @@ -63,7 +59,7 @@ func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160, // `args` for TransferNEP11: to util.Uint160, tokenID string; // `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string. func (c *Client) createNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160, - gas int64, args ...interface{}) (*transaction.Transaction, error) { + gas int64, cosigners []SignerAccount, args ...interface{}) (*transaction.Transaction, error) { w := io.NewBufBinWriter() emit.AppCall(w.BinWriter, tokenHash, "transfer", callflag.All, args...) emit.Opcodes(w.BinWriter, opcode.ASSERT) @@ -74,13 +70,13 @@ func (c *Client) createNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1 if err != nil { return nil, fmt.Errorf("bad account address: %w", err) } - return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []SignerAccount{{ + return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, append([]SignerAccount{{ Signer: transaction.Signer{ Account: from, Scopes: transaction.CalledByEntry, }, Account: acc, - }}) + }}, cosigners...)) } // Non-divisible NFT methods section start. @@ -114,7 +110,7 @@ func (c *Client) NEP11NDOwnerOf(tokenHash util.Uint160, tokenID string) (util.Ui // (in FixedN format using contract's number of decimals) to given account and // sends it to the network returning just a hash of it. func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160, - tokenHash util.Uint160, amount int64, tokenID string, gas int64) (util.Uint256, error) { + tokenHash util.Uint160, amount int64, tokenID string, gas int64, cosigners []SignerAccount) (util.Uint256, error) { if !c.initDone { return util.Uint256{}, errNetworkNotInitialized } @@ -122,16 +118,12 @@ func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160, if err != nil { return util.Uint256{}, fmt.Errorf("bad account address: %w", err) } - tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, acc.Address, from, to, amount, tokenID) + tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, cosigners, acc.Address, from, to, amount, tokenID) if err != nil { return util.Uint256{}, err } - if err := acc.SignTx(c.GetNetwork(), tx); err != nil { - return util.Uint256{}, fmt.Errorf("can't sign NEP11 divisible transfer tx: %w", err) - } - - return c.SendRawTransaction(tx) + return c.SignAndPushTx(tx, acc, cosigners) } // NEP11DBalanceOf invokes `balanceOf` divisible NEP11 method on a diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index b71e32ab2..826981bf5 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -822,7 +822,7 @@ func TestClient_NEP11(t *testing.T) { require.EqualValues(t, expected, p) }) t.Run("Transfer", func(t *testing.T) { - _, err := c.TransferNEP11(wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)), testchain.PrivateKeyByID(1).GetScriptHash(), h, "neo.com", 0) + _, err := c.TransferNEP11(wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)), testchain.PrivateKeyByID(1).GetScriptHash(), h, "neo.com", 0, nil) require.NoError(t, err) }) } From f848783d5de628f9dd198f518d8105b1da8124ea Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 21 Apr 2021 12:50:04 +0300 Subject: [PATCH 08/11] cli: allow to provide cosigners for 'wallet nep17 transfer' --- cli/contract_test.go | 45 +++++++++++++++++++++++++++----------------- cli/nep17_test.go | 42 ++++++++++++++++++++++++++++++----------- cli/wallet/nep17.go | 35 +++++++++++++++++++++------------- 3 files changed, 81 insertions(+), 41 deletions(-) diff --git a/cli/contract_test.go b/cli/contract_test.go index 5e9202c9e..17f7fec96 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -226,6 +226,33 @@ func TestContractDeployWithData(t *testing.T) { require.Equal(t, []byte("take_me_to_church"), res.Stack[0].Value()) } +func deployVerifyContract(t *testing.T, e *executor) util.Uint160 { + tmpDir := path.Join(os.TempDir(), "neogo.test.deployverifycontract") + require.NoError(t, os.Mkdir(tmpDir, os.ModePerm)) + t.Cleanup(func() { + os.RemoveAll(tmpDir) + }) + // deploy verification contract + nefName := path.Join(tmpDir, "verify.nef") + manifestName := path.Join(tmpDir, "verify.manifest.json") + e.Run(t, "neo-go", "contract", "compile", + "--in", "testdata/verify.go", + "--config", "testdata/verify.yml", + "--out", nefName, "--manifest", manifestName) + e.In.WriteString("one\r") + e.Run(t, "neo-go", "contract", "deploy", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, "--address", validatorAddr, + "--in", nefName, "--manifest", manifestName) + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: ")) + hVerify, err := util.Uint160DecodeStringLE(line) + require.NoError(t, err) + e.checkTxPersisted(t) + return hVerify +} + func TestComlileAndInvokeFunction(t *testing.T) { e := newExecutor(t, true) @@ -315,23 +342,7 @@ func TestComlileAndInvokeFunction(t *testing.T) { require.Equal(t, []byte("on create|sub create"), res.Stack[0].Value()) // deploy verification contract - nefName = path.Join(tmpDir, "verify.nef") - manifestName = path.Join(tmpDir, "verify.manifest.json") - e.Run(t, "neo-go", "contract", "compile", - "--in", "testdata/verify.go", - "--config", "testdata/verify.yml", - "--out", nefName, "--manifest", manifestName) - e.In.WriteString("one\r") - e.Run(t, "neo-go", "contract", "deploy", - "--rpc-endpoint", "http://"+e.RPC.Addr, - "--wallet", validatorWallet, "--address", validatorAddr, - "--in", nefName, "--manifest", manifestName) - line, err = e.Out.ReadString('\n') - require.NoError(t, err) - line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: ")) - hVerify, err := util.Uint160DecodeStringLE(line) - require.NoError(t, err) - e.checkTxPersisted(t) + hVerify := deployVerifyContract(t, e) t.Run("real invoke", func(t *testing.T) { cmd := []string{"neo-go", "contract", "invokefunction", diff --git a/cli/nep17_test.go b/cli/nep17_test.go index e74b1563e..8226b8bb6 100644 --- a/cli/nep17_test.go +++ b/cli/nep17_test.go @@ -132,6 +132,8 @@ func TestNEP17Transfer(t *testing.T) { b, _ := e.Chain.GetGoverningTokenBalance(sh) require.Equal(t, big.NewInt(1), b) + hVerify := deployVerifyContract(t, e) + t.Run("default address", func(t *testing.T) { const validatorDefault = "NTh9TnZTstvAePEYWDGLLxidBikJE24uTo" e.In.WriteString("one\r") @@ -159,21 +161,39 @@ func TestNEP17Transfer(t *testing.T) { require.Equal(t, big.NewInt(41), b) }) + validTil := e.Chain.BlockHeight() + 100 + cmd := []string{ + "neo-go", "wallet", "nep17", "transfer", + "--rpc-endpoint", "http://" + e.RPC.Addr, + "--wallet", validatorWallet, + "--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()), + "--token", "GAS", + "--amount", "1", + "--from", validatorAddr, + "[", validatorAddr, strconv.Itoa(int(validTil)), "]"} + t.Run("with data", func(t *testing.T) { e.In.WriteString("one\r") - validTil := e.Chain.BlockHeight() + 100 - e.Run(t, []string{ - "neo-go", "wallet", "nep17", "transfer", - "--rpc-endpoint", "http://" + e.RPC.Addr, - "--wallet", validatorWallet, - "--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()), - "--token", "GAS", - "--amount", "1", - "--from", validatorAddr, - "[", validatorAddr, strconv.Itoa(int(validTil)), "]", - }...) + e.Run(t, cmd...) e.checkTxPersisted(t) }) + + t.Run("with data and signers", func(t *testing.T) { + t.Run("invalid sender's scope", func(t *testing.T) { + e.In.WriteString("one\r") + e.RunWithError(t, append(cmd, "--", validatorAddr+":None")...) + }) + t.Run("good", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(cmd, "--", validatorAddr+":Global")...) // CalledByEntry is enough, but it's the default value, so check something else + e.checkTxPersisted(t) + }) + t.Run("several signers", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(cmd, "--", validatorAddr, hVerify.StringLE())...) + e.checkTxPersisted(t) + }) + }) } func TestNEP17MultiTransfer(t *testing.T) { diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index f23b89f5a..88f90c108 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -112,12 +112,15 @@ func newNEP17Commands() []cli.Command { { Name: "transfer", Usage: "transfer NEP17 tokens", - UsageText: "transfer --wallet --rpc-endpoint --timeout