diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/fills-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/fills-controller.test.ts index 80cf5e87cb..9dcbaf9a7b 100644 --- a/indexer/services/comlink/__tests__/controllers/api/v4/fills-controller.test.ts +++ b/indexer/services/comlink/__tests__/controllers/api/v4/fills-controller.test.ts @@ -193,6 +193,91 @@ describe('fills-controller#V4', () => { ); }); + it('Get /fills with market gets fills ordered by createdAtHeight descending and paginated', async () => { + // Order and fill for BTC-USD + await OrderTable.create(testConstants.defaultOrder); + await FillTable.create(testConstants.defaultFill); + + // Order and fill for ETH-USD + const ethOrder: OrderFromDatabase = await OrderTable.create({ + ...testConstants.defaultOrder, + clientId: '3', + clobPairId: testConstants.defaultPerpetualMarket2.clobPairId, + }); + const ethFill: FillFromDatabase = await FillTable.create({ + ...testConstants.defaultFill, + orderId: ethOrder.id, + clobPairId: testConstants.defaultPerpetualMarket2.clobPairId, + eventId: testConstants.defaultTendermintEventId2, + createdAtHeight: '1', + }); + + const responsePage1: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/fills?address=${testConstants.defaultAddress}` + + `&subaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}&page=1&limit=1`, + }); + + const responsePage2: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/fills?address=${testConstants.defaultAddress}` + + `&subaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}&page=2&limit=1`, + }); + + const expected: Partial[] = [ + { + side: testConstants.defaultFill.side, + liquidity: testConstants.defaultFill.liquidity, + market: testConstants.defaultPerpetualMarket.ticker, + marketType: MarketType.PERPETUAL, + price: testConstants.defaultFill.price, + size: testConstants.defaultFill.size, + fee: testConstants.defaultFill.fee, + type: testConstants.defaultFill.type, + orderId: testConstants.defaultFill.orderId, + createdAt: testConstants.defaultFill.createdAt, + createdAtHeight: testConstants.defaultFill.createdAtHeight, + }, + { + side: ethFill.side, + liquidity: ethFill.liquidity, + market: testConstants.defaultPerpetualMarket2.ticker, + marketType: MarketType.PERPETUAL, + price: ethFill.price, + size: ethFill.size, + fee: ethFill.fee, + type: ethFill.type, + orderId: ethOrder.id, + createdAt: ethFill.createdAt, + createdAtHeight: ethFill.createdAtHeight, + }, + ]; + + expect(responsePage1.body.pageSize).toStrictEqual(1); + expect(responsePage1.body.offset).toStrictEqual(0); + expect(responsePage1.body.totalResults).toStrictEqual(2); + expect(responsePage1.body.fills).toHaveLength(1); + expect(responsePage1.body.fills).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expected[0], + }), + ]), + ); + + expect(responsePage2.body.pageSize).toStrictEqual(1); + expect(responsePage2.body.offset).toStrictEqual(1); + expect(responsePage2.body.totalResults).toStrictEqual(2); + expect(responsePage2.body.fills).toHaveLength(1); + expect(responsePage2.body.fills).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expected[1], + }), + ]), + ); + }); + it('Get /fills with market with no fills', async () => { // Order and fill for BTC-USD await OrderTable.create(testConstants.defaultOrder); diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/trades-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/trades-controller.test.ts index 1c8230133c..e85cd95daa 100644 --- a/indexer/services/comlink/__tests__/controllers/api/v4/trades-controller.test.ts +++ b/indexer/services/comlink/__tests__/controllers/api/v4/trades-controller.test.ts @@ -123,6 +123,73 @@ describe('trades-controller#V4', () => { ); }); + it('Get /:ticker gets trades for a ticker in descending order by createdAtHeight and paginated', async () => { + await testMocks.seedData(); + await perpetualMarketRefresher.updatePerpetualMarkets(); + // Order and fill for BTC-USD (maker and taker) + const fills1: { + makerFill: FillFromDatabase, + takerFill: FillFromDatabase, + } = await createMakerTakerOrderAndFill( + testConstants.defaultOrder, + testConstants.defaultFill, + ); + + const btcSize2: string = '600'; + const fills2: { + makerFill: FillFromDatabase, + takerFill: FillFromDatabase, + } = await createMakerTakerOrderAndFill( + testConstants.defaultOrder, + { + ...testConstants.defaultFill, + size: btcSize2, + eventId: testConstants.defaultTendermintEventId2, + createdAtHeight: '1', + }, + ); + + const responsePage1: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/trades/perpetualMarket/${testConstants.defaultPerpetualMarket.ticker}?page=1&limit=1`, + }); + + const responsePage2: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/trades/perpetualMarket/${testConstants.defaultPerpetualMarket.ticker}?page=2&limit=1`, + }); + + const expected: TradeResponseObject[] = [ + fillToTradeResponseObject(fills1.takerFill), + fillToTradeResponseObject(fills2.takerFill), + ]; + + // Expect both trades, ordered by createdAtHeight in descending order + expect(responsePage1.body.pageSize).toStrictEqual(1); + expect(responsePage1.body.offset).toStrictEqual(0); + expect(responsePage1.body.totalResults).toStrictEqual(2); + expect(responsePage1.body.trades).toHaveLength(1); + expect(responsePage1.body.trades).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expected[0], + }), + ]), + ); + + expect(responsePage1.body.pageSize).toStrictEqual(1); + expect(responsePage1.body.offset).toStrictEqual(0); + expect(responsePage2.body.totalResults).toStrictEqual(2); + expect(responsePage2.body.trades).toHaveLength(1); + expect(responsePage2.body.trades).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expected[1], + }), + ]), + ); + }); + it('Get /:ticker for ticker with no fills', async () => { await testMocks.seedData(); await perpetualMarketRefresher.updatePerpetualMarkets(); @@ -138,6 +205,21 @@ describe('trades-controller#V4', () => { expect(response.body.trades).toEqual([]); }); + it('Get /:ticker for ticker with no fills and paginated', async () => { + await testMocks.seedData(); + await perpetualMarketRefresher.updatePerpetualMarkets(); + // Order and fill for BTC-USD + await OrderTable.create(testConstants.defaultOrder); + await FillTable.create(testConstants.defaultFill); + + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/trades/perpetualMarket/${testConstants.defaultPerpetualMarket2.ticker}?page=1&limit=1`, + }); + + expect(response.body.trades).toEqual([]); + }); + it('Get /:ticker for ticker with price < 1e-6', async () => { await testMocks.seedData(); await perpetualMarketRefresher.updatePerpetualMarkets(); diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/transfers-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/transfers-controller.test.ts index d2fef6368c..3a57bf4a87 100644 --- a/indexer/services/comlink/__tests__/controllers/api/v4/transfers-controller.test.ts +++ b/indexer/services/comlink/__tests__/controllers/api/v4/transfers-controller.test.ts @@ -156,6 +156,148 @@ describe('transfers-controller#V4', () => { ); }); + it('Get /transfers returns transfers/deposits/withdrawals with pagination', async () => { + await testMocks.seedData(); + const transfer2: TransferCreateObject = { + senderSubaccountId: testConstants.defaultSubaccountId2, + recipientSubaccountId: testConstants.defaultSubaccountId, + assetId: testConstants.defaultAsset2.id, + size: '5', + eventId: testConstants.defaultTendermintEventId2, + transactionHash: '', // TODO: Add a real transaction Hash + createdAt: testConstants.createdDateTime.toISO(), + createdAtHeight: testConstants.createdHeight, + }; + await WalletTable.create({ + address: testConstants.defaultWalletAddress, + totalTradingRewards: '0', + }); + await Promise.all([ + TransferTable.create(testConstants.defaultTransfer), + TransferTable.create(transfer2), + TransferTable.create(testConstants.defaultWithdrawal), + TransferTable.create(testConstants.defaultDeposit), + ]); + + const responsePage1: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/transfers?address=${testConstants.defaultAddress}` + + `&subaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}&page=1&limit=2`, + }); + const responsePage2: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/transfers?address=${testConstants.defaultAddress}` + + `&subaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}&page=2&limit=2`, + }); + + const expectedTransferResponse: TransferResponseObject = { + id: testConstants.defaultTransferId, + sender: { + address: testConstants.defaultAddress, + subaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + recipient: { + address: testConstants.defaultAddress, + subaccountNumber: testConstants.defaultSubaccount2.subaccountNumber, + }, + size: testConstants.defaultTransfer.size, + createdAt: testConstants.defaultTransfer.createdAt, + createdAtHeight: testConstants.defaultTransfer.createdAtHeight, + symbol: testConstants.defaultAsset.symbol, + type: TransferType.TRANSFER_OUT, + transactionHash: testConstants.defaultTransfer.transactionHash, + }; + + const expectedTransfer2Response: TransferResponseObject = { + id: TransferTable.uuid( + transfer2.eventId, + transfer2.assetId, + transfer2.senderSubaccountId, + transfer2.recipientSubaccountId, + transfer2.senderWalletAddress, + transfer2.recipientWalletAddress, + ), + sender: { + address: testConstants.defaultAddress, + subaccountNumber: testConstants.defaultSubaccount2.subaccountNumber, + }, + recipient: { + address: testConstants.defaultAddress, + subaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + size: transfer2.size, + createdAt: transfer2.createdAt, + createdAtHeight: transfer2.createdAtHeight, + symbol: testConstants.defaultAsset2.symbol, + type: TransferType.TRANSFER_IN, + transactionHash: transfer2.transactionHash, + }; + + const expectedDepositResponse: TransferResponseObject = { + id: testConstants.defaultDepositId, + sender: { + address: testConstants.defaultWalletAddress, + }, + recipient: { + address: testConstants.defaultAddress, + subaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + size: testConstants.defaultDeposit.size, + createdAt: testConstants.defaultDeposit.createdAt, + createdAtHeight: testConstants.defaultDeposit.createdAtHeight, + symbol: testConstants.defaultAsset.symbol, + type: TransferType.DEPOSIT, + transactionHash: testConstants.defaultDeposit.transactionHash, + }; + + const expectedWithdrawalResponse: TransferResponseObject = { + id: testConstants.defaultWithdrawalId, + sender: { + address: testConstants.defaultAddress, + subaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + recipient: { + address: testConstants.defaultWalletAddress, + }, + size: testConstants.defaultWithdrawal.size, + createdAt: testConstants.defaultWithdrawal.createdAt, + createdAtHeight: testConstants.defaultWithdrawal.createdAtHeight, + symbol: testConstants.defaultAsset.symbol, + type: TransferType.WITHDRAWAL, + transactionHash: testConstants.defaultWithdrawal.transactionHash, + }; + + expect(responsePage1.body.pageSize).toStrictEqual(2); + expect(responsePage1.body.offset).toStrictEqual(0); + expect(responsePage1.body.totalResults).toStrictEqual(4); + expect(responsePage1.body.transfers).toHaveLength(2); + expect(responsePage1.body.transfers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expectedTransferResponse, + }), + expect.objectContaining({ + ...expectedTransfer2Response, + }), + ]), + ); + + expect(responsePage2.body.pageSize).toStrictEqual(2); + expect(responsePage2.body.offset).toStrictEqual(2); + expect(responsePage2.body.totalResults).toStrictEqual(4); + expect(responsePage2.body.transfers).toHaveLength(2); + expect(responsePage2.body.transfers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expectedWithdrawalResponse, + }), + expect.objectContaining({ + ...expectedDepositResponse, + }), + ]), + ); + }); + it('Get /transfers respects createdBeforeOrAt field', async () => { await testMocks.seedData(); const createdAt: string = '2000-05-25T00:00:00.000Z'; @@ -419,6 +561,149 @@ describe('transfers-controller#V4', () => { ); }); + it('Get /transfers/parentSubaccountNumber returns transfers/deposits/withdrawals and paginated', async () => { + await testMocks.seedData(); + const transfer2: TransferCreateObject = { + senderSubaccountId: testConstants.defaultSubaccountId2, + recipientSubaccountId: testConstants.defaultSubaccountId, + assetId: testConstants.defaultAsset2.id, + size: '5', + eventId: testConstants.defaultTendermintEventId2, + transactionHash: '', // TODO: Add a real transaction Hash + createdAt: testConstants.createdDateTime.toISO(), + createdAtHeight: testConstants.createdHeight, + }; + await WalletTable.create({ + address: testConstants.defaultWalletAddress, + totalTradingRewards: '0', + }); + await Promise.all([ + TransferTable.create(testConstants.defaultTransfer), + TransferTable.create(transfer2), + TransferTable.create(testConstants.defaultWithdrawal), + TransferTable.create(testConstants.defaultDeposit), + ]); + + const responsePage1: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/transfers/parentSubaccountNumber?address=${testConstants.defaultAddress}` + + `&parentSubaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}&page=1&limit=2`, + }); + + const responsePage2: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/transfers/parentSubaccountNumber?address=${testConstants.defaultAddress}` + + `&parentSubaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}&page=2&limit=2`, + }); + + const expectedTransferResponse: ParentSubaccountTransferResponseObject = { + id: testConstants.defaultTransferId, + sender: { + address: testConstants.defaultAddress, + parentSubaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + recipient: { + address: testConstants.defaultAddress, + parentSubaccountNumber: testConstants.defaultSubaccount2.subaccountNumber, + }, + size: testConstants.defaultTransfer.size, + createdAt: testConstants.defaultTransfer.createdAt, + createdAtHeight: testConstants.defaultTransfer.createdAtHeight, + symbol: testConstants.defaultAsset.symbol, + type: TransferType.TRANSFER_OUT, + transactionHash: testConstants.defaultTransfer.transactionHash, + }; + + const expectedTransfer2Response: ParentSubaccountTransferResponseObject = { + id: TransferTable.uuid( + transfer2.eventId, + transfer2.assetId, + transfer2.senderSubaccountId, + transfer2.recipientSubaccountId, + transfer2.senderWalletAddress, + transfer2.recipientWalletAddress, + ), + sender: { + address: testConstants.defaultAddress, + parentSubaccountNumber: testConstants.defaultSubaccount2.subaccountNumber, + }, + recipient: { + address: testConstants.defaultAddress, + parentSubaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + size: transfer2.size, + createdAt: transfer2.createdAt, + createdAtHeight: transfer2.createdAtHeight, + symbol: testConstants.defaultAsset2.symbol, + type: TransferType.TRANSFER_IN, + transactionHash: transfer2.transactionHash, + }; + + const expectedDepositResponse: ParentSubaccountTransferResponseObject = { + id: testConstants.defaultDepositId, + sender: { + address: testConstants.defaultWalletAddress, + }, + recipient: { + address: testConstants.defaultAddress, + parentSubaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + size: testConstants.defaultDeposit.size, + createdAt: testConstants.defaultDeposit.createdAt, + createdAtHeight: testConstants.defaultDeposit.createdAtHeight, + symbol: testConstants.defaultAsset.symbol, + type: TransferType.DEPOSIT, + transactionHash: testConstants.defaultDeposit.transactionHash, + }; + + const expectedWithdrawalResponse: ParentSubaccountTransferResponseObject = { + id: testConstants.defaultWithdrawalId, + sender: { + address: testConstants.defaultAddress, + parentSubaccountNumber: testConstants.defaultSubaccount.subaccountNumber, + }, + recipient: { + address: testConstants.defaultWalletAddress, + }, + size: testConstants.defaultWithdrawal.size, + createdAt: testConstants.defaultWithdrawal.createdAt, + createdAtHeight: testConstants.defaultWithdrawal.createdAtHeight, + symbol: testConstants.defaultAsset.symbol, + type: TransferType.WITHDRAWAL, + transactionHash: testConstants.defaultWithdrawal.transactionHash, + }; + + expect(responsePage1.body.pageSize).toStrictEqual(2); + expect(responsePage1.body.offset).toStrictEqual(0); + expect(responsePage1.body.totalResults).toStrictEqual(4); + expect(responsePage1.body.transfers).toHaveLength(2); + expect(responsePage1.body.transfers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expectedTransferResponse, + }), + expect.objectContaining({ + ...expectedTransfer2Response, + }), + ]), + ); + + expect(responsePage2.body.pageSize).toStrictEqual(2); + expect(responsePage2.body.offset).toStrictEqual(2); + expect(responsePage2.body.totalResults).toStrictEqual(4); + expect(responsePage2.body.transfers).toHaveLength(2); + expect(responsePage2.body.transfers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expectedWithdrawalResponse, + }), + expect.objectContaining({ + ...expectedDepositResponse, + }), + ]), + ); + }); + it('Get /transfers/parentSubaccountNumber excludes transfers for parent <> child subaccounts', async () => { await testMocks.seedData(); const transfer2: TransferCreateObject = { diff --git a/indexer/services/comlink/public/api-documentation.md b/indexer/services/comlink/public/api-documentation.md index 925f224fe2..c3bf137dd1 100644 --- a/indexer/services/comlink/public/api-documentation.md +++ b/indexer/services/comlink/public/api-documentation.md @@ -775,6 +775,7 @@ fetch('https://dydx-testnet.imperator.co/v4/fills?address=string&subaccountNumbe |limit|query|number(double)|false|none| |createdBeforeOrAtHeight|query|number(double)|false|none| |createdBeforeOrAt|query|[IsoString](#schemaisostring)|false|none| +|page|query|number(double)|false|none| #### Enumerated Values @@ -789,6 +790,9 @@ fetch('https://dydx-testnet.imperator.co/v4/fills?address=string&subaccountNumbe ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "fills": [ { "id": "string", @@ -873,6 +877,7 @@ fetch('https://dydx-testnet.imperator.co/v4/fills/parentSubaccount?address=strin |limit|query|number(double)|false|none| |createdBeforeOrAtHeight|query|number(double)|false|none| |createdBeforeOrAt|query|[IsoString](#schemaisostring)|false|none| +|page|query|number(double)|false|none| #### Enumerated Values @@ -887,6 +892,9 @@ fetch('https://dydx-testnet.imperator.co/v4/fills/parentSubaccount?address=strin ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "fills": [ { "id": "string", @@ -2442,6 +2450,7 @@ fetch('https://dydx-testnet.imperator.co/v4/trades/perpetualMarket/{ticker}', |limit|query|number(double)|false|none| |createdBeforeOrAtHeight|query|number(double)|false|none| |createdBeforeOrAt|query|[IsoString](#schemaisostring)|false|none| +|page|query|number(double)|false|none| > Example responses @@ -2449,6 +2458,9 @@ fetch('https://dydx-testnet.imperator.co/v4/trades/perpetualMarket/{ticker}', ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "trades": [ { "id": "string", @@ -2524,6 +2536,7 @@ fetch('https://dydx-testnet.imperator.co/v4/transfers?address=string&subaccountN |limit|query|number(double)|false|none| |createdBeforeOrAtHeight|query|number(double)|false|none| |createdBeforeOrAt|query|[IsoString](#schemaisostring)|false|none| +|page|query|number(double)|false|none| > Example responses @@ -2531,6 +2544,9 @@ fetch('https://dydx-testnet.imperator.co/v4/transfers?address=string&subaccountN ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "transfers": [ { "id": "string", @@ -2614,6 +2630,7 @@ fetch('https://dydx-testnet.imperator.co/v4/transfers/parentSubaccountNumber?add |limit|query|number(double)|false|none| |createdBeforeOrAtHeight|query|number(double)|false|none| |createdBeforeOrAt|query|[IsoString](#schemaisostring)|false|none| +|page|query|number(double)|false|none| > Example responses @@ -2621,6 +2638,9 @@ fetch('https://dydx-testnet.imperator.co/v4/transfers/parentSubaccountNumber?add ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "transfers": [ { "id": "string", @@ -3547,6 +3567,9 @@ This operation does not require authentication ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "fills": [ { "id": "string", @@ -3573,6 +3596,9 @@ This operation does not require authentication |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| +|pageSize|number(double)|false|none|none| +|totalResults|number(double)|false|none|none| +|offset|number(double)|false|none|none| |fills|[[FillResponseObject](#schemafillresponseobject)]|true|none|none| ## HeightResponse @@ -4468,6 +4494,9 @@ or ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "trades": [ { "id": "string", @@ -4487,6 +4516,9 @@ or |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| +|pageSize|number(double)|false|none|none| +|totalResults|number(double)|false|none|none| +|offset|number(double)|false|none|none| |trades|[[TradeResponseObject](#schematraderesponseobject)]|true|none|none| ## TransferType @@ -4571,6 +4603,9 @@ or ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "transfers": [ { "id": "string", @@ -4598,6 +4633,9 @@ or |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| +|pageSize|number(double)|false|none|none| +|totalResults|number(double)|false|none|none| +|offset|number(double)|false|none|none| |transfers|[[TransferResponseObject](#schematransferresponseobject)]|true|none|none| ## ParentSubaccountTransferResponse @@ -4609,6 +4647,9 @@ or ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "transfers": [ { "id": "string", @@ -4636,5 +4677,8 @@ or |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| +|pageSize|number(double)|false|none|none| +|totalResults|number(double)|false|none|none| +|offset|number(double)|false|none|none| |transfers|[[TransferResponseObject](#schematransferresponseobject)]|true|none|none| diff --git a/indexer/services/comlink/public/swagger.json b/indexer/services/comlink/public/swagger.json index a1a271b04b..1301f9534f 100644 --- a/indexer/services/comlink/public/swagger.json +++ b/indexer/services/comlink/public/swagger.json @@ -484,6 +484,18 @@ }, "FillResponse": { "properties": { + "pageSize": { + "type": "number", + "format": "double" + }, + "totalResults": { + "type": "number", + "format": "double" + }, + "offset": { + "type": "number", + "format": "double" + }, "fills": { "items": { "$ref": "#/components/schemas/FillResponseObject" @@ -1105,6 +1117,18 @@ }, "TradeResponse": { "properties": { + "pageSize": { + "type": "number", + "format": "double" + }, + "totalResults": { + "type": "number", + "format": "double" + }, + "offset": { + "type": "number", + "format": "double" + }, "trades": { "items": { "$ref": "#/components/schemas/TradeResponseObject" @@ -1197,6 +1221,18 @@ }, "TransferResponse": { "properties": { + "pageSize": { + "type": "number", + "format": "double" + }, + "totalResults": { + "type": "number", + "format": "double" + }, + "offset": { + "type": "number", + "format": "double" + }, "transfers": { "items": { "$ref": "#/components/schemas/TransferResponseObject" @@ -1212,6 +1248,18 @@ }, "ParentSubaccountTransferResponse": { "properties": { + "pageSize": { + "type": "number", + "format": "double" + }, + "totalResults": { + "type": "number", + "format": "double" + }, + "offset": { + "type": "number", + "format": "double" + }, "transfers": { "items": { "$ref": "#/components/schemas/TransferResponseObject" @@ -1602,6 +1650,15 @@ "schema": { "$ref": "#/components/schemas/IsoString" } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "double", + "type": "number" + } } ] } @@ -1681,6 +1738,15 @@ "schema": { "$ref": "#/components/schemas/IsoString" } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "double", + "type": "number" + } } ] } @@ -2588,6 +2654,15 @@ "schema": { "$ref": "#/components/schemas/IsoString" } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "double", + "type": "number" + } } ] } @@ -2651,6 +2726,15 @@ "schema": { "$ref": "#/components/schemas/IsoString" } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "double", + "type": "number" + } } ] } @@ -2714,6 +2798,15 @@ "schema": { "$ref": "#/components/schemas/IsoString" } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "double", + "type": "number" + } } ] } 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 0df1441c11..3ba6d86c3c 100644 --- a/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts @@ -7,6 +7,8 @@ import { FillTable, FillFromDatabase, QueryableField, + FillColumns, + Ordering, } from '@dydxprotocol-indexer/postgres'; import express from 'express'; import { @@ -28,7 +30,12 @@ import { getClobPairId, handleControllerError, isDefined, } from '../../../lib/helpers'; import { rateLimiterMiddleware } from '../../../lib/rate-limit'; -import { CheckLimitAndCreatedBeforeOrAtSchema, CheckSubaccountSchema, CheckParentSubaccountSchema } from '../../../lib/validation/schemas'; +import { + CheckLimitAndCreatedBeforeOrAtSchema, + CheckSubaccountSchema, + CheckParentSubaccountSchema, + CheckPaginationSchema, +} from '../../../lib/validation/schemas'; import { handleValidationErrors } from '../../../request-helpers/error-handler'; import ExportResponseCodeStats from '../../../request-helpers/export-response-code-stats'; import { fillToResponseObject } from '../../../request-helpers/request-transformer'; @@ -55,6 +62,7 @@ class FillsController extends Controller { @Query() limit?: number, @Query() createdBeforeOrAtHeight?: number, @Query() createdBeforeOrAt?: IsoString, + @Query() page?: number, ): Promise { // TODO(DEC-656): Change to using a cache of markets in Redis similar to Librarian instead of // querying the DB. @@ -68,7 +76,12 @@ class FillsController extends Controller { } const subaccountId: string = SubaccountTable.uuid(address, subaccountNumber); - const { results: fills } = await FillTable.findAll( + const { + results: fills, + limit: pageSize, + offset, + total, + } = await FillTable.findAll( { subaccountId: [subaccountId], clobPairId, @@ -77,8 +90,10 @@ class FillsController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIMIT], + { orderBy: [[FillColumns.eventId, Ordering.ASC]] }, ); const clobPairIdToPerpetualMarket: Record< @@ -98,6 +113,9 @@ class FillsController extends Controller { fills: fills.map((fill: FillFromDatabase): FillResponseObject => { return fillToResponseObject(fill, clobPairIdToMarket, subaccountNumber); }), + pageSize, + totalResults: total, + offset, }; } @@ -110,6 +128,7 @@ class FillsController extends Controller { @Query() limit?: number, @Query() createdBeforeOrAtHeight?: number, @Query() createdBeforeOrAt?: IsoString, + @Query() page?: number, ): Promise { // TODO(DEC-656): Change to using a cache of markets in Redis similar to Librarian instead of // querying the DB. @@ -132,7 +151,12 @@ class FillsController extends Controller { ); const subaccountIds: string[] = Object.keys(childIdtoSubaccountNumber); - const { results: fills } = await FillTable.findAll( + const { + results: fills, + limit: pageSize, + offset, + total, + } = await FillTable.findAll( { subaccountId: subaccountIds, clobPairId, @@ -141,8 +165,10 @@ class FillsController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIMIT], + { orderBy: [[FillColumns.eventId, Ordering.ASC]] }, ); const clobPairIdToPerpetualMarket: Record< @@ -163,6 +189,9 @@ class FillsController extends Controller { return fillToResponseObject(fill, clobPairIdToMarket, childIdtoSubaccountNumber[fill.subaccountId]); }), + pageSize, + totalResults: total, + offset, }; } } @@ -172,6 +201,7 @@ router.get( rateLimiterMiddleware(getReqRateLimiter), ...CheckSubaccountSchema, ...CheckLimitAndCreatedBeforeOrAtSchema, + ...CheckPaginationSchema, // Use conditional validations such that market is required if marketType is in the query // parameters and vice-versa. // Reference https://express-validator.github.io/docs/validation-chain-api.html#ifcondition @@ -208,6 +238,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, }: FillRequest = matchedData(req) as FillRequest; // The schema checks allow subaccountNumber to be a string, but we know it's a number here. @@ -225,6 +256,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, ); return res.send(response); @@ -250,6 +282,7 @@ router.get( rateLimiterMiddleware(getReqRateLimiter), ...CheckParentSubaccountSchema, ...CheckLimitAndCreatedBeforeOrAtSchema, + ...CheckPaginationSchema, // Use conditional validations such that market is required if marketType is in the query // parameters and vice-versa. // Reference https://express-validator.github.io/docs/validation-chain-api.html#ifcondition @@ -286,6 +319,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, }: ParentSubaccountFillRequest = matchedData(req) as ParentSubaccountFillRequest; // The schema checks allow subaccountNumber to be a string, but we know it's a number here. @@ -303,6 +337,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, ); return res.send(response); diff --git a/indexer/services/comlink/src/controllers/api/v4/trades-controller.ts b/indexer/services/comlink/src/controllers/api/v4/trades-controller.ts index f9059b69f4..0504f6d2b4 100644 --- a/indexer/services/comlink/src/controllers/api/v4/trades-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/trades-controller.ts @@ -6,6 +6,8 @@ import { Liquidity, QueryableField, perpetualMarketRefresher, + FillColumns, + Ordering, } from '@dydxprotocol-indexer/postgres'; import express from 'express'; import { @@ -23,7 +25,7 @@ import { handleControllerError, } from '../../../lib/helpers'; import { rateLimiterMiddleware } from '../../../lib/rate-limit'; -import { CheckLimitAndCreatedBeforeOrAtSchema } from '../../../lib/validation/schemas'; +import { CheckLimitAndCreatedBeforeOrAtSchema, CheckPaginationSchema } from '../../../lib/validation/schemas'; import { handleValidationErrors } from '../../../request-helpers/error-handler'; import ExportResponseCodeStats from '../../../request-helpers/export-response-code-stats'; import { fillToTradeResponseObject } from '../../../request-helpers/request-transformer'; @@ -45,6 +47,7 @@ class TradesController extends Controller { @Query() limit?: number, @Query() createdBeforeOrAtHeight?: number, @Query() createdBeforeOrAt?: IsoString, + @Query() page?: number, ): Promise { const clobPairId: string | undefined = perpetualMarketRefresher .getClobPairIdFromTicker(ticker); @@ -53,7 +56,12 @@ class TradesController extends Controller { throw new NotFoundError(`${ticker} not found in tickers of type ${MarketType.PERPETUAL}`); } - const { results: fills } = await FillTable.findAll( + const { + results: fills, + limit: pageSize, + offset, + total, + } = await FillTable.findAll( { clobPairId, liquidity: Liquidity.TAKER, @@ -62,14 +70,19 @@ class TradesController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIQUIDITY, QueryableField.CLOB_PAIR_ID, QueryableField.LIMIT], + { orderBy: [[FillColumns.eventId, Ordering.ASC]] }, ); return { trades: fills.map((fill: FillFromDatabase): TradeResponseObject => { return fillToTradeResponseObject(fill); }), + pageSize, + totalResults: total, + offset, }; } } @@ -78,6 +91,7 @@ router.get( '/perpetualMarket/:ticker', rateLimiterMiddleware(getReqRateLimiter), ...CheckLimitAndCreatedBeforeOrAtSchema, + ...CheckPaginationSchema, ...checkSchema({ ticker: { in: ['params'], @@ -97,6 +111,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, }: TradeRequest = matchedData(req) as TradeRequest; try { @@ -106,6 +121,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, ); return res.send(response); diff --git a/indexer/services/comlink/src/controllers/api/v4/transfers-controller.ts b/indexer/services/comlink/src/controllers/api/v4/transfers-controller.ts index 354e02ba3e..198597f0d3 100644 --- a/indexer/services/comlink/src/controllers/api/v4/transfers-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/transfers-controller.ts @@ -30,6 +30,7 @@ import { getChildSubaccountNums, handleControllerError } from '../../../lib/help import { rateLimiterMiddleware } from '../../../lib/rate-limit'; import { CheckLimitAndCreatedBeforeOrAtSchema, + CheckPaginationSchema, CheckParentSubaccountSchema, CheckSubaccountSchema, } from '../../../lib/validation/schemas'; @@ -61,11 +62,14 @@ class TransfersController extends Controller { @Query() limit?: number, @Query() createdBeforeOrAtHeight?: number, @Query() createdBeforeOrAt?: IsoString, + @Query() page?: number, ): Promise { const subaccountId: string = SubaccountTable.uuid(address, subaccountNumber); // TODO(DEC-656): Change to a cache in Redis similar to Librarian instead of querying DB. - const [subaccount, { results: transfers }, assets] = await Promise.all([ + const [subaccount, { + results: transfers, limit: pageSize, offset, total, + }, assets] = await Promise.all([ SubaccountTable.findById( subaccountId, ), @@ -77,11 +81,17 @@ class TransfersController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIMIT], { ...DEFAULT_POSTGRES_OPTIONS, - orderBy: [[TransferColumns.createdAtHeight, Ordering.DESC]], + orderBy: page !== undefined ? [ + [TransferColumns.eventId, Ordering.DESC], + ] + : [ + [TransferColumns.createdAtHeight, Ordering.DESC], + ], }, ), AssetTable.findAll( @@ -129,6 +139,9 @@ class TransfersController extends Controller { transfers: transfers.map((transfer: TransferFromDatabase) => { return transferToResponseObject(transfer, idToAsset, idToSubaccount, subaccountId); }), + pageSize, + totalResults: total, + offset, }; } @@ -139,6 +152,7 @@ class TransfersController extends Controller { @Query() limit?: number, @Query() createdBeforeOrAtHeight?: number, @Query() createdBeforeOrAt?: IsoString, + @Query() page?: number, ): Promise { // get all child subaccountIds for the parent subaccount number @@ -147,7 +161,12 @@ class TransfersController extends Controller { ); // TODO(DEC-656): Change to a cache in Redis similar to Librarian instead of querying DB. - const [subaccounts, { results: transfers }, assets]: [ + const [subaccounts, { + results: transfers, + limit: pageSize, + offset, + total, + }, assets]: [ SubaccountFromDatabase[] | undefined, PaginationFromDatabase, AssetFromDatabase[] @@ -165,11 +184,17 @@ class TransfersController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIMIT], { ...DEFAULT_POSTGRES_OPTIONS, - orderBy: [[TransferColumns.createdAtHeight, Ordering.DESC]], + orderBy: page !== undefined ? [ + [TransferColumns.eventId, Ordering.DESC], + ] + : [ + [TransferColumns.createdAtHeight, Ordering.DESC], + ], }, ), AssetTable.findAll( @@ -229,7 +254,12 @@ class TransfersController extends Controller { return transfer.sender.parentSubaccountNumber !== transfer.recipient.parentSubaccountNumber; }); - return { transfers: transfersFiltered }; + return { + transfers: transfersFiltered, + pageSize, + totalResults: total, + offset, + }; } } @@ -238,6 +268,7 @@ router.get( rateLimiterMiddleware(getReqRateLimiter), ...CheckSubaccountSchema, ...CheckLimitAndCreatedBeforeOrAtSchema, + ...CheckPaginationSchema, handleValidationErrors, complianceAndGeoCheck, ExportResponseCodeStats({ controllerName }), @@ -249,6 +280,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, }: TransferRequest = matchedData(req) as TransferRequest; try { @@ -259,6 +291,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, ); return res.send(response); @@ -284,6 +317,7 @@ router.get( rateLimiterMiddleware(getReqRateLimiter), ...CheckParentSubaccountSchema, ...CheckLimitAndCreatedBeforeOrAtSchema, + ...CheckPaginationSchema, handleValidationErrors, complianceAndGeoCheck, ExportResponseCodeStats({ controllerName }), @@ -295,6 +329,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, }: ParentSubaccountTransferRequest = matchedData(req) as ParentSubaccountTransferRequest; // The schema checks allow subaccountNumber to be a string, but we know it's a number here. @@ -308,6 +343,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, ); return res.send(response); diff --git a/indexer/services/comlink/src/lib/validation/schemas.ts b/indexer/services/comlink/src/lib/validation/schemas.ts index 27e13a4d2c..4a2d39b11c 100644 --- a/indexer/services/comlink/src/lib/validation/schemas.ts +++ b/indexer/services/comlink/src/lib/validation/schemas.ts @@ -66,6 +66,17 @@ const limitSchemaRecord: Record = { }, }; +const paginationSchemaRecord: Record = { + page: { + in: ['query'], + optional: true, + isInt: { + options: { gt: 0 }, + }, + errorMessage: 'page must be a non-negative integer', + }, +}; + const createdBeforeOrAtSchemaRecord: Record = { createdBeforeOrAtHeight: { in: ['query'], @@ -116,6 +127,8 @@ const createdOnOrAfterSchemaRecord: Record = { export const CheckLimitSchema = checkSchema(limitSchemaRecord); +export const CheckPaginationSchema = checkSchema(paginationSchemaRecord); + export const CheckLimitAndCreatedBeforeOrAtSchema = checkSchema({ ...limitSchemaRecord, ...createdBeforeOrAtSchemaRecord, diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts index 9c65e6326c..c39fd57ae2 100644 --- a/indexer/services/comlink/src/types.ts +++ b/indexer/services/comlink/src/types.ts @@ -41,6 +41,13 @@ export enum RequestMethod { PUT = 'PUT', } +/* ------- Pagination ------- */ +export interface PaginationResponse { + pageSize?: number, + totalResults?: number, + offset?: number, +} + /* ------- SUBACCOUNT TYPES ------- */ export interface AddressResponse { @@ -123,7 +130,7 @@ export type AssetPositionsMap = { [symbol: string]: AssetPositionResponseObject /* ------- FILL TYPES ------- */ -export interface FillResponse { +export interface FillResponse extends PaginationResponse { fills: FillResponseObject[], } @@ -146,7 +153,7 @@ export interface FillResponseObject { /* ------- TRANSFER TYPES ------- */ -export interface TransferResponse { +export interface TransferResponse extends PaginationResponse { transfers: TransferResponseObject[], } @@ -168,7 +175,7 @@ export interface TransferResponseObject { transactionHash: string, } -export interface ParentSubaccountTransferResponse { +export interface ParentSubaccountTransferResponse extends PaginationResponse { transfers: TransferResponseObject[], } @@ -209,7 +216,7 @@ export interface PnlTicksResponseObject { /* ------- TRADE TYPES ------- */ -export interface TradeResponse { +export interface TradeResponse extends PaginationResponse { trades: TradeResponseObject[], } @@ -352,6 +359,10 @@ export interface ParentSubaccountRequest extends AddressRequest { parentSubaccountNumber: number, } +export interface PaginationRequest { + page?: number; +} + export interface LimitRequest { limit: number, } @@ -389,24 +400,26 @@ export interface AssetPositionRequest extends SubaccountRequest {} export interface ParentSubaccountAssetPositionRequest extends ParentSubaccountRequest { } -export interface TransferRequest extends SubaccountRequest, LimitAndCreatedBeforeRequest {} +export interface TransferRequest + extends SubaccountRequest, LimitAndCreatedBeforeRequest, PaginationRequest {} export interface ParentSubaccountTransferRequest - extends ParentSubaccountRequest, LimitAndCreatedBeforeRequest { + extends ParentSubaccountRequest, LimitAndCreatedBeforeRequest, PaginationRequest { } -export interface FillRequest extends SubaccountRequest, LimitAndCreatedBeforeRequest { +export interface FillRequest + extends SubaccountRequest, LimitAndCreatedBeforeRequest, PaginationRequest { market: string, marketType: MarketType, } export interface ParentSubaccountFillRequest - extends ParentSubaccountRequest, LimitAndCreatedBeforeRequest { + extends ParentSubaccountRequest, LimitAndCreatedBeforeRequest, PaginationRequest { market: string, marketType: MarketType, } -export interface TradeRequest extends LimitAndCreatedBeforeRequest { +export interface TradeRequest extends LimitAndCreatedBeforeRequest, PaginationRequest { ticker: string, }