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 ------- */