diff --git a/x/action/v1/keeper/query_list_actions.go b/x/action/v1/keeper/query_list_actions.go index 4e5de75..37dc126 100644 --- a/x/action/v1/keeper/query_list_actions.go +++ b/x/action/v1/keeper/query_list_actions.go @@ -2,6 +2,8 @@ package keeper import ( "context" + "sort" + "strconv" "github.com/LumeraProtocol/lumera/x/action/v1/types" actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" @@ -14,6 +16,64 @@ import ( "google.golang.org/grpc/status" ) +func shouldUseNumericReverseOrdering(pageReq *query.PageRequest) bool { + return pageReq != nil && pageReq.Reverse && len(pageReq.Key) == 0 +} + +func parseNumericActionID(actionID string) (uint64, bool) { + parsed, err := strconv.ParseUint(actionID, 10, 64) + if err != nil { + return 0, false + } + return parsed, true +} + +func sortActionsByNumericID(actions []*types.Action) { + sort.SliceStable(actions, func(i, j int) bool { + leftNumericID, leftIsNumeric := parseNumericActionID(actions[i].ActionID) + rightNumericID, rightIsNumeric := parseNumericActionID(actions[j].ActionID) + + switch { + case leftIsNumeric && rightIsNumeric: + if leftNumericID == rightNumericID { + return actions[i].ActionID < actions[j].ActionID + } + return leftNumericID < rightNumericID + case leftIsNumeric != rightIsNumeric: + return leftIsNumeric + default: + return actions[i].ActionID < actions[j].ActionID + } + }) +} + +func paginateActionSlice(actions []*types.Action, pageReq *query.PageRequest) ([]*types.Action, *query.PageResponse) { + if pageReq == nil { + return actions, &query.PageResponse{} + } + + total := uint64(len(actions)) + offset := pageReq.Offset + if offset > total { + offset = total + } + + limit := pageReq.Limit + if limit == 0 || offset+limit > total { + limit = total - offset + } + + end := offset + limit + page := actions[int(offset):int(end)] + + pageRes := &query.PageResponse{} + if pageReq.CountTotal { + pageRes.Total = total + } + + return page, pageRes +} + // ListActions returns a list of actions, optionally filtered by type and state func (q queryServer) ListActions(goCtx context.Context, req *types.QueryListActionsRequest) (*types.QueryListActionsResponse, error) { if req == nil { @@ -81,19 +141,39 @@ func (q queryServer) ListActions(goCtx context.Context, req *types.QueryListActi } else { actionStore := prefix.NewStore(storeAdapter, []byte(ActionKeyPrefix)) - onResult := func(key, value []byte, accumulate bool) (bool, error) { - var act actiontypes.Action - if err := q.k.cdc.Unmarshal(value, &act); err != nil { - return false, err - } + if shouldUseNumericReverseOrdering(req.Pagination) { + iter := actionStore.Iterator(nil, nil) + defer iter.Close() - if accumulate { + for ; iter.Valid(); iter.Next() { + var act actiontypes.Action + if unmarshalErr := q.k.cdc.Unmarshal(iter.Value(), &act); unmarshalErr != nil { + return nil, status.Errorf(codes.Internal, "failed to unmarshal action: %v", unmarshalErr) + } actions = append(actions, &act) } - return true, nil + sortActionsByNumericID(actions) + for i, j := 0, len(actions)-1; i < j; i, j = i+1, j-1 { + actions[i], actions[j] = actions[j], actions[i] + } + + actions, pageRes = paginateActionSlice(actions, req.Pagination) + } else { + onResult := func(key, value []byte, accumulate bool) (bool, error) { + var act actiontypes.Action + if err := q.k.cdc.Unmarshal(value, &act); err != nil { + return false, err + } + + if accumulate { + actions = append(actions, &act) + } + + return true, nil + } + pageRes, err = query.FilteredPaginate(actionStore, req.Pagination, onResult) } - pageRes, err = query.FilteredPaginate(actionStore, req.Pagination, onResult) } if err != nil { return nil, status.Errorf(codes.Internal, "failed to paginate actions: %v", err) diff --git a/x/action/v1/keeper/query_list_actions_test.go b/x/action/v1/keeper/query_list_actions_test.go index c92f28f..e0bf1fe 100644 --- a/x/action/v1/keeper/query_list_actions_test.go +++ b/x/action/v1/keeper/query_list_actions_test.go @@ -9,8 +9,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" - "go.uber.org/mock/gomock" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -172,3 +172,49 @@ func TestKeeper_ListActions(t *testing.T) { }) } } + +func TestKeeper_ListActions_ReversePaginationUsesNumericActionIDOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + k, ctx := keepertest.ActionKeeper(t, ctrl) + q := keeper.NewQueryServerImpl(k) + price := sdk.NewInt64Coin("stake", 100) + + // Reproduces the mainnet boundary where lexical ordering would place 99999 before 123611. + actionLowLexical := types.Action{ + Creator: "creator-low", + ActionID: "99999", + ActionType: types.ActionTypeCascade, + Metadata: []byte("metadata-low"), + Price: price.String(), + ExpirationTime: 1234567891, + State: types.ActionStateApproved, + BlockHeight: 100, + SuperNodes: []string{"supernode-1"}, + } + actionHighNumeric := types.Action{ + Creator: "creator-high", + ActionID: "123611", + ActionType: types.ActionTypeCascade, + Metadata: []byte("metadata-high"), + Price: price.String(), + ExpirationTime: 1234567892, + State: types.ActionStateApproved, + BlockHeight: 101, + SuperNodes: []string{"supernode-2"}, + } + + require.NoError(t, k.SetAction(ctx, &actionLowLexical)) + require.NoError(t, k.SetAction(ctx, &actionHighNumeric)) + + resp, err := q.ListActions(ctx, &types.QueryListActionsRequest{ + Pagination: &query.PageRequest{ + Limit: 1, + Reverse: true, + }, + }) + require.NoError(t, err) + require.Len(t, resp.Actions, 1) + require.Equal(t, "123611", resp.Actions[0].ActionID) +}