@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.
@@ -94,6 +94,7 @@ const axiosInstances = {
94
94
  v2: createIndexerClient("v2"),
95
95
  v4: createIndexerClient("v4"),
96
96
  };
97
+ const INDEXER_HEAD_BUFFER = 50;
97
98
  function checkIndexerConfig(version) {
98
99
  const { endpoint, apiKey } = indexerConfigs[version];
99
100
  if (!endpoint || !apiKey)
@@ -318,7 +319,7 @@ async function getLogs(options) {
318
319
  if (processor && typeof options.collect === "undefined" && clientStreaming) {
319
320
  options = { ...options, collect: false };
320
321
  }
321
- const { chain, fromBlock, toBlock, topic, topic1, topic2, topic3, transformLog, } = await (0, logs_1.getLogParams)(options, true);
322
+ const { chain, fromBlock, toBlock, topic, topic1, topic2, topic3, topics, transformLog, } = await (0, logs_1.getLogParams)(options, true);
322
323
  const viemFastPath =
323
324
  // Preserve the historical streaming shape, including entireLog: the viem
324
325
  // fast path exposes args without adding the buffered decoder's parsedLog.
@@ -331,9 +332,11 @@ async function getLogs(options) {
331
332
  if (processor && (targets === null || targets === void 0 ? void 0 : targets.length) > 1 && !flatten)
332
333
  throw new Error("processor is not supported with multiple targets when flatten=false");
333
334
  const blockRange = toBlock - fromBlock;
335
+ if (!Number.isSafeInteger(fromBlock) || !Number.isSafeInteger(toBlock) || fromBlock > toBlock)
336
+ throw new Error("getLogs requires integer block bounds with fromBlock <= toBlock");
334
337
  const effectiveMaxBlockRange = maxBlockRange !== null && maxBlockRange !== void 0 ? maxBlockRange : (noTarget ? 10000 : Infinity);
335
- if (!(effectiveMaxBlockRange > 0))
336
- throw new Error("maxBlockRange must be greater than zero");
338
+ if (!(effectiveMaxBlockRange > 0) || (effectiveMaxBlockRange !== Infinity && !Number.isSafeInteger(effectiveMaxBlockRange)))
339
+ throw new Error("maxBlockRange must be a positive integer");
337
340
  if (noTarget && blockRange > 500000) {
338
341
  throw new Error("When noTarget is true, block range must be less than 500k blocks. Please narrow down your block range.");
339
342
  }
@@ -342,21 +345,19 @@ async function getLogs(options) {
342
345
  checkIndexerConfig(indexerVersion);
343
346
  const chainId = getChainId(chain);
344
347
  const chainIndexStatus = await getChainIndexStatus(indexerVersion);
345
- const lastIndexedBlock = (_b = (_a = chainIndexStatus[chain]) === null || _a === void 0 ? void 0 : _a.block) !== null && _b !== void 0 ? _b : 0;
348
+ const lastIndexedBlock = Number((_b = (_a = chainIndexStatus[chain]) === null || _a === void 0 ? void 0 : _a.block) !== null && _b !== void 0 ? _b : 0);
349
+ if (!Number.isSafeInteger(lastIndexedBlock) || lastIndexedBlock < 0)
350
+ throw new Error(`Indexer returned an invalid last indexed block for ${chain}`);
346
351
  if (lastIndexedBlock < toBlock) {
347
352
  const percentageMissing = ((toBlock - lastIndexedBlock) / (toBlock - fromBlock)) * 100;
348
353
  if (percentageMissing > 50)
349
354
  throw new Error(`Indexer not up to date for ${chain}. Last indexed block: ${lastIndexedBlock}, requested block: ${toBlock}`);
350
355
  if (env_1.ENV_CONSTANTS.GET_LOGS_INDEXER)
351
356
  (0, debugLog_1.debugLog)(`Indexer only partially up to date for ${chain}. Last indexed block: ${lastIndexedBlock}, requested block: ${toBlock}, missing ${Number(percentageMissing).toFixed(2)}%. Pulling part of the logs through RPC calls.`);
352
- const breakBlock = lastIndexedBlock - 50; // small buffer
353
- const indexerLogs = await getLogs({ ...options, fromBlock, toBlock: breakBlock });
354
- const rpcLogs = await (0, logs_1.getLogs)({ ...options, fromBlock: breakBlock + 1, toBlock, skipIndexer: true });
355
- // When flatten=false, preserve per-target buckets: merge RPC logs into their corresponding indexer bucket so the result still maintains one element per target.
356
- if (!flatten) {
357
- return indexerLogs.map((bucket, i) => bucket.concat(rpcLogs[i]));
358
- }
359
- return indexerLogs.concat(rpcLogs);
357
+ // An empty indexer prefix uses RPC for exactly the original window. The two
358
+ // inclusive ranges otherwise meet at breakBlock / breakBlock + 1.
359
+ const breakBlock = Math.max(fromBlock - 1, lastIndexedBlock - INDEXER_HEAD_BUFFER);
360
+ return getPartiallyIndexedLogs(breakBlock);
360
361
  }
361
362
  // Re-curse if the requested range is too large
362
363
  if (blockRange > effectiveMaxBlockRange) {
@@ -366,9 +367,10 @@ async function getLogs(options) {
366
367
  const chunk = await getLogs({ ...options, fromBlock: currentFromBlock, toBlock: currentToBlock });
367
368
  results.push(chunk);
368
369
  }
369
- if (flatten || !(targets === null || targets === void 0 ? void 0 : targets.length))
370
+ const splitTargets = typeof target === "string" ? [target] : targets;
371
+ if (flatten || !splitTargets.length)
370
372
  return results.flat();
371
- return targets.map((_, i) => results.map((r) => r[i]).flat());
373
+ return splitTargets.map((_, i) => results.map((r) => r[i]).flat());
372
374
  }
373
375
  let address = target;
374
376
  if (typeof target === "string")
@@ -548,6 +550,110 @@ async function getLogs(options) {
548
550
  return mapped;
549
551
  }
550
552
  return allLogsPairs.map(i => i.transformed);
553
+ async function getPartiallyIndexedLogs(breakBlock) {
554
+ if (!Number.isSafeInteger(limit) || limit < 0 || !Number.isSafeInteger(initialOffset) || initialOffset < 0)
555
+ throw new Error("Indexer pagination requires a non-negative integer limit and offset");
556
+ const requestedTargets = typeof target === "string" ? [target] : targets;
557
+ const uniqueTargets = [...new Set(requestedTargets.map(t => t.toLowerCase()))];
558
+ const splitByAddress = requestedTargets.length > 0 && !flatten;
559
+ const collected = splitByAddress ? requestedTargets.map(() => []) : [];
560
+ const targetBuckets = new Map();
561
+ requestedTargets.forEach((t, i) => {
562
+ var _a;
563
+ const key = t.toLowerCase();
564
+ targetBuckets.set(key, [...((_a = targetBuckets.get(key)) !== null && _a !== void 0 ? _a : []), i]);
565
+ });
566
+ let remainingOffset = initialOffset;
567
+ let remainingLimit = all ? Infinity : limit;
568
+ if (limit === 0)
569
+ return collected;
570
+ const rpcTransform = (await (0, logs_1.getLogParams)(options, false)).transformLog;
571
+ const finished = Symbol("partial log limit reached");
572
+ let seenBlock;
573
+ const seen = new Set();
574
+ async function consume(rawRows, isIndexer, start, end) {
575
+ var _a, _b, _c, _d;
576
+ const selected = [];
577
+ for (const row of rawRows) {
578
+ const block = Number((_a = row.blockNumber) !== null && _a !== void 0 ? _a : row.block_number);
579
+ if (!Number.isSafeInteger(block))
580
+ throw new Error("Log returned without a valid block number");
581
+ if (block < start || block > end)
582
+ continue;
583
+ if (block !== seenBlock) {
584
+ seenBlock = block;
585
+ seen.clear();
586
+ }
587
+ const hash = (_b = row.transactionHash) !== null && _b !== void 0 ? _b : row.transaction_hash;
588
+ const index = (_d = (_c = row.logIndex) !== null && _c !== void 0 ? _c : row.index) !== null && _d !== void 0 ? _d : row.log_index;
589
+ if (hash !== undefined && index !== undefined) {
590
+ const key = `${String(hash).toLowerCase()}:${index}`;
591
+ if (seen.has(key))
592
+ continue;
593
+ seen.add(key);
594
+ }
595
+ if (remainingOffset > 0) {
596
+ remainingOffset--;
597
+ continue;
598
+ }
599
+ if (remainingLimit <= 0)
600
+ break;
601
+ selected.push(clientStreaming && !isIndexer ? { ...row } : row);
602
+ remainingLimit--;
603
+ }
604
+ if (selected.length) {
605
+ const transform = isIndexer ? transformLog : rpcTransform;
606
+ const startTime = Date.now();
607
+ const decoded = clientStreaming
608
+ ? await decodeStreamingLogs(selected, transform, viemFastPath !== null && viemFastPath !== void 0 ? viemFastPath : transform.batch, options.onlyArgs)
609
+ : await Promise.all(selected.map(row => transform(row)));
610
+ onDecodeStats === null || onDecodeStats === void 0 ? void 0 : onDecodeStats({ batchSize: selected.length, decodeTime: Date.now() - startTime, itemsDecoded: selected.length });
611
+ if (processor)
612
+ await processor(decoded);
613
+ if (options.collect !== false) {
614
+ if (splitByAddress)
615
+ selected.forEach((row, i) => {
616
+ var _a, _b, _c;
617
+ for (const bucket of (_c = targetBuckets.get((_b = ((_a = row.source) !== null && _a !== void 0 ? _a : row.address)) === null || _b === void 0 ? void 0 : _b.toLowerCase())) !== null && _c !== void 0 ? _c : [])
618
+ collected[bucket].push(decoded[i]);
619
+ });
620
+ else
621
+ safePush(collected, decoded);
622
+ }
623
+ }
624
+ if (remainingLimit <= 0)
625
+ throw finished;
626
+ }
627
+ const rawOptions = {
628
+ ...options, chain, topic, topics, target: undefined, targets: uniqueTargets,
629
+ eventAbi: undefined, parseLog: false, entireLog: true, onlyArgs: false,
630
+ flatten: true, onDecodeStats: undefined,
631
+ };
632
+ try {
633
+ if (breakBlock >= fromBlock) {
634
+ await getLogs({
635
+ ...rawOptions, fromBlock, toBlock: breakBlock, all: true, offset: 0, collect: false,
636
+ processor: rows => consume(rows, true, fromBlock, breakBlock),
637
+ });
638
+ }
639
+ // RPC cache reads may extend the physical fetch by a buffer, but logs.ts
640
+ // filters to these exact bounds before any decoding or processing here.
641
+ for (let start = breakBlock + 1; start <= toBlock; start += effectiveMaxBlockRange) {
642
+ const end = Math.min(toBlock, start + effectiveMaxBlockRange - 1);
643
+ const rows = await (0, logs_1.getLogs)({
644
+ ...rawOptions, fromBlock: start, toBlock: end, skipIndexer: true,
645
+ maxBlockRange: undefined, processor: undefined,
646
+ });
647
+ rows.sort((a, b) => { var _a, _b; return a.blockNumber - b.blockNumber || ((_a = a.logIndex) !== null && _a !== void 0 ? _a : a.index) - ((_b = b.logIndex) !== null && _b !== void 0 ? _b : b.index); });
648
+ await consume(rows, false, start, end);
649
+ }
650
+ }
651
+ catch (error) {
652
+ if (error !== finished)
653
+ throw error;
654
+ }
655
+ return collected;
656
+ }
551
657
  }
552
658
  async function getTokenTransfers({ chain = "ethereum", fromAddressFilter, fromBlock, toBlock, all = true, limit = 1000, offset = 0, target, targets = [], flatten = true, fromTimestamp, toTimestamp, debugMode = false, transferType = "in", token, tokens, }) {
553
659
  var _a, _b;
@@ -1,6 +1,7 @@
1
1
  import { EventLog, Interface } from "ethers";
2
2
  import { Address } from "../types";
3
3
  import { IndexerGetLogsOptions } from "./indexer";
4
+ export declare function getBufferedToBlock(toBlock: number, head?: number, buffer?: number): number;
4
5
  export type GetLogsOptions = {
5
6
  target?: Address;
6
7
  topic?: string;
@@ -30,6 +31,8 @@ export type GetLogsOptions = {
30
31
  allowParseFailure?: boolean;
31
32
  };
32
33
  export declare function getLogs(options: GetLogsOptions): Promise<EventLog[] | EventLog[][] | any[]>;
34
+ export declare function maxLogsForStorage(bytesPerLog: number, cap?: number, budgetMB?: number): number;
35
+ export declare function trimCachesForStorage(caches: logCache[], maxLogs?: number): logCache[];
33
36
  export declare function toFilterTopic(topic: string): string;
34
37
  export type logCache = {
35
38
  logs: EventLog[];
@@ -3,7 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getBufferedToBlock = getBufferedToBlock;
6
7
  exports.getLogs = getLogs;
8
+ exports.maxLogsForStorage = maxLogsForStorage;
9
+ exports.trimCachesForStorage = trimCachesForStorage;
7
10
  exports.toFilterTopic = toFilterTopic;
8
11
  exports.getLogParams = getLogParams;
9
12
  const ethers_1 = require("ethers");
@@ -17,6 +20,15 @@ const env_1 = require("./env");
17
20
  const common_1 = require("./common");
18
21
  const logs_decode_shared_1 = require("./logs.decode.shared");
19
22
  const currentVersion = "v3";
23
+ const BLOCK_BUFFER = 10;
24
+ // widen the fetched range forward by BLOCK_BUFFER, without asking for blocks the chain does not
25
+ // have yet and without ever returning less than the caller asked for
26
+ function getBufferedToBlock(toBlock, head, buffer = BLOCK_BUFFER) {
27
+ const buffered = toBlock + buffer;
28
+ if (head === undefined)
29
+ return buffered;
30
+ return Math.min(buffered, Math.max(toBlock, head));
31
+ }
20
32
  /* -------------------------------------------------------------------------- */
21
33
  /* getLogs */
22
34
  /* -------------------------------------------------------------------------- */
@@ -133,8 +145,16 @@ async function getLogs(options) {
133
145
  (0, debugLog_1.debugLog)("adding logs to cache:", fromBlock, toBlock);
134
146
  if (fromBlock > toBlock)
135
147
  return;
136
- fromBlock = Math.max(fromBlock - 10, 0); // avoid negative block
137
- toBlock = toBlock + 10;
148
+ fromBlock = Math.max(fromBlock - BLOCK_BUFFER, 0); // avoid negative block
149
+ // the forward buffer must not run past the chain head. Several RPCs reject a range that ends
150
+ // beyond their latest block outright, and since every RPC in the pool rejects it the whole call
151
+ // fails. If the head cannot be read (halted chain, provider error) keep the previous behaviour.
152
+ let head;
153
+ try {
154
+ head = await (0, blocks_1.getBlockNumber)(chain);
155
+ }
156
+ catch (e) { }
157
+ toBlock = getBufferedToBlock(toBlock, head);
138
158
  if (debugMode)
139
159
  console.time(dbgKey);
140
160
  const { output: newLogs } = await (0, _1.getLogs)({
@@ -156,8 +176,8 @@ async function getLogs(options) {
156
176
  const merged = [caches[0]];
157
177
  caches.slice(1).forEach((c) => {
158
178
  const last = merged[merged.length - 1];
159
- if (last.metadata.toBlock + 1 > c.metadata.fromBlock) {
160
- last.metadata.toBlock = c.metadata.toBlock;
179
+ if (last.metadata.toBlock + 1 >= c.metadata.fromBlock) {
180
+ last.metadata.toBlock = Math.max(last.metadata.toBlock, c.metadata.toBlock);
161
181
  last.logs = dedupLogs(last.logs, c.logs);
162
182
  }
163
183
  else
@@ -174,28 +194,9 @@ async function getLogs(options) {
174
194
  }, 0);
175
195
  const fileSizeInMB = (totalLogCount * randomLogLength) / (1024 * 1024);
176
196
  if (fileSizeInMB > 300) { // if cache size is larger than ~300MB, trim it down
177
- console.log(`getLogs: large cache size detected ${totalLogCount} logs size: ${fileSizeInMB}MB, retaining only the latest 200k logs to limit memory usage target=${target} topic=${topic} fromBlock=${fromBlock} toBlock=${toBlock}`);
178
- // retain only the latest logs that fit within 300,000 logs
179
- if (caches.length > 1) {
180
- let cacheWithLatestFromBlock = caches[0];
181
- for (const c of caches) {
182
- if (c.metadata.fromBlock > cacheWithLatestFromBlock.metadata.fromBlock) {
183
- cacheWithLatestFromBlock = c;
184
- }
185
- }
186
- storeCaches = [cacheWithLatestFromBlock];
187
- }
188
- storeCaches = storeCaches.map(c => {
189
- const clone = { logs: [], metadata: JSON.parse(JSON.stringify(c.metadata)) };
190
- if (c.logs.length > 200000) {
191
- // sort the logs by block number descending and keep only the latest 200k logs
192
- c.logs.sort((a, b) => b.blockNumber - a.blockNumber);
193
- clone.logs = c.logs.slice(0, 200000);
194
- const lastLog = clone.logs[clone.logs.length - 1];
195
- clone.metadata.fromBlock = lastLog.blockNumber;
196
- }
197
- return clone;
198
- });
197
+ const maxLogs = maxLogsForStorage(randomLogLength);
198
+ console.log(`getLogs: large cache size detected ${totalLogCount} logs size: ${fileSizeInMB}MB, retaining only the latest ${maxLogs} logs to limit memory usage target=${target} topic=${topic} fromBlock=${fromBlock} toBlock=${toBlock}`);
199
+ storeCaches = trimCachesForStorage(storeCaches, maxLogs);
199
200
  }
200
201
  // we are skipping compression by default, so reads & writes are faster
201
202
  await (0, cache_1.writeCache)(getFile(), { caches: storeCaches, version: currentVersion }, { skipR2CacheWrite: !cacheInCloud, skipCompression: !cacheInCloud });
@@ -246,6 +247,36 @@ async function getLogs(options) {
246
247
  /* -------------------------------------------------------------------------- */
247
248
  /* Utility helpers exported */
248
249
  /* -------------------------------------------------------------------------- */
250
+ // the log count that fits the same byte budget the size check uses, so a wide log trims before the flat cap
251
+ function maxLogsForStorage(bytesPerLog, cap = 200000, budgetMB = 300) {
252
+ if (!bytesPerLog || bytesPerLog < 1)
253
+ return cap;
254
+ return Math.max(1, Math.min(cap, Math.floor((budgetMB * 1024 * 1024) / bytesPerLog)));
255
+ }
256
+ // keeps the newest range and at most maxLogs of its logs, narrowing the metadata to match what is kept
257
+ function trimCachesForStorage(caches, maxLogs = 200000) {
258
+ if (!caches.length)
259
+ return caches;
260
+ let trimmed = caches;
261
+ if (caches.length > 1) {
262
+ let cacheWithLatestFromBlock = caches[0];
263
+ for (const c of caches) {
264
+ if (c.metadata.fromBlock > cacheWithLatestFromBlock.metadata.fromBlock) {
265
+ cacheWithLatestFromBlock = c;
266
+ }
267
+ }
268
+ trimmed = [cacheWithLatestFromBlock];
269
+ }
270
+ return trimmed.map(c => {
271
+ const clone = { logs: c.logs, metadata: JSON.parse(JSON.stringify(c.metadata)) };
272
+ if (c.logs.length > maxLogs) {
273
+ const sorted = [...c.logs].sort((a, b) => b.blockNumber - a.blockNumber);
274
+ clone.logs = sorted.slice(0, maxLogs);
275
+ clone.metadata.fromBlock = clone.logs[clone.logs.length - 1].blockNumber;
276
+ }
277
+ return clone;
278
+ });
279
+ }
249
280
  function toFilterTopic(topic) {
250
281
  if (topic.startsWith("0x"))
251
282
  return topic;
@@ -259,7 +259,8 @@ async function filterForWorkingRPCs(rpc, chain, chainId) {
259
259
  if (data.result) {
260
260
  const returnedChainId = parseInt(data.result, 16);
261
261
  if (returnedChainId !== chainId) {
262
- console.log(`RPC ${i} for ${chain} returned invalid chainId ${returnedChainId}, expected ${chainId}`);
262
+ console.log(`RPC ${i} for ${chain} returned invalid chainId ${returnedChainId}, expected ${chainId}, skipping`);
263
+ return;
263
264
  }
264
265
  filteredRPCs.push(i);
265
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defillama/sdk",
3
- "version": "5.0.236",
3
+ "version": "5.0.238",
4
4
  "description": "SDK for calculating TVL",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -11,8 +11,8 @@
11
11
  "test": "npm run load-providers && TEST_MODE=true LLAMA_DEBUG_MODE='' SDK_DEBUG='' LLAMA_DEBUG_LEVEL2='' LLAMA_DEBUG_LEVEL3='' jest",
12
12
  "test-only": "echo 'skipped building providers.json' && TEST_MODE=true LLAMA_DEBUG_MODE='' SDK_DEBUG='' LLAMA_DEBUG_LEVEL2='' LLAMA_DEBUG_LEVEL3='' jest",
13
13
  "test-debug": "TEST_MODE=true JEST_VERBOSE=true jest",
14
- "test-indexer": "jest --runInBand --runTestsByPath src/util/indexer.test.ts src/util/indexer.e2e.test.ts src/util/indexerSql.test.ts src/util/indexerSql.parity.test.ts",
15
- "test-indexer-unit": "jest --runInBand --runTestsByPath src/util/indexer.test.ts src/util/indexerSql.test.ts",
14
+ "test-indexer": "jest --runInBand --runTestsByPath src/util/indexer.test.ts src/util/indexer.partialSync.test.ts src/util/indexer.partialSync.e2e.test.ts src/util/indexer.e2e.test.ts src/util/indexerSql.test.ts src/util/indexerSql.parity.test.ts",
15
+ "test-indexer-unit": "jest --runInBand --runTestsByPath src/util/indexer.test.ts src/util/indexer.partialSync.test.ts src/util/indexerSql.test.ts",
16
16
  "update-providers": "ts-node src/util/updateProviderList.ts",
17
17
  "update-chain-labels": "ts-node src/util/updateChainLabels.ts",
18
18
  "remove-rpcs": "ts-node src/util/removeRedundantRPCs.ts",