diff --git a/indexer/packages/postgres/__tests__/helpers/constants.ts b/indexer/packages/postgres/__tests__/helpers/constants.ts
index f6fe55c99d..5ff0a194eb 100644
--- a/indexer/packages/postgres/__tests__/helpers/constants.ts
+++ b/indexer/packages/postgres/__tests__/helpers/constants.ts
@@ -468,7 +468,7 @@ export const isolatedPerpetualPosition: PerpetualPositionCreateObject = {
status: PerpetualPositionStatus.OPEN,
size: '10',
maxSize: '25',
- entryPrice: '20000',
+ entryPrice: '1.5',
sumOpen: '10',
sumClose: '0',
createdAt: createdDateTime.toISO(),
@@ -643,7 +643,7 @@ export const isolatedMarket: MarketCreateObject = {
pair: 'ISO-USD',
exponent: -12,
minPriceChangePpm: 50,
- oraclePrice: '0.000000075',
+ oraclePrice: '1.00',
};
export const isolatedMarket2: MarketCreateObject = {
diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/perpetual-positions-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/perpetual-positions-controller.test.ts
index c60f47b979..112ade7bb7 100644
--- a/indexer/services/comlink/__tests__/controllers/api/v4/perpetual-positions-controller.test.ts
+++ b/indexer/services/comlink/__tests__/controllers/api/v4/perpetual-positions-controller.test.ts
@@ -1,13 +1,13 @@
import {
+ BlockTable,
dbHelpers,
- testMocks,
- testConstants,
+ FundingIndexUpdatesTable,
perpetualMarketRefresher,
+ PerpetualPositionStatus,
PerpetualPositionTable,
PositionSide,
- BlockTable,
- FundingIndexUpdatesTable,
- PerpetualPositionStatus,
+ testConstants,
+ testMocks,
} from '@dydxprotocol-indexer/postgres';
import { PerpetualPositionResponseObject, RequestMethod } from '../../../../src/types';
import request from 'supertest';
@@ -245,5 +245,241 @@ describe('perpetual-positions-controller#V4', () => {
]),
}));
});
+
+ it('Get /perpetualPositions/parentSubaccountNumber gets long/short positions across subaccounts', async () => {
+ await Promise.all([
+ PerpetualPositionTable.create(testConstants.defaultPerpetualPosition),
+ PerpetualPositionTable.create({
+ ...testConstants.isolatedPerpetualPosition,
+ side: PositionSide.SHORT,
+ size: '-10',
+ }),
+ ]);
+ await Promise.all([
+ FundingIndexUpdatesTable.create({
+ ...testConstants.isolatedMarketFundingIndexUpdate,
+ fundingIndex: '10000',
+ effectiveAtHeight: testConstants.createdHeight,
+ }),
+ FundingIndexUpdatesTable.create({
+ ...testConstants.isolatedMarketFundingIndexUpdate,
+ eventId: testConstants.defaultTendermintEventId2,
+ effectiveAtHeight: latestHeight,
+ }),
+ ]);
+
+ const parentSubaccountNumber: number = 0;
+ const response: request.Response = await sendRequest({
+ type: RequestMethod.GET,
+ path: `/v4/perpetualPositions/parentSubaccountNumber?address=${testConstants.defaultAddress}` +
+ `&parentSubaccountNumber=${parentSubaccountNumber}`,
+ });
+
+ const expected: PerpetualPositionResponseObject = {
+ market: testConstants.defaultPerpetualMarket.ticker,
+ side: testConstants.defaultPerpetualPosition.side,
+ status: testConstants.defaultPerpetualPosition.status,
+ size: testConstants.defaultPerpetualPosition.size,
+ maxSize: testConstants.defaultPerpetualPosition.maxSize,
+ entryPrice: getFixedRepresentation(testConstants.defaultPerpetualPosition.entryPrice!),
+ exitPrice: null,
+ sumOpen: testConstants.defaultPerpetualPosition.sumOpen!,
+ sumClose: testConstants.defaultPerpetualPosition.sumClose!,
+ // For the calculation of the net funding (long position):
+ // settled funding on position = 200_000, size = 10, latest funding index = 10050
+ // last updated funding index = 10000
+ // total funding = 200_000 + (10 * (10000 - 10050)) = 199_500
+ netFunding: getFixedRepresentation('199500'),
+ // sumClose=0, so realized Pnl is the same as the net funding of the position.
+ // Unsettled funding is funding payments that already "happened" but not reflected
+ // in the subaccount's balance yet, so it's considered a part of realizedPnl.
+ realizedPnl: getFixedRepresentation('199500'),
+ // For the calculation of the unrealized pnl (long position):
+ // index price = 15_000, entry price = 20_000, size = 10
+ // unrealizedPnl = size * (index price - entry price)
+ // unrealizedPnl = 10 * (15_000 - 20_000)
+ unrealizedPnl: getFixedRepresentation('-50000'),
+ createdAt: testConstants.createdDateTime.toISO(),
+ closedAt: null,
+ createdAtHeight: testConstants.createdHeight,
+ subaccountNumber: testConstants.defaultSubaccount.subaccountNumber,
+ };
+ // object for expected 2 which holds an isolated position in an isolated perpetual
+ // in the isolated subaccount
+ const expected2: PerpetualPositionResponseObject = {
+ market: testConstants.isolatedPerpetualMarket.ticker,
+ side: PositionSide.SHORT,
+ status: testConstants.isolatedPerpetualPosition.status,
+ size: '-10',
+ maxSize: testConstants.isolatedPerpetualPosition.maxSize,
+ entryPrice: getFixedRepresentation(testConstants.isolatedPerpetualPosition.entryPrice!),
+ exitPrice: null,
+ sumOpen: testConstants.isolatedPerpetualPosition.sumOpen!,
+ sumClose: testConstants.isolatedPerpetualPosition.sumClose!,
+ // For the calculation of the net funding (short position):
+ // settled funding on position = 200_000, size = -10, latest funding index = 10200
+ // last updated funding index = 10000
+ // total funding = 200_000 + (-10 * (10000 - 10200)) = 202_000
+ netFunding: getFixedRepresentation('202000'),
+ // sumClose=0, so realized Pnl is the same as the net funding of the position.
+ // Unsettled funding is funding payments that already "happened" but not reflected
+ // in the subaccount's balance yet, so it's considered a part of realizedPnl.
+ realizedPnl: getFixedRepresentation('202000'),
+ // For the calculation of the unrealized pnl (short position):
+ // index price = 1, entry price = 1.5, size = -10
+ // unrealizedPnl = size * (index price - entry price)
+ // unrealizedPnl = -10 * (1-1.5)
+ unrealizedPnl: getFixedRepresentation('5'),
+ createdAt: testConstants.createdDateTime.toISO(),
+ closedAt: null,
+ createdAtHeight: testConstants.createdHeight,
+ subaccountNumber: testConstants.isolatedSubaccount.subaccountNumber,
+ };
+
+ expect(response.body.positions).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ ...expected,
+ }),
+ expect.objectContaining({
+ ...expected2,
+ }),
+ ]),
+ );
+ });
+
+ it('Get /perpetualPositions/parentSubaccountNumber gets CLOSED position without adjusting funding', async () => {
+ await Promise.all([
+ PerpetualPositionTable.create({
+ ...testConstants.defaultPerpetualPosition,
+ status: PerpetualPositionStatus.CLOSED,
+ }),
+ PerpetualPositionTable.create({
+ ...testConstants.isolatedPerpetualPosition,
+ side: PositionSide.SHORT,
+ size: '-10',
+ }),
+ ]);
+
+ const parentSubaccountNumber: number = 0;
+ const response: request.Response = await sendRequest({
+ type: RequestMethod.GET,
+ path: `/v4/perpetualPositions/parentSubaccountNumber?address=${testConstants.defaultAddress}` +
+ `&parentSubaccountNumber=${parentSubaccountNumber}`,
+ });
+
+ const expected: PerpetualPositionResponseObject = {
+ market: testConstants.defaultPerpetualMarket.ticker,
+ side: testConstants.defaultPerpetualPosition.side,
+ status: PerpetualPositionStatus.CLOSED,
+ size: testConstants.defaultPerpetualPosition.size,
+ maxSize: testConstants.defaultPerpetualPosition.maxSize,
+ entryPrice: getFixedRepresentation(testConstants.defaultPerpetualPosition.entryPrice!),
+ exitPrice: null,
+ sumOpen: testConstants.defaultPerpetualPosition.sumOpen!,
+ sumClose: testConstants.defaultPerpetualPosition.sumClose!,
+ // CLOSED position should not have funding adjusted
+ netFunding: getFixedRepresentation(
+ testConstants.defaultPerpetualPosition.settledFunding,
+ ),
+ realizedPnl: getFixedRepresentation(
+ testConstants.defaultPerpetualPosition.settledFunding,
+ ),
+ // For the calculation of the unrealized pnl (short position):
+ // index price = 15_000, entry price = 20_000, size = 10
+ // unrealizedPnl = size * (index price - entry price)
+ // unrealizedPnl = 10 * (15_000 - 20_000)
+ unrealizedPnl: getFixedRepresentation('-50000'),
+ createdAt: testConstants.createdDateTime.toISO(),
+ closedAt: null,
+ createdAtHeight: testConstants.createdHeight,
+ subaccountNumber: testConstants.defaultSubaccount.subaccountNumber,
+ };
+ const expected2: PerpetualPositionResponseObject = {
+ market: testConstants.isolatedPerpetualMarket.ticker,
+ side: PositionSide.SHORT,
+ status: testConstants.isolatedPerpetualPosition.status,
+ size: '-10',
+ maxSize: testConstants.isolatedPerpetualPosition.maxSize,
+ entryPrice: getFixedRepresentation(testConstants.isolatedPerpetualPosition.entryPrice!),
+ exitPrice: null,
+ sumOpen: testConstants.isolatedPerpetualPosition.sumOpen!,
+ sumClose: testConstants.isolatedPerpetualPosition.sumClose!,
+ // CLOSED position should not have funding adjusted
+ netFunding: getFixedRepresentation(
+ testConstants.isolatedPerpetualPosition.settledFunding,
+ ),
+ realizedPnl: getFixedRepresentation(
+ testConstants.isolatedPerpetualPosition.settledFunding,
+ ),
+ // For the calculation of the unrealized pnl (short position):
+ // index price = 1, entry price = 1.5, size = -10
+ // unrealizedPnl = size * (index price - entry price)
+ // unrealizedPnl = -10 * (1-1.5)
+ unrealizedPnl: getFixedRepresentation('5'),
+ createdAt: testConstants.createdDateTime.toISO(),
+ closedAt: null,
+ createdAtHeight: testConstants.createdHeight,
+ subaccountNumber: testConstants.isolatedSubaccount.subaccountNumber,
+ };
+
+ expect(response.body.positions).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ ...expected,
+ }),
+ expect.objectContaining({
+ ...expected2,
+ }),
+ ]),
+ );
+ });
+
+ it.each([
+ [
+ 'invalid status',
+ {
+ address: defaultAddress,
+ parentSubaccountNumber: defaultSubaccountNumber,
+ status: 'INVALID',
+ },
+ 'status',
+ 'status must be a valid Position Status (OPEN, etc)',
+ ],
+ [
+ 'multiple invalid status',
+ {
+ address: defaultAddress,
+ parentSubaccountNumber: defaultSubaccountNumber,
+ status: 'INVALID,INVALID',
+ },
+ 'status',
+ 'status must be a valid Position Status (OPEN, etc)',
+ ],
+ ])('Returns 400 when validation fails: %s', async (
+ _reason: string,
+ queryParams: {
+ address?: string,
+ subaccountNumber?: number,
+ status?: string,
+ },
+ fieldWithError: string,
+ expectedErrorMsg: string,
+ ) => {
+ const response: request.Response = await sendRequest({
+ type: RequestMethod.GET,
+ path: `/v4/perpetualPositions/parentSubaccountNumber?${getQueryString(queryParams)}`,
+ expectedStatus: 400,
+ });
+
+ expect(response.body).toEqual(expect.objectContaining({
+ errors: expect.arrayContaining([
+ expect.objectContaining({
+ param: fieldWithError,
+ msg: expectedErrorMsg,
+ }),
+ ]),
+ }));
+ });
});
});
diff --git a/indexer/services/comlink/public/api-documentation.md b/indexer/services/comlink/public/api-documentation.md
index 2fd059f3b0..8c5bd6f4ce 100644
--- a/indexer/services/comlink/public/api-documentation.md
+++ b/indexer/services/comlink/public/api-documentation.md
@@ -1863,6 +1863,106 @@ fetch('https://dydx-testnet.imperator.co/v4/perpetualPositions?address=string&su
This operation does not require authentication
+## ListPositionsForParentSubaccount
+
+
+
+> Code samples
+
+```python
+import requests
+headers = {
+ 'Accept': 'application/json'
+}
+
+r = requests.get('https://dydx-testnet.imperator.co/v4/perpetualPositions/parentSubaccountNumber', params={
+ 'address': 'string', 'parentSubaccountNumber': '0'
+}, headers = headers)
+
+print(r.json())
+
+```
+
+```javascript
+
+const headers = {
+ 'Accept':'application/json'
+};
+
+fetch('https://dydx-testnet.imperator.co/v4/perpetualPositions/parentSubaccountNumber?address=string&parentSubaccountNumber=0',
+{
+ method: 'GET',
+
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+`GET /perpetualPositions/parentSubaccountNumber`
+
+### Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|address|query|string|true|none|
+|parentSubaccountNumber|query|number(double)|true|none|
+|status|query|array[string]|false|none|
+|limit|query|number(double)|false|none|
+|createdBeforeOrAtHeight|query|number(double)|false|none|
+|createdBeforeOrAt|query|[IsoString](#schemaisostring)|false|none|
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|status|OPEN|
+|status|CLOSED|
+|status|LIQUIDATED|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "positions": [
+ {
+ "market": "string",
+ "status": "OPEN",
+ "side": "LONG",
+ "size": "string",
+ "maxSize": "string",
+ "entryPrice": "string",
+ "realizedPnl": "string",
+ "createdAt": "string",
+ "createdAtHeight": "string",
+ "sumOpen": "string",
+ "sumClose": "string",
+ "netFunding": "string",
+ "unrealizedPnl": "string",
+ "closedAt": "string",
+ "exitPrice": "string",
+ "subaccountNumber": 0
+ }
+ ]
+}
+```
+
+### Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Ok|[PerpetualPositionResponse](#schemaperpetualpositionresponse)|
+
+
+
## Get
diff --git a/indexer/services/comlink/public/swagger.json b/indexer/services/comlink/public/swagger.json
index 5de5c88151..96b6394ce6 100644
--- a/indexer/services/comlink/public/swagger.json
+++ b/indexer/services/comlink/public/swagger.json
@@ -2205,6 +2205,80 @@
]
}
},
+ "/perpetualPositions/parentSubaccountNumber": {
+ "get": {
+ "operationId": "ListPositionsForParentSubaccount",
+ "responses": {
+ "200": {
+ "description": "Ok",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PerpetualPositionResponse"
+ }
+ }
+ }
+ }
+ },
+ "security": [],
+ "parameters": [
+ {
+ "in": "query",
+ "name": "address",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "parentSubaccountNumber",
+ "required": true,
+ "schema": {
+ "format": "double",
+ "type": "number"
+ }
+ },
+ {
+ "in": "query",
+ "name": "status",
+ "required": false,
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/PerpetualPositionStatus"
+ }
+ }
+ },
+ {
+ "in": "query",
+ "name": "limit",
+ "required": false,
+ "schema": {
+ "format": "double",
+ "type": "number"
+ }
+ },
+ {
+ "in": "query",
+ "name": "createdBeforeOrAtHeight",
+ "required": false,
+ "schema": {
+ "format": "double",
+ "type": "number"
+ }
+ },
+ {
+ "in": "query",
+ "name": "createdBeforeOrAt",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/IsoString"
+ }
+ }
+ ]
+ }
+ },
"/sparklines": {
"get": {
"operationId": "Get",
diff --git a/indexer/services/comlink/src/controllers/api/v4/perpetual-positions-controller.ts b/indexer/services/comlink/src/controllers/api/v4/perpetual-positions-controller.ts
index f54c961f92..1607a82377 100644
--- a/indexer/services/comlink/src/controllers/api/v4/perpetual-positions-controller.ts
+++ b/indexer/services/comlink/src/controllers/api/v4/perpetual-positions-controller.ts
@@ -36,10 +36,11 @@ import {
handleControllerError,
getPerpetualPositionsWithUpdatedFunding,
initializePerpetualPositionsWithFunding,
+ getChildSubaccountNums,
} from '../../../lib/helpers';
import { rateLimiterMiddleware } from '../../../lib/rate-limit';
import {
- CheckLimitAndCreatedBeforeOrAtSchema,
+ CheckLimitAndCreatedBeforeOrAtSchema, CheckParentSubaccountSchema,
CheckSubaccountSchema,
} from '../../../lib/validation/schemas';
import { handleValidationErrors } from '../../../request-helpers/error-handler';
@@ -47,7 +48,12 @@ import ExportResponseCodeStats from '../../../request-helpers/export-response-co
import { perpetualPositionToResponseObject } from '../../../request-helpers/request-transformer';
import { sanitizeArray } from '../../../request-helpers/sanitizers';
import { validateArray } from '../../../request-helpers/validators';
-import { PerpetualPositionRequest, PerpetualPositionResponse, PerpetualPositionWithFunding } from '../../../types';
+import {
+ ParentSubaccountPerpetualPositionRequest,
+ PerpetualPositionRequest,
+ PerpetualPositionResponse,
+ PerpetualPositionWithFunding,
+} from '../../../types';
const router: express.Router = express.Router();
const controllerName: string = 'perpetual-positions-controller';
@@ -150,6 +156,143 @@ class PerpetualPositionsController extends Controller {
}),
};
}
+
+ @Get('/parentSubaccountNumber')
+ async listPositionsForParentSubaccount(
+ @Query() address: string,
+ @Query() parentSubaccountNumber: number,
+ @Query() status?: PerpetualPositionStatus[],
+ @Query() limit?: number,
+ @Query() createdBeforeOrAtHeight?: number,
+ @Query() createdBeforeOrAt?: IsoString,
+ ): Promise {
+ // Get subaccountIds for all child subaccounts of the parent subaccount
+ // Create a record of subaccountId to subaccount number
+ const childIdtoSubaccountNumber: Record = {};
+ getChildSubaccountNums(parentSubaccountNumber).forEach(
+ (subaccountNum: number) => {
+ childIdtoSubaccountNumber[SubaccountTable.uuid(address, subaccountNum)] = subaccountNum;
+ },
+ );
+ const childSubaccountIds: string[] = Object.keys(childIdtoSubaccountNumber);
+
+ const [
+ positions,
+ markets,
+ ]: [
+ PerpetualPositionFromDatabase[],
+ MarketFromDatabase[],
+ ] = await Promise.all([
+ PerpetualPositionTable.findAll(
+ {
+ subaccountId: childSubaccountIds,
+ status,
+ limit,
+ createdBeforeOrAtHeight: createdBeforeOrAtHeight
+ ? createdBeforeOrAtHeight.toString()
+ : undefined,
+ createdBeforeOrAt,
+ },
+ [QueryableField.LIMIT],
+ ),
+ MarketTable.findAll(
+ {},
+ [],
+ ),
+ ]);
+
+ const openPositionsExist: boolean = positions.some(
+ (position: PerpetualPositionFromDatabase) => position.status === PerpetualPositionStatus.OPEN,
+ );
+
+ let updatedPerpetualPositions:
+ PerpetualPositionWithFunding[] = initializePerpetualPositionsWithFunding(positions);
+
+ // Only update the funding for open positions
+ if (openPositionsExist) {
+ const subaccountIds: string[] = updatedPerpetualPositions.map(
+ (position: PerpetualPositionFromDatabase) => position.subaccountId,
+ );
+ const [
+ subaccounts,
+ latestBlock,
+ ]: [
+ SubaccountFromDatabase[],
+ BlockFromDatabase | undefined,
+ ] = await Promise.all([
+ SubaccountTable.findAll(
+ {
+ id: subaccountIds,
+ },
+ [],
+ ),
+ BlockTable.getLatest(),
+ ]);
+
+ if (subaccounts.length === 0 || latestBlock === undefined) {
+ throw new NotFoundError(
+ `Found OPEN perpetual positions but no subaccounts with address ${address} ` +
+ `and parent Subaccount number ${parentSubaccountNumber}`,
+ );
+ }
+
+ const perpetualPositionsBySubaccount:
+ { [subaccountId: string]: PerpetualPositionWithFunding[] } = _.groupBy(
+ updatedPerpetualPositions,
+ 'subaccountId',
+ );
+
+ // For each subaccount, update all perpetual positions with the latest funding and
+ // store the updated positions in updatedPerpetualPositions.
+ const updatedPerpetualPositionsPromises:
+ Promise[] = subaccounts.map(
+ (subaccount: SubaccountFromDatabase) => adjustPerpetualPositionsWithUpdatedFunding(
+ perpetualPositionsBySubaccount[subaccount.id],
+ subaccount,
+ latestBlock,
+ ),
+ );
+ updatedPerpetualPositions = _.flatten(await Promise.all(updatedPerpetualPositionsPromises));
+ }
+
+ const perpetualMarketsMap: PerpetualMarketsMap = perpetualMarketRefresher
+ .getPerpetualMarketsMap();
+
+ const marketIdToMarket: MarketsMap = _.keyBy(
+ markets,
+ MarketColumns.id,
+ );
+
+ return {
+ positions: updatedPerpetualPositions.map((position: PerpetualPositionWithFunding) => {
+ return perpetualPositionToResponseObject(
+ position,
+ perpetualMarketsMap,
+ marketIdToMarket,
+ childIdtoSubaccountNumber[position.subaccountId],
+ );
+ }),
+ };
+ }
+}
+
+async function adjustPerpetualPositionsWithUpdatedFunding(
+ perpetualPositions: PerpetualPositionWithFunding[],
+ subaccount: SubaccountFromDatabase,
+ latestBlock: BlockFromDatabase,
+): Promise {
+ const {
+ lastUpdatedFundingIndexMap,
+ latestFundingIndexMap,
+ }: {
+ lastUpdatedFundingIndexMap: FundingIndexMap,
+ latestFundingIndexMap: FundingIndexMap,
+ } = await getFundingIndexMaps(subaccount, latestBlock);
+ return getPerpetualPositionsWithUpdatedFunding(
+ perpetualPositions,
+ latestFundingIndexMap,
+ lastUpdatedFundingIndexMap,
+ );
}
router.get(
@@ -216,4 +359,70 @@ router.get(
},
);
+router.get(
+ '/parentSubaccountNumber',
+ rateLimiterMiddleware(getReqRateLimiter),
+ ...CheckParentSubaccountSchema,
+ ...CheckLimitAndCreatedBeforeOrAtSchema,
+ ...checkSchema({
+ status: {
+ in: ['query'],
+ optional: true,
+ customSanitizer: {
+ options: sanitizeArray,
+ },
+ custom: {
+ options: (inputArray) => validateArray(inputArray, Object.values(PerpetualPositionStatus)),
+ errorMessage: 'status must be a valid Position Status (OPEN, etc)',
+ },
+ },
+ }),
+ handleValidationErrors,
+ complianceAndGeoCheck,
+ ExportResponseCodeStats({ controllerName }),
+ async (req: express.Request, res: express.Response) => {
+ const start: number = Date.now();
+ const {
+ address,
+ parentSubaccountNumber,
+ status,
+ limit,
+ createdBeforeOrAtHeight,
+ createdBeforeOrAt,
+ }: ParentSubaccountPerpetualPositionRequest = matchedData(
+ req,
+ ) as ParentSubaccountPerpetualPositionRequest;
+
+ // The schema checks allow subaccountNumber to be a string, but we know it's a number here.
+ const parentSubaccountNum: number = +parentSubaccountNumber;
+
+ try {
+ const controller: PerpetualPositionsController = new PerpetualPositionsController();
+ const response: PerpetualPositionResponse = await controller.listPositionsForParentSubaccount(
+ address,
+ parentSubaccountNum,
+ status,
+ limit,
+ createdBeforeOrAtHeight,
+ createdBeforeOrAt,
+ );
+
+ return res.send(response);
+ } catch (error) {
+ return handleControllerError(
+ 'PerpetualPositionsController GET /parentSubaccountNumber',
+ 'Perpetual positions error',
+ error,
+ req,
+ res,
+ );
+ } finally {
+ stats.timing(
+ `${config.SERVICE_NAME}.${controllerName}.get_perpetual_positions_parent_subaccount.timing`,
+ Date.now() - start,
+ );
+ }
+ },
+);
+
export default router;
diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts
index c61da8f2aa..61bf8e4007 100644
--- a/indexer/services/comlink/src/types.ts
+++ b/indexer/services/comlink/src/types.ts
@@ -374,6 +374,11 @@ export interface PerpetualPositionRequest extends SubaccountRequest, LimitAndCre
status: PerpetualPositionStatus[],
}
+export interface ParentSubaccountPerpetualPositionRequest extends ParentSubaccountRequest,
+ LimitAndCreatedBeforeRequest {
+ status: PerpetualPositionStatus[],
+}
+
export interface AssetPositionRequest extends SubaccountRequest {}
export interface ParentSubaccountAssetPositionRequest extends ParentSubaccountRequest {