From 56608e9dc74bafdb3ecffef8c4682a1497119466 Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Fri, 5 Jan 2024 15:15:01 -0500 Subject: [PATCH] respond to wills comments --- .../trading-reward-aggregation-model.ts | 2 +- ...te-trading-rewards-processed-cache.test.ts | 2 +- ...gregate-trading-rewards-processed-cache.ts | 5 + .../tasks/aggregate-trading-rewards.test.ts | 128 +++++++++++------- indexer/services/roundtable/src/config.ts | 13 +- indexer/services/roundtable/src/index.ts | 6 +- .../src/tasks/aggregate-trading-rewards.ts | 127 ++++++++--------- 7 files changed, 164 insertions(+), 119 deletions(-) diff --git a/indexer/packages/postgres/src/models/trading-reward-aggregation-model.ts b/indexer/packages/postgres/src/models/trading-reward-aggregation-model.ts index fcca5ce6c82..cffdf8a74ea 100644 --- a/indexer/packages/postgres/src/models/trading-reward-aggregation-model.ts +++ b/indexer/packages/postgres/src/models/trading-reward-aggregation-model.ts @@ -51,7 +51,7 @@ export default class TradingRewardAggregationModel extends Model { address: { type: 'string' }, startedAt: { type: 'string', format: 'date-time' }, // Inclusive startedAtHeight: { type: 'string', pattern: IntegerPattern }, // Inclusive - endedAt: { type: ['string', 'null'], format: 'date-time' }, // Inclusive + endedAt: { type: ['string', 'null'], format: 'date-time' }, // Exclusive endedAtHeight: { type: ['string', 'null'], pattern: IntegerPattern }, // Inclusive period: { type: 'string', enum: [...Object.values(TradingRewardAggregationPeriod)] }, amount: { type: 'string', pattern: NonNegativeNumericPattern }, diff --git a/indexer/packages/redis/__tests__/caches/aggregate-trading-rewards-processed-cache.test.ts b/indexer/packages/redis/__tests__/caches/aggregate-trading-rewards-processed-cache.test.ts index c9048e1a0a5..29f0602cd21 100644 --- a/indexer/packages/redis/__tests__/caches/aggregate-trading-rewards-processed-cache.test.ts +++ b/indexer/packages/redis/__tests__/caches/aggregate-trading-rewards-processed-cache.test.ts @@ -6,7 +6,7 @@ import { } from '../../src/caches/aggregate-trading-rewards-processed-cache'; import { IsoString, TradingRewardAggregationPeriod } from '@dydxprotocol-indexer/postgres'; -describe('cancelledOrdersCache', () => { +describe('aggregateTradingRewardsProcessedCache', () => { beforeEach(async () => { await deleteAllAsync(client); }); diff --git a/indexer/packages/redis/src/caches/aggregate-trading-rewards-processed-cache.ts b/indexer/packages/redis/src/caches/aggregate-trading-rewards-processed-cache.ts index 10e5c6cdcf6..994789a194d 100644 --- a/indexer/packages/redis/src/caches/aggregate-trading-rewards-processed-cache.ts +++ b/indexer/packages/redis/src/caches/aggregate-trading-rewards-processed-cache.ts @@ -3,6 +3,11 @@ import { RedisClient } from 'redis'; import { getAsync } from '../helpers/redis'; +/** + * Cache key for the aggregate trading rewards processed cache. Given a + * TradingRewardAggregationPeriod, this cache stores the timestamp of the + * trading rewards that have been processed up to and excluding that timestamp. + */ export const AGGREGATE_TRADING_REWARDS_PROCESSED_CACHE_KEY: string = 'v4/aggregate_trading_rewards_processed/'; function getKey(period: TradingRewardAggregationPeriod): string { diff --git a/indexer/services/roundtable/__tests__/tasks/aggregate-trading-rewards.test.ts b/indexer/services/roundtable/__tests__/tasks/aggregate-trading-rewards.test.ts index 146c9bf43e6..88325a30f3b 100644 --- a/indexer/services/roundtable/__tests__/tasks/aggregate-trading-rewards.test.ts +++ b/indexer/services/roundtable/__tests__/tasks/aggregate-trading-rewards.test.ts @@ -13,6 +13,7 @@ import { UTC_OPTIONS } from '../../src/lib/constants'; import { deleteAllAsync } from '@dydxprotocol-indexer/redis/build/src/helpers/redis'; import { redisClient } from '../../src/helpers/redis'; import { AggregateTradingRewardsProcessedCache } from '@dydxprotocol-indexer/redis'; +import config from '../../src/config'; describe('aggregate-trading-rewards', () => { beforeAll(async () => { @@ -56,33 +57,76 @@ describe('aggregate-trading-rewards', () => { amount: '10', }; + describe('maybeDeleteIncompleteAggregatedTradingReward', () => { + it( + 'Deletes incomplete aggregations when cache is empty and only one incomplete aggregations exist', + async () => { + await Promise.all([ + TradingRewardAggregationTable.create(defaultMonthlyTradingRewardAggregation2), + TradingRewardAggregationTable.create({ + ...defaultMonthlyTradingRewardAggregation2, + period: TradingRewardAggregationPeriod.WEEKLY, + }), + ]); + const aggregateTradingReward: AggregateTradingReward = new AggregateTradingReward( + TradingRewardAggregationPeriod.MONTHLY, + ); + await aggregateTradingReward.maybeDeleteIncompleteAggregatedTradingReward(); + const aggregations: + TradingRewardAggregationFromDatabase[] = await TradingRewardAggregationTable.findAll( + {}, + [], + ); + expect(aggregations.length).toEqual(1); + }, + ); + + it( + 'Deletes incomplete aggregations when cache is empty and multiple aggregations exist', + async () => { + await Promise.all([ + TradingRewardAggregationTable.create(defaultMonthlyTradingRewardAggregation), + TradingRewardAggregationTable.create(defaultMonthlyTradingRewardAggregation2), + TradingRewardAggregationTable.create({ + ...defaultMonthlyTradingRewardAggregation2, + period: TradingRewardAggregationPeriod.WEEKLY, + }), + createBlockWithTime(startedAt2.plus({ hours: 1 })), + ]); + const aggregateTradingReward: AggregateTradingReward = new AggregateTradingReward( + TradingRewardAggregationPeriod.MONTHLY, + ); + await aggregateTradingReward.maybeDeleteIncompleteAggregatedTradingReward(); + + const aggregations: + TradingRewardAggregationFromDatabase[] = await TradingRewardAggregationTable.findAll( + {}, + [], + ); + expect(aggregations.length).toEqual(2); + }, + ); + }); + describe('getTradingRewardDataToProcessInterval', () => { it.each([ TradingRewardAggregationPeriod.DAILY, TradingRewardAggregationPeriod.WEEKLY, TradingRewardAggregationPeriod.MONTHLY, - ])('Successfully returns undefined if there are no blocks in the database', async ( + ])('Throws error if there are no blocks in the database', async ( period: TradingRewardAggregationPeriod, ) => { await dbHelpers.clearData(); const aggregateTradingReward: AggregateTradingReward = new AggregateTradingReward(period); - const interval: - Interval | undefined = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); - - expect(interval).toBeUndefined(); - expect(logger.info).toHaveBeenCalledWith( - expect.objectContaining({ - message: - 'Unable to aggregate trading rewards because there are no blocks in the database.', - }), - ); + await expect(aggregateTradingReward.getTradingRewardDataToProcessInterval()) + .rejects.toEqual(new Error('Unable to find latest block')); }); it.each([ TradingRewardAggregationPeriod.DAILY, TradingRewardAggregationPeriod.WEEKLY, TradingRewardAggregationPeriod.MONTHLY, - ])('Successfully returns first block time if cache is empty and no aggregations', async ( + ])('Successfully returns interval if cache is empty and no aggregations', async ( period: TradingRewardAggregationPeriod, ) => { const firstBlockTime: DateTime = DateTime.fromISO( @@ -92,7 +136,7 @@ describe('aggregate-trading-rewards', () => { await createBlockWithTime(firstBlockTime.plus({ hours: 1 })); const aggregateTradingReward: AggregateTradingReward = new AggregateTradingReward(period); const interval: - Interval | undefined = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); + Interval = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); expect(interval).not.toBeUndefined(); expect(interval).toEqual(Interval.fromDateTimes( @@ -102,20 +146,17 @@ describe('aggregate-trading-rewards', () => { }); it( - 'Deletes incomplete aggregations when cache is empty and only one incomplete aggregations exist', + 'Successfully returns interval when cache is empty and no', async () => { - await Promise.all([ - TradingRewardAggregationTable.create(defaultMonthlyTradingRewardAggregation2), - TradingRewardAggregationTable.create({ - ...defaultMonthlyTradingRewardAggregation2, - period: TradingRewardAggregationPeriod.WEEKLY, - }), - ]); + await TradingRewardAggregationTable.create({ + ...defaultMonthlyTradingRewardAggregation, + period: TradingRewardAggregationPeriod.WEEKLY, + }); const aggregateTradingReward: AggregateTradingReward = new AggregateTradingReward( TradingRewardAggregationPeriod.MONTHLY, ); const interval: - Interval | undefined = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); + Interval = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); const firstBlockTime: DateTime = DateTime.fromISO( testConstants.defaultBlock.time, @@ -123,24 +164,18 @@ describe('aggregate-trading-rewards', () => { ).toUTC(); expect(interval).toEqual(Interval.fromDateTimes( firstBlockTime, - firstBlockTime.plus({ hours: 1 })), - ); - - const aggregations: - TradingRewardAggregationFromDatabase[] = await TradingRewardAggregationTable.findAll( - {}, - [], + firstBlockTime.plus({ + milliseconds: config.AGGREGATE_TRADING_REWARDS_MAX_INTERVAL_SIZE_MS, + })), ); - expect(aggregations.length).toEqual(1); }, ); it( - 'Deletes incomplete aggregations when cache is empty and multiple aggregations exist', + 'Successfully returns interval when cache is empty and a complete aggregations exist', async () => { await Promise.all([ TradingRewardAggregationTable.create(defaultMonthlyTradingRewardAggregation), - TradingRewardAggregationTable.create(defaultMonthlyTradingRewardAggregation2), TradingRewardAggregationTable.create({ ...defaultMonthlyTradingRewardAggregation2, period: TradingRewardAggregationPeriod.WEEKLY, @@ -151,9 +186,12 @@ describe('aggregate-trading-rewards', () => { TradingRewardAggregationPeriod.MONTHLY, ); const interval: - Interval | undefined = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); + Interval = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); expect(interval).not.toBeUndefined(); - expect(interval).toEqual(Interval.fromDateTimes(startedAt2, startedAt2.plus({ hours: 1 }))); + expect(interval).toEqual(Interval.fromDateTimes( + startedAt2, + startedAt2.plus({ milliseconds: config.AGGREGATE_TRADING_REWARDS_MAX_INTERVAL_SIZE_MS }), + )); const aggregations: TradingRewardAggregationFromDatabase[] = await TradingRewardAggregationTable.findAll( @@ -185,7 +223,7 @@ describe('aggregate-trading-rewards', () => { TradingRewardAggregationPeriod.MONTHLY, ); const interval: - Interval | undefined = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); + Interval = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); expect(interval).toEqual(Interval.fromDateTimes(endedAt2, endedAt2)); const aggregations: @@ -218,7 +256,7 @@ describe('aggregate-trading-rewards', () => { TradingRewardAggregationPeriod.MONTHLY, ); const interval: - Interval | undefined = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); + Interval = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); expect(interval).toEqual(Interval.fromDateTimes(endedAt2, endedAt2.plus({ minutes: 1 }))); const aggregations: @@ -251,8 +289,11 @@ describe('aggregate-trading-rewards', () => { TradingRewardAggregationPeriod.MONTHLY, ); const interval: - Interval | undefined = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); - expect(interval).toEqual(Interval.fromDateTimes(endedAt2, endedAt2.plus({ hour: 1 }))); + Interval = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); + expect(interval).toEqual(Interval.fromDateTimes( + endedAt2, + endedAt2.plus({ milliseconds: config.AGGREGATE_TRADING_REWARDS_MAX_INTERVAL_SIZE_MS }), + )); const aggregations: TradingRewardAggregationFromDatabase[] = await TradingRewardAggregationTable.findAll( @@ -284,7 +325,7 @@ describe('aggregate-trading-rewards', () => { TradingRewardAggregationPeriod.MONTHLY, ); const interval: - Interval | undefined = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); + Interval = await aggregateTradingReward.getTradingRewardDataToProcessInterval(); expect(interval).toEqual(Interval.fromDateTimes( endedAt2.plus({ hours: 23, minutes: 55 }), endedAt2.plus({ days: 1 })), @@ -302,13 +343,8 @@ describe('aggregate-trading-rewards', () => { describe('runTask', () => { it('Successfully logs and exits if there are no blocks in the database', async () => { await dbHelpers.clearData(); - await generateTaskFromPeriod(TradingRewardAggregationPeriod.MONTHLY)(); - - expect(logger.info).toHaveBeenCalledWith( - expect.objectContaining({ - message: 'No interval to aggregate trading rewards', - }), - ); + await expect(generateTaskFromPeriod(TradingRewardAggregationPeriod.MONTHLY)()) + .rejects.toEqual(new Error('Unable to find latest block')); }); }); }); diff --git a/indexer/services/roundtable/src/config.ts b/indexer/services/roundtable/src/config.ts index efa36367541..49a3fab9698 100644 --- a/indexer/services/roundtable/src/config.ts +++ b/indexer/services/roundtable/src/config.ts @@ -77,13 +77,7 @@ export const configSchema = { LOOPS_INTERVAL_MS_REMOVE_OLD_ORDER_UPDATES: parseInteger({ default: THIRTY_SECONDS_IN_MILLISECONDS, }), - LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS_DAILY: parseInteger({ - default: THIRTY_SECONDS_IN_MILLISECONDS, - }), - LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS_WEEKLY: parseInteger({ - default: THIRTY_SECONDS_IN_MILLISECONDS, - }), - LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS_MONTHLY: parseInteger({ + LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS: parseInteger({ default: THIRTY_SECONDS_IN_MILLISECONDS, }), @@ -138,6 +132,11 @@ export const configSchema = { // Remove old cached order updates OLD_CACHED_ORDER_UPDATES_WINDOW_MS: parseInteger({ default: 30 * ONE_SECOND_IN_MILLISECONDS }), + + // Aggregate Trading Rewards + AGGREGATE_TRADING_REWARDS_MAX_INTERVAL_SIZE_MS: parseInteger({ + default: ONE_HOUR_IN_MILLISECONDS, + }), }; export default parseSchema(configSchema); diff --git a/indexer/services/roundtable/src/index.ts b/indexer/services/roundtable/src/index.ts index 51fff890d24..c27dc1bcc71 100644 --- a/indexer/services/roundtable/src/index.ts +++ b/indexer/services/roundtable/src/index.ts @@ -127,7 +127,7 @@ async function start(): Promise { startLoop( aggregateTradingRewardsTasks(TradingRewardAggregationPeriod.DAILY), 'aggregate_trading_rewards_daily', - config.LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS_DAILY, + config.LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS, ); } @@ -135,7 +135,7 @@ async function start(): Promise { startLoop( aggregateTradingRewardsTasks(TradingRewardAggregationPeriod.WEEKLY), 'aggregate_trading_rewards_weekly', - config.LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS_WEEKLY, + config.LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS, ); } @@ -143,7 +143,7 @@ async function start(): Promise { startLoop( aggregateTradingRewardsTasks(TradingRewardAggregationPeriod.MONTHLY), 'aggregate_trading_rewards_monthly', - config.LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS_MONTHLY, + config.LOOPS_INTERVAL_MS_AGGREGATE_TRADING_REWARDS, ); } diff --git a/indexer/services/roundtable/src/tasks/aggregate-trading-rewards.ts b/indexer/services/roundtable/src/tasks/aggregate-trading-rewards.ts index 4c1f8a3b99d..4f13d5d821a 100644 --- a/indexer/services/roundtable/src/tasks/aggregate-trading-rewards.ts +++ b/indexer/services/roundtable/src/tasks/aggregate-trading-rewards.ts @@ -16,6 +16,7 @@ import { import { AggregateTradingRewardsProcessedCache } from '@dydxprotocol-indexer/redis'; import { DateTime, Interval } from 'luxon'; +import config from '../config'; import { redisClient } from '../helpers/redis'; import { UTC_OPTIONS } from '../lib/constants'; @@ -46,14 +47,8 @@ export class AggregateTradingReward { } async runTask(): Promise { - const interval: Interval | undefined = await this.getTradingRewardDataToProcessInterval(); - if (interval === undefined) { - logger.info({ - at: 'aggregate-trading-rewards#runTask', - message: 'No interval to aggregate trading rewards', - }); - return; - } + await this.maybeDeleteIncompleteAggregatedTradingReward(); + const interval: Interval = await this.getTradingRewardDataToProcessInterval(); logger.info({ at: 'aggregate-trading-rewards#runTask', message: 'Generated interval to aggregate trading rewards', @@ -74,38 +69,75 @@ export class AggregateTradingReward { ); } + /** + * If the latest processed time is null (should only happen during a fast sync), + * and the latest period of aggregated trading rewards is incomplete. + */ + async maybeDeleteIncompleteAggregatedTradingReward(): Promise { + const processedTime: + IsoString | null = await AggregateTradingRewardsProcessedCache.getProcessedTime( + this.period, + redisClient, + ); + if (processedTime !== null) { + return; + } + const latestAggregation: + TradingRewardAggregationFromDatabase | undefined = await + TradingRewardAggregationTable.getLatestAggregatedTradeReward(this.period); + + // endedAt is only set when the entire interval has been processed for an aggregation + if (latestAggregation !== undefined && latestAggregation.endedAt === null) { + await this.deleteIncompleteAggregatedTradingReward(latestAggregation); + } + } + + /** + * Deletes the latest this.period of aggregated trading rewards if it is incomplete. This is + * called when the processedTime is null, and the latest aggregated trading rewards is incomplete. + * We delete the latest this.period of aggregated trading rewards data because we don't know how + * much data was processed within the interval, and we don't want to double count rewards. + */ + private async deleteIncompleteAggregatedTradingReward( + latestAggregation: TradingRewardAggregationFromDatabase, + ): Promise { + logger.info({ + at: 'aggregate-trading-rewards#deleteIncompleteAggregatedTradingReward', + message: `Deleting the latest ${this.period} aggregated trading rewards.`, + }); + await TradingRewardAggregationTable.deleteAll({ + period: this.period, + startedAtHeightOrAfter: latestAggregation.startedAtHeight, + }); + logger.info({ + at: 'aggregate-trading-rewards#deleteIncompleteAggregatedTradingReward', + message: `Deleted the last ${this.period} aggregated trading rewards`, + height: latestAggregation.startedAtHeight, + time: latestAggregation.startedAt, + }); + } + /** * Gets the interval of time to aggregate trading rewards for. - * If there are no blocks in the database, then do not process any data. + * If there are no blocks in the database, then throw an error. * If There is no processedTime in the cache, then delete the latest month of data, * and reprocess that data or start from block 1. * If the processedTime is not null, and blocks exist in the database, then process up to the - * next hour of data. + * next config.AGGREGATE_TRADING_REWARDS_MAX_INTERVAL_SIZE_MS of data. */ - async getTradingRewardDataToProcessInterval(): Promise { - let latestBlock: BlockFromDatabase; - try { - latestBlock = await BlockTable.getLatest(); - } catch (e) { - logger.info({ - at: 'aggregate-trading-rewards#getTradingRewardDataToProcessInterval', - message: 'Unable to aggregate trading rewards because there are no blocks in the database.', - }); - return; - } - + async getTradingRewardDataToProcessInterval(): Promise { const processedTime: IsoString | null = await AggregateTradingRewardsProcessedCache.getProcessedTime( this.period, redisClient, ); + const latestBlock: BlockFromDatabase = await BlockTable.getLatest(); if (processedTime === null) { - await this.deleteIncompleteAggregatedTradingReward(); logger.info({ at: 'aggregate-trading-rewards#getTradingRewardDataToProcessInterval', message: 'Resetting AggregateTradingRewardsProcessedCache', }); - const nextStartTime: DateTime = await this.getNextIntervalStart(); + const nextStartTime: DateTime = await this.getNextIntervalStartWhenCacheEmpty(); await AggregateTradingRewardsProcessedCache.setProcessedTime( this.period, nextStartTime.toISO(), @@ -120,41 +152,12 @@ export class AggregateTradingReward { } /** - * Deletes the latest this.period of aggregated trading rewards if it is incomplete. This is - * called when the processedTime is null, and we need to reprocess the latest month of data. - * We delete the latest this.period of aggregated trading rewards data because we don't know if - * the data is complete. - */ - private async deleteIncompleteAggregatedTradingReward(): Promise { - logger.info({ - at: 'aggregate-trading-rewards#deleteIncompleteAggregatedTradingReward', - message: `Deleting the latest ${this.period} aggregated trading rewards.`, - }); - - const latestAggregation: - TradingRewardAggregationFromDatabase | undefined = await - TradingRewardAggregationTable.getLatestAggregatedTradeReward(this.period); - - if (latestAggregation !== undefined && latestAggregation.endedAt === null) { - await TradingRewardAggregationTable.deleteAll({ - period: this.period, - startedAtHeightOrAfter: latestAggregation.startedAtHeight, - }); - logger.info({ - at: 'aggregate-trading-rewards#deleteIncompleteAggregatedTradingReward', - message: `Deleted the last ${this.period} aggregated trading rewards`, - height: latestAggregation.startedAtHeight, - time: latestAggregation.startedAt, - }); - } - } - - /** - * The start time of the next interval to process. This will be the start time of the - * first block in the database, or the start time of the next month after the latest - * monthly aggregation. + * Returns the start time of the next interval to process if the + * AggregateTradingRewardProcessedCache is empty. If there is a most recent complete aggregation + * for this period, returns the end time of the most recent aggregation, otherwise returns the + * start time of the first block in the database. */ - private async getNextIntervalStart(): Promise { + private async getNextIntervalStartWhenCacheEmpty(): Promise { const latestAggregation: TradingRewardAggregationFromDatabase | undefined = await TradingRewardAggregationTable.getLatestAggregatedTradeReward(this.period); @@ -176,7 +179,7 @@ export class AggregateTradingReward { * Generate the interval that will be processed. The end time of the interval is calculated from * a start time and the latest block. This will be the earliest of the following: * 1. The next day - * 2. An hour after start time + * 2. Start time plus config.AGGREGATE_TRADING_REWARDS_MAX_INTERVAL_SIZE_MS * 3. The start of the minute of the latest block * @param startTime - startTime of the interval * @param latestBlock @@ -198,11 +201,13 @@ export class AggregateTradingReward { const normalizedNextDay: Date = floorDate(nextDay, ONE_DAY_IN_MILLISECONDS); const startDate: Date = startTime.toJSDate(); - const oneHourAfterStart: Date = DateTime.fromJSDate(startDate).plus({ hour: 1 }).toJSDate(); + const startTimePlusMaxIntervalSize: Date = DateTime.fromJSDate(startDate).plus( + { milliseconds: config.AGGREGATE_TRADING_REWARDS_MAX_INTERVAL_SIZE_MS }, + ).toJSDate(); const endTime: Date = new Date(Math.min( normalizedLatestBlockTime.getTime(), normalizedNextDay.getTime(), - oneHourAfterStart.getTime(), + startTimePlusMaxIntervalSize.getTime(), )); const endDateTime: DateTime = DateTime.fromJSDate(endTime).toUTC(); return Interval.fromDateTimes(startTime, endDateTime);