mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2024-12-26 05:17:22 +00:00
ac4fe2db5a
* added Czech from go-bip39 * go mod tidy Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> Co-authored-by: Preston Van Loon <preston@prysmaticlabs.com>
240 lines
7.7 KiB
Go
240 lines
7.7 KiB
Go
package accounts
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/prysmaticlabs/prysm/cmd/validator/flags"
|
|
"github.com/prysmaticlabs/prysm/shared/promptutil"
|
|
"github.com/prysmaticlabs/prysm/validator/accounts/prompt"
|
|
"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
|
|
"github.com/prysmaticlabs/prysm/validator/keymanager"
|
|
"github.com/prysmaticlabs/prysm/validator/keymanager/derived"
|
|
"github.com/tyler-smith/go-bip39"
|
|
"github.com/tyler-smith/go-bip39/wordlists"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
const (
|
|
phraseWordCount = 24
|
|
newMnemonicPassphraseYesNoText = "(Advanced) Do you want to setup a '25th word' passphrase for your mnemonic? [y/n]"
|
|
newMnemonicPassphrasePromptText = "(Advanced) Setup a passphrase '25th word' for your mnemonic " +
|
|
"(WARNING: You cannot recover your keys from your mnemonic if you forget this passphrase!)"
|
|
mnemonicPassphraseYesNoText = "(Advanced) Do you have an optional '25th word' passphrase for your mnemonic? [y/n]"
|
|
mnemonicPassphrasePromptText = "(Advanced) Enter the '25th word' passphrase for your mnemonic"
|
|
)
|
|
|
|
// RecoverWalletConfig to run the recover wallet function.
|
|
type RecoverWalletConfig struct {
|
|
WalletDir string
|
|
WalletPassword string
|
|
Mnemonic string
|
|
NumAccounts int
|
|
Mnemonic25thWord string
|
|
}
|
|
|
|
// RecoverWalletCli uses a menmonic seed phrase to recover a wallet into the path provided. This
|
|
// uses the CLI to extract necessary values to run the function.
|
|
func RecoverWalletCli(cliCtx *cli.Context) error {
|
|
mnemonic, err := inputMnemonic(cliCtx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not get mnemonic phrase")
|
|
}
|
|
config := &RecoverWalletConfig{
|
|
Mnemonic: mnemonic,
|
|
}
|
|
skipMnemonic25thWord := cliCtx.IsSet(flags.SkipMnemonic25thWordCheckFlag.Name)
|
|
has25thWordFile := cliCtx.IsSet(flags.Mnemonic25thWordFileFlag.Name)
|
|
if !skipMnemonic25thWord && !has25thWordFile {
|
|
resp, err := promptutil.ValidatePrompt(
|
|
os.Stdin, mnemonicPassphraseYesNoText, promptutil.ValidateYesOrNo,
|
|
)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not validate choice")
|
|
}
|
|
if strings.EqualFold(resp, "y") {
|
|
mnemonicPassphrase, err := promptutil.InputPassword(
|
|
cliCtx,
|
|
flags.Mnemonic25thWordFileFlag,
|
|
mnemonicPassphrasePromptText,
|
|
"Confirm mnemonic passphrase",
|
|
false, /* Should confirm password */
|
|
func(input string) error {
|
|
if strings.TrimSpace(input) == "" {
|
|
return errors.New("input cannot be empty")
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.Mnemonic25thWord = mnemonicPassphrase
|
|
}
|
|
}
|
|
walletDir, err := prompt.InputDirectory(cliCtx, prompt.WalletDirPromptText, flags.WalletDirFlag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
walletPassword, err := promptutil.InputPassword(
|
|
cliCtx,
|
|
flags.WalletPasswordFileFlag,
|
|
wallet.NewWalletPasswordPromptText,
|
|
wallet.ConfirmPasswordPromptText,
|
|
true, /* Should confirm password */
|
|
promptutil.ValidatePasswordInput,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
numAccounts, err := inputNumAccounts(cliCtx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not get number of accounts to recover")
|
|
}
|
|
config.WalletDir = walletDir
|
|
config.WalletPassword = walletPassword
|
|
config.NumAccounts = int(numAccounts)
|
|
if _, err = RecoverWallet(cliCtx.Context, config); err != nil {
|
|
return err
|
|
}
|
|
log.Infof(
|
|
"Successfully recovered HD wallet with accounts and saved configuration to disk",
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// RecoverWallet uses a menmonic seed phrase to recover a wallet into the path provided.
|
|
func RecoverWallet(ctx context.Context, cfg *RecoverWalletConfig) (*wallet.Wallet, error) {
|
|
// Ensure that the wallet directory does not contain a wallet already
|
|
dirExists, err := wallet.Exists(cfg.WalletDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if dirExists {
|
|
return nil, errors.New("a wallet already exists at this location. Please input an" +
|
|
" alternative location for the new wallet or remove the current wallet")
|
|
}
|
|
w := wallet.New(&wallet.Config{
|
|
WalletDir: cfg.WalletDir,
|
|
KeymanagerKind: keymanager.Derived,
|
|
WalletPassword: cfg.WalletPassword,
|
|
})
|
|
if err := w.SaveWallet(); err != nil {
|
|
return nil, errors.Wrap(err, "could not save wallet to disk")
|
|
}
|
|
km, err := derived.NewKeymanager(ctx, &derived.SetupConfig{
|
|
Wallet: w,
|
|
ListenForChanges: false,
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not make keymanager for given phrase")
|
|
}
|
|
if err := km.RecoverAccountsFromMnemonic(ctx, cfg.Mnemonic, cfg.Mnemonic25thWord, cfg.NumAccounts); err != nil {
|
|
return nil, err
|
|
}
|
|
log.WithField("wallet-path", w.AccountsDir()).Infof(
|
|
"Successfully recovered HD wallet with %d accounts. Please use `accounts list` to view details for your accounts",
|
|
cfg.NumAccounts,
|
|
)
|
|
return w, nil
|
|
}
|
|
|
|
func inputMnemonic(cliCtx *cli.Context) (mnemonicPhrase string, err error) {
|
|
if cliCtx.IsSet(flags.MnemonicFileFlag.Name) {
|
|
mnemonicFilePath := cliCtx.String(flags.MnemonicFileFlag.Name)
|
|
data, err := ioutil.ReadFile(mnemonicFilePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
enteredMnemonic := string(data)
|
|
if err := ValidateMnemonic(enteredMnemonic); err != nil {
|
|
return "", errors.Wrap(err, "mnemonic phrase did not pass validation")
|
|
}
|
|
return enteredMnemonic, nil
|
|
}
|
|
allowedLanguages := map[string][]string{
|
|
"chinese_simplified": wordlists.ChineseSimplified,
|
|
"chinese_traditional": wordlists.ChineseTraditional,
|
|
"czech": wordlists.Czech,
|
|
"english": wordlists.English,
|
|
"french": wordlists.French,
|
|
"japanese": wordlists.Japanese,
|
|
"korean": wordlists.Korean,
|
|
"italian": wordlists.Italian,
|
|
"spanish": wordlists.Spanish,
|
|
}
|
|
languages := make([]string, 0)
|
|
for k := range allowedLanguages {
|
|
languages = append(languages, k)
|
|
}
|
|
sort.Strings(languages)
|
|
selectedLanguage, err := promptutil.ValidatePrompt(
|
|
os.Stdin,
|
|
fmt.Sprintf("Enter the language of your seed phrase: %s", strings.Join(languages, ", ")),
|
|
func(input string) error {
|
|
if _, ok := allowedLanguages[input]; !ok {
|
|
return errors.New("input not in the list of allowed languages")
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not get mnemonic language: %w", err)
|
|
}
|
|
bip39.SetWordList(allowedLanguages[selectedLanguage])
|
|
mnemonicPhrase, err = promptutil.ValidatePrompt(
|
|
os.Stdin,
|
|
"Enter the seed phrase for the wallet you would like to recover",
|
|
ValidateMnemonic)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not get mnemonic phrase: %w", err)
|
|
}
|
|
return mnemonicPhrase, nil
|
|
}
|
|
|
|
func inputNumAccounts(cliCtx *cli.Context) (int64, error) {
|
|
if cliCtx.IsSet(flags.NumAccountsFlag.Name) {
|
|
numAccounts := cliCtx.Int64(flags.NumAccountsFlag.Name)
|
|
if numAccounts <= 0 {
|
|
return 0, errors.New("must recover at least 1 account")
|
|
}
|
|
return numAccounts, nil
|
|
}
|
|
numAccounts, err := promptutil.ValidatePrompt(os.Stdin, "Enter how many accounts you would like to generate from the mnemonic", promptutil.ValidateNumber)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
numAccountsInt, err := strconv.Atoi(numAccounts)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if numAccountsInt <= 0 {
|
|
return 0, errors.New("must recover at least 1 account")
|
|
}
|
|
return int64(numAccountsInt), nil
|
|
}
|
|
|
|
// ValidateMnemonic ensures that it is not empty and that the count of the words are
|
|
// as specified(currently 24).
|
|
func ValidateMnemonic(mnemonic string) error {
|
|
if strings.Trim(mnemonic, " ") == "" {
|
|
return errors.New("phrase cannot be empty")
|
|
}
|
|
words := strings.Split(mnemonic, " ")
|
|
for i, word := range words {
|
|
if strings.Trim(word, " ") == "" {
|
|
words = append(words[:i], words[i+1:]...)
|
|
}
|
|
}
|
|
if len(words) != phraseWordCount {
|
|
return fmt.Errorf("phrase must be %d words, entered %d", phraseWordCount, len(words))
|
|
}
|
|
return nil
|
|
}
|