diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index 3bd5fea50a24e..ff3a4f8288e2b 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -92,7 +92,7 @@ jobs: - name: Install Foundry Dependencies working-directory: bridge/evm run: | - forge soldeer update + forge install https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable@v5.0.1 https://github.com/foundry-rs/forge-std@v1.3.0 https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades --no-git --no-commit - name: cargo test run: | cargo nextest run --profile ci -E 'package(sui-bridge)' @@ -114,7 +114,7 @@ jobs: - name: Install Foundry Dependencies working-directory: bridge/evm run: | - forge soldeer update + forge install https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable@v5.0.1 https://github.com/foundry-rs/forge-std@v1.3.0 https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades --no-git --no-commit - name: Check Bridge EVM Unit Tests shell: bash working-directory: bridge/evm diff --git a/bridge/evm/.gitignore b/bridge/evm/.gitignore index eede9111c4f1a..b03db232ccd9e 100644 --- a/bridge/evm/.gitignore +++ b/bridge/evm/.gitignore @@ -10,4 +10,5 @@ out*/ lcov.info broadcast/**/31337 -dependencies +lib/* + diff --git a/bridge/evm/README.md b/bridge/evm/README.md index 94667326299ca..97cbdb270a45c 100644 --- a/bridge/evm/README.md +++ b/bridge/evm/README.md @@ -1,6 +1,6 @@ # 🏄‍♂️ Quick Start -This project leverages [Foundry](https://github.com/foundry-rs/foundry) to manage dependencies (via soldeer), contract compilation, testing, deployment, and on chain interactions via Solidity scripting. +This project leverages [Foundry](https://github.com/foundry-rs/foundry) to manage dependencies, contract compilation, testing, deployment, and on chain interactions via Solidity scripting. #### Environment configuration @@ -14,7 +14,7 @@ Duplicate rename the `.env.example` file to `.env`. You'll need accounts and api To install the project dependencies, run: ```bash -forge soldeer update +forge install https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable@v5.0.1 https://github.com/foundry-rs/forge-std@v1.3.0 https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades --no-git --no-commit ``` #### Compilation @@ -28,7 +28,8 @@ forge compile #### Testing ```bash -forge test +forge clean +forge test --ffi ``` #### Coverage @@ -44,13 +45,15 @@ forge coverage > The file should be named `.json` and should have the same fields and in the same order (alphabetical) as the `example.json`. ```bash -forge script script/deploy_bridge.s.sol --rpc-url <> --broadcast --verify +forge clean +forge script script/deploy_bridge.s.sol --rpc-url <> --broadcast --verify --ffi ``` **Local deployment** ```bash -forge script script/deploy_bridge.s.sol --fork-url anvil --broadcast +forge clean +forge script script/deploy_bridge.s.sol --fork-url anvil --broadcast --ffi ``` All deployments are saved in the `broadcast` directory. diff --git a/bridge/evm/foundry.toml b/bridge/evm/foundry.toml index ba31fcbba8e08..b2a3ebfec2a6d 100644 --- a/bridge/evm/foundry.toml +++ b/bridge/evm/foundry.toml @@ -3,31 +3,20 @@ src = 'contracts' test = 'test' no_match_test = "testSkip" out = 'out' -libs = ['dependencies'] +libs = ['lib'] solc = "0.8.20" build_info = true extra_output = ["storageLayout"] fs_permissions = [{ access = "read", path = "/"}] gas_reports = ["SuiBridge"] -ffi = true - [fmt] line_length = 100 - [fuzz] runs = 1000 - [rpc_endpoints] mainnet = "${MAINNET_RPC_URL}" sepolia = "${SEPOLIA_RPC_URL}" anvil = "http://localhost:8545" - [etherscan] sepolia = { key = "${ETHERSCAN_API_KEY}" } -mainnet = { key = "${ETHERSCAN_API_KEY}" } - -[dependencies] -forge-std = "1.9.2" -openzeppelin-foundry-upgrades = "0.3.1" -"@openzeppelin-contracts-upgradeable" = "5.0.1" -"@openzeppelin-contracts" = "5.0.1" \ No newline at end of file +mainnet = { key = "${ETHERSCAN_API_KEY}" } \ No newline at end of file diff --git a/bridge/evm/remappings.txt b/bridge/evm/remappings.txt index c680ee33d8dd9..5279b569511f7 100644 --- a/bridge/evm/remappings.txt +++ b/bridge/evm/remappings.txt @@ -1,8 +1,5 @@ -@forge-std=dependencies/forge-std-1.9.2/src -@openzeppelin/foundry-upgrades=dependencies/openzeppelin-foundry-upgrades-0.3.1/src -@openzeppelin/contracts=dependencies/@openzeppelin-contracts-5.0.1 -@openzeppelin/contracts-upgradeable=dependencies/@openzeppelin-contracts-upgradeable-5.0.1 -@forge-std-1.9.2=dependencies/forge-std-1.9.2 -@openzeppelin-foundry-upgrades-0.3.1=dependencies/openzeppelin-foundry-upgrades-0.3.1 -@openzeppelin-contracts-upgradeable-5.0.1=dependencies/@openzeppelin-contracts-upgradeable-5.0.1 -@openzeppelin-contracts-5.0.1=dependencies/@openzeppelin-contracts-5.0.1 \ No newline at end of file +@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@openzeppelin/openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/ +ds-test/=lib/forge-std/lib/ds-test/src/ +forge-std/=lib/openzeppelin-foundry-upgrades/lib/forge-std/src/ \ No newline at end of file diff --git a/bridge/evm/soldeer.lock b/bridge/evm/soldeer.lock deleted file mode 100644 index 20bd2407a347b..0000000000000 --- a/bridge/evm/soldeer.lock +++ /dev/null @@ -1,24 +0,0 @@ - -[[dependencies]] -name = "forge-std" -version = "1.9.2" -source = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_9_2_06-08-2024_17:31:25_forge-std-1.9.2.zip" -checksum = "20fd008c7c69b6c737cc0284469d1c76497107bc3e004d8381f6d8781cb27980" - -[[dependencies]] -name = "openzeppelin-foundry-upgrades" -version = "0.3.1" -source = "https://soldeer-revisions.s3.amazonaws.com/openzeppelin-foundry-upgrades/0_3_1_25-06-2024_18:12:33_openzeppelin-foundry-upgrades.zip" -checksum = "16a43c67b7c62e4a638b669b35f7b19c98a37278811fe910750b62b6e6fdffa7" - -[[dependencies]] -name = "@openzeppelin-contracts-upgradeable" -version = "5.0.1" -source = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts-upgradeable/5_0_1_22-01-2024_13:15:10_contracts-upgradeable.zip" -checksum = "cca37ad1d376a5c3954d1c2a8d2675339f182eee535caa7ba7ebf8d589a2c19a" - -[[dependencies]] -name = "@openzeppelin-contracts" -version = "5.0.1" -source = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_0_1_22-01-2024_13:14:01_contracts.zip" -checksum = "c256cbf6f5f38d3b65c7528bbffb530d0bdb818a20c9d5b61235a829202d7df7" diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/combo_filter_error.exp b/crates/sui-graphql-e2e-tests/tests/event_connection/combo_filter_error.exp new file mode 100644 index 0000000000000..2e134867efbdc --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/combo_filter_error.exp @@ -0,0 +1,43 @@ +processed 5 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 9-28: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5380800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 30: +//# run Test::M2::emit_emit_a --sender A --args 20 +events: Event { package_id: Test, transaction_module: Identifier("M2"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [20, 0, 0, 0, 0, 0, 0, 0] } +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3, line 32: +//# create-checkpoint +Checkpoint created: 1 + +task 4, lines 34-51: +//# run-graphql +Response: { + "data": null, + "errors": [ + { + "message": "Filtering by both emitting module and event type is not supported", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "events" + ], + "extensions": { + "code": "BAD_USER_INPUT" + } + } + ] +} diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/combo_filter_error.move b/crates/sui-graphql-e2e-tests/tests/event_connection/combo_filter_error.move new file mode 100644 index 0000000000000..ad38316463e76 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/combo_filter_error.move @@ -0,0 +1,51 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Tests that fetching events filtered on both emitting module and event would result +// in an error. + +//# init --protocol-version 51 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + use sui::event; + + public struct EventA has copy, drop { + new_value: u64 + } + + public fun emit_a(value: u64) { + event::emit(EventA { new_value: value }) + } +} + +module Test::M2 { + use Test::M1; + + public fun emit_emit_a(value: u64) { + M1::emit_a(value); + } +} + +//# run Test::M2::emit_emit_a --sender A --args 20 + +//# create-checkpoint + +//# run-graphql +{ + events(filter: {sender: "@{A}", emittingModule: "@{Test}::M2", eventType: "@{Test}::M1::EventA"}) { + nodes { + sendingModule { + name + } + type { + repr + } + sender { + address + } + json + bcs + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/no_filter.exp b/crates/sui-graphql-e2e-tests/tests/event_connection/no_filter.exp new file mode 100644 index 0000000000000..3d916fe5b9508 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/no_filter.exp @@ -0,0 +1,254 @@ +processed 6 tasks + +init: +A: object(0,0) + +task 1, lines 6-25: +//# publish +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 4970400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 27: +//# run Test::M1::emit --sender A --args 0 +events: Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [0, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [1, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [2, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [3, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [4, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [5, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [6, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [7, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [8, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [9, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [10, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [11, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [12, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [13, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [14, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [15, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [16, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [17, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [18, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [19, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [20, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [21, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [22, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [23, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [24, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [25, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [26, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [27, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [28, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [29, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [30, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [31, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [32, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [33, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [34, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [35, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [36, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [37, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [38, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [39, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [40, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [41, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [42, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [43, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [44, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [45, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [46, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [47, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [48, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [49, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [50, 0, 0, 0, 0, 0, 0, 0] } +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3, line 29: +//# create-checkpoint +Checkpoint created: 1 + +task 4, lines 31-44: +//# run-graphql +Response: { + "data": { + "events": { + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "eyJ0eCI6MiwiZSI6MCwiYyI6MX0", + "endCursor": "eyJ0eCI6MiwiZSI6MTksImMiOjF9" + }, + "nodes": [ + { + "json": { + "new_value": "0" + } + }, + { + "json": { + "new_value": "1" + } + }, + { + "json": { + "new_value": "2" + } + }, + { + "json": { + "new_value": "3" + } + }, + { + "json": { + "new_value": "4" + } + }, + { + "json": { + "new_value": "5" + } + }, + { + "json": { + "new_value": "6" + } + }, + { + "json": { + "new_value": "7" + } + }, + { + "json": { + "new_value": "8" + } + }, + { + "json": { + "new_value": "9" + } + }, + { + "json": { + "new_value": "10" + } + }, + { + "json": { + "new_value": "11" + } + }, + { + "json": { + "new_value": "12" + } + }, + { + "json": { + "new_value": "13" + } + }, + { + "json": { + "new_value": "14" + } + }, + { + "json": { + "new_value": "15" + } + }, + { + "json": { + "new_value": "16" + } + }, + { + "json": { + "new_value": "17" + } + }, + { + "json": { + "new_value": "18" + } + }, + { + "json": { + "new_value": "19" + } + } + ] + } + } +} + +task 5, lines 46-59: +//# run-graphql --cursors {"tx":2,"e":19,"c":1} +Response: { + "data": { + "events": { + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "eyJ0eCI6MiwiZSI6MjAsImMiOjF9", + "endCursor": "eyJ0eCI6MiwiZSI6MzksImMiOjF9" + }, + "nodes": [ + { + "json": { + "new_value": "20" + } + }, + { + "json": { + "new_value": "21" + } + }, + { + "json": { + "new_value": "22" + } + }, + { + "json": { + "new_value": "23" + } + }, + { + "json": { + "new_value": "24" + } + }, + { + "json": { + "new_value": "25" + } + }, + { + "json": { + "new_value": "26" + } + }, + { + "json": { + "new_value": "27" + } + }, + { + "json": { + "new_value": "28" + } + }, + { + "json": { + "new_value": "29" + } + }, + { + "json": { + "new_value": "30" + } + }, + { + "json": { + "new_value": "31" + } + }, + { + "json": { + "new_value": "32" + } + }, + { + "json": { + "new_value": "33" + } + }, + { + "json": { + "new_value": "34" + } + }, + { + "json": { + "new_value": "35" + } + }, + { + "json": { + "new_value": "36" + } + }, + { + "json": { + "new_value": "37" + } + }, + { + "json": { + "new_value": "38" + } + }, + { + "json": { + "new_value": "39" + } + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/no_filter.move b/crates/sui-graphql-e2e-tests/tests/event_connection/no_filter.move new file mode 100644 index 0000000000000..aaca18be1a12a --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/no_filter.move @@ -0,0 +1,59 @@ +// 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 { + use sui::event; + + public struct EventA has copy, drop { + new_value: u64 + } + + public entry fun no_emit(value: u64): u64 { + value + } + + public entry fun emit(value: u64) { + let mut i = 0; + while (i < 51) { + event::emit(EventA { new_value: value + i }); + i = i + 1; + } + } +} + +//# run Test::M1::emit --sender A --args 0 + +//# create-checkpoint + +//# run-graphql +{ + events { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + nodes { + json + } + } +} + +//# run-graphql --cursors {"tx":2,"e":19,"c":1} +{ + events(after: "@{cursor_0}") { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + nodes { + json + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/tx_digest.exp b/crates/sui-graphql-e2e-tests/tests/event_connection/tx_digest.exp new file mode 100644 index 0000000000000..12f3b6c2ebbb3 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/tx_digest.exp @@ -0,0 +1,326 @@ +processed 21 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 10-26: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 4795600, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 28: +//# run Test::M1::no_emit --sender A --args 0 +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3, line 30: +//# run Test::M1::emit_2 --sender A --args 2 +events: Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [2, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [3, 0, 0, 0, 0, 0, 0, 0] } +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 4, line 32: +//# run Test::M1::emit_2 --sender B --args 4 +events: Event { package_id: Test, transaction_module: Identifier("M1"), sender: B, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [4, 0, 0, 0, 0, 0, 0, 0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: B, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [5, 0, 0, 0, 0, 0, 0, 0] } +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 5, line 34: +//# create-checkpoint +Checkpoint created: 1 + +task 6, lines 36-43: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "nodes": [ + { + "digest": "AXoD3PWjAdYov3o7FaWgAqJA8RmvQrjwxGxAi2MNEujz" + }, + { + "digest": "3nuQk9o2VVoqWbF6gS5vBTPwVLhMRbFJREDCvQJUavZ2" + }, + { + "digest": "5VAhspujQVcgJNvqe9Ed8BuFTeZdTRCCTzS6WwSZ9Dke" + }, + { + "digest": "8n1pk5fYM7v7tvsh4dcKxLRy8uf3he24FZCwdEKi9cSj" + }, + { + "digest": "4dqR1zeomDMNHbUAZjooSZXrosPEb67gvvsUFeUSet9v" + } + ] + } + } +} + +task 7, lines 45-55: +//# run-graphql +Response: { + "data": { + "events": { + "edges": [ + { + "cursor": "eyJ0eCI6MywiZSI6MCwiYyI6MX0", + "node": { + "json": { + "new_value": "2" + } + } + }, + { + "cursor": "eyJ0eCI6MywiZSI6MSwiYyI6MX0", + "node": { + "json": { + "new_value": "3" + } + } + } + ] + } + } +} + +task 8, lines 57-68: +//# run-graphql --cursors {"tx":3,"e":1,"c":1} +Response: { + "data": { + "events": { + "edges": [] + } + } +} + +task 9, lines 70-83: +//# run-graphql --cursors {"tx":1,"e":1,"c":1} +Response: { + "data": { + "events": { + "edges": [] + } + } +} + +task 10, lines 86-96: +//# run-graphql +Response: { + "data": { + "events": { + "edges": [ + { + "cursor": "eyJ0eCI6NCwiZSI6MCwiYyI6MX0", + "node": { + "json": { + "new_value": "4" + } + } + }, + { + "cursor": "eyJ0eCI6NCwiZSI6MSwiYyI6MX0", + "node": { + "json": { + "new_value": "5" + } + } + } + ] + } + } +} + +task 11, lines 98-108: +//# run-graphql --cursors {"tx":4,"e":0,"c":1} +Response: { + "data": { + "events": { + "edges": [ + { + "cursor": "eyJ0eCI6NCwiZSI6MSwiYyI6MX0", + "node": { + "json": { + "new_value": "5" + } + } + } + ] + } + } +} + +task 12, lines 111-121: +//# run-graphql +Response: { + "data": { + "events": { + "edges": [ + { + "cursor": "eyJ0eCI6MywiZSI6MCwiYyI6MX0", + "node": { + "json": { + "new_value": "2" + } + } + }, + { + "cursor": "eyJ0eCI6MywiZSI6MSwiYyI6MX0", + "node": { + "json": { + "new_value": "3" + } + } + } + ] + } + } +} + +task 13, lines 123-134: +//# run-graphql --cursors {"tx":3,"e":1,"c":1} +Response: { + "data": { + "events": { + "edges": [ + { + "cursor": "eyJ0eCI6MywiZSI6MCwiYyI6MX0", + "node": { + "json": { + "new_value": "2" + } + } + } + ] + } + } +} + +task 14, lines 136-149: +//# run-graphql --cursors {"tx":4,"e":1,"c":1} +Response: { + "data": { + "events": { + "edges": [] + } + } +} + +task 15, lines 152-162: +//# run-graphql +Response: { + "data": { + "events": { + "edges": [ + { + "cursor": "eyJ0eCI6NCwiZSI6MCwiYyI6MX0", + "node": { + "json": { + "new_value": "4" + } + } + }, + { + "cursor": "eyJ0eCI6NCwiZSI6MSwiYyI6MX0", + "node": { + "json": { + "new_value": "5" + } + } + } + ] + } + } +} + +task 16, lines 164-174: +//# run-graphql --cursors {"tx":4,"e":1,"c":1} +Response: { + "data": { + "events": { + "edges": [ + { + "cursor": "eyJ0eCI6NCwiZSI6MCwiYyI6MX0", + "node": { + "json": { + "new_value": "4" + } + } + } + ] + } + } +} + +task 17, lines 176-187: +//# run-graphql +Response: { + "data": { + "events": { + "edges": [ + { + "cursor": "eyJ0eCI6MywiZSI6MCwiYyI6MX0", + "node": { + "json": { + "new_value": "2" + } + } + }, + { + "cursor": "eyJ0eCI6MywiZSI6MSwiYyI6MX0", + "node": { + "json": { + "new_value": "3" + } + } + } + ] + } + } +} + +task 18, lines 189-200: +//# run-graphql +Response: { + "data": { + "events": { + "edges": [ + { + "cursor": "eyJ0eCI6NCwiZSI6MCwiYyI6MX0", + "node": { + "json": { + "new_value": "4" + } + } + }, + { + "cursor": "eyJ0eCI6NCwiZSI6MSwiYyI6MX0", + "node": { + "json": { + "new_value": "5" + } + } + } + ] + } + } +} + +task 19, lines 202-213: +//# run-graphql +Response: { + "data": { + "events": { + "edges": [] + } + } +} + +task 20, lines 215-226: +//# run-graphql +Response: { + "data": { + "events": { + "edges": [] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/tx_digest.move b/crates/sui-graphql-e2e-tests/tests/event_connection/tx_digest.move new file mode 100644 index 0000000000000..58b17e5fd21d8 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/tx_digest.move @@ -0,0 +1,226 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Tests that fetching events filtered on a tx digest that has no events correctly returns no nodes. +// Also tests that fetching events filtered on a tx digest that has events returns the correct +// number of page-limit-bound nodes. + +//# init --protocol-version 48 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + use sui::event; + + public struct EventA has copy, drop { + new_value: u64 + } + + public entry fun no_emit(value: u64): u64 { + value + } + + public entry fun emit_2(value: u64) { + event::emit(EventA { new_value: value }); + event::emit(EventA { new_value: value + 1}) + } +} + +//# run Test::M1::no_emit --sender A --args 0 + +//# run Test::M1::emit_2 --sender A --args 2 + +//# run Test::M1::emit_2 --sender B --args 4 + +//# create-checkpoint + +//# run-graphql +{ + transactionBlocks { + nodes { + digest + } + } +} + +//# run-graphql +{ + events(filter: {transactionDigest: "8n1pk5fYM7v7tvsh4dcKxLRy8uf3he24FZCwdEKi9cSj"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql --cursors {"tx":3,"e":1,"c":1} +# When the tx digest and after cursor are on the same tx, we'll use the after cursor's event sequence number +{ + events(after: "@{cursor_0}" filter: {transactionDigest: "8n1pk5fYM7v7tvsh4dcKxLRy8uf3he24FZCwdEKi9cSj"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql --cursors {"tx":1,"e":1,"c":1} +# If the after cursor does not match the transaction digest's tx sequence number, +# we will get an empty response, since it's not possible to fetch an event +# that isn't of the same tx sequence number +{ + events(after: "@{cursor_0}" filter: {transactionDigest: "8n1pk5fYM7v7tvsh4dcKxLRy8uf3he24FZCwdEKi9cSj"}) { + edges { + cursor + node { + json + } + } + } +} + + +//# run-graphql +{ + events(filter: {transactionDigest: "4dqR1zeomDMNHbUAZjooSZXrosPEb67gvvsUFeUSet9v"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql --cursors {"tx":4,"e":0,"c":1} +{ + events(after: "@{cursor_0}" filter: {transactionDigest: "4dqR1zeomDMNHbUAZjooSZXrosPEb67gvvsUFeUSet9v"}) { + edges { + cursor + node { + json + } + } + } +} + + +//# run-graphql +{ + events(last: 10 filter: {transactionDigest: "8n1pk5fYM7v7tvsh4dcKxLRy8uf3he24FZCwdEKi9cSj"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql --cursors {"tx":3,"e":1,"c":1} +# When the tx digest and cursor are on the same tx, we'll use the cursor's event sequence number +{ + events(last: 10 before: "@{cursor_0}" filter: {transactionDigest: "8n1pk5fYM7v7tvsh4dcKxLRy8uf3he24FZCwdEKi9cSj"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql --cursors {"tx":4,"e":1,"c":1} +# If the cursor does not match the transaction digest's tx sequence number, +# we will get an empty response, since it's not possible to fetch an event +# that isn't of the same tx sequence number +{ + events(last: 10 before: "@{cursor_0}" filter: {transactionDigest: "8n1pk5fYM7v7tvsh4dcKxLRy8uf3he24FZCwdEKi9cSj"}) { + edges { + cursor + node { + json + } + } + } +} + + +//# run-graphql +{ + events(last: 10 filter: {transactionDigest: "4dqR1zeomDMNHbUAZjooSZXrosPEb67gvvsUFeUSet9v"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql --cursors {"tx":4,"e":1,"c":1} +{ + events(last: 10 before: "@{cursor_0}" filter: {transactionDigest: "4dqR1zeomDMNHbUAZjooSZXrosPEb67gvvsUFeUSet9v"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql +# correct sender +{ + events(filter: {sender: "@{A}" transactionDigest: "8n1pk5fYM7v7tvsh4dcKxLRy8uf3he24FZCwdEKi9cSj"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql +# correct sender +{ + events(filter: {sender: "@{B}" transactionDigest: "4dqR1zeomDMNHbUAZjooSZXrosPEb67gvvsUFeUSet9v"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql +# incorrect sender +{ + events(filter: {sender: "@{B}" transactionDigest: "8n1pk5fYM7v7tvsh4dcKxLRy8uf3he24FZCwdEKi9cSj"}) { + edges { + cursor + node { + json + } + } + } +} + +//# run-graphql +# incorrect sender +{ + events(filter: {sender: "@{A}" transactionDigest: "4dqR1zeomDMNHbUAZjooSZXrosPEb67gvvsUFeUSet9v"}) { + edges { + cursor + node { + json + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/type_filter.exp b/crates/sui-graphql-e2e-tests/tests/event_connection/type_filter.exp new file mode 100644 index 0000000000000..3afb31848780e --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/type_filter.exp @@ -0,0 +1,211 @@ +processed 12 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 6-34: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 6604400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 36: +//# run Test::M2::emit_emit_a --sender A --args 20 +events: Event { package_id: Test, transaction_module: Identifier("M2"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [] }, contents: [20, 0, 0, 0, 0, 0, 0, 0] } +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3, line 38: +//# create-checkpoint +Checkpoint created: 1 + +task 4, lines 40-57: +//# run-graphql +Response: { + "data": { + "events": { + "nodes": [ + { + "sendingModule": { + "name": "M2" + }, + "type": { + "repr": "0x6edb181eb03cea19a3c4b09d2d6b5de8d0a741df186d072d18b2030eb36faee1::M1::EventA" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "new_value": "20" + }, + "bcs": "FAAAAAAAAAA=" + } + ] + } + } +} + +task 5, line 59: +//# run Test::M2::emit_b --sender A --args 42 +events: Event { package_id: Test, transaction_module: Identifier("M2"), sender: A, type_: StructTag { address: Test, module: Identifier("M2"), name: Identifier("EventB"), type_params: [] }, contents: [42, 0, 0, 0, 0, 0, 0, 0] } +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 6, line 61: +//# run Test::M2::emit_b --sender B --args 43 +events: Event { package_id: Test, transaction_module: Identifier("M2"), sender: B, type_: StructTag { address: Test, module: Identifier("M2"), name: Identifier("EventB"), type_params: [] }, contents: [43, 0, 0, 0, 0, 0, 0, 0] } +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 7, line 63: +//# create-checkpoint +Checkpoint created: 2 + +task 8, lines 65-82: +//# run-graphql +Response: { + "data": { + "events": { + "nodes": [ + { + "sendingModule": { + "name": "M2" + }, + "type": { + "repr": "0x6edb181eb03cea19a3c4b09d2d6b5de8d0a741df186d072d18b2030eb36faee1::M1::EventA" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "new_value": "20" + }, + "bcs": "FAAAAAAAAAA=" + } + ] + } + } +} + +task 9, lines 84-101: +//# run-graphql +Response: { + "data": { + "events": { + "nodes": [ + { + "sendingModule": { + "name": "M2" + }, + "type": { + "repr": "0x6edb181eb03cea19a3c4b09d2d6b5de8d0a741df186d072d18b2030eb36faee1::M2::EventB" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "new_value": "42" + }, + "bcs": "KgAAAAAAAAA=" + } + ] + } + } +} + +task 10, lines 103-120: +//# run-graphql +Response: { + "data": { + "events": { + "nodes": [ + { + "sendingModule": { + "name": "M2" + }, + "type": { + "repr": "0x6edb181eb03cea19a3c4b09d2d6b5de8d0a741df186d072d18b2030eb36faee1::M1::EventA" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "new_value": "20" + }, + "bcs": "FAAAAAAAAAA=" + }, + { + "sendingModule": { + "name": "M2" + }, + "type": { + "repr": "0x6edb181eb03cea19a3c4b09d2d6b5de8d0a741df186d072d18b2030eb36faee1::M2::EventB" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "new_value": "42" + }, + "bcs": "KgAAAAAAAAA=" + } + ] + } + } +} + +task 11, lines 122-139: +//# run-graphql +Response: { + "data": { + "events": { + "nodes": [ + { + "sendingModule": { + "name": "M2" + }, + "type": { + "repr": "0x6edb181eb03cea19a3c4b09d2d6b5de8d0a741df186d072d18b2030eb36faee1::M1::EventA" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "new_value": "20" + }, + "bcs": "FAAAAAAAAAA=" + }, + { + "sendingModule": { + "name": "M2" + }, + "type": { + "repr": "0x6edb181eb03cea19a3c4b09d2d6b5de8d0a741df186d072d18b2030eb36faee1::M2::EventB" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "new_value": "42" + }, + "bcs": "KgAAAAAAAAA=" + }, + { + "sendingModule": { + "name": "M2" + }, + "type": { + "repr": "0x6edb181eb03cea19a3c4b09d2d6b5de8d0a741df186d072d18b2030eb36faee1::M2::EventB" + }, + "sender": { + "address": "0xa7b032703878aa74c3126935789fd1d4d7e111d5911b09247d6963061c312b5a" + }, + "json": { + "new_value": "43" + }, + "bcs": "KwAAAAAAAAA=" + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/type_filter.move b/crates/sui-graphql-e2e-tests/tests/event_connection/type_filter.move new file mode 100644 index 0000000000000..0ca546a3a94ef --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/type_filter.move @@ -0,0 +1,140 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --protocol-version 51 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + use sui::event; + + public struct EventA has copy, drop { + new_value: u64 + } + + public fun emit_a(value: u64) { + event::emit(EventA { new_value: value }) + } +} + +module Test::M2 { + use sui::event; + use Test::M1; + + public struct EventB has copy, drop { + new_value: u64 + } + + public fun emit_emit_a(value: u64) { + M1::emit_a(value); + } + + public fun emit_b(value: u64) { + event::emit(EventB { new_value: value }) + } +} + +//# run Test::M2::emit_emit_a --sender A --args 20 + +//# create-checkpoint + +//# run-graphql +{ + events(filter: {sender: "@{A}", eventType: "@{Test}::M1::EventA"}) { + nodes { + sendingModule { + name + } + type { + repr + } + sender { + address + } + json + bcs + } + } +} + +//# run Test::M2::emit_b --sender A --args 42 + +//# run Test::M2::emit_b --sender B --args 43 + +//# create-checkpoint + +//# run-graphql +{ + events(filter: {sender: "@{A}", eventType: "@{Test}::M1"}) { + nodes { + sendingModule { + name + } + type { + repr + } + sender { + address + } + json + bcs + } + } +} + +//# run-graphql +{ + events(filter: {sender: "@{A}", eventType: "@{Test}::M2"}) { + nodes { + sendingModule { + name + } + type { + repr + } + sender { + address + } + json + bcs + } + } +} + +//# run-graphql +{ + events(filter: {sender: "@{A}", eventType: "@{Test}"}) { + nodes { + sendingModule { + name + } + type { + repr + } + sender { + address + } + json + bcs + } + } +} + +//# run-graphql +{ + events(filter: {eventType: "@{Test}"}) { + nodes { + sendingModule { + name + } + type { + repr + } + sender { + address + } + json + bcs + } + } +} + diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/type_param_filter.exp b/crates/sui-graphql-e2e-tests/tests/event_connection/type_param_filter.exp new file mode 100644 index 0000000000000..383af1455f8f3 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/type_param_filter.exp @@ -0,0 +1,182 @@ +processed 10 tasks + +init: +A: object(0,0), B: object(0,1) + +task 1, lines 6-29: +//# publish +created: object(1,0) +mutated: object(0,2) +gas summary: computation_cost: 1000000, storage_cost: 5996400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2, line 32: +//# run Test::M1::emit_T1 --sender A +events: Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [Struct(StructTag { address: Test, module: Identifier("M1"), name: Identifier("T1"), type_params: [] })] }, contents: [0] } +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3, line 34: +//# run Test::M1::emit_T2 --sender A +events: Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [Struct(StructTag { address: Test, module: Identifier("M1"), name: Identifier("T2"), type_params: [] })] }, contents: [0] } +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 4, line 36: +//# run Test::M1::emit_both --sender A +events: Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [Struct(StructTag { address: Test, module: Identifier("M1"), name: Identifier("T1"), type_params: [] })] }, contents: [0] }, Event { package_id: Test, transaction_module: Identifier("M1"), sender: A, type_: StructTag { address: Test, module: Identifier("M1"), name: Identifier("EventA"), type_params: [Struct(StructTag { address: Test, module: Identifier("M1"), name: Identifier("T2"), type_params: [] })] }, contents: [0] } +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 988000, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5, line 38: +//# create-checkpoint +Checkpoint created: 1 + +task 6, lines 40-47: +//# run-graphql +Response: { + "data": { + "transactionBlocks": { + "nodes": [ + { + "digest": "J7mHXcoa7LXwyjzZUWsk8zvYZjek359TM4d2hQK4LGHo" + }, + { + "digest": "Ch3i5cdtNPU5v8oSg3V5cdKtZDa3YjqjMd7Qh4NQLAx6" + }, + { + "digest": "6taTf6v2NQCFtd9A4nw3mBbkHwUw8RUoJjqQGSB5cBNt" + }, + { + "digest": "AEXpVZ7Vpsk7ZSTsR2QNPT7zhq8oqpJXRGgx3kAaTdn" + }, + { + "digest": "9nu1ivpL9hHcbJ9GwGfmD3Kuet5w74t2GBp8f1Ggy3UD" + } + ] + } + } +} + +task 7, lines 49-62: +//# run-graphql +Response: { + "data": { + "events": { + "nodes": [ + { + "type": { + "repr": "0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::EventA<0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::T1>" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "value": { + "dummy_field": false + } + } + }, + { + "type": { + "repr": "0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::EventA<0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::T2>" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "value": { + "dummy_field": false + } + } + }, + { + "type": { + "repr": "0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::EventA<0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::T1>" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "value": { + "dummy_field": false + } + } + }, + { + "type": { + "repr": "0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::EventA<0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::T2>" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "value": { + "dummy_field": false + } + } + } + ] + } + } +} + +task 8, lines 64-77: +//# run-graphql +Response: { + "data": { + "events": { + "nodes": [ + { + "type": { + "repr": "0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::EventA<0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::T1>" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "value": { + "dummy_field": false + } + } + }, + { + "type": { + "repr": "0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::EventA<0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::T1>" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "value": { + "dummy_field": false + } + } + } + ] + } + } +} + +task 9, lines 79-92: +//# run-graphql +Response: { + "data": { + "events": { + "nodes": [ + { + "type": { + "repr": "0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::EventA<0xe722de9e58a9bab3a202b769b7518f91f852460d3d2c6d6743c301d08b9e614a::M1::T2>" + }, + "sender": { + "address": "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" + }, + "json": { + "value": { + "dummy_field": false + } + } + } + ] + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/event_connection/type_param_filter.move b/crates/sui-graphql-e2e-tests/tests/event_connection/type_param_filter.move new file mode 100644 index 0000000000000..128362999b50b --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/event_connection/type_param_filter.move @@ -0,0 +1,92 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --protocol-version 51 --addresses Test=0x0 --accounts A B --simulator + +//# publish +module Test::M1 { + use sui::event; + + public struct T1 has copy, drop {} + public struct T2 has copy, drop {} + + public struct EventA has copy, drop { + value: T + } + + public fun emit_T1() { + event::emit(EventA { value: T1 {} }) + } + + public fun emit_T2() { + event::emit(EventA { value: T2 {} }) + } + + public fun emit_both() { + event::emit(EventA { value: T1 {} }); + event::emit(EventA { value: T2 {} }) + } +} + + +//# run Test::M1::emit_T1 --sender A + +//# run Test::M1::emit_T2 --sender A + +//# run Test::M1::emit_both --sender A + +//# create-checkpoint + +//# run-graphql +{ + transactionBlocks { + nodes { + digest + } + } +} + +//# run-graphql +{ + events(filter: {eventType: "@{Test}::M1::EventA"}) { + nodes { + type { + repr + } + sender { + address + } + json + } + } +} + +//# run-graphql +{ + events(filter: {eventType: "@{Test}::M1::EventA<@{Test}::M1::T1>"}) { + nodes { + type { + repr + } + sender { + address + } + json + } + } +} + +//# run-graphql +{ + events(filter: {eventType: "@{Test}::M1::EventA<@{Test}::M1::T2>", transactionDigest: "9nu1ivpL9hHcbJ9GwGfmD3Kuet5w74t2GBp8f1Ggy3UD"}) { + nodes { + type { + repr + } + sender { + address + } + json + } + } +} diff --git a/crates/sui-graphql-rpc/schema.graphql b/crates/sui-graphql-rpc/schema.graphql index defc55cc329d2..faca90cc4e354 100644 --- a/crates/sui-graphql-rpc/schema.graphql +++ b/crates/sui-graphql-rpc/schema.graphql @@ -1272,6 +1272,8 @@ input EventFilter { PTB and emits an event. Modules can be filtered by their package, or package::module. + We currently do not support filtering by emitting module and event type + at the same time so if both are provided in one filter, the query will error. """ emittingModule: String """ @@ -3312,7 +3314,9 @@ type Query { """ transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ - The events that exist in the network. + Query events that are emitted in the network. + We currently do not support filtering by emitting module and event type + at the same time so if both are provided in one filter, the query will error. """ events(first: Int, after: String, last: Int, before: String, filter: EventFilter): EventConnection! """ diff --git a/crates/sui-graphql-rpc/src/types/event/cursor.rs b/crates/sui-graphql-rpc/src/types/event/cursor.rs new file mode 100644 index 0000000000000..9a64399f1fab6 --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/event/cursor.rs @@ -0,0 +1,183 @@ +// 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, + BoolExpressionMethods, ExpressionMethods, QueryDsl, +}; +use serde::{Deserialize, Serialize}; +use sui_indexer::{models::events::StoredEvent, schema::events}; + +use super::Query; + +/// Contents of an Event's cursor. +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +pub(crate) struct EventKey { + /// Transaction Sequence Number + pub tx: u64, + + /// Event Sequence Number + pub e: u64, + + /// The checkpoint sequence number this was viewed at. + #[serde(rename = "c")] + pub checkpoint_viewed_at: u64, +} + +pub(crate) type Cursor = cursor::JsonCursor; + +/// Results from raw queries in Diesel can only be deserialized into structs that implement +/// `QueryableByName`. This struct is used to represent a row of `tx_sequence_number` and +/// `event_sequence_number` returned from subqueries against event lookup tables. +#[derive(Clone, Debug)] +pub struct EvLookup { + pub tx: i64, + pub ev: i64, +} + +impl Paginated for StoredEvent { + type Source = events::table; + + fn filter_ge(cursor: &Cursor, query: Query) -> Query { + use events::dsl::{event_sequence_number as event, tx_sequence_number as tx}; + query.filter( + tx.gt(cursor.tx as i64) + .or(tx.eq(cursor.tx as i64).and(event.ge(cursor.e as i64))), + ) + } + + fn filter_le(cursor: &Cursor, query: Query) -> Query { + use events::dsl::{event_sequence_number as event, tx_sequence_number as tx}; + query.filter( + tx.lt(cursor.tx as i64) + .or(tx.eq(cursor.tx as i64).and(event.le(cursor.e as i64))), + ) + } + + fn order(asc: bool, query: Query) -> Query { + use events::dsl; + if asc { + query + .order_by(dsl::tx_sequence_number.asc()) + .then_order_by(dsl::event_sequence_number.asc()) + } else { + query + .order_by(dsl::tx_sequence_number.desc()) + .then_order_by(dsl::event_sequence_number.desc()) + } + } +} + +impl RawPaginated for StoredEvent { + fn filter_ge(cursor: &Cursor, query: RawQuery) -> RawQuery { + filter!( + query, + format!( + "ROW(tx_sequence_number, event_sequence_number) >= ({}, {})", + cursor.tx, cursor.e + ) + ) + } + + fn filter_le(cursor: &Cursor, query: RawQuery) -> RawQuery { + filter!( + query, + format!( + "ROW(tx_sequence_number, event_sequence_number) <= ({}, {})", + cursor.tx, cursor.e + ) + ) + } + + fn order(asc: bool, query: RawQuery) -> RawQuery { + if asc { + query.order_by("tx_sequence_number ASC, event_sequence_number ASC") + } else { + query.order_by("tx_sequence_number DESC, event_sequence_number DESC") + } + } +} + +impl Target for StoredEvent { + fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor { + Cursor::new(EventKey { + tx: self.tx_sequence_number as u64, + e: self.event_sequence_number as u64, + checkpoint_viewed_at, + }) + } +} + +impl Checkpointed for Cursor { + fn checkpoint_viewed_at(&self) -> u64 { + self.checkpoint_viewed_at + } +} + +impl ScanLimited for Cursor {} + +impl Target for EvLookup { + fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor { + Cursor::new(EventKey { + tx: self.tx as u64, + e: self.ev as u64, + checkpoint_viewed_at, + }) + } +} + +impl RawPaginated for EvLookup { + fn filter_ge(cursor: &Cursor, query: RawQuery) -> RawQuery { + filter!( + query, + format!( + "ROW(tx_sequence_number, event_sequence_number) >= ({}, {})", + cursor.tx, cursor.e + ) + ) + } + + fn filter_le(cursor: &Cursor, query: RawQuery) -> RawQuery { + filter!( + query, + format!( + "ROW(tx_sequence_number, event_sequence_number) <= ({}, {})", + cursor.tx, cursor.e + ) + ) + } + + fn order(asc: bool, query: RawQuery) -> RawQuery { + if asc { + query.order_by("tx_sequence_number ASC, event_sequence_number ASC") + } else { + query.order_by("tx_sequence_number DESC, event_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 +/// `EvLookup`, its fields are derived from the common `tx_sequence_number` and +/// `event_sequence_number` columns for all events-related tables. +impl QueryableByName for EvLookup +where + DB: Backend, + i64: FromSql, +{ + fn build<'a>(row: &impl NamedRow<'a, DB>) -> deserialize::Result { + let tx = NamedRow::get::(row, "tx_sequence_number")?; + let ev = NamedRow::get::(row, "event_sequence_number")?; + + Ok(Self { tx, ev }) + } +} diff --git a/crates/sui-graphql-rpc/src/types/event/filter.rs b/crates/sui-graphql-rpc/src/types/event/filter.rs new file mode 100644 index 0000000000000..0a02a31be8969 --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/event/filter.rs @@ -0,0 +1,44 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::{ + digest::Digest, + sui_address::SuiAddress, + type_filter::{ModuleFilter, TypeFilter}, +}; +use async_graphql::*; + +#[derive(InputObject, Clone, Default)] +pub(crate) struct EventFilter { + pub sender: Option, + pub transaction_digest: Option, + // Enhancement (post-MVP) + // after_checkpoint + // before_checkpoint + /// Events emitted by a particular module. An event is emitted by a + /// particular module if some function in the module is called by a + /// PTB and emits an event. + /// + /// Modules can be filtered by their package, or package::module. + /// We currently do not support filtering by emitting module and event type + /// at the same time so if both are provided in one filter, the query will error. + pub emitting_module: Option, + + /// This field is used to specify the type of event emitted. + /// + /// Events can be filtered by their type's package, package::module, + /// or their fully qualified type name. + /// + /// Generic types can be queried by either the generic type name, e.g. + /// `0x2::coin::Coin`, or by the full type name, such as + /// `0x2::coin::Coin<0x2::sui::SUI>`. + pub event_type: Option, + // Enhancement (post-MVP) + // pub start_time + // pub end_time + + // Enhancement (post-MVP) + // pub any + // pub all + // pub not +} diff --git a/crates/sui-graphql-rpc/src/types/event/lookups.rs b/crates/sui-graphql-rpc/src/types/event/lookups.rs new file mode 100644 index 0000000000000..93a9a55bf6e6b --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/event/lookups.rs @@ -0,0 +1,158 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + data::pg::bytea_literal, + filter, query, + raw_query::RawQuery, + types::{ + cursor::Page, + digest::Digest, + sui_address::SuiAddress, + type_filter::{ModuleFilter, TypeFilter}, + }, +}; + +use std::fmt::Write; + +use super::Cursor; + +fn select_ev(sender: Option, from: &str) -> RawQuery { + let query = query!(format!( + "SELECT tx_sequence_number, event_sequence_number FROM {}", + from + )); + + if let Some(sender) = sender { + return query.filter(format!("sender = {}", bytea_literal(sender.as_slice()))); + } + + query +} + +pub(crate) fn select_sender(sender: SuiAddress) -> RawQuery { + select_ev(Some(sender), "event_senders") +} + +pub(crate) fn select_event_type(event_type: &TypeFilter, sender: Option) -> RawQuery { + match event_type { + TypeFilter::ByModule(ModuleFilter::ByPackage(p)) => { + filter!( + select_ev(sender, "event_struct_package"), + format!("package = {}", bytea_literal(p.as_slice())) + ) + } + TypeFilter::ByModule(ModuleFilter::ByModule(p, m)) => { + filter!( + select_ev(sender, "event_struct_module"), + format!( + "package = {} and module = {{}}", + bytea_literal(p.as_slice()) + ), + m + ) + } + TypeFilter::ByType(tag) => { + let package = tag.address; + let module = tag.module.to_string(); + let mut name = tag.name.as_str().to_owned(); + let (table, col_name) = if tag.type_params.is_empty() { + ("event_struct_name", "type_name") + } else { + let mut prefix = "<"; + for param in &tag.type_params { + name += prefix; + // SAFETY: write! to String always succeeds. + write!( + name, + "{}", + param.to_canonical_display(/* with_prefix */ true) + ) + .unwrap(); + prefix = ", "; + } + name += ">"; + ("event_struct_instantiation", "type_instantiation") + }; + + filter!( + select_ev(sender, table), + format!( + "package = {} and module = {{}} and {} = {{}}", + bytea_literal(package.as_slice()), + col_name + ), + module, + name + ) + } + } +} + +pub(crate) fn select_emit_module( + emit_module: &ModuleFilter, + sender: Option, +) -> RawQuery { + match emit_module { + ModuleFilter::ByPackage(p) => { + filter!( + select_ev(sender, "event_emit_package"), + format!("package = {}", bytea_literal(p.as_slice())) + ) + } + ModuleFilter::ByModule(p, m) => { + filter!( + select_ev(sender, "event_emit_module"), + format!( + "package = {} and module = {{}}", + bytea_literal(p.as_slice()) + ), + m + ) + } + } +} + +/// Adds filters to bound an events query from above and below based on cursors and filters. The +/// query will always at least be bounded by `tx_hi`, the current exclusive upperbound on +/// transaction sequence numbers, based on the consistency cursor. +pub(crate) fn add_bounds( + mut query: RawQuery, + tx_digest_filter: &Option, + page: &Page, + tx_hi: i64, +) -> RawQuery { + query = filter!(query, format!("tx_sequence_number < {}", tx_hi)); + + if let Some(after) = page.after() { + query = filter!( + query, + format!( + "ROW(tx_sequence_number, event_sequence_number) >= ({}, {})", + after.tx, after.e + ) + ); + } + + if let Some(before) = page.before() { + query = filter!( + query, + format!( + "ROW(tx_sequence_number, event_sequence_number) <= ({}, {})", + before.tx, before.e + ) + ); + } + + if let Some(digest) = tx_digest_filter { + query = filter!( + query, + format!( + "tx_sequence_number = (SELECT tx_sequence_number FROM tx_digests WHERE tx_digest = {})", + bytea_literal(digest.as_slice()), + ) + ); + } + + query +} diff --git a/crates/sui-graphql-rpc/src/types/event.rs b/crates/sui-graphql-rpc/src/types/event/mod.rs similarity index 50% rename from crates/sui-graphql-rpc/src/types/event.rs rename to crates/sui-graphql-rpc/src/types/event/mod.rs index cb558c2fba6c3..10c130c7f43c1 100644 --- a/crates/sui-graphql-rpc/src/types/event.rs +++ b/crates/sui-graphql-rpc/src/types/event/mod.rs @@ -3,28 +3,33 @@ use std::str::FromStr; -use super::cursor::{self, Page, Paginated, ScanLimited, Target}; -use super::digest::Digest; -use super::type_filter::{ModuleFilter, TypeFilter}; +use super::cursor::{Page, Target}; use super::{ address::Address, base64::Base64, date_time::DateTime, move_module::MoveModule, - move_value::MoveValue, sui_address::SuiAddress, + move_value::MoveValue, }; -use crate::consistency::Checkpointed; -use crate::data::{self, QueryExecutor}; +use crate::data::{self, DbConnection, QueryExecutor}; +use crate::query; use crate::{data::Db, error::Error}; use async_graphql::connection::{Connection, CursorType, Edge}; use async_graphql::*; -use diesel::{BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, QueryDsl}; -use serde::{Deserialize, Serialize}; +use cursor::EvLookup; +use diesel::{ExpressionMethods, QueryDsl}; +use lookups::{add_bounds, select_emit_module, select_event_type, select_sender}; use sui_indexer::models::{events::StoredEvent, transactions::StoredTransaction}; -use sui_indexer::schema::{events, transactions, tx_senders}; +use sui_indexer::schema::{checkpoints, events}; use sui_types::base_types::ObjectID; use sui_types::Identifier; use sui_types::{ base_types::SuiAddress as NativeSuiAddress, event::Event as NativeEvent, parse_sui_struct_tag, }; +mod cursor; +mod filter; +mod lookups; +pub(crate) use cursor::Cursor; +pub(crate) use filter::EventFilter; + /// A Sui node emits one of the following events: /// Move event /// Publish event @@ -40,56 +45,8 @@ pub(crate) struct Event { pub checkpoint_viewed_at: u64, } -/// Contents of an Event's cursor. -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] -pub(crate) struct EventKey { - /// Transaction Sequence Number - tx: u64, - - /// Event Sequence Number - e: u64, - - /// The checkpoint sequence number this was viewed at. - #[serde(rename = "c")] - checkpoint_viewed_at: u64, -} - -pub(crate) type Cursor = cursor::JsonCursor; type Query = data::Query; -#[derive(InputObject, Clone, Default)] -pub(crate) struct EventFilter { - pub sender: Option, - pub transaction_digest: Option, - // Enhancement (post-MVP) - // after_checkpoint - // before_checkpoint - /// Events emitted by a particular module. An event is emitted by a - /// particular module if some function in the module is called by a - /// PTB and emits an event. - /// - /// Modules can be filtered by their package, or package::module. - pub emitting_module: Option, - - /// This field is used to specify the type of event emitted. - /// - /// Events can be filtered by their type's package, package::module, - /// or their fully qualified type name. - /// - /// Generic types can be queried by either the generic type name, e.g. - /// `0x2::coin::Coin`, or by the full type name, such as - /// `0x2::coin::Coin<0x2::sui::SUI>`. - pub event_type: Option, - // Enhancement (post-MVP) - // pub start_time - // pub end_time - - // Enhancement (post-MVP) - // pub any - // pub all - // pub not -} - #[Object] impl Event { /// The Move module containing some function that when called by @@ -158,64 +115,82 @@ impl Event { let cursor_viewed_at = page.validate_cursor_consistency()?; let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at); + // Construct tx and ev sequence number query with table-relevant filters, if they exist. The + // resulting query will look something like `SELECT tx_sequence_number, + // event_sequence_number FROM lookup_table WHERE ...`. If no filter is provided we don't + // need to use any lookup tables and can just query `events` table, as can be seen in the + // code below. + let query_constraint = match (filter.sender, &filter.emitting_module, &filter.event_type) { + (None, None, None) => None, + (Some(sender), None, None) => Some(select_sender(sender)), + (sender, None, Some(event_type)) => Some(select_event_type(event_type, sender)), + (sender, Some(module), None) => Some(select_emit_module(module, sender)), + (_, Some(_), Some(_)) => { + return Err(Error::Client( + "Filtering by both emitting module and event type is not supported".to_string(), + )) + } + }; + + use checkpoints::dsl; let (prev, next, results) = db .execute(move |conn| { - page.paginate_query::(conn, checkpoint_viewed_at, move || { - let mut query = events::dsl::events.into_boxed(); - - // Bound events by the provided `checkpoint_viewed_at`. From EXPLAIN - // ANALYZE, using the checkpoint sequence number directly instead of - // translating into a transaction sequence number bound is more efficient. - query = query.filter( - events::dsl::checkpoint_sequence_number.le(checkpoint_viewed_at as i64), - ); - - // The transactions table doesn't have an index on the senders column, so use - // `tx_senders`. - if let Some(sender) = &filter.sender { - query = query.filter( - events::dsl::tx_sequence_number.eq_any( - tx_senders::dsl::tx_senders - .select(tx_senders::dsl::tx_sequence_number) - .filter(tx_senders::dsl::sender.eq(sender.into_vec())), - ), - ) - } - - if let Some(digest) = &filter.transaction_digest { - // Since the event filter takes in a single tx_digest, we know that - // there will only be one corresponding transaction. We can use - // single_value() to tell the query planner that we expect only one - // instead of a range of values, which will subsequently speed up query - // execution time. - query = query.filter( - events::dsl::tx_sequence_number.nullable().eq( - transactions::dsl::transactions - .select(transactions::dsl::tx_sequence_number) - .filter( - transactions::dsl::transaction_digest.eq(digest.to_vec()), - ) - .single_value(), - ), - ) - } - - if let Some(module) = &filter.emitting_module { - query = module.apply(query, events::dsl::package, events::dsl::module); - } - - if let Some(type_) = &filter.event_type { - query = type_.apply( - query, - events::dsl::event_type, - events::dsl::event_type_package, - events::dsl::event_type_module, - events::dsl::event_type_name, - ); - } - - query - }) + let tx_hi: i64 = conn.first(move || { + dsl::checkpoints.select(dsl::network_total_transactions) + .filter(dsl::sequence_number.eq(checkpoint_viewed_at as i64)) + })?; + + let (prev, next, mut events): (bool, bool, Vec) = + if let Some(filter_query) = query_constraint { + let query = add_bounds(filter_query, &filter.transaction_digest, &page, tx_hi); + + let (prev, next, results) = + page.paginate_raw_query::(conn, checkpoint_viewed_at, query)?; + + let ev_lookups = results + .into_iter() + .map(|x| (x.tx, x.ev)) + .collect::>(); + + if ev_lookups.is_empty() { + return Ok::<_, diesel::result::Error>((prev, next, vec![])); + } + + // Unlike a multi-get on a single column which can be serviced by a query `IN + // (...)`, because events have a composite primary key, the query planner tends + // to perform a sequential scan when given a list of tuples to lookup. A query + // using `UNION ALL` allows us to leverage the index on the composite key. + let events = conn.results(move || { + // Diesel's DSL does not current support chained `UNION ALL`, so we have to turn + // to `RawQuery` here. + let query_string = ev_lookups.iter() + .map(|&(tx, ev)| { + format!("SELECT * FROM events WHERE tx_sequence_number = {} AND event_sequence_number = {}", tx, ev) + }) + .collect::>() + .join(" UNION ALL "); + + query!(query_string).into_boxed() + })?; + (prev, next, events) + } else { + // No filter is provided so we add bounds to the basic `SELECT * FROM + // events` query and call it a day. + let query = add_bounds(query!("SELECT * FROM events"), &filter.transaction_digest, &page, tx_hi); + let (prev, next, events_iter) = page.paginate_raw_query::(conn, checkpoint_viewed_at, query)?; + let events = events_iter.collect::>(); + (prev, next, events) + }; + + // UNION ALL does not guarantee order, so we need to sort the results. Whether + // `first` or `last, the result set is always sorted in ascending order. + events.sort_by(|a, b| { + a.tx_sequence_number.cmp(&b.tx_sequence_number) + .then_with(|| a.event_sequence_number.cmp(&b.event_sequence_number)) + }); + + + Ok::<_, diesel::result::Error>((prev, next, events)) }) .await?; @@ -256,7 +231,6 @@ impl Event { tx_sequence_number: stored_tx.tx_sequence_number, event_sequence_number: idx as i64, transaction_digest: stored_tx.transaction_digest.clone(), - checkpoint_sequence_number: stored_tx.checkpoint_sequence_number, #[cfg(feature = "postgres-feature")] senders: vec![Some(native_event.sender.to_vec())], package: native_event.package_id.to_vec(), @@ -264,9 +238,6 @@ impl Event { event_type: native_event .type_ .to_canonical_string(/* with_prefix */ true), - event_type_package: native_event.type_.address.to_vec(), - event_type_module: native_event.type_.module.to_string(), - event_type_name: native_event.type_.name.to_string(), bcs: native_event.contents.clone(), timestamp_ms: stored_tx.timestamp_ms, }; @@ -312,54 +283,3 @@ impl Event { }) } } - -impl Paginated for StoredEvent { - type Source = events::table; - - fn filter_ge(cursor: &Cursor, query: Query) -> Query { - use events::dsl::{event_sequence_number as event, tx_sequence_number as tx}; - query.filter( - tx.gt(cursor.tx as i64) - .or(tx.eq(cursor.tx as i64).and(event.ge(cursor.e as i64))), - ) - } - - fn filter_le(cursor: &Cursor, query: Query) -> Query { - use events::dsl::{event_sequence_number as event, tx_sequence_number as tx}; - query.filter( - tx.lt(cursor.tx as i64) - .or(tx.eq(cursor.tx as i64).and(event.le(cursor.e as i64))), - ) - } - - fn order(asc: bool, query: Query) -> Query { - use events::dsl; - if asc { - query - .order_by(dsl::tx_sequence_number.asc()) - .then_order_by(dsl::event_sequence_number.asc()) - } else { - query - .order_by(dsl::tx_sequence_number.desc()) - .then_order_by(dsl::event_sequence_number.desc()) - } - } -} - -impl Target for StoredEvent { - fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor { - Cursor::new(EventKey { - tx: self.tx_sequence_number as u64, - e: self.event_sequence_number as u64, - checkpoint_viewed_at, - }) - } -} - -impl Checkpointed for Cursor { - fn checkpoint_viewed_at(&self) -> u64 { - self.checkpoint_viewed_at - } -} - -impl ScanLimited for Cursor {} diff --git a/crates/sui-graphql-rpc/src/types/query.rs b/crates/sui-graphql-rpc/src/types/query.rs index f403fbf8657b5..f6f4704b3009f 100644 --- a/crates/sui-graphql-rpc/src/types/query.rs +++ b/crates/sui-graphql-rpc/src/types/query.rs @@ -414,7 +414,9 @@ impl Query { .extend() } - /// The events that exist in the network. + /// Query events that are emitted in the network. + /// We currently do not support filtering by emitting module and event type + /// at the same time so if both are provided in one filter, the query will error. async fn events( &self, ctx: &Context<'_>, diff --git a/crates/sui-graphql-rpc/src/types/type_filter.rs b/crates/sui-graphql-rpc/src/types/type_filter.rs index f2028483989ff..16f9dd03181f1 100644 --- a/crates/sui-graphql-rpc/src/types/type_filter.rs +++ b/crates/sui-graphql-rpc/src/types/type_filter.rs @@ -2,18 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use super::{string_input::impl_string_input, sui_address::SuiAddress}; +use crate::filter; use crate::raw_query::RawQuery; -use crate::{ - data::{DieselBackend, Query}, - filter, -}; use async_graphql::*; -use diesel::{ - expression::{is_aggregate::No, ValidGrouping}, - query_builder::QueryFragment, - sql_types::{Binary, Text}, - AppearsOnTable, Expression, ExpressionMethods, QueryDsl, QuerySource, -}; use move_core_types::language_storage::StructTag; use std::{fmt, result::Result, str::FromStr}; use sui_types::{ @@ -67,89 +58,7 @@ pub(crate) enum Error { InvalidFormat(&'static str), } -/// Trait for a field that can be used in a query. -pub(crate) trait Field: - ExpressionMethods - + Expression - + QueryFragment - + AppearsOnTable - + ValidGrouping<(), IsAggregate = No> - + Send - + 'static -{ -} - -impl Field for T where - T: ExpressionMethods - + Expression - + QueryFragment - + AppearsOnTable - + ValidGrouping<(), IsAggregate = No> - + Send - + 'static -{ -} - impl TypeFilter { - /// Modify `query` to apply this filter to `type_field`, `package_field`, `module_field` - /// and `name_field`, where `type_field` stores the full type tag while the rest - /// store the package, module and name of the type tag respectively. The new query - /// after applying the filter is returned. - pub(crate) fn apply( - &self, - query: Query, - // Field storing the full type tag, including type parameters. - type_field: T, - package_field: P, - module_field: M, - // Name field only includes the name of the struct, like `Coin`, not including type parameters. - name_field: N, - ) -> Query - where - Query: QueryDsl, - T: Field, - P: Field, - M: Field, - N: Field, - QS: QuerySource, - { - match self { - TypeFilter::ByModule(ModuleFilter::ByPackage(p)) => { - query.filter(package_field.eq(p.into_vec())) - } - - TypeFilter::ByModule(ModuleFilter::ByModule(p, m)) => query - .filter(package_field.eq(p.into_vec())) - .filter(module_field.eq(m.clone())), - - // A type filter without type parameters is interpreted as either an exact match, or a - // match for all generic instantiations of the type so we check against only package, module - // and name fields. - TypeFilter::ByType(tag) if tag.type_params.is_empty() => { - let p = tag.address.to_vec(); - let m = tag.module.to_string(); - let n = tag.name.to_string(); - query - .filter(package_field.eq(p)) - .filter(module_field.eq(m)) - .filter(name_field.eq(n)) - } - - TypeFilter::ByType(tag) => { - let p = tag.address.to_vec(); - let m = tag.module.to_string(); - let n = tag.name.to_string(); - let exact = tag.to_canonical_string(/* with_prefix */ true); - // We check against the full type field for an exact match, including type parameters. - query - .filter(package_field.eq(p)) - .filter(module_field.eq(m)) - .filter(name_field.eq(n)) - .filter(type_field.eq(exact)) - } - } - } - /// Modify `query` to apply this filter to `field`, returning the new query. pub(crate) fn apply_raw( &self, @@ -295,28 +204,6 @@ impl FqNameFilter { } impl ModuleFilter { - /// Modify `query` to apply this filter, treating `package` as the column containing the package - /// address and `module` as the module containing the module name. - pub(crate) fn apply( - &self, - query: Query, - package: P, - module: M, - ) -> Query - where - Query: QueryDsl, - P: Field, - M: Field, - QS: QuerySource, - { - match self { - ModuleFilter::ByPackage(p) => query.filter(package.eq(p.into_vec())), - ModuleFilter::ByModule(p, m) => query - .filter(package.eq(p.into_vec())) - .filter(module.eq(m.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'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 fd04f186f34b6..ed3ec693362f4 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 @@ -1276,6 +1276,8 @@ input EventFilter { PTB and emits an event. Modules can be filtered by their package, or package::module. + We currently do not support filtering by emitting module and event type + at the same time so if both are provided in one filter, the query will error. """ emittingModule: String """ @@ -3316,7 +3318,9 @@ type Query { """ transactionBlocks(first: Int, after: String, last: Int, before: String, filter: TransactionBlockFilter, scanLimit: Int): TransactionBlockConnection! """ - The events that exist in the network. + Query events that are emitted in the network. + We currently do not support filtering by emitting module and event type + at the same time so if both are provided in one filter, the query will error. """ events(first: Int, after: String, last: Int, before: String, filter: EventFilter): EventConnection! """ diff --git a/crates/sui-indexer/migrations/pg/2023-08-19-044020_events/up.sql b/crates/sui-indexer/migrations/pg/2023-08-19-044020_events/up.sql index dfbfa3ea14495..14aa6a098161f 100644 --- a/crates/sui-indexer/migrations/pg/2023-08-19-044020_events/up.sql +++ b/crates/sui-indexer/migrations/pg/2023-08-19-044020_events/up.sql @@ -4,7 +4,6 @@ CREATE TABLE events tx_sequence_number BIGINT NOT NULL, event_sequence_number BIGINT NOT NULL, transaction_digest bytea NOT NULL, - checkpoint_sequence_number bigint NOT NULL, -- array of SuiAddress in bytes. All signers of the transaction. senders bytea[] NOT NULL, -- bytes of the entry package ID. Notice that the package and module here @@ -15,11 +14,6 @@ CREATE TABLE events module text NOT NULL, -- StructTag in Display format, fully qualified including type parameters event_type text NOT NULL, - -- Components of the StructTag of the event type: package, module, - -- name (name of the struct, without type parameters) - event_type_package bytea NOT NULL, - event_type_module text NOT NULL, - event_type_name text NOT NULL, -- timestamp of the checkpoint when the event was emitted timestamp_ms BIGINT NOT NULL, -- bcs of the Event contents (Event.contents) @@ -30,5 +24,3 @@ CREATE TABLE events_partition_0 PARTITION OF events FOR VALUES FROM (0) TO (MAXV CREATE INDEX events_package ON events (package, tx_sequence_number, event_sequence_number); CREATE INDEX events_package_module ON events (package, module, tx_sequence_number, event_sequence_number); CREATE INDEX events_event_type ON events (event_type text_pattern_ops, tx_sequence_number, event_sequence_number); -CREATE INDEX events_type_package_module_name ON events (event_type_package, event_type_module, event_type_name, tx_sequence_number, event_sequence_number); -CREATE INDEX events_checkpoint_sequence_number ON events (checkpoint_sequence_number); diff --git a/crates/sui-indexer/src/models/events.rs b/crates/sui-indexer/src/models/events.rs index 455b22e1741fe..01f79c41d6ab2 100644 --- a/crates/sui-indexer/src/models/events.rs +++ b/crates/sui-indexer/src/models/events.rs @@ -31,9 +31,6 @@ pub struct StoredEvent { #[diesel(sql_type = diesel::sql_types::Binary)] pub transaction_digest: Vec, - #[diesel(sql_type = diesel::sql_types::BigInt)] - pub checkpoint_sequence_number: i64, - #[cfg(feature = "postgres-feature")] #[diesel(sql_type = diesel::sql_types::Array>)] pub senders: Vec>>, @@ -52,15 +49,6 @@ pub struct StoredEvent { #[diesel(sql_type = diesel::sql_types::Text)] pub event_type: String, - #[diesel(sql_type = diesel::sql_types::Binary)] - pub event_type_package: Vec, - - #[diesel(sql_type = diesel::sql_types::Text)] - pub event_type_module: String, - - #[diesel(sql_type = diesel::sql_types::Text)] - pub event_type_name: String, - #[diesel(sql_type = diesel::sql_types::BigInt)] pub timestamp_ms: i64, @@ -81,7 +69,6 @@ impl From for StoredEvent { tx_sequence_number: event.tx_sequence_number as i64, event_sequence_number: event.event_sequence_number as i64, transaction_digest: event.transaction_digest.into_inner().to_vec(), - checkpoint_sequence_number: event.checkpoint_sequence_number as i64, #[cfg(feature = "postgres-feature")] senders: event .senders @@ -94,9 +81,6 @@ impl From for StoredEvent { package: event.package.to_vec(), module: event.module.clone(), event_type: event.event_type.clone(), - event_type_package: event.event_type_package.to_vec(), - event_type_module: event.event_type_module.clone(), - event_type_name: event.event_type_name.clone(), bcs: event.bcs.clone(), timestamp_ms: event.timestamp_ms as i64, } diff --git a/crates/sui-indexer/src/schema/pg.rs b/crates/sui-indexer/src/schema/pg.rs index 2515c98d34c06..c1720c94b37a1 100644 --- a/crates/sui-indexer/src/schema/pg.rs +++ b/crates/sui-indexer/src/schema/pg.rs @@ -137,14 +137,10 @@ diesel::table! { tx_sequence_number -> Int8, event_sequence_number -> Int8, transaction_digest -> Bytea, - checkpoint_sequence_number -> Int8, senders -> Array>, package -> Bytea, module -> Text, event_type -> Text, - event_type_package -> Bytea, - event_type_module -> Text, - event_type_name -> Text, timestamp_ms -> Int8, bcs -> Bytea, } @@ -155,14 +151,10 @@ diesel::table! { tx_sequence_number -> Int8, event_sequence_number -> Int8, transaction_digest -> Bytea, - checkpoint_sequence_number -> Int8, senders -> Array>, package -> Bytea, module -> Text, event_type -> Text, - event_type_package -> Bytea, - event_type_module -> Text, - event_type_name -> Text, timestamp_ms -> Int8, bcs -> Bytea, } diff --git a/crates/sui-indexer/tests/ingestion_tests.rs b/crates/sui-indexer/tests/ingestion_tests.rs index af67a061bbde3..8eee88f8bd448 100644 --- a/crates/sui-indexer/tests/ingestion_tests.rs +++ b/crates/sui-indexer/tests/ingestion_tests.rs @@ -5,7 +5,6 @@ mod ingestion_tests { use diesel::ExpressionMethods; use diesel::{QueryDsl, RunQueryDsl}; - use move_core_types::language_storage::StructTag; use simulacrum::Simulacrum; use std::net::SocketAddr; use std::path::PathBuf; @@ -14,18 +13,14 @@ mod ingestion_tests { use sui_indexer::db::get_pool_connection; use sui_indexer::errors::Context; use sui_indexer::errors::IndexerError; - use sui_indexer::models::{ - events::StoredEvent, objects::StoredObject, transactions::StoredTransaction, - }; - use sui_indexer::schema::{events, objects, transactions}; + use sui_indexer::models::{objects::StoredObject, transactions::StoredTransaction}; + use sui_indexer::schema::{objects, transactions}; use sui_indexer::store::{indexer_store::IndexerStore, PgIndexerStore}; use sui_indexer::test_utils::{start_test_indexer, ReaderWriterConfig}; use sui_types::base_types::SuiAddress; use sui_types::effects::TransactionEffectsAPI; use sui_types::gas_coin::GasCoin; - use sui_types::{ - Identifier, SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_ADDRESS, SUI_SYSTEM_PACKAGE_ID, - }; + use sui_types::SUI_FRAMEWORK_PACKAGE_ID; use tempfile::tempdir; use tokio::task::JoinHandle; @@ -93,24 +88,6 @@ mod ingestion_tests { Ok(()) } - /// Wait for the indexer to catch up to the given epoch id. - async fn wait_for_epoch( - pg_store: &PgIndexerStore, - epoch: u64, - ) -> Result<(), IndexerError> { - tokio::time::timeout(Duration::from_secs(10), async { - while { - let cp_opt = pg_store.get_latest_epoch_id().unwrap(); - cp_opt.is_none() || (cp_opt.unwrap() < epoch) - } { - tokio::time::sleep(Duration::from_secs(1)).await; - } - }) - .await - .expect("Timeout waiting for indexer to catchup to epoch"); - Ok(()) - } - #[tokio::test] pub async fn test_transaction_table() -> Result<(), IndexerError> { let mut sim = Simulacrum::new(); @@ -156,46 +133,6 @@ mod ingestion_tests { Ok(()) } - #[tokio::test] - pub async fn test_event_type() -> Result<(), IndexerError> { - let mut sim = Simulacrum::new(); - let data_ingestion_path = tempdir().unwrap().into_path(); - sim.set_data_ingestion_path(data_ingestion_path.clone()); - - // Advance the epoch to generate some events. - sim.advance_epoch(false); - - let (_, pg_store, _) = set_up(Arc::new(sim), data_ingestion_path).await; - - // Wait for the epoch to change so we can get some events. - wait_for_epoch(&pg_store, 1).await?; - - // Read the event from the database directly. - let db_event: StoredEvent = read_only_blocking!(&pg_store.blocking_cp(), |conn| { - events::table - .filter(events::event_type_name.eq("SystemEpochInfoEvent")) - .first::(conn) - }) - .context("Failed reading SystemEpochInfoEvent from PostgresDB")?; - - let event_type_tag = StructTag { - address: SUI_SYSTEM_ADDRESS, - module: Identifier::new("sui_system_state_inner").unwrap(), - name: Identifier::new("SystemEpochInfoEvent").unwrap(), - type_params: vec![], - }; - - // Check that the different components of the event type were stored correctly. - assert_eq!( - db_event.event_type, - event_type_tag.to_canonical_string(true) - ); - assert_eq!(db_event.event_type_package, SUI_SYSTEM_PACKAGE_ID.to_vec()); - assert_eq!(db_event.event_type_module, "sui_system_state_inner"); - assert_eq!(db_event.event_type_name, "SystemEpochInfoEvent"); - Ok(()) - } - #[tokio::test] pub async fn test_object_type() -> Result<(), IndexerError> { let mut sim = Simulacrum::new();