From 44f770a3ec8052064e4c9169816f7e49a14373ce Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Fri, 26 Apr 2024 13:38:00 +0200 Subject: [PATCH 1/9] feat: :sparkles: add pagination for getFills --- .../src/controllers/api/v4/fills-controller.ts | 12 +++++++++++- indexer/services/comlink/src/types.ts | 11 +++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) 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..e4e74bc3b6 100644 --- a/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts @@ -55,6 +55,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 +69,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,6 +83,7 @@ class FillsController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIMIT], ); @@ -98,6 +105,9 @@ class FillsController extends Controller { fills: fills.map((fill: FillFromDatabase): FillResponseObject => { return fillToResponseObject(fill, clobPairIdToMarket, subaccountNumber); }), + pageSize, + totalResults: total, + offset, }; } diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts index 6f3f3c46a5..b96d23597c 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 { @@ -122,7 +129,7 @@ export type AssetPositionsMap = { [symbol: string]: AssetPositionResponseObject /* ------- FILL TYPES ------- */ -export interface FillResponse { +export interface FillResponse extends PaginationResponse { fills: FillResponseObject[], } @@ -208,7 +215,7 @@ export interface PnlTicksResponseObject { /* ------- TRADE TYPES ------- */ -export interface TradeResponse { +export interface TradeResponse extends PaginationResponse { trades: TradeResponseObject[], } From 71e98315532ab6a46ec7d18cc95410d06bcfd86b Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Fri, 26 Apr 2024 13:39:04 +0200 Subject: [PATCH 2/9] feat: :sparkles: add pagination support for getTrades --- .../src/controllers/api/v4/trades-controller.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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..a9725ac823 100644 --- a/indexer/services/comlink/src/controllers/api/v4/trades-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/trades-controller.ts @@ -45,6 +45,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 +54,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,6 +68,7 @@ class TradesController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIQUIDITY, QueryableField.CLOB_PAIR_ID, QueryableField.LIMIT], ); @@ -70,6 +77,9 @@ class TradesController extends Controller { trades: fills.map((fill: FillFromDatabase): TradeResponseObject => { return fillToTradeResponseObject(fill); }), + pageSize, + totalResults: total, + offset, }; } } From c63ea4f8feb8c39981f1313aa63388667c53a1d5 Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Fri, 26 Apr 2024 13:41:34 +0200 Subject: [PATCH 3/9] feat: :sparkles: add pagination support for getTransfers --- .../src/controllers/api/v4/transfers-controller.ts | 9 ++++++++- indexer/services/comlink/src/types.ts | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) 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..54ab9a7d02 100644 --- a/indexer/services/comlink/src/controllers/api/v4/transfers-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/transfers-controller.ts @@ -61,11 +61,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,6 +80,7 @@ class TransfersController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIMIT], { @@ -129,6 +133,9 @@ class TransfersController extends Controller { transfers: transfers.map((transfer: TransferFromDatabase) => { return transferToResponseObject(transfer, idToAsset, idToSubaccount, subaccountId); }), + pageSize, + totalResults: total, + offset, }; } diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts index b96d23597c..d77bfeac3a 100644 --- a/indexer/services/comlink/src/types.ts +++ b/indexer/services/comlink/src/types.ts @@ -152,7 +152,7 @@ export interface FillResponseObject { /* ------- TRANSFER TYPES ------- */ -export interface TransferResponse { +export interface TransferResponse extends PaginationResponse { transfers: TransferResponseObject[], } From e1bb0960cf0fa0a400d592ab1de767eed19c6132 Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Fri, 26 Apr 2024 16:31:51 +0200 Subject: [PATCH 4/9] test(comlink): :white_check_mark: add trades controller pagination tests --- .../api/v4/trades-controller.test.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) 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..e54c41ea4c 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).toHaveLength(1); + expect(responsePage1.body.offset).toHaveLength(0); + expect(responsePage1.body.totalResults).toHaveLength(2); + expect(responsePage1.body.trades).toHaveLength(1); + expect(responsePage1.body.trades).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expected[0], + }), + ]), + ); + + expect(responsePage1.body.pageSize).toHaveLength(1); + expect(responsePage1.body.offset).toHaveLength(1); + expect(responsePage2.body.totalResults).toHaveLength(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(); From f29228f557b5f79207c8dba0dc34229f4f286dc6 Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Fri, 26 Apr 2024 16:40:28 +0200 Subject: [PATCH 5/9] test(comlink): :white_check_mark: add transfers controller pagination tests --- .../api/v4/transfers-controller.test.ts | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) 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..00aa8525c7 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).toHaveLength(2); + expect(responsePage1.body.offset).toHaveLength(0); + expect(responsePage1.body.totalResults).toHaveLength(4); + expect(responsePage1.body.transfers).toHaveLength(2); + expect(responsePage1.body.transfers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expectedTransferResponse, + }), + expect.objectContaining({ + ...expectedTransfer2Response, + }), + ]), + ); + + expect(responsePage2.body.pageSize).toHaveLength(2); + expect(responsePage2.body.offset).toHaveLength(2); + expect(responsePage2.body.totalResults).toHaveLength(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'; From ae618afeb1d4e5f178e8457a947e27684d676c98 Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Fri, 26 Apr 2024 16:54:43 +0200 Subject: [PATCH 6/9] test(comlink): :white_check_mark: add fills controller pagination tests --- .../api/v4/fills-controller.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) 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..99cb56209a 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).toHaveLength(1); + expect(responsePage1.body.offset).toHaveLength(0); + expect(responsePage1.body.totalResults).toHaveLength(2); + expect(responsePage1.body.fills).toHaveLength(1); + expect(responsePage1.body.fills).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...expected[0], + }), + ]), + ); + + expect(responsePage2.body.pageSize).toHaveLength(1); + expect(responsePage2.body.offset).toHaveLength(1); + expect(responsePage2.body.totalResults).toHaveLength(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); From b69ad38e92b0c9f19600add44a94e4652da16484 Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Fri, 26 Apr 2024 19:09:54 +0200 Subject: [PATCH 7/9] feat: :sparkles: add pagination check schema --- .../comlink/public/api-documentation.md | 44 +++++++++ indexer/services/comlink/public/swagger.json | 93 +++++++++++++++++++ .../controllers/api/v4/fills-controller.ts | 25 ++++- .../controllers/api/v4/trades-controller.ts | 5 +- .../api/v4/transfers-controller.ts | 23 ++++- .../comlink/src/lib/validation/schemas.ts | 13 +++ indexer/services/comlink/src/types.ts | 18 ++-- 7 files changed, 210 insertions(+), 11 deletions(-) diff --git a/indexer/services/comlink/public/api-documentation.md b/indexer/services/comlink/public/api-documentation.md index dec8c188c5..a8b8e9e99c 100644 --- a/indexer/services/comlink/public/api-documentation.md +++ b/indexer/services/comlink/public/api-documentation.md @@ -772,6 +772,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 @@ -786,6 +787,9 @@ fetch('https://dydx-testnet.imperator.co/v4/fills?address=string&subaccountNumbe ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "fills": [ { "id": "string", @@ -870,6 +874,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 @@ -884,6 +889,9 @@ fetch('https://dydx-testnet.imperator.co/v4/fills/parentSubaccount?address=strin ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "fills": [ { "id": "string", @@ -2354,6 +2362,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 @@ -2361,6 +2370,9 @@ fetch('https://dydx-testnet.imperator.co/v4/trades/perpetualMarket/{ticker}', ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "trades": [ { "id": "string", @@ -2436,6 +2448,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 @@ -2443,6 +2456,9 @@ fetch('https://dydx-testnet.imperator.co/v4/transfers?address=string&subaccountN ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "transfers": [ { "id": "string", @@ -2526,6 +2542,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 @@ -2533,6 +2550,9 @@ fetch('https://dydx-testnet.imperator.co/v4/transfers/parentSubaccountNumber?add ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "transfers": [ { "id": "string", @@ -3455,6 +3475,9 @@ This operation does not require authentication ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "fills": [ { "id": "string", @@ -3481,6 +3504,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 @@ -4376,6 +4402,9 @@ or ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "trades": [ { "id": "string", @@ -4395,6 +4424,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 @@ -4479,6 +4511,9 @@ or ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "transfers": [ { "id": "string", @@ -4506,6 +4541,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 @@ -4517,6 +4555,9 @@ or ```json { + "pageSize": 0, + "totalResults": 0, + "offset": 0, "transfers": [ { "id": "string", @@ -4544,5 +4585,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 4c0de94744..b11b092c2b 100644 --- a/indexer/services/comlink/public/swagger.json +++ b/indexer/services/comlink/public/swagger.json @@ -480,6 +480,18 @@ }, "FillResponse": { "properties": { + "pageSize": { + "type": "number", + "format": "double" + }, + "totalResults": { + "type": "number", + "format": "double" + }, + "offset": { + "type": "number", + "format": "double" + }, "fills": { "items": { "$ref": "#/components/schemas/FillResponseObject" @@ -1101,6 +1113,18 @@ }, "TradeResponse": { "properties": { + "pageSize": { + "type": "number", + "format": "double" + }, + "totalResults": { + "type": "number", + "format": "double" + }, + "offset": { + "type": "number", + "format": "double" + }, "trades": { "items": { "$ref": "#/components/schemas/TradeResponseObject" @@ -1193,6 +1217,18 @@ }, "TransferResponse": { "properties": { + "pageSize": { + "type": "number", + "format": "double" + }, + "totalResults": { + "type": "number", + "format": "double" + }, + "offset": { + "type": "number", + "format": "double" + }, "transfers": { "items": { "$ref": "#/components/schemas/TransferResponseObject" @@ -1208,6 +1244,18 @@ }, "ParentSubaccountTransferResponse": { "properties": { + "pageSize": { + "type": "number", + "format": "double" + }, + "totalResults": { + "type": "number", + "format": "double" + }, + "offset": { + "type": "number", + "format": "double" + }, "transfers": { "items": { "$ref": "#/components/schemas/TransferResponseObject" @@ -1598,6 +1646,15 @@ "schema": { "$ref": "#/components/schemas/IsoString" } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "double", + "type": "number" + } } ] } @@ -1677,6 +1734,15 @@ "schema": { "$ref": "#/components/schemas/IsoString" } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "double", + "type": "number" + } } ] } @@ -2504,6 +2570,15 @@ "schema": { "$ref": "#/components/schemas/IsoString" } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "double", + "type": "number" + } } ] } @@ -2567,6 +2642,15 @@ "schema": { "$ref": "#/components/schemas/IsoString" } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "double", + "type": "number" + } } ] } @@ -2630,6 +2714,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 e4e74bc3b6..a6b482e603 100644 --- a/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/fills-controller.ts @@ -28,7 +28,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'; @@ -120,6 +125,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. @@ -142,7 +148,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, @@ -151,6 +162,7 @@ class FillsController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIMIT], ); @@ -173,6 +185,9 @@ class FillsController extends Controller { return fillToResponseObject(fill, clobPairIdToMarket, childIdtoSubaccountNumber[fill.subaccountId]); }), + pageSize, + totalResults: total, + offset, }; } } @@ -182,6 +197,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 @@ -218,6 +234,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. @@ -235,6 +252,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, ); return res.send(response); @@ -260,6 +278,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 @@ -296,6 +315,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. @@ -313,6 +333,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 a9725ac823..335860b9af 100644 --- a/indexer/services/comlink/src/controllers/api/v4/trades-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/trades-controller.ts @@ -23,7 +23,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'; @@ -88,6 +88,7 @@ router.get( '/perpetualMarket/:ticker', rateLimiterMiddleware(getReqRateLimiter), ...CheckLimitAndCreatedBeforeOrAtSchema, + ...CheckPaginationSchema, ...checkSchema({ ticker: { in: ['params'], @@ -107,6 +108,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, }: TradeRequest = matchedData(req) as TradeRequest; try { @@ -116,6 +118,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 54ab9a7d02..5c67d3c949 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'; @@ -146,6 +147,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 @@ -154,7 +156,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[] @@ -172,6 +179,7 @@ class TransfersController extends Controller { ? createdBeforeOrAtHeight.toString() : undefined, createdBeforeOrAt, + page, }, [QueryableField.LIMIT], { @@ -236,7 +244,12 @@ class TransfersController extends Controller { return transfer.sender.parentSubaccountNumber !== transfer.recipient.parentSubaccountNumber; }); - return { transfers: transfersFiltered }; + return { + transfers: transfersFiltered, + pageSize, + totalResults: total, + offset, + }; } } @@ -245,6 +258,7 @@ router.get( rateLimiterMiddleware(getReqRateLimiter), ...CheckSubaccountSchema, ...CheckLimitAndCreatedBeforeOrAtSchema, + ...CheckPaginationSchema, handleValidationErrors, complianceAndGeoCheck, ExportResponseCodeStats({ controllerName }), @@ -256,6 +270,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, }: TransferRequest = matchedData(req) as TransferRequest; try { @@ -266,6 +281,7 @@ router.get( limit, createdBeforeOrAtHeight, createdBeforeOrAt, + page, ); return res.send(response); @@ -291,6 +307,7 @@ router.get( rateLimiterMiddleware(getReqRateLimiter), ...CheckParentSubaccountSchema, ...CheckLimitAndCreatedBeforeOrAtSchema, + ...CheckPaginationSchema, handleValidationErrors, complianceAndGeoCheck, ExportResponseCodeStats({ controllerName }), @@ -302,6 +319,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. @@ -315,6 +333,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 5d43d0efb5..4f545190a7 100644 --- a/indexer/services/comlink/src/lib/validation/schemas.ts +++ b/indexer/services/comlink/src/lib/validation/schemas.ts @@ -63,6 +63,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'], @@ -113,6 +124,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 d77bfeac3a..fdb15d494f 100644 --- a/indexer/services/comlink/src/types.ts +++ b/indexer/services/comlink/src/types.ts @@ -174,7 +174,7 @@ export interface TransferResponseObject { transactionHash: string, } -export interface ParentSubaccountTransferResponse { +export interface ParentSubaccountTransferResponse extends PaginationResponse { transfers: TransferResponseObject[], } @@ -358,6 +358,10 @@ export interface ParentSubaccountRequest extends AddressRequest { parentSubaccountNumber: number, } +export interface PaginationRequest { + page?: number; +} + export interface LimitRequest { limit: number, } @@ -395,24 +399,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, } From 6ea2dbf3509fa893dcc9b7a1e3c045f71304e8d8 Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Fri, 26 Apr 2024 19:11:33 +0200 Subject: [PATCH 8/9] test(comlink): :white_check_mark: update pagination tests --- .../api/v4/fills-controller.test.ts | 16 +- .../api/v4/trades-controller.test.ts | 12 +- .../api/v4/transfers-controller.test.ts | 159 +++++++++++++++++- 3 files changed, 165 insertions(+), 22 deletions(-) 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 99cb56209a..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 @@ -215,13 +215,13 @@ describe('fills-controller#V4', () => { const responsePage1: request.Response = await sendRequest({ type: RequestMethod.GET, path: `/v4/fills?address=${testConstants.defaultAddress}` + - `&subaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}?page=1&limit=1`, + `&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`, + `&subaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}&page=2&limit=1`, }); const expected: Partial[] = [ @@ -253,9 +253,9 @@ describe('fills-controller#V4', () => { }, ]; - expect(responsePage1.body.pageSize).toHaveLength(1); - expect(responsePage1.body.offset).toHaveLength(0); - expect(responsePage1.body.totalResults).toHaveLength(2); + 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([ @@ -265,9 +265,9 @@ describe('fills-controller#V4', () => { ]), ); - expect(responsePage2.body.pageSize).toHaveLength(1); - expect(responsePage2.body.offset).toHaveLength(1); - expect(responsePage2.body.totalResults).toHaveLength(2); + 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([ 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 e54c41ea4c..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 @@ -165,9 +165,9 @@ describe('trades-controller#V4', () => { ]; // Expect both trades, ordered by createdAtHeight in descending order - expect(responsePage1.body.pageSize).toHaveLength(1); - expect(responsePage1.body.offset).toHaveLength(0); - expect(responsePage1.body.totalResults).toHaveLength(2); + 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([ @@ -177,9 +177,9 @@ describe('trades-controller#V4', () => { ]), ); - expect(responsePage1.body.pageSize).toHaveLength(1); - expect(responsePage1.body.offset).toHaveLength(1); - expect(responsePage2.body.totalResults).toHaveLength(2); + 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([ 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 00aa8525c7..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 @@ -182,12 +182,12 @@ describe('transfers-controller#V4', () => { const responsePage1: request.Response = await sendRequest({ type: RequestMethod.GET, path: `/v4/transfers?address=${testConstants.defaultAddress}` + - `&subaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}?page=1&limit=2`, + `&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`, + `&subaccountNumber=${testConstants.defaultSubaccount.subaccountNumber}&page=2&limit=2`, }); const expectedTransferResponse: TransferResponseObject = { @@ -267,9 +267,9 @@ describe('transfers-controller#V4', () => { transactionHash: testConstants.defaultWithdrawal.transactionHash, }; - expect(responsePage1.body.pageSize).toHaveLength(2); - expect(responsePage1.body.offset).toHaveLength(0); - expect(responsePage1.body.totalResults).toHaveLength(4); + 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([ @@ -282,9 +282,9 @@ describe('transfers-controller#V4', () => { ]), ); - expect(responsePage2.body.pageSize).toHaveLength(2); - expect(responsePage2.body.offset).toHaveLength(2); - expect(responsePage2.body.totalResults).toHaveLength(4); + 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([ @@ -561,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 = { From 2fd8d09f2b96b3cb0e71f79b02c10d7b8bba992e Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Thu, 16 May 2024 10:44:19 +0200 Subject: [PATCH 9/9] feat: :sparkles: add default orderBy for pagination --- .../src/controllers/api/v4/fills-controller.ts | 4 ++++ .../src/controllers/api/v4/trades-controller.ts | 3 +++ .../src/controllers/api/v4/transfers-controller.ts | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) 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 a6b482e603..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 { @@ -91,6 +93,7 @@ class FillsController extends Controller { page, }, [QueryableField.LIMIT], + { orderBy: [[FillColumns.eventId, Ordering.ASC]] }, ); const clobPairIdToPerpetualMarket: Record< @@ -165,6 +168,7 @@ class FillsController extends Controller { page, }, [QueryableField.LIMIT], + { orderBy: [[FillColumns.eventId, Ordering.ASC]] }, ); const clobPairIdToPerpetualMarket: Record< 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 335860b9af..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 { @@ -71,6 +73,7 @@ class TradesController extends Controller { page, }, [QueryableField.LIQUIDITY, QueryableField.CLOB_PAIR_ID, QueryableField.LIMIT], + { orderBy: [[FillColumns.eventId, Ordering.ASC]] }, ); return { 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 5c67d3c949..198597f0d3 100644 --- a/indexer/services/comlink/src/controllers/api/v4/transfers-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/transfers-controller.ts @@ -86,7 +86,12 @@ class TransfersController extends Controller { [QueryableField.LIMIT], { ...DEFAULT_POSTGRES_OPTIONS, - orderBy: [[TransferColumns.createdAtHeight, Ordering.DESC]], + orderBy: page !== undefined ? [ + [TransferColumns.eventId, Ordering.DESC], + ] + : [ + [TransferColumns.createdAtHeight, Ordering.DESC], + ], }, ), AssetTable.findAll( @@ -184,7 +189,12 @@ class TransfersController extends Controller { [QueryableField.LIMIT], { ...DEFAULT_POSTGRES_OPTIONS, - orderBy: [[TransferColumns.createdAtHeight, Ordering.DESC]], + orderBy: page !== undefined ? [ + [TransferColumns.eventId, Ordering.DESC], + ] + : [ + [TransferColumns.createdAtHeight, Ordering.DESC], + ], }, ), AssetTable.findAll(