1use 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::QueryEndReason;
10use sui_rpc::proto::sui::rpc::v2::QueryOptions as ProtoQueryOptions;
11use sui_rpc_cursor::CursorToken;
12use sui_rpc_cursor::Position;
13
14use crate::ErrorReason;
15use crate::RpcError;
16use crate::proto::google::rpc::bad_request::FieldViolation;
17
18const ORDERING_ASCENDING: i32 = ProtoOrdering::Ascending as i32;
19const ORDERING_DESCENDING: i32 = ProtoOrdering::Descending as i32;
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
22pub enum Ordering {
23 Ascending,
24 Descending,
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
29pub struct EventPosition {
30 pub tx_seq: u64,
31 pub event_index: u32,
32}
33
34impl EventPosition {
35 pub fn start_of_tx(tx_seq: u64) -> Self {
38 Self {
39 tx_seq,
40 event_index: 0,
41 }
42 }
43}
44
45impl From<EventPosition> for (u64, u32) {
46 fn from(position: EventPosition) -> Self {
47 (position.tx_seq, position.event_index)
48 }
49}
50
51impl From<(u64, u32)> for EventPosition {
52 fn from((tx_seq, event_index): (u64, u32)) -> Self {
53 Self {
54 tx_seq,
55 event_index,
56 }
57 }
58}
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub enum RangeExhaustion {
66 LedgerTip,
68 CheckpointBound,
70 CursorBound { kind: sui_rpc_cursor::CursorKind },
75}
76
77impl RangeExhaustion {
78 pub fn reason(self) -> QueryEndReason {
79 match self {
80 Self::LedgerTip => QueryEndReason::LedgerTip,
81 Self::CheckpointBound => QueryEndReason::CheckpointBound,
82 Self::CursorBound { .. } => QueryEndReason::CursorBound,
83 }
84 }
85}
86
87#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct QueryOptions {
90 pub limit_items: usize,
91 pub ordering: Ordering,
92 after: Option<CursorToken>,
93 before: Option<CursorToken>,
94}
95
96#[derive(Clone, Debug, Eq, PartialEq)]
101pub struct ResolvedCheckpointRange {
102 pub range: Range<u64>,
105 pub exhaustion: RangeExhaustion,
107}
108
109#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct ResolvedRange {
127 pub range: Range<u64>,
130 pub end_checkpoint: u64,
134 pub entry_checkpoint: u64,
141 pub end_position: u64,
148 pub exhaustion: RangeExhaustion,
150}
151
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154pub struct EventScanBounds {
155 pub lo: Bound<EventPosition>,
156 pub hi: Bound<EventPosition>,
157}
158
159#[derive(Clone, Debug, Eq, PartialEq)]
164pub struct ResolvedEventRange {
165 pub bounds: EventScanBounds,
168 pub entry_checkpoint: u64,
172 pub end_checkpoint: u64,
175 pub end_position: EventPosition,
178 pub exhaustion: RangeExhaustion,
180}
181
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub struct CheckpointRange {
184 start: u64,
185 end: u64,
186 high_exhaustion: RangeExhaustion,
187 indexed_tip: u64,
188}
189
190impl QueryOptions {
191 pub fn checkpoints_from_proto(
192 request: Option<&ProtoQueryOptions>,
193 default_limit_items: u32,
194 max_limit_items: u32,
195 ) -> Result<Self, RpcError> {
196 Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
197 matches!(position, Position::Checkpoints { .. })
198 })
199 }
200
201 pub fn transactions_from_proto(
202 request: Option<&ProtoQueryOptions>,
203 default_limit_items: u32,
204 max_limit_items: u32,
205 ) -> Result<Self, RpcError> {
206 Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
207 matches!(position, Position::Transactions { .. })
208 })
209 }
210
211 pub fn events_from_proto(
212 request: Option<&ProtoQueryOptions>,
213 default_limit_items: u32,
214 max_limit_items: u32,
215 ) -> Result<Self, RpcError> {
216 Self::from_proto_with_position(request, default_limit_items, max_limit_items, |position| {
217 matches!(position, Position::Events { .. })
218 })
219 }
220
221 fn from_proto_with_position(
222 request: Option<&ProtoQueryOptions>,
223 default_limit_items: u32,
224 max_limit_items: u32,
225 position_matches: fn(&Position) -> bool,
226 ) -> Result<Self, RpcError> {
227 let limit_items = request
228 .and_then(|options| options.limit)
229 .unwrap_or(default_limit_items)
230 .clamp(1, max_limit_items) as usize;
231
232 let ordering = match request.and_then(|options| options.ordering) {
233 None | Some(ORDERING_ASCENDING) => Ordering::Ascending,
234 Some(ORDERING_DESCENDING) => Ordering::Descending,
235 Some(_) => {
236 return Err(FieldViolation::new("options.ordering")
237 .with_description("invalid ordering")
238 .with_reason(ErrorReason::FieldInvalid)
239 .into());
240 }
241 };
242
243 let after = parse_cursor(
244 "options.after",
245 request.and_then(|options| options.after.as_ref()),
246 position_matches,
247 )?;
248 let before = parse_cursor(
249 "options.before",
250 request.and_then(|options| options.before.as_ref()),
251 position_matches,
252 )?;
253
254 Ok(Self {
255 limit_items,
256 ordering,
257 after,
258 before,
259 })
260 }
261
262 pub fn subscription() -> Self {
266 Self {
267 limit_items: usize::MAX,
268 ordering: Ordering::Ascending,
269 after: None,
270 before: None,
271 }
272 }
273
274 pub fn scan_direction(&self) -> ScanDirection {
275 match self.ordering {
276 Ordering::Ascending => ScanDirection::Ascending,
277 Ordering::Descending => ScanDirection::Descending,
278 }
279 }
280
281 pub fn is_ascending(&self) -> bool {
282 matches!(self.ordering, Ordering::Ascending)
283 }
284
285 pub fn has_after_cursor(&self) -> bool {
292 self.after.is_some()
293 }
294
295 pub fn apply_cursor_bounds(&self, resolved: ResolvedRange) -> ResolvedRange {
296 if resolved.is_empty() {
297 return resolved;
298 }
299
300 let mut start = resolved.range.start;
301 let mut end = resolved.range.end;
302 let mut end_checkpoint = resolved.end_checkpoint;
303 let mut end_position = resolved.end_position;
304 let mut exhaustion = resolved.exhaustion;
305 let mut entry_checkpoint = resolved.entry_checkpoint;
306 let mut cursor_terminal = None;
307
308 if let Some(cursor) = &self.after {
309 let position = u64_cursor_position(cursor);
310 if matches!(self.ordering, Ordering::Ascending) {
311 entry_checkpoint = entry_checkpoint.max(cursor.position.checkpoint());
312 }
313 let Some(after) = (match cursor.kind {
314 sui_rpc_cursor::CursorKind::Item => position.checked_add(1),
315 sui_rpc_cursor::CursorKind::Boundary => Some(position),
316 }) else {
317 return ResolvedRange {
323 entry_checkpoint,
324 ..ResolvedRange::empty_at(
325 cursor.position.checkpoint(),
326 position,
327 RangeExhaustion::CursorBound {
328 kind: sui_rpc_cursor::CursorKind::Boundary,
329 },
330 )
331 };
332 };
333 if after >= start {
334 start = after;
335 if matches!(self.ordering, Ordering::Descending) || after >= end {
336 cursor_terminal = Some((cursor.position.checkpoint(), after));
337 }
338 if matches!(self.ordering, Ordering::Descending) {
339 end_checkpoint = cursor.position.checkpoint();
340 end_position = after;
341 exhaustion = RangeExhaustion::CursorBound {
342 kind: sui_rpc_cursor::CursorKind::Boundary,
343 };
344 }
345 }
346 }
347
348 if let Some(cursor) = &self.before {
349 let position = u64_cursor_position(cursor);
350 if matches!(self.ordering, Ordering::Descending) {
351 entry_checkpoint = entry_checkpoint.min(cursor.position.checkpoint());
352 }
353 if position <= end {
354 end = position;
355 if matches!(self.ordering, Ordering::Ascending) || position <= start {
356 cursor_terminal = Some((cursor.position.checkpoint(), position));
357 }
358 if matches!(self.ordering, Ordering::Ascending) {
359 end_checkpoint = cursor.position.checkpoint();
360 end_position = position;
361 exhaustion = RangeExhaustion::CursorBound {
362 kind: sui_rpc_cursor::CursorKind::Boundary,
363 };
364 }
365 }
366 }
367
368 if start >= end {
369 if let Some((checkpoint, position)) = cursor_terminal {
370 end_checkpoint = checkpoint;
371 end_position = position;
372 }
373 if self.after.is_some() || self.before.is_some() {
374 exhaustion = RangeExhaustion::CursorBound {
375 kind: sui_rpc_cursor::CursorKind::Boundary,
376 };
377 }
378 ResolvedRange {
379 range: end_position..end_position,
380 end_checkpoint,
381 end_position,
382 exhaustion,
383 entry_checkpoint,
384 }
385 } else {
386 ResolvedRange {
387 range: start..end,
388 end_checkpoint,
389 end_position,
390 exhaustion,
391 entry_checkpoint,
392 }
393 }
394 }
395
396 pub fn apply_event_cursor_bounds(&self, resolved: ResolvedEventRange) -> ResolvedEventRange {
397 if resolved.is_empty() {
398 return resolved;
399 }
400
401 let mut bounds = resolved.bounds;
402 let mut end_checkpoint = resolved.end_checkpoint;
403 let mut end_position = resolved.end_position;
404 let mut exhaustion = resolved.exhaustion;
405 let mut entry_checkpoint = resolved.entry_checkpoint;
406 let mut cursor_terminal = None;
407
408 if let Some(cursor) = &self.after {
409 let position = event_cursor_position(cursor);
410 if matches!(self.ordering, Ordering::Ascending) {
411 entry_checkpoint = entry_checkpoint.max(cursor.position.checkpoint());
412 }
413 let candidate = match cursor.kind {
414 sui_rpc_cursor::CursorKind::Item => Bound::Excluded(position),
415 sui_rpc_cursor::CursorKind::Boundary => Bound::Included(position),
416 };
417 if lower_bound_gte(candidate, bounds.lo) {
418 let candidate_bounds = EventScanBounds {
419 lo: candidate,
420 hi: bounds.hi,
421 };
422 bounds.lo = candidate;
423 if matches!(self.ordering, Ordering::Descending) || candidate_bounds.is_empty() {
424 let kind = if matches!(self.ordering, Ordering::Ascending) {
425 cursor.kind
426 } else {
427 sui_rpc_cursor::CursorKind::Boundary
428 };
429 cursor_terminal = Some((cursor.position.checkpoint(), position, kind));
430 }
431 if matches!(self.ordering, Ordering::Descending) {
432 end_checkpoint = cursor.position.checkpoint();
433 end_position = position;
434 exhaustion = RangeExhaustion::CursorBound {
435 kind: sui_rpc_cursor::CursorKind::Boundary,
436 };
437 }
438 }
439 }
440
441 if let Some(cursor) = &self.before {
442 let position = event_cursor_position(cursor);
443 if matches!(self.ordering, Ordering::Descending) {
444 entry_checkpoint = entry_checkpoint.min(cursor.position.checkpoint());
445 }
446 if hi_admits_upper_bound(bounds.hi, position) {
447 let candidate = Bound::Excluded(position);
448 let candidate_bounds = EventScanBounds {
449 lo: bounds.lo,
450 hi: candidate,
451 };
452 bounds.hi = candidate;
453 if matches!(self.ordering, Ordering::Ascending) || candidate_bounds.is_empty() {
454 cursor_terminal = Some((
455 cursor.position.checkpoint(),
456 position,
457 sui_rpc_cursor::CursorKind::Boundary,
458 ));
459 }
460 if matches!(self.ordering, Ordering::Ascending) {
461 end_checkpoint = cursor.position.checkpoint();
462 end_position = position;
463 exhaustion = RangeExhaustion::CursorBound {
464 kind: sui_rpc_cursor::CursorKind::Boundary,
465 };
466 }
467 }
468 }
469
470 if bounds.is_empty() {
478 if let Some((checkpoint, position, kind)) = cursor_terminal {
479 end_checkpoint = checkpoint;
480 end_position = position;
481 exhaustion = RangeExhaustion::CursorBound { kind };
482 } else if self.after.is_some() || self.before.is_some() {
483 exhaustion = RangeExhaustion::CursorBound {
484 kind: sui_rpc_cursor::CursorKind::Boundary,
485 };
486 }
487 ResolvedEventRange {
488 bounds: EventScanBounds::empty_at(end_position),
489 end_checkpoint,
490 end_position,
491 exhaustion,
492 entry_checkpoint,
493 }
494 } else {
495 ResolvedEventRange {
496 bounds,
497 end_checkpoint,
498 end_position,
499 exhaustion,
500 entry_checkpoint,
501 }
502 }
503 }
504}
505
506fn u64_cursor_position(cursor: &CursorToken) -> u64 {
507 match cursor.position {
508 Position::Checkpoints { checkpoint } => checkpoint,
509 Position::Transactions { tx_seq, .. } => tx_seq,
510 Position::Events { .. } => panic!("event queries must use apply_event_cursor_bounds"),
511 }
512}
513
514fn event_cursor_position(cursor: &CursorToken) -> EventPosition {
515 match cursor.position {
516 Position::Events {
517 tx_seq,
518 event_index,
519 ..
520 } => EventPosition {
521 tx_seq,
522 event_index,
523 },
524 _ => unreachable!("validated at decode"),
525 }
526}
527
528impl ResolvedCheckpointRange {
529 pub fn empty_at(checkpoint: u64, exhaustion: RangeExhaustion) -> Self {
530 Self {
531 range: checkpoint..checkpoint,
532 exhaustion,
533 }
534 }
535
536 pub fn is_empty(&self) -> bool {
537 self.range.is_empty()
538 }
539
540 pub fn terminal_checkpoint(&self, ordering: Ordering) -> u64 {
541 match ordering {
542 Ordering::Ascending => self.range.end,
543 Ordering::Descending => self.range.start,
544 }
545 }
546
547 pub fn with_range(self, range: Range<u64>, ordering: Ordering) -> ResolvedRange {
548 let end_position = match ordering {
549 Ordering::Ascending => range.end,
550 Ordering::Descending => range.start,
551 };
552 let entry_checkpoint = match ordering {
553 Ordering::Ascending => self.range.start,
554 Ordering::Descending => self.range.end.saturating_sub(1),
555 };
556 ResolvedRange {
557 range,
558 end_checkpoint: self.terminal_checkpoint(ordering),
559 end_position,
560 exhaustion: self.exhaustion,
561 entry_checkpoint,
562 }
563 }
564}
565
566impl ResolvedRange {
567 pub fn empty_at(end_checkpoint: u64, end_position: u64, exhaustion: RangeExhaustion) -> Self {
568 Self {
569 range: end_position..end_position,
570 end_checkpoint,
571 end_position,
572 exhaustion,
573 entry_checkpoint: end_checkpoint,
574 }
575 }
576
577 pub fn is_empty(&self) -> bool {
578 self.range.is_empty()
579 }
580
581 pub fn apply_serving_floor(
595 &mut self,
596 floor_tx: u64,
597 floor_checkpoint: u64,
598 options: &QueryOptions,
599 ) {
600 if floor_tx >= self.range.end {
601 self.range = self.end_position..self.end_position;
604 return;
605 }
606 self.range.start = floor_tx;
607 if options.is_ascending() {
608 self.entry_checkpoint = self.entry_checkpoint.max(floor_checkpoint);
609 } else {
610 self.end_checkpoint = floor_checkpoint;
611 self.end_position = floor_tx;
612 }
613 }
614}
615
616impl EventScanBounds {
617 pub fn tx_span(start_tx: u64, end_tx: u64) -> Self {
618 Self {
619 lo: Bound::Included(EventPosition::start_of_tx(start_tx)),
620 hi: Bound::Excluded(EventPosition::start_of_tx(end_tx)),
621 }
622 }
623
624 pub fn empty_at(position: EventPosition) -> Self {
625 Self {
626 lo: Bound::Included(position),
627 hi: Bound::Excluded(position),
628 }
629 }
630
631 pub fn is_empty(&self) -> bool {
632 match (self.lo, self.hi) {
633 (Bound::Included(a), Bound::Excluded(b))
634 | (Bound::Excluded(a), Bound::Excluded(b))
635 | (Bound::Excluded(a), Bound::Included(b)) => a >= b,
636 (Bound::Included(a), Bound::Included(b)) => a > b,
637 (Bound::Unbounded, _) | (_, Bound::Unbounded) => false,
638 }
639 }
640
641 pub fn contains(&self, position: EventPosition) -> bool {
642 let above_lo = match self.lo {
643 Bound::Included(lo) => position >= lo,
644 Bound::Excluded(lo) => position > lo,
645 Bound::Unbounded => true,
646 };
647 let below_hi = match self.hi {
648 Bound::Included(hi) => position <= hi,
649 Bound::Excluded(hi) => position < hi,
650 Bound::Unbounded => true,
651 };
652 above_lo && below_hi
653 }
654
655 pub fn tx_range(&self) -> Option<Range<u64>> {
660 let start_tx = match self.lo {
661 Bound::Included(position) | Bound::Excluded(position) => position.tx_seq,
662 Bound::Unbounded => 0,
663 };
664 let end_tx = match self.hi {
665 Bound::Excluded(position) if position.event_index == 0 => position.tx_seq,
666 Bound::Included(position) | Bound::Excluded(position) => {
667 position.tx_seq.saturating_add(1)
668 }
669 Bound::Unbounded => u64::MAX,
670 };
671 (start_tx < end_tx).then_some(start_tx..end_tx)
672 }
673}
674
675impl ResolvedEventRange {
676 pub fn empty_at(
677 end_checkpoint: u64,
678 end_position: EventPosition,
679 exhaustion: RangeExhaustion,
680 ) -> Self {
681 Self {
682 bounds: EventScanBounds::empty_at(end_position),
683 end_checkpoint,
684 end_position,
685 exhaustion,
686 entry_checkpoint: end_checkpoint,
687 }
688 }
689
690 pub fn is_empty(&self) -> bool {
691 self.bounds.is_empty()
692 }
693
694 pub fn apply_serving_floor(
701 &mut self,
702 floor_tx: u64,
703 floor_checkpoint: u64,
704 options: &QueryOptions,
705 ) {
706 let floored_lo = Bound::Included(EventPosition::start_of_tx(floor_tx));
707 let floored = EventScanBounds {
708 lo: floored_lo,
709 hi: self.bounds.hi,
710 };
711 if floored.is_empty() {
712 self.bounds = EventScanBounds::empty_at(self.end_position);
715 return;
716 }
717 self.bounds.lo = floored_lo;
718 if options.is_ascending() {
719 self.entry_checkpoint = self.entry_checkpoint.max(floor_checkpoint);
720 } else {
721 self.end_checkpoint = floor_checkpoint;
722 self.end_position = EventPosition::start_of_tx(floor_tx);
723 }
724 }
725}
726
727fn lower_bound_gte(candidate: Bound<EventPosition>, current: Bound<EventPosition>) -> bool {
728 let Some(candidate) = lower_bound_key(candidate) else {
729 return false;
730 };
731 match lower_bound_key(current) {
732 Some(current) => candidate >= current,
733 None => true,
734 }
735}
736
737fn lower_bound_key(bound: Bound<EventPosition>) -> Option<(EventPosition, u8)> {
738 match bound {
739 Bound::Included(position) => Some((position, 0)),
740 Bound::Excluded(position) => Some((position, 1)),
741 Bound::Unbounded => None,
742 }
743}
744
745fn hi_admits_upper_bound(current: Bound<EventPosition>, candidate: EventPosition) -> bool {
746 match current {
747 Bound::Included(position) | Bound::Excluded(position) => candidate <= position,
748 Bound::Unbounded => true,
749 }
750}
751
752impl CheckpointRange {
753 pub fn from_request(
754 start_checkpoint: Option<u64>,
755 end_checkpoint: Option<u64>,
756 checkpoint_hi_exclusive: u64,
757 ) -> Result<Self, RpcError> {
758 let start = start_checkpoint.unwrap_or(0);
759 if let Some(end) = end_checkpoint
760 && end < start
761 {
762 return Err(FieldViolation::new("end_checkpoint")
763 .with_description(
764 "end_checkpoint must be greater than or equal to start_checkpoint",
765 )
766 .with_reason(ErrorReason::FieldInvalid)
767 .into());
768 }
769
770 let requested_end = end_checkpoint.unwrap_or(checkpoint_hi_exclusive);
771 let high_exhaustion = if end_checkpoint.is_none() || requested_end > checkpoint_hi_exclusive
772 {
773 RangeExhaustion::LedgerTip
774 } else {
775 RangeExhaustion::CheckpointBound
776 };
777 let end = requested_end.min(checkpoint_hi_exclusive);
778
779 Ok(Self {
780 start,
781 end,
782 high_exhaustion,
783 indexed_tip: checkpoint_hi_exclusive,
784 })
785 }
786
787 pub fn resolve(self, options: &QueryOptions) -> ResolvedCheckpointRange {
788 let mut start = self.start;
789 let mut end = self.end;
790 let mut low_exhaustion = RangeExhaustion::CheckpointBound;
791 let mut high_exhaustion = self.high_exhaustion;
792 let mut cursor_bound = false;
793
794 if let Some(cursor) = &options.after
795 && cursor.position.checkpoint() >= start
796 {
797 start = cursor.position.checkpoint();
798 cursor_bound = true;
799 if matches!(options.ordering, Ordering::Descending) {
800 low_exhaustion = RangeExhaustion::CursorBound {
801 kind: sui_rpc_cursor::CursorKind::Boundary,
802 };
803 }
804 }
805
806 if let Some(cursor) = &options.before
807 && let Some(upper) = match cursor.kind {
808 sui_rpc_cursor::CursorKind::Item => cursor.position.checkpoint().checked_add(1),
809 sui_rpc_cursor::CursorKind::Boundary => Some(cursor.position.checkpoint()),
810 }
811 && upper <= end
812 {
813 end = upper;
814 cursor_bound = true;
815 if matches!(options.ordering, Ordering::Ascending) {
816 high_exhaustion = RangeExhaustion::CursorBound {
817 kind: sui_rpc_cursor::CursorKind::Boundary,
818 };
819 }
820 }
821
822 if start >= self.indexed_tip {
823 return ResolvedCheckpointRange::empty_at(self.indexed_tip, RangeExhaustion::LedgerTip);
824 }
825
826 if start >= end {
827 let exhaustion = if cursor_bound {
828 RangeExhaustion::CursorBound {
829 kind: sui_rpc_cursor::CursorKind::Boundary,
830 }
831 } else {
832 match options.ordering {
833 Ordering::Ascending => high_exhaustion,
834 Ordering::Descending => low_exhaustion,
835 }
836 };
837 let checkpoint = match options.ordering {
838 Ordering::Ascending => end,
839 Ordering::Descending => start,
840 };
841 return ResolvedCheckpointRange::empty_at(checkpoint, exhaustion);
842 }
843
844 let exhaustion = match options.ordering {
845 Ordering::Ascending => high_exhaustion,
846 Ordering::Descending => low_exhaustion,
847 };
848 ResolvedCheckpointRange {
849 range: start..end,
850 exhaustion,
851 }
852 }
853}
854
855fn parse_cursor(
856 field: &'static str,
857 cursor: Option<&Bytes>,
858 position_matches: fn(&Position) -> bool,
859) -> Result<Option<CursorToken>, RpcError> {
860 cursor
861 .map(|cursor| {
862 CursorToken::decode(cursor).map_err(|_| invalid_cursor(field, "invalid cursor"))
863 })
864 .transpose()?
865 .map(|token| {
866 if position_matches(&token.position) {
867 Ok(token)
868 } else {
869 Err(invalid_cursor(field, "invalid cursor"))
870 }
871 })
872 .transpose()
873}
874
875fn invalid_cursor(field: &'static str, description: impl Into<String>) -> RpcError {
876 FieldViolation::new(field)
877 .with_description(description)
878 .with_reason(ErrorReason::FieldInvalid)
879 .into()
880}
881
882#[cfg(test)]
883mod tests {
884 use super::*;
885
886 fn query_options_from_proto(
887 request: Option<&ProtoQueryOptions>,
888 ) -> Result<QueryOptions, RpcError> {
889 QueryOptions::transactions_from_proto(request, 100, 1_000)
890 }
891
892 fn resolved_range(range: Range<u64>) -> ResolvedRange {
893 ResolvedRange {
894 range,
895 end_checkpoint: 20,
896 end_position: 20,
897 exhaustion: RangeExhaustion::CheckpointBound,
898 entry_checkpoint: 0,
899 }
900 }
901
902 fn tx_item(checkpoint: u64, tx_seq: u64) -> CursorToken {
903 CursorToken::item(Position::Transactions { checkpoint, tx_seq })
904 }
905
906 fn tx_boundary(checkpoint: u64, tx_seq: u64) -> CursorToken {
907 CursorToken::boundary(Position::Transactions { checkpoint, tx_seq })
908 }
909
910 fn cp_item(checkpoint: u64) -> CursorToken {
911 CursorToken::item(Position::Checkpoints { checkpoint })
912 }
913
914 fn directional_options(ascending: bool) -> QueryOptions {
915 let mut request = ProtoQueryOptions::default();
916 if !ascending {
917 request.ordering = Some(ProtoOrdering::Descending as i32);
918 }
919 query_options_from_proto(Some(&request)).unwrap()
920 }
921
922 #[test]
928 fn tx_serving_floor_reconciles_or_canonicalizes_empty() {
929 let asc = directional_options(true);
930 let desc = directional_options(false);
931
932 let mut resolved = ResolvedRange {
935 range: 0..100,
936 end_checkpoint: 20,
937 end_position: 100,
938 exhaustion: RangeExhaustion::CheckpointBound,
939 entry_checkpoint: 0,
940 };
941 resolved.apply_serving_floor(50, 10, &asc);
942 assert_eq!(resolved.range, 50..100);
943 assert_eq!(resolved.entry_checkpoint, 10);
944 assert_eq!(resolved.end_checkpoint, 20);
945 assert_eq!(resolved.end_position, 100);
946
947 let mut resolved = ResolvedRange {
950 range: 0..100,
951 end_checkpoint: 0,
952 end_position: 0,
953 exhaustion: RangeExhaustion::CheckpointBound,
954 entry_checkpoint: 20,
955 };
956 resolved.apply_serving_floor(50, 10, &desc);
957 assert_eq!(resolved.range, 50..100);
958 assert_eq!(resolved.entry_checkpoint, 20);
959 assert_eq!(resolved.end_checkpoint, 10);
960 assert_eq!(resolved.end_position, 50);
961
962 for floor_tx in [40, 50] {
968 let mut resolved = ResolvedRange {
969 range: 0..40,
970 end_checkpoint: 8,
971 end_position: 40,
972 exhaustion: RangeExhaustion::CheckpointBound,
973 entry_checkpoint: 0,
974 };
975 resolved.apply_serving_floor(floor_tx, 10, &asc);
976 assert!(resolved.is_empty());
977 assert_eq!(resolved.range, 40..40);
978 assert_eq!(resolved.entry_checkpoint, 0);
979 assert_eq!(resolved.end_checkpoint, 8);
980 assert_eq!(resolved.end_position, 40);
981
982 let mut resolved = ResolvedRange {
983 range: 0..40,
984 end_checkpoint: 0,
985 end_position: 0,
986 exhaustion: RangeExhaustion::CheckpointBound,
987 entry_checkpoint: 8,
988 };
989 resolved.apply_serving_floor(floor_tx, 10, &desc);
990 assert!(resolved.is_empty());
991 assert_eq!(resolved.range, 0..0);
992 assert_eq!(resolved.entry_checkpoint, 8);
993 assert_eq!(resolved.end_checkpoint, 0);
994 assert_eq!(resolved.end_position, 0);
995 }
996 }
997
998 #[test]
1001 fn event_serving_floor_reconciles_or_canonicalizes_empty() {
1002 let asc = directional_options(true);
1003 let desc = directional_options(false);
1004
1005 let mut resolved = ResolvedEventRange {
1008 bounds: EventScanBounds::tx_span(0, 100),
1009 end_checkpoint: 20,
1010 end_position: EventPosition::start_of_tx(100),
1011 exhaustion: RangeExhaustion::CheckpointBound,
1012 entry_checkpoint: 0,
1013 };
1014 resolved.apply_serving_floor(50, 10, &asc);
1015 assert_eq!(
1016 resolved.bounds.lo,
1017 Bound::Included(EventPosition::start_of_tx(50))
1018 );
1019 assert_eq!(resolved.entry_checkpoint, 10);
1020 assert_eq!(resolved.end_checkpoint, 20);
1021 assert_eq!(resolved.end_position, EventPosition::start_of_tx(100));
1022
1023 let mut resolved = ResolvedEventRange {
1025 bounds: EventScanBounds::tx_span(0, 100),
1026 end_checkpoint: 0,
1027 end_position: EventPosition::start_of_tx(0),
1028 exhaustion: RangeExhaustion::CheckpointBound,
1029 entry_checkpoint: 20,
1030 };
1031 resolved.apply_serving_floor(50, 10, &desc);
1032 assert_eq!(
1033 resolved.bounds.lo,
1034 Bound::Included(EventPosition::start_of_tx(50))
1035 );
1036 assert_eq!(resolved.entry_checkpoint, 20);
1037 assert_eq!(resolved.end_checkpoint, 10);
1038 assert_eq!(resolved.end_position, EventPosition::start_of_tx(50));
1039
1040 for floor_tx in [40, 50] {
1044 let mut resolved = ResolvedEventRange {
1045 bounds: EventScanBounds::tx_span(0, 40),
1046 end_checkpoint: 8,
1047 end_position: EventPosition::start_of_tx(40),
1048 exhaustion: RangeExhaustion::CheckpointBound,
1049 entry_checkpoint: 0,
1050 };
1051 resolved.apply_serving_floor(floor_tx, 10, &asc);
1052 assert!(resolved.is_empty());
1053 assert_eq!(
1054 resolved.bounds,
1055 EventScanBounds::empty_at(EventPosition::start_of_tx(40))
1056 );
1057 assert_eq!(resolved.entry_checkpoint, 0);
1058 assert_eq!(resolved.end_checkpoint, 8);
1059 assert_eq!(resolved.end_position, EventPosition::start_of_tx(40));
1060
1061 let mut resolved = ResolvedEventRange {
1062 bounds: EventScanBounds::tx_span(0, 40),
1063 end_checkpoint: 0,
1064 end_position: EventPosition::start_of_tx(0),
1065 exhaustion: RangeExhaustion::CheckpointBound,
1066 entry_checkpoint: 8,
1067 };
1068 resolved.apply_serving_floor(floor_tx, 10, &desc);
1069 assert!(resolved.is_empty());
1070 assert_eq!(
1071 resolved.bounds,
1072 EventScanBounds::empty_at(EventPosition::start_of_tx(0))
1073 );
1074 assert_eq!(resolved.entry_checkpoint, 8);
1075 assert_eq!(resolved.end_checkpoint, 0);
1076 assert_eq!(resolved.end_position, EventPosition::start_of_tx(0));
1077 }
1078 }
1079
1080 #[test]
1081 fn tx_range_covers_partial_endpoint_transactions() {
1082 let bounds = EventScanBounds {
1083 lo: Bound::Included(EventPosition {
1084 tx_seq: 10,
1085 event_index: 2,
1086 }),
1087 hi: Bound::Excluded(EventPosition::start_of_tx(13)),
1088 };
1089
1090 assert_eq!(bounds.tx_range(), Some(10..13));
1091 }
1092
1093 #[test]
1094 fn tx_range_keeps_tx_of_nonzero_exclusive_hi() {
1095 let bounds = EventScanBounds {
1096 lo: Bound::Unbounded,
1097 hi: Bound::Excluded(EventPosition {
1098 tx_seq: 13,
1099 event_index: 1,
1100 }),
1101 };
1102
1103 assert_eq!(bounds.tx_range(), Some(0..14));
1104 }
1105
1106 #[test]
1107 fn tx_range_empty_bounds_yield_none() {
1108 let bounds = EventScanBounds::tx_span(10, 10);
1109 assert_eq!(bounds.tx_range(), None);
1110 }
1111
1112 #[test]
1113 fn parses_cursors_and_ordering() {
1114 let after = tx_item(2, 20).encode();
1115 let before = tx_item(3, 30).encode();
1116 let mut request = ProtoQueryOptions::default();
1117 request.limit = Some(500);
1118 request.after = Some(after);
1119 request.before = Some(before);
1120 request.ordering = Some(ProtoOrdering::Descending as i32);
1121
1122 let options = query_options_from_proto(Some(&request)).unwrap();
1123
1124 assert_eq!(options.limit_items, 500);
1125 assert_eq!(options.ordering, Ordering::Descending);
1126 assert_eq!(options.scan_direction(), ScanDirection::Descending);
1127 assert_eq!(
1128 options.apply_cursor_bounds(resolved_range(0..100)).range,
1129 21..30
1130 );
1131 }
1132
1133 #[test]
1134 fn has_after_cursor_reflects_only_the_after_field() {
1135 let options = query_options_from_proto(Some(&ProtoQueryOptions::default())).unwrap();
1137 assert!(!options.has_after_cursor());
1138
1139 let mut request = ProtoQueryOptions::default();
1141 request.before = Some(tx_item(3, 30).encode());
1142 let options = query_options_from_proto(Some(&request)).unwrap();
1143 assert!(!options.has_after_cursor());
1144
1145 let mut request = ProtoQueryOptions::default();
1147 request.after = Some(tx_item(2, 20).encode());
1148 let options = query_options_from_proto(Some(&request)).unwrap();
1149 assert!(options.has_after_cursor());
1150 }
1151
1152 #[test]
1153 fn clamps_limit_items_and_defaults_to_ascending() {
1154 let mut request = ProtoQueryOptions::default();
1155 request.limit = Some(5_000);
1156
1157 let options = query_options_from_proto(Some(&request)).unwrap();
1158
1159 assert_eq!(options.limit_items, 1_000);
1160 assert_eq!(options.ordering, Ordering::Ascending);
1161 assert_eq!(options.scan_direction(), ScanDirection::Ascending);
1162 }
1163
1164 #[test]
1165 fn rejects_malformed_cursors_and_unknown_ordering() {
1166 let mut request = ProtoQueryOptions::default();
1167 request.after = Some(Bytes::from_static(b"short"));
1168 assert!(query_options_from_proto(Some(&request)).is_err());
1169
1170 let mut request = ProtoQueryOptions::default();
1171 request.before = Some(Bytes::from_static(b"short"));
1172 assert!(query_options_from_proto(Some(&request)).is_err());
1173
1174 let mut request = ProtoQueryOptions::default();
1175 request.ordering = Some(99);
1176 assert!(query_options_from_proto(Some(&request)).is_err());
1177 }
1178
1179 #[test]
1180 fn rejects_cursor_for_different_position_variant() {
1181 let token = cp_item(9).encode();
1182 let mut request = ProtoQueryOptions::default();
1183 request.after = Some(token);
1184 assert!(query_options_from_proto(Some(&request)).is_err());
1185 }
1186
1187 #[test]
1188 fn accepts_cursor_regardless_of_filter_scope() {
1189 let after = tx_item(1, 9).encode();
1194 let before = tx_item(3, 30).encode();
1195 let mut request = ProtoQueryOptions::default();
1196 request.after = Some(after);
1197 request.before = Some(before);
1198 assert!(query_options_from_proto(Some(&request)).is_ok());
1199 }
1200
1201 #[test]
1202 fn accepts_cursors_for_different_checkpoint_range_and_ordering() {
1203 let token = tx_item(9, 9).encode();
1204 let mut request = ProtoQueryOptions::default();
1205 request.after = Some(token);
1206 request.ordering = Some(ProtoOrdering::Descending as i32);
1207
1208 let options = query_options_from_proto(Some(&request)).unwrap();
1209 let range = CheckpointRange::from_request(Some(1_000), Some(1_100), 2_000).unwrap();
1210
1211 assert_eq!(range.resolve(&options).range, 1_000..1_100);
1212 }
1213
1214 #[test]
1215 fn applies_canonical_cursor_bounds() {
1216 let options = QueryOptions {
1217 limit_items: 2,
1218 ordering: Ordering::Ascending,
1219 after: Some(tx_item(1, 11)),
1220 before: None,
1221 };
1222 assert_eq!(
1223 options.apply_cursor_bounds(resolved_range(10..20)).range,
1224 12..20
1225 );
1226
1227 let options = QueryOptions {
1228 after: Some(tx_item(1, u64::MAX)),
1229 ..options
1230 };
1231 assert_eq!(
1232 options.apply_cursor_bounds(resolved_range(10..20)),
1233 ResolvedRange::empty_at(
1234 1,
1235 u64::MAX,
1236 RangeExhaustion::CursorBound {
1237 kind: sui_rpc_cursor::CursorKind::Boundary,
1238 },
1239 )
1240 );
1241
1242 let options = QueryOptions {
1243 ordering: Ordering::Descending,
1244 after: Some(tx_item(1, 11)),
1245 before: Some(tx_item(1, 19)),
1246 ..options
1247 };
1248 let bounded = options.apply_cursor_bounds(resolved_range(10..20));
1249 assert_eq!(bounded.range, 12..19);
1250 assert_eq!(
1251 bounded.exhaustion,
1252 RangeExhaustion::CursorBound {
1253 kind: sui_rpc_cursor::CursorKind::Boundary,
1254 }
1255 );
1256 assert_eq!(bounded.end_position, 12);
1257
1258 let options = QueryOptions {
1259 before: Some(tx_item(1, 12)),
1260 ..options
1261 };
1262 assert_eq!(
1263 options.apply_cursor_bounds(resolved_range(10..20)),
1264 ResolvedRange {
1265 entry_checkpoint: 0,
1266 ..ResolvedRange::empty_at(
1267 1,
1268 12,
1269 RangeExhaustion::CursorBound {
1270 kind: sui_rpc_cursor::CursorKind::Boundary,
1271 },
1272 )
1273 }
1274 );
1275 }
1276
1277 #[test]
1278 fn applies_boundary_cursor_bounds_without_item_offset() {
1279 let options = QueryOptions {
1280 limit_items: 2,
1281 ordering: Ordering::Ascending,
1282 after: Some(tx_boundary(2, 20)),
1283 before: None,
1284 };
1285 assert_eq!(
1286 options.apply_cursor_bounds(resolved_range(10..30)).range,
1287 20..30
1288 );
1289
1290 let options = QueryOptions {
1291 ordering: Ordering::Descending,
1292 after: None,
1293 before: Some(tx_boundary(2, 20)),
1294 ..options
1295 };
1296 assert_eq!(
1297 options.apply_cursor_bounds(resolved_range(10..30)).range,
1298 10..20
1299 );
1300 }
1301
1302 #[test]
1303 fn resolves_checkpoint_range_with_terminal_reason() {
1304 assert_eq!(
1305 CheckpointRange::from_request(None, None, 20)
1306 .unwrap()
1307 .resolve(&query_options_from_proto(None).unwrap())
1308 .exhaustion,
1309 RangeExhaustion::LedgerTip
1310 );
1311 assert!(CheckpointRange::from_request(Some(10), Some(9), 20).is_err());
1312
1313 let range = CheckpointRange::from_request(Some(10), None, 20).unwrap();
1314 let resolved = range.resolve(&query_options_from_proto(None).unwrap());
1315 assert_eq!(resolved.range, 10..20);
1316 assert_eq!(resolved.exhaustion, RangeExhaustion::LedgerTip);
1317
1318 let range = CheckpointRange::from_request(Some(30), None, 20).unwrap();
1319 assert_eq!(
1320 range.resolve(&query_options_from_proto(None).unwrap()),
1321 ResolvedCheckpointRange::empty_at(20, RangeExhaustion::LedgerTip)
1322 );
1323 }
1324
1325 #[test]
1329 fn resolves_checkpoint_range_no_longer_clamped_by_width() {
1330 let options = query_options_from_proto(None).unwrap();
1331 let range = CheckpointRange::from_request(Some(10), Some(10_000_000), 10_000_000).unwrap();
1332 let resolved = range.resolve(&options);
1333 assert_eq!(resolved.range, 10..10_000_000);
1334 assert_eq!(resolved.exhaustion, RangeExhaustion::CheckpointBound);
1335 }
1336
1337 #[test]
1338 fn event_after_item_empty_interval_retains_item_kind() {
1339 let position = Position::Events {
1340 checkpoint: 1,
1341 tx_seq: 3,
1342 event_index: 0,
1343 };
1344 let resolved = ResolvedEventRange {
1345 bounds: EventScanBounds::tx_span(0, 3),
1346 end_checkpoint: 1,
1347 end_position: EventPosition::start_of_tx(3),
1348 exhaustion: RangeExhaustion::CheckpointBound,
1349 entry_checkpoint: 0,
1350 };
1351
1352 let mut request = ProtoQueryOptions::default();
1353 request.after = Some(CursorToken::item(position).encode());
1354 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1355 let item_bounded = options.apply_event_cursor_bounds(resolved.clone());
1356
1357 assert!(item_bounded.is_empty());
1358 assert_eq!(
1359 item_bounded.end_position,
1360 EventPosition {
1361 tx_seq: 3,
1362 event_index: 0,
1363 }
1364 );
1365 assert_eq!(
1366 item_bounded.exhaustion,
1367 RangeExhaustion::CursorBound {
1368 kind: sui_rpc_cursor::CursorKind::Item,
1369 }
1370 );
1371
1372 request.after = Some(CursorToken::boundary(position).encode());
1373 let options = QueryOptions::events_from_proto(Some(&request), 100, 100).unwrap();
1374 let boundary_bounded = options.apply_event_cursor_bounds(resolved);
1375
1376 assert!(boundary_bounded.is_empty());
1377 assert_eq!(
1378 boundary_bounded.end_position,
1379 EventPosition {
1380 tx_seq: 3,
1381 event_index: 0,
1382 }
1383 );
1384 assert_eq!(
1385 boundary_bounded.exhaustion,
1386 RangeExhaustion::CursorBound {
1387 kind: sui_rpc_cursor::CursorKind::Boundary,
1388 }
1389 );
1390 }
1391
1392 #[test]
1393 fn cursor_fold_advances_entry_checkpoint() {
1394 let ascending = QueryOptions {
1395 limit_items: 100,
1396 ordering: Ordering::Ascending,
1397 after: Some(tx_item(7, 30)),
1398 before: None,
1399 };
1400 let resolved = ResolvedRange {
1401 range: 20..40,
1402 end_checkpoint: 9,
1403 end_position: 40,
1404 exhaustion: RangeExhaustion::CheckpointBound,
1405 entry_checkpoint: 5,
1406 };
1407 assert_eq!(
1408 ascending
1409 .apply_cursor_bounds(resolved.clone())
1410 .entry_checkpoint,
1411 7
1412 );
1413
1414 let descending = QueryOptions {
1415 limit_items: 100,
1416 ordering: Ordering::Descending,
1417 after: None,
1418 before: Some(tx_boundary(7, 30)),
1419 };
1420 let resolved = ResolvedRange {
1421 entry_checkpoint: 9,
1422 ..resolved
1423 };
1424 assert_eq!(descending.apply_cursor_bounds(resolved).entry_checkpoint, 7);
1425 }
1426
1427 #[test]
1428 fn item_cursor_can_be_used_as_after_or_before() {
1429 let token = CursorToken::item(Position::Transactions {
1430 checkpoint: 1,
1431 tx_seq: 11,
1432 })
1433 .encode();
1434
1435 let mut request = ProtoQueryOptions::default();
1436 request.after = Some(token.clone());
1437 let options = query_options_from_proto(Some(&request)).unwrap();
1438 assert_eq!(
1439 options.apply_cursor_bounds(resolved_range(10..20)).range,
1440 12..20
1441 );
1442
1443 request.after = None;
1444 request.before = Some(token);
1445 let options = query_options_from_proto(Some(&request)).unwrap();
1446 assert_eq!(
1447 options.apply_cursor_bounds(resolved_range(10..20)).range,
1448 10..11
1449 );
1450 }
1451}