@secondlayer/subgraphs 6.4.0 → 6.5.0
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 +1 -0
- package/dist/index.d.ts +121 -121
- package/dist/index.js +262 -27
- package/dist/index.js.map +9 -8
- package/dist/runtime/emitter.js +1 -1
- package/dist/runtime/emitter.js.map +2 -2
- package/dist/runtime/replay.d.ts +32 -13
- package/dist/runtime/replay.js +723 -22
- package/dist/runtime/replay.js.map +8 -5
- package/dist/schema/index.d.ts +39 -39
- package/dist/schema/index.js +27 -14
- package/dist/schema/index.js.map +5 -5
- package/dist/testing/index.d.ts +105 -105
- package/dist/testing/index.js +406 -95
- package/dist/testing/index.js.map +7 -6
- package/dist/types.d.ts +31 -31
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +27 -27
- package/dist/validate.js +1 -1
- package/dist/validate.js.map +2 -2
- package/dist/verification.d.ts +41 -26
- package/dist/verification.js +23 -13
- package/dist/verification.js.map +4 -4
- package/dist/verify/index.d.ts +475 -0
- package/dist/verify/index.js +3341 -0
- package/dist/verify/index.js.map +27 -0
- package/package.json +9 -3
package/dist/runtime/replay.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/runtime/replay.ts
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3
3
|
import { getTargetDb } from "@secondlayer/shared/db";
|
|
4
4
|
import { getWebhook } from "@secondlayer/shared/db/queries/webhooks";
|
|
5
5
|
import { logger as logger2 } from "@secondlayer/shared/logger";
|
|
6
|
-
import { sql } from "kysely";
|
|
6
|
+
import { sql as sql2 } from "kysely";
|
|
7
7
|
|
|
8
8
|
// src/schema/utils.ts
|
|
9
9
|
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
@@ -28,16 +28,405 @@ import {
|
|
|
28
28
|
} from "@secondlayer/shared/index-http";
|
|
29
29
|
import { logger } from "@secondlayer/shared/logger";
|
|
30
30
|
|
|
31
|
+
// src/verification.ts
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
33
|
+
// package.json
|
|
34
|
+
var package_default = {
|
|
35
|
+
name: "@secondlayer/subgraphs",
|
|
36
|
+
version: "6.5.0",
|
|
37
|
+
description: "Describe the Stacks data you want as one TypeScript file; it fills itself and survives reorgs.",
|
|
38
|
+
license: "MIT",
|
|
39
|
+
repository: {
|
|
40
|
+
type: "git",
|
|
41
|
+
url: "git+https://github.com/ryanwaits/secondlayer.git",
|
|
42
|
+
directory: "packages/subgraphs"
|
|
43
|
+
},
|
|
44
|
+
homepage: "https://github.com/ryanwaits/secondlayer/tree/main/packages/subgraphs#readme",
|
|
45
|
+
type: "module",
|
|
46
|
+
main: "./dist/index.js",
|
|
47
|
+
types: "./dist/index.d.ts",
|
|
48
|
+
exports: {
|
|
49
|
+
".": {
|
|
50
|
+
types: "./dist/index.d.ts",
|
|
51
|
+
import: "./dist/index.js"
|
|
52
|
+
},
|
|
53
|
+
"./types": {
|
|
54
|
+
types: "./dist/types.d.ts",
|
|
55
|
+
import: "./dist/types.js"
|
|
56
|
+
},
|
|
57
|
+
"./schema": {
|
|
58
|
+
types: "./dist/schema/index.d.ts",
|
|
59
|
+
import: "./dist/schema/index.js"
|
|
60
|
+
},
|
|
61
|
+
"./validate": {
|
|
62
|
+
types: "./dist/validate.d.ts",
|
|
63
|
+
import: "./dist/validate.js"
|
|
64
|
+
},
|
|
65
|
+
"./verification": {
|
|
66
|
+
types: "./dist/verification.d.ts",
|
|
67
|
+
import: "./dist/verification.js"
|
|
68
|
+
},
|
|
69
|
+
"./runtime/replay": {
|
|
70
|
+
types: "./dist/runtime/replay.d.ts",
|
|
71
|
+
import: "./dist/runtime/replay.js"
|
|
72
|
+
},
|
|
73
|
+
"./runtime/emitter": {
|
|
74
|
+
types: "./dist/runtime/emitter.d.ts",
|
|
75
|
+
import: "./dist/runtime/emitter.js"
|
|
76
|
+
},
|
|
77
|
+
"./testing": {
|
|
78
|
+
types: "./dist/testing/index.d.ts",
|
|
79
|
+
import: "./dist/testing/index.js"
|
|
80
|
+
},
|
|
81
|
+
"./verify": {
|
|
82
|
+
types: "./dist/verify/index.d.ts",
|
|
83
|
+
import: "./dist/verify/index.js"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
files: ["dist"],
|
|
87
|
+
scripts: {
|
|
88
|
+
build: "bunup",
|
|
89
|
+
dev: "bunup --watch",
|
|
90
|
+
test: "bun test",
|
|
91
|
+
typecheck: "tsc --noEmit",
|
|
92
|
+
prepublishOnly: "bun run build"
|
|
93
|
+
},
|
|
94
|
+
dependencies: {
|
|
95
|
+
"@secondlayer/shared": "^11.23.0",
|
|
96
|
+
"@secondlayer/stacks": "^6.2.0",
|
|
97
|
+
"@secondlayer/verify": "^0.3.0",
|
|
98
|
+
esbuild: "^0.28.2",
|
|
99
|
+
kysely: "0.28.17",
|
|
100
|
+
zod: "^4.3.6"
|
|
101
|
+
},
|
|
102
|
+
devDependencies: {
|
|
103
|
+
"@types/bun": "latest",
|
|
104
|
+
bunup: "0.16.32",
|
|
105
|
+
typescript: "^5"
|
|
106
|
+
},
|
|
107
|
+
publishConfig: {
|
|
108
|
+
access: "public"
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/verification.ts
|
|
113
|
+
var SUBGRAPHS_RUNTIME = `@secondlayer/subgraphs@${package_default.version}`;
|
|
114
|
+
var FORBIDDEN_GLOBALS = {
|
|
115
|
+
Date: "wall-clock time differs per run",
|
|
116
|
+
performance: "timers differ per run",
|
|
117
|
+
process: "host environment differs per run",
|
|
118
|
+
fetch: "network reads are not chain inputs",
|
|
119
|
+
crypto: "randomness differs per run",
|
|
120
|
+
setTimeout: "scheduling order differs per run",
|
|
121
|
+
setInterval: "scheduling order differs per run",
|
|
122
|
+
setImmediate: "scheduling order differs per run",
|
|
123
|
+
clearTimeout: "scheduling order differs per run",
|
|
124
|
+
clearInterval: "scheduling order differs per run",
|
|
125
|
+
queueMicrotask: "scheduling order differs per run",
|
|
126
|
+
WeakRef: "garbage collection timing differs per run",
|
|
127
|
+
FinalizationRegistry: "garbage collection timing differs per run",
|
|
128
|
+
Atomics: "shared memory is not single-threaded",
|
|
129
|
+
SharedArrayBuffer: "shared memory is not single-threaded",
|
|
130
|
+
Intl: "locale and time zone data differ per host",
|
|
131
|
+
Temporal: "locale and time zone data differ per host",
|
|
132
|
+
eval: "generated code hides inputs from the scan",
|
|
133
|
+
WebAssembly: "compiled code hides inputs from the scan",
|
|
134
|
+
ShadowRealm: "a nested realm escapes the allow-list",
|
|
135
|
+
require: "imports must be bundled into the handler"
|
|
136
|
+
};
|
|
137
|
+
var ALLOWED_GLOBALS = [
|
|
138
|
+
"undefined",
|
|
139
|
+
"NaN",
|
|
140
|
+
"Infinity",
|
|
141
|
+
"globalThis",
|
|
142
|
+
"isNaN",
|
|
143
|
+
"isFinite",
|
|
144
|
+
"parseInt",
|
|
145
|
+
"parseFloat",
|
|
146
|
+
"decodeURI",
|
|
147
|
+
"decodeURIComponent",
|
|
148
|
+
"encodeURI",
|
|
149
|
+
"encodeURIComponent",
|
|
150
|
+
"escape",
|
|
151
|
+
"unescape",
|
|
152
|
+
"Object",
|
|
153
|
+
"Function",
|
|
154
|
+
"Array",
|
|
155
|
+
"Number",
|
|
156
|
+
"Boolean",
|
|
157
|
+
"String",
|
|
158
|
+
"Symbol",
|
|
159
|
+
"BigInt",
|
|
160
|
+
"Math",
|
|
161
|
+
"JSON",
|
|
162
|
+
"Reflect",
|
|
163
|
+
"Proxy",
|
|
164
|
+
"Promise",
|
|
165
|
+
"Map",
|
|
166
|
+
"Set",
|
|
167
|
+
"WeakMap",
|
|
168
|
+
"WeakSet",
|
|
169
|
+
"RegExp",
|
|
170
|
+
"Iterator",
|
|
171
|
+
"DisposableStack",
|
|
172
|
+
"AsyncDisposableStack",
|
|
173
|
+
"Error",
|
|
174
|
+
"AggregateError",
|
|
175
|
+
"SuppressedError",
|
|
176
|
+
"EvalError",
|
|
177
|
+
"RangeError",
|
|
178
|
+
"ReferenceError",
|
|
179
|
+
"SyntaxError",
|
|
180
|
+
"TypeError",
|
|
181
|
+
"URIError",
|
|
182
|
+
"ArrayBuffer",
|
|
183
|
+
"DataView",
|
|
184
|
+
"Int8Array",
|
|
185
|
+
"Uint8Array",
|
|
186
|
+
"Uint8ClampedArray",
|
|
187
|
+
"Int16Array",
|
|
188
|
+
"Uint16Array",
|
|
189
|
+
"Int32Array",
|
|
190
|
+
"Uint32Array",
|
|
191
|
+
"Float16Array",
|
|
192
|
+
"Float32Array",
|
|
193
|
+
"Float64Array",
|
|
194
|
+
"BigInt64Array",
|
|
195
|
+
"BigUint64Array",
|
|
196
|
+
"TextEncoder",
|
|
197
|
+
"TextDecoder",
|
|
198
|
+
"atob",
|
|
199
|
+
"btoa",
|
|
200
|
+
"console"
|
|
201
|
+
];
|
|
202
|
+
var ALLOWED_MATH = [
|
|
203
|
+
"abs",
|
|
204
|
+
"min",
|
|
205
|
+
"max",
|
|
206
|
+
"floor",
|
|
207
|
+
"ceil",
|
|
208
|
+
"trunc",
|
|
209
|
+
"sign",
|
|
210
|
+
"round",
|
|
211
|
+
"E",
|
|
212
|
+
"LN10",
|
|
213
|
+
"LN2",
|
|
214
|
+
"LOG10E",
|
|
215
|
+
"LOG2E",
|
|
216
|
+
"PI",
|
|
217
|
+
"SQRT1_2",
|
|
218
|
+
"SQRT2"
|
|
219
|
+
];
|
|
220
|
+
var LOCALE_METHODS = [
|
|
221
|
+
"toLocaleString",
|
|
222
|
+
"toLocaleDateString",
|
|
223
|
+
"toLocaleTimeString",
|
|
224
|
+
"toLocaleUpperCase",
|
|
225
|
+
"toLocaleLowerCase",
|
|
226
|
+
"localeCompare"
|
|
227
|
+
];
|
|
228
|
+
function formatFinding(f) {
|
|
229
|
+
return `${f.file}:${f.line}:${f.column} ${f.name}: ${f.reason}`;
|
|
230
|
+
}
|
|
231
|
+
var STATE_SOURCE_TYPES = new Set([
|
|
232
|
+
"var_set",
|
|
233
|
+
"map_set",
|
|
234
|
+
"map_delete"
|
|
235
|
+
]);
|
|
236
|
+
var LEVEL_RANK = { state: 0, events: 1, none: 2 };
|
|
237
|
+
var MAX_LISTED_FINDINGS = 10;
|
|
238
|
+
function deriveVerification(def, findings = []) {
|
|
239
|
+
let level = "state";
|
|
240
|
+
const raise = (to) => {
|
|
241
|
+
if (LEVEL_RANK[to] > LEVEL_RANK[level])
|
|
242
|
+
level = to;
|
|
243
|
+
};
|
|
244
|
+
const reasons = [];
|
|
245
|
+
for (const [name, source] of Object.entries(def.sources ?? {})) {
|
|
246
|
+
const filter = source;
|
|
247
|
+
if (filter.trait) {
|
|
248
|
+
raise("none");
|
|
249
|
+
reasons.push(`source "${name}": trait scope needs a proven contract registry`);
|
|
250
|
+
} else if (filter.type === "map_insert") {
|
|
251
|
+
raise("events");
|
|
252
|
+
reasons.push(`map_insert source "${name}": map_insert vs map_set needs event proofs`);
|
|
253
|
+
} else if (!STATE_SOURCE_TYPES.has(filter.type)) {
|
|
254
|
+
raise("events");
|
|
255
|
+
reasons.push(`${filter.type} source "${name}" needs event proofs`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (def.backfillMode === "concurrent") {
|
|
259
|
+
raise("none");
|
|
260
|
+
reasons.push("backfillMode concurrent: tip-first order is not chain order");
|
|
261
|
+
}
|
|
262
|
+
if (findings.some((f) => f.kind === "needs-events"))
|
|
263
|
+
raise("events");
|
|
264
|
+
for (const f of findings.slice(0, MAX_LISTED_FINDINGS)) {
|
|
265
|
+
reasons.push(f.kind === "needs-events" ? `${formatFinding(f)} (needs event proofs)` : formatFinding(f));
|
|
266
|
+
}
|
|
267
|
+
if (findings.length > MAX_LISTED_FINDINGS) {
|
|
268
|
+
reasons.push(`…and ${findings.length - MAX_LISTED_FINDINGS} more handler findings`);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
level,
|
|
272
|
+
reasons,
|
|
273
|
+
unproven: reasons.length === 0 ? [
|
|
274
|
+
"tx attribution of writes: event.tx, _tx_id (needs event proofs)",
|
|
275
|
+
"intermediate writes within a block (needs event proofs)"
|
|
276
|
+
] : []
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function isDeterminismViolation(v) {
|
|
280
|
+
return v.level === "state" && v.reasons.length > 0;
|
|
281
|
+
}
|
|
282
|
+
function sha256(input) {
|
|
283
|
+
return createHash("sha256").update(input).digest("hex");
|
|
284
|
+
}
|
|
285
|
+
function pinPreimage(input) {
|
|
286
|
+
const fields = {
|
|
287
|
+
handlerHash: sha256(input.handlerCode),
|
|
288
|
+
network: input.network,
|
|
289
|
+
runtime: input.runtime ?? SUBGRAPHS_RUNTIME,
|
|
290
|
+
schemaHash: input.schemaHash,
|
|
291
|
+
startBlock: input.startBlock ?? null
|
|
292
|
+
};
|
|
293
|
+
return JSON.stringify(fields);
|
|
294
|
+
}
|
|
295
|
+
function computePin(input) {
|
|
296
|
+
return sha256(pinPreimage(input));
|
|
297
|
+
}
|
|
298
|
+
|
|
31
299
|
// src/runtime/batch-loader.ts
|
|
300
|
+
import { sql } from "kysely";
|
|
301
|
+
|
|
302
|
+
// src/runtime/state-writes.ts
|
|
303
|
+
var DATA_MAP = "0";
|
|
304
|
+
var VARIABLE = "1";
|
|
305
|
+
var HEX = /^(?:[0-9a-f]{2})+$/;
|
|
306
|
+
function parseStateWriteKey(key) {
|
|
307
|
+
if (!key.startsWith("vm::"))
|
|
308
|
+
return null;
|
|
309
|
+
const parts = key.slice(4).split("::");
|
|
310
|
+
const [contractId, store] = parts;
|
|
311
|
+
if (!contractId?.includes("."))
|
|
312
|
+
return null;
|
|
313
|
+
if (store === DATA_MAP && parts.length === 4) {
|
|
314
|
+
const [, , map, rawKey] = parts;
|
|
315
|
+
if (!map || !rawKey || !HEX.test(rawKey))
|
|
316
|
+
return null;
|
|
317
|
+
return { contractId, kind: "map", map, rawKey };
|
|
318
|
+
}
|
|
319
|
+
if (store === VARIABLE && parts.length === 3 && parts[2]) {
|
|
320
|
+
return { contractId, kind: "var", varName: parts[2] };
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
325
|
+
function storedValue(w) {
|
|
326
|
+
const h = w.value_hex.startsWith("0x") ? w.value_hex.slice(2) : w.value_hex;
|
|
327
|
+
const bytes = new Uint8Array(h.length / 2);
|
|
328
|
+
for (let i = 0;i < bytes.length; i++) {
|
|
329
|
+
bytes[i] = Number.parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
330
|
+
}
|
|
331
|
+
let value;
|
|
332
|
+
try {
|
|
333
|
+
value = utf8.decode(bytes);
|
|
334
|
+
} catch {
|
|
335
|
+
throw new Error(`state write ${w.ordinal} value is not UTF-8 hex`);
|
|
336
|
+
}
|
|
337
|
+
value = value.toLowerCase();
|
|
338
|
+
if (!HEX.test(value)) {
|
|
339
|
+
throw new Error(`state write ${w.ordinal} value is not a Clarity value`);
|
|
340
|
+
}
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
343
|
+
function writeEvent(w) {
|
|
344
|
+
const key = parseStateWriteKey(w.key);
|
|
345
|
+
if (!key)
|
|
346
|
+
return null;
|
|
347
|
+
const value = storedValue(w);
|
|
348
|
+
if (key.kind === "var") {
|
|
349
|
+
return {
|
|
350
|
+
type: "var_set",
|
|
351
|
+
data: {
|
|
352
|
+
contract_identifier: key.contractId,
|
|
353
|
+
var_name: key.varName,
|
|
354
|
+
raw_value: `0x${value}`
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const base = {
|
|
359
|
+
contract_identifier: key.contractId,
|
|
360
|
+
map_name: key.map,
|
|
361
|
+
raw_key: `0x${key.rawKey}`
|
|
362
|
+
};
|
|
363
|
+
if (value === "09")
|
|
364
|
+
return { type: "map_delete", data: base };
|
|
365
|
+
if (value.startsWith("0a")) {
|
|
366
|
+
return {
|
|
367
|
+
type: "map_set",
|
|
368
|
+
data: { ...base, raw_value: `0x${value.slice(2)}` }
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
throw new Error(`state write ${w.ordinal} map value is neither (some v) nor none`);
|
|
372
|
+
}
|
|
373
|
+
function stateWriteEvents(writes, txByIndex) {
|
|
374
|
+
const txs = new Map;
|
|
375
|
+
const vmEvents = [];
|
|
376
|
+
for (const w of [...writes].sort((a, b) => a.ordinal - b.ordinal)) {
|
|
377
|
+
const event = writeEvent(w);
|
|
378
|
+
if (!event)
|
|
379
|
+
continue;
|
|
380
|
+
let tx = txs.get(w.tx_index);
|
|
381
|
+
if (!tx) {
|
|
382
|
+
if (w.tx_index === null) {
|
|
383
|
+
tx = { tx_id: "", type: "block", sender: "", status: "success" };
|
|
384
|
+
} else {
|
|
385
|
+
const real = txByIndex.get(w.tx_index);
|
|
386
|
+
if (!real) {
|
|
387
|
+
throw new Error(`state write ${w.ordinal} names tx_index ${w.tx_index} the block does not have`);
|
|
388
|
+
}
|
|
389
|
+
tx = real;
|
|
390
|
+
}
|
|
391
|
+
tx = { ...tx, tx_index: 0 };
|
|
392
|
+
txs.set(w.tx_index, tx);
|
|
393
|
+
}
|
|
394
|
+
vmEvents.push({
|
|
395
|
+
id: vmEventId(tx.tx_id || "block", w.ordinal),
|
|
396
|
+
tx_id: tx.tx_id,
|
|
397
|
+
type: event.type,
|
|
398
|
+
event_index: w.ordinal,
|
|
399
|
+
data: event.data,
|
|
400
|
+
clock: "vm"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return { txs: [...txs.values()], vmEvents };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/runtime/batch-loader.ts
|
|
407
|
+
function stateWriteBlocks(writesByHeight, txsByHeight) {
|
|
408
|
+
const out = new Map;
|
|
409
|
+
for (const [height, writes] of writesByHeight) {
|
|
410
|
+
const txByIndex = new Map;
|
|
411
|
+
for (const tx of txsByHeight.get(height) ?? []) {
|
|
412
|
+
if (tx.tx_index !== undefined)
|
|
413
|
+
txByIndex.set(tx.tx_index, tx);
|
|
414
|
+
}
|
|
415
|
+
out.set(height, stateWriteEvents(writes, txByIndex));
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
32
419
|
function vmEventId(txId, vmEventIndex) {
|
|
33
420
|
return `${txId}#vm:${vmEventIndex}`;
|
|
34
421
|
}
|
|
35
|
-
async function loadBlockRange(db, fromHeight, toHeight) {
|
|
36
|
-
const
|
|
422
|
+
async function loadBlockRange(db, fromHeight, toHeight, opts = {}) {
|
|
423
|
+
const feed = opts.stateWrites;
|
|
424
|
+
const [blocks, txs, events, vmRows, writeRows] = await Promise.all([
|
|
37
425
|
db.selectFrom("blocks").selectAll().where("height", ">=", fromHeight).where("height", "<=", toHeight).where("canonical", "=", true).execute(),
|
|
38
426
|
db.selectFrom("transactions").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
|
|
39
427
|
db.selectFrom("events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
|
|
40
|
-
db.selectFrom("vm_events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).orderBy("ordinal", "asc").execute()
|
|
428
|
+
feed ? Promise.resolve([]) : db.selectFrom("vm_events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).orderBy("ordinal", "asc").execute(),
|
|
429
|
+
feed ? loadStateWrites(db, fromHeight, toHeight, feed) : Promise.resolve([])
|
|
41
430
|
]);
|
|
42
431
|
const txsByHeight = new Map;
|
|
43
432
|
for (const tx of txs) {
|
|
@@ -69,24 +458,265 @@ async function loadBlockRange(db, fromHeight, toHeight) {
|
|
|
69
458
|
});
|
|
70
459
|
vmByHeight.set(h, list);
|
|
71
460
|
}
|
|
461
|
+
const writesByHeight = new Map;
|
|
462
|
+
for (const row of writeRows) {
|
|
463
|
+
const h = Number(row.block_height);
|
|
464
|
+
const list = writesByHeight.get(h) ?? [];
|
|
465
|
+
list.push({
|
|
466
|
+
ordinal: Number(row.ordinal),
|
|
467
|
+
tx_index: row.tx_index === null ? null : Number(row.tx_index),
|
|
468
|
+
key: row.key,
|
|
469
|
+
value_hex: row.value_hex
|
|
470
|
+
});
|
|
471
|
+
writesByHeight.set(h, list);
|
|
472
|
+
}
|
|
473
|
+
const named = stateWriteBlocks(writesByHeight, txsByHeight);
|
|
72
474
|
const result = new Map;
|
|
73
475
|
for (const block of blocks) {
|
|
74
476
|
const h = Number(block.height);
|
|
477
|
+
const fed = named.get(h);
|
|
75
478
|
result.set(h, {
|
|
76
479
|
block,
|
|
77
|
-
txs: txsByHeight.get(h) ?? [],
|
|
480
|
+
txs: feed ? fed?.txs ?? [] : txsByHeight.get(h) ?? [],
|
|
78
481
|
events: eventsByHeight.get(h) ?? [],
|
|
79
|
-
vmEvents: vmByHeight.get(h) ?? []
|
|
482
|
+
vmEvents: feed ? fed?.vmEvents ?? [] : vmByHeight.get(h) ?? []
|
|
80
483
|
});
|
|
81
484
|
}
|
|
82
485
|
return result;
|
|
83
486
|
}
|
|
487
|
+
async function loadStateWrites(db, fromHeight, toHeight, feed) {
|
|
488
|
+
const scope = feed.contracts ? sql`AND (${sql.join(feed.contracts.map((c) => sql`starts_with(key, ${`vm::${c}::`})`), sql` OR `)})` : sql``;
|
|
489
|
+
const { rows } = await sql`
|
|
490
|
+
SELECT block_height, ordinal, tx_index, key, value_hex
|
|
491
|
+
FROM state_writes
|
|
492
|
+
WHERE block_height >= ${fromHeight} AND block_height <= ${toHeight} ${scope}
|
|
493
|
+
ORDER BY block_height, ordinal
|
|
494
|
+
`.execute(db);
|
|
495
|
+
return rows;
|
|
496
|
+
}
|
|
84
497
|
|
|
85
498
|
// src/runtime/observer-http-source.ts
|
|
86
499
|
import {
|
|
87
500
|
VM_NODE_TO_STORED_TYPE
|
|
88
501
|
} from "@secondlayer/shared";
|
|
89
502
|
|
|
503
|
+
// src/runtime/realm.ts
|
|
504
|
+
import vm from "node:vm";
|
|
505
|
+
class NondeterminismError extends Error {
|
|
506
|
+
name = "NondeterminismError";
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
class HandlerReadError extends Error {
|
|
510
|
+
name = "HandlerReadError";
|
|
511
|
+
}
|
|
512
|
+
function abortsBlock(err) {
|
|
513
|
+
return err instanceof NondeterminismError || err instanceof HandlerReadError;
|
|
514
|
+
}
|
|
515
|
+
var HOST_GLOBALS = [
|
|
516
|
+
"ArrayBuffer",
|
|
517
|
+
"DataView",
|
|
518
|
+
"Int8Array",
|
|
519
|
+
"Uint8Array",
|
|
520
|
+
"Uint8ClampedArray",
|
|
521
|
+
"Int16Array",
|
|
522
|
+
"Uint16Array",
|
|
523
|
+
"Int32Array",
|
|
524
|
+
"Uint32Array",
|
|
525
|
+
"Float16Array",
|
|
526
|
+
"Float32Array",
|
|
527
|
+
"Float64Array",
|
|
528
|
+
"BigInt64Array",
|
|
529
|
+
"BigUint64Array",
|
|
530
|
+
"TextEncoder",
|
|
531
|
+
"TextDecoder",
|
|
532
|
+
"atob",
|
|
533
|
+
"btoa"
|
|
534
|
+
];
|
|
535
|
+
function forbid(name, reason) {
|
|
536
|
+
return () => {
|
|
537
|
+
throw new NondeterminismError(`${name}: ${reason}`);
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function createRealm() {
|
|
541
|
+
const host = globalThis;
|
|
542
|
+
const sandbox = Object.create(null);
|
|
543
|
+
for (const name of HOST_GLOBALS) {
|
|
544
|
+
if (host[name] !== undefined)
|
|
545
|
+
sandbox[name] = host[name];
|
|
546
|
+
}
|
|
547
|
+
const context = vm.createContext(sandbox, {
|
|
548
|
+
name: "subgraph-handler",
|
|
549
|
+
codeGeneration: { strings: false, wasm: false }
|
|
550
|
+
});
|
|
551
|
+
const g = vm.runInContext("globalThis", context);
|
|
552
|
+
const allowed = new Set(ALLOWED_GLOBALS);
|
|
553
|
+
for (const name of Object.getOwnPropertyNames(g)) {
|
|
554
|
+
if (!allowed.has(name))
|
|
555
|
+
delete g[name];
|
|
556
|
+
}
|
|
557
|
+
for (const [name, reason] of Object.entries(FORBIDDEN_GLOBALS)) {
|
|
558
|
+
Object.defineProperty(g, name, { get: forbid(name, reason) });
|
|
559
|
+
}
|
|
560
|
+
const realmMath = g.Math;
|
|
561
|
+
const safeMath = {};
|
|
562
|
+
const allowedMath = new Set(ALLOWED_MATH);
|
|
563
|
+
for (const key of Object.getOwnPropertyNames(realmMath)) {
|
|
564
|
+
if (allowedMath.has(key))
|
|
565
|
+
safeMath[key] = realmMath[key];
|
|
566
|
+
else
|
|
567
|
+
Object.defineProperty(safeMath, key, {
|
|
568
|
+
get: forbid(`Math.${key}`, key === "random" ? "randomness differs per run" : "result is engine-approximated")
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
Object.defineProperty(g, "Math", { value: Object.freeze(safeMath) });
|
|
572
|
+
for (const ctor of ["String", "Number", "BigInt", "Array", "Object"]) {
|
|
573
|
+
const proto = g[ctor].prototype;
|
|
574
|
+
for (const method of LOCALE_METHODS) {
|
|
575
|
+
if (Object.prototype.hasOwnProperty.call(proto, method)) {
|
|
576
|
+
Object.defineProperty(proto, method, {
|
|
577
|
+
value: forbid(method, "output depends on the host's locale data")
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
Object.preventExtensions(g);
|
|
583
|
+
return context;
|
|
584
|
+
}
|
|
585
|
+
function checkRowValues(where, values) {
|
|
586
|
+
for (const [col, value] of Object.entries(values)) {
|
|
587
|
+
checkValue(value, `${where}.${col}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function checkValue(value, path) {
|
|
591
|
+
switch (typeof value) {
|
|
592
|
+
case "number":
|
|
593
|
+
if (!Number.isSafeInteger(value)) {
|
|
594
|
+
throw new TypeError(`${path}: ${value} is not a safe integer; verifiable subgraphs store bigint or integers only`);
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
case "function":
|
|
598
|
+
case "symbol":
|
|
599
|
+
throw new TypeError(`${path}: a ${typeof value} is not a row value`);
|
|
600
|
+
case "object":
|
|
601
|
+
if (value === null || ArrayBuffer.isView(value))
|
|
602
|
+
return;
|
|
603
|
+
if (Array.isArray(value)) {
|
|
604
|
+
value.forEach((v, i) => checkValue(v, `${path}[${i}]`));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
for (const [k, v] of Object.entries(value))
|
|
608
|
+
checkValue(v, `${path}.${k}`);
|
|
609
|
+
return;
|
|
610
|
+
default:
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function deterministicContext(ctx, schema) {
|
|
615
|
+
let pendingReads = 0;
|
|
616
|
+
const knownTable = (table) => {
|
|
617
|
+
if (!Object.prototype.hasOwnProperty.call(schema, table)) {
|
|
618
|
+
throw new Error(`Table "${table}" not found in subgraph schema`);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
const guard = (op) => {
|
|
622
|
+
if (pendingReads > 0) {
|
|
623
|
+
throw new NondeterminismError(`ctx.${op} while a ctx read is pending: concurrent ctx calls order writes by database latency; await each read before the next call`);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const read = async (op, run) => {
|
|
627
|
+
guard(op);
|
|
628
|
+
pendingReads++;
|
|
629
|
+
try {
|
|
630
|
+
return await run();
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if (err instanceof NondeterminismError)
|
|
633
|
+
throw err;
|
|
634
|
+
throw new HandlerReadError(`ctx.${op} failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
635
|
+
} finally {
|
|
636
|
+
pendingReads--;
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
return {
|
|
640
|
+
get block() {
|
|
641
|
+
return ctx.block;
|
|
642
|
+
},
|
|
643
|
+
get tx() {
|
|
644
|
+
return ctx.tx;
|
|
645
|
+
},
|
|
646
|
+
get client() {
|
|
647
|
+
throw new NondeterminismError("ctx.client: contract reads need re-execution proofs");
|
|
648
|
+
},
|
|
649
|
+
insert(table, row) {
|
|
650
|
+
guard("insert");
|
|
651
|
+
checkRowValues(`insert ${table}`, row);
|
|
652
|
+
ctx.insert(table, row);
|
|
653
|
+
},
|
|
654
|
+
update(table, where, set) {
|
|
655
|
+
guard("update");
|
|
656
|
+
checkRowValues(`update ${table}`, where);
|
|
657
|
+
checkRowValues(`update ${table}`, set);
|
|
658
|
+
ctx.update(table, where, set);
|
|
659
|
+
},
|
|
660
|
+
upsert(table, key, row) {
|
|
661
|
+
guard("upsert");
|
|
662
|
+
checkRowValues(`upsert ${table}`, key);
|
|
663
|
+
checkRowValues(`upsert ${table}`, row);
|
|
664
|
+
ctx.upsert(table, key, row);
|
|
665
|
+
},
|
|
666
|
+
delete(table, where) {
|
|
667
|
+
guard("delete");
|
|
668
|
+
ctx.delete(table, where);
|
|
669
|
+
},
|
|
670
|
+
increment(table, key, deltas) {
|
|
671
|
+
guard("increment");
|
|
672
|
+
checkRowValues(`increment ${table}`, key);
|
|
673
|
+
checkRowValues(`increment ${table}`, deltas);
|
|
674
|
+
ctx.increment(table, key, deltas);
|
|
675
|
+
},
|
|
676
|
+
findOne: (table, where) => {
|
|
677
|
+
knownTable(table);
|
|
678
|
+
return read("findOne", () => ctx.findOne(table, where));
|
|
679
|
+
},
|
|
680
|
+
findMany: (table, ...rest) => {
|
|
681
|
+
knownTable(table);
|
|
682
|
+
return read("findMany", () => ctx.findMany(table, ...rest));
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
var realmDefinitions = new WeakSet;
|
|
687
|
+
async function loadDeterministicDefinition(handlerCode) {
|
|
688
|
+
const { transform } = await import("esbuild");
|
|
689
|
+
const { code } = await transform(handlerCode, {
|
|
690
|
+
format: "cjs",
|
|
691
|
+
loader: "js",
|
|
692
|
+
logLevel: "silent"
|
|
693
|
+
});
|
|
694
|
+
const context = createRealm();
|
|
695
|
+
const evaluate = vm.runInContext(`(function (module, exports) {
|
|
696
|
+
${code}
|
|
697
|
+
})`, context, { filename: "handler.js" });
|
|
698
|
+
const mod = { exports: {} };
|
|
699
|
+
evaluate(mod, mod.exports);
|
|
700
|
+
const def = mod.exports.default ?? mod.exports;
|
|
701
|
+
const sources = structuredClone(def.sources);
|
|
702
|
+
const schema = structuredClone(def.schema);
|
|
703
|
+
const facades = new WeakMap;
|
|
704
|
+
const handlers = {};
|
|
705
|
+
for (const [name, handler] of Object.entries(def.handlers ?? {})) {
|
|
706
|
+
handlers[name] = (event, ctx) => {
|
|
707
|
+
let facade = facades.get(ctx);
|
|
708
|
+
if (!facade) {
|
|
709
|
+
facade = deterministicContext(ctx, schema);
|
|
710
|
+
facades.set(ctx, facade);
|
|
711
|
+
}
|
|
712
|
+
return handler(event, facade);
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const loaded = { ...def, sources, schema, handlers };
|
|
716
|
+
realmDefinitions.add(loaded);
|
|
717
|
+
return loaded;
|
|
718
|
+
}
|
|
719
|
+
|
|
90
720
|
// src/runtime/reconstruct.ts
|
|
91
721
|
function isoToUnixSeconds(iso) {
|
|
92
722
|
if (!iso)
|
|
@@ -282,14 +912,39 @@ function sparseProbeTargets(subgraph) {
|
|
|
282
912
|
}
|
|
283
913
|
return targets;
|
|
284
914
|
}
|
|
285
|
-
|
|
915
|
+
var COVERAGE_TTL_MS = 60000;
|
|
916
|
+
function cachedCoverage(read, ttlMs = COVERAGE_TTL_MS) {
|
|
917
|
+
let cached;
|
|
918
|
+
const coverage = () => {
|
|
919
|
+
const now = Date.now();
|
|
920
|
+
if (!cached || now - cached.at > ttlMs) {
|
|
921
|
+
cached = { at: now, value: read().catch(() => null) };
|
|
922
|
+
}
|
|
923
|
+
return cached.value;
|
|
924
|
+
};
|
|
925
|
+
return Object.assign(coverage, {
|
|
926
|
+
forget: () => {
|
|
927
|
+
cached = undefined;
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
var dbStateWritesCoverage = cachedCoverage(async () => {
|
|
932
|
+
const row = await getSourceDb().selectFrom("state_writes").select("block_height").orderBy("block_height", "asc").limit(1).executeTakeFirst();
|
|
933
|
+
return row ? Number(row.block_height) : null;
|
|
934
|
+
});
|
|
286
935
|
class PostgresBlockSource {
|
|
936
|
+
feed;
|
|
937
|
+
constructor(feed) {
|
|
938
|
+
this.feed = feed;
|
|
939
|
+
}
|
|
287
940
|
async getTip() {
|
|
288
941
|
const progress = await getSourceDb().selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
|
|
289
942
|
return progress ? Number(progress.highest_seen_block) : 0;
|
|
290
943
|
}
|
|
291
944
|
loadBlockRange(fromHeight, toHeight) {
|
|
292
|
-
return loadBlockRange(getSourceDb(), fromHeight, toHeight
|
|
945
|
+
return loadBlockRange(getSourceDb(), fromHeight, toHeight, {
|
|
946
|
+
stateWrites: this.feed
|
|
947
|
+
});
|
|
293
948
|
}
|
|
294
949
|
}
|
|
295
950
|
var EVENT_FILTER_TO_INDEX_TYPE = {
|
|
@@ -375,15 +1030,18 @@ class PublicApiBlockSource {
|
|
|
375
1030
|
eventTypes;
|
|
376
1031
|
probeTargets;
|
|
377
1032
|
needsTransactions;
|
|
378
|
-
|
|
1033
|
+
feed;
|
|
1034
|
+
constructor(http, eventTypes, probeTargets, needsTransactions = true, feed) {
|
|
379
1035
|
this.http = http;
|
|
380
1036
|
this.eventTypes = eventTypes;
|
|
381
1037
|
this.probeTargets = probeTargets;
|
|
382
1038
|
this.needsTransactions = needsTransactions;
|
|
1039
|
+
this.feed = feed;
|
|
383
1040
|
}
|
|
384
1041
|
walkTargets() {
|
|
385
1042
|
const scoped = this.needsTransactions ? undefined : this.probeTargets;
|
|
386
|
-
|
|
1043
|
+
const types = this.feed ? this.eventTypes.filter((t) => !VM_INDEX_EVENT_TYPES.has(t)) : this.eventTypes;
|
|
1044
|
+
return types.flatMap((eventType) => {
|
|
387
1045
|
const own = scoped?.filter((t) => t.eventType === eventType);
|
|
388
1046
|
return own?.length ? own : [{ eventType }];
|
|
389
1047
|
});
|
|
@@ -406,10 +1064,11 @@ class PublicApiBlockSource {
|
|
|
406
1064
|
}
|
|
407
1065
|
async loadBlockRange(fromHeight, toHeight) {
|
|
408
1066
|
const withTx = !this.needsTransactions;
|
|
409
|
-
const [blocks, txRows, eventLists] = await Promise.all([
|
|
1067
|
+
const [blocks, txRows, eventLists, writes] = await Promise.all([
|
|
410
1068
|
this.http.walkBlocks(fromHeight, toHeight),
|
|
411
1069
|
this.needsTransactions ? this.http.walkTransactions(fromHeight, toHeight) : Promise.resolve([]),
|
|
412
|
-
Promise.all(this.walkTargets().map((t) => this.http.walkEvents(t.eventType, fromHeight, toHeight, withTx, t.contractId)))
|
|
1070
|
+
Promise.all(this.walkTargets().map((t) => this.http.walkEvents(t.eventType, fromHeight, toHeight, withTx, t.contractId))),
|
|
1071
|
+
this.feed ? this.walkWrites(fromHeight, toHeight, this.feed) : null
|
|
413
1072
|
]);
|
|
414
1073
|
const events = dedupeEvents(eventLists.flat());
|
|
415
1074
|
const map = new Map;
|
|
@@ -435,13 +1094,55 @@ class PublicApiBlockSource {
|
|
|
435
1094
|
bd.events.push(reconstructEvent(e));
|
|
436
1095
|
}
|
|
437
1096
|
}
|
|
1097
|
+
if (writes) {
|
|
1098
|
+
for (const [height, fed] of writes) {
|
|
1099
|
+
const bd = map.get(height);
|
|
1100
|
+
if (!bd)
|
|
1101
|
+
continue;
|
|
1102
|
+
bd.txs.push(...fed.txs);
|
|
1103
|
+
bd.vmEvents = fed.vmEvents;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
438
1106
|
for (const bd of map.values()) {
|
|
439
|
-
bd.txs.sort((a, b) => a.tx_index - b.tx_index);
|
|
1107
|
+
bd.txs.sort((a, b) => (a.tx_index ?? 0) - (b.tx_index ?? 0));
|
|
440
1108
|
bd.events.sort((a, b) => a.event_index - b.event_index);
|
|
441
1109
|
bd.vmEvents?.sort((a, b) => a.event_index - b.event_index);
|
|
442
1110
|
}
|
|
443
1111
|
return map;
|
|
444
1112
|
}
|
|
1113
|
+
async walkWrites(fromHeight, toHeight, feed) {
|
|
1114
|
+
const walks = await Promise.all((feed.contracts ?? [undefined]).map((c) => this.http.walkStateWrites(fromHeight, toHeight, c)));
|
|
1115
|
+
const writesByHeight = new Map;
|
|
1116
|
+
const txsByHeight = new Map;
|
|
1117
|
+
for (const row of walks.flat()) {
|
|
1118
|
+
const writes = writesByHeight.get(row.block_height) ?? new Map;
|
|
1119
|
+
writes.set(row.ordinal, row);
|
|
1120
|
+
writesByHeight.set(row.block_height, writes);
|
|
1121
|
+
const tx = writeTx(row);
|
|
1122
|
+
if (tx) {
|
|
1123
|
+
const txs = txsByHeight.get(row.block_height) ?? new Map;
|
|
1124
|
+
txs.set(row.tx_index, tx);
|
|
1125
|
+
txsByHeight.set(row.block_height, txs);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return stateWriteBlocks(new Map([...writesByHeight].map(([h, w]) => [
|
|
1129
|
+
h,
|
|
1130
|
+
[...w.values()].sort((a, b) => a.ordinal - b.ordinal)
|
|
1131
|
+
])), new Map([...txsByHeight].map(([h, t]) => [h, [...t.values()]])));
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function writeTx(row) {
|
|
1135
|
+
if (row.tx_index === null || !row.tx_id)
|
|
1136
|
+
return null;
|
|
1137
|
+
return {
|
|
1138
|
+
tx_id: row.tx_id,
|
|
1139
|
+
tx_index: row.tx_index,
|
|
1140
|
+
type: row.tx_type ?? "",
|
|
1141
|
+
sender: row.tx_sender ?? "",
|
|
1142
|
+
status: row.tx_status ?? "",
|
|
1143
|
+
contract_id: row.tx_contract_id ?? null,
|
|
1144
|
+
function_name: row.tx_function_name ?? null
|
|
1145
|
+
};
|
|
445
1146
|
}
|
|
446
1147
|
var postgresBlockSource = new PostgresBlockSource;
|
|
447
1148
|
function buildHttpClient() {
|
|
@@ -1097,7 +1798,7 @@ function __setReplayInFlightForTest(webhookId, inFlight) {
|
|
|
1097
1798
|
}
|
|
1098
1799
|
function replayDedupKey(subgraphName, tableName, row, replayId) {
|
|
1099
1800
|
const canonical = `replay:${replayId}:${subgraphName}:${tableName}:${stableStringify(row)}`;
|
|
1100
|
-
return
|
|
1801
|
+
return createHash2("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
1101
1802
|
}
|
|
1102
1803
|
function stableStringify(obj) {
|
|
1103
1804
|
const keys = Object.keys(obj).sort();
|
|
@@ -1108,7 +1809,7 @@ function stableStringify(obj) {
|
|
|
1108
1809
|
}
|
|
1109
1810
|
function deterministicReplayId(webhookId, fromBlock, toBlock, suffix) {
|
|
1110
1811
|
const canonical = `${webhookId}:${fromBlock}:${toBlock}${suffix ? `:${suffix}` : ""}`;
|
|
1111
|
-
return
|
|
1812
|
+
return createHash2("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
1112
1813
|
}
|
|
1113
1814
|
async function resolveSchemaName(db, subgraphName) {
|
|
1114
1815
|
const row = await db.selectFrom("subgraphs").select("schema_name").where("name", "=", subgraphName).executeTakeFirst();
|
|
@@ -1148,12 +1849,12 @@ async function replayWebhook(input) {
|
|
|
1148
1849
|
let enqueued = 0;
|
|
1149
1850
|
let lastId = 0n;
|
|
1150
1851
|
while (true) {
|
|
1151
|
-
const { rows } = await
|
|
1152
|
-
WHERE _block_height >= ${
|
|
1153
|
-
AND _block_height <= ${
|
|
1154
|
-
AND _id > ${
|
|
1852
|
+
const { rows } = await sql2`SELECT * FROM ${sql2.raw(`"${schema}"."${tableName}"`)}
|
|
1853
|
+
WHERE _block_height >= ${sql2.lit(input.fromBlock)}
|
|
1854
|
+
AND _block_height <= ${sql2.lit(input.toBlock)}
|
|
1855
|
+
AND _id > ${sql2.lit(lastId)}
|
|
1155
1856
|
ORDER BY _id ASC
|
|
1156
|
-
LIMIT ${
|
|
1857
|
+
LIMIT ${sql2.lit(BATCH_SIZE)}`.execute(db);
|
|
1157
1858
|
if (rows.length === 0)
|
|
1158
1859
|
break;
|
|
1159
1860
|
scanned += rows.length;
|
|
@@ -1237,5 +1938,5 @@ export {
|
|
|
1237
1938
|
replayWebhook
|
|
1238
1939
|
};
|
|
1239
1940
|
|
|
1240
|
-
//# debugId=
|
|
1941
|
+
//# debugId=AEC33CA4613E4B7264756E2164756E21
|
|
1241
1942
|
//# sourceMappingURL=replay.js.map
|