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

Emergency patch to handle invalid TimeInForce. (backport #907) #947

Open
wants to merge 1 commit into
base: release/indexer/v3.x
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ describe('protocolTranslations', () => {
['IOC', IndexerOrder_TimeInForce.TIME_IN_FORCE_IOC, TimeInForce.IOC],
['UNSPECIFIED', IndexerOrder_TimeInForce.TIME_IN_FORCE_UNSPECIFIED, TimeInForce.GTT],
['POST_ONLY', IndexerOrder_TimeInForce.TIME_IN_FORCE_POST_ONLY, TimeInForce.POST_ONLY],
// Test case for emergency patch
['INVALID', (100 as IndexerOrder_TimeInForce), TimeInForce.GTT],
])('successfully gets TimeInForce given protocol order TIF: %s', (
_name: string,
protocolTIF: IndexerOrder_TimeInForce,
Expand All @@ -176,13 +178,14 @@ describe('protocolTranslations', () => {
expect(protocolOrderTIFToTIF(protocolTIF)).toEqual(expectedTimeInForce);
});

it('throws error if unrecognized protocolTIF given', () => {
// Commented out for emergency patch
/* it('throws error if unrecognized protocolTIF given', () => {
expect(
() => {
protocolOrderTIFToTIF(100 as IndexerOrder_TimeInForce);
},
).toThrow(new Error('Unexpected TimeInForce from protocol: 100'));
});
});*/
});

describe('tifToProtocolOrderTIF', () => {
Expand Down
4 changes: 3 additions & 1 deletion indexer/packages/postgres/src/lib/protocol-translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@ export function protocolOrderTIFToTIF(
protocolOrderTIF: IndexerOrder_TimeInForce,
): TimeInForce {
if (!(protocolOrderTIF in PROTOCOL_TIF_TO_INDEXER_TIF_MAP)) {
throw new Error(`Unexpected TimeInForce from protocol: ${protocolOrderTIF}`);
return TimeInForce.GTT;
// Removed for emergency patch
// throw new Error(`Unexpected TimeInForce from protocol: ${protocolOrderTIF}`);
}

return PROTOCOL_TIF_TO_INDEXER_TIF_MAP[protocolOrderTIF];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1451,12 +1451,236 @@ describe('OrderHandler', () => {
]);
});

// Testing emergency patch
it.each([
[
'via knex',
false,
],
[
'via SQL function',
true,
],
])('handles invalid TimeInForce (%s)', async (
_name: string,
useSqlFunction: boolean,
) => {
config.USE_ORDER_HANDLER_SQL_FUNCTION = useSqlFunction;
const transactionIndex: number = 0;
const eventIndex: number = 0;
const makerQuantums: number = 100;
const makerSubticks: number = 1_000_000;

const makerOrderProto: IndexerOrder = createOrder({
subaccountId: defaultSubaccountId,
clientId: 0,
side: IndexerOrder_Side.SIDE_BUY,
quantums: makerQuantums,
subticks: makerSubticks,
goodTilOneof: {
goodTilBlock: 10,
},
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
orderFlags: ORDER_FLAG_SHORT_TERM.toString(),
// Invalid TIF
timeInForce: (4 as IndexerOrder_TimeInForce),
reduceOnly: false,
clientMetadata: 0,
});

const takerSubticks: number = 150_000;
const takerQuantums: number = 10;
const takerOrderProto: IndexerOrder = createOrder({
subaccountId: defaultSubaccountId2,
clientId: 0,
side: IndexerOrder_Side.SIDE_SELL,
quantums: takerQuantums,
subticks: takerSubticks,
goodTilOneof: {
goodTilBlock: 10,
},
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
orderFlags: ORDER_FLAG_LONG_TERM.toString(),
// Invalid TIF
timeInForce: (17 as IndexerOrder_TimeInForce),
reduceOnly: true,
clientMetadata: 0,
});

// create initial PerpetualPositions with closed previous positions
await Promise.all([
// previous position for subaccount 1
PerpetualPositionTable.create({
...defaultPerpetualPosition,
perpetualId: testConstants.defaultPerpetualMarket3.id,
createdAtHeight: '1',
size: '0',
status: PerpetualPositionStatus.CLOSED,
openEventId: testConstants.defaultTendermintEventId2,
}),
// previous position for subaccount 2
PerpetualPositionTable.create({
...defaultPerpetualPosition,
perpetualId: testConstants.defaultPerpetualMarket3.id,
subaccountId: testConstants.defaultSubaccountId2,
createdAtHeight: '1',
size: '0',
status: PerpetualPositionStatus.CLOSED,
openEventId: testConstants.defaultTendermintEventId2,
}),
// initial position for subaccount 2
PerpetualPositionTable.create({
...defaultPerpetualPosition,
perpetualId: testConstants.defaultPerpetualMarket3.id,
}),
PerpetualPositionTable.create({
...defaultPerpetualPosition,
perpetualId: testConstants.defaultPerpetualMarket3.id,
subaccountId: testConstants.defaultSubaccountId2,
}),
]);

const fillAmount: number = 10;
const orderFillEvent: OrderFillEventV1 = createOrderFillEvent(
makerOrderProto,
takerOrderProto,
fillAmount,
fillAmount,
fillAmount,
);
const kafkaMessage: KafkaMessage = createKafkaMessageFromOrderFillEvent({
orderFillEvent,
transactionIndex,
eventIndex,
height: parseInt(defaultHeight, 10),
time: defaultTime,
txHash: defaultTxHash,
});

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

// This price should be in fixed-point notation rather than exponential notation (1e-8)
const makerOrderSize: string = '1'; // quantums in human = 1e2 * 1e-2 = 1
const makerPrice: string = '0.00000000000001'; // quote currency / base currency = 1e6 * 1e-16 * 1e-6 / 1e-2 = 1e-14
const takerPrice: string = '0.0000000000000015'; // quote currency / base currency = 1.5e5 * 1e-16 * 1e-6 / 1e-2 = 1.5e-15
const totalFilled: string = '0.1'; // fillAmount in human = 1e1 * 1e-2 = 1e-1
await expectOrderInDatabase({
subaccountId: testConstants.defaultSubaccountId,
clientId: '0',
size: makerOrderSize,
totalFilled,
price: makerPrice,
status: OrderStatus.OPEN, // orderSize > totalFilled so status is open
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
side: protocolTranslations.protocolOrderSideToOrderSide(makerOrderProto.side),
orderFlags: makerOrderProto.orderId!.orderFlags.toString(),
// Invalid TIF in order is treated as GTT
timeInForce: TimeInForce.GTT,
reduceOnly: false,
goodTilBlock: protocolTranslations.getGoodTilBlock(makerOrderProto)?.toString(),
goodTilBlockTime: protocolTranslations.getGoodTilBlockTime(makerOrderProto),
clientMetadata: makerOrderProto.clientMetadata.toString(),
updatedAt: defaultDateTime.toISO(),
updatedAtHeight: defaultHeight.toString(),
});

const takerOrderSize: string = '0.1'; // quantums in human = 1e1 * 1e-2 = 1e-1
await expectOrderInDatabase({
subaccountId: testConstants.defaultSubaccountId2,
clientId: '0',
size: takerOrderSize,
totalFilled,
price: takerPrice,
status: OrderStatus.FILLED, // orderSize == totalFilled so status is filled
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
side: protocolTranslations.protocolOrderSideToOrderSide(takerOrderProto.side),
orderFlags: takerOrderProto.orderId!.orderFlags.toString(),
// Invalid TIF in order is treated as GTT
timeInForce: TimeInForce.GTT,
reduceOnly: true,
goodTilBlock: protocolTranslations.getGoodTilBlock(takerOrderProto)?.toString(),
goodTilBlockTime: protocolTranslations.getGoodTilBlockTime(takerOrderProto),
clientMetadata: takerOrderProto.clientMetadata.toString(),
updatedAt: defaultDateTime.toISO(),
updatedAtHeight: defaultHeight.toString(),
});

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

const quoteAmount: string = '0.000000000000001'; // quote amount is price * fillAmount = 1e-14 * 1e-1 = 1e-15
await expectFillInDatabase({
subaccountId: testConstants.defaultSubaccountId,
clientId: '0',
liquidity: Liquidity.MAKER,
size: totalFilled,
price: makerPrice,
quoteAmount,
eventId,
transactionHash: defaultTxHash,
createdAt: defaultDateTime.toISO(),
createdAtHeight: defaultHeight,
type: FillType.LIMIT,
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
side: protocolTranslations.protocolOrderSideToOrderSide(makerOrderProto.side),
orderFlags: makerOrderProto.orderId!.orderFlags.toString(),
clientMetadata: makerOrderProto.clientMetadata.toString(),
fee: defaultMakerFee,
});
await expectFillInDatabase({
subaccountId: testConstants.defaultSubaccountId2,
clientId: '0',
liquidity: Liquidity.TAKER,
size: totalFilled,
price: makerPrice,
quoteAmount,
eventId,
transactionHash: defaultTxHash,
createdAt: defaultDateTime.toISO(),
createdAtHeight: defaultHeight,
type: FillType.LIMIT,
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
side: protocolTranslations.protocolOrderSideToOrderSide(takerOrderProto.side),
orderFlags: takerOrderProto.orderId!.orderFlags.toString(),
clientMetadata: takerOrderProto.clientMetadata.toString(),
fee: defaultTakerFee,
});

await Promise.all([
expectDefaultOrderAndFillSubaccountKafkaMessages(
producerSendMock,
eventId,
ORDER_FLAG_SHORT_TERM,
ORDER_FLAG_LONG_TERM,
testConstants.defaultPerpetualMarket3.id,
testConstants.defaultPerpetualMarket3.clobPairId,
),
expectDefaultTradeKafkaMessageFromTakerFillId(
producerSendMock,
eventId,
),
expectCandlesUpdated(),
expectStateFilledQuantums(
OrderTable.orderIdToUuid(makerOrderProto.orderId!),
orderFillEvent.totalFilledMaker.toString(),
),
expectStateFilledQuantums(
OrderTable.orderIdToUuid(takerOrderProto.orderId!),
orderFillEvent.totalFilledTaker.toString(),
),
]);
});

it.each([
[
undefined, // no maker order
],
[
IndexerOrder.fromPartial({ // no orderId
IndexerOrder.fromPartial({
orderId: undefined,
side: IndexerOrder_Side.SIDE_BUY,
quantums: 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
Converts the TimeInForce field from an IndexerOrder proto (https://github.com/dydxprotocol/v4-proto/blob/437f6d8/dydxprotocol/indexer/protocol/v1/clob.proto#L95)
to a TimeInForce enum in postgres.

Raise an exception if the input TimeInForce enum is not in the known enum values for TimeInForce.
*/
CREATE OR REPLACE FUNCTION dydx_from_protocol_time_in_force(tif jsonb) RETURNS text AS $$
BEGIN
CASE tif
-- Default behavior with UNRECOGNIZED = GTT (Good-Til-Time)
WHEN '-1'::jsonb THEN RETURN 'GTT';
-- Default behavior with TIME_IN_FORCE_UNSPECIFIED = GTT (Good-Til-Time)
WHEN '0'::jsonb THEN RETURN 'GTT';
WHEN '1'::jsonb THEN RETURN 'IOC';
WHEN '2'::jsonb THEN RETURN 'POST_ONLY';
WHEN '3'::jsonb THEN RETURN 'FOK';
ELSE RETURN 'GTT';
-- Removed for an emergency patch
-- ELSE RAISE EXCEPTION 'Unexpected TimeInForce from protocol %', tif;
END CASE;
END;
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
Loading