diff --git a/indexer/packages/v4-proto-parser/__tests__/order-helpers.test.ts b/indexer/packages/v4-proto-parser/__tests__/order-helpers.test.ts index bd8d3f811a..90468e4cbd 100644 --- a/indexer/packages/v4-proto-parser/__tests__/order-helpers.test.ts +++ b/indexer/packages/v4-proto-parser/__tests__/order-helpers.test.ts @@ -1,5 +1,5 @@ import { IndexerOrderId } from '@dydxprotocol-indexer/v4-protos'; -import { getOrderIdHash, isStatefulOrder } from '../src/order-helpers'; +import { getOrderIdHash, isLongTermOrder, isStatefulOrder } from '../src/order-helpers'; import { ORDER_FLAG_CONDITIONAL, ORDER_FLAG_LONG_TERM, ORDER_FLAG_SHORT_TERM } from '../src'; describe('getOrderIdHash', () => { @@ -65,3 +65,23 @@ describe('isStatefulOrder', () => { expect(isStatefulOrder(flag)).toEqual(isStateful); }); }); + +describe('isLongTermOrder', () => { + it.each([ + [ORDER_FLAG_SHORT_TERM.toString(), 'string', false], + ['4', 'string', false], + [ORDER_FLAG_CONDITIONAL.toString(), 'string', false], + [ORDER_FLAG_LONG_TERM.toString(), 'string', true], + [ORDER_FLAG_SHORT_TERM, 'number', false], + [3, 'number', false], + [ORDER_FLAG_CONDITIONAL, 'number', false], + [ORDER_FLAG_LONG_TERM, 'number', true], + ['abc', 'string', false], + ])('Checks if flag %s with type %s is a long term order', ( + flag: number | string, + _type: string, + isLongTerm: boolean, + ) => { + expect(isLongTermOrder(flag)).toEqual(isLongTerm); + }); +}); diff --git a/indexer/packages/v4-proto-parser/src/order-helpers.ts b/indexer/packages/v4-proto-parser/src/order-helpers.ts index beeb567ac1..6724942941 100644 --- a/indexer/packages/v4-proto-parser/src/order-helpers.ts +++ b/indexer/packages/v4-proto-parser/src/order-helpers.ts @@ -24,6 +24,15 @@ export function isStatefulOrder(orderFlag: number | String): boolean { return numberOrderFlag === ORDER_FLAG_CONDITIONAL || numberOrderFlag === ORDER_FLAG_LONG_TERM; } +export function isLongTermOrder(orderFlag: number | String): boolean { + const numberOrderFlag: number = Number(orderFlag); + // A string that is not a number will be converted to NaN, and should return false. + if (Number.isNaN(numberOrderFlag)) { + return false; + } + return numberOrderFlag === ORDER_FLAG_LONG_TERM; +} + export function requiresImmediateExecution(tif: IndexerOrder_TimeInForce): boolean { return ( tif === IndexerOrder_TimeInForce.TIME_IN_FORCE_FILL_OR_KILL || diff --git a/indexer/services/vulcan/__tests__/handlers/order-place-handler.test.ts b/indexer/services/vulcan/__tests__/handlers/order-place-handler.test.ts index ab43b71d13..d26015e30e 100644 --- a/indexer/services/vulcan/__tests__/handlers/order-place-handler.test.ts +++ b/indexer/services/vulcan/__tests__/handlers/order-place-handler.test.ts @@ -61,7 +61,8 @@ import { onMessage } from '../../src/lib/on-message'; import { expectCanceledOrderStatus, expectOpenOrderIds, handleInitialOrderPlace } from '../helpers/helpers'; import { expectOffchainUpdateMessage, expectWebsocketOrderbookMessage, expectWebsocketSubaccountMessage } from '../helpers/websocket-helpers'; import { OrderbookSide } from '../../src/lib/types'; -import { getOrderIdHash, isStatefulOrder } from '@dydxprotocol-indexer/v4-proto-parser'; +import { getOrderIdHash, isLongTermOrder, isStatefulOrder } from '@dydxprotocol-indexer/v4-proto-parser'; +import config from '../../src/config'; jest.mock('@dydxprotocol-indexer/base', () => ({ ...jest.requireActual('@dydxprotocol-indexer/base'), @@ -82,6 +83,10 @@ describe('order-place-handler', () => { jest.useRealTimers(); }); + afterEach(() => { + config.SEND_SUBACCOUNT_WEBSOCKET_MESSAGE_FOR_STATEFUL_ORDERS = true; + }); + describe('handle', () => { const replacementOrder: IndexerOrder = { ...redisTestConstants.defaultOrder, @@ -830,19 +835,38 @@ describe('order-place-handler', () => { redisTestConstants.defaultOrderGoodTilBlockTime, redisTestConstants.defaultRedisOrderGoodTilBlockTime, dbOrderGoodTilBlockTime, + false, ], [ 'conditional', redisTestConstants.defaultConditionalOrder, redisTestConstants.defaultRedisOrderConditional, dbConditionalOrder, + false, + ], + [ + 'good-til-block-time', + redisTestConstants.defaultOrderGoodTilBlockTime, + redisTestConstants.defaultRedisOrderGoodTilBlockTime, + dbOrderGoodTilBlockTime, + true, + ], + [ + 'conditional', + redisTestConstants.defaultConditionalOrder, + redisTestConstants.defaultRedisOrderConditional, + dbConditionalOrder, + true, ], ])('handles order place with OPEN placement status, exists initially (with %s)', async ( _name: string, orderToPlace: IndexerOrder, expectedRedisOrder: RedisOrder, placedOrder: OrderFromDatabase, + sendSubaccountWebsocketMessage: boolean, ) => { + // eslint-disable-next-line max-len + config.SEND_SUBACCOUNT_WEBSOCKET_MESSAGE_FOR_STATEFUL_ORDERS = sendSubaccountWebsocketMessage; synchronizeWrapBackgroundTask(wrapBackgroundTask); const producerSendSpy: jest.SpyInstance = jest.spyOn(producer, 'send').mockReturnThis(); // Handle the order place event for the initial order with BEST_EFFORT_OPENED @@ -883,7 +907,9 @@ describe('order-place-handler', () => { testConstants.defaultPerpetualMarket, APIOrderStatusEnum.OPEN, // Subaccount messages should be sent for stateful order with OPEN status - true, + !( + isLongTermOrder(expectedRedisOrder.order!.orderId!.orderFlags) && + !sendSubaccountWebsocketMessage), ); expect(logger.error).not.toHaveBeenCalled(); diff --git a/indexer/services/vulcan/src/config.ts b/indexer/services/vulcan/src/config.ts index 82e21bc18b..8ff39ab975 100644 --- a/indexer/services/vulcan/src/config.ts +++ b/indexer/services/vulcan/src/config.ts @@ -30,6 +30,9 @@ export const configSchema = { SEND_WEBSOCKET_MESSAGES: parseBoolean({ default: true, }), + SEND_SUBACCOUNT_WEBSOCKET_MESSAGE_FOR_STATEFUL_ORDERS: parseBoolean({ + default: true, + }), }; export default parseSchema(configSchema); diff --git a/indexer/services/vulcan/src/handlers/order-place-handler.ts b/indexer/services/vulcan/src/handlers/order-place-handler.ts index 874889af84..1d36ad7de0 100644 --- a/indexer/services/vulcan/src/handlers/order-place-handler.ts +++ b/indexer/services/vulcan/src/handlers/order-place-handler.ts @@ -19,6 +19,7 @@ import { import { getOrderIdHash, isStatefulOrder, + isLongTermOrder, ORDER_FLAG_SHORT_TERM, requiresImmediateExecution, } from '@dydxprotocol-indexer/v4-proto-parser'; @@ -120,7 +121,12 @@ export class OrderPlaceHandler extends Handler { // TODO(CLOB-597): Remove this logic and log erorrs once best-effort-open is not sent for // stateful orders in the protocol - if (this.shouldSendSubaccountMessage(orderPlace, placeOrderResult, placementStatus)) { + if (this.shouldSendSubaccountMessage( + orderPlace, + placeOrderResult, + placementStatus, + redisOrder, + )) { // TODO(IND-171): Determine whether we should always be sending a message, even when the cache // isn't updated. // For stateful and conditional orders, look the order up in the db for the createdAtHeight @@ -273,7 +279,15 @@ export class OrderPlaceHandler extends Handler { orderPlace: OrderPlaceV1, placeOrderResult: PlaceOrderResult, placementStatus: OrderPlaceV1_OrderPlacementStatus, + redisOrder: RedisOrder, ): boolean { + if ( + isLongTermOrder(redisOrder.order!.orderId!.orderFlags) && + !config.SEND_SUBACCOUNT_WEBSOCKET_MESSAGE_FOR_STATEFUL_ORDERS + ) { + return false; + } + const orderFlags: number = orderPlace.order!.orderId!.orderFlags; const status: OrderPlaceV1_OrderPlacementStatus = orderPlace.placementStatus; // Best-effort-opened status should only be sent for short-term orders