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. """