diff --git a/src/api/pair-liquidity-info-history/controller.spec.ts b/src/api/pair-liquidity-info-history/controller.spec.ts new file mode 100644 index 0000000..f3bb694 --- /dev/null +++ b/src/api/pair-liquidity-info-history/controller.spec.ts @@ -0,0 +1,228 @@ +import { PairLiquidityInfoHistoryController } from './controller'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PairLiquidityInfoHistoryService } from './service'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { Pair, PairLiquidityInfoHistory } from '@prisma/client'; +import { OrderQueryEnum } from '../../dto'; + +const mockPairLiquidityInfoHistoryService = { + getAllHistoryEntries: jest.fn(), +}; +describe('PairLiquidityInfoHistoryController', () => { + let app: INestApplication; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [PairLiquidityInfoHistoryController], + providers: [ + { + provide: PairLiquidityInfoHistoryService, + useValue: mockPairLiquidityInfoHistoryService, + }, + ], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + }); + + describe('GET /history/liquidity', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return history entries and use default values for empty params', async () => { + // Mocks + const historyEntry1: { pair: Pair } & PairLiquidityInfoHistory = { + id: 1, + pairId: 1, + totalSupply: '2000148656239820912122563', + reserve0: '950875688379385634428666', + reserve1: '4208476309359648851631167', + height: 912485, + microBlockHash: 'mh_Tx43Gh3acudUNSUWihPcV1Se4XcoFK3aUFAtFZk2Z4Zv7igZs', + microBlockTime: 1709027642807n, + updatedAt: new Date('2024-03-20 17:04:51.625'), + pair: { + id: 1, + address: 'ct_efYtiwDg4YZxDWE3iLPzvrjb92CJPvzGwriv4ZRuvuTDMNMb9', + t0: 15, + t1: 5, + synchronized: true, + }, + }; + + const historyEntry2: { pair: Pair } & PairLiquidityInfoHistory = { + id: 2, + pairId: 3, + totalSupply: '9954575303087659158151', + reserve0: '20210309618736130321327', + reserve1: '4903471477408475598460', + height: 707395, + microBlockHash: 'mh_2dUTfmwFc2ymeroB534giVwEvsa8d44Vf8SXtvy6GeHjdgQoHj', + microBlockTime: 1671708830503n, + updatedAt: new Date('2024-03-20 12:16:49.065'), + pair: { + id: 3, + address: 'ct_22iY9F7hng23gN8awi4aGnLy54YSR41wztbqgQCquuLYvTiGcm', + t0: 17, + t1: 22, + synchronized: true, + }, + }; + + mockPairLiquidityInfoHistoryService.getAllHistoryEntries.mockResolvedValue( + [historyEntry1, historyEntry2], + ); + + // Call route + const result = await request(app.getHttpServer()).get( + '/history/liquidity', + ); + + // Assertions + expect( + mockPairLiquidityInfoHistoryService.getAllHistoryEntries, + ).toHaveBeenCalledWith( + 100, + 0, + OrderQueryEnum.asc, + undefined, + undefined, + undefined, + undefined, + ); + expect(result.status).toBe(200); + expect(result.body).toEqual([ + { + pairAddress: historyEntry1.pair.address, + liquidityInfo: { + totalSupply: historyEntry1.totalSupply, + reserve0: historyEntry1.reserve0, + reserve1: historyEntry1.reserve1, + }, + height: historyEntry1.height, + microBlockHash: historyEntry1.microBlockHash, + microBlockTime: historyEntry1.microBlockTime.toString(), + }, + { + pairAddress: historyEntry2.pair.address, + liquidityInfo: { + totalSupply: historyEntry2.totalSupply, + reserve0: historyEntry2.reserve0, + reserve1: historyEntry2.reserve1, + }, + height: historyEntry2.height, + microBlockHash: historyEntry2.microBlockHash, + microBlockTime: historyEntry2.microBlockTime.toString(), + }, + ]); + }); + + it('should parse all query params correctly', async () => { + // Mocks + mockPairLiquidityInfoHistoryService.getAllHistoryEntries.mockResolvedValue( + [], + ); + + // Call route + const result = await request(app.getHttpServer()).get( + '/history/liquidity?limit=50&offset=50&order=desc&pair-address=ct_22iY9&height=912485&from-block-time=1709027642807&to-block-time=1709027642807', + ); + + // Assertions + expect( + mockPairLiquidityInfoHistoryService.getAllHistoryEntries, + ).toHaveBeenCalledWith( + 50, + 50, + OrderQueryEnum.desc, + 'ct_22iY9', + 912485, + 1709027642807n, + 1709027642807n, + ); + expect(result.status).toBe(200); + expect(result.body).toEqual([]); + }); + + it('should validate limit query param correctly', async () => { + // Call route + const result = await request(app.getHttpServer()).get( + '/history/liquidity?limit=xyz', + ); + + // Assertions + expect( + mockPairLiquidityInfoHistoryService.getAllHistoryEntries, + ).toHaveBeenCalledTimes(0); + expect(result.status).toBe(400); + }); + + it('should validate offset query param correctly', async () => { + // Call route + const result = await request(app.getHttpServer()).get( + '/history/liquidity?offset=xyz', + ); + + // Assertions + expect( + mockPairLiquidityInfoHistoryService.getAllHistoryEntries, + ).toHaveBeenCalledTimes(0); + expect(result.status).toBe(400); + }); + + it('should validate order query param correctly', async () => { + // Call route + const result = await request(app.getHttpServer()).get( + '/history/liquidity?order=xyz', + ); + + // Assertions + expect( + mockPairLiquidityInfoHistoryService.getAllHistoryEntries, + ).toHaveBeenCalledTimes(0); + expect(result.status).toBe(400); + }); + + it('should validate height query param correctly', async () => { + // Call route + const result = await request(app.getHttpServer()).get( + '/history/liquidity?height=xyz', + ); + + // Assertions + expect( + mockPairLiquidityInfoHistoryService.getAllHistoryEntries, + ).toHaveBeenCalledTimes(0); + expect(result.status).toBe(400); + }); + + it('should validate from-block-time query param correctly', async () => { + // Call route + const result = await request(app.getHttpServer()).get( + '/history/liquidity?from-block-time=xyz', + ); + + // Assertions + expect( + mockPairLiquidityInfoHistoryService.getAllHistoryEntries, + ).toHaveBeenCalledTimes(0); + expect(result.status).toBe(400); + }); + + it('should validate to-block-time query param correctly', async () => { + // Call route + const result = await request(app.getHttpServer()).get( + '/history/liquidity?to-block-time=xyz', + ); + + // Assertions + expect( + mockPairLiquidityInfoHistoryService.getAllHistoryEntries, + ).toHaveBeenCalledTimes(0); + expect(result.status).toBe(400); + }); + }); +}); diff --git a/src/api/pair-liquidity-info-history/controller.ts b/src/api/pair-liquidity-info-history/controller.ts new file mode 100644 index 0000000..8013cbc --- /dev/null +++ b/src/api/pair-liquidity-info-history/controller.ts @@ -0,0 +1,106 @@ +import { + Controller, + Get, + ParseEnumPipe, + ParseIntPipe, + Query, +} from '@nestjs/common'; +import { PairLiquidityInfoHistoryService } from './service'; +import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import * as dto from '../../dto'; +import { OrderQueryEnum } from '../../dto'; +import { ContractAddress } from '../../lib/utils'; + +@Controller('history/liquidity') +export class PairLiquidityInfoHistoryController { + constructor( + private readonly pairLiquidityInfoHistoryService: PairLiquidityInfoHistoryService, + ) {} + + @Get() + @ApiOperation({ + summary: 'Retrieve all entries of the pair liquidity info history', + }) + @ApiQuery({ + name: 'limit', + type: Number, + description: 'Limit of history entries per page (default: 100, max: 100)', + required: false, + }) + @ApiQuery({ + name: 'offset', + type: Number, + description: 'Offset of page (default: 0)', + required: false, + }) + @ApiQuery({ + name: 'order', + enum: OrderQueryEnum, + description: + 'Sorts history entries in ascending or descending order (default: asc)', + required: false, + }) + @ApiQuery({ + name: 'pair-address', + type: String, + description: 'Retrieve only history entries for the given pair address', + required: false, + }) + @ApiQuery({ + name: 'height', + type: Number, + description: 'Retrieve only history entries for the given height', + required: false, + }) + @ApiQuery({ + name: 'from-block-time', + type: Number, + description: + 'Retrieve only history entries that are equal or newer than the given micro block time', + required: false, + }) + @ApiQuery({ + name: 'to-block-time', + type: Number, + description: + 'Retrieve only history entries that are equal or older than the given micro block time', + required: false, + }) + @ApiResponse({ status: 200, type: [dto.PairLiquidityInfoHistoryEntry] }) + async findAll( + @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 100, + @Query('offset', new ParseIntPipe({ optional: true })) offset: number = 0, + @Query('order', new ParseEnumPipe(OrderQueryEnum, { optional: true })) + order: OrderQueryEnum = OrderQueryEnum.asc, + @Query('pair-address') pairAddress?: ContractAddress, + @Query('height', new ParseIntPipe({ optional: true })) height?: number, + @Query('from-block-time', new ParseIntPipe({ optional: true })) + fromBlockTime?: number, + @Query('to-block-time', new ParseIntPipe({ optional: true })) + toBlockTime?: number, + ): Promise { + return this.pairLiquidityInfoHistoryService + .getAllHistoryEntries( + Number(limit), + Number(offset), + order, + pairAddress, + height ? Number(height) : undefined, + fromBlockTime ? BigInt(fromBlockTime) : undefined, + toBlockTime ? BigInt(toBlockTime) : undefined, + ) + .then((entries) => + entries.map((entry) => ({ + pairAddress: entry.pair.address, + liquidityInfo: { + totalSupply: entry.totalSupply, + reserve0: entry.reserve0, + reserve1: entry.reserve1, + }, + height: entry.height, + microBlockHash: entry.microBlockHash, + microBlockTime: entry.microBlockTime.toString(), + })), + ); + } +} diff --git a/src/api/pair-liquidity-info-history/module.ts b/src/api/pair-liquidity-info-history/module.ts new file mode 100644 index 0000000..e5bbb0d --- /dev/null +++ b/src/api/pair-liquidity-info-history/module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { PairLiquidityInfoHistoryService } from './service'; +import { PairLiquidityInfoHistoryController } from './controller'; +import { DatabaseModule } from '../../database/database.module'; + +@Module({ + imports: [DatabaseModule], + controllers: [PairLiquidityInfoHistoryController], + providers: [PairLiquidityInfoHistoryService], +}) +export class PairLiquidityInfoHistoryModule {} diff --git a/src/api/pair-liquidity-info-history/service.ts b/src/api/pair-liquidity-info-history/service.ts new file mode 100644 index 0000000..0231391 --- /dev/null +++ b/src/api/pair-liquidity-info-history/service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { PairLiquidityInfoHistoryDbService } from '../../database/pair-liquidity-info-history-db.service'; +import { Pair, PairLiquidityInfoHistory } from '@prisma/client'; +import { OrderQueryEnum } from '../../dto'; +import { ContractAddress } from '../../lib/utils'; + +@Injectable() +export class PairLiquidityInfoHistoryService { + constructor( + private readonly pairLiquidityInfoHistoryDb: PairLiquidityInfoHistoryDbService, + ) {} + getAllHistoryEntries( + limit: number, + offset: number, + order: OrderQueryEnum, + pairAddress?: ContractAddress, + height?: number, + fromBlockTime?: bigint, + toBlockTime?: bigint, + ): Promise<({ pair: Pair } & PairLiquidityInfoHistory)[]> { + return this.pairLiquidityInfoHistoryDb.getAll( + limit, + offset, + order, + pairAddress, + height, + fromBlockTime, + toBlockTime, + ); + } +} diff --git a/src/app.module.ts b/src/app.module.ts index ed75409..6f530d4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,6 +7,7 @@ import { TasksModule } from './tasks/tasks.module'; import { TokensService } from './api/tokens/service'; import { PairsService } from './api/pairs/service'; import { ClientsModule } from './clients/clients.module'; +import { PairLiquidityInfoHistoryModule } from './api/pair-liquidity-info-history/module'; @Module({ imports: [ @@ -15,6 +16,7 @@ import { ClientsModule } from './clients/clients.module'; ClientsModule, DatabaseModule, TasksModule, + PairLiquidityInfoHistoryModule, ], controllers: [AppController], providers: [TokensService, PairsService], diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 21bac48..77f4c30 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -1,20 +1,20 @@ import { Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; -import { PairService } from './pair.service'; -import { PairLiquidityInfoHistoryService } from './pair-liquidity-info-history.service'; -import { PairLiquidityInfoHistoryErrorService } from './pair-liquidity-info-history-error.service'; +import { PairDbService } from './pair-db.service'; +import { PairLiquidityInfoHistoryDbService } from './pair-liquidity-info-history-db.service'; +import { PairLiquidityInfoHistoryErrorDbService } from './pair-liquidity-info-history-error-db.service'; @Module({ providers: [ PrismaService, - PairService, - PairLiquidityInfoHistoryService, - PairLiquidityInfoHistoryErrorService, + PairDbService, + PairLiquidityInfoHistoryDbService, + PairLiquidityInfoHistoryErrorDbService, ], exports: [ - PairService, - PairLiquidityInfoHistoryService, - PairLiquidityInfoHistoryErrorService, + PairDbService, + PairLiquidityInfoHistoryDbService, + PairLiquidityInfoHistoryErrorDbService, ], }) export class DatabaseModule {} diff --git a/src/database/pair.service.ts b/src/database/pair-db.service.ts similarity index 94% rename from src/database/pair.service.ts rename to src/database/pair-db.service.ts index f79d1d9..86bc450 100644 --- a/src/database/pair.service.ts +++ b/src/database/pair-db.service.ts @@ -5,7 +5,7 @@ import { PrismaService } from './prisma.service'; export type PairWithTokens = { token0: Token; token1: Token } & Pair; @Injectable() -export class PairService { +export class PairDbService { constructor(private prisma: PrismaService) {} getAll(): Promise { return this.prisma.pair.findMany({ diff --git a/src/database/pair-liquidity-info-history-db.service.e2e-spec.ts b/src/database/pair-liquidity-info-history-db.service.e2e-spec.ts new file mode 100644 index 0000000..acec1c7 --- /dev/null +++ b/src/database/pair-liquidity-info-history-db.service.e2e-spec.ts @@ -0,0 +1,215 @@ +import { PairLiquidityInfoHistoryDbService } from './pair-liquidity-info-history-db.service'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from './prisma.service'; +import { Pair, PairLiquidityInfoHistory, Token } from '@prisma/client'; +import { OrderQueryEnum } from '../dto'; +import { ContractAddress } from '../lib/utils'; + +const token1: Token = { + id: 1, + address: 'ct_token1', + symbol: '1', + name: '1', + decimals: 18, + malformed: false, + noContract: false, + listed: false, +}; +const token2: Token = { + id: 2, + address: 'ct_token2', + symbol: '2', + name: '2', + decimals: 18, + malformed: false, + noContract: false, + listed: false, +}; +const token3: Token = { + id: 3, + address: 'ct_token3', + symbol: '3', + name: '3', + decimals: 18, + malformed: false, + noContract: false, + listed: false, +}; +const pair1: Pair = { + id: 1, + address: 'ct_pair1', + t0: 1, + t1: 2, + synchronized: true, +}; +const pair2: Pair = { + id: 2, + address: 'ct_pair2', + t0: 2, + t1: 3, + synchronized: true, +}; +const pair3: Pair = { + id: 3, + address: 'ct_pair4', + t0: 2, + t1: 3, + synchronized: true, +}; +const historyEntry1: PairLiquidityInfoHistory = { + id: 1, + pairId: 1, + totalSupply: '2000148656239820912122563', + reserve0: '950875688379385634428666', + reserve1: '4208476309359648851631167', + height: 100001, + microBlockHash: 'mh_entry1', + microBlockTime: 1000000000001n, + updatedAt: new Date(), +}; +const historyEntry2: PairLiquidityInfoHistory = { + id: 2, + pairId: 1, + totalSupply: '9954575303087659158151', + reserve0: '20210309618736130321327', + reserve1: '4903471477408475598460', + height: 200002, + microBlockHash: 'mh_entry2', + microBlockTime: 2000000000002n, + updatedAt: new Date(), +}; +const historyEntry3: PairLiquidityInfoHistory = { + id: 3, + pairId: 2, + totalSupply: '56931443813890767374824', + reserve0: '20556919390913460010617', + reserve1: '157691178959228289022449', + height: 300003, + microBlockHash: 'mh_entry3', + microBlockTime: 3000000000003n, + updatedAt: new Date(), +}; +const historyEntry4: PairLiquidityInfoHistory = { + id: 4, + pairId: 2, + totalSupply: '56931443813890767374824', + reserve0: '20556919390913460010617', + reserve1: '157691178959228289022449', + height: 300003, + microBlockHash: 'mh_entry4', + microBlockTime: 4000000000004n, + updatedAt: new Date(), +}; + +describe('PairLiquidityInfoHistoryDbService', () => { + let service: PairLiquidityInfoHistoryDbService; + let prismaService: PrismaService; + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PairLiquidityInfoHistoryDbService, PrismaService], + }).compile(); + + service = module.get( + PairLiquidityInfoHistoryDbService, + ); + prismaService = module.get(PrismaService); + + await prismaService.token.createMany({ data: [token1, token2, token3] }); + await prismaService.pair.createMany({ data: [pair1, pair2, pair3] }); + await prismaService.pairLiquidityInfoHistory.createMany({ + data: [historyEntry1, historyEntry2, historyEntry3, historyEntry4], + }); + }); + + afterEach(async () => { + await prismaService.pairLiquidityInfoHistory.deleteMany(); + await prismaService.pair.deleteMany(); + await prismaService.token.deleteMany(); + }); + + describe('getAll', () => { + it('should return all entries', async () => { + const result = await service.getAll(100, 0, OrderQueryEnum.asc); + expect(result.map((e) => e.id)).toEqual([1, 2, 3, 4]); + }); + + it('should return return entries with limit, offset and order', async () => { + const result = await service.getAll(2, 1, OrderQueryEnum.desc); + expect(result.map((e) => e.id)).toEqual([3, 2]); + }); + + it('should correctly filter by pair address', async () => { + const result = await service.getAll( + 100, + 0, + OrderQueryEnum.asc, + pair1.address as ContractAddress, + undefined, + undefined, + undefined, + ); + expect(result.map((e) => e.id)).toEqual([1, 2]); + }); + + it('should correctly filter by height', async () => { + const result = await service.getAll( + 100, + 0, + OrderQueryEnum.asc, + undefined, + 300003, + undefined, + undefined, + ); + expect(result.map((e) => e.id)).toEqual([3, 4]); + }); + + it('should correctly return entries newer or equal fromBlockTime', async () => { + const result = await service.getAll( + 100, + 0, + OrderQueryEnum.asc, + undefined, + undefined, + 2000000000002n, + undefined, + ); + expect(result.map((e) => e.id)).toEqual([2, 3, 4]); + }); + + it('should correctly return entries older or equal toBlockTime', async () => { + const result = await service.getAll( + 100, + 0, + OrderQueryEnum.desc, + undefined, + undefined, + undefined, + 3000000000003n, + ); + expect(result.map((e) => e.id)).toEqual([3, 2, 1]); + }); + }); + + describe('getLastlySyncedBlockByPairId', () => { + it('should correctly return the last synced block for a given pairId', async () => { + const result = await service.getLastlySyncedBlockByPairId(1); + expect(result?.microBlockTime).toEqual(2000000000002n); + }); + }); + + describe('getWithinHeightSorted', () => { + it('should correctly return all entries greater or equal a given height limit sorted by microBlockTime ascending', async () => { + const result = await service.getWithinHeightSorted(200002); + expect(result.map((e) => e.id)).toEqual([2, 3, 4]); + }); + }); + + describe('deleteFromMicroBlockTime', () => { + it('should correctly delete all entries newer or equal a given block time', async () => { + await service.deleteFromMicroBlockTime(3000000000003n); + const result = await prismaService.pairLiquidityInfoHistory.findMany(); + expect(result.map((e) => e.id)).toEqual([1, 2]); + }); + }); +}); diff --git a/src/database/pair-liquidity-info-history.service.ts b/src/database/pair-liquidity-info-history-db.service.ts similarity index 64% rename from src/database/pair-liquidity-info-history.service.ts rename to src/database/pair-liquidity-info-history-db.service.ts index 959c18a..df441c3 100644 --- a/src/database/pair-liquidity-info-history.service.ts +++ b/src/database/pair-liquidity-info-history-db.service.ts @@ -1,11 +1,43 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from './prisma.service'; import { PairLiquidityInfoHistory } from '@prisma/client'; +import { OrderQueryEnum } from '../dto'; +import { ContractAddress } from '../lib/utils'; @Injectable() -export class PairLiquidityInfoHistoryService { +export class PairLiquidityInfoHistoryDbService { constructor(private prisma: PrismaService) {} + getAll = ( + limit: number, + offset: number, + order: OrderQueryEnum, + pairAddress?: ContractAddress, + height?: number, + fromBlockTime?: bigint, + toBlockTime?: bigint, + ) => + this.prisma.pairLiquidityInfoHistory.findMany({ + where: { + pair: pairAddress ? { address: { equals: pairAddress } } : {}, + height: height ? { equals: height } : {}, + microBlockTime: { + gte: fromBlockTime, + lte: toBlockTime, + }, + }, + include: { + pair: true, + }, + orderBy: order + ? { + microBlockTime: order, + } + : {}, + take: limit, + skip: offset, + }); + getCountByPairId(pairId: number) { return this.prisma.pairLiquidityInfoHistory.count({ where: { diff --git a/src/database/pair-liquidity-info-history-error-db.service.e2e-spec.ts b/src/database/pair-liquidity-info-history-error-db.service.e2e-spec.ts new file mode 100644 index 0000000..fe6269a --- /dev/null +++ b/src/database/pair-liquidity-info-history-error-db.service.e2e-spec.ts @@ -0,0 +1,116 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from './prisma.service'; +import { Pair, PairLiquidityInfoHistoryError, Token } from '@prisma/client'; +import { PairLiquidityInfoHistoryErrorDbService } from './pair-liquidity-info-history-error-db.service'; + +const token1: Token = { + id: 1, + address: 'ct_token1', + symbol: '1', + name: '1', + decimals: 18, + malformed: false, + noContract: false, + listed: false, +}; +const token2: Token = { + id: 2, + address: 'ct_token2', + symbol: '2', + name: '2', + decimals: 18, + malformed: false, + noContract: false, + listed: false, +}; +const pair1: Pair = { + id: 1, + address: 'ct_pair1', + t0: 1, + t1: 2, + synchronized: true, +}; +const pair2: Pair = { + id: 2, + address: 'ct_pair2', + t0: 1, + t1: 2, + synchronized: true, +}; +const errorEntry1: PairLiquidityInfoHistoryError = { + id: 1, + pairId: 1, + microBlockHash: '', + error: 'error_1', + timesOccurred: 1, + createdAt: new Date('2024-01-01 12:00:00.000'), + updatedAt: new Date('2024-01-01 12:00:00.000'), +}; +const errorEntry2: PairLiquidityInfoHistoryError = { + id: 2, + pairId: 1, + microBlockHash: 'mh_1', + error: 'error_2', + timesOccurred: 1, + createdAt: new Date('2024-01-01 12:00:00.000'), + updatedAt: new Date('2024-01-01 12:00:00.000'), +}; + +describe('PairLiquidityInfoHistoryErrorDbService', () => { + let service: PairLiquidityInfoHistoryErrorDbService; + let prismaService: PrismaService; + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PairLiquidityInfoHistoryErrorDbService, PrismaService], + }).compile(); + + service = module.get( + PairLiquidityInfoHistoryErrorDbService, + ); + prismaService = module.get(PrismaService); + + await prismaService.token.createMany({ data: [token1, token2] }); + await prismaService.pair.createMany({ data: [pair1, pair2] }); + await prismaService.pairLiquidityInfoHistoryError.createMany({ + data: [errorEntry1, errorEntry2], + }); + jest.useFakeTimers().setSystemTime(new Date('2024-01-01 17:59:00.000')); + }); + + afterEach(async () => { + await prismaService.pairLiquidityInfoHistoryError.deleteMany(); + await prismaService.pair.deleteMany(); + await prismaService.token.deleteMany(); + jest.useRealTimers(); + }); + + describe('getErrorByPairIdAndMicroBlockHashWithinHours', () => { + it('should correctly return an error within a recent given time window in hours by pairId', async () => { + jest.useFakeTimers().setSystemTime(new Date('2024-01-01 17:59:00.000')); + const result = await service.getErrorByPairIdAndMicroBlockHashWithinHours( + 1, + '', + 6, + ); + expect(result?.id).toEqual(1); + }); + + it('should correctly return an error within a recent given time window in hours by pairId and microBlockHash', async () => { + const result = await service.getErrorByPairIdAndMicroBlockHashWithinHours( + 1, + 'mh_1', + 6, + ); + expect(result?.id).toEqual(2); + }); + + it('should not return errors older than the given time window in hours', async () => { + const result = await service.getErrorByPairIdAndMicroBlockHashWithinHours( + 1, + '', + 5, + ); + expect(result).toBe(null); + }); + }); +}); diff --git a/src/database/pair-liquidity-info-history-error.service.ts b/src/database/pair-liquidity-info-history-error-db.service.ts similarity index 95% rename from src/database/pair-liquidity-info-history-error.service.ts rename to src/database/pair-liquidity-info-history-error-db.service.ts index a0a003b..9582993 100644 --- a/src/database/pair-liquidity-info-history-error.service.ts +++ b/src/database/pair-liquidity-info-history-error-db.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from './prisma.service'; import { PairLiquidityInfoHistoryError } from '@prisma/client'; @Injectable() -export class PairLiquidityInfoHistoryErrorService { +export class PairLiquidityInfoHistoryErrorDbService { constructor(private prisma: PrismaService) {} getErrorByPairIdAndMicroBlockHashWithinHours( diff --git a/src/dto.ts b/src/dto.ts index 1a5ca6e..7e2afec 100644 --- a/src/dto.ts +++ b/src/dto.ts @@ -1,7 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -export const contractPattern = 'ct_([1-9a-zA-Z]){49,50}'; +export const contractPattern = + 'ct_([23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz]){49,50}'; export const bigNumberPattern = '[1-9]+'; +export const microBlockTimePattern = '[1-9]{13}'; +export const microBlockHashPattern = + 'mh_([23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz]){49,50}'; + export const pairAddressPropertyOptions = { pattern: contractPattern, description: 'Pair contract address', @@ -247,3 +252,29 @@ export class GlobalState { }) listedPairs: number; } + +export enum OrderQueryEnum { + asc = 'asc', + desc = 'desc', +} + +export class PairLiquidityInfoHistoryEntry { + @ApiProperty(pairAddressPropertyOptions) + pairAddress: string; + @ApiProperty({ description: 'Liquidity info of the pair' }) + liquidityInfo: LiquidityInfo; + @ApiProperty({ + description: 'Block height of the history entry', + }) + height: number; + @ApiProperty({ + description: 'Micro block hash of the history entry', + pattern: microBlockHashPattern, + }) + microBlockHash: string; + @ApiProperty({ + description: 'Micro block time of the history entry', + pattern: microBlockTimePattern, + }) + microBlockTime: string; +} diff --git a/src/tasks/pair-liquidity-info-history-importer.service.spec.ts b/src/tasks/pair-liquidity-info-history-importer.service.spec.ts index ca2c9fc..7dafb7b 100644 --- a/src/tasks/pair-liquidity-info-history-importer.service.spec.ts +++ b/src/tasks/pair-liquidity-info-history-importer.service.spec.ts @@ -1,9 +1,9 @@ import { PairLiquidityInfoHistoryImporterService } from './pair-liquidity-info-history-importer.service'; import { Test, TestingModule } from '@nestjs/testing'; import { MdwClientService } from '../clients/mdw-client.service'; -import { PairService } from '../database/pair.service'; -import { PairLiquidityInfoHistoryService } from '../database/pair-liquidity-info-history.service'; -import { PairLiquidityInfoHistoryErrorService } from '../database/pair-liquidity-info-history-error.service'; +import { PairDbService } from '../database/pair-db.service'; +import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history-db.service'; +import { PairLiquidityInfoHistoryErrorDbService } from '../database/pair-liquidity-info-history-error-db.service'; import { ContractAddress } from '../lib/utils'; import { Contract } from '../clients/mdw-client.model'; @@ -15,16 +15,16 @@ const mockMdwClientService = { getAccountBalanceForContractAtMicroBlockHash: jest.fn(), }; -const mockPairService = { +const mockPairDb = { getAll: jest.fn(), }; -const mockPairLiquidityInfoHistoryService = { +const mockPairLiquidityInfoHistoryDb = { getLastlySyncedBlockByPairId: jest.fn(), upsert: jest.fn(), }; -const mockPairLiquidityInfoHistoryErrorService = { +const mockPairLiquidityInfoHistoryErrorDb = { getErrorByPairIdAndMicroBlockHashWithinHours: jest.fn(), }; @@ -36,14 +36,14 @@ describe('PairLiquidityInfoHistoryImporterService', () => { providers: [ PairLiquidityInfoHistoryImporterService, { provide: MdwClientService, useValue: mockMdwClientService }, - { provide: PairService, useValue: mockPairService }, + { provide: PairDbService, useValue: mockPairDb }, { - provide: PairLiquidityInfoHistoryService, - useValue: mockPairLiquidityInfoHistoryService, + provide: PairLiquidityInfoHistoryDbService, + useValue: mockPairLiquidityInfoHistoryDb, }, { - provide: PairLiquidityInfoHistoryErrorService, - useValue: mockPairLiquidityInfoHistoryErrorService, + provide: PairLiquidityInfoHistoryErrorDbService, + useValue: mockPairLiquidityInfoHistoryErrorDb, }, ], }).compile(); @@ -86,16 +86,16 @@ describe('PairLiquidityInfoHistoryImporterService', () => { }; // Mock functions - mockPairService.getAll.mockResolvedValue([pair1]); - mockPairLiquidityInfoHistoryErrorService.getErrorByPairIdAndMicroBlockHashWithinHours.mockResolvedValue( + mockPairDb.getAll.mockResolvedValue([pair1]); + mockPairLiquidityInfoHistoryErrorDb.getErrorByPairIdAndMicroBlockHashWithinHours.mockResolvedValue( undefined, ); - mockPairLiquidityInfoHistoryService.getLastlySyncedBlockByPairId.mockReturnValue( + mockPairLiquidityInfoHistoryDb.getLastlySyncedBlockByPairId.mockReturnValue( undefined, ); mockMdwClientService.getContract.mockResolvedValue(pair1Contract); mockMdwClientService.getMicroBlock.mockResolvedValue(initialMicroBlock); - mockPairLiquidityInfoHistoryService.upsert.mockResolvedValue(null); + mockPairLiquidityInfoHistoryDb.upsert.mockResolvedValue(null); mockMdwClientService.getContractLogsUntilCondition.mockResolvedValue([ pairContractLog1, pairContractLog2, @@ -113,7 +113,7 @@ describe('PairLiquidityInfoHistoryImporterService', () => { // Assertions // Insert initial liquidity - expect(mockPairLiquidityInfoHistoryService.upsert).toHaveBeenCalledWith({ + expect(mockPairLiquidityInfoHistoryDb.upsert).toHaveBeenCalledWith({ pairId: pair1.id, totalSupply: '0', reserve0: '0', @@ -123,12 +123,12 @@ describe('PairLiquidityInfoHistoryImporterService', () => { microBlockTime: BigInt(initialMicroBlock.time), }); expect( - mockPairLiquidityInfoHistoryErrorService.getErrorByPairIdAndMicroBlockHashWithinHours, + mockPairLiquidityInfoHistoryErrorDb.getErrorByPairIdAndMicroBlockHashWithinHours, ).toHaveBeenCalledTimes(3); expect(service.logger.log).toHaveBeenCalledWith( `Started syncing pair ${pair1.id} ${pair1.address}. Need to sync 2 micro block(s). This can take some time.`, ); - expect(mockPairLiquidityInfoHistoryService.upsert).toHaveBeenCalledWith({ + expect(mockPairLiquidityInfoHistoryDb.upsert).toHaveBeenCalledWith({ pairId: pair1.id, totalSupply: '2', reserve0: '1', @@ -137,7 +137,7 @@ describe('PairLiquidityInfoHistoryImporterService', () => { microBlockHash: pairContractLog1.block_hash, microBlockTime: BigInt(pairContractLog1.block_time), }); - expect(mockPairLiquidityInfoHistoryService.upsert).toHaveBeenCalledWith({ + expect(mockPairLiquidityInfoHistoryDb.upsert).toHaveBeenCalledWith({ pairId: pair1.id, totalSupply: '2', reserve0: '1', diff --git a/src/tasks/pair-liquidity-info-history-importer.service.ts b/src/tasks/pair-liquidity-info-history-importer.service.ts index f549038..57cdada 100644 --- a/src/tasks/pair-liquidity-info-history-importer.service.ts +++ b/src/tasks/pair-liquidity-info-history-importer.service.ts @@ -1,14 +1,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { MdwClientService } from '../clients/mdw-client.service'; -import { PairService, PairWithTokens } from '../database/pair.service'; +import { PairDbService, PairWithTokens } from '../database/pair-db.service'; import { isEqual, orderBy, uniqWith } from 'lodash'; -import { PairLiquidityInfoHistoryService } from '../database/pair-liquidity-info-history.service'; +import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history-db.service'; import { ContractAddress, contractAddrToAccountAddr, MicroBlockHash, } from '../lib/utils'; -import { PairLiquidityInfoHistoryErrorService } from '../database/pair-liquidity-info-history-error.service'; +import { PairLiquidityInfoHistoryErrorDbService } from '../database/pair-liquidity-info-history-error-db.service'; import { getClient } from '../lib/contracts'; import { ContractLog } from '../clients/mdw-client.model'; @@ -22,9 +22,9 @@ type MicroBlock = { export class PairLiquidityInfoHistoryImporterService { constructor( private mdwClientService: MdwClientService, - private pairService: PairService, - private pairLiquidityInfoHistoryService: PairLiquidityInfoHistoryService, - private pairLiquidityInfoHistoryErrorService: PairLiquidityInfoHistoryErrorService, + private pairDb: PairDbService, + private pairLiquidityInfoHistoryDb: PairLiquidityInfoHistoryDbService, + private pairLiquidityInfoHistoryErrorDb: PairLiquidityInfoHistoryErrorDbService, ) {} readonly logger = new Logger(PairLiquidityInfoHistoryImporterService.name); @@ -34,7 +34,7 @@ export class PairLiquidityInfoHistoryImporterService { this.logger.log(`Started syncing pair liquidity info history.`); // Fetch all pairs from DB - const pairsWithTokens = await this.pairService.getAll(); + const pairsWithTokens = await this.pairDb.getAll(); this.logger.log( `Syncing liquidity info history for ${pairsWithTokens.length} pairs.`, ); @@ -43,7 +43,7 @@ export class PairLiquidityInfoHistoryImporterService { try { // If an error occurred for this pair recently, skip pair const error = - await this.pairLiquidityInfoHistoryErrorService.getErrorByPairIdAndMicroBlockHashWithinHours( + await this.pairLiquidityInfoHistoryErrorDb.getErrorByPairIdAndMicroBlockHashWithinHours( pairWithTokens.id, '', this.WITHIN_HOURS_TO_SKIP_IF_ERROR, @@ -64,10 +64,9 @@ export class PairLiquidityInfoHistoryImporterService { const { height: lastSyncedHeight, microBlockTime: lastSyncedBlockTime, - } = - (await this.pairLiquidityInfoHistoryService.getLastlySyncedBlockByPairId( - pairWithTokens.id, - )) || { height: 0, microBlockTime: 0n }; + } = (await this.pairLiquidityInfoHistoryDb.getLastlySyncedBlockByPairId( + pairWithTokens.id, + )) || { height: 0, microBlockTime: 0n }; // If first sync (= no entries present yet for pair), insert initial liquidity if (lastSyncedHeight === 0 && lastSyncedBlockTime === 0n) { @@ -132,7 +131,7 @@ export class PairLiquidityInfoHistoryImporterService { try { // If an error occurred for this block recently, skip block const error = - await this.pairLiquidityInfoHistoryErrorService.getErrorByPairIdAndMicroBlockHashWithinHours( + await this.pairLiquidityInfoHistoryErrorDb.getErrorByPairIdAndMicroBlockHashWithinHours( pairWithTokens.id, block.hash, this.WITHIN_HOURS_TO_SKIP_IF_ERROR, @@ -151,7 +150,7 @@ export class PairLiquidityInfoHistoryImporterService { ); // Upsert liquidity - await this.pairLiquidityInfoHistoryService + await this.pairLiquidityInfoHistoryDb .upsert({ pairId: pairWithTokens.id, totalSupply: liquidity.totalSupply.toString(), @@ -171,7 +170,7 @@ export class PairLiquidityInfoHistoryImporterService { this.logger.error( `Skipped microBlock. ${JSON.stringify(errorData)}`, ); - await this.pairLiquidityInfoHistoryErrorService.upsert(errorData); + await this.pairLiquidityInfoHistoryErrorDb.upsert(errorData); } } @@ -187,7 +186,7 @@ export class PairLiquidityInfoHistoryImporterService { error: error.toString(), }; this.logger.error(`Skipped pair. ${JSON.stringify(errorData)}`); - await this.pairLiquidityInfoHistoryErrorService.upsert(errorData); + await this.pairLiquidityInfoHistoryErrorDb.upsert(errorData); } } @@ -201,7 +200,7 @@ export class PairLiquidityInfoHistoryImporterService { const microBlock = await this.mdwClientService.getMicroBlock( pairContract.block_hash, ); - await this.pairLiquidityInfoHistoryService + await this.pairLiquidityInfoHistoryDb .upsert({ pairId: pairWithTokens.id, totalSupply: '0', diff --git a/src/tasks/pair-liquidity-info-history-validator.service.spec.ts b/src/tasks/pair-liquidity-info-history-validator.service.spec.ts index d07ab22..17039d7 100644 --- a/src/tasks/pair-liquidity-info-history-validator.service.spec.ts +++ b/src/tasks/pair-liquidity-info-history-validator.service.spec.ts @@ -1,13 +1,13 @@ import { PairLiquidityInfoHistoryValidatorService } from './pair-liquidity-info-history-validator.service'; import { Test, TestingModule } from '@nestjs/testing'; import { MdwClientService } from '../clients/mdw-client.service'; -import { PairLiquidityInfoHistoryService } from '../database/pair-liquidity-info-history.service'; +import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history-db.service'; const mockMdwClientService = { getKeyBlockMicroBlocks: jest.fn(), }; -const mockPairLiquidityInfoHistoryService = { +const mockPairLiquidityInfoHistoryDb = { getWithinHeightSorted: jest.fn(), deleteFromMicroBlockTime: jest.fn(), }; @@ -21,8 +21,8 @@ describe('PairLiquidityInfoHistoryValidatorService', () => { PairLiquidityInfoHistoryValidatorService, { provide: MdwClientService, useValue: mockMdwClientService }, { - provide: PairLiquidityInfoHistoryService, - useValue: mockPairLiquidityInfoHistoryService, + provide: PairLiquidityInfoHistoryDbService, + useValue: mockPairLiquidityInfoHistoryDb, }, ], }).compile(); @@ -60,9 +60,12 @@ describe('PairLiquidityInfoHistoryValidatorService', () => { microBlockHash: 'mh_hash5', }; // Mock functions - mockPairLiquidityInfoHistoryService.getWithinHeightSorted.mockResolvedValue( - [historyEntry1, historyEntry2, historyEntry3, historyEntry4], - ); + mockPairLiquidityInfoHistoryDb.getWithinHeightSorted.mockResolvedValue([ + historyEntry1, + historyEntry2, + historyEntry3, + historyEntry4, + ]); mockMdwClientService.getKeyBlockMicroBlocks.mockImplementation( (height: number) => { if (height === historyEntry1.height) { @@ -78,7 +81,7 @@ describe('PairLiquidityInfoHistoryValidatorService', () => { } }, ); - mockPairLiquidityInfoHistoryService.deleteFromMicroBlockTime.mockResolvedValue( + mockPairLiquidityInfoHistoryDb.deleteFromMicroBlockTime.mockResolvedValue( { count: 2 }, ); jest.spyOn(service.logger, 'log'); @@ -100,7 +103,7 @@ describe('PairLiquidityInfoHistoryValidatorService', () => { mockMdwClientService.getKeyBlockMicroBlocks, ).not.toHaveBeenCalledWith(historyEntry5.height); expect( - mockPairLiquidityInfoHistoryService.deleteFromMicroBlockTime, + mockPairLiquidityInfoHistoryDb.deleteFromMicroBlockTime, ).toHaveBeenCalledWith(historyEntry4.microBlockTime); expect(service.logger.log).toHaveBeenCalledWith( `Found an inconsistency in pair liquidity info history. Deleted 2 entries.`, diff --git a/src/tasks/pair-liquidity-info-history-validator.service.ts b/src/tasks/pair-liquidity-info-history-validator.service.ts index 03e2db4..b81d36f 100644 --- a/src/tasks/pair-liquidity-info-history-validator.service.ts +++ b/src/tasks/pair-liquidity-info-history-validator.service.ts @@ -1,5 +1,5 @@ import { MdwClientService } from '../clients/mdw-client.service'; -import { PairLiquidityInfoHistoryService } from '../database/pair-liquidity-info-history.service'; +import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history-db.service'; import { Injectable, Logger } from '@nestjs/common'; import { uniq } from 'lodash'; import { getClient } from '../lib/contracts'; @@ -9,7 +9,7 @@ import { MicroBlockHash } from '../lib/utils'; export class PairLiquidityInfoHistoryValidatorService { constructor( private mdwClientService: MdwClientService, - private pairLiquidityInfoHistoryService: PairLiquidityInfoHistoryService, + private pairLiquidityInfoHistoryDb: PairLiquidityInfoHistoryDbService, ) {} readonly logger = new Logger(PairLiquidityInfoHistoryValidatorService.name); @@ -24,7 +24,7 @@ export class PairLiquidityInfoHistoryValidatorService { // Get all liquidity entries greater or equal the current height minus 20 const liquidityEntriesWithinHeightSorted = - await this.pairLiquidityInfoHistoryService.getWithinHeightSorted( + await this.pairLiquidityInfoHistoryDb.getWithinHeightSorted( currentHeight - 20, ); @@ -53,7 +53,7 @@ export class PairLiquidityInfoHistoryValidatorService { ) ) { numDeleted = ( - await this.pairLiquidityInfoHistoryService.deleteFromMicroBlockTime( + await this.pairLiquidityInfoHistoryDb.deleteFromMicroBlockTime( liquidityEntry.microBlockTime, ) ).count; diff --git a/src/tasks/tasks.service.spec.ts b/src/tasks/tasks.service.spec.ts index 2622733..c8068dc 100644 --- a/src/tasks/tasks.service.spec.ts +++ b/src/tasks/tasks.service.spec.ts @@ -3,9 +3,9 @@ import { TasksService } from './tasks.service'; import { PairLiquidityInfoHistoryImporterService } from './pair-liquidity-info-history-importer.service'; import { PairLiquidityInfoHistoryValidatorService } from './pair-liquidity-info-history-validator.service'; import { MdwClientService } from '../clients/mdw-client.service'; -import { PairService } from '../database/pair.service'; -import { PairLiquidityInfoHistoryService } from '../database/pair-liquidity-info-history.service'; -import { PairLiquidityInfoHistoryErrorService } from '../database/pair-liquidity-info-history-error.service'; +import { PairDbService } from '../database/pair-db.service'; +import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history-db.service'; +import { PairLiquidityInfoHistoryErrorDbService } from '../database/pair-liquidity-info-history-error-db.service'; import { PrismaService } from '../database/prisma.service'; describe('TasksService', () => { @@ -20,9 +20,9 @@ describe('TasksService', () => { PairLiquidityInfoHistoryImporterService, PairLiquidityInfoHistoryValidatorService, MdwClientService, - PairService, - PairLiquidityInfoHistoryService, - PairLiquidityInfoHistoryErrorService, + PairDbService, + PairLiquidityInfoHistoryDbService, + PairLiquidityInfoHistoryErrorDbService, PrismaService, ], }).compile(); diff --git a/test/jest-e2e.json b/test/jest-e2e.json index e9d912f..93400cd 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -1,6 +1,6 @@ { "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", + "rootDir": "../", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", "transform": {