diff --git a/packages/bayc-watcher/.eslintignore b/packages/bayc-watcher/.eslintignore new file mode 100644 index 00000000..55cb5225 --- /dev/null +++ b/packages/bayc-watcher/.eslintignore @@ -0,0 +1,2 @@ +# Don't lint build output. +dist diff --git a/packages/bayc-watcher/.eslintrc.json b/packages/bayc-watcher/.eslintrc.json new file mode 100644 index 00000000..476d529d --- /dev/null +++ b/packages/bayc-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/bayc-watcher/README.md b/packages/bayc-watcher/README.md new file mode 100644 index 00000000..f62e7551 --- /dev/null +++ b/packages/bayc-watcher/README.md @@ -0,0 +1,182 @@ +# output + +## Setup + +* Run the following command to install required packages: + + ```bash + yarn + ``` + +* Create a postgres12 database for the watcher: + + ```bash + sudo su - postgres + createdb output + ``` + +* 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 output-job-queue + ``` + + ``` + postgres@tesla:~$ psql -U postgres -h localhost output-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. + + output-job-queue=# CREATE EXTENSION pgcrypto; + CREATE EXTENSION + output-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 endpoint. + + * Update the `server` config with state checkpoint settings. + +## 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 `createInitialState` (triggered if the watcher passes the start block, checkpoint: `true`) in [hooks.ts](./src/hooks.ts) to save an initial `State` 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` `State` 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` `State` using the `Indexer` object. + +## 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 create --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 watcher: + + ```bash + yarn reset watcher --block-number + ``` + + * Reset job-queue: + + ```bash + yarn reset job-queue --block-number + ``` + + * Reset state: + + ```bash + yarn reset state --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] --block-number [snapshot-block-height] + ``` + + * `export-file`: Path of file to which to export the watcher data. + * `block-number`: Block height at which to take snapshot for export. + + * In target watcher, run job-runner: + + ```bash + yarn job-runner + ``` + + * Import watcher state: + + ```bash + yarn import-state --import-file + ``` + + * `import-file`: Path of file from which to import the watcher data. + + * Run server: + + ```bash + yarn server + ``` + + * To inspect a CID: + + ```bash + yarn inspect-cid --cid + ``` + + * `cid`: CID to be inspected. diff --git a/packages/bayc-watcher/environments/local.toml b/packages/bayc-watcher/environments/local.toml new file mode 100644 index 00000000..925a1eeb --- /dev/null +++ b/packages/bayc-watcher/environments/local.toml @@ -0,0 +1,56 @@ +[server] + host = "127.0.0.1" + port = 3008 + kind = "active" + + # Checkpointing state. + checkpointing = true + + # Checkpoint interval in number of blocks. + checkpointInterval = 2000 + + # Enable state creation + # CAUTION: Disable only if state creation is not desired or can be filled subsequently + enableState = true + + # Boolean to filter logs by contract. + filterLogs = false + + # Max block range for which to return events in eventsInRange GQL query. + # Use -1 for skipping check on block range. + maxEventsBlockRange = 1000 + +[metrics] + host = "127.0.0.1" + port = 9000 + [metrics.gql] + port = 9001 + +[database] + type = "postgres" + host = "localhost" + port = 5432 + database = "output" + username = "postgres" + password = "postgres" + synchronize = true + logging = false + +[upstream] + [upstream.ethServer] + gqlApiEndpoint = "http://127.0.0.1:8082/graphql" + rpcProviderEndpoint = "http://127.0.0.1:8081" + + [upstream.cache] + name = "requests" + enabled = false + deleteOnStart = false + +[jobQueue] + dbConnectionString = "postgres://postgres:postgres@localhost/output-job-queue" + maxCompletionLagInSecs = 300 + jobDelayInMilliSecs = 100 + eventsInBatch = 50 + blockDelayInMilliSecs = 2000 + prefetchBlocksInMem = true + prefetchBlockCount = 10 diff --git a/packages/bayc-watcher/package.json b/packages/bayc-watcher/package.json new file mode 100644 index 00000000..87fa1227 --- /dev/null +++ b/packages/bayc-watcher/package.json @@ -0,0 +1,75 @@ +{ + "name": "@cerc-io/output", + "version": "0.1.0", + "description": "output", + "private": true, + "main": "dist/index.js", + "scripts": { + "lint": "eslint .", + "build": "yarn clean && tsc && yarn copy-assets", + "clean": "rm -rf ./dist", + "copy-assets": "copyfiles -u 1 src/**/*.gql dist/", + "server": "DEBUG=vulcanize:* node --enable-source-maps dist/server.js", + "server:dev": "DEBUG=vulcanize:* ts-node src/server.ts", + "job-runner": "DEBUG=vulcanize:* YARN_CHILD_PROCESS=true node --enable-source-maps dist/job-runner.js", + "job-runner:dev": "DEBUG=vulcanize:* YARN_CHILD_PROCESS=true 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:* node --enable-source-maps dist/cli/checkpoint.js", + "checkpoint:dev": "DEBUG=vulcanize:* ts-node src/cli/checkpoint.ts", + "export-state": "DEBUG=vulcanize:* node --enable-source-maps dist/cli/export-state.js", + "export-state:dev": "DEBUG=vulcanize:* ts-node src/cli/export-state.ts", + "import-state": "DEBUG=vulcanize:* node --enable-source-maps dist/cli/import-state.js", + "import-state:dev": "DEBUG=vulcanize:* ts-node src/cli/import-state.ts", + "inspect-cid": "DEBUG=vulcanize:* ts-node src/cli/inspect-cid.ts", + "index-block": "DEBUG=vulcanize:* ts-node src/cli/index-block.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cerc-io/watcher-ts.git" + }, + "author": "", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/cerc-io/watcher-ts/issues" + }, + "homepage": "https://github.com/cerc-io/watcher-ts#readme", + "dependencies": { + "@apollo/client": "^3.3.19", + "@ethersproject/providers": "^5.4.4", + "@ipld/dag-cbor": "^6.0.12", + "@cerc-io/ipld-eth-client": "^0.2.13", + "@cerc-io/solidity-mapper": "^0.2.13", + "@cerc-io/util": "^0.2.13", + "apollo-server-express": "^2.25.0", + "apollo-type-bigint": "^0.1.3", + "debug": "^4.3.1", + "ethers": "^5.4.4", + "express": "^4.17.1", + "graphql": "^15.5.0", + "graphql-import-node": "^0.0.4", + "json-bigint": "^1.0.0", + "reflect-metadata": "^0.1.13", + "typeorm": "^0.2.32", + "yargs": "^17.0.1", + "decimal.js": "^10.3.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", + "copyfiles": "^2.4.1" + } +} diff --git a/packages/bayc-watcher/src/artifacts/BoredApeYachtClub.json b/packages/bayc-watcher/src/artifacts/BoredApeYachtClub.json new file mode 100644 index 00000000..5281c220 --- /dev/null +++ b/packages/bayc-watcher/src/artifacts/BoredApeYachtClub.json @@ -0,0 +1,996 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint256", + "name": "maxNftSupply", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "saleStart", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "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": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "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" + }, + { + "inputs": [], + "name": "BAYC_PROVENANCE", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_APES", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "REVEAL_TIMESTAMP", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "apePrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "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": [], + "name": "baseURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "emergencySetStartingIndexBlock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "flipSaleState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "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": "maxApePurchase", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "numberOfTokens", + "type": "uint256" + } + ], + "name": "mintApe", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "reserveApes", + "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": [], + "name": "saleIsActive", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "baseURI", + "type": "string" + } + ], + "name": "setBaseURI", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "provenanceHash", + "type": "string" + } + ], + "name": "setProvenanceHash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "revealTimeStamp", + "type": "uint256" + } + ], + "name": "setRevealTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "setStartingIndex", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "startingIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "startingIndexBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "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": [ + { + "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": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "storageLayout": { + "storage": [ + { + "astId": 230, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_supportedInterfaces", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes4,t_bool)" + }, + { + "astId": 2092, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_holderTokens", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_address,t_struct(UintSet)1318_storage)" + }, + { + "astId": 2094, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_tokenOwners", + "offset": 0, + "slot": "2", + "type": "t_struct(UintToAddressMap)1746_storage" + }, + { + "astId": 2098, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_tokenApprovals", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_address)" + }, + { + "astId": 2104, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_operatorApprovals", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))" + }, + { + "astId": 2106, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_name", + "offset": 0, + "slot": "6", + "type": "t_string_storage" + }, + { + "astId": 2108, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_symbol", + "offset": 0, + "slot": "7", + "type": "t_string_storage" + }, + { + "astId": 2112, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_tokenURIs", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_string_storage)" + }, + { + "astId": 2114, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_baseURI", + "offset": 0, + "slot": "9", + "type": "t_string_storage" + }, + { + "astId": 2996, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_owner", + "offset": 0, + "slot": "10", + "type": "t_address" + }, + { + "astId": 3110, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "BAYC_PROVENANCE", + "offset": 0, + "slot": "11", + "type": "t_string_storage" + }, + { + "astId": 3112, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "startingIndexBlock", + "offset": 0, + "slot": "12", + "type": "t_uint256" + }, + { + "astId": 3114, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "startingIndex", + "offset": 0, + "slot": "13", + "type": "t_uint256" + }, + { + "astId": 3122, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "MAX_APES", + "offset": 0, + "slot": "14", + "type": "t_uint256" + }, + { + "astId": 3125, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "saleIsActive", + "offset": 0, + "slot": "15", + "type": "t_bool" + }, + { + "astId": 3127, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "REVEAL_TIMESTAMP", + "offset": 0, + "slot": "16", + "type": "t_uint256" + } + ], + "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_struct(MapEntry)1420_storage)dyn_storage": { + "base": "t_struct(MapEntry)1420_storage", + "encoding": "dynamic_array", + "label": "struct EnumerableMap.MapEntry[]", + "numberOfBytes": "32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "encoding": "inplace", + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "encoding": "inplace", + "label": "bytes4", + "numberOfBytes": "4" + }, + "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_struct(UintSet)1318_storage)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => struct EnumerableSet.UintSet)", + "numberOfBytes": "32", + "value": "t_struct(UintSet)1318_storage" + }, + "t_mapping(t_bytes32,t_uint256)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_bytes4,t_bool)": { + "encoding": "mapping", + "key": "t_bytes4", + "label": "mapping(bytes4 => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "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_string_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => string)", + "numberOfBytes": "32", + "value": "t_string_storage" + }, + "t_string_storage": { + "encoding": "bytes", + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Map)1428_storage": { + "encoding": "inplace", + "label": "struct EnumerableMap.Map", + "members": [ + { + "astId": 1423, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_entries", + "offset": 0, + "slot": "0", + "type": "t_array(t_struct(MapEntry)1420_storage)dyn_storage" + }, + { + "astId": 1427, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_indexes", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_uint256)" + } + ], + "numberOfBytes": "64" + }, + "t_struct(MapEntry)1420_storage": { + "encoding": "inplace", + "label": "struct EnumerableMap.MapEntry", + "members": [ + { + "astId": 1417, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_key", + "offset": 0, + "slot": "0", + "type": "t_bytes32" + }, + { + "astId": 1419, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_value", + "offset": 0, + "slot": "1", + "type": "t_bytes32" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Set)932_storage": { + "encoding": "inplace", + "label": "struct EnumerableSet.Set", + "members": [ + { + "astId": 927, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_values", + "offset": 0, + "slot": "0", + "type": "t_array(t_bytes32)dyn_storage" + }, + { + "astId": 931, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_indexes", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_uint256)" + } + ], + "numberOfBytes": "64" + }, + "t_struct(UintSet)1318_storage": { + "encoding": "inplace", + "label": "struct EnumerableSet.UintSet", + "members": [ + { + "astId": 1317, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_inner", + "offset": 0, + "slot": "0", + "type": "t_struct(Set)932_storage" + } + ], + "numberOfBytes": "64" + }, + "t_struct(UintToAddressMap)1746_storage": { + "encoding": "inplace", + "label": "struct EnumerableMap.UintToAddressMap", + "members": [ + { + "astId": 1745, + "contract": "BAYC.sol:BoredApeYachtClub", + "label": "_inner", + "offset": 0, + "slot": "0", + "type": "t_struct(Map)1428_storage" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/cli/checkpoint-cmds/create.ts b/packages/bayc-watcher/src/cli/checkpoint-cmds/create.ts new file mode 100644 index 00000000..59778814 --- /dev/null +++ b/packages/bayc-watcher/src/cli/checkpoint-cmds/create.ts @@ -0,0 +1,57 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import debug from 'debug'; +import assert from 'assert'; + +import { getConfig, initClients, JobQueue, Config } from '@cerc-io/util'; + +import { Database } from '../../database'; +import { Indexer } from '../../indexer'; + +const log = debug('vulcanize:checkpoint-create'); + +export const command = 'create'; + +export const desc = 'Create checkpoint'; + +export const builder = { + 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.' + } +}; + +export const handler = async (argv: any): Promise => { + const config: Config = await getConfig(argv.configFile); + const { ethClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.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(); +}; diff --git a/packages/bayc-watcher/src/cli/checkpoint.ts b/packages/bayc-watcher/src/cli/checkpoint.ts new file mode 100644 index 00000000..d05ad8ad --- /dev/null +++ b/packages/bayc-watcher/src/cli/checkpoint.ts @@ -0,0 +1,39 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import yargs from 'yargs'; +import 'reflect-metadata'; +import debug from 'debug'; + +import { DEFAULT_CONFIG_PATH } from '@cerc-io/util'; + +import { hideBin } from 'yargs/helpers'; + +const log = debug('vulcanize:checkpoint'); + +const main = async () => { + return yargs(hideBin(process.argv)) + .parserConfiguration({ + 'parse-numbers': false + }).options({ + configFile: { + alias: 'f', + type: 'string', + require: true, + demandOption: true, + describe: 'configuration file path (toml)', + default: DEFAULT_CONFIG_PATH + } + }) + .commandDir('checkpoint-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/bayc-watcher/src/cli/export-state.ts b/packages/bayc-watcher/src/cli/export-state.ts new file mode 100644 index 00000000..94a39edb --- /dev/null +++ b/packages/bayc-watcher/src/cli/export-state.ts @@ -0,0 +1,151 @@ +// +// 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, + JobQueue, + StateKind +} from '@cerc-io/util'; +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' + }, + blockNumber: { + type: 'number', + describe: 'Block number to create snapshot at' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.init(); + + const exportData: any = { + snapshotBlock: {}, + contracts: [], + stateCheckpoints: [] + }; + + const contracts = await db.getContracts(); + + // Get latest block with hooks processed. + let block = await indexer.getLatestStateIndexedBlock(); + assert(block); + + if (argv.blockNumber) { + if (argv.blockNumber > block.blockNumber) { + throw new Error(`Export snapshot block height ${argv.blockNumber} should be less than latest state indexed block height ${block.blockNumber}`); + } + + const blocksAtSnapshotHeight = await indexer.getBlocksAtHeight(argv.blockNumber, false); + + if (!blocksAtSnapshotHeight.length) { + throw new Error(`No blocks at snapshot height ${argv.blockNumber}`); + } + + block = blocksAtSnapshotHeight[0]; + } + + log(`Creating export snapshot at block height ${block.blockNumber}`); + + // Export snapshot block. + exportData.snapshotBlock = { + blockNumber: block.blockNumber, + blockHash: block.blockHash + }; + + // Export contracts and checkpoints. + for (const contract of contracts) { + if (contract.startingBlock > block.blockNumber) { + continue; + } + + 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 state = await indexer.getLatestState(contract.address, StateKind.Checkpoint, block.blockNumber); + assert(state); + + const data = indexer.getStateData(state); + + exportData.stateCheckpoints.push({ + contractAddress: state.contractAddress, + cid: state.cid, + kind: state.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/bayc-watcher/src/cli/import-state.ts b/packages/bayc-watcher/src/cli/import-state.ts new file mode 100644 index 00000000..729d3027 --- /dev/null +++ b/packages/bayc-watcher/src/cli/import-state.ts @@ -0,0 +1,125 @@ +// +// 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, StateKind } from '@cerc-io/util'; +import * as codec from '@ipld/dag-cbor'; + +import { Database } from '../database'; +import { Indexer } from '../indexer'; +import { EventWatcher } from '../events'; +import { State } from '../entity/State'; + +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, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.init(); + + // 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 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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.init(); + + const eventWatcher = new EventWatcher(config.upstream, ethClient, 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, + eventWatcher, + jobQueueConfig.blockDelayInMilliSecs, + { + prefetch: true, + startBlock: importData.snapshotBlock.blockNumber, + endBlock: importData.snapshotBlock.blockNumber + } + ); + + // Fill the Contracts. + for (const contract of importData.contracts) { + await indexer.watchContract(contract.address, contract.kind, contract.checkpoint, contract.startingBlock); + } + + // Get the snapshot block. + const block = await indexer.getBlockProgress(importData.snapshotBlock.blockHash); + assert(block); + + // Fill the States. + for (const checkpoint of importData.stateCheckpoints) { + let state = new State(); + + state = Object.assign(state, checkpoint); + state.block = block; + + state.data = Buffer.from(codec.encode(state.data)); + + state = await indexer.saveOrUpdateState(state); + } + + // Mark snapshot block as completely processed. + block.isComplete = true; + await indexer.updateBlockProgress(block, block.lastProcessedEventIndex); + await indexer.updateSyncStatusChainHead(block.blockHash, block.blockNumber); + await indexer.updateSyncStatusIndexedBlock(block.blockHash, block.blockNumber); + await indexer.updateStateSyncStatusIndexedBlock(block.blockNumber); + await indexer.updateStateSyncStatusCheckpointBlock(block.blockNumber); + + // The 'diff_staged' and 'init' State entries are unnecessary as checkpoints have been already created for the snapshot block. + await indexer.removeStates(block.blockNumber, StateKind.Init); + await indexer.removeStates(block.blockNumber, StateKind.DiffStaged); + + log(`Import completed for snapshot block at height ${block.blockNumber}`); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/bayc-watcher/src/cli/index-block.ts b/packages/bayc-watcher/src/cli/index-block.ts new file mode 100644 index 00000000..8b00f79f --- /dev/null +++ b/packages/bayc-watcher/src/cli/index-block.ts @@ -0,0 +1,63 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import yargs from 'yargs'; +import 'reflect-metadata'; +import debug from 'debug'; +import assert from 'assert'; + +import { Config, DEFAULT_CONFIG_PATH, getConfig, initClients, JobQueue, indexBlock } from '@cerc-io/util'; + +import { Database } from '../database'; +import { Indexer } from '../indexer'; + +const log = debug('vulcanize:index-block'); + +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 + }, + block: { + type: 'number', + require: true, + demandOption: true, + describe: 'Block number to index' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.init(); + + await indexBlock(indexer, jobQueueConfig.eventsInBatch, argv); + + await db.close(); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/bayc-watcher/src/cli/inspect-cid.ts b/packages/bayc-watcher/src/cli/inspect-cid.ts new file mode 100644 index 00000000..c32ba47c --- /dev/null +++ b/packages/bayc-watcher/src/cli/inspect-cid.ts @@ -0,0 +1,68 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +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, JobQueue } from '@cerc-io/util'; + +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, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.init(); + + const state = await indexer.getStateByCID(argv.cid); + assert(state, 'State for the provided CID doesn\'t exist.'); + + const stateData = await indexer.getStateData(state); + + log(util.inspect(stateData, false, null)); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/bayc-watcher/src/cli/reset-cmds/job-queue.ts b/packages/bayc-watcher/src/cli/reset-cmds/job-queue.ts new file mode 100644 index 00000000..520cd8b9 --- /dev/null +++ b/packages/bayc-watcher/src/cli/reset-cmds/job-queue.ts @@ -0,0 +1,22 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import debug from 'debug'; + +import { getConfig, resetJobs } from '@cerc-io/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/bayc-watcher/src/cli/reset-cmds/state.ts b/packages/bayc-watcher/src/cli/reset-cmds/state.ts new file mode 100644 index 00000000..e5a9918f --- /dev/null +++ b/packages/bayc-watcher/src/cli/reset-cmds/state.ts @@ -0,0 +1,62 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import debug from 'debug'; + +import { getConfig } from '@cerc-io/util'; + +import { Database } from '../../database'; + +const log = debug('vulcanize:reset-state'); + +export const command = 'state'; + +export const desc = 'Reset State to a given block number'; + +export const builder = { + blockNumber: { + type: 'number' + } +}; + +export const handler = async (argv: any): Promise => { + const { blockNumber } = argv; + const config = await getConfig(argv.configFile); + + // Initialize database + const db = new Database(config.database); + await db.init(); + + // Create a DB transaction + const dbTx = await db.createTransactionRunner(); + + console.time('time:reset-state'); + try { + // Delete all State entries in the given range + await db.removeStatesAfterBlock(dbTx, blockNumber); + + // Reset the stateSyncStatus. + const stateSyncStatus = await db.getStateSyncStatus(); + + if (stateSyncStatus) { + if (stateSyncStatus.latestIndexedBlockNumber > blockNumber) { + await db.updateStateSyncStatusIndexedBlock(dbTx, blockNumber, true); + } + + if (stateSyncStatus.latestCheckpointBlockNumber > blockNumber) { + await db.updateStateSyncStatusCheckpointBlock(dbTx, blockNumber, true); + } + } + + dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + console.timeEnd('time:reset-state'); + + log(`Reset State successfully to block ${blockNumber}`); +}; diff --git a/packages/bayc-watcher/src/cli/reset-cmds/watcher.ts b/packages/bayc-watcher/src/cli/reset-cmds/watcher.ts new file mode 100644 index 00000000..38063f4f --- /dev/null +++ b/packages/bayc-watcher/src/cli/reset-cmds/watcher.ts @@ -0,0 +1,64 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import debug from 'debug'; +import { MoreThan } from 'typeorm'; +import assert from 'assert'; + +import { getConfig, initClients, resetJobs, JobQueue } from '@cerc-io/util'; + +import { Database } from '../../database'; +import { Indexer } from '../../indexer'; +import { BlockProgress } from '../../entity/BlockProgress'; + +import { SupportsInterface } from '../../entity/SupportsInterface'; +import { BalanceOf } from '../../entity/BalanceOf'; +import { OwnerOf } from '../../entity/OwnerOf'; +import { GetApproved } from '../../entity/GetApproved'; +import { IsApprovedForAll } from '../../entity/IsApprovedForAll'; +import { Name } from '../../entity/Name'; +import { Symbol } from '../../entity/Symbol'; +import { TokenURI } from '../../entity/TokenURI'; +import { TotalSupply } from '../../entity/TotalSupply'; +import { TokenOfOwnerByIndex } from '../../entity/TokenOfOwnerByIndex'; +import { TokenByIndex } from '../../entity/TokenByIndex'; +import { BaseURI } from '../../entity/BaseURI'; +import { Owner } from '../../entity/Owner'; + +const log = debug('vulcanize:reset-watcher'); + +export const command = 'watcher'; + +export const desc = 'Reset watcher to a 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, ethProvider } = await initClients(config); + + // Initialize database. + const db = new Database(config.database); + await db.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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.init(); + + await indexer.resetWatcherToBlock(argv.blockNumber); + log('Reset watcher successfully'); +}; diff --git a/packages/bayc-watcher/src/cli/reset.ts b/packages/bayc-watcher/src/cli/reset.ts new file mode 100644 index 00000000..95648c88 --- /dev/null +++ b/packages/bayc-watcher/src/cli/reset.ts @@ -0,0 +1,24 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import 'reflect-metadata'; +import debug from 'debug'; + +import { getResetYargs } from '@cerc-io/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/bayc-watcher/src/cli/watch-contract.ts b/packages/bayc-watcher/src/cli/watch-contract.ts new file mode 100644 index 00000000..440e35a4 --- /dev/null +++ b/packages/bayc-watcher/src/cli/watch-contract.ts @@ -0,0 +1,82 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import yargs from 'yargs'; +import 'reflect-metadata'; +import debug from 'debug'; +import assert from 'assert'; + +import { Config, DEFAULT_CONFIG_PATH, getConfig, initClients, JobQueue } from '@cerc-io/util'; + +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', + default: 1, + describe: 'Starting block' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.init(); + + await indexer.watchContract(argv.address, argv.kind, argv.checkpoint, argv.startingBlock); + + await db.close(); + await jobQueue.stop(); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(0); +}); diff --git a/packages/bayc-watcher/src/client.ts b/packages/bayc-watcher/src/client.ts new file mode 100644 index 00000000..c6a086d9 --- /dev/null +++ b/packages/bayc-watcher/src/client.ts @@ -0,0 +1,172 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { gql } from '@apollo/client/core'; +import { GraphQLClient, GraphQLConfig } from '@cerc-io/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 getSupportsInterface (blockHash: string, contractAddress: string, interfaceId: string): Promise { + const { supportsInterface } = await this._client.query( + gql(queries.supportsInterface), + { blockHash, contractAddress, interfaceId } + ); + + return supportsInterface; + } + + async getBalanceOf (blockHash: string, contractAddress: string, owner: string): Promise { + const { balanceOf } = await this._client.query( + gql(queries.balanceOf), + { blockHash, contractAddress, owner } + ); + + return balanceOf; + } + + async getOwnerOf (blockHash: string, contractAddress: string, tokenId: bigint): Promise { + const { ownerOf } = await this._client.query( + gql(queries.ownerOf), + { blockHash, contractAddress, tokenId } + ); + + return ownerOf; + } + + async getGetApproved (blockHash: string, contractAddress: string, tokenId: bigint): Promise { + const { getApproved } = await this._client.query( + gql(queries.getApproved), + { blockHash, contractAddress, tokenId } + ); + + return getApproved; + } + + async getIsApprovedForAll (blockHash: string, contractAddress: string, owner: string, operator: string): Promise { + const { isApprovedForAll } = await this._client.query( + gql(queries.isApprovedForAll), + { blockHash, contractAddress, owner, operator } + ); + + return isApprovedForAll; + } + + async getName (blockHash: string, contractAddress: string): Promise { + const { name } = await this._client.query( + gql(queries.name), + { blockHash, contractAddress } + ); + + return name; + } + + async getSymbol (blockHash: string, contractAddress: string): Promise { + const { symbol } = await this._client.query( + gql(queries.symbol), + { blockHash, contractAddress } + ); + + return symbol; + } + + async getTokenURI (blockHash: string, contractAddress: string, tokenId: bigint): Promise { + const { tokenURI } = await this._client.query( + gql(queries.tokenURI), + { blockHash, contractAddress, tokenId } + ); + + return tokenURI; + } + + async getTotalSupply (blockHash: string, contractAddress: string): Promise { + const { totalSupply } = await this._client.query( + gql(queries.totalSupply), + { blockHash, contractAddress } + ); + + return totalSupply; + } + + async getTokenOfOwnerByIndex (blockHash: string, contractAddress: string, owner: string, index: bigint): Promise { + const { tokenOfOwnerByIndex } = await this._client.query( + gql(queries.tokenOfOwnerByIndex), + { blockHash, contractAddress, owner, index } + ); + + return tokenOfOwnerByIndex; + } + + async getTokenByIndex (blockHash: string, contractAddress: string, index: bigint): Promise { + const { tokenByIndex } = await this._client.query( + gql(queries.tokenByIndex), + { blockHash, contractAddress, index } + ); + + return tokenByIndex; + } + + async getBaseURI (blockHash: string, contractAddress: string): Promise { + const { baseURI } = await this._client.query( + gql(queries.baseURI), + { blockHash, contractAddress } + ); + + return baseURI; + } + + async getOwner (blockHash: string, contractAddress: string): Promise { + const { owner } = await this._client.query( + gql(queries.owner), + { blockHash, contractAddress } + ); + + return owner; + } + + 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/bayc-watcher/src/database.ts b/packages/bayc-watcher/src/database.ts new file mode 100644 index 00000000..1a84b76c --- /dev/null +++ b/packages/bayc-watcher/src/database.ts @@ -0,0 +1,471 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import { Connection, ConnectionOptions, DeepPartial, FindConditions, QueryRunner, FindManyOptions, EntityTarget } from 'typeorm'; +import path from 'path'; + +import { Database as BaseDatabase, DatabaseInterface, QueryOptions, StateKind, Where } from '@cerc-io/util'; + +import { Contract } from './entity/Contract'; +import { Event } from './entity/Event'; +import { SyncStatus } from './entity/SyncStatus'; +import { StateSyncStatus } from './entity/StateSyncStatus'; +import { BlockProgress } from './entity/BlockProgress'; +import { State } from './entity/State'; +import { SupportsInterface } from './entity/SupportsInterface'; +import { BalanceOf } from './entity/BalanceOf'; +import { OwnerOf } from './entity/OwnerOf'; +import { GetApproved } from './entity/GetApproved'; +import { IsApprovedForAll } from './entity/IsApprovedForAll'; +import { Name } from './entity/Name'; +import { Symbol } from './entity/Symbol'; +import { TokenURI } from './entity/TokenURI'; +import { TotalSupply } from './entity/TotalSupply'; +import { TokenOfOwnerByIndex } from './entity/TokenOfOwnerByIndex'; +import { TokenByIndex } from './entity/TokenByIndex'; +import { BaseURI } from './entity/BaseURI'; +import { Owner } from './entity/Owner'; + +export class Database implements DatabaseInterface { + _config: ConnectionOptions; + _conn!: Connection; + _baseDatabase: BaseDatabase; + _propColMaps: { [key: string]: Map; } + + constructor (config: ConnectionOptions) { + assert(config); + + this._config = { + ...config, + entities: [path.join(__dirname, 'entity/*')] + }; + + this._baseDatabase = new BaseDatabase(this._config); + this._propColMaps = {}; + } + + get baseDatabase (): BaseDatabase { + return this._baseDatabase; + } + + async init (): Promise { + this._conn = await this._baseDatabase.init(); + this._setPropColMaps(); + } + + async close (): Promise { + return this._baseDatabase.close(); + } + + async getSupportsInterface ({ blockHash, contractAddress, interfaceId }: { blockHash: string, contractAddress: string, interfaceId: string }): Promise { + return this._conn.getRepository(SupportsInterface) + .findOne({ + blockHash, + contractAddress, + interfaceId + }); + } + + async getBalanceOf ({ blockHash, contractAddress, owner }: { blockHash: string, contractAddress: string, owner: string }): Promise { + return this._conn.getRepository(BalanceOf) + .findOne({ + blockHash, + contractAddress, + owner + }); + } + + async getOwnerOf ({ blockHash, contractAddress, tokenId }: { blockHash: string, contractAddress: string, tokenId: bigint }): Promise { + return this._conn.getRepository(OwnerOf) + .findOne({ + blockHash, + contractAddress, + tokenId + }); + } + + async getGetApproved ({ blockHash, contractAddress, tokenId }: { blockHash: string, contractAddress: string, tokenId: bigint }): Promise { + return this._conn.getRepository(GetApproved) + .findOne({ + blockHash, + contractAddress, + tokenId + }); + } + + async getIsApprovedForAll ({ blockHash, contractAddress, owner, operator }: { blockHash: string, contractAddress: string, owner: string, operator: string }): Promise { + return this._conn.getRepository(IsApprovedForAll) + .findOne({ + blockHash, + contractAddress, + owner, + operator + }); + } + + async getName ({ blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise { + return this._conn.getRepository(Name) + .findOne({ + blockHash, + contractAddress + }); + } + + // eslint-disable-next-line @typescript-eslint/ban-types + async getSymbol ({ blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise { + return this._conn.getRepository(Symbol) + .findOne({ + blockHash, + contractAddress + }); + } + + async getTokenURI ({ blockHash, contractAddress, tokenId }: { blockHash: string, contractAddress: string, tokenId: bigint }): Promise { + return this._conn.getRepository(TokenURI) + .findOne({ + blockHash, + contractAddress, + tokenId + }); + } + + async getTotalSupply ({ blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise { + return this._conn.getRepository(TotalSupply) + .findOne({ + blockHash, + contractAddress + }); + } + + async getTokenOfOwnerByIndex ({ blockHash, contractAddress, owner, index }: { blockHash: string, contractAddress: string, owner: string, index: bigint }): Promise { + return this._conn.getRepository(TokenOfOwnerByIndex) + .findOne({ + blockHash, + contractAddress, + owner, + index + }); + } + + async getTokenByIndex ({ blockHash, contractAddress, index }: { blockHash: string, contractAddress: string, index: bigint }): Promise { + return this._conn.getRepository(TokenByIndex) + .findOne({ + blockHash, + contractAddress, + index + }); + } + + async getBaseURI ({ blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise { + return this._conn.getRepository(BaseURI) + .findOne({ + blockHash, + contractAddress + }); + } + + async getOwner ({ blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise { + return this._conn.getRepository(Owner) + .findOne({ + blockHash, + contractAddress + }); + } + + async saveSupportsInterface ({ blockHash, blockNumber, contractAddress, interfaceId, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(SupportsInterface); + const entity = repo.create({ blockHash, blockNumber, contractAddress, interfaceId, value, proof }); + return repo.save(entity); + } + + async saveBalanceOf ({ blockHash, blockNumber, contractAddress, owner, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(BalanceOf); + const entity = repo.create({ blockHash, blockNumber, contractAddress, owner, value, proof }); + return repo.save(entity); + } + + async saveOwnerOf ({ blockHash, blockNumber, contractAddress, tokenId, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(OwnerOf); + const entity = repo.create({ blockHash, blockNumber, contractAddress, tokenId, value, proof }); + return repo.save(entity); + } + + async saveGetApproved ({ blockHash, blockNumber, contractAddress, tokenId, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(GetApproved); + const entity = repo.create({ blockHash, blockNumber, contractAddress, tokenId, value, proof }); + return repo.save(entity); + } + + async saveIsApprovedForAll ({ blockHash, blockNumber, contractAddress, owner, operator, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(IsApprovedForAll); + const entity = repo.create({ blockHash, blockNumber, contractAddress, owner, operator, value, proof }); + return repo.save(entity); + } + + async saveName ({ blockHash, blockNumber, contractAddress, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(Name); + const entity = repo.create({ blockHash, blockNumber, contractAddress, value, proof }); + return repo.save(entity); + } + + // eslint-disable-next-line @typescript-eslint/ban-types + async saveSymbol ({ blockHash, blockNumber, contractAddress, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(Symbol); + const entity = repo.create({ blockHash, blockNumber, contractAddress, value, proof }); + return repo.save(entity); + } + + async saveTokenURI ({ blockHash, blockNumber, contractAddress, tokenId, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(TokenURI); + const entity = repo.create({ blockHash, blockNumber, contractAddress, tokenId, value, proof }); + return repo.save(entity); + } + + async saveTotalSupply ({ blockHash, blockNumber, contractAddress, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(TotalSupply); + const entity = repo.create({ blockHash, blockNumber, contractAddress, value, proof }); + return repo.save(entity); + } + + async saveTokenOfOwnerByIndex ({ blockHash, blockNumber, contractAddress, owner, index, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(TokenOfOwnerByIndex); + const entity = repo.create({ blockHash, blockNumber, contractAddress, owner, index, value, proof }); + return repo.save(entity); + } + + async saveTokenByIndex ({ blockHash, blockNumber, contractAddress, index, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(TokenByIndex); + const entity = repo.create({ blockHash, blockNumber, contractAddress, index, value, proof }); + return repo.save(entity); + } + + async saveBaseURI ({ blockHash, blockNumber, contractAddress, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(BaseURI); + const entity = repo.create({ blockHash, blockNumber, contractAddress, value, proof }); + return repo.save(entity); + } + + async saveOwner ({ blockHash, blockNumber, contractAddress, value, proof }: DeepPartial): Promise { + const repo = this._conn.getRepository(Owner); + const entity = repo.create({ blockHash, blockNumber, contractAddress, value, proof }); + return repo.save(entity); + } + + getNewState (): State { + return new State(); + } + + async getStates (where: FindConditions): Promise { + const repo = this._conn.getRepository(State); + + return this._baseDatabase.getStates(repo, where); + } + + async getLatestState (contractAddress: string, kind: StateKind | null, blockNumber?: number): Promise { + const repo = this._conn.getRepository(State); + + return this._baseDatabase.getLatestState(repo, contractAddress, kind, blockNumber); + } + + async getPrevState (blockHash: string, contractAddress: string, kind?: string): Promise { + const repo = this._conn.getRepository(State); + + return this._baseDatabase.getPrevState(repo, blockHash, contractAddress, kind); + } + + // Fetch all diff States after the specified block number. + async getDiffStatesInRange (contractAddress: string, startblock: number, endBlock: number): Promise { + const repo = this._conn.getRepository(State); + + return this._baseDatabase.getDiffStatesInRange(repo, contractAddress, startblock, endBlock); + } + + async saveOrUpdateState (dbTx: QueryRunner, state: State): Promise { + const repo = dbTx.manager.getRepository(State); + + return this._baseDatabase.saveOrUpdateState(repo, state); + } + + async removeStates (dbTx: QueryRunner, blockNumber: number, kind: string): Promise { + const repo = dbTx.manager.getRepository(State); + + await this._baseDatabase.removeStates(repo, blockNumber, kind); + } + + async removeStatesAfterBlock (dbTx: QueryRunner, blockNumber: number): Promise { + const repo = dbTx.manager.getRepository(State); + + await this._baseDatabase.removeStatesAfterBlock(repo, blockNumber); + } + + async getStateSyncStatus (): Promise { + const repo = this._conn.getRepository(StateSyncStatus); + + return this._baseDatabase.getStateSyncStatus(repo); + } + + async updateStateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockNumber: number, force?: boolean): Promise { + const repo = queryRunner.manager.getRepository(StateSyncStatus); + + return this._baseDatabase.updateStateSyncStatusIndexedBlock(repo, blockNumber, force); + } + + async updateStateSyncStatusCheckpointBlock (queryRunner: QueryRunner, blockNumber: number, force?: boolean): Promise { + const repo = queryRunner.manager.getRepository(StateSyncStatus); + + return this._baseDatabase.updateStateSyncStatusCheckpointBlock(repo, blockNumber, force); + } + + async getContracts (): Promise { + const repo = this._conn.getRepository(Contract); + + return this._baseDatabase.getContracts(repo); + } + + 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: Where, queryOptions: QueryOptions): Promise { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getBlockEvents(repo, blockHash, where, queryOptions); + } + + async saveBlockWithEvents (queryRunner: QueryRunner, block: DeepPartial, events: DeepPartial[]): Promise { + const blockRepo = queryRunner.manager.getRepository(BlockProgress); + const eventRepo = queryRunner.manager.getRepository(Event); + + return this._baseDatabase.saveBlockWithEvents(blockRepo, eventRepo, block, events); + } + + async saveEvents (queryRunner: QueryRunner, events: Event[]): Promise { + const eventRepo = queryRunner.manager.getRepository(Event); + + return this._baseDatabase.saveEvents(eventRepo, events); + } + + async saveBlockProgress (queryRunner: QueryRunner, block: DeepPartial): Promise { + const repo = queryRunner.manager.getRepository(BlockProgress); + + return this._baseDatabase.saveBlockProgress(repo, block); + } + + async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise { + const repo = queryRunner.manager.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, force = false): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force); + } + + 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 getBlockProgressEntities (where: FindConditions, options: FindManyOptions): Promise { + const repo = this._conn.getRepository(BlockProgress); + + return this._baseDatabase.getBlockProgressEntities(repo, where, options); + } + + async updateBlockProgress (queryRunner: QueryRunner, block: BlockProgress, lastProcessedEventIndex: number): Promise { + const repo = queryRunner.manager.getRepository(BlockProgress); + + return this._baseDatabase.updateBlockProgress(repo, block, lastProcessedEventIndex); + } + + async removeEntities (queryRunner: QueryRunner, entity: new () => Entity, findConditions?: FindManyOptions | FindConditions): Promise { + return this._baseDatabase.removeEntities(queryRunner, entity, findConditions); + } + + async deleteEntitiesByConditions (queryRunner: QueryRunner, entity: EntityTarget, findConditions: FindConditions): Promise { + await this._baseDatabase.deleteEntitiesByConditions(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()); + } + + _setPropColMaps (): void { + this._propColMaps.SupportsInterface = this._getPropertyColumnMapForEntity('SupportsInterface'); + this._propColMaps.BalanceOf = this._getPropertyColumnMapForEntity('BalanceOf'); + this._propColMaps.OwnerOf = this._getPropertyColumnMapForEntity('OwnerOf'); + this._propColMaps.GetApproved = this._getPropertyColumnMapForEntity('GetApproved'); + this._propColMaps.IsApprovedForAll = this._getPropertyColumnMapForEntity('IsApprovedForAll'); + this._propColMaps.Name = this._getPropertyColumnMapForEntity('Name'); + this._propColMaps.Symbol = this._getPropertyColumnMapForEntity('Symbol'); + this._propColMaps.TokenURI = this._getPropertyColumnMapForEntity('TokenURI'); + this._propColMaps.TotalSupply = this._getPropertyColumnMapForEntity('TotalSupply'); + this._propColMaps.TokenOfOwnerByIndex = this._getPropertyColumnMapForEntity('TokenOfOwnerByIndex'); + this._propColMaps.TokenByIndex = this._getPropertyColumnMapForEntity('TokenByIndex'); + this._propColMaps.BaseURI = this._getPropertyColumnMapForEntity('BaseURI'); + this._propColMaps.Owner = this._getPropertyColumnMapForEntity('Owner'); + } +} diff --git a/packages/bayc-watcher/src/entity/BalanceOf.ts b/packages/bayc-watcher/src/entity/BalanceOf.ts new file mode 100644 index 00000000..4578c8c7 --- /dev/null +++ b/packages/bayc-watcher/src/entity/BalanceOf.ts @@ -0,0 +1,31 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockHash', 'contractAddress', 'owner'], { unique: true }) +export class BalanceOf { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar', { length: 42 }) + owner!: string; + + @Column('numeric', { transformer: bigintTransformer }) + value!: bigint; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/BaseURI.ts b/packages/bayc-watcher/src/entity/BaseURI.ts new file mode 100644 index 00000000..b49320ee --- /dev/null +++ b/packages/bayc-watcher/src/entity/BaseURI.ts @@ -0,0 +1,27 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; + +@Entity() +@Index(['blockHash', 'contractAddress'], { unique: true }) +export class BaseURI { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar') + value!: string; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/BlockProgress.ts b/packages/bayc-watcher/src/entity/BlockProgress.ts new file mode 100644 index 00000000..1d9d9328 --- /dev/null +++ b/packages/bayc-watcher/src/entity/BlockProgress.ts @@ -0,0 +1,48 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index, CreateDateColumn } from 'typeorm'; +import { BlockProgressInterface } from '@cerc-io/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; + + @CreateDateColumn() + createdAt!: Date; +} diff --git a/packages/bayc-watcher/src/entity/Contract.ts b/packages/bayc-watcher/src/entity/Contract.ts new file mode 100644 index 00000000..0727c538 --- /dev/null +++ b/packages/bayc-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/bayc-watcher/src/entity/Event.ts b/packages/bayc-watcher/src/entity/Event.ts new file mode 100644 index 00000000..c7c09d6b --- /dev/null +++ b/packages/bayc-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/bayc-watcher/src/entity/GetApproved.ts b/packages/bayc-watcher/src/entity/GetApproved.ts new file mode 100644 index 00000000..1bfc8048 --- /dev/null +++ b/packages/bayc-watcher/src/entity/GetApproved.ts @@ -0,0 +1,31 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockHash', 'contractAddress', 'tokenId'], { unique: true }) +export class GetApproved { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('numeric', { transformer: bigintTransformer }) + tokenId!: bigint; + + @Column('varchar') + value!: string; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/IsApprovedForAll.ts b/packages/bayc-watcher/src/entity/IsApprovedForAll.ts new file mode 100644 index 00000000..7f0c2423 --- /dev/null +++ b/packages/bayc-watcher/src/entity/IsApprovedForAll.ts @@ -0,0 +1,33 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; + +@Entity() +@Index(['blockHash', 'contractAddress', 'owner', 'operator'], { unique: true }) +export class IsApprovedForAll { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar', { length: 42 }) + owner!: string; + + @Column('varchar', { length: 42 }) + operator!: string; + + @Column('boolean') + value!: boolean; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/Name.ts b/packages/bayc-watcher/src/entity/Name.ts new file mode 100644 index 00000000..19850a1e --- /dev/null +++ b/packages/bayc-watcher/src/entity/Name.ts @@ -0,0 +1,27 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; + +@Entity() +@Index(['blockHash', 'contractAddress'], { unique: true }) +export class Name { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar') + value!: string; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/Owner.ts b/packages/bayc-watcher/src/entity/Owner.ts new file mode 100644 index 00000000..88c5a4e2 --- /dev/null +++ b/packages/bayc-watcher/src/entity/Owner.ts @@ -0,0 +1,27 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; + +@Entity() +@Index(['blockHash', 'contractAddress'], { unique: true }) +export class Owner { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar') + value!: string; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/OwnerOf.ts b/packages/bayc-watcher/src/entity/OwnerOf.ts new file mode 100644 index 00000000..0a15110c --- /dev/null +++ b/packages/bayc-watcher/src/entity/OwnerOf.ts @@ -0,0 +1,31 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockHash', 'contractAddress', 'tokenId'], { unique: true }) +export class OwnerOf { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('numeric', { transformer: bigintTransformer }) + tokenId!: bigint; + + @Column('varchar') + value!: string; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/State.ts b/packages/bayc-watcher/src/entity/State.ts new file mode 100644 index 00000000..b1776753 --- /dev/null +++ b/packages/bayc-watcher/src/entity/State.ts @@ -0,0 +1,31 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm'; +import { StateKind } from '@cerc-io/util'; +import { BlockProgress } from './BlockProgress'; + +@Entity() +@Index(['cid'], { unique: true }) +@Index(['block', 'contractAddress']) +@Index(['block', 'contractAddress', 'kind'], { unique: true }) +export class State { + @PrimaryGeneratedColumn() + id!: number; + + @ManyToOne(() => BlockProgress, { onDelete: 'CASCADE' }) + block!: BlockProgress; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar') + cid!: string; + + @Column({ type: 'enum', enum: StateKind }) + kind!: StateKind; + + @Column('bytea') + data!: Buffer; +} diff --git a/packages/bayc-watcher/src/entity/StateSyncStatus.ts b/packages/bayc-watcher/src/entity/StateSyncStatus.ts new file mode 100644 index 00000000..3d499c8f --- /dev/null +++ b/packages/bayc-watcher/src/entity/StateSyncStatus.ts @@ -0,0 +1,17 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity() +export class StateSyncStatus { + @PrimaryGeneratedColumn() + id!: number; + + @Column('integer') + latestIndexedBlockNumber!: number; + + @Column('integer') + latestCheckpointBlockNumber!: number; +} diff --git a/packages/bayc-watcher/src/entity/SupportsInterface.ts b/packages/bayc-watcher/src/entity/SupportsInterface.ts new file mode 100644 index 00000000..f668540b --- /dev/null +++ b/packages/bayc-watcher/src/entity/SupportsInterface.ts @@ -0,0 +1,30 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; + +@Entity() +@Index(['blockHash', 'contractAddress', 'interfaceId'], { unique: true }) +export class SupportsInterface { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar') + interfaceId!: string; + + @Column('boolean') + value!: boolean; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/Symbol.ts b/packages/bayc-watcher/src/entity/Symbol.ts new file mode 100644 index 00000000..e77aa200 --- /dev/null +++ b/packages/bayc-watcher/src/entity/Symbol.ts @@ -0,0 +1,27 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; + +@Entity() +@Index(['blockHash', 'contractAddress'], { unique: true }) +export class Symbol { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar') + value!: string; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/SyncStatus.ts b/packages/bayc-watcher/src/entity/SyncStatus.ts new file mode 100644 index 00000000..1cfacda8 --- /dev/null +++ b/packages/bayc-watcher/src/entity/SyncStatus.ts @@ -0,0 +1,36 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { SyncStatusInterface } from '@cerc-io/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; + + @Column('varchar', { length: 66 }) + initialIndexedBlockHash!: string; + + @Column('integer') + initialIndexedBlockNumber!: number; +} diff --git a/packages/bayc-watcher/src/entity/TokenByIndex.ts b/packages/bayc-watcher/src/entity/TokenByIndex.ts new file mode 100644 index 00000000..911ee840 --- /dev/null +++ b/packages/bayc-watcher/src/entity/TokenByIndex.ts @@ -0,0 +1,31 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockHash', 'contractAddress', 'index'], { unique: true }) +export class TokenByIndex { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('numeric', { transformer: bigintTransformer }) + index!: bigint; + + @Column('numeric', { transformer: bigintTransformer }) + value!: bigint; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/TokenOfOwnerByIndex.ts b/packages/bayc-watcher/src/entity/TokenOfOwnerByIndex.ts new file mode 100644 index 00000000..bcb5eadc --- /dev/null +++ b/packages/bayc-watcher/src/entity/TokenOfOwnerByIndex.ts @@ -0,0 +1,34 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockHash', 'contractAddress', 'owner', 'index'], { unique: true }) +export class TokenOfOwnerByIndex { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('varchar', { length: 42 }) + owner!: string; + + @Column('numeric', { transformer: bigintTransformer }) + index!: bigint; + + @Column('numeric', { transformer: bigintTransformer }) + value!: bigint; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/TokenURI.ts b/packages/bayc-watcher/src/entity/TokenURI.ts new file mode 100644 index 00000000..b0645f93 --- /dev/null +++ b/packages/bayc-watcher/src/entity/TokenURI.ts @@ -0,0 +1,31 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockHash', 'contractAddress', 'tokenId'], { unique: true }) +export class TokenURI { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('numeric', { transformer: bigintTransformer }) + tokenId!: bigint; + + @Column('varchar') + value!: string; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/entity/TotalSupply.ts b/packages/bayc-watcher/src/entity/TotalSupply.ts new file mode 100644 index 00000000..38628c58 --- /dev/null +++ b/packages/bayc-watcher/src/entity/TotalSupply.ts @@ -0,0 +1,28 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; +import { bigintTransformer } from '@cerc-io/util'; + +@Entity() +@Index(['blockHash', 'contractAddress'], { unique: true }) +export class TotalSupply { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('integer') + blockNumber!: number; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + @Column('numeric', { transformer: bigintTransformer }) + value!: bigint; + + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/bayc-watcher/src/events.ts b/packages/bayc-watcher/src/events.ts new file mode 100644 index 00000000..0eda7615 --- /dev/null +++ b/packages/bayc-watcher/src/events.ts @@ -0,0 +1,121 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import debug from 'debug'; +import { PubSub } from 'apollo-server-express'; + +import { EthClient } from '@cerc-io/ipld-eth-client'; +import { + JobQueue, + EventWatcher as BaseEventWatcher, + EventWatcherInterface, + QUEUE_BLOCK_PROCESSING, + QUEUE_EVENT_PROCESSING, + UNKNOWN_EVENT_NAME, + UpstreamConfig +} from '@cerc-io/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, 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, 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(); + this._baseEventWatcher.startBlockProcessing(); + } + + async stop (): Promise { + this._baseEventWatcher.stop(); + } + + async initBlockProcessingOnCompleteHandler (): Promise { + this._jobQueue.onComplete(QUEUE_BLOCK_PROCESSING, async (job) => { + const { id, data: { failed } } = job; + + if (failed) { + log(`Job ${id} for queue ${QUEUE_BLOCK_PROCESSING} failed`); + return; + } + + await this._baseEventWatcher.blockProcessingCompleteHandler(job); + }); + } + + 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 dbEvents = await this._baseEventWatcher.eventProcessingCompleteHandler(job); + const timeElapsedInSeconds = (Date.now() - Date.parse(createdOn)) / 1000; + + // Cannot publish individual event as they are processed together in a single job. + // TODO: Use a different pubsub to publish event from job-runner. + // https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries + for (const dbEvent of dbEvents) { + log(`Job onComplete event ${dbEvent.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 ${dbEvent.id} is too old (${timeElapsedInSeconds}s), not broadcasting to live subscribers`); + } + } + } + }); + } + + 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 + }); + } + } +} diff --git a/packages/bayc-watcher/src/fill.ts b/packages/bayc-watcher/src/fill.ts new file mode 100644 index 00000000..f20037c7 --- /dev/null +++ b/packages/bayc-watcher/src/fill.ts @@ -0,0 +1,91 @@ +// +// 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 { Config, getConfig, fillBlocks, JobQueue, DEFAULT_CONFIG_PATH, initClients } from '@cerc-io/util'; + +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 + }).env( + 'FILL' + ).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' + }, + prefetch: { + type: 'boolean', + default: false, + describe: 'Block and events prefetch mode' + }, + batchBlocks: { + type: 'number', + default: 10, + describe: 'Number of blocks prefetched in batch' + } + }).argv; + + const config: Config = await getConfig(argv.configFile); + const { ethClient, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.init(); + + // 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 eventWatcher = new EventWatcher(config.upstream, ethClient, indexer, pubsub, jobQueue); + + await fillBlocks(jobQueue, indexer, eventWatcher, jobQueueConfig.blockDelayInMilliSecs, argv); +}; + +main().catch(err => { + log(err); +}).finally(() => { + process.exit(); +}); + +process.on('SIGINT', () => { + log(`Exiting process ${process.pid} with code 0`); + process.exit(0); +}); diff --git a/packages/bayc-watcher/src/gql/index.ts b/packages/bayc-watcher/src/gql/index.ts new file mode 100644 index 00000000..4732f682 --- /dev/null +++ b/packages/bayc-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/bayc-watcher/src/gql/mutations/index.ts b/packages/bayc-watcher/src/gql/mutations/index.ts new file mode 100644 index 00000000..0c3bd853 --- /dev/null +++ b/packages/bayc-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/bayc-watcher/src/gql/mutations/watchContract.gql b/packages/bayc-watcher/src/gql/mutations/watchContract.gql new file mode 100644 index 00000000..2ecc74f7 --- /dev/null +++ b/packages/bayc-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) +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/balanceOf.gql b/packages/bayc-watcher/src/gql/queries/balanceOf.gql new file mode 100644 index 00000000..40af44c4 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/balanceOf.gql @@ -0,0 +1,8 @@ +query balanceOf($blockHash: String!, $contractAddress: String!, $owner: String!){ + balanceOf(blockHash: $blockHash, contractAddress: $contractAddress, owner: $owner){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/baseURI.gql b/packages/bayc-watcher/src/gql/queries/baseURI.gql new file mode 100644 index 00000000..184ff0a9 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/baseURI.gql @@ -0,0 +1,8 @@ +query baseURI($blockHash: String!, $contractAddress: String!){ + baseURI(blockHash: $blockHash, contractAddress: $contractAddress){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/events.gql b/packages/bayc-watcher/src/gql/queries/events.gql new file mode 100644 index 00000000..b06f243e --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/events.gql @@ -0,0 +1,43 @@ +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 ApprovalEvent { + owner + approved + tokenId + } + ... on ApprovalForAllEvent { + owner + operator + approved + } + ... on OwnershipTransferredEvent { + previousOwner + newOwner + } + ... on TransferEvent { + from + to + tokenId + } + } + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/eventsInRange.gql b/packages/bayc-watcher/src/gql/queries/eventsInRange.gql new file mode 100644 index 00000000..d54c4cb3 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/eventsInRange.gql @@ -0,0 +1,43 @@ +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 ApprovalEvent { + owner + approved + tokenId + } + ... on ApprovalForAllEvent { + owner + operator + approved + } + ... on OwnershipTransferredEvent { + previousOwner + newOwner + } + ... on TransferEvent { + from + to + tokenId + } + } + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/getApproved.gql b/packages/bayc-watcher/src/gql/queries/getApproved.gql new file mode 100644 index 00000000..c290206d --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/getApproved.gql @@ -0,0 +1,8 @@ +query getApproved($blockHash: String!, $contractAddress: String!, $tokenId: BigInt!){ + getApproved(blockHash: $blockHash, contractAddress: $contractAddress, tokenId: $tokenId){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/getState.gql b/packages/bayc-watcher/src/gql/queries/getState.gql new file mode 100644 index 00000000..3b8f6050 --- /dev/null +++ b/packages/bayc-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 + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/getStateByCID.gql b/packages/bayc-watcher/src/gql/queries/getStateByCID.gql new file mode 100644 index 00000000..6c3c4fd8 --- /dev/null +++ b/packages/bayc-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 + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/getSyncStatus.gql b/packages/bayc-watcher/src/gql/queries/getSyncStatus.gql new file mode 100644 index 00000000..2f80efdf --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/getSyncStatus.gql @@ -0,0 +1,8 @@ +query getSyncStatus{ + getSyncStatus{ + latestIndexedBlockHash + latestIndexedBlockNumber + latestCanonicalBlockHash + latestCanonicalBlockNumber + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/index.ts b/packages/bayc-watcher/src/gql/queries/index.ts new file mode 100644 index 00000000..3030f054 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/index.ts @@ -0,0 +1,21 @@ +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 supportsInterface = fs.readFileSync(path.join(__dirname, 'supportsInterface.gql'), 'utf8'); +export const balanceOf = fs.readFileSync(path.join(__dirname, 'balanceOf.gql'), 'utf8'); +export const ownerOf = fs.readFileSync(path.join(__dirname, 'ownerOf.gql'), 'utf8'); +export const getApproved = fs.readFileSync(path.join(__dirname, 'getApproved.gql'), 'utf8'); +export const isApprovedForAll = fs.readFileSync(path.join(__dirname, 'isApprovedForAll.gql'), 'utf8'); +export const name = fs.readFileSync(path.join(__dirname, 'name.gql'), 'utf8'); +export const symbol = fs.readFileSync(path.join(__dirname, 'symbol.gql'), 'utf8'); +export const tokenURI = fs.readFileSync(path.join(__dirname, 'tokenURI.gql'), 'utf8'); +export const totalSupply = fs.readFileSync(path.join(__dirname, 'totalSupply.gql'), 'utf8'); +export const tokenOfOwnerByIndex = fs.readFileSync(path.join(__dirname, 'tokenOfOwnerByIndex.gql'), 'utf8'); +export const tokenByIndex = fs.readFileSync(path.join(__dirname, 'tokenByIndex.gql'), 'utf8'); +export const baseURI = fs.readFileSync(path.join(__dirname, 'baseURI.gql'), 'utf8'); +export const owner = fs.readFileSync(path.join(__dirname, 'owner.gql'), 'utf8'); +export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.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/bayc-watcher/src/gql/queries/isApprovedForAll.gql b/packages/bayc-watcher/src/gql/queries/isApprovedForAll.gql new file mode 100644 index 00000000..6ea8ee5f --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/isApprovedForAll.gql @@ -0,0 +1,8 @@ +query isApprovedForAll($blockHash: String!, $contractAddress: String!, $owner: String!, $operator: String!){ + isApprovedForAll(blockHash: $blockHash, contractAddress: $contractAddress, owner: $owner, operator: $operator){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/name.gql b/packages/bayc-watcher/src/gql/queries/name.gql new file mode 100644 index 00000000..0431f0d6 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/name.gql @@ -0,0 +1,8 @@ +query name($blockHash: String!, $contractAddress: String!){ + name(blockHash: $blockHash, contractAddress: $contractAddress){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/owner.gql b/packages/bayc-watcher/src/gql/queries/owner.gql new file mode 100644 index 00000000..9212ad03 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/owner.gql @@ -0,0 +1,8 @@ +query owner($blockHash: String!, $contractAddress: String!){ + owner(blockHash: $blockHash, contractAddress: $contractAddress){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/ownerOf.gql b/packages/bayc-watcher/src/gql/queries/ownerOf.gql new file mode 100644 index 00000000..0c281503 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/ownerOf.gql @@ -0,0 +1,8 @@ +query ownerOf($blockHash: String!, $contractAddress: String!, $tokenId: BigInt!){ + ownerOf(blockHash: $blockHash, contractAddress: $contractAddress, tokenId: $tokenId){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/supportsInterface.gql b/packages/bayc-watcher/src/gql/queries/supportsInterface.gql new file mode 100644 index 00000000..7cc2a2cd --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/supportsInterface.gql @@ -0,0 +1,8 @@ +query supportsInterface($blockHash: String!, $contractAddress: String!, $interfaceId: String!){ + supportsInterface(blockHash: $blockHash, contractAddress: $contractAddress, interfaceId: $interfaceId){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/symbol.gql b/packages/bayc-watcher/src/gql/queries/symbol.gql new file mode 100644 index 00000000..fa278b80 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/symbol.gql @@ -0,0 +1,8 @@ +query symbol($blockHash: String!, $contractAddress: String!){ + symbol(blockHash: $blockHash, contractAddress: $contractAddress){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/tokenByIndex.gql b/packages/bayc-watcher/src/gql/queries/tokenByIndex.gql new file mode 100644 index 00000000..80655fc3 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/tokenByIndex.gql @@ -0,0 +1,8 @@ +query tokenByIndex($blockHash: String!, $contractAddress: String!, $index: BigInt!){ + tokenByIndex(blockHash: $blockHash, contractAddress: $contractAddress, index: $index){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/tokenOfOwnerByIndex.gql b/packages/bayc-watcher/src/gql/queries/tokenOfOwnerByIndex.gql new file mode 100644 index 00000000..f94a252e --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/tokenOfOwnerByIndex.gql @@ -0,0 +1,8 @@ +query tokenOfOwnerByIndex($blockHash: String!, $contractAddress: String!, $owner: String!, $index: BigInt!){ + tokenOfOwnerByIndex(blockHash: $blockHash, contractAddress: $contractAddress, owner: $owner, index: $index){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/tokenURI.gql b/packages/bayc-watcher/src/gql/queries/tokenURI.gql new file mode 100644 index 00000000..80679694 --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/tokenURI.gql @@ -0,0 +1,8 @@ +query tokenURI($blockHash: String!, $contractAddress: String!, $tokenId: BigInt!){ + tokenURI(blockHash: $blockHash, contractAddress: $contractAddress, tokenId: $tokenId){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/queries/totalSupply.gql b/packages/bayc-watcher/src/gql/queries/totalSupply.gql new file mode 100644 index 00000000..d1be1e4a --- /dev/null +++ b/packages/bayc-watcher/src/gql/queries/totalSupply.gql @@ -0,0 +1,8 @@ +query totalSupply($blockHash: String!, $contractAddress: String!){ + totalSupply(blockHash: $blockHash, contractAddress: $contractAddress){ + value + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/gql/subscriptions/index.ts b/packages/bayc-watcher/src/gql/subscriptions/index.ts new file mode 100644 index 00000000..f12910c5 --- /dev/null +++ b/packages/bayc-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/bayc-watcher/src/gql/subscriptions/onEvent.gql b/packages/bayc-watcher/src/gql/subscriptions/onEvent.gql new file mode 100644 index 00000000..2bc0160c --- /dev/null +++ b/packages/bayc-watcher/src/gql/subscriptions/onEvent.gql @@ -0,0 +1,43 @@ +subscription onEvent{ + onEvent{ + block{ + cid + hash + number + timestamp + parentHash + } + tx{ + hash + index + from + to + } + contract + eventIndex + event{ + ... on ApprovalEvent { + owner + approved + tokenId + } + ... on ApprovalForAllEvent { + owner + operator + approved + } + ... on OwnershipTransferredEvent { + previousOwner + newOwner + } + ... on TransferEvent { + from + to + tokenId + } + } + proof{ + data + } + } +} \ No newline at end of file diff --git a/packages/bayc-watcher/src/hooks.ts b/packages/bayc-watcher/src/hooks.ts new file mode 100644 index 00000000..7e43a634 --- /dev/null +++ b/packages/bayc-watcher/src/hooks.ts @@ -0,0 +1,80 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; + +import { updateStateForMappingType, updateStateForElementaryType, ResultEvent } from '@cerc-io/util'; + +import { Indexer } from './indexer'; + +/** + * Hook function to store an initial state. + * @param indexer Indexer instance. + * @param blockHash Hash of the concerned block. + * @param contractAddress Address of the concerned contract. + * @returns Data block to be stored. + */ +export async function createInitialState (indexer: Indexer, contractAddress: string, blockHash: string): Promise { + assert(indexer); + assert(blockHash); + assert(contractAddress); + + // Store an empty State. + const stateData: any = { + state: {} + }; + + // Use updateStateForElementaryType to update initial state with an elementary property. + // Eg. const stateData = updateStateForElementaryType(stateData, '_totalBalance', result.value.toString()); + + // Use updateStateForMappingType to update initial state with a nested property. + // Eg. const stateData = updateStateForMappingType(stateData, '_allowances', [owner, spender], allowance.value.toString()); + + // Return initial state data to be saved. + return stateData; +} + +/** + * Hook function to create state diff. + * @param indexer Indexer instance that contains methods to fetch the contract varaiable values. + * @param blockHash Block hash of the concerned block. + */ +export async function createStateDiff (indexer: Indexer, blockHash: string): Promise { + assert(indexer); + assert(blockHash); + + // Use indexer.createDiff() method to save custom state diff(s). +} + +/** + * Hook function to create state checkpoint + * @param indexer Indexer instance. + * @param contractAddress Address of the concerned contract. + * @param blockHash Block hash of the concerned block. + * @returns Whether to disable default checkpoint. If false, the state from this hook is updated with that from default checkpoint. + */ +export async function createStateCheckpoint (indexer: Indexer, contractAddress: string, blockHash: string): Promise { + assert(indexer); + assert(blockHash); + assert(contractAddress); + + // Use indexer.createStateCheckpoint() method to create a custom checkpoint. + + // Return false to update the state created by this hook by auto-generated checkpoint state. + // Return true to disable update of the state created by this hook by auto-generated checkpoint state. + return false; +} + +/** + * Event hook function. + * @param indexer Indexer instance that contains methods to fetch and update the contract values in the database. + * @param eventData ResultEvent object containing event information. + */ +export async function handleEvent (indexer: Indexer, eventData: ResultEvent): Promise { + assert(indexer); + assert(eventData); + + // Use indexer methods to index data. + // Pass `diff` parameter to indexer methods as true to save an auto-generated state from the indexed data. +} diff --git a/packages/bayc-watcher/src/indexer.ts b/packages/bayc-watcher/src/indexer.ts new file mode 100644 index 00000000..d389019e --- /dev/null +++ b/packages/bayc-watcher/src/indexer.ts @@ -0,0 +1,845 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import debug from 'debug'; +import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm'; +import JSONbig from 'json-bigint'; +import { ethers } from 'ethers'; +import _ from 'lodash'; + +import { JsonFragment } from '@ethersproject/abi'; +import { BaseProvider } from '@ethersproject/providers'; +import { EthClient } from '@cerc-io/ipld-eth-client'; +import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper'; +import { + Indexer as BaseIndexer, + IndexerInterface, + ValueResult, + ServerConfig, + JobQueue, + Where, + QueryOptions, + updateStateForElementaryType, + updateStateForMappingType, + StateKind, + StateStatus, + ResultEvent, + getResultEvent +} from '@cerc-io/util'; + +import BoredApeYachtClubArtifacts from './artifacts/BoredApeYachtClub.json'; +import { Database } from './database'; +import { createInitialState, handleEvent, createStateDiff, createStateCheckpoint } from './hooks'; +import { Contract } from './entity/Contract'; +import { Event } from './entity/Event'; +import { SyncStatus } from './entity/SyncStatus'; +import { StateSyncStatus } from './entity/StateSyncStatus'; +import { BlockProgress } from './entity/BlockProgress'; +import { State } from './entity/State'; + +import { SupportsInterface } from './entity/SupportsInterface'; +import { BalanceOf } from './entity/BalanceOf'; +import { OwnerOf } from './entity/OwnerOf'; +import { GetApproved } from './entity/GetApproved'; +import { IsApprovedForAll } from './entity/IsApprovedForAll'; +import { Name } from './entity/Name'; +import { Symbol } from './entity/Symbol'; +import { TokenURI } from './entity/TokenURI'; +import { TotalSupply } from './entity/TotalSupply'; +import { TokenOfOwnerByIndex } from './entity/TokenOfOwnerByIndex'; +import { TokenByIndex } from './entity/TokenByIndex'; +import { BaseURI } from './entity/BaseURI'; +import { Owner } from './entity/Owner'; + +const log = debug('vulcanize:indexer'); +const JSONbigNative = JSONbig({ useNativeBigInt: true }); + +const KIND_BOREDAPEYACHTCLUB = 'Empty'; + +export class Indexer implements IndexerInterface { + _db: Database + _ethClient: EthClient + _ethProvider: BaseProvider + _baseIndexer: BaseIndexer + _serverConfig: ServerConfig + + _abiMap: Map + _storageLayoutMap: Map + _contractMap: Map + + constructor (serverConfig: ServerConfig, db: Database, ethClient: EthClient, ethProvider: BaseProvider, jobQueue: JobQueue) { + assert(db); + assert(ethClient); + + this._db = db; + this._ethClient = ethClient; + this._ethProvider = ethProvider; + this._serverConfig = serverConfig; + this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue); + + this._abiMap = new Map(); + this._storageLayoutMap = new Map(); + this._contractMap = new Map(); + + const { + abi: BoredApeYachtClubABI, + storageLayout: BoredApeYachtClubStorageLayout + } = BoredApeYachtClubArtifacts; + + assert(BoredApeYachtClubABI); + this._abiMap.set(KIND_BOREDAPEYACHTCLUB, BoredApeYachtClubABI); + assert(BoredApeYachtClubStorageLayout); + this._storageLayoutMap.set(KIND_BOREDAPEYACHTCLUB, BoredApeYachtClubStorageLayout); + this._contractMap.set(KIND_BOREDAPEYACHTCLUB, new ethers.utils.Interface(BoredApeYachtClubABI)); + + } + + get serverConfig (): ServerConfig { + return this._serverConfig; + } + + get storageLayoutMap (): Map { + return this._storageLayoutMap; + } + + async init (): Promise { + await this._baseIndexer.fetchContracts(); + await this._baseIndexer.fetchStateStatus(); + } + + getResultEvent (event: Event): ResultEvent { + return getResultEvent(event); + } + + async supportsInterface (blockHash: string, contractAddress: string, interfaceId: string): Promise { + const entity = await this._db.getSupportsInterface({ blockHash, contractAddress, interfaceId }); + if (entity) { + log('supportsInterface: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('supportsInterface: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + const value = await contract.supportsInterface(interfaceId, { blockTag: blockHash }); + + const result: ValueResult = { value }; + + await this._db.saveSupportsInterface({ blockHash, blockNumber, contractAddress, interfaceId, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async balanceOf (blockHash: string, contractAddress: string, owner: string): Promise { + const entity = await this._db.getBalanceOf({ blockHash, contractAddress, owner }); + if (entity) { + log('balanceOf: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('balanceOf: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + let value = await contract.balanceOf(owner, { blockTag: blockHash }); + value = value.toString(); + value = BigInt(value); + + const result: ValueResult = { value }; + + await this._db.saveBalanceOf({ blockHash, blockNumber, contractAddress, owner, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async ownerOf (blockHash: string, contractAddress: string, tokenId: bigint): Promise { + const entity = await this._db.getOwnerOf({ blockHash, contractAddress, tokenId }); + if (entity) { + log('ownerOf: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('ownerOf: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + const value = await contract.ownerOf(tokenId, { blockTag: blockHash }); + + const result: ValueResult = { value }; + + await this._db.saveOwnerOf({ blockHash, blockNumber, contractAddress, tokenId, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async getApproved (blockHash: string, contractAddress: string, tokenId: bigint): Promise { + const entity = await this._db.getGetApproved({ blockHash, contractAddress, tokenId }); + if (entity) { + log('getApproved: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('getApproved: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + const value = await contract.getApproved(tokenId, { blockTag: blockHash }); + + const result: ValueResult = { value }; + + await this._db.saveGetApproved({ blockHash, blockNumber, contractAddress, tokenId, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async isApprovedForAll (blockHash: string, contractAddress: string, owner: string, operator: string): Promise { + const entity = await this._db.getIsApprovedForAll({ blockHash, contractAddress, owner, operator }); + if (entity) { + log('isApprovedForAll: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('isApprovedForAll: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + const value = await contract.isApprovedForAll(owner, operator, { blockTag: blockHash }); + + const result: ValueResult = { value }; + + await this._db.saveIsApprovedForAll({ blockHash, blockNumber, contractAddress, owner, operator, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async name (blockHash: string, contractAddress: string): Promise { + const entity = await this._db.getName({ blockHash, contractAddress }); + if (entity) { + log('name: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('name: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + const value = await contract.name({ blockTag: blockHash }); + + const result: ValueResult = { value }; + + await this._db.saveName({ blockHash, blockNumber, contractAddress, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async symbol (blockHash: string, contractAddress: string): Promise { + const entity = await this._db.getSymbol({ blockHash, contractAddress }); + if (entity) { + log('symbol: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('symbol: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + const value = await contract.symbol({ blockTag: blockHash }); + + const result: ValueResult = { value }; + + await this._db.saveSymbol({ blockHash, blockNumber, contractAddress, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async tokenURI (blockHash: string, contractAddress: string, tokenId: bigint): Promise { + const entity = await this._db.getTokenURI({ blockHash, contractAddress, tokenId }); + if (entity) { + log('tokenURI: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('tokenURI: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + const value = await contract.tokenURI(tokenId, { blockTag: blockHash }); + + const result: ValueResult = { value }; + + await this._db.saveTokenURI({ blockHash, blockNumber, contractAddress, tokenId, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async totalSupply (blockHash: string, contractAddress: string): Promise { + const entity = await this._db.getTotalSupply({ blockHash, contractAddress }); + if (entity) { + log('totalSupply: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('totalSupply: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + let value = await contract.totalSupply({ blockTag: blockHash }); + value = value.toString(); + value = BigInt(value); + + const result: ValueResult = { value }; + + await this._db.saveTotalSupply({ blockHash, blockNumber, contractAddress, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async tokenOfOwnerByIndex (blockHash: string, contractAddress: string, owner: string, index: bigint): Promise { + const entity = await this._db.getTokenOfOwnerByIndex({ blockHash, contractAddress, owner, index }); + if (entity) { + log('tokenOfOwnerByIndex: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('tokenOfOwnerByIndex: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + let value = await contract.tokenOfOwnerByIndex(owner, index, { blockTag: blockHash }); + value = value.toString(); + value = BigInt(value); + + const result: ValueResult = { value }; + + await this._db.saveTokenOfOwnerByIndex({ blockHash, blockNumber, contractAddress, owner, index, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async tokenByIndex (blockHash: string, contractAddress: string, index: bigint): Promise { + const entity = await this._db.getTokenByIndex({ blockHash, contractAddress, index }); + if (entity) { + log('tokenByIndex: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('tokenByIndex: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + let value = await contract.tokenByIndex(index, { blockTag: blockHash }); + value = value.toString(); + value = BigInt(value); + + const result: ValueResult = { value }; + + await this._db.saveTokenByIndex({ blockHash, blockNumber, contractAddress, index, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async baseURI (blockHash: string, contractAddress: string): Promise { + const entity = await this._db.getBaseURI({ blockHash, contractAddress }); + if (entity) { + log('baseURI: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('baseURI: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + const value = await contract.baseURI({ blockTag: blockHash }); + + const result: ValueResult = { value }; + + await this._db.saveBaseURI({ blockHash, blockNumber, contractAddress, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async owner (blockHash: string, contractAddress: string): Promise { + const entity = await this._db.getOwner({ blockHash, contractAddress }); + if (entity) { + log('owner: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('owner: db miss, fetching from upstream server'); + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = ethers.BigNumber.from(number).toNumber(); + + const abi = this._abiMap.get(KIND_BOREDAPEYACHTCLUB); + assert(abi); + + const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); + const value = await contract.owner({ blockTag: blockHash }); + + const result: ValueResult = { value }; + + await this._db.saveOwner({ blockHash, blockNumber, contractAddress, value: result.value, proof: JSONbigNative.stringify(result.proof) }); + + return result; + } + + async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise { + return this._baseIndexer.getStorageValue( + storageLayout, + blockHash, + contractAddress, + variable, + ...mappingKeys + ); + } + + async processInitialState (contractAddress: string, blockHash: string): Promise { + // Call initial state hook. + return createInitialState(this, contractAddress, blockHash); + } + + async processStateCheckpoint (contractAddress: string, blockHash: string): Promise { + // Call checkpoint hook. + return createStateCheckpoint(this, contractAddress, blockHash); + } + + async processCanonicalBlock (blockHash: string, blockNumber: number): Promise { + // Finalize staged diff blocks if any. + await this._baseIndexer.finalizeDiffStaged(blockHash); + + // Call custom stateDiff hook. + await createStateDiff(this, blockHash); + } + + async processCheckpoint (blockHash: string): Promise { + // Return if checkpointInterval is <= 0. + const checkpointInterval = this._serverConfig.checkpointInterval; + if (checkpointInterval <= 0) return; + + await this._baseIndexer.processCheckpoint(this, blockHash, checkpointInterval); + } + + async processCLICheckpoint (contractAddress: string, blockHash?: string): Promise { + return this._baseIndexer.processCLICheckpoint(this, contractAddress, blockHash); + } + + async getPrevState (blockHash: string, contractAddress: string, kind?: string): Promise { + return this._db.getPrevState(blockHash, contractAddress, kind); + } + + async getLatestState (contractAddress: string, kind: StateKind | null, blockNumber?: number): Promise { + return this._db.getLatestState(contractAddress, kind, blockNumber); + } + + async getStatesByHash (blockHash: string): Promise { + return this._baseIndexer.getStatesByHash(blockHash); + } + + async getStateByCID (cid: string): Promise { + return this._baseIndexer.getStateByCID(cid); + } + + async getStates (where: FindConditions): Promise { + return this._db.getStates(where); + } + + getStateData (state: State): any { + return this._baseIndexer.getStateData(state); + } + + // Method used to create auto diffs (diff_staged). + async createDiffStaged (contractAddress: string, blockHash: string, data: any): Promise { + await this._baseIndexer.createDiffStaged(contractAddress, blockHash, data); + } + + // Method to be used by createStateDiff hook. + async createDiff (contractAddress: string, blockHash: string, data: any): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + await this._baseIndexer.createDiff(contractAddress, block, data); + } + + // Method to be used by createStateCheckpoint hook. + async createStateCheckpoint (contractAddress: string, blockHash: string, data: any): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + return this._baseIndexer.createStateCheckpoint(contractAddress, block, data); + } + + // Method to be used by export-state CLI. + async createCheckpoint (contractAddress: string, blockHash: string): Promise { + const block = await this.getBlockProgress(blockHash); + assert(block); + + return this._baseIndexer.createCheckpoint(this, contractAddress, block); + } + + // Method to be used by fill-state CLI. + async createInit (blockHash: string, blockNumber: number): Promise { + // Create initial state for contracts. + await this._baseIndexer.createInit(this, blockHash, blockNumber); + } + + async saveOrUpdateState (state: State): Promise { + return this._baseIndexer.saveOrUpdateState(state); + } + + async removeStates (blockNumber: number, kind: StateKind): Promise { + await this._baseIndexer.removeStates(blockNumber, kind); + } + + async triggerIndexingOnEvent (event: Event): Promise { + const resultEvent = this.getResultEvent(event); + + // 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); + } + + async processBlock (blockProgress: BlockProgress): Promise { + // Call a function to create initial state for contracts. + await this._baseIndexer.createInit(this, blockProgress.blockHash, blockProgress.blockNumber); + } + + parseEventNameAndArgs (kind: string, logObj: any): any { + const { topics, data } = logObj; + + const contract = this._contractMap.get(kind); + assert(contract); + + const logDescription = contract.parseLog({ data, topics }); + + const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription); + + return { + eventName, + eventInfo, + eventSignature + }; + } + + async getStateSyncStatus (): Promise { + return this._db.getStateSyncStatus(); + } + + async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise { + const dbTx = await this._db.createTransactionRunner(); + let res; + + try { + res = await this._db.updateStateSyncStatusIndexedBlock(dbTx, blockNumber, force); + await dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + + return res; + } + + async updateStateSyncStatusCheckpointBlock (blockNumber: number, force?: boolean): Promise { + const dbTx = await this._db.createTransactionRunner(); + let res; + + try { + res = await this._db.updateStateSyncStatusCheckpointBlock(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); + + const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash); + assert(latestCanonicalBlock); + + return latestCanonicalBlock; + } + + async getLatestStateIndexedBlock (): Promise { + return this._baseIndexer.getLatestStateIndexedBlock(); + } + + async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise { + return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock); + } + + updateStateStatusMap (address: string, stateStatus: StateStatus): void { + this._baseIndexer.updateStateStatusMap(address, stateStatus); + } + + cacheContract (contract: Contract): void { + return this._baseIndexer.cacheContract(contract); + } + + async saveEventEntity (dbEvent: Event): Promise { + return this._baseIndexer.saveEventEntity(dbEvent); + } + + async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise> { + return this._baseIndexer.getEventsByFilter(blockHash, contract, name); + } + + isWatchedContract (address : string): Contract | undefined { + return this._baseIndexer.isWatchedContract(address); + } + + getContractsByKind (kind: string): Contract[] { + return this._baseIndexer.getContractsByKind(kind); + } + + 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, this._serverConfig.maxEventsBlockRange); + } + + async getSyncStatus (): Promise { + return this._baseIndexer.getSyncStatus(); + } + + async getBlocks (blockFilter: { blockHash?: string, blockNumber?: number }): Promise { + return this._baseIndexer.getBlocks(blockFilter); + } + + async updateSyncStatusIndexedBlock (blockHash: string, blockNumber: number, force = false): Promise { + return this._baseIndexer.updateSyncStatusIndexedBlock(blockHash, blockNumber, force); + } + + async updateSyncStatusChainHead (blockHash: string, blockNumber: number, force = false): Promise { + return this._baseIndexer.updateSyncStatusChainHead(blockHash, blockNumber, force); + } + + async updateSyncStatusCanonicalBlock (blockHash: string, blockNumber: number, force = false): Promise { + return this._baseIndexer.updateSyncStatusCanonicalBlock(blockHash, blockNumber, force); + } + + async getEvent (id: string): Promise { + return this._baseIndexer.getEvent(id); + } + + async getBlockProgress (blockHash: string): Promise { + return this._baseIndexer.getBlockProgress(blockHash); + } + + async getBlockProgressEntities (where: FindConditions, options: FindManyOptions): Promise { + return this._baseIndexer.getBlockProgressEntities(where, options); + } + + async getBlocksAtHeight (height: number, isPruned: boolean): Promise { + return this._baseIndexer.getBlocksAtHeight(height, isPruned); + } + + async saveBlockAndFetchEvents (block: DeepPartial): Promise<[BlockProgress, DeepPartial[]]> { + return this._baseIndexer.saveBlockAndFetchEvents(block, this._saveBlockAndFetchEvents.bind(this)); + } + + async getBlockEvents (blockHash: string, where: Where, queryOptions: QueryOptions): Promise> { + return this._baseIndexer.getBlockEvents(blockHash, where, queryOptions); + } + + async removeUnknownEvents (block: BlockProgress): Promise { + return this._baseIndexer.removeUnknownEvents(Event, block); + } + + async markBlocksAsPruned (blocks: BlockProgress[]): Promise { + return this._baseIndexer.markBlocksAsPruned(blocks); + } + + async updateBlockProgress (block: BlockProgress, lastProcessedEventIndex: number): Promise { + return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex); + } + + async getAncestorAtDepth (blockHash: string, depth: number): Promise { + return this._baseIndexer.getAncestorAtDepth(blockHash, depth); + } + + async resetWatcherToBlock (blockNumber: number): Promise { + const entities = [ + SupportsInterface, + BalanceOf, + OwnerOf, + GetApproved, + IsApprovedForAll, + Name, + Symbol, + TokenURI, + TotalSupply, + TokenOfOwnerByIndex, + TokenByIndex, + BaseURI, + Owner, + ]; + await this._baseIndexer.resetWatcherToBlock(blockNumber, entities); + } + + + async _saveBlockAndFetchEvents ({ + cid: blockCid, + blockHash, + blockNumber, + blockTimestamp, + parentHash + }: DeepPartial): Promise<[BlockProgress, DeepPartial[]]> { + assert(blockHash); + assert(blockNumber); + + const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this)); + + const dbTx = await this._db.createTransactionRunner(); + try { + const block = { + cid: blockCid, + blockHash, + blockNumber, + blockTimestamp, + parentHash + }; + + console.time(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`); + const blockProgress = await this._db.saveBlockWithEvents(dbTx, block, dbEvents); + await dbTx.commitTransaction(); + console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`); + + return [blockProgress, []]; + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + } +} diff --git a/packages/bayc-watcher/src/job-runner.ts b/packages/bayc-watcher/src/job-runner.ts new file mode 100644 index 00000000..716f4343 --- /dev/null +++ b/packages/bayc-watcher/src/job-runner.ts @@ -0,0 +1,122 @@ +// +// 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 { + getConfig, + Config, + JobQueue, + JobRunner as BaseJobRunner, + QUEUE_BLOCK_PROCESSING, + QUEUE_EVENT_PROCESSING, + QUEUE_BLOCK_CHECKPOINT, + QUEUE_HOOKS, + JOB_KIND_PRUNE, + JobQueueConfig, + DEFAULT_CONFIG_PATH, + initClients, + startMetricsServer +} from '@cerc-io/util'; + +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._jobQueue.deleteAllJobs(); + await this._baseJobRunner.resetToPrevIndexedBlock(); + await this.subscribeBlockProcessingQueue(); + await this.subscribeEventProcessingQueue(); + await this.subscribeBlockCheckpointQueue(); + await this.subscribeHooksQueue(); + } + + async subscribeBlockProcessingQueue (): Promise { + await this._jobQueue.subscribe(QUEUE_BLOCK_PROCESSING, async (job) => { + await this._baseJobRunner.processBlock(job); + }); + } + + async subscribeEventProcessingQueue (): Promise { + await this._jobQueue.subscribe(QUEUE_EVENT_PROCESSING, async (job) => { + await this._baseJobRunner.processEvent(job); + }); + } + + async subscribeHooksQueue (): Promise { + await this._jobQueue.subscribe(QUEUE_HOOKS, async (job) => { + await this._baseJobRunner.processHooks(job); + }); + } + + async subscribeBlockCheckpointQueue (): Promise { + await this._jobQueue.subscribe(QUEUE_BLOCK_CHECKPOINT, async (job) => { + await this._baseJobRunner.processCheckpoint(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, ethProvider } = await initClients(config); + + const db = new Database(config.database); + await db.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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.init(); + + const jobRunner = new JobRunner(jobQueueConfig, indexer, jobQueue); + await jobRunner.start(); + + startMetricsServer(config, indexer); +}; + +main().then(() => { + log('Starting job runner...'); +}).catch(err => { + log(err); +}); + +process.on('uncaughtException', err => { + log('uncaughtException', err); +}); diff --git a/packages/bayc-watcher/src/resolvers.ts b/packages/bayc-watcher/src/resolvers.ts new file mode 100644 index 00000000..ff48d1d4 --- /dev/null +++ b/packages/bayc-watcher/src/resolvers.ts @@ -0,0 +1,223 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import BigInt from 'apollo-type-bigint'; +import debug from 'debug'; +import Decimal from 'decimal.js'; +import { GraphQLResolveInfo, GraphQLScalarType } from 'graphql'; + +import { ValueResult, BlockHeight, gqlTotalQueryCount, gqlQueryCount, jsonBigIntStringReplacer, getResultState } from '@cerc-io/util'; + +import { Indexer } from './indexer'; +import { EventWatcher } from './events'; + + +const log = debug('vulcanize:resolver'); + +export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatcher): Promise => { + assert(indexer); + + return { + BigInt: new BigInt('bigInt'), + + BigDecimal: new GraphQLScalarType({ + name: 'BigDecimal', + description: 'BigDecimal custom scalar type', + parseValue (value) { + // value from the client + return new Decimal(value); + }, + serialize (value: Decimal) { + // value sent to the client + return value.toFixed(); + } + }), + + Event: { + __resolveType: (obj: any) => { + assert(obj.__typename); + + return obj.__typename; + } + }, + + Subscription: { + onEvent: { + subscribe: () => eventWatcher.getEventIterator() + } + }, + + Mutation: { + watchContract: async (_: any, { address, kind, checkpoint, startingBlock = 1 }: { address: string, kind: string, checkpoint: boolean, startingBlock: number }): Promise => { + log('watchContract', address, kind, checkpoint, startingBlock); + await indexer.watchContract(address, kind, checkpoint, startingBlock); + + return true; + } + }, + + Query: { + supportsInterface: (_: any, { blockHash, contractAddress, interfaceId }: { blockHash: string, contractAddress: string, interfaceId: string }): Promise => { + log('supportsInterface', blockHash, contractAddress, interfaceId); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('supportsInterface').inc(1); + + return indexer.supportsInterface(blockHash, contractAddress, interfaceId); + }, + + balanceOf: (_: any, { blockHash, contractAddress, owner }: { blockHash: string, contractAddress: string, owner: string }): Promise => { + log('balanceOf', blockHash, contractAddress, owner); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('balanceOf').inc(1); + + return indexer.balanceOf(blockHash, contractAddress, owner); + }, + + ownerOf: (_: any, { blockHash, contractAddress, tokenId }: { blockHash: string, contractAddress: string, tokenId: bigint }): Promise => { + log('ownerOf', blockHash, contractAddress, tokenId); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('ownerOf').inc(1); + + return indexer.ownerOf(blockHash, contractAddress, tokenId); + }, + + getApproved: (_: any, { blockHash, contractAddress, tokenId }: { blockHash: string, contractAddress: string, tokenId: bigint }): Promise => { + log('getApproved', blockHash, contractAddress, tokenId); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('getApproved').inc(1); + + return indexer.getApproved(blockHash, contractAddress, tokenId); + }, + + isApprovedForAll: (_: any, { blockHash, contractAddress, owner, operator }: { blockHash: string, contractAddress: string, owner: string, operator: string }): Promise => { + log('isApprovedForAll', blockHash, contractAddress, owner, operator); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('isApprovedForAll').inc(1); + + return indexer.isApprovedForAll(blockHash, contractAddress, owner, operator); + }, + + name: (_: any, { blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise => { + log('name', blockHash, contractAddress); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('name').inc(1); + + return indexer.name(blockHash, contractAddress); + }, + + symbol: (_: any, { blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise => { + log('symbol', blockHash, contractAddress); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('symbol').inc(1); + + return indexer.symbol(blockHash, contractAddress); + }, + + tokenURI: (_: any, { blockHash, contractAddress, tokenId }: { blockHash: string, contractAddress: string, tokenId: bigint }): Promise => { + log('tokenURI', blockHash, contractAddress, tokenId); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('tokenURI').inc(1); + + return indexer.tokenURI(blockHash, contractAddress, tokenId); + }, + + totalSupply: (_: any, { blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise => { + log('totalSupply', blockHash, contractAddress); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('totalSupply').inc(1); + + return indexer.totalSupply(blockHash, contractAddress); + }, + + tokenOfOwnerByIndex: (_: any, { blockHash, contractAddress, owner, index }: { blockHash: string, contractAddress: string, owner: string, index: bigint }): Promise => { + log('tokenOfOwnerByIndex', blockHash, contractAddress, owner, index); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('tokenOfOwnerByIndex').inc(1); + + return indexer.tokenOfOwnerByIndex(blockHash, contractAddress, owner, index); + }, + + tokenByIndex: (_: any, { blockHash, contractAddress, index }: { blockHash: string, contractAddress: string, index: bigint }): Promise => { + log('tokenByIndex', blockHash, contractAddress, index); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('tokenByIndex').inc(1); + + return indexer.tokenByIndex(blockHash, contractAddress, index); + }, + + baseURI: (_: any, { blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise => { + log('baseURI', blockHash, contractAddress); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('baseURI').inc(1); + + return indexer.baseURI(blockHash, contractAddress); + }, + + owner: (_: any, { blockHash, contractAddress }: { blockHash: string, contractAddress: string }): Promise => { + log('owner', blockHash, contractAddress); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('owner').inc(1); + + return indexer.owner(blockHash, contractAddress); + }, + + events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => { + log('events', blockHash, contractAddress, name); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('events').inc(1); + + 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); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('eventsInRange').inc(1); + + 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); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('getStateByCID').inc(1); + + const state = await indexer.getStateByCID(cid); + + return state && state.block.isComplete ? getResultState(state) : undefined; + }, + + getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => { + log('getState', blockHash, contractAddress, kind); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('getState').inc(1); + + const state = await indexer.getPrevState(blockHash, contractAddress, kind); + + return state && state.block.isComplete ? getResultState(state) : undefined; + }, + + getSyncStatus: async () => { + log('getSyncStatus'); + gqlTotalQueryCount.inc(1); + gqlQueryCount.labels('getSyncStatus').inc(1); + + return indexer.getSyncStatus(); + } + } + }; +}; diff --git a/packages/bayc-watcher/src/schema.gql b/packages/bayc-watcher/src/schema.gql new file mode 100644 index 00000000..199e4bbc --- /dev/null +++ b/packages/bayc-watcher/src/schema.gql @@ -0,0 +1,122 @@ +scalar BigInt + +scalar BigDecimal + +scalar Bytes + +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 = ApprovalEvent | ApprovalForAllEvent | OwnershipTransferredEvent | TransferEvent + +type ApprovalEvent { + owner: String! + approved: String! + tokenId: BigInt! +} + +type ApprovalForAllEvent { + owner: String! + operator: String! + approved: Boolean! +} + +type OwnershipTransferredEvent { + previousOwner: String! + newOwner: String! +} + +type TransferEvent { + from: String! + to: String! + tokenId: BigInt! +} + +type SyncStatus { + latestIndexedBlockHash: String! + latestIndexedBlockNumber: Int! + latestCanonicalBlockHash: String! + latestCanonicalBlockNumber: Int! +} + +type ResultState { + 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!] + supportsInterface(blockHash: String!, contractAddress: String!, interfaceId: String!): ResultBoolean! + balanceOf(blockHash: String!, contractAddress: String!, owner: String!): ResultBigInt! + ownerOf(blockHash: String!, contractAddress: String!, tokenId: BigInt!): ResultString! + getApproved(blockHash: String!, contractAddress: String!, tokenId: BigInt!): ResultString! + isApprovedForAll(blockHash: String!, contractAddress: String!, owner: String!, operator: String!): ResultBoolean! + name(blockHash: String!, contractAddress: String!): ResultString! + symbol(blockHash: String!, contractAddress: String!): ResultString! + tokenURI(blockHash: String!, contractAddress: String!, tokenId: BigInt!): ResultString! + totalSupply(blockHash: String!, contractAddress: String!): ResultBigInt! + tokenOfOwnerByIndex(blockHash: String!, contractAddress: String!, owner: String!, index: BigInt!): ResultBigInt! + tokenByIndex(blockHash: String!, contractAddress: String!, index: BigInt!): ResultBigInt! + baseURI(blockHash: String!, contractAddress: String!): ResultString! + owner(blockHash: String!, contractAddress: String!): ResultString! + getSyncStatus: SyncStatus + getStateByCID(cid: String!): ResultState + getState(blockHash: String!, contractAddress: String!, kind: String): ResultState +} + +type Mutation { + watchContract(address: String!, kind: String!, checkpoint: Boolean!, startingBlock: Int): Boolean! +} + +type Subscription { + onEvent: ResultEvent! +} diff --git a/packages/bayc-watcher/src/server.ts b/packages/bayc-watcher/src/server.ts new file mode 100644 index 00000000..3aeb726e --- /dev/null +++ b/packages/bayc-watcher/src/server.ts @@ -0,0 +1,102 @@ +// +// 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, startGQLMetricsServer } from '@cerc-io/util'; + +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, ethProvider } = await initClients(config); + + const { host, port, kind: watcherKind } = config.server; + + const db = new Database(config.database); + await db.init(); + + // 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 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 indexer = new Indexer(config.server, db, ethClient, ethProvider, jobQueue); + await indexer.init(); + + const eventWatcher = new EventWatcher(config.upstream, ethClient, indexer, pubsub, jobQueue); + + if (watcherKind === KIND_ACTIVE) { + await jobQueue.start(); + // Delete jobs to prevent creating jobs after completion of processing previous block. + await jobQueue.deleteAllJobs(); + 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}`); + }); + + startGQLMetricsServer(config); + + return { app, server }; +}; + +main().then(() => { + log('Starting server...'); +}).catch(err => { + log(err); +}); + +process.on('SIGINT', () => { + log(`Exiting process ${process.pid} with code 0`); + process.exit(0); +}); diff --git a/packages/bayc-watcher/src/types.ts b/packages/bayc-watcher/src/types.ts new file mode 100644 index 00000000..6da3055d --- /dev/null +++ b/packages/bayc-watcher/src/types.ts @@ -0,0 +1,4 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + diff --git a/packages/bayc-watcher/tsconfig.json b/packages/bayc-watcher/tsconfig.json new file mode 100644 index 00000000..9d406d30 --- /dev/null +++ b/packages/bayc-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": ["es2019"], /* 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/**/*"] +}