//
// Copyright 2021 Vulcanize, Inc.
//

import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions, ObjectLiteral } from 'typeorm';
import debug from 'debug';
import { ethers, constants } from 'ethers';
import { GraphQLResolveInfo } from 'graphql';

import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import {
  Indexer as BaseIndexer,
  IndexerInterface,
  ValueResult,
  ServerConfig,
  JobQueue,
  Where,
  QueryOptions,
  BlockHeight,
  ResultMeta,
  updateSubgraphState,
  dumpSubgraphState,
  GraphWatcherInterface,
  StateKind,
  StateStatus,
  ResultEvent,
  getResultEvent,
  DatabaseInterface,
  Clients,
  EthClient,
  UpstreamConfig,
  EthFullBlock,
  EthFullTransaction,
  ExtraEventData
} from '@cerc-io/util';
import { GraphWatcher } from '@cerc-io/graph-node';

import PositionManagerArtifacts from './artifacts/PositionManager.json';
import ERC20PoolFactoryArtifacts from './artifacts/ERC20PoolFactory.json';
import ERC721PoolFactoryArtifacts from './artifacts/ERC721PoolFactory.json';
import GrantFundArtifacts from './artifacts/GrantFund.json';
import AjnaTokenArtifacts from './artifacts/AjnaToken.json';
import BurnWrappedAjnaArtifacts from './artifacts/BurnWrappedAjna.json';
import ERC20PoolArtifacts from './artifacts/ERC20Pool.json';
import ERC721PoolArtifacts from './artifacts/ERC721Pool.json';
import { Database, ENTITIES, SUBGRAPH_ENTITIES } from './database';
import { createInitialState, handleEvent, createStateDiff, createStateCheckpoint } from './hooks';
import { Contract } from './entity/Contract';
import { Event } from './entity/Event';
import { SyncStatus } from './entity/SyncStatus';
import { StateSyncStatus } from './entity/StateSyncStatus';
import { BlockProgress } from './entity/BlockProgress';
import { State } from './entity/State';
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Token } from './entity/Token';
import { PoolFactory } from './entity/PoolFactory';
import { Pool } from './entity/Pool';
import { Bucket } from './entity/Bucket';
import { Lend } from './entity/Lend';
import { Loan } from './entity/Loan';
import { Account } from './entity/Account';
import { LiquidationAuction } from './entity/LiquidationAuction';
import { ReserveAuction } from './entity/ReserveAuction';
import { LPTransferorList } from './entity/LPTransferorList';
import { LPAllowance } from './entity/LPAllowance';
import { LPAllowanceList } from './entity/LPAllowanceList';
import { AddCollateral } from './entity/AddCollateral';
import { AddQuoteToken } from './entity/AddQuoteToken';
import { AuctionSettle } from './entity/AuctionSettle';
import { BondWithdrawn } from './entity/BondWithdrawn';
import { BucketBankruptcy } from './entity/BucketBankruptcy';
import { BucketTake } from './entity/BucketTake';
import { BucketTakeLPAwarded } from './entity/BucketTakeLPAwarded';
import { DrawDebt } from './entity/DrawDebt';
import { Flashloan } from './entity/Flashloan';
import { Kick } from './entity/Kick';
import { LoanStamped } from './entity/LoanStamped';
import { MoveQuoteToken } from './entity/MoveQuoteToken';
import { RemoveCollateral } from './entity/RemoveCollateral';
import { RemoveQuoteToken } from './entity/RemoveQuoteToken';
import { RepayDebt } from './entity/RepayDebt';
import { ReserveAuctionKick } from './entity/ReserveAuctionKick';
import { ReserveAuctionTake } from './entity/ReserveAuctionTake';
import { ResetInterestRate } from './entity/ResetInterestRate';
import { Settle } from './entity/Settle';
import { Take } from './entity/Take';
import { TransferLP } from './entity/TransferLP';
import { UpdateInterestRate } from './entity/UpdateInterestRate';
import { Approval } from './entity/Approval';
import { ApprovalForAll } from './entity/ApprovalForAll';
import { MoveLiquidity } from './entity/MoveLiquidity';
import { Transfer } from './entity/Transfer';
import { PoolCreated } from './entity/PoolCreated';
import { AddCollateralNFT } from './entity/AddCollateralNFT';
import { AuctionNFTSettle } from './entity/AuctionNFTSettle';
import { DrawDebtNFT } from './entity/DrawDebtNFT';
import { MergeOrRemoveCollateralNFT } from './entity/MergeOrRemoveCollateralNFT';
import { Position } from './entity/Position';
import { PositionLend } from './entity/PositionLend';
import { Burn } from './entity/Burn';
import { MemorializePosition } from './entity/MemorializePosition';
import { Mint } from './entity/Mint';
import { RedeemPosition } from './entity/RedeemPosition';
import { GrantFund } from './entity/GrantFund';
import { DistributionPeriod } from './entity/DistributionPeriod';
import { Proposal } from './entity/Proposal';
import { ProposalParams } from './entity/ProposalParams';
import { DistributionPeriodVote } from './entity/DistributionPeriodVote';
import { ScreeningVote } from './entity/ScreeningVote';
import { FundingVote } from './entity/FundingVote';
import { FundedSlate } from './entity/FundedSlate';
import { DelegateRewardClaimed } from './entity/DelegateRewardClaimed';
import { FundTreasury } from './entity/FundTreasury';
import { FundedSlateUpdated } from './entity/FundedSlateUpdated';
import { ProposalCreated } from './entity/ProposalCreated';
import { ProposalExecuted } from './entity/ProposalExecuted';
import { DistributionPeriodStarted } from './entity/DistributionPeriodStarted';
import { VoteCast } from './entity/VoteCast';
import { DelegateChanged } from './entity/DelegateChanged';
import { DelegateVotesChanged } from './entity/DelegateVotesChanged';
import { BurnWrap } from './entity/BurnWrap';
/* eslint-enable @typescript-eslint/no-unused-vars */

import { FrothyEntity } from './entity/FrothyEntity';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const log = debug('vulcanize:indexer');

const KIND_POSITIONMANAGER = 'PositionManager';

const KIND_ERC20POOLFACTORY = 'ERC20PoolFactory';

const KIND_ERC721POOLFACTORY = 'ERC721PoolFactory';

const KIND_GRANTFUND = 'GrantFund';

const KIND_AJNATOKEN = 'AjnaToken';

const KIND_BURNWRAPPEDAJNA = 'BurnWrappedAjna';

const KIND_ERC20POOL = 'ERC20Pool';

const KIND_ERC721POOL = 'ERC721Pool';

export class Indexer implements IndexerInterface {
  _db: Database;
  _ethClient: EthClient;
  _ethProvider: BaseProvider;
  _baseIndexer: BaseIndexer;
  _serverConfig: ServerConfig;
  _upstreamConfig: UpstreamConfig;
  _graphWatcher: GraphWatcher;

  _abiMap: Map<string, JsonFragment[]>;
  _storageLayoutMap: Map<string, StorageLayout>;
  _contractMap: Map<string, ethers.utils.Interface>;
  eventSignaturesMap: Map<string, string[]>;

  _entityTypesMap: Map<string, { [key: string]: string }>;
  _relationsMap: Map<any, { [key: string]: any }>;

  _subgraphStateMap: Map<string, any>;

  constructor (
    config: {
      server: ServerConfig;
      upstream: UpstreamConfig;
    },
    db: DatabaseInterface,
    clients: Clients,
    ethProvider: BaseProvider,
    jobQueue: JobQueue,
    graphWatcher?: GraphWatcherInterface
  ) {
    assert(db);
    assert(clients.ethClient);

    this._db = db as Database;
    this._ethClient = clients.ethClient;
    this._ethProvider = ethProvider;
    this._serverConfig = config.server;
    this._upstreamConfig = config.upstream;
    this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
    assert(graphWatcher);
    this._graphWatcher = graphWatcher as GraphWatcher;

    this._abiMap = new Map();
    this._storageLayoutMap = new Map();
    this._contractMap = new Map();
    this.eventSignaturesMap = new Map();

    const { abi: PositionManagerABI } = PositionManagerArtifacts;

    const { abi: ERC20PoolFactoryABI } = ERC20PoolFactoryArtifacts;

    const { abi: ERC721PoolFactoryABI } = ERC721PoolFactoryArtifacts;

    const { abi: GrantFundABI } = GrantFundArtifacts;

    const { abi: AjnaTokenABI } = AjnaTokenArtifacts;

    const { abi: BurnWrappedAjnaABI } = BurnWrappedAjnaArtifacts;

    const { abi: ERC20PoolABI } = ERC20PoolArtifacts;

    const { abi: ERC721PoolABI } = ERC721PoolArtifacts;

    assert(PositionManagerABI);
    this._abiMap.set(KIND_POSITIONMANAGER, PositionManagerABI);

    const PositionManagerContractInterface = new ethers.utils.Interface(PositionManagerABI);
    this._contractMap.set(KIND_POSITIONMANAGER, PositionManagerContractInterface);

    const PositionManagerEventSignatures = Object.values(PositionManagerContractInterface.events).map(value => {
      return PositionManagerContractInterface.getEventTopic(value);
    });
    this.eventSignaturesMap.set(KIND_POSITIONMANAGER, PositionManagerEventSignatures);

    assert(ERC20PoolFactoryABI);
    this._abiMap.set(KIND_ERC20POOLFACTORY, ERC20PoolFactoryABI);

    const ERC20PoolFactoryContractInterface = new ethers.utils.Interface(ERC20PoolFactoryABI);
    this._contractMap.set(KIND_ERC20POOLFACTORY, ERC20PoolFactoryContractInterface);

    const ERC20PoolFactoryEventSignatures = Object.values(ERC20PoolFactoryContractInterface.events).map(value => {
      return ERC20PoolFactoryContractInterface.getEventTopic(value);
    });
    this.eventSignaturesMap.set(KIND_ERC20POOLFACTORY, ERC20PoolFactoryEventSignatures);

    assert(ERC721PoolFactoryABI);
    this._abiMap.set(KIND_ERC721POOLFACTORY, ERC721PoolFactoryABI);

    const ERC721PoolFactoryContractInterface = new ethers.utils.Interface(ERC721PoolFactoryABI);
    this._contractMap.set(KIND_ERC721POOLFACTORY, ERC721PoolFactoryContractInterface);

    const ERC721PoolFactoryEventSignatures = Object.values(ERC721PoolFactoryContractInterface.events).map(value => {
      return ERC721PoolFactoryContractInterface.getEventTopic(value);
    });
    this.eventSignaturesMap.set(KIND_ERC721POOLFACTORY, ERC721PoolFactoryEventSignatures);

    assert(GrantFundABI);
    this._abiMap.set(KIND_GRANTFUND, GrantFundABI);

    const GrantFundContractInterface = new ethers.utils.Interface(GrantFundABI);
    this._contractMap.set(KIND_GRANTFUND, GrantFundContractInterface);

    const GrantFundEventSignatures = Object.values(GrantFundContractInterface.events).map(value => {
      return GrantFundContractInterface.getEventTopic(value);
    });
    this.eventSignaturesMap.set(KIND_GRANTFUND, GrantFundEventSignatures);

    assert(AjnaTokenABI);
    this._abiMap.set(KIND_AJNATOKEN, AjnaTokenABI);

    const AjnaTokenContractInterface = new ethers.utils.Interface(AjnaTokenABI);
    this._contractMap.set(KIND_AJNATOKEN, AjnaTokenContractInterface);

    const AjnaTokenEventSignatures = Object.values(AjnaTokenContractInterface.events).map(value => {
      return AjnaTokenContractInterface.getEventTopic(value);
    });
    this.eventSignaturesMap.set(KIND_AJNATOKEN, AjnaTokenEventSignatures);

    assert(BurnWrappedAjnaABI);
    this._abiMap.set(KIND_BURNWRAPPEDAJNA, BurnWrappedAjnaABI);

    const BurnWrappedAjnaContractInterface = new ethers.utils.Interface(BurnWrappedAjnaABI);
    this._contractMap.set(KIND_BURNWRAPPEDAJNA, BurnWrappedAjnaContractInterface);

    const BurnWrappedAjnaEventSignatures = Object.values(BurnWrappedAjnaContractInterface.events).map(value => {
      return BurnWrappedAjnaContractInterface.getEventTopic(value);
    });
    this.eventSignaturesMap.set(KIND_BURNWRAPPEDAJNA, BurnWrappedAjnaEventSignatures);

    assert(ERC20PoolABI);
    this._abiMap.set(KIND_ERC20POOL, ERC20PoolABI);

    const ERC20PoolContractInterface = new ethers.utils.Interface(ERC20PoolABI);
    this._contractMap.set(KIND_ERC20POOL, ERC20PoolContractInterface);

    const ERC20PoolEventSignatures = Object.values(ERC20PoolContractInterface.events).map(value => {
      return ERC20PoolContractInterface.getEventTopic(value);
    });
    this.eventSignaturesMap.set(KIND_ERC20POOL, ERC20PoolEventSignatures);

    assert(ERC721PoolABI);
    this._abiMap.set(KIND_ERC721POOL, ERC721PoolABI);

    const ERC721PoolContractInterface = new ethers.utils.Interface(ERC721PoolABI);
    this._contractMap.set(KIND_ERC721POOL, ERC721PoolContractInterface);

    const ERC721PoolEventSignatures = Object.values(ERC721PoolContractInterface.events).map(value => {
      return ERC721PoolContractInterface.getEventTopic(value);
    });
    this.eventSignaturesMap.set(KIND_ERC721POOL, ERC721PoolEventSignatures);

    this._entityTypesMap = new Map();
    this._populateEntityTypesMap();

    this._relationsMap = new Map();
    this._populateRelationsMap();

    this._subgraphStateMap = new Map();
  }

  get serverConfig (): ServerConfig {
    return this._serverConfig;
  }

  get upstreamConfig (): UpstreamConfig {
    return this._upstreamConfig;
  }

  get storageLayoutMap (): Map<string, StorageLayout> {
    return this._storageLayoutMap;
  }

  get graphWatcher (): GraphWatcher {
    return this._graphWatcher;
  }

  async init (): Promise<void> {
    await this._baseIndexer.fetchContracts();
    await this._baseIndexer.fetchStateStatus();
  }

  switchClients ({ ethClient, ethProvider }: { ethClient: EthClient, ethProvider: BaseProvider }): void {
    this._ethClient = ethClient;
    this._ethProvider = ethProvider;
    this._baseIndexer.switchClients({ ethClient, ethProvider });
    this._graphWatcher.switchClients({ ethClient, ethProvider });
  }

  async getMetaData (block: BlockHeight): Promise<ResultMeta | null> {
    return this._baseIndexer.getMetaData(block);
  }

  getResultEvent (event: Event): ResultEvent {
    return getResultEvent(event);
  }

  async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult> {
    return this._baseIndexer.getStorageValue(
      storageLayout,
      blockHash,
      contractAddress,
      variable,
      ...mappingKeys
    );
  }

  async getEntitiesForBlock (blockHash: string, tableName: string): Promise<any[]> {
    return this._db.getEntitiesForBlock(blockHash, tableName);
  }

  async processInitialState (contractAddress: string, blockHash: string): Promise<any> {
    // Call initial state hook.
    return createInitialState(this, contractAddress, blockHash);
  }

  async processStateCheckpoint (contractAddress: string, blockHash: string): Promise<boolean> {
    // Call checkpoint hook.
    return createStateCheckpoint(this, contractAddress, blockHash);
  }

  async processCanonicalBlock (blockHash: string, blockNumber: number): Promise<void> {
    console.time('time:indexer#processCanonicalBlock-finalize_auto_diffs');
    // Finalize staged diff blocks if any.
    await this._baseIndexer.finalizeDiffStaged(blockHash);
    console.timeEnd('time:indexer#processCanonicalBlock-finalize_auto_diffs');

    // Call custom stateDiff hook.
    await createStateDiff(this, blockHash);

    this._graphWatcher.pruneEntityCacheFrothyBlocks(blockHash, blockNumber);
  }

  async processCheckpoint (blockHash: string): Promise<void> {
    // Return if checkpointInterval is <= 0.
    const checkpointInterval = this._serverConfig.checkpointInterval;
    if (checkpointInterval <= 0) return;

    console.time('time:indexer#processCheckpoint-checkpoint');
    await this._baseIndexer.processCheckpoint(this, blockHash, checkpointInterval);
    console.timeEnd('time:indexer#processCheckpoint-checkpoint');
  }

  async processCLICheckpoint (contractAddress: string, blockHash?: string): Promise<string | undefined> {
    return this._baseIndexer.processCLICheckpoint(this, contractAddress, blockHash);
  }

  async getPrevState (blockHash: string, contractAddress: string, kind?: string): Promise<State | undefined> {
    return this._db.getPrevState(blockHash, contractAddress, kind);
  }

  async getLatestState (contractAddress: string, kind: StateKind | null, blockNumber?: number): Promise<State | undefined> {
    return this._db.getLatestState(contractAddress, kind, blockNumber);
  }

  async getStatesByHash (blockHash: string): Promise<State[]> {
    return this._baseIndexer.getStatesByHash(blockHash);
  }

  async getStateByCID (cid: string): Promise<State | undefined> {
    return this._baseIndexer.getStateByCID(cid);
  }

  async getStates (where: FindConditions<State>): Promise<State[]> {
    return this._db.getStates(where);
  }

  getStateData (state: State): any {
    return this._baseIndexer.getStateData(state);
  }

  // Method used to create auto diffs (diff_staged).
  async createDiffStaged (contractAddress: string, blockHash: string, data: any): Promise<void> {
    console.time('time:indexer#createDiffStaged-auto_diff');
    await this._baseIndexer.createDiffStaged(contractAddress, blockHash, data);
    console.timeEnd('time:indexer#createDiffStaged-auto_diff');
  }

  // Method to be used by createStateDiff hook.
  async createDiff (contractAddress: string, blockHash: string, data: any): Promise<void> {
    const block = await this.getBlockProgress(blockHash);
    assert(block);

    await this._baseIndexer.createDiff(contractAddress, block, data);
  }

  // Method to be used by createStateCheckpoint hook.
  async createStateCheckpoint (contractAddress: string, blockHash: string, data: any): Promise<void> {
    const block = await this.getBlockProgress(blockHash);
    assert(block);

    return this._baseIndexer.createStateCheckpoint(contractAddress, block, data);
  }

  // Method to be used by export-state CLI.
  async createCheckpoint (contractAddress: string, blockHash: string): Promise<string | undefined> {
    const block = await this.getBlockProgress(blockHash);
    assert(block);

    return this._baseIndexer.createCheckpoint(this, contractAddress, block);
  }

  // Method to be used by fill-state CLI.
  async createInit (blockHash: string, blockNumber: number): Promise<void> {
    // Create initial state for contracts.
    await this._baseIndexer.createInit(this, blockHash, blockNumber);
  }

  async saveOrUpdateState (state: State): Promise<State> {
    return this._baseIndexer.saveOrUpdateState(state);
  }

  async removeStates (blockNumber: number, kind: StateKind): Promise<void> {
    await this._baseIndexer.removeStates(blockNumber, kind);
  }

  async getSubgraphEntity<Entity extends ObjectLiteral> (
    entity: new () => Entity,
    id: string,
    block: BlockHeight,
    queryInfo: GraphQLResolveInfo
  ): Promise<any> {
    const data = await this._graphWatcher.getEntity(entity, id, this._relationsMap, block, queryInfo);

    return data;
  }

  async getSubgraphEntities<Entity extends ObjectLiteral> (
    entity: new () => Entity,
    block: BlockHeight,
    where: { [key: string]: any } = {},
    queryOptions: QueryOptions = {},
    queryInfo: GraphQLResolveInfo
  ): Promise<any[]> {
    return this._graphWatcher.getEntities(entity, this._relationsMap, block, where, queryOptions, queryInfo);
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
    const resultEvent = this.getResultEvent(event);

    console.time('time:indexer#processEvent-mapping_code');
    // Call subgraph handler for event.
    await this._graphWatcher.handleEvent(resultEvent, extraData);
    console.timeEnd('time:indexer#processEvent-mapping_code');

    // Call custom hook function for indexing on event.
    await handleEvent(this, resultEvent);
  }

  async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
    // Trigger indexing of data based on the event.
    await this.triggerIndexingOnEvent(event, extraData);
  }

  async processBlock (blockProgress: BlockProgress): Promise<void> {
    console.time('time:indexer#processBlock-init_state');
    // Call a function to create initial state for contracts.
    await this._baseIndexer.createInit(this, blockProgress.blockHash, blockProgress.blockNumber);
    console.timeEnd('time:indexer#processBlock-init_state');

    this._graphWatcher.updateEntityCacheFrothyBlocks(blockProgress);
  }

  async processBlockAfterEvents (blockHash: string, blockNumber: number, extraData: ExtraEventData): Promise<void> {
    console.time('time:indexer#processBlockAfterEvents-mapping_code');
    // Call subgraph handler for block.
    await this._graphWatcher.handleBlock(blockHash, blockNumber, extraData);
    console.timeEnd('time:indexer#processBlockAfterEvents-mapping_code');

    console.time('time:indexer#processBlockAfterEvents-dump_subgraph_state');
    // Persist subgraph state to the DB.
    await this.dumpSubgraphState(blockHash);
    console.timeEnd('time:indexer#processBlockAfterEvents-dump_subgraph_state');
  }

  parseEventNameAndArgs (kind: string, 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: {} };
      }

      throw err;
    }

    const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);

    return {
      eventParsed: true,
      eventDetails: {
        eventName,
        eventInfo,
        eventSignature
      }
    };
  }

  async getStateSyncStatus (): Promise<StateSyncStatus | undefined> {
    return this._db.getStateSyncStatus();
  }

  async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
    if (!this._serverConfig.enableState) {
      return;
    }

    const dbTx = await this._db.createTransactionRunner();
    let res;

    try {
      res = await this._db.updateStateSyncStatusIndexedBlock(dbTx, blockNumber, force);
      await dbTx.commitTransaction();
    } catch (error) {
      await dbTx.rollbackTransaction();
      throw error;
    } finally {
      await dbTx.release();
    }

    return res;
  }

  async updateStateSyncStatusCheckpointBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
    const dbTx = await this._db.createTransactionRunner();
    let res;

    try {
      res = await this._db.updateStateSyncStatusCheckpointBlock(dbTx, blockNumber, force);
      await dbTx.commitTransaction();
    } catch (error) {
      await dbTx.rollbackTransaction();
      throw error;
    } finally {
      await dbTx.release();
    }

    return res;
  }

  async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
    const syncStatus = await this.getSyncStatus();
    assert(syncStatus);

    if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
      return;
    }

    const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
    assert(latestCanonicalBlock);

    return latestCanonicalBlock;
  }

  async getLatestStateIndexedBlock (): Promise<BlockProgress> {
    return this._baseIndexer.getLatestStateIndexedBlock();
  }

  async addContracts (): Promise<void> {
    // Watching all the contracts in the subgraph.
    await this._graphWatcher.addContracts();
  }

  async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
    return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
  }

  updateStateStatusMap (address: string, stateStatus: StateStatus): void {
    this._baseIndexer.updateStateStatusMap(address, stateStatus);
  }

  cacheContract (contract: Contract): void {
    return this._baseIndexer.cacheContract(contract);
  }

  async saveEventEntity (dbEvent: Event): Promise<Event> {
    return this._baseIndexer.saveEventEntity(dbEvent);
  }

  async saveEvents (dbEvents: Event[]): Promise<void> {
    return this._baseIndexer.saveEvents(dbEvents);
  }

  async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
    return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
  }

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

  getWatchedContracts (): Contract[] {
    return this._baseIndexer.getWatchedContracts();
  }

  getContractsByKind (kind: string): Contract[] {
    return this._baseIndexer.getContractsByKind(kind);
  }

  async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> {
    return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
  }

  async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
    return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
  }

  async getSyncStatus (): Promise<SyncStatus | undefined> {
    return this._baseIndexer.getSyncStatus();
  }

  async getBlocks (blockFilter: { blockHash?: string, blockNumber?: number }): Promise<any> {
    return this._baseIndexer.getBlocks(blockFilter);
  }

  async updateSyncStatusIndexedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    return this._baseIndexer.updateSyncStatusIndexedBlock(blockHash, blockNumber, force);
  }

  async updateSyncStatusChainHead (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    return this._baseIndexer.updateSyncStatusChainHead(blockHash, blockNumber, force);
  }

  async updateSyncStatusCanonicalBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    const syncStatus = this._baseIndexer.updateSyncStatusCanonicalBlock(blockHash, blockNumber, force);
    await this.pruneFrothyEntities(blockNumber);

    return syncStatus;
  }

  async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
  }

  async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
    return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
  }

  async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
    return this._baseIndexer.updateSyncStatus(syncStatus);
  }

  async getEvent (id: string): Promise<Event | undefined> {
    return this._baseIndexer.getEvent(id);
  }

  async getBlockProgress (blockHash: string): Promise<BlockProgress | undefined> {
    return this._baseIndexer.getBlockProgress(blockHash);
  }

  async getBlockProgressEntities (where: FindConditions<BlockProgress>, options: FindManyOptions<BlockProgress>): Promise<BlockProgress[]> {
    return this._baseIndexer.getBlockProgressEntities(where, options);
  }

  async getBlocksAtHeight (height: number, isPruned: boolean): Promise<BlockProgress[]> {
    return this._baseIndexer.getBlocksAtHeight(height, isPruned);
  }

  async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
    blockProgress: BlockProgress,
    events: DeepPartial<Event>[],
    ethFullBlock: EthFullBlock;
    ethFullTransactions: EthFullTransaction[];
  }[]> {
    return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
  }

  async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
    return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
  }

  async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
    BlockProgress,
    DeepPartial<Event>[],
    EthFullTransaction[]
  ]> {
    return this._saveBlockAndFetchEvents(block);
  }

  async getBlockEvents (blockHash: string, where: Where, queryOptions: QueryOptions): Promise<Array<Event>> {
    return this._baseIndexer.getBlockEvents(blockHash, where, queryOptions);
  }

  async removeUnknownEvents (block: BlockProgress): Promise<void> {
    return this._baseIndexer.removeUnknownEvents(Event, block);
  }

  async markBlocksAsPruned (blocks: BlockProgress[]): Promise<void> {
    await this._baseIndexer.markBlocksAsPruned(blocks);

    await this._graphWatcher.pruneEntities(FrothyEntity, blocks, SUBGRAPH_ENTITIES);
  }

  async pruneFrothyEntities (blockNumber: number): Promise<void> {
    await this._graphWatcher.pruneFrothyEntities(FrothyEntity, blockNumber);
  }

  async resetLatestEntities (blockNumber: number): Promise<void> {
    await this._graphWatcher.resetLatestEntities(blockNumber);
  }

  async updateBlockProgress (block: BlockProgress, lastProcessedEventIndex: number): Promise<BlockProgress> {
    return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
  }

  async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
    return this._baseIndexer.getAncestorAtHeight(blockHash, height);
  }

  async resetWatcherToBlock (blockNumber: number): Promise<void> {
    const entities = [...ENTITIES, FrothyEntity];
    await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);

    await this.resetLatestEntities(blockNumber);
  }

  async clearProcessedBlockData (block: BlockProgress): Promise<void> {
    const entities = [...ENTITIES, FrothyEntity];
    await this._baseIndexer.clearProcessedBlockData(block, entities);

    await this.resetLatestEntities(block.blockNumber);
  }

  getEntityTypesMap (): Map<string, { [key: string]: string }> {
    return this._entityTypesMap;
  }

  getRelationsMap (): Map<any, { [key: string]: any }> {
    return this._relationsMap;
  }

  updateSubgraphState (contractAddress: string, data: any): void {
    return updateSubgraphState(this._subgraphStateMap, contractAddress, data);
  }

  async dumpSubgraphState (blockHash: string, isStateFinalized = false): Promise<void> {
    return dumpSubgraphState(this, this._subgraphStateMap, blockHash, isStateFinalized);
  }

  _populateEntityTypesMap (): void {
    this._entityTypesMap.set('Token', {
      id: 'Bytes',
      symbol: 'String',
      name: 'String',
      decimals: 'Int',
      tokenType: 'String',
      poolCount: 'BigInt',
      pools: 'Pool',
      totalSupply: 'BigInt',
      txCount: 'BigInt'
    });
    this._entityTypesMap.set('PoolFactory', {
      id: 'Bytes',
      poolType: 'String',
      poolCount: 'BigInt',
      pools: 'Pool',
      txCount: 'BigInt'
    });
    this._entityTypesMap.set('Pool', {
      id: 'Bytes',
      createdAtBlockNumber: 'BigInt',
      createdAtTimestamp: 'BigInt',
      collateralToken: 'Token',
      quoteToken: 'Token',
      poolSize: 'BigDecimal',
      t0debt: 'BigDecimal',
      inflator: 'BigDecimal',
      borrowRate: 'BigDecimal',
      lendRate: 'BigDecimal',
      borrowFeeRate: 'BigDecimal',
      depositFeeRate: 'BigDecimal',
      pledgedCollateral: 'BigDecimal',
      totalInterestEarned: 'BigDecimal',
      txCount: 'BigInt',
      poolType: 'String',
      loansCount: 'BigInt',
      maxBorrower: 'Bytes',
      quoteTokenFlashloaned: 'BigDecimal',
      collateralFlashloaned: 'BigDecimal',
      hpb: 'BigDecimal',
      hpbIndex: 'Int',
      htp: 'BigDecimal',
      htpIndex: 'Int',
      lup: 'BigDecimal',
      lupIndex: 'Int',
      reserves: 'BigDecimal',
      claimableReserves: 'BigDecimal',
      claimableReservesRemaining: 'BigDecimal',
      burnEpoch: 'BigInt',
      totalAjnaBurned: 'BigDecimal',
      reserveAuctions: 'ReserveAuction',
      minDebtAmount: 'BigDecimal',
      actualUtilization: 'BigDecimal',
      targetUtilization: 'BigDecimal',
      totalBondEscrowed: 'BigDecimal',
      liquidationAuctions: 'LiquidationAuction',
      quoteTokenBalance: 'BigDecimal',
      collateralBalance: 'BigDecimal',
      subsetHash: 'Bytes',
      tokenIdsPledged: 'BigInt',
      bucketTokenIds: 'BigInt',
      tokenIdsAllowed: 'BigInt'
    });
    this._entityTypesMap.set('Bucket', {
      id: 'Bytes',
      bucketIndex: 'Int',
      bucketPrice: 'BigDecimal',
      exchangeRate: 'BigDecimal',
      poolAddress: 'String',
      pool: 'Pool',
      collateral: 'BigDecimal',
      deposit: 'BigDecimal',
      lpb: 'BigDecimal',
      lends: 'Lend',
      positionLends: 'PositionLend'
    });
    this._entityTypesMap.set('Lend', {
      id: 'Bytes',
      bucket: 'Bucket',
      bucketIndex: 'Int',
      poolAddress: 'String',
      lender: 'Bytes',
      pool: 'Pool',
      lpb: 'BigDecimal',
      lpbValueInQuote: 'BigDecimal',
      depositTime: 'BigInt'
    });
    this._entityTypesMap.set('Loan', {
      id: 'Bytes',
      poolAddress: 'String',
      borrower: 'Bytes',
      pool: 'Pool',
      inLiquidation: 'Boolean',
      liquidationAuction: 'LiquidationAuction',
      collateralPledged: 'BigDecimal',
      thresholdPrice: 'BigDecimal',
      tokenIdsPledged: 'BigInt',
      t0debt: 'BigDecimal',
      t0Np: 'BigDecimal'
    });
    this._entityTypesMap.set('Account', {
      id: 'Bytes',
      pools: 'Pool',
      kicks: 'Kick',
      lends: 'Lend',
      loans: 'Loan',
      settles: 'Settle',
      takes: 'Take',
      reserveAuctions: 'ReserveAuction',
      proposalsCreated: 'Proposal',
      proposalsExecuted: 'Proposal',
      delegatedTo: 'Account',
      delegatedFrom: 'Account',
      rewardsClaimed: 'BigDecimal',
      distributionPeriodVotes: 'DistributionPeriodVote',
      tokensDelegated: 'BigDecimal',
      positions: 'Position',
      txCount: 'BigInt'
    });
    this._entityTypesMap.set('LiquidationAuction', {
      id: 'Bytes',
      pool: 'Pool',
      borrower: 'Bytes',
      lastTakePrice: 'BigDecimal',
      collateral: 'BigDecimal',
      collateralRemaining: 'BigDecimal',
      debt: 'BigDecimal',
      debtRemaining: 'BigDecimal',
      loan: 'Loan',
      kicker: 'Bytes',
      kick: 'Kick',
      kickTime: 'BigInt',
      takes: 'Take',
      bucketTakes: 'BucketTake',
      settles: 'Settle',
      settle: 'AuctionSettle',
      settleTime: 'BigInt',
      settled: 'Boolean',
      bondSize: 'BigDecimal',
      bondFactor: 'BigDecimal',
      neutralPrice: 'BigDecimal',
      referencePrice: 'BigDecimal',
      thresholdPrice: 'BigDecimal'
    });
    this._entityTypesMap.set('ReserveAuction', {
      id: 'Bytes',
      pool: 'Pool',
      claimableReservesRemaining: 'BigDecimal',
      lastTakePrice: 'BigDecimal',
      burnEpoch: 'BigInt',
      kick: 'ReserveAuctionKick',
      takes: 'ReserveAuctionTake',
      ajnaBurned: 'BigDecimal'
    });
    this._entityTypesMap.set('LPTransferorList', {
      id: 'Bytes',
      pool: 'Pool',
      lender: 'Bytes',
      transferors: 'Bytes'
    });
    this._entityTypesMap.set('LPAllowance', {
      id: 'Bytes',
      index: 'Int',
      amount: 'BigDecimal'
    });
    this._entityTypesMap.set('LPAllowanceList', {
      id: 'Bytes',
      pool: 'Pool',
      lender: 'Bytes',
      spender: 'Bytes',
      allowances: 'LPAllowance'
    });
    this._entityTypesMap.set('AddCollateral', {
      id: 'Bytes',
      pool: 'Pool',
      bucket: 'Bucket',
      actor: 'Bytes',
      index: 'Int',
      amount: 'BigDecimal',
      lpAwarded: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('AddQuoteToken', {
      id: 'Bytes',
      pool: 'Pool',
      bucket: 'Bucket',
      lender: 'Bytes',
      index: 'Int',
      amount: 'BigDecimal',
      lpAwarded: 'BigDecimal',
      lup: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('AuctionSettle', {
      id: 'Bytes',
      pool: 'Pool',
      loan: 'Loan',
      borrower: 'Bytes',
      collateral: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('BondWithdrawn', {
      id: 'Bytes',
      kicker: 'Bytes',
      reciever: 'Bytes',
      amount: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('BucketBankruptcy', {
      id: 'Bytes',
      bucket: 'Bucket',
      index: 'Int',
      pool: 'Pool',
      lpForfeited: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('BucketTake', {
      id: 'Bytes',
      borrower: 'Bytes',
      taker: 'Bytes',
      liquidationAuction: 'LiquidationAuction',
      loan: 'Loan',
      pool: 'Pool',
      index: 'Int',
      auctionPrice: 'BigDecimal',
      amount: 'BigDecimal',
      collateral: 'BigDecimal',
      bondChange: 'BigDecimal',
      isReward: 'Boolean',
      lpAwarded: 'BucketTakeLPAwarded',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('BucketTakeLPAwarded', {
      id: 'Bytes',
      taker: 'Bytes',
      pool: 'Pool',
      kicker: 'Bytes',
      lpAwardedTaker: 'BigDecimal',
      lpAwardedKicker: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('DrawDebt', {
      id: 'Bytes',
      pool: 'Pool',
      borrower: 'Bytes',
      amountBorrowed: 'BigDecimal',
      collateralPledged: 'BigDecimal',
      lup: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('Flashloan', {
      id: 'Bytes',
      pool: 'Pool',
      borrower: 'Bytes',
      amount: 'BigDecimal'
    });
    this._entityTypesMap.set('Kick', {
      id: 'Bytes',
      kicker: 'Bytes',
      pool: 'Pool',
      loan: 'Loan',
      locked: 'BigDecimal',
      claimable: 'BigDecimal',
      liquidationAuction: 'LiquidationAuction',
      borrower: 'Bytes',
      debt: 'BigDecimal',
      collateral: 'BigDecimal',
      bond: 'BigDecimal',
      startingPrice: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('LoanStamped', {
      id: 'Bytes',
      pool: 'Pool',
      borrower: 'Bytes',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('MoveQuoteToken', {
      id: 'Bytes',
      pool: 'Pool',
      lender: 'Bytes',
      from: 'Bucket',
      to: 'Bucket',
      amount: 'BigDecimal',
      lpRedeemedFrom: 'BigDecimal',
      lpAwardedTo: 'BigDecimal',
      lup: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('RemoveCollateral', {
      id: 'Bytes',
      pool: 'Pool',
      bucket: 'Bucket',
      claimer: 'Bytes',
      index: 'Int',
      amount: 'BigDecimal',
      lpRedeemed: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('RemoveQuoteToken', {
      id: 'Bytes',
      pool: 'Pool',
      bucket: 'Bucket',
      lender: 'Bytes',
      index: 'Int',
      amount: 'BigDecimal',
      lpRedeemed: 'BigDecimal',
      lup: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('RepayDebt', {
      id: 'Bytes',
      pool: 'Pool',
      borrower: 'Bytes',
      quoteRepaid: 'BigDecimal',
      collateralPulled: 'BigDecimal',
      lup: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('ReserveAuctionKick', {
      id: 'Bytes',
      kicker: 'Bytes',
      reserveAuction: 'ReserveAuction',
      pool: 'Pool',
      claimableReserves: 'BigDecimal',
      startingPrice: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('ReserveAuctionTake', {
      id: 'Bytes',
      taker: 'Bytes',
      reserveAuction: 'ReserveAuction',
      pool: 'Pool',
      claimableReservesRemaining: 'BigDecimal',
      auctionPrice: 'BigDecimal',
      quotePurchased: 'BigDecimal',
      ajnaBurned: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('ResetInterestRate', {
      id: 'Bytes',
      pool: 'Pool',
      oldBorrowRate: 'BigDecimal',
      newBorrowRate: 'BigDecimal',
      oldLendRate: 'BigDecimal',
      newLendRate: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('Settle', {
      id: 'Bytes',
      pool: 'Pool',
      liquidationAuction: 'LiquidationAuction',
      loan: 'Loan',
      borrower: 'Bytes',
      settledDebt: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('Take', {
      id: 'Bytes',
      taker: 'Bytes',
      pool: 'Pool',
      borrower: 'Bytes',
      liquidationAuction: 'LiquidationAuction',
      loan: 'Loan',
      auctionPrice: 'BigDecimal',
      amount: 'BigDecimal',
      collateral: 'BigDecimal',
      bondChange: 'BigDecimal',
      isReward: 'Boolean',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('TransferLP', {
      id: 'Bytes',
      owner: 'Bytes',
      newOwner: 'Bytes',
      indexes: 'Int',
      lp: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('UpdateInterestRate', {
      id: 'Bytes',
      pool: 'Pool',
      oldBorrowRate: 'BigDecimal',
      newBorrowRate: 'BigDecimal',
      oldLendRate: 'BigDecimal',
      newLendRate: 'BigDecimal',
      borrowFeeRate: 'BigDecimal',
      depositFeeRate: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('Approval', {
      id: 'Bytes',
      owner: 'Bytes',
      approved: 'Bytes',
      tokenId: 'BigInt',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('ApprovalForAll', {
      id: 'Bytes',
      owner: 'Bytes',
      operator: 'Bytes',
      approved: 'Boolean',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('MoveLiquidity', {
      id: 'Bytes',
      lender: 'Bytes',
      tokenId: 'BigInt',
      pool: 'Pool',
      fromIndex: 'Int',
      toIndex: 'Int',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('Transfer', {
      id: 'Bytes',
      token: 'Token',
      pool: 'Pool',
      from: 'Bytes',
      to: 'Bytes',
      tokenId: 'BigInt',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('PoolCreated', {
      id: 'Bytes',
      pool: 'Pool',
      poolType: 'String',
      factory: 'PoolFactory',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('AddCollateralNFT', {
      id: 'Bytes',
      pool: 'Pool',
      bucket: 'Bucket',
      actor: 'Bytes',
      index: 'BigInt',
      tokenIds: 'BigInt',
      lpAwarded: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('AuctionNFTSettle', {
      id: 'Bytes',
      pool: 'Pool',
      loan: 'Loan',
      borrower: 'Bytes',
      collateral: 'BigDecimal',
      lp: 'BigDecimal',
      index: 'Int',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('DrawDebtNFT', {
      id: 'Bytes',
      pool: 'Pool',
      borrower: 'Bytes',
      amountBorrowed: 'BigDecimal',
      tokenIdsPledged: 'BigInt',
      lup: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('MergeOrRemoveCollateralNFT', {
      id: 'Bytes',
      pool: 'Pool',
      actor: 'Bytes',
      collateralMerged: 'BigDecimal',
      toIndexLps: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('Position', {
      id: 'Bytes',
      tokenId: 'BigInt',
      indexes: 'PositionLend',
      owner: 'Bytes',
      pool: 'Pool',
      token: 'Token',
      tokenURI: 'String'
    });
    this._entityTypesMap.set('PositionLend', {
      id: 'Bytes',
      bucket: 'Bucket',
      bucketIndex: 'Int',
      depositTime: 'BigInt',
      lpb: 'BigDecimal',
      lpbValueInQuote: 'BigDecimal',
      tokenId: 'BigInt'
    });
    this._entityTypesMap.set('Burn', {
      id: 'Bytes',
      lender: 'Bytes',
      tokenId: 'BigInt',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('MemorializePosition', {
      id: 'Bytes',
      pool: 'Pool',
      lender: 'Bytes',
      tokenId: 'BigInt',
      indexes: 'Int',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('Mint', {
      id: 'Bytes',
      lender: 'Bytes',
      pool: 'Pool',
      tokenId: 'BigInt',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('RedeemPosition', {
      id: 'Bytes',
      pool: 'Pool',
      lender: 'Bytes',
      tokenId: 'BigInt',
      indexes: 'Int',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('GrantFund', {
      id: 'Bytes',
      treasury: 'BigDecimal',
      distributionPeriods: 'DistributionPeriod',
      totalDelegationRewardsClaimed: 'BigDecimal'
    });
    this._entityTypesMap.set('DistributionPeriod', {
      id: 'Bytes',
      distributionId: 'BigInt',
      startBlock: 'BigInt',
      endBlock: 'BigInt',
      topSlate: 'FundedSlate',
      slatesSubmitted: 'FundedSlate',
      fundsAvailable: 'BigDecimal',
      delegationRewardsClaimed: 'BigDecimal',
      fundingVotePowerUsed: 'BigDecimal',
      screeningVotesCast: 'BigDecimal',
      votes: 'DistributionPeriodVote',
      proposals: 'Proposal',
      totalTokensDistributed: 'BigDecimal'
    });
    this._entityTypesMap.set('Proposal', {
      id: 'Bytes',
      proposalId: 'BigInt',
      description: 'String',
      distribution: 'DistributionPeriod',
      executed: 'Boolean',
      screeningVotesReceived: 'BigDecimal',
      fundingVotesReceived: 'BigDecimal',
      fundingVotesNegative: 'BigDecimal',
      fundingVotesPositive: 'BigDecimal',
      totalTokensRequested: 'BigDecimal',
      params: 'ProposalParams'
    });
    this._entityTypesMap.set('ProposalParams', {
      id: 'Bytes',
      target: 'Bytes',
      value: 'BigInt',
      calldata: 'Bytes',
      recipient: 'Bytes',
      tokensRequested: 'BigDecimal'
    });
    this._entityTypesMap.set('DistributionPeriodVote', {
      id: 'Bytes',
      voter: 'Account',
      distribution: 'DistributionPeriod',
      initialFundingStageVotingPowerRecordedPostVote: 'BigDecimal',
      remainingFundingStageVotingPowerRecordedPostVote: 'BigDecimal',
      initialScreeningStageVotingPowerRecordedPostVote: 'BigDecimal',
      remainingScreeningStageVotingPowerRecordedPostVote: 'BigDecimal',
      screeningVotes: 'ScreeningVote',
      fundingVotes: 'FundingVote'
    });
    this._entityTypesMap.set('ScreeningVote', {
      id: 'Bytes',
      distribution: 'DistributionPeriod',
      voter: 'Account',
      proposal: 'Proposal',
      totalVotesCast: 'BigDecimal',
      votesCast: 'VoteCast'
    });
    this._entityTypesMap.set('FundingVote', {
      id: 'Bytes',
      distribution: 'DistributionPeriod',
      voter: 'Account',
      proposal: 'Proposal',
      totalVotesCast: 'BigDecimal',
      votingPowerUsed: 'BigDecimal',
      votesCast: 'VoteCast'
    });
    this._entityTypesMap.set('FundedSlate', {
      id: 'Bytes',
      distribution: 'DistributionPeriod',
      proposals: 'Proposal',
      totalTokensRequested: 'BigDecimal',
      totalFundingVotesReceived: 'BigDecimal',
      updateBlock: 'BigInt'
    });
    this._entityTypesMap.set('DelegateRewardClaimed', {
      id: 'Bytes',
      delegateeAddress_: 'Bytes',
      distribution: 'DistributionPeriod',
      rewardClaimed_: 'BigInt',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('FundTreasury', {
      id: 'Bytes',
      amount: 'BigInt',
      treasuryBalance: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('FundedSlateUpdated', {
      id: 'Bytes',
      distributionId_: 'BigInt',
      fundedSlateHash_: 'Bytes',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('ProposalCreated', {
      id: 'Bytes',
      proposal: 'Proposal',
      proposer: 'Bytes',
      targets: 'Bytes',
      values: 'BigDecimal',
      signatures: 'String',
      calldatas: 'Bytes',
      startBlock: 'BigInt',
      endBlock: 'BigInt',
      description: 'String',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('ProposalExecuted', {
      id: 'Bytes',
      proposalId: 'BigInt',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('DistributionPeriodStarted', {
      id: 'Bytes',
      distribution: 'DistributionPeriod',
      startBlock: 'BigInt',
      endBlock: 'BigInt',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('VoteCast', {
      id: 'Bytes',
      voter: 'Bytes',
      proposalId: 'BigInt',
      support: 'Int',
      weight: 'BigDecimal',
      reason: 'String',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('DelegateChanged', {
      id: 'Bytes',
      delegator: 'Bytes',
      fromDelegate: 'Bytes',
      toDelegate: 'Bytes',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('DelegateVotesChanged', {
      id: 'Bytes',
      delegate: 'Bytes',
      previousBalance: 'BigDecimal',
      newBalance: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
    this._entityTypesMap.set('BurnWrap', {
      id: 'Bytes',
      wrapper: 'Bytes',
      account: 'Account',
      amount: 'BigDecimal',
      blockNumber: 'BigInt',
      blockTimestamp: 'BigInt',
      transactionHash: 'Bytes'
    });
  }

  _populateRelationsMap (): void {
    this._relationsMap.set(Token, {
      pools: {
        entity: Pool,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(PoolFactory, {
      pools: {
        entity: Pool,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(Pool, {
      collateralToken: {
        entity: Token,
        isArray: false,
        isDerived: false
      },
      quoteToken: {
        entity: Token,
        isArray: false,
        isDerived: false
      },
      reserveAuctions: {
        entity: ReserveAuction,
        isArray: true,
        isDerived: false
      },
      liquidationAuctions: {
        entity: LiquidationAuction,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(Bucket, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      lends: {
        entity: Lend,
        isArray: true,
        isDerived: false
      },
      positionLends: {
        entity: PositionLend,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(Lend, {
      bucket: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      },
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(Loan, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      liquidationAuction: {
        entity: LiquidationAuction,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(Account, {
      pools: {
        entity: Pool,
        isArray: true,
        isDerived: false
      },
      kicks: {
        entity: Kick,
        isArray: true,
        isDerived: false
      },
      lends: {
        entity: Lend,
        isArray: true,
        isDerived: false
      },
      loans: {
        entity: Loan,
        isArray: true,
        isDerived: false
      },
      settles: {
        entity: Settle,
        isArray: true,
        isDerived: false
      },
      takes: {
        entity: Take,
        isArray: true,
        isDerived: false
      },
      reserveAuctions: {
        entity: ReserveAuction,
        isArray: true,
        isDerived: false
      },
      proposalsCreated: {
        entity: Proposal,
        isArray: true,
        isDerived: false
      },
      proposalsExecuted: {
        entity: Proposal,
        isArray: true,
        isDerived: false
      },
      delegatedTo: {
        entity: Account,
        isArray: false,
        isDerived: false
      },
      delegatedFrom: {
        entity: Account,
        isArray: true,
        isDerived: false
      },
      distributionPeriodVotes: {
        entity: DistributionPeriodVote,
        isArray: true,
        isDerived: false
      },
      positions: {
        entity: Position,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(LiquidationAuction, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      loan: {
        entity: Loan,
        isArray: false,
        isDerived: false
      },
      kick: {
        entity: Kick,
        isArray: false,
        isDerived: false
      },
      takes: {
        entity: Take,
        isArray: true,
        isDerived: false
      },
      bucketTakes: {
        entity: BucketTake,
        isArray: true,
        isDerived: false
      },
      settles: {
        entity: Settle,
        isArray: true,
        isDerived: false
      },
      settle: {
        entity: AuctionSettle,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(ReserveAuction, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      kick: {
        entity: ReserveAuctionKick,
        isArray: false,
        isDerived: false
      },
      takes: {
        entity: ReserveAuctionTake,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(LPTransferorList, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(LPAllowanceList, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      allowances: {
        entity: LPAllowance,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(AddCollateral, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      bucket: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(AddQuoteToken, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      bucket: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(AuctionSettle, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      loan: {
        entity: Loan,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(BucketBankruptcy, {
      bucket: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      },
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(BucketTake, {
      liquidationAuction: {
        entity: LiquidationAuction,
        isArray: false,
        isDerived: false
      },
      loan: {
        entity: Loan,
        isArray: false,
        isDerived: false
      },
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      lpAwarded: {
        entity: BucketTakeLPAwarded,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(BucketTakeLPAwarded, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(DrawDebt, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(Flashloan, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(Kick, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      loan: {
        entity: Loan,
        isArray: false,
        isDerived: false
      },
      liquidationAuction: {
        entity: LiquidationAuction,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(LoanStamped, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(MoveQuoteToken, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      from: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      },
      to: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(RemoveCollateral, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      bucket: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(RemoveQuoteToken, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      bucket: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(RepayDebt, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(ReserveAuctionKick, {
      reserveAuction: {
        entity: ReserveAuction,
        isArray: false,
        isDerived: false
      },
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(ReserveAuctionTake, {
      reserveAuction: {
        entity: ReserveAuction,
        isArray: false,
        isDerived: false
      },
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(ResetInterestRate, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(Settle, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      liquidationAuction: {
        entity: LiquidationAuction,
        isArray: false,
        isDerived: false
      },
      loan: {
        entity: Loan,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(Take, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      liquidationAuction: {
        entity: LiquidationAuction,
        isArray: false,
        isDerived: false
      },
      loan: {
        entity: Loan,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(UpdateInterestRate, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(MoveLiquidity, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(Transfer, {
      token: {
        entity: Token,
        isArray: false,
        isDerived: false
      },
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(PoolCreated, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      factory: {
        entity: PoolFactory,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(AddCollateralNFT, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      bucket: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(AuctionNFTSettle, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      loan: {
        entity: Loan,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(DrawDebtNFT, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(MergeOrRemoveCollateralNFT, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(Position, {
      indexes: {
        entity: PositionLend,
        isArray: true,
        isDerived: false
      },
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      },
      token: {
        entity: Token,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(PositionLend, {
      bucket: {
        entity: Bucket,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(MemorializePosition, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(Mint, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(RedeemPosition, {
      pool: {
        entity: Pool,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(GrantFund, {
      distributionPeriods: {
        entity: DistributionPeriod,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(DistributionPeriod, {
      topSlate: {
        entity: FundedSlate,
        isArray: false,
        isDerived: false
      },
      slatesSubmitted: {
        entity: FundedSlate,
        isArray: true,
        isDerived: false
      },
      votes: {
        entity: DistributionPeriodVote,
        isArray: true,
        isDerived: false
      },
      proposals: {
        entity: Proposal,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(Proposal, {
      distribution: {
        entity: DistributionPeriod,
        isArray: false,
        isDerived: false
      },
      params: {
        entity: ProposalParams,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(DistributionPeriodVote, {
      voter: {
        entity: Account,
        isArray: false,
        isDerived: false
      },
      distribution: {
        entity: DistributionPeriod,
        isArray: false,
        isDerived: false
      },
      screeningVotes: {
        entity: ScreeningVote,
        isArray: true,
        isDerived: false
      },
      fundingVotes: {
        entity: FundingVote,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(ScreeningVote, {
      distribution: {
        entity: DistributionPeriod,
        isArray: false,
        isDerived: false
      },
      voter: {
        entity: Account,
        isArray: false,
        isDerived: false
      },
      proposal: {
        entity: Proposal,
        isArray: false,
        isDerived: false
      },
      votesCast: {
        entity: VoteCast,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(FundingVote, {
      distribution: {
        entity: DistributionPeriod,
        isArray: false,
        isDerived: false
      },
      voter: {
        entity: Account,
        isArray: false,
        isDerived: false
      },
      proposal: {
        entity: Proposal,
        isArray: false,
        isDerived: false
      },
      votesCast: {
        entity: VoteCast,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(FundedSlate, {
      distribution: {
        entity: DistributionPeriod,
        isArray: false,
        isDerived: false
      },
      proposals: {
        entity: Proposal,
        isArray: true,
        isDerived: false
      }
    });
    this._relationsMap.set(DelegateRewardClaimed, {
      distribution: {
        entity: DistributionPeriod,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(ProposalCreated, {
      proposal: {
        entity: Proposal,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(DistributionPeriodStarted, {
      distribution: {
        entity: DistributionPeriod,
        isArray: false,
        isDerived: false
      }
    });
    this._relationsMap.set(BurnWrap, {
      account: {
        entity: Account,
        isArray: false,
        isDerived: false
      }
    });
  }

  async _saveBlockAndFetchEvents ({
    cid: blockCid,
    blockHash,
    blockNumber,
    blockTimestamp,
    parentHash
  }: DeepPartial<BlockProgress>): Promise<[
    BlockProgress,
    DeepPartial<Event>[],
    EthFullTransaction[]
  ]> {
    assert(blockHash);
    assert(blockNumber);

    const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));

    const dbTx = await this._db.createTransactionRunner();
    try {
      const block = {
        cid: blockCid,
        blockHash,
        blockNumber,
        blockTimestamp,
        parentHash
      };

      console.time(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
      const blockProgress = await this._db.saveBlockWithEvents(dbTx, block, dbEvents);
      await dbTx.commitTransaction();
      console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);

      return [blockProgress, [], transactions];
    } catch (error) {
      await dbTx.rollbackTransaction();
      throw error;
    } finally {
      await dbTx.release();
    }
  }

  async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
    return this._baseIndexer.getFullTransactions(txHashList);
  }
}
