@@ -2,6 +2,10 @@ package keeper
22
33import (
44 "context"
5+ "encoding/binary"
6+ "fmt"
7+ "sort"
8+ "strconv"
59
610 "github.com/LumeraProtocol/lumera/x/action/v1/types"
711 actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
@@ -14,6 +18,161 @@ import (
1418 "google.golang.org/grpc/status"
1519)
1620
21+ // shouldUseNumericReverseOrdering returns true when reverse pagination should use
22+ // numeric action ID ordering instead of lexical store-key ordering.
23+ func shouldUseNumericReverseOrdering (pageReq * query.PageRequest ) bool {
24+ return pageReq != nil && pageReq .Reverse
25+ }
26+
27+ // parseNumericActionID parses action IDs that are strictly base-10 uint64 values.
28+ func parseNumericActionID (actionID string ) (uint64 , bool ) {
29+ parsed , err := strconv .ParseUint (actionID , 10 , 64 )
30+ if err != nil {
31+ return 0 , false
32+ }
33+ return parsed , true
34+ }
35+
36+ // sortActionsByNumericID sorts actions by ActionID using numeric ordering when
37+ // possible, falling back to lexical ordering for non-numeric IDs.
38+ func sortActionsByNumericID (actions []* types.Action ) {
39+ sort .SliceStable (actions , func (i , j int ) bool {
40+ leftNumericID , leftIsNumeric := parseNumericActionID (actions [i ].ActionID )
41+ rightNumericID , rightIsNumeric := parseNumericActionID (actions [j ].ActionID )
42+
43+ switch {
44+ case leftIsNumeric && rightIsNumeric :
45+ if leftNumericID == rightNumericID {
46+ return actions [i ].ActionID < actions [j ].ActionID
47+ }
48+ return leftNumericID < rightNumericID
49+ case leftIsNumeric != rightIsNumeric :
50+ return leftIsNumeric
51+ default :
52+ return actions [i ].ActionID < actions [j ].ActionID
53+ }
54+ })
55+ }
56+
57+ // applyNumericReverseOrderingAndPaginate applies numeric ActionID ordering in
58+ // descending order and then paginates the resulting slice.
59+ func applyNumericReverseOrderingAndPaginate (actions []* types.Action , pageReq * query.PageRequest ) ([]* types.Action , * query.PageResponse , error ) {
60+ sortActionsByNumericID (actions )
61+ for i , j := 0 , len (actions )- 1 ; i < j ; i , j = i + 1 , j - 1 {
62+ actions [i ], actions [j ] = actions [j ], actions [i ]
63+ }
64+
65+ return paginateActionSlice (actions , pageReq )
66+ }
67+
68+ // collectActionsFromIDIndexStore loads actions by ID from an index store whose keys
69+ // are action IDs. Stale index entries are ignored.
70+ func (q queryServer ) collectActionsFromIDIndexStore (
71+ ctx sdk.Context ,
72+ indexStore prefix.Store ,
73+ actionTypeFilter types.ActionType ,
74+ ) ([]* types.Action , error ) {
75+ actions := make ([]* types.Action , 0 )
76+ iter := indexStore .Iterator (nil , nil )
77+ defer iter .Close ()
78+
79+ for ; iter .Valid (); iter .Next () {
80+ actionID := string (iter .Key ())
81+ act , found := q .k .GetActionByID (ctx , actionID )
82+ if ! found {
83+ continue
84+ }
85+ if actionTypeFilter != types .ActionTypeUnspecified && act .ActionType != actiontypes .ActionType (actionTypeFilter ) {
86+ continue
87+ }
88+
89+ actions = append (actions , act )
90+ }
91+
92+ return actions , nil
93+ }
94+
95+ // collectActionsFromPrimaryStore loads all actions from the primary action store.
96+ func (q queryServer ) collectActionsFromPrimaryStore (actionStore prefix.Store ) ([]* types.Action , error ) {
97+ actions := make ([]* types.Action , 0 )
98+ iter := actionStore .Iterator (nil , nil )
99+ defer iter .Close ()
100+
101+ for ; iter .Valid (); iter .Next () {
102+ var act actiontypes.Action
103+ if unmarshalErr := q .k .cdc .Unmarshal (iter .Value (), & act ); unmarshalErr != nil {
104+ return nil , status .Errorf (codes .Internal , "failed to unmarshal action: %v" , unmarshalErr )
105+ }
106+ actions = append (actions , & act )
107+ }
108+
109+ return actions , nil
110+ }
111+
112+ // decodeActionPaginationOffset decodes an opaque pagination key into an offset.
113+ func decodeActionPaginationOffset (key []byte ) (uint64 , error ) {
114+ if len (key ) != 8 {
115+ return 0 , fmt .Errorf ("invalid key length %d" , len (key ))
116+ }
117+ return binary .BigEndian .Uint64 (key ), nil
118+ }
119+
120+ // encodeActionPaginationOffset encodes an offset as an opaque pagination key.
121+ func encodeActionPaginationOffset (offset uint64 ) []byte {
122+ key := make ([]byte , 8 )
123+ binary .BigEndian .PutUint64 (key , offset )
124+ return key
125+ }
126+
127+ // paginateActionSlice paginates an already materialized action slice and returns
128+ // a PageResponse compatible with cursor- and offset-based pagination.
129+ func paginateActionSlice (actions []* types.Action , pageReq * query.PageRequest ) ([]* types.Action , * query.PageResponse , error ) {
130+ if pageReq == nil {
131+ return actions , & query.PageResponse {}, nil
132+ }
133+ if pageReq .Offset > 0 && pageReq .Key != nil {
134+ return nil , nil , status .Error (codes .InvalidArgument , "paginate: invalid request, either offset or key is expected, got both" )
135+ }
136+
137+ total := uint64 (len (actions ))
138+ offset := pageReq .Offset
139+
140+ if len (pageReq .Key ) > 0 {
141+ decodedOffset , err := decodeActionPaginationOffset (pageReq .Key )
142+ if err != nil {
143+ return nil , nil , status .Error (codes .InvalidArgument , "invalid pagination key" )
144+ }
145+ offset = decodedOffset
146+ }
147+
148+ if offset > total {
149+ offset = total
150+ }
151+
152+ limit := pageReq .Limit
153+ if limit == 0 {
154+ limit = query .DefaultLimit
155+ }
156+
157+ remaining := total - offset
158+ if limit > remaining {
159+ limit = remaining
160+ }
161+
162+ end := offset + limit
163+ page := actions [int (offset ):int (end )]
164+
165+ pageRes := & query.PageResponse {}
166+ if pageReq .CountTotal {
167+ pageRes .Total = total
168+ }
169+ if end < total {
170+ pageRes .NextKey = encodeActionPaginationOffset (end )
171+ }
172+
173+ return page , pageRes , nil
174+ }
175+
17176// ListActions returns a list of actions, optionally filtered by type and state
18177func (q queryServer ) ListActions (goCtx context.Context , req * types.QueryListActionsRequest ) (* types.QueryListActionsResponse , error ) {
19178 if req == nil {
@@ -37,65 +196,101 @@ func (q queryServer) ListActions(goCtx context.Context, req *types.QueryListActi
37196 statePrefix := []byte (ActionByStatePrefix + types .ActionState (req .ActionState ).String () + "/" )
38197 indexStore := prefix .NewStore (storeAdapter , statePrefix )
39198
40- onResult := func ( key , _ [] byte , accumulate bool ) ( bool , error ) {
41- actionID := string ( key )
42- act , found := q . k . GetActionByID ( ctx , actionID )
43- if ! found {
44- // Stale index entry; skip without counting
45- return false , nil
199+ if shouldUseNumericReverseOrdering ( req . Pagination ) {
200+ // Numeric reverse ordering cannot be derived from lexical KV iteration, so
201+ // we materialize the matched set, sort it numerically, then paginate.
202+ actions , err = q . collectActionsFromIDIndexStore ( ctx , indexStore , req . ActionType )
203+ if err != nil {
204+ return nil , err
46205 }
47206
48- if req .ActionType != types .ActionTypeUnspecified && act .ActionType != actiontypes .ActionType (req .ActionType ) {
49- return false , nil
50- }
207+ actions , pageRes , err = applyNumericReverseOrderingAndPaginate (actions , req .Pagination )
208+ } else {
209+ onResult := func (key , _ []byte , accumulate bool ) (bool , error ) {
210+ actionID := string (key )
211+ act , found := q .k .GetActionByID (ctx , actionID )
212+ if ! found {
213+ // Stale index entry; skip without counting
214+ return false , nil
215+ }
216+
217+ if req .ActionType != types .ActionTypeUnspecified && act .ActionType != actiontypes .ActionType (req .ActionType ) {
218+ return false , nil
219+ }
220+
221+ if accumulate {
222+ actions = append (actions , act )
223+ }
51224
52- if accumulate {
53- actions = append (actions , act )
225+ return true , nil
54226 }
55227
56- return true , nil
228+ pageRes , err = query . FilteredPaginate ( indexStore , req . Pagination , onResult )
57229 }
58-
59- pageRes , err = query .FilteredPaginate (indexStore , req .Pagination , onResult )
60230 } else if useTypeIndex {
61231 // When filtering only by type, use the type index
62232 typePrefix := []byte (ActionByTypePrefix + types .ActionType (req .ActionType ).String () + "/" )
63233 indexStore := prefix .NewStore (storeAdapter , typePrefix )
64234
65- onResult := func ( key , _ [] byte , accumulate bool ) ( bool , error ) {
66- actionID := string ( key )
67- act , found := q . k . GetActionByID ( ctx , actionID )
68- if ! found {
69- // Stale index entry; skip
70- return false , nil
235+ if shouldUseNumericReverseOrdering ( req . Pagination ) {
236+ // Numeric reverse ordering cannot be derived from lexical KV iteration, so
237+ // we materialize the matched set, sort it numerically, then paginate.
238+ actions , err = q . collectActionsFromIDIndexStore ( ctx , indexStore , types . ActionTypeUnspecified )
239+ if err != nil {
240+ return nil , err
71241 }
72242
73- if accumulate {
74- actions = append (actions , act )
243+ actions , pageRes , err = applyNumericReverseOrderingAndPaginate (actions , req .Pagination )
244+ } else {
245+ onResult := func (key , _ []byte , accumulate bool ) (bool , error ) {
246+ actionID := string (key )
247+ act , found := q .k .GetActionByID (ctx , actionID )
248+ if ! found {
249+ // Stale index entry; skip
250+ return false , nil
251+ }
252+
253+ if accumulate {
254+ actions = append (actions , act )
255+ }
256+
257+ return true , nil
75258 }
76259
77- return true , nil
260+ pageRes , err = query . FilteredPaginate ( indexStore , req . Pagination , onResult )
78261 }
79-
80- pageRes , err = query .FilteredPaginate (indexStore , req .Pagination , onResult )
81262 } else {
82263 actionStore := prefix .NewStore (storeAdapter , []byte (ActionKeyPrefix ))
83264
84- onResult := func (key , value []byte , accumulate bool ) (bool , error ) {
85- var act actiontypes.Action
86- if err := q .k .cdc .Unmarshal (value , & act ); err != nil {
87- return false , err
265+ if shouldUseNumericReverseOrdering (req .Pagination ) {
266+ // Numeric reverse ordering cannot be derived from lexical KV iteration, so
267+ // we materialize the matched set, sort it numerically, then paginate.
268+ actions , err = q .collectActionsFromPrimaryStore (actionStore )
269+ if err != nil {
270+ return nil , err
88271 }
89272
90- if accumulate {
91- actions = append (actions , & act )
92- }
273+ actions , pageRes , err = applyNumericReverseOrderingAndPaginate (actions , req .Pagination )
274+ } else {
275+ onResult := func (key , value []byte , accumulate bool ) (bool , error ) {
276+ var act actiontypes.Action
277+ if err := q .k .cdc .Unmarshal (value , & act ); err != nil {
278+ return false , err
279+ }
280+
281+ if accumulate {
282+ actions = append (actions , & act )
283+ }
93284
94- return true , nil
285+ return true , nil
286+ }
287+ pageRes , err = query .FilteredPaginate (actionStore , req .Pagination , onResult )
95288 }
96- pageRes , err = query .FilteredPaginate (actionStore , req .Pagination , onResult )
97289 }
98290 if err != nil {
291+ if st , ok := status .FromError (err ); ok && st .Code () != codes .Unknown {
292+ return nil , err
293+ }
99294 return nil , status .Errorf (codes .Internal , "failed to paginate actions: %v" , err )
100295 }
101296
0 commit comments