Skip to main content

sui_rpc_api/ledger_history/
query_options.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::ops::{Bound, Range};
5
6use bytes::Bytes;
7use sui_inverted_index::ScanDirection;
8use sui_rpc::proto::sui::rpc::v2::Ordering as ProtoOrdering;
9use sui_rpc::proto::sui::rpc::v2::QueryOptions as ProtoQueryOptions;
10use sui_rpc_cursor::CursorToken;
11use sui_rpc_cursor::Position;
12
13use crate::ErrorReason;
14use crate::RpcError;
15use crate::proto::google::rpc::bad_request::FieldViolation;
16
17const ORDERING_ASCENDING: i32 = ProtoOrdering::Ascending as i32;
18const ORDERING_DESCENDING: i32 = ProtoOrdering::Descending as i32;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
21pub enum Ordering {
22    Ascending,
23    Descending,
24}
25
26/// Intra-transaction coordinate: a transaction and an index within it
27/// (events today; any per-transaction-indexed lane). Boundary cursors may
28/// point at slots with no item.
29#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
30pub struct IntraTxCoordinate {
31    pub tx_seq: u64,
32    pub index: u32,
33}
34
35/// Why a resolved scan interval is exhausted. Fixed at range-resolution time,
36/// carried through scan state, and rendered by [`ScanTerminal`].
37///
38/// [`ScanTerminal`]: crate::ledger_history::watermark::ScanTerminal
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum RangeExhaustion {
41    /// Reached the currently indexed ledger tip.
42    LedgerTip,
43    /// Reached the requested (or implicit-genesis) checkpoint range bound.
44    CheckpointBound,
45    /// Truncated by a client `after`/`before` cursor. `kind` is the cursor kind the terminal
46    /// cursor must carry so resume neither repeats nor skips an item: Item only for an `after`
47    /// Item cursor at the last position, which has no next position to name as a boundary;
48    /// Boundary everywhere else.
49    CursorBound { kind: sui_rpc_cursor::CursorKind },
50    /// A descending scan with no lower bound of its own reached this server's serving floor, the
51    /// lowest checkpoint it still serves; everything below is pruned. The stream sends a watermark
52    /// at the floor claiming `floor_checkpoint`, then fails with `OutOfRange` (see
53    /// [`below_serving_floor`]) instead of ending with a `QueryEnd`.
54    ServingFloor { floor_checkpoint: u64 },
55}
56
57/// Validated, normalized form of `QueryOptions` (the proto wire type).
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct QueryOptions {
60    pub limit_items: usize,
61    pub ordering: Ordering,
62    after: Option<CursorToken>,
63    before: Option<CursorToken>,
64}
65
66/// A request's checkpoint window resolved into the checkpoint-sequence interval to scan. Cursors
67/// do not narrow it: [`ResolvedScan::resolve`] applies them by scan position. Unlike
68/// [`ResolvedScan<u64>`]/[`ResolvedScan<IntraTxCoordinate>`], the scan domain here *is*
69/// checkpoint space, so the entry and terminal checkpoints derive from `range` directly and need
70/// no extra fields.
71#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct ResolvedCheckpointRange {
73    /// Half-open checkpoint-sequence interval to scan (numeric order; a
74    /// descending scan walks it from `end - 1` down to `start`).
75    pub range: Range<u64>,
76    /// Why the interval is exhausted once the scan drains it.
77    pub exhaustion: RangeExhaustion,
78    /// Whether `range.end` is this server's indexed tip rather than a bound the request set, so
79    /// that a position at or past it is one this server has not indexed.
80    pub reaches_ledger_tip: bool,
81}
82
83/// Semantic scan bounds over explicit scan positions (cursor trims need
84/// exclusive bounds on either side).
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub struct ScanBounds<P> {
87    pub lo: Bound<P>,
88    pub hi: Bound<P>,
89}
90
91pub type IntraTxScanBounds = ScanBounds<IntraTxCoordinate>;
92
93/// A request's checkpoint bounds resolved into the scan-position interval to scan, along with the
94/// checkpoint-space facts for watermark rendering.
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct ResolvedScan<P> {
97    /// Scan-position interval to scan, expressed as explicit lo/hi
98    /// [`Bound`]s.
99    pub bounds: ScanBounds<P>,
100    /// The window's first checkpoint in scan direction, raised to the serving floor for an
101    /// ascending scan. No coverage claim passes it. Cursors do not move it: a cursor vouches for
102    /// every position before it in scan order, so a scan claims each checkpoint whose end it
103    /// reaches, wherever its cursor started.
104    pub entry_checkpoint: u64,
105    /// Checkpoint containing the `end_position`.
106    pub end_checkpoint: u64,
107    /// Scan-direction terminal edge of the interval.
108    pub end_position: P,
109    /// Why the interval is exhausted once the scan drains it.
110    pub exhaustion: RangeExhaustion,
111    /// The window's terminal checkpoint in scan direction (see
112    /// [`ResolvedCheckpointRange::terminal_checkpoint`]), or `None` when the window holds no
113    /// checkpoint. Cursors do not move it, unlike `end_checkpoint`, which carries the checkpoint
114    /// field of a cursor that ends the scan: a non-empty window's natural end claims its edge even
115    /// when the scan reads nothing, because the request's starting cursor vouches for every
116    /// position before it.
117    pub window_terminal_checkpoint: Option<u64>,
118}
119
120/// How a lane reads wire cursors: the token's coordinate in the lane's
121/// position space.
122pub trait ScanCursor<P> {
123    fn coordinate(&self) -> P;
124}
125
126/// A scan position with a successor, so scan bounds can tell exactly whether they admit any
127/// position.
128pub trait ScanPosition: Copy + Ord {
129    /// The next position in scan order, if there is one.
130    fn successor(self) -> Option<Self>;
131}
132
133impl ScanPosition for u64 {
134    fn successor(self) -> Option<Self> {
135        self.checked_add(1)
136    }
137}
138
139impl ScanPosition for IntraTxCoordinate {
140    fn successor(self) -> Option<Self> {
141        match self.index.checked_add(1) {
142            Some(index) => Some(Self {
143                tx_seq: self.tx_seq,
144                index,
145            }),
146            None => self.tx_seq.checked_add(1).map(Self::start_of_tx),
147        }
148    }
149}
150
151impl IntraTxCoordinate {
152    /// Fencepost at the first event slot of `tx_seq`; valid as a boundary even
153    /// if the transaction has no events.
154    pub fn start_of_tx(tx_seq: u64) -> Self {
155        Self { tx_seq, index: 0 }
156    }
157
158    /// The event-coordinate window spanning whole transactions `[start, end)`.
159    pub fn tx_window(range: Range<u64>) -> Range<Self> {
160        Self::start_of_tx(range.start)..Self::start_of_tx(range.end)
161    }
162}
163
164/// The error for a request that needs data below this server's serving floor, whose lowest
165/// available checkpoint is `floor_checkpoint`: that data is pruned and permanently gone.
166pub fn below_serving_floor(floor_checkpoint: u64) -> RpcError {
167    RpcError::new(
168        tonic::Code::OutOfRange,
169        format!(
170            "requested data below earliest available; lowest available checkpoint is {floor_checkpoint}"
171        ),
172    )
173}
174
175impl QueryOptions {
176    pub fn checkpoints_from_proto(
177        request: Option<&ProtoQueryOptions>,
178        default_limit_items: u32,
179        max_limit_items: u32,
180    ) -> Result<Self, RpcError> {
181        Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
182            matches!(position, Position::Checkpoints { .. })
183        })
184    }
185
186    pub fn transactions_from_proto(
187        request: Option<&ProtoQueryOptions>,
188        default_limit_items: u32,
189        max_limit_items: u32,
190    ) -> Result<Self, RpcError> {
191        Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
192            matches!(position, Position::Transactions { .. })
193        })
194    }
195
196    pub fn events_from_proto(
197        request: Option<&ProtoQueryOptions>,
198        default_limit_items: u32,
199        max_limit_items: u32,
200    ) -> Result<Self, RpcError> {
201        Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
202            matches!(position, Position::Events { .. })
203        })
204    }
205
206    fn from_proto_with_position(
207        request: Option<&ProtoQueryOptions>,
208        default_limit_items: u32,
209        max_limit_items: u32,
210        position_matches: fn(&Position) -> bool,
211    ) -> Result<Self, RpcError> {
212        let limit_items = request
213            .and_then(|options| options.limit)
214            .unwrap_or(default_limit_items)
215            .clamp(1, max_limit_items) as usize;
216
217        let ordering = match request.and_then(|options| options.ordering) {
218            None | Some(ORDERING_ASCENDING) => Ordering::Ascending,
219            Some(ORDERING_DESCENDING) => Ordering::Descending,
220            Some(_) => {
221                return Err(FieldViolation::new("options.ordering")
222                    .with_description("invalid ordering")
223                    .with_reason(ErrorReason::FieldInvalid)
224                    .into());
225            }
226        };
227
228        let after = parse_cursor(
229            "options.after",
230            request.and_then(|options| options.after.as_ref()),
231            position_matches,
232        )?;
233        let before = parse_cursor(
234            "options.before",
235            request.and_then(|options| options.before.as_ref()),
236            position_matches,
237        )?;
238
239        Ok(Self {
240            limit_items,
241            ordering,
242            after,
243            before,
244        })
245    }
246
247    /// Options for a subscription stream: an unbounded ascending scan with no
248    /// cursor bounds. The ascending direction is consulted by the watermark
249    /// builders; `limit_items` is irrelevant.
250    pub fn subscription() -> Self {
251        Self {
252            limit_items: usize::MAX,
253            ordering: Ordering::Ascending,
254            after: None,
255            before: None,
256        }
257    }
258
259    pub fn scan_direction(&self) -> ScanDirection {
260        match self.ordering {
261            Ordering::Ascending => ScanDirection::Ascending,
262            Ordering::Descending => ScanDirection::Descending,
263        }
264    }
265
266    pub fn is_ascending(&self) -> bool {
267        matches!(self.ordering, Ordering::Ascending)
268    }
269
270    /// Whether the request explicitly positioned the low end of the scan via an
271    /// `after` cursor. `apply_cursor_bounds` only ever raises `range.start` from
272    /// `after` (in both orderings); `before` bounds the high end. Together with an
273    /// explicit `start_checkpoint`, this lets the pruning-floor check distinguish
274    /// "resume/start from here" (error if below the floor — the data is gone) from
275    /// an open-ended low end (clamp up to the floor).
276    pub fn has_after_cursor(&self) -> bool {
277        self.after.is_some()
278    }
279}
280
281impl ResolvedCheckpointRange {
282    pub fn empty_at(
283        checkpoint: u64,
284        exhaustion: RangeExhaustion,
285        reaches_ledger_tip: bool,
286    ) -> Self {
287        Self {
288            range: checkpoint..checkpoint,
289            exhaustion,
290            reaches_ledger_tip,
291        }
292    }
293
294    pub fn is_empty(&self) -> bool {
295        self.range.is_empty()
296    }
297
298    pub fn terminal_checkpoint(&self, ordering: Ordering) -> u64 {
299        match ordering {
300            Ordering::Ascending => self.range.end,
301            Ordering::Descending => self.range.start,
302        }
303    }
304}
305
306impl ResolvedScan<u64> {
307    /// Collapse the symbolic bounds to the half-open store range.
308    pub fn range(&self) -> Range<u64> {
309        self.bounds.to_range()
310    }
311}
312
313impl<P: Copy + Ord> ScanBounds<P> {
314    pub fn from_range(range: Range<P>) -> Self {
315        Self {
316            lo: Bound::Included(range.start),
317            hi: Bound::Excluded(range.end),
318        }
319    }
320
321    pub fn empty_at(position: P) -> Self {
322        Self {
323            lo: Bound::Included(position),
324            hi: Bound::Excluded(position),
325        }
326    }
327
328    pub fn is_empty(&self) -> bool {
329        match (self.lo, self.hi) {
330            (Bound::Included(a), Bound::Excluded(b))
331            | (Bound::Excluded(a), Bound::Excluded(b))
332            | (Bound::Excluded(a), Bound::Included(b)) => a >= b,
333            (Bound::Included(a), Bound::Included(b)) => a > b,
334            (Bound::Unbounded, _) | (_, Bound::Unbounded) => false,
335        }
336    }
337
338    pub fn contains(&self, position: P) -> bool {
339        let above_lo = match self.lo {
340            Bound::Included(lo) => position >= lo,
341            Bound::Excluded(lo) => position > lo,
342            Bound::Unbounded => true,
343        };
344        let below_hi = match self.hi {
345            Bound::Included(hi) => position <= hi,
346            Bound::Excluded(hi) => position < hi,
347            Bound::Unbounded => true,
348        };
349        above_lo && below_hi
350    }
351}
352
353impl<P: ScanPosition> ScanBounds<P> {
354    /// Whether no position lies within the bounds. [`Self::is_empty`] compares the bounds
355    /// symbolically, so it keeps an exclusive low bound right below an exclusive high bound;
356    /// this resolves the low bound to its successor first.
357    pub fn admits_no_position(&self) -> bool {
358        let lo = match self.lo {
359            Bound::Excluded(position) => match position.successor() {
360                Some(successor) => Bound::Included(successor),
361                None => return true,
362            },
363            lo => lo,
364        };
365        ScanBounds { lo, hi: self.hi }.is_empty()
366    }
367}
368
369impl ScanBounds<u64> {
370    pub fn to_range(&self) -> Range<u64> {
371        let start = match self.lo {
372            Bound::Included(position) => position,
373            Bound::Excluded(position) => position.saturating_add(1),
374            Bound::Unbounded => 0,
375        };
376        let end = match self.hi {
377            Bound::Included(position) => position.saturating_add(1),
378            Bound::Excluded(position) => position,
379            Bound::Unbounded => u64::MAX,
380        };
381        start..end
382    }
383}
384
385impl ScanBounds<IntraTxCoordinate> {
386    /// Smallest half-open tx range covering every position these bounds could
387    /// admit. An exclusive `hi` at the start of tx N excludes tx N entirely;
388    /// any other bounded endpoint keeps its transaction, since earlier events
389    /// of that tx may still be in bounds. `None` when no tx can qualify.
390    pub fn tx_range(&self) -> Option<Range<u64>> {
391        let start_tx = match self.lo {
392            Bound::Included(position) | Bound::Excluded(position) => position.tx_seq,
393            Bound::Unbounded => 0,
394        };
395        let end_tx = match self.hi {
396            Bound::Excluded(position) if position.index == 0 => position.tx_seq,
397            Bound::Included(position) | Bound::Excluded(position) => {
398                position.tx_seq.saturating_add(1)
399            }
400            Bound::Unbounded => u64::MAX,
401        };
402        (start_tx < end_tx).then_some(start_tx..end_tx)
403    }
404}
405
406impl<P: ScanPosition> ResolvedScan<P>
407where
408    CursorToken: ScanCursor<P>,
409{
410    /// Record the request's checkpoint bounds, then apply cursor bounds to tighten the `range`
411    /// scanning interval. Cursors bound the scan by position alone; their checkpoint fields only
412    /// label the cursors this scan hands back. If the backend data source has a serving floor,
413    /// `apply_serving_floor` must be called to reconcile the scanning interval.
414    pub fn resolve(
415        cp_range: ResolvedCheckpointRange,
416        range: Range<P>,
417        options: &QueryOptions,
418    ) -> Result<Self, RpcError> {
419        if cp_range.reaches_ledger_tip {
420            reject_cursors_past_tip(options, range.end, cp_range.range.end)?;
421        }
422
423        let entry_checkpoint = if cp_range.is_empty() {
424            // No checkpoint entered, pin entry to terminal boundary
425            cp_range.range.end
426        } else if options.is_ascending() {
427            cp_range.range.start
428        } else {
429            cp_range.range.end - 1
430        };
431
432        let resolved = Self {
433            bounds: ScanBounds::from_range(range.start..range.end),
434            entry_checkpoint,
435            end_checkpoint: cp_range.terminal_checkpoint(options.ordering),
436            end_position: match options.ordering {
437                Ordering::Ascending => range.end,
438                Ordering::Descending => range.start,
439            },
440            exhaustion: cp_range.exhaustion,
441            window_terminal_checkpoint: (!cp_range.is_empty())
442                .then(|| cp_range.terminal_checkpoint(options.ordering)),
443        }
444        .apply_cursor_bounds(options);
445
446        if !cp_range.is_empty()
447            && resolved.bounds.admits_no_position()
448            && let Some((checkpoint, position)) =
449                starting_cursor_end(options, &cp_range.range, &range)
450        {
451            // The starting cursor leaves the window nothing to scan, and no far cursor stops the
452            // scan first: the page ends with the window's reason, where `starting_cursor_end`
453            // places it. A far cursor at or before the window's edge ends the page instead.
454            return Ok(Self {
455                bounds: ScanBounds::empty_at(position),
456                end_checkpoint: checkpoint,
457                end_position: position,
458                exhaustion: cp_range.exhaustion,
459                ..resolved
460            });
461        }
462        Ok(resolved)
463    }
464
465    pub fn is_empty(&self) -> bool {
466        self.bounds.is_empty()
467    }
468
469    /// The checkpoint this scan's natural end reports as fully covered in `Watermark.checkpoint`:
470    /// the window's last checkpoint in scan direction, just below its exclusive upper edge
471    /// ascending, or its inclusive lower edge descending. A scan that drains its window has
472    /// covered every checkpoint up to that edge, and so has a scan its starting cursor leaves
473    /// empty, because the cursor vouches for every position before it. `None` when the window
474    /// has no checkpoint.
475    pub fn natural_end_claim(&self, options: &QueryOptions) -> Option<u64> {
476        let window_terminal_checkpoint = self.window_terminal_checkpoint?;
477        match options.ordering {
478            Ordering::Ascending => window_terminal_checkpoint.checked_sub(1),
479            Ordering::Descending => Some(window_terminal_checkpoint),
480        }
481    }
482
483    fn apply_cursor_bounds(mut self, options: &QueryOptions) -> Self {
484        if self.is_empty() {
485            return self;
486        }
487
488        let mut cursor_terminal = self.apply_after_cursor(options);
489
490        // Either zero, one, or two cursors emptied the bounds. If the bound is empty, before's
491        // position is provably the lower coordinate. Report this minimum of the crossed positions,
492        // otherwise we will skip an interval in the ascending case, or rollback scan progress in
493        // the descending case.
494        if let Some(recording) = self.apply_before_cursor(options) {
495            cursor_terminal = Some(recording);
496        }
497
498        if let Some((checkpoint, position, kind)) = cursor_terminal {
499            self.set_terminal_record(checkpoint, position, RangeExhaustion::CursorBound { kind });
500
501            self.bounds = ScanBounds::empty_at(self.end_position);
502        }
503
504        self
505    }
506
507    fn set_terminal_record(&mut self, checkpoint: u64, position: P, exhaustion: RangeExhaustion) {
508        self.end_checkpoint = checkpoint;
509        self.end_position = position;
510        self.exhaustion = exhaustion;
511    }
512
513    /// Tightens the low bound, and returns the terminal record if the cursor empties the interval.
514    fn apply_after_cursor(
515        &mut self,
516        options: &QueryOptions,
517    ) -> Option<(u64, P, sui_rpc_cursor::CursorKind)> {
518        let cursor = options.after.as_ref()?;
519        let checkpoint = cursor.position.checkpoint();
520        let position: P = cursor.coordinate();
521
522        // Symbolic resume in every lane: an Item admits strictly-after, a
523        // Boundary admits from itself.
524        let candidate = match cursor.kind {
525            sui_rpc_cursor::CursorKind::Item => Bound::Excluded(position),
526            sui_rpc_cursor::CursorKind::Boundary => Bound::Included(position),
527        };
528
529        if !lower_bound_gte(candidate, self.bounds.lo) {
530            return None;
531        }
532
533        // The after cursor is the terminal bound for a descending scan. Its closing cursor names
534        // the boundary just below the lowest position the scan admits, so that sent back as
535        // `before` it resumes with the next position down: `after: Boundary(X)` admits X, so the
536        // boundary is at X; `after: Item(X)` excludes X, so the boundary is at the position after
537        // X. That boundary keeps the item's checkpoint field, the checkpoint of the position
538        // before it, as an ascending scan frontier's does. With no position after X, the
539        // closing cursor restates the item.
540        let (descending_terminal, descending_terminal_kind) = match cursor.kind {
541            sui_rpc_cursor::CursorKind::Boundary => {
542                (position, sui_rpc_cursor::CursorKind::Boundary)
543            }
544            sui_rpc_cursor::CursorKind::Item => match position.successor() {
545                Some(next) => (next, sui_rpc_cursor::CursorKind::Boundary),
546                None => (position, sui_rpc_cursor::CursorKind::Item),
547            },
548        };
549        if !options.is_ascending() {
550            self.set_terminal_record(
551                checkpoint,
552                descending_terminal,
553                RangeExhaustion::CursorBound {
554                    kind: descending_terminal_kind,
555                },
556            );
557        }
558
559        self.bounds.lo = candidate;
560
561        if !self.bounds.is_empty() {
562            return None;
563        }
564
565        // Report the cursor that emptied the bound. An ascending `after` cursor reports its own
566        // position and kind; `ResolvedScan::resolve` restates it as the boundary at the first
567        // position it admits. An Item at the last position admits none, so it keeps Item kind.
568        if options.is_ascending() {
569            Some((checkpoint, position, cursor.kind))
570        } else {
571            Some((checkpoint, descending_terminal, descending_terminal_kind))
572        }
573    }
574
575    /// Tightens the high bound, and returns the terminal record if the cursor empties the interval.
576    fn apply_before_cursor(
577        &mut self,
578        options: &QueryOptions,
579    ) -> Option<(u64, P, sui_rpc_cursor::CursorKind)> {
580        let cursor = options.before.as_ref()?;
581        let checkpoint = cursor.position.checkpoint();
582        let position: P = cursor.coordinate();
583
584        if !hi_admits_upper_bound(self.bounds.hi, position) {
585            return None;
586        }
587
588        if options.is_ascending() {
589            self.set_terminal_record(
590                checkpoint,
591                position,
592                RangeExhaustion::CursorBound {
593                    kind: sui_rpc_cursor::CursorKind::Boundary,
594                },
595            );
596        }
597
598        self.bounds.hi = Bound::Excluded(position);
599
600        if !self.bounds.is_empty() {
601            return None;
602        }
603
604        Some((checkpoint, position, sui_rpc_cursor::CursorKind::Boundary))
605    }
606
607    /// Raises the scan's low end to the serving floor, for a scan with no lower bound of its own
608    /// whose low end lies below the floor. An ascending scan starts at the floor and claims
609    /// coverage from `floor_checkpoint` on. A descending scan ends at the floor with
610    /// [`RangeExhaustion::ServingFloor`]: everything below it is pruned.
611    ///
612    /// A floor that leaves the scan nothing to cover (its `before` cursor sits at or below the
613    /// floor) is `OutOfRange`.
614    pub fn apply_serving_floor(
615        &mut self,
616        floor: P,
617        floor_checkpoint: u64,
618        options: &QueryOptions,
619    ) -> Result<(), RpcError> {
620        let floored_lo = Bound::Included(floor);
621        let floored = ScanBounds {
622            lo: floored_lo,
623            hi: self.bounds.hi,
624        };
625        if floored.is_empty() {
626            return Err(below_serving_floor(floor_checkpoint));
627        }
628        self.bounds.lo = floored_lo;
629        if options.is_ascending() {
630            self.entry_checkpoint = self.entry_checkpoint.max(floor_checkpoint);
631        } else {
632            self.set_terminal_record(
633                floor_checkpoint,
634                floor,
635                RangeExhaustion::ServingFloor { floor_checkpoint },
636            );
637        }
638        Ok(())
639    }
640}
641
642/// The requested checkpoint window's validity check, standalone so backends
643/// can sequence it independently of options parsing (kv validates bounds
644/// before the read mask and options; the fused resolution re-runs it
645/// harmlessly).
646pub fn validate_checkpoint_bounds(
647    start_checkpoint: Option<u64>,
648    end_checkpoint: Option<u64>,
649) -> Result<(), RpcError> {
650    let start = start_checkpoint.unwrap_or(0);
651    if let Some(end) = end_checkpoint
652        && end < start
653    {
654        return Err(FieldViolation::new("end_checkpoint")
655            .with_description("end_checkpoint must be greater than or equal to start_checkpoint")
656            .with_reason(ErrorReason::FieldInvalid)
657            .into());
658    }
659    Ok(())
660}
661
662impl ResolvedCheckpointRange {
663    /// Validate the requested checkpoint window, clamp it to the indexed tip, and attribute the
664    /// exhaustion reason the scan will report once it drains it.
665    ///
666    /// A descending window that reaches past the tip returns `OutOfRange`: an `end_checkpoint`
667    /// above the tip, or, with `end_checkpoint` unset, a `start_checkpoint` at or above it. A
668    /// descending scan starts at its window's high end, so answering from this view would skip
669    /// the checkpoints between the tip and that end; like a cursor past the tip, the request
670    /// becomes servable once this server catches up. An ascending window past the tip ends at the
671    /// tip, where the client resumes once this server has caught up, so only an ascending scan
672    /// ends with `LedgerTip`.
673    ///
674    /// The request's cursors do not narrow the window here. A cursor's checkpoint field is a
675    /// label whose meaning depends on the scan that issued it, so [`ResolvedScan::resolve`]
676    /// bounds the scan by the cursors' positions and reads the fields only to label checkpoints.
677    pub fn from_request(
678        start_checkpoint: Option<u64>,
679        end_checkpoint: Option<u64>,
680        checkpoint_hi_exclusive: u64,
681        options: &QueryOptions,
682    ) -> Result<Self, RpcError> {
683        validate_checkpoint_bounds(start_checkpoint, end_checkpoint)?;
684        let start = start_checkpoint.unwrap_or(0);
685        let descending_past_tip = match end_checkpoint {
686            _ if options.is_ascending() => None,
687            Some(end) if end > checkpoint_hi_exclusive => Some(("end_checkpoint", end)),
688            None if start >= checkpoint_hi_exclusive => Some(("start_checkpoint", start)),
689            Some(_) | None => None,
690        };
691        if let Some((field, requested)) = descending_past_tip {
692            return Err(RpcError::new(
693                tonic::Code::OutOfRange,
694                format!(
695                    "requested data above latest available; {field} is {requested}, and only \
696                     checkpoints below {checkpoint_hi_exclusive} are available"
697                ),
698            ));
699        }
700        let requested_end = end_checkpoint.unwrap_or(checkpoint_hi_exclusive);
701        // Whether the request leaves its window's high end to this server's tip.
702        let window_reaches_tip =
703            end_checkpoint.is_none() || requested_end > checkpoint_hi_exclusive;
704        let end = requested_end.min(checkpoint_hi_exclusive);
705        let exhaustion = match options.ordering {
706            Ordering::Ascending if window_reaches_tip => RangeExhaustion::LedgerTip,
707            Ordering::Ascending | Ordering::Descending => RangeExhaustion::CheckpointBound,
708        };
709
710        if options.is_ascending() && start >= checkpoint_hi_exclusive {
711            return Ok(Self::empty_at(
712                checkpoint_hi_exclusive,
713                RangeExhaustion::LedgerTip,
714                window_reaches_tip,
715            ));
716        }
717        if start >= end {
718            let checkpoint = match options.ordering {
719                Ordering::Ascending => end,
720                Ordering::Descending => start,
721            };
722            return Ok(Self::empty_at(
723                checkpoint,
724                exhaustion,
725                window_reaches_tip && checkpoint == checkpoint_hi_exclusive,
726            ));
727        }
728        Ok(Self {
729            range: start..end,
730            exhaustion,
731            reaches_ledger_tip: window_reaches_tip && end == checkpoint_hi_exclusive,
732        })
733    }
734}
735
736/// Where a page ends when its starting cursor sits at or past the window's edge in scan
737/// direction, `window.end` ascending or `window.start` descending, and no far cursor stops the
738/// scan at or before that edge, as `(checkpoint field, boundary position)`. An `after` cursor
739/// starts an ascending scan at the first position it admits; a `before` cursor starts a
740/// descending scan below its position. A far cursor stops the scan as `apply_cursor_bounds`
741/// applies it: a `before` at or below the window's end, an `after` at or above the window's
742/// start.
743///
744/// - At the edge, the page ends at the window's edge, as the page that handed the cursor back
745///   did.
746/// - Past the edge, the page ends at the boundary where the starting cursor resumes, under the
747///   cursor's own checkpoint field: the boundary after an `after` Item cursor's item, or a
748///   `before` cursor's position. Ending at the window's edge instead would hand back a cursor
749///   behind the client's, which replays the positions in between when resumed without the
750///   window.
751///
752/// `None` otherwise, including for an `after` Item cursor at the last position, which names no
753/// boundary after it.
754fn starting_cursor_end<P: ScanPosition>(
755    options: &QueryOptions,
756    checkpoints: &Range<u64>,
757    window: &Range<P>,
758) -> Option<(u64, P)>
759where
760    CursorToken: ScanCursor<P>,
761{
762    let position = |cursor: &CursorToken| -> P { cursor.coordinate() };
763    match options.ordering {
764        Ordering::Ascending => {
765            if options
766                .before
767                .as_ref()
768                .is_some_and(|before| position(before) <= window.end)
769            {
770                return None;
771            }
772            let after = options.after.as_ref()?;
773            let first_admitted = match after.kind {
774                sui_rpc_cursor::CursorKind::Item => position(after).successor()?,
775                sui_rpc_cursor::CursorKind::Boundary => position(after),
776            };
777            match first_admitted.cmp(&window.end) {
778                std::cmp::Ordering::Less => None,
779                std::cmp::Ordering::Equal => Some((checkpoints.end, window.end)),
780                std::cmp::Ordering::Greater => Some((after.position.checkpoint(), first_admitted)),
781            }
782        }
783        Ordering::Descending => {
784            if options
785                .after
786                .as_ref()
787                .is_some_and(|after| position(after) >= window.start)
788            {
789                return None;
790            }
791            let before = options.before.as_ref()?;
792            let upper = position(before);
793            match upper.cmp(&window.start) {
794                std::cmp::Ordering::Greater => None,
795                std::cmp::Ordering::Equal => Some((checkpoints.start, window.start)),
796                std::cmp::Ordering::Less => Some((before.position.checkpoint(), upper)),
797            }
798        }
799    }
800}
801
802/// Rejects a request whose cursor names a position at or past `tip`, the first position this
803/// server has not indexed. Such a cursor comes from a newer view of the ledger than this server's,
804/// from another replica or derived from a newer tip. Answering from this view would resume an
805/// ascending scan behind its `after` cursor, replaying items the client already has, start a
806/// descending scan below its `before` cursor, skipping items the client has not seen, or end a
807/// descending scan bounded by its `after` cursor empty, though items lie above that cursor.
808/// `OutOfRange` marks the request as reading past the data this server has, the mirror of a
809/// cursor below the pruned floor; unlike that one, it becomes servable once this server catches
810/// up, so clients may retry it. An ascending scan's `before` cursor may lie past the tip: the
811/// scan stops at the tip, and resumes from there once this server has caught up.
812fn reject_cursors_past_tip<P: Ord>(
813    options: &QueryOptions,
814    tip: P,
815    checkpoint_hi_exclusive: u64,
816) -> Result<(), RpcError>
817where
818    CursorToken: ScanCursor<P>,
819{
820    let cursors = match options.ordering {
821        Ordering::Ascending => [options.after.as_ref(), None],
822        Ordering::Descending => [options.before.as_ref(), options.after.as_ref()],
823    };
824    for cursor in cursors.into_iter().flatten() {
825        let position = cursor.coordinate();
826        // A boundary exactly at the tip is where a caught-up scan resumes; an item there, or any
827        // position beyond it, has not been indexed here.
828        let past_tip = match cursor.kind {
829            sui_rpc_cursor::CursorKind::Item => position >= tip,
830            sui_rpc_cursor::CursorKind::Boundary => position > tip,
831        };
832        if past_tip {
833            return Err(RpcError::new(
834                tonic::Code::OutOfRange,
835                format!(
836                    "requested data above latest available; cursor is at checkpoint {}, and only \
837                     checkpoints below {checkpoint_hi_exclusive} are available",
838                    cursor.position.checkpoint(),
839                ),
840            ));
841        }
842    }
843    Ok(())
844}
845
846impl ScanCursor<u64> for CursorToken {
847    fn coordinate(&self) -> u64 {
848        match self.position {
849            Position::Checkpoints { checkpoint } => checkpoint,
850            Position::Transactions { tx_seq, .. } => tx_seq,
851            Position::Events { .. } => unreachable!("validated at decode"),
852        }
853    }
854}
855
856impl ScanCursor<IntraTxCoordinate> for CursorToken {
857    fn coordinate(&self) -> IntraTxCoordinate {
858        match self.position {
859            Position::Events {
860                tx_seq,
861                event_index,
862                ..
863            } => IntraTxCoordinate {
864                tx_seq,
865                index: event_index,
866            },
867            _ => unreachable!("validated at decode"),
868        }
869    }
870}
871
872impl From<IntraTxCoordinate> for (u64, u32) {
873    fn from(position: IntraTxCoordinate) -> Self {
874        (position.tx_seq, position.index)
875    }
876}
877
878impl From<(u64, u32)> for IntraTxCoordinate {
879    fn from((tx_seq, index): (u64, u32)) -> Self {
880        Self { tx_seq, index }
881    }
882}
883
884fn lower_bound_gte<P: Ord + Copy>(candidate: Bound<P>, current: Bound<P>) -> bool {
885    let Some(candidate) = lower_bound_key(candidate) else {
886        return false;
887    };
888    match lower_bound_key(current) {
889        Some(current) => candidate >= current,
890        None => true,
891    }
892}
893
894fn lower_bound_key<P: Ord + Copy>(bound: Bound<P>) -> Option<(P, u8)> {
895    match bound {
896        Bound::Included(position) => Some((position, 0)),
897        Bound::Excluded(position) => Some((position, 1)),
898        Bound::Unbounded => None,
899    }
900}
901
902fn hi_admits_upper_bound<P: Ord + Copy>(current: Bound<P>, candidate: P) -> bool {
903    match current {
904        Bound::Included(position) | Bound::Excluded(position) => candidate <= position,
905        Bound::Unbounded => true,
906    }
907}
908
909fn parse_cursor(
910    field: &'static str,
911    cursor: Option<&Bytes>,
912    position_matches: fn(&Position) -> bool,
913) -> Result<Option<CursorToken>, RpcError> {
914    cursor
915        .map(|cursor| {
916            CursorToken::decode(cursor).map_err(|_| invalid_cursor(field, "invalid cursor"))
917        })
918        .transpose()?
919        .map(|token| {
920            if position_matches(&token.position) {
921                Ok(token)
922            } else {
923                Err(invalid_cursor(field, "invalid cursor"))
924            }
925        })
926        .transpose()
927}
928
929fn invalid_cursor(field: &'static str, description: impl Into<String>) -> RpcError {
930    FieldViolation::new(field)
931        .with_description(description)
932        .with_reason(ErrorReason::FieldInvalid)
933        .into()
934}
935
936#[cfg(test)]
937mod tests {
938    use super::*;
939
940    fn query_options_from_proto(
941        request: Option<&ProtoQueryOptions>,
942    ) -> Result<QueryOptions, RpcError> {
943        QueryOptions::transactions_from_proto(request, 100, 1_000)
944    }
945
946    fn resolved_range(range: Range<u64>) -> ResolvedScan<u64> {
947        ResolvedScan {
948            bounds: ScanBounds::from_range(range),
949            end_checkpoint: 20,
950            end_position: 20,
951            exhaustion: RangeExhaustion::CheckpointBound,
952            entry_checkpoint: 0,
953            window_terminal_checkpoint: Some(20),
954        }
955    }
956
957    /// The empty resolution `resolved_range` reaches when a cursor empties it.
958    fn empty_resolved_range(
959        end_checkpoint: u64,
960        end_position: u64,
961        exhaustion: RangeExhaustion,
962    ) -> ResolvedScan<u64> {
963        ResolvedScan {
964            bounds: ScanBounds::empty_at(end_position),
965            end_checkpoint,
966            end_position,
967            exhaustion,
968            entry_checkpoint: 0,
969            window_terminal_checkpoint: Some(20),
970        }
971    }
972
973    fn tx_item(checkpoint: u64, tx_seq: u64) -> CursorToken {
974        CursorToken::item(Position::Transactions { checkpoint, tx_seq })
975    }
976
977    fn tx_boundary(checkpoint: u64, tx_seq: u64) -> CursorToken {
978        CursorToken::boundary(Position::Transactions { checkpoint, tx_seq })
979    }
980
981    fn cp_item(checkpoint: u64) -> CursorToken {
982        CursorToken::item(Position::Checkpoints { checkpoint })
983    }
984
985    fn ev_item(checkpoint: u64, tx_seq: u64, index: u32) -> CursorToken {
986        CursorToken::item(Position::Events {
987            checkpoint,
988            tx_seq,
989            event_index: index,
990        })
991    }
992
993    fn ev_boundary(checkpoint: u64, tx_seq: u64, index: u32) -> CursorToken {
994        CursorToken::boundary(Position::Events {
995            checkpoint,
996            tx_seq,
997            event_index: index,
998        })
999    }
1000
1001    fn resolved_intra_tx() -> ResolvedScan<IntraTxCoordinate> {
1002        ResolvedScan {
1003            bounds: IntraTxScanBounds::from_range(
1004                IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(10),
1005            ),
1006            entry_checkpoint: 2,
1007            end_checkpoint: 9,
1008            end_position: IntraTxCoordinate::start_of_tx(10),
1009            exhaustion: RangeExhaustion::CheckpointBound,
1010            window_terminal_checkpoint: Some(9),
1011        }
1012    }
1013
1014    fn after_options(ordering: Ordering, cursor: CursorToken) -> QueryOptions {
1015        QueryOptions {
1016            limit_items: 100,
1017            ordering,
1018            after: Some(cursor),
1019            before: None,
1020        }
1021    }
1022
1023    fn before_options(ordering: Ordering, cursor: CursorToken) -> QueryOptions {
1024        QueryOptions {
1025            limit_items: 100,
1026            ordering,
1027            after: None,
1028            before: Some(cursor),
1029        }
1030    }
1031
1032    fn directional_options(ascending: bool) -> QueryOptions {
1033        let mut request = ProtoQueryOptions::default();
1034        if !ascending {
1035            request.ordering = Some(ProtoOrdering::Descending as i32);
1036        }
1037        query_options_from_proto(Some(&request)).unwrap()
1038    }
1039
1040    /// Asserts `result` is the `OutOfRange` error for data below a serving floor at
1041    /// `floor_checkpoint`.
1042    fn assert_below_serving_floor(result: Result<(), RpcError>, floor_checkpoint: u64) {
1043        let status = result
1044            .expect_err("expected OutOfRange below the serving floor")
1045            .into_status_proto();
1046        assert_eq!(status.code, tonic::Code::OutOfRange as i32, "{status:?}");
1047        assert_eq!(
1048            status.message,
1049            format!(
1050                "requested data below earliest available; lowest available checkpoint is {floor_checkpoint}"
1051            )
1052        );
1053    }
1054
1055    /// Serving-floor reconciliation for tx intervals: a floor inside the interval starts an
1056    /// ascending scan there and raises its entry claim, and ends a descending scan there with
1057    /// [`RangeExhaustion::ServingFloor`]. A floor at/past the high end leaves nothing to cover and
1058    /// is `OutOfRange` in both directions.
1059    #[test]
1060    fn tx_serving_floor_reconciles_or_rejects_consumed_scan() {
1061        let asc = directional_options(true);
1062        let desc = directional_options(false);
1063
1064        // Ascending, floor inside 0..100: scan starts at the floor; the
1065        // entry claim rises to the floor checkpoint; terminal untouched.
1066        let mut resolved = ResolvedScan {
1067            bounds: ScanBounds::from_range(0..100),
1068            end_checkpoint: 20,
1069            end_position: 100,
1070            exhaustion: RangeExhaustion::CheckpointBound,
1071            entry_checkpoint: 0,
1072            window_terminal_checkpoint: Some(20),
1073        };
1074        resolved.apply_serving_floor(50, 10, &asc).unwrap();
1075        assert_eq!(resolved.natural_end_claim(&asc), Some(19));
1076        assert_eq!(resolved.range(), 50..100);
1077        assert_eq!(resolved.entry_checkpoint, 10);
1078        assert_eq!(resolved.end_checkpoint, 20);
1079        assert_eq!(resolved.end_position, 100);
1080        assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
1081
1082        // Descending, floor inside: entry (the high edge) untouched; the
1083        // scan ends at the floor, which it reaches as its serving floor.
1084        let mut resolved = ResolvedScan {
1085            bounds: ScanBounds::from_range(0..100),
1086            end_checkpoint: 0,
1087            end_position: 0,
1088            exhaustion: RangeExhaustion::CheckpointBound,
1089            entry_checkpoint: 20,
1090            window_terminal_checkpoint: Some(0),
1091        };
1092        resolved.apply_serving_floor(50, 10, &desc).unwrap();
1093        assert_eq!(resolved.range(), 50..100);
1094        assert_eq!(resolved.entry_checkpoint, 20);
1095        assert_eq!(resolved.end_checkpoint, 10);
1096        assert_eq!(resolved.end_position, 50);
1097        assert_eq!(
1098            resolved.exhaustion,
1099            RangeExhaustion::ServingFloor {
1100                floor_checkpoint: 10
1101            }
1102        );
1103
1104        // Floor at/past the high end (covers the == boundary), both
1105        // directions: nothing left to cover is `OutOfRange`.
1106        for floor_tx in [40, 50] {
1107            let mut resolved = ResolvedScan {
1108                bounds: ScanBounds::from_range(0..40),
1109                end_checkpoint: 8,
1110                end_position: 40,
1111                exhaustion: RangeExhaustion::CheckpointBound,
1112                entry_checkpoint: 0,
1113                window_terminal_checkpoint: Some(8),
1114            };
1115            assert_below_serving_floor(resolved.apply_serving_floor(floor_tx, 10, &asc), 10);
1116
1117            let mut resolved = ResolvedScan {
1118                bounds: ScanBounds::from_range(0..40),
1119                end_checkpoint: 0,
1120                end_position: 0,
1121                exhaustion: RangeExhaustion::CheckpointBound,
1122                entry_checkpoint: 8,
1123                window_terminal_checkpoint: Some(0),
1124            };
1125            assert_below_serving_floor(resolved.apply_serving_floor(floor_tx, 10, &desc), 10);
1126        }
1127    }
1128
1129    /// [`ResolvedScan::<IntraTxCoordinate>::apply_serving_floor`] mirrors the tx behavior in
1130    /// event coordinates.
1131    #[test]
1132    fn event_serving_floor_reconciles_or_rejects_consumed_scan() {
1133        let asc = directional_options(true);
1134        let desc = directional_options(false);
1135
1136        // Ascending, floor inside: low bound moves, entry rises, terminal
1137        // untouched.
1138        let mut resolved = ResolvedScan {
1139            bounds: IntraTxScanBounds::from_range(
1140                IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(100),
1141            ),
1142            end_checkpoint: 20,
1143            end_position: IntraTxCoordinate::start_of_tx(100),
1144            exhaustion: RangeExhaustion::CheckpointBound,
1145            entry_checkpoint: 0,
1146            window_terminal_checkpoint: Some(20),
1147        };
1148        resolved
1149            .apply_serving_floor(IntraTxCoordinate::start_of_tx(50), 10, &asc)
1150            .unwrap();
1151        assert_eq!(resolved.natural_end_claim(&asc), Some(19));
1152        assert_eq!(
1153            resolved.bounds.lo,
1154            Bound::Included(IntraTxCoordinate::start_of_tx(50))
1155        );
1156        assert_eq!(resolved.entry_checkpoint, 10);
1157        assert_eq!(resolved.end_checkpoint, 20);
1158        assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(100));
1159
1160        // Descending, floor inside: the scan ends at the floor, which it reaches as its serving
1161        // floor.
1162        let mut resolved = ResolvedScan {
1163            bounds: IntraTxScanBounds::from_range(
1164                IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(100),
1165            ),
1166            end_checkpoint: 0,
1167            end_position: IntraTxCoordinate::start_of_tx(0),
1168            exhaustion: RangeExhaustion::CheckpointBound,
1169            entry_checkpoint: 20,
1170            window_terminal_checkpoint: Some(0),
1171        };
1172        resolved
1173            .apply_serving_floor(IntraTxCoordinate::start_of_tx(50), 10, &desc)
1174            .unwrap();
1175        assert_eq!(
1176            resolved.bounds.lo,
1177            Bound::Included(IntraTxCoordinate::start_of_tx(50))
1178        );
1179        assert_eq!(resolved.entry_checkpoint, 20);
1180        assert_eq!(resolved.end_checkpoint, 10);
1181        assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(50));
1182        assert_eq!(
1183            resolved.exhaustion,
1184            RangeExhaustion::ServingFloor {
1185                floor_checkpoint: 10
1186            }
1187        );
1188
1189        // Floor that empties the bounds (covers the == boundary), both
1190        // directions: nothing left to cover is `OutOfRange`.
1191        for floor_tx in [40, 50] {
1192            let mut resolved = ResolvedScan {
1193                bounds: IntraTxScanBounds::from_range(
1194                    IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(40),
1195                ),
1196                end_checkpoint: 8,
1197                end_position: IntraTxCoordinate::start_of_tx(40),
1198                exhaustion: RangeExhaustion::CheckpointBound,
1199                entry_checkpoint: 0,
1200                window_terminal_checkpoint: Some(8),
1201            };
1202            assert_below_serving_floor(
1203                resolved.apply_serving_floor(IntraTxCoordinate::start_of_tx(floor_tx), 10, &asc),
1204                10,
1205            );
1206
1207            let mut resolved = ResolvedScan {
1208                bounds: IntraTxScanBounds::from_range(
1209                    IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(40),
1210                ),
1211                end_checkpoint: 0,
1212                end_position: IntraTxCoordinate::start_of_tx(0),
1213                exhaustion: RangeExhaustion::CheckpointBound,
1214                entry_checkpoint: 8,
1215                window_terminal_checkpoint: Some(0),
1216            };
1217            assert_below_serving_floor(
1218                resolved.apply_serving_floor(IntraTxCoordinate::start_of_tx(floor_tx), 10, &desc),
1219                10,
1220            );
1221        }
1222    }
1223
1224    /// A natural end claims the window's edge in scan direction, whatever checkpoint field its
1225    /// terminal carries: the checkpoint below the window's exclusive upper edge ascending, and its
1226    /// inclusive lower edge descending. An ascending window ending at genesis, or an empty window,
1227    /// claims nothing.
1228    #[test]
1229    fn natural_end_claims_the_window_edge() {
1230        let asc = directional_options(true);
1231        let desc = directional_options(false);
1232        let resolved = ResolvedScan {
1233            // A cursor that ends the scan leaves its checkpoint field in `end_checkpoint`; the
1234            // claim reads the window's edge alone.
1235            end_checkpoint: 3,
1236            window_terminal_checkpoint: Some(10),
1237            ..resolved_range(0..100)
1238        };
1239        assert_eq!(resolved.natural_end_claim(&asc), Some(9));
1240        assert_eq!(resolved.natural_end_claim(&desc), Some(10));
1241
1242        let at_genesis = ResolvedScan {
1243            window_terminal_checkpoint: Some(0),
1244            ..resolved_range(0..100)
1245        };
1246        assert_eq!(at_genesis.natural_end_claim(&asc), None);
1247        assert_eq!(at_genesis.natural_end_claim(&desc), Some(0));
1248
1249        let empty_window = ResolvedScan {
1250            window_terminal_checkpoint: None,
1251            ..resolved
1252        };
1253        assert_eq!(empty_window.natural_end_claim(&asc), None);
1254        assert_eq!(empty_window.natural_end_claim(&desc), None);
1255    }
1256
1257    /// A descending walk with no lower bound of its own ends at the serving floor with
1258    /// [`RangeExhaustion::ServingFloor`], claiming the floor checkpoint. Resuming it with a
1259    /// `before` cursor at the floor or below it, in either ordering, leaves nothing above the
1260    /// floor to cover and is `OutOfRange`.
1261    #[test]
1262    fn descending_walk_ends_at_serving_floor_and_resuming_below_it_is_out_of_range() {
1263        let floor_checkpoint = 10;
1264        let floor = first_tx(floor_checkpoint);
1265
1266        let walk = directional_options(false);
1267        let mut reached_floor = resolve_tx_request((None, None), &walk).unwrap();
1268        reached_floor
1269            .apply_serving_floor(floor, floor_checkpoint, &walk)
1270            .unwrap();
1271        assert_eq!(reached_floor.range(), floor..first_tx(TIP));
1272        assert_eq!(
1273            (
1274                reached_floor.exhaustion,
1275                reached_floor.end_checkpoint,
1276                reached_floor.end_position,
1277            ),
1278            (
1279                RangeExhaustion::ServingFloor { floor_checkpoint },
1280                floor_checkpoint,
1281                floor,
1282            )
1283        );
1284
1285        let at_floor = tx_boundary(reached_floor.end_checkpoint, reached_floor.end_position);
1286        let below_floor = tx_boundary(6, 33);
1287        for ordering in [Ordering::Descending, Ordering::Ascending] {
1288            for before in [at_floor.clone(), below_floor.clone()] {
1289                let resume = before_options(ordering, before);
1290                let mut resumed = resolve_tx_request((None, None), &resume).unwrap();
1291                assert_below_serving_floor(
1292                    resumed.apply_serving_floor(floor, floor_checkpoint, &resume),
1293                    floor_checkpoint,
1294                );
1295            }
1296        }
1297    }
1298
1299    #[test]
1300    fn tx_range_covers_partial_endpoint_transactions() {
1301        let bounds = IntraTxScanBounds {
1302            lo: Bound::Included(IntraTxCoordinate {
1303                tx_seq: 10,
1304                index: 2,
1305            }),
1306            hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(13)),
1307        };
1308
1309        assert_eq!(bounds.tx_range(), Some(10..13));
1310    }
1311
1312    #[test]
1313    fn tx_range_keeps_tx_of_nonzero_exclusive_hi() {
1314        let bounds = IntraTxScanBounds {
1315            lo: Bound::Unbounded,
1316            hi: Bound::Excluded(IntraTxCoordinate {
1317                tx_seq: 13,
1318                index: 1,
1319            }),
1320        };
1321
1322        assert_eq!(bounds.tx_range(), Some(0..14));
1323    }
1324
1325    #[test]
1326    fn tx_range_empty_bounds_yield_none() {
1327        let bounds = IntraTxScanBounds::from_range(
1328            IntraTxCoordinate::start_of_tx(10)..IntraTxCoordinate::start_of_tx(10),
1329        );
1330        assert_eq!(bounds.tx_range(), None);
1331    }
1332
1333    /// Every after-Item cursor now resumes as an Excluded lo; the successor
1334    /// arithmetic and its `u64::MAX` saturation live here at the store edge.
1335    #[test]
1336    fn to_range_collapses_excluded_lo_at_successor() {
1337        let bounds = ScanBounds {
1338            lo: Bound::Excluded(14u64),
1339            hi: Bound::Excluded(20u64),
1340        };
1341        assert_eq!(bounds.to_range(), 15..20);
1342
1343        // The saturated successor at the unoccupiable sentinel admits
1344        // nothing, through the ordinary emptiness of MAX..MAX.
1345        let bounds = ScanBounds {
1346            lo: Bound::Excluded(u64::MAX),
1347            hi: Bound::Unbounded,
1348        };
1349        assert!(bounds.to_range().is_empty());
1350    }
1351
1352    #[test]
1353    fn parses_cursors_and_ordering() {
1354        let after = tx_item(2, 20).encode();
1355        let before = tx_item(3, 30).encode();
1356        let mut request = ProtoQueryOptions::default();
1357        request.limit = Some(500);
1358        request.after = Some(after);
1359        request.before = Some(before);
1360        request.ordering = Some(ProtoOrdering::Descending as i32);
1361
1362        let options = query_options_from_proto(Some(&request)).unwrap();
1363
1364        assert_eq!(options.limit_items, 500);
1365        assert_eq!(options.ordering, Ordering::Descending);
1366        assert_eq!(options.scan_direction(), ScanDirection::Descending);
1367        assert_eq!(
1368            resolved_range(0..100).apply_cursor_bounds(&options).range(),
1369            21..30
1370        );
1371    }
1372
1373    #[test]
1374    fn has_after_cursor_reflects_only_the_after_field() {
1375        // No cursors → open-ended low end.
1376        let options = query_options_from_proto(Some(&ProtoQueryOptions::default())).unwrap();
1377        assert!(!options.has_after_cursor());
1378
1379        // `before` bounds the high end, so it must not count as an explicit low end.
1380        let mut request = ProtoQueryOptions::default();
1381        request.before = Some(tx_item(3, 30).encode());
1382        let options = query_options_from_proto(Some(&request)).unwrap();
1383        assert!(!options.has_after_cursor());
1384
1385        // `after` raises the low end → explicit.
1386        let mut request = ProtoQueryOptions::default();
1387        request.after = Some(tx_item(2, 20).encode());
1388        let options = query_options_from_proto(Some(&request)).unwrap();
1389        assert!(options.has_after_cursor());
1390    }
1391
1392    #[test]
1393    fn clamps_limit_items_and_defaults_to_ascending() {
1394        let mut request = ProtoQueryOptions::default();
1395        request.limit = Some(5_000);
1396
1397        let options = query_options_from_proto(Some(&request)).unwrap();
1398
1399        assert_eq!(options.limit_items, 1_000);
1400        assert_eq!(options.ordering, Ordering::Ascending);
1401        assert_eq!(options.scan_direction(), ScanDirection::Ascending);
1402    }
1403
1404    #[test]
1405    fn rejects_malformed_cursors_and_unknown_ordering() {
1406        let mut request = ProtoQueryOptions::default();
1407        request.after = Some(Bytes::from_static(b"short"));
1408        assert!(query_options_from_proto(Some(&request)).is_err());
1409
1410        let mut request = ProtoQueryOptions::default();
1411        request.before = Some(Bytes::from_static(b"short"));
1412        assert!(query_options_from_proto(Some(&request)).is_err());
1413
1414        let mut request = ProtoQueryOptions::default();
1415        request.ordering = Some(99);
1416        assert!(query_options_from_proto(Some(&request)).is_err());
1417    }
1418
1419    #[test]
1420    fn rejects_cursor_for_different_position_variant() {
1421        let token = cp_item(9).encode();
1422        let mut request = ProtoQueryOptions::default();
1423        request.after = Some(token);
1424        assert!(query_options_from_proto(Some(&request)).is_err());
1425    }
1426
1427    #[test]
1428    fn accepts_cursor_regardless_of_filter_scope() {
1429        // Cursors are portable across filters: a Transactions cursor must be
1430        // accepted by a Transactions query even though `query_options_from_proto`
1431        // applies no filter. Position is an absolute, filter-independent
1432        // coordinate, so resuming under a different filter is correct.
1433        let after = tx_item(1, 9).encode();
1434        let before = tx_item(3, 30).encode();
1435        let mut request = ProtoQueryOptions::default();
1436        request.after = Some(after);
1437        request.before = Some(before);
1438        assert!(query_options_from_proto(Some(&request)).is_ok());
1439    }
1440
1441    #[test]
1442    fn accepts_cursors_for_different_checkpoint_range_and_ordering() {
1443        let token = tx_item(9, 9).encode();
1444        let mut request = ProtoQueryOptions::default();
1445        request.after = Some(token);
1446        request.ordering = Some(ProtoOrdering::Descending as i32);
1447
1448        let options = query_options_from_proto(Some(&request)).unwrap();
1449        let range =
1450            ResolvedCheckpointRange::from_request(Some(1_000), Some(1_100), 2_000, &options)
1451                .unwrap();
1452
1453        assert_eq!(range.range, 1_000..1_100);
1454    }
1455
1456    #[test]
1457    fn applies_canonical_cursor_bounds() {
1458        let options = QueryOptions {
1459            limit_items: 2,
1460            ordering: Ordering::Ascending,
1461            after: Some(tx_item(1, 11)),
1462            before: None,
1463        };
1464        assert_eq!(
1465            resolved_range(10..20).apply_cursor_bounds(&options).range(),
1466            12..20
1467        );
1468
1469        let options = QueryOptions {
1470            after: Some(tx_item(1, u64::MAX)),
1471            ..options
1472        };
1473        assert_eq!(
1474            resolved_range(10..20).apply_cursor_bounds(&options),
1475            empty_resolved_range(
1476                1,
1477                u64::MAX,
1478                RangeExhaustion::CursorBound {
1479                    kind: sui_rpc_cursor::CursorKind::Item,
1480                },
1481            )
1482        );
1483
1484        let options = QueryOptions {
1485            ordering: Ordering::Descending,
1486            after: Some(tx_item(1, 11)),
1487            before: Some(tx_item(1, 19)),
1488            ..options
1489        };
1490        let bounded = resolved_range(10..20).apply_cursor_bounds(&options);
1491        assert_eq!(bounded.range(), 12..19);
1492        assert_eq!(
1493            bounded.exhaustion,
1494            RangeExhaustion::CursorBound {
1495                kind: sui_rpc_cursor::CursorKind::Boundary,
1496            }
1497        );
1498        // The terminal is the boundary just above the `after` item, which the scan excluded.
1499        assert_eq!(bounded.end_position, 12);
1500    }
1501
1502    // (descending, after = Item 11, before = Item 12): the cursors leave the
1503    // empty gap (11, 12), so the fetch serves nothing and the CursorBound
1504    // frame comes from the descending stop side: the boundary just above the
1505    // after item, at 12.
1506    #[test]
1507    fn descending_before_adjacent_to_after_empties_above_the_after_item() {
1508        let options = QueryOptions {
1509            limit_items: 2,
1510            ordering: Ordering::Descending,
1511            after: Some(tx_item(1, 11)),
1512            before: Some(tx_item(1, 12)),
1513        };
1514        let crossed = resolved_range(10..20).apply_cursor_bounds(&options);
1515        assert_eq!(
1516            crossed,
1517            ResolvedScan {
1518                bounds: ScanBounds {
1519                    lo: Bound::Excluded(11),
1520                    hi: Bound::Excluded(12),
1521                },
1522                entry_checkpoint: 0,
1523                end_checkpoint: 1,
1524                end_position: 12,
1525                exhaustion: RangeExhaustion::CursorBound {
1526                    kind: sui_rpc_cursor::CursorKind::Boundary,
1527                },
1528                window_terminal_checkpoint: Some(20),
1529            }
1530        );
1531        assert!(crossed.range().is_empty());
1532    }
1533
1534    #[test]
1535    fn applies_boundary_cursor_bounds_without_item_offset() {
1536        let options = QueryOptions {
1537            limit_items: 2,
1538            ordering: Ordering::Ascending,
1539            after: Some(tx_boundary(2, 20)),
1540            before: None,
1541        };
1542        assert_eq!(
1543            resolved_range(10..30).apply_cursor_bounds(&options).range(),
1544            20..30
1545        );
1546
1547        let options = QueryOptions {
1548            ordering: Ordering::Descending,
1549            after: None,
1550            before: Some(tx_boundary(2, 20)),
1551            ..options
1552        };
1553        assert_eq!(
1554            resolved_range(10..30).apply_cursor_bounds(&options).range(),
1555            10..20
1556        );
1557    }
1558
1559    #[test]
1560    fn resolves_checkpoint_range_with_terminal_reason() {
1561        let options = query_options_from_proto(None).unwrap();
1562        assert_eq!(
1563            ResolvedCheckpointRange::from_request(None, None, 20, &options)
1564                .unwrap()
1565                .exhaustion,
1566            RangeExhaustion::LedgerTip
1567        );
1568        assert!(ResolvedCheckpointRange::from_request(Some(10), Some(9), 20, &options).is_err());
1569
1570        let resolved = ResolvedCheckpointRange::from_request(Some(10), None, 20, &options).unwrap();
1571        assert_eq!(resolved.range, 10..20);
1572        assert_eq!(resolved.exhaustion, RangeExhaustion::LedgerTip);
1573
1574        assert_eq!(
1575            ResolvedCheckpointRange::from_request(Some(30), None, 20, &options).unwrap(),
1576            ResolvedCheckpointRange::empty_at(20, RangeExhaustion::LedgerTip, true)
1577        );
1578    }
1579
1580    /// The cp-width clamp was removed when scan limiting moved to the runtime
1581    /// bucket-budget path. Whatever range the request asks for is honored at
1582    /// resolve time; the bitmap layer terminates scans on budget exhaustion.
1583    #[test]
1584    fn resolves_checkpoint_range_no_longer_clamped_by_width() {
1585        let options = query_options_from_proto(None).unwrap();
1586        let resolved =
1587            ResolvedCheckpointRange::from_request(Some(10), Some(10_000_000), 10_000_000, &options)
1588                .unwrap();
1589        assert_eq!(resolved.range, 10..10_000_000);
1590        assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
1591    }
1592
1593    /// (start_checkpoint = 30, tip = 20, ascending): resolves empty at the tip — entry and
1594    /// terminal checkpoints 20, LedgerTip.
1595    #[test]
1596    fn ascending_window_past_tip_empties_with_ledger_tip() {
1597        let options = QueryOptions::events_from_proto(None, 100, 100).unwrap();
1598        let cp_range = ResolvedCheckpointRange::from_request(Some(30), None, 20, &options).unwrap();
1599        assert!(cp_range.is_empty());
1600        assert_eq!(
1601            ResolvedScan::<IntraTxCoordinate>::resolve(
1602                cp_range,
1603                IntraTxCoordinate::tx_window(100..100),
1604                &options
1605            )
1606            .unwrap(),
1607            ResolvedScan {
1608                bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(100)),
1609                entry_checkpoint: 20,
1610                end_checkpoint: 20,
1611                end_position: IntraTxCoordinate::start_of_tx(100),
1612                exhaustion: RangeExhaustion::LedgerTip,
1613                window_terminal_checkpoint: None,
1614            }
1615        );
1616    }
1617
1618    /// A descending window that reaches past the tip (20) is out of range: an `end_checkpoint`
1619    /// above the tip, or a `start_checkpoint` at or above it with `end_checkpoint` unset. A
1620    /// window that ends at the tip resolves, and a zero-width window there is empty with
1621    /// CheckpointBound.
1622    #[test]
1623    fn descending_window_past_tip_is_out_of_range() {
1624        let mut request = ProtoQueryOptions::default();
1625        request.ordering = Some(ProtoOrdering::Descending as i32);
1626        let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1627        for (start, end) in [
1628            (Some(30), None),
1629            (Some(20), None),
1630            (Some(10), Some(21)),
1631            (None, Some(u64::MAX)),
1632            (Some(25), Some(30)),
1633        ] {
1634            let error = ResolvedCheckpointRange::from_request(start, end, 20, &options)
1635                .expect_err("descending window past the tip");
1636            assert_eq!(
1637                error.into_status_proto().code,
1638                tonic::Code::OutOfRange as i32,
1639                "window {start:?}..{end:?}"
1640            );
1641        }
1642
1643        let resolved = ResolvedCheckpointRange::from_request(Some(10), Some(20), 20, &options);
1644        assert_eq!(resolved.unwrap().range, 10..20);
1645        assert_eq!(
1646            ResolvedCheckpointRange::from_request(Some(20), Some(20), 20, &options).unwrap(),
1647            ResolvedCheckpointRange::empty_at(20, RangeExhaustion::CheckpointBound, false)
1648        );
1649    }
1650
1651    /// When the resolved checkpoint range is below checkpoint_hi_exclusive and empties, the
1652    /// exhaustion reason is CheckpointBound, not LedgerTip.
1653    #[test]
1654    fn resolve_zero_width_window_empties_with_checkpoint_bound() {
1655        for ascending in [true, false] {
1656            let mut request = ProtoQueryOptions::default();
1657            if !ascending {
1658                request.ordering = Some(ProtoOrdering::Descending as i32);
1659            }
1660            let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1661            let cp_range =
1662                ResolvedCheckpointRange::from_request(Some(10), Some(10), 20, &options).unwrap();
1663            assert!(cp_range.is_empty());
1664            assert_eq!(
1665                ResolvedScan::<IntraTxCoordinate>::resolve(
1666                    cp_range,
1667                    IntraTxCoordinate::tx_window(100..100),
1668                    &options
1669                )
1670                .unwrap(),
1671                ResolvedScan {
1672                    bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(100)),
1673                    entry_checkpoint: 10,
1674                    end_checkpoint: 10,
1675                    end_position: IntraTxCoordinate::start_of_tx(100),
1676                    exhaustion: RangeExhaustion::CheckpointBound,
1677                    window_terminal_checkpoint: None,
1678                }
1679            );
1680        }
1681    }
1682
1683    /// If the bounds are already empty, cursors do not apply.
1684    #[test]
1685    fn cursor_bounds_pass_empty_resolution_through_unchanged() {
1686        let position = Position::Events {
1687            checkpoint: 4,
1688            tx_seq: 50,
1689            event_index: 2,
1690        };
1691        for ascending in [true, false] {
1692            let mut request = ProtoQueryOptions::default();
1693            if !ascending {
1694                request.ordering = Some(ProtoOrdering::Descending as i32);
1695            }
1696            request.after = Some(CursorToken::item(position).encode());
1697            let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1698            let cp_range =
1699                ResolvedCheckpointRange::from_request(Some(10), Some(10), 20, &options).unwrap();
1700            let resolved = ResolvedScan::<IntraTxCoordinate>::resolve(
1701                cp_range,
1702                IntraTxCoordinate::tx_window(100..100),
1703                &options,
1704            )
1705            .unwrap();
1706            assert_eq!(resolved.clone().apply_cursor_bounds(&options), resolved);
1707        }
1708    }
1709
1710    /// resolve sets entry_checkpoint to the scan's first checkpoint and the terminal
1711    /// (end_checkpoint, end_position) to its last, per ordering.
1712    #[test]
1713    fn resolve_orients_entry_and_terminal_by_ordering() {
1714        let options = directional_options(true);
1715        let cp_range =
1716            ResolvedCheckpointRange::from_request(Some(3), Some(10), 20, &options).unwrap();
1717        assert_eq!(cp_range.range, 3..10);
1718        let resolved = ResolvedScan::<IntraTxCoordinate>::resolve(
1719            cp_range.clone(),
1720            IntraTxCoordinate::tx_window(100..200),
1721            &options,
1722        )
1723        .unwrap();
1724        assert_eq!(
1725            resolved.bounds,
1726            IntraTxScanBounds::from_range(
1727                IntraTxCoordinate::start_of_tx(100)..IntraTxCoordinate::start_of_tx(200)
1728            )
1729        );
1730        assert_eq!(resolved.entry_checkpoint, 3);
1731        assert_eq!(resolved.end_checkpoint, 10);
1732        assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(200));
1733
1734        let options = directional_options(false);
1735        let resolved = ResolvedScan::<IntraTxCoordinate>::resolve(
1736            cp_range,
1737            IntraTxCoordinate::tx_window(100..200),
1738            &options,
1739        )
1740        .unwrap();
1741        assert_eq!(resolved.entry_checkpoint, 9);
1742        assert_eq!(resolved.end_checkpoint, 3);
1743        assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(100));
1744    }
1745
1746    /// (ascending, `after` past the window's end): the page ends with the window's reason at the
1747    /// boundary where the cursor resumes, under the cursor's checkpoint field, and claims the
1748    /// window's last checkpoint. An Item cursor's boundary sits after its event, so resuming from
1749    /// it does not re-serve the event.
1750    #[test]
1751    fn event_after_cursor_past_window_ends_where_it_resumes() {
1752        // Checkpoints 1..4, transactions 5..20.
1753        let window = (Some(1), Some(4));
1754        for (cursor, end_position) in [
1755            (
1756                ev_item(5, 25, 0),
1757                IntraTxCoordinate {
1758                    tx_seq: 25,
1759                    index: 1,
1760                },
1761            ),
1762            (ev_boundary(5, 25, 0), IntraTxCoordinate::start_of_tx(25)),
1763        ] {
1764            let options = after_options(Ordering::Ascending, cursor.clone());
1765            let resolved = resolve_event_request(window, &options).unwrap();
1766            assert!(resolved.is_empty(), "{cursor:?}");
1767            assert_eq!(
1768                resolved.exhaustion,
1769                RangeExhaustion::CheckpointBound,
1770                "{cursor:?}"
1771            );
1772            assert_eq!(
1773                (resolved.end_checkpoint, resolved.end_position),
1774                (5, end_position),
1775                "{cursor:?}"
1776            );
1777            assert_eq!(resolved.natural_end_claim(&options), Some(3), "{cursor:?}");
1778        }
1779    }
1780
1781    /// `before` cursor that empties the interval sets end_position, and exhaustion to CursorBound.
1782    #[test]
1783    fn event_before_cursor_empties_descending_interval() {
1784        // Start descending over tx range [100, 200) with entry at checkpoint 9 terminating at checkpoint 3
1785        let resolved = ResolvedScan {
1786            // always in ascending order
1787            bounds: IntraTxScanBounds::from_range(
1788                IntraTxCoordinate::start_of_tx(100)..IntraTxCoordinate::start_of_tx(200),
1789            ),
1790            // checkpoints reflect ordering
1791            entry_checkpoint: 9,
1792            end_checkpoint: 3,
1793            end_position: IntraTxCoordinate::start_of_tx(100),
1794            exhaustion: RangeExhaustion::CheckpointBound,
1795            window_terminal_checkpoint: Some(3),
1796        };
1797
1798        // This cursor empties the interval; the entry checkpoint stays at the window's.
1799        let cursor = Position::Events {
1800            checkpoint: 2,
1801            tx_seq: 90,
1802            event_index: 0,
1803        };
1804
1805        for token in [CursorToken::item(cursor), CursorToken::boundary(cursor)] {
1806            let mut request = ProtoQueryOptions::default();
1807            request.ordering = Some(ProtoOrdering::Descending as i32);
1808            request.before = Some(token.encode());
1809            let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1810
1811            let new_coordinate = IntraTxCoordinate::start_of_tx(90);
1812            assert_eq!(
1813                resolved.clone().apply_cursor_bounds(&options),
1814                ResolvedScan {
1815                    bounds: IntraTxScanBounds::empty_at(new_coordinate),
1816                    entry_checkpoint: 9,
1817                    end_checkpoint: 2,
1818                    // takes on the cursor position that emptied the range
1819                    end_position: new_coordinate,
1820                    exhaustion: RangeExhaustion::CursorBound {
1821                        kind: sui_rpc_cursor::CursorKind::Boundary,
1822                    },
1823                    window_terminal_checkpoint: Some(3),
1824                }
1825            );
1826        }
1827    }
1828
1829    /// (ascending, after inside the window): Item admits strictly after its
1830    /// coordinate, Boundary from it; entry checkpoint and terminal untouched.
1831    #[test]
1832    fn after_cursor_tightens_ascending_lower_bound() {
1833        let options = after_options(Ordering::Ascending, ev_item(3, 5, 1));
1834        assert_eq!(
1835            resolved_intra_tx().apply_cursor_bounds(&options),
1836            ResolvedScan {
1837                bounds: IntraTxScanBounds {
1838                    lo: Bound::Excluded(IntraTxCoordinate {
1839                        tx_seq: 5,
1840                        index: 1,
1841                    }),
1842                    hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1843                },
1844                ..resolved_intra_tx()
1845            }
1846        );
1847
1848        let options = after_options(Ordering::Ascending, ev_boundary(3, 5, 1));
1849        assert_eq!(
1850            resolved_intra_tx().apply_cursor_bounds(&options),
1851            ResolvedScan {
1852                bounds: IntraTxScanBounds {
1853                    lo: Bound::Included(IntraTxCoordinate {
1854                        tx_seq: 5,
1855                        index: 1,
1856                    }),
1857                    hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1858                },
1859                ..resolved_intra_tx()
1860            }
1861        );
1862    }
1863
1864    #[test]
1865    fn after_cursor_below_lower_bound_changes_nothing() {
1866        let seed = ResolvedScan {
1867            bounds: IntraTxScanBounds {
1868                lo: Bound::Included(IntraTxCoordinate::start_of_tx(5)),
1869                hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1870            },
1871            ..resolved_intra_tx()
1872        };
1873        let options = after_options(Ordering::Ascending, ev_boundary(4, 2, 0));
1874        assert_eq!(seed.clone().apply_cursor_bounds(&options), seed);
1875        let options = after_options(Ordering::Ascending, ev_item(4, 2, 0));
1876        assert_eq!(seed.clone().apply_cursor_bounds(&options), seed);
1877    }
1878
1879    /// Tightens the lower bound, and the terminal becomes the boundary just below the lowest
1880    /// admitted position, because the after cursor is where a descending scan stops: the slot
1881    /// after an Item cursor, which the scan excluded, and a Boundary cursor itself.
1882    #[test]
1883    fn descending_after_cursor_sets_terminal_and_tightens() {
1884        let options = after_options(Ordering::Descending, ev_item(3, 5, 1));
1885        assert_eq!(
1886            resolved_intra_tx().apply_cursor_bounds(&options),
1887            ResolvedScan {
1888                bounds: IntraTxScanBounds {
1889                    lo: Bound::Excluded(IntraTxCoordinate {
1890                        tx_seq: 5,
1891                        index: 1,
1892                    }),
1893                    hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1894                },
1895                end_checkpoint: 3,
1896                end_position: IntraTxCoordinate {
1897                    tx_seq: 5,
1898                    index: 2,
1899                },
1900                exhaustion: RangeExhaustion::CursorBound {
1901                    kind: sui_rpc_cursor::CursorKind::Boundary,
1902                },
1903                ..resolved_intra_tx()
1904            }
1905        );
1906
1907        let options = after_options(Ordering::Descending, ev_boundary(3, 5, 1));
1908        assert_eq!(
1909            resolved_intra_tx().apply_cursor_bounds(&options),
1910            ResolvedScan {
1911                bounds: IntraTxScanBounds {
1912                    lo: Bound::Included(IntraTxCoordinate {
1913                        tx_seq: 5,
1914                        index: 1,
1915                    }),
1916                    hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
1917                },
1918                end_checkpoint: 3,
1919                end_position: IntraTxCoordinate {
1920                    tx_seq: 5,
1921                    index: 1,
1922                },
1923                exhaustion: RangeExhaustion::CursorBound {
1924                    kind: sui_rpc_cursor::CursorKind::Boundary,
1925                },
1926                ..resolved_intra_tx()
1927            }
1928        );
1929    }
1930
1931    /// When the after cursor empties the window, the record empties at the boundary just below
1932    /// what the cursor admits, at the cursor's checkpoint: the slot after an Item cursor, a
1933    /// Boundary cursor itself, both as Boundary.
1934    #[test]
1935    fn after_cursor_empties_descending_interval() {
1936        let seed = ResolvedScan {
1937            bounds: IntraTxScanBounds::from_range(
1938                IntraTxCoordinate::start_of_tx(3)..IntraTxCoordinate::start_of_tx(9),
1939            ),
1940            entry_checkpoint: 9,
1941            end_checkpoint: 3,
1942            end_position: IntraTxCoordinate::start_of_tx(3),
1943            exhaustion: RangeExhaustion::CheckpointBound,
1944            window_terminal_checkpoint: Some(3),
1945        };
1946
1947        let emptied_at = |coordinate| ResolvedScan {
1948            bounds: IntraTxScanBounds::empty_at(coordinate),
1949            entry_checkpoint: 9,
1950            end_checkpoint: 100,
1951            end_position: coordinate,
1952            exhaustion: RangeExhaustion::CursorBound {
1953                kind: sui_rpc_cursor::CursorKind::Boundary,
1954            },
1955            window_terminal_checkpoint: Some(3),
1956        };
1957
1958        let options = after_options(Ordering::Descending, ev_item(100, 100, 10));
1959        assert_eq!(
1960            seed.clone().apply_cursor_bounds(&options),
1961            emptied_at(IntraTxCoordinate {
1962                tx_seq: 100,
1963                index: 11,
1964            })
1965        );
1966        let options = after_options(Ordering::Descending, ev_boundary(100, 100, 10));
1967        assert_eq!(
1968            seed.apply_cursor_bounds(&options),
1969            emptied_at(IntraTxCoordinate {
1970                tx_seq: 100,
1971                index: 10,
1972            })
1973        );
1974    }
1975
1976    /// (descending, before inside the window, either cursor kind (Item or
1977    /// Boundary)): hi becomes exclusive at the cursor; entry checkpoint and
1978    /// terminal untouched.
1979    #[test]
1980    fn before_cursor_tightens_descending_upper_bound() {
1981        let seed = ResolvedScan {
1982            entry_checkpoint: 8,
1983            ..resolved_intra_tx()
1984        };
1985        let expected = ResolvedScan {
1986            bounds: IntraTxScanBounds {
1987                lo: Bound::Included(IntraTxCoordinate::start_of_tx(0)),
1988                hi: Bound::Excluded(IntraTxCoordinate {
1989                    tx_seq: 5,
1990                    index: 1,
1991                }),
1992            },
1993            ..seed.clone()
1994        };
1995        let options = before_options(Ordering::Descending, ev_item(6, 5, 1));
1996        assert_eq!(seed.clone().apply_cursor_bounds(&options), expected);
1997        let options = before_options(Ordering::Descending, ev_boundary(6, 5, 1));
1998        assert_eq!(seed.apply_cursor_bounds(&options), expected);
1999    }
2000
2001    #[test]
2002    fn before_cursor_above_window_never_tightens() {
2003        for cursor in [ev_boundary(1, 12, 0), ev_item(1, 12, 0)] {
2004            let options = before_options(Ordering::Descending, cursor.clone());
2005            assert_eq!(
2006                resolved_intra_tx().apply_cursor_bounds(&options),
2007                resolved_intra_tx()
2008            );
2009
2010            let options = before_options(Ordering::Ascending, cursor);
2011            assert_eq!(
2012                resolved_intra_tx().apply_cursor_bounds(&options),
2013                resolved_intra_tx()
2014            );
2015        }
2016    }
2017
2018    #[test]
2019    fn ascending_before_cursor_sets_terminal_and_tightens() {
2020        let expected = ResolvedScan {
2021            bounds: IntraTxScanBounds {
2022                lo: Bound::Included(IntraTxCoordinate::start_of_tx(0)),
2023                hi: Bound::Excluded(IntraTxCoordinate {
2024                    tx_seq: 5,
2025                    index: 1,
2026                }),
2027            },
2028            entry_checkpoint: 2,
2029            end_checkpoint: 6,
2030            end_position: IntraTxCoordinate {
2031                tx_seq: 5,
2032                index: 1,
2033            },
2034            exhaustion: RangeExhaustion::CursorBound {
2035                kind: sui_rpc_cursor::CursorKind::Boundary,
2036            },
2037            window_terminal_checkpoint: Some(9),
2038        };
2039        let options = before_options(Ordering::Ascending, ev_boundary(6, 5, 1));
2040        assert_eq!(resolved_intra_tx().apply_cursor_bounds(&options), expected);
2041        let options = before_options(Ordering::Ascending, ev_item(6, 5, 1));
2042        assert_eq!(resolved_intra_tx().apply_cursor_bounds(&options), expected);
2043    }
2044
2045    /// When a before cursor empties an ascending interval, the bounds are emptied at the cursor,
2046    /// and end_position and end_checkpoint are set to the cursor's.
2047    #[test]
2048    fn before_cursor_empties_ascending_interval() {
2049        let seed = ResolvedScan {
2050            bounds: IntraTxScanBounds::from_range(
2051                IntraTxCoordinate::start_of_tx(3)..IntraTxCoordinate::start_of_tx(10),
2052            ),
2053            entry_checkpoint: 2,
2054            end_checkpoint: 9,
2055            ..resolved_intra_tx()
2056        };
2057        let expected = ResolvedScan {
2058            bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(3)),
2059            end_checkpoint: 1,
2060            end_position: IntraTxCoordinate::start_of_tx(3),
2061            exhaustion: RangeExhaustion::CursorBound {
2062                kind: sui_rpc_cursor::CursorKind::Boundary,
2063            },
2064            ..seed.clone()
2065        };
2066        let options = before_options(Ordering::Ascending, ev_boundary(1, 3, 0));
2067        assert_eq!(seed.clone().apply_cursor_bounds(&options), expected);
2068        let options = before_options(Ordering::Ascending, ev_item(1, 3, 0));
2069        assert_eq!(seed.apply_cursor_bounds(&options), expected);
2070    }
2071
2072    /// (ascending, after and before both present): a `before` past the window's end does not stop
2073    /// the scan, so an `after` past the window ends the page with the window's reason where it
2074    /// resumes; if both empty the interval, the before wins at the min position.
2075    #[test]
2076    fn ascending_with_both_cursors_picks_the_terminal_owner() {
2077        // Checkpoints 1..4, transactions 5..20. The before cursor lies past the window's end, and
2078        // the after cursor past it empties the window.
2079        let options = QueryOptions {
2080            limit_items: 100,
2081            ordering: Ordering::Ascending,
2082            after: Some(ev_item(5, 25, 0)),
2083            before: Some(ev_boundary(6, 30, 0)),
2084        };
2085        let resolved = resolve_event_request((Some(1), Some(4)), &options).unwrap();
2086        assert!(resolved.is_empty());
2087        assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
2088        assert_eq!(
2089            (resolved.end_checkpoint, resolved.end_position),
2090            (
2091                5,
2092                IntraTxCoordinate {
2093                    tx_seq: 25,
2094                    index: 1
2095                }
2096            )
2097        );
2098
2099        // When both cursors would empty, `before` cursor attribution remains.
2100        let options = QueryOptions {
2101            limit_items: 100,
2102            ordering: Ordering::Ascending,
2103            after: Some(ev_boundary(12, 12, 0)),
2104            before: Some(ev_boundary(3, 3, 0)),
2105        };
2106        assert_eq!(
2107            resolved_intra_tx().apply_cursor_bounds(&options),
2108            ResolvedScan {
2109                bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(3)),
2110                entry_checkpoint: 2,
2111                end_checkpoint: 3,
2112                end_position: IntraTxCoordinate::start_of_tx(3),
2113                exhaustion: RangeExhaustion::CursorBound {
2114                    kind: sui_rpc_cursor::CursorKind::Boundary,
2115                },
2116                window_terminal_checkpoint: Some(9),
2117            }
2118        );
2119    }
2120
2121    /// (descending, after and before both present): the after's frame stands when
2122    /// only it files; when both file, the min position wins; a before that empties
2123    /// overrides the after's earlier terminal.
2124    #[test]
2125    fn descending_with_both_cursors_picks_the_terminal_owner() {
2126        // After empties, before rejected: the after-claim survives.
2127        let options = QueryOptions {
2128            limit_items: 100,
2129            ordering: Ordering::Descending,
2130            after: Some(ev_boundary(9, 12, 0)),
2131            before: Some(ev_boundary(1, 15, 0)),
2132        };
2133        assert_eq!(
2134            resolved_intra_tx().apply_cursor_bounds(&options),
2135            ResolvedScan {
2136                bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(12)),
2137                entry_checkpoint: 2,
2138                end_checkpoint: 9,
2139                end_position: IntraTxCoordinate::start_of_tx(12),
2140                exhaustion: RangeExhaustion::CursorBound {
2141                    kind: sui_rpc_cursor::CursorKind::Boundary,
2142                },
2143                window_terminal_checkpoint: Some(9),
2144            }
2145        );
2146
2147        // After empties, before also files: before wins at the min position.
2148        let options = QueryOptions {
2149            limit_items: 100,
2150            ordering: Ordering::Descending,
2151            after: Some(ev_boundary(9, 12, 0)),
2152            before: Some(ev_item(1, 5, 2)),
2153        };
2154        assert_eq!(
2155            resolved_intra_tx().apply_cursor_bounds(&options),
2156            ResolvedScan {
2157                bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate {
2158                    tx_seq: 5,
2159                    index: 2,
2160                }),
2161                entry_checkpoint: 2,
2162                end_checkpoint: 1,
2163                end_position: IntraTxCoordinate {
2164                    tx_seq: 5,
2165                    index: 2,
2166                },
2167                exhaustion: RangeExhaustion::CursorBound {
2168                    kind: sui_rpc_cursor::CursorKind::Boundary,
2169                },
2170                window_terminal_checkpoint: Some(9),
2171            }
2172        );
2173
2174        // After tightens without emptying, before empties: the before-claim
2175        // overrides the after-cursor's eagerly written terminal.
2176        let options = QueryOptions {
2177            limit_items: 100,
2178            ordering: Ordering::Descending,
2179            after: Some(ev_item(8, 5, 0)),
2180            before: Some(ev_item(1, 5, 0)),
2181        };
2182        assert_eq!(
2183            resolved_intra_tx().apply_cursor_bounds(&options),
2184            ResolvedScan {
2185                bounds: IntraTxScanBounds::empty_at(IntraTxCoordinate::start_of_tx(5)),
2186                entry_checkpoint: 2,
2187                end_checkpoint: 1,
2188                end_position: IntraTxCoordinate::start_of_tx(5),
2189                exhaustion: RangeExhaustion::CursorBound {
2190                    kind: sui_rpc_cursor::CursorKind::Boundary,
2191                },
2192                window_terminal_checkpoint: Some(9),
2193            }
2194        );
2195    }
2196
2197    /// (descending, after below the window): complete noop.
2198    #[test]
2199    fn after_cursor_rejected_descending_is_noop() {
2200        let seed = ResolvedScan {
2201            bounds: IntraTxScanBounds {
2202                lo: Bound::Included(IntraTxCoordinate::start_of_tx(5)),
2203                hi: Bound::Excluded(IntraTxCoordinate::start_of_tx(10)),
2204            },
2205            ..resolved_intra_tx()
2206        };
2207        let options = after_options(Ordering::Descending, ev_boundary(4, 2, 0));
2208        assert_eq!(seed.clone().apply_cursor_bounds(&options), seed);
2209    }
2210
2211    /// A cursor whose bound coincides with the window's own is a no-op tighten, but
2212    /// the terminal still becomes the cursor's — CursorBound on an unchanged,
2213    /// non-empty record.
2214    #[test]
2215    fn cursor_at_the_window_bound_still_takes_the_terminal() {
2216        // Admission at the exact bound is a no-op tighten, but the stop-side
2217        // cursor still takes over the terminal: exhaustion flips to
2218        // CursorBound on a non-empty result whose bounds did not change.
2219        let options = before_options(Ordering::Ascending, ev_boundary(6, 10, 0));
2220        assert_eq!(
2221            resolved_intra_tx().apply_cursor_bounds(&options),
2222            ResolvedScan {
2223                bounds: IntraTxScanBounds::from_range(
2224                    IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(10)
2225                ),
2226                entry_checkpoint: 2,
2227                end_checkpoint: 6,
2228                end_position: IntraTxCoordinate::start_of_tx(10),
2229                exhaustion: RangeExhaustion::CursorBound {
2230                    kind: sui_rpc_cursor::CursorKind::Boundary,
2231                },
2232                window_terminal_checkpoint: Some(9),
2233            }
2234        );
2235
2236        let options = after_options(Ordering::Descending, ev_boundary(6, 0, 0));
2237        assert_eq!(
2238            resolved_intra_tx().apply_cursor_bounds(&options),
2239            ResolvedScan {
2240                bounds: IntraTxScanBounds::from_range(
2241                    IntraTxCoordinate::start_of_tx(0)..IntraTxCoordinate::start_of_tx(10)
2242                ),
2243                entry_checkpoint: 2,
2244                end_checkpoint: 6,
2245                end_position: IntraTxCoordinate::start_of_tx(0),
2246                exhaustion: RangeExhaustion::CursorBound {
2247                    kind: sui_rpc_cursor::CursorKind::Boundary,
2248                },
2249                window_terminal_checkpoint: Some(9),
2250            }
2251        );
2252    }
2253
2254    // An after-Item cursor makes the exclusive lower bound one past the cursor, while an
2255    // after-Boundary cursor makes the lower bound equal to the cursor.
2256    #[test]
2257    fn tx_after_cursor_tightens_ascending_lower_bound() {
2258        // Item at n admits strictly-after symbolically; the successor lives
2259        // at the store edge, so both forms fetch from n + 1.
2260        let options = after_options(Ordering::Ascending, tx_item(3, 12));
2261        // Resolves to `(12, 20)`
2262        let tightened = resolved_range(10..20).apply_cursor_bounds(&options);
2263        assert_eq!(
2264            tightened,
2265            ResolvedScan {
2266                bounds: ScanBounds {
2267                    lo: Bound::Excluded(12),
2268                    hi: Bound::Excluded(20),
2269                },
2270                ..resolved_range(10..20)
2271            }
2272        );
2273        // Projection into Range<u64> sets the lower bound as n + 1
2274        assert_eq!(tightened.range(), 13..20);
2275
2276        let options = after_options(Ordering::Ascending, tx_boundary(3, 12));
2277        assert_eq!(
2278            resolved_range(10..20).apply_cursor_bounds(&options),
2279            ResolvedScan {
2280                bounds: ScanBounds {
2281                    lo: Bound::Included(12),
2282                    hi: Bound::Excluded(20),
2283                },
2284                ..resolved_range(10..20)
2285            }
2286        );
2287    }
2288
2289    /// Tightens the lower bound, and the terminal becomes the boundary just below the lowest
2290    /// admitted position, because the after cursor is where a descending scan stops: the position
2291    /// after an Item cursor, which the scan excluded, and a Boundary cursor itself.
2292    #[test]
2293    fn tx_descending_after_cursor_sets_terminal_and_tightens() {
2294        // Item at 12 and Boundary at 13 scan the same exclusive edge (13..20),
2295        // and end on the same terminal: the boundary at 13.
2296        let options = after_options(Ordering::Descending, tx_item(3, 12));
2297        assert_eq!(
2298            resolved_range(10..20).apply_cursor_bounds(&options),
2299            ResolvedScan {
2300                bounds: ScanBounds {
2301                    lo: Bound::Excluded(12),
2302                    hi: Bound::Excluded(20),
2303                },
2304                entry_checkpoint: 0,
2305                end_checkpoint: 3,
2306                end_position: 13,
2307                exhaustion: RangeExhaustion::CursorBound {
2308                    kind: sui_rpc_cursor::CursorKind::Boundary,
2309                },
2310                window_terminal_checkpoint: Some(20),
2311            }
2312        );
2313        let options = after_options(Ordering::Descending, tx_boundary(3, 13));
2314        assert_eq!(
2315            resolved_range(10..20).apply_cursor_bounds(&options),
2316            ResolvedScan {
2317                bounds: ScanBounds::from_range(13..20),
2318                entry_checkpoint: 0,
2319                end_checkpoint: 3,
2320                end_position: 13,
2321                exhaustion: RangeExhaustion::CursorBound {
2322                    kind: sui_rpc_cursor::CursorKind::Boundary,
2323                },
2324                window_terminal_checkpoint: Some(20),
2325            }
2326        );
2327    }
2328
2329    /// When the after cursor empties the window, the record empties at the boundary just below
2330    /// what the cursor admits, at the cursor's checkpoint, as Boundary for either cursor kind.
2331    #[test]
2332    fn tx_after_cursor_empties_descending_interval() {
2333        // Item at 24 and Boundary at 25 scan the same exclusive edge, and both empty the window
2334        // at the boundary at 25.
2335        let options = after_options(Ordering::Descending, tx_boundary(9, 25));
2336        assert_eq!(
2337            resolved_range(10..20).apply_cursor_bounds(&options),
2338            ResolvedScan {
2339                bounds: ScanBounds::from_range(25..25),
2340                entry_checkpoint: 0,
2341                end_checkpoint: 9,
2342                end_position: 25,
2343                exhaustion: RangeExhaustion::CursorBound {
2344                    kind: sui_rpc_cursor::CursorKind::Boundary,
2345                },
2346                window_terminal_checkpoint: Some(20),
2347            }
2348        );
2349        let options = after_options(Ordering::Descending, tx_item(9, 24));
2350        assert_eq!(
2351            resolved_range(10..20).apply_cursor_bounds(&options),
2352            ResolvedScan {
2353                bounds: ScanBounds::from_range(25..25),
2354                entry_checkpoint: 0,
2355                end_checkpoint: 9,
2356                end_position: 25,
2357                exhaustion: RangeExhaustion::CursorBound {
2358                    kind: sui_rpc_cursor::CursorKind::Boundary,
2359                },
2360                window_terminal_checkpoint: Some(20),
2361            }
2362        );
2363    }
2364
2365    /// after-Item at n and after-Boundary at n + 1 admit the same interval, and a page they leave
2366    /// empty past the window ends on the same terminal: the boundary at n + 1.
2367    #[test]
2368    fn tx_after_item_and_successor_boundary_admit_the_same_interval() {
2369        let item = after_options(Ordering::Ascending, tx_item(4, 24));
2370        let successor = after_options(Ordering::Ascending, tx_boundary(4, 25));
2371
2372        // Checkpoints 1..6, transactions 5..30: both admit 25..30.
2373        let wide = (Some(1), Some(6));
2374        assert_eq!(resolve_tx_request(wide, &item).unwrap().range(), 25..30);
2375        assert_eq!(
2376            resolve_tx_request(wide, &successor).unwrap().range(),
2377            25..30
2378        );
2379
2380        // Checkpoints 1..4, transactions 5..20: both leave the window empty and end at the
2381        // boundary at 25 with the window's reason.
2382        let narrow = (Some(1), Some(4));
2383        let resolved = resolve_tx_request(narrow, &item).unwrap();
2384        assert_eq!(resolved, resolve_tx_request(narrow, &successor).unwrap());
2385        assert!(resolved.is_empty());
2386        assert_eq!(
2387            (
2388                resolved.exhaustion,
2389                resolved.end_checkpoint,
2390                resolved.end_position
2391            ),
2392            (RangeExhaustion::CheckpointBound, 4, 25)
2393        );
2394    }
2395
2396    /// (ascending, after-Boundary at u64::MAX): nothing can lie at MAX; empties at
2397    /// (cursor checkpoint, MAX), Boundary.
2398    #[test]
2399    fn tx_after_boundary_at_max_empties_interval() {
2400        let options = after_options(Ordering::Ascending, tx_boundary(5, u64::MAX));
2401        assert_eq!(
2402            resolved_range(10..20).apply_cursor_bounds(&options),
2403            empty_resolved_range(
2404                5,
2405                u64::MAX,
2406                RangeExhaustion::CursorBound {
2407                    kind: sui_rpc_cursor::CursorKind::Boundary,
2408                },
2409            )
2410        );
2411    }
2412
2413    /// If the bounds are already empty, cursors do not apply.
2414    #[test]
2415    fn tx_after_item_at_max_lets_before_cursor_win() {
2416        // An after-Item at u64::MAX is an ordinary emptying admission (the
2417        // saturated successor), not a short-circuit: a crossed before-cursor
2418        // still participates and wins the attribution at the min position.
2419        let options = QueryOptions {
2420            limit_items: 100,
2421            ordering: Ordering::Ascending,
2422            after: Some(tx_item(5, u64::MAX)),
2423            before: Some(tx_boundary(3, 15)),
2424        };
2425        assert_eq!(
2426            resolved_range(10..20).apply_cursor_bounds(&options),
2427            ResolvedScan {
2428                bounds: ScanBounds::from_range(15..15),
2429                entry_checkpoint: 0,
2430                end_checkpoint: 3,
2431                end_position: 15,
2432                exhaustion: RangeExhaustion::CursorBound {
2433                    kind: sui_rpc_cursor::CursorKind::Boundary,
2434                },
2435                window_terminal_checkpoint: Some(20),
2436            }
2437        );
2438
2439        // Descending: the seed's entry checkpoint stands.
2440        let seed = ResolvedScan {
2441            entry_checkpoint: 7,
2442            ..resolved_range(10..20)
2443        };
2444        let options = QueryOptions {
2445            limit_items: 100,
2446            ordering: Ordering::Descending,
2447            after: Some(tx_item(5, u64::MAX)),
2448            before: Some(tx_boundary(3, 15)),
2449        };
2450        assert_eq!(
2451            seed.apply_cursor_bounds(&options),
2452            ResolvedScan {
2453                bounds: ScanBounds::from_range(15..15),
2454                entry_checkpoint: 7,
2455                end_checkpoint: 3,
2456                end_position: 15,
2457                exhaustion: RangeExhaustion::CursorBound {
2458                    kind: sui_rpc_cursor::CursorKind::Boundary,
2459                },
2460                window_terminal_checkpoint: Some(20),
2461            }
2462        );
2463    }
2464
2465    /// For some ResolvedScan<u64>, if its bounds are Excluded(A) and Excluded(A + 1), its bounds
2466    /// report non-empty, but its range is empty.
2467    /// `test_list_transactions_after_last_item_ends_at_window_bound` checks that the result set is
2468    /// empty.
2469    #[test]
2470    fn tx_after_item_at_window_edge_defers_to_drain() {
2471        let options = after_options(Ordering::Ascending, tx_item(5, 19));
2472        let initial_range = resolved_range(10..20);
2473        let resolved = initial_range.clone().apply_cursor_bounds(&options);
2474        assert_eq!(
2475            resolved,
2476            ResolvedScan {
2477                bounds: ScanBounds {
2478                    lo: Bound::Excluded(19),
2479                    hi: Bound::Excluded(20),
2480                },
2481                ..initial_range
2482            }
2483        );
2484        assert!(!resolved.is_empty());
2485        assert!(resolved.range().is_empty());
2486    }
2487
2488    /// Exclusive after on descending bumps the bounds, and sets the end_checkpoint, end_position
2489    /// (the boundary just above the item), and exhaustion from the after cursor.
2490    #[test]
2491    fn tx_after_item_at_window_edge_descending_keeps_stamped_terminal() {
2492        let options = after_options(Ordering::Descending, tx_item(5, 19));
2493        let initial_range = resolved_range(10..20);
2494        let resolved = initial_range.clone().apply_cursor_bounds(&options);
2495        assert_eq!(
2496            resolved,
2497            ResolvedScan {
2498                bounds: ScanBounds {
2499                    lo: Bound::Excluded(19),
2500                    hi: Bound::Excluded(20),
2501                },
2502                end_checkpoint: 5,
2503                end_position: 20,
2504                exhaustion: RangeExhaustion::CursorBound {
2505                    kind: sui_rpc_cursor::CursorKind::Boundary,
2506                },
2507                ..initial_range
2508            }
2509        );
2510        assert!(resolved.range().is_empty());
2511    }
2512
2513    /// On a descending scan, when the after-Item cursor is exactly on the terminating window, the
2514    /// terminal values remain unchanged; the cursor does not overwrite the original bounds as it is
2515    /// not additionally restrictive.
2516    #[test]
2517    fn tx_after_item_below_window_start_descending_is_noop() {
2518        let options = after_options(Ordering::Descending, tx_item(5, 9));
2519        assert_eq!(
2520            resolved_range(10..20).apply_cursor_bounds(&options),
2521            resolved_range(10..20)
2522        );
2523    }
2524
2525    #[test]
2526    fn item_cursor_can_be_used_as_after_or_before() {
2527        let token = CursorToken::item(Position::Transactions {
2528            checkpoint: 1,
2529            tx_seq: 11,
2530        })
2531        .encode();
2532
2533        let mut request = ProtoQueryOptions::default();
2534        request.after = Some(token.clone());
2535        let options = query_options_from_proto(Some(&request)).unwrap();
2536        assert_eq!(
2537            resolved_range(10..20).apply_cursor_bounds(&options).range(),
2538            12..20
2539        );
2540
2541        request.after = None;
2542        request.before = Some(token);
2543        let options = query_options_from_proto(Some(&request)).unwrap();
2544        assert_eq!(
2545            resolved_range(10..20).apply_cursor_bounds(&options).range(),
2546            10..11
2547        );
2548    }
2549
2550    /// Tip for the cursor-past-tip tests: checkpoints below 20 are indexed, and checkpoint `c`
2551    /// starts at transaction `5 * c`, so the first unindexed transaction is 100.
2552    const TIP: u64 = 20;
2553
2554    fn first_tx(checkpoint: u64) -> u64 {
2555        5 * checkpoint
2556    }
2557
2558    fn resolve_checkpoints(
2559        end_checkpoint: Option<u64>,
2560        options: &QueryOptions,
2561    ) -> Result<ResolvedScan<u64>, RpcError> {
2562        let cp_range = ResolvedCheckpointRange::from_request(None, end_checkpoint, TIP, options)?;
2563        let range = cp_range.range.clone();
2564        ResolvedScan::<u64>::resolve(cp_range, range, options)
2565    }
2566
2567    fn resolve_transactions(options: &QueryOptions) -> Result<ResolvedScan<u64>, RpcError> {
2568        let cp_range = ResolvedCheckpointRange::from_request(None, None, TIP, options)?;
2569        let tx_range = first_tx(cp_range.range.start)..first_tx(cp_range.range.end);
2570        ResolvedScan::<u64>::resolve(cp_range, tx_range, options)
2571    }
2572
2573    fn resolve_events(options: &QueryOptions) -> Result<ResolvedScan<IntraTxCoordinate>, RpcError> {
2574        let cp_range = ResolvedCheckpointRange::from_request(None, None, TIP, options)?;
2575        let tx_range = first_tx(cp_range.range.start)..first_tx(cp_range.range.end);
2576        ResolvedScan::<IntraTxCoordinate>::resolve(
2577            cp_range,
2578            IntraTxCoordinate::tx_window(tx_range),
2579            options,
2580        )
2581    }
2582
2583    fn is_out_of_range<P>(resolved: Result<ResolvedScan<P>, RpcError>) -> bool {
2584        resolved.is_err_and(|e| e.into_status_proto().code == tonic::Code::OutOfRange as i32)
2585    }
2586
2587    #[test]
2588    fn ascending_after_cursor_past_the_tip_is_out_of_range() {
2589        let ascending = |cursor| after_options(Ordering::Ascending, cursor);
2590
2591        // The last indexed checkpoint, and the boundary a caught-up scan resumes from.
2592        assert!(resolve_checkpoints(None, &ascending(cp_item(TIP - 1))).is_ok());
2593        let caught_up = resolve_checkpoints(
2594            None,
2595            &ascending(CursorToken::boundary(Position::Checkpoints {
2596                checkpoint: TIP,
2597            })),
2598        )
2599        .unwrap();
2600        assert!(caught_up.is_empty());
2601        assert_eq!(caught_up.end_position, TIP);
2602
2603        // Positions this server has not indexed.
2604        assert!(is_out_of_range(resolve_checkpoints(
2605            None,
2606            &ascending(cp_item(TIP))
2607        )));
2608        assert!(is_out_of_range(resolve_checkpoints(
2609            None,
2610            &ascending(CursorToken::boundary(Position::Checkpoints {
2611                checkpoint: TIP + 1,
2612            })),
2613        )));
2614
2615        // A window the request ends at or below the tip is exhausted, not rejected; one that
2616        // ends above it is bounded by the tip.
2617        let past_window = resolve_checkpoints(Some(TIP), &ascending(cp_item(TIP + 5))).unwrap();
2618        assert!(past_window.is_empty());
2619        assert!(is_out_of_range(resolve_checkpoints(
2620            Some(TIP + 1),
2621            &ascending(cp_item(TIP + 5)),
2622        )));
2623    }
2624
2625    #[test]
2626    fn descending_cursors_past_the_tip_are_out_of_range() {
2627        let before = |cursor| before_options(Ordering::Descending, cursor);
2628        let after = |cursor| after_options(Ordering::Descending, cursor);
2629        let tip_boundary = || CursorToken::boundary(Position::Checkpoints { checkpoint: TIP });
2630
2631        assert!(resolve_checkpoints(None, &before(cp_item(TIP - 1))).is_ok());
2632        assert!(resolve_checkpoints(None, &before(tip_boundary())).is_ok());
2633        assert!(is_out_of_range(resolve_checkpoints(
2634            None,
2635            &before(cp_item(TIP))
2636        )));
2637
2638        // `after` bounds the far end of a descending scan. At the tip it leaves an empty page that
2639        // ends at that cursor; past it, it admits only positions this server has not indexed.
2640        let at_tip = resolve_checkpoints(None, &after(tip_boundary())).unwrap();
2641        assert!(at_tip.is_empty());
2642        assert_eq!(
2643            at_tip.exhaustion,
2644            RangeExhaustion::CursorBound {
2645                kind: sui_rpc_cursor::CursorKind::Boundary
2646            }
2647        );
2648        assert!(is_out_of_range(resolve_checkpoints(
2649            None,
2650            &after(cp_item(TIP))
2651        )));
2652        assert!(is_out_of_range(resolve_checkpoints(
2653            None,
2654            &after(cp_item(TIP + 5))
2655        )));
2656
2657        // A window the request ends at or below the tip is exhausted, not rejected.
2658        let past_window = resolve_checkpoints(Some(TIP), &after(cp_item(TIP + 5))).unwrap();
2659        assert!(past_window.is_empty());
2660    }
2661
2662    /// A transaction or event cursor inside the first unindexed checkpoint is past the tip even
2663    /// though its checkpoint equals the tip; only the start of that checkpoint is caught up.
2664    #[test]
2665    fn cursor_inside_the_first_unindexed_checkpoint_is_out_of_range() {
2666        let ascending = |cursor| after_options(Ordering::Ascending, cursor);
2667        let tip_tx = first_tx(TIP);
2668
2669        assert!(resolve_transactions(&ascending(tx_item(TIP - 1, tip_tx - 1))).is_ok());
2670        assert!(resolve_transactions(&ascending(tx_boundary(TIP, tip_tx))).is_ok());
2671        assert!(is_out_of_range(resolve_transactions(&ascending(
2672            tx_boundary(TIP, tip_tx + 2,)
2673        ))));
2674        assert!(is_out_of_range(resolve_transactions(&ascending(tx_item(
2675            TIP, tip_tx
2676        )))));
2677
2678        assert!(resolve_events(&ascending(ev_boundary(TIP, tip_tx, 0))).is_ok());
2679        assert!(is_out_of_range(resolve_events(&ascending(ev_item(
2680            TIP, tip_tx, 0
2681        )))));
2682        assert!(is_out_of_range(resolve_events(&ascending(ev_boundary(
2683            TIP, tip_tx, 1
2684        )))));
2685    }
2686
2687    // Boundary checkpoint fields read in either direction, on the ledger of the cursor-past-tip
2688    // tests (checkpoint `c` holds transactions `5c..5c + 5`, tip at checkpoint 20), where every
2689    // transaction emits events 0 and 1.
2690
2691    /// What issued a cursor, which decides how its checkpoint field relates to its position.
2692    #[derive(Clone, Copy, Debug)]
2693    enum Issuer {
2694        /// An item: the checkpoint containing the position.
2695        Item,
2696        /// A range end, a subscription boundary or a restated cursor bound: the checkpoint
2697        /// containing the position.
2698        ExactBoundary,
2699        /// An ascending scan frontier: the checkpoint of the position before it.
2700        AscendingFrontier,
2701        /// A descending scan frontier: one past the checkpoint containing the position.
2702        DescendingFrontier,
2703    }
2704
2705    const ISSUERS: [Issuer; 4] = [
2706        Issuer::Item,
2707        Issuer::ExactBoundary,
2708        Issuer::AscendingFrontier,
2709        Issuer::DescendingFrontier,
2710    ];
2711
2712    /// A request's `start_checkpoint` and `end_checkpoint`.
2713    type Window = (Option<u64>, Option<u64>);
2714
2715    /// Checkpoint windows: open, inside the ledger, open at one end, and one checkpoint wide.
2716    const WINDOWS: [Window; 5] = [
2717        (None, None),
2718        (Some(4), Some(13)),
2719        (Some(7), None),
2720        (None, Some(7)),
2721        (Some(5), Some(6)),
2722    ];
2723
2724    /// Checkpoint windows reaching past the tip: an `end_checkpoint` above it, and a
2725    /// `start_checkpoint` at it with `end_checkpoint` unset. A descending request over either is
2726    /// out of range.
2727    const PAST_TIP_WINDOWS: [Window; 2] = [(Some(15), Some(TIP + 1)), (Some(TIP), None)];
2728
2729    const ORDERINGS: [Ordering; 2] = [Ordering::Ascending, Ordering::Descending];
2730
2731    fn containing_checkpoint(tx: u64) -> u64 {
2732        tx / 5
2733    }
2734
2735    /// The transaction cursor `issuer` mints at `tx`, where it can mint one.
2736    fn issued_tx_cursor(issuer: Issuer, tx: u64) -> Option<CursorToken> {
2737        let indexed = tx < first_tx(TIP);
2738        let checkpoint = containing_checkpoint(tx);
2739        match issuer {
2740            Issuer::Item => indexed.then(|| tx_item(checkpoint, tx)),
2741            Issuer::ExactBoundary => Some(tx_boundary(checkpoint, tx)),
2742            Issuer::AscendingFrontier => Some(tx_boundary(
2743                tx.checked_sub(1).map_or(0, containing_checkpoint),
2744                tx,
2745            )),
2746            Issuer::DescendingFrontier => indexed.then(|| tx_boundary(checkpoint + 1, tx)),
2747        }
2748    }
2749
2750    /// The event cursor `issuer` mints at `(tx, index)`, where it can mint one.
2751    fn issued_event_cursor(issuer: Issuer, tx: u64, index: u32) -> Option<CursorToken> {
2752        let indexed = tx < first_tx(TIP);
2753        let checkpoint = containing_checkpoint(tx);
2754        let slot_before = if index > 0 {
2755            Some(tx)
2756        } else {
2757            tx.checked_sub(1)
2758        };
2759        match issuer {
2760            Issuer::Item => (indexed && index < 2).then(|| ev_item(checkpoint, tx, index)),
2761            Issuer::ExactBoundary => {
2762                (indexed || index == 0).then(|| ev_boundary(checkpoint, tx, index))
2763            }
2764            Issuer::AscendingFrontier => (indexed || index == 0)
2765                .then(|| ev_boundary(slot_before.map_or(0, containing_checkpoint), tx, index)),
2766            Issuer::DescendingFrontier => indexed.then(|| ev_boundary(checkpoint + 1, tx, index)),
2767        }
2768    }
2769
2770    fn every_tx_cursor(txs: impl Iterator<Item = u64> + Clone) -> Vec<CursorToken> {
2771        ISSUERS
2772            .iter()
2773            .flat_map(|&issuer| {
2774                txs.clone()
2775                    .filter_map(move |tx| issued_tx_cursor(issuer, tx))
2776            })
2777            .collect()
2778    }
2779
2780    fn every_event_cursor(txs: impl Iterator<Item = u64> + Clone) -> Vec<CursorToken> {
2781        ISSUERS
2782            .iter()
2783            .flat_map(|&issuer| {
2784                txs.clone().flat_map(move |tx| {
2785                    (0..3).filter_map(move |index| issued_event_cursor(issuer, tx, index))
2786                })
2787            })
2788            .collect()
2789    }
2790
2791    /// Transactions on both sides of checkpoint edges, the window edges, genesis and the tip.
2792    fn edge_txs() -> impl Iterator<Item = u64> + Clone {
2793        [
2794            0, 1, 4, 5, 6, 19, 20, 21, 24, 25, 26, 29, 30, 34, 35, 36, 64, 65, 66, 94, 95, 96, 99,
2795            100,
2796        ]
2797        .into_iter()
2798    }
2799
2800    /// A smaller set of edge transactions for event cursor pairs, which carry three slots each.
2801    fn event_edge_txs() -> impl Iterator<Item = u64> + Clone {
2802        [0, 4, 5, 20, 24, 25, 29, 30, 34, 35, 64, 65, 95, 99, 100].into_iter()
2803    }
2804
2805    fn options_with(
2806        ordering: Ordering,
2807        after: Option<CursorToken>,
2808        before: Option<CursorToken>,
2809    ) -> QueryOptions {
2810        QueryOptions {
2811            limit_items: 100,
2812            ordering,
2813            after,
2814            before,
2815        }
2816    }
2817
2818    /// Every single-cursor request and, over `pair_cursors`, every two-cursor request, in each
2819    /// window, including those reaching past the tip, and each ordering.
2820    fn requests(
2821        cursors: &[CursorToken],
2822        pair_cursors: &[CursorToken],
2823    ) -> Vec<(Window, QueryOptions)> {
2824        let mut requests = Vec::new();
2825        for window in WINDOWS.into_iter().chain(PAST_TIP_WINDOWS) {
2826            for ordering in ORDERINGS {
2827                for cursor in cursors {
2828                    requests.push((window, options_with(ordering, Some(cursor.clone()), None)));
2829                    requests.push((window, options_with(ordering, None, Some(cursor.clone()))));
2830                }
2831                for after in pair_cursors {
2832                    for before in pair_cursors {
2833                        requests.push((
2834                            window,
2835                            options_with(ordering, Some(after.clone()), Some(before.clone())),
2836                        ));
2837                    }
2838                }
2839            }
2840        }
2841        requests
2842    }
2843
2844    fn resolve_tx_request(
2845        window: Window,
2846        options: &QueryOptions,
2847    ) -> Result<ResolvedScan<u64>, RpcError> {
2848        let cp_range = ResolvedCheckpointRange::from_request(window.0, window.1, TIP, options)?;
2849        let tx_range = first_tx(cp_range.range.start)..first_tx(cp_range.range.end);
2850        ResolvedScan::<u64>::resolve(cp_range, tx_range, options)
2851    }
2852
2853    fn resolve_event_request(
2854        window: Window,
2855        options: &QueryOptions,
2856    ) -> Result<ResolvedScan<IntraTxCoordinate>, RpcError> {
2857        let cp_range = ResolvedCheckpointRange::from_request(window.0, window.1, TIP, options)?;
2858        let tx_range = first_tx(cp_range.range.start)..first_tx(cp_range.range.end);
2859        ResolvedScan::<IntraTxCoordinate>::resolve(
2860            cp_range,
2861            IntraTxCoordinate::tx_window(tx_range),
2862            options,
2863        )
2864    }
2865
2866    fn resolve_checkpoint_request(
2867        window: Window,
2868        options: &QueryOptions,
2869    ) -> Result<ResolvedScan<u64>, RpcError> {
2870        let cp_range = ResolvedCheckpointRange::from_request(window.0, window.1, TIP, options)?;
2871        let range = cp_range.range.clone();
2872        ResolvedScan::<u64>::resolve(cp_range, range, options)
2873    }
2874
2875    fn ledger_txs() -> Vec<u64> {
2876        (0..first_tx(TIP)).collect()
2877    }
2878
2879    /// Every event slot a cursor can name: events 0 and 1 of each transaction, and the empty
2880    /// slot after them.
2881    fn ledger_event_slots() -> Vec<IntraTxCoordinate> {
2882        (0..first_tx(TIP))
2883            .flat_map(|tx_seq| (0..3).map(move |index| IntraTxCoordinate { tx_seq, index }))
2884            .collect()
2885    }
2886
2887    /// Whether the window and cursors of a request admit the position at `key` (scan order).
2888    fn admits(window: Window, options: &QueryOptions, key: (u64, u32)) -> bool {
2889        let start = first_tx(window.0.unwrap_or(0));
2890        let end = first_tx(window.1.unwrap_or(TIP).min(TIP));
2891        let in_window = start <= key.0 && key.0 < end;
2892        let after = options.after.as_ref().is_none_or(|cursor| {
2893            let cursor_key = scan_order(&cursor.position);
2894            match cursor.kind {
2895                sui_rpc_cursor::CursorKind::Item => key > cursor_key,
2896                sui_rpc_cursor::CursorKind::Boundary => key >= cursor_key,
2897            }
2898        });
2899        let before = options
2900            .before
2901            .as_ref()
2902            .is_none_or(|cursor| key < scan_order(&cursor.position));
2903        in_window && after && before
2904    }
2905
2906    /// A position's place in ascending scan order on the model ledger, in transaction space:
2907    /// checkpoint `c` sits at its first transaction.
2908    fn scan_order(position: &Position) -> (u64, u32) {
2909        match *position {
2910            Position::Checkpoints { checkpoint } => (first_tx(checkpoint), 0),
2911            Position::Transactions { tx_seq, .. } => (tx_seq, 0),
2912            Position::Events {
2913                tx_seq,
2914                event_index,
2915                ..
2916            } => (tx_seq, event_index),
2917        }
2918    }
2919
2920    fn status(error: RpcError) -> (i32, String) {
2921        let status = error.into_status_proto();
2922        (status.code, status.message)
2923    }
2924
2925    /// One request on the model ledger, over `positions`, every scan position its cursors can
2926    /// name:
2927    /// - it admits exactly the positions its window and cursors admit;
2928    /// - it errs exactly when it is descending and its window reaches past the tip, or when its
2929    ///   window reaches the tip and a cursor other than an ascending scan's `before` names a
2930    ///   position past the tip, and then with `OutOfRange`;
2931    /// - in a non-empty window, its entry checkpoint is the window's first checkpoint in scan
2932    ///   direction, whatever its cursors' checkpoint fields;
2933    /// - in a non-empty window, it ends with the window's reason and claims the window's last
2934    ///   checkpoint in scan direction exactly when no far cursor stops the scan at or before the
2935    ///   window's edge (ascending, a `before` at or below the window's end; descending, an
2936    ///   `after` at or above the window's start), and otherwise with `CursorBound`. It ends at
2937    ///   the window's edge, or past it, where a starting cursor beyond the edge resumes: the
2938    ///   first position an `after` cursor admits, or a `before` cursor's position.
2939    fn check_request<P: ScanPosition + std::fmt::Debug>(
2940        window: Window,
2941        options: &QueryOptions,
2942        resolve: impl Fn(Window, &QueryOptions) -> Result<ResolvedScan<P>, RpcError>,
2943        positions: &[P],
2944        key: impl Fn(P) -> (u64, u32),
2945    ) where
2946        CursorToken: ScanCursor<P>,
2947    {
2948        let context = format!("window {window:?}, {options:?}");
2949        let tip_checked_cursors = match options.ordering {
2950            Ordering::Ascending => [options.after.as_ref(), None],
2951            Ordering::Descending => [options.before.as_ref(), options.after.as_ref()],
2952        };
2953        let tip_key = (first_tx(TIP), 0);
2954        let window_reaches_tip = window.1.is_none_or(|end| end > TIP);
2955        let descending_window_past_tip = options.ordering == Ordering::Descending
2956            && match window.1 {
2957                Some(end) => end > TIP,
2958                None => window.0.unwrap_or(0) >= TIP,
2959            };
2960        let cursor_past_tip = window_reaches_tip
2961            && tip_checked_cursors.into_iter().flatten().any(|cursor| {
2962                let cursor_key = scan_order(&cursor.position);
2963                match cursor.kind {
2964                    sui_rpc_cursor::CursorKind::Item => cursor_key >= tip_key,
2965                    sui_rpc_cursor::CursorKind::Boundary => cursor_key > tip_key,
2966                }
2967            });
2968        let out_of_range = descending_window_past_tip || cursor_past_tip;
2969        let resolved = match resolve(window, options) {
2970            Ok(resolved) => resolved,
2971            Err(error) => {
2972                let (code, message) = status(error);
2973                assert!(out_of_range, "{code}: {message}: {context}");
2974                assert_eq!(code, tonic::Code::OutOfRange as i32, "{message}: {context}");
2975                return;
2976            }
2977        };
2978        assert!(!out_of_range, "a request past the tip resolves: {context}");
2979        let admitted: Vec<P> = positions
2980            .iter()
2981            .copied()
2982            .filter(|position| resolved.bounds.contains(*position))
2983            .collect();
2984        let expected: Vec<P> = positions
2985            .iter()
2986            .copied()
2987            .filter(|position| admits(window, options, key(*position)))
2988            .collect();
2989        assert_eq!(admitted, expected, "admitted positions: {context}");
2990
2991        let window_start = window.0.unwrap_or(0);
2992        let window_end = window.1.unwrap_or(TIP).min(TIP);
2993        if window_start >= window_end {
2994            return;
2995        }
2996        let window_lo = (first_tx(window_start), 0);
2997        let window_hi = (first_tx(window_end), 0);
2998        let cursor_key = |cursor: &CursorToken| key(cursor.coordinate());
2999        let first_admitted_key = |cursor: &CursorToken| {
3000            let position: P = cursor.coordinate();
3001            match cursor.kind {
3002                sui_rpc_cursor::CursorKind::Item => position.successor().map(&key),
3003                sui_rpc_cursor::CursorKind::Boundary => Some(key(position)),
3004            }
3005        };
3006        // Where the page ends with the window's reason, or `None` when a far cursor stops the
3007        // scan at or before the window's edge.
3008        let (entry, claim, reason, window_reason_end) = match options.ordering {
3009            Ordering::Ascending => (
3010                window_start,
3011                window_end - 1,
3012                if window_reaches_tip {
3013                    RangeExhaustion::LedgerTip
3014                } else {
3015                    RangeExhaustion::CheckpointBound
3016                },
3017                if options
3018                    .before
3019                    .as_ref()
3020                    .is_some_and(|before| cursor_key(before) <= window_hi)
3021                {
3022                    None
3023                } else {
3024                    match options.after.as_ref() {
3025                        None => Some(window_hi),
3026                        Some(after) => first_admitted_key(after).map(|first| first.max(window_hi)),
3027                    }
3028                },
3029            ),
3030            Ordering::Descending => (
3031                window_end - 1,
3032                window_start,
3033                RangeExhaustion::CheckpointBound,
3034                if options
3035                    .after
3036                    .as_ref()
3037                    .is_some_and(|after| cursor_key(after) >= window_lo)
3038                {
3039                    None
3040                } else {
3041                    Some(
3042                        options
3043                            .before
3044                            .as_ref()
3045                            .map_or(window_lo, |before| cursor_key(before).min(window_lo)),
3046                    )
3047                },
3048            ),
3049        };
3050        assert_eq!(
3051            resolved.entry_checkpoint, entry,
3052            "entry checkpoint: {context}"
3053        );
3054        match window_reason_end {
3055            Some(end) => {
3056                assert_eq!(resolved.exhaustion, reason, "end reason: {context}");
3057                assert_eq!(key(resolved.end_position), end, "end position: {context}");
3058                assert_eq!(
3059                    resolved.natural_end_claim(options),
3060                    Some(claim),
3061                    "checkpoint claim: {context}"
3062                );
3063            }
3064            None => assert!(
3065                matches!(resolved.exhaustion, RangeExhaustion::CursorBound { .. }),
3066                "end reason: {context}: {:?}",
3067                resolved.exhaustion
3068            ),
3069        }
3070    }
3071
3072    /// Regression: an ascending scan frontier sent as `before`, or a descending one sent as
3073    /// `after`, carries a checkpoint field a checkpoint away from the one its position needs,
3074    /// and reading it as written dropped the positions in between.
3075    #[test]
3076    fn opposite_direction_frontier_admits_the_positions_next_to_it() {
3077        // Ascending frontier at transaction 23 (22 is in checkpoint 4), sent as `before`.
3078        let options = before_options(Ordering::Descending, tx_boundary(4, 23));
3079        let resolved = resolve_transactions(&options).unwrap();
3080        assert_eq!(resolved.range(), 0..23);
3081
3082        // Descending frontier at transaction 23 (checkpoint 4, so the field is 5), sent as
3083        // `after`.
3084        let options = after_options(Ordering::Ascending, tx_boundary(5, 23));
3085        let resolved = resolve_transactions(&options).unwrap();
3086        assert_eq!(resolved.range(), 23..100);
3087
3088        // Ascending event frontier at the start of transaction 23, sent as `before`.
3089        let options = before_options(Ordering::Descending, ev_boundary(4, 23, 0));
3090        let resolved = resolve_events(&options).unwrap();
3091        assert_eq!(
3092            resolved.bounds.hi,
3093            Bound::Excluded(IntraTxCoordinate::start_of_tx(23))
3094        );
3095        assert_eq!(
3096            resolved.bounds.lo,
3097            Bound::Included(IntraTxCoordinate::start_of_tx(0))
3098        );
3099    }
3100
3101    #[test]
3102    fn transaction_cursors_admit_their_positions_in_either_direction() {
3103        let cursors = every_tx_cursor(0..=first_tx(TIP));
3104        let pair_cursors = every_tx_cursor(edge_txs());
3105        let positions = ledger_txs();
3106        for (window, options) in requests(&cursors, &pair_cursors) {
3107            check_request(window, &options, resolve_tx_request, &positions, |tx| {
3108                (tx, 0)
3109            });
3110        }
3111    }
3112
3113    #[test]
3114    fn event_cursors_admit_their_positions_in_either_direction() {
3115        let cursors = every_event_cursor(0..=first_tx(TIP));
3116        let pair_cursors = every_event_cursor(event_edge_txs());
3117        let positions = ledger_event_slots();
3118        for (window, options) in requests(&cursors, &pair_cursors) {
3119            check_request(
3120                window,
3121                &options,
3122                resolve_event_request,
3123                &positions,
3124                |position| (position.tx_seq, position.index),
3125            );
3126        }
3127    }
3128
3129    /// The cursor a resolved request's terminal frame carries.
3130    fn terminal_cursor<P: ScanPosition>(
3131        resolved: &ResolvedScan<P>,
3132        position_at: impl Fn(u64, P) -> Position,
3133    ) -> CursorToken {
3134        let kind = match resolved.exhaustion {
3135            RangeExhaustion::CursorBound { kind } => kind,
3136            RangeExhaustion::LedgerTip
3137            | RangeExhaustion::CheckpointBound
3138            | RangeExhaustion::ServingFloor { .. } => sui_rpc_cursor::CursorKind::Boundary,
3139        };
3140        CursorToken {
3141            kind,
3142            position: position_at(resolved.end_checkpoint, resolved.end_position),
3143        }
3144    }
3145
3146    /// A request bounded only on its far side (`after` descending, `before` ascending) ends on a
3147    /// closing cursor. Sent back in the same ordering without the far bound, it resumes with
3148    /// exactly the window's positions the far bound excluded.
3149    fn check_closing_cursor<P: ScanPosition + std::fmt::Debug>(
3150        window: Window,
3151        options: &QueryOptions,
3152        resolve: impl Fn(Window, &QueryOptions) -> Result<ResolvedScan<P>, RpcError>,
3153        positions: &[P],
3154        key: impl Fn(P) -> (u64, u32),
3155        position_at: impl Fn(u64, P) -> Position,
3156    ) {
3157        let context = format!("window {window:?}, {options:?}");
3158        let resolved = resolve(window, options).unwrap();
3159        let closing = terminal_cursor(&resolved, position_at);
3160        let resume = if options.is_ascending() {
3161            options_with(options.ordering, Some(closing.clone()), None)
3162        } else {
3163            options_with(options.ordering, None, Some(closing.clone()))
3164        };
3165        let resumed = resolve(window, &resume)
3166            .unwrap_or_else(|e| panic!("{closing:?} resumes: {:?}: {context}", status(e)));
3167        let unbounded = options_with(options.ordering, None, None);
3168        let excluded: Vec<P> = positions
3169            .iter()
3170            .copied()
3171            .filter(|position| {
3172                admits(window, &unbounded, key(*position))
3173                    && !admits(window, options, key(*position))
3174            })
3175            .collect();
3176        let admitted: Vec<P> = positions
3177            .iter()
3178            .copied()
3179            .filter(|position| resumed.bounds.contains(*position))
3180            .collect();
3181        assert_eq!(admitted, excluded, "{closing:?} resumes: {context}");
3182    }
3183
3184    /// Requests bounded only on their far side by each of `cursors`.
3185    fn far_bounded_requests(cursors: &[CursorToken]) -> Vec<(Window, QueryOptions)> {
3186        let mut requests = Vec::new();
3187        for window in WINDOWS {
3188            for cursor in cursors {
3189                requests.push((
3190                    window,
3191                    options_with(Ordering::Descending, Some(cursor.clone()), None),
3192                ));
3193                requests.push((
3194                    window,
3195                    options_with(Ordering::Ascending, None, Some(cursor.clone())),
3196                ));
3197            }
3198        }
3199        requests
3200    }
3201
3202    #[test]
3203    fn transaction_closing_cursors_resume_without_their_far_bound() {
3204        let positions = ledger_txs();
3205        for (window, options) in far_bounded_requests(&every_tx_cursor(0..=first_tx(TIP))) {
3206            check_closing_cursor(
3207                window,
3208                &options,
3209                resolve_tx_request,
3210                &positions,
3211                |tx| (tx, 0),
3212                |checkpoint, tx_seq| Position::Transactions { checkpoint, tx_seq },
3213            );
3214        }
3215    }
3216
3217    #[test]
3218    fn event_closing_cursors_resume_without_their_far_bound() {
3219        let positions = ledger_event_slots();
3220        for (window, options) in far_bounded_requests(&every_event_cursor(0..=first_tx(TIP))) {
3221            check_closing_cursor(
3222                window,
3223                &options,
3224                resolve_event_request,
3225                &positions,
3226                |position| (position.tx_seq, position.index),
3227                |checkpoint, position| Position::Events {
3228                    checkpoint,
3229                    tx_seq: position.tx_seq,
3230                    event_index: position.index,
3231                },
3232            );
3233        }
3234    }
3235
3236    /// A page its starting cursor leaves empty in a non-empty window, and that ends with the
3237    /// window's reason, closes on a cursor that resumes without the window exactly where the
3238    /// starting cursor does: it neither re-serves an `after` Item cursor's item nor falls back to
3239    /// the window's edge. (In a zero-width window the page closes at the window's edge.)
3240    fn check_empty_page_closing_cursor<P: ScanPosition + std::fmt::Debug>(
3241        window: Window,
3242        options: &QueryOptions,
3243        resolve: impl Fn(Window, &QueryOptions) -> Result<ResolvedScan<P>, RpcError>,
3244        positions: &[P],
3245        position_at: impl Fn(u64, P) -> Position,
3246    ) where
3247        CursorToken: ScanCursor<P>,
3248    {
3249        let Ok(resolved) = resolve(window, options) else {
3250            return;
3251        };
3252        if !resolved.is_empty()
3253            || matches!(resolved.exhaustion, RangeExhaustion::CursorBound { .. })
3254        {
3255            return;
3256        }
3257        let context = format!("window {window:?}, {options:?}");
3258        let closing = terminal_cursor(&resolved, position_at);
3259        let resume = if options.is_ascending() {
3260            options_with(options.ordering, Some(closing.clone()), None)
3261        } else {
3262            options_with(options.ordering, None, Some(closing.clone()))
3263        };
3264        let admitted = |options: &QueryOptions| {
3265            resolve((None, None), options).map(|resolved| {
3266                positions
3267                    .iter()
3268                    .copied()
3269                    .filter(|position| resolved.bounds.contains(*position))
3270                    .collect::<Vec<P>>()
3271            })
3272        };
3273        match (admitted(options), admitted(&resume)) {
3274            (Ok(starting), Ok(resumed)) => {
3275                assert_eq!(resumed, starting, "{closing:?} resumes: {context}")
3276            }
3277            (Err(_), Err(_)) => {}
3278            (starting, resumed) => panic!(
3279                "{closing:?} resumes as {:?}, the starting cursor as {:?}: {context}",
3280                resumed.map_err(status),
3281                starting.map_err(status),
3282            ),
3283        }
3284    }
3285
3286    /// Requests that start from each of `cursors`: `after` ascending, `before` descending.
3287    fn starting_cursor_requests(cursors: &[CursorToken]) -> Vec<(Window, QueryOptions)> {
3288        let mut requests = Vec::new();
3289        for window in WINDOWS {
3290            for cursor in cursors {
3291                requests.push((
3292                    window,
3293                    options_with(Ordering::Ascending, Some(cursor.clone()), None),
3294                ));
3295                requests.push((
3296                    window,
3297                    options_with(Ordering::Descending, None, Some(cursor.clone())),
3298                ));
3299            }
3300        }
3301        requests
3302    }
3303
3304    #[test]
3305    fn transaction_empty_pages_close_where_their_starting_cursor_resumes() {
3306        let positions = ledger_txs();
3307        for (window, options) in starting_cursor_requests(&every_tx_cursor(0..=first_tx(TIP))) {
3308            check_empty_page_closing_cursor(
3309                window,
3310                &options,
3311                resolve_tx_request,
3312                &positions,
3313                |checkpoint, tx_seq| Position::Transactions { checkpoint, tx_seq },
3314            );
3315        }
3316    }
3317
3318    #[test]
3319    fn event_empty_pages_close_where_their_starting_cursor_resumes() {
3320        let positions = ledger_event_slots();
3321        for (window, options) in starting_cursor_requests(&every_event_cursor(0..=first_tx(TIP))) {
3322            check_empty_page_closing_cursor(
3323                window,
3324                &options,
3325                resolve_event_request,
3326                &positions,
3327                |checkpoint, position| Position::Events {
3328                    checkpoint,
3329                    tx_seq: position.tx_seq,
3330                    event_index: position.index,
3331                },
3332            );
3333        }
3334    }
3335
3336    #[test]
3337    fn checkpoint_empty_pages_close_where_their_starting_cursor_resumes() {
3338        let positions: Vec<u64> = (0..TIP).collect();
3339        for (window, options) in starting_cursor_requests(&every_checkpoint_cursor()) {
3340            check_empty_page_closing_cursor(
3341                window,
3342                &options,
3343                resolve_checkpoint_request,
3344                &positions,
3345                |_, checkpoint| Position::Checkpoints { checkpoint },
3346            );
3347        }
3348    }
3349
3350    /// Regression: a descending scan bounded by `after: Item(X)` excludes X, so its closing
3351    /// cursor is the boundary at the position after X, under the item's checkpoint field. Sent
3352    /// back as `before`, it resumes with X.
3353    #[test]
3354    fn descending_after_item_closes_at_the_next_position() {
3355        // Inside a checkpoint: transaction 12 of checkpoint 2 closes at the boundary at 13.
3356        let resolved =
3357            resolve_transactions(&after_options(Ordering::Descending, tx_item(2, 12))).unwrap();
3358        assert_eq!(resolved.range(), 13..100);
3359        assert_eq!((resolved.end_checkpoint, resolved.end_position), (2, 13));
3360        let resumed =
3361            resolve_transactions(&before_options(Ordering::Descending, tx_boundary(2, 13)))
3362                .unwrap();
3363        assert_eq!(resumed.range(), 0..13);
3364
3365        // Transaction 14 is the last of checkpoint 2: the next position starts checkpoint 3,
3366        // and the field still names checkpoint 2. The boundary's position, not its field, bounds
3367        // the resumed scan, so it still admits transaction 14.
3368        let resolved =
3369            resolve_transactions(&after_options(Ordering::Descending, tx_item(2, 14))).unwrap();
3370        assert_eq!(resolved.range(), 15..100);
3371        assert_eq!((resolved.end_checkpoint, resolved.end_position), (2, 15));
3372        let resumed =
3373            resolve_transactions(&before_options(Ordering::Descending, tx_boundary(2, 15)))
3374                .unwrap();
3375        assert_eq!(resumed.range(), 0..15);
3376        assert!(resumed.bounds.contains(14));
3377
3378        let resolved =
3379            resolve_events(&after_options(Ordering::Descending, ev_item(2, 14, 1))).unwrap();
3380        assert_eq!(
3381            resolved.end_position,
3382            IntraTxCoordinate {
3383                tx_seq: 14,
3384                index: 2
3385            }
3386        );
3387        let resolved = resolve_events(&after_options(
3388            Ordering::Descending,
3389            ev_item(2, 14, u32::MAX),
3390        ))
3391        .unwrap();
3392        assert_eq!(resolved.end_position, IntraTxCoordinate::start_of_tx(15));
3393
3394        // No position follows the last one, so the closing cursor restates the item.
3395        let resolved = resolved_range(10..20)
3396            .apply_cursor_bounds(&after_options(Ordering::Descending, tx_item(1, u64::MAX)));
3397        assert_eq!(resolved.end_position, u64::MAX);
3398        assert_eq!(
3399            resolved.exhaustion,
3400            RangeExhaustion::CursorBound {
3401                kind: sui_rpc_cursor::CursorKind::Item
3402            }
3403        );
3404    }
3405
3406    /// An Item and a Boundary cursor at every checkpoint up to the tip.
3407    fn every_checkpoint_cursor() -> Vec<CursorToken> {
3408        (0..=TIP)
3409            .flat_map(|checkpoint| {
3410                [
3411                    cp_item(checkpoint),
3412                    CursorToken::boundary(Position::Checkpoints { checkpoint }),
3413                ]
3414            })
3415            .collect()
3416    }
3417
3418    #[test]
3419    fn checkpoint_cursors_admit_their_positions_in_either_direction() {
3420        let cursors = every_checkpoint_cursor();
3421        let positions: Vec<u64> = (0..TIP).collect();
3422        for (window, options) in requests(&cursors, &cursors) {
3423            check_request(
3424                window,
3425                &options,
3426                resolve_checkpoint_request,
3427                &positions,
3428                |checkpoint| (first_tx(checkpoint), 0),
3429            );
3430        }
3431    }
3432
3433    /// An ascending scan whose high end is the tip, with nothing left to scan, ends at the tip
3434    /// with `LedgerTip`, however its `after` cursor names the tip. A `before` cursor at the tip
3435    /// bounds the scan instead, and a descending scan ends at its `after` cursor.
3436    #[test]
3437    fn caught_up_ascending_scan_ends_with_ledger_tip() {
3438        let tip_tx = first_tx(TIP);
3439        let cursor_bound = RangeExhaustion::CursorBound {
3440            kind: sui_rpc_cursor::CursorKind::Boundary,
3441        };
3442        // The `LedgerTip` terminal, an ascending frontier at the tip, and the last item.
3443        for cursor in [
3444            tx_boundary(TIP, tip_tx),
3445            tx_boundary(TIP - 1, tip_tx),
3446            tx_item(TIP - 1, tip_tx - 1),
3447        ] {
3448            for window in [(None, None), (Some(3), None), (Some(3), Some(TIP + 5))] {
3449                let options = after_options(Ordering::Ascending, cursor.clone());
3450                let resolved = resolve_tx_request(window, &options).unwrap();
3451                let context = format!("{cursor:?} {window:?}");
3452                assert!(resolved.is_empty(), "{context}");
3453                assert_eq!(resolved.exhaustion, RangeExhaustion::LedgerTip, "{context}");
3454                assert_eq!(
3455                    (resolved.end_checkpoint, resolved.end_position),
3456                    (TIP, tip_tx),
3457                    "{context}"
3458                );
3459                assert_eq!(
3460                    resolved.entry_checkpoint,
3461                    window.0.unwrap_or(0),
3462                    "{context}"
3463                );
3464            }
3465
3466            let before = |position| {
3467                options_with(
3468                    Ordering::Ascending,
3469                    Some(cursor.clone()),
3470                    Some(tx_boundary(TIP, position)),
3471                )
3472            };
3473            assert_eq!(
3474                resolve_transactions(&before(tip_tx + 1))
3475                    .unwrap()
3476                    .exhaustion,
3477                RangeExhaustion::LedgerTip,
3478                "a `before` past the tip leaves the tip as the high end: {cursor:?}"
3479            );
3480            assert_eq!(
3481                resolve_transactions(&before(tip_tx)).unwrap().exhaustion,
3482                cursor_bound,
3483                "a `before` at the tip bounds the scan: {cursor:?}"
3484            );
3485        }
3486
3487        let events = resolve_events(&after_options(
3488            Ordering::Ascending,
3489            ev_boundary(TIP, tip_tx, 0),
3490        ))
3491        .unwrap();
3492        assert!(events.is_empty());
3493        assert_eq!(events.exhaustion, RangeExhaustion::LedgerTip);
3494        assert_eq!(events.end_position, IntraTxCoordinate::start_of_tx(tip_tx));
3495
3496        let descending = resolve_transactions(&after_options(
3497            Ordering::Descending,
3498            tx_boundary(TIP, tip_tx),
3499        ))
3500        .unwrap();
3501        assert!(descending.is_empty());
3502        assert_eq!(descending.exhaustion, cursor_bound);
3503        assert_eq!(
3504            (descending.end_checkpoint, descending.end_position),
3505            (TIP, tip_tx)
3506        );
3507    }
3508
3509    /// A descending frontier resumed descending claims coverage from the window's end: the
3510    /// cursor vouches for every position above it, so the scan's first item claims the
3511    /// checkpoint above the item's, though the scan never read that checkpoint.
3512    #[test]
3513    fn descending_resume_claims_what_its_cursor_vouches_for() {
3514        // Frontier at transaction 92 (checkpoint 18), so the field is 19.
3515        let options = before_options(Ordering::Descending, tx_boundary(19, 92));
3516        let resolved = resolve_transactions(&options).unwrap();
3517        assert_eq!(resolved.range(), 0..92);
3518        assert_eq!(resolved.entry_checkpoint, TIP - 1);
3519        assert_eq!(
3520            crate::ledger_history::watermark::advance_covered_bound_before_checkpoint(
3521                None,
3522                18,
3523                resolved.entry_checkpoint,
3524                &options
3525            ),
3526            Some(19),
3527            "the first item, in checkpoint 18, claims checkpoint 19"
3528        );
3529    }
3530
3531    /// A cursor whose checkpoint field lies a checkpoint past its position, on the side the
3532    /// cursor bounds, still gets the positions its position bound admits, and the end reason of
3533    /// the scan over them.
3534    #[test]
3535    fn checkpoint_field_alone_never_empties_a_page() {
3536        // Descending, `before` is an ascending frontier inside the window's first checkpoint:
3537        // the scan reaches the window start.
3538        let resolved = resolve_tx_request(
3539            (Some(4), None),
3540            &before_options(Ordering::Descending, tx_boundary(4, 23)),
3541        )
3542        .unwrap();
3543        assert_eq!(resolved.range(), 20..23);
3544        assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
3545        assert_eq!((resolved.end_checkpoint, resolved.end_position), (4, 20));
3546
3547        // Descending, `after` is a descending frontier in the top checkpoint: the scan ends at
3548        // the cursor.
3549        let resolved =
3550            resolve_transactions(&after_options(Ordering::Descending, tx_boundary(TIP, 97)))
3551                .unwrap();
3552        assert_eq!(resolved.range(), 97..100);
3553        assert_eq!(
3554            resolved.exhaustion,
3555            RangeExhaustion::CursorBound {
3556                kind: sui_rpc_cursor::CursorKind::Boundary
3557            }
3558        );
3559        assert_eq!((resolved.end_checkpoint, resolved.end_position), (TIP, 97));
3560
3561        // Ascending, both cursors are descending frontiers in the top checkpoint: the scan ends
3562        // at `before`.
3563        let options = options_with(
3564            Ordering::Ascending,
3565            Some(tx_boundary(TIP, 97)),
3566            Some(tx_boundary(TIP, 99)),
3567        );
3568        let resolved = resolve_transactions(&options).unwrap();
3569        assert_eq!(resolved.range(), 97..99);
3570        assert_eq!(
3571            resolved.exhaustion,
3572            RangeExhaustion::CursorBound {
3573                kind: sui_rpc_cursor::CursorKind::Boundary
3574            }
3575        );
3576        assert_eq!((resolved.end_checkpoint, resolved.end_position), (TIP, 99));
3577
3578        // Ascending, `after` is a descending frontier in the top checkpoint: the scan reaches
3579        // the tip.
3580        let resolved =
3581            resolve_transactions(&after_options(Ordering::Ascending, tx_boundary(TIP, 97)))
3582                .unwrap();
3583        assert_eq!(resolved.range(), 97..100);
3584        assert_eq!(resolved.exhaustion, RangeExhaustion::LedgerTip);
3585
3586        // Ascending, `after` is a descending frontier in the window's last checkpoint: the scan
3587        // reaches the window end.
3588        let resolved = resolve_tx_request(
3589            (None, Some(10)),
3590            &after_options(Ordering::Ascending, tx_boundary(10, 47)),
3591        )
3592        .unwrap();
3593        assert_eq!(resolved.range(), 47..50);
3594        assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
3595        assert_eq!((resolved.end_checkpoint, resolved.end_position), (10, 50));
3596    }
3597
3598    /// Cursor fields and positions at the numeric edges resolve without overflow.
3599    #[test]
3600    fn cursors_at_numeric_edges_resolve_without_overflow() {
3601        // A boundary at genesis admits every position.
3602        let resolved =
3603            resolve_transactions(&after_options(Ordering::Ascending, tx_boundary(0, 0))).unwrap();
3604        assert_eq!(resolved.range(), 0..100);
3605        // A `before` at `u64::MAX` is past every window, so it narrows nothing.
3606        let resolved = resolve_transactions(&before_options(
3607            Ordering::Ascending,
3608            tx_boundary(u64::MAX, u64::MAX),
3609        ))
3610        .unwrap();
3611        assert_eq!(resolved.range(), 0..100);
3612        // An `after` at `u64::MAX` is past the tip, and past a window that ends below the tip.
3613        let options = after_options(Ordering::Descending, tx_boundary(u64::MAX, u64::MAX));
3614        assert!(is_out_of_range(resolve_transactions(&options)));
3615        let resolved = resolve_tx_request((None, Some(10)), &options).unwrap();
3616        assert!(resolved.is_empty());
3617    }
3618
3619    /// Cursors in the decode-only legacy format resolve like current ones.
3620    #[test]
3621    fn legacy_cursors_resolve_like_current_ones() {
3622        #[derive(Clone, PartialEq, prost::Message)]
3623        struct LegacyCursorToken {
3624            #[prost(int32, tag = "1")]
3625            query_type: i32,
3626            #[prost(int32, tag = "2")]
3627            kind: i32,
3628            #[prost(uint64, optional, tag = "3")]
3629            checkpoint: Option<u64>,
3630            #[prost(uint64, optional, tag = "4")]
3631            position: Option<u64>,
3632        }
3633        // An ascending transaction frontier at transaction 23 (22 is in checkpoint 4).
3634        let legacy = prost::Message::encode_to_vec(&LegacyCursorToken {
3635            query_type: 2,
3636            kind: 2,
3637            checkpoint: Some(4),
3638            position: Some(23),
3639        });
3640        let mut proto = ProtoQueryOptions::default();
3641        proto.ordering = Some(ProtoOrdering::Descending as i32);
3642        proto.before = Some(legacy.into());
3643        let options = query_options_from_proto(Some(&proto)).unwrap();
3644        assert_eq!(options.before, Some(tx_boundary(4, 23)));
3645        assert_eq!(resolve_transactions(&options).unwrap().range(), 0..23);
3646    }
3647
3648    /// `cursor` with its checkpoint field zeroed, as a transaction or event cursor minted without
3649    /// one decodes.
3650    fn without_checkpoint_field(cursor: &CursorToken) -> CursorToken {
3651        let position = match cursor.position {
3652            Position::Transactions { tx_seq, .. } => Position::Transactions {
3653                checkpoint: 0,
3654                tx_seq,
3655            },
3656            Position::Events {
3657                tx_seq,
3658                event_index,
3659                ..
3660            } => Position::Events {
3661                checkpoint: 0,
3662                tx_seq,
3663                event_index,
3664            },
3665            Position::Checkpoints { .. } => panic!("checkpoint cursors keep their checkpoint"),
3666        };
3667        CursorToken {
3668            kind: cursor.kind,
3669            position,
3670        }
3671    }
3672
3673    /// Every request built from `cursors` (each alone as `after` or `before`, and paired with a
3674    /// sample of the others) resolves to the same scan with the cursors' checkpoint fields zeroed.
3675    /// Only the checkpoints labelling the terminal cursor may differ, and errors only in message.
3676    fn assert_resolves_without_checkpoint_fields<P: Copy + Eq + std::fmt::Debug>(
3677        cursors: &[CursorToken],
3678        resolve: impl Fn(Window, &QueryOptions) -> Result<ResolvedScan<P>, RpcError>,
3679    ) {
3680        let mut requests = Vec::new();
3681        for after in cursors {
3682            requests.push((Some(after), None));
3683            requests.push((None, Some(after)));
3684            for before in cursors.iter().step_by(7) {
3685                requests.push((Some(after), Some(before)));
3686            }
3687        }
3688        for window in WINDOWS {
3689            for ordering in [Ordering::Ascending, Ordering::Descending] {
3690                for &(after, before) in &requests {
3691                    let labelled = options_with(ordering, after.cloned(), before.cloned());
3692                    let unlabelled = options_with(
3693                        ordering,
3694                        after.map(without_checkpoint_field),
3695                        before.map(without_checkpoint_field),
3696                    );
3697                    let context = format!("window {window:?}, {labelled:?}");
3698                    match (resolve(window, &labelled), resolve(window, &unlabelled)) {
3699                        (Ok(labelled), Ok(unlabelled)) => {
3700                            assert_eq!(labelled.bounds, unlabelled.bounds, "{context}");
3701                            assert_eq!(labelled.end_position, unlabelled.end_position, "{context}");
3702                            assert_eq!(labelled.exhaustion, unlabelled.exhaustion, "{context}");
3703                            assert_eq!(
3704                                labelled.entry_checkpoint, unlabelled.entry_checkpoint,
3705                                "{context}"
3706                            );
3707                        }
3708                        (Err(labelled), Err(unlabelled)) => {
3709                            assert_eq!(status(labelled).0, status(unlabelled).0, "{context}")
3710                        }
3711                        (labelled, unlabelled) => panic!(
3712                            "{context}: resolves {:?} with its checkpoint fields, {:?} without",
3713                            labelled.map(|_| ()).map_err(status),
3714                            unlabelled.map(|_| ()).map_err(status),
3715                        ),
3716                    }
3717                }
3718            }
3719        }
3720    }
3721
3722    /// A transaction or event cursor minted without a checkpoint field decodes with checkpoint 0,
3723    /// and must resume exactly as the cursor that carried its checkpoint.
3724    #[test]
3725    fn cursors_resolve_the_same_without_their_checkpoint_field() {
3726        let txs = 0..=first_tx(TIP) + 1;
3727        assert_resolves_without_checkpoint_fields(
3728            &every_tx_cursor(txs.clone()),
3729            resolve_tx_request,
3730        );
3731        assert_resolves_without_checkpoint_fields(&every_event_cursor(txs), resolve_event_request);
3732    }
3733}