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