mirror of
https://github.com/cerc-io/watcher-ts
synced 2026-09-08 08:54:05 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
export * from './watcher';
|
||||
export * from './database';
|
||||
export {
|
||||
resolveEntityFieldConflicts,
|
||||
afterEntityInsertOrUpdate
|
||||
} from './utils';
|
||||
export * from './state-utils';
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>);
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
//
|
||||
|
||||
import { BaseProvider } from '@ethersproject/providers';
|
||||
import { getCustomProvider, Database as BaseDatabase, ServerConfig } from '@cerc-io/util';
|
||||
import { getCustomProvider, Database as BaseDatabase, ServerConfig, GraphDatabase, EventData } from '@cerc-io/util';
|
||||
import { EthClient } from '@cerc-io/ipld-eth-client';
|
||||
import { StorageLayout } from '@cerc-io/solidity-mapper';
|
||||
|
||||
import { EventData } from '../../src/utils';
|
||||
import { Database } from '../../src/database';
|
||||
import { Indexer } from './indexer';
|
||||
|
||||
const NETWORK_URL = 'http://127.0.0.1:8081';
|
||||
@@ -70,10 +68,10 @@ export const getDummyGraphData = (): any => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getTestDatabase = (): Database => {
|
||||
export const getTestDatabase = (): GraphDatabase => {
|
||||
const baseDatabase = new BaseDatabase({ type: 'postgres' });
|
||||
const serverConfig = {} as ServerConfig;
|
||||
return new Database(serverConfig, baseDatabase);
|
||||
return new GraphDatabase(serverConfig, baseDatabase);
|
||||
};
|
||||
|
||||
export const getTestIndexer = (storageLayout?: Map<string, StorageLayout>): Indexer => {
|
||||
|
||||
Reference in New Issue
Block a user