Invoke subgraph handler in watcher event processing (#34)

* Invoke subgraph handler in watcher event processing

* Fix error when invoking subgraph handler

* Parse events using event signature specified in subgraph yaml

* Use contract abi to parse event params

* Invoke event handler based on event signature

* Fill event with block and transaction data

* Comment missing fields in block and transaction data
This commit is contained in:
nikugogoi 2021-11-01 15:13:22 +05:30 committed by nabarun
parent 6cca55a1ab
commit 43d64f9e4b
21 changed files with 409 additions and 230 deletions

View File

@ -4,6 +4,7 @@
import path from 'path'; import path from 'path';
import { getDummyEventData } from '../test/utils';
import { instantiate } from './loader'; import { instantiate } from './loader';
import { createEvent } from './utils'; import { createEvent } from './utils';
@ -26,8 +27,10 @@ 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.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'param1', name: 'param1',
value: 'abc', value: 'abc',
@ -36,7 +39,7 @@ describe('call handler in mapping code', () => {
{ {
name: 'param2', name: 'param2',
value: BigInt(123), value: BigInt(123),
kind: 'unsignedBigInt' kind: 'uint256'
} }
]; ];
@ -44,7 +47,7 @@ describe('call handler in mapping code', () => {
const contractAddress = '0xCA6D29232D1435D8198E3E5302495417dD073d61'; const contractAddress = '0xCA6D29232D1435D8198E3E5302495417dD073d61';
// Create Test event to be passed to handler. // Create Test event to be passed to handler.
const test = await createEvent(exports, contractAddress, eventParamsData); const test = await createEvent(exports, contractAddress, eventData);
await handleTest(test); await handleTest(test);
}); });

View File

@ -11,10 +11,13 @@ import { createEvent } 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';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
describe('eden wasm loader tests', () => { describe('eden wasm loader tests', () => {
const eventData = getDummyEventData();
describe('EdenNetwork wasm', () => { describe('EdenNetwork wasm', () => {
let exports: any; let exports: any;
@ -44,10 +47,10 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy SlotClaimedEvent params. // Create dummy SlotClaimedEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'slot', name: 'slot',
kind: 'i32', kind: 'uint8',
value: 0 value: 0
}, },
{ {
@ -62,28 +65,28 @@ describe('eden wasm loader tests', () => {
}, },
{ {
name: 'newBidAmount', name: 'newBidAmount',
kind: 'unsignedBigInt', kind: 'uint128',
value: BigInt(1) value: BigInt(1)
}, },
{ {
name: 'oldBidAmount', name: 'oldBidAmount',
kind: 'unsignedBigInt', kind: 'uint128',
value: BigInt(1) value: BigInt(1)
}, },
{ {
name: 'taxNumerator', name: 'taxNumerator',
kind: 'i32', kind: 'uint16',
value: 1 value: 1
}, },
{ {
name: 'taxDenominator', name: 'taxDenominator',
kind: 'i32', kind: 'uint16',
value: 1 value: 1
} }
]; ];
// Create dummy SlotClaimedEvent to be passed to handler. // Create dummy SlotClaimedEvent to be passed to handler.
const slotClaimedEvent = await createEvent(exports, contractAddress, eventParamsData); const slotClaimedEvent = await createEvent(exports, contractAddress, eventData);
await slotClaimed(slotClaimedEvent); await slotClaimed(slotClaimedEvent);
}); });
@ -94,10 +97,10 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy SlotDelegateUpdatedEvent params. // Create dummy SlotDelegateUpdatedEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'slot', name: 'slot',
kind: 'i32', kind: 'uint8',
value: 0 value: 0
}, },
{ {
@ -118,7 +121,7 @@ describe('eden wasm loader tests', () => {
]; ];
// Create dummy SlotDelegateUpdatedEvent to be passed to handler. // Create dummy SlotDelegateUpdatedEvent to be passed to handler.
const slotClaimedEvent = await createEvent(exports, contractAddress, eventParamsData); const slotClaimedEvent = await createEvent(exports, contractAddress, eventData);
await slotDelegateUpdated(slotClaimedEvent); await slotDelegateUpdated(slotClaimedEvent);
}); });
@ -129,7 +132,7 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy StakeEvent params. // Create dummy StakeEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'staker', name: 'staker',
kind: 'address', kind: 'address',
@ -137,13 +140,13 @@ describe('eden wasm loader tests', () => {
}, },
{ {
name: 'stakeAmount', name: 'stakeAmount',
kind: 'unsignedBigInt', kind: 'uint256',
value: BigInt(1) value: BigInt(1)
} }
]; ];
// Create dummy StakeEvent to be passed to handler. // Create dummy StakeEvent to be passed to handler.
const stakeEvent = await createEvent(exports, contractAddress, eventParamsData); const stakeEvent = await createEvent(exports, contractAddress, eventData);
await stake(stakeEvent); await stake(stakeEvent);
}); });
@ -154,7 +157,7 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy UnstakeEvent params. // Create dummy UnstakeEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'staker', name: 'staker',
kind: 'address', kind: 'address',
@ -162,13 +165,13 @@ describe('eden wasm loader tests', () => {
}, },
{ {
name: 'unstakedAmount', name: 'unstakedAmount',
kind: 'unsignedBigInt', kind: 'uin256',
value: BigInt(1) value: BigInt(1)
} }
]; ];
// Create dummy UnstakeEvent to be passed to handler. // Create dummy UnstakeEvent to be passed to handler.
const unstakeEvent = await createEvent(exports, contractAddress, eventParamsData); const unstakeEvent = await createEvent(exports, contractAddress, eventData);
await unstake(unstakeEvent); await unstake(unstakeEvent);
}); });
@ -203,15 +206,15 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy ClaimedEvent params. // Create dummy ClaimedEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'index', name: 'index',
kind: 'unsignedBigInt', kind: 'uint256',
value: BigInt(1) value: BigInt(1)
}, },
{ {
name: 'totalEarned', name: 'totalEarned',
kind: 'unsignedBigInt', kind: 'uint256',
value: BigInt(1) value: BigInt(1)
}, },
{ {
@ -221,13 +224,13 @@ describe('eden wasm loader tests', () => {
}, },
{ {
name: 'claimed', name: 'claimed',
kind: 'unsignedBigInt', kind: 'uint256',
value: BigInt(1) value: BigInt(1)
} }
]; ];
// Create dummy ClaimedEvent to be passed to handler. // Create dummy ClaimedEvent to be passed to handler.
const claimedEvent = await createEvent(exports, contractAddress, eventParamsData); const claimedEvent = await createEvent(exports, contractAddress, eventData);
await claimed(claimedEvent); await claimed(claimedEvent);
}); });
@ -238,7 +241,7 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy SlashedEvent params. // Create dummy SlashedEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'account', name: 'account',
kind: 'address', kind: 'address',
@ -246,13 +249,13 @@ describe('eden wasm loader tests', () => {
}, },
{ {
name: 'slashed', name: 'slashed',
kind: 'unsignedBigInt', kind: 'uint256',
value: BigInt(1) value: BigInt(1)
} }
]; ];
// Create dummy SlashedEvent to be passed to handler. // Create dummy SlashedEvent to be passed to handler.
const slashedEvent = await createEvent(exports, contractAddress, eventParamsData); const slashedEvent = await createEvent(exports, contractAddress, eventData);
await slashed(slashedEvent); await slashed(slashedEvent);
}); });
@ -263,15 +266,15 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy MerkleRootUpdatedEvent params. // Create dummy MerkleRootUpdatedEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'merkleRoot', name: 'merkleRoot',
kind: 'bytes', kind: 'bytes32',
value: ethers.utils.hexlify(ethers.utils.randomBytes(32)) value: ethers.utils.hexlify(ethers.utils.randomBytes(32))
}, },
{ {
name: 'distributionNumber', name: 'distributionNumber',
kind: 'unsignedBigInt', kind: 'uint256',
value: BigInt(1) value: BigInt(1)
}, },
{ {
@ -282,7 +285,7 @@ describe('eden wasm loader tests', () => {
]; ];
// Create dummy MerkleRootUpdatedEvent to be passed to handler. // Create dummy MerkleRootUpdatedEvent to be passed to handler.
const merkleRootUpdatedEvent = await createEvent(exports, contractAddress, eventParamsData); const merkleRootUpdatedEvent = await createEvent(exports, contractAddress, eventData);
await merkleRootUpdated(merkleRootUpdatedEvent); await merkleRootUpdated(merkleRootUpdatedEvent);
}); });
@ -293,7 +296,7 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy AccountUpdatedEvent params. // Create dummy AccountUpdatedEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'account', name: 'account',
kind: 'address', kind: 'address',
@ -301,18 +304,18 @@ describe('eden wasm loader tests', () => {
}, },
{ {
name: 'totalClaimed', name: 'totalClaimed',
kind: 'unsignedBigInt', kind: 'uint256',
value: BigInt(1) value: BigInt(1)
}, },
{ {
name: 'totalSlashed', name: 'totalSlashed',
kind: 'unsignedBigInt', kind: 'uint256',
value: BigInt(1) value: BigInt(1)
} }
]; ];
// Create dummy AccountUpdatedEvent to be passed to handler. // Create dummy AccountUpdatedEvent to be passed to handler.
const accountUpdatedEvent = await createEvent(exports, contractAddress, eventParamsData); const accountUpdatedEvent = await createEvent(exports, contractAddress, eventData);
await accountUpdated(accountUpdatedEvent); await accountUpdated(accountUpdatedEvent);
}); });
@ -347,7 +350,7 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy BlockProducerAddedEvent params. // Create dummy BlockProducerAddedEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'produces', name: 'produces',
kind: 'address', kind: 'address',
@ -356,7 +359,7 @@ describe('eden wasm loader tests', () => {
]; ];
// Create dummy BlockProducerAddedEvent to be passed to handler. // Create dummy BlockProducerAddedEvent to be passed to handler.
const blockProducerAddedEvent = await createEvent(exports, contractAddress, eventParamsData); const blockProducerAddedEvent = await createEvent(exports, contractAddress, eventData);
await blockProducerAdded(blockProducerAddedEvent); await blockProducerAdded(blockProducerAddedEvent);
}); });
@ -367,7 +370,7 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy BlockProducerRemovedEvent params. // Create dummy BlockProducerRemovedEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'producer', name: 'producer',
kind: 'address', kind: 'address',
@ -376,7 +379,7 @@ describe('eden wasm loader tests', () => {
]; ];
// Create dummy BlockProducerRemovedEvent to be passed to handler. // Create dummy BlockProducerRemovedEvent to be passed to handler.
const blockProducerRemovedEvent = await createEvent(exports, contractAddress, eventParamsData); const blockProducerRemovedEvent = await createEvent(exports, contractAddress, eventData);
await blockProducerRemoved(blockProducerRemovedEvent); await blockProducerRemoved(blockProducerRemovedEvent);
}); });
@ -387,7 +390,7 @@ describe('eden wasm loader tests', () => {
} = exports; } = exports;
// Create dummy BlockProducerRewardCollectorChangedEvent params. // Create dummy BlockProducerRewardCollectorChangedEvent params.
const eventParamsData = [ eventData.eventParams = [
{ {
name: 'producer', name: 'producer',
kind: 'address', kind: 'address',
@ -406,7 +409,7 @@ describe('eden wasm loader tests', () => {
]; ];
// Create dummy BlockProducerRewardCollectorChangedEvent to be passed to handler. // Create dummy BlockProducerRewardCollectorChangedEvent to be passed to handler.
const blockProducerRewardCollectorChangedEvent = await createEvent(exports, contractAddress, eventParamsData); const blockProducerRewardCollectorChangedEvent = await createEvent(exports, contractAddress, eventData);
await blockProducerRewardCollectorChanged(blockProducerRewardCollectorChangedEvent); await blockProducerRewardCollectorChanged(blockProducerRewardCollectorChangedEvent);
}); });
@ -416,8 +419,10 @@ describe('eden wasm loader tests', () => {
rewardScheduleChanged rewardScheduleChanged
} = exports; } = exports;
eventData.eventParams = [];
// Create dummy RewardScheduleChangedEvent to be passed to handler. // Create dummy RewardScheduleChangedEvent to be passed to handler.
const rewardScheduleChangedEvent = await createEvent(exports, contractAddress, []); const rewardScheduleChangedEvent = await createEvent(exports, contractAddress, eventData);
await rewardScheduleChanged(rewardScheduleChangedEvent); await rewardScheduleChanged(rewardScheduleChangedEvent);
}); });

View File

@ -14,128 +14,39 @@ interface EventParam {
kind: string; kind: string;
} }
/** interface Block {
* Method to create ethereum event. hash: string;
* @param exports number: number;
* @param contractAddress timestamp: number;
* @param eventParamsData parentHash: string;
* @returns }
*/
export const createEvent = async (exports: any, contractAddress: string, eventParamsData: EventParam[]): Promise<any> => {
const {
__newString,
__newArray,
Address,
BigInt,
ethereum,
Bytes,
ByteArray,
id_of_type: idOfType
} = exports;
// Create dummy block data. interface Transaction {
const block = await ethereum.Block.__new( hash: string;
await Bytes.empty(), index: number;
await Bytes.empty(), from: string;
await Bytes.empty(), to: string;
await Address.zero(), }
await Bytes.empty(),
await Bytes.empty(),
await Bytes.empty(),
await BigInt.fromI32(0),
await BigInt.fromI32(0),
await BigInt.fromI32(0),
await BigInt.fromI32(0),
await BigInt.fromI32(0),
await BigInt.fromI32(0),
null
);
// Create dummy transaction data. export interface EventData {
const transaction = await ethereum.Transaction.__new( block: Block;
await Bytes.empty(), tx: Transaction;
await BigInt.fromI32(0), eventParams: EventParam[];
await Address.zero(), eventIndex: number;
null, }
await BigInt.fromI32(0),
await BigInt.fromI32(0),
await BigInt.fromI32(0),
await Bytes.empty()
);
const eventParamArrayPromise = eventParamsData.map(async data => {
const { name, value, kind } = data;
let ethValue;
switch (kind) {
case 'unsignedBigInt': {
const bigIntString = await (await __newString(value.toString()));
const bigInt = await BigInt.fromString(bigIntString);
ethValue = await ethereum.Value.fromUnsignedBigInt(bigInt);
break;
}
case 'string': {
ethValue = await ethereum.Value.fromString(await __newString(value));
break;
}
case 'i32': {
ethValue = await ethereum.Value.fromI32(value);
break;
}
case 'address': {
ethValue = await ethereum.Value.fromAddress(await Address.fromString(await __newString(value)));
break;
}
case 'bytes': {
const byteArray = await ByteArray.fromHexString(await __newString(value));
ethValue = await ethereum.Value.fromBytes(await Bytes.fromByteArray(byteArray));
break;
}
default:
break;
}
return ethereum.EventParam.__new(
await __newString(name),
ethValue
);
});
const eventParamArray = await Promise.all(eventParamArrayPromise);
const eventParams = await __newArray(await idOfType(TypeId.ArrayEventParam), eventParamArray);
// Dummy contract address string.
const addStrPtr = await __newString(contractAddress);
// Create Test event to be passed to handler.
return ethereum.Event.__new(
await Address.fromString(addStrPtr),
await BigInt.fromI32(0),
await BigInt.fromI32(0),
null,
block,
transaction,
eventParams
);
};
/** /**
* Method to get value from graph-ts ethereum.Value wasm instance. * Method to get value from graph-ts ethereum.Value wasm instance.
* @param exports * @param instanceExports
* @param value * @param value
* @returns * @returns
*/ */
export const fromEthereumValue = async (exports: any, value: any): Promise<any> => { export const fromEthereumValue = async (instanceExports: any, value: any): Promise<any> => {
const { const {
__getString, __getString,
BigInt, BigInt,
Address Address
} = exports; } = instanceExports;
const kind = await value.kind; const kind = await value.kind;
@ -173,12 +84,12 @@ export const fromEthereumValue = async (exports: any, value: any): Promise<any>
/** /**
* Method to get ethereum value for passing to wasm instance. * Method to get ethereum value for passing to wasm instance.
* @param exports * @param instanceExports
* @param value * @param value
* @param type * @param type
* @returns * @returns
*/ */
export const toEthereumValue = async (exports: any, value: any, type: string): Promise<any> => { export const toEthereumValue = async (instanceExports: any, value: any, type: string): Promise<any> => {
const { const {
__newString, __newString,
ByteArray, ByteArray,
@ -186,7 +97,7 @@ export const toEthereumValue = async (exports: any, value: any, type: string): P
Address, Address,
ethereum, ethereum,
BigInt BigInt
} = exports; } = instanceExports;
// For boolean type. // For boolean type.
if (type === 'bool') { if (type === 'bool') {
@ -223,6 +134,125 @@ export const toEthereumValue = async (exports: any, value: any, type: string): P
return ethereum.Value.fromString(await __newString(value)); return ethereum.Value.fromString(await __newString(value));
}; };
/**
* 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,
eventParams: eventParamsData,
block: blockData
} = eventData;
const {
__newString,
__newArray,
Address,
BigInt,
ethereum,
Bytes,
ByteArray,
id_of_type: idOfType
} = instanceExports;
// Fill block data.
const blockHashByteArray = await ByteArray.fromHexString(await __newString(blockData.hash));
const blockHash = await Bytes.fromByteArray(blockHashByteArray);
const parentHashByteArray = await ByteArray.fromHexString(await __newString(blockData.parentHash));
const parentHash = await Bytes.fromByteArray(parentHashByteArray);
const blockNumber = await BigInt.fromI32(blockData.number);
const blockTimestamp = await BigInt.fromI32(blockData.timestamp);
// Missing fields from watcher in block data:
// unclesHash
// author
// stateRoot
// transactionsRoot
// receiptsRoot
// gasUsed
// gasLimit
// difficulty
// totalDifficulty
// size
const block = await ethereum.Block.__new(
blockHash,
parentHash,
await Bytes.empty(),
await Address.zero(),
await Bytes.empty(),
await Bytes.empty(),
await Bytes.empty(),
blockNumber,
await BigInt.fromI32(0),
await BigInt.fromI32(0),
blockTimestamp,
await BigInt.fromI32(0),
await BigInt.fromI32(0),
null
);
// Fill transaction data.
const txHashByteArray = await ByteArray.fromHexString(await __newString(tx.hash));
const txHash = await Bytes.fromByteArray(txHashByteArray);
const txIndex = await BigInt.fromI32(tx.index);
const txFrom = await Address.fromString(await __newString(tx.from));
const txTo = tx.to && await Address.fromString(await __newString(tx.to));
// Missing fields from watcher in transaction data:
// value
// gasLimit
// gasPrice
// input
const transaction = await ethereum.Transaction.__new(
txHash,
txIndex,
txFrom,
txTo,
await BigInt.fromI32(0),
await BigInt.fromI32(0),
await BigInt.fromI32(0),
await Bytes.empty()
);
const eventParamArrayPromise = eventParamsData.map(async data => {
const { name, value, kind } = data;
const ethValue = await toEthereumValue(instanceExports, value, kind);
return ethereum.EventParam.__new(
await __newString(name),
ethValue
);
});
const eventParamArray = await Promise.all(eventParamArrayPromise);
const eventParams = await __newArray(await idOfType(TypeId.ArrayEventParam), eventParamArray);
const addStrPtr = await __newString(contractAddress);
// Create event to be passed to handler.
return ethereum.Event.__new(
await Address.fromString(addStrPtr),
await BigInt.fromI32(eventIndex),
await BigInt.fromI32(0),
null,
block,
transaction,
eventParams
);
};
export const getSubgraphConfig = async (subgraphPath: string): Promise<any> => { export const getSubgraphConfig = async (subgraphPath: string): Promise<any> => {
const configFilePath = path.resolve(path.join(subgraphPath, 'subgraph.yaml')); const configFilePath = path.resolve(path.join(subgraphPath, 'subgraph.yaml'));
const fileExists = await fs.pathExists(configFilePath); const fileExists = await fs.pathExists(configFilePath);
@ -231,7 +261,6 @@ export const getSubgraphConfig = async (subgraphPath: string): Promise<any> => {
throw new Error(`Config file not found: ${configFilePath}`); throw new Error(`Config file not found: ${configFilePath}`);
} }
console.log(configFilePath);
const config = yaml.load(await fs.readFile(configFilePath, 'utf8')); const config = yaml.load(await fs.readFile(configFilePath, 'utf8'));
log('config', JSON.stringify(config, null, 2)); log('config', JSON.stringify(config, null, 2));

View File

@ -6,18 +6,24 @@ import 'reflect-metadata';
import debug from 'debug'; import debug from 'debug';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { ContractInterface } from 'ethers'; import { ContractInterface, utils } from 'ethers';
import { getSubgraphConfig } from './utils';
import { instantiate } from './loader';
import { ResultObject } from '@vulcanize/assemblyscript/lib/loader'; import { ResultObject } from '@vulcanize/assemblyscript/lib/loader';
import { createEvent, getSubgraphConfig } from './utils';
import { instantiate } from './loader';
const log = debug('vulcanize:graph-watcher'); const log = debug('vulcanize:graph-watcher');
interface DataSource {
instance: ResultObject & { exports: any },
contractInterface: utils.Interface
}
export class GraphWatcher { export class GraphWatcher {
_subgraphPath: string; _subgraphPath: string;
_dataSources: any[] = [] _dataSources: any[] = [];
_instanceMap: { [key: string]: ResultObject & { exports: any } } = {}; _dataSourceMap: { [key: string]: DataSource } = {};
constructor (subgraphPath: string) { constructor (subgraphPath: string) {
this._subgraphPath = subgraphPath; this._subgraphPath = subgraphPath;
@ -27,33 +33,57 @@ export class GraphWatcher {
const { dataSources } = await getSubgraphConfig(this._subgraphPath); const { dataSources } = await getSubgraphConfig(this._subgraphPath);
this._dataSources = dataSources; this._dataSources = dataSources;
this._instanceMap = this._dataSources.reduce(async (acc: { [key: string]: ResultObject & { exports: any } }, dataSource: any) => { // Create wasm instance and contract interface for each dataSource in subgraph yaml.
const { source: { address }, mapping } = dataSource; const dataPromises = this._dataSources.map(async (dataSource: any) => {
const { source: { address, abi }, mapping } = dataSource;
const { abis, file } = mapping; const { abis, file } = mapping;
const data = { const abisMap = abis.reduce((acc: {[key: string]: ContractInterface}, abi: any) => {
abis: abis.reduce((acc: {[key: string]: ContractInterface}, abi: any) => {
const { name, file } = abi; const { name, file } = abi;
const abiFilePath = path.join(this._subgraphPath, file); const abiFilePath = path.join(this._subgraphPath, file);
acc[name] = JSON.parse(fs.readFileSync(abiFilePath).toString()); acc[name] = JSON.parse(fs.readFileSync(abiFilePath).toString());
return acc; return acc;
}, {}), }, {});
const contractInterface = new utils.Interface(abisMap[abi]);
const data = {
abis: abisMap,
dataSource: { dataSource: {
address address
} }
}; };
const filePath = path.join(this._subgraphPath, file); const filePath = path.join(this._subgraphPath, file);
const instance = await instantiate(filePath, data);
acc[address] = instance; return {
instance: await instantiate(filePath, data),
contractInterface
};
}, {});
const data = await Promise.all(dataPromises);
// Create a map from dataSource contract address to instance and contract interface.
this._dataSourceMap = this._dataSources.reduce((acc: { [key: string]: DataSource }, dataSource: any, index: number) => {
const { instance } = data[index];
// Important to call _start for built subgraphs on instantiation!
// TODO: Check api version https://github.com/graphprotocol/graph-node/blob/6098daa8955bdfac597cec87080af5449807e874/runtime/wasm/src/module/mod.rs#L533
instance.exports._start();
const { source: { address } } = dataSource;
acc[address] = data[index];
return acc; return acc;
}, {}); }, {});
} }
async handleEvent (eventData: any) { async handleEvent (eventData: any) {
const { contract } = eventData; const { contract, event, eventSignature, block, tx, eventIndex } = eventData;
// 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);
if (!dataSource) { if (!dataSource) {
@ -61,16 +91,36 @@ export class GraphWatcher {
return; return;
} }
// TODO: Call instance methods based on event signature. // Get event handler based on event signature.
// value should contain event signature. const eventHandler = dataSource.mapping.eventHandlers.find((eventHandler: any) => eventHandler.event === eventSignature);
const [{ handler }] = dataSource.mapping.eventHandlers; if (!eventHandler) {
const { exports } = this._instanceMap[contract]; log(`No handler configured in subgraph for event ${eventSignature}`);
return;
}
// Create ethereum event to be passed to handler. const { instance: { exports }, contractInterface } = this._dataSourceMap[contract];
// TODO: Create ethereum event to be passed to handler.
// const ethereumEvent = await createEvent(exports, address, event);
await exports[handler](); const eventFragment = contractInterface.getEvent(eventSignature);
const eventParams = eventFragment.inputs.map((input) => {
return {
name: input.name,
value: event[input.name],
kind: input.type
};
});
const data = {
eventParams: eventParams,
block,
tx,
eventIndex
};
// Create ethereum event to be passed to the wasm event handler.
const ethereumEvent = await createEvent(exports, contract, data);
await exports[eventHandler.handler](ethereumEvent);
} }
} }

View File

@ -10,14 +10,27 @@
}, },
{ {
"indexed": false, "indexed": false,
"internalType": "uint256", "internalType": "uint8",
"name": "param2", "name": "param2",
"type": "uint256" "type": "uint8"
} }
], ],
"name": "Test", "name": "Test",
"type": "event" "type": "event"
}, },
{
"inputs": [],
"name": "emitEvent",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "getMethod", "name": "getMethod",

View File

@ -27,8 +27,8 @@ export class Test__Params {
return this._event.parameters[0].value.toString(); return this._event.parameters[0].value.toString();
} }
get param2(): BigInt { get param2(): i32 {
return this._event.parameters[1].value.toBigInt(); return this._event.parameters[1].value.toI32();
} }
} }
@ -37,6 +37,21 @@ export class Example1 extends ethereum.SmartContract {
return new Example1("Example1", address); return new Example1("Example1", address);
} }
emitEvent(): boolean {
let result = super.call("emitEvent", "emitEvent():(bool)", []);
return result[0].toBoolean();
}
try_emitEvent(): ethereum.CallResult<boolean> {
let result = super.tryCall("emitEvent", "emitEvent():(bool)", []);
if (result.reverted) {
return new ethereum.CallResult();
}
let value = result.value;
return ethereum.CallResult.fromValue(value[0].toBoolean());
}
getMethod(): string { getMethod(): string {
let result = super.call("getMethod", "getMethod():(string)", []); let result = super.call("getMethod", "getMethod():(string)", []);
@ -52,3 +67,33 @@ export class Example1 extends ethereum.SmartContract {
return ethereum.CallResult.fromValue(value[0].toString()); return ethereum.CallResult.fromValue(value[0].toString());
} }
} }
export class EmitEventCall extends ethereum.Call {
get inputs(): EmitEventCall__Inputs {
return new EmitEventCall__Inputs(this);
}
get outputs(): EmitEventCall__Outputs {
return new EmitEventCall__Outputs(this);
}
}
export class EmitEventCall__Inputs {
_call: EmitEventCall;
constructor(call: EmitEventCall) {
this._call = call;
}
}
export class EmitEventCall__Outputs {
_call: EmitEventCall;
constructor(call: EmitEventCall) {
this._call = call;
}
get value0(): boolean {
return this._call.outputValues[0].value.toBoolean();
}
}

View File

@ -6,21 +6,19 @@ import {
BigInt, BigInt,
ethereum, ethereum,
Address, Address,
Bytes ByteArray,
Bytes,
Entity
} from '@graphprotocol/graph-ts'; } from '@graphprotocol/graph-ts';
import {
Test
} from './Example1/Example1';
export { export {
BigDecimal, BigDecimal,
BigInt, BigInt,
ethereum, ethereum,
Entity,
Address, Address,
Bytes, ByteArray,
Bytes
Test
} }

View File

@ -19,7 +19,7 @@ export class ExampleEntity extends Entity {
this.set("count", Value.fromBigInt(BigInt.zero())); this.set("count", Value.fromBigInt(BigInt.zero()));
this.set("param1", Value.fromString("")); this.set("param1", Value.fromString(""));
this.set("param2", Value.fromBigInt(BigInt.zero())); this.set("param2", Value.fromI32(0));
} }
save(): void { save(): void {
@ -66,12 +66,12 @@ export class ExampleEntity extends Entity {
this.set("param1", Value.fromString(value)); this.set("param1", Value.fromString(value));
} }
get param2(): BigInt { get param2(): i32 {
let value = this.get("param2"); let value = this.get("param2");
return value!.toBigInt(); return value!.toI32();
} }
set param2(value: BigInt) { set param2(value: i32) {
this.set("param2", Value.fromBigInt(value)); this.set("param2", Value.fromI32(value));
} }
} }

View File

@ -10,7 +10,7 @@
"deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 example1" "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 example1"
}, },
"dependencies": { "dependencies": {
"@graphprotocol/graph-cli": "ssh://git@github.com:vulcanize/graph-cli.git#graph-watcher-v0.22.1", "@graphprotocol/graph-cli": "ssh://git@github.com:vulcanize/graph-cli.git#ng-add-exports",
"@graphprotocol/graph-ts": "0.22.0" "@graphprotocol/graph-ts": "^0.22.1"
} }
} }

View File

@ -2,5 +2,5 @@ type ExampleEntity @entity {
id: ID! id: ID!
count: BigInt! count: BigInt!
param1: String! # string param1: String! # string
param2: BigInt! # uint256 param2: Int! # uint8
} }

View File

@ -10,6 +10,7 @@ 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()]);
// 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

View File

@ -6,7 +6,7 @@ dataSources:
name: Example1 name: Example1
network: mainnet network: mainnet
source: source:
address: "0x3ebd8bb51fF52aDAc490117B31F5F137BB125A9D" address: "0x4Ab7aE18973491Df21d6103dfA55170fdB2CCC98"
abi: Example1 abi: Example1
mapping: mapping:
kind: ethereum/events kind: ethereum/events
@ -18,6 +18,6 @@ dataSources:
- name: Example1 - name: Example1
file: ./abis/Example1.json file: ./abis/Example1.json
eventHandlers: eventHandlers:
- event: Test(string,uint256) - event: Test(string,uint8)
handler: handleTest handler: handleTest
file: ./src/mapping.ts file: ./src/mapping.ts

View File

@ -23,9 +23,9 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@graphprotocol/graph-cli@ssh://git@github.com:vulcanize/graph-cli.git#graph-watcher-v0.22.1": "@graphprotocol/graph-cli@ssh://git@github.com:vulcanize/graph-cli.git#ng-add-exports":
version "0.22.1" version "0.22.1"
resolved "ssh://git@github.com:vulcanize/graph-cli.git#4241570e91578a0128deccc518d52eca00bd587c" resolved "ssh://git@github.com:vulcanize/graph-cli.git#d70ae40a7517a666f550b25a0fa4ae0d3758e7d0"
dependencies: dependencies:
assemblyscript "0.19.10" assemblyscript "0.19.10"
binary-install-raw "0.0.13" binary-install-raw "0.0.13"
@ -51,10 +51,10 @@
which "2.0.2" which "2.0.2"
yaml "^1.5.1" yaml "^1.5.1"
"@graphprotocol/graph-ts@0.22.0": "@graphprotocol/graph-ts@^0.22.1":
version "0.22.0" version "0.22.1"
resolved "https://registry.yarnpkg.com/@graphprotocol/graph-ts/-/graph-ts-0.22.0.tgz#5280513d1c8a162077f82ce7f9a492bb5783d6f4" resolved "https://registry.yarnpkg.com/@graphprotocol/graph-ts/-/graph-ts-0.22.1.tgz#3189b2495b33497280f617316cce68074d48e236"
integrity sha512-kJIBL73xBxj0+NJdRABukAtcrc3Mb8jt31s0tCLPe6c57rQXEf7KR9oYrFdzE1ZsJCP6j+MX+A2+sj1Pj3aJtQ== integrity sha512-T5xrHN0tHJwd7ZnSTLhk5hAL3rCIp6rJ40kBCrETnv1mfK9hVyoojJK6VtBQXTbLsYtKe4SYjjD0cdOsAR9QiA==
dependencies: dependencies:
assemblyscript "0.19.10" assemblyscript "0.19.10"

View File

@ -0,0 +1,31 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import { EventData } from '../../src/utils';
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export const ZERO_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000';
export const getDummyEventData = (): EventData => {
const block = {
hash: ZERO_HASH,
number: 0,
timestamp: 0,
parentHash: ZERO_HASH
};
const tx = {
hash: ZERO_HASH,
index: 0,
from: ZERO_ADDRESS,
to: ZERO_ADDRESS
};
return {
block,
tx,
eventParams: [],
eventIndex: 0
};
};

View File

@ -19,6 +19,7 @@
gqlApiEndpoint = "http://127.0.0.1:8082/graphql" gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
gqlPostgraphileEndpoint = "http://127.0.0.1:5000/graphql" gqlPostgraphileEndpoint = "http://127.0.0.1:5000/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8081" rpcProviderEndpoint = "http://127.0.0.1:8081"
blockDelayInMilliSecs = 2000
[upstream.cache] [upstream.cache]
name = "requests" name = "requests"

View File

@ -12,7 +12,8 @@ import {
EventWatcher as BaseEventWatcher, EventWatcher as BaseEventWatcher,
QUEUE_BLOCK_PROCESSING, QUEUE_BLOCK_PROCESSING,
QUEUE_EVENT_PROCESSING, QUEUE_EVENT_PROCESSING,
UNKNOWN_EVENT_NAME UNKNOWN_EVENT_NAME,
UpstreamConfig
} from '@vulcanize/util'; } from '@vulcanize/util';
import { Indexer } from './indexer'; import { Indexer } from './indexer';
@ -30,7 +31,7 @@ export class EventWatcher {
_pubsub: PubSub _pubsub: PubSub
_jobQueue: JobQueue _jobQueue: JobQueue
constructor (ethClient: EthClient, indexer: Indexer, pubsub: PubSub, jobQueue: JobQueue) { constructor (upstreamConfig: UpstreamConfig, ethClient: EthClient, postgraphileClient: EthClient, indexer: Indexer, pubsub: PubSub, jobQueue: JobQueue) {
assert(ethClient); assert(ethClient);
assert(indexer); assert(indexer);
@ -38,7 +39,7 @@ export class EventWatcher {
this._indexer = indexer; this._indexer = indexer;
this._pubsub = pubsub; this._pubsub = pubsub;
this._jobQueue = jobQueue; this._jobQueue = jobQueue;
this._baseEventWatcher = new BaseEventWatcher(this._ethClient, this._indexer, this._pubsub, this._jobQueue); this._baseEventWatcher = new BaseEventWatcher(upstreamConfig, this._ethClient, postgraphileClient, this._indexer, this._pubsub, this._jobQueue);
} }
getEventIterator (): AsyncIterator<any> { getEventIterator (): AsyncIterator<any> {
@ -52,22 +53,15 @@ export class EventWatcher {
async start (): Promise<void> { async start (): Promise<void> {
assert(!this._subscription, 'subscription already started'); assert(!this._subscription, 'subscription already started');
await this.watchBlocksAtChainHead();
await this.initBlockProcessingOnCompleteHandler(); await this.initBlockProcessingOnCompleteHandler();
await this.initEventProcessingOnCompleteHandler(); await this.initEventProcessingOnCompleteHandler();
this._baseEventWatcher.startBlockProcessing();
} }
async stop (): Promise<void> { async stop (): Promise<void> {
this._baseEventWatcher.stop(); this._baseEventWatcher.stop();
} }
async watchBlocksAtChainHead (): Promise<void> {
log('Started watching upstream blocks...');
this._subscription = await this._ethClient.watchBlocks(async (value) => {
await this._baseEventWatcher.blocksHandler(value);
});
}
async initBlockProcessingOnCompleteHandler (): Promise<void> { async initBlockProcessingOnCompleteHandler (): Promise<void> {
this._jobQueue.onComplete(QUEUE_BLOCK_PROCESSING, async (job) => { this._jobQueue.onComplete(QUEUE_BLOCK_PROCESSING, async (job) => {
const { id, data: { failed } } = job; const { id, data: { failed } } = job;

View File

@ -54,13 +54,13 @@ export const main = async (): Promise<any> => {
await db.init(); await db.init();
assert(upstream, 'Missing upstream config'); assert(upstream, 'Missing upstream config');
const { ethServer: { gqlPostgraphileEndpoint, rpcProviderEndpoint }, cache: cacheConfig } = upstream; const { ethServer: { gqlApiEndpoint, gqlPostgraphileEndpoint, rpcProviderEndpoint, blockDelayInMilliSecs }, cache: cacheConfig } = upstream;
assert(gqlPostgraphileEndpoint, 'Missing upstream ethServer.gqlPostgraphileEndpoint'); assert(gqlPostgraphileEndpoint, 'Missing upstream ethServer.gqlPostgraphileEndpoint');
const cache = await getCache(cacheConfig); const cache = await getCache(cacheConfig);
const ethClient = new EthClient({ const ethClient = new EthClient({
gqlEndpoint: gqlPostgraphileEndpoint, gqlEndpoint: gqlApiEndpoint,
gqlSubscriptionEndpoint: gqlPostgraphileEndpoint, gqlSubscriptionEndpoint: gqlPostgraphileEndpoint,
cache cache
}); });
@ -83,11 +83,11 @@ export const main = async (): Promise<any> => {
const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs }); const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs });
await jobQueue.start(); await jobQueue.start();
const eventWatcher = new EventWatcher(ethClient, indexer, pubsub, jobQueue); const eventWatcher = new EventWatcher(upstream, ethClient, postgraphileClient, indexer, pubsub, jobQueue);
assert(jobQueueConfig, 'Missing job queue config'); assert(jobQueueConfig, 'Missing job queue config');
await fillBlocks(jobQueue, indexer, ethClient, eventWatcher, argv); await fillBlocks(jobQueue, indexer, postgraphileClient, eventWatcher, blockDelayInMilliSecs, argv);
}; };
main().catch(err => { main().catch(err => {

View File

@ -44,6 +44,7 @@ export type ResultEvent = {
contract: string; contract: string;
eventIndex: number; eventIndex: number;
eventSignature: string;
event: any; event: any;
proof: string; proof: string;
@ -67,8 +68,8 @@ export class Indexer {
this._db = db; this._db = db;
this._ethClient = ethClient; this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._postgraphileClient = postgraphileClient; this._postgraphileClient = postgraphileClient;
this._ethProvider = ethProvider;
this._graphWatcher = graphWatcher; this._graphWatcher = graphWatcher;
this._baseIndexer = new BaseIndexer(this._db, this._ethClient, this._ethProvider); this._baseIndexer = new BaseIndexer(this._db, this._ethClient, this._ethProvider);
@ -86,7 +87,7 @@ export class Indexer {
getResultEvent (event: Event): ResultEvent { getResultEvent (event: Event): ResultEvent {
const block = event.block; const block = event.block;
const eventFields = JSONbig.parse(event.eventInfo); const eventFields = JSONbig.parse(event.eventInfo);
const { tx } = JSON.parse(event.extraInfo); const { tx, eventSignature } = JSON.parse(event.extraInfo);
return { return {
block: { block: {
@ -106,6 +107,7 @@ export class Indexer {
contract: event.contract, contract: event.contract,
eventIndex: event.index, eventIndex: event.index,
eventSignature,
event: { event: {
__typename: `${event.eventName}Event`, __typename: `${event.eventName}Event`,
...eventFields ...eventFields
@ -168,7 +170,8 @@ export class Indexer {
async triggerIndexingOnEvent (event: Event): Promise<void> { async triggerIndexingOnEvent (event: Event): Promise<void> {
const resultEvent = this.getResultEvent(event); const resultEvent = this.getResultEvent(event);
this._graphWatcher.handleEvent(resultEvent); // Call subgraph handler for event.
await this._graphWatcher.handleEvent(resultEvent);
// Call custom hook function for indexing on event. // Call custom hook function for indexing on event.
await handleEvent(this, resultEvent); await handleEvent(this, resultEvent);
@ -199,7 +202,11 @@ export class Indexer {
} }
} }
return { eventName, eventInfo }; return {
eventName,
eventInfo,
eventSignature: logDescription.signature
};
} }
async watchContract (address: string, startingBlock: number): Promise<boolean> { async watchContract (address: string, startingBlock: number): Promise<boolean> {
@ -326,7 +333,7 @@ export class Indexer {
let eventName = UNKNOWN_EVENT_NAME; let eventName = UNKNOWN_EVENT_NAME;
let eventInfo = {}; let eventInfo = {};
const tx = transactionMap[txHash]; const tx = transactionMap[txHash];
const extraInfo = { topics, data, tx }; const extraInfo: { [key: string]: any } = { topics, data, tx };
const contract = ethers.utils.getAddress(address); const contract = ethers.utils.getAddress(address);
const watchedContract = await this.isWatchedContract(contract); const watchedContract = await this.isWatchedContract(contract);
@ -335,6 +342,7 @@ export class Indexer {
const eventDetails = this.parseEventNameAndArgs(watchedContract.kind, logObj); const eventDetails = this.parseEventNameAndArgs(watchedContract.kind, logObj);
eventName = eventDetails.eventName; eventName = eventDetails.eventName;
eventInfo = eventDetails.eventInfo; eventInfo = eventDetails.eventInfo;
extraInfo.eventSignature = eventDetails.eventSignature;
} }
dbEvents.push({ dbEvents.push({

View File

@ -34,9 +34,9 @@ export class JobRunner {
_jobQueueConfig: JobQueueConfig _jobQueueConfig: JobQueueConfig
constructor (jobQueueConfig: JobQueueConfig, indexer: Indexer, jobQueue: JobQueue) { constructor (jobQueueConfig: JobQueueConfig, indexer: Indexer, jobQueue: JobQueue) {
this._jobQueueConfig = jobQueueConfig;
this._indexer = indexer; this._indexer = indexer;
this._jobQueue = jobQueue; this._jobQueue = jobQueue;
this._jobQueueConfig = jobQueueConfig;
this._baseJobRunner = new BaseJobRunner(this._jobQueueConfig, this._indexer, this._jobQueue); this._baseJobRunner = new BaseJobRunner(this._jobQueueConfig, this._indexer, this._jobQueue);
} }

View File

@ -10,6 +10,7 @@ type ResultEvent {
tx: Transaction! tx: Transaction!
contract: String! contract: String!
eventIndex: Int! eventIndex: Int!
eventSignature: String!
event: Event! event: Event!
proof: Proof proof: Proof
} }

View File

@ -85,7 +85,7 @@ export const main = async (): Promise<any> => {
const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs }); const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs });
const eventWatcher = new EventWatcher(ethClient, indexer, pubsub, jobQueue); const eventWatcher = new EventWatcher(upstream, ethClient, postgraphileClient, indexer, pubsub, jobQueue);
if (watcherKind === KIND_ACTIVE) { if (watcherKind === KIND_ACTIVE) {
await jobQueue.start(); await jobQueue.start();