Skip to content

Commit

Permalink
add pagination for stakerinfos query, format (#257)
Browse files Browse the repository at this point in the history
* add pagination for stakerinfos query, format

* test: add ut

* remov max-line-length for protolint

* add limit check, rebase

* regenerate swagger

* test case for offset exceeds limit

* update docker dependency

* downgrade curl

* culr 8.12 for dockerfile
  • Loading branch information
leonz789 authored Feb 17, 2025
1 parent 73f004c commit 4b1fd01
Show file tree
Hide file tree
Showing 11 changed files with 438 additions and 103 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -634,4 +634,4 @@ check-licenses:
@python3 scripts/check_licenses.py .

swagger-ui:
docker run -p 8080:8080 -e SWAGGER_JSON=/app/swagger.json -v $(pwd)/client/docs/swagger-ui:/app swaggerapi/swagger-ui
docker run -p 8080:8080 -e SWAGGER_JSON=/app/swagger.json -v $(pwd)/client/docs/swagger-ui:/app swaggerapi/swagger-ui
70 changes: 70 additions & 0 deletions client/docs/swagger-ui/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -19974,6 +19974,22 @@
"title": "StakerInfo represents all related information for a staker of native-restaking"
},
"title": "all staker infos under the specified asset"
},
"pagination": {
"description": "pagination defines the pagination in the response.",
"type": "object",
"properties": {
"next_key": {
"type": "string",
"format": "byte",
"description": "next_key is the key to be passed to PageRequest.key to\nquery the next page most efficiently. It will be empty if\nthere are no more results."
},
"total": {
"type": "string",
"format": "uint64",
"title": "total is total number of results available if PageRequest.count_total\nwas set, its value is undefined otherwise"
}
}
}
},
"title": "QueryStakerInfosResponse is response type for Query/StakerInfo RCP method"
Expand Down Expand Up @@ -20020,6 +20036,44 @@
"in": "path",
"required": true,
"type": "string"
},
{
"name": "pagination.key",
"description": "key is a value returned in PageResponse.next_key to begin\nquerying the next page most efficiently. Only one of offset or key\nshould be set.",
"in": "query",
"required": false,
"type": "string",
"format": "byte"
},
{
"name": "pagination.offset",
"description": "offset is a numeric offset that can be used when key is unavailable.\nIt is less efficient than using key. Only one of offset or key should\nbe set.",
"in": "query",
"required": false,
"type": "string",
"format": "uint64"
},
{
"name": "pagination.limit",
"description": "limit is the total number of results to be returned in the result page.\nIf left empty it will default to a value to be set by each app.",
"in": "query",
"required": false,
"type": "string",
"format": "uint64"
},
{
"name": "pagination.count_total",
"description": "count_total is set to true to indicate that the result set should include\na count of the total number of items available for pagination in UIs.\ncount_total is only respected when offset is used. It is ignored when key\nis set.",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "pagination.reverse",
"description": "reverse is set to true if results are to be returned in the descending order.\n\nSince: cosmos-sdk 0.43",
"in": "query",
"required": false,
"type": "boolean"
}
],
"tags": [
Expand Down Expand Up @@ -42607,6 +42661,22 @@
"title": "StakerInfo represents all related information for a staker of native-restaking"
},
"title": "all staker infos under the specified asset"
},
"pagination": {
"description": "pagination defines the pagination in the response.",
"type": "object",
"properties": {
"next_key": {
"type": "string",
"format": "byte",
"description": "next_key is the key to be passed to PageRequest.key to\nquery the next page most efficiently. It will be empty if\nthere are no more results."
},
"total": {
"type": "string",
"format": "uint64",
"title": "total is total number of results available if PageRequest.count_total\nwas set, its value is undefined otherwise"
}
}
}
},
"title": "QueryStakerInfosResponse is response type for Query/StakerInfo RCP method"
Expand Down
4 changes: 4 additions & 0 deletions proto/exocore/oracle/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,16 @@ message QueryStakerInfoResponse {
message QueryStakerInfosRequest {
// asset id for the staker info request for
string asset_id = 1;
// pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}

// QueryStakerInfosResponse is response type for Query/StakerInfo RCP method
message QueryStakerInfosResponse {
// all staker infos under the specified asset
repeated StakerInfo staker_infos = 1;
// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

// QueryParamsRequest is request type for the Query/Params RPC method.
Expand Down
10 changes: 9 additions & 1 deletion x/oracle/client/cli/query_native_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,17 @@ func CmdQueryStakerInfos() *cobra.Command {
if _, _, err := assetstypes.ValidateID(assetID, true, false); err != nil {
return err
}
pageReq, err := client.ReadPageRequest(cmd.Flags())
if err != nil {
return err
}
if pageReq.Limit > types.MaxPageLimit {
return types.ErrInvalidPagination.Wrapf("QueryStgakerInfos max page limitation is %d, got %d", types.MaxPageLimit, pageReq.Limit)
}

request := &types.QueryStakerInfosRequest{
AssetId: assetID,
AssetId: assetID,
Pagination: pageReq,
}

res, err := queryClient.StakerInfos(cmd.Context(), request)
Expand Down
33 changes: 21 additions & 12 deletions x/oracle/keeper/native_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import (
"github.com/ExocoreNetwork/exocore/x/oracle/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"github.com/ethereum/go-ethereum/common/hexutil"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// deposit: update staker's totalDeposit
Expand Down Expand Up @@ -70,24 +73,30 @@ func (k Keeper) GetStakerInfo(ctx sdk.Context, assetID, stakerAddr string) types
return stakerInfo
}

// TODO: pagination
// GetStakerInfos returns all stakers information
func (k Keeper) GetStakerInfos(ctx sdk.Context, assetID string) (ret []*types.StakerInfo) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.NativeTokenStakerKeyPrefix(assetID))
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
func (k Keeper) GetStakerInfos(ctx sdk.Context, req *types.QueryStakerInfosRequest) (*types.QueryStakerInfosResponse, error) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.NativeTokenStakerKeyPrefix(req.AssetId))
retStakerInfos := make([]*types.StakerInfo, 0)
if req.Pagination != nil && req.Pagination.Limit > types.MaxPageLimit {
return nil, status.Errorf(codes.InvalidArgument, "pagination limit %d exceeds maximum allowed %d", req.Pagination.Limit, types.MaxPageLimit)
}
resPage, err := query.Paginate(store, req.Pagination, func(_ []byte, value []byte) error {
sInfo := types.StakerInfo{}
k.cdc.MustUnmarshal(iterator.Value(), &sInfo)
k.cdc.MustUnmarshal(value, &sInfo)
// keep only the latest effective-balance
if len(sInfo.BalanceList) > 0 {
sInfo.BalanceList = sInfo.BalanceList[len(sInfo.BalanceList)-1:]
}
// this is mainly used by price feeder, so we remove the stakerAddr to reduce the size of return value
sInfo.StakerAddr = ""
ret = append(ret, &sInfo)
retStakerInfos = append(retStakerInfos, &sInfo)
return nil
})
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "paginate: %v", err)
}
return ret
return &types.QueryStakerInfosResponse{
StakerInfos: retStakerInfos,
Pagination: resPage,
}, nil
}

// GetAllStakerInfosAssets returns all stakerInfos combined with assetIDs they belong to, used for genesisstate exporting
Expand Down Expand Up @@ -242,7 +251,7 @@ func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, staker
if newBalance.Balance <= 0 {
store.Delete(key)
} else {
stakerInfo.BalanceList = append(stakerInfo.BalanceList, &newBalance)
stakerInfo.Append(&newBalance)
bz := k.cdc.MustMarshal(stakerInfo)
store.Set(key, bz)
}
Expand Down
3 changes: 1 addition & 2 deletions x/oracle/keeper/query_native_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ func (k Keeper) StakerInfos(goCtx context.Context, req *types.QueryStakerInfosRe
return nil, ErrUnsupportedAsset
}
ctx := sdk.UnwrapSDKContext(goCtx)
stakerInfos := k.GetStakerInfos(ctx, req.AssetId)
return &types.QueryStakerInfosResponse{StakerInfos: stakerInfos}, nil
return k.GetStakerInfos(ctx, req)
}

func (k Keeper) StakerInfo(goCtx context.Context, req *types.QueryStakerInfoRequest) (*types.QueryStakerInfoResponse, error) {
Expand Down
100 changes: 100 additions & 0 deletions x/oracle/keeper/query_native_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package keeper_test

import (
"fmt"
"strconv"
"testing"

keepertest "github.com/ExocoreNetwork/exocore/testutil/keeper"
"github.com/ExocoreNetwork/exocore/x/oracle/keeper"
"github.com/ExocoreNetwork/exocore/x/oracle/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func TestQueryStakerInfosPaginated(t *testing.T) {
assetID := string(keeper.NSTETHAssetIDMainnet)
keeper, ctx := keepertest.OracleKeeper(t)
wctx := sdk.WrapSDKContext(ctx)
msgs := createNStakerInfos(keeper, ctx, assetID, 5)

request := func(assetID string, next []byte, offset, limit uint64, total bool) *types.QueryStakerInfosRequest {
return &types.QueryStakerInfosRequest{
AssetId: assetID,
Pagination: &query.PageRequest{
Key: next,
Offset: offset,
Limit: limit,
CountTotal: total,
},
}
}
t.Run("ByOffset", func(t *testing.T) {
step := 2
for i := 0; i < len(msgs); i += step {
resp, err := keeper.StakerInfos(wctx, request(assetID, nil, uint64(i), uint64(step), false))
require.NoError(t, err)
require.LessOrEqual(t, len(resp.StakerInfos), step)
require.Subset(t,
msgs,
resp.StakerInfos,
)
}
resp, err := keeper.StakerInfos(wctx, request(assetID, nil, uint64(len(msgs)), 0, false))
require.Empty(t, resp.StakerInfos)
require.Equal(t, uint64(len(msgs)), resp.Pagination.Total)
require.NoError(t, err)
})
t.Run("ByKey", func(t *testing.T) {
step := 2
var next []byte
for i := 0; i < len(msgs); i += step {
resp, err := keeper.StakerInfos(wctx, request(assetID, next, 0, uint64(step), false))
require.NoError(t, err)
require.LessOrEqual(t, len(resp.StakerInfos), step)
require.Subset(t,
msgs,
resp.StakerInfos,
)
next = resp.Pagination.NextKey
}
})
t.Run("Total", func(t *testing.T) {
resp, err := keeper.StakerInfos(wctx, request(assetID, nil, 0, 0, true))
require.NoError(t, err)
require.Equal(t, len(msgs), int(resp.Pagination.Total))
require.ElementsMatch(t,
msgs,
resp.StakerInfos,
)
})
t.Run("InvalidRequest", func(t *testing.T) {
_, err := keeper.StakerInfos(wctx, nil)
require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "invalid request"))
})
}

func createNStakerInfos(keeper *keeper.Keeper, ctx sdk.Context, assetID string, n int) []*types.StakerInfo {
ret := make([]*types.StakerInfo, 0, n)
for i := 0; i < n; i++ {
ret = append(ret, &types.StakerInfo{
StakerAddr: fmt.Sprintf("Staker_%d", i),
StakerIndex: int64(i),
ValidatorPubkeyList: []string{strconv.Itoa(i + 1)},
BalanceList: []*types.BalanceInfo{
{
RoundID: 0,
Block: 0,
Index: 0,
Balance: 32,
Change: types.Action_ACTION_DEPOSIT,
},
},
})
}
keeper.SetStakerInfos(ctx, assetID, ret)
return ret
}
2 changes: 2 additions & 0 deletions x/oracle/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
getPriceFailedRoundNotFound
updateNativeTokenVirtualPriceFail
nstAssetNotSurpported
invalidPageLimit
)

// x/oracle module sentinel errors
Expand All @@ -27,4 +28,5 @@ var (
ErrGetPriceRoundNotFound = sdkerrors.Register(ModuleName, getPriceFailedRoundNotFound, "get price failed for round not found")
ErrUpdateNativeTokenVirtualPriceFail = sdkerrors.Register(ModuleName, updateNativeTokenVirtualPriceFail, "update native token balance change failed")
ErrNSTAssetNotSupported = sdkerrors.Register(ModuleName, nstAssetNotSurpported, "nstAsset not supported")
ErrInvalidPagination = sdkerrors.Register(ModuleName, invalidPageLimit, "params for pagination is invalid")
)
Loading

0 comments on commit 4b1fd01

Please sign in to comment.