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