2020-06-17 18:13:37 +00:00
/ *
Package options contains a set of common CLI options and helper functions to use them .
* /
package options
import (
2020-06-17 21:15:13 +00:00
"context"
"errors"
2022-10-07 11:51:51 +00:00
"fmt"
"net/url"
"os"
"runtime"
2022-09-08 16:05:32 +00:00
"strconv"
2023-12-04 14:02:44 +00:00
"strings"
2020-06-17 21:15:13 +00:00
"time"
2023-12-04 14:02:44 +00:00
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/input"
2022-10-03 12:05:34 +00:00
"github.com/nspcc-dev/neo-go/pkg/config"
2020-06-17 18:13:37 +00:00
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
2022-09-08 16:05:32 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
2023-12-04 14:02:44 +00:00
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
2022-10-07 11:51:51 +00:00
"github.com/nspcc-dev/neo-go/pkg/io"
2022-07-21 19:39:53 +00:00
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
2023-12-07 07:35:37 +00:00
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
2022-09-08 16:05:32 +00:00
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/util"
2023-12-04 14:02:44 +00:00
"github.com/nspcc-dev/neo-go/pkg/wallet"
2020-06-17 18:13:37 +00:00
"github.com/urfave/cli"
2022-10-07 11:51:51 +00:00
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
2023-12-04 14:02:44 +00:00
"gopkg.in/yaml.v3"
2020-06-17 18:13:37 +00:00
)
2023-12-28 11:58:38 +00:00
const (
// DefaultTimeout is the default timeout used for RPC requests.
DefaultTimeout = 10 * time . Second
// DefaultAwaitableTimeout is the default timeout used for RPC requests that
// require transaction awaiting. It is set to the approximate time of three
// Neo N3 mainnet blocks accepting.
DefaultAwaitableTimeout = 3 * 15 * time . Second
)
2020-06-17 21:15:13 +00:00
2022-04-20 18:30:09 +00:00
// RPCEndpointFlag is a long flag name for an RPC endpoint. It can be used to
2020-06-17 21:15:13 +00:00
// check for flag presence in the context.
const RPCEndpointFlag = "rpc-endpoint"
2023-11-25 05:58:26 +00:00
// Wallet is a set of flags used for wallet operations.
var Wallet = [ ] cli . Flag { cli . StringFlag {
Name : "wallet, w" ,
Usage : "wallet to use to get the key for transaction signing; conflicts with --wallet-config flag" ,
} , cli . StringFlag {
Name : "wallet-config" ,
Usage : "path to wallet config to use to get the key for transaction signing; conflicts with --wallet flag" } ,
}
2020-06-17 18:13:37 +00:00
// Network is a set of flags for choosing the network to operate on
// (privnet/mainnet/testnet).
2020-10-21 14:38:35 +00:00
var Network = [ ] cli . Flag {
2023-05-12 13:53:57 +00:00
cli . BoolFlag { Name : "privnet, p" , Usage : "use private network configuration (if --config-file option is not specified)" } ,
cli . BoolFlag { Name : "mainnet, m" , Usage : "use mainnet network configuration (if --config-file option is not specified)" } ,
cli . BoolFlag { Name : "testnet, t" , Usage : "use testnet network configuration (if --config-file option is not specified)" } ,
2020-10-21 14:38:35 +00:00
cli . BoolFlag { Name : "unittest" , Hidden : true } ,
}
2020-06-17 18:13:37 +00:00
2020-06-17 21:15:13 +00:00
// 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 {
2020-06-18 06:43:37 +00:00
Name : "timeout, s" ,
2022-03-25 08:50:55 +00:00
Value : DefaultTimeout ,
Usage : "Timeout for the operation" ,
2020-06-17 21:15:13 +00:00
} ,
}
2022-09-08 16:05:32 +00:00
// Historic is a flag for commands that can perform historic invocations.
var Historic = cli . StringFlag {
Name : "historic" ,
Usage : "Use historic state (height, block hash or state root hash)" ,
}
2022-10-03 12:05:34 +00:00
// Config is a flag for commands that use node configuration.
var Config = cli . StringFlag {
Name : "config-path" ,
2023-05-12 13:53:57 +00:00
Usage : "path to directory with per-network configuration files (may be overridden by --config-file option for the configuration file)" ,
}
// ConfigFile is a flag for commands that use node configuration and provide
// path to the specific config file instead of config path.
var ConfigFile = cli . StringFlag {
Name : "config-file" ,
Usage : "path to the node configuration file (overrides --config-path option)" ,
2022-10-03 12:05:34 +00:00
}
2023-11-23 17:41:50 +00:00
// RelativePath is a flag for commands that use node configuration and provide
// a prefix to all relative paths in config files.
var RelativePath = cli . StringFlag {
Name : "relative-path" ,
Usage : "a prefix to all relative paths in the node configuration file" ,
}
2022-10-03 12:05:40 +00:00
// Debug is a flag for commands that allow node in debug mode usage.
var Debug = cli . BoolFlag {
Name : "debug, d" ,
2022-12-05 11:58:16 +00:00
Usage : "enable debug logging (LOTS of output, overrides configuration)" ,
2022-10-03 12:05:40 +00:00
}
2020-06-17 21:15:13 +00:00
var errNoEndpoint = errors . New ( "no RPC endpoint specified, use option '--" + RPCEndpointFlag + "' or '-r'" )
2022-09-08 16:05:32 +00:00
var errInvalidHistoric = errors . New ( "invalid 'historic' parameter, neither a block number, nor a block/state hash" )
2023-12-04 14:02:44 +00:00
var errNoWallet = errors . New ( "no wallet parameter found, specify it with the '--wallet' or '-w' flag or specify wallet config file with the '--wallet-config' flag" )
var errConflictingWalletFlags = errors . New ( "--wallet flag conflicts with --wallet-config flag, please, provide one of them to specify wallet location" )
2020-06-17 21:15:13 +00:00
2020-06-17 18:13:37 +00:00
// 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 {
var net = netmode . PrivNet
if ctx . Bool ( "testnet" ) {
net = netmode . TestNet
}
if ctx . Bool ( "mainnet" ) {
net = netmode . MainNet
}
2020-08-31 09:42:42 +00:00
if ctx . Bool ( "unittest" ) {
net = netmode . UnitTestNet
}
2020-06-17 18:13:37 +00:00
return net
}
2020-06-17 21:15:13 +00:00
2022-04-20 18:30:09 +00:00
// GetTimeoutContext returns a context.Context with the default or a user-set timeout.
2020-06-17 21:15:13 +00:00
func GetTimeoutContext ( ctx * cli . Context ) ( context . Context , func ( ) ) {
dur := ctx . Duration ( "timeout" )
if dur == 0 {
dur = DefaultTimeout
}
2023-12-28 11:58:38 +00:00
if ! ctx . IsSet ( "timeout" ) && ctx . Bool ( "await" ) {
dur = DefaultAwaitableTimeout
}
2020-06-17 21:15:13 +00:00
return context . WithTimeout ( context . Background ( ) , dur )
}
// GetRPCClient returns an RPC client instance for the given Context.
2022-07-21 19:39:53 +00:00
func GetRPCClient ( gctx context . Context , ctx * cli . Context ) ( * rpcclient . Client , cli . ExitCoder ) {
2020-06-17 21:15:13 +00:00
endpoint := ctx . String ( RPCEndpointFlag )
if len ( endpoint ) == 0 {
return nil , cli . NewExitError ( errNoEndpoint , 1 )
}
2022-07-21 19:39:53 +00:00
c , err := rpcclient . New ( gctx , endpoint , rpcclient . Options { } )
2020-10-14 15:13:20 +00:00
if err != nil {
return nil , cli . NewExitError ( err , 1 )
}
err = c . Init ( )
2020-06-17 21:15:13 +00:00
if err != nil {
return nil , cli . NewExitError ( err , 1 )
}
return c , nil
}
2022-09-08 16:05:32 +00:00
// GetInvoker returns an invoker using the given RPC client, context and signers.
// It parses "--historic" parameter to adjust it.
func GetInvoker ( c * rpcclient . Client , ctx * cli . Context , signers [ ] transaction . Signer ) ( * invoker . Invoker , cli . ExitCoder ) {
historic := ctx . String ( "historic" )
if historic == "" {
return invoker . New ( c , signers ) , nil
}
if index , err := strconv . ParseUint ( historic , 10 , 32 ) ; err == nil {
return invoker . NewHistoricAtHeight ( uint32 ( index ) , c , signers ) , nil
}
if u256 , err := util . Uint256DecodeStringLE ( historic ) ; err == nil {
// Might as well be a block hash, but it makes no practical difference.
return invoker . NewHistoricWithState ( u256 , c , signers ) , nil
}
return nil , cli . NewExitError ( errInvalidHistoric , 1 )
}
// GetRPCWithInvoker combines GetRPCClient with GetInvoker for cases where it's
// appropriate to do so.
func GetRPCWithInvoker ( gctx context . Context , ctx * cli . Context , signers [ ] transaction . Signer ) ( * rpcclient . Client , * invoker . Invoker , cli . ExitCoder ) {
c , err := GetRPCClient ( gctx , ctx )
if err != nil {
return nil , nil , err
}
inv , err := GetInvoker ( c , ctx , signers )
if err != nil {
c . Close ( )
return nil , nil , err
}
return c , inv , err
}
2022-10-03 12:05:34 +00:00
// GetConfigFromContext looks at the path and the mode flags in the given config and
// returns an appropriate config.
func GetConfigFromContext ( ctx * cli . Context ) ( config . Config , error ) {
2023-11-23 17:41:50 +00:00
var (
configFile = ctx . String ( "config-file" )
relativePath = ctx . String ( "relative-path" )
)
2023-05-12 13:53:57 +00:00
if len ( configFile ) != 0 {
2023-11-23 17:41:50 +00:00
return config . LoadFile ( configFile , relativePath )
2023-05-12 13:53:57 +00:00
}
var configPath = "./config"
2022-10-03 12:05:34 +00:00
if argCp := ctx . String ( "config-path" ) ; argCp != "" {
configPath = argCp
}
2023-11-23 17:41:50 +00:00
return config . Load ( configPath , GetNetwork ( ctx ) , relativePath )
2022-10-03 12:05:34 +00:00
}
2022-10-07 11:51:51 +00:00
var (
// _winfileSinkRegistered denotes whether zap has registered
// user-supplied factory for all sinks with `winfile`-prefixed scheme.
_winfileSinkRegistered bool
_winfileSinkCloser func ( ) error
)
// HandleLoggingParams reads logging parameters.
// If a user selected debug level -- function enables it.
// If logPath is configured -- function creates a dir and a file for logging.
// If logPath is configured on Windows -- function returns closer to be
// able to close sink for the opened log output file.
2022-12-05 12:43:55 +00:00
func HandleLoggingParams ( debug bool , cfg config . ApplicationConfiguration ) ( * zap . Logger , * zap . AtomicLevel , func ( ) error , error ) {
2022-12-05 11:58:16 +00:00
var (
level = zapcore . InfoLevel
err error
)
if len ( cfg . LogLevel ) > 0 {
level , err = zapcore . ParseLevel ( cfg . LogLevel )
if err != nil {
2022-12-05 12:43:55 +00:00
return nil , nil , nil , fmt . Errorf ( "log setting: %w" , err )
2022-12-05 11:58:16 +00:00
}
}
2022-10-07 11:51:51 +00:00
if debug {
level = zapcore . DebugLevel
}
cc := zap . NewProductionConfig ( )
cc . DisableCaller = true
cc . DisableStacktrace = true
cc . EncoderConfig . EncodeDuration = zapcore . StringDurationEncoder
cc . EncoderConfig . EncodeLevel = zapcore . CapitalLevelEncoder
cc . EncoderConfig . EncodeTime = zapcore . ISO8601TimeEncoder
cc . Encoding = "console"
cc . Level = zap . NewAtomicLevelAt ( level )
cc . Sampling = nil
if logPath := cfg . LogPath ; logPath != "" {
if err := io . MakeDirForFile ( logPath , "logger" ) ; err != nil {
2022-12-05 12:43:55 +00:00
return nil , nil , nil , err
2022-10-07 11:51:51 +00:00
}
if runtime . GOOS == "windows" {
if ! _winfileSinkRegistered {
// See https://github.com/uber-go/zap/issues/621.
err := zap . RegisterSink ( "winfile" , func ( u * url . URL ) ( zap . Sink , error ) {
if u . User != nil {
return nil , fmt . Errorf ( "user and password not allowed with file URLs: got %v" , u )
}
if u . Fragment != "" {
return nil , fmt . Errorf ( "fragments not allowed with file URLs: got %v" , u )
}
if u . RawQuery != "" {
return nil , fmt . Errorf ( "query parameters not allowed with file URLs: got %v" , u )
}
// Error messages are better if we check hostname and port separately.
if u . Port ( ) != "" {
return nil , fmt . Errorf ( "ports not allowed with file URLs: got %v" , u )
}
if hn := u . Hostname ( ) ; hn != "" && hn != "localhost" {
return nil , fmt . Errorf ( "file URLs must leave host empty or use localhost: got %v" , u )
}
switch u . Path {
case "stdout" :
return os . Stdout , nil
case "stderr" :
return os . Stderr , nil
}
f , err := os . OpenFile ( u . Path [ 1 : ] , // Remove leading slash left after url.Parse.
os . O_WRONLY | os . O_APPEND | os . O_CREATE , 0644 )
_winfileSinkCloser = func ( ) error {
_winfileSinkCloser = nil
return f . Close ( )
}
return f , err
} )
if err != nil {
2022-12-05 12:43:55 +00:00
return nil , nil , nil , fmt . Errorf ( "failed to register windows-specific sinc: %w" , err )
2022-10-07 11:51:51 +00:00
}
_winfileSinkRegistered = true
}
logPath = "winfile:///" + logPath
}
cc . OutputPaths = [ ] string { logPath }
}
log , err := cc . Build ( )
2022-12-05 12:43:55 +00:00
return log , & cc . Level , _winfileSinkCloser , err
2022-10-07 11:51:51 +00:00
}
2023-12-04 14:02:44 +00:00
2023-12-07 07:35:37 +00:00
// GetRPCWithActor returns an RPC client instance and Actor instance for the given context.
func GetRPCWithActor ( gctx context . Context , ctx * cli . Context , signers [ ] actor . SignerAccount ) ( * rpcclient . Client , * actor . Actor , cli . ExitCoder ) {
c , err := GetRPCClient ( gctx , ctx )
if err != nil {
return nil , nil , err
}
a , actorErr := actor . New ( c , signers )
if actorErr != nil {
c . Close ( )
return nil , nil , cli . NewExitError ( fmt . Errorf ( "failed to create Actor: %w" , actorErr ) , 1 )
}
return c , a , nil
}
2023-12-04 14:02:44 +00:00
// GetAccFromContext returns account and wallet from context. If address is not set, default address is used.
func GetAccFromContext ( ctx * cli . Context ) ( * wallet . Account , * wallet . Wallet , error ) {
var addr util . Uint160
wPath := ctx . String ( "wallet" )
walletConfigPath := ctx . String ( "wallet-config" )
if len ( wPath ) != 0 && len ( walletConfigPath ) != 0 {
return nil , nil , errConflictingWalletFlags
}
if len ( wPath ) == 0 && len ( walletConfigPath ) == 0 {
return nil , nil , errNoWallet
}
var pass * string
if len ( walletConfigPath ) != 0 {
cfg , err := ReadWalletConfig ( walletConfigPath )
if err != nil {
return nil , nil , err
}
wPath = cfg . Path
pass = & cfg . Password
}
wall , err := wallet . NewWalletFromFile ( wPath )
if err != nil {
return nil , nil , err
}
addrFlag := ctx . Generic ( "address" ) . ( * flags . Address )
if addrFlag . IsSet {
addr = addrFlag . Uint160 ( )
} else {
addr = wall . GetChangeAddress ( )
if addr . Equals ( util . Uint160 { } ) {
return nil , wall , errors . New ( "can't get default address" )
}
}
acc , err := GetUnlockedAccount ( wall , addr , pass )
return acc , wall , err
}
// GetUnlockedAccount returns account from wallet, address and uses pass to unlock specified account if given.
// If the password is not given, then it is requested from user.
func GetUnlockedAccount ( wall * wallet . Wallet , addr util . Uint160 , pass * string ) ( * wallet . Account , error ) {
acc := wall . GetAccount ( addr )
if acc == nil {
return nil , fmt . Errorf ( "wallet contains no account for '%s'" , address . Uint160ToString ( addr ) )
}
if acc . CanSign ( ) || acc . EncryptedWIF == "" {
return acc , nil
}
if pass == nil {
rawPass , err := input . ReadPassword (
fmt . Sprintf ( "Enter account %s password > " , address . Uint160ToString ( addr ) ) )
if err != nil {
return nil , fmt . Errorf ( "Error reading password: %w" , err )
}
trimmed := strings . TrimRight ( string ( rawPass ) , "\n" )
pass = & trimmed
}
err := acc . Decrypt ( * pass , wall . Scrypt )
if err != nil {
return nil , err
}
return acc , nil
}
// ReadWalletConfig reads wallet config from the given path.
func ReadWalletConfig ( configPath string ) ( * config . Wallet , error ) {
file , err := os . Open ( configPath )
if err != nil {
return nil , err
}
defer file . Close ( )
configData , err := os . ReadFile ( configPath )
if err != nil {
return nil , fmt . Errorf ( "unable to read wallet config: %w" , err )
}
cfg := & config . Wallet { }
err = yaml . Unmarshal ( configData , & cfg )
if err != nil {
return nil , fmt . Errorf ( "failed to unmarshal wallet config YAML: %w" , err )
}
return cfg , nil
}