From 63854736981fba9c9fbd3d90ef0e161b3bdbca17 Mon Sep 17 00:00:00 2001 From: Christopher-Li Date: Thu, 11 Jan 2024 13:15:57 -0500 Subject: [PATCH] [IND-503]: Add historical trading reward block and aggregation endpoints (#954) --- .../trading-reward-aggregation-table.ts | 16 + .../src/stores/trading-reward-table.ts | 6 + .../postgres/src/types/query-types.ts | 6 + ...al-block-trading-reward-controller.test.ts | 140 ++++++++ ...ing-reward-aggregations-controller.test.ts | 153 +++++++++ .../comlink/public/api-documentation.md | 301 ++++++++++++++++++ indexer/services/comlink/public/swagger.json | 202 ++++++++++++ .../comlink/src/controllers/api/index-v4.ts | 8 +- .../api/v4/addresses-controller.ts | 10 +- ...orical-block-trading-rewards-controller.ts | 103 ++++++ ...-trading-reward-aggregations-controller.ts | 118 +++++++ .../comlink/src/lib/validation/schemas.ts | 27 ++ .../request-helpers/request-transformer.ts | 27 ++ indexer/services/comlink/src/types.ts | 44 +++ 14 files changed, 1151 insertions(+), 10 deletions(-) create mode 100644 indexer/services/comlink/__tests__/controllers/api/v4/historical-block-trading-reward-controller.test.ts create mode 100644 indexer/services/comlink/__tests__/controllers/api/v4/historical-trading-reward-aggregations-controller.test.ts create mode 100644 indexer/services/comlink/src/controllers/api/v4/historical-block-trading-rewards-controller.ts create mode 100644 indexer/services/comlink/src/controllers/api/v4/historical-trading-reward-aggregations-controller.ts diff --git a/indexer/packages/postgres/src/stores/trading-reward-aggregation-table.ts b/indexer/packages/postgres/src/stores/trading-reward-aggregation-table.ts index 6f69c36e21..39f5253363 100644 --- a/indexer/packages/postgres/src/stores/trading-reward-aggregation-table.ts +++ b/indexer/packages/postgres/src/stores/trading-reward-aggregation-table.ts @@ -34,6 +34,8 @@ export async function findAll( startedAtHeight, period, limit, + startedAtBeforeOrAt, + startedAtHeightBeforeOrAt, }: TradingRewardAggregationQueryConfig, requiredFields: QueryableField[], options: Options = DEFAULT_POSTGRES_OPTIONS, @@ -45,6 +47,8 @@ export async function findAll( startedAtHeight, period, limit, + startedAtBeforeOrAt, + startedAtHeightBeforeOrAt, } as QueryConfig, requiredFields, ); @@ -71,6 +75,18 @@ export async function findAll( baseQuery = baseQuery.where(TradingRewardAggregationColumns.period, period); } + if (startedAtBeforeOrAt) { + baseQuery = baseQuery.where(TradingRewardAggregationColumns.startedAt, '<=', startedAtBeforeOrAt); + } + + if (startedAtHeightBeforeOrAt) { + baseQuery = baseQuery.where( + TradingRewardAggregationColumns.startedAtHeight, + '<=', + startedAtHeightBeforeOrAt, + ); + } + if (options.orderBy !== undefined) { for (const [column, order] of options.orderBy) { baseQuery = baseQuery.orderBy( diff --git a/indexer/packages/postgres/src/stores/trading-reward-table.ts b/indexer/packages/postgres/src/stores/trading-reward-table.ts index 6becfe3299..858da434c4 100644 --- a/indexer/packages/postgres/src/stores/trading-reward-table.ts +++ b/indexer/packages/postgres/src/stores/trading-reward-table.ts @@ -28,6 +28,7 @@ export async function findAll( blockTimeBeforeOrAt, blockTimeAfterOrAt, blockTimeBefore, + blockHeightBeforeOrAt, limit, }: TradingRewardQueryConfig, requiredFields: QueryableField[], @@ -40,6 +41,7 @@ export async function findAll( blockTimeBeforeOrAt, blockTimeAfterOrAt, blockTimeBefore, + blockHeightBeforeOrAt, limit, } as QueryConfig, requiredFields, @@ -70,6 +72,10 @@ export async function findAll( baseQuery = baseQuery.where(TradingRewardColumns.blockTime, '<', blockTimeBefore); } + if (blockHeightBeforeOrAt) { + baseQuery = baseQuery.where(TradingRewardColumns.blockHeight, '<=', blockHeightBeforeOrAt); + } + if (options.orderBy !== undefined) { for (const [column, order] of options.orderBy) { baseQuery = baseQuery.orderBy( diff --git a/indexer/packages/postgres/src/types/query-types.ts b/indexer/packages/postgres/src/types/query-types.ts index 99eceee8fe..4a33a6564d 100644 --- a/indexer/packages/postgres/src/types/query-types.ts +++ b/indexer/packages/postgres/src/types/query-types.ts @@ -79,6 +79,9 @@ export enum QueryableField { BLOCK_TIME_AFTER_OR_AT = 'blockTimeAfterOrAt', BLOCK_TIME_BEFORE = 'blockTimeBefore', ADDRESSES = 'addresses', + BLOCK_HEIGHT_BEFORE_OR_AT = 'blockHeightBeforeOrAt', + STARTED_AT_BEFORE_OR_AT = 'startedAtBeforeOrAt', + STARTED_AT_HEIGHT_BEFORE_OR_AT = 'startedAtHeightBeforeOrAt', } export interface QueryConfig { @@ -280,6 +283,7 @@ export interface TradingRewardQueryConfig extends QueryConfig { [QueryableField.BLOCK_TIME_BEFORE_OR_AT]?: IsoString; [QueryableField.BLOCK_TIME_AFTER_OR_AT]?: IsoString; [QueryableField.BLOCK_TIME_BEFORE]?: IsoString; + [QueryableField.BLOCK_HEIGHT_BEFORE_OR_AT]?: IsoString; } export interface TradingRewardAggregationQueryConfig extends QueryConfig { @@ -288,4 +292,6 @@ export interface TradingRewardAggregationQueryConfig extends QueryConfig { [QueryableField.STARTED_AT_HEIGHT]?: string; [QueryableField.STARTED_AT_HEIGHT_OR_AFTER]?: string; [QueryableField.PERIOD]?: TradingRewardAggregationPeriod; + [QueryableField.STARTED_AT_BEFORE_OR_AT]?: IsoString; + [QueryableField.STARTED_AT_HEIGHT_BEFORE_OR_AT]?: string; } diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/historical-block-trading-reward-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/historical-block-trading-reward-controller.test.ts new file mode 100644 index 0000000000..efe35b07e8 --- /dev/null +++ b/indexer/services/comlink/__tests__/controllers/api/v4/historical-block-trading-reward-controller.test.ts @@ -0,0 +1,140 @@ +import { + HistoricalBlockTradingReward, + HistoricalBlockTradingRewardsResponse, + HistoricalTradingRewardAggregation, + HistoricalTradingRewardAggregationsResponse, + RequestMethod, +} from '../../../../src/types'; +import { getQueryString, sendRequest } from '../../../helpers/helpers'; +import { + TradingRewardCreateObject, + TradingRewardFromDatabase, + TradingRewardTable, + dbHelpers, + testConstants, + testConversionHelpers, + testMocks, +} from '@dydxprotocol-indexer/postgres'; +import { stats } from '@dydxprotocol-indexer/base'; +import request from 'supertest'; +import { tradingRewardToResponse } from '../../../../src/request-helpers/request-transformer'; + +describe('historical-block-trading-reward-controller#V4', () => { + beforeAll(async () => { + await dbHelpers.migrate(); + jest.spyOn(stats, 'increment'); + jest.spyOn(stats, 'timing'); + }); + + beforeEach(async () => { + await testMocks.seedData(); + await Promise.all([ + TradingRewardTable.create(defaultTradingRewardCreate), + TradingRewardTable.create(defaultTradingRewardCreate2), + ]); + + const rewards: TradingRewardFromDatabase[] = await TradingRewardTable.findAll({}, []); + + defaultTradingReward = rewards[1]; + defaultTradingReward2 = rewards[0]; + }); + + afterAll(async () => { + await dbHelpers.teardown(); + }); + + afterEach(async () => { + await dbHelpers.clearData(); + }); + + const defaultTradingRewardCreate: TradingRewardCreateObject = { + address: testConstants.defaultAddress, + blockTime: testConstants.defaultBlock.time, + blockHeight: testConstants.defaultBlock.blockHeight, + amount: testConversionHelpers.convertToDenomScale('10'), + }; + let defaultTradingReward: TradingRewardFromDatabase; + const defaultTradingRewardCreate2: TradingRewardCreateObject = { + address: testConstants.defaultAddress, + blockTime: testConstants.defaultBlock2.time, + blockHeight: testConstants.defaultBlock2.blockHeight, + amount: testConversionHelpers.convertToDenomScale('5'), + }; + let defaultTradingReward2: TradingRewardFromDatabase; + + describe('GET', () => { + it('Get /historicalBlockTradingReward/:address returns all valid rewards', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/historicalBlockTradingRewards/${testConstants.defaultAddress}`, + }); + + const responseBody: HistoricalTradingRewardAggregationsResponse = response.body; + const rewards: HistoricalTradingRewardAggregation[] = responseBody.rewards; + expect(rewards.length).toEqual(2); + console.log(JSON.stringify(rewards)); + expect(rewards[0]).toEqual(tradingRewardToResponse( + defaultTradingReward2, + )); + expect(rewards[1]).toEqual(tradingRewardToResponse( + defaultTradingReward, + )); + }); + + it('Get /historicalBlockTradingRewards/:address returns all valid rewards with limit', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/historicalBlockTradingRewards/${testConstants.defaultAddress}` + + `?${getQueryString({ limit: 1 })}`, + }); + + const responseBody: HistoricalBlockTradingRewardsResponse = response.body; + const rewards: HistoricalBlockTradingReward[] = responseBody.rewards; + expect(rewards.length).toEqual(1); + expect(rewards[0]).toEqual(tradingRewardToResponse( + defaultTradingReward2, + )); + }); + + it('Get /historicalBlockTradingRewards/:address returns no rewards when none exist', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: '/v4/historicalBlockTradingRewards/fakeAddress', + }); + + const responseBody: HistoricalBlockTradingRewardsResponse = response.body; + const rewards: HistoricalBlockTradingReward[] = responseBody.rewards; + expect(rewards.length).toEqual(0); + }); + + it('Get /historicalBlockTradingRewards/:address returns rewards with blockTimeBeforeOrAt', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/historicalBlockTradingRewards/${testConstants.defaultAddress}` + + `?${getQueryString({ startingBeforeOrAt: testConstants.defaultBlock.time })}`, + }); + + const responseBody: HistoricalBlockTradingRewardsResponse = response.body; + const rewards: HistoricalBlockTradingReward[] = responseBody.rewards; + expect(rewards.length).toEqual(1); + expect(rewards[0]).toEqual(tradingRewardToResponse( + defaultTradingReward, + )); + }); + + it('Get /historicalBlockTradingRewards/:address returns rewards with blockHeightBeforeOrAt', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/historicalBlockTradingRewards/${testConstants.defaultAddress}` + + `?${getQueryString({ startingBeforeOrAtHeight: testConstants.defaultBlock.blockHeight })}`, + }); + + const responseBody: HistoricalBlockTradingRewardsResponse = response.body; + const rewards: HistoricalBlockTradingReward[] = responseBody.rewards; + expect(rewards.length).toEqual(1); + expect(rewards[0]).toEqual(tradingRewardToResponse( + defaultTradingReward, + )); + }); + }); +}); diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/historical-trading-reward-aggregations-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/historical-trading-reward-aggregations-controller.test.ts new file mode 100644 index 0000000000..62f4448e14 --- /dev/null +++ b/indexer/services/comlink/__tests__/controllers/api/v4/historical-trading-reward-aggregations-controller.test.ts @@ -0,0 +1,153 @@ +import { + HistoricalTradingRewardAggregation, + HistoricalTradingRewardAggregationsResponse, + RequestMethod, +} from '../../../../src/types'; +import { getQueryString, sendRequest } from '../../../helpers/helpers'; +import { + TradingRewardAggregationCreateObject, + TradingRewardAggregationFromDatabase, + TradingRewardAggregationPeriod, + TradingRewardAggregationTable, + dbHelpers, + testConstants, + testConversionHelpers, + testMocks, +} from '@dydxprotocol-indexer/postgres'; +import { stats } from '@dydxprotocol-indexer/base'; +import { DateTime } from 'luxon'; +import request from 'supertest'; +import { tradingRewardAggregationToResponse } from '../../../../src/request-helpers/request-transformer'; + +describe('historical-trading-reward-aggregations-controller#V4', () => { + beforeAll(async () => { + await dbHelpers.migrate(); + jest.spyOn(stats, 'increment'); + jest.spyOn(stats, 'timing'); + }); + + beforeEach(async () => { + await testMocks.seedData(); + await Promise.all([ + TradingRewardAggregationTable.create(defaultCompletedTradingRewardAggregationCreate), + TradingRewardAggregationTable.create(defaultIncompleteTradingRewardAggregationCreate), + ]); + const aggregations: + TradingRewardAggregationFromDatabase[] = await TradingRewardAggregationTable.findAll({}, []); + + defaultCompletedTradingRewardAggregation = aggregations[0]; + defaultIncompleteTradingRewardAggregation = aggregations[1]; + }); + + afterAll(async () => { + await dbHelpers.teardown(); + }); + + afterEach(async () => { + await dbHelpers.clearData(); + }); + + const startedAt: DateTime = testConstants.createdDateTime.startOf('month').toUTC(); + const startedAt2: DateTime = startedAt.plus({ month: 1 }); + const defaultCompletedTradingRewardAggregationCreate: TradingRewardAggregationCreateObject = { + address: testConstants.defaultAddress, + startedAt: startedAt.toISO(), + startedAtHeight: testConstants.defaultBlock.blockHeight, + endedAt: startedAt2.toISO(), + endedAtHeight: '10000', // ignored field for the purposes of this test + period: TradingRewardAggregationPeriod.MONTHLY, + amount: testConversionHelpers.convertToDenomScale('10'), + }; + let defaultCompletedTradingRewardAggregation: TradingRewardAggregationFromDatabase; + const defaultIncompleteTradingRewardAggregationCreate: TradingRewardAggregationCreateObject = { + address: testConstants.defaultAddress, + startedAt: startedAt2.toISO(), + startedAtHeight: testConstants.defaultBlock2.blockHeight, + period: TradingRewardAggregationPeriod.MONTHLY, + amount: testConversionHelpers.convertToDenomScale('20'), + }; + let defaultIncompleteTradingRewardAggregation: TradingRewardAggregationFromDatabase; + + describe('GET', () => { + it('Get /historicalTradingRewardAggregations/:address returns all valid aggregations', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/historicalTradingRewardAggregations/${testConstants.defaultAddress}` + + `?${getQueryString({ period: TradingRewardAggregationPeriod.MONTHLY })}`, + }); + + const responseBody: HistoricalTradingRewardAggregationsResponse = response.body; + const rewards: HistoricalTradingRewardAggregation[] = responseBody.rewards; + expect(rewards.length).toEqual(2); + expect(rewards[0]).toEqual(tradingRewardAggregationToResponse( + defaultIncompleteTradingRewardAggregation, + )); + expect(rewards[1]).toEqual(tradingRewardAggregationToResponse( + defaultCompletedTradingRewardAggregation, + )); + }); + + it('Get /historicalTradingRewardAggregations/:address returns all valid aggregations with limit', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/historicalTradingRewardAggregations/${testConstants.defaultAddress}` + + `?${getQueryString({ period: TradingRewardAggregationPeriod.MONTHLY, limit: 1 })}`, + }); + + const responseBody: HistoricalTradingRewardAggregationsResponse = response.body; + const rewards: HistoricalTradingRewardAggregation[] = responseBody.rewards; + expect(rewards.length).toEqual(1); + expect(rewards[0]).toEqual(tradingRewardAggregationToResponse( + defaultIncompleteTradingRewardAggregation, + )); + }); + + it('Get /historicalTradingRewardAggregations/:address returns no aggregations when none exist', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/historicalTradingRewardAggregations/${testConstants.defaultAddress}` + + `?${getQueryString({ period: TradingRewardAggregationPeriod.DAILY })}`, + }); + + const responseBody: HistoricalTradingRewardAggregationsResponse = response.body; + const rewards: HistoricalTradingRewardAggregation[] = responseBody.rewards; + expect(rewards.length).toEqual(0); + }); + + it('Get /historicalTradingRewardAggregations/:address returns aggregations with startedAtBeforeOrAt', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/historicalTradingRewardAggregations/${testConstants.defaultAddress}` + + `?${getQueryString({ + period: TradingRewardAggregationPeriod.MONTHLY, + startingBeforeOrAt: startedAt.toISO(), + })}`, + }); + + const responseBody: HistoricalTradingRewardAggregationsResponse = response.body; + const rewards: HistoricalTradingRewardAggregation[] = responseBody.rewards; + expect(rewards.length).toEqual(1); + expect(rewards[0]).toEqual(tradingRewardAggregationToResponse( + defaultCompletedTradingRewardAggregation, + )); + }); + + it('Get /historicalTradingRewardAggregations/:address returns aggregations with startedAtHeightBeforeOrAt', async () => { + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/historicalTradingRewardAggregations/${testConstants.defaultAddress}` + + `?${getQueryString({ + period: TradingRewardAggregationPeriod.MONTHLY, + startingBeforeOrAtHeight: testConstants.defaultBlock.blockHeight, + })}`, + }); + + const responseBody: HistoricalTradingRewardAggregationsResponse = response.body; + const rewards: HistoricalTradingRewardAggregation[] = responseBody.rewards; + expect(rewards.length).toEqual(1); + expect(rewards[0]).toEqual(tradingRewardAggregationToResponse( + defaultCompletedTradingRewardAggregation, + )); + }); + }); +}); diff --git a/indexer/services/comlink/public/api-documentation.md b/indexer/services/comlink/public/api-documentation.md index 65bac1b7d4..c827bdf3b8 100644 --- a/indexer/services/comlink/public/api-documentation.md +++ b/indexer/services/comlink/public/api-documentation.md @@ -659,6 +659,83 @@ fetch('https://dydx-testnet.imperator.co/v4/height', This operation does not require authentication +## GetTradingRewards + + + +> Code samples + +```python +import requests +headers = { + 'Accept': 'application/json' +} + +r = requests.get('https://dydx-testnet.imperator.co/v4/historicalBlockTradingRewards/{address}', params={ + 'limit': '0' +}, headers = headers) + +print(r.json()) + +``` + +```javascript + +const headers = { + 'Accept':'application/json' +}; + +fetch('https://dydx-testnet.imperator.co/v4/historicalBlockTradingRewards/{address}?limit=0', +{ + method: 'GET', + + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +`GET /historicalBlockTradingRewards/{address}` + +### Parameters + +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|address|path|string|true|none| +|limit|query|number(double)|true|none| +|startingBeforeOrAt|query|[IsoString](#schemaisostring)|false|none| +|startingBeforeOrAtHeight|query|string|false|none| + +> Example responses + +> 200 Response + +```json +{ + "rewards": [ + { + "tradingReward": "string", + "createdAt": "string", + "createdAtHeight": "string" + } + ] +} +``` + +### Responses + +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Ok|[HistoricalBlockTradingRewardsResponse](#schemahistoricalblocktradingrewardsresponse)| + + + ## GetHistoricalFunding @@ -823,6 +900,95 @@ fetch('https://dydx-testnet.imperator.co/v4/historical-pnl?address=string&subacc This operation does not require authentication +## GetAggregations + + + +> Code samples + +```python +import requests +headers = { + 'Accept': 'application/json' +} + +r = requests.get('https://dydx-testnet.imperator.co/v4/historicalTradingRewardAggregations/{address}', params={ + 'period': 'DAILY', 'limit': '0' +}, headers = headers) + +print(r.json()) + +``` + +```javascript + +const headers = { + 'Accept':'application/json' +}; + +fetch('https://dydx-testnet.imperator.co/v4/historicalTradingRewardAggregations/{address}?period=DAILY&limit=0', +{ + method: 'GET', + + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +`GET /historicalTradingRewardAggregations/{address}` + +### Parameters + +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|address|path|string|true|none| +|period|query|[TradingRewardAggregationPeriod](#schematradingrewardaggregationperiod)|true|none| +|limit|query|number(double)|true|none| +|startingBeforeOrAt|query|[IsoString](#schemaisostring)|false|none| +|startingBeforeOrAtHeight|query|string|false|none| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|period|DAILY| +|period|WEEKLY| +|period|MONTHLY| + +> Example responses + +> 200 Response + +```json +{ + "rewards": [ + { + "tradingReward": "string", + "startedAt": "string", + "startedAtHeight": "string", + "endedAt": "string", + "endedAtHeight": "string", + "period": "DAILY" + } + ] +} +``` + +### Responses + +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Ok|[HistoricalTradingRewardAggregationsResponse](#schemahistoricaltradingrewardaggregationsresponse)| + + + ## GetPerpetualMarket @@ -2450,6 +2616,56 @@ This operation does not require authentication |height|string|true|none|none| |time|[IsoString](#schemaisostring)|true|none|none| +## HistoricalBlockTradingReward + + + + + + +```json +{ + "tradingReward": "string", + "createdAt": "string", + "createdAtHeight": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|tradingReward|string|true|none|none| +|createdAt|[IsoString](#schemaisostring)|true|none|none| +|createdAtHeight|string|true|none|none| + +## HistoricalBlockTradingRewardsResponse + + + + + + +```json +{ + "rewards": [ + { + "tradingReward": "string", + "createdAt": "string", + "createdAtHeight": "string" + } + ] +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|rewards|[[HistoricalBlockTradingReward](#schemahistoricalblocktradingreward)]|true|none|none| + ## HistoricalFundingResponseObject @@ -2571,6 +2787,91 @@ This operation does not require authentication |---|---|---|---|---| |historicalPnl|[[PnlTicksResponseObject](#schemapnlticksresponseobject)]|true|none|none| +## TradingRewardAggregationPeriod + + + + + + +```json +"DAILY" + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|string|false|none|none| + +#### Enumerated Values + +|Property|Value| +|---|---| +|*anonymous*|DAILY| +|*anonymous*|WEEKLY| +|*anonymous*|MONTHLY| + +## HistoricalTradingRewardAggregation + + + + + + +```json +{ + "tradingReward": "string", + "startedAt": "string", + "startedAtHeight": "string", + "endedAt": "string", + "endedAtHeight": "string", + "period": "DAILY" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|tradingReward|string|true|none|none| +|startedAt|[IsoString](#schemaisostring)|true|none|none| +|startedAtHeight|string|true|none|none| +|endedAt|[IsoString](#schemaisostring)|false|none|none| +|endedAtHeight|string|false|none|none| +|period|[TradingRewardAggregationPeriod](#schematradingrewardaggregationperiod)|true|none|none| + +## HistoricalTradingRewardAggregationsResponse + + + + + + +```json +{ + "rewards": [ + { + "tradingReward": "string", + "startedAt": "string", + "startedAtHeight": "string", + "endedAt": "string", + "endedAtHeight": "string", + "period": "DAILY" + } + ] +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|rewards|[[HistoricalTradingRewardAggregation](#schemahistoricaltradingrewardaggregation)]|true|none|none| + ## OrderbookResponsePriceLevel diff --git a/indexer/services/comlink/public/swagger.json b/indexer/services/comlink/public/swagger.json index 1529c3fbe2..5069c88ede 100644 --- a/indexer/services/comlink/public/swagger.json +++ b/indexer/services/comlink/public/swagger.json @@ -424,6 +424,41 @@ "type": "object", "additionalProperties": false }, + "HistoricalBlockTradingReward": { + "properties": { + "tradingReward": { + "type": "string" + }, + "createdAt": { + "$ref": "#/components/schemas/IsoString" + }, + "createdAtHeight": { + "type": "string" + } + }, + "required": [ + "tradingReward", + "createdAt", + "createdAtHeight" + ], + "type": "object", + "additionalProperties": false + }, + "HistoricalBlockTradingRewardsResponse": { + "properties": { + "rewards": { + "items": { + "$ref": "#/components/schemas/HistoricalBlockTradingReward" + }, + "type": "array" + } + }, + "required": [ + "rewards" + ], + "type": "object", + "additionalProperties": false + }, "HistoricalFundingResponseObject": { "properties": { "ticker": { @@ -522,6 +557,59 @@ "type": "object", "additionalProperties": false }, + "TradingRewardAggregationPeriod": { + "enum": [ + "DAILY", + "WEEKLY", + "MONTHLY" + ], + "type": "string" + }, + "HistoricalTradingRewardAggregation": { + "properties": { + "tradingReward": { + "type": "string" + }, + "startedAt": { + "$ref": "#/components/schemas/IsoString" + }, + "startedAtHeight": { + "type": "string" + }, + "endedAt": { + "$ref": "#/components/schemas/IsoString" + }, + "endedAtHeight": { + "type": "string" + }, + "period": { + "$ref": "#/components/schemas/TradingRewardAggregationPeriod" + } + }, + "required": [ + "tradingReward", + "startedAt", + "startedAtHeight", + "period" + ], + "type": "object", + "additionalProperties": false + }, + "HistoricalTradingRewardAggregationsResponse": { + "properties": { + "rewards": { + "items": { + "$ref": "#/components/schemas/HistoricalTradingRewardAggregation" + }, + "type": "array" + } + }, + "required": [ + "rewards" + ], + "type": "object", + "additionalProperties": false + }, "OrderbookResponsePriceLevel": { "properties": { "price": { @@ -1309,6 +1397,59 @@ "parameters": [] } }, + "/historicalBlockTradingRewards/{address}": { + "get": { + "operationId": "GetTradingRewards", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistoricalBlockTradingRewardsResponse" + } + } + } + } + }, + "security": [], + "parameters": [ + { + "in": "path", + "name": "address", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": true, + "schema": { + "format": "double", + "type": "number" + } + }, + { + "in": "query", + "name": "startingBeforeOrAt", + "required": false, + "schema": { + "$ref": "#/components/schemas/IsoString" + } + }, + { + "in": "query", + "name": "startingBeforeOrAtHeight", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, "/historicalFunding/{ticker}": { "get": { "operationId": "GetHistoricalFunding", @@ -1443,6 +1584,67 @@ ] } }, + "/historicalTradingRewardAggregations/{address}": { + "get": { + "operationId": "GetAggregations", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistoricalTradingRewardAggregationsResponse" + } + } + } + } + }, + "security": [], + "parameters": [ + { + "in": "path", + "name": "address", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "period", + "required": true, + "schema": { + "$ref": "#/components/schemas/TradingRewardAggregationPeriod" + } + }, + { + "in": "query", + "name": "limit", + "required": true, + "schema": { + "format": "double", + "type": "number" + } + }, + { + "in": "query", + "name": "startingBeforeOrAt", + "required": false, + "schema": { + "$ref": "#/components/schemas/IsoString" + } + }, + { + "in": "query", + "name": "startingBeforeOrAtHeight", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, "/orderbooks/perpetualMarket/{ticker}": { "get": { "operationId": "GetPerpetualMarket", diff --git a/indexer/services/comlink/src/controllers/api/index-v4.ts b/indexer/services/comlink/src/controllers/api/index-v4.ts index 20bb951bd6..0689c08983 100644 --- a/indexer/services/comlink/src/controllers/api/index-v4.ts +++ b/indexer/services/comlink/src/controllers/api/index-v4.ts @@ -6,8 +6,10 @@ import CandlesController from './v4/candles-controller'; import ComplianceController from './v4/compliance-controller'; import FillsController from './v4/fills-controller'; import HeightController from './v4/height-controller'; +import HistoricalBlockTradingRewardController from './v4/historical-block-trading-rewards-controller'; import HistoricalFundingController from './v4/historical-funding-controller'; import PnlticksController from './v4/historical-pnl-controller'; +import HistoricalTradingRewardController from './v4/historical-trading-reward-aggregations-controller'; import OrderbooksController from './v4/orderbook-controller'; import OrdersController from './v4/orders-controller'; import PerpetualMarketController from './v4/perpetual-markets-controller'; @@ -25,12 +27,14 @@ router.use('/assetPositions', AssetPositionsController); router.use('/candles', CandlesController); router.use('/fills', FillsController); router.use('/height', HeightController); +router.use('/historicalBlockTradingRewards', HistoricalBlockTradingRewardController); +router.use('/historicalFunding', HistoricalFundingController); +router.use('/historical-pnl', PnlticksController); +router.use('/historicalTradingRewardAggregations', HistoricalTradingRewardController); router.use('/orders', OrdersController); router.use('/orderbooks', OrderbooksController); router.use('/perpetualMarkets', PerpetualMarketController); router.use('/perpetualPositions', PerpetualPositionsController); -router.use('/historicalFunding', HistoricalFundingController); -router.use('/historical-pnl', PnlticksController); router.use('/sparklines', SparklinesController); router.use('/time', TimeController); router.use('/trades', TradesController); diff --git a/indexer/services/comlink/src/controllers/api/v4/addresses-controller.ts b/indexer/services/comlink/src/controllers/api/v4/addresses-controller.ts index 1094641fc8..83d6034983 100644 --- a/indexer/services/comlink/src/controllers/api/v4/addresses-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/addresses-controller.ts @@ -29,7 +29,6 @@ import Big from 'big.js'; import express from 'express'; import { matchedData, - checkSchema, } from 'express-validator'; import _ from 'lodash'; import { @@ -53,7 +52,7 @@ import { } from '../../../lib/helpers'; import { rateLimiterMiddleware } from '../../../lib/rate-limit'; import { rejectRestrictedCountries } from '../../../lib/restrict-countries'; -import { CheckSubaccountSchema } from '../../../lib/validation/schemas'; +import { CheckAddressSchema, CheckSubaccountSchema } from '../../../lib/validation/schemas'; import { handleValidationErrors } from '../../../request-helpers/error-handler'; import ExportResponseCodeStats from '../../../request-helpers/export-response-code-stats'; import { @@ -258,12 +257,7 @@ router.get( '/:address', rejectRestrictedCountries, rateLimiterMiddleware(getReqRateLimiter), - ...checkSchema({ - address: { - in: ['params'], - isString: true, - }, - }), + ...CheckAddressSchema, handleValidationErrors, complianceCheck, ExportResponseCodeStats({ controllerName }), diff --git a/indexer/services/comlink/src/controllers/api/v4/historical-block-trading-rewards-controller.ts b/indexer/services/comlink/src/controllers/api/v4/historical-block-trading-rewards-controller.ts new file mode 100644 index 0000000000..64d723e917 --- /dev/null +++ b/indexer/services/comlink/src/controllers/api/v4/historical-block-trading-rewards-controller.ts @@ -0,0 +1,103 @@ +import { stats } from '@dydxprotocol-indexer/base'; +import { + IsoString, + Ordering, + TradingRewardColumns, + TradingRewardFromDatabase, + TradingRewardTable, +} from '@dydxprotocol-indexer/postgres'; +import express from 'express'; +import { matchedData } from 'express-validator'; +import _ from 'lodash'; +import { + Controller, Get, Path, Query, Route, +} from 'tsoa'; + +import { getReqRateLimiter } from '../../../caches/rate-limiters'; +import config from '../../../config'; +import { handleControllerError } from '../../../lib/helpers'; +import { rateLimiterMiddleware } from '../../../lib/rate-limit'; +import { rejectRestrictedCountries } from '../../../lib/restrict-countries'; +import { CheckHistoricalBlockTradingRewardsSchema } from '../../../lib/validation/schemas'; +import { handleValidationErrors } from '../../../request-helpers/error-handler'; +import ExportResponseCodeStats from '../../../request-helpers/export-response-code-stats'; +import { tradingRewardToResponse } from '../../../request-helpers/request-transformer'; +import { HistoricalBlockTradingRewardRequest as HistoricalBlockTradingRewardsRequest, HistoricalBlockTradingRewardsResponse } from '../../../types'; + +const router: express.Router = express.Router(); +const controllerName: string = 'height-controller'; + +@Route('historicalBlockTradingRewards') +class HistoricalBlockTradingRewardsController extends Controller { + @Get('/:address') + async getTradingRewards( + @Path() address: string, + @Query() limit: number, + @Query() startingBeforeOrAt?: IsoString, + @Query() startingBeforeOrAtHeight?: string, + ): Promise { + const tradingRewardAggregations: + TradingRewardFromDatabase[] = await TradingRewardTable.findAll({ + address, + limit, + blockTimeBeforeOrAt: startingBeforeOrAt, + blockHeightBeforeOrAt: startingBeforeOrAtHeight, + }, [], { orderBy: [[TradingRewardColumns.blockHeight, Ordering.DESC]] }); + + return { + rewards: _.map( + tradingRewardAggregations, + tradingRewardToResponse, + ), + }; + } +} + +router.get( + '/:address', + rejectRestrictedCountries, + rateLimiterMiddleware(getReqRateLimiter), + ...CheckHistoricalBlockTradingRewardsSchema, + handleValidationErrors, + ExportResponseCodeStats({ controllerName }), + async (req: express.Request, res: express.Response) => { + const start: number = Date.now(); + const { + address, + limit, + startingBeforeOrAt, + startingBeforeOrAtHeight, + }: HistoricalBlockTradingRewardsRequest = matchedData( + req, + ) as HistoricalBlockTradingRewardsRequest; + + try { + const controller: + HistoricalBlockTradingRewardsController = new HistoricalBlockTradingRewardsController(); + const response: HistoricalBlockTradingRewardsResponse = await controller.getTradingRewards( + address, + limit, + startingBeforeOrAt, + startingBeforeOrAtHeight, + ); + console.log(`response: ${JSON.stringify(response)}`); + + return res.send(response); + } catch (error) { + return handleControllerError( + 'HistoricalBlockTradingRewardsController GET /', + 'HistoricalBlockTradingRewards error', + error, + req, + res, + ); + } finally { + stats.timing( + `${config.SERVICE_NAME}.${controllerName}.get_historical_block_trading_reward.timing`, + Date.now() - start, + ); + } + }, +); + +export default router; diff --git a/indexer/services/comlink/src/controllers/api/v4/historical-trading-reward-aggregations-controller.ts b/indexer/services/comlink/src/controllers/api/v4/historical-trading-reward-aggregations-controller.ts new file mode 100644 index 0000000000..4bf1237406 --- /dev/null +++ b/indexer/services/comlink/src/controllers/api/v4/historical-trading-reward-aggregations-controller.ts @@ -0,0 +1,118 @@ +import { stats } from '@dydxprotocol-indexer/base'; +import { + TradingRewardAggregationTable, + TradingRewardAggregationPeriod, + IsoString, + TradingRewardAggregationFromDatabase, + TradingRewardAggregationColumns, + Ordering, +} from '@dydxprotocol-indexer/postgres'; +import express from 'express'; +import { checkSchema, matchedData } from 'express-validator'; +import _ from 'lodash'; +import { + Controller, Get, Path, Query, Route, +} from 'tsoa'; + +import { getReqRateLimiter } from '../../../caches/rate-limiters'; +import config from '../../../config'; +import { handleControllerError } from '../../../lib/helpers'; +import { rateLimiterMiddleware } from '../../../lib/rate-limit'; +import { rejectRestrictedCountries } from '../../../lib/restrict-countries'; +import { CheckHistoricalBlockTradingRewardsSchema } from '../../../lib/validation/schemas'; +import { handleValidationErrors } from '../../../request-helpers/error-handler'; +import ExportResponseCodeStats from '../../../request-helpers/export-response-code-stats'; +import { tradingRewardAggregationToResponse } from '../../../request-helpers/request-transformer'; +import { HistoricalTradingRewardAggregationRequest, HistoricalTradingRewardAggregationsResponse } from '../../../types'; + +const router: express.Router = express.Router(); +const controllerName: string = 'height-controller'; + +@Route('historicalTradingRewardAggregations') +class HistoricalTradingRewardAggregationsController extends Controller { + @Get('/:address') + async getAggregations( + @Path() address: string, + @Query() period: TradingRewardAggregationPeriod, + @Query() limit: number, + @Query() startingBeforeOrAt?: IsoString, + @Query() startingBeforeOrAtHeight?: string, + ): Promise { + const tradingRewardAggregations: + TradingRewardAggregationFromDatabase[] = await TradingRewardAggregationTable.findAll({ + address, + period, + limit, + startedAtBeforeOrAt: startingBeforeOrAt, + startedAtHeightBeforeOrAt: startingBeforeOrAtHeight, + }, [], { orderBy: [[TradingRewardAggregationColumns.startedAtHeight, Ordering.DESC]] }); + return { + rewards: _.map( + tradingRewardAggregations, + tradingRewardAggregationToResponse, + ), + }; + } +} + +router.get( + '/:address', + rejectRestrictedCountries, + rateLimiterMiddleware(getReqRateLimiter), + ...CheckHistoricalBlockTradingRewardsSchema, + ...checkSchema({ + period: { + in: 'query', + isString: true, + isIn: { + options: [Object.values(TradingRewardAggregationPeriod)], + }, + errorMessage: `period must be a valid Trading Reward Aggregation Period, one of ${Object.values(TradingRewardAggregationPeriod)}`, + }, + }), + handleValidationErrors, + ExportResponseCodeStats({ controllerName }), + async (req: express.Request, res: express.Response) => { + const start: number = Date.now(); + const { + address, + limit, + period, + startingBeforeOrAt, + startingBeforeOrAtHeight, + }: HistoricalTradingRewardAggregationRequest = matchedData( + req, + ) as HistoricalTradingRewardAggregationRequest; + + try { + const controller: + HistoricalTradingRewardAggregationsController = new + HistoricalTradingRewardAggregationsController(); + const response: + HistoricalTradingRewardAggregationsResponse = await controller.getAggregations( + address, + period, + limit, + startingBeforeOrAt, + startingBeforeOrAtHeight, + ); + + return res.send(response); + } catch (error) { + return handleControllerError( + 'HistoricalTradingRewardAggregationsController GET /', + 'HistoricalTradingRewardAggregations error', + error, + req, + res, + ); + } finally { + stats.timing( + `${config.SERVICE_NAME}.${controllerName}.get_historical_trading_reward_aggregations.timing`, + Date.now() - start, + ); + } + }, +); + +export default router; diff --git a/indexer/services/comlink/src/lib/validation/schemas.ts b/indexer/services/comlink/src/lib/validation/schemas.ts index 76cdfba237..c13a17ed83 100644 --- a/indexer/services/comlink/src/lib/validation/schemas.ts +++ b/indexer/services/comlink/src/lib/validation/schemas.ts @@ -18,6 +18,15 @@ export const CheckSubaccountSchema = checkSchema({ }, }); +export const checkAddressSchemaRecord: Record = { + address: { + in: ['params'], + isString: true, + }, +}; + +export const CheckAddressSchema = checkSchema(checkAddressSchemaRecord); + const limitSchemaRecord: Record = { limit: { in: ['query'], @@ -127,3 +136,21 @@ export const CheckTickerParamSchema = checkSchema({ export const CheckTickerOptionalQuerySchema = checkSchema({ ticker: checkTickerOptionalQuerySchema, }); + +export const CheckHistoricalBlockTradingRewardsSchema = checkSchema({ + ...checkAddressSchemaRecord, + ...limitSchemaRecord, + startingBeforeOrAt: { + in: ['query'], + optional: true, + isISO8601: true, + }, + startingBeforeOrAtHeight: { + in: ['query'], + optional: true, + isInt: { + options: { gt: -1 }, + }, + errorMessage: 'startingBeforeOrAtHeight must be a non-negative integer', + }, +}); diff --git a/indexer/services/comlink/src/request-helpers/request-transformer.ts b/indexer/services/comlink/src/request-helpers/request-transformer.ts index 521987fcdf..bb920ef233 100644 --- a/indexer/services/comlink/src/request-helpers/request-transformer.ts +++ b/indexer/services/comlink/src/request-helpers/request-transformer.ts @@ -22,6 +22,8 @@ import { SubaccountFromDatabase, SubaccountTable, TimeInForce, + TradingRewardAggregationFromDatabase, + TradingRewardFromDatabase, TransferFromDatabase, } from '@dydxprotocol-indexer/postgres'; import { OrderbookLevels, PriceLevel } from '@dydxprotocol-indexer/redis'; @@ -35,7 +37,9 @@ import { AssetPositionsMap, CandleResponseObject, FillResponseObject, + HistoricalBlockTradingReward, HistoricalFundingResponseObject, + HistoricalTradingRewardAggregation, MarketAndTypeByClobPairId, OrderbookResponseObject, OrderbookResponsePriceLevel, @@ -450,3 +454,26 @@ export function candlesToSparklineResponseObject( }, response, ); } + +export function tradingRewardAggregationToResponse( + aggregation: TradingRewardAggregationFromDatabase, +): HistoricalTradingRewardAggregation { + return { + tradingReward: aggregation.amount, + startedAt: aggregation.startedAt, + startedAtHeight: aggregation.startedAtHeight, + endedAt: aggregation.endedAt, + endedAtHeight: aggregation.endedAtHeight, + period: aggregation.period, + }; +} + +export function tradingRewardToResponse( + tradingReward: TradingRewardFromDatabase, +): HistoricalBlockTradingReward { + return { + tradingReward: tradingReward.amount, + createdAt: tradingReward.blockTime, + createdAtHeight: tradingReward.blockHeight, + }; +} diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts index 81a0d894c4..6fcaa098d1 100644 --- a/indexer/services/comlink/src/types.ts +++ b/indexer/services/comlink/src/types.ts @@ -18,6 +18,7 @@ import { PositionSide, SubaccountFromDatabase, TradeType, + TradingRewardAggregationPeriod, TransferType, } from '@dydxprotocol-indexer/postgres'; import { RedisOrder } from '@dydxprotocol-indexer/v4-protos'; @@ -402,3 +403,46 @@ export enum BlockedCode { GEOBLOCKED = 'GEOBLOCKED', COMPLIANCE_BLOCKED = 'COMPLIANCE_BLOCKED', } + +/* ------- HISTORICAL TRADING REWARD TYPES ------- */ + +export interface HistoricalTradingRewardAggregationRequest extends AddressRequest, LimitRequest { + period: TradingRewardAggregationPeriod, + startingBeforeOrAt: IsoString, + startingBeforeOrAtHeight: string, +} + +export interface HistoricalTradingRewardAggregationsResponse { + // Indexer will not fill in empty periods, if there is no data after this period, + // Indexer will return an empty list. Will return in descending order, the most + // recent at the start + rewards: HistoricalTradingRewardAggregation[], +} + +export interface HistoricalTradingRewardAggregation { + tradingReward: string, // i.e. '100.1' for 100.1 token earned through trading rewards + startedAt: IsoString, // Start of the aggregation period, inclusive + startedAtHeight: string, // first block included in the aggregation, inclusive + endedAt?: IsoString, // End of the aggregation period, exclusive + endedAtHeight?: string, // last block included in the aggregation, inclusive + period: TradingRewardAggregationPeriod, +} + +/* ------- HISTORICAL BLOCK TRADING REWARD TYPES ------- */ +export interface HistoricalBlockTradingRewardRequest extends AddressRequest, LimitRequest { + startingBeforeOrAt: IsoString, + startingBeforeOrAtHeight: string, +} + +export interface HistoricalBlockTradingRewardsResponse { + // Indexer will not fill in empty periods, if there is no data after this period, + // Indexer will return an empty list. Will return in descending order, the most + // recent at the start + rewards: HistoricalBlockTradingReward[], +} + +export interface HistoricalBlockTradingReward { + tradingReward: string, // i.e. '100.1' for 100.1 token earned through trading rewards + createdAt: IsoString, + createdAtHeight: string, +}