github.com/prysmaticlabs/prysm@v1.4.4/validator/accounts/wallet_recover.go (about) 1 package accounts 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "sort" 9 "strconv" 10 "strings" 11 12 "github.com/pkg/errors" 13 "github.com/prysmaticlabs/prysm/cmd/validator/flags" 14 "github.com/prysmaticlabs/prysm/shared/promptutil" 15 "github.com/prysmaticlabs/prysm/validator/accounts/prompt" 16 "github.com/prysmaticlabs/prysm/validator/accounts/wallet" 17 "github.com/prysmaticlabs/prysm/validator/keymanager" 18 "github.com/prysmaticlabs/prysm/validator/keymanager/derived" 19 "github.com/tyler-smith/go-bip39" 20 "github.com/tyler-smith/go-bip39/wordlists" 21 "github.com/urfave/cli/v2" 22 ) 23 24 const ( 25 phraseWordCount = 24 26 newMnemonicPassphraseYesNoText = "(Advanced) Do you want to setup a '25th word' passphrase for your mnemonic? [y/n]" 27 newMnemonicPassphrasePromptText = "(Advanced) Setup a passphrase '25th word' for your mnemonic " + 28 "(WARNING: You cannot recover your keys from your mnemonic if you forget this passphrase!)" 29 mnemonicPassphraseYesNoText = "(Advanced) Do you have an optional '25th word' passphrase for your mnemonic? [y/n]" 30 mnemonicPassphrasePromptText = "(Advanced) Enter the '25th word' passphrase for your mnemonic" 31 ) 32 33 // RecoverWalletConfig to run the recover wallet function. 34 type RecoverWalletConfig struct { 35 WalletDir string 36 WalletPassword string 37 Mnemonic string 38 NumAccounts int 39 Mnemonic25thWord string 40 } 41 42 // RecoverWalletCli uses a menmonic seed phrase to recover a wallet into the path provided. This 43 // uses the CLI to extract necessary values to run the function. 44 func RecoverWalletCli(cliCtx *cli.Context) error { 45 mnemonic, err := inputMnemonic(cliCtx) 46 if err != nil { 47 return errors.Wrap(err, "could not get mnemonic phrase") 48 } 49 config := &RecoverWalletConfig{ 50 Mnemonic: mnemonic, 51 } 52 skipMnemonic25thWord := cliCtx.IsSet(flags.SkipMnemonic25thWordCheckFlag.Name) 53 has25thWordFile := cliCtx.IsSet(flags.Mnemonic25thWordFileFlag.Name) 54 if !skipMnemonic25thWord && !has25thWordFile { 55 resp, err := promptutil.ValidatePrompt( 56 os.Stdin, mnemonicPassphraseYesNoText, promptutil.ValidateYesOrNo, 57 ) 58 if err != nil { 59 return errors.Wrap(err, "could not validate choice") 60 } 61 if strings.EqualFold(resp, "y") { 62 mnemonicPassphrase, err := promptutil.InputPassword( 63 cliCtx, 64 flags.Mnemonic25thWordFileFlag, 65 mnemonicPassphrasePromptText, 66 "Confirm mnemonic passphrase", 67 false, /* Should confirm password */ 68 func(input string) error { 69 if strings.TrimSpace(input) == "" { 70 return errors.New("input cannot be empty") 71 } 72 return nil 73 }, 74 ) 75 if err != nil { 76 return err 77 } 78 config.Mnemonic25thWord = mnemonicPassphrase 79 } 80 } 81 walletDir, err := prompt.InputDirectory(cliCtx, prompt.WalletDirPromptText, flags.WalletDirFlag) 82 if err != nil { 83 return err 84 } 85 walletPassword, err := promptutil.InputPassword( 86 cliCtx, 87 flags.WalletPasswordFileFlag, 88 wallet.NewWalletPasswordPromptText, 89 wallet.ConfirmPasswordPromptText, 90 true, /* Should confirm password */ 91 promptutil.ValidatePasswordInput, 92 ) 93 if err != nil { 94 return err 95 } 96 numAccounts, err := inputNumAccounts(cliCtx) 97 if err != nil { 98 return errors.Wrap(err, "could not get number of accounts to recover") 99 } 100 config.WalletDir = walletDir 101 config.WalletPassword = walletPassword 102 config.NumAccounts = int(numAccounts) 103 if _, err = RecoverWallet(cliCtx.Context, config); err != nil { 104 return err 105 } 106 log.Infof( 107 "Successfully recovered HD wallet with accounts and saved configuration to disk", 108 ) 109 return nil 110 } 111 112 // RecoverWallet uses a menmonic seed phrase to recover a wallet into the path provided. 113 func RecoverWallet(ctx context.Context, cfg *RecoverWalletConfig) (*wallet.Wallet, error) { 114 // Ensure that the wallet directory does not contain a wallet already 115 dirExists, err := wallet.Exists(cfg.WalletDir) 116 if err != nil { 117 return nil, err 118 } 119 if dirExists { 120 return nil, errors.New("a wallet already exists at this location. Please input an" + 121 " alternative location for the new wallet or remove the current wallet") 122 } 123 w := wallet.New(&wallet.Config{ 124 WalletDir: cfg.WalletDir, 125 KeymanagerKind: keymanager.Derived, 126 WalletPassword: cfg.WalletPassword, 127 }) 128 if err := w.SaveWallet(); err != nil { 129 return nil, errors.Wrap(err, "could not save wallet to disk") 130 } 131 km, err := derived.NewKeymanager(ctx, &derived.SetupConfig{ 132 Wallet: w, 133 ListenForChanges: false, 134 }) 135 if err != nil { 136 return nil, errors.Wrap(err, "could not make keymanager for given phrase") 137 } 138 if err := km.RecoverAccountsFromMnemonic(ctx, cfg.Mnemonic, cfg.Mnemonic25thWord, cfg.NumAccounts); err != nil { 139 return nil, err 140 } 141 log.WithField("wallet-path", w.AccountsDir()).Infof( 142 "Successfully recovered HD wallet with %d accounts. Please use `accounts list` to view details for your accounts", 143 cfg.NumAccounts, 144 ) 145 return w, nil 146 } 147 148 func inputMnemonic(cliCtx *cli.Context) (mnemonicPhrase string, err error) { 149 if cliCtx.IsSet(flags.MnemonicFileFlag.Name) { 150 mnemonicFilePath := cliCtx.String(flags.MnemonicFileFlag.Name) 151 data, err := ioutil.ReadFile(mnemonicFilePath) 152 if err != nil { 153 return "", err 154 } 155 enteredMnemonic := string(data) 156 if err := ValidateMnemonic(enteredMnemonic); err != nil { 157 return "", errors.Wrap(err, "mnemonic phrase did not pass validation") 158 } 159 return enteredMnemonic, nil 160 } 161 allowedLanguages := map[string][]string{ 162 "chinese_simplified": wordlists.ChineseSimplified, 163 "chinese_traditional": wordlists.ChineseTraditional, 164 "czech": wordlists.Czech, 165 "english": wordlists.English, 166 "french": wordlists.French, 167 "japanese": wordlists.Japanese, 168 "korean": wordlists.Korean, 169 "italian": wordlists.Italian, 170 "spanish": wordlists.Spanish, 171 } 172 languages := make([]string, 0) 173 for k := range allowedLanguages { 174 languages = append(languages, k) 175 } 176 sort.Strings(languages) 177 selectedLanguage, err := promptutil.ValidatePrompt( 178 os.Stdin, 179 fmt.Sprintf("Enter the language of your seed phrase: %s", strings.Join(languages, ", ")), 180 func(input string) error { 181 if _, ok := allowedLanguages[input]; !ok { 182 return errors.New("input not in the list of allowed languages") 183 } 184 return nil 185 }, 186 ) 187 if err != nil { 188 return "", fmt.Errorf("could not get mnemonic language: %w", err) 189 } 190 bip39.SetWordList(allowedLanguages[selectedLanguage]) 191 mnemonicPhrase, err = promptutil.ValidatePrompt( 192 os.Stdin, 193 "Enter the seed phrase for the wallet you would like to recover", 194 ValidateMnemonic) 195 if err != nil { 196 return "", fmt.Errorf("could not get mnemonic phrase: %w", err) 197 } 198 return mnemonicPhrase, nil 199 } 200 201 func inputNumAccounts(cliCtx *cli.Context) (int64, error) { 202 if cliCtx.IsSet(flags.NumAccountsFlag.Name) { 203 numAccounts := cliCtx.Int64(flags.NumAccountsFlag.Name) 204 if numAccounts <= 0 { 205 return 0, errors.New("must recover at least 1 account") 206 } 207 return numAccounts, nil 208 } 209 numAccounts, err := promptutil.ValidatePrompt(os.Stdin, "Enter how many accounts you would like to generate from the mnemonic", promptutil.ValidateNumber) 210 if err != nil { 211 return 0, err 212 } 213 numAccountsInt, err := strconv.Atoi(numAccounts) 214 if err != nil { 215 return 0, err 216 } 217 if numAccountsInt <= 0 { 218 return 0, errors.New("must recover at least 1 account") 219 } 220 return int64(numAccountsInt), nil 221 } 222 223 // ValidateMnemonic ensures that it is not empty and that the count of the words are 224 // as specified(currently 24). 225 func ValidateMnemonic(mnemonic string) error { 226 if strings.Trim(mnemonic, " ") == "" { 227 return errors.New("phrase cannot be empty") 228 } 229 words := strings.Split(mnemonic, " ") 230 for i, word := range words { 231 if strings.Trim(word, " ") == "" { 232 words = append(words[:i], words[i+1:]...) 233 } 234 } 235 if len(words) != phraseWordCount { 236 return fmt.Errorf("phrase must be %d words, entered %d", phraseWordCount, len(words)) 237 } 238 return nil 239 }