From b7ae655c20f2f6c3a4fcad60eead8254be42d322 Mon Sep 17 00:00:00 2001 From: prathamesh0 <42446521+prathamesh0@users.noreply.github.com> Date: Tue, 16 Nov 2021 14:47:03 +0530 Subject: [PATCH] Add eden-watcher generated using codegen (#47) * Add a watcher for EdenNetwork contract * Add events from MerkleDistributor contract to eden-watcher * Add ERC721 Transfer and Approval events to eden-watcher schema * Add artifacts for DistributorGovernance and MerkleDistributor contracts --- packages/eden-watcher/.eslintignore | 2 + packages/eden-watcher/.eslintrc.json | 27 + packages/eden-watcher/README.md | 191 +++ packages/eden-watcher/environments/local.toml | 42 + packages/eden-watcher/package.json | 69 + .../src/artifacts/DistributorGovernance.json | 723 ++++++++ .../src/artifacts/EdenNetwork.json | 942 +++++++++++ .../src/artifacts/MerkleDistributor.json | 1482 +++++++++++++++++ packages/eden-watcher/src/cli/checkpoint.ts | 69 + packages/eden-watcher/src/cli/export-state.ts | 119 ++ packages/eden-watcher/src/cli/import-state.ts | 118 ++ packages/eden-watcher/src/cli/inspect-cid.ts | 68 + .../src/cli/reset-cmds/job-queue.ts | 22 + .../eden-watcher/src/cli/reset-cmds/state.ts | 91 + packages/eden-watcher/src/cli/reset.ts | 24 + .../eden-watcher/src/cli/watch-contract.ts | 79 + packages/eden-watcher/src/client.ts | 55 + packages/eden-watcher/src/database.ts | 313 ++++ packages/eden-watcher/src/entity/Account.ts | 32 + packages/eden-watcher/src/entity/Block.ts | 63 + .../eden-watcher/src/entity/BlockProgress.ts | 45 + packages/eden-watcher/src/entity/Claim.ts | 34 + packages/eden-watcher/src/entity/Contract.ts | 24 + .../eden-watcher/src/entity/Distribution.ts | 34 + .../eden-watcher/src/entity/Distributor.ts | 21 + packages/eden-watcher/src/entity/Epoch.ts | 44 + packages/eden-watcher/src/entity/Event.ts | 38 + .../eden-watcher/src/entity/HookStatus.ts | 14 + packages/eden-watcher/src/entity/IPLDBlock.ts | 30 + packages/eden-watcher/src/entity/Network.ts | 41 + packages/eden-watcher/src/entity/Producer.ts | 33 + .../eden-watcher/src/entity/ProducerEpoch.ts | 34 + .../entity/ProducerRewardCollectorChange.ts | 23 + .../eden-watcher/src/entity/ProducerSet.ts | 21 + .../src/entity/ProducerSetChange.ts | 28 + .../eden-watcher/src/entity/RewardSchedule.ts | 31 + .../src/entity/RewardScheduleEntry.ts | 27 + packages/eden-watcher/src/entity/Slash.ts | 28 + packages/eden-watcher/src/entity/Slot.ts | 43 + packages/eden-watcher/src/entity/SlotClaim.ts | 40 + packages/eden-watcher/src/entity/Staker.ts | 24 + .../eden-watcher/src/entity/SyncStatus.ts | 30 + packages/eden-watcher/src/events.ts | 180 ++ packages/eden-watcher/src/fill.ts | 82 + packages/eden-watcher/src/gql/index.ts | 3 + .../eden-watcher/src/gql/mutations/index.ts | 4 + .../src/gql/mutations/watchContract.gql | 3 + .../eden-watcher/src/gql/queries/account.gql | 29 + .../eden-watcher/src/gql/queries/claim.gql | 36 + .../src/gql/queries/distribution.gql | 22 + .../src/gql/queries/distributor.gql | 15 + .../eden-watcher/src/gql/queries/epoch.gql | 39 + .../eden-watcher/src/gql/queries/events.gql | 75 + .../src/gql/queries/eventsInRange.gql | 75 + .../eden-watcher/src/gql/queries/getState.gql | 15 + .../src/gql/queries/getStateByCID.gql | 15 + .../eden-watcher/src/gql/queries/index.ts | 24 + .../eden-watcher/src/gql/queries/network.gql | 89 + .../eden-watcher/src/gql/queries/producer.gql | 10 + .../src/gql/queries/producerEpoch.gql | 46 + .../queries/producerRewardCollectorChange.gql | 8 + .../src/gql/queries/producerSet.gql | 13 + .../src/gql/queries/producerSetChange.gql | 8 + .../src/gql/queries/rewardSchedule.gql | 104 ++ .../src/gql/queries/rewardScheduleEntry.gql | 8 + .../eden-watcher/src/gql/queries/slash.gql | 34 + .../eden-watcher/src/gql/queries/slot.gql | 31 + .../src/gql/queries/slotClaim.gql | 40 + .../eden-watcher/src/gql/queries/staker.gql | 7 + .../src/gql/subscriptions/index.ts | 4 + .../src/gql/subscriptions/onEvent.gql | 75 + packages/eden-watcher/src/hooks.ts | 38 + packages/eden-watcher/src/indexer.ts | 1031 ++++++++++++ packages/eden-watcher/src/ipfs.ts | 17 + packages/eden-watcher/src/job-runner.ts | 163 ++ packages/eden-watcher/src/resolvers.ts | 210 +++ packages/eden-watcher/src/schema.gql | 391 +++++ packages/eden-watcher/src/server.ts | 100 ++ packages/eden-watcher/tsconfig.json | 74 + 79 files changed, 8336 insertions(+) create mode 100644 packages/eden-watcher/.eslintignore create mode 100644 packages/eden-watcher/.eslintrc.json create mode 100644 packages/eden-watcher/README.md create mode 100644 packages/eden-watcher/environments/local.toml create mode 100644 packages/eden-watcher/package.json create mode 100644 packages/eden-watcher/src/artifacts/DistributorGovernance.json create mode 100644 packages/eden-watcher/src/artifacts/EdenNetwork.json create mode 100644 packages/eden-watcher/src/artifacts/MerkleDistributor.json create mode 100644 packages/eden-watcher/src/cli/checkpoint.ts create mode 100644 packages/eden-watcher/src/cli/export-state.ts create mode 100644 packages/eden-watcher/src/cli/import-state.ts create mode 100644 packages/eden-watcher/src/cli/inspect-cid.ts create mode 100644 packages/eden-watcher/src/cli/reset-cmds/job-queue.ts create mode 100644 packages/eden-watcher/src/cli/reset-cmds/state.ts create mode 100644 packages/eden-watcher/src/cli/reset.ts create mode 100644 packages/eden-watcher/src/cli/watch-contract.ts create mode 100644 packages/eden-watcher/src/client.ts create mode 100644 packages/eden-watcher/src/database.ts create mode 100644 packages/eden-watcher/src/entity/Account.ts create mode 100644 packages/eden-watcher/src/entity/Block.ts create mode 100644 packages/eden-watcher/src/entity/BlockProgress.ts create mode 100644 packages/eden-watcher/src/entity/Claim.ts create mode 100644 packages/eden-watcher/src/entity/Contract.ts create mode 100644 packages/eden-watcher/src/entity/Distribution.ts create mode 100644 packages/eden-watcher/src/entity/Distributor.ts create mode 100644 packages/eden-watcher/src/entity/Epoch.ts create mode 100644 packages/eden-watcher/src/entity/Event.ts create mode 100644 packages/eden-watcher/src/entity/HookStatus.ts create mode 100644 packages/eden-watcher/src/entity/IPLDBlock.ts create mode 100644 packages/eden-watcher/src/entity/Network.ts create mode 100644 packages/eden-watcher/src/entity/Producer.ts create mode 100644 packages/eden-watcher/src/entity/ProducerEpoch.ts create mode 100644 packages/eden-watcher/src/entity/ProducerRewardCollectorChange.ts create mode 100644 packages/eden-watcher/src/entity/ProducerSet.ts create mode 100644 packages/eden-watcher/src/entity/ProducerSetChange.ts create mode 100644 packages/eden-watcher/src/entity/RewardSchedule.ts create mode 100644 packages/eden-watcher/src/entity/RewardScheduleEntry.ts create mode 100644 packages/eden-watcher/src/entity/Slash.ts create mode 100644 packages/eden-watcher/src/entity/Slot.ts create mode 100644 packages/eden-watcher/src/entity/SlotClaim.ts create mode 100644 packages/eden-watcher/src/entity/Staker.ts create mode 100644 packages/eden-watcher/src/entity/SyncStatus.ts create mode 100644 packages/eden-watcher/src/events.ts create mode 100644 packages/eden-watcher/src/fill.ts create mode 100644 packages/eden-watcher/src/gql/index.ts create mode 100644 packages/eden-watcher/src/gql/mutations/index.ts create mode 100644 packages/eden-watcher/src/gql/mutations/watchContract.gql create mode 100644 packages/eden-watcher/src/gql/queries/account.gql create mode 100644 packages/eden-watcher/src/gql/queries/claim.gql create mode 100644 packages/eden-watcher/src/gql/queries/distribution.gql create mode 100644 packages/eden-watcher/src/gql/queries/distributor.gql create mode 100644 packages/eden-watcher/src/gql/queries/epoch.gql create mode 100644 packages/eden-watcher/src/gql/queries/events.gql create mode 100644 packages/eden-watcher/src/gql/queries/eventsInRange.gql create mode 100644 packages/eden-watcher/src/gql/queries/getState.gql create mode 100644 packages/eden-watcher/src/gql/queries/getStateByCID.gql create mode 100644 packages/eden-watcher/src/gql/queries/index.ts create mode 100644 packages/eden-watcher/src/gql/queries/network.gql create mode 100644 packages/eden-watcher/src/gql/queries/producer.gql create mode 100644 packages/eden-watcher/src/gql/queries/producerEpoch.gql create mode 100644 packages/eden-watcher/src/gql/queries/producerRewardCollectorChange.gql create mode 100644 packages/eden-watcher/src/gql/queries/producerSet.gql create mode 100644 packages/eden-watcher/src/gql/queries/producerSetChange.gql create mode 100644 packages/eden-watcher/src/gql/queries/rewardSchedule.gql create mode 100644 packages/eden-watcher/src/gql/queries/rewardScheduleEntry.gql create mode 100644 packages/eden-watcher/src/gql/queries/slash.gql create mode 100644 packages/eden-watcher/src/gql/queries/slot.gql create mode 100644 packages/eden-watcher/src/gql/queries/slotClaim.gql create mode 100644 packages/eden-watcher/src/gql/queries/staker.gql create mode 100644 packages/eden-watcher/src/gql/subscriptions/index.ts create mode 100644 packages/eden-watcher/src/gql/subscriptions/onEvent.gql create mode 100644 packages/eden-watcher/src/hooks.ts create mode 100644 packages/eden-watcher/src/indexer.ts create mode 100644 packages/eden-watcher/src/ipfs.ts create mode 100644 packages/eden-watcher/src/job-runner.ts create mode 100644 packages/eden-watcher/src/resolvers.ts create mode 100644 packages/eden-watcher/src/schema.gql create mode 100644 packages/eden-watcher/src/server.ts create mode 100644 packages/eden-watcher/tsconfig.json diff --git a/packages/eden-watcher/.eslintignore b/packages/eden-watcher/.eslintignore new file mode 100644 index 00000000..55cb5225 --- /dev/null +++ b/packages/eden-watcher/.eslintignore @@ -0,0 +1,2 @@ +# Don't lint build output. +dist diff --git a/packages/eden-watcher/.eslintrc.json b/packages/eden-watcher/.eslintrc.json new file mode 100644 index 00000000..476d529d --- /dev/null +++ b/packages/eden-watcher/.eslintrc.json @@ -0,0 +1,27 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "semistandard", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + } +} diff --git a/packages/eden-watcher/README.md b/packages/eden-watcher/README.md new file mode 100644 index 00000000..0435c97d --- /dev/null +++ b/packages/eden-watcher/README.md @@ -0,0 +1,191 @@ +# EdenNetwork Watcher + +## Setup + +* Run the following command to install required packages: + + ```bash + yarn + ``` + +* Run the IPFS (go-ipfs version 0.9.0) daemon: + + ```bash + ipfs daemon + ``` + +* Create a postgres12 database for the watcher: + + ```bash + sudo su - postgres + createdb eden-watcher + ``` + +* If the watcher is an `active` watcher: + + Create database for the job queue and enable the `pgcrypto` extension on them (https://github.com/timgit/pg-boss/blob/master/docs/usage.md#intro): + + ``` + createdb eden-watcher-job-queue + ``` + + ``` + postgres@tesla:~$ psql -U postgres -h localhost eden-watcher-job-queue + Password for user postgres: + psql (12.7 (Ubuntu 12.7-1.pgdg18.04+1)) + SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) + Type "help" for help. + + eden-watcher-job-queue=# CREATE EXTENSION pgcrypto; + CREATE EXTENSION + eden-watcher-job-queue=# exit + ``` + +* In the [config file](./environments/local.toml): + + * Update the database connection settings. + + * Update the `upstream` config and provide the `ipld-eth-server` GQL API and the `indexer-db` postgraphile endpoints. + + * Update the `server` config with state checkpoint settings and provide the IPFS API address. + +## Customize + +* Indexing on an event: + + * Edit the custom hook function `handleEvent` (triggered on an event) in [hooks.ts](./src/hooks.ts) to perform corresponding indexing using the `Indexer` object. + + * While using the indexer storage methods for indexing, pass `diff` as true if default state is desired to be generated using the state variables being indexed. + +* Generating state: + + * Edit the custom hook function `createInitialCheckpoint` (triggered on watch-contract, checkpoint: `true`) in [hooks.ts](./src/hooks.ts) to save an initial checkpoint `IPLDBlock` using the `Indexer` object. + + * Edit the custom hook function `createStateDiff` (triggered on a block) in [hooks.ts](./src/hooks.ts) to save the state in a `diff` `IPLDBlock` using the `Indexer` object. The default state (if exists) is updated. + + * Edit the custom hook function `createStateCheckpoint` (triggered just before default and CLI checkpoint) in [hooks.ts](./src/hooks.ts) to save the state in a `checkpoint` `IPLDBlock` using the `Indexer` object. + +* The existing example hooks in [hooks.ts](./src/hooks.ts) are for an `ERC20` contract. + +## Run + +* Run the watcher: + + ```bash + yarn server + ``` + +GQL console: http://localhost:3008/graphql + +* If the watcher is an `active` watcher: + + * Run the job-runner: + + ```bash + yarn job-runner + ``` + + * To watch a contract: + + ```bash + yarn watch:contract --address --kind --checkpoint --starting-block [block-number] + ``` + + * `address`: Address or identifier of the contract to be watched. + * `kind`: Kind of the contract. + * `checkpoint`: Turn checkpointing on (`true` | `false`). + * `starting-block`: Starting block for the contract (default: `1`). + + Examples: + + Watch a contract with its address and checkpointing on: + + ```bash + yarn watch:contract --address 0x1F78641644feB8b64642e833cE4AFE93DD6e7833 --kind ERC20 --checkpoint true + ``` + + Watch a contract with its identifier and checkpointing on: + + ```bash + yarn watch:contract --address MyProtocol --kind protocol --checkpoint true + ``` + + * To fill a block range: + + ```bash + yarn fill --start-block --end-block + ``` + + * `start-block`: Block number to start filling from. + * `end-block`: Block number till which to fill. + + * To create a checkpoint for a contract: + + ```bash + yarn checkpoint --address --block-hash [block-hash] + ``` + + * `address`: Address or identifier of the contract for which to create a checkpoint. + * `block-hash`: Hash of a block (in the pruned region) at which to create the checkpoint (default: latest canonical block hash). + + * To reset the watcher to a previous block number: + + * Reset state: + + ```bash + yarn reset state --block-number + ``` + + * Reset job-queue: + + ```bash + yarn reset job-queue --block-number + ``` + + * `block-number`: Block number to which to reset the watcher. + + * To export and import the watcher state: + + * In source watcher, export watcher state: + + ```bash + yarn export-state --export-file [export-file-path] + ``` + + * `export-file`: Path of JSON file to which to export the watcher data. + + * In target watcher, run job-runner: + + ```bash + yarn job-runner + ``` + + * Import watcher state: + + ```bash + yarn import-state --import-file + ``` + + * `import-file`: Path of JSON file from which to import the watcher data. + + * Run fill: + + ```bash + yarn fill --start-block --end-block + ``` + + * `snapshot-block`: Block number at which the watcher state was exported. + + * Run server: + + ```bash + yarn server + ``` + + * To inspect a CID: + + ```bash + yarn inspect-cid --cid + ``` + + * `cid`: CID to be inspected. diff --git a/packages/eden-watcher/environments/local.toml b/packages/eden-watcher/environments/local.toml new file mode 100644 index 00000000..47c77134 --- /dev/null +++ b/packages/eden-watcher/environments/local.toml @@ -0,0 +1,42 @@ +[server] + host = "127.0.0.1" + port = 3012 + kind = "active" + + # Checkpointing state. + checkpointing = true + + # Checkpoint interval in number of blocks. + checkpointInterval = 2000 + + # IPFS API address (can be taken from the output on running the IPFS daemon). + ipfsApiAddr = "/ip4/127.0.0.1/tcp/5001" + + subgraphPath = "../graph-node/test/subgraph/eden" + +[database] + type = "postgres" + host = "localhost" + port = 5432 + database = "eden-watcher" + username = "postgres" + password = "postgres" + synchronize = true + logging = false + +[upstream] + [upstream.ethServer] + gqlApiEndpoint = "http://127.0.0.1:8082/graphql" + gqlPostgraphileEndpoint = "http://127.0.0.1:5000/graphql" + rpcProviderEndpoint = "http://127.0.0.1:8081" + blockDelayInMilliSecs = 2000 + + [upstream.cache] + name = "requests" + enabled = false + deleteOnStart = false + +[jobQueue] + dbConnectionString = "postgres://postgres:postgres@localhost/eden-watcher-job-queue" + maxCompletionLagInSecs = 300 + jobDelayInMilliSecs = 100 diff --git a/packages/eden-watcher/package.json b/packages/eden-watcher/package.json new file mode 100644 index 00000000..87780fb2 --- /dev/null +++ b/packages/eden-watcher/package.json @@ -0,0 +1,69 @@ +{ + "name": "@vulcanize/eden-watcher", + "version": "0.1.0", + "description": "eden-watcher", + "private": true, + "main": "dist/index.js", + "scripts": { + "lint": "eslint .", + "build": "tsc", + "server": "DEBUG=vulcanize:* ts-node src/server.ts", + "job-runner": "DEBUG=vulcanize:* ts-node src/job-runner.ts", + "watch:contract": "DEBUG=vulcanize:* ts-node src/cli/watch-contract.ts", + "fill": "DEBUG=vulcanize:* ts-node src/fill.ts", + "reset": "DEBUG=vulcanize:* ts-node src/cli/reset.ts", + "checkpoint": "DEBUG=vulcanize:* ts-node src/cli/checkpoint.ts", + "export-state": "DEBUG=vulcanize:* ts-node src/cli/export-state.ts", + "import-state": "DEBUG=vulcanize:* ts-node src/cli/import-state.ts", + "inspect-cid": "DEBUG=vulcanize:* ts-node src/cli/inspect-cid.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vulcanize/watcher-ts.git" + }, + "author": "", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/vulcanize/watcher-ts/issues" + }, + "homepage": "https://github.com/vulcanize/watcher-ts#readme", + "dependencies": { + "@apollo/client": "^3.3.19", + "@ethersproject/providers": "5.3.0", + "@ipld/dag-cbor": "^6.0.12", + "@vulcanize/cache": "^0.1.0", + "@vulcanize/ipld-eth-client": "^0.1.0", + "@vulcanize/solidity-mapper": "^0.1.0", + "@vulcanize/util": "^0.1.0", + "apollo-server-express": "^2.25.0", + "apollo-type-bigint": "^0.1.3", + "debug": "^4.3.1", + "ethers": "^5.2.0", + "express": "^4.17.1", + "graphql": "^15.5.0", + "graphql-import-node": "^0.0.4", + "ipfs-http-client": "^53.0.1", + "json-bigint": "^1.0.0", + "lodash": "^4.17.21", + "multiformats": "^9.4.8", + "reflect-metadata": "^0.1.13", + "typeorm": "^0.2.32", + "yargs": "^17.0.1" + }, + "devDependencies": { + "@ethersproject/abi": "^5.3.0", + "@types/express": "^4.17.11", + "@types/yargs": "^17.0.0", + "@typescript-eslint/eslint-plugin": "^4.25.0", + "@typescript-eslint/parser": "^4.25.0", + "eslint": "^7.27.0", + "eslint-config-semistandard": "^15.0.1", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.23.3", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.1.0", + "eslint-plugin-standard": "^5.0.0", + "ts-node": "^10.0.0", + "typescript": "^4.3.2" + } +} diff --git a/packages/eden-watcher/src/artifacts/DistributorGovernance.json b/packages/eden-watcher/src/artifacts/DistributorGovernance.json new file mode 100644 index 00000000..f308b6cc --- /dev/null +++ b/packages/eden-watcher/src/artifacts/DistributorGovernance.json @@ -0,0 +1,723 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_admin", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_blockProducers", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_collectors", + "type": "address[]" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "producer", + "type": "address" + } + ], + "name": "BlockProducerAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "producer", + "type": "address" + } + ], + "name": "BlockProducerRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "producer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "collector", + "type": "address" + } + ], + "name": "BlockProducerRewardCollectorChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "RewardScheduleChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DELEGATOR_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GOV_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "REWARD_SCHEDULE_ENTRY_LENGTH", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "producer", + "type": "address" + } + ], + "name": "add", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "producers", + "type": "address[]" + } + ], + "name": "addBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "blockProducer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "producer", + "type": "address" + }, + { + "internalType": "address", + "name": "collector", + "type": "address" + } + ], + "name": "delegate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "producers", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "collectors", + "type": "address[]" + } + ], + "name": "delegateBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getRoleMember", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleMemberCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "producer", + "type": "address" + } + ], + "name": "remove", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "producers", + "type": "address[]" + } + ], + "name": "removeBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "rewardCollector", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardScheduleEntries", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "rewardScheduleEntry", + "outputs": [ + { + "components": [ + { + "internalType": "uint64", + "name": "startTime", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "epochDuration", + "type": "uint64" + }, + { + "internalType": "uint128", + "name": "rewardsPerEpoch", + "type": "uint128" + } + ], + "internalType": "struct IGovernance.RewardScheduleEntry", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "set", + "type": "bytes" + } + ], + "name": "setRewardSchedule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "storageLayout": { + "storage": [ + { + "astId": 380, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)375_storage)" + }, + { + "astId": 1210, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "_roleMembers", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_struct(AddressSet)969_storage)" + }, + { + "astId": 1721, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "rewardCollector", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_address,t_address)" + }, + { + "astId": 1727, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "blockProducer", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_bool)" + }, + { + "astId": 1730, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "_rewardSchedule", + "offset": 0, + "slot": "4", + "type": "t_bytes_storage" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "base": "t_bytes32", + "encoding": "dynamic_array", + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "encoding": "inplace", + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "encoding": "bytes", + "label": "bytes", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_address)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => address)", + "numberOfBytes": "32", + "value": "t_address" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_bytes32,t_struct(AddressSet)969_storage)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => struct EnumerableSet.AddressSet)", + "numberOfBytes": "32", + "value": "t_struct(AddressSet)969_storage" + }, + "t_mapping(t_bytes32,t_struct(RoleData)375_storage)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32", + "value": "t_struct(RoleData)375_storage" + }, + "t_mapping(t_bytes32,t_uint256)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_struct(AddressSet)969_storage": { + "encoding": "inplace", + "label": "struct EnumerableSet.AddressSet", + "members": [ + { + "astId": 968, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "_inner", + "offset": 0, + "slot": "0", + "type": "t_struct(Set)698_storage" + } + ], + "numberOfBytes": "64" + }, + "t_struct(RoleData)375_storage": { + "encoding": "inplace", + "label": "struct AccessControl.RoleData", + "members": [ + { + "astId": 372, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "members", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_address,t_bool)" + }, + { + "astId": 374, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "adminRole", + "offset": 0, + "slot": "1", + "type": "t_bytes32" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Set)698_storage": { + "encoding": "inplace", + "label": "struct EnumerableSet.Set", + "members": [ + { + "astId": 693, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "_values", + "offset": 0, + "slot": "0", + "type": "t_array(t_bytes32)dyn_storage" + }, + { + "astId": 697, + "contract": "DistributorGovernance.sol:DistributorGovernance", + "label": "_indexes", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_uint256)" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } +} \ No newline at end of file diff --git a/packages/eden-watcher/src/artifacts/EdenNetwork.json b/packages/eden-watcher/src/artifacts/EdenNetwork.json new file mode 100644 index 00000000..205c9be0 --- /dev/null +++ b/packages/eden-watcher/src/artifacts/EdenNetwork.json @@ -0,0 +1,942 @@ +{ + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "oldAdmin", + "type": "address" + } + ], + "name": "AdminUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint8", + "name": "slot", + "type": "uint8" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "newBidAmount", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "oldBidAmount", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "taxNumerator", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "taxDenominator", + "type": "uint16" + } + ], + "name": "SlotClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint8", + "name": "slot", + "type": "uint8" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newDelegate", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "oldDelegate", + "type": "address" + } + ], + "name": "SlotDelegateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "staker", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "stakeAmount", + "type": "uint256" + } + ], + "name": "Stake", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "newNumerator", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "newDenominator", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "oldNumerator", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "oldDenominator", + "type": "uint16" + } + ], + "name": "TaxRateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "staker", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "unstakedAmount", + "type": "uint256" + } + ], + "name": "Unstake", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "withdrawer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "withdrawalAmount", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [], + "name": "MIN_BID", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "slot", + "type": "uint8" + }, + { + "internalType": "uint128", + "name": "bid", + "type": "uint128" + }, + { + "internalType": "address", + "name": "delegate", + "type": "address" + } + ], + "name": "claimSlot", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "slot", + "type": "uint8" + }, + { + "internalType": "uint128", + "name": "bid", + "type": "uint128" + }, + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "claimSlotWithPermit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20Extended", + "name": "_token", + "type": "address" + }, + { + "internalType": "contract ILockManager", + "name": "_lockManager", + "type": "address" + }, + { + "internalType": "address", + "name": "_admin", + "type": "address" + }, + { + "internalType": "uint16", + "name": "_taxNumerator", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "_taxDenominator", + "type": "uint16" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "lockManager", + "outputs": [ + { + "internalType": "contract ILockManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "lockedBalance", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "setAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "slot", + "type": "uint8" + }, + { + "internalType": "address", + "name": "delegate", + "type": "address" + } + ], + "name": "setSlotDelegate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "numerator", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "denominator", + "type": "uint16" + } + ], + "name": "setTaxRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "slot", + "type": "uint8" + } + ], + "name": "slotBalance", + "outputs": [ + { + "internalType": "uint128", + "name": "balance", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "name": "slotBid", + "outputs": [ + { + "internalType": "address", + "name": "bidder", + "type": "address" + }, + { + "internalType": "uint16", + "name": "taxNumerator", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "taxDenominator", + "type": "uint16" + }, + { + "internalType": "uint64", + "name": "periodStart", + "type": "uint64" + }, + { + "internalType": "uint128", + "name": "bidAmount", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "slot", + "type": "uint8" + } + ], + "name": "slotCost", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "slot", + "type": "uint8" + } + ], + "name": "slotDelegate", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "name": "slotExpiration", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "slot", + "type": "uint8" + } + ], + "name": "slotForeclosed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "slot", + "type": "uint8" + } + ], + "name": "slotOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "name": "stake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "stakeWithPermit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "stakedBalance", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "taxDenominator", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "taxNumerator", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token", + "outputs": [ + { + "internalType": "contract IERC20Extended", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "name": "unstake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "storageLayout": { + "storage": [ + { + "astId": 322, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_bool" + }, + { + "astId": 325, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool" + }, + { + "astId": 381, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "slotExpiration", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_uint8,t_uint64)" + }, + { + "astId": 386, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "_slotDelegate", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint8,t_address)" + }, + { + "astId": 391, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "_slotOwner", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint8,t_address)" + }, + { + "astId": 397, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "slotBid", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint8,t_struct(Bid)376_storage)" + }, + { + "astId": 402, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "stakedBalance", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_uint128)" + }, + { + "astId": 407, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "lockedBalance", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_uint128)" + }, + { + "astId": 411, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "token", + "offset": 0, + "slot": "7", + "type": "t_contract(IERC20Extended)318" + }, + { + "astId": 415, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "lockManager", + "offset": 0, + "slot": "8", + "type": "t_contract(ILockManager)133" + }, + { + "astId": 418, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "admin", + "offset": 0, + "slot": "9", + "type": "t_address" + }, + { + "astId": 421, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "taxNumerator", + "offset": 20, + "slot": "9", + "type": "t_uint16" + }, + { + "astId": 424, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "taxDenominator", + "offset": 22, + "slot": "9", + "type": "t_uint16" + }, + { + "astId": 427, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "MIN_BID", + "offset": 0, + "slot": "10", + "type": "t_uint128" + }, + { + "astId": 430, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "_NOT_ENTERED", + "offset": 0, + "slot": "11", + "type": "t_uint256" + }, + { + "astId": 433, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "_ENTERED", + "offset": 0, + "slot": "12", + "type": "t_uint256" + }, + { + "astId": 436, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "_status", + "offset": 0, + "slot": "13", + "type": "t_uint256" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IERC20Extended)318": { + "encoding": "inplace", + "label": "contract IERC20Extended", + "numberOfBytes": "20" + }, + "t_contract(ILockManager)133": { + "encoding": "inplace", + "label": "contract ILockManager", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_uint128)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint128)", + "numberOfBytes": "32", + "value": "t_uint128" + }, + "t_mapping(t_uint8,t_address)": { + "encoding": "mapping", + "key": "t_uint8", + "label": "mapping(uint8 => address)", + "numberOfBytes": "32", + "value": "t_address" + }, + "t_mapping(t_uint8,t_struct(Bid)376_storage)": { + "encoding": "mapping", + "key": "t_uint8", + "label": "mapping(uint8 => struct EdenNetwork.Bid)", + "numberOfBytes": "32", + "value": "t_struct(Bid)376_storage" + }, + "t_mapping(t_uint8,t_uint64)": { + "encoding": "mapping", + "key": "t_uint8", + "label": "mapping(uint8 => uint64)", + "numberOfBytes": "32", + "value": "t_uint64" + }, + "t_struct(Bid)376_storage": { + "encoding": "inplace", + "label": "struct EdenNetwork.Bid", + "members": [ + { + "astId": 367, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "bidder", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 369, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "taxNumerator", + "offset": 20, + "slot": "0", + "type": "t_uint16" + }, + { + "astId": 371, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "taxDenominator", + "offset": 22, + "slot": "0", + "type": "t_uint16" + }, + { + "astId": 373, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "periodStart", + "offset": 24, + "slot": "0", + "type": "t_uint64" + }, + { + "astId": 375, + "contract": "EdenNetwork.sol:EdenNetwork", + "label": "bidAmount", + "offset": 0, + "slot": "1", + "type": "t_uint128" + } + ], + "numberOfBytes": "64" + }, + "t_uint128": { + "encoding": "inplace", + "label": "uint128", + "numberOfBytes": "16" + }, + "t_uint16": { + "encoding": "inplace", + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "encoding": "inplace", + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "encoding": "inplace", + "label": "uint8", + "numberOfBytes": "1" + } + } + } +} \ No newline at end of file diff --git a/packages/eden-watcher/src/artifacts/MerkleDistributor.json b/packages/eden-watcher/src/artifacts/MerkleDistributor.json new file mode 100644 index 00000000..04ae9a3c --- /dev/null +++ b/packages/eden-watcher/src/artifacts/MerkleDistributor.json @@ -0,0 +1,1482 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "contract IERC20Mintable", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_admin", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_updateThreshold", + "type": "uint8" + }, + { + "internalType": "address[]", + "name": "_updaters", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_slashers", + "type": "address[]" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalClaimed", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSlashed", + "type": "uint256" + } + ], + "name": "AccountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalEarned", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "claimed", + "type": "uint256" + } + ], + "name": "Claimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "GovernanceChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "distributionNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "metadataURI", + "type": "string" + } + ], + "name": "MerkleRootUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "PermanentURI", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "slashed", + "type": "uint256" + } + ], + "name": "Slashed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "updateThreshold", + "type": "uint256" + } + ], + "name": "UpdateThresholdChanged", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DISTRIBUTOR_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SLASHER_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UPDATER_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "accountState", + "outputs": [ + { + "internalType": "uint256", + "name": "totalClaimed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalSlashed", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "newUpdaters", + "type": "address[]" + }, + { + "internalType": "uint256", + "name": "newThreshold", + "type": "uint256" + } + ], + "name": "addUpdaters", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "totalEarned", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "merkleProof", + "type": "bytes32[]" + } + ], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "distributionCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getRoleMember", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleMemberCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "merkleRoot", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "previousMerkleRoot", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "existingUpdaters", + "type": "address[]" + }, + { + "internalType": "uint256", + "name": "newThreshold", + "type": "uint256" + } + ], + "name": "removeUpdaters", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "setGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "to", + "type": "uint256" + } + ], + "name": "setUpdateThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "slash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token", + "outputs": [ + { + "internalType": "contract IERC20Mintable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "newMerkleRoot", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "uri", + "type": "string" + }, + { + "internalType": "uint256", + "name": "newDistributionNumber", + "type": "uint256" + } + ], + "name": "updateMerkleRoot", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "updateThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "storageLayout": { + "storage": [ + { + "astId": 2154, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_name", + "offset": 0, + "slot": "0", + "type": "t_string_storage" + }, + { + "astId": 2156, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_symbol", + "offset": 0, + "slot": "1", + "type": "t_string_storage" + }, + { + "astId": 2160, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_owners", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_address)" + }, + { + "astId": 2164, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_balances", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 2168, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_tokenApprovals", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_address)" + }, + { + "astId": 2174, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_operatorApprovals", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))" + }, + { + "astId": 774, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_roles", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_bytes32,t_struct(RoleData)769_storage)" + }, + { + "astId": 1604, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_roleMembers", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_bytes32,t_struct(AddressSet)1363_storage)" + }, + { + "astId": 2957, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_ownedTokens", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))" + }, + { + "astId": 2961, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_ownedTokensIndex", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_uint256,t_uint256)" + }, + { + "astId": 2964, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_allTokens", + "offset": 0, + "slot": "10", + "type": "t_array(t_uint256)dyn_storage" + }, + { + "astId": 2968, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_allTokensIndex", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_uint256)" + }, + { + "astId": 3314, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "merkleRoot", + "offset": 0, + "slot": "12", + "type": "t_bytes32" + }, + { + "astId": 3318, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "distributionCount", + "offset": 0, + "slot": "13", + "type": "t_uint256" + }, + { + "astId": 3321, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "updateThreshold", + "offset": 0, + "slot": "14", + "type": "t_uint256" + }, + { + "astId": 3324, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "governance", + "offset": 0, + "slot": "15", + "type": "t_address" + }, + { + "astId": 3336, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "accountState", + "offset": 0, + "slot": "16", + "type": "t_mapping(t_address,t_struct(AccountState)3329_storage)" + }, + { + "astId": 3342, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "previousMerkleRoot", + "offset": 0, + "slot": "17", + "type": "t_mapping(t_bytes32,t_bool)" + }, + { + "astId": 3347, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_tokenURI", + "offset": 0, + "slot": "18", + "type": "t_mapping(t_uint256,t_string_storage)" + }, + { + "astId": 3352, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_updateVotes", + "offset": 0, + "slot": "19", + "type": "t_mapping(t_bytes32,t_uint256)" + }, + { + "astId": 3359, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_updaterVotes", + "offset": 0, + "slot": "20", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_bytes32))" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "base": "t_bytes32", + "encoding": "dynamic_array", + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)dyn_storage": { + "base": "t_uint256", + "encoding": "dynamic_array", + "label": "uint256[]", + "numberOfBytes": "32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "encoding": "inplace", + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_bool)" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_bytes32))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(uint256 => bytes32))", + "numberOfBytes": "32", + "value": "t_mapping(t_uint256,t_bytes32)" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(uint256 => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_uint256,t_uint256)" + }, + "t_mapping(t_address,t_struct(AccountState)3329_storage)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => struct MerkleDistributor.AccountState)", + "numberOfBytes": "32", + "value": "t_struct(AccountState)3329_storage" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_bytes32,t_bool)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_bytes32,t_struct(AddressSet)1363_storage)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => struct EnumerableSet.AddressSet)", + "numberOfBytes": "32", + "value": "t_struct(AddressSet)1363_storage" + }, + "t_mapping(t_bytes32,t_struct(RoleData)769_storage)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32", + "value": "t_struct(RoleData)769_storage" + }, + "t_mapping(t_bytes32,t_uint256)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint256,t_address)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => address)", + "numberOfBytes": "32", + "value": "t_address" + }, + "t_mapping(t_uint256,t_bytes32)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => bytes32)", + "numberOfBytes": "32", + "value": "t_bytes32" + }, + "t_mapping(t_uint256,t_string_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => string)", + "numberOfBytes": "32", + "value": "t_string_storage" + }, + "t_mapping(t_uint256,t_uint256)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_string_storage": { + "encoding": "bytes", + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(AccountState)3329_storage": { + "encoding": "inplace", + "label": "struct MerkleDistributor.AccountState", + "members": [ + { + "astId": 3326, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "totalClaimed", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 3328, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "totalSlashed", + "offset": 0, + "slot": "1", + "type": "t_uint256" + } + ], + "numberOfBytes": "64" + }, + "t_struct(AddressSet)1363_storage": { + "encoding": "inplace", + "label": "struct EnumerableSet.AddressSet", + "members": [ + { + "astId": 1362, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_inner", + "offset": 0, + "slot": "0", + "type": "t_struct(Set)1092_storage" + } + ], + "numberOfBytes": "64" + }, + "t_struct(RoleData)769_storage": { + "encoding": "inplace", + "label": "struct AccessControl.RoleData", + "members": [ + { + "astId": 766, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "members", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_address,t_bool)" + }, + { + "astId": 768, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "adminRole", + "offset": 0, + "slot": "1", + "type": "t_bytes32" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Set)1092_storage": { + "encoding": "inplace", + "label": "struct EnumerableSet.Set", + "members": [ + { + "astId": 1087, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_values", + "offset": 0, + "slot": "0", + "type": "t_array(t_bytes32)dyn_storage" + }, + { + "astId": 1091, + "contract": "MerkleDistributor.sol:MerkleDistributor", + "label": "_indexes", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_uint256)" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } +} \ No newline at end of file diff --git a/packages/eden-watcher/src/cli/checkpoint.ts b/packages/eden-watcher/src/cli/checkpoint.ts new file mode 100644 index 00000000..5d48dacd --- /dev/null +++ b/packages/eden-watcher/src/cli/checkpoint.ts @@ -0,0 +1,69 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import path from 'path'; +import yargs from 'yargs'; +import 'reflect-metadata'; +import debug from 'debug'; + +import { Config, DEFAULT_CONFIG_PATH, getConfig, initClients } from '@vulcanize/util'; +import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node'; + +import { Database } from '../database'; +import { Indexer } from '../indexer'; + +const log = debug('vulcanize:checkpoint'); + +const main = async (): Promise => { + const argv = await yargs.parserConfiguration({ + 'parse-numbers': false + }).options({ + configFile: { + alias: 'f', + type: 'string', + require: true, + demandOption: true, + describe: 'Configuration file path (toml)', + default: DEFAULT_CONFIG_PATH + }, + address: { + type: 'string', + require: true, + demandOption: true, + describe: 'Contract address to create the checkpoint for.' + }, + blockHash: { + type: 'string', + describe: 'Blockhash at which to create the checkpoint.' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, postgraphileClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.init(); + + const graphDb = new GraphDatabase(config.database, path.resolve(__dirname, 'entity/*')); + await graphDb.init(); + + const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, config.server.subgraphPath); + + const indexer = new Indexer(config.server, db, ethClient, postgraphileClient, ethProvider, graphWatcher); + + graphWatcher.setIndexer(indexer); + await graphWatcher.init(); + + const blockHash = await indexer.processCLICheckpoint(argv.address, argv.blockHash); + + log(`Created a checkpoint for contract ${argv.address} at block-hash ${blockHash}`); + + await db.close(); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/eden-watcher/src/cli/export-state.ts b/packages/eden-watcher/src/cli/export-state.ts new file mode 100644 index 00000000..73b6e624 --- /dev/null +++ b/packages/eden-watcher/src/cli/export-state.ts @@ -0,0 +1,119 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import yargs from 'yargs'; +import 'reflect-metadata'; +import debug from 'debug'; +import fs from 'fs'; +import path from 'path'; + +import { Config, DEFAULT_CONFIG_PATH, getConfig, initClients } from '@vulcanize/util'; +import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node'; +import * as codec from '@ipld/dag-cbor'; + +import { Database } from '../database'; +import { Indexer } from '../indexer'; + +const log = debug('vulcanize:export-state'); + +const main = async (): Promise => { + const argv = await yargs.parserConfiguration({ + 'parse-numbers': false + }).options({ + configFile: { + alias: 'f', + type: 'string', + require: true, + demandOption: true, + describe: 'Configuration file path (toml)', + default: DEFAULT_CONFIG_PATH + }, + exportFile: { + alias: 'o', + type: 'string', + describe: 'Export file path' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, postgraphileClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.init(); + + const graphDb = new GraphDatabase(config.database, path.resolve(__dirname, 'entity/*')); + await graphDb.init(); + + const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, config.server.subgraphPath); + + const indexer = new Indexer(config.server, db, ethClient, postgraphileClient, ethProvider, graphWatcher); + + graphWatcher.setIndexer(indexer); + await graphWatcher.init(); + + const exportData: any = { + snapshotBlock: {}, + contracts: [], + ipldCheckpoints: [] + }; + + const contracts = await db.getContracts({}); + + // Get latest canonical block. + const block = await indexer.getLatestCanonicalBlock(); + assert(block); + + // Export snapshot block. + exportData.snapshotBlock = { + blockNumber: block.blockNumber, + blockHash: block.blockHash + }; + + // Export contracts and checkpoints. + for (const contract of contracts) { + exportData.contracts.push({ + address: contract.address, + kind: contract.kind, + checkpoint: contract.checkpoint, + startingBlock: block.blockNumber + }); + + // Create and export checkpoint if checkpointing is on for the contract. + if (contract.checkpoint) { + await indexer.createCheckpoint(contract.address, block.blockHash); + + const ipldBlock = await indexer.getLatestIPLDBlock(contract.address, 'checkpoint', block.blockNumber); + assert(ipldBlock); + + const data = codec.decode(Buffer.from(ipldBlock.data)) as any; + + exportData.ipldCheckpoints.push({ + contractAddress: ipldBlock.contractAddress, + cid: ipldBlock.cid, + kind: ipldBlock.kind, + data + }); + } + } + + if (argv.exportFile) { + const encodedExportData = codec.encode(exportData); + + const filePath = path.resolve(argv.exportFile); + const fileDir = path.dirname(filePath); + + if (!fs.existsSync(fileDir)) fs.mkdirSync(fileDir, { recursive: true }); + + fs.writeFileSync(filePath, encodedExportData); + } else { + log(exportData); + } +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/eden-watcher/src/cli/import-state.ts b/packages/eden-watcher/src/cli/import-state.ts new file mode 100644 index 00000000..b98e5997 --- /dev/null +++ b/packages/eden-watcher/src/cli/import-state.ts @@ -0,0 +1,118 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import 'reflect-metadata'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import debug from 'debug'; +import { PubSub } from 'apollo-server-express'; +import fs from 'fs'; +import path from 'path'; + +import { getConfig, fillBlocks, JobQueue, DEFAULT_CONFIG_PATH, Config, initClients } from '@vulcanize/util'; +import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node'; +import * as codec from '@ipld/dag-cbor'; + +import { Database } from '../database'; +import { Indexer } from '../indexer'; +import { EventWatcher } from '../events'; +import { IPLDBlock } from '../entity/IPLDBlock'; + +const log = debug('vulcanize:import-state'); + +export const main = async (): Promise => { + const argv = await yargs(hideBin(process.argv)).parserConfiguration({ + 'parse-numbers': false + }).options({ + configFile: { + alias: 'f', + type: 'string', + demandOption: true, + describe: 'configuration file path (toml)', + default: DEFAULT_CONFIG_PATH + }, + importFile: { + alias: 'i', + type: 'string', + demandOption: true, + describe: 'Import file path (JSON)' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, postgraphileClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.init(); + + const graphDb = new GraphDatabase(config.database, path.resolve(__dirname, 'entity/*')); + await graphDb.init(); + + const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, config.server.subgraphPath); + + // Note: In-memory pubsub works fine for now, as each watcher is a single process anyway. + // Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries + const pubsub = new PubSub(); + const indexer = new Indexer(config.server, db, ethClient, postgraphileClient, ethProvider, graphWatcher); + + graphWatcher.setIndexer(indexer); + await graphWatcher.init(); + + const jobQueueConfig = config.jobQueue; + assert(jobQueueConfig, 'Missing job queue config'); + + const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig; + assert(dbConnectionString, 'Missing job queue db connection string'); + + const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs }); + await jobQueue.start(); + + const eventWatcher = new EventWatcher(config.upstream, ethClient, postgraphileClient, indexer, pubsub, jobQueue); + + // Import data. + const importFilePath = path.resolve(argv.importFile); + const encodedImportData = fs.readFileSync(importFilePath); + const importData = codec.decode(Buffer.from(encodedImportData)) as any; + + // Fill the snapshot block. + await fillBlocks( + jobQueue, + indexer, + postgraphileClient, + eventWatcher, + config.upstream.ethServer.blockDelayInMilliSecs, + { + startBlock: importData.snapshotBlock.blockNumber, + endBlock: importData.snapshotBlock.blockNumber + } + ); + + // Fill the Contracts. + for (const contract of importData.contracts) { + await db.saveContract(contract.address, contract.kind, contract.checkpoint, contract.startingBlock); + } + + // Get the snapshot block. + const block = await indexer.getBlockProgress(importData.snapshotBlock.blockHash); + assert(block); + + // Fill the IPLDBlocks. + for (const checkpoint of importData.ipldCheckpoints) { + let ipldBlock = new IPLDBlock(); + + ipldBlock = Object.assign(ipldBlock, checkpoint); + ipldBlock.block = block; + + ipldBlock.data = Buffer.from(codec.encode(ipldBlock.data)); + + await db.saveOrUpdateIPLDBlock(ipldBlock); + } +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/eden-watcher/src/cli/inspect-cid.ts b/packages/eden-watcher/src/cli/inspect-cid.ts new file mode 100644 index 00000000..7c91e59d --- /dev/null +++ b/packages/eden-watcher/src/cli/inspect-cid.ts @@ -0,0 +1,68 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import path from 'path'; +import assert from 'assert'; +import yargs from 'yargs'; +import 'reflect-metadata'; +import debug from 'debug'; +import util from 'util'; + +import { Config, DEFAULT_CONFIG_PATH, getConfig, initClients } from '@vulcanize/util'; +import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node'; + +import { Database } from '../database'; +import { Indexer } from '../indexer'; + +const log = debug('vulcanize:inspect-cid'); + +const main = async (): Promise => { + const argv = await yargs.parserConfiguration({ + 'parse-numbers': false + }).options({ + configFile: { + alias: 'f', + type: 'string', + require: true, + demandOption: true, + describe: 'Configuration file path (toml)', + default: DEFAULT_CONFIG_PATH + }, + cid: { + alias: 'c', + type: 'string', + demandOption: true, + describe: 'CID to be inspected' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, postgraphileClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.init(); + + const graphDb = new GraphDatabase(config.database, path.resolve(__dirname, 'entity/*')); + await graphDb.init(); + + const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, config.server.subgraphPath); + + const indexer = new Indexer(config.server, db, ethClient, postgraphileClient, ethProvider, graphWatcher); + + graphWatcher.setIndexer(indexer); + await graphWatcher.init(); + + const ipldBlock = await indexer.getIPLDBlockByCid(argv.cid); + assert(ipldBlock, 'IPLDBlock for the provided CID doesn\'t exist.'); + + const ipldData = await indexer.getIPLDData(ipldBlock); + + log(util.inspect(ipldData, false, null)); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/eden-watcher/src/cli/reset-cmds/job-queue.ts b/packages/eden-watcher/src/cli/reset-cmds/job-queue.ts new file mode 100644 index 00000000..a8766bcf --- /dev/null +++ b/packages/eden-watcher/src/cli/reset-cmds/job-queue.ts @@ -0,0 +1,22 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import debug from 'debug'; + +import { getConfig, resetJobs } from '@vulcanize/util'; + +const log = debug('vulcanize:reset-job-queue'); + +export const command = 'job-queue'; + +export const desc = 'Reset job queue'; + +export const builder = {}; + +export const handler = async (argv: any): Promise => { + const config = await getConfig(argv.configFile); + await resetJobs(config); + + log('Job queue reset successfully'); +}; diff --git a/packages/eden-watcher/src/cli/reset-cmds/state.ts b/packages/eden-watcher/src/cli/reset-cmds/state.ts new file mode 100644 index 00000000..19ec7a16 --- /dev/null +++ b/packages/eden-watcher/src/cli/reset-cmds/state.ts @@ -0,0 +1,91 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import path from 'path'; +import debug from 'debug'; +import { MoreThan } from 'typeorm'; +import assert from 'assert'; + +import { getConfig, initClients, resetJobs } from '@vulcanize/util'; +import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node'; + +import { Database } from '../../database'; +import { Indexer } from '../../indexer'; +import { BlockProgress } from '../../entity/BlockProgress'; + +const log = debug('vulcanize:reset-state'); + +export const command = 'state'; + +export const desc = 'Reset state to block number'; + +export const builder = { + blockNumber: { + type: 'number' + } +}; + +export const handler = async (argv: any): Promise => { + const config = await getConfig(argv.configFile); + await resetJobs(config); + const { ethClient, postgraphileClient, ethProvider } = await initClients(config); + + // Initialize database. + const db = new Database(config.database); + await db.init(); + + const graphDb = new GraphDatabase(config.database, path.resolve(__dirname, 'entity/*')); + await graphDb.init(); + + const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, config.server.subgraphPath); + + const indexer = new Indexer(config.server, db, ethClient, postgraphileClient, ethProvider, graphWatcher); + + graphWatcher.setIndexer(indexer); + await graphWatcher.init(); + + const syncStatus = await indexer.getSyncStatus(); + assert(syncStatus, 'Missing syncStatus'); + + const hooksStatus = await indexer.getHookStatus(); + assert(hooksStatus, 'Missing hooksStatus'); + + const blockProgresses = await indexer.getBlocksAtHeight(argv.blockNumber, false); + assert(blockProgresses.length, `No blocks at specified block number ${argv.blockNumber}`); + assert(!blockProgresses.some(block => !block.isComplete), `Incomplete block at block number ${argv.blockNumber} with unprocessed events`); + const [blockProgress] = blockProgresses; + + const dbTx = await db.createTransactionRunner(); + + try { + const entities = [BlockProgress]; + + const removeEntitiesPromise = entities.map(async entityClass => { + return db.removeEntities(dbTx, entityClass, { blockNumber: MoreThan(argv.blockNumber) }); + }); + + await Promise.all(removeEntitiesPromise); + + if (syncStatus.latestIndexedBlockNumber > blockProgress.blockNumber) { + await indexer.updateSyncStatusIndexedBlock(blockProgress.blockHash, blockProgress.blockNumber, true); + } + + if (syncStatus.latestCanonicalBlockNumber > blockProgress.blockNumber) { + await indexer.updateSyncStatusCanonicalBlock(blockProgress.blockHash, blockProgress.blockNumber, true); + } + + if (hooksStatus.latestProcessedBlockNumber > blockProgress.blockNumber) { + await indexer.updateHookStatusProcessedBlock(blockProgress.blockNumber, true); + } + + dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + + log('Reset state successfully'); +}; diff --git a/packages/eden-watcher/src/cli/reset.ts b/packages/eden-watcher/src/cli/reset.ts new file mode 100644 index 00000000..2ddebf10 --- /dev/null +++ b/packages/eden-watcher/src/cli/reset.ts @@ -0,0 +1,24 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import 'reflect-metadata'; +import debug from 'debug'; + +import { getResetYargs } from '@vulcanize/util'; + +const log = debug('vulcanize:reset'); + +const main = async () => { + return getResetYargs() + .commandDir('reset-cmds', { extensions: ['ts', 'js'], exclude: /([a-zA-Z0-9\s_\\.\-:])+(.d.ts)$/ }) + .demandCommand(1) + .help() + .argv; +}; + +main().then(() => { + process.exit(); +}).catch(err => { + log(err); +}); diff --git a/packages/eden-watcher/src/cli/watch-contract.ts b/packages/eden-watcher/src/cli/watch-contract.ts new file mode 100644 index 00000000..8b80c586 --- /dev/null +++ b/packages/eden-watcher/src/cli/watch-contract.ts @@ -0,0 +1,79 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import path from 'path'; +import yargs from 'yargs'; +import 'reflect-metadata'; +import debug from 'debug'; + +import { Config, DEFAULT_CONFIG_PATH, getConfig, initClients } from '@vulcanize/util'; +import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node'; + +import { Database } from '../database'; +import { Indexer } from '../indexer'; + +const log = debug('vulcanize:watch-contract'); + +const main = async (): Promise => { + const argv = await yargs.parserConfiguration({ + 'parse-numbers': false + }).options({ + configFile: { + alias: 'f', + type: 'string', + require: true, + demandOption: true, + describe: 'Configuration file path (toml)', + default: DEFAULT_CONFIG_PATH + }, + address: { + type: 'string', + require: true, + demandOption: true, + describe: 'Address of the deployed contract' + }, + kind: { + type: 'string', + require: true, + demandOption: true, + describe: 'Kind of contract' + }, + checkpoint: { + type: 'boolean', + require: true, + demandOption: true, + describe: 'Turn checkpointing on' + }, + startingBlock: { + type: 'number', + describe: 'Starting block' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, postgraphileClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.init(); + + const graphDb = new GraphDatabase(config.database, path.resolve(__dirname, 'entity/*')); + await graphDb.init(); + + const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, config.server.subgraphPath); + + const indexer = new Indexer(config.server, db, ethClient, postgraphileClient, ethProvider, graphWatcher); + + graphWatcher.setIndexer(indexer); + await graphWatcher.init(); + + await indexer.watchContract(argv.address, argv.kind, argv.checkpoint, argv.startingBlock); + + await db.close(); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/eden-watcher/src/client.ts b/packages/eden-watcher/src/client.ts new file mode 100644 index 00000000..8946b4f9 --- /dev/null +++ b/packages/eden-watcher/src/client.ts @@ -0,0 +1,55 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { gql } from '@apollo/client/core'; +import { GraphQLClient, GraphQLConfig } from '@vulcanize/ipld-eth-client'; + +import { queries, mutations, subscriptions } from './gql'; + +export class Client { + _config: GraphQLConfig; + _client: GraphQLClient; + + constructor (config: GraphQLConfig) { + this._config = config; + + this._client = new GraphQLClient(config); + } + + async getEvents (blockHash: string, contractAddress: string, name: string): Promise { + const { events } = await this._client.query( + gql(queries.events), + { blockHash, contractAddress, name } + ); + + return events; + } + + async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise { + const { eventsInRange } = await this._client.query( + gql(queries.eventsInRange), + { fromBlockNumber, toBlockNumber } + ); + + return eventsInRange; + } + + async watchContract (contractAddress: string, startingBlock?: number): Promise { + const { watchContract } = await this._client.mutate( + gql(mutations.watchContract), + { contractAddress, startingBlock } + ); + + return watchContract; + } + + async watchEvents (onNext: (value: any) => void): Promise { + return this._client.subscribe( + gql(subscriptions.onEvent), + ({ data }) => { + onNext(data.onEvent); + } + ); + } +} diff --git a/packages/eden-watcher/src/database.ts b/packages/eden-watcher/src/database.ts new file mode 100644 index 00000000..887099bb --- /dev/null +++ b/packages/eden-watcher/src/database.ts @@ -0,0 +1,313 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import { Connection, ConnectionOptions, DeepPartial, FindConditions, QueryRunner, FindManyOptions, MoreThan } from 'typeorm'; +import path from 'path'; + +import { Database as BaseDatabase, MAX_REORG_DEPTH, DatabaseInterface } from '@vulcanize/util'; + +import { Contract } from './entity/Contract'; +import { Event } from './entity/Event'; +import { SyncStatus } from './entity/SyncStatus'; +import { HookStatus } from './entity/HookStatus'; +import { BlockProgress } from './entity/BlockProgress'; +import { IPLDBlock } from './entity/IPLDBlock'; + +export class Database implements DatabaseInterface { + _config: ConnectionOptions; + _conn!: Connection; + _baseDatabase: BaseDatabase; + + constructor (config: ConnectionOptions) { + assert(config); + + this._config = { + ...config, + entities: [path.join(__dirname, 'entity/*')] + }; + + this._baseDatabase = new BaseDatabase(this._config); + } + + async init (): Promise { + this._conn = await this._baseDatabase.init(); + } + + async close (): Promise { + return this._baseDatabase.close(); + } + + async getIPLDBlocks (where: FindConditions): Promise { + const repo = this._conn.getRepository(IPLDBlock); + return repo.find({ where, relations: ['block'] }); + } + + async getLatestIPLDBlock (contractAddress: string, kind: string | null, blockNumber?: number): Promise { + const repo = this._conn.getRepository(IPLDBlock); + + let queryBuilder = repo.createQueryBuilder('ipld_block') + .leftJoinAndSelect('ipld_block.block', 'block') + .where('block.is_pruned = false') + .andWhere('ipld_block.contract_address = :contractAddress', { contractAddress }) + .orderBy('block.block_number', 'DESC'); + + // Filter out blocks after the provided block number. + if (blockNumber) { + queryBuilder.andWhere('block.block_number <= :blockNumber', { blockNumber }); + } + + // Filter using kind if specified else order by id to give preference to checkpoint. + queryBuilder = kind + ? queryBuilder.andWhere('ipld_block.kind = :kind', { kind }) + : queryBuilder.andWhere('ipld_block.kind != :kind', { kind: 'diff_staged' }) + .addOrderBy('ipld_block.id', 'DESC'); + + return queryBuilder.getOne(); + } + + async getPrevIPLDBlock (queryRunner: QueryRunner, blockHash: string, contractAddress: string, kind?: string): Promise { + const heirerchicalQuery = ` + WITH RECURSIVE cte_query AS + ( + SELECT + b.block_hash, + b.block_number, + b.parent_hash, + 1 as depth, + i.id, + i.kind + FROM + block_progress b + LEFT JOIN + ipld_block i ON i.block_id = b.id + AND i.contract_address = $2 + WHERE + b.block_hash = $1 + UNION ALL + SELECT + b.block_hash, + b.block_number, + b.parent_hash, + c.depth + 1, + i.id, + i.kind + FROM + block_progress b + LEFT JOIN + ipld_block i + ON i.block_id = b.id + AND i.contract_address = $2 + INNER JOIN + cte_query c ON c.parent_hash = b.block_hash + WHERE + c.depth < $3 + ) + SELECT + block_number, id, kind + FROM + cte_query + ORDER BY block_number DESC, id DESC + `; + + // Fetching block and id for previous IPLDBlock in frothy region. + const queryResult = await queryRunner.query(heirerchicalQuery, [blockHash, contractAddress, MAX_REORG_DEPTH]); + const latestRequiredResult = kind + ? queryResult.find((obj: any) => obj.kind === kind) + : queryResult.find((obj: any) => obj.id); + + let result: IPLDBlock | undefined; + if (latestRequiredResult) { + result = await queryRunner.manager.findOne(IPLDBlock, { id: latestRequiredResult.id }, { relations: ['block'] }); + } else { + // If IPLDBlock not found in frothy region get latest IPLDBlock in the pruned region. + // Filter out IPLDBlocks from pruned blocks. + const canonicalBlockNumber = queryResult.pop().block_number + 1; + + let queryBuilder = queryRunner.manager.createQueryBuilder(IPLDBlock, 'ipld_block') + .leftJoinAndSelect('ipld_block.block', 'block') + .where('block.is_pruned = false') + .andWhere('ipld_block.contract_address = :contractAddress', { contractAddress }) + .andWhere('block.block_number <= :canonicalBlockNumber', { canonicalBlockNumber }) + .orderBy('block.block_number', 'DESC'); + + // Filter using kind if specified else order by id to give preference to checkpoint. + queryBuilder = kind + ? queryBuilder.andWhere('ipld_block.kind = :kind', { kind }) + : queryBuilder.addOrderBy('ipld_block.id', 'DESC'); + + result = await queryBuilder.getOne(); + } + + return result; + } + + // Fetch all diff IPLDBlocks after the specified checkpoint. + async getDiffIPLDBlocksByCheckpoint (contractAddress: string, checkpointBlockNumber: number): Promise { + const repo = this._conn.getRepository(IPLDBlock); + + return repo.find({ + relations: ['block'], + where: { + contractAddress, + kind: 'diff', + block: { + isPruned: false, + blockNumber: MoreThan(checkpointBlockNumber) + } + }, + order: { + block: 'ASC' + } + }); + } + + async saveOrUpdateIPLDBlock (ipldBlock: IPLDBlock): Promise { + const repo = this._conn.getRepository(IPLDBlock); + return repo.save(ipldBlock); + } + + async getHookStatus (queryRunner: QueryRunner): Promise { + const repo = queryRunner.manager.getRepository(HookStatus); + + return repo.findOne(); + } + + async updateHookStatusProcessedBlock (queryRunner: QueryRunner, blockNumber: number, force?: boolean): Promise { + const repo = queryRunner.manager.getRepository(HookStatus); + let entity = await repo.findOne(); + + if (!entity) { + entity = repo.create({ + latestProcessedBlockNumber: blockNumber + }); + } + + if (force || blockNumber > entity.latestProcessedBlockNumber) { + entity.latestProcessedBlockNumber = blockNumber; + } + + return repo.save(entity); + } + + async getContracts (where: FindConditions): Promise { + const repo = this._conn.getRepository(Contract); + return repo.find({ where }); + } + + async getContract (address: string): Promise { + const repo = this._conn.getRepository(Contract); + + return this._baseDatabase.getContract(repo, address); + } + + async createTransactionRunner (): Promise { + return this._baseDatabase.createTransactionRunner(); + } + + async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> { + const repo = this._conn.getRepository(BlockProgress); + + return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber); + } + + async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise> { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber); + } + + async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise { + const repo = queryRunner.manager.getRepository(Event); + return this._baseDatabase.saveEventEntity(repo, entity); + } + + async getBlockEvents (blockHash: string, where: FindConditions): Promise { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getBlockEvents(repo, blockHash, where); + } + + async saveEvents (queryRunner: QueryRunner, block: DeepPartial, events: DeepPartial[]): Promise { + const blockRepo = queryRunner.manager.getRepository(BlockProgress); + const eventRepo = queryRunner.manager.getRepository(Event); + + return this._baseDatabase.saveEvents(blockRepo, eventRepo, block, events); + } + + async saveContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise { + await this._conn.transaction(async (tx) => { + const repo = tx.getRepository(Contract); + + return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock); + }); + } + + async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusIndexedBlock(repo, blockHash, blockNumber, force); + } + + async updateSyncStatusCanonicalBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusCanonicalBlock(repo, blockHash, blockNumber, force); + } + + async updateSyncStatusChainHead (queryRunner: QueryRunner, blockHash: string, blockNumber: number): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber); + } + + async getSyncStatus (queryRunner: QueryRunner): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.getSyncStatus(repo); + } + + async getEvent (id: string): Promise { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getEvent(repo, id); + } + + async getBlocksAtHeight (height: number, isPruned: boolean): Promise { + const repo = this._conn.getRepository(BlockProgress); + + return this._baseDatabase.getBlocksAtHeight(repo, height, isPruned); + } + + async markBlocksAsPruned (queryRunner: QueryRunner, blocks: BlockProgress[]): Promise { + const repo = queryRunner.manager.getRepository(BlockProgress); + + return this._baseDatabase.markBlocksAsPruned(repo, blocks); + } + + async getBlockProgress (blockHash: string): Promise { + const repo = this._conn.getRepository(BlockProgress); + return this._baseDatabase.getBlockProgress(repo, blockHash); + } + + async updateBlockProgress (queryRunner: QueryRunner, blockHash: string, lastProcessedEventIndex: number): Promise { + const repo = queryRunner.manager.getRepository(BlockProgress); + + return this._baseDatabase.updateBlockProgress(repo, blockHash, lastProcessedEventIndex); + } + + async removeEntities (queryRunner: QueryRunner, entity: new () => Entity, findConditions?: FindManyOptions | FindConditions): Promise { + return this._baseDatabase.removeEntities(queryRunner, entity, findConditions); + } + + async getAncestorAtDepth (blockHash: string, depth: number): Promise { + return this._baseDatabase.getAncestorAtDepth(blockHash, depth); + } + + _getPropertyColumnMapForEntity (entityName: string): Map { + return this._conn.getMetadata(entityName).ownColumns.reduce((acc, curr) => { + return acc.set(curr.propertyName, curr.databaseName); + }, new Map()); + } +} diff --git a/packages/eden-watcher/src/entity/Account.ts b/packages/eden-watcher/src/entity/Account.ts new file mode 100644 index 00000000..9da06987 --- /dev/null +++ b/packages/eden-watcher/src/entity/Account.ts @@ -0,0 +1,32 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Claim } from './Claim'; +import { Slash } from './Slash'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Account { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('bigint', { transformer: bigintTransformer }) + totalClaimed!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + totalSlashed!: bigint; + + @ManyToOne(() => Claim) + claims!: Claim; + + @ManyToOne(() => Slash) + slashes!: Slash; +} diff --git a/packages/eden-watcher/src/entity/Block.ts b/packages/eden-watcher/src/entity/Block.ts new file mode 100644 index 00000000..d8c0400e --- /dev/null +++ b/packages/eden-watcher/src/entity/Block.ts @@ -0,0 +1,63 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column } from 'typeorm'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Block { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('boolean') + fromActiveProducer!: boolean; + + @Column('varchar') + hash!: string; + + @Column('varchar') + parentHash!: string; + + @Column('varchar') + unclesHash!: string; + + @Column('varchar') + author!: string; + + @Column('varchar') + stateRoot!: string; + + @Column('varchar') + transactionsRoot!: string; + + @Column('varchar') + receiptsRoot!: string; + + @Column('bigint', { transformer: bigintTransformer }) + number!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + gasUsed!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + gasLimit!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + timestamp!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + difficulty!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + totalDifficulty!: bigint; + + @Column('bigint', { nullable: true, transformer: bigintTransformer }) + size!: bigint; +} diff --git a/packages/eden-watcher/src/entity/BlockProgress.ts b/packages/eden-watcher/src/entity/BlockProgress.ts new file mode 100644 index 00000000..9fe65afd --- /dev/null +++ b/packages/eden-watcher/src/entity/BlockProgress.ts @@ -0,0 +1,45 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; +import { BlockProgressInterface } from '@vulcanize/util'; + +@Entity() +@Index(['blockHash'], { unique: true }) +@Index(['blockNumber']) +@Index(['parentHash']) +export class BlockProgress implements BlockProgressInterface { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar') + cid!: string; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('varchar', { length: 66 }) + parentHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('integer') + blockTimestamp!: number; + + @Column('integer') + numEvents!: number; + + @Column('integer') + numProcessedEvents!: number; + + @Column('integer') + lastProcessedEventIndex!: number; + + @Column('boolean') + isComplete!: boolean; + + @Column('boolean', { default: false }) + isPruned!: boolean; +} diff --git a/packages/eden-watcher/src/entity/Claim.ts b/packages/eden-watcher/src/entity/Claim.ts new file mode 100644 index 00000000..53ab3a5a --- /dev/null +++ b/packages/eden-watcher/src/entity/Claim.ts @@ -0,0 +1,34 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Account } from './Account'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Claim { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('bigint', { transformer: bigintTransformer }) + timestamp!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + index!: bigint; + + @ManyToOne(() => Account) + account!: Account; + + @Column('bigint', { transformer: bigintTransformer }) + totalEarned!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + claimed!: bigint; +} diff --git a/packages/eden-watcher/src/entity/Contract.ts b/packages/eden-watcher/src/entity/Contract.ts new file mode 100644 index 00000000..0727c538 --- /dev/null +++ b/packages/eden-watcher/src/entity/Contract.ts @@ -0,0 +1,24 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; + +@Entity() +@Index(['address'], { unique: true }) +export class Contract { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 42 }) + address!: string; + + @Column('varchar') + kind!: string; + + @Column('boolean') + checkpoint!: boolean; + + @Column('integer') + startingBlock!: number; +} diff --git a/packages/eden-watcher/src/entity/Distribution.ts b/packages/eden-watcher/src/entity/Distribution.ts new file mode 100644 index 00000000..f817e39d --- /dev/null +++ b/packages/eden-watcher/src/entity/Distribution.ts @@ -0,0 +1,34 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Distributor } from './Distributor'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Distribution { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @ManyToOne(() => Distributor) + distributor!: Distributor; + + @Column('bigint', { transformer: bigintTransformer }) + timestamp!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + distributionNumber!: bigint; + + @Column('varchar') + merkleRoot!: string; + + @Column('varchar') + metadataURI!: string; +} diff --git a/packages/eden-watcher/src/entity/Distributor.ts b/packages/eden-watcher/src/entity/Distributor.ts new file mode 100644 index 00000000..f243f556 --- /dev/null +++ b/packages/eden-watcher/src/entity/Distributor.ts @@ -0,0 +1,21 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Distribution } from './Distribution'; + +@Entity() +export class Distributor { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @ManyToOne(() => Distribution, { nullable: true }) + currentDistribution!: Distribution; +} diff --git a/packages/eden-watcher/src/entity/Epoch.ts b/packages/eden-watcher/src/entity/Epoch.ts new file mode 100644 index 00000000..decf58de --- /dev/null +++ b/packages/eden-watcher/src/entity/Epoch.ts @@ -0,0 +1,44 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Block } from './Block'; +import { ProducerEpoch } from './ProducerEpoch'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Epoch { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('boolean') + finalized!: boolean; + + @Column('bigint', { transformer: bigintTransformer }) + epochNumber!: bigint; + + @ManyToOne(() => Block, { nullable: true }) + startBlock!: Block; + + @ManyToOne(() => Block, { nullable: true }) + endBlock!: Block; + + @Column('bigint', { transformer: bigintTransformer }) + producerBlocks!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + allBlocks!: bigint; + + @Column('varchar') + producerBlocksRatio!: string; + + @ManyToOne(() => ProducerEpoch) + producerRewards!: ProducerEpoch; +} diff --git a/packages/eden-watcher/src/entity/Event.ts b/packages/eden-watcher/src/entity/Event.ts new file mode 100644 index 00000000..c7c09d6b --- /dev/null +++ b/packages/eden-watcher/src/entity/Event.ts @@ -0,0 +1,38 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm'; +import { BlockProgress } from './BlockProgress'; + +@Entity() +@Index(['block', 'contract']) +@Index(['block', 'contract', 'eventName']) +export class Event { + @PrimaryGeneratedColumn() + id!: number; + + @ManyToOne(() => BlockProgress, { onDelete: 'CASCADE' }) + block!: BlockProgress; + + @Column('varchar', { length: 66 }) + txHash!: string; + + @Column('integer') + index!: number; + + @Column('varchar', { length: 42 }) + contract!: string; + + @Column('varchar', { length: 256 }) + eventName!: string; + + @Column('text') + eventInfo!: string; + + @Column('text') + extraInfo!: string; + + @Column('text') + proof!: string; +} diff --git a/packages/eden-watcher/src/entity/HookStatus.ts b/packages/eden-watcher/src/entity/HookStatus.ts new file mode 100644 index 00000000..7e67d2bb --- /dev/null +++ b/packages/eden-watcher/src/entity/HookStatus.ts @@ -0,0 +1,14 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity() +export class HookStatus { + @PrimaryGeneratedColumn() + id!: number; + + @Column('integer') + latestProcessedBlockNumber!: number; +} diff --git a/packages/eden-watcher/src/entity/IPLDBlock.ts b/packages/eden-watcher/src/entity/IPLDBlock.ts new file mode 100644 index 00000000..60a1c5ec --- /dev/null +++ b/packages/eden-watcher/src/entity/IPLDBlock.ts @@ -0,0 +1,30 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm'; +import { BlockProgress } from './BlockProgress'; + +@Entity() +@Index(['cid'], { unique: true }) +@Index(['block', 'contractAddress']) +@Index(['block', 'contractAddress', 'kind'], { unique: true }) +export class IPLDBlock { + @PrimaryGeneratedColumn() + id!: number; + + @ManyToOne(() => BlockProgress, { onDelete: 'CASCADE' }) + block!: BlockProgress; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar') + cid!: string; + + @Column('varchar') + kind!: string; + + @Column('bytea') + data!: Buffer; +} diff --git a/packages/eden-watcher/src/entity/Network.ts b/packages/eden-watcher/src/entity/Network.ts new file mode 100644 index 00000000..7964de6c --- /dev/null +++ b/packages/eden-watcher/src/entity/Network.ts @@ -0,0 +1,41 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Slot } from './Slot'; +import { Staker } from './Staker'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Network { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @ManyToOne(() => Slot, { nullable: true }) + slot0!: Slot; + + @ManyToOne(() => Slot, { nullable: true }) + slot1!: Slot; + + @ManyToOne(() => Slot, { nullable: true }) + slot2!: Slot; + + @ManyToOne(() => Staker) + stakers!: Staker; + + @Column('bigint', { nullable: true, transformer: bigintTransformer }) + numStakers!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + totalStaked!: bigint; + + @Column('bigint', { array: true }) + stakedPercentiles!: bigint[]; +} diff --git a/packages/eden-watcher/src/entity/Producer.ts b/packages/eden-watcher/src/entity/Producer.ts new file mode 100644 index 00000000..e47472fe --- /dev/null +++ b/packages/eden-watcher/src/entity/Producer.ts @@ -0,0 +1,33 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column } from 'typeorm'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Producer { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('boolean') + active!: boolean; + + @Column('varchar', { nullable: true }) + rewardCollector!: string; + + @Column('bigint', { transformer: bigintTransformer }) + rewards!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + confirmedBlocks!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + pendingEpochBlocks!: bigint; +} diff --git a/packages/eden-watcher/src/entity/ProducerEpoch.ts b/packages/eden-watcher/src/entity/ProducerEpoch.ts new file mode 100644 index 00000000..1dfe983a --- /dev/null +++ b/packages/eden-watcher/src/entity/ProducerEpoch.ts @@ -0,0 +1,34 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Epoch } from './Epoch'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class ProducerEpoch { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar') + address!: string; + + @ManyToOne(() => Epoch) + epoch!: Epoch; + + @Column('bigint', { transformer: bigintTransformer }) + totalRewards!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + blocksProduced!: bigint; + + @Column('varchar') + blocksProducedRatio!: string; +} diff --git a/packages/eden-watcher/src/entity/ProducerRewardCollectorChange.ts b/packages/eden-watcher/src/entity/ProducerRewardCollectorChange.ts new file mode 100644 index 00000000..37ae2760 --- /dev/null +++ b/packages/eden-watcher/src/entity/ProducerRewardCollectorChange.ts @@ -0,0 +1,23 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column } from 'typeorm'; + +@Entity() +export class ProducerRewardCollectorChange { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar') + producer!: string; + + @Column('varchar') + rewardCollector!: string; +} diff --git a/packages/eden-watcher/src/entity/ProducerSet.ts b/packages/eden-watcher/src/entity/ProducerSet.ts new file mode 100644 index 00000000..481aa006 --- /dev/null +++ b/packages/eden-watcher/src/entity/ProducerSet.ts @@ -0,0 +1,21 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Producer } from './Producer'; + +@Entity() +export class ProducerSet { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @ManyToOne(() => Producer) + producers!: Producer; +} diff --git a/packages/eden-watcher/src/entity/ProducerSetChange.ts b/packages/eden-watcher/src/entity/ProducerSetChange.ts new file mode 100644 index 00000000..bac451b7 --- /dev/null +++ b/packages/eden-watcher/src/entity/ProducerSetChange.ts @@ -0,0 +1,28 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column } from 'typeorm'; + +enum ProducerSetChangeType { + Added, + Removed +} + +@Entity() +export class ProducerSetChange { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar') + producer!: string; + + @Column('integer') + changeType!: ProducerSetChangeType; +} diff --git a/packages/eden-watcher/src/entity/RewardSchedule.ts b/packages/eden-watcher/src/entity/RewardSchedule.ts new file mode 100644 index 00000000..135f2b84 --- /dev/null +++ b/packages/eden-watcher/src/entity/RewardSchedule.ts @@ -0,0 +1,31 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { RewardScheduleEntry } from './RewardScheduleEntry'; +import { Epoch } from './Epoch'; + +@Entity() +export class RewardSchedule { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @ManyToOne(() => RewardScheduleEntry) + rewardScheduleEntries!: RewardScheduleEntry; + + @ManyToOne(() => Epoch, { nullable: true }) + lastEpoch!: Epoch; + + @ManyToOne(() => Epoch, { nullable: true }) + pendingEpoch!: Epoch; + + @ManyToOne(() => RewardScheduleEntry, { nullable: true }) + activeRewardScheduleEntry!: RewardScheduleEntry; +} diff --git a/packages/eden-watcher/src/entity/RewardScheduleEntry.ts b/packages/eden-watcher/src/entity/RewardScheduleEntry.ts new file mode 100644 index 00000000..6754d26b --- /dev/null +++ b/packages/eden-watcher/src/entity/RewardScheduleEntry.ts @@ -0,0 +1,27 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column } from 'typeorm'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class RewardScheduleEntry { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('bigint', { transformer: bigintTransformer }) + startTime!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + epochDuration!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + rewardsPerEpoch!: bigint; +} diff --git a/packages/eden-watcher/src/entity/Slash.ts b/packages/eden-watcher/src/entity/Slash.ts new file mode 100644 index 00000000..5589409b --- /dev/null +++ b/packages/eden-watcher/src/entity/Slash.ts @@ -0,0 +1,28 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Account } from './Account'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Slash { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('bigint', { transformer: bigintTransformer }) + timestamp!: bigint; + + @ManyToOne(() => Account) + account!: Account; + + @Column('bigint', { transformer: bigintTransformer }) + slashed!: bigint; +} diff --git a/packages/eden-watcher/src/entity/Slot.ts b/packages/eden-watcher/src/entity/Slot.ts new file mode 100644 index 00000000..862add69 --- /dev/null +++ b/packages/eden-watcher/src/entity/Slot.ts @@ -0,0 +1,43 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { SlotClaim } from './SlotClaim'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Slot { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar') + owner!: string; + + @Column('varchar') + delegate!: string; + + @Column('bigint', { transformer: bigintTransformer }) + winningBid!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + oldBid!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + startTime!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + expirationTime!: bigint; + + @Column('varchar') + taxRatePerDay!: string; + + @ManyToOne(() => SlotClaim) + claims!: SlotClaim; +} diff --git a/packages/eden-watcher/src/entity/SlotClaim.ts b/packages/eden-watcher/src/entity/SlotClaim.ts new file mode 100644 index 00000000..c8804b03 --- /dev/null +++ b/packages/eden-watcher/src/entity/SlotClaim.ts @@ -0,0 +1,40 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; +import { Slot } from './Slot'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class SlotClaim { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @ManyToOne(() => Slot) + slot!: Slot; + + @Column('varchar') + owner!: string; + + @Column('bigint', { transformer: bigintTransformer }) + winningBid!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + oldBid!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + startTime!: bigint; + + @Column('bigint', { transformer: bigintTransformer }) + expirationTime!: bigint; + + @Column('varchar') + taxRatePerDay!: string; +} diff --git a/packages/eden-watcher/src/entity/Staker.ts b/packages/eden-watcher/src/entity/Staker.ts new file mode 100644 index 00000000..375f976e --- /dev/null +++ b/packages/eden-watcher/src/entity/Staker.ts @@ -0,0 +1,24 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryColumn, Column } from 'typeorm'; +import { bigintTransformer } from '@vulcanize/util'; + +@Entity() +export class Staker { + @PrimaryColumn('varchar') + id!: string; + + @PrimaryColumn('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('bigint', { transformer: bigintTransformer }) + staked!: bigint; + + @Column('bigint', { nullable: true, transformer: bigintTransformer }) + rank!: bigint; +} diff --git a/packages/eden-watcher/src/entity/SyncStatus.ts b/packages/eden-watcher/src/entity/SyncStatus.ts new file mode 100644 index 00000000..74983ed5 --- /dev/null +++ b/packages/eden-watcher/src/entity/SyncStatus.ts @@ -0,0 +1,30 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { SyncStatusInterface } from '@vulcanize/util'; + +@Entity() +export class SyncStatus implements SyncStatusInterface { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + chainHeadBlockHash!: string; + + @Column('integer') + chainHeadBlockNumber!: number; + + @Column('varchar', { length: 66 }) + latestIndexedBlockHash!: string; + + @Column('integer') + latestIndexedBlockNumber!: number; + + @Column('varchar', { length: 66 }) + latestCanonicalBlockHash!: string; + + @Column('integer') + latestCanonicalBlockNumber!: number; +} diff --git a/packages/eden-watcher/src/events.ts b/packages/eden-watcher/src/events.ts new file mode 100644 index 00000000..700aee60 --- /dev/null +++ b/packages/eden-watcher/src/events.ts @@ -0,0 +1,180 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import debug from 'debug'; +import { PubSub } from 'apollo-server-express'; + +import { EthClient } from '@vulcanize/ipld-eth-client'; +import { + JobQueue, + EventWatcher as BaseEventWatcher, + EventWatcherInterface, + QUEUE_BLOCK_PROCESSING, + QUEUE_EVENT_PROCESSING, + QUEUE_BLOCK_CHECKPOINT, + QUEUE_HOOKS, + QUEUE_IPFS, + UNKNOWN_EVENT_NAME, + UpstreamConfig, + JOB_KIND_PRUNE +} from '@vulcanize/util'; + +import { Indexer } from './indexer'; +import { Event } from './entity/Event'; + +const EVENT = 'event'; + +const log = debug('vulcanize:events'); + +export class EventWatcher implements EventWatcherInterface { + _ethClient: EthClient + _indexer: Indexer + _subscription: ZenObservable.Subscription | undefined + _baseEventWatcher: BaseEventWatcher + _pubsub: PubSub + _jobQueue: JobQueue + + constructor (upstreamConfig: UpstreamConfig, ethClient: EthClient, postgraphileClient: EthClient, indexer: Indexer, pubsub: PubSub, jobQueue: JobQueue) { + assert(ethClient); + assert(indexer); + + this._ethClient = ethClient; + this._indexer = indexer; + this._pubsub = pubsub; + this._jobQueue = jobQueue; + this._baseEventWatcher = new BaseEventWatcher(upstreamConfig, this._ethClient, postgraphileClient, this._indexer, this._pubsub, this._jobQueue); + } + + getEventIterator (): AsyncIterator { + return this._pubsub.asyncIterator([EVENT]); + } + + getBlockProgressEventIterator (): AsyncIterator { + return this._baseEventWatcher.getBlockProgressEventIterator(); + } + + async start (): Promise { + assert(!this._subscription, 'subscription already started'); + + await this.initBlockProcessingOnCompleteHandler(); + await this.initEventProcessingOnCompleteHandler(); + await this.initHooksOnCompleteHandler(); + await this.initBlockCheckpointOnCompleteHandler(); + this._baseEventWatcher.startBlockProcessing(); + } + + async stop (): Promise { + this._baseEventWatcher.stop(); + } + + async initBlockProcessingOnCompleteHandler (): Promise { + this._jobQueue.onComplete(QUEUE_BLOCK_PROCESSING, async (job) => { + const { id, data: { failed, request: { data: { kind } } } } = job; + + if (failed) { + log(`Job ${id} for queue ${QUEUE_BLOCK_PROCESSING} failed`); + return; + } + + await this._baseEventWatcher.blockProcessingCompleteHandler(job); + + await this.createHooksJob(kind); + }); + } + + async initEventProcessingOnCompleteHandler (): Promise { + await this._jobQueue.onComplete(QUEUE_EVENT_PROCESSING, async (job) => { + const { id, data: { request, failed, state, createdOn } } = job; + + if (failed) { + log(`Job ${id} for queue ${QUEUE_EVENT_PROCESSING} failed`); + return; + } + + const dbEvent = await this._baseEventWatcher.eventProcessingCompleteHandler(job); + + const timeElapsedInSeconds = (Date.now() - Date.parse(createdOn)) / 1000; + log(`Job onComplete event ${request.data.id} publish ${!!request.data.publish}`); + if (!failed && state === 'completed' && request.data.publish) { + // Check for max acceptable lag time between request and sending results to live subscribers. + if (timeElapsedInSeconds <= this._jobQueue.maxCompletionLag) { + await this.publishEventToSubscribers(dbEvent, timeElapsedInSeconds); + } else { + log(`event ${request.data.id} is too old (${timeElapsedInSeconds}s), not broadcasting to live subscribers`); + } + } + }); + } + + async initHooksOnCompleteHandler (): Promise { + this._jobQueue.onComplete(QUEUE_HOOKS, async (job) => { + const { data: { request: { data: { blockNumber, blockHash } } } } = job; + + await this._indexer.updateHookStatusProcessedBlock(blockNumber); + + // Create a checkpoint job after completion of a hook job. + await this.createCheckpointJob(blockHash, blockNumber); + }); + } + + async initBlockCheckpointOnCompleteHandler (): Promise { + this._jobQueue.onComplete(QUEUE_BLOCK_CHECKPOINT, async (job) => { + const { data: { request: { data: { blockHash } } } } = job; + + if (this._indexer.isIPFSConfigured()) { + await this.createIPFSPutJob(blockHash); + } + }); + } + + async publishEventToSubscribers (dbEvent: Event, timeElapsedInSeconds: number): Promise { + if (dbEvent && dbEvent.eventName !== UNKNOWN_EVENT_NAME) { + const resultEvent = this._indexer.getResultEvent(dbEvent); + + log(`pushing event to GQL subscribers (${timeElapsedInSeconds}s elapsed): ${resultEvent.event.__typename}`); + + // Publishing the event here will result in pushing the payload to GQL subscribers for `onEvent`. + await this._pubsub.publish(EVENT, { + onEvent: resultEvent + }); + } + } + + async createHooksJob (kind: string): Promise { + // If it's a pruning job: Create a hook job for the latest canonical block. + if (kind === JOB_KIND_PRUNE) { + const latestCanonicalBlock = await this._indexer.getLatestCanonicalBlock(); + assert(latestCanonicalBlock); + + await this._jobQueue.pushJob( + QUEUE_HOOKS, + { + blockHash: latestCanonicalBlock.blockHash, + blockNumber: latestCanonicalBlock.blockNumber + } + ); + } + } + + async createCheckpointJob (blockHash: string, blockNumber: number): Promise { + await this._jobQueue.pushJob( + QUEUE_BLOCK_CHECKPOINT, + { + blockHash, + blockNumber + } + ); + } + + async createIPFSPutJob (blockHash: string): Promise { + const ipldBlocks = await this._indexer.getIPLDBlocksByHash(blockHash); + + for (const ipldBlock of ipldBlocks) { + const data = this._indexer.getIPLDData(ipldBlock); + + await this._jobQueue.pushJob(QUEUE_IPFS, { data }); + } + } +} diff --git a/packages/eden-watcher/src/fill.ts b/packages/eden-watcher/src/fill.ts new file mode 100644 index 00000000..0c3a5408 --- /dev/null +++ b/packages/eden-watcher/src/fill.ts @@ -0,0 +1,82 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import path from 'path'; +import assert from 'assert'; +import 'reflect-metadata'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import debug from 'debug'; +import { PubSub } from 'apollo-server-express'; + +import { Config, getConfig, fillBlocks, JobQueue, DEFAULT_CONFIG_PATH, initClients } from '@vulcanize/util'; +import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node'; + +import { Database } from './database'; +import { Indexer } from './indexer'; +import { EventWatcher } from './events'; + +const log = debug('vulcanize:server'); + +export const main = async (): Promise => { + const argv = await yargs(hideBin(process.argv)).parserConfiguration({ + 'parse-numbers': false + }).options({ + configFile: { + alias: 'f', + type: 'string', + demandOption: true, + describe: 'configuration file path (toml)', + default: DEFAULT_CONFIG_PATH + }, + startBlock: { + type: 'number', + demandOption: true, + describe: 'Block number to start processing at' + }, + endBlock: { + type: 'number', + demandOption: true, + describe: 'Block number to stop processing at' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, postgraphileClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.init(); + + const graphDb = new GraphDatabase(config.database, path.resolve(__dirname, 'entity/*')); + await graphDb.init(); + + const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, config.server.subgraphPath); + + // Note: In-memory pubsub works fine for now, as each watcher is a single process anyway. + // Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries + const pubsub = new PubSub(); + const indexer = new Indexer(config.server, db, ethClient, postgraphileClient, ethProvider, graphWatcher); + + graphWatcher.setIndexer(indexer); + await graphWatcher.init(); + + const jobQueueConfig = config.jobQueue; + assert(jobQueueConfig, 'Missing job queue config'); + + const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig; + assert(dbConnectionString, 'Missing job queue db connection string'); + + const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs }); + await jobQueue.start(); + + const eventWatcher = new EventWatcher(config.upstream, ethClient, postgraphileClient, indexer, pubsub, jobQueue); + + await fillBlocks(jobQueue, indexer, postgraphileClient, eventWatcher, config.upstream.ethServer.blockDelayInMilliSecs, argv); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(); +}); diff --git a/packages/eden-watcher/src/gql/index.ts b/packages/eden-watcher/src/gql/index.ts new file mode 100644 index 00000000..4732f682 --- /dev/null +++ b/packages/eden-watcher/src/gql/index.ts @@ -0,0 +1,3 @@ +export * as mutations from './mutations'; +export * as queries from './queries'; +export * as subscriptions from './subscriptions'; diff --git a/packages/eden-watcher/src/gql/mutations/index.ts b/packages/eden-watcher/src/gql/mutations/index.ts new file mode 100644 index 00000000..0c3bd853 --- /dev/null +++ b/packages/eden-watcher/src/gql/mutations/index.ts @@ -0,0 +1,4 @@ +import fs from 'fs'; +import path from 'path'; + +export const watchContract = fs.readFileSync(path.join(__dirname, 'watchContract.gql'), 'utf8'); diff --git a/packages/eden-watcher/src/gql/mutations/watchContract.gql b/packages/eden-watcher/src/gql/mutations/watchContract.gql new file mode 100644 index 00000000..ef65c717 --- /dev/null +++ b/packages/eden-watcher/src/gql/mutations/watchContract.gql @@ -0,0 +1,3 @@ +mutation watchContract($address: String!, $kind: String!, $checkpoint: Boolean!, $startingBlock: Int){ + watchContract(address: $address, kind: $kind, checkpoint: $checkpoint, startingBlock: $startingBlock) +} diff --git a/packages/eden-watcher/src/gql/queries/account.gql b/packages/eden-watcher/src/gql/queries/account.gql new file mode 100644 index 00000000..dbe2ec78 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/account.gql @@ -0,0 +1,29 @@ +query account($id: String!, $blockHash: String!){ + account(id: $id, blockHash: $blockHash){ + id + totalClaimed + totalSlashed + claims{ + id + timestamp + index + account{ + id + totalClaimed + totalSlashed + slashes{ + id + timestamp + account{ + id + totalClaimed + totalSlashed + } + slashed + } + } + totalEarned + claimed + } + } +} diff --git a/packages/eden-watcher/src/gql/queries/claim.gql b/packages/eden-watcher/src/gql/queries/claim.gql new file mode 100644 index 00000000..b21112a4 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/claim.gql @@ -0,0 +1,36 @@ +query claim($id: String!, $blockHash: String!){ + claim(id: $id, blockHash: $blockHash){ + id + timestamp + index + account{ + id + totalClaimed + totalSlashed + claims{ + id + timestamp + index + account{ + id + totalClaimed + totalSlashed + slashes{ + id + timestamp + account{ + id + totalClaimed + totalSlashed + } + slashed + } + } + totalEarned + claimed + } + } + totalEarned + claimed + } +} diff --git a/packages/eden-watcher/src/gql/queries/distribution.gql b/packages/eden-watcher/src/gql/queries/distribution.gql new file mode 100644 index 00000000..dbb490cf --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/distribution.gql @@ -0,0 +1,22 @@ +query distribution($id: String!, $blockHash: String!){ + distribution(id: $id, blockHash: $blockHash){ + id + distributor{ + id + currentDistribution{ + id + distributor{ + id + } + timestamp + distributionNumber + merkleRoot + metadataURI + } + } + timestamp + distributionNumber + merkleRoot + metadataURI + } +} diff --git a/packages/eden-watcher/src/gql/queries/distributor.gql b/packages/eden-watcher/src/gql/queries/distributor.gql new file mode 100644 index 00000000..a0a991eb --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/distributor.gql @@ -0,0 +1,15 @@ +query distributor($id: String!, $blockHash: String!){ + distributor(id: $id, blockHash: $blockHash){ + id + currentDistribution{ + id + distributor{ + id + } + timestamp + distributionNumber + merkleRoot + metadataURI + } + } +} diff --git a/packages/eden-watcher/src/gql/queries/epoch.gql b/packages/eden-watcher/src/gql/queries/epoch.gql new file mode 100644 index 00000000..adcbf7e8 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/epoch.gql @@ -0,0 +1,39 @@ +query epoch($id: String!, $blockHash: String!){ + epoch(id: $id, blockHash: $blockHash){ + id + finalized + epochNumber + startBlock{ + cid + hash + number + timestamp + parentHash + } + endBlock{ + cid + hash + number + timestamp + parentHash + } + producerBlocks + allBlocks + producerBlocksRatio + producerRewards{ + id + address + epoch{ + id + finalized + epochNumber + producerBlocks + allBlocks + producerBlocksRatio + } + totalRewards + blocksProduced + blocksProducedRatio + } + } +} diff --git a/packages/eden-watcher/src/gql/queries/events.gql b/packages/eden-watcher/src/gql/queries/events.gql new file mode 100644 index 00000000..75e2ad66 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/events.gql @@ -0,0 +1,75 @@ +query events($blockHash: String!, $contractAddress: String!, $name: String){ + events(blockHash: $blockHash, contractAddress: $contractAddress, name: $name){ + block{ + cid + hash + number + timestamp + parentHash + } + tx{ + hash + index + from + to + } + contract + eventIndex + event{ + ... on TransferEvent { + from + to + value + } + ... on ApprovalEvent { + owner + spender + value + } + ... on AuthorizationUsedEvent { + authorizer + nonce + } + ... on AdminUpdatedEvent { + newAdmin + oldAdmin + } + ... on TaxRateUpdatedEvent { + newNumerator + newDenominator + oldNumerator + oldDenominator + } + ... on SlotClaimedEvent { + slot + owner + delegate + newBidAmount + oldBidAmount + taxNumerator + taxDenominator + } + ... on SlotDelegateUpdatedEvent { + slot + owner + newDelegate + oldDelegate + } + ... on StakeEvent { + staker + stakeAmount + } + ... on UnstakeEvent { + staker + unstakedAmount + } + ... on WithdrawEvent { + withdrawer + withdrawalAmount + } + } + proof{ + data + } + } +} diff --git a/packages/eden-watcher/src/gql/queries/eventsInRange.gql b/packages/eden-watcher/src/gql/queries/eventsInRange.gql new file mode 100644 index 00000000..25495340 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/eventsInRange.gql @@ -0,0 +1,75 @@ +query eventsInRange($fromBlockNumber: Int!, $toBlockNumber: Int!){ + eventsInRange(fromBlockNumber: $fromBlockNumber, toBlockNumber: $toBlockNumber){ + block{ + cid + hash + number + timestamp + parentHash + } + tx{ + hash + index + from + to + } + contract + eventIndex + event{ + ... on TransferEvent { + from + to + value + } + ... on ApprovalEvent { + owner + spender + value + } + ... on AuthorizationUsedEvent { + authorizer + nonce + } + ... on AdminUpdatedEvent { + newAdmin + oldAdmin + } + ... on TaxRateUpdatedEvent { + newNumerator + newDenominator + oldNumerator + oldDenominator + } + ... on SlotClaimedEvent { + slot + owner + delegate + newBidAmount + oldBidAmount + taxNumerator + taxDenominator + } + ... on SlotDelegateUpdatedEvent { + slot + owner + newDelegate + oldDelegate + } + ... on StakeEvent { + staker + stakeAmount + } + ... on UnstakeEvent { + staker + unstakedAmount + } + ... on WithdrawEvent { + withdrawer + withdrawalAmount + } + } + proof{ + data + } + } +} diff --git a/packages/eden-watcher/src/gql/queries/getState.gql b/packages/eden-watcher/src/gql/queries/getState.gql new file mode 100644 index 00000000..7fded350 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/getState.gql @@ -0,0 +1,15 @@ +query getState($blockHash: String!, $contractAddress: String!, $kind: String){ + getState(blockHash: $blockHash, contractAddress: $contractAddress, kind: $kind){ + block{ + cid + hash + number + timestamp + parentHash + } + contractAddress + cid + kind + data + } +} diff --git a/packages/eden-watcher/src/gql/queries/getStateByCID.gql b/packages/eden-watcher/src/gql/queries/getStateByCID.gql new file mode 100644 index 00000000..8e57fea1 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/getStateByCID.gql @@ -0,0 +1,15 @@ +query getStateByCID($cid: String!){ + getStateByCID(cid: $cid){ + block{ + cid + hash + number + timestamp + parentHash + } + contractAddress + cid + kind + data + } +} diff --git a/packages/eden-watcher/src/gql/queries/index.ts b/packages/eden-watcher/src/gql/queries/index.ts new file mode 100644 index 00000000..a57136ed --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/index.ts @@ -0,0 +1,24 @@ +import fs from 'fs'; +import path from 'path'; + +export const events = fs.readFileSync(path.join(__dirname, 'events.gql'), 'utf8'); +export const eventsInRange = fs.readFileSync(path.join(__dirname, 'eventsInRange.gql'), 'utf8'); +export const producer = fs.readFileSync(path.join(__dirname, 'producer.gql'), 'utf8'); +export const producerSet = fs.readFileSync(path.join(__dirname, 'producerSet.gql'), 'utf8'); +export const producerSetChange = fs.readFileSync(path.join(__dirname, 'producerSetChange.gql'), 'utf8'); +export const producerRewardCollectorChange = fs.readFileSync(path.join(__dirname, 'producerRewardCollectorChange.gql'), 'utf8'); +export const rewardScheduleEntry = fs.readFileSync(path.join(__dirname, 'rewardScheduleEntry.gql'), 'utf8'); +export const rewardSchedule = fs.readFileSync(path.join(__dirname, 'rewardSchedule.gql'), 'utf8'); +export const producerEpoch = fs.readFileSync(path.join(__dirname, 'producerEpoch.gql'), 'utf8'); +export const epoch = fs.readFileSync(path.join(__dirname, 'epoch.gql'), 'utf8'); +export const slotClaim = fs.readFileSync(path.join(__dirname, 'slotClaim.gql'), 'utf8'); +export const slot = fs.readFileSync(path.join(__dirname, 'slot.gql'), 'utf8'); +export const staker = fs.readFileSync(path.join(__dirname, 'staker.gql'), 'utf8'); +export const network = fs.readFileSync(path.join(__dirname, 'network.gql'), 'utf8'); +export const distributor = fs.readFileSync(path.join(__dirname, 'distributor.gql'), 'utf8'); +export const distribution = fs.readFileSync(path.join(__dirname, 'distribution.gql'), 'utf8'); +export const claim = fs.readFileSync(path.join(__dirname, 'claim.gql'), 'utf8'); +export const slash = fs.readFileSync(path.join(__dirname, 'slash.gql'), 'utf8'); +export const account = fs.readFileSync(path.join(__dirname, 'account.gql'), 'utf8'); +export const getStateByCID = fs.readFileSync(path.join(__dirname, 'getStateByCID.gql'), 'utf8'); +export const getState = fs.readFileSync(path.join(__dirname, 'getState.gql'), 'utf8'); diff --git a/packages/eden-watcher/src/gql/queries/network.gql b/packages/eden-watcher/src/gql/queries/network.gql new file mode 100644 index 00000000..0dc896bc --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/network.gql @@ -0,0 +1,89 @@ +query network($id: String!, $blockHash: String!){ + network(id: $id, blockHash: $blockHash){ + id + slot0{ + id + owner + delegate + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + claims{ + id + slot{ + id + owner + delegate + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + claims{ + id + owner + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + } + } + owner + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + } + } + slot1{ + id + owner + delegate + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + claims{ + id + owner + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + } + } + slot2{ + id + owner + delegate + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + claims{ + id + owner + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + } + } + stakers{ + id + staked + rank + } + numStakers + totalStaked + stakedPercentiles + } +} diff --git a/packages/eden-watcher/src/gql/queries/producer.gql b/packages/eden-watcher/src/gql/queries/producer.gql new file mode 100644 index 00000000..479542b1 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/producer.gql @@ -0,0 +1,10 @@ +query producer($id: String!, $blockHash: String!){ + producer(id: $id, blockHash: $blockHash){ + id + active + rewardCollector + rewards + confirmedBlocks + pendingEpochBlocks + } +} diff --git a/packages/eden-watcher/src/gql/queries/producerEpoch.gql b/packages/eden-watcher/src/gql/queries/producerEpoch.gql new file mode 100644 index 00000000..05afbb02 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/producerEpoch.gql @@ -0,0 +1,46 @@ +query producerEpoch($id: String!, $blockHash: String!){ + producerEpoch(id: $id, blockHash: $blockHash){ + id + address + epoch{ + id + finalized + epochNumber + startBlock{ + cid + hash + number + timestamp + parentHash + } + endBlock{ + cid + hash + number + timestamp + parentHash + } + producerBlocks + allBlocks + producerBlocksRatio + producerRewards{ + id + address + epoch{ + id + finalized + epochNumber + producerBlocks + allBlocks + producerBlocksRatio + } + totalRewards + blocksProduced + blocksProducedRatio + } + } + totalRewards + blocksProduced + blocksProducedRatio + } +} diff --git a/packages/eden-watcher/src/gql/queries/producerRewardCollectorChange.gql b/packages/eden-watcher/src/gql/queries/producerRewardCollectorChange.gql new file mode 100644 index 00000000..debb4327 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/producerRewardCollectorChange.gql @@ -0,0 +1,8 @@ +query producerRewardCollectorChange($id: String!, $blockHash: String!){ + producerRewardCollectorChange(id: $id, blockHash: $blockHash){ + id + blockNumber + producer + rewardCollector + } +} diff --git a/packages/eden-watcher/src/gql/queries/producerSet.gql b/packages/eden-watcher/src/gql/queries/producerSet.gql new file mode 100644 index 00000000..8a2276ac --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/producerSet.gql @@ -0,0 +1,13 @@ +query producerSet($id: String!, $blockHash: String!){ + producerSet(id: $id, blockHash: $blockHash){ + id + producers{ + id + active + rewardCollector + rewards + confirmedBlocks + pendingEpochBlocks + } + } +} diff --git a/packages/eden-watcher/src/gql/queries/producerSetChange.gql b/packages/eden-watcher/src/gql/queries/producerSetChange.gql new file mode 100644 index 00000000..d2a4d734 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/producerSetChange.gql @@ -0,0 +1,8 @@ +query producerSetChange($id: String!, $blockHash: String!){ + producerSetChange(id: $id, blockHash: $blockHash){ + id + blockNumber + producer + changeType + } +} diff --git a/packages/eden-watcher/src/gql/queries/rewardSchedule.gql b/packages/eden-watcher/src/gql/queries/rewardSchedule.gql new file mode 100644 index 00000000..b188c260 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/rewardSchedule.gql @@ -0,0 +1,104 @@ +query rewardSchedule($id: String!, $blockHash: String!){ + rewardSchedule(id: $id, blockHash: $blockHash){ + id + rewardScheduleEntries{ + id + startTime + epochDuration + rewardsPerEpoch + } + lastEpoch{ + id + finalized + epochNumber + startBlock{ + cid + hash + number + timestamp + parentHash + } + endBlock{ + cid + hash + number + timestamp + parentHash + } + producerBlocks + allBlocks + producerBlocksRatio + producerRewards{ + id + address + epoch{ + id + finalized + epochNumber + startBlock{ + cid + hash + number + timestamp + parentHash + } + endBlock{ + cid + hash + number + timestamp + parentHash + } + producerBlocks + allBlocks + producerBlocksRatio + producerRewards{ + id + address + totalRewards + blocksProduced + blocksProducedRatio + } + } + totalRewards + blocksProduced + blocksProducedRatio + } + } + pendingEpoch{ + id + finalized + epochNumber + startBlock{ + cid + hash + number + timestamp + parentHash + } + endBlock{ + cid + hash + number + timestamp + parentHash + } + producerBlocks + allBlocks + producerBlocksRatio + producerRewards{ + id + address + totalRewards + blocksProduced + blocksProducedRatio + } + } + activeRewardScheduleEntry{ + id + startTime + epochDuration + rewardsPerEpoch + } + } +} diff --git a/packages/eden-watcher/src/gql/queries/rewardScheduleEntry.gql b/packages/eden-watcher/src/gql/queries/rewardScheduleEntry.gql new file mode 100644 index 00000000..29412fb8 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/rewardScheduleEntry.gql @@ -0,0 +1,8 @@ +query rewardScheduleEntry($id: String!, $blockHash: String!){ + rewardScheduleEntry(id: $id, blockHash: $blockHash){ + id + startTime + epochDuration + rewardsPerEpoch + } +} diff --git a/packages/eden-watcher/src/gql/queries/slash.gql b/packages/eden-watcher/src/gql/queries/slash.gql new file mode 100644 index 00000000..f9b9e839 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/slash.gql @@ -0,0 +1,34 @@ +query slash($id: String!, $blockHash: String!){ + slash(id: $id, blockHash: $blockHash){ + id + timestamp + account{ + id + totalClaimed + totalSlashed + claims{ + id + timestamp + index + account{ + id + totalClaimed + totalSlashed + slashes{ + id + timestamp + account{ + id + totalClaimed + totalSlashed + } + slashed + } + } + totalEarned + claimed + } + } + slashed + } +} diff --git a/packages/eden-watcher/src/gql/queries/slot.gql b/packages/eden-watcher/src/gql/queries/slot.gql new file mode 100644 index 00000000..039a563d --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/slot.gql @@ -0,0 +1,31 @@ +query slot($id: String!, $blockHash: String!){ + slot(id: $id, blockHash: $blockHash){ + id + owner + delegate + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + claims{ + id + slot{ + id + owner + delegate + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + } + owner + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + } + } +} diff --git a/packages/eden-watcher/src/gql/queries/slotClaim.gql b/packages/eden-watcher/src/gql/queries/slotClaim.gql new file mode 100644 index 00000000..5dec1ee7 --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/slotClaim.gql @@ -0,0 +1,40 @@ +query slotClaim($id: String!, $blockHash: String!){ + slotClaim(id: $id, blockHash: $blockHash){ + id + slot{ + id + owner + delegate + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + claims{ + id + slot{ + id + owner + delegate + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + } + owner + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + } + } + owner + winningBid + oldBid + startTime + expirationTime + taxRatePerDay + } +} diff --git a/packages/eden-watcher/src/gql/queries/staker.gql b/packages/eden-watcher/src/gql/queries/staker.gql new file mode 100644 index 00000000..c59e9d8f --- /dev/null +++ b/packages/eden-watcher/src/gql/queries/staker.gql @@ -0,0 +1,7 @@ +query staker($id: String!, $blockHash: String!){ + staker(id: $id, blockHash: $blockHash){ + id + staked + rank + } +} diff --git a/packages/eden-watcher/src/gql/subscriptions/index.ts b/packages/eden-watcher/src/gql/subscriptions/index.ts new file mode 100644 index 00000000..f12910c5 --- /dev/null +++ b/packages/eden-watcher/src/gql/subscriptions/index.ts @@ -0,0 +1,4 @@ +import fs from 'fs'; +import path from 'path'; + +export const onEvent = fs.readFileSync(path.join(__dirname, 'onEvent.gql'), 'utf8'); diff --git a/packages/eden-watcher/src/gql/subscriptions/onEvent.gql b/packages/eden-watcher/src/gql/subscriptions/onEvent.gql new file mode 100644 index 00000000..31805350 --- /dev/null +++ b/packages/eden-watcher/src/gql/subscriptions/onEvent.gql @@ -0,0 +1,75 @@ +subscription onEvent{ + onEvent{ + block{ + cid + hash + number + timestamp + parentHash + } + tx{ + hash + index + from + to + } + contract + eventIndex + event{ + ... on TransferEvent { + from + to + value + } + ... on ApprovalEvent { + owner + spender + value + } + ... on AuthorizationUsedEvent { + authorizer + nonce + } + ... on AdminUpdatedEvent { + newAdmin + oldAdmin + } + ... on TaxRateUpdatedEvent { + newNumerator + newDenominator + oldNumerator + oldDenominator + } + ... on SlotClaimedEvent { + slot + owner + delegate + newBidAmount + oldBidAmount + taxNumerator + taxDenominator + } + ... on SlotDelegateUpdatedEvent { + slot + owner + newDelegate + oldDelegate + } + ... on StakeEvent { + staker + stakeAmount + } + ... on UnstakeEvent { + staker + unstakedAmount + } + ... on WithdrawEvent { + withdrawer + withdrawalAmount + } + } + proof{ + data + } + } +} diff --git a/packages/eden-watcher/src/hooks.ts b/packages/eden-watcher/src/hooks.ts new file mode 100644 index 00000000..14211ffc --- /dev/null +++ b/packages/eden-watcher/src/hooks.ts @@ -0,0 +1,38 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; + +import { Indexer, ResultEvent } from './indexer'; + +export async function createInitialCheckpoint (indexer: Indexer, contractAddress: string, blockHash: string): Promise { + assert(indexer); + assert(blockHash); + assert(contractAddress); + + // Store an empty state in an IPLDBlock. + const ipldBlockData: any = { + state: {} + }; + + await indexer.createCheckpoint(contractAddress, blockHash, ipldBlockData); +} + +export async function createStateDiff (indexer: Indexer, blockHash: string): Promise { + assert(indexer); + assert(blockHash); +} + +export async function createStateCheckpoint (indexer: Indexer, contractAddress: string, blockHash: string): Promise { + assert(indexer); + assert(blockHash); + assert(contractAddress); + + return false; +} + +export async function handleEvent (indexer: Indexer, eventData: ResultEvent): Promise { + assert(indexer); + assert(eventData); +} diff --git a/packages/eden-watcher/src/indexer.ts b/packages/eden-watcher/src/indexer.ts new file mode 100644 index 00000000..ea4aa439 --- /dev/null +++ b/packages/eden-watcher/src/indexer.ts @@ -0,0 +1,1031 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import debug from 'debug'; +import { DeepPartial } from 'typeorm'; +import JSONbig from 'json-bigint'; +import { ethers } from 'ethers'; +import { sha256 } from 'multiformats/hashes/sha2'; +import { CID } from 'multiformats/cid'; +import _ from 'lodash'; + +import { JsonFragment } from '@ethersproject/abi'; +import { BaseProvider } from '@ethersproject/providers'; +import * as codec from '@ipld/dag-cbor'; +import { EthClient } from '@vulcanize/ipld-eth-client'; +import { StorageLayout } from '@vulcanize/solidity-mapper'; +import { EventInterface, Indexer as BaseIndexer, IndexerInterface, UNKNOWN_EVENT_NAME, ServerConfig } from '@vulcanize/util'; +import { GraphWatcher } from '@vulcanize/graph-node'; + +import { Database } from './database'; +import { Contract } from './entity/Contract'; +import { Event } from './entity/Event'; +import { SyncStatus } from './entity/SyncStatus'; +import { HookStatus } from './entity/HookStatus'; +import { BlockProgress } from './entity/BlockProgress'; +import { IPLDBlock } from './entity/IPLDBlock'; +import artifacts from './artifacts/EdenNetwork.json'; +import { createInitialCheckpoint, handleEvent, createStateDiff, createStateCheckpoint } from './hooks'; +import { IPFSClient } from './ipfs'; + +const log = debug('vulcanize:indexer'); + +const TRANSFER_EVENT = 'Transfer'; +const APPROVAL_EVENT = 'Approval'; +const AUTHORIZATIONUSED_EVENT = 'AuthorizationUsed'; +const ADMINUPDATED_EVENT = 'AdminUpdated'; +const TAXRATEUPDATED_EVENT = 'TaxRateUpdated'; +const SLOTCLAIMED_EVENT = 'SlotClaimed'; +const SLOTDELEGATEUPDATED_EVENT = 'SlotDelegateUpdated'; +const STAKE_EVENT = 'Stake'; +const UNSTAKE_EVENT = 'Unstake'; +const WITHDRAW_EVENT = 'Withdraw'; +const APPROVALFORALL_EVENT = 'ApprovalForAll'; +const BLOCKPRODUCERADDED_EVENT = 'BlockProducerAdded'; +const BLOCKPRODUCERREMOVED_EVENT = 'BlockProducerRemoved'; +const BLOCKPRODUCERREWARDCOLLECTORCHANGED_EVENT = 'BlockProducerRewardCollectorChanged'; +const REWARDSCHEDULECHANGED_EVENT = 'RewardScheduleChanged'; +const CLAIMED_EVENT = 'Claimed'; +const SLASHED_EVENT = 'Slashed'; +const MERKLEROOTUPDATED_EVENT = 'MerkleRootUpdated'; +const ACCOUNTUPDATED_EVENT = 'AccountUpdated'; +const PERMANENTURI_EVENT = 'PermanentURI'; +const GOVERNANCECHANGED_EVENT = 'GovernanceChanged'; +const UPDATETHRESHOLDCHANGED_EVENT = 'UpdateThresholdChanged'; +const ROLEADMINCHANGED_EVENT = 'RoleAdminChanged'; +const ROLEGRANTED_EVENT = 'RoleGranted'; +const ROLEREVOKED_EVENT = 'RoleRevoked'; + +export type ResultEvent = { + block: { + cid: string; + hash: string; + number: number; + timestamp: number; + parentHash: string; + }; + tx: { + hash: string; + from: string; + to: string; + index: number; + }; + + contract: string; + + eventIndex: number; + eventSignature: string; + event: any; + + proof: string; +}; + +export type ResultIPLDBlock = { + block: { + cid: string; + hash: string; + number: number; + timestamp: number; + parentHash: string; + }; + contractAddress: string; + cid: string; + kind: string; + data: string; +}; + +export class Indexer implements IndexerInterface { + _db: Database + _ethClient: EthClient + _ethProvider: BaseProvider + _postgraphileClient: EthClient + _baseIndexer: BaseIndexer + _serverConfig: ServerConfig + _graphWatcher: GraphWatcher; + + _abi: JsonFragment[] + _storageLayout: StorageLayout + _contract: ethers.utils.Interface + + _ipfsClient: IPFSClient + + constructor (serverConfig: ServerConfig, db: Database, ethClient: EthClient, postgraphileClient: EthClient, ethProvider: BaseProvider, graphWatcher: GraphWatcher) { + assert(db); + assert(ethClient); + assert(postgraphileClient); + + this._db = db; + this._ethClient = ethClient; + this._postgraphileClient = postgraphileClient; + this._ethProvider = ethProvider; + this._serverConfig = serverConfig; + this._baseIndexer = new BaseIndexer(this._db, this._ethClient, this._postgraphileClient, this._ethProvider); + this._graphWatcher = graphWatcher; + + const { abi, storageLayout } = artifacts; + + assert(abi); + assert(storageLayout); + + this._abi = abi; + this._storageLayout = storageLayout; + + this._contract = new ethers.utils.Interface(this._abi); + + this._ipfsClient = new IPFSClient(this._serverConfig.ipfsApiAddr); + } + + getResultEvent (event: Event): ResultEvent { + const block = event.block; + const eventFields = JSONbig.parse(event.eventInfo); + const { tx, eventSignature } = JSON.parse(event.extraInfo); + + return { + block: { + cid: block.cid, + hash: block.blockHash, + number: block.blockNumber, + timestamp: block.blockTimestamp, + parentHash: block.parentHash + }, + + tx: { + hash: event.txHash, + from: tx.src, + to: tx.dst, + index: tx.index + }, + + contract: event.contract, + + eventIndex: event.index, + eventSignature, + event: { + __typename: `${event.eventName}Event`, + ...eventFields + }, + + // TODO: Return proof only if requested. + proof: JSON.parse(event.proof) + }; + } + + getResultIPLDBlock (ipldBlock: IPLDBlock): ResultIPLDBlock { + const block = ipldBlock.block; + + const data = codec.decode(Buffer.from(ipldBlock.data)) as any; + + return { + block: { + cid: block.cid, + hash: block.blockHash, + number: block.blockNumber, + timestamp: block.blockTimestamp, + parentHash: block.parentHash + }, + contractAddress: ipldBlock.contractAddress, + cid: ipldBlock.cid, + kind: ipldBlock.kind, + data: JSON.stringify(data) + }; + } + + async processCanonicalBlock (job: any): Promise { + const { data: { blockHash } } = job; + + // Finalize staged diff blocks if any. + await this.finalizeDiffStaged(blockHash); + + // Call custom stateDiff hook. + await createStateDiff(this, blockHash); + } + + async createDiffStaged (contractAddress: string, blockHash: string, data: any): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + // Create a staged diff block. + const ipldBlock = await this.prepareIPLDBlock(block, contractAddress, data, 'diff_staged'); + await this.saveOrUpdateIPLDBlock(ipldBlock); + } + + async finalizeDiffStaged (blockHash: string): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + // Get all the staged diff blocks for the given blockHash. + const stagedBlocks = await this._db.getIPLDBlocks({ block, kind: 'diff_staged' }); + + // For each staged block, create a diff block. + for (const stagedBlock of stagedBlocks) { + const data = codec.decode(Buffer.from(stagedBlock.data)); + await this.createDiff(stagedBlock.contractAddress, stagedBlock.block.blockHash, data); + } + + // Remove all the staged diff blocks for current blockNumber. + await this.removeStagedIPLDBlocks(block.blockNumber); + } + + async createDiff (contractAddress: string, blockHash: string, data: any): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + // Fetch the latest checkpoint for the contract. + const checkpoint = await this.getLatestIPLDBlock(contractAddress, 'checkpoint'); + + // There should be an initial checkpoint at least. + // Assumption: There should be no events for the contract at the starting block. + assert(checkpoint, 'Initial checkpoint doesn\'t exist'); + + // Check if the latest checkpoint is in the same block. + assert(checkpoint.block.blockHash !== block.blockHash, 'Checkpoint already created for the block hash.'); + + const ipldBlock = await this.prepareIPLDBlock(block, contractAddress, data, 'diff'); + await this.saveOrUpdateIPLDBlock(ipldBlock); + } + + async processCheckpoint (job: any): Promise { + // Return if checkpointInterval is <= 0. + const checkpointInterval = this._serverConfig.checkpointInterval; + if (checkpointInterval <= 0) return; + + const { data: { blockHash, blockNumber } } = job; + + // Get all the contracts. + const contracts = await this._db.getContracts({}); + + // For each contract, merge the diff till now to create a checkpoint. + for (const contract of contracts) { + // Check if contract has checkpointing on. + if (contract.checkpoint) { + // If a checkpoint doesn't already exist and blockNumber is equal to startingBlock, create an initial checkpoint. + const checkpointBlock = await this.getLatestIPLDBlock(contract.address, 'checkpoint'); + + if (!checkpointBlock) { + if (blockNumber === contract.startingBlock) { + // Call initial checkpoint hook. + await createInitialCheckpoint(this, contract.address, blockHash); + } + } else { + await this.createCheckpoint(contract.address, blockHash, null, checkpointInterval); + } + } + } + } + + async processCLICheckpoint (contractAddress: string, blockHash?: string): Promise { + const checkpointBlockHash = await this.createCheckpoint(contractAddress, blockHash); + assert(checkpointBlockHash); + + const block = await this.getBlockProgress(checkpointBlockHash); + const checkpointIPLDBlocks = await this._db.getIPLDBlocks({ block, contractAddress, kind: 'checkpoint' }); + + // There can be at most one IPLDBlock for a (block, contractAddress, kind) combination. + assert(checkpointIPLDBlocks.length <= 1); + const checkpointIPLDBlock = checkpointIPLDBlocks[0]; + + const checkpointData = this.getIPLDData(checkpointIPLDBlock); + + await this.pushToIPFS(checkpointData); + + return checkpointBlockHash; + } + + async createCheckpoint (contractAddress: string, blockHash?: string, data?: any, checkpointInterval?: number): Promise { + const syncStatus = await this.getSyncStatus(); + assert(syncStatus); + + // Getting the current block. + let currentBlock; + + if (blockHash) { + currentBlock = await this.getBlockProgress(blockHash); + } else { + // In case of empty blockHash from checkpoint CLI, get the latest canonical block for the checkpoint. + currentBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash); + } + + assert(currentBlock); + + // Data is passed in case of initial checkpoint and checkpoint hook. + // Assumption: There should be no events for the contract at the starting block. + if (data) { + const ipldBlock = await this.prepareIPLDBlock(currentBlock, contractAddress, data, 'checkpoint'); + await this.saveOrUpdateIPLDBlock(ipldBlock); + + return; + } + + // If data is not passed, create from previous checkpoint and diffs after that. + + // Make sure the block is marked complete. + assert(currentBlock.isComplete, 'Block for a checkpoint should be marked as complete'); + + // Make sure the block is in the pruned region. + assert(currentBlock.blockNumber <= syncStatus.latestCanonicalBlockNumber, 'Block for a checkpoint should be in the pruned region'); + + // Fetch the latest checkpoint for the contract. + const checkpointBlock = await this.getLatestIPLDBlock(contractAddress, 'checkpoint', currentBlock.blockNumber); + assert(checkpointBlock); + + // Check (only if checkpointInterval is passed) if it is time for a new checkpoint. + if (checkpointInterval && checkpointBlock.block.blockNumber > (currentBlock.blockNumber - checkpointInterval)) { + return; + } + + // Call state checkpoint hook and check if default checkpoint is disabled. + const disableDefaultCheckpoint = await createStateCheckpoint(this, contractAddress, currentBlock.blockHash); + + if (disableDefaultCheckpoint) { + // Return if default checkpoint is disabled. + // Return block hash for checkpoint CLI. + return currentBlock.blockHash; + } + + const { block: { blockNumber: checkpointBlockNumber } } = checkpointBlock; + + // Fetching all diff blocks after checkpoint. + const diffBlocks = await this.getDiffIPLDBlocksByCheckpoint(contractAddress, checkpointBlockNumber); + + const checkpointBlockData = codec.decode(Buffer.from(checkpointBlock.data)) as any; + data = { + state: checkpointBlockData.state + }; + + for (const diffBlock of diffBlocks) { + const diff = codec.decode(Buffer.from(diffBlock.data)) as any; + data.state = _.merge(data.state, diff.state); + } + + const ipldBlock = await this.prepareIPLDBlock(currentBlock, contractAddress, data, 'checkpoint'); + await this.saveOrUpdateIPLDBlock(ipldBlock); + + return currentBlock.blockHash; + } + + getIPLDData (ipldBlock: IPLDBlock): any { + return codec.decode(Buffer.from(ipldBlock.data)); + } + + async getIPLDBlocksByHash (blockHash: string): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + return this._db.getIPLDBlocks({ block }); + } + + async getIPLDBlockByCid (cid: string): Promise { + const ipldBlocks = await this._db.getIPLDBlocks({ cid }); + + // There can be only one IPLDBlock with a particular cid. + assert(ipldBlocks.length <= 1); + + return ipldBlocks[0]; + } + + async getLatestIPLDBlock (contractAddress: string, kind: string | null, blockNumber?: number): Promise { + return this._db.getLatestIPLDBlock(contractAddress, kind, blockNumber); + } + + async getPrevIPLDBlock (blockHash: string, contractAddress: string, kind?: string): Promise { + const dbTx = await this._db.createTransactionRunner(); + let res; + + try { + res = await this._db.getPrevIPLDBlock(dbTx, blockHash, contractAddress, kind); + await dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + return res; + } + + async getDiffIPLDBlocksByCheckpoint (contractAddress: string, checkpointBlockNumber: number): Promise { + return this._db.getDiffIPLDBlocksByCheckpoint(contractAddress, checkpointBlockNumber); + } + + async prepareIPLDBlock (block: BlockProgress, contractAddress: string, data: any, kind: string):Promise { + assert(_.includes(['diff', 'checkpoint', 'diff_staged'], kind)); + + // Get an existing 'diff' | 'diff_staged' | 'checkpoint' IPLDBlock for current block, contractAddress. + const currentIPLDBlocks = await this._db.getIPLDBlocks({ block, contractAddress, kind }); + + // There can be at most one IPLDBlock for a (block, contractAddress, kind) combination. + assert(currentIPLDBlocks.length <= 1); + const currentIPLDBlock = currentIPLDBlocks[0]; + + // Update currentIPLDBlock if it exists and is of same kind. + let ipldBlock; + if (currentIPLDBlock) { + ipldBlock = currentIPLDBlock; + + // Update the data field. + const oldData = codec.decode(Buffer.from(currentIPLDBlock.data)); + data = _.merge(oldData, data); + } else { + ipldBlock = new IPLDBlock(); + + // Fetch the parent IPLDBlock. + const parentIPLDBlock = await this.getLatestIPLDBlock(contractAddress, null, block.blockNumber); + + // Setting the meta-data for an IPLDBlock (done only once per block). + data.meta = { + id: contractAddress, + kind, + parent: { + '/': parentIPLDBlock ? parentIPLDBlock.cid : null + }, + ethBlock: { + cid: { + '/': block.cid + }, + num: block.blockNumber + } + }; + } + + // Encoding the data using dag-cbor codec. + const bytes = codec.encode(data); + + // Calculating sha256 (multi)hash of the encoded data. + const hash = await sha256.digest(bytes); + + // Calculating the CID: v1, code: dag-cbor, hash. + const cid = CID.create(1, codec.code, hash); + + // Update ipldBlock with new data. + ipldBlock = Object.assign(ipldBlock, { + block, + contractAddress, + cid: cid.toString(), + kind: data.meta.kind, + data: Buffer.from(bytes) + }); + + return ipldBlock; + } + + async saveOrUpdateIPLDBlock (ipldBlock: IPLDBlock): Promise { + return this._db.saveOrUpdateIPLDBlock(ipldBlock); + } + + async removeStagedIPLDBlocks (blockNumber: number): Promise { + const dbTx = await this._db.createTransactionRunner(); + + try { + await this._db.removeEntities(dbTx, IPLDBlock, { relations: ['block'], where: { block: { blockNumber }, kind: 'diff_staged' } }); + await dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + } + + async pushToIPFS (data: any): Promise { + await this._ipfsClient.push(data); + } + + isIPFSConfigured (): boolean { + const ipfsAddr = this._serverConfig.ipfsApiAddr; + + // Return false if ipfsAddr is undefined | null | empty string. + return (ipfsAddr !== undefined && ipfsAddr !== null && ipfsAddr !== ''); + } + + async getSubgraphEntity (entity: new () => Entity, id: string, blockHash: string): Promise { + return this._graphWatcher.getEntity(entity, id, blockHash); + } + + async triggerIndexingOnEvent (event: Event): Promise { + const resultEvent = this.getResultEvent(event); + + // Call subgraph handler for event. + await this._graphWatcher.handleEvent(resultEvent); + + // Call custom hook function for indexing on event. + await handleEvent(this, resultEvent); + } + + async processEvent (event: Event): Promise { + // Trigger indexing of data based on the event. + await this.triggerIndexingOnEvent(event); + } + + parseEventNameAndArgs (kind: string, logObj: any): any { + let eventName = UNKNOWN_EVENT_NAME; + let eventInfo = {}; + + const { topics, data } = logObj; + const logDescription = this._contract.parseLog({ data, topics }); + + switch (logDescription.name) { + case TRANSFER_EVENT: { + eventName = logDescription.name; + const { from, to, value } = logDescription.args; + eventInfo = { + from, + to, + value: BigInt(ethers.BigNumber.from(value).toString()) + }; + + break; + } + case APPROVAL_EVENT: { + eventName = logDescription.name; + const { owner, spender, value } = logDescription.args; + eventInfo = { + owner, + spender, + value: BigInt(ethers.BigNumber.from(value).toString()) + }; + + break; + } + case AUTHORIZATIONUSED_EVENT: { + eventName = logDescription.name; + const { authorizer, nonce } = logDescription.args; + eventInfo = { + authorizer, + nonce + }; + + break; + } + case ADMINUPDATED_EVENT: { + eventName = logDescription.name; + const { newAdmin, oldAdmin } = logDescription.args; + eventInfo = { + newAdmin, + oldAdmin + }; + + break; + } + case TAXRATEUPDATED_EVENT: { + eventName = logDescription.name; + const { newNumerator, newDenominator, oldNumerator, oldDenominator } = logDescription.args; + eventInfo = { + newNumerator, + newDenominator, + oldNumerator, + oldDenominator + }; + + break; + } + case SLOTCLAIMED_EVENT: { + eventName = logDescription.name; + const { slot, owner, delegate, newBidAmount, oldBidAmount, taxNumerator, taxDenominator } = logDescription.args; + eventInfo = { + slot, + owner, + delegate, + newBidAmount, + oldBidAmount, + taxNumerator, + taxDenominator + }; + + break; + } + case SLOTDELEGATEUPDATED_EVENT: { + eventName = logDescription.name; + const { slot, owner, newDelegate, oldDelegate } = logDescription.args; + eventInfo = { + slot, + owner, + newDelegate, + oldDelegate + }; + + break; + } + case STAKE_EVENT: { + eventName = logDescription.name; + const { staker, stakeAmount } = logDescription.args; + eventInfo = { + staker, + stakeAmount: BigInt(ethers.BigNumber.from(stakeAmount).toString()) + }; + + break; + } + case UNSTAKE_EVENT: { + eventName = logDescription.name; + const { staker, unstakedAmount } = logDescription.args; + eventInfo = { + staker, + unstakedAmount: BigInt(ethers.BigNumber.from(unstakedAmount).toString()) + }; + + break; + } + case WITHDRAW_EVENT: { + eventName = logDescription.name; + const { withdrawer, withdrawalAmount } = logDescription.args; + eventInfo = { + withdrawer, + withdrawalAmount: BigInt(ethers.BigNumber.from(withdrawalAmount).toString()) + }; + + break; + } + case APPROVALFORALL_EVENT: { + eventName = logDescription.name; + const { owner, operator, approved } = logDescription.args; + eventInfo = { + owner, + operator, + approved + }; + + break; + } + case BLOCKPRODUCERADDED_EVENT: { + eventName = logDescription.name; + const { producer } = logDescription.args; + eventInfo = { + producer + }; + + break; + } + case BLOCKPRODUCERREMOVED_EVENT: { + eventName = logDescription.name; + const { producer } = logDescription.args; + eventInfo = { + producer + }; + + break; + } + case BLOCKPRODUCERREWARDCOLLECTORCHANGED_EVENT: { + eventName = logDescription.name; + const { producer, collector } = logDescription.args; + eventInfo = { + producer, + collector + }; + + break; + } + case REWARDSCHEDULECHANGED_EVENT: { + eventName = logDescription.name; + eventInfo = {}; + + break; + } + case CLAIMED_EVENT: { + eventName = logDescription.name; + const { index, totalEarned, account, claimed } = logDescription.args; + eventInfo = { + index: BigInt(ethers.BigNumber.from(index).toString()), + totalEarned: BigInt(ethers.BigNumber.from(totalEarned).toString()), + account, + claimed: BigInt(ethers.BigNumber.from(claimed).toString()) + }; + + break; + } + case SLASHED_EVENT: { + eventName = logDescription.name; + const { account, slashed } = logDescription.args; + eventInfo = { + account, + slashed: BigInt(ethers.BigNumber.from(slashed).toString()) + }; + + break; + } + case MERKLEROOTUPDATED_EVENT: { + eventName = logDescription.name; + const { merkleRoot, distributionNumber, metadataURI } = logDescription.args; + eventInfo = { + merkleRoot, + distributionNumber: BigInt(ethers.BigNumber.from(distributionNumber).toString()), + metadataURI + }; + + break; + } + case ACCOUNTUPDATED_EVENT: { + eventName = logDescription.name; + const { account, totalClaimed, totalSlashed } = logDescription.args; + eventInfo = { + account, + totalClaimed: BigInt(ethers.BigNumber.from(totalClaimed).toString()), + totalSlashed: BigInt(ethers.BigNumber.from(totalSlashed).toString()) + }; + + break; + } + case PERMANENTURI_EVENT: { + eventName = logDescription.name; + const { value, id } = logDescription.args; + eventInfo = { + value, + id: BigInt(ethers.BigNumber.from(id).toString()) + }; + + break; + } + case GOVERNANCECHANGED_EVENT: { + eventName = logDescription.name; + const { from, to } = logDescription.args; + eventInfo = { + from, + to + }; + + break; + } + case UPDATETHRESHOLDCHANGED_EVENT: { + eventName = logDescription.name; + const { updateThreshold } = logDescription.args; + eventInfo = { + updateThreshold: BigInt(ethers.BigNumber.from(updateThreshold).toString()) + }; + + break; + } + case ROLEADMINCHANGED_EVENT: { + eventName = logDescription.name; + const { role, previousAdminRole, newAdminRole } = logDescription.args; + eventInfo = { + role, + previousAdminRole, + newAdminRole + }; + + break; + } + case ROLEGRANTED_EVENT: { + eventName = logDescription.name; + const { role, account, sender } = logDescription.args; + eventInfo = { + role, + account, + sender + }; + + break; + } + case ROLEREVOKED_EVENT: { + eventName = logDescription.name; + const { role, account, sender } = logDescription.args; + eventInfo = { + role, + account, + sender + }; + + break; + } + } + + return { + eventName, + eventInfo, + eventSignature: logDescription.signature + }; + } + + async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock?: number): Promise { + // Use the checksum address (https://docs.ethers.io/v5/api/utils/address/#utils-getAddress) if input to address is a contract address. + // If a contract identifier is passed as address instead, no need to convert to checksum address. + // Customize: use the kind input to filter out non-contract-address input to address. + const formattedAddress = (kind === '__protocol__') ? address : ethers.utils.getAddress(address); + + if (!startingBlock) { + const syncStatus = await this.getSyncStatus(); + assert(syncStatus); + + startingBlock = syncStatus.latestIndexedBlockNumber; + } + + await this._db.saveContract(formattedAddress, kind, checkpoint, startingBlock); + + return true; + } + + async getHookStatus (): Promise { + const dbTx = await this._db.createTransactionRunner(); + let res; + + try { + res = await this._db.getHookStatus(dbTx); + await dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + + return res; + } + + async updateHookStatusProcessedBlock (blockNumber: number, force?: boolean): Promise { + const dbTx = await this._db.createTransactionRunner(); + let res; + + try { + res = await this._db.updateHookStatusProcessedBlock(dbTx, blockNumber, force); + await dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + + return res; + } + + async getLatestCanonicalBlock (): Promise { + const syncStatus = await this.getSyncStatus(); + assert(syncStatus); + + return this.getBlockProgress(syncStatus.latestCanonicalBlockHash); + } + + async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise> { + return this._baseIndexer.getEventsByFilter(blockHash, contract, name); + } + + async isWatchedContract (address : string): Promise { + return this._baseIndexer.isWatchedContract(address); + } + + async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> { + return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber); + } + + async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise> { + return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber); + } + + async getSyncStatus (): Promise { + return this._baseIndexer.getSyncStatus(); + } + + async updateSyncStatusIndexedBlock (blockHash: string, blockNumber: number, force = false): Promise { + return this._baseIndexer.updateSyncStatusIndexedBlock(blockHash, blockNumber, force); + } + + async updateSyncStatusChainHead (blockHash: string, blockNumber: number): Promise { + return this._baseIndexer.updateSyncStatusChainHead(blockHash, blockNumber); + } + + async updateSyncStatusCanonicalBlock (blockHash: string, blockNumber: number, force = false): Promise { + return this._baseIndexer.updateSyncStatusCanonicalBlock(blockHash, blockNumber, force); + } + + async getBlock (blockHash: string): Promise { + return this._baseIndexer.getBlock(blockHash); + } + + async getEvent (id: string): Promise { + return this._baseIndexer.getEvent(id); + } + + async getBlockProgress (blockHash: string): Promise { + return this._baseIndexer.getBlockProgress(blockHash); + } + + async getBlocksAtHeight (height: number, isPruned: boolean): Promise { + return this._baseIndexer.getBlocksAtHeight(height, isPruned); + } + + async getOrFetchBlockEvents (block: DeepPartial): Promise> { + return this._baseIndexer.getOrFetchBlockEvents(block, this._fetchAndSaveEvents.bind(this)); + } + + async getBlockEvents (blockHash: string): Promise> { + return this._baseIndexer.getBlockEvents(blockHash); + } + + async removeUnknownEvents (block: BlockProgress): Promise { + return this._baseIndexer.removeUnknownEvents(Event, block); + } + + async markBlocksAsPruned (blocks: BlockProgress[]): Promise { + return this._baseIndexer.markBlocksAsPruned(blocks); + } + + async updateBlockProgress (blockHash: string, lastProcessedEventIndex: number): Promise { + return this._baseIndexer.updateBlockProgress(blockHash, lastProcessedEventIndex); + } + + async getAncestorAtDepth (blockHash: string, depth: number): Promise { + return this._baseIndexer.getAncestorAtDepth(blockHash, depth); + } + + async _fetchAndSaveEvents ({ cid: blockCid, blockHash }: DeepPartial): Promise { + assert(blockHash); + let { block, logs } = await this._ethClient.getLogs({ blockHash }); + + const { + allEthHeaderCids: { + nodes: [ + { + ethTransactionCidsByHeaderId: { + nodes: transactions + } + } + ] + } + } = await this._postgraphileClient.getBlockWithTransactions({ blockHash }); + + const transactionMap = transactions.reduce((acc: {[key: string]: any}, transaction: {[key: string]: any}) => { + acc[transaction.txHash] = transaction; + return acc; + }, {}); + + const dbEvents: Array> = []; + + for (let li = 0; li < logs.length; li++) { + const logObj = logs[li]; + const { + topics, + data, + index: logIndex, + cid, + ipldBlock, + account: { + address + }, + transaction: { + hash: txHash + }, + receiptCID, + status + } = logObj; + + if (status) { + let eventName = UNKNOWN_EVENT_NAME; + let eventInfo = {}; + const tx = transactionMap[txHash]; + const extraInfo: { [key: string]: any } = { topics, data, tx }; + + const contract = ethers.utils.getAddress(address); + const watchedContract = await this.isWatchedContract(contract); + + if (watchedContract) { + const eventDetails = this.parseEventNameAndArgs(watchedContract.kind, logObj); + eventName = eventDetails.eventName; + eventInfo = eventDetails.eventInfo; + extraInfo.eventSignature = eventDetails.eventSignature; + } + + dbEvents.push({ + index: logIndex, + txHash, + contract, + eventName, + eventInfo: JSONbig.stringify(eventInfo), + extraInfo: JSONbig.stringify(extraInfo), + proof: JSONbig.stringify({ + data: JSONbig.stringify({ + blockHash, + receiptCID, + log: { + cid, + ipldBlock + } + }) + }) + }); + } else { + log(`Skipping event for receipt ${receiptCID} due to failed transaction.`); + } + } + + const dbTx = await this._db.createTransactionRunner(); + + try { + block = { + cid: blockCid, + blockHash, + blockNumber: block.number, + blockTimestamp: block.timestamp, + parentHash: block.parent.hash + }; + + await this._db.saveEvents(dbTx, block, dbEvents); + await dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + } +} diff --git a/packages/eden-watcher/src/ipfs.ts b/packages/eden-watcher/src/ipfs.ts new file mode 100644 index 00000000..3c92443d --- /dev/null +++ b/packages/eden-watcher/src/ipfs.ts @@ -0,0 +1,17 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { create, IPFSHTTPClient } from 'ipfs-http-client'; + +export class IPFSClient { + _client: IPFSHTTPClient + + constructor (url: string) { + this._client = create({ url }); + } + + async push (data: any): Promise { + await this._client.dag.put(data, { format: 'dag-cbor', hashAlg: 'sha2-256' }); + } +} diff --git a/packages/eden-watcher/src/job-runner.ts b/packages/eden-watcher/src/job-runner.ts new file mode 100644 index 00000000..9f7cbea3 --- /dev/null +++ b/packages/eden-watcher/src/job-runner.ts @@ -0,0 +1,163 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import path from 'path'; +import assert from 'assert'; +import 'reflect-metadata'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import debug from 'debug'; + +import { + getConfig, + Config, + JobQueue, + JobRunner as BaseJobRunner, + QUEUE_BLOCK_PROCESSING, + QUEUE_EVENT_PROCESSING, + QUEUE_BLOCK_CHECKPOINT, + QUEUE_HOOKS, + QUEUE_IPFS, + JobQueueConfig, + DEFAULT_CONFIG_PATH, + initClients +} from '@vulcanize/util'; +import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node'; + +import { Indexer } from './indexer'; +import { Database } from './database'; + +const log = debug('vulcanize:job-runner'); + +export class JobRunner { + _indexer: Indexer + _jobQueue: JobQueue + _baseJobRunner: BaseJobRunner + _jobQueueConfig: JobQueueConfig + + constructor (jobQueueConfig: JobQueueConfig, indexer: Indexer, jobQueue: JobQueue) { + this._jobQueueConfig = jobQueueConfig; + this._indexer = indexer; + this._jobQueue = jobQueue; + this._baseJobRunner = new BaseJobRunner(this._jobQueueConfig, this._indexer, this._jobQueue); + } + + async start (): Promise { + await this.subscribeBlockProcessingQueue(); + await this.subscribeEventProcessingQueue(); + await this.subscribeBlockCheckpointQueue(); + await this.subscribeHooksQueue(); + await this.subscribeIPFSQueue(); + } + + async subscribeBlockProcessingQueue (): Promise { + await this._jobQueue.subscribe(QUEUE_BLOCK_PROCESSING, async (job) => { + // TODO Call pre-block hook here (Directly or indirectly (Like done through indexer.processEvent for events)). + + await this._baseJobRunner.processBlock(job); + + await this._jobQueue.markComplete(job); + }); + } + + async subscribeEventProcessingQueue (): Promise { + await this._jobQueue.subscribe(QUEUE_EVENT_PROCESSING, async (job) => { + const event = await this._baseJobRunner.processEvent(job); + + const watchedContract = await this._indexer.isWatchedContract(event.contract); + if (watchedContract) { + await this._indexer.processEvent(event); + } + + await this._jobQueue.markComplete(job); + }); + } + + async subscribeHooksQueue (): Promise { + await this._jobQueue.subscribe(QUEUE_HOOKS, async (job) => { + const { data: { blockNumber } } = job; + + const hookStatus = await this._indexer.getHookStatus(); + + if (hookStatus && hookStatus.latestProcessedBlockNumber < (blockNumber - 1)) { + const message = `Hooks for blockNumber ${blockNumber - 1} not processed yet, aborting`; + log(message); + + throw new Error(message); + } + + await this._indexer.processCanonicalBlock(job); + + await this._jobQueue.markComplete(job); + }); + } + + async subscribeBlockCheckpointQueue (): Promise { + await this._jobQueue.subscribe(QUEUE_BLOCK_CHECKPOINT, async (job) => { + await this._indexer.processCheckpoint(job); + + await this._jobQueue.markComplete(job); + }); + } + + async subscribeIPFSQueue (): Promise { + await this._jobQueue.subscribe(QUEUE_IPFS, async (job) => { + const { data: { data } } = job; + + await this._indexer.pushToIPFS(data); + + await this._jobQueue.markComplete(job); + }); + } +} + +export const main = async (): Promise => { + const argv = await yargs(hideBin(process.argv)) + .option('f', { + alias: 'config-file', + demandOption: true, + describe: 'configuration file path (toml)', + type: 'string', + default: DEFAULT_CONFIG_PATH + }) + .argv; + + const config: Config = await getConfig(argv.f); + const { ethClient, postgraphileClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.init(); + + const graphDb = new GraphDatabase(config.database, path.resolve(__dirname, 'entity/*')); + await graphDb.init(); + + const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, config.server.subgraphPath); + + const indexer = new Indexer(config.server, db, ethClient, postgraphileClient, ethProvider, graphWatcher); + + graphWatcher.setIndexer(indexer); + await graphWatcher.init(); + + const jobQueueConfig = config.jobQueue; + assert(jobQueueConfig, 'Missing job queue config'); + + const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig; + assert(dbConnectionString, 'Missing job queue db connection string'); + + const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs }); + await jobQueue.start(); + + const jobRunner = new JobRunner(jobQueueConfig, indexer, jobQueue); + await jobRunner.start(); +}; + +main().then(() => { + log('Starting job runner...'); +}).catch(err => { + log(err); +}); + +process.on('uncaughtException', err => { + log('uncaughtException', err); +}); diff --git a/packages/eden-watcher/src/resolvers.ts b/packages/eden-watcher/src/resolvers.ts new file mode 100644 index 00000000..22a58d16 --- /dev/null +++ b/packages/eden-watcher/src/resolvers.ts @@ -0,0 +1,210 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import BigInt from 'apollo-type-bigint'; +import debug from 'debug'; + +import { Indexer } from './indexer'; +import { EventWatcher } from './events'; + +import { Producer } from './entity/Producer'; +import { ProducerSet } from './entity/ProducerSet'; +import { ProducerSetChange } from './entity/ProducerSetChange'; +import { ProducerRewardCollectorChange } from './entity/ProducerRewardCollectorChange'; +import { RewardScheduleEntry } from './entity/RewardScheduleEntry'; +import { RewardSchedule } from './entity/RewardSchedule'; +import { ProducerEpoch } from './entity/ProducerEpoch'; +import { Epoch } from './entity/Epoch'; +import { SlotClaim } from './entity/SlotClaim'; +import { Slot } from './entity/Slot'; +import { Staker } from './entity/Staker'; +import { Network } from './entity/Network'; +import { Distributor } from './entity/Distributor'; +import { Distribution } from './entity/Distribution'; +import { Claim } from './entity/Claim'; +import { Slash } from './entity/Slash'; +import { Account } from './entity/Account'; + +const log = debug('vulcanize:resolver'); + +export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatcher): Promise => { + assert(indexer); + + return { + BigInt: new BigInt('bigInt'), + + Event: { + __resolveType: (obj: any) => { + assert(obj.__typename); + + return obj.__typename; + } + }, + + Subscription: { + onEvent: { + subscribe: () => eventWatcher.getEventIterator() + } + }, + + Mutation: { + watchContract: (_: any, { address, kind, checkpoint, startingBlock }: { address: string, kind: string, checkpoint: boolean, startingBlock: number }): Promise => { + log('watchContract', address, kind, checkpoint, startingBlock); + + return indexer.watchContract(address, kind, checkpoint, startingBlock); + } + }, + + Query: { + producer: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('producer', id, blockHash); + + return indexer.getSubgraphEntity(Producer, id, blockHash); + }, + + producerSet: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('producerSet', id, blockHash); + + return indexer.getSubgraphEntity(ProducerSet, id, blockHash); + }, + + producerSetChange: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('producerSetChange', id, blockHash); + + return indexer.getSubgraphEntity(ProducerSetChange, id, blockHash); + }, + + producerRewardCollectorChange: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('producerRewardCollectorChange', id, blockHash); + + return indexer.getSubgraphEntity(ProducerRewardCollectorChange, id, blockHash); + }, + + rewardScheduleEntry: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('rewardScheduleEntry', id, blockHash); + + return indexer.getSubgraphEntity(RewardScheduleEntry, id, blockHash); + }, + + rewardSchedule: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('rewardSchedule', id, blockHash); + + return indexer.getSubgraphEntity(RewardSchedule, id, blockHash); + }, + + producerEpoch: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('producerEpoch', id, blockHash); + + return indexer.getSubgraphEntity(ProducerEpoch, id, blockHash); + }, + + // block: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + // log('block', id, blockHash); + + // return indexer.getSubgraphEntity(Block, id, blockHash); + // }, + + epoch: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('epoch', id, blockHash); + + return indexer.getSubgraphEntity(Epoch, id, blockHash); + }, + + slotClaim: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('slotClaim', id, blockHash); + + return indexer.getSubgraphEntity(SlotClaim, id, blockHash); + }, + + slot: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('slot', id, blockHash); + + return indexer.getSubgraphEntity(Slot, id, blockHash); + }, + + staker: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('staker', id, blockHash); + + return indexer.getSubgraphEntity(Staker, id, blockHash); + }, + + network: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('network', id, blockHash); + + return indexer.getSubgraphEntity(Network, id, blockHash); + }, + + distributor: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('distributor', id, blockHash); + + return indexer.getSubgraphEntity(Distributor, id, blockHash); + }, + + distribution: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('distribution', id, blockHash); + + return indexer.getSubgraphEntity(Distribution, id, blockHash); + }, + + claim: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('claim', id, blockHash); + + return indexer.getSubgraphEntity(Claim, id, blockHash); + }, + + slash: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('slash', id, blockHash); + + return indexer.getSubgraphEntity(Slash, id, blockHash); + }, + + account: async (_: any, { id, blockHash }: { id: string, blockHash: string }): Promise => { + log('account', id, blockHash); + + return indexer.getSubgraphEntity(Account, id, blockHash); + }, + + events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => { + log('events', blockHash, contractAddress, name); + + const block = await indexer.getBlockProgress(blockHash); + if (!block || !block.isComplete) { + throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`); + } + + const events = await indexer.getEventsByFilter(blockHash, contractAddress, name); + return events.map(event => indexer.getResultEvent(event)); + }, + + eventsInRange: async (_: any, { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }) => { + log('eventsInRange', fromBlockNumber, toBlockNumber); + + const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber); + if (expected !== actual) { + throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`); + } + + const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber); + return events.map(event => indexer.getResultEvent(event)); + }, + + getStateByCID: async (_: any, { cid }: { cid: string }) => { + log('getStateByCID', cid); + + const ipldBlock = await indexer.getIPLDBlockByCid(cid); + + return ipldBlock && ipldBlock.block.isComplete ? indexer.getResultIPLDBlock(ipldBlock) : undefined; + }, + + getState: async (_: any, { blockHash, contractAddress, kind = 'diff' }: { blockHash: string, contractAddress: string, kind: string }) => { + log('getState', blockHash, contractAddress, kind); + + const ipldBlock = await indexer.getPrevIPLDBlock(blockHash, contractAddress, kind); + + return ipldBlock && ipldBlock.block.isComplete ? indexer.getResultIPLDBlock(ipldBlock) : undefined; + } + } + }; +}; diff --git a/packages/eden-watcher/src/schema.gql b/packages/eden-watcher/src/schema.gql new file mode 100644 index 00000000..0935b8e0 --- /dev/null +++ b/packages/eden-watcher/src/schema.gql @@ -0,0 +1,391 @@ +scalar BigInt + +type Proof { + data: String! +} + +type ResultBoolean { + value: Boolean! + proof: Proof +} + +type ResultString { + value: String! + proof: Proof +} + +type ResultInt { + value: Int! + proof: Proof +} + +type ResultBigInt { + value: BigInt! + proof: Proof +} + +type Block { + cid: String! + hash: String! + number: Int! + timestamp: Int! + parentHash: String! +} + +type Transaction { + hash: String! + index: Int! + from: String! + to: String! +} + +type ResultEvent { + block: Block! + tx: Transaction! + contract: String! + eventIndex: Int! + event: Event! + proof: Proof +} + +union Event = TransferERC20Event | ApprovalERC20Event | AuthorizationUsedEvent | AdminUpdatedEvent | TaxRateUpdatedEvent | SlotClaimedEvent | SlotDelegateUpdatedEvent | StakeEvent | UnstakeEvent | WithdrawEvent | TransferERC721Event | ApprovalERC721Event | ApprovalForAllEvent | BlockProducerAddedEvent | BlockProducerRemovedEvent | BlockProducerRewardCollectorChangedEvent | ClaimedEvent | SlashedEvent | MerkleRootUpdatedEvent | AccountUpdatedEvent | PermanentURIEvent | GovernanceChangedEvent | UpdateThresholdChangedEvent | RoleAdminChangedEvent | RoleGrantedEvent | RoleRevokedEvent | RewardScheduleChangedEvent + +type TransferERC20Event { + from: String! + to: String! + value: BigInt! +} + +type ApprovalERC20Event { + owner: String! + spender: String! + value: BigInt! +} + +type AuthorizationUsedEvent { + authorizer: String! + nonce: String! +} + +type AdminUpdatedEvent { + newAdmin: String! + oldAdmin: String! +} + +type TaxRateUpdatedEvent { + newNumerator: Int! + newDenominator: Int! + oldNumerator: Int! + oldDenominator: Int! +} + +type SlotClaimedEvent { + slot: Int! + owner: String! + delegate: String! + newBidAmount: Int! + oldBidAmount: Int! + taxNumerator: Int! + taxDenominator: Int! +} + +type SlotDelegateUpdatedEvent { + slot: Int! + owner: String! + newDelegate: String! + oldDelegate: String! +} + +type StakeEvent { + staker: String! + stakeAmount: BigInt! +} + +type UnstakeEvent { + staker: String! + unstakedAmount: BigInt! +} + +type WithdrawEvent { + withdrawer: String! + withdrawalAmount: BigInt! +} + +type TransferERC721Event { + from: String! + to: String! + tokenId: BigInt! +} + +type ApprovalERC721Event { + owner: String! + approved: String! + tokenId: BigInt! +} + +type ApprovalForAllEvent { + owner: String! + operator: String! + approved: Boolean! +} + +type BlockProducerAddedEvent { + producer: String! +} + +type BlockProducerRemovedEvent { + producer: String! +} + +type BlockProducerRewardCollectorChangedEvent { + producer: String! + collector: String! +} + +type RewardScheduleChangedEvent { + # Note: dummy property added as server throws an error for type without any fields. + dummy: String +} + +type ClaimedEvent { + index: BigInt! + totalEarned: BigInt! + account: String! + claimed: BigInt! +} + +type SlashedEvent { + account: String! + slashed: BigInt! +} + +type MerkleRootUpdatedEvent { + merkleRoot: String! + distributionNumber: BigInt! + metadataURI: String! +} + +type AccountUpdatedEvent { + account: String! + totalClaimed: BigInt! + totalSlashed: BigInt! +} + +type PermanentURIEvent { + value: String! + id: BigInt! +} + +type GovernanceChangedEvent { + from: String! + to: String! +} + +type UpdateThresholdChangedEvent { + updateThreshold: BigInt! +} + +type RoleAdminChangedEvent { + role: String! + previousAdminRole: String! + newAdminRole: String! +} + +type RoleGrantedEvent { + role: String! + account: String! + sender: String! +} + +type RoleRevokedEvent { + role: String! + account: String! + sender: String! +} + +type ResultIPLDBlock { + block: Block! + contractAddress: String! + cid: String! + kind: String! + data: String! +} + +type Query { + events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!] + eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!] + producer(id: String!, blockHash: String!): Producer! + producerSet(id: String!, blockHash: String!): ProducerSet! + producerSetChange(id: String!, blockHash: String!): ProducerSetChange! + producerRewardCollectorChange(id: String!, blockHash: String!): ProducerRewardCollectorChange! + rewardScheduleEntry(id: String!, blockHash: String!): RewardScheduleEntry! + rewardSchedule(id: String!, blockHash: String!): RewardSchedule! + producerEpoch(id: String!, blockHash: String!): ProducerEpoch! + epoch(id: String!, blockHash: String!): Epoch! + slotClaim(id: String!, blockHash: String!): SlotClaim! + slot(id: String!, blockHash: String!): Slot! + staker(id: String!, blockHash: String!): Staker! + network(id: String!, blockHash: String!): Network! + distributor(id: String!, blockHash: String!): Distributor! + distribution(id: String!, blockHash: String!): Distribution! + claim(id: String!, blockHash: String!): Claim! + slash(id: String!, blockHash: String!): Slash! + account(id: String!, blockHash: String!): Account! + getStateByCID(cid: String!): ResultIPLDBlock + getState(blockHash: String!, contractAddress: String!, kind: String): ResultIPLDBlock +} + +type Producer { + id: ID! + active: Boolean! + rewardCollector: String + rewards: BigInt! + confirmedBlocks: BigInt! + pendingEpochBlocks: BigInt! +} + +type ProducerSet { + id: ID! + producers: [Producer!]! +} + +type ProducerSetChange { + id: ID! + blockNumber: BigInt! + producer: String! + changeType: ProducerSetChangeType! +} + +enum ProducerSetChangeType { + Added + Removed +} + +type ProducerRewardCollectorChange { + id: ID! + blockNumber: BigInt! + producer: String! + rewardCollector: String! +} + +type RewardScheduleEntry { + id: ID! + startTime: BigInt! + epochDuration: BigInt! + rewardsPerEpoch: BigInt! +} + +type RewardSchedule { + id: ID! + rewardScheduleEntries: [RewardScheduleEntry!]! + lastEpoch: Epoch + pendingEpoch: Epoch + activeRewardScheduleEntry: RewardScheduleEntry +} + +type Epoch { + id: ID! + finalized: Boolean! + epochNumber: BigInt! + startBlock: Block + endBlock: Block + producerBlocks: BigInt! + allBlocks: BigInt! + producerBlocksRatio: String! + producerRewards: [ProducerEpoch!]! +} + +type ProducerEpoch { + id: ID! + address: String! + epoch: Epoch! + totalRewards: BigInt! + blocksProduced: BigInt! + blocksProducedRatio: String! +} + +type SlotClaim { + id: ID! + slot: Slot! + owner: String! + winningBid: BigInt! + oldBid: BigInt! + startTime: BigInt! + expirationTime: BigInt! + taxRatePerDay: String! +} + +type Slot { + id: ID! + owner: String! + delegate: String! + winningBid: BigInt! + oldBid: BigInt! + startTime: BigInt! + expirationTime: BigInt! + taxRatePerDay: String! + claims: [SlotClaim!]! +} + +type Staker { + id: ID! + staked: BigInt! + rank: BigInt +} + +type Network { + id: ID! + slot0: Slot + slot1: Slot + slot2: Slot + stakers: [Staker!]! + numStakers: BigInt + totalStaked: BigInt! + stakedPercentiles: [BigInt!]! +} + +type Distributor { + id: ID! + currentDistribution: Distribution +} + +type Distribution { + id: ID! + distributor: Distributor! + timestamp: BigInt! + distributionNumber: BigInt! + merkleRoot: String! + metadataURI: String! +} + +type Claim { + id: ID! + timestamp: BigInt! + index: BigInt! + account: Account! + totalEarned: BigInt! + claimed: BigInt! +} + +type Account { + id: ID! + totalClaimed: BigInt! + totalSlashed: BigInt! + claims: [Claim!]! + slashes: [Slash!]! +} + +type Slash { + id: ID! + timestamp: BigInt! + account: Account! + slashed: BigInt! +} + +type Mutation { + watchContract(address: String!, kind: String!, checkpoint: Boolean!, startingBlock: Int): Boolean! +} + +type Subscription { + onEvent: ResultEvent! +} diff --git a/packages/eden-watcher/src/server.ts b/packages/eden-watcher/src/server.ts new file mode 100644 index 00000000..aec430ec --- /dev/null +++ b/packages/eden-watcher/src/server.ts @@ -0,0 +1,100 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import assert from 'assert'; +import 'reflect-metadata'; +import express, { Application } from 'express'; +import { ApolloServer, PubSub } from 'apollo-server-express'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import debug from 'debug'; +import 'graphql-import-node'; +import { createServer } from 'http'; + +import { DEFAULT_CONFIG_PATH, getConfig, Config, JobQueue, KIND_ACTIVE, initClients } from '@vulcanize/util'; +import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node'; + +import { createResolvers } from './resolvers'; +import { Indexer } from './indexer'; +import { Database } from './database'; +import { EventWatcher } from './events'; + +const log = debug('vulcanize:server'); + +export const main = async (): Promise => { + const argv = await yargs(hideBin(process.argv)) + .option('f', { + alias: 'config-file', + demandOption: true, + describe: 'configuration file path (toml)', + type: 'string', + default: DEFAULT_CONFIG_PATH + }) + .argv; + + const config: Config = await getConfig(argv.f); + const { ethClient, postgraphileClient, ethProvider } = await initClients(config); + + const { host, port, kind: watcherKind } = config.server; + + const db = new Database(config.database); + await db.init(); + + const graphDb = new GraphDatabase(config.database, path.resolve(__dirname, 'entity/*')); + await graphDb.init(); + + const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, config.server.subgraphPath); + + // Note: In-memory pubsub works fine for now, as each watcher is a single process anyway. + // Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries + const pubsub = new PubSub(); + const indexer = new Indexer(config.server, db, ethClient, postgraphileClient, ethProvider, graphWatcher); + + graphWatcher.setIndexer(indexer); + await graphWatcher.init(); + + const jobQueueConfig = config.jobQueue; + assert(jobQueueConfig, 'Missing job queue config'); + + const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig; + assert(dbConnectionString, 'Missing job queue db connection string'); + + const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs }); + + const eventWatcher = new EventWatcher(config.upstream, ethClient, postgraphileClient, indexer, pubsub, jobQueue); + + if (watcherKind === KIND_ACTIVE) { + await jobQueue.start(); + await eventWatcher.start(); + } + + const resolvers = await createResolvers(indexer, eventWatcher); + + const app: Application = express(); + const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString(); + const server = new ApolloServer({ + typeDefs, + resolvers + }); + + await server.start(); + server.applyMiddleware({ app }); + + const httpServer = createServer(app); + server.installSubscriptionHandlers(httpServer); + + httpServer.listen(port, host, () => { + log(`Server is listening on host ${host} port ${port}`); + }); + + return { app, server }; +}; + +main().then(() => { + log('Starting server...'); +}).catch(err => { + log(err); +}); diff --git a/packages/eden-watcher/tsconfig.json b/packages/eden-watcher/tsconfig.json new file mode 100644 index 00000000..99712bdf --- /dev/null +++ b/packages/eden-watcher/tsconfig.json @@ -0,0 +1,74 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "resolveJsonModule": true /* Enabling the option allows importing JSON, and validating the types in that JSON file. */ + }, + "include": ["src/**/*"] +}