Skip to content

Commit

Permalink
add/fix unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dydxwill committed Nov 1, 2023
1 parent 3275946 commit 6fdc2d5
Show file tree
Hide file tree
Showing 10 changed files with 410 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export async function up(knex: Knex): Promise<void> {
'LIMIT',
'LIQUIDATED',
'LIQUIDATION',
'DELEVERAGED',
'OFFSETTING',
]).notNullable();
table.bigInteger('clobPairId').notNullable();
table.uuid('orderId').nullable();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as Knex from 'knex';

export async function up(knex: Knex): Promise<void> {
return knex.raw(`
ALTER TABLE ONLY fills
DROP CONSTRAINT IF EXISTS fills_type_check;
ALTER TABLE ONLY fills
ADD CONSTRAINT fills_type_check
CHECK (type = ANY (ARRAY['MARKET'::text, 'LIMIT'::text, 'LIQUIDATED'::text, 'LIQUIDATION'::text, 'DELEVERAGED'::text, 'OFFSETTING'::text]));
`);
}

export async function down(knex: Knex): Promise<void> {
return knex.raw(`
ALTER TABLE ONLY fills
DROP CONSTRAINT IF EXISTS fills_type_check;
ALTER TABLE ONLY fills
ADD CONSTRAINT fills_type_check
CHECK (type = ANY (ARRAY['MARKET'::text, 'LIMIT'::text, 'LIQUIDATED'::text, 'LIQUIDATION'::text]));
`);
}
329 changes: 329 additions & 0 deletions indexer/services/ender/__tests__/handlers/deleveraging-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
import { logger, stats, STATS_FUNCTION_NAME } from '@dydxprotocol-indexer/base';
import { redis } from '@dydxprotocol-indexer/redis';
import {
assetRefresher,
dbHelpers,
FillTable,
FillType,
Liquidity,
OrderSide,
perpetualMarketRefresher,
PerpetualPositionCreateObject,
PerpetualPositionStatus,
PerpetualPositionTable,
PositionSide,
SubaccountCreateObject,
SubaccountTable,
TendermintEventTable,
testConstants,
testMocks,
} from '@dydxprotocol-indexer/postgres';
import { updateBlockCache } from '../../src/caches/block-cache';
import { defaultDeleveragingEvent, defaultPreviousHeight } from '../helpers/constants';
import { clearCandlesMap } from '../../src/caches/candle-cache';
import { createPostgresFunctions } from '../../src/helpers/postgres/postgres-functions';
import { redisClient } from '../../src/helpers/redis/redis-controller';
import {
DeleveragingEventV1,
IndexerSubaccountId,
IndexerTendermintBlock,
IndexerTendermintEvent,
Timestamp,
} from '@dydxprotocol-indexer/v4-protos';
import {
createIndexerTendermintBlock,
createIndexerTendermintEvent,
createKafkaMessageFromDeleveragingEvent,
expectDefaultTradeKafkaMessageFromTakerFillId,
expectFillInDatabase,
expectFillSubaccountKafkaMessageFromLiquidationEvent,
} from '../helpers/indexer-proto-helpers';
import { DydxIndexerSubtypes } from '../../src/lib/types';
import {
DELEVERAGING_EVENT_TYPE,
MILLIS_IN_NANOS,
SECONDS_IN_MILLIS,
SUBACCOUNT_ORDER_FILL_EVENT_TYPE,
} from '../../src/constants';
import { DateTime } from 'luxon';
import Long from 'long';
import { DeleveragingHandler } from '../../src/handlers/deleveraging-handler';
import { KafkaMessage } from 'kafkajs';
import { onMessage } from '../../src/lib/on-message';
import { producer } from '@dydxprotocol-indexer/kafka';
import { createdDateTime, createdHeight } from '@dydxprotocol-indexer/postgres/build/__tests__/helpers/constants';

describe('DeleveragingHandler', () => {
const offsettingSubaccount: SubaccountCreateObject = {
address: defaultDeleveragingEvent.offsetting!.owner,
subaccountNumber: defaultDeleveragingEvent.offsetting!.number,
updatedAt: createdDateTime.toISO(),
updatedAtHeight: createdHeight,
};

const deleveragedSubaccount: SubaccountCreateObject = {
address: defaultDeleveragingEvent.liquidated!.owner,
subaccountNumber: defaultDeleveragingEvent.liquidated!.number,
updatedAt: createdDateTime.toISO(),
updatedAtHeight: createdHeight,
};

beforeAll(async () => {
await dbHelpers.migrate();
await createPostgresFunctions();
jest.spyOn(stats, 'increment');
jest.spyOn(stats, 'timing');
jest.spyOn(stats, 'gauge');
});

beforeEach(async () => {
await testMocks.seedData();
await perpetualMarketRefresher.updatePerpetualMarkets();
await assetRefresher.updateAssets();
updateBlockCache(defaultPreviousHeight);
});

afterEach(async () => {
await dbHelpers.clearData();
jest.clearAllMocks();
clearCandlesMap();
await redis.deleteAllAsync(redisClient);
});

afterAll(async () => {
await dbHelpers.teardown();
jest.resetAllMocks();
});

const defaultHeight: string = '3';
const defaultDateTime: DateTime = DateTime.utc(2022, 6, 1, 12, 1, 1, 2);
const defaultTime: Timestamp = {
seconds: Long.fromValue(Math.floor(defaultDateTime.toSeconds()), true),
nanos: (defaultDateTime.toMillis() % SECONDS_IN_MILLIS) * MILLIS_IN_NANOS,
};
const defaultTxHash: string = '0x32343534306431622d306461302d343831322d613730372d3965613162336162';

const offsettingPerpetualPosition: PerpetualPositionCreateObject = {
subaccountId: SubaccountTable.subaccountIdToUuid(defaultDeleveragingEvent.offsetting!),
perpetualId: testConstants.defaultPerpetualMarket.id,
side: PositionSide.LONG,
status: PerpetualPositionStatus.OPEN,
size: '10',
maxSize: '25',
sumOpen: '10',
entryPrice: '15000',
createdAt: DateTime.utc().toISO(),
createdAtHeight: '1',
openEventId: testConstants.defaultTendermintEventId,
lastEventId: testConstants.defaultTendermintEventId,
settledFunding: '200000',
};

it('getParallelizationIds', () => {
const offsettingSubaccountId: IndexerSubaccountId = defaultDeleveragingEvent.offsetting!;
const deleveragedSubaccountId: IndexerSubaccountId = defaultDeleveragingEvent.liquidated!;
const transactionIndex: number = 0;
const eventIndex: number = 0;

const indexerTendermintEvent: IndexerTendermintEvent = createIndexerTendermintEvent(
DydxIndexerSubtypes.DELEVERAGING,
DeleveragingEventV1.encode(defaultDeleveragingEvent).finish(),
transactionIndex,
eventIndex,
);
const block: IndexerTendermintBlock = createIndexerTendermintBlock(
0,
defaultTime,
[indexerTendermintEvent],
[defaultTxHash],
);

const handler: DeleveragingHandler = new DeleveragingHandler(
block,
indexerTendermintEvent,
0,
defaultDeleveragingEvent,
);

const offsettingSubaccountUuid: string = SubaccountTable.subaccountIdToUuid(
offsettingSubaccountId,
);
const deleveragedSubaccountUuid: string = SubaccountTable.subaccountIdToUuid(
deleveragedSubaccountId,
);

expect(handler.getParallelizationIds()).toEqual([
`${handler.eventType}_${offsettingSubaccountUuid}_${defaultDeleveragingEvent.clobPairId}`,
`${handler.eventType}_${deleveragedSubaccountUuid}_${defaultDeleveragingEvent.clobPairId}`,
// To ensure that SubaccountUpdateEvents and OrderFillEvents for the same subaccount are not
// processed in parallel
`${SUBACCOUNT_ORDER_FILL_EVENT_TYPE}_${offsettingSubaccountUuid}`,
`${SUBACCOUNT_ORDER_FILL_EVENT_TYPE}_${deleveragedSubaccountUuid}`,
// To ensure that StatefulOrderEvents and OrderFillEvents for the same order are not
// processed in parallel
`${DELEVERAGING_EVENT_TYPE}_${offsettingSubaccountUuid}`,
`${DELEVERAGING_EVENT_TYPE}_${deleveragedSubaccountUuid}`,
]);
});

it('DeleveragingEvent fails validation', async () => {
const deleveragingEvent: DeleveragingEventV1 = DeleveragingEventV1
.fromPartial({ // no liquidated subaccount
...defaultDeleveragingEvent,
liquidated: undefined,
});
const transactionIndex: number = 0;
const eventIndex: number = 0;
const kafkaMessage: KafkaMessage = createKafkaMessageFromDeleveragingEvent({
deleveragingEvent,
transactionIndex,
eventIndex,
height: parseInt(defaultHeight, 10),
time: defaultTime,
txHash: defaultTxHash,
});
const loggerCrit = jest.spyOn(logger, 'crit');
await expect(onMessage(kafkaMessage)).rejects.toThrowError();

expect(loggerCrit).toHaveBeenCalledWith(expect.objectContaining({
at: 'onMessage#onMessage',
message: 'Error: Unable to parse message, this must be due to a bug in V4 node',
}));
});

it('creates fills and updates perpetual positions', async () => {
const transactionIndex: number = 0;
const eventIndex: number = 0;
const kafkaMessage: KafkaMessage = createKafkaMessageFromDeleveragingEvent({
deleveragingEvent: defaultDeleveragingEvent,
transactionIndex,
eventIndex,
height: parseInt(defaultHeight, 10),
time: defaultTime,
txHash: defaultTxHash,
});

// create initial Subaccounts
await Promise.all([
SubaccountTable.create(offsettingSubaccount),
SubaccountTable.create(deleveragedSubaccount),
]);
// create initial PerpetualPositions
await Promise.all([
PerpetualPositionTable.create(offsettingPerpetualPosition),
PerpetualPositionTable.create({
...offsettingPerpetualPosition,
subaccountId: SubaccountTable.subaccountIdToUuid(defaultDeleveragingEvent.liquidated!),
}),
]);

const producerSendMock: jest.SpyInstance = jest.spyOn(producer, 'send');
await onMessage(kafkaMessage);

const eventId: Buffer = TendermintEventTable.createEventId(
defaultHeight,
transactionIndex,
eventIndex,
);

// This size should be in fixed-point notation rather than exponential notation.
const quoteAmount: string = '0.1'; // quote amount is price * fillAmount = 1e5 * 1e-6 = 1e-1
const totalFilled: string = '0.000001'; // fillAmount in human = 1e4 * 1e-10 = 1e-6
const price: string = '100000'; // 10^9*10^-8*10^-6/10^-10=10^5

await expectFillInDatabase({
subaccountId: SubaccountTable.subaccountIdToUuid(defaultDeleveragingEvent.offsetting!),
clientId: '0',
liquidity: Liquidity.MAKER,
size: totalFilled,
price,
quoteAmount,
eventId,
transactionHash: defaultTxHash,
createdAt: defaultDateTime.toISO(),
createdAtHeight: defaultHeight,
type: FillType.OFFSETTING,
clobPairId: defaultDeleveragingEvent.clobPairId.toString(),
side: OrderSide.SELL,
orderFlags: '0',
clientMetadata: null,
hasOrderId: false,
fee: '0',
});
await expectFillInDatabase({
subaccountId: SubaccountTable.subaccountIdToUuid(defaultDeleveragingEvent.liquidated!),
clientId: '0',
liquidity: Liquidity.TAKER,
size: totalFilled,
price,
quoteAmount,
eventId,
transactionHash: defaultTxHash,
createdAt: defaultDateTime.toISO(),
createdAtHeight: defaultHeight,
type: FillType.DELEVERAGED,
clobPairId: defaultDeleveragingEvent.clobPairId.toString(),
side: OrderSide.BUY,
orderFlags: '0',
clientMetadata: null,
hasOrderId: false,
fee: '0',
});

await Promise.all([
expectFillsAndPositionsSubaccountKafkaMessages(
producerSendMock,
eventId,
true,
),
expectFillsAndPositionsSubaccountKafkaMessages(
producerSendMock,
eventId,
false,
),
expectDefaultTradeKafkaMessageFromTakerFillId(
producerSendMock,
eventId,
),
]);
expectTimingStats();
});

async function expectFillsAndPositionsSubaccountKafkaMessages(
producerSendMock: jest.SpyInstance,
eventId: Buffer,
deleveraged: boolean,
) {
const subaccountId: IndexerSubaccountId = deleveraged
? defaultDeleveragingEvent.liquidated! : defaultDeleveragingEvent.offsetting!;
const liquidity: Liquidity = deleveraged ? Liquidity.TAKER : Liquidity.MAKER;
const positionId: string = (
await PerpetualPositionTable.findOpenPositionForSubaccountPerpetual(
SubaccountTable.subaccountIdToUuid(subaccountId),
testConstants.defaultPerpetualMarket.id,
)
)!.id;

await Promise.all([
expectFillSubaccountKafkaMessageFromLiquidationEvent(
producerSendMock,
subaccountId,
FillTable.uuid(eventId, liquidity),
positionId,
),
]);
}
});

function expectTimingStats() {
expectTimingStat('create_fills');
expectTimingStat('update_perpetual_positions');
}

function expectTimingStat(fnName: string) {
expect(stats.timing).toHaveBeenCalledWith(
`ender.${STATS_FUNCTION_NAME}.timing`,
expect.any(Number),
{ className: 'DeleveragingHandler', eventType: 'DeleveragingEvent', fnName },
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import { KafkaMessage } from 'kafkajs';
import { DateTime } from 'luxon';
import {
DELEVERAGING_EVENT_TYPE,
MILLIS_IN_NANOS,
SECONDS_IN_MILLIS,
STATEFUL_ORDER_ORDER_FILL_EVENT_TYPE,
Expand Down Expand Up @@ -197,6 +198,9 @@ describe('LiquidationHandler', () => {
if (orderId !== undefined) {
parallelizationIds.push(`${STATEFUL_ORDER_ORDER_FILL_EVENT_TYPE}_${orderId}`);
}
parallelizationIds.push(
`${DELEVERAGING_EVENT_TYPE}_${SubaccountTable.subaccountIdToUuid(subaccountId)}`,
);
expect(handler.getParallelizationIds()).toEqual(parallelizationIds);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { KafkaMessage } from 'kafkajs';
import { DateTime } from 'luxon';
import {
DELEVERAGING_EVENT_TYPE,
MILLIS_IN_NANOS,
SECONDS_IN_MILLIS,
STATEFUL_ORDER_ORDER_FILL_EVENT_TYPE,
Expand Down Expand Up @@ -196,6 +197,7 @@ describe('OrderHandler', () => {
`${handler.eventType}_${SubaccountTable.subaccountIdToUuid(subaccountId)}_${defaultOrderEvent.makerOrder!.orderId!.clobPairId}`,
`${SUBACCOUNT_ORDER_FILL_EVENT_TYPE}_${SubaccountTable.subaccountIdToUuid(subaccountId)}`,
`${STATEFUL_ORDER_ORDER_FILL_EVENT_TYPE}_${orderUuid}`,
`${DELEVERAGING_EVENT_TYPE}_${SubaccountTable.subaccountIdToUuid(subaccountId)}`,
]);
});
});
Expand Down
Loading

0 comments on commit 6fdc2d5

Please sign in to comment.