Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Pair Liquidity Info History API #11

Merged
merged 4 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions src/api/pair-liquidity-info-history/controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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([]);
kenodressel marked this conversation as resolved.
Show resolved Hide resolved
});

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);
});
});
});
106 changes: 106 additions & 0 deletions src/api/pair-liquidity-info-history/controller.ts
Original file line number Diff line number Diff line change
@@ -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<dto.PairLiquidityInfoHistoryEntry[]> {
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(),
})),
);
}
}
11 changes: 11 additions & 0 deletions src/api/pair-liquidity-info-history/module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
31 changes: 31 additions & 0 deletions src/api/pair-liquidity-info-history/service.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -15,6 +16,7 @@ import { ClientsModule } from './clients/clients.module';
ClientsModule,
DatabaseModule,
TasksModule,
PairLiquidityInfoHistoryModule,
],
controllers: [AppController],
providers: [TokensService, PairsService],
Expand Down
Loading