@warptoad/skinny-fat-imt-js 0.0.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Trees.d.ts","sourceRoot":"","sources":["../src/Trees.ts"],"names":[],"mappings":"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"}
package/dist/Trees.js ADDED
@@ -0,0 +1,611 @@
1
+ import { LeanIMT } from "@zk-kit/lean-imt";
2
+ import { poseidon2Hash } from "@zkpassport/poseidon2";
3
+ import { getContract, toHex } from "viem";
4
+ import { getInterfaceId, detectSupportedInterfaces, supportsInterface } from "./interfaceId.js";
5
+ import { minBigInt, queryEventInChunks, queryMultiEventsInChunks } from "./eventScanning.js";
6
+ // ABIs baked out of the hardhat artifacts by `scripts/genAbis.mjs` — `as const` tuples, so viem
7
+ // infers exactly what the generated `*$Type["abi"]` used to give us, without the published
8
+ // typings depending on hardhat. Regenerate with `pnpm compile`.
9
+ //
10
+ // The four `i*` ones are the read *interfaces*, used only to derive their ERC-165 ids (below)
11
+ // directly from each interface — no need to diff the full contract ABIs.
12
+ import { skinnyStorageAbi, skinnyEventAbi, fatStorageAbi, fatEventAbi, iimtEventsAbi, iSkinnyEventAbi, iSkinnyStorageAbi, iFatEventAbi, iFatStorageAbi, } from "./abis.js";
13
+ import { DEPLOY_BLOCK, getDeploymentBlock } from "./config.js";
14
+ export { skinnyStorageAbi, skinnyEventAbi, fatStorageAbi, fatEventAbi, iimtEventsAbi };
15
+ export const readableStorageAbi = [
16
+ ...skinnyStorageAbi,
17
+ ...fatStorageAbi,
18
+ ];
19
+ export const readableEventAbi = [
20
+ ...skinnyEventAbi,
21
+ ...fatEventAbi,
22
+ ...iimtEventsAbi,
23
+ ];
24
+ export const ERC165_IDS = {
25
+ skinnyEvent: getInterfaceId(iSkinnyEventAbi),
26
+ skinnyStorage: getInterfaceId(iSkinnyStorageAbi, [iSkinnyEventAbi]),
27
+ fatEvent: getInterfaceId(iFatEventAbi),
28
+ fatStorage: getInterfaceId(iFatStorageAbi, [iFatEventAbi]),
29
+ };
30
+ var TREE_TYPE;
31
+ (function (TREE_TYPE) {
32
+ TREE_TYPE[TREE_TYPE["UNKNOWN"] = 0] = "UNKNOWN";
33
+ TREE_TYPE[TREE_TYPE["FAT_STORAGE"] = 1] = "FAT_STORAGE";
34
+ TREE_TYPE[TREE_TYPE["FAT_EVENT"] = 2] = "FAT_EVENT";
35
+ TREE_TYPE[TREE_TYPE["SKINNY_STORAGE"] = 3] = "SKINNY_STORAGE";
36
+ TREE_TYPE[TREE_TYPE["SKINNY_EVENT"] = 4] = "SKINNY_EVENT";
37
+ TREE_TYPE[TREE_TYPE["NO_INTERFACE"] = 5] = "NO_INTERFACE";
38
+ })(TREE_TYPE || (TREE_TYPE = {}));
39
+ export const LeanIMTHashFuncPoseidon2 = (a, b) => poseidon2Hash([a, b]);
40
+ /**
41
+ * A detached LeanIMT holding the same leaves *and* internal nodes as `tree`, so it can be grown
42
+ * without touching the original. Copies the nodes rather than replaying the leaves on purpose:
43
+ * `new LeanIMT(hash, tree.leaves)` would rehash the whole tree, which is the exact cost a caller
44
+ * growing a cached tree is trying to avoid. This is array copies only, no hashing.
45
+ */
46
+ export function copyTree(tree, hashFunc) {
47
+ // `_nodes[level][index]` holds every computed node, it isn't in the public typings
48
+ const nodes = tree._nodes;
49
+ const copy = new LeanIMT(hashFunc);
50
+ copy._nodes = nodes.map((level) => level.slice());
51
+ return copy;
52
+ }
53
+ export class Trees {
54
+ trees = {};
55
+ /** last known size per tree, with the block it was read at. See {@link getTreeSizeStorage}. */
56
+ sizes = {};
57
+ contractAddress;
58
+ contract;
59
+ hashFunc;
60
+ publicClient;
61
+ constructor(contractAddress, client, hashFunc = LeanIMTHashFuncPoseidon2) {
62
+ this.contractAddress = contractAddress;
63
+ this.publicClient = client;
64
+ this.hashFunc = hashFunc;
65
+ }
66
+ async sync(treeIds = undefined, { fullNodeMode = true, attemptFastSizeMatch = true, syncToRoot, eventChunkSize = 2000n, storageChunkSize = 2000n, insertOnlyTree, autoDiscovery, hasRepeatedLeafs = true, blockNumber } = {}) {
67
+ let syncedTrees;
68
+ // fullNode mode syncs all cached trees, non fullNode mode will auto discover them, so left empty
69
+ const treeIdsNotSet = treeIds === undefined || treeIds.length === 0;
70
+ treeIds = treeIdsNotSet ? fullNodeMode ? Object.keys(this.trees).map((v) => BigInt(v)) : [] : treeIds;
71
+ if (autoDiscovery && fullNodeMode) {
72
+ throw new Error(`auto treeId discovery not supported in fullNodeMode, please provide treeIds to sync. Or turn off fullNodeMode: \`Trees.sync([],{fullNodeMode:false})\``);
73
+ }
74
+ if (fullNodeMode) {
75
+ syncedTrees = await this.syncTreesStorage(treeIds, { chunkSize: storageChunkSize, attemptFastSizeMatch, blockNumber });
76
+ // syncing to a specific root is not possible with pure storage, so we sync with storage first
77
+ // then syncTreesEvent will look for the root and try to trim off the leaves that happened after the root we want
78
+ // in tree with only inserts this will work and will allows us to get any tree that is up to 1 year old
79
+ // if there are updates or resets, currently syncTreesEvent will have to go down till it found all leaves
80
+ if (syncToRoot !== undefined) {
81
+ syncedTrees = await this.syncTreesEvent(treeIds, { blockNumber, attemptFastSizeMatch, syncToRoot, chunkSize: eventChunkSize, insertOnlyTree, autoDiscovery, hasRepeatedLeafs });
82
+ }
83
+ }
84
+ else {
85
+ syncedTrees = await this.syncTreesEvent(treeIds, { blockNumber, attemptFastSizeMatch, syncToRoot, chunkSize: eventChunkSize, insertOnlyTree, autoDiscovery, hasRepeatedLeafs });
86
+ }
87
+ return syncedTrees;
88
+ }
89
+ /**
90
+ * Reads trees straight from contract storage, at `blockNumber` (defaults to the safe block).
91
+ *
92
+ * `attemptFastSizeMatch` (default true) bets that the cached tree is still the right *prefix* of the
93
+ * onchain one, so only the difference in size has to be dealt with: read the leaves past the
94
+ * cache's end, or drop the ones past the onchain size. It is a hope, not a promise, so it always
95
+ * ends in a root check — a tree that had an update(), or was reset and regrown with different
96
+ * leaves, fails that check and is re-read in full. Turning it off re-reads every leaf up front.
97
+ */
98
+ async syncTreesStorage(treeIds = undefined, { chunkSize = 2000n, attemptFastSizeMatch = true, blockNumber } = {}) {
99
+ // default syncs all previously synced trees
100
+ treeIds ??= Object.keys(this.trees).map((v) => BigInt(v));
101
+ if (treeIds.length === 0) {
102
+ throw new Error(`Nothing to sync. No treeIds in cache and no treeIds provided. Please specify which treeIds to sync. SyncTreesStorage cant automatically find them. If you don't know the treeIds, use syncTreesEvent or maybe the contract using the skinny/fat-imt library if it can provide the treeId.`);
103
+ }
104
+ if (chunkSize <= 0n) {
105
+ throw new Error(`chunkSize has to be at least 1, got: ${chunkSize}`);
106
+ }
107
+ const treeIdsHex = treeIds.map((id) => toHex(id));
108
+ const chainId = await this.publicClient.getChainId();
109
+ const cachedTrees = treeIdsHex.filter((id) => this.trees[id] !== undefined && this.trees[id].tree.size > 0);
110
+ if (attemptFastSizeMatch === false && cachedTrees.length > 0) {
111
+ console.warn(`treeIds:${cachedTrees} where found in cache from a previous sync. But attemptFastSizeMatch=false so this cache is not tried and every leaf is read again. The attempt is checked against the onchain root, so it can't cache a wrong tree, it only costs a re-read when it doesn't hold.`);
112
+ }
113
+ const identifiedTreesEnt = await Promise.all(treeIdsHex.map(async (id) => [id, await this.getTreeType(id, chainId)]));
114
+ const unsupportedTypes = [TREE_TYPE.NO_INTERFACE, TREE_TYPE.UNKNOWN, TREE_TYPE.SKINNY_EVENT];
115
+ const unsupportedTrees = identifiedTreesEnt.filter(([id, type]) => unsupportedTypes.includes(type));
116
+ if (unsupportedTrees.length > 0) {
117
+ throw new Error(`Some or more unsupported types found. Types not supported are: ${unsupportedTypes.map((t) => TREE_TYPE[t])}, unsupported treeIds found: ${unsupportedTrees.map(([id, type]) => `id:${id},type:${TREE_TYPE[type]}`)}`);
118
+ }
119
+ // prevent re-orgs, get a state at a safe block that requires 2/3 of the stake to re-org
120
+ // a blockNumber from the caller is taken as is: older than head needs an archive node, and
121
+ // newer than safe leaves the re-org risk to whoever asked for that block. Like syncToRoot in
122
+ // syncTreesEvent it can also move a cached tree *backwards*, which the trim below handles.
123
+ blockNumber ??= (await this.publicClient.getBlock({ blockTag: "safe" })).number;
124
+ const syncedTrees = {};
125
+ for (const treeId of treeIdsHex) {
126
+ const targetSize = await this.getTreeSizeStorage(treeId, chainId, blockNumber);
127
+ const onchainRoot = await this.getRootStorage(treeId, chainId, blockNumber);
128
+ const cached = this.trees[treeId];
129
+ const cacheSize = BigInt(cached ? cached.tree.size : 0);
130
+ let tree = undefined;
131
+ // leaves the attempt below already read from the contract, covering [readFrom, targetSize).
132
+ // They came from this block, not from the cache, so a failed attempt doesn't invalidate them
133
+ // and the fallback read only has to cover [0, readFrom).
134
+ let newInsertedLeaves = [];
135
+ let readFrom = targetSize;
136
+ if (attemptFastSizeMatch && cacheSize > 0n) {
137
+ const startTime = performance.now();
138
+ let attempt;
139
+ if (cacheSize > targetSize) {
140
+ // the cache is ahead of the onchain tree: either it was synced to a newer block
141
+ // than this one, or a reset() happened. Either way the hope is that the leaves it
142
+ // keeps are the same ones, so only the indexes past targetSize have to go.
143
+ // @TODO trimming rehashes the whole prefix, LeanIMT can't drop leaves and keep
144
+ // its internal nodes. Same trade-off syncTreesEvent's trim makes.
145
+ attempt = new LeanIMT(this.hashFunc, cached.tree.leaves.slice(0, Number(targetSize)));
146
+ }
147
+ else {
148
+ // the hope is that the tree only grew, so only the indexes the cache is missing
149
+ // are read. An update() below cacheSize is invisible here, the root check catches it.
150
+ // @notice grows a copy, never cached.tree itself: a throw does not undo a mutation,
151
+ // so growing it in place would leave leaves from this block under the block the
152
+ // cache was last synced at, if anything below here fails.
153
+ newInsertedLeaves = await this.readLeavesStorage(treeId, chainId, blockNumber, chunkSize, cacheSize, targetSize);
154
+ readFrom = cacheSize;
155
+ attempt = copyTree(cached.tree, this.hashFunc);
156
+ if (newInsertedLeaves.length > 0) {
157
+ attempt.insertMany(newInsertedLeaves);
158
+ }
159
+ }
160
+ // check root match. (attempt.root ?? 0n) <= to also be fine if tree is empty since attempt.root will be undefined
161
+ if ((attempt.root ?? 0n) === onchainRoot) {
162
+ tree = attempt;
163
+ }
164
+ else {
165
+ // the leaves the cache held were not the onchain ones, so an update(), updateMany()
166
+ // or a reset() rewrote them. Only the tree build is wasted, the leaves read above
167
+ // are kept, so the fallback re-reads the cached ones and nothing more.
168
+ console.warn(`failed fast size match on treeId:${treeId}, wasted ${performance.now() - startTime}ms on a tree build. Falling back to reading the ${readFrom} leaves the cache claimed, reusing the ${newInsertedLeaves.length} already read from the contract.`);
169
+ }
170
+ }
171
+ // attempt failed or was not tried, so read every leaf the attempt didn't already get
172
+ if (tree === undefined) {
173
+ const readLeaves = await this.readLeavesStorage(treeId, chainId, blockNumber, chunkSize, 0n, readFrom);
174
+ tree = new LeanIMT(this.hashFunc, [...readLeaves, ...newInsertedLeaves]);
175
+ if ((tree.root ?? 0n) !== onchainRoot) {
176
+ // every leaf came straight from the contract at this block, so this is not a stale
177
+ // cache. Drop what's cached for this tree, the attempt above may have grown it.
178
+ delete this.trees[treeId];
179
+ throw new Error(`Synced treeId:${treeId} to root:${tree.root ?? 0n} at block:${blockNumber}, but the contract reports root:${onchainRoot} for it. Its leaves and its root disagree, so one of the two reads is not what this lib expects.`);
180
+ }
181
+ }
182
+ this.trees[treeId] = {
183
+ tree: tree,
184
+ type: this.trees[treeId].type, // getTreeType() above put every treeId in the cache
185
+ lastSynced: blockNumber,
186
+ // attemptFastSizeMatch is a guess this sync made, not something it learned about the tree,
187
+ // so whatever is known about updates stays as it was
188
+ insertOnlyTree: this.trees[treeId].insertOnlyTree
189
+ };
190
+ syncedTrees[treeId] = this.trees[treeId];
191
+ }
192
+ return syncedTrees;
193
+ }
194
+ /**
195
+ * The leaves of `[startIndex, endIndex)`, read in chunks of `chunkSize`. Same range as
196
+ * {@link Trees#getLeavesStorage}, which does it in a single call, but split up so a big range
197
+ * doesn't have to fit in one eth_call. What the caller does with them (build a tree, grow one)
198
+ * is up to it.
199
+ */
200
+ async readLeavesStorage(treeId, chainId, blockNumber, chunkSize, startIndex, endIndex) {
201
+ const leaves = [];
202
+ for (let firstIndex = startIndex; firstIndex < endIndex; firstIndex += chunkSize) {
203
+ const lastIndex = minBigInt(firstIndex + chunkSize, endIndex); // lastIndex is exclusive
204
+ leaves.push(...await this.getLeavesStorage(treeId, chainId, firstIndex, lastIndex, blockNumber));
205
+ }
206
+ return leaves;
207
+ }
208
+ async syncTreesEvent(treeIds = undefined, { blockNumber, attemptFastSizeMatch = true, syncToRoot, chunkSize = 2000n, insertOnlyTree, autoDiscovery, hasRepeatedLeafs = true } = {}) {
209
+ treeIds ??= [];
210
+ if (treeIds.length !== 0 && autoDiscovery === true) {
211
+ throw new Error(`TreeIds where provided while autoDiscovery=true. Either turn off autoDiscovery to only sync those specific ids or set treeIds=undefined to automatically discover and sync all treeId.`);
212
+ }
213
+ if (treeIds.length === 0 && autoDiscovery === false) {
214
+ throw new Error(`Please specify which treeIds to sync. Or set autoDiscovery=true`);
215
+ }
216
+ const chainId = await this.publicClient.getChainId();
217
+ const defaultToAutoDiscover = treeIds.length === 0 && autoDiscovery === undefined && syncToRoot === undefined;
218
+ const discoverAllIds = treeIds.length === 0 && autoDiscovery === undefined && syncToRoot === undefined || autoDiscovery === true;
219
+ if (autoDiscovery && syncToRoot !== undefined) {
220
+ console.warn(`autoDiscovery was set to true, but a root was provided to syncToRoot. This causes the function to run all the way till block: ${getDeploymentBlock(chainId)}, to discover all treeIds. Even if a tree is already found with that root and synced, it will keep going to find another tree with that root. If you intent on only syncing one tree (or known list of ids), please provide them and set autoDiscovery=false.`);
221
+ }
222
+ if (defaultToAutoDiscover) {
223
+ console.warn(`No treeIds provided for event sync, defaulting to autoDiscovery of all treeIds. This will run all the way till block: ${getDeploymentBlock(chainId)}, to discover all treeIds. If this intended you can safely ignore this warning or silence it by setting autoDiscovery=true. If you intent on only syncing one or a set of treeIds, please provide them to speed up syncing.`);
224
+ }
225
+ // sync backwards, sync every NewLeaf, UpdatedLeaf, RepeatedLeafs, event
226
+ const newestBlock = blockNumber ?? (await this.publicClient.getBlock({ blockTag: 'safe' })).number;
227
+ // in discovery we go all the way down, if treeIds are provided, we go down till all them are synced
228
+ // if all of them are synced before, we don't sync all leaves, just those who happened after the most out of date tree was synced at
229
+ // when looking for a specific root we need to also go down to the lowest block since the root might be older then the last sync
230
+ const oldestBlock = discoverAllIds || syncToRoot !== undefined ? BigInt(getDeploymentBlock(chainId)) : this.getOldestSyncBlock(treeIds);
231
+ const syncState = { treeCache: this.trees, treeState: {}, lastBlockSynced: newestBlock, unsyncedIds: new Set(treeIds.map((id) => toHex(id))) };
232
+ const insertEvents = hasRepeatedLeafs ? ["NewLeaf", "RepeatedLeafs"] : ["NewLeaf"];
233
+ const leafEvents = insertOnlyTree ? insertEvents : ["UpdatedLeaf", ...insertEvents];
234
+ do {
235
+ const foundAllRoots = syncState.unsyncedIds.size === 0 && syncToRoot === undefined && discoverAllIds == false;
236
+ await queryMultiEventsInChunks({
237
+ publicClient: this.publicClient,
238
+ contract: await this.getContract(),
239
+ eventNames: foundAllRoots ? leafEvents : ["NewRoot", ...leafEvents],
240
+ // only our treeIds. treeId is the 1st indexed param of NewRoot, an array means "any of these"
241
+ sharedEventFilterArgs: discoverAllIds ? undefined : { treeId: [...syncState.unsyncedIds].map((id) => BigInt(id)) },
242
+ firstBlock: oldestBlock,
243
+ lastBlock: syncState.lastBlockSynced,
244
+ reverseOrder: true,
245
+ maxEvents: Infinity,
246
+ chunkSize: chunkSize,
247
+ // if we need all treeIds?
248
+ postQueryFilter: getEventFilter(oldestBlock, syncState, { hashFunc: this.hashFunc, attemptFastSizeMatch: attemptFastSizeMatch, autoDiscover: discoverAllIds, syncToRoot: syncToRoot })
249
+ });
250
+ } while (syncState.unsyncedIds.size > 0 && syncState.lastBlockSynced > oldestBlock);
251
+ // cache the tree, or update the new/updated leafs of an existing cache tree.
252
+ const syncedTrees = {};
253
+ const allTreeIds = new Set([...treeIds.map((id) => toHex(id)), ...Object.keys(syncState.treeState)]);
254
+ for (const treeId of allTreeIds) {
255
+ const newSyncState = syncState.treeState[treeId];
256
+ const cacheTree = this.trees[treeId];
257
+ const isNotOld = cacheTree === undefined
258
+ || (newSyncState !== undefined && newSyncState.lastSynced >= cacheTree.lastSynced);
259
+ // no-op, no new state and no rollback (syncToRoot on old root can be used to go back to a older root)
260
+ if (isNotOld === false && syncToRoot === undefined) {
261
+ syncedTrees[treeId] = cacheTree;
262
+ continue;
263
+ }
264
+ const canMerge = newSyncState !== undefined && cacheTree !== undefined && isNotOld
265
+ && BigInt(cacheTree.tree.size) <= newSyncState.targetSize;
266
+ if (newSyncState !== undefined && newSyncState.tree === undefined) {
267
+ const targetSize = Number(newSyncState.targetSize);
268
+ const missing = [];
269
+ for (let index = canMerge ? cacheTree.tree.size : 0; index < targetSize; index++) {
270
+ if (newSyncState.leaves[index] === undefined) {
271
+ missing.push(index);
272
+ break;
273
+ }
274
+ }
275
+ if (missing.length > 0) {
276
+ throw new Error(`Incomplete sync of treeId:${treeId}. Root ${toHex(newSyncState.expectedRoot)} has ${targetSize} leaves, ` +
277
+ `but no event was found indexes ${missing}. Possible causes: hasRepeatedLeafs:false, ` +
278
+ `insertOnlyTree:true, a scan that did not reach far enough back (it started at block ${oldestBlock}), ` +
279
+ `or a contract that does not emit an event for every leaf it stores.`);
280
+ }
281
+ }
282
+ let synced;
283
+ if (newSyncState?.tree !== undefined) {
284
+ // attemptFastSizeMatch was success full by trimming of leaves of the pre-existing cache tree.
285
+ // that caused a tree to already be built and checked so we can just use that rn.
286
+ // it's also almost always a rollback so we should not store in cache
287
+ // (almost always rollback, it could be that a reset happened, and just happens to have the same leaves as the previous tree but smaller size)
288
+ synced = {
289
+ tree: newSyncState.tree,
290
+ type: cacheTree.type,
291
+ lastSynced: newSyncState.lastSynced,
292
+ insertOnlyTree: cacheTree.insertOnlyTree
293
+ };
294
+ }
295
+ else if (canMerge) {
296
+ // @notice mutates the cached tree in place, so `synced` *is* the cache entry and the
297
+ // isNotOld check below can no longer keep it out. See canMerge above.
298
+ const updatedIndexes = [];
299
+ const updatedLeaves = [];
300
+ const newLeafs = [];
301
+ newSyncState.leaves.forEach((leaf, index) => {
302
+ if (index < cacheTree.tree.size) {
303
+ updatedLeaves.push(leaf);
304
+ updatedIndexes.push(index);
305
+ }
306
+ else {
307
+ newLeafs.push(leaf);
308
+ }
309
+ });
310
+ cacheTree.tree.updateMany(updatedIndexes, updatedLeaves);
311
+ if (newLeafs.length > 0) {
312
+ cacheTree.tree.insertMany(newLeafs);
313
+ }
314
+ cacheTree.lastSynced = newSyncState.lastSynced;
315
+ synced = cacheTree;
316
+ }
317
+ else {
318
+ // cached tree does not exist or synced tree is smaller and newSyncState.leaves is complete
319
+ synced = {
320
+ tree: new LeanIMT(this.hashFunc, newSyncState ? newSyncState.leaves : []),
321
+ type: cacheTree ? cacheTree.type : await identifyTree(BigInt(treeId), this.contract),
322
+ lastSynced: newSyncState?.lastSynced ? newSyncState.lastSynced : 0n,
323
+ // what the caller claims about this sync, falling back to whatever was known before
324
+ insertOnlyTree: insertOnlyTree ?? cacheTree?.insertOnlyTree
325
+ };
326
+ }
327
+ if (newSyncState !== undefined && (synced.tree.root ?? 0n) !== newSyncState.expectedRoot) {
328
+ throw new Error(`syncing failed, expected root:${toHex(newSyncState.expectedRoot)} but got ${toHex(synced.tree.root ?? 0n)}`);
329
+ }
330
+ // a sync may only ever move the cache forward. A syncToRoot rollback is handed to the
331
+ // caller, but the cache keeps the newer tree it already had.
332
+ if (isNotOld) {
333
+ this.trees[treeId] = synced;
334
+ }
335
+ syncedTrees[treeId] = synced;
336
+ }
337
+ if (syncToRoot !== undefined) {
338
+ const allTreeIds = Object.keys(syncedTrees);
339
+ const treeIdsWithNoRoot = allTreeIds.filter((id) => syncedTrees[id].tree.root !== syncToRoot);
340
+ // not one tree came back with this root. Also covers "no tree came back at all", which is
341
+ // what a syncToRoot with no treeIds does: it never auto discovers, so it filters on an
342
+ // empty set of ids and matches nothing.
343
+ if (allTreeIds.length === treeIdsWithNoRoot.length) {
344
+ throw new Error(`Root: ${syncToRoot} was never found for ${allTreeIds.length === 0
345
+ ? `any tree, none were synced at all. syncToRoot does not auto discover treeIds, so pass them in or set autoDiscovery=true`
346
+ : `any of the treeIds: ${allTreeIds}`}`);
347
+ }
348
+ if (treeIdsWithNoRoot.length > 0) {
349
+ const foundTreeIds = allTreeIds.filter((id) => false === treeIdsWithNoRoot.includes(id));
350
+ console.warn(`Root: ${syncToRoot} was never found for some treeIds: ${treeIdsWithNoRoot}. But was found for ${foundTreeIds})`);
351
+ treeIdsWithNoRoot.forEach((id) => delete syncedTrees[id]);
352
+ }
353
+ }
354
+ return syncedTrees;
355
+ }
356
+ async getContract() {
357
+ if (this.contract) {
358
+ return this.contract;
359
+ }
360
+ else {
361
+ //[skinnyStorage, skinnyEvent, fatStorage, fatEvent]
362
+ const interfaceSupports = await Promise.all([
363
+ ERC165_IDS.skinnyStorage, ERC165_IDS.skinnyEvent, ERC165_IDS.fatStorage, ERC165_IDS.fatEvent
364
+ ].map((id) => supportsInterface(this.publicClient, this.contractAddress, id)));
365
+ const abis = [skinnyStorageAbi, skinnyEventAbi, fatStorageAbi, fatEventAbi];
366
+ const unionAbi = abis.filter((abi, i) => interfaceSupports[i]).flat();
367
+ this.contract = getContract({
368
+ address: this.contractAddress,
369
+ client: this.publicClient,
370
+ abi: [...unionAbi, ...iimtEventsAbi],
371
+ // yeah no amount of type juggling will be able to deal with these crazy union types.
372
+ });
373
+ return this.contract;
374
+ }
375
+ }
376
+ getOldestSyncTreeId(treeIds) {
377
+ let oldest = treeIds[0];
378
+ let oldestBlock = 2n ** 256n; // Infinity replacement
379
+ for (const id of treeIds) {
380
+ const tree = this.trees[id];
381
+ if (tree === undefined) {
382
+ return id;
383
+ }
384
+ if (tree.lastSynced < oldestBlock) {
385
+ oldestBlock = tree.lastSynced;
386
+ oldest = id;
387
+ }
388
+ }
389
+ return oldest;
390
+ }
391
+ getOldestSyncBlock(treeIds) {
392
+ const oldestTree = this.trees[this.getOldestSyncTreeId(treeIds.map((id) => toHex(id)))];
393
+ return oldestTree ? oldestTree.lastSynced : 0n;
394
+ }
395
+ async getTreeType(treeId, chainId) {
396
+ return (await this.initTree(treeId, chainId)).type;
397
+ }
398
+ async getLeavesStorage(treeId, chainId, startIndex, endIndex, blockNumber) {
399
+ const size = await this.getTreeSizeStorage(treeId, chainId, blockNumber);
400
+ if (startIndex > endIndex || endIndex > size) {
401
+ throw new Error(`Leaf range [${startIndex}, ${endIndex}) is out of range for treeId:${treeId}, which holds ${size} leaves at block:${blockNumber}.`);
402
+ }
403
+ const treeType = (await this.initTree(treeId, chainId)).type;
404
+ const contract = await this.getContract();
405
+ switch (treeType) {
406
+ case TREE_TYPE.SKINNY_STORAGE:
407
+ // TODO debug_storageRange at for these 2 but not FAT_EVENT
408
+ return await contract.read.getSkinnyLeaves([BigInt(treeId), startIndex, endIndex], { blockNumber: blockNumber });
409
+ break;
410
+ case TREE_TYPE.FAT_STORAGE:
411
+ return await contract.read.getFatLeaves([BigInt(treeId), startIndex, endIndex], { blockNumber: blockNumber });
412
+ break;
413
+ case TREE_TYPE.FAT_EVENT:
414
+ return await contract.read.getFatLeaves([BigInt(treeId), startIndex, endIndex], { blockNumber: blockNumber });
415
+ break;
416
+ default:
417
+ throw new Error(`treeId ${treeId} has type: ${TREE_TYPE[treeType]} which is not supported`);
418
+ break;
419
+ }
420
+ }
421
+ async getTreeSizeStorage(treeId, chainId, blockNumber) {
422
+ const cachedSize = this.sizes[treeId];
423
+ if (cachedSize !== undefined && cachedSize.blockNumber === blockNumber) {
424
+ return cachedSize.size;
425
+ }
426
+ const treeType = (await this.initTree(treeId, chainId)).type;
427
+ const contract = await this.getContract();
428
+ let size;
429
+ switch (treeType) {
430
+ case TREE_TYPE.SKINNY_STORAGE:
431
+ case TREE_TYPE.SKINNY_EVENT:
432
+ size = await contract.read.getSkinnySize([BigInt(treeId)], { blockNumber: blockNumber });
433
+ break;
434
+ case TREE_TYPE.FAT_STORAGE:
435
+ case TREE_TYPE.FAT_EVENT:
436
+ size = await contract.read.getFatSize([BigInt(treeId)], { blockNumber: blockNumber });
437
+ break;
438
+ default:
439
+ throw new Error(`treeId ${treeId} has type: ${TREE_TYPE[treeType]} which is not supported`);
440
+ }
441
+ this.sizes[treeId] = { blockNumber: blockNumber, size: size };
442
+ return size;
443
+ }
444
+ async getRootStorage(treeId, chainId, blockNumber) {
445
+ const treeType = (await this.initTree(treeId, chainId)).type;
446
+ const contract = await this.getContract();
447
+ switch (treeType) {
448
+ case TREE_TYPE.SKINNY_STORAGE:
449
+ case TREE_TYPE.SKINNY_EVENT:
450
+ return await contract.read.getSkinnyRoot([BigInt(treeId)], { blockNumber: blockNumber });
451
+ case TREE_TYPE.FAT_STORAGE:
452
+ case TREE_TYPE.FAT_EVENT:
453
+ return await contract.read.getFatRoot([BigInt(treeId)], { blockNumber: blockNumber });
454
+ default:
455
+ throw new Error(`treeId ${treeId} has type: ${TREE_TYPE[treeType]} which is not supported`);
456
+ }
457
+ }
458
+ async initTree(treeId, chainId) {
459
+ if (this.trees[treeId]) {
460
+ return this.trees[treeId];
461
+ }
462
+ const treeType = await identifyTree(BigInt(treeId), await this.getContract());
463
+ this.trees[treeId] = {
464
+ tree: new LeanIMT(this.hashFunc),
465
+ type: treeType,
466
+ lastSynced: BigInt(getDeploymentBlock(chainId))
467
+ };
468
+ return this.trees[treeId];
469
+ }
470
+ }
471
+ export async function identifyTree(treeId, contract) {
472
+ // TODO we can dish out these calls concurrently. but storage always > event, remember storage inherits event!!
473
+ let skinnyStorageProm, skinnyEventProm, fatStorageProm, fatEventProm;
474
+ const differentiatingFunctions = ["getSkinnyLeavesBaseSlot", "getSkinnySize", "getFatLeavesBaseSlot", "getFatSize"];
475
+ const contractFunctions = contract.abi.filter((abi) => abi.type === "function" && "name" in abi && differentiatingFunctions.includes(abi.name)).map((abi) => abi.name);
476
+ if (contractFunctions.length === 0) {
477
+ // is okay we can still look for events! SkinnyEvent interface is not even use full while syncing, since we
478
+ // need events to get the leaves anyway.
479
+ // TODO we cant deal with NO_INTERFACE case rn because storage emits both repeatedLeaves and newLeaf which is double
480
+ // prob not really a problem?
481
+ return TREE_TYPE.NO_INTERFACE;
482
+ }
483
+ if (contractFunctions.includes("getSkinnyLeavesBaseSlot")) {
484
+ skinnyStorageProm = contract.read.getSkinnyLeavesBaseSlot([treeId]);
485
+ }
486
+ if (contractFunctions.includes("getSkinnySize")) {
487
+ skinnyEventProm = contract.read.getSkinnySize([treeId]);
488
+ }
489
+ if (contractFunctions.includes("getFatLeavesBaseSlot")) {
490
+ fatStorageProm = contract.read.getFatLeavesBaseSlot([treeId]);
491
+ }
492
+ if (contractFunctions.includes("getFatSize")) {
493
+ fatEventProm = contract.read.getFatSize([treeId]);
494
+ }
495
+ // @notice storage needs to come first in the arr, since both storage and event resolve if a tree type == STORAGE
496
+ const proms = [skinnyStorageProm, skinnyEventProm, fatStorageProm, fatEventProm];
497
+ const settled = await Promise.allSettled(proms);
498
+ const types = [TREE_TYPE.SKINNY_STORAGE, TREE_TYPE.SKINNY_EVENT, TREE_TYPE.FAT_STORAGE, TREE_TYPE.FAT_EVENT];
499
+ for (let index = 0; index < settled.length; index++) {
500
+ // proms[index] guard: undefined entries (interface absent) become fulfilled in allSettled
501
+ if (proms[index] !== undefined && settled[index].status == "fulfilled") {
502
+ return types[index];
503
+ }
504
+ }
505
+ // usually un-initialized or does not exist
506
+ return TREE_TYPE.UNKNOWN;
507
+ }
508
+ export function getEventFilter(firstBlock, syncState, { autoDiscover, syncToRoot, attemptFastSizeMatch, hashFunc = LeanIMTHashFuncPoseidon2 } = {}) {
509
+ const quitEarly = (allEvents, chunkEvents, chunkStart, chunkEnd) => {
510
+ let quit = false;
511
+ for (const event of chunkEvents.toReversed()) {
512
+ const treeId = toHex(event.args.treeId);
513
+ // have we found our NewRoot event yet?
514
+ if (syncState.treeState[treeId] === undefined) {
515
+ if (event.eventName === "NewRoot") {
516
+ if (syncToRoot === undefined || syncToRoot === event.args.root) {
517
+ //TODO in case that syncToRoot is provided, check the tree in cache, if size >= as root says we need.
518
+ // trim of excess leaves and check if root is already correct.
519
+ // this allows us to quit very early on insert only trees or ones with update if we are lucky no update happened
520
+ const simpleTrim = attemptFastSizeMatch && syncState.treeCache[treeId]?.tree.size > event.args.size;
521
+ if (simpleTrim) {
522
+ const trimmed = syncState.treeCache[treeId]?.tree.leaves.slice(0, Number(event.args.size));
523
+ const startTime = performance.now();
524
+ const trimmedTree = new LeanIMT(hashFunc, trimmed);
525
+ const endTime = performance.now();
526
+ if (event.args.root === trimmedTree.root) {
527
+ console.log(`success full trim on treeId: ${treeId}`);
528
+ syncState.treeState[treeId] = {
529
+ leaves: [],
530
+ count: 0n,
531
+ targetSize: event.args.size,
532
+ lastSynced: event.blockNumber - 1n, // 2 roots can be in one block, so -1 to be safe
533
+ tree: trimmedTree,
534
+ expectedRoot: event.args.root
535
+ };
536
+ syncState.unsyncedIds.delete(treeId);
537
+ quit = true;
538
+ continue;
539
+ }
540
+ else {
541
+ console.warn(`failed trim, wasted ${endTime - startTime}ms on tree build`);
542
+ // console.warn(`Rolling back to root: ${toHex(event.args.root)} failed. Likely because a tree reset or update happened. If that is true, you can safely ignore this warning. TODO optimize this`)
543
+ //TODO: the entire tree was rebuilt here, just to fail. For a future re-write of a more scalable LeanIMTjs, make it so it can trim leaves, while re-using the internal nodes so rehashing is not needed for all.`)
544
+ }
545
+ }
546
+ syncState.treeState[treeId] = {
547
+ leaves: [],
548
+ count: 0n,
549
+ targetSize: event.args.size,
550
+ lastSynced: event.blockNumber - 1n, // 2 roots can be in one block, so -1 to be safe
551
+ tree: undefined,
552
+ expectedRoot: event.args.root
553
+ };
554
+ }
555
+ }
556
+ // we have found a NewRoot, or our specific new root if syncToRoot was set
557
+ // so syncState[treeId] contains something now
558
+ }
559
+ else {
560
+ const tree = syncState.treeState[treeId];
561
+ if (tree.count === tree.targetSize || syncState.treeState[treeId].tree) {
562
+ syncState.unsyncedIds.delete(treeId);
563
+ quit = true;
564
+ continue;
565
+ }
566
+ if (event.eventName === "RepeatedLeafs") {
567
+ const start = Number(event.args.startIndex); // startIndex == inclusive, index = start is correct
568
+ const end = Number(event.args.nextIndex); // nextIndex == exclusive, so index < end is correct here
569
+ for (let index = start; index < end; index++) {
570
+ if (tree.leaves[index] === undefined) {
571
+ tree.leaves[index] = event.args.leaf;
572
+ tree.count++;
573
+ }
574
+ }
575
+ }
576
+ else if (event.eventName !== "NewRoot") {
577
+ const leafIndex = Number(event.args.index);
578
+ if (tree.leaves[leafIndex] === undefined) {
579
+ if (event.eventName === "NewLeaf") {
580
+ // @TODO this is not super scalable, but realistically on L1 you wont really hit numbers high
581
+ // enough, besides LeanIMT-js uses normal js arrays indexed by numbers so that is the first one to fail
582
+ // also due to ram usage
583
+ tree.leaves[leafIndex] = event.args.leaf;
584
+ tree.count++;
585
+ }
586
+ else {
587
+ // if (event.eventName === "UpdatedLeaf")
588
+ tree.leaves[leafIndex] = event.args.newLeaf;
589
+ tree.count++;
590
+ }
591
+ }
592
+ }
593
+ }
594
+ }
595
+ // we process all events on every chunk right away, we don't need allEvents, so just discard all events to save memory
596
+ // [allEvents, quit]
597
+ // `startBlock === firstBlock` <= detects, "is last chunk", assumes scanning backwards
598
+ if (quit || chunkStart === firstBlock) {
599
+ syncState.lastBlockSynced = chunkStart;
600
+ }
601
+ return [[], quit];
602
+ };
603
+ if (autoDiscover) {
604
+ const quitAtEnd = (allEvents, chunkEvents, chunkStart, chunkEnd) => { quitEarly(allEvents, chunkEvents, chunkStart, chunkEnd); return [[], false]; };
605
+ return quitAtEnd;
606
+ }
607
+ else {
608
+ return quitEarly;
609
+ }
610
+ }
611
+ //# sourceMappingURL=Trees.js.map