Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
914b71c98a | ||
|
|
08f08316cb | ||
|
|
85be626f1c | ||
|
|
53be2a87c4 | ||
|
|
4927f2fba6 | ||
|
|
92f496a23f | ||
|
|
14fd03d097 | ||
|
|
4a3c0a2cd1 | ||
|
|
be0dbc1701 | ||
|
|
ffd8baa4bc | ||
|
|
0b222eea77 | ||
|
|
a63368eab6 | ||
|
|
5402445f44 | ||
|
|
fb76365f4e | ||
|
|
581e7d76f9 | ||
|
|
4b1c8c6c90 | ||
|
|
a7cffce8fb | ||
|
|
ee0d059090 | ||
|
|
8b236ed7ed | ||
|
|
f987e126fb | ||
|
|
facde5d370 | ||
|
|
ba4ed24296 | ||
|
|
51d909ac6c |
@@ -0,0 +1,29 @@
|
||||
name: Publish azimuth-watcher docker image on release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Run docker build and publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run docker build
|
||||
run: docker build -t cerc/watcher-azimuth -f Dockerfile .
|
||||
- name: Get the version
|
||||
id: vars
|
||||
run: |
|
||||
echo "sha=$(echo ${GITHUB_SHA:0:7})" >> $GITHUB_OUTPUT
|
||||
echo "tag=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_OUTPUT
|
||||
- name: Tag docker image with SHA
|
||||
run: docker tag cerc/watcher-azimuth git.vdb.to/laconicnetwork/cerc/watcher-azimuth:${{steps.vars.outputs.sha}}
|
||||
- name: Tag docker image with release tag
|
||||
run: docker tag git.vdb.to/laconicnetwork/cerc/watcher-azimuth:${{steps.vars.outputs.sha}} git.vdb.to/laconicnetwork/cerc/watcher-azimuth:${{steps.vars.outputs.tag}}
|
||||
- name: Docker Login
|
||||
run: echo ${{ secrets.CICD_PUBLISH_TOKEN }} | docker login https://git.vdb.to -u laconiccicd --password-stdin
|
||||
- name: Docker Push SHA
|
||||
run: docker push git.vdb.to/laconicnetwork/cerc/watcher-azimuth:${{steps.vars.outputs.sha}}
|
||||
- name: Docker Push TAGGED
|
||||
run: docker push git.vdb.to/laconicnetwork/cerc/watcher-azimuth:${{steps.vars.outputs.tag}}
|
||||
@@ -0,0 +1,118 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a monorepo containing blockchain watchers for the Azimuth PKI system used in Urbit identities. It watches multiple Ethereum contracts (Azimuth, Censures, Claims, ConditionalStarRelease, DelegatedSending, Ecliptic, LinearStarRelease, Polls) and provides GraphQL APIs for querying their state.
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Building and Development
|
||||
```bash
|
||||
# Build all packages
|
||||
yarn build
|
||||
# or
|
||||
lerna run build --stream
|
||||
|
||||
# Lint all packages (max warnings = 0)
|
||||
yarn lint
|
||||
# or
|
||||
lerna run lint --stream -- --max-warnings=0
|
||||
|
||||
# Set versions across packages
|
||||
yarn version:set
|
||||
# or
|
||||
lerna version --no-git-tag-version
|
||||
```
|
||||
|
||||
### Individual Watcher Commands
|
||||
Each watcher package supports these commands:
|
||||
```bash
|
||||
# Development server (with hot reload)
|
||||
yarn server:dev
|
||||
|
||||
# Production server
|
||||
yarn server
|
||||
|
||||
# Job runner (processes blockchain events)
|
||||
yarn job-runner:dev # development
|
||||
yarn job-runner # production
|
||||
|
||||
# Watch contract for events
|
||||
yarn watch:contract
|
||||
|
||||
# Fill historical data
|
||||
yarn fill
|
||||
|
||||
# Reset operations
|
||||
yarn reset
|
||||
|
||||
# State management
|
||||
yarn checkpoint:dev
|
||||
yarn export-state:dev
|
||||
yarn import-state:dev
|
||||
|
||||
# Utilities
|
||||
yarn inspect-cid
|
||||
yarn index-block
|
||||
```
|
||||
|
||||
### Gateway Server
|
||||
```bash
|
||||
# Development gateway server (proxies to all watchers)
|
||||
yarn server:dev
|
||||
|
||||
# Production gateway server
|
||||
yarn server
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
- **Lerna-managed** yarn workspaces with 8 watcher packages + 1 gateway server
|
||||
- Each watcher is a standalone service with its own database and GraphQL endpoint
|
||||
- **Gateway server** acts as a unified GraphQL proxy that routes queries to appropriate watchers
|
||||
|
||||
### Watcher Packages
|
||||
Each watcher follows identical structure:
|
||||
- **Port allocation**: azimuth(3001), censures(3002), claims(3003), conditionalStarRelease(3004), delegatedSending(3005), ecliptic(3006), linearStarRelease(3007), polls(3008)
|
||||
- **Database**: Individual PostgreSQL database per watcher
|
||||
- **Configuration**: TOML files in `environments/` directory
|
||||
- **Generated code**: Built from contract ABIs using `@cerc-io/codegen`
|
||||
|
||||
### Key Components Per Watcher
|
||||
- `src/server.ts` - GraphQL server
|
||||
- `src/job-runner.ts` - Event processing worker
|
||||
- `src/indexer.ts` - Blockchain event indexing logic
|
||||
- `src/resolvers.ts` - GraphQL resolvers
|
||||
- `src/entity/` - TypeORM entities for all contract methods
|
||||
- `src/gql/queries/` - GraphQL query definitions
|
||||
- `src/cli/` - Command-line utilities for management
|
||||
|
||||
### Gateway Server Architecture
|
||||
- **Schema stitching**: Combines all watcher schemas with prefixed field names
|
||||
- **Health checking**: Monitors watcher availability before routing
|
||||
- **Configuration**: `src/watchers.json` defines watcher endpoints and prefixes
|
||||
- **GraphQL proxy**: Routes queries like `azimuthGetKeys` to azimuth-watcher at localhost:3001
|
||||
|
||||
### Data Flow
|
||||
1. **Event Processing**: job-runner fetches Ethereum events → processes through indexer → stores in database
|
||||
2. **Query Processing**: GraphQL queries → gateway server → appropriate watcher → database → response
|
||||
3. **State Management**: Supports checkpointing, state export/import for data recovery
|
||||
|
||||
## Configuration Notes
|
||||
|
||||
### Environment Setup
|
||||
- Each watcher requires PostgreSQL database (configurable in `environments/local.toml`)
|
||||
- Requires Ethereum RPC endpoint (ipld-eth-server or standard RPC)
|
||||
- Gateway server expects all watchers running on their designated ports
|
||||
|
||||
### Development Workflow
|
||||
- Start individual watchers with `yarn server:dev` and `yarn job-runner:dev`
|
||||
- Start gateway server for unified GraphQL endpoint
|
||||
- Use `yarn fill` to sync historical blockchain data
|
||||
- Monitor with debug logs using `DEBUG=vulcanize:*`
|
||||
|
||||
### Generated Watcher Creation
|
||||
Watchers are generated using `@cerc-io/codegen` from contract ABIs. The process involves creating config.yaml files specifying contract paths, output folders, and generation modes (eth_call/storage/all).
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
FROM node:18.16.0-alpine3.16
|
||||
|
||||
RUN apk --update --no-cache add git python3 alpine-sdk jq
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
# Get the latest Git commit hash and set it in package.json of all watchers
|
||||
RUN COMMIT_HASH=$(git rev-parse HEAD) && \
|
||||
find . -name 'package.json' -exec sh -c ' \
|
||||
for packageFile; do \
|
||||
jq --arg commitHash "$0" ".commitHash = \$commitHash" "$packageFile" > "$packageFile.tmp" && \
|
||||
mv "$packageFile.tmp" "$packageFile"; \
|
||||
done \
|
||||
' "$COMMIT_HASH" {} \;
|
||||
|
||||
RUN echo "installing dependencies" && \
|
||||
yarn
|
||||
|
||||
RUN echo "Building azimuth-watcher and gateway-server" && \
|
||||
yarn build --scope @cerc-io/azimuth-watcher --scope @cerc-io/gateway-server
|
||||
|
||||
RUN echo "Install toml-js to update watcher config files" && \
|
||||
yarn add --dev --ignore-workspace-root-check toml-js
|
||||
@@ -1,21 +1,143 @@
|
||||
# azimuth-watcher-ts
|
||||
|
||||
Watcher for the Azimuth PKI on Ethereum, used in Urbit identities. Read more about Azimuth:
|
||||
A comprehensive blockchain indexing and querying system for [Azimuth](https://docs.urbit.org/system/identity), Urbit's identity layer on Ethereum. This system monitors multiple Ethereum smart contracts that make up the Azimuth PKI and provides a unified GraphQL API for querying Urbit identity information.
|
||||
|
||||
- [https://developers.urbit.org/reference/azimuth/azimuth](https://developers.urbit.org/reference/azimuth/azimuth)
|
||||
## What is Azimuth?
|
||||
|
||||
This app can be run using Stack Orchestrator:
|
||||
Azimuth is Urbit's public key infrastructure (PKI) that lives on Ethereum. It's a set of smart contracts that manage Urbit identities called "points" (similar to usernames), their ownership, cryptographic keys, and hierarchical relationships. By storing identity data on Ethereum, the system is decentralized and censorship-resistant.
|
||||
|
||||
- [Azimuth stack](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/app/data/stacks/azimuth)
|
||||
## What are Watchers?
|
||||
|
||||
It is also hosted [here](https://azimuth.dev.vdb.to/graphql)
|
||||
Watchers are services that continuously monitor smart contracts on Ethereum, index their events and state changes, and provide efficient APIs for querying blockchain data. Instead of directly querying the Ethereum blockchain (which is slow and expensive), applications can query watchers for fast, indexed access to current and historical blockchain state.
|
||||
|
||||
## Usage
|
||||
## System Architecture
|
||||
|
||||
Here are some example queries:
|
||||
This system consists of **8 specialized watchers** that each monitor different Azimuth smart contracts:
|
||||
|
||||
- **azimuth-watcher** - Core identity operations (ownership, keys, sponsorship)
|
||||
- **censures-watcher** - Reputation and censure system
|
||||
- **claims-watcher** - On-chain claims and metadata
|
||||
- **ecliptic-watcher** - Galaxy operations and governance
|
||||
- **polls-watcher** - Voting and governance proposals
|
||||
- **conditional-star-release-watcher** - Conditional star distribution
|
||||
- **linear-star-release-watcher** - Linear star distribution
|
||||
- **delegated-sending-watcher** - Delegated operations
|
||||
|
||||
Plus a **gateway server** that provides a unified GraphQL endpoint routing queries to the appropriate watcher.
|
||||
|
||||
## Hosted Service
|
||||
|
||||
A public instance is available at: **<https://azimuth.dev.vdb.to/graphql>**
|
||||
|
||||
You can also run the system locally using [Stack Orchestrator](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/app/data/stacks/azimuth).
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Querying Urbit Point Information
|
||||
|
||||
The **azimuth-watcher** is the primary service for querying Urbit identity data. Here are the most common operations:
|
||||
|
||||
#### 1. Get Point Owner and Basic Info
|
||||
|
||||
```bash
|
||||
# Check who owns a specific Urbit point
|
||||
# Example:
|
||||
curl 'https://azimuth.dev.vdb.to/graphql' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"query":"{ azimuthGetOwner(blockHash: \"latest\", contractAddress: \"0x223c067F8CF28ae173EE5CafEa60cA44C335fecB\", _point: 1234) { value } }"}' \
|
||||
| jq
|
||||
|
||||
# Response:
|
||||
# {
|
||||
# "data": {
|
||||
# "azimuthGetOwner": {
|
||||
# "value": "0x4b22764F2Db640aB4d0Ecfd0F84344F3CB5C3715"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
#### 2. Get Cryptographic Keys for a Point
|
||||
|
||||
```bash
|
||||
# Get encryption and authentication keys for networking
|
||||
# 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"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
#### 3. Check Point Status
|
||||
|
||||
```bash
|
||||
# Check if a point is active (booted) and has a sponsor
|
||||
# Example:
|
||||
curl 'https://azimuth.dev.vdb.to/graphql' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"query":"{ azimuthIsActive(blockHash: \"latest\", contractAddress: \"0x223c067F8CF28ae173EE5CafEa60cA44C335fecB\", _point: 1234) { value } azimuthHasSponsor(blockHash: \"latest\", contractAddress: \"0x223c067F8CF28ae173EE5CafEa60cA44C335fecB\", _point: 1234) { value } azimuthGetSponsor(blockHash: \"latest\", contractAddress: \"0x223c067F8CF28ae173EE5CafEa60cA44C335fecB\", _point: 1234) { value } }"}' \
|
||||
| jq
|
||||
|
||||
# Response:
|
||||
# {
|
||||
# "data": {
|
||||
# "azimuthIsActive": {
|
||||
# "value": true
|
||||
# },
|
||||
# "azimuthHasSponsor": {
|
||||
# "value": true
|
||||
# },
|
||||
# "azimuthGetSponsor": {
|
||||
# "value": "210"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
#### 4. Get All Points Owned by an Address
|
||||
|
||||
```bash
|
||||
# Find all Urbit points owned by an Ethereum address
|
||||
# Example:
|
||||
curl 'https://azimuth.dev.vdb.to/graphql' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"query":"{ azimuthGetOwnedPoints(blockHash: \"latest\", contractAddress: \"0x223c067F8CF28ae173EE5CafEa60cA44C335fecB\", _whose: \"0x1234567890123456789012345678901234567890\") { value } }"}' \
|
||||
| jq
|
||||
|
||||
# Response:
|
||||
# {
|
||||
# "data": {
|
||||
# "azimuthGetOwnedPoints": {
|
||||
# "value": [
|
||||
# "57965",
|
||||
# "1234"
|
||||
# ]
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
#### 5. Multi-Watcher Queries
|
||||
|
||||
The gateway server allows querying multiple watchers in a single request:
|
||||
|
||||
```graphql
|
||||
{
|
||||
# Check point status (azimuth-watcher)
|
||||
azimuthIsActive(
|
||||
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
|
||||
contractAddress: "0x223c067F8CF28ae173EE5CafEa60cA44C335fecB"
|
||||
@@ -23,13 +145,17 @@ Here are some example queries:
|
||||
) {
|
||||
value
|
||||
}
|
||||
|
||||
# Check censure count (censures-watcher)
|
||||
censuresGetCensuredByCount(
|
||||
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
|
||||
contractAddress: "0x325f68d32BdEe6Ed86E7235ff2480e2A433D6189"
|
||||
_who: 6054
|
||||
) {
|
||||
value
|
||||
}
|
||||
} }
|
||||
|
||||
# Find a claim (claims-watcher)
|
||||
claimsFindClaim(
|
||||
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
|
||||
contractAddress: "0xe7e7f69b34D7d9Bd8d61Fb22C33b22708947971A"
|
||||
@@ -39,6 +165,8 @@ Here are some example queries:
|
||||
) {
|
||||
value
|
||||
}
|
||||
|
||||
# Check star release balance (linear-star-release-watcher)
|
||||
linearStarReleaseVerifyBalance(
|
||||
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
|
||||
contractAddress: "0x86cd9cd0992F04231751E3761De45cEceA5d1801"
|
||||
@@ -46,6 +174,8 @@ Here are some example queries:
|
||||
) {
|
||||
value
|
||||
}
|
||||
|
||||
# Check conditional star release (conditional-star-release-watcher)
|
||||
conditionalStarReleaseWithdrawLimit(
|
||||
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
|
||||
contractAddress: "0x8C241098C3D3498Fe1261421633FD57986D74AeA"
|
||||
@@ -54,12 +184,16 @@ Here are some example queries:
|
||||
) {
|
||||
value
|
||||
}
|
||||
|
||||
# Check governance proposals (polls-watcher)
|
||||
pollsGetUpgradeProposalCount(
|
||||
blockHash: "0xeaf611fabbe604932d36b97c89955c091e9582e292b741ebf144962b9ff5c271"
|
||||
contractAddress: "0x7fEcaB617c868Bb5996d99D95200D2Fa708218e4"
|
||||
) {
|
||||
value
|
||||
}
|
||||
|
||||
# Check NFT balance (ecliptic-watcher)
|
||||
eclipticBalanceOf(
|
||||
blockHash: "0x5e82abbe6474caf7b5325022db1d1287ce352488b303685493289770484f54f4"
|
||||
contractAddress: "0x33EeCbf908478C10614626A9D304bfe18B78DD73"
|
||||
@@ -67,6 +201,8 @@ Here are some example queries:
|
||||
) {
|
||||
value
|
||||
}
|
||||
|
||||
# Check delegation permissions (delegated-sending-watcher)
|
||||
delegatedSendingCanSend(
|
||||
blockHash: "0x2461e78f075e618173c524b5ab4309111001517bb50cfd1b3505aed5433cf5f9"
|
||||
contractAddress: "0xf6b461fE1aD4bd2ce25B23Fe0aff2ac19B3dFA76"
|
||||
@@ -77,3 +213,29 @@ Here are some example queries:
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Understanding Query Parameters
|
||||
|
||||
All queries require these standard parameters:
|
||||
|
||||
- **`blockHash`**: Use `"latest"` for current state, or a specific block hash for historical queries
|
||||
- **`contractAddress`**: Azimuth contract address (`0x223c067F8CF28ae173EE5CafEa60cA44C335fecB`)
|
||||
- **`_point`**: The Urbit point number you're querying
|
||||
- **`_whose`**: Ethereum address when querying by owner
|
||||
|
||||
## How It Works
|
||||
|
||||
### Data Source
|
||||
|
||||
The watchers continuously monitor Ethereum smart contracts by connecting to Ethereum RPC endpoint(s), indexing blockchain events and state changes.
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Indexing**: Job runners fetch Ethereum events and blocks from RPC endpoint(s)
|
||||
2. **Processing**: Events are processed and state changes stored in PostgreSQL databases
|
||||
3. **Querying**: GraphQL servers provide fast, indexed access to current and historical blockchain state
|
||||
4. **Gateway**: Unified endpoint routes queries to appropriate specialized watchers
|
||||
|
||||
### Storage
|
||||
|
||||
Each watcher maintains its own PostgreSQL database for efficient querying and data isolation.
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Generate Azimuth Watchers
|
||||
|
||||
* 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
@@ -3,6 +3,6 @@
|
||||
"packages/*"
|
||||
],
|
||||
"useWorkspaces": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.10",
|
||||
"npmClient": "yarn"
|
||||
}
|
||||
|
||||
+11
-4
@@ -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",
|
||||
|
||||
@@ -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: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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cerc-io/azimuth-watcher",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.10",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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: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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user