Skip to content

Commit

Permalink
[ioctl] Build hdwallet derive command line into new ioctl (iotexproje…
Browse files Browse the repository at this point in the history
…ct#3418)

* [ioctl] build hdwallet derive command line into new ioctl

* inline DeriveKey

* adjust order

* revert DeriveKey inline

* create ExportHdwallet client interface

combine hdwalletderive & hdwalletexport code and create ExportHdwallet client interface

* [ioctl] create main for ioctl/newcmd (iotexproject#3296)

* create main for ioctl/newcmd

* change return type of HdwalletMnemonic

* refactor unit test to cover the modification

* refactor unit test to cover the modification

* fix commit

* fix commit

Co-authored-by: huofei <[email protected]>
Co-authored-by: CoderZhi <[email protected]>
  • Loading branch information
3 people authored Jun 30, 2022
1 parent 38fb852 commit a4b1098
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 40 deletions.
16 changes: 8 additions & 8 deletions ioctl/newcmd/account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func PrivateKeyFromSigner(client ioctl.Client, cmd *cobra.Command, signer, passw
}

if password == "" {
cmd.Println(fmt.Sprintf("Enter password for #%s:\n", signer))
cmd.Printf("Enter password for #%s:\n", signer)
password, err = client.ReadSecret()
if err != nil {
return nil, errors.Wrap(err, "failed to get password")
Expand All @@ -160,7 +160,7 @@ func PrivateKeyFromSigner(client ioctl.Client, cmd *cobra.Command, signer, passw
if err != nil {
return nil, errors.Wrap(err, "invalid HDWallet key format")
}
_, prvKey, err = hdwallet.DeriveKey(account, change, index, password)
_, prvKey, err = hdwallet.DeriveKey(client, account, change, index, password)
if err != nil {
return nil, errors.Wrap(err, "failed to derive key from HDWallet")
}
Expand Down Expand Up @@ -218,12 +218,12 @@ func IsSignerExist(client ioctl.Client, signer string) bool {
}

func newAccount(client ioctl.Client, cmd *cobra.Command, alias string) (string, error) {
cmd.Println(fmt.Sprintf("#%s: Set password\n", alias))
cmd.Printf("#%s: Set password\n", alias)
password, err := client.ReadSecret()
if err != nil {
return "", errors.Wrap(err, "failed to get password")
}
cmd.Println(fmt.Sprintf("#%s: Enter password again\n", alias))
cmd.Printf("#%s: Enter password again\n", alias)
passwordAgain, err := client.ReadSecret()
if err != nil {
return "", errors.Wrap(err, "failed to get password")
Expand All @@ -244,12 +244,12 @@ func newAccount(client ioctl.Client, cmd *cobra.Command, alias string) (string,
}

func newAccountSm2(client ioctl.Client, cmd *cobra.Command, alias string) (string, error) {
cmd.Println(fmt.Sprintf("#%s: Set password\n", alias))
cmd.Printf("#%s: Set password\n", alias)
password, err := client.ReadSecret()
if err != nil {
return "", errors.Wrap(err, "failed to get password")
}
cmd.Println(fmt.Sprintf("#%s: Enter password again\n", alias))
cmd.Printf("#%s: Enter password again\n", alias)
passwordAgain, err := client.ReadSecret()
if err != nil {
return "", errors.Wrap(err, "failed to get password")
Expand All @@ -276,12 +276,12 @@ func newAccountSm2(client ioctl.Client, cmd *cobra.Command, alias string) (strin
}

func newAccountByKey(client ioctl.Client, cmd *cobra.Command, alias string, privateKey string) (string, error) {
cmd.Println(fmt.Sprintf("#%s: Set password\n", alias))
cmd.Printf("#%s: Set password\n", alias)
password, err := client.ReadSecret()
if err != nil {
return "", errors.Wrap(err, "failed to get password")
}
cmd.Println(fmt.Sprintf("#%s: Enter password again\n", alias))
cmd.Printf("#%s: Enter password again\n", alias)
passwordAgain, err := client.ReadSecret()
if err != nil {
return "", errors.Wrap(err, "failed to get password")
Expand Down
85 changes: 53 additions & 32 deletions ioctl/newcmd/hdwallet/hdwalletderive.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// 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
Expand All @@ -7,50 +7,71 @@
package hdwallet

import (
"bytes"
"fmt"
"os"

ecrypt "github.com/ethereum/go-ethereum/crypto"
"github.com/iotexproject/go-pkgs/crypto"
hdwallet "github.com/miguelmota/go-ethereum-hdwallet"
"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/iotexproject/iotex-core/ioctl"
"github.com/iotexproject/iotex-core/ioctl/config"
"github.com/iotexproject/iotex-core/ioctl/output"
"github.com/iotexproject/iotex-core/ioctl/util"
"github.com/iotexproject/iotex-core/pkg/util/fileutil"
)

// DeriveKey derives the key according to path
func DeriveKey(account, change, index uint32, password string) (string, crypto.PrivateKey, error) {
// derive key as "m/44'/304'/account'/change/index"
hdWalletConfigFile := config.ReadConfig.Wallet + "/hdwallet"
if !fileutil.FileExists(hdWalletConfigFile) {
return "", nil, output.NewError(output.InputError, "Run 'ioctl hdwallet create' to create your HDWallet first.", nil)
// Multi-language support
var (
_hdwalletDeriveCmdUses = map[config.Language]string{
config.English: "derive id1/id2/id3",
config.Chinese: "derive id1/id2/id3",
}

enctxt, err := os.ReadFile(hdWalletConfigFile)
if err != nil {
return "", nil, output.NewError(output.InputError, "failed to read config", err)
_hdwalletDeriveCmdShorts = map[config.Language]string{
config.English: "derive key from HDWallet",
config.Chinese: "查询HDWallet钱包的派生key地址",
}
)

enckey := util.HashSHA256([]byte(password))
dectxt, err := util.Decrypt(enctxt, enckey)
if err != nil {
return "", nil, output.NewError(output.InputError, "failed to decrypt", err)
}
// NewHdwalletDeriveCmd represents the hdwallet derive command
func NewHdwalletDeriveCmd(client ioctl.Client) *cobra.Command {
use, _ := client.SelectTranslation(_hdwalletDeriveCmdUses)
short, _ := client.SelectTranslation(_hdwalletDeriveCmdShorts)

dectxtLen := len(dectxt)
if dectxtLen <= 32 {
return "", nil, output.NewError(output.ValidationError, "incorrect data", nil)
}
return &cobra.Command{
Use: use,
Short: short,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
signer := "hdw::" + args[0]
account, change, index, err := util.ParseHdwPath(signer)
if err != nil {
return errors.Wrap(err, "invalid hdwallet key format")
}

mnemonic, hash := dectxt[:dectxtLen-32], dectxt[dectxtLen-32:]
if !bytes.Equal(hash, util.HashSHA256(mnemonic)) {
return "", nil, output.NewError(output.ValidationError, "password error", nil)
cmd.Println("Enter password:")
password, err := client.ReadSecret()
if err != nil {
return errors.Wrap(err, "failed to get password")
}

addr, _, err := DeriveKey(client, account, change, index, password)
if err != nil {
return err
}
cmd.Printf("address: %s\n", addr)
return nil
},
}
}

wallet, err := hdwallet.NewFromMnemonic(string(mnemonic))
// DeriveKey derives the key according to path
func DeriveKey(client ioctl.Client, account, change, index uint32, password string) (string, crypto.PrivateKey, error) {
mnemonic, err := client.HdwalletMnemonic(password)
if err != nil {
return "", nil, err
}
wallet, err := hdwallet.NewFromMnemonic(mnemonic)
if err != nil {
return "", nil, err
}
Expand All @@ -59,21 +80,21 @@ func DeriveKey(account, change, index uint32, password string) (string, crypto.P
path := hdwallet.MustParseDerivationPath(derivationPath)
walletAccount, err := wallet.Derive(path, false)
if err != nil {
return "", nil, output.NewError(output.InputError, "failed to get account by derive path", err)
return "", nil, errors.Wrap(err, "failed to get account by derive path")
}

private, err := wallet.PrivateKey(walletAccount)
if err != nil {
return "", nil, output.NewError(output.InputError, "failed to get private key", err)
return "", nil, errors.Wrap(err, "failed to get private key")
}
prvKey, err := crypto.BytesToPrivateKey(ecrypt.FromECDSA(private))
if err != nil {
return "", nil, output.NewError(output.InputError, "failed to Bytes private key", err)
return "", nil, errors.Wrap(err, "failed to Bytes private key")
}

addr := prvKey.PublicKey().Address()
if addr == nil {
return "", nil, output.NewError(output.ConvertError, "failed to convert public key into address", nil)
return "", nil, errors.Wrap(err, "failed to convert public key into address")
}
return addr.String(), prvKey, nil
}
55 changes: 55 additions & 0 deletions ioctl/newcmd/hdwallet/hdwalletderive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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 (
"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 TestNewHdwalletDeriveCmd(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"

client.EXPECT().SelectTranslation(gomock.Any()).Return("mockTranslationString", config.English).Times(6)
client.EXPECT().ReadSecret().Return("cfa6ef757dee2e50351620dca002d32b9c090cfda55fb81f37f1d26b273743f1", nil).Times(2)

t.Run("get address from hdwallet", func(t *testing.T) {
client.EXPECT().HdwalletMnemonic(gomock.Any()).Return(mnemonic, nil)

cmd := NewHdwalletDeriveCmd(client)
result, err := util.ExecuteCmd(cmd, "1/1/2")
require.NoError(err)
require.Contains(result, "address: io1pt2wml6v2tx683ctjn3k9mfgq97zx4c7rzwfuf")
})

t.Run("invalid hdwallet key format", func(t *testing.T) {
expectedErr := errors.New("invalid hdwallet key format")

cmd := NewHdwalletDeriveCmd(client)
_, err := util.ExecuteCmd(cmd, "1")
require.Contains(err.Error(), expectedErr.Error())
})

t.Run("failed to export mnemonic", func(t *testing.T) {
expectedErr := errors.New("failed to export mnemonic")
client.EXPECT().HdwalletMnemonic(gomock.Any()).Return("", expectedErr)

cmd := NewHdwalletDeriveCmd(client)
_, err := util.ExecuteCmd(cmd, "1/1/2")
require.Contains(err.Error(), expectedErr.Error())
})
}

0 comments on commit a4b1098

Please sign in to comment.