From 8b39398612f098348a66454c0712ee52271f9492 Mon Sep 17 00:00:00 2001 From: yys Date: Thu, 11 Jul 2019 23:47:49 +0900 Subject: [PATCH] Feature/recover-old-hd-path (#193) * add chain id check to update endblocker only for columbus-2 * move chain-id check to update module * upgrade $terracli keys add to support old mnemonic support * add address selection when interactive mode is enabled * remove tmp keys folder * change log update * gitignore update * add old-hd-path option to give option for recover --- CHANGELOG.md | 20 +++ Makefile | 2 +- client/keys/.gitignore | 1 + client/keys/add.go | 308 +++++++++++++++++++++++++++++++++ client/keys/add_ledger_test.go | 58 +++++++ client/keys/add_test.go | 289 +++++++++++++++++++++++++++++++ client/keys/codec.go | 22 +++ client/keys/codec_test.go | 102 +++++++++++ client/keys/delete.go | 98 +++++++++++ client/keys/delete_test.go | 136 +++++++++++++++ client/keys/errors.go | 31 ++++ client/keys/list.go | 31 ++++ client/keys/list_test.go | 55 ++++++ client/keys/mnemonic.go | 75 ++++++++ client/keys/mnemonic_test.go | 59 +++++++ client/keys/root.go | 31 ++++ client/keys/root_test.go | 15 ++ client/keys/show.go | 167 ++++++++++++++++++ client/keys/show_test.go | 164 ++++++++++++++++++ client/keys/types.go | 55 ++++++ client/keys/update.go | 47 +++++ client/keys/update_test.go | 63 +++++++ client/keys/utils.go | 152 ++++++++++++++++ cmd/terracli/main.go | 6 +- cmd/terrad/main.go | 4 +- go.mod | 3 +- types/util/hdpath.go | 8 + 27 files changed, 1995 insertions(+), 7 deletions(-) create mode 100644 client/keys/.gitignore create mode 100755 client/keys/add.go create mode 100755 client/keys/add_ledger_test.go create mode 100755 client/keys/add_test.go create mode 100755 client/keys/codec.go create mode 100755 client/keys/codec_test.go create mode 100755 client/keys/delete.go create mode 100755 client/keys/delete_test.go create mode 100755 client/keys/errors.go create mode 100755 client/keys/list.go create mode 100755 client/keys/list_test.go create mode 100755 client/keys/mnemonic.go create mode 100755 client/keys/mnemonic_test.go create mode 100755 client/keys/root.go create mode 100755 client/keys/root_test.go create mode 100755 client/keys/show.go create mode 100755 client/keys/show_test.go create mode 100755 client/keys/types.go create mode 100755 client/keys/update.go create mode 100755 client/keys/update_test.go create mode 100755 client/keys/utils.go create mode 100644 types/util/hdpath.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f428c729..48825cb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.2.4 +- [\#193](https://github.com/terra-project/core/pull/193) + +### Changes +#### [\#193](https://github.com/terra-project/core/pull/193) Feature/recover-old-hd-path +Upgrade `$terracli keys add --recover` to support old version hd path + +In order to support old bip44 path (atom), address chose option is appended at recover. +``` +$ terracli keys add tmp2 --recover +Enter a passphrase to encrypt your key to disk: +Repeat the passphrase: +> Enter your bip39 mnemonic +candy hint hamster cute inquiry bright industry decide assist wedding carpet fiber arm menu machine lottery type alert fan march argue adapt recycle stomach +> +1) terra1wxuq9hkt4kes7r9kxh953l7p2cpcw8l73ek5dg +2) terra1gaczd45crhwfa4x05k9747cuxwfmnduvmtyefs +Please select the address want to recover(1 or 2): +``` + ## 0.2.3 - [\#187](https://github.com/terra-project/core/pull/187): Change all time instance timezone to UTC to remove gap in time calculation diff --git a/Makefile b/Makefile index 093ae7bb9..0cca2e056 100755 --- a/Makefile +++ b/Makefile @@ -128,7 +128,7 @@ distclean: clean test: test_unit test_unit: - @VERSION=$(VERSION) go test $(PACKAGES_NOSIMULATION) + @VERSION=$(VERSION) go test $(PACKAGES_NOSIMULATION) -coverprofile cp.out test_race: @VERSION=$(VERSION) go test -race $(PACKAGES_NOSIMULATION) diff --git a/client/keys/.gitignore b/client/keys/.gitignore new file mode 100644 index 000000000..2744c9935 --- /dev/null +++ b/client/keys/.gitignore @@ -0,0 +1 @@ +keys/ \ No newline at end of file diff --git a/client/keys/add.go b/client/keys/add.go new file mode 100755 index 000000000..38a5c921e --- /dev/null +++ b/client/keys/add.go @@ -0,0 +1,308 @@ +package keys + +import ( + "bytes" + "errors" + "fmt" + "os" + "sort" + + "github.com/terra-project/core/types/util" + + "github.com/cosmos/cosmos-sdk/client" + sdkkeys "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/cmd/gaia/app" + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + bip39 "github.com/cosmos/go-bip39" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/multisig" + "github.com/tendermint/tendermint/libs/cli" +) + +const ( + flagInteractive = "interactive" + flagRecover = "recover" + flagOldHdPath = "old-hd-path" + flagNoBackup = "no-backup" + flagDryRun = "dry-run" + flagAccount = "account" + flagIndex = "index" + flagMultisig = "multisig" + flagNoSort = "nosort" +) + +func addKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "add ", + Short: "Add an encrypted private key (either newly generated or recovered), encrypt it, and save to disk", + Long: `Derive a new private key and encrypt to disk. +Optionally specify a BIP39 mnemonic, a BIP39 passphrase to further secure the mnemonic, +and a bip32 HD path to derive a specific account. The key will be stored under the given name +and encrypted with the given password. The only input that is required is the encryption password. + +If run with -i, it will prompt the user for BIP44 path, BIP39 mnemonic, and passphrase. +The flag --recover allows one to recover a key from a seed passphrase. +If run with --dry-run, a key would be generated (or recovered) but not stored to the +local keystore. +Use the --pubkey flag to add arbitrary public keys to the keystore for constructing +multisig transactions. + +You can add a multisig key by passing the list of key names you want the public +key to be composed of to the --multisig flag and the minimum number of signatures +required through --multisig-threshold. The keys are sorted by address, unless +the flag --nosort is set. +`, + Args: cobra.ExactArgs(1), + RunE: runAddCmd, + } + cmd.Flags().StringSlice(flagMultisig, nil, "Construct and store a multisig public key (implies --pubkey)") + cmd.Flags().Uint(flagMultiSigThreshold, 1, "K out of N required signatures. For use in conjunction with --multisig") + cmd.Flags().Bool(flagNoSort, false, "Keys passed to --multisig are taken in the order they're supplied") + cmd.Flags().String(sdkkeys.FlagPublicKey, "", "Parse a public key in bech32 format and save it to disk") + cmd.Flags().BoolP(flagInteractive, "i", false, "Interactively prompt user for BIP39 passphrase and mnemonic") + cmd.Flags().Bool(client.FlagUseLedger, false, "Store a local reference to a private key on a Ledger device") + cmd.Flags().Bool(flagRecover, false, "Provide seed phrase to recover existing key instead of creating") + cmd.Flags().Bool(flagNoBackup, false, "Don't print out seed phrase (if others are watching the terminal)") + cmd.Flags().Bool(flagDryRun, false, "Perform action, but don't add key to local keystore") + cmd.Flags().Uint32(flagAccount, 0, "Account number for HD derivation") + cmd.Flags().Uint32(flagIndex, 0, "Address index number for HD derivation") + cmd.Flags().Bool(flagOldHdPath, false, "Recover key with old hd path") + cmd.Flags().Bool(client.FlagIndentResponse, false, "Add indent to JSON response") + return cmd +} + +/* +input + - bip39 mnemonic + - bip39 passphrase + - bip44 path + - local encryption password +output + - armor encrypted private key (saved to file) +*/ +func runAddCmd(_ *cobra.Command, args []string) error { + var kb keys.Keybase + var err error + var encryptPassword string + + buf := client.BufferStdin() + name := args[0] + + interactive := viper.GetBool(flagInteractive) + showMnemonic := !viper.GetBool(flagNoBackup) + + if viper.GetBool(flagDryRun) { + // we throw this away, so don't enforce args, + // we want to get a new random seed phrase quickly + kb = keys.NewInMemory() + encryptPassword = app.DefaultKeyPass + } else { + kb, err = sdkkeys.NewKeyBaseFromHomeFlag() + if err != nil { + return err + } + + _, err = kb.Get(name) + if err == nil { + // account exists, ask for user confirmation + if response, err2 := client.GetConfirmation( + fmt.Sprintf("override the existing name %s", name), buf); err2 != nil || !response { + return err2 + } + } + + multisigKeys := viper.GetStringSlice(flagMultisig) + if len(multisigKeys) != 0 { + var pks []crypto.PubKey + + multisigThreshold := viper.GetInt(flagMultiSigThreshold) + if err := validateMultisigThreshold(multisigThreshold, len(multisigKeys)); err != nil { + return err + } + + for _, keyname := range multisigKeys { + k, err := kb.Get(keyname) + if err != nil { + return err + } + pks = append(pks, k.GetPubKey()) + } + + // Handle --nosort + if !viper.GetBool(flagNoSort) { + sort.Slice(pks, func(i, j int) bool { + return bytes.Compare(pks[i].Address(), pks[j].Address()) < 0 + }) + } + + pk := multisig.NewPubKeyMultisigThreshold(multisigThreshold, pks) + if _, err := kb.CreateMulti(name, pk); err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Key %q saved to disk.\n", name) + return nil + } + + // ask for a password when generating a local key + if viper.GetString(sdkkeys.FlagPublicKey) == "" && !viper.GetBool(client.FlagUseLedger) { + encryptPassword, err = client.GetCheckPassword( + "Enter a passphrase to encrypt your key to disk:", + "Repeat the passphrase:", buf) + if err != nil { + return err + } + } + } + + if viper.GetString(sdkkeys.FlagPublicKey) != "" { + pk, err := sdk.GetAccPubKeyBech32(viper.GetString(sdkkeys.FlagPublicKey)) + if err != nil { + return err + } + _, err = kb.CreateOffline(name, pk) + if err != nil { + return err + } + return nil + } + + account := uint32(viper.GetInt(flagAccount)) + index := uint32(viper.GetInt(flagIndex)) + + // If we're using ledger, only thing we need is the path and the bech32 prefix. + if viper.GetBool(client.FlagUseLedger) { + bech32PrefixAccAddr := sdk.GetConfig().GetBech32AccountAddrPrefix() + info, err := kb.CreateLedger(name, keys.Secp256k1, bech32PrefixAccAddr, account, index) + if err != nil { + return err + } + + return printCreate(info, false, "") + } + + // Get bip39 mnemonic + var mnemonic string + var bip39Passphrase string + + if interactive || viper.GetBool(flagRecover) { + bip39Message := "Enter your bip39 mnemonic" + if !viper.GetBool(flagRecover) { + bip39Message = "Enter your bip39 mnemonic, or hit enter to generate one." + } + + mnemonic, err = client.GetString(bip39Message, buf) + if err != nil { + return err + } + + if !bip39.IsMnemonicValid(mnemonic) { + return errors.New("invalid mnemonic") + } + } + + if len(mnemonic) == 0 { + // read entropy seed straight from crypto.Rand and convert to mnemonic + entropySeed, err := bip39.NewEntropy(mnemonicEntropySize) + if err != nil { + return err + } + + mnemonic, err = bip39.NewMnemonic(entropySeed[:]) + if err != nil { + return err + } + } + + // override bip39 passphrase + if interactive { + bip39Passphrase, err = client.GetString( + "Enter your bip39 passphrase. This is combined with the mnemonic to derive the seed. "+ + "Most users should just hit enter to use the default, \"\"", buf) + if err != nil { + return err + } + + // if they use one, make them re-enter it + if len(bip39Passphrase) != 0 { + p2, err := client.GetString("Repeat the passphrase:", buf) + if err != nil { + return err + } + + if bip39Passphrase != p2 { + return errors.New("passphrases don't match") + } + } + } + + coinType := util.CoinType + if (viper.GetBool(flagRecover) || viper.GetBool(flagInteractive)) && viper.GetBool(flagOldHdPath) { + coinType = sdk.CoinType + } + + hdParams := hd.NewFundraiserParams(account, coinType, index) + info, err := kb.Derive(name, mnemonic, bip39Passphrase, encryptPassword, *hdParams) + if err != nil { + return err + } + + // Recover key from seed passphrase + if viper.GetBool(flagRecover) { + // Hide mnemonic from output + showMnemonic = false + mnemonic = "" + } + + return printCreate(info, showMnemonic, mnemonic) +} + +func printCreate(info keys.Info, showMnemonic bool, mnemonic string) error { + output := viper.Get(cli.OutputFlag) + + switch output { + case sdkkeys.OutputFormatText: + fmt.Fprintln(os.Stderr) + printKeyInfo(info, keys.Bech32KeyOutput) + + // print mnemonic unless requested not to. + if showMnemonic { + fmt.Fprintln(os.Stderr, "\n**Important** write this mnemonic phrase in a safe place.") + fmt.Fprintln(os.Stderr, "It is the only way to recover your account if you ever forget your password.") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, mnemonic) + } + case sdkkeys.OutputFormatJSON: + out, err := keys.Bech32KeyOutput(info) + if err != nil { + return err + } + + if showMnemonic { + out.Mnemonic = mnemonic + } + + var jsonString []byte + if viper.GetBool(client.FlagIndentResponse) { + jsonString, err = cdc.MarshalJSONIndent(out, "", " ") + } else { + jsonString, err = cdc.MarshalJSON(out) + } + + if err != nil { + return err + } + fmt.Fprintln(os.Stderr, string(jsonString)) + default: + return fmt.Errorf("I can't speak: %s", output) + } + + return nil +} diff --git a/client/keys/add_ledger_test.go b/client/keys/add_ledger_test.go new file mode 100755 index 000000000..835db4176 --- /dev/null +++ b/client/keys/add_ledger_test.go @@ -0,0 +1,58 @@ +//+build ledger,test_ledger_mock + +package keys + +import ( + "bufio" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keys" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/terra-project/core/testutil" + + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + + "github.com/cosmos/cosmos-sdk/tests" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/stretchr/testify/assert" +) + +func Test_runAddCmdLedger(t *testing.T) { + testutil.PrepareCmdTest() + + cmd := addKeyCommand() + assert.NotNil(t, cmd) + + // Prepare a keybase + kbHome, kbCleanUp := tests.NewTestCaseDir(t) + assert.NotNil(t, kbHome) + defer kbCleanUp() + viper.Set(cli.HomeFlag, kbHome) + viper.Set(client.FlagUseLedger, true) + + /// Test Text + viper.Set(cli.OutputFlag, OutputFormatText) + // Now enter password + cleanUp1 := client.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\ntest1234\n"))) + defer cleanUp1() + err := runAddCmd(cmd, []string{"keyname1"}) + assert.NoError(t, err) + + // Now check that it has been stored properly + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + assert.NotNil(t, kb) + key1, err := kb.Get("keyname1") + assert.NoError(t, err) + assert.NotNil(t, key1) + + assert.Equal(t, "keyname1", key1.GetName()) + assert.Equal(t, keys.TypeLedger, key1.GetType()) + assert.Equal(t, + "terrapub1addwnpepqd87l8xhcnrrtzxnkql7k55ph8fr9jarf4hn6udwukfprlalu8lgw0fsqm8", + sdk.MustBech32ifyAccPub(key1.GetPubKey())) +} diff --git a/client/keys/add_test.go b/client/keys/add_test.go new file mode 100755 index 000000000..948191301 --- /dev/null +++ b/client/keys/add_test.go @@ -0,0 +1,289 @@ +package keys + +import ( + "bufio" + "strings" + "testing" + + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/tests" + + "github.com/stretchr/testify/assert" + + "github.com/terra-project/core/testutil" +) + +func Test_runAddCmdBasic(t *testing.T) { + cmd := addKeyCommand() + assert.NotNil(t, cmd) + + // Prepare a keybase + kbHome, kbCleanUp := tests.NewTestCaseDir(t) + assert.NotNil(t, kbHome) + defer kbCleanUp() + viper.Set(cli.HomeFlag, kbHome) + + /// Test Text + viper.Set(cli.OutputFlag, OutputFormatText) + // Now enter password + cleanUp1 := client.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\n"))) + defer cleanUp1() + err := runAddCmd(cmd, []string{"keyname1"}) + assert.NoError(t, err) + + /// Test Text - Replace? >> FAIL + viper.Set(cli.OutputFlag, OutputFormatText) + // Now enter password + cleanUp2 := client.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\n"))) + defer cleanUp2() + err = runAddCmd(cmd, []string{"keyname1"}) + assert.Error(t, err) + + /// Test Text - Replace? Answer >> PASS + viper.Set(cli.OutputFlag, OutputFormatText) + // Now enter password + cleanUp3 := client.OverrideStdin(bufio.NewReader(strings.NewReader("y\ntest1234\n"))) + defer cleanUp3() + err = runAddCmd(cmd, []string{"keyname1"}) + assert.NoError(t, err) + + // Check JSON + viper.Set(cli.OutputFlag, OutputFormatJSON) + // Now enter password + cleanUp4 := client.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\n"))) + defer cleanUp4() + err = runAddCmd(cmd, []string{"keyname2"}) + assert.NoError(t, err) + + recoverInitialViperState() +} + +func Test_runnAddCmdDryRun(t *testing.T) { + cmd := addKeyCommand() + assert.NotNil(t, cmd) + + // Prepare a keybase + kbHome, kbCleanUp := tests.NewTestCaseDir(t) + assert.NotNil(t, kbHome) + defer kbCleanUp() + viper.Set(cli.HomeFlag, kbHome) + + /// Test Text + viper.Set(cli.OutputFlag, OutputFormatText) + viper.Set(flagDryRun, true) + + keyName := "keyname1" + + // Without password + err := runAddCmd(cmd, []string{keyName}) + assert.NoError(t, err) + + // dry-run will not make any key info + _, err = GetKeyInfo(keyName) + assert.Error(t, err) + + recoverInitialViperState() +} + +func Test_runAddCmdRecover(t *testing.T) { + testutil.PrepareCmdTest() + + cmd := addKeyCommand() + assert.NotNil(t, cmd) + + // Prepare a keybase + kbHome, kbCleanUp := tests.NewTestCaseDir(t) + assert.NotNil(t, kbHome) + defer kbCleanUp() + viper.Set(cli.HomeFlag, kbHome) + + /// Test Text + viper.Set(cli.OutputFlag, OutputFormatText) + viper.Set(flagRecover, true) + viper.Set(flagOldHdPath, false) + + keyName := "keyname1" + password := "test1234\n" + mnemonic := "candy hint hamster cute inquiry bright industry decide assist wedding carpet fiber arm menu machine lottery type alert fan march argue adapt recycle stomach\n" + + // New HD Path + cleanUp1 := client.OverrideStdin(bufio.NewReader(strings.NewReader(password + mnemonic))) + defer cleanUp1() + + err := runAddCmd(cmd, []string{"keyname1"}) + assert.NoError(t, err) + + info, err := GetKeyInfo(keyName) + assert.NoError(t, err) + assert.Equal(t, "terra1wxuq9hkt4kes7r9kxh953l7p2cpcw8l73ek5dg", info.GetAddress().String()) + + // Old HD Path + viper.Set(flagOldHdPath, true) + cleanUp2 := client.OverrideStdin(bufio.NewReader(strings.NewReader("y\n" + password + mnemonic))) + defer cleanUp2() + + err = runAddCmd(cmd, []string{"keyname1"}) + assert.NoError(t, err) + + info, err = GetKeyInfo(keyName) + assert.NoError(t, err) + assert.Equal(t, "terra1gaczd45crhwfa4x05k9747cuxwfmnduvmtyefs", info.GetAddress().String()) + + // recover with dry-run flag (default password) + viper.Set(flagDryRun, true) + viper.Set(flagOldHdPath, true) + + cleanUp3 := client.OverrideStdin(bufio.NewReader(strings.NewReader(mnemonic))) + defer cleanUp3() + + err = runAddCmd(cmd, []string{"keyname1"}) + assert.NoError(t, err) + + recoverInitialViperState() +} + +func Test_runAddCmdPubkeyAndMultisig(t *testing.T) { + testutil.PrepareCmdTest() + + cmd := addKeyCommand() + assert.NotNil(t, cmd) + + // Prepare a keybase + kbHome, kbCleanUp := tests.NewTestCaseDir(t) + assert.NotNil(t, kbHome) + defer kbCleanUp() + viper.Set(cli.HomeFlag, kbHome) + + /// Public Key Test + viper.Set(cli.OutputFlag, OutputFormatText) + viper.Set(flagRecover, true) + + pubkey1 := "terrapub1addwnpepqtmg9m7jy8xxqwnq05xh2rymsfph0mrfhzuz2lae3k09sn7qqwew7cgk76c" + pubkey2 := "terrapub1addwnpepqdn2knqsda3zxq4uv24yg5wp97e48sxdhuqyplmpya5eeujlm5zk5chrdt8" + pubkey3 := "terrapub1addwnpepqtycrza0rc9lk288gk9epwhdmft95t737vrctu75vp7h39l9rh24vxag39p" + + keyName1 := "keyname1" + keyName2 := "keyname2" + keyName3 := "keyname3" + + // Invalid Public Key + viper.Set(FlagPublicKey, "invalid") + + err := runAddCmd(cmd, []string{keyName1}) + assert.Error(t, err) + + // Valid + viper.Set(FlagPublicKey, pubkey1) + + err = runAddCmd(cmd, []string{keyName1}) + assert.NoError(t, err) + + info, err := GetKeyInfo(keyName1) + assert.NoError(t, err) + assert.Equal(t, "terra18smrf782hvjjeu3am06flc7nge2xvf8f2426q4", info.GetAddress().String()) + + viper.Set(FlagPublicKey, pubkey2) + + err = runAddCmd(cmd, []string{keyName2}) + assert.NoError(t, err) + + info, err = GetKeyInfo(keyName2) + assert.NoError(t, err) + assert.Equal(t, "terra1mzm0v94uchdufn806hxxzu6q4m3xclx2yzpdv8", info.GetAddress().String()) + + viper.Set(FlagPublicKey, pubkey3) + + err = runAddCmd(cmd, []string{keyName3}) + assert.NoError(t, err) + + info, err = GetKeyInfo(keyName3) + assert.NoError(t, err) + assert.Equal(t, "terra1yr0sqzfraffdwv9c33gx2dqhcl525muheuefaf", info.GetAddress().String()) + + // Multisig Test + keyNameMultisig := "keyNameMultisig" + viper.Set(FlagPublicKey, "") + viper.Set(flagMultisig, keyName1+" "+keyName2+" "+keyName3) + + // Invalid Threashold + viper.Set(flagMultiSigThreshold, 0) + err = runAddCmd(cmd, []string{keyNameMultisig}) + assert.Error(t, err) + + // Invalid Key Name + viper.Set(flagMultiSigThreshold, 2) + viper.Set(flagMultisig, keyName1+" "+keyName2+" hihi") + err = runAddCmd(cmd, []string{keyNameMultisig}) + assert.Error(t, err) + + // Valid + viper.Set(flagMultisig, keyName1+" "+keyName2+" "+keyName3) + err = runAddCmd(cmd, []string{keyNameMultisig}) + assert.NoError(t, err) + + info, err = GetKeyInfo(keyNameMultisig) + assert.NoError(t, err) + assert.Equal(t, "terra1tswgrqcdauaw06dxeycj8ctr5etlah6aqg7elm", info.GetAddress().String()) + + recoverInitialViperState() +} + +func Test_runAddCmdInteractive(t *testing.T) { + testutil.PrepareCmdTest() + + cmd := addKeyCommand() + assert.NotNil(t, cmd) + + // Prepare a keybase + kbHome, kbCleanUp := tests.NewTestCaseDir(t) + assert.NotNil(t, kbHome) + defer kbCleanUp() + viper.Set(cli.HomeFlag, kbHome) + + /// Test Text + viper.Set(cli.OutputFlag, OutputFormatText) + viper.Set(flagInteractive, true) + viper.Set(flagOldHdPath, false) + + keyName := "keyname1" + password := "test1234\n" + mnemonic := "candy hint hamster cute inquiry bright industry decide assist wedding carpet fiber arm menu machine lottery type alert fan march argue adapt recycle stomach\n" + bip39Passphrase := "hihi\nhihi\n" + + // New HD path + cleanUp1 := client.OverrideStdin(bufio.NewReader(strings.NewReader(password + mnemonic + bip39Passphrase))) + defer cleanUp1() + + err := runAddCmd(cmd, []string{keyName}) + assert.NoError(t, err) + + info, err := GetKeyInfo(keyName) + assert.NoError(t, err) + assert.Equal(t, "terra1smea3fuwun5ggfjep25gd7yv8kvw3mvx2hw3zm", info.GetAddress().String()) + + viper.Set(flagOldHdPath, true) + // Old HD path + cleanUp2 := client.OverrideStdin(bufio.NewReader(strings.NewReader("y\n" + password + mnemonic + bip39Passphrase))) + defer cleanUp2() + + err = runAddCmd(cmd, []string{keyName}) + assert.NoError(t, err) + + info, err = GetKeyInfo(keyName) + assert.NoError(t, err) + assert.Equal(t, "terra1nv4nsd7tfl8xc2dm7rry5exwcf350wjguk0x2c", info.GetAddress().String()) + + recoverInitialViperState() +} + +func recoverInitialViperState() { + viper.Set(flagInteractive, false) + viper.Set(flagRecover, false) + viper.Set(FlagPublicKey, "") + viper.Set(flagMultisig, "") + viper.Set(flagMultiSigThreshold, 0) + viper.Set(flagDryRun, false) +} diff --git a/client/keys/codec.go b/client/keys/codec.go new file mode 100755 index 000000000..6bbb16850 --- /dev/null +++ b/client/keys/codec.go @@ -0,0 +1,22 @@ +package keys + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +var cdc *codec.Codec + +func init() { + cdc = codec.New() + codec.RegisterCrypto(cdc) +} + +// marshal keys +func MarshalJSON(o interface{}) ([]byte, error) { + return cdc.MarshalJSON(o) +} + +// unmarshal json +func UnmarshalJSON(bz []byte, ptr interface{}) error { + return cdc.UnmarshalJSON(bz, ptr) +} diff --git a/client/keys/codec_test.go b/client/keys/codec_test.go new file mode 100755 index 000000000..a30aea30a --- /dev/null +++ b/client/keys/codec_test.go @@ -0,0 +1,102 @@ +package keys + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/crypto/keys" +) + +type testCases struct { + Keys []keys.KeyOutput + Answers []keys.KeyOutput + JSON [][]byte +} + +func getTestCases() testCases { + return testCases{ + []keys.KeyOutput{ + {Name: "A", Type: "B", Address: "C", PubKey: "D", Mnemonic: "E", Threshold: 0, PubKeys: nil}, + {Name: "A", Type: "B", Address: "C", PubKey: "D", Mnemonic: "", Threshold: 0, PubKeys: nil}, + {Name: "", Type: "B", Address: "C", PubKey: "D", Mnemonic: "", Threshold: 0, PubKeys: nil}, + {Name: "", Type: "", Address: "", PubKey: "", Mnemonic: "", Threshold: 0, PubKeys: nil}, + }, + make([]keys.KeyOutput, 4), + [][]byte{ + []byte(`{"name":"A","type":"B","address":"C","pubkey":"D","mnemonic":"E"}`), + []byte(`{"name":"A","type":"B","address":"C","pubkey":"D"}`), + []byte(`{"name":"","type":"B","address":"C","pubkey":"D"}`), + []byte(`{"name":"","type":"","address":"","pubkey":""}`), + }, + } +} + +func TestMarshalJSON(t *testing.T) { + type args struct { + o keys.KeyOutput + } + + data := getTestCases() + + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + {"basic", args{data.Keys[0]}, []byte(data.JSON[0]), false}, + {"mnemonic is optional", args{data.Keys[1]}, []byte(data.JSON[1]), false}, + + // REVIEW: Are the next results expected?? + {"empty name", args{data.Keys[2]}, []byte(data.JSON[2]), false}, + {"empty object", args{data.Keys[3]}, []byte(data.JSON[3]), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MarshalJSON(tt.args.o) + if (err != nil) != tt.wantErr { + t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + fmt.Printf("%s\n", got) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalJSON() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnmarshalJSON(t *testing.T) { + type args struct { + bz []byte + ptr interface{} + } + + data := getTestCases() + + tests := []struct { + name string + args args + wantErr bool + }{ + {"basic", args{data.JSON[0], &data.Answers[0]}, false}, + {"mnemonic is optional", args{data.JSON[1], &data.Answers[1]}, false}, + + // REVIEW: Are the next results expected?? + {"empty name", args{data.JSON[2], &data.Answers[2]}, false}, + {"empty object", args{data.JSON[3], &data.Answers[3]}, false}, + } + for idx, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := UnmarshalJSON(tt.args.bz, tt.args.ptr); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + + // Confirm deserialized objects are the same + require.Equal(t, data.Keys[idx], data.Answers[idx]) + }) + } +} diff --git a/client/keys/delete.go b/client/keys/delete.go new file mode 100755 index 000000000..5ae96aab9 --- /dev/null +++ b/client/keys/delete.go @@ -0,0 +1,98 @@ +package keys + +import ( + "bufio" + "errors" + "fmt" + "os" + + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keys" + + "github.com/spf13/cobra" +) + +const ( + flagYes = "yes" + flagForce = "force" +) + +func deleteKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete the given key", + Long: `Delete a key from the store. + +Note that removing offline or ledger keys will remove +only the public key references stored locally, i.e. +private keys stored in a ledger device cannot be deleted with +gaiacli. +`, + RunE: runDeleteCmd, + Args: cobra.ExactArgs(1), + } + + cmd.Flags().BoolP(flagYes, "y", false, + "Skip confirmation prompt when deleting offline or ledger key references") + cmd.Flags().BoolP(flagForce, "f", false, + "Remove the key unconditionally without asking for the passphrase") + return cmd +} + +func runDeleteCmd(cmd *cobra.Command, args []string) error { + name := args[0] + + kb, err := NewKeyBaseFromHomeFlag() + if err != nil { + return err + } + + info, err := kb.Get(name) + if err != nil { + return err + } + + buf := client.BufferStdin() + if info.GetType() == keys.TypeLedger || info.GetType() == keys.TypeOffline || info.GetType() == keys.TypeMulti { + if !viper.GetBool(flagYes) { + if err := confirmDeletion(buf); err != nil { + return err + } + } + if err := kb.Delete(name, "", true); err != nil { + return err + } + fmt.Fprintln(os.Stderr, "Public key reference deleted") + return nil + } + + // skip passphrase check if run with --force + skipPass := viper.GetBool(flagForce) + var oldpass string + if !skipPass { + if oldpass, err = client.GetPassword( + "DANGER - enter password to permanently delete key:", buf); err != nil { + return err + } + } + + err = kb.Delete(name, oldpass, skipPass) + if err != nil { + return err + } + fmt.Fprintln(os.Stderr, "Key deleted forever (uh oh!)") + return nil +} + +func confirmDeletion(buf *bufio.Reader) error { + answer, err := client.GetConfirmation("Key reference will be deleted. Continue?", buf) + if err != nil { + return err + } + if !answer { + return errors.New("aborted") + } + return nil +} diff --git a/client/keys/delete_test.go b/client/keys/delete_test.go new file mode 100755 index 000000000..c388e02b7 --- /dev/null +++ b/client/keys/delete_test.go @@ -0,0 +1,136 @@ +package keys + +import ( + "bufio" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/cosmos/cosmos-sdk/tests" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/cli" +) + +func Test_runDeleteCmd(t *testing.T) { + deleteKeyCommand := deleteKeyCommand() + + yesF, _ := deleteKeyCommand.Flags().GetBool(flagYes) + forceF, _ := deleteKeyCommand.Flags().GetBool(flagForce) + assert.False(t, yesF) + assert.False(t, forceF) + + fakeKeyName1 := "runDeleteCmd_Key1" + fakeKeyName2 := "runDeleteCmd_Key2" + fakeKeyName3 := "runDeleteCmd_Key3" + fakeKeyName4 := "runDeleteCmd_Key4" + + // Now add a temporary keybase + kbHome, cleanUp := tests.NewTestCaseDir(t) + defer cleanUp() + viper.Set(cli.HomeFlag, kbHome) + + // Now + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName1, tests.TestMnemonic, "", "", 0, 0) + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName2, tests.TestMnemonic, "", "", 0, 1) + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName3, tests.TestMnemonic, "", "test1234", 0, 0) + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName4, tests.TestMnemonic, "", "test1234", 0, 0) + assert.NoError(t, err) + + err = runDeleteCmd(deleteKeyCommand, []string{"blah"}) + require.Error(t, err) + require.Equal(t, "Key blah not found", err.Error()) + + // User confirmation missing + err = runDeleteCmd(deleteKeyCommand, []string{fakeKeyName1}) + require.Error(t, err) + require.Equal(t, "EOF", err.Error()) + + { + _, err = kb.Get(fakeKeyName1) + require.NoError(t, err) + + // Now there is a confirmation + cleanUp := client.OverrideStdin(bufio.NewReader(strings.NewReader("y\n"))) + defer cleanUp() + err = runDeleteCmd(deleteKeyCommand, []string{fakeKeyName1}) + require.NoError(t, err) + + _, err = kb.Get(fakeKeyName1) + require.Error(t, err) // Key1 is gone + } + + viper.Set(flagYes, true) + _, err = kb.Get(fakeKeyName2) + require.NoError(t, err) + err = runDeleteCmd(deleteKeyCommand, []string{fakeKeyName2}) + require.NoError(t, err) + _, err = kb.Get(fakeKeyName2) + require.Error(t, err) // Key2 is gone + + // Invalid Password + viper.Set(flagYes, false) + _, err = kb.Get(fakeKeyName3) + require.NoError(t, err) + cleanUp2 := client.OverrideStdin(bufio.NewReader(strings.NewReader("invalid\n"))) + defer cleanUp2() + err = runDeleteCmd(deleteKeyCommand, []string{fakeKeyName3}) + require.Error(t, err) + _, err = kb.Get(fakeKeyName3) + require.NoError(t, err) // Key3 is not gone + + // Valid Password + cleanUp3 := client.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\n"))) + defer cleanUp3() + err = runDeleteCmd(deleteKeyCommand, []string{fakeKeyName3}) + require.NoError(t, err) + _, err = kb.Get(fakeKeyName3) + require.Error(t, err) // Key3 is gone + + // Force Delete + viper.Set(flagForce, true) + _, err = kb.Get(fakeKeyName4) + require.NoError(t, err) + err = runDeleteCmd(deleteKeyCommand, []string{fakeKeyName4}) + require.NoError(t, err) + _, err = kb.Get(fakeKeyName4) + require.Error(t, err) // Key4 is gone + + // TODO: Write another case for !keys.Local +} + +func Test_confirmDeletion(t *testing.T) { + type args struct { + buf *bufio.Reader + } + + answerYes := bufio.NewReader(strings.NewReader("y\n")) + answerYes2 := bufio.NewReader(strings.NewReader("Y\n")) + answerNo := bufio.NewReader(strings.NewReader("n\n")) + answerInvalid := bufio.NewReader(strings.NewReader("245\n")) + + tests := []struct { + name string + args args + wantErr bool + }{ + {"Y", args{answerYes}, false}, + {"y", args{answerYes2}, false}, + {"N", args{answerNo}, true}, + {"BAD", args{answerInvalid}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := confirmDeletion(tt.args.buf); (err != nil) != tt.wantErr { + t.Errorf("confirmDeletion() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/client/keys/errors.go b/client/keys/errors.go new file mode 100755 index 000000000..a603b1d1a --- /dev/null +++ b/client/keys/errors.go @@ -0,0 +1,31 @@ +package keys + +import "fmt" + +func errKeyNameConflict(name string) error { + return fmt.Errorf("account with name %s already exists", name) +} + +func errMissingName() error { + return fmt.Errorf("you have to specify a name for the locally stored account") +} + +func errMissingPassword() error { + return fmt.Errorf("you have to specify a password for the locally stored account") +} + +func errMissingMnemonic() error { + return fmt.Errorf("you have to specify a mnemonic for key recovery") +} + +func errInvalidMnemonic() error { + return fmt.Errorf("the mnemonic is invalid") +} + +func errInvalidAccountNumber() error { + return fmt.Errorf("the account number is invalid") +} + +func errInvalidIndexNumber() error { + return fmt.Errorf("the index number is invalid") +} diff --git a/client/keys/list.go b/client/keys/list.go new file mode 100755 index 000000000..19873aabb --- /dev/null +++ b/client/keys/list.go @@ -0,0 +1,31 @@ +package keys + +import ( + "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" +) + +func listKeysCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all keys", + Long: `Return a list of all public keys stored by this key manager +along with their associated name and address.`, + RunE: runListCmd, + } + cmd.Flags().Bool(client.FlagIndentResponse, false, "Add indent to JSON response") + return cmd +} + +func runListCmd(cmd *cobra.Command, args []string) error { + kb, err := NewKeyBaseFromHomeFlag() + if err != nil { + return err + } + + infos, err := kb.List() + if err == nil { + printInfos(infos) + } + return err +} diff --git a/client/keys/list_test.go b/client/keys/list_test.go new file mode 100755 index 000000000..9fdab6a75 --- /dev/null +++ b/client/keys/list_test.go @@ -0,0 +1,55 @@ +package keys + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/tests" + "github.com/stretchr/testify/assert" + + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + + "github.com/spf13/cobra" +) + +func Test_runListCmd(t *testing.T) { + type args struct { + cmd *cobra.Command + args []string + } + + cmdBasic := listKeysCmd() + + // Prepare some keybases + kbHome1, cleanUp1 := tests.NewTestCaseDir(t) + defer cleanUp1() + // Do nothing, leave home1 empty + + kbHome2, cleanUp2 := tests.NewTestCaseDir(t) + defer cleanUp2() + viper.Set(cli.HomeFlag, kbHome2) + + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + _, err = kb.CreateAccount("something", tests.TestMnemonic, "", "", 0, 0) + assert.NoError(t, err) + + testData := []struct { + name string + kbDir string + args args + wantErr bool + }{ + {"invalid keybase", "/dev/null", args{cmdBasic, []string{}}, true}, + {"keybase: empty", kbHome1, args{cmdBasic, []string{}}, false}, + {"keybase: w/key", kbHome2, args{cmdBasic, []string{}}, false}, + } + for _, tt := range testData { + t.Run(tt.name, func(t *testing.T) { + viper.Set(cli.HomeFlag, tt.kbDir) + if err := runListCmd(tt.args.cmd, tt.args.args); (err != nil) != tt.wantErr { + t.Errorf("runListCmd() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/client/keys/mnemonic.go b/client/keys/mnemonic.go new file mode 100755 index 000000000..b9d434d2b --- /dev/null +++ b/client/keys/mnemonic.go @@ -0,0 +1,75 @@ +package keys + +import ( + "crypto/sha256" + "fmt" + + bip39 "github.com/bartekn/go-bip39" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" +) + +const ( + flagUserEntropy = "unsafe-entropy" + + mnemonicEntropySize = 256 +) + +func mnemonicKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "mnemonic", + Short: "Compute the bip39 mnemonic for some input entropy", + Long: "Create a bip39 mnemonic, sometimes called a seed phrase, by reading from the system entropy. To pass your own entropy, use --unsafe-entropy", + RunE: runMnemonicCmd, + } + cmd.Flags().Bool(flagUserEntropy, false, "Prompt the user to supply their own entropy, instead of relying on the system") + return cmd +} + +func runMnemonicCmd(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + + userEntropy, _ := flags.GetBool(flagUserEntropy) + + var entropySeed []byte + + if userEntropy { + // prompt the user to enter some entropy + buf := client.BufferStdin() + inputEntropy, err := client.GetString("> WARNING: Generate at least 256-bits of entropy and enter the results here:", buf) + if err != nil { + return err + } + if len(inputEntropy) < 43 { + return fmt.Errorf("256-bits is 43 characters in Base-64, and 100 in Base-6. You entered %v, and probably want more", len(inputEntropy)) + } + conf, err := client.GetConfirmation(fmt.Sprintf("> Input length: %d", len(inputEntropy)), buf) + if err != nil { + return err + } + if !conf { + return nil + } + + // hash input entropy to get entropy seed + hashedEntropy := sha256.Sum256([]byte(inputEntropy)) + entropySeed = hashedEntropy[:] + } else { + // read entropy seed straight from crypto.Rand + var err error + entropySeed, err = bip39.NewEntropy(mnemonicEntropySize) + if err != nil { + return err + } + } + + mnemonic, err := bip39.NewMnemonic(entropySeed[:]) + if err != nil { + return err + } + + fmt.Println(mnemonic) + + return nil +} diff --git a/client/keys/mnemonic_test.go b/client/keys/mnemonic_test.go new file mode 100755 index 000000000..64fd404a1 --- /dev/null +++ b/client/keys/mnemonic_test.go @@ -0,0 +1,59 @@ +package keys + +import ( + "bufio" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" +) + +func Test_RunMnemonicCmdNormal(t *testing.T) { + cmdBasic := mnemonicKeyCommand() + err := runMnemonicCmd(cmdBasic, []string{}) + require.NoError(t, err) +} + +func Test_RunMnemonicCmdUser(t *testing.T) { + cmdUser := mnemonicKeyCommand() + err := cmdUser.Flags().Set(flagUserEntropy, "1") + assert.NoError(t, err) + + err = runMnemonicCmd(cmdUser, []string{}) + require.Error(t, err) + require.Equal(t, "EOF", err.Error()) + + // Try again + cleanUp := client.OverrideStdin(bufio.NewReader(strings.NewReader("Hi!\n"))) + defer cleanUp() + err = runMnemonicCmd(cmdUser, []string{}) + require.Error(t, err) + require.Equal(t, + "256-bits is 43 characters in Base-64, and 100 in Base-6. You entered 3, and probably want more", + err.Error()) + + // Now provide "good" entropy :) + fakeEntropy := strings.Repeat(":)", 40) + "\ny\n" // entropy + accept count + cleanUp2 := client.OverrideStdin(bufio.NewReader(strings.NewReader(fakeEntropy))) + defer cleanUp2() + err = runMnemonicCmd(cmdUser, []string{}) + require.NoError(t, err) + + // Now provide "good" entropy but no answer + fakeEntropy = strings.Repeat(":)", 40) + "\n" // entropy + accept count + cleanUp3 := client.OverrideStdin(bufio.NewReader(strings.NewReader(fakeEntropy))) + defer cleanUp3() + err = runMnemonicCmd(cmdUser, []string{}) + require.Error(t, err) + + // Now provide "good" entropy but say no + fakeEntropy = strings.Repeat(":)", 40) + "\nn\n" // entropy + accept count + cleanUp4 := client.OverrideStdin(bufio.NewReader(strings.NewReader(fakeEntropy))) + defer cleanUp4() + err = runMnemonicCmd(cmdUser, []string{}) + require.NoError(t, err) +} diff --git a/client/keys/root.go b/client/keys/root.go new file mode 100755 index 000000000..e49b57d4a --- /dev/null +++ b/client/keys/root.go @@ -0,0 +1,31 @@ +package keys + +import ( + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" +) + +// Commands registers a sub-tree of commands to interact with +// local private key storage. +func Commands() *cobra.Command { + cmd := &cobra.Command{ + Use: "keys", + Short: "Add or view local private keys", + Long: `Keys allows you to manage your local keystore for tendermint. + + These keys may be in any format supported by go-crypto and can be + used by light-clients, full nodes, or any other application that + needs to sign with a private key.`, + } + cmd.AddCommand( + mnemonicKeyCommand(), + addKeyCommand(), + listKeysCmd(), + showKeysCmd(), + client.LineBreak, + deleteKeyCommand(), + updateKeyCommand(), + ) + return cmd +} diff --git a/client/keys/root_test.go b/client/keys/root_test.go new file mode 100755 index 000000000..1cc2db425 --- /dev/null +++ b/client/keys/root_test.go @@ -0,0 +1,15 @@ +package keys + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommands(t *testing.T) { + rootCommands := Commands() + assert.NotNil(t, rootCommands) + + // Commands are registered + assert.Equal(t, 7, len(rootCommands.Commands())) +} diff --git a/client/keys/show.go b/client/keys/show.go new file mode 100755 index 000000000..8f87482e0 --- /dev/null +++ b/client/keys/show.go @@ -0,0 +1,167 @@ +package keys + +import ( + "errors" + "fmt" + "github.com/cosmos/cosmos-sdk/client" + + "github.com/cosmos/cosmos-sdk/crypto" + "github.com/cosmos/cosmos-sdk/crypto/keys" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + tmcrypto "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/multisig" + "github.com/tendermint/tendermint/libs/cli" +) + +const ( + // FlagAddress is the flag for the user's address on the command line. + FlagAddress = "address" + // FlagPublicKey represents the user's public key on the command line. + FlagPublicKey = "pubkey" + // FlagBechPrefix defines a desired Bech32 prefix encoding for a key. + FlagBechPrefix = "bech" + // FlagDevice indicates that the information should be shown in the device + FlagDevice = "device" + + flagMultiSigThreshold = "multisig-threshold" + flagShowMultiSig = "show-multisig" + + defaultMultiSigKeyName = "multi" +) + +func showKeysCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "show [name [name...]]", + Short: "Show key info for the given name", + Long: `Return public details of a single local key. If multiple names are +provided, then an ephemeral multisig key will be created under the name "multi" +consisting of all the keys provided by name and multisig threshold.`, + Args: cobra.MinimumNArgs(1), + RunE: runShowCmd, + } + + cmd.Flags().String(FlagBechPrefix, sdk.PrefixAccount, "The Bech32 prefix encoding for a key (acc|val|cons)") + cmd.Flags().BoolP(FlagAddress, "a", false, "Output the address only (overrides --output)") + cmd.Flags().BoolP(FlagPublicKey, "p", false, "Output the public key only (overrides --output)") + cmd.Flags().BoolP(FlagDevice, "d", false, "Output the address in a ledger device") + cmd.Flags().Uint(flagMultiSigThreshold, 1, "K out of N required signatures") + cmd.Flags().BoolP(flagShowMultiSig, "m", false, "Output multisig pubkey constituents, threshold, and weights") + cmd.Flags().Bool(client.FlagIndentResponse, false, "Add indent to JSON response") + + return cmd +} + +func runShowCmd(cmd *cobra.Command, args []string) (err error) { + var info keys.Info + + if len(args) == 1 { + info, err = GetKeyInfo(args[0]) + if err != nil { + return err + } + } else { + pks := make([]tmcrypto.PubKey, len(args)) + for i, keyName := range args { + info, err := GetKeyInfo(keyName) + if err != nil { + return err + } + + pks[i] = info.GetPubKey() + } + + multisigThreshold := viper.GetInt(flagMultiSigThreshold) + err = validateMultisigThreshold(multisigThreshold, len(args)) + if err != nil { + return err + } + + multikey := multisig.NewPubKeyMultisigThreshold(multisigThreshold, pks) + info = keys.NewMultiInfo(defaultMultiSigKeyName, multikey) + } + + isShowAddr := viper.GetBool(FlagAddress) + isShowPubKey := viper.GetBool(FlagPublicKey) + isShowDevice := viper.GetBool(FlagDevice) + isShowMultiSig := viper.GetBool(flagShowMultiSig) + + isOutputSet := false + tmp := cmd.Flag(cli.OutputFlag) + if tmp != nil { + isOutputSet = tmp.Changed + } + + if isShowAddr && isShowPubKey { + return errors.New("cannot use both --address and --pubkey at once") + } + + if isOutputSet && (isShowAddr || isShowPubKey) { + return errors.New("cannot use --output with --address or --pubkey") + } + + bechKeyOut, err := getBechKeyOut(viper.GetString(FlagBechPrefix)) + if err != nil { + return err + } + + switch { + case isShowAddr: + printKeyAddress(info, bechKeyOut) + case isShowPubKey: + printPubKey(info, bechKeyOut) + case isShowMultiSig: + printMultiSigKeyInfo(info, bechKeyOut) + default: + printKeyInfo(info, bechKeyOut) + } + + if isShowDevice { + if isShowPubKey { + return fmt.Errorf("the device flag (-d) can only be used for addresses not pubkeys") + } + if viper.GetString(FlagBechPrefix) != "acc" { + return fmt.Errorf("the device flag (-d) can only be used for accounts") + } + // Override and show in the device + if info.GetType() != keys.TypeLedger { + return fmt.Errorf("the device flag (-d) can only be used for accounts stored in devices") + } + + hdpath, err := info.GetPath() + if err != nil { + return nil + } + + return crypto.LedgerShowAddress(*hdpath, info.GetPubKey()) + } + + return nil +} + +func validateMultisigThreshold(k, nKeys int) error { + if k <= 0 { + return fmt.Errorf("threshold must be a positive integer") + } + if nKeys < k { + return fmt.Errorf( + "threshold k of n multisignature: %d < %d", nKeys, k) + } + return nil +} + +func getBechKeyOut(bechPrefix string) (bechKeyOutFn, error) { + switch bechPrefix { + case sdk.PrefixAccount: + return keys.Bech32KeyOutput, nil + case sdk.PrefixValidator: + return keys.Bech32ValKeyOutput, nil + case sdk.PrefixConsensus: + return keys.Bech32ConsKeyOutput, nil + } + + return nil, fmt.Errorf("invalid Bech32 prefix encoding provided: %s", bechPrefix) +} diff --git a/client/keys/show_test.go b/client/keys/show_test.go new file mode 100755 index 000000000..26f438eb7 --- /dev/null +++ b/client/keys/show_test.go @@ -0,0 +1,164 @@ +package keys + +import ( + "testing" + + "github.com/terra-project/core/testutil" + + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/tests" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/multisig" + "github.com/tendermint/tendermint/crypto/secp256k1" + "github.com/tendermint/tendermint/libs/cli" +) + +func Test_multiSigKey_Properties(t *testing.T) { + testutil.PrepareCmdTest() + + tmpKey1 := secp256k1.GenPrivKeySecp256k1([]byte("mySecret")) + pk := multisig.NewPubKeyMultisigThreshold(1, []crypto.PubKey{tmpKey1.PubKey()}) + tmp := keys.NewMultiInfo("myMultisig", pk) + + assert.Equal(t, "myMultisig", tmp.GetName()) + assert.Equal(t, keys.TypeMulti, tmp.GetType()) + assert.Equal(t, "D3923267FA8A3DD367BB768FA8BDC8FF7F89DA3F", tmp.GetPubKey().Address().String()) + assert.Equal(t, "terra16wfryel63g7axeamw68630wglalcnk3lfxxa0c", tmp.GetAddress().String()) +} + +func Test_showKeysCmd(t *testing.T) { + cmd := showKeysCmd() + assert.NotNil(t, cmd) + assert.Equal(t, "false", cmd.Flag(FlagAddress).DefValue) + assert.Equal(t, "false", cmd.Flag(FlagPublicKey).DefValue) +} + +func Test_runShowCmd(t *testing.T) { + cmd := showKeysCmd() + + err := runShowCmd(cmd, []string{"invalid"}) + assert.EqualError(t, err, "Key invalid not found") + + err = runShowCmd(cmd, []string{"invalid1", "invalid2"}) + assert.EqualError(t, err, "Key invalid1 not found") + + // Prepare a key base + // Now add a temporary keybase + kbHome, cleanUp := tests.NewTestCaseDir(t) + defer cleanUp() + viper.Set(cli.HomeFlag, kbHome) + + fakeKeyName1 := "runShowCmd_Key1" + fakeKeyName2 := "runShowCmd_Key2" + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName1, tests.TestMnemonic, "", "", 0, 0) + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName2, tests.TestMnemonic, "", "", 0, 1) + assert.NoError(t, err) + + // Now try single key + err = runShowCmd(cmd, []string{fakeKeyName1}) + assert.EqualError(t, err, "invalid Bech32 prefix encoding provided: ") + + // Now try single key - set bech to acc + viper.Set(FlagBechPrefix, sdk.PrefixAccount) + err = runShowCmd(cmd, []string{fakeKeyName1}) + assert.NoError(t, err) + + // Now try multisig key - set bech to acc + viper.Set(FlagBechPrefix, sdk.PrefixAccount) + viper.Set(flagMultiSigThreshold, 0) + err = runShowCmd(cmd, []string{fakeKeyName1, fakeKeyName2}) + assert.EqualError(t, err, "threshold must be a positive integer") + + // Now try multisig key - set bech to acc + threshold=2 + viper.Set(FlagBechPrefix, sdk.PrefixAccount) + viper.Set(flagMultiSigThreshold, 2) + err = runShowCmd(cmd, []string{fakeKeyName1, fakeKeyName2}) + assert.NoError(t, err) + + // Now try multisig key - set bech to acc + threshold=2 + viper.Set(FlagBechPrefix, "acc") + viper.Set(FlagDevice, true) + viper.Set(flagMultiSigThreshold, 2) + err = runShowCmd(cmd, []string{fakeKeyName1, fakeKeyName2}) + assert.EqualError(t, err, "the device flag (-d) can only be used for accounts stored in devices") + + viper.Set(FlagBechPrefix, "val") + err = runShowCmd(cmd, []string{fakeKeyName1, fakeKeyName2}) + assert.EqualError(t, err, "the device flag (-d) can only be used for accounts") + + viper.Set(FlagPublicKey, true) + err = runShowCmd(cmd, []string{fakeKeyName1, fakeKeyName2}) + assert.EqualError(t, err, "the device flag (-d) can only be used for addresses not pubkeys") + + // TODO: Capture stdout and compare +} + +func Test_validateMultisigThreshold(t *testing.T) { + type args struct { + k int + nKeys int + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"zeros", args{0, 0}, true}, + {"1-0", args{1, 0}, true}, + {"1-1", args{1, 1}, false}, + {"1-2", args{1, 1}, false}, + {"1-2", args{2, 1}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateMultisigThreshold(tt.args.k, tt.args.nKeys); (err != nil) != tt.wantErr { + t.Errorf("validateMultisigThreshold() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_getBechKeyOut(t *testing.T) { + type args struct { + bechPrefix string + } + tests := []struct { + name string + args args + want bechKeyOutFn + wantErr bool + }{ + {"empty", args{""}, nil, true}, + {"wrong", args{"???"}, nil, true}, + {"acc", args{sdk.PrefixAccount}, keys.Bech32KeyOutput, false}, + {"val", args{sdk.PrefixValidator}, keys.Bech32ValKeyOutput, false}, + {"cons", args{sdk.PrefixConsensus}, keys.Bech32ConsKeyOutput, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getBechKeyOut(tt.args.bechPrefix) + if (err != nil) != tt.wantErr { + t.Errorf("getBechKeyOut() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + assert.NotNil(t, got) + } + + // TODO: Still not possible to compare functions + // Maybe in next release: https://github.com/stretchr/testify/issues/182 + //if &got != &tt.want { + // t.Errorf("getBechKeyOut() = %v, want %v", got, tt.want) + //} + }) + } +} diff --git a/client/keys/types.go b/client/keys/types.go new file mode 100755 index 000000000..079ef4962 --- /dev/null +++ b/client/keys/types.go @@ -0,0 +1,55 @@ +package keys + +// used for outputting keys.Info over REST + +// AddNewKey request a new key +type AddNewKey struct { + Name string `json:"name"` + Password string `json:"password"` + Mnemonic string `json:"mnemonic"` + Account int `json:"account,string,omitempty"` + Index int `json:"index,string,omitempty"` +} + +// NewAddNewKey constructs a new AddNewKey request structure. +func NewAddNewKey(name, password, mnemonic string, account, index int) AddNewKey { + return AddNewKey{ + Name: name, + Password: password, + Mnemonic: mnemonic, + Account: account, + Index: index, + } +} + +// RecoverKeyBody recovers a key +type RecoverKey struct { + Password string `json:"password"` + Mnemonic string `json:"mnemonic"` + Account int `json:"account,string,omitempty"` + Index int `json:"index,string,omitempty"` +} + +// NewRecoverKey constructs a new RecoverKey request structure. +func NewRecoverKey(password, mnemonic string, account, index int) RecoverKey { + return RecoverKey{Password: password, Mnemonic: mnemonic, Account: account, Index: index} +} + +// UpdateKeyReq requests updating a key +type UpdateKeyReq struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} + +// NewUpdateKeyReq constructs a new UpdateKeyReq structure. +func NewUpdateKeyReq(old, new string) UpdateKeyReq { + return UpdateKeyReq{OldPassword: old, NewPassword: new} +} + +// DeleteKeyReq requests deleting a key +type DeleteKeyReq struct { + Password string `json:"password"` +} + +// NewDeleteKeyReq constructs a new DeleteKeyReq structure. +func NewDeleteKeyReq(password string) DeleteKeyReq { return DeleteKeyReq{Password: password} } diff --git a/client/keys/update.go b/client/keys/update.go new file mode 100755 index 000000000..392286d48 --- /dev/null +++ b/client/keys/update.go @@ -0,0 +1,47 @@ +package keys + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/spf13/cobra" +) + +func updateKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Change the password used to protect private key", + RunE: runUpdateCmd, + Args: cobra.ExactArgs(1), + } + return cmd +} + +func runUpdateCmd(cmd *cobra.Command, args []string) error { + name := args[0] + + buf := client.BufferStdin() + kb, err := NewKeyBaseFromHomeFlag() + if err != nil { + return err + } + oldpass, err := client.GetPassword( + "Enter the current passphrase:", buf) + if err != nil { + return err + } + + getNewpass := func() (string, error) { + return client.GetCheckPassword( + "Enter the new passphrase:", + "Repeat the new passphrase:", buf) + } + + err = kb.Update(name, oldpass, getNewpass) + if err != nil { + return err + } + fmt.Println("Password successfully updated!") + return nil +} diff --git a/client/keys/update_test.go b/client/keys/update_test.go new file mode 100755 index 000000000..e1c6784f1 --- /dev/null +++ b/client/keys/update_test.go @@ -0,0 +1,63 @@ +package keys + +import ( + "bufio" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/cosmos/cosmos-sdk/tests" + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + + "github.com/stretchr/testify/assert" +) + +func Test_updateKeyCommand(t *testing.T) { + cmd := updateKeyCommand() + assert.NotNil(t, cmd) + // No flags or defaults to validate +} + +func Test_runUpdateCmd(t *testing.T) { + fakeKeyName1 := "runUpdateCmd_Key1" + fakeKeyName2 := "runUpdateCmd_Key2" + + cmd := updateKeyCommand() + + // fails because it requests a password + err := runUpdateCmd(cmd, []string{fakeKeyName1}) + assert.EqualError(t, err, "EOF") + + cleanUp := client.OverrideStdin(bufio.NewReader(strings.NewReader("pass1234\n"))) + defer cleanUp() + + // try again + err = runUpdateCmd(cmd, []string{fakeKeyName1}) + assert.EqualError(t, err, "Key runUpdateCmd_Key1 not found") + + // Prepare a key base + // Now add a temporary keybase + kbHome, cleanUp1 := tests.NewTestCaseDir(t) + defer cleanUp1() + viper.Set(cli.HomeFlag, kbHome) + + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName1, tests.TestMnemonic, "", "", 0, 0) + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName2, tests.TestMnemonic, "", "", 0, 1) + assert.NoError(t, err) + + // Try again now that we have keys + cleanUp2 := client.OverrideStdin(bufio.NewReader(strings.NewReader("pass1234\nNew1234\nNew1234"))) + defer cleanUp2() + + // Incorrect key type + err = runUpdateCmd(cmd, []string{fakeKeyName1}) + assert.EqualError(t, err, "locally stored key required. Received: keys.offlineInfo") + + // TODO: Check for other type types? + +} diff --git a/client/keys/utils.go b/client/keys/utils.go new file mode 100755 index 000000000..5af2f53e3 --- /dev/null +++ b/client/keys/utils.go @@ -0,0 +1,152 @@ +package keys + +import ( + "fmt" + "path/filepath" + + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keys" +) + +// available output formats. +const ( + OutputFormatText = "text" + OutputFormatJSON = "json" + + // defaultKeyDBName is the client's subdirectory where keys are stored. + defaultKeyDBName = "keys" +) + +type bechKeyOutFn func(keyInfo keys.Info) (keys.KeyOutput, error) + +// GetKeyInfo returns key info for a given name. An error is returned if the +// keybase cannot be retrieved or getting the info fails. +func GetKeyInfo(name string) (keys.Info, error) { + keybase, err := NewKeyBaseFromHomeFlag() + if err != nil { + return nil, err + } + + return keybase.Get(name) +} + +// NewKeyBaseFromHomeFlag initializes a Keybase based on the configuration. +func NewKeyBaseFromHomeFlag() (keys.Keybase, error) { + rootDir := viper.GetString(cli.HomeFlag) + return NewKeyBaseFromDir(rootDir) +} + +// NewKeyBaseFromDir initializes a keybase at a particular dir. +func NewKeyBaseFromDir(rootDir string) (keys.Keybase, error) { + return getLazyKeyBaseFromDir(rootDir) +} + +func getLazyKeyBaseFromDir(rootDir string) (keys.Keybase, error) { + return keys.New(defaultKeyDBName, filepath.Join(rootDir, "keys")), nil +} + +func printKeyTextHeader() { + fmt.Printf("NAME:\tTYPE:\tADDRESS:\t\t\t\t\tPUBKEY:\n") +} + +func printMultiSigKeyTextHeader() { + fmt.Printf("WEIGHT:\tTHRESHOLD:\tADDRESS:\t\t\t\t\tPUBKEY:\n") +} + +func printMultiSigKeyInfo(keyInfo keys.Info, bechKeyOut bechKeyOutFn) { + ko, err := bechKeyOut(keyInfo) + if err != nil { + panic(err) + } + + printMultiSigKeyTextHeader() + printMultiSigKeyOutput(ko) +} + +func printKeyInfo(keyInfo keys.Info, bechKeyOut bechKeyOutFn) { + ko, err := bechKeyOut(keyInfo) + if err != nil { + panic(err) + } + + switch viper.Get(cli.OutputFlag) { + case OutputFormatText: + printKeyTextHeader() + printKeyOutput(ko) + + case OutputFormatJSON: + var out []byte + var err error + if viper.GetBool(client.FlagIndentResponse) { + out, err = cdc.MarshalJSONIndent(ko, "", " ") + } else { + out, err = cdc.MarshalJSON(ko) + } + if err != nil { + panic(err) + } + + fmt.Println(string(out)) + } +} + +func printInfos(infos []keys.Info) { + kos, err := keys.Bech32KeysOutput(infos) + if err != nil { + panic(err) + } + + switch viper.Get(cli.OutputFlag) { + case OutputFormatText: + printKeyTextHeader() + for _, ko := range kos { + printKeyOutput(ko) + } + + case OutputFormatJSON: + var out []byte + var err error + + if viper.GetBool(client.FlagIndentResponse) { + out, err = cdc.MarshalJSONIndent(kos, "", " ") + } else { + out, err = cdc.MarshalJSON(kos) + } + + if err != nil { + panic(err) + } + fmt.Println(string(out)) + } +} + +func printKeyOutput(ko keys.KeyOutput) { + fmt.Printf("%s\t%s\t%s\t%s\n", ko.Name, ko.Type, ko.Address, ko.PubKey) +} + +func printMultiSigKeyOutput(ko keys.KeyOutput) { + for _, pk := range ko.PubKeys { + fmt.Printf("%d\t%d\t\t%s\t%s\n", pk.Weight, ko.Threshold, pk.Address, pk.PubKey) + } +} + +func printKeyAddress(info keys.Info, bechKeyOut bechKeyOutFn) { + ko, err := bechKeyOut(info) + if err != nil { + panic(err) + } + + fmt.Println(ko.Address) +} + +func printPubKey(info keys.Info, bechKeyOut bechKeyOutFn) { + ko, err := bechKeyOut(info) + if err != nil { + panic(err) + } + + fmt.Println(ko.PubKey) +} diff --git a/cmd/terracli/main.go b/cmd/terracli/main.go index 28214f371..4deb12cf0 100755 --- a/cmd/terracli/main.go +++ b/cmd/terracli/main.go @@ -14,11 +14,11 @@ import ( "github.com/tendermint/tendermint/libs/cli" "github.com/terra-project/core/app" + "github.com/terra-project/core/client/keys" "github.com/terra-project/core/types/util" "github.com/terra-project/core/version" "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/keys" "github.com/cosmos/cosmos-sdk/client/lcd" "github.com/cosmos/cosmos-sdk/client/rpc" "github.com/cosmos/cosmos-sdk/client/tx" @@ -74,8 +74,8 @@ func main() { // Read in the configuration file for the sdk config := sdk.GetConfig() - config.SetCoinType(330) - config.SetFullFundraiserPath("44'/330'/0'/0/0") + config.SetCoinType(util.CoinType) + config.SetFullFundraiserPath(util.FullFundraiserPath) config.SetBech32PrefixForAccount(util.Bech32PrefixAccAddr, util.Bech32PrefixAccPub) config.SetBech32PrefixForValidator(util.Bech32PrefixValAddr, util.Bech32PrefixValPub) config.SetBech32PrefixForConsensusNode(util.Bech32PrefixConsAddr, util.Bech32PrefixConsPub) diff --git a/cmd/terrad/main.go b/cmd/terrad/main.go index 1e05de74c..9c581a722 100755 --- a/cmd/terrad/main.go +++ b/cmd/terrad/main.go @@ -36,8 +36,8 @@ func main() { cdc := app.MakeCodec() config := sdk.GetConfig() - config.SetCoinType(330) - config.SetFullFundraiserPath("44'/330'/0'/0/0") + config.SetCoinType(util.CoinType) + config.SetFullFundraiserPath(util.FullFundraiserPath) config.SetBech32PrefixForAccount(util.Bech32PrefixAccAddr, util.Bech32PrefixAccPub) config.SetBech32PrefixForValidator(util.Bech32PrefixValAddr, util.Bech32PrefixValPub) config.SetBech32PrefixForConsensusNode(util.Bech32PrefixConsAddr, util.Bech32PrefixConsPub) diff --git a/go.mod b/go.mod index 4aee86756..a339e2dec 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/terra-project/core go 1.12 require ( + github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d github.com/btcsuite/btcd v0.0.0-20190315201642-aa6e0f35703c // indirect github.com/btcsuite/btcutil v0.0.0-20190316010144-3ac1210f4b38 // indirect github.com/cosmos/cosmos-sdk v0.0.0-00010101000000-000000000000 - github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d // indirect + github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d github.com/golangci/golangci-lint v1.17.1 // indirect github.com/gorilla/mux v1.7.0 github.com/pkg/errors v0.8.1 diff --git a/types/util/hdpath.go b/types/util/hdpath.go new file mode 100644 index 000000000..781624a1a --- /dev/null +++ b/types/util/hdpath.go @@ -0,0 +1,8 @@ +package util + +const ( + // CoinType defines LUNA bip44 coin type + CoinType = uint32(330) + // FullFundraiserPath defines full fundraiser path for LUNA coin type + FullFundraiserPath = "44'/330'/0'/0/0" +)