@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.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/Trees.d.ts +702 -0
- package/dist/Trees.d.ts.map +1 -0
- package/dist/Trees.js +611 -0
- package/dist/Trees.js.map +1 -0
- package/dist/abis.d.ts +979 -0
- package/dist/abis.d.ts.map +1 -0
- package/dist/abis.js +1305 -0
- package/dist/abis.js.map +1 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -0
- package/dist/eventScanning.d.ts +125 -0
- package/dist/eventScanning.d.ts.map +1 -0
- package/dist/eventScanning.js +253 -0
- package/dist/eventScanning.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaceId.d.ts +48 -0
- package/dist/interfaceId.d.ts.map +1 -0
- package/dist/interfaceId.js +70 -0
- package/dist/interfaceId.js.map +1 -0
- package/package.json +78 -0
- package/src/Trees.ts +738 -0
- package/src/abis.ts +1313 -0
- package/src/config.ts +11 -0
- package/src/eventScanning.ts +414 -0
- package/src/index.ts +4 -0
- package/src/interfaceId.ts +77 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Trees.d.ts","sourceRoot":"","sources":["../src/Trees.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAErE,OAAO,EAAsB,KAAK,GAAG,EAAE,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAE,KAAK,qBAAqB,EAAE,KAAK,GAAG,EAAY,KAAK,YAAY,EAAqB,MAAM,MAAM,CAAC;AAIvK,OAAO,EAA0E,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAQvI,OAAO,EACH,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,WAAW,EACX,aAAa,EAKhB,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;AAEvF,MAAM,MAAM,WAAW,GAAG;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,GAAG,CAAA;IACR,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAA;CACzD,CAAA;AAED,KAAK,kBAAkB,GAAG,SAAS;IAC/B,GAAG,OAAO,gBAAgB;IAC1B,GAAG,OAAO,aAAa;CAC1B,CAAC;AAEF,KAAK,gBAAgB,GAAG,SAAS;IAC7B,GAAG,OAAO,cAAc;IACxB,GAAG,OAAO,WAAW;IACrB,GAAG,OAAO,aAAa;CAC1B,CAAA;AAED,KAAK,WAAW,GAAG,SAAS,CAAC,GAAG,kBAAkB,EAAE,GAAG,gBAAgB,CAAC,CAAA;AAExE;;;;GAIG;AACH,KAAK,aAAa,CAAC,KAAK,SAAS,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,QAAQ,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC;AAGpG,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGD,CAAC;AAE/B,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAID,CAAC;AAE7B,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;AAChF,MAAM,MAAM,uBAAuB,GAAG,qBAAqB,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAC;AAC9F,MAAM,MAAM,qBAAqB,GAAG,qBAAqB,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAC;AAE1F,MAAM,MAAM,2BAA2B,GAAG,qBAAqB,CAAC,OAAO,cAAc,EAAE,YAAY,CAAC,CAAC;AACrG,MAAM,MAAM,6BAA6B,GAAG,qBAAqB,CAAC,OAAO,gBAAgB,EAAE,YAAY,CAAC,CAAC;AACzG,MAAM,MAAM,wBAAwB,GAAG,qBAAqB,CAAC,OAAO,WAAW,EAAE,YAAY,CAAC,CAAC;AAC/F,MAAM,MAAM,0BAA0B,GAAG,qBAAqB,CAAC,OAAO,aAAa,EAAE,YAAY,CAAC,CAAC;AAEnG,eAAO,MAAM,UAAU;;;;;CAKiB,CAAC;AAYzC,aAAK,SAAS;IACV,OAAO,IAAA;IACP,WAAW,IAAA;IACX,SAAS,IAAA;IACT,cAAc,IAAA;IACd,YAAY,IAAA;IACZ,YAAY,IAAA;CACf;AAED,eAAO,MAAM,wBAAwB,EAAE,mBAAqE,CAAA;AAE5G,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAEjH;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9F;AAED,qBAAa,KAAK;IACd,OAAO,CAAC,KAAK,CAAoC;IACjD,+FAA+F;IAC/F,OAAO,CAAC,KAAK,CAA+D;IAErE,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAG,gBAAgB,CAAC;IAC5B,QAAQ,EAAE,mBAAmB,CAAC;IACrC,OAAO,CAAC,YAAY,CAAe;gBAEvB,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,sBAA2B;IAMzF,IAAI,CAAC,OAAO,GAAE,MAAM,EAAE,GAAG,SAAqB,EAAE,EAAE,YAAmB,EAAE,oBAA2B,EAAE,UAAU,EAAE,cAAsB,EAAE,gBAAwB,EAAE,cAAc,EAAE,aAAa,EAAE,gBAAuB,EAAE,WAAW,EAAE,GAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAAO;;;IA+Bxe;;;;;;;;OAQG;IACG,gBAAgB,CAClB,OAAO,GAAE,MAAM,EAAE,GAAG,SAAqB,EACzC,EAAE,SAAiB,EAAE,oBAA2B,EAAE,WAAW,EAAE,GAAE;QAAE,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GACnJ,OAAO,CAAC;QAAE,CAAC,MAAM,EAAE,GAAG,GAAG,UAAU,CAAA;KAAE,CAAC;IAoGzC;;;;;OAKG;YACW,iBAAiB;IAWzB,cAAc,CAAC,OAAO,GAAE,MAAM,EAAE,GAAG,SAAqB,EAAE,EAAC,WAAW,EAAE,oBAA2B,EAAE,UAAU,EAAE,SAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,gBAAuB,EAAE,GAAE;QAAC,WAAW,CAAC,EAAC,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAAO;;;IAoK7X,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAqB9C,mBAAmB,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG;IAgBxC,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE;IAK9B,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM;IAIxC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;IAwBxG,kBAAkB,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;IAwBpE,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;IAehE,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM;CAY9C;AAED,wBAAsB,YAAY,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,WAAW,sBAwCxB;AAED,KAAK,SAAS,GAAG;IACb,eAAe,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/C,SAAS,EAAE;QAAE,CAAC,MAAM,EAAE,GAAG,GAAG,UAAU,CAAA;KAAE,CAAC;IACzC,SAAS,EAAE;QAAE,CAAC,MAAM,EAAE,GAAG,GAAG;YAAE,YAAY,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YAAC,MAAM,EAAE,MAAM,EAAE,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAA;CAC1J,CAAA;AAED,KAAK,cAAc,GAAG,oBAAoB,CAAC,aAAa,CAAC,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,eAAe,CAAC,CAAC,CAAA;AAClH,wBAAgB,cAAc,CAC1B,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EACxC,EAAE,YAAY,EAAE,UAAU,EAAE,oBAAoB,EAAE,QAAmC,EAAE,GAAE;IAAE,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,kBAmGhN"}
|
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
|