@secondlayer/subgraphs 6.3.6 → 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.
@@ -0,0 +1,3341 @@
1
+ // src/verify/replay.ts
2
+ import { createHash as createHash3 } from "node:crypto";
3
+ import { decodeRawTx } from "@secondlayer/shared/node/tx-summary";
4
+ import {
5
+ BlockVerifier,
6
+ MAINNET_CHECKPOINT,
7
+ hex
8
+ } from "@secondlayer/verify";
9
+
10
+ // src/runtime/runner.ts
11
+ import { getErrorMessage } from "@secondlayer/shared";
12
+ import { logger as logger2 } from "@secondlayer/shared/logger";
13
+ import {
14
+ clarityValueToJS,
15
+ deserializeCV as deserializeCV3,
16
+ toCamelCase
17
+ } from "@secondlayer/stacks/clarity";
18
+
19
+ // src/print-schema.ts
20
+ import {
21
+ deserializeCV
22
+ } from "@secondlayer/stacks/clarity";
23
+ function camelizeDataKey(str) {
24
+ return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
25
+ }
26
+ function camelizeKeys(obj) {
27
+ if (obj === null || obj === undefined)
28
+ return obj;
29
+ if (typeof obj !== "object")
30
+ return obj;
31
+ if (Array.isArray(obj))
32
+ return obj.map(camelizeKeys);
33
+ const result = {};
34
+ for (const [k, v] of Object.entries(obj)) {
35
+ result[camelizeDataKey(k)] = camelizeKeys(v);
36
+ }
37
+ return result;
38
+ }
39
+ function cvToTree(cv) {
40
+ switch (cv.type) {
41
+ case "uint":
42
+ case "int":
43
+ return { kind: cv.type };
44
+ case "true":
45
+ case "false":
46
+ return { kind: "bool" };
47
+ case "address":
48
+ case "contract":
49
+ return { kind: "principal" };
50
+ case "buffer":
51
+ return { kind: "buffer", len: cv.value.length / 2 };
52
+ case "ascii":
53
+ return { kind: "ascii", len: cv.value.length };
54
+ case "utf8":
55
+ return { kind: "utf8", len: new TextEncoder().encode(cv.value).length };
56
+ case "none":
57
+ return { kind: "optional", inner: null };
58
+ case "some":
59
+ return { kind: "optional", inner: cvToTree(cv.value) };
60
+ case "ok":
61
+ return { kind: "response", ok: cvToTree(cv.value), err: null };
62
+ case "err":
63
+ return { kind: "response", ok: null, err: cvToTree(cv.value) };
64
+ case "list": {
65
+ let inner = null;
66
+ for (const el of cv.value) {
67
+ const t = cvToTree(el);
68
+ inner = inner ? unify(inner, t) : t;
69
+ }
70
+ return { kind: "list", inner };
71
+ }
72
+ case "tuple": {
73
+ const entries = new Map;
74
+ for (const [k, v] of Object.entries(cv.value)) {
75
+ entries.set(k, { tree: cvToTree(v), present: 1 });
76
+ }
77
+ return { kind: "tuple", count: 1, entries };
78
+ }
79
+ }
80
+ }
81
+ function unifyNullable(a, b) {
82
+ if (!a)
83
+ return b;
84
+ if (!b)
85
+ return a;
86
+ return unify(a, b);
87
+ }
88
+ var UNION_KIND_ORDER = {
89
+ uint: 0,
90
+ int: 1,
91
+ bool: 2,
92
+ principal: 3,
93
+ buffer: 4,
94
+ ascii: 5,
95
+ utf8: 6,
96
+ list: 7,
97
+ tuple: 8,
98
+ response: 9
99
+ };
100
+ function makeUnion(members) {
101
+ const sorted = [...members].sort((a, b) => (UNION_KIND_ORDER[a.kind] ?? 99) - (UNION_KIND_ORDER[b.kind] ?? 99));
102
+ return { kind: "union", members: sorted };
103
+ }
104
+ function unify(a, b) {
105
+ if (a.kind === "optional" || b.kind === "optional") {
106
+ const ai = a.kind === "optional" ? a.inner : a;
107
+ const bi = b.kind === "optional" ? b.inner : b;
108
+ return { kind: "optional", inner: unifyNullable(ai, bi) };
109
+ }
110
+ if (a.kind === "union")
111
+ return unionAdd(a.members, b);
112
+ if (b.kind === "union")
113
+ return unionAdd(b.members, a);
114
+ if (a.kind !== b.kind)
115
+ return makeUnion([a, b]);
116
+ switch (a.kind) {
117
+ case "uint":
118
+ case "int":
119
+ case "bool":
120
+ case "principal":
121
+ return a;
122
+ case "buffer":
123
+ case "ascii":
124
+ case "utf8":
125
+ return { kind: a.kind, len: Math.max(a.len, b.len) };
126
+ case "list":
127
+ return {
128
+ kind: "list",
129
+ inner: unifyNullable(a.inner, b.inner)
130
+ };
131
+ case "response": {
132
+ const rb = b;
133
+ return {
134
+ kind: "response",
135
+ ok: unifyNullable(a.ok, rb.ok),
136
+ err: unifyNullable(a.err, rb.err)
137
+ };
138
+ }
139
+ case "tuple": {
140
+ const tb = b;
141
+ const entries = new Map([...a.entries].map(([k, e]) => [k, { ...e }]));
142
+ for (const [k, e] of tb.entries) {
143
+ const existing = entries.get(k);
144
+ entries.set(k, existing ? {
145
+ tree: unify(existing.tree, e.tree),
146
+ present: existing.present + e.present
147
+ } : { ...e });
148
+ }
149
+ return { kind: "tuple", count: a.count + tb.count, entries };
150
+ }
151
+ }
152
+ }
153
+ function unionAdd(members, t) {
154
+ if (t.kind === "union") {
155
+ let acc = { kind: "union", members };
156
+ for (const m of t.members)
157
+ acc = unify(acc, m);
158
+ return acc;
159
+ }
160
+ const next = [...members];
161
+ for (let i = 0;i < next.length; i++) {
162
+ const member = next[i];
163
+ if (!member)
164
+ continue;
165
+ const merged = unify(member, t);
166
+ if (merged.kind !== "union") {
167
+ next[i] = merged;
168
+ return makeUnion(next);
169
+ }
170
+ }
171
+ next.push(t);
172
+ return makeUnion(next);
173
+ }
174
+ function wrapOptional(t) {
175
+ return t.kind === "optional" ? t : { kind: "optional", inner: t };
176
+ }
177
+ function renderClarity(t) {
178
+ if (!t)
179
+ return "?";
180
+ switch (t.kind) {
181
+ case "uint":
182
+ case "int":
183
+ case "bool":
184
+ case "principal":
185
+ return t.kind;
186
+ case "buffer":
187
+ return `(buff ${t.len})`;
188
+ case "ascii":
189
+ return `(string-ascii ${t.len})`;
190
+ case "utf8":
191
+ return `(string-utf8 ${t.len})`;
192
+ case "optional":
193
+ return `(optional ${renderClarity(t.inner)})`;
194
+ case "list":
195
+ return `(list ${renderClarity(t.inner)})`;
196
+ case "response":
197
+ return `(response ${renderClarity(t.ok)} ${renderClarity(t.err)})`;
198
+ case "tuple": {
199
+ const parts = [...t.entries].map(([k, e]) => {
200
+ const tree = e.present < t.count ? wrapOptional(e.tree) : e.tree;
201
+ return `(${k} ${renderClarity(tree)})`;
202
+ });
203
+ return `(tuple ${parts.join(" ")})`;
204
+ }
205
+ case "union":
206
+ return t.members.map(renderClarity).join(" | ");
207
+ }
208
+ }
209
+ function renderTs(t) {
210
+ if (!t)
211
+ return "unknown";
212
+ switch (t.kind) {
213
+ case "uint":
214
+ case "int":
215
+ return "bigint";
216
+ case "bool":
217
+ return "boolean";
218
+ case "principal":
219
+ case "buffer":
220
+ case "ascii":
221
+ case "utf8":
222
+ return "string";
223
+ case "optional":
224
+ return t.inner ? `${renderTs(t.inner)} | null` : "unknown | null";
225
+ case "list": {
226
+ const inner = renderTs(t.inner);
227
+ return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
228
+ }
229
+ case "response": {
230
+ const sides = [...new Set([renderTs(t.ok), renderTs(t.err)])];
231
+ return sides.join(" | ");
232
+ }
233
+ case "tuple": {
234
+ const parts = [...t.entries].map(([k, e]) => {
235
+ const opt = e.present < t.count ? "?" : "";
236
+ return `${camelizeDataKey(k)}${opt}: ${renderTs(e.tree)}`;
237
+ });
238
+ return `{ ${parts.join("; ")} }`;
239
+ }
240
+ case "union":
241
+ return [...new Set(t.members.map((m) => renderTs(m)))].join(" | ");
242
+ }
243
+ }
244
+ function toColumnType(t) {
245
+ if (!t)
246
+ return "jsonb";
247
+ switch (t.kind) {
248
+ case "uint":
249
+ return "uint";
250
+ case "int":
251
+ return "int";
252
+ case "bool":
253
+ return "boolean";
254
+ case "principal":
255
+ return "principal";
256
+ case "buffer":
257
+ case "ascii":
258
+ case "utf8":
259
+ return "text";
260
+ case "list":
261
+ case "tuple":
262
+ return "jsonb";
263
+ case "optional":
264
+ return toColumnType(t.inner);
265
+ case "response":
266
+ return t.ok ? toColumnType(t.ok) : "jsonb";
267
+ case "union":
268
+ return "jsonb";
269
+ }
270
+ }
271
+ var MAX_DECODED_PER_TOPIC_NEWEST = 75;
272
+ var MAX_DECODED_PER_TOPIC_OLDEST = 25;
273
+ function inferPrintTopics(samples) {
274
+ const groups = new Map;
275
+ for (const s of samples) {
276
+ const group = groups.get(s.topic);
277
+ if (group)
278
+ group.push(s);
279
+ else
280
+ groups.set(s.topic, [s]);
281
+ }
282
+ const out = [];
283
+ for (const [topic, rows] of groups) {
284
+ let first = Number.POSITIVE_INFINITY;
285
+ let last = Number.NEGATIVE_INFINITY;
286
+ for (const r of rows) {
287
+ if (r.blockHeight < first)
288
+ first = r.blockHeight;
289
+ if (r.blockHeight > last)
290
+ last = r.blockHeight;
291
+ }
292
+ const withHex = [...rows].filter((r) => r.rawHex !== null).sort((a, b) => b.blockHeight - a.blockHeight);
293
+ const budget = MAX_DECODED_PER_TOPIC_NEWEST + MAX_DECODED_PER_TOPIC_OLDEST;
294
+ const picked = withHex.length <= budget ? withHex : [
295
+ ...withHex.slice(0, MAX_DECODED_PER_TOPIC_NEWEST),
296
+ ...withHex.slice(-MAX_DECODED_PER_TOPIC_OLDEST)
297
+ ];
298
+ const tuples = [];
299
+ let decoded = 0;
300
+ for (const p of picked) {
301
+ try {
302
+ const cv = deserializeCV(p.rawHex);
303
+ decoded++;
304
+ if (cv.type === "tuple")
305
+ tuples.push(cv);
306
+ } catch {}
307
+ }
308
+ const nonTuple = decoded > 0 && tuples.length === 0;
309
+ const fields = [];
310
+ if (!nonTuple) {
311
+ const stats = new Map;
312
+ for (const t of tuples) {
313
+ for (const [key, value] of Object.entries(t.value)) {
314
+ if (key === "topic")
315
+ continue;
316
+ const tree = cvToTree(value);
317
+ const existing = stats.get(key);
318
+ if (existing) {
319
+ existing.present++;
320
+ existing.tree = unify(existing.tree, tree);
321
+ if (value.type === "none")
322
+ existing.noneCount++;
323
+ if (value.type === "none" || value.type === "some") {
324
+ existing.optionalSeen = true;
325
+ }
326
+ } else {
327
+ stats.set(key, {
328
+ tree,
329
+ present: 1,
330
+ noneCount: value.type === "none" ? 1 : 0,
331
+ optionalSeen: value.type === "none" || value.type === "some"
332
+ });
333
+ }
334
+ }
335
+ }
336
+ for (const [name, st] of [...stats].sort(([a], [b]) => a.localeCompare(b))) {
337
+ const field = {
338
+ name,
339
+ camel_name: camelizeDataKey(name),
340
+ clarity_type: renderClarity(st.tree),
341
+ ts_type: renderTs(st.tree),
342
+ column_type: toColumnType(st.tree),
343
+ always_present: st.present === tuples.length
344
+ };
345
+ if (st.optionalSeen) {
346
+ field.optional_some_rate = (st.present - st.noneCount) / st.present;
347
+ }
348
+ fields.push(field);
349
+ }
350
+ }
351
+ out.push({
352
+ topic,
353
+ count: rows.length,
354
+ first_height: first,
355
+ last_height: last,
356
+ non_tuple: nonTuple,
357
+ fields
358
+ });
359
+ }
360
+ return out.sort((a, b) => b.count - a.count);
361
+ }
362
+
363
+ // src/runtime/clarity.ts
364
+ import { cvToValue, deserializeCV as deserializeCV2 } from "@secondlayer/stacks/clarity";
365
+ function decodeClarityValue(hex) {
366
+ try {
367
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
368
+ const cv = deserializeCV2(cleanHex);
369
+ return cvToValue(cv);
370
+ } catch {
371
+ return hex;
372
+ }
373
+ }
374
+ function decodeEventData(data) {
375
+ if (typeof data === "string" && data.startsWith("0x") && data.length > 10) {
376
+ return decodeClarityValue(data);
377
+ }
378
+ if (Array.isArray(data)) {
379
+ return data.map(decodeEventData);
380
+ }
381
+ if (typeof data === "object" && data !== null) {
382
+ const decoded = {};
383
+ for (const [key, value] of Object.entries(data)) {
384
+ decoded[key] = decodeEventData(value);
385
+ }
386
+ return decoded;
387
+ }
388
+ return data;
389
+ }
390
+
391
+ // src/runtime/print-validate.ts
392
+ var COLUMN_TYPES = new Set([
393
+ "text",
394
+ "uint",
395
+ "int",
396
+ "principal",
397
+ "boolean",
398
+ "timestamp",
399
+ "jsonb"
400
+ ]);
401
+ function isColumnType(field) {
402
+ return typeof field === "string" && COLUMN_TYPES.has(field);
403
+ }
404
+ function matchesField(field, value) {
405
+ if (isColumnType(field)) {
406
+ switch (field) {
407
+ case "uint":
408
+ case "int":
409
+ return typeof value === "bigint" || typeof value === "number";
410
+ case "boolean":
411
+ return typeof value === "boolean";
412
+ case "text":
413
+ case "principal":
414
+ case "timestamp":
415
+ return typeof value === "string" || typeof value === "number" || typeof value === "bigint";
416
+ case "jsonb":
417
+ return value !== undefined;
418
+ }
419
+ }
420
+ if (typeof field !== "object" || field === null)
421
+ return false;
422
+ if ("optional" in field) {
423
+ return value === undefined || value === null || matchesField(field.type, value);
424
+ }
425
+ if ("tuple" in field) {
426
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
427
+ return false;
428
+ }
429
+ return matchesTopic(field.tuple, value).ok;
430
+ }
431
+ if ("list" in field) {
432
+ return Array.isArray(value) && value.every((item) => matchesField(field.list, item));
433
+ }
434
+ return false;
435
+ }
436
+ function matchesTopic(declared, data) {
437
+ for (const [name, field] of Object.entries(declared)) {
438
+ const present = name in data;
439
+ const optional = typeof field === "object" && field !== null && "optional" in field;
440
+ if (!present) {
441
+ if (optional)
442
+ continue;
443
+ return { ok: false, reason: `missing declared field "${name}"` };
444
+ }
445
+ if (!matchesField(field, data[name])) {
446
+ return {
447
+ ok: false,
448
+ reason: `field "${name}" does not match its declared type (got ${describe(data[name])})`
449
+ };
450
+ }
451
+ }
452
+ return { ok: true };
453
+ }
454
+ function describe(value) {
455
+ if (value === null)
456
+ return "null";
457
+ if (Array.isArray(value))
458
+ return "array";
459
+ return typeof value;
460
+ }
461
+ function validatePrintPayload(prints, topic, data) {
462
+ if (!prints)
463
+ return { ok: true };
464
+ const declared = prints[topic];
465
+ if (!declared)
466
+ return { ok: true };
467
+ return matchesTopic(declared, data);
468
+ }
469
+
470
+ // src/runtime/realm.ts
471
+ import vm from "node:vm";
472
+
473
+ // src/verification.ts
474
+ import { createHash } from "node:crypto";
475
+ // package.json
476
+ var package_default = {
477
+ name: "@secondlayer/subgraphs",
478
+ version: "6.5.0",
479
+ description: "Describe the Stacks data you want as one TypeScript file; it fills itself and survives reorgs.",
480
+ license: "MIT",
481
+ repository: {
482
+ type: "git",
483
+ url: "git+https://github.com/ryanwaits/secondlayer.git",
484
+ directory: "packages/subgraphs"
485
+ },
486
+ homepage: "https://github.com/ryanwaits/secondlayer/tree/main/packages/subgraphs#readme",
487
+ type: "module",
488
+ main: "./dist/index.js",
489
+ types: "./dist/index.d.ts",
490
+ exports: {
491
+ ".": {
492
+ types: "./dist/index.d.ts",
493
+ import: "./dist/index.js"
494
+ },
495
+ "./types": {
496
+ types: "./dist/types.d.ts",
497
+ import: "./dist/types.js"
498
+ },
499
+ "./schema": {
500
+ types: "./dist/schema/index.d.ts",
501
+ import: "./dist/schema/index.js"
502
+ },
503
+ "./validate": {
504
+ types: "./dist/validate.d.ts",
505
+ import: "./dist/validate.js"
506
+ },
507
+ "./verification": {
508
+ types: "./dist/verification.d.ts",
509
+ import: "./dist/verification.js"
510
+ },
511
+ "./runtime/replay": {
512
+ types: "./dist/runtime/replay.d.ts",
513
+ import: "./dist/runtime/replay.js"
514
+ },
515
+ "./runtime/emitter": {
516
+ types: "./dist/runtime/emitter.d.ts",
517
+ import: "./dist/runtime/emitter.js"
518
+ },
519
+ "./testing": {
520
+ types: "./dist/testing/index.d.ts",
521
+ import: "./dist/testing/index.js"
522
+ },
523
+ "./verify": {
524
+ types: "./dist/verify/index.d.ts",
525
+ import: "./dist/verify/index.js"
526
+ }
527
+ },
528
+ files: ["dist"],
529
+ scripts: {
530
+ build: "bunup",
531
+ dev: "bunup --watch",
532
+ test: "bun test",
533
+ typecheck: "tsc --noEmit",
534
+ prepublishOnly: "bun run build"
535
+ },
536
+ dependencies: {
537
+ "@secondlayer/shared": "^11.23.0",
538
+ "@secondlayer/stacks": "^6.2.0",
539
+ "@secondlayer/verify": "^0.3.0",
540
+ esbuild: "^0.28.2",
541
+ kysely: "0.28.17",
542
+ zod: "^4.3.6"
543
+ },
544
+ devDependencies: {
545
+ "@types/bun": "latest",
546
+ bunup: "0.16.32",
547
+ typescript: "^5"
548
+ },
549
+ publishConfig: {
550
+ access: "public"
551
+ }
552
+ };
553
+
554
+ // src/verification.ts
555
+ var SUBGRAPHS_RUNTIME = `@secondlayer/subgraphs@${package_default.version}`;
556
+ var FORBIDDEN_GLOBALS = {
557
+ Date: "wall-clock time differs per run",
558
+ performance: "timers differ per run",
559
+ process: "host environment differs per run",
560
+ fetch: "network reads are not chain inputs",
561
+ crypto: "randomness differs per run",
562
+ setTimeout: "scheduling order differs per run",
563
+ setInterval: "scheduling order differs per run",
564
+ setImmediate: "scheduling order differs per run",
565
+ clearTimeout: "scheduling order differs per run",
566
+ clearInterval: "scheduling order differs per run",
567
+ queueMicrotask: "scheduling order differs per run",
568
+ WeakRef: "garbage collection timing differs per run",
569
+ FinalizationRegistry: "garbage collection timing differs per run",
570
+ Atomics: "shared memory is not single-threaded",
571
+ SharedArrayBuffer: "shared memory is not single-threaded",
572
+ Intl: "locale and time zone data differ per host",
573
+ Temporal: "locale and time zone data differ per host",
574
+ eval: "generated code hides inputs from the scan",
575
+ WebAssembly: "compiled code hides inputs from the scan",
576
+ ShadowRealm: "a nested realm escapes the allow-list",
577
+ require: "imports must be bundled into the handler"
578
+ };
579
+ var ALLOWED_GLOBALS = [
580
+ "undefined",
581
+ "NaN",
582
+ "Infinity",
583
+ "globalThis",
584
+ "isNaN",
585
+ "isFinite",
586
+ "parseInt",
587
+ "parseFloat",
588
+ "decodeURI",
589
+ "decodeURIComponent",
590
+ "encodeURI",
591
+ "encodeURIComponent",
592
+ "escape",
593
+ "unescape",
594
+ "Object",
595
+ "Function",
596
+ "Array",
597
+ "Number",
598
+ "Boolean",
599
+ "String",
600
+ "Symbol",
601
+ "BigInt",
602
+ "Math",
603
+ "JSON",
604
+ "Reflect",
605
+ "Proxy",
606
+ "Promise",
607
+ "Map",
608
+ "Set",
609
+ "WeakMap",
610
+ "WeakSet",
611
+ "RegExp",
612
+ "Iterator",
613
+ "DisposableStack",
614
+ "AsyncDisposableStack",
615
+ "Error",
616
+ "AggregateError",
617
+ "SuppressedError",
618
+ "EvalError",
619
+ "RangeError",
620
+ "ReferenceError",
621
+ "SyntaxError",
622
+ "TypeError",
623
+ "URIError",
624
+ "ArrayBuffer",
625
+ "DataView",
626
+ "Int8Array",
627
+ "Uint8Array",
628
+ "Uint8ClampedArray",
629
+ "Int16Array",
630
+ "Uint16Array",
631
+ "Int32Array",
632
+ "Uint32Array",
633
+ "Float16Array",
634
+ "Float32Array",
635
+ "Float64Array",
636
+ "BigInt64Array",
637
+ "BigUint64Array",
638
+ "TextEncoder",
639
+ "TextDecoder",
640
+ "atob",
641
+ "btoa",
642
+ "console"
643
+ ];
644
+ var ALLOWED_MATH = [
645
+ "abs",
646
+ "min",
647
+ "max",
648
+ "floor",
649
+ "ceil",
650
+ "trunc",
651
+ "sign",
652
+ "round",
653
+ "E",
654
+ "LN10",
655
+ "LN2",
656
+ "LOG10E",
657
+ "LOG2E",
658
+ "PI",
659
+ "SQRT1_2",
660
+ "SQRT2"
661
+ ];
662
+ var LOCALE_METHODS = [
663
+ "toLocaleString",
664
+ "toLocaleDateString",
665
+ "toLocaleTimeString",
666
+ "toLocaleUpperCase",
667
+ "toLocaleLowerCase",
668
+ "localeCompare"
669
+ ];
670
+ function formatFinding(f) {
671
+ return `${f.file}:${f.line}:${f.column} ${f.name}: ${f.reason}`;
672
+ }
673
+ var STATE_SOURCE_TYPES = new Set([
674
+ "var_set",
675
+ "map_set",
676
+ "map_delete"
677
+ ]);
678
+ var LEVEL_RANK = { state: 0, events: 1, none: 2 };
679
+ var MAX_LISTED_FINDINGS = 10;
680
+ function deriveVerification(def, findings = []) {
681
+ let level = "state";
682
+ const raise = (to) => {
683
+ if (LEVEL_RANK[to] > LEVEL_RANK[level])
684
+ level = to;
685
+ };
686
+ const reasons = [];
687
+ for (const [name, source] of Object.entries(def.sources ?? {})) {
688
+ const filter = source;
689
+ if (filter.trait) {
690
+ raise("none");
691
+ reasons.push(`source "${name}": trait scope needs a proven contract registry`);
692
+ } else if (filter.type === "map_insert") {
693
+ raise("events");
694
+ reasons.push(`map_insert source "${name}": map_insert vs map_set needs event proofs`);
695
+ } else if (!STATE_SOURCE_TYPES.has(filter.type)) {
696
+ raise("events");
697
+ reasons.push(`${filter.type} source "${name}" needs event proofs`);
698
+ }
699
+ }
700
+ if (def.backfillMode === "concurrent") {
701
+ raise("none");
702
+ reasons.push("backfillMode concurrent: tip-first order is not chain order");
703
+ }
704
+ if (findings.some((f) => f.kind === "needs-events"))
705
+ raise("events");
706
+ for (const f of findings.slice(0, MAX_LISTED_FINDINGS)) {
707
+ reasons.push(f.kind === "needs-events" ? `${formatFinding(f)} (needs event proofs)` : formatFinding(f));
708
+ }
709
+ if (findings.length > MAX_LISTED_FINDINGS) {
710
+ reasons.push(`…and ${findings.length - MAX_LISTED_FINDINGS} more handler findings`);
711
+ }
712
+ return {
713
+ level,
714
+ reasons,
715
+ unproven: reasons.length === 0 ? [
716
+ "tx attribution of writes: event.tx, _tx_id (needs event proofs)",
717
+ "intermediate writes within a block (needs event proofs)"
718
+ ] : []
719
+ };
720
+ }
721
+ function isDeterminismViolation(v) {
722
+ return v.level === "state" && v.reasons.length > 0;
723
+ }
724
+ function sha256(input) {
725
+ return createHash("sha256").update(input).digest("hex");
726
+ }
727
+ function pinPreimage(input) {
728
+ const fields = {
729
+ handlerHash: sha256(input.handlerCode),
730
+ network: input.network,
731
+ runtime: input.runtime ?? SUBGRAPHS_RUNTIME,
732
+ schemaHash: input.schemaHash,
733
+ startBlock: input.startBlock ?? null
734
+ };
735
+ return JSON.stringify(fields);
736
+ }
737
+ function computePin(input) {
738
+ return sha256(pinPreimage(input));
739
+ }
740
+
741
+ // src/runtime/realm.ts
742
+ class NondeterminismError extends Error {
743
+ name = "NondeterminismError";
744
+ }
745
+
746
+ class HandlerReadError extends Error {
747
+ name = "HandlerReadError";
748
+ }
749
+ function abortsBlock(err) {
750
+ return err instanceof NondeterminismError || err instanceof HandlerReadError;
751
+ }
752
+ var HOST_GLOBALS = [
753
+ "ArrayBuffer",
754
+ "DataView",
755
+ "Int8Array",
756
+ "Uint8Array",
757
+ "Uint8ClampedArray",
758
+ "Int16Array",
759
+ "Uint16Array",
760
+ "Int32Array",
761
+ "Uint32Array",
762
+ "Float16Array",
763
+ "Float32Array",
764
+ "Float64Array",
765
+ "BigInt64Array",
766
+ "BigUint64Array",
767
+ "TextEncoder",
768
+ "TextDecoder",
769
+ "atob",
770
+ "btoa"
771
+ ];
772
+ function forbid(name, reason) {
773
+ return () => {
774
+ throw new NondeterminismError(`${name}: ${reason}`);
775
+ };
776
+ }
777
+ function createRealm() {
778
+ const host = globalThis;
779
+ const sandbox = Object.create(null);
780
+ for (const name of HOST_GLOBALS) {
781
+ if (host[name] !== undefined)
782
+ sandbox[name] = host[name];
783
+ }
784
+ const context = vm.createContext(sandbox, {
785
+ name: "subgraph-handler",
786
+ codeGeneration: { strings: false, wasm: false }
787
+ });
788
+ const g = vm.runInContext("globalThis", context);
789
+ const allowed = new Set(ALLOWED_GLOBALS);
790
+ for (const name of Object.getOwnPropertyNames(g)) {
791
+ if (!allowed.has(name))
792
+ delete g[name];
793
+ }
794
+ for (const [name, reason] of Object.entries(FORBIDDEN_GLOBALS)) {
795
+ Object.defineProperty(g, name, { get: forbid(name, reason) });
796
+ }
797
+ const realmMath = g.Math;
798
+ const safeMath = {};
799
+ const allowedMath = new Set(ALLOWED_MATH);
800
+ for (const key of Object.getOwnPropertyNames(realmMath)) {
801
+ if (allowedMath.has(key))
802
+ safeMath[key] = realmMath[key];
803
+ else
804
+ Object.defineProperty(safeMath, key, {
805
+ get: forbid(`Math.${key}`, key === "random" ? "randomness differs per run" : "result is engine-approximated")
806
+ });
807
+ }
808
+ Object.defineProperty(g, "Math", { value: Object.freeze(safeMath) });
809
+ for (const ctor of ["String", "Number", "BigInt", "Array", "Object"]) {
810
+ const proto = g[ctor].prototype;
811
+ for (const method of LOCALE_METHODS) {
812
+ if (Object.prototype.hasOwnProperty.call(proto, method)) {
813
+ Object.defineProperty(proto, method, {
814
+ value: forbid(method, "output depends on the host's locale data")
815
+ });
816
+ }
817
+ }
818
+ }
819
+ Object.preventExtensions(g);
820
+ return context;
821
+ }
822
+ function checkRowValues(where, values) {
823
+ for (const [col, value] of Object.entries(values)) {
824
+ checkValue(value, `${where}.${col}`);
825
+ }
826
+ }
827
+ function checkValue(value, path) {
828
+ switch (typeof value) {
829
+ case "number":
830
+ if (!Number.isSafeInteger(value)) {
831
+ throw new TypeError(`${path}: ${value} is not a safe integer; verifiable subgraphs store bigint or integers only`);
832
+ }
833
+ return;
834
+ case "function":
835
+ case "symbol":
836
+ throw new TypeError(`${path}: a ${typeof value} is not a row value`);
837
+ case "object":
838
+ if (value === null || ArrayBuffer.isView(value))
839
+ return;
840
+ if (Array.isArray(value)) {
841
+ value.forEach((v, i) => checkValue(v, `${path}[${i}]`));
842
+ return;
843
+ }
844
+ for (const [k, v] of Object.entries(value))
845
+ checkValue(v, `${path}.${k}`);
846
+ return;
847
+ default:
848
+ return;
849
+ }
850
+ }
851
+ function deterministicContext(ctx, schema) {
852
+ let pendingReads = 0;
853
+ const knownTable = (table) => {
854
+ if (!Object.prototype.hasOwnProperty.call(schema, table)) {
855
+ throw new Error(`Table "${table}" not found in subgraph schema`);
856
+ }
857
+ };
858
+ const guard = (op) => {
859
+ if (pendingReads > 0) {
860
+ 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`);
861
+ }
862
+ };
863
+ const read = async (op, run) => {
864
+ guard(op);
865
+ pendingReads++;
866
+ try {
867
+ return await run();
868
+ } catch (err) {
869
+ if (err instanceof NondeterminismError)
870
+ throw err;
871
+ throw new HandlerReadError(`ctx.${op} failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
872
+ } finally {
873
+ pendingReads--;
874
+ }
875
+ };
876
+ return {
877
+ get block() {
878
+ return ctx.block;
879
+ },
880
+ get tx() {
881
+ return ctx.tx;
882
+ },
883
+ get client() {
884
+ throw new NondeterminismError("ctx.client: contract reads need re-execution proofs");
885
+ },
886
+ insert(table, row) {
887
+ guard("insert");
888
+ checkRowValues(`insert ${table}`, row);
889
+ ctx.insert(table, row);
890
+ },
891
+ update(table, where, set) {
892
+ guard("update");
893
+ checkRowValues(`update ${table}`, where);
894
+ checkRowValues(`update ${table}`, set);
895
+ ctx.update(table, where, set);
896
+ },
897
+ upsert(table, key, row) {
898
+ guard("upsert");
899
+ checkRowValues(`upsert ${table}`, key);
900
+ checkRowValues(`upsert ${table}`, row);
901
+ ctx.upsert(table, key, row);
902
+ },
903
+ delete(table, where) {
904
+ guard("delete");
905
+ ctx.delete(table, where);
906
+ },
907
+ increment(table, key, deltas) {
908
+ guard("increment");
909
+ checkRowValues(`increment ${table}`, key);
910
+ checkRowValues(`increment ${table}`, deltas);
911
+ ctx.increment(table, key, deltas);
912
+ },
913
+ findOne: (table, where) => {
914
+ knownTable(table);
915
+ return read("findOne", () => ctx.findOne(table, where));
916
+ },
917
+ findMany: (table, ...rest) => {
918
+ knownTable(table);
919
+ return read("findMany", () => ctx.findMany(table, ...rest));
920
+ }
921
+ };
922
+ }
923
+ var realmDefinitions = new WeakSet;
924
+ async function loadDeterministicDefinition(handlerCode) {
925
+ const { transform } = await import("esbuild");
926
+ const { code } = await transform(handlerCode, {
927
+ format: "cjs",
928
+ loader: "js",
929
+ logLevel: "silent"
930
+ });
931
+ const context = createRealm();
932
+ const evaluate = vm.runInContext(`(function (module, exports) {
933
+ ${code}
934
+ })`, context, { filename: "handler.js" });
935
+ const mod = { exports: {} };
936
+ evaluate(mod, mod.exports);
937
+ const def = mod.exports.default ?? mod.exports;
938
+ const sources = structuredClone(def.sources);
939
+ const schema = structuredClone(def.schema);
940
+ const facades = new WeakMap;
941
+ const handlers = {};
942
+ for (const [name, handler] of Object.entries(def.handlers ?? {})) {
943
+ handlers[name] = (event, ctx) => {
944
+ let facade = facades.get(ctx);
945
+ if (!facade) {
946
+ facade = deterministicContext(ctx, schema);
947
+ facades.set(ctx, facade);
948
+ }
949
+ return handler(event, facade);
950
+ };
951
+ }
952
+ const loaded = { ...def, sources, schema, handlers };
953
+ realmDefinitions.add(loaded);
954
+ return loaded;
955
+ }
956
+
957
+ // src/runtime/source-matcher.ts
958
+ var patternCache = new Map;
959
+ function matchAny(value, pattern) {
960
+ return Array.isArray(pattern) ? pattern.some((p) => matchPattern(value, p)) : matchPattern(value, pattern);
961
+ }
962
+ function matchPattern(value, pattern) {
963
+ if (!pattern.includes("*"))
964
+ return value === pattern;
965
+ let re = patternCache.get(pattern);
966
+ if (!re) {
967
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
968
+ re = new RegExp(`^${regex}$`);
969
+ patternCache.set(pattern, re);
970
+ }
971
+ return re.test(value);
972
+ }
973
+ var EMPTY_SET = new Set;
974
+ function factoryAllows(filter, contractId, factoryContracts) {
975
+ if (!filter.factory)
976
+ return true;
977
+ if (!contractId)
978
+ return false;
979
+ return (factoryContracts.get(filter.factory.from) ?? EMPTY_SET).has(contractId);
980
+ }
981
+ function traitAllows(filter, contractId, traitContracts) {
982
+ const trait = filter.trait;
983
+ if (!trait)
984
+ return true;
985
+ if (!contractId)
986
+ return false;
987
+ return (traitContracts.get(trait) ?? EMPTY_SET).has(contractId);
988
+ }
989
+ function assetContract(assetId) {
990
+ return assetId?.split("::")[0];
991
+ }
992
+ function printContractId(data) {
993
+ if (!data)
994
+ return;
995
+ const id = (typeof data.contract_identifier === "string" ? data.contract_identifier : undefined) ?? (typeof data.contract_id === "string" ? data.contract_id : undefined);
996
+ return id || undefined;
997
+ }
998
+ function matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts = new Map, vmEventsByTx = new Map) {
999
+ const results = [];
1000
+ switch (filter.type) {
1001
+ case "stx_transfer":
1002
+ case "stx_mint":
1003
+ case "stx_burn":
1004
+ case "stx_lock": {
1005
+ const eventType = `${filter.type}_event`;
1006
+ for (const tx of transactions) {
1007
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1008
+ const matched = txEvents.filter((e) => e.type === eventType);
1009
+ if (matched.length === 0)
1010
+ continue;
1011
+ const filtered = matched.filter((e) => {
1012
+ const data = e.data;
1013
+ if (!data)
1014
+ return false;
1015
+ if ("sender" in filter && filter.sender) {
1016
+ if (!matchPattern(data.sender, filter.sender))
1017
+ return false;
1018
+ }
1019
+ if ("recipient" in filter && filter.recipient) {
1020
+ if (!matchPattern(data.recipient, filter.recipient))
1021
+ return false;
1022
+ }
1023
+ if ("lockedAddress" in filter && filter.lockedAddress) {
1024
+ if (!matchPattern(data.locked_address, filter.lockedAddress))
1025
+ return false;
1026
+ }
1027
+ if ("minAmount" in filter && filter.minAmount !== undefined) {
1028
+ const amount = BigInt(data.amount ?? data.locked_amount ?? "0");
1029
+ if (amount < filter.minAmount)
1030
+ return false;
1031
+ }
1032
+ if ("maxAmount" in filter && filter.maxAmount !== undefined) {
1033
+ const amount = BigInt(data.amount ?? "0");
1034
+ if (amount > filter.maxAmount)
1035
+ return false;
1036
+ }
1037
+ return true;
1038
+ });
1039
+ if (filtered.length > 0) {
1040
+ results.push({ tx, events: filtered });
1041
+ }
1042
+ }
1043
+ break;
1044
+ }
1045
+ case "ft_transfer":
1046
+ case "ft_mint":
1047
+ case "ft_burn": {
1048
+ const eventType = `${filter.type}_event`;
1049
+ for (const tx of transactions) {
1050
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1051
+ const matched = txEvents.filter((e) => {
1052
+ if (e.type !== eventType)
1053
+ return false;
1054
+ const data = e.data;
1055
+ if (!data)
1056
+ return false;
1057
+ if (filter.assetIdentifier) {
1058
+ const assetId = data.asset_identifier;
1059
+ if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
1060
+ return false;
1061
+ }
1062
+ if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
1063
+ return false;
1064
+ if ("sender" in filter && filter.sender) {
1065
+ if (!matchPattern(data.sender, filter.sender))
1066
+ return false;
1067
+ }
1068
+ if ("recipient" in filter && filter.recipient) {
1069
+ if (!matchPattern(data.recipient, filter.recipient))
1070
+ return false;
1071
+ }
1072
+ if (filter.minAmount !== undefined) {
1073
+ const amount = BigInt(data.amount ?? "0");
1074
+ if (amount < filter.minAmount)
1075
+ return false;
1076
+ }
1077
+ return true;
1078
+ });
1079
+ if (matched.length > 0) {
1080
+ results.push({ tx, events: matched });
1081
+ }
1082
+ }
1083
+ break;
1084
+ }
1085
+ case "nft_transfer":
1086
+ case "nft_mint":
1087
+ case "nft_burn": {
1088
+ const eventType = `${filter.type}_event`;
1089
+ for (const tx of transactions) {
1090
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1091
+ const matched = txEvents.filter((e) => {
1092
+ if (e.type !== eventType)
1093
+ return false;
1094
+ const data = e.data;
1095
+ if (!data)
1096
+ return false;
1097
+ if (filter.assetIdentifier) {
1098
+ const assetId = data.asset_identifier;
1099
+ if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
1100
+ return false;
1101
+ }
1102
+ if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
1103
+ return false;
1104
+ if ("sender" in filter && filter.sender) {
1105
+ if (!matchPattern(data.sender, filter.sender))
1106
+ return false;
1107
+ }
1108
+ if ("recipient" in filter && filter.recipient) {
1109
+ if (!matchPattern(data.recipient, filter.recipient))
1110
+ return false;
1111
+ }
1112
+ return true;
1113
+ });
1114
+ if (matched.length > 0) {
1115
+ results.push({ tx, events: matched });
1116
+ }
1117
+ }
1118
+ break;
1119
+ }
1120
+ case "contract_call": {
1121
+ for (const tx of transactions) {
1122
+ if (tx.type !== "contract_call")
1123
+ continue;
1124
+ if (filter.contractId) {
1125
+ if (!tx.contract_id || !matchAny(tx.contract_id, filter.contractId))
1126
+ continue;
1127
+ }
1128
+ if (!factoryAllows(filter, tx.contract_id, factoryContracts))
1129
+ continue;
1130
+ if (filter.functionName) {
1131
+ if (!tx.function_name || !matchPattern(tx.function_name, filter.functionName))
1132
+ continue;
1133
+ }
1134
+ if (filter.caller) {
1135
+ if (!matchPattern(tx.sender, filter.caller))
1136
+ continue;
1137
+ }
1138
+ if (!traitAllows(filter, tx.contract_id, traitContracts))
1139
+ continue;
1140
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1141
+ results.push({ tx, events: txEvents });
1142
+ }
1143
+ break;
1144
+ }
1145
+ case "contract_deploy": {
1146
+ for (const tx of transactions) {
1147
+ if (tx.type !== "smart_contract")
1148
+ continue;
1149
+ if (filter.deployer) {
1150
+ if (!matchPattern(tx.sender, filter.deployer))
1151
+ continue;
1152
+ }
1153
+ if (filter.contractName) {
1154
+ const name = tx.contract_id?.split(".")[1] ?? "";
1155
+ if (!matchPattern(name, filter.contractName))
1156
+ continue;
1157
+ }
1158
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1159
+ results.push({ tx, events: txEvents });
1160
+ }
1161
+ break;
1162
+ }
1163
+ case "nested_contract_call":
1164
+ case "var_set":
1165
+ case "map_set":
1166
+ case "map_insert":
1167
+ case "map_delete": {
1168
+ for (const tx of transactions) {
1169
+ const txEvents = vmEventsByTx.get(tx.tx_id) ?? [];
1170
+ const matched = txEvents.filter((e) => {
1171
+ if (e.type !== filter.type)
1172
+ return false;
1173
+ const data = e.data;
1174
+ if (!data)
1175
+ return false;
1176
+ const contractId = printContractId(data);
1177
+ if ("contractId" in filter && filter.contractId) {
1178
+ if (!contractId || !matchAny(contractId, filter.contractId))
1179
+ return false;
1180
+ }
1181
+ if (!traitAllows(filter, contractId, traitContracts))
1182
+ return false;
1183
+ if (!factoryAllows(filter, contractId, factoryContracts))
1184
+ return false;
1185
+ if (filter.type === "nested_contract_call") {
1186
+ if (filter.functionName) {
1187
+ if (typeof data.function_name !== "string" || !matchPattern(data.function_name, filter.functionName))
1188
+ return false;
1189
+ }
1190
+ if (filter.caller) {
1191
+ if (typeof data.caller !== "string" || !matchPattern(data.caller, filter.caller))
1192
+ return false;
1193
+ }
1194
+ if (filter.sender) {
1195
+ if (typeof data.sender !== "string" || !matchPattern(data.sender, filter.sender))
1196
+ return false;
1197
+ }
1198
+ }
1199
+ if (filter.type === "var_set" && filter.varName) {
1200
+ if (typeof data.var_name !== "string" || !matchPattern(data.var_name, filter.varName))
1201
+ return false;
1202
+ }
1203
+ if ((filter.type === "map_set" || filter.type === "map_insert" || filter.type === "map_delete") && filter.map) {
1204
+ if (typeof data.map_name !== "string" || !matchPattern(data.map_name, filter.map))
1205
+ return false;
1206
+ }
1207
+ return true;
1208
+ });
1209
+ if (matched.length > 0)
1210
+ results.push({ tx, events: matched });
1211
+ }
1212
+ break;
1213
+ }
1214
+ case "print_event": {
1215
+ for (const tx of transactions) {
1216
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1217
+ const matched = txEvents.filter((e) => {
1218
+ if (e.type !== "smart_contract_event" && e.type !== "contract_event")
1219
+ return false;
1220
+ const data = e.data;
1221
+ if (!data)
1222
+ return false;
1223
+ if (data.topic !== "print")
1224
+ return false;
1225
+ const contractId = printContractId(data);
1226
+ if (filter.contractId) {
1227
+ if (!contractId || !matchAny(contractId, filter.contractId))
1228
+ return false;
1229
+ }
1230
+ if (!traitAllows(filter, contractId, traitContracts))
1231
+ return false;
1232
+ if (!factoryAllows(filter, contractId, factoryContracts))
1233
+ return false;
1234
+ return true;
1235
+ });
1236
+ if (matched.length > 0) {
1237
+ results.push({ tx, events: matched });
1238
+ }
1239
+ }
1240
+ break;
1241
+ }
1242
+ }
1243
+ return results;
1244
+ }
1245
+ function matchSources(sources, transactions, events, traitContracts = new Map, factoryContracts = new Map, vmEvents = []) {
1246
+ const eventsByTx = new Map;
1247
+ for (const event of events) {
1248
+ const list = eventsByTx.get(event.tx_id) ?? [];
1249
+ list.push(event);
1250
+ eventsByTx.set(event.tx_id, list);
1251
+ }
1252
+ const vmEventsByTx = new Map;
1253
+ for (const event of vmEvents) {
1254
+ const list = vmEventsByTx.get(event.tx_id) ?? [];
1255
+ list.push(event);
1256
+ vmEventsByTx.set(event.tx_id, list);
1257
+ }
1258
+ const seen = new Set;
1259
+ const results = [];
1260
+ for (const [sourceName, filter] of Object.entries(sources)) {
1261
+ const matches = matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts, vmEventsByTx);
1262
+ for (const match of matches) {
1263
+ const dedupeKey = `${match.tx.tx_id}:${sourceName}`;
1264
+ if (!seen.has(dedupeKey)) {
1265
+ seen.add(dedupeKey);
1266
+ results.push({ ...match, sourceName });
1267
+ }
1268
+ }
1269
+ }
1270
+ return results;
1271
+ }
1272
+
1273
+ // src/runtime/violations.ts
1274
+ import { getTargetDb } from "@secondlayer/shared/db";
1275
+ import { logger } from "@secondlayer/shared/logger";
1276
+ import { sql } from "kysely";
1277
+ var VIOLATIONS_CAP_PER_SUBGRAPH = 100;
1278
+ function jsonSafe(value) {
1279
+ if (typeof value === "bigint")
1280
+ return value.toString();
1281
+ if (value === null || value === undefined)
1282
+ return value;
1283
+ if (Array.isArray(value))
1284
+ return value.map(jsonSafe);
1285
+ if (typeof value === "object") {
1286
+ const out = {};
1287
+ for (const [k, v] of Object.entries(value)) {
1288
+ out[k] = jsonSafe(v);
1289
+ }
1290
+ return out;
1291
+ }
1292
+ return value;
1293
+ }
1294
+ async function defaultRecordPrintViolation(input) {
1295
+ try {
1296
+ const db = getTargetDb();
1297
+ const sample = jsonSafe(input.samplePayload);
1298
+ await db.insertInto("subgraph_violations").values({
1299
+ subgraph_name: input.subgraphName,
1300
+ source_name: input.sourceName,
1301
+ block_height: input.blockHeight,
1302
+ tx_id: input.txId,
1303
+ reason: input.reason,
1304
+ sample_payload: sample
1305
+ }).execute();
1306
+ await sql`
1307
+ DELETE FROM subgraph_violations
1308
+ WHERE id IN (
1309
+ SELECT id FROM subgraph_violations
1310
+ WHERE subgraph_name = ${input.subgraphName}
1311
+ ORDER BY seen_at DESC
1312
+ OFFSET ${VIOLATIONS_CAP_PER_SUBGRAPH}
1313
+ )
1314
+ `.execute(db);
1315
+ } catch (err) {
1316
+ logger.warn("Failed to record print violation", {
1317
+ subgraph: input.subgraphName,
1318
+ error: err instanceof Error ? err.message : String(err)
1319
+ });
1320
+ }
1321
+ }
1322
+ var recorder = defaultRecordPrintViolation;
1323
+ async function recordPrintViolation(input) {
1324
+ await recorder(input);
1325
+ }
1326
+
1327
+ // src/runtime/runner.ts
1328
+ var DEFAULT_ERROR_THRESHOLD = 50;
1329
+ function decodeFunctionArgs(args) {
1330
+ let parsed = args;
1331
+ if (typeof parsed === "string") {
1332
+ try {
1333
+ parsed = JSON.parse(parsed);
1334
+ } catch {
1335
+ return [];
1336
+ }
1337
+ }
1338
+ if (!Array.isArray(parsed))
1339
+ return [];
1340
+ return parsed.map((arg) => {
1341
+ if (typeof arg === "string")
1342
+ return decodeClarityValue(arg);
1343
+ return arg;
1344
+ });
1345
+ }
1346
+ function decodeRawResult(raw) {
1347
+ if (typeof raw === "string" && raw.length > 2) {
1348
+ return decodeClarityValue(raw);
1349
+ }
1350
+ return null;
1351
+ }
1352
+ function decodeRawHex(hex) {
1353
+ if (hex.startsWith("0x") && hex.length > 2) {
1354
+ return decodeClarityValue(hex);
1355
+ }
1356
+ return hex;
1357
+ }
1358
+ function safeBigInt(val) {
1359
+ if (typeof val === "bigint")
1360
+ return val;
1361
+ if (typeof val === "number")
1362
+ return BigInt(val);
1363
+ if (typeof val === "string") {
1364
+ try {
1365
+ return BigInt(val);
1366
+ } catch {
1367
+ return 0n;
1368
+ }
1369
+ }
1370
+ return 0n;
1371
+ }
1372
+ function buildContractCallInput(filter, tx) {
1373
+ const abi = filter.abi;
1374
+ const fnName = tx.function_name;
1375
+ if (!abi || !fnName)
1376
+ return;
1377
+ const fn = abi.functions?.find((f) => f.name === fnName);
1378
+ if (!fn || !Array.isArray(fn.args))
1379
+ return;
1380
+ let rawArgs = tx.function_args;
1381
+ if (typeof rawArgs === "string") {
1382
+ try {
1383
+ rawArgs = JSON.parse(rawArgs);
1384
+ } catch {
1385
+ return;
1386
+ }
1387
+ }
1388
+ if (!Array.isArray(rawArgs))
1389
+ return;
1390
+ const decodeArg = clarityValueToJS;
1391
+ const input = {};
1392
+ fn.args.forEach((arg, i) => {
1393
+ const hex = rawArgs[i];
1394
+ if (typeof hex !== "string")
1395
+ return;
1396
+ try {
1397
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
1398
+ input[toCamelCase(arg.name)] = decodeArg(arg.type, deserializeCV3(clean));
1399
+ } catch {}
1400
+ });
1401
+ return input;
1402
+ }
1403
+ function buildEventPayload(filter, tx, event) {
1404
+ const txMeta = {
1405
+ txId: tx.tx_id,
1406
+ sender: tx.sender,
1407
+ type: tx.type,
1408
+ status: tx.status,
1409
+ contractId: tx.contract_id ?? null,
1410
+ functionName: tx.function_name ?? null
1411
+ };
1412
+ const decodedArgs = decodeFunctionArgs(tx.function_args);
1413
+ const decodedResult = decodeRawResult(tx.raw_result);
1414
+ if (!event) {
1415
+ switch (filter.type) {
1416
+ case "contract_call": {
1417
+ const input = buildContractCallInput(filter, tx);
1418
+ return {
1419
+ type: "contract_call",
1420
+ contractId: tx.contract_id ?? "",
1421
+ functionName: tx.function_name ?? "",
1422
+ sender: tx.sender,
1423
+ args: decodedArgs,
1424
+ ...input !== undefined ? { input } : {},
1425
+ result: decodedResult,
1426
+ resultHex: tx.raw_result ?? null,
1427
+ tx: txMeta
1428
+ };
1429
+ }
1430
+ case "contract_deploy":
1431
+ return {
1432
+ contractId: tx.contract_id ?? "",
1433
+ deployer: tx.sender,
1434
+ tx: txMeta
1435
+ };
1436
+ default:
1437
+ return { tx: txMeta };
1438
+ }
1439
+ }
1440
+ const decoded = decodeEventData(event.data);
1441
+ switch (filter.type) {
1442
+ case "ft_transfer":
1443
+ return {
1444
+ sender: decoded.sender,
1445
+ recipient: decoded.recipient,
1446
+ amount: safeBigInt(decoded.amount),
1447
+ assetIdentifier: decoded.asset_identifier,
1448
+ tx: txMeta
1449
+ };
1450
+ case "ft_mint":
1451
+ return {
1452
+ recipient: decoded.recipient,
1453
+ amount: safeBigInt(decoded.amount),
1454
+ assetIdentifier: decoded.asset_identifier,
1455
+ tx: txMeta
1456
+ };
1457
+ case "ft_burn":
1458
+ return {
1459
+ sender: decoded.sender,
1460
+ amount: safeBigInt(decoded.amount),
1461
+ assetIdentifier: decoded.asset_identifier,
1462
+ tx: txMeta
1463
+ };
1464
+ case "nft_transfer":
1465
+ return {
1466
+ sender: decoded.sender,
1467
+ recipient: decoded.recipient,
1468
+ tokenId: decoded.raw_value ?? decoded.value,
1469
+ assetIdentifier: decoded.asset_identifier,
1470
+ tx: txMeta
1471
+ };
1472
+ case "nft_mint":
1473
+ return {
1474
+ recipient: decoded.recipient,
1475
+ tokenId: decoded.raw_value ?? decoded.value,
1476
+ assetIdentifier: decoded.asset_identifier,
1477
+ tx: txMeta
1478
+ };
1479
+ case "nft_burn":
1480
+ return {
1481
+ sender: decoded.sender,
1482
+ tokenId: decoded.raw_value ?? decoded.value,
1483
+ assetIdentifier: decoded.asset_identifier,
1484
+ tx: txMeta
1485
+ };
1486
+ case "stx_transfer":
1487
+ return {
1488
+ sender: decoded.sender,
1489
+ recipient: decoded.recipient,
1490
+ amount: safeBigInt(decoded.amount),
1491
+ memo: decoded.memo ?? "",
1492
+ tx: txMeta
1493
+ };
1494
+ case "stx_mint":
1495
+ return {
1496
+ recipient: decoded.recipient,
1497
+ amount: safeBigInt(decoded.amount),
1498
+ tx: txMeta
1499
+ };
1500
+ case "stx_burn":
1501
+ return {
1502
+ sender: decoded.sender,
1503
+ amount: safeBigInt(decoded.amount),
1504
+ tx: txMeta
1505
+ };
1506
+ case "stx_lock":
1507
+ return {
1508
+ lockedAddress: decoded.locked_address,
1509
+ lockedAmount: safeBigInt(decoded.locked_amount),
1510
+ unlockHeight: safeBigInt(decoded.unlock_height),
1511
+ tx: txMeta
1512
+ };
1513
+ case "print_event": {
1514
+ const rawHex = event.data?.raw_value;
1515
+ const rawValue = typeof rawHex === "string" && rawHex.startsWith("0x") ? decodeClarityValue(rawHex) : decoded.value;
1516
+ const clarityObj = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : null;
1517
+ const topic = clarityObj?.topic ? String(clarityObj.topic) : decoded.topic ?? "";
1518
+ const { topic: _, ...rest } = clarityObj ?? {};
1519
+ const data = Object.keys(rest).length > 0 ? camelizeKeys(rest) : rawValue && typeof rawValue !== "object" ? rawValue : {};
1520
+ return {
1521
+ contractId: printContractId(decoded) ?? tx.contract_id ?? "",
1522
+ topic,
1523
+ data: data ?? {},
1524
+ tx: txMeta
1525
+ };
1526
+ }
1527
+ case "contract_call": {
1528
+ const ccRawHex = event.data?.raw_value;
1529
+ const normalized = typeof ccRawHex === "string" && ccRawHex.startsWith("0x") ? { ...decoded, value: decodeClarityValue(ccRawHex) } : decoded;
1530
+ const input = buildContractCallInput(filter, tx);
1531
+ return {
1532
+ ...normalized,
1533
+ type: "contract_call",
1534
+ _eventType: event.type,
1535
+ contractId: tx.contract_id ?? "",
1536
+ functionName: tx.function_name ?? "",
1537
+ sender: tx.sender,
1538
+ args: decodedArgs,
1539
+ ...input !== undefined ? { input } : {},
1540
+ result: decodedResult,
1541
+ resultHex: tx.raw_result ?? null,
1542
+ tx: txMeta
1543
+ };
1544
+ }
1545
+ case "contract_deploy":
1546
+ return {
1547
+ contractId: tx.contract_id ?? "",
1548
+ deployer: tx.sender,
1549
+ tx: txMeta
1550
+ };
1551
+ case "nested_contract_call": {
1552
+ const data = event.data ?? {};
1553
+ const argsHex = Array.isArray(data.function_args) ? data.function_args.filter((a) => typeof a === "string") : [];
1554
+ return {
1555
+ contractId: printContractId(data) ?? "",
1556
+ sender: typeof data.sender === "string" ? data.sender : null,
1557
+ caller: typeof data.caller === "string" ? data.caller : "",
1558
+ functionName: typeof data.function_name === "string" ? data.function_name : "",
1559
+ args: decodeFunctionArgs(argsHex),
1560
+ arguments: argsHex,
1561
+ rawResult: typeof data.raw_result === "string" ? data.raw_result : "",
1562
+ result: decodeRawResult(data.raw_result),
1563
+ tx: txMeta
1564
+ };
1565
+ }
1566
+ case "var_set": {
1567
+ const data = event.data ?? {};
1568
+ const rawValue = typeof data.raw_value === "string" ? data.raw_value : "";
1569
+ return {
1570
+ contractId: printContractId(data) ?? "",
1571
+ varName: typeof data.var_name === "string" ? data.var_name : "",
1572
+ rawValue,
1573
+ value: decodeRawHex(rawValue),
1574
+ tx: txMeta
1575
+ };
1576
+ }
1577
+ case "map_set":
1578
+ case "map_insert": {
1579
+ const data = event.data ?? {};
1580
+ const rawKey = typeof data.raw_key === "string" ? data.raw_key : "";
1581
+ const rawValue = typeof data.raw_value === "string" ? data.raw_value : "";
1582
+ return {
1583
+ contractId: printContractId(data) ?? "",
1584
+ map: typeof data.map_name === "string" ? data.map_name : "",
1585
+ rawKey,
1586
+ key: decodeRawHex(rawKey),
1587
+ rawValue,
1588
+ value: decodeRawHex(rawValue),
1589
+ tx: txMeta
1590
+ };
1591
+ }
1592
+ case "map_delete": {
1593
+ const data = event.data ?? {};
1594
+ const rawKey = typeof data.raw_key === "string" ? data.raw_key : "";
1595
+ return {
1596
+ contractId: printContractId(data) ?? "",
1597
+ map: typeof data.map_name === "string" ? data.map_name : "",
1598
+ rawKey,
1599
+ key: decodeRawHex(rawKey),
1600
+ tx: txMeta
1601
+ };
1602
+ }
1603
+ default:
1604
+ return {
1605
+ ...decoded,
1606
+ _eventType: event.type,
1607
+ tx: txMeta
1608
+ };
1609
+ }
1610
+ }
1611
+ async function runHandlers(subgraph, matched, ctx, opts) {
1612
+ let processed = 0;
1613
+ let errors = 0;
1614
+ let skipped = 0;
1615
+ let delivered = 0;
1616
+ const threshold = opts?.errorThreshold ?? DEFAULT_ERROR_THRESHOLD;
1617
+ const filterLookup = new Map;
1618
+ if (!Array.isArray(subgraph.sources)) {
1619
+ for (const [name, filter] of Object.entries(subgraph.sources)) {
1620
+ filterLookup.set(name, filter);
1621
+ }
1622
+ }
1623
+ const units = [];
1624
+ for (const { tx, events, sourceName } of matched) {
1625
+ if (events.length === 0) {
1626
+ units.push({ tx, sourceName, event: null });
1627
+ } else {
1628
+ for (const event of events)
1629
+ units.push({ tx, sourceName, event });
1630
+ }
1631
+ }
1632
+ const clockRank = (e) => e?.clock === "vm" ? 1 : 0;
1633
+ units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || clockRank(a.event) - clockRank(b.event) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
1634
+ for (const { tx, event, sourceName } of units) {
1635
+ if (errors >= threshold) {
1636
+ logger2.error("Subgraph error threshold reached, skipping remaining events", {
1637
+ subgraph: subgraph.name,
1638
+ errors,
1639
+ threshold
1640
+ });
1641
+ return {
1642
+ processed,
1643
+ errors,
1644
+ delivered,
1645
+ ...skipped > 0 ? { skipped } : {}
1646
+ };
1647
+ }
1648
+ const filter = filterLookup.get(sourceName);
1649
+ const handler = subgraph.handlers[sourceName] ?? null;
1650
+ if (!handler) {
1651
+ logger2.warn("No handler found for source", {
1652
+ subgraph: subgraph.name,
1653
+ sourceName,
1654
+ txId: tx.tx_id
1655
+ });
1656
+ continue;
1657
+ }
1658
+ ctx.setTx({
1659
+ txId: tx.tx_id,
1660
+ sender: tx.sender,
1661
+ type: tx.type,
1662
+ status: tx.status,
1663
+ contractId: tx.contract_id ?? null,
1664
+ functionName: tx.function_name ?? null
1665
+ });
1666
+ const checkpoint = ctx.opsCheckpoint();
1667
+ try {
1668
+ let payload;
1669
+ if (event === null) {
1670
+ payload = filter ? buildEventPayload(filter, tx, null) : {
1671
+ tx: {
1672
+ txId: tx.tx_id,
1673
+ sender: tx.sender,
1674
+ type: tx.type,
1675
+ status: tx.status,
1676
+ contractId: tx.contract_id,
1677
+ functionName: tx.function_name
1678
+ }
1679
+ };
1680
+ } else if (filter) {
1681
+ payload = buildEventPayload(filter, tx, event);
1682
+ } else {
1683
+ const decoded = decodeEventData(event.data);
1684
+ payload = {
1685
+ ...decoded,
1686
+ _eventId: event.id,
1687
+ _eventType: event.type,
1688
+ _eventIndex: event.event_index,
1689
+ tx: {
1690
+ txId: tx.tx_id,
1691
+ sender: tx.sender,
1692
+ type: tx.type,
1693
+ status: tx.status,
1694
+ contractId: tx.contract_id,
1695
+ functionName: tx.function_name
1696
+ }
1697
+ };
1698
+ }
1699
+ if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
1700
+ continue;
1701
+ }
1702
+ delivered++;
1703
+ if (event !== null && filter?.type === "print_event" && filter.prints) {
1704
+ const printPayload = payload;
1705
+ const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
1706
+ if (!verdict.ok) {
1707
+ skipped++;
1708
+ logger2.warn("Print payload does not match declared prints schema", {
1709
+ subgraph: subgraph.name,
1710
+ sourceName,
1711
+ topic: printPayload.topic,
1712
+ txId: tx.tx_id,
1713
+ reason: verdict.reason
1714
+ });
1715
+ recordPrintViolation({
1716
+ subgraphName: subgraph.name,
1717
+ sourceName,
1718
+ blockHeight: ctx.block.height,
1719
+ txId: tx.tx_id,
1720
+ reason: verdict.reason,
1721
+ samplePayload: {
1722
+ topic: printPayload.topic ?? "",
1723
+ data: printPayload.data ?? {}
1724
+ }
1725
+ });
1726
+ continue;
1727
+ }
1728
+ }
1729
+ await handler(payload, ctx);
1730
+ processed++;
1731
+ } catch (err) {
1732
+ if (abortsBlock(err))
1733
+ throw err;
1734
+ ctx.rollbackTo(checkpoint);
1735
+ errors++;
1736
+ logger2.error("Subgraph handler error", {
1737
+ subgraph: subgraph.name,
1738
+ sourceName,
1739
+ txId: tx.tx_id,
1740
+ ...event !== null ? { eventId: event.id, eventType: event.type } : {},
1741
+ error: getErrorMessage(err)
1742
+ });
1743
+ }
1744
+ }
1745
+ return { processed, errors, delivered, ...skipped > 0 ? { skipped } : {} };
1746
+ }
1747
+
1748
+ // src/runtime/apply-block.ts
1749
+ function matchBlock(def, data, scopes = {}) {
1750
+ return matchSources(def.sources, data.txs, data.events, scopes.trait ?? new Map, scopes.factory ?? new Map, data.vmEvents ?? []);
1751
+ }
1752
+ async function applyBlock(def, data, ctx, scopes = {}) {
1753
+ const matched = matchBlock(def, data, scopes);
1754
+ if (matched.length === 0) {
1755
+ return { matched: 0, processed: 0, errors: 0, delivered: 0 };
1756
+ }
1757
+ return { matched: matched.length, ...await runHandlers(def, matched, ctx) };
1758
+ }
1759
+
1760
+ // src/runtime/memory-store.ts
1761
+ import { normalizeAbi as normalizeAbi2, toCamelCase as toCamelCase3 } from "@secondlayer/stacks/clarity";
1762
+
1763
+ // src/runtime/context.ts
1764
+ import { logger as logger4 } from "@secondlayer/shared/logger";
1765
+ import { sql as sql2 } from "kysely";
1766
+
1767
+ // src/schema/generator.ts
1768
+ import { createHash as createHash2 } from "node:crypto";
1769
+
1770
+ // src/schema/utils.ts
1771
+ import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
1772
+ function quotePgIdent(name) {
1773
+ if (/^[a-z_][a-z0-9_]*$/i.test(name))
1774
+ return name;
1775
+ return `"${name.replace(/"/g, '""')}"`;
1776
+ }
1777
+ function snakeToCamel(name) {
1778
+ return name.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
1779
+ }
1780
+ function pascalCase(name) {
1781
+ const camel = snakeToCamel(name);
1782
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
1783
+ }
1784
+
1785
+ // src/schema/generator.ts
1786
+ var TYPE_MAP = {
1787
+ text: "TEXT",
1788
+ uint: "NUMERIC",
1789
+ int: "NUMERIC",
1790
+ principal: "TEXT",
1791
+ boolean: "BOOLEAN",
1792
+ timestamp: "TIMESTAMPTZ",
1793
+ jsonb: "JSONB"
1794
+ };
1795
+ var MAX_IDENT_BYTES = 63;
1796
+ function safeIndexName(name) {
1797
+ if (name.length <= MAX_IDENT_BYTES)
1798
+ return name;
1799
+ const hash = createHash2("sha256").update(name).digest("hex").slice(0, 8);
1800
+ return `${name.slice(0, MAX_IDENT_BYTES - hash.length - 1)}_${hash}`;
1801
+ }
1802
+ function emitIndexedColumnIndexDDL(schemaName, tableName, colName, opts) {
1803
+ const concurrently = opts?.concurrently ? " CONCURRENTLY" : "";
1804
+ const qualified = `${quotePgIdent(schemaName)}.${quotePgIdent(tableName)}`;
1805
+ const plain = `idx_${schemaName}_${tableName}_${colName}`;
1806
+ const composite = safeIndexName(`idx_${schemaName}_${tableName}_${colName}_id`);
1807
+ const col = quotePgIdent(colName);
1808
+ const id = quotePgIdent("_id");
1809
+ return {
1810
+ create: [
1811
+ `CREATE INDEX${concurrently} IF NOT EXISTS ${quotePgIdent(plain)} ON ${qualified} (${col})`,
1812
+ `CREATE INDEX${concurrently} IF NOT EXISTS ${quotePgIdent(composite)} ON ${qualified} (${col}, ${id})`
1813
+ ],
1814
+ drop: [
1815
+ `DROP INDEX${concurrently} IF EXISTS ${quotePgIdent(schemaName)}.${quotePgIdent(plain)}`,
1816
+ `DROP INDEX${concurrently} IF EXISTS ${quotePgIdent(schemaName)}.${quotePgIdent(composite)}`
1817
+ ]
1818
+ };
1819
+ }
1820
+ function escapeLiteralDefault(value) {
1821
+ if (value === null || value === undefined)
1822
+ return "NULL";
1823
+ if (typeof value === "number" || typeof value === "bigint")
1824
+ return String(value);
1825
+ if (typeof value === "boolean")
1826
+ return value ? "TRUE" : "FALSE";
1827
+ return `'${String(value).replace(/'/g, "''")}'`;
1828
+ }
1829
+ function tableNeedsTrgm(tableDef) {
1830
+ return Object.values(tableDef.columns).some((col) => col.search);
1831
+ }
1832
+ function emitTableDDL(schemaName, tableName, tableDef) {
1833
+ const qualifiedName = `${quotePgIdent(schemaName)}.${quotePgIdent(tableName)}`;
1834
+ const statements = [];
1835
+ const columnDefs = [
1836
+ "_id BIGSERIAL PRIMARY KEY",
1837
+ "_block_height BIGINT NOT NULL",
1838
+ "_tx_id TEXT NOT NULL",
1839
+ "_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
1840
+ ];
1841
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
1842
+ const sqlType = TYPE_MAP[col.type];
1843
+ const nullable = col.nullable ? "" : " NOT NULL";
1844
+ let colDef = `${quotePgIdent(colName)} ${sqlType}${nullable}`;
1845
+ if (col.default !== undefined) {
1846
+ colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
1847
+ }
1848
+ if (col.type === "uint") {
1849
+ colDef += ` CHECK (${quotePgIdent(colName)} >= 0)`;
1850
+ }
1851
+ columnDefs.push(colDef);
1852
+ }
1853
+ statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
1854
+ ${columnDefs.join(`,
1855
+ `)}
1856
+ )`);
1857
+ statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_block_height`)} ON ${qualifiedName} (_block_height)`);
1858
+ statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_tx_id`)} ON ${qualifiedName} (_tx_id)`);
1859
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
1860
+ if (col.indexed) {
1861
+ statements.push(...emitIndexedColumnIndexDDL(schemaName, tableName, colName).create);
1862
+ }
1863
+ }
1864
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
1865
+ if (col.search) {
1866
+ statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_${colName}_trgm`)} ON ${qualifiedName} USING gin (${quotePgIdent(colName)} gin_trgm_ops)`);
1867
+ }
1868
+ }
1869
+ if (tableDef.indexes) {
1870
+ for (let i = 0;i < tableDef.indexes.length; i++) {
1871
+ const cols = tableDef.indexes[i];
1872
+ const idxName = quotePgIdent(`idx_${schemaName}_${tableName}_composite_${i}`);
1873
+ statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.map(quotePgIdent).join(", ")})`);
1874
+ }
1875
+ }
1876
+ if (tableDef.uniqueKeys) {
1877
+ for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
1878
+ const cols = tableDef.uniqueKeys[i];
1879
+ const constraintName = quotePgIdent(`uq_${schemaName}_${tableName}_${cols.join("_")}`);
1880
+ statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.map(quotePgIdent).join(", ")})`);
1881
+ }
1882
+ }
1883
+ return statements;
1884
+ }
1885
+ function emitJournalDDL(schemaName) {
1886
+ return [
1887
+ `CREATE TABLE IF NOT EXISTS ${schemaName}._journal (
1888
+ _jid BIGSERIAL PRIMARY KEY,
1889
+ block_height BIGINT NOT NULL,
1890
+ table_name TEXT NOT NULL,
1891
+ row_key JSONB NOT NULL,
1892
+ prev_row JSONB,
1893
+ _created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1894
+ )`,
1895
+ `CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
1896
+ ];
1897
+ }
1898
+ function emitFactoryDDL(schemaName) {
1899
+ return [
1900
+ `CREATE TABLE IF NOT EXISTS ${schemaName}._factory_addresses (
1901
+ source_name TEXT NOT NULL,
1902
+ address TEXT NOT NULL,
1903
+ block_height BIGINT NOT NULL,
1904
+ _created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1905
+ PRIMARY KEY (source_name, address)
1906
+ )`,
1907
+ `CREATE INDEX IF NOT EXISTS idx_${schemaName}_factory_height ON ${schemaName}._factory_addresses (block_height)`
1908
+ ];
1909
+ }
1910
+ function generateSubgraphSQL(def, schemaNameOverride) {
1911
+ const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
1912
+ const statements = [];
1913
+ const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
1914
+ if (needsTrgm) {
1915
+ statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
1916
+ }
1917
+ statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
1918
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
1919
+ statements.push(...emitTableDDL(schemaName, tableName, tableDef));
1920
+ }
1921
+ statements.push(...emitJournalDDL(schemaName));
1922
+ if (Object.values(def.sources ?? {}).some((src) => src.factory !== undefined)) {
1923
+ statements.push(...emitFactoryDDL(schemaName));
1924
+ }
1925
+ const hashInput = JSON.stringify({
1926
+ name: def.name,
1927
+ schema: def.schema,
1928
+ sources: def.sources
1929
+ }, (_key, value) => typeof value === "bigint" ? value.toString() : value);
1930
+ const hash = createHash2("sha256").update(hashInput).digest("hex");
1931
+ return { statements, hash };
1932
+ }
1933
+
1934
+ // src/runtime/chain-read.ts
1935
+ import { getSourceDb } from "@secondlayer/shared/db";
1936
+ import { logger as logger3 } from "@secondlayer/shared/logger";
1937
+ import { http, createPublicClient } from "@secondlayer/stacks";
1938
+ import {
1939
+ ContractResponseError,
1940
+ buildFunctionArgs,
1941
+ isResponseOutput,
1942
+ readContract
1943
+ } from "@secondlayer/stacks/actions";
1944
+ import {
1945
+ clarityValueToJSUntyped,
1946
+ deserializeCV as deserializeCV4,
1947
+ normalizeAbi,
1948
+ serializeCV,
1949
+ toCamelCase as toCamelCase2
1950
+ } from "@secondlayer/stacks/clarity";
1951
+ function readContractAt(ctx, contractId, abi, options) {
1952
+ return ctx.client.contract(contractId, abi, options);
1953
+ }
1954
+
1955
+ class ChainReadError extends Error {
1956
+ name = "ChainReadError";
1957
+ }
1958
+ function nodeClient(rpcUrl) {
1959
+ return createPublicClient({
1960
+ transport: http(rpcUrl, { retryCount: 0, timeout: 1e4 })
1961
+ });
1962
+ }
1963
+
1964
+ class ConcurrencyGate {
1965
+ limit;
1966
+ active = 0;
1967
+ queue = [];
1968
+ constructor(limit) {
1969
+ this.limit = limit;
1970
+ }
1971
+ async run(fn) {
1972
+ if (this.active >= this.limit) {
1973
+ await new Promise((resolve) => this.queue.push(resolve));
1974
+ }
1975
+ this.active++;
1976
+ try {
1977
+ return await fn();
1978
+ } finally {
1979
+ this.active--;
1980
+ this.queue.shift()?.();
1981
+ }
1982
+ }
1983
+ }
1984
+ var DEFAULT_MAX_CONCURRENT_READS = 4;
1985
+ function maxConcurrentReads() {
1986
+ const raw = process.env.SUBGRAPH_CHAIN_READ_CONCURRENCY;
1987
+ const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
1988
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT_READS;
1989
+ }
1990
+ var gate = new ConcurrencyGate(maxConcurrentReads());
1991
+ function hashArgs(args) {
1992
+ const serialized = args.map((arg) => serializeCV(arg)).join(",");
1993
+ return Bun.hash(serialized).toString(16);
1994
+ }
1995
+ function findFunction(abi, name) {
1996
+ const fn = abi.functions.find((f) => f.name === name);
1997
+ if (!fn) {
1998
+ throw new ChainReadError(`No function "${name}" in the supplied ABI.`);
1999
+ }
2000
+ if (fn.access !== "read-only") {
2001
+ throw new ChainReadError(`"${name}" is ${fn.access}, not read-only — a handler cannot call it.`);
2002
+ }
2003
+ return fn;
2004
+ }
2005
+ function createChainReadClient(ctx) {
2006
+ const readValue = async (contractId, abi, functionName, args, options) => {
2007
+ const rpcUrl = ctx.rpcUrl ?? process.env.STACKS_NODE_RPC_URL;
2008
+ if (!rpcUrl) {
2009
+ throw new ChainReadError("ctx.client needs a Stacks node: set STACKS_NODE_RPC_URL on the subgraph processor.");
2010
+ }
2011
+ const constant = options.cache === "contract-constant";
2012
+ if (!constant && !ctx.indexBlockHash) {
2013
+ throw new ChainReadError(`Block ${ctx.blockHeight} has no index_block_hash, so a read cannot be pinned to it. Reading at the node's tip would make this handler non-deterministic (the same reindex would produce different rows). Re-ingest the block, or pass cache: "contract-constant" if the value genuinely cannot change.`);
2014
+ }
2015
+ const fn = findFunction(abi, functionName);
2016
+ const clarityArgs = buildFunctionArgs(fn, args);
2017
+ const db = ctx.db ?? getSourceDb();
2018
+ const argsHash = hashArgs(clarityArgs);
2019
+ const pinnedTo = constant ? null : ctx.indexBlockHash;
2020
+ const cached = await db.selectFrom("chain_read_cache").select("result_hex").where("contract_id", "=", contractId).where("function_name", "=", fn.name).where("args_hash", "=", argsHash).where((eb) => pinnedTo === null ? eb("index_block_hash", "is", null) : eb("index_block_hash", "=", pinnedTo)).executeTakeFirst();
2021
+ const resultHex = cached?.result_hex ?? await gate.run(async () => {
2022
+ if (!ctx.indexBlockHash) {
2023
+ logger3.warn("Chain read is not pinned to a block (no index_block_hash)", {
2024
+ contractId,
2025
+ functionName: fn.name,
2026
+ blockHeight: ctx.blockHeight,
2027
+ note: 'cache: "contract-constant" asserts this value cannot change; the node answered at its own tip'
2028
+ });
2029
+ }
2030
+ const value = await readContract(nodeClient(rpcUrl), {
2031
+ contract: contractId,
2032
+ functionName: fn.name,
2033
+ args: clarityArgs,
2034
+ tip: ctx.indexBlockHash ?? undefined
2035
+ });
2036
+ return serializeCV(value);
2037
+ });
2038
+ if (!cached) {
2039
+ await db.insertInto("chain_read_cache").values({
2040
+ contract_id: contractId,
2041
+ function_name: fn.name,
2042
+ args_hash: argsHash,
2043
+ index_block_hash: pinnedTo,
2044
+ block_height: constant ? null : ctx.blockHeight,
2045
+ result_hex: resultHex
2046
+ }).onConflict((oc) => oc.doNothing()).execute();
2047
+ }
2048
+ const value = deserializeCV4(resultHex);
2049
+ const js = clarityValueToJSUntyped(fn.outputs, value);
2050
+ if (isResponseOutput(fn.outputs)) {
2051
+ const response = js;
2052
+ if ("ok" in response)
2053
+ return response.ok;
2054
+ throw new ContractResponseError(`${contractId}::${fn.name} returned an err response.`, response.err);
2055
+ }
2056
+ return js;
2057
+ };
2058
+ return {
2059
+ contract(contractId, rawAbi, options = {}) {
2060
+ const abi = normalizeAbi(rawAbi);
2061
+ const camelToKebab = new Map;
2062
+ for (const fn of abi.functions) {
2063
+ camelToKebab.set(toCamelCase2(fn.name), fn.name);
2064
+ }
2065
+ const read = new Proxy({}, {
2066
+ get(_target, prop) {
2067
+ const fnName = camelToKebab.get(prop) ?? prop;
2068
+ return (args = {}) => readValue(contractId, abi, fnName, args, options);
2069
+ }
2070
+ });
2071
+ return { read };
2072
+ }
2073
+ };
2074
+ }
2075
+
2076
+ // src/runtime/context.ts
2077
+ var journalEnsured = new Set;
2078
+ function validateColumnName(name) {
2079
+ if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
2080
+ throw new Error(`Invalid column name: ${name}`);
2081
+ }
2082
+ }
2083
+
2084
+ class SubgraphContext {
2085
+ block;
2086
+ _tx;
2087
+ db;
2088
+ pgSchemaName;
2089
+ subgraphSchema;
2090
+ ops = [];
2091
+ journal;
2092
+ _client;
2093
+ constructor(db, pgSchemaName, subgraphSchema, block, tx, journal = false) {
2094
+ this.db = db;
2095
+ this.pgSchemaName = pgSchemaName;
2096
+ this.subgraphSchema = subgraphSchema;
2097
+ this.block = block;
2098
+ this._tx = tx;
2099
+ this.journal = journal;
2100
+ }
2101
+ get client() {
2102
+ this._client ??= createChainReadClient({
2103
+ blockHeight: this.block.height,
2104
+ indexBlockHash: this.block.indexBlockHash
2105
+ });
2106
+ return this._client;
2107
+ }
2108
+ get tx() {
2109
+ return this._tx;
2110
+ }
2111
+ setTx(tx) {
2112
+ this._tx = tx;
2113
+ }
2114
+ insert(table, row) {
2115
+ this.validateTable(table);
2116
+ this.ops.push({
2117
+ kind: "insert",
2118
+ table,
2119
+ data: { ...row, _block_height: this.block.height, _tx_id: this._tx.txId }
2120
+ });
2121
+ }
2122
+ update(table, where, set) {
2123
+ this.validateTable(table);
2124
+ this.ops.push({ kind: "update", table, data: where, set });
2125
+ }
2126
+ upsert(table, key, row) {
2127
+ this.validateTable(table);
2128
+ const tableDef = this.subgraphSchema[table];
2129
+ if (!tableDef)
2130
+ return;
2131
+ const keyColumns = Object.keys(key);
2132
+ const hasUniqueConstraint = tableDef.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
2133
+ const meta = { _block_height: this.block.height, _tx_id: this._tx.txId };
2134
+ if (hasUniqueConstraint) {
2135
+ this.ops.push({
2136
+ kind: "insert",
2137
+ table,
2138
+ data: { ...key, ...row, ...meta, _upsert_keys: keyColumns }
2139
+ });
2140
+ } else {
2141
+ logger4.warn("upsert called without matching uniqueKeys constraint, using fallback", {
2142
+ table,
2143
+ keys: keyColumns
2144
+ });
2145
+ this.ops.push({
2146
+ kind: "insert",
2147
+ table,
2148
+ data: {
2149
+ ...key,
2150
+ ...row,
2151
+ ...meta,
2152
+ _upsert_fallback_keys: keyColumns,
2153
+ _upsert_fallback_set: row
2154
+ }
2155
+ });
2156
+ }
2157
+ }
2158
+ delete(table, where) {
2159
+ this.validateTable(table);
2160
+ this.ops.push({ kind: "delete", table, data: where });
2161
+ }
2162
+ increment(table, key, deltas) {
2163
+ this.validateTable(table);
2164
+ const tableDef = this.subgraphSchema[table];
2165
+ const keyColumns = Object.keys(key);
2166
+ const hasUniqueConstraint = tableDef?.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
2167
+ if (!hasUniqueConstraint) {
2168
+ throw new Error(`increment("${table}") requires a uniqueKeys constraint on [${keyColumns.join(", ")}]`);
2169
+ }
2170
+ for (const [col, v] of Object.entries(deltas)) {
2171
+ validateColumnName(col);
2172
+ if (keyColumns.includes(col)) {
2173
+ throw new Error(`increment("${table}"): "${col}" is a key column`);
2174
+ }
2175
+ if (typeof v !== "bigint" && typeof v !== "number") {
2176
+ throw new Error(`increment("${table}"): delta for "${col}" must be bigint or number`);
2177
+ }
2178
+ }
2179
+ this.ops.push({
2180
+ kind: "increment",
2181
+ table,
2182
+ data: {
2183
+ ...key,
2184
+ _block_height: this.block.height,
2185
+ _tx_id: this._tx.txId,
2186
+ _upsert_keys: keyColumns
2187
+ },
2188
+ set: { ...deltas }
2189
+ });
2190
+ }
2191
+ opsCheckpoint() {
2192
+ return this.ops.length;
2193
+ }
2194
+ rollbackTo(checkpoint) {
2195
+ if (checkpoint < 0 || checkpoint > this.ops.length)
2196
+ return;
2197
+ this.ops.length = checkpoint;
2198
+ }
2199
+ async readRows(table, where, limit) {
2200
+ const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
2201
+ const { clause } = buildWhereClause(where);
2202
+ const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} ORDER BY "_id"${limit !== undefined ? ` LIMIT ${limit}` : ""}`;
2203
+ const { rows } = await sql2.raw(query).execute(this.db);
2204
+ return rows.map((r) => this.coerceRow(table, r));
2205
+ }
2206
+ async findOne(table, where) {
2207
+ this.validateTable(table);
2208
+ const row = (await this.readRows(table, where, 1))[0] ?? null;
2209
+ return this.overlayOne(table, where, row);
2210
+ }
2211
+ async findMany(table, where) {
2212
+ this.validateTable(table);
2213
+ return this.overlayMany(table, where, await this.readRows(table, where));
2214
+ }
2215
+ overlayOne(table, where, dbRow) {
2216
+ if (this.ops.length === 0)
2217
+ return dbRow;
2218
+ let row = dbRow;
2219
+ for (const op of this.ops) {
2220
+ if (op.table !== table)
2221
+ continue;
2222
+ row = this.applyOpToRow(op, row, where);
2223
+ }
2224
+ return row;
2225
+ }
2226
+ overlayMany(table, where, dbRows) {
2227
+ if (this.ops.length === 0)
2228
+ return [...dbRows];
2229
+ let result = [...dbRows];
2230
+ for (const op of this.ops) {
2231
+ if (op.table !== table)
2232
+ continue;
2233
+ if (op.kind === "update") {
2234
+ for (let i = 0;i < result.length; i++) {
2235
+ if (rowMatches(result[i], op.data))
2236
+ result[i] = { ...result[i], ...op.set ?? {} };
2237
+ }
2238
+ } else if (op.kind === "delete") {
2239
+ result = result.filter((r) => !rowMatches(r, op.data));
2240
+ } else {
2241
+ const upsertKeys = op.data._upsert_keys;
2242
+ const clean = stripControlKeys(op.data);
2243
+ const idx = upsertKeys ? result.findIndex((r) => upsertKeys.every((k) => valEq(r[k], clean[k]))) : -1;
2244
+ if (idx >= 0) {
2245
+ const existing = result[idx];
2246
+ result[idx] = this.applyOpToRow(op, existing, where) ?? existing;
2247
+ } else {
2248
+ const created = this.applyOpToRow(op, null, where);
2249
+ if (created)
2250
+ result.push(created);
2251
+ }
2252
+ }
2253
+ }
2254
+ return result;
2255
+ }
2256
+ applyOpToRow(op, row, where) {
2257
+ const upsertKeys = op.data._upsert_keys;
2258
+ const clean = stripControlKeys(op.data);
2259
+ switch (op.kind) {
2260
+ case "insert": {
2261
+ if (row) {
2262
+ if (upsertKeys?.every((k) => valEq(row[k], clean[k]))) {
2263
+ const merged = { ...row };
2264
+ for (const [k, v] of Object.entries(clean)) {
2265
+ if (!upsertKeys.includes(k) && !k.startsWith("_"))
2266
+ merged[k] = v;
2267
+ }
2268
+ return merged;
2269
+ }
2270
+ return row;
2271
+ }
2272
+ return rowMatches(clean, where) ? { ...clean } : null;
2273
+ }
2274
+ case "increment": {
2275
+ const deltas = op.set ?? {};
2276
+ if (row) {
2277
+ if (upsertKeys.every((k) => valEq(row[k], clean[k]))) {
2278
+ const merged = { ...row };
2279
+ for (const [col, d] of Object.entries(deltas)) {
2280
+ merged[col] = toBigIntOr0(merged[col]) + toBigIntOr0(d);
2281
+ }
2282
+ return merged;
2283
+ }
2284
+ return row;
2285
+ }
2286
+ if (!rowMatches(clean, where))
2287
+ return null;
2288
+ const created = { ...clean };
2289
+ for (const [col, d] of Object.entries(deltas)) {
2290
+ created[col] = toBigIntOr0(d);
2291
+ }
2292
+ return created;
2293
+ }
2294
+ case "update":
2295
+ return row && rowMatches(row, op.data) ? { ...row, ...op.set ?? {} } : row;
2296
+ case "delete":
2297
+ return row && rowMatches(row, op.data) ? null : row;
2298
+ }
2299
+ }
2300
+ coerceRow(table, row) {
2301
+ const tableDef = this.subgraphSchema[table];
2302
+ if (!tableDef)
2303
+ return row;
2304
+ const result = { ...row };
2305
+ for (const [col, def] of Object.entries(tableDef.columns)) {
2306
+ if ((def.type === "uint" || def.type === "int") && typeof result[col] === "string") {
2307
+ result[col] = BigInt(result[col]);
2308
+ }
2309
+ }
2310
+ return result;
2311
+ }
2312
+ get pendingOps() {
2313
+ return this.ops.length;
2314
+ }
2315
+ async flush() {
2316
+ if (this.ops.length === 0)
2317
+ return { count: 0, writes: [] };
2318
+ await this.ensureJournalTable();
2319
+ const opsToFlush = [...this.ops];
2320
+ this.ops.length = 0;
2321
+ const statements = this.buildStatements(opsToFlush);
2322
+ if ("isTransaction" in this.db) {
2323
+ await this.execStatements(this.db, statements);
2324
+ } else {
2325
+ await this.db.transaction().execute(async (tx) => {
2326
+ await this.execStatements(tx, statements);
2327
+ });
2328
+ }
2329
+ const writes = opsToFlush.map((op, rowIndex) => {
2330
+ const blockHeight = op.data._block_height ?? this.block.height;
2331
+ const txId = op.data._tx_id ?? this._tx.txId;
2332
+ const baseRow = op.kind === "update" || op.kind === "increment" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
2333
+ baseRow._upsert_keys = undefined;
2334
+ baseRow._upsert_fallback_keys = undefined;
2335
+ baseRow._upsert_fallback_set = undefined;
2336
+ return {
2337
+ op: op.kind === "increment" ? "update" : op.kind,
2338
+ table: op.table,
2339
+ row: jsonSafe2(baseRow),
2340
+ pk: { blockHeight, txId, rowIndex }
2341
+ };
2342
+ });
2343
+ return { count: opsToFlush.length, writes };
2344
+ }
2345
+ async execStatements(executor, statements) {
2346
+ const affected = (r) => r.numAffectedRows ?? 0n;
2347
+ for (const stmt of statements) {
2348
+ if (typeof stmt === "string") {
2349
+ await sql2.raw(stmt).execute(executor);
2350
+ continue;
2351
+ }
2352
+ const updated = await sql2.raw(stmt.update).execute(executor);
2353
+ if (affected(updated) > 0n)
2354
+ continue;
2355
+ const inserted = await sql2.raw(stmt.insert).execute(executor);
2356
+ if (affected(inserted) > 0n)
2357
+ continue;
2358
+ const retried = await sql2.raw(stmt.update).execute(executor);
2359
+ if (affected(retried) === 0n) {
2360
+ throw new Error(`increment could not be applied to ${this.pgSchemaName}.${stmt.table} where ${stmt.keyPred}: row neither updated nor inserted`);
2361
+ }
2362
+ }
2363
+ }
2364
+ prepareInsert(op) {
2365
+ const upsertKeys = op.data._upsert_keys;
2366
+ const data = { ...op.data };
2367
+ delete data._upsert_keys;
2368
+ delete data._upsert_fallback_keys;
2369
+ delete data._upsert_fallback_set;
2370
+ if (!data._block_height)
2371
+ data._block_height = this.block.height;
2372
+ if (!data._tx_id)
2373
+ data._tx_id = this._tx.txId;
2374
+ data._created_at = "NOW()";
2375
+ const cols = Object.keys(data);
2376
+ cols.forEach(validateColumnName);
2377
+ const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
2378
+ const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
2379
+ return { data, cols, vals, upsertKeys, batchKey };
2380
+ }
2381
+ async ensureJournalTable() {
2382
+ if (!this.journal || journalEnsured.has(this.pgSchemaName))
2383
+ return;
2384
+ const { rows } = await sql2.raw(`SELECT to_regclass('"${this.pgSchemaName}"."_journal"') AS r`).execute(this.db);
2385
+ if (rows[0]?.r) {
2386
+ journalEnsured.add(this.pgSchemaName);
2387
+ return;
2388
+ }
2389
+ for (const stmt of emitJournalDDL(this.pgSchemaName)) {
2390
+ await sql2.raw(stmt).execute(this.db);
2391
+ }
2392
+ }
2393
+ columnSqlType(table, col) {
2394
+ const def = this.subgraphSchema[table]?.columns?.[col];
2395
+ return def ? TYPE_MAP[def.type] : undefined;
2396
+ }
2397
+ journalCaptureSQL(table, keyCols, keyLiteralRows) {
2398
+ const cast = (col, expr) => {
2399
+ const t = this.columnSqlType(table, col);
2400
+ return t ? `CAST(${expr} AS ${t})` : expr;
2401
+ };
2402
+ const keyObj = keyCols.map((k) => `'${k}', ${cast(k, `v."${k}"`)}`).join(", ");
2403
+ const joinCond = keyCols.map((k) => `t."${k}" = ${cast(k, `v."${k}"`)}`).join(" AND ");
2404
+ const valuesList = keyLiteralRows.map((r) => `(${r.join(", ")})`).join(", ");
2405
+ return `INSERT INTO "${this.pgSchemaName}"."_journal" ("block_height", "table_name", "row_key", "prev_row") ` + `SELECT ${this.block.height}, '${table}', jsonb_build_object(${keyObj}), to_jsonb(t.*) ` + `FROM (VALUES ${valuesList}) AS v(${keyCols.map((k) => `"${k}"`).join(", ")}) ` + `LEFT JOIN "${this.pgSchemaName}"."${table}" t ON ${joinCond}`;
2406
+ }
2407
+ journalCaptureByWhereSQL(table, clause) {
2408
+ return `INSERT INTO "${this.pgSchemaName}"."_journal" ("block_height", "table_name", "row_key", "prev_row") ` + `SELECT ${this.block.height}, '${table}', jsonb_build_object('_id', t."_id"), to_jsonb(t.*) ` + `FROM "${this.pgSchemaName}"."${table}" t WHERE ${clause}`;
2409
+ }
2410
+ buildStatements(ops) {
2411
+ const statements = [];
2412
+ let currentBatch = null;
2413
+ let currentBatchKey = "";
2414
+ let incBatch = null;
2415
+ let incBatchKey = "";
2416
+ const flushIncrementBatch = () => {
2417
+ if (!incBatch)
2418
+ return;
2419
+ const batch = incBatch;
2420
+ const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
2421
+ const cols = [
2422
+ ...batch.keyCols,
2423
+ ...batch.deltaCols,
2424
+ "_block_height",
2425
+ "_tx_id",
2426
+ "_created_at"
2427
+ ];
2428
+ if (this.journal) {
2429
+ statements.push(this.journalCaptureSQL(batch.table, batch.keyCols, Array.from(batch.rows.values()).map((r) => batch.keyCols.map((k) => escapeLiteral(r.keys[k])))));
2430
+ }
2431
+ for (const r of batch.rows.values()) {
2432
+ const keyPred = batch.keyCols.map((k) => `"${k}" = ${escapeLiteral(r.keys[k])}`).join(" AND ");
2433
+ const updSet = batch.deltaCols.map((c) => `"${c}" = COALESCE("${c}", 0) + (${String(r.deltas[c] ?? 0n)})`).join(", ");
2434
+ const insertVals = [
2435
+ ...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
2436
+ ...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
2437
+ escapeLiteral(r.meta.blockHeight),
2438
+ escapeLiteral(r.meta.txId),
2439
+ "NOW()"
2440
+ ];
2441
+ statements.push({
2442
+ kind: "increment",
2443
+ table: batch.table,
2444
+ keyPred,
2445
+ update: `UPDATE ${qualifiedTable} SET ${updSet} WHERE ${keyPred}`,
2446
+ insert: `INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) ` + `SELECT ${insertVals.join(", ")} ` + `WHERE NOT EXISTS (SELECT 1 FROM ${qualifiedTable} WHERE ${keyPred})`
2447
+ });
2448
+ }
2449
+ incBatch = null;
2450
+ incBatchKey = "";
2451
+ };
2452
+ const flushInsertBatch = () => {
2453
+ if (!currentBatch)
2454
+ return;
2455
+ const batch = currentBatch;
2456
+ const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
2457
+ const colList = batch.cols.map((c) => `"${c}"`).join(", ");
2458
+ let rows = batch.rows;
2459
+ if (batch.upsertKeys && batch.upsertKeys.length > 0) {
2460
+ const uKeys = batch.upsertKeys;
2461
+ const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
2462
+ const seen = new Map;
2463
+ for (let i = 0;i < rows.length; i++) {
2464
+ const key = keyIndices.map((ki) => rows[i][ki]).join("\x00");
2465
+ seen.set(key, i);
2466
+ }
2467
+ if (seen.size < rows.length) {
2468
+ rows = Array.from(seen.values()).map((i) => rows[i]);
2469
+ }
2470
+ }
2471
+ const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
2472
+ let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
2473
+ if (this.journal && batch.upsertKeys && batch.upsertKeys.length > 0) {
2474
+ const uKeys = batch.upsertKeys;
2475
+ const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
2476
+ statements.push(this.journalCaptureSQL(batch.table, uKeys, rows.map((r) => keyIndices.map((ki) => r[ki]))));
2477
+ }
2478
+ if (batch.upsertKeys && batch.upsertKeys.length > 0) {
2479
+ const batchKeys = batch.upsertKeys;
2480
+ const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !c.startsWith("_"));
2481
+ if (updateCols.length > 0) {
2482
+ const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
2483
+ stmt += ` ON CONFLICT (${batchKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
2484
+ } else {
2485
+ stmt += ` ON CONFLICT (${batchKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
2486
+ }
2487
+ }
2488
+ statements.push(stmt);
2489
+ currentBatch = null;
2490
+ currentBatchKey = "";
2491
+ };
2492
+ for (const op of ops) {
2493
+ const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
2494
+ if (op.kind === "insert") {
2495
+ flushIncrementBatch();
2496
+ const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
2497
+ if (batchKey === currentBatchKey && currentBatch) {
2498
+ currentBatch.rows.push(vals);
2499
+ } else {
2500
+ flushInsertBatch();
2501
+ currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
2502
+ currentBatchKey = batchKey;
2503
+ }
2504
+ } else if (op.kind === "increment") {
2505
+ flushInsertBatch();
2506
+ const keyCols = [...op.data._upsert_keys].sort();
2507
+ const deltaCols = Object.keys(op.set ?? {}).sort();
2508
+ const batchKey = `inc:${op.table}:${keyCols.join(",")}:${deltaCols.join(",")}`;
2509
+ if (batchKey !== incBatchKey || !incBatch) {
2510
+ flushIncrementBatch();
2511
+ incBatch = { table: op.table, keyCols, deltaCols, rows: new Map };
2512
+ incBatchKey = batchKey;
2513
+ }
2514
+ const clean = stripControlKeys(op.data);
2515
+ const keySig = keyCols.map((k) => escapeLiteral(clean[k])).join("\x00");
2516
+ const existing = incBatch.rows.get(keySig);
2517
+ if (existing) {
2518
+ for (const c of deltaCols) {
2519
+ existing.deltas[c] = (existing.deltas[c] ?? 0n) + toBigIntOr0(op.set?.[c]);
2520
+ }
2521
+ } else {
2522
+ const deltas = {};
2523
+ for (const c of deltaCols)
2524
+ deltas[c] = toBigIntOr0(op.set?.[c]);
2525
+ incBatch.rows.set(keySig, {
2526
+ keys: clean,
2527
+ deltas,
2528
+ meta: {
2529
+ blockHeight: op.data._block_height ?? this.block.height,
2530
+ txId: op.data._tx_id ?? this._tx.txId
2531
+ }
2532
+ });
2533
+ }
2534
+ } else {
2535
+ flushInsertBatch();
2536
+ flushIncrementBatch();
2537
+ if (op.kind === "update") {
2538
+ const setEntries = Object.entries(op.set ?? {});
2539
+ for (const [k] of setEntries)
2540
+ validateColumnName(k);
2541
+ const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
2542
+ const { clause } = buildWhereClause(op.data);
2543
+ if (this.journal) {
2544
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
2545
+ }
2546
+ statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
2547
+ } else if (op.kind === "delete") {
2548
+ const { clause } = buildWhereClause(op.data);
2549
+ if (this.journal) {
2550
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
2551
+ }
2552
+ statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
2553
+ }
2554
+ }
2555
+ }
2556
+ flushInsertBatch();
2557
+ flushIncrementBatch();
2558
+ return statements;
2559
+ }
2560
+ validateTable(table) {
2561
+ if (!this.subgraphSchema[table]) {
2562
+ throw new Error(`Table "${table}" not found in subgraph schema. Available: [${Object.keys(this.subgraphSchema).join(", ")}]`);
2563
+ }
2564
+ }
2565
+ }
2566
+ function stripControlKeys(data) {
2567
+ const {
2568
+ _upsert_keys: _a,
2569
+ _upsert_fallback_keys: _b,
2570
+ _upsert_fallback_set: _c,
2571
+ ...clean
2572
+ } = data;
2573
+ return clean;
2574
+ }
2575
+ function valEq(a, b) {
2576
+ if (a === b)
2577
+ return true;
2578
+ if (a == null || b == null)
2579
+ return false;
2580
+ return String(a) === String(b);
2581
+ }
2582
+ function rowMatches(row, where) {
2583
+ return Object.entries(where).every(([k, v]) => valEq(row[k], v));
2584
+ }
2585
+ function toBigIntOr0(v) {
2586
+ if (typeof v === "bigint")
2587
+ return v;
2588
+ if (v == null)
2589
+ return 0n;
2590
+ try {
2591
+ return BigInt(String(v));
2592
+ } catch {
2593
+ return 0n;
2594
+ }
2595
+ }
2596
+ function jsonSafe2(row) {
2597
+ const out = {};
2598
+ for (const [k, v] of Object.entries(row)) {
2599
+ out[k] = typeof v === "bigint" ? v.toString() : v;
2600
+ }
2601
+ return out;
2602
+ }
2603
+ function escapeLiteral(value) {
2604
+ if (value === null || value === undefined)
2605
+ return "NULL";
2606
+ if (typeof value === "number" || typeof value === "bigint")
2607
+ return String(value);
2608
+ if (typeof value === "boolean")
2609
+ return value ? "TRUE" : "FALSE";
2610
+ if (typeof value === "object")
2611
+ return `'${JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v).replace(/'/g, "''")}'::jsonb`;
2612
+ return `'${String(value).replace(/'/g, "''")}'`;
2613
+ }
2614
+ function buildWhereClause(where) {
2615
+ const entries = Object.entries(where);
2616
+ if (entries.length === 0)
2617
+ return { clause: "TRUE", values: [] };
2618
+ for (const [k] of entries)
2619
+ validateColumnName(k);
2620
+ const parts = entries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
2621
+ return { clause: parts.join(" AND "), values: [] };
2622
+ }
2623
+
2624
+ // src/runtime/memory-store.ts
2625
+ class MemoryStore {
2626
+ tables = new Map;
2627
+ }
2628
+
2629
+ class MemorySubgraphContext extends SubgraphContext {
2630
+ store;
2631
+ tables;
2632
+ constructor(store, schema, block, tx) {
2633
+ super(undefined, "memory", schema, block, tx, false);
2634
+ this.store = store;
2635
+ this.tables = schema;
2636
+ }
2637
+ async readRows(table, where, limit) {
2638
+ const rows = (this.store.tables.get(table) ?? []).filter((row) => Object.entries(where).every(([k, v]) => sameValue(row[k], v)));
2639
+ return limit === undefined ? rows : rows.slice(0, limit);
2640
+ }
2641
+ readDependent = null;
2642
+ dependsOnPrior(what) {
2643
+ this.readDependent ??= what;
2644
+ }
2645
+ findOne(table, where) {
2646
+ this.dependsOnPrior(`findOne("${table}")`);
2647
+ return super.findOne(table, where);
2648
+ }
2649
+ findMany(table, where) {
2650
+ this.dependsOnPrior(`findMany("${table}")`);
2651
+ return super.findMany(table, where);
2652
+ }
2653
+ update(table, where, set) {
2654
+ this.dependsOnPrior(`update("${table}")`);
2655
+ super.update(table, where, set);
2656
+ }
2657
+ delete(table, where) {
2658
+ this.dependsOnPrior(`delete("${table}")`);
2659
+ super.delete(table, where);
2660
+ }
2661
+ increment(table, key, deltas) {
2662
+ this.dependsOnPrior(`increment("${table}")`);
2663
+ super.increment(table, key, deltas);
2664
+ }
2665
+ upsert(table, key, row) {
2666
+ const def = this.tables[table];
2667
+ if (def) {
2668
+ const keyCols = Object.keys(key);
2669
+ const keyed = def.uniqueKeys?.some((uk) => uk.length === keyCols.length && uk.every((c) => keyCols.includes(c)));
2670
+ const omitted = Object.keys(def.columns).find((c) => !(c in key) && !(c in row));
2671
+ if (!keyed)
2672
+ this.dependsOnPrior(`upsert("${table}") without a unique key`);
2673
+ else if (omitted)
2674
+ this.dependsOnPrior(`upsert("${table}") leaves "${omitted}" as it was`);
2675
+ }
2676
+ super.upsert(table, key, row);
2677
+ }
2678
+ async rowsOf(table) {
2679
+ return this.overlayMany(table, {}, await this.readRows(table, {}));
2680
+ }
2681
+ setReads(reads) {
2682
+ this._client = {
2683
+ contract(contractId, abi) {
2684
+ const camelToKebab = new Map;
2685
+ for (const fn of normalizeAbi2(abi).functions) {
2686
+ camelToKebab.set(toCamelCase3(fn.name), fn.name);
2687
+ }
2688
+ const read = new Proxy({}, {
2689
+ get(_target, prop) {
2690
+ const fnName = camelToKebab.get(prop) ?? prop;
2691
+ const key = `${contractId}.${fnName}`;
2692
+ return async () => {
2693
+ if (!(key in reads)) {
2694
+ throw new Error(`No stubbed chain read for "${key}" — pass it via createTestContext(schema, { reads: { "${key}": … } }).`);
2695
+ }
2696
+ return reads[key];
2697
+ };
2698
+ }
2699
+ });
2700
+ return { read };
2701
+ }
2702
+ };
2703
+ }
2704
+ async commitOps() {
2705
+ const tables = new Set;
2706
+ for (const op of this.ops)
2707
+ tables.add(op.table);
2708
+ for (const table of tables) {
2709
+ this.store.tables.set(table, await this.rowsOf(table));
2710
+ }
2711
+ this.ops.length = 0;
2712
+ }
2713
+ insertsSince(checkpoint) {
2714
+ const out = [];
2715
+ for (const op of this.ops.slice(Math.max(0, checkpoint))) {
2716
+ if (op.kind !== "insert")
2717
+ continue;
2718
+ const row = { ...op.data };
2719
+ out.push({
2720
+ table: op.table,
2721
+ keys: Object.keys(row).filter((k) => !k.startsWith("_")),
2722
+ row
2723
+ });
2724
+ }
2725
+ return out;
2726
+ }
2727
+ }
2728
+ function sameValue(a, b) {
2729
+ if (a === b)
2730
+ return true;
2731
+ if ((typeof a === "bigint" || typeof a === "number") && (typeof b === "bigint" || typeof b === "number")) {
2732
+ return BigInt(a) === BigInt(b);
2733
+ }
2734
+ return String(a) === String(b);
2735
+ }
2736
+
2737
+ // src/runtime/batch-loader.ts
2738
+ import { sql as sql3 } from "kysely";
2739
+ function stateWriteBlocks(writesByHeight, txsByHeight) {
2740
+ const out = new Map;
2741
+ for (const [height, writes] of writesByHeight) {
2742
+ const txByIndex = new Map;
2743
+ for (const tx of txsByHeight.get(height) ?? []) {
2744
+ if (tx.tx_index !== undefined)
2745
+ txByIndex.set(tx.tx_index, tx);
2746
+ }
2747
+ out.set(height, stateWriteEvents(writes, txByIndex));
2748
+ }
2749
+ return out;
2750
+ }
2751
+ function vmEventId(txId, vmEventIndex) {
2752
+ return `${txId}#vm:${vmEventIndex}`;
2753
+ }
2754
+ async function loadBlockRange(db, fromHeight, toHeight, opts = {}) {
2755
+ const feed = opts.stateWrites;
2756
+ const [blocks, txs, events, vmRows, writeRows] = await Promise.all([
2757
+ db.selectFrom("blocks").selectAll().where("height", ">=", fromHeight).where("height", "<=", toHeight).where("canonical", "=", true).execute(),
2758
+ db.selectFrom("transactions").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
2759
+ db.selectFrom("events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
2760
+ feed ? Promise.resolve([]) : db.selectFrom("vm_events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).orderBy("ordinal", "asc").execute(),
2761
+ feed ? loadStateWrites(db, fromHeight, toHeight, feed) : Promise.resolve([])
2762
+ ]);
2763
+ const txsByHeight = new Map;
2764
+ for (const tx of txs) {
2765
+ const h = Number(tx.block_height);
2766
+ const list = txsByHeight.get(h) ?? [];
2767
+ list.push(tx);
2768
+ txsByHeight.set(h, list);
2769
+ }
2770
+ const eventsByHeight = new Map;
2771
+ for (const evt of events) {
2772
+ const h = Number(evt.block_height);
2773
+ const list = eventsByHeight.get(h) ?? [];
2774
+ list.push(evt);
2775
+ eventsByHeight.set(h, list);
2776
+ }
2777
+ const vmByHeight = new Map;
2778
+ for (const row of vmRows) {
2779
+ const h = Number(row.block_height);
2780
+ const list = vmByHeight.get(h) ?? [];
2781
+ list.push({
2782
+ id: vmEventId(row.tx_id, Number(row.ordinal)),
2783
+ tx_id: row.tx_id,
2784
+ block_height: h,
2785
+ event_index: Number(row.ordinal),
2786
+ type: row.type,
2787
+ data: row.data,
2788
+ created_at: row.created_at,
2789
+ clock: "vm"
2790
+ });
2791
+ vmByHeight.set(h, list);
2792
+ }
2793
+ const writesByHeight = new Map;
2794
+ for (const row of writeRows) {
2795
+ const h = Number(row.block_height);
2796
+ const list = writesByHeight.get(h) ?? [];
2797
+ list.push({
2798
+ ordinal: Number(row.ordinal),
2799
+ tx_index: row.tx_index === null ? null : Number(row.tx_index),
2800
+ key: row.key,
2801
+ value_hex: row.value_hex
2802
+ });
2803
+ writesByHeight.set(h, list);
2804
+ }
2805
+ const named = stateWriteBlocks(writesByHeight, txsByHeight);
2806
+ const result = new Map;
2807
+ for (const block of blocks) {
2808
+ const h = Number(block.height);
2809
+ const fed = named.get(h);
2810
+ result.set(h, {
2811
+ block,
2812
+ txs: feed ? fed?.txs ?? [] : txsByHeight.get(h) ?? [],
2813
+ events: eventsByHeight.get(h) ?? [],
2814
+ vmEvents: feed ? fed?.vmEvents ?? [] : vmByHeight.get(h) ?? []
2815
+ });
2816
+ }
2817
+ return result;
2818
+ }
2819
+ async function loadStateWrites(db, fromHeight, toHeight, feed) {
2820
+ const scope = feed.contracts ? sql3`AND (${sql3.join(feed.contracts.map((c) => sql3`starts_with(key, ${`vm::${c}::`})`), sql3` OR `)})` : sql3``;
2821
+ const { rows } = await sql3`
2822
+ SELECT block_height, ordinal, tx_index, key, value_hex
2823
+ FROM state_writes
2824
+ WHERE block_height >= ${fromHeight} AND block_height <= ${toHeight} ${scope}
2825
+ ORDER BY block_height, ordinal
2826
+ `.execute(db);
2827
+ return rows;
2828
+ }
2829
+
2830
+ // src/runtime/state-writes.ts
2831
+ var DATA_MAP = "0";
2832
+ var VARIABLE = "1";
2833
+ var HEX = /^(?:[0-9a-f]{2})+$/;
2834
+ function parseStateWriteKey(key) {
2835
+ if (!key.startsWith("vm::"))
2836
+ return null;
2837
+ const parts = key.slice(4).split("::");
2838
+ const [contractId, store] = parts;
2839
+ if (!contractId?.includes("."))
2840
+ return null;
2841
+ if (store === DATA_MAP && parts.length === 4) {
2842
+ const [, , map, rawKey] = parts;
2843
+ if (!map || !rawKey || !HEX.test(rawKey))
2844
+ return null;
2845
+ return { contractId, kind: "map", map, rawKey };
2846
+ }
2847
+ if (store === VARIABLE && parts.length === 3 && parts[2]) {
2848
+ return { contractId, kind: "var", varName: parts[2] };
2849
+ }
2850
+ return null;
2851
+ }
2852
+ var utf8 = new TextDecoder("utf-8", { fatal: true });
2853
+ function storedValue(w) {
2854
+ const h = w.value_hex.startsWith("0x") ? w.value_hex.slice(2) : w.value_hex;
2855
+ const bytes = new Uint8Array(h.length / 2);
2856
+ for (let i = 0;i < bytes.length; i++) {
2857
+ bytes[i] = Number.parseInt(h.slice(i * 2, i * 2 + 2), 16);
2858
+ }
2859
+ let value;
2860
+ try {
2861
+ value = utf8.decode(bytes);
2862
+ } catch {
2863
+ throw new Error(`state write ${w.ordinal} value is not UTF-8 hex`);
2864
+ }
2865
+ value = value.toLowerCase();
2866
+ if (!HEX.test(value)) {
2867
+ throw new Error(`state write ${w.ordinal} value is not a Clarity value`);
2868
+ }
2869
+ return value;
2870
+ }
2871
+ function writeEvent(w) {
2872
+ const key = parseStateWriteKey(w.key);
2873
+ if (!key)
2874
+ return null;
2875
+ const value = storedValue(w);
2876
+ if (key.kind === "var") {
2877
+ return {
2878
+ type: "var_set",
2879
+ data: {
2880
+ contract_identifier: key.contractId,
2881
+ var_name: key.varName,
2882
+ raw_value: `0x${value}`
2883
+ }
2884
+ };
2885
+ }
2886
+ const base = {
2887
+ contract_identifier: key.contractId,
2888
+ map_name: key.map,
2889
+ raw_key: `0x${key.rawKey}`
2890
+ };
2891
+ if (value === "09")
2892
+ return { type: "map_delete", data: base };
2893
+ if (value.startsWith("0a")) {
2894
+ return {
2895
+ type: "map_set",
2896
+ data: { ...base, raw_value: `0x${value.slice(2)}` }
2897
+ };
2898
+ }
2899
+ throw new Error(`state write ${w.ordinal} map value is neither (some v) nor none`);
2900
+ }
2901
+ function stateWriteEvents(writes, txByIndex) {
2902
+ const txs = new Map;
2903
+ const vmEvents = [];
2904
+ for (const w of [...writes].sort((a, b) => a.ordinal - b.ordinal)) {
2905
+ const event = writeEvent(w);
2906
+ if (!event)
2907
+ continue;
2908
+ let tx = txs.get(w.tx_index);
2909
+ if (!tx) {
2910
+ if (w.tx_index === null) {
2911
+ tx = { tx_id: "", type: "block", sender: "", status: "success" };
2912
+ } else {
2913
+ const real = txByIndex.get(w.tx_index);
2914
+ if (!real) {
2915
+ throw new Error(`state write ${w.ordinal} names tx_index ${w.tx_index} the block does not have`);
2916
+ }
2917
+ tx = real;
2918
+ }
2919
+ tx = { ...tx, tx_index: 0 };
2920
+ txs.set(w.tx_index, tx);
2921
+ }
2922
+ vmEvents.push({
2923
+ id: vmEventId(tx.tx_id || "block", w.ordinal),
2924
+ tx_id: tx.tx_id,
2925
+ type: event.type,
2926
+ event_index: w.ordinal,
2927
+ data: event.data,
2928
+ clock: "vm"
2929
+ });
2930
+ }
2931
+ return { txs: [...txs.values()], vmEvents };
2932
+ }
2933
+
2934
+ // src/verify/replay.ts
2935
+ var REPLAY_WINDOW = 512;
2936
+ var STEP_OF = {
2937
+ ancestry: "blocks",
2938
+ block: "blocks",
2939
+ bitcoin: "blocks",
2940
+ burn: "blocks",
2941
+ "signer-set": "blocks",
2942
+ signatures: "blocks",
2943
+ witness: "blocks",
2944
+ txs: "txs",
2945
+ names: "inputs"
2946
+ };
2947
+ var EMPTY_TX = { txId: "", sender: "", type: "", status: "" };
2948
+ function replayContracts(def) {
2949
+ const out = new Set;
2950
+ for (const [name, source] of Object.entries(def.sources)) {
2951
+ const f = source;
2952
+ const ids = Array.isArray(f.contractId) ? f.contractId : f.contractId ? [f.contractId] : [];
2953
+ if (f.factory || ids.length === 0 || ids.some((id) => id.includes("*")))
2954
+ return {
2955
+ error: `source "${name}": wildcard contract ids need a full-block name check per contract`
2956
+ };
2957
+ for (const id of ids)
2958
+ out.add(id);
2959
+ }
2960
+ return [...out];
2961
+ }
2962
+ var sha2562 = (s) => createHash3("sha256").update(s).digest("hex");
2963
+ function checkPin(served, def) {
2964
+ if (!served.pin || !served.pinPreimage)
2965
+ return { status: "unchecked", notes: [] };
2966
+ const failed = (message) => ({
2967
+ status: "failed",
2968
+ failure: { step: "pin", message },
2969
+ notes: []
2970
+ });
2971
+ if (sha2562(served.pinPreimage) !== served.pin)
2972
+ return failed(`pin ${served.pin} is not the sha256 of its preimage`);
2973
+ let fields;
2974
+ try {
2975
+ fields = JSON.parse(served.pinPreimage);
2976
+ } catch {
2977
+ return failed("pin preimage is not JSON");
2978
+ }
2979
+ if (fields.handlerHash !== sha2562(served.handlerCode))
2980
+ return failed("the served handler code is not the bundle the pin names");
2981
+ const schemaHash = generateSubgraphSQL(def).hash;
2982
+ if (fields.schemaHash !== schemaHash)
2983
+ return failed(`the pin names schema ${fields.schemaHash}, the served bundle defines ${schemaHash}`);
2984
+ if (fields.startBlock !== (def.startBlock ?? null))
2985
+ return failed(`the pin names startBlock ${fields.startBlock}, the served bundle ${def.startBlock ?? null}`);
2986
+ const notes = [];
2987
+ if (fields.runtime !== SUBGRAPHS_RUNTIME)
2988
+ notes.push(`deployed on ${fields.runtime}, replayed on ${SUBGRAPHS_RUNTIME}`);
2989
+ if (fields.network !== "mainnet")
2990
+ notes.push(`pinned to network ${fields.network}`);
2991
+ return { status: "ok", notes };
2992
+ }
2993
+ function decodeTxs(v) {
2994
+ return (v.transactions ?? []).map((t, i) => {
2995
+ const d = decodeRawTx(hex(t.raw), t.txid);
2996
+ return {
2997
+ tx_id: `0x${t.txid}`,
2998
+ tx_index: i,
2999
+ type: d?.txType ?? "unknown",
3000
+ sender: d?.sender ?? "unknown",
3001
+ status: "success",
3002
+ contract_id: d?.contractId ?? null,
3003
+ function_name: d?.functionName ?? null,
3004
+ function_args: d?.functionArgs ?? null,
3005
+ raw_result: null
3006
+ };
3007
+ });
3008
+ }
3009
+ async function replayBlocks(deps, opts) {
3010
+ const out = {
3011
+ ok: false,
3012
+ from: opts.from,
3013
+ to: opts.to,
3014
+ blocks: 0,
3015
+ txs: 0,
3016
+ writes: 0,
3017
+ events: 0,
3018
+ handlerErrors: 0,
3019
+ marfProofs: 0,
3020
+ tables: new Map,
3021
+ notes: [],
3022
+ failures: []
3023
+ };
3024
+ const done = () => {
3025
+ out.ok = out.failures.length === 0 && !out.inconclusive;
3026
+ return out;
3027
+ };
3028
+ let def;
3029
+ try {
3030
+ def = await loadDeterministicDefinition(opts.handlerCode);
3031
+ } catch (err) {
3032
+ out.failures.push({
3033
+ step: "handlers",
3034
+ message: `the bundle does not load in the deterministic realm: ${err.message}`
3035
+ });
3036
+ return done();
3037
+ }
3038
+ const level = deriveVerification(def);
3039
+ if (level.level !== "state") {
3040
+ out.failures.push({
3041
+ step: "handlers",
3042
+ message: `level ${level.level}, not state: ${level.reasons.join("; ")}`
3043
+ });
3044
+ return done();
3045
+ }
3046
+ const contracts = replayContracts(def);
3047
+ if (!Array.isArray(contracts)) {
3048
+ out.failures.push({ step: "inputs", message: contracts.error });
3049
+ return done();
3050
+ }
3051
+ const ours = (key) => contracts.some((c) => key.startsWith(`vm::${c}::`));
3052
+ const startBlock = opts.startBlock ?? def.startBlock ?? 0;
3053
+ const midHistory = opts.from > startBlock;
3054
+ const store = new MemoryStore;
3055
+ const burnHints = new Map;
3056
+ let notedBurn = false;
3057
+ for (let a = opts.from;a <= opts.to; a += REPLAY_WINDOW) {
3058
+ const b = Math.min(a + REPLAY_WINDOW - 1, opts.to);
3059
+ const proven = new Map;
3060
+ for (let h = b;h >= a; h--) {
3061
+ const v = await deps.verify(h);
3062
+ const first = v.failures[0];
3063
+ if (first) {
3064
+ out.failures.push({
3065
+ step: STEP_OF[first.step] ?? "blocks",
3066
+ message: `block ${h}: ${first.message}`,
3067
+ height: h,
3068
+ ...first.code === "unavailable" ? { unavailable: true } : {}
3069
+ });
3070
+ return done();
3071
+ }
3072
+ if (v.ancestry?.via === "marf")
3073
+ out.marfProofs++;
3074
+ if (!v.blockHash || !v.blockId) {
3075
+ out.failures.push({
3076
+ step: "blocks",
3077
+ message: `block ${h}: epoch 2.x; replay needs Nakamoto blocks`,
3078
+ height: h,
3079
+ unavailable: true
3080
+ });
3081
+ return done();
3082
+ }
3083
+ if (!v.transactions) {
3084
+ out.failures.push({
3085
+ step: "txs",
3086
+ message: `block ${h}: the source served no transactions`,
3087
+ height: h,
3088
+ unavailable: true
3089
+ });
3090
+ return done();
3091
+ }
3092
+ if (!v.diff?.named || !v.writes) {
3093
+ out.failures.push({
3094
+ step: "inputs",
3095
+ message: `block ${h}: the source serves no state_writes for it (its node does not deliver them yet), so its writes cannot be named and the subgraph cannot be verified here`,
3096
+ height: h,
3097
+ unavailable: true
3098
+ });
3099
+ return done();
3100
+ }
3101
+ let burnHeight = v.burnHeight;
3102
+ if (burnHeight === undefined) {
3103
+ const ch = v.consensusHash;
3104
+ burnHeight = burnHints.get(ch);
3105
+ if (burnHeight === undefined) {
3106
+ burnHeight = await deps.burnHeightHint(ch);
3107
+ burnHints.set(ch, burnHeight);
3108
+ }
3109
+ if (!notedBurn) {
3110
+ out.notes.push(`burn height unproven below Bitcoin ${opts.bitcoinCheckpoint ?? "checkpoint"}: ctx.block.burnBlockHeight is the source's claim`);
3111
+ notedBurn = true;
3112
+ }
3113
+ }
3114
+ proven.set(h, {
3115
+ meta: {
3116
+ height: h,
3117
+ hash: `0x${v.blockHash}`,
3118
+ timestamp: v.timestamp ?? 0,
3119
+ burnBlockHeight: burnHeight,
3120
+ indexBlockHash: `0x${v.blockId}`
3121
+ },
3122
+ txs: decodeTxs(v),
3123
+ writes: v.writes.filter((w) => ours(w.key))
3124
+ });
3125
+ }
3126
+ for (let h = a;h <= b; h++) {
3127
+ const block = proven.get(h);
3128
+ const byIndex = new Map(block.txs.map((t) => [t.tx_index, t]));
3129
+ let fed;
3130
+ try {
3131
+ fed = stateWriteEvents(block.writes, byIndex);
3132
+ } catch (err) {
3133
+ out.failures.push({
3134
+ step: "inputs",
3135
+ message: `block ${h}: ${err.message}`,
3136
+ height: h
3137
+ });
3138
+ return done();
3139
+ }
3140
+ const ctx = new MemorySubgraphContext(store, def.schema, block.meta, EMPTY_TX);
3141
+ try {
3142
+ const r = await applyBlock(def, { txs: fed.txs, events: [], vmEvents: fed.vmEvents }, ctx);
3143
+ out.events += r.delivered;
3144
+ out.handlerErrors += r.errors;
3145
+ } catch (err) {
3146
+ if (!abortsBlock(err))
3147
+ throw err;
3148
+ out.failures.push({
3149
+ step: "handlers",
3150
+ message: `block ${h} ${err.name}: ${err.message}`,
3151
+ height: h
3152
+ });
3153
+ return done();
3154
+ }
3155
+ await ctx.commitOps();
3156
+ if (midHistory && ctx.readDependent && !out.inconclusive) {
3157
+ out.inconclusive = `block ${h}: ${ctx.readDependent} depends on rows written before ${opts.from}; replay from startBlock ${startBlock}`;
3158
+ }
3159
+ out.blocks++;
3160
+ out.txs += block.txs.length;
3161
+ out.writes += block.writes.length;
3162
+ opts.onProgress?.(h);
3163
+ }
3164
+ }
3165
+ out.tables = store.tables;
3166
+ return done();
3167
+ }
3168
+ function cacheWitnesses(source) {
3169
+ const recent = new Map;
3170
+ const out = {
3171
+ getBlock: (ref) => source.getBlock(ref),
3172
+ getMarfProof: (path, tip) => source.getMarfProof(path, tip),
3173
+ getBurnPreimage: (ch) => source.getBurnPreimage(ch),
3174
+ getBitcoinHeaders: (from, count) => source.getBitcoinHeaders(from, count),
3175
+ getWitness: (id) => {
3176
+ let w = recent.get(id);
3177
+ if (!w) {
3178
+ w = source.getWitness(id);
3179
+ w.catch(() => recent.delete(id));
3180
+ recent.set(id, w);
3181
+ while (recent.size > 2)
3182
+ recent.delete(recent.keys().next().value);
3183
+ }
3184
+ return w;
3185
+ }
3186
+ };
3187
+ const { getEpoch2Header, getStateWrites } = source;
3188
+ if (getEpoch2Header)
3189
+ out.getEpoch2Header = (id) => getEpoch2Header.call(source, id);
3190
+ if (getStateWrites)
3191
+ out.getStateWrites = (height) => getStateWrites.call(source, height);
3192
+ return out;
3193
+ }
3194
+ async function replaySubgraph(opts) {
3195
+ const checkpoint = opts.checkpoint ?? MAINNET_CHECKPOINT;
3196
+ const source = cacheWitnesses(opts.source);
3197
+ const verifier = new BlockVerifier({ source, checkpoint });
3198
+ return replayBlocks({
3199
+ verify: (h) => verifier.verify(h),
3200
+ burnHeightHint: async (ch) => (await source.getBurnPreimage(ch)).burnHeight
3201
+ }, { ...opts, bitcoinCheckpoint: checkpoint.bitcoin.height });
3202
+ }
3203
+ // src/verify/compare.ts
3204
+ import { createHash as createHash4 } from "node:crypto";
3205
+ function canonicalJson(value) {
3206
+ if (typeof value === "bigint")
3207
+ return value.toString();
3208
+ if (Array.isArray(value))
3209
+ return value.map(canonicalJson);
3210
+ if (value && typeof value === "object" && !(value instanceof Date)) {
3211
+ const out = {};
3212
+ for (const k of Object.keys(value).sort()) {
3213
+ out[k] = canonicalJson(value[k]);
3214
+ }
3215
+ return out;
3216
+ }
3217
+ return value ?? null;
3218
+ }
3219
+ function canonicalValue(type, value) {
3220
+ if (value === undefined || value === null)
3221
+ return null;
3222
+ switch (type) {
3223
+ case "uint":
3224
+ case "int":
3225
+ return typeof value === "string" ? BigInt(value).toString() : String(value);
3226
+ case "boolean":
3227
+ return value === true || value === "true" || value === "t" ? "true" : "false";
3228
+ case "timestamp": {
3229
+ const ms = value instanceof Date ? value.getTime() : typeof value === "number" ? value : Date.parse(String(value));
3230
+ return String(ms);
3231
+ }
3232
+ case "jsonb":
3233
+ return JSON.stringify(canonicalJson(typeof value === "string" ? JSON.parse(value) : value));
3234
+ default:
3235
+ return value instanceof Uint8Array ? Buffer.from(value).toString("hex") : String(value);
3236
+ }
3237
+ }
3238
+ function canonicalRow(def, row, columns = Object.keys(def.columns).sort()) {
3239
+ const out = {};
3240
+ for (const c of columns)
3241
+ out[c] = canonicalValue(def.columns[c]?.type, row[c]);
3242
+ out._block_height = canonicalValue("uint", row._block_height);
3243
+ return out;
3244
+ }
3245
+ var line = (r) => JSON.stringify(r);
3246
+ function digest(lines) {
3247
+ return createHash4("sha256").update([...lines].sort().join(`
3248
+ `)).digest("hex");
3249
+ }
3250
+ var heightOf = (row) => Number(row._block_height);
3251
+ function firstDifference(a, b) {
3252
+ return Object.keys(a).find((k) => a[k] !== b[k]);
3253
+ }
3254
+ function compareRows(schema, replayed, served) {
3255
+ const { from, to } = replayed;
3256
+ const inWindow = (row) => heightOf(row) >= from && heightOf(row) <= to;
3257
+ const failures = [];
3258
+ const tables = [];
3259
+ const fail = (table, message, key) => failures.push({
3260
+ step: "rows",
3261
+ table,
3262
+ message: `${table} ${message}`,
3263
+ ...key ? { key } : {}
3264
+ });
3265
+ for (const [table, def] of Object.entries(schema)) {
3266
+ const mine = (replayed.tables.get(table) ?? []).filter(inWindow);
3267
+ const theirs = served.get(table) ?? [];
3268
+ const theirsInWindow = theirs.filter(inWindow);
3269
+ const mineLines = mine.map((r) => line(canonicalRow(def, r)));
3270
+ const theirLines = theirsInWindow.map((r) => line(canonicalRow(def, r)));
3271
+ const summary = {
3272
+ table,
3273
+ equal: 0,
3274
+ superseded: 0,
3275
+ digest: digest(mineLines),
3276
+ servedDigest: digest(theirLines)
3277
+ };
3278
+ tables.push(summary);
3279
+ const keyCols = def.uniqueKeys?.[0];
3280
+ if (!keyCols?.length) {
3281
+ const counts = new Map;
3282
+ for (const l of mineLines)
3283
+ counts.set(l, (counts.get(l) ?? 0) + 1);
3284
+ for (const l of theirLines)
3285
+ counts.set(l, (counts.get(l) ?? 0) - 1);
3286
+ const diff = [...counts].find(([, n]) => n !== 0);
3287
+ if (diff) {
3288
+ const [l, n] = diff;
3289
+ const at = JSON.parse(l);
3290
+ fail(table, `row at block ${at._block_height}: ${n > 0 ? "replayed but not served" : "served but no proven input produces it"}`);
3291
+ } else
3292
+ summary.equal = mine.length;
3293
+ continue;
3294
+ }
3295
+ const keyOf = (row) => JSON.stringify(keyCols.map((c) => canonicalValue(def.columns[c]?.type, row[c])));
3296
+ const keyRow = (row) => Object.fromEntries(keyCols.map((c) => [c, row[c]]));
3297
+ const servedByKey = new Map(theirs.map((r) => [keyOf(r), r]));
3298
+ const replayedKeys = new Set;
3299
+ const columns = Object.keys(def.columns).sort();
3300
+ for (const row of mine) {
3301
+ const key = keyOf(row);
3302
+ replayedKeys.add(key);
3303
+ const s = servedByKey.get(key);
3304
+ if (!s) {
3305
+ fail(table, `${key}: missing (dropped, or deleted after --to)`, keyRow(row));
3306
+ continue;
3307
+ }
3308
+ if (heightOf(s) > to) {
3309
+ summary.superseded++;
3310
+ continue;
3311
+ }
3312
+ const a = canonicalRow(def, s, columns);
3313
+ const b = canonicalRow(def, row, columns);
3314
+ if (heightOf(s) < from) {
3315
+ a._block_height = b._block_height;
3316
+ }
3317
+ const col = firstDifference(a, b);
3318
+ if (col === undefined)
3319
+ summary.equal++;
3320
+ else
3321
+ fail(table, `${key}: ${col} served ${String(a[col])}, replayed ${String(b[col])}`, keyRow(row));
3322
+ }
3323
+ for (const s of theirsInWindow) {
3324
+ if (!replayedKeys.has(keyOf(s)))
3325
+ fail(table, `${keyOf(s)}: extra: no proven input produces it`, keyRow(s));
3326
+ }
3327
+ }
3328
+ return { failures, tables };
3329
+ }
3330
+ export {
3331
+ REPLAY_WINDOW,
3332
+ checkPin,
3333
+ compareRows,
3334
+ loadDeterministicDefinition,
3335
+ replayBlocks,
3336
+ replayContracts,
3337
+ replaySubgraph
3338
+ };
3339
+
3340
+ //# debugId=2078A9B6D93D428264756E2164756E21
3341
+ //# sourceMappingURL=index.js.map