From 18c89f6d7e87c0624fc1ea39f0d9c943cff0da53 Mon Sep 17 00:00:00 2001 From: wlmyng <127570466+wlmyng@users.noreply.github.com> Date: Thu, 22 Aug 2024 05:28:51 -0700 Subject: [PATCH] [GraphQL/TransactionBlock] Scan Limits (#18413) ## Description Implement learnings from GraphQL performance benchmarks: - Implement transaction block pagination as a two step process: First fetch the relevant transaction sequence numbers, then fetch their contents. - Every "atomic" filter on transaction blocks is served by a single `tx_` table, with two indices on it, both of which are prepped to perform index-only scans. - The primary index is used to apply the filter directly. - The secondary index applies the filter after limiting results to one sender. - Compound filters are served by joining multiple atomic filters together. - The "scan limit" concept is introduced to limit the amount of work done when dealing with compound filters (see below). ### Scan Limits - If a filter is compound, a scan limit must be provided, and controls how many transactions are considered as candidates when building a page of results. - There is an upperbound on the scan limit, currently 100M, which is enough for over a week of transactions at 100TPS. - When scan limits are enabled, pagination behaviour changes: Pages can be returned with fewer results than the page size (including no results), but still have a previous or next page, because there were no valid candidates in the area scanned but there is more room to scan on either side. - The start and end cursor for the page may no longer point to an element in the results, because they point to the first and last candidate transaction. ## Test plan ``` sui$ cargo build -p sui-indexer sui$ cargo nextest run -p sui-graphql-rpc sui$ cargo nextest run -p sui-graphql-e2e-tests --features pg_integration ``` --- ## Release notes Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required. For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates. - [ ] Protocol: - [ ] Nodes (Validators and Full nodes): - [ ] Indexer: - [ ] JSON-RPC: - [x] GraphQL: Introduce `scanLimit` for paginating `TransactionBlocks`. Queries that include multiple complex filters (filters on the function called, affected objects, recipient), need to include a scan limit which controls the number of transactions that are looked at as candidates. - [ ] CLI: - [ ] Rust SDK: --------- Co-authored-by: Ashok Menon --- .../tests/consistency/balances.exp | 24 +- .../tests/consistency/balances.move | 24 +- .../checkpoints/transaction_blocks.exp | 18 +- .../consistency/epochs/transaction_blocks.exp | 122 ++-- .../epochs/transaction_blocks.move | 8 +- .../tests/transactions/at_checkpoint.exp | 210 ++++++ .../tests/transactions/at_checkpoint.move | 63 ++ .../tests/transactions/filters/kind.exp | 246 +++++++ .../tests/transactions/filters/kind.move | 173 +++++ .../transactions/filters/transaction_ids.exp | 95 +++ .../transactions/filters/transaction_ids.move | 105 +++ .../tests/transactions/programmable.exp | 34 +- .../transactions/scan_limit/alternating.exp | 300 +++++++++ .../transactions/scan_limit/alternating.move | 235 +++++++ .../transactions/scan_limit/both_cursors.exp | 177 +++++ .../transactions/scan_limit/both_cursors.move | 133 ++++ .../transactions/scan_limit/equal/first.exp | 358 +++++++++++ .../transactions/scan_limit/equal/first.move | 281 ++++++++ .../transactions/scan_limit/equal/last.exp | 309 +++++++++ .../transactions/scan_limit/equal/last.move | 235 +++++++ .../transactions/scan_limit/ge_page/first.exp | 378 +++++++++++ .../scan_limit/ge_page/first.move | 258 ++++++++ .../transactions/scan_limit/ge_page/last.exp | 355 ++++++++++ .../transactions/scan_limit/ge_page/last.move | 251 ++++++++ .../scan_limit/invalid_limits.exp | 112 ++++ .../scan_limit/invalid_limits.move | 135 ++++ .../transactions/scan_limit/le_page/first.exp | 364 +++++++++++ .../scan_limit/le_page/first.move | 232 +++++++ .../transactions/scan_limit/le_page/last.exp | 365 +++++++++++ .../transactions/scan_limit/le_page/last.move | 227 +++++++ .../tests/transactions/scan_limit/require.exp | 606 ++++++++++++++++++ .../transactions/scan_limit/require.move | 269 ++++++++ ...h_tx_block_connection_latest_epoch.graphql | 2 +- crates/sui-graphql-rpc/schema.graphql | 237 ++++++- crates/sui-graphql-rpc/src/config.rs | 26 + crates/sui-graphql-rpc/src/connection.rs | 125 ++++ crates/sui-graphql-rpc/src/consistency.rs | 6 +- crates/sui-graphql-rpc/src/data/pg.rs | 16 +- crates/sui-graphql-rpc/src/lib.rs | 1 + crates/sui-graphql-rpc/src/raw_query.rs | 24 +- crates/sui-graphql-rpc/src/types/address.rs | 37 +- crates/sui-graphql-rpc/src/types/balance.rs | 4 +- .../sui-graphql-rpc/src/types/checkpoint.rs | 39 +- crates/sui-graphql-rpc/src/types/coin.rs | 25 +- .../src/types/coin_metadata.rs | 25 +- crates/sui-graphql-rpc/src/types/cursor.rs | 60 +- crates/sui-graphql-rpc/src/types/epoch.rs | 35 +- crates/sui-graphql-rpc/src/types/event.rs | 4 +- .../sui-graphql-rpc/src/types/move_object.rs | 25 +- .../sui-graphql-rpc/src/types/move_package.rs | 29 +- crates/sui-graphql-rpc/src/types/object.rs | 50 +- crates/sui-graphql-rpc/src/types/query.rs | 27 +- crates/sui-graphql-rpc/src/types/stake.rs | 25 +- .../src/types/suins_registration.rs | 25 +- .../src/types/transaction_block/cursor.rs | 173 +++++ .../src/types/transaction_block/filter.rs | 130 ++++ .../mod.rs} | 440 ++++++------- .../src/types/transaction_block/tx_lookups.rs | 433 +++++++++++++ .../sui-graphql-rpc/src/types/type_filter.rs | 26 - .../snapshot_tests__schema_sdl_export.snap | 237 ++++++- 60 files changed, 8523 insertions(+), 465 deletions(-) create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/at_checkpoint.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/at_checkpoint.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/filters/kind.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/filters/kind.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/filters/transaction_ids.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/filters/transaction_ids.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/alternating.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/alternating.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/both_cursors.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/both_cursors.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/first.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/first.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/last.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/last.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/first.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/first.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/last.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/last.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/invalid_limits.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/invalid_limits.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/first.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/first.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/last.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/last.move create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/require.exp create mode 100644 crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/require.move create mode 100644 crates/sui-graphql-rpc/src/connection.rs create mode 100644 crates/sui-graphql-rpc/src/types/transaction_block/cursor.rs create mode 100644 crates/sui-graphql-rpc/src/types/transaction_block/filter.rs rename crates/sui-graphql-rpc/src/types/{transaction_block.rs => transaction_block/mod.rs} (63%) create mode 100644 crates/sui-graphql-rpc/src/types/transaction_block/tx_lookups.rs diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/balances.exp b/crates/sui-graphql-e2e-tests/tests/consistency/balances.exp index 0420427fbe84c..e4529876bfe9b 100644 --- a/crates/sui-graphql-e2e-tests/tests/consistency/balances.exp +++ b/crates/sui-graphql-e2e-tests/tests/consistency/balances.exp @@ -59,7 +59,7 @@ task 15, line 76: Checkpoint created: 7 task 16, lines 78-99: -//# run-graphql --cursors {"c":2,"t":1,"tc":1} +//# run-graphql --cursors {"c":2,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -95,7 +95,7 @@ Response: { } task 17, lines 101-122: -//# run-graphql --cursors {"c":3,"t":1,"tc":1} +//# run-graphql --cursors {"c":3,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -131,7 +131,7 @@ Response: { } task 18, lines 124-145: -//# run-graphql --cursors {"c":4,"t":1,"tc":1} +//# run-graphql --cursors {"c":4,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -175,7 +175,7 @@ task 20, line 149: Checkpoint created: 8 task 21, lines 151-172: -//# run-graphql --cursors {"c":2,"t":1,"tc":1} +//# run-graphql --cursors {"c":2,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -211,7 +211,7 @@ Response: { } task 22, lines 174-195: -//# run-graphql --cursors {"c":3,"t":1,"tc":1} +//# run-graphql --cursors {"c":3,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -247,7 +247,7 @@ Response: { } task 23, lines 197-218: -//# run-graphql --cursors {"c":4,"t":1,"tc":1} +//# run-graphql --cursors {"c":4,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -291,7 +291,7 @@ task 25, line 222: Checkpoint created: 9 task 26, lines 224-245: -//# run-graphql --cursors {"c":2,"t":1,"tc":1} +//# run-graphql --cursors {"c":2,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -326,7 +326,7 @@ Response: { } task 27, lines 247-268: -//# run-graphql --cursors {"c":3,"t":1,"tc":1} +//# run-graphql --cursors {"c":3,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -362,7 +362,7 @@ Response: { } task 28, lines 270-291: -//# run-graphql --cursors {"c":4,"t":1,"tc":1} +//# run-graphql --cursors {"c":4,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -406,7 +406,7 @@ task 30, line 296: Checkpoint created: 10 task 31, lines 298-319: -//# run-graphql --cursors {"c":2,"t":1,"tc":1} +//# run-graphql --cursors {"c":2,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -441,7 +441,7 @@ Response: { } task 32, lines 321-342: -//# run-graphql --cursors {"c":3,"t":1,"tc":1} +//# run-graphql --cursors {"c":3,"t":1,"i":false} Response: { "data": { "transactionBlocks": { @@ -476,7 +476,7 @@ Response: { } task 33, lines 344-365: -//# run-graphql --cursors {"c":4,"t":1,"tc":1} +//# run-graphql --cursors {"c":4,"t":1,"i":false} Response: { "data": { "transactionBlocks": { diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/balances.move b/crates/sui-graphql-e2e-tests/tests/consistency/balances.move index 70059773d3946..48bb049a865ab 100644 --- a/crates/sui-graphql-e2e-tests/tests/consistency/balances.move +++ b/crates/sui-graphql-e2e-tests/tests/consistency/balances.move @@ -75,7 +75,7 @@ module P0::fake { //# create-checkpoint -//# run-graphql --cursors {"c":2,"t":1,"tc":1} +//# run-graphql --cursors {"c":2,"t":1,"i":false} # Emulating viewing transaction blocks at checkpoint 2. Fake coin balance should be 700. { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -98,7 +98,7 @@ module P0::fake { } } -//# run-graphql --cursors {"c":3,"t":1,"tc":1} +//# run-graphql --cursors {"c":3,"t":1,"i":false} # Emulating viewing transaction blocks at checkpoint 3. Fake coin balance should be 500. { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -121,7 +121,7 @@ module P0::fake { } } -//# run-graphql --cursors {"c":4,"t":1,"tc":1} +//# run-graphql --cursors {"c":4,"t":1,"i":false} # Emulating viewing transaction blocks at checkpoint 4. Fake coin balance should be 400. { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -148,7 +148,7 @@ module P0::fake { //# create-checkpoint -//# run-graphql --cursors {"c":2,"t":1,"tc":1} +//# run-graphql --cursors {"c":2,"t":1,"i":false} # Emulating viewing transaction blocks at checkpoint 2. Fake coin balance should be 700. { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -171,7 +171,7 @@ module P0::fake { } } -//# run-graphql --cursors {"c":3,"t":1,"tc":1} +//# run-graphql --cursors {"c":3,"t":1,"i":false} # Emulating viewing transaction blocks at checkpoint 3. Fake coin balance should be 500. { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -194,7 +194,7 @@ module P0::fake { } } -//# run-graphql --cursors {"c":4,"t":1,"tc":1} +//# run-graphql --cursors {"c":4,"t":1,"i":false} # Emulating viewing transaction blocks at checkpoint 4. Fake coin balance should be 400. { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -221,7 +221,7 @@ module P0::fake { //# create-checkpoint -//# run-graphql --cursors {"c":2,"t":1,"tc":1} +//# run-graphql --cursors {"c":2,"t":1,"i":false} # Outside available range { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -244,7 +244,7 @@ module P0::fake { } } -//# run-graphql --cursors {"c":3,"t":1,"tc":1} +//# run-graphql --cursors {"c":3,"t":1,"i":false} # Emulating viewing transaction blocks at checkpoint 3. Fake coin balance should be 500. { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -267,7 +267,7 @@ module P0::fake { } } -//# run-graphql --cursors {"c":4,"t":1,"tc":1} +//# run-graphql --cursors {"c":4,"t":1,"i":false} # Emulating viewing transaction blocks at checkpoint 4. Fake coin balance should be 400. { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -295,7 +295,7 @@ module P0::fake { //# create-checkpoint -//# run-graphql --cursors {"c":2,"t":1,"tc":1} +//# run-graphql --cursors {"c":2,"t":1,"i":false} # Outside available range { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -318,7 +318,7 @@ module P0::fake { } } -//# run-graphql --cursors {"c":3,"t":1,"tc":1} +//# run-graphql --cursors {"c":3,"t":1,"i":false} # Outside available range { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { @@ -341,7 +341,7 @@ module P0::fake { } } -//# run-graphql --cursors {"c":4,"t":1,"tc":1} +//# run-graphql --cursors {"c":4,"t":1,"i":false} # Outside available range { transactionBlocks(first: 1, after: "@{cursor_0}", filter: {signAddress: "@{A}"}) { diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/checkpoints/transaction_blocks.exp b/crates/sui-graphql-e2e-tests/tests/consistency/checkpoints/transaction_blocks.exp index a02e97976ee4a..add91acf3898d 100644 --- a/crates/sui-graphql-e2e-tests/tests/consistency/checkpoints/transaction_blocks.exp +++ b/crates/sui-graphql-e2e-tests/tests/consistency/checkpoints/transaction_blocks.exp @@ -92,7 +92,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjozLCJ0IjoyLCJ0YyI6MX0", + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", "node": { "digest": "Cwqr9jTgQjajoYaqcjzAaQGcQEyCg8XxoN7smGCLiBrs", "sender": { @@ -107,7 +107,7 @@ Response: { } }, { - "cursor": "eyJjIjozLCJ0IjozLCJ0YyI6MX0", + "cursor": "eyJjIjozLCJ0IjozLCJpIjpmYWxzZX0", "node": { "digest": "H1WU8uXMGaENQs54EpoHGpV1iMYdH8P5scd1d16s9ECB", "sender": { @@ -122,7 +122,7 @@ Response: { } }, { - "cursor": "eyJjIjozLCJ0Ijo0LCJ0YyI6MX0", + "cursor": "eyJjIjozLCJ0Ijo0LCJpIjpmYWxzZX0", "node": { "digest": "4vJbSYKwEJb5sYU2jiayqsZNRnBywD8y6sd3RQoMppF9", "sender": { @@ -137,7 +137,7 @@ Response: { } }, { - "cursor": "eyJjIjozLCJ0Ijo1LCJ0YyI6MX0", + "cursor": "eyJjIjozLCJ0Ijo1LCJpIjpmYWxzZX0", "node": { "digest": "4W23PZz7dHVxoZ2VMCWU9j38Jxy7tLkqcFBcJUB3aCSB", "sender": { @@ -159,7 +159,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjozLCJ0Ijo2LCJ0YyI6Mn0", + "cursor": "eyJjIjozLCJ0Ijo2LCJpIjpmYWxzZX0", "node": { "digest": "JLAF7P6DumC8rgzT1Ygp2QgTwpHE2FUqQbVXL6cGEEQ", "sender": { @@ -174,7 +174,7 @@ Response: { } }, { - "cursor": "eyJjIjozLCJ0Ijo3LCJ0YyI6Mn0", + "cursor": "eyJjIjozLCJ0Ijo3LCJpIjpmYWxzZX0", "node": { "digest": "BVMVdn7DDpTbCjtYwWFekcFA9sNeMgDh1wTNWRrngZxh", "sender": { @@ -189,7 +189,7 @@ Response: { } }, { - "cursor": "eyJjIjozLCJ0Ijo4LCJ0YyI6Mn0", + "cursor": "eyJjIjozLCJ0Ijo4LCJpIjpmYWxzZX0", "node": { "digest": "4J5tno4AoU4NPS2NgEseAZK7cpLDh6KJduVtbtwzmHk5", "sender": { @@ -211,7 +211,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjozLCJ0Ijo5LCJ0YyI6M30", + "cursor": "eyJjIjozLCJ0Ijo5LCJpIjpmYWxzZX0", "node": { "digest": "5BCS9sencxEJRJHBBPeGhx3rWutYoGSuLFCmnMAaYcDm", "sender": { @@ -226,7 +226,7 @@ Response: { } }, { - "cursor": "eyJjIjozLCJ0IjoxMCwidGMiOjN9", + "cursor": "eyJjIjozLCJ0IjoxMCwiaSI6ZmFsc2V9", "node": { "digest": "HQYJnLLcGf4DwgTkpqF4zHbQsLHwc1s4WbQ3Xr5BBaxh", "sender": { diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/epochs/transaction_blocks.exp b/crates/sui-graphql-e2e-tests/tests/consistency/epochs/transaction_blocks.exp index 721c1e0dafaf0..7b25ef908d6bf 100644 --- a/crates/sui-graphql-e2e-tests/tests/consistency/epochs/transaction_blocks.exp +++ b/crates/sui-graphql-e2e-tests/tests/consistency/epochs/transaction_blocks.exp @@ -39,25 +39,25 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjozLCJ0IjowLCJ0YyI6MH0", + "cursor": "eyJjIjozLCJ0IjowLCJpIjpmYWxzZX0", "node": { "digest": "J7mHXcoa7LXwyjzZUWsk8zvYZjek359TM4d2hQK4LGHo" } }, { - "cursor": "eyJjIjozLCJ0IjoxLCJ0YyI6MX0", + "cursor": "eyJjIjozLCJ0IjoxLCJpIjpmYWxzZX0", "node": { "digest": "J1pYPDrTgsKgzB8XWtW8jLJ8RPsbJcC1SQ4Mv2T1hAWt" } }, { - "cursor": "eyJjIjozLCJ0IjoyLCJ0YyI6Mn0", + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", "node": { "digest": "Cwqr9jTgQjajoYaqcjzAaQGcQEyCg8XxoN7smGCLiBrs" } }, { - "cursor": "eyJjIjozLCJ0IjozLCJ0YyI6M30", + "cursor": "eyJjIjozLCJ0IjozLCJpIjpmYWxzZX0", "node": { "digest": "Bym7b7ELP77KxVHtgj6F4FB7H6n5LYQuBQYmdvvFxEmM" } @@ -141,7 +141,7 @@ task 21, line 91: Epoch advanced: 3 task 22, lines 93-157: -//# run-graphql --cursors {"t":3,"tc":3,"c":4} {"t":7,"tc":7,"c":8} {"t":11,"tc":11,"c":12} +//# run-graphql --cursors {"t":3,"i":false,"c":4} {"t":7,"i":false,"c":8} {"t":11,"i":false,"c":12} Response: { "data": { "checkpoint": { @@ -152,25 +152,25 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjoxMiwidCI6MCwidGMiOjB9", + "cursor": "eyJjIjoxMiwidCI6MCwiaSI6ZmFsc2V9", "node": { "digest": "J7mHXcoa7LXwyjzZUWsk8zvYZjek359TM4d2hQK4LGHo" } }, { - "cursor": "eyJjIjoxMiwidCI6MSwidGMiOjF9", + "cursor": "eyJjIjoxMiwidCI6MSwiaSI6ZmFsc2V9", "node": { "digest": "J1pYPDrTgsKgzB8XWtW8jLJ8RPsbJcC1SQ4Mv2T1hAWt" } }, { - "cursor": "eyJjIjoxMiwidCI6MiwidGMiOjJ9", + "cursor": "eyJjIjoxMiwidCI6MiwiaSI6ZmFsc2V9", "node": { "digest": "Cwqr9jTgQjajoYaqcjzAaQGcQEyCg8XxoN7smGCLiBrs" } }, { - "cursor": "eyJjIjoxMiwidCI6MywidGMiOjN9", + "cursor": "eyJjIjoxMiwidCI6MywiaSI6ZmFsc2V9", "node": { "digest": "Bym7b7ELP77KxVHtgj6F4FB7H6n5LYQuBQYmdvvFxEmM" } @@ -181,19 +181,19 @@ Response: { "txs_epoch_0": { "edges": [ { - "cursor": "eyJjIjo0LCJ0IjowLCJ0YyI6MH0", + "cursor": "eyJjIjo0LCJ0IjowLCJpIjpmYWxzZX0", "node": { "digest": "J7mHXcoa7LXwyjzZUWsk8zvYZjek359TM4d2hQK4LGHo" } }, { - "cursor": "eyJjIjo0LCJ0IjoxLCJ0YyI6MX0", + "cursor": "eyJjIjo0LCJ0IjoxLCJpIjpmYWxzZX0", "node": { "digest": "J1pYPDrTgsKgzB8XWtW8jLJ8RPsbJcC1SQ4Mv2T1hAWt" } }, { - "cursor": "eyJjIjo0LCJ0IjoyLCJ0YyI6Mn0", + "cursor": "eyJjIjo0LCJ0IjoyLCJpIjpmYWxzZX0", "node": { "digest": "Cwqr9jTgQjajoYaqcjzAaQGcQEyCg8XxoN7smGCLiBrs" } @@ -205,25 +205,25 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjoxMiwidCI6NCwidGMiOjR9", + "cursor": "eyJjIjoxMiwidCI6NCwiaSI6ZmFsc2V9", "node": { "digest": "H1WU8uXMGaENQs54EpoHGpV1iMYdH8P5scd1d16s9ECB" } }, { - "cursor": "eyJjIjoxMiwidCI6NSwidGMiOjV9", + "cursor": "eyJjIjoxMiwidCI6NSwiaSI6ZmFsc2V9", "node": { "digest": "4vJbSYKwEJb5sYU2jiayqsZNRnBywD8y6sd3RQoMppF9" } }, { - "cursor": "eyJjIjoxMiwidCI6NiwidGMiOjZ9", + "cursor": "eyJjIjoxMiwidCI6NiwiaSI6ZmFsc2V9", "node": { "digest": "4W23PZz7dHVxoZ2VMCWU9j38Jxy7tLkqcFBcJUB3aCSB" } }, { - "cursor": "eyJjIjoxMiwidCI6NywidGMiOjd9", + "cursor": "eyJjIjoxMiwidCI6NywiaSI6ZmFsc2V9", "node": { "digest": "D251V1BnvyRKNFZmiFxaf7gSZLGdLo8fYbbVDb5vJWfd" } @@ -234,43 +234,43 @@ Response: { "txs_epoch_1": { "edges": [ { - "cursor": "eyJjIjo4LCJ0IjowLCJ0YyI6MH0", + "cursor": "eyJjIjo4LCJ0IjowLCJpIjpmYWxzZX0", "node": { "digest": "J7mHXcoa7LXwyjzZUWsk8zvYZjek359TM4d2hQK4LGHo" } }, { - "cursor": "eyJjIjo4LCJ0IjoxLCJ0YyI6MX0", + "cursor": "eyJjIjo4LCJ0IjoxLCJpIjpmYWxzZX0", "node": { "digest": "J1pYPDrTgsKgzB8XWtW8jLJ8RPsbJcC1SQ4Mv2T1hAWt" } }, { - "cursor": "eyJjIjo4LCJ0IjoyLCJ0YyI6Mn0", + "cursor": "eyJjIjo4LCJ0IjoyLCJpIjpmYWxzZX0", "node": { "digest": "Cwqr9jTgQjajoYaqcjzAaQGcQEyCg8XxoN7smGCLiBrs" } }, { - "cursor": "eyJjIjo4LCJ0IjozLCJ0YyI6M30", + "cursor": "eyJjIjo4LCJ0IjozLCJpIjpmYWxzZX0", "node": { "digest": "Bym7b7ELP77KxVHtgj6F4FB7H6n5LYQuBQYmdvvFxEmM" } }, { - "cursor": "eyJjIjo4LCJ0Ijo0LCJ0YyI6NH0", + "cursor": "eyJjIjo4LCJ0Ijo0LCJpIjpmYWxzZX0", "node": { "digest": "H1WU8uXMGaENQs54EpoHGpV1iMYdH8P5scd1d16s9ECB" } }, { - "cursor": "eyJjIjo4LCJ0Ijo1LCJ0YyI6NX0", + "cursor": "eyJjIjo4LCJ0Ijo1LCJpIjpmYWxzZX0", "node": { "digest": "4vJbSYKwEJb5sYU2jiayqsZNRnBywD8y6sd3RQoMppF9" } }, { - "cursor": "eyJjIjo4LCJ0Ijo2LCJ0YyI6Nn0", + "cursor": "eyJjIjo4LCJ0Ijo2LCJpIjpmYWxzZX0", "node": { "digest": "4W23PZz7dHVxoZ2VMCWU9j38Jxy7tLkqcFBcJUB3aCSB" } @@ -282,25 +282,25 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjoxMiwidCI6OCwidGMiOjh9", + "cursor": "eyJjIjoxMiwidCI6OCwiaSI6ZmFsc2V9", "node": { "digest": "JLAF7P6DumC8rgzT1Ygp2QgTwpHE2FUqQbVXL6cGEEQ" } }, { - "cursor": "eyJjIjoxMiwidCI6OSwidGMiOjl9", + "cursor": "eyJjIjoxMiwidCI6OSwiaSI6ZmFsc2V9", "node": { "digest": "BVMVdn7DDpTbCjtYwWFekcFA9sNeMgDh1wTNWRrngZxh" } }, { - "cursor": "eyJjIjoxMiwidCI6MTAsInRjIjoxMH0", + "cursor": "eyJjIjoxMiwidCI6MTAsImkiOmZhbHNlfQ", "node": { "digest": "4J5tno4AoU4NPS2NgEseAZK7cpLDh6KJduVtbtwzmHk5" } }, { - "cursor": "eyJjIjoxMiwidCI6MTEsInRjIjoxMX0", + "cursor": "eyJjIjoxMiwidCI6MTEsImkiOmZhbHNlfQ", "node": { "digest": "GngPX2ztACkKE96VUfoujZ3vA11MMDhPSwwgKhK7hVa" } @@ -311,67 +311,67 @@ Response: { "txs_epoch_2": { "edges": [ { - "cursor": "eyJjIjoxMiwidCI6MCwidGMiOjB9", + "cursor": "eyJjIjoxMiwidCI6MCwiaSI6ZmFsc2V9", "node": { "digest": "J7mHXcoa7LXwyjzZUWsk8zvYZjek359TM4d2hQK4LGHo" } }, { - "cursor": "eyJjIjoxMiwidCI6MSwidGMiOjF9", + "cursor": "eyJjIjoxMiwidCI6MSwiaSI6ZmFsc2V9", "node": { "digest": "J1pYPDrTgsKgzB8XWtW8jLJ8RPsbJcC1SQ4Mv2T1hAWt" } }, { - "cursor": "eyJjIjoxMiwidCI6MiwidGMiOjJ9", + "cursor": "eyJjIjoxMiwidCI6MiwiaSI6ZmFsc2V9", "node": { "digest": "Cwqr9jTgQjajoYaqcjzAaQGcQEyCg8XxoN7smGCLiBrs" } }, { - "cursor": "eyJjIjoxMiwidCI6MywidGMiOjN9", + "cursor": "eyJjIjoxMiwidCI6MywiaSI6ZmFsc2V9", "node": { "digest": "Bym7b7ELP77KxVHtgj6F4FB7H6n5LYQuBQYmdvvFxEmM" } }, { - "cursor": "eyJjIjoxMiwidCI6NCwidGMiOjR9", + "cursor": "eyJjIjoxMiwidCI6NCwiaSI6ZmFsc2V9", "node": { "digest": "H1WU8uXMGaENQs54EpoHGpV1iMYdH8P5scd1d16s9ECB" } }, { - "cursor": "eyJjIjoxMiwidCI6NSwidGMiOjV9", + "cursor": "eyJjIjoxMiwidCI6NSwiaSI6ZmFsc2V9", "node": { "digest": "4vJbSYKwEJb5sYU2jiayqsZNRnBywD8y6sd3RQoMppF9" } }, { - "cursor": "eyJjIjoxMiwidCI6NiwidGMiOjZ9", + "cursor": "eyJjIjoxMiwidCI6NiwiaSI6ZmFsc2V9", "node": { "digest": "4W23PZz7dHVxoZ2VMCWU9j38Jxy7tLkqcFBcJUB3aCSB" } }, { - "cursor": "eyJjIjoxMiwidCI6NywidGMiOjd9", + "cursor": "eyJjIjoxMiwidCI6NywiaSI6ZmFsc2V9", "node": { "digest": "D251V1BnvyRKNFZmiFxaf7gSZLGdLo8fYbbVDb5vJWfd" } }, { - "cursor": "eyJjIjoxMiwidCI6OCwidGMiOjh9", + "cursor": "eyJjIjoxMiwidCI6OCwiaSI6ZmFsc2V9", "node": { "digest": "JLAF7P6DumC8rgzT1Ygp2QgTwpHE2FUqQbVXL6cGEEQ" } }, { - "cursor": "eyJjIjoxMiwidCI6OSwidGMiOjl9", + "cursor": "eyJjIjoxMiwidCI6OSwiaSI6ZmFsc2V9", "node": { "digest": "BVMVdn7DDpTbCjtYwWFekcFA9sNeMgDh1wTNWRrngZxh" } }, { - "cursor": "eyJjIjoxMiwidCI6MTAsInRjIjoxMH0", + "cursor": "eyJjIjoxMiwidCI6MTAsImkiOmZhbHNlfQ", "node": { "digest": "4J5tno4AoU4NPS2NgEseAZK7cpLDh6KJduVtbtwzmHk5" } @@ -382,7 +382,7 @@ Response: { } task 23, lines 159-199: -//# run-graphql --cursors {"t":0,"tc":0,"c":7} {"t":4,"tc":4,"c":11} {"t":8,"tc":8,"c":12} +//# run-graphql --cursors {"t":0,"i":false,"c":7} {"t":4,"i":false,"c":11} {"t":8,"i":false,"c":12} Response: { "data": { "checkpoint": { @@ -393,19 +393,19 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjo3LCJ0IjoxLCJ0YyI6MX0", + "cursor": "eyJjIjo3LCJ0IjoxLCJpIjpmYWxzZX0", "node": { "digest": "J1pYPDrTgsKgzB8XWtW8jLJ8RPsbJcC1SQ4Mv2T1hAWt" } }, { - "cursor": "eyJjIjo3LCJ0IjoyLCJ0YyI6Mn0", + "cursor": "eyJjIjo3LCJ0IjoyLCJpIjpmYWxzZX0", "node": { "digest": "Cwqr9jTgQjajoYaqcjzAaQGcQEyCg8XxoN7smGCLiBrs" } }, { - "cursor": "eyJjIjo3LCJ0IjozLCJ0YyI6M30", + "cursor": "eyJjIjo3LCJ0IjozLCJpIjpmYWxzZX0", "node": { "digest": "Bym7b7ELP77KxVHtgj6F4FB7H6n5LYQuBQYmdvvFxEmM" } @@ -418,19 +418,19 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjoxMSwidCI6NSwidGMiOjV9", + "cursor": "eyJjIjoxMSwidCI6NSwiaSI6ZmFsc2V9", "node": { "digest": "4vJbSYKwEJb5sYU2jiayqsZNRnBywD8y6sd3RQoMppF9" } }, { - "cursor": "eyJjIjoxMSwidCI6NiwidGMiOjZ9", + "cursor": "eyJjIjoxMSwidCI6NiwiaSI6ZmFsc2V9", "node": { "digest": "4W23PZz7dHVxoZ2VMCWU9j38Jxy7tLkqcFBcJUB3aCSB" } }, { - "cursor": "eyJjIjoxMSwidCI6NywidGMiOjd9", + "cursor": "eyJjIjoxMSwidCI6NywiaSI6ZmFsc2V9", "node": { "digest": "D251V1BnvyRKNFZmiFxaf7gSZLGdLo8fYbbVDb5vJWfd" } @@ -443,19 +443,19 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjoxMiwidCI6OSwidGMiOjl9", + "cursor": "eyJjIjoxMiwidCI6OSwiaSI6ZmFsc2V9", "node": { "digest": "BVMVdn7DDpTbCjtYwWFekcFA9sNeMgDh1wTNWRrngZxh" } }, { - "cursor": "eyJjIjoxMiwidCI6MTAsInRjIjoxMH0", + "cursor": "eyJjIjoxMiwidCI6MTAsImkiOmZhbHNlfQ", "node": { "digest": "4J5tno4AoU4NPS2NgEseAZK7cpLDh6KJduVtbtwzmHk5" } }, { - "cursor": "eyJjIjoxMiwidCI6MTEsInRjIjoxMX0", + "cursor": "eyJjIjoxMiwidCI6MTEsImkiOmZhbHNlfQ", "node": { "digest": "GngPX2ztACkKE96VUfoujZ3vA11MMDhPSwwgKhK7hVa" } @@ -467,7 +467,7 @@ Response: { } task 24, lines 201-241: -//# run-graphql --cursors {"t":1,"tc":1,"c":2} {"t":5,"tc":5,"c":6} {"t":9,"tc":9,"c":10} +//# run-graphql --cursors {"t":1,"i":false,"c":2} {"t":5,"i":false,"c":6} {"t":9,"i":false,"c":10} Response: { "data": { "checkpoint": { @@ -478,7 +478,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjoyLCJ0IjoyLCJ0YyI6Mn0", + "cursor": "eyJjIjoyLCJ0IjoyLCJpIjpmYWxzZX0", "node": { "digest": "Cwqr9jTgQjajoYaqcjzAaQGcQEyCg8XxoN7smGCLiBrs" } @@ -491,7 +491,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjo2LCJ0Ijo2LCJ0YyI6Nn0", + "cursor": "eyJjIjo2LCJ0Ijo2LCJpIjpmYWxzZX0", "node": { "digest": "4W23PZz7dHVxoZ2VMCWU9j38Jxy7tLkqcFBcJUB3aCSB" } @@ -504,7 +504,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjoxMCwidCI6MTAsInRjIjoxMH0", + "cursor": "eyJjIjoxMCwidCI6MTAsImkiOmZhbHNlfQ", "node": { "digest": "4J5tno4AoU4NPS2NgEseAZK7cpLDh6KJduVtbtwzmHk5" } @@ -516,7 +516,7 @@ Response: { } task 25, lines 243-282: -//# run-graphql --cursors {"t":5,"tc":5,"c":6} +//# run-graphql --cursors {"t":5,"i":false,"c":6} Response: { "data": { "checkpoint": { @@ -525,7 +525,7 @@ Response: { "with_cursor": { "edges": [ { - "cursor": "eyJjIjo2LCJ0Ijo2LCJ0YyI6Nn0", + "cursor": "eyJjIjo2LCJ0Ijo2LCJpIjpmYWxzZX0", "node": { "digest": "4W23PZz7dHVxoZ2VMCWU9j38Jxy7tLkqcFBcJUB3aCSB", "sender": { @@ -556,7 +556,7 @@ Response: { "without_cursor": { "edges": [ { - "cursor": "eyJjIjoxMiwidCI6MiwidGMiOjJ9", + "cursor": "eyJjIjoxMiwidCI6MiwiaSI6ZmFsc2V9", "node": { "digest": "Cwqr9jTgQjajoYaqcjzAaQGcQEyCg8XxoN7smGCLiBrs", "sender": { @@ -592,7 +592,7 @@ Response: { } }, { - "cursor": "eyJjIjoxMiwidCI6NCwidGMiOjR9", + "cursor": "eyJjIjoxMiwidCI6NCwiaSI6ZmFsc2V9", "node": { "digest": "H1WU8uXMGaENQs54EpoHGpV1iMYdH8P5scd1d16s9ECB", "sender": { @@ -628,7 +628,7 @@ Response: { } }, { - "cursor": "eyJjIjoxMiwidCI6NSwidGMiOjV9", + "cursor": "eyJjIjoxMiwidCI6NSwiaSI6ZmFsc2V9", "node": { "digest": "4vJbSYKwEJb5sYU2jiayqsZNRnBywD8y6sd3RQoMppF9", "sender": { @@ -664,7 +664,7 @@ Response: { } }, { - "cursor": "eyJjIjoxMiwidCI6NiwidGMiOjZ9", + "cursor": "eyJjIjoxMiwidCI6NiwiaSI6ZmFsc2V9", "node": { "digest": "4W23PZz7dHVxoZ2VMCWU9j38Jxy7tLkqcFBcJUB3aCSB", "sender": { @@ -700,7 +700,7 @@ Response: { } }, { - "cursor": "eyJjIjoxMiwidCI6OCwidGMiOjh9", + "cursor": "eyJjIjoxMiwidCI6OCwiaSI6ZmFsc2V9", "node": { "digest": "JLAF7P6DumC8rgzT1Ygp2QgTwpHE2FUqQbVXL6cGEEQ", "sender": { @@ -736,7 +736,7 @@ Response: { } }, { - "cursor": "eyJjIjoxMiwidCI6OSwidGMiOjl9", + "cursor": "eyJjIjoxMiwidCI6OSwiaSI6ZmFsc2V9", "node": { "digest": "BVMVdn7DDpTbCjtYwWFekcFA9sNeMgDh1wTNWRrngZxh", "sender": { @@ -772,7 +772,7 @@ Response: { } }, { - "cursor": "eyJjIjoxMiwidCI6MTAsInRjIjoxMH0", + "cursor": "eyJjIjoxMiwidCI6MTAsImkiOmZhbHNlfQ", "node": { "digest": "4J5tno4AoU4NPS2NgEseAZK7cpLDh6KJduVtbtwzmHk5", "sender": { diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/epochs/transaction_blocks.move b/crates/sui-graphql-e2e-tests/tests/consistency/epochs/transaction_blocks.move index 39c8368818df3..425849aef9e16 100644 --- a/crates/sui-graphql-e2e-tests/tests/consistency/epochs/transaction_blocks.move +++ b/crates/sui-graphql-e2e-tests/tests/consistency/epochs/transaction_blocks.move @@ -90,7 +90,7 @@ module Test::M1 { //# advance-epoch -//# run-graphql --cursors {"t":3,"tc":3,"c":4} {"t":7,"tc":7,"c":8} {"t":11,"tc":11,"c":12} +//# run-graphql --cursors {"t":3,"i":false,"c":4} {"t":7,"i":false,"c":8} {"t":11,"i":false,"c":12} # View transactions before the last transaction in each epoch, from the perspective of the first # checkpoint in the next epoch. { @@ -156,7 +156,7 @@ module Test::M1 { } } -//# run-graphql --cursors {"t":0,"tc":0,"c":7} {"t":4,"tc":4,"c":11} {"t":8,"tc":8,"c":12} +//# run-graphql --cursors {"t":0,"i":false,"c":7} {"t":4,"i":false,"c":11} {"t":8,"i":false,"c":12} # View transactions after the first transaction in each epoch, from the perspective of the last # checkpoint in the next epoch. { @@ -198,7 +198,7 @@ module Test::M1 { } } -//# run-graphql --cursors {"t":1,"tc":1,"c":2} {"t":5,"tc":5,"c":6} {"t":9,"tc":9,"c":10} +//# run-graphql --cursors {"t":1,"i":false,"c":2} {"t":5,"i":false,"c":6} {"t":9,"i":false,"c":10} # View transactions after the second transaction in each epoch, from the perspective of a checkpoint # around the middle of each epoch. { @@ -240,7 +240,7 @@ module Test::M1 { } } -//# run-graphql --cursors {"t":5,"tc":5,"c":6} +//# run-graphql --cursors {"t":5,"i":false,"c":6} # Verify that with a cursor, we are locked into a view as if we were at the checkpoint stored in # the cursor. Compare against `without_cursor`, which should show the latest state at the actual # latest checkpoint. There should only be 1 transaction block in the `with_cursor` query, but diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/at_checkpoint.exp b/crates/sui-graphql-e2e-tests/tests/transactions/at_checkpoint.exp new file mode 100644 index 0000000000000..48209abf430e8 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/at_checkpoint.exp @@ -0,0 +1,210 @@ +processed 11 tasks + +init: +A: object(0,0) + +task 2, lines 10-12: +//# programmable --sender A --inputs 1 @A +//> 0: SplitCoins(Gas, [Input(0)]); +//> TransferObjects([Result(0)], Input(1)) +created: object(2,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 1976000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3, line 14: +//# create-checkpoint +Checkpoint created: 1 + +task 5, line 18: +//# create-checkpoint +Checkpoint created: 2 + +task 7, line 22: +//# create-checkpoint +Checkpoint created: 3 + +task 8, lines 24-36: +//# run-graphql +Response: { + "data": { + "c0": { + "nodes": [ + { + "digest": "FPhSSzT7tHmrPhs3H9GT1n4Dqj3eyCgaFLkQSc9FEDVV", + "kind": { + "__typename": "GenesisTransaction" + } + } + ] + }, + "c1": { + "nodes": [ + { + "digest": "43wY12GuxKzFAJAAW7oCcYfRGb3BSKXxgrVTtXwuELfn", + "kind": { + "__typename": "ConsensusCommitPrologueTransaction" + } + }, + { + "digest": "Cn6D9eKgVx5EeZddUSpQeTFcVyHKjqmt6yeiroKgr9h6", + "kind": { + "__typename": "ProgrammableTransactionBlock" + } + } + ] + }, + "c2": { + "nodes": [ + { + "digest": "9eMYXfB8mzZhdQgJ6HJTTdcwyXZ3EHVXDpcvERnnBWvR", + "kind": { + "__typename": "ConsensusCommitPrologueTransaction" + } + } + ] + }, + "c3": { + "nodes": [ + { + "digest": "E1TmDoToDfVSW7kMEFiYsNFL2UeCaL1wNbWLdFjxe5mx", + "kind": { + "__typename": "ConsensusCommitPrologueTransaction" + } + } + ] + }, + "c4": { + "nodes": [] + } + } +} + +task 9, lines 38-50: +//# run-graphql +Response: { + "data": { + "c0": { + "transactionBlocks": { + "nodes": [ + { + "digest": "FPhSSzT7tHmrPhs3H9GT1n4Dqj3eyCgaFLkQSc9FEDVV", + "kind": { + "__typename": "GenesisTransaction" + } + } + ] + } + }, + "c1": { + "transactionBlocks": { + "nodes": [ + { + "digest": "43wY12GuxKzFAJAAW7oCcYfRGb3BSKXxgrVTtXwuELfn", + "kind": { + "__typename": "ConsensusCommitPrologueTransaction" + } + }, + { + "digest": "Cn6D9eKgVx5EeZddUSpQeTFcVyHKjqmt6yeiroKgr9h6", + "kind": { + "__typename": "ProgrammableTransactionBlock" + } + } + ] + } + }, + "c2": { + "transactionBlocks": { + "nodes": [ + { + "digest": "9eMYXfB8mzZhdQgJ6HJTTdcwyXZ3EHVXDpcvERnnBWvR", + "kind": { + "__typename": "ConsensusCommitPrologueTransaction" + } + } + ] + } + }, + "c3": { + "transactionBlocks": { + "nodes": [ + { + "digest": "E1TmDoToDfVSW7kMEFiYsNFL2UeCaL1wNbWLdFjxe5mx", + "kind": { + "__typename": "ConsensusCommitPrologueTransaction" + } + } + ] + } + }, + "c4": null + } +} + +task 10, lines 52-63: +//# run-graphql +Response: { + "data": { + "checkpoints": { + "pageInfo": { + "hasNextPage": false + }, + "nodes": [ + { + "transactionBlocks": { + "nodes": [ + { + "digest": "FPhSSzT7tHmrPhs3H9GT1n4Dqj3eyCgaFLkQSc9FEDVV", + "kind": { + "__typename": "GenesisTransaction" + } + } + ] + } + }, + { + "transactionBlocks": { + "nodes": [ + { + "digest": "43wY12GuxKzFAJAAW7oCcYfRGb3BSKXxgrVTtXwuELfn", + "kind": { + "__typename": "ConsensusCommitPrologueTransaction" + } + }, + { + "digest": "Cn6D9eKgVx5EeZddUSpQeTFcVyHKjqmt6yeiroKgr9h6", + "kind": { + "__typename": "ProgrammableTransactionBlock" + } + } + ] + } + }, + { + "transactionBlocks": { + "nodes": [ + { + "digest": "9eMYXfB8mzZhdQgJ6HJTTdcwyXZ3EHVXDpcvERnnBWvR", + "kind": { + "__typename": "ConsensusCommitPrologueTransaction" + } + } + ] + } + }, + { + "transactionBlocks": { + "nodes": [ + { + "digest": "E1TmDoToDfVSW7kMEFiYsNFL2UeCaL1wNbWLdFjxe5mx", + "kind": { + "__typename": "ConsensusCommitPrologueTransaction" + } + } + ] + } + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/at_checkpoint.move b/crates/sui-graphql-e2e-tests/tests/transactions/at_checkpoint.move new file mode 100644 index 0000000000000..5d5e27dbed2e8 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/at_checkpoint.move @@ -0,0 +1,63 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --protocol-version 51 --accounts A --simulator + +// Limiting transactions by the checkpoint they are in + +//# advance-clock --duration-ns 1 + +//# programmable --sender A --inputs 1 @A +//> 0: SplitCoins(Gas, [Input(0)]); +//> TransferObjects([Result(0)], Input(1)) + +//# create-checkpoint + +//# advance-clock --duration-ns 1 + +//# create-checkpoint + +//# advance-clock --duration-ns 1 + +//# create-checkpoint + +//# run-graphql +{ # Top-level query, with a filter + c0: transactionBlocks(filter: { atCheckpoint: 0 }) { nodes { ...Tx } } + c1: transactionBlocks(filter: { atCheckpoint: 1 }) { nodes { ...Tx } } + c2: transactionBlocks(filter: { atCheckpoint: 2 }) { nodes { ...Tx } } + c3: transactionBlocks(filter: { atCheckpoint: 3 }) { nodes { ...Tx } } + c4: transactionBlocks(filter: { atCheckpoint: 4 }) { nodes { ...Tx } } +} + +fragment Tx on TransactionBlock { + digest + kind { __typename } +} + +//# run-graphql +{ # Via a checkpoint query + c0: checkpoint(id: { sequenceNumber: 0 }) { transactionBlocks { nodes { ...Tx } } } + c1: checkpoint(id: { sequenceNumber: 1 }) { transactionBlocks { nodes { ...Tx } } } + c2: checkpoint(id: { sequenceNumber: 2 }) { transactionBlocks { nodes { ...Tx } } } + c3: checkpoint(id: { sequenceNumber: 3 }) { transactionBlocks { nodes { ...Tx } } } + c4: checkpoint(id: { sequenceNumber: 4 }) { transactionBlocks { nodes { ...Tx } } } +} + +fragment Tx on TransactionBlock { + digest + kind { __typename } +} + +//# run-graphql +{ # Via paginating checkpoints + checkpoints(first: 5) { + pageInfo { hasNextPage } + nodes { transactionBlocks { nodes { ...Tx } } } + } +} + +fragment Tx on TransactionBlock { + digest + kind { __typename } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/filters/kind.exp b/crates/sui-graphql-e2e-tests/tests/transactions/filters/kind.exp new file mode 100644 index 0000000000000..5f9783d3a76fe --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/filters/kind.exp @@ -0,0 +1,246 @@ +processed 16 tasks + +init: +A: object(0,0), B: object(0,1), C: object(0,2), D: object(0,3), E: object(0,4) + +task 1, lines 6-19: +//# publish +created: object(1,0) +mutated: object(0,5) +gas summary: computation_cost: 1000000, storage_cost: 5175600, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 21: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 23: +//# run Test::M1::create --args 0 @A --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 25: +//# run Test::M1::create --args 1 @A --sender B +created: object(4,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 5, line 27: +//# run Test::M1::create --args 2 @A --sender C +created: object(5,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 6, line 29: +//# run Test::M1::create --args 3 @A --sender D +created: object(6,0) +mutated: object(0,3) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 7, line 31: +//# run Test::M1::create --args 4 @A --sender E +created: object(7,0) +mutated: object(0,4) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 8, line 33: +//# create-checkpoint +Checkpoint created: 2 + +task 9, lines 35-53: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjoyLCJ0Ijo2LCJpIjpmYWxzZX0", + "startCursor": "eyJjIjoyLCJ0IjoyLCJpIjpmYWxzZX0" + }, + "nodes": [ + { + "digest": "78YAzuJPHbHXsqqj2GjiBibpAiWim7sha6MseSCN2Y6g", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "9zVSnZuHjSQZKcbrwmpkUfsRTA4J9VKqSiqzNCmycPex", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "BmipCooPMB1CHeRuFHS15q4VQ14pSLoYvuuNEfNsvZBc", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "W3YEPxp4z4LzuwoGq4kmCBiy12xv4cNuEvwusrsxDem", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "HCNwnSLqsQYEju3KoQbxpa3SD5mVG6FLcggkd2ZYxHvB", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + ] + } + } +} + +task 10, lines 55-73: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjoyLCJ0IjoyLCJpIjpmYWxzZX0", + "startCursor": "eyJjIjoyLCJ0IjoyLCJpIjpmYWxzZX0" + }, + "nodes": [ + { + "digest": "78YAzuJPHbHXsqqj2GjiBibpAiWim7sha6MseSCN2Y6g", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + ] + } + } +} + +task 11, lines 75-93: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjoyLCJ0IjozLCJpIjpmYWxzZX0", + "startCursor": "eyJjIjoyLCJ0IjozLCJpIjpmYWxzZX0" + }, + "nodes": [ + { + "digest": "9zVSnZuHjSQZKcbrwmpkUfsRTA4J9VKqSiqzNCmycPex", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + ] + } + } +} + +task 12, lines 95-113: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjoyLCJ0Ijo0LCJpIjpmYWxzZX0", + "startCursor": "eyJjIjoyLCJ0Ijo0LCJpIjpmYWxzZX0" + }, + "nodes": [ + { + "digest": "BmipCooPMB1CHeRuFHS15q4VQ14pSLoYvuuNEfNsvZBc", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + ] + } + } +} + +task 13, lines 115-133: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjoyLCJ0Ijo1LCJpIjpmYWxzZX0", + "startCursor": "eyJjIjoyLCJ0Ijo1LCJpIjpmYWxzZX0" + }, + "nodes": [ + { + "digest": "W3YEPxp4z4LzuwoGq4kmCBiy12xv4cNuEvwusrsxDem", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + ] + } + } +} + +task 14, lines 135-153: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjoyLCJ0Ijo2LCJpIjpmYWxzZX0", + "startCursor": "eyJjIjoyLCJ0Ijo2LCJpIjpmYWxzZX0" + }, + "nodes": [ + { + "digest": "HCNwnSLqsQYEju3KoQbxpa3SD5mVG6FLcggkd2ZYxHvB", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + ] + } + } +} + +task 15, lines 155-173: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": null, + "startCursor": null + }, + "nodes": [] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/filters/kind.move b/crates/sui-graphql-e2e-tests/tests/transactions/filters/kind.move new file mode 100644 index 0000000000000..485cb837f2023 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/filters/kind.move @@ -0,0 +1,173 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B C D E --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @A --sender A + +//# run Test::M1::create --args 1 @A --sender B + +//# run Test::M1::create --args 2 @A --sender C + +//# run Test::M1::create --args 3 @A --sender D + +//# run Test::M1::create --args 4 @A --sender E + +//# create-checkpoint + +//# run-graphql +{ + transactionBlocks(first: 50 filter: {kind: PROGRAMMABLE_TX atCheckpoint: 2}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(first: 50 filter: {kind: PROGRAMMABLE_TX atCheckpoint: 2 signAddress: "@{A}"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(first: 50 filter: {kind: PROGRAMMABLE_TX atCheckpoint: 2 signAddress: "@{B}"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(first: 50 filter: {kind: PROGRAMMABLE_TX atCheckpoint: 2 signAddress: "@{C}"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(first: 50 filter: {kind: PROGRAMMABLE_TX atCheckpoint: 2 signAddress: "@{D}"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(first: 50 filter: {kind: PROGRAMMABLE_TX atCheckpoint: 2 signAddress: "@{E}"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(first: 50 filter: {kind: SYSTEM_TX atCheckpoint: 2}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/filters/transaction_ids.exp b/crates/sui-graphql-e2e-tests/tests/transactions/filters/transaction_ids.exp new file mode 100644 index 0000000000000..68ccdaff1ab7f --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/filters/transaction_ids.exp @@ -0,0 +1,95 @@ +processed 9 tasks + +init: +A: object(0,0) + +task 1, lines 6-19: +//# publish +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 5175600, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 21: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 23: +//# run Test::M1::create --args 0 @A --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 25: +//# create-checkpoint +Checkpoint created: 2 + +task 5, lines 27-45: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": null, + "startCursor": null + }, + "nodes": [] + } + } +} + +task 6, lines 47-65: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": null, + "startCursor": null + }, + "nodes": [] + } + } +} + +task 7, lines 67-85: +//# run-graphql +Response: { + "data": null, + "errors": [ + { + "message": "A scan limit must be specified for the given filter combination", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "transactionBlocks" + ], + "extensions": { + "code": "BAD_USER_INPUT" + } + } + ] +} + +task 8, lines 87-105: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjoyLCJ0IjoyLCJpIjp0cnVlfQ", + "startCursor": "eyJjIjoyLCJ0IjowLCJpIjp0cnVlfQ" + }, + "nodes": [] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/filters/transaction_ids.move b/crates/sui-graphql-e2e-tests/tests/transactions/filters/transaction_ids.move new file mode 100644 index 0000000000000..ba15bcdc5ce9b --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/filters/transaction_ids.move @@ -0,0 +1,105 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @A --sender A + +//# create-checkpoint + +//# run-graphql +{ + transactionBlocks(filter: {transactionIds: []}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(filter: {signAddress: "@{A}" transactionIds: []}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(filter: {recvAddress: "@{A}" transactionIds: []}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(scanLimit: 10 filter: {recvAddress: "@{A}" transactionIds: []}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/programmable.exp b/crates/sui-graphql-e2e-tests/tests/transactions/programmable.exp index d1a45fd72cb9b..96578345e23ca 100644 --- a/crates/sui-graphql-e2e-tests/tests/transactions/programmable.exp +++ b/crates/sui-graphql-e2e-tests/tests/transactions/programmable.exp @@ -1008,7 +1008,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjo0LCJ0IjowLCJ0YyI6MH0", + "cursor": "eyJjIjo0LCJ0IjowLCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "GenesisTransaction" @@ -1016,7 +1016,7 @@ Response: { } }, { - "cursor": "eyJjIjo0LCJ0IjoxLCJ0YyI6MX0", + "cursor": "eyJjIjo0LCJ0IjoxLCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1024,7 +1024,7 @@ Response: { } }, { - "cursor": "eyJjIjo0LCJ0IjoyLCJ0YyI6Mn0", + "cursor": "eyJjIjo0LCJ0IjoyLCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1032,7 +1032,7 @@ Response: { } }, { - "cursor": "eyJjIjo0LCJ0IjozLCJ0YyI6M30", + "cursor": "eyJjIjo0LCJ0IjozLCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1040,7 +1040,7 @@ Response: { } }, { - "cursor": "eyJjIjo0LCJ0Ijo0LCJ0YyI6M30", + "cursor": "eyJjIjo0LCJ0Ijo0LCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1048,7 +1048,7 @@ Response: { } }, { - "cursor": "eyJjIjo0LCJ0Ijo1LCJ0YyI6NH0", + "cursor": "eyJjIjo0LCJ0Ijo1LCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1067,7 +1067,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjo0LCJ0IjowLCJ0YyI6MH0", + "cursor": "eyJjIjo0LCJ0IjowLCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "GenesisTransaction" @@ -1086,7 +1086,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjo0LCJ0IjoxLCJ0YyI6MX0", + "cursor": "eyJjIjo0LCJ0IjoxLCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1094,7 +1094,7 @@ Response: { } }, { - "cursor": "eyJjIjo0LCJ0IjoyLCJ0YyI6Mn0", + "cursor": "eyJjIjo0LCJ0IjoyLCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1102,7 +1102,7 @@ Response: { } }, { - "cursor": "eyJjIjo0LCJ0IjozLCJ0YyI6M30", + "cursor": "eyJjIjo0LCJ0IjozLCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1110,7 +1110,7 @@ Response: { } }, { - "cursor": "eyJjIjo0LCJ0Ijo0LCJ0YyI6M30", + "cursor": "eyJjIjo0LCJ0Ijo0LCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1118,7 +1118,7 @@ Response: { } }, { - "cursor": "eyJjIjo0LCJ0Ijo1LCJ0YyI6NH0", + "cursor": "eyJjIjo0LCJ0Ijo1LCJpIjpmYWxzZX0", "node": { "kind": { "__typename": "ProgrammableTransactionBlock" @@ -1149,10 +1149,10 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjo0LCJ0IjoyLCJ0YyI6Mn0" + "cursor": "eyJjIjo0LCJ0IjoyLCJpIjpmYWxzZX0" }, { - "cursor": "eyJjIjo0LCJ0IjozLCJ0YyI6M30" + "cursor": "eyJjIjo0LCJ0IjozLCJpIjpmYWxzZX0" } ] } @@ -1166,10 +1166,10 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjo0LCJ0IjozLCJ0YyI6M30" + "cursor": "eyJjIjo0LCJ0IjozLCJpIjpmYWxzZX0" }, { - "cursor": "eyJjIjo0LCJ0Ijo0LCJ0YyI6M30" + "cursor": "eyJjIjo0LCJ0Ijo0LCJpIjpmYWxzZX0" } ] } @@ -1183,7 +1183,7 @@ Response: { "transactionBlocks": { "edges": [ { - "cursor": "eyJjIjo0LCJ0IjozLCJ0YyI6M30" + "cursor": "eyJjIjo0LCJ0IjozLCJpIjpmYWxzZX0" } ] } diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/alternating.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/alternating.exp new file mode 100644 index 0000000000000..3ae2d4b5218c5 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/alternating.exp @@ -0,0 +1,300 @@ +processed 22 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 8-29: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 31: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 33: +//# run Test::M1::create --args 0 @A --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 35: +//# run Test::M1::create --args 1 @B --sender B +created: object(4,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 5, line 37: +//# run Test::M1::create --args 2 @A --sender A +created: object(5,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 6, line 39: +//# run Test::M1::create --args 3 @B --sender B +created: object(6,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 41: +//# run Test::M1::create --args 4 @A --sender A +created: object(7,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 43: +//# create-checkpoint +Checkpoint created: 2 + +task 9, line 45: +//# run Test::M1::create --args 100 @B --sender B +created: object(9,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10, line 47: +//# run Test::M1::create --args 101 @A --sender A +created: object(10,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 11, line 49: +//# run Test::M1::create --args 102 @B --sender B +created: object(11,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 12, line 51: +//# run Test::M1::create --args 103 @A --sender A +created: object(12,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13, line 53: +//# run Test::M1::create --args 104 @B --sender B +created: object(13,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 14, line 55: +//# create-checkpoint +Checkpoint created: 3 + +task 15, lines 57-78: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjozLCJ0IjozLCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "CReUjLynvpq4dD4w6zekGxvSyBBQF2e3KG3K2Rs7oD8L", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 16, lines 80-104: +//# run-graphql --cursors {"c":3,"t":3,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "eyJjIjozLCJ0Ijo0LCJpIjpmYWxzZX0", + "endCursor": "eyJjIjozLCJ0Ijo2LCJpIjpmYWxzZX0" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "Hgu3LePqrpyR8Vq3Ve4L2KmvErcdcz92u7YiiotkKJ1N", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo2LCJpIjpmYWxzZX0", + "node": { + "digest": "2EwyAHiMofhbM5z5ty7XT1QXs4sNZfHmZLX513Ag8sD3", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 17, lines 106-129: +//# run-graphql --cursors {"c":3,"t":3,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjozLCJ0Ijo0LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjozLCJ0Ijo1LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "Hgu3LePqrpyR8Vq3Ve4L2KmvErcdcz92u7YiiotkKJ1N", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 18, lines 131-155: +//# run-graphql --cursors {"c":3,"t":6,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjozLCJ0Ijo3LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjozLCJ0Ijo4LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0Ijo4LCJpIjpmYWxzZX0", + "node": { + "digest": "4LUhoFJMmZfG71RHiRkwa9KHovrDv3S3mqUM1vu9JWKJ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 19, lines 157-184: +//# run-graphql --cursors {"c":3,"t":5,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjozLCJ0Ijo2LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjozLCJ0Ijo4LCJpIjpmYWxzZX0" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0Ijo2LCJpIjpmYWxzZX0", + "node": { + "digest": "2EwyAHiMofhbM5z5ty7XT1QXs4sNZfHmZLX513Ag8sD3", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo4LCJpIjpmYWxzZX0", + "node": { + "digest": "4LUhoFJMmZfG71RHiRkwa9KHovrDv3S3mqUM1vu9JWKJ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 20, lines 186-209: +//# run-graphql --cursors {"c":3,"t":8,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjozLCJ0IjoxMCwiaSI6ZmFsc2V9", + "endCursor": "eyJjIjozLCJ0IjoxMCwiaSI6ZmFsc2V9" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoxMCwiaSI6ZmFsc2V9", + "node": { + "digest": "AnqDERsdbEiE26CACJa6KtJTLsggisgu7yxhMJ6mU1JZ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 21, lines 211-235: +//# run-graphql --cursors {"c":3,"t":8,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjozLCJ0Ijo5LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoxMCwiaSI6ZmFsc2V9", + "node": { + "digest": "AnqDERsdbEiE26CACJa6KtJTLsggisgu7yxhMJ6mU1JZ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/alternating.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/alternating.move new file mode 100644 index 0000000000000..a5f339d31d94e --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/alternating.move @@ -0,0 +1,235 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Testing behavior of alternating between a scan-limited and normal query + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @A --sender A + +//# run Test::M1::create --args 1 @B --sender B + +//# run Test::M1::create --args 2 @A --sender A + +//# run Test::M1::create --args 3 @B --sender B + +//# run Test::M1::create --args 4 @A --sender A + +//# create-checkpoint + +//# run Test::M1::create --args 100 @B --sender B + +//# run Test::M1::create --args 101 @A --sender A + +//# run Test::M1::create --args 102 @B --sender B + +//# run Test::M1::create --args 103 @A --sender A + +//# run Test::M1::create --args 104 @B --sender B + +//# create-checkpoint + +//# run-graphql +{ + transactionBlocks(first: 2 scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":3,"t":3,"i":true} +# This should return the next two matching transactions after 3, +# so tx 4 and 6. the boundary cursors should wrap the response set, +# and both should have isScanLimited set to false +{ + transactionBlocks(first: 2 after: "@{cursor_0}" filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":3,"t":3,"i":true} +# Meanwhile, because of the scanLimit of 2, the boundary cursors are +# startCursor: 4, endCursor: 5, and both are scan limited +{ + transactionBlocks(first: 2 after: "@{cursor_0}" scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":3,"t":6,"i":false} +# From a previous query that was not scan limited, paginate with scan limit +# startCursor: 7, endCursor: 8, both scan limited +# response set consists of single tx 8 +{ + transactionBlocks(first: 2 after: "@{cursor_0}" scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":3,"t":5,"i":true} +# from tx 5, select the next two transactions that match +# setting the scanLimit to impose all of the remaining txs +# even though we've finished scanning +# we should indicate there is a next page so we don't skip any txs +# consequently, the endCursor wraps the result set +# startCursor: 6, endCursor: 8, endCursor is not scan limited +{ + transactionBlocks(first: 2 after: "@{cursor_0}" scanLimit: 6 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":3,"t":8,"i":false} +# fetch the last tx without scan limit +# startCursor = endCursor = 10, wrapping the response set +{ + transactionBlocks(first: 2 after: "@{cursor_0}" filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":3,"t":8,"i":false} +# fetch the last tx with scan limit +# unlike the not-scan-limited query, the start and end cursors +# are expanded out to the scanned window, instead of wrapping the response set +{ + transactionBlocks(first: 2 after: "@{cursor_0}" scanLimit: 6 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/both_cursors.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/both_cursors.exp new file mode 100644 index 0000000000000..2fee54d94c6f6 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/both_cursors.exp @@ -0,0 +1,177 @@ +processed 18 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 9-30: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 32: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 34: +//# run Test::M1::create --args 0 @B --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 36: +//# run Test::M1::create --args 1 @A --sender A +created: object(4,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5, line 38: +//# run Test::M1::create --args 2 @B --sender A +created: object(5,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 6, line 40: +//# run Test::M1::create --args 3 @A --sender A +created: object(6,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 42: +//# run Test::M1::create --args 4 @B --sender A +created: object(7,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 44: +//# create-checkpoint +Checkpoint created: 2 + +task 9, line 46: +//# run Test::M1::create --args 100 @A --sender A +created: object(9,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10, line 48: +//# run Test::M1::create --args 101 @A --sender A +created: object(10,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 11, line 50: +//# run Test::M1::create --args 102 @A --sender A +created: object(11,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 12, line 52: +//# run Test::M1::create --args 103 @B --sender A +created: object(12,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13, line 54: +//# run Test::M1::create --args 104 @B --sender A +created: object(13,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 14, line 56: +//# create-checkpoint +Checkpoint created: 3 + +task 15, lines 58-81: +//# run-graphql --cursors {"c":4,"t":2,"i":true} {"c":4,"t":7,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0IjozLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo0LCJpIjpmYWxzZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "6RKZYt946ztfY8ZVspCv8faXBzKxDcTUEHnrCyBSZ4Li", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 16, lines 83-108: +//# run-graphql --cursors {"c":4,"t":2,"i":true} {"c":4,"t":7,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0IjozLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo2LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "6RKZYt946ztfY8ZVspCv8faXBzKxDcTUEHnrCyBSZ4Li", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjo0LCJ0Ijo2LCJpIjpmYWxzZX0", + "node": { + "digest": "83AZLnLVtQeUdrXGg3igLkeo94j3wTLuwY4izobLLVBT", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 17, lines 110-133: +//# run-graphql --cursors {"c":4,"t":4,"i":true} {"c":4,"t":8,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo1LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo1LCJpIjpmYWxzZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo1LCJpIjpmYWxzZX0", + "node": { + "digest": "74pdWZw8nEhvtan9aoYHAeZxGouHsj9cwBr5GcgncNAz", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/both_cursors.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/both_cursors.move new file mode 100644 index 0000000000000..ba913319b65ad --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/both_cursors.move @@ -0,0 +1,133 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Tests paginating forwards where first and scanLimit are equal. The 1st, 3rd, 5th, and 7th through +// 10th transactions will match the filtering criteria. + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @B --sender A + +//# run Test::M1::create --args 1 @A --sender A + +//# run Test::M1::create --args 2 @B --sender A + +//# run Test::M1::create --args 3 @A --sender A + +//# run Test::M1::create --args 4 @B --sender A + +//# create-checkpoint + +//# run Test::M1::create --args 100 @A --sender A + +//# run Test::M1::create --args 101 @A --sender A + +//# run Test::M1::create --args 102 @A --sender A + +//# run Test::M1::create --args 103 @B --sender A + +//# run Test::M1::create --args 104 @B --sender A + +//# create-checkpoint + +//# run-graphql --cursors {"c":4,"t":2,"i":true} {"c":4,"t":7,"i":true} +# startCursor is at 3 + scanLimited, endCursor at 4 + not scanLimited +# this is because between (2, 7), txs 4 and 6 match, and thus endCursor snaps to last of result +{ + transactionBlocks(first: 1 scanLimit: 4 after: "@{cursor_0}" before: "@{cursor_1}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":2,"i":true} {"c":4,"t":7,"i":true} +# startCursor is at 3 + scanLimited, endCursor at 6 + scanLimited +# we return txs 4 and 6, paginate_results thinks we do not have a next page, +# and scan-limit logic will override this as there are still more txs to scan +# note that we're scanning txs [3, 6] +{ + transactionBlocks(first: 3 scanLimit: 4 after: "@{cursor_0}" before: "@{cursor_1}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":4,"i":true} {"c":4,"t":8,"i":true} +# txs 5 and 7 match, but due to page size of `first: 1`, we only return tx 5 +# startCursor is 5 + scan limited, endCursor is also 5 + scan limited +{ + transactionBlocks(first: 1 scanLimit: 3 after: "@{cursor_0}" before: "@{cursor_1}" filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/first.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/first.exp new file mode 100644 index 0000000000000..b02eb2652a412 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/first.exp @@ -0,0 +1,358 @@ +processed 25 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 9-30: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 32: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 34: +//# run Test::M1::create --args 0 @B --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 36: +//# run Test::M1::create --args 1 @A --sender A +created: object(4,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5, line 38: +//# run Test::M1::create --args 2 @B --sender A +created: object(5,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 6, line 40: +//# run Test::M1::create --args 3 @A --sender A +created: object(6,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 42: +//# run Test::M1::create --args 4 @B --sender A +created: object(7,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 44: +//# create-checkpoint +Checkpoint created: 2 + +task 9, line 46: +//# run Test::M1::create --args 100 @A --sender A +created: object(9,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10, line 48: +//# run Test::M1::create --args 101 @A --sender A +created: object(10,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 11, line 50: +//# run Test::M1::create --args 102 @A --sender A +created: object(11,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 12, line 52: +//# run Test::M1::create --args 103 @B --sender A +created: object(12,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13, line 54: +//# run Test::M1::create --args 104 @B --sender A +created: object(13,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 14, line 56: +//# create-checkpoint +Checkpoint created: 3 + +task 15, lines 58-82: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "HzyC8gcn4m1ymKxYSpWMaNnmbrqm4hX7UBteJ4me3LFd", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "6RKZYt946ztfY8ZVspCv8faXBzKxDcTUEHnrCyBSZ4Li", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo2LCJpIjpmYWxzZX0", + "node": { + "digest": "83AZLnLVtQeUdrXGg3igLkeo94j3wTLuwY4izobLLVBT", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjoxMCwiaSI6ZmFsc2V9", + "node": { + "digest": "AWWgnnumcijVEY2YUzs4MtqzFPeLKFAbHnnjXwUyn6Gj", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9", + "node": { + "digest": "DVVVd1cLYDpV3KHhXpirV5NFpk3DKaCuKvXXFeG2owA7", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 16, lines 85-111: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjozLCJ0IjozLCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "HzyC8gcn4m1ymKxYSpWMaNnmbrqm4hX7UBteJ4me3LFd", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 17, lines 113-137: +//# run-graphql --cursors {"c":4,"t":3,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo0LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo0LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "6RKZYt946ztfY8ZVspCv8faXBzKxDcTUEHnrCyBSZ4Li", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 18, lines 139-165: +//# run-graphql --cursors {"c":4,"t":4,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo1LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo3LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo2LCJpIjpmYWxzZX0", + "node": { + "digest": "83AZLnLVtQeUdrXGg3igLkeo94j3wTLuwY4izobLLVBT", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 19, lines 167-193: +//# run-graphql --cursors {"c":4,"t":7,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo4LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo5LCJpIjp0cnVlfQ" + }, + "edges": [] + } + } +} + +task 20, lines 195-220: +//# run-graphql --cursors {"c":4,"t":9,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjo0LCJ0IjoxMCwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjo0LCJ0IjoxMSwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0IjoxMCwiaSI6ZmFsc2V9", + "node": { + "digest": "AWWgnnumcijVEY2YUzs4MtqzFPeLKFAbHnnjXwUyn6Gj", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjo0LCJ0IjoxMSwiaSI6ZmFsc2V9", + "node": { + "digest": "DVVVd1cLYDpV3KHhXpirV5NFpk3DKaCuKvXXFeG2owA7", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 21, line 222: +//# run Test::M1::create --args 105 @A --sender A +created: object(21,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 22, line 224: +//# create-checkpoint +Checkpoint created: 4 + +task 23, lines 226-252: +//# run-graphql --cursors {"c":4,"t":11,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjo0LCJ0IjoxMiwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjo0LCJ0IjoxMiwiaSI6dHJ1ZX0" + }, + "edges": [] + } + } +} + +task 24, lines 254-281: +//# run-graphql --cursors {"c":4,"t":12,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0IjoxMCwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjo0LCJ0IjoxMSwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0IjoxMCwiaSI6ZmFsc2V9", + "node": { + "digest": "AWWgnnumcijVEY2YUzs4MtqzFPeLKFAbHnnjXwUyn6Gj", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjo0LCJ0IjoxMSwiaSI6ZmFsc2V9", + "node": { + "digest": "DVVVd1cLYDpV3KHhXpirV5NFpk3DKaCuKvXXFeG2owA7", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/first.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/first.move new file mode 100644 index 0000000000000..a3a3705413cba --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/first.move @@ -0,0 +1,281 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Tests paginating forwards where first and scanLimit are equal. The 1st, 3rd, 5th, and 7th through +// 10th transactions will match the filtering criteria. + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @B --sender A + +//# run Test::M1::create --args 1 @A --sender A + +//# run Test::M1::create --args 2 @B --sender A + +//# run Test::M1::create --args 3 @A --sender A + +//# run Test::M1::create --args 4 @B --sender A + +//# create-checkpoint + +//# run Test::M1::create --args 100 @A --sender A + +//# run Test::M1::create --args 101 @A --sender A + +//# run Test::M1::create --args 102 @A --sender A + +//# run Test::M1::create --args 103 @B --sender A + +//# run Test::M1::create --args 104 @B --sender A + +//# create-checkpoint + +//# run-graphql +# Expect 7 results +# [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] <- tx_sequence_number +# [B, A, B, A, B, A, A, A, B, B] +{ + transactionBlocks(first: 50 filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + + +//# run-graphql +# scans [B, A] -> [2, 3] +# Because `scanLimit` is specified, both the start and end cursors should have `is_scan_limited` flag to true +# startCursor is at 2, endCursor is at 3 +# The cursor for the node will have `is_scan_limited` flag set to false, because we know for sure there is +# a corresponding element for the cursor in the result set. +{ + transactionBlocks(first: 2 scanLimit: 2 filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":3,"i":true} +# scans [B] -> [4] +# Still paginating with `scanLimit`, both the start and end cursors should have `is_scan_limited` flag to true +# because of the scanLimit of 4, startCursor = endCursor = 4 +{ + transactionBlocks(first: 1 scanLimit: 1 after: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":4,"i":true} +# scans [A, B, A] -> [5, 6, 7] +# both the start and end cursors should have `is_scan_limited` flag to true +# startCursor at 5, the sole element has cursor at 6, endCursor at 7 +# instead of wrapping around the result set, the boundary cursors are pushed out +# to the first and last transaction scanned in this query +{ + transactionBlocks(first: 3 scanLimit: 3 after: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":7,"i":true} +# scans [A, A] -> [8, 9] +# both the start and end cursors should have `is_scan_limited` flag to true +# startCursor at 5, the sole element has cursor at 8, endCursor at 9 +# instead of returninng None, we set the boundary cursors +# to the first and last transaction scanned in this query +{ + transactionBlocks(first: 2 scanLimit: 2 after: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":9,"i":true} +# scans [B, B] -> [10, 11] +# both the start and end cursors should have `is_scan_limited` flag to true +# startCursor at 10, endCursor at 11 +# correctly detects we've reached the end of the upper bound +{ + transactionBlocks(first: 2 scanLimit: 2 after: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run Test::M1::create --args 105 @A --sender A + +//# create-checkpoint + +//# run-graphql --cursors {"c":4,"t":11,"i":true} +# we've introduced a new final transaction that doesn't match the filter +# both the start and end cursors should have `is_scan_limited` flag to true +# startCursor = endCursor = 12, because there is only 1 more from the given cursor, +# regardless of the specified scanLimit +# correctly detects we've reached the end of the upper bound +{ + transactionBlocks(first: 2 scanLimit: 2 after: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 5}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":12,"i":true} +# try paginating backwards on the last `endCursor` +# should yield startCursor at 10, endCursor at 11 +# and the result set consists of txs 10 and 11 +# the scanLimit is exclusive of the cursor, hence we reach tx 10 inclusively +# there is a next page, which is the 12th tx, which should yield an empty set +# per the filtering criteria +{ + transactionBlocks(last: 2 scanLimit: 2 before: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 5}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/last.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/last.exp new file mode 100644 index 0000000000000..e2b301c1dc7e3 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/last.exp @@ -0,0 +1,309 @@ +processed 22 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 8-29: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 31: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 33: +//# run Test::M1::create --args 0 @B --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 35: +//# run Test::M1::create --args 1 @B --sender A +created: object(4,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5, line 37: +//# run Test::M1::create --args 2 @A --sender A +created: object(5,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 6, line 39: +//# run Test::M1::create --args 3 @A --sender A +created: object(6,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 41: +//# run Test::M1::create --args 4 @A --sender A +created: object(7,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 43: +//# create-checkpoint +Checkpoint created: 2 + +task 9, line 45: +//# run Test::M1::create --args 100 @B --sender A +created: object(9,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10, line 47: +//# run Test::M1::create --args 101 @A --sender A +created: object(10,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 11, line 49: +//# run Test::M1::create --args 102 @B --sender A +created: object(11,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 12, line 51: +//# run Test::M1::create --args 103 @A --sender A +created: object(12,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13, line 53: +//# run Test::M1::create --args 104 @B --sender A +created: object(13,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 14, line 55: +//# create-checkpoint +Checkpoint created: 3 + +task 15, lines 57-79: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "HzyC8gcn4m1ymKxYSpWMaNnmbrqm4hX7UBteJ4me3LFd", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjozLCJpIjpmYWxzZX0", + "node": { + "digest": "DiywoRFzC33smQhVf5K7AcM853XFgfgFxBGErLTEvVWi", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo3LCJpIjpmYWxzZX0", + "node": { + "digest": "F32vrNL7p5sa1iFeykvFQ17UYLeM1urXnSNbbGyoqDRx", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo5LCJpIjpmYWxzZX0", + "node": { + "digest": "3eHY9XENiqep4VvNBr3ws79TEMGhp5egGvFEymk679dc", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9", + "node": { + "digest": "4p5avK1cStj1xtBnvCnHgEUVVr5qnfXQ76ujUZx4ZvP8", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 16, lines 82-106: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjozLCJ0IjoxMCwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9", + "node": { + "digest": "4p5avK1cStj1xtBnvCnHgEUVVr5qnfXQ76ujUZx4ZvP8", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 17, lines 108-132: +//# run-graphql --cursors {"c":4,"t":10,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo5LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo5LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo5LCJpIjpmYWxzZX0", + "node": { + "digest": "3eHY9XENiqep4VvNBr3ws79TEMGhp5egGvFEymk679dc", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 18, lines 134-158: +//# run-graphql --cursors {"c":4,"t":9,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo2LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo4LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo3LCJpIjpmYWxzZX0", + "node": { + "digest": "F32vrNL7p5sa1iFeykvFQ17UYLeM1urXnSNbbGyoqDRx", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 19, lines 160-184: +//# run-graphql --cursors {"c":4,"t":6,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo0LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo1LCJpIjp0cnVlfQ" + }, + "edges": [] + } + } +} + +task 20, lines 186-209: +//# run-graphql --cursors {"c":4,"t":4,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0IjoyLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0IjozLCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "HzyC8gcn4m1ymKxYSpWMaNnmbrqm4hX7UBteJ4me3LFd", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjo0LCJ0IjozLCJpIjpmYWxzZX0", + "node": { + "digest": "DiywoRFzC33smQhVf5K7AcM853XFgfgFxBGErLTEvVWi", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 21, lines 212-235: +//# run-graphql --cursors {"c":4,"t":2,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": null, + "endCursor": null + }, + "edges": [] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/last.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/last.move new file mode 100644 index 0000000000000..6a84f9eabe90b --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/equal/last.move @@ -0,0 +1,235 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Mirrors scan_limit/equal/first.move, paginating backwards where first and scanLimit are equal. + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @B --sender A + +//# run Test::M1::create --args 1 @B --sender A + +//# run Test::M1::create --args 2 @A --sender A + +//# run Test::M1::create --args 3 @A --sender A + +//# run Test::M1::create --args 4 @A --sender A + +//# create-checkpoint + +//# run Test::M1::create --args 100 @B --sender A + +//# run Test::M1::create --args 101 @A --sender A + +//# run Test::M1::create --args 102 @B --sender A + +//# run Test::M1::create --args 103 @A --sender A + +//# run Test::M1::create --args 104 @B --sender A + +//# create-checkpoint + +//# run-graphql +# Expect ten results +{ + transactionBlocks(last: 50 filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + + +//# run-graphql +# boundary cursors are scan limited +# startCursor: 10, endCursor: 11 +# result is single element with cursor: 11 +{ + transactionBlocks(last: 2 scanLimit: 2 filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":10,"i":true} +# boundary cursors are scan limited +# startCursor: 9, endCursor: 9 +# result is single element with cursor: 9 +{ + transactionBlocks(last: 1 scanLimit: 1 before: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":9,"i":true} +# boundary cursors are scan limited +# startCursor: 6, endCursor: 8 +# result is single element with cursor: 7 +{ + transactionBlocks(last: 3 scanLimit: 3 before: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":6,"i":true} +# boundary cursors are scan limited +# startCursor: 4, endCursor: 5 +# expect empty set +{ + transactionBlocks(last: 2 scanLimit: 2 before: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":4,"i":true} +# Returns the first two matching transactions, boundary cursors both have `is_scan_limited: true` +# startCursor: 2, endCursor: 3 +{ + transactionBlocks(last: 2 scanLimit: 2 before: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + + +//# run-graphql --cursors {"c":4,"t":2,"i":true} +# Since we know from the previous query that there is not a previous page at this cursor, +# Expect false for page flags and null for cursors +{ + transactionBlocks(last: 2 scanLimit: 2 before: "@{cursor_0}" filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/first.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/first.exp new file mode 100644 index 0000000000000..9f785684321d9 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/first.exp @@ -0,0 +1,378 @@ +processed 34 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 6-27: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 29: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 31: +//# run Test::M1::create --args 0 @A --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 33: +//# run Test::M1::create --args 1 @A --sender A +created: object(4,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5, line 35: +//# run Test::M1::create --args 2 @B --sender B +created: object(5,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 6, line 37: +//# run Test::M1::create --args 3 @B --sender B +created: object(6,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 39: +//# run Test::M1::create --args 4 @B --sender B +created: object(7,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 41: +//# create-checkpoint +Checkpoint created: 2 + +task 9, line 43: +//# run Test::M1::create --args 100 @B --sender B +created: object(9,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10, line 45: +//# run Test::M1::create --args 101 @B --sender B +created: object(10,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 11, line 47: +//# run Test::M1::create --args 102 @B --sender B +created: object(11,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 12, line 49: +//# run Test::M1::create --args 103 @B --sender B +created: object(12,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13, line 51: +//# run Test::M1::create --args 104 @B --sender B +created: object(13,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 14, line 53: +//# create-checkpoint +Checkpoint created: 3 + +task 15, line 55: +//# run Test::M1::create --args 100 @B --sender B +created: object(15,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 16, line 57: +//# run Test::M1::create --args 101 @B --sender B +created: object(16,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 17, line 59: +//# run Test::M1::create --args 102 @B --sender B +created: object(17,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 18, line 61: +//# run Test::M1::create --args 103 @B --sender B +created: object(18,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 19, line 63: +//# run Test::M1::create --args 104 @B --sender B +created: object(19,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 20, line 65: +//# create-checkpoint +Checkpoint created: 4 + +task 21, line 67: +//# run Test::M1::create --args 200 @A --sender A +created: object(21,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 22, line 69: +//# run Test::M1::create --args 201 @B --sender A +created: object(22,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 23, line 71: +//# run Test::M1::create --args 202 @B --sender B +created: object(23,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 24, line 73: +//# run Test::M1::create --args 203 @B --sender B +created: object(24,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 25, line 75: +//# run Test::M1::create --args 204 @A --sender A +created: object(25,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 26, line 77: +//# create-checkpoint +Checkpoint created: 5 + +task 27, lines 79-100: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "eyJjIjo1LCJ0IjoyLCJpIjpmYWxzZX0", + "endCursor": "eyJjIjo1LCJ0IjoyMSwiaSI6ZmFsc2V9" + }, + "edges": [ + { + "cursor": "eyJjIjo1LCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "CReUjLynvpq4dD4w6zekGxvSyBBQF2e3KG3K2Rs7oD8L", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjo1LCJ0IjozLCJpIjpmYWxzZX0", + "node": { + "digest": "BB6bMUxrBJX9wANzzptKnJM3bMVFKnD4xtY8DGWthaH9", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjo1LCJ0IjoxNywiaSI6ZmFsc2V9", + "node": { + "digest": "BhW3cLzaCgdRYc6jcmXB6DrQ6KaTfmYmb6vBNiu5xPhg", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + }, + { + "cursor": "eyJjIjo1LCJ0IjoxOCwiaSI6ZmFsc2V9", + "node": { + "digest": "FjpAF5bT173BfV6HLuBdBd2bYevsUVDeJNmYnmJYsTLb", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + }, + { + "cursor": "eyJjIjo1LCJ0IjoyMSwiaSI6ZmFsc2V9", + "node": { + "digest": "Eq611DTcfsBjH1P9Smrv7xFYQZXNtUMgbQCUr5jo3ycJ", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + } + ] + } + } +} + +task 28, lines 103-129: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "eyJjIjo1LCJ0IjoyLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo1LCJ0IjoyLCJpIjpmYWxzZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo1LCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "CReUjLynvpq4dD4w6zekGxvSyBBQF2e3KG3K2Rs7oD8L", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 29, lines 131-155: +//# run-graphql --cursors {"c":7,"t":2,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo3LCJ0IjozLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo3LCJ0Ijo3LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo3LCJ0IjozLCJpIjpmYWxzZX0", + "node": { + "digest": "BB6bMUxrBJX9wANzzptKnJM3bMVFKnD4xtY8DGWthaH9", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 30, lines 157-180: +//# run-graphql --cursors {"c":7,"t":7,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo3LCJ0Ijo4LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo3LCJ0IjoxMiwiaSI6dHJ1ZX0" + }, + "edges": [] + } + } +} + +task 31, lines 182-205: +//# run-graphql --cursors {"c":7,"t":12,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo3LCJ0IjoxMywiaSI6dHJ1ZX0", + "endCursor": "eyJjIjo3LCJ0IjoxNywiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo3LCJ0IjoxNywiaSI6ZmFsc2V9", + "node": { + "digest": "BhW3cLzaCgdRYc6jcmXB6DrQ6KaTfmYmb6vBNiu5xPhg", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + } + ] + } + } +} + +task 32, lines 207-232: +//# run-graphql --cursors {"c":7,"t":17,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo3LCJ0IjoxOCwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjo3LCJ0IjoxOCwiaSI6ZmFsc2V9" + }, + "edges": [ + { + "cursor": "eyJjIjo3LCJ0IjoxOCwiaSI6ZmFsc2V9", + "node": { + "digest": "FjpAF5bT173BfV6HLuBdBd2bYevsUVDeJNmYnmJYsTLb", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + } + ] + } + } +} + +task 33, lines 234-258: +//# run-graphql --cursors {"c":7,"t":18,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjo3LCJ0IjoxOSwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjo3LCJ0IjoyMSwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo3LCJ0IjoyMSwiaSI6ZmFsc2V9", + "node": { + "digest": "Eq611DTcfsBjH1P9Smrv7xFYQZXNtUMgbQCUr5jo3ycJ", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/first.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/first.move new file mode 100644 index 0000000000000..b9aea53452ff2 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/first.move @@ -0,0 +1,258 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @A --sender A + +//# run Test::M1::create --args 1 @A --sender A + +//# run Test::M1::create --args 2 @B --sender B + +//# run Test::M1::create --args 3 @B --sender B + +//# run Test::M1::create --args 4 @B --sender B + +//# create-checkpoint + +//# run Test::M1::create --args 100 @B --sender B + +//# run Test::M1::create --args 101 @B --sender B + +//# run Test::M1::create --args 102 @B --sender B + +//# run Test::M1::create --args 103 @B --sender B + +//# run Test::M1::create --args 104 @B --sender B + +//# create-checkpoint + +//# run Test::M1::create --args 100 @B --sender B + +//# run Test::M1::create --args 101 @B --sender B + +//# run Test::M1::create --args 102 @B --sender B + +//# run Test::M1::create --args 103 @B --sender B + +//# run Test::M1::create --args 104 @B --sender B + +//# create-checkpoint + +//# run Test::M1::create --args 200 @A --sender A + +//# run Test::M1::create --args 201 @B --sender A + +//# run Test::M1::create --args 202 @B --sender B + +//# run Test::M1::create --args 203 @B --sender B + +//# run Test::M1::create --args 204 @A --sender A + +//# create-checkpoint + +//# run-graphql +{ + transactionBlocks(filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + + +//# run-graphql +# startCursor is 2 and scanLimited, endCursor is 2 and not scanLimited +# instead of setting the endCursor to the last transaction scanned, +# we set it to the last transaction in the set +# this is so we don't end up skipping any other matches in the scan range +# but beyond the scope of the `limit` +{ + transactionBlocks(first: 1 scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":2,"i":false} +# startCursor: 3, endCursor: 7, both are scan-limited +# endCursor ends at 7, not 3, because we've exhausted all the matches +# within the window of scanning range, and will overwrite the endCursor to 7. +{ + transactionBlocks(first: 1 after: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":7,"i":true} +# startCursor: 8, endCursor: 12, both are scan-limited +# expect an empty set +{ + transactionBlocks(first: 1 after: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":12,"i":true} +# startCursor: 13, endCursor: 17, both are scan-limited +# single element returned, coincidentally also the last scanned transaction +{ + transactionBlocks(first: 1 after: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":17,"i":true} +# startCursor: 18 scanLimited, endCursor: 18 not scanLimited +# this is because we have multiple matches within the scanning range +# but due to the `first` limit, we return a subset. +# we don't want to skip over other matches, so we don't push the endCursor out +{ + transactionBlocks(first: 1 after: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":18,"i":false} +# startCursor: 19, endCursor: 21, both are scan-limited +# single element returned, coincidentally also the last scanned transaction +# note that the startCursor is 19, not 18 or 21, since we can use the scan-limited behavior +{ + transactionBlocks(first: 1 after: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/last.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/last.exp new file mode 100644 index 0000000000000..6f4367472900e --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/last.exp @@ -0,0 +1,355 @@ +processed 34 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 6-27: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 29: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 31: +//# run Test::M1::create --args 0 @A --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 33: +//# run Test::M1::create --args 1 @A --sender A +created: object(4,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5, line 35: +//# run Test::M1::create --args 2 @B --sender B +created: object(5,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 6, line 37: +//# run Test::M1::create --args 3 @B --sender B +created: object(6,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 39: +//# run Test::M1::create --args 4 @B --sender B +created: object(7,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 41: +//# create-checkpoint +Checkpoint created: 2 + +task 9, line 43: +//# run Test::M1::create --args 100 @B --sender B +created: object(9,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10, line 45: +//# run Test::M1::create --args 101 @B --sender B +created: object(10,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 11, line 47: +//# run Test::M1::create --args 102 @B --sender B +created: object(11,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 12, line 49: +//# run Test::M1::create --args 103 @B --sender B +created: object(12,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13, line 51: +//# run Test::M1::create --args 104 @B --sender B +created: object(13,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 14, line 53: +//# create-checkpoint +Checkpoint created: 3 + +task 15, line 55: +//# run Test::M1::create --args 100 @B --sender B +created: object(15,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 16, line 57: +//# run Test::M1::create --args 101 @B --sender B +created: object(16,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 17, line 59: +//# run Test::M1::create --args 102 @B --sender B +created: object(17,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 18, line 61: +//# run Test::M1::create --args 103 @B --sender B +created: object(18,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 19, line 63: +//# run Test::M1::create --args 104 @B --sender B +created: object(19,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 20, line 65: +//# create-checkpoint +Checkpoint created: 4 + +task 21, line 67: +//# run Test::M1::create --args 200 @A --sender A +created: object(21,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 22, line 69: +//# run Test::M1::create --args 201 @B --sender B +created: object(22,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 23, line 71: +//# run Test::M1::create --args 202 @B --sender B +created: object(23,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 24, line 73: +//# run Test::M1::create --args 203 @B --sender B +created: object(24,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 25, line 75: +//# run Test::M1::create --args 204 @A --sender A +created: object(25,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 26, line 77: +//# create-checkpoint +Checkpoint created: 5 + +task 27, lines 79-100: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "eyJjIjo1LCJ0IjoyLCJpIjpmYWxzZX0", + "endCursor": "eyJjIjo1LCJ0IjoyMSwiaSI6ZmFsc2V9" + }, + "edges": [ + { + "cursor": "eyJjIjo1LCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "CReUjLynvpq4dD4w6zekGxvSyBBQF2e3KG3K2Rs7oD8L", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjo1LCJ0IjozLCJpIjpmYWxzZX0", + "node": { + "digest": "BB6bMUxrBJX9wANzzptKnJM3bMVFKnD4xtY8DGWthaH9", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjo1LCJ0IjoxNywiaSI6ZmFsc2V9", + "node": { + "digest": "BhW3cLzaCgdRYc6jcmXB6DrQ6KaTfmYmb6vBNiu5xPhg", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + }, + { + "cursor": "eyJjIjo1LCJ0IjoyMSwiaSI6ZmFsc2V9", + "node": { + "digest": "G7DhbdbJeze8e5kmVLNY6aVKckM7yfrzadaABv71ssjx", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + } + ] + } + } +} + +task 28, lines 103-125: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjo1LCJ0IjoyMSwiaSI6ZmFsc2V9", + "endCursor": "eyJjIjo1LCJ0IjoyMSwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo1LCJ0IjoyMSwiaSI6ZmFsc2V9", + "node": { + "digest": "G7DhbdbJeze8e5kmVLNY6aVKckM7yfrzadaABv71ssjx", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + } + ] + } + } +} + +task 29, lines 127-152: +//# run-graphql --cursors {"c":7,"t":21,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo3LCJ0IjoxNiwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjo3LCJ0IjoyMCwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo3LCJ0IjoxNywiaSI6ZmFsc2V9", + "node": { + "digest": "BhW3cLzaCgdRYc6jcmXB6DrQ6KaTfmYmb6vBNiu5xPhg", + "effects": { + "checkpoint": { + "sequenceNumber": 5 + } + } + } + } + ] + } + } +} + +task 30, lines 154-177: +//# run-graphql --cursors {"c":7,"t":16,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo3LCJ0IjoxMSwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjo3LCJ0IjoxNSwiaSI6dHJ1ZX0" + }, + "edges": [] + } + } +} + +task 31, lines 179-201: +//# run-graphql --cursors {"c":7,"t":11,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo3LCJ0Ijo2LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo3LCJ0IjoxMCwiaSI6dHJ1ZX0" + }, + "edges": [] + } + } +} + +task 32, lines 203-227: +//# run-graphql --cursors {"c":7,"t":6,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo3LCJ0IjozLCJpIjpmYWxzZX0", + "endCursor": "eyJjIjo3LCJ0Ijo1LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo3LCJ0IjozLCJpIjpmYWxzZX0", + "node": { + "digest": "BB6bMUxrBJX9wANzzptKnJM3bMVFKnD4xtY8DGWthaH9", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 33, lines 229-251: +//# run-graphql --cursors {"c":7,"t":3,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "eyJjIjo3LCJ0IjoyLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo3LCJ0IjoyLCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo3LCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "CReUjLynvpq4dD4w6zekGxvSyBBQF2e3KG3K2Rs7oD8L", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/last.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/last.move new file mode 100644 index 0000000000000..1b8103b356dd2 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/ge_page/last.move @@ -0,0 +1,251 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @A --sender A + +//# run Test::M1::create --args 1 @A --sender A + +//# run Test::M1::create --args 2 @B --sender B + +//# run Test::M1::create --args 3 @B --sender B + +//# run Test::M1::create --args 4 @B --sender B + +//# create-checkpoint + +//# run Test::M1::create --args 100 @B --sender B + +//# run Test::M1::create --args 101 @B --sender B + +//# run Test::M1::create --args 102 @B --sender B + +//# run Test::M1::create --args 103 @B --sender B + +//# run Test::M1::create --args 104 @B --sender B + +//# create-checkpoint + +//# run Test::M1::create --args 100 @B --sender B + +//# run Test::M1::create --args 101 @B --sender B + +//# run Test::M1::create --args 102 @B --sender B + +//# run Test::M1::create --args 103 @B --sender B + +//# run Test::M1::create --args 104 @B --sender B + +//# create-checkpoint + +//# run Test::M1::create --args 200 @A --sender A + +//# run Test::M1::create --args 201 @B --sender B + +//# run Test::M1::create --args 202 @B --sender B + +//# run Test::M1::create --args 203 @B --sender B + +//# run Test::M1::create --args 204 @A --sender A + +//# create-checkpoint + +//# run-graphql +{ + transactionBlocks(filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + + +//# run-graphql +# startCursor 21 not scan limited, endCursor 21 is scan limited +{ + transactionBlocks(last: 1 scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":21,"i":false} +# startCursor 16, endCursor 20, both isScanLimited +# This might be a bit confusing, but the `startCursor` is 16 and not 17 because +# `scanLimit` is 5 - if we set the `startCursor` to 17, then we will never yield tx 17 +# when paginating the other way, since the cursors are exclusive. +{ + transactionBlocks(last: 1 before: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":16,"i":true} +# continuing paginating backwards with scan limit +# startCursor 11, endCursor 15, both scan limited +{ + transactionBlocks(last: 1 before: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":11,"i":true} +# startCursor is 7, endCursor is 10, both scan limited +{ + transactionBlocks(last: 1 before: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":6,"i":true} +# startCursor is 3, not scanLimited, endCursor is 5, scanLimited +# this is because we found a matching element at tx 3, but within +# the scanned window there is another tx that we need to return for +{ + transactionBlocks(last: 1 before: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":7,"t":3,"i":false} +# Reached the end +{ + transactionBlocks(last: 1 before: "@{cursor_0}" scanLimit: 5 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 6}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/invalid_limits.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/invalid_limits.exp new file mode 100644 index 0000000000000..2dc10793ef702 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/invalid_limits.exp @@ -0,0 +1,112 @@ +processed 13 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 8-29: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 31: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 33: +//# run Test::M1::create --args 0 @B --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 35: +//# run Test::M1::create --args 1 @A --sender A +created: object(4,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5, line 37: +//# run Test::M1::create --args 2 @B --sender A +created: object(5,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 6, line 39: +//# run Test::M1::create --args 3 @A --sender A +created: object(6,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 41: +//# run Test::M1::create --args 4 @B --sender A +created: object(7,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 43: +//# create-checkpoint +Checkpoint created: 2 + +task 9, lines 45-66: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": null, + "endCursor": null + }, + "edges": [] + } + } +} + +task 10, lines 68-89: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": null, + "endCursor": null + }, + "edges": [] + } + } +} + +task 11, lines 91-112: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": null, + "endCursor": null + }, + "edges": [] + } + } +} + +task 12, lines 114-135: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": null, + "endCursor": null + }, + "edges": [] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/invalid_limits.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/invalid_limits.move new file mode 100644 index 0000000000000..18c025402d2f3 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/invalid_limits.move @@ -0,0 +1,135 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// For any instance where limit is 0 or scan limit is 0, we should return an empty result + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @B --sender A + +//# run Test::M1::create --args 1 @A --sender A + +//# run Test::M1::create --args 2 @B --sender A + +//# run Test::M1::create --args 3 @A --sender A + +//# run Test::M1::create --args 4 @B --sender A + +//# create-checkpoint + +//# run-graphql +{ + transactionBlocks(first: 0 scanLimit: 2 filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(first: 2 scanLimit: 0 filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(first: 0 scanLimit: 0 filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql +{ + transactionBlocks(first: 0 filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/first.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/first.exp new file mode 100644 index 0000000000000..4cd2dabaa365b --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/first.exp @@ -0,0 +1,364 @@ +processed 22 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 10-31: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 33: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 35: +//# run Test::M1::create --args 0 @A --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 37: +//# run Test::M1::create --args 1 @B --sender B +created: object(4,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 5, line 39: +//# run Test::M1::create --args 2 @A --sender A +created: object(5,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 6, line 41: +//# run Test::M1::create --args 3 @B --sender B +created: object(6,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 43: +//# run Test::M1::create --args 4 @A --sender A +created: object(7,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 45: +//# create-checkpoint +Checkpoint created: 2 + +task 9, line 47: +//# run Test::M1::create --args 100 @B --sender B +created: object(9,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10, line 49: +//# run Test::M1::create --args 101 @A --sender A +created: object(10,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 11, line 51: +//# run Test::M1::create --args 102 @B --sender B +created: object(11,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 12, line 53: +//# run Test::M1::create --args 103 @A --sender A +created: object(12,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13, line 55: +//# run Test::M1::create --args 104 @B --sender B +created: object(13,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 14, line 57: +//# create-checkpoint +Checkpoint created: 3 + +task 15, lines 59-81: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "CReUjLynvpq4dD4w6zekGxvSyBBQF2e3KG3K2Rs7oD8L", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjozLCJpIjpmYWxzZX0", + "node": { + "digest": "FveHd4cC5ykjtknvewFjmf6V1gHYhdkuEoUEBAV3y52h", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "Hgu3LePqrpyR8Vq3Ve4L2KmvErcdcz92u7YiiotkKJ1N", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo1LCJpIjpmYWxzZX0", + "node": { + "digest": "5pVYLMHazkfshyLvmQyow1UY4vn9V481KWDxUYpX65BZ", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo2LCJpIjpmYWxzZX0", + "node": { + "digest": "2EwyAHiMofhbM5z5ty7XT1QXs4sNZfHmZLX513Ag8sD3", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo3LCJpIjpmYWxzZX0", + "node": { + "digest": "FtVAPJKNoPumPcQuyzznmZB99yhYNqdsmC7wKPj17xR3", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo4LCJpIjpmYWxzZX0", + "node": { + "digest": "4LUhoFJMmZfG71RHiRkwa9KHovrDv3S3mqUM1vu9JWKJ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo5LCJpIjpmYWxzZX0", + "node": { + "digest": "FasAf1kHei9QkZuPLBSkCdXXMtv13RbCgiywktFzx58m", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjoxMCwiaSI6ZmFsc2V9", + "node": { + "digest": "AnqDERsdbEiE26CACJa6KtJTLsggisgu7yxhMJ6mU1JZ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9", + "node": { + "digest": "GKKT2n7jc6YyJDcckP3kXhVN5FEZJEk1fN3aFzcgRyRr", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 16, lines 84-106: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjozLCJ0IjozLCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "CReUjLynvpq4dD4w6zekGxvSyBBQF2e3KG3K2Rs7oD8L", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 17, lines 108-130: +//# run-graphql --cursors {"c":4,"t":3,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo0LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo1LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "Hgu3LePqrpyR8Vq3Ve4L2KmvErcdcz92u7YiiotkKJ1N", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 18, lines 132-158: +//# run-graphql --cursors {"c":4,"t":5,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo2LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo4LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo2LCJpIjpmYWxzZX0", + "node": { + "digest": "2EwyAHiMofhbM5z5ty7XT1QXs4sNZfHmZLX513Ag8sD3", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjo0LCJ0Ijo4LCJpIjpmYWxzZX0", + "node": { + "digest": "4LUhoFJMmZfG71RHiRkwa9KHovrDv3S3mqUM1vu9JWKJ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 19, lines 160-182: +//# run-graphql --cursors {"c":4,"t":8,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjo0LCJ0Ijo5LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0IjoxMSwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0IjoxMCwiaSI6ZmFsc2V9", + "node": { + "digest": "AnqDERsdbEiE26CACJa6KtJTLsggisgu7yxhMJ6mU1JZ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 20, lines 184-207: +//# run-graphql --cursors {"c":4,"t":10,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjo0LCJ0IjoxMSwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjo0LCJ0IjoxMSwiaSI6dHJ1ZX0" + }, + "edges": [] + } + } +} + +task 21, lines 209-232: +//# run-graphql --cursors {"c":4,"t":11,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": null, + "endCursor": null + }, + "edges": [] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/first.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/first.move new file mode 100644 index 0000000000000..bd9b3d2fe2ace --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/first.move @@ -0,0 +1,232 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Without a scan limit, we would expect each query to yield a response containing two results. +// However, because we have a scanLimit of 2, we'll be limited to filtering only two candidate +// transactions per page, of which one will match the filtering criteria. + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @A --sender A + +//# run Test::M1::create --args 1 @B --sender B + +//# run Test::M1::create --args 2 @A --sender A + +//# run Test::M1::create --args 3 @B --sender B + +//# run Test::M1::create --args 4 @A --sender A + +//# create-checkpoint + +//# run Test::M1::create --args 100 @B --sender B + +//# run Test::M1::create --args 101 @A --sender A + +//# run Test::M1::create --args 102 @B --sender B + +//# run Test::M1::create --args 103 @A --sender A + +//# run Test::M1::create --args 104 @B --sender B + +//# create-checkpoint + +//# run-graphql +# ten transactions total +{ + transactionBlocks(first: 50 filter: {afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + + +//# run-graphql +# startCursor 2, endCursor 3, both scan limited +{ + transactionBlocks(first: 3 scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":3,"i":true} +# startCursor: 4, endCursor 5, both scan limited +{ + transactionBlocks(first: 3 scanLimit: 2 after: "@{cursor_0}" filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":5,"i":true} +# note the changes: first 3 -> 4, scanLimit 2 -> 3 +# startCursor: 6, endCursor: 8 both scanLimited +# because we've exhausted all matches in the scanned window, +# we set the endCursor to the final tx scanned, rather than snapping +# to the last matched tx +{ + transactionBlocks(first: 4 scanLimit: 3 after: "@{cursor_0}" filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":8,"i":true} +# startCursor: 9, endCursor: 11 both scanLimited +{ + transactionBlocks(first: 4 scanLimit: 3 after: "@{cursor_0}" filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":10,"i":false} +# using the last element's cursor from the previous query +# will yield an empty set, fixed on the last scannable tx +{ + transactionBlocks(first: 4 scanLimit: 3 after: "@{cursor_0}" filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":11,"i":true} +# trying to paginate on the `endCursor` even though hasNextPage is false +# cursors are null, both page flags are false +{ + transactionBlocks(first: 4 scanLimit: 3 after: "@{cursor_0}" filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/last.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/last.exp new file mode 100644 index 0000000000000..714548cfc5c09 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/last.exp @@ -0,0 +1,365 @@ +processed 22 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 10-31: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 33: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 35: +//# run Test::M1::create --args 0 @A --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 37: +//# run Test::M1::create --args 1 @B --sender B +created: object(4,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 5, line 39: +//# run Test::M1::create --args 2 @A --sender A +created: object(5,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 6, line 41: +//# run Test::M1::create --args 3 @B --sender B +created: object(6,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 43: +//# run Test::M1::create --args 4 @A --sender A +created: object(7,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 45: +//# create-checkpoint +Checkpoint created: 2 + +task 9, line 47: +//# run Test::M1::create --args 100 @B --sender B +created: object(9,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10, line 49: +//# run Test::M1::create --args 101 @A --sender A +created: object(10,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 11, line 51: +//# run Test::M1::create --args 102 @B --sender B +created: object(11,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 12, line 53: +//# run Test::M1::create --args 103 @A --sender A +created: object(12,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13, line 55: +//# run Test::M1::create --args 104 @B --sender B +created: object(13,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 14, line 57: +//# create-checkpoint +Checkpoint created: 3 + +task 15, lines 59-81: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "CReUjLynvpq4dD4w6zekGxvSyBBQF2e3KG3K2Rs7oD8L", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjozLCJpIjpmYWxzZX0", + "node": { + "digest": "FveHd4cC5ykjtknvewFjmf6V1gHYhdkuEoUEBAV3y52h", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "Hgu3LePqrpyR8Vq3Ve4L2KmvErcdcz92u7YiiotkKJ1N", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo1LCJpIjpmYWxzZX0", + "node": { + "digest": "5pVYLMHazkfshyLvmQyow1UY4vn9V481KWDxUYpX65BZ", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo2LCJpIjpmYWxzZX0", + "node": { + "digest": "2EwyAHiMofhbM5z5ty7XT1QXs4sNZfHmZLX513Ag8sD3", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo3LCJpIjpmYWxzZX0", + "node": { + "digest": "FtVAPJKNoPumPcQuyzznmZB99yhYNqdsmC7wKPj17xR3", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo4LCJpIjpmYWxzZX0", + "node": { + "digest": "4LUhoFJMmZfG71RHiRkwa9KHovrDv3S3mqUM1vu9JWKJ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0Ijo5LCJpIjpmYWxzZX0", + "node": { + "digest": "FasAf1kHei9QkZuPLBSkCdXXMtv13RbCgiywktFzx58m", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjoxMCwiaSI6ZmFsc2V9", + "node": { + "digest": "AnqDERsdbEiE26CACJa6KtJTLsggisgu7yxhMJ6mU1JZ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + }, + { + "cursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9", + "node": { + "digest": "GKKT2n7jc6YyJDcckP3kXhVN5FEZJEk1fN3aFzcgRyRr", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 16, lines 83-105: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "eyJjIjozLCJ0IjoxMCwiaSI6dHJ1ZX0", + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoxMCwiaSI6ZmFsc2V9", + "node": { + "digest": "AnqDERsdbEiE26CACJa6KtJTLsggisgu7yxhMJ6mU1JZ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 17, lines 107-129: +//# run-graphql --cursors {"c":4,"t":10,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo4LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo5LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo4LCJpIjpmYWxzZX0", + "node": { + "digest": "4LUhoFJMmZfG71RHiRkwa9KHovrDv3S3mqUM1vu9JWKJ", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + } + ] + } + } +} + +task 18, lines 131-154: +//# run-graphql --cursors {"c":4,"t":8,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo2LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo3LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo2LCJpIjpmYWxzZX0", + "node": { + "digest": "2EwyAHiMofhbM5z5ty7XT1QXs4sNZfHmZLX513Ag8sD3", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 19, lines 156-178: +//# run-graphql --cursors {"c":4,"t":6,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0Ijo0LCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0Ijo1LCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0Ijo0LCJpIjpmYWxzZX0", + "node": { + "digest": "Hgu3LePqrpyR8Vq3Ve4L2KmvErcdcz92u7YiiotkKJ1N", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 20, lines 180-202: +//# run-graphql --cursors {"c":4,"t":4,"i":false} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "eyJjIjo0LCJ0IjoyLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjo0LCJ0IjozLCJpIjp0cnVlfQ" + }, + "edges": [ + { + "cursor": "eyJjIjo0LCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "CReUjLynvpq4dD4w6zekGxvSyBBQF2e3KG3K2Rs7oD8L", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} + +task 21, lines 205-227: +//# run-graphql --cursors {"c":4,"t":2,"i":true} +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": null, + "endCursor": null + }, + "edges": [] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/last.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/last.move new file mode 100644 index 0000000000000..9a1280bed8b3d --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/le_page/last.move @@ -0,0 +1,227 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Without a scan limit, we would expect each query to yield a response containing two results. +// However, because we have a scanLimit of 2, we'll be limited to filtering only two candidate +// transactions per page, of which one will match the filtering criteria. + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @A --sender A + +//# run Test::M1::create --args 1 @B --sender B + +//# run Test::M1::create --args 2 @A --sender A + +//# run Test::M1::create --args 3 @B --sender B + +//# run Test::M1::create --args 4 @A --sender A + +//# create-checkpoint + +//# run Test::M1::create --args 100 @B --sender B + +//# run Test::M1::create --args 101 @A --sender A + +//# run Test::M1::create --args 102 @B --sender B + +//# run Test::M1::create --args 103 @A --sender A + +//# run Test::M1::create --args 104 @B --sender B + +//# create-checkpoint + +//# run-graphql +# ten transactions total +{ + transactionBlocks(last: 50 filter: {afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql +# startCursor: 10, endCursor: 11, both scan limited +{ + transactionBlocks(last: 3 scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":10,"i":true} +# startCursor: 8, endCursor: 9, both scan limited +{ + transactionBlocks(last: 3 before: "@{cursor_0}" scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":8,"i":false} +# use result's cursor instead of boundary cursor +# startCursor: 6, endCursor: 7, both scan limited +{ + transactionBlocks(last: 3 before: "@{cursor_0}" scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":6,"i":true} +# startCursor: 4, endCursor: 5, both scan limited +{ + transactionBlocks(last: 3 before: "@{cursor_0}" scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + +//# run-graphql --cursors {"c":4,"t":4,"i":false} +# reached the end with this query +{ + transactionBlocks(last: 3 before: "@{cursor_0}" scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} + + +//# run-graphql --cursors {"c":4,"t":2,"i":true} +# cursors are null, and page flags are both false +{ + transactionBlocks(last: 3 before: "@{cursor_0}" scanLimit: 2 filter: {recvAddress: "@{A}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/require.exp b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/require.exp new file mode 100644 index 0000000000000..1e5207ad10ed9 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/require.exp @@ -0,0 +1,606 @@ +processed 25 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 6-27: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5798800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 29: +//# create-checkpoint +Checkpoint created: 1 + +task 3, line 31: +//# run Test::M1::create --args 0 @B --sender A +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 4, line 33: +//# run Test::M1::create --args 1 @B --sender A +created: object(4,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5, line 35: +//# run Test::M1::create --args 2 @B --sender A +created: object(5,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 6, line 37: +//# run Test::M1::create --args 3 @B --sender A +created: object(6,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 7, line 39: +//# run Test::M1::create --args 4 @B --sender A +created: object(7,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 8, line 41: +//# create-checkpoint +Checkpoint created: 2 + +task 9, line 43: +//# run Test::M1::create --args 100 @B --sender A +created: object(9,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 10, line 45: +//# run Test::M1::create --args 101 @B --sender A +created: object(10,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 11, line 47: +//# run Test::M1::create --args 102 @B --sender A +created: object(11,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 12, line 49: +//# run Test::M1::create --args 103 @B --sender A +created: object(12,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 13, line 51: +//# run Test::M1::create --args 104 @B --sender A +created: object(13,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 14, line 53: +//# create-checkpoint +Checkpoint created: 3 + +task 15, lines 55-74: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9", + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0" + }, + "nodes": [ + { + "digest": "HzyC8gcn4m1ymKxYSpWMaNnmbrqm4hX7UBteJ4me3LFd", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "DiywoRFzC33smQhVf5K7AcM853XFgfgFxBGErLTEvVWi", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "7MgEfj6QXfsjDFtvJSAE9FNL3RYt8Kdw21NnfuuVXkbt", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "B8mg7JvC64cnh656yuwHyXyFPULutxBECMFkCPQNStmZ", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "7i7Evom2DUeS1PYxKwDnLfoZpv2r6kxdGoEWxXdFA9xV", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "5HoMDKMTYX3gibs8VroZUeSC3134MroLJN7hfAVZxdPM", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "BU8q4bm7XjaZiV8cPmchVp6SsmU8cmBWNUVmFTUNAHfb", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "HckXhLnDYV8hfMh1p8M8r7dpEpvzp5GpG9oHzv9dmv4R", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "Hvh3oJuoTeRYRU2wkTriwDsozpaay5c7dhRS7Ru3S2S5", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "ADhRGJc24AQCvUnQNgkfjnna3hsTFK51YTgq5J5VAKQr", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + ] + } + } +} + +task 16, lines 76-95: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6ZmFsc2V9", + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0" + }, + "nodes": [ + { + "digest": "HzyC8gcn4m1ymKxYSpWMaNnmbrqm4hX7UBteJ4me3LFd", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "DiywoRFzC33smQhVf5K7AcM853XFgfgFxBGErLTEvVWi", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "7MgEfj6QXfsjDFtvJSAE9FNL3RYt8Kdw21NnfuuVXkbt", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "B8mg7JvC64cnh656yuwHyXyFPULutxBECMFkCPQNStmZ", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "7i7Evom2DUeS1PYxKwDnLfoZpv2r6kxdGoEWxXdFA9xV", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "5HoMDKMTYX3gibs8VroZUeSC3134MroLJN7hfAVZxdPM", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "BU8q4bm7XjaZiV8cPmchVp6SsmU8cmBWNUVmFTUNAHfb", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "HckXhLnDYV8hfMh1p8M8r7dpEpvzp5GpG9oHzv9dmv4R", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "Hvh3oJuoTeRYRU2wkTriwDsozpaay5c7dhRS7Ru3S2S5", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "ADhRGJc24AQCvUnQNgkfjnna3hsTFK51YTgq5J5VAKQr", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + ] + } + } +} + +task 17, lines 97-116: +//# run-graphql +Response: { + "data": null, + "errors": [ + { + "message": "A scan limit must be specified for the given filter combination", + "locations": [ + { + "line": 3, + "column": 3 + } + ], + "path": [ + "transactionBlocks" + ], + "extensions": { + "code": "BAD_USER_INPUT" + } + } + ] +} + +task 18, lines 118-137: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6dHJ1ZX0", + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjp0cnVlfQ" + }, + "nodes": [ + { + "digest": "HzyC8gcn4m1ymKxYSpWMaNnmbrqm4hX7UBteJ4me3LFd", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "DiywoRFzC33smQhVf5K7AcM853XFgfgFxBGErLTEvVWi", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "7MgEfj6QXfsjDFtvJSAE9FNL3RYt8Kdw21NnfuuVXkbt", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "B8mg7JvC64cnh656yuwHyXyFPULutxBECMFkCPQNStmZ", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "7i7Evom2DUeS1PYxKwDnLfoZpv2r6kxdGoEWxXdFA9xV", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "5HoMDKMTYX3gibs8VroZUeSC3134MroLJN7hfAVZxdPM", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "BU8q4bm7XjaZiV8cPmchVp6SsmU8cmBWNUVmFTUNAHfb", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "HckXhLnDYV8hfMh1p8M8r7dpEpvzp5GpG9oHzv9dmv4R", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "Hvh3oJuoTeRYRU2wkTriwDsozpaay5c7dhRS7Ru3S2S5", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "ADhRGJc24AQCvUnQNgkfjnna3hsTFK51YTgq5J5VAKQr", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + ] + } + } +} + +task 19, lines 139-158: +//# run-graphql +Response: { + "data": null, + "errors": [ + { + "message": "A scan limit must be specified for the given filter combination", + "locations": [ + { + "line": 3, + "column": 3 + } + ], + "path": [ + "transactionBlocks" + ], + "extensions": { + "code": "BAD_USER_INPUT" + } + } + ] +} + +task 20, lines 160-179: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6dHJ1ZX0", + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjp0cnVlfQ" + }, + "nodes": [ + { + "digest": "HzyC8gcn4m1ymKxYSpWMaNnmbrqm4hX7UBteJ4me3LFd", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "DiywoRFzC33smQhVf5K7AcM853XFgfgFxBGErLTEvVWi", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "7MgEfj6QXfsjDFtvJSAE9FNL3RYt8Kdw21NnfuuVXkbt", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "B8mg7JvC64cnh656yuwHyXyFPULutxBECMFkCPQNStmZ", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "7i7Evom2DUeS1PYxKwDnLfoZpv2r6kxdGoEWxXdFA9xV", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + }, + { + "digest": "5HoMDKMTYX3gibs8VroZUeSC3134MroLJN7hfAVZxdPM", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "BU8q4bm7XjaZiV8cPmchVp6SsmU8cmBWNUVmFTUNAHfb", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "HckXhLnDYV8hfMh1p8M8r7dpEpvzp5GpG9oHzv9dmv4R", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "Hvh3oJuoTeRYRU2wkTriwDsozpaay5c7dhRS7Ru3S2S5", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + }, + { + "digest": "ADhRGJc24AQCvUnQNgkfjnna3hsTFK51YTgq5J5VAKQr", + "effects": { + "checkpoint": { + "sequenceNumber": 3 + } + } + } + ] + } + } +} + +task 21, lines 181-200: +//# run-graphql +Response: { + "data": null, + "errors": [ + { + "message": "A scan limit must be specified for the given filter combination", + "locations": [ + { + "line": 3, + "column": 3 + } + ], + "path": [ + "transactionBlocks" + ], + "extensions": { + "code": "BAD_USER_INPUT" + } + } + ] +} + +task 22, lines 202-221: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6dHJ1ZX0", + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjp0cnVlfQ" + }, + "nodes": [] + } + } +} + +task 23, lines 224-243: +//# run-graphql +Response: { + "data": null, + "errors": [ + { + "message": "A scan limit must be specified for the given filter combination", + "locations": [ + { + "line": 3, + "column": 3 + } + ], + "path": [ + "transactionBlocks" + ], + "extensions": { + "code": "BAD_USER_INPUT" + } + } + ] +} + +task 24, lines 245-269: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "eyJjIjozLCJ0IjoyLCJpIjp0cnVlfQ", + "endCursor": "eyJjIjozLCJ0IjoxMSwiaSI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJjIjozLCJ0IjoyLCJpIjpmYWxzZX0", + "node": { + "digest": "HzyC8gcn4m1ymKxYSpWMaNnmbrqm4hX7UBteJ4me3LFd", + "effects": { + "checkpoint": { + "sequenceNumber": 2 + } + } + } + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/require.move b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/require.move new file mode 100644 index 0000000000000..90b5570b7ded3 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/transactions/scan_limit/require.move @@ -0,0 +1,269 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + public struct Object has key, store { + id: UID, + value: u64, + } + + public entry fun create(value: u64, recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Object { id: object::new(ctx), value }, + recipient + ) + } + + public fun swap_value_and_send(mut lhs: Object, mut rhs: Object, recipient: address) { + let tmp = lhs.value; + lhs.value = rhs.value; + rhs.value = tmp; + transfer::public_transfer(lhs, recipient); + transfer::public_transfer(rhs, recipient); + } +} + +//# create-checkpoint + +//# run Test::M1::create --args 0 @B --sender A + +//# run Test::M1::create --args 1 @B --sender A + +//# run Test::M1::create --args 2 @B --sender A + +//# run Test::M1::create --args 3 @B --sender A + +//# run Test::M1::create --args 4 @B --sender A + +//# create-checkpoint + +//# run Test::M1::create --args 100 @B --sender A + +//# run Test::M1::create --args 101 @B --sender A + +//# run Test::M1::create --args 102 @B --sender A + +//# run Test::M1::create --args 103 @B --sender A + +//# run Test::M1::create --args 104 @B --sender A + +//# create-checkpoint + +//# run-graphql +# Expect ten results +{ + transactionBlocks(filter: {recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +# Don't need scanLimit with sender +{ + transactionBlocks(filter: {signAddress: "@{A}" recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +# scanLimit required +{ + transactionBlocks(filter: {signAddress: "@{A}" recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4 function: "@{Test}::M1::create"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +# valid +{ + transactionBlocks(scanLimit: 50 filter: {signAddress: "@{A}" recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4 function: "@{Test}::M1::create"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +# scanLimit required +{ + transactionBlocks(filter: {signAddress: "@{A}" recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4 kind: PROGRAMMABLE_TX}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +# valid +{ + transactionBlocks(scanLimit: 50 filter: {signAddress: "@{A}" recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4 kind: PROGRAMMABLE_TX}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +# scanLimit required +{ + transactionBlocks(filter: {signAddress: "@{A}" recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4 inputObject: "@{obj_3_0}"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +# valid +{ + transactionBlocks(scanLimit: 50 filter: {signAddress: "@{A}" recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4 inputObject: "@{obj_3_0}"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + + +//# run-graphql +# scanLimit required +{ + transactionBlocks(filter: {signAddress: "@{A}" recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4 changedObject: "@{obj_3_0}"}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + nodes { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } +} + +//# run-graphql +# Only one of the transactions will match this filter +# Because scanLimit is specified, the boundary cursors should be at 2 and 11, +# and both will indicate is_scan_limited +{ + transactionBlocks(scanLimit: 50 filter: {signAddress: "@{A}" recvAddress: "@{B}" afterCheckpoint: 1 beforeCheckpoint: 4 changedObject: "@{obj_3_0}"}) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + } + } +} diff --git a/crates/sui-graphql-rpc/examples/epoch/with_tx_block_connection_latest_epoch.graphql b/crates/sui-graphql-rpc/examples/epoch/with_tx_block_connection_latest_epoch.graphql index 9fdc8ea5a7fa3..62bc23f9f5855 100644 --- a/crates/sui-graphql-rpc/examples/epoch/with_tx_block_connection_latest_epoch.graphql +++ b/crates/sui-graphql-rpc/examples/epoch/with_tx_block_connection_latest_epoch.graphql @@ -1,6 +1,6 @@ { epoch { - transactionBlocks(first: 20, after: "eyJjIjoyNjkzMzMyNCwidCI6MTEwMTYxMDQ4MywidGMiOjI2ODUxMjQ4fQ") { + transactionBlocks(first: 20, after: "eyJjIjoyNjkzMzMyNCwidCI6MTEwMTYxMDQ4MywiaSI6ZmFsc2V9") { pageInfo { hasNextPage endCursor diff --git a/crates/sui-graphql-rpc/schema.graphql b/crates/sui-graphql-rpc/schema.graphql index db2f967fa4085..defc55cc329d2 100644 --- a/crates/sui-graphql-rpc/schema.graphql +++ b/crates/sui-graphql-rpc/schema.graphql @@ -98,8 +98,27 @@ type Address implements IOwner { """ Similar behavior to the `transactionBlocks` in Query but supporting the additional `AddressTransactionBlockRelationship` filter, which defaults to `SIGN`. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - transactionBlocks(first: Int, after: String, last: Int, before: String, relation: AddressTransactionBlockRelationship, filter: TransactionBlockFilter): TransactionBlockConnection! + transactionBlocks(first: Int, after: String, last: Int, before: String, relation: AddressTransactionBlockRelationship, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! } type AddressConnection { @@ -409,8 +428,25 @@ type Checkpoint { epoch: Epoch """ Transactions in this checkpoint. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range consists of all transactions in this checkpoint. """ - transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! } type CheckpointConnection { @@ -517,8 +553,27 @@ type Coin implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -676,8 +731,27 @@ type CoinMetadata implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -1093,8 +1167,25 @@ type Epoch { checkpoints(first: Int, after: String, last: Int, before: String): CheckpointConnection! """ The epoch's corresponding transaction blocks. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range consists of all transactions in this epoch. """ - transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! } type Event { @@ -1429,7 +1520,7 @@ interface IObject { """ The transaction blocks that sent objects to this object. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -1972,8 +2063,27 @@ type MoveObject implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -2161,8 +2271,27 @@ type MovePackage implements IObject & IOwner { The transaction blocks that sent objects to this package. Note that objects that have been sent to a package become inaccessible. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the package's content. """ @@ -2540,8 +2669,27 @@ type Object implements IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -3142,8 +3290,27 @@ type Query { checkpoints(first: Int, after: String, last: Int, before: String): CheckpointConnection! """ The transaction blocks that exist in the network. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The events that exist in the network. """ @@ -3361,6 +3528,14 @@ type ServiceConfig { Maximum nesting allowed in struct fields when calculating the layout of a single Move Type. """ maxMoveValueDepth: Int! + """ + Maximum number of transaction ids that can be passed to a `TransactionBlockFilter`. + """ + maxTransactionIds: Int! + """ + Maximum number of candidates to scan when gathering a page of results. + """ + maxScanLimit: Int! } """ @@ -3578,8 +3753,27 @@ type StakedSui implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -3780,8 +3974,27 @@ type SuinsRegistration implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ diff --git a/crates/sui-graphql-rpc/src/config.rs b/crates/sui-graphql-rpc/src/config.rs index 71590683d308f..42f6feab0b084 100644 --- a/crates/sui-graphql-rpc/src/config.rs +++ b/crates/sui-graphql-rpc/src/config.rs @@ -92,6 +92,10 @@ pub struct Limits { pub max_type_nodes: u32, /// Maximum deph of a move value. pub max_move_value_depth: u32, + /// Maximum number of transaction ids that can be passed to a `TransactionBlockFilter`. + pub max_transaction_ids: u32, + /// Maximum number of candidates to scan when gathering a page of results. + pub max_scan_limit: u32, } #[GraphQLConfig] @@ -282,6 +286,16 @@ impl ServiceConfig { async fn max_move_value_depth(&self) -> u32 { self.limits.max_move_value_depth } + + /// Maximum number of transaction ids that can be passed to a `TransactionBlockFilter`. + async fn max_transaction_ids(&self) -> u32 { + self.limits.max_transaction_ids + } + + /// Maximum number of candidates to scan when gathering a page of results. + async fn max_scan_limit(&self) -> u32 { + self.limits.max_scan_limit + } } impl TxExecFullNodeConfig { @@ -452,6 +466,10 @@ impl Default for Limits { max_type_nodes: 256, // max_move_value_depth: 128, + // Filter-specific limits, such as the number of transaction ids that can be specified + // for the `TransactionBlockFilter`. + max_transaction_ids: 1000, + max_scan_limit: 100_000_000, } } } @@ -514,6 +532,8 @@ mod tests { max-type-argument-width = 64 max-type-nodes = 128 max-move-value-depth = 256 + max-transaction-ids = 11 + max-scan-limit = 50 "#, ) .unwrap(); @@ -533,6 +553,8 @@ mod tests { max_type_argument_width: 64, max_type_nodes: 128, max_move_value_depth: 256, + max_transaction_ids: 11, + max_scan_limit: 50, }, ..Default::default() }; @@ -596,6 +618,8 @@ mod tests { max-type-argument-width = 64 max-type-nodes = 128 max-move-value-depth = 256 + max-transaction-ids = 42 + max-scan-limit = 420 [experiments] test-flag = true @@ -618,6 +642,8 @@ mod tests { max_type_argument_width: 64, max_type_nodes: 128, max_move_value_depth: 256, + max_transaction_ids: 42, + max_scan_limit: 420, }, disabled_features: BTreeSet::from([FunctionalGroup::Analytics]), experiments: Experiments { test_flag: true }, diff --git a/crates/sui-graphql-rpc/src/connection.rs b/crates/sui-graphql-rpc/src/connection.rs new file mode 100644 index 0000000000000..4c48fc1727d0e --- /dev/null +++ b/crates/sui-graphql-rpc/src/connection.rs @@ -0,0 +1,125 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::borrow::Cow; +use std::marker::PhantomData; + +use async_graphql::connection::{ + ConnectionNameType, CursorType, DefaultConnectionName, DefaultEdgeName, Edge, EdgeNameType, + EmptyFields, EnableNodesField, NodesFieldSwitcherSealed, PageInfo, +}; +use async_graphql::{Object, ObjectType, OutputType, TypeName}; + +/// Mirrors the `Connection` type from async-graphql, with the exception that if `start_cursor` and/ +/// or `end_cursor` is set on the struct, then when `page_info` is called, it will use those values +/// before deferring to `edges`. The default implementation derives these cursors from the first and +/// last element of `edges`, so if `edges` is empty, both are set to null. This is undesirable for +/// queries that make use of `scan_limit`; when the scan limit is reached, a caller can continue to +/// paginate forwards or backwards until all candidates in the scanning range have been visited, +/// even if the current page yields no results. +pub(crate) struct ScanConnection< + Cursor, + Node, + EdgeFields = EmptyFields, + Name = DefaultConnectionName, + EdgeName = DefaultEdgeName, + NodesField = EnableNodesField, +> where + Cursor: CursorType + Send + Sync, + Node: OutputType, + EdgeFields: ObjectType, + Name: ConnectionNameType, + EdgeName: EdgeNameType, + NodesField: NodesFieldSwitcherSealed, +{ + _mark1: PhantomData, + _mark2: PhantomData, + _mark3: PhantomData, + pub edges: Vec>, + pub has_previous_page: bool, + pub has_next_page: bool, + pub start_cursor: Option, + pub end_cursor: Option, +} + +#[Object(name_type)] +impl + ScanConnection +where + Cursor: CursorType + Send + Sync, + Node: OutputType, + EdgeFields: ObjectType, + Name: ConnectionNameType, + EdgeName: EdgeNameType, +{ + /// Information to aid in pagination. + async fn page_info(&self) -> PageInfo { + // Unlike the default implementation, this Connection will use `start_cursor` and + // `end_cursor` if they are `Some`. + PageInfo { + has_previous_page: self.has_previous_page, + has_next_page: self.has_next_page, + start_cursor: self + .start_cursor + .clone() + .or_else(|| self.edges.first().map(|edge| edge.cursor.encode_cursor())), + end_cursor: self + .end_cursor + .clone() + .or_else(|| self.edges.last().map(|edge| edge.cursor.encode_cursor())), + } + } + + /// A list of edges. + #[inline] + async fn edges(&self) -> &[Edge] { + &self.edges + } + + /// A list of nodes. + async fn nodes(&self) -> Vec<&Node> { + self.edges.iter().map(|e| &e.node).collect() + } +} + +impl + ScanConnection +where + Cursor: CursorType + Send + Sync, + Node: OutputType, + EdgeFields: ObjectType, + Name: ConnectionNameType, + EdgeName: EdgeNameType, + NodesField: NodesFieldSwitcherSealed, +{ + /// Create a new connection. + #[inline] + pub fn new(has_previous_page: bool, has_next_page: bool) -> Self { + ScanConnection { + _mark1: PhantomData, + _mark2: PhantomData, + _mark3: PhantomData, + edges: Vec::new(), + has_previous_page, + has_next_page, + start_cursor: None, + end_cursor: None, + } + } +} + +impl TypeName + for ScanConnection +where + Cursor: CursorType + Send + Sync, + Node: OutputType, + EdgeFields: ObjectType, + Name: ConnectionNameType, + EdgeName: EdgeNameType, + NodesField: NodesFieldSwitcherSealed, +{ + #[inline] + fn type_name() -> Cow<'static, str> { + Name::type_name::().into() + } +} diff --git a/crates/sui-graphql-rpc/src/consistency.rs b/crates/sui-graphql-rpc/src/consistency.rs index a4719e5855cdd..285e6ce8f36ba 100644 --- a/crates/sui-graphql-rpc/src/consistency.rs +++ b/crates/sui-graphql-rpc/src/consistency.rs @@ -7,7 +7,7 @@ use sui_indexer::models::objects::StoredHistoryObject; use crate::raw_query::RawQuery; use crate::types::available_range::AvailableRange; -use crate::types::cursor::{JsonCursor, Page}; +use crate::types::cursor::{JsonCursor, Page, ScanLimited}; use crate::types::object::Cursor; use crate::{filter, query}; @@ -59,6 +59,10 @@ impl Checkpointed for JsonCursor { } } +impl ScanLimited for JsonCursor {} + +impl ScanLimited for JsonCursor {} + /// Constructs a `RawQuery` against the `objects_snapshot` and `objects_history` table to fetch /// objects that satisfy some filtering criteria `filter_fn` within the provided checkpoint `range`. /// The `objects_snapshot` table contains the latest versions of objects up to a checkpoint sequence diff --git a/crates/sui-graphql-rpc/src/data/pg.rs b/crates/sui-graphql-rpc/src/data/pg.rs index 71b45248a2e2f..cdf57f7d542b9 100644 --- a/crates/sui-graphql-rpc/src/data/pg.rs +++ b/crates/sui-graphql-rpc/src/data/pg.rs @@ -1,8 +1,6 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::time::Instant; - use super::QueryExecutor; use crate::{config::Limits, error::Error, metrics::Metrics}; use async_trait::async_trait; @@ -12,6 +10,8 @@ use diesel::{ query_dsl::LoadQuery, QueryResult, RunQueryDsl, }; +use std::fmt; +use std::time::Instant; use sui_indexer::indexer_reader::IndexerReader; use sui_indexer::{run_query_async, run_query_repeatable_async, spawn_read_only_blocking}; @@ -29,6 +29,8 @@ pub(crate) struct PgConnection<'c> { conn: &'c mut diesel::PgConnection, } +pub(crate) struct ByteaLiteral<'a>(pub &'a [u8]); + impl PgExecutor { pub(crate) fn new( inner: IndexerReader, @@ -118,6 +120,16 @@ impl<'c> super::DbConnection for PgConnection<'c> { } } +impl fmt::Display for ByteaLiteral<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "'\\x{}'::bytea", hex::encode(self.0)) + } +} + +pub(crate) fn bytea_literal(slice: &[u8]) -> ByteaLiteral<'_> { + ByteaLiteral(slice) +} + /// Support for calculating estimated query cost using EXPLAIN and then logging it. mod query_cost { use super::*; diff --git a/crates/sui-graphql-rpc/src/lib.rs b/crates/sui-graphql-rpc/src/lib.rs index c2f7cd3f8687b..e299aa241eec9 100644 --- a/crates/sui-graphql-rpc/src/lib.rs +++ b/crates/sui-graphql-rpc/src/lib.rs @@ -4,6 +4,7 @@ pub use sui_graphql_rpc_client as client; pub mod commands; pub mod config; +pub(crate) mod connection; pub(crate) mod consistency; pub mod context_data; pub(crate) mod data; diff --git a/crates/sui-graphql-rpc/src/raw_query.rs b/crates/sui-graphql-rpc/src/raw_query.rs index e372d478fb22f..55fed2d96c2f3 100644 --- a/crates/sui-graphql-rpc/src/raw_query.rs +++ b/crates/sui-graphql-rpc/src/raw_query.rs @@ -179,6 +179,27 @@ macro_rules! or_filter { }}; } +/// Accepts two `RawQuery` instances and a third expression consisting of which columns to join on. +#[macro_export] +macro_rules! inner_join { + ($lhs:expr, $alias:expr => $rhs_query:expr, using: [$using:expr $(, $more_using:expr)*]) => {{ + use $crate::raw_query::RawQuery; + + let (lhs_sql, mut binds) = $lhs.finish(); + let (rhs_sql, rhs_binds) = $rhs_query.finish(); + + binds.extend(rhs_binds); + + let sql = format!( + "{lhs_sql} INNER JOIN ({rhs_sql}) AS {} USING ({})", + $alias, + stringify!($using $(, $more_using)*), + ); + + RawQuery::new(sql, binds) + }}; +} + /// Accepts a `SELECT FROM` format string and optional subqueries. If subqueries are provided, there /// should be curly braces `{}` in the format string to interpolate each subquery's sql string into. /// Concatenates subqueries to the `SELECT FROM` clause, and creates a new `RawQuery` from the @@ -193,7 +214,8 @@ macro_rules! query { }; // Expects a select clause and one or more subqueries. The select clause should contain curly - // braces for subqueries to be interpolated into. + // braces for subqueries to be interpolated into. Use when the subqueries can be aliased + // directly in the select statement. ($select:expr $(,$subquery:expr)+) => {{ use $crate::raw_query::RawQuery; let mut binds = vec![]; diff --git a/crates/sui-graphql-rpc/src/types/address.rs b/crates/sui-graphql-rpc/src/types/address.rs index 6ba4dd390c79d..abd07bc9701e9 100644 --- a/crates/sui-graphql-rpc/src/types/address.rs +++ b/crates/sui-graphql-rpc/src/types/address.rs @@ -1,6 +1,8 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::connection::ScanConnection; + use super::{ balance::{self, Balance}, coin::Coin, @@ -135,6 +137,25 @@ impl Address { /// Similar behavior to the `transactionBlocks` in Query but supporting the additional /// `AddressTransactionBlockRelationship` filter, which defaults to `SIGN`. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range includes all transactions known to GraphQL, but it can be + /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + /// `afterCheckpoint` and `atCheckpoint` filters. async fn transaction_blocks( &self, ctx: &Context<'_>, @@ -144,7 +165,8 @@ impl Address { before: Option, relation: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { use AddressTransactionBlockRelationship as R; let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?; @@ -160,17 +182,12 @@ impl Address { ..Default::default() }, }) else { - return Ok(Connection::new(false, false)); + return Ok(ScanConnection::new(false, false)); }; - TransactionBlock::paginate( - ctx.data_unchecked(), - page, - filter, - self.checkpoint_viewed_at, - ) - .await - .extend() + TransactionBlock::paginate(ctx, page, filter, self.checkpoint_viewed_at, scan_limit) + .await + .extend() } } diff --git a/crates/sui-graphql-rpc/src/types/balance.rs b/crates/sui-graphql-rpc/src/types/balance.rs index 8e4d199df0be0..57eb83935d407 100644 --- a/crates/sui-graphql-rpc/src/types/balance.rs +++ b/crates/sui-graphql-rpc/src/types/balance.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use super::available_range::AvailableRange; -use super::cursor::{self, Page, RawPaginated, Target}; +use super::cursor::{self, Page, RawPaginated, ScanLimited, Target}; use super::uint53::UInt53; use super::{big_int::BigInt, move_type::MoveType, sui_address::SuiAddress}; use crate::consistency::Checkpointed; @@ -161,6 +161,8 @@ impl Checkpointed for Cursor { } } +impl ScanLimited for Cursor {} + impl TryFrom for Balance { type Error = Error; diff --git a/crates/sui-graphql-rpc/src/types/checkpoint.rs b/crates/sui-graphql-rpc/src/types/checkpoint.rs index f59ad8c46ae17..852c492967f21 100644 --- a/crates/sui-graphql-rpc/src/types/checkpoint.rs +++ b/crates/sui-graphql-rpc/src/types/checkpoint.rs @@ -5,7 +5,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use super::{ base64::Base64, - cursor::{self, Page, Paginated, Target}, + cursor::{self, Page, Paginated, ScanLimited, Target}, date_time::DateTime, digest::Digest, epoch::Epoch, @@ -13,7 +13,7 @@ use super::{ transaction_block::{self, TransactionBlock, TransactionBlockFilter}, uint53::UInt53, }; -use crate::consistency::Checkpointed; +use crate::{connection::ScanConnection, consistency::Checkpointed}; use crate::{ data::{self, Conn, DataLoader, Db, DbConnection, QueryExecutor}, error::Error, @@ -144,6 +144,23 @@ impl Checkpoint { } /// Transactions in this checkpoint. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range consists of all transactions in this checkpoint. async fn transaction_blocks( &self, ctx: &Context<'_>, @@ -152,7 +169,8 @@ impl Checkpoint { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?; let Some(filter) = filter @@ -162,17 +180,12 @@ impl Checkpoint { ..Default::default() }) else { - return Ok(Connection::new(false, false)); + return Ok(ScanConnection::new(false, false)); }; - TransactionBlock::paginate( - ctx.data_unchecked(), - page, - filter, - self.checkpoint_viewed_at, - ) - .await - .extend() + TransactionBlock::paginate(ctx, page, filter, self.checkpoint_viewed_at, scan_limit) + .await + .extend() } } @@ -373,6 +386,8 @@ impl Checkpointed for Cursor { } } +impl ScanLimited for Cursor {} + #[async_trait::async_trait] impl Loader for Db { type Value = Checkpoint; diff --git a/crates/sui-graphql-rpc/src/types/coin.rs b/crates/sui-graphql-rpc/src/types/coin.rs index 537c7ca2c44b0..d9654bbe87f10 100644 --- a/crates/sui-graphql-rpc/src/types/coin.rs +++ b/crates/sui-graphql-rpc/src/types/coin.rs @@ -1,6 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::connection::ScanConnection; use crate::consistency::{build_objects_query, View}; use crate::data::{Db, QueryExecutor}; use crate::error::Error; @@ -193,6 +194,25 @@ impl Coin { } /// The transaction blocks that sent objects to this object. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range includes all transactions known to GraphQL, but it can be + /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + /// `afterCheckpoint` and `atCheckpoint` filters. pub(crate) async fn received_transaction_blocks( &self, ctx: &Context<'_>, @@ -201,9 +221,10 @@ impl Coin { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { ObjectImpl(&self.super_.super_) - .received_transaction_blocks(ctx, first, after, last, before, filter) + .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit) .await } diff --git a/crates/sui-graphql-rpc/src/types/coin_metadata.rs b/crates/sui-graphql-rpc/src/types/coin_metadata.rs index bad0545636b84..2052c8b42b03d 100644 --- a/crates/sui-graphql-rpc/src/types/coin_metadata.rs +++ b/crates/sui-graphql-rpc/src/types/coin_metadata.rs @@ -17,6 +17,7 @@ use super::suins_registration::{DomainFormat, SuinsRegistration}; use super::transaction_block::{self, TransactionBlock, TransactionBlockFilter}; use super::type_filter::ExactTypeFilter; use super::uint53::UInt53; +use crate::connection::ScanConnection; use crate::data::Db; use crate::error::Error; use async_graphql::connection::Connection; @@ -182,6 +183,25 @@ impl CoinMetadata { } /// The transaction blocks that sent objects to this object. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range includes all transactions known to GraphQL, but it can be + /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + /// `afterCheckpoint` and `atCheckpoint` filters. pub(crate) async fn received_transaction_blocks( &self, ctx: &Context<'_>, @@ -190,9 +210,10 @@ impl CoinMetadata { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { ObjectImpl(&self.super_.super_) - .received_transaction_blocks(ctx, first, after, last, before, filter) + .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit) .await } diff --git a/crates/sui-graphql-rpc/src/types/cursor.rs b/crates/sui-graphql-rpc/src/types/cursor.rs index 65f7e21673bbb..868bfed21388b 100644 --- a/crates/sui-graphql-rpc/src/types/cursor.rs +++ b/crates/sui-graphql-rpc/src/types/cursor.rs @@ -50,7 +50,7 @@ pub(crate) struct Page { /// Whether the page is extracted from the beginning or the end of the range bounded by the cursors. #[derive(PartialEq, Eq, Debug, Clone, Copy)] -enum End { +pub(crate) enum End { Front, Back, } @@ -101,6 +101,21 @@ pub(crate) trait Target { fn cursor(&self, checkpoint_viewed_at: u64) -> C; } +/// Interface for dealing with cursors that may come from a scan-limit-ed query. +pub(crate) trait ScanLimited: Clone + PartialEq { + /// Whether the cursor was derived from a scan limit. Only applicable to the `startCursor` and + /// `endCursor` returned from a Connection's `PageInfo`, and indicates that the cursor may not + /// have a corresponding node in the result set. + fn is_scan_limited(&self) -> bool { + false + } + + /// Returns a version of the cursor that is not scan limited. + fn unlimited(&self) -> Self { + self.clone() + } +} + impl JsonCursor { pub(crate) fn new(cursor: C) -> Self { JsonCursor(OpaqueCursor(cursor)) @@ -184,6 +199,10 @@ impl Page { pub(crate) fn is_from_front(&self) -> bool { matches!(self.end, End::Front) } + + pub(crate) fn end(&self) -> End { + self.end + } } impl Page @@ -261,7 +280,7 @@ impl Page> { } } -impl Page { +impl Page { /// Treat the cursors of this page as upper- and lowerbound filters for a database `query`. /// Returns two booleans indicating whether there is a previous or next page in the range, /// followed by an iterator of values in the page, fetched from the database. @@ -361,7 +380,9 @@ impl Page { } /// Given the results of a database query, determine whether the result set has a previous and - /// next page and is consistent with the provided cursors. + /// next page and is consistent with the provided cursors. Slightly different logic applies + /// depending on whether the provided cursors stem from either tip of the response, or if they + /// were derived from a scan limit. /// /// Returns two booleans indicating whether there is a previous or next page in the range, /// followed by an iterator of values in the page, fetched from the database. The values @@ -373,7 +394,7 @@ impl Page { results: Vec, ) -> (bool, bool, impl Iterator) where - T: Send + 'static, + T: Target + Send + 'static, { // Detect whether the results imply the existence of a previous or next page. let (prev, next, prefix, suffix) = @@ -386,27 +407,32 @@ impl Page { } // Page drawn from the front, and the cursor for the first element does not match - // `after`. This implies the bound was invalid, so we return an empty result. - (Some(a), Some(f), _, _, End::Front) if f != *a => { + // `after`. If that cursor is not from a scan limit, then it must have appeared in + // the previous page, and should also be at the tip of the current page. This + // absence implies the bound was invalid, so we return an empty result. + (Some(a), Some(f), _, _, End::Front) if f != *a && !a.is_scan_limited() => { return (false, false, vec![].into_iter()); } // Similar to above case, but for back of results. - (_, _, Some(l), Some(b), End::Back) if l != *b => { + (_, _, Some(l), Some(b), End::Back) if l != *b && !b.is_scan_limited() => { return (false, false, vec![].into_iter()); } - // From here onwards, we know that the results are non-empty and if a cursor was - // supplied on the end the page is being drawn from, it was found in the results - // (implying a page follows in that direction). - (after, _, Some(l), before, End::Front) => { - let has_previous_page = after.is_some(); + // From here onwards, we know that the results are non-empty. In the forward + // pagination scenario, the presence of a previous page is determined by whether a + // cursor supplied on the end the page is being drawn from is found in the first + // position. The presence of a next page is determined by whether we have more + // results than the provided limit, and/ or if the end cursor element appears in the + // result set. + (after, Some(f), Some(l), before, End::Front) => { + let has_previous_page = after.is_some_and(|a| a.unlimited() == f); let prefix = has_previous_page as usize; // If results end with the before cursor, we will at least need to trim one element // from the suffix and we trim more off the end if there is more after applying the // limit. - let mut suffix = before.is_some_and(|b| *b == l) as usize; + let mut suffix = before.is_some_and(|b| b.unlimited() == l) as usize; suffix += results.len().saturating_sub(self.limit() + prefix + suffix); let has_next_page = suffix > 0; @@ -414,11 +440,13 @@ impl Page { } // Symmetric to the previous case, but drawing from the back. - (after, Some(f), _, before, End::Back) => { - let has_next_page = before.is_some(); + (after, Some(f), Some(l), before, End::Back) => { + // There is a next page if the last element of the results matches the `before`. + // This last element will get pruned from the result set. + let has_next_page = before.is_some_and(|b| b.unlimited() == l); let suffix = has_next_page as usize; - let mut prefix = after.is_some_and(|a| *a == f) as usize; + let mut prefix = after.is_some_and(|a| a.unlimited() == f) as usize; prefix += results.len().saturating_sub(self.limit() + prefix + suffix); let has_previous_page = prefix > 0; diff --git a/crates/sui-graphql-rpc/src/types/epoch.rs b/crates/sui-graphql-rpc/src/types/epoch.rs index 7ecfa9c7d773e..915493217d1f7 100644 --- a/crates/sui-graphql-rpc/src/types/epoch.rs +++ b/crates/sui-graphql-rpc/src/types/epoch.rs @@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; +use crate::connection::ScanConnection; use crate::context_data::db_data_provider::{convert_to_validators, PgManager}; use crate::data::{DataLoader, Db, DbConnection, QueryExecutor}; use crate::error::Error; @@ -229,6 +230,23 @@ impl Epoch { } /// The epoch's corresponding transaction blocks. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range consists of all transactions in this epoch. async fn transaction_blocks( &self, ctx: &Context<'_>, @@ -237,13 +255,15 @@ impl Epoch { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?; #[allow(clippy::unnecessary_lazy_evaluations)] // rust-lang/rust-clippy#9422 let Some(filter) = filter .unwrap_or_default() .intersect(TransactionBlockFilter { + // If `first_checkpoint_id` is 0, we include the 0th checkpoint by leaving it None after_checkpoint: (self.stored.first_checkpoint_id > 0) .then(|| UInt53::from(self.stored.first_checkpoint_id as u64 - 1)), before_checkpoint: self @@ -253,17 +273,12 @@ impl Epoch { ..Default::default() }) else { - return Ok(Connection::new(false, false)); + return Ok(ScanConnection::new(false, false)); }; - TransactionBlock::paginate( - ctx.data_unchecked(), - page, - filter, - self.checkpoint_viewed_at, - ) - .await - .extend() + TransactionBlock::paginate(ctx, page, filter, self.checkpoint_viewed_at, scan_limit) + .await + .extend() } } diff --git a/crates/sui-graphql-rpc/src/types/event.rs b/crates/sui-graphql-rpc/src/types/event.rs index e195b10844c26..e2555bb0214eb 100644 --- a/crates/sui-graphql-rpc/src/types/event.rs +++ b/crates/sui-graphql-rpc/src/types/event.rs @@ -3,7 +3,7 @@ use std::str::FromStr; -use super::cursor::{self, Page, Paginated, Target}; +use super::cursor::{self, Page, Paginated, ScanLimited, Target}; use super::digest::Digest; use super::type_filter::{ModuleFilter, TypeFilter}; use super::{ @@ -375,3 +375,5 @@ impl Checkpointed for Cursor { self.checkpoint_viewed_at } } + +impl ScanLimited for Cursor {} diff --git a/crates/sui-graphql-rpc/src/types/move_object.rs b/crates/sui-graphql-rpc/src/types/move_object.rs index d41b8dd639420..c5cf724604089 100644 --- a/crates/sui-graphql-rpc/src/types/move_object.rs +++ b/crates/sui-graphql-rpc/src/types/move_object.rs @@ -20,6 +20,7 @@ use super::transaction_block::{self, TransactionBlock, TransactionBlockFilter}; use super::type_filter::ExactTypeFilter; use super::uint53::UInt53; use super::{coin::Coin, object::Object}; +use crate::connection::ScanConnection; use crate::data::Db; use crate::error::Error; use crate::types::stake::StakedSui; @@ -261,6 +262,25 @@ impl MoveObject { } /// The transaction blocks that sent objects to this object. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range includes all transactions known to GraphQL, but it can be + /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + /// `afterCheckpoint` and `atCheckpoint` filters. pub(crate) async fn received_transaction_blocks( &self, ctx: &Context<'_>, @@ -269,9 +289,10 @@ impl MoveObject { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { ObjectImpl(&self.super_) - .received_transaction_blocks(ctx, first, after, last, before, filter) + .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit) .await } diff --git a/crates/sui-graphql-rpc/src/types/move_package.rs b/crates/sui-graphql-rpc/src/types/move_package.rs index bb92ba1e599a3..a85bc75d8a661 100644 --- a/crates/sui-graphql-rpc/src/types/move_package.rs +++ b/crates/sui-graphql-rpc/src/types/move_package.rs @@ -7,7 +7,7 @@ use super::balance::{self, Balance}; use super::base64::Base64; use super::big_int::BigInt; use super::coin::Coin; -use super::cursor::{BcsCursor, JsonCursor, Page, RawPaginated, Target}; +use super::cursor::{BcsCursor, JsonCursor, Page, RawPaginated, ScanLimited, Target}; use super::move_module::MoveModule; use super::move_object::MoveObject; use super::object::{self, Object, ObjectFilter, ObjectImpl, ObjectOwner, ObjectStatus}; @@ -18,6 +18,7 @@ use super::suins_registration::{DomainFormat, SuinsRegistration}; use super::transaction_block::{self, TransactionBlock, TransactionBlockFilter}; use super::type_filter::ExactTypeFilter; use super::uint53::UInt53; +use crate::connection::ScanConnection; use crate::consistency::{Checkpointed, ConsistentNamedCursor}; use crate::data::{DataLoader, Db, DbConnection, QueryExecutor}; use crate::error::Error; @@ -332,6 +333,25 @@ impl MovePackage { /// The transaction blocks that sent objects to this package. /// /// Note that objects that have been sent to a package become inaccessible. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range includes all transactions known to GraphQL, but it can be + /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + /// `afterCheckpoint` and `atCheckpoint` filters. pub(crate) async fn received_transaction_blocks( &self, ctx: &Context<'_>, @@ -340,9 +360,10 @@ impl MovePackage { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { ObjectImpl(&self.super_) - .received_transaction_blocks(ctx, first, after, last, before, filter) + .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit) .await } @@ -859,6 +880,8 @@ impl Target for StoredHistoryPackage { } } +impl ScanLimited for BcsCursor {} + #[async_trait::async_trait] impl Loader for Db { type Value = SuiAddress; diff --git a/crates/sui-graphql-rpc/src/types/object.rs b/crates/sui-graphql-rpc/src/types/object.rs index 66887aa7be54c..5cb49d63ff03c 100644 --- a/crates/sui-graphql-rpc/src/types/object.rs +++ b/crates/sui-graphql-rpc/src/types/object.rs @@ -9,7 +9,7 @@ use super::balance::{self, Balance}; use super::big_int::BigInt; use super::coin::Coin; use super::coin_metadata::CoinMetadata; -use super::cursor::{self, Page, RawPaginated, Target}; +use super::cursor::{self, Page, RawPaginated, ScanLimited, Target}; use super::digest::Digest; use super::display::{Display, DisplayEntry}; use super::dynamic_field::{DynamicField, DynamicFieldName}; @@ -24,6 +24,7 @@ use super::transaction_block::TransactionBlockFilter; use super::type_filter::{ExactTypeFilter, TypeFilter}; use super::uint53::UInt53; use super::{owner::Owner, sui_address::SuiAddress, transaction_block::TransactionBlock}; +use crate::connection::ScanConnection; use crate::consistency::{build_objects_query, Checkpointed, View}; use crate::data::package_resolver::PackageResolver; use crate::data::{DataLoader, Db, DbConnection, QueryExecutor}; @@ -261,7 +262,8 @@ pub(crate) struct HistoricalObjectCursor { arg(name = "last", ty = "Option"), arg(name = "before", ty = "Option"), arg(name = "filter", ty = "Option"), - ty = "Connection", + arg(name = "scan_limit", ty = "Option"), + ty = "ScanConnection", desc = "The transaction blocks that sent objects to this object." ), field( @@ -452,6 +454,25 @@ impl Object { } /// The transaction blocks that sent objects to this object. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range includes all transactions known to GraphQL, but it can be + /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + /// `afterCheckpoint` and `atCheckpoint` filters. pub(crate) async fn received_transaction_blocks( &self, ctx: &Context<'_>, @@ -460,9 +481,10 @@ impl Object { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { ObjectImpl(self) - .received_transaction_blocks(ctx, first, after, last, before, filter) + .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit) .await } @@ -621,7 +643,8 @@ impl ObjectImpl<'_> { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?; let Some(filter) = filter @@ -631,17 +654,12 @@ impl ObjectImpl<'_> { ..Default::default() }) else { - return Ok(Connection::new(false, false)); + return Ok(ScanConnection::new(false, false)); }; - TransactionBlock::paginate( - ctx.data_unchecked(), - page, - filter, - self.0.checkpoint_viewed_at, - ) - .await - .extend() + TransactionBlock::paginate(ctx, page, filter, self.0.checkpoint_viewed_at, scan_limit) + .await + .extend() } pub(crate) async fn bcs(&self) -> Result> { @@ -1084,7 +1102,7 @@ impl ObjectFilter { hex::encode(id.into_vec()) ) .unwrap(); - prefix = ","; + prefix = ", "; } inner.push(')'); query = or_filter!(query, inner); @@ -1158,6 +1176,8 @@ impl Checkpointed for Cursor { } } +impl ScanLimited for Cursor {} + impl RawPaginated for StoredHistoryObject { fn filter_ge(cursor: &Cursor, query: RawQuery) -> RawQuery { filter!( diff --git a/crates/sui-graphql-rpc/src/types/query.rs b/crates/sui-graphql-rpc/src/types/query.rs index 07b51ef649d88..f403fbf8657b5 100644 --- a/crates/sui-graphql-rpc/src/types/query.rs +++ b/crates/sui-graphql-rpc/src/types/query.rs @@ -39,6 +39,7 @@ use super::{ transaction_metadata::TransactionMetadata, type_filter::ExactTypeFilter, }; +use crate::connection::ScanConnection; use crate::server::watermark_task::Watermark; use crate::types::base64::Base64 as GraphQLBase64; use crate::types::zklogin_verify_signature::verify_zklogin_signature; @@ -369,6 +370,25 @@ impl Query { } /// The transaction blocks that exist in the network. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range includes all transactions known to GraphQL, but it can be + /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + /// `afterCheckpoint` and `atCheckpoint` filters. async fn transaction_blocks( &self, ctx: &Context<'_>, @@ -377,15 +397,18 @@ impl Query { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { let Watermark { checkpoint, .. } = *ctx.data()?; let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?; + TransactionBlock::paginate( - ctx.data_unchecked(), + ctx, page, filter.unwrap_or_default(), checkpoint, + scan_limit, ) .await .extend() diff --git a/crates/sui-graphql-rpc/src/types/stake.rs b/crates/sui-graphql-rpc/src/types/stake.rs index 763741c67b747..75c0a07dabf5a 100644 --- a/crates/sui-graphql-rpc/src/types/stake.rs +++ b/crates/sui-graphql-rpc/src/types/stake.rs @@ -1,6 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::connection::ScanConnection; use crate::error::Error; use crate::{context_data::db_data_provider::PgManager, data::Db}; @@ -201,6 +202,25 @@ impl StakedSui { } /// The transaction blocks that sent objects to this object. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range includes all transactions known to GraphQL, but it can be + /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + /// `afterCheckpoint` and `atCheckpoint` filters. pub(crate) async fn received_transaction_blocks( &self, ctx: &Context<'_>, @@ -209,9 +229,10 @@ impl StakedSui { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { ObjectImpl(&self.super_.super_) - .received_transaction_blocks(ctx, first, after, last, before, filter) + .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit) .await } diff --git a/crates/sui-graphql-rpc/src/types/suins_registration.rs b/crates/sui-graphql-rpc/src/types/suins_registration.rs index ed5337cb5d84f..e7391a258346e 100644 --- a/crates/sui-graphql-rpc/src/types/suins_registration.rs +++ b/crates/sui-graphql-rpc/src/types/suins_registration.rs @@ -25,6 +25,7 @@ use super::{ uint53::UInt53, }; use crate::{ + connection::ScanConnection, consistency::{build_objects_query, View}, data::{Db, DbConnection, QueryExecutor}, error::Error, @@ -238,6 +239,25 @@ impl SuinsRegistration { } /// The transaction blocks that sent objects to this object. + /// + /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + /// results. It is required for queries that apply more than two complex filters (on function, + /// kind, sender, recipient, input object, changed object, or ids), and can be at most + /// `serviceConfig.maxScanLimit`. + /// + /// When the scan limit is reached the page will be returned even if it has fewer than `first` + /// results when paginating forward (`last` when paginating backwards). If there are more + /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + /// transaction that was scanned as opposed to the last (or first) transaction in the page. + /// + /// Requesting the next (or previous) page after this cursor will resume the search, scanning + /// the next `scanLimit` many transactions in the direction of pagination, and so on until all + /// transactions in the scanning range have been visited. + /// + /// By default, the scanning range includes all transactions known to GraphQL, but it can be + /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + /// `afterCheckpoint` and `atCheckpoint` filters. pub(crate) async fn received_transaction_blocks( &self, ctx: &Context<'_>, @@ -246,9 +266,10 @@ impl SuinsRegistration { last: Option, before: Option, filter: Option, - ) -> Result> { + scan_limit: Option, + ) -> Result> { ObjectImpl(&self.super_.super_) - .received_transaction_blocks(ctx, first, after, last, before, filter) + .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit) .await } diff --git a/crates/sui-graphql-rpc/src/types/transaction_block/cursor.rs b/crates/sui-graphql-rpc/src/types/transaction_block/cursor.rs new file mode 100644 index 0000000000000..56a82609bda1e --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/transaction_block/cursor.rs @@ -0,0 +1,173 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + consistency::Checkpointed, + filter, + raw_query::RawQuery, + types::cursor::{self, Paginated, RawPaginated, ScanLimited, Target}, +}; +use diesel::{ + backend::Backend, + deserialize::{self, FromSql, QueryableByName}, + row::NamedRow, + ExpressionMethods, QueryDsl, +}; +use serde::{Deserialize, Serialize}; +use sui_indexer::{models::transactions::StoredTransaction, schema::transactions}; + +use super::Query; + +pub(crate) type Cursor = cursor::JsonCursor; + +/// The cursor returned for each `TransactionBlock` in a connection's page of results. The +/// `checkpoint_viewed_at` will set the consistent upper bound for subsequent queries made on this +/// cursor. +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +pub(crate) struct TransactionBlockCursor { + /// The checkpoint sequence number this was viewed at. + #[serde(rename = "c")] + pub checkpoint_viewed_at: u64, + #[serde(rename = "t")] + pub tx_sequence_number: u64, + /// Whether the cursor was derived from a `scan_limit`. Only applicable to the `startCursor` and + /// `endCursor` returned from a Connection's `PageInfo`, and indicates that the cursor may not + /// have a corresponding node in the result set. + #[serde(rename = "i")] + pub is_scan_limited: bool, +} + +/// Results from raw queries in Diesel can only be deserialized into structs that implements +/// `QueryableByName`. This struct is used to represent a row of `tx_sequence_number` returned from +/// subqueries against tx lookup tables. +#[derive(Clone, Debug)] +pub struct TxLookup { + pub tx_sequence_number: i64, +} + +impl Checkpointed for Cursor { + fn checkpoint_viewed_at(&self) -> u64 { + self.checkpoint_viewed_at + } +} + +impl ScanLimited for Cursor { + fn is_scan_limited(&self) -> bool { + self.is_scan_limited + } + + fn unlimited(&self) -> Self { + Cursor::new(TransactionBlockCursor { + is_scan_limited: false, + tx_sequence_number: self.tx_sequence_number, + checkpoint_viewed_at: self.checkpoint_viewed_at, + }) + } +} + +impl Paginated for StoredTransaction { + type Source = transactions::table; + + fn filter_ge(cursor: &Cursor, query: Query) -> Query { + query.filter(transactions::dsl::tx_sequence_number.ge(cursor.tx_sequence_number as i64)) + } + + fn filter_le(cursor: &Cursor, query: Query) -> Query { + query.filter(transactions::dsl::tx_sequence_number.le(cursor.tx_sequence_number as i64)) + } + + fn order(asc: bool, query: Query) -> Query { + use transactions::dsl; + if asc { + query.order_by(dsl::tx_sequence_number.asc()) + } else { + query.order_by(dsl::tx_sequence_number.desc()) + } + } +} + +impl Target for StoredTransaction { + fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor { + Cursor::new(TransactionBlockCursor { + tx_sequence_number: self.tx_sequence_number as u64, + checkpoint_viewed_at, + is_scan_limited: false, + }) + } +} + +impl RawPaginated for StoredTransaction { + fn filter_ge(cursor: &Cursor, query: RawQuery) -> RawQuery { + filter!( + query, + format!("tx_sequence_number >= {}", cursor.tx_sequence_number) + ) + } + + fn filter_le(cursor: &Cursor, query: RawQuery) -> RawQuery { + filter!( + query, + format!("tx_sequence_number <= {}", cursor.tx_sequence_number) + ) + } + + fn order(asc: bool, query: RawQuery) -> RawQuery { + if asc { + query.order_by("tx_sequence_number ASC") + } else { + query.order_by("tx_sequence_number DESC") + } + } +} + +impl Target for TxLookup { + fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor { + Cursor::new(TransactionBlockCursor { + tx_sequence_number: self.tx_sequence_number as u64, + checkpoint_viewed_at, + is_scan_limited: false, + }) + } +} + +impl RawPaginated for TxLookup { + fn filter_ge(cursor: &Cursor, query: RawQuery) -> RawQuery { + filter!( + query, + format!("tx_sequence_number >= {}", cursor.tx_sequence_number) + ) + } + + fn filter_le(cursor: &Cursor, query: RawQuery) -> RawQuery { + filter!( + query, + format!("tx_sequence_number <= {}", cursor.tx_sequence_number) + ) + } + + fn order(asc: bool, query: RawQuery) -> RawQuery { + if asc { + query.order_by("tx_sequence_number ASC") + } else { + query.order_by("tx_sequence_number DESC") + } + } +} + +/// `sql_query` raw queries require `QueryableByName`. The default implementation looks for a table +/// based on the struct name, and it also expects the struct's fields to reflect the table's +/// columns. We can override this behavior by implementing `QueryableByName` for our struct. For +/// `TxBounds`, its fields are derived from `checkpoints`, so we can't leverage the default +/// implementation directly. +impl QueryableByName for TxLookup +where + DB: Backend, + i64: FromSql, +{ + fn build<'a>(row: &impl NamedRow<'a, DB>) -> deserialize::Result { + let tx_sequence_number = + NamedRow::get::(row, "tx_sequence_number")?; + + Ok(Self { tx_sequence_number }) + } +} diff --git a/crates/sui-graphql-rpc/src/types/transaction_block/filter.rs b/crates/sui-graphql-rpc/src/types/transaction_block/filter.rs new file mode 100644 index 0000000000000..29c104ac9484c --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/transaction_block/filter.rs @@ -0,0 +1,130 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use super::TransactionBlockKindInput; +use crate::types::{digest::Digest, sui_address::SuiAddress, type_filter::FqNameFilter}; +use crate::types::{intersect, uint53::UInt53}; +use async_graphql::InputObject; +use std::collections::BTreeSet; +use sui_types::base_types::SuiAddress as NativeSuiAddress; + +#[derive(InputObject, Debug, Default, Clone)] +pub(crate) struct TransactionBlockFilter { + pub function: Option, + + /// An input filter selecting for either system or programmable transactions. + pub kind: Option, + pub after_checkpoint: Option, + pub at_checkpoint: Option, + pub before_checkpoint: Option, + + pub sign_address: Option, + pub recv_address: Option, + + pub input_object: Option, + pub changed_object: Option, + + pub transaction_ids: Option>, +} + +impl TransactionBlockFilter { + /// Try to create a filter whose results are the intersection of transaction blocks in `self`'s + /// results and transaction blocks in `other`'s results. This may not be possible if the + /// resulting filter is inconsistent in some way (e.g. a filter that requires one field to be + /// two different values simultaneously). + pub(crate) fn intersect(self, other: Self) -> Option { + macro_rules! intersect { + ($field:ident, $body:expr) => { + intersect::field(self.$field, other.$field, $body) + }; + } + + Some(Self { + function: intersect!(function, FqNameFilter::intersect)?, + kind: intersect!(kind, intersect::by_eq)?, + + after_checkpoint: intersect!(after_checkpoint, intersect::by_max)?, + at_checkpoint: intersect!(at_checkpoint, intersect::by_eq)?, + before_checkpoint: intersect!(before_checkpoint, intersect::by_min)?, + + sign_address: intersect!(sign_address, intersect::by_eq)?, + recv_address: intersect!(recv_address, intersect::by_eq)?, + input_object: intersect!(input_object, intersect::by_eq)?, + changed_object: intersect!(changed_object, intersect::by_eq)?, + + transaction_ids: intersect!(transaction_ids, |a, b| { + let a = BTreeSet::from_iter(a.into_iter()); + let b = BTreeSet::from_iter(b.into_iter()); + Some(a.intersection(&b).cloned().collect()) + })?, + }) + } + + /// Most filter conditions require a scan limit if used in tandem with other filters. The + /// exception to this is sender and checkpoint, since sender is denormalized on all tables, and + /// the corresponding tx range can be determined for a checkpoint. + pub(crate) fn requires_scan_limit(&self) -> bool { + [ + self.function.is_some(), + self.kind.is_some(), + self.recv_address.is_some(), + self.input_object.is_some(), + self.changed_object.is_some(), + self.transaction_ids.is_some(), + ] + .into_iter() + .filter(|is_set| *is_set) + .count() + > 1 + } + + /// If we don't query a lookup table that has a denormalized sender column, we need to + /// explicitly sp + pub(crate) fn explicit_sender(&self) -> Option { + if self.function.is_none() + && self.kind.is_none() + && self.recv_address.is_none() + && self.input_object.is_none() + && self.changed_object.is_none() + { + self.sign_address + } else { + None + } + } + + /// A TransactionBlockFilter is considered not to have any filters if no filters are specified, + /// or if the only filters are on `checkpoint`. + pub(crate) fn has_filters(&self) -> bool { + self.function.is_some() + || self.kind.is_some() + || self.sign_address.is_some() + || self.recv_address.is_some() + || self.input_object.is_some() + || self.changed_object.is_some() + || self.transaction_ids.is_some() + } + + pub(crate) fn is_empty(&self) -> bool { + self.before_checkpoint == Some(UInt53::from(0)) + || matches!( + (self.after_checkpoint, self.before_checkpoint), + (Some(after), Some(before)) if after >= before + ) + || matches!( + (self.after_checkpoint, self.at_checkpoint), + (Some(after), Some(at)) if after >= at + ) + || matches!( + (self.at_checkpoint, self.before_checkpoint), + (Some(at), Some(before)) if at >= before + ) + // If SystemTx, sender if specified must be 0x0. Conversely, if sender is 0x0, kind must be SystemTx. + || matches!( + (self.kind, self.sign_address), + (Some(kind), Some(signer)) + if (kind == TransactionBlockKindInput::SystemTx) + != (signer == SuiAddress::from(NativeSuiAddress::ZERO)) + ) + } +} diff --git a/crates/sui-graphql-rpc/src/types/transaction_block.rs b/crates/sui-graphql-rpc/src/types/transaction_block/mod.rs similarity index 63% rename from crates/sui-graphql-rpc/src/types/transaction_block.rs rename to crates/sui-graphql-rpc/src/types/transaction_block/mod.rs index c33da8133001a..1573fe97cfeab 100644 --- a/crates/sui-graphql-rpc/src/types/transaction_block.rs +++ b/crates/sui-graphql-rpc/src/types/transaction_block/mod.rs @@ -1,22 +1,34 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::collections::{BTreeMap, BTreeSet, HashMap}; - -use async_graphql::{ - connection::{Connection, CursorType, Edge}, - dataloader::Loader, - *, +use super::{ + address::Address, + base64::Base64, + cursor::{Page, Target}, + digest::Digest, + epoch::Epoch, + gas::GasInput, + sui_address::SuiAddress, + transaction_block_effects::{TransactionBlockEffects, TransactionBlockEffectsKind}, + transaction_block_kind::TransactionBlockKind, }; +use crate::{ + config::ServiceConfig, + connection::ScanConnection, + data::{self, DataLoader, Db, DbConnection, QueryExecutor}, + error::Error, + server::watermark_task::Watermark, +}; +use async_graphql::{connection::CursorType, dataloader::Loader, *}; +use connection::Edge; +use cursor::TxLookup; use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl, SelectableHelper}; use fastcrypto::encoding::{Base58, Encoding}; use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; use sui_indexer::{ models::transactions::StoredTransaction, - schema::{ - transactions, tx_calls_fun, tx_changed_objects, tx_digests, tx_input_objects, - tx_recipients, tx_senders, - }, + schema::{transactions, tx_digests}, }; use sui_types::{ base_types::SuiAddress as NativeSuiAddress, @@ -29,27 +41,13 @@ use sui_types::{ }, }; -use crate::{ - consistency::Checkpointed, - data::{self, DataLoader, Db, DbConnection, QueryExecutor}, - error::Error, - server::watermark_task::Watermark, - types::intersect, -}; +mod cursor; +mod filter; +mod tx_lookups; -use super::{ - address::Address, - base64::Base64, - cursor::{self, Page, Paginated, Target}, - digest::Digest, - epoch::Epoch, - gas::GasInput, - sui_address::SuiAddress, - transaction_block_effects::{TransactionBlockEffects, TransactionBlockEffectsKind}, - transaction_block_kind::TransactionBlockKind, - type_filter::FqNameFilter, - uint53::UInt53, -}; +pub(crate) use cursor::Cursor; +pub(crate) use filter::TransactionBlockFilter; +pub(crate) use tx_lookups::{subqueries, TxBounds}; /// Wraps the actual transaction block data with the checkpoint sequence number at which the data /// was viewed, for consistent results on paginating through and resolving nested types. @@ -94,26 +92,6 @@ pub(crate) enum TransactionBlockKindInput { ProgrammableTx = 1, } -#[derive(InputObject, Debug, Default, Clone)] -pub(crate) struct TransactionBlockFilter { - pub function: Option, - - /// An input filter selecting for either system or programmable transactions. - pub kind: Option, - pub after_checkpoint: Option, - pub at_checkpoint: Option, - pub before_checkpoint: Option, - - pub sign_address: Option, - pub recv_address: Option, - - pub input_object: Option, - pub changed_object: Option, - - pub transaction_ids: Option>, -} - -pub(crate) type Cursor = cursor::JsonCursor; type Query = data::Query; /// The cursor returned for each `TransactionBlock` in a connection's page of results. The @@ -298,104 +276,132 @@ impl TransactionBlock { /// /// If the `Page` is set, then this function will defer to the `checkpoint_viewed_at` in /// the cursor if they are consistent. + /// + /// Filters that involve a combination of `recvAddress`, `inputObject`, `changedObject`, and + /// `function` should provide a value for `scan_limit`. This modifies querying behavior by + /// limiting how many transactions to scan through before applying filters, and also affects + /// pagination behavior. pub(crate) async fn paginate( - db: &Db, + ctx: &Context<'_>, page: Page, filter: TransactionBlockFilter, checkpoint_viewed_at: u64, - ) -> Result, Error> { - use transactions as tx; + scan_limit: Option, + ) -> Result, Error> { + let limits = &ctx.data_unchecked::().limits; + + // If the caller has provided some arbitrary combination of `function`, `kind`, + // `recvAddress`, `inputObject`, or `changedObject`, we require setting a `scanLimit`. + if let Some(scan_limit) = scan_limit { + if scan_limit > limits.max_scan_limit as u64 { + return Err(Error::Client(format!( + "Scan limit exceeds max limit of '{}'", + limits.max_scan_limit + ))); + } + } else if filter.requires_scan_limit() { + return Err(Error::Client( + "A scan limit must be specified for the given filter combination".to_string(), + )); + } + + if let Some(tx_ids) = &filter.transaction_ids { + if tx_ids.len() > limits.max_transaction_ids as usize { + return Err(Error::Client(format!( + "Transaction IDs exceed max limit of '{}'", + limits.max_transaction_ids + ))); + } + } + + // If page size or scan limit is 0, we want to standardize behavior by returning an empty + // connection + if filter.is_empty() || page.limit() == 0 || scan_limit.is_some_and(|v| v == 0) { + return Ok(ScanConnection::new(false, false)); + } let cursor_viewed_at = page.validate_cursor_consistency()?; let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at); + let db: &Db = ctx.data_unchecked(); + let is_from_front = page.is_from_front(); - let (prev, next, results) = db - .execute(move |conn| { - page.paginate_query::( + use transactions::dsl as tx; + let (prev, next, transactions, tx_bounds): ( + bool, + bool, + Vec, + Option, + ) = db + .execute_repeatable(move |conn| { + let Some(tx_bounds) = TxBounds::query( conn, + filter.after_checkpoint.map(u64::from), + filter.at_checkpoint.map(u64::from), + filter.before_checkpoint.map(u64::from), checkpoint_viewed_at, - move || { - let mut query = tx::dsl::transactions.into_boxed(); - - if let Some(f) = &filter.function { - let sub_query = tx_calls_fun::dsl::tx_calls_fun - .select(tx_calls_fun::dsl::tx_sequence_number) - .into_boxed(); - - query = query.filter(tx::dsl::tx_sequence_number.eq_any(f.apply( - sub_query, - tx_calls_fun::dsl::package, - tx_calls_fun::dsl::module, - tx_calls_fun::dsl::func, - ))); - } - - if let Some(k) = &filter.kind { - query = query.filter(tx::dsl::transaction_kind.eq(*k as i16)) - } - - if let Some(c) = &filter.after_checkpoint { - query = - query.filter(tx::dsl::checkpoint_sequence_number.gt(i64::from(*c))); - } - - if let Some(c) = &filter.at_checkpoint { - query = - query.filter(tx::dsl::checkpoint_sequence_number.eq(i64::from(*c))); - } - - let before_checkpoint = filter - .before_checkpoint - .map_or(checkpoint_viewed_at + 1, |c| { - u64::from(c).min(checkpoint_viewed_at + 1) - }); - query = query.filter( - tx::dsl::checkpoint_sequence_number.lt(before_checkpoint as i64), - ); - - if let Some(a) = &filter.sign_address { - let sub_query = tx_senders::dsl::tx_senders - .select(tx_senders::dsl::tx_sequence_number) - .filter(tx_senders::dsl::sender.eq(a.into_vec())); - query = query.filter(tx::dsl::tx_sequence_number.eq_any(sub_query)); - } - - if let Some(a) = &filter.recv_address { - let sub_query = tx_recipients::dsl::tx_recipients - .select(tx_recipients::dsl::tx_sequence_number) - .filter(tx_recipients::dsl::recipient.eq(a.into_vec())); - query = query.filter(tx::dsl::tx_sequence_number.eq_any(sub_query)); - } - - if let Some(o) = &filter.input_object { - let sub_query = tx_input_objects::dsl::tx_input_objects - .select(tx_input_objects::dsl::tx_sequence_number) - .filter(tx_input_objects::dsl::object_id.eq(o.into_vec())); - query = query.filter(tx::dsl::tx_sequence_number.eq_any(sub_query)); - } - - if let Some(o) = &filter.changed_object { - let sub_query = tx_changed_objects::dsl::tx_changed_objects - .select(tx_changed_objects::dsl::tx_sequence_number) - .filter(tx_changed_objects::dsl::object_id.eq(o.into_vec())); - query = query.filter(tx::dsl::tx_sequence_number.eq_any(sub_query)); - } - - if let Some(txs) = &filter.transaction_ids { - let digests: Vec<_> = txs.iter().map(|d| d.to_vec()).collect(); - query = query.filter(tx::dsl::transaction_digest.eq_any(digests)); - } - - query - }, - ) + scan_limit, + &page, + )? + else { + return Ok::<_, diesel::result::Error>((false, false, Vec::new(), None)); + }; + + // If no filters are selected, or if the filter is composed of only checkpoint + // filters, we can directly query the main `transactions` table. Otherwise, we first + // fetch the set of `tx_sequence_number` from a join over relevant lookup tables, + // and then issue a query against the `transactions` table to fetch the remaining + // contents. + let (prev, next, transactions) = if !filter.has_filters() { + let (prev, next, iter) = page.paginate_query::( + conn, + checkpoint_viewed_at, + move || { + tx::transactions + .filter(tx::tx_sequence_number.ge(tx_bounds.scan_lo() as i64)) + .filter(tx::tx_sequence_number.lt(tx_bounds.scan_hi() as i64)) + .into_boxed() + }, + )?; + + (prev, next, iter.collect()) + } else { + let subquery = subqueries(&filter, tx_bounds).unwrap(); + let (prev, next, results) = + page.paginate_raw_query::(conn, checkpoint_viewed_at, subquery)?; + + let tx_sequence_numbers = results + .into_iter() + .map(|x| x.tx_sequence_number) + .collect::>(); + + let transactions = conn.results(move || { + tx::transactions + .filter(tx::tx_sequence_number.eq_any(tx_sequence_numbers.clone())) + })?; + + (prev, next, transactions) + }; + + Ok::<_, diesel::result::Error>((prev, next, transactions, Some(tx_bounds))) }) .await?; - let mut conn = Connection::new(prev, next); + let mut conn = ScanConnection::new(prev, next); + + let Some(tx_bounds) = tx_bounds else { + return Ok(conn); + }; - // The "checkpoint viewed at" sets a consistent upper bound for the nested queries. - for stored in results { + if scan_limit.is_some() { + apply_scan_limited_pagination( + &mut conn, + tx_bounds, + checkpoint_viewed_at, + is_from_front, + ); + } + + for stored in transactions { let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor(); let inner = TransactionBlockInner::try_from(stored)?; let transaction = TransactionBlock { @@ -409,87 +415,6 @@ impl TransactionBlock { } } -impl TransactionBlockFilter { - /// Try to create a filter whose results are the intersection of transaction blocks in `self`'s - /// results and transaction blocks in `other`'s results. This may not be possible if the - /// resulting filter is inconsistent in some way (e.g. a filter that requires one field to be - /// two different values simultaneously). - pub(crate) fn intersect(self, other: Self) -> Option { - macro_rules! intersect { - ($field:ident, $body:expr) => { - intersect::field(self.$field, other.$field, $body) - }; - } - - Some(Self { - function: intersect!(function, FqNameFilter::intersect)?, - kind: intersect!(kind, intersect::by_eq)?, - - after_checkpoint: intersect!(after_checkpoint, intersect::by_max)?, - at_checkpoint: intersect!(at_checkpoint, intersect::by_eq)?, - before_checkpoint: intersect!(before_checkpoint, intersect::by_min)?, - - sign_address: intersect!(sign_address, intersect::by_eq)?, - recv_address: intersect!(recv_address, intersect::by_eq)?, - input_object: intersect!(input_object, intersect::by_eq)?, - changed_object: intersect!(changed_object, intersect::by_eq)?, - - transaction_ids: intersect!(transaction_ids, |a, b| { - let a = BTreeSet::from_iter(a.into_iter()); - let b = BTreeSet::from_iter(b.into_iter()); - Some(a.intersection(&b).cloned().collect()) - })?, - }) - } -} - -impl Paginated for StoredTransaction { - type Source = transactions::table; - - fn filter_ge(cursor: &Cursor, query: Query) -> Query { - query - .filter(transactions::dsl::tx_sequence_number.ge(cursor.tx_sequence_number as i64)) - .filter( - transactions::dsl::checkpoint_sequence_number - .ge(cursor.tx_checkpoint_number as i64), - ) - } - - fn filter_le(cursor: &Cursor, query: Query) -> Query { - query - .filter(transactions::dsl::tx_sequence_number.le(cursor.tx_sequence_number as i64)) - .filter( - transactions::dsl::checkpoint_sequence_number - .le(cursor.tx_checkpoint_number as i64), - ) - } - - fn order(asc: bool, query: Query) -> Query { - use transactions::dsl; - if asc { - query.order_by(dsl::tx_sequence_number.asc()) - } else { - query.order_by(dsl::tx_sequence_number.desc()) - } - } -} - -impl Target for StoredTransaction { - fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor { - Cursor::new(TransactionBlockCursor { - tx_sequence_number: self.tx_sequence_number as u64, - tx_checkpoint_number: self.checkpoint_sequence_number as u64, - checkpoint_viewed_at, - }) - } -} - -impl Checkpointed for Cursor { - fn checkpoint_viewed_at(&self) -> u64 { - self.checkpoint_viewed_at - } -} - #[async_trait::async_trait] impl Loader for Db { type Value = TransactionBlock; @@ -599,3 +524,88 @@ impl TryFrom for TransactionBlock { }) } } + +fn apply_scan_limited_pagination( + conn: &mut ScanConnection, + tx_bounds: TxBounds, + checkpoint_viewed_at: u64, + is_from_front: bool, +) { + if is_from_front { + apply_forward_scan_limited_pagination(conn, tx_bounds, checkpoint_viewed_at); + } else { + apply_backward_scan_limited_pagination(conn, tx_bounds, checkpoint_viewed_at); + } +} + +/// When paginating forwards on a scan-limited query, the starting cursor and previous page flag +/// will be the first tx scanned in the current window, and whether this window is within the +/// scanning range. The ending cursor and next page flag wraps the last element of the result set if +/// there are more matches in the scanned window that are truncated - if the page size is smaller +/// than the scan limit - but otherwise is expanded out to the last tx scanned. +fn apply_forward_scan_limited_pagination( + conn: &mut ScanConnection, + tx_bounds: TxBounds, + checkpoint_viewed_at: u64, +) { + conn.has_previous_page = tx_bounds.scan_has_prev_page(); + conn.start_cursor = Some( + Cursor::new(cursor::TransactionBlockCursor { + checkpoint_viewed_at, + tx_sequence_number: tx_bounds.scan_start_cursor(), + is_scan_limited: true, + }) + .encode_cursor(), + ); + + // There may be more results within the scanned range that got truncated, which occurs when page + // size is less than `scan_limit`, so only overwrite the end when the base pagination reports no + // next page. + if !conn.has_next_page { + conn.has_next_page = tx_bounds.scan_has_next_page(); + conn.end_cursor = Some( + Cursor::new(cursor::TransactionBlockCursor { + checkpoint_viewed_at, + tx_sequence_number: tx_bounds.scan_end_cursor(), + is_scan_limited: true, + }) + .encode_cursor(), + ); + } +} + +/// When paginating backwards on a scan-limited query, the ending cursor and next page flag will be +/// the last tx scanned in the current window, and whether this window is within the scanning range. +/// The starting cursor and previous page flag wraps the first element of the result set if there +/// are more matches in the scanned window that are truncated - if the page size is smaller than the +/// scan limit - but otherwise is expanded out to the first tx scanned. +fn apply_backward_scan_limited_pagination( + conn: &mut ScanConnection, + tx_bounds: TxBounds, + checkpoint_viewed_at: u64, +) { + conn.has_next_page = tx_bounds.scan_has_next_page(); + conn.end_cursor = Some( + Cursor::new(cursor::TransactionBlockCursor { + checkpoint_viewed_at, + tx_sequence_number: tx_bounds.scan_end_cursor(), + is_scan_limited: true, + }) + .encode_cursor(), + ); + + // There may be more results within the scanned range that are truncated, especially if page + // size is less than `scan_limit`, so only overwrite the end when the base pagination reports no + // next page. + if !conn.has_previous_page { + conn.has_previous_page = tx_bounds.scan_has_prev_page(); + conn.start_cursor = Some( + Cursor::new(cursor::TransactionBlockCursor { + checkpoint_viewed_at, + tx_sequence_number: tx_bounds.scan_start_cursor(), + is_scan_limited: true, + }) + .encode_cursor(), + ); + } +} diff --git a/crates/sui-graphql-rpc/src/types/transaction_block/tx_lookups.rs b/crates/sui-graphql-rpc/src/types/transaction_block/tx_lookups.rs new file mode 100644 index 0000000000000..e3d551976d5d4 --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/transaction_block/tx_lookups.rs @@ -0,0 +1,433 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use super::{Cursor, TransactionBlockFilter}; +use crate::{ + data::{pg::bytea_literal, Conn, DbConnection}, + filter, inner_join, query, + raw_query::RawQuery, + types::{ + cursor::{End, Page}, + digest::Digest, + sui_address::SuiAddress, + transaction_block::TransactionBlockKindInput, + type_filter::{FqNameFilter, ModuleFilter}, + }, +}; +use diesel::{ExpressionMethods, OptionalExtension, QueryDsl}; +use std::fmt::Write; +use sui_indexer::schema::checkpoints; + +/// Bounds on transaction sequence number, imposed by filters, cursors, and the scan limit. The +/// outermost bounds are determined by the checkpoint filters. These get translated into bounds in +/// terms of transaction sequence numbers: +/// +/// ```ignore +/// tx_lo tx_hi +/// [-----------------------------------------------------------------) +/// ``` +/// +/// If cursors are provided, they further restrict the range of transactions to scan. Cursors are +/// exclusive, but when issuing database queries, we treat them inclusively so that we can detect +/// previous and next pages based on the existence of cursors in the results: +/// +/// ```ignore +/// cursor_lo cursor_hi_inclusive +/// [------------------------------------------] +/// ``` +/// +/// Finally, the scan limit restricts the number of transactions to scan. The scan limit can be +/// applied to either the front (forward pagination) or the back (backward pagination): +/// +/// ```ignore +/// [-----scan-limit-----)---------------------| end = Front +/// |---------------------[-----scan-limit------) end = Back +/// ``` +/// +/// This data structure can be used to compute the interval of transactions to look in for +/// candidates to include in a page of results. It can also determine whether the scanning has been +/// cut short on either side, implying that there is a previous or next page of values to scan. +/// +/// NOTE: for consistency, assume that lowerbounds are inclusive and upperbounds are exclusive. +/// Bounds that do not follow this convention will be annotated explicitly (e.g. `lo_exclusive` or +/// `hi_inclusive`). +#[derive(Clone, Debug, Copy)] +pub(crate) struct TxBounds { + /// The inclusive lower bound tx_sequence_number derived from checkpoint bounds. If checkpoint + /// bounds are not provided, this will default to `0`. + tx_lo: u64, + + /// The exclusive upper bound tx_sequence_number derived from checkpoint bounds. If checkpoint + /// bounds are not provided, this will default to the total transaction count at the checkpoint + /// viewed. + tx_hi: u64, + + /// The starting cursor (aka `after`). + cursor_lo_exclusive: Option, + + // The ending cursor (aka `before`). + cursor_hi: Option, + + /// The number of transactions to treat as candidates, defaults to all the transactions in the + /// range defined by the bounds above. + scan_limit: Option, + + /// Which end of the range candidates will be scanned from. + end: End, +} + +impl TxBounds { + /// Determines the `tx_sequence_number` range from the checkpoint bounds for a transaction block + /// query. If no checkpoint range is specified, the default is between 0 and the + /// `checkpoint_viewed_at`. The corresponding `tx_sequence_number` range is fetched from db, and + /// further adjusted by cursors and scan limit. If there are any inconsistencies or invalid + /// combinations, i.e. `after` cursor is greater than the upper bound, return None. + pub(crate) fn query( + conn: &mut Conn, + cp_after: Option, + cp_at: Option, + cp_before: Option, + checkpoint_viewed_at: u64, + scan_limit: Option, + page: &Page, + ) -> Result, diesel::result::Error> { + // Lowerbound in terms of checkpoint sequence number. We want to get the total transaction + // count of the checkpoint before this one, or 0 if there is no previous checkpoint. + let cp_lo = max_option([cp_after.map(|x| x.saturating_add(1)), cp_at]).unwrap_or(0); + + let cp_before_inclusive = match cp_before { + // There are no results strictly before checkpoint 0. + Some(0) => return Ok(None), + Some(x) => Some(x - 1), + None => None, + }; + + // Upperbound in terms of checkpoint sequence number. We want to get the total transaction + // count at the end of this checkpoint. If no upperbound is given, use + // `checkpoint_viewed_at`. + // + // SAFETY: we can unwrap because of the `Some(checkpoint_viewed_at) + let cp_hi = min_option([cp_before_inclusive, cp_at, Some(checkpoint_viewed_at)]).unwrap(); + + use checkpoints::dsl; + let (tx_lo, tx_hi) = if let Some(cp_prev) = cp_lo.checked_sub(1) { + let res: Vec = conn.results(move || { + dsl::checkpoints + .select(dsl::network_total_transactions) + .filter(dsl::sequence_number.eq_any([cp_prev as i64, cp_hi as i64])) + .order_by(dsl::network_total_transactions.asc()) + })?; + + // If there are not two distinct results, it means that the transaction bounds are + // empty (lo and hi are the same), or it means that the one or other of the checkpoints + // doesn't exist, so we can return early. + let &[lo, hi] = res.as_slice() else { + return Ok(None); + }; + + (lo as u64, hi as u64) + } else { + let res: Option = conn + .first(move || { + dsl::checkpoints + .select(dsl::network_total_transactions) + .filter(dsl::sequence_number.eq(cp_hi as i64)) + }) + .optional()?; + + // If there is no result, it means that the checkpoint doesn't exist, so we can return + // early. + let Some(hi) = res else { + return Ok(None); + }; + + (0, hi as u64) + }; + + // If the cursors point outside checkpoint bounds, we can return early. + if matches!(page.after(), Some(a) if tx_hi <= a.tx_sequence_number.saturating_add(1)) { + return Ok(None); + } + + if matches!(page.before(), Some(b) if b.tx_sequence_number <= tx_lo) { + return Ok(None); + } + + Ok(Some(Self { + tx_lo, + tx_hi, + cursor_lo_exclusive: page.after().map(|a| a.tx_sequence_number), + cursor_hi: page.before().map(|b| b.tx_sequence_number), + scan_limit, + end: page.end(), + })) + } + + /// Inclusive lowerbound for range of transactions to scan, accounting for the bounds from + /// filters and the cursor, but not scan limits. For the purposes of scanning records in the + /// DB, cursors are treated inclusively, even though they are exclusive bounds. + fn db_lo(&self) -> u64 { + max_option([self.cursor_lo_exclusive, Some(self.tx_lo)]).unwrap() + } + + /// Exclusive upperbound for range of transactions to scan, accounting for the bounds from + /// filters and the cursor, but not scan limits. For the purposes of scanning records in the + /// DB, cursors are treated inclusively, even though they are exclusive bounds. + fn db_hi(&self) -> u64 { + min_option([ + self.cursor_hi.map(|h| h.saturating_add(1)), + Some(self.tx_hi), + ]) + .unwrap() + } + + /// Whether the cursor lowerbound restricts the transaction range. + fn has_cursor_prev_page(&self) -> bool { + self.cursor_lo_exclusive.is_some_and(|lo| self.tx_lo <= lo) + } + + /// Whether the cursor upperbound restricts the transaction range. + fn has_cursor_next_page(&self) -> bool { + self.cursor_hi.is_some_and(|hi| hi < self.tx_hi) + } + + /// Inclusive lowerbound of range of transactions to scan. + pub(crate) fn scan_lo(&self) -> u64 { + match (self.end, self.scan_limit) { + (End::Front, _) | (_, None) => self.db_lo(), + (End::Back, Some(scan_limit)) => self + .db_hi() + // If there is a next page, additionally scan the cursor upperbound. + .saturating_sub(self.has_cursor_next_page() as u64) + .saturating_sub(scan_limit) + .max(self.db_lo()), + } + } + + /// Exclusive upperbound of range of transactions to scan. + pub(crate) fn scan_hi(&self) -> u64 { + match (self.end, self.scan_limit) { + (End::Back, _) | (_, None) => self.db_hi(), + (End::Front, Some(scan_limit)) => self + .db_lo() + // If there is a previous page, additionally scan the cursor lowerbound. + .saturating_add(self.has_cursor_prev_page() as u64) + .saturating_add(scan_limit) + .min(self.db_hi()), + } + } + + /// The first transaction scanned, ignoring transactions pointed at by cursors. + pub(crate) fn scan_start_cursor(&self) -> u64 { + let skip_cursor_lo = self.end == End::Front && self.has_cursor_prev_page(); + self.scan_lo().saturating_add(skip_cursor_lo as u64) + } + + /// The last transaction scanned, ignoring transactions pointed at by cursors. + pub(crate) fn scan_end_cursor(&self) -> u64 { + let skip_cursor_hi = self.end == End::Back && self.has_cursor_next_page(); + self.scan_hi().saturating_sub(skip_cursor_hi as u64 + 1) + } + + /// Whether there are more transactions to scan before this page. + pub(crate) fn scan_has_prev_page(&self) -> bool { + self.tx_lo < self.scan_start_cursor() + } + + /// Whether there are more transactions to scan after this page. + pub(crate) fn scan_has_next_page(&self) -> bool { + self.scan_end_cursor() + 1 < self.tx_hi + } +} + +/// Determines the maximum value in an arbitrary number of Option. +fn max_option(xs: impl IntoIterator>) -> Option { + xs.into_iter().flatten().max() +} + +/// Determines the minimum value in an arbitrary number of Option. +fn min_option(xs: impl IntoIterator>) -> Option { + xs.into_iter().flatten().min() +} + +/// Constructs a `RawQuery` as a join over all relevant side tables, filtered on their own filter +/// condition, plus optionally a sender, plus optionally tx/cp bounds. +pub(crate) fn subqueries(filter: &TransactionBlockFilter, tx_bounds: TxBounds) -> Option { + let sender = filter.sign_address; + + let mut subqueries = vec![]; + + if let Some(f) = &filter.function { + subqueries.push(match f { + FqNameFilter::ByModule(filter) => match filter { + ModuleFilter::ByPackage(p) => ("tx_calls_pkg", select_pkg(p, sender, tx_bounds)), + ModuleFilter::ByModule(p, m) => { + ("tx_calls_mod", select_mod(p, m.clone(), sender, tx_bounds)) + } + }, + FqNameFilter::ByFqName(p, m, n) => ( + "tx_calls_fun", + select_fun(p, m.clone(), n.clone(), sender, tx_bounds), + ), + }); + } + if let Some(kind) = &filter.kind { + subqueries.push(("tx_kinds", select_kind(*kind, sender, tx_bounds))); + } + if let Some(recv) = &filter.recv_address { + subqueries.push(("tx_recipients", select_recipient(recv, sender, tx_bounds))); + } + if let Some(input) = &filter.input_object { + subqueries.push(("tx_input_objects", select_input(input, sender, tx_bounds))); + } + if let Some(changed) = &filter.changed_object { + subqueries.push(( + "tx_changed_objects", + select_changed(changed, sender, tx_bounds), + )); + } + if let Some(sender) = &filter.explicit_sender() { + subqueries.push(("tx_senders", select_sender(sender, tx_bounds))); + } + if let Some(txs) = &filter.transaction_ids { + subqueries.push(("tx_digests", select_ids(txs, tx_bounds))); + } + + let Some((_, mut subquery)) = subqueries.pop() else { + return None; + }; + + if !subqueries.is_empty() { + subquery = query!("SELECT tx_sequence_number FROM ({}) AS initial", subquery); + while let Some((alias, subselect)) = subqueries.pop() { + subquery = inner_join!(subquery, alias => subselect, using: ["tx_sequence_number"]); + } + } + + Some(subquery) +} + +fn select_tx(sender: Option, bound: TxBounds, from: &str) -> RawQuery { + let mut query = filter!( + query!(format!("SELECT tx_sequence_number FROM {from}")), + format!( + "{} <= tx_sequence_number AND tx_sequence_number < {}", + bound.scan_lo(), + bound.scan_hi() + ) + ); + + if let Some(sender) = sender { + query = filter!( + query, + format!("sender = {}", bytea_literal(sender.as_slice())) + ); + } + + query +} + +fn select_pkg(pkg: &SuiAddress, sender: Option, bound: TxBounds) -> RawQuery { + filter!( + select_tx(sender, bound, "tx_calls_pkg"), + format!("package = {}", bytea_literal(pkg.as_slice())) + ) +} + +fn select_mod( + pkg: &SuiAddress, + mod_: String, + sender: Option, + bound: TxBounds, +) -> RawQuery { + filter!( + select_tx(sender, bound, "tx_calls_mod"), + format!( + "package = {} and module = {{}}", + bytea_literal(pkg.as_slice()) + ), + mod_ + ) +} + +fn select_fun( + pkg: &SuiAddress, + mod_: String, + fun: String, + sender: Option, + bound: TxBounds, +) -> RawQuery { + filter!( + select_tx(sender, bound, "tx_calls_fun"), + format!( + "package = {} AND module = {{}} AND func = {{}}", + bytea_literal(pkg.as_slice()), + ), + mod_, + fun + ) +} + +/// Returns a RawQuery that selects transactions of a specific kind. If SystemTX is specified, we +/// ignore the `sender`. If ProgrammableTX is specified, we filter against the `tx_kinds` table if +/// no `sender` is provided; otherwise, we just query the `tx_senders` table. Other combinations, in +/// particular when kind is SystemTx and sender is specified and not 0x0, are inconsistent and will +/// not produce any results. These inconsistent cases are expected to be checked for before this is +/// called. +fn select_kind( + kind: TransactionBlockKindInput, + sender: Option, + bound: TxBounds, +) -> RawQuery { + match (kind, sender) { + // We can simplify the query to just the `tx_senders` table if ProgrammableTX and sender is + // specified. + (TransactionBlockKindInput::ProgrammableTx, Some(sender)) => select_sender(&sender, bound), + // Otherwise, we can ignore the sender always, and just query the `tx_kinds` table. + _ => filter!( + select_tx(None, bound, "tx_kinds"), + format!("tx_kind = {}", kind as i16) + ), + } +} + +fn select_sender(sender: &SuiAddress, bound: TxBounds) -> RawQuery { + select_tx(Some(*sender), bound, "tx_senders") +} + +fn select_recipient(recv: &SuiAddress, sender: Option, bound: TxBounds) -> RawQuery { + filter!( + select_tx(sender, bound, "tx_recipients"), + format!("recipient = {}", bytea_literal(recv.as_slice())) + ) +} + +fn select_input(input: &SuiAddress, sender: Option, bound: TxBounds) -> RawQuery { + filter!( + select_tx(sender, bound, "tx_input_objects"), + format!("object_id = {}", bytea_literal(input.as_slice())) + ) +} + +fn select_changed(changed: &SuiAddress, sender: Option, bound: TxBounds) -> RawQuery { + filter!( + select_tx(sender, bound, "tx_changed_objects"), + format!("object_id = {}", bytea_literal(changed.as_slice())) + ) +} + +fn select_ids(ids: &Vec, bound: TxBounds) -> RawQuery { + let query = select_tx(None, bound, "tx_digests"); + if ids.is_empty() { + filter!(query, "1=0") + } else { + let mut inner = String::new(); + let mut prefix = "tx_digest IN ("; + for id in ids { + write!(&mut inner, "{prefix}{}", bytea_literal(id.as_slice())).unwrap(); + prefix = ", "; + } + inner.push(')'); + filter!(query, inner) + } +} diff --git a/crates/sui-graphql-rpc/src/types/type_filter.rs b/crates/sui-graphql-rpc/src/types/type_filter.rs index 31fa106d43577..f2028483989ff 100644 --- a/crates/sui-graphql-rpc/src/types/type_filter.rs +++ b/crates/sui-graphql-rpc/src/types/type_filter.rs @@ -268,32 +268,6 @@ impl TypeFilter { } impl FqNameFilter { - /// Modify `query` to apply this filter, treating `package` as the column containing the package - /// address, `module` as the module containing the module name, and `name` as the column - /// containing the module member name. - pub(crate) fn apply( - &self, - query: Query, - package: P, - module: M, - name: N, - ) -> Query - where - Query: QueryDsl, - P: Field, - M: Field, - N: Field, - QS: QuerySource, - { - match self { - FqNameFilter::ByModule(filter) => filter.apply(query, package, module), - FqNameFilter::ByFqName(p, m, n) => query - .filter(package.eq(p.into_vec())) - .filter(module.eq(m.clone())) - .filter(name.eq(n.clone())), - } - } - /// Try to create a filter whose results are the intersection of the results of the input /// filters (`self` and `other`). This may not be possible if the resulting filter is /// inconsistent (e.g. a filter that requires the module member's package to be at two different diff --git a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap index 93eadb6691d92..fd04f186f34b6 100644 --- a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap +++ b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap @@ -102,8 +102,27 @@ type Address implements IOwner { """ Similar behavior to the `transactionBlocks` in Query but supporting the additional `AddressTransactionBlockRelationship` filter, which defaults to `SIGN`. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - transactionBlocks(first: Int, after: String, last: Int, before: String, relation: AddressTransactionBlockRelationship, filter: TransactionBlockFilter): TransactionBlockConnection! + transactionBlocks(first: Int, after: String, last: Int, before: String, relation: AddressTransactionBlockRelationship, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! } type AddressConnection { @@ -413,8 +432,25 @@ type Checkpoint { epoch: Epoch """ Transactions in this checkpoint. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range consists of all transactions in this checkpoint. """ - transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! } type CheckpointConnection { @@ -521,8 +557,27 @@ type Coin implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -680,8 +735,27 @@ type CoinMetadata implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -1097,8 +1171,25 @@ type Epoch { checkpoints(first: Int, after: String, last: Int, before: String): CheckpointConnection! """ The epoch's corresponding transaction blocks. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range consists of all transactions in this epoch. """ - transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! } type Event { @@ -1433,7 +1524,7 @@ interface IObject { """ The transaction blocks that sent objects to this object. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -1976,8 +2067,27 @@ type MoveObject implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -2165,8 +2275,27 @@ type MovePackage implements IObject & IOwner { The transaction blocks that sent objects to this package. Note that objects that have been sent to a package become inaccessible. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the package's content. """ @@ -2544,8 +2673,27 @@ type Object implements IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -3146,8 +3294,27 @@ type Query { checkpoints(first: Int, after: String, last: Int, before: String): CheckpointConnection! """ The transaction blocks that exist in the network. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The events that exist in the network. """ @@ -3365,6 +3532,14 @@ type ServiceConfig { Maximum nesting allowed in struct fields when calculating the layout of a single Move Type. """ maxMoveValueDepth: Int! + """ + Maximum number of transaction ids that can be passed to a `TransactionBlockFilter`. + """ + maxTransactionIds: Int! + """ + Maximum number of candidates to scan when gathering a page of results. + """ + maxScanLimit: Int! } """ @@ -3582,8 +3757,27 @@ type StakedSui implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """ @@ -3784,8 +3978,27 @@ type SuinsRegistration implements IMoveObject & IObject & IOwner { storageRebate: BigInt """ The transaction blocks that sent objects to this object. + + `scanLimit` restricts the number of candidate transactions scanned when gathering a page of + results. It is required for queries that apply more than two complex filters (on function, + kind, sender, recipient, input object, changed object, or ids), and can be at most + `serviceConfig.maxScanLimit`. + + When the scan limit is reached the page will be returned even if it has fewer than `first` + results when paginating forward (`last` when paginating backwards). If there are more + transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to + `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last + transaction that was scanned as opposed to the last (or first) transaction in the page. + + Requesting the next (or previous) page after this cursor will resume the search, scanning + the next `scanLimit` many transactions in the direction of pagination, and so on until all + transactions in the scanning range have been visited. + + By default, the scanning range includes all transactions known to GraphQL, but it can be + restricted by the `after` and `before` cursors, and the `beforeCheckpoint`, + `afterCheckpoint` and `atCheckpoint` filters. """ - receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter): TransactionBlockConnection! + receivedTransactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ The Base64-encoded BCS serialization of the object's content. """