envio 3.14.0-subgraph → 3.14.1

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 (63) hide show
  1. package/evm.schema.json +1 -1
  2. package/package.json +6 -6
  3. package/src/Config.res +0 -9
  4. package/src/Config.res.mjs +2 -6
  5. package/src/Core.res +0 -21
  6. package/src/Core.res.mjs +0 -17
  7. package/src/EventProcessing.res +6 -6
  8. package/src/EventProcessing.res.mjs +6 -8
  9. package/src/HandlerLoader.res +22 -48
  10. package/src/HandlerLoader.res.mjs +15 -41
  11. package/src/HandlerTypeCheck.res +207 -0
  12. package/src/HandlerTypeCheck.res.mjs +262 -0
  13. package/src/LoadLayer.res +7 -39
  14. package/src/LoadLayer.res.mjs +38 -65
  15. package/src/LoadLayer.resi +0 -28
  16. package/src/PgStorage.res +5 -24
  17. package/src/PgStorage.res.mjs +1 -18
  18. package/src/UserContext.res +62 -377
  19. package/src/UserContext.res.mjs +39 -297
  20. package/src/bindings/NodeJs.res +31 -0
  21. package/src/bindings/NodeJs.res.mjs +3 -0
  22. package/src/db/Table.res +0 -10
  23. package/src/db/Table.res.mjs +11 -22
  24. package/src/sources/EvmRpcClient.res +2 -0
  25. package/src/sources/RpcSource.res +17 -4
  26. package/src/sources/RpcSource.res.mjs +23 -5
  27. package/src/sources/SourceManager.res +63 -40
  28. package/src/sources/SourceManager.res.mjs +32 -20
  29. package/src/HandlerTypeCheck.mjs +0 -14
  30. package/src/HandlerTypeCheckWorker.mjs +0 -68
  31. package/src/SyncWrites.res +0 -86
  32. package/src/SyncWrites.res.mjs +0 -85
  33. package/src/subgraph/abi-types.ts +0 -63
  34. package/src/subgraph/assemblyscript.ts +0 -85
  35. package/src/subgraph/blocks.ts +0 -183
  36. package/src/subgraph/calls.ts +0 -149
  37. package/src/subgraph/conformance.ts +0 -83
  38. package/src/subgraph/errors.ts +0 -96
  39. package/src/subgraph/graph-cli-worker.mjs +0 -26
  40. package/src/subgraph/graph-cli.ts +0 -190
  41. package/src/subgraph/graph-ts-enums.ts +0 -28
  42. package/src/subgraph/graph-ts-types/VERSION +0 -2
  43. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +0 -70
  44. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +0 -327
  45. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +0 -233
  46. package/src/subgraph/graph-ts-types/chain/near.d.ts +0 -253
  47. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +0 -32
  48. package/src/subgraph/graph-ts-types/common/collections.d.ts +0 -136
  49. package/src/subgraph/graph-ts-types/common/conversion.d.ts +0 -11
  50. package/src/subgraph/graph-ts-types/common/datasource.d.ts +0 -30
  51. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  52. package/src/subgraph/graph-ts-types/common/json.d.ts +0 -17
  53. package/src/subgraph/graph-ts-types/common/numbers.d.ts +0 -120
  54. package/src/subgraph/graph-ts-types/common/value.d.ts +0 -120
  55. package/src/subgraph/graph-ts-types/common/yaml.d.ts +0 -90
  56. package/src/subgraph/graph-ts-types/global/global.d.ts +0 -194
  57. package/src/subgraph/graph-ts-types/helper-functions.d.ts +0 -22
  58. package/src/subgraph/graph-ts-types/index.d.ts +0 -102
  59. package/src/subgraph/graph-ts.ts +0 -2285
  60. package/src/subgraph/hosts.ts +0 -158
  61. package/src/subgraph/rpc.ts +0 -65
  62. package/src/subgraph/runtime.ts +0 -595
  63. package/src/subgraph/scope.ts +0 -68
@@ -1,595 +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 { existsSync, readFileSync } from "node:fs";
11
- import { registerHooks } from "node:module";
12
- import { fileURLToPath, pathToFileURL } from "node:url";
13
- import path from "node:path";
14
- import { indexer } from "../Api.res.mjs";
15
- import { subgraphFileToJavascript } from "../Core.res.mjs";
16
- import { currentScope, runInScope, type Scope, type SubgraphSchema } from "./scope.ts";
17
- import {
18
- Address,
19
- assemblyScriptPrimitives,
20
- BigInt as GraphBigInt,
21
- Bytes,
22
- changetype,
23
- retagChangetype,
24
- installCallHook,
25
- installHosts,
26
- installRegisterHook,
27
- ethereum,
28
- type EventInput,
29
- type EventKind,
30
- json as jsonNamespace,
31
- makeBlockHandlerBlock,
32
- ethereumValueToJs,
33
- } from "./graph-ts.ts";
34
- import { encodeArg, decodeArg, makeCallEffect } from "./calls.ts";
35
- import { resetRpcClients } from "./rpc.ts";
36
- import { EVENT_CLASSES_EXPORT, OPERATORS_HELPER, RETAG_HELPER, operators } from "./assemblyscript.ts";
37
- import { makeHostEffects } from "./hosts.ts";
38
- import { ensureGeneratedCode, generatedDir, missingGeneratedCode, typeCheckMappings } from "./graph-cli.ts";
39
- import { unknown, unsupported } from "./errors.ts";
40
-
41
- const SHIM_URL = new URL("./graph-ts.ts", import.meta.url).href;
42
-
43
- const jsonFromString = (line: string) => (jsonNamespace as any).fromString(line);
44
-
45
- /**
46
- * Skips the preload pass, so every read a mapping makes misses and the handler
47
- * replays as often as it possibly can. The replay-stress suite runs the whole
48
- * subgraph suite this way: a mapping must produce the same store whether its
49
- * reads were preloaded or not.
50
- */
51
- const skipPreload = process.env.ENVIO_SUBGRAPH_SKIP_PRELOAD === "1";
52
-
53
- type EventHandler = {
54
- event: string;
55
- name: string;
56
- handler: string;
57
- receipt: boolean;
58
- inputs?: EventInput[];
59
- /** `topic1`–`topic3`, keyed by position among the indexed parameters. */
60
- topics?: Record<string, string[]>;
61
- };
62
- /** As the translator serialises its `BlockFilter`. */
63
- type BlockHandler = { handler: string; filter: { every: number } | "once" };
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
- /** The config's contract name for each data source and template. */
84
- contractAccessors: Record<string, string>;
85
- root: string;
86
- rpcUrls: string[];
87
- isDev: boolean;
88
- } & SubgraphSchema;
89
-
90
- let hooksInstalled = false;
91
- let projectRoot: string | null = null;
92
-
93
- /**
94
- * Mappings resolve `@graphprotocol/graph-ts` to the shim. The project's own
95
- * files — its mappings and `generated/` — are AssemblyScript, and load through
96
- * the addon, which turns each into the JavaScript that computes what `asc`
97
- * would (`subgraph/assemblyscript.rs`).
98
- */
99
- function installResolveHook(root: string) {
100
- projectRoot = pathToFileURL(path.resolve(root) + path.sep).href;
101
- if (hooksInstalled) return;
102
- hooksInstalled = true;
103
- // The addon's output carries an inline source map back to the mapping, so a
104
- // stack trace names the line the developer wrote.
105
- process.setSourceMapsEnabled(true);
106
-
107
- registerHooks({
108
- resolve(specifier: string, context: any, nextResolve: any) {
109
- if (
110
- specifier === "@graphprotocol/graph-ts" ||
111
- specifier.startsWith("@graphprotocol/graph-ts/")
112
- ) {
113
- return { url: SHIM_URL, shortCircuit: true };
114
- }
115
- const resolved = nextResolve(specifier, context);
116
- // A subgraph project's package.json has no `type`, so its mappings and
117
- // its `generated/` would load as CommonJS and reach the shim — a real ES
118
- // module — through `require()`, which Node refuses inside a cycle. They
119
- // are ES modules; saying so is what lets them import it.
120
- if (projectRoot && resolved?.url?.startsWith(projectRoot)) {
121
- return { ...resolved, format: "module" };
122
- }
123
- return resolved;
124
- },
125
- // Taken over before any other loader sees the file: a TypeScript
126
- // transpiler would refuse AssemblyScript it has no reason to accept, and
127
- // would strip `changetype`'s type argument before it could be read.
128
- load(url: string, context: any, nextLoad: any) {
129
- if (
130
- !projectRoot ||
131
- !url.startsWith(projectRoot) ||
132
- url.includes("/node_modules/") ||
133
- !url.endsWith(".ts")
134
- ) {
135
- return nextLoad(url, context);
136
- }
137
- const file = fileURLToPath(url);
138
- return {
139
- format: "module",
140
- source: subgraphFileToJavascript(readFileSync(file, "utf8"), file),
141
- shortCircuit: true,
142
- };
143
- },
144
- });
145
-
146
- // AssemblyScript builtins the generated code uses as globals.
147
- const globals = globalThis as any;
148
- globals.changetype ??= changetype;
149
- globals[OPERATORS_HELPER] ??= operators;
150
- globals[RETAG_HELPER] ??= retagChangetype;
151
- globals.assert ??= (value: unknown, message?: string) => {
152
- if (!value) throw new Error(message ?? "assertion failed");
153
- return value;
154
- };
155
- for (const [name, namespace] of Object.entries(assemblyScriptPrimitives)) {
156
- globals[name] ??= namespace;
157
- }
158
- }
159
-
160
- /**
161
- * `topicN` narrows the Nth indexed parameter to the logs whose topic is one of
162
- * the listed words; as a `where`, it narrows to that parameter's decoded
163
- * values. The translator has refused a parameter hashed into its topic.
164
- */
165
- function topicFilter(handler: EventHandler): { params: Record<string, unknown[]> } | undefined {
166
- const topics = Object.entries(handler.topics ?? {});
167
- if (topics.length === 0) return undefined;
168
- const indexed = (handler.inputs ?? []).filter((input) => input.indexed);
169
- const params: Record<string, unknown[]> = {};
170
- for (const [position, words] of topics) {
171
- const input = indexed[Number(position) - 1];
172
- params[input.key] = words.map((word) => fromTopic(input.type, word));
173
- }
174
- return { params };
175
- }
176
-
177
- function fromTopic(type: string, topic: string): unknown {
178
- const word = topic.toLowerCase().replace(/^0x/, "").padStart(64, "0");
179
- if (type === "address") return `0x${word.slice(24)}`;
180
- if (type === "bool") return BigInt(`0x${word}`) !== 0n;
181
- // A fixed-size byte string is left-aligned in its word.
182
- if (type.startsWith("bytes")) return `0x${word.slice(0, Number(type.slice(5)) * 2)}`;
183
- const bits = Number(type.replace(/^u?int/, "") || 256);
184
- const value = BigInt(`0x${word}`);
185
- return type.startsWith("uint") ? BigInt.asUintN(bits, value) : BigInt.asIntN(bits, value);
186
- }
187
-
188
- async function loadMapping(
189
- root: string,
190
- generated: string,
191
- mappingFile: string,
192
- scope: Scope,
193
- ): Promise<Record<string, any>> {
194
- const url = pathToFileURL(path.resolve(root, mappingFile)).href;
195
- try {
196
- // graph-node instantiates the module once per data source, with a host
197
- // context already in place, so a mapping may read `dataSource.network()`
198
- // at module top level — Balancer's constants do. The import runs inside a
199
- // scope carrying that data source for the same reason.
200
- return await runInScope(scope, () => import(url));
201
- } catch (exn) {
202
- const message = exn instanceof Error ? exn.message : String(exn);
203
- // Only reachable when codegen couldn't run up front, so this reports why
204
- // rather than retrying: Node caches a failed resolution for the process.
205
- if (message.includes(generated) && !existsSync(generated)) {
206
- throw missingGeneratedCode(root, generated);
207
- }
208
- // An unknown named import fails at Node's ESM link step, before any Proxy
209
- // in the shim can see it — rewrap it with the mapping that caused it.
210
- throw new Error(`Envio Subgraph failed to load the mapping ${mappingFile}.\n ${message}`);
211
- }
212
- }
213
-
214
- type Effect = {
215
- name: string;
216
- handler: (args: { input: unknown; context: unknown; cacheKey: string; checkpointId: bigint }) => Promise<unknown>;
217
- };
218
-
219
- const REGISTER_SUSPEND = Symbol("envio.subgraph.register.suspend");
220
-
221
- /**
222
- * Results the register pass has already fetched, across events and blocks.
223
- *
224
- * A host op's input carries the block it is evaluated at, so an entry can never
225
- * go stale — and a factory calling `symbol()` on the same token for every pair
226
- * it creates asks the same question hundreds of times. envio's own effect cache
227
- * is out of reach here: it hangs off the processing context, and this runs at
228
- * fetch time.
229
- */
230
- const registerCache = new Map<string, { value?: unknown; error?: unknown }>();
231
- const REGISTER_CACHE_LIMIT = 20_000;
232
-
233
- async function runRegisterHost(effect: Effect, input: unknown): Promise<unknown> {
234
- return effect.handler({
235
- input,
236
- // Only `blockTimestamp` reads the context, and only for the chain id, which
237
- // the input already carries for every other host op.
238
- context: { chain: { id: 0 } },
239
- cacheKey: "",
240
- checkpointId: 0n,
241
- });
242
- }
243
-
244
- /**
245
- * The register pass's half of the sync bridge (§5).
246
- *
247
- * `contractRegister` runs at fetch time with a context that can only register
248
- * addresses — no in-memory store, so no effects. But a factory mapping routinely
249
- * reads a contract *before* deciding what to create, so the answer has to arrive
250
- * somehow: the op is started, the mapping is suspended, and the round is
251
- * replayed once it lands. Same shape as the handler pass, with the results
252
- * memoised here instead of in envio's effect tables.
253
- */
254
- /**
255
- * A host op that failed, not a mapping that threw. The two are told apart
256
- * because the register pass forgives a mapping error — see `runRegisterRounds`
257
- * — and forgiving this one would silently drop the `dataSource.create()` calls
258
- * the pass exists to collect.
259
- */
260
- class RegisterHostFailure extends Error {
261
- constructor(op: string, readonly reason: unknown) {
262
- super(`Envio Subgraph could not resolve ${op} while registering contracts: ${String(reason)}`);
263
- this.cause = reason;
264
- }
265
- }
266
-
267
- function registerHostSync(scope: Scope, effect: Effect, input: unknown) {
268
- const key = `${effect.name}\u0000${typeof input === "string" ? input : JSON.stringify(input)}`;
269
- const resolved = scope.resolved ?? new Map();
270
- scope.resolved = resolved;
271
-
272
- const hit = resolved.get(key) ?? registerCache.get(key);
273
- if (hit) {
274
- if ("error" in hit) throw new RegisterHostFailure(effect.name, hit.error);
275
- return hit.value;
276
- }
277
-
278
- const awaiting = scope.awaiting ?? [];
279
- scope.awaiting = awaiting;
280
- awaiting.push(
281
- runRegisterHost(effect, input).then(
282
- (value) => {
283
- resolved.set(key, { value });
284
- if (registerCache.size >= REGISTER_CACHE_LIMIT) registerCache.clear();
285
- registerCache.set(key, { value });
286
- },
287
- // A failure is remembered for this event only: a transport error that
288
- // fails the batch must not poison every later block too.
289
- (error) => resolved.set(key, { error }),
290
- ),
291
- );
292
- throw REGISTER_SUSPEND;
293
- }
294
-
295
- const maxRegisterRounds = 100;
296
-
297
- async function runRegisterRounds(scope: Scope, fn: () => void): Promise<void> {
298
- for (let round = 1; round <= maxRegisterRounds; round++) {
299
- scope.awaiting = undefined;
300
- let suspended = false;
301
- try {
302
- runInScope(scope, fn);
303
- } catch (error) {
304
- if (error !== REGISTER_SUSPEND) {
305
- // A host op that failed is never forgiven: the mapping was about to
306
- // decide what to create from its answer, and the handler pass cannot
307
- // repair a missed registration — addresses are registered at fetch
308
- // time. Fail the batch and let the retry get a real answer.
309
- if (error instanceof RegisterHostFailure) throw error;
310
- // This pass exists to collect `dataSource.create()` calls, and it runs
311
- // against a context that reads null for everything — so a handler that
312
- // assumes its entities exist throws here even though it is perfectly
313
- // correct. Nothing was registered, so there is nothing this pass could
314
- // have been for; the same mapping runs again with a real store, and any
315
- // error that is genuinely the mapping's surfaces there.
316
- if (scope.registered.size === 0) return;
317
- throw error;
318
- }
319
- suspended = true;
320
- }
321
- const awaiting = scope.awaiting;
322
- if (!suspended) return;
323
- if (!awaiting || awaiting.length === 0) {
324
- throw new Error(
325
- "Envio Subgraph suspended the register pass with nothing to wait for. " +
326
- "Please open an issue: https://github.com/enviodev/hyperindex/issues",
327
- );
328
- }
329
- await Promise.all(awaiting);
330
- }
331
- throw new Error(
332
- `Envio Subgraph replayed a mapping's dataSource.create() pass ${maxRegisterRounds} times ` +
333
- "without it settling. This usually means the mapping isn't deterministic across reruns.",
334
- );
335
- }
336
-
337
- export async function registerSubgraph(config: SubgraphConfig): Promise<void> {
338
- installResolveHook(config.root);
339
- resetRpcClients();
340
-
341
- // One effect for every contract call: envio already batches and dedupes
342
- // effect calls in preload, and the block number in the input is what keeps
343
- // a cached result tied to the state the mapping saw.
344
- const rpcUrls = config.rpcUrls ?? [];
345
- const callEffect = makeCallEffect(rpcUrls);
346
- const hosts = makeHostEffects(rpcUrls);
347
- const callSync = (effect: unknown, input: unknown, what: string) => {
348
- const scope = currentScope();
349
- void what;
350
- if (scope.mode === "register") {
351
- return registerHostSync(scope, effect as Effect, input);
352
- }
353
- return scope.context.effectSync(effect, input);
354
- };
355
-
356
- installHosts({
357
- ipfsCat: (hash) => callSync(hosts.ipfsCat, hash, "ipfs.cat()"),
358
- ipfsMap: (hash, callback, userData, flags) => {
359
- const scope = currentScope();
360
- const fn = scope.mappingExports[callback];
361
- if (typeof fn !== "function") {
362
- throw new Error(
363
- `ipfs.map() names the callback "${callback}", which the mapping doesn't export.`,
364
- );
365
- }
366
- const encoded: string | null = callSync(hosts.ipfsCat, hash, "ipfs.map()");
367
- if (encoded === null) return;
368
- const body = Buffer.from(encoded, "base64").toString("utf8");
369
- for (const line of body.split("\n")) {
370
- if (line.trim() === "") continue;
371
- fn(jsonFromString(line), userData);
372
- }
373
- void flags;
374
- },
375
- arweaveData: (txId) => callSync(hosts.arweaveData, txId, "arweave.transactionData()"),
376
- ensName: (hash) => callSync(hosts.ensName, hash, "ens.nameByHash()"),
377
- getBalance: (address) =>
378
- callSync(
379
- hosts.getBalance,
380
- JSON.stringify({ address, blockNumber: currentScope().blockNumber }),
381
- "ethereum.getBalance()",
382
- ),
383
- hasCode: (address) =>
384
- callSync(
385
- hosts.hasCode,
386
- JSON.stringify({ address, blockNumber: currentScope().blockNumber }),
387
- "ethereum.hasCode()",
388
- ),
389
- blockTimestamp: (blockNumber) =>
390
- callSync(hosts.blockTimestamp, blockNumber, "block.timestamp"),
391
- });
392
-
393
- // Reads the scope rather than closing over one context: register passes for
394
- // the items in a batch run concurrently, and this hook is process-wide.
395
- installRegisterHook((templateName, address) => {
396
- const accessor = config.contractAccessors[templateName];
397
- if (accessor === undefined) throw unknown(`the template ${templateName}`, "a mapping handler");
398
- currentScope().context.chain[accessor].add(address);
399
- });
400
-
401
- installCallHook((call) => {
402
- const scope = currentScope();
403
- void scope;
404
- const encoded = JSON.stringify({
405
- chainId: currentScope().dataSource.chainId,
406
- address: call.contractAddress.toHexString(),
407
- signature: call.functionSignature,
408
- args: call.functionParams.map((param: any) => encodeArg(ethereumValueToJs(param))),
409
- blockNumber: scope.blockNumber,
410
- });
411
- const raw = callSync(callEffect, encoded, `the contract call ${call.functionSignature}`);
412
- if (raw === null) {
413
- return { reverted: true, value: null };
414
- }
415
- const output = JSON.parse(raw);
416
- return {
417
- reverted: output.reverted,
418
- value: output.values === null ? null : output.values.map(decodeArg),
419
- };
420
- });
421
-
422
- const schema: SubgraphSchema = {
423
- timestampFields: config.timestampFields ?? {},
424
- bytesIdEntities: config.bytesIdEntities ?? [],
425
- entityListFields: config.entityListFields ?? {},
426
- entityFields: config.entityFields ?? {},
427
- entityRefFields: config.entityRefFields ?? {},
428
- entityFieldTypes: config.entityFieldTypes ?? {},
429
- entityAccessors: config.entityAccessors,
430
- };
431
-
432
- // Before any mapping is imported: Node caches a failed module resolution for
433
- // the life of the process, so generating after the import has already failed
434
- // wouldn't help.
435
- const sources = [...config.dataSources, ...config.templates];
436
- const generated = generatedDir(
437
- config.root,
438
- sources.map((source) => source.mappingFile),
439
- );
440
- await ensureGeneratedCode(config.root, generated);
441
-
442
- if (config.isDev) {
443
- await typeCheckMappings(config.root);
444
- }
445
-
446
- const templateNames = new Set(config.templates.map((template) => template.name));
447
-
448
- // A template sharing a data source's name is folded into its envio contract,
449
- // so both sets of handlers fire for every address of it. graph-node runs the
450
- // data source's for its own address and the template's for the ones it
451
- // created, so each is gated to its side.
452
- const staticAddresses = new Map<string, Set<string>>();
453
- for (const source of config.dataSources) {
454
- if (!source.address || !templateNames.has(source.name)) continue;
455
- const addresses = staticAddresses.get(source.name) ?? new Set();
456
- addresses.add(`${source.chainId}:${source.address.toLowerCase()}`);
457
- staticAddresses.set(source.name, addresses);
458
- }
459
- const ownerOf = (source: DataSource) => {
460
- const addresses = staticAddresses.get(source.name);
461
- if (!addresses) return () => true;
462
- return (event: any) =>
463
- addresses.has(`${event.chainId}:${String(event.srcAddress).toLowerCase()}`) !== source.isTemplate;
464
- };
465
-
466
- for (const source of sources) {
467
- if (source.kind !== "contract") continue;
468
- const mapping = await loadMapping(config.root, generated, source.mappingFile, {
469
- context: null,
470
- event: null,
471
- mode: "handler",
472
- schema,
473
- dataSource: {
474
- name: source.name,
475
- address: source.address ?? "",
476
- chainId: source.chainId ?? 0,
477
- network: source.network ?? "",
478
- context: source.context ?? {},
479
- },
480
- registered: new Set(),
481
- blockNumber: source.startBlock ?? 0,
482
- mappingExports: {},
483
- });
484
-
485
- const owns = ownerOf(source);
486
-
487
- for (const handler of source.eventHandlers) {
488
- const fn = mapping[handler.handler];
489
- // `graph build` doesn't check that a named handler is exported — Aave's
490
- // mainnet manifest names one its mappings renamed years ago — so a
491
- // subgraph that deploys today would be refused here for a stale line the
492
- // event it names never reaches.
493
- if (typeof fn !== "function") continue;
494
-
495
- const kind: EventKind = {
496
- location: `data source "${source.name}" → "${handler.name}"`,
497
- inputs: handler.inputs ?? [],
498
- hasReceipt: handler.receipt ?? false,
499
- };
500
- // Built as the generated class the handler declares, when it declares
501
- // one: its `params` getters are graph codegen's own.
502
- const EventClass = mapping[EVENT_CLASSES_EXPORT]?.[handler.handler] ?? ethereum.Event;
503
- const makeEvent = (event: unknown) => Reflect.construct(ethereum.Event, [event, kind], EventClass);
504
-
505
- const makeScope = (event: any, context: any, mode: Scope["mode"]): Scope => ({
506
- context,
507
- event,
508
- mode,
509
- schema,
510
- dataSource: {
511
- name: source.name,
512
- address: event.srcAddress,
513
- chainId: event.chainId,
514
- network: source.network ?? "",
515
- context: source.context ?? {},
516
- },
517
- registered: new Set(),
518
- blockNumber: event.block.number,
519
- mappingExports: mapping,
520
- });
521
-
522
- const where = topicFilter(handler);
523
-
524
- indexer.onEvent(
525
- { contract: config.contractAccessors[source.name], event: handler.name, where },
526
- async ({ event, context }: any) => {
527
- if ((skipPreload && context.isPreload) || !owns(event)) return;
528
- const graphEvent = makeEvent(event);
529
- await (context as any).runSync(() =>
530
- runInScope(makeScope(event, context, "handler"), () => fn(graphEvent)),
531
- );
532
- },
533
- );
534
-
535
- // `dataSource.create` has to reach envio's contractRegister, which runs
536
- // at fetch time — before any entity exists. The same mapping reruns in
537
- // register mode, where writes and logs are no-ops and reads are null.
538
- if (templateNames.size > 0) {
539
- indexer.contractRegister(
540
- { contract: config.contractAccessors[source.name], event: handler.name, where },
541
- async ({ event, context }: any) => {
542
- if (!owns(event)) return;
543
- const graphEvent = makeEvent(event);
544
- await runRegisterRounds(makeScope(event, context, "register"), () => fn(graphEvent));
545
- },
546
- );
547
- }
548
- }
549
-
550
- for (const handler of source.blockHandlers) {
551
- const fn = mapping[handler.handler];
552
- if (typeof fn !== "function") continue;
553
- const start = source.startBlock ?? 0;
554
- const blocks =
555
- handler.filter === "once"
556
- ? { _gte: start, _lte: start }
557
- : { _gte: start, _every: handler.filter.every };
558
- indexer.onBlock(
559
- {
560
- name: `${source.name}_${handler.handler}`,
561
- where: ({ chain }: { chain: { id: number } }) =>
562
- chain.id === source.chainId && { block: { number: blocks } },
563
- },
564
- async ({ block, context }: any) => {
565
- if (skipPreload && context.isPreload) return;
566
- const graphBlock = makeBlockHandlerBlock(
567
- block.number,
568
- `data source "${source.name}" → "${handler.handler}"`,
569
- );
570
- await (context as any).runSync(() =>
571
- runInScope(
572
- {
573
- context,
574
- event: block,
575
- mode: "handler",
576
- schema,
577
- dataSource: {
578
- name: source.name,
579
- address: source.address ?? "",
580
- chainId: source.chainId ?? 0,
581
- network: source.network ?? "",
582
- context: source.context ?? {},
583
- },
584
- registered: new Set(),
585
- blockNumber: block.number,
586
- mappingExports: mapping,
587
- },
588
- () => fn(graphBlock),
589
- ),
590
- );
591
- },
592
- );
593
- }
594
- }
595
- }
@@ -1,68 +0,0 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
-
3
- /** What a wrapper is doing when it runs the mapping. */
4
- export type Mode = "handler" | "register";
5
-
6
- export type SubgraphSchema = {
7
- timestampFields: Record<string, string[]>;
8
- bytesIdEntities: string[];
9
- entityListFields: Record<string, string[]>;
10
- entityFields: Record<string, string[]>;
11
- entityRefFields: Record<string, string[]>;
12
- /** Each field as the subgraph schema declares it, which is what a mapping reads. */
13
- entityFieldTypes: Record<
14
- string,
15
- Record<string, { kind: string; target?: string; derivedFrom?: string; list: boolean }>
16
- >;
17
- /** The context accessor envio generated for each entity, by schema name. */
18
- entityAccessors: Record<string, string>;
19
- };
20
-
21
- export type Scope = {
22
- /** envio's handler or contractRegister context. */
23
- context: any;
24
- event: any;
25
- mode: Mode;
26
- schema: SubgraphSchema;
27
- dataSource: {
28
- name: string;
29
- address: string;
30
- chainId: number;
31
- network: string;
32
- /** The manifest's `context` entries, as `dataSource.context()` serves them. */
33
- context: Record<string, { type: string; data: string }>;
34
- };
35
- /** Addresses already registered this round, so replays stay idempotent. */
36
- registered: Set<string>;
37
- /**
38
- * Host results the register pass has resolved, keyed by effect and input.
39
- * The register pass has no envio context to run effects through, so it
40
- * replays the mapping itself and answers from here (§5, register mode).
41
- */
42
- resolved?: Map<string, { value?: unknown; error?: unknown }>;
43
- /** Lookups the current register round is waiting on. */
44
- awaiting?: Promise<unknown>[];
45
- /** The block a contract call is evaluated against, as graph-node does. */
46
- blockNumber: number;
47
- /** The mapping module, so `ipfs.map` can reach the callback it names. */
48
- mappingExports: Record<string, any>;
49
- };
50
-
51
- const storage = new AsyncLocalStorage<Scope>();
52
-
53
- export function runInScope<T>(scope: Scope, fn: () => T): T {
54
- return storage.run(scope, fn);
55
- }
56
-
57
- export function currentScope(): Scope {
58
- const scope = storage.getStore();
59
- if (!scope) {
60
- throw new Error(
61
- "A graph-ts API was called outside of a mapping handler. Envio Subgraph " +
62
- "runs mappings inside a scope that carries the event and the store; " +
63
- "calling into graph-ts from module top-level or from a detached " +
64
- "callback escapes it.",
65
- );
66
- }
67
- return scope;
68
- }