Skip to content

Commit

Permalink
Support events handlers in multiple data sources for a contract addre…
Browse files Browse the repository at this point in the history
…ss (#526)

* Support processing events in multiple subgraph datasources for a single contract address

* Fix parsing event topic in graph-node watcher

* Update codegen templates

* Fix dummy indexer method in graph-node test

* Upgrade package versions to 0.2.102
  • Loading branch information
nikugogoi authored Jun 26, 2024
1 parent b9a899a commit 2217cd3
Show file tree
Hide file tree
Showing 22 changed files with 212 additions and 157 deletions.
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"packages": [
"packages/*"
],
"version": "0.2.101",
"version": "0.2.102",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cache/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/cache",
"version": "0.2.101",
"version": "0.2.102",
"description": "Generic object cache",
"main": "dist/index.js",
"scripts": {
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/cli",
"version": "0.2.101",
"version": "0.2.102",
"main": "dist/index.js",
"license": "AGPL-3.0",
"scripts": {
Expand All @@ -15,13 +15,13 @@
},
"dependencies": {
"@apollo/client": "^3.7.1",
"@cerc-io/cache": "^0.2.101",
"@cerc-io/ipld-eth-client": "^0.2.101",
"@cerc-io/cache": "^0.2.102",
"@cerc-io/ipld-eth-client": "^0.2.102",
"@cerc-io/libp2p": "^0.42.2-laconic-0.1.4",
"@cerc-io/nitro-node": "^0.1.15",
"@cerc-io/peer": "^0.2.101",
"@cerc-io/rpc-eth-client": "^0.2.101",
"@cerc-io/util": "^0.2.101",
"@cerc-io/peer": "^0.2.102",
"@cerc-io/rpc-eth-client": "^0.2.102",
"@cerc-io/util": "^0.2.102",
"@ethersproject/providers": "^5.4.4",
"@graphql-tools/utils": "^9.1.1",
"@ipld/dag-cbor": "^8.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/codegen/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/codegen",
"version": "0.2.101",
"version": "0.2.102",
"description": "Code generator",
"private": true,
"main": "index.js",
Expand All @@ -20,7 +20,7 @@
},
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@cerc-io/util": "^0.2.101",
"@cerc-io/util": "^0.2.102",
"@graphql-tools/load-files": "^6.5.2",
"@npmcli/package-json": "^5.0.0",
"@poanet/solidity-flattener": "https://github.com/vulcanize/solidity-flattener.git",
Expand Down
1 change: 1 addition & 0 deletions packages/codegen/src/data/entities/Contract.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ className: Contract
indexOn:
- columns:
- address
- kind
unique: true
columns:
- name: id
Expand Down
36 changes: 20 additions & 16 deletions packages/codegen/src/templates/indexer-template.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -525,23 +525,27 @@ export class Indexer implements IndexerInterface {
}

{{/if}}
parseEventNameAndArgs (kind: string, logObj: any): { eventParsed: boolean, eventDetails: any } {
parseEventNameAndArgs (watchedContracts: Contract[], logObj: any): { eventParsed: boolean, eventDetails: any } {
const { topics, data } = logObj;

const contract = this._contractMap.get(kind);
assert(contract);

let logDescription: ethers.utils.LogDescription;
try {
logDescription = contract.parseLog({ data, topics });
} catch (err) {
// Return if no matching event found
if ((err as Error).message.includes('no matching event')) {
log(`WARNING: Skipping event for contract ${kind} as no matching event found in the ABI`);
return { eventParsed: false, eventDetails: {} };
let logDescription: ethers.utils.LogDescription | undefined;

for (const watchedContract of watchedContracts) {
const contract = this._contractMap.get(watchedContract.kind);
assert(contract);

try {
logDescription = contract.parseLog({ data, topics });
break;
} catch (err) {
// Continue loop only if no matching event found
if (!((err as Error).message.includes('no matching event'))) {
throw err;
}
}
}

throw err;
if (!logDescription) {
return { eventParsed: false, eventDetails: {} };
}

const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);
Expand Down Expand Up @@ -647,8 +651,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}

isWatchedContract (address : string): Contract | undefined {
return this._baseIndexer.isWatchedContract(address);
isContractAddressWatched (address : string): Contract[] | undefined {
return this._baseIndexer.isContractAddressWatched(address);
}

getWatchedContracts (): Contract[] {
Expand Down
10 changes: 5 additions & 5 deletions packages/codegen/src/templates/package-template.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@apollo/client": "^3.3.19",
"@cerc-io/cli": "^0.2.101",
"@cerc-io/ipld-eth-client": "^0.2.101",
"@cerc-io/solidity-mapper": "^0.2.101",
"@cerc-io/util": "^0.2.101",
"@cerc-io/cli": "^0.2.102",
"@cerc-io/ipld-eth-client": "^0.2.102",
"@cerc-io/solidity-mapper": "^0.2.102",
"@cerc-io/util": "^0.2.102",
{{#if (subgraphPath)}}
"@cerc-io/graph-node": "^0.2.101",
"@cerc-io/graph-node": "^0.2.102",
{{/if}}
"@ethersproject/providers": "^5.4.4",
"debug": "^4.3.1",
Expand Down
10 changes: 5 additions & 5 deletions packages/graph-node/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "@cerc-io/graph-node",
"version": "0.2.101",
"version": "0.2.102",
"main": "dist/index.js",
"license": "AGPL-3.0",
"devDependencies": {
"@cerc-io/solidity-mapper": "^0.2.101",
"@cerc-io/solidity-mapper": "^0.2.102",
"@ethersproject/providers": "^5.4.4",
"@graphprotocol/graph-ts": "^0.22.0",
"@nomiclabs/hardhat-ethers": "^2.0.2",
Expand Down Expand Up @@ -51,9 +51,9 @@
"dependencies": {
"@apollo/client": "^3.3.19",
"@cerc-io/assemblyscript": "0.19.10-watcher-ts-0.1.2",
"@cerc-io/cache": "^0.2.101",
"@cerc-io/ipld-eth-client": "^0.2.101",
"@cerc-io/util": "^0.2.101",
"@cerc-io/cache": "^0.2.102",
"@cerc-io/ipld-eth-client": "^0.2.102",
"@cerc-io/util": "^0.2.102",
"@types/json-diff": "^0.5.2",
"@types/yargs": "^17.0.0",
"bn.js": "^4.11.9",
Expand Down
8 changes: 5 additions & 3 deletions packages/graph-node/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface Context {
rpcSupportsBlockHashParam: boolean;
block?: Block;
contractAddress?: string;
dataSourceName?: string;
}

const log = debug('vulcanize:graph-node');
Expand Down Expand Up @@ -719,13 +720,14 @@ export const instantiate = async (
},
'dataSource.context': async () => {
assert(context.contractAddress);
const contract = indexer.isWatchedContract(context.contractAddress);
const watchedContracts = indexer.isContractAddressWatched(context.contractAddress);
const dataSourceContract = watchedContracts?.find(contract => contract.kind === context.dataSourceName);

if (!contract) {
if (!dataSourceContract) {
return null;
}

return database.toGraphContext(instanceExports, contract.context);
return database.toGraphContext(instanceExports, dataSourceContract.context);
},
'dataSource.network': async () => {
assert(dataSource);
Expand Down
111 changes: 63 additions & 48 deletions packages/graph-node/src/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,14 @@ export class GraphWatcher {
async addContracts () {
assert(this._indexer);
assert(this._indexer.watchContract);
assert(this._indexer.isWatchedContract);

// Watching the contract(s) if not watched already.
for (const dataSource of this._dataSources) {
const { source: { address, startBlock }, name } = dataSource;

// Skip for templates as they are added dynamically.
if (address) {
const watchedContract = await this._indexer.isWatchedContract(address);
const watchedContract = this._indexer.isContractAddressWatched(address);

if (!watchedContract) {
await this._indexer.watchContract(address, name, true, startBlock);
Expand All @@ -197,64 +196,79 @@ export class GraphWatcher {
const blockData = this._context.block;
assert(blockData);

assert(this._indexer && this._indexer.isWatchedContract);
const watchedContract = this._indexer.isWatchedContract(contract);
assert(watchedContract);
assert(this._indexer);
const watchedContracts = this._indexer.isContractAddressWatched(contract);
assert(watchedContracts);

// Get dataSource in subgraph yaml based on contract address.
const dataSource = this._dataSources.find(dataSource => dataSource.name === watchedContract.kind);
// Get dataSources in subgraph yaml based on contract kind (same as dataSource.name)
const dataSources = this._dataSources
.filter(dataSource => watchedContracts.some(contract => contract.kind === dataSource.name));

if (!dataSource) {
if (!dataSources.length) {
log(`Subgraph doesn't have configuration for contract ${contract}`);
return;
}

this._context.contractAddress = contract;
for (const dataSource of dataSources) {
this._context.contractAddress = contract;
this._context.dataSourceName = dataSource.name;

const { instance, contractInterface } = this._dataSourceMap[watchedContract.kind];
assert(instance);
const { exports: instanceExports } = instance;

// Get event handler based on event topic (from event signature).
const eventTopic = contractInterface.getEventTopic(eventSignature);
const eventHandler = dataSource.mapping.eventHandlers.find((eventHandler: any) => {
// The event signature we get from logDescription is different than that given in the subgraph yaml file.
// For eg. event in subgraph.yaml: Stake(indexed address,uint256); from logDescription: Stake(address,uint256)
// ethers.js doesn't recognize the subgraph event signature with indexed keyword before param type.
// Match event topics from cleaned subgraph event signature (Stake(indexed address,uint256) -> Stake(address,uint256)).
const subgraphEventTopic = contractInterface.getEventTopic(eventHandler.event.replace(/indexed /g, ''));

return subgraphEventTopic === eventTopic;
});
const { instance, contractInterface } = this._dataSourceMap[dataSource.name];
assert(instance);
const { exports: instanceExports } = instance;
let eventTopic: string;

try {
eventTopic = contractInterface.getEventTopic(eventSignature);
} catch (err) {
// Continue loop only if no matching event found
if (!((err as Error).message.includes('no matching event'))) {
throw err;
}

if (!eventHandler) {
log(`No handler configured in subgraph for event ${eventSignature}`);
return;
}
continue;
}

const eventFragment = contractInterface.getEvent(eventSignature);
// Get event handler based on event topic (from event signature).
const eventHandler = dataSource.mapping.eventHandlers.find((eventHandler: any) => {
// The event signature we get from logDescription is different than that given in the subgraph yaml file.
// For eg. event in subgraph.yaml: Stake(indexed address,uint256); from logDescription: Stake(address,uint256)
// ethers.js doesn't recognize the subgraph event signature with indexed keyword before param type.
// Match event topics from cleaned subgraph event signature (Stake(indexed address,uint256) -> Stake(address,uint256)).
const subgraphEventTopic = contractInterface.getEventTopic(eventHandler.event.replace(/indexed /g, ''));

const tx = this._getTransactionData(txHash, extraData.ethFullTransactions);
return subgraphEventTopic === eventTopic;
});

const data = {
block: blockData,
inputs: eventFragment.inputs,
event,
tx,
eventIndex
};
if (!eventHandler) {
log(`No handler configured in subgraph for event ${eventSignature}`);
return;
}

// Create ethereum event to be passed to the wasm event handler.
console.time(`time:graph-watcher#handleEvent-createEvent-block-${block.number}-event-${eventSignature}`);
const ethereumEvent = await createEvent(instanceExports, contract, data);
console.timeEnd(`time:graph-watcher#handleEvent-createEvent-block-${block.number}-event-${eventSignature}`);
try {
console.time(`time:graph-watcher#handleEvent-exec-${dataSource.name}-event-handler-${eventSignature}`);
await this._handleMemoryError(instanceExports[eventHandler.handler](ethereumEvent), dataSource.name);
console.timeEnd(`time:graph-watcher#handleEvent-exec-${dataSource.name}-event-handler-${eventSignature}`);
} catch (error) {
this._clearCachedEntities();
throw error;
const eventFragment = contractInterface.getEvent(eventSignature);

const tx = this._getTransactionData(txHash, extraData.ethFullTransactions);

const data = {
block: blockData,
inputs: eventFragment.inputs,
event,
tx,
eventIndex
};

// Create ethereum event to be passed to the wasm event handler.
console.time(`time:graph-watcher#handleEvent-createEvent-block-${block.number}-event-${eventSignature}`);
const ethereumEvent = await createEvent(instanceExports, contract, data);
console.timeEnd(`time:graph-watcher#handleEvent-createEvent-block-${block.number}-event-${eventSignature}`);
try {
console.time(`time:graph-watcher#handleEvent-exec-${dataSource.name}-event-handler-${eventSignature}`);
await this._handleMemoryError(instanceExports[eventHandler.handler](ethereumEvent), dataSource.name);
console.timeEnd(`time:graph-watcher#handleEvent-exec-${dataSource.name}-event-handler-${eventSignature}`);
} catch (error) {
this._clearCachedEntities();
throw error;
}
}
}

Expand Down Expand Up @@ -311,6 +325,7 @@ export class GraphWatcher {

for (const contractAddress of contractAddressList) {
this._context.contractAddress = contractAddress;
this._context.dataSourceName = dataSource.name;

// Call all the block handlers one after another for a contract.
const blockHandlerPromises = dataSource.mapping.blockHandlers.map(async (blockHandler: any): Promise<void> => {
Expand Down
2 changes: 1 addition & 1 deletion packages/graph-node/test/utils/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export class Indexer implements IndexerInterface {
return undefined;
}

isWatchedContract (address : string): ContractInterface | undefined {
isContractAddressWatched (address : string): ContractInterface[] | undefined {
return undefined;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/ipld-eth-client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/ipld-eth-client",
"version": "0.2.101",
"version": "0.2.102",
"description": "IPLD ETH Client",
"main": "dist/index.js",
"scripts": {
Expand All @@ -20,8 +20,8 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@apollo/client": "^3.7.1",
"@cerc-io/cache": "^0.2.101",
"@cerc-io/util": "^0.2.101",
"@cerc-io/cache": "^0.2.102",
"@cerc-io/util": "^0.2.102",
"cross-fetch": "^3.1.4",
"debug": "^4.3.1",
"ethers": "^5.4.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/peer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/peer",
"version": "0.2.101",
"version": "0.2.102",
"description": "libp2p module",
"main": "dist/index.js",
"exports": "./dist/index.js",
Expand Down
8 changes: 4 additions & 4 deletions packages/rpc-eth-client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/rpc-eth-client",
"version": "0.2.101",
"version": "0.2.102",
"description": "RPC ETH Client",
"main": "dist/index.js",
"scripts": {
Expand All @@ -19,9 +19,9 @@
},
"homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": {
"@cerc-io/cache": "^0.2.101",
"@cerc-io/ipld-eth-client": "^0.2.101",
"@cerc-io/util": "^0.2.101",
"@cerc-io/cache": "^0.2.102",
"@cerc-io/ipld-eth-client": "^0.2.102",
"@cerc-io/util": "^0.2.102",
"chai": "^4.3.4",
"ethers": "^5.4.4",
"left-pad": "^1.3.0",
Expand Down
Loading

0 comments on commit 2217cd3

Please sign in to comment.