From d51fcd402ae958fed47aa024f575f5d47381e836 Mon Sep 17 00:00:00 2001 From: Timo Erdelt Date: Thu, 18 Apr 2024 14:42:09 +0200 Subject: [PATCH] feat: implemented validator v2 --- ...ir-liquidity-info-history-v2-db.service.ts | 25 ++++ src/database/pair/pair-db.service.ts | 7 ++ src/lib/utils.ts | 2 +- ...idity-info-history-validator-v2.service.ts | 109 ++++++++++++++++++ src/tasks/tasks.module.ts | 2 + src/tasks/tasks.service.spec.ts | 14 ++- src/tasks/tasks.service.ts | 24 +++- ...ity-info-history-v2-db.service.e2e-spec.ts | 22 ++++ 8 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 src/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator-v2.service.ts diff --git a/src/database/pair-liquidity-info-history/pair-liquidity-info-history-v2-db.service.ts b/src/database/pair-liquidity-info-history/pair-liquidity-info-history-v2-db.service.ts index 8b0f710..82bd31b 100644 --- a/src/database/pair-liquidity-info-history/pair-liquidity-info-history-v2-db.service.ts +++ b/src/database/pair-liquidity-info-history/pair-liquidity-info-history-v2-db.service.ts @@ -36,4 +36,29 @@ export class PairLiquidityInfoHistoryV2DbService { ], }); } + + getWithinHeightSorted(heightLimit: number) { + return this.prisma.pairLiquidityInfoHistoryV2.findMany({ + where: { + height: { + gte: heightLimit, + }, + }, + orderBy: [ + { microBlockTime: 'asc' }, + { transactionIndex: 'asc' }, + { logIndex: 'asc' }, + ], + }); + } + + deleteFromMicroBlockTime(microBlockTime: bigint) { + return this.prisma.pairLiquidityInfoHistoryV2.deleteMany({ + where: { + microBlockTime: { + gte: microBlockTime, + }, + }, + }); + } } diff --git a/src/database/pair/pair-db.service.ts b/src/database/pair/pair-db.service.ts index 9fb1348..927abe0 100644 --- a/src/database/pair/pair-db.service.ts +++ b/src/database/pair/pair-db.service.ts @@ -60,6 +60,13 @@ export class PairDbService { }); } + get(pairId: number): Promise { + return this.prisma.pair.findUnique({ + where: { id: pairId }, + include: { token0: true, token1: true }, + }); + } + getOne(address: string) { return this.prisma.pair.findUnique({ where: { address }, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 74f89dc..2ab9377 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -27,4 +27,4 @@ export const bigIntToDecimal = (bigInt: bigint): Decimal => new Decimal(bigInt.toString()); export const decimalToBigInt = (decimal: Decimal): bigint => - BigInt(decimal.toString()); + BigInt(decimal.toFixed().toString()); diff --git a/src/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator-v2.service.ts b/src/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator-v2.service.ts new file mode 100644 index 0000000..f453945 --- /dev/null +++ b/src/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator-v2.service.ts @@ -0,0 +1,109 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { groupBy, last, map } from 'lodash'; + +import { MdwHttpClientService } from '@/clients/mdw-http-client.service'; +import { + ContractAddress, + contractAddrToAccountAddr, +} from '@/clients/sdk-client.model'; +import { SdkClientService } from '@/clients/sdk-client.service'; +import { PairDbService } from '@/database/pair/pair-db.service'; +import { PairLiquidityInfoHistoryV2DbService } from '@/database/pair-liquidity-info-history/pair-liquidity-info-history-v2-db.service'; +import { decimalToBigInt } from '@/lib/utils'; + +@Injectable() +export class PairLiquidityInfoHistoryValidatorV2Service { + constructor( + private pairLiquidityInfoHistoryDb: PairLiquidityInfoHistoryV2DbService, + private pairDb: PairDbService, + private mdwClient: MdwHttpClientService, + private sdkClient: SdkClientService, + ) {} + + readonly logger = new Logger(PairLiquidityInfoHistoryValidatorV2Service.name); + + readonly VALIDATION_WINDOW_BLOCKS = 20; + + async validate() { + this.logger.log(`Started validating pair liquidity info history.`); + + // Get current height + const currentHeight = await this.sdkClient.getHeight(); + + // Get all liquidity entries greater or equal the current height minus VALIDATION_WINDOW_BLOCKS + // and take the last entry of every microBlock to get the final reserve in that microBlock + const liquidityEntriesWithinHeightSorted = map( + groupBy( + await this.pairLiquidityInfoHistoryDb.getWithinHeightSorted( + currentHeight - this.VALIDATION_WINDOW_BLOCKS, + ), + 'microBlockHash', + ), + (group) => last(group)!, + ); + + // If the reserves of a local microBlock do not match with the data from the middleware or the block does not exist, + // delete this block and all newer entries + let numDeleted = 0; + for (const liquidityEntry of liquidityEntriesWithinHeightSorted) { + let isError = false; + let mdwReserve0: bigint | undefined; + let mdwReserve1: bigint | undefined; + + const pairWithTokens = (await this.pairDb.get(liquidityEntry?.pairId))!; + + try { + // reserve0 is the balance of the pair contract's account of token0 + mdwReserve0 = BigInt( + ( + await this.mdwClient.getAccountBalanceForContractAtMicroBlockHash( + pairWithTokens.token0.address as ContractAddress, + contractAddrToAccountAddr( + pairWithTokens.address as ContractAddress, + ), + liquidityEntry.microBlockHash, + ) + ).amount, + ); + + // reserve1 is the balance of the pair contract's account of token1 + mdwReserve1 = BigInt( + ( + await this.mdwClient.getAccountBalanceForContractAtMicroBlockHash( + pairWithTokens.token1.address as ContractAddress, + contractAddrToAccountAddr( + pairWithTokens.address as ContractAddress, + ), + liquidityEntry.microBlockHash, + ) + ).amount, + ); + } catch (e) { + this.logger.error(e); + isError = true; + } + if ( + isError || + decimalToBigInt(liquidityEntry.reserve0) !== mdwReserve0 || + decimalToBigInt(liquidityEntry.reserve1) !== mdwReserve1 + ) { + numDeleted = ( + await this.pairLiquidityInfoHistoryDb.deleteFromMicroBlockTime( + liquidityEntry.microBlockTime, + ) + ).count; + break; + } + } + + if (numDeleted > 0) { + this.logger.log( + `Found an inconsistency in pair liquidity info history. Deleted ${numDeleted} entries.`, + ); + } else { + this.logger.log('No problems in pair liquidity info history found.'); + } + + this.logger.log('Finished validating pair liquidity info history.'); + } +} diff --git a/src/tasks/tasks.module.ts b/src/tasks/tasks.module.ts index ddede2e..137ae89 100644 --- a/src/tasks/tasks.module.ts +++ b/src/tasks/tasks.module.ts @@ -6,6 +6,7 @@ import { DatabaseModule } from '@/database/database.module'; import { PairLiquidityInfoHistoryImporterService } from '@/tasks/pair-liquidity-info-history-importer/pair-liquidity-info-history-importer.service'; import { PairLiquidityInfoHistoryImporterV2Service } from '@/tasks/pair-liquidity-info-history-importer/pair-liquidity-info-history-importer-v2.service'; import { PairLiquidityInfoHistoryValidatorService } from '@/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator.service'; +import { PairLiquidityInfoHistoryValidatorV2Service } from '@/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator-v2.service'; import { PairSyncService } from '@/tasks/pair-sync/pair-sync.service'; import { TasksService } from '@/tasks/tasks.service'; @@ -15,6 +16,7 @@ import { TasksService } from '@/tasks/tasks.service'; PairLiquidityInfoHistoryImporterService, PairLiquidityInfoHistoryImporterV2Service, PairLiquidityInfoHistoryValidatorService, + PairLiquidityInfoHistoryValidatorV2Service, TasksService, PairSyncService, ], diff --git a/src/tasks/tasks.service.spec.ts b/src/tasks/tasks.service.spec.ts index 4560115..9d47b7e 100644 --- a/src/tasks/tasks.service.spec.ts +++ b/src/tasks/tasks.service.spec.ts @@ -12,11 +12,12 @@ import { PairLiquidityInfoHistoryImporterService } from '@/tasks/pair-liquidity- import { PairLiquidityInfoHistoryImporterV2Service } from '@/tasks/pair-liquidity-info-history-importer/pair-liquidity-info-history-importer-v2.service'; import { PairLiquidityInfoHistoryValidatorService } from '@/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator.service'; import { TasksService } from '@/tasks/tasks.service'; +import { PairLiquidityInfoHistoryValidatorV2Service } from '@/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator-v2.service'; describe('TasksService', () => { let tasksService: TasksService; let pairLiquidityInfoHistoryImporterService: PairLiquidityInfoHistoryImporterV2Service; - let pairLiquidityInfoHistoryValidatorService: PairLiquidityInfoHistoryValidatorService; + let pairLiquidityInfoHistoryValidatorService: PairLiquidityInfoHistoryValidatorV2Service; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -25,6 +26,7 @@ describe('TasksService', () => { PairLiquidityInfoHistoryImporterService, PairLiquidityInfoHistoryImporterV2Service, PairLiquidityInfoHistoryValidatorService, + PairLiquidityInfoHistoryValidatorV2Service, MdwHttpClientService, SdkClientService, PairDbService, @@ -42,8 +44,8 @@ describe('TasksService', () => { PairLiquidityInfoHistoryImporterV2Service, ); pairLiquidityInfoHistoryValidatorService = - module.get( - PairLiquidityInfoHistoryValidatorService, + module.get( + PairLiquidityInfoHistoryValidatorV2Service, ); }); @@ -88,7 +90,7 @@ describe('TasksService', () => { .spyOn(pairLiquidityInfoHistoryValidatorService, 'validate') .mockResolvedValue(); - await tasksService.runPairLiquidityInfoHistoryValidator(); + await tasksService.runPairLiquidityInfoHistoryValidatorV2(); expect( pairLiquidityInfoHistoryValidatorService.validate, ).toHaveBeenCalled(); @@ -99,7 +101,7 @@ describe('TasksService', () => { jest.spyOn(pairLiquidityInfoHistoryValidatorService, 'validate'); - await tasksService.runPairLiquidityInfoHistoryValidator(); + await tasksService.runPairLiquidityInfoHistoryValidatorV2(); expect( pairLiquidityInfoHistoryValidatorService.validate, ).not.toHaveBeenCalled(); @@ -112,7 +114,7 @@ describe('TasksService', () => { .mockRejectedValue(error); jest.spyOn(pairLiquidityInfoHistoryValidatorService.logger, 'error'); - await tasksService.runPairLiquidityInfoHistoryValidator(); + await tasksService.runPairLiquidityInfoHistoryValidatorV2(); expect( pairLiquidityInfoHistoryValidatorService.logger.error, ).toHaveBeenCalledWith(`Validation failed. ${error}`); diff --git a/src/tasks/tasks.service.ts b/src/tasks/tasks.service.ts index aea5b42..7a23693 100644 --- a/src/tasks/tasks.service.ts +++ b/src/tasks/tasks.service.ts @@ -4,6 +4,7 @@ import { Cron, CronExpression } from '@nestjs/schedule'; import { PairLiquidityInfoHistoryImporterService } from '@/tasks/pair-liquidity-info-history-importer/pair-liquidity-info-history-importer.service'; import { PairLiquidityInfoHistoryImporterV2Service } from '@/tasks/pair-liquidity-info-history-importer/pair-liquidity-info-history-importer-v2.service'; import { PairLiquidityInfoHistoryValidatorService } from '@/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator.service'; +import { PairLiquidityInfoHistoryValidatorV2Service } from '@/tasks/pair-liquidity-info-history-validator/pair-liquidity-info-history-validator-v2.service'; const EVERY_5_MINUTES_STARTING_AT_02_30 = '30 2-57/5 * * * *'; @@ -13,6 +14,7 @@ export class TasksService { private pairLiquidityInfoHistoryImporterService: PairLiquidityInfoHistoryImporterService, private pairLiquidityInfoHistoryImporterV2Service: PairLiquidityInfoHistoryImporterV2Service, private pairLiquidityInfoHistoryValidatorService: PairLiquidityInfoHistoryValidatorService, + private pairLiquidityInfoHistoryValidatorV2Service: PairLiquidityInfoHistoryValidatorV2Service, ) {} private _isRunning = false; @@ -57,16 +59,32 @@ export class TasksService { } } + // @Cron(EVERY_5_MINUTES_STARTING_AT_02_30) + // async runPairLiquidityInfoHistoryValidator() { + // try { + // if (!this.isRunning) { + // this.setIsRunning(true); + // await this.pairLiquidityInfoHistoryValidatorService.validate(); + // this.setIsRunning(false); + // } + // } catch (error) { + // this.pairLiquidityInfoHistoryValidatorService.logger.error( + // `Validation failed. ${error}`, + // ); + // this.setIsRunning(false); + // } + // } + @Cron(EVERY_5_MINUTES_STARTING_AT_02_30) - async runPairLiquidityInfoHistoryValidator() { + async runPairLiquidityInfoHistoryValidatorV2() { try { if (!this.isRunning) { this.setIsRunning(true); - await this.pairLiquidityInfoHistoryValidatorService.validate(); + await this.pairLiquidityInfoHistoryValidatorV2Service.validate(); this.setIsRunning(false); } } catch (error) { - this.pairLiquidityInfoHistoryValidatorService.logger.error( + this.pairLiquidityInfoHistoryValidatorV2Service.logger.error( `Validation failed. ${error}`, ); this.setIsRunning(false); diff --git a/test/e2e/pair-liquidity-info-history-v2-db.service.e2e-spec.ts b/test/e2e/pair-liquidity-info-history-v2-db.service.e2e-spec.ts index 86a282b..5964865 100644 --- a/test/e2e/pair-liquidity-info-history-v2-db.service.e2e-spec.ts +++ b/test/e2e/pair-liquidity-info-history-v2-db.service.e2e-spec.ts @@ -105,4 +105,26 @@ describe('PairLiquidityInfoHistoryV2DbService', () => { expect(result2?.id).toEqual(historyEntry4.id); }); }); + + describe('getWithinHeightSorted', () => { + it('should correctly return all entries greater or equal a given height limit sorted ascending', async () => { + const result = await service.getWithinHeightSorted(200002); + expect(result.map((e) => e.id)).toEqual([ + historyEntry2.id, + historyEntry3.id, + historyEntry4.id, + ]); + }); + }); + + describe('deleteFromMicroBlockTime', () => { + it('should correctly delete all entries newer or equal a given block time', async () => { + await service.deleteFromMicroBlockTime(3000000000003n); + const result = await prismaService.pairLiquidityInfoHistoryV2.findMany(); + expect(result.map((e) => e.id)).toEqual([ + historyEntry1.id, + historyEntry2.id, + ]); + }); + }); });