diff --git a/ioctl/newcmd/account/account.go b/ioctl/newcmd/account/account.go index 98b68915c3..27057ee02e 100644 --- a/ioctl/newcmd/account/account.go +++ b/ioctl/newcmd/account/account.go @@ -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") @@ -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") } @@ -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") @@ -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") @@ -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") diff --git a/ioctl/newcmd/hdwallet/hdwalletderive.go b/ioctl/newcmd/hdwallet/hdwalletderive.go index 71f1ec74a1..c60ab6faf6 100644 --- a/ioctl/newcmd/hdwallet/hdwalletderive.go +++ b/ioctl/newcmd/hdwallet/hdwalletderive.go @@ -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 @@ -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 } @@ -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 } diff --git a/ioctl/newcmd/hdwallet/hdwalletderive_test.go b/ioctl/newcmd/hdwallet/hdwalletderive_test.go new file mode 100644 index 0000000000..40828b53f1 --- /dev/null +++ b/ioctl/newcmd/hdwallet/hdwalletderive_test.go @@ -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()) + }) +}