2021-10-28 11:01:56 +00:00
//
// Copyright 2021 Vulcanize, Inc.
//
2021-11-12 12:39:03 +00:00
import assert from 'assert' ;
2021-10-28 11:01:56 +00:00
import 'reflect-metadata' ;
import debug from 'debug' ;
import path from 'path' ;
import fs from 'fs' ;
2021-12-01 10:48:38 +00:00
import { ContractInterface , utils , providers } from 'ethers' ;
2021-10-28 11:01:56 +00:00
import { ResultObject } from '@vulcanize/assemblyscript/lib/loader' ;
2021-11-10 07:42:37 +00:00
import { EthClient } from '@vulcanize/ipld-eth-client' ;
2021-12-29 07:51:39 +00:00
import { IndexerInterface , getFullBlock , BlockHeight , ServerConfig , getFullTransaction } from '@vulcanize/util' ;
2021-10-28 11:01:56 +00:00
2021-12-29 07:51:39 +00:00
import { createBlock , createEvent , getSubgraphConfig , resolveEntityFieldConflicts , Transaction } from './utils' ;
2021-12-21 10:28:17 +00:00
import { Context , GraphData , instantiate } from './loader' ;
2021-11-10 07:42:37 +00:00
import { Database } from './database' ;
2021-11-01 09:43:22 +00:00
2021-10-28 11:01:56 +00:00
const log = debug ( 'vulcanize:graph-watcher' ) ;
2021-11-01 09:43:22 +00:00
interface DataSource {
2021-12-21 10:28:17 +00:00
instance? : ResultObject & { exports : any } ,
contractInterface : utils.Interface ,
data : GraphData ,
2021-11-01 09:43:22 +00:00
}
2021-10-28 11:01:56 +00:00
export class GraphWatcher {
2021-11-10 07:42:37 +00:00
_database : Database ;
2021-11-12 12:39:03 +00:00
_indexer? : IndexerInterface ;
2021-11-10 07:42:37 +00:00
_postgraphileClient : EthClient ;
2021-12-01 10:48:38 +00:00
_ethProvider : providers.BaseProvider ;
2021-10-28 11:01:56 +00:00
_subgraphPath : string ;
2021-12-21 10:28:17 +00:00
_wasmRestartBlocksInterval : number ;
2021-11-01 09:43:22 +00:00
_dataSources : any [ ] = [ ] ;
_dataSourceMap : { [ key : string ] : DataSource } = { } ;
2021-12-29 07:51:39 +00:00
_transactionsMap : Map < string , Transaction > = new Map ( )
2021-10-28 11:01:56 +00:00
2021-12-30 12:27:34 +00:00
_context : Context = { } ;
2021-11-10 07:42:37 +00:00
2021-12-21 10:28:17 +00:00
constructor ( database : Database , postgraphileClient : EthClient , ethProvider : providers.BaseProvider , serverConfig : ServerConfig ) {
2021-11-10 07:42:37 +00:00
this . _database = database ;
this . _postgraphileClient = postgraphileClient ;
2021-12-01 10:48:38 +00:00
this . _ethProvider = ethProvider ;
2021-12-21 10:28:17 +00:00
this . _subgraphPath = serverConfig . subgraphPath ;
this . _wasmRestartBlocksInterval = serverConfig . wasmRestartBlocksInterval ;
2021-10-28 11:01:56 +00:00
}
async init ( ) {
const { dataSources } = await getSubgraphConfig ( this . _subgraphPath ) ;
this . _dataSources = dataSources ;
2021-11-01 09:43:22 +00:00
// Create wasm instance and contract interface for each dataSource in subgraph yaml.
const dataPromises = this . _dataSources . map ( async ( dataSource : any ) = > {
2021-12-30 12:27:34 +00:00
const { source : { address , abi } , mapping , network } = dataSource ;
2021-10-28 11:01:56 +00:00
const { abis , file } = mapping ;
2021-11-01 09:43:22 +00:00
const abisMap = abis . reduce ( ( acc : { [ key : string ] : ContractInterface } , abi : any ) = > {
const { name , file } = abi ;
const abiFilePath = path . join ( this . _subgraphPath , file ) ;
acc [ name ] = JSON . parse ( fs . readFileSync ( abiFilePath ) . toString ( ) ) ;
return acc ;
} , { } ) ;
const contractInterface = new utils . Interface ( abisMap [ abi ] ) ;
2021-10-28 11:01:56 +00:00
const data = {
2021-11-01 09:43:22 +00:00
abis : abisMap ,
2021-10-28 11:01:56 +00:00
dataSource : {
2021-12-30 12:27:34 +00:00
address ,
network
2021-10-28 11:01:56 +00:00
}
} ;
const filePath = path . join ( this . _subgraphPath , file ) ;
2021-11-12 12:39:03 +00:00
assert ( this . _indexer ) ;
2021-11-01 09:43:22 +00:00
return {
2021-12-08 12:54:46 +00:00
instance : await instantiate ( this . _database , this . _indexer , this . _ethProvider , this . _context , filePath , data ) ,
2021-12-21 10:28:17 +00:00
contractInterface ,
data
2021-11-01 09:43:22 +00:00
} ;
} , { } ) ;
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 ] ;
2021-10-28 11:01:56 +00:00
return acc ;
} , { } ) ;
}
2021-11-18 10:46:37 +00:00
async addContracts ( ) {
2021-12-13 10:56:01 +00:00
assert ( this . _indexer ) ;
assert ( this . _indexer . watchContract ) ;
assert ( this . _indexer . isWatchedContract ) ;
2021-11-18 10:46:37 +00:00
2021-12-13 10:56:01 +00:00
// Watching the contract(s) if not watched already.
2021-11-18 10:46:37 +00:00
for ( const dataSource of this . _dataSources ) {
const { source : { address , startBlock } , name } = dataSource ;
2021-12-13 10:56:01 +00:00
const watchedContract = await this . _indexer . isWatchedContract ( address ) ;
if ( ! watchedContract ) {
await this . _indexer . watchContract ( address , name , true , startBlock ) ;
}
2021-11-18 10:46:37 +00:00
}
}
2021-10-28 11:01:56 +00:00
async handleEvent ( eventData : any ) {
2021-12-29 07:51:39 +00:00
const { contract , event , eventSignature , block , tx : { hash : txHash } , eventIndex } = eventData ;
2021-10-28 11:01:56 +00:00
2021-12-29 07:51:39 +00:00
if ( ! this . _context . block ) {
this . _context . block = await getFullBlock ( this . _postgraphileClient , this . _ethProvider , block . hash ) ;
}
2021-11-10 07:42:37 +00:00
2021-12-29 07:51:39 +00:00
const blockData = this . _context . block ;
assert ( blockData ) ;
2021-11-10 07:42:37 +00:00
2021-11-01 09:43:22 +00:00
// Get dataSource in subgraph yaml based on contract address.
2021-10-28 11:01:56 +00:00
const dataSource = this . _dataSources . find ( dataSource = > dataSource . source . address === contract ) ;
if ( ! dataSource ) {
log ( ` Subgraph doesnt have configuration for contract ${ contract } ` ) ;
return ;
}
2021-12-21 10:28:17 +00:00
const { instance , contractInterface } = this . _dataSourceMap [ contract ] ;
assert ( instance ) ;
const { exports : instanceExports } = instance ;
2021-12-08 10:52:20 +00:00
// 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 ;
} ) ;
2021-11-01 09:43:22 +00:00
if ( ! eventHandler ) {
log ( ` No handler configured in subgraph for event ${ eventSignature } ` ) ;
return ;
}
const eventFragment = contractInterface . getEvent ( eventSignature ) ;
2021-12-29 07:51:39 +00:00
const tx = await this . _getTransactionData ( blockData . headerId , txHash ) ;
2021-11-01 09:43:22 +00:00
const data = {
2021-11-10 07:42:37 +00:00
block : blockData ,
2021-11-23 05:53:47 +00:00
inputs : eventFragment.inputs ,
event ,
2021-11-01 09:43:22 +00:00
tx ,
eventIndex
} ;
2021-10-28 11:01:56 +00:00
2021-11-01 09:43:22 +00:00
// Create ethereum event to be passed to the wasm event handler.
2021-11-29 13:07:11 +00:00
const ethereumEvent = await createEvent ( instanceExports , contract , data ) ;
2021-10-28 11:01:56 +00:00
2021-12-21 10:28:17 +00:00
await this . _handleMemoryError ( instanceExports [ eventHandler . handler ] ( ethereumEvent ) , dataSource . source . address ) ;
2021-10-28 11:01:56 +00:00
}
2021-11-10 07:42:37 +00:00
2021-11-15 06:41:56 +00:00
async handleBlock ( blockHash : string ) {
2021-12-01 10:48:38 +00:00
const blockData = await getFullBlock ( this . _postgraphileClient , this . _ethProvider , blockHash ) ;
2021-11-15 06:41:56 +00:00
2021-12-29 07:51:39 +00:00
this . _context . block = blockData ;
// Clear transactions map on handling new block.
this . _transactionsMap . clear ( ) ;
2021-11-18 10:46:37 +00:00
2021-11-15 06:41:56 +00:00
// Call block handler(s) for each contract.
for ( const dataSource of this . _dataSources ) {
2021-12-21 10:28:17 +00:00
// Reinstantiate WASM after every N blocks.
if ( blockData . blockNumber % this . _wasmRestartBlocksInterval === 0 ) {
// The WASM instance allocates memory as required and the limit is 4GB.
// https://stackoverflow.com/a/40453962
// https://github.com/AssemblyScript/assemblyscript/pull/1268#issue-618411291
// https://github.com/WebAssembly/memory64/blob/main/proposals/memory64/Overview.md#motivation
await this . _reInitWasm ( dataSource . source . address ) ;
}
2021-12-16 09:23:28 +00:00
// Check if block handler(s) are configured and start block has been reached.
if ( ! dataSource . mapping . blockHandlers || blockData . blockNumber < dataSource . source . startBlock ) {
2021-11-15 06:41:56 +00:00
continue ;
}
2021-12-21 10:28:17 +00:00
const { instance } = this . _dataSourceMap [ dataSource . source . address ] ;
assert ( instance ) ;
const { exports : instanceExports } = instance ;
2021-11-15 06:41:56 +00:00
// Create ethereum block to be passed to a wasm block handler.
2021-11-29 13:07:11 +00:00
const ethereumBlock = await createBlock ( instanceExports , blockData ) ;
2021-11-15 06:41:56 +00:00
// Call all the block handlers one after the another for a contract.
const blockHandlerPromises = dataSource . mapping . blockHandlers . map ( async ( blockHandler : any ) : Promise < void > = > {
2021-11-29 13:07:11 +00:00
await instanceExports [ blockHandler . handler ] ( ethereumBlock ) ;
2021-11-15 06:41:56 +00:00
} ) ;
2021-12-21 10:28:17 +00:00
await this . _handleMemoryError ( Promise . all ( blockHandlerPromises ) , dataSource . source . address ) ;
2021-11-15 06:41:56 +00:00
}
}
2021-11-12 12:39:03 +00:00
setIndexer ( indexer : IndexerInterface ) : void {
this . _indexer = indexer ;
}
2021-11-26 11:07:33 +00:00
async getEntity < Entity > ( entity : new ( ) = > Entity , id : string , relations : { [ key : string ] : any } , block? : BlockHeight ) : Promise < any > {
2021-11-17 05:25:05 +00:00
// Get entity from the database.
2021-11-26 11:07:33 +00:00
const result = await this . _database . getEntityWithRelations ( entity , id , relations , block ) as any ;
2021-11-17 05:25:05 +00:00
// Resolve any field name conflicts in the entity result.
return resolveEntityFieldConflicts ( result ) ;
2021-11-10 07:42:37 +00:00
}
2021-12-21 10:28:17 +00:00
/ * *
* Method to reinstantiate WASM instance for specified contract address .
* @param contractAddress
* /
async _reInitWasm ( contractAddress : string ) : Promise < void > {
const { data , instance } = this . _dataSourceMap [ contractAddress ] ;
assert ( instance ) ;
const { module } = instance ;
delete this . _dataSourceMap [ contractAddress ] . instance ;
assert ( this . _indexer ) ;
// Reinstantiate with existing module.
this . _dataSourceMap [ contractAddress ] . instance = await instantiate (
this . _database ,
this . _indexer ,
this . _ethProvider ,
this . _context ,
module ,
data
) ;
// 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
this . _dataSourceMap [ contractAddress ] . instance ! . exports . _start ( ) ;
}
async _handleMemoryError ( handlerPromise : Promise < any > , contractAddress : string ) : Promise < void > {
try {
await handlerPromise ;
} catch ( error ) {
if ( error instanceof WebAssembly . RuntimeError && error instanceof Error ) {
if ( error . message === 'unreachable' ) {
// Reintantiate WASM for out of memory error.
this . _reInitWasm ( contractAddress ) ;
}
}
// Job will retry after throwing error.
throw error ;
}
}
2021-12-29 07:51:39 +00:00
async _getTransactionData ( headerId : number , txHash : string ) : Promise < Transaction > {
let transaction = this . _transactionsMap . get ( txHash ) ;
if ( transaction ) {
return transaction ;
}
transaction = await getFullTransaction ( this . _postgraphileClient , headerId , txHash ) ;
assert ( transaction ) ;
this . _transactionsMap . set ( txHash , transaction ) ;
return transaction ;
}
2021-10-28 11:01:56 +00:00
}