diff --git a/cli/options/options.go b/cli/options/options.go index 34cd31777..568106e63 100644 --- a/cli/options/options.go +++ b/cli/options/options.go @@ -4,10 +4,22 @@ Package options contains a set of common CLI options and helper functions to use package options import ( + "context" + "errors" + "time" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/urfave/cli" ) +// DefaultTimeout is the default timeout used for RPC requests. +const DefaultTimeout = 10 * time.Second + +// RPCEndpointFlag is a long flag name for RPC endpoint. It can be used to +// check for flag presence in the context. +const RPCEndpointFlag = "rpc-endpoint" + // Network is a set of flags for choosing the network to operate on // (privnet/mainnet/testnet). var Network = []cli.Flag{ @@ -16,6 +28,20 @@ var Network = []cli.Flag{ cli.BoolFlag{Name: "testnet, t"}, } +// RPC is a set of flags used for RPC connections (endpoint and timeout). +var RPC = []cli.Flag{ + cli.StringFlag{ + Name: RPCEndpointFlag + ", r", + Usage: "RPC node address", + }, + cli.DurationFlag{ + Name: "timeout, t", + Usage: "Timeout for the operation (10 seconds by default)", + }, +} + +var errNoEndpoint = errors.New("no RPC endpoint specified, use option '--" + RPCEndpointFlag + "' or '-r'") + // GetNetwork examines Context's flags and returns the appropriate network. It // defaults to PrivNet if no flags are given. func GetNetwork(ctx *cli.Context) netmode.Magic { @@ -28,3 +54,25 @@ func GetNetwork(ctx *cli.Context) netmode.Magic { } return net } + +// GetTimeoutContext returns a context.Context with default of user-set timeout. +func GetTimeoutContext(ctx *cli.Context) (context.Context, func()) { + dur := ctx.Duration("timeout") + if dur == 0 { + dur = DefaultTimeout + } + return context.WithTimeout(context.Background(), dur) +} + +// GetRPCClient returns an RPC client instance for the given Context. +func GetRPCClient(gctx context.Context, ctx *cli.Context) (*client.Client, cli.ExitCoder) { + endpoint := ctx.String(RPCEndpointFlag) + if len(endpoint) == 0 { + return nil, cli.NewExitError(errNoEndpoint, 1) + } + c, err := client.New(gctx, endpoint, client.Options{}) + if err != nil { + return nil, cli.NewExitError(err, 1) + } + return c, nil +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index cc7be5a0d..a5a62e010 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -2,7 +2,6 @@ package smartcontract import ( "bytes" - "context" "encoding/hex" "encoding/json" "fmt" @@ -14,11 +13,11 @@ import ( "github.com/go-yaml/yaml" "github.com/nspcc-dev/neo-go/cli/flags" + "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "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/rpc/request" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" @@ -32,7 +31,6 @@ import ( ) var ( - errNoEndpoint = errors.New("no RPC endpoint specified, use option '--endpoint' or '-e'") errNoInput = errors.New("no input file was found, specify an input file with the '--in or -i' flag") errNoConfFile = errors.New("no config file was found, specify a config file with the '--config' or '-c' flag") errNoMethod = errors.New("no method specified for function invocation command") @@ -41,10 +39,6 @@ var ( errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag") errFileExist = errors.New("A file with given smart-contract name already exists") - endpointFlag = cli.StringFlag{ - Name: "endpoint, e", - Usage: "trusted RPC endpoint address (like 'http://localhost:20331')", - } walletFlag = cli.StringFlag{ Name: "wallet, w", Usage: "wallet to use to get the key for transaction signing", @@ -76,6 +70,33 @@ func Main(op string, args []interface{}) { // NewCommands returns 'contract' command. func NewCommands() []cli.Command { + testInvokeScriptFlags := []cli.Flag{ + cli.StringFlag{ + Name: "in, i", + Usage: "Input location of the avm file that needs to be invoked", + }, + } + testInvokeScriptFlags = append(testInvokeScriptFlags, options.RPC...) + deployFlags := []cli.Flag{ + cli.StringFlag{ + Name: "in, i", + Usage: "Input file for the smart contract (*.avm)", + }, + cli.StringFlag{ + Name: "config, c", + Usage: "configuration input file (*.yml)", + }, + walletFlag, + addressFlag, + gasFlag, + } + deployFlags = append(deployFlags, options.RPC...) + invokeFunctionFlags := []cli.Flag{ + walletFlag, + addressFlag, + gasFlag, + } + invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...) return []cli.Command{{ Name: "contract", Usage: "compile - debug - deploy smart contracts", @@ -121,42 +142,24 @@ func NewCommands() []cli.Command { to it). `, Action: contractDeploy, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "in, i", - Usage: "Input file for the smart contract (*.avm)", - }, - cli.StringFlag{ - Name: "config, c", - Usage: "configuration input file (*.yml)", - }, - endpointFlag, - walletFlag, - addressFlag, - gasFlag, - }, + Flags: deployFlags, }, { Name: "invokefunction", Usage: "invoke deployed contract on the blockchain", - UsageText: "neo-go contract invokefunction -e endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...] [--] [cosigners...]", + UsageText: "neo-go contract invokefunction -r endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...] [--] [cosigners...]", Description: `Executes given (as a script hash) deployed script with the given method, arguments and cosigners. See testinvokefunction documentation for the details about parameters. It differs from testinvokefunction in that this command sends an invocation transaction to the network. `, Action: invokeFunction, - Flags: []cli.Flag{ - endpointFlag, - walletFlag, - addressFlag, - gasFlag, - }, + Flags: invokeFunctionFlags, }, { Name: "testinvokefunction", Usage: "invoke deployed contract on the blockchain (test mode)", - UsageText: "neo-go contract testinvokefunction -e endpoint scripthash [method] [arguments...] [--] [cosigners...]", + UsageText: "neo-go contract testinvokefunction -r endpoint scripthash [method] [arguments...] [--] [cosigners...]", Description: `Executes given (as a script hash) deployed script with the given method, arguments and cosigners. If no method is given "" is passed to the script, if no arguments are given, an empty array is passed, if no cosigners are given, @@ -247,25 +250,17 @@ func NewCommands() []cli.Command { * '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,CustomGroups' `, Action: testInvokeFunction, - Flags: []cli.Flag{ - endpointFlag, - }, + Flags: options.RPC, }, { Name: "testinvokescript", Usage: "Invoke compiled AVM code on the blockchain (test mode, not creating a transaction for it)", - UsageText: "neo-go contract testinvokescript -e endpoint -i input.avm [cosigners...]", + UsageText: "neo-go contract testinvokescript -r endpoint -i input.avm [cosigners...]", Description: `Executes given compiled AVM instructions with the given set of cosigners. See testinvokefunction documentation for the details about parameters. `, Action: testInvokeScript, - Flags: []cli.Flag{ - endpointFlag, - cli.StringFlag{ - Name: "in, i", - Usage: "Input location of the avm file that needs to be invoked", - }, - }, + Flags: testInvokeScriptFlags, }, { Name: "init", @@ -407,11 +402,6 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { acc *wallet.Account ) - endpoint := ctx.String("endpoint") - if len(endpoint) == 0 { - return cli.NewExitError(errNoEndpoint, 1) - } - args := ctx.Args() if !args.Present() { return cli.NewExitError(errNoScriptHash, 1) @@ -455,9 +445,12 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { return err } } - c, err := client.New(context.TODO(), endpoint, client.Options{}) + gctx, cancel := options.GetTimeoutContext(ctx) + defer cancel() + + c, err := options.GetRPCClient(gctx, ctx) if err != nil { - return cli.NewExitError(err, 1) + return err } resp, err = c.InvokeFunction(script, operation, params, cosigners) @@ -494,10 +487,6 @@ func testInvokeScript(ctx *cli.Context) error { if len(src) == 0 { return cli.NewExitError(errNoInput, 1) } - endpoint := ctx.String("endpoint") - if len(endpoint) == 0 { - return cli.NewExitError(errNoEndpoint, 1) - } b, err := ioutil.ReadFile(src) if err != nil { @@ -516,9 +505,12 @@ func testInvokeScript(ctx *cli.Context) error { } } - c, err := client.New(context.TODO(), endpoint, client.Options{}) + gctx, cancel := options.GetTimeoutContext(ctx) + defer cancel() + + c, err := options.GetRPCClient(gctx, ctx) if err != nil { - return cli.NewExitError(err, 1) + return err } scriptHex := hex.EncodeToString(b) @@ -640,10 +632,6 @@ func contractDeploy(ctx *cli.Context) error { if len(confFile) == 0 { return cli.NewExitError(errNoConfFile, 1) } - endpoint := ctx.String("endpoint") - if len(endpoint) == 0 { - return cli.NewExitError(errNoEndpoint, 1) - } gas := flags.Fixed8FromContext(ctx, "gas") acc, err := getAccFromContext(ctx) @@ -659,9 +647,12 @@ func contractDeploy(ctx *cli.Context) error { return err } - c, err := client.New(context.TODO(), endpoint, client.Options{}) + gctx, cancel := options.GetTimeoutContext(ctx) + defer cancel() + + c, err := options.GetRPCClient(gctx, ctx) if err != nil { - return cli.NewExitError(err, 1) + return err } m := conf.ToManifest(avm) diff --git a/cli/wallet/multisig.go b/cli/wallet/multisig.go index 639b77235..9f8176dd7 100644 --- a/cli/wallet/multisig.go +++ b/cli/wallet/multisig.go @@ -5,31 +5,31 @@ import ( "fmt" "io/ioutil" + "github.com/nspcc-dev/neo-go/cli/options" "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/context" "github.com/urfave/cli" ) func newMultisigCommands() []cli.Command { + signFlags := []cli.Flag{ + walletPathFlag, + outFlag, + inFlag, + cli.StringFlag{ + Name: "addr", + Usage: "Address to use", + }, + } + signFlags = append(signFlags, options.RPC...) return []cli.Command{ { Name: "sign", Usage: "sign a transaction", UsageText: "multisig sign --path --addr --in --out ", Action: signMultisig, - Flags: []cli.Flag{ - walletPathFlag, - rpcFlag, - timeoutFlag, - outFlag, - inFlag, - cli.StringFlag{ - Name: "addr", - Usage: "Address to use", - }, - }, + Flags: signFlags, }, } } @@ -75,20 +75,21 @@ func signMultisig(ctx *cli.Context) error { } else if err := writeParameterContext(c, ctx.String("out")); err != nil { return cli.NewExitError(err, 1) } - if endpoint := ctx.String("rpc"); endpoint != "" { + if len(ctx.String(options.RPCEndpointFlag)) != 0 { w, err := c.GetWitness(acc.Contract) if err != nil { return cli.NewExitError(err, 1) } tx.Scripts = append(tx.Scripts, *w) - gctx, cancel := getGoContext(ctx) + gctx, cancel := options.GetTimeoutContext(ctx) defer cancel() - c, err := client.New(gctx, ctx.String("rpc"), client.Options{}) + c, err := options.GetRPCClient(gctx, ctx) if err != nil { - return cli.NewExitError(err, 1) - } else if err := c.SendRawTransaction(tx); err != nil { + return err + } + if err := c.SendRawTransaction(tx); err != nil { return cli.NewExitError(err, 1) } } diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go index cf61345ff..99e267620 100644 --- a/cli/wallet/nep5.go +++ b/cli/wallet/nep5.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "github.com/nspcc-dev/neo-go/cli/flags" + "github.com/nspcc-dev/neo-go/cli/options" "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/context" @@ -16,39 +17,59 @@ import ( ) func newNEP5Commands() []cli.Command { + balanceFlags := []cli.Flag{ + walletPathFlag, + cli.StringFlag{ + Name: "addr", + Usage: "Address to use", + }, + cli.StringFlag{ + Name: "token", + Usage: "Token to use", + }, + } + balanceFlags = append(balanceFlags, options.RPC...) + importFlags := []cli.Flag{ + walletPathFlag, + cli.StringFlag{ + Name: "token", + Usage: "Token contract hash in LE", + }, + } + importFlags = append(importFlags, options.RPC...) + transferFlags := []cli.Flag{ + walletPathFlag, + outFlag, + fromAddrFlag, + toAddrFlag, + cli.StringFlag{ + Name: "token", + Usage: "Token to use", + }, + cli.StringFlag{ + Name: "amount", + Usage: "Amount of asset to send", + }, + flags.Fixed8Flag{ + Name: "gas", + Usage: "Amount of GAS to attach to a tx", + }, + } + transferFlags = append(transferFlags, options.RPC...) return []cli.Command{ { Name: "balance", Usage: "get address balance", - UsageText: "balance --path --rpc --addr [--token ]", + UsageText: "balance --path --rpc-endpoint --timeout