Compare commits

...
26 Commits
Author SHA1 Message Date
nabarun 699bf8ee9f Upgrade watcher-ts packages to fix block processing (#8)
Publish azimuth-watcher docker image on release / Run docker build and publish (release) Successful in 4m52s
Part of milestone 6 (Testnet)

Reviewed-on: #8
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2025-11-18 10:05:50 +00:00
nabarun 8ab8f8e261 Upgrade watcher-ts packages for handling network error (#6)
Publish azimuth-watcher docker image on release / Run docker build and publish (release) Successful in 5m10s
Part of https://plan.wireit.in/deepstack/browse/VUL-271/ and milestone 6 (Testnet)

Reviewed-on: #6
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2025-11-13 08:42:43 +00:00
nabarunandpranav 48210eed8e Add GQL mutation to mock sponsorship and ownership change events (#4)
Publish azimuth-watcher docker image on release / Run docker build and publish (release) Successful in 5m20s
Part of https://plan.wireit.in/deepstack/browse/VUL-266/

Co-authored-by: Pranav <jadhavpranav89@gmail.com>
Reviewed-on: #4
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2025-10-24 10:20:05 +00:00
nabarun 914b71c98a Add CI for publishing docker image (#3)
Publish azimuth-watcher docker image on release / Run docker build and publish (release) Successful in 2m5s
Reviewed-on: #3
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2025-07-17 02:46:12 +00:00
prathamesh 08f08316cb Add generated docs (#2)
Part of https://www.notion.so/Implement-stacks-1b5a6b22d472806a82f5dafed6955138

Reviewed-on: #2
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2025-06-11 08:46:56 +00:00
prathamesh 85be626f1c Add an example to query keys for a point (#1)
Part of https://www.notion.so/Subscription-API-for-scry-bindings-1c2a6b22d47280a98bb2e2d06496ac89

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

* Update all watchers for event name filter

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

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

* Update package versions

* Upgrade dependencies for block processing offset option

* Upgrade watcher packages version to 0.2.98

---------

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

* Regenerate censures watcher

* Regenerate claims watcher

* Regenerate conditional-star-release watcher

* Regenerate delegated-sending watcher

* Regenerate ecliptic watcher

* Regenerate linear-star-release watcher

* Regenerate polls watcher

* Update README

* Update package versions

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

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

* Update config and add an indexer method to switch clients

* Update indexer and database methods

* Update package version

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

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

* Upgrade packages in all watchers

* Upgrade package versions to 0.2.76

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

* Update config data

* Update README for port

---------

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

* Update config data

---------

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

* Update config data

---------

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

* Update config data

---------

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

* Update config data

---------

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

* Update config data

---------

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

* Update config data

* Update ports in config

---------

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

* Add block range check in eventsInRange query

* Add more fields in sync status query

* Update config file

* Upgrade watcher-ts packages to 0.2.74

* Avoid hoisting packages to prevent dependency issues

* Set enableState to false

* Change server port to 3001

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2023-11-22 13:43:26 +05:30
134 changed files with 5717 additions and 1482 deletions
+29
View File
@@ -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}}
+187
View File
@@ -0,0 +1,187 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What is Azimuth?
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.
## 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.
**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.
## 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 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
The gateway server runs on port 4000 by default.
```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
- **Port**: Runs on port 4000 by default (http://localhost:4000/graphql)
- **Schema stitching**: Combines all watcher schemas with prefixed field names
- **Health checking**: Monitors watcher availability before routing
- **Configuration**: `packages/gateway-server/src/watchers.json` defines watcher endpoints and prefixes
- **GraphQL proxy**: Routes queries like `azimuthGetKeys` to azimuth-watcher at localhost:3001
- **Query prefixing**: Each watcher's queries are prefixed (e.g., `azimuthGetKeys`, `censuresGetCensuredByCount`, `claimsFindClaim`)
### 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).
## Docker & Deployment
### Building Docker Images
The project includes a Dockerfile for building production-ready container images:
```bash
# Build the watcher-azimuth image
docker build -t cerc/watcher-azimuth -f Dockerfile .
```
The Dockerfile:
- Uses Node.js 18.16.0 on Alpine Linux 3.16
- Installs build dependencies (git, python3, alpine-sdk, jq)
- Embeds the Git commit hash in all package.json files
- Builds only azimuth-watcher and gateway-server packages
- Includes toml-js for runtime configuration updates
- Results in a ~1.1GB production image
### CI/CD Pipeline
The project uses GitHub Actions for automated Docker image publishing:
- **Trigger**: On release publication (tags)
- **Workflow**: `.gitea/workflows/docker-image.yml`
- **Outputs**:
- Image tagged with git SHA (e.g., `git.vdb.to/laconicnetwork/cerc/watcher-azimuth:abc1234`)
- Image tagged with release version (e.g., `git.vdb.to/laconicnetwork/cerc/watcher-azimuth:v0.1.10`)
- **Registry**: git.vdb.to
To trigger a release build:
1. Create and push a new git tag
2. Publish the release on GitHub
3. CI will automatically build and push Docker images
## Git Hooks
The project uses [Husky](https://typicode.github.io/husky/) for Git hooks:
- **Pre-commit hook**: Automatically runs `yarn lint` before every commit
- **Configuration**: `.husky/pre-commit`
- **Setup**: Run `yarn install` or `yarn prepare` to install hooks
- **Bypass**: Use `git commit --no-verify` to skip hooks (not recommended)
This ensures code quality by enforcing linting rules before code is committed.
## Key Technologies
- **Language**: TypeScript 5.0+
- **Database**: PostgreSQL with TypeORM 0.2.37
- **Blockchain**: ethers.js 5.4+ for Ethereum interaction
- **GraphQL**: graphql 15.5+ with custom resolvers
- **API Framework**: @cerc-io packages for watcher infrastructure
- **Monorepo**: Lerna 6.6+ with Yarn workspaces
- **Base Image**: Node.js 18.16.0 on Alpine Linux (Docker)
+25
View File
@@ -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
+170 -8
View File
@@ -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.
+76
View File
@@ -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
View File
@@ -3,6 +3,6 @@
"packages/*"
],
"useWorkspaces": true,
"version": "0.0.0",
"version": "0.2.1",
"npmClient": "yarn"
}
+11 -4
View File
@@ -2,13 +2,20 @@
"name": "azimuth-watcher-ts",
"private": true,
"license": "AGPL-3.0",
"workspaces": [
"packages/*"
],
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/@cerc-io/rpc-eth-client",
"**/typeorm-naming-strategies"
]
},
"scripts": {
"build": "lerna run build --stream",
"lint": "lerna run lint --stream -- --max-warnings=0",
"prepare": "husky install"
"prepare": "husky install",
"version:set": "lerna version --no-git-tag-version"
},
"devDependencies": {
"husky": "^7.0.2",
+2
View File
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+131
View File
@@ -0,0 +1,131 @@
# Mock Sponsorship Events
This document describes how to inject mock sponsorship change events into the azimuth-watcher for testing purposes.
## Overview
The azimuth-watcher supports injecting mock events for sponsorship changes (EscapeRequested, EscapeAccepted, EscapeCanceled, LostSponsor) via a GraphQL mutation.
## Usage
### Inject Mock Events
Open the GraphQL playground and run the `injectMockEvents` mutation:
```graphql
mutation {
injectMockEvents(
events: [
{
type: ESCAPE_REQUESTED
point: 256
sponsor: 1
blockNumber: 1000
}
{
type: ESCAPE_ACCEPTED
point: 256
sponsor: 1
blockNumber: 1001
}
]
) {
success
eventsInjected
}
}
```
## Event Types
The following event types are supported:
### Sponsorship Events
- `ESCAPE_REQUESTED` - Star requests to change sponsor
- `ESCAPE_ACCEPTED` - New galaxy accepts the star
- `ESCAPE_CANCELED` - Escape request is canceled (by star or rejected by galaxy)
- `LOST_SPONSOR` - Current galaxy stops sponsoring the star
### Ownership Events
- `OWNER_CHANGED` - Ownership of a point changes
## Example Scenarios
### Successful Sponsor Change
```graphql
mutation {
injectMockEvents(
events: [
{ type: ESCAPE_REQUESTED, point: 256, sponsor: 1, blockNumber: 1000 }
{ type: ESCAPE_ACCEPTED, point: 256, sponsor: 1, blockNumber: 1001 }
]
) {
success
eventsInjected
}
}
```
### Galaxy Detaches Star
```graphql
mutation {
injectMockEvents(
events: [
{ type: LOST_SPONSOR, point: 512, sponsor: 0, blockNumber: 2000 }
]
) {
success
eventsInjected
}
}
```
### Canceled Escape
```graphql
mutation {
injectMockEvents(
events: [
{ type: ESCAPE_REQUESTED, point: 768, sponsor: 2, blockNumber: 3000 }
{ type: ESCAPE_CANCELED, point: 768, sponsor: 2, blockNumber: 3001 }
]
) {
success
eventsInjected
}
}
```
### Ownership Change
```graphql
mutation {
injectMockEvents(
events: [
{ type: OWNER_CHANGED, point: 256, owner: "0x1234567890123456789012345678901234567890", blockNumber: 4000 }
]
) {
success
eventsInjected
}
}
```
## Testing from zenithd
1. Set the azimuth-watcher endpoint:
```bash
export AZIMUTH_WATCHER_ENDPOINT=http://localhost:3001/graphql
```
2. Inject mock events via GraphQL playground (step 2 above)
3. Run zenithd tests - it will receive the mock events through normal GraphQL queries
## Files Modified
- `src/mock-event-store.ts` - In-memory event storage
- `src/schema.gql` - Added mutation and types
- `src/resolvers.ts` - Added mutation resolver and modified `eventsInRange` query
+3 -3
View File
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3001/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3001/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3001
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,34 @@
# 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
# Flag to enable injecting mock events for testing
enableMockEvents = false
# 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 +58,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 +84,23 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Number of blocks by which block processing lags behind head
blockProcessingOffset = 0
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/azimuth-watcher",
"version": "0.1.0",
"version": "0.2.1",
"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.5",
"@cerc-io/ipld-eth-client": "0.2.98-patch.5",
"@cerc-io/solidity-mapper": "0.2.98-patch.5",
"@cerc-io/util": "0.2.98-patch.5",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
+24 -6
View File
@@ -852,10 +852,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -888,10 +888,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -912,6 +912,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -965,8 +983,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -47,6 +47,6 @@ export const canTransfer = fs.readFileSync(path.join(__dirname, 'canTransfer.gql
export const getTransferringForCount = fs.readFileSync(path.join(__dirname, 'getTransferringForCount.gql'), 'utf8');
export const getTransferringFor = fs.readFileSync(path.join(__dirname, 'getTransferringFor.gql'), 'utf8');
export const isOperator = fs.readFileSync(path.join(__dirname, 'isOperator.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
export const getStateByCID = fs.readFileSync(path.join(__dirname, 'getStateByCID.gql'), 'utf8');
export const getState = fs.readFileSync(path.join(__dirname, 'getState.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import { EthClient } from '@cerc-io/ipld-eth-client';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import {
Indexer as BaseIndexer,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import AzimuthArtifacts from './artifacts/Azimuth.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: AzimuthABI } = AzimuthArtifacts;
assert(AzimuthABI);
this._abiMap.set(KIND_AZIMUTH, AzimuthABI);
this._contractMap.set(KIND_AZIMUTH, new ethers.utils.Interface(AzimuthABI));
const AzimuthContractInterface = new ethers.utils.Interface(AzimuthABI);
this._contractMap.set(KIND_AZIMUTH, AzimuthContractInterface);
const AzimuthEventSignatures = Object.values(AzimuthContractInterface.events).map(value => {
return AzimuthContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_AZIMUTH, AzimuthEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -1548,16 +1582,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -1567,20 +1602,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -1588,7 +1637,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -1622,10 +1675,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -1636,8 +1693,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -1652,6 +1709,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -1660,6 +1721,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -1668,8 +1733,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -1694,6 +1759,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -1710,7 +1787,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -1730,8 +1824,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -1739,17 +1833,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -1766,7 +1869,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -1774,4 +1877,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
@@ -0,0 +1,105 @@
//
// Copyright 2025 DeepStack
//
import { ResultEvent } from '@cerc-io/util';
const AZIMUTH_CONTRACT = '0x223c067F8CF28ae173EE5CafEa60cA44C335fecB';
const MOCK_TX_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000';
const MOCK_FROM_ADDRESS = '0x0000000000000000000000000000000000000000';
export type MockEventType = 'ESCAPE_REQUESTED' | 'ESCAPE_ACCEPTED' | 'ESCAPE_CANCELED' | 'LOST_SPONSOR' | 'OWNER_CHANGED';
export interface MockEventInput {
type: MockEventType;
point: number;
sponsor?: number;
owner?: string;
blockNumber: number;
}
class MockEventStore {
private events: ResultEvent[] = [];
addEvents (inputs: MockEventInput[]): number {
const newEvents = inputs.map((input, index) => this.createMockEvent(input, index));
this.events.push(...newEvents);
return newEvents.length;
}
getEventsInRange (fromBlock: number, toBlock: number, eventName?: string): ResultEvent[] {
return this.events.filter(event => {
const blockNumber = event.block.number;
const matchesRange = blockNumber >= fromBlock && blockNumber <= toBlock;
const matchesName = !eventName || event.event.__typename === `${eventName}Event`;
return matchesRange && matchesName;
});
}
private createMockEvent (input: MockEventInput, eventIndex: number): ResultEvent {
const { type, point, sponsor, owner, blockNumber } = input;
const blockHash = `0x${blockNumber.toString(16).padStart(64, '0')}`;
const parentHash = `0x${(blockNumber - 1).toString(16).padStart(64, '0')}`;
return {
block: {
cid: null,
hash: blockHash,
number: blockNumber,
timestamp: Math.floor(Date.now() / 1000),
parentHash
},
tx: {
hash: MOCK_TX_HASH,
index: 0,
from: MOCK_FROM_ADDRESS,
to: AZIMUTH_CONTRACT
},
contract: AZIMUTH_CONTRACT,
eventIndex,
eventSignature: '',
event: this.createEventData(type, point, sponsor, owner),
proof: ''
};
}
private createEventData (type: MockEventType, point: number, sponsor?: number, owner?: string): any {
switch (type) {
case 'ESCAPE_REQUESTED':
return {
__typename: 'EscapeRequestedEvent',
point: BigInt(point),
sponsor: BigInt(sponsor ?? 0)
};
case 'ESCAPE_ACCEPTED':
return {
__typename: 'EscapeAcceptedEvent',
point: BigInt(point),
sponsor: BigInt(sponsor ?? 0)
};
case 'ESCAPE_CANCELED':
return {
__typename: 'EscapeCanceledEvent',
point: BigInt(point),
sponsor: BigInt(sponsor ?? 0)
};
case 'LOST_SPONSOR':
return {
__typename: 'LostSponsorEvent',
point: BigInt(point),
sponsor: BigInt(sponsor ?? 0)
};
case 'OWNER_CHANGED':
return {
__typename: 'OwnerChangedEvent',
point: BigInt(point),
owner: owner ?? ''
};
default:
throw new Error(`Unknown mock event type: ${type}`);
}
}
}
export const mockEventStore = new MockEventStore();
File diff suppressed because it is too large Load Diff
+36 -10
View File
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -155,13 +155,6 @@ type ResultString {
proof: Proof
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -170,9 +163,41 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
enum MockEventType {
ESCAPE_REQUESTED
ESCAPE_ACCEPTED
ESCAPE_CANCELED
LOST_SPONSOR
OWNER_CHANGED
}
input MockEventInput {
type: MockEventType!
point: Int!
sponsor: Int
owner: String
blockNumber: Int!
}
type MockInjectionResult {
success: Boolean!
eventsInjected: 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,13 +242,14 @@ 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 {
watchContract(address: String!, kind: String!, checkpoint: Boolean!, startingBlock: Int): Boolean!
injectMockEvents(events: [MockEventInput!]!): MockInjectionResult!
}
type Subscription {
+2
View File
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+3 -3
View File
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3002/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3002/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3002
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/censures-watcher",
"version": "0.1.0",
"version": "0.2.1",
"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.5",
"@cerc-io/ipld-eth-client": "0.2.98-patch.5",
"@cerc-io/solidity-mapper": "0.2.98-patch.5",
"@cerc-io/util": "0.2.98-patch.5",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
+24 -6
View File
@@ -199,10 +199,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -235,10 +235,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -259,6 +259,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -312,8 +330,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -7,6 +7,6 @@ export const getCensuringCount = fs.readFileSync(path.join(__dirname, 'getCensur
export const getCensuring = fs.readFileSync(path.join(__dirname, 'getCensuring.gql'), 'utf8');
export const getCensuredByCount = fs.readFileSync(path.join(__dirname, 'getCensuredByCount.gql'), 'utf8');
export const getCensuredBy = fs.readFileSync(path.join(__dirname, 'getCensuredBy.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
export const getStateByCID = fs.readFileSync(path.join(__dirname, 'getStateByCID.gql'), 'utf8');
export const getState = fs.readFileSync(path.join(__dirname, 'getState.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import { EthClient } from '@cerc-io/ipld-eth-client';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import {
Indexer as BaseIndexer,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import CensuresArtifacts from './artifacts/Censures.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: CensuresABI } = CensuresArtifacts;
assert(CensuresABI);
this._abiMap.set(KIND_CENSURES, CensuresABI);
this._contractMap.set(KIND_CENSURES, new ethers.utils.Interface(CensuresABI));
const CensuresContractInterface = new ethers.utils.Interface(CensuresABI);
this._contractMap.set(KIND_CENSURES, CensuresContractInterface);
const CensuresEventSignatures = Object.values(CensuresContractInterface.events).map(value => {
return CensuresContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_CENSURES, CensuresEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -326,16 +360,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -345,20 +380,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -366,7 +415,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -400,10 +453,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -414,8 +471,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -430,6 +487,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -438,6 +499,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -446,8 +511,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -472,6 +537,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -488,7 +565,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -508,8 +602,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -517,17 +611,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -544,7 +647,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -552,4 +655,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
+176 -70
View File
@@ -3,20 +3,20 @@
//
import assert from 'assert';
import BigInt from 'apollo-type-bigint';
import debug from 'debug';
import Decimal from 'decimal.js';
import {
GraphQLScalarType,
GraphQLResolveInfo
} from 'graphql';
import { GraphQLResolveInfo } from 'graphql';
import { ExpressContext } from 'apollo-server-express';
import winston from 'winston';
import {
ValueResult,
gqlTotalQueryCount,
gqlQueryCount,
gqlQueryDuration,
getResultState,
IndexerInterface,
GraphQLBigInt,
GraphQLBigDecimal,
EventWatcher,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setGQLCacheHints
@@ -26,27 +26,64 @@ import { Indexer } from './indexer';
const log = debug('vulcanize:resolver');
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const executeAndRecordMetrics = async (
indexer: Indexer,
gqlLogger: winston.Logger,
opName: string,
expressContext: ExpressContext,
operation: () => Promise<any>
) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(opName).inc(1);
const endTimer = gqlQueryDuration.labels(opName).startTimer();
try {
const [result, syncStatus] = await Promise.all([
operation(),
indexer.getSyncStatus()
]);
gqlLogger.info({
opName,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
return result;
} catch (error) {
gqlLogger.error({
opName,
error,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
throw error;
} finally {
endTimer();
}
};
export const createResolvers = async (
indexerArg: IndexerInterface,
eventWatcher: EventWatcher,
gqlLogger: winston.Logger
): Promise<any> => {
const indexer = indexerArg as Indexer;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const gqlCacheConfig = indexer.serverConfig.gqlCache;
const gqlCacheConfig = indexer.serverConfig.gql.cache;
return {
BigInt: new BigInt('bigInt'),
BigInt: GraphQLBigInt,
BigDecimal: new GraphQLScalarType({
name: 'BigDecimal',
description: 'BigDecimal custom scalar type',
parseValue (value) {
// value from the client
return new Decimal(value);
},
serialize (value: Decimal) {
// value sent to the client
return value.toFixed();
}
}),
BigDecimal: GraphQLBigDecimal,
Event: {
__resolveType: (obj: any) => {
@@ -76,128 +113,197 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
_: any,
{ blockHash, contractAddress, _whose }: { blockHash: string, contractAddress: string, _whose: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getCensuringCount', blockHash, contractAddress, _whose);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getCensuringCount').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getCensuringCount(blockHash, contractAddress, _whose);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getCensuringCount',
expressContext,
async () => indexer.getCensuringCount(blockHash, contractAddress, _whose)
);
},
getCensuring: (
_: any,
{ blockHash, contractAddress, _whose }: { blockHash: string, contractAddress: string, _whose: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getCensuring', blockHash, contractAddress, _whose);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getCensuring').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getCensuring(blockHash, contractAddress, _whose);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getCensuring',
expressContext,
async () => indexer.getCensuring(blockHash, contractAddress, _whose)
);
},
getCensuredByCount: (
_: any,
{ blockHash, contractAddress, _who }: { blockHash: string, contractAddress: string, _who: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getCensuredByCount', blockHash, contractAddress, _who);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getCensuredByCount').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getCensuredByCount(blockHash, contractAddress, _who);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getCensuredByCount',
expressContext,
async () => indexer.getCensuredByCount(blockHash, contractAddress, _who)
);
},
getCensuredBy: (
_: any,
{ blockHash, contractAddress, _who }: { blockHash: string, contractAddress: string, _who: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getCensuredBy', blockHash, contractAddress, _who);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getCensuredBy').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getCensuredBy(blockHash, contractAddress, _who);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getCensuredBy',
expressContext,
async () => indexer.getCensuredBy(blockHash, contractAddress, _who)
);
},
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
events: async (
_: any,
{ blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string },
expressContext: ExpressContext
) => {
log('events', blockHash, contractAddress, name);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('events').inc(1);
const block = await indexer.getBlockProgress(blockHash);
if (!block || !block.isComplete) {
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'events',
expressContext,
async () => {
const block = await indexer.getBlockProgress(blockHash);
if (!block || !block.isComplete) {
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`);
}
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event));
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
eventsInRange: async (_: any, { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }) => {
log('eventsInRange', fromBlockNumber, toBlockNumber);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('eventsInRange').inc(1);
eventsInRange: async (
_: any,
{ fromBlockNumber, toBlockNumber, name }: { fromBlockNumber: number, toBlockNumber: number, name?: string },
expressContext: ExpressContext
) => {
log('eventsInRange', fromBlockNumber, toBlockNumber, name);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'eventsInRange',
expressContext,
async () => {
const syncStatus = await indexer.getSyncStatus();
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
if (!syncStatus) {
throw new Error('No blocks processed yet');
}
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
getStateByCID: async (_: any, { cid }: { cid: string }) => {
getStateByCID: async (
_: any,
{ cid }: { cid: string },
expressContext: ExpressContext
) => {
log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getStateByCID',
expressContext,
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
getState: async (
_: any,
{ blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string },
expressContext: ExpressContext
) => {
log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getState',
expressContext,
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getSyncStatus: async () => {
getSyncStatus: async (
_: any,
__: Record<string, never>,
expressContext: ExpressContext
) => {
log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus();
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSyncStatus',
expressContext,
async () => indexer.getSyncStatus()
);
}
}
};
+14 -10
View File
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -66,13 +66,6 @@ type ResultIntArray {
proof: Proof
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -81,16 +74,27 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
getCensuringCount(blockHash: String!, contractAddress: String!, _whose: Int!): ResultBigInt!
getCensuring(blockHash: String!, contractAddress: String!, _whose: Int!): ResultBigIntArray!
getCensuredByCount(blockHash: String!, contractAddress: String!, _who: Int!): ResultBigInt!
getCensuredBy(blockHash: String!, contractAddress: String!, _who: Int!): ResultIntArray!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
+2
View File
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+3 -3
View File
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3003/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3003/graphql
* To watch a contract:
+52 -18
View File
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3003
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/claims-watcher",
"version": "0.1.0",
"version": "0.2.1",
"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.5",
"@cerc-io/ipld-eth-client": "0.2.98-patch.5",
"@cerc-io/solidity-mapper": "0.2.98-patch.5",
"@cerc-io/util": "0.2.98-patch.5",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
+24 -6
View File
@@ -153,10 +153,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -189,10 +189,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -213,6 +213,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -266,8 +284,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -4,6 +4,6 @@ import path from 'path';
export const events = fs.readFileSync(path.join(__dirname, 'events.gql'), 'utf8');
export const eventsInRange = fs.readFileSync(path.join(__dirname, 'eventsInRange.gql'), 'utf8');
export const findClaim = fs.readFileSync(path.join(__dirname, 'findClaim.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
export const getStateByCID = fs.readFileSync(path.join(__dirname, 'getStateByCID.gql'), 'utf8');
export const getState = fs.readFileSync(path.join(__dirname, 'getState.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import { EthClient } from '@cerc-io/ipld-eth-client';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import {
Indexer as BaseIndexer,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import ClaimsArtifacts from './artifacts/Claims.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: ClaimsABI } = ClaimsArtifacts;
assert(ClaimsABI);
this._abiMap.set(KIND_CLAIMS, ClaimsABI);
this._contractMap.set(KIND_CLAIMS, new ethers.utils.Interface(ClaimsABI));
const ClaimsContractInterface = new ethers.utils.Interface(ClaimsABI);
this._contractMap.set(KIND_CLAIMS, ClaimsContractInterface);
const ClaimsEventSignatures = Object.values(ClaimsContractInterface.events).map(value => {
return ClaimsContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_CLAIMS, ClaimsEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -234,16 +268,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -253,20 +288,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -274,7 +323,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -308,10 +361,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -322,8 +379,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -338,6 +395,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -346,6 +407,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -354,8 +419,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -380,6 +445,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -396,7 +473,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -416,8 +510,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -425,17 +519,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -452,7 +555,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -460,4 +563,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
+152 -58
View File
@@ -3,20 +3,20 @@
//
import assert from 'assert';
import BigInt from 'apollo-type-bigint';
import debug from 'debug';
import Decimal from 'decimal.js';
import {
GraphQLScalarType,
GraphQLResolveInfo
} from 'graphql';
import { GraphQLResolveInfo } from 'graphql';
import { ExpressContext } from 'apollo-server-express';
import winston from 'winston';
import {
ValueResult,
gqlTotalQueryCount,
gqlQueryCount,
gqlQueryDuration,
getResultState,
IndexerInterface,
GraphQLBigInt,
GraphQLBigDecimal,
EventWatcher,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setGQLCacheHints
@@ -26,27 +26,64 @@ import { Indexer } from './indexer';
const log = debug('vulcanize:resolver');
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const executeAndRecordMetrics = async (
indexer: Indexer,
gqlLogger: winston.Logger,
opName: string,
expressContext: ExpressContext,
operation: () => Promise<any>
) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(opName).inc(1);
const endTimer = gqlQueryDuration.labels(opName).startTimer();
try {
const [result, syncStatus] = await Promise.all([
operation(),
indexer.getSyncStatus()
]);
gqlLogger.info({
opName,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
return result;
} catch (error) {
gqlLogger.error({
opName,
error,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
throw error;
} finally {
endTimer();
}
};
export const createResolvers = async (
indexerArg: IndexerInterface,
eventWatcher: EventWatcher,
gqlLogger: winston.Logger
): Promise<any> => {
const indexer = indexerArg as Indexer;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const gqlCacheConfig = indexer.serverConfig.gqlCache;
const gqlCacheConfig = indexer.serverConfig.gql.cache;
return {
BigInt: new BigInt('bigInt'),
BigInt: GraphQLBigInt,
BigDecimal: new GraphQLScalarType({
name: 'BigDecimal',
description: 'BigDecimal custom scalar type',
parseValue (value) {
// value from the client
return new Decimal(value);
},
serialize (value: Decimal) {
// value sent to the client
return value.toFixed();
}
}),
BigDecimal: GraphQLBigDecimal,
Event: {
__resolveType: (obj: any) => {
@@ -76,74 +113,131 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
_: any,
{ blockHash, contractAddress, _whose, _protocol, _claim }: { blockHash: string, contractAddress: string, _whose: bigint, _protocol: string, _claim: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('findClaim', blockHash, contractAddress, _whose, _protocol, _claim);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('findClaim').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.findClaim(blockHash, contractAddress, _whose, _protocol, _claim);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'findClaim',
expressContext,
async () => indexer.findClaim(blockHash, contractAddress, _whose, _protocol, _claim)
);
},
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
events: async (
_: any,
{ blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string },
expressContext: ExpressContext
) => {
log('events', blockHash, contractAddress, name);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('events').inc(1);
const block = await indexer.getBlockProgress(blockHash);
if (!block || !block.isComplete) {
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'events',
expressContext,
async () => {
const block = await indexer.getBlockProgress(blockHash);
if (!block || !block.isComplete) {
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`);
}
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event));
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
eventsInRange: async (_: any, { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }) => {
log('eventsInRange', fromBlockNumber, toBlockNumber);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('eventsInRange').inc(1);
eventsInRange: async (
_: any,
{ fromBlockNumber, toBlockNumber, name }: { fromBlockNumber: number, toBlockNumber: number, name?: string },
expressContext: ExpressContext
) => {
log('eventsInRange', fromBlockNumber, toBlockNumber, name);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'eventsInRange',
expressContext,
async () => {
const syncStatus = await indexer.getSyncStatus();
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
if (!syncStatus) {
throw new Error('No blocks processed yet');
}
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
getStateByCID: async (_: any, { cid }: { cid: string }) => {
getStateByCID: async (
_: any,
{ cid }: { cid: string },
expressContext: ExpressContext
) => {
log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getStateByCID',
expressContext,
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
getState: async (
_: any,
{ blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string },
expressContext: ExpressContext
) => {
log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getState',
expressContext,
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getSyncStatus: async () => {
getSyncStatus: async (
_: any,
__: Record<string, never>,
expressContext: ExpressContext
) => {
log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus();
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSyncStatus',
expressContext,
async () => indexer.getSyncStatus()
);
}
}
};
+14 -10
View File
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -59,13 +59,6 @@ type ResultInt {
proof: Proof
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -74,13 +67,24 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
findClaim(blockHash: String!, contractAddress: String!, _whose: BigInt!, _protocol: String!, _claim: String!): ResultInt!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3004/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3004/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3004
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/conditional-star-release-watcher",
"version": "0.1.0",
"version": "0.2.1",
"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.5",
"@cerc-io/ipld-eth-client": "0.2.98-patch.5",
"@cerc-io/solidity-mapper": "0.2.98-patch.5",
"@cerc-io/util": "0.2.98-patch.5",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
@@ -298,10 +298,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -334,10 +334,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -358,6 +358,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -411,8 +429,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -13,6 +13,6 @@ export const getForfeited = fs.readFileSync(path.join(__dirname, 'getForfeited.g
export const hasForfeitedBatch = fs.readFileSync(path.join(__dirname, 'hasForfeitedBatch.gql'), 'utf8');
export const getRemainingStars = fs.readFileSync(path.join(__dirname, 'getRemainingStars.gql'), 'utf8');
export const getConditionsState = fs.readFileSync(path.join(__dirname, 'getConditionsState.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
export const getStateByCID = fs.readFileSync(path.join(__dirname, 'getStateByCID.gql'), 'utf8');
export const getState = fs.readFileSync(path.join(__dirname, 'getState.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import { EthClient } from '@cerc-io/ipld-eth-client';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import {
Indexer as BaseIndexer,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import ConditionalStarReleaseArtifacts from './artifacts/ConditionalStarRelease.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: ConditionalStarReleaseABI } = ConditionalStarReleaseArtifacts;
assert(ConditionalStarReleaseABI);
this._abiMap.set(KIND_CONDITIONALSTARRELEASE, ConditionalStarReleaseABI);
this._contractMap.set(KIND_CONDITIONALSTARRELEASE, new ethers.utils.Interface(ConditionalStarReleaseABI));
const ConditionalStarReleaseContractInterface = new ethers.utils.Interface(ConditionalStarReleaseABI);
this._contractMap.set(KIND_CONDITIONALSTARRELEASE, ConditionalStarReleaseContractInterface);
const ConditionalStarReleaseEventSignatures = Object.values(ConditionalStarReleaseContractInterface.events).map(value => {
return ConditionalStarReleaseContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_CONDITIONALSTARRELEASE, ConditionalStarReleaseEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -515,16 +549,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -534,20 +569,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -555,7 +604,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -589,10 +642,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -603,8 +660,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -619,6 +676,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -627,6 +688,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -635,8 +700,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -661,6 +726,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -677,7 +754,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -697,8 +791,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -706,17 +800,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -733,7 +836,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -741,4 +844,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
@@ -3,20 +3,20 @@
//
import assert from 'assert';
import BigInt from 'apollo-type-bigint';
import debug from 'debug';
import Decimal from 'decimal.js';
import {
GraphQLScalarType,
GraphQLResolveInfo
} from 'graphql';
import { GraphQLResolveInfo } from 'graphql';
import { ExpressContext } from 'apollo-server-express';
import winston from 'winston';
import {
ValueResult,
gqlTotalQueryCount,
gqlQueryCount,
gqlQueryDuration,
getResultState,
IndexerInterface,
GraphQLBigInt,
GraphQLBigDecimal,
EventWatcher,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setGQLCacheHints
@@ -26,27 +26,64 @@ import { Indexer } from './indexer';
const log = debug('vulcanize:resolver');
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const executeAndRecordMetrics = async (
indexer: Indexer,
gqlLogger: winston.Logger,
opName: string,
expressContext: ExpressContext,
operation: () => Promise<any>
) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(opName).inc(1);
const endTimer = gqlQueryDuration.labels(opName).startTimer();
try {
const [result, syncStatus] = await Promise.all([
operation(),
indexer.getSyncStatus()
]);
gqlLogger.info({
opName,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
return result;
} catch (error) {
gqlLogger.error({
opName,
error,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
throw error;
} finally {
endTimer();
}
};
export const createResolvers = async (
indexerArg: IndexerInterface,
eventWatcher: EventWatcher,
gqlLogger: winston.Logger
): Promise<any> => {
const indexer = indexerArg as Indexer;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const gqlCacheConfig = indexer.serverConfig.gqlCache;
const gqlCacheConfig = indexer.serverConfig.gql.cache;
return {
BigInt: new BigInt('bigInt'),
BigInt: GraphQLBigInt,
BigDecimal: new GraphQLScalarType({
name: 'BigDecimal',
description: 'BigDecimal custom scalar type',
parseValue (value) {
// value from the client
return new Decimal(value);
},
serialize (value: Decimal) {
// value sent to the client
return value.toFixed();
}
}),
BigDecimal: GraphQLBigDecimal,
Event: {
__resolveType: (obj: any) => {
@@ -76,236 +113,329 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
_: any,
{ blockHash, contractAddress, _participant, _batch }: { blockHash: string, contractAddress: string, _participant: string, _batch: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('withdrawLimit', blockHash, contractAddress, _participant, _batch);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('withdrawLimit').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.withdrawLimit(blockHash, contractAddress, _participant, _batch);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'withdrawLimit',
expressContext,
async () => indexer.withdrawLimit(blockHash, contractAddress, _participant, _batch)
);
},
verifyBalance: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('verifyBalance', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('verifyBalance').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.verifyBalance(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'verifyBalance',
expressContext,
async () => indexer.verifyBalance(blockHash, contractAddress, _participant)
);
},
getBatches: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getBatches', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getBatches').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getBatches(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getBatches',
expressContext,
async () => indexer.getBatches(blockHash, contractAddress, _participant)
);
},
getBatch: (
_: any,
{ blockHash, contractAddress, _participant, _batch }: { blockHash: string, contractAddress: string, _participant: string, _batch: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getBatch', blockHash, contractAddress, _participant, _batch);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getBatch').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getBatch(blockHash, contractAddress, _participant, _batch);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getBatch',
expressContext,
async () => indexer.getBatch(blockHash, contractAddress, _participant, _batch)
);
},
getWithdrawn: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getWithdrawn', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getWithdrawn').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getWithdrawn(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getWithdrawn',
expressContext,
async () => indexer.getWithdrawn(blockHash, contractAddress, _participant)
);
},
getWithdrawnFromBatch: (
_: any,
{ blockHash, contractAddress, _participant, _batch }: { blockHash: string, contractAddress: string, _participant: string, _batch: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getWithdrawnFromBatch', blockHash, contractAddress, _participant, _batch);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getWithdrawnFromBatch').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getWithdrawnFromBatch(blockHash, contractAddress, _participant, _batch);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getWithdrawnFromBatch',
expressContext,
async () => indexer.getWithdrawnFromBatch(blockHash, contractAddress, _participant, _batch)
);
},
getForfeited: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getForfeited', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getForfeited').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getForfeited(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getForfeited',
expressContext,
async () => indexer.getForfeited(blockHash, contractAddress, _participant)
);
},
hasForfeitedBatch: (
_: any,
{ blockHash, contractAddress, _participant, _batch }: { blockHash: string, contractAddress: string, _participant: string, _batch: number },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('hasForfeitedBatch', blockHash, contractAddress, _participant, _batch);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('hasForfeitedBatch').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.hasForfeitedBatch(blockHash, contractAddress, _participant, _batch);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'hasForfeitedBatch',
expressContext,
async () => indexer.hasForfeitedBatch(blockHash, contractAddress, _participant, _batch)
);
},
getRemainingStars: (
_: any,
{ blockHash, contractAddress, _participant }: { blockHash: string, contractAddress: string, _participant: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getRemainingStars', blockHash, contractAddress, _participant);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getRemainingStars').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getRemainingStars(blockHash, contractAddress, _participant);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getRemainingStars',
expressContext,
async () => indexer.getRemainingStars(blockHash, contractAddress, _participant)
);
},
getConditionsState: (
_: any,
{ blockHash, contractAddress }: { blockHash: string, contractAddress: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('getConditionsState', blockHash, contractAddress);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getConditionsState').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.getConditionsState(blockHash, contractAddress);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getConditionsState',
expressContext,
async () => indexer.getConditionsState(blockHash, contractAddress)
);
},
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
events: async (
_: any,
{ blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string },
expressContext: ExpressContext
) => {
log('events', blockHash, contractAddress, name);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('events').inc(1);
const block = await indexer.getBlockProgress(blockHash);
if (!block || !block.isComplete) {
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'events',
expressContext,
async () => {
const block = await indexer.getBlockProgress(blockHash);
if (!block || !block.isComplete) {
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`);
}
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event));
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
eventsInRange: async (_: any, { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }) => {
log('eventsInRange', fromBlockNumber, toBlockNumber);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('eventsInRange').inc(1);
eventsInRange: async (
_: any,
{ fromBlockNumber, toBlockNumber, name }: { fromBlockNumber: number, toBlockNumber: number, name?: string },
expressContext: ExpressContext
) => {
log('eventsInRange', fromBlockNumber, toBlockNumber, name);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'eventsInRange',
expressContext,
async () => {
const syncStatus = await indexer.getSyncStatus();
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
if (!syncStatus) {
throw new Error('No blocks processed yet');
}
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
getStateByCID: async (_: any, { cid }: { cid: string }) => {
getStateByCID: async (
_: any,
{ cid }: { cid: string },
expressContext: ExpressContext
) => {
log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getStateByCID',
expressContext,
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
getState: async (
_: any,
{ blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string },
expressContext: ExpressContext
) => {
log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getState',
expressContext,
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getSyncStatus: async () => {
getSyncStatus: async (
_: any,
__: Record<string, never>,
expressContext: ExpressContext
) => {
log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus();
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSyncStatus',
expressContext,
async () => indexer.getSyncStatus()
);
}
}
};
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -93,13 +93,6 @@ type GetConditionsStateType {
value3: [BigInt!]!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -108,9 +101,20 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
withdrawLimit(blockHash: String!, contractAddress: String!, _participant: String!, _batch: Int!): ResultInt!
verifyBalance(blockHash: String!, contractAddress: String!, _participant: String!): ResultBoolean!
getBatches(blockHash: String!, contractAddress: String!, _participant: String!): ResultIntArray!
@@ -121,9 +125,9 @@ type Query {
hasForfeitedBatch(blockHash: String!, contractAddress: String!, _participant: String!, _batch: Int!): ResultBoolean!
getRemainingStars(blockHash: String!, contractAddress: String!, _participant: String!): ResultIntArray!
getConditionsState(blockHash: String!, contractAddress: String!): ResultGetConditionsStateType!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+3 -3
View File
@@ -65,7 +65,7 @@ This watcher has been generated based on the `DelegatedSending` contract deploye
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -83,7 +83,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3005/graphql
* If the watcher is an `active` watcher:
@@ -99,7 +99,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3005/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3005
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/delegated-sending-watcher",
"version": "0.1.0",
"version": "0.2.1",
"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.5",
"@cerc-io/ipld-eth-client": "0.2.98-patch.5",
"@cerc-io/solidity-mapper": "0.2.98-patch.5",
"@cerc-io/util": "0.2.98-patch.5",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
@@ -168,10 +168,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -204,10 +204,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -228,6 +228,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -281,8 +299,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -0,0 +1,31 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
import { bigintTransformer, bigintArrayTransformer } from '@cerc-io/util';
@Entity()
@Index(['blockHash', 'contractAddress', '_who'], { unique: true })
export class GetInvited {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar', { length: 66 })
blockHash!: string;
@Column('integer')
blockNumber!: number;
@Column('varchar', { length: 42 })
contractAddress!: string;
@Column('numeric', { transformer: bigintTransformer })
_who!: bigint;
@Column('numeric', { array: true, transformer: bigintArrayTransformer })
value!: bigint[];
@Column('text', { nullable: true })
proof!: string;
}
@@ -0,0 +1,28 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
import { bigintArrayTransformer } from '@cerc-io/util';
@Entity()
@Index(['blockHash', 'contractAddress'], { unique: true })
export class GetInviters {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar', { length: 66 })
blockHash!: string;
@Column('integer')
blockNumber!: number;
@Column('varchar', { length: 42 })
contractAddress!: string;
@Column('numeric', { array: true, transformer: bigintArrayTransformer })
value!: bigint[];
@Column('text', { nullable: true })
proof!: string;
}
@@ -0,0 +1,31 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
import { bigintTransformer } from '@cerc-io/util';
@Entity()
@Index(['blockHash', 'contractAddress', '_who'], { unique: true })
export class GetPoolStars {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar', { length: 66 })
blockHash!: string;
@Column('integer')
blockNumber!: number;
@Column('varchar', { length: 42 })
contractAddress!: string;
@Column('numeric', { transformer: bigintTransformer })
_who!: bigint;
@Column('integer', { array: true })
value!: number[];
@Column('text', { nullable: true })
proof!: string;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -5,6 +5,6 @@ export const events = fs.readFileSync(path.join(__dirname, 'events.gql'), 'utf8'
export const eventsInRange = fs.readFileSync(path.join(__dirname, 'eventsInRange.gql'), 'utf8');
export const canSend = fs.readFileSync(path.join(__dirname, 'canSend.gql'), 'utf8');
export const canReceive = fs.readFileSync(path.join(__dirname, 'canReceive.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
export const getStateByCID = fs.readFileSync(path.join(__dirname, 'getStateByCID.gql'), 'utf8');
export const getState = fs.readFileSync(path.join(__dirname, 'getState.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import { EthClient } from '@cerc-io/ipld-eth-client';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import {
Indexer as BaseIndexer,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import DelegatedSendingArtifacts from './artifacts/DelegatedSending.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: DelegatedSendingABI } = DelegatedSendingArtifacts;
assert(DelegatedSendingABI);
this._abiMap.set(KIND_DELEGATEDSENDING, DelegatedSendingABI);
this._contractMap.set(KIND_DELEGATEDSENDING, new ethers.utils.Interface(DelegatedSendingABI));
const DelegatedSendingContractInterface = new ethers.utils.Interface(DelegatedSendingABI);
this._contractMap.set(KIND_DELEGATEDSENDING, DelegatedSendingContractInterface);
const DelegatedSendingEventSignatures = Object.values(DelegatedSendingContractInterface.events).map(value => {
return DelegatedSendingContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_DELEGATEDSENDING, DelegatedSendingEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -264,16 +298,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -283,20 +318,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -304,7 +353,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -338,10 +391,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -352,8 +409,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -368,6 +425,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -376,6 +437,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -384,8 +449,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -410,6 +475,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -426,7 +503,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -446,8 +540,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -455,17 +549,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -482,7 +585,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -490,4 +593,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();
@@ -3,20 +3,20 @@
//
import assert from 'assert';
import BigInt from 'apollo-type-bigint';
import debug from 'debug';
import Decimal from 'decimal.js';
import {
GraphQLScalarType,
GraphQLResolveInfo
} from 'graphql';
import { GraphQLResolveInfo } from 'graphql';
import { ExpressContext } from 'apollo-server-express';
import winston from 'winston';
import {
ValueResult,
gqlTotalQueryCount,
gqlQueryCount,
gqlQueryDuration,
getResultState,
IndexerInterface,
GraphQLBigInt,
GraphQLBigDecimal,
EventWatcher,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setGQLCacheHints
@@ -26,27 +26,64 @@ import { Indexer } from './indexer';
const log = debug('vulcanize:resolver');
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const executeAndRecordMetrics = async (
indexer: Indexer,
gqlLogger: winston.Logger,
opName: string,
expressContext: ExpressContext,
operation: () => Promise<any>
) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(opName).inc(1);
const endTimer = gqlQueryDuration.labels(opName).startTimer();
try {
const [result, syncStatus] = await Promise.all([
operation(),
indexer.getSyncStatus()
]);
gqlLogger.info({
opName,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
latestIndexedBlockNumber: syncStatus?.latestIndexedBlockNumber,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
return result;
} catch (error) {
gqlLogger.error({
opName,
error,
query: expressContext.req.body.query,
variables: expressContext.req.body.variables,
urlPath: expressContext.req.path,
apiKey: expressContext.req.header('x-api-key'),
origin: expressContext.req.headers.origin
});
throw error;
} finally {
endTimer();
}
};
export const createResolvers = async (
indexerArg: IndexerInterface,
eventWatcher: EventWatcher,
gqlLogger: winston.Logger
): Promise<any> => {
const indexer = indexerArg as Indexer;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const gqlCacheConfig = indexer.serverConfig.gqlCache;
const gqlCacheConfig = indexer.serverConfig.gql.cache;
return {
BigInt: new BigInt('bigInt'),
BigInt: GraphQLBigInt,
BigDecimal: new GraphQLScalarType({
name: 'BigDecimal',
description: 'BigDecimal custom scalar type',
parseValue (value) {
// value from the client
return new Decimal(value);
},
serialize (value: Decimal) {
// value sent to the client
return value.toFixed();
}
}),
BigDecimal: GraphQLBigDecimal,
Event: {
__resolveType: (obj: any) => {
@@ -76,92 +113,153 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
_: any,
{ blockHash, contractAddress, _as, _point }: { blockHash: string, contractAddress: string, _as: bigint, _point: bigint },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('canSend', blockHash, contractAddress, _as, _point);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('canSend').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.canSend(blockHash, contractAddress, _as, _point);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'canSend',
expressContext,
async () => indexer.canSend(blockHash, contractAddress, _as, _point)
);
},
canReceive: (
_: any,
{ blockHash, contractAddress, _recipient }: { blockHash: string, contractAddress: string, _recipient: string },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: any,
expressContext: ExpressContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: GraphQLResolveInfo
): Promise<ValueResult> => {
log('canReceive', blockHash, contractAddress, _recipient);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('canReceive').inc(1);
// Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.canReceive(blockHash, contractAddress, _recipient);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'canReceive',
expressContext,
async () => indexer.canReceive(blockHash, contractAddress, _recipient)
);
},
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
events: async (
_: any,
{ blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string },
expressContext: ExpressContext
) => {
log('events', blockHash, contractAddress, name);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('events').inc(1);
const block = await indexer.getBlockProgress(blockHash);
if (!block || !block.isComplete) {
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'events',
expressContext,
async () => {
const block = await indexer.getBlockProgress(blockHash);
if (!block || !block.isComplete) {
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`);
}
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event));
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
eventsInRange: async (_: any, { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }) => {
log('eventsInRange', fromBlockNumber, toBlockNumber);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('eventsInRange').inc(1);
eventsInRange: async (
_: any,
{ fromBlockNumber, toBlockNumber, name }: { fromBlockNumber: number, toBlockNumber: number, name?: string },
expressContext: ExpressContext
) => {
log('eventsInRange', fromBlockNumber, toBlockNumber, name);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
return executeAndRecordMetrics(
indexer,
gqlLogger,
'eventsInRange',
expressContext,
async () => {
const syncStatus = await indexer.getSyncStatus();
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
if (!syncStatus) {
throw new Error('No blocks processed yet');
}
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name);
return events.map(event => indexer.getResultEvent(event));
}
);
},
getStateByCID: async (_: any, { cid }: { cid: string }) => {
getStateByCID: async (
_: any,
{ cid }: { cid: string },
expressContext: ExpressContext
) => {
log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getStateByCID',
expressContext,
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
getState: async (
_: any,
{ blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string },
expressContext: ExpressContext
) => {
log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getState',
expressContext,
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined;
return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
},
getSyncStatus: async () => {
getSyncStatus: async (
_: any,
__: Record<string, never>,
expressContext: ExpressContext
) => {
log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus();
return executeAndRecordMetrics(
indexer,
gqlLogger,
'getSyncStatus',
expressContext,
async () => indexer.getSyncStatus()
);
}
}
};
@@ -16,7 +16,7 @@ type Proof {
}
type _Block_ {
cid: String!
cid: String
hash: String!
number: Int!
timestamp: Int!
@@ -54,13 +54,6 @@ type ResultBoolean {
proof: Proof
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
}
type ResultState {
block: _Block_!
contractAddress: String!
@@ -69,14 +62,25 @@ type ResultState {
data: String!
}
type SyncStatus {
latestIndexedBlockHash: String!
latestIndexedBlockNumber: Int!
latestCanonicalBlockHash: String!
latestCanonicalBlockNumber: Int!
initialIndexedBlockHash: String!
initialIndexedBlockNumber: Int!
latestProcessedBlockHash: String!
latestProcessedBlockNumber: Int!
}
type Query {
events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
canSend(blockHash: String!, contractAddress: String!, _as: BigInt!, _point: BigInt!): ResultBoolean!
canReceive(blockHash: String!, contractAddress: String!, _recipient: String!): ResultBoolean!
getSyncStatus: SyncStatus
getStateByCID(cid: String!): ResultState
getState(blockHash: String!, contractAddress: String!, kind: String): ResultState
getSyncStatus: SyncStatus
}
type Mutation {
+2
View File
@@ -4,3 +4,5 @@ out/
.vscode
.idea
gql-logs/
+3 -3
View File
@@ -63,7 +63,7 @@
To enable GQL requests caching:
* Update the `server.gqlCache` config with required settings.
* Update the `server.gql.cache` config with required settings.
* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level.
@@ -81,7 +81,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3006/graphql
* If the watcher is an `active` watcher:
@@ -97,7 +97,7 @@ To enable GQL requests caching:
yarn server
```
GQL console: http://localhost:3009/graphql
GQL console: http://localhost:3006/graphql
* To watch a contract:
@@ -1,7 +1,7 @@
[server]
host = "127.0.0.1"
port = 3006
kind = "lazy"
kind = "active"
# Checkpointing state.
checkpointing = true
@@ -11,24 +11,31 @@
# Enable state creation
# CAUTION: Disable only if state creation is not desired or can be filled subsequently
enableState = true
enableState = false
# Boolean to filter logs by contract.
filterLogs = false
# Flag to specify whether RPC endpoint supports block hash as block tag parameter
rpcSupportsBlockHashParam = false
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Server GQL config
[server.gql]
path = "/graphql"
# GQL cache settings
[server.gqlCache]
enabled = true
# Max block range for which to return events in eventsInRange GQL query.
# Use -1 for skipping check on block range.
maxEventsBlockRange = 1000
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# Log directory for GQL requests
logDir = "./gql-logs"
# GQL cache-control max-age settings (in seconds)
maxAge = 15
# GQL cache settings
[server.gql.cache]
enabled = true
# Max in-memory cache size (in bytes) (default 8 MB)
# maxCacheSize
# GQL cache-control max-age settings (in seconds)
maxAge = 15
[metrics]
host = "127.0.0.1"
@@ -48,8 +55,21 @@
[upstream]
[upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8083/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8082"
gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
rpcProviderEndpoints = [
"http://127.0.0.1:8081"
]
# Boolean flag to specify if rpc-eth-client should be used for RPC endpoint instead of ipld-eth-client (ipld-eth-server GQL client)
rpcClient = true
# Boolean flag to specify if rpcProviderEndpoint is an FEVM RPC endpoint
isFEVM = false
# Boolean flag to filter event logs by contracts
filterLogsByAddresses = true
# Boolean flag to filter event logs by topics
filterLogsByTopics = false
[upstream.cache]
name = "requests"
@@ -61,6 +81,20 @@
maxCompletionLagInSecs = 300
jobDelayInMilliSecs = 100
eventsInBatch = 50
subgraphEventsOrder = true
blockDelayInMilliSecs = 2000
prefetchBlocksInMem = true
prefetchBlockCount = 10
# Boolean to switch between modes of processing events when starting the server.
# Setting to true will fetch filtered events and required blocks in a range of blocks and then process them.
# Setting to false will fetch blocks consecutively with its events and then process them (Behaviour is followed in realtime processing near head).
useBlockRanges = true
# Block range in which logs are fetched during historical blocks processing
historicalLogsBlockRange = 2000
# Max block range of historical processing after which it waits for completion of events processing
# If set to -1 historical processing does not wait for events processing and completes till latest canonical block
historicalMaxFetchAhead = 10000
# Max number of retries to fetch new block after which watcher will failover to other RPC endpoints
maxNewBlockRetries = 3
+13 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/ecliptic-watcher",
"version": "0.1.0",
"version": "0.2.1",
"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.5",
"@cerc-io/ipld-eth-client": "0.2.98-patch.5",
"@cerc-io/solidity-mapper": "0.2.98-patch.5",
"@cerc-io/util": "0.2.98-patch.5",
"@ethersproject/providers": "^5.4.4",
"@cerc-io/cli": "^0.2.40",
"@cerc-io/ipld-eth-client": "^0.2.40",
"@cerc-io/solidity-mapper": "^0.2.40",
"@cerc-io/util": "^0.2.40",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
"graphql": "^15.5.0",
"json-bigint": "^1.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.32",
"yargs": "^17.0.1",
"decimal.js": "^10.3.1"
"typeorm": "0.2.37",
"yargs": "^17.0.1"
},
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@types/debug": "^4.1.5",
"@types/json-bigint": "^1.0.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^8.35.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@@ -70,6 +71,6 @@
"husky": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^5.0.2",
"copyfiles": "^2.4.1"
"winston": "^3.13.0"
}
}
+24 -7
View File
@@ -314,10 +314,9 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber, name);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
@@ -350,10 +349,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.saveBlockProgress(repo, block);
}
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
}
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
@@ -374,6 +373,24 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
}
async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
}
async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatus(repo, syncStatus);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
@@ -427,8 +444,8 @@ export class Database implements DatabaseInterface {
await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseDatabase.getAncestorAtHeight(blockHash, height);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
@@ -13,8 +13,8 @@ export class BlockProgress implements BlockProgressInterface {
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar')
cid!: string;
@Column('varchar', { nullable: true })
cid!: string | null;
@Column('varchar', { length: 66 })
blockHash!: string;
@@ -21,4 +21,7 @@ export class Contract {
@Column('integer')
startingBlock!: number;
@Column('jsonb', { nullable: true })
context!: Record<string, { data: any, type: number }>;
}
@@ -12,6 +12,6 @@ export class StateSyncStatus {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('integer', { nullable: true })
@Column('integer')
latestCheckpointBlockNumber!: number;
}
@@ -22,6 +22,12 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
latestIndexedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestProcessedBlockHash!: string;
@Column('integer')
latestProcessedBlockNumber!: number;
@Column('varchar', { length: 66 })
latestCanonicalBlockHash!: string;
@@ -33,4 +39,7 @@ export class SyncStatus implements SyncStatusInterface {
@Column('integer')
initialIndexedBlockNumber!: number;
@Column('boolean', { default: false })
hasIndexingError!: boolean;
}
@@ -4,5 +4,9 @@ query getSyncStatus{
latestIndexedBlockNumber
latestCanonicalBlockHash
latestCanonicalBlockNumber
initialIndexedBlockHash
initialIndexedBlockNumber
latestProcessedBlockHash
latestProcessedBlockNumber
}
}
@@ -14,6 +14,6 @@ export const getApproved = fs.readFileSync(path.join(__dirname, 'getApproved.gql
export const isApprovedForAll = fs.readFileSync(path.join(__dirname, 'isApprovedForAll.gql'), 'utf8');
export const getSpawnLimit = fs.readFileSync(path.join(__dirname, 'getSpawnLimit.gql'), 'utf8');
export const canEscapeTo = fs.readFileSync(path.join(__dirname, 'canEscapeTo.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
export const getStateByCID = fs.readFileSync(path.join(__dirname, 'getStateByCID.gql'), 'utf8');
export const getState = fs.readFileSync(path.join(__dirname, 'getState.gql'), 'utf8');
export const getSyncStatus = fs.readFileSync(path.join(__dirname, 'getSyncStatus.gql'), 'utf8');
+134 -27
View File
@@ -6,11 +6,10 @@ import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import debug from 'debug';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { ethers, constants } from 'ethers';
import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import { EthClient } from '@cerc-io/ipld-eth-client';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import {
Indexer as BaseIndexer,
@@ -25,7 +24,12 @@ import {
ResultEvent,
getResultEvent,
DatabaseInterface,
Clients
Clients,
EthClient,
UpstreamConfig,
EthFullBlock,
EthFullTransaction,
ExtraEventData
} from '@cerc-io/util';
import EclipticArtifacts from './artifacts/Ecliptic.json';
@@ -50,36 +54,60 @@ export class Indexer implements IndexerInterface {
_ethProvider: BaseProvider;
_baseIndexer: BaseIndexer;
_serverConfig: ServerConfig;
_upstreamConfig: UpstreamConfig;
_abiMap: Map<string, JsonFragment[]>;
_storageLayoutMap: Map<string, StorageLayout>;
_contractMap: Map<string, ethers.utils.Interface>;
eventSignaturesMap: Map<string, string[]>;
constructor (serverConfig: ServerConfig, db: DatabaseInterface, clients: Clients, ethProvider: BaseProvider, jobQueue: JobQueue) {
constructor (
config: {
server: ServerConfig;
upstream: UpstreamConfig;
},
db: DatabaseInterface,
clients: Clients,
ethProvider: BaseProvider,
jobQueue: JobQueue
) {
assert(db);
assert(clients.ethClient);
this._db = db as Database;
this._ethClient = clients.ethClient;
this._ethProvider = ethProvider;
this._serverConfig = serverConfig;
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._ethProvider, jobQueue);
this._serverConfig = config.server;
this._upstreamConfig = config.upstream;
this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
this._abiMap = new Map();
this._storageLayoutMap = new Map();
this._contractMap = new Map();
this.eventSignaturesMap = new Map();
const { abi: EclipticABI } = EclipticArtifacts;
assert(EclipticABI);
this._abiMap.set(KIND_ECLIPTIC, EclipticABI);
this._contractMap.set(KIND_ECLIPTIC, new ethers.utils.Interface(EclipticABI));
const EclipticContractInterface = new ethers.utils.Interface(EclipticABI);
this._contractMap.set(KIND_ECLIPTIC, EclipticContractInterface);
const EclipticEventSignatures = Object.values(EclipticContractInterface.events).map(value => {
return EclipticContractInterface.getEventTopic(value);
});
this.eventSignaturesMap.set(KIND_ECLIPTIC, EclipticEventSignatures);
}
get serverConfig (): ServerConfig {
return this._serverConfig;
}
get upstreamConfig (): UpstreamConfig {
return this._upstreamConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
@@ -89,6 +117,12 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.fetchStateStatus();
}
switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._baseIndexer.switchClients({ ethClient, ethProvider });
}
getResultEvent (event: Event): ResultEvent {
return getResultEvent(event);
}
@@ -536,16 +570,17 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
const resultEvent = this.getResultEvent(event);
// Call custom hook function for indexing on event.
await handleEvent(this, resultEvent);
}
async processEvent (event: Event): Promise<void> {
async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(event);
await this.triggerIndexingOnEvent(event, extraData);
}
async processBlock (blockProgress: BlockProgress): Promise<void> {
@@ -555,20 +590,34 @@ export class Indexer implements IndexerInterface {
console.timeEnd('time:indexer#processBlock-init_state');
}
parseEventNameAndArgs (kind: string, logObj: any): any {
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;
const contract = this._contractMap.get(kind);
assert(contract);
const logDescription = contract.parseLog({ data, topics });
let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
}
throw err;
}
const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
return {
eventName,
eventInfo,
eventSignature
eventParsed: true,
eventDetails: {
eventName,
eventInfo,
eventSignature
}
};
}
@@ -576,7 +625,11 @@ export class Indexer implements IndexerInterface {
return this._db.getStateSyncStatus();
}
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
if (!this._serverConfig.enableState) {
return;
}
const dbTx = await this._db.createTransactionRunner();
let res;
@@ -610,10 +663,14 @@ export class Indexer implements IndexerInterface {
return res;
}
async getLatestCanonicalBlock (): Promise<BlockProgress> {
async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
const syncStatus = await this.getSyncStatus();
assert(syncStatus);
if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
return;
}
const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
assert(latestCanonicalBlock);
@@ -624,8 +681,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getLatestStateIndexedBlock();
}
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
}
updateStateStatusMap (address: string, stateStatus: StateStatus): void {
@@ -640,6 +697,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.saveEventEntity(dbEvent);
}
async saveEvents (dbEvents: Event[]): Promise<void> {
return this._baseIndexer.saveEvents(dbEvents);
}
async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
@@ -648,6 +709,10 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address);
}
getWatchedContracts (): Contract[] {
return this._baseIndexer.getWatchedContracts();
}
getContractsByKind (kind: string): Contract[] {
return this._baseIndexer.getContractsByKind(kind);
}
@@ -656,8 +721,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number, name?: string): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.gql.maxEventsBlockRange, name);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
@@ -682,6 +747,18 @@ export class Indexer implements IndexerInterface {
return syncStatus;
}
async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
}
async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatus(syncStatus);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
@@ -698,7 +775,24 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
blockProgress: BlockProgress,
events: DeepPartial<Event>[],
ethFullBlock: EthFullBlock;
ethFullTransactions: EthFullTransaction[];
}[]> {
return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
}
async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
return this._saveBlockAndFetchEvents(block);
}
@@ -718,8 +812,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
return this._baseIndexer.getAncestorAtHeight(blockHash, height);
}
async resetWatcherToBlock (blockNumber: number): Promise<void> {
@@ -727,17 +821,26 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
}
async clearProcessedBlockData (block: BlockProgress): Promise<void> {
const entities = [...ENTITIES];
await this._baseIndexer.clearProcessedBlockData(block, entities);
}
async _saveBlockAndFetchEvents ({
cid: blockCid,
blockHash,
blockNumber,
blockTimestamp,
parentHash
}: DeepPartial<BlockProgress>): Promise<[BlockProgress, DeepPartial<Event>[]]> {
}: DeepPartial<BlockProgress>): Promise<[
BlockProgress,
DeepPartial<Event>[],
EthFullTransaction[]
]> {
assert(blockHash);
assert(blockNumber);
const dbEvents = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.parseEventNameAndArgs.bind(this));
const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
const dbTx = await this._db.createTransactionRunner();
try {
@@ -754,7 +857,7 @@ export class Indexer implements IndexerInterface {
await dbTx.commitTransaction();
console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
return [blockProgress, []];
return [blockProgress, [], transactions];
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
@@ -762,4 +865,8 @@ export class Indexer implements IndexerInterface {
await dbTx.release();
}
}
async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
return this._baseIndexer.getFullTransactions(txHashList);
}
}
@@ -20,6 +20,7 @@ export const main = async (): Promise<any> => {
await jobRunnerCmd.exec(async (jobRunner: JobRunner): Promise<void> => {
await jobRunner.subscribeBlockProcessingQueue();
await jobRunner.subscribeHistoricalProcessingQueue();
await jobRunner.subscribeEventProcessingQueue();
await jobRunner.subscribeBlockCheckpointQueue();
await jobRunner.subscribeHooksQueue();

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