diff --git a/indexer/services/ender/__tests__/handlers/stateful-order/conditional-order-placement-handler.test.ts b/indexer/services/ender/__tests__/handlers/stateful-order/conditional-order-placement-handler.test.ts index edc47dfd76..f05747c6a8 100644 --- a/indexer/services/ender/__tests__/handlers/stateful-order/conditional-order-placement-handler.test.ts +++ b/indexer/services/ender/__tests__/handlers/stateful-order/conditional-order-placement-handler.test.ts @@ -45,7 +45,7 @@ import { producer } from '@dydxprotocol-indexer/kafka'; import { ConditionalOrderPlacementHandler } from '../../../src/handlers/stateful-order/conditional-order-placement-handler'; import { createPostgresFunctions } from '../../../src/helpers/postgres/postgres-functions'; -describe('conditionalOrderPlacementHandler', () => { +describe('conditional-order-placement-handler', () => { beforeAll(async () => { await dbHelpers.migrate(); await createPostgresFunctions(); diff --git a/indexer/services/ender/__tests__/handlers/stateful-order/conditional-order-triggered-handler.test.ts b/indexer/services/ender/__tests__/handlers/stateful-order/conditional-order-triggered-handler.test.ts index cb5ad98721..193e646ac7 100644 --- a/indexer/services/ender/__tests__/handlers/stateful-order/conditional-order-triggered-handler.test.ts +++ b/indexer/services/ender/__tests__/handlers/stateful-order/conditional-order-triggered-handler.test.ts @@ -39,7 +39,7 @@ import { ConditionalOrderTriggeredHandler } from '../../../src/handlers/stateful import { defaultPerpetualMarket } from '@dydxprotocol-indexer/postgres/build/__tests__/helpers/constants'; import { createPostgresFunctions } from '../../../src/helpers/postgres/postgres-functions'; -describe('conditionalOrderTriggeredHandler', () => { +describe('conditional-order-triggered-handler', () => { beforeAll(async () => { await dbHelpers.migrate(); await createPostgresFunctions(); diff --git a/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-placement-handler.test.ts b/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-placement-handler.test.ts index b65e6fe6f6..11ef476a38 100644 --- a/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-placement-handler.test.ts +++ b/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-placement-handler.test.ts @@ -45,7 +45,7 @@ import { producer } from '@dydxprotocol-indexer/kafka'; import { ORDER_FLAG_LONG_TERM } from '@dydxprotocol-indexer/v4-proto-parser'; import { createPostgresFunctions } from '../../../src/helpers/postgres/postgres-functions'; -describe('statefulOrderPlacementHandler', () => { +describe('stateful-order-placement-handler', () => { beforeAll(async () => { await dbHelpers.migrate(); await createPostgresFunctions(); diff --git a/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-removal-handler.test.ts b/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-removal-handler.test.ts index 565b361057..7e55d6553d 100644 --- a/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-removal-handler.test.ts +++ b/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-removal-handler.test.ts @@ -38,7 +38,7 @@ import { STATEFUL_ORDER_ORDER_FILL_EVENT_TYPE } from '../../../src/constants'; import { producer } from '@dydxprotocol-indexer/kafka'; import { createPostgresFunctions } from '../../../src/helpers/postgres/postgres-functions'; -describe('statefulOrderRemovalHandler', () => { +describe('stateful-order-removal-handler', () => { beforeAll(async () => { await dbHelpers.migrate(); await createPostgresFunctions(); diff --git a/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-replacement-handler.test.ts b/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-replacement-handler.test.ts new file mode 100644 index 0000000000..088f5773dd --- /dev/null +++ b/indexer/services/ender/__tests__/handlers/stateful-order/stateful-order-replacement-handler.test.ts @@ -0,0 +1,249 @@ +import { + dbHelpers, + OrderFromDatabase, + OrderSide, + OrderStatus, + OrderTable, + OrderType, + perpetualMarketRefresher, + protocolTranslations, + SubaccountTable, + testConstants, + testMocks, +} from '@dydxprotocol-indexer/postgres'; +import { + OffChainUpdateV1, + IndexerOrder, + OrderPlaceV1_OrderPlacementStatus, + StatefulOrderEventV1, +} from '@dydxprotocol-indexer/v4-protos'; +import { KafkaMessage } from 'kafkajs'; +import { onMessage } from '../../../src/lib/on-message'; +import { + defaultDateTime, + defaultHeight, + defaultMakerOrder, + defaultOrderId2, + defaultPreviousHeight, +} from '../../helpers/constants'; +import { createKafkaMessageFromStatefulOrderEvent } from '../../helpers/kafka-helpers'; +import { updateBlockCache } from '../../../src/caches/block-cache'; +import { + expectVulcanKafkaMessage, +} from '../../helpers/indexer-proto-helpers'; +import { getPrice, getSize } from '../../../src/lib/helper'; +import { producer } from '@dydxprotocol-indexer/kafka'; +import { ORDER_FLAG_LONG_TERM } from '@dydxprotocol-indexer/v4-proto-parser'; +import { createPostgresFunctions } from '../../../src/helpers/postgres/postgres-functions'; +import { logger } from '@dydxprotocol-indexer/base'; + +describe('stateful-order-replacement-handler', () => { + beforeAll(async () => { + await dbHelpers.migrate(); + await createPostgresFunctions(); + }); + + beforeEach(async () => { + await testMocks.seedData(); + updateBlockCache(defaultPreviousHeight); + await perpetualMarketRefresher.updatePerpetualMarkets(); + producerSendMock = jest.spyOn(producer, 'send'); + jest.spyOn(logger, 'error'); + }); + + afterEach(async () => { + await dbHelpers.clearData(); + jest.clearAllMocks(); + }); + + afterAll(async () => { + await dbHelpers.teardown(); + jest.resetAllMocks(); + }); + + const goodTilBlockTime: number = 123; + const defaultOldOrder: IndexerOrder = { + ...defaultMakerOrder, + orderId: { + ...defaultMakerOrder.orderId!, + orderFlags: ORDER_FLAG_LONG_TERM, + }, + goodTilBlock: undefined, + goodTilBlockTime, + }; + const defaultNewOrder: IndexerOrder = { + ...defaultMakerOrder, + orderId: defaultOrderId2, + quantums: defaultOldOrder.quantums.mul(2), + goodTilBlock: undefined, + goodTilBlockTime, + }; + + // replacing order with a different order ID + const defaultStatefulOrderReplacementEvent: StatefulOrderEventV1 = { + orderReplacement: { + oldOrderId: defaultOldOrder.orderId!, + order: defaultNewOrder, + }, + }; + + // replacing order with the same order ID + const statefulOrderReplacementEventSameId: StatefulOrderEventV1 = { + orderReplacement: { + oldOrderId: defaultOldOrder.orderId!, + order: { + ...defaultNewOrder, + orderId: defaultOldOrder.orderId, + }, + }, + }; + + const oldOrderUuid: string = OrderTable.orderIdToUuid(defaultOldOrder.orderId!); + const newOrderUuid: string = OrderTable.orderIdToUuid(defaultNewOrder.orderId!); + let producerSendMock: jest.SpyInstance; + + it.each([ + ['stateful order replacement as txn event', defaultStatefulOrderReplacementEvent, 0], + ['stateful order replacement as txn event', defaultStatefulOrderReplacementEvent, 0], + ['stateful order replacement as block event', defaultStatefulOrderReplacementEvent, -1], + ['stateful order replacement as block event', defaultStatefulOrderReplacementEvent, -1], + ])('successfully replaces order with %s', async ( + _name: string, + statefulOrderEvent: StatefulOrderEventV1, + transactionIndex: number, + ) => { + await OrderTable.create({ + ...testConstants.defaultOrder, + clientId: '0', + orderFlags: ORDER_FLAG_LONG_TERM.toString(), + }); + const kafkaMessage: KafkaMessage = createKafkaMessageFromStatefulOrderEvent( + statefulOrderEvent, + transactionIndex, + ); + + await onMessage(kafkaMessage); + + const oldOrder: OrderFromDatabase | undefined = await OrderTable.findById(oldOrderUuid); + expect(oldOrder).toBeDefined(); + expect(oldOrder).toEqual(expect.objectContaining({ + status: OrderStatus.CANCELED, + updatedAt: defaultDateTime.toISO(), + updatedAtHeight: defaultHeight.toString(), + })); + + const newOrder: OrderFromDatabase | undefined = await OrderTable.findById(newOrderUuid); + expect(newOrder).toEqual({ + id: newOrderUuid, + subaccountId: SubaccountTable.subaccountIdToUuid(defaultNewOrder.orderId!.subaccountId!), + clientId: defaultNewOrder.orderId!.clientId.toString(), + clobPairId: defaultNewOrder.orderId!.clobPairId.toString(), + side: OrderSide.BUY, + size: getSize(defaultNewOrder, testConstants.defaultPerpetualMarket), + totalFilled: '0', + price: getPrice(defaultNewOrder, testConstants.defaultPerpetualMarket), + type: OrderType.LIMIT, // TODO: Add additional order types once we support + status: OrderStatus.OPEN, + timeInForce: protocolTranslations.protocolOrderTIFToTIF(defaultNewOrder.timeInForce), + reduceOnly: defaultNewOrder.reduceOnly, + orderFlags: defaultNewOrder.orderId!.orderFlags.toString(), + goodTilBlock: null, + goodTilBlockTime: protocolTranslations.getGoodTilBlockTime(defaultNewOrder), + createdAtHeight: '3', + clientMetadata: '0', + triggerPrice: null, + updatedAt: defaultDateTime.toISO(), + updatedAtHeight: defaultHeight.toString(), + }); + + const expectedOffchainUpdate: OffChainUpdateV1 = { + orderReplace: { + oldOrderId: defaultOldOrder.orderId!, + order: defaultNewOrder, + placementStatus: OrderPlaceV1_OrderPlacementStatus.ORDER_PLACEMENT_STATUS_OPENED, + }, + }; + expectVulcanKafkaMessage({ + producerSendMock, + orderId: defaultNewOrder.orderId!, + offchainUpdate: expectedOffchainUpdate, + headers: { message_received_timestamp: kafkaMessage.timestamp, event_type: 'StatefulOrderReplacement' }, + }); + }); + + it('successfully replaces order where old order ID is the same as new order ID', async () => { + // create existing order with the same ID as the one we will cancel and place again + await OrderTable.create({ + ...testConstants.defaultOrderGoodTilBlockTime, + clientId: '0', + }); + + const kafkaMessage: KafkaMessage = createKafkaMessageFromStatefulOrderEvent( + statefulOrderReplacementEventSameId, + ); + + await onMessage(kafkaMessage); + const order: OrderFromDatabase | undefined = await OrderTable.findById(oldOrderUuid); + expect(order).toEqual({ + id: oldOrderUuid, + subaccountId: SubaccountTable.subaccountIdToUuid(defaultOldOrder.orderId!.subaccountId!), + clientId: defaultNewOrder.orderId!.clientId.toString(), + clobPairId: defaultNewOrder.orderId!.clobPairId.toString(), + side: OrderSide.BUY, + size: getSize(defaultNewOrder, testConstants.defaultPerpetualMarket), + totalFilled: '0', + price: getPrice(defaultNewOrder, testConstants.defaultPerpetualMarket), + type: OrderType.LIMIT, // TODO: Add additional order types once we support + status: OrderStatus.OPEN, + timeInForce: protocolTranslations.protocolOrderTIFToTIF(defaultNewOrder.timeInForce), + reduceOnly: defaultNewOrder.reduceOnly, + orderFlags: defaultNewOrder.orderId!.orderFlags.toString(), + goodTilBlock: null, + goodTilBlockTime: protocolTranslations.getGoodTilBlockTime(defaultNewOrder), + createdAtHeight: '3', + clientMetadata: '0', + triggerPrice: null, + updatedAt: defaultDateTime.toISO(), + updatedAtHeight: defaultHeight.toString(), + }); + }); + + it('logs error if old order ID does not exist in DB', async () => { + const kafkaMessage: KafkaMessage = createKafkaMessageFromStatefulOrderEvent( + defaultStatefulOrderReplacementEvent, + ); + + await onMessage(kafkaMessage); + + expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ + at: 'StatefulOrderReplacementHandler#handleOrderReplacement', + message: 'StatefulOrderReplacementHandler#Unable to cancel replaced order because orderId not found', + orderId: defaultStatefulOrderReplacementEvent.orderReplacement!.oldOrderId, + })); + + // We still expect new order to be created + const newOrder: OrderFromDatabase | undefined = await OrderTable.findById(newOrderUuid); + expect(newOrder).toEqual({ + id: newOrderUuid, + subaccountId: SubaccountTable.subaccountIdToUuid(defaultNewOrder.orderId!.subaccountId!), + clientId: defaultNewOrder.orderId!.clientId.toString(), + clobPairId: defaultNewOrder.orderId!.clobPairId.toString(), + side: OrderSide.BUY, + size: getSize(defaultNewOrder, testConstants.defaultPerpetualMarket), + totalFilled: '0', + price: getPrice(defaultNewOrder, testConstants.defaultPerpetualMarket), + type: OrderType.LIMIT, // TODO: Add additional order types once we support + status: OrderStatus.OPEN, + timeInForce: protocolTranslations.protocolOrderTIFToTIF(defaultNewOrder.timeInForce), + reduceOnly: defaultNewOrder.reduceOnly, + orderFlags: defaultNewOrder.orderId!.orderFlags.toString(), + goodTilBlock: null, + goodTilBlockTime: protocolTranslations.getGoodTilBlockTime(defaultNewOrder), + createdAtHeight: '3', + clientMetadata: '0', + triggerPrice: null, + updatedAt: defaultDateTime.toISO(), + updatedAtHeight: defaultHeight.toString(), + }); + }); +}); diff --git a/indexer/services/ender/__tests__/helpers/constants.ts b/indexer/services/ender/__tests__/helpers/constants.ts index 5dac1683d5..c5644de70f 100644 --- a/indexer/services/ender/__tests__/helpers/constants.ts +++ b/indexer/services/ender/__tests__/helpers/constants.ts @@ -448,6 +448,20 @@ export const defaultLongTermOrderPlacementEvent: StatefulOrderEventV1 = { }, }; +export const defaultStatefulOrderReplacementEvent: StatefulOrderEventV1 = { + orderReplacement: { + oldOrderId: defaultOrderId2, + order: { + ...defaultMakerOrder, + orderId: { + ...defaultMakerOrder.orderId!, + orderFlags: ORDER_FLAG_LONG_TERM, + }, + goodTilBlockTime: 123, + }, + }, +}; + export const defaultTradingRewardsEvent: TradingRewardsEventV1 = { tradingRewards: [ { diff --git a/indexer/services/ender/__tests__/validators/stateful-order-validator.test.ts b/indexer/services/ender/__tests__/validators/stateful-order-validator.test.ts index b6d074706f..08ee708537 100644 --- a/indexer/services/ender/__tests__/validators/stateful-order-validator.test.ts +++ b/indexer/services/ender/__tests__/validators/stateful-order-validator.test.ts @@ -18,6 +18,7 @@ import { defaultOrderId, defaultStatefulOrderPlacementEvent, defaultStatefulOrderRemovalEvent, + defaultStatefulOrderReplacementEvent, defaultTime, defaultTxHash, } from '../helpers/constants'; @@ -42,6 +43,7 @@ describe('stateful-order-validator', () => { ['conditional order placement', defaultConditionalOrderPlacementEvent], ['conditional order triggered', defaultConditionalOrderTriggeredEvent], ['long term order placement', defaultLongTermOrderPlacementEvent], + ['stateful order replacement', defaultStatefulOrderReplacementEvent], ])('does not throw error on valid %s', (_message: string, event: StatefulOrderEventV1) => { const validator: StatefulOrderValidator = new StatefulOrderValidator( event, @@ -59,7 +61,7 @@ describe('stateful-order-validator', () => { 'does not contain any event', {}, 'One of orderPlace, orderRemoval, conditionalOrderPlacement, ' + - 'conditionalOrderTriggered, longTermOrderPlacement must be defined in StatefulOrderEvent', + 'conditionalOrderTriggered, longTermOrderPlacement, orderReplacement must be defined in StatefulOrderEvent', ], // TODO(IND-334): Remove tests after deprecating StatefulOrderPlacement events diff --git a/indexer/services/ender/src/handlers/stateful-order/stateful-order-replacement-handler.ts b/indexer/services/ender/src/handlers/stateful-order/stateful-order-replacement-handler.ts new file mode 100644 index 0000000000..b392443bec --- /dev/null +++ b/indexer/services/ender/src/handlers/stateful-order/stateful-order-replacement-handler.ts @@ -0,0 +1,79 @@ +import { logger, stats } from '@dydxprotocol-indexer/base'; +import { + OrderTable, +} from '@dydxprotocol-indexer/postgres'; +import { getOrderIdHash } from '@dydxprotocol-indexer/v4-proto-parser'; +import { + IndexerOrder, + IndexerOrderId, + OffChainUpdateV1, + OrderPlaceV1_OrderPlacementStatus, + StatefulOrderEventV1, +} from '@dydxprotocol-indexer/v4-protos'; +import * as pg from 'pg'; + +import config from '../../config'; +import { ConsolidatedKafkaEvent } from '../../lib/types'; +import { AbstractStatefulOrderHandler } from '../abstract-stateful-order-handler'; + +export class StatefulOrderReplacementHandler + extends AbstractStatefulOrderHandler { + eventType: string = 'StatefulOrderEvent'; + + private getOrderId(): string { + const orderId = OrderTable.orderIdToUuid(this.event.orderReplacement!.order!.orderId!); + return orderId; + } + + public getParallelizationIds(): string[] { + // Stateful Order Events with the same orderId + return this.getParallelizationIdsFromOrderId(this.getOrderId()); + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async internalHandle(resultRow: pg.QueryResultRow): Promise { + const oldOrderId = this.event.orderReplacement!.oldOrderId!; + const order = this.event.orderReplacement!.order!; + + if (resultRow.errors != null) { + // We expect the first error to be related to order replacement. + // If more errors are added to `dydx_stateful_order_handler.sql`, this may need to be updated + const errorMessage = resultRow.errors[0]; + if (errorMessage.includes('StatefulOrderReplacementHandler#')) { + logger.error({ + at: 'StatefulOrderReplacementHandler#handleOrderReplacement', + message: errorMessage, + orderId: oldOrderId, + }); + stats.increment(`${config.SERVICE_NAME}.handle_stateful_order_replacement.old_order_id_not_found_in_db`, 1); + } + } + + return this.createKafkaEvents(oldOrderId, order); + } + + private createKafkaEvents( + oldOrderId: IndexerOrderId, + order: IndexerOrder, + ): ConsolidatedKafkaEvent[] { + const kafkaEvents: ConsolidatedKafkaEvent[] = []; + + const offChainUpdate: OffChainUpdateV1 = OffChainUpdateV1.fromPartial({ + orderReplace: { + oldOrderId, + order, + placementStatus: OrderPlaceV1_OrderPlacementStatus.ORDER_PLACEMENT_STATUS_OPENED, + }, + }); + kafkaEvents.push(this.generateConsolidatedVulcanKafkaEvent( + getOrderIdHash(order.orderId!), + offChainUpdate, + { + message_received_timestamp: this.messageReceivedTimestamp, + event_type: 'StatefulOrderReplacement', + }, + )); + + return kafkaEvents; + } +} diff --git a/indexer/services/ender/src/scripts/handlers/dydx_stateful_order_handler.sql b/indexer/services/ender/src/scripts/handlers/dydx_stateful_order_handler.sql index e4962c07a3..1d5eb9efb3 100644 --- a/indexer/services/ender/src/scripts/handlers/dydx_stateful_order_handler.sql +++ b/indexer/services/ender/src/scripts/handlers/dydx_stateful_order_handler.sql @@ -21,10 +21,43 @@ DECLARE perpetual_market_record perpetual_markets%ROWTYPE; order_record orders%ROWTYPE; subaccount_record subaccounts%ROWTYPE; + + errors_response jsonb[]; /* Array of error responses to return to the client. */ BEGIN + /* For order replacement, remove old order first and don't return immediately */ + IF event_data->'orderReplacement' IS NOT NULL THEN + order_id = event_data->'orderReplacement'->'oldOrderId'; + order_record."status" = 'CANCELED'; + + clob_pair_id = (order_id->'clobPairId')::bigint; + perpetual_market_record = dydx_get_perpetual_market_for_clob_pair(clob_pair_id); + + subaccount_id = dydx_uuid_from_subaccount_id(order_id->'subaccountId'); + SELECT * INTO subaccount_record FROM subaccounts WHERE "id" = subaccount_id; + IF NOT FOUND THEN + RAISE EXCEPTION 'Subaccount for order not found: %', order_; + END IF; + + order_record."id" = dydx_uuid_from_order_id(order_id); + order_record."updatedAt" = block_time; + order_record."updatedAtHeight" = block_height; + UPDATE orders + SET + "status" = order_record."status", + "updatedAt" = order_record."updatedAt", + "updatedAtHeight" = order_record."updatedAtHeight" + WHERE "id" = order_record."id" + RETURNING * INTO order_record; + + IF NOT FOUND THEN + errors_response = array_append(errors_response, '"StatefulOrderReplacementHandler#Unable to cancel replaced order because orderId not found"'::jsonb); + END IF; + END IF; + order_record := NULL; /* Reset order_record so the order place below doesn't carry over any values set above. */ + /** TODO(IND-334): Remove after deprecating StatefulOrderPlacementEvent. */ - IF event_data->'orderPlace' IS NOT NULL OR event_data->'longTermOrderPlacement' IS NOT NULL OR event_data->'conditionalOrderPlacement' IS NOT NULL THEN - order_ = coalesce(event_data->'orderPlace'->'order', event_data->'longTermOrderPlacement'->'order', event_data->'conditionalOrderPlacement'->'order'); + IF event_data->'orderPlace' IS NOT NULL OR event_data->'longTermOrderPlacement' IS NOT NULL OR event_data->'conditionalOrderPlacement' IS NOT NULL OR event_data->'orderReplacement' IS NOT NULL THEN + order_ = coalesce(event_data->'orderPlace'->'order', event_data->'longTermOrderPlacement'->'order', event_data->'conditionalOrderPlacement'->'order', event_data->'orderReplacement'->'order'); clob_pair_id = (order_->'orderId'->'clobPairId')::bigint; perpetual_market_record = dydx_get_perpetual_market_for_clob_pair(clob_pair_id); @@ -94,7 +127,9 @@ BEGIN 'order', dydx_to_jsonb(order_record), 'perpetual_market', - dydx_to_jsonb(perpetual_market_record) + dydx_to_jsonb(perpetual_market_record), + 'errors', + to_jsonb(errors_response) ); ELSIF event_data->'conditionalOrderTriggered' IS NOT NULL OR event_data->'orderRemoval' IS NOT NULL THEN CASE @@ -136,10 +171,12 @@ BEGIN 'perpetual_market', dydx_to_jsonb(perpetual_market_record), 'subaccount', - dydx_to_jsonb(subaccount_record) + dydx_to_jsonb(subaccount_record), + 'errors', + to_jsonb(errors_response) ); ELSE - RAISE EXCEPTION 'Unkonwn sub-event type %', event_data; + RAISE EXCEPTION 'Unknown sub-event type %', event_data; END IF; END; $$ LANGUAGE plpgsql; diff --git a/indexer/services/ender/src/validators/stateful-order-validator.ts b/indexer/services/ender/src/validators/stateful-order-validator.ts index 8507fb6ca7..bb50922abc 100644 --- a/indexer/services/ender/src/validators/stateful-order-validator.ts +++ b/indexer/services/ender/src/validators/stateful-order-validator.ts @@ -2,6 +2,7 @@ import { ORDER_FLAG_CONDITIONAL, ORDER_FLAG_LONG_TERM } from '@dydxprotocol-inde import { IndexerTendermintEvent, IndexerOrder, + IndexerOrderId, StatefulOrderEventV1, StatefulOrderEventV1_StatefulOrderPlacementV1, OrderRemovalReason, @@ -9,6 +10,7 @@ import { StatefulOrderEventV1_ConditionalOrderPlacementV1, StatefulOrderEventV1_ConditionalOrderTriggeredV1, StatefulOrderEventV1_LongTermOrderPlacementV1, + StatefulOrderEventV1_LongTermOrderReplacementV1, IndexerOrder_ConditionType, } from '@dydxprotocol-indexer/v4-protos'; import Long from 'long'; @@ -18,6 +20,7 @@ import { ConditionalOrderPlacementHandler } from '../handlers/stateful-order/con import { ConditionalOrderTriggeredHandler } from '../handlers/stateful-order/conditional-order-triggered-handler'; import { StatefulOrderPlacementHandler } from '../handlers/stateful-order/stateful-order-placement-handler'; import { StatefulOrderRemovalHandler } from '../handlers/stateful-order/stateful-order-removal-handler'; +import { StatefulOrderReplacementHandler } from '../handlers/stateful-order/stateful-order-replacement-handler'; import { validateOrderAndReturnErrorMessage, validateOrderIdAndReturnErrorMessage } from './helpers'; import { Validator } from './validator'; @@ -28,11 +31,12 @@ export class StatefulOrderValidator extends Validator { this.event.orderRemoval === undefined && this.event.conditionalOrderPlacement === undefined && this.event.conditionalOrderTriggered === undefined && - this.event.longTermOrderPlacement === undefined + this.event.longTermOrderPlacement === undefined && + this.event.orderReplacement === undefined ) { return this.logAndThrowParseMessageError( 'One of orderPlace, orderRemoval, conditionalOrderPlacement, conditionalOrderTriggered, ' + - 'longTermOrderPlacement must be defined in StatefulOrderEvent', + 'longTermOrderPlacement, orderReplacement must be defined in StatefulOrderEvent', { event: this.event }, ); } @@ -44,8 +48,10 @@ export class StatefulOrderValidator extends Validator { this.validateConditionalOrderPlacement(this.event.conditionalOrderPlacement); } else if (this.event.conditionalOrderTriggered !== undefined) { this.validateConditionalOrderTriggered(this.event.conditionalOrderTriggered); - } else { // longTermOrderPlacement + } else if (this.event.longTermOrderPlacement !== undefined) { this.validateLongTermOrderPlacement(this.event.longTermOrderPlacement!); + } else if (this.event.orderReplacement !== undefined) { + this.validateOrderReplacement(this.event.orderReplacement!); } } @@ -193,6 +199,45 @@ export class StatefulOrderValidator extends Validator { } } + private validateOrderReplacement( + orderReplacement: StatefulOrderEventV1_LongTermOrderReplacementV1, + ): void { + const oldOrderId: IndexerOrderId | undefined = orderReplacement.oldOrderId; + const order: IndexerOrder | undefined = orderReplacement.order; + + if (oldOrderId === undefined) { + return this.logAndThrowParseMessageError( + 'StatefulOrderEvent order replacement must contain an oldOrderId', + { event: this.event }, + ); + } + const orderIdErrorMessage: string | undefined = validateOrderIdAndReturnErrorMessage( + oldOrderId, + ); + if (orderIdErrorMessage !== undefined) { + return this.logAndThrowParseMessageError( + `StatefulOrderEvent order replacement oldOrderId error: ${orderIdErrorMessage}`, + { event: this.event }, + ); + } + + if (order === undefined) { + return this.logAndThrowParseMessageError( + 'StatefulOrderEvent order replacement must contain an order', + { event: this.event }, + ); + } + + this.validateStatefulOrder(order); + + if (order.orderId!.orderFlags !== ORDER_FLAG_LONG_TERM) { + return this.logAndThrowParseMessageError( + `StatefulOrderEvent order replacement must have order flag ${ORDER_FLAG_LONG_TERM}`, + { event: this.event }, + ); + } + } + public getHandlerInitializer() : HandlerInitializer | undefined { if (this.event.orderPlace !== undefined) { return StatefulOrderPlacementHandler; @@ -204,6 +249,8 @@ export class StatefulOrderValidator extends Validator { return ConditionalOrderTriggeredHandler; } else if (this.event.longTermOrderPlacement !== undefined) { return StatefulOrderPlacementHandler; + } else if (this.event.orderReplacement !== undefined) { + return StatefulOrderReplacementHandler; } return undefined; }