From eadfc82853ad9a8ca84df03d851c9e1b4590de15 Mon Sep 17 00:00:00 2001 From: shrenujb <98204323+shrenujb@users.noreply.github.com> Date: Wed, 8 May 2024 10:50:46 -0400 Subject: [PATCH] [TRA-107] Add parentSubaccountNumber endpoint for historical-pnl (#1476) Signed-off-by: Shrenuj Bansal --- .../api/v4/historical-pnl-controller.test.ts | 60 ++++++++ .../comlink/public/api-documentation.md | 85 +++++++++++ indexer/services/comlink/public/swagger.json | 80 ++++++++++ .../api/v4/historical-pnl-controller.ts | 137 +++++++++++++++++- indexer/services/comlink/src/types.ts | 4 + 5 files changed, 363 insertions(+), 3 deletions(-) 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, }