envio 3.15.0-subgraph → 3.16.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.
Files changed (186) hide show
  1. package/README.md +40 -34
  2. package/evm.schema.json +3 -3
  3. package/fuel.schema.json +2 -2
  4. package/index.d.ts +3 -3
  5. package/licenses/README.md +1 -1
  6. package/package.json +9 -10
  7. package/src/Batch.res.mjs +5 -5
  8. package/src/BatchProcessing.res +1 -0
  9. package/src/BatchProcessing.res.mjs +1 -0
  10. package/src/Bin.res +0 -7
  11. package/src/Bin.res.mjs +0 -8
  12. package/src/ChainFetching.res +17 -456
  13. package/src/ChainFetching.res.mjs +33 -404
  14. package/src/ChainState.res +48 -83
  15. package/src/ChainState.res.mjs +83 -115
  16. package/src/ChainState.resi +0 -26
  17. package/src/Clock.res +57 -0
  18. package/src/Clock.res.mjs +72 -0
  19. package/src/Config.res +7 -16
  20. package/src/Config.res.mjs +4 -11
  21. package/src/ContractRegisterContext.res +6 -3
  22. package/src/ContractRegisterContext.res.mjs +2 -4
  23. package/src/Core.res +5 -136
  24. package/src/Core.res.mjs +1 -90
  25. package/src/CrossChainState.res +2 -8
  26. package/src/CrossChainState.res.mjs +3 -3
  27. package/src/Ecosystem.res +4 -7
  28. package/src/Ecosystem.res.mjs +8 -8
  29. package/src/EffectState.res +10 -2
  30. package/src/EffectState.res.mjs +8 -4
  31. package/src/EffectState.resi +3 -1
  32. package/src/EntityId.res +2 -7
  33. package/src/EntityId.res.mjs +1 -8
  34. package/src/EntityTables.res +4 -1
  35. package/src/EntityTables.res.mjs +1 -1
  36. package/src/Env.res +0 -16
  37. package/src/Env.res.mjs +0 -11
  38. package/src/EventProcessing.res +1 -38
  39. package/src/EventProcessing.res.mjs +1 -28
  40. package/src/FetchState.res +160 -319
  41. package/src/FetchState.res.mjs +182 -292
  42. package/src/FinalizeBackfill.res +2 -4
  43. package/src/FinalizeBackfill.res.mjs +3 -4
  44. package/src/HandlerLoader.res +15 -39
  45. package/src/HandlerLoader.res.mjs +8 -27
  46. package/src/HandlerRegister.res +0 -4
  47. package/src/HandlerRegister.res.mjs +2 -5
  48. package/src/HandlerRegister.resi +0 -2
  49. package/src/InMemoryTable.res +17 -16
  50. package/src/InMemoryTable.res.mjs +14 -15
  51. package/src/IndexerState.res +28 -4
  52. package/src/IndexerState.res.mjs +19 -6
  53. package/src/IndexerState.resi +4 -0
  54. package/src/Internal.res +0 -110
  55. package/src/Internal.res.mjs +1 -115
  56. package/src/LoadLayer.res +21 -47
  57. package/src/LoadLayer.res.mjs +47 -72
  58. package/src/LoadLayer.resi +0 -26
  59. package/src/Main.res +1 -14
  60. package/src/Main.res.mjs +3 -4
  61. package/src/Persistence.res +3 -23
  62. package/src/Persistence.res.mjs +3 -3
  63. package/src/Rollback.res +0 -1
  64. package/src/Rollback.res.mjs +1 -1
  65. package/src/RunScope.res +0 -3
  66. package/src/SafeCheckpointTracking.res +4 -4
  67. package/src/SafeCheckpointTracking.res.mjs +1 -1
  68. package/src/SimulateItems.res +26 -42
  69. package/src/SimulateItems.res.mjs +16 -40
  70. package/src/StagedRows.res +106 -0
  71. package/src/StagedRows.res.mjs +102 -0
  72. package/src/Staging.res +32 -14
  73. package/src/Staging.res.mjs +17 -20
  74. package/src/Storage.res +277 -998
  75. package/src/Storage.res.mjs +216 -731
  76. package/src/Supervisor.res +75 -56
  77. package/src/Supervisor.res.mjs +55 -26
  78. package/src/TestIndexer.res +22 -27
  79. package/src/TestIndexer.res.mjs +20 -17
  80. package/src/Throttler.res +15 -8
  81. package/src/Throttler.res.mjs +9 -6
  82. package/src/Throttler.resi +1 -1
  83. package/src/UserContext.res +46 -410
  84. package/src/UserContext.res.mjs +30 -318
  85. package/src/Utils.res +0 -23
  86. package/src/Utils.res.mjs +1 -17
  87. package/src/Writing.res +0 -1
  88. package/src/Writing.res.mjs +1 -1
  89. package/src/bindings/ClickHouse.res +0 -322
  90. package/src/bindings/ClickHouse.res.mjs +0 -338
  91. package/src/bindings/StorageClient.res +107 -175
  92. package/src/bindings/StorageClient.res.mjs +19 -8
  93. package/src/bindings/Vitest.res +0 -26
  94. package/src/bindings/Vitest.res.mjs +0 -3
  95. package/src/db/Table.res +24 -150
  96. package/src/db/Table.res.mjs +38 -272
  97. package/src/sources/AddressSet.res +0 -2
  98. package/src/sources/AddressStore.res +0 -3
  99. package/src/sources/ChainSources.res +2 -0
  100. package/src/sources/ChainSources.res.mjs +2 -2
  101. package/src/sources/EvmChain.res +4 -7
  102. package/src/sources/EvmChain.res.mjs +5 -4
  103. package/src/sources/EvmHyperSyncSource.res +2 -2
  104. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  105. package/src/sources/EvmRpcClient.res +13 -3
  106. package/src/sources/EvmRpcClient.res.mjs +4 -3
  107. package/src/sources/EvmRpcWs.res +4 -3
  108. package/src/sources/EvmRpcWs.res.mjs +2 -2
  109. package/src/sources/HeightFeed.res +31 -10
  110. package/src/sources/HeightFeed.res.mjs +10 -6
  111. package/src/sources/HeightStream.res +32 -17
  112. package/src/sources/HeightStream.res.mjs +10 -10
  113. package/src/sources/HyperSync.res +1 -2
  114. package/src/sources/HyperSync.res.mjs +1 -2
  115. package/src/sources/HyperSyncSSE.res +4 -6
  116. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  117. package/src/sources/RpcSource.res +10 -1
  118. package/src/sources/RpcSource.res.mjs +4 -3
  119. package/src/sources/Source.res +3 -0
  120. package/src/sources/SourceManager.res +85 -104
  121. package/src/sources/SourceManager.res.mjs +18 -37
  122. package/src/sources/SourceManager.resi +3 -5
  123. package/src/sources/StartBlockResolver.res +1 -0
  124. package/src/sources/StartBlockResolver.res.mjs +4 -3
  125. package/src/sources/SvmHyperSyncSource.res +2 -2
  126. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  127. package/svm.schema.json +3 -3
  128. package/src/EffectCacheLookup.res +0 -98
  129. package/src/EffectCacheLookup.res.mjs +0 -116
  130. package/src/PgWriting.res +0 -36
  131. package/src/PgWriting.res.mjs +0 -34
  132. package/src/PreloadLookahead.res +0 -184
  133. package/src/PreloadLookahead.res.mjs +0 -197
  134. package/src/SubgraphLog.res +0 -19
  135. package/src/SubgraphLog.res.mjs +0 -26
  136. package/src/SyncWrites.res +0 -101
  137. package/src/SyncWrites.res.mjs +0 -98
  138. package/src/db/IndexDefinition.res +0 -47
  139. package/src/db/IndexDefinition.res.mjs +0 -60
  140. package/src/sources/CallSource.res +0 -274
  141. package/src/sources/CallSource.res.mjs +0 -196
  142. package/src/subgraph/abi-types.ts +0 -84
  143. package/src/subgraph/aggregations.ts +0 -745
  144. package/src/subgraph/as-package.ts +0 -143
  145. package/src/subgraph/assemblyscript-regex.ts +0 -67
  146. package/src/subgraph/assemblyscript.ts +0 -731
  147. package/src/subgraph/blocks.ts +0 -398
  148. package/src/subgraph/calls-source.ts +0 -702
  149. package/src/subgraph/calls.ts +0 -258
  150. package/src/subgraph/conformance.ts +0 -106
  151. package/src/subgraph/errors.ts +0 -85
  152. package/src/subgraph/graft.ts +0 -73
  153. package/src/subgraph/graph-cli-worker.mjs +0 -35
  154. package/src/subgraph/graph-cli.ts +0 -900
  155. package/src/subgraph/graph-node-decimal.ts +0 -199
  156. package/src/subgraph/graph-ts-enums.ts +0 -28
  157. package/src/subgraph/graph-ts-types/VERSION +0 -2
  158. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +0 -70
  159. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +0 -327
  160. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +0 -233
  161. package/src/subgraph/graph-ts-types/chain/near.d.ts +0 -253
  162. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +0 -32
  163. package/src/subgraph/graph-ts-types/common/collections.d.ts +0 -136
  164. package/src/subgraph/graph-ts-types/common/conversion.d.ts +0 -11
  165. package/src/subgraph/graph-ts-types/common/datasource.d.ts +0 -30
  166. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  167. package/src/subgraph/graph-ts-types/common/json.d.ts +0 -17
  168. package/src/subgraph/graph-ts-types/common/numbers.d.ts +0 -120
  169. package/src/subgraph/graph-ts-types/common/value.d.ts +0 -120
  170. package/src/subgraph/graph-ts-types/common/yaml.d.ts +0 -90
  171. package/src/subgraph/graph-ts-types/global/global.d.ts +0 -194
  172. package/src/subgraph/graph-ts-types/helper-functions.d.ts +0 -22
  173. package/src/subgraph/graph-ts-types/index.d.ts +0 -102
  174. package/src/subgraph/graph-ts.ts +0 -4280
  175. package/src/subgraph/hosts.ts +0 -554
  176. package/src/subgraph/log.ts +0 -28
  177. package/src/subgraph/receipts.ts +0 -184
  178. package/src/subgraph/register-slots.ts +0 -93
  179. package/src/subgraph/rollup.ts +0 -109
  180. package/src/subgraph/rpc.ts +0 -337
  181. package/src/subgraph/runtime.ts +0 -2060
  182. package/src/subgraph/scope.ts +0 -177
  183. package/src/subgraph/stats.ts +0 -42
  184. package/src/subgraph/template-instances.ts +0 -66
  185. package/src/subgraph/toolchain.ts +0 -327
  186. package/src/subgraph/transient.ts +0 -176
@@ -1,2060 +0,0 @@
1
- /**
2
- * Turns a translated manifest into envio registrations.
3
- *
4
- * Each manifest handler becomes an `onEvent` / `onBlock` / `contractRegister`
5
- * wrapper that runs the mapping synchronously inside `runSync`: reads try the
6
- * in-memory state first, a miss schedules the async op and suspends, and the
7
- * replay loop reruns the mapping once what it asked for has landed.
8
- */
9
-
10
- import { instancesAt, rememberInstance } from "./template-instances.ts";
11
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
- import { registerHooks } from "node:module";
13
- import { fileURLToPath, pathToFileURL } from "node:url";
14
- import path from "node:path";
15
- import { indexer } from "../Api.res.mjs";
16
- import { Db, Hasura, serverPort } from "../Env.res.mjs";
17
- import { info, setLogger, warn } from "./log.ts";
18
- import { cached as storedEffectOutput, resetStartupTables } from "../EffectCacheLookup.res.mjs";
19
- import {
20
- subgraphFileToJavascript,
21
- subgraphRegisterHandlers,
22
- subgraphSureCreates,
23
- subgraphWorkspacePackages,
24
- subgraphWorkspaceRoot,
25
- subgraphScriptsWriting,
26
- } from "../Core.res.mjs";
27
- import { install as installCallSource } from "../sources/CallSource.res.mjs";
28
- import { fetchCalls, resetCallBackends } from "./calls-source.ts";
29
- import {
30
- currentScope,
31
- runInScope,
32
- runInScopeUntilSettled,
33
- type FileCreate,
34
- type RegisterPass,
35
- type Scope,
36
- type SubgraphSchema,
37
- } from "./scope.ts";
38
- import {
39
- contextOf,
40
- assemblyScriptPrimitives,
41
- changetype,
42
- retagChangetype,
43
- retagEach,
44
- installCallHook,
45
- installLegacySignatureHook,
46
- installHosts,
47
- installHandlerCreateHook,
48
- setLegacyNullOperands,
49
- installRegisterHook,
50
- installRegisterMissHook,
51
- installFileTemplates,
52
- fileCreateId,
53
- decodeBase64,
54
- ethereum,
55
- type EventInput,
56
- type EventKind,
57
- json as jsonNamespace,
58
- makeBlockHandlerBlock,
59
- makeEvent as makeGraphEvent,
60
- makeCall as makeGraphCall,
61
- makeCallBlock,
62
- callInputDecodes,
63
- type CallKind,
64
- commitRegisterWrites,
65
- resetRegisterStore,
66
- setRegisterFrontier,
67
- POSITION_BLOCK,
68
- ROUND_SPAN,
69
- ethereumValueToJs,
70
- } from "./graph-ts.ts";
71
- import { encodeArg, decodeArg, makeCallEffect } from "./calls.ts";
72
- import { functionSignature } from "./abi-types.ts";
73
- import { resetRpcClients } from "./rpc.ts";
74
- import {
75
- AS_MAP,
76
- AS_SET,
77
- AsMap,
78
- AsSet,
79
- Pointer,
80
- builtinMax,
81
- builtinMin,
82
- memory as assemblyScriptMemory,
83
- EVENT_CLASSES_EXPORT,
84
- MODULE_INIT,
85
- OPERATORS_HELPER,
86
- RETAG_EACH_HELPER,
87
- RETAG_HELPER,
88
- operators,
89
- resetNullObjects,
90
- stringEncodings,
91
- } from "./assemblyscript.ts";
92
- import { makeHostEffects, normalizeIpfsPath, type HostEffects } from "./hosts.ts";
93
- import { createHash } from "node:crypto";
94
- import { advanceRollup } from "./rollup.ts";
95
- import { graftBaseNotice } from "./graft.ts";
96
- import { fetchMappingPackages, missingMappingPackages } from "./toolchain.ts";
97
- import { ensureGeneratedCode, generatedDir, missingGeneratedCode, typeCheckMappings } from "./graph-cli.ts";
98
- import { unknown, unsupported } from "./errors.ts";
99
- import { runSyncCounted } from "./stats.ts";
100
- import { resolveAssemblyScriptPackage, resolveWorkspacePackage, unwrittenWorkspaceEntry } from "./as-package.ts";
101
- import { registerSlots } from "./register-slots.ts";
102
-
103
- const SHIM_URL = new URL("./graph-ts.ts", import.meta.url).href;
104
- const REGEX_SHIM_URL = new URL("./assemblyscript-regex.ts", import.meta.url).href;
105
- const ENVIO_PACKAGE_URL = new URL("../../", import.meta.url).href;
106
-
107
- const jsonFromString = (line: string) => (jsonNamespace as any).fromString(line);
108
-
109
- /**
110
- * Skips the preload pass, so every read a mapping makes misses and the handler
111
- * replays as often as it possibly can. The replay-stress suite runs the whole
112
- * subgraph suite this way: a mapping must produce the same store whether its
113
- * reads were preloaded or not.
114
- */
115
- const skipPreload = process.env.ENVIO_SUBGRAPH_SKIP_PRELOAD === "1";
116
-
117
- /** Escape hatch from the create analysis: every handler gets a register pass. */
118
- const registerEveryHandler = process.env.ENVIO_SUBGRAPH_REGISTER_ALL_HANDLERS === "1";
119
-
120
- type EventHandler = {
121
- event: string;
122
- name: string;
123
- handler: string;
124
- receipt: boolean;
125
- inputs?: EventInput[];
126
- /** `topic1`–`topic3`, keyed by position among the indexed parameters. */
127
- /** The ABI declares the event anonymous: topic N is its Nth indexed param, from 0. */
128
- anonymous?: boolean;
129
- topics?: Record<string, string[]>;
130
- };
131
- type CallHandler = {
132
- function: string;
133
- handler: string;
134
- /** The call trigger the translator minted for it. */
135
- name: string;
136
- inputs?: { name: string; type: string }[];
137
- outputs?: { name: string; type: string }[];
138
- };
139
- /** As the translator serialises its `BlockFilter`. */
140
- type BlockHandler = {
141
- handler: string;
142
- filter: { every: number } | "once" | "call";
143
- /** The call trigger a `filter: call` handler runs on. */
144
- callEvent?: string;
145
- /** The block fields the mapping reads, by envio's names. */
146
- blockFields?: string[];
147
- };
148
- type DataSource = {
149
- kind: string;
150
- name: string;
151
- network?: string;
152
- chainId?: number;
153
- address?: string;
154
- startBlock?: number;
155
- endBlock?: number;
156
- mappingFile: string;
157
- apiVersion?: string;
158
- /** ABI name to file, relative to `root`. */
159
- abis?: Record<string, string>;
160
- entities?: string[];
161
- eventHandlers: EventHandler[];
162
- callHandlers?: CallHandler[];
163
- blockHandlers: BlockHandler[];
164
- context?: Record<string, { type: string; data: string }>;
165
- isTemplate: boolean;
166
- };
167
- type SubgraphConfig = {
168
- specVersion: string;
169
- dataSources: DataSource[];
170
- templates: DataSource[];
171
- declaresEthCalls: boolean;
172
- /** Set when the manifest has a `graft:` block; the indexer ignores it. */
173
- graft?: { base: string; block: number };
174
- graftNotice?: string;
175
- /** What the translation left out of the config and why, logged at startup. */
176
- notices?: string[];
177
- timeseriesEntities?: string[];
178
- skipDuplicateEntities?: string[];
179
- aggregations?: SubgraphSchema["aggregations"];
180
- /** The config's contract name for each data source and template. */
181
- contractAccessors: Record<string, string>;
182
- root: string;
183
- /** The manifest's name within `root`: `--config` may name another than subgraph.yaml. */
184
- manifestFile?: string;
185
- rpcUrls: string[];
186
- /** Per chain id, the reorg depth a block handler's header reads are held back by. */
187
- blockHeaderLag?: Record<number, number>;
188
- /** Chains HyperSync doesn't serve: their headers and receipts come from RPC alone. */
189
- rpcOnlyChains?: number[];
190
- /** Set when envio wrote the manifest from the repo's template, or ran the codegen steps that write it. */
191
- renderedManifest?: {
192
- script: string;
193
- inlined?: string[];
194
- network: string;
195
- template: string;
196
- view: string;
197
- steps?: string[];
198
- commands?: string[];
199
- };
200
- /** Files the manifest names, as written and where they are read from (another case, or a fetched package). */
201
- pathFixes?: [string, string][];
202
- /** The network networks.json was applied for when none was picked: the repo build's `--network`. */
203
- buildNetwork?: string;
204
- isDev: boolean;
205
- } & SubgraphSchema;
206
-
207
- const AS_DATE = Symbol.for("envio.subgraph.date");
208
-
209
- /** `file` as the filesystem spells it, each missing component matched to the one entry differing only in case. */
210
- function onDiskPath(file: string): string | null {
211
- if (existsSync(file)) return file;
212
- const parent = path.dirname(file);
213
- if (parent === file) return null;
214
- const dir = onDiskPath(parent);
215
- if (dir === null) return null;
216
- let entries: string[];
217
- try {
218
- entries = readdirSync(dir);
219
- } catch {
220
- return null;
221
- }
222
- const name = path.basename(file).toLowerCase();
223
- const matches = entries.filter((entry) => entry.toLowerCase() === name);
224
- return matches.length === 1 ? path.join(dir, matches[0]!) : null;
225
- }
226
-
227
- const caseWarned = new Set<string>();
228
-
229
- /**
230
- * A project file's relative import that exists only in another case
231
- * (`../generated/staking/staking` for codegen's `generated/Staking/Staking.ts`):
232
- * asc resolves it on the macOS or Windows filesystem the subgraph builds on.
233
- */
234
- function caseMatchedImport(specifier: string, parentURL: string | undefined): string | null {
235
- if (!projectRoot || !parentURL?.startsWith(projectRoot) || parentURL.includes("/node_modules/")) return null;
236
- const parent = fileURLToPath(parentURL);
237
- const base = path.resolve(path.dirname(parent), specifier);
238
- for (const candidate of [base, `${base}.ts`, path.join(base, "index.ts")]) {
239
- const found = onDiskPath(candidate);
240
- if (found === null || found === candidate || !statSync(found).isFile()) continue;
241
- if (!pathToFileURL(found).href.startsWith(projectRoot)) continue;
242
- const key = `${parent}\0${specifier}`;
243
- if (!caseWarned.has(key)) {
244
- caseWarned.add(key);
245
- warn(
246
- `Envio Subgraph: ${path.relative(fileURLToPath(projectRoot), parent)} imports "${specifier}", and the repo has ` +
247
- `${path.relative(path.dirname(parent), found)}. They match only on a filesystem that ignores case ` +
248
- "(macOS's and Windows' by default), where the subgraph builds; envio loads the file the repo has.",
249
- );
250
- }
251
- return found;
252
- }
253
- return null;
254
- }
255
-
256
- /**
257
- * Where a call sits in its chain for the register pass's store: after every log
258
- * of its block, then by transaction and position in the transaction. graph-node
259
- * orders a call by its transaction among the logs (after that transaction's own
260
- * events); the handler pass follows that order exactly, so this only steers
261
- * what a register pass may read — a create decided on state written by a log of
262
- * a later transaction of the same block reads it one transaction early.
263
- */
264
- function callPosition(event: any): number {
265
- const transaction = Math.min(Number(event.transaction?.transactionIndex ?? 0), 499);
266
- return event.block.number * POSITION_BLOCK + 500_000 + transaction * 1000 + Math.min(event.logIndex, 999);
267
- }
268
-
269
- /**
270
- * A mapping may import AssemblyScript from outside the manifest's directory:
271
- * graph codegen writes `generated/` beside the package.json while the manifest,
272
- * schema and mappings sit in `src/` (StakeWise). Any TypeScript that is neither
273
- * a dependency nor envio's own is the project's.
274
- */
275
- function isOutsideSource(url: string): boolean {
276
- return (
277
- url.startsWith("file:") &&
278
- url.endsWith(".ts") &&
279
- !url.endsWith(".d.ts") &&
280
- !url.includes("/node_modules/") &&
281
- !url.startsWith(ENVIO_PACKAGE_URL)
282
- );
283
- }
284
-
285
- /** A bare import Node resolved to a dependency's JavaScript, which has no AssemblyScript entry at all. */
286
- function isJavaScriptPackage(specifier: string, url: string | undefined, parentUrl: string): boolean {
287
- const bare = !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.includes(":");
288
- return (
289
- bare &&
290
- url !== undefined &&
291
- url.includes("/node_modules/") &&
292
- /\.[cm]?js$/.test(url) &&
293
- resolveAssemblyScriptPackage(specifier, parentUrl) === null
294
- );
295
- }
296
-
297
- let hooksInstalled = false;
298
- /**
299
- * Some mapping is below apiVersion 0.0.5, where graph-node reads through a null
300
- * pointer instead of trapping. Files are compiled once for the whole project,
301
- * so one old mapping makes every file tolerant.
302
- */
303
- let legacyNulls = false;
304
- let projectRoot: string | null = null;
305
- /**
306
- * `node_modules` envio fetched for packages the mappings import and the
307
- * project can't resolve (its own install failed or never ran). A parent URL
308
- * inside it is where those specifiers are resolved from, and so are the
309
- * packages' own dependencies.
310
- */
311
- let fetchedModulesParent: string | null = null;
312
- /** The packages of the workspace the project sits in, by name, which an install would link. */
313
- let workspacePackages = new Map<string, string>();
314
- let workspaceRoot: string | null = null;
315
- /** Every project file the load hook has compiled, so their exports can be read back. */
316
- const projectModules = new Set<string>();
317
-
318
- /**
319
- * Mappings resolve `@graphprotocol/graph-ts` to the shim. The project's own
320
- * files — its mappings and `generated/` — are AssemblyScript, and load through
321
- * the addon, which turns each into the JavaScript that computes what `asc`
322
- * would (`subgraph/assemblyscript.rs`).
323
- */
324
- function installResolveHook(root: string) {
325
- projectRoot = pathToFileURL(path.resolve(root) + path.sep).href;
326
- workspacePackages = new Map(Object.entries(subgraphWorkspacePackages(root)));
327
- workspaceRoot = subgraphWorkspaceRoot(root);
328
- if (hooksInstalled) return;
329
- hooksInstalled = true;
330
- // The addon's output carries an inline source map back to the mapping, so a
331
- // stack trace names the line the developer wrote.
332
- process.setSourceMapsEnabled(true);
333
-
334
- registerHooks({
335
- resolve(specifier: string, context: any, nextResolve: any) {
336
- if (
337
- specifier === "@graphprotocol/graph-ts" ||
338
- specifier.startsWith("@graphprotocol/graph-ts/") ||
339
- // Matchstick re-exports graph-ts' `log`, and some mappings import it
340
- // from there; its test helpers have no meaning outside a test.
341
- specifier === "matchstick-as" ||
342
- specifier.startsWith("matchstick-as/")
343
- ) {
344
- return { url: SHIM_URL, shortCircuit: true };
345
- }
346
- // A library of AssemblyScript's own memory-level matching, which the
347
- // JavaScript transform of a mapping library can't run.
348
- if (specifier === "assemblyscript-regex") return { url: REGEX_SHIM_URL, shortCircuit: true };
349
- let resolved;
350
- try {
351
- resolved = nextResolve(specifier, context);
352
- } catch (error: any) {
353
- const caseMatched =
354
- error?.code === "ERR_MODULE_NOT_FOUND" && specifier.startsWith(".")
355
- ? caseMatchedImport(specifier, context?.parentURL)
356
- : null;
357
- if (caseMatched) return nextResolve(pathToFileURL(caseMatched).href, context);
358
- const bare = !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.includes(":");
359
- // The workspace's own package before any copy fetched from npm, which
360
- // may not be it.
361
- const workspaceFile = bare ? resolveWorkspacePackage(specifier, workspacePackages) : null;
362
- if (workspaceFile) {
363
- return workspaceFile.endsWith(".ts")
364
- ? { url: workspaceFile, format: "module", shortCircuit: true }
365
- : { url: workspaceFile, shortCircuit: true };
366
- }
367
- const unwritten =
368
- bare && error?.code === "ERR_MODULE_NOT_FOUND"
369
- ? unwrittenWorkspaceEntry(specifier, workspacePackages, workspaceRoot)
370
- : null;
371
- if (unwritten) throw unwritten;
372
- let fetched;
373
- if (fetchedModulesParent && bare && error?.code === "ERR_MODULE_NOT_FOUND") {
374
- try {
375
- fetched = nextResolve(specifier, { ...context, parentURL: fetchedModulesParent });
376
- } catch {}
377
- }
378
- if (fetched) {
379
- resolved = fetched;
380
- } else {
381
- // A library written in AssemblyScript has no JavaScript entry for Node
382
- // to find (as-base64's package.json names no `main`); asc reads its
383
- // assembly/index.ts.
384
- const asFile =
385
- resolveAssemblyScriptPackage(specifier, context?.parentURL) ??
386
- resolveAssemblyScriptPackage(specifier, fetchedModulesParent ?? undefined);
387
- if (!asFile) throw error;
388
- return { url: asFile, shortCircuit: true };
389
- }
390
- }
391
- const parentUrl: string | undefined = context?.parentURL;
392
- const fromProject =
393
- projectRoot !== null &&
394
- typeof parentUrl === "string" &&
395
- (parentUrl.startsWith(projectRoot) || isOutsideSource(parentUrl)) &&
396
- !parentUrl.includes("/node_modules/");
397
- // Packages envio fetched for the mappings resolve from a parent of their own.
398
- const forProject = fromProject || (parentUrl !== undefined && parentUrl === fetchedModulesParent);
399
- if (forProject && isJavaScriptPackage(specifier, resolved?.url, parentUrl!)) {
400
- const importer = fromProject ? path.relative(fileURLToPath(projectRoot!), fileURLToPath(parentUrl!)) : "A mapping";
401
- throw new Error(
402
- `${importer} imports the JavaScript package "${specifier}", which AssemblyScript can't compile: ` +
403
- "asc reads a package's `ascMain`, assembly/index.ts or a .ts entry, and this one has none, so " +
404
- "`graph build` fails on this mapping too.",
405
- );
406
- }
407
- // A subgraph project's package.json has no `type`, so its mappings and
408
- // its `generated/` would load as CommonJS and reach the shim — a real ES
409
- // module — through `require()`, which Node refuses inside a cycle. They
410
- // are ES modules; saying so is what lets them import it.
411
- if (projectRoot && resolved?.url && (resolved.url.startsWith(projectRoot) || isOutsideSource(resolved.url))) {
412
- return { ...resolved, format: "module" };
413
- }
414
- return resolved;
415
- },
416
- // Taken over before any other loader sees the file: a TypeScript
417
- // transpiler would refuse AssemblyScript it has no reason to accept, and
418
- // would strip `changetype`'s type argument before it could be read.
419
- load(url: string, context: any, nextLoad: any) {
420
- const isProjectFile =
421
- projectRoot !== null && (url.startsWith(projectRoot) || isOutsideSource(url)) && !url.includes("/node_modules/");
422
- // A mapping library is AssemblyScript too — OpenZeppelin's
423
- // @amxx/graphprotocol-utils, @protofire/subgraph-toolkit — and Node
424
- // won't even strip types under node_modules. envio's own sources are
425
- // TypeScript, not AssemblyScript, and stay out of it.
426
- const isMappingLibrary =
427
- url.includes("/node_modules/") && !url.endsWith(".d.ts") && !url.startsWith(ENVIO_PACKAGE_URL);
428
- if (!projectRoot || !url.endsWith(".ts") || !(isProjectFile || isMappingLibrary)) {
429
- return nextLoad(url, context);
430
- }
431
- const file = fileURLToPath(url);
432
- projectModules.add(url);
433
- return {
434
- format: "module",
435
- source: subgraphFileToJavascript(readFileSync(file, "utf8"), file, legacyNulls),
436
- shortCircuit: true,
437
- };
438
- },
439
- });
440
-
441
- // AssemblyScript builtins the generated code uses as globals.
442
- const globals = globalThis as any;
443
- globals.changetype ??= changetype;
444
- globals[OPERATORS_HELPER] ??= operators;
445
- globals[RETAG_HELPER] ??= retagChangetype;
446
- globals[RETAG_EACH_HELPER] ??= retagEach;
447
- globals[AS_MAP] ??= AsMap;
448
- globals[AS_SET] ??= AsSet;
449
- // AssemblyScript's memory builtins, over the buffers a typed array's
450
- // `dataStart` points into.
451
- globals.memory ??= assemblyScriptMemory;
452
- globals.min ??= builtinMin;
453
- globals.max ??= builtinMax;
454
- globals.unchecked ??= (value: unknown) => value;
455
- globals.String.UTF8 ??= stringEncodings.UTF8;
456
- globals.String.UTF16 ??= stringEncodings.UTF16;
457
- const typedArray = Object.getPrototypeOf(Uint8Array.prototype);
458
- if (!Object.getOwnPropertyDescriptor(typedArray, "dataStart")) {
459
- Object.defineProperty(typedArray, "dataStart", {
460
- get(this: Uint8Array) {
461
- return new Pointer(this.buffer, this.byteOffset);
462
- },
463
- configurable: true,
464
- });
465
- }
466
- globals.assert ??= (value: unknown, message?: string) => {
467
- if (!value) throw new Error(message ?? "assertion failed");
468
- return value;
469
- };
470
- for (const [name, namespace] of Object.entries(assemblyScriptPrimitives)) {
471
- globals[name] ??= namespace;
472
- }
473
-
474
- // AssemblyScript's Date takes and sets its time as an i64, which the shim
475
- // holds as a bigint; JavaScript's refuses one.
476
- if (!(globals.Date as any)[AS_DATE]) {
477
- const JsDate: DateConstructor = globals.Date;
478
- const toNumber = (value: unknown) => (typeof value === "bigint" ? Number(value) : value);
479
- class AsDate extends JsDate {
480
- constructor(...args: unknown[]) {
481
- super(...(args.map(toNumber) as []));
482
- }
483
- setTime(time: number | bigint): number {
484
- return super.setTime(Number(time));
485
- }
486
- static fromString(text: string): Date {
487
- return new AsDate(JsDate.parse(text));
488
- }
489
- }
490
- Object.defineProperty(AsDate, AS_DATE, { value: true });
491
- globals.Date = AsDate;
492
- }
493
-
494
- // AssemblyScript's typed arrays are made over an existing buffer with
495
- // `Uint8Array.wrap(buffer)`; JavaScript has the constructor for it.
496
- for (const Array of [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]) {
497
- (Array as any).wrap ??= (buffer: ArrayBuffer, byteOffset?: number, length?: number) =>
498
- new Array(buffer, byteOffset, length);
499
- }
500
-
501
- // AssemblyScript's DataView reads and writes a 64-bit integer as `u64`/`i64`
502
- // under these names; JavaScript's only as a bigint, under `*Big*` ones.
503
- const view = DataView.prototype as any;
504
- view.getUint64 ??= function (this: DataView, offset: number, littleEndian?: boolean) {
505
- return this.getBigUint64(offset, littleEndian);
506
- };
507
- view.getInt64 ??= function (this: DataView, offset: number, littleEndian?: boolean) {
508
- return this.getBigInt64(offset, littleEndian);
509
- };
510
- view.setUint64 ??= function (this: DataView, offset: number, value: bigint | number, littleEndian?: boolean) {
511
- this.setBigUint64(offset, BigInt.asUintN(64, BigInt(value)), littleEndian);
512
- };
513
- view.setInt64 ??= function (this: DataView, offset: number, value: bigint | number, littleEndian?: boolean) {
514
- this.setBigInt64(offset, BigInt.asIntN(64, BigInt(value)), littleEndian);
515
- };
516
- }
517
-
518
- /**
519
- * `topicN` narrows the indexed parameter in that topic to the logs whose topic
520
- * is one of the listed words; as a `where`, it narrows to that parameter's
521
- * decoded values. An anonymous event has no signature topic, so topic0 is its
522
- * first indexed parameter, which the translator always narrows. It has refused
523
- * a parameter hashed into its topic, and a word no value encodes to.
524
- */
525
- function topicFilter(handler: EventHandler): { params: Record<string, unknown[]> } | undefined {
526
- const topics = Object.entries(handler.topics ?? {});
527
- if (topics.length === 0) return undefined;
528
- const indexed = (handler.inputs ?? []).filter((input) => input.indexed);
529
- const params: Record<string, unknown[]> = {};
530
- for (const [position, words] of topics) {
531
- const input = indexed[handler.anonymous ? Number(position) : Number(position) - 1];
532
- params[input.key] = words.map((word) => fromTopic(input.type, word));
533
- }
534
- return { params };
535
- }
536
-
537
- function fromTopic(type: string, topic: string): unknown {
538
- const word = topic.toLowerCase().replace(/^0x/, "").padStart(64, "0");
539
- if (type === "address") return `0x${word.slice(24)}`;
540
- if (type === "bool") return BigInt(`0x${word}`) !== 0n;
541
- // A fixed-size byte string is left-aligned in its word.
542
- if (type.startsWith("bytes")) return `0x${word.slice(0, Number(type.slice(5)) * 2)}`;
543
- const bits = Number(type.replace(/^u?int/, "") || 256);
544
- const value = BigInt(`0x${word}`);
545
- return type.startsWith("uint") ? BigInt.asUintN(bits, value) : BigInt.asIntN(bits, value);
546
- }
547
-
548
- /**
549
- * Why `graph codegen` never wrote a generated file a mapping imports: it writes
550
- * `schema.ts`, `templates.ts`, `<data source>/<ABI>.ts` and
551
- * `templates/<template>/<ABI>.ts` for the manifest it runs on, no `index.ts`.
552
- * `graph build` then fails on the import too (asc TS6054), so a project that
553
- * builds has a tree left from an earlier manifest.
554
- */
555
- function notGenerated(root: string, generated: string, message: string, sources: DataSource[]): string | undefined {
556
- const missing = /Cannot find module '([^']+)'/.exec(message)?.[1] ?? /Directory import '([^']+)'/.exec(message)?.[1];
557
- if (!missing) return undefined;
558
- const relative = path.relative(generated, missing);
559
- if (relative.startsWith("..") || path.isAbsolute(relative)) return undefined;
560
- const dir = path.relative(root, generated) || ".";
561
- const stale = "A tree generated for an earlier manifest may still have the file.";
562
- if (message.includes("Directory import")) {
563
- return `graph codegen writes no index.ts in ${dir}/, so \`graph build\` fails on this directory import as well. ${stale}`;
564
- }
565
- const parts = relative.split(path.sep);
566
- const [kind, name, abi] =
567
- parts[0] === "templates" && parts.length === 3
568
- ? ["template", parts[1], parts[2]]
569
- : parts.length === 2
570
- ? ["data source", parts[0], parts[1]]
571
- : [undefined, undefined, undefined];
572
- if (!kind || !name || !abi) return undefined;
573
- const candidates = sources.filter((source) => source.isTemplate === (kind === "template"));
574
- const where = `${dir}/${kind === "template" ? "templates/" : ""}<${kind}>/<ABI>.ts`;
575
- const source = candidates.find((candidate) => candidate.name === name);
576
- const declared = source
577
- ? `its ${kind} "${name}" has no ABI named "${abi.replace(/\.ts$/, "")}" (it has ${Object.keys(source.abis ?? {}).join(", ")})`
578
- : `the manifest declares no ${kind} named "${name}" (it has ${candidates.map((candidate) => candidate.name).join(", ") || "none"})`;
579
- return (
580
- `graph codegen writes ${where} for each ${kind} of the manifest, and ${declared}, so \`graph build\` fails on ` +
581
- `this import as well. ${stale}`
582
- );
583
- }
584
-
585
- /**
586
- * Why a generated file lacks a name a mapping imports from it: graph codegen
587
- * writes a class per entity, event and ABI type it knows, and `graph build`
588
- * fails on any other name too.
589
- */
590
- function notExported(root: string, generated: string, mappingFile: string, message: string): string | undefined {
591
- const found = /The requested module '([^']+)' does not provide an export named '([^']+)'/.exec(message);
592
- if (!found) return undefined;
593
- const [, specifier, name] = found;
594
- if (!specifier.startsWith(".")) return undefined;
595
- const target = path.resolve(path.dirname(path.resolve(root, mappingFile)), specifier);
596
- const file = [target, `${target}.ts`].find((candidate) => existsSync(candidate) && candidate.endsWith(".ts"));
597
- if (!file || !file.startsWith(path.resolve(generated) + path.sep)) return undefined;
598
- const exported = [...readFileSync(file, "utf8").matchAll(/^export class (\w+)/gm)].map(([, klass]) => klass);
599
- const lower = name.toLowerCase();
600
- const similar = exported.filter((klass) => klass.toLowerCase().includes(lower) || lower.includes(klass.toLowerCase()));
601
- const listed = (similar.length > 0 ? similar : exported.slice(0, 8)).sort();
602
- return (
603
- `${path.relative(root, file)} is graph codegen's output for this project and has no ${name}` +
604
- `${listed.length > 0 ? ` (it exports ${listed.join(", ")})` : ""}, so \`graph build\` fails on this import as well.`
605
- );
606
- }
607
-
608
- /**
609
- * Why a project file a mapping imports isn't there: a package.json script
610
- * writes it before `graph build` (Goldfinch's `"prebuild": "ts-node
611
- * ./scripts/update-address-manifest.ts"` writes src/address-manifest.ts).
612
- */
613
- function unwrittenImport(root: string, generated: string, message: string): string | undefined {
614
- const found = /Cannot find module '([^']+)' imported from /.exec(message);
615
- if (!found) return undefined;
616
- const target = found[1]!;
617
- const inside = (dir: string) => target.startsWith(path.resolve(dir) + path.sep);
618
- if (!path.isAbsolute(target) || !inside(root) || inside(generated)) return undefined;
619
- const stem = path.basename(target).replace(/\.[cm]?[jt]s$/, "");
620
- const writers = subgraphScriptsWriting(root, stem) as string[][];
621
- if (writers.length === 0) return undefined;
622
- const file = path.relative(root, target) + (path.extname(target) ? "" : ".ts");
623
- const names = writers.map(([name]) => `"${name}"`).join(", ");
624
- return writers.length === 1
625
- ? `${file}, which the mapping imports, isn't in the repo: the package.json script ${names} writes it ` +
626
- `(${writers[0]![1]}), and \`graph build\` fails on this import too until it has run. Run it, then start ` +
627
- "envio again."
628
- : `${file}, which the mapping imports, isn't in the repo: the package.json scripts ${names} write it, and ` +
629
- "`graph build` fails on this import too until one has run. Run the one for your network, then start " +
630
- "envio again.";
631
- }
632
-
633
- async function loadMapping(
634
- root: string,
635
- generated: string,
636
- mappingFile: string,
637
- scope: Scope,
638
- sources: DataSource[] = [],
639
- ): Promise<Record<string, any>> {
640
- const url = pathToFileURL(path.resolve(root, mappingFile)).href;
641
- try {
642
- // graph-node instantiates the module once per data source, with a host
643
- // context already in place, so a mapping may read `dataSource.network()`
644
- // at module top level — Balancer's constants do. The import runs inside a
645
- // scope carrying that data source for the same reason.
646
- return await runInScopeUntilSettled(scope, () => import(url));
647
- } catch (exn) {
648
- const message = exn instanceof Error ? exn.message : String(exn);
649
- if (!existsSync(path.resolve(root, mappingFile))) {
650
- throw new Error(
651
- `Envio Subgraph failed to load the mapping ${mappingFile}: the manifest names it as a data source's ` +
652
- `mapping file, and there is no such file under ${path.resolve(root)}. graph-cli refuses the manifest the ` +
653
- 'same way ("File does not exist").',
654
- );
655
- }
656
- // Only reachable when codegen couldn't run up front, so this reports why
657
- // rather than retrying: Node caches a failed resolution for the process.
658
- if (message.includes(generated) && !existsSync(generated)) {
659
- throw missingGeneratedCode(root, generated);
660
- }
661
- // An unknown named import fails at Node's ESM link step, before any Proxy
662
- // in the shim can see it — rewrap it with the mapping that caused it.
663
- const template = undeclaredTemplate(message);
664
- if (template) throw new Error(`Envio Subgraph failed to load the mapping ${mappingFile}.\n ${template}`);
665
- const why =
666
- notGenerated(root, generated, message, sources) ??
667
- notExported(root, generated, mappingFile, message) ??
668
- unwrittenImport(root, generated, message);
669
- throw new Error(`Envio Subgraph failed to load the mapping ${mappingFile}.\n ${message}${why ? `\n${why}` : ""}`);
670
- }
671
- }
672
-
673
- let declaredTemplates: string[] = [];
674
- let manifestName = "subgraph.yaml";
675
-
676
- /**
677
- * daohaus' per-network manifests leave out templates a listed mapping still
678
- * imports; graph codegen writes a class only for a declared template, so
679
- * `graph build` fails on the import as well.
680
- */
681
- function undeclaredTemplate(message: string): string | null {
682
- const missing = message.match(/requested module '([^']*)' does not provide an export named '([^']+)'/);
683
- if (!missing || !/(^|\/)templates(\.ts)?$/.test(missing[1]!) || declaredTemplates.includes(missing[2]!)) return null;
684
- const name = missing[2]!;
685
- const declared = declaredTemplates.length > 0 ? `it declares ${declaredTemplates.join(", ")}` : "it declares none";
686
- return (
687
- `It imports ${name} from generated/templates, and ${manifestName} declares no template ${name} (${declared}). ` +
688
- "graph codegen writes a class only for a template the manifest declares, so `graph build` fails on this " +
689
- "import too: the manifest was likely written for another network or deployment than the mappings."
690
- );
691
- }
692
-
693
- /**
694
- * The generated event class each handler declares, keyed by the handler
695
- * function itself. A manifest's mapping file may re-export handlers written in
696
- * other files (SSV's does), and the class is exported only by the module the
697
- * handler is written in.
698
- */
699
- const eventClasses = new Map<Function, Function>();
700
- const indexedModules = new Set<string>();
701
-
702
- async function indexEventClasses(): Promise<void> {
703
- for (const url of projectModules) {
704
- if (indexedModules.has(url)) continue;
705
- indexedModules.add(url);
706
- let module: Record<string, any>;
707
- try {
708
- // Already evaluated: this only reads its namespace back.
709
- module = await import(url);
710
- } catch {
711
- continue;
712
- }
713
- const classes = module[EVENT_CLASSES_EXPORT];
714
- if (!classes) continue;
715
- for (const [name, Class] of Object.entries(classes)) {
716
- const handler = module[name];
717
- if (typeof handler === "function" && typeof Class === "function") eventClasses.set(handler, Class);
718
- }
719
- }
720
- }
721
-
722
- type Effect = {
723
- name: string;
724
- handler: (args: { input: unknown; context: unknown; cacheKey: string; checkpointId: bigint }) => Promise<unknown>;
725
- };
726
-
727
- const REGISTER_SUSPEND = Symbol("envio.subgraph.register.suspend");
728
-
729
- /** envio's `Suspend` (a ReScript exception) and the register pass's own: control flow, not errors. */
730
- function isBridgeSignal(error: unknown): boolean {
731
- return error === REGISTER_SUSPEND || (typeof error === "object" && error !== null && "RE_EXN_ID" in error);
732
- }
733
-
734
- /**
735
- * Results the register pass has already fetched, across events and blocks.
736
- *
737
- * A host op's input carries the block it is evaluated at, so an entry can never
738
- * go stale — and a factory calling `symbol()` on the same token for every pair
739
- * it creates asks the same question hundreds of times. envio's own effect cache
740
- * is out of reach here: it hangs off the processing context, and this runs at
741
- * fetch time.
742
- */
743
- const registerCache = new Map<string, { value?: unknown; error?: unknown }>();
744
- const REGISTER_CACHE_LIMIT = 20_000;
745
- /**
746
- * The register passes of a response's items run concurrently, so the same op
747
- * at the same block — every pair a factory creates in one block reading the
748
- * factory — is usually still in flight when the next pass asks for it.
749
- */
750
- const registerInFlight = new Map<string, Promise<unknown>>();
751
-
752
- /**
753
- * What the finished register passes cost in host ops (contract calls, balances,
754
- * block lookups) each. `ChainFetching` reads it to decide how many register
755
- * passes one fetched response may hold: a pass that makes no call needs no
756
- * limit, a pass that makes a dozen is what keeps a response from committing.
757
- * Unknown (undefined) until enough passes have finished to say.
758
- */
759
- const registerCost = { hostOps: 0, passes: 0 };
760
- const REGISTER_COST_MIN_PASSES = 20;
761
- // ChainFetching reports each chain's lowest partition frontier after every
762
- // response it applies; the register store keeps the versions above it.
763
- (globalThis as any).__envioSubgraphRegisterFrontier = setRegisterFrontier;
764
- (globalThis as any).__envioSubgraphRegisterCostPerItem = () =>
765
- registerCost.passes < REGISTER_COST_MIN_PASSES ? undefined : registerCost.hostOps / registerCost.passes;
766
-
767
- async function runRegisterHost(effect: Effect, input: unknown, chainId: number): Promise<unknown> {
768
- // A resync after `envio start -r` keeps the stored call answers; the handler
769
- // pass reads them through the effect machinery, this pass reads them here.
770
- const stored = await storedEffectOutput(effect as any, chainId, input as any);
771
- if (stored !== undefined) return stored;
772
- return effect.handler({
773
- input,
774
- // Only `blockTimestamp` reads the context, and only for the chain id, which
775
- // the input already carries for every other host op.
776
- context: { chain: { id: chainId } },
777
- cacheKey: "",
778
- checkpointId: 0n,
779
- });
780
- }
781
-
782
- /**
783
- * The register pass's half of the sync bridge (§5).
784
- *
785
- * `contractRegister` runs at fetch time with a context that can only register
786
- * addresses — no in-memory store, so no effects. But a factory mapping routinely
787
- * reads a contract *before* deciding what to create, so the answer has to arrive
788
- * somehow: the op is started, the mapping is suspended, and the round is
789
- * replayed once it lands. Same shape as the handler pass, with the results
790
- * memoised here instead of in envio's effect tables.
791
- */
792
- /**
793
- * A host op that failed, not a mapping that threw. The two are told apart
794
- * because the register pass forgives a mapping error — see `runRegisterRounds`
795
- * — and forgiving this one would silently drop the `dataSource.create()` calls
796
- * the pass exists to collect.
797
- */
798
- class RegisterHostFailure extends Error {
799
- constructor(op: string, readonly reason: unknown) {
800
- super(`Envio Subgraph could not resolve ${op} while registering contracts: ${String(reason)}`);
801
- this.cause = reason;
802
- }
803
- }
804
-
805
- function registerHostSync(scope: Scope, effect: Effect, input: unknown) {
806
- const key = `${effect.name}\u0000${typeof input === "string" ? input : JSON.stringify(input)}`;
807
- const resolved = scope.resolved ?? new Map();
808
- scope.resolved = resolved;
809
-
810
- const hit = resolved.get(key) ?? registerCache.get(key);
811
- if (hit) {
812
- if ("error" in hit) throw new RegisterHostFailure(effect.name, hit.error);
813
- return hit.value;
814
- }
815
-
816
- const awaiting = scope.awaiting ?? [];
817
- scope.awaiting = awaiting;
818
- let flight = registerInFlight.get(key);
819
- if (!flight) {
820
- registerCost.hostOps++;
821
- const started = runRegisterHost(effect, input, scope.dataSource.chainId);
822
- flight = started;
823
- registerInFlight.set(key, started);
824
- const settle = () => {
825
- if (registerInFlight.get(key) === started) registerInFlight.delete(key);
826
- };
827
- started.then(settle, settle);
828
- }
829
- awaiting.push(
830
- flight.then(
831
- (value) => {
832
- resolved.set(key, { value });
833
- if (registerCache.size >= REGISTER_CACHE_LIMIT) registerCache.clear();
834
- registerCache.set(key, { value });
835
- },
836
- // A failure is remembered for this event only: a transport error that
837
- // fails the batch must not poison every later block too.
838
- (error) => resolved.set(key, { error }),
839
- ),
840
- );
841
- throw REGISTER_SUSPEND;
842
- }
843
-
844
- /**
845
- * A batch's register passes run concurrently, and a pass's writes reach the
846
- * register store only when it completes. Every Comptroller event in Venus's
847
- * first batch missed the Market an earlier event was still creating, and
848
- * repeated its ~20 creation calls at its own block. So a pass that misses an
849
- * entity an earlier pass of its chain also missed, and is still running,
850
- * waits for the earliest such pass once and reads again — the per-key order
851
- * graph-node's sequential processing gives. Independent events stay
852
- * concurrent; a key nobody ends up writing costs one wait.
853
- *
854
- * An earlier pass that hasn't read the key yet — Dolomite's admin handler
855
- * saves each Token after a contract call — isn't among its missers, so a miss
856
- * while any earlier pass of the chain still runs is only provisional: once the
857
- * pass ends, it waits for those passes and runs again before its writes count.
858
- */
859
- const missers = new Map<string, RegisterPass[]>();
860
-
861
- /** Each chain's running passes and the passes waiting on them, by position. */
862
- type ChainPasses = {
863
- running: RegisterPass[];
864
- waiters: { position: number; release: () => void }[];
865
- };
866
- const chainPasses = new Map<number, ChainPasses>();
867
-
868
- /** Passes start in about the order of their events, so this mostly appends. */
869
- function insertByPosition<T extends { position: number }>(list: T[], item: T) {
870
- let at = list.length;
871
- while (at > 0 && list[at - 1].position > item.position) at--;
872
- list.splice(at, 0, item);
873
- }
874
-
875
- function startRegisterPass(chainId: number, position: number): { pass: RegisterPass; settle: () => void } {
876
- let resolve!: () => void;
877
- const pass: RegisterPass = {
878
- chainId,
879
- position,
880
- done: new Promise<void>((done) => (resolve = done)),
881
- settled: false,
882
- missed: [],
883
- provisional: false,
884
- };
885
- const passes = chainPasses.get(chainId) ?? { running: [], waiters: [] };
886
- chainPasses.set(chainId, passes);
887
- insertByPosition(passes.running, pass);
888
- const settle = () => {
889
- pass.settled = true;
890
- passes.running.splice(passes.running.indexOf(pass), 1);
891
- const first = passes.running[0]?.position ?? Infinity;
892
- while (passes.waiters.length > 0 && passes.waiters[0].position <= first) passes.waiters.shift()!.release();
893
- if (passes.running.length === 0 && passes.waiters.length === 0) chainPasses.delete(chainId);
894
- for (const key of pass.missed) {
895
- const remaining = (missers.get(key) ?? []).filter((other) => other !== pass);
896
- if (remaining.length === 0) missers.delete(key);
897
- else missers.set(key, remaining);
898
- }
899
- registerCost.passes++;
900
- resolve();
901
- };
902
- return { pass, settle };
903
- }
904
-
905
- function earlierPassRunning(pass: RegisterPass): boolean {
906
- const first = chainPasses.get(pass.chainId)?.running[0];
907
- return first !== undefined && first.position < pass.position;
908
- }
909
-
910
- function earlierPassesSettled(pass: RegisterPass): Promise<void> {
911
- const passes = chainPasses.get(pass.chainId);
912
- if (!passes || !earlierPassRunning(pass)) return Promise.resolve();
913
- return new Promise((release) => insertByPosition(passes.waiters, { position: pass.position, release }));
914
- }
915
-
916
- function onRegisterMiss(scope: Scope, key: string) {
917
- const pass = scope.pass;
918
- if (!pass) return;
919
- const list = missers.get(key) ?? [];
920
- if (!scope.waitedKeys?.has(key)) {
921
- const earlier = list.find((other) => other.position < pass.position && !other.settled);
922
- if (earlier) {
923
- (scope.waitedKeys ??= new Set()).add(key);
924
- (scope.awaiting ??= []).push(earlier.done);
925
- scope.waitsOnPass = true;
926
- throw REGISTER_SUSPEND;
927
- }
928
- }
929
- if (!list.includes(pass)) {
930
- list.push(pass);
931
- list.sort((a, b) => a.position - b.position);
932
- missers.set(key, list);
933
- pass.missed.push(key);
934
- }
935
- if (!pass.provisional && earlierPassRunning(pass)) pass.provisional = true;
936
- }
937
-
938
- /**
939
- * True when the pass's round must run again: a read missed while an earlier
940
- * pass was running, which may have written it since. Its writes are dropped;
941
- * a create it made stands, and the rerun registers nothing twice.
942
- */
943
- async function rerunProvisional(scope: Scope): Promise<boolean> {
944
- const pass = scope.pass;
945
- if (!pass?.provisional) return false;
946
- pass.provisional = false;
947
- scope.writes = undefined;
948
- const settled = earlierPassesSettled(pass);
949
- await (scope.slot ? scope.slot.pause(settled) : settled);
950
- return true;
951
- }
952
-
953
- /**
954
- * graph-node runs every trigger in a fresh instance of the mapping's module
955
- * (runtime/wasm/src/mapping.rs instantiates it before each `handle_trigger`):
956
- * the files' top-level code runs again in that trigger's context, and no
957
- * global keeps what an earlier trigger left in it. Each project module's
958
- * `__envio_init` reruns its top-level code after that of the modules it
959
- * imports, once per generation.
960
- */
961
- let moduleGeneration = 0;
962
- function freshModuleState(scope: Scope): void {
963
- resetNullObjects();
964
- scope.mappingExports[MODULE_INIT]?.(++moduleGeneration);
965
- }
966
-
967
- // A round answers the one call the mapping stopped at, so a handler making N
968
- // calls before its create() takes N rounds; graph-node has no limit at all.
969
- // The handler pass's own bound (UserContext.maxSyncRounds).
970
- const maxRegisterRounds = 10_000;
971
-
972
- async function runRegisterRounds(scope: Scope, fn: () => void): Promise<void> {
973
- for (let round = 1; round <= maxRegisterRounds; round++) {
974
- scope.awaiting = undefined;
975
- // A suspended round is replayed from the top, so its writes are dropped.
976
- scope.writes = undefined;
977
- let suspended = false;
978
- try {
979
- runInScope(scope, () => {
980
- freshModuleState(scope);
981
- fn();
982
- });
983
- } catch (error) {
984
- if (error !== REGISTER_SUSPEND) {
985
- // A host op that failed is never forgiven: the mapping was about to
986
- // decide what to create from its answer, and the handler pass cannot
987
- // repair a missed registration — addresses are registered at fetch
988
- // time. `untilAnswered` has already outlasted anything transient.
989
- if (error instanceof RegisterHostFailure) throw error;
990
- // This pass exists to collect `dataSource.create()` calls, and it runs
991
- // against a store that only holds what earlier register passes wrote —
992
- // so a handler that assumes its entities exist can throw here even
993
- // though it is perfectly correct, before or after a create. What it
994
- // registered until then stands; the same mapping runs again with the
995
- // real store, and any error that is genuinely the mapping's surfaces
996
- // there.
997
- if (await rerunProvisional(scope)) continue;
998
- commitRegisterWrites(scope);
999
- return;
1000
- }
1001
- suspended = true;
1002
- }
1003
- const awaiting = scope.awaiting;
1004
- if (!suspended) {
1005
- if (await rerunProvisional(scope)) continue;
1006
- commitRegisterWrites(scope);
1007
- return;
1008
- }
1009
- if (!awaiting || awaiting.length === 0) {
1010
- throw new Error(
1011
- "Envio Subgraph suspended the register pass with nothing to wait for. " +
1012
- "Please open an issue: https://github.com/enviodev/hyperindex/issues",
1013
- );
1014
- }
1015
- const everything = Promise.all(awaiting);
1016
- const pausing = scope.waitsOnPass === true;
1017
- scope.waitsOnPass = false;
1018
- await (pausing && scope.slot ? scope.slot.pause(everything) : everything);
1019
- }
1020
- throw new Error(
1021
- `Envio Subgraph replayed a mapping's dataSource.create() pass ${maxRegisterRounds} times ` +
1022
- "without it settling. This usually means the mapping isn't deterministic across reruns.",
1023
- );
1024
- }
1025
-
1026
- /**
1027
- * A subgraph team's first move is to POST a query to the indexer's port, the
1028
- * way graph-node serves one — which answers /metrics only.
1029
- */
1030
- function announceWhereToQuery() {
1031
- info(
1032
- `Envio Subgraph writes the subgraph's entities to Postgres (database "${Db.database}", ` +
1033
- `schema "${Db.publicSchema}"). This process serves no GraphQL: port ${serverPort} answers ` +
1034
- "/metrics and /healthz only. " +
1035
- (Hasura.enabled
1036
- ? `Hasura (${Hasura.url}) serves envio's own GraphQL over those tables, not The Graph's query API. `
1037
- : "") +
1038
- "The Graph's query API (/subgraphs/name/<name>) is served from the same database by " +
1039
- "envio-serve's subgraph API.",
1040
- );
1041
- }
1042
-
1043
- /**
1044
- * The template addresses register passes registered, by chain. A handler pass
1045
- * runs soon after its event's register pass, so the latest ones are enough.
1046
- */
1047
- const registeredTemplates = new Set<string>();
1048
- /** Templates with a block handler: their instances' creation blocks are kept. */
1049
- const blockTemplates = new Set<string>();
1050
- const REGISTERED_TEMPLATES_LIMIT = 100_000;
1051
- const unregisteredCreates = new Set<string>();
1052
-
1053
- /**
1054
- * A `call` block handler is a call handler, and a data source's other block
1055
- * handlers get a register pass of their own for each of their blocks. A
1056
- * template's `once` one gets a pass right after the one that creates each
1057
- * instance; its unfiltered and polling ones run per instance as a handler
1058
- * only: its instances are known only as register passes register them, so no
1059
- * pass of one block can list them.
1060
- */
1061
- function hasRegisterPass(source: DataSource, handler: BlockHandler): boolean {
1062
- return handler.filter === "call" || handler.filter === "once" || !source.isTemplate;
1063
- }
1064
-
1065
- const BLOCK_HANDLER_CREATE =
1066
- "envio registers a template's instances in a register pass ahead of the handlers, and a template's " +
1067
- "unfiltered and polling block handlers get none, nor does a data source's that runs on many blocks " +
1068
- "unless its code surely reaches a template's generated class, so the instances one creates would " +
1069
- "never have their events indexed";
1070
-
1071
- /** A data source's block handler that runs on at most this many blocks may get a pass on suspicion. */
1072
- const FEW_BLOCK_HANDLER_BLOCKS = 1_000;
1073
-
1074
- /**
1075
- * Where a block handler sits in its block for the register pass's store: a
1076
- * `once` handler ahead of the block's logs (graph-node's
1077
- * `EthereumBlockTriggerType::Start`), any other after its logs and calls, ahead
1078
- * of the data sources the block creates.
1079
- */
1080
- function blockHandlerPosition(blockNumber: number, once: boolean): number {
1081
- return once ? blockNumber * POSITION_BLOCK - 1 : blockNumber * POSITION_BLOCK + ROUND_SPAN - 0.5;
1082
- }
1083
-
1084
- /**
1085
- * The block and round each registered address was created in, by the same key.
1086
- * graph-node runs a data source's events of its creation block in a later round
1087
- * than the handler that created it, so a register pass for them must read what
1088
- * that handler's pass saved, whatever the log order. A round past the position
1089
- * span is clamped.
1090
- */
1091
- const registrationRounds = new Map<string, { block: number; round: number }>();
1092
-
1093
- function roundOf(chainId: number, name: string, address: string, block: number): number {
1094
- const entry = registrationRounds.get(`${chainId}:${name}:${address.toLowerCase()}`);
1095
- return entry !== undefined && entry.block === block ? entry.round : 0;
1096
- }
1097
-
1098
- type FileTemplate = {
1099
- name: string;
1100
- kind: "ipfs" | "arweave";
1101
- /** The mapping function the manifest names. */
1102
- handler: string;
1103
- /** The `entities` the manifest lists: all a file data source may touch. */
1104
- entities: Set<string>;
1105
- mapping: Record<string, any>;
1106
- };
1107
-
1108
- /** The table that records which file data sources have run (`FILE_DATA_SOURCE_ENTITY`, Rust). */
1109
- const FILE_DATA_SOURCE_ENTITY = "EnvioFileDataSource";
1110
- /** A mapping that makes its file data sources create more, forever, is stopped here. */
1111
- const MAX_FILES_PER_TRIGGER = 10_000;
1112
-
1113
- /**
1114
- * Runs the file data sources a handler created, right after it, in the block
1115
- * that created them (graph-node runs them when their file arrives, which is
1116
- * some later block; running at the creating block is deterministic, and
1117
- * entities a file handler writes are in the store the moment the creating block
1118
- * is). Each runs in isolation, as graph-node's do:
1119
- *
1120
- * - only once per template, file and context (a record in its own table, so a
1121
- * reorg or a restart doesn't run it twice, and a second create is ignored);
1122
- * - it may touch only the entities its manifest entry lists, and sees only what
1123
- * it wrote itself — never a chain entity (`graph-ts.ts` store);
1124
- * - a file that isn't there after `ENVIO_SUBGRAPH_FILE_ATTEMPTS` attempts
1125
- * skips the handler, with a warning (graph-node polls on, with backoff);
1126
- * - a CID that doesn't parse is ignored, as graph-node ignores it.
1127
- *
1128
- * A file handler may itself create file data sources; those run after it.
1129
- * During preload only the files are fetched, so a batch's files download
1130
- * concurrently and the execute pass finds them cached.
1131
- */
1132
- function makeFileRunner(
1133
- fileTemplates: Map<string, FileTemplate>,
1134
- hosts: HostEffects,
1135
- schema: SubgraphSchema,
1136
- ) {
1137
- return async (parent: Scope | undefined, context: any): Promise<void> => {
1138
- if (!parent?.fileCreates || parent.fileCreates.size === 0) return;
1139
- const queue: FileCreate[] = [...parent.fileCreates.values()];
1140
- for (let index = 0; index < queue.length; index++) {
1141
- if (index >= MAX_FILES_PER_TRIGGER) {
1142
- throw new Error(
1143
- `A handler created more than ${MAX_FILES_PER_TRIGGER} file data sources from one trigger; ` +
1144
- "a file data source that keeps creating more is not going to stop.",
1145
- );
1146
- }
1147
- const create = queue[index];
1148
- const template = fileTemplates.get(create.template);
1149
- if (!template) continue;
1150
- const path =
1151
- template.kind === "ipfs" ? normalizeIpfsPath(create.source) : create.source.trim().replace(/^\//, "") || null;
1152
- if (path === null) {
1153
- context.log.warn(
1154
- `${template.name}.create() was given "${create.source}", which isn't a content id; ignoring the file data source.`,
1155
- );
1156
- continue;
1157
- }
1158
- const input = `${template.kind}:${path}`;
1159
- if (context.isPreload) {
1160
- await context.effect(hosts.fileData, input);
1161
- continue;
1162
- }
1163
- const handler = template.mapping[template.handler];
1164
- if (typeof handler !== "function") continue;
1165
-
1166
- const doneId = createHash("sha1").update(fileCreateId(create)).digest("hex");
1167
- const done = context[schema.entityAccessors[FILE_DATA_SOURCE_ENTITY]];
1168
- if (await done.get(doneId)) continue;
1169
-
1170
- const content: string | null = await context.effect(hosts.fileData, input);
1171
- if (content === null) {
1172
- context.log.warn(`${template.name}: ${input} isn't available; its handler is skipped.`);
1173
- continue;
1174
- }
1175
- done.set({ id: doneId });
1176
-
1177
- const ran: { scope?: Scope } = {};
1178
- await context.runSync(() =>
1179
- runInScope(
1180
- (ran.scope = {
1181
- context,
1182
- event: parent.event,
1183
- mode: "handler",
1184
- schema,
1185
- dataSource: { ...parent.dataSource, name: template.name, address: path, context: {} },
1186
- registered: new Set(),
1187
- blockNumber: parent.blockNumber,
1188
- position: parent.position,
1189
- mappingExports: template.mapping,
1190
- file: {
1191
- kind: template.kind,
1192
- entities: template.entities,
1193
- written: new Set(),
1194
- context: create.context,
1195
- },
1196
- }),
1197
- () => {
1198
- freshModuleState(ran.scope!);
1199
- return handler(decodeBase64(content));
1200
- },
1201
- ),
1202
- );
1203
- if (ran.scope?.fileCreates) queue.push(...ran.scope.fileCreates.values());
1204
- }
1205
- };
1206
- }
1207
-
1208
- export async function registerSubgraph(config: SubgraphConfig, logger: unknown): Promise<void> {
1209
- setLogger(logger);
1210
- legacyNulls = [...config.dataSources, ...config.templates].some((source) => {
1211
- const [major, minor, patch] = (source.apiVersion ?? "").split(".").map(Number);
1212
- return major === 0 && minor === 0 && patch < 5;
1213
- });
1214
- setLegacyNullOperands(legacyNulls);
1215
- installResolveHook(config.root);
1216
- resetRpcClients();
1217
- resetStartupTables();
1218
- resetCallBackends();
1219
- // The chain's sources read calls only when a handler wants them.
1220
- installCallSource(fetchCalls, config.rpcUrls ?? []);
1221
- resetRegisterStore();
1222
- registeredTemplates.clear();
1223
- blockTemplates.clear();
1224
- for (const template of config.templates) {
1225
- if (template.blockHandlers.length > 0) blockTemplates.add(template.name);
1226
- }
1227
- registrationRounds.clear();
1228
- unregisteredCreates.clear();
1229
- announceWhereToQuery();
1230
- if (config.graftNotice) warn(config.graftNotice);
1231
- for (const notice of config.notices ?? []) warn(notice);
1232
- if (config.graft && process.env.ENVIO_SUBGRAPH_GRAFT_CHECK !== "off") {
1233
- const ownStarts = config.dataSources.flatMap((source) =>
1234
- source.startBlock === undefined ? [] : [source.startBlock],
1235
- );
1236
- // Best effort and not waited on: an offline box or an unpinned base says nothing.
1237
- void graftBaseNotice(config.graft, ownStarts).then(
1238
- (notice) => notice && warn(notice),
1239
- () => undefined,
1240
- );
1241
- }
1242
-
1243
- // One effect for every contract call: envio already batches and dedupes
1244
- // effect calls in preload, and the block number in the input is what keeps
1245
- // a cached result tied to the state the mapping saw.
1246
- const rpcUrls = config.rpcUrls ?? [];
1247
- const callEffect = makeCallEffect(rpcUrls, (encoded) => {
1248
- const hit = registerCache.get(`envio_subgraph_eth_call\u0000${encoded}`);
1249
- return hit && "value" in hit ? (hit.value as string) : undefined;
1250
- });
1251
- const hosts = makeHostEffects(rpcUrls, config.blockHeaderLag ?? {}, config.rpcOnlyChains ?? []);
1252
- const callSync = (effect: unknown, input: unknown) => {
1253
- const scope = currentScope();
1254
- if (scope.mode === "register") {
1255
- return registerHostSync(scope, effect as Effect, input);
1256
- }
1257
- return scope.context.effectSync(effect, input);
1258
- };
1259
-
1260
- installHosts({
1261
- // graph-node's `ipfs.cat` answers null for a path its ContentPath refuses.
1262
- ipfsCat: (hash) => {
1263
- const path = normalizeIpfsPath(hash);
1264
- return path === null ? null : callSync(hosts.ipfsCat, path);
1265
- },
1266
- ipfsMap: (hash, callback, userData, flags) => {
1267
- const scope = currentScope();
1268
- // graph-node: `ensure!(flags.contains("json"), "Flags must contain 'json'")`.
1269
- if (!flags.includes("json")) throw new Error("ipfs.map(): Flags must contain 'json'");
1270
- const fn = scope.mappingExports[callback];
1271
- if (typeof fn !== "function") {
1272
- throw new Error(
1273
- `ipfs.map() names the callback "${callback}", which the mapping doesn't export.`,
1274
- );
1275
- }
1276
- const path = normalizeIpfsPath(hash);
1277
- if (path === null) throw new Error(`ipfs.map(): "${hash}" is not an IPFS content path`);
1278
- const encoded: string | null = callSync(hosts.ipfsMapFile, path);
1279
- if (encoded === null) return;
1280
- const body = Buffer.from(encoded, "base64").toString("utf8");
1281
- // One JSON value per line, each handed to the callback in turn; a line
1282
- // that isn't JSON, or a callback that throws, fails the call, as
1283
- // graph-node's `ipfs_map: callback '<name>' failed when processing file`.
1284
- const lines = body.split("\n");
1285
- for (let index = 0; index < lines.length; index++) {
1286
- const line = lines[index];
1287
- if (line.trim() === "") continue;
1288
- try {
1289
- fn(jsonFromString(line), userData);
1290
- } catch (error) {
1291
- // A suspend is the sync bridge asking for a replay, not a failure.
1292
- if (isBridgeSignal(error) || !(error instanceof Error)) throw error;
1293
- error.message = `ipfs_map: callback '${callback}' failed when processing file '${hash}' (line ${index + 1}): ${error.message}`;
1294
- throw error;
1295
- }
1296
- }
1297
- },
1298
- arweaveData: (txId) => callSync(hosts.arweaveData, txId),
1299
- ensName: (hash) => callSync(hosts.ensName, hash),
1300
- getBalance: (address) =>
1301
- callSync(
1302
- hosts.getBalance,
1303
- JSON.stringify({ address, blockNumber: currentScope().blockNumber }),
1304
- ),
1305
- hasCode: (address) =>
1306
- callSync(
1307
- hosts.hasCode,
1308
- JSON.stringify({ address, blockNumber: currentScope().blockNumber }),
1309
- ),
1310
- blockTimestamp: (blockNumber) =>
1311
- callSync(hosts.blockTimestamp, blockNumber),
1312
- blockHeader: (blockNumber) => callSync(hosts.blockHeader, blockNumber),
1313
- receiptLogs: (transaction) => callSync(hosts.receiptLogs, transaction),
1314
- });
1315
-
1316
- // Reads the scope rather than closing over one context: register passes for
1317
- // the items in a batch run concurrently, and this hook is process-wide.
1318
- installRegisterMissHook(onRegisterMiss);
1319
-
1320
- // A template's `once` handler runs for an instance at the block that created
1321
- // it, in the round after its creator (graph-node's reprocessing of that
1322
- // block for the new data source starts with its `Start` trigger). Its
1323
- // register pass runs right after the pass that created the instance, with
1324
- // that pass's register context, so what it creates is registered at the
1325
- // same block.
1326
- const templateOnces = new Map<string, (parent: Scope, address: string) => Promise<Scope>>();
1327
- const runTemplateOnces = async (parent: Scope): Promise<void> => {
1328
- const queue = [...(parent.templateOnces ?? [])];
1329
- parent.templateOnces = undefined;
1330
- for (let index = 0; index < queue.length; index++) {
1331
- if (index >= MAX_FILES_PER_TRIGGER) {
1332
- throw new Error(
1333
- `A handler created more than ${MAX_FILES_PER_TRIGGER} template instances with a once handler from ` +
1334
- "one trigger; a once handler that keeps creating more is not going to stop.",
1335
- );
1336
- }
1337
- const { template, address } = queue[index]!;
1338
- const nested = await templateOnces.get(template)!(parent, address);
1339
- queue.push(...(nested.templateOnces ?? []));
1340
- }
1341
- };
1342
-
1343
- installRegisterHook((templateName, address) => {
1344
- const accessor = config.contractAccessors[templateName];
1345
- if (accessor === undefined) throw unknown(`the template ${templateName}`, "a mapping handler");
1346
- const scope = currentScope();
1347
- scope.context.chain[accessor].add(address);
1348
- if (templateOnces.has(templateName)) {
1349
- (scope.templateOnces ??= []).push({ template: templateName, address });
1350
- }
1351
- if (blockTemplates.has(templateName)) {
1352
- rememberInstance(scope.dataSource.chainId, templateName, address, scope.blockNumber);
1353
- }
1354
- const key = `${scope.dataSource.chainId}:${templateName}:${address.toLowerCase()}`;
1355
- registeredTemplates.delete(key);
1356
- registeredTemplates.add(key);
1357
- if (!registrationRounds.has(key)) {
1358
- const { dataSource, blockNumber } = scope;
1359
- const round = roundOf(dataSource.chainId, dataSource.name, dataSource.address, blockNumber) + 1;
1360
- registrationRounds.set(key, { block: blockNumber, round });
1361
- }
1362
- if (registeredTemplates.size > REGISTERED_TEMPLATES_LIMIT) {
1363
- const oldest = registeredTemplates.values().next().value!;
1364
- registeredTemplates.delete(oldest);
1365
- registrationRounds.delete(oldest);
1366
- }
1367
- });
1368
-
1369
- // Only the register pass registers an address, at fetch time. A create it
1370
- // didn't make (it threw on an entity no register pass saved) loses that
1371
- // template's events; one from a template's block handler, which has no
1372
- // register pass, is refused.
1373
- installHandlerCreateHook((templateName, address) => {
1374
- const scope = currentScope();
1375
- if (scope.context.isPreload) return;
1376
- const key = `${scope.dataSource.chainId}:${templateName}:${address.toLowerCase()}`;
1377
- if (registeredTemplates.has(key) || unregisteredCreates.has(key)) return;
1378
- if (scope.blockHandler) {
1379
- throw unsupported(
1380
- "creating contract templates from a block handler without a register pass",
1381
- `data source "${scope.dataSource.name}" → blockHandlers → "${scope.blockHandler}", which created ` +
1382
- `${templateName} ${address.toLowerCase()} at block ${scope.blockNumber}`,
1383
- BLOCK_HANDLER_CREATE,
1384
- );
1385
- }
1386
- unregisteredCreates.add(key);
1387
- warn(
1388
- `Envio Subgraph: data source "${scope.dataSource.name}" created the template "${templateName}" for ` +
1389
- `${address.toLowerCase()} at block ${scope.blockNumber} in its handler, but no register pass did, so the ` +
1390
- `events of that ${templateName} are not indexed. A register pass reads only what earlier register passes ` +
1391
- "saved. Please open an issue: https://github.com/enviodev/hyperindex/issues",
1392
- );
1393
- });
1394
-
1395
- const abiFiles = new Map<string, string>();
1396
- for (const source of [...config.dataSources, ...config.templates]) {
1397
- for (const [name, file] of Object.entries(source.abis ?? {})) {
1398
- if (!abiFiles.has(name)) abiFiles.set(name, path.resolve(config.root, file));
1399
- }
1400
- }
1401
- const legacySignatures = new Map<string, string>();
1402
- installLegacySignatureHook((abiName, functionName) => {
1403
- const key = `${abiName}.${functionName}`;
1404
- const known = legacySignatures.get(key);
1405
- if (known) return known;
1406
- const file = abiFiles.get(abiName);
1407
- const signature = file ? functionSignature(JSON.parse(readFileSync(file, "utf8")), functionName) : null;
1408
- if (!signature) {
1409
- throw new Error(
1410
- `Envio Subgraph: the mapping calls ${key} by name, and the manifest's ABI "${abiName}" has no function ` +
1411
- `${functionName}. graph-node fails the call the same way.`,
1412
- );
1413
- }
1414
- legacySignatures.set(key, signature);
1415
- return signature;
1416
- });
1417
-
1418
- installCallHook((call) => {
1419
- const scope = currentScope();
1420
- const encoded = JSON.stringify({
1421
- chainId: scope.dataSource.chainId,
1422
- address: call.contractAddress.toHexString(),
1423
- signature: call.functionSignature,
1424
- args: call.functionParams.map((param: any) => encodeArg(ethereumValueToJs(param))),
1425
- blockNumber: scope.blockNumber,
1426
- });
1427
- const raw = callSync(callEffect, encoded);
1428
- if (raw === null) {
1429
- return { reverted: true, value: null };
1430
- }
1431
- const output = JSON.parse(raw);
1432
- return {
1433
- reverted: output.reverted,
1434
- value: output.values === null ? null : output.values.map(decodeArg),
1435
- };
1436
- });
1437
-
1438
- const schema: SubgraphSchema = {
1439
- timestampFields: config.timestampFields ?? {},
1440
- bytesIdEntities: config.bytesIdEntities ?? [],
1441
- entityListFields: config.entityListFields ?? {},
1442
- entityFields: config.entityFields ?? {},
1443
- entityRefFields: config.entityRefFields ?? {},
1444
- entityFieldTypes: config.entityFieldTypes ?? {},
1445
- entityAccessors: config.entityAccessors,
1446
- timeseriesEntities: config.timeseriesEntities ?? [],
1447
- skipDuplicateEntities: config.skipDuplicateEntities ?? [],
1448
- aggregations: config.aggregations ?? [],
1449
- };
1450
- const secondsOf = (blockNumber: number, given: unknown) => () =>
1451
- given !== undefined && given !== null
1452
- ? Number(given)
1453
- : Number(callSync(hosts.blockTimestamp, blockNumber));
1454
- // Aggregations roll up when a block's time passes a bucket's end, so every
1455
- // handler checks first; the timeseries it saves carry the block's time.
1456
- // graph-node's `SeqGenerator`: one id sequence per block, which its handlers
1457
- // draw from in the order they run, so a timeseries row gets graph-node's id.
1458
- // The execute pass runs a chain's handlers one at a time; a run starts where
1459
- // the block's previous run stopped, and a replayed run starts over there.
1460
- const idSequences = new Map<number, { block: number; next: number }>();
1461
- const idSequence = (scope: Scope) => {
1462
- const chainId = scope.dataSource.chainId;
1463
- let sequence = idSequences.get(chainId);
1464
- if (!sequence || sequence.block !== scope.blockNumber) {
1465
- sequence = { block: scope.blockNumber, next: 0 };
1466
- idSequences.set(chainId, sequence);
1467
- }
1468
- return sequence;
1469
- };
1470
- const endRun = (scope: Scope) => {
1471
- if (scope.timeseriesBase === undefined) return;
1472
- idSequence(scope).next = scope.timeseriesBase + (scope.timeseriesSequence ?? 0);
1473
- };
1474
- const startRun = (scope: Scope) => {
1475
- freshModuleState(scope);
1476
- scope.timeseriesSequence = 0;
1477
- if (!scope.context.isPreload) scope.timeseriesBase ??= idSequence(scope).next;
1478
- // A replayed round creates its file data sources again.
1479
- scope.fileCreates = undefined;
1480
- if (schema.aggregations.length > 0) {
1481
- advanceRollup(scope, BigInt(scope.blockSeconds!()) * 1_000_000n);
1482
- }
1483
- };
1484
-
1485
- // Before any mapping is imported: Node caches a failed module resolution for
1486
- // the life of the process, so generating after the import has already failed
1487
- // wouldn't help.
1488
- const sources = [...config.dataSources, ...config.templates];
1489
- const handlersOf = (source: (typeof sources)[number]): string[] =>
1490
- [...source.eventHandlers, ...(source.callHandlers ?? []), ...source.blockHandlers].map(
1491
- (handler) => handler.handler,
1492
- );
1493
- let registerPassHandlers: Map<(typeof sources)[number], Set<string>> | undefined;
1494
- const generated = generatedDir(
1495
- config.root,
1496
- sources.map((source) => source.mappingFile),
1497
- );
1498
- const fetchedModules = await fetchMappingPackages(
1499
- config.root,
1500
- missingMappingPackages(
1501
- config.root,
1502
- sources.map((source) => source.mappingFile),
1503
- ).filter((name) => !workspacePackages.has(name)),
1504
- );
1505
- fetchedModulesParent = fetchedModules
1506
- ? pathToFileURL(path.join(path.dirname(fetchedModules), "resolve-from.mjs")).href
1507
- : null;
1508
- const rendered = config.renderedManifest;
1509
- manifestName = config.manifestFile ?? "subgraph.yaml";
1510
- declaredTemplates = config.templates.map((template) => template.name);
1511
- if (rendered?.commands?.length) {
1512
- info(
1513
- `Envio Subgraph ran ${rendered.commands.map((command) => `\`${command}\``).join(", ")} from the package.json ` +
1514
- `script "${rendered.script}" first: it is the one that writes ${manifestName}` +
1515
- (rendered.network ? ` (network "${rendered.network}").` : "."),
1516
- );
1517
- } else if (rendered?.steps?.length) {
1518
- info(
1519
- `Envio Subgraph ran ${rendered.steps.map((step) => `\`${step}\``).join(", ")} from the codegen script ` +
1520
- `first: it writes ${manifestName}` +
1521
- (rendered.network ? ` (network "${rendered.network}"; ENVIO_SUBGRAPH_NETWORK picks another).` : "."),
1522
- );
1523
- } else if (rendered) {
1524
- const how =
1525
- rendered.template === rendered.view
1526
- ? `copied ${rendered.template} to ${manifestName}`
1527
- : `rendered ${manifestName} from ${rendered.template} with ${rendered.view}`;
1528
- info(
1529
- `Envio Subgraph ${how} for the network "${rendered.network}", as the package.json script ` +
1530
- `"${rendered.script}" does. ENVIO_SUBGRAPH_NETWORK picks another.`,
1531
- );
1532
- }
1533
- if (config.buildNetwork) {
1534
- info(
1535
- `Envio Subgraph built ${manifestName} for the network "${config.buildNetwork}" from networks.json, ` +
1536
- `as the project's own build does (\`graph build --network ${config.buildNetwork}\`). ` +
1537
- "ENVIO_SUBGRAPH_NETWORK picks another.",
1538
- );
1539
- }
1540
- for (const [written, found] of config.pathFixes ?? []) {
1541
- warn(
1542
- path.isAbsolute(found)
1543
- ? `Envio Subgraph: ${manifestName} names ${written}, in a package the project's install didn't ` +
1544
- `provide; envio reads it from the package fetched into ${found}.`
1545
- : `Envio Subgraph: ${manifestName} names ${written}, and the repo has ${found}. They match only on a ` +
1546
- "filesystem that ignores case (macOS's and Windows' by default), where the subgraph builds; " +
1547
- `envio reads ${found}.`,
1548
- );
1549
- }
1550
- await ensureGeneratedCode(
1551
- config.root,
1552
- generated,
1553
- config.manifestFile,
1554
- sources.map((source) => source.mappingFile),
1555
- {
1556
- renderScripts: rendered && !rendered.steps?.length ? [rendered.script, ...(rendered.inlined ?? [])] : [],
1557
- ranSteps: rendered?.steps,
1558
- pathFixes: config.pathFixes,
1559
- },
1560
- );
1561
-
1562
- if (config.isDev) {
1563
- await typeCheckMappings(config.root, config.manifestFile, config.pathFixes);
1564
- }
1565
-
1566
- // A file template has no contract: nothing is registered for it, so it takes
1567
- // no part in the create analysis or the folding below.
1568
- const isFile = (source: DataSource) => source.kind === "fileIpfs" || source.kind === "fileArweave";
1569
- const templateNames = new Set(
1570
- config.templates.filter((template) => !isFile(template)).map((template) => template.name),
1571
- );
1572
- const sureCreatingBySource = new Map<DataSource, Set<string>>();
1573
- const sureCreating = (source: DataSource): Set<string> => {
1574
- let found = sureCreatingBySource.get(source);
1575
- if (!found) {
1576
- const handlers = source.blockHandlers.map((handler) => handler.handler);
1577
- const created = subgraphSureCreates(path.resolve(config.root, source.mappingFile), handlers, [
1578
- ...templateNames,
1579
- ]);
1580
- found = new Set(handlers.filter((_, index) => created[index]!.length > 0));
1581
- sureCreatingBySource.set(source, found);
1582
- }
1583
- return found;
1584
- };
1585
- // A template's block handler's create is refused when it runs (the create
1586
- // hook above). Whether one will run depends on data — moonwell's Feed
1587
- // handler creates only at the blocks its network's config lists, none on
1588
- // base — so the creates the analysis is sure the code reaches are named up
1589
- // front, as a warning.
1590
- if (templateNames.size > 0) {
1591
- for (const source of sources) {
1592
- const handlers = source.blockHandlers
1593
- .filter((handler) => !hasRegisterPass(source, handler))
1594
- .map((handler) => handler.handler);
1595
- if (source.kind !== "contract" || handlers.length === 0) continue;
1596
- const created = subgraphSureCreates(path.resolve(config.root, source.mappingFile), handlers, [...templateNames]);
1597
- handlers.forEach((handler, index) => {
1598
- if (created[index]!.length === 0) return;
1599
- warn(
1600
- `Envio Subgraph: data source "${source.name}" → blockHandlers → "${handler}" can create ` +
1601
- `${created[index]!.join(", ")}, and envio stops with an error if it does: ${BLOCK_HANDLER_CREATE}.`,
1602
- );
1603
- });
1604
- }
1605
- }
1606
- const fileTemplates = new Map<string, FileTemplate>();
1607
- for (const source of config.templates) {
1608
- if (!isFile(source)) continue;
1609
- const mapping = await loadMapping(config.root, generated, source.mappingFile, {
1610
- context: null,
1611
- event: null,
1612
- mode: "handler",
1613
- schema,
1614
- dataSource: { name: source.name, address: "", chainId: 0, network: "", context: {} },
1615
- registered: new Set(),
1616
- blockNumber: 0,
1617
- mappingExports: {},
1618
- }, sources);
1619
- await indexEventClasses();
1620
- fileTemplates.set(source.name, {
1621
- name: source.name,
1622
- kind: source.kind === "fileArweave" ? "arweave" : "ipfs",
1623
- handler: source.eventHandlers[0]?.handler ?? "",
1624
- entities: new Set(source.entities ?? []),
1625
- mapping,
1626
- });
1627
- }
1628
- installFileTemplates(new Map([...fileTemplates].map(([name, template]) => [name, template.kind])));
1629
- const runFileDataSources = makeFileRunner(fileTemplates, hosts, schema);
1630
-
1631
- // A template sharing a data source's name is folded into its envio contract,
1632
- // so both sets of handlers fire for every address of it. graph-node runs the
1633
- // data source's for its own address and the template's for the ones it
1634
- // created, so each is gated to its side.
1635
- const staticAddresses = new Map<string, Set<string>>();
1636
- for (const source of config.dataSources) {
1637
- if (!source.address || !templateNames.has(source.name)) continue;
1638
- const addresses = staticAddresses.get(source.name) ?? new Set();
1639
- addresses.add(`${source.chainId}:${source.address.toLowerCase()}`);
1640
- staticAddresses.set(source.name, addresses);
1641
- }
1642
- const ownerOf = (source: DataSource) => {
1643
- const addresses = staticAddresses.get(source.name);
1644
- if (!addresses) return () => true;
1645
- return (event: any) =>
1646
- addresses.has(`${event.chainId}:${String(event.srcAddress).toLowerCase()}`) !== source.isTemplate;
1647
- };
1648
-
1649
- for (const source of sources) {
1650
- if (source.kind !== "contract") continue;
1651
- const mapping = await loadMapping(config.root, generated, source.mappingFile, {
1652
- context: null,
1653
- event: null,
1654
- mode: "handler",
1655
- schema,
1656
- dataSource: {
1657
- name: source.name,
1658
- address: source.address ?? "",
1659
- chainId: source.chainId ?? 0,
1660
- network: source.network ?? "",
1661
- context: source.context ?? {},
1662
- },
1663
- registered: new Set(),
1664
- blockNumber: source.startBlock ?? 0,
1665
- mappingExports: {},
1666
- }, sources);
1667
-
1668
- await indexEventClasses();
1669
- const owns = ownerOf(source);
1670
- // graph-node runs none of a data source's handlers past its endBlock
1671
- // (`has_expired`), while its chain and every template instance go on.
1672
- const live = (event: any) =>
1673
- source.isTemplate || source.endBlock === undefined || event.block.number <= source.endBlock;
1674
- // After codegen, so the analysis can follow the mapping into
1675
- // `generated/templates.ts`; for every source at once, as a create in one
1676
- // can hang on what another one's handlers save.
1677
- registerPassHandlers ??=
1678
- templateNames.size === 0
1679
- ? new Map()
1680
- : new Map(
1681
- (registerEveryHandler
1682
- ? sources.map((each) => handlersOf(each))
1683
- : subgraphRegisterHandlers(
1684
- sources.map((each) => path.resolve(config.root, each.mappingFile)),
1685
- sources.map((each) => handlersOf(each)),
1686
- )
1687
- ).map((names: string[], index: number) => [sources[index], new Set(names)]),
1688
- );
1689
- const creating = registerPassHandlers.get(source) ?? new Set<string>();
1690
- // graph-node matches a data source without `source.address` against every
1691
- // contract's logs — ENS's Resolver is one data source for every resolver
1692
- // anyone deploys. Nothing would ever register an address for it here.
1693
- const wildcard = !source.isTemplate && !source.address;
1694
-
1695
- for (const handler of source.eventHandlers) {
1696
- const fn = mapping[handler.handler];
1697
- // `graph build` doesn't check that a named handler is exported — Aave's
1698
- // mainnet manifest names one its mappings renamed years ago — so a
1699
- // subgraph that deploys today would be refused here for a stale line the
1700
- // event it names never reaches.
1701
- if (typeof fn !== "function") continue;
1702
-
1703
- const kind: EventKind = {
1704
- location: `data source "${source.name}" → "${handler.name}"`,
1705
- inputs: handler.inputs ?? [],
1706
- hasReceipt: handler.receipt ?? false,
1707
- };
1708
- // Built as the generated class the handler declares, when it declares
1709
- // one: its `params` getters are graph codegen's own.
1710
- const EventClass = mapping[EVENT_CLASSES_EXPORT]?.[handler.handler] ?? eventClasses.get(fn) ?? ethereum.Event;
1711
- const makeEvent = (event: unknown) => makeGraphEvent(event, kind, EventClass);
1712
-
1713
- const makeScope = (event: any, context: any, mode: Scope["mode"]): Scope => ({
1714
- context,
1715
- event,
1716
- mode,
1717
- schema,
1718
- dataSource: {
1719
- name: source.name,
1720
- address: event.srcAddress,
1721
- chainId: event.chainId,
1722
- network: source.network ?? "",
1723
- context: contextOf(event.chainId, source.name, event.srcAddress) ?? source.context ?? {},
1724
- wildcard,
1725
- },
1726
- registered: new Set(),
1727
- blockNumber: event.block.number,
1728
- blockSeconds: secondsOf(event.block.number, event.block.timestamp),
1729
- position:
1730
- event.block.number * POSITION_BLOCK +
1731
- Math.min(roundOf(event.chainId, source.name, event.srcAddress, event.block.number), 15) * ROUND_SPAN +
1732
- event.logIndex,
1733
- mappingExports: mapping,
1734
- });
1735
-
1736
- const where = topicFilter(handler);
1737
-
1738
- indexer.onEvent(
1739
- { contract: config.contractAccessors[source.name], event: handler.name, where, wildcard },
1740
- async ({ event, context }: any) => {
1741
- if ((skipPreload && context.isPreload) || !owns(event) || !live(event)) return;
1742
- const scope = makeScope(event, context, "handler");
1743
- await runSyncCounted(context, () =>
1744
- runInScope(scope, () => {
1745
- startRun(scope);
1746
- // A fresh event per run: the handler is replayed while it waits on
1747
- // calls, and one that edits its event (Seamless rewrites
1748
- // `event.parameters[2]` into the newer shape) must not meet its own
1749
- // edit on the next round.
1750
- return fn(makeEvent(event));
1751
- }),
1752
- );
1753
- endRun(scope);
1754
- await runFileDataSources(scope, context);
1755
- },
1756
- );
1757
-
1758
- // `dataSource.create` has to reach envio's contractRegister, which runs
1759
- // at fetch time — before any entity exists. The same mapping reruns in
1760
- // register mode, where writes and logs are no-ops and reads are null —
1761
- // only when its code can reach a create (`subgraph/creates.rs`): the
1762
- // rerun costs every contract call the handler makes on the way.
1763
- if (creating.has(handler.handler)) {
1764
- indexer.contractRegister(
1765
- { contract: config.contractAccessors[source.name], event: handler.name, where, wildcard },
1766
- async ({ event, context }: any) => {
1767
- if (!owns(event) || !live(event)) return;
1768
- const scope = makeScope(event, context, "register");
1769
- scope.slot = await registerSlots.acquire(scope.position ?? 0);
1770
- const { pass, settle } = startRegisterPass(event.chainId, scope.position ?? 0);
1771
- scope.pass = pass;
1772
- try {
1773
- await runRegisterRounds(scope, () => fn(makeEvent(event)));
1774
- } finally {
1775
- settle();
1776
- scope.slot?.release();
1777
- }
1778
- await runTemplateOnces(scope);
1779
- },
1780
- );
1781
- }
1782
- }
1783
-
1784
- // A call is an envio item like a log: the source that reads traces produces
1785
- // it, and everything between the fetch and here — partitioning, order, the
1786
- // register pass, rollback — treats it as one. Its params carry the raw
1787
- // calldata and return data, decoded against the function's ABI on demand.
1788
- const callScope = (event: any, context: any, mode: Scope["mode"]): Scope => ({
1789
- context,
1790
- event,
1791
- mode,
1792
- schema,
1793
- dataSource: {
1794
- name: source.name,
1795
- address: event.srcAddress,
1796
- chainId: event.chainId,
1797
- network: source.network ?? "",
1798
- context: source.context ?? {},
1799
- },
1800
- registered: new Set(),
1801
- blockNumber: event.block.number,
1802
- blockSeconds: secondsOf(event.block.number, event.block?.timestamp),
1803
- position: callPosition(event),
1804
- mappingExports: mapping,
1805
- });
1806
-
1807
- const registerCallHandler = (
1808
- trigger: string,
1809
- handlerName: string,
1810
- kind: CallKind | null,
1811
- run: (event: any, call: unknown) => unknown,
1812
- ) => {
1813
- const fn = mapping[handlerName];
1814
- if (typeof fn !== "function") return;
1815
- const CallClass = mapping[EVENT_CLASSES_EXPORT]?.[handlerName] ?? eventClasses.get(fn) ?? ethereum.Call;
1816
- const make = (event: any) =>
1817
- kind === null ? makeCallBlock(event.block) : makeGraphCall(event, kind, CallClass);
1818
- const target = { contract: config.contractAccessors[source.name], event: trigger };
1819
-
1820
- indexer.onEvent(target, async ({ event, context }: any) => {
1821
- if ((skipPreload && context.isPreload) || !owns(event) || !live(event)) return;
1822
- // graph-node skips a call whose calldata doesn't decode against the
1823
- // function, with a warning — a proxy's fallback shares selectors.
1824
- if (kind !== null && !callInputDecodes(kind, event.params.callInput)) {
1825
- if (!context.isPreload) {
1826
- context.log.warn(
1827
- `Skipping a call to ${event.srcAddress} in transaction ${event.transaction.hash}: its input does not decode as ${kind.location}`,
1828
- );
1829
- }
1830
- return;
1831
- }
1832
- const scope = callScope(event, context, "handler");
1833
- // A fresh call per run, as for events: a replayed handler must not meet
1834
- // its own edits.
1835
- await runSyncCounted(context, () =>
1836
- runInScope(scope, () => {
1837
- startRun(scope);
1838
- return run(event, make(event));
1839
- }),
1840
- );
1841
- endRun(scope);
1842
- await runFileDataSources(scope, context);
1843
- });
1844
-
1845
- if (creating.has(handlerName)) {
1846
- indexer.contractRegister(target, async ({ event, context }: any) => {
1847
- if (!owns(event) || !live(event)) return;
1848
- if (kind !== null && !callInputDecodes(kind, event.params.callInput)) return;
1849
- const scope = callScope(event, context, "register");
1850
- scope.slot = await registerSlots.acquire(scope.position ?? 0);
1851
- const { pass, settle } = startRegisterPass(event.chainId, scope.position ?? 0);
1852
- scope.pass = pass;
1853
- try {
1854
- await runRegisterRounds(scope, () => run(event, make(event)));
1855
- } finally {
1856
- settle();
1857
- scope.slot?.release();
1858
- }
1859
- await runTemplateOnces(scope);
1860
- });
1861
- }
1862
- };
1863
-
1864
- for (const handler of source.callHandlers ?? []) {
1865
- registerCallHandler(
1866
- handler.name,
1867
- handler.handler,
1868
- {
1869
- location: `data source "${source.name}" → callHandlers → "${handler.function}"`,
1870
- inputs: handler.inputs ?? [],
1871
- outputs: handler.outputs ?? [],
1872
- },
1873
- (_, call) => mapping[handler.handler](call),
1874
- );
1875
- }
1876
-
1877
- for (const handler of source.blockHandlers) {
1878
- // Once per call to the data source's address in the block, after the
1879
- // block's logs and calls — graph-node's `WithCallTo` block trigger.
1880
- if (handler.filter === "call" && handler.callEvent) {
1881
- registerCallHandler(handler.callEvent, handler.handler, null, (_, block) =>
1882
- mapping[handler.handler](block),
1883
- );
1884
- }
1885
- }
1886
-
1887
- for (const handler of source.blockHandlers) {
1888
- if (handler.filter === "call") continue;
1889
- const fn = mapping[handler.handler];
1890
- if (typeof fn !== "function") continue;
1891
- const start = source.startBlock ?? 0;
1892
- const once = handler.filter === "once";
1893
- const every = typeof handler.filter === "object" ? handler.filter.every : 1;
1894
- // graph-node creates a data source from any handler, a block handler's
1895
- // included, and runs it from that block on. A data source's block handler
1896
- // that can reach a create runs in register mode at each of its blocks,
1897
- // ahead of them, as an event's does; so does every `once` one when there
1898
- // are templates at all: Venus's saves the Comptroller that
1899
- // getOrCreateMarket's register pass reads before it creates a VToken.
1900
- // One that runs on every block, or every few, gets a pass only when the
1901
- // code surely reaches a template's class: anything the analysis can't
1902
- // follow counts as a create, and kiln's vsuite handleBlock, which checks
1903
- // BLS signatures in a module it can't follow, would redo them at every
1904
- // block. A create from one without a pass is refused where it runs.
1905
- const blocksRun = once
1906
- ? 1
1907
- : source.endBlock === undefined
1908
- ? Number.POSITIVE_INFINITY
1909
- : Math.floor((source.endBlock - start) / every) + 1;
1910
- const registers =
1911
- !source.isTemplate &&
1912
- templateNames.size > 0 &&
1913
- (once ||
1914
- (blocksRun <= FEW_BLOCK_HANDLER_BLOCKS
1915
- ? creating.has(handler.handler)
1916
- : sureCreating(source).has(handler.handler)));
1917
- const contractRegister =
1918
- registers
1919
- ? async ({ block, context }: any) => {
1920
- const blockNumber: number = block.number;
1921
- const position = blockHandlerPosition(blockNumber, once);
1922
- const scope: Scope = {
1923
- context,
1924
- event: block,
1925
- mode: "register",
1926
- schema,
1927
- dataSource: {
1928
- name: source.name,
1929
- address: source.address ?? "",
1930
- chainId: source.chainId ?? 0,
1931
- network: source.network ?? "",
1932
- context: source.context ?? {},
1933
- },
1934
- registered: new Set(),
1935
- blockNumber,
1936
- position,
1937
- mappingExports: mapping,
1938
- };
1939
- scope.slot = await registerSlots.acquire(position);
1940
- const { pass, settle } = startRegisterPass(source.chainId ?? 0, position);
1941
- scope.pass = pass;
1942
- try {
1943
- await runRegisterRounds(scope, () => fn(makeBlockHandlerBlock(blockNumber)));
1944
- } finally {
1945
- settle();
1946
- scope.slot?.release();
1947
- }
1948
- await runTemplateOnces(scope);
1949
- }
1950
- : undefined;
1951
- if (source.isTemplate && once && templateNames.size > 0) {
1952
- templateOnces.set(source.name, async (parent, address) => {
1953
- const { chainId } = parent.dataSource;
1954
- const { blockNumber } = parent;
1955
- const round = roundOf(chainId, source.name, address, blockNumber);
1956
- const position = blockNumber * POSITION_BLOCK + Math.min(round, 15) * ROUND_SPAN - 0.5;
1957
- const scope: Scope = {
1958
- context: parent.context,
1959
- event: { number: blockNumber },
1960
- mode: "register",
1961
- schema,
1962
- dataSource: {
1963
- name: source.name,
1964
- address,
1965
- chainId,
1966
- network: source.network ?? "",
1967
- context: contextOf(chainId, source.name, address) ?? source.context ?? {},
1968
- },
1969
- registered: new Set(),
1970
- blockNumber,
1971
- position,
1972
- mappingExports: mapping,
1973
- };
1974
- scope.slot = await registerSlots.acquire(position);
1975
- const { pass, settle } = startRegisterPass(chainId, position);
1976
- scope.pass = pass;
1977
- try {
1978
- await runRegisterRounds(scope, () => fn(makeBlockHandlerBlock(blockNumber)));
1979
- } finally {
1980
- settle();
1981
- scope.slot?.release();
1982
- }
1983
- return scope;
1984
- });
1985
- }
1986
- // A template's instances all exist from the first static source on its
1987
- // chain, and the handler runs once per instance from its creation block.
1988
- const chainStarts = config.dataSources
1989
- .filter((other) => other.chainId === source.chainId)
1990
- .map((other) => other.startBlock ?? 0);
1991
- const first = source.isTemplate ? (chainStarts.length > 0 ? Math.min(...chainStarts) : 0) : start;
1992
- // An instance's creation block is its start block (graph-node), so a
1993
- // template's `once` and `polling` schedules differ by instance: every
1994
- // block is visited and each instance checked.
1995
- const blocks = source.isTemplate
1996
- ? { _gte: first, _every: 1 }
1997
- : handler.filter === "once"
1998
- ? { _gte: start, _lte: start }
1999
- : source.endBlock === undefined
2000
- ? { _gte: first, _every: handler.filter.every }
2001
- : { _gte: start, _lte: source.endBlock, _every: handler.filter.every };
2002
- const scheduled = (created: number, blockNumber: number) =>
2003
- once ? blockNumber === created : (blockNumber - created) % every === 0;
2004
- indexer.onBlock(
2005
- {
2006
- name: `${source.name}_${handler.handler}`,
2007
- where: ({ chain }: { chain: { id: number } }) =>
2008
- chain.id === source.chainId && { block: { number: blocks } },
2009
- runsFirst: once && !source.isTemplate,
2010
- contractRegister,
2011
- // A template's handler visits every block for instances it may not
2012
- // have, and a register pass runs on blocks generated locally: both
2013
- // read their fields afterwards, only where they run.
2014
- fields:
2015
- !source.isTemplate && !contractRegister && (handler.blockFields?.length ?? 0) > 0
2016
- ? { block: handler.blockFields }
2017
- : undefined,
2018
- } as any,
2019
- async ({ block, context }: any) => {
2020
- if (skipPreload && context.isPreload) return;
2021
- const graphBlock = makeBlockHandlerBlock(block.number, block);
2022
- const addresses = source.isTemplate
2023
- ? instancesAt(block.chainId ?? source.chainId, source.name, block.number)
2024
- .filter((instance) => scheduled(instance.block, block.number))
2025
- .map((instance) => instance.address)
2026
- : [source.address ?? ""];
2027
- for (const address of addresses) {
2028
- const scope: Scope = {
2029
- context,
2030
- event: block,
2031
- mode: "handler",
2032
- schema,
2033
- dataSource: {
2034
- name: source.name,
2035
- address,
2036
- chainId: source.chainId ?? 0,
2037
- network: source.network ?? "",
2038
- context: (source.isTemplate ? contextOf(source.chainId ?? 0, source.name, address) : undefined) ?? source.context ?? {},
2039
- },
2040
- registered: new Set(),
2041
- blockNumber: block.number,
2042
- blockSeconds: secondsOf(block.number, block.timestamp),
2043
- mappingExports: mapping,
2044
- blockHandler:
2045
- (source.isTemplate ? hasRegisterPass(source, handler) : registers) ? undefined : handler.handler,
2046
- };
2047
- await runSyncCounted(context, () =>
2048
- runInScope(scope, () => {
2049
- startRun(scope);
2050
- return fn(graphBlock);
2051
- }),
2052
- );
2053
- endRun(scope);
2054
- await runFileDataSources(scope, context);
2055
- }
2056
- },
2057
- );
2058
- }
2059
- }
2060
- }