Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 88 additions & 8 deletions x/action/v1/keeper/query_list_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
48 changes: 47 additions & 1 deletion x/action/v1/keeper/query_list_actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}