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.
- package/evm.schema.json +1 -1
- package/package.json +6 -6
- package/src/Config.res +0 -9
- package/src/Config.res.mjs +2 -6
- package/src/Core.res +0 -21
- package/src/Core.res.mjs +0 -17
- package/src/EventProcessing.res +6 -6
- package/src/EventProcessing.res.mjs +6 -8
- package/src/HandlerLoader.res +22 -48
- package/src/HandlerLoader.res.mjs +15 -41
- package/src/HandlerTypeCheck.res +207 -0
- package/src/HandlerTypeCheck.res.mjs +262 -0
- package/src/LoadLayer.res +7 -39
- package/src/LoadLayer.res.mjs +38 -65
- package/src/LoadLayer.resi +0 -28
- package/src/PgStorage.res +5 -24
- package/src/PgStorage.res.mjs +1 -18
- package/src/UserContext.res +62 -377
- package/src/UserContext.res.mjs +39 -297
- package/src/bindings/NodeJs.res +31 -0
- package/src/bindings/NodeJs.res.mjs +3 -0
- package/src/db/Table.res +0 -10
- package/src/db/Table.res.mjs +11 -22
- package/src/sources/EvmRpcClient.res +2 -0
- package/src/sources/RpcSource.res +17 -4
- package/src/sources/RpcSource.res.mjs +23 -5
- package/src/sources/SourceManager.res +63 -40
- package/src/sources/SourceManager.res.mjs +32 -20
- package/src/HandlerTypeCheck.mjs +0 -14
- package/src/HandlerTypeCheckWorker.mjs +0 -68
- package/src/SyncWrites.res +0 -86
- package/src/SyncWrites.res.mjs +0 -85
- package/src/subgraph/abi-types.ts +0 -63
- package/src/subgraph/assemblyscript.ts +0 -85
- package/src/subgraph/blocks.ts +0 -183
- package/src/subgraph/calls.ts +0 -149
- package/src/subgraph/conformance.ts +0 -83
- package/src/subgraph/errors.ts +0 -96
- package/src/subgraph/graph-cli-worker.mjs +0 -26
- package/src/subgraph/graph-cli.ts +0 -190
- package/src/subgraph/graph-ts-enums.ts +0 -28
- package/src/subgraph/graph-ts-types/VERSION +0 -2
- package/src/subgraph/graph-ts-types/chain/arweave.d.ts +0 -70
- package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +0 -327
- package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +0 -233
- package/src/subgraph/graph-ts-types/chain/near.d.ts +0 -253
- package/src/subgraph/graph-ts-types/chain/starknet.d.ts +0 -32
- package/src/subgraph/graph-ts-types/common/collections.d.ts +0 -136
- package/src/subgraph/graph-ts-types/common/conversion.d.ts +0 -11
- package/src/subgraph/graph-ts-types/common/datasource.d.ts +0 -30
- package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
- package/src/subgraph/graph-ts-types/common/json.d.ts +0 -17
- package/src/subgraph/graph-ts-types/common/numbers.d.ts +0 -120
- package/src/subgraph/graph-ts-types/common/value.d.ts +0 -120
- package/src/subgraph/graph-ts-types/common/yaml.d.ts +0 -90
- package/src/subgraph/graph-ts-types/global/global.d.ts +0 -194
- package/src/subgraph/graph-ts-types/helper-functions.d.ts +0 -22
- package/src/subgraph/graph-ts-types/index.d.ts +0 -102
- package/src/subgraph/graph-ts.ts +0 -2285
- package/src/subgraph/hosts.ts +0 -158
- package/src/subgraph/rpc.ts +0 -65
- package/src/subgraph/runtime.ts +0 -595
- package/src/subgraph/scope.ts +0 -68
package/src/subgraph/runtime.ts
DELETED
|
@@ -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
|
-
}
|
package/src/subgraph/scope.ts
DELETED
|
@@ -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
|
-
}
|