Compare commits

...
24 Commits
Author SHA1 Message Date
prathamesh 85be626f1c Add an example to query keys for a point (#1)
Part of https://www.notion.so/Subscription-API-for-scry-bindings-1c2a6b22d47280a98bb2e2d06496ac89

Reviewed-on: LaconicNetwork/azimuth-watcher-ts#1
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2025-06-02 11:31:35 +00:00
nabarun 53be2a87c4 Update watchers to add event name filter in eventsInRange GQL query (#50)
* Update azimuth-watcher with latest watcher-ts version

* Update all watchers for event name filter

* Upgrade package version
2025-03-20 21:20:32 +05:30
nabarun 4927f2fba6 Update package versions to handle missing totalDifficulty in block data (#49)
* Update package versions to handle missing totalDifficulty

* Upgrade package versions
2025-03-11 15:43:37 +05:30
prathamesh0andnabarun 92f496a23f Upgrade watcher packages for delayed block processing (#48)
* Upgrade dependencies for delayed block processing

* Update package versions

* Upgrade dependencies for block processing offset option

* Upgrade watcher packages version to 0.2.98

---------

Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
2024-06-13 14:35:34 +05:30
prathamesh0 14fd03d097 Regenerate watchers for updated GQL query metrics (#47)
* Regenerate azimuth watcher

* Regenerate censures watcher

* Regenerate claims watcher

* Regenerate conditional-star-release watcher

* Regenerate delegated-sending watcher

* Regenerate ecliptic watcher

* Regenerate linear-star-release watcher

* Regenerate polls watcher

* Update README

* Update package versions

* Update watcher repo URLs
2024-06-07 15:47:00 +05:30
nabarun 4a3c0a2cd1 Upgrade watcher packages to version 0.2.92 for exposing release version and commit hash metrics (#45)
* Upgrade packages to latest watcher-ts version 0.2.92

* Upgrade version in watchers
2024-05-31 11:41:51 +05:30
prathamesh0 be0dbc1701 Upgrade watcher-ts packages to configure failover RPC endpoints (#44)
* Upgrade watcher-ts dependencies

* Update config and add an indexer method to switch clients

* Update indexer and database methods

* Update package version

* Update event parsing in watcher indexer
2024-05-22 10:33:25 +05:30
prathamesh0 ffd8baa4bc Upgrade watcher-ts packages (#43) 2023-12-19 17:35:52 +05:30
nabarun 0b222eea77 Upgrade watcher package versions to 0.2.77 (#42)
* Upgrade watcher package versions

* Upgrade watcher versions
2023-11-27 10:08:38 +05:30
prathamesh0 a63368eab6 Add instructions to generate the watchers (#41) 2023-11-24 15:14:09 +05:30
nabarun 5402445f44 Update watchers with fix for historical processing sync status update (#40)
* Update azimuth watcher with fix for historical processing sync status

* Upgrade packages in all watchers

* Upgrade package versions to 0.2.76

* Update watcher version to 0.1.2
2023-11-24 12:44:09 +05:30
prathamesh0 fb76365f4e Change back GQL endpoint for gateway server to /graphql (#39) 2023-11-23 18:04:45 +05:30
prathamesh0 581e7d76f9 Change GQL endpoint for gateway server from /graphql to / (#38) 2023-11-23 16:15:57 +05:30
prathamesh0andneeraj 4b1c8c6c90 Regenerate polls-watcher with latest changes in watcher-ts and run in active mode (#37)
* Regenerate polls watcher

* Update config data

* Update README for port

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2023-11-23 11:30:42 +05:30
prathamesh0andneeraj a7cffce8fb Regenerate linear-star-release-watcher with latest changes in watcher-ts and run in active mode (#36)
* Regenerate linear star release watcher

* Update config data

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2023-11-23 10:43:34 +05:30
prathamesh0andneeraj ee0d059090 Regenerate ecliptic-watcher with latest changes in watcher-ts and run in active mode (#35)
* Regenerate ecliptic watcher

* Update config data

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2023-11-23 10:42:37 +05:30
prathamesh0andneeraj 8b236ed7ed Regenerate delegated-sending-watcher with latest changes in watcher-ts and run in active mode (#34)
* Regenerate delegated sending watcher

* Update config data

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2023-11-23 10:41:35 +05:30
prathamesh0andneeraj f987e126fb Regenerate conditional-star-release-watcher with latest changes in watcher-ts and run in active mode (#33)
* Regenerate conditional star release watcher

* Update config data

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2023-11-23 10:38:22 +05:30
prathamesh0andneeraj facde5d370 Regenerate claims-watcher with latest changes in watcher-ts and run in active mode (#32)
* Regenerate claims watcher

* Update config data

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2023-11-23 10:29:42 +05:30
nabarunandneeraj ba4ed24296 Regenerate censures-watcher with latest changes in watcher-ts and run in active mode (#31)
* Regenerate censures watcher

* Update config data

* Update ports in config

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2023-11-23 10:18:27 +05:30
nabarunandneeraj 51d909ac6c Regenerate azimuth-watcher with latest changes in watcher-ts and run in active mode (#30)
* Generate azimuth watcher

* Add block range check in eventsInRange query

* Add more fields in sync status query

* Update config file

* Upgrade watcher-ts packages to 0.2.74

* Avoid hoisting packages to prevent dependency issues

* Set enableState to false

* Change server port to 3001

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2023-11-22 13:43:26 +05:30
Zach e005b7e69d Add runnable default query with info text (#23)
* defaultQuery

* add azimuth examples to default query
2023-08-02 15:19:47 +05:30
Zach 1c42500e2d Update README.md 2023-08-01 15:13:33 -04:00
Zach d8da2de323 Update README.md 2023-08-01 15:13:33 -04:00
129 changed files with 5214 additions and 1475 deletions
+185
View File
@@ -1 +1,186 @@
# azimuth-watcher-ts
Watcher for the Azimuth PKI on Ethereum, used in Urbit identities. Read more about Azimuth:
* [https://developers.urbit.org/reference/azimuth/azimuth](https://developers.urbit.org/reference/azimuth/azimuth)
This app can be run using Stack Orchestrator:
* [Azimuth stack](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/app/data/stacks/azimuth)
It is also hosted at <https://azimuth.dev.vdb.to/graphql>
## Usage
* Query public keys for a point:
```bash
# Example
curl 'https://azimuth.dev.vdb.to/graphql' \
-H 'Content-Type: application/json' \
--data-raw '{"query":"{ azimuthGetKeys(blockHash: \"latest\", contractAddress: \"0x223c067F8CF28ae173EE5CafEa60cA44C335fecB\", _point: 58213) { value { encryptionKey: value0 authenticationKey: value1 cryptoSuiteVersion: value2 keyRevisionNumber: value3 } } }"}' \
| jq
# Response
# {
# "data": {
# "azimuthGetKeys": {
# "value": {
# "encryptionKey": "0xc248f759474b16192bd8bdca0bff1b8bff555cd3d118022095331d6d98690c6d",
# "authenticationKey": "0x21188bac08542730e1c4697636d6fa25968f404470ccf917756f05e28c69045a",
# "cryptoSuiteVersion": "1",
# "keyRevisionNumber": "1"
# }
# }
# }
# }
```
* API params:
* `contractAddress`: Azimuth contract address
* `blockHash`: block hash at which you want to query the contract state
* Example GQL queries:
```gql
{
azimuthIsActive(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0x223c067F8CF28ae173EE5CafEa60cA44C335fecB"
_point: 1
) {
value
}
censuresGetCensuredByCount(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0x325f68d32BdEe6Ed86E7235ff2480e2A433D6189"
_who: 6054
) {
value
}
claimsFindClaim(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0xe7e7f69b34D7d9Bd8d61Fb22C33b22708947971A"
_whose: 1967913144
_protocol: "text"
_claim: "Shrek is NOT Drek!"
) {
value
}
linearStarReleaseVerifyBalance(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0x86cd9cd0992F04231751E3761De45cEceA5d1801"
_participant: "0xbD396c580d868FBbE4a115DD667E756079880801"
) {
value
}
conditionalStarReleaseWithdrawLimit(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0x8C241098C3D3498Fe1261421633FD57986D74AeA"
_participant: "0x7F0584938E649061e80e45cF88E6d8dDDb22f2aB"
_batch: 2
) {
value
}
pollsGetUpgradeProposalCount(
blockHash: "0xeaf611fabbe604932d36b97c89955c091e9582e292b741ebf144962b9ff5c271"
contractAddress: "0x7fEcaB617c868Bb5996d99D95200D2Fa708218e4"
) {
value
}
eclipticBalanceOf(
blockHash: "0x5e82abbe6474caf7b5325022db1d1287ce352488b303685493289770484f54f4"
contractAddress: "0x33EeCbf908478C10614626A9D304bfe18B78DD73"
_owner: "0x4b5E239C1bbb98d44ea23BC9f8eC7584F54096E8"
) {
value
}
delegatedSendingCanSend(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0xf6b461fE1aD4bd2ce25B23Fe0aff2ac19B3dFA76"
_as: 1
_point: 1
) {
value
}
}
```
## Generate Watchers
Steps to generate Azimuth watchers using the code generator ([`@cerc-io/codegen`](https://github.com/cerc-io/watcher-ts/tree/v0.2.76/packages/codegen))
* Clone the original Azimuth repo for required contracts:
```bash
git clone git@github.com:urbit/azimuth.git
# Install dependencies
npm install
# Contracts are located in the contracts folder
```
* Setup `cerc-io/watcher-ts` repo:
```bash
git clone git@github.com:cerc-io/watcher-ts.git
# Install dependencies and build packages
yarn install && yarn build
```
* Create a folder to place all the generated watchers in:
```bash
mkdir -p azimuth-watcher-ts/packages
```
* In `watcher-ts/packages/codegen`, create a `config.yaml` file with required codegen config for generating the watcher for a contract
For example, for `Azimuth` contract:
```yaml
# Contracts to watch (required).
contracts:
# Contract name.
- name: Azimuth
# Contract file path or an url.
path: /home/user/azimuth/contracts/Azimuth.sol
# Contract kind
kind: Azimuth
# Output folder path (logs output using `stdout` if not provided).
outputFolder: /home/user/azimuth-watcher-ts/packages/azimuth-watcher
# Code generation mode [eth_call | storage | all | none] (default: none).
mode: eth_call
# Kind of watcher [lazy | active] (default: active).
kind: active
# Watcher server port (default: 3008).
port: 3001
# Solc version to use (optional)
# If not defined, uses solc version listed in dependencies
solc: v0.4.24+commit.e67f0147
# Flatten the input contract file(s) [true | false] (default: true).
flatten: true
```
Note: Create `.sol` files with the contract code from Etherscan for [`ConditionalStarRelease`](https://etherscan.io/address/0x8C241098C3D3498Fe1261421633FD57986D74AeA#code), [`DelegatedSending`](https://etherscan.io/address/0xf6b461fe1ad4bd2ce25b23fe0aff2ac19b3dfa76#code), [`Ecliptic`](https://etherscan.io/address/ecliptic.eth#code) and [`LinearStarRelease`](https://etherscan.io/address/0x86cd9cd0992F04231751E3761De45cEceA5d1801#code) contracts and use the file path for `contracts.path`
* Run codegen command to generate the watcher:
```bash
# In watcher-ts/packages/codegen
yarn codegen --config-file ./config.yaml
```
* Update `contracts`, `outputFolder` and `port` fields in the config and re-run the codegen command for all other contracts
* Setup the parent folder `/home/user/azimuth-watcher-ts` where all the generated watchers are placed as a monorepo
* The [gateway GQL server](packages/gateway-server) can be used to proxy queries to their respective watchers
+1 -1
View File
@@ -3,6 +3,6 @@
"packages/*"
],
"useWorkspaces": true,
"version": "0.0.0",
"version": "0.1.9",
"npmClient": "yarn"
}
+11 -4
View File
@@ -2,13 +2,20 @@
"name": "azimuth-watcher-ts",
"private": true,
"license": "AGPL-3.0",
"workspaces": [
"packages/*"
],
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/@cerc-io/rpc-eth-client",
"**/typeorm-naming-strategies"
]
},
"scripts": {
"build": "lerna run build --stream",
"lint": "lerna run lint --stream -- --max-warnings=0",
"prepare": "husky install"
"prepare": "husky install",
"version:set": "lerna version --no-git-tag-version"
},
"devDependencies": {
"husky": "^7.0.2",
+2
View File
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+3 -3
View File
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3001/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3001/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3001
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,23 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Number of blocks by which block processing lags behind head
blockProcessingOffset = 0
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/azimuth-watcher",
"version": "0.1.0",
"version": "0.1.9",
"description": "azimuth-watcher",
"private": true,
"main": "dist/index.js",
@@ -28,7 +28,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/cerc-io/watcher-ts.git"
"url": "https://github.com/cerc-io/azimuth-watcher-ts",
"directory": "packages/azimuth-watcher"
},
"author": "",
"license": "AGPL-3.0",
@@ -38,28 +39,28 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@apollo/client": "^3.3.19",
"@cerc-io/cli": "0.2.98-patch.2",
"@cerc-io/ipld-eth-client": "0.2.98-patch.2",
"@cerc-io/solidity-mapper": "0.2.98-patch.2",
"@cerc-io/util": "0.2.98-patch.2",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
+24 -6
View File
@@ -852,10 +852,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -888,10 +888,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -912,6 +912,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -965,8 +983,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -47,6 +47,6 @@ export const canTransfer = fs.readFileSync(path.join(__dirname, 'canTransfer.gql
export const getTransferringForCount = fs.readFileSync(path.join(__dirname, 'getTransferringForCount.gql'), 'utf8');
export const getTransferringFor = fs.readFileSync(path.join(__dirname, 'getTransferringFor.gql'), 'utf8');
export const isOperator = fs.readFileSync(path.join(__dirname, 'isOperator.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');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
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,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import AzimuthArtifacts from './artifacts/Azimuth.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: AzimuthABI } = AzimuthArtifacts;
assert(AzimuthABI);
this._abiMap.set(KIND_AZIMUTH, AzimuthABI);
this._contractMap.set(KIND_AZIMUTH, new ethers.utils.Interface(AzimuthABI));
const AzimuthContractInterface = new ethers.utils.Interface(AzimuthABI);
this._contractMap.set(KIND_AZIMUTH, AzimuthContractInterface);
const AzimuthEventSignatures = Object.values(AzimuthContractInterface.events).map(value => {
return AzimuthContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_AZIMUTH, AzimuthEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -1548,16 +1582,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -1567,20 +1602,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -1588,7 +1637,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -1622,10 +1675,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -1636,8 +1693,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -1652,6 +1709,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -1660,6 +1721,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -1668,8 +1733,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -1694,6 +1759,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -1710,7 +1787,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -1730,8 +1824,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -1739,17 +1833,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -1766,7 +1869,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -1774,4 +1877,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
File diff suppressed because it is too large Load Diff
+14 -10
View File
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -155,13 +155,6 @@ type ResultString {
proof: Proof
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -170,9 +163,20 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
isActive(blockHash: String!, contractAddress: String!, _point: BigInt!): ResultBoolean!
getKeys(blockHash: String!, contractAddress: String!, _point: BigInt!): ResultGetKeysType!
getKeyRevisionNumber(blockHash: String!, contractAddress: String!, _point: BigInt!): ResultBigInt!
@@ -217,9 +221,9 @@ type Query {
getTransferringForCount(blockHash: String!, contractAddress: String!, _proxy: String!): ResultBigInt!
getTransferringFor(blockHash: String!, contractAddress: String!, _proxy: String!): ResultBigIntArray!
isOperator(blockHash: String!, contractAddress: String!, _owner: String!, _operator: String!): ResultBoolean!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
+2
View File
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+3 -3
View File
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3002/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3002/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3002
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/censures-watcher",
"version": "0.1.0",
"version": "0.1.9",
"description": "censures-watcher",
"private": true,
"main": "dist/index.js",
@@ -28,7 +28,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/cerc-io/watcher-ts.git"
"url": "https://github.com/cerc-io/azimuth-watcher-ts",
"directory": "packages/censures-watcher"
},
"author": "",
"license": "AGPL-3.0",
@@ -38,28 +39,28 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@apollo/client": "^3.3.19",
"@cerc-io/cli": "0.2.98-patch.2",
"@cerc-io/ipld-eth-client": "0.2.98-patch.2",
"@cerc-io/solidity-mapper": "0.2.98-patch.2",
"@cerc-io/util": "0.2.98-patch.2",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
+24 -6
View File
@@ -199,10 +199,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -235,10 +235,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -259,6 +259,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -312,8 +330,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -7,6 +7,6 @@ export const getCensuringCount = fs.readFileSync(path.join(__dirname, 'getCensur
export const getCensuring = fs.readFileSync(path.join(__dirname, 'getCensuring.gql'), 'utf8');
export const getCensuredByCount = fs.readFileSync(path.join(__dirname, 'getCensuredByCount.gql'), 'utf8');
export const getCensuredBy = fs.readFileSync(path.join(__dirname, 'getCensuredBy.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');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
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,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import CensuresArtifacts from './artifacts/Censures.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: CensuresABI } = CensuresArtifacts;
assert(CensuresABI);
this._abiMap.set(KIND_CENSURES, CensuresABI);
this._contractMap.set(KIND_CENSURES, new ethers.utils.Interface(CensuresABI));
const CensuresContractInterface = new ethers.utils.Interface(CensuresABI);
this._contractMap.set(KIND_CENSURES, CensuresContractInterface);
const CensuresEventSignatures = Object.values(CensuresContractInterface.events).map(value => {
return CensuresContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_CENSURES, CensuresEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -326,16 +360,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -345,20 +380,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -366,7 +415,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -400,10 +453,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -414,8 +471,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -430,6 +487,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -438,6 +499,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -446,8 +511,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -472,6 +537,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -488,7 +565,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -508,8 +602,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -517,17 +611,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -544,7 +647,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -552,4 +655,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
+176 -70
View File
@@ -3,20 +3,20 @@
//
import assert from 'assert';
import BigInt from 'apollo-type-bigint';
import debug from 'debug';
import Decimal from 'decimal.js';
import {
GraphQLScalarType,
GraphQLResolveInfo
} from 'graphql';
import { GraphQLResolveInfo } from 'graphql';
import { ExpressContext } from 'apollo-server-express';
import winston from 'winston';
import {
ValueResult,
gqlTotalQueryCount,
gqlQueryCount,
gqlQueryDuration,
getResultState,
IndexerInterface,
GraphQLBigInt,
GraphQLBigDecimal,
EventWatcher,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setGQLCacheHints
@@ -26,27 +26,64 @@ import { Indexer } from './indexer';
const log = debug('vulcanize:resolver');
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const executeAndRecordMetrics = async (
indexer: Indexer,
gqlLogger: winston.Logger,
opName: string,
expressContext: ExpressContext,
operation: () => Promise<any>
) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(opName).inc(1);
const endTimer = gqlQueryDuration.labels(opName).startTimer();
try {
const [result, syncStatus] = await Promise.all([
operation(),
indexer.getSyncStatus()
]);
gqlLogger.info({
opName,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
return result;
} catch (error) {
gqlLogger.error({
opName,
error,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
throw error;
} finally {
endTimer();
}
};
export const createResolvers = async (
indexerArg: IndexerInterface,
eventWatcher: EventWatcher,
gqlLogger: winston.Logger
): Promise<any> => {
const indexer = indexerArg as Indexer;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const gqlCacheConfig = indexer.serverConfig.gqlCache;
const gqlCacheConfig = indexer.serverConfig.gql.cache;
return {
BigInt: new BigInt('bigInt'),
BigInt: GraphQLBigInt,
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();
}
}),
BigDecimal: GraphQLBigDecimal,
Event: {
__resolveType: (obj: any) => {
@@ -76,128 +113,197 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
_: any,
{ blockHash, contractAddress, _whose }: { blockHash: string, contractAddress: string, _whose: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getCensuringCount', blockHash, contractAddress, _whose);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getCensuringCount').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getCensuringCount(blockHash, contractAddress, _whose);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getCensuringCount',
expressContext,
async () => indexer.getCensuringCount(blockHash, contractAddress, _whose)
);
},
getCensuring: (
_: any,
{ blockHash, contractAddress, _whose }: { blockHash: string, contractAddress: string, _whose: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getCensuring', blockHash, contractAddress, _whose);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getCensuring').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getCensuring(blockHash, contractAddress, _whose);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getCensuring',
expressContext,
async () => indexer.getCensuring(blockHash, contractAddress, _whose)
);
},
getCensuredByCount: (
_: any,
{ blockHash, contractAddress, _who }: { blockHash: string, contractAddress: string, _who: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getCensuredByCount', blockHash, contractAddress, _who);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getCensuredByCount').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getCensuredByCount(blockHash, contractAddress, _who);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getCensuredByCount',
expressContext,
async () => indexer.getCensuredByCount(blockHash, contractAddress, _who)
);
},
getCensuredBy: (
_: any,
{ blockHash, contractAddress, _who }: { blockHash: string, contractAddress: string, _who: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getCensuredBy', blockHash, contractAddress, _who);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getCensuredBy').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getCensuredBy(blockHash, contractAddress, _who);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getCensuredBy',
expressContext,
async () => indexer.getCensuredBy(blockHash, contractAddress, _who)
);
},
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
events: async (
_: any,
{ blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string },
expressContext: ExpressContext
) => {
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`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'events',
expressContext,
async () => {
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));
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);
eventsInRange: async (
_: any,
{ fromBlockNumber, toBlockNumber, name }: { fromBlockNumber: number, toBlockNumber: number, name?: string },
expressContext: ExpressContext
) => {
log('eventsInRange', fromBlockNumber, toBlockNumber, name);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'eventsInRange',
expressContext,
async () => {
const syncStatus = await indexer.getSyncStatus();
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
if (!syncStatus) {
throw new Error('No blocks processed yet');
}
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
getStateByCID: async (_: any, { cid }: { cid: string }) => {
getStateByCID: async (
_: any,
{ cid }: { cid: string },
expressContext: ExpressContext
) => {
log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getStateByCID',
expressContext,
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
getState: async (
_: any,
{ blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string },
expressContext: ExpressContext
) => {
log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getState',
expressContext,
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getSyncStatus: async () => {
getSyncStatus: async (
_: any,
__: Record<string, never>,
expressContext: ExpressContext
) => {
log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus();
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSyncStatus',
expressContext,
async () => indexer.getSyncStatus()
);
}
}
};
+14 -10
View File
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -66,13 +66,6 @@ type ResultIntArray {
proof: Proof
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -81,16 +74,27 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
getCensuringCount(blockHash: String!, contractAddress: String!, _whose: Int!): ResultBigInt!
getCensuring(blockHash: String!, contractAddress: String!, _whose: Int!): ResultBigIntArray!
getCensuredByCount(blockHash: String!, contractAddress: String!, _who: Int!): ResultBigInt!
getCensuredBy(blockHash: String!, contractAddress: String!, _who: Int!): ResultIntArray!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
+2
View File
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+3 -3
View File
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3003/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3003/graphql
* To watch a contract:
+52 -18
View File
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3003
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/claims-watcher",
"version": "0.1.0",
"version": "0.1.9",
"description": "claims-watcher",
"private": true,
"main": "dist/index.js",
@@ -28,7 +28,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/cerc-io/watcher-ts.git"
"url": "https://github.com/cerc-io/azimuth-watcher-ts",
"directory": "packages/claims-watcher"
},
"author": "",
"license": "AGPL-3.0",
@@ -38,28 +39,28 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@apollo/client": "^3.3.19",
"@cerc-io/cli": "0.2.98-patch.2",
"@cerc-io/ipld-eth-client": "0.2.98-patch.2",
"@cerc-io/solidity-mapper": "0.2.98-patch.2",
"@cerc-io/util": "0.2.98-patch.2",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
+24 -6
View File
@@ -153,10 +153,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -189,10 +189,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -213,6 +213,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -266,8 +284,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -4,6 +4,6 @@ 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 findClaim = fs.readFileSync(path.join(__dirname, 'findClaim.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');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
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,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import ClaimsArtifacts from './artifacts/Claims.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: ClaimsABI } = ClaimsArtifacts;
assert(ClaimsABI);
this._abiMap.set(KIND_CLAIMS, ClaimsABI);
this._contractMap.set(KIND_CLAIMS, new ethers.utils.Interface(ClaimsABI));
const ClaimsContractInterface = new ethers.utils.Interface(ClaimsABI);
this._contractMap.set(KIND_CLAIMS, ClaimsContractInterface);
const ClaimsEventSignatures = Object.values(ClaimsContractInterface.events).map(value => {
return ClaimsContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_CLAIMS, ClaimsEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -234,16 +268,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -253,20 +288,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -274,7 +323,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -308,10 +361,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -322,8 +379,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -338,6 +395,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -346,6 +407,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -354,8 +419,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -380,6 +445,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -396,7 +473,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -416,8 +510,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -425,17 +519,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -452,7 +555,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -460,4 +563,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
+152 -58
View File
@@ -3,20 +3,20 @@
//
import assert from 'assert';
import BigInt from 'apollo-type-bigint';
import debug from 'debug';
import Decimal from 'decimal.js';
import {
GraphQLScalarType,
GraphQLResolveInfo
} from 'graphql';
import { GraphQLResolveInfo } from 'graphql';
import { ExpressContext } from 'apollo-server-express';
import winston from 'winston';
import {
ValueResult,
gqlTotalQueryCount,
gqlQueryCount,
gqlQueryDuration,
getResultState,
IndexerInterface,
GraphQLBigInt,
GraphQLBigDecimal,
EventWatcher,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setGQLCacheHints
@@ -26,27 +26,64 @@ import { Indexer } from './indexer';
const log = debug('vulcanize:resolver');
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const executeAndRecordMetrics = async (
indexer: Indexer,
gqlLogger: winston.Logger,
opName: string,
expressContext: ExpressContext,
operation: () => Promise<any>
) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(opName).inc(1);
const endTimer = gqlQueryDuration.labels(opName).startTimer();
try {
const [result, syncStatus] = await Promise.all([
operation(),
indexer.getSyncStatus()
]);
gqlLogger.info({
opName,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
return result;
} catch (error) {
gqlLogger.error({
opName,
error,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
throw error;
} finally {
endTimer();
}
};
export const createResolvers = async (
indexerArg: IndexerInterface,
eventWatcher: EventWatcher,
gqlLogger: winston.Logger
): Promise<any> => {
const indexer = indexerArg as Indexer;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const gqlCacheConfig = indexer.serverConfig.gqlCache;
const gqlCacheConfig = indexer.serverConfig.gql.cache;
return {
BigInt: new BigInt('bigInt'),
BigInt: GraphQLBigInt,
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();
}
}),
BigDecimal: GraphQLBigDecimal,
Event: {
__resolveType: (obj: any) => {
@@ -76,74 +113,131 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
_: any,
{ blockHash, contractAddress, _whose, _protocol, _claim }: { blockHash: string, contractAddress: string, _whose: bigint, _protocol: string, _claim: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('findClaim', blockHash, contractAddress, _whose, _protocol, _claim);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('findClaim').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.findClaim(blockHash, contractAddress, _whose, _protocol, _claim);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'findClaim',
expressContext,
async () => indexer.findClaim(blockHash, contractAddress, _whose, _protocol, _claim)
);
},
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
events: async (
_: any,
{ blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string },
expressContext: ExpressContext
) => {
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`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'events',
expressContext,
async () => {
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));
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);
eventsInRange: async (
_: any,
{ fromBlockNumber, toBlockNumber, name }: { fromBlockNumber: number, toBlockNumber: number, name?: string },
expressContext: ExpressContext
) => {
log('eventsInRange', fromBlockNumber, toBlockNumber, name);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'eventsInRange',
expressContext,
async () => {
const syncStatus = await indexer.getSyncStatus();
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
if (!syncStatus) {
throw new Error('No blocks processed yet');
}
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
getStateByCID: async (_: any, { cid }: { cid: string }) => {
getStateByCID: async (
_: any,
{ cid }: { cid: string },
expressContext: ExpressContext
) => {
log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getStateByCID',
expressContext,
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
getState: async (
_: any,
{ blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string },
expressContext: ExpressContext
) => {
log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getState',
expressContext,
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getSyncStatus: async () => {
getSyncStatus: async (
_: any,
__: Record<string, never>,
expressContext: ExpressContext
) => {
log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus();
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSyncStatus',
expressContext,
async () => indexer.getSyncStatus()
);
}
}
};
+14 -10
View File
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -59,13 +59,6 @@ type ResultInt {
proof: Proof
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -74,13 +67,24 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
findClaim(blockHash: String!, contractAddress: String!, _whose: BigInt!, _protocol: String!, _claim: String!): ResultInt!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3004/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3004/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3004
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/conditional-star-release-watcher",
"version": "0.1.0",
"version": "0.1.9",
"description": "conditional-star-release-watcher",
"private": true,
"main": "dist/index.js",
@@ -28,7 +28,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/cerc-io/watcher-ts.git"
"url": "https://github.com/cerc-io/azimuth-watcher-ts",
"directory": "packages/conditional-star-release-watcher"
},
"author": "",
"license": "AGPL-3.0",
@@ -38,28 +39,28 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@apollo/client": "^3.3.19",
"@cerc-io/cli": "0.2.98-patch.2",
"@cerc-io/ipld-eth-client": "0.2.98-patch.2",
"@cerc-io/solidity-mapper": "0.2.98-patch.2",
"@cerc-io/util": "0.2.98-patch.2",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
@@ -298,10 +298,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -334,10 +334,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -358,6 +358,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -411,8 +429,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -13,6 +13,6 @@ export const getForfeited = fs.readFileSync(path.join(__dirname, 'getForfeited.g
export const hasForfeitedBatch = fs.readFileSync(path.join(__dirname, 'hasForfeitedBatch.gql'), 'utf8');
export const getRemainingStars = fs.readFileSync(path.join(__dirname, 'getRemainingStars.gql'), 'utf8');
export const getConditionsState = fs.readFileSync(path.join(__dirname, 'getConditionsState.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');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
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,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import ConditionalStarReleaseArtifacts from './artifacts/ConditionalStarRelease.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: ConditionalStarReleaseABI } = ConditionalStarReleaseArtifacts;
assert(ConditionalStarReleaseABI);
this._abiMap.set(KIND_CONDITIONALSTARRELEASE, ConditionalStarReleaseABI);
this._contractMap.set(KIND_CONDITIONALSTARRELEASE, new ethers.utils.Interface(ConditionalStarReleaseABI));
const ConditionalStarReleaseContractInterface = new ethers.utils.Interface(ConditionalStarReleaseABI);
this._contractMap.set(KIND_CONDITIONALSTARRELEASE, ConditionalStarReleaseContractInterface);
const ConditionalStarReleaseEventSignatures = Object.values(ConditionalStarReleaseContractInterface.events).map(value => {
return ConditionalStarReleaseContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_CONDITIONALSTARRELEASE, ConditionalStarReleaseEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -515,16 +549,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -534,20 +569,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -555,7 +604,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -589,10 +642,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -603,8 +660,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -619,6 +676,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -627,6 +688,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -635,8 +700,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -661,6 +726,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -677,7 +754,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -697,8 +791,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -706,17 +800,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -733,7 +836,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -741,4 +844,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
@@ -3,20 +3,20 @@
//
import assert from 'assert';
import BigInt from 'apollo-type-bigint';
import debug from 'debug';
import Decimal from 'decimal.js';
import {
GraphQLScalarType,
GraphQLResolveInfo
} from 'graphql';
import { GraphQLResolveInfo } from 'graphql';
import { ExpressContext } from 'apollo-server-express';
import winston from 'winston';
import {
ValueResult,
gqlTotalQueryCount,
gqlQueryCount,
gqlQueryDuration,
getResultState,
IndexerInterface,
GraphQLBigInt,
GraphQLBigDecimal,
EventWatcher,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setGQLCacheHints
@@ -26,27 +26,64 @@ import { Indexer } from './indexer';
const log = debug('vulcanize:resolver');
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const executeAndRecordMetrics = async (
indexer: Indexer,
gqlLogger: winston.Logger,
opName: string,
expressContext: ExpressContext,
operation: () => Promise<any>
) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(opName).inc(1);
const endTimer = gqlQueryDuration.labels(opName).startTimer();
try {
const [result, syncStatus] = await Promise.all([
operation(),
indexer.getSyncStatus()
]);
gqlLogger.info({
opName,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
return result;
} catch (error) {
gqlLogger.error({
opName,
error,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
throw error;
} finally {
endTimer();
}
};
export const createResolvers = async (
indexerArg: IndexerInterface,
eventWatcher: EventWatcher,
gqlLogger: winston.Logger
): Promise<any> => {
const indexer = indexerArg as Indexer;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const gqlCacheConfig = indexer.serverConfig.gqlCache;
const gqlCacheConfig = indexer.serverConfig.gql.cache;
return {
BigInt: new BigInt('bigInt'),
BigInt: GraphQLBigInt,
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();
}
}),
BigDecimal: GraphQLBigDecimal,
Event: {
__resolveType: (obj: any) => {
@@ -76,236 +113,329 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
_: any,
{ blockHash, contractAddress, _participant, _batch }: { blockHash: string, contractAddress: string, _participant: string, _batch: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('withdrawLimit', blockHash, contractAddress, _participant, _batch);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('withdrawLimit').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.withdrawLimit(blockHash, contractAddress, _participant, _batch);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'withdrawLimit',
expressContext,
async () => indexer.withdrawLimit(blockHash, contractAddress, _participant, _batch)
);
},
verifyBalance: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('verifyBalance', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('verifyBalance').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.verifyBalance(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'verifyBalance',
expressContext,
async () => indexer.verifyBalance(blockHash, contractAddress, _participant)
);
},
getBatches: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getBatches', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getBatches').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getBatches(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getBatches',
expressContext,
async () => indexer.getBatches(blockHash, contractAddress, _participant)
);
},
getBatch: (
_: any,
{ blockHash, contractAddress, _participant, _batch }: { blockHash: string, contractAddress: string, _participant: string, _batch: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getBatch', blockHash, contractAddress, _participant, _batch);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getBatch').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getBatch(blockHash, contractAddress, _participant, _batch);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getBatch',
expressContext,
async () => indexer.getBatch(blockHash, contractAddress, _participant, _batch)
);
},
getWithdrawn: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getWithdrawn', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getWithdrawn').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getWithdrawn(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getWithdrawn',
expressContext,
async () => indexer.getWithdrawn(blockHash, contractAddress, _participant)
);
},
getWithdrawnFromBatch: (
_: any,
{ blockHash, contractAddress, _participant, _batch }: { blockHash: string, contractAddress: string, _participant: string, _batch: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getWithdrawnFromBatch', blockHash, contractAddress, _participant, _batch);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getWithdrawnFromBatch').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getWithdrawnFromBatch(blockHash, contractAddress, _participant, _batch);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getWithdrawnFromBatch',
expressContext,
async () => indexer.getWithdrawnFromBatch(blockHash, contractAddress, _participant, _batch)
);
},
getForfeited: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getForfeited', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getForfeited').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getForfeited(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getForfeited',
expressContext,
async () => indexer.getForfeited(blockHash, contractAddress, _participant)
);
},
hasForfeitedBatch: (
_: any,
{ blockHash, contractAddress, _participant, _batch }: { blockHash: string, contractAddress: string, _participant: string, _batch: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('hasForfeitedBatch', blockHash, contractAddress, _participant, _batch);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('hasForfeitedBatch').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.hasForfeitedBatch(blockHash, contractAddress, _participant, _batch);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'hasForfeitedBatch',
expressContext,
async () => indexer.hasForfeitedBatch(blockHash, contractAddress, _participant, _batch)
);
},
getRemainingStars: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getRemainingStars', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getRemainingStars').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getRemainingStars(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getRemainingStars',
expressContext,
async () => indexer.getRemainingStars(blockHash, contractAddress, _participant)
);
},
getConditionsState: (
_: any,
{ blockHash, contractAddress }: { blockHash: string, contractAddress: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getConditionsState', blockHash, contractAddress);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getConditionsState').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getConditionsState(blockHash, contractAddress);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getConditionsState',
expressContext,
async () => indexer.getConditionsState(blockHash, contractAddress)
);
},
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
events: async (
_: any,
{ blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string },
expressContext: ExpressContext
) => {
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`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'events',
expressContext,
async () => {
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));
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);
eventsInRange: async (
_: any,
{ fromBlockNumber, toBlockNumber, name }: { fromBlockNumber: number, toBlockNumber: number, name?: string },
expressContext: ExpressContext
) => {
log('eventsInRange', fromBlockNumber, toBlockNumber, name);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'eventsInRange',
expressContext,
async () => {
const syncStatus = await indexer.getSyncStatus();
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
if (!syncStatus) {
throw new Error('No blocks processed yet');
}
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
getStateByCID: async (_: any, { cid }: { cid: string }) => {
getStateByCID: async (
_: any,
{ cid }: { cid: string },
expressContext: ExpressContext
) => {
log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getStateByCID',
expressContext,
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
getState: async (
_: any,
{ blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string },
expressContext: ExpressContext
) => {
log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getState',
expressContext,
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getSyncStatus: async () => {
getSyncStatus: async (
_: any,
__: Record<string, never>,
expressContext: ExpressContext
) => {
log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus();
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSyncStatus',
expressContext,
async () => indexer.getSyncStatus()
);
}
}
};
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -93,13 +93,6 @@ type GetConditionsStateType {
value3: [BigInt!]!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -108,9 +101,20 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
withdrawLimit(blockHash: String!, contractAddress: String!, _participant: String!, _batch: Int!): ResultInt!
verifyBalance(blockHash: String!, contractAddress: String!, _participant: String!): ResultBoolean!
getBatches(blockHash: String!, contractAddress: String!, _participant: String!): ResultIntArray!
@@ -121,9 +125,9 @@ type Query {
hasForfeitedBatch(blockHash: String!, contractAddress: String!, _participant: String!, _batch: Int!): ResultBoolean!
getRemainingStars(blockHash: String!, contractAddress: String!, _participant: String!): ResultIntArray!
getConditionsState(blockHash: String!, contractAddress: String!): ResultGetConditionsStateType!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+3 -3
View File
@@ -65,7 +65,7 @@ This watcher has been generated based on the `DelegatedSending` contract deploye
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -83,7 +83,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3005/graphql
* If the watcher is an `active` watcher:
@@ -99,7 +99,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3005/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3005
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/delegated-sending-watcher",
"version": "0.1.0",
"version": "0.1.9",
"description": "delegated-sending-watcher",
"private": true,
"main": "dist/index.js",
@@ -28,7 +28,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/cerc-io/watcher-ts.git"
"url": "https://github.com/cerc-io/azimuth-watcher-ts",
"directory": "packages/delegated-sending-watcher"
},
"author": "",
"license": "AGPL-3.0",
@@ -38,28 +39,28 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@apollo/client": "^3.3.19",
"@cerc-io/cli": "0.2.98-patch.2",
"@cerc-io/ipld-eth-client": "0.2.98-patch.2",
"@cerc-io/solidity-mapper": "0.2.98-patch.2",
"@cerc-io/util": "0.2.98-patch.2",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
@@ -168,10 +168,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -204,10 +204,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -228,6 +228,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -281,8 +299,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -0,0 +1,31 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
import { bigintTransformer, bigintArrayTransformer } from '@cerc-io/util';
@Entity()
@Index(['blockHash', 'contractAddress', '_who'], { unique: true })
export class GetInvited {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar', { length: 66 })
blockHash!: string;
@Column('integer')
blockNumber!: number;
@Column('varchar', { length: 42 })
contractAddress!: string;
@Column('numeric', { transformer: bigintTransformer })
_who!: bigint;
@Column('numeric', { array: true, transformer: bigintArrayTransformer })
value!: bigint[];
@Column('text', { nullable: true })
proof!: string;
}
@@ -0,0 +1,28 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
import { bigintArrayTransformer } from '@cerc-io/util';
@Entity()
@Index(['blockHash', 'contractAddress'], { unique: true })
export class GetInviters {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar', { length: 66 })
blockHash!: string;
@Column('integer')
blockNumber!: number;
@Column('varchar', { length: 42 })
contractAddress!: string;
@Column('numeric', { array: true, transformer: bigintArrayTransformer })
value!: bigint[];
@Column('text', { nullable: true })
proof!: string;
}
@@ -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', '_who'], { unique: true })
export class GetPoolStars {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar', { length: 66 })
blockHash!: string;
@Column('integer')
blockNumber!: number;
@Column('varchar', { length: 42 })
contractAddress!: string;
@Column('numeric', { transformer: bigintTransformer })
_who!: bigint;
@Column('integer', { array: true })
value!: number[];
@Column('text', { nullable: true })
proof!: string;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -5,6 +5,6 @@ export const events = fs.readFileSync(path.join(__dirname, 'events.gql'), 'utf8'
export const eventsInRange = fs.readFileSync(path.join(__dirname, 'eventsInRange.gql'), 'utf8');
export const canSend = fs.readFileSync(path.join(__dirname, 'canSend.gql'), 'utf8');
export const canReceive = fs.readFileSync(path.join(__dirname, 'canReceive.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');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
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,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import DelegatedSendingArtifacts from './artifacts/DelegatedSending.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: DelegatedSendingABI } = DelegatedSendingArtifacts;
assert(DelegatedSendingABI);
this._abiMap.set(KIND_DELEGATEDSENDING, DelegatedSendingABI);
this._contractMap.set(KIND_DELEGATEDSENDING, new ethers.utils.Interface(DelegatedSendingABI));
const DelegatedSendingContractInterface = new ethers.utils.Interface(DelegatedSendingABI);
this._contractMap.set(KIND_DELEGATEDSENDING, DelegatedSendingContractInterface);
const DelegatedSendingEventSignatures = Object.values(DelegatedSendingContractInterface.events).map(value => {
return DelegatedSendingContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_DELEGATEDSENDING, DelegatedSendingEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -264,16 +298,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -283,20 +318,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -304,7 +353,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -338,10 +391,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -352,8 +409,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -368,6 +425,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -376,6 +437,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -384,8 +449,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -410,6 +475,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -426,7 +503,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -446,8 +540,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -455,17 +549,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -482,7 +585,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -490,4 +593,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
@@ -3,20 +3,20 @@
//
import assert from 'assert';
import BigInt from 'apollo-type-bigint';
import debug from 'debug';
import Decimal from 'decimal.js';
import {
GraphQLScalarType,
GraphQLResolveInfo
} from 'graphql';
import { GraphQLResolveInfo } from 'graphql';
import { ExpressContext } from 'apollo-server-express';
import winston from 'winston';
import {
ValueResult,
gqlTotalQueryCount,
gqlQueryCount,
gqlQueryDuration,
getResultState,
IndexerInterface,
GraphQLBigInt,
GraphQLBigDecimal,
EventWatcher,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setGQLCacheHints
@@ -26,27 +26,64 @@ import { Indexer } from './indexer';
const log = debug('vulcanize:resolver');
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const executeAndRecordMetrics = async (
indexer: Indexer,
gqlLogger: winston.Logger,
opName: string,
expressContext: ExpressContext,
operation: () => Promise<any>
) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(opName).inc(1);
const endTimer = gqlQueryDuration.labels(opName).startTimer();
try {
const [result, syncStatus] = await Promise.all([
operation(),
indexer.getSyncStatus()
]);
gqlLogger.info({
opName,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
return result;
} catch (error) {
gqlLogger.error({
opName,
error,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
throw error;
} finally {
endTimer();
}
};
export const createResolvers = async (
indexerArg: IndexerInterface,
eventWatcher: EventWatcher,
gqlLogger: winston.Logger
): Promise<any> => {
const indexer = indexerArg as Indexer;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const gqlCacheConfig = indexer.serverConfig.gqlCache;
const gqlCacheConfig = indexer.serverConfig.gql.cache;
return {
BigInt: new BigInt('bigInt'),
BigInt: GraphQLBigInt,
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();
}
}),
BigDecimal: GraphQLBigDecimal,
Event: {
__resolveType: (obj: any) => {
@@ -76,92 +113,153 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
_: any,
{ blockHash, contractAddress, _as, _point }: { blockHash: string, contractAddress: string, _as: bigint, _point: bigint },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('canSend', blockHash, contractAddress, _as, _point);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('canSend').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.canSend(blockHash, contractAddress, _as, _point);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'canSend',
expressContext,
async () => indexer.canSend(blockHash, contractAddress, _as, _point)
);
},
canReceive: (
_: any,
{ blockHash, contractAddress, _recipient }: { blockHash: string, contractAddress: string, _recipient: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('canReceive', blockHash, contractAddress, _recipient);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('canReceive').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.canReceive(blockHash, contractAddress, _recipient);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'canReceive',
expressContext,
async () => indexer.canReceive(blockHash, contractAddress, _recipient)
);
},
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
events: async (
_: any,
{ blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string },
expressContext: ExpressContext
) => {
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`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'events',
expressContext,
async () => {
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));
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);
eventsInRange: async (
_: any,
{ fromBlockNumber, toBlockNumber, name }: { fromBlockNumber: number, toBlockNumber: number, name?: string },
expressContext: ExpressContext
) => {
log('eventsInRange', fromBlockNumber, toBlockNumber, name);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'eventsInRange',
expressContext,
async () => {
const syncStatus = await indexer.getSyncStatus();
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
if (!syncStatus) {
throw new Error('No blocks processed yet');
}
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
getStateByCID: async (_: any, { cid }: { cid: string }) => {
getStateByCID: async (
_: any,
{ cid }: { cid: string },
expressContext: ExpressContext
) => {
log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getStateByCID',
expressContext,
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
getState: async (
_: any,
{ blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string },
expressContext: ExpressContext
) => {
log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getState',
expressContext,
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getSyncStatus: async () => {
getSyncStatus: async (
_: any,
__: Record<string, never>,
expressContext: ExpressContext
) => {
log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus();
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSyncStatus',
expressContext,
async () => indexer.getSyncStatus()
);
}
}
};
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -54,13 +54,6 @@ type ResultBoolean {
proof: Proof
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -69,14 +62,25 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
canSend(blockHash: String!, contractAddress: String!, _as: BigInt!, _point: BigInt!): ResultBoolean!
canReceive(blockHash: String!, contractAddress: String!, _recipient: String!): ResultBoolean!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
+2
View File
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+3 -3
View File
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3006/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3006/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3006
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/ecliptic-watcher",
"version": "0.1.0",
"version": "0.1.9",
"description": "ecliptic-watcher",
"private": true,
"main": "dist/index.js",
@@ -28,7 +28,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/cerc-io/watcher-ts.git"
"url": "https://github.com/cerc-io/azimuth-watcher-ts",
"directory": "packages/ecliptic-watcher"
},
"author": "",
"license": "AGPL-3.0",
@@ -38,28 +39,28 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@apollo/client": "^3.3.19",
"@cerc-io/cli": "0.2.98-patch.2",
"@cerc-io/ipld-eth-client": "0.2.98-patch.2",
"@cerc-io/solidity-mapper": "0.2.98-patch.2",
"@cerc-io/util": "0.2.98-patch.2",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
+24 -7
View File
@@ -314,10 +314,9 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -350,10 +349,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -374,6 +373,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -427,8 +444,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -14,6 +14,6 @@ export const getApproved = fs.readFileSync(path.join(__dirname, 'getApproved.gql
export const isApprovedForAll = fs.readFileSync(path.join(__dirname, 'isApprovedForAll.gql'), 'utf8');
export const getSpawnLimit = fs.readFileSync(path.join(__dirname, 'getSpawnLimit.gql'), 'utf8');
export const canEscapeTo = fs.readFileSync(path.join(__dirname, 'canEscapeTo.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');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
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,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import EclipticArtifacts from './artifacts/Ecliptic.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: EclipticABI } = EclipticArtifacts;
assert(EclipticABI);
this._abiMap.set(KIND_ECLIPTIC, EclipticABI);
this._contractMap.set(KIND_ECLIPTIC, new ethers.utils.Interface(EclipticABI));
const EclipticContractInterface = new ethers.utils.Interface(EclipticABI);
this._contractMap.set(KIND_ECLIPTIC, EclipticContractInterface);
const EclipticEventSignatures = Object.values(EclipticContractInterface.events).map(value => {
return EclipticContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_ECLIPTIC, EclipticEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -536,16 +570,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -555,20 +590,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -576,7 +625,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -610,10 +663,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -624,8 +681,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -640,6 +697,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -648,6 +709,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -656,8 +721,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -682,6 +747,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -698,7 +775,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -718,8 +812,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -727,17 +821,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -754,7 +857,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -762,4 +865,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
+232 -98
View File
@@ -3,20 +3,20 @@
//
import assert from 'assert';
import BigInt from 'apollo-type-bigint';
import debug from 'debug';
import Decimal from 'decimal.js';
import {
GraphQLScalarType,
GraphQLResolveInfo
} from 'graphql';
import { GraphQLResolveInfo } from 'graphql';
import { ExpressContext } from 'apollo-server-express';
import winston from 'winston';
import {
ValueResult,
gqlTotalQueryCount,
gqlQueryCount,
gqlQueryDuration,
getResultState,
IndexerInterface,
GraphQLBigInt,
GraphQLBigDecimal,
EventWatcher,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setGQLCacheHints
@@ -26,27 +26,64 @@ import { Indexer } from './indexer';
const log = debug('vulcanize:resolver');
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const executeAndRecordMetrics = async (
indexer: Indexer,
gqlLogger: winston.Logger,
opName: string,
expressContext: ExpressContext,
operation: () => Promise<any>
) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(opName).inc(1);
const endTimer = gqlQueryDuration.labels(opName).startTimer();
try {
const [result, syncStatus] = await Promise.all([
operation(),
indexer.getSyncStatus()
]);
gqlLogger.info({
opName,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
return result;
} catch (error) {
gqlLogger.error({
opName,
error,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
throw error;
} finally {
endTimer();
}
};
export const createResolvers = async (
indexerArg: IndexerInterface,
eventWatcher: EventWatcher,
gqlLogger: winston.Logger
): Promise<any> => {
const indexer = indexerArg as Indexer;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const gqlCacheConfig = indexer.serverConfig.gqlCache;
const gqlCacheConfig = indexer.serverConfig.gql.cache;
return {
BigInt: new BigInt('bigInt'),
BigInt: GraphQLBigInt,
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();
}
}),
BigDecimal: GraphQLBigDecimal,
Event: {
__resolveType: (obj: any) => {
@@ -76,254 +113,351 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
_: any,
{ blockHash, contractAddress, _interfaceId }: { blockHash: string, contractAddress: string, _interfaceId: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('supportsInterface', blockHash, contractAddress, _interfaceId);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('supportsInterface').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.supportsInterface(blockHash, contractAddress, _interfaceId);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'supportsInterface',
expressContext,
async () => indexer.supportsInterface(blockHash, contractAddress, _interfaceId)
);
},
name: (
_: any,
{ blockHash, contractAddress }: { blockHash: string, contractAddress: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('name', blockHash, contractAddress);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('name').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.name(blockHash, contractAddress);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'name',
expressContext,
async () => indexer.name(blockHash, contractAddress)
);
},
symbol: (
_: any,
{ blockHash, contractAddress }: { blockHash: string, contractAddress: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('symbol', blockHash, contractAddress);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('symbol').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.symbol(blockHash, contractAddress);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'symbol',
expressContext,
async () => indexer.symbol(blockHash, contractAddress)
);
},
tokenURI: (
_: any,
{ blockHash, contractAddress, _tokenId }: { blockHash: string, contractAddress: string, _tokenId: bigint },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('tokenURI', blockHash, contractAddress, _tokenId);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('tokenURI').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.tokenURI(blockHash, contractAddress, _tokenId);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'tokenURI',
expressContext,
async () => indexer.tokenURI(blockHash, contractAddress, _tokenId)
);
},
balanceOf: (
_: any,
{ blockHash, contractAddress, _owner }: { blockHash: string, contractAddress: string, _owner: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('balanceOf', blockHash, contractAddress, _owner);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('balanceOf').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.balanceOf(blockHash, contractAddress, _owner);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'balanceOf',
expressContext,
async () => indexer.balanceOf(blockHash, contractAddress, _owner)
);
},
ownerOf: (
_: any,
{ blockHash, contractAddress, _tokenId }: { blockHash: string, contractAddress: string, _tokenId: bigint },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('ownerOf', blockHash, contractAddress, _tokenId);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('ownerOf').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.ownerOf(blockHash, contractAddress, _tokenId);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'ownerOf',
expressContext,
async () => indexer.ownerOf(blockHash, contractAddress, _tokenId)
);
},
exists: (
_: any,
{ blockHash, contractAddress, _tokenId }: { blockHash: string, contractAddress: string, _tokenId: bigint },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('exists', blockHash, contractAddress, _tokenId);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('exists').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.exists(blockHash, contractAddress, _tokenId);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'exists',
expressContext,
async () => indexer.exists(blockHash, contractAddress, _tokenId)
);
},
getApproved: (
_: any,
{ blockHash, contractAddress, _tokenId }: { blockHash: string, contractAddress: string, _tokenId: bigint },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getApproved', blockHash, contractAddress, _tokenId);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getApproved').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getApproved(blockHash, contractAddress, _tokenId);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getApproved',
expressContext,
async () => indexer.getApproved(blockHash, contractAddress, _tokenId)
);
},
isApprovedForAll: (
_: any,
{ blockHash, contractAddress, _owner, _operator }: { blockHash: string, contractAddress: string, _owner: string, _operator: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('isApprovedForAll', blockHash, contractAddress, _owner, _operator);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('isApprovedForAll').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.isApprovedForAll(blockHash, contractAddress, _owner, _operator);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'isApprovedForAll',
expressContext,
async () => indexer.isApprovedForAll(blockHash, contractAddress, _owner, _operator)
);
},
getSpawnLimit: (
_: any,
{ blockHash, contractAddress, _point, _time }: { blockHash: string, contractAddress: string, _point: bigint, _time: bigint },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getSpawnLimit', blockHash, contractAddress, _point, _time);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSpawnLimit').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getSpawnLimit(blockHash, contractAddress, _point, _time);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSpawnLimit',
expressContext,
async () => indexer.getSpawnLimit(blockHash, contractAddress, _point, _time)
);
},
canEscapeTo: (
_: any,
{ blockHash, contractAddress, _point, _sponsor }: { blockHash: string, contractAddress: string, _point: bigint, _sponsor: bigint },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('canEscapeTo', blockHash, contractAddress, _point, _sponsor);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('canEscapeTo').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.canEscapeTo(blockHash, contractAddress, _point, _sponsor);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'canEscapeTo',
expressContext,
async () => indexer.canEscapeTo(blockHash, contractAddress, _point, _sponsor)
);
},
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
events: async (
_: any,
{ blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string },
expressContext: ExpressContext
) => {
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`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'events',
expressContext,
async () => {
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));
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);
eventsInRange: async (
_: any,
{ fromBlockNumber, toBlockNumber, name }: { fromBlockNumber: number, toBlockNumber: number, name?: string },
expressContext: ExpressContext
) => {
log('eventsInRange', fromBlockNumber, toBlockNumber, name);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'eventsInRange',
expressContext,
async () => {
const syncStatus = await indexer.getSyncStatus();
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
if (!syncStatus) {
throw new Error('No blocks processed yet');
}
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
getStateByCID: async (_: any, { cid }: { cid: string }) => {
getStateByCID: async (
_: any,
{ cid }: { cid: string },
expressContext: ExpressContext
) => {
log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getStateByCID',
expressContext,
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
getState: async (
_: any,
{ blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string },
expressContext: ExpressContext
) => {
log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getState',
expressContext,
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getSyncStatus: async () => {
getSyncStatus: async (
_: any,
__: Record<string, never>,
expressContext: ExpressContext
) => {
log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus();
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSyncStatus',
expressContext,
async () => indexer.getSyncStatus()
);
}
}
};
+14 -10
View File
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -87,13 +87,6 @@ type ResultBigInt {
proof: Proof
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -102,9 +95,20 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
supportsInterface(blockHash: String!, contractAddress: String!, _interfaceId: String!): ResultBoolean!
name(blockHash: String!, contractAddress: String!): ResultString!
symbol(blockHash: String!, contractAddress: String!): ResultString!
@@ -116,9 +120,9 @@ type Query {
isApprovedForAll(blockHash: String!, contractAddress: String!, _owner: String!, _operator: String!): ResultBoolean!
getSpawnLimit(blockHash: String!, contractAddress: String!, _point: BigInt!, _time: BigInt!): ResultBigInt!
canEscapeTo(blockHash: String!, contractAddress: String!, _point: BigInt!, _sponsor: BigInt!): ResultBoolean!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/gateway-server",
"version": "0.1.0",
"version": "0.1.9",
"main": "index.js",
"license": "AGPL-3.0",
"private": true,
@@ -21,6 +21,7 @@
"is-reachable": "^5.2.1"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
+73 -1
View File
@@ -53,6 +53,78 @@ export const gatewayApp = createYoga({
schema: makeGatewaySchema(),
maskedErrors: false,
graphiql: {
title: 'Azimuth Watchers'
title: 'Azimuth Watchers',
defaultQuery: `#
# Welcome to the Azimuth Watcher for the Urbit PKI on Ethereum
#
# Below are example queries that run out of the box
#
# See the following document for background, rational, and relevant links:
#
# https://github.com/LaconicNetwork/awesome-urbit/blob/main/docs/azimuth-watcher.md
#
{
azimuthIsActive(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0x223c067F8CF28ae173EE5CafEa60cA44C335fecB"
_point: 1
) {
value
}
censuresGetCensuredByCount(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0x325f68d32BdEe6Ed86E7235ff2480e2A433D6189"
_who: 6054
) {
value
}
claimsFindClaim(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0xe7e7f69b34D7d9Bd8d61Fb22C33b22708947971A"
_whose: 1967913144
_protocol: "text"
_claim: "Shrek is NOT Drek!"
) {
value
}
linearStarReleaseVerifyBalance(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0x86cd9cd0992F04231751E3761De45cEceA5d1801"
_participant: "0xbD396c580d868FBbE4a115DD667E756079880801"
) {
value
}
conditionalStarReleaseWithdrawLimit(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0x8C241098C3D3498Fe1261421633FD57986D74AeA"
_participant: "0x7F0584938E649061e80e45cF88E6d8dDDb22f2aB"
_batch: 2
) {
value
}
pollsGetUpgradeProposalCount(
blockHash: "0xeaf611fabbe604932d36b97c89955c091e9582e292b741ebf144962b9ff5c271"
contractAddress: "0x7fEcaB617c868Bb5996d99D95200D2Fa708218e4"
) {
value
}
eclipticBalanceOf(
blockHash: "0x5e82abbe6474caf7b5325022db1d1287ce352488b303685493289770484f54f4"
contractAddress: "0x33EeCbf908478C10614626A9D304bfe18B78DD73"
_owner: "0x4b5E239C1bbb98d44ea23BC9f8eC7584F54096E8"
) {
value
}
delegatedSendingCanSend(
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
contractAddress: "0xf6b461fE1aD4bd2ce25B23Fe0aff2ac19B3dFA76"
_as: 1
_point: 1
) {
value
}
}
`
}
});
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3007/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3007/graphql
* To watch a contract:

Some files were not shown because too many files have changed in this diff Show More