diff --git a/indexer/packages/postgres/src/stores/fill-table.ts b/indexer/packages/postgres/src/stores/fill-table.ts index 536000ea640..3a3a8fee807 100644 --- a/indexer/packages/postgres/src/stores/fill-table.ts +++ b/indexer/packages/postgres/src/stores/fill-table.ts @@ -14,6 +14,7 @@ import { getUuid } from '../helpers/uuid'; import { getSubaccountQueryForParent } from '../lib/parent-subaccount-helpers'; import FillModel from '../models/fill-model'; import { + CostOfFills, FillColumns, FillCreateObject, FillFromDatabase, @@ -26,7 +27,6 @@ import { OrderedFillsWithFundingIndices, Ordering, OrderSide, - CostOfFills, PaginationFromDatabase, QueryableField, QueryConfig, @@ -96,6 +96,8 @@ export async function findAll( side, liquidity, type, + includeTypes, + excludeTypes, clobPairId, eventId, transactionHash, @@ -165,6 +167,14 @@ export async function findAll( baseQuery = baseQuery.where(FillColumns.type, type); } + if (includeTypes !== undefined && includeTypes.length > 0) { + baseQuery = baseQuery.whereIn(FillColumns.type, includeTypes); + } + + if (excludeTypes !== undefined && excludeTypes.length > 0) { + baseQuery = baseQuery.whereNotIn(FillColumns.type, excludeTypes); + } + if (clobPairId !== undefined) { baseQuery = baseQuery.where(FillColumns.clobPairId, clobPairId); } @@ -246,8 +256,8 @@ export async function update( ): Promise { const fill = await FillModel.query( Transaction.get(options.txId), - // TODO fix expression typing so we dont have to use any - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // TODO fix expression typing so we dont have to use any + // eslint-disable-next-line @typescript-eslint/no-explicit-any ).findById(fields.id).patch(fields as any).returning('*'); // The objection types mistakenly think the query returns an array of fills. return fill as unknown as (FillFromDatabase | undefined); diff --git a/indexer/packages/postgres/src/stores/order-table.ts b/indexer/packages/postgres/src/stores/order-table.ts index 4119b16e4df..048be4890f4 100644 --- a/indexer/packages/postgres/src/stores/order-table.ts +++ b/indexer/packages/postgres/src/stores/order-table.ts @@ -65,6 +65,8 @@ export async function findAll( totalFilled, price, type, + includeTypes, + excludeTypes, statuses, reduceOnly, orderFlags, @@ -153,6 +155,14 @@ export async function findAll( baseQuery = baseQuery.where(OrderColumns.type, type); } + if (includeTypes !== undefined && includeTypes.length > 0) { + baseQuery = baseQuery.whereIn(OrderColumns.type, includeTypes); + } + + if (excludeTypes !== undefined && excludeTypes.length > 0) { + baseQuery = baseQuery.whereNotIn(OrderColumns.type, excludeTypes); + } + if (statuses !== undefined) { baseQuery = baseQuery.whereIn(OrderColumns.status, statuses); } diff --git a/indexer/packages/postgres/src/types/query-types.ts b/indexer/packages/postgres/src/types/query-types.ts index f1e48f237c1..7fe0a26e3ee 100644 --- a/indexer/packages/postgres/src/types/query-types.ts +++ b/indexer/packages/postgres/src/types/query-types.ts @@ -1,7 +1,7 @@ /* ------- QUERY TYPES ------- */ import { CandleResolution } from './candle-types'; -import { Liquidity } from './fill-types'; +import { FillType, Liquidity } from './fill-types'; import { OrderSide, OrderStatus, OrderType } from './order-types'; import { PerpetualPositionStatus } from './perpetual-position-types'; import { PositionSide } from './position-types'; @@ -29,6 +29,8 @@ export enum QueryableField { TYPE = 'type', STATUS = 'status', STATUSES = 'statuses', + INCLUDE_TYPES = 'includeTypes', + EXCLUDE_TYPES = 'excludeTypes', POST_ONLY = 'postOnly', REDUCE_ONLY = 'reduceOnly', PERPETUAL_ID = 'perpetualId', @@ -149,6 +151,8 @@ export interface OrderQueryConfig extends QueryConfig { [QueryableField.TOTAL_FILLED]?: string, [QueryableField.PRICE]?: string, [QueryableField.TYPE]?: OrderType, + [QueryableField.INCLUDE_TYPES]?: OrderType[], + [QueryableField.EXCLUDE_TYPES]?: OrderType[], [QueryableField.STATUSES]?: OrderStatus[], [QueryableField.POST_ONLY]?: boolean, [QueryableField.REDUCE_ONLY]?: boolean, @@ -173,7 +177,9 @@ export interface FillQueryConfig extends QueryConfig { [QueryableField.SUBACCOUNT_ID]?: string[], [QueryableField.SIDE]?: OrderSide, [QueryableField.LIQUIDITY]?: Liquidity, - [QueryableField.TYPE]?: OrderType, + [QueryableField.TYPE]?: FillType, + [QueryableField.INCLUDE_TYPES]?: FillType[], + [QueryableField.EXCLUDE_TYPES]?: FillType[], [QueryableField.CLOB_PAIR_ID]?: string, [QueryableField.EVENT_ID]?: Buffer, [QueryableField.TRANSACTION_HASH]?: string, diff --git a/indexer/services/comlink/public/api-documentation.md b/indexer/services/comlink/public/api-documentation.md index 407c62953ab..9c7afb6a268 100644 --- a/indexer/services/comlink/public/api-documentation.md +++ b/indexer/services/comlink/public/api-documentation.md @@ -1253,6 +1253,8 @@ fetch(`${baseURL}/fills?address=string&subaccountNumber=0.1`, |subaccountNumber|query|number(double)|true|none| |market|query|string|false|none| |marketType|query|[MarketType](#schemamarkettype)|false|none| +|includeTypes|query|array[string]|false|none| +|excludeTypes|query|array[string]|false|none| |limit|query|number(double)|false|none| |createdBeforeOrAtHeight|query|number(double)|false|none| |createdBeforeOrAt|query|[IsoString](#schemaisostring)|false|none| @@ -1264,6 +1266,18 @@ fetch(`${baseURL}/fills?address=string&subaccountNumber=0.1`, |---|---| |marketType|PERPETUAL| |marketType|SPOT| +|includeTypes|LIMIT| +|includeTypes|LIQUIDATED| +|includeTypes|LIQUIDATION| +|includeTypes|DELEVERAGED| +|includeTypes|OFFSETTING| +|includeTypes|TWAP_SUBORDER| +|excludeTypes|LIMIT| +|excludeTypes|LIQUIDATED| +|excludeTypes|LIQUIDATION| +|excludeTypes|DELEVERAGED| +|excludeTypes|OFFSETTING| +|excludeTypes|TWAP_SUBORDER| > Example responses @@ -1366,9 +1380,28 @@ fetch(`${baseURL}/fills/parentSubaccount?address=string&parentSubaccountNumber=0 |---|---|---|---|---| |address|query|string|true|none| |parentSubaccountNumber|query|number(double)|true|none| +|includeTypes|query|array[string]|false|none| +|excludeTypes|query|array[string]|false|none| |limit|query|number(double)|false|none| |page|query|number(double)|false|none| +#### Enumerated Values + +|Parameter|Value| +|---|---| +|includeTypes|LIMIT| +|includeTypes|LIQUIDATED| +|includeTypes|LIQUIDATION| +|includeTypes|DELEVERAGED| +|includeTypes|OFFSETTING| +|includeTypes|TWAP_SUBORDER| +|excludeTypes|LIMIT| +|excludeTypes|LIQUIDATED| +|excludeTypes|LIQUIDATION| +|excludeTypes|DELEVERAGED| +|excludeTypes|OFFSETTING| +|excludeTypes|TWAP_SUBORDER| + > Example responses > 200 Response @@ -2279,6 +2312,8 @@ fetch(`${baseURL}/orders?address=string&subaccountNumber=0.1`, |ticker|query|string|false|none| |side|query|[OrderSide](#schemaorderside)|false|none| |type|query|[OrderType](#schemaordertype)|false|none| +|includeTypes|query|array[string]|false|none| +|excludeTypes|query|array[string]|false|none| |status|query|array[any]|false|none| |goodTilBlockBeforeOrAt|query|number(double)|false|none| |goodTilBlockAfter|query|number(double)|false|none| @@ -2301,6 +2336,24 @@ fetch(`${baseURL}/orders?address=string&subaccountNumber=0.1`, |type|TAKE_PROFIT_MARKET| |type|TWAP| |type|TWAP_SUBORDER| +|includeTypes|LIMIT| +|includeTypes|MARKET| +|includeTypes|STOP_LIMIT| +|includeTypes|STOP_MARKET| +|includeTypes|TRAILING_STOP| +|includeTypes|TAKE_PROFIT| +|includeTypes|TAKE_PROFIT_MARKET| +|includeTypes|TWAP| +|includeTypes|TWAP_SUBORDER| +|excludeTypes|LIMIT| +|excludeTypes|MARKET| +|excludeTypes|STOP_LIMIT| +|excludeTypes|STOP_MARKET| +|excludeTypes|TRAILING_STOP| +|excludeTypes|TAKE_PROFIT| +|excludeTypes|TAKE_PROFIT_MARKET| +|excludeTypes|TWAP| +|excludeTypes|TWAP_SUBORDER| > Example responses @@ -2492,6 +2545,8 @@ fetch(`${baseURL}/orders/parentSubaccountNumber?address=string&parentSubaccountN |ticker|query|string|false|none| |side|query|[OrderSide](#schemaorderside)|false|none| |type|query|[OrderType](#schemaordertype)|false|none| +|includeTypes|query|array[string]|false|none| +|excludeTypes|query|array[string]|false|none| |status|query|array[any]|false|none| |goodTilBlockBeforeOrAt|query|number(double)|false|none| |goodTilBlockAfter|query|number(double)|false|none| @@ -2514,6 +2569,24 @@ fetch(`${baseURL}/orders/parentSubaccountNumber?address=string&parentSubaccountN |type|TAKE_PROFIT_MARKET| |type|TWAP| |type|TWAP_SUBORDER| +|includeTypes|LIMIT| +|includeTypes|MARKET| +|includeTypes|STOP_LIMIT| +|includeTypes|STOP_MARKET| +|includeTypes|TRAILING_STOP| +|includeTypes|TAKE_PROFIT| +|includeTypes|TAKE_PROFIT_MARKET| +|includeTypes|TWAP| +|includeTypes|TWAP_SUBORDER| +|excludeTypes|LIMIT| +|excludeTypes|MARKET| +|excludeTypes|STOP_LIMIT| +|excludeTypes|STOP_MARKET| +|excludeTypes|TRAILING_STOP| +|excludeTypes|TAKE_PROFIT| +|excludeTypes|TAKE_PROFIT_MARKET| +|excludeTypes|TWAP| +|excludeTypes|TWAP_SUBORDER| > Example responses diff --git a/indexer/services/comlink/public/swagger.json b/indexer/services/comlink/public/swagger.json index 78f3895c8eb..18db46f4363 100644 --- a/indexer/services/comlink/public/swagger.json +++ b/indexer/services/comlink/public/swagger.json @@ -2508,6 +2508,28 @@ "$ref": "#/components/schemas/MarketType" } }, + { + "in": "query", + "name": "includeTypes", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FillType" + } + } + }, + { + "in": "query", + "name": "excludeTypes", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FillType" + } + } + }, { "in": "query", "name": "limit", @@ -2580,6 +2602,28 @@ "type": "number" } }, + { + "in": "query", + "name": "includeTypes", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FillType" + } + } + }, + { + "in": "query", + "name": "excludeTypes", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FillType" + } + } + }, { "in": "query", "name": "limit", @@ -3205,6 +3249,28 @@ "$ref": "#/components/schemas/OrderType" } }, + { + "in": "query", + "name": "includeTypes", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderType" + } + } + }, + { + "in": "query", + "name": "excludeTypes", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderType" + } + } + }, { "in": "query", "name": "status", @@ -3331,6 +3397,28 @@ "$ref": "#/components/schemas/OrderType" } }, + { + "in": "query", + "name": "includeTypes", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderType" + } + } + }, + { + "in": "query", + "name": "excludeTypes", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderType" + } + } + }, { "in": "query", "name": "status", diff --git a/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts b/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts index b2cb5f62217..7d308e7cf4b 100644 --- a/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts @@ -1,14 +1,15 @@ -import { stats, cacheControlMiddleware } from '@dydxprotocol-indexer/base'; +import { cacheControlMiddleware, stats } from '@dydxprotocol-indexer/base'; import { - SubaccountTable, + FillColumns, + FillFromDatabase, + FillTable, + FillType, IsoString, - perpetualMarketRefresher, + Ordering, PerpetualMarketFromDatabase, - FillTable, - FillFromDatabase, + perpetualMarketRefresher, QueryableField, - FillColumns, - Ordering, + SubaccountTable, } from '@dydxprotocol-indexer/postgres'; import express from 'express'; import { @@ -32,13 +33,15 @@ import { import { rateLimiterMiddleware } from '../../../lib/rate-limit'; import { CheckLimitAndCreatedBeforeOrAtSchema, - CheckSubaccountSchema, - CheckParentSubaccountSchema, CheckPaginationSchema, + CheckParentSubaccountSchema, + CheckSubaccountSchema, } from '../../../lib/validation/schemas'; import { handleValidationErrors } from '../../../request-helpers/error-handler'; import ExportResponseCodeStats from '../../../request-helpers/export-response-code-stats'; import { fillToResponseObject } from '../../../request-helpers/request-transformer'; +import { sanitizeArray } from '../../../request-helpers/sanitizers'; +import { validateArray } from '../../../request-helpers/validators'; import { FillRequest, FillResponse, @@ -60,6 +63,8 @@ class FillsController extends Controller { @Query() subaccountNumber: number, @Query() market?: string, @Query() marketType?: MarketType, + @Query() includeTypes?: FillType[], + @Query() excludeTypes?: FillType[], @Query() limit?: number, @Query() createdBeforeOrAtHeight?: number, @Query() createdBeforeOrAt?: IsoString, @@ -86,6 +91,8 @@ class FillsController extends Controller { { subaccountId: [subaccountId], clobPairId, + includeTypes, + excludeTypes, limit, createdBeforeOrAtHeight: createdBeforeOrAtHeight ? createdBeforeOrAtHeight.toString() @@ -126,6 +133,8 @@ class FillsController extends Controller { async getFillsForParentSubaccount( @Query() address: string, @Query() parentSubaccountNumber: number, + @Query() includeTypes?: FillType[], + @Query() excludeTypes?: FillType[], @Query() limit?: number, @Query() page?: number, ): Promise { @@ -149,6 +158,8 @@ class FillsController extends Controller { address, subaccountNumber: parentSubaccountNumber, }, + includeTypes, + excludeTypes, limit, page, }, @@ -157,8 +168,8 @@ class FillsController extends Controller { ); const clobPairIdToPerpetualMarket: Record< - string, - PerpetualMarketFromDatabase> = perpetualMarketRefresher.getClobPairIdToPerpetualMarket(); + string, + PerpetualMarketFromDatabase> = perpetualMarketRefresher.getClobPairIdToPerpetualMarket(); const clobPairIdToMarket: MarketAndTypeByClobPairId = _.mapValues( clobPairIdToPerpetualMarket, (perpetualMarket: PerpetualMarketFromDatabase) => { @@ -210,6 +221,28 @@ router.get( optional: true, errorMessage: 'marketType must be a valid market type (PERPETUAL/SPOT)', }, + includeTypes: { + in: ['query'], + optional: true, + customSanitizer: { + options: sanitizeArray, + }, + custom: { + options: (inputArray) => validateArray(inputArray, Object.values(FillType)), + errorMessage: `includeTypes must be one of ${Object.values(FillType)}`, + }, + }, + excludeTypes: { + in: ['query'], + optional: true, + customSanitizer: { + options: sanitizeArray, + }, + custom: { + options: (inputArray) => validateArray(inputArray, Object.values(FillType)), + errorMessage: `excludeTypes must be one of ${Object.values(FillType)}`, + }, + }, }), handleValidationErrors, complianceAndGeoCheck, @@ -221,6 +254,8 @@ router.get( subaccountNumber, market, marketType, + includeTypes, + excludeTypes, limit, createdBeforeOrAtHeight, createdBeforeOrAt, @@ -228,7 +263,7 @@ router.get( }: FillRequest = matchedData(req) as FillRequest; // The schema checks allow subaccountNumber to be a string, but we know it's a number here. - const subaccountNum : number = +subaccountNumber; + const subaccountNum: number = +subaccountNumber; // TODO(DEC-656): Change to using a cache of markets in Redis similar to Librarian instead of // querying the DB. @@ -239,6 +274,8 @@ router.get( subaccountNum, market, marketType, + includeTypes, + excludeTypes, limit, createdBeforeOrAtHeight, createdBeforeOrAt, @@ -292,6 +329,28 @@ router.get( optional: true, errorMessage: 'marketType must be a valid market type (PERPETUAL/SPOT)', }, + includeTypes: { + in: ['query'], + optional: true, + customSanitizer: { + options: sanitizeArray, + }, + custom: { + options: (inputArray) => validateArray(inputArray, Object.values(FillType)), + errorMessage: `includeTypes must be one of ${Object.values(FillType)}`, + }, + }, + excludeTypes: { + in: ['query'], + optional: true, + customSanitizer: { + options: sanitizeArray, + }, + custom: { + options: (inputArray) => validateArray(inputArray, Object.values(FillType)), + errorMessage: `excludeTypes must be one of ${Object.values(FillType)}`, + }, + }, }), handleValidationErrors, complianceAndGeoCheck, @@ -301,12 +360,14 @@ router.get( const { address, parentSubaccountNumber, + includeTypes, + excludeTypes, limit, page, }: ParentSubaccountFillRequest = matchedData(req) as ParentSubaccountFillRequest; // The schema checks allow subaccountNumber to be a string, but we know it's a number here. - const parentSubaccountNum : number = +parentSubaccountNumber; + const parentSubaccountNum: number = +parentSubaccountNumber; // TODO(DEC-656): Change to using a cache of markets in Redis similar to Librarian instead of // querying the DB. @@ -315,6 +376,8 @@ router.get( const response: FillResponse = await controller.getFillsForParentSubaccount( address, parentSubaccountNum, + includeTypes, + excludeTypes, limit, page, ); diff --git a/indexer/services/comlink/src/controllers/api/v4/orders-controller.ts b/indexer/services/comlink/src/controllers/api/v4/orders-controller.ts index ca7a640836a..7e49873639f 100644 --- a/indexer/services/comlink/src/controllers/api/v4/orders-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/orders-controller.ts @@ -1,4 +1,4 @@ -import { logger, stats, cacheControlMiddleware } from '@dydxprotocol-indexer/base'; +import { cacheControlMiddleware, logger, stats } from '@dydxprotocol-indexer/base'; import { APIOrderStatus, APIOrderStatusEnum, @@ -12,8 +12,8 @@ import { OrderStatus, OrderTable, OrderType, - ParentSubaccount, PaginationFromDatabase, + ParentSubaccount, perpetualMarketRefresher, protocolTranslations, SubaccountTable, @@ -85,6 +85,8 @@ async function listOrdersCommon( ticker?: string, side?: OrderSide, type?: OrderType, + includeTypes?: OrderType[], + excludeTypes?: OrderType[], status?: APIOrderStatus[], goodTilBlockBeforeOrAt?: number, goodTilBlockAfter?: number, @@ -104,6 +106,8 @@ async function listOrdersCommon( clobPairId, side, type, + includeTypes, + excludeTypes, goodTilBlockBeforeOrAt: goodTilBlockBeforeOrAt?.toString(), goodTilBlockAfter: goodTilBlockAfter?.toString(), goodTilBlockTimeBeforeOrAt, @@ -219,6 +223,8 @@ class OrdersController extends Controller { @Query() ticker?: string, @Query() side?: OrderSide, @Query() type?: OrderType, + @Query() includeTypes?: OrderType[], + @Query() excludeTypes?: OrderType[], @Query() status?: APIOrderStatus[], @Query() goodTilBlockBeforeOrAt?: number, @Query() goodTilBlockAfter?: number, @@ -236,6 +242,8 @@ class OrdersController extends Controller { ticker, side, type, + includeTypes, + excludeTypes, status, goodTilBlockBeforeOrAt, goodTilBlockAfter, @@ -253,6 +261,8 @@ class OrdersController extends Controller { @Query() ticker?: string, @Query() side?: OrderSide, @Query() type?: OrderType, + @Query() includeTypes?: OrderType[], + @Query() excludeTypes?: OrderType[], @Query() status?: APIOrderStatus[], @Query() goodTilBlockBeforeOrAt?: number, @Query() goodTilBlockAfter?: number, @@ -277,6 +287,8 @@ class OrdersController extends Controller { ticker, side, type, + includeTypes, + excludeTypes, status, goodTilBlockBeforeOrAt, goodTilBlockAfter, @@ -401,6 +413,28 @@ router.get( isBoolean: true, optional: true, }, + includeTypes: { + in: ['query'], + optional: true, + customSanitizer: { + options: sanitizeArray, + }, + custom: { + options: (inputArray) => validateArray(inputArray, Object.values(OrderType)), + errorMessage: `includeTypes must be one of ${Object.values(OrderType)}`, + }, + }, + excludeTypes: { + in: ['query'], + optional: true, + customSanitizer: { + options: sanitizeArray, + }, + custom: { + options: (inputArray) => validateArray(inputArray, Object.values(OrderType)), + errorMessage: `excludeTypes must be one of ${Object.values(OrderType)}`, + }, + }, }), query('goodTilBlock').if(query('goodTilBlockTime').exists()).isEmpty() .withMessage('Cannot provide both goodTilBlock and goodTilBlockTime'), @@ -416,6 +450,8 @@ router.get( ticker, side, type, + includeTypes, + excludeTypes, status, goodTilBlockBeforeOrAt, goodTilBlockAfter, @@ -436,6 +472,8 @@ router.get( ticker, side, type, + includeTypes, + excludeTypes, status, goodTilBlockBeforeOrAt, goodTilBlockAfter, @@ -529,6 +567,28 @@ router.get( isBoolean: true, optional: true, }, + includeTypes: { + in: ['query'], + optional: true, + customSanitizer: { + options: sanitizeArray, + }, + custom: { + options: (inputArray) => validateArray(inputArray, Object.values(OrderType)), + errorMessage: `includeTypes must be one of ${Object.values(OrderType)}`, + }, + }, + excludeTypes: { + in: ['query'], + optional: true, + customSanitizer: { + options: sanitizeArray, + }, + custom: { + options: (inputArray) => validateArray(inputArray, Object.values(OrderType)), + errorMessage: `excludeTypes must be one of ${Object.values(OrderType)}`, + }, + }, }), query('goodTilBlock').if(query('goodTilBlockTime').exists()).isEmpty() .withMessage('Cannot provide both goodTilBlock and goodTilBlockTime'), @@ -544,6 +604,8 @@ router.get( ticker, side, type, + includeTypes, + excludeTypes, status, goodTilBlockBeforeOrAt, goodTilBlockAfter, @@ -564,6 +626,8 @@ router.get( ticker, side, type, + includeTypes, + excludeTypes, status, goodTilBlockBeforeOrAt, goodTilBlockAfter, @@ -710,12 +774,12 @@ async function getRedisOrderMapForSubaccountIds( if (redisGoodTilBlockTime) { const redisGoodTilBlockTimeDateObj: DateTime = DateTime.fromISO(redisGoodTilBlockTime); if (goodTilBlockTimeBeforeOrAt !== undefined && - redisGoodTilBlockTimeDateObj > DateTime.fromISO(goodTilBlockTimeBeforeOrAt) + redisGoodTilBlockTimeDateObj > DateTime.fromISO(goodTilBlockTimeBeforeOrAt) ) { return false; } if (goodTilBlockTimeAfter !== undefined && - redisGoodTilBlockTimeDateObj <= DateTime.fromISO(goodTilBlockTimeAfter) + redisGoodTilBlockTimeDateObj <= DateTime.fromISO(goodTilBlockTimeAfter) ) { return false; } diff --git a/indexer/services/comlink/src/request-helpers/sanitizers.ts b/indexer/services/comlink/src/request-helpers/sanitizers.ts index 3f89ae2321f..e33b33d8043 100644 --- a/indexer/services/comlink/src/request-helpers/sanitizers.ts +++ b/indexer/services/comlink/src/request-helpers/sanitizers.ts @@ -3,16 +3,28 @@ import { logger } from '@dydxprotocol-indexer/base'; /** * @function sanitizeArray * @param input input value from query - * @description Checks if the input is empty and if it isn't, set the string to upper case and split - * it using `,` as the delimiter. + * @description Handles both comma-separated strings and repeated query parameters. + * If input is a string, converts to uppercase and splits by comma. + * If input is already an array, uppercases each element. + * Returns null for empty values. */ export function sanitizeArray( - input: string, + input: string | string[], ): string[] | null { try { - return ( - // eslint-disable-next-line no-mixed-operators - (input !== '') && input.toUpperCase().split(',') || null); + // Handle array input (repeated query parameters: ?includeTypes=LIMIT&includeTypes=MARKET) + if (Array.isArray(input)) { + if (input.length === 0) { + return null; + } + return input.map((item) => item.toUpperCase()); + } + + // Handle string input (comma-separated: ?includeTypes=LIMIT,MARKET) + if (input === '') { + return null; + } + return input.toUpperCase().split(','); } catch (error) { logger.error({ at: 'request-helpers#sanitizeArray', diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts index c1cf8d984d9..c8bdc99fc94 100644 --- a/indexer/services/comlink/src/types.ts +++ b/indexer/services/comlink/src/types.ts @@ -95,7 +95,7 @@ export interface ParentSubaccountResponse { childSubaccounts: SubaccountResponseObject[], } -export type SubaccountById = {[id: string]: SubaccountFromDatabase}; +export type SubaccountById = { [id: string]: SubaccountFromDatabase }; /* ------- TIME TYPES ------- */ @@ -270,7 +270,7 @@ export interface PnlTicksResponseObject { blockTime: IsoString, } -export interface AggregatedPnlTick{ +export interface AggregatedPnlTick { pnlTick: PnlTicksFromDatabase, numTicks: number, } @@ -300,14 +300,14 @@ export interface HeightResponse { /* ------- MARKET TYPES ------- */ -export type AssetById = {[assetId: string]: AssetFromDatabase}; +export type AssetById = { [assetId: string]: AssetFromDatabase }; export interface MarketAndType { marketType: MarketType, market: string, } -export type MarketAndTypeByClobPairId = {[clobPairId: string]: MarketAndType}; +export type MarketAndTypeByClobPairId = { [clobPairId: string]: MarketAndType }; export enum MarketType { PERPETUAL = 'PERPETUAL', @@ -398,7 +398,7 @@ export interface CandleResponse { candles: CandleResponseObject[], } -export interface CandleResponseObject extends Omit {} +export interface CandleResponseObject extends Omit { } /* ------- CANDLE TYPES ------- */ @@ -456,7 +456,7 @@ interface CreatedBeforeRequest { createdBeforeOrAt?: IsoString, } -export interface LimitAndCreatedBeforeRequest extends LimitRequest, CreatedBeforeRequest {} +export interface LimitAndCreatedBeforeRequest extends LimitRequest, CreatedBeforeRequest { } export interface LimitAndEffectiveBeforeRequest extends LimitRequest { effectiveBeforeOrAtHeight?: number, @@ -477,13 +477,13 @@ export interface ParentSubaccountPerpetualPositionRequest extends ParentSubaccou status: PerpetualPositionStatus[], } -export interface AssetPositionRequest extends SubaccountRequest {} +export interface AssetPositionRequest extends SubaccountRequest { } export interface ParentSubaccountAssetPositionRequest extends ParentSubaccountRequest { } export interface TransferRequest - extends SubaccountRequest, LimitAndCreatedBeforeRequest, PaginationRequest {} + extends SubaccountRequest, LimitAndCreatedBeforeRequest, PaginationRequest { } export interface ParentSubaccountTransferRequest extends ParentSubaccountRequest, LimitAndCreatedBeforeRequest, PaginationRequest { @@ -500,12 +500,16 @@ export interface FillRequest extends SubaccountRequest, LimitAndCreatedBeforeRequest, PaginationRequest { market: string, marketType: MarketType, + includeTypes?: FillType[], + excludeTypes?: FillType[], } export interface ParentSubaccountFillRequest extends ParentSubaccountRequest, LimitAndCreatedBeforeRequest, PaginationRequest { market: string, marketType: MarketType, + includeTypes?: FillType[], + excludeTypes?: FillType[], } export interface TradeRequest extends LimitAndCreatedBeforeRequest, PaginationRequest { @@ -517,7 +521,7 @@ export interface PerpetualMarketRequest extends LimitRequest, TickerRequest { } export interface PnlTicksRequest - extends SubaccountRequest, LimitAndCreatedBeforeAndAfterRequest, PaginationRequest {} + extends SubaccountRequest, LimitAndCreatedBeforeAndAfterRequest, PaginationRequest { } export interface ParentSubaccountPnlTicksRequest extends ParentSubaccountRequest, LimitAndCreatedBeforeAndAfterRequest { @@ -543,6 +547,8 @@ export interface GetOrderRequest { export interface ListOrderRequest extends SubaccountRequest, LimitRequest, TickerRequest { side?: OrderSide, type?: OrderType, + includeTypes?: OrderType[], + excludeTypes?: OrderType[], status?: OrderStatus[], goodTilBlockBeforeOrAt?: number, goodTilBlockAfter?: number, @@ -555,6 +561,8 @@ export interface ParentSubaccountListOrderRequest extends ParentSubaccountRequest, LimitRequest, TickerRequest { side?: OrderSide, type?: OrderType, + includeTypes?: OrderType[], + excludeTypes?: OrderType[], status?: OrderStatus[], goodTilBlockBeforeOrAt?: number, goodTilBlockAfter?: number, @@ -603,7 +611,7 @@ export interface ComplianceResponse { reason?: string, } -export interface ComplianceRequest extends AddressRequest {} +export interface ComplianceRequest extends AddressRequest { } export interface SetComplianceStatusRequest extends AddressRequest { status: ComplianceStatus, @@ -711,29 +719,29 @@ export interface MegavaultHistoricalPnlRequest { resolution: PnlTickInterval, } -export interface VaultsHistoricalPnlRequest extends MegavaultHistoricalPnlRequest {} +export interface VaultsHistoricalPnlRequest extends MegavaultHistoricalPnlRequest { } export interface VaultMapping { [subaccountId: string]: VaultFromDatabase, } /* ------- Affiliates Types ------- */ -export interface AffiliateMetadataRequest{ +export interface AffiliateMetadataRequest { address: string, } -export interface AffiliateAddressRequest{ +export interface AffiliateAddressRequest { referralCode: string, } -export interface AffiliateSnapshotRequest{ +export interface AffiliateSnapshotRequest { addressFilter?: string[], limit?: number, offset?: number, sortByAffiliateEarning?: boolean, } -export interface AffiliateTotalVolumeRequest{ +export interface AffiliateTotalVolumeRequest { address: string, } @@ -883,7 +891,7 @@ export interface PnlResponseObject { createdAtHeight: string, } -export interface AggregatedPnl{ +export interface AggregatedPnl { pnl: PnlFromDatabase, numPnls: number, }