cmd: migrate to urfave/cli/v2 (#24751)

This change updates our urfave/cli dependency to the v2 branch of the library.
There are some Go API changes in cli v2:

- Flag values can now be accessed using the methods ctx.Bool,
  ctx.Int, ctx.String, ... regardless of whether the flag is 'local' or
  'global'.

- v2 has built-in support for flag categories. Our home-grown category
  system is removed and the categories of flags are assigned as part of
  the flag definition.

For users, there is only one observable difference with cli v2: flags must now
strictly appear before regular arguments. For example, the following command is
now invalid:

   geth account import mykey.json --password file.txt

Instead, the command must be invoked as follows:

   geth account import --password file.txt mykey.json
This commit is contained in:
willian.eth
2022-06-27 18:22:36 +02:00
committed by GitHub
parent 119f955686
commit 52ed3570c4
61 changed files with 2353 additions and 2320 deletions
+78 -134
View File
@@ -30,7 +30,6 @@ import (
"os/signal"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
@@ -55,7 +54,7 @@ import (
"github.com/ethereum/go-ethereum/signer/storage"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"gopkg.in/urfave/cli.v1"
"github.com/urfave/cli/v2"
)
const legalWarning = `
@@ -73,70 +72,70 @@ PURPOSE. See the GNU General Public License for more details.
`
var (
logLevelFlag = cli.IntFlag{
logLevelFlag = &cli.IntFlag{
Name: "loglevel",
Value: 4,
Usage: "log level to emit to the screen",
}
advancedMode = cli.BoolFlag{
advancedMode = &cli.BoolFlag{
Name: "advanced",
Usage: "If enabled, issues warnings instead of rejections for suspicious requests. Default off",
}
acceptFlag = cli.BoolFlag{
acceptFlag = &cli.BoolFlag{
Name: "suppress-bootwarn",
Usage: "If set, does not show the warning during boot",
}
keystoreFlag = cli.StringFlag{
keystoreFlag = &cli.StringFlag{
Name: "keystore",
Value: filepath.Join(node.DefaultDataDir(), "keystore"),
Usage: "Directory for the keystore",
}
configdirFlag = cli.StringFlag{
configdirFlag = &cli.StringFlag{
Name: "configdir",
Value: DefaultConfigDir(),
Usage: "Directory for Clef configuration",
}
chainIdFlag = cli.Int64Flag{
chainIdFlag = &cli.Int64Flag{
Name: "chainid",
Value: params.MainnetChainConfig.ChainID.Int64(),
Usage: "Chain id to use for signing (1=mainnet, 3=Ropsten, 4=Rinkeby, 5=Goerli)",
}
rpcPortFlag = cli.IntFlag{
Name: "http.port",
Usage: "HTTP-RPC server listening port",
Value: node.DefaultHTTPPort + 5,
rpcPortFlag = &cli.IntFlag{
Name: "http.port",
Usage: "HTTP-RPC server listening port",
Value: node.DefaultHTTPPort + 5,
Category: flags.APICategory,
}
signerSecretFlag = cli.StringFlag{
signerSecretFlag = &cli.StringFlag{
Name: "signersecret",
Usage: "A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash",
}
customDBFlag = cli.StringFlag{
customDBFlag = &cli.StringFlag{
Name: "4bytedb-custom",
Usage: "File used for writing new 4byte-identifiers submitted via API",
Value: "./4byte-custom.json",
}
auditLogFlag = cli.StringFlag{
auditLogFlag = &cli.StringFlag{
Name: "auditlog",
Usage: "File used to emit audit logs. Set to \"\" to disable",
Value: "audit.log",
}
ruleFlag = cli.StringFlag{
ruleFlag = &cli.StringFlag{
Name: "rules",
Usage: "Path to the rule file to auto-authorize requests with",
}
stdiouiFlag = cli.BoolFlag{
stdiouiFlag = &cli.BoolFlag{
Name: "stdio-ui",
Usage: "Use STDIN/STDOUT as a channel for an external UI. " +
"This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " +
"interface, and can be used when Clef is started by an external process.",
}
testFlag = cli.BoolFlag{
testFlag = &cli.BoolFlag{
Name: "stdio-ui-test",
Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.",
}
app = cli.NewApp()
initCommand = cli.Command{
Action: utils.MigrateFlags(initializeSecrets),
initCommand = &cli.Command{
Action: initializeSecrets,
Name: "init",
Usage: "Initialize the signer, generate secret storage",
ArgsUsage: "",
@@ -148,8 +147,8 @@ var (
The init command generates a master seed which Clef can use to store credentials and data needed for
the rule-engine to work.`,
}
attestCommand = cli.Command{
Action: utils.MigrateFlags(attestFile),
attestCommand = &cli.Command{
Action: attestFile,
Name: "attest",
Usage: "Attest that a js-file is to be used",
ArgsUsage: "<sha256sum>",
@@ -165,8 +164,8 @@ incoming requests.
Whenever you make an edit to the rule file, you need to use attestation to tell
Clef that the file is 'safe' to execute.`,
}
setCredentialCommand = cli.Command{
Action: utils.MigrateFlags(setCredential),
setCredentialCommand = &cli.Command{
Action: setCredential,
Name: "setpw",
Usage: "Store a credential for a keystore file",
ArgsUsage: "<address>",
@@ -178,8 +177,8 @@ Clef that the file is 'safe' to execute.`,
Description: `
The setpw command stores a password for a given address (keyfile).
`}
delCredentialCommand = cli.Command{
Action: utils.MigrateFlags(removeCredential),
delCredentialCommand = &cli.Command{
Action: removeCredential,
Name: "delpw",
Usage: "Remove a credential for a keystore file",
ArgsUsage: "<address>",
@@ -191,8 +190,8 @@ The setpw command stores a password for a given address (keyfile).
Description: `
The delpw command removes a password for a given address (keyfile).
`}
newAccountCommand = cli.Command{
Action: utils.MigrateFlags(newAccount),
newAccountCommand = &cli.Command{
Action: newAccount,
Name: "newaccount",
Usage: "Create a new account",
ArgsUsage: "",
@@ -207,7 +206,7 @@ The newaccount command creates a new keystore-backed account. It is a convenienc
which can be used in lieu of an external UI.`,
}
gendocCommand = cli.Command{
gendocCommand = &cli.Command{
Action: GenDoc,
Name: "gendoc",
Usage: "Generate documentation about json-rpc format",
@@ -216,39 +215,16 @@ The gendoc generates example structures of the json-rpc communication types.
`}
)
// AppHelpFlagGroups is the application flags, grouped by functionality.
var AppHelpFlagGroups = []flags.FlagGroup{
{
Name: "FLAGS",
Flags: []cli.Flag{
logLevelFlag,
keystoreFlag,
configdirFlag,
chainIdFlag,
utils.LightKDFFlag,
utils.NoUSBFlag,
utils.SmartCardDaemonPathFlag,
utils.HTTPListenAddrFlag,
utils.HTTPVirtualHostsFlag,
utils.IPCDisabledFlag,
utils.IPCPathFlag,
utils.HTTPEnabledFlag,
rpcPortFlag,
signerSecretFlag,
customDBFlag,
auditLogFlag,
ruleFlag,
stdiouiFlag,
testFlag,
advancedMode,
acceptFlag,
},
},
}
var (
// Git SHA1 commit hash of the release (set via linker flags)
gitCommit = ""
gitDate = ""
app = flags.NewApp(gitCommit, gitDate, "Manage Ethereum account operations")
)
func init() {
app.Name = "Clef"
app.Usage = "Manage Ethereum account operations"
app.Flags = []cli.Flag{
logLevelFlag,
keystoreFlag,
@@ -273,46 +249,12 @@ func init() {
acceptFlag,
}
app.Action = signer
app.Commands = []cli.Command{initCommand,
app.Commands = []*cli.Command{initCommand,
attestCommand,
setCredentialCommand,
delCredentialCommand,
newAccountCommand,
gendocCommand}
cli.CommandHelpTemplate = flags.CommandHelpTemplate
// Override the default app help template
cli.AppHelpTemplate = flags.ClefAppHelpTemplate
// Override the default app help printer, but only for the global app help
originalHelpPrinter := cli.HelpPrinter
cli.HelpPrinter = func(w io.Writer, tmpl string, data interface{}) {
if tmpl == flags.ClefAppHelpTemplate {
// Render out custom usage screen
originalHelpPrinter(w, tmpl, flags.HelpData{App: data, FlagGroups: AppHelpFlagGroups})
} else if tmpl == flags.CommandHelpTemplate {
// Iterate over all command specific flags and categorize them
categorized := make(map[string][]cli.Flag)
for _, flag := range data.(cli.Command).Flags {
if _, ok := categorized[flag.String()]; !ok {
categorized[flags.FlagCategory(flag, AppHelpFlagGroups)] = append(categorized[flags.FlagCategory(flag, AppHelpFlagGroups)], flag)
}
}
// sort to get a stable ordering
sorted := make([]flags.FlagGroup, 0, len(categorized))
for cat, flgs := range categorized {
sorted = append(sorted, flags.FlagGroup{Name: cat, Flags: flgs})
}
sort.Sort(flags.ByCategory(sorted))
// add sorted array to data and render with default printer
originalHelpPrinter(w, tmpl, map[string]interface{}{
"cmd": data,
"categorizedFlags": sorted,
})
} else {
originalHelpPrinter(w, tmpl, data)
}
gendocCommand,
}
}
@@ -329,7 +271,7 @@ func initializeSecrets(c *cli.Context) error {
return err
}
// Ensure the master key does not yet exist, we're not willing to overwrite
configDir := c.GlobalString(configdirFlag.Name)
configDir := c.String(configdirFlag.Name)
if err := os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) {
return err
}
@@ -347,7 +289,7 @@ func initializeSecrets(c *cli.Context) error {
return fmt.Errorf("failed to read enough random")
}
n, p := keystore.StandardScryptN, keystore.StandardScryptP
if c.GlobalBool(utils.LightKDFFlag.Name) {
if c.Bool(utils.LightKDFFlag.Name) {
n, p = keystore.LightScryptN, keystore.LightScryptP
}
text := "The master seed of clef will be locked with a password.\nPlease specify a password. Do not forget this password!"
@@ -390,8 +332,9 @@ You should treat 'masterseed.json' with utmost secrecy and make a backup of it!
`)
return nil
}
func attestFile(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
if ctx.NArg() < 1 {
utils.Fatalf("This command requires an argument.")
}
if err := initialize(ctx); err != nil {
@@ -402,7 +345,7 @@ func attestFile(ctx *cli.Context) error {
if err != nil {
utils.Fatalf(err.Error())
}
configDir := ctx.GlobalString(configdirFlag.Name)
configDir := ctx.String(configdirFlag.Name)
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
confKey := crypto.Keccak256([]byte("config"), stretchedKey)
@@ -415,7 +358,7 @@ func attestFile(ctx *cli.Context) error {
}
func setCredential(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
if ctx.NArg() < 1 {
utils.Fatalf("This command requires an address to be passed as an argument")
}
if err := initialize(ctx); err != nil {
@@ -433,7 +376,7 @@ func setCredential(ctx *cli.Context) error {
if err != nil {
utils.Fatalf(err.Error())
}
configDir := ctx.GlobalString(configdirFlag.Name)
configDir := ctx.String(configdirFlag.Name)
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
@@ -445,7 +388,7 @@ func setCredential(ctx *cli.Context) error {
}
func removeCredential(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
if ctx.NArg() < 1 {
utils.Fatalf("This command requires an address to be passed as an argument")
}
if err := initialize(ctx); err != nil {
@@ -461,7 +404,7 @@ func removeCredential(ctx *cli.Context) error {
if err != nil {
utils.Fatalf(err.Error())
}
configDir := ctx.GlobalString(configdirFlag.Name)
configDir := ctx.String(configdirFlag.Name)
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
@@ -481,8 +424,8 @@ func newAccount(c *cli.Context) error {
var (
ui = core.NewCommandlineUI()
pwStorage storage.Storage = &storage.NoStorage{}
ksLoc = c.GlobalString(keystoreFlag.Name)
lightKdf = c.GlobalBool(utils.LightKDFFlag.Name)
ksLoc = c.String(keystoreFlag.Name)
lightKdf = c.Bool(utils.LightKDFFlag.Name)
)
log.Info("Starting clef", "keystore", ksLoc, "light-kdf", lightKdf)
am := core.StartClefAccountManager(ksLoc, true, lightKdf, "")
@@ -500,13 +443,13 @@ func newAccount(c *cli.Context) error {
func initialize(c *cli.Context) error {
// Set up the logger to print everything
logOutput := os.Stdout
if c.GlobalBool(stdiouiFlag.Name) {
if c.Bool(stdiouiFlag.Name) {
logOutput = os.Stderr
// If using the stdioui, we can't do the 'confirm'-flow
if !c.GlobalBool(acceptFlag.Name) {
if !c.Bool(acceptFlag.Name) {
fmt.Fprint(logOutput, legalWarning)
}
} else if !c.GlobalBool(acceptFlag.Name) {
} else if !c.Bool(acceptFlag.Name) {
if !confirm(legalWarning) {
return fmt.Errorf("aborted by user")
}
@@ -545,8 +488,8 @@ func ipcEndpoint(ipcPath, datadir string) string {
func signer(c *cli.Context) error {
// If we have some unrecognized command, bail out
if args := c.Args(); len(args) > 0 {
return fmt.Errorf("invalid command: %q", args[0])
if c.NArg() > 0 {
return fmt.Errorf("invalid command: %q", c.Args().First())
}
if err := initialize(c); err != nil {
return err
@@ -554,7 +497,7 @@ func signer(c *cli.Context) error {
var (
ui core.UIClientAPI
)
if c.GlobalBool(stdiouiFlag.Name) {
if c.Bool(stdiouiFlag.Name) {
log.Info("Using stdin/stdout as UI-channel")
ui = core.NewStdIOUI()
} else {
@@ -562,7 +505,7 @@ func signer(c *cli.Context) error {
ui = core.NewCommandlineUI()
}
// 4bytedb data
fourByteLocal := c.GlobalString(customDBFlag.Name)
fourByteLocal := c.String(customDBFlag.Name)
db, err := fourbyte.NewWithFile(fourByteLocal)
if err != nil {
utils.Fatalf(err.Error())
@@ -574,7 +517,7 @@ func signer(c *cli.Context) error {
api core.ExternalAPI
pwStorage storage.Storage = &storage.NoStorage{}
)
configDir := c.GlobalString(configdirFlag.Name)
configDir := c.String(configdirFlag.Name)
if stretchedKey, err := readMasterKey(c, ui); err != nil {
log.Warn("Failed to open master, rules disabled", "err", err)
} else {
@@ -591,7 +534,7 @@ func signer(c *cli.Context) error {
configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey)
// Do we have a rule-file?
if ruleFile := c.GlobalString(ruleFlag.Name); ruleFile != "" {
if ruleFile := c.String(ruleFlag.Name); ruleFile != "" {
ruleJS, err := os.ReadFile(ruleFile)
if err != nil {
log.Warn("Could not load rules, disabling", "file", ruleFile, "err", err)
@@ -615,12 +558,12 @@ func signer(c *cli.Context) error {
}
}
var (
chainId = c.GlobalInt64(chainIdFlag.Name)
ksLoc = c.GlobalString(keystoreFlag.Name)
lightKdf = c.GlobalBool(utils.LightKDFFlag.Name)
advanced = c.GlobalBool(advancedMode.Name)
nousb = c.GlobalBool(utils.NoUSBFlag.Name)
scpath = c.GlobalString(utils.SmartCardDaemonPathFlag.Name)
chainId = c.Int64(chainIdFlag.Name)
ksLoc = c.String(keystoreFlag.Name)
lightKdf = c.Bool(utils.LightKDFFlag.Name)
advanced = c.Bool(advancedMode.Name)
nousb = c.Bool(utils.NoUSBFlag.Name)
scpath = c.String(utils.SmartCardDaemonPathFlag.Name)
)
log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc,
"light-kdf", lightKdf, "advanced", advanced)
@@ -632,7 +575,7 @@ func signer(c *cli.Context) error {
ui.RegisterUIServer(core.NewUIServerAPI(apiImpl))
api = apiImpl
// Audit logging
if logfile := c.GlobalString(auditLogFlag.Name); logfile != "" {
if logfile := c.String(auditLogFlag.Name); logfile != "" {
api, err = core.NewAuditLogger(logfile, api)
if err != nil {
utils.Fatalf(err.Error())
@@ -650,9 +593,9 @@ func signer(c *cli.Context) error {
Service: api,
Version: "1.0"},
}
if c.GlobalBool(utils.HTTPEnabledFlag.Name) {
vhosts := utils.SplitAndTrim(c.GlobalString(utils.HTTPVirtualHostsFlag.Name))
cors := utils.SplitAndTrim(c.GlobalString(utils.HTTPCORSDomainFlag.Name))
if c.Bool(utils.HTTPEnabledFlag.Name) {
vhosts := utils.SplitAndTrim(c.String(utils.HTTPVirtualHostsFlag.Name))
cors := utils.SplitAndTrim(c.String(utils.HTTPCORSDomainFlag.Name))
srv := rpc.NewServer()
err := node.RegisterApis(rpcAPI, []string{"account"}, srv)
@@ -665,7 +608,7 @@ func signer(c *cli.Context) error {
port := c.Int(rpcPortFlag.Name)
// start http server
httpEndpoint := fmt.Sprintf("%s:%d", c.GlobalString(utils.HTTPListenAddrFlag.Name), port)
httpEndpoint := fmt.Sprintf("%s:%d", c.String(utils.HTTPListenAddrFlag.Name), port)
httpServer, addr, err := node.StartHTTPEndpoint(httpEndpoint, rpc.DefaultHTTPTimeouts, handler)
if err != nil {
utils.Fatalf("Could not start RPC api: %v", err)
@@ -679,8 +622,8 @@ func signer(c *cli.Context) error {
log.Info("HTTP endpoint closed", "url", extapiURL)
}()
}
if !c.GlobalBool(utils.IPCDisabledFlag.Name) {
givenPath := c.GlobalString(utils.IPCPathFlag.Name)
if !c.Bool(utils.IPCDisabledFlag.Name) {
givenPath := c.String(utils.IPCPathFlag.Name)
ipcapiURL = ipcEndpoint(filepath.Join(givenPath, "clef.ipc"), configDir)
listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI)
if err != nil {
@@ -693,7 +636,7 @@ func signer(c *cli.Context) error {
}()
}
if c.GlobalBool(testFlag.Name) {
if c.Bool(testFlag.Name) {
log.Info("Performing UI test")
go testExternalUI(apiImpl)
}
@@ -719,7 +662,7 @@ func signer(c *cli.Context) error {
// persistence requirements.
func DefaultConfigDir() string {
// Try to place the data folder in the user's home dir
home := utils.HomeDir()
home := flags.HomeDir()
if home != "" {
if runtime.GOOS == "darwin" {
return filepath.Join(home, "Library", "Signer")
@@ -739,10 +682,10 @@ func DefaultConfigDir() string {
func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) {
var (
file string
configDir = ctx.GlobalString(configdirFlag.Name)
configDir = ctx.String(configdirFlag.Name)
)
if ctx.GlobalIsSet(signerSecretFlag.Name) {
file = ctx.GlobalString(signerSecretFlag.Name)
if ctx.IsSet(signerSecretFlag.Name) {
file = ctx.String(signerSecretFlag.Name)
} else {
file = filepath.Join(configDir, "masterseed.json")
}
@@ -995,7 +938,7 @@ func decryptSeed(keyjson []byte, auth string) ([]byte, error) {
}
// GenDoc outputs examples of all structures used in json-rpc communication
func GenDoc(ctx *cli.Context) {
func GenDoc(ctx *cli.Context) error {
var (
a = common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef")
@@ -1145,4 +1088,5 @@ These data types are defined in the channel between clef and the UI`)
for _, elem := range output {
fmt.Println(elem)
}
return nil
}