From 612495da6df02ec63bdf0ae384f3a243538ffa05 Mon Sep 17 00:00:00 2001
From: Jeremy Chou <lucky90322@gmail.com>
Date: Sat, 25 Jun 2022 14:38:24 +0800
Subject: [PATCH] [ioctl] Refactor nodereward command in new ioctl (#3416)

* [ioctl] refactor nodereward command in new ioctl

* refactor unit test to cover the modification

* refactor new nodereward as ioctl/cmd/nodereward structure

* refactor unit test to cover the modification

* delete useless message

* fix client.APIServiceClient()

Co-authored-by: huofei <68298506@qq.com>
Co-authored-by: CoderZhi <thecoderzhi@gmail.com>
---
 ioctl/newcmd/node/nodereward.go      | 239 ++++++++++++++-------------
 ioctl/newcmd/node/nodereward_test.go | 164 +++++++++++++-----
 2 files changed, 247 insertions(+), 156 deletions(-)

diff --git a/ioctl/newcmd/node/nodereward.go b/ioctl/newcmd/node/nodereward.go
index 19d0e493f6..3ab44fc421 100644
--- a/ioctl/newcmd/node/nodereward.go
+++ b/ioctl/newcmd/node/nodereward.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
@@ -8,165 +8,166 @@ package node
 
 import (
 	"context"
-	"fmt"
 	"math/big"
 
+	"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
+	"github.com/iotexproject/iotex-proto/golang/iotexapi"
+	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 	"google.golang.org/grpc/status"
 
-	"github.com/iotexproject/iotex-proto/golang/iotexapi"
-
 	"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"
 )
 
 // Multi-language support
 var (
-	_rewardUses = map[config.Language]string{
-		config.English: "reward [ALIAS|DELEGATE_ADDRESS]",
-		config.Chinese: "reward [别名|委托地址]",
+	_rewardCmdUses = map[config.Language]string{
+		config.English: "reward unclaimed|pool [ALIAS|DELEGATE_ADDRESS]",
+		config.Chinese: "reward 未支取|奖金池 [别名|委托地址]",
 	}
-	_rewardShorts = map[config.Language]string{
+	_rewardCmdShorts = map[config.Language]string{
 		config.English: "Query rewards",
 		config.Chinese: "查询奖励",
 	}
-	_rewardPoolMessageTranslations = map[config.Language]string{
-		config.English: "Available Reward: %s IOTX   Total Reward: %s IOTX",
-		config.Chinese: "可用奖金: %s IOTX   总奖金: %s IOTX",
+	_rewardPoolLong = map[config.Language]string{
+		config.English: "ioctl node reward pool returns unclaimed and available Rewards in fund pool.\nTotalUnclaimed is the amount of all delegates that have been issued but are not claimed;\nTotalAvailable is the amount of balance that has not been issued to anyone.\n\nioctl node reward unclaimed [ALIAS|DELEGATE_ADDRESS] returns unclaimed rewards of a specific delegate.",
+		config.Chinese: "ioctl node reward 返回奖金池中的未支取奖励和可获取的奖励. TotalUnclaimed是所有代表已被发放但未支取的奖励的总和; TotalAvailable 是奖金池中未被发放的奖励的总和.\n\nioctl node [ALIAS|DELEGATE_ADDRESS] 返回特定代表的已被发放但未支取的奖励.",
 	}
 )
 
 // NewNodeRewardCmd represents the node reward command
-func NewNodeRewardCmd(c ioctl.Client) *cobra.Command {
-	use, _ := c.SelectTranslation(_rewardUses)
-	short, _ := c.SelectTranslation(_rewardShorts)
-	rewardPoolMessageTranslation, _ := c.SelectTranslation(_rewardPoolMessageTranslations)
-	nc := &cobra.Command{
+func NewNodeRewardCmd(client ioctl.Client) *cobra.Command {
+	use, _ := client.SelectTranslation(_rewardCmdUses)
+	short, _ := client.SelectTranslation(_rewardCmdShorts)
+	long, _ := client.SelectTranslation(_rewardPoolLong)
+
+	return &cobra.Command{
 		Use:   use,
 		Short: short,
-		Args:  cobra.MaximumNArgs(1),
+		Args:  cobra.RangeArgs(1, 2),
+		Long:  long,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
-			var err error
-			if len(args) == 0 {
-
-				apiClient, err := c.APIServiceClient()
-				if err != nil {
-					return err
-				}
-
-				response, err := apiClient.ReadState(
-					context.Background(),
-					&iotexapi.ReadStateRequest{
-						ProtocolID: []byte("rewarding"),
-						MethodName: []byte("AvailableBalance"),
-					},
-				)
-
-				if err != nil {
-					sta, ok := status.FromError(err)
-					if ok {
-						return output.NewError(output.APIError, sta.Message(), nil)
-					}
-					return output.NewError(output.NetworkError, "failed to invoke ReadState api", err)
-				}
-
-				availableRewardRau, ok := new(big.Int).SetString(string(response.Data), 10)
-				if !ok {
-					return output.NewError(output.ConvertError, "failed to convert string into big int", err)
+			switch args[0] {
+			case "pool":
+				if len(args) != 1 {
+					return errors.New("wrong number of arg(s) for ioctl node reward pool command. \nRun 'ioctl node reward --help' for usage")
 				}
-
-				response, err = apiClient.ReadState(
-					context.Background(),
-					&iotexapi.ReadStateRequest{
-						ProtocolID: []byte("rewarding"),
-						MethodName: []byte("TotalBalance"),
-					},
-				)
+				totalUnclaimed, totalAvailable, totalBalance, err := rewardPool(client)
 				if err != nil {
-					sta, ok := status.FromError(err)
-					if ok {
-						return output.NewError(output.APIError, sta.Message(), nil)
-					}
-					return output.NewError(output.NetworkError, "failed to invoke ReadState api", err)
-				}
-				totalRewardRau, ok := new(big.Int).SetString(string(response.Data), 10)
-				if !ok {
-					return output.NewError(output.ConvertError, "failed to convert string into big int", err)
-				}
-
-				message := rewardPoolMessage{
-					AvailableReward: util.RauToString(availableRewardRau, util.IotxDecimalNum),
-					TotalReward:     util.RauToString(totalRewardRau, util.IotxDecimalNum),
+					return err
 				}
-				fmt.Println(message.String(rewardPoolMessageTranslation))
-
-			} else {
-				arg := args[0]
-				address, err := c.Address(arg)
-				if err != nil {
-					return output.NewError(output.AddressError, "failed to get address", err)
+				cmd.Printf("Total Unclaimed:\t %s IOTX\nTotal Available:\t %s IOTX\nTotal Balance:\t\t %s IOTX\n",
+					totalUnclaimed, totalAvailable, totalBalance)
+			case "unclaimed":
+				if len(args) != 2 {
+					return errors.New("wrong number of arg(s) for ioctl node reward unclaimed [ALIAS|DELEGATE_ADDRESS] command. \nRun 'ioctl node reward --help' for usage")
 				}
-				apiClient, err := c.APIServiceClient()
+				address, reward, err := reward(client, args[1])
 				if err != nil {
 					return err
 				}
-
-				response, err := apiClient.ReadState(
-					context.Background(),
-					&iotexapi.ReadStateRequest{
-						ProtocolID: []byte("rewarding"),
-						MethodName: []byte("UnclaimedBalance"),
-						Arguments:  [][]byte{[]byte(address)},
-					},
-				)
-				if err != nil {
-					sta, ok := status.FromError(err)
-					if ok {
-						return output.NewError(output.APIError, sta.Message(), nil)
-					}
-					return output.NewError(output.NetworkError, "failed to get version from server", err)
-				}
-				rewardRau, ok := new(big.Int).SetString(string(response.Data), 10)
-				if !ok {
-					return output.NewError(output.ConvertError, "failed to convert string into big int", err)
-				}
-				message := rewardMessage{Address: address, Reward: util.RauToString(rewardRau, util.IotxDecimalNum)}
-				fmt.Println(message.String())
-
+				cmd.Printf("%s: %s IOTX\n", address, reward)
+			default:
+				return errors.New("unknown command. \nRun 'ioctl node reward --help' for usage")
 			}
-			return output.PrintError(err)
+			return nil
 		},
 	}
-	return nc
 }
 
-type rewardPoolMessage struct {
-	AvailableReward string `json:"availableReward"`
-	TotalReward     string `json:"totalReward"`
-}
+func rewardPool(client ioctl.Client) (string, string, string, error) {
+	apiClient, err := client.APIServiceClient()
+	if err != nil {
+		return "", "", "", err
+	}
+	ctx := context.Background()
+	jwtMD, err := util.JwtAuth()
+	if err == nil {
+		ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx)
+	}
+	response, err := apiClient.ReadState(
+		ctx,
+		&iotexapi.ReadStateRequest{
+			ProtocolID: []byte("rewarding"),
+			MethodName: []byte("AvailableBalance"),
+		},
+	)
+	if err != nil {
+		sta, ok := status.FromError(err)
+		if ok {
+			return "", "", "", errors.New(sta.Message())
+		}
+		return "", "", "", errors.Wrap(err, "failed to invoke ReadState api")
+	}
 
-func (m *rewardPoolMessage) String(trans ...string) string {
+	availableRewardRau, ok := new(big.Int).SetString(string(response.Data), 10)
+	if !ok {
+		return "", "", "", errors.New("failed to convert string into big int")
+	}
 
-	if output.Format == "" {
-		message := fmt.Sprintf(trans[0],
-			m.AvailableReward, m.TotalReward)
-		return message
+	response, err = apiClient.ReadState(
+		context.Background(),
+		&iotexapi.ReadStateRequest{
+			ProtocolID: []byte("rewarding"),
+			MethodName: []byte("TotalBalance"),
+		},
+	)
+	if err != nil {
+		sta, ok := status.FromError(err)
+		if ok {
+			return "", "", "", errors.New(sta.Message())
+		}
+		return "", "", "", errors.Wrap(err, "failed to invoke ReadState api")
 	}
-	return output.FormatStringWithTrans(output.Result, m)
-}
+	totalRewardRau, ok := new(big.Int).SetString(string(response.Data), 10)
+	if !ok {
+		return "", "", "", errors.New("failed to convert string into big int")
+	}
+
+	totalUnclaimedRewardRau := big.NewInt(0)
+	totalUnclaimedRewardRau.Sub(totalRewardRau, availableRewardRau)
 
-type rewardMessage struct {
-	Address string `json:"address"`
-	Reward  string `json:"reward"`
+	return util.RauToString(totalUnclaimedRewardRau, util.IotxDecimalNum),
+		util.RauToString(availableRewardRau, util.IotxDecimalNum),
+		util.RauToString(totalRewardRau, util.IotxDecimalNum),
+		err
 }
 
-func (m *rewardMessage) String(trans ...string) string {
-	if output.Format == "" {
-		message := fmt.Sprintf("%s: %s IOTX", m.Address, m.Reward)
-		return message
+func reward(client ioctl.Client, arg string) (string, string, error) {
+	address, err := client.Address(arg)
+	if err != nil {
+		return "", "", errors.Wrap(err, "failed to get address")
+	}
+	apiClient, err := client.APIServiceClient()
+	if err != nil {
+		return "", "", errors.Wrap(err, "failed to connect to endpoint")
+	}
+	ctx := context.Background()
+	jwtMD, err := util.JwtAuth()
+	if err == nil {
+		ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx)
+	}
+	response, err := apiClient.ReadState(
+		ctx,
+		&iotexapi.ReadStateRequest{
+			ProtocolID: []byte("rewarding"),
+			MethodName: []byte("UnclaimedBalance"),
+			Arguments:  [][]byte{[]byte(address)},
+		},
+	)
+	if err != nil {
+		sta, ok := status.FromError(err)
+		if ok {
+			return "", "", errors.New(sta.Message())
+		}
+		return "", "", errors.Wrap(err, "failed to get version from server")
+	}
+	rewardRau, ok := new(big.Int).SetString(string(response.Data), 10)
+	if !ok {
+		return "", "", errors.New("failed to convert string into big int")
 	}
-	return output.FormatStringWithTrans(output.Result, m)
+	return address, util.RauToString(rewardRau, util.IotxDecimalNum), err
 }
diff --git a/ioctl/newcmd/node/nodereward_test.go b/ioctl/newcmd/node/nodereward_test.go
index 51fd347c43..eed0d3c6b0 100644
--- a/ioctl/newcmd/node/nodereward_test.go
+++ b/ioctl/newcmd/node/nodereward_test.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
@@ -12,6 +12,7 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/iotexproject/iotex-proto/golang/iotexapi"
 	"github.com/iotexproject/iotex-proto/golang/iotexapi/mock_iotexapi"
+	"github.com/pkg/errors"
 	"github.com/stretchr/testify/require"
 
 	"github.com/iotexproject/iotex-core/ioctl/config"
@@ -20,46 +21,135 @@ import (
 )
 
 func TestNewNodeRewardCmd(t *testing.T) {
+	require := require.New(t)
 	ctrl := gomock.NewController(t)
-
 	client := mock_ioctlclient.NewMockClient(ctrl)
-	client.EXPECT().SelectTranslation(gomock.Any()).Return("mockTranslationString", config.English).AnyTimes()
+	apiClient := mock_iotexapi.NewMockAPIServiceClient(ctrl)
 
-	client.EXPECT().Address(gomock.Any()).Return("test_address", nil).Times(1)
+	client.EXPECT().SelectTranslation(gomock.Any()).Return("mockTranslationString", config.English).Times(24)
+	client.EXPECT().APIServiceClient().Return(apiClient, nil).Times(7)
 
-	apiClient := mock_iotexapi.NewMockAPIServiceClient(ctrl)
+	t.Run("get node reward pool", func(t *testing.T) {
+		t.Run("get available reward & total reward", func(t *testing.T) {
+			apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
+				ProtocolID: []byte("rewarding"),
+				MethodName: []byte("AvailableBalance"),
+			}).Return(&iotexapi.ReadStateResponse{
+				Data: []byte("24361490367906930338205776")},
+				nil)
+
+			apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
+				ProtocolID: []byte("rewarding"),
+				MethodName: []byte("TotalBalance"),
+			}).Return(&iotexapi.ReadStateResponse{
+				Data: []byte("52331682309272536203174665")},
+				nil)
+
+			cmd := NewNodeRewardCmd(client)
+			result, err := util.ExecuteCmd(cmd, "pool")
+			require.NoError(err)
+			require.Contains(result, "24361490.367906930338205776")
+			require.Contains(result, "52331682.309272536203174665")
+		})
+
+		t.Run("failed to invoke AvailableBalance api", func(t *testing.T) {
+			expectedErr := errors.New("failed to invoke ReadState api")
+
+			apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
+				ProtocolID: []byte("rewarding"),
+				MethodName: []byte("AvailableBalance"),
+			}).Return(nil, expectedErr)
+
+			cmd := NewNodeRewardCmd(client)
+			_, err := util.ExecuteCmd(cmd, "pool")
+			require.Contains(err.Error(), expectedErr.Error())
+		})
+
+		t.Run("failed to convert string into big int", func(t *testing.T) {
+			expectedErr := errors.New("failed to convert string into big int")
+
+			apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
+				ProtocolID: []byte("rewarding"),
+				MethodName: []byte("AvailableBalance"),
+			}).Return(&iotexapi.ReadStateResponse{
+				Data: []byte("0x24361490367906930338205776")},
+				nil)
+
+			cmd := NewNodeRewardCmd(client)
+			_, err := util.ExecuteCmd(cmd, "pool")
+			require.Contains(err.Error(), expectedErr.Error())
+		})
+
+		t.Run("failed to invoke TotalBalance api", func(t *testing.T) {
+			expectedErr := errors.New("failed to invoke ReadState api")
+
+			apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
+				ProtocolID: []byte("rewarding"),
+				MethodName: []byte("AvailableBalance"),
+			}).Return(&iotexapi.ReadStateResponse{
+				Data: []byte("24361490367906930338205776")},
+				nil)
+
+			apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
+				ProtocolID: []byte("rewarding"),
+				MethodName: []byte("TotalBalance"),
+			}).Return(nil, expectedErr)
+
+			cmd := NewNodeRewardCmd(client)
+			_, err := util.ExecuteCmd(cmd, "pool")
+			require.Contains(err.Error(), expectedErr.Error())
+		})
+	})
+
+	t.Run("get unclaimed node reward", func(t *testing.T) {
+		t.Run("get balance by address", func(t *testing.T) {
+			client.EXPECT().Address(gomock.Any()).Return("test_address", nil).Times(1)
+
+			apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
+				ProtocolID: []byte("rewarding"),
+				MethodName: []byte("UnclaimedBalance"),
+				Arguments:  [][]byte{[]byte("test_address")},
+			}).Return(&iotexapi.ReadStateResponse{
+				Data: []byte("0"),
+			}, nil)
+
+			cmd := NewNodeRewardCmd(client)
+			result, err := util.ExecuteCmd(cmd, "unclaimed", "test")
+			require.NoError(err)
+			require.Contains(result, "test_address: 0 IOTX")
+		})
+
+		t.Run("failed to get address", func(t *testing.T) {
+			expectedErr := errors.New("failed to get address")
+			client.EXPECT().Address(gomock.Any()).Return("", expectedErr)
+
+			cmd := NewNodeRewardCmd(client)
+			_, err := util.ExecuteCmd(cmd, "unclaimed", "test")
+			require.Contains(err.Error(), expectedErr.Error())
+		})
+
+		t.Run("failed to get version from server", func(t *testing.T) {
+			expectedErr := errors.New("failed to get version from server")
+
+			client.EXPECT().Address(gomock.Any()).Return("test_address", nil).Times(1)
+
+			apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
+				ProtocolID: []byte("rewarding"),
+				MethodName: []byte("UnclaimedBalance"),
+				Arguments:  [][]byte{[]byte("test_address")},
+			}).Return(nil, expectedErr)
+
+			cmd := NewNodeRewardCmd(client)
+			_, err := util.ExecuteCmd(cmd, "unclaimed", "test")
+			require.Contains(err.Error(), expectedErr.Error())
+		})
+	})
 
-	apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
-		ProtocolID: []byte("rewarding"),
-		MethodName: []byte("UnclaimedBalance"),
-		Arguments:  [][]byte{[]byte("test_address")},
-	}).Return(&iotexapi.ReadStateResponse{
-		Data: []byte("0")},
-		nil)
-
-	apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
-		ProtocolID: []byte("rewarding"),
-		MethodName: []byte("AvailableBalance"),
-	}).Return(&iotexapi.ReadStateResponse{
-		Data: []byte("24361490367906930338205776")},
-		nil)
-
-	apiClient.EXPECT().ReadState(gomock.Any(), &iotexapi.ReadStateRequest{
-		ProtocolID: []byte("rewarding"),
-		MethodName: []byte("TotalBalance"),
-	}).Return(&iotexapi.ReadStateResponse{
-		Data: []byte("52331682309272536203174665")},
-		nil)
-
-	client.EXPECT().APIServiceClient().Return(apiClient, nil).AnyTimes()
-	cmd := NewNodeRewardCmd(client)
-
-	result, err := util.ExecuteCmd(cmd, "test")
-	require.NotNil(t, result)
-	require.NoError(t, err)
-
-	result, err = util.ExecuteCmd(cmd)
-	require.NotNil(t, result)
-	require.NoError(t, err)
+	t.Run("unknown command", func(t *testing.T) {
+		expectedErr := errors.New("unknown command. \nRun 'ioctl node reward --help' for usage")
 
+		cmd := NewNodeRewardCmd(client)
+		_, err := util.ExecuteCmd(cmd, "")
+		require.Contains(err.Error(), expectedErr.Error())
+	})
 }