Refactor graph-node database and move to util (#259)

* Move graph-database from graph-node to util

* Refactor and remove graph-node dependency from cli package

* Modify dependencies using depcheck

* Implement CLI refactoring changes in other watchers

* Review changes to remove eden comment and fix local import in util

* Import GraphDatabase from util instead of graph-node

* Move graph-node non assemblyscript code to util package

* Implement CLI refactoring changes in codegen

* Fix graph-node tests after refactoring

* Move fromStateEntityValues to graph state utils
This commit is contained in:
2022-11-25 15:54:35 +05:30
committed by GitHub
parent 63a2c5804e
commit b66dcb4af9
105 changed files with 1120 additions and 428 deletions
+2 -3
View File
@@ -8,12 +8,11 @@ import spies from 'chai-spies';
import { utils } from 'ethers';
import { BaseProvider } from '@ethersproject/providers';
import { GraphDatabase, createEvent, createBlock, Block, EventData } from '@cerc-io/util';
import { getDummyEventData, getDummyGraphData, getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import abi from '../test/subgraph/example1/build/Example1/abis/Example1.json';
import { instantiate } from './loader';
import { createEvent, createBlock, Block, EventData } from './utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
chai.use(spies);
@@ -22,7 +21,7 @@ const sandbox = chai.spy.sandbox();
xdescribe('call handler in mapping code', () => {
let exports: any;
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
@@ -8,7 +8,15 @@ import debug from 'debug';
import path from 'path';
import assert from 'assert';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { getConfig as getWatcherConfig, wait, Database as BaseDatabase, Config as WatcherConfig } from '@cerc-io/util';
import {
getConfig as getWatcherConfig,
wait,
Database as BaseDatabase,
Config as WatcherConfig,
GraphDatabase,
getSubgraphConfig
} from '@cerc-io/util';
import { GraphQLClient } from '@cerc-io/ipld-eth-client';
import {
@@ -22,8 +30,6 @@ import {
getConfig,
checkGQLEntitiesInState
} from './utils';
import { Database } from '../../database';
import { getSubgraphConfig } from '../../utils';
const DEFAULT_ENTITIES_LIMIT = 100;
@@ -116,7 +122,7 @@ export const main = async (): Promise<void> => {
let blockDelay = wait(0);
let subgraphContracts: string[] = [];
const contractLatestStateCIDMap: Map<string, { diff: string, checkpoint: string }> = new Map();
let db: Database | undefined, subgraphGQLClient: GraphQLClient | undefined;
let db: GraphDatabase | undefined, subgraphGQLClient: GraphQLClient | undefined;
if (config.watcher) {
const watcherConfigPath = path.resolve(path.dirname(configFile), config.watcher.configPath);
@@ -126,7 +132,7 @@ export const main = async (): Promise<void> => {
const baseDatabase = new BaseDatabase({ ...watcherConfig.database, entities: [entitiesDir] });
await baseDatabase.init();
db = new Database(watcherConfig.server, baseDatabase);
db = new GraphDatabase(watcherConfig.server, baseDatabase);
await db.init();
if (config.watcher.verifyState) {
+1 -1
View File
@@ -15,9 +15,9 @@ import debug from 'debug';
import { Config as CacheConfig, getCache } from '@cerc-io/cache';
import { GraphQLClient } from '@cerc-io/ipld-eth-client';
import { gql } from '@apollo/client/core';
import { DEFAULT_LIMIT } from '@cerc-io/util';
import { Client } from './client';
import { DEFAULT_LIMIT } from '../../database';
const log = debug('vulcanize:compare-utils');
+2 -2
View File
@@ -7,15 +7,15 @@ import { expect } from 'chai';
import { utils } from 'ethers';
import { BaseProvider } from '@ethersproject/providers';
import { GraphDatabase } from '@cerc-io/util';
import { instantiate } from './loader';
import { getDummyGraphData, getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
describe('crypto host api', () => {
let exports: any;
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -9,14 +9,13 @@ import chai from 'chai';
import spies from 'chai-spies';
import { BaseProvider } from '@ethersproject/providers';
import { GraphDatabase, createEvent, Block, createBlock, EventData } from '@cerc-io/util';
import { instantiate } from './loader';
import { createEvent, Block, createBlock, EventData } from './utils';
import edenNetworkAbi from '../test/subgraph/eden/EdenNetwork/abis/EdenNetwork.json';
import merkleDistributorAbi from '../test/subgraph/eden/EdenNetworkDistribution/abis/MerkleDistributor.json';
import distributorGovernanceAbi from '../test/subgraph/eden/EdenNetworkGovernance/abis/DistributorGovernance.json';
import { getDummyEventData, getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
@@ -26,7 +25,7 @@ chai.use(spies);
const sandbox = chai.spy.sandbox();
xdescribe('eden wasm loader tests', async () => {
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
+2 -2
View File
@@ -6,15 +6,15 @@ import path from 'path';
import { expect } from 'chai';
import { BaseProvider } from '@ethersproject/providers';
import { GraphDatabase } from '@cerc-io/util';
import { instantiate } from './loader';
import { getDummyGraphData, getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
describe('ethereum ABI encode decode', () => {
let exports: any;
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
let encoded: string;
+2 -3
View File
@@ -6,17 +6,16 @@ import assert from 'assert';
import path from 'path';
import { BaseProvider } from '@ethersproject/providers';
import { GraphDatabase, EventData } from '@cerc-io/util';
import { instantiate } from './loader';
import exampleAbi from '../test/subgraph/example1/build/Example1/abis/Example1.json';
import { getTestDatabase, getTestIndexer, getTestProvider, getDummyEventData } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
import { EventData } from './utils';
xdescribe('eth-call wasm tests', () => {
let exports: any;
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
-6
View File
@@ -1,7 +1 @@
export * from './watcher';
export * from './database';
export {
resolveEntityFieldConflicts,
afterEntityInsertOrUpdate
} from './utils';
export * from './state-utils';
+2 -2
View File
@@ -5,15 +5,15 @@
import path from 'path';
import { BaseProvider } from '@ethersproject/providers';
import { GraphDatabase } from '@cerc-io/util';
import { instantiate } from './loader';
import { getDummyGraphData, getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
describe('json host api', () => {
let exports: any;
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
+2 -2
View File
@@ -7,17 +7,17 @@ import { expect } from 'chai';
import { utils } from 'ethers';
import { BaseProvider } from '@ethersproject/providers';
import { GraphDatabase } from '@cerc-io/util';
import { instantiate } from './loader';
import { getDummyGraphData, getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
const WASM_FILE_PATH = '../build/debug.wasm';
describe('wasm loader tests', () => {
let exports: any;
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
let module: WebAssembly.Module;
+9 -7
View File
@@ -15,19 +15,21 @@ import debug from 'debug';
import { BaseProvider } from '@ethersproject/providers';
import loader from '@vulcanize/assemblyscript/lib/loader';
import { IndexerInterface, GraphDecimal, getGraphDigitsAndExp } from '@cerc-io/util';
import { TypeId, Level } from './types';
import {
IndexerInterface,
GraphDecimal,
getGraphDigitsAndExp,
prepareEntityState,
TypeId,
Level,
GraphDatabase,
Block,
fromEthereumValue,
toEthereumValue,
getEthereumTypes,
jsonFromBytes,
getStorageValueType
} from './utils';
import { prepareEntityState } from './state-utils';
import { Database } from './database';
} from '@cerc-io/util';
// Endianness of BN used in bigInt store host API.
// Negative bigInt is being stored in wasm in 2's compliment, 'le' representation.
@@ -52,7 +54,7 @@ export interface Context {
const log = debug('vulcanize:graph-node');
export const instantiate = async (
database: Database,
database: GraphDatabase,
indexer: IndexerInterface,
provider: BaseProvider,
context: Context,
+9 -9
View File
@@ -6,14 +6,9 @@ import path from 'path';
import { expect } from 'chai';
import BN from 'bn.js';
import { GraphDecimal } from '@cerc-io/util';
import { BaseProvider } from '@ethersproject/providers';
import { instantiate } from './loader';
import { getDummyGraphData, getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
import {
GraphDecimal,
GraphDatabase,
UINT128_MAX,
UINT256_MAX,
INT256_MIN,
@@ -22,13 +17,18 @@ import {
DECIMAL128_MAX,
DECIMAL128_PMIN,
DECIMAL128_NMAX
} from './utils';
} from '@cerc-io/util';
import { BaseProvider } from '@ethersproject/providers';
import { instantiate } from './loader';
import { getDummyGraphData, getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import { Indexer } from '../test/utils/indexer';
const EXAMPLE_WASM_FILE_PATH = '../test/subgraph/example1/build/Example1/Example1.wasm';
describe('numbers wasm tests', () => {
let exports: any;
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
-208
View File
@@ -1,208 +0,0 @@
//
// Copyright 2022 Vulcanize, Inc.
//
import assert from 'assert';
import debug from 'debug';
import _ from 'lodash';
import { Between } from 'typeorm';
import { IndexerInterface, jsonBigIntStringReplacer, StateInterface } from '@cerc-io/util';
import { Database } from './database';
import { resolveEntityFieldConflicts } from './utils';
const log = debug('vulcanize:state-utils');
export const prepareEntityState = (updatedEntity: any, entityName: string, relationsMap: Map<any, { [key: string]: any }>): any => {
// Resolve any field name conflicts in the dbData for auto-diff.
updatedEntity = resolveEntityFieldConflicts(updatedEntity);
// Prepare the diff data.
const diffData: any = { state: {} };
const result = Array.from(relationsMap.entries())
.find(([key]) => key.name === entityName);
if (result) {
// Update entity data if relations exist.
const [_, relations] = result;
// Update relation fields for diff data to be similar to GQL query entities.
Object.entries(relations).forEach(([relation, { isArray, isDerived }]) => {
if (isDerived || !updatedEntity[relation]) {
// Field is not present in dbData for derived relations
return;
}
if (isArray) {
updatedEntity[relation] = updatedEntity[relation].map((id: string) => ({ id }));
} else {
updatedEntity[relation] = { id: updatedEntity[relation] };
}
});
}
// JSON stringify and parse data for handling unknown types when encoding.
// For example, decimal.js values are converted to string in the diff data.
diffData.state[entityName] = {
// Using custom replacer to store bigints as string values to be encoded by IPLD dag-cbor.
// TODO: Parse and store as native bigint by using Type encoders in IPLD dag-cbor encode.
// https://github.com/rvagg/cborg#type-encoders
[updatedEntity.id]: JSON.parse(JSON.stringify(updatedEntity, jsonBigIntStringReplacer))
};
return diffData;
};
export const updateEntitiesFromState = async (database: Database, indexer: IndexerInterface, state: StateInterface) => {
const data = indexer.getStateData(state);
// Get relations for subgraph entity
assert(indexer.getRelationsMap);
const relationsMap = indexer.getRelationsMap();
for (const [entityName, entities] of Object.entries(data.state)) {
const result = Array.from(relationsMap.entries())
.find(([key]) => key.name === entityName);
const relations = result ? result[1] : {};
log(`Updating entities from State for entity ${entityName}`);
console.time(`time:watcher#GraphWatcher-updateEntitiesFromState-update-entity-${entityName}`);
for (const [id, entityData] of Object.entries(entities as any)) {
const dbData = database.fromState(state.block, entityName, entityData, relations);
await database.saveEntity(entityName, dbData);
}
console.timeEnd(`time:watcher#GraphWatcher-updateEntitiesFromState-update-entity-${entityName}`);
}
};
export const updateSubgraphState = (subgraphStateMap: Map<string, any>, contractAddress: string, data: any): void => {
// Update the subgraph state for a given contract.
const oldData = subgraphStateMap.get(contractAddress);
const updatedData = _.merge(oldData, data);
subgraphStateMap.set(contractAddress, updatedData);
};
export const dumpSubgraphState = async (
indexer: IndexerInterface,
subgraphStateMap: Map<string, any>,
blockHash: string,
isStateFinalized = false
): Promise<void> => {
// Create a diff for each contract in the subgraph state map.
const createDiffPromises = Array.from(subgraphStateMap.entries())
.map(([contractAddress, data]): Promise<void> => {
if (isStateFinalized) {
return indexer.createDiff(contractAddress, blockHash, data);
}
return indexer.createDiffStaged(contractAddress, blockHash, data);
});
await Promise.all(createDiffPromises);
// Reset the subgraph state map.
subgraphStateMap.clear();
};
export const getContractEntitiesMap = (dataSources: any[]): Map<string, string[]> => {
// Map: contractAddress -> entities updated
const contractEntitiesMap: Map<string, string[]> = new Map();
// Populate contractEntitiesMap using data sources from subgraph
dataSources.forEach((dataSource: any) => {
const { source: { address: contractAddress }, mapping: { entities } } = dataSource;
contractEntitiesMap.set(contractAddress, entities as string[]);
});
return contractEntitiesMap;
};
export const fillState = async (
indexer: IndexerInterface,
contractEntitiesMap: Map<string, string[]>,
argv: {
startBlock: number,
endBlock: number
}
): Promise<void> => {
const { startBlock, endBlock } = argv;
if (startBlock > endBlock) {
log('endBlock should be greater than or equal to startBlock');
process.exit(1);
}
// Check that there are no existing diffs in this range
const existingStates = await indexer.getStates({ block: { blockNumber: Between(startBlock, endBlock) } });
if (existingStates.length > 0) {
log('found existing state(s) in the given range');
process.exit(1);
}
console.time('time:fill-state');
// Fill state for blocks in the given range
for (let blockNumber = startBlock; blockNumber <= endBlock; blockNumber++) {
console.time(`time:fill-state-${blockNumber}`);
// Get the canonical block hash at current height
const blocks = await indexer.getBlocksAtHeight(blockNumber, false);
if (blocks.length === 0) {
log(`block not found at height ${blockNumber}`);
process.exit(1);
} else if (blocks.length > 1) {
log(`found more than one non-pruned block at height ${blockNumber}`);
process.exit(1);
}
const blockHash = blocks[0].blockHash;
// Create initial state for contracts
assert(indexer.createInit);
await indexer.createInit(blockHash, blockNumber);
// Fill state for each contract in contractEntitiesMap
const contractStatePromises = Array.from(contractEntitiesMap.entries())
.map(async ([contractAddress, entities]): Promise<void> => {
// Get all the updated entities at this block
const updatedEntitiesListPromises = entities.map(async (entity): Promise<any[]> => {
return indexer.getEntitiesForBlock(blockHash, entity);
});
const updatedEntitiesList = await Promise.all(updatedEntitiesListPromises);
// Populate state with all the updated entities of each entity type
updatedEntitiesList.forEach((updatedEntities, index) => {
const entityName = entities[index];
updatedEntities.forEach((updatedEntity) => {
assert(indexer.getRelationsMap);
assert(indexer.updateSubgraphState);
// Prepare diff data for the entity update
const diffData = prepareEntityState(updatedEntity, entityName, indexer.getRelationsMap());
// Update the in-memory subgraph state
indexer.updateSubgraphState(contractAddress, diffData);
});
});
});
await Promise.all(contractStatePromises);
// Persist subgraph state to the DB
assert(indexer.dumpSubgraphState);
await indexer.dumpSubgraphState(blockHash, true);
await indexer.updateStateSyncStatusIndexedBlock(blockNumber);
// Create checkpoints
await indexer.processCheckpoint(blockHash);
await indexer.updateStateSyncStatusCheckpointBlock(blockNumber);
console.timeEnd(`time:fill-state-${blockNumber}`);
}
console.timeEnd('time:fill-state');
};
+2 -3
View File
@@ -6,18 +6,17 @@ import assert from 'assert';
import path from 'path';
import { BaseProvider } from '@ethersproject/providers';
import { GraphDatabase, EventData } from '@cerc-io/util';
import { instantiate } from './loader';
import exampleAbi from '../test/subgraph/example1/build/Example1/abis/Example1.json';
import { storageLayout } from '../test/artifacts/Example1.json';
import { getTestDatabase, getTestIndexer, getTestProvider, getDummyEventData } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
import { EventData } from './utils';
xdescribe('storage-call wasm tests', () => {
let exports: any;
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
@@ -7,17 +7,17 @@ import { expect } from 'chai';
import { utils, BigNumber } from 'ethers';
import { BaseProvider } from '@ethersproject/providers';
import { GraphDatabase } from '@cerc-io/util';
import { instantiate } from './loader';
import { getDummyGraphData, getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
const EXAMPLE_WASM_FILE_PATH = '../test/subgraph/example1/build/Example1/Example1.wasm';
describe('typeConversion wasm tests', () => {
let exports: any;
let db: Database;
let db: GraphDatabase;
let indexer: Indexer;
let provider: BaseProvider;
-101
View File
@@ -1,101 +0,0 @@
//
// Copyright 2021 Vulcanize, Inc.
//
// Enum types from @graphprotocol/graph-ts.
export enum TypeId {
String = 0,
ArrayBuffer = 1,
Int8Array = 2,
Int16Array = 3,
Int32Array = 4,
Int64Array = 5,
Uint8Array = 6,
Uint16Array = 7,
Uint32Array = 8,
Uint64Array = 9,
Float32Array = 10,
Float64Array = 11,
BigDecimal = 12,
ArrayBool = 13,
ArrayUint8Array = 14,
ArrayEthereumValue = 15,
ArrayStoreValue = 16,
ArrayJsonValue = 17,
ArrayString = 18,
ArrayEventParam = 19,
ArrayTypedMapEntryStringJsonValue = 20,
ArrayTypedMapEntryStringStoreValue = 21,
SmartContractCall = 22,
EventParam = 23,
EthereumTransaction = 24,
EthereumBlock = 25,
EthereumCall = 26,
WrappedTypedMapStringJsonValue = 27,
WrappedBool = 28,
WrappedJsonValue = 29,
EthereumValue = 30,
StoreValue = 31,
JsonValue = 32,
EthereumEvent = 33,
TypedMapEntryStringStoreValue = 34,
TypedMapEntryStringJsonValue = 35,
TypedMapStringStoreValue = 36,
TypedMapStringJsonValue = 37,
TypedMapStringTypedMapStringJsonValue = 38,
ResultTypedMapStringJsonValueBool = 39,
ResultJsonValueBool = 40,
ArrayU8 = 41,
ArrayU16 = 42,
ArrayU32 = 43,
ArrayU64 = 44,
ArrayI8 = 45,
ArrayI16 = 46,
ArrayI32 = 47,
ArrayI64 = 48,
ArrayF32 = 49,
ArrayF64 = 50,
ArrayBigDecimal = 51,
}
export enum EthereumValueKind {
ADDRESS = 0,
FIXED_BYTES = 1,
BYTES = 2,
INT = 3,
UINT = 4,
BOOL = 5,
STRING = 6,
FIXED_ARRAY = 7,
ARRAY = 8,
TUPLE = 9,
}
export enum ValueKind {
STRING = 0,
INT = 1,
BIGDECIMAL = 2,
BOOL = 3,
ARRAY = 4,
NULL = 5,
BYTES = 6,
BIGINT = 7,
}
export enum Level {
CRITICAL = 0,
ERROR = 1,
WARNING = 2,
INFO = 3,
DEBUG = 4,
}
export enum JSONValueKind {
NULL = 0,
BOOL = 1,
NUMBER = 2,
STRING = 3,
ARRAY = 4,
OBJECT = 5,
}
-898
View File
@@ -1,898 +0,0 @@
//
// Copyright 2022 Vulcanize, Inc.
//
import { BigNumber, utils } from 'ethers';
import path from 'path';
import fs from 'fs-extra';
import debug from 'debug';
import yaml from 'js-yaml';
import { DeepPartial, EntityTarget, InsertEvent, Repository, UpdateEvent, ValueTransformer } from 'typeorm';
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
import assert from 'assert';
import _ from 'lodash';
import { GraphDecimal } from '@cerc-io/util';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import { TypeId, EthereumValueKind, ValueKind } from './types';
const log = debug('vulcanize:utils');
export const INT256_MIN = '-57896044618658097711785492504343953926634992332820282019728792003956564819968';
export const INT256_MAX = '57896044618658097711785492504343953926634992332820282019728792003956564819967';
export const UINT128_MAX = '340282366920938463463374607431768211455';
export const UINT256_MAX = '115792089237316195423570985008687907853269984665640564039457584007913129639935';
// Maximum decimal value.
export const DECIMAL128_MAX = '9.999999999999999999999999999999999e+6144';
// Minimum decimal value.
export const DECIMAL128_MIN = '-9.999999999999999999999999999999999e+6144';
// Minimum +ve decimal value.
export const DECIMAL128_PMIN = '1e-6143';
// Maximum -ve decimal value.
export const DECIMAL128_NMAX = '-1e-6143';
export interface Transaction {
hash: string;
index: number;
from: string;
to: string;
value: string;
gasLimit: string;
gasPrice?: string;
input: string;
maxPriorityFeePerGas?: string,
maxFeePerGas?: string,
}
export interface Block {
headerId: number;
blockHash: string;
blockNumber: string;
timestamp: string;
parentHash: string;
stateRoot: string;
td: string;
txRoot: string;
receiptRoot: string;
uncleHash: string;
difficulty: string;
gasLimit: string;
gasUsed: string;
author: string;
size: string;
baseFee?: string;
}
export interface EventData {
block: Block;
tx: Transaction;
inputs: utils.ParamType[];
event: { [key: string]: any }
eventIndex: number;
}
export const getEthereumTypes = async (instanceExports: any, value: any): Promise<any> => {
const {
__getArray,
Bytes,
ethereum
} = instanceExports;
const kind = await value.kind;
switch (kind) {
case EthereumValueKind.ADDRESS:
return 'address';
case EthereumValueKind.BOOL:
return 'bool';
case EthereumValueKind.STRING:
return 'string';
case EthereumValueKind.BYTES:
return 'bytes';
case EthereumValueKind.FIXED_BYTES: {
const bytesPtr = await value.toBytes();
const bytes = await Bytes.wrap(bytesPtr);
const length = await bytes.length;
return `bytes${length}`;
}
case EthereumValueKind.INT:
return 'int256';
case EthereumValueKind.UINT: {
return 'uint256';
}
case EthereumValueKind.ARRAY: {
const valuesPtr = await value.toArray();
const [firstValuePtr] = await __getArray(valuesPtr);
const firstValue = await ethereum.Value.wrap(firstValuePtr);
const type = await getEthereumTypes(instanceExports, firstValue);
return `${type}[]`;
}
case EthereumValueKind.FIXED_ARRAY: {
const valuesPtr = await value.toArray();
const values = await __getArray(valuesPtr);
const firstValue = await ethereum.Value.wrap(values[0]);
const type = await getEthereumTypes(instanceExports, firstValue);
return `${type}[${values.length}]`;
}
case EthereumValueKind.TUPLE: {
let values = await value.toTuple();
values = await __getArray(values);
const typePromises = values.map(async (value: any) => {
value = await ethereum.Value.wrap(value);
return getEthereumTypes(instanceExports, value);
});
const types = await Promise.all(typePromises);
return `tuple(${types.join(',')})`;
}
default:
break;
}
};
/**
* Method to get value from graph-ts ethereum.Value wasm instance.
* @param instanceExports
* @param value
* @returns
*/
export const fromEthereumValue = async (instanceExports: any, value: any): Promise<any> => {
const {
__getArray,
__getString,
BigInt,
Address,
Bytes,
ethereum
} = instanceExports;
const kind = await value.kind;
switch (kind) {
case EthereumValueKind.ADDRESS: {
const addressPtr = await value.toAddress();
const address = Address.wrap(addressPtr);
const addressStringPtr = await address.toHexString();
return __getString(addressStringPtr);
}
case EthereumValueKind.BOOL: {
const bool = await value.toBoolean();
return Boolean(bool);
}
case EthereumValueKind.STRING: {
const stringPtr = await value.toString();
return __getString(stringPtr);
}
case EthereumValueKind.BYTES:
case EthereumValueKind.FIXED_BYTES: {
const bytesPtr = await value.toBytes();
const bytes = await Bytes.wrap(bytesPtr);
const bytesStringPtr = await bytes.toHexString();
return __getString(bytesStringPtr);
}
case EthereumValueKind.INT:
case EthereumValueKind.UINT: {
const bigIntPtr = await value.toBigInt();
const bigInt = BigInt.wrap(bigIntPtr);
const bigIntStringPtr = await bigInt.toString();
const bigIntString = __getString(bigIntStringPtr);
return BigNumber.from(bigIntString);
}
case EthereumValueKind.ARRAY:
case EthereumValueKind.FIXED_ARRAY: {
const valuesPtr = await value.toArray();
const values = __getArray(valuesPtr);
const valuePromises = values.map(async (value: any) => {
value = await ethereum.Value.wrap(value);
return fromEthereumValue(instanceExports, value);
});
return Promise.all(valuePromises);
}
case EthereumValueKind.TUPLE: {
let values = await value.toTuple();
values = await __getArray(values);
const valuePromises = values.map(async (value: any) => {
value = await ethereum.Value.wrap(value);
return fromEthereumValue(instanceExports, value);
});
return Promise.all(valuePromises);
}
default:
break;
}
};
/**
* Method to get ethereum value for passing to wasm instance.
* @param instanceExports
* @param value
* @param type
* @returns
*/
export const toEthereumValue = async (instanceExports: any, output: utils.ParamType, value: any): Promise<any> => {
const {
__newString,
__newArray,
ByteArray,
Bytes,
Address,
ethereum,
BigInt,
id_of_type: getIdOfType
} = instanceExports;
const { type, baseType, arrayChildren } = output;
// For array type.
if (baseType === 'array') {
const arrayEthereumValueId = await getIdOfType(TypeId.ArrayEthereumValue);
// Get values for array elements.
const ethereumValuePromises = value.map(
async (value: any) => toEthereumValue(
instanceExports,
arrayChildren,
value
)
);
const ethereumValues: any[] = await Promise.all(ethereumValuePromises);
const ethereumValuesArray = await __newArray(arrayEthereumValueId, ethereumValues);
return ethereum.Value.fromArray(ethereumValuesArray);
}
// For tuple type.
if (type === 'tuple') {
const arrayEthereumValueId = await getIdOfType(TypeId.ArrayEthereumValue);
// Get values for struct elements.
const ethereumValuePromises = output.components
.map(
async (component: utils.ParamType, index) => toEthereumValue(
instanceExports,
component,
value[index]
)
);
const ethereumValues: any[] = await Promise.all(ethereumValuePromises);
const ethereumValuesArrayPtr = await __newArray(arrayEthereumValueId, ethereumValues);
const ethereumTuple = await ethereum.Tuple.wrap(ethereumValuesArrayPtr);
return ethereum.Value.fromTuple(ethereumTuple);
}
// For boolean type.
if (type === 'bool') {
return ethereum.Value.fromBoolean(value ? 1 : 0);
}
const [isIntegerOrEnum, isInteger, isUnsigned] = type.match(/^enum|((u?)int([0-9]+))/) || [false];
// For uint/int type or enum type.
if (isIntegerOrEnum) {
const valueStringPtr = await __newString(value.toString());
const bigInt = await BigInt.fromString(valueStringPtr);
let ethereumValue = await ethereum.Value.fromUnsignedBigInt(bigInt);
if (Boolean(isInteger) && !isUnsigned) {
ethereumValue = await ethereum.Value.fromSignedBigInt(bigInt);
}
return ethereumValue;
}
if (type.startsWith('address')) {
const valueStringPtr = await __newString(value);
const addressPtr = await Address.fromString(valueStringPtr);
return ethereum.Value.fromAddress(addressPtr);
}
// TODO: Check between fixed bytes and dynamic bytes.
if (type.startsWith('bytes')) {
const valueStringPtr = await __newString(value);
const byteArray = await ByteArray.fromHexString(valueStringPtr);
const bytes = await Bytes.fromByteArray(byteArray);
return ethereum.Value.fromBytes(bytes);
}
// For string type.
const valueStringPtr = await __newString(value);
return ethereum.Value.fromString(valueStringPtr);
};
/**
* Method to create ethereum event.
* @param instanceExports
* @param contractAddress
* @param eventParamsData
* @returns
*/
export const createEvent = async (instanceExports: any, contractAddress: string, eventData: EventData): Promise<any> => {
const {
tx,
eventIndex,
inputs,
event,
block: blockData
} = eventData;
const {
__newString,
__newArray,
Address,
BigInt,
ethereum,
Bytes,
ByteArray,
id_of_type: idOfType
} = instanceExports;
const block = await createBlock(instanceExports, blockData);
// Fill transaction data.
const txHashStringPtr = await __newString(tx.hash);
const txHashByteArray = await ByteArray.fromHexString(txHashStringPtr);
const txHash = await Bytes.fromByteArray(txHashByteArray);
const txIndex = await BigInt.fromI32(tx.index);
const txFromStringPtr = await __newString(tx.from);
const txFrom = await Address.fromString(txFromStringPtr);
const txToStringPtr = await __newString(tx.to);
const txTo = tx.to && await Address.fromString(txToStringPtr);
const valueStringPtr = await __newString(tx.value);
const txValuePtr = await BigInt.fromString(valueStringPtr);
const gasLimitStringPtr = await __newString(tx.gasLimit);
const txGasLimitPtr = await BigInt.fromString(gasLimitStringPtr);
let gasPrice = tx.gasPrice;
if (!gasPrice) {
// Compute gasPrice for EIP-1559 transaction
// https://ethereum.stackexchange.com/questions/122090/what-does-tx-gasprice-represent-after-eip-1559
const feeDifference = BigNumber.from(tx.maxFeePerGas).sub(BigNumber.from(blockData.baseFee));
const maxPriorityFeePerGas = BigNumber.from(tx.maxPriorityFeePerGas);
const priorityFeePerGas = maxPriorityFeePerGas.lt(feeDifference) ? maxPriorityFeePerGas : feeDifference;
gasPrice = BigNumber.from(blockData.baseFee).add(priorityFeePerGas).toString();
}
const gasPriceStringPtr = await __newString(gasPrice);
const txGasPricePtr = await BigInt.fromString(gasPriceStringPtr);
const inputStringPtr = await __newString(tx.input);
const txInputByteArray = await ByteArray.fromHexString(inputStringPtr);
const txInputPtr = await Bytes.fromByteArray(txInputByteArray);
const transaction = await ethereum.Transaction.__new(
txHash,
txIndex,
txFrom,
txTo,
txValuePtr,
txGasLimitPtr,
txGasPricePtr,
txInputPtr
);
const eventParamArrayPromise = inputs.map(async input => {
const { name } = input;
const ethValue = await toEthereumValue(instanceExports, input, event[name]);
const namePtr = await __newString(name);
return ethereum.EventParam.__new(
namePtr,
ethValue
);
});
const eventParamArray = await Promise.all(eventParamArrayPromise);
const arrayEventParamId = await idOfType(TypeId.ArrayEventParam);
const eventParams = await __newArray(arrayEventParamId, eventParamArray);
const addStrPtr = await __newString(contractAddress);
const eventAddressPtr = await Address.fromString(addStrPtr);
const eventIndexPtr = await BigInt.fromI32(eventIndex);
const transactionLogIndexPtr = await BigInt.fromI32(0);
// Create event to be passed to handler.
return ethereum.Event.__new(
eventAddressPtr,
eventIndexPtr,
transactionLogIndexPtr,
null,
block,
transaction,
eventParams
);
};
export const createBlock = async (instanceExports: any, blockData: Block): Promise<any> => {
const {
__newString,
Address,
BigInt,
ethereum,
Bytes,
ByteArray
} = instanceExports;
// Fill block data.
const blockHashStringPtr = await __newString(blockData.blockHash);
const blockHashByteArray = await ByteArray.fromHexString(blockHashStringPtr);
const blockHash = await Bytes.fromByteArray(blockHashByteArray);
const parentHashStringPtr = await __newString(blockData.parentHash);
const parentHashByteArray = await ByteArray.fromHexString(parentHashStringPtr);
const parentHash = await Bytes.fromByteArray(parentHashByteArray);
const uncleHashStringPtr = await __newString(blockData.uncleHash);
const uncleHashByteArray = await ByteArray.fromHexString(uncleHashStringPtr);
const uncleHash = await Bytes.fromByteArray(uncleHashByteArray);
const blockNumberStringPtr = await __newString(blockData.blockNumber);
const blockNumber = await BigInt.fromString(blockNumberStringPtr);
const gasUsedStringPtr = await __newString(blockData.gasUsed);
const gasUsed = await BigInt.fromString(gasUsedStringPtr);
const gasLimitStringPtr = await __newString(blockData.gasLimit);
const gasLimit = await BigInt.fromString(gasLimitStringPtr);
const timestampStringPtr = await __newString(blockData.timestamp);
const blockTimestamp = await BigInt.fromString(timestampStringPtr);
const stateRootStringPtr = await __newString(blockData.stateRoot);
const stateRootByteArray = await ByteArray.fromHexString(stateRootStringPtr);
const stateRoot = await Bytes.fromByteArray(stateRootByteArray);
const txRootStringPtr = await __newString(blockData.txRoot);
const transactionsRootByteArray = await ByteArray.fromHexString(txRootStringPtr);
const transactionsRoot = await Bytes.fromByteArray(transactionsRootByteArray);
const receiptRootStringPtr = await __newString(blockData.receiptRoot);
const receiptsRootByteArray = await ByteArray.fromHexString(receiptRootStringPtr);
const receiptsRoot = await Bytes.fromByteArray(receiptsRootByteArray);
const difficultyStringPtr = await __newString(blockData.difficulty);
const difficulty = await BigInt.fromString(difficultyStringPtr);
const tdStringPtr = await __newString(blockData.td);
const totalDifficulty = await BigInt.fromString(tdStringPtr);
const authorStringPtr = await __newString(blockData.author);
const authorPtr = await Address.fromString(authorStringPtr);
const sizePtr = await __newString(blockData.size);
const size = await BigInt.fromString(sizePtr);
// Missing fields from watcher in block data:
// author
// size
return await ethereum.Block.__new(
blockHash,
parentHash,
uncleHash,
authorPtr,
stateRoot,
transactionsRoot,
receiptsRoot,
blockNumber,
gasUsed,
gasLimit,
blockTimestamp,
difficulty,
totalDifficulty,
size
);
};
export const getSubgraphConfig = async (subgraphPath: string): Promise<any> => {
const configFilePath = path.resolve(path.join(subgraphPath, 'subgraph.yaml'));
const fileExists = await fs.pathExists(configFilePath);
if (!fileExists) {
throw new Error(`Config file not found: ${configFilePath}`);
}
const configFile = await fs.readFile(configFilePath, 'utf8');
const config = yaml.load(configFile);
log('config', JSON.stringify(config, null, 2));
return config;
};
export const toEntityValue = async (instanceExports: any, entityInstance: any, data: any, field: ColumnMetadata, type: string) => {
const { __newString, Value } = instanceExports;
const { isArray, propertyName, isNullable } = field;
const entityKey = await __newString(propertyName);
const entityValuePtr = await entityInstance.get(entityKey);
const subgraphValue = Value.wrap(entityValuePtr);
const value = data[propertyName];
// Check if the entity property is nullable.
// No need to set the property if the value is null as well.
if (isNullable && value === null) {
return;
}
const entityValue = await formatEntityValue(instanceExports, subgraphValue, type, value, isArray);
return entityInstance.set(entityKey, entityValue);
};
export const fromEntityValue = async (instanceExports: any, entityInstance: any, key: string): Promise<any> => {
const { __newString } = instanceExports;
const entityKey = await __newString(key);
const entityValuePtr = await entityInstance.get(entityKey);
return parseEntityValue(instanceExports, entityValuePtr);
};
const parseEntityValue = async (instanceExports: any, valuePtr: number) => {
const {
__getString,
__getArray,
BigInt: ASBigInt,
Bytes,
BigDecimal,
Value
} = instanceExports;
const value = Value.wrap(valuePtr);
const kind = await value.kind;
switch (kind) {
case ValueKind.STRING: {
const stringValue = await value.toString();
return __getString(stringValue);
}
case ValueKind.BYTES: {
const bytesPtr = await value.toBytes();
const bytes = await Bytes.wrap(bytesPtr);
const bytesStringPtr = await bytes.toHexString();
return __getString(bytesStringPtr);
}
case ValueKind.BOOL: {
const bool = await value.toBoolean();
return Boolean(bool);
}
case ValueKind.INT: {
return value.toI32();
}
case ValueKind.BIGINT: {
const bigIntPtr = await value.toBigInt();
const bigInt = ASBigInt.wrap(bigIntPtr);
const bigIntStringPtr = await bigInt.toString();
const bigIntString = __getString(bigIntStringPtr);
return BigInt(bigIntString);
}
case ValueKind.BIGDECIMAL: {
const bigDecimalPtr = await value.toBigDecimal();
const bigDecimal = BigDecimal.wrap(bigDecimalPtr);
const bigDecimalStringPtr = await bigDecimal.toString();
return new GraphDecimal(__getString(bigDecimalStringPtr)).toFixed();
}
case ValueKind.ARRAY: {
const arrayPtr = await value.toArray();
const arr = await __getArray(arrayPtr);
const arrDataPromises = arr.map((arrValuePtr: any) => parseEntityValue(instanceExports, arrValuePtr));
return Promise.all(arrDataPromises);
}
case ValueKind.NULL: {
return null;
}
default:
throw new Error(`Unsupported value kind: ${kind}`);
}
};
const formatEntityValue = async (instanceExports: any, subgraphValue: any, type: string, value: any, isArray: boolean): Promise<any> => {
const { __newString, __newArray, BigInt: ASBigInt, Value, ByteArray, Bytes, BigDecimal, id_of_type: getIdOfType } = instanceExports;
if (isArray) {
const dataArrayPromises = value.map((el: any) => formatEntityValue(instanceExports, subgraphValue, type, el, false));
const dataArray = await Promise.all(dataArrayPromises);
const arrayStoreValueId = await getIdOfType(TypeId.ArrayStoreValue);
const valueArray = await __newArray(arrayStoreValueId, dataArray);
return Value.fromArray(valueArray);
}
switch (type) {
case 'ID':
case 'String': {
const entityValue = await __newString(value);
return Value.fromString(entityValue);
}
case 'Boolean': {
return Value.fromBoolean(value ? 1 : 0);
}
case 'Int': {
return Value.fromI32(value);
}
case 'BigInt': {
const valueStringPtr = await __newString(value.toString());
const bigInt = await ASBigInt.fromString(valueStringPtr);
return Value.fromBigInt(bigInt);
}
case 'BigDecimal': {
const valueStringPtr = await __newString(value.toString());
const bigDecimal = await BigDecimal.fromString(valueStringPtr);
return Value.fromBigDecimal(bigDecimal);
}
case 'Bytes': {
const entityValue = await __newString(value);
const byteArray = await ByteArray.fromHexString(entityValue);
const bytes = await Bytes.fromByteArray(byteArray);
return Value.fromBytes(bytes);
}
// Return default as string for enum or custom type.
default: {
const entityValue = await __newString(value);
return Value.fromString(entityValue);
}
}
};
export const resolveEntityFieldConflicts = (entity: any): any => {
if (entity) {
// Remove fields blockHash and blockNumber from the entity.
delete entity.blockHash;
delete entity.blockNumber;
// Rename _blockHash -> blockHash.
if ('_blockHash' in entity) {
entity.blockHash = entity._blockHash;
delete entity._blockHash;
}
// Rename _blockNumber -> blockNumber.
if ('_blockNumber' in entity) {
entity.blockNumber = entity._blockNumber;
delete entity._blockNumber;
}
}
return entity;
};
export const toJSONValue = async (instanceExports: any, value: any): Promise<any> => {
const { CustomJSONValue, JSONValueTypedMap, __newString, __newArray, id_of_type: getIdOfType } = instanceExports;
if (!value) {
return CustomJSONValue.fromNull();
}
if (Array.isArray(value)) {
const arrayPromise = value.map(async (el: any) => toJSONValue(instanceExports, el));
const array = await Promise.all(arrayPromise);
const arrayJsonValueId = await getIdOfType(TypeId.ArrayJsonValue);
const arrayPtr = __newArray(arrayJsonValueId, array);
return CustomJSONValue.fromArray(arrayPtr);
}
if (typeof value === 'object') {
const map = await JSONValueTypedMap.__new();
const valuePromises = Object.entries(value).map(async ([key, value]) => {
const valuePtr = await toJSONValue(instanceExports, value);
const keyPtr = await __newString(key);
await map.set(keyPtr, valuePtr);
});
await Promise.all(valuePromises);
return CustomJSONValue.fromObject(map);
}
if (typeof value === 'string') {
const stringPtr = await __newString(value);
return CustomJSONValue.fromString(stringPtr);
}
if (typeof value === 'number') {
const stringPtr = await __newString(value.toString());
return CustomJSONValue.fromNumber(stringPtr);
}
if (typeof value === 'boolean') {
return CustomJSONValue.fromBoolean(value);
}
};
export const jsonFromBytes = async (instanceExports: any, bytesPtr: number): Promise<any> => {
const { ByteArray, __getString } = instanceExports;
const byteArray = await ByteArray.wrap(bytesPtr);
const jsonStringPtr = await byteArray.toString();
const json = JSON.parse(__getString(jsonStringPtr));
const jsonValue = await toJSONValue(instanceExports, json);
return jsonValue;
};
export const getStorageValueType = (storageLayout: StorageLayout, variableString: string, mappingKeys: MappingKey[]): utils.ParamType => {
const storage = storageLayout.storage.find(({ label }) => label === variableString);
assert(storage);
return getEthereumType(storageLayout.types, storage.type, mappingKeys);
};
const getEthereumType = (storageTypes: StorageLayout['types'], type: string, mappingKeys: MappingKey[]): utils.ParamType => {
const { label, encoding, members, value } = storageTypes[type];
if (encoding === 'mapping') {
assert(value);
return getEthereumType(storageTypes, value, mappingKeys.slice(1));
}
// Struct type contains members field.
if (members) {
const mappingKey = mappingKeys.shift();
const member = members.find(({ label }) => label === mappingKey);
assert(member);
const { type } = member;
return getEthereumType(storageTypes, type, mappingKeys);
}
return utils.ParamType.from(label);
};
export const fromStateEntityValues = (
stateEntity: any,
propertyName: string,
relations: { [key: string]: any } = {},
transformer?: ValueTransformer | ValueTransformer[]
): any => {
// Parse DB data value from state entity data.
if (relations) {
const relation = relations[propertyName];
if (relation) {
if (relation.isArray) {
return stateEntity[propertyName].map((relatedEntity: { id: string }) => relatedEntity.id);
} else {
return stateEntity[propertyName]?.id;
}
}
}
if (transformer) {
if (Array.isArray(transformer)) {
// Apply transformer in reverse order similar to when reading from DB.
return transformer.reduceRight((acc, elTransformer) => {
return elTransformer.from(acc);
}, stateEntity[propertyName]);
}
return transformer.from(stateEntity[propertyName]);
}
return stateEntity[propertyName];
};
export const afterEntityInsertOrUpdate = async<Entity> (
frothyEntityType: EntityTarget<Entity>,
entities: Set<any>,
event: InsertEvent<any> | UpdateEvent<any>,
entityToLatestEntityMap: Map<new () => any, new () => any> = new Map()
): Promise<void> => {
const entity = event.entity;
// Return if the entity is being pruned
if (entity.isPruned) {
return;
}
// Insert the entity details in FrothyEntity table
if (entities.has(entity.constructor)) {
const frothyEntity = event.manager.create(
frothyEntityType,
{
..._.pick(entity, ['id', 'blockHash', 'blockNumber']),
...{ name: entity.constructor.name }
}
);
await event.manager.createQueryBuilder()
.insert()
.into(frothyEntityType)
.values(frothyEntity as any)
.orIgnore()
.execute();
}
// Get latest entity's type
const entityTarget = entityToLatestEntityMap.get(entity.constructor);
if (!entityTarget) {
return;
}
// Get latest entity's fields to be updated
const latestEntityRepo = event.manager.getRepository(entityTarget);
const fieldsToUpdate = latestEntityRepo.metadata.columns.map(column => column.databaseName).filter(val => val !== 'id');
// Create a latest entity instance and upsert in the db
const latestEntity = getLatestEntityFromEntity(latestEntityRepo, entity);
await event.manager.createQueryBuilder()
.insert()
.into(entityTarget)
.values(latestEntity)
.orUpdate(
{ conflict_target: ['id'], overwrite: fieldsToUpdate }
)
.execute();
};
export const getLatestEntityFromEntity = <Entity> (latestEntityRepo: Repository<Entity>, entity: any): Entity => {
const latestEntityFields = latestEntityRepo.metadata.columns.map(column => column.propertyName);
return latestEntityRepo.create(_.pick(entity, latestEntityFields) as DeepPartial<Entity>);
};
+38 -5
View File
@@ -12,11 +12,25 @@ import { SelectionNode } from 'graphql';
import { ResultObject } from '@vulcanize/assemblyscript/lib/loader';
import { EthClient } from '@cerc-io/ipld-eth-client';
import { getFullBlock, BlockHeight, ServerConfig, getFullTransaction, QueryOptions, IndexerInterface, BlockProgressInterface } from '@cerc-io/util';
import {
getFullBlock,
BlockHeight,
ServerConfig,
getFullTransaction,
QueryOptions,
IndexerInterface,
BlockProgressInterface,
Database as BaseDatabase,
GraphDatabase,
resolveEntityFieldConflicts,
createBlock,
createEvent,
getSubgraphConfig,
Transaction,
DEFAULT_LIMIT
} from '@cerc-io/util';
import { createBlock, createEvent, getSubgraphConfig, resolveEntityFieldConflicts, Transaction } from './utils';
import { Context, GraphData, instantiate } from './loader';
import { Database, DEFAULT_LIMIT } from './database';
const log = debug('vulcanize:graph-watcher');
@@ -27,7 +41,7 @@ interface DataSource {
}
export class GraphWatcher {
_database: Database;
_database: GraphDatabase;
_indexer?: IndexerInterface;
_ethClient: EthClient;
_ethProvider: providers.BaseProvider;
@@ -39,7 +53,7 @@ export class GraphWatcher {
_context: Context = {};
constructor (database: Database, ethClient: EthClient, ethProvider: providers.BaseProvider, serverConfig: ServerConfig) {
constructor (database: GraphDatabase, ethClient: EthClient, ethProvider: providers.BaseProvider, serverConfig: ServerConfig) {
this._database = database;
this._ethClient = ethClient;
this._ethProvider = ethProvider;
@@ -462,3 +476,22 @@ export class GraphWatcher {
return transaction;
}
}
export const getGraphDbAndWatcher = async (
serverConfig: ServerConfig,
ethClient: EthClient,
ethProvider: providers.BaseProvider,
baseDatabase: BaseDatabase,
entityQueryTypeMap?: Map<any, any>,
entityToLatestEntityMap?: Map<any, any>
): Promise<{ graphDb: GraphDatabase, graphWatcher: GraphWatcher }> => {
const graphDb = new GraphDatabase(serverConfig, baseDatabase, entityQueryTypeMap, entityToLatestEntityMap);
await graphDb.init();
const graphWatcher = new GraphWatcher(graphDb, ethClient, ethProvider, serverConfig);
return {
graphDb,
graphWatcher
};
};