diff --git a/cmd/goal/account.go b/cmd/goal/account.go index 97f6bac9b2..23aa655771 100644 --- a/cmd/goal/account.go +++ b/cmd/goal/account.go @@ -71,6 +71,7 @@ func init() { accountCmd.AddCommand(addParticipationKeyCmd) accountCmd.AddCommand(listParticipationKeysCmd) accountCmd.AddCommand(importCmd) + accountCmd.AddCommand(exportCmd) accountCmd.AddCommand(importRootKeysCmd) accountCmd.AddCommand(accountMultisigCmd) @@ -140,7 +141,9 @@ func init() { // import flags importCmd.Flags().BoolVarP(&importDefault, "default", "f", false, "Set this account as the default one") importCmd.Flags().StringVarP(&mnemonic, "mnemonic", "m", "", "Mnemonic to import (will prompt otherwise)") - + // export flags + exportCmd.Flags().StringVarP(&accountAddress, "address", "a", "", "Address of account to export") + exportCmd.MarkFlagRequired("address") // importRootKeys flags importRootKeysCmd.Flags().BoolVarP(&unencryptedWallet, "unencrypted-wallet", "u", false, "Import into the default unencrypted wallet, potentially creating it") @@ -756,6 +759,7 @@ var listParticipationKeysCmd = &cobra.Command{ var importCmd = &cobra.Command{ Use: "import", Short: "Import an account key from mnemonic", + Long: "Import an account key from a mnemonic generated by the export command or by algokey (NOT a mnemonic from the goal wallet command). The imported account will be listed alongside your wallet-generated accounts, but will not be tied to your wallet.", Run: func(cmd *cobra.Command, args []string) { dataDir := ensureSingleDataDir() accountList := makeAccountsList(dataDir) @@ -810,10 +814,43 @@ var importCmd = &cobra.Command{ }, } +var exportCmd = &cobra.Command{ + Use: "export", + Short: "Export an account key for use with account import", + Long: "Export an account mnemonic seed, for use with account import. This exports the seed for a single account and should not be confused with the wallet mnemonic.", + Run: func(cmd *cobra.Command, args []string) { + dataDir := ensureSingleDataDir() + client := ensureKmdClient(dataDir) + + wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) + passwordString := string(pw) + + response, err := client.ExportKey(wh, passwordString, accountAddress) + + if err != nil { + reportErrorf(errorRequestFail, err) + } + + seed, err := crypto.SecretKeyToSeed(response.PrivateKey) + + if err != nil { + reportErrorf(errorSeedConversion, accountAddress, err) + } + + privKeyAsMnemonic, err := passphrase.KeyToMnemonic(seed[:]) + + if err != nil { + reportErrorf(errorMnemonicConversion, accountAddress, err) + } + + reportInfof(infoExportedKey, accountAddress, privKeyAsMnemonic) + }, +} + var importRootKeysCmd = &cobra.Command{ Use: "importrootkey", Short: "Import .rootkey files from the data directory into a kmd wallet", - Long: "Import .rootkey files from the data directory into a kmd wallet", + Long: "Import .rootkey files from the data directory into a kmd wallet. This is analogous to using the import command with an account seed mnemonic: the imported account will be displayed alongside your wallet-derived accounts, but will not be tied to your wallet mnemonic.", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { dataDir := ensureSingleDataDir() diff --git a/cmd/goal/messages.go b/cmd/goal/messages.go index 309084551c..1a3140cb3c 100644 --- a/cmd/goal/messages.go +++ b/cmd/goal/messages.go @@ -28,6 +28,7 @@ const ( infoNoAccounts = "Did not find any account. Please import or create a new one." infoRenamedAccount = "Renamed account '%s' to '%s'" infoImportedKey = "Imported %s" + infoExportedKey = "Exported key for account %s: \"%s\"" infoImportedNKeys = "Imported %d key%s" infoCreatedNewAccount = "Created new account with address %s" errorNameAlreadyTaken = "The account name '%s' is already taken, please choose another." @@ -40,6 +41,8 @@ const ( warnMultisigDuplicatesDetected = "Warning: one or more duplicate addresses detected in multisig account creation. This will effectively give the duplicated address(es) extra signature weight. Continuing multisig account creation." errLastRoundInvalid = "roundLastValid needs to be well after the current round (%d)" errExistingPartKey = "Account already has a participation key valid at least until roundLastValid (%d) - current is %d" + errorSeedConversion = "Got private key for account %s, but was unable to convert to seed: %s" + errorMnemonicConversion = "Got seed for account %s, but was unable to convert to mnemonic: %s" // KMD infoKMDStopped = "Stopped kmd" diff --git a/cmd/goal/wallet.go b/cmd/goal/wallet.go index 479d638d4d..ad70eff50e 100644 --- a/cmd/goal/wallet.go +++ b/cmd/goal/wallet.go @@ -43,7 +43,7 @@ func init() { walletCmd.Flags().StringVarP(&defaultWalletName, "default", "f", "", "Set the wallet with this name to be the default wallet") // Should we recover the wallet? - newWalletCmd.Flags().BoolVarP(&recoverWallet, "recover", "r", false, "Recover the wallet from a backup mnemonic. Regenerate accounts in the wallet with `goal account new`") + newWalletCmd.Flags().BoolVarP(&recoverWallet, "recover", "r", false, "Recover the wallet from the backup mnemonic provided at wallet creation (NOT the mnemonic provided by goal account export or by algokey). Regenerate accounts in the wallet with `goal account new`") } var walletCmd = &cobra.Command{