Repository: iotexproject/iotex-core (go)

Issue:
[ioctl] build contract compile command line into new ioctl
### What would you like to be added
 - Step 1: create `contractcompile.go` and `contractcompile_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/contractcompile.go in `contractcompile.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/contractcompile.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 description:
[ioctl] Build contract compile command line into new ioctl
# Description
Build `contract` and `contractcompile` 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 #3304 

## 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] TestNewContractCmd
- [x] TestNewContractCompileCmd

**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
new file mode 100644
index 000000000..8fc9e2082
--- /dev/null
+++ b/ioctl/newcmd/contract/contract.go
@@ -0,0 +1,75 @@
+// 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 (
+	"github.com/ethereum/go-ethereum/common/compiler"
+	"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/util"
+)
+
+const _solCompiler = "solc"
+
+// Multi-language support
+var (
+	_contractCmdShorts = map[config.Language]string{
+		config.English: "Deal with smart contract of IoTeX blockchain",
+		config.Chinese: "处理IoTeX区块链的智能合约",
+	}
+	_flagInitialAmountUsage = map[config.Language]string{
+		config.English: "transfer an initial amount to the new deployed contract",
+		config.Chinese: "为部署的新合约转入一笔初始资金",
+	}
+)
+
+// NewContractCmd represents the contract command
+func NewContractCmd(client ioctl.Client) *cobra.Command {
+	short, _ := client.SelectTranslation(_contractCmdShorts)
+	cmd := &cobra.Command{
+		Use:   "contract",
+		Short: short,
+	}
+
+	// TODO add sub commands
+	// cmd.AddCommand(NewContractPrepareCmd)
+	// cmd.AddCommand(NewContractCompileCmd)
+	// cmd.AddCommand(NewContractDeployCmd)
+	// cmd.AddCommand(NewContractInvokeCmd)
+	// cmd.AddCommand(NewContractTestCmd)
+	// cmd.AddCommand(NewContractShareCmd)
+	cmd.AddCommand(NewContractCompileCmd(client))
+	client.SetEndpointWithFlag(cmd.PersistentFlags().StringVar)
+	client.SetInsecureWithFlag(cmd.PersistentFlags().BoolVar)
+	return cmd
+}
+
+// Compile compiles smart contract from source code
+func Compile(sourceFiles ...string) (map[string]*compiler.Contract, error) {
+	solc, err := util.SolidityVersion(_solCompiler)
+	if err != nil {
+		return nil, errors.Wrap(err, "solidity compiler not ready")
+	}
+	if !checkCompilerVersion(solc) {
+		return nil, errors.Errorf("unsupported solc version %d.%d.%d", solc.Major, solc.Minor, solc.Patch)
+	}
+
+	contracts, err := util.CompileSolidity(_solCompiler, sourceFiles...)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to compile")
+	}
+	return contracts, nil
+}
+
+func checkCompilerVersion(solc *util.Solidity) bool {
+	if solc.Major == 0 && solc.Minor == 8 {
+		return true
+	}
+	return false
+}
diff --git a/ioctl/newcmd/contract/contractcompile.go b/ioctl/newcmd/contract/contractcompile.go
new file mode 100644
index 000000000..92e0289e1
--- /dev/null
+++ b/ioctl/newcmd/contract/contractcompile.go
@@ -0,0 +1,124 @@
+// 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/json"
+	"os"
+	"strings"
+
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+
+	"github.com/iotexproject/iotex-core/ioctl"
+	"github.com/iotexproject/iotex-core/ioctl/config"
+)
+
+// Multi-language support
+var (
+	_contractCompileCmdUses = map[config.Language]string{
+		config.English: "compile CONTRACT_NAME [CODE_FILES...] [--abi-out ABI_PATH] [--bin-out BIN_PATH]",
+		config.Chinese: "compile 合约名 [代码文件...] [--abi-out ABI路径] [--bin-out BIN路径]",
+	}
+	_contractCompileCmdShorts = map[config.Language]string{
+		config.English: "Compile smart contract of IoTeX blockchain from source code file(s).",
+		config.Chinese: "编译IoTeX区块链的智能合约代码,支持多文件编译",
+	}
+	_flagAbiOutUsage = map[config.Language]string{
+		config.English: "set abi file output path",
+		config.Chinese: "设置abi文件输出路径",
+	}
+	_flagBinOutUsage = map[config.Language]string{
+		config.English: "set bin file output path",
+		config.Chinese: "设置bin文件输出路径",
+	}
+)
+
+// NewContractCompileCmd represents the contract compile command
+func NewContractCompileCmd(client ioctl.Client) *cobra.Command {
+	var (
+		_abiOut string
+		_binOut string
+	)
+
+	use, _ := client.SelectTranslation(_contractCompileCmdUses)
+	short, _ := client.SelectTranslation(_contractCompileCmdShorts)
+	flagAbi, _ := client.SelectTranslation(_flagAbiOutUsage)
+	flagBin, _ := client.SelectTranslation(_flagBinOutUsage)
+
+	cmd := &cobra.Command{
+		Use:   use,
+		Short: short,
+		Args:  cobra.MinimumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			cmd.SilenceUsage = true
+			contractName := args[0]
+
+			files := args[1:]
+			if len(files) == 0 {
+				dirInfo, err := os.ReadDir("./")
+				if err != nil {
+					return errors.Wrap(err, "failed to get current directory")
+				}
+
+				for _, fileInfo := range dirInfo {
+					if !fileInfo.IsDir() && strings.HasSuffix(fileInfo.Name(), ".sol") {
+						files = append(files, fileInfo.Name())
+					}
+				}
+
+				if len(files) == 0 {
+					return errors.New("failed to get source file(s)")
+				}
+			}
+
+			contracts, err := Compile(files...)
+			if err != nil {
+				return errors.WithMessage(err, "failed to compile")
+			}
+
+			for name := range contracts {
+				if name == contractName {
+					break
+				}
+				nameSplit := strings.Split(name, ":")
+				if nameSplit[len(nameSplit)-1] == contractName {
+					contractName = name
+					break
+				}
+			}
+
+			contract, ok := contracts[contractName]
+			if !ok {
+				return errors.Errorf("failed to find out contract %s", contractName)
+			}
+
+			abiByte, err := json.Marshal(contract.Info.AbiDefinition)
+			if err != nil {
+				return errors.Wrap(err, "failed to marshal abi")
+			}
+			cmd.Printf("======= %s =======\nBinary:\n%s\nContract JSON ABI\n%s", contractName, contract.Code, string(abiByte))
+
+			if _binOut != "" {
+				// bin file starts with "0x" prefix
+				if err := os.WriteFile(_binOut, []byte(contract.Code), 0600); err != nil {
+					return errors.Wrap(err, "failed to write bin file")
+				}
+			}
+
+			if _abiOut != "" {
+				if err := os.WriteFile(_abiOut, abiByte, 0600); err != nil {
+					return errors.Wrap(err, "failed to write abi file")
+				}
+			}
+
+			return nil
+		},
+	}
+	cmd.Flags().StringVar(&_abiOut, "abi-out", "", flagAbi)
+	cmd.Flags().StringVar(&_binOut, "bin-out", "", flagBin)
+	return cmd
+}


Test patch:
diff --git a/ioctl/newcmd/contract/contract_test.go b/ioctl/newcmd/contract/contract_test.go
new file mode 100644
index 000000000..6ebe8c881
--- /dev/null
+++ b/ioctl/newcmd/contract/contract_test.go
@@ -0,0 +1,33 @@
+// 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 (
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"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 TestNewContractCmd(t *testing.T) {
+	require := require.New(t)
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	client := mock_ioctlclient.NewMockClient(ctrl)
+
+	client.EXPECT().SelectTranslation(gomock.Any()).Return("contract", config.English).AnyTimes()
+	client.EXPECT().SetEndpointWithFlag(gomock.Any())
+	client.EXPECT().SetInsecureWithFlag(gomock.Any())
+
+	cmd := NewContractCmd(client)
+	result, err := util.ExecuteCmd(cmd)
+	require.NoError(err)
+	require.Contains(result, "Available Commands")
+}
diff --git a/ioctl/newcmd/contract/contractcompile_test.go b/ioctl/newcmd/contract/contractcompile_test.go
new file mode 100644
index 000000000..37cd6818a
--- /dev/null
+++ b/ioctl/newcmd/contract/contractcompile_test.go
@@ -0,0 +1,65 @@
+// 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 (
+	"os/exec"
+	"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 TestNewContractCompileCmd(t *testing.T) {
+	skipWithoutSolc(t)
+	require := require.New(t)
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	client := mock_ioctlclient.NewMockClient(ctrl)
+
+	client.EXPECT().SelectTranslation(gomock.Any()).Return("contract", config.English).AnyTimes()
+	client.EXPECT().SetEndpointWithFlag(gomock.Any()).AnyTimes()
+	client.EXPECT().SetInsecureWithFlag(gomock.Any()).AnyTimes()
+
+	t.Run("compile contract", func(t *testing.T) {
+		cmd := NewContractCompileCmd(client)
+		result, err := util.ExecuteCmd(cmd, "A", "../../../action/protocol/execution/testdata-london/array-return.sol")
+		require.NoError(err)
+		require.Contains(result, "array-return.sol:A")
+	})
+
+	t.Run("failed to get source file(s)", func(t *testing.T) {
+		expectedErr := errors.New("failed to get source file(s)")
+		cmd := NewContractCompileCmd(client)
+		_, err := util.ExecuteCmd(cmd, "HelloWorld")
+		require.Contains(err.Error(), expectedErr.Error())
+	})
+
+	t.Run("failed to find out contract", func(t *testing.T) {
+		expectedErr := errors.New("failed to find out contract WrongContract")
+		cmd := NewContractCompileCmd(client)
+		_, err := util.ExecuteCmd(cmd, "WrongContract", "../../../action/protocol/execution/testdata-london/array-return.sol")
+		require.Contains(err.Error(), expectedErr.Error())
+	})
+
+	t.Run("failed to compile", func(t *testing.T) {
+		expectedErr := errors.New("failed to compile")
+		cmd := NewContractCompileCmd(client)
+		_, err := util.ExecuteCmd(cmd, "", "")
+		require.Contains(err.Error(), expectedErr.Error())
+	})
+}
+
+func skipWithoutSolc(t *testing.T) {
+	if _, err := exec.LookPath(_solCompiler); err != nil {
+		t.Skip(err)
+	}
+}