Skip to content

Commit

Permalink
[ioctl] Build hdwallet import command line into new ioctl (#3419)
Browse files Browse the repository at this point in the history
* [ioctl] build hdwallet import command line into new ioctl
Co-authored-by: huofei <[email protected]>
  • Loading branch information
LuckyPigeon authored Jun 20, 2022
1 parent 6f33246 commit 61d189f
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 9 deletions.
52 changes: 44 additions & 8 deletions ioctl/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package ioctl

import (
"bufio"
"bytes"
"context"
"crypto/ecdsa"
Expand All @@ -28,6 +29,7 @@ import (
"github.com/iotexproject/iotex-core/ioctl/config"
"github.com/iotexproject/iotex-core/ioctl/util"
"github.com/iotexproject/iotex-core/ioctl/validator"
"github.com/iotexproject/iotex-core/pkg/util/fileutil"
)

type (
Expand Down Expand Up @@ -75,15 +77,22 @@ type (
IsCryptoSm2() bool
// QueryAnalyser sends request to Analyser endpoint
QueryAnalyser(interface{}) (*http.Response, error)
// ReadInput reads the input from stdin
ReadInput() (string, error)
// WriteHdWalletConfigFile write encrypting mnemonic into config file
WriteHdWalletConfigFile(string, string) error
// IsHdWalletConfigFileExist return true if config file is existed, false if not existed
IsHdWalletConfigFileExist() bool
}

client struct {
cfg config.Config
conn *grpc.ClientConn
cryptoSm2 bool
configFilePath string
endpoint string
insecure bool
cfg config.Config
conn *grpc.ClientConn
cryptoSm2 bool
configFilePath string
endpoint string
insecure bool
hdWalletConfigFile string
}

// Option sets client construction parameter
Expand All @@ -106,8 +115,9 @@ func EnableCryptoSm2() Option {
// NewClient creates a new ioctl client
func NewClient(cfg config.Config, configFilePath string, opts ...Option) Client {
c := &client{
cfg: cfg,
configFilePath: configFilePath,
cfg: cfg,
configFilePath: configFilePath,
hdWalletConfigFile: cfg.Wallet + "/hdwallet",
}
for _, opt := range opts {
opt(c)
Expand Down Expand Up @@ -310,6 +320,32 @@ func (c *client) QueryAnalyser(reqData interface{}) (*http.Response, error) {
return resp, nil
}

func (c *client) ReadInput() (string, error) { // notest
in := bufio.NewReader(os.Stdin)
line, err := in.ReadString('\n')
if err != nil {
return "", err
}
return line, nil
}

func (c *client) WriteHdWalletConfigFile(mnemonic string, password string) error {
enctxt := append([]byte(mnemonic), util.HashSHA256([]byte(mnemonic))...)
enckey := util.HashSHA256([]byte(password))
out, err := util.Encrypt(enctxt, enckey)
if err != nil {
return errors.Wrap(err, "failed to encrypting mnemonic")
}
if err := os.WriteFile(c.hdWalletConfigFile, out, 0600); err != nil {
return errors.Wrapf(err, "failed to write to config file %s", c.hdWalletConfigFile)
}
return nil
}

func (c *client) IsHdWalletConfigFileExist() bool { // notest
return fileutil.FileExists(c.hdWalletConfigFile)
}

func (m *ConfirmationMessage) String() string {
line := fmt.Sprintf("%s\nOptions:", m.Info)
for _, option := range m.Options {
Expand Down
14 changes: 14 additions & 0 deletions ioctl/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,20 @@ func TestDeleteAlias(t *testing.T) {
}
}

func TestWriteHdWalletConfigFile(t *testing.T) {
r := require.New(t)
testPathWallet, err := os.MkdirTemp(os.TempDir(), "cfgWallet")
r.NoError(err)
defer testutil.CleanupPath(testPathWallet)

c := NewClient(config.Config{
Wallet: testPathWallet,
}, testPathWallet+"/config.default")
mnemonic := "lake stove quarter shove dry matrix hire split wide attract argue core"
password := "123"
r.NoError(c.WriteHdWalletConfigFile(mnemonic, password))
}

func writeTempConfig(t *testing.T, cfg *config.Config) string {
r := require.New(t)
testPathd, err := os.MkdirTemp(os.TempDir(), "testConfig")
Expand Down
13 changes: 12 additions & 1 deletion ioctl/newcmd/hdwallet/hdwallet.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
// Copyright (c) 2019 IoTeX Foundation
// Copyright (c) 2022 IoTeX Foundation
// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no
// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent
// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache
// License 2.0 that can be found in the LICENSE file.

package hdwallet

import (
"github.com/pkg/errors"
)

// Errors
var (
ErrPasswdNotMatch = errors.New("password doesn't match")
)

// DefaultRootDerivationPath for iotex
// https:/satoshilabs/slips/blob/master/slip-0044.md
const DefaultRootDerivationPath = "m/44'/304'"

var _hdWalletConfigFile string
81 changes: 81 additions & 0 deletions ioctl/newcmd/hdwallet/hdwalletimport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) 2022 IoTeX Foundation
// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no
// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent
// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache
// License 2.0 that can be found in the LICENSE file.

package hdwallet

import (
"strings"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/tyler-smith/go-bip39"

"github.com/iotexproject/iotex-core/ioctl"
"github.com/iotexproject/iotex-core/ioctl/config"
)

// Multi-language support
var (
_importCmdShorts = map[config.Language]string{
config.English: "import hdwallet using mnemonic",
config.Chinese: "通过助记词导入钱包",
}
_importCmdUses = map[config.Language]string{
config.English: "import",
config.Chinese: "import 导入",
}
)

// NewHdwalletImportCmd represents the hdwallet import command
func NewHdwalletImportCmd(client ioctl.Client) *cobra.Command {
use, _ := client.SelectTranslation(_importCmdUses)
short, _ := client.SelectTranslation(_importCmdShorts)

return &cobra.Command{
Use: use,
Short: short,
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
if client.IsHdWalletConfigFileExist() {
cmd.Println("Please run 'ioctl hdwallet delete' before import")
return nil
}

cmd.Println("Enter 12 mnemonic words you saved, separated by space")

line, err := client.ReadInput()
if err != nil {
return err
}
mnemonic := strings.TrimSpace(line)
if _, err = bip39.MnemonicToByteArray(mnemonic); err != nil {
return err
}

cmd.Println("Set password")
password, err := client.ReadSecret()
if err != nil {
return errors.Wrap(err, "failed to get password")
}
cmd.Println("Enter password again")
passwordAgain, err := client.ReadSecret()
if err != nil {
return errors.Wrap(err, "failed to get password")
}
if password != passwordAgain {
return ErrPasswdNotMatch
}

if err := client.WriteHdWalletConfigFile(mnemonic, password); err != nil {
return errors.Wrap(err, "failed to write to config file")
}
cmd.Printf("Mnemonic phrase: %s\n"+
"It is used to recover your wallet in case you forgot the password. Write them down and store it in a safe place.\n", mnemonic)
return nil
},
}
}
55 changes: 55 additions & 0 deletions ioctl/newcmd/hdwallet/hdwalletimport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2022 IoTeX
// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no
// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent
// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache
// License 2.0 that can be found in the LICENSE file.

package hdwallet

import (
"testing"

"github.com/golang/mock/gomock"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"

"github.com/iotexproject/iotex-core/ioctl/config"
"github.com/iotexproject/iotex-core/ioctl/util"
"github.com/iotexproject/iotex-core/test/mock/mock_ioctlclient"
)

func TestNewHdwalletImportCmd(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
client := mock_ioctlclient.NewMockClient(ctrl)

mnemonic := "lake stove quarter shove dry matrix hire split wide attract argue core"
password := "123"

client.EXPECT().SelectTranslation(gomock.Any()).Return("mockTranslationString", config.English).Times(6)
client.EXPECT().IsHdWalletConfigFileExist().Return(false).Times(2)

t.Run("import hdwallet", func(t *testing.T) {
client.EXPECT().ReadInput().Return(mnemonic, nil)
client.EXPECT().ReadSecret().Return(password, nil)
client.EXPECT().ReadSecret().Return(password, nil)
client.EXPECT().WriteHdWalletConfigFile(gomock.Any(), gomock.Any()).Return(nil)

cmd := NewHdwalletImportCmd(client)
result, err := util.ExecuteCmd(cmd)
require.NoError(err)
require.Contains(result, mnemonic)
})

t.Run("failed to write to config file", func(t *testing.T) {
expectErr := errors.New("failed to write to config file")
client.EXPECT().ReadInput().Return(mnemonic, nil)
client.EXPECT().ReadSecret().Return(password, nil)
client.EXPECT().ReadSecret().Return(password, nil)
client.EXPECT().WriteHdWalletConfigFile(gomock.Any(), gomock.Any()).Return(expectErr)

cmd := NewHdwalletImportCmd(client)
_, err := util.ExecuteCmd(cmd)
require.Contains(err.Error(), expectErr.Error())
})
}
43 changes: 43 additions & 0 deletions test/mock/mock_ioctlclient/mock_ioctlclient.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 61d189f

Please sign in to comment.