diff --git a/ioctl/newcmd/bc/bc.go b/ioctl/newcmd/bc/bc.go index 003e3e4f0e..dcbbe8a10f 100644 --- a/ioctl/newcmd/bc/bc.go +++ b/ioctl/newcmd/bc/bc.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "github.com/iotexproject/iotex-core/ioctl" "github.com/iotexproject/iotex-core/ioctl/config" @@ -129,3 +130,46 @@ func GetProbationList(client ioctl.Client, epochNum uint64, epochStartHeight uin } return response, nil } + +// GetBucketList get bucket list +func GetBucketList( + client ioctl.Client, + methodName iotexapi.ReadStakingDataMethod_Name, + readStakingDataRequest *iotexapi.ReadStakingDataRequest, +) (*iotextypes.VoteBucketList, error) { + apiServiceClient, err := client.APIServiceClient() + if err != nil { + return nil, err + } + methodData, err := proto.Marshal(&iotexapi.ReadStakingDataMethod{Method: methodName}) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal read staking data method") + } + requestData, err := proto.Marshal(readStakingDataRequest) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal read staking data request") + } + request := &iotexapi.ReadStateRequest{ + ProtocolID: []byte("staking"), + MethodName: methodData, + Arguments: [][]byte{requestData}, + } + ctx := context.Background() + if jwtMD, err := util.JwtAuth(); err == nil { + ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx) + } + + response, err := apiServiceClient.ReadState(ctx, request) + if err != nil { + sta, ok := status.FromError(err) + if ok { + return nil, errors.New(sta.Message()) + } + return nil, errors.Wrap(err, "failed to invoke ReadState api") + } + bucketlist := iotextypes.VoteBucketList{} + if err := proto.Unmarshal(response.Data, &bucketlist); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal response") + } + return &bucketlist, nil +} diff --git a/ioctl/newcmd/bc/bc_test.go b/ioctl/newcmd/bc/bc_test.go new file mode 100644 index 0000000000..54768eadd2 --- /dev/null +++ b/ioctl/newcmd/bc/bc_test.go @@ -0,0 +1,49 @@ +// 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 bc + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/iotexproject/iotex-proto/golang/iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotexapi/mock_iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-core/test/mock/mock_ioctlclient" +) + +func TestGetBucketList(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + client := mock_ioctlclient.NewMockClient(ctrl) + apiServiceClient := mock_iotexapi.NewMockAPIServiceClient(ctrl) + request := &iotexapi.ReadStakingDataRequest{} + response := &iotexapi.ReadStateResponse{} + client.EXPECT().APIServiceClient().Return(apiServiceClient, nil).Times(2) + + t.Run("get bucket list", func(t *testing.T) { + expectedValue := &iotextypes.VoteBucketList{ + Buckets: []*iotextypes.VoteBucket{}, + } + apiServiceClient.EXPECT().ReadState(gomock.Any(), gomock.Any()).Return(response, nil) + + result, err := GetBucketList(client, iotexapi.ReadStakingDataMethod_BUCKETS_BY_CANDIDATE, request) + require.NoError(err) + require.Contains(result.String(), expectedValue.String()) + }) + + t.Run("failed to invoke ReadState api", func(t *testing.T) { + expectedErr := errors.New("failed to invoke ReadState api") + apiServiceClient.EXPECT().ReadState(gomock.Any(), gomock.Any()).Return(nil, expectedErr) + + _, err := GetBucketList(client, iotexapi.ReadStakingDataMethod_BUCKETS_BY_VOTER, request) + require.Contains(err.Error(), expectedErr.Error()) + }) +} diff --git a/ioctl/newcmd/bc/bcbucketlist.go b/ioctl/newcmd/bc/bcbucketlist.go new file mode 100644 index 0000000000..6bb4f07ac9 --- /dev/null +++ b/ioctl/newcmd/bc/bcbucketlist.go @@ -0,0 +1,145 @@ +// 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 bc + +import ( + "strconv" + "strings" + + "github.com/iotexproject/iotex-proto/golang/iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/iotexproject/iotex-core/ioctl" + "github.com/iotexproject/iotex-core/ioctl/config" +) + +// constants +const ( + MethodVoter = "voter" + MethodCandidate = "cand" +) + +// Multi-language support +var ( + _bcBucketListCmdShorts = map[config.Language]string{ + config.English: "Get bucket list with method and arg(s) on IoTeX blockchain", + config.Chinese: "根据方法和参数在IoTeX区块链上读取投票列表", + } + _bcBucketListCmdUses = map[config.Language]string{ + config.English: "bucketlist [arguments]", + config.Chinese: "bucketlist <方法> [参数]", + } + _bcBucketListCmdLongs = map[config.Language]string{ + config.English: "Read bucket list\nValid methods: [" + + MethodVoter + ", " + MethodCandidate + "]", + config.Chinese: "根据方法和参数在IoTeX区块链上读取投票列表\n可用方法有:" + + MethodVoter + "," + MethodCandidate, + } +) + +// NewBCBucketListCmd represents the bc bucketlist command +func NewBCBucketListCmd(client ioctl.Client) *cobra.Command { + use, _ := client.SelectTranslation(_bcBucketListCmdUses) + short, _ := client.SelectTranslation(_bcBucketListCmdShorts) + long, _ := client.SelectTranslation(_bcBucketListCmdLongs) + + return &cobra.Command{ + Use: use, + Short: short, + Long: long, + Args: cobra.MinimumNArgs(2), + Example: `ioctl bc bucketlist voter [VOTER_ADDRESS] [OFFSET] [LIMIT] + ioctl bc bucketlist cand [CANDIDATE_NAME] [OFFSET] [LIMIT]`, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + + var ( + bl *iotextypes.VoteBucketList + address string + err error + ) + + offset, limit := uint64(0), uint64(1000) + method, addr := args[0], args[1] + s := args[2:] + + if len(s) > 0 { + offset, err = strconv.ParseUint(s[0], 10, 64) + if err != nil { + return errors.Wrap(err, "invalid offset") + } + } + if len(s) > 1 { + limit, err = strconv.ParseUint(s[1], 10, 64) + if err != nil { + return errors.Wrap(err, "invalid limit") + } + } + switch method { + case MethodVoter: + address, err = client.AddressWithDefaultIfNotExist(addr) + if err != nil { + return err + } + bl, err = getBucketListByVoterAddress(client, address, uint32(offset), uint32(limit)) + case MethodCandidate: + bl, err = getBucketListByCandidateName(client, addr, uint32(offset), uint32(limit)) + default: + return errors.New("unknown ") + } + if err != nil { + return err + } + var lines []string + if len(bl.Buckets) == 0 { + lines = append(lines, "Empty bucketlist with given address") + } else { + for _, b := range bl.Buckets { + bucket, err := newBucket(b) + if err != nil { + return err + } + lines = append(lines, bucket.String()) + } + } + cmd.Println(strings.Join(lines, "\n")) + return nil + }, + } +} + +func getBucketListByVoterAddress(client ioctl.Client, addr string, offset, limit uint32) (*iotextypes.VoteBucketList, error) { + readStakingdataRequest := &iotexapi.ReadStakingDataRequest{ + Request: &iotexapi.ReadStakingDataRequest_BucketsByVoter{ + BucketsByVoter: &iotexapi.ReadStakingDataRequest_VoteBucketsByVoter{ + VoterAddress: addr, + Pagination: &iotexapi.PaginationParam{ + Offset: offset, + Limit: limit, + }, + }, + }, + } + return GetBucketList(client, iotexapi.ReadStakingDataMethod_BUCKETS_BY_VOTER, readStakingdataRequest) +} + +func getBucketListByCandidateName(client ioctl.Client, candName string, offset, limit uint32) (*iotextypes.VoteBucketList, error) { + readStakingDataRequest := &iotexapi.ReadStakingDataRequest{ + Request: &iotexapi.ReadStakingDataRequest_BucketsByCandidate{ + BucketsByCandidate: &iotexapi.ReadStakingDataRequest_VoteBucketsByCandidate{ + CandName: candName, + Pagination: &iotexapi.PaginationParam{ + Offset: offset, + Limit: limit, + }, + }, + }, + } + return GetBucketList(client, iotexapi.ReadStakingDataMethod_BUCKETS_BY_CANDIDATE, readStakingDataRequest) +} diff --git a/ioctl/newcmd/bc/bcbucketlist_test.go b/ioctl/newcmd/bc/bcbucketlist_test.go new file mode 100644 index 0000000000..facec37302 --- /dev/null +++ b/ioctl/newcmd/bc/bcbucketlist_test.go @@ -0,0 +1,104 @@ +// 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 bc + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/iotexproject/iotex-proto/golang/iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotexapi/mock_iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/util" + "github.com/iotexproject/iotex-core/test/mock/mock_ioctlclient" + "github.com/iotexproject/iotex-core/testutil" +) + +func TestNewBCBucketListCmd(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + client := mock_ioctlclient.NewMockClient(ctrl) + apiServiceClient := mock_iotexapi.NewMockAPIServiceClient(ctrl) + + client.EXPECT().SelectTranslation(gomock.Any()).Return("", config.English).Times(21) + client.EXPECT().APIServiceClient().Return(apiServiceClient, nil).Times(2) + client.EXPECT().Config().Return(config.Config{}).Times(2) + + t.Run("get bucket list by voter", func(t *testing.T) { + client.EXPECT().AddressWithDefaultIfNotExist(gomock.Any()).Return("io1uwnr55vqmhf3xeg5phgurlyl702af6eju542sx", nil) + vblist, err := proto.Marshal(&iotextypes.VoteBucketList{ + Buckets: []*iotextypes.VoteBucket{ + { + Index: 1, + StakedAmount: "10", + UnstakeStartTime: timestamppb.New(testutil.TimestampNow()), + }, + { + Index: 2, + StakedAmount: "20", + UnstakeStartTime: timestamppb.New(testutil.TimestampNow()), + }, + }, + }) + require.NoError(err) + apiServiceClient.EXPECT().ReadState(gomock.Any(), gomock.All()).Return(&iotexapi.ReadStateResponse{ + Data: vblist, + }, nil) + + cmd := NewBCBucketListCmd(client) + result, err := util.ExecuteCmd(cmd, "voter", "io1uwnr55vqmhf3xeg5phgurlyl702af6eju542sx") + require.NoError(err) + require.Contains(result, + "index: 1", + "stakedAmount: 0.00000000000000001 IOTX", + "index: 2", + "stakedAmount: 0.00000000000000002 IOTX") + }) + + t.Run("get bucket list by candidate", func(t *testing.T) { + apiServiceClient.EXPECT().ReadState(gomock.Any(), gomock.All()).Return(&iotexapi.ReadStateResponse{}, nil) + cmd := NewBCBucketListCmd(client) + result, err := util.ExecuteCmd(cmd, "cand", "io1uwnr55vqmhf3xeg5phgurlyl702af6eju542sx") + require.NoError(err) + require.Equal("Empty bucketlist with given address\n", result) + }) + + t.Run("invalid voter address", func(t *testing.T) { + expectedErr := errors.New("cannot find address for alias test") + client.EXPECT().AddressWithDefaultIfNotExist(gomock.Any()).Return("", expectedErr) + cmd := NewBCBucketListCmd(client) + _, err := util.ExecuteCmd(cmd, "voter", "test") + require.Contains(err.Error(), expectedErr.Error()) + }) + + t.Run("unknown method", func(t *testing.T) { + expectedErr := errors.New("unknown ") + cmd := NewBCBucketListCmd(client) + _, err := util.ExecuteCmd(cmd, "unknown", "io1uwnr55vqmhf3xeg5phgurlyl702af6eju542sx") + require.Equal(expectedErr.Error(), err.Error()) + }) + + t.Run("invalid offset", func(t *testing.T) { + expectedErr := errors.New("invalid offset") + cmd := NewBCBucketListCmd(client) + _, err := util.ExecuteCmd(cmd, "voter", "io1uwnr55vqmhf3xeg5phgurlyl702af6eju542sx", "test") + require.Contains(err.Error(), expectedErr.Error()) + }) + + t.Run("invalid limit", func(t *testing.T) { + expectedErr := errors.New("invalid limit") + cmd := NewBCBucketListCmd(client) + _, err := util.ExecuteCmd(cmd, "voter", "io1uwnr55vqmhf3xeg5phgurlyl702af6eju542sx", "0", "test") + require.Contains(err.Error(), expectedErr.Error()) + }) +}