Repository: iotexproject/iotex-core (go)

Issue:
[ioctl] build contract test bytecode command line into new ioctl 
### What would you like to be added
 - Step 1: create `contracttestbytecode.go` and `contracttestbytecode_test.go` files in `./ioctl/newcmd/contract`. create this directory if not exist. 
 - Step 2: refactor  command  https://github.com/iotexproject/iotex-core/blob/master/ioctl/cmd/contract/contracttestbytecode.go in `contracttestbytecode.go`
 - Step 3: add unit test in xx_test.go

#### Notes:
1. Use [client interface](https://github.com/iotexproject/iotex-core/blob/master/ioctl/client.go) to construct the Cobra command.
2. [Output package](https://github.com/iotexproject/iotex-core/blob/master/ioctl/output) is deprecated. `e.g.` `output.PrintError` could be replaced with `errors.Wrap`.
3. Global variables (config.ReadConfig  config.DefaultConfigFile etc.) have been wrapped into client interface. Please use [client interface](https://github.com/iotexproject/iotex-core/blob/master/ioctl/client.go) to access them if needed
4. The logic of new code should be similar to [the old one](https://github.com/iotexproject/iotex-core/blob/master/ioctl/cmd/contract/contracttestbytecode.go).
5. In each unit test, the results should be checked whether they are as the expected. 




#### Example:
1. https://github.com/iotexproject/iotex-core/blob/master/ioctl/newcmd/account/accountinfo.go
2. https://github.com/iotexproject/iotex-core/blob/master/ioctl/newcmd/account/accountdelete.go
3. https://github.com/iotexproject/iotex-core/blob/master/ioctl/newcmd/account/accountcreateadd.go

### Why is this needed
Unit tests couldn't be added for the old ioctl command (./ioctl/cmd) 


Pull request:
[ioctl] Build contract test bytecode command line into new ioctl
# Description
Build `contract` and `contracttestbytecode` command in new `ioctl`, with the following note.

* Use [client interface](https://github.com/iotexproject/iotex-core/blob/master/ioctl/client.go) to construct the Cobra command.
* Output package is deprecated, replace it with errors package.
* Replace fmt.Println with cmd.Println
* Refactor unit test to cover the modification.

Fixes #3314  

## Type of change
Please delete options that are not relevant.
- [x] Code refactor or improvement

# How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [x] make test
- [x] TestNewContractTestBytecodeCmd

**Test Configuration**:
- Firmware version: Ubuntu 21.04
- Hardware:
- Toolchain:
- SDK:

# Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules

Patch:
diff --git a/ioctl/newcmd/contract/contract.go b/ioctl/newcmd/contract/contract.go
index 8fc9e2082..d0b824d58 100644
--- a/ioctl/newcmd/contract/contract.go
+++ b/ioctl/newcmd/contract/contract.go
@@ -6,6 +6,8 @@
 package contract
 
 import (
+	"encoding/hex"
+
 	"github.com/ethereum/go-ethereum/common/compiler"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
@@ -73,3 +75,7 @@ func checkCompilerVersion(solc *util.Solidity) bool {
 	}
 	return false
 }
+
+func decodeBytecode(bytecode string) ([]byte, error) {
+	return hex.DecodeString(util.TrimHexPrefix(bytecode))
+}


Test patch:
diff --git a/ioctl/newcmd/contract/contracttestbytecode.go b/ioctl/newcmd/contract/contracttestbytecode.go
new file mode 100644
index 000000000..cec3e24e7
--- /dev/null
+++ b/ioctl/newcmd/contract/contracttestbytecode.go
@@ -0,0 +1,82 @@
+// Copyright (c) 2022 IoTeX Foundation
+// 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 contract
+
+import (
+	"math/big"
+
+	"github.com/iotexproject/iotex-address/address"
+	"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/newcmd/action"
+	"github.com/iotexproject/iotex-core/ioctl/util"
+)
+
+// Multi-language support
+var (
+	_testBytecodeCmdUses = map[config.Language]string{
+		config.English: "bytecode (CONTRACT_ADDRESS|ALIAS) PACKED_ARGUMENTS [AMOUNT_IOTX]",
+		config.Chinese: "bytecode (合约地址|别名) 已打包参数 [IOTX数量]",
+	}
+	_testBytecodeCmdShorts = map[config.Language]string{
+		config.English: "test smart contract on IoTeX blockchain with packed arguments",
+		config.Chinese: "传入bytecode测试IoTeX区块链上的智能合约",
+	}
+)
+
+// NewContractTestBytecodeCmd represents the contract test bytecode command
+func NewContractTestBytecodeCmd(client ioctl.Client) *cobra.Command {
+	use, _ := client.SelectTranslation(_testBytecodeCmdUses)
+	short, _ := client.SelectTranslation(_testBytecodeCmdShorts)
+
+	cmd := &cobra.Command{
+		Use:   use,
+		Short: short,
+		Args:  cobra.RangeArgs(2, 3),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			cmd.SilenceUsage = true
+			addr, err := client.Address(args[0])
+			if err != nil {
+				return errors.Wrap(err, "failed to get contract address")
+			}
+
+			contract, err := address.FromString(addr)
+			if err != nil {
+				return errors.Wrap(err, "failed to convert string into address")
+			}
+
+			bytecode, err := decodeBytecode(args[1])
+			if err != nil {
+				return errors.Wrap(err, "invalid bytecode")
+			}
+
+			amount := big.NewInt(0)
+			if len(args) == 3 {
+				amount, err = util.StringToRau(args[2], util.IotxDecimalNum)
+				if err != nil {
+					return errors.Wrap(err, "invalid amount")
+				}
+			}
+
+			_, signer, _, _, gasLimit, _, err := action.GetWriteCommandFlag(cmd)
+			if err != nil {
+				return err
+			}
+			result, err := action.Read(client, contract, amount.String(), bytecode, signer, gasLimit)
+			if err != nil {
+				return errors.Wrap(err, "failed to read contract")
+			}
+
+			cmd.Printf("return: %s\n", result)
+			return nil
+		},
+	}
+	action.RegisterWriteCommand(client, cmd)
+	return cmd
+}
diff --git a/ioctl/newcmd/contract/contracttestbytecode_test.go b/ioctl/newcmd/contract/contracttestbytecode_test.go
new file mode 100644
index 000000000..e7fe9108a
--- /dev/null
+++ b/ioctl/newcmd/contract/contracttestbytecode_test.go
@@ -0,0 +1,97 @@
+// Copyright (c) 2022 IoTeX Foundation
+// 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 contract
+
+import (
+	"encoding/hex"
+	"testing"
+
+	"github.com/ethereum/go-ethereum/accounts/keystore"
+	"github.com/golang/mock/gomock"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
+
+	"github.com/iotexproject/iotex-address/address"
+	"github.com/iotexproject/iotex-proto/golang/iotexapi"
+	"github.com/iotexproject/iotex-proto/golang/iotexapi/mock_iotexapi"
+
+	"github.com/iotexproject/iotex-core/ioctl/config"
+	"github.com/iotexproject/iotex-core/ioctl/util"
+	"github.com/iotexproject/iotex-core/test/identityset"
+	"github.com/iotexproject/iotex-core/test/mock/mock_ioctlclient"
+)
+
+func TestNewContractTestBytecodeCmd(t *testing.T) {
+	require := require.New(t)
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	client := mock_ioctlclient.NewMockClient(ctrl)
+	apiServiceClient := mock_iotexapi.NewMockAPIServiceClient(ctrl)
+	addr := identityset.Address(0).String()
+
+	ks := keystore.NewKeyStore(t.TempDir(), 2, 1)
+	acc, err := ks.NewAccount("")
+	require.NoError(err)
+	accAddr, err := address.FromBytes(acc.Address.Bytes())
+	require.NoError(err)
+
+	client.EXPECT().SelectTranslation(gomock.Any()).Return("contract", config.English).Times(40)
+	client.EXPECT().SetEndpointWithFlag(gomock.Any()).AnyTimes()
+	client.EXPECT().SetInsecureWithFlag(gomock.Any()).AnyTimes()
+	client.EXPECT().Alias(gomock.Any()).Return("producer", nil).AnyTimes()
+	client.EXPECT().APIServiceClient().Return(apiServiceClient, nil).AnyTimes()
+	client.EXPECT().IsCryptoSm2().Return(false).AnyTimes()
+	client.EXPECT().ReadSecret().Return("", nil).AnyTimes()
+	client.EXPECT().Address(gomock.Any()).Return(accAddr.String(), nil).Times(4)
+	client.EXPECT().AddressWithDefaultIfNotExist(gomock.Any()).Return(accAddr.String(), nil).AnyTimes()
+	client.EXPECT().NewKeyStore().Return(ks).AnyTimes()
+	client.EXPECT().AskToConfirm(gomock.Any()).Return(true, nil).AnyTimes()
+	client.EXPECT().Config().Return(config.Config{
+		Explorer: "iotexscan",
+		Endpoint: "testnet1",
+	}).AnyTimes()
+
+	apiServiceClient.EXPECT().ReadContract(gomock.Any(), gomock.Any()).Return(&iotexapi.ReadContractResponse{
+		Data: hex.EncodeToString([]byte("60fe47b100000000000000000000000000000000000000000000000000000000")),
+	}, nil)
+
+	t.Run("compile contract", func(t *testing.T) {
+		cmd := NewContractTestBytecodeCmd(client)
+		result, err := util.ExecuteCmd(cmd, addr, "a9059cbb0000000000000000000000004867c4bada9553216bf296c4c64e9ff0749206490000000000000000000000000000000000000000000000000000000000000001")
+		require.NoError(err)
+		require.Contains(result, "return")
+	})
+
+	t.Run("failed to read contract", func(t *testing.T) {
+		expectedErr := errors.New("failed to read contract")
+		apiServiceClient.EXPECT().ReadContract(gomock.Any(), gomock.Any()).Return(nil, expectedErr)
+		cmd := NewContractTestBytecodeCmd(client)
+		_, err := util.ExecuteCmd(cmd, addr, "a9059cbb0000000000000000000000004867c4bada9553216bf296c4c64e9ff0749206490000000000000000000000000000000000000000000000000000000000000001")
+		require.Contains(err.Error(), expectedErr.Error())
+	})
+
+	t.Run("invalid amount", func(t *testing.T) {
+		expectedErr := errors.New("invalid amount")
+		cmd := NewContractTestBytecodeCmd(client)
+		_, err := util.ExecuteCmd(cmd, addr, "a9059cbb0000000000000000000000004867c4bada9553216bf296c4c64e9ff0749206490000000000000000000000000000000000000000000000000000000000000001", "test")
+		require.Contains(err.Error(), expectedErr.Error())
+	})
+
+	t.Run("invalid bytecode", func(t *testing.T) {
+		expectedErr := errors.New("invalid bytecode")
+		cmd := NewContractTestBytecodeCmd(client)
+		_, err := util.ExecuteCmd(cmd, addr, "test")
+		require.Contains(err.Error(), expectedErr.Error())
+	})
+
+	t.Run("failed to get contract address", func(t *testing.T) {
+		expectedErr := errors.New("failed to get contract address")
+		client.EXPECT().Address(gomock.Any()).Return("", expectedErr)
+		cmd := NewContractTestBytecodeCmd(client)
+		_, err := util.ExecuteCmd(cmd, "test", "")
+		require.Contains(err.Error(), expectedErr.Error())
+	})
+}