github.com/0xsequence/ethkit@v1.25.0/cmd/ethkit/wallet.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"strings"
    11  	"syscall"
    12  
    13  	"github.com/0xsequence/ethkit/ethwallet"
    14  	"github.com/0xsequence/ethkit/go-ethereum/accounts/keystore"
    15  	"github.com/0xsequence/ethkit/go-ethereum/common"
    16  	"github.com/spf13/cobra"
    17  	"golang.org/x/crypto/ssh/terminal"
    18  )
    19  
    20  func init() {
    21  	wallet := &wallet{}
    22  	cmd := &cobra.Command{
    23  		Use:   "wallet",
    24  		Short: "EOA wallet",
    25  		Run:   wallet.Run,
    26  	}
    27  
    28  	cmd.Flags().String("keyfile", "", "wallet key file path")
    29  	cmd.Flags().Bool("new", false, "create a new wallet and save it to the keyfile")
    30  	cmd.Flags().Bool("print-account", true, "print wallet account address from keyfile")
    31  	cmd.Flags().Bool("print-mnemonic", false, "print wallet secret mnemonic from keyfile (danger!)")
    32  	cmd.Flags().Bool("print-private-key", false, "print wallet private key from keyfile (danger!)")
    33  	cmd.Flags().Bool("import-mnemonic", false, "import a secret mnemonic to a new keyfile")
    34  	cmd.Flags().String("path", "", fmt.Sprintf("set derivation path, default: %s", ethwallet.DefaultWalletOptions.DerivationPath))
    35  
    36  	rootCmd.AddCommand(cmd)
    37  }
    38  
    39  type wallet struct {
    40  	// flags
    41  	fKeyFile         string
    42  	fCreateNew       bool
    43  	fPrintAccount    bool
    44  	fPrintMnemonic   bool
    45  	fPrintPrivateKey bool
    46  	fImportMnemonic  bool
    47  	fPath            string
    48  
    49  	// wallet key file
    50  	keyFile walletKeyFile
    51  	wallet  *ethwallet.Wallet
    52  }
    53  
    54  func (c *wallet) Run(cmd *cobra.Command, args []string) {
    55  	c.fKeyFile, _ = cmd.Flags().GetString("keyfile")
    56  	c.fCreateNew, _ = cmd.Flags().GetBool("new")
    57  	c.fPrintAccount, _ = cmd.Flags().GetBool("print-account")
    58  	c.fPrintMnemonic, _ = cmd.Flags().GetBool("print-mnemonic")
    59  	c.fPrintPrivateKey, _ = cmd.Flags().GetBool("print-private-key")
    60  	c.fImportMnemonic, _ = cmd.Flags().GetBool("import-mnemonic")
    61  	c.fPath, _ = cmd.Flags().GetString("path")
    62  
    63  	if c.fKeyFile == "" {
    64  		fmt.Println("error: please pass --keyfile")
    65  		help(cmd)
    66  		return
    67  	}
    68  	if fileExists(c.fKeyFile) && (c.fCreateNew || c.fImportMnemonic) {
    69  		fmt.Println("error: keyfile already exists on this filename, for safety we do not overwrite existing keyfiles.")
    70  		help(cmd)
    71  		return
    72  	}
    73  	if !c.fCreateNew && !c.fImportMnemonic && !c.fPrintMnemonic && !c.fPrintPrivateKey && !c.fPrintAccount {
    74  		fmt.Println("error: not enough options provided to ethkit cli.")
    75  		help(cmd)
    76  		return
    77  	}
    78  
    79  	// Gen new wallet
    80  	if c.fCreateNew || c.fImportMnemonic {
    81  		if err := c.createNew(); err != nil {
    82  			log.Fatal(err)
    83  		}
    84  		return
    85  	}
    86  
    87  	// Load wallet from the key file
    88  	data, err := os.ReadFile(c.fKeyFile)
    89  	if err != nil {
    90  		log.Fatal(err)
    91  	}
    92  	keyFile := walletKeyFile{}
    93  	err = json.Unmarshal(data, &keyFile)
    94  	if err != nil {
    95  		log.Fatal(err)
    96  	}
    97  	c.keyFile = keyFile
    98  
    99  	derivationPath := c.fPath
   100  	if derivationPath == "" {
   101  		derivationPath = c.keyFile.Path
   102  	}
   103  
   104  	pw, err := readSecretInput("Password: ")
   105  	if err != nil {
   106  		log.Fatal(err)
   107  	}
   108  
   109  	cipherText, err := keystore.DecryptDataV3(c.keyFile.Crypto, string(pw))
   110  	if err != nil {
   111  		log.Fatal(err)
   112  	}
   113  
   114  	wallet, err := ethwallet.NewWalletFromMnemonic(string(cipherText), derivationPath)
   115  	if err != nil {
   116  		log.Fatal(err)
   117  	}
   118  	c.wallet = wallet
   119  
   120  	// Print mnemonic
   121  	if c.fPrintMnemonic {
   122  		if err := c.printMnemonic(); err != nil {
   123  			log.Fatal(err)
   124  		}
   125  		return
   126  	}
   127  
   128  	// Print private key
   129  	if c.fPrintPrivateKey {
   130  		if err := c.printPrivateKey(); err != nil {
   131  			log.Fatal(err)
   132  		}
   133  		return
   134  	}
   135  
   136  	// Print acconut address
   137  	if c.fPrintAccount {
   138  		if err := c.printAccount(); err != nil {
   139  			log.Fatal(err)
   140  		}
   141  		return
   142  	}
   143  }
   144  
   145  func (c *wallet) printMnemonic() error {
   146  	fmt.Println("")
   147  	fmt.Println("=> Your Ethereum private mnemonic is:")
   148  	fmt.Println("=>", c.wallet.HDNode().Mnemonic())
   149  	fmt.Println("")
   150  	return nil
   151  }
   152  
   153  func (c *wallet) printPrivateKey() error {
   154  	fmt.Println("")
   155  	fmt.Println("=> Your Ethereum private key is:")
   156  	fmt.Println("=>", c.wallet.PrivateKeyHex())
   157  	fmt.Println("")
   158  	return nil
   159  }
   160  
   161  func (c *wallet) printAccount() error {
   162  	fmt.Println("")
   163  	fmt.Println("")
   164  	fmt.Println("=> Your Ethereum wallet address is:", c.wallet.Address().String())
   165  	fmt.Println("")
   166  	return nil
   167  }
   168  
   169  func (c *wallet) createNew() error {
   170  	var err error
   171  	var importMnemonic string
   172  
   173  	if c.fImportMnemonic {
   174  		var mnemonic []byte
   175  		// TODO: use crypto/terminal and print *'s on each keypress of input
   176  		mnemonic, err = readPlainInput("Enter your mnemonic to import: ")
   177  		if err != nil {
   178  			return err
   179  		}
   180  		importMnemonic = strings.TrimSpace(string(mnemonic))
   181  	}
   182  
   183  	derivationPath := c.fPath
   184  	if derivationPath == "" {
   185  		derivationPath = ethwallet.DefaultWalletOptions.DerivationPath
   186  	}
   187  
   188  	c.wallet, err = getWallet(importMnemonic, derivationPath)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	pw, err := readSecretInput("Password: ")
   194  	if err != nil {
   195  		return err
   196  	}
   197  	if len(pw) < 8 {
   198  		return errors.New("password must be at least 8 characters")
   199  	}
   200  
   201  	fmt.Println("")
   202  	confirmPw, err := readSecretInput("Confirm Password: ")
   203  	if err != nil {
   204  		return err
   205  	}
   206  	if string(pw) != string(confirmPw) {
   207  		return errors.New("passwords do not match")
   208  	}
   209  
   210  	cryptoJSON, err := keystore.EncryptDataV3([]byte(c.wallet.HDNode().Mnemonic()), pw, keystore.StandardScryptN, keystore.StandardScryptP)
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	keyFile := walletKeyFile{
   216  		Address: c.wallet.Address(),
   217  		Path:    c.wallet.HDNode().DerivationPath().String(),
   218  		Crypto:  cryptoJSON,
   219  		Client:  fmt.Sprintf("ethkit/%s - github.com/0xsequence/ethkit", VERSION),
   220  	}
   221  
   222  	data, err := json.MarshalIndent(keyFile, "", "  ")
   223  	if err != nil {
   224  		return err
   225  	}
   226  	data = append(data, []byte("\n")...)
   227  
   228  	if err := os.WriteFile(c.fKeyFile, data, 0600); err != nil {
   229  		return err
   230  	}
   231  
   232  	fmt.Println("")
   233  	fmt.Println("")
   234  	fmt.Println("=> success! ethkit has generated a new Ethereum wallet for you and saved")
   235  	fmt.Println("=> it in an encrypted+password protected file at:")
   236  	fmt.Println("=> ---")
   237  	fmt.Println("=>", c.fKeyFile)
   238  	fmt.Println("")
   239  	fmt.Printf("=> to confirm, please run: ./ethkit wallet --keyfile=%s --print-account\n", c.fKeyFile)
   240  	fmt.Println("")
   241  	fmt.Println("=> Your new Ethereum wallet address is:", c.wallet.Address().String())
   242  	fmt.Println("")
   243  
   244  	return nil
   245  }
   246  
   247  type walletKeyFile struct {
   248  	Address common.Address      `json:"address"`
   249  	Path    string              `json:"path"`
   250  	Crypto  keystore.CryptoJSON `json:"crypto"`
   251  	Client  string              `json:"client"`
   252  }
   253  
   254  func fileExists(filename string) bool {
   255  	info, err := os.Stat(filename)
   256  	if os.IsNotExist(err) {
   257  		return false
   258  	}
   259  	return !info.IsDir()
   260  }
   261  
   262  func readSecretInput(prompt string) ([]byte, error) {
   263  	fmt.Print(prompt)
   264  	password, err := terminal.ReadPassword(int(syscall.Stdin))
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	return password, nil
   269  }
   270  
   271  func readPlainInput(prompt string) ([]byte, error) {
   272  	fmt.Print(prompt)
   273  	reader := bufio.NewReader(os.Stdin)
   274  	text, _ := reader.ReadString('\n')
   275  	return []byte(text), nil
   276  }
   277  
   278  func getWallet(mnemonic, derivationPath string) (*ethwallet.Wallet, error) {
   279  	var err error
   280  	var wallet *ethwallet.Wallet
   281  
   282  	if derivationPath == "" {
   283  		return nil, fmt.Errorf("derivationPath cannot be empty")
   284  	}
   285  
   286  	if mnemonic != "" {
   287  		wallet, err = ethwallet.NewWalletFromMnemonic(mnemonic, derivationPath)
   288  	} else {
   289  		wallet, err = ethwallet.NewWalletFromRandomEntropy(ethwallet.WalletOptions{
   290  			DerivationPath:             derivationPath,
   291  			RandomWalletEntropyBitSize: ethwallet.EntropyBitSize24WordMnemonic,
   292  		})
   293  	}
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	return wallet, nil
   299  }