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()) + }) }