@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.
- package/README.md +1 -0
- package/dist/index.d.ts +200 -119
- package/dist/index.js +761 -21
- package/dist/index.js.map +9 -6
- package/dist/runtime/emitter.js +1 -1
- package/dist/runtime/emitter.js.map +2 -2
- package/dist/runtime/replay.d.ts +32 -13
- package/dist/runtime/replay.js +723 -22
- package/dist/runtime/replay.js.map +8 -5
- package/dist/schema/index.d.ts +43 -36
- package/dist/schema/index.js +307 -6
- package/dist/schema/index.js.map +6 -5
- package/dist/testing/index.d.ts +105 -105
- package/dist/testing/index.js +867 -84
- package/dist/testing/index.js.map +9 -6
- package/dist/types.d.ts +31 -31
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +27 -27
- package/dist/validate.js +1 -1
- package/dist/validate.js.map +2 -2
- package/dist/verification.d.ts +441 -0
- package/dist/verification.js +282 -0
- package/dist/verification.js.map +10 -0
- package/dist/verify/index.d.ts +475 -0
- package/dist/verify/index.js +3341 -0
- package/dist/verify/index.js.map +27 -0
- package/package.json +13 -3
package/dist/testing/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// src/testing/harness.ts
|
|
2
|
-
import { normalizeAbi as normalizeAbi2, toCamelCase as toCamelCase3 } from "@secondlayer/stacks/clarity";
|
|
3
|
-
|
|
4
1
|
// src/runtime/context.ts
|
|
5
2
|
import { logger as logger2 } from "@secondlayer/shared/logger";
|
|
6
3
|
import { sql } from "kysely";
|
|
@@ -440,7 +437,7 @@ class SubgraphContext {
|
|
|
440
437
|
async readRows(table, where, limit) {
|
|
441
438
|
const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
|
|
442
439
|
const { clause } = buildWhereClause(where);
|
|
443
|
-
const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}${limit !== undefined ? ` LIMIT ${limit}` : ""}`;
|
|
440
|
+
const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} ORDER BY "_id"${limit !== undefined ? ` LIMIT ${limit}` : ""}`;
|
|
444
441
|
const { rows } = await sql.raw(query).execute(this.db);
|
|
445
442
|
return rows.map((r) => this.coerceRow(table, r));
|
|
446
443
|
}
|
|
@@ -862,13 +859,127 @@ function buildWhereClause(where) {
|
|
|
862
859
|
return { clause: parts.join(" AND "), values: [] };
|
|
863
860
|
}
|
|
864
861
|
|
|
862
|
+
// src/runtime/memory-store.ts
|
|
863
|
+
import { normalizeAbi as normalizeAbi2, toCamelCase as toCamelCase2 } from "@secondlayer/stacks/clarity";
|
|
864
|
+
class MemoryStore {
|
|
865
|
+
tables = new Map;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
class MemorySubgraphContext extends SubgraphContext {
|
|
869
|
+
store;
|
|
870
|
+
tables;
|
|
871
|
+
constructor(store, schema, block, tx) {
|
|
872
|
+
super(undefined, "memory", schema, block, tx, false);
|
|
873
|
+
this.store = store;
|
|
874
|
+
this.tables = schema;
|
|
875
|
+
}
|
|
876
|
+
async readRows(table, where, limit) {
|
|
877
|
+
const rows = (this.store.tables.get(table) ?? []).filter((row) => Object.entries(where).every(([k, v]) => sameValue(row[k], v)));
|
|
878
|
+
return limit === undefined ? rows : rows.slice(0, limit);
|
|
879
|
+
}
|
|
880
|
+
readDependent = null;
|
|
881
|
+
dependsOnPrior(what) {
|
|
882
|
+
this.readDependent ??= what;
|
|
883
|
+
}
|
|
884
|
+
findOne(table, where) {
|
|
885
|
+
this.dependsOnPrior(`findOne("${table}")`);
|
|
886
|
+
return super.findOne(table, where);
|
|
887
|
+
}
|
|
888
|
+
findMany(table, where) {
|
|
889
|
+
this.dependsOnPrior(`findMany("${table}")`);
|
|
890
|
+
return super.findMany(table, where);
|
|
891
|
+
}
|
|
892
|
+
update(table, where, set) {
|
|
893
|
+
this.dependsOnPrior(`update("${table}")`);
|
|
894
|
+
super.update(table, where, set);
|
|
895
|
+
}
|
|
896
|
+
delete(table, where) {
|
|
897
|
+
this.dependsOnPrior(`delete("${table}")`);
|
|
898
|
+
super.delete(table, where);
|
|
899
|
+
}
|
|
900
|
+
increment(table, key, deltas) {
|
|
901
|
+
this.dependsOnPrior(`increment("${table}")`);
|
|
902
|
+
super.increment(table, key, deltas);
|
|
903
|
+
}
|
|
904
|
+
upsert(table, key, row) {
|
|
905
|
+
const def = this.tables[table];
|
|
906
|
+
if (def) {
|
|
907
|
+
const keyCols = Object.keys(key);
|
|
908
|
+
const keyed = def.uniqueKeys?.some((uk) => uk.length === keyCols.length && uk.every((c) => keyCols.includes(c)));
|
|
909
|
+
const omitted = Object.keys(def.columns).find((c) => !(c in key) && !(c in row));
|
|
910
|
+
if (!keyed)
|
|
911
|
+
this.dependsOnPrior(`upsert("${table}") without a unique key`);
|
|
912
|
+
else if (omitted)
|
|
913
|
+
this.dependsOnPrior(`upsert("${table}") leaves "${omitted}" as it was`);
|
|
914
|
+
}
|
|
915
|
+
super.upsert(table, key, row);
|
|
916
|
+
}
|
|
917
|
+
async rowsOf(table) {
|
|
918
|
+
return this.overlayMany(table, {}, await this.readRows(table, {}));
|
|
919
|
+
}
|
|
920
|
+
setReads(reads) {
|
|
921
|
+
this._client = {
|
|
922
|
+
contract(contractId, abi) {
|
|
923
|
+
const camelToKebab = new Map;
|
|
924
|
+
for (const fn of normalizeAbi2(abi).functions) {
|
|
925
|
+
camelToKebab.set(toCamelCase2(fn.name), fn.name);
|
|
926
|
+
}
|
|
927
|
+
const read = new Proxy({}, {
|
|
928
|
+
get(_target, prop) {
|
|
929
|
+
const fnName = camelToKebab.get(prop) ?? prop;
|
|
930
|
+
const key = `${contractId}.${fnName}`;
|
|
931
|
+
return async () => {
|
|
932
|
+
if (!(key in reads)) {
|
|
933
|
+
throw new Error(`No stubbed chain read for "${key}" — pass it via createTestContext(schema, { reads: { "${key}": … } }).`);
|
|
934
|
+
}
|
|
935
|
+
return reads[key];
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
return { read };
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
async commitOps() {
|
|
944
|
+
const tables = new Set;
|
|
945
|
+
for (const op of this.ops)
|
|
946
|
+
tables.add(op.table);
|
|
947
|
+
for (const table of tables) {
|
|
948
|
+
this.store.tables.set(table, await this.rowsOf(table));
|
|
949
|
+
}
|
|
950
|
+
this.ops.length = 0;
|
|
951
|
+
}
|
|
952
|
+
insertsSince(checkpoint) {
|
|
953
|
+
const out = [];
|
|
954
|
+
for (const op of this.ops.slice(Math.max(0, checkpoint))) {
|
|
955
|
+
if (op.kind !== "insert")
|
|
956
|
+
continue;
|
|
957
|
+
const row = { ...op.data };
|
|
958
|
+
out.push({
|
|
959
|
+
table: op.table,
|
|
960
|
+
keys: Object.keys(row).filter((k) => !k.startsWith("_")),
|
|
961
|
+
row
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
return out;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function sameValue(a, b) {
|
|
968
|
+
if (a === b)
|
|
969
|
+
return true;
|
|
970
|
+
if ((typeof a === "bigint" || typeof a === "number") && (typeof b === "bigint" || typeof b === "number")) {
|
|
971
|
+
return BigInt(a) === BigInt(b);
|
|
972
|
+
}
|
|
973
|
+
return String(a) === String(b);
|
|
974
|
+
}
|
|
975
|
+
|
|
865
976
|
// src/runtime/runner.ts
|
|
866
977
|
import { getErrorMessage } from "@secondlayer/shared";
|
|
867
978
|
import { logger as logger4 } from "@secondlayer/shared/logger";
|
|
868
979
|
import {
|
|
869
980
|
clarityValueToJS,
|
|
870
981
|
deserializeCV as deserializeCV4,
|
|
871
|
-
toCamelCase as
|
|
982
|
+
toCamelCase as toCamelCase3
|
|
872
983
|
} from "@secondlayer/stacks/clarity";
|
|
873
984
|
|
|
874
985
|
// src/print-schema.ts
|
|
@@ -1253,6 +1364,561 @@ var COLUMN_TYPES = new Set([
|
|
|
1253
1364
|
"timestamp",
|
|
1254
1365
|
"jsonb"
|
|
1255
1366
|
]);
|
|
1367
|
+
function isColumnType(field) {
|
|
1368
|
+
return typeof field === "string" && COLUMN_TYPES.has(field);
|
|
1369
|
+
}
|
|
1370
|
+
function matchesField(field, value) {
|
|
1371
|
+
if (isColumnType(field)) {
|
|
1372
|
+
switch (field) {
|
|
1373
|
+
case "uint":
|
|
1374
|
+
case "int":
|
|
1375
|
+
return typeof value === "bigint" || typeof value === "number";
|
|
1376
|
+
case "boolean":
|
|
1377
|
+
return typeof value === "boolean";
|
|
1378
|
+
case "text":
|
|
1379
|
+
case "principal":
|
|
1380
|
+
case "timestamp":
|
|
1381
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "bigint";
|
|
1382
|
+
case "jsonb":
|
|
1383
|
+
return value !== undefined;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (typeof field !== "object" || field === null)
|
|
1387
|
+
return false;
|
|
1388
|
+
if ("optional" in field) {
|
|
1389
|
+
return value === undefined || value === null || matchesField(field.type, value);
|
|
1390
|
+
}
|
|
1391
|
+
if ("tuple" in field) {
|
|
1392
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
return matchesTopic(field.tuple, value).ok;
|
|
1396
|
+
}
|
|
1397
|
+
if ("list" in field) {
|
|
1398
|
+
return Array.isArray(value) && value.every((item) => matchesField(field.list, item));
|
|
1399
|
+
}
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
function matchesTopic(declared, data) {
|
|
1403
|
+
for (const [name, field] of Object.entries(declared)) {
|
|
1404
|
+
const present = name in data;
|
|
1405
|
+
const optional = typeof field === "object" && field !== null && "optional" in field;
|
|
1406
|
+
if (!present) {
|
|
1407
|
+
if (optional)
|
|
1408
|
+
continue;
|
|
1409
|
+
return { ok: false, reason: `missing declared field "${name}"` };
|
|
1410
|
+
}
|
|
1411
|
+
if (!matchesField(field, data[name])) {
|
|
1412
|
+
return {
|
|
1413
|
+
ok: false,
|
|
1414
|
+
reason: `field "${name}" does not match its declared type (got ${describe(data[name])})`
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return { ok: true };
|
|
1419
|
+
}
|
|
1420
|
+
function describe(value) {
|
|
1421
|
+
if (value === null)
|
|
1422
|
+
return "null";
|
|
1423
|
+
if (Array.isArray(value))
|
|
1424
|
+
return "array";
|
|
1425
|
+
return typeof value;
|
|
1426
|
+
}
|
|
1427
|
+
function validatePrintPayload(prints, topic, data) {
|
|
1428
|
+
if (!prints)
|
|
1429
|
+
return { ok: true };
|
|
1430
|
+
const declared = prints[topic];
|
|
1431
|
+
if (!declared)
|
|
1432
|
+
return { ok: true };
|
|
1433
|
+
return matchesTopic(declared, data);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// src/runtime/realm.ts
|
|
1437
|
+
import vm from "node:vm";
|
|
1438
|
+
|
|
1439
|
+
// src/verification.ts
|
|
1440
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1441
|
+
// package.json
|
|
1442
|
+
var package_default = {
|
|
1443
|
+
name: "@secondlayer/subgraphs",
|
|
1444
|
+
version: "6.5.0",
|
|
1445
|
+
description: "Describe the Stacks data you want as one TypeScript file; it fills itself and survives reorgs.",
|
|
1446
|
+
license: "MIT",
|
|
1447
|
+
repository: {
|
|
1448
|
+
type: "git",
|
|
1449
|
+
url: "git+https://github.com/ryanwaits/secondlayer.git",
|
|
1450
|
+
directory: "packages/subgraphs"
|
|
1451
|
+
},
|
|
1452
|
+
homepage: "https://github.com/ryanwaits/secondlayer/tree/main/packages/subgraphs#readme",
|
|
1453
|
+
type: "module",
|
|
1454
|
+
main: "./dist/index.js",
|
|
1455
|
+
types: "./dist/index.d.ts",
|
|
1456
|
+
exports: {
|
|
1457
|
+
".": {
|
|
1458
|
+
types: "./dist/index.d.ts",
|
|
1459
|
+
import: "./dist/index.js"
|
|
1460
|
+
},
|
|
1461
|
+
"./types": {
|
|
1462
|
+
types: "./dist/types.d.ts",
|
|
1463
|
+
import: "./dist/types.js"
|
|
1464
|
+
},
|
|
1465
|
+
"./schema": {
|
|
1466
|
+
types: "./dist/schema/index.d.ts",
|
|
1467
|
+
import: "./dist/schema/index.js"
|
|
1468
|
+
},
|
|
1469
|
+
"./validate": {
|
|
1470
|
+
types: "./dist/validate.d.ts",
|
|
1471
|
+
import: "./dist/validate.js"
|
|
1472
|
+
},
|
|
1473
|
+
"./verification": {
|
|
1474
|
+
types: "./dist/verification.d.ts",
|
|
1475
|
+
import: "./dist/verification.js"
|
|
1476
|
+
},
|
|
1477
|
+
"./runtime/replay": {
|
|
1478
|
+
types: "./dist/runtime/replay.d.ts",
|
|
1479
|
+
import: "./dist/runtime/replay.js"
|
|
1480
|
+
},
|
|
1481
|
+
"./runtime/emitter": {
|
|
1482
|
+
types: "./dist/runtime/emitter.d.ts",
|
|
1483
|
+
import: "./dist/runtime/emitter.js"
|
|
1484
|
+
},
|
|
1485
|
+
"./testing": {
|
|
1486
|
+
types: "./dist/testing/index.d.ts",
|
|
1487
|
+
import: "./dist/testing/index.js"
|
|
1488
|
+
},
|
|
1489
|
+
"./verify": {
|
|
1490
|
+
types: "./dist/verify/index.d.ts",
|
|
1491
|
+
import: "./dist/verify/index.js"
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
files: ["dist"],
|
|
1495
|
+
scripts: {
|
|
1496
|
+
build: "bunup",
|
|
1497
|
+
dev: "bunup --watch",
|
|
1498
|
+
test: "bun test",
|
|
1499
|
+
typecheck: "tsc --noEmit",
|
|
1500
|
+
prepublishOnly: "bun run build"
|
|
1501
|
+
},
|
|
1502
|
+
dependencies: {
|
|
1503
|
+
"@secondlayer/shared": "^11.23.0",
|
|
1504
|
+
"@secondlayer/stacks": "^6.2.0",
|
|
1505
|
+
"@secondlayer/verify": "^0.3.0",
|
|
1506
|
+
esbuild: "^0.28.2",
|
|
1507
|
+
kysely: "0.28.17",
|
|
1508
|
+
zod: "^4.3.6"
|
|
1509
|
+
},
|
|
1510
|
+
devDependencies: {
|
|
1511
|
+
"@types/bun": "latest",
|
|
1512
|
+
bunup: "0.16.32",
|
|
1513
|
+
typescript: "^5"
|
|
1514
|
+
},
|
|
1515
|
+
publishConfig: {
|
|
1516
|
+
access: "public"
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
// src/verification.ts
|
|
1521
|
+
var SUBGRAPHS_RUNTIME = `@secondlayer/subgraphs@${package_default.version}`;
|
|
1522
|
+
var FORBIDDEN_GLOBALS = {
|
|
1523
|
+
Date: "wall-clock time differs per run",
|
|
1524
|
+
performance: "timers differ per run",
|
|
1525
|
+
process: "host environment differs per run",
|
|
1526
|
+
fetch: "network reads are not chain inputs",
|
|
1527
|
+
crypto: "randomness differs per run",
|
|
1528
|
+
setTimeout: "scheduling order differs per run",
|
|
1529
|
+
setInterval: "scheduling order differs per run",
|
|
1530
|
+
setImmediate: "scheduling order differs per run",
|
|
1531
|
+
clearTimeout: "scheduling order differs per run",
|
|
1532
|
+
clearInterval: "scheduling order differs per run",
|
|
1533
|
+
queueMicrotask: "scheduling order differs per run",
|
|
1534
|
+
WeakRef: "garbage collection timing differs per run",
|
|
1535
|
+
FinalizationRegistry: "garbage collection timing differs per run",
|
|
1536
|
+
Atomics: "shared memory is not single-threaded",
|
|
1537
|
+
SharedArrayBuffer: "shared memory is not single-threaded",
|
|
1538
|
+
Intl: "locale and time zone data differ per host",
|
|
1539
|
+
Temporal: "locale and time zone data differ per host",
|
|
1540
|
+
eval: "generated code hides inputs from the scan",
|
|
1541
|
+
WebAssembly: "compiled code hides inputs from the scan",
|
|
1542
|
+
ShadowRealm: "a nested realm escapes the allow-list",
|
|
1543
|
+
require: "imports must be bundled into the handler"
|
|
1544
|
+
};
|
|
1545
|
+
var ALLOWED_GLOBALS = [
|
|
1546
|
+
"undefined",
|
|
1547
|
+
"NaN",
|
|
1548
|
+
"Infinity",
|
|
1549
|
+
"globalThis",
|
|
1550
|
+
"isNaN",
|
|
1551
|
+
"isFinite",
|
|
1552
|
+
"parseInt",
|
|
1553
|
+
"parseFloat",
|
|
1554
|
+
"decodeURI",
|
|
1555
|
+
"decodeURIComponent",
|
|
1556
|
+
"encodeURI",
|
|
1557
|
+
"encodeURIComponent",
|
|
1558
|
+
"escape",
|
|
1559
|
+
"unescape",
|
|
1560
|
+
"Object",
|
|
1561
|
+
"Function",
|
|
1562
|
+
"Array",
|
|
1563
|
+
"Number",
|
|
1564
|
+
"Boolean",
|
|
1565
|
+
"String",
|
|
1566
|
+
"Symbol",
|
|
1567
|
+
"BigInt",
|
|
1568
|
+
"Math",
|
|
1569
|
+
"JSON",
|
|
1570
|
+
"Reflect",
|
|
1571
|
+
"Proxy",
|
|
1572
|
+
"Promise",
|
|
1573
|
+
"Map",
|
|
1574
|
+
"Set",
|
|
1575
|
+
"WeakMap",
|
|
1576
|
+
"WeakSet",
|
|
1577
|
+
"RegExp",
|
|
1578
|
+
"Iterator",
|
|
1579
|
+
"DisposableStack",
|
|
1580
|
+
"AsyncDisposableStack",
|
|
1581
|
+
"Error",
|
|
1582
|
+
"AggregateError",
|
|
1583
|
+
"SuppressedError",
|
|
1584
|
+
"EvalError",
|
|
1585
|
+
"RangeError",
|
|
1586
|
+
"ReferenceError",
|
|
1587
|
+
"SyntaxError",
|
|
1588
|
+
"TypeError",
|
|
1589
|
+
"URIError",
|
|
1590
|
+
"ArrayBuffer",
|
|
1591
|
+
"DataView",
|
|
1592
|
+
"Int8Array",
|
|
1593
|
+
"Uint8Array",
|
|
1594
|
+
"Uint8ClampedArray",
|
|
1595
|
+
"Int16Array",
|
|
1596
|
+
"Uint16Array",
|
|
1597
|
+
"Int32Array",
|
|
1598
|
+
"Uint32Array",
|
|
1599
|
+
"Float16Array",
|
|
1600
|
+
"Float32Array",
|
|
1601
|
+
"Float64Array",
|
|
1602
|
+
"BigInt64Array",
|
|
1603
|
+
"BigUint64Array",
|
|
1604
|
+
"TextEncoder",
|
|
1605
|
+
"TextDecoder",
|
|
1606
|
+
"atob",
|
|
1607
|
+
"btoa",
|
|
1608
|
+
"console"
|
|
1609
|
+
];
|
|
1610
|
+
var ALLOWED_MATH = [
|
|
1611
|
+
"abs",
|
|
1612
|
+
"min",
|
|
1613
|
+
"max",
|
|
1614
|
+
"floor",
|
|
1615
|
+
"ceil",
|
|
1616
|
+
"trunc",
|
|
1617
|
+
"sign",
|
|
1618
|
+
"round",
|
|
1619
|
+
"E",
|
|
1620
|
+
"LN10",
|
|
1621
|
+
"LN2",
|
|
1622
|
+
"LOG10E",
|
|
1623
|
+
"LOG2E",
|
|
1624
|
+
"PI",
|
|
1625
|
+
"SQRT1_2",
|
|
1626
|
+
"SQRT2"
|
|
1627
|
+
];
|
|
1628
|
+
var LOCALE_METHODS = [
|
|
1629
|
+
"toLocaleString",
|
|
1630
|
+
"toLocaleDateString",
|
|
1631
|
+
"toLocaleTimeString",
|
|
1632
|
+
"toLocaleUpperCase",
|
|
1633
|
+
"toLocaleLowerCase",
|
|
1634
|
+
"localeCompare"
|
|
1635
|
+
];
|
|
1636
|
+
function formatFinding(f) {
|
|
1637
|
+
return `${f.file}:${f.line}:${f.column} ${f.name}: ${f.reason}`;
|
|
1638
|
+
}
|
|
1639
|
+
var STATE_SOURCE_TYPES = new Set([
|
|
1640
|
+
"var_set",
|
|
1641
|
+
"map_set",
|
|
1642
|
+
"map_delete"
|
|
1643
|
+
]);
|
|
1644
|
+
var LEVEL_RANK = { state: 0, events: 1, none: 2 };
|
|
1645
|
+
var MAX_LISTED_FINDINGS = 10;
|
|
1646
|
+
function deriveVerification(def, findings = []) {
|
|
1647
|
+
let level = "state";
|
|
1648
|
+
const raise = (to) => {
|
|
1649
|
+
if (LEVEL_RANK[to] > LEVEL_RANK[level])
|
|
1650
|
+
level = to;
|
|
1651
|
+
};
|
|
1652
|
+
const reasons = [];
|
|
1653
|
+
for (const [name, source] of Object.entries(def.sources ?? {})) {
|
|
1654
|
+
const filter = source;
|
|
1655
|
+
if (filter.trait) {
|
|
1656
|
+
raise("none");
|
|
1657
|
+
reasons.push(`source "${name}": trait scope needs a proven contract registry`);
|
|
1658
|
+
} else if (filter.type === "map_insert") {
|
|
1659
|
+
raise("events");
|
|
1660
|
+
reasons.push(`map_insert source "${name}": map_insert vs map_set needs event proofs`);
|
|
1661
|
+
} else if (!STATE_SOURCE_TYPES.has(filter.type)) {
|
|
1662
|
+
raise("events");
|
|
1663
|
+
reasons.push(`${filter.type} source "${name}" needs event proofs`);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (def.backfillMode === "concurrent") {
|
|
1667
|
+
raise("none");
|
|
1668
|
+
reasons.push("backfillMode concurrent: tip-first order is not chain order");
|
|
1669
|
+
}
|
|
1670
|
+
if (findings.some((f) => f.kind === "needs-events"))
|
|
1671
|
+
raise("events");
|
|
1672
|
+
for (const f of findings.slice(0, MAX_LISTED_FINDINGS)) {
|
|
1673
|
+
reasons.push(f.kind === "needs-events" ? `${formatFinding(f)} (needs event proofs)` : formatFinding(f));
|
|
1674
|
+
}
|
|
1675
|
+
if (findings.length > MAX_LISTED_FINDINGS) {
|
|
1676
|
+
reasons.push(`…and ${findings.length - MAX_LISTED_FINDINGS} more handler findings`);
|
|
1677
|
+
}
|
|
1678
|
+
return {
|
|
1679
|
+
level,
|
|
1680
|
+
reasons,
|
|
1681
|
+
unproven: reasons.length === 0 ? [
|
|
1682
|
+
"tx attribution of writes: event.tx, _tx_id (needs event proofs)",
|
|
1683
|
+
"intermediate writes within a block (needs event proofs)"
|
|
1684
|
+
] : []
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function isDeterminismViolation(v) {
|
|
1688
|
+
return v.level === "state" && v.reasons.length > 0;
|
|
1689
|
+
}
|
|
1690
|
+
function sha256(input) {
|
|
1691
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
1692
|
+
}
|
|
1693
|
+
function pinPreimage(input) {
|
|
1694
|
+
const fields = {
|
|
1695
|
+
handlerHash: sha256(input.handlerCode),
|
|
1696
|
+
network: input.network,
|
|
1697
|
+
runtime: input.runtime ?? SUBGRAPHS_RUNTIME,
|
|
1698
|
+
schemaHash: input.schemaHash,
|
|
1699
|
+
startBlock: input.startBlock ?? null
|
|
1700
|
+
};
|
|
1701
|
+
return JSON.stringify(fields);
|
|
1702
|
+
}
|
|
1703
|
+
function computePin(input) {
|
|
1704
|
+
return sha256(pinPreimage(input));
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/runtime/realm.ts
|
|
1708
|
+
class NondeterminismError extends Error {
|
|
1709
|
+
name = "NondeterminismError";
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
class HandlerReadError extends Error {
|
|
1713
|
+
name = "HandlerReadError";
|
|
1714
|
+
}
|
|
1715
|
+
function abortsBlock(err) {
|
|
1716
|
+
return err instanceof NondeterminismError || err instanceof HandlerReadError;
|
|
1717
|
+
}
|
|
1718
|
+
var HOST_GLOBALS = [
|
|
1719
|
+
"ArrayBuffer",
|
|
1720
|
+
"DataView",
|
|
1721
|
+
"Int8Array",
|
|
1722
|
+
"Uint8Array",
|
|
1723
|
+
"Uint8ClampedArray",
|
|
1724
|
+
"Int16Array",
|
|
1725
|
+
"Uint16Array",
|
|
1726
|
+
"Int32Array",
|
|
1727
|
+
"Uint32Array",
|
|
1728
|
+
"Float16Array",
|
|
1729
|
+
"Float32Array",
|
|
1730
|
+
"Float64Array",
|
|
1731
|
+
"BigInt64Array",
|
|
1732
|
+
"BigUint64Array",
|
|
1733
|
+
"TextEncoder",
|
|
1734
|
+
"TextDecoder",
|
|
1735
|
+
"atob",
|
|
1736
|
+
"btoa"
|
|
1737
|
+
];
|
|
1738
|
+
function forbid(name, reason) {
|
|
1739
|
+
return () => {
|
|
1740
|
+
throw new NondeterminismError(`${name}: ${reason}`);
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
function createRealm() {
|
|
1744
|
+
const host = globalThis;
|
|
1745
|
+
const sandbox = Object.create(null);
|
|
1746
|
+
for (const name of HOST_GLOBALS) {
|
|
1747
|
+
if (host[name] !== undefined)
|
|
1748
|
+
sandbox[name] = host[name];
|
|
1749
|
+
}
|
|
1750
|
+
const context = vm.createContext(sandbox, {
|
|
1751
|
+
name: "subgraph-handler",
|
|
1752
|
+
codeGeneration: { strings: false, wasm: false }
|
|
1753
|
+
});
|
|
1754
|
+
const g = vm.runInContext("globalThis", context);
|
|
1755
|
+
const allowed = new Set(ALLOWED_GLOBALS);
|
|
1756
|
+
for (const name of Object.getOwnPropertyNames(g)) {
|
|
1757
|
+
if (!allowed.has(name))
|
|
1758
|
+
delete g[name];
|
|
1759
|
+
}
|
|
1760
|
+
for (const [name, reason] of Object.entries(FORBIDDEN_GLOBALS)) {
|
|
1761
|
+
Object.defineProperty(g, name, { get: forbid(name, reason) });
|
|
1762
|
+
}
|
|
1763
|
+
const realmMath = g.Math;
|
|
1764
|
+
const safeMath = {};
|
|
1765
|
+
const allowedMath = new Set(ALLOWED_MATH);
|
|
1766
|
+
for (const key of Object.getOwnPropertyNames(realmMath)) {
|
|
1767
|
+
if (allowedMath.has(key))
|
|
1768
|
+
safeMath[key] = realmMath[key];
|
|
1769
|
+
else
|
|
1770
|
+
Object.defineProperty(safeMath, key, {
|
|
1771
|
+
get: forbid(`Math.${key}`, key === "random" ? "randomness differs per run" : "result is engine-approximated")
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
Object.defineProperty(g, "Math", { value: Object.freeze(safeMath) });
|
|
1775
|
+
for (const ctor of ["String", "Number", "BigInt", "Array", "Object"]) {
|
|
1776
|
+
const proto = g[ctor].prototype;
|
|
1777
|
+
for (const method of LOCALE_METHODS) {
|
|
1778
|
+
if (Object.prototype.hasOwnProperty.call(proto, method)) {
|
|
1779
|
+
Object.defineProperty(proto, method, {
|
|
1780
|
+
value: forbid(method, "output depends on the host's locale data")
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
Object.preventExtensions(g);
|
|
1786
|
+
return context;
|
|
1787
|
+
}
|
|
1788
|
+
function checkRowValues(where, values) {
|
|
1789
|
+
for (const [col, value] of Object.entries(values)) {
|
|
1790
|
+
checkValue(value, `${where}.${col}`);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
function checkValue(value, path) {
|
|
1794
|
+
switch (typeof value) {
|
|
1795
|
+
case "number":
|
|
1796
|
+
if (!Number.isSafeInteger(value)) {
|
|
1797
|
+
throw new TypeError(`${path}: ${value} is not a safe integer; verifiable subgraphs store bigint or integers only`);
|
|
1798
|
+
}
|
|
1799
|
+
return;
|
|
1800
|
+
case "function":
|
|
1801
|
+
case "symbol":
|
|
1802
|
+
throw new TypeError(`${path}: a ${typeof value} is not a row value`);
|
|
1803
|
+
case "object":
|
|
1804
|
+
if (value === null || ArrayBuffer.isView(value))
|
|
1805
|
+
return;
|
|
1806
|
+
if (Array.isArray(value)) {
|
|
1807
|
+
value.forEach((v, i) => checkValue(v, `${path}[${i}]`));
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
for (const [k, v] of Object.entries(value))
|
|
1811
|
+
checkValue(v, `${path}.${k}`);
|
|
1812
|
+
return;
|
|
1813
|
+
default:
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function deterministicContext(ctx, schema) {
|
|
1818
|
+
let pendingReads = 0;
|
|
1819
|
+
const knownTable = (table) => {
|
|
1820
|
+
if (!Object.prototype.hasOwnProperty.call(schema, table)) {
|
|
1821
|
+
throw new Error(`Table "${table}" not found in subgraph schema`);
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
const guard = (op) => {
|
|
1825
|
+
if (pendingReads > 0) {
|
|
1826
|
+
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`);
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
const read = async (op, run) => {
|
|
1830
|
+
guard(op);
|
|
1831
|
+
pendingReads++;
|
|
1832
|
+
try {
|
|
1833
|
+
return await run();
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
if (err instanceof NondeterminismError)
|
|
1836
|
+
throw err;
|
|
1837
|
+
throw new HandlerReadError(`ctx.${op} failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1838
|
+
} finally {
|
|
1839
|
+
pendingReads--;
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
return {
|
|
1843
|
+
get block() {
|
|
1844
|
+
return ctx.block;
|
|
1845
|
+
},
|
|
1846
|
+
get tx() {
|
|
1847
|
+
return ctx.tx;
|
|
1848
|
+
},
|
|
1849
|
+
get client() {
|
|
1850
|
+
throw new NondeterminismError("ctx.client: contract reads need re-execution proofs");
|
|
1851
|
+
},
|
|
1852
|
+
insert(table, row) {
|
|
1853
|
+
guard("insert");
|
|
1854
|
+
checkRowValues(`insert ${table}`, row);
|
|
1855
|
+
ctx.insert(table, row);
|
|
1856
|
+
},
|
|
1857
|
+
update(table, where, set) {
|
|
1858
|
+
guard("update");
|
|
1859
|
+
checkRowValues(`update ${table}`, where);
|
|
1860
|
+
checkRowValues(`update ${table}`, set);
|
|
1861
|
+
ctx.update(table, where, set);
|
|
1862
|
+
},
|
|
1863
|
+
upsert(table, key, row) {
|
|
1864
|
+
guard("upsert");
|
|
1865
|
+
checkRowValues(`upsert ${table}`, key);
|
|
1866
|
+
checkRowValues(`upsert ${table}`, row);
|
|
1867
|
+
ctx.upsert(table, key, row);
|
|
1868
|
+
},
|
|
1869
|
+
delete(table, where) {
|
|
1870
|
+
guard("delete");
|
|
1871
|
+
ctx.delete(table, where);
|
|
1872
|
+
},
|
|
1873
|
+
increment(table, key, deltas) {
|
|
1874
|
+
guard("increment");
|
|
1875
|
+
checkRowValues(`increment ${table}`, key);
|
|
1876
|
+
checkRowValues(`increment ${table}`, deltas);
|
|
1877
|
+
ctx.increment(table, key, deltas);
|
|
1878
|
+
},
|
|
1879
|
+
findOne: (table, where) => {
|
|
1880
|
+
knownTable(table);
|
|
1881
|
+
return read("findOne", () => ctx.findOne(table, where));
|
|
1882
|
+
},
|
|
1883
|
+
findMany: (table, ...rest) => {
|
|
1884
|
+
knownTable(table);
|
|
1885
|
+
return read("findMany", () => ctx.findMany(table, ...rest));
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
var realmDefinitions = new WeakSet;
|
|
1890
|
+
async function loadDeterministicDefinition(handlerCode) {
|
|
1891
|
+
const { transform } = await import("esbuild");
|
|
1892
|
+
const { code } = await transform(handlerCode, {
|
|
1893
|
+
format: "cjs",
|
|
1894
|
+
loader: "js",
|
|
1895
|
+
logLevel: "silent"
|
|
1896
|
+
});
|
|
1897
|
+
const context = createRealm();
|
|
1898
|
+
const evaluate = vm.runInContext(`(function (module, exports) {
|
|
1899
|
+
${code}
|
|
1900
|
+
})`, context, { filename: "handler.js" });
|
|
1901
|
+
const mod = { exports: {} };
|
|
1902
|
+
evaluate(mod, mod.exports);
|
|
1903
|
+
const def = mod.exports.default ?? mod.exports;
|
|
1904
|
+
const sources = structuredClone(def.sources);
|
|
1905
|
+
const schema = structuredClone(def.schema);
|
|
1906
|
+
const facades = new WeakMap;
|
|
1907
|
+
const handlers = {};
|
|
1908
|
+
for (const [name, handler] of Object.entries(def.handlers ?? {})) {
|
|
1909
|
+
handlers[name] = (event, ctx) => {
|
|
1910
|
+
let facade = facades.get(ctx);
|
|
1911
|
+
if (!facade) {
|
|
1912
|
+
facade = deterministicContext(ctx, schema);
|
|
1913
|
+
facades.set(ctx, facade);
|
|
1914
|
+
}
|
|
1915
|
+
return handler(event, facade);
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
const loaded = { ...def, sources, schema, handlers };
|
|
1919
|
+
realmDefinitions.add(loaded);
|
|
1920
|
+
return loaded;
|
|
1921
|
+
}
|
|
1256
1922
|
|
|
1257
1923
|
// src/runtime/source-matcher.ts
|
|
1258
1924
|
var patternCache = new Map;
|
|
@@ -1574,8 +2240,58 @@ function matchSources(sources, transactions, events, traitContracts = new Map, f
|
|
|
1574
2240
|
import { getTargetDb } from "@secondlayer/shared/db";
|
|
1575
2241
|
import { logger as logger3 } from "@secondlayer/shared/logger";
|
|
1576
2242
|
import { sql as sql2 } from "kysely";
|
|
2243
|
+
var VIOLATIONS_CAP_PER_SUBGRAPH = 100;
|
|
2244
|
+
function jsonSafe2(value) {
|
|
2245
|
+
if (typeof value === "bigint")
|
|
2246
|
+
return value.toString();
|
|
2247
|
+
if (value === null || value === undefined)
|
|
2248
|
+
return value;
|
|
2249
|
+
if (Array.isArray(value))
|
|
2250
|
+
return value.map(jsonSafe2);
|
|
2251
|
+
if (typeof value === "object") {
|
|
2252
|
+
const out = {};
|
|
2253
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2254
|
+
out[k] = jsonSafe2(v);
|
|
2255
|
+
}
|
|
2256
|
+
return out;
|
|
2257
|
+
}
|
|
2258
|
+
return value;
|
|
2259
|
+
}
|
|
2260
|
+
async function defaultRecordPrintViolation(input) {
|
|
2261
|
+
try {
|
|
2262
|
+
const db = getTargetDb();
|
|
2263
|
+
const sample = jsonSafe2(input.samplePayload);
|
|
2264
|
+
await db.insertInto("subgraph_violations").values({
|
|
2265
|
+
subgraph_name: input.subgraphName,
|
|
2266
|
+
source_name: input.sourceName,
|
|
2267
|
+
block_height: input.blockHeight,
|
|
2268
|
+
tx_id: input.txId,
|
|
2269
|
+
reason: input.reason,
|
|
2270
|
+
sample_payload: sample
|
|
2271
|
+
}).execute();
|
|
2272
|
+
await sql2`
|
|
2273
|
+
DELETE FROM subgraph_violations
|
|
2274
|
+
WHERE id IN (
|
|
2275
|
+
SELECT id FROM subgraph_violations
|
|
2276
|
+
WHERE subgraph_name = ${input.subgraphName}
|
|
2277
|
+
ORDER BY seen_at DESC
|
|
2278
|
+
OFFSET ${VIOLATIONS_CAP_PER_SUBGRAPH}
|
|
2279
|
+
)
|
|
2280
|
+
`.execute(db);
|
|
2281
|
+
} catch (err) {
|
|
2282
|
+
logger3.warn("Failed to record print violation", {
|
|
2283
|
+
subgraph: input.subgraphName,
|
|
2284
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
var recorder = defaultRecordPrintViolation;
|
|
2289
|
+
async function recordPrintViolation(input) {
|
|
2290
|
+
await recorder(input);
|
|
2291
|
+
}
|
|
1577
2292
|
|
|
1578
2293
|
// src/runtime/runner.ts
|
|
2294
|
+
var DEFAULT_ERROR_THRESHOLD = 50;
|
|
1579
2295
|
function decodeFunctionArgs(args) {
|
|
1580
2296
|
let parsed = args;
|
|
1581
2297
|
if (typeof parsed === "string") {
|
|
@@ -1645,7 +2361,7 @@ function buildContractCallInput(filter, tx) {
|
|
|
1645
2361
|
return;
|
|
1646
2362
|
try {
|
|
1647
2363
|
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1648
|
-
input[
|
|
2364
|
+
input[toCamelCase3(arg.name)] = decodeArg(arg.type, deserializeCV4(clean));
|
|
1649
2365
|
} catch {}
|
|
1650
2366
|
});
|
|
1651
2367
|
return input;
|
|
@@ -1858,9 +2574,144 @@ function buildEventPayload(filter, tx, event) {
|
|
|
1858
2574
|
};
|
|
1859
2575
|
}
|
|
1860
2576
|
}
|
|
2577
|
+
async function runHandlers(subgraph, matched, ctx, opts) {
|
|
2578
|
+
let processed = 0;
|
|
2579
|
+
let errors = 0;
|
|
2580
|
+
let skipped = 0;
|
|
2581
|
+
let delivered = 0;
|
|
2582
|
+
const threshold = opts?.errorThreshold ?? DEFAULT_ERROR_THRESHOLD;
|
|
2583
|
+
const filterLookup = new Map;
|
|
2584
|
+
if (!Array.isArray(subgraph.sources)) {
|
|
2585
|
+
for (const [name, filter] of Object.entries(subgraph.sources)) {
|
|
2586
|
+
filterLookup.set(name, filter);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
const units = [];
|
|
2590
|
+
for (const { tx, events, sourceName } of matched) {
|
|
2591
|
+
if (events.length === 0) {
|
|
2592
|
+
units.push({ tx, sourceName, event: null });
|
|
2593
|
+
} else {
|
|
2594
|
+
for (const event of events)
|
|
2595
|
+
units.push({ tx, sourceName, event });
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
const clockRank = (e) => e?.clock === "vm" ? 1 : 0;
|
|
2599
|
+
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));
|
|
2600
|
+
for (const { tx, event, sourceName } of units) {
|
|
2601
|
+
if (errors >= threshold) {
|
|
2602
|
+
logger4.error("Subgraph error threshold reached, skipping remaining events", {
|
|
2603
|
+
subgraph: subgraph.name,
|
|
2604
|
+
errors,
|
|
2605
|
+
threshold
|
|
2606
|
+
});
|
|
2607
|
+
return {
|
|
2608
|
+
processed,
|
|
2609
|
+
errors,
|
|
2610
|
+
delivered,
|
|
2611
|
+
...skipped > 0 ? { skipped } : {}
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
const filter = filterLookup.get(sourceName);
|
|
2615
|
+
const handler = subgraph.handlers[sourceName] ?? null;
|
|
2616
|
+
if (!handler) {
|
|
2617
|
+
logger4.warn("No handler found for source", {
|
|
2618
|
+
subgraph: subgraph.name,
|
|
2619
|
+
sourceName,
|
|
2620
|
+
txId: tx.tx_id
|
|
2621
|
+
});
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
ctx.setTx({
|
|
2625
|
+
txId: tx.tx_id,
|
|
2626
|
+
sender: tx.sender,
|
|
2627
|
+
type: tx.type,
|
|
2628
|
+
status: tx.status,
|
|
2629
|
+
contractId: tx.contract_id ?? null,
|
|
2630
|
+
functionName: tx.function_name ?? null
|
|
2631
|
+
});
|
|
2632
|
+
const checkpoint = ctx.opsCheckpoint();
|
|
2633
|
+
try {
|
|
2634
|
+
let payload;
|
|
2635
|
+
if (event === null) {
|
|
2636
|
+
payload = filter ? buildEventPayload(filter, tx, null) : {
|
|
2637
|
+
tx: {
|
|
2638
|
+
txId: tx.tx_id,
|
|
2639
|
+
sender: tx.sender,
|
|
2640
|
+
type: tx.type,
|
|
2641
|
+
status: tx.status,
|
|
2642
|
+
contractId: tx.contract_id,
|
|
2643
|
+
functionName: tx.function_name
|
|
2644
|
+
}
|
|
2645
|
+
};
|
|
2646
|
+
} else if (filter) {
|
|
2647
|
+
payload = buildEventPayload(filter, tx, event);
|
|
2648
|
+
} else {
|
|
2649
|
+
const decoded = decodeEventData(event.data);
|
|
2650
|
+
payload = {
|
|
2651
|
+
...decoded,
|
|
2652
|
+
_eventId: event.id,
|
|
2653
|
+
_eventType: event.type,
|
|
2654
|
+
_eventIndex: event.event_index,
|
|
2655
|
+
tx: {
|
|
2656
|
+
txId: tx.tx_id,
|
|
2657
|
+
sender: tx.sender,
|
|
2658
|
+
type: tx.type,
|
|
2659
|
+
status: tx.status,
|
|
2660
|
+
contractId: tx.contract_id,
|
|
2661
|
+
functionName: tx.function_name
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
|
|
2666
|
+
continue;
|
|
2667
|
+
}
|
|
2668
|
+
delivered++;
|
|
2669
|
+
if (event !== null && filter?.type === "print_event" && filter.prints) {
|
|
2670
|
+
const printPayload = payload;
|
|
2671
|
+
const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
|
|
2672
|
+
if (!verdict.ok) {
|
|
2673
|
+
skipped++;
|
|
2674
|
+
logger4.warn("Print payload does not match declared prints schema", {
|
|
2675
|
+
subgraph: subgraph.name,
|
|
2676
|
+
sourceName,
|
|
2677
|
+
topic: printPayload.topic,
|
|
2678
|
+
txId: tx.tx_id,
|
|
2679
|
+
reason: verdict.reason
|
|
2680
|
+
});
|
|
2681
|
+
recordPrintViolation({
|
|
2682
|
+
subgraphName: subgraph.name,
|
|
2683
|
+
sourceName,
|
|
2684
|
+
blockHeight: ctx.block.height,
|
|
2685
|
+
txId: tx.tx_id,
|
|
2686
|
+
reason: verdict.reason,
|
|
2687
|
+
samplePayload: {
|
|
2688
|
+
topic: printPayload.topic ?? "",
|
|
2689
|
+
data: printPayload.data ?? {}
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
await handler(payload, ctx);
|
|
2696
|
+
processed++;
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
if (abortsBlock(err))
|
|
2699
|
+
throw err;
|
|
2700
|
+
ctx.rollbackTo(checkpoint);
|
|
2701
|
+
errors++;
|
|
2702
|
+
logger4.error("Subgraph handler error", {
|
|
2703
|
+
subgraph: subgraph.name,
|
|
2704
|
+
sourceName,
|
|
2705
|
+
txId: tx.tx_id,
|
|
2706
|
+
...event !== null ? { eventId: event.id, eventType: event.type } : {},
|
|
2707
|
+
error: getErrorMessage(err)
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
return { processed, errors, delivered, ...skipped > 0 ? { skipped } : {} };
|
|
2712
|
+
}
|
|
1861
2713
|
|
|
1862
2714
|
// src/testing/harness.ts
|
|
1863
|
-
var TEST_SCHEMA_NAME = "subgraph_test";
|
|
1864
2715
|
function defaultBlock(overrides = {}) {
|
|
1865
2716
|
return {
|
|
1866
2717
|
height: 1,
|
|
@@ -1881,76 +2732,8 @@ function defaultTx(overrides = {}) {
|
|
|
1881
2732
|
...overrides
|
|
1882
2733
|
};
|
|
1883
2734
|
}
|
|
1884
|
-
|
|
1885
|
-
class InMemorySubgraphContext extends SubgraphContext {
|
|
1886
|
-
store = new Map;
|
|
1887
|
-
constructor(schema, block, tx) {
|
|
1888
|
-
super(undefined, TEST_SCHEMA_NAME, schema, block, tx, false);
|
|
1889
|
-
}
|
|
1890
|
-
async readRows(table, where, limit) {
|
|
1891
|
-
const rows = (this.store.get(table) ?? []).filter((row) => Object.entries(where).every(([k, v]) => sameValue(row[k], v)));
|
|
1892
|
-
return limit === undefined ? rows : rows.slice(0, limit);
|
|
1893
|
-
}
|
|
1894
|
-
async rowsOf(table) {
|
|
1895
|
-
return this.overlayMany(table, {}, await this.readRows(table, {}));
|
|
1896
|
-
}
|
|
1897
|
-
setReads(reads) {
|
|
1898
|
-
this._client = {
|
|
1899
|
-
contract(contractId, abi) {
|
|
1900
|
-
const camelToKebab = new Map;
|
|
1901
|
-
for (const fn of normalizeAbi2(abi).functions) {
|
|
1902
|
-
camelToKebab.set(toCamelCase3(fn.name), fn.name);
|
|
1903
|
-
}
|
|
1904
|
-
const read = new Proxy({}, {
|
|
1905
|
-
get(_target, prop) {
|
|
1906
|
-
const fnName = camelToKebab.get(prop) ?? prop;
|
|
1907
|
-
const key = `${contractId}.${fnName}`;
|
|
1908
|
-
return async () => {
|
|
1909
|
-
if (!(key in reads)) {
|
|
1910
|
-
throw new Error(`No stubbed chain read for "${key}" — pass it via createTestContext(schema, { reads: { "${key}": … } }).`);
|
|
1911
|
-
}
|
|
1912
|
-
return reads[key];
|
|
1913
|
-
};
|
|
1914
|
-
}
|
|
1915
|
-
});
|
|
1916
|
-
return { read };
|
|
1917
|
-
}
|
|
1918
|
-
};
|
|
1919
|
-
}
|
|
1920
|
-
async commitOps() {
|
|
1921
|
-
const tables = new Set;
|
|
1922
|
-
for (const op of this.ops)
|
|
1923
|
-
tables.add(op.table);
|
|
1924
|
-
for (const table of tables) {
|
|
1925
|
-
this.store.set(table, await this.rowsOf(table));
|
|
1926
|
-
}
|
|
1927
|
-
this.ops.length = 0;
|
|
1928
|
-
}
|
|
1929
|
-
insertsSince(checkpoint) {
|
|
1930
|
-
const out = [];
|
|
1931
|
-
for (const op of this.ops.slice(Math.max(0, checkpoint))) {
|
|
1932
|
-
if (op.kind !== "insert")
|
|
1933
|
-
continue;
|
|
1934
|
-
const row = { ...op.data };
|
|
1935
|
-
out.push({
|
|
1936
|
-
table: op.table,
|
|
1937
|
-
keys: Object.keys(row).filter((k) => !k.startsWith("_")),
|
|
1938
|
-
row
|
|
1939
|
-
});
|
|
1940
|
-
}
|
|
1941
|
-
return out;
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
function sameValue(a, b) {
|
|
1945
|
-
if (a === b)
|
|
1946
|
-
return true;
|
|
1947
|
-
if ((typeof a === "bigint" || typeof a === "number") && (typeof b === "bigint" || typeof b === "number")) {
|
|
1948
|
-
return BigInt(a) === BigInt(b);
|
|
1949
|
-
}
|
|
1950
|
-
return String(a) === String(b);
|
|
1951
|
-
}
|
|
1952
2735
|
function createTestContext(schema, options = {}) {
|
|
1953
|
-
const impl = new
|
|
2736
|
+
const impl = new MemorySubgraphContext(new MemoryStore, schema, defaultBlock(options.block), defaultTx(options.tx));
|
|
1954
2737
|
impl.setReads(options.reads ?? {});
|
|
1955
2738
|
const ctx = impl;
|
|
1956
2739
|
ctx.rows = (table) => impl.rowsOf(table);
|
|
@@ -2065,17 +2848,17 @@ function toContractCallPayload(row) {
|
|
|
2065
2848
|
}
|
|
2066
2849
|
};
|
|
2067
2850
|
}
|
|
2068
|
-
function
|
|
2851
|
+
function jsonSafe3(value) {
|
|
2069
2852
|
if (typeof value === "bigint")
|
|
2070
2853
|
return value.toString();
|
|
2071
2854
|
if (value === null || value === undefined)
|
|
2072
2855
|
return value;
|
|
2073
2856
|
if (Array.isArray(value))
|
|
2074
|
-
return value.map(
|
|
2857
|
+
return value.map(jsonSafe3);
|
|
2075
2858
|
if (typeof value === "object") {
|
|
2076
2859
|
const out = {};
|
|
2077
2860
|
for (const [k, v] of Object.entries(value)) {
|
|
2078
|
-
out[k] =
|
|
2861
|
+
out[k] = jsonSafe3(v);
|
|
2079
2862
|
}
|
|
2080
2863
|
return out;
|
|
2081
2864
|
}
|
|
@@ -2186,7 +2969,7 @@ async function runSubgraphTest(input) {
|
|
|
2186
2969
|
tables.push({
|
|
2187
2970
|
name: table,
|
|
2188
2971
|
rows: tableRows.length,
|
|
2189
|
-
...sample ? { sampleRow:
|
|
2972
|
+
...sample ? { sampleRow: jsonSafe3(sample) } : {}
|
|
2190
2973
|
});
|
|
2191
2974
|
}
|
|
2192
2975
|
const fetched = Object.values(events).reduce((n, r) => n + r.length, 0);
|
|
@@ -2215,7 +2998,7 @@ async function runSubgraphTest(input) {
|
|
|
2215
2998
|
matched,
|
|
2216
2999
|
written: 0,
|
|
2217
3000
|
tables,
|
|
2218
|
-
firstEvent: firstEvent ? { ...firstEvent, data:
|
|
3001
|
+
firstEvent: firstEvent ? { ...firstEvent, data: jsonSafe3(firstEvent.data) } : undefined,
|
|
2219
3002
|
hint: `${matched} event${matched === 1 ? "" : "s"} matched, but NO rows were written — the shape of the field-mapping bug that ships a 0-row subgraph.${keyPart} Map only observed keys; do not invent fields.`,
|
|
2220
3003
|
...traceFields
|
|
2221
3004
|
};
|
|
@@ -2229,7 +3012,7 @@ async function runSubgraphTest(input) {
|
|
|
2229
3012
|
matched,
|
|
2230
3013
|
written,
|
|
2231
3014
|
tables,
|
|
2232
|
-
firstEvent: firstEvent ? { ...firstEvent, data:
|
|
3015
|
+
firstEvent: firstEvent ? { ...firstEvent, data: jsonSafe3(firstEvent.data) } : undefined,
|
|
2233
3016
|
hint: `At least one matched event wrote 0 rows.${keyPart}`,
|
|
2234
3017
|
...traceFields
|
|
2235
3018
|
};
|
|
@@ -2239,7 +3022,7 @@ async function runSubgraphTest(input) {
|
|
|
2239
3022
|
matched,
|
|
2240
3023
|
written,
|
|
2241
3024
|
tables,
|
|
2242
|
-
firstEvent: firstEvent ? { ...firstEvent, data:
|
|
3025
|
+
firstEvent: firstEvent ? { ...firstEvent, data: jsonSafe3(firstEvent.data) } : undefined,
|
|
2243
3026
|
...traceFields
|
|
2244
3027
|
};
|
|
2245
3028
|
}
|
|
@@ -2285,5 +3068,5 @@ export {
|
|
|
2285
3068
|
toHandlerPayload
|
|
2286
3069
|
};
|
|
2287
3070
|
|
|
2288
|
-
//# debugId=
|
|
3071
|
+
//# debugId=13B7A8082FE1831F64756E2164756E21
|
|
2289
3072
|
//# sourceMappingURL=index.js.map
|