@secondlayer/subgraphs 6.4.0 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.d.ts +121 -121
- package/dist/index.js +262 -27
- package/dist/index.js.map +9 -8
- package/dist/runtime/emitter.js +1 -1
- package/dist/runtime/emitter.js.map +2 -2
- package/dist/runtime/replay.d.ts +32 -13
- package/dist/runtime/replay.js +723 -22
- package/dist/runtime/replay.js.map +8 -5
- package/dist/schema/index.d.ts +39 -39
- package/dist/schema/index.js +27 -14
- package/dist/schema/index.js.map +5 -5
- package/dist/testing/index.d.ts +105 -105
- package/dist/testing/index.js +406 -95
- package/dist/testing/index.js.map +7 -6
- package/dist/types.d.ts +31 -31
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +27 -27
- package/dist/validate.js +1 -1
- package/dist/validate.js.map +2 -2
- package/dist/verification.d.ts +41 -26
- package/dist/verification.js +23 -13
- package/dist/verification.js.map +4 -4
- package/dist/verify/index.d.ts +475 -0
- package/dist/verify/index.js +3341 -0
- package/dist/verify/index.js.map +27 -0
- package/package.json +9 -3
package/dist/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";
|
|
@@ -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,74 @@ 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
|
+
}
|
|
1256
1435
|
|
|
1257
1436
|
// src/runtime/realm.ts
|
|
1258
1437
|
import vm from "node:vm";
|
|
@@ -1262,7 +1441,7 @@ import { createHash as createHash2 } from "node:crypto";
|
|
|
1262
1441
|
// package.json
|
|
1263
1442
|
var package_default = {
|
|
1264
1443
|
name: "@secondlayer/subgraphs",
|
|
1265
|
-
version: "6.
|
|
1444
|
+
version: "6.5.0",
|
|
1266
1445
|
description: "Describe the Stacks data you want as one TypeScript file; it fills itself and survives reorgs.",
|
|
1267
1446
|
license: "MIT",
|
|
1268
1447
|
repository: {
|
|
@@ -1306,6 +1485,10 @@ var package_default = {
|
|
|
1306
1485
|
"./testing": {
|
|
1307
1486
|
types: "./dist/testing/index.d.ts",
|
|
1308
1487
|
import: "./dist/testing/index.js"
|
|
1488
|
+
},
|
|
1489
|
+
"./verify": {
|
|
1490
|
+
types: "./dist/verify/index.d.ts",
|
|
1491
|
+
import: "./dist/verify/index.js"
|
|
1309
1492
|
}
|
|
1310
1493
|
},
|
|
1311
1494
|
files: ["dist"],
|
|
@@ -1317,14 +1500,16 @@ var package_default = {
|
|
|
1317
1500
|
prepublishOnly: "bun run build"
|
|
1318
1501
|
},
|
|
1319
1502
|
dependencies: {
|
|
1320
|
-
"@secondlayer/shared": "^11.
|
|
1321
|
-
"@secondlayer/stacks": "^6.
|
|
1503
|
+
"@secondlayer/shared": "^11.23.0",
|
|
1504
|
+
"@secondlayer/stacks": "^6.2.0",
|
|
1505
|
+
"@secondlayer/verify": "^0.3.0",
|
|
1322
1506
|
esbuild: "^0.28.2",
|
|
1323
1507
|
kysely: "0.28.17",
|
|
1324
1508
|
zod: "^4.3.6"
|
|
1325
1509
|
},
|
|
1326
1510
|
devDependencies: {
|
|
1327
1511
|
"@types/bun": "latest",
|
|
1512
|
+
bunup: "0.16.32",
|
|
1328
1513
|
typescript: "^5"
|
|
1329
1514
|
},
|
|
1330
1515
|
publishConfig: {
|
|
@@ -1454,7 +1639,6 @@ function formatFinding(f) {
|
|
|
1454
1639
|
var STATE_SOURCE_TYPES = new Set([
|
|
1455
1640
|
"var_set",
|
|
1456
1641
|
"map_set",
|
|
1457
|
-
"map_insert",
|
|
1458
1642
|
"map_delete"
|
|
1459
1643
|
]);
|
|
1460
1644
|
var LEVEL_RANK = { state: 0, events: 1, none: 2 };
|
|
@@ -1471,6 +1655,9 @@ function deriveVerification(def, findings = []) {
|
|
|
1471
1655
|
if (filter.trait) {
|
|
1472
1656
|
raise("none");
|
|
1473
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`);
|
|
1474
1661
|
} else if (!STATE_SOURCE_TYPES.has(filter.type)) {
|
|
1475
1662
|
raise("events");
|
|
1476
1663
|
reasons.push(`${filter.type} source "${name}" needs event proofs`);
|
|
@@ -1488,14 +1675,12 @@ function deriveVerification(def, findings = []) {
|
|
|
1488
1675
|
if (findings.length > MAX_LISTED_FINDINGS) {
|
|
1489
1676
|
reasons.push(`…and ${findings.length - MAX_LISTED_FINDINGS} more handler findings`);
|
|
1490
1677
|
}
|
|
1491
|
-
const writesMapInsert = Object.values(def.sources ?? {}).some((s) => s.type === "map_insert");
|
|
1492
1678
|
return {
|
|
1493
1679
|
level,
|
|
1494
1680
|
reasons,
|
|
1495
1681
|
unproven: reasons.length === 0 ? [
|
|
1496
1682
|
"tx attribution of writes: event.tx, _tx_id (needs event proofs)",
|
|
1497
|
-
"intermediate writes within a block (needs event proofs)"
|
|
1498
|
-
...writesMapInsert ? ["map_insert vs map_set (needs event proofs)"] : []
|
|
1683
|
+
"intermediate writes within a block (needs event proofs)"
|
|
1499
1684
|
] : []
|
|
1500
1685
|
};
|
|
1501
1686
|
}
|
|
@@ -1505,15 +1690,18 @@ function isDeterminismViolation(v) {
|
|
|
1505
1690
|
function sha256(input) {
|
|
1506
1691
|
return createHash2("sha256").update(input).digest("hex");
|
|
1507
1692
|
}
|
|
1508
|
-
function
|
|
1509
|
-
const
|
|
1693
|
+
function pinPreimage(input) {
|
|
1694
|
+
const fields = {
|
|
1510
1695
|
handlerHash: sha256(input.handlerCode),
|
|
1511
1696
|
network: input.network,
|
|
1512
1697
|
runtime: input.runtime ?? SUBGRAPHS_RUNTIME,
|
|
1513
1698
|
schemaHash: input.schemaHash,
|
|
1514
1699
|
startBlock: input.startBlock ?? null
|
|
1515
|
-
}
|
|
1516
|
-
return
|
|
1700
|
+
};
|
|
1701
|
+
return JSON.stringify(fields);
|
|
1702
|
+
}
|
|
1703
|
+
function computePin(input) {
|
|
1704
|
+
return sha256(pinPreimage(input));
|
|
1517
1705
|
}
|
|
1518
1706
|
|
|
1519
1707
|
// src/runtime/realm.ts
|
|
@@ -1524,6 +1712,9 @@ class NondeterminismError extends Error {
|
|
|
1524
1712
|
class HandlerReadError extends Error {
|
|
1525
1713
|
name = "HandlerReadError";
|
|
1526
1714
|
}
|
|
1715
|
+
function abortsBlock(err) {
|
|
1716
|
+
return err instanceof NondeterminismError || err instanceof HandlerReadError;
|
|
1717
|
+
}
|
|
1527
1718
|
var HOST_GLOBALS = [
|
|
1528
1719
|
"ArrayBuffer",
|
|
1529
1720
|
"DataView",
|
|
@@ -1695,6 +1886,7 @@ function deterministicContext(ctx, schema) {
|
|
|
1695
1886
|
}
|
|
1696
1887
|
};
|
|
1697
1888
|
}
|
|
1889
|
+
var realmDefinitions = new WeakSet;
|
|
1698
1890
|
async function loadDeterministicDefinition(handlerCode) {
|
|
1699
1891
|
const { transform } = await import("esbuild");
|
|
1700
1892
|
const { code } = await transform(handlerCode, {
|
|
@@ -1723,7 +1915,9 @@ ${code}
|
|
|
1723
1915
|
return handler(event, facade);
|
|
1724
1916
|
};
|
|
1725
1917
|
}
|
|
1726
|
-
|
|
1918
|
+
const loaded = { ...def, sources, schema, handlers };
|
|
1919
|
+
realmDefinitions.add(loaded);
|
|
1920
|
+
return loaded;
|
|
1727
1921
|
}
|
|
1728
1922
|
|
|
1729
1923
|
// src/runtime/source-matcher.ts
|
|
@@ -2046,8 +2240,58 @@ function matchSources(sources, transactions, events, traitContracts = new Map, f
|
|
|
2046
2240
|
import { getTargetDb } from "@secondlayer/shared/db";
|
|
2047
2241
|
import { logger as logger3 } from "@secondlayer/shared/logger";
|
|
2048
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
|
+
}
|
|
2049
2292
|
|
|
2050
2293
|
// src/runtime/runner.ts
|
|
2294
|
+
var DEFAULT_ERROR_THRESHOLD = 50;
|
|
2051
2295
|
function decodeFunctionArgs(args) {
|
|
2052
2296
|
let parsed = args;
|
|
2053
2297
|
if (typeof parsed === "string") {
|
|
@@ -2117,7 +2361,7 @@ function buildContractCallInput(filter, tx) {
|
|
|
2117
2361
|
return;
|
|
2118
2362
|
try {
|
|
2119
2363
|
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
2120
|
-
input[
|
|
2364
|
+
input[toCamelCase3(arg.name)] = decodeArg(arg.type, deserializeCV4(clean));
|
|
2121
2365
|
} catch {}
|
|
2122
2366
|
});
|
|
2123
2367
|
return input;
|
|
@@ -2330,9 +2574,144 @@ function buildEventPayload(filter, tx, event) {
|
|
|
2330
2574
|
};
|
|
2331
2575
|
}
|
|
2332
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
|
+
}
|
|
2333
2713
|
|
|
2334
2714
|
// src/testing/harness.ts
|
|
2335
|
-
var TEST_SCHEMA_NAME = "subgraph_test";
|
|
2336
2715
|
function defaultBlock(overrides = {}) {
|
|
2337
2716
|
return {
|
|
2338
2717
|
height: 1,
|
|
@@ -2353,76 +2732,8 @@ function defaultTx(overrides = {}) {
|
|
|
2353
2732
|
...overrides
|
|
2354
2733
|
};
|
|
2355
2734
|
}
|
|
2356
|
-
|
|
2357
|
-
class InMemorySubgraphContext extends SubgraphContext {
|
|
2358
|
-
store = new Map;
|
|
2359
|
-
constructor(schema, block, tx) {
|
|
2360
|
-
super(undefined, TEST_SCHEMA_NAME, schema, block, tx, false);
|
|
2361
|
-
}
|
|
2362
|
-
async readRows(table, where, limit) {
|
|
2363
|
-
const rows = (this.store.get(table) ?? []).filter((row) => Object.entries(where).every(([k, v]) => sameValue(row[k], v)));
|
|
2364
|
-
return limit === undefined ? rows : rows.slice(0, limit);
|
|
2365
|
-
}
|
|
2366
|
-
async rowsOf(table) {
|
|
2367
|
-
return this.overlayMany(table, {}, await this.readRows(table, {}));
|
|
2368
|
-
}
|
|
2369
|
-
setReads(reads) {
|
|
2370
|
-
this._client = {
|
|
2371
|
-
contract(contractId, abi) {
|
|
2372
|
-
const camelToKebab = new Map;
|
|
2373
|
-
for (const fn of normalizeAbi2(abi).functions) {
|
|
2374
|
-
camelToKebab.set(toCamelCase3(fn.name), fn.name);
|
|
2375
|
-
}
|
|
2376
|
-
const read = new Proxy({}, {
|
|
2377
|
-
get(_target, prop) {
|
|
2378
|
-
const fnName = camelToKebab.get(prop) ?? prop;
|
|
2379
|
-
const key = `${contractId}.${fnName}`;
|
|
2380
|
-
return async () => {
|
|
2381
|
-
if (!(key in reads)) {
|
|
2382
|
-
throw new Error(`No stubbed chain read for "${key}" — pass it via createTestContext(schema, { reads: { "${key}": … } }).`);
|
|
2383
|
-
}
|
|
2384
|
-
return reads[key];
|
|
2385
|
-
};
|
|
2386
|
-
}
|
|
2387
|
-
});
|
|
2388
|
-
return { read };
|
|
2389
|
-
}
|
|
2390
|
-
};
|
|
2391
|
-
}
|
|
2392
|
-
async commitOps() {
|
|
2393
|
-
const tables = new Set;
|
|
2394
|
-
for (const op of this.ops)
|
|
2395
|
-
tables.add(op.table);
|
|
2396
|
-
for (const table of tables) {
|
|
2397
|
-
this.store.set(table, await this.rowsOf(table));
|
|
2398
|
-
}
|
|
2399
|
-
this.ops.length = 0;
|
|
2400
|
-
}
|
|
2401
|
-
insertsSince(checkpoint) {
|
|
2402
|
-
const out = [];
|
|
2403
|
-
for (const op of this.ops.slice(Math.max(0, checkpoint))) {
|
|
2404
|
-
if (op.kind !== "insert")
|
|
2405
|
-
continue;
|
|
2406
|
-
const row = { ...op.data };
|
|
2407
|
-
out.push({
|
|
2408
|
-
table: op.table,
|
|
2409
|
-
keys: Object.keys(row).filter((k) => !k.startsWith("_")),
|
|
2410
|
-
row
|
|
2411
|
-
});
|
|
2412
|
-
}
|
|
2413
|
-
return out;
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
function sameValue(a, b) {
|
|
2417
|
-
if (a === b)
|
|
2418
|
-
return true;
|
|
2419
|
-
if ((typeof a === "bigint" || typeof a === "number") && (typeof b === "bigint" || typeof b === "number")) {
|
|
2420
|
-
return BigInt(a) === BigInt(b);
|
|
2421
|
-
}
|
|
2422
|
-
return String(a) === String(b);
|
|
2423
|
-
}
|
|
2424
2735
|
function createTestContext(schema, options = {}) {
|
|
2425
|
-
const impl = new
|
|
2736
|
+
const impl = new MemorySubgraphContext(new MemoryStore, schema, defaultBlock(options.block), defaultTx(options.tx));
|
|
2426
2737
|
impl.setReads(options.reads ?? {});
|
|
2427
2738
|
const ctx = impl;
|
|
2428
2739
|
ctx.rows = (table) => impl.rowsOf(table);
|
|
@@ -2537,17 +2848,17 @@ function toContractCallPayload(row) {
|
|
|
2537
2848
|
}
|
|
2538
2849
|
};
|
|
2539
2850
|
}
|
|
2540
|
-
function
|
|
2851
|
+
function jsonSafe3(value) {
|
|
2541
2852
|
if (typeof value === "bigint")
|
|
2542
2853
|
return value.toString();
|
|
2543
2854
|
if (value === null || value === undefined)
|
|
2544
2855
|
return value;
|
|
2545
2856
|
if (Array.isArray(value))
|
|
2546
|
-
return value.map(
|
|
2857
|
+
return value.map(jsonSafe3);
|
|
2547
2858
|
if (typeof value === "object") {
|
|
2548
2859
|
const out = {};
|
|
2549
2860
|
for (const [k, v] of Object.entries(value)) {
|
|
2550
|
-
out[k] =
|
|
2861
|
+
out[k] = jsonSafe3(v);
|
|
2551
2862
|
}
|
|
2552
2863
|
return out;
|
|
2553
2864
|
}
|
|
@@ -2658,7 +2969,7 @@ async function runSubgraphTest(input) {
|
|
|
2658
2969
|
tables.push({
|
|
2659
2970
|
name: table,
|
|
2660
2971
|
rows: tableRows.length,
|
|
2661
|
-
...sample ? { sampleRow:
|
|
2972
|
+
...sample ? { sampleRow: jsonSafe3(sample) } : {}
|
|
2662
2973
|
});
|
|
2663
2974
|
}
|
|
2664
2975
|
const fetched = Object.values(events).reduce((n, r) => n + r.length, 0);
|
|
@@ -2687,7 +2998,7 @@ async function runSubgraphTest(input) {
|
|
|
2687
2998
|
matched,
|
|
2688
2999
|
written: 0,
|
|
2689
3000
|
tables,
|
|
2690
|
-
firstEvent: firstEvent ? { ...firstEvent, data:
|
|
3001
|
+
firstEvent: firstEvent ? { ...firstEvent, data: jsonSafe3(firstEvent.data) } : undefined,
|
|
2691
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.`,
|
|
2692
3003
|
...traceFields
|
|
2693
3004
|
};
|
|
@@ -2701,7 +3012,7 @@ async function runSubgraphTest(input) {
|
|
|
2701
3012
|
matched,
|
|
2702
3013
|
written,
|
|
2703
3014
|
tables,
|
|
2704
|
-
firstEvent: firstEvent ? { ...firstEvent, data:
|
|
3015
|
+
firstEvent: firstEvent ? { ...firstEvent, data: jsonSafe3(firstEvent.data) } : undefined,
|
|
2705
3016
|
hint: `At least one matched event wrote 0 rows.${keyPart}`,
|
|
2706
3017
|
...traceFields
|
|
2707
3018
|
};
|
|
@@ -2711,7 +3022,7 @@ async function runSubgraphTest(input) {
|
|
|
2711
3022
|
matched,
|
|
2712
3023
|
written,
|
|
2713
3024
|
tables,
|
|
2714
|
-
firstEvent: firstEvent ? { ...firstEvent, data:
|
|
3025
|
+
firstEvent: firstEvent ? { ...firstEvent, data: jsonSafe3(firstEvent.data) } : undefined,
|
|
2715
3026
|
...traceFields
|
|
2716
3027
|
};
|
|
2717
3028
|
}
|
|
@@ -2757,5 +3068,5 @@ export {
|
|
|
2757
3068
|
toHandlerPayload
|
|
2758
3069
|
};
|
|
2759
3070
|
|
|
2760
|
-
//# debugId=
|
|
3071
|
+
//# debugId=13B7A8082FE1831F64756E2164756E21
|
|
2761
3072
|
//# sourceMappingURL=index.js.map
|