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.
Files changed (186) hide show
  1. package/README.md +40 -34
  2. package/evm.schema.json +11 -2
  3. package/fuel.schema.json +10 -1
  4. package/index.d.ts +6 -0
  5. package/licenses/README.md +1 -1
  6. package/package.json +10 -9
  7. package/src/Batch.res.mjs +5 -5
  8. package/src/BatchProcessing.res +1 -0
  9. package/src/BatchProcessing.res.mjs +1 -0
  10. package/src/Bin.res +7 -0
  11. package/src/Bin.res.mjs +8 -0
  12. package/src/ChainFetching.res +450 -16
  13. package/src/ChainFetching.res.mjs +403 -32
  14. package/src/ChainState.res +124 -7
  15. package/src/ChainState.res.mjs +128 -67
  16. package/src/ChainState.resi +26 -0
  17. package/src/Clock.res +57 -0
  18. package/src/Clock.res.mjs +72 -0
  19. package/src/Config.res +31 -3
  20. package/src/Config.res.mjs +15 -4
  21. package/src/ContractRegisterContext.res +3 -6
  22. package/src/ContractRegisterContext.res.mjs +4 -2
  23. package/src/Core.res +95 -36
  24. package/src/Core.res.mjs +90 -1
  25. package/src/CrossChainState.res +12 -2
  26. package/src/CrossChainState.res.mjs +3 -3
  27. package/src/Ecosystem.res +7 -4
  28. package/src/Ecosystem.res.mjs +8 -8
  29. package/src/EffectCacheLookup.res +98 -0
  30. package/src/EffectCacheLookup.res.mjs +116 -0
  31. package/src/EffectState.res +10 -2
  32. package/src/EffectState.res.mjs +8 -4
  33. package/src/EffectState.resi +3 -1
  34. package/src/EntityId.res +2 -7
  35. package/src/EntityId.res.mjs +1 -8
  36. package/src/EntityTables.res +4 -1
  37. package/src/EntityTables.res.mjs +1 -1
  38. package/src/Env.res +20 -0
  39. package/src/Env.res.mjs +16 -0
  40. package/src/EventProcessing.res +38 -1
  41. package/src/EventProcessing.res.mjs +28 -1
  42. package/src/FetchState.res +408 -73
  43. package/src/FetchState.res.mjs +361 -91
  44. package/src/FinalizeBackfill.res +2 -4
  45. package/src/FinalizeBackfill.res.mjs +3 -4
  46. package/src/HandlerLoader.res +39 -15
  47. package/src/HandlerLoader.res.mjs +27 -8
  48. package/src/HandlerRegister.res +4 -0
  49. package/src/HandlerRegister.res.mjs +5 -2
  50. package/src/HandlerRegister.resi +2 -0
  51. package/src/InMemoryTable.res +17 -16
  52. package/src/InMemoryTable.res.mjs +14 -15
  53. package/src/IndexerState.res +28 -4
  54. package/src/IndexerState.res.mjs +19 -6
  55. package/src/IndexerState.resi +4 -0
  56. package/src/Internal.res +61 -49
  57. package/src/Internal.res.mjs +36 -80
  58. package/src/LoadLayer.res +54 -15
  59. package/src/LoadLayer.res.mjs +75 -45
  60. package/src/LoadLayer.resi +26 -0
  61. package/src/Main.res +14 -1
  62. package/src/Main.res.mjs +4 -3
  63. package/src/Persistence.res +3 -23
  64. package/src/Persistence.res.mjs +3 -3
  65. package/src/PreloadLookahead.res +202 -0
  66. package/src/PreloadLookahead.res.mjs +218 -0
  67. package/src/Rollback.res +0 -1
  68. package/src/Rollback.res.mjs +1 -1
  69. package/src/RunScope.res +3 -0
  70. package/src/SafeCheckpointTracking.res +4 -4
  71. package/src/SafeCheckpointTracking.res.mjs +1 -1
  72. package/src/SimulateItems.res +42 -26
  73. package/src/SimulateItems.res.mjs +40 -16
  74. package/src/StagedRows.res +106 -0
  75. package/src/StagedRows.res.mjs +102 -0
  76. package/src/Staging.res +32 -14
  77. package/src/Staging.res.mjs +17 -20
  78. package/src/Storage.res +303 -948
  79. package/src/Storage.res.mjs +229 -708
  80. package/src/SubgraphLog.res +19 -0
  81. package/src/SubgraphLog.res.mjs +26 -0
  82. package/src/Supervisor.res +75 -56
  83. package/src/Supervisor.res.mjs +55 -26
  84. package/src/SyncWrites.res +107 -0
  85. package/src/SyncWrites.res.mjs +98 -0
  86. package/src/TestIndexer.res +22 -27
  87. package/src/TestIndexer.res.mjs +20 -17
  88. package/src/Throttler.res +15 -8
  89. package/src/Throttler.res.mjs +9 -6
  90. package/src/Throttler.resi +1 -1
  91. package/src/UserContext.res +419 -46
  92. package/src/UserContext.res.mjs +318 -30
  93. package/src/Utils.res +17 -6
  94. package/src/Utils.res.mjs +12 -6
  95. package/src/Writing.res +0 -1
  96. package/src/Writing.res.mjs +1 -1
  97. package/src/bindings/ClickHouse.res +0 -322
  98. package/src/bindings/ClickHouse.res.mjs +0 -338
  99. package/src/bindings/StorageClient.res +108 -174
  100. package/src/bindings/StorageClient.res.mjs +19 -8
  101. package/src/bindings/Vitest.res +0 -26
  102. package/src/bindings/Vitest.res.mjs +0 -3
  103. package/src/db/Table.res +24 -150
  104. package/src/db/Table.res.mjs +38 -272
  105. package/src/sources/AddressSet.res +2 -0
  106. package/src/sources/AddressStore.res +3 -0
  107. package/src/sources/CallSource.res +274 -0
  108. package/src/sources/CallSource.res.mjs +196 -0
  109. package/src/sources/ChainSources.res +2 -0
  110. package/src/sources/ChainSources.res.mjs +2 -2
  111. package/src/sources/EvmChain.res +10 -1
  112. package/src/sources/EvmChain.res.mjs +6 -3
  113. package/src/sources/EvmHyperSyncSource.res +2 -2
  114. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  115. package/src/sources/EvmRpcClient.res +13 -3
  116. package/src/sources/EvmRpcClient.res.mjs +4 -3
  117. package/src/sources/EvmRpcWs.res +4 -3
  118. package/src/sources/EvmRpcWs.res.mjs +2 -2
  119. package/src/sources/HeightFeed.res +31 -10
  120. package/src/sources/HeightFeed.res.mjs +10 -6
  121. package/src/sources/HeightStream.res +32 -17
  122. package/src/sources/HeightStream.res.mjs +10 -10
  123. package/src/sources/HyperSync.res +2 -1
  124. package/src/sources/HyperSync.res.mjs +2 -1
  125. package/src/sources/HyperSyncSSE.res +4 -6
  126. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  127. package/src/sources/RpcSource.res +10 -1
  128. package/src/sources/RpcSource.res.mjs +4 -3
  129. package/src/sources/Source.res +3 -0
  130. package/src/sources/SourceManager.res +84 -77
  131. package/src/sources/SourceManager.res.mjs +17 -20
  132. package/src/sources/SourceManager.resi +3 -4
  133. package/src/sources/StartBlockResolver.res +1 -0
  134. package/src/sources/StartBlockResolver.res.mjs +3 -2
  135. package/src/sources/SvmHyperSyncSource.res +2 -2
  136. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  137. package/src/subgraph/abi-types.ts +84 -0
  138. package/src/subgraph/aggregations.ts +745 -0
  139. package/src/subgraph/as-package.ts +143 -0
  140. package/src/subgraph/assemblyscript-regex.ts +67 -0
  141. package/src/subgraph/assemblyscript.ts +731 -0
  142. package/src/subgraph/blocks.ts +398 -0
  143. package/src/subgraph/calls-source.ts +702 -0
  144. package/src/subgraph/calls.ts +258 -0
  145. package/src/subgraph/conformance.ts +106 -0
  146. package/src/subgraph/errors.ts +85 -0
  147. package/src/subgraph/graft.ts +73 -0
  148. package/src/subgraph/graph-cli-worker.mjs +35 -0
  149. package/src/subgraph/graph-cli.ts +900 -0
  150. package/src/subgraph/graph-node-decimal.ts +199 -0
  151. package/src/subgraph/graph-ts-enums.ts +28 -0
  152. package/src/subgraph/graph-ts-types/VERSION +2 -0
  153. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  154. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  155. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  156. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  157. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  158. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  159. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  160. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  161. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  162. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  163. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  164. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  165. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  166. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  167. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  168. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  169. package/src/subgraph/graph-ts.ts +4339 -0
  170. package/src/subgraph/hosts.ts +554 -0
  171. package/src/subgraph/log.ts +28 -0
  172. package/src/subgraph/receipts.ts +184 -0
  173. package/src/subgraph/register-slots.ts +93 -0
  174. package/src/subgraph/rollup.ts +109 -0
  175. package/src/subgraph/rpc.ts +337 -0
  176. package/src/subgraph/runtime.ts +2059 -0
  177. package/src/subgraph/scope.ts +176 -0
  178. package/src/subgraph/stats.ts +42 -0
  179. package/src/subgraph/template-instances.ts +66 -0
  180. package/src/subgraph/toolchain.ts +327 -0
  181. package/src/subgraph/transient.ts +176 -0
  182. package/svm.schema.json +11 -2
  183. package/src/PgWriting.res +0 -36
  184. package/src/PgWriting.res.mjs +0 -34
  185. package/src/db/IndexDefinition.res +0 -47
  186. package/src/db/IndexDefinition.res.mjs +0 -67
@@ -0,0 +1,702 @@
1
+ /**
2
+ * Reads the calls a subgraph's call handlers and `filter: call` block handlers
3
+ * run on, the way graph-node does (`chain/ethereum`): every successful call
4
+ * (CALL, STATICCALL, DELEGATECALL, CALLCODE — top level and internal) to a data
5
+ * source's address whose input is at least a selector long, from a transaction
6
+ * that succeeded.
7
+ *
8
+ * Two backends serve the same page: HyperSync's trace endpoint (Ethereum,
9
+ * Gnosis, Base, Arbitrum, Monad) and a node's `trace_filter` or
10
+ * `debug_traceBlockByNumber`. `CallSource.res` wraps a chain's log source and
11
+ * asks `fetchCalls` for the same block range; the calls come back as the event
12
+ * items they travel as through the rest of envio.
13
+ */
14
+
15
+ import { rpcClient } from "./rpc.ts";
16
+ import { HttpStatusError, untilAnswered } from "./transient.ts";
17
+ import { warn } from "./log.ts";
18
+
19
+ /** The chains HyperSync serves traces for, at `<chainId>-traces.hypersync.xyz`. */
20
+ export const HYPERSYNC_TRACE_CHAINS = new Set([1, 100, 143, 8453, 42161]);
21
+
22
+ export type CallRegistration = {
23
+ /** The chain's registration index, echoed back on each item. */
24
+ index: number;
25
+ contractName: string;
26
+ eventName: string;
27
+ /** The function's 4-byte selector as `0x…`; `null` for a block handler, which wants every call. */
28
+ selector: string | null;
29
+ /** 1 for a call handler, 2 for a `filter: call` block handler. */
30
+ rank: 1 | 2;
31
+ isWildcard: boolean;
32
+ startBlock: number | null;
33
+ };
34
+
35
+ export type CallRequest = {
36
+ chainId: number;
37
+ /** Inclusive. */
38
+ fromBlock: number;
39
+ /** Inclusive. */
40
+ toBlock: number;
41
+ registrations: CallRegistration[];
42
+ /** Lowercase addresses the partition holds, by contract name. */
43
+ addresses: Record<string, string[]>;
44
+ rpcUrls: string[];
45
+ };
46
+
47
+ export type CallItem = {
48
+ registrationIndex: number;
49
+ blockNumber: number;
50
+ transactionIndex: number;
51
+ /** The call's position in its transaction's trace tree. */
52
+ traceAddress: number[];
53
+ payload: Record<string, unknown>;
54
+ };
55
+
56
+ export type CallResponse = {
57
+ items: CallItem[];
58
+ /** The last block the answer covers: every call up to it is in `items`. */
59
+ latestFetchedBlockNumber: number;
60
+ /** The hash and time of each block a call was in, for reorg detection. */
61
+ blocks: { blockNumber: number; blockHash: string; blockTimestamp: number }[];
62
+ knownHeight: number | null;
63
+ };
64
+
65
+ /** One call as a backend reports it, before any handler's filter. */
66
+ export type RawCall = {
67
+ blockNumber: number;
68
+ transactionIndex: number;
69
+ traceAddress: number[];
70
+ callType: string;
71
+ from: string;
72
+ to: string;
73
+ input: string;
74
+ output: string;
75
+ value: bigint;
76
+ gasUsed: bigint;
77
+ error: boolean;
78
+ };
79
+
80
+ type TxInfo = Record<string, unknown> & { status: number | null };
81
+ type BlockInfo = Record<string, unknown> & { number: number; hash: string; timestamp: number };
82
+
83
+ type TracePage = {
84
+ calls: RawCall[];
85
+ blocks: Map<number, BlockInfo>;
86
+ transactions: Map<string, TxInfo>;
87
+ latestFetchedBlockNumber: number;
88
+ knownHeight: number | null;
89
+ };
90
+
91
+ /** What a backend can narrow its search by; the exact match is made here. */
92
+ type Selection = { addresses: string[]; selectors: string[] | null };
93
+
94
+ type Backend = {
95
+ name: string;
96
+ page(request: CallRequest, selections: Selection[]): Promise<TracePage>;
97
+ };
98
+
99
+ export const txKey = (blockNumber: number, transactionIndex: number) => `${blockNumber}:${transactionIndex}`;
100
+
101
+ const hexToBigInt = (value: unknown): bigint =>
102
+ value === null || value === undefined ? 0n : BigInt(value as string | number | bigint);
103
+
104
+ const lower = (value: unknown): string => String(value ?? "").toLowerCase();
105
+
106
+ /** HyperSync packs a trace address as a little-endian u64 length, then that many u64s. */
107
+ export function parseHyperSyncTraceAddress(packed: string): number[] {
108
+ const bytes = Buffer.from(packed.replace(/^0x/, ""), "hex");
109
+ const length = Number(bytes.readBigUInt64LE(0));
110
+ const address: number[] = [];
111
+ for (let i = 0; i < length; i++) address.push(Number(bytes.readBigUInt64LE(8 + 8 * i)));
112
+ return address;
113
+ }
114
+
115
+ /** Pre-order of a trace tree is lexicographic order of the addresses, a prefix first. */
116
+ export function compareTraceAddress(a: number[], b: number[]): number {
117
+ const shared = Math.min(a.length, b.length);
118
+ for (let i = 0; i < shared; i++) if (a[i] !== b[i]) return a[i] - b[i];
119
+ return a.length - b.length;
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // HyperSync
124
+ // ---------------------------------------------------------------------------
125
+
126
+ const hyperSyncUrl = (chainId: number) =>
127
+ (process.env.ENVIO_SUBGRAPH_TRACES_URL ?? "https://{chainId}-traces.hypersync.xyz").replace(
128
+ "{chainId}",
129
+ String(chainId),
130
+ );
131
+
132
+ const ADDRESSES_PER_SELECTION = 2_000;
133
+
134
+ function hyperSyncBackend(): Backend {
135
+ return {
136
+ name: "HyperSync traces",
137
+ async page(request, selections) {
138
+ const token = process.env.ENVIO_API_TOKEN;
139
+ const traces: Record<string, unknown>[] = [];
140
+ for (const selection of selections) {
141
+ for (let i = 0; i < selection.addresses.length; i += ADDRESSES_PER_SELECTION) {
142
+ traces.push({
143
+ to: selection.addresses.slice(i, i + ADDRESSES_PER_SELECTION),
144
+ type: ["call"],
145
+ ...(selection.selectors ? { sighash: selection.selectors } : {}),
146
+ });
147
+ }
148
+ }
149
+ const body = JSON.stringify({
150
+ from_block: request.fromBlock,
151
+ to_block: request.toBlock + 1,
152
+ // Joins the matched traces' transactions, which the default does not.
153
+ join_mode: "JoinAll",
154
+ traces,
155
+ field_selection: {
156
+ trace: [
157
+ "block_number",
158
+ "transaction_position",
159
+ "trace_address",
160
+ "type",
161
+ "call_type",
162
+ "from",
163
+ "to",
164
+ "input",
165
+ "output",
166
+ "error",
167
+ "gas_used",
168
+ "value",
169
+ ],
170
+ transaction: [
171
+ "block_number",
172
+ "transaction_index",
173
+ "hash",
174
+ "from",
175
+ "to",
176
+ "value",
177
+ "gas",
178
+ "gas_price",
179
+ "input",
180
+ "nonce",
181
+ "status",
182
+ ],
183
+ block: [
184
+ "number",
185
+ "hash",
186
+ "parent_hash",
187
+ "sha3_uncles",
188
+ "miner",
189
+ "state_root",
190
+ "transactions_root",
191
+ "receipts_root",
192
+ "gas_used",
193
+ "gas_limit",
194
+ "timestamp",
195
+ "difficulty",
196
+ "total_difficulty",
197
+ "size",
198
+ "base_fee_per_gas",
199
+ ],
200
+ },
201
+ });
202
+ const url = `${hyperSyncUrl(request.chainId)}/query`;
203
+ const response: any = await untilAnswered("The HyperSync traces request", async (signal) => {
204
+ const res = await fetch(url, {
205
+ method: "POST",
206
+ headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
207
+ body,
208
+ signal,
209
+ });
210
+ if (!res.ok) {
211
+ const detail = (await res.text()).slice(0, 300);
212
+ throw new HttpStatusError(url, res.status, detail || res.statusText);
213
+ }
214
+ return res.json();
215
+ });
216
+
217
+ const calls: RawCall[] = [];
218
+ const blocks = new Map<number, BlockInfo>();
219
+ const transactions = new Map<string, TxInfo>();
220
+ for (const page of response.data ?? []) {
221
+ for (const trace of page.traces ?? []) {
222
+ calls.push({
223
+ blockNumber: trace.block_number,
224
+ transactionIndex: trace.transaction_position,
225
+ traceAddress: parseHyperSyncTraceAddress(trace.trace_address),
226
+ callType: trace.call_type ?? "call",
227
+ from: lower(trace.from),
228
+ to: lower(trace.to),
229
+ input: lower(trace.input),
230
+ output: lower(trace.output ?? "0x"),
231
+ value: hexToBigInt(trace.value),
232
+ gasUsed: hexToBigInt(trace.gas_used),
233
+ error: trace.error !== null && trace.error !== undefined,
234
+ });
235
+ }
236
+ for (const block of page.blocks ?? []) {
237
+ blocks.set(block.number, {
238
+ number: block.number,
239
+ hash: block.hash,
240
+ parentHash: block.parent_hash,
241
+ sha3Uncles: block.sha3_uncles,
242
+ miner: lower(block.miner),
243
+ stateRoot: block.state_root,
244
+ transactionsRoot: block.transactions_root,
245
+ receiptsRoot: block.receipts_root,
246
+ gasUsed: hexToBigInt(block.gas_used),
247
+ gasLimit: hexToBigInt(block.gas_limit),
248
+ timestamp: Number(hexToBigInt(block.timestamp)),
249
+ difficulty: block.difficulty === undefined ? null : hexToBigInt(block.difficulty),
250
+ totalDifficulty: block.total_difficulty === undefined ? null : hexToBigInt(block.total_difficulty),
251
+ size: block.size === undefined ? null : hexToBigInt(block.size),
252
+ baseFeePerGas: block.base_fee_per_gas === undefined ? null : hexToBigInt(block.base_fee_per_gas),
253
+ });
254
+ }
255
+ for (const tx of page.transactions ?? []) {
256
+ transactions.set(txKey(tx.block_number, tx.transaction_index), {
257
+ hash: tx.hash,
258
+ transactionIndex: tx.transaction_index,
259
+ from: lower(tx.from),
260
+ to: tx.to ? lower(tx.to) : null,
261
+ value: hexToBigInt(tx.value),
262
+ gas: hexToBigInt(tx.gas),
263
+ gasPrice: tx.gas_price === undefined ? null : hexToBigInt(tx.gas_price),
264
+ input: tx.input,
265
+ nonce: hexToBigInt(tx.nonce),
266
+ status: tx.status ?? null,
267
+ });
268
+ }
269
+ }
270
+ return {
271
+ calls,
272
+ blocks,
273
+ transactions,
274
+ // `next_block` is the first block the answer does not cover.
275
+ latestFetchedBlockNumber: Math.min(request.toBlock, Number(response.next_block) - 1),
276
+ knownHeight: response.archive_height ?? null,
277
+ };
278
+ },
279
+ };
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // RPC
284
+ // ---------------------------------------------------------------------------
285
+
286
+ const toHex = (n: number) => `0x${n.toString(16)}`;
287
+
288
+ function positive(name: string, fallback: number): number {
289
+ const value = Number(process.env[name]);
290
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
291
+ }
292
+
293
+ type RpcRequest = (method: string, params: unknown[], options?: { maxTries?: number }) => Promise<any>;
294
+
295
+ function makeRpcRequest(rpcUrls: string[]): RpcRequest {
296
+ if (rpcUrls.length === 0) {
297
+ throw new Error(
298
+ "Envio Subgraph needs an RPC endpoint to read this chain's calls: HyperSync serves traces " +
299
+ "for Ethereum, Gnosis, Base, Arbitrum and Monad only. Set ENVIO_SUBGRAPH_RPC to an archive " +
300
+ "node that serves trace_filter or debug_traceBlockByNumber.",
301
+ );
302
+ }
303
+ const client = rpcClient(rpcUrls);
304
+ return (method, params, options) =>
305
+ untilAnswered(`The ${method} request`, () => client.request({ method, params }), {
306
+ deadline: false,
307
+ ...options,
308
+ });
309
+ }
310
+
311
+ /**
312
+ * The block, and the transactions at `wanted` positions, an RPC call trigger
313
+ * carries. Some endpoints answer `eth_getBlockByNumber(…, true)` with no
314
+ * transactions at all (BNB Chain's, through dRPC), so the block comes as hashes
315
+ * and each transaction a handler needs is read by its own hash.
316
+ */
317
+ async function blockAndTransactions(request: RpcRequest, blockNumber: number, wanted: Set<number>) {
318
+ const raw = await request("eth_getBlockByNumber", [toHex(blockNumber), false]);
319
+ if (!raw) throw new Error(`The RPC node has no block ${blockNumber}.`);
320
+ const block: BlockInfo = {
321
+ number: blockNumber,
322
+ hash: raw.hash,
323
+ parentHash: raw.parentHash,
324
+ sha3Uncles: raw.sha3Uncles,
325
+ miner: lower(raw.miner),
326
+ stateRoot: raw.stateRoot,
327
+ transactionsRoot: raw.transactionsRoot,
328
+ receiptsRoot: raw.receiptsRoot,
329
+ gasUsed: hexToBigInt(raw.gasUsed),
330
+ gasLimit: hexToBigInt(raw.gasLimit),
331
+ timestamp: Number(hexToBigInt(raw.timestamp)),
332
+ difficulty: raw.difficulty === undefined ? null : hexToBigInt(raw.difficulty),
333
+ totalDifficulty: raw.totalDifficulty === undefined ? null : hexToBigInt(raw.totalDifficulty),
334
+ size: raw.size === undefined ? null : hexToBigInt(raw.size),
335
+ baseFeePerGas: raw.baseFeePerGas === undefined ? null : hexToBigInt(raw.baseFeePerGas),
336
+ };
337
+ const transactions = new Map<string, TxInfo>();
338
+ await Promise.all(
339
+ [...wanted].map(async (transactionIndex) => {
340
+ const entry = raw.transactions[transactionIndex];
341
+ const tx = typeof entry === "string" ? await request("eth_getTransactionByHash", [entry]) : entry;
342
+ if (!tx) throw new Error(`The RPC node has no transaction ${transactionIndex} in block ${blockNumber}.`);
343
+ transactions.set(txKey(blockNumber, transactionIndex), {
344
+ hash: tx.hash,
345
+ transactionIndex,
346
+ from: lower(tx.from),
347
+ to: tx.to ? lower(tx.to) : null,
348
+ value: hexToBigInt(tx.value),
349
+ gas: hexToBigInt(tx.gas),
350
+ gasPrice: tx.gasPrice === undefined ? null : hexToBigInt(tx.gasPrice),
351
+ input: tx.input,
352
+ nonce: hexToBigInt(tx.nonce),
353
+ status: null,
354
+ });
355
+ }),
356
+ );
357
+ return { block, transactions };
358
+ }
359
+
360
+ /** Receipt statuses for a block's transactions: `eth_getBlockReceipts`, else one receipt each. */
361
+ async function fillStatuses(request: RpcRequest, blockNumber: number, transactions: Map<string, TxInfo>) {
362
+ let receipts: any[] | null = null;
363
+ try {
364
+ receipts = await request("eth_getBlockReceipts", [toHex(blockNumber)], { maxTries: 2 });
365
+ } catch {
366
+ receipts = null;
367
+ }
368
+ if (receipts) {
369
+ for (const receipt of receipts) {
370
+ const tx = transactions.get(txKey(blockNumber, Number(receipt.transactionIndex)));
371
+ if (tx) tx.status = receipt.status === undefined ? null : Number(receipt.status);
372
+ }
373
+ return;
374
+ }
375
+ await Promise.all(
376
+ [...transactions.entries()]
377
+ .filter(([key]) => key.startsWith(`${blockNumber}:`))
378
+ .map(async ([, tx]) => {
379
+ const receipt = await request("eth_getTransactionReceipt", [tx.hash]);
380
+ tx.status = receipt?.status === undefined ? null : Number(receipt.status);
381
+ }),
382
+ );
383
+ }
384
+
385
+ /** A parity-format trace (`trace_filter`) as a call, or null for anything but a CALL. */
386
+ export function callOfParityTrace(trace: any): RawCall | null {
387
+ if (trace.type !== "call" || trace.action?.input === undefined) return null;
388
+ if (trace.transactionPosition === null || trace.transactionPosition === undefined) return null;
389
+ return {
390
+ blockNumber: Number(trace.blockNumber),
391
+ transactionIndex: Number(trace.transactionPosition),
392
+ traceAddress: (trace.traceAddress ?? []).map(Number),
393
+ callType: String(trace.action.callType ?? "call").toLowerCase(),
394
+ from: lower(trace.action.from),
395
+ to: lower(trace.action.to),
396
+ input: lower(trace.action.input),
397
+ output: lower(trace.result?.output ?? "0x"),
398
+ value: hexToBigInt(trace.action.value),
399
+ gasUsed: hexToBigInt(trace.result?.gasUsed),
400
+ error: trace.error !== undefined && trace.error !== null,
401
+ };
402
+ }
403
+
404
+ const CALL_TYPES = new Set(["CALL", "STATICCALL", "DELEGATECALL", "CALLCODE"]);
405
+
406
+ /** Flattens `debug_traceBlockByNumber`'s callTracer trees into calls, in trace order. */
407
+ export function callsOfCallTracer(blockNumber: number, traced: any[]): { calls: RawCall[]; topLevelFailed: Set<number> } {
408
+ const calls: RawCall[] = [];
409
+ const topLevelFailed = new Set<number>();
410
+ traced.forEach((entry, transactionIndex) => {
411
+ const root = entry?.result ?? entry;
412
+ if (!root) return;
413
+ if (root.error) topLevelFailed.add(transactionIndex);
414
+ const walk = (frame: any, traceAddress: number[]) => {
415
+ if (CALL_TYPES.has(String(frame.type ?? "CALL").toUpperCase()) && frame.to) {
416
+ calls.push({
417
+ blockNumber,
418
+ transactionIndex,
419
+ traceAddress,
420
+ callType: String(frame.type ?? "CALL").toLowerCase(),
421
+ from: lower(frame.from),
422
+ to: lower(frame.to),
423
+ input: lower(frame.input ?? "0x"),
424
+ output: lower(frame.output ?? "0x"),
425
+ value: hexToBigInt(frame.value),
426
+ gasUsed: hexToBigInt(frame.gasUsed),
427
+ error: frame.error !== undefined && frame.error !== null && frame.error !== "",
428
+ });
429
+ }
430
+ (frame.calls ?? []).forEach((child: any, index: number) => walk(child, [...traceAddress, index]));
431
+ };
432
+ walk(root, []);
433
+ });
434
+ return { calls, topLevelFailed };
435
+ }
436
+
437
+ function rpcBackend(): Backend {
438
+ // One mode per process once it is known: trace_filter where the node has it.
439
+ let mode: "trace_filter" | "debug" | null =
440
+ process.env.ENVIO_SUBGRAPH_TRACE_MODE === "debug"
441
+ ? "debug"
442
+ : process.env.ENVIO_SUBGRAPH_TRACE_MODE === "trace_filter"
443
+ ? "trace_filter"
444
+ : null;
445
+
446
+ const viaTraceFilter = async (
447
+ request: CallRequest,
448
+ selections: Selection[],
449
+ rpc: RpcRequest,
450
+ options?: { maxTries?: number; span?: number },
451
+ ) => {
452
+ const span = options?.span ?? positive("ENVIO_SUBGRAPH_TRACE_FILTER_BLOCKS", 500);
453
+ const toBlock = Math.min(request.toBlock, request.fromBlock + span - 1);
454
+ const anyAddress = selections.some((selection) => selection.addresses.length === 0);
455
+ const addresses = [...new Set(selections.flatMap((selection) => selection.addresses))];
456
+ const traces: any[] = await rpc(
457
+ "trace_filter",
458
+ [
459
+ {
460
+ fromBlock: toHex(request.fromBlock),
461
+ toBlock: toHex(toBlock),
462
+ ...(anyAddress ? {} : { toAddress: addresses }),
463
+ },
464
+ ],
465
+ options,
466
+ );
467
+ const calls = traces.map(callOfParityTrace).filter((call): call is RawCall => call !== null);
468
+ return { calls, toBlock, topLevelFailed: new Map<number, Set<number>>() };
469
+ };
470
+
471
+ const viaDebugTrace = async (request: CallRequest, rpc: RpcRequest) => {
472
+ const span = positive("ENVIO_SUBGRAPH_TRACE_BLOCKS_PER_PAGE", 25);
473
+ const toBlock = Math.min(request.toBlock, request.fromBlock + span - 1);
474
+ const calls: RawCall[] = [];
475
+ const topLevelFailed = new Map<number, Set<number>>();
476
+ const blocks = Array.from({ length: toBlock - request.fromBlock + 1 }, (_, i) => request.fromBlock + i);
477
+ const concurrency = positive("ENVIO_SUBGRAPH_TRACE_CONCURRENCY", 4);
478
+ let next = 0;
479
+ await Promise.all(
480
+ Array.from({ length: Math.min(concurrency, blocks.length) }, async () => {
481
+ while (next < blocks.length) {
482
+ const blockNumber = blocks[next++];
483
+ const traced: any[] = await rpc("debug_traceBlockByNumber", [toHex(blockNumber), { tracer: "callTracer" }]);
484
+ const flat = callsOfCallTracer(blockNumber, traced);
485
+ calls.push(...flat.calls);
486
+ topLevelFailed.set(blockNumber, flat.topLevelFailed);
487
+ }
488
+ }),
489
+ );
490
+ return { calls, toBlock, topLevelFailed };
491
+ };
492
+
493
+ return {
494
+ name: "RPC traces",
495
+ async page(request, selections) {
496
+ const rpc = makeRpcRequest(request.rpcUrls);
497
+ let result: { calls: RawCall[]; toBlock: number; topLevelFailed: Map<number, Set<number>> };
498
+ if (mode === "debug") result = await viaDebugTrace(request, rpc);
499
+ else if (mode === "trace_filter") result = await viaTraceFilter(request, selections, rpc);
500
+ else {
501
+ try {
502
+ // A node without trace_filter answers every try alike — or, for an address filter over
503
+ // old blocks, doesn't answer — so this probe is short and doesn't wait out a rate limit.
504
+ result = await viaTraceFilter(request, selections, rpc, { maxTries: 2, span: 10 });
505
+ mode = "trace_filter";
506
+ } catch (error) {
507
+ warn(
508
+ `Envio Subgraph: the RPC node doesn't serve trace_filter (${String(error).split("\n")[0]}); ` +
509
+ "reading calls with debug_traceBlockByNumber, one block at a time.",
510
+ );
511
+ mode = "debug";
512
+ result = await viaDebugTrace(request, rpc);
513
+ }
514
+ }
515
+
516
+ // Only blocks holding a call a handler wants need their body and receipts.
517
+ const watched = watchedRegistrations(request);
518
+ result.calls = result.calls.filter((call) => watched(call).length > 0);
519
+ const wanted = new Map<number, Set<number>>();
520
+ for (const call of result.calls) {
521
+ const positions = wanted.get(call.blockNumber) ?? new Set<number>();
522
+ positions.add(call.transactionIndex);
523
+ wanted.set(call.blockNumber, positions);
524
+ }
525
+ const blocks = new Map<number, BlockInfo>();
526
+ const transactions = new Map<string, TxInfo>();
527
+ await Promise.all(
528
+ [...wanted].map(async ([blockNumber, positions]) => {
529
+ const body = await blockAndTransactions(rpc, blockNumber, positions);
530
+ await fillStatuses(rpc, blockNumber, body.transactions);
531
+ blocks.set(blockNumber, body.block);
532
+ for (const [key, tx] of body.transactions) transactions.set(key, tx);
533
+ }),
534
+ );
535
+ // A transaction whose own top-level call failed has failed whatever its receipt.
536
+ for (const [blockNumber, failed] of result.topLevelFailed) {
537
+ for (const transactionIndex of failed) {
538
+ const tx = transactions.get(txKey(blockNumber, transactionIndex));
539
+ if (tx) tx.status = 0;
540
+ }
541
+ }
542
+ return {
543
+ calls: result.calls,
544
+ blocks,
545
+ transactions,
546
+ latestFetchedBlockNumber: result.toBlock,
547
+ knownHeight: null,
548
+ };
549
+ },
550
+ };
551
+ }
552
+
553
+ // ---------------------------------------------------------------------------
554
+ // Matching: graph-node's call filter and trigger rules
555
+ // ---------------------------------------------------------------------------
556
+
557
+ /**
558
+ * `EthereumCallFilter::matches` and `parse_call_triggers`: a call to an
559
+ * address a handler watches, with input of a selector or more, that did not
560
+ * itself fail and — for a call handler — whose transaction succeeded. A block
561
+ * handler's `WithCallTo` trigger takes every such call; it is not held to the
562
+ * transaction's status.
563
+ */
564
+ /** The registrations a call is a trigger for, before its transaction's status is known. */
565
+ export function watchedRegistrations(request: CallRequest): (call: RawCall) => CallRegistration[] {
566
+ const owned = new Map<string, Set<string>>();
567
+ for (const [contract, addresses] of Object.entries(request.addresses)) owned.set(contract, new Set(addresses));
568
+ return (call) => {
569
+ if (call.error || (call.input.length - 2) / 2 < 4) return [];
570
+ const selector = call.input.slice(0, 10);
571
+ return request.registrations.filter(
572
+ (registration) =>
573
+ (registration.startBlock === null || call.blockNumber >= registration.startBlock) &&
574
+ (registration.isWildcard || owned.get(registration.contractName)?.has(call.to)) &&
575
+ (registration.rank === 2 || registration.selector === selector),
576
+ );
577
+ };
578
+ }
579
+
580
+ export function buildItems(request: CallRequest, page: TracePage): CallItem[] {
581
+ const items: CallItem[] = [];
582
+ const calls = page.calls
583
+ .filter((call) => call.blockNumber >= request.fromBlock && call.blockNumber <= page.latestFetchedBlockNumber)
584
+ .sort(
585
+ (a, b) =>
586
+ a.blockNumber - b.blockNumber ||
587
+ a.transactionIndex - b.transactionIndex ||
588
+ compareTraceAddress(a.traceAddress, b.traceAddress),
589
+ );
590
+ const watched = watchedRegistrations(request);
591
+
592
+ for (const call of calls) {
593
+ for (const registration of watched(call)) {
594
+ const transaction = page.transactions.get(txKey(call.blockNumber, call.transactionIndex));
595
+ const block = page.blocks.get(call.blockNumber);
596
+ if (!transaction || !block) {
597
+ throw new Error(
598
+ `The ${request.chainId} trace backend returned a call in block ${call.blockNumber} ` +
599
+ `(transaction ${call.transactionIndex}) without that ${transaction ? "block" : "transaction"}.`,
600
+ );
601
+ }
602
+ if (registration.rank === 1 && transaction.status === 0) continue;
603
+
604
+ const { status: _status, ...transactionFields } = transaction;
605
+ items.push({
606
+ registrationIndex: registration.index,
607
+ blockNumber: call.blockNumber,
608
+ transactionIndex: call.transactionIndex,
609
+ traceAddress: call.traceAddress,
610
+ payload: {
611
+ contractName: registration.contractName,
612
+ eventName: registration.eventName,
613
+ params: {
614
+ callInput: call.input,
615
+ callOutput: call.output,
616
+ callFrom: call.from,
617
+ callValue: call.value,
618
+ callGasUsed: call.gasUsed,
619
+ },
620
+ chainId: request.chainId,
621
+ srcAddress: call.to,
622
+ logIndex: 0,
623
+ transaction: transactionFields,
624
+ block,
625
+ },
626
+ });
627
+ }
628
+ }
629
+ return items;
630
+ }
631
+
632
+ // ---------------------------------------------------------------------------
633
+ // Entry point
634
+ // ---------------------------------------------------------------------------
635
+
636
+ const backends = new Map<number, Promise<Backend>>();
637
+
638
+ /** HyperSync where its trace endpoint answers for the chain, else the node. */
639
+ async function backendFor(chainId: number): Promise<Backend> {
640
+ const forced = process.env.ENVIO_SUBGRAPH_TRACE_MODE;
641
+ if (forced === "debug" || forced === "trace_filter") return rpcBackend();
642
+ if (forced === "hypersync") return hyperSyncBackend();
643
+ const known = HYPERSYNC_TRACE_CHAINS.has(chainId) || process.env.ENVIO_SUBGRAPH_TRACES_URL !== undefined;
644
+ return known ? hyperSyncBackend() : rpcBackend();
645
+ }
646
+
647
+ export function resetCallBackends() {
648
+ backends.clear();
649
+ }
650
+
651
+ export async function fetchCalls(request: CallRequest): Promise<CallResponse> {
652
+ let backend = backends.get(request.chainId);
653
+ if (!backend) {
654
+ backend = backendFor(request.chainId);
655
+ backends.set(request.chainId, backend);
656
+ }
657
+ const resolved = await backend;
658
+
659
+ // One search per contract: its addresses, narrowed to the selectors of its
660
+ // call handlers unless a block handler watches every call.
661
+ const selections: Selection[] = [];
662
+ const contracts = new Set(request.registrations.map((registration) => registration.contractName));
663
+ for (const contract of contracts) {
664
+ const registrations = request.registrations.filter((registration) => registration.contractName === contract);
665
+ const wildcard = registrations.some((registration) => registration.isWildcard);
666
+ const addresses = request.addresses[contract] ?? [];
667
+ if (addresses.length === 0 && !wildcard) continue;
668
+ const everything = registrations.some((registration) => registration.rank === 2);
669
+ selections.push({
670
+ addresses: wildcard ? [] : addresses,
671
+ selectors: everything
672
+ ? null
673
+ : [...new Set(registrations.map((registration) => registration.selector as string))],
674
+ });
675
+ }
676
+ if (selections.length === 0) {
677
+ return { items: [], latestFetchedBlockNumber: request.toBlock, blocks: [], knownHeight: null };
678
+ }
679
+
680
+ const page = await resolved.page(request, selections);
681
+ if (page.latestFetchedBlockNumber < request.fromBlock) {
682
+ throw new Error(
683
+ `${resolved.name} for chain ${request.chainId} has not reached block ${request.fromBlock} yet ` +
684
+ `(it answered through ${page.latestFetchedBlockNumber}).`,
685
+ );
686
+ }
687
+ const items = buildItems(request, page);
688
+ const seen = new Set<number>();
689
+ const blocks = [];
690
+ for (const item of items) {
691
+ if (seen.has(item.blockNumber)) continue;
692
+ seen.add(item.blockNumber);
693
+ const block = page.blocks.get(item.blockNumber)!;
694
+ blocks.push({ blockNumber: item.blockNumber, blockHash: block.hash, blockTimestamp: block.timestamp });
695
+ }
696
+ return {
697
+ items,
698
+ latestFetchedBlockNumber: page.latestFetchedBlockNumber,
699
+ blocks,
700
+ knownHeight: page.knownHeight,
701
+ };
702
+ }