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' ;
2022-10-04 08:01:29 +00:00
import { SelectionNode } from 'graphql' ;
2021-10-28 11:01:56 +00:00
import { ResultObject } from '@vulcanize/assemblyscript/lib/loader' ;
2022-09-09 11:43:01 +00:00
import { EthClient } from '@cerc-io/ipld-eth-client' ;
2022-10-20 09:09:32 +00:00
import { getFullBlock , BlockHeight , ServerConfig , getFullTransaction , QueryOptions , StateInterface , IndexerInterface , BlockProgressInterface } from '@cerc-io/util' ;
2021-10-28 11:01:56 +00:00
2022-10-20 09:09:32 +00:00
import { createBlock , createEvent , getSubgraphConfig , resolveEntityFieldConflicts , Transaction , updateEntitiesFromState } from './utils' ;
2021-12-21 10:28:17 +00:00
import { Context , GraphData , instantiate } from './loader' ;
2022-09-01 08:47:43 +00:00
import { Database , DEFAULT_LIMIT } 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 ;
2022-10-11 08:11:26 +00:00
_indexer? : IndexerInterface ;
2022-06-08 06:43:52 +00:00
_ethClient : 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
2022-06-08 06:43:52 +00:00
constructor ( database : Database , ethClient : EthClient , ethProvider : providers.BaseProvider , serverConfig : ServerConfig ) {
2021-11-10 07:42:37 +00:00
this . _database = database ;
2022-06-08 06:43:52 +00:00
this . _ethClient = ethClient ;
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 ( ) {
2022-01-06 13:02:08 +00:00
const { dataSources , templates = [ ] } = await getSubgraphConfig ( this . _subgraphPath ) ;
this . _dataSources = dataSources . concat ( templates ) ;
2021-10-28 11:01:56 +00:00
2022-01-06 13:02:08 +00:00
// Create wasm instance and contract interface for each dataSource and template in subgraph yaml.
2021-11-01 09:43:22 +00:00
const dataPromises = this . _dataSources . map ( async ( dataSource : any ) = > {
2022-08-26 06:32:39 +00:00
const { source : { abi } , mapping , network , name } = 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 : {
2022-08-26 06:32:39 +00:00
network ,
name
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 ( ) ;
2022-01-06 13:02:08 +00:00
const { name } = dataSource ;
acc [ name ] = data [ index ] ;
2021-11-01 09:43:22 +00:00
2021-10-28 11:01:56 +00:00
return acc ;
} , { } ) ;
}
2022-09-09 10:53:41 +00:00
get dataSources ( ) : any [ ] {
return this . _dataSources ;
}
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
2022-01-06 13:02:08 +00:00
// Skip for templates as they are added dynamically.
if ( address ) {
const watchedContract = await this . _indexer . isWatchedContract ( address ) ;
2021-12-13 10:56:01 +00:00
2022-01-06 13:02:08 +00:00
if ( ! watchedContract ) {
await this . _indexer . watchContract ( address , name , true , startBlock ) ;
}
2021-12-13 10:56:01 +00:00
}
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
2022-09-07 12:59:04 +00:00
// Check if block data is already fetched by a previous event in the same block.
2022-08-08 10:28:13 +00:00
if ( ! this . _context . block || this . _context . block . blockHash !== block . hash ) {
2022-11-03 11:19:41 +00:00
this . _context . block = await getFullBlock ( this . _ethClient , this . _ethProvider , block . hash , block . number ) ;
2021-12-29 07:51:39 +00:00
}
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
2022-01-06 13:02:08 +00:00
assert ( this . _indexer && this . _indexer . isWatchedContract ) ;
2022-11-03 08:44:58 +00:00
const watchedContract = this . _indexer . isWatchedContract ( contract ) ;
2022-01-06 13:02:08 +00:00
assert ( watchedContract ) ;
2021-11-01 09:43:22 +00:00
// Get dataSource in subgraph yaml based on contract address.
2022-01-06 13:02:08 +00:00
const dataSource = this . _dataSources . find ( dataSource = > dataSource . name === watchedContract . kind ) ;
2021-10-28 11:01:56 +00:00
if ( ! dataSource ) {
2022-01-06 13:02:08 +00:00
log ( ` Subgraph doesn't have configuration for contract ${ contract } ` ) ;
2021-10-28 11:01:56 +00:00
return ;
}
2022-01-06 13:02:08 +00:00
this . _context . contractAddress = contract ;
const { instance , contractInterface } = this . _dataSourceMap [ watchedContract . kind ] ;
2021-12-21 10:28:17 +00:00
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 ) ;
2022-11-03 08:44:58 +00:00
const tx = await this . _getTransactionData ( txHash , Number ( blockData . blockNumber ) ) ;
2021-12-29 07:51:39 +00:00
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 ) ;
2022-10-19 08:56:10 +00:00
try {
await this . _handleMemoryError ( instanceExports [ eventHandler . handler ] ( ethereumEvent ) , dataSource . name ) ;
} catch ( error ) {
this . _clearCachedEntities ( ) ;
throw error ;
}
2021-10-28 11:01:56 +00:00
}
2021-11-10 07:42:37 +00:00
2022-11-03 11:19:41 +00:00
async handleBlock ( blockHash : string , blockNumber : number ) {
2022-09-07 12:59:04 +00:00
// Check if block data is already fetched in handleEvent method for the same block.
if ( ! this . _context . block || this . _context . block . blockHash !== blockHash ) {
2022-11-03 11:19:41 +00:00
this . _context . block = await getFullBlock ( this . _ethClient , this . _ethProvider , blockHash , blockNumber ) ;
2022-09-07 12:59:04 +00:00
}
2021-11-15 06:41:56 +00:00
2022-09-07 12:59:04 +00:00
const blockData = this . _context . block ;
assert ( blockData ) ;
2021-12-29 07:51:39 +00:00
// 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.
2022-09-07 12:59:04 +00:00
if ( Number ( blockData . blockNumber ) % this . _wasmRestartBlocksInterval === 0 ) {
2021-12-21 10:28:17 +00:00
// 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
2022-01-06 13:02:08 +00:00
await this . _reInitWasm ( dataSource . name ) ;
2021-12-21 10:28:17 +00:00
}
2022-01-06 13:02:08 +00:00
// Check if block handler(s) are configured.
if ( ! dataSource . mapping . blockHandlers ) {
2021-11-15 06:41:56 +00:00
continue ;
}
2022-01-06 13:02:08 +00:00
const { instance } = this . _dataSourceMap [ dataSource . name ] ;
2021-12-21 10:28:17 +00:00
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
2022-01-06 13:02:08 +00:00
let contractAddressList : string [ ] = [ ] ;
if ( dataSource . source . address ) {
// Check if start block has been reached.
if ( blockData . blockNumber >= dataSource . source . startBlock ) {
contractAddressList . push ( dataSource . source . address ) ;
}
} else {
// Data source templates will have multiple watched contracts.
assert ( this . _indexer ? . getContractsByKind ) ;
const watchedContracts = this . _indexer . getContractsByKind ( dataSource . name ) ;
2022-09-07 12:59:04 +00:00
contractAddressList = watchedContracts . filter ( contract = > Number ( blockData . blockNumber ) >= contract . startingBlock )
2022-01-06 13:02:08 +00:00
. map ( contract = > contract . address ) ;
}
for ( const contractAddress of contractAddressList ) {
this . _context . contractAddress = contractAddress ;
2021-11-15 06:41:56 +00:00
2022-01-06 13:02:08 +00:00
// Call all the block handlers one after another for a contract.
const blockHandlerPromises = dataSource . mapping . blockHandlers . map ( async ( blockHandler : any ) : Promise < void > = > {
await instanceExports [ blockHandler . handler ] ( ethereumBlock ) ;
} ) ;
2022-10-19 08:56:10 +00:00
try {
await this . _handleMemoryError ( Promise . all ( blockHandlerPromises ) , dataSource . name ) ;
} catch ( error ) {
this . _clearCachedEntities ( ) ;
throw error ;
}
2022-01-06 13:02:08 +00:00
}
2021-11-15 06:41:56 +00:00
}
}
2022-10-11 08:11:26 +00:00
setIndexer ( indexer : IndexerInterface ) : void {
2021-11-12 12:39:03 +00:00
this . _indexer = indexer ;
}
2022-10-04 08:01:29 +00:00
async getEntity < Entity > (
entity : new ( ) = > Entity ,
id : string ,
relationsMap : Map < any , { [ key : string ] : any } > ,
block : BlockHeight ,
selections : ReadonlyArray < SelectionNode > = [ ]
) : Promise < any > {
2022-09-13 11:54:14 +00:00
const dbTx = await this . _database . createTransactionRunner ( ) ;
try {
// Get entity from the database.
2022-10-04 08:01:29 +00:00
const result = await this . _database . getEntityWithRelations ( dbTx , entity , id , relationsMap , block , selections ) ;
2022-09-13 11:54:14 +00:00
await dbTx . commitTransaction ( ) ;
// Resolve any field name conflicts in the entity result.
return resolveEntityFieldConflicts ( result ) ;
} catch ( error ) {
await dbTx . rollbackTransaction ( ) ;
throw error ;
} finally {
await dbTx . release ( ) ;
}
2021-11-10 07:42:37 +00:00
}
2021-12-21 10:28:17 +00:00
2022-10-04 08:01:29 +00:00
async getEntities < Entity > (
entity : new ( ) = > Entity ,
relationsMap : Map < any , { [ key : string ] : any } > ,
block : BlockHeight ,
where : { [ key : string ] : any } = { } ,
queryOptions : QueryOptions ,
selections : ReadonlyArray < SelectionNode > = [ ]
) : Promise < any > {
2022-09-13 11:54:14 +00:00
const dbTx = await this . _database . createTransactionRunner ( ) ;
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
try {
where = Object . entries ( where ) . reduce ( ( acc : { [ key : string ] : any } , [ fieldWithSuffix , value ] ) = > {
const [ field , . . . suffix ] = fieldWithSuffix . split ( '_' ) ;
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
if ( ! acc [ field ] ) {
acc [ field ] = [ ] ;
}
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
const filter = {
value ,
not : false ,
operator : 'equals'
} ;
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
let operator = suffix . shift ( ) ;
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
if ( operator === 'not' ) {
filter . not = true ;
operator = suffix . shift ( ) ;
}
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
if ( operator ) {
filter . operator = operator ;
}
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
acc [ field ] . push ( filter ) ;
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
return acc ;
} , { } ) ;
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
if ( ! queryOptions . limit ) {
queryOptions . limit = DEFAULT_LIMIT ;
}
2022-09-01 08:47:43 +00:00
2022-09-13 11:54:14 +00:00
// Get entities from the database.
2022-10-04 08:01:29 +00:00
const entities = await this . _database . getEntities ( dbTx , entity , relationsMap , block , where , queryOptions , selections ) ;
2022-09-13 11:54:14 +00:00
await dbTx . commitTransaction ( ) ;
} catch ( error ) {
await dbTx . rollbackTransaction ( ) ;
throw error ;
} finally {
await dbTx . release ( ) ;
}
2022-09-01 08:47:43 +00:00
}
2022-10-19 09:54:14 +00:00
async updateEntitiesFromState ( state : StateInterface ) {
2022-09-22 09:56:06 +00:00
assert ( this . _indexer ) ;
2022-10-20 09:09:32 +00:00
await updateEntitiesFromState ( this . _database , this . _indexer , state ) ;
2022-09-22 09:56:06 +00:00
}
2022-10-04 08:01:29 +00:00
updateEntityCacheFrothyBlocks ( blockProgress : BlockProgressInterface ) : void {
2022-10-19 08:56:10 +00:00
assert ( this . _indexer ) ;
2022-10-20 09:09:32 +00:00
this . _database . updateEntityCacheFrothyBlocks ( blockProgress , this . _indexer . serverConfig . clearEntitiesCacheInterval ) ;
2022-10-04 08:01:29 +00:00
}
pruneEntityCacheFrothyBlocks ( canonicalBlockHash : string , canonicalBlockNumber : number ) {
2022-10-20 09:09:32 +00:00
this . _database . pruneEntityCacheFrothyBlocks ( canonicalBlockHash , canonicalBlockNumber ) ;
2022-10-04 08:01:29 +00:00
}
2022-10-19 08:56:10 +00:00
_clearCachedEntities ( ) {
this . _database . cachedEntities . frothyBlocks . clear ( ) ;
this . _database . cachedEntities . latestPrunedEntities . clear ( ) ;
}
2021-12-21 10:28:17 +00:00
/ * *
2022-01-06 13:02:08 +00:00
* Method to reinstantiate WASM instance for specified dataSource .
* @param dataSourceName
2021-12-21 10:28:17 +00:00
* /
2022-01-06 13:02:08 +00:00
async _reInitWasm ( dataSourceName : string ) : Promise < void > {
const { data , instance } = this . _dataSourceMap [ dataSourceName ] ;
2021-12-21 10:28:17 +00:00
assert ( instance ) ;
const { module } = instance ;
2022-01-06 13:02:08 +00:00
delete this . _dataSourceMap [ dataSourceName ] . instance ;
2021-12-21 10:28:17 +00:00
assert ( this . _indexer ) ;
// Reinstantiate with existing module.
2022-01-06 13:02:08 +00:00
this . _dataSourceMap [ dataSourceName ] . instance = await instantiate (
2021-12-21 10:28:17 +00:00
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
2022-01-06 13:02:08 +00:00
this . _dataSourceMap [ dataSourceName ] . instance ! . exports . _start ( ) ;
2021-12-21 10:28:17 +00:00
}
2022-01-06 13:02:08 +00:00
async _handleMemoryError ( handlerPromise : Promise < any > , dataSourceName : string ) : Promise < void > {
2021-12-21 10:28:17 +00:00
try {
await handlerPromise ;
} catch ( error ) {
if ( error instanceof WebAssembly . RuntimeError && error instanceof Error ) {
if ( error . message === 'unreachable' ) {
// Reintantiate WASM for out of memory error.
2022-01-06 13:02:08 +00:00
this . _reInitWasm ( dataSourceName ) ;
2021-12-21 10:28:17 +00:00
}
}
// Job will retry after throwing error.
throw error ;
}
}
2021-12-29 07:51:39 +00:00
2022-11-03 08:44:58 +00:00
async _getTransactionData ( txHash : string , blockNumber : number ) : Promise < Transaction > {
2021-12-29 07:51:39 +00:00
let transaction = this . _transactionsMap . get ( txHash ) ;
if ( transaction ) {
return transaction ;
}
2022-11-03 08:44:58 +00:00
transaction = await getFullTransaction ( this . _ethClient , txHash , blockNumber ) ;
2021-12-29 07:51:39 +00:00
assert ( transaction ) ;
this . _transactionsMap . set ( txHash , transaction ) ;
return transaction ;
}
2021-10-28 11:01:56 +00:00
}