From 6e84110a757020a7e7c8cef1330462af74b094c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= Date: Thu, 19 Oct 2023 11:16:10 +0200 Subject: [PATCH 1/3] feat(cmd/wallet): Refactor wallet cmd into separate package --- cmd/root.go | 3 +- cmd/wallet.go | 399 ------------------------------------- cmd/wallet/create.go | 65 ++++++ cmd/wallet/export.go | 27 +++ cmd/wallet/import.go | 87 ++++++++ cmd/wallet/list.go | 43 ++++ cmd/wallet/remoteSigner.go | 70 +++++++ cmd/wallet/remove.go | 47 +++++ cmd/wallet/rename.go | 29 +++ cmd/wallet/set_default.go | 23 +++ cmd/wallet/show.go | 35 ++++ cmd/wallet/wallet.go | 58 ++++++ 12 files changed, 486 insertions(+), 400 deletions(-) delete mode 100644 cmd/wallet.go create mode 100644 cmd/wallet/create.go create mode 100644 cmd/wallet/export.go create mode 100644 cmd/wallet/import.go create mode 100644 cmd/wallet/list.go create mode 100644 cmd/wallet/remoteSigner.go create mode 100644 cmd/wallet/remove.go create mode 100644 cmd/wallet/rename.go create mode 100644 cmd/wallet/set_default.go create mode 100644 cmd/wallet/show.go create mode 100644 cmd/wallet/wallet.go diff --git a/cmd/root.go b/cmd/root.go index 7f2b6c7e..c324b2a0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,6 +13,7 @@ import ( "github.com/oasisprotocol/cli/cmd/account" "github.com/oasisprotocol/cli/cmd/network" "github.com/oasisprotocol/cli/cmd/paratime" + "github.com/oasisprotocol/cli/cmd/wallet" "github.com/oasisprotocol/cli/config" "github.com/oasisprotocol/cli/version" _ "github.com/oasisprotocol/cli/wallet/file" // Register file wallet backend. @@ -98,7 +99,7 @@ func init() { rootCmd.AddCommand(network.Cmd) rootCmd.AddCommand(paratime.Cmd) - rootCmd.AddCommand(walletCmd) + rootCmd.AddCommand(wallet.Cmd) rootCmd.AddCommand(account.Cmd) rootCmd.AddCommand(addressBookCmd) rootCmd.AddCommand(contractCmd) diff --git a/cmd/wallet.go b/cmd/wallet.go deleted file mode 100644 index 5641c6f4..00000000 --- a/cmd/wallet.go +++ /dev/null @@ -1,399 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "sort" - "strings" - - "github.com/AlecAivazis/survey/v2" - "github.com/spf13/cobra" - flag "github.com/spf13/pflag" - - "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" - "github.com/oasisprotocol/oasis-core/go/common/crypto/signature/signers/remote" - "github.com/oasisprotocol/oasis-core/go/common/grpc" - "github.com/oasisprotocol/oasis-core/go/common/identity" - "github.com/oasisprotocol/oasis-core/go/common/logging" - cmdBackground "github.com/oasisprotocol/oasis-core/go/oasis-node/cmd/common/background" - - "github.com/oasisprotocol/cli/cmd/common" - "github.com/oasisprotocol/cli/config" - "github.com/oasisprotocol/cli/table" - "github.com/oasisprotocol/cli/wallet" - walletFile "github.com/oasisprotocol/cli/wallet/file" -) - -var ( - accKind string - - walletCmd = &cobra.Command{ - Use: "wallet", - Short: "Manage accounts in the local wallet", - Aliases: []string{"w"}, - } - - walletListCmd = &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List configured accounts", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cfg := config.Global() - table := table.New() - table.SetHeader([]string{"Account", "Kind", "Address"}) - - var output [][]string - for name, acc := range cfg.Wallet.All { - if cfg.Wallet.Default == name { - name += common.DefaultMarker - } - output = append(output, []string{ - name, - acc.PrettyKind(), - acc.Address, - }) - } - - // Sort output by name. - sort.Slice(output, func(i, j int) bool { - return output[i][0] < output[j][0] - }) - - table.AppendBulk(output) - table.Render() - }, - } - - walletCreateCmd = &cobra.Command{ - Use: "create ", - Short: "Create a new account", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - cfg := config.Global() - name := args[0] - - af, err := wallet.Load(accKind) - cobra.CheckErr(err) - - // Ask for passphrase to encrypt the wallet with. - var passphrase string - if af.RequiresPassphrase() { - passphrase = common.AskNewPassphrase() - } - - accCfg := &config.Account{ - Kind: accKind, - } - err = accCfg.SetConfigFromFlags() - cobra.CheckErr(err) - - if _, exists := cfg.AddressBook.All[name]; exists { - cobra.CheckErr(fmt.Errorf("address named '%s' already exists in address book", name)) - } - err = cfg.Wallet.Create(name, passphrase, accCfg) - cobra.CheckErr(err) - - err = cfg.Save() - cobra.CheckErr(err) - }, - } - - walletShowCmd = &cobra.Command{ - Use: "show ", - Short: "Show public account information", - Aliases: []string{"s"}, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - name := args[0] - - acc := common.LoadAccount(config.Global(), name) - showPublicWalletInfo(name, acc) - }, - } - - walletRmCmd = &cobra.Command{ - Use: "remove ", - Aliases: []string{"rm"}, - Short: "Remove an existing account", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - cfg := config.Global() - name := args[0] - - // Early check for whether the wallet exists so that we don't ask for confirmation first. - if _, exists := cfg.Wallet.All[name]; !exists { - cobra.CheckErr(fmt.Errorf("account '%s' does not exist", name)) - } - - fmt.Printf("WARNING: Removing the account will ERASE secret key material!\n") - fmt.Printf("WARNING: THIS ACTION IS IRREVERSIBLE!\n") - - var result string - confirmText := fmt.Sprintf("I really want to remove account %s", name) - prompt := &survey.Input{ - Message: fmt.Sprintf("Enter '%s' (without quotes) to confirm removal:", confirmText), - } - err := survey.AskOne(prompt, &result) - cobra.CheckErr(err) - - if result != confirmText { - cobra.CheckErr("Aborted.") - } - - err = cfg.Wallet.Remove(name) - cobra.CheckErr(err) - - err = cfg.Save() - cobra.CheckErr(err) - }, - } - - walletRenameCmd = &cobra.Command{ - Use: "rename ", - Short: "Rename an existing account", - Aliases: []string{"mv"}, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - cfg := config.Global() - oldName, newName := args[0], args[1] - - if _, exists := cfg.AddressBook.All[newName]; exists { - cobra.CheckErr(fmt.Errorf("address named '%s' already exists in the address book", newName)) - } - err := cfg.Wallet.Rename(oldName, newName) - cobra.CheckErr(err) - - err = cfg.Save() - cobra.CheckErr(err) - }, - } - - walletSetDefaultCmd = &cobra.Command{ - Use: "set-default ", - Short: "Sets the given account as the default account", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - cfg := config.Global() - name := args[0] - - err := cfg.Wallet.SetDefault(name) - cobra.CheckErr(err) - - err = cfg.Save() - cobra.CheckErr(err) - }, - } - - walletImportCmd = &cobra.Command{ - Use: "import ", - Short: "Import an existing account", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - cfg := config.Global() - name := args[0] - - if _, exists := cfg.Wallet.All[name]; exists { - cobra.CheckErr(fmt.Errorf("account '%s' already exists in the wallet", name)) - } - if _, exists := cfg.AddressBook.All[name]; exists { - cobra.CheckErr(fmt.Errorf("address named '%s' already exists in the address book", name)) - } - - // NOTE: We only support importing into the file-based wallet for now. - af, err := wallet.Load(walletFile.Kind) - cobra.CheckErr(err) - - // Ask for import kind. - var supportedKinds []string - for _, kind := range af.SupportedImportKinds() { - supportedKinds = append(supportedKinds, string(kind)) - } - - var kindRaw string - err = survey.AskOne(&survey.Select{ - Message: "Kind:", - Options: supportedKinds, - }, &kindRaw) - cobra.CheckErr(err) - - var kind wallet.ImportKind - err = kind.UnmarshalText([]byte(kindRaw)) - cobra.CheckErr(err) - - // Ask for wallet configuration. - afCfg, err := af.GetConfigFromSurvey(&kind) - cobra.CheckErr(err) - - // Ask for import data. - var answers struct { - Data string - } - questions := []*survey.Question{ - { - Name: "data", - Prompt: af.DataPrompt(kind, afCfg), - Validate: af.DataValidator(kind, afCfg), - }, - } - err = survey.Ask(questions, &answers) - cobra.CheckErr(err) - - // Ask for passphrase. - passphrase := common.AskNewPassphrase() - - accCfg := &config.Account{ - Kind: af.Kind(), - Config: afCfg, - } - src := &wallet.ImportSource{ - Kind: kind, - Data: answers.Data, - } - - err = cfg.Wallet.Import(name, passphrase, accCfg, src) - cobra.CheckErr(err) - - err = cfg.Save() - cobra.CheckErr(err) - }, - } - - walletExportCmd = &cobra.Command{ - Use: "export ", - Short: "Export secret account information", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - name := args[0] - - fmt.Printf("WARNING: Exporting the account will expose secret key material!\n") - acc := common.LoadAccount(config.Global(), name) - - showPublicWalletInfo(name, acc) - - fmt.Printf("Export:\n") - fmt.Println(acc.UnsafeExport()) - }, - } - - walletRemoteSignerCmd = &cobra.Command{ - Use: "remote-signer ", - Short: "Act as a oasis-node remote entity signer over AF_LOCAL", - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - name, socketPath := args[0], args[1] - - acc := common.LoadAccount(config.Global(), name) - - sf := &accountEntitySignerFactory{ - signer: acc.ConsensusSigner(), - } - if sf.signer == nil { - cobra.CheckErr("account not compatible with consensus layer usage") - } - - // The domain separation is entirely handled on the client side. - signature.UnsafeAllowUnregisteredContexts() - - // Suppress oasis-core logging. - err := logging.Initialize( - nil, - logging.FmtLogfmt, - logging.LevelInfo, - nil, - ) - cobra.CheckErr(err) - - // Setup the gRPC service. - srvCfg := &grpc.ServerConfig{ - Name: "remote-signer", - Path: socketPath, // XXX: Maybe fix this up to be nice. - Identity: &identity.Identity{}, - } - srv, err := grpc.NewServer(srvCfg) - cobra.CheckErr(err) - remote.RegisterService(srv.Server(), sf) - - // Start the service and wait for graceful termination. - err = srv.Start() - cobra.CheckErr(err) - - fmt.Printf("Address: %s\n", acc.Address()) - fmt.Printf("Node Args:\n --signer.backend=remote \\\n --signer.remote.address=unix:%s\n", socketPath) - fmt.Printf("\n*** REMOTE SIGNER READY ***\n") - - sm := cmdBackground.NewServiceManager(logging.GetLogger("remote-signer")) - sm.Register(srv) - defer sm.Cleanup() - sm.Wait() - }, - } -) - -type accountEntitySignerFactory struct { - signer signature.Signer -} - -func (sf *accountEntitySignerFactory) EnsureRole( - role signature.SignerRole, -) error { - if role != signature.SignerEntity { - return signature.ErrInvalidRole - } - return nil -} - -func (sf *accountEntitySignerFactory) Generate( - _ signature.SignerRole, - _ io.Reader, -) (signature.Signer, error) { - // The remote signer should never require this. - return nil, fmt.Errorf("refusing to generate new signing keys") -} - -func (sf *accountEntitySignerFactory) Load( - role signature.SignerRole, -) (signature.Signer, error) { - if err := sf.EnsureRole(role); err != nil { - return nil, err - } - return sf.signer, nil -} - -func showPublicWalletInfo(name string, wallet wallet.Account) { - fmt.Printf("Name: %s\n", name) - if signer := wallet.Signer(); signer != nil { - fmt.Printf("Public Key: %s\n", signer.Public()) - } - if ethAddr := wallet.EthAddress(); ethAddr != nil { - fmt.Printf("Ethereum address: %s\n", ethAddr.Hex()) - } - fmt.Printf("Native address: %s\n", wallet.Address()) -} - -func init() { - walletCmd.AddCommand(walletListCmd) - - walletFlags := flag.NewFlagSet("", flag.ContinueOnError) - kinds := make([]string, 0, len(wallet.AvailableKinds())) - for _, w := range wallet.AvailableKinds() { - kinds = append(kinds, w.Kind()) - } - walletFlags.StringVar(&accKind, "kind", "file", fmt.Sprintf("Account kind [%s]", strings.Join(kinds, ", "))) - - // TODO: Group flags in usage by tweaking the usage template/function. - for _, af := range wallet.AvailableKinds() { - walletFlags.AddFlagSet(af.Flags()) - } - - walletCreateCmd.Flags().AddFlagSet(walletFlags) - - walletCmd.AddCommand(walletCreateCmd) - walletCmd.AddCommand(walletShowCmd) - walletCmd.AddCommand(walletRmCmd) - walletCmd.AddCommand(walletRenameCmd) - walletCmd.AddCommand(walletSetDefaultCmd) - walletCmd.AddCommand(walletImportCmd) - walletCmd.AddCommand(walletExportCmd) - walletCmd.AddCommand(walletRemoteSignerCmd) -} diff --git a/cmd/wallet/create.go b/cmd/wallet/create.go new file mode 100644 index 00000000..db709406 --- /dev/null +++ b/cmd/wallet/create.go @@ -0,0 +1,65 @@ +package wallet + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + "github.com/oasisprotocol/cli/cmd/common" + "github.com/oasisprotocol/cli/config" + "github.com/oasisprotocol/cli/wallet" +) + +var accKind string + +var createCmd = &cobra.Command{ + Use: "create ", + Short: "Create a new account", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg := config.Global() + name := args[0] + + af, err := wallet.Load(accKind) + cobra.CheckErr(err) + + // Ask for passphrase to encrypt the wallet with. + var passphrase string + if af.RequiresPassphrase() { + passphrase = common.AskNewPassphrase() + } + + accCfg := &config.Account{ + Kind: accKind, + } + err = accCfg.SetConfigFromFlags() + cobra.CheckErr(err) + + if _, exists := cfg.AddressBook.All[name]; exists { + cobra.CheckErr(fmt.Errorf("address named '%s' already exists in address book", name)) + } + err = cfg.Wallet.Create(name, passphrase, accCfg) + cobra.CheckErr(err) + + err = cfg.Save() + cobra.CheckErr(err) + }, +} + +func init() { + flags := flag.NewFlagSet("", flag.ContinueOnError) + kinds := make([]string, 0, len(wallet.AvailableKinds())) + for _, w := range wallet.AvailableKinds() { + kinds = append(kinds, w.Kind()) + } + flags.StringVar(&accKind, "kind", "file", fmt.Sprintf("Account kind [%s]", strings.Join(kinds, ", "))) + + // TODO: Group flags in usage by tweaking the usage template/function. + for _, af := range wallet.AvailableKinds() { + flags.AddFlagSet(af.Flags()) + } + + createCmd.Flags().AddFlagSet(flags) +} diff --git a/cmd/wallet/export.go b/cmd/wallet/export.go new file mode 100644 index 00000000..67381c12 --- /dev/null +++ b/cmd/wallet/export.go @@ -0,0 +1,27 @@ +package wallet + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/cmd/common" + "github.com/oasisprotocol/cli/config" +) + +var exportCmd = &cobra.Command{ + Use: "export ", + Short: "Export secret account information", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + + fmt.Printf("WARNING: Exporting the account will expose secret key material!\n") + acc := common.LoadAccount(config.Global(), name) + + showPublicWalletInfo(name, acc) + + fmt.Printf("Export:\n") + fmt.Println(acc.UnsafeExport()) + }, +} diff --git a/cmd/wallet/import.go b/cmd/wallet/import.go new file mode 100644 index 00000000..1184236a --- /dev/null +++ b/cmd/wallet/import.go @@ -0,0 +1,87 @@ +package wallet + +import ( + "fmt" + + "github.com/AlecAivazis/survey/v2" + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/cmd/common" + "github.com/oasisprotocol/cli/config" + "github.com/oasisprotocol/cli/wallet" + walletFile "github.com/oasisprotocol/cli/wallet/file" +) + +var importCmd = &cobra.Command{ + Use: "import ", + Short: "Import an existing account", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg := config.Global() + name := args[0] + + if _, exists := cfg.Wallet.All[name]; exists { + cobra.CheckErr(fmt.Errorf("account '%s' already exists in the wallet", name)) + } + if _, exists := cfg.AddressBook.All[name]; exists { + cobra.CheckErr(fmt.Errorf("address named '%s' already exists in the address book", name)) + } + + // NOTE: We only support importing into the file-based wallet for now. + af, err := wallet.Load(walletFile.Kind) + cobra.CheckErr(err) + + // Ask for import kind. + var supportedKinds []string + for _, kind := range af.SupportedImportKinds() { + supportedKinds = append(supportedKinds, string(kind)) + } + + var kindRaw string + err = survey.AskOne(&survey.Select{ + Message: "Kind:", + Options: supportedKinds, + }, &kindRaw) + cobra.CheckErr(err) + + var kind wallet.ImportKind + err = kind.UnmarshalText([]byte(kindRaw)) + cobra.CheckErr(err) + + // Ask for wallet configuration. + afCfg, err := af.GetConfigFromSurvey(&kind) + cobra.CheckErr(err) + + // Ask for import data. + var answers struct { + Data string + } + questions := []*survey.Question{ + { + Name: "data", + Prompt: af.DataPrompt(kind, afCfg), + Validate: af.DataValidator(kind, afCfg), + }, + } + err = survey.Ask(questions, &answers) + cobra.CheckErr(err) + + // Ask for passphrase. + passphrase := common.AskNewPassphrase() + + accCfg := &config.Account{ + Kind: af.Kind(), + Config: afCfg, + } + src := &wallet.ImportSource{ + Kind: kind, + Data: answers.Data, + } + + err = cfg.Wallet.Import(name, passphrase, accCfg, src) + cobra.CheckErr(err) + + err = cfg.Save() + cobra.CheckErr(err) + }, +} diff --git a/cmd/wallet/list.go b/cmd/wallet/list.go new file mode 100644 index 00000000..49b470c0 --- /dev/null +++ b/cmd/wallet/list.go @@ -0,0 +1,43 @@ +package wallet + +import ( + "sort" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/cmd/common" + "github.com/oasisprotocol/cli/config" + "github.com/oasisprotocol/cli/table" +) + +var listCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List configured accounts", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + cfg := config.Global() + table := table.New() + table.SetHeader([]string{"Account", "Kind", "Address"}) + + var output [][]string + for name, acc := range cfg.Wallet.All { + if cfg.Wallet.Default == name { + name += common.DefaultMarker + } + output = append(output, []string{ + name, + acc.PrettyKind(), + acc.Address, + }) + } + + // Sort output by name. + sort.Slice(output, func(i, j int) bool { + return output[i][0] < output[j][0] + }) + + table.AppendBulk(output) + table.Render() + }, +} diff --git a/cmd/wallet/remoteSigner.go b/cmd/wallet/remoteSigner.go new file mode 100644 index 00000000..b020e454 --- /dev/null +++ b/cmd/wallet/remoteSigner.go @@ -0,0 +1,70 @@ +package wallet + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" + "github.com/oasisprotocol/oasis-core/go/common/crypto/signature/signers/remote" + "github.com/oasisprotocol/oasis-core/go/common/grpc" + "github.com/oasisprotocol/oasis-core/go/common/identity" + "github.com/oasisprotocol/oasis-core/go/common/logging" + "github.com/oasisprotocol/oasis-core/go/oasis-node/cmd/common/background" + + "github.com/oasisprotocol/cli/cmd/common" + "github.com/oasisprotocol/cli/config" +) + +var remoteSignerCmd = &cobra.Command{ + Use: "remote-signer ", + Short: "Act as a oasis-node remote entity signer over AF_LOCAL", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + name, socketPath := args[0], args[1] + + acc := common.LoadAccount(config.Global(), name) + + sf := &accountEntitySignerFactory{ + signer: acc.ConsensusSigner(), + } + if sf.signer == nil { + cobra.CheckErr("account not compatible with consensus layer usage") + } + + // The domain separation is entirely handled on the client side. + signature.UnsafeAllowUnregisteredContexts() + + // Suppress oasis-core logging. + err := logging.Initialize( + nil, + logging.FmtLogfmt, + logging.LevelInfo, + nil, + ) + cobra.CheckErr(err) + + // Setup the gRPC service. + srvCfg := &grpc.ServerConfig{ + Name: "remote-signer", + Path: socketPath, // XXX: Maybe fix this up to be nice. + Identity: &identity.Identity{}, + } + srv, err := grpc.NewServer(srvCfg) + cobra.CheckErr(err) + remote.RegisterService(srv.Server(), sf) + + // Start the service and wait for graceful termination. + err = srv.Start() + cobra.CheckErr(err) + + fmt.Printf("Address: %s\n", acc.Address()) + fmt.Printf("Node Args:\n --signer.backend=remote \\\n --signer.remote.address=unix:%s\n", socketPath) + fmt.Printf("\n*** REMOTE SIGNER READY ***\n") + + sm := background.NewServiceManager(logging.GetLogger("remote-signer")) + sm.Register(srv) + defer sm.Cleanup() + sm.Wait() + }, +} diff --git a/cmd/wallet/remove.go b/cmd/wallet/remove.go new file mode 100644 index 00000000..04e6bb34 --- /dev/null +++ b/cmd/wallet/remove.go @@ -0,0 +1,47 @@ +package wallet + +import ( + "fmt" + + "github.com/AlecAivazis/survey/v2" + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/config" +) + +var rmCmd = &cobra.Command{ + Use: "remove ", + Aliases: []string{"rm"}, + Short: "Remove an existing account", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg := config.Global() + name := args[0] + + // Early check for whether the wallet exists so that we don't ask for confirmation first. + if _, exists := cfg.Wallet.All[name]; !exists { + cobra.CheckErr(fmt.Errorf("account '%s' does not exist", name)) + } + + fmt.Printf("WARNING: Removing the account will ERASE secret key material!\n") + fmt.Printf("WARNING: THIS ACTION IS IRREVERSIBLE!\n") + + var result string + confirmText := fmt.Sprintf("I really want to remove account %s", name) + prompt := &survey.Input{ + Message: fmt.Sprintf("Enter '%s' (without quotes) to confirm removal:", confirmText), + } + err := survey.AskOne(prompt, &result) + cobra.CheckErr(err) + + if result != confirmText { + cobra.CheckErr("Aborted.") + } + + err = cfg.Wallet.Remove(name) + cobra.CheckErr(err) + + err = cfg.Save() + cobra.CheckErr(err) + }, +} diff --git a/cmd/wallet/rename.go b/cmd/wallet/rename.go new file mode 100644 index 00000000..f8da4655 --- /dev/null +++ b/cmd/wallet/rename.go @@ -0,0 +1,29 @@ +package wallet + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/config" +) + +var renameCmd = &cobra.Command{ + Use: "rename ", + Short: "Rename an existing account", + Aliases: []string{"mv"}, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + cfg := config.Global() + oldName, newName := args[0], args[1] + + if _, exists := cfg.AddressBook.All[newName]; exists { + cobra.CheckErr(fmt.Errorf("address named '%s' already exists in the address book", newName)) + } + err := cfg.Wallet.Rename(oldName, newName) + cobra.CheckErr(err) + + err = cfg.Save() + cobra.CheckErr(err) + }, +} diff --git a/cmd/wallet/set_default.go b/cmd/wallet/set_default.go new file mode 100644 index 00000000..5121bc05 --- /dev/null +++ b/cmd/wallet/set_default.go @@ -0,0 +1,23 @@ +package wallet + +import ( + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/config" +) + +var setDefaultCmd = &cobra.Command{ + Use: "set-default ", + Short: "Sets the given account as the default account", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg := config.Global() + name := args[0] + + err := cfg.Wallet.SetDefault(name) + cobra.CheckErr(err) + + err = cfg.Save() + cobra.CheckErr(err) + }, +} diff --git a/cmd/wallet/show.go b/cmd/wallet/show.go new file mode 100644 index 00000000..6ce45335 --- /dev/null +++ b/cmd/wallet/show.go @@ -0,0 +1,35 @@ +package wallet + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/cmd/common" + "github.com/oasisprotocol/cli/config" + "github.com/oasisprotocol/cli/wallet" +) + +var showCmd = &cobra.Command{ + Use: "show ", + Short: "Show public account information", + Aliases: []string{"s"}, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + + acc := common.LoadAccount(config.Global(), name) + showPublicWalletInfo(name, acc) + }, +} + +func showPublicWalletInfo(name string, wallet wallet.Account) { + fmt.Printf("Name: %s\n", name) + if signer := wallet.Signer(); signer != nil { + fmt.Printf("Public Key: %s\n", signer.Public()) + } + if ethAddr := wallet.EthAddress(); ethAddr != nil { + fmt.Printf("Ethereum address: %s\n", ethAddr.Hex()) + } + fmt.Printf("Native address: %s\n", wallet.Address()) +} diff --git a/cmd/wallet/wallet.go b/cmd/wallet/wallet.go new file mode 100644 index 00000000..43f80229 --- /dev/null +++ b/cmd/wallet/wallet.go @@ -0,0 +1,58 @@ +package wallet + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" +) + +var Cmd = &cobra.Command{ + Use: "wallet", + Short: "Manage accounts in the local wallet", + Aliases: []string{"w"}, +} + +type accountEntitySignerFactory struct { + signer signature.Signer +} + +func (sf *accountEntitySignerFactory) EnsureRole( + role signature.SignerRole, +) error { + if role != signature.SignerEntity { + return signature.ErrInvalidRole + } + return nil +} + +func (sf *accountEntitySignerFactory) Generate( + _ signature.SignerRole, + _ io.Reader, +) (signature.Signer, error) { + // The remote signer should never require this. + return nil, fmt.Errorf("refusing to generate new signing keys") +} + +func (sf *accountEntitySignerFactory) Load( + role signature.SignerRole, +) (signature.Signer, error) { + if err := sf.EnsureRole(role); err != nil { + return nil, err + } + return sf.signer, nil +} + +func init() { + Cmd.AddCommand(listCmd) + Cmd.AddCommand(createCmd) + Cmd.AddCommand(showCmd) + Cmd.AddCommand(rmCmd) + Cmd.AddCommand(renameCmd) + Cmd.AddCommand(setDefaultCmd) + Cmd.AddCommand(importCmd) + Cmd.AddCommand(exportCmd) + Cmd.AddCommand(remoteSignerCmd) +} From 9883374fb480f793653774372e76cc4fad20cb5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= Date: Thu, 19 Oct 2023 12:16:57 +0200 Subject: [PATCH 2/3] feat(cmd/wallet): Import key from PEM file --- cmd/wallet/create.go | 14 ++- cmd/wallet/import.go | 9 +- cmd/wallet/import_file.go | 88 +++++++++++++++++++ .../{remoteSigner.go => remote_signer.go} | 0 cmd/wallet/wallet.go | 1 + docs/wallet.md | 38 +++++++- 6 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 cmd/wallet/import_file.go rename cmd/wallet/{remoteSigner.go => remote_signer.go} (100%) diff --git a/cmd/wallet/create.go b/cmd/wallet/create.go index db709406..5fdfcf1a 100644 --- a/cmd/wallet/create.go +++ b/cmd/wallet/create.go @@ -22,6 +22,8 @@ var createCmd = &cobra.Command{ cfg := config.Global() name := args[0] + checkAccountExists(cfg, name) + af, err := wallet.Load(accKind) cobra.CheckErr(err) @@ -37,9 +39,6 @@ var createCmd = &cobra.Command{ err = accCfg.SetConfigFromFlags() cobra.CheckErr(err) - if _, exists := cfg.AddressBook.All[name]; exists { - cobra.CheckErr(fmt.Errorf("address named '%s' already exists in address book", name)) - } err = cfg.Wallet.Create(name, passphrase, accCfg) cobra.CheckErr(err) @@ -48,6 +47,15 @@ var createCmd = &cobra.Command{ }, } +func checkAccountExists(cfg *config.Config, name string) { + if _, exists := cfg.Wallet.All[name]; exists { + cobra.CheckErr(fmt.Errorf("account '%s' already exists in the wallet", name)) + } + if _, exists := cfg.AddressBook.All[name]; exists { + cobra.CheckErr(fmt.Errorf("address named '%s' already exists in the address book", name)) + } +} + func init() { flags := flag.NewFlagSet("", flag.ContinueOnError) kinds := make([]string, 0, len(wallet.AvailableKinds())) diff --git a/cmd/wallet/import.go b/cmd/wallet/import.go index 1184236a..19ec77d4 100644 --- a/cmd/wallet/import.go +++ b/cmd/wallet/import.go @@ -1,8 +1,6 @@ package wallet import ( - "fmt" - "github.com/AlecAivazis/survey/v2" "github.com/spf13/cobra" @@ -20,12 +18,7 @@ var importCmd = &cobra.Command{ cfg := config.Global() name := args[0] - if _, exists := cfg.Wallet.All[name]; exists { - cobra.CheckErr(fmt.Errorf("account '%s' already exists in the wallet", name)) - } - if _, exists := cfg.AddressBook.All[name]; exists { - cobra.CheckErr(fmt.Errorf("address named '%s' already exists in the address book", name)) - } + checkAccountExists(cfg, name) // NOTE: We only support importing into the file-based wallet for now. af, err := wallet.Load(walletFile.Kind) diff --git a/cmd/wallet/import_file.go b/cmd/wallet/import_file.go new file mode 100644 index 00000000..453c987c --- /dev/null +++ b/cmd/wallet/import_file.go @@ -0,0 +1,88 @@ +package wallet + +import ( + "encoding/base64" + "encoding/hex" + "encoding/pem" + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/cmd/common" + "github.com/oasisprotocol/cli/config" + "github.com/oasisprotocol/cli/wallet" + walletFile "github.com/oasisprotocol/cli/wallet/file" +) + +var importFileCmd = &cobra.Command{ + Use: "import-file ", + Short: "Import an existing account from file", + Long: "Import the private key from an existing PEM file", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + cfg := config.Global() + name := args[0] + filename := args[1] + + checkAccountExists(cfg, name) + + rawFile, err := os.ReadFile(filename) + cobra.CheckErr(err) + + block, _ := pem.Decode(rawFile) + if block == nil { //nolint: staticcheck + cobra.CheckErr(fmt.Errorf("failed to decode PEM file")) + } + + algorithm, err := detectAlgorithm(block.Type) //nolint: staticcheck + cobra.CheckErr(err) + + // Ask for passphrase. + passphrase := common.AskNewPassphrase() + + accCfg := &config.Account{ + Kind: walletFile.Kind, + Config: map[string]interface{}{ + "algorithm": algorithm, + }, + } + + src := &wallet.ImportSource{ + Kind: wallet.ImportKindPrivateKey, + Data: encodeKeyData(algorithm, block.Bytes), //nolint: staticcheck + } + + err = cfg.Wallet.Import(name, passphrase, accCfg, src) + cobra.CheckErr(err) + + err = cfg.Save() + cobra.CheckErr(err) + }, +} + +// detectAlgorithm detects the key type based on the PEM type. +func detectAlgorithm(pemType string) (string, error) { + switch pemType { + case "ED25519 PRIVATE KEY": + return wallet.AlgorithmEd25519Raw, nil + case "EC PRIVATE KEY": + return wallet.AlgorithmSecp256k1Raw, nil + case "SR25519 PRIVATE KEY": + return wallet.AlgorithmSr25519Raw, nil + } + + return "", fmt.Errorf("unsupported PEM type: %s", pemType) +} + +// encodeKeyData re-encodes the key in raw bytes back to the user-readable string for import. +func encodeKeyData(algorithm string, rawKey []byte) string { + switch algorithm { + case wallet.AlgorithmEd25519Raw, wallet.AlgorithmSr25519Raw: + return base64.StdEncoding.EncodeToString(rawKey) + case wallet.AlgorithmSecp256k1Raw: + return hex.EncodeToString(rawKey) + } + + return "" +} diff --git a/cmd/wallet/remoteSigner.go b/cmd/wallet/remote_signer.go similarity index 100% rename from cmd/wallet/remoteSigner.go rename to cmd/wallet/remote_signer.go diff --git a/cmd/wallet/wallet.go b/cmd/wallet/wallet.go index 43f80229..8594f0fd 100644 --- a/cmd/wallet/wallet.go +++ b/cmd/wallet/wallet.go @@ -53,6 +53,7 @@ func init() { Cmd.AddCommand(renameCmd) Cmd.AddCommand(setDefaultCmd) Cmd.AddCommand(importCmd) + Cmd.AddCommand(importFileCmd) Cmd.AddCommand(exportCmd) Cmd.AddCommand(remoteSignerCmd) } diff --git a/docs/wallet.md b/docs/wallet.md index f96673c9..ff32eb13 100644 --- a/docs/wallet.md +++ b/docs/wallet.md @@ -20,15 +20,19 @@ Oasis CLI for your accounts: used for accounts living on EVM-compatible ParaTimes such as Sapphire or Emerald. The same account can be imported into Metamask and other Ethereum wallets. +- `ed25519-raw`: [Ed25519] keypair imported directly from the Base64-encoded + private key. No key derivation is involved. This setting is primarily used by + the network validators to sign the governance and other consensus-layer + transactions. - `ed25519-legacy`: [Ed25519] keypair using a legacy 5-component derivation path. This is the preferred setting for Oasis accounts stored on a hardware wallet like Ledger. It is called legacy, because it was first implemented before the [ADR-8] was standardized. - `sr25519-adr8`: [Sr25519] keypair using the [ADR-8] derivation path. This is an alternative signature scheme for signing ParaTime transactions. -- `ed25519-raw`, `secp256k1-raw` and `sr25519-raw`: Respective Ed25519, - Secp256k1 and Sr25519 keypairs imported directly from Base32 or Hex-encoded - private keys. No key derivation is involved. +- `secp256k1-raw` and `sr25519-raw`: Respective Secp256k1 and Sr25519 keypairs + imported directly from the Hex- or Base64-encoded private key. No key + derivation is involved. :::tip @@ -358,6 +362,34 @@ name of the desired default account. ## Advanced +### Import an Existing Keypair from PEM file {#import-file} + +Existing node operators may already use their Ed25519 private key for running +their nodes stored in a PEM-encoded file typically named `entity.pem`. In order +to submit their governance transaction, for example to vote on the network +upgrade using the Oasis CLI, they need to import the key into the Oasis CLI +wallet: + +```shell +oasis wallet import-file my_entity entity.pem +``` + +``` +? Choose a new passphrase: +? Repeat passphrase: +``` + +The key is now safely stored and encrypted inside the Oasis CLI. + +```shell +oasis wallet list +``` + +``` +ACCOUNT KIND ADDRESS +my_entity file (ed25519-raw) oasis1qpe0vnm0ahczgc353vytvtz9r829le4pjux8lc5z +``` + ### Remote Signer for `oasis-node` {#remote-signer} You can bind the account in your Oasis CLI wallet with a local instance of From 3f33600f14e4d7d717ad45a437309e0227a623c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= Date: Thu, 19 Oct 2023 12:50:34 +0200 Subject: [PATCH 3/3] fix(docs/network): Add npa-selector notices --- docs/network.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/network.md b/docs/network.md index 0b95d24a..19345b05 100644 --- a/docs/network.md +++ b/docs/network.md @@ -138,6 +138,13 @@ proposal was created and when it closes and a state. ![code](../examples/network-governance/list.out) +:::info + +[Network](./account.md#npa) selector is available for the +`governance list` command. + +::: + #### `show` {#governance-show} `network governance show ` shows detailed information on @@ -154,6 +161,13 @@ respond. If you encounter timeouts, consider setting up your own gRPC endpoint! ::: +:::info + +[Network](./account.md#npa) selector is available for the +`governance show` command. + +::: + #### `cast-vote` {#governance-cast-vote} `network governance cast-vote { yes | no | abstain }` is used @@ -164,6 +178,13 @@ to submit your vote on the governance proposal. The vote can either be `yes`, ![code](../examples/network-governance/cast-vote.out.static) +:::info + +[Network and account](./account.md#npa) selectors are available for the +`governance cast-vote` command. + +::: + #### `create-proposal` {#governance-create-proposal} To submit a new governance proposal use `network governance create-proposal`. @@ -174,6 +195,13 @@ The following proposal types are currently supported: - `cancel-upgrade `: Cancel network proposed upgrade. Provide the ID of the network upgrade proposal you wish to cancel. +:::info + +[Network and account](./account.md#npa) selectors are available for all +`governance create-proposal` subcommands. + +::: + ### Show Network Properties {#show} `network show` shows the network property stored in the registry, scheduler, @@ -182,6 +210,13 @@ genesis document or on chain. By passing `--height ` with a block number, you can obtain a historic value of the property. +:::info + +[Network](./account.md#npa) selector is available for the +`network show` command. + +::: + The command expects one of the following parameters: #### `entities` {#show-entities} @@ -319,3 +354,10 @@ Mainnet was reported: ![code shell](../examples/network/status.in.static) ![code json](../examples/network/status.out.static) + +:::info + +[Network](./account.md#npa) selector is available for the +`network status` command. + +:::