import alot from 'alot'; import memd from 'memd'; import { Web3Client } from '@dequanto/clients/Web3Client'; import { TEth } from '@dequanto/models/TEth'; import { $block } from '@dequanto/utils/$block'; import { $date } from '@dequanto/utils/$date'; export class BlockDateResolver { private AVG_INITIAL = { eth: 12_000, bsc: 3_000, polygon: 3_000, }; private _known = [] as IKnownBlock[] private _requestedDate: Date; constructor(public client: Web3Client) { } async getDate (blockNumber: number): Promise { let block = await this.getBlock(blockNumber); return new Date(block.timestamp * 1000); } async getBlockNumberFor (date: Date): Promise { this._requestedDate = date; let avg = this.AVG_INITIAL[this.client.platform] ?? this.client.blockTimeAvg ?? this.AVG_INITIAL['eth']; let now = new Date(); let topBlock = { blockNumber: await this.client.getBlockNumberCached(), date: now, avg, }; this._known.push(topBlock); const result = await this.moveNext(date, { closestTime: null, blockNumber: null, date: null }); return result.blockNumber; } async getBlockInfoFor (date: Date): Promise<{ block: number, timestamp: number }> { this._requestedDate = date; let avg = this.AVG_INITIAL[this.client.platform] ?? this.client.blockTimeAvg ?? this.AVG_INITIAL['eth']; let now = new Date(); let topBlock = { blockNumber: await this.client.getBlockNumberCached(), date: now, avg, }; this._known.push(topBlock); const result = await this.moveNext(date, { closestTime: null, blockNumber: null, date: null, }); return { block: result.blockNumber, timestamp: result.date ? $date.toUnixTimestamp(result.date) : null, }; } private async moveNext (date: Date, ctx: { closestTime: number; blockNumber: number; date: Date; }): Promise { let closestIndex = this.getClosest(date); let block = this._known[closestIndex]; let timeDiff = this.diffTime(block.date, date); let timeDistance = Math.abs(timeDiff); const BLOCKS_TOLERANCE = 2; if (timeDistance <= block.avg * BLOCKS_TOLERANCE) { return block; } if (ctx.closestTime != null && timeDistance >= ctx.closestTime) { return ctx; } ctx.closestTime = timeDistance; ctx.blockNumber = block.blockNumber; ctx.date = block.date; let nextInfo = await this.checkPoint(block, timeDiff); if (nextInfo == null) { return block; } return this.moveNext(date, ctx); } /** * Returns index of the first known block, which is most near to specified block (it can be before or after the specified date). */ private getClosest (date: Date) { let entry = alot(this._known).map(x => [ this.diffTimeAbs(x.date, date), x ] as const).minItem(x => x[0])[1]; let i = this._known.indexOf(entry); return i; } private async checkPoint (anchor: IKnownBlock, diffTime: number) { let diffCount = Math.round(diffTime / anchor.avg); if (diffCount === 0) { return null; } let blockNumber = anchor.blockNumber + diffCount; if (blockNumber < 0) { throw new Error(`Date Out of range: ${ this._requestedDate.toISOString() }. Based on the AVG block time, the blockchain was not active on that date`); } let date = await this.getBlockDate(blockNumber); let info = { blockNumber: blockNumber, date: date, }; this.push(info); this.refineAvg(); return info; } /** Add a know block to set */ private push(info: IKnownBlock) { for (let i = 0; i < this._known.length; i++) { let x = this._known[i]; if (info.date < x.date) { this._known.splice(i, 0, info); return; } } this._known.push(info); } /** Loads the block and gets the Date of the block */ private async getBlockDate (blockNumber: number) { let block = await this.getBlock(blockNumber); if (block == null) { throw new Error(`Block not loaded: ${blockNumber}`); } let date = $block.getDate(block); return date; } /** Returns SIGNED time in milliseconds between two dates. Negative values when t2 < t1 */ private diffTime (t1: Date, t2: Date) { return (t2.getTime() - t1.getTime()); } /** Returns ABSOLUTE time in milliseconds between two dates. */ private diffTimeAbs (t1: Date, t2: Date) { return Math.abs(this.diffTime(t1, t2)); } /** Returns AVG block count between two dates */ private getAvgBlockCountBetween (b1: IKnownBlock, b2: IKnownBlock) { let diff = this.diffTimeAbs(b1.date, b2.date); return Math.round(diff / Math.abs(b2.blockNumber - b1.blockNumber)); } /** With N>1 blocks we can better find out the AVG block time */ private refineAvg () { for (let i = 1; i < this._known.length; i++) { let info = this._known[i]; let prev = this._known[i - 1]; info.avg = this.getAvgBlockCountBetween(prev, info); if (i === 1) { this._known[0].avg = info.avg; } } } @memd.deco.memoize() private getBlock (blockNumber: number): Promise { return this.client.getBlock(blockNumber); } } interface IKnownBlock { blockNumber: number; date: Date avg?: number }