mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-08-02 12:32:07 +00:00
Update CLI to compare only updated entities and verify IPLD state (#161)
* Change compare CLI to verify only updated entities * Implement IPLD state verification in compare CLI * Changes to IPLD state to match with GQL result entity
This commit is contained in:
parent
ec56de057f
commit
7238f614c0
@ -585,6 +585,10 @@ export class Indexer implements IPLDIndexerInterface {
|
|||||||
return this._entityTypesMap;
|
return this._entityTypesMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRelationsMap (): Map<any, { [key: string]: any }> {
|
||||||
|
return this._relationsMap;
|
||||||
|
}
|
||||||
|
|
||||||
_populateEntityTypesMap (): void {
|
_populateEntityTypesMap (): void {
|
||||||
this._entityTypesMap.set(
|
this._entityTypesMap.set(
|
||||||
'Producer',
|
'Producer',
|
||||||
|
13
packages/eden-watcher/test/queries/account.gql
Normal file
13
packages/eden-watcher/test/queries/account.gql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
query account($id: String!, $block: Block_height){
|
||||||
|
account(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
totalClaimed
|
||||||
|
totalSlashed
|
||||||
|
claims{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
slashes{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
packages/eden-watcher/test/queries/block.gql
Normal file
20
packages/eden-watcher/test/queries/block.gql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
query block($id: String!, $block: Block_height){
|
||||||
|
block(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
fromActiveProducer
|
||||||
|
hash
|
||||||
|
parentHash
|
||||||
|
unclesHash
|
||||||
|
author
|
||||||
|
stateRoot
|
||||||
|
transactionsRoot
|
||||||
|
receiptsRoot
|
||||||
|
number
|
||||||
|
gasUsed
|
||||||
|
gasLimit
|
||||||
|
timestamp
|
||||||
|
difficulty
|
||||||
|
totalDifficulty
|
||||||
|
size
|
||||||
|
}
|
||||||
|
}
|
12
packages/eden-watcher/test/queries/claim.gql
Normal file
12
packages/eden-watcher/test/queries/claim.gql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
query claim($id: String!, $block: Block_height){
|
||||||
|
claim(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
timestamp
|
||||||
|
index
|
||||||
|
account{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
totalEarned
|
||||||
|
claimed
|
||||||
|
}
|
||||||
|
}
|
12
packages/eden-watcher/test/queries/distribution.gql
Normal file
12
packages/eden-watcher/test/queries/distribution.gql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
query distribution($id: String!, $block: Block_height){
|
||||||
|
distribution(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
distributor{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
timestamp
|
||||||
|
distributionNumber
|
||||||
|
merkleRoot
|
||||||
|
metadataURI
|
||||||
|
}
|
||||||
|
}
|
8
packages/eden-watcher/test/queries/distributor.gql
Normal file
8
packages/eden-watcher/test/queries/distributor.gql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
query distributor($id: String!, $block: Block_height){
|
||||||
|
distributor(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
currentDistribution{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
packages/eden-watcher/test/queries/epoch.gql
Normal file
19
packages/eden-watcher/test/queries/epoch.gql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
query epoch($id: String!, $block: Block_height){
|
||||||
|
epoch(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
finalized
|
||||||
|
epochNumber
|
||||||
|
startBlock{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
endBlock{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
producerBlocks
|
||||||
|
allBlocks
|
||||||
|
producerBlocksRatio
|
||||||
|
producerRewards{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
packages/eden-watcher/test/queries/network.gql
Normal file
20
packages/eden-watcher/test/queries/network.gql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
query network($id: String!, $block: Block_height){
|
||||||
|
network(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
slot0{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
slot1{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
slot2{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
stakers{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
numStakers
|
||||||
|
totalStaked
|
||||||
|
stakedPercentiles
|
||||||
|
}
|
||||||
|
}
|
10
packages/eden-watcher/test/queries/producer.gql
Normal file
10
packages/eden-watcher/test/queries/producer.gql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
query producer($id: String!, $block: Block_height){
|
||||||
|
producer(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
active
|
||||||
|
rewardCollector
|
||||||
|
rewards
|
||||||
|
confirmedBlocks
|
||||||
|
pendingEpochBlocks
|
||||||
|
}
|
||||||
|
}
|
12
packages/eden-watcher/test/queries/producerEpoch.gql
Normal file
12
packages/eden-watcher/test/queries/producerEpoch.gql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
query producerEpoch($id: String!, $block: Block_height){
|
||||||
|
producerEpoch(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
address
|
||||||
|
epoch{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
totalRewards
|
||||||
|
blocksProduced
|
||||||
|
blocksProducedRatio
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
query producerRewardCollectorChange($id: String!, $block: Block_height){
|
||||||
|
producerRewardCollectorChange(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
blockNumber
|
||||||
|
producer
|
||||||
|
rewardCollector
|
||||||
|
}
|
||||||
|
}
|
8
packages/eden-watcher/test/queries/producerSet.gql
Normal file
8
packages/eden-watcher/test/queries/producerSet.gql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
query producerSet($id: String!, $block: Block_height){
|
||||||
|
producerSet(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
producers{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
packages/eden-watcher/test/queries/producerSetChange.gql
Normal file
8
packages/eden-watcher/test/queries/producerSetChange.gql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
query producerSetChange($id: String!, $block: Block_height){
|
||||||
|
producerSetChange(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
blockNumber
|
||||||
|
producer
|
||||||
|
changeType
|
||||||
|
}
|
||||||
|
}
|
17
packages/eden-watcher/test/queries/rewardSchedule.gql
Normal file
17
packages/eden-watcher/test/queries/rewardSchedule.gql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
query rewardSchedule($id: String!, $block: Block_height){
|
||||||
|
rewardSchedule(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
rewardScheduleEntries{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
lastEpoch{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
pendingEpoch{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
activeRewardScheduleEntry{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
query rewardScheduleEntry($id: String!, $block: Block_height){
|
||||||
|
rewardScheduleEntry(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
startTime
|
||||||
|
epochDuration
|
||||||
|
rewardsPerEpoch
|
||||||
|
}
|
||||||
|
}
|
10
packages/eden-watcher/test/queries/slash.gql
Normal file
10
packages/eden-watcher/test/queries/slash.gql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
query slash($id: String!, $block: Block_height){
|
||||||
|
slash(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
timestamp
|
||||||
|
account{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
slashed
|
||||||
|
}
|
||||||
|
}
|
15
packages/eden-watcher/test/queries/slot.gql
Normal file
15
packages/eden-watcher/test/queries/slot.gql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
query slot($id: String!, $block: Block_height){
|
||||||
|
slot(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
owner
|
||||||
|
delegate
|
||||||
|
winningBid
|
||||||
|
oldBid
|
||||||
|
startTime
|
||||||
|
expirationTime
|
||||||
|
taxRatePerDay
|
||||||
|
claims{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
packages/eden-watcher/test/queries/slotClaim.gql
Normal file
14
packages/eden-watcher/test/queries/slotClaim.gql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
query slotClaim($id: String!, $block: Block_height){
|
||||||
|
slotClaim(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
slot{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
owner
|
||||||
|
winningBid
|
||||||
|
oldBid
|
||||||
|
startTime
|
||||||
|
expirationTime
|
||||||
|
taxRatePerDay
|
||||||
|
}
|
||||||
|
}
|
7
packages/eden-watcher/test/queries/staker.gql
Normal file
7
packages/eden-watcher/test/queries/staker.gql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
query staker($id: String!, $block: Block_height){
|
||||||
|
staker(id: $id, block: $block){
|
||||||
|
id
|
||||||
|
staked
|
||||||
|
rank
|
||||||
|
}
|
||||||
|
}
|
@ -75,9 +75,9 @@
|
|||||||
./bin/compare-blocks --config-file environments/compare-cli-config.toml --start-block 1 --end-block 10
|
./bin/compare-blocks --config-file environments/compare-cli-config.toml --start-block 1 --end-block 10
|
||||||
```
|
```
|
||||||
|
|
||||||
* For comparing entities after fetching ids from one of the endpoints and then querying individually by ids:
|
* For comparing entities after fetching updated entity ids from watcher database:
|
||||||
|
|
||||||
* Set the `idsEndpoint` to choose which endpoint the ids should be fetched from.
|
* Set the watcher config file path and entities directory.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[endpoints]
|
[endpoints]
|
||||||
@ -90,7 +90,20 @@
|
|||||||
"author",
|
"author",
|
||||||
"blog"
|
"blog"
|
||||||
]
|
]
|
||||||
idsEndpoint = "gqlEndpoint1"
|
|
||||||
|
[watcher]
|
||||||
|
configPath = "../../graph-test-watcher/environments/local.toml"
|
||||||
|
entitiesDir = "../../graph-test-watcher/dist/entity/*"
|
||||||
|
```
|
||||||
|
|
||||||
|
* To verify diff IPLD state generated at each block, set the watcher endpoint and `verifyState` flag to true
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[watcher]
|
||||||
|
configPath = "../../graph-test-watcher/environments/local.toml"
|
||||||
|
entitiesDir = "../../graph-test-watcher/dist/entity/*"
|
||||||
|
endpoint = "gqlEndpoint2"
|
||||||
|
verifyState = true
|
||||||
```
|
```
|
||||||
|
|
||||||
* Run the CLI with `fetch-ids` flag set to true:\
|
* Run the CLI with `fetch-ids` flag set to true:\
|
||||||
|
@ -6,6 +6,13 @@
|
|||||||
queryDir = "../graph-test-watcher/src/gql/queries"
|
queryDir = "../graph-test-watcher/src/gql/queries"
|
||||||
names = []
|
names = []
|
||||||
idsEndpoint = "gqlEndpoint1"
|
idsEndpoint = "gqlEndpoint1"
|
||||||
|
blockDelayInMs = 250
|
||||||
|
|
||||||
|
[watcher]
|
||||||
|
configpath = "../../graph-test-watcher/environments/local.toml"
|
||||||
|
entitiesDir = "../../graph-test-watcher/src/entity"
|
||||||
|
endpoint = "gqlEndpoint2"
|
||||||
|
verifyState = true
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
endpoint = "gqlEndpoint1"
|
endpoint = "gqlEndpoint1"
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json-bigint": "^1.0.0",
|
"json-bigint": "^1.0.0",
|
||||||
"json-diff": "^0.5.4",
|
"json-diff": "^0.5.4",
|
||||||
|
"omit-deep": "^0.3.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
|
@ -5,10 +5,17 @@
|
|||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import path from 'path';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import omitDeep from 'omit-deep';
|
||||||
|
import { getConfig as getWatcherConfig, wait } from '@vulcanize/util';
|
||||||
|
import { GraphQLClient } from '@vulcanize/ipld-eth-client';
|
||||||
|
|
||||||
import { compareQuery, Config, getClients, getConfig } from './utils';
|
import { compareObjects, compareQuery, Config, getBlockIPLDState as getIPLDStateByBlock, getClients, getConfig } from './utils';
|
||||||
import { Client } from './client';
|
import { Database } from '../../database';
|
||||||
|
import { getSubgraphConfig } from '../../utils';
|
||||||
|
|
||||||
const log = debug('vulcanize:compare-blocks');
|
const log = debug('vulcanize:compare-blocks');
|
||||||
|
|
||||||
@ -50,48 +57,109 @@ export const main = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}).argv;
|
}).argv;
|
||||||
|
|
||||||
const config: Config = await getConfig(argv.configFile);
|
const { startBlock, endBlock, rawJson, queryDir, fetchIds, configFile } = argv;
|
||||||
|
const config: Config = await getConfig(configFile);
|
||||||
const { startBlock, endBlock, rawJson, queryDir, fetchIds } = argv;
|
const snakeNamingStrategy = new SnakeNamingStrategy();
|
||||||
|
const clients = await getClients(config, queryDir);
|
||||||
const queryNames = config.queries.names;
|
const queryNames = config.queries.names;
|
||||||
let diffFound = false;
|
let diffFound = false;
|
||||||
|
let blockDelay = wait(0);
|
||||||
|
let subgraphContracts: string[] = [];
|
||||||
|
let db: Database | undefined, subgraphGQLClient: GraphQLClient | undefined;
|
||||||
|
|
||||||
const clients = await getClients(config, queryDir);
|
if (config.watcher) {
|
||||||
|
const watcherConfigPath = path.resolve(path.dirname(configFile), config.watcher.configPath);
|
||||||
|
const entitiesDir = path.resolve(path.dirname(configFile), config.watcher.entitiesDir);
|
||||||
|
const watcherConfig = await getWatcherConfig(watcherConfigPath);
|
||||||
|
db = new Database(watcherConfig.database, entitiesDir);
|
||||||
|
await db.init();
|
||||||
|
|
||||||
|
if (config.watcher.verifyState) {
|
||||||
|
const { dataSources } = await getSubgraphConfig(watcherConfig.server.subgraphPath);
|
||||||
|
subgraphContracts = dataSources.map((dataSource: any) => dataSource.source.address);
|
||||||
|
const watcherEndpoint = config.endpoints[config.watcher.endpoint] as string;
|
||||||
|
subgraphGQLClient = new GraphQLClient({ gqlEndpoint: watcherEndpoint });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let blockNumber = startBlock; blockNumber <= endBlock; blockNumber++) {
|
for (let blockNumber = startBlock; blockNumber <= endBlock; blockNumber++) {
|
||||||
const block = { number: blockNumber };
|
const block = { number: blockNumber };
|
||||||
|
let updatedEntityIds: string[][] = [];
|
||||||
|
let ipldStateByBlock = {};
|
||||||
console.time(`time:compare-block-${blockNumber}`);
|
console.time(`time:compare-block-${blockNumber}`);
|
||||||
|
|
||||||
for (const queryName of queryNames) {
|
if (fetchIds) {
|
||||||
|
// Fetch entity ids updated at block.
|
||||||
|
console.time(`time:fetch-updated-ids-${blockNumber}`);
|
||||||
|
|
||||||
|
const updatedEntityIdPromises = queryNames.map(
|
||||||
|
queryName => {
|
||||||
|
assert(db);
|
||||||
|
|
||||||
|
return db.getEntityIdsAtBlockNumber(
|
||||||
|
blockNumber,
|
||||||
|
snakeNamingStrategy.tableName(queryName, '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
updatedEntityIds = await Promise.all(updatedEntityIdPromises);
|
||||||
|
console.timeEnd(`time:fetch-updated-ids-${blockNumber}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.watcher.verifyState) {
|
||||||
|
assert(db);
|
||||||
|
const [block] = await db?.getBlocksAtHeight(blockNumber, false);
|
||||||
|
assert(subgraphGQLClient);
|
||||||
|
ipldStateByBlock = await getIPLDStateByBlock(subgraphGQLClient, subgraphContracts, block.blockHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
await blockDelay;
|
||||||
|
for (const [index, queryName] of queryNames.entries()) {
|
||||||
try {
|
try {
|
||||||
log(`At block ${blockNumber} for query ${queryName}:`);
|
log(`At block ${blockNumber} for query ${queryName}:`);
|
||||||
|
let resultDiff = '';
|
||||||
|
|
||||||
if (fetchIds) {
|
if (fetchIds) {
|
||||||
const { idsEndpoint } = config.queries;
|
for (const id of updatedEntityIds[index]) {
|
||||||
assert(idsEndpoint, 'Specify endpoint for fetching ids when fetchId is true');
|
const { diff, result1: result } = await compareQuery(
|
||||||
const client = Object.values(clients).find(client => client.endpoint === config.endpoints[idsEndpoint]);
|
clients,
|
||||||
assert(client);
|
queryName,
|
||||||
const ids = await client.getIds(queryName, blockNumber);
|
{ block, id },
|
||||||
|
rawJson
|
||||||
|
);
|
||||||
|
|
||||||
for (const id of ids) {
|
if (config.watcher.verifyState) {
|
||||||
const isDiff = await compareAndLog(clients, queryName, { block, id }, rawJson);
|
await checkEntityInIPLDState(ipldStateByBlock, queryName, result, id, rawJson);
|
||||||
|
}
|
||||||
|
|
||||||
if (isDiff) {
|
if (diff) {
|
||||||
diffFound = isDiff;
|
resultDiff = diff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const isDiff = await compareAndLog(clients, queryName, { block }, rawJson);
|
({ diff: resultDiff } = await compareQuery(
|
||||||
|
clients,
|
||||||
if (isDiff) {
|
queryName,
|
||||||
diffFound = isDiff;
|
{ block },
|
||||||
|
rawJson
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resultDiff) {
|
||||||
|
log('Results mismatch:', resultDiff);
|
||||||
|
diffFound = true;
|
||||||
|
} else {
|
||||||
|
log('Results match.');
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
log('Error:', err.message);
|
log('Error:', err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set delay between requests for a block.
|
||||||
|
blockDelay = wait(config.queries.blockDelayInMs || 0);
|
||||||
|
|
||||||
console.timeEnd(`time:compare-block-${blockNumber}`);
|
console.timeEnd(`time:compare-block-${blockNumber}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,24 +168,21 @@ export const main = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const compareAndLog = async (
|
const checkEntityInIPLDState = async (
|
||||||
clients: { client1: Client, client2: Client },
|
ipldState: {[key: string]: any},
|
||||||
queryName: string,
|
queryName: string,
|
||||||
params: { [key: string]: any },
|
entityResult: {[key: string]: any},
|
||||||
|
id: string,
|
||||||
rawJson: boolean
|
rawJson: boolean
|
||||||
): Promise<boolean> => {
|
) => {
|
||||||
const resultDiff = await compareQuery(
|
const entityName = _.startCase(queryName);
|
||||||
clients,
|
const ipldEntity = ipldState[entityName][id];
|
||||||
queryName,
|
|
||||||
params,
|
|
||||||
rawJson
|
|
||||||
);
|
|
||||||
|
|
||||||
if (resultDiff) {
|
// Filter __typename key in GQL result.
|
||||||
log('Results mismatch:', resultDiff);
|
const resultEntity = omitDeep(entityResult[queryName], '__typename');
|
||||||
return true;
|
const diff = compareObjects(ipldEntity, resultEntity, rawJson);
|
||||||
|
|
||||||
|
if (diff) {
|
||||||
|
log('Results mismatch for IPLD state:', diff);
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Results match.');
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
@ -4,9 +4,12 @@
|
|||||||
|
|
||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
import { compareQuery, Config, getClients, getConfig } from './utils';
|
import { compareQuery, Config, getClients, getConfig } from './utils';
|
||||||
|
|
||||||
|
const log = debug('vulcanize:compare-entity');
|
||||||
|
|
||||||
export const main = async (): Promise<void> => {
|
export const main = async (): Promise<void> => {
|
||||||
const argv = await yargs.parserConfiguration({
|
const argv = await yargs.parserConfiguration({
|
||||||
'parse-numbers': false
|
'parse-numbers': false
|
||||||
@ -62,10 +65,10 @@ export const main = async (): Promise<void> => {
|
|||||||
|
|
||||||
const clients = await getClients(config, argv.queryDir);
|
const clients = await getClients(config, argv.queryDir);
|
||||||
|
|
||||||
const resultDiff = await compareQuery(clients, queryName, { id, block }, argv.rawJson);
|
const { diff } = await compareQuery(clients, queryName, { id, block }, argv.rawJson);
|
||||||
|
|
||||||
if (resultDiff) {
|
if (diff) {
|
||||||
console.log(resultDiff);
|
log(diff);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -8,25 +8,43 @@ import path from 'path';
|
|||||||
import toml from 'toml';
|
import toml from 'toml';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { diffString, diff } from 'json-diff';
|
import { diffString, diff } from 'json-diff';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { Config as CacheConfig, getCache } from '@vulcanize/cache';
|
import { Config as CacheConfig, getCache } from '@vulcanize/cache';
|
||||||
|
import { GraphQLClient } from '@vulcanize/ipld-eth-client';
|
||||||
|
import { gql } from '@apollo/client/core';
|
||||||
|
|
||||||
import { Client } from './client';
|
import { Client } from './client';
|
||||||
|
|
||||||
|
const IPLD_STATE_QUERY = `
|
||||||
|
query getState($blockHash: String!, $contractAddress: String!, $kind: String){
|
||||||
|
getState(blockHash: $blockHash, contractAddress: $contractAddress, kind: $kind){
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
interface EndpointConfig {
|
interface EndpointConfig {
|
||||||
gqlEndpoint1: string;
|
gqlEndpoint1: string;
|
||||||
gqlEndpoint2: string;
|
gqlEndpoint2: string;
|
||||||
|
requestDelayInMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryConfig {
|
interface QueryConfig {
|
||||||
queryDir: string;
|
queryDir: string;
|
||||||
names: string[];
|
names: string[];
|
||||||
idsEndpoint: keyof EndpointConfig;
|
blockDelayInMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
endpoints: EndpointConfig;
|
endpoints: EndpointConfig;
|
||||||
queries: QueryConfig;
|
queries: QueryConfig;
|
||||||
|
watcher: {
|
||||||
|
configPath: string;
|
||||||
|
entitiesDir: string;
|
||||||
|
verifyState: boolean;
|
||||||
|
endpoint: keyof EndpointConfig;
|
||||||
|
}
|
||||||
cache: {
|
cache: {
|
||||||
endpoint: keyof EndpointConfig;
|
endpoint: keyof EndpointConfig;
|
||||||
config: CacheConfig;
|
config: CacheConfig;
|
||||||
@ -52,6 +70,12 @@ export const getConfig = async (configFile: string): Promise<Config> => {
|
|||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface CompareResult {
|
||||||
|
diff: string,
|
||||||
|
result1: any,
|
||||||
|
result2: any
|
||||||
|
}
|
||||||
|
|
||||||
export const compareQuery = async (
|
export const compareQuery = async (
|
||||||
clients: {
|
clients: {
|
||||||
client1: Client,
|
client1: Client,
|
||||||
@ -60,27 +84,22 @@ export const compareQuery = async (
|
|||||||
queryName: string,
|
queryName: string,
|
||||||
params: { [key: string]: any },
|
params: { [key: string]: any },
|
||||||
rawJson: boolean
|
rawJson: boolean
|
||||||
): Promise<string> => {
|
): Promise<CompareResult> => {
|
||||||
const { client1, client2 } = clients;
|
const { client1, client2 } = clients;
|
||||||
|
|
||||||
const result2 = await client2.getResult(queryName, params);
|
const [result1, result2] = await Promise.all([
|
||||||
const result1 = await client1.getResult(queryName, params);
|
client1.getResult(queryName, params),
|
||||||
|
client2.getResult(queryName, params)
|
||||||
|
]);
|
||||||
|
|
||||||
// Getting the diff of two result objects.
|
// Getting the diff of two result objects.
|
||||||
let resultDiff;
|
const resultDiff = compareObjects(result1, result2, rawJson);
|
||||||
|
|
||||||
if (rawJson) {
|
return {
|
||||||
resultDiff = diff(result1, result2);
|
diff: resultDiff,
|
||||||
|
result1,
|
||||||
if (resultDiff) {
|
result2
|
||||||
// Use util.inspect to extend depth limit in the output.
|
};
|
||||||
resultDiff = util.inspect(diff(result1, result2), false, null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resultDiff = diffString(result1, result2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultDiff;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getClients = async (config: Config, queryDir?: string):Promise<{
|
export const getClients = async (config: Config, queryDir?: string):Promise<{
|
||||||
@ -121,3 +140,40 @@ export const getClients = async (config: Config, queryDir?: string):Promise<{
|
|||||||
client2
|
client2
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getBlockIPLDState = async (client: GraphQLClient, contracts: string[], blockHash: string): Promise<{[key: string]: any}> => {
|
||||||
|
const contractIPLDStates: {[key: string]: any}[] = await Promise.all(contracts.map(async contract => {
|
||||||
|
const { getState } = await client.query(
|
||||||
|
gql(IPLD_STATE_QUERY),
|
||||||
|
{
|
||||||
|
blockHash,
|
||||||
|
contractAddress: contract,
|
||||||
|
kind: 'diff'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (getState) {
|
||||||
|
const data = JSON.parse(getState.data);
|
||||||
|
return data.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}));
|
||||||
|
|
||||||
|
return contractIPLDStates.reduce((acc, state) => _.merge(acc, state));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compareObjects = (obj1: any, obj2: any, rawJson: boolean): string => {
|
||||||
|
if (rawJson) {
|
||||||
|
const diffObj = diff(obj1, obj2);
|
||||||
|
|
||||||
|
if (diffObj) {
|
||||||
|
// Use util.inspect to extend depth limit in the output.
|
||||||
|
return util.inspect(diffObj, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return diffString(obj1, obj2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -7,11 +7,13 @@ import {
|
|||||||
Connection,
|
Connection,
|
||||||
ConnectionOptions,
|
ConnectionOptions,
|
||||||
FindOneOptions,
|
FindOneOptions,
|
||||||
LessThanOrEqual
|
LessThanOrEqual,
|
||||||
|
Repository
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BlockHeight,
|
BlockHeight,
|
||||||
|
BlockProgressInterface,
|
||||||
Database as BaseDatabase
|
Database as BaseDatabase
|
||||||
} from '@vulcanize/util';
|
} from '@vulcanize/util';
|
||||||
|
|
||||||
@ -77,6 +79,19 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEntityIdsAtBlockNumber (blockNumber: number, tableName: string): Promise<string[]> {
|
||||||
|
const repo = this._conn.getRepository(tableName);
|
||||||
|
|
||||||
|
const entities = await repo.find({
|
||||||
|
select: ['id'],
|
||||||
|
where: {
|
||||||
|
blockNumber
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return entities.map((entity: any) => entity.id);
|
||||||
|
}
|
||||||
|
|
||||||
async getEntityWithRelations<Entity> (entity: (new () => Entity) | string, id: string, relations: { [key: string]: any }, block: BlockHeight = {}): Promise<Entity | undefined> {
|
async getEntityWithRelations<Entity> (entity: (new () => Entity) | string, id: string, relations: { [key: string]: any }, block: BlockHeight = {}): Promise<Entity | undefined> {
|
||||||
const queryRunner = this._conn.createQueryRunner();
|
const queryRunner = this._conn.createQueryRunner();
|
||||||
let { hash: blockHash, number: blockNumber } = block;
|
let { hash: blockHash, number: blockNumber } = block;
|
||||||
@ -265,4 +280,10 @@ export class Database {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBlocksAtHeight (height: number, isPruned: boolean) {
|
||||||
|
const repo: Repository<BlockProgressInterface> = this._conn.getRepository('block_progress');
|
||||||
|
|
||||||
|
return this._baseDatabase.getBlocksAtHeight(repo, height, isPruned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
Contract,
|
Contract,
|
||||||
ContractInterface
|
ContractInterface
|
||||||
} from 'ethers';
|
} from 'ethers';
|
||||||
import JSONbig from 'json-bigint';
|
|
||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
@ -26,12 +25,11 @@ import {
|
|||||||
resolveEntityFieldConflicts,
|
resolveEntityFieldConflicts,
|
||||||
getEthereumTypes,
|
getEthereumTypes,
|
||||||
jsonFromBytes,
|
jsonFromBytes,
|
||||||
getStorageValueType
|
getStorageValueType,
|
||||||
|
jsonBigIntStringReplacer
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
|
|
||||||
const JSONbigString = JSONbig({ storeAsString: true });
|
|
||||||
|
|
||||||
// Endianness of BN used in bigInt store host API.
|
// Endianness of BN used in bigInt store host API.
|
||||||
// Negative bigInt is being stored in wasm in 2's compliment, 'le' representation.
|
// Negative bigInt is being stored in wasm in 2's compliment, 'le' representation.
|
||||||
// (for eg. bigInt.fromString(negativeI32Value))
|
// (for eg. bigInt.fromString(negativeI32Value))
|
||||||
@ -104,14 +102,39 @@ export const instantiate = async (
|
|||||||
|
|
||||||
// Prepare the diff data.
|
// Prepare the diff data.
|
||||||
const diffData: any = { state: {} };
|
const diffData: any = { state: {} };
|
||||||
|
assert(indexer.getRelationsMap);
|
||||||
|
|
||||||
|
const result = Array.from(indexer.getRelationsMap().entries())
|
||||||
|
.find(([key]) => key.name === entityName);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// Update dbData 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 || !dbData[relation]) {
|
||||||
|
// Field is not present in dbData for derived relations
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray) {
|
||||||
|
dbData[relation] = dbData[relation]
|
||||||
|
.map((id: string) => ({ id }))
|
||||||
|
.sort((a: any, b: any) => a.id.localeCompare(b.id));
|
||||||
|
} else {
|
||||||
|
dbData[relation] = { id: dbData[relation] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// JSON stringify and parse data for handling unknown types when encoding.
|
// JSON stringify and parse data for handling unknown types when encoding.
|
||||||
// For example, decimal.js values are converted to string in the diff data.
|
// For example, decimal.js values are converted to string in the diff data.
|
||||||
diffData.state[entityName] = {
|
diffData.state[entityName] = {
|
||||||
// Using JSONbigString to store bigints as string values to be encoded by IPLD dag-cbor.
|
// 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.
|
// TODO: Parse and store as native bigint by using Type encoders in IPLD dag-cbor encode.
|
||||||
// https://github.com/rvagg/cborg#type-encoders
|
// https://github.com/rvagg/cborg#type-encoders
|
||||||
[dbData.id]: JSONbigString.parse(JSONbigString.stringify(dbData))
|
[dbData.id]: JSON.parse(JSON.stringify(dbData, jsonBigIntStringReplacer))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create an auto-diff.
|
// Create an auto-diff.
|
||||||
|
5
packages/graph-node/src/types/common/main.d.ts
vendored
Normal file
5
packages/graph-node/src/types/common/main.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2022 Vulcanize, Inc.
|
||||||
|
//
|
||||||
|
|
||||||
|
declare module 'omit-deep';
|
6
packages/graph-node/src/types/common/package.json
Normal file
6
packages/graph-node/src/types/common/package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "common",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"typings": "main.d.ts"
|
||||||
|
}
|
@ -798,3 +798,11 @@ const getEthereumType = (storageTypes: StorageLayout['types'], type: string, map
|
|||||||
|
|
||||||
return utils.ParamType.from(label);
|
return utils.ParamType.from(label);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const jsonBigIntStringReplacer = (_: string, value: any) => {
|
||||||
|
if (typeof value === 'bigint') {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
"downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
@ -106,6 +106,7 @@ export interface IndexerInterface {
|
|||||||
cacheContract?: (contract: ContractInterface) => void;
|
cacheContract?: (contract: ContractInterface) => void;
|
||||||
watchContract?: (address: string, kind: string, checkpoint: boolean, startingBlock: number) => Promise<void>
|
watchContract?: (address: string, kind: string, checkpoint: boolean, startingBlock: number) => Promise<void>
|
||||||
getEntityTypesMap?: () => Map<string, { [key: string]: string }>
|
getEntityTypesMap?: () => Map<string, { [key: string]: string }>
|
||||||
|
getRelationsMap?: () => Map<any, { [key: string]: any }>
|
||||||
createDiffStaged?: (contractAddress: string, blockHash: string, data: any) => Promise<void>
|
createDiffStaged?: (contractAddress: string, blockHash: string, data: any) => Promise<void>
|
||||||
processInitialState?: (contractAddress: string, blockHash: string) => Promise<any>
|
processInitialState?: (contractAddress: string, blockHash: string) => Promise<any>
|
||||||
processStateCheckpoint?: (contractAddress: string, blockHash: string) => Promise<boolean>
|
processStateCheckpoint?: (contractAddress: string, blockHash: string) => Promise<boolean>
|
||||||
|
18
yarn.lock
18
yarn.lock
@ -8596,7 +8596,7 @@ is-plain-obj@^2.0.0, is-plain-obj@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
|
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
|
||||||
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
|
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
|
||||||
|
|
||||||
is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
|
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
|
||||||
integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
|
integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
|
||||||
@ -10817,6 +10817,14 @@ oboe@2.1.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
http-https "^1.0.0"
|
http-https "^1.0.0"
|
||||||
|
|
||||||
|
omit-deep@^0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/omit-deep/-/omit-deep-0.3.0.tgz#21c8af3499bcadd29651a232cbcacbc52445ebec"
|
||||||
|
integrity sha512-Lbl/Ma59sss2b15DpnWnGmECBRL8cRl/PjPbPMVW+Y8zIQzRrwMaI65Oy6HvxyhYeILVKBJb2LWeG81bj5zbMg==
|
||||||
|
dependencies:
|
||||||
|
is-plain-object "^2.0.1"
|
||||||
|
unset-value "^0.1.1"
|
||||||
|
|
||||||
on-finished@~2.3.0:
|
on-finished@~2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||||
@ -13984,6 +13992,14 @@ unpipe@1.0.0, unpipe@~1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
|
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
|
||||||
|
|
||||||
|
unset-value@^0.1.1:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-0.1.2.tgz#506810b867f27c2a5a6e9b04833631f6de58d310"
|
||||||
|
integrity sha512-yhv5I4TsldLdE3UcVQn0hD2T5sNCPv4+qm/CTUpRKIpwthYRIipsAPdsrNpOI79hPQa0rTTeW22Fq6JWRcTgNg==
|
||||||
|
dependencies:
|
||||||
|
has-value "^0.3.1"
|
||||||
|
isobject "^3.0.0"
|
||||||
|
|
||||||
unset-value@^1.0.0:
|
unset-value@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
|
resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
|
||||||
|
Loading…
Reference in New Issue
Block a user