diff --git a/indexer/packages/postgres/__tests__/helpers/constants.ts b/indexer/packages/postgres/__tests__/helpers/constants.ts index 84791623a0..7fb5214d1c 100644 --- a/indexer/packages/postgres/__tests__/helpers/constants.ts +++ b/indexer/packages/postgres/__tests__/helpers/constants.ts @@ -179,6 +179,16 @@ export const defaultAssetPositionId2: string = AssetPositionTable.uuid( defaultAssetPosition2.subaccountId, defaultAssetPosition2.assetId, ); +export const isolatedSubaccountAssetPosition: AssetPositionCreateObject = { + subaccountId: isolatedSubaccountId, + assetId: '0', + size: '5000', + isLong: true, +}; +export const isolatedSubaccountAssetPositionId: string = AssetPositionTable.uuid( + isolatedSubaccountAssetPosition.subaccountId, + isolatedSubaccountAssetPosition.assetId, +); // ============== PerpetualMarkets ============== diff --git a/indexer/packages/postgres/__tests__/lib/api-translations.test.ts b/indexer/packages/postgres/__tests__/lib/api-translations.test.ts index 2ba80bc4a3..ec54f29eae 100644 --- a/indexer/packages/postgres/__tests__/lib/api-translations.test.ts +++ b/indexer/packages/postgres/__tests__/lib/api-translations.test.ts @@ -1,7 +1,5 @@ import { APITimeInForce, TimeInForce } from '../../src/types'; import { - getChildSubaccountNums, - getParentSubaccountNum, isOrderTIFPostOnly, orderTIFToAPITIF, } from '../../src/lib/api-translations'; @@ -36,41 +34,4 @@ describe('apiTranslations', () => { expect(isOrderTIFPostOnly(orderTimeInForce)).toEqual(expectedPostOnly); }); }); - - describe('getChildSubaccountNums', () => { - it('Gets a list of all possible child subaccount numbers for a parent subaccount 0', () => { - const childSubaccounts = getChildSubaccountNums(0); - expect(childSubaccounts.length).toEqual(1000); - expect(childSubaccounts[0]).toEqual(0); - expect(childSubaccounts[1]).toEqual(128); - expect(childSubaccounts[999]).toEqual(128 * 999); - }); - it('Gets a list of all possible child subaccount numbers for a parent subaccount 127', () => { - const childSubaccounts = getChildSubaccountNums(127); - expect(childSubaccounts.length).toEqual(1000); - expect(childSubaccounts[0]).toEqual(127); - expect(childSubaccounts[1]).toEqual(128 + 127); - expect(childSubaccounts[999]).toEqual(128 * 999 + 127); - }); - }); - - describe('getChildSubaccountNums', () => { - it('Throws an error if the parent subaccount number is greater than or equal to the maximum parent subaccount number', () => { - expect(() => getChildSubaccountNums(128)).toThrowError('Parent subaccount number must be less than 128'); - }); - }); - - describe('getParentSubaccountNum', () => { - it('Gets the parent subaccount number from a child subaccount number', () => { - expect(getParentSubaccountNum(0)).toEqual(0); - expect(getParentSubaccountNum(128)).toEqual(0); - expect(getParentSubaccountNum(128 * 999 - 1)).toEqual(127); - }); - }); - - describe('getParentSubaccountNum', () => { - it('Throws an error if the child subaccount number is greater than the max child subaccount number', () => { - expect(() => getParentSubaccountNum(128001)).toThrowError('Child subaccount number must be less than 128000'); - }); - }); }); diff --git a/indexer/packages/postgres/src/lib/api-translations.ts b/indexer/packages/postgres/src/lib/api-translations.ts index c6e69e248f..27c776b91e 100644 --- a/indexer/packages/postgres/src/lib/api-translations.ts +++ b/indexer/packages/postgres/src/lib/api-translations.ts @@ -1,4 +1,4 @@ -import { TIME_IN_FORCE_TO_API_TIME_IN_FORCE, CHILD_SUBACCOUNT_MULTIPLIER, MAX_PARENT_SUBACCOUNTS } from '../constants'; +import { TIME_IN_FORCE_TO_API_TIME_IN_FORCE } from '../constants'; import { APITimeInForce, TimeInForce } from '../types'; /** @@ -20,30 +20,3 @@ export function isOrderTIFPostOnly(timeInForce: TimeInForce): boolean { export function orderTIFToAPITIF(timeInForce: TimeInForce): APITimeInForce { return TIME_IN_FORCE_TO_API_TIME_IN_FORCE[timeInForce]; } - -/** - * Gets a list of all possible child subaccount numbers for a parent subaccount number - * Child subaccounts = [128*0+parentSubaccount, 128*1+parentSubaccount ... 128*999+parentSubaccount] - * @param parentSubaccount - * @returns - */ -export function getChildSubaccountNums(parentSubaccountNum: number): number[] { - if (parentSubaccountNum >= MAX_PARENT_SUBACCOUNTS) { - throw new Error(`Parent subaccount number must be less than ${MAX_PARENT_SUBACCOUNTS}`); - } - return Array.from({ length: CHILD_SUBACCOUNT_MULTIPLIER }, - (_, i) => MAX_PARENT_SUBACCOUNTS * i + parentSubaccountNum); -} - -/** - * Gets the parent subaccount number from a child subaccount number - * Parent subaccount = childSubaccount % 128 - * @param childSubaccountNum - * @returns - */ -export function getParentSubaccountNum(childSubaccountNum: number): number { - if (childSubaccountNum > MAX_PARENT_SUBACCOUNTS * CHILD_SUBACCOUNT_MULTIPLIER) { - throw new Error(`Child subaccount number must be less than ${MAX_PARENT_SUBACCOUNTS * CHILD_SUBACCOUNT_MULTIPLIER}`); - } - return childSubaccountNum % MAX_PARENT_SUBACCOUNTS; -} diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/addresses-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/addresses-controller.test.ts index e28feb7e21..119d085d40 100644 --- a/indexer/services/comlink/__tests__/controllers/api/v4/addresses-controller.test.ts +++ b/indexer/services/comlink/__tests__/controllers/api/v4/addresses-controller.test.ts @@ -15,6 +15,7 @@ import { RequestMethod } from '../../../../src/types'; import request from 'supertest'; import { getFixedRepresentation, sendRequest } from '../../../helpers/helpers'; import { stats } from '@dydxprotocol-indexer/base'; +import { defaultAddress } from '@dydxprotocol-indexer/postgres/build/__tests__/helpers/constants'; describe('addresses-controller#V4', () => { const latestHeight: string = '3'; @@ -379,4 +380,176 @@ describe('addresses-controller#V4', () => { }); }); }); + + describe('/addresses/:address/parentSubaccountNumber/:parentSubaccountNumber', () => { + afterEach(async () => { + await dbHelpers.clearData(); + }); + + it('Get /:address/parentSubaccountNumber/ gets all subaccounts for the provided parent', async () => { + await PerpetualPositionTable.create( + testConstants.defaultPerpetualPosition, + ); + + await Promise.all([ + AssetPositionTable.upsert(testConstants.defaultAssetPosition), + AssetPositionTable.upsert({ + ...testConstants.defaultAssetPosition2, + subaccountId: testConstants.defaultSubaccountId, + }), + AssetPositionTable.upsert(testConstants.isolatedSubaccountAssetPosition), + FundingIndexUpdatesTable.create({ + ...testConstants.defaultFundingIndexUpdate, + fundingIndex: initialFundingIndex, + effectiveAtHeight: testConstants.createdHeight, + }), + FundingIndexUpdatesTable.create({ + ...testConstants.defaultFundingIndexUpdate, + eventId: testConstants.defaultTendermintEventId2, + effectiveAtHeight: latestHeight, + }), + ]); + + const parentSubaccountNumber: number = 0; + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/addresses/${testConstants.defaultAddress}/parentSubaccountNumber/${parentSubaccountNumber}`, + }); + + expect(response.body).toEqual({ + subaccount: { + address: testConstants.defaultAddress, + parentSubaccountNumber, + equity: getFixedRepresentation(164500), + freeCollateral: getFixedRepresentation(157000), + childSubaccounts: [ + { + address: testConstants.defaultAddress, + subaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + equity: getFixedRepresentation(159500), + freeCollateral: getFixedRepresentation(152000), + marginEnabled: true, + openPerpetualPositions: { + [testConstants.defaultPerpetualMarket.ticker]: { + market: testConstants.defaultPerpetualMarket.ticker, + size: testConstants.defaultPerpetualPosition.size, + side: testConstants.defaultPerpetualPosition.side, + entryPrice: getFixedRepresentation( + testConstants.defaultPerpetualPosition.entryPrice!, + ), + maxSize: testConstants.defaultPerpetualPosition.maxSize, + // 200000 + 10*(10000-10050)=199500 + 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'), + // size * (index-entry) = 10*(15000-20000) = -50000 + unrealizedPnl: getFixedRepresentation(-50000), + status: testConstants.defaultPerpetualPosition.status, + sumOpen: testConstants.defaultPerpetualPosition.sumOpen, + sumClose: testConstants.defaultPerpetualPosition.sumClose, + createdAt: testConstants.defaultPerpetualPosition.createdAt, + createdAtHeight: testConstants.defaultPerpetualPosition.createdAtHeight, + exitPrice: null, + closedAt: null, + }, + }, + assetPositions: { + [testConstants.defaultAsset.symbol]: { + symbol: testConstants.defaultAsset.symbol, + size: '9500', + side: PositionSide.LONG, + assetId: testConstants.defaultAssetPosition.assetId, + subaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + [testConstants.defaultAsset2.symbol]: { + symbol: testConstants.defaultAsset2.symbol, + size: testConstants.defaultAssetPosition2.size, + side: PositionSide.SHORT, + assetId: testConstants.defaultAssetPosition2.assetId, + subaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + }, + }, + { + address: testConstants.defaultAddress, + subaccountNumber: testConstants.isolatedSubaccount.subaccountNumber, + equity: getFixedRepresentation(5000), + freeCollateral: getFixedRepresentation(5000), + marginEnabled: true, + openPerpetualPositions: {}, + assetPositions: { + [testConstants.defaultAsset.symbol]: { + symbol: testConstants.defaultAsset.symbol, + size: testConstants.isolatedSubaccountAssetPosition.size, + side: PositionSide.LONG, + assetId: testConstants.isolatedSubaccountAssetPosition.assetId, + subaccountNumber: testConstants.isolatedSubaccount.subaccountNumber, + }, + }, + }, + { + address: testConstants.defaultAddress, + subaccountNumber: testConstants.isolatedSubaccount2.subaccountNumber, + equity: getFixedRepresentation(0), + freeCollateral: getFixedRepresentation(0), + marginEnabled: true, + openPerpetualPositions: {}, + assetPositions: {}, + }, + ], + }, + }); + expect(stats.increment).toHaveBeenCalledWith('comlink.addresses-controller.response_status_code.200', 1, + { + path: '/:address/parentSubaccountNumber/:parentSubaccountNumber', + method: 'GET', + }); + }); + }); + + it('Get /:address/parentSubaccountNumber/ with non-existent address returns 404', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/addresses/${invalidAddress}/parentSubaccountNumber/` + + `${testConstants.defaultSubaccount.subaccountNumber}`, + expectedStatus: 404, + }); + + expect(response.body).toEqual({ + errors: [ + { + msg: `No subaccounts found for address ${invalidAddress} and ` + + `parentSubaccountNumber ${testConstants.defaultSubaccount.subaccountNumber}`, + }, + ], + }); + expect(stats.increment).toHaveBeenCalledWith('comlink.addresses-controller.response_status_code.404', 1, + { + path: '/:address/parentSubaccountNumber/:parentSubaccountNumber', + method: 'GET', + }); + }); + + it('Get /:address/parentSubaccountNumber/ with invalid parentSubaccount number returns 400', async () => { + const parentSubaccountNumber: number = 128; + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/addresses/${defaultAddress}/parentSubaccountNumber/${parentSubaccountNumber}`, + expectedStatus: 400, + }); + + expect(response.body).toEqual({ + errors: [ + { + location: 'params', + msg: 'parentSubaccountNumber must be a non-negative integer less than 128', + param: 'parentSubaccountNumber', + value: '128', + }, + ], + }); + }); + }); diff --git a/indexer/services/comlink/__tests__/lib/helpers.test.ts b/indexer/services/comlink/__tests__/lib/helpers.test.ts index 815d1382cc..9f6728b522 100644 --- a/indexer/services/comlink/__tests__/lib/helpers.test.ts +++ b/indexer/services/comlink/__tests__/lib/helpers.test.ts @@ -39,7 +39,7 @@ import { getSignedNotionalAndRisk, getTotalUnsettledFunding, getPerpetualPositionsWithUpdatedFunding, - initializePerpetualPositionsWithFunding, + initializePerpetualPositionsWithFunding, getChildSubaccountNums, getParentSubaccountNum, } from '../../src/lib/helpers'; import _ from 'lodash'; import Big from 'big.js'; @@ -697,4 +697,41 @@ describe('helpers', () => { .toEqual('0'); }); }); + + describe('getChildSubaccountNums', () => { + it('Gets a list of all possible child subaccount numbers for a parent subaccount 0', () => { + const childSubaccounts = getChildSubaccountNums(0); + expect(childSubaccounts.length).toEqual(1000); + expect(childSubaccounts[0]).toEqual(0); + expect(childSubaccounts[1]).toEqual(128); + expect(childSubaccounts[999]).toEqual(128 * 999); + }); + it('Gets a list of all possible child subaccount numbers for a parent subaccount 127', () => { + const childSubaccounts = getChildSubaccountNums(127); + expect(childSubaccounts.length).toEqual(1000); + expect(childSubaccounts[0]).toEqual(127); + expect(childSubaccounts[1]).toEqual(128 + 127); + expect(childSubaccounts[999]).toEqual(128 * 999 + 127); + }); + }); + + describe('getChildSubaccountNums', () => { + it('Throws an error if the parent subaccount number is greater than or equal to the maximum parent subaccount number', () => { + expect(() => getChildSubaccountNums(128)).toThrowError('Parent subaccount number must be less than 128'); + }); + }); + + describe('getParentSubaccountNum', () => { + it('Gets the parent subaccount number from a child subaccount number', () => { + expect(getParentSubaccountNum(0)).toEqual(0); + expect(getParentSubaccountNum(128)).toEqual(0); + expect(getParentSubaccountNum(128 * 999 - 1)).toEqual(127); + }); + }); + + describe('getParentSubaccountNum', () => { + it('Throws an error if the child subaccount number is greater than the max child subaccount number', () => { + expect(() => getParentSubaccountNum(128001)).toThrowError('Child subaccount number must be less than 128000'); + }); + }); }); diff --git a/indexer/services/comlink/public/api-documentation.md b/indexer/services/comlink/public/api-documentation.md index 50a9a43c87..d1af0c5567 100644 --- a/indexer/services/comlink/public/api-documentation.md +++ b/indexer/services/comlink/public/api-documentation.md @@ -261,6 +261,137 @@ fetch('https://dydx-testnet.imperator.co/v4/addresses/{address}/subaccountNumber This operation does not require authentication +## GetParentSubaccount + + + +> Code samples + +```python +import requests +headers = { + 'Accept': 'application/json' +} + +r = requests.get('https://dydx-testnet.imperator.co/v4/addresses/{address}/parentSubaccountNumber/{parentSubaccountNumber}', headers = headers) + +print(r.json()) + +``` + +```javascript + +const headers = { + 'Accept':'application/json' +}; + +fetch('https://dydx-testnet.imperator.co/v4/addresses/{address}/parentSubaccountNumber/{parentSubaccountNumber}', +{ + method: 'GET', + + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +`GET /addresses/{address}/parentSubaccountNumber/{parentSubaccountNumber}` + +### Parameters + +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|address|path|string|true|none| +|parentSubaccountNumber|path|number(double)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "address": "string", + "parentSubaccountNumber": 0, + "equity": "string", + "freeCollateral": "string", + "childSubaccounts": [ + { + "address": "string", + "subaccountNumber": 0, + "equity": "string", + "freeCollateral": "string", + "openPerpetualPositions": { + "property1": { + "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": null, + "exitPrice": "string" + }, + "property2": { + "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": null, + "exitPrice": "string" + } + }, + "assetPositions": { + "property1": { + "symbol": "string", + "side": "LONG", + "size": "string", + "assetId": "string", + "subaccountNumber": 0 + }, + "property2": { + "symbol": "string", + "side": "LONG", + "size": "string", + "assetId": "string", + "subaccountNumber": 0 + } + }, + "marginEnabled": true + } + ] +} +``` + +### Responses + +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Ok|[ParentSubaccountResponse](#schemaparentsubaccountresponse)| + + + ## GetAssetPositions @@ -2353,6 +2484,94 @@ This operation does not require authentication |subaccounts|[[SubaccountResponseObject](#schemasubaccountresponseobject)]|true|none|none| |totalTradingRewards|string|true|none|none| +## ParentSubaccountResponse + + + + + + +```json +{ + "address": "string", + "parentSubaccountNumber": 0, + "equity": "string", + "freeCollateral": "string", + "childSubaccounts": [ + { + "address": "string", + "subaccountNumber": 0, + "equity": "string", + "freeCollateral": "string", + "openPerpetualPositions": { + "property1": { + "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": null, + "exitPrice": "string" + }, + "property2": { + "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": null, + "exitPrice": "string" + } + }, + "assetPositions": { + "property1": { + "symbol": "string", + "side": "LONG", + "size": "string", + "assetId": "string", + "subaccountNumber": 0 + }, + "property2": { + "symbol": "string", + "side": "LONG", + "size": "string", + "assetId": "string", + "subaccountNumber": 0 + } + }, + "marginEnabled": true + } + ] +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|address|string|true|none|none| +|parentSubaccountNumber|number(double)|true|none|none| +|equity|string|true|none|none| +|freeCollateral|string|true|none|none| +|childSubaccounts|[[SubaccountResponseObject](#schemasubaccountresponseobject)]|true|none|none| + ## AssetPositionResponse diff --git a/indexer/services/comlink/public/swagger.json b/indexer/services/comlink/public/swagger.json index ad36977d51..9efb4d4804 100644 --- a/indexer/services/comlink/public/swagger.json +++ b/indexer/services/comlink/public/swagger.json @@ -195,6 +195,38 @@ "type": "object", "additionalProperties": false }, + "ParentSubaccountResponse": { + "properties": { + "address": { + "type": "string" + }, + "parentSubaccountNumber": { + "type": "number", + "format": "double" + }, + "equity": { + "type": "string" + }, + "freeCollateral": { + "type": "string" + }, + "childSubaccounts": { + "items": { + "$ref": "#/components/schemas/SubaccountResponseObject" + }, + "type": "array" + } + }, + "required": [ + "address", + "parentSubaccountNumber", + "equity", + "freeCollateral", + "childSubaccounts" + ], + "type": "object", + "additionalProperties": false + }, "AssetPositionResponse": { "properties": { "positions": { @@ -1213,6 +1245,43 @@ ] } }, + "/addresses/{address}/parentSubaccountNumber/{parentSubaccountNumber}": { + "get": { + "operationId": "GetParentSubaccount", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ParentSubaccountResponse" + } + } + } + } + }, + "security": [], + "parameters": [ + { + "in": "path", + "name": "address", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "parentSubaccountNumber", + "required": true, + "schema": { + "format": "double", + "type": "number" + } + } + ] + } + }, "/assetPositions": { "get": { "operationId": "GetAssetPositions", diff --git a/indexer/services/comlink/src/controllers/api/v4/addresses-controller.ts b/indexer/services/comlink/src/controllers/api/v4/addresses-controller.ts index 1539fea52c..cd5b1f77e9 100644 --- a/indexer/services/comlink/src/controllers/api/v4/addresses-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/addresses-controller.ts @@ -49,9 +49,10 @@ import { handleControllerError, getPerpetualPositionsWithUpdatedFunding, initializePerpetualPositionsWithFunding, + getChildSubaccountIds, } from '../../../lib/helpers'; import { rateLimiterMiddleware } from '../../../lib/rate-limit'; -import { CheckAddressSchema, CheckSubaccountSchema } from '../../../lib/validation/schemas'; +import { CheckAddressSchema, CheckParentSubaccountSchema, CheckSubaccountSchema } from '../../../lib/validation/schemas'; import { handleValidationErrors } from '../../../request-helpers/error-handler'; import ExportResponseCodeStats from '../../../request-helpers/export-response-code-stats'; import { @@ -69,7 +70,7 @@ import { AssetPositionResponseObject, AssetPositionsMap, PerpetualPositionWithFunding, - AddressResponse, + AddressResponse, ParentSubaccountResponse, ParentSubaccountRequest, } from '../../../types'; const router: express.Router = express.Router(); @@ -241,6 +242,105 @@ class AddressesController extends Controller { ); return subaccountResponse; } + + @Get('/:address/parentSubaccountNumber/:parentSubaccountNumber') + public async getParentSubaccount( + @Path() address: string, + @Path() parentSubaccountNumber: number, + ): Promise { + + const childSubaccountIds: string[] = getChildSubaccountIds(address, parentSubaccountNumber); + + // TODO(IND-189): Use a transaction across all the DB queries + const [subaccounts, latestBlock]: [ + SubaccountFromDatabase[], + BlockFromDatabase, + ] = await Promise.all([ + SubaccountTable.findAll( + { + id: childSubaccountIds, + address, + }, + [], + ), + BlockTable.getLatest(), + ]); + + if (subaccounts.length === 0) { + throw new NotFoundError(`No subaccounts found for address ${address} and parentSubaccountNumber ${parentSubaccountNumber}`); + } + + const latestFundingIndexMap: FundingIndexMap = await FundingIndexUpdatesTable + .findFundingIndexMap( + latestBlock.blockHeight, + ); + + const [assets, markets]: [AssetFromDatabase[], MarketFromDatabase[]] = await Promise.all([ + AssetTable.findAll( + {}, + [], + ), + MarketTable.findAll( + {}, + [], + ), + ]); + const subaccountResponses: SubaccountResponseObject[] = await Promise.all(subaccounts.map( + async (subaccount: SubaccountFromDatabase): Promise => { + const [ + perpetualPositions, + assetPositions, + lastUpdatedFundingIndexMap, + ] = await Promise.all([ + getOpenPerpetualPositionsForSubaccount( + subaccount.id, + ), + getAssetPositionsForSubaccount( + subaccount.id, + ), + FundingIndexUpdatesTable.findFundingIndexMap( + subaccount.updatedAtHeight, + ), + ]); + const unsettledFunding: Big = getTotalUnsettledFunding( + perpetualPositions, + latestFundingIndexMap, + lastUpdatedFundingIndexMap, + ); + + const updatedPerpetualPositions: + PerpetualPositionWithFunding[] = getPerpetualPositionsWithUpdatedFunding( + initializePerpetualPositionsWithFunding(perpetualPositions), + latestFundingIndexMap, + lastUpdatedFundingIndexMap, + ); + + return getSubaccountResponse( + subaccount, + updatedPerpetualPositions, + assetPositions, + assets, + markets, + unsettledFunding, + ); + }, + )); + + return { + address, + parentSubaccountNumber, + equity: subaccountResponses.reduce( + (acc: Big, subaccount: SubaccountResponseObject): Big => acc.plus(subaccount.equity), + Big(0), + ).toString(), + freeCollateral: subaccountResponses.reduce( + // eslint-disable-next-line max-len + (acc: Big, subaccount: SubaccountResponseObject): Big => acc.plus(subaccount.freeCollateral), + Big(0), + ).toString(), + childSubaccounts: subaccountResponses, + }; + } } router.get( @@ -251,6 +351,7 @@ router.get( complianceAndGeoCheck, ExportResponseCodeStats({ controllerName }), async (req: express.Request, res: express.Response) => { + const start: number = Date.now(); const { address, }: { @@ -272,6 +373,11 @@ router.get( req, res, ); + } finally { + stats.timing( + `${config.SERVICE_NAME}.${controllerName}.get_addresses.timing`, + Date.now() - start, + ); } }, ); @@ -313,7 +419,54 @@ router.get( ); } finally { stats.timing( - `${config.SERVICE_NAME}.${controllerName}.get_addresses.timing`, + `${config.SERVICE_NAME}.${controllerName}.get_subaccount.timing`, + Date.now() - start, + ); + } + }, +); + +router.get( + '/:address/parentSubaccountNumber/:parentSubaccountNumber', + rateLimiterMiddleware(getReqRateLimiter), + ...CheckParentSubaccountSchema, + handleValidationErrors, + complianceAndGeoCheck, + ExportResponseCodeStats({ controllerName }), + async (req: express.Request, res: express.Response) => { + const start: number = Date.now(); + const { + address, + parentSubaccountNumber, + }: { + address: string, + parentSubaccountNumber: number, + } = matchedData(req) as ParentSubaccountRequest; + + // The schema checks allow subaccountNumber to be a string, but we know it's a number here. + const parentSubaccountNum = +parentSubaccountNumber; + + try { + const controller: AddressesController = new AddressesController(); + const subaccountResponse: ParentSubaccountResponse = await controller.getParentSubaccount( + address, + parentSubaccountNum, + ); + + return res.send({ + subaccount: subaccountResponse, + }); + } catch (error) { + return handleControllerError( + 'AddressesController GET /:address/parentSubaccountNumber/:parentSubaccountNumber', + 'Addresses subaccount error', + error, + req, + res, + ); + } finally { + stats.timing( + `${config.SERVICE_NAME}.${controllerName}.get_parentSubaccount.timing`, Date.now() - start, ); } @@ -387,6 +540,7 @@ async function getSubaccountResponse( assetPositionResponses, 'symbol', ); + const { assetPositionsMap: adjustedAssetPositionsMap, adjustedUSDCAssetPositionSize, 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 aff2ed3f51..f9ae639b92 100644 --- a/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts @@ -8,7 +8,6 @@ import { FillFromDatabase, QueryableField, } from '@dydxprotocol-indexer/postgres'; -import { getChildSubaccountNums } from '@dydxprotocol-indexer/postgres/build/src/lib/api-translations'; import express from 'express'; import { checkSchema, @@ -25,6 +24,7 @@ import config from '../../../config'; import { complianceAndGeoCheck } from '../../../lib/compliance-and-geo-check'; import { NotFoundError } from '../../../lib/errors'; import { + getChildSubaccountNums, getClobPairId, handleControllerError, isDefined, } from '../../../lib/helpers'; import { rateLimiterMiddleware } from '../../../lib/rate-limit'; diff --git a/indexer/services/comlink/src/lib/helpers.ts b/indexer/services/comlink/src/lib/helpers.ts index cf260aaf97..eab74bf228 100644 --- a/indexer/services/comlink/src/lib/helpers.ts +++ b/indexer/services/comlink/src/lib/helpers.ts @@ -2,6 +2,7 @@ import { logger } from '@dydxprotocol-indexer/base'; import { AssetPositionFromDatabase, BlockFromDatabase, + CHILD_SUBACCOUNT_MULTIPLIER, FundingIndexMap, FundingIndexUpdatesTable, helpers, @@ -9,6 +10,7 @@ import { LiquidityTiersFromDatabase, MarketFromDatabase, MarketsMap, + MAX_PARENT_SUBACCOUNTS, PerpetualMarketFromDatabase, PerpetualMarketsMap, PerpetualMarketTable, @@ -16,6 +18,7 @@ import { PerpetualPositionStatus, PositionSide, SubaccountFromDatabase, + SubaccountTable, TendermintEventFromDatabase, TendermintEventTable, USDC_SYMBOL, @@ -434,7 +437,8 @@ export function adjustUSDCAssetPosition( _.set( adjustedAssetPositionsMap, USDC_SYMBOL, - getUSDCAssetPosition(adjustedSize), + getUSDCAssetPosition(adjustedSize, + adjustedAssetPositionsMap[USDC_SYMBOL]?.subaccountNumber ?? 0), ); // Remove the USDC position in the map if the adjusted size is zero } else { @@ -447,12 +451,14 @@ export function adjustUSDCAssetPosition( }; } -function getUSDCAssetPosition(signedSize: Big): AssetPositionResponseObject { +function getUSDCAssetPosition(signedSize: Big, subaccountNumber: number): + AssetPositionResponseObject { const side: PositionSide = signedSize.gt(ZERO) ? PositionSide.LONG : PositionSide.SHORT; return { ...ZERO_USDC_POSITION, side, size: signedSize.abs().toFixed(), + subaccountNumber, }; } @@ -492,3 +498,43 @@ export function initializePerpetualPositionsWithFunding( }; }); } + +/** + * Gets a list of all possible child subaccount numbers for a parent subaccount number + * Child subaccounts = [128*0+parentSubaccount, 128*1+parentSubaccount ... 128*999+parentSubaccount] + * @param parentSubaccount + * @returns + */ +export function getChildSubaccountNums(parentSubaccountNum: number): number[] { + if (parentSubaccountNum >= MAX_PARENT_SUBACCOUNTS) { + throw new NotFoundError(`Parent subaccount number must be less than ${MAX_PARENT_SUBACCOUNTS}`); + } + return Array.from({ length: CHILD_SUBACCOUNT_MULTIPLIER }, + // eslint-disable-next-line @typescript-eslint/no-shadow + (_, i) => MAX_PARENT_SUBACCOUNTS * i + parentSubaccountNum); +} + +/** + * Gets the subaccount uuids of all the child subaccounts given a parent subaccount number + * @param address + * @param parentSubaccountNum + * @returns + */ +export function getChildSubaccountIds(address: string, parentSubaccountNum: number): string[] { + return getChildSubaccountNums(parentSubaccountNum).map( + (subaccountNumber: number): string => SubaccountTable.uuid(address, subaccountNumber), + ); +} + +/** + * Gets the parent subaccount number from a child subaccount number + * Parent subaccount = childSubaccount % 128 + * @param childSubaccountNum + * @returns + */ +export function getParentSubaccountNum(childSubaccountNum: number): number { + if (childSubaccountNum > MAX_PARENT_SUBACCOUNTS * CHILD_SUBACCOUNT_MULTIPLIER) { + throw new Error(`Child subaccount number must be less than ${MAX_PARENT_SUBACCOUNTS * CHILD_SUBACCOUNT_MULTIPLIER}`); + } + return childSubaccountNum % MAX_PARENT_SUBACCOUNTS; +} diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts index 3a1ba4402e..256f9179ea 100644 --- a/indexer/services/comlink/src/types.ts +++ b/indexer/services/comlink/src/types.ts @@ -57,6 +57,14 @@ export interface SubaccountResponseObject { marginEnabled: boolean, } +export interface ParentSubaccountResponse { + address: string; + parentSubaccountNumber: number; + equity: string; // aggregated over all child subaccounts + freeCollateral: string; // aggregated over all child subaccounts + childSubaccounts: SubaccountResponseObject[]; +} + export type SubaccountById = {[id: string]: SubaccountFromDatabase}; /* ------- TIME TYPES ------- */