Implement subgraph store host API (#35)

* Implement store get api without blockHash and blockNumber

* Pass database instance to GraphWatcher

* Implement store set without block data

* Store blockHash and blockNumber in database entity table

* Implement getting entity in subgraph from store.get

* Add block data present in postgraphile

* Pass db and context to instantiate method in tests

* GQL API in graph-test-watcher to test store.set

* Remove contract address from subgraph file

* Fix block in dummy event data

* Pass just blockHash to get an entity from the database

* Review changes and add TODOs

Co-authored-by: prathamesh <prathamesh.musale0@gmail.com>
This commit is contained in:
nikugogoi 2021-11-10 13:12:37 +05:30 committed by nabarun
parent 43d64f9e4b
commit 83775608ec
23 changed files with 486 additions and 79 deletions

View File

@ -5,9 +5,13 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@graphprotocol/graph-ts": "^0.22.0", "@graphprotocol/graph-ts": "^0.22.0",
"@types/chai": "^4.2.18",
"@types/chai-spies": "^1.0.3",
"@types/js-yaml": "^4.0.4", "@types/js-yaml": "^4.0.4",
"@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0", "@typescript-eslint/parser": "^4.25.0",
"chai": "^4.3.4",
"chai-spies": "^1.0.0",
"eslint": "^7.27.0", "eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1", "eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3", "eslint-config-standard": "^16.0.3",
@ -32,6 +36,7 @@
}, },
"dependencies": { "dependencies": {
"@vulcanize/assemblyscript": "0.0.1", "@vulcanize/assemblyscript": "0.0.1",
"js-yaml": "^4.1.0" "js-yaml": "^4.1.0",
"typeorm": "^0.2.32"
} }
} }

View File

@ -3,17 +3,54 @@
// //
import path from 'path'; import path from 'path';
import chai, { assert, expect } from 'chai';
import spies from 'chai-spies';
import { getDummyEventData } from '../test/utils'; import { getDummyEventData, getTestDatabase } from '../test/utils';
import { instantiate } from './loader'; import { instantiate } from './loader';
import { createEvent } from './utils'; import { createEvent, Block } from './utils';
import { Database } from './database';
chai.use(spies);
const sandbox = chai.spy.sandbox();
describe('call handler in mapping code', () => { describe('call handler in mapping code', () => {
let exports: any; let exports: any;
let db: Database;
const eventData = getDummyEventData();
before(async () => {
db = getTestDatabase();
sandbox.on(db, 'getEntity', (blockHash: string, entityString: string, idString: string) => {
assert(blockHash);
assert(entityString);
assert(idString);
});
sandbox.on(db, 'fromGraphEntity', async (instanceExports: any, block: Block, entity: string, entityInstance: any) => {
const entityFields = [
{ type: 'varchar', propertyName: 'blockHash' },
{ type: 'integer', propertyName: 'blockNumber' },
{ type: 'bigint', propertyName: 'count' },
{ type: 'varchar', propertyName: 'param1' },
{ type: 'integer', propertyName: 'param2' }
];
return db.getEntityValues(instanceExports, block, entityInstance, entityFields);
});
sandbox.on(db, 'saveEntity', (entity: string, data: any) => {
assert(entity);
assert(data);
});
});
it('should load the subgraph example wasm', async () => { it('should load the subgraph example wasm', async () => {
const filePath = path.resolve(__dirname, '../test/subgraph/example1/build/Example1/Example1.wasm'); const filePath = path.resolve(__dirname, '../test/subgraph/example1/build/Example1/Example1.wasm');
const instance = await instantiate(filePath); const instance = await instantiate(db, { event: { block: eventData.block } }, filePath);
exports = instance.exports; exports = instance.exports;
}); });
@ -27,8 +64,6 @@ describe('call handler in mapping code', () => {
// TODO: Check api version https://github.com/graphprotocol/graph-node/blob/6098daa8955bdfac597cec87080af5449807e874/runtime/wasm/src/module/mod.rs#L533 // TODO: Check api version https://github.com/graphprotocol/graph-node/blob/6098daa8955bdfac597cec87080af5449807e874/runtime/wasm/src/module/mod.rs#L533
_start(); _start();
const eventData = getDummyEventData();
// Create event params data. // Create event params data.
eventData.eventParams = [ eventData.eventParams = [
{ {
@ -50,5 +85,13 @@ describe('call handler in mapping code', () => {
const test = await createEvent(exports, contractAddress, eventData); const test = await createEvent(exports, contractAddress, eventData);
await handleTest(test); await handleTest(test);
expect(db.getEntity).to.have.been.called();
expect(db.fromGraphEntity).to.have.been.called();
expect(db.saveEntity).to.have.been.called();
});
after(() => {
sandbox.restore();
}); });
}); });

View File

@ -0,0 +1,136 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import assert from 'assert';
import {
Connection,
ConnectionOptions,
FindOneOptions
} from 'typeorm';
import {
Database as BaseDatabase
} from '@vulcanize/util';
import { Block, fromEntityValue, toEntityValue } from './utils';
export class Database {
_config: ConnectionOptions
_conn!: Connection
_baseDatabase: BaseDatabase
constructor (config: ConnectionOptions, entitiesPath: string) {
assert(config);
this._config = {
name: 'subgraph',
...config,
entities: [entitiesPath]
};
this._baseDatabase = new BaseDatabase(this._config);
}
async init (): Promise<void> {
this._conn = await this._baseDatabase.init();
}
async close (): Promise<void> {
return this._baseDatabase.close();
}
async getEntity (blockHash: string, entity: string, id: string): Promise<any> {
const queryRunner = this._conn.createQueryRunner();
const repo = queryRunner.manager.getRepository(entity);
const whereOptions: { [key: string]: any } = { id };
if (blockHash) {
whereOptions.blockHash = blockHash;
}
const findOptions = {
where: whereOptions,
order: {
blockNumber: 'DESC'
}
};
let entityData = await repo.findOne(findOptions as FindOneOptions<any>);
if (!entityData && findOptions.where.blockHash) {
entityData = await this._baseDatabase.getPrevEntityVersion(queryRunner, repo, findOptions);
}
return entityData;
}
async saveEntity (entity: string, data: any): Promise<void> {
const repo = this._conn.getRepository(entity);
const dbEntity: any = repo.create(data);
await repo.save(dbEntity);
}
async toGraphEntity (instanceExports: any, entity: string, data: any): Promise<any> {
// TODO: Cache schema/columns.
const repo = this._conn.getRepository(entity);
const entityFields = repo.metadata.columns;
const { Entity } = instanceExports;
const entityInstance = await Entity.__new();
const entityValuePromises = entityFields.filter(field => {
const { propertyName } = field;
// TODO: Will clash if entity has blockHash and blockNumber fields.
if (propertyName === 'blockHash' || propertyName === 'blockNumber') {
return false;
}
return true;
}).map(async (field) => {
const { type, propertyName } = field;
return toEntityValue(instanceExports, entityInstance, data, type.toString(), propertyName);
}, {});
await Promise.all(entityValuePromises);
return entityInstance;
}
async fromGraphEntity (instanceExports: any, block: Block, entity: string, entityInstance: any): Promise<{ [key: string]: any } > {
// TODO: Cache schema/columns.
const repo = this._conn.getRepository(entity);
const entityFields = repo.metadata.columns;
return this.getEntityValues(instanceExports, block, entityInstance, entityFields);
}
async getEntityValues (instanceExports: any, block: Block, entityInstance: any, entityFields: any): Promise<{ [key: string]: any } > {
const entityValuePromises = entityFields.map(async (field: any) => {
const { type, propertyName } = field;
// TODO: Will clash if entity has blockHash and blockNumber fields.
if (propertyName === 'blockHash') {
return block.blockHash;
}
if (propertyName === 'blockNumber') {
return block.blockNumber;
}
return fromEntityValue(instanceExports, entityInstance, type.toString(), propertyName);
}, {});
const entityValues = await Promise.all(entityValuePromises);
return entityFields.reduce((acc: { [key: string]: any }, field: any, index: number) => {
const { propertyName } = field;
acc[propertyName] = entityValues[index];
return acc;
}, {});
}
}

View File

@ -5,19 +5,48 @@
import assert from 'assert'; import assert from 'assert';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import path from 'path'; import path from 'path';
import chai from 'chai';
import spies from 'chai-spies';
import { instantiate } from './loader'; import { instantiate } from './loader';
import { createEvent } from './utils'; import { createEvent, Block } from './utils';
import edenNetworkAbi from '../test/subgraph/eden/EdenNetwork/abis/EdenNetwork.json'; import edenNetworkAbi from '../test/subgraph/eden/EdenNetwork/abis/EdenNetwork.json';
import merkleDistributorAbi from '../test/subgraph/eden/EdenNetworkDistribution/abis/MerkleDistributor.json'; import merkleDistributorAbi from '../test/subgraph/eden/EdenNetworkDistribution/abis/MerkleDistributor.json';
import distributorGovernanceAbi from '../test/subgraph/eden/EdenNetworkGovernance/abis/DistributorGovernance.json'; import distributorGovernanceAbi from '../test/subgraph/eden/EdenNetworkGovernance/abis/DistributorGovernance.json';
import { getDummyEventData } from '../test/utils'; import { getDummyEventData, getTestDatabase } from '../test/utils';
import { Database } from './database';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
describe('eden wasm loader tests', () => { chai.use(spies);
const sandbox = chai.spy.sandbox();
describe('eden wasm loader tests', async () => {
let db: Database;
const eventData = getDummyEventData(); const eventData = getDummyEventData();
before(async () => {
db = getTestDatabase();
sandbox.on(db, 'getEntity', (blockHash: string, entityString: string, idString: string) => {
assert(blockHash);
assert(entityString);
assert(idString);
});
sandbox.on(db, 'fromGraphEntity', async (instanceExports: any, block: Block, entity: string, entityInstance: any) => {
const entityFields: any = [];
return db.getEntityValues(instanceExports, block, entityInstance, entityFields);
});
sandbox.on(db, 'saveEntity', (entity: string, data: any) => {
assert(entity);
assert(data);
});
});
describe('EdenNetwork wasm', () => { describe('EdenNetwork wasm', () => {
let exports: any; let exports: any;
@ -36,7 +65,7 @@ describe('eden wasm loader tests', () => {
it('should load the subgraph network wasm', async () => { it('should load the subgraph network wasm', async () => {
const filePath = path.resolve(__dirname, '../test/subgraph/eden/EdenNetwork/EdenNetwork.wasm'); const filePath = path.resolve(__dirname, '../test/subgraph/eden/EdenNetwork/EdenNetwork.wasm');
({ exports } = await instantiate(filePath, data)); ({ exports } = await instantiate(db, { event: { block: eventData.block } }, filePath, data));
const { _start } = exports; const { _start } = exports;
_start(); _start();
}); });
@ -195,7 +224,7 @@ describe('eden wasm loader tests', () => {
it('should load the subgraph network distribution wasm', async () => { it('should load the subgraph network distribution wasm', async () => {
const filePath = path.resolve(__dirname, '../test/subgraph/eden/EdenNetworkDistribution/EdenNetworkDistribution.wasm'); const filePath = path.resolve(__dirname, '../test/subgraph/eden/EdenNetworkDistribution/EdenNetworkDistribution.wasm');
({ exports } = await instantiate(filePath, data)); ({ exports } = await instantiate(db, { event: { block: eventData.block } }, filePath, data));
const { _start } = exports; const { _start } = exports;
_start(); _start();
}); });
@ -339,7 +368,7 @@ describe('eden wasm loader tests', () => {
it('should load the subgraph network governance wasm', async () => { it('should load the subgraph network governance wasm', async () => {
const filePath = path.resolve(__dirname, '../test/subgraph/eden/EdenNetworkGovernance/EdenNetworkGovernance.wasm'); const filePath = path.resolve(__dirname, '../test/subgraph/eden/EdenNetworkGovernance/EdenNetworkGovernance.wasm');
({ exports } = await instantiate(filePath, data)); ({ exports } = await instantiate(db, { event: { block: eventData.block } }, filePath, data));
const { _start } = exports; const { _start } = exports;
_start(); _start();
}); });
@ -427,4 +456,8 @@ describe('eden wasm loader tests', () => {
await rewardScheduleChanged(rewardScheduleChangedEvent); await rewardScheduleChanged(rewardScheduleChangedEvent);
}); });
}); });
after(() => {
sandbox.restore();
});
}); });

View File

@ -7,9 +7,12 @@ import path from 'path';
import { instantiate } from './loader'; import { instantiate } from './loader';
import exampleAbi from '../test/subgraph/example1/build/Example1/abis/Example1.json'; import exampleAbi from '../test/subgraph/example1/build/Example1/abis/Example1.json';
import { getTestDatabase } from '../test/utils';
import { Database } from './database';
describe('eth-call wasm tests', () => { describe('eth-call wasm tests', () => {
let exports: any; let exports: any;
let db: Database;
const contractAddress = process.env.EXAMPLE_CONTRACT_ADDRESS; const contractAddress = process.env.EXAMPLE_CONTRACT_ADDRESS;
assert(contractAddress); assert(contractAddress);
@ -23,9 +26,13 @@ describe('eth-call wasm tests', () => {
} }
}; };
before(async () => {
db = getTestDatabase();
});
it('should load the subgraph example wasm', async () => { it('should load the subgraph example wasm', async () => {
const filePath = path.resolve(__dirname, '../test/subgraph/example1/build/Example1/Example1.wasm'); const filePath = path.resolve(__dirname, '../test/subgraph/example1/build/Example1/Example1.wasm');
const instance = await instantiate(filePath, data); const instance = await instantiate(db, { event: {} }, filePath, data);
exports = instance.exports; exports = instance.exports;
}); });

View File

@ -1 +1,2 @@
export * from './watcher'; export * from './watcher';
export * from './database';

View File

@ -6,15 +6,21 @@ import path from 'path';
import { expect } from 'chai'; import { expect } from 'chai';
import { instantiate } from './loader'; import { instantiate } from './loader';
import { getTestDatabase } from '../test/utils';
import { Database } from './database';
const WASM_FILE_PATH = '../build/debug.wasm'; const WASM_FILE_PATH = '../build/debug.wasm';
describe('wasm loader tests', () => { describe('wasm loader tests', () => {
let exports: any; let exports: any;
let db: Database;
before(async () => { before(async () => {
db = getTestDatabase();
const filePath = path.resolve(__dirname, WASM_FILE_PATH); const filePath = path.resolve(__dirname, WASM_FILE_PATH);
const instance = await instantiate(filePath); const instance = await instantiate(db, { event: {} }, filePath);
exports = instance.exports; exports = instance.exports;
}); });

View File

@ -14,7 +14,8 @@ import {
} from 'ethers'; } from 'ethers';
import { TypeId } from './types'; import { TypeId } from './types';
import { fromEthereumValue, toEthereumValue } from './utils'; import { Block, fromEthereumValue, toEthereumValue } from './utils';
import { Database } from './database';
const NETWORK_URL = 'http://127.0.0.1:8081'; const NETWORK_URL = 'http://127.0.0.1:8081';
@ -29,7 +30,13 @@ interface GraphData {
dataSource?: DataSource; dataSource?: DataSource;
} }
export const instantiate = async (filePath: string, data: GraphData = {}): Promise<loader.ResultObject & { exports: any }> => { export interface Context {
event: {
block?: Block
}
}
export const instantiate = async (database: Database, context: Context, filePath: string, data: GraphData = {}): Promise<loader.ResultObject & { exports: any }> => {
const { abis = {}, dataSource } = data; const { abis = {}, dataSource } = data;
const buffer = await fs.readFile(filePath); const buffer = await fs.readFile(filePath);
const provider = getDefaultProvider(NETWORK_URL); const provider = getDefaultProvider(NETWORK_URL);
@ -37,31 +44,26 @@ export const instantiate = async (filePath: string, data: GraphData = {}): Promi
const imports: WebAssembly.Imports = { const imports: WebAssembly.Imports = {
index: { index: {
'store.get': async (entity: number, id: number) => { 'store.get': async (entity: number, id: number) => {
console.log('store.get'); const entityName = __getString(entity);
const entityId = __getString(id);
const entityString = __getString(entity); assert(context.event.block);
console.log('entity:', entityString); const entityData = await database.getEntity(context.event.block.blockHash, entityName, entityId);
const idString = __getString(id);
console.log('id:', idString);
// TODO: Implement store get to fetch from DB using entity and id. if (!entityData) {
// TODO: Fill entity with field values.
// return Entity.__new()
return null; return null;
}
return database.toGraphEntity(exports, entityName, entityData);
}, },
'store.set': async (entity: number, id: number, data: number) => { 'store.set': async (entity: number, id: number, data: number) => {
console.log('store.set'); const entityName = __getString(entity);
const entityString = __getString(entity);
console.log('entity:', entityString);
const idString = __getString(id);
console.log('id:', idString);
const entityInstance = await Entity.wrap(data); const entityInstance = await Entity.wrap(data);
const entityInstanceId = __getString(await entityInstance.getString(await __newString('id')));
console.log('entity instance id:', entityInstanceId);
// TODO: Implement store set to save entity in db with values from entityInstance. assert(context.event.block);
const dbData = await database.fromGraphEntity(exports, context.event.block, entityName, entityInstance);
await database.saveEntity(entityName, dbData);
}, },
'typeConversion.stringToH160': () => { 'typeConversion.stringToH160': () => {

View File

@ -6,15 +6,20 @@ import path from 'path';
import { expect } from 'chai'; import { expect } from 'chai';
import { instantiate } from './loader'; import { instantiate } from './loader';
import { getTestDatabase } from '../test/utils';
import { Database } from './database';
const EXAMPLE_WASM_FILE_PATH = '../test/subgraph/example1/build/Example1/Example1.wasm'; const EXAMPLE_WASM_FILE_PATH = '../test/subgraph/example1/build/Example1/Example1.wasm';
describe('numbers wasm tests', () => { describe('numbers wasm tests', () => {
let exports: any; let exports: any;
let db: Database;
before(async () => { before(async () => {
db = getTestDatabase();
const filePath = path.resolve(__dirname, EXAMPLE_WASM_FILE_PATH); const filePath = path.resolve(__dirname, EXAMPLE_WASM_FILE_PATH);
const instance = await instantiate(filePath); const instance = await instantiate(db, { event: {} }, filePath);
exports = instance.exports; exports = instance.exports;
const { _start } = exports; const { _start } = exports;

View File

@ -6,15 +6,20 @@ import path from 'path';
import { expect } from 'chai'; import { expect } from 'chai';
import { instantiate } from './loader'; import { instantiate } from './loader';
import { getTestDatabase } from '../test/utils';
import { Database } from './database';
const EXAMPLE_WASM_FILE_PATH = '../test/subgraph/example1/build/Example1/Example1.wasm'; const EXAMPLE_WASM_FILE_PATH = '../test/subgraph/example1/build/Example1/Example1.wasm';
describe('typeConversion wasm tests', () => { describe('typeConversion wasm tests', () => {
let exports: any; let exports: any;
let db: Database;
before(async () => { before(async () => {
db = getTestDatabase();
const filePath = path.resolve(__dirname, EXAMPLE_WASM_FILE_PATH); const filePath = path.resolve(__dirname, EXAMPLE_WASM_FILE_PATH);
const instance = await instantiate(filePath); const instance = await instantiate(db, { event: {} }, filePath);
exports = instance.exports; exports = instance.exports;
const { _start } = exports; const { _start } = exports;

View File

@ -14,13 +14,6 @@ interface EventParam {
kind: string; kind: string;
} }
interface Block {
hash: string;
number: number;
timestamp: number;
parentHash: string;
}
interface Transaction { interface Transaction {
hash: string; hash: string;
index: number; index: number;
@ -28,6 +21,17 @@ interface Transaction {
to: string; to: string;
} }
export interface Block {
blockHash: string;
blockNumber: string;
timestamp: string;
parentHash: string;
stateRoot: string;
td: string;
txRoot: string;
receiptRoot: string;
}
export interface EventData { export interface EventData {
block: Block; block: Block;
tx: Transaction; tx: Transaction;
@ -161,41 +165,48 @@ export const createEvent = async (instanceExports: any, contractAddress: string,
} = instanceExports; } = instanceExports;
// Fill block data. // Fill block data.
const blockHashByteArray = await ByteArray.fromHexString(await __newString(blockData.hash)); const blockHashByteArray = await ByteArray.fromHexString(await __newString(blockData.blockHash));
const blockHash = await Bytes.fromByteArray(blockHashByteArray); const blockHash = await Bytes.fromByteArray(blockHashByteArray);
const parentHashByteArray = await ByteArray.fromHexString(await __newString(blockData.parentHash)); const parentHashByteArray = await ByteArray.fromHexString(await __newString(blockData.parentHash));
const parentHash = await Bytes.fromByteArray(parentHashByteArray); const parentHash = await Bytes.fromByteArray(parentHashByteArray);
const blockNumber = await BigInt.fromI32(blockData.number); const blockNumber = await BigInt.fromString(await __newString(blockData.blockNumber));
const blockTimestamp = await BigInt.fromI32(blockData.timestamp); const blockTimestamp = await BigInt.fromString(await __newString(blockData.timestamp));
const stateRootByteArray = await ByteArray.fromHexString(await __newString(blockData.stateRoot));
const stateRoot = await Bytes.fromByteArray(stateRootByteArray);
const transactionsRootByteArray = await ByteArray.fromHexString(await __newString(blockData.txRoot));
const transactionsRoot = await Bytes.fromByteArray(transactionsRootByteArray);
const receiptsRootByteArray = await ByteArray.fromHexString(await __newString(blockData.receiptRoot));
const receiptsRoot = await Bytes.fromByteArray(receiptsRootByteArray);
const totalDifficulty = await BigInt.fromString(await __newString(blockData.td));
// Missing fields from watcher in block data: // Missing fields from watcher in block data:
// unclesHash // unclesHash
// author // author
// stateRoot
// transactionsRoot
// receiptsRoot
// gasUsed // gasUsed
// gasLimit // gasLimit
// difficulty // difficulty
// totalDifficulty
// size // size
const block = await ethereum.Block.__new( const block = await ethereum.Block.__new(
blockHash, blockHash,
parentHash, parentHash,
await Bytes.empty(), await Bytes.empty(),
await Address.zero(), await Address.zero(),
await Bytes.empty(), stateRoot,
await Bytes.empty(), transactionsRoot,
await Bytes.empty(), receiptsRoot,
blockNumber, blockNumber,
await BigInt.fromI32(0), await BigInt.fromI32(0),
await BigInt.fromI32(0), await BigInt.fromI32(0),
blockTimestamp, blockTimestamp,
await BigInt.fromI32(0), await BigInt.fromI32(0),
await BigInt.fromI32(0), totalDifficulty,
null null
); );
@ -266,3 +277,55 @@ export const getSubgraphConfig = async (subgraphPath: string): Promise<any> => {
return config; return config;
}; };
export const toEntityValue = async (instanceExports: any, entityInstance: any, data: any, type: string, key: string) => {
const { __newString, BigInt: ExportBigInt } = instanceExports;
const entityKey = await __newString(key);
const value = data[key];
switch (type) {
case 'varchar': {
const entityValue = await __newString(value);
return entityInstance.setString(entityKey, entityValue);
}
case 'integer': {
return entityInstance.setI32(entityKey, value);
}
case 'bigint': {
const bigInt = await ExportBigInt.fromString(await __newString(value.toString()));
return entityInstance.setBigInt(entityKey, bigInt);
}
// TODO: Support more types.
default:
throw new Error(`Unsupported type: ${type}`);
}
};
export const fromEntityValue = async (instanceExports: any, entityInstance: any, type: string, key: string): Promise<any> => {
const { __newString, __getString, BigInt: ExportBigInt } = instanceExports;
const entityKey = await __newString(key);
switch (type) {
case 'varchar': {
return __getString(await entityInstance.getString(entityKey));
}
case 'integer': {
return entityInstance.getI32(entityKey);
}
case 'bigint': {
const bigInt = ExportBigInt.wrap(await entityInstance.getBigInt(entityKey));
return BigInt(__getString(await bigInt.toString()));
}
// TODO: Support more types.
default:
throw new Error(`Unsupported type: ${type}`);
}
};

View File

@ -9,9 +9,11 @@ import fs from 'fs';
import { ContractInterface, utils } from 'ethers'; import { ContractInterface, utils } from 'ethers';
import { ResultObject } from '@vulcanize/assemblyscript/lib/loader'; import { ResultObject } from '@vulcanize/assemblyscript/lib/loader';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { createEvent, getSubgraphConfig } from './utils'; import { createEvent, getSubgraphConfig } from './utils';
import { instantiate } from './loader'; import { Context, instantiate } from './loader';
import { Database } from './database';
const log = debug('vulcanize:graph-watcher'); const log = debug('vulcanize:graph-watcher');
@ -21,11 +23,19 @@ interface DataSource {
} }
export class GraphWatcher { export class GraphWatcher {
_database: Database;
_postgraphileClient: EthClient;
_subgraphPath: string; _subgraphPath: string;
_dataSources: any[] = []; _dataSources: any[] = [];
_dataSourceMap: { [key: string]: DataSource } = {}; _dataSourceMap: { [key: string]: DataSource } = {};
constructor (subgraphPath: string) { _context: Context = {
event: {}
}
constructor (database: Database, postgraphileClient: EthClient, subgraphPath: string) {
this._database = database;
this._postgraphileClient = postgraphileClient;
this._subgraphPath = subgraphPath; this._subgraphPath = subgraphPath;
} }
@ -58,7 +68,7 @@ export class GraphWatcher {
const filePath = path.join(this._subgraphPath, file); const filePath = path.join(this._subgraphPath, file);
return { return {
instance: await instantiate(filePath, data), instance: await instantiate(this._database, this._context, filePath, data),
contractInterface contractInterface
}; };
}, {}); }, {});
@ -83,6 +93,16 @@ export class GraphWatcher {
async handleEvent (eventData: any) { async handleEvent (eventData: any) {
const { contract, event, eventSignature, block, tx, eventIndex } = eventData; const { contract, event, eventSignature, block, tx, eventIndex } = eventData;
const {
allEthHeaderCids: {
nodes: [
blockData
]
}
} = await this._postgraphileClient.getBlocks({ blockHash: block.hash });
this._context.event.block = blockData;
// Get dataSource in subgraph yaml based on contract address. // Get dataSource in subgraph yaml based on contract address.
const dataSource = this._dataSources.find(dataSource => dataSource.source.address === contract); const dataSource = this._dataSources.find(dataSource => dataSource.source.address === contract);
@ -113,7 +133,7 @@ export class GraphWatcher {
const data = { const data = {
eventParams: eventParams, eventParams: eventParams,
block, block: blockData,
tx, tx,
eventIndex eventIndex
}; };
@ -123,4 +143,8 @@ export class GraphWatcher {
await exports[eventHandler.handler](ethereumEvent); await exports[eventHandler.handler](ethereumEvent);
} }
async getEntity (blockHash: string, entity: string, id: string): Promise<any> {
return this._database.getEntity(blockHash, entity, id);
}
} }

View File

@ -4,36 +4,37 @@ import {
Example1, Example1,
Test Test
} from '../generated/Example1/Example1'; } from '../generated/Example1/Example1';
// import { ExampleEntity } from '../generated/schema'; import { ExampleEntity } from '../generated/schema';
export function handleTest (event: Test): void { export function handleTest (event: Test): void {
log.debug('event.address: {}', [event.address.toHexString()]); log.debug('event.address: {}', [event.address.toHexString()]);
log.debug('event.params.param1: {}', [event.params.param1]); log.debug('event.params.param1: {}', [event.params.param1]);
log.debug('event.params.param2: {}', [event.params.param2.toString()]); log.debug('event.params.param2: {}', [event.params.param2.toString()]);
log.debug('event.block.hash: {}', [event.block.hash.toHexString()]); log.debug('event.block.hash: {}', [event.block.hash.toHexString()]);
log.debug('event.block.stateRoot: {}', [event.block.stateRoot.toHexString()]);
// Entities can be loaded from the store using a string ID; this ID // Entities can be loaded from the store using a string ID; this ID
// needs to be unique across all entities of the same type // needs to be unique across all entities of the same type
// let entity = ExampleEntity.load(event.transaction.from.toHex()); let entity = ExampleEntity.load(event.transaction.from.toHex());
// Entities only exist after they have been saved to the store; // Entities only exist after they have been saved to the store;
// `null` checks allow to create entities on demand // `null` checks allow to create entities on demand
// if (!entity) { if (!entity) {
// entity = new ExampleEntity(event.transaction.from.toHex()); entity = new ExampleEntity(event.transaction.from.toHex());
// // Entity fields can be set using simple assignments // Entity fields can be set using simple assignments
// entity.count = BigInt.fromI32(0); entity.count = BigInt.fromString('0');
// } }
// BigInt and BigDecimal math are supported // BigInt and BigDecimal math are supported
// entity.count = entity.count + BigInt.fromI32(1) entity.count = entity.count + BigInt.fromString('1');
// Entity fields can be set based on event parameters // Entity fields can be set based on event parameters
// entity.param1 = event.params.param1; entity.param1 = event.params.param1;
// entity.param2 = event.params.param2; entity.param2 = event.params.param2;
// Entities can be written to the store with `.save()` // Entities can be written to the store with `.save()`
// entity.save(); entity.save();
// Note: If a handler doesn't require existing field values, it is faster // Note: If a handler doesn't require existing field values, it is faster
// _not_ to load the entity from the store. Instead, create it fresh with // _not_ to load the entity from the store. Instead, create it fresh with

View File

@ -6,7 +6,7 @@ dataSources:
name: Example1 name: Example1
network: mainnet network: mainnet
source: source:
address: "0x4Ab7aE18973491Df21d6103dfA55170fdB2CCC98" address: ""
abi: Example1 abi: Example1
mapping: mapping:
kind: ethereum/events kind: ethereum/events

View File

@ -3,16 +3,21 @@
// //
import { EventData } from '../../src/utils'; import { EventData } from '../../src/utils';
import { Database } from '../../src/database';
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export const ZERO_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000'; export const ZERO_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000';
export const getDummyEventData = (): EventData => { export const getDummyEventData = (): EventData => {
const block = { const block = {
hash: ZERO_HASH, blockHash: ZERO_HASH,
number: 0, blockNumber: '0',
timestamp: 0, timestamp: '0',
parentHash: ZERO_HASH parentHash: ZERO_HASH,
stateRoot: ZERO_HASH,
td: ZERO_HASH,
txRoot: ZERO_HASH,
receiptRoot: ZERO_HASH
}; };
const tx = { const tx = {
@ -29,3 +34,7 @@ export const getDummyEventData = (): EventData => {
eventIndex: 0 eventIndex: 0
}; };
}; };
export const getTestDatabase = (): Database => {
return new Database({ type: 'postgres' }, '');
};

View File

@ -0,0 +1,27 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import { Entity, PrimaryColumn, Column } from 'typeorm';
@Entity()
export class ExampleEntity {
@PrimaryColumn('varchar')
id!: string;
// https://typeorm.io/#/entities/primary-columns
@PrimaryColumn('varchar', { length: 66 })
blockHash!: string
@Column('integer')
blockNumber!: number;
@Column('bigint')
count!: bigint
@Column('varchar')
param1!: string
@Column('integer')
param2!: number
}

View File

@ -8,10 +8,12 @@ import yargs from 'yargs';
import { hideBin } from 'yargs/helpers'; import { hideBin } from 'yargs/helpers';
import debug from 'debug'; import debug from 'debug';
import { PubSub } from 'apollo-server-express'; import { PubSub } from 'apollo-server-express';
import path from 'path';
import { getCache } from '@vulcanize/cache'; import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client'; import { EthClient } from '@vulcanize/ipld-eth-client';
import { getConfig, fillBlocks, JobQueue, DEFAULT_CONFIG_PATH, getCustomProvider } from '@vulcanize/util'; import { getConfig, fillBlocks, JobQueue, DEFAULT_CONFIG_PATH, getCustomProvider } from '@vulcanize/util';
import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node';
import { Database } from './database'; import { Database } from './database';
import { Indexer } from './indexer'; import { Indexer } from './indexer';
@ -46,7 +48,7 @@ export const main = async (): Promise<any> => {
assert(config.server, 'Missing server config'); assert(config.server, 'Missing server config');
const { upstream, database: dbConfig, jobQueue: jobQueueConfig } = config; const { upstream, database: dbConfig, jobQueue: jobQueueConfig, server: { subgraphPath } } = config;
assert(dbConfig, 'Missing database config'); assert(dbConfig, 'Missing database config');
@ -72,10 +74,16 @@ export const main = async (): Promise<any> => {
const ethProvider = getCustomProvider(rpcProviderEndpoint); const ethProvider = getCustomProvider(rpcProviderEndpoint);
const graphDb = new GraphDatabase(dbConfig, path.resolve(__dirname, 'entity/*'));
await graphDb.init();
const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, subgraphPath);
await graphWatcher.init();
// Note: In-memory pubsub works fine for now, as each watcher is a single process anyway. // Note: In-memory pubsub works fine for now, as each watcher is a single process anyway.
// Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries // Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries
const pubsub = new PubSub(); const pubsub = new PubSub();
const indexer = new Indexer(db, ethClient, postgraphileClient, ethProvider); const indexer = new Indexer(db, ethClient, postgraphileClient, ethProvider, graphWatcher);
const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig; const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig;
assert(dbConnectionString, 'Missing job queue db connection string'); assert(dbConnectionString, 'Missing job queue db connection string');

View File

@ -167,6 +167,10 @@ export class Indexer {
return result; return result;
} }
async getExampleEntity (blockHash: string, id: string): Promise<string> {
return this._graphWatcher.getEntity(blockHash, 'ExampleEntity', id);
}
async triggerIndexingOnEvent (event: Event): Promise<void> { async triggerIndexingOnEvent (event: Event): Promise<void> {
const resultEvent = this.getResultEvent(event); const resultEvent = this.getResultEvent(event);

View File

@ -7,6 +7,7 @@ import 'reflect-metadata';
import yargs from 'yargs'; import yargs from 'yargs';
import { hideBin } from 'yargs/helpers'; import { hideBin } from 'yargs/helpers';
import debug from 'debug'; import debug from 'debug';
import path from 'path';
import { getCache } from '@vulcanize/cache'; import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client'; import { EthClient } from '@vulcanize/ipld-eth-client';
@ -20,7 +21,7 @@ import {
DEFAULT_CONFIG_PATH, DEFAULT_CONFIG_PATH,
getCustomProvider getCustomProvider
} from '@vulcanize/util'; } from '@vulcanize/util';
import { GraphWatcher } from '@vulcanize/graph-node'; import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node';
import { Indexer } from './indexer'; import { Indexer } from './indexer';
import { Database } from './database'; import { Database } from './database';
@ -107,7 +108,10 @@ export const main = async (): Promise<any> => {
cache cache
}); });
const graphWatcher = new GraphWatcher(subgraphPath); const graphDb = new GraphDatabase(dbConfig, path.resolve(__dirname, 'entity/*'));
await graphDb.init();
const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, subgraphPath);
await graphWatcher.init(); await graphWatcher.init();
const ethProvider = getCustomProvider(rpcProviderEndpoint); const ethProvider = getCustomProvider(rpcProviderEndpoint);

View File

@ -73,6 +73,14 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber); const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event)); return events.map(event => indexer.getResultEvent(event));
},
exampleEntity: async (_: any, { blockHash, id }: { blockHash: string, id: string }) => {
log('exampleEntity', blockHash, id);
const exampleEntity = await indexer.getExampleEntity(blockHash, id);
return JSON.stringify(exampleEntity, undefined, 2);
} }
} }
}; };

View File

@ -3,6 +3,7 @@ type Query {
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!] eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!): [ResultEvent!]
getMethod(blockHash: String!, contractAddress: String!): ResultString! getMethod(blockHash: String!, contractAddress: String!): ResultString!
_test(blockHash: String!, contractAddress: String!): ResultBigInt! _test(blockHash: String!, contractAddress: String!): ResultBigInt!
exampleEntity(blockHash: String!, id: String!): String!
} }
type ResultEvent { type ResultEvent {

View File

@ -17,7 +17,7 @@ import { createServer } from 'http';
import { getCache } from '@vulcanize/cache'; import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client'; import { EthClient } from '@vulcanize/ipld-eth-client';
import { DEFAULT_CONFIG_PATH, getConfig, JobQueue, KIND_ACTIVE, getCustomProvider } from '@vulcanize/util'; import { DEFAULT_CONFIG_PATH, getConfig, JobQueue, KIND_ACTIVE, getCustomProvider } from '@vulcanize/util';
import { GraphWatcher } from '@vulcanize/graph-node'; import { GraphWatcher, Database as GraphDatabase } from '@vulcanize/graph-node';
import { createResolvers } from './resolvers'; import { createResolvers } from './resolvers';
import { Indexer } from './indexer'; import { Indexer } from './indexer';
@ -70,7 +70,10 @@ export const main = async (): Promise<any> => {
const ethProvider = getCustomProvider(rpcProviderEndpoint); const ethProvider = getCustomProvider(rpcProviderEndpoint);
const graphWatcher = new GraphWatcher(subgraphPath); const graphDb = new GraphDatabase(dbConfig, path.resolve(__dirname, 'entity/*'));
await graphDb.init();
const graphWatcher = new GraphWatcher(graphDb, postgraphileClient, subgraphPath);
await graphWatcher.init(); await graphWatcher.init();
const indexer = new Indexer(db, ethClient, postgraphileClient, ethProvider, graphWatcher); const indexer = new Indexer(db, ethClient, postgraphileClient, ethProvider, graphWatcher);

View File

@ -2287,6 +2287,13 @@
"@types/connect" "*" "@types/connect" "*"
"@types/node" "*" "@types/node" "*"
"@types/chai-spies@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@types/chai-spies/-/chai-spies-1.0.3.tgz#a52dc61af3853ec9b80965040811d15dfd401542"
integrity sha512-RBZjhVuK7vrg4rWMt04UF5zHYwfHnpk5mIWu3nQvU3AKGDixXzSjZ6v0zke6pBcaJqMv3IBZ5ibLWPMRDL0sLw==
dependencies:
"@types/chai" "*"
"@types/chai@*", "@types/chai@^4.2.18": "@types/chai@*", "@types/chai@^4.2.18":
version "4.2.18" version "4.2.18"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.18.tgz#0c8e298dbff8205e2266606c1ea5fbdba29b46e4" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.18.tgz#0c8e298dbff8205e2266606c1ea5fbdba29b46e4"
@ -4617,6 +4624,11 @@ catering@^2.0.0:
resolved "https://registry.yarnpkg.com/catering/-/catering-2.0.0.tgz#15ce31bcbffafbf62855ea7677b0e5d23581233d" resolved "https://registry.yarnpkg.com/catering/-/catering-2.0.0.tgz#15ce31bcbffafbf62855ea7677b0e5d23581233d"
integrity sha512-aD/WmxhGwUGsVPrj8C80vH7C7GphJilYVSdudoV4u16XdrLF7CVyfBmENsc4tLTVsJJzCRid8GbwJ7mcPLee6Q== integrity sha512-aD/WmxhGwUGsVPrj8C80vH7C7GphJilYVSdudoV4u16XdrLF7CVyfBmENsc4tLTVsJJzCRid8GbwJ7mcPLee6Q==
chai-spies@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/chai-spies/-/chai-spies-1.0.0.tgz#d16b39336fb316d03abf8c375feb23c0c8bb163d"
integrity sha512-elF2ZUczBsFoP07qCfMO/zeggs8pqCf3fZGyK5+2X4AndS8jycZYID91ztD9oQ7d/0tnS963dPkd0frQEThDsg==
chai@^4.3.4: chai@^4.3.4:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49"