Files
ethdo/cmd/root.go
2020-01-13 19:20:59 +00:00

247 lines
7.4 KiB
Go

// Copyright © 2019 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"errors"
"fmt"
"os"
"strings"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
types "github.com/wealdtech/go-eth2-types"
wallet "github.com/wealdtech/go-eth2-wallet"
wtypes "github.com/wealdtech/go-eth2-wallet-types"
)
var cfgFile string
var quiet bool
var verbose bool
var debug bool
// Root variables, present for all commands
var rootStore string
var rootAccount string
var rootStorePassphrase string
var rootWalletPassphrase string
var rootAccountPassphrase string
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "ethdo",
Short: "Ethereum 2 CLI",
Long: `Manage common Ethereum 2 tasks from the command line.`,
PersistentPreRun: persistentPreRun,
}
func persistentPreRun(cmd *cobra.Command, args []string) {
if cmd.Name() == "help" {
// User just wants help
return
}
if cmd.Name() == "version" {
// User just wants the version
return
}
// We bind viper here so that we bind to the correct command
quiet = viper.GetBool("quiet")
verbose = viper.GetBool("verbose")
debug = viper.GetBool("debug")
rootStore = viper.GetString("store")
rootAccount = viper.GetString("account")
rootStorePassphrase = viper.GetString("storepassphrase")
rootWalletPassphrase = viper.GetString("walletpassphrase")
rootAccountPassphrase = viper.GetString("passphrase")
if quiet && verbose {
die("Cannot supply both quiet and verbose flags")
}
if quiet && debug {
die("Cannot supply both quiet and debug flags")
}
// Set up our wallet store
err := wallet.SetStore(rootStore, []byte(rootStorePassphrase))
errCheck(err, "Failed to set up wallet store")
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(_exit_failure)
}
}
func init() {
cobra.OnInitialize(initConfig)
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.ethdo.yaml)")
RootCmd.PersistentFlags().String("log", "", "log activity to the named file (default $HOME/ethdo.log). Logs are written for every action that generates a transaction")
if err := viper.BindPFlag("log", RootCmd.PersistentFlags().Lookup("log")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("store", "filesystem", "Store for accounts")
if err := viper.BindPFlag("store", RootCmd.PersistentFlags().Lookup("store")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("account", "", "Account name (in format \"wallet/account\")")
if err := viper.BindPFlag("account", RootCmd.PersistentFlags().Lookup("account")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("storepassphrase", "", "Passphrase for store (if applicable)")
if err := viper.BindPFlag("storepassphrase", RootCmd.PersistentFlags().Lookup("storepassphrase")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("walletpassphrase", "", "Passphrase for wallet (if applicable)")
if err := viper.BindPFlag("walletpassphrase", RootCmd.PersistentFlags().Lookup("walletpassphrase")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("passphrase", "", "Passphrase for account (if applicable)")
if err := viper.BindPFlag("passphrase", RootCmd.PersistentFlags().Lookup("passphrase")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Bool("quiet", false, "do not generate any output")
if err := viper.BindPFlag("quiet", RootCmd.PersistentFlags().Lookup("quiet")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Bool("verbose", false, "generate additional output where appropriate")
if err := viper.BindPFlag("verbose", RootCmd.PersistentFlags().Lookup("verbose")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Bool("debug", false, "generate debug output")
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
panic(err)
}
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(_exit_failure)
}
// Search config in home directory with name ".ethdo" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".ethdo")
}
viper.SetEnvPrefix("ETHDO")
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err != nil {
// Don't report lack of config file...
if !strings.Contains(err.Error(), "Not Found") {
fmt.Println(err)
os.Exit(_exit_failure)
}
}
}
//
// Helpers
//
func outputIf(condition bool, msg string) {
if condition {
fmt.Println(msg)
}
}
// walletAndAccountNamesFromPath breaks a path in to wallet and account names.
func walletAndAccountNamesFromPath(path string) (string, string, error) {
if len(path) == 0 {
return "", "", errors.New("invalid account format")
}
index := strings.Index(path, "/")
if index == -1 {
// Just the wallet
return path, "", nil
}
if index == len(path)-1 {
// Trailing /
return path[:index], "", nil
}
return path[:index], path[index+1:], nil
}
// walletFromPath obtains a wallet given a path specification.
func walletFromPath(path string) (wtypes.Wallet, error) {
walletName, _, err := walletAndAccountNamesFromPath(path)
if err != nil {
return nil, err
}
w, err := wallet.OpenWallet(walletName)
if err != nil {
if strings.Contains(err.Error(), "failed to decrypt wallet") {
return nil, errors.New("Incorrect store passphrase")
}
return nil, err
}
return w, nil
}
// accountFromPath obtains an account given a path specification.
func accountFromPath(path string) (wtypes.Account, error) {
wallet, err := walletFromPath(path)
if err != nil {
return nil, err
}
_, accountName, err := walletAndAccountNamesFromPath(path)
if err != nil {
return nil, err
}
if accountName == "" {
return nil, errors.New("no account name")
}
if wallet.Type() == "hierarchical deterministic" && strings.HasPrefix(accountName, "m/") && rootWalletPassphrase != "" {
err = wallet.Unlock([]byte(rootWalletPassphrase))
if err != nil {
return nil, errors.New("invalid wallet passphrase")
}
defer wallet.Lock()
}
return wallet.AccountByName(accountName)
}
func sign(path string, data []byte, domain uint64) (types.Signature, error) {
assert(rootAccountPassphrase != "", "--passphrase is required")
account, err := accountFromPath(path)
if err != nil {
return nil, err
}
err = account.Unlock([]byte(rootAccountPassphrase))
if err != nil {
return nil, err
}
defer account.Lock()
return account.Sign(data, domain)
}