diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/historical-pnl-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/historical-pnl-controller.test.ts
index 6371f6c2b1..6436dae861 100644
--- a/indexer/services/comlink/__tests__/controllers/api/v4/historical-pnl-controller.test.ts
+++ b/indexer/services/comlink/__tests__/controllers/api/v4/historical-pnl-controller.test.ts
@@ -207,5 +207,65 @@ describe('pnlTicks-controller#V4', () => {
],
});
});
+
+ it('Get /historical-pnl/parentSubaccountNumber', async () => {
+ await testMocks.seedData();
+ const pnlTick2: PnlTicksCreateObject = {
+ ...testConstants.defaultPnlTick,
+ subaccountId: testConstants.isolatedSubaccountId,
+ };
+ await Promise.all([
+ PnlTicksTable.create(testConstants.defaultPnlTick),
+ PnlTicksTable.create(pnlTick2),
+ ]);
+
+ const parentSubaccountNumber: number = 0;
+ const response: request.Response = await sendRequest({
+ type: RequestMethod.GET,
+ path: `/v4/historical-pnl/parentSubaccountNumber?address=${testConstants.defaultAddress}` +
+ `&parentSubaccountNumber=${parentSubaccountNumber}`,
+ });
+
+ const expectedPnlTickResponse: any = {
+ // id and subaccountId don't matter
+ equity: (parseFloat(testConstants.defaultPnlTick.equity) +
+ parseFloat(pnlTick2.equity)).toString(),
+ totalPnl: (parseFloat(testConstants.defaultPnlTick.totalPnl) +
+ parseFloat(pnlTick2.totalPnl)).toString(),
+ netTransfers: (parseFloat(testConstants.defaultPnlTick.netTransfers) +
+ parseFloat(pnlTick2.netTransfers)).toString(),
+ createdAt: testConstants.defaultPnlTick.createdAt,
+ blockHeight: testConstants.defaultPnlTick.blockHeight,
+ blockTime: testConstants.defaultPnlTick.blockTime,
+ };
+
+ expect(response.body.historicalPnl).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ ...expectedPnlTickResponse,
+ }),
+ ]),
+ );
+ });
+ });
+
+ it('Get /historical-pnl/parentSubaccountNumber with invalid subaccount number returns error', async () => {
+ const response: request.Response = await sendRequest({
+ type: RequestMethod.GET,
+ path: `/v4/historical-pnl/parentSubaccountNumber?address=${testConstants.defaultAddress}` +
+ '&parentSubaccountNumber=128',
+ expectedStatus: 400,
+ });
+
+ expect(response.body).toEqual({
+ errors: [
+ {
+ location: 'query',
+ msg: 'parentSubaccountNumber must be a non-negative integer less than 128',
+ param: 'parentSubaccountNumber',
+ value: '128',
+ },
+ ],
+ });
});
});
diff --git a/indexer/services/comlink/public/api-documentation.md b/indexer/services/comlink/public/api-documentation.md
index dec8c188c5..f980b2dd9a 100644
--- a/indexer/services/comlink/public/api-documentation.md
+++ b/indexer/services/comlink/public/api-documentation.md
@@ -1213,6 +1213,91 @@ fetch('https://dydx-testnet.imperator.co/v4/historical-pnl?address=string&subacc
This operation does not require authentication
+## GetHistoricalPnlForParentSubaccount
+
+
+
+> Code samples
+
+```python
+import requests
+headers = {
+ 'Accept': 'application/json'
+}
+
+r = requests.get('https://dydx-testnet.imperator.co/v4/historical-pnl/parentSubaccount', params={
+ 'address': 'string', 'parentSubaccountNumber': '0'
+}, headers = headers)
+
+print(r.json())
+
+```
+
+```javascript
+
+const headers = {
+ 'Accept':'application/json'
+};
+
+fetch('https://dydx-testnet.imperator.co/v4/historical-pnl/parentSubaccount?address=string&parentSubaccountNumber=0',
+{
+ method: 'GET',
+
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+`GET /historical-pnl/parentSubaccount`
+
+### Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|address|query|string|true|none|
+|parentSubaccountNumber|query|number(double)|true|none|
+|limit|query|number(double)|false|none|
+|createdBeforeOrAtHeight|query|number(double)|false|none|
+|createdBeforeOrAt|query|[IsoString](#schemaisostring)|false|none|
+|createdOnOrAfterHeight|query|number(double)|false|none|
+|createdOnOrAfter|query|[IsoString](#schemaisostring)|false|none|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "historicalPnl": [
+ {
+ "id": "string",
+ "subaccountId": "string",
+ "equity": "string",
+ "totalPnl": "string",
+ "netTransfers": "string",
+ "createdAt": "string",
+ "blockHeight": "string",
+ "blockTime": "string"
+ }
+ ]
+}
+```
+
+### Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Ok|[HistoricalPnlResponse](#schemahistoricalpnlresponse)|
+
+
+
## GetAggregations
diff --git a/indexer/services/comlink/public/swagger.json b/indexer/services/comlink/public/swagger.json
index 4c0de94744..13adf05a63 100644
--- a/indexer/services/comlink/public/swagger.json
+++ b/indexer/services/comlink/public/swagger.json
@@ -1887,6 +1887,86 @@
]
}
},
+ "/historical-pnl/parentSubaccount": {
+ "get": {
+ "operationId": "GetHistoricalPnlForParentSubaccount",
+ "responses": {
+ "200": {
+ "description": "Ok",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HistoricalPnlResponse"
+ }
+ }
+ }
+ }
+ },
+ "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": "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"
+ }
+ },
+ {
+ "in": "query",
+ "name": "createdOnOrAfterHeight",
+ "required": false,
+ "schema": {
+ "format": "double",
+ "type": "number"
+ }
+ },
+ {
+ "in": "query",
+ "name": "createdOnOrAfter",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/IsoString"
+ }
+ }
+ ]
+ }
+ },
"/historicalTradingRewardAggregations/{address}": {
"get": {
"operationId": "GetAggregations",
diff --git a/indexer/services/comlink/src/controllers/api/v4/historical-pnl-controller.ts b/indexer/services/comlink/src/controllers/api/v4/historical-pnl-controller.ts
index 4d4b116ec2..6f70c54ca8 100644
--- a/indexer/services/comlink/src/controllers/api/v4/historical-pnl-controller.ts
+++ b/indexer/services/comlink/src/controllers/api/v4/historical-pnl-controller.ts
@@ -19,16 +19,16 @@ import { getReqRateLimiter } from '../../../caches/rate-limiters';
import config from '../../../config';
import { complianceAndGeoCheck } from '../../../lib/compliance-and-geo-check';
import { NotFoundError } from '../../../lib/errors';
-import { handleControllerError } from '../../../lib/helpers';
+import { getChildSubaccountIds, handleControllerError } from '../../../lib/helpers';
import { rateLimiterMiddleware } from '../../../lib/rate-limit';
import {
- CheckLimitAndCreatedBeforeOrAtAndOnOrAfterSchema,
+ CheckLimitAndCreatedBeforeOrAtAndOnOrAfterSchema, CheckParentSubaccountSchema,
CheckSubaccountSchema,
} from '../../../lib/validation/schemas';
import { handleValidationErrors } from '../../../request-helpers/error-handler';
import ExportResponseCodeStats from '../../../request-helpers/export-response-code-stats';
import { pnlTicksToResponseObject } from '../../../request-helpers/request-transformer';
-import { PnlTicksRequest, HistoricalPnlResponse } from '../../../types';
+import { PnlTicksRequest, HistoricalPnlResponse, ParentSubaccountPnlTicksRequest } from '../../../types';
const router: express.Router = express.Router();
const controllerName: string = 'historical-pnl-controller';
@@ -86,6 +86,84 @@ class HistoricalPnlController extends Controller {
}),
};
}
+
+ @Get('/parentSubaccount')
+ async getHistoricalPnlForParentSubaccount(
+ @Query() address: string,
+ @Query() parentSubaccountNumber: number,
+ @Query() limit?: number,
+ @Query() createdBeforeOrAtHeight?: number,
+ @Query() createdBeforeOrAt?: IsoString,
+ @Query() createdOnOrAfterHeight?: number,
+ @Query() createdOnOrAfter?: IsoString,
+ ): Promise {
+
+ const childSubaccountIds: string[] = getChildSubaccountIds(address, parentSubaccountNumber);
+
+ const [subaccounts, pnlTicks]: [
+ SubaccountFromDatabase[],
+ PnlTicksFromDatabase[],
+ ] = await Promise.all([
+ SubaccountTable.findAll(
+ {
+ id: childSubaccountIds,
+ },
+ [QueryableField.ID],
+ ),
+ PnlTicksTable.findAll(
+ {
+ subaccountId: childSubaccountIds,
+ limit,
+ createdBeforeOrAtBlockHeight: createdBeforeOrAtHeight
+ ? createdBeforeOrAtHeight.toString()
+ : undefined,
+ createdBeforeOrAt,
+ createdOnOrAfterBlockHeight: createdOnOrAfterHeight
+ ? createdOnOrAfterHeight.toString()
+ : undefined,
+ createdOnOrAfter,
+ },
+ [QueryableField.LIMIT],
+ {
+ ...DEFAULT_POSTGRES_OPTIONS,
+ orderBy: [[QueryableField.BLOCK_HEIGHT, Ordering.DESC]],
+ },
+ ),
+ ]);
+
+ if (subaccounts.length === 0) {
+ throw new NotFoundError(
+ `No subaccounts found with address ${address} and parentSubaccountNumber ${parentSubaccountNumber}`,
+ );
+ }
+
+ // aggregate pnlTicks for all subaccounts grouped by blockHeight
+ const aggregatedPnlTicks: Map = new Map();
+ for (const pnlTick of pnlTicks) {
+ const blockHeight: number = parseInt(pnlTick.blockHeight, 10);
+ if (aggregatedPnlTicks.has(blockHeight)) {
+ const currentPnlTick: PnlTicksFromDatabase = aggregatedPnlTicks.get(
+ blockHeight,
+ ) as PnlTicksFromDatabase;
+ aggregatedPnlTicks.set(blockHeight, {
+ ...currentPnlTick,
+ equity: (parseFloat(currentPnlTick.equity) + parseFloat(pnlTick.equity)).toString(),
+ totalPnl: (parseFloat(currentPnlTick.totalPnl) + parseFloat(pnlTick.totalPnl)).toString(),
+ netTransfers: (parseFloat(currentPnlTick.netTransfers) +
+ parseFloat(pnlTick.netTransfers)).toString(),
+ });
+ } else {
+ aggregatedPnlTicks.set(blockHeight, pnlTick);
+ }
+ }
+
+ return {
+ historicalPnl: Array.from(aggregatedPnlTicks.values()).map(
+ (pnlTick: PnlTicksFromDatabase) => {
+ return pnlTicksToResponseObject(pnlTick);
+ }),
+ };
+ }
}
router.get(
@@ -138,4 +216,57 @@ router.get(
},
);
+router.get(
+ '/parentSubaccountNumber',
+ rateLimiterMiddleware(getReqRateLimiter),
+ ...CheckParentSubaccountSchema,
+ ...CheckLimitAndCreatedBeforeOrAtAndOnOrAfterSchema,
+ handleValidationErrors,
+ complianceAndGeoCheck,
+ ExportResponseCodeStats({ controllerName }),
+ async (req: express.Request, res: express.Response) => {
+ const start: number = Date.now();
+ const {
+ address,
+ parentSubaccountNumber,
+ limit,
+ createdBeforeOrAtHeight,
+ createdBeforeOrAt,
+ createdOnOrAfterHeight,
+ createdOnOrAfter,
+ }: ParentSubaccountPnlTicksRequest = matchedData(req) as ParentSubaccountPnlTicksRequest;
+
+ // The schema checks allow subaccountNumber to be a string, but we know it's a number here.
+ const parentSubaccountNum: number = +parentSubaccountNumber;
+
+ try {
+ const controllers: HistoricalPnlController = new HistoricalPnlController();
+ const response: HistoricalPnlResponse = await controllers.getHistoricalPnlForParentSubaccount(
+ address,
+ parentSubaccountNum,
+ limit,
+ createdBeforeOrAtHeight,
+ createdBeforeOrAt,
+ createdOnOrAfterHeight,
+ createdOnOrAfter,
+ );
+
+ return res.send(response);
+ } catch (error) {
+ return handleControllerError(
+ 'HistoricalPnlController GET /parentSubaccountNumber',
+ 'Historical Pnl error',
+ error,
+ req,
+ res,
+ );
+ } finally {
+ stats.timing(
+ `${config.SERVICE_NAME}.${controllerName}.get_historical_pnl_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 6f3f3c46a5..9d7330fbc6 100644
--- a/indexer/services/comlink/src/types.ts
+++ b/indexer/services/comlink/src/types.ts
@@ -413,6 +413,10 @@ export interface PerpetualMarketRequest extends LimitRequest, TickerRequest {}
export interface PnlTicksRequest extends SubaccountRequest, LimitAndCreatedBeforeAndAfterRequest {}
+export interface ParentSubaccountPnlTicksRequest
+ extends ParentSubaccountRequest, LimitAndCreatedBeforeAndAfterRequest {
+}
+
export interface OrderbookRequest {
ticker: string,
}