From 2b4c811da2741f08990c9c7dc8878a94ab1bec8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Tkaczy=C5=84ski?= Date: Tue, 26 Nov 2024 09:57:22 +0100 Subject: [PATCH] feat: add indexer to metadata (#325) With support for multiple indexers in single Checkpoint instance we have to accomodate for those in our database. This commit makes necessary changes in our metadata store. Currently checkpoint still supports single indexer, but now metadata can accomodate it. --- src/checkpoint.ts | 67 ++++--- src/stores/checkpoints.ts | 139 +++++++------ .../__snapshots__/checkpoints.test.ts.snap | 156 +++++++++++++-- test/unit/stores/checkpoints.test.ts | 184 +++++++++++++++++- 4 files changed, 442 insertions(+), 104 deletions(-) diff --git a/src/checkpoint.ts b/src/checkpoint.ts index ae60c94..d69d931 100644 --- a/src/checkpoint.ts +++ b/src/checkpoint.ts @@ -17,6 +17,7 @@ import { sleep } from './utils/helpers'; import { ContractSourceConfig, CheckpointConfig, CheckpointOptions, TemplateSource } from './types'; import { getTableName } from './utils/database'; +const INDEXER_NAME = 'default'; const SCHEMA_VERSION = 1; const BLOCK_PRELOAD_START_RANGE = 1000; @@ -162,7 +163,7 @@ export default class Checkpoint { await this.validateStore(); await this.indexer.getProvider().init(); - const templateSources = await this.store.getTemplateSources(); + const templateSources = await this.store.getTemplateSources(INDEXER_NAME); await Promise.all( templateSources.map(source => this.executeTemplate( @@ -200,9 +201,9 @@ export default class Checkpoint { this.log.debug('reset'); await this.store.createStore(); - await this.store.setMetadata(MetadataId.LastIndexedBlock, 0); - await this.store.setMetadata(MetadataId.SchemaVersion, SCHEMA_VERSION); - await this.store.removeBlocks(); + await this.store.setMetadata(INDEXER_NAME, MetadataId.LastIndexedBlock, 0); + await this.store.setMetadata(INDEXER_NAME, MetadataId.SchemaVersion, SCHEMA_VERSION); + await this.store.removeBlocks(INDEXER_NAME); await this.entityController.createEntityStores(this.knex); } @@ -216,7 +217,7 @@ export default class Checkpoint { */ public async resetMetadata() { await this.store.resetStore(); - await this.store.setMetadata(MetadataId.SchemaVersion, SCHEMA_VERSION); + await this.store.setMetadata(INDEXER_NAME, MetadataId.SchemaVersion, SCHEMA_VERSION); } private addSource(source: ContractSourceConfig) { @@ -249,7 +250,7 @@ export default class Checkpoint { this.activeTemplates.push({ template: name, contractAddress: contract, startBlock: start }); if (persist) { - await this.store.insertTemplateSource(contract, start, name); + await this.store.insertTemplateSource(INDEXER_NAME, contract, start, name); } this.addSource({ @@ -287,21 +288,21 @@ export default class Checkpoint { }); }); - await this.store.insertCheckpoints(checkpoints); + await this.store.insertCheckpoints(INDEXER_NAME, checkpoints); } public async setBlockHash(blockNum: number, hash: string) { this.blockHashCache = { blockNumber: blockNum, hash }; - return this.store.setBlockHash(blockNum, hash); + return this.store.setBlockHash(INDEXER_NAME, blockNum, hash); } public async setLastIndexedBlock(block: number) { - await this.store.setMetadata(MetadataId.LastIndexedBlock, block); + await this.store.setMetadata(INDEXER_NAME, MetadataId.LastIndexedBlock, block); } public async insertCheckpoints(checkpoints: CheckpointRecord[]) { - await this.store.insertCheckpoints(checkpoints); + await this.store.insertCheckpoints(INDEXER_NAME, checkpoints); } public async getWriterParams(): Promise<{ @@ -322,7 +323,8 @@ export default class Checkpoint { private async getStartBlockNum() { const start = this.getConfigStartBlock(); - const lastBlock = (await this.store.getMetadataNumber(MetadataId.LastIndexedBlock)) ?? 0; + const lastBlock = + (await this.store.getMetadataNumber(INDEXER_NAME, MetadataId.LastIndexedBlock)) ?? 0; const nextBlock = lastBlock + 1; @@ -426,7 +428,7 @@ export default class Checkpoint { let current = blockNumber - 1; let lastGoodBlock: null | number = null; while (lastGoodBlock === null) { - const storedBlockHash = await this.store.getBlockHash(current); + const storedBlockHash = await this.store.getBlockHash(INDEXER_NAME, current); const currentBlockHash = await this.indexer.getProvider().getBlockHash(current); if (storedBlockHash === null || storedBlockHash === currentBlockHash) { @@ -453,7 +455,7 @@ export default class Checkpoint { }); // TODO: when we have full transaction support, we should include this in the transaction - await this.store.removeFutureData(lastGoodBlock); + await this.store.removeFutureData(INDEXER_NAME, lastGoodBlock); this.cpBlocksCache = null; this.blockHashCache = null; @@ -469,6 +471,7 @@ export default class Checkpoint { } const checkpointBlocks = await this.store.getNextCheckpointBlocks( + INDEXER_NAME, blockNum, this.sourceContracts, 15 @@ -485,7 +488,7 @@ export default class Checkpoint { return this.blockHashCache.hash; } - return this.store.getBlockHash(blockNumber); + return this.store.getBlockHash(INDEXER_NAME, blockNumber); } private get store(): CheckpointsStore { @@ -542,10 +545,22 @@ export default class Checkpoint { const networkIdentifier = await this.indexer.getProvider().getNetworkIdentifier(); const configChecksum = getConfigChecksum(this.config); - const storedNetworkIdentifier = await this.store.getMetadata(MetadataId.NetworkIdentifier); - const storedStartBlock = await this.store.getMetadataNumber(MetadataId.StartBlock); - const storedConfigChecksum = await this.store.getMetadata(MetadataId.ConfigChecksum); - const storedSchemaVersion = await this.store.getMetadataNumber(MetadataId.SchemaVersion); + const storedNetworkIdentifier = await this.store.getMetadata( + INDEXER_NAME, + MetadataId.NetworkIdentifier + ); + const storedStartBlock = await this.store.getMetadataNumber( + INDEXER_NAME, + MetadataId.StartBlock + ); + const storedConfigChecksum = await this.store.getMetadata( + INDEXER_NAME, + MetadataId.ConfigChecksum + ); + const storedSchemaVersion = await this.store.getMetadataNumber( + INDEXER_NAME, + MetadataId.SchemaVersion + ); const hasNetworkChanged = storedNetworkIdentifier && storedNetworkIdentifier !== networkIdentifier; const hasStartBlockChanged = @@ -560,9 +575,9 @@ export default class Checkpoint { await this.resetMetadata(); await this.reset(); - await this.store.setMetadata(MetadataId.NetworkIdentifier, networkIdentifier); - await this.store.setMetadata(MetadataId.StartBlock, this.getConfigStartBlock()); - await this.store.setMetadata(MetadataId.ConfigChecksum, configChecksum); + await this.store.setMetadata(INDEXER_NAME, MetadataId.NetworkIdentifier, networkIdentifier); + await this.store.setMetadata(INDEXER_NAME, MetadataId.StartBlock, this.getConfigStartBlock()); + await this.store.setMetadata(INDEXER_NAME, MetadataId.ConfigChecksum, configChecksum); } else if (hasNetworkChanged) { this.log.error( `network identifier changed from ${storedNetworkIdentifier} to ${networkIdentifier}. @@ -597,15 +612,19 @@ export default class Checkpoint { throw new Error('schema changed'); } else { if (!storedNetworkIdentifier) { - await this.store.setMetadata(MetadataId.NetworkIdentifier, networkIdentifier); + await this.store.setMetadata(INDEXER_NAME, MetadataId.NetworkIdentifier, networkIdentifier); } if (!storedStartBlock) { - await this.store.setMetadata(MetadataId.StartBlock, this.getConfigStartBlock()); + await this.store.setMetadata( + INDEXER_NAME, + MetadataId.StartBlock, + this.getConfigStartBlock() + ); } if (!storedConfigChecksum) { - await this.store.setMetadata(MetadataId.ConfigChecksum, configChecksum); + await this.store.setMetadata(INDEXER_NAME, MetadataId.ConfigChecksum, configChecksum); } } } diff --git a/src/stores/checkpoints.ts b/src/stores/checkpoints.ts index 38b8af2..b8dff69 100644 --- a/src/stores/checkpoints.ts +++ b/src/stores/checkpoints.ts @@ -4,29 +4,32 @@ import { Logger } from '../utils/logger'; import { chunk } from '../utils/helpers'; import { TemplateSource } from '../types'; -const Table = { +export const Table = { Blocks: '_blocks', Checkpoints: '_checkpoints', Metadata: '_metadatas', // using plural names to conform with standards entities, TemplateSources: '_template_sources' }; -const Fields = { +export const Fields = { Blocks: { + Indexer: 'indexer', Number: 'block_number', Hash: 'hash' }, Checkpoints: { Id: 'id', + Indexer: 'indexer', BlockNumber: 'block_number', ContractAddress: 'contract_address' }, Metadata: { Id: 'id', + Indexer: 'indexer', Value: 'value' }, TemplateSources: { - Id: 'id', + Indexer: 'indexer', ContractAddress: 'contract_address', StartBlock: 'start_block', Template: 'template' @@ -98,29 +101,35 @@ export class CheckpointsStore { if (!hasBlocksTable) { builder = builder.createTable(Table.Blocks, t => { - t.bigint(Fields.Blocks.Number).primary(); - t.string(Fields.Blocks.Hash).notNullable().unique(); + t.string(Fields.Blocks.Indexer).notNullable(); + t.bigint(Fields.Blocks.Number); + t.string(Fields.Blocks.Hash).notNullable(); + t.primary([Fields.Blocks.Indexer, Fields.Blocks.Number]); }); } if (!hasCheckpointsTable) { builder = builder.createTable(Table.Checkpoints, t => { - t.string(Fields.Checkpoints.Id, CheckpointIdSize).primary(); + t.string(Fields.Checkpoints.Id, CheckpointIdSize); + t.string(Fields.Checkpoints.Indexer).notNullable(); t.bigint(Fields.Checkpoints.BlockNumber).notNullable().index(); t.string(Fields.Checkpoints.ContractAddress, 66).notNullable().index(); + t.primary([Fields.Checkpoints.Id, Fields.Checkpoints.Indexer]); }); } if (!hasMetadataTable) { builder = builder.createTable(Table.Metadata, t => { - t.string(Fields.Metadata.Id, 20).primary(); + t.string(Fields.Metadata.Id, 20); + t.string(Fields.Metadata.Indexer).notNullable(); t.string(Fields.Metadata.Value, 128).notNullable(); + t.primary([Fields.Metadata.Id, Fields.Metadata.Indexer]); }); } if (!hasTemplateSourcesTable) { builder = builder.createTable(Table.TemplateSources, t => { - t.increments(Fields.TemplateSources.Id); + t.string(Fields.TemplateSources.Indexer).notNullable(); t.string(Fields.TemplateSources.ContractAddress, 66); t.bigint(Fields.TemplateSources.StartBlock).notNullable(); t.string(Fields.TemplateSources.Template, 128).notNullable(); @@ -170,14 +179,41 @@ export class CheckpointsStore { await this.createStore(); } - public async removeBlocks(): Promise { - return this.knex(Table.Blocks).del(); + public async removeFutureData(indexer: string, blockNumber: number): Promise { + return this.knex.transaction(async trx => { + await trx + .table(Table.Metadata) + .insert({ + [Fields.Metadata.Id]: MetadataId.LastIndexedBlock, + [Fields.Metadata.Indexer]: indexer, + [Fields.Metadata.Value]: blockNumber + }) + .onConflict([Fields.Metadata.Id, Fields.Metadata.Indexer]) + .merge(); + + await trx + .table(Table.Checkpoints) + .where(Fields.Checkpoints.Indexer, indexer) + .where(Fields.Checkpoints.BlockNumber, '>', blockNumber) + .del(); + + await trx.table(Table.Blocks).where(Fields.Blocks.Number, '>', blockNumber).del(); + }); } - public async getBlockHash(blockNumber: number): Promise { + public async setBlockHash(indexer: string, blockNumber: number, hash: string): Promise { + await this.knex.table(Table.Blocks).insert({ + [Fields.Blocks.Indexer]: indexer, + [Fields.Blocks.Number]: blockNumber, + [Fields.Blocks.Hash]: hash + }); + } + + public async getBlockHash(indxer: string, blockNumber: number): Promise { const blocks = await this.knex .select(Fields.Blocks.Hash) .from(Table.Blocks) + .where(Fields.Blocks.Indexer, indxer) .where(Fields.Blocks.Number, blockNumber) .limit(1); @@ -188,18 +224,28 @@ export class CheckpointsStore { return blocks[0][Fields.Blocks.Hash]; } - public async setBlockHash(blockNumber: number, hash: string): Promise { - await this.knex.table(Table.Blocks).insert({ - [Fields.Blocks.Number]: blockNumber, - [Fields.Blocks.Hash]: hash - }); + public async removeBlocks(indexer: string): Promise { + return this.knex(Table.Blocks).where(Fields.Blocks.Indexer, indexer).del(); + } + + public async setMetadata(indexer: string, id: string, value: ToString): Promise { + await this.knex + .table(Table.Metadata) + .insert({ + [Fields.Metadata.Id]: id, + [Fields.Metadata.Indexer]: indexer, + [Fields.Metadata.Value]: value + }) + .onConflict([Fields.Metadata.Id, Fields.Metadata.Indexer]) + .merge(); } - public async getMetadata(id: string): Promise { + public async getMetadata(indexer: string, id: string): Promise { const value = await this.knex .select(Fields.Metadata.Value) .from(Table.Metadata) .where(Fields.Metadata.Id, id) + .where(Fields.Metadata.Indexer, indexer) .limit(1); if (value.length == 0) { @@ -209,33 +255,16 @@ export class CheckpointsStore { return value[0][Fields.Metadata.Value]; } - public async getMetadataNumber(id: string, base = 10): Promise { - const strValue = await this.getMetadata(id); - if (!strValue) { - return undefined; - } + public async getMetadataNumber(indexer: string, id: string, base = 10): Promise { + const strValue = await this.getMetadata(indexer, id); + if (strValue === null) return null; return parseInt(strValue, base); } - public async setMetadata(id: string, value: ToString): Promise { - await this.knex - .table(Table.Metadata) - .insert({ - [Fields.Metadata.Id]: id, - [Fields.Metadata.Value]: value - }) - .onConflict(Fields.Metadata.Id) - .merge(); - } - - public async insertCheckpoints(checkpoints: CheckpointRecord[]): Promise { + public async insertCheckpoints(indexer: string, checkpoints: CheckpointRecord[]): Promise { const insert = async (items: CheckpointRecord[]) => { try { - if (items.length === 0) { - return; - } - await this.knex .table(Table.Checkpoints) .insert( @@ -244,17 +273,18 @@ export class CheckpointsStore { return { [Fields.Checkpoints.Id]: id, + [Fields.Checkpoints.Indexer]: indexer, [Fields.Checkpoints.BlockNumber]: checkpoint.blockNumber, [Fields.Checkpoints.ContractAddress]: checkpoint.contractAddress }; }) ) - .onConflict(Fields.Checkpoints.Id) + .onConflict([Fields.Checkpoints.Id, Fields.Checkpoints.Indexer]) .ignore(); } catch (err: any) { if (['ER_LOCK_DEADLOCK', '40P01'].includes(err.code)) { this.log.debug('deadlock detected, retrying...'); - return this.insertCheckpoints(items); + return this.insertCheckpoints(indexer, items); } throw err; @@ -264,26 +294,6 @@ export class CheckpointsStore { await Promise.all(chunk(checkpoints, 1000).map(chunk => insert(chunk))); } - public async removeFutureData(blockNumber: number): Promise { - return this.knex.transaction(async trx => { - await trx - .table(Table.Metadata) - .insert({ - [Fields.Metadata.Id]: MetadataId.LastIndexedBlock, - [Fields.Metadata.Value]: blockNumber - }) - .onConflict(Fields.Metadata.Id) - .merge(); - - await trx - .table(Table.Checkpoints) - .where(Fields.Checkpoints.BlockNumber, '>', blockNumber) - .del(); - - await trx.table(Table.Blocks).where(Fields.Blocks.Number, '>', blockNumber).del(); - }); - } - /** * Fetch list of checkpoint blocks greater than or equal to the * block number arguments, that have some events related to the @@ -293,6 +303,7 @@ export class CheckpointsStore { * can be modified by the limit command. */ public async getNextCheckpointBlocks( + indexer: string, block: number, contracts: string[], limit = 15 @@ -300,6 +311,7 @@ export class CheckpointsStore { const result = await this.knex .distinct(Fields.Checkpoints.BlockNumber) .from(Table.Checkpoints) + .where(Fields.Checkpoints.Indexer, indexer) .where(Fields.Checkpoints.BlockNumber, '>=', block) .whereIn(Fields.Checkpoints.ContractAddress, contracts) .orderBy(Fields.Checkpoints.BlockNumber, 'asc') @@ -311,25 +323,28 @@ export class CheckpointsStore { } public async insertTemplateSource( + indexer: string, contractAddress: string, startBlock: number, template: string ): Promise { return this.knex.table(Table.TemplateSources).insert({ + [Fields.TemplateSources.Indexer]: indexer, [Fields.TemplateSources.ContractAddress]: contractAddress, [Fields.TemplateSources.StartBlock]: startBlock, [Fields.TemplateSources.Template]: template }); } - public async getTemplateSources(): Promise { + public async getTemplateSources(indexer: string): Promise { const data = await this.knex .select( Fields.TemplateSources.ContractAddress, Fields.TemplateSources.StartBlock, Fields.TemplateSources.Template ) - .from(Table.TemplateSources); + .from(Table.TemplateSources) + .where(Fields.TemplateSources.Indexer, indexer); return data.map(row => ({ contractAddress: row[Fields.TemplateSources.ContractAddress], diff --git a/test/unit/stores/__snapshots__/checkpoints.test.ts.snap b/test/unit/stores/__snapshots__/checkpoints.test.ts.snap index c248dd3..fda4af0 100644 --- a/test/unit/stores/__snapshots__/checkpoints.test.ts.snap +++ b/test/unit/stores/__snapshots__/checkpoints.test.ts.snap @@ -1,26 +1,160 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`CheckpointsStore blocks should remove blocks 1`] = ` +[ + { + "block_number": 5000, + "hash": "0x0", + "indexer": "default", + }, + { + "block_number": 5001, + "hash": "0x1", + "indexer": "default", + }, +] +`; + +exports[`CheckpointsStore blocks should set block hash 1`] = ` +[ + { + "block_number": 5000, + "hash": "0x0", + "indexer": "default", + }, + { + "block_number": 5001, + "hash": "0x1", + "indexer": "default", + }, + { + "block_number": 5000, + "hash": "0xa", + "indexer": "OTHER", + }, +] +`; + +exports[`CheckpointsStore checkpoints should insert checkpoints 1`] = ` +[ + { + "block_number": 5000, + "contract_address": "0x01", + "id": "6f1246bdea", + "indexer": "default", + }, + { + "block_number": 9000, + "contract_address": "0x02", + "id": "92df4d22e3", + "indexer": "default", + }, + { + "block_number": 11000, + "contract_address": "0x01", + "id": "24a44a0b0b", + "indexer": "default", + }, +] +`; + exports[`CheckpointsStore createStore should execute correct query 1`] = ` -"create table \`_blocks\` (\`block_number\` bigint, \`hash\` varchar(255) not null, primary key (\`block_number\`)); -create unique index \`_blocks_hash_unique\` on \`_blocks\` (\`hash\`); -create table \`_checkpoints\` (\`id\` varchar(10), \`block_number\` bigint not null, \`contract_address\` varchar(66) not null, primary key (\`id\`)); +"create table \`_blocks\` (\`indexer\` varchar(255) not null, \`block_number\` bigint, \`hash\` varchar(255) not null, primary key (\`indexer\`, \`block_number\`)); +create table \`_checkpoints\` (\`id\` varchar(10), \`indexer\` varchar(255) not null, \`block_number\` bigint not null, \`contract_address\` varchar(66) not null, primary key (\`id\`, \`indexer\`)); create index \`_checkpoints_block_number_index\` on \`_checkpoints\` (\`block_number\`); create index \`_checkpoints_contract_address_index\` on \`_checkpoints\` (\`contract_address\`); -create table \`_metadatas\` (\`id\` varchar(20), \`value\` varchar(128) not null, primary key (\`id\`)); -create table \`_template_sources\` (\`id\` integer not null primary key autoincrement, \`contract_address\` varchar(66), \`start_block\` bigint not null, \`template\` varchar(128) not null)" +create table \`_metadatas\` (\`id\` varchar(20), \`indexer\` varchar(255) not null, \`value\` varchar(128) not null, primary key (\`id\`, \`indexer\`)); +create table \`_template_sources\` (\`indexer\` varchar(255) not null, \`contract_address\` varchar(66), \`start_block\` bigint not null, \`template\` varchar(128) not null)" +`; + +exports[`CheckpointsStore metadata should set metadata 1`] = ` +[ + { + "id": "key", + "indexer": "default", + "value": "default_value", + }, + { + "id": "number_key", + "indexer": "default", + "value": "1111", + }, + { + "id": "key", + "indexer": "OTHER", + "value": "other_value", + }, +] `; -exports[`CheckpointsStore insertCheckpoints should insert checkpoints 1`] = ` +exports[`CheckpointsStore removeFutureData should remove future data 1`] = ` [ { "block_number": 5000, - "contract_address": "0x0625dc1290b6e936be5f1a3e963cf629326b1f4dfd5a56738dea98e1ad31b7f3", - "id": "a739beda26", + "contract_address": "0x01", + "id": "6f1246bdea", + "indexer": "default", + }, + { + "block_number": 9000, + "contract_address": "0x02", + "id": "92df4d22e3", + "indexer": "default", + }, + { + "block_number": 11000, + "contract_address": "0x01", + "id": "24a44a0b0b", + "indexer": "OTHER", + }, +] +`; + +exports[`CheckpointsStore template sources should insert template sources 1`] = ` +[ + { + "contract_address": "0x01", + "indexer": "default", + "start_block": 1000, + "template": "Template1", + }, + { + "contract_address": "0x01", + "indexer": "default", + "start_block": 2000, + "template": "Template1", + }, + { + "contract_address": "0x02", + "indexer": "default", + "start_block": 2100, + "template": "Template3", + }, + { + "contract_address": "0x01", + "indexer": "OTHER", + "start_block": 50, + "template": "Template1", + }, +] +`; + +exports[`CheckpointsStore template sources should retrieve template sources 1`] = ` +[ + { + "contractAddress": "0x01", + "startBlock": 1000, + "template": "Template1", + }, + { + "contractAddress": "0x01", + "startBlock": 2000, + "template": "Template1", }, { - "block_number": 123222, - "contract_address": "0x0625dc1290b6e936be5f1a3e963cf629326b1f4dfd5a56738dea98e1ad31b7f3", - "id": "d2b8dcc2b5", + "contractAddress": "0x02", + "startBlock": 2100, + "template": "Template3", }, ] `; diff --git a/test/unit/stores/checkpoints.test.ts b/test/unit/stores/checkpoints.test.ts index cbef5f2..23f0c05 100644 --- a/test/unit/stores/checkpoints.test.ts +++ b/test/unit/stores/checkpoints.test.ts @@ -1,6 +1,6 @@ import knex from 'knex'; import { mockDeep } from 'jest-mock-extended'; -import { CheckpointsStore } from '../../../src/stores/checkpoints'; +import { CheckpointsStore, MetadataId, Table } from '../../../src/stores/checkpoints'; import { Logger } from '../../../src/utils/logger'; function createMockLogger() { @@ -10,6 +10,8 @@ function createMockLogger() { } describe('CheckpointsStore', () => { + const INDEXER = 'default'; + const mockKnex = knex({ client: 'sqlite3', connection: { @@ -33,22 +35,190 @@ describe('CheckpointsStore', () => { }); }); - describe('insertCheckpoints', () => { + describe('removeFutureData', () => { + afterAll(async () => { + await store.resetStore(); + }); + + it('should remove future data', async () => { + await store.setMetadata(INDEXER, MetadataId.LastIndexedBlock, 11001); + await store.setMetadata('OTHER', MetadataId.LastIndexedBlock, 11001); + + await store.insertCheckpoints(INDEXER, [ + { + contractAddress: '0x01', + blockNumber: 5000 + }, + { + contractAddress: '0x02', + blockNumber: 9000 + }, + { + contractAddress: '0x01', + blockNumber: 11000 + } + ]); + await store.insertCheckpoints('OTHER', [ + { + contractAddress: '0x01', + blockNumber: 11000 + } + ]); + + await store.removeFutureData(INDEXER, 10000); + + const defaultLastIndexedBlock = await store.getMetadataNumber( + INDEXER, + MetadataId.LastIndexedBlock + ); + expect(defaultLastIndexedBlock).toEqual(10000); + + const otherLastIndexedBlock = await store.getMetadataNumber( + 'OTHER', + MetadataId.LastIndexedBlock + ); + expect(otherLastIndexedBlock).toEqual(11001); + + const result = await mockKnex.select('*').from(Table.Checkpoints); + expect(result).toMatchSnapshot(); + }); + }); + + describe('blocks', () => { + afterAll(async () => { + await store.resetStore(); + }); + + it('should set block hash', async () => { + await store.setBlockHash(INDEXER, 5000, '0x0'); + await store.setBlockHash(INDEXER, 5001, '0x1'); + await store.setBlockHash('OTHER', 5000, '0xa'); + + const result = await mockKnex.select('*').from(Table.Blocks); + expect(result).toMatchSnapshot(); + }); + + it('should retrieve block hash', async () => { + const result = await store.getBlockHash(INDEXER, 5000); + expect(result).toEqual('0x0'); + }); + + it('should return null if retrieving non-existent block hash', async () => { + const result = await store.getBlockHash(INDEXER, 6000); + expect(result).toBeNull(); + }); + + it('should remove blocks', async () => { + await store.removeBlocks('OTHER'); + + const result = await mockKnex.select('*').from(Table.Blocks); + expect(result).toMatchSnapshot(); + }); + }); + + describe('metadata', () => { + afterAll(async () => { + await store.resetStore(); + }); + + it('should set metadata', async () => { + await store.setMetadata(INDEXER, 'key', 'default_value'); + await store.setMetadata(INDEXER, 'number_key', 1111); + await store.setMetadata('OTHER', 'key', 'other_value'); + + const result = await mockKnex.select('*').from(Table.Metadata); + expect(result).toMatchSnapshot(); + }); + + it('should retrieve metadata', async () => { + const result = await store.getMetadata(INDEXER, 'key'); + expect(result).toEqual('default_value'); + }); + + it('should retrieve metadata as number', async () => { + const result = await store.getMetadataNumber(INDEXER, 'number_key'); + expect(result).toEqual(1111); + }); + + it('should return null if retrieving non-existent metadata value', async () => { + const result = await store.getMetadata(INDEXER, 'non_existent_key'); + expect(result).toBeNull(); + }); + + it('should return null if retrieving non-existent metadata value as number', async () => { + const result = await store.getMetadataNumber(INDEXER, 'non_existent_key'); + expect(result).toBeNull(); + }); + + it('should update metadata', async () => { + await store.setMetadata(INDEXER, 'key', 'new_value'); + const result = await store.getMetadata(INDEXER, 'key'); + expect(result).toEqual('new_value'); + }); + }); + + describe('checkpoints', () => { + const CONTRACT_A = '0x01'; + const CONTRACT_B = '0x02'; + + afterAll(async () => { + await store.resetStore(); + }); + it('should insert checkpoints', async () => { const checkpoints = [ { - contractAddress: '0x0625dc1290b6e936be5f1a3e963cf629326b1f4dfd5a56738dea98e1ad31b7f3', + contractAddress: CONTRACT_A, blockNumber: 5000 }, { - contractAddress: '0x0625dc1290b6e936be5f1a3e963cf629326b1f4dfd5a56738dea98e1ad31b7f3', - blockNumber: 123222 + contractAddress: CONTRACT_B, + blockNumber: 9000 + }, + { + contractAddress: CONTRACT_A, + blockNumber: 11000 } ]; - await store.insertCheckpoints(checkpoints); + await store.insertCheckpoints(INDEXER, checkpoints); + + const result = await mockKnex.select('*').from(Table.Checkpoints); + expect(result).toMatchSnapshot(); + }); + + it('should return next checkpoint blocks', async () => { + let result = await store.getNextCheckpointBlocks(INDEXER, 4000, [CONTRACT_A, CONTRACT_B]); + expect(result).toEqual([5000, 9000, 11000]); + + result = await store.getNextCheckpointBlocks(INDEXER, 7000, [CONTRACT_A, CONTRACT_B]); + expect(result).toEqual([9000, 11000]); + + result = await store.getNextCheckpointBlocks(INDEXER, 4000, [CONTRACT_B]); + expect(result).toEqual([9000]); + }); + }); + + describe('template sources', () => { + const CONTRACT_A = '0x01'; + const CONTRACT_B = '0x02'; + + afterAll(async () => { + await store.resetStore(); + }); + + it('should insert template sources', async () => { + await store.insertTemplateSource(INDEXER, CONTRACT_A, 1000, 'Template1'); + await store.insertTemplateSource(INDEXER, CONTRACT_A, 2000, 'Template1'); + await store.insertTemplateSource(INDEXER, CONTRACT_B, 2100, 'Template3'); + await store.insertTemplateSource('OTHER', CONTRACT_A, 50, 'Template1'); + + const result = await mockKnex.select('*').from(Table.TemplateSources); + expect(result).toMatchSnapshot(); + }); - const result = await mockKnex.select('*').from('_checkpoints'); + it('should retrieve template sources', async () => { + const result = await store.getTemplateSources(INDEXER); expect(result).toMatchSnapshot(); }); });