univo 0.3.2 → 0.4.0

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.
package/dist/index.mjs CHANGED
@@ -1,99 +1,11 @@
1
- import * as viem from 'viem';
2
- import { g as getException, c as createException, a as catchException, l as local } from './shared/univo.j1HWrILH.mjs';
3
- import { c as createLogger, d as decompress, b as decoder, r as retry, v as version, e as normalizeHex, a as isHexEqual, f as compress } from './shared/univo.vwk4Xrn3.mjs';
1
+ import { hexToNumber, numberToHex, hexToBytes, keccak256, toRlp, hexToBigInt, formatTransaction, serializeTransaction, numberToBytes, isHex, concatBytes } from 'viem';
2
+ import { local } from './transport.mjs';
3
+ import { c as createLogger, r as retry, v as version, n as normalizeHex, d as decompress, i as isHexEqual, a as compress, b as iife, e as decoder } from './shared/univo.mlthZw0m.mjs';
4
4
  import { AdapterError } from './metadata/adapters.mjs';
5
+ import { c as createException, a as catchException } from './shared/univo.C0flyVee.mjs';
5
6
  import 'ws';
6
7
  import 'partysocket';
7
8
 
8
- function createServer(opts) {
9
- const log = createLogger({ quiet: opts.quiet, prefix: "[server]" });
10
- const http = async (req) => {
11
- let body_buffer;
12
- try {
13
- body_buffer = await req.arrayBuffer();
14
- } catch {
15
- return Response.json(
16
- { jsonrpc: "2.0", id: null, error: { code: 0, message: "Invalid request body" } },
17
- //
18
- { status: 400 }
19
- );
20
- }
21
- const authorization = req.headers.get("Authorization");
22
- const authenticated = authorization === `Bearer ${opts.signingKey}`;
23
- let body_string;
24
- if (req.headers.get("Content-Encoding") === "gzip") {
25
- if (!authorization) {
26
- return Response.json(
27
- { jsonrpc: "2.0", id: null, error: { code: 0, message: "No bearer token provided" } },
28
- //
29
- { status: 400 }
30
- );
31
- }
32
- if (!authenticated) {
33
- return Response.json(
34
- { jsonrpc: "2.0", id: null, error: { code: 0, message: "Invalid bearer token" } },
35
- //
36
- { status: 400 }
37
- );
38
- }
39
- body_string = await decompress(body_buffer);
40
- } else {
41
- body_string = decoder.decode(body_buffer);
42
- }
43
- let json;
44
- try {
45
- json = JSON.parse(body_string);
46
- } catch {
47
- return Response.json(
48
- { jsonrpc: "2.0", id: null, error: { code: 0, message: "Malformed JSON request body" } },
49
- //
50
- { status: 400 }
51
- );
52
- }
53
- if (json.method.startsWith("private_")) {
54
- if (!authorization) {
55
- return Response.json(
56
- { jsonrpc: "2.0", id: json.id, error: { code: 0, message: "No bearer token provided" } },
57
- //
58
- { status: 400 }
59
- );
60
- }
61
- if (authorization !== `Bearer ${opts.signingKey}`) {
62
- return Response.json(
63
- { jsonrpc: "2.0", id: json.id, error: { code: 0, message: "Invalid bearer token" } },
64
- //
65
- { status: 400 }
66
- );
67
- }
68
- }
69
- try {
70
- const result = await opts.transport.request({ method: json.method, params: json.params });
71
- return Response.json({ jsonrpc: "2.0", id: json.id, result });
72
- } catch (error) {
73
- const message = getException(error);
74
- if (message) {
75
- log.error(message);
76
- return Response.json(
77
- { jsonrpc: "2.0", id: json.id, error: { code: 0, message } },
78
- //
79
- { status: 400 }
80
- );
81
- }
82
- if (error instanceof Error) {
83
- log.error(error.message);
84
- }
85
- return Response.json(
86
- { jsonrpc: "2.0", id: json.id, error: { code: 0, message: "Internal server error" } },
87
- //
88
- { status: 500 }
89
- );
90
- }
91
- };
92
- return {
93
- http
94
- };
95
- }
96
-
97
9
  function matchFilter(block, filter) {
98
10
  if (!chainValid(block, filter)) return false;
99
11
  if (!toBlockValid(block, filter)) return false;
@@ -103,16 +15,16 @@ function matchFilter(block, filter) {
103
15
  return true;
104
16
  }
105
17
  const chainValid = (block, filter) => {
106
- if (viem.hexToNumber(block.eth_chainId) === filter.chain) return true;
18
+ if (hexToNumber(block.eth_chainId) === filter.chain) return true;
107
19
  return false;
108
20
  };
109
21
  const fromBlockValid = (block, filter) => {
110
- if (viem.hexToNumber(block.eth_getBlockByNumber.number) >= filter.fromBlock) return true;
22
+ if (hexToNumber(block.eth_getBlockByNumber.number) >= filter.fromBlock) return true;
111
23
  return false;
112
24
  };
113
25
  const toBlockValid = (block, filter) => {
114
26
  if (filter.toBlock === void 0) return true;
115
- if (viem.hexToNumber(block.eth_getBlockByNumber.number) <= filter.toBlock) return true;
27
+ if (hexToNumber(block.eth_getBlockByNumber.number) <= filter.toBlock) return true;
116
28
  return false;
117
29
  };
118
30
  const includesLogAddress = (block, filter) => {
@@ -133,18 +45,45 @@ const includesLogEvent = (block, filter) => {
133
45
  }
134
46
  return false;
135
47
  };
48
+ function defineEvent(event) {
49
+ return event;
50
+ }
51
+ function defineAction(action) {
52
+ return action;
53
+ }
136
54
  function defineIndexer(opts) {
137
55
  const log = createLogger({ quiet: opts.quiet ?? false, prefix: "[indexer]" });
138
- const allEvents = [];
56
+ const allEvents = [...opts.events];
139
57
  const eventsGroupedByStorageMap = /* @__PURE__ */ new Map();
140
- const allActions = [];
58
+ for (const event of allEvents) {
59
+ if (!/^[A-Za-z0-9_-]+$/.test(event.id)) {
60
+ throw new Error(`Invalid event id \`${event.id}\`. Only characters A-Z, a-z, 0-9, underscores, and hyphens are permitted.`);
61
+ }
62
+ const group = eventsGroupedByStorageMap.get(event.storage) ?? [];
63
+ group.push(event);
64
+ eventsGroupedByStorageMap.set(event.storage, group);
65
+ }
66
+ const allActions = [...opts.actions];
67
+ const actionIds = /* @__PURE__ */ new Set();
68
+ for (const action of allActions) {
69
+ if (!/^[A-Za-z0-9_-]+$/.test(action.id)) {
70
+ throw new Error(`Invalid action id \`${action.id}\`. Only characters A-Z, a-z, 0-9, underscores, and hyphens are permitted.`);
71
+ }
72
+ if (actionIds.has(action.id)) {
73
+ throw new Error(`Duplicate action id \`${action.id}\`.`);
74
+ }
75
+ actionIds.add(action.id);
76
+ }
141
77
  async function getBlockFromChain(head) {
142
78
  return await retry(() => retry_getBlockFromChain(head), 2);
143
79
  }
144
80
  async function retry_getBlockFromChain(head) {
145
81
  const block = await opts.getBlock({ chain: head.chain, number: head.number });
146
- if (block === null) {
147
- throw new Error("Provided `getBlock` function returned null");
82
+ if (block.eth_getBlockByNumber === null) {
83
+ throw new Error("eth_getBlockByNumber returned null response");
84
+ }
85
+ if (block.eth_getBlockReceipts === null) {
86
+ throw new Error("eth_getBlockReceipts returned null response");
148
87
  }
149
88
  if (typeof head.hash === "string") {
150
89
  if (!isHexEqual(head.hash, block.eth_getBlockByNumber.hash)) {
@@ -156,6 +95,7 @@ function defineIndexer(opts) {
156
95
  throw new Error("Block returned unexpected parent hash");
157
96
  }
158
97
  }
98
+ verifyBlockHash(block);
159
99
  verifyBlockHashes(block);
160
100
  verifyLogIndicies(block);
161
101
  verifyReceiptsRoot(block);
@@ -164,6 +104,48 @@ function defineIndexer(opts) {
164
104
  verifyTransactionGasUsage(block);
165
105
  return block;
166
106
  }
107
+ function verifyBlockHash(block) {
108
+ const header = block.eth_getBlockByNumber;
109
+ const fields = [
110
+ hexToBytes(header.parentHash),
111
+ hexToBytes(header.sha3Uncles),
112
+ hexToBytes(header.miner),
113
+ hexToBytes(header.stateRoot),
114
+ hexToBytes(header.transactionsRoot),
115
+ hexToBytes(header.receiptsRoot),
116
+ hexToBytes(header.logsBloom),
117
+ quantityToBytes(header.difficulty),
118
+ quantityToBytes(header.number),
119
+ quantityToBytes(header.gasLimit),
120
+ quantityToBytes(header.gasUsed),
121
+ quantityToBytes(header.timestamp),
122
+ hexToBytes(header.extraData),
123
+ hexToBytes(header.mixHash),
124
+ hexToBytes(header.nonce)
125
+ ];
126
+ const extensions = [
127
+ [header.baseFeePerGas, quantityToBytes],
128
+ [header.withdrawalsRoot, hexToBytes],
129
+ [header.blobGasUsed, quantityToBytes],
130
+ [header.excessBlobGas, quantityToBytes],
131
+ [header.parentBeaconBlockRoot, hexToBytes],
132
+ [header.requestsHash, hexToBytes]
133
+ ];
134
+ let missingField = false;
135
+ for (const [value, encode] of extensions) {
136
+ if (value == null) {
137
+ missingField = true;
138
+ continue;
139
+ }
140
+ if (missingField) {
141
+ throw new Error("Block returned incomplete block header");
142
+ }
143
+ fields.push(encode(value));
144
+ }
145
+ if (!isHexEqual(header.hash, keccak256(toRlp(fields, "bytes")))) {
146
+ throw new Error("Block returned header that does not match the block hash");
147
+ }
148
+ }
167
149
  function verifyBlockHashes(block) {
168
150
  const blockHash = block.eth_getBlockByNumber.hash;
169
151
  const transactions = block.eth_getBlockByNumber.transactions;
@@ -176,7 +158,7 @@ function defineIndexer(opts) {
176
158
  if (!isHexEqual(block.eth_getBlockByNumber.hash, receipt.blockHash)) {
177
159
  throw new Error("Block returned receipt with unexpected block hash");
178
160
  }
179
- const transactionIndex = viem.hexToNumber(receipt.transactionIndex);
161
+ const transactionIndex = hexToNumber(receipt.transactionIndex);
180
162
  const transaction = transactions[transactionIndex];
181
163
  if (transaction === void 0 || !isHexEqual(transaction.hash, receipt.transactionHash)) {
182
164
  throw new Error("Block returned receipt with unexpected transaction hash");
@@ -195,7 +177,7 @@ function defineIndexer(opts) {
195
177
  let expectedLogIndex = 0n;
196
178
  for (const receipt of block.eth_getBlockReceipts) {
197
179
  for (const entry of receipt.logs) {
198
- if (viem.hexToBigInt(entry.logIndex) !== expectedLogIndex) {
180
+ if (hexToBigInt(entry.logIndex) !== expectedLogIndex) {
199
181
  throw new Error("Block returned non-contiguous log indices");
200
182
  }
201
183
  expectedLogIndex++;
@@ -210,17 +192,17 @@ function defineIndexer(opts) {
210
192
  }
211
193
  function calculateTrieRoot(values) {
212
194
  if (values.length === 0) {
213
- return viem.keccak256(viem.toRlp(new Uint8Array(), "bytes"));
195
+ return keccak256(toRlp(new Uint8Array(), "bytes"));
214
196
  }
215
197
  const entries = values.map((value, index) => ({
216
- key: bytesToNibbles(viem.toRlp(quantityToBytes(BigInt(index)), "bytes")),
198
+ key: bytesToNibbles(toRlp(quantityToBytes(BigInt(index)), "bytes")),
217
199
  value
218
200
  }));
219
- return viem.keccak256(viem.toRlp(encodeTrieNode(entries), "bytes"));
201
+ return keccak256(toRlp(encodeTrieNode(entries), "bytes"));
220
202
  }
221
203
  function trieNodeReference(node) {
222
- const encoded = viem.toRlp(node, "bytes");
223
- return encoded.length < 32 ? node : viem.hexToBytes(viem.keccak256(encoded));
204
+ const encoded = toRlp(node, "bytes");
205
+ return encoded.length < 32 ? node : hexToBytes(keccak256(encoded));
224
206
  }
225
207
  function encodeTrieNode(entries, depth = 0) {
226
208
  if (entries.length === 1) {
@@ -258,43 +240,53 @@ function defineIndexer(opts) {
258
240
  return bytes;
259
241
  }
260
242
  function quantityToBytes(value) {
261
- const number = typeof value === "bigint" ? value : viem.hexToBigInt(value);
262
- return number === 0n ? new Uint8Array() : viem.numberToBytes(number);
243
+ const number = typeof value === "bigint" ? value : hexToBigInt(value);
244
+ return number === 0n ? new Uint8Array() : numberToBytes(number);
263
245
  }
264
246
  function bytesToNibbles(value) {
265
247
  return Array.from(value).flatMap((byte) => [byte >> 4, byte & 15]);
266
248
  }
267
249
  function serializeReceipt(receipt) {
268
- const outcome = receipt.status === void 0 ? viem.hexToBytes(receipt.root) : quantityToBytes(receipt.status);
250
+ const outcome = receipt.status === void 0 ? hexToBytes(receipt.root) : quantityToBytes(receipt.status);
269
251
  const fields = [
270
252
  outcome,
271
253
  quantityToBytes(receipt.cumulativeGasUsed),
272
- viem.hexToBytes(receipt.logsBloom),
273
- receipt.logs.map((log2) => [viem.hexToBytes(log2.address), log2.topics.map(viem.hexToBytes), viem.hexToBytes(log2.data)])
254
+ hexToBytes(receipt.logsBloom),
255
+ receipt.logs.map((log2) => [hexToBytes(log2.address), log2.topics.map(hexToBytes), hexToBytes(log2.data)])
274
256
  ];
275
- const encoded = viem.toRlp(fields, "bytes");
276
- if (!viem.isHex(receipt.type)) {
257
+ const encoded = toRlp(fields, "bytes");
258
+ if (!isHex(receipt.type)) {
277
259
  throw new Error(`Unsupported receipt type ${receipt.type}`);
278
260
  }
279
- const type = viem.hexToBigInt(receipt.type);
280
- return type === 0n ? encoded : viem.concatBytes([Uint8Array.of(Number(type)), encoded]);
261
+ const type = hexToBigInt(receipt.type);
262
+ return type === 0n ? encoded : concatBytes([Uint8Array.of(Number(type)), encoded]);
281
263
  }
264
+ const SUPPORTED_TRANSACTION_TYPES = /* @__PURE__ */ new Set([0n, 1n, 2n, 3n, 4n]);
282
265
  function verifyTransactionsRoot(block) {
283
- const transactions = block.eth_getBlockByNumber.transactions.map((transaction) => {
284
- const { input, ...formatted } = viem.formatTransaction(transaction);
266
+ const unsupportedTxs = block.eth_getBlockByNumber.transactions.filter((tx) => {
267
+ return !SUPPORTED_TRANSACTION_TYPES.has(hexToBigInt(tx.type));
268
+ });
269
+ for (const tx of unsupportedTxs) {
270
+ log.warn(`Encountered unsupported tx type for ${tx.hash} on ${block.eth_chainId}`);
271
+ }
272
+ if (unsupportedTxs.length > 0) {
273
+ return log.warn("Unable to verify block `transactionsRoot` because of unsupported tx type");
274
+ }
275
+ const transactions = block.eth_getBlockByNumber.transactions.map((tx) => {
276
+ const { input, ...formatted } = formatTransaction(tx);
285
277
  if (formatted.type === "legacy" && (formatted.v === 27n || formatted.v === 28n)) {
286
278
  formatted.chainId = void 0;
287
279
  }
288
280
  const signature = formatted.type === "legacy" ? { r: formatted.r, s: formatted.s, v: formatted.v } : { r: formatted.r, s: formatted.s, yParity: formatted.yParity };
289
- const serialized = viem.serializeTransaction(
281
+ const serialized = serializeTransaction(
290
282
  { ...formatted, data: input },
291
283
  //
292
284
  signature
293
285
  );
294
- if (!isHexEqual(transaction.hash, viem.keccak256(serialized))) {
295
- throw new Error(`Transaction ${transaction.hash} failed serialization check`);
286
+ if (!isHexEqual(tx.hash, keccak256(serialized))) {
287
+ throw new Error(`Transaction ${tx.hash} failed serialization check`);
296
288
  }
297
- return viem.hexToBytes(serialized);
289
+ return hexToBytes(serialized);
298
290
  });
299
291
  const transactionsRoot = calculateTrieRoot(transactions);
300
292
  if (!isHexEqual(block.eth_getBlockByNumber.transactionsRoot, transactionsRoot)) {
@@ -309,29 +301,29 @@ function defineIndexer(opts) {
309
301
  }
310
302
  for (let index = 0; index < transactions.length; index++) {
311
303
  const expectedIndex = BigInt(index);
312
- if (viem.hexToBigInt(transactions[index].transactionIndex) !== expectedIndex) {
304
+ if (hexToBigInt(transactions[index].transactionIndex) !== expectedIndex) {
313
305
  throw new Error("Block returned non-contiguous transaction indices");
314
306
  }
315
- if (viem.hexToBigInt(receipts[index].transactionIndex) !== expectedIndex) {
307
+ if (hexToBigInt(receipts[index].transactionIndex) !== expectedIndex) {
316
308
  throw new Error("Block returned non-contiguous transaction indices");
317
309
  }
318
310
  for (const entry of receipts[index].logs) {
319
- if (viem.hexToBigInt(entry.transactionIndex) !== expectedIndex) {
311
+ if (hexToBigInt(entry.transactionIndex) !== expectedIndex) {
320
312
  throw new Error("Block returned log with unexpected transaction index");
321
313
  }
322
314
  }
323
315
  }
324
316
  }
325
317
  function verifyTransactionGasUsage(block) {
326
- const gasLimit = viem.hexToBigInt(block.eth_getBlockByNumber.gasLimit);
327
- const blockGasUsed = viem.hexToBigInt(block.eth_getBlockByNumber.gasUsed);
318
+ const gasLimit = hexToBigInt(block.eth_getBlockByNumber.gasLimit);
319
+ const blockGasUsed = hexToBigInt(block.eth_getBlockByNumber.gasUsed);
328
320
  if (blockGasUsed > gasLimit) {
329
321
  throw new Error("Block returned gas used greater than the block gas limit");
330
322
  }
331
323
  let cumulativeGasUsed = 0n;
332
324
  for (const receipt of block.eth_getBlockReceipts) {
333
- cumulativeGasUsed += viem.hexToBigInt(receipt.gasUsed);
334
- if (viem.hexToBigInt(receipt.cumulativeGasUsed) !== cumulativeGasUsed) {
325
+ cumulativeGasUsed += hexToBigInt(receipt.gasUsed);
326
+ if (hexToBigInt(receipt.cumulativeGasUsed) !== cumulativeGasUsed) {
335
327
  throw new Error("Block returned inconsistent cumulative gas used");
336
328
  }
337
329
  if (cumulativeGasUsed > gasLimit) {
@@ -342,13 +334,17 @@ function defineIndexer(opts) {
342
334
  throw new Error("Block returned inconsistent gas used");
343
335
  }
344
336
  }
337
+ const public_getFinalizedHeight = async (chain) => {
338
+ const manifest = await getOrInitManifest(chain);
339
+ return manifest.finalized_block_height;
340
+ };
345
341
  async function getOrInitManifest(chain) {
346
342
  const path = `manifest/v1/${normalizeHex(chain)}`;
347
343
  const manifestRes = await opts.metadataStorage.adapter.get(path);
348
344
  let manifest;
349
345
  if (manifestRes === null) {
350
346
  const block = await getBlockFromChain({ chain, number: "finalized" });
351
- const chainFinalizedHeight = viem.hexToNumber(block.eth_getBlockByNumber.number);
347
+ const chainFinalizedHeight = hexToNumber(block.eth_getBlockByNumber.number);
352
348
  const newManifest = {
353
349
  finalized_block_height: chainFinalizedHeight,
354
350
  finalized_block_hash: block.eth_getBlockByNumber.hash,
@@ -362,39 +358,70 @@ function defineIndexer(opts) {
362
358
  }
363
359
  return manifest;
364
360
  }
365
- const public_getFinalizedHeight = async (chain) => {
366
- const manifest = await getOrInitManifest(chain);
367
- return manifest.finalized_block_height;
361
+ const public_writeUnfinalizedHead = async (head) => {
362
+ log.debug("Received unfinalized head...");
363
+ const blocksStart = Date.now();
364
+ const [block, manifest] = await Promise.all([
365
+ getBlockFromChain(head),
366
+ //
367
+ getOrInitManifest(head.chain)
368
+ ]);
369
+ log.debug(`Loaded block in ${Date.now() - blocksStart}ms`);
370
+ const indexerFinalizedHeight = manifest.finalized_block_height;
371
+ if (hexToNumber(head.number) <= indexerFinalizedHeight) {
372
+ return log.debug("Receiving finalized head, ignoring...");
373
+ }
374
+ const metadata_start = Date.now();
375
+ const chain = normalizeHex(block.eth_chainId);
376
+ const hash = normalizeHex(block.eth_getBlockByNumber.hash);
377
+ const number = normalizeHex(block.eth_getBlockByNumber.number, 16);
378
+ const parentHash = normalizeHex(block.eth_getBlockByNumber.parentHash);
379
+ const blocksKey = `blocks/v1/${chain}/${number}/${hash}/${parentHash}`;
380
+ const blocksValue = await compress(JSON.stringify(block));
381
+ const conditional = { ifNoneMatch: "*" };
382
+ const result = await opts.metadataStorage.adapter.put(blocksKey, blocksValue, conditional).catch((error) => {
383
+ if (error instanceof AdapterError) {
384
+ if (error.tag === "PreconditionFailed") {
385
+ return null;
386
+ }
387
+ }
388
+ throw error;
389
+ });
390
+ if (result === null) {
391
+ return log.debug("Block already persisted to wal, ignoring...");
392
+ }
393
+ log.debug(`Persisted block to wal in ${Date.now() - metadata_start}ms`);
394
+ await writeUnfinalizedBlock(block);
368
395
  };
369
396
  async function writeUnfinalizedBlock(block) {
370
397
  const events_start = Date.now();
371
398
  const events = eventsGroupedByStorageMap.entries().map(async ([storage, grouped_events]) => {
372
399
  const batch = [];
373
- for (const event2 of grouped_events) {
400
+ for (const event of grouped_events) {
374
401
  try {
375
- if (!event2.filters.some((filter) => matchFilter(block, filter))) {
376
- log.debug(`Block matches no filters for event ${event2.id}`);
402
+ if (!event.filters.some((filter) => matchFilter(block, filter))) {
403
+ log.debug(`Block matches no filters for event ${event.id}`);
377
404
  continue;
378
405
  }
379
- const events2 = event2.handler(block);
380
- for (const event3 of events2) {
381
- batch.push(event3);
406
+ const events2 = event.handler(block);
407
+ for (const event2 of events2) {
408
+ batch.push(event2);
382
409
  }
383
410
  } catch (error) {
384
- log.error(`Failed to run your 'handler' for event ${event2.id}`);
411
+ log.error(`Failed to run your 'handler' for event ${event.id}`);
385
412
  throw error;
386
413
  }
387
414
  }
388
415
  if (batch.length > 0) {
389
416
  const start = Date.now();
390
417
  await retry(() => storage.upsert(batch), 2).catch((error) => {
391
- for (const event2 of grouped_events) {
392
- log.error(`Failed to run your 'upsert' handler for event ${event2.id}`);
418
+ for (const event of grouped_events) {
419
+ log.error(`Failed to run your 'upsert' handler for event ${event.id}`);
393
420
  }
394
421
  throw error;
395
422
  });
396
- for (const event2 of grouped_events) {
397
- log.debug(`Recorded ${batch.length} ${event2.id} in ${Date.now() - start}ms`);
423
+ for (const event of grouped_events) {
424
+ log.debug(`Recorded ${batch.length} ${event.id} in ${Date.now() - start}ms`);
398
425
  }
399
426
  }
400
427
  });
@@ -408,84 +435,71 @@ function defineIndexer(opts) {
408
435
  const commitsValue = JSON.stringify({ hello: "world" });
409
436
  await opts.metadataStorage.adapter.put(commitsKey, commitsValue);
410
437
  }
411
- const public_writeUnfinalizedHead = async (head) => {
412
- log.debug("Received unfinalized head...");
413
- const blocksStart = Date.now();
414
- const [block, manifest] = await Promise.all([
415
- getBlockFromChain(head),
438
+ const public_deleteReorganisedHead = async (head) => {
439
+ log.debug(`Received reorganised head ${hexToNumber(head.number)}`);
440
+ const chain = normalizeHex(head.chain);
441
+ const number = normalizeHex(head.number, 16);
442
+ const hash = normalizeHex(head.hash);
443
+ const parentHash = normalizeHex(head.parent_hash);
444
+ const blocksKey = `blocks/v1/${chain}/${number}/${hash}/${parentHash}`;
445
+ const [blocksRes, canonicalBlock] = await Promise.all([
446
+ opts.metadataStorage.adapter.get(blocksKey),
416
447
  //
417
- getOrInitManifest(head.chain)
448
+ getBlockFromChain({ chain: head.chain, number: head.number })
418
449
  ]);
419
- log.debug(`Loaded block in ${Date.now() - blocksStart}ms`);
420
- const indexerFinalizedHeight = manifest.finalized_block_height;
421
- if (viem.hexToNumber(head.number) <= indexerFinalizedHeight) {
422
- return log.debug("Receiving finalized head, ignoring...");
450
+ if (blocksRes === null) {
451
+ return log.debug("Reorganised block never/already processed");
423
452
  }
424
- const metadata_start = Date.now();
425
- const chain = normalizeHex(block.eth_chainId);
426
- const hash = normalizeHex(block.eth_getBlockByNumber.hash);
427
- const number = normalizeHex(block.eth_getBlockByNumber.number, 16);
428
- const parentHash = normalizeHex(block.eth_getBlockByNumber.parentHash);
429
- const blocksKey = `blocks/v1/${chain}/${number}/${hash}/${parentHash}`;
430
- const blocksValue = await compress(JSON.stringify(block));
431
- const conditional = { ifNoneMatch: "*" };
432
- const result = await opts.metadataStorage.adapter.put(blocksKey, blocksValue, conditional).catch((error) => {
433
- if (error instanceof AdapterError) {
434
- if (error.tag === "PreconditionFailed") {
435
- return null;
436
- }
437
- }
438
- throw error;
439
- });
440
- if (result === null) {
441
- return log.debug("Block already persisted to wal, ignoring...");
453
+ const decompressedBlock = await decompress(blocksRes.body);
454
+ const storedBlock = JSON.parse(decompressedBlock);
455
+ if (isHexEqual(head.hash, canonicalBlock.eth_getBlockByNumber.hash)) {
456
+ throw new Error("Attempted to delete canonical block");
442
457
  }
443
- log.debug(`Persisted block to wal in ${Date.now() - metadata_start}ms`);
444
- await writeUnfinalizedBlock(block);
458
+ await deleteReorganisedBlocksAndWriteCanonicalBlock([storedBlock], canonicalBlock);
445
459
  };
446
460
  async function deleteReorganisedBlocksAndWriteCanonicalBlock(reorganised, canonical) {
447
- const deletes = allEvents.map(async (event2) => {
461
+ const deletes = allEvents.map(async (event) => {
448
462
  const batch = [];
449
463
  try {
450
464
  for (const block of reorganised) {
451
- const events = event2.handler(block);
452
- for (const event3 of events) {
453
- batch.push(event3);
465
+ const events = event.handler(block);
466
+ for (const event2 of events) {
467
+ batch.push(event2);
454
468
  }
455
469
  }
456
470
  } catch (error) {
457
- log.error(`Failed to run your 'handler' for event ${event2.id}`);
471
+ log.error(`Failed to run your 'handler' for event ${event.id}`);
458
472
  throw error;
459
473
  }
460
474
  if (batch.length === 0) {
461
475
  return;
462
476
  }
463
- await retry(() => event2.storage.delete(batch), 2).catch((error) => {
464
- log.error(`Failed to run your 'delete' handler for event ${event2.id}`);
477
+ await retry(() => event.storage.delete(batch), 2).catch((error) => {
478
+ log.error(`Failed to run your 'delete' handler for event ${event.id}`);
465
479
  throw error;
466
480
  });
467
481
  });
468
482
  await Promise.all(deletes);
469
483
  const upserts = eventsGroupedByStorageMap.entries().map(async ([storage, grouped_events]) => {
470
484
  const batch = [];
471
- for (const event2 of grouped_events) {
485
+ for (const event of grouped_events) {
472
486
  try {
473
- if (!event2.filters.some((filter) => matchFilter(canonical, filter))) {
487
+ if (!event.filters.some((filter) => matchFilter(canonical, filter))) {
474
488
  continue;
475
489
  }
476
- const events = event2.handler(canonical);
477
- for (const event3 of events) {
478
- batch.push(event3);
490
+ const events = event.handler(canonical);
491
+ for (const event2 of events) {
492
+ batch.push(event2);
479
493
  }
480
494
  } catch (error) {
481
- log.error(`Failed to run your 'handler' for event ${event2.id}`);
495
+ log.error(`Failed to run your 'handler' for event ${event.id}`);
482
496
  throw error;
483
497
  }
484
498
  }
485
499
  if (batch.length > 0) {
486
500
  await retry(() => storage.upsert(batch), 2).catch((error) => {
487
- for (const event2 of grouped_events) {
488
- log.error(`Failed to run your 'upsert' handler for event ${event2.id}`);
501
+ for (const event of grouped_events) {
502
+ log.error(`Failed to run your 'upsert' handler for event ${event.id}`);
489
503
  }
490
504
  throw error;
491
505
  });
@@ -493,55 +507,38 @@ function defineIndexer(opts) {
493
507
  });
494
508
  await Promise.all(upserts);
495
509
  }
496
- const public_deleteReorganisedHead = async (head) => {
497
- log.debug(`Received reorganised head ${viem.hexToNumber(head.number)}`);
498
- const chain = normalizeHex(head.chain);
499
- const number = normalizeHex(head.number, 16);
500
- const hash = normalizeHex(head.hash);
501
- const parentHash = normalizeHex(head.parent_hash);
502
- const blocksKey = `blocks/v1/${chain}/${number}/${hash}/${parentHash}`;
503
- const [blocksRes, canonicalBlock] = await Promise.all([
504
- opts.metadataStorage.adapter.get(blocksKey),
510
+ const public_writeFinalizedHeight = async (chain) => {
511
+ const path = `cache/v1/${normalizeHex(chain)}`;
512
+ const [cached, block] = await Promise.all([
513
+ getChainCache(chain),
514
+ getBlockFromChain({ chain, number: "finalized" })
505
515
  //
506
- getBlockFromChain({ chain: head.chain, number: head.number })
507
516
  ]);
508
- if (blocksRes === null) {
509
- return log.debug("Reorganised block never/already processed");
510
- }
511
- const decompressedBlock = await decompress(blocksRes.body);
512
- const storedBlock = JSON.parse(decompressedBlock);
513
- if (isHexEqual(head.hash, canonicalBlock.eth_getBlockByNumber.hash)) {
514
- throw new Error("Attempted to delete canonical block");
517
+ const cache = {
518
+ chain_finalized_height: hexToNumber(block.eth_getBlockByNumber.number),
519
+ chain_finalized_block_hash: block.eth_getBlockByNumber.hash,
520
+ chain_finalized_parent_hash: block.eth_getBlockByNumber.parentHash
521
+ };
522
+ if (cached.chain_finalized_height >= cache.chain_finalized_height) {
523
+ return;
515
524
  }
516
- await deleteReorganisedBlocksAndWriteCanonicalBlock([storedBlock], canonicalBlock);
525
+ await opts.metadataStorage.adapter.put(path, JSON.stringify(cache));
517
526
  };
518
- async function writeFinalizedBlock(block, actions) {
519
- if (actions.length === 0) {
520
- return;
527
+ async function getChainCache(chain) {
528
+ const path = `cache/v1/${normalizeHex(chain)}`;
529
+ const cacheRes = await opts.metadataStorage.adapter.get(path);
530
+ if (cacheRes === null) {
531
+ const block = await getBlockFromChain({ chain, number: "finalized" });
532
+ const cache2 = {
533
+ chain_finalized_height: hexToNumber(block.eth_getBlockByNumber.number),
534
+ chain_finalized_block_hash: block.eth_getBlockByNumber.hash,
535
+ chain_finalized_parent_hash: block.eth_getBlockByNumber.parentHash
536
+ };
537
+ await opts.metadataStorage.adapter.put(path, JSON.stringify(cache2));
538
+ return cache2;
521
539
  }
522
- const chain = normalizeHex(block.eth_chainId);
523
- const number = normalizeHex(block.eth_getBlockByNumber.number, 16);
524
- const hash = normalizeHex(block.eth_getBlockByNumber.hash);
525
- const parentHash = normalizeHex(block.eth_getBlockByNumber.parentHash);
526
- const prefix = `commits/v1/${chain}/${number}/${hash}/${parentHash}`;
527
- const promises = actions.map(async (action2) => {
528
- const events = action2.event.filters.some((filter) => matchFilter(block, filter)) ? action2.event.handler(block) : [];
529
- const promises2 = events.map(async (event2) => {
530
- await retry(() => action2.handler(event2), 2).catch((error) => {
531
- log.error(`Failed to execute action ${action2.id}`);
532
- throw error;
533
- });
534
- });
535
- const results = await Promise.allSettled(promises2);
536
- const failure = results.find((result) => result.status === "rejected");
537
- if (failure !== void 0) {
538
- throw failure.reason;
539
- }
540
- const commitsKey = `${prefix}/action/${action2.id}`;
541
- const commitsValue = JSON.stringify({ hello: "world" });
542
- await opts.metadataStorage.adapter.put(commitsKey, commitsValue);
543
- });
544
- await Promise.all(promises);
540
+ const cache = JSON.parse(decoder.decode(cacheRes.body));
541
+ return cache;
545
542
  }
546
543
  const public_writeFinalizedHead = async (head) => {
547
544
  log.debug("Received finalized head...");
@@ -549,24 +546,59 @@ function defineIndexer(opts) {
549
546
  return;
550
547
  }
551
548
  const blocksStart = Date.now();
552
- const [block, manifest, chainFinalizedBlock] = await Promise.all([
553
- getBlockFromChain(head),
549
+ const [cache, blockFromMetadata, manifest, heads] = await Promise.all([
550
+ getChainCache(head.chain),
551
+ //
552
+ getBlockFromMetadata(head),
554
553
  getOrInitManifest(head.chain),
555
- getBlockFromChain({ chain: head.chain, number: "finalized" })
554
+ listUnfinalizedHeads(head.chain)
556
555
  ]);
557
- log.debug(`Loaded block in ${Date.now() - blocksStart}ms`);
558
- const receivedHeight = viem.hexToNumber(head.number);
556
+ const receivedHeight = hexToNumber(head.number);
557
+ const chainFinalizedHeight = cache.chain_finalized_height;
559
558
  const indexerFinalizedHeight = manifest.finalized_block_height;
560
- const chainFinalizedHeight = viem.hexToNumber(chainFinalizedBlock.eth_getBlockByNumber.number);
561
559
  if (receivedHeight <= indexerFinalizedHeight) {
562
560
  return log.debug(`Received finalized head (${receivedHeight}) below indexer height (${indexerFinalizedHeight})`);
563
561
  }
564
562
  if (receivedHeight > chainFinalizedHeight) {
565
- return log.error(`Received finalized head (${receivedHeight}) that has not finalized (${chainFinalizedHeight})`);
563
+ return log.error(`Received finalized head (${receivedHeight}) that has not actually finalized (${chainFinalizedHeight})`);
566
564
  }
565
+ log.debug(`Loaded block in ${Date.now() - blocksStart}ms`);
566
+ const canonical = iife(() => {
567
+ let matchesHead = false;
568
+ let canonicalHead = {
569
+ chain: head.chain,
570
+ hash: cache.chain_finalized_block_hash,
571
+ parent_hash: cache.chain_finalized_parent_hash,
572
+ number: numberToHex(cache.chain_finalized_height)
573
+ };
574
+ const receivedHeight2 = hexToNumber(head.number);
575
+ for (let number2 = cache.chain_finalized_height; number2 > manifest.finalized_block_height; number2--) {
576
+ if (number2 === receivedHeight2) {
577
+ matchesHead = isHexEqual(head.hash, canonicalHead.hash) && isHexEqual(head.parent_hash, canonicalHead.parent_hash);
578
+ if (!matchesHead) {
579
+ return false;
580
+ }
581
+ }
582
+ if (number2 === manifest.finalized_block_height + 1) {
583
+ return matchesHead && isHexEqual(canonicalHead.parent_hash, manifest.finalized_block_hash);
584
+ }
585
+ const parent = heads.get(`${number2 - 1}/${normalizeHex(canonicalHead.parent_hash)}`);
586
+ if (parent === void 0) {
587
+ return false;
588
+ }
589
+ canonicalHead = parent;
590
+ }
591
+ return false;
592
+ });
593
+ const block = await iife(async () => {
594
+ if (canonical === false || blockFromMetadata === null) {
595
+ return await getBlockFromChain(head);
596
+ }
597
+ return blockFromMetadata;
598
+ });
567
599
  const chain = normalizeHex(block.eth_chainId);
568
- const number = normalizeHex(block.eth_getBlockByNumber.number, 16);
569
600
  const hash = normalizeHex(block.eth_getBlockByNumber.hash);
601
+ const number = normalizeHex(block.eth_getBlockByNumber.number, 16);
570
602
  const parentHash = normalizeHex(block.eth_getBlockByNumber.parentHash);
571
603
  const finalizedKey = `finalized/v1/${chain}/${number}/${hash}/${parentHash}`;
572
604
  const finalizedValue = JSON.stringify({ hello: "world" });
@@ -582,68 +614,62 @@ function defineIndexer(opts) {
582
614
  }
583
615
  await writeFinalizedBlock(block, allActions);
584
616
  };
585
- const FINALIZATION_BATCH_SIZE = 32;
586
- const LEASE_DURATION_MS = 60 * 1e3;
587
- async function getBlocksProcessedAndGarbageCollect(chain, finalizedHeight) {
588
- while (true) {
589
- const blocksKey = `blocks/v1/${normalizeHex(chain)}/`;
590
- const blocks = await opts.metadataStorage.adapter.list({ prefix: blocksKey, limit: 1e3 });
591
- if (blocks.keys.length === 0) {
592
- return [];
593
- }
594
- const garbageCollectionKeys = blocks.keys.filter((key) => {
595
- const [_, __, ___, number] = key.split("/");
596
- return viem.hexToNumber(number) <= finalizedHeight;
597
- });
598
- if (garbageCollectionKeys.length === 0) {
599
- return blocks.keys.map((key) => {
600
- const [_, __, ___, number, hash, parent_hash] = key.split("/");
601
- return { chain, number, hash, parent_hash };
602
- });
603
- }
604
- const garbageCollectionPromises = garbageCollectionKeys.map(async (key) => {
605
- await opts.metadataStorage.adapter.delete(key);
606
- });
607
- await Promise.all(garbageCollectionPromises);
608
- }
609
- }
610
- async function getCommitsAndGarbageCollect(chain, finalizedHeight) {
611
- while (true) {
612
- const commitsKey = `commits/v1/${normalizeHex(chain)}/`;
613
- const commits = await opts.metadataStorage.adapter.list({ prefix: commitsKey, limit: 1e3 });
614
- if (commits.keys.length === 0) {
615
- return [];
616
- }
617
- const garbageCollectionKeys = commits.keys.filter((key) => {
618
- const [_, __, ___, number] = key.split("/");
619
- return viem.hexToNumber(number) <= finalizedHeight;
620
- });
621
- if (garbageCollectionKeys.length === 0) {
622
- return commits.keys.map((key) => {
623
- const [_, __, ___, number, hash, parent_hash, type, id] = key.split("/");
624
- return { chain, number, hash, parent_hash, type, id };
625
- });
626
- }
627
- const garbageCollectionPromises = garbageCollectionKeys.map(async (key) => {
628
- await opts.metadataStorage.adapter.delete(key);
629
- });
630
- await Promise.all(garbageCollectionPromises);
631
- }
632
- }
633
- async function getBlockFromMetadataOrChain(head) {
617
+ async function getBlockFromMetadata(head) {
618
+ const hash = normalizeHex(head.hash);
634
619
  const chain = normalizeHex(head.chain);
635
620
  const number = normalizeHex(head.number, 16);
636
- const hash = normalizeHex(head.hash);
637
621
  const parentHash = normalizeHex(head.parent_hash);
638
622
  const prefix = `blocks/v1/${chain}/${number}/${hash}/${parentHash}`;
639
623
  const object = await opts.metadataStorage.adapter.get(prefix);
640
- if (object !== null) {
641
- const block = await decompress(object.body);
642
- const parsed = JSON.parse(block);
643
- return parsed;
624
+ if (object === null) {
625
+ return null;
626
+ }
627
+ const block = await decompress(object.body);
628
+ const parsed = JSON.parse(block);
629
+ return parsed;
630
+ }
631
+ async function listUnfinalizedHeads(chain) {
632
+ const blocks = await opts.metadataStorage.adapter.list({
633
+ prefix: `blocks/v1/${normalizeHex(chain)}/`,
634
+ limit: 1e3
635
+ });
636
+ const heads = /* @__PURE__ */ new Map();
637
+ for (const key of blocks.keys) {
638
+ const [_, __, chain2, number, hash, parent_hash] = key.split("/");
639
+ heads.set(`${hexToNumber(number)}/${normalizeHex(hash)}`, { chain: chain2, number, hash, parent_hash });
644
640
  }
645
- return await getBlockFromChain(head);
641
+ return heads;
642
+ }
643
+ async function writeFinalizedBlock(block, actions) {
644
+ if (actions.length === 0) {
645
+ return;
646
+ }
647
+ const chain = normalizeHex(block.eth_chainId);
648
+ const number = normalizeHex(block.eth_getBlockByNumber.number, 16);
649
+ const hash = normalizeHex(block.eth_getBlockByNumber.hash);
650
+ const parentHash = normalizeHex(block.eth_getBlockByNumber.parentHash);
651
+ const prefix = `commits/v1/${chain}/${number}/${hash}/${parentHash}`;
652
+ const promises = actions.map(async (action) => {
653
+ const events = action.event.filters.some((filter) => matchFilter(block, filter)) ? action.event.handler(block) : [];
654
+ const promises2 = events.map(async (event) => {
655
+ await retry(() => action.handler(event), 2).catch((error) => {
656
+ log.error(`Failed to execute action ${action.id}`);
657
+ throw error;
658
+ });
659
+ });
660
+ const results = await Promise.allSettled(promises2);
661
+ const failure = results.find((result) => result.status === "rejected");
662
+ if (failure !== void 0) {
663
+ throw failure.reason;
664
+ }
665
+ const commitsKey = `${prefix}/action/${action.id}`;
666
+ const commitsValue = JSON.stringify({ hello: "world" });
667
+ await opts.metadataStorage.adapter.put(commitsKey, commitsValue);
668
+ });
669
+ await Promise.all(promises);
646
670
  }
671
+ const FINALIZATION_BATCH_SIZE = 32;
672
+ const LEASE_DURATION_MS = 60 * 1e3;
647
673
  const public_finalize = async (chain) => {
648
674
  const manifestKey = `manifest/v1/${normalizeHex(chain)}`;
649
675
  const [chainFinalizedBlock, manifestGetRes] = await Promise.all([
@@ -651,7 +677,7 @@ function defineIndexer(opts) {
651
677
  opts.metadataStorage.adapter.get(manifestKey)
652
678
  //
653
679
  ]);
654
- const chainFinalizedHeight = viem.hexToNumber(chainFinalizedBlock.eth_getBlockByNumber.number);
680
+ const chainFinalizedHeight = hexToNumber(chainFinalizedBlock.eth_getBlockByNumber.number);
655
681
  if (manifestGetRes === null) {
656
682
  log.debug("No manifest file found");
657
683
  const manifest2 = {
@@ -698,7 +724,7 @@ function defineIndexer(opts) {
698
724
  const nextFinalizedHeight = Math.min(chainFinalizedHeight, indexerFinalizedBlockHeight + FINALIZATION_BATCH_SIZE);
699
725
  const finalizationBatchSize = nextFinalizedHeight - indexerFinalizedBlockHeight;
700
726
  const [nextFinalizedBlock, blocksProcessed] = await Promise.all([
701
- getBlockFromChain({ chain, number: viem.numberToHex(nextFinalizedHeight) }),
727
+ getBlockFromChain({ chain, number: numberToHex(nextFinalizedHeight) }),
702
728
  getBlocksProcessedAndGarbageCollect(chain, indexerFinalizedBlockHeight)
703
729
  ]);
704
730
  const nextFinalizedHead = {
@@ -708,7 +734,7 @@ function defineIndexer(opts) {
708
734
  parent_hash: nextFinalizedBlock.eth_getBlockByNumber.parentHash
709
735
  };
710
736
  const nextFinalizedBlockProcessed = blocksProcessed.some((block) => {
711
- return isHexEqual(block.hash, nextFinalizedHead.hash) && isHexEqual(block.parent_hash, nextFinalizedHead.parent_hash) && viem.hexToNumber(block.number) === viem.hexToNumber(nextFinalizedHead.number);
737
+ return isHexEqual(block.hash, nextFinalizedHead.hash) && isHexEqual(block.parent_hash, nextFinalizedHead.parent_hash) && hexToNumber(block.number) === hexToNumber(nextFinalizedHead.number);
712
738
  });
713
739
  if (nextFinalizedBlockProcessed === false) {
714
740
  await Promise.all([
@@ -723,7 +749,7 @@ function defineIndexer(opts) {
723
749
  for (let index = 1; index < finalizationBatchSize; index++) {
724
750
  const number = nextFinalizedHeight - index;
725
751
  const blocksProcessedForHeight = blocksProcessed.filter((block) => {
726
- return viem.hexToNumber(block.number) === number;
752
+ return hexToNumber(block.number) === number;
727
753
  });
728
754
  const canonicalHead = blocksProcessedForHeight.find((block) => {
729
755
  return isHexEqual(block.hash, parentHash);
@@ -734,7 +760,7 @@ function defineIndexer(opts) {
734
760
  continue;
735
761
  }
736
762
  log.debug("Canonical head never processed, loading from chain...");
737
- const canonicalBlock = await getBlockFromChain({ chain, number: viem.numberToHex(number) });
763
+ const canonicalBlock = await getBlockFromChain({ chain, number: numberToHex(number) });
738
764
  const head = {
739
765
  chain,
740
766
  hash: canonicalBlock.eth_getBlockByNumber.hash,
@@ -761,17 +787,17 @@ function defineIndexer(opts) {
761
787
  const commits = await getCommitsAndGarbageCollect(chain, indexerFinalizedBlockHeight);
762
788
  for (const canonicalHead of canonicalHeads) {
763
789
  const blocksProcessedForHeight = blocksProcessed.filter((head) => {
764
- return viem.hexToNumber(canonicalHead.number) === viem.hexToNumber(head.number);
790
+ return hexToNumber(canonicalHead.number) === hexToNumber(head.number);
765
791
  });
766
792
  const processedOnlyCanonicalBlock = blocksProcessedForHeight.every((head) => {
767
793
  return isHexEqual(canonicalHead.hash, head.hash) && isHexEqual(canonicalHead.parent_hash, head.parent_hash);
768
794
  });
769
795
  const eventsCommittedForHeight = commits.some((commit) => {
770
- return commit.type === void 0 && isHexEqual(canonicalHead.hash, commit.hash) && isHexEqual(canonicalHead.parent_hash, commit.parent_hash) && viem.hexToNumber(canonicalHead.number) === viem.hexToNumber(commit.number);
796
+ return commit.type === void 0 && isHexEqual(canonicalHead.hash, commit.hash) && isHexEqual(canonicalHead.parent_hash, commit.parent_hash) && hexToNumber(canonicalHead.number) === hexToNumber(commit.number);
771
797
  });
772
- const actionsWithoutCommit = allActions.filter((action2) => {
798
+ const actionsWithoutCommit = allActions.filter((action) => {
773
799
  const commitExists = commits.some((commit) => {
774
- return commit.id === action2.id && commit.type === "action" && isHexEqual(canonicalHead.hash, commit.hash) && isHexEqual(canonicalHead.parent_hash, commit.parent_hash) && viem.hexToNumber(canonicalHead.number) === viem.hexToNumber(commit.number);
800
+ return commit.id === action.id && commit.type === "action" && isHexEqual(canonicalHead.hash, commit.hash) && isHexEqual(canonicalHead.parent_hash, commit.parent_hash) && hexToNumber(canonicalHead.number) === hexToNumber(commit.number);
775
801
  });
776
802
  return !commitExists;
777
803
  });
@@ -827,6 +853,59 @@ function defineIndexer(opts) {
827
853
  }
828
854
  log.debug("Indexer finalized, returning...");
829
855
  };
856
+ async function getBlocksProcessedAndGarbageCollect(chain, finalizedHeight) {
857
+ while (true) {
858
+ const blocksKey = `blocks/v1/${normalizeHex(chain)}/`;
859
+ const blocks = await opts.metadataStorage.adapter.list({ prefix: blocksKey, limit: 1e3 });
860
+ if (blocks.keys.length === 0) {
861
+ return [];
862
+ }
863
+ const garbageCollectionKeys = blocks.keys.filter((key) => {
864
+ const [_, __, ___, number] = key.split("/");
865
+ return hexToNumber(number) <= finalizedHeight;
866
+ });
867
+ if (garbageCollectionKeys.length === 0) {
868
+ return blocks.keys.map((key) => {
869
+ const [_, __, ___, number, hash, parent_hash] = key.split("/");
870
+ return { chain, number, hash, parent_hash };
871
+ });
872
+ }
873
+ const garbageCollectionPromises = garbageCollectionKeys.map(async (key) => {
874
+ await opts.metadataStorage.adapter.delete(key);
875
+ });
876
+ await Promise.all(garbageCollectionPromises);
877
+ }
878
+ }
879
+ async function getCommitsAndGarbageCollect(chain, finalizedHeight) {
880
+ while (true) {
881
+ const commitsKey = `commits/v1/${normalizeHex(chain)}/`;
882
+ const commits = await opts.metadataStorage.adapter.list({ prefix: commitsKey, limit: 1e3 });
883
+ if (commits.keys.length === 0) {
884
+ return [];
885
+ }
886
+ const garbageCollectionKeys = commits.keys.filter((key) => {
887
+ const [_, __, ___, number] = key.split("/");
888
+ return hexToNumber(number) <= finalizedHeight;
889
+ });
890
+ if (garbageCollectionKeys.length === 0) {
891
+ return commits.keys.map((key) => {
892
+ const [_, __, ___, number, hash, parent_hash, type, id] = key.split("/");
893
+ return { chain, number, hash, parent_hash, type, id };
894
+ });
895
+ }
896
+ const garbageCollectionPromises = garbageCollectionKeys.map(async (key) => {
897
+ await opts.metadataStorage.adapter.delete(key);
898
+ });
899
+ await Promise.all(garbageCollectionPromises);
900
+ }
901
+ }
902
+ async function getBlockFromMetadataOrChain(head) {
903
+ const metadata = await getBlockFromMetadata(head);
904
+ if (metadata === null) {
905
+ return await getBlockFromChain(head);
906
+ }
907
+ return metadata;
908
+ }
830
909
  const private_getMetadata = async () => {
831
910
  return {
832
911
  version,
@@ -834,8 +913,8 @@ function defineIndexer(opts) {
834
913
  };
835
914
  };
836
915
  const private_getEvents = async () => {
837
- return allEvents.map((event2) => {
838
- const filters = event2.filters.map((filter) => {
916
+ return allEvents.map((event) => {
917
+ const filters = event.filters.map((filter) => {
839
918
  return {
840
919
  chain: filter.chain,
841
920
  event: filter.event,
@@ -844,7 +923,7 @@ function defineIndexer(opts) {
844
923
  from_block: filter.fromBlock
845
924
  };
846
925
  });
847
- return { id: event2.id, filters };
926
+ return { id: event.id, filters };
848
927
  });
849
928
  };
850
929
  const ignored = [
@@ -890,26 +969,40 @@ function defineIndexer(opts) {
890
969
  };
891
970
  const IncompleteBlockError = createException("Received block with missing required property");
892
971
  const private_writeEvents = async (params) => {
893
- if (allEvents.length === 0) return { failures: [] };
894
- const relevant_events = allEvents.filter((event2) => params.events.includes(event2.id));
895
- if (relevant_events.length === 0) return { failures: [] };
896
- if (params.events.length === 0) return { failures: [] };
972
+ if (allEvents.length === 0) {
973
+ return { failures: [] };
974
+ }
975
+ const relevantEvents = allEvents.filter((event) => params.events.includes(event.id));
976
+ if (relevantEvents.length === 0) {
977
+ return { failures: [] };
978
+ }
979
+ if (params.events.length === 0) {
980
+ return { failures: [] };
981
+ }
897
982
  const failures = {};
898
- let accessed_undefined_key = false;
899
- const proxied_blocks = getWriteEventsProxy(params.blocks, () => {
900
- accessed_undefined_key = true;
983
+ let accessedUndefinedKey = false;
984
+ const proxiedBlocks = getWriteEventsProxy(params.blocks, () => {
985
+ accessedUndefinedKey = true;
901
986
  throw new Error(IncompleteBlockError);
902
987
  });
903
988
  const promises = eventsGroupedByStorageMap.entries().map(async ([storage, grouped_events]) => {
904
989
  const batch = [];
905
- for (const event2 of grouped_events) {
906
- if (!params.events.includes(event2.id)) continue;
907
- for (const block of proxied_blocks) {
990
+ for (const event of grouped_events) {
991
+ if (!params.events.includes(event.id)) {
992
+ continue;
993
+ }
994
+ for (const block of proxiedBlocks) {
908
995
  try {
909
- if (!event2.filters.some((filter) => matchFilter(block, filter))) continue;
910
- const events = event2.handler(block);
911
- if (accessed_undefined_key) throw new Error(IncompleteBlockError);
912
- for (const event3 of events) batch.push(event3);
996
+ if (!event.filters.some((filter) => matchFilter(block, filter))) {
997
+ continue;
998
+ }
999
+ const events = event.handler(block);
1000
+ if (accessedUndefinedKey) {
1001
+ throw new Error(IncompleteBlockError);
1002
+ }
1003
+ for (const event2 of events) {
1004
+ batch.push(event2);
1005
+ }
913
1006
  } catch (error) {
914
1007
  const status = catchException(error, IncompleteBlockError) ? "incomplete_error" : "handler_error";
915
1008
  if (status === "handler_error") {
@@ -917,9 +1010,9 @@ function defineIndexer(opts) {
917
1010
  log.error(error.message);
918
1011
  }
919
1012
  }
920
- failures[event2.id + block.eth_getBlockByNumber.number] ??= {
1013
+ failures[event.id + block.eth_getBlockByNumber.number] ??= {
921
1014
  status,
922
- event_id: event2.id,
1015
+ event_id: event.id,
923
1016
  chain: block.eth_chainId,
924
1017
  hash: block.eth_getBlockByNumber.hash,
925
1018
  number: block.eth_getBlockByNumber.number,
@@ -929,11 +1022,15 @@ function defineIndexer(opts) {
929
1022
  }
930
1023
  }
931
1024
  }
932
- if (batch.length === 0) return;
1025
+ if (batch.length === 0) {
1026
+ return;
1027
+ }
933
1028
  const start = Date.now();
934
1029
  try {
935
1030
  await retry(() => storage.upsert(batch), 2);
936
- if (accessed_undefined_key) throw new Error(IncompleteBlockError);
1031
+ if (accessedUndefinedKey) {
1032
+ throw new Error(IncompleteBlockError);
1033
+ }
937
1034
  } catch (error) {
938
1035
  const status = catchException(error, IncompleteBlockError) ? "incomplete_error" : "upsert_error";
939
1036
  if (status === "upsert_error") {
@@ -941,12 +1038,14 @@ function defineIndexer(opts) {
941
1038
  log.error(error.message);
942
1039
  }
943
1040
  }
944
- for (const event2 of relevant_events) {
945
- for (const block of proxied_blocks) {
946
- if (!event2.filters.some((filter) => matchFilter(block, filter))) continue;
947
- failures[event2.id + block.eth_getBlockByNumber.number] ??= {
1041
+ for (const event of relevantEvents) {
1042
+ for (const block of proxiedBlocks) {
1043
+ if (!event.filters.some((filter) => matchFilter(block, filter))) {
1044
+ continue;
1045
+ }
1046
+ failures[event.id + block.eth_getBlockByNumber.number] ??= {
948
1047
  status,
949
- event_id: event2.id,
1048
+ event_id: event.id,
950
1049
  chain: block.eth_chainId,
951
1050
  hash: block.eth_getBlockByNumber.hash,
952
1051
  number: block.eth_getBlockByNumber.number,
@@ -957,13 +1056,13 @@ function defineIndexer(opts) {
957
1056
  }
958
1057
  }
959
1058
  const stop = Date.now() - start;
960
- for (const event2 of relevant_events) {
961
- log.debug(`Recorded ${batch.length} ${event2.id} in ${stop}ms`);
1059
+ for (const event of relevantEvents) {
1060
+ log.debug(`Recorded ${batch.length} ${event.id} in ${stop}ms`);
962
1061
  }
963
1062
  });
964
1063
  await Promise.all(promises);
965
- const failures_array = Object.values(failures);
966
- return { failures: failures_array };
1064
+ const failuresArray = Object.values(failures);
1065
+ return { failures: failuresArray };
967
1066
  };
968
1067
  const private_writeEventsAndGetKeys = async (params) => {
969
1068
  if (allEvents.length === 0) {
@@ -972,7 +1071,7 @@ function defineIndexer(opts) {
972
1071
  if (params.events.length === 0) {
973
1072
  return { results: [], keys: [] };
974
1073
  }
975
- const relevant_events = allEvents.filter((event2) => params.events.includes(event2.id));
1074
+ const relevant_events = allEvents.filter((event) => params.events.includes(event.id));
976
1075
  if (relevant_events.length === 0) {
977
1076
  return { results: [], keys: [] };
978
1077
  }
@@ -1002,20 +1101,20 @@ function defineIndexer(opts) {
1002
1101
  }
1003
1102
  const proxy = new Proxy(block, createProxyHandler(""));
1004
1103
  await Promise.all(
1005
- relevant_events.map(async (event2) => {
1006
- if (!event2.filters.some((filter) => matchFilter(proxy, filter))) {
1104
+ relevant_events.map(async (event) => {
1105
+ if (!event.filters.some((filter) => matchFilter(proxy, filter))) {
1007
1106
  return;
1008
1107
  }
1009
1108
  let events = [];
1010
1109
  try {
1011
- events = event2.handler(proxy);
1110
+ events = event.handler(proxy);
1012
1111
  } catch (error) {
1013
1112
  if (error instanceof Error) {
1014
1113
  log.error(error.message);
1015
1114
  }
1016
- results[event2.id + proxy.eth_getBlockByNumber.number] = {
1115
+ results[event.id + proxy.eth_getBlockByNumber.number] = {
1017
1116
  status: "handler_error",
1018
- event_id: event2.id,
1117
+ event_id: event.id,
1019
1118
  chain: proxy.eth_chainId,
1020
1119
  hash: proxy.eth_getBlockByNumber.hash,
1021
1120
  number: proxy.eth_getBlockByNumber.number,
@@ -1025,9 +1124,9 @@ function defineIndexer(opts) {
1025
1124
  return;
1026
1125
  }
1027
1126
  if (events.length === 0) {
1028
- results[event2.id + proxy.eth_getBlockByNumber.number] = {
1127
+ results[event.id + proxy.eth_getBlockByNumber.number] = {
1029
1128
  status: "ok",
1030
- event_id: event2.id,
1129
+ event_id: event.id,
1031
1130
  chain: proxy.eth_chainId,
1032
1131
  hash: proxy.eth_getBlockByNumber.hash,
1033
1132
  number: proxy.eth_getBlockByNumber.number,
@@ -1038,15 +1137,15 @@ function defineIndexer(opts) {
1038
1137
  }
1039
1138
  try {
1040
1139
  const start = Date.now();
1041
- await event2.storage.upsert(events);
1042
- log.debug(`Recorded ${events.length} ${event2.id} in ${Date.now() - start}ms`);
1140
+ await event.storage.upsert(events);
1141
+ log.debug(`Recorded ${events.length} ${event.id} in ${Date.now() - start}ms`);
1043
1142
  } catch (error) {
1044
1143
  if (error instanceof Error) {
1045
1144
  log.error(error.message);
1046
1145
  }
1047
- results[event2.id + proxy.eth_getBlockByNumber.number] = {
1146
+ results[event.id + proxy.eth_getBlockByNumber.number] = {
1048
1147
  status: "upsert_error",
1049
- event_id: event2.id,
1148
+ event_id: event.id,
1050
1149
  chain: proxy.eth_chainId,
1051
1150
  hash: proxy.eth_getBlockByNumber.hash,
1052
1151
  number: proxy.eth_getBlockByNumber.number,
@@ -1055,9 +1154,9 @@ function defineIndexer(opts) {
1055
1154
  };
1056
1155
  return;
1057
1156
  }
1058
- results[event2.id + proxy.eth_getBlockByNumber.number] = {
1157
+ results[event.id + proxy.eth_getBlockByNumber.number] = {
1059
1158
  status: "ok",
1060
- event_id: event2.id,
1159
+ event_id: event.id,
1061
1160
  chain: proxy.eth_chainId,
1062
1161
  hash: proxy.eth_getBlockByNumber.hash,
1063
1162
  number: proxy.eth_getBlockByNumber.number,
@@ -1075,52 +1174,20 @@ function defineIndexer(opts) {
1075
1174
  });
1076
1175
  return { results: results_array, keys: filtered_keys };
1077
1176
  };
1078
- const event = (event2) => {
1079
- if (!/^[A-Za-z0-9_-]+$/.test(event2.id)) {
1080
- throw new Error(`Invalid event id \`${event2.id}\`. Only characters A-Z, a-z, 0-9, underscores, and hyphens are permitted.`);
1081
- }
1082
- allEvents.push(event2);
1083
- const group = eventsGroupedByStorageMap.get(event2.storage) ?? [];
1084
- group.push(event2);
1085
- eventsGroupedByStorageMap.set(event2.storage, group);
1086
- return event2;
1087
- };
1088
- const action = (action2) => {
1089
- if (!/^[A-Za-z0-9_-]+$/.test(action2.id)) {
1090
- throw new Error(`Invalid action id \`${action2.id}\`. Only characters A-Z, a-z, 0-9, underscores, and hyphens are permitted.`);
1091
- }
1092
- if (allActions.some((existing) => existing.id === action2.id)) {
1093
- throw new Error(`Duplicate action id \`${action2.id}\`.`);
1094
- }
1095
- allActions.push(action2);
1096
- return action2;
1097
- };
1098
1177
  const rpc = {
1099
1178
  request: {
1100
1179
  public_finalize,
1101
1180
  public_getFinalizedHeight,
1102
1181
  public_writeFinalizedHead,
1182
+ public_writeFinalizedHeight,
1103
1183
  public_writeUnfinalizedHead,
1104
1184
  public_deleteReorganisedHead,
1105
1185
  private_getEvents,
1106
1186
  private_getMetadata,
1107
1187
  private_writeEvents,
1108
1188
  private_writeEventsAndGetKeys
1109
- },
1110
- subscribe: {}
1111
- };
1112
- const server = createServer({
1113
- transport: local(rpc),
1114
- quiet: opts.quiet ?? false,
1115
- signingKey: opts.signingKey
1116
- });
1117
- const indexer = {
1118
- ...rpc,
1119
- event,
1120
- action,
1121
- fetch: server.http
1122
- };
1123
- return indexer;
1189
+ }};
1190
+ return local(rpc);
1124
1191
  }
1125
1192
 
1126
- export { defineIndexer };
1193
+ export { defineAction, defineEvent, defineIndexer };