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  }