@defillama/sdk 5.0.236 → 5.0.238

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -270,6 +270,8 @@ const httpRPC = {
270
270
  });
271
271
  if (error)
272
272
  throw error;
273
+ if (!result)
274
+ throw new Error(`block ${params[0]} not found`); // pruned or not yet synced, let callers recognise it
273
275
  result.number = parseInt(result.number);
274
276
  result.timestamp = parseInt(result.timestamp);
275
277
  return result;
@@ -130,6 +130,116 @@ const intialBlocks = {
130
130
  terra: 4724001,
131
131
  crab: 4969901
132
132
  };
133
+ // pruned nodes don't serve old blocks, we find the earliest available block per chain and remember it for this process
134
+ const earliestAvailableBlock = {};
135
+ const prunedBlockBuffer = 1000; // pruning keeps moving forward, so stay a bit ahead of the reported height
136
+ // known formats:
137
+ // sei: "requested height 150022713 has been pruned; earliest available is 229666772"
138
+ // sei (pre evm): "EVM is only supported from block 79123881 onwards"
139
+ // publicnode: "pruned history unavailable: requested 100, earliest available 51000000"
140
+ // cosmos/tendermint/evm: "height 100 is not available, lowest height is 47180386" (often in error.data)
141
+ // both patterns need a block/height/available word next to the number, so unrelated numbers in error pages don't match
142
+ const earliestBlockPatterns = [
143
+ /(?:earliest|lowest|oldest)\s+(?:available\s+(?:block\s+|height\s+)?|(?:block|height)\s+)(?:number\s+)?(?:is\s+|:\s*|=\s*)?#?(\d+)/i,
144
+ /(?:supported|available)\s+(?:from|since|after)\s+(?:block|height)\s+#?(\d+)/i,
145
+ ];
146
+ // the node's message sits in different fields depending on who wrapped the error (ethers, axios, formError, llamaRPCError)
147
+ const errorMessageFields = ['message', 'shortMessage', 'reason', 'data', '_underlyingError', 'error', 'info', 'response', 'errors'];
148
+ function getErrorText(e, depth = 0) {
149
+ if (typeof e === 'string')
150
+ return e;
151
+ if (!e || typeof e !== 'object' || depth > 5)
152
+ return ''; // llamaRPCError: errors[i].error.message sits 4 deep
153
+ if (Array.isArray(e))
154
+ return e.map(i => getErrorText(i, depth + 1)).filter(Boolean).join(' ');
155
+ return errorMessageFields
156
+ .map(key => getErrorText(e[key], depth + 1))
157
+ .filter(Boolean)
158
+ .join(' ');
159
+ }
160
+ function getEarliestAvailableFromError(e, requested) {
161
+ const text = getErrorText(e);
162
+ const found = earliestBlockPatterns
163
+ .map(pattern => { var _a; return (_a = text.match(pattern)) === null || _a === void 0 ? void 0 : _a[1]; })
164
+ .filter(Boolean)
165
+ .map(Number)
166
+ .filter(block => block > requested);
167
+ return found.length ? Math.max(...found) : undefined;
168
+ }
169
+ // true only when the node says the block doesn't exist for it, transient errors (timeouts, 429s) are not pruning
170
+ function isMissingBlockError(e) {
171
+ const text = getErrorText(e);
172
+ return /Can't get block of chain|not found|not available|pruned|missing trie node|does not have this data|unknown block|no block/i.test(text);
173
+ }
174
+ // last resort for nodes that return null/not found without telling where the history starts
175
+ async function searchEarliestAvailableBlock(low, chain) {
176
+ const { number: latest } = await (0, exports.getLatestBlock)(chain);
177
+ let hi = latest - prunedBlockBuffer;
178
+ if (hi <= low || !(await isBlockAvailable(hi)))
179
+ return; // node is broken, not pruned
180
+ while (hi - low > prunedBlockBuffer) {
181
+ const mid = Math.floor((low + hi) / 2);
182
+ if (await isBlockAvailable(mid))
183
+ hi = mid;
184
+ else
185
+ low = mid;
186
+ }
187
+ return hi;
188
+ // we only get here after the node confirmed pruning, so a failure in the pruned region is most likely pruning too
189
+ // (sei answers "method handler crashed" for blocks just above its reported horizon); retry once to rule out a blip
190
+ async function isBlockAvailable(block, retry = true) {
191
+ try {
192
+ await fetchBlockFromProvider(block, chain);
193
+ return true;
194
+ }
195
+ catch (e) {
196
+ const earliest = getEarliestAvailableFromError(e, block);
197
+ if (earliest)
198
+ low = Math.max(low, earliest - 1); // node told us, skip ahead
199
+ else if (!isMissingBlockError(e) && retry)
200
+ return isBlockAvailable(block, false);
201
+ return false;
202
+ }
203
+ }
204
+ }
205
+ // fetches the given block, or the earliest available block if the node has pruned it
206
+ async function fetchAvailableBlock(low, chain) {
207
+ var _a, _b;
208
+ low = Math.max(low, (_a = earliestAvailableBlock[chain]) !== null && _a !== void 0 ? _a : 0); // already known to be pruned, don't ask again
209
+ let originalError;
210
+ // a few hops: sei first reports the evm start block, then the pruned height
211
+ for (let i = 0; i < 3; i++) {
212
+ try {
213
+ return await fetchBlockFromProvider(low, chain);
214
+ }
215
+ catch (e) {
216
+ originalError = originalError !== null && originalError !== void 0 ? originalError : e;
217
+ const earliest = getEarliestAvailableFromError(e, low);
218
+ if (!earliest)
219
+ break;
220
+ low = earliestAvailableBlock[chain] = earliest + prunedBlockBuffer;
221
+ (0, debugLog_1.debugLog)(`chain: ${chain} block is pruned, using earliest available block ${low}`);
222
+ }
223
+ }
224
+ // only search when the node says the block is missing, anything else is a transient failure
225
+ if (!isMissingBlockError(originalError))
226
+ throw originalError;
227
+ // parallel probes can all land in the pruned range, run a single search per chain
228
+ if (!earliestBlockSearches[chain]) {
229
+ earliestBlockSearches[chain] = searchEarliestAvailableBlock(low, chain)
230
+ .catch(() => undefined)
231
+ .finally(() => delete earliestBlockSearches[chain]);
232
+ }
233
+ const earliest = await earliestBlockSearches[chain];
234
+ if (!earliest)
235
+ throw originalError;
236
+ if (((_b = earliestAvailableBlock[chain]) !== null && _b !== void 0 ? _b : 0) < earliest + prunedBlockBuffer) {
237
+ earliestAvailableBlock[chain] = earliest + prunedBlockBuffer;
238
+ (0, debugLog_1.debugLog)(`chain: ${chain} block is pruned, found earliest available block ${earliestAvailableBlock[chain]} by searching`);
239
+ }
240
+ return fetchBlockFromProvider(earliestAvailableBlock[chain], chain);
241
+ }
242
+ const earliestBlockSearches = {};
133
243
  let lastBlockCacheSave = Date.now();
134
244
  const blockCacheSaveInterval = 1000 * 60 * 5; // 5 minutes
135
245
  const { data: blockTimeCache, saveCacheFile: saveBlockCacheFile } = (0, cache_1.getTempLocalCache)({ file: 'BlockCache-v1.json', defaultData: {}, clearAfter: cache_1.ONE_WEEK, returnWithSaveFunction: true });
@@ -152,23 +262,97 @@ function getChainLimiter(chain) {
152
262
  return (0, p_limit_1.default)((0, env_1.getParallelGetBlocksLimit)(chain));
153
263
  }
154
264
  }
265
+ // concurrent lookups for the same chain & timestamp share one search
266
+ const inflightLookups = {};
155
267
  async function lookupBlock(timestamp, extraParams) {
156
- const { chain = "ethereum" } = extraParams !== null && extraParams !== void 0 ? extraParams : {};
157
- const limiter = getChainLimiter(chain);
158
- return limiter(() => _lookupBlock(timestamp, extraParams));
268
+ const { chain = "ethereum", allowedTimeRange, acceptableBlockImprecision } = extraParams !== null && extraParams !== void 0 ? extraParams : {};
269
+ if (timestamp > Date.now() / 1000)
270
+ throw new Error(`Requesting for block in the future ${timestamp} is in the future, current time is ${Math.floor(Date.now() / 1000)}`);
271
+ const key = [chain, timestamp, allowedTimeRange, acceptableBlockImprecision].join(':');
272
+ if (!inflightLookups[key]) {
273
+ const limiter = getChainLimiter(chain);
274
+ inflightLookups[key] = lookupBlockFromService(timestamp, extraParams)
275
+ .then(block => block !== null && block !== void 0 ? block : limiter(() => _lookupBlock(timestamp, extraParams)))
276
+ .finally(() => delete inflightLookups[key]);
277
+ }
278
+ return inflightLookups[key];
279
+ }
280
+ const blockServiceRoundSeconds = 300; // the service's default route rounds timestamps down to this
281
+ const blockServiceTimeout = 5000;
282
+ const blockServiceMaxFailures = 3;
283
+ const blockServicePauseDuration = 1000 * 60 * 5; // after repeated failures, go straight to the local search for a while
284
+ let blockServiceFailures = 0;
285
+ let blockServicePausedUntil = 0;
286
+ // ask the block-service when BLOCK_SERVICE_URL is set, any failure falls back to the local search
287
+ async function lookupBlockFromService(timestamp, extraParams) {
288
+ var _a;
289
+ const serviceUrl = (0, env_1.getBlockServiceUrl)();
290
+ if (!serviceUrl || Date.now() < blockServicePausedUntil)
291
+ return;
292
+ const { chain = "ethereum", allowedTimeRange = 5 * 60 } = extraParams !== null && extraParams !== void 0 ? extraParams : {};
293
+ if (chain === 'waves')
294
+ return;
295
+ const route = allowedTimeRange >= blockServiceRoundSeconds ? '' : '/precise';
296
+ try {
297
+ const { block, timestamp: blockTimestamp } = await (0, common_1.fetchJson)(`${serviceUrl}/block/${chain}/${timestamp}${route}`, { timeout: blockServiceTimeout });
298
+ if (!Number.isInteger(block) || !Number.isInteger(blockTimestamp))
299
+ throw new Error('bad response');
300
+ // same guard as the local search: a wildly off answer is worse than no answer
301
+ if (blockTimestamp > timestamp || timestamp - blockTimestamp > 3600)
302
+ throw new Error(`block ${block} (${blockTimestamp}) is too far from ${timestamp}`);
303
+ blockServiceFailures = 0;
304
+ addBlockToCache(chain, { number: block, timestamp: blockTimestamp });
305
+ return { block, number: block, timestamp: blockTimestamp };
306
+ }
307
+ catch (e) {
308
+ if (++blockServiceFailures >= blockServiceMaxFailures) {
309
+ blockServicePausedUntil = Date.now() + blockServicePauseDuration;
310
+ blockServiceFailures = 0;
311
+ }
312
+ (0, debugLog_1.debugLog)(`block-service lookup failed for ${chain} ${timestamp}, falling back to local search: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
313
+ }
314
+ }
315
+ function getChainBlockTimeCache(chain) {
316
+ if (!blockTimeCache[chain])
317
+ blockTimeCache[chain] = {};
318
+ return blockTimeCache[chain];
319
+ }
320
+ function addBlockToCache(chain, block) {
321
+ const { number, timestamp } = block;
322
+ getChainBlockTimeCache(chain)[number] = { number, timestamp, };
323
+ // save the block cache every 5 minutes
324
+ if (Date.now() - lastBlockCacheSave > blockCacheSaveInterval) {
325
+ lastBlockCacheSave = Date.now();
326
+ saveBlockCacheFile();
327
+ }
159
328
  }
160
329
  const earliestAvailableBlockRange = {
161
330
  nibiru: 4 * 1e5, // nibiru hold only the last 400k block data
162
331
  evmos: 2 * 1e5,
163
332
  };
333
+ // block time measured over the most recent blocks, a far better predictor of the near past than the all time average
334
+ const blockTimeSampleSize = 10000;
335
+ const blockTimeCacheDuration = 1000 * 60 * 60; // 1 hour
336
+ const recentBlockTimes = {};
337
+ async function getRecentBlockTime(chain, anchor, floor, onBlock) {
338
+ const cached = recentBlockTimes[chain];
339
+ if (cached && Date.now() - cached.updatedAt < blockTimeCacheDuration)
340
+ return cached.blockTime;
341
+ const sample = await fetchAvailableBlock(Math.max(floor, anchor.number - blockTimeSampleSize), chain);
342
+ if (sample.number >= anchor.number)
343
+ return 2; // pruning horizon is above the anchor, nothing to measure, rough guess, don't cache
344
+ onBlock(sample);
345
+ const blockTime = (anchor.timestamp - sample.timestamp) / (anchor.number - sample.number);
346
+ if (!(blockTime > 0) || !isFinite(blockTime))
347
+ return 2; // not enough history to measure, rough guess, don't cache
348
+ recentBlockTimes[chain] = { blockTime, updatedAt: Date.now() };
349
+ return blockTime;
350
+ }
164
351
  async function _lookupBlock(timestamp, extraParams) {
165
352
  var _a, _b;
166
353
  const { allowedTimeRange = 5 * 60, acceptableBlockImprecision = 10 } = extraParams !== null && extraParams !== void 0 ? extraParams : {};
167
354
  const chain = (_a = extraParams === null || extraParams === void 0 ? void 0 : extraParams.chain) !== null && _a !== void 0 ? _a : "ethereum";
168
355
  let time = Date.now();
169
- if (timestamp > time / 1000) {
170
- throw new Error(`Requesting for block in the future ${timestamp} is in the future, current time is ${Math.floor(time / 1000)}`);
171
- }
172
356
  if (chain === 'waves') {
173
357
  const api = `https://nodes.wavesnodes.com/blocks/heightByTimestamp/${timestamp}`;
174
358
  const data = await (0, common_1.fetchJson)(api);
@@ -178,100 +362,124 @@ async function _lookupBlock(timestamp, extraParams) {
178
362
  number: +data.height,
179
363
  };
180
364
  }
181
- let low = (_b = intialBlocks[chain]) !== null && _b !== void 0 ? _b : 100;
182
- let envLowValue = process.env[`${chain.toUpperCase()}_BLOCK_LOW`];
183
- if (envLowValue)
184
- low = parseInt(envLowValue);
185
- let lowBlock = getLowBlock();
186
- let highBlock = getHighBlock();
187
- if ((lowBlock === null || lowBlock === void 0 ? void 0 : lowBlock.number) < low)
188
- lowBlock = undefined;
189
- let block;
190
- let i = 0;
191
- let blockImprecision;
192
- let imprecision;
365
+ const maxCalls = 30;
366
+ let calls = 0;
367
+ let lowBlock; // closest known block at or before the timestamp
368
+ let highBlock; // closest known block after the timestamp
369
+ let bestBlock;
370
+ let blockTime;
371
+ let lastBracketWidth = Infinity;
193
372
  try {
194
- let firstBlock, lastBlock;
195
- if (!!earliestAvailableBlockRange[chain]) {
196
- lastBlock = await (0, exports.getLatestBlock)(chain);
197
- let firstBlockNum = lastBlock.number - earliestAvailableBlockRange[chain];
198
- firstBlock = await fetchBlockFromProvider(firstBlockNum, chain);
199
- }
200
- else {
201
- [lastBlock, firstBlock] = await Promise.all([
202
- highBlock ? highBlock : (0, exports.getLatestBlock)(chain),
203
- lowBlock ? lowBlock : fetchBlockFromProvider(low, chain),
204
- ]);
205
- }
206
- lowBlock = firstBlock;
207
- if (!highBlock)
208
- highBlock = lastBlock;
209
- if (Math.abs(highBlock.timestamp - timestamp) < 60 * 5) {
210
- // Short-circuit in case we are trying to get the current block
211
- return {
212
- block: highBlock.number,
213
- number: highBlock.number,
214
- timestamp: highBlock.timestamp
215
- };
373
+ let floor = getFloorBlock();
374
+ const cachedLow = getLowBlock();
375
+ if (cachedLow && cachedLow.number >= floor)
376
+ updateBounds(cachedLow, false);
377
+ const cachedHigh = getHighBlock();
378
+ if (cachedHigh)
379
+ updateBounds(cachedHigh, false);
380
+ // anchor on the chain head unless the cache already has a block after the timestamp
381
+ if (!highBlock || earliestAvailableBlockRange[chain]) {
382
+ const head = await (0, exports.getLatestBlock)(chain);
383
+ // Short-circuit in case we are trying to get the current block, or the rpc head lags behind the requested time
384
+ if (Math.abs(head.timestamp - timestamp) < 60 * 5 || head.timestamp <= timestamp)
385
+ return { block: head.number, number: head.number, timestamp: head.timestamp };
386
+ floor = getFloorBlock(head);
387
+ if (!highBlock || head.number < highBlock.number)
388
+ updateBounds(head, false);
216
389
  }
217
- updateBlock();
218
- while (imprecision > allowedTimeRange && blockImprecision > acceptableBlockImprecision && i < 50) { // We lose some precision (max ~15 minutes) but reduce #calls needed
219
- ++i;
220
- const blockDiff = highBlock.number - lowBlock.number;
221
- const timeDiff = highBlock.timestamp - lowBlock.timestamp;
222
- const avgBlockTime = timeDiff / blockDiff;
223
- let closeBlock = Math.floor(lowBlock.number + (timestamp - lowBlock.timestamp) / avgBlockTime);
224
- if (closeBlock > highBlock.number)
225
- closeBlock = highBlock.number;
226
- const midBlock = Math.floor((lowBlock.number + highBlock.number) / 2);
227
- const blocks = await Promise.all([
228
- fetchBlockFromProvider(closeBlock, chain),
229
- fetchBlockFromProvider(midBlock, chain),
230
- ]);
231
- updateBlock(blocks);
390
+ while (!isPreciseEnough() && calls < maxCalls) {
391
+ floor = Math.max(floor, (_b = earliestAvailableBlock[chain]) !== null && _b !== void 0 ? _b : 0);
392
+ if (lowBlock && lowBlock.number < floor)
393
+ lowBlock = undefined; // pruned away, guesses below the floor would just repeat the horizon block
394
+ let guesses;
395
+ if (lowBlock) {
396
+ // bracketed: interpolate between the bounds, bisect when the previous step didn't at least halve the bracket
397
+ const width = highBlock.number - lowBlock.number;
398
+ if (width <= 1)
399
+ break;
400
+ let guess = lowBlock.number + Math.round((timestamp - lowBlock.timestamp) * width / (highBlock.timestamp - lowBlock.timestamp));
401
+ if (width > lastBracketWidth / 2)
402
+ guess = Math.floor((lowBlock.number + highBlock.number) / 2);
403
+ lastBracketWidth = width;
404
+ guesses = [clamp(guess, lowBlock.number + 1, highBlock.number - 1)];
405
+ }
406
+ else {
407
+ // only an upper bound: extrapolate back from it using the recent block time
408
+ if (highBlock.number <= floor)
409
+ break; // requested time is before the available history
410
+ if (!blockTime) {
411
+ const recentBlockTime = await getRecentBlockTime(chain, highBlock, floor, block => { calls++; updateBounds(block); });
412
+ blockTime = blockTime !== null && blockTime !== void 0 ? blockTime : recentBlockTime; // updateBounds may have set a more local slope meanwhile, keep that
413
+ }
414
+ if (lowBlock)
415
+ continue; // the block time sample already bracketed the timestamp
416
+ const guess = clamp(Math.round(highBlock.number - (highBlock.timestamp - timestamp) / blockTime), floor, highBlock.number - 1);
417
+ // probe around the guess too, so a slightly off block time still brackets the timestamp in one round trip
418
+ const spread = Math.max(acceptableBlockImprecision, Math.round((highBlock.number - guess) * 0.02));
419
+ guesses = [...new Set([guess - spread, guess, guess + spread].map(b => clamp(b, floor, highBlock.number - 1)))];
420
+ }
421
+ const blocks = await Promise.all(guesses.map(b => fetchAvailableBlock(b, chain)));
422
+ calls += blocks.length;
423
+ blocks.forEach(block => updateBounds(block));
232
424
  }
233
- if (i > 2 && env_1.ENV_CONSTANTS.GET_BLOCK_STATS)
234
- (0, debugLog_1.debugLog)(`chain: ${chain} block: ${block.number} #calls: ${i} imprecision: ${Number((imprecision) / 60).toFixed(2)} (min) Time Taken: ${Number((Date.now() - time) / 1000).toFixed(2)} (in sec)`);
235
- if (Math.abs(block.timestamp - timestamp) > 3600
425
+ if (calls > 2 && env_1.ENV_CONSTANTS.GET_BLOCK_STATS)
426
+ (0, debugLog_1.debugLog)(`chain: ${chain} block: ${bestBlock.number} #calls: ${calls} imprecision: ${Number(getImprecision(bestBlock) / 60).toFixed(2)} (min) Time Taken: ${Number((Date.now() - time) / 1000).toFixed(2)} (in sec)`);
427
+ if (getImprecision(bestBlock) > 3600
236
428
  && chain !== "bsc" // this check is there because bsc halted the chain for few days
237
429
  && !(0, env_1.shouldSkipBlockValidation)(chain)) {
430
+ if (!lowBlock && highBlock.number <= floor)
431
+ throw new Error(`Requested timestamp ${timestamp} (${new Date(timestamp * 1000).toISOString()}) is before the earliest available block ${highBlock.number} (${new Date(highBlock.timestamp * 1000).toISOString()}) of ${chain}`);
238
432
  throw new Error("Block selected is more than 1 hour away from the requested timestamp");
239
433
  }
240
434
  return {
241
- number: block.number,
242
- block: block.number,
243
- timestamp: block.timestamp
435
+ number: bestBlock.number,
436
+ block: bestBlock.number,
437
+ timestamp: bestBlock.timestamp
244
438
  };
245
439
  }
246
440
  catch (e) {
247
441
  throw (0, common_1.formError)(e);
248
442
  }
249
- function updateBlock(blocks = []) {
250
- blocks.forEach(addBlockToCache);
251
- const getPrecision = (block) => block.timestamp - timestamp > 0 ? block.timestamp - timestamp : timestamp - block.timestamp;
252
- blocks.push(highBlock, lowBlock);
253
- blocks.sort((a, b) => getPrecision(a) - getPrecision(b));
254
- block = blocks[0];
255
- // find the closest upper and lower bound between 4 points
256
- lowBlock = blocks.filter(i => i.timestamp < timestamp).reduce((lowestBlock, block) => (timestamp - lowestBlock.timestamp) < (timestamp - block.timestamp) ? lowestBlock : block, lowBlock);
257
- highBlock = blocks.filter(i => i.timestamp > timestamp).reduce((highestBlock, block) => (highestBlock.timestamp - timestamp) < (block.timestamp - timestamp) ? highestBlock : block, highBlock);
258
- imprecision = getPrecision(block);
259
- blockImprecision = highBlock.number - lowBlock.number;
260
- // debugLog(`chain: ${chain} block: ${block.number} #calls: ${i} imprecision: ${Number((imprecision)/60).toFixed(2)} (min) block diff: ${blockImprecision} Time Taken: ${Number((Date.now()-time)/1000).toFixed(2)} (in sec)`)
443
+ function getFloorBlock(head) {
444
+ var _a, _b;
445
+ let floor = (_a = intialBlocks[chain]) !== null && _a !== void 0 ? _a : 100;
446
+ const envLowValue = process.env[`${chain.toUpperCase()}_BLOCK_LOW`];
447
+ if (envLowValue)
448
+ floor = parseInt(envLowValue);
449
+ floor = Math.max(floor, (_b = earliestAvailableBlock[chain]) !== null && _b !== void 0 ? _b : 0);
450
+ if (head && earliestAvailableBlockRange[chain])
451
+ floor = Math.max(floor, head.number - earliestAvailableBlockRange[chain]);
452
+ return floor;
453
+ }
454
+ function getImprecision(block) {
455
+ return Math.abs(block.timestamp - timestamp);
261
456
  }
262
- function getChainBlockTimeCache(chain) {
263
- if (!blockTimeCache[chain])
264
- blockTimeCache[chain] = {};
265
- return blockTimeCache[chain];
457
+ function isPreciseEnough() {
458
+ if (bestBlock && getImprecision(bestBlock) <= allowedTimeRange)
459
+ return true;
460
+ return !!lowBlock && !!highBlock && highBlock.number - lowBlock.number <= acceptableBlockImprecision;
266
461
  }
267
- function addBlockToCache(block) {
268
- const { number, timestamp } = block;
269
- getChainBlockTimeCache(chain)[block.number] = { number, timestamp, };
270
- // save the block cache every 5 minutes
271
- if (Date.now() - lastBlockCacheSave > blockCacheSaveInterval) {
272
- lastBlockCacheSave = Date.now();
273
- saveBlockCacheFile();
462
+ function updateBounds(block, saveToCache = true) {
463
+ if (saveToCache)
464
+ addBlockToCache(chain, block);
465
+ if (block.timestamp <= timestamp) {
466
+ if (!lowBlock || block.number > lowBlock.number)
467
+ lowBlock = block;
274
468
  }
469
+ else if (!highBlock || block.number < highBlock.number) {
470
+ // while unbracketed, the slope between consecutive upper bounds is the most local block time we have
471
+ if (highBlock && !lowBlock) {
472
+ const localBlockTime = (highBlock.timestamp - block.timestamp) / (highBlock.number - block.number);
473
+ if (localBlockTime > 0)
474
+ blockTime = localBlockTime;
475
+ }
476
+ highBlock = block;
477
+ }
478
+ if (!bestBlock || getImprecision(block) < getImprecision(bestBlock))
479
+ bestBlock = block;
480
+ }
481
+ function clamp(value, min, max) {
482
+ return Math.min(Math.max(value, min), max);
275
483
  }
276
484
  function getLowBlock() {
277
485
  return Object.values(getChainBlockTimeCache(chain))