From 6b69c2998776e68ca8f96e306bc2f40f5f4432c9 Mon Sep 17 00:00:00 2001 From: Tomer Weller Date: Sat, 20 Dec 2025 15:18:58 -0500 Subject: [PATCH 1/2] Add Order field to getEvents PaginationOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds an optional `order` field to PaginationOptions for the getEvents endpoint, allowing clients to retrieve events in descending order (newest first). Changes: - Add EventOrder type with "asc" and "desc" constants - Add Order field to PaginationOptions struct - Add IsValid() method for order validation - Update GetEventsRequest.Valid() to validate order parameter - Add tests for order validation This enables efficient querying of the N most recent events without scanning the entire retention window. Fully backwards compatible - defaults to ascending order when not specified. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- protocols/rpc/get_events.go | 25 +++++++++++++++++++++++-- protocols/rpc/get_events_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/protocols/rpc/get_events.go b/protocols/rpc/get_events.go index 21fc07459e..3a0a06baaf 100644 --- a/protocols/rpc/get_events.go +++ b/protocols/rpc/get_events.go @@ -179,6 +179,11 @@ func (g *GetEventsRequest) Valid(maxLimit uint) error { return fmt.Errorf("limit must not exceed %d", maxLimit) } + // Validate order + if g.Pagination != nil && !g.Pagination.Order.IsValid() { + return errors.New("order must be 'asc' or 'desc'") + } + // Validate filters if len(g.Filters) > MaxFiltersLimit { return errors.New("maximum 5 filters per request") @@ -394,9 +399,25 @@ func (s SegmentFilter) MarshalJSON() ([]byte, error) { return json.Marshal(scv) } +// EventOrder represents the order in which events are returned +type EventOrder string + +const ( + // EventOrderAsc returns events in ascending order (oldest first) - default + EventOrderAsc EventOrder = "asc" + // EventOrderDesc returns events in descending order (newest first) + EventOrderDesc EventOrder = "desc" +) + +// IsValid checks if the order value is valid +func (o EventOrder) IsValid() bool { + return o == "" || o == EventOrderAsc || o == EventOrderDesc +} + type PaginationOptions struct { - Cursor *Cursor `json:"cursor,omitempty"` - Limit uint `json:"limit,omitempty"` + Cursor *Cursor `json:"cursor,omitempty"` + Limit uint `json:"limit,omitempty"` + Order EventOrder `json:"order,omitempty"` } type GetEventsResponse struct { diff --git a/protocols/rpc/get_events_test.go b/protocols/rpc/get_events_test.go index 754a54124f..fc0e3b14b7 100644 --- a/protocols/rpc/get_events_test.go +++ b/protocols/rpc/get_events_test.go @@ -829,6 +829,31 @@ func TestGetEventsRequestValid(t *testing.T) { Pagination: nil, }).Valid(1000), "filter 1 invalid: topic 1 invalid: "+ "segment 1 invalid: wildcard '**' is only allowed as the last segment") + + // Test order validation + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{}, + Pagination: &PaginationOptions{Order: "invalid"}, + }).Valid(1000), "order must be 'asc' or 'desc'") + + require.NoError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{}, + Pagination: &PaginationOptions{Order: EventOrderAsc}, + }).Valid(1000)) + + require.NoError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{}, + Pagination: &PaginationOptions{Order: EventOrderDesc}, + }).Valid(1000)) + + require.NoError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{}, + Pagination: &PaginationOptions{Order: ""}, + }).Valid(1000)) } func TestEventFilterSerialization(t *testing.T) { From 9853d7bfa25450221a2e8d543417e28899d850cd Mon Sep 17 00:00:00 2001 From: Tomer Weller Date: Sun, 21 Dec 2025 16:13:19 -0500 Subject: [PATCH 2/2] Add documentation for order-dependent startLedger/endLedger semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document how the interpretation of StartLedger and EndLedger changes based on the pagination order parameter: - For ascending order: StartLedger is lower bound, EndLedger is upper bound - For descending order: StartLedger is upper bound, EndLedger is lower bound This ensures consistent semantics where StartLedger always represents where scanning begins and EndLedger where it ends. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- protocols/rpc/get_events.go | 56 +++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/protocols/rpc/get_events.go b/protocols/rpc/get_events.go index 3a0a06baaf..b623a86abc 100644 --- a/protocols/rpc/get_events.go +++ b/protocols/rpc/get_events.go @@ -153,12 +153,42 @@ type EventFilter struct { Topics []TopicFilter `json:"topics,omitempty"` } +// GetEventsRequest is the request parameters for the getEvents RPC method. +// +// The interpretation of StartLedger and EndLedger depends on the pagination order: +// +// - For ascending order (default): StartLedger is the lower bound (where scanning begins) +// and EndLedger is the upper bound. Events are returned from oldest to newest. +// +// - For descending order: StartLedger is the upper bound (where scanning begins, going backwards) +// and EndLedger is the lower bound. Events are returned from newest to oldest. +// +// In both cases, StartLedger represents where the scan starts, and EndLedger represents +// where the scan ends (exclusive). This ensures consistent semantics: you always start +// at StartLedger and scan toward EndLedger. type GetEventsRequest struct { - StartLedger uint32 `json:"startLedger,omitempty"` - EndLedger uint32 `json:"endLedger,omitempty"` - Filters []EventFilter `json:"filters"` - Pagination *PaginationOptions `json:"pagination,omitempty"` - Format string `json:"xdrFormat,omitempty"` + // StartLedger is the ledger where the event scan begins. + // For ascending order: this is the lower bound (oldest ledger to include). + // For descending order: this is the upper bound (newest ledger to include). + // Required when cursor is not provided. + StartLedger uint32 `json:"startLedger,omitempty"` + + // EndLedger is the ledger where the event scan ends (exclusive). + // For ascending order: this is the upper bound (scan stops before this ledger). + // For descending order: this is the lower bound (scan stops before this ledger). + // Optional; if not provided, defaults to a system-defined scan limit. + EndLedger uint32 `json:"endLedger,omitempty"` + + // Filters specify which events to include in the response. + // Events must match at least one filter to be included. + Filters []EventFilter `json:"filters"` + + // Pagination controls result ordering, limits, and cursor-based pagination. + Pagination *PaginationOptions `json:"pagination,omitempty"` + + // Format specifies the encoding format for XDR data in the response. + // Valid values: "base64" (default), "json". + Format string `json:"xdrFormat,omitempty"` } func (g *GetEventsRequest) Valid(maxLimit uint) error { @@ -414,10 +444,20 @@ func (o EventOrder) IsValid() bool { return o == "" || o == EventOrderAsc || o == EventOrderDesc } +// PaginationOptions controls pagination for event queries. type PaginationOptions struct { - Cursor *Cursor `json:"cursor,omitempty"` - Limit uint `json:"limit,omitempty"` - Order EventOrder `json:"order,omitempty"` + // Cursor is the pagination cursor from a previous response. + // When provided, StartLedger and EndLedger must not be set. + Cursor *Cursor `json:"cursor,omitempty"` + + // Limit is the maximum number of events to return (default: 100, max: 10000). + Limit uint `json:"limit,omitempty"` + + // Order specifies the sort order of returned events. + // "asc" (default): oldest first, scanning from StartLedger upward toward EndLedger. + // "desc": newest first, scanning from StartLedger downward toward EndLedger. + // See GetEventsRequest for how order affects StartLedger/EndLedger interpretation. + Order EventOrder `json:"order,omitempty"` } type GetEventsResponse struct {