diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go new file mode 100644 index 000000000..252ca4cfc --- /dev/null +++ b/cli/cmdargs/parser.go @@ -0,0 +1,150 @@ +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/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" +) + +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) { + 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 +} + +// 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") +} + +// 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/cmdargs/parser_test.go b/cli/cmdargs/parser_test.go new file mode 100644 index 000000000..7b09aa05d --- /dev/null +++ b/cli/cmdargs/parser_test.go @@ -0,0 +1,362 @@ +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" +) + +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 + 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/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..b614682d5 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) { @@ -191,17 +211,38 @@ func TestNEP17MultiTransfer(t *testing.T) { "GAS:" + privs[1].Address() + ":7", neoContractHash.StringLE() + ":" + privs[2].Address() + ":13", } + hVerify := deployVerifyContract(t, e) - e.In.WriteString("one\r") - e.Run(t, args...) - e.checkTxPersisted(t) + t.Run("no cosigners", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, args...) + e.checkTxPersisted(t) - b, _ := e.Chain.GetGoverningTokenBalance(privs[0].GetScriptHash()) - require.Equal(t, big.NewInt(42), b) - b = e.Chain.GetUtilityTokenBalance(privs[1].GetScriptHash()) - require.Equal(t, big.NewInt(int64(fixedn.Fixed8FromInt64(7))), b) - b, _ = e.Chain.GetGoverningTokenBalance(privs[2].GetScriptHash()) - require.Equal(t, big.NewInt(13), b) + b, _ := e.Chain.GetGoverningTokenBalance(privs[0].GetScriptHash()) + require.Equal(t, big.NewInt(42), b) + b = e.Chain.GetUtilityTokenBalance(privs[1].GetScriptHash()) + require.Equal(t, big.NewInt(int64(fixedn.Fixed8FromInt64(7))), b) + b, _ = e.Chain.GetGoverningTokenBalance(privs[2].GetScriptHash()) + require.Equal(t, big.NewInt(13), b) + }) + + t.Run("invalid sender scope", func(t *testing.T) { + e.In.WriteString("one\r") + e.RunWithError(t, append(args, + "--", validatorAddr+":None")...) // invalid sender scope + }) + t.Run("Global sender scope", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(args, + "--", validatorAddr+":Global")...) + e.checkTxPersisted(t) + }) + t.Run("Several cosigners", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(args, + "--", validatorAddr, hVerify.StringLE())...) + e.checkTxPersisted(t) + }) } func TestNEP17ImportToken(t *testing.T) { diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index a3306fe0d..cb95e8627 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" @@ -86,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. @@ -513,6 +509,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) @@ -540,21 +537,16 @@ 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) } } 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 { @@ -563,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) @@ -625,52 +611,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 { @@ -686,16 +626,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) @@ -838,7 +771,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 } @@ -898,26 +831,6 @@ func contractDeploy(ctx *cli.Context) error { return nil } -// GetDataFromContext returns data parameter from context args. -func GetDataFromContext(ctx *cli.Context) (interface{}, *cli.ExitError) { - var data interface{} - args := ctx.Args() - if args.Present() { - _, params, err := ParseParams(args, true) - if err != nil { - return 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) - } - 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 data, nil -} - // ParseContractConfig reads contract configuration file (.yaml) and returns unmarshalled ProjectConfig. func ParseContractConfig(confFile string) (ProjectConfig, error) { conf := ProjectConfig{} @@ -932,25 +845,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..3dfbb298c 100644 --- a/cli/smartcontract/smart_contract_test.go +++ b/cli/smartcontract/smart_contract_test.go @@ -4,12 +4,8 @@ import ( "flag" "io/ioutil" "os" - "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" ) @@ -68,354 +64,3 @@ events: type: Array `, 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 - 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 70817a15b..4c01dbf14 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" @@ -112,18 +112,21 @@ func newNEP17Commands() []cli.Command { { Name: "transfer", Usage: "transfer NEP17 tokens", - UsageText: "transfer --wallet --rpc-endpoint --timeout