diff --git a/CHANGELOG.md b/CHANGELOG.md index ededd10f6baf..097d6b3c91c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ to generate only usage. * [\#4135](https://github.com/cosmos/cosmos-sdk/pull/4135) Fix `NewResponseFormatBroadcastTxCommit` * [\#4053](https://github.com/cosmos/cosmos-sdk/issues/4053) Add `--inv-check-period` flag to gaiad to set period at which invariants checks will run. +* [\#4099](https://github.com/cosmos/cosmos-sdk/issues/4099) Update the /staking/validators endpoint to support +status and pagination query flags. ## 0.34.1 diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index cb479a63f36b..11deeab65600 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -763,7 +763,23 @@ paths: description: Internal Server Error /staking/validators: get: - summary: Get all validator candidates + summary: Get all validator candidates. By default it returns only the bonded validators. + parameters: + - in: query + name: status + type: string + description: The validator bond status. Must be either 'bonded', 'unbonded', or 'unbonding'. + x-example: bonded + - in: query + name: page + description: The gage number. + type: integer + x-example: 1 + - in: query + name: limit + description: The maximum number of items per page. + type: integer + x-example: 1 tags: - ICS21 produces: diff --git a/client/tx/query.go b/client/tx/query.go index 8bb8e10cd705..3cf290044548 100644 --- a/client/tx/query.go +++ b/client/tx/query.go @@ -162,7 +162,6 @@ func QueryTxsByTagsRequestHandlerFn(cliCtx context.CLIContext, cdc *codec.Codec) } tags, page, limit, err = rest.ParseHTTPArgs(r) - if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return diff --git a/types/staking.go b/types/staking.go index 79832ac00ab0..54f28f97279d 100644 --- a/types/staking.go +++ b/types/staking.go @@ -25,19 +25,23 @@ const ( // Constant as this should not change without a hard fork. // TODO: Link to some Tendermint docs, this is very unobvious. ValidatorUpdateDelay int64 = 1 + + BondStatusUnbonded = "Unbonded" + BondStatusUnbonding = "Unbonding" + BondStatusBonded = "Bonded" ) -//BondStatusToString for pretty prints of Bond Status -func BondStatusToString(b BondStatus) string { +// String implements the Stringer interface for BondStatus. +func (b BondStatus) String() string { switch b { case 0x00: - return "Unbonded" + return BondStatusUnbonded case 0x01: - return "Unbonding" + return BondStatusUnbonding case 0x02: - return "Bonded" + return BondStatusBonded default: - panic("improper use of BondStatusToString") + panic("invalid bond status") } } diff --git a/x/staking/alias.go b/x/staking/alias.go index cee0c7b4c475..d1e48ef79081 100644 --- a/x/staking/alias.go +++ b/x/staking/alias.go @@ -36,6 +36,7 @@ type ( QueryValidatorParams = querier.QueryValidatorParams QueryBondsParams = querier.QueryBondsParams QueryRedelegationParams = querier.QueryRedelegationParams + QueryValidatorsParams = querier.QueryValidatorsParams ) var ( @@ -97,10 +98,11 @@ var ( NewMsgUndelegate = types.NewMsgUndelegate NewMsgBeginRedelegate = types.NewMsgBeginRedelegate - NewQuerier = querier.NewQuerier - NewQueryDelegatorParams = querier.NewQueryDelegatorParams - NewQueryValidatorParams = querier.NewQueryValidatorParams - NewQueryBondsParams = querier.NewQueryBondsParams + NewQuerier = querier.NewQuerier + NewQueryDelegatorParams = querier.NewQueryDelegatorParams + NewQueryValidatorParams = querier.NewQueryValidatorParams + NewQueryBondsParams = querier.NewQueryBondsParams + NewQueryValidatorsParams = querier.NewQueryValidatorsParams ) const ( diff --git a/x/staking/client/rest/query.go b/x/staking/client/rest/query.go index 63fcc6d24b92..a9439bd803ae 100644 --- a/x/staking/client/rest/query.go +++ b/x/staking/client/rest/query.go @@ -1,6 +1,7 @@ package rest import ( + "fmt" "net/http" "strings" @@ -15,7 +16,6 @@ import ( ) func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec) { - // Get all delegations from a delegator r.HandleFunc( "/staking/delegators/{delegatorAddr}/delegations", @@ -249,7 +249,31 @@ func delegatorValidatorHandlerFn(cliCtx context.CLIContext, cdc *codec.Codec) ht // HTTP request handler to query list of validators func validatorsHandlerFn(cliCtx context.CLIContext, cdc *codec.Codec) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - res, err := cliCtx.QueryWithData("custom/staking/validators", nil) + _, page, limit, err := rest.ParseHTTPArgs(r) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // override default limit if it wasn't provided + if l := r.FormValue("limit"); l == "" { + limit = 0 + } + + status := r.FormValue("status") + if status == "" { + status = sdk.BondStatusBonded + } + + params := staking.NewQueryValidatorsParams(page, limit, status) + bz, err := cdc.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + route := fmt.Sprintf("custom/%s/%s", staking.QuerierRoute, staking.QueryValidators) + res, err := cliCtx.QueryWithData(route, bz) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return diff --git a/x/staking/keeper/validator_test.go b/x/staking/keeper/validator_test.go index d2aa5b270bea..d359d637c682 100644 --- a/x/staking/keeper/validator_test.go +++ b/x/staking/keeper/validator_test.go @@ -162,9 +162,8 @@ func TestUpdateBondedValidatorsDecreaseCliff(t *testing.T) { assert.Equal( t, status, val.GetStatus(), - fmt.Sprintf("expected validator at index %v to have status: %s", - valIdx, - sdk.BondStatusToString(status))) + fmt.Sprintf("expected validator at index %v to have status: %s", valIdx, status), + ) } } diff --git a/x/staking/querier/querier.go b/x/staking/querier/querier.go index 4230dc6a43bc..8694a35b9ca2 100644 --- a/x/staking/querier/querier.go +++ b/x/staking/querier/querier.go @@ -2,6 +2,7 @@ package querier import ( "fmt" + "strings" abci "github.com/tendermint/tendermint/abci/types" @@ -35,7 +36,7 @@ func NewQuerier(k keep.Keeper, cdc *codec.Codec) sdk.Querier { return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { switch path[0] { case QueryValidators: - return queryValidators(ctx, cdc, k) + return queryValidators(ctx, cdc, req, k) case QueryValidator: return queryValidator(ctx, cdc, req, k) case QueryValidatorDelegations: @@ -128,14 +129,47 @@ func NewQueryRedelegationParams(delegatorAddr sdk.AccAddress, srcValidatorAddr s } } -func queryValidators(ctx sdk.Context, cdc *codec.Codec, k keep.Keeper) (res []byte, err sdk.Error) { +func queryValidators(ctx sdk.Context, cdc *codec.Codec, req abci.RequestQuery, k keep.Keeper) ([]byte, sdk.Error) { + var params QueryValidatorsParams + + err := cdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) + } + stakingParams := k.GetParams(ctx) - validators := k.GetValidators(ctx, stakingParams.MaxValidators) + if params.Limit == 0 { + params.Limit = int(stakingParams.MaxValidators) + } + + validators := k.GetAllValidators(ctx) + filteredVals := make([]types.Validator, 0, len(validators)) + + for _, val := range validators { + if strings.ToLower(val.GetStatus().String()) == strings.ToLower(params.Status) { + filteredVals = append(filteredVals, val) + } + } + + // get pagination bounds + start := (params.Page - 1) * params.Limit + end := params.Limit + start + if end >= len(filteredVals) { + end = len(filteredVals) + } - res, errRes := codec.MarshalJSONIndent(cdc, validators) + if start >= len(filteredVals) { + // page is out of bounds + filteredVals = []types.Validator{} + } else { + filteredVals = filteredVals[start:end] + } + + res, err := codec.MarshalJSONIndent(cdc, filteredVals) if err != nil { - return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", errRes.Error())) + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to JSON marshal result: %s", err.Error())) } + return res, nil } @@ -354,3 +388,14 @@ func queryParameters(ctx sdk.Context, cdc *codec.Codec, k keep.Keeper) (res []by } return res, nil } + +// QueryValidatorsParams defines the params for the following queries: +// - 'custom/staking/validators' +type QueryValidatorsParams struct { + Page, Limit int + Status string +} + +func NewQueryValidatorsParams(page, limit int, status string) QueryValidatorsParams { + return QueryValidatorsParams{page, limit, status} +} diff --git a/x/staking/querier/querier_test.go b/x/staking/querier/querier_test.go index f4fe9592b33d..f83c950706e9 100644 --- a/x/staking/querier/querier_test.go +++ b/x/staking/querier/querier_test.go @@ -1,6 +1,7 @@ package querier import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -44,9 +45,6 @@ func TestNewQuerier(t *testing.T) { require.NotNil(t, err) require.Nil(t, bz) - _, err = querier(ctx, []string{"validators"}, query) - require.Nil(t, err) - _, err = querier(ctx, []string{"pool"}, query) require.Nil(t, err) @@ -121,28 +119,44 @@ func TestQueryValidators(t *testing.T) { params := keeper.GetParams(ctx) // Create Validators - amts := []sdk.Int{sdk.NewInt(9), sdk.NewInt(8)} - var validators [2]types.Validator + amts := []sdk.Int{sdk.NewInt(9), sdk.NewInt(8), sdk.NewInt(7)} + status := []sdk.BondStatus{sdk.Bonded, sdk.Unbonded, sdk.Unbonding} + var validators [3]types.Validator for i, amt := range amts { validators[i] = types.NewValidator(sdk.ValAddress(keep.Addrs[i]), keep.PKs[i], types.Description{}) validators[i], pool, _ = validators[i].AddTokensFromDel(pool, amt) + validators[i], pool = validators[i].UpdateStatus(pool, status[i]) } + keeper.SetPool(ctx, pool) keeper.SetValidator(ctx, validators[0]) keeper.SetValidator(ctx, validators[1]) + keeper.SetValidator(ctx, validators[2]) // Query Validators queriedValidators := keeper.GetValidators(ctx, params.MaxValidators) - res, err := queryValidators(ctx, cdc, keeper) - require.Nil(t, err) + for i, s := range status { + queryValsParams := NewQueryValidatorsParams(1, int(params.MaxValidators), s.String()) + bz, errRes := cdc.MarshalJSON(queryValsParams) + require.Nil(t, errRes) - var validatorsResp []types.Validator - errRes := cdc.UnmarshalJSON(res, &validatorsResp) - require.Nil(t, errRes) + req := abci.RequestQuery{ + Path: fmt.Sprintf("/custom/%s/%s", types.QuerierRoute, QueryValidators), + Data: bz, + } + + res, err := queryValidators(ctx, cdc, req, keeper) + require.Nil(t, err) - require.Equal(t, len(queriedValidators), len(validatorsResp)) - require.ElementsMatch(t, queriedValidators, validatorsResp) + var validatorsResp []types.Validator + errRes = cdc.UnmarshalJSON(res, &validatorsResp) + require.Nil(t, errRes) + + require.Equal(t, 1, len(validatorsResp)) + require.ElementsMatch(t, validators[i].OperatorAddress, validatorsResp[0].OperatorAddress) + + } // Query each validator queryParams := NewQueryValidatorParams(addrVal1) @@ -153,7 +167,7 @@ func TestQueryValidators(t *testing.T) { Path: "/custom/staking/validator", Data: bz, } - res, err = queryValidator(ctx, cdc, query, keeper) + res, err := queryValidator(ctx, cdc, query, keeper) require.Nil(t, err) var validator types.Validator diff --git a/x/staking/types/validator.go b/x/staking/types/validator.go index c048de744c0d..962345aac59f 100644 --- a/x/staking/types/validator.go +++ b/x/staking/types/validator.go @@ -117,7 +117,7 @@ func (v Validator) String() string { Unbonding Completion Time: %v Minimum Self Delegation: %v Commission: %s`, v.OperatorAddress, bechConsPubKey, - v.Jailed, sdk.BondStatusToString(v.Status), v.Tokens, + v.Jailed, v.Status, v.Tokens, v.DelegatorShares, v.Description, v.UnbondingHeight, v.UnbondingCompletionTime, v.MinSelfDelegation, v.Commission) }