envio 3.6.1 → 3.7.0-subgraph

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 (114) hide show
  1. package/index.d.ts +197 -9
  2. package/package.json +6 -6
  3. package/src/Batch.res +64 -25
  4. package/src/Batch.res.mjs +69 -46
  5. package/src/ChainFetching.res +13 -17
  6. package/src/ChainFetching.res.mjs +9 -14
  7. package/src/ChainState.res +160 -119
  8. package/src/ChainState.res.mjs +75 -46
  9. package/src/ChainState.resi +15 -12
  10. package/src/Config.res +18 -2
  11. package/src/Config.res.mjs +9 -6
  12. package/src/Core.res +20 -0
  13. package/src/Core.res.mjs +11 -0
  14. package/src/Ecosystem.res +0 -1
  15. package/src/Envio.res +16 -3
  16. package/src/EventConfigBuilder.res +155 -40
  17. package/src/EventConfigBuilder.res.mjs +76 -24
  18. package/src/EventProcessing.res +10 -14
  19. package/src/EventProcessing.res.mjs +12 -10
  20. package/src/EventUtils.res +3 -47
  21. package/src/FetchState.res +124 -104
  22. package/src/FetchState.res.mjs +116 -100
  23. package/src/HandlerLoader.res +20 -8
  24. package/src/HandlerLoader.res.mjs +11 -2
  25. package/src/HandlerRegister.res +108 -2
  26. package/src/HandlerRegister.res.mjs +59 -10
  27. package/src/HandlerRegister.resi +4 -0
  28. package/src/InMemoryStore.res +1 -6
  29. package/src/InMemoryStore.res.mjs +1 -3
  30. package/src/Internal.res +84 -24
  31. package/src/Internal.res.mjs +33 -2
  32. package/src/LazyLoader.res +12 -11
  33. package/src/LazyLoader.res.mjs +13 -7
  34. package/src/Main.res +22 -9
  35. package/src/Main.res.mjs +18 -5
  36. package/src/MemoryStorage.res +756 -0
  37. package/src/MemoryStorage.res.mjs +623 -0
  38. package/src/Persistence.res +4 -2
  39. package/src/PgStorage.res +24 -8
  40. package/src/PgStorage.res.mjs +15 -12
  41. package/src/ReorgDetection.res +0 -222
  42. package/src/ReorgDetection.res.mjs +0 -163
  43. package/src/Rollback.res +14 -15
  44. package/src/Rollback.res.mjs +4 -5
  45. package/src/SimulateItems.res +2 -2
  46. package/src/UserContext.res +358 -51
  47. package/src/UserContext.res.mjs +271 -35
  48. package/src/Utils.res +3 -0
  49. package/src/bindings/ClickHouse.res +16 -11
  50. package/src/bindings/ClickHouse.res.mjs +10 -10
  51. package/src/bindings/Vitest.res +1 -1
  52. package/src/sources/BlockStore.res +115 -5
  53. package/src/sources/BlockStore.res.mjs +25 -0
  54. package/src/sources/Evm.res +0 -1
  55. package/src/sources/Evm.res.mjs +0 -1
  56. package/src/sources/EvmHyperSyncSource.res +19 -84
  57. package/src/sources/EvmHyperSyncSource.res.mjs +24 -60
  58. package/src/sources/Fuel.res +24 -4
  59. package/src/sources/Fuel.res.mjs +23 -3
  60. package/src/sources/FuelHyperSync.res +8 -3
  61. package/src/sources/FuelHyperSync.res.mjs +5 -4
  62. package/src/sources/FuelHyperSync.resi +3 -1
  63. package/src/sources/FuelHyperSyncClient.res +7 -10
  64. package/src/sources/FuelHyperSyncSource.res +18 -45
  65. package/src/sources/FuelHyperSyncSource.res.mjs +18 -35
  66. package/src/sources/HyperSync.res +49 -229
  67. package/src/sources/HyperSync.res.mjs +74 -191
  68. package/src/sources/HyperSync.resi +11 -35
  69. package/src/sources/HyperSyncClient.res +9 -79
  70. package/src/sources/HyperSyncClient.res.mjs +2 -6
  71. package/src/sources/RequestStat.res +5 -0
  72. package/src/sources/RequestStat.res.mjs +2 -0
  73. package/src/sources/RpcSource.res +149 -43
  74. package/src/sources/RpcSource.res.mjs +104 -41
  75. package/src/sources/SimulateSource.res +8 -5
  76. package/src/sources/SimulateSource.res.mjs +6 -6
  77. package/src/sources/Source.res +89 -16
  78. package/src/sources/Source.res.mjs +50 -1
  79. package/src/sources/SourceManager.res +255 -50
  80. package/src/sources/SourceManager.res.mjs +149 -55
  81. package/src/sources/SourceManager.resi +2 -6
  82. package/src/sources/Svm.res +0 -7
  83. package/src/sources/Svm.res.mjs +0 -8
  84. package/src/sources/SvmHyperSyncClient.res +21 -16
  85. package/src/sources/SvmHyperSyncClient.res.mjs +3 -4
  86. package/src/sources/SvmHyperSyncSource.res +25 -117
  87. package/src/sources/SvmHyperSyncSource.res.mjs +8 -121
  88. package/src/subgraph/blocks.ts +176 -0
  89. package/src/subgraph/calls.ts +217 -0
  90. package/src/subgraph/conformance.ts +99 -0
  91. package/src/subgraph/division.ts +110 -0
  92. package/src/subgraph/errors.ts +90 -0
  93. package/src/subgraph/graph-ts-types/VERSION +2 -0
  94. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  95. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  96. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  97. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  98. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  99. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  100. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  101. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  102. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  103. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  104. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  105. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  106. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  107. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  108. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  109. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  110. package/src/subgraph/graph-ts.ts +1871 -0
  111. package/src/subgraph/hosts.ts +148 -0
  112. package/src/subgraph/runtime.ts +845 -0
  113. package/src/subgraph/scope.ts +63 -0
  114. package/svm.schema.json +3 -2
@@ -0,0 +1,845 @@
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 { execFileSync } from "node:child_process";
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ statSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { registerHooks } from "node:module";
20
+ import { pathToFileURL } from "node:url";
21
+ import path from "node:path";
22
+ import { indexer } from "../Api.res.mjs";
23
+ import { currentScope, runInScope, type Scope, type SubgraphSchema } from "./scope.ts";
24
+ import {
25
+ Address,
26
+ assemblyScriptPrimitives,
27
+ BigInt as GraphBigInt,
28
+ Bytes,
29
+ changetype,
30
+ retagChangetype,
31
+ installCallHook,
32
+ installHosts,
33
+ installRegisterHook,
34
+ ethereum,
35
+ json as jsonNamespace,
36
+ makeBlockHandlerBlock,
37
+ valueToJs,
38
+ } from "./graph-ts.ts";
39
+ import { encodeArg, decodeArg, makeCallEffect, resetClients } from "./calls.ts";
40
+ import {
41
+ DIVIDE_HELPER,
42
+ RETAG_HELPER,
43
+ integerDivision,
44
+ loadTypeScript,
45
+ rewriteChangetype,
46
+ rewriteDivision,
47
+ } from "./division.ts";
48
+ import { makeHostEffects } from "./hosts.ts";
49
+ import { unsupported } from "./errors.ts";
50
+
51
+ const SHIM_URL = new URL("./graph-ts.ts", import.meta.url).href;
52
+
53
+ const jsonFromString = (line: string) => (jsonNamespace as any).fromString(line);
54
+
55
+ type EventHandler = {
56
+ event: string;
57
+ name: string;
58
+ handler: string;
59
+ receipt: boolean;
60
+ /** Each parameter's ABI type, keyed by the name envio decodes it under. */
61
+ params?: Record<string, string>;
62
+ };
63
+ type BlockHandler = { handler: string; filter: { Every: number } | "Once" | any };
64
+ type DataSource = {
65
+ kind: string;
66
+ name: string;
67
+ network?: string;
68
+ chainId?: number;
69
+ address?: string;
70
+ startBlock?: number;
71
+ endBlock?: number;
72
+ mappingFile: string;
73
+ eventHandlers: EventHandler[];
74
+ blockHandlers: BlockHandler[];
75
+ context?: Record<string, { type: string; data: string }>;
76
+ isTemplate: boolean;
77
+ };
78
+ type SubgraphConfig = {
79
+ specVersion: string;
80
+ dataSources: DataSource[];
81
+ templates: DataSource[];
82
+ declaresEthCalls: boolean;
83
+ root: string;
84
+ rpcUrls: string[];
85
+ isDev: boolean;
86
+ } & SubgraphSchema;
87
+
88
+ let hooksInstalled = false;
89
+ let projectRoot: string | null = null;
90
+
91
+ /**
92
+ * Mappings resolve `@graphprotocol/graph-ts` to the shim; everything else,
93
+ * including the project's own `generated/`, resolves normally and runs as-is.
94
+ */
95
+ function installResolveHook(root: string) {
96
+ projectRoot = pathToFileURL(path.resolve(root) + path.sep).href;
97
+ // Loaded here rather than from inside the hook: requiring a module while a
98
+ // load hook is on the stack re-enters the loader.
99
+ loadTypeScript(path.resolve(root));
100
+ if (hooksInstalled) return;
101
+ hooksInstalled = true;
102
+
103
+ registerHooks({
104
+ resolve(specifier: string, context: any, nextResolve: any) {
105
+ if (
106
+ specifier === "@graphprotocol/graph-ts" ||
107
+ specifier.startsWith("@graphprotocol/graph-ts/")
108
+ ) {
109
+ return { url: SHIM_URL, shortCircuit: true };
110
+ }
111
+ const resolved = nextResolve(specifier, context);
112
+ // A subgraph project's package.json has no `type`, so its mappings and
113
+ // its `generated/` would load as CommonJS and reach the shim — a real ES
114
+ // module — through `require()`, which Node refuses inside a cycle. They
115
+ // are ES modules; saying so is what lets them import it.
116
+ if (projectRoot && resolved?.url?.startsWith(projectRoot)) {
117
+ return { ...resolved, format: "module" };
118
+ }
119
+ return resolved;
120
+ },
121
+ load(url: string, context: any, nextLoad: any) {
122
+ const loaded = nextLoad(url, context);
123
+ if (!projectRoot || !url.startsWith(projectRoot) || url.includes("/node_modules/")) {
124
+ return loaded;
125
+ }
126
+ const source = loaded?.source;
127
+ if (typeof source !== "string" && !(source instanceof Uint8Array)) return loaded;
128
+ const text = typeof source === "string" ? source : Buffer.from(source).toString("utf8");
129
+ return { ...loaded, source: rewriteChangetype(rewriteDivision(text)) };
130
+ },
131
+ });
132
+
133
+ // AssemblyScript builtins the generated code uses as globals.
134
+ const globals = globalThis as any;
135
+ globals.changetype ??= changetype;
136
+ globals[DIVIDE_HELPER] ??= integerDivision;
137
+ globals[RETAG_HELPER] ??= retagChangetype;
138
+ globals.assert ??= (value: unknown, message?: string) => {
139
+ if (!value) throw new Error(message ?? "assertion failed");
140
+ return value;
141
+ };
142
+ for (const [name, namespace] of Object.entries(assemblyScriptPrimitives)) {
143
+ globals[name] ??= namespace;
144
+ }
145
+ }
146
+
147
+ function blockInterval(handler: BlockHandler): { every?: number; once?: boolean } {
148
+ const filter = handler.filter as any;
149
+ if (filter === "Once" || filter?.Once !== undefined) return { once: true };
150
+ if (typeof filter?.Every === "number") return { every: filter.Every };
151
+ return { every: 1 };
152
+ }
153
+
154
+ const ADDRESS_HEX = /^0x[0-9a-fA-F]{40}$/;
155
+ const ANY_HEX = /^0x[0-9a-fA-F]*$/;
156
+
157
+ type Converter = (value: unknown) => unknown;
158
+
159
+ /**
160
+ * An ABI type as graph codegen types it. Anything that fits in 32 bits is an
161
+ * `i32` in a mapping, and everything wider is a `BigInt` — which the value
162
+ * alone can't tell you, since envio decodes every integer width as a bigint.
163
+ */
164
+ const SMALL_INT = /^u?int(8|16|24|32)?$/;
165
+
166
+ function converterForAbiType(abiType: string): Converter {
167
+ const type = abiType.trim();
168
+ if (type.endsWith("]")) {
169
+ const each = converterForAbiType(type.slice(0, type.lastIndexOf("[")));
170
+ return (v) => (Array.isArray(v) ? v.map(each) : v);
171
+ }
172
+ if (type === "address") return (v) => Address.fromString(v as string);
173
+ if (type === "bool") return (v) => v;
174
+ if (type === "string") return (v) => v;
175
+ if (type.startsWith("bytes")) return (v) => Bytes.fromHexString(v as string);
176
+ // `int`/`uint` with no width are 256-bit.
177
+ if (SMALL_INT.test(type) && type !== "int" && type !== "uint") {
178
+ return (v) => (typeof v === "bigint" ? Number(v) : v);
179
+ }
180
+ if (/^u?int/.test(type)) return (v) => new (GraphBigInt as any)(v as bigint);
181
+ return (v) => v;
182
+ }
183
+
184
+ /** Falls back to the value's own shape for a type the signature didn't carry. */
185
+ function converterForValue(value: unknown): Converter {
186
+ if (typeof value === "bigint") return (v) => new (GraphBigInt as any)(v as bigint);
187
+ if (typeof value === "string" && ADDRESS_HEX.test(value)) {
188
+ return (v) => Address.fromString(v as string);
189
+ }
190
+ if (typeof value === "string" && ANY_HEX.test(value)) {
191
+ return (v) => Bytes.fromHexString(v as string);
192
+ }
193
+ if (Array.isArray(value)) {
194
+ const each = value.length > 0 ? converterForValue(value[0]) : (v: unknown) => v;
195
+ return (v) => (v as unknown[]).map(each);
196
+ }
197
+ return (v) => v;
198
+ }
199
+
200
+ /**
201
+ * An event's parameter types don't vary between occurrences, so the shape is
202
+ * resolved once per event kind rather than per event.
203
+ */
204
+ function convertersFor(
205
+ cache: Map<string, Converter>,
206
+ source: Record<string, unknown>,
207
+ types: Map<string, string>,
208
+ ) {
209
+ for (const [name, value] of Object.entries(source)) {
210
+ if (cache.has(name)) continue;
211
+ const abiType = types.get(name);
212
+ if (abiType) {
213
+ cache.set(name, converterForAbiType(abiType));
214
+ continue;
215
+ }
216
+ // A null carries no shape, and `to` is null on a contract creation. Caching
217
+ // what it implies would pin the identity converter for every later event.
218
+ if (value === null || value === undefined) continue;
219
+ cache.set(name, converterForValue(value));
220
+ }
221
+ return cache;
222
+ }
223
+
224
+ function convertAll(
225
+ cache: Map<string, Converter>,
226
+ source: Record<string, unknown>,
227
+ types: Map<string, string>,
228
+ ) {
229
+ const converters = convertersFor(cache, source, types);
230
+ const out: Record<string, unknown> = {};
231
+ for (const [name, value] of Object.entries(source)) {
232
+ out[name] = (converters.get(name) ?? ((v: unknown) => v))(value);
233
+ }
234
+ return out;
235
+ }
236
+
237
+ /**
238
+ * graph-ts' `ethereum.Block`, `Transaction` and `TransactionReceipt`, paired
239
+ * with the name envio decodes each field under and the graph-ts type it has to
240
+ * arrive as. The names diverge in both directions — graph-ts' `author` is
241
+ * envio's `miner`, its `gasLimit` on a transaction is envio's `gas` — so a
242
+ * mapping reading the graph-ts name gets `undefined` unless it's translated.
243
+ */
244
+ type ShapeField = [graphName: string, rawName: string, kind: "bytes" | "address" | "bigint"];
245
+
246
+ const BLOCK_SHAPE: ShapeField[] = [
247
+ ["hash", "hash", "bytes"],
248
+ ["parentHash", "parentHash", "bytes"],
249
+ ["unclesHash", "sha3Uncles", "bytes"],
250
+ ["author", "miner", "address"],
251
+ ["stateRoot", "stateRoot", "bytes"],
252
+ ["transactionsRoot", "transactionsRoot", "bytes"],
253
+ ["receiptsRoot", "receiptsRoot", "bytes"],
254
+ ["number", "number", "bigint"],
255
+ ["gasUsed", "gasUsed", "bigint"],
256
+ ["gasLimit", "gasLimit", "bigint"],
257
+ ["timestamp", "timestamp", "bigint"],
258
+ ["difficulty", "difficulty", "bigint"],
259
+ ["totalDifficulty", "totalDifficulty", "bigint"],
260
+ ["size", "size", "bigint"],
261
+ ["baseFeePerGas", "baseFeePerGas", "bigint"],
262
+ ];
263
+
264
+ const TRANSACTION_SHAPE: ShapeField[] = [
265
+ ["hash", "hash", "bytes"],
266
+ ["index", "transactionIndex", "bigint"],
267
+ ["from", "from", "address"],
268
+ ["to", "to", "address"],
269
+ ["value", "value", "bigint"],
270
+ ["gasLimit", "gas", "bigint"],
271
+ ["gasPrice", "gasPrice", "bigint"],
272
+ ["input", "input", "bytes"],
273
+ ["nonce", "nonce", "bigint"],
274
+ ];
275
+
276
+ /** envio carries the receipt scalars on the transaction, not beside it. */
277
+ const RECEIPT_SHAPE: ShapeField[] = [
278
+ ["transactionHash", "hash", "bytes"],
279
+ ["transactionIndex", "transactionIndex", "bigint"],
280
+ ["cumulativeGasUsed", "cumulativeGasUsed", "bigint"],
281
+ ["gasUsed", "gasUsed", "bigint"],
282
+ ["contractAddress", "contractAddress", "address"],
283
+ ["status", "status", "bigint"],
284
+ ["root", "root", "bytes"],
285
+ ["logsBloom", "logsBloom", "bytes"],
286
+ ];
287
+
288
+ function graphValue(kind: ShapeField[2], value: unknown): unknown {
289
+ if (value === null || value === undefined) return null;
290
+ if (kind === "bytes") return Bytes.fromHexString(value as string);
291
+ if (kind === "address") return Address.fromString(value as string);
292
+ return new (GraphBigInt as any)(typeof value === "bigint" ? value : BigInt(value as number));
293
+ }
294
+
295
+ function shaped(fields: ShapeField[], raw: Record<string, unknown> | undefined) {
296
+ const out: Record<string, unknown> = {};
297
+ for (const [graphName, rawName, kind] of fields) {
298
+ out[graphName] = graphValue(kind, raw?.[rawName]);
299
+ }
300
+ return out;
301
+ }
302
+
303
+ /**
304
+ * envio capitalizes a contract name for the config it stores, so a data source
305
+ * whose manifest name starts lowercase — `crvUSD` — is `CrvUSD` by the time the
306
+ * runtime looks it up. Register under the name the config actually holds.
307
+ */
308
+ function contractName(name: string): string {
309
+ return name.charAt(0).toUpperCase() + name.slice(1);
310
+ }
311
+
312
+ /**
313
+ * The graph-ts `ethereum.Event` a mapping sees, built per event kind so the
314
+ * refusals and conversions live on a prototype rather than being installed on
315
+ * every event.
316
+ *
317
+ * Everything is deferred: a mapping that reads two parameters shouldn't pay to
318
+ * convert the block, the transaction and the positional parameter list — and
319
+ * envio runs each handler twice over the same payload, so anything eager is
320
+ * paid twice.
321
+ */
322
+ function makeEventClass(
323
+ dataSourceName: string,
324
+ eventName: string,
325
+ declared: Map<string, string>,
326
+ hasReceipt: boolean,
327
+ ) {
328
+ const paramConverters = new Map<string, Converter>();
329
+
330
+ class SubgraphEvent {
331
+ _raw: any;
332
+ _address: any = undefined;
333
+ _logIndex: any = undefined;
334
+ _block: any = undefined;
335
+ _transaction: any = undefined;
336
+ _params: any = undefined;
337
+ _parameters: any = undefined;
338
+ _receipt: any = undefined;
339
+
340
+ constructor(raw: any) {
341
+ this._raw = raw;
342
+ }
343
+
344
+ get address() {
345
+ return (this._address ??= Address.fromString(this._raw.srcAddress));
346
+ }
347
+ get logIndex() {
348
+ return (this._logIndex ??= GraphBigInt.fromI32(this._raw.logIndex));
349
+ }
350
+ get block() {
351
+ return (this._block ??= shaped(BLOCK_SHAPE, this._raw.block));
352
+ }
353
+ get transaction() {
354
+ return (this._transaction ??= shaped(TRANSACTION_SHAPE, this._raw.transaction));
355
+ }
356
+ get receipt() {
357
+ if (!hasReceipt) return null;
358
+ return (this._receipt ??= Object.defineProperty(
359
+ shaped(RECEIPT_SHAPE, this._raw.transaction),
360
+ "logs",
361
+ {
362
+ get: () => {
363
+ throw unsupported(
364
+ "event.receipt.logs",
365
+ `data source "${dataSourceName}" -> "${eventName}"`,
366
+ );
367
+ },
368
+ },
369
+ ));
370
+ }
371
+ /** Read by name, the way a hand-written mapping does. */
372
+ get params() {
373
+ return (this._params ??= convertAll(paramConverters, this._raw.params ?? {}, declared));
374
+ }
375
+ /**
376
+ * Read positionally, the way `graph codegen`'s param classes do. Built off
377
+ * the converted params so an array or a bytes value carries the type the
378
+ * ABI declares rather than one guessed from its JS shape.
379
+ */
380
+ get parameters() {
381
+ return (this._parameters ??= Object.entries(this.params).map(([name, value]) => ({
382
+ name,
383
+ value: toEthereumValue(value),
384
+ })));
385
+ }
386
+ get transactionLogIndex(): never {
387
+ throw unsupported(
388
+ "event.transactionLogIndex",
389
+ `data source "${dataSourceName}" → "${eventName}"`,
390
+ );
391
+ }
392
+ }
393
+
394
+ return SubgraphEvent;
395
+ }
396
+
397
+ function toEthereumValue(value: unknown): any {
398
+ const V = (ethereum as any).Value;
399
+ if (value === null || value === undefined) return V.fromNull();
400
+ if (Array.isArray(value)) return V.fromArray(value.map(toEthereumValue));
401
+ if (value instanceof Address || value instanceof Bytes) return V.fromBytes(value);
402
+ if (value instanceof GraphBigInt) return V.fromBigInt(value);
403
+ if (typeof value === "bigint") return V.fromBigInt(new (GraphBigInt as any)(value));
404
+ if (typeof value === "boolean") return V.fromBoolean(value);
405
+ if (typeof value === "number") return V.fromI32(value);
406
+ if (typeof value === "string" && ANY_HEX.test(value)) {
407
+ return V.fromBytes(Bytes.fromHexString(value));
408
+ }
409
+ return V.fromString(String(value));
410
+ }
411
+
412
+ async function loadMapping(
413
+ root: string,
414
+ mappingFile: string,
415
+ scope: Scope,
416
+ ): Promise<Record<string, any>> {
417
+ const url = pathToFileURL(path.resolve(root, mappingFile)).href;
418
+ try {
419
+ // graph-node instantiates the module once per data source, with a host
420
+ // context already in place, so a mapping may read `dataSource.network()`
421
+ // at module top level — Balancer's constants do. The import runs inside a
422
+ // scope carrying that data source for the same reason.
423
+ return await runInScope(scope, () => import(url));
424
+ } catch (exn) {
425
+ const message = exn instanceof Error ? exn.message : String(exn);
426
+ // Only reachable when codegen couldn't run up front, so this reports why
427
+ // rather than retrying: Node caches a failed resolution for the process.
428
+ if (/Cannot find (module|package)/.test(message) && message.includes("generated")) {
429
+ ensureGeneratedCode(root, { required: true });
430
+ }
431
+ // An unknown named import fails at Node's ESM link step, before any Proxy
432
+ // in the shim can see it — rewrap it with the mapping that caused it.
433
+ throw new Error(`Envio Subgraph failed to load the mapping ${mappingFile}.\n ${message}`);
434
+ }
435
+ }
436
+
437
+ /**
438
+ * `generated/` is usually gitignored, so it's built with the project's own
439
+ * graph-cli — which makes the output identical to the user's normal workflow
440
+ * by definition.
441
+ */
442
+ function ensureGeneratedCode(root: string, { required }: { required: boolean }) {
443
+ if (existsSync(path.join(root, "generated"))) return;
444
+
445
+ const graphCli = path.join(root, "node_modules", ".bin", "graph");
446
+ if (!existsSync(graphCli)) {
447
+ if (!required) return;
448
+ throw new Error(
449
+ 'Envio Subgraph needs the project\'s generated code, but "generated/" is\n' +
450
+ "missing and @graphprotocol/graph-cli isn't installed.\n" +
451
+ "Install dependencies and try again:\n" +
452
+ " pnpm install\n" +
453
+ "Or generate manually:\n" +
454
+ " pnpm exec graph codegen",
455
+ );
456
+ }
457
+
458
+ try {
459
+ execFileSync(graphCli, ["codegen"], { cwd: root, stdio: "inherit" });
460
+ } catch {
461
+ throw new Error(
462
+ 'Envio Subgraph ran `graph codegen` to build "generated/", but it failed —\n' +
463
+ "the error above comes from The Graph's own codegen, so fix it there and\n" +
464
+ "rerun. If `graph codegen` succeeds on its own but fails through envio,\n" +
465
+ "please open an issue: https://github.com/enviodev/hyperindex/issues",
466
+ );
467
+ }
468
+ }
469
+
470
+ /**
471
+ * `graph build` compiles the mappings with `asc` against the real
472
+ * `@graphprotocol/graph-ts`. That is the type check a subgraph project already
473
+ * has, and running it in `envio dev` keeps the feedback loop the developer
474
+ * knows — a type error reads the same here as it does on Graph Node.
475
+ *
476
+ * Only in dev, and only when something it reads has changed: it is an
477
+ * AssemblyScript compile, not something to pay on every restart.
478
+ */
479
+ function typeCheckMappings(root: string) {
480
+ const graphCli = path.join(root, "node_modules", ".bin", "graph");
481
+ if (!existsSync(graphCli)) return;
482
+
483
+ const inputs = ["subgraph.yaml", "schema.graphql", "src", "abis"]
484
+ .map((entry) => path.join(root, entry))
485
+ .filter((entry) => existsSync(entry))
486
+ .map((entry) => fingerprint(entry))
487
+ .join("|");
488
+
489
+ const stamp = path.join(root, ".envio", "graph-build.stamp");
490
+ if (existsSync(stamp) && readFileSync(stamp, "utf8") === inputs) return;
491
+
492
+ try {
493
+ execFileSync(graphCli, ["build"], { cwd: root, stdio: "inherit" });
494
+ } catch {
495
+ throw new Error(
496
+ "Envio Subgraph ran `graph build` to type-check the mappings, and it\n" +
497
+ "failed — the error above comes from The Graph's own AssemblyScript\n" +
498
+ "compiler, so fix it there and rerun.",
499
+ );
500
+ }
501
+
502
+ mkdirSync(path.dirname(stamp), { recursive: true });
503
+ writeFileSync(stamp, inputs);
504
+ }
505
+
506
+ function fingerprint(entry: string): string {
507
+ const stats = statSync(entry);
508
+ if (!stats.isDirectory()) {
509
+ return `${entry}:${stats.mtimeMs}:${stats.size}`;
510
+ }
511
+ return readdirSync(entry)
512
+ .sort()
513
+ .map((child) => fingerprint(path.join(entry, child)))
514
+ .join(",");
515
+ }
516
+
517
+ type Effect = {
518
+ name: string;
519
+ handler: (args: { input: unknown; context: unknown; cacheKey: string; checkpointId: bigint }) => Promise<unknown>;
520
+ };
521
+
522
+ const REGISTER_SUSPEND = Symbol("envio.subgraph.register.suspend");
523
+
524
+ /**
525
+ * Results the register pass has already fetched, across events and blocks.
526
+ *
527
+ * A host op's input carries the block it is evaluated at, so an entry can never
528
+ * go stale — and a factory calling `symbol()` on the same token for every pair
529
+ * it creates asks the same question hundreds of times. envio's own effect cache
530
+ * is out of reach here: it hangs off the processing context, and this runs at
531
+ * fetch time.
532
+ */
533
+ const registerCache = new Map<string, { value?: unknown; error?: unknown }>();
534
+ const REGISTER_CACHE_LIMIT = 20_000;
535
+
536
+ async function runRegisterHost(effect: Effect, input: unknown): Promise<unknown> {
537
+ return effect.handler({
538
+ input,
539
+ // Only `blockTimestamp` reads the context, and only for the chain id, which
540
+ // the input already carries for every other host op.
541
+ context: { chain: { id: 0 } },
542
+ cacheKey: "",
543
+ checkpointId: 0n,
544
+ });
545
+ }
546
+
547
+ /**
548
+ * The register pass's half of the sync bridge (§5).
549
+ *
550
+ * `contractRegister` runs at fetch time with a context that can only register
551
+ * addresses — no in-memory store, so no effects. But a factory mapping routinely
552
+ * reads a contract *before* deciding what to create, so the answer has to arrive
553
+ * somehow: the op is started, the mapping is suspended, and the round is
554
+ * replayed once it lands. Same shape as the handler pass, with the results
555
+ * memoised here instead of in envio's effect tables.
556
+ */
557
+ function registerHostSync(scope: Scope, effect: Effect, input: unknown) {
558
+ const key = `${effect.name}${typeof input === "string" ? input : JSON.stringify(input)}`;
559
+ const resolved = scope.resolved ?? new Map();
560
+ scope.resolved = resolved;
561
+
562
+ const hit = resolved.get(key) ?? registerCache.get(key);
563
+ if (hit) {
564
+ if ("error" in hit) throw hit.error;
565
+ return hit.value;
566
+ }
567
+
568
+ const awaiting = scope.awaiting ?? [];
569
+ scope.awaiting = awaiting;
570
+ awaiting.push(
571
+ runRegisterHost(effect, input).then(
572
+ (value) => {
573
+ resolved.set(key, { value });
574
+ if (registerCache.size >= REGISTER_CACHE_LIMIT) registerCache.clear();
575
+ registerCache.set(key, { value });
576
+ },
577
+ // A failure is remembered for this event only: a transport error that
578
+ // fails the batch must not poison every later block too.
579
+ (error) => resolved.set(key, { error }),
580
+ ),
581
+ );
582
+ throw REGISTER_SUSPEND;
583
+ }
584
+
585
+ const maxRegisterRounds = 100;
586
+
587
+ async function runRegisterRounds(scope: Scope, fn: () => void): Promise<void> {
588
+ for (let round = 1; round <= maxRegisterRounds; round++) {
589
+ scope.awaiting = undefined;
590
+ let suspended = false;
591
+ try {
592
+ runInScope(scope, fn);
593
+ } catch (error) {
594
+ if (error !== REGISTER_SUSPEND) {
595
+ // This pass exists to collect `dataSource.create()` calls, and it runs
596
+ // against a context that reads null for everything — so a handler that
597
+ // assumes its entities exist throws here even though it is perfectly
598
+ // correct. Nothing was registered, so there is nothing this pass could
599
+ // have been for; the same mapping runs again with a real store, and any
600
+ // error that is genuinely the mapping's surfaces there.
601
+ if (scope.registered.size === 0) return;
602
+ throw error;
603
+ }
604
+ suspended = true;
605
+ }
606
+ const awaiting = scope.awaiting;
607
+ if (!suspended) return;
608
+ if (!awaiting || awaiting.length === 0) {
609
+ throw new Error(
610
+ "Envio Subgraph suspended the register pass with nothing to wait for. " +
611
+ "Please open an issue: https://github.com/enviodev/hyperindex/issues",
612
+ );
613
+ }
614
+ await Promise.all(awaiting);
615
+ }
616
+ throw new Error(
617
+ `Envio Subgraph replayed a mapping's dataSource.create() pass ${maxRegisterRounds} times ` +
618
+ "without it settling. This usually means the mapping isn't deterministic across reruns.",
619
+ );
620
+ }
621
+
622
+ export async function registerSubgraph(config: SubgraphConfig): Promise<void> {
623
+ installResolveHook(config.root);
624
+ resetClients();
625
+
626
+ // One effect for every contract call: envio already batches and dedupes
627
+ // effect calls in preload, and the block number in the input is what keeps
628
+ // a cached result tied to the state the mapping saw.
629
+ const rpcUrls = config.rpcUrls ?? [];
630
+ const callEffect = makeCallEffect(rpcUrls);
631
+ const hosts = makeHostEffects(rpcUrls);
632
+ const callSync = (effect: unknown, input: unknown, what: string) => {
633
+ const scope = currentScope();
634
+ void what;
635
+ if (scope.mode === "register") {
636
+ return registerHostSync(scope, effect as Effect, input);
637
+ }
638
+ return scope.context.effectSync(effect, input);
639
+ };
640
+
641
+ installHosts({
642
+ ipfsCat: (hash) => callSync(hosts.ipfsCat, hash, "ipfs.cat()"),
643
+ ipfsMap: (hash, callback, userData, flags) => {
644
+ const scope = currentScope();
645
+ const fn = scope.mappingExports[callback];
646
+ if (typeof fn !== "function") {
647
+ throw new Error(
648
+ `ipfs.map() names the callback "${callback}", which the mapping doesn't export.`,
649
+ );
650
+ }
651
+ const encoded: string | null = callSync(hosts.ipfsCat, hash, "ipfs.map()");
652
+ if (encoded === null) return;
653
+ const body = Buffer.from(encoded, "base64").toString("utf8");
654
+ for (const line of body.split("\n")) {
655
+ if (line.trim() === "") continue;
656
+ fn(jsonFromString(line), userData);
657
+ }
658
+ void flags;
659
+ },
660
+ arweaveData: (txId) => callSync(hosts.arweaveData, txId, "arweave.transactionData()"),
661
+ ensName: (hash) => callSync(hosts.ensName, hash, "ens.nameByHash()"),
662
+ getBalance: (address) =>
663
+ callSync(
664
+ hosts.getBalance,
665
+ JSON.stringify({ address, blockNumber: currentScope().blockNumber }),
666
+ "ethereum.getBalance()",
667
+ ),
668
+ hasCode: (address) =>
669
+ callSync(
670
+ hosts.hasCode,
671
+ JSON.stringify({ address, blockNumber: currentScope().blockNumber }),
672
+ "ethereum.hasCode()",
673
+ ),
674
+ blockTimestamp: (blockNumber) =>
675
+ callSync(hosts.blockTimestamp, blockNumber, "block.timestamp"),
676
+ });
677
+
678
+ // Reads the scope rather than closing over one context: register passes for
679
+ // the items in a batch run concurrently, and this hook is process-wide.
680
+ installRegisterHook((templateName, address) => {
681
+ currentScope().context.chain[templateName].add(address);
682
+ });
683
+
684
+ installCallHook((call) => {
685
+ const scope = currentScope();
686
+ void scope;
687
+ const encoded = JSON.stringify({
688
+ chainId: currentScope().dataSource.chainId,
689
+ address: call.contractAddress.toHexString(),
690
+ signature: call.functionSignature,
691
+ args: call.functionParams.map((param: any) => encodeArg(valueToJs(param))),
692
+ blockNumber: scope.blockNumber,
693
+ });
694
+ const raw = callSync(callEffect, encoded, `the contract call ${call.functionSignature}`);
695
+ if (raw === null) {
696
+ return { reverted: true, value: null };
697
+ }
698
+ const output = JSON.parse(raw);
699
+ return {
700
+ reverted: output.reverted,
701
+ value: output.values === null ? null : output.values.map(decodeArg),
702
+ };
703
+ });
704
+
705
+ const schema: SubgraphSchema = {
706
+ timestampFields: config.timestampFields ?? {},
707
+ bytesIdEntities: config.bytesIdEntities ?? [],
708
+ entityListFields: config.entityListFields ?? {},
709
+ entityFields: config.entityFields ?? {},
710
+ entityRefFields: config.entityRefFields ?? {},
711
+ entityFieldTypes: config.entityFieldTypes ?? {},
712
+ };
713
+
714
+ // Before any mapping is imported: Node caches a failed module resolution for
715
+ // the life of the process, so generating after the import has already failed
716
+ // wouldn't help.
717
+ ensureGeneratedCode(config.root, { required: false });
718
+
719
+ if (config.isDev) {
720
+ typeCheckMappings(config.root);
721
+ }
722
+
723
+ const sources = [...config.dataSources, ...config.templates];
724
+ const templateNames = new Set(config.templates.map((template) => template.name));
725
+
726
+ for (const source of sources) {
727
+ if (source.kind !== "contract") continue;
728
+ const mapping = await loadMapping(config.root, source.mappingFile, {
729
+ context: null,
730
+ event: null,
731
+ mode: "handler",
732
+ schema,
733
+ dataSource: {
734
+ name: source.name,
735
+ address: source.address ?? "",
736
+ chainId: source.chainId ?? 0,
737
+ network: source.network ?? "",
738
+ context: source.context ?? {},
739
+ },
740
+ registered: new Set(),
741
+ blockNumber: source.startBlock ?? 0,
742
+ mappingExports: {},
743
+ });
744
+
745
+ for (const handler of source.eventHandlers) {
746
+ const fn = mapping[handler.handler];
747
+ // `graph build` doesn't check that a named handler is exported — Aave's
748
+ // mainnet manifest names one its mappings renamed years ago — so a
749
+ // subgraph that deploys today would be refused here for a stale line the
750
+ // event it names never reaches.
751
+ if (typeof fn !== "function") continue;
752
+
753
+ // Parameter shapes are fixed per event kind, not per data source.
754
+ const SubgraphEvent = makeEventClass(
755
+ source.name,
756
+ handler.name,
757
+ new Map(Object.entries(handler.params ?? {})),
758
+ handler.receipt ?? false,
759
+ );
760
+
761
+ const makeScope = (event: any, context: any, mode: Scope["mode"]): Scope => ({
762
+ context,
763
+ event,
764
+ mode,
765
+ schema,
766
+ dataSource: {
767
+ name: source.name,
768
+ address: event.srcAddress,
769
+ chainId: event.chainId,
770
+ network: source.network ?? "",
771
+ context: source.context ?? {},
772
+ },
773
+ registered: new Set(),
774
+ blockNumber: event.block.number,
775
+ mappingExports: mapping,
776
+ });
777
+
778
+ indexer.onEvent(
779
+ { contract: contractName(source.name), event: handler.name },
780
+ async ({ event, context }: any) => {
781
+ const graphEvent = new SubgraphEvent(event);
782
+ await (context as any).runSync(() =>
783
+ runInScope(makeScope(event, context, "handler"), () => fn(graphEvent)),
784
+ );
785
+ },
786
+ );
787
+
788
+ // `dataSource.create` has to reach envio's contractRegister, which runs
789
+ // at fetch time — before any entity exists. The same mapping reruns in
790
+ // register mode, where writes and logs are no-ops and reads are null.
791
+ if (templateNames.size > 0) {
792
+ indexer.contractRegister(
793
+ { contract: contractName(source.name), event: handler.name },
794
+ async ({ event, context }: any) => {
795
+ const graphEvent = new SubgraphEvent(event);
796
+ await runRegisterRounds(makeScope(event, context, "register"), () => fn(graphEvent));
797
+ },
798
+ );
799
+ }
800
+ }
801
+
802
+ for (const handler of source.blockHandlers) {
803
+ const fn = mapping[handler.handler];
804
+ if (typeof fn !== "function") continue;
805
+ const interval = blockInterval(handler);
806
+ indexer.onBlock(
807
+ {
808
+ chain: source.chainId,
809
+ name: `${source.name}_${handler.handler}`,
810
+ interval: interval.once ? undefined : interval.every,
811
+ ...(interval.once
812
+ ? { block: { _gte: source.startBlock ?? 0, _lte: source.startBlock ?? 0 } }
813
+ : {}),
814
+ } as any,
815
+ async ({ block, context }: any) => {
816
+ const graphBlock = makeBlockHandlerBlock(
817
+ block.number,
818
+ `data source "${source.name}" → "${handler.handler}"`,
819
+ );
820
+ await (context as any).runSync(() =>
821
+ runInScope(
822
+ {
823
+ context,
824
+ event: block,
825
+ mode: "handler",
826
+ schema,
827
+ dataSource: {
828
+ name: source.name,
829
+ address: source.address ?? "",
830
+ chainId: source.chainId ?? 0,
831
+ network: source.network ?? "",
832
+ context: source.context ?? {},
833
+ },
834
+ registered: new Set(),
835
+ blockNumber: block.number,
836
+ mappingExports: mapping,
837
+ },
838
+ () => fn(graphBlock),
839
+ ),
840
+ );
841
+ },
842
+ );
843
+ }
844
+ }
845
+ }