// // Copyright 2021 Vulcanize, Inc. // import assert from 'assert'; import { Connection, ConnectionOptions, DeepPartial, FindConditions, QueryRunner, FindManyOptions, MoreThan } from 'typeorm'; import path from 'path'; import { Database as BaseDatabase, QueryOptions, Where, MAX_REORG_DEPTH, DatabaseInterface } from '@vulcanize/util'; import { Contract } from './entity/Contract'; import { Event } from './entity/Event'; import { SyncStatus } from './entity/SyncStatus'; import { HookStatus } from './entity/HookStatus'; import { BlockProgress } from './entity/BlockProgress'; import { IPLDBlock } from './entity/IPLDBlock'; {{#each queries as | query |}} import { {{query.entityName}} } from './entity/{{query.entityName}}'; {{/each}} export class Database implements DatabaseInterface { _config: ConnectionOptions; _conn!: Connection; _baseDatabase: BaseDatabase; _propColMaps: { [key: string]: Map; } constructor (config: ConnectionOptions) { assert(config); this._config = { ...config, entities: [path.join(__dirname, 'entity/*')] }; this._baseDatabase = new BaseDatabase(this._config); this._propColMaps = {}; } async init (): Promise { this._conn = await this._baseDatabase.init(); this._setPropColMaps(); } async close (): Promise { return this._baseDatabase.close(); } {{#each queries as | query |}} {{#if (reservedNameCheck query.entityName) }} // eslint-disable-next-line @typescript-eslint/ban-types {{/if}} async {{query.getQueryName}} ({ blockHash, contractAddress {{~#each query.params}}, {{this.name~}} {{/each}} }: { blockHash: string, contractAddress: string {{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}} }): Promise<{{query.entityName}} | undefined> { return this._conn.getRepository({{query.entityName}}) .findOne({ blockHash, contractAddress{{#if query.params.length}},{{/if}} {{#each query.params}} {{this.name}}{{#unless @last}},{{/unless}} {{/each}} }); } {{/each}} {{~#each queries as | query |}} {{#if (reservedNameCheck query.entityName) }} // eslint-disable-next-line @typescript-eslint/ban-types {{/if}} async {{query.saveQueryName}} ({ blockHash, blockNumber, contractAddress {{~#each query.params}}, {{this.name~}} {{/each}}, value, proof }: DeepPartial<{{query.entityName}}>): Promise<{{query.entityName}}> { const repo = this._conn.getRepository({{query.entityName}}); const entity = repo.create({ blockHash, blockNumber, contractAddress {{~#each query.params}}, {{this.name~}} {{/each}}, value, proof }); return repo.save(entity); } {{/each}} async getIPLDBlocks (where: FindConditions): Promise { const repo = this._conn.getRepository(IPLDBlock); return repo.find({ where, relations: ['block'] }); } async getLatestIPLDBlock (contractAddress: string, kind: string | null, blockNumber?: number): Promise { const repo = this._conn.getRepository(IPLDBlock); let queryBuilder = repo.createQueryBuilder('ipld_block') .leftJoinAndSelect('ipld_block.block', 'block') .where('block.is_pruned = false') .andWhere('ipld_block.contract_address = :contractAddress', { contractAddress }) .orderBy('block.block_number', 'DESC'); // Filter out blocks after the provided block number. if (blockNumber) { queryBuilder.andWhere('block.block_number <= :blockNumber', { blockNumber }); } // Filter using kind if specified else order by id to give preference to checkpoint. queryBuilder = kind ? queryBuilder.andWhere('ipld_block.kind = :kind', { kind }) : queryBuilder.andWhere('ipld_block.kind != :kind', { kind: 'diff_staged' }) .addOrderBy('ipld_block.id', 'DESC'); return queryBuilder.getOne(); } async getPrevIPLDBlock (queryRunner: QueryRunner, blockHash: string, contractAddress: string, kind?: string): Promise { const heirerchicalQuery = ` WITH RECURSIVE cte_query AS ( SELECT b.block_hash, b.block_number, b.parent_hash, 1 as depth, i.id, i.kind FROM block_progress b LEFT JOIN ipld_block i ON i.block_id = b.id AND i.contract_address = $2 WHERE b.block_hash = $1 UNION ALL SELECT b.block_hash, b.block_number, b.parent_hash, c.depth + 1, i.id, i.kind FROM block_progress b LEFT JOIN ipld_block i ON i.block_id = b.id AND i.contract_address = $2 INNER JOIN cte_query c ON c.parent_hash = b.block_hash WHERE c.depth < $3 ) SELECT block_number, id, kind FROM cte_query ORDER BY block_number DESC, id DESC `; // Fetching block and id for previous IPLDBlock in frothy region. const queryResult = await queryRunner.query(heirerchicalQuery, [blockHash, contractAddress, MAX_REORG_DEPTH]); const latestRequiredResult = kind ? queryResult.find((obj: any) => obj.kind === kind) : queryResult.find((obj: any) => obj.id); let result: IPLDBlock | undefined; if (latestRequiredResult) { result = await queryRunner.manager.findOne(IPLDBlock, { id: latestRequiredResult.id }, { relations: ['block'] }); } else { // If IPLDBlock not found in frothy region get latest IPLDBlock in the pruned region. // Filter out IPLDBlocks from pruned blocks. const canonicalBlockNumber = queryResult.pop().block_number + 1; let queryBuilder = queryRunner.manager.createQueryBuilder(IPLDBlock, 'ipld_block') .leftJoinAndSelect('ipld_block.block', 'block') .where('block.is_pruned = false') .andWhere('ipld_block.contract_address = :contractAddress', { contractAddress }) .andWhere('block.block_number <= :canonicalBlockNumber', { canonicalBlockNumber }) .orderBy('block.block_number', 'DESC'); // Filter using kind if specified else order by id to give preference to checkpoint. queryBuilder = kind ? queryBuilder.andWhere('ipld_block.kind = :kind', { kind }) : queryBuilder.addOrderBy('ipld_block.id', 'DESC'); result = await queryBuilder.getOne(); } return result; } // Fetch all diff IPLDBlocks after the specified checkpoint. async getDiffIPLDBlocksByCheckpoint (contractAddress: string, checkpointBlockNumber: number): Promise { const repo = this._conn.getRepository(IPLDBlock); return repo.find({ relations: ['block'], where: { contractAddress, kind: 'diff', block: { isPruned: false, blockNumber: MoreThan(checkpointBlockNumber) } }, order: { block: 'ASC' } }); } async saveOrUpdateIPLDBlock (ipldBlock: IPLDBlock): Promise { const repo = this._conn.getRepository(IPLDBlock); return repo.save(ipldBlock); } async getHookStatus (queryRunner: QueryRunner): Promise { const repo = queryRunner.manager.getRepository(HookStatus); return repo.findOne(); } async updateHookStatusProcessedBlock (queryRunner: QueryRunner, blockNumber: number, force?: boolean): Promise { const repo = queryRunner.manager.getRepository(HookStatus); let entity = await repo.findOne(); if (!entity) { entity = repo.create({ latestProcessedBlockNumber: blockNumber }); } if (force || blockNumber > entity.latestProcessedBlockNumber) { entity.latestProcessedBlockNumber = blockNumber; } return repo.save(entity); } async getContracts (): Promise { const repo = this._conn.getRepository(Contract); return this._baseDatabase.getContracts(repo); } async getContract (address: string): Promise { const repo = this._conn.getRepository(Contract); return this._baseDatabase.getContract(repo, address); } async createTransactionRunner (): Promise { return this._baseDatabase.createTransactionRunner(); } async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> { const repo = this._conn.getRepository(BlockProgress); return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber); } async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise> { const repo = this._conn.getRepository(Event); return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber); } async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise { const repo = queryRunner.manager.getRepository(Event); return this._baseDatabase.saveEventEntity(repo, entity); } async getBlockEvents (blockHash: string, where: Where, queryOptions: QueryOptions): Promise { const repo = this._conn.getRepository(Event); return this._baseDatabase.getBlockEvents(repo, blockHash, where, queryOptions); } async saveEvents (queryRunner: QueryRunner, block: DeepPartial, events: DeepPartial[]): Promise { const blockRepo = queryRunner.manager.getRepository(BlockProgress); const eventRepo = queryRunner.manager.getRepository(Event); return this._baseDatabase.saveEvents(blockRepo, eventRepo, block, events); } async saveContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise { const repo = queryRunner.manager.getRepository(Contract); return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock); } async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise { const repo = queryRunner.manager.getRepository(SyncStatus); return this._baseDatabase.updateSyncStatusIndexedBlock(repo, blockHash, blockNumber, force); } async updateSyncStatusCanonicalBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise { const repo = queryRunner.manager.getRepository(SyncStatus); return this._baseDatabase.updateSyncStatusCanonicalBlock(repo, blockHash, blockNumber, force); } async updateSyncStatusChainHead (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise { const repo = queryRunner.manager.getRepository(SyncStatus); return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force); } async getSyncStatus (queryRunner: QueryRunner): Promise { const repo = queryRunner.manager.getRepository(SyncStatus); return this._baseDatabase.getSyncStatus(repo); } async getEvent (id: string): Promise { const repo = this._conn.getRepository(Event); return this._baseDatabase.getEvent(repo, id); } async getBlocksAtHeight (height: number, isPruned: boolean): Promise { const repo = this._conn.getRepository(BlockProgress); return this._baseDatabase.getBlocksAtHeight(repo, height, isPruned); } async markBlocksAsPruned (queryRunner: QueryRunner, blocks: BlockProgress[]): Promise { const repo = queryRunner.manager.getRepository(BlockProgress); return this._baseDatabase.markBlocksAsPruned(repo, blocks); } async getBlockProgress (blockHash: string): Promise { const repo = this._conn.getRepository(BlockProgress); return this._baseDatabase.getBlockProgress(repo, blockHash); } async getBlockProgressEntities (where: FindConditions, options: FindManyOptions): Promise { const repo = this._conn.getRepository(BlockProgress); return this._baseDatabase.getBlockProgressEntities(repo, where, options); } async updateBlockProgress (queryRunner: QueryRunner, block: BlockProgress, lastProcessedEventIndex: number): Promise { const repo = queryRunner.manager.getRepository(BlockProgress); return this._baseDatabase.updateBlockProgress(repo, block, lastProcessedEventIndex); } async removeEntities (queryRunner: QueryRunner, entity: new () => Entity, findConditions?: FindManyOptions | FindConditions): Promise { return this._baseDatabase.removeEntities(queryRunner, entity, findConditions); } async getAncestorAtDepth (blockHash: string, depth: number): Promise { return this._baseDatabase.getAncestorAtDepth(blockHash, depth); } _getPropertyColumnMapForEntity (entityName: string): Map { return this._conn.getMetadata(entityName).ownColumns.reduce((acc, curr) => { return acc.set(curr.propertyName, curr.databaseName); }, new Map()); } _setPropColMaps (): void { {{#each queries as | query |}} this._propColMaps.{{query.entityName}} = this._getPropertyColumnMapForEntity('{{query.entityName}}'); {{/each}} } }