@secondlayer/subgraphs 4.1.4 → 4.2.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.
Files changed (41) hide show
  1. package/dist/src/index.d.ts +91 -31
  2. package/dist/src/index.js +3319 -3067
  3. package/dist/src/index.js.map +11 -9
  4. package/dist/src/runtime/block-processor.d.ts +72 -23
  5. package/dist/src/runtime/block-processor.js +867 -400
  6. package/dist/src/runtime/block-processor.js.map +9 -6
  7. package/dist/src/runtime/catchup.d.ts +72 -23
  8. package/dist/src/runtime/catchup.js +878 -411
  9. package/dist/src/runtime/catchup.js.map +9 -6
  10. package/dist/src/runtime/processor.js +943 -475
  11. package/dist/src/runtime/processor.js.map +10 -7
  12. package/dist/src/runtime/reindex.d.ts +72 -23
  13. package/dist/src/runtime/reindex.js +911 -444
  14. package/dist/src/runtime/reindex.js.map +9 -6
  15. package/dist/src/runtime/reorg.d.ts +72 -23
  16. package/dist/src/runtime/reorg.js +890 -422
  17. package/dist/src/runtime/reorg.js.map +10 -7
  18. package/dist/src/runtime/replay.js +12 -6
  19. package/dist/src/runtime/replay.js.map +4 -4
  20. package/dist/src/runtime/runner.d.ts +72 -23
  21. package/dist/src/runtime/runner.js +757 -28
  22. package/dist/src/runtime/runner.js.map +9 -5
  23. package/dist/src/runtime/source-matcher.d.ts +80 -24
  24. package/dist/src/runtime/source-matcher.js +12 -5
  25. package/dist/src/runtime/source-matcher.js.map +3 -3
  26. package/dist/src/schema/index.d.ts +72 -23
  27. package/dist/src/schema/index.js +115 -1
  28. package/dist/src/schema/index.js.map +3 -3
  29. package/dist/src/service.js +983 -515
  30. package/dist/src/service.js.map +10 -7
  31. package/dist/src/testing/index.d.ts +195 -24
  32. package/dist/src/testing/index.js +1049 -30
  33. package/dist/src/testing/index.js.map +10 -5
  34. package/dist/src/triggers/index.d.ts +83 -24
  35. package/dist/src/triggers/index.js +5 -2
  36. package/dist/src/triggers/index.js.map +3 -3
  37. package/dist/src/types.d.ts +73 -24
  38. package/dist/src/validate.d.ts +72 -23
  39. package/dist/src/validate.js +115 -1
  40. package/dist/src/validate.js.map +3 -3
  41. package/package.json +2 -2
@@ -942,15 +942,679 @@ function decodeFunctionArgs(args) {
942
942
  return args.map(decodeClarityValue);
943
943
  }
944
944
 
945
+ // src/runtime/source-matcher.ts
946
+ var patternCache = new Map;
947
+ function matchAny(value, pattern) {
948
+ return Array.isArray(pattern) ? pattern.some((p) => matchPattern(value, p)) : matchPattern(value, pattern);
949
+ }
950
+ function matchPattern(value, pattern) {
951
+ if (!pattern.includes("*"))
952
+ return value === pattern;
953
+ let re = patternCache.get(pattern);
954
+ if (!re) {
955
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
956
+ re = new RegExp(`^${regex}$`);
957
+ patternCache.set(pattern, re);
958
+ }
959
+ return re.test(value);
960
+ }
961
+ var EMPTY_SET = new Set;
962
+ function factoryAllows(filter, contractId, factoryContracts) {
963
+ if (!filter.factory)
964
+ return true;
965
+ if (!contractId)
966
+ return false;
967
+ return (factoryContracts.get(filter.factory.from) ?? EMPTY_SET).has(contractId);
968
+ }
969
+ function readPath(payload, path) {
970
+ let cur = payload;
971
+ for (const key of path.split(".")) {
972
+ if (cur === null || typeof cur !== "object")
973
+ return;
974
+ cur = cur[key];
975
+ }
976
+ return cur;
977
+ }
978
+ function traitAllows(filter, contractId, traitContracts) {
979
+ const trait = filter.trait;
980
+ if (!trait)
981
+ return true;
982
+ if (!contractId)
983
+ return false;
984
+ return (traitContracts.get(trait) ?? EMPTY_SET).has(contractId);
985
+ }
986
+ function assetContract(assetId) {
987
+ return assetId?.split("::")[0];
988
+ }
989
+ function printContractId(data) {
990
+ if (!data)
991
+ return;
992
+ const id = (typeof data.contract_identifier === "string" ? data.contract_identifier : undefined) ?? (typeof data.contract_id === "string" ? data.contract_id : undefined);
993
+ return id || undefined;
994
+ }
995
+ function matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts = new Map) {
996
+ const results = [];
997
+ switch (filter.type) {
998
+ case "stx_transfer":
999
+ case "stx_mint":
1000
+ case "stx_burn":
1001
+ case "stx_lock": {
1002
+ const eventType = `${filter.type}_event`;
1003
+ for (const tx of transactions) {
1004
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1005
+ const matched = txEvents.filter((e) => e.type === eventType);
1006
+ if (matched.length === 0)
1007
+ continue;
1008
+ const filtered = matched.filter((e) => {
1009
+ const data = e.data;
1010
+ if (!data)
1011
+ return false;
1012
+ if ("sender" in filter && filter.sender) {
1013
+ if (!matchPattern(data.sender, filter.sender))
1014
+ return false;
1015
+ }
1016
+ if ("recipient" in filter && filter.recipient) {
1017
+ if (!matchPattern(data.recipient, filter.recipient))
1018
+ return false;
1019
+ }
1020
+ if ("lockedAddress" in filter && filter.lockedAddress) {
1021
+ if (!matchPattern(data.locked_address, filter.lockedAddress))
1022
+ return false;
1023
+ }
1024
+ if ("minAmount" in filter && filter.minAmount !== undefined) {
1025
+ const amount = BigInt(data.amount ?? data.locked_amount ?? "0");
1026
+ if (amount < filter.minAmount)
1027
+ return false;
1028
+ }
1029
+ if ("maxAmount" in filter && filter.maxAmount !== undefined) {
1030
+ const amount = BigInt(data.amount ?? "0");
1031
+ if (amount > filter.maxAmount)
1032
+ return false;
1033
+ }
1034
+ return true;
1035
+ });
1036
+ if (filtered.length > 0) {
1037
+ results.push({ tx, events: filtered });
1038
+ }
1039
+ }
1040
+ break;
1041
+ }
1042
+ case "ft_transfer":
1043
+ case "ft_mint":
1044
+ case "ft_burn": {
1045
+ const eventType = `${filter.type}_event`;
1046
+ for (const tx of transactions) {
1047
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1048
+ const matched = txEvents.filter((e) => {
1049
+ if (e.type !== eventType)
1050
+ return false;
1051
+ const data = e.data;
1052
+ if (!data)
1053
+ return false;
1054
+ if (filter.assetIdentifier) {
1055
+ const assetId = data.asset_identifier;
1056
+ if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
1057
+ return false;
1058
+ }
1059
+ if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
1060
+ return false;
1061
+ if ("sender" in filter && filter.sender) {
1062
+ if (!matchPattern(data.sender, filter.sender))
1063
+ return false;
1064
+ }
1065
+ if ("recipient" in filter && filter.recipient) {
1066
+ if (!matchPattern(data.recipient, filter.recipient))
1067
+ return false;
1068
+ }
1069
+ if (filter.minAmount !== undefined) {
1070
+ const amount = BigInt(data.amount ?? "0");
1071
+ if (amount < filter.minAmount)
1072
+ return false;
1073
+ }
1074
+ return true;
1075
+ });
1076
+ if (matched.length > 0) {
1077
+ results.push({ tx, events: matched });
1078
+ }
1079
+ }
1080
+ break;
1081
+ }
1082
+ case "nft_transfer":
1083
+ case "nft_mint":
1084
+ case "nft_burn": {
1085
+ const eventType = `${filter.type}_event`;
1086
+ for (const tx of transactions) {
1087
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1088
+ const matched = txEvents.filter((e) => {
1089
+ if (e.type !== eventType)
1090
+ return false;
1091
+ const data = e.data;
1092
+ if (!data)
1093
+ return false;
1094
+ if (filter.assetIdentifier) {
1095
+ const assetId = data.asset_identifier;
1096
+ if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
1097
+ return false;
1098
+ }
1099
+ if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
1100
+ return false;
1101
+ if ("sender" in filter && filter.sender) {
1102
+ if (!matchPattern(data.sender, filter.sender))
1103
+ return false;
1104
+ }
1105
+ if ("recipient" in filter && filter.recipient) {
1106
+ if (!matchPattern(data.recipient, filter.recipient))
1107
+ return false;
1108
+ }
1109
+ return true;
1110
+ });
1111
+ if (matched.length > 0) {
1112
+ results.push({ tx, events: matched });
1113
+ }
1114
+ }
1115
+ break;
1116
+ }
1117
+ case "contract_call": {
1118
+ for (const tx of transactions) {
1119
+ if (tx.type !== "contract_call")
1120
+ continue;
1121
+ if (filter.contractId) {
1122
+ if (!tx.contract_id || !matchAny(tx.contract_id, filter.contractId))
1123
+ continue;
1124
+ }
1125
+ if (!factoryAllows(filter, tx.contract_id, factoryContracts))
1126
+ continue;
1127
+ if (filter.functionName) {
1128
+ if (!tx.function_name || !matchPattern(tx.function_name, filter.functionName))
1129
+ continue;
1130
+ }
1131
+ if (filter.caller) {
1132
+ if (!matchPattern(tx.sender, filter.caller))
1133
+ continue;
1134
+ }
1135
+ if (!traitAllows(filter, tx.contract_id, traitContracts))
1136
+ continue;
1137
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1138
+ results.push({ tx, events: txEvents });
1139
+ }
1140
+ break;
1141
+ }
1142
+ case "contract_deploy": {
1143
+ for (const tx of transactions) {
1144
+ if (tx.type !== "smart_contract")
1145
+ continue;
1146
+ if (filter.deployer) {
1147
+ if (!matchPattern(tx.sender, filter.deployer))
1148
+ continue;
1149
+ }
1150
+ if (filter.contractName) {
1151
+ const name = tx.contract_id?.split(".")[1] ?? "";
1152
+ if (!matchPattern(name, filter.contractName))
1153
+ continue;
1154
+ }
1155
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1156
+ results.push({ tx, events: txEvents });
1157
+ }
1158
+ break;
1159
+ }
1160
+ case "print_event": {
1161
+ for (const tx of transactions) {
1162
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1163
+ const matched = txEvents.filter((e) => {
1164
+ if (e.type !== "smart_contract_event" && e.type !== "contract_event")
1165
+ return false;
1166
+ const data = e.data;
1167
+ if (!data)
1168
+ return false;
1169
+ if (data.topic !== "print")
1170
+ return false;
1171
+ const contractId = printContractId(data);
1172
+ if (filter.contractId) {
1173
+ if (!contractId || !matchAny(contractId, filter.contractId))
1174
+ return false;
1175
+ }
1176
+ if (!traitAllows(filter, contractId, traitContracts))
1177
+ return false;
1178
+ if (!factoryAllows(filter, contractId, factoryContracts))
1179
+ return false;
1180
+ return true;
1181
+ });
1182
+ if (matched.length > 0) {
1183
+ results.push({ tx, events: matched });
1184
+ }
1185
+ }
1186
+ break;
1187
+ }
1188
+ }
1189
+ return results;
1190
+ }
1191
+ function matchSources(sources, transactions, events, traitContracts = new Map, factoryContracts = new Map) {
1192
+ const eventsByTx = new Map;
1193
+ for (const event of events) {
1194
+ const list = eventsByTx.get(event.tx_id) ?? [];
1195
+ list.push(event);
1196
+ eventsByTx.set(event.tx_id, list);
1197
+ }
1198
+ const seen = new Set;
1199
+ const results = [];
1200
+ for (const [sourceName, filter] of Object.entries(sources)) {
1201
+ const matches = matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts);
1202
+ for (const match of matches) {
1203
+ const dedupeKey = `${match.tx.tx_id}:${sourceName}`;
1204
+ if (!seen.has(dedupeKey)) {
1205
+ seen.add(dedupeKey);
1206
+ results.push({ ...match, sourceName });
1207
+ }
1208
+ }
1209
+ }
1210
+ return results;
1211
+ }
1212
+
945
1213
  // src/runtime/runner.ts
946
1214
  import { getErrorMessage } from "@secondlayer/shared";
947
- import { logger as logger3 } from "@secondlayer/shared/logger";
1215
+ import { logger as logger4 } from "@secondlayer/shared/logger";
948
1216
  import {
949
1217
  clarityValueToJS,
950
- deserializeCV as deserializeCV3,
1218
+ deserializeCV as deserializeCV4,
951
1219
  toCamelCase as toCamelCase2
952
1220
  } from "@secondlayer/stacks/clarity";
953
1221
 
1222
+ // src/print-schema.ts
1223
+ import {
1224
+ deserializeCV as deserializeCV3
1225
+ } from "@secondlayer/stacks/clarity";
1226
+ function camelizeDataKey(str) {
1227
+ return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
1228
+ }
1229
+ function camelizeKeys(obj) {
1230
+ if (obj === null || obj === undefined)
1231
+ return obj;
1232
+ if (typeof obj !== "object")
1233
+ return obj;
1234
+ if (Array.isArray(obj))
1235
+ return obj.map(camelizeKeys);
1236
+ const result = {};
1237
+ for (const [k, v] of Object.entries(obj)) {
1238
+ result[camelizeDataKey(k)] = camelizeKeys(v);
1239
+ }
1240
+ return result;
1241
+ }
1242
+ function cvToTree(cv) {
1243
+ switch (cv.type) {
1244
+ case "uint":
1245
+ case "int":
1246
+ return { kind: cv.type };
1247
+ case "true":
1248
+ case "false":
1249
+ return { kind: "bool" };
1250
+ case "address":
1251
+ case "contract":
1252
+ return { kind: "principal" };
1253
+ case "buffer":
1254
+ return { kind: "buffer", len: cv.value.length / 2 };
1255
+ case "ascii":
1256
+ return { kind: "ascii", len: cv.value.length };
1257
+ case "utf8":
1258
+ return { kind: "utf8", len: new TextEncoder().encode(cv.value).length };
1259
+ case "none":
1260
+ return { kind: "optional", inner: null };
1261
+ case "some":
1262
+ return { kind: "optional", inner: cvToTree(cv.value) };
1263
+ case "ok":
1264
+ return { kind: "response", ok: cvToTree(cv.value), err: null };
1265
+ case "err":
1266
+ return { kind: "response", ok: null, err: cvToTree(cv.value) };
1267
+ case "list": {
1268
+ let inner = null;
1269
+ for (const el of cv.value) {
1270
+ const t = cvToTree(el);
1271
+ inner = inner ? unify(inner, t) : t;
1272
+ }
1273
+ return { kind: "list", inner };
1274
+ }
1275
+ case "tuple": {
1276
+ const entries = new Map;
1277
+ for (const [k, v] of Object.entries(cv.value)) {
1278
+ entries.set(k, { tree: cvToTree(v), present: 1 });
1279
+ }
1280
+ return { kind: "tuple", count: 1, entries };
1281
+ }
1282
+ }
1283
+ }
1284
+ function unifyNullable(a, b) {
1285
+ if (!a)
1286
+ return b;
1287
+ if (!b)
1288
+ return a;
1289
+ return unify(a, b);
1290
+ }
1291
+ var UNION_KIND_ORDER = {
1292
+ uint: 0,
1293
+ int: 1,
1294
+ bool: 2,
1295
+ principal: 3,
1296
+ buffer: 4,
1297
+ ascii: 5,
1298
+ utf8: 6,
1299
+ list: 7,
1300
+ tuple: 8,
1301
+ response: 9
1302
+ };
1303
+ function makeUnion(members) {
1304
+ const sorted = [...members].sort((a, b) => (UNION_KIND_ORDER[a.kind] ?? 99) - (UNION_KIND_ORDER[b.kind] ?? 99));
1305
+ return { kind: "union", members: sorted };
1306
+ }
1307
+ function unify(a, b) {
1308
+ if (a.kind === "optional" || b.kind === "optional") {
1309
+ const ai = a.kind === "optional" ? a.inner : a;
1310
+ const bi = b.kind === "optional" ? b.inner : b;
1311
+ return { kind: "optional", inner: unifyNullable(ai, bi) };
1312
+ }
1313
+ if (a.kind === "union")
1314
+ return unionAdd(a.members, b);
1315
+ if (b.kind === "union")
1316
+ return unionAdd(b.members, a);
1317
+ if (a.kind !== b.kind)
1318
+ return makeUnion([a, b]);
1319
+ switch (a.kind) {
1320
+ case "uint":
1321
+ case "int":
1322
+ case "bool":
1323
+ case "principal":
1324
+ return a;
1325
+ case "buffer":
1326
+ case "ascii":
1327
+ case "utf8":
1328
+ return { kind: a.kind, len: Math.max(a.len, b.len) };
1329
+ case "list":
1330
+ return {
1331
+ kind: "list",
1332
+ inner: unifyNullable(a.inner, b.inner)
1333
+ };
1334
+ case "response": {
1335
+ const rb = b;
1336
+ return {
1337
+ kind: "response",
1338
+ ok: unifyNullable(a.ok, rb.ok),
1339
+ err: unifyNullable(a.err, rb.err)
1340
+ };
1341
+ }
1342
+ case "tuple": {
1343
+ const tb = b;
1344
+ const entries = new Map([...a.entries].map(([k, e]) => [k, { ...e }]));
1345
+ for (const [k, e] of tb.entries) {
1346
+ const existing = entries.get(k);
1347
+ entries.set(k, existing ? {
1348
+ tree: unify(existing.tree, e.tree),
1349
+ present: existing.present + e.present
1350
+ } : { ...e });
1351
+ }
1352
+ return { kind: "tuple", count: a.count + tb.count, entries };
1353
+ }
1354
+ }
1355
+ }
1356
+ function unionAdd(members, t) {
1357
+ if (t.kind === "union") {
1358
+ let acc = { kind: "union", members };
1359
+ for (const m of t.members)
1360
+ acc = unify(acc, m);
1361
+ return acc;
1362
+ }
1363
+ const next = [...members];
1364
+ for (let i = 0;i < next.length; i++) {
1365
+ const member = next[i];
1366
+ if (!member)
1367
+ continue;
1368
+ const merged = unify(member, t);
1369
+ if (merged.kind !== "union") {
1370
+ next[i] = merged;
1371
+ return makeUnion(next);
1372
+ }
1373
+ }
1374
+ next.push(t);
1375
+ return makeUnion(next);
1376
+ }
1377
+ function wrapOptional(t) {
1378
+ return t.kind === "optional" ? t : { kind: "optional", inner: t };
1379
+ }
1380
+ function renderClarity(t) {
1381
+ if (!t)
1382
+ return "?";
1383
+ switch (t.kind) {
1384
+ case "uint":
1385
+ case "int":
1386
+ case "bool":
1387
+ case "principal":
1388
+ return t.kind;
1389
+ case "buffer":
1390
+ return `(buff ${t.len})`;
1391
+ case "ascii":
1392
+ return `(string-ascii ${t.len})`;
1393
+ case "utf8":
1394
+ return `(string-utf8 ${t.len})`;
1395
+ case "optional":
1396
+ return `(optional ${renderClarity(t.inner)})`;
1397
+ case "list":
1398
+ return `(list ${renderClarity(t.inner)})`;
1399
+ case "response":
1400
+ return `(response ${renderClarity(t.ok)} ${renderClarity(t.err)})`;
1401
+ case "tuple": {
1402
+ const parts = [...t.entries].map(([k, e]) => {
1403
+ const tree = e.present < t.count ? wrapOptional(e.tree) : e.tree;
1404
+ return `(${k} ${renderClarity(tree)})`;
1405
+ });
1406
+ return `(tuple ${parts.join(" ")})`;
1407
+ }
1408
+ case "union":
1409
+ return t.members.map(renderClarity).join(" | ");
1410
+ }
1411
+ }
1412
+ function renderTs(t) {
1413
+ if (!t)
1414
+ return "unknown";
1415
+ switch (t.kind) {
1416
+ case "uint":
1417
+ case "int":
1418
+ return "bigint";
1419
+ case "bool":
1420
+ return "boolean";
1421
+ case "principal":
1422
+ case "buffer":
1423
+ case "ascii":
1424
+ case "utf8":
1425
+ return "string";
1426
+ case "optional":
1427
+ return t.inner ? `${renderTs(t.inner)} | null` : "unknown | null";
1428
+ case "list": {
1429
+ const inner = renderTs(t.inner);
1430
+ return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
1431
+ }
1432
+ case "response": {
1433
+ const sides = [...new Set([renderTs(t.ok), renderTs(t.err)])];
1434
+ return sides.join(" | ");
1435
+ }
1436
+ case "tuple": {
1437
+ const parts = [...t.entries].map(([k, e]) => {
1438
+ const opt = e.present < t.count ? "?" : "";
1439
+ return `${camelizeDataKey(k)}${opt}: ${renderTs(e.tree)}`;
1440
+ });
1441
+ return `{ ${parts.join("; ")} }`;
1442
+ }
1443
+ case "union":
1444
+ return [...new Set(t.members.map((m) => renderTs(m)))].join(" | ");
1445
+ }
1446
+ }
1447
+ function toColumnType(t) {
1448
+ if (!t)
1449
+ return "jsonb";
1450
+ switch (t.kind) {
1451
+ case "uint":
1452
+ return "uint";
1453
+ case "int":
1454
+ return "int";
1455
+ case "bool":
1456
+ return "boolean";
1457
+ case "principal":
1458
+ return "principal";
1459
+ case "buffer":
1460
+ case "ascii":
1461
+ case "utf8":
1462
+ return "text";
1463
+ case "list":
1464
+ case "tuple":
1465
+ return "jsonb";
1466
+ case "optional":
1467
+ return toColumnType(t.inner);
1468
+ case "response":
1469
+ return t.ok ? toColumnType(t.ok) : "jsonb";
1470
+ case "union":
1471
+ return "jsonb";
1472
+ }
1473
+ }
1474
+ var MAX_DECODED_PER_TOPIC_NEWEST = 75;
1475
+ var MAX_DECODED_PER_TOPIC_OLDEST = 25;
1476
+ function inferPrintTopics(samples) {
1477
+ const groups = new Map;
1478
+ for (const s of samples) {
1479
+ const group = groups.get(s.topic);
1480
+ if (group)
1481
+ group.push(s);
1482
+ else
1483
+ groups.set(s.topic, [s]);
1484
+ }
1485
+ const out = [];
1486
+ for (const [topic, rows] of groups) {
1487
+ let first = Number.POSITIVE_INFINITY;
1488
+ let last = Number.NEGATIVE_INFINITY;
1489
+ for (const r of rows) {
1490
+ if (r.blockHeight < first)
1491
+ first = r.blockHeight;
1492
+ if (r.blockHeight > last)
1493
+ last = r.blockHeight;
1494
+ }
1495
+ const withHex = [...rows].filter((r) => r.rawHex !== null).sort((a, b) => b.blockHeight - a.blockHeight);
1496
+ const budget = MAX_DECODED_PER_TOPIC_NEWEST + MAX_DECODED_PER_TOPIC_OLDEST;
1497
+ const picked = withHex.length <= budget ? withHex : [
1498
+ ...withHex.slice(0, MAX_DECODED_PER_TOPIC_NEWEST),
1499
+ ...withHex.slice(-MAX_DECODED_PER_TOPIC_OLDEST)
1500
+ ];
1501
+ const tuples = [];
1502
+ let decoded = 0;
1503
+ for (const p of picked) {
1504
+ try {
1505
+ const cv = deserializeCV3(p.rawHex);
1506
+ decoded++;
1507
+ if (cv.type === "tuple")
1508
+ tuples.push(cv);
1509
+ } catch {}
1510
+ }
1511
+ const nonTuple = decoded > 0 && tuples.length === 0;
1512
+ const fields = [];
1513
+ if (!nonTuple) {
1514
+ const stats = new Map;
1515
+ for (const t of tuples) {
1516
+ for (const [key, value] of Object.entries(t.value)) {
1517
+ if (key === "topic")
1518
+ continue;
1519
+ const tree = cvToTree(value);
1520
+ const existing = stats.get(key);
1521
+ if (existing) {
1522
+ existing.present++;
1523
+ existing.tree = unify(existing.tree, tree);
1524
+ if (value.type === "none")
1525
+ existing.noneCount++;
1526
+ if (value.type === "none" || value.type === "some") {
1527
+ existing.optionalSeen = true;
1528
+ }
1529
+ } else {
1530
+ stats.set(key, {
1531
+ tree,
1532
+ present: 1,
1533
+ noneCount: value.type === "none" ? 1 : 0,
1534
+ optionalSeen: value.type === "none" || value.type === "some"
1535
+ });
1536
+ }
1537
+ }
1538
+ }
1539
+ for (const [name, st] of [...stats].sort(([a], [b]) => a.localeCompare(b))) {
1540
+ const field = {
1541
+ name,
1542
+ camel_name: camelizeDataKey(name),
1543
+ clarity_type: renderClarity(st.tree),
1544
+ ts_type: renderTs(st.tree),
1545
+ column_type: toColumnType(st.tree),
1546
+ always_present: st.present === tuples.length
1547
+ };
1548
+ if (st.optionalSeen) {
1549
+ field.optional_some_rate = (st.present - st.noneCount) / st.present;
1550
+ }
1551
+ fields.push(field);
1552
+ }
1553
+ }
1554
+ out.push({
1555
+ topic,
1556
+ count: rows.length,
1557
+ first_height: first,
1558
+ last_height: last,
1559
+ non_tuple: nonTuple,
1560
+ fields
1561
+ });
1562
+ }
1563
+ return out.sort((a, b) => b.count - a.count);
1564
+ }
1565
+
1566
+ // src/runtime/materialize.ts
1567
+ function resolveColumn(col, event, tx, block) {
1568
+ if ("fromTx" in col) {
1569
+ return { ok: true, value: tx[col.fromTx] };
1570
+ }
1571
+ if ("fromBlock" in col) {
1572
+ return { ok: true, value: block[col.fromBlock] };
1573
+ }
1574
+ const data = event.data;
1575
+ const dataObj = data !== null && typeof data === "object" && !Array.isArray(data) ? data : undefined;
1576
+ if (dataObj && Object.prototype.hasOwnProperty.call(dataObj, col.from)) {
1577
+ return { ok: true, value: dataObj[col.from] };
1578
+ }
1579
+ if (col.from === "topic" && "topic" in event) {
1580
+ return { ok: true, value: event.topic };
1581
+ }
1582
+ if (Object.prototype.hasOwnProperty.call(event, col.from)) {
1583
+ return { ok: true, value: event[col.from] };
1584
+ }
1585
+ return { ok: false, missing: col.from };
1586
+ }
1587
+ function buildMaterializeRow(spec, event, ctx, tableColumns) {
1588
+ const row = {};
1589
+ for (const [colName, mapping] of Object.entries(spec.columns)) {
1590
+ const resolved = resolveColumn(mapping, event, ctx.tx, ctx.block);
1591
+ if (!resolved.ok) {
1592
+ const colDef = tableColumns[colName];
1593
+ if (colDef?.nullable) {
1594
+ row[colName] = null;
1595
+ continue;
1596
+ }
1597
+ return {
1598
+ ok: false,
1599
+ reason: `required materialize field "${resolved.missing}" missing for column "${colName}"`
1600
+ };
1601
+ }
1602
+ row[colName] = resolved.value ?? null;
1603
+ }
1604
+ return { ok: true, row };
1605
+ }
1606
+ function applyMaterializeInsert(spec, event, ctx, schema) {
1607
+ const table = schema[spec.table];
1608
+ if (!table) {
1609
+ return { ok: false, reason: `materialize table "${spec.table}" missing` };
1610
+ }
1611
+ const built = buildMaterializeRow(spec, event, ctx, table.columns);
1612
+ if (!built.ok)
1613
+ return built;
1614
+ ctx.insert(spec.table, built.row);
1615
+ return { ok: true };
1616
+ }
1617
+
954
1618
  // src/runtime/print-validate.ts
955
1619
  var COLUMN_TYPES = new Set([
956
1620
  "text",
@@ -1030,24 +1694,62 @@ function validatePrintPayload(prints, topic, data) {
1030
1694
  return matchesTopic(declared, data);
1031
1695
  }
1032
1696
 
1033
- // src/runtime/runner.ts
1034
- var DEFAULT_ERROR_THRESHOLD = 50;
1035
- function camelCase(str) {
1036
- return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
1697
+ // src/runtime/violations.ts
1698
+ import { getTargetDb } from "@secondlayer/shared/db";
1699
+ import { logger as logger3 } from "@secondlayer/shared/logger";
1700
+ import { sql as sql2 } from "kysely";
1701
+ var VIOLATIONS_CAP_PER_SUBGRAPH = 100;
1702
+ function jsonSafe2(value) {
1703
+ if (typeof value === "bigint")
1704
+ return value.toString();
1705
+ if (value === null || value === undefined)
1706
+ return value;
1707
+ if (Array.isArray(value))
1708
+ return value.map(jsonSafe2);
1709
+ if (typeof value === "object") {
1710
+ const out = {};
1711
+ for (const [k, v] of Object.entries(value)) {
1712
+ out[k] = jsonSafe2(v);
1713
+ }
1714
+ return out;
1715
+ }
1716
+ return value;
1037
1717
  }
1038
- function camelizeKeys(obj) {
1039
- if (obj === null || obj === undefined)
1040
- return obj;
1041
- if (typeof obj !== "object")
1042
- return obj;
1043
- if (Array.isArray(obj))
1044
- return obj.map(camelizeKeys);
1045
- const result = {};
1046
- for (const [k, v] of Object.entries(obj)) {
1047
- result[camelCase(k)] = camelizeKeys(v);
1718
+ async function defaultRecordPrintViolation(input) {
1719
+ try {
1720
+ const db = getTargetDb();
1721
+ const sample = jsonSafe2(input.samplePayload);
1722
+ await db.insertInto("subgraph_violations").values({
1723
+ subgraph_name: input.subgraphName,
1724
+ source_name: input.sourceName,
1725
+ block_height: input.blockHeight,
1726
+ tx_id: input.txId,
1727
+ reason: input.reason,
1728
+ sample_payload: sample
1729
+ }).execute();
1730
+ await sql2`
1731
+ DELETE FROM subgraph_violations
1732
+ WHERE id IN (
1733
+ SELECT id FROM subgraph_violations
1734
+ WHERE subgraph_name = ${input.subgraphName}
1735
+ ORDER BY seen_at DESC
1736
+ OFFSET ${VIOLATIONS_CAP_PER_SUBGRAPH}
1737
+ )
1738
+ `.execute(db);
1739
+ } catch (err) {
1740
+ logger3.warn("Failed to record print violation", {
1741
+ subgraph: input.subgraphName,
1742
+ error: err instanceof Error ? err.message : String(err)
1743
+ });
1048
1744
  }
1049
- return result;
1050
1745
  }
1746
+ var recorder = defaultRecordPrintViolation;
1747
+ async function recordPrintViolation(input) {
1748
+ await recorder(input);
1749
+ }
1750
+
1751
+ // src/runtime/runner.ts
1752
+ var DEFAULT_ERROR_THRESHOLD = 50;
1051
1753
  function decodeFunctionArgs2(args) {
1052
1754
  let parsed = args;
1053
1755
  if (typeof parsed === "string") {
@@ -1111,7 +1813,7 @@ function buildContractCallInput(filter, tx) {
1111
1813
  return;
1112
1814
  try {
1113
1815
  const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
1114
- input[toCamelCase2(arg.name)] = decodeArg(arg.type, deserializeCV3(clean));
1816
+ input[toCamelCase2(arg.name)] = decodeArg(arg.type, deserializeCV4(clean));
1115
1817
  } catch {}
1116
1818
  });
1117
1819
  return input;
@@ -1234,7 +1936,7 @@ function buildEventPayload(filter, tx, event) {
1234
1936
  const { topic: _, ...rest } = clarityObj ?? {};
1235
1937
  const data = Object.keys(rest).length > 0 ? camelizeKeys(rest) : rawValue && typeof rawValue !== "object" ? rawValue : {};
1236
1938
  return {
1237
- contractId: decoded.contract_identifier ?? tx.contract_id ?? "",
1939
+ contractId: printContractId(decoded) ?? tx.contract_id ?? "",
1238
1940
  topic,
1239
1941
  data: data ?? {},
1240
1942
  tx: txMeta
@@ -1286,372 +1988,137 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1286
1988
  const units = [];
1287
1989
  for (const { tx, events, sourceName } of matched) {
1288
1990
  if (events.length === 0) {
1289
- units.push({ tx, sourceName, event: null });
1290
- } else {
1291
- for (const event of events)
1292
- units.push({ tx, sourceName, event });
1293
- }
1294
- }
1295
- units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
1296
- for (const { tx, event, sourceName } of units) {
1297
- if (errors >= threshold) {
1298
- logger3.error("Subgraph error threshold reached, skipping remaining events", {
1299
- subgraph: subgraph.name,
1300
- errors,
1301
- threshold
1302
- });
1303
- return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1304
- }
1305
- const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
1306
- if (!handler) {
1307
- logger3.warn("No handler found for source", {
1308
- subgraph: subgraph.name,
1309
- sourceName,
1310
- txId: tx.tx_id
1311
- });
1312
- continue;
1313
- }
1314
- ctx.setTx({
1315
- txId: tx.tx_id,
1316
- sender: tx.sender,
1317
- type: tx.type,
1318
- status: tx.status,
1319
- contractId: tx.contract_id ?? null,
1320
- functionName: tx.function_name ?? null
1321
- });
1322
- const filter = filterLookup.get(sourceName);
1323
- const checkpoint = ctx.opsCheckpoint();
1324
- try {
1325
- let payload;
1326
- if (event === null) {
1327
- payload = filter ? buildEventPayload(filter, tx, null) : {
1328
- tx: {
1329
- txId: tx.tx_id,
1330
- sender: tx.sender,
1331
- type: tx.type,
1332
- status: tx.status,
1333
- contractId: tx.contract_id,
1334
- functionName: tx.function_name
1335
- }
1336
- };
1337
- } else if (filter) {
1338
- payload = buildEventPayload(filter, tx, event);
1339
- } else {
1340
- const decoded = decodeEventData(event.data);
1341
- payload = {
1342
- ...decoded,
1343
- _eventId: event.id,
1344
- _eventType: event.type,
1345
- _eventIndex: event.event_index,
1346
- tx: {
1347
- txId: tx.tx_id,
1348
- sender: tx.sender,
1349
- type: tx.type,
1350
- status: tx.status,
1351
- contractId: tx.contract_id,
1352
- functionName: tx.function_name
1353
- }
1354
- };
1355
- }
1356
- if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
1357
- continue;
1358
- }
1359
- if (event !== null && filter?.type === "print_event" && filter.prints) {
1360
- const printPayload = payload;
1361
- const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
1362
- if (!verdict.ok) {
1363
- skipped++;
1364
- logger3.warn("Print payload does not match declared prints schema", {
1365
- subgraph: subgraph.name,
1366
- sourceName,
1367
- topic: printPayload.topic,
1368
- txId: tx.tx_id,
1369
- reason: verdict.reason
1370
- });
1371
- continue;
1372
- }
1373
- }
1374
- await handler(payload, ctx);
1375
- processed++;
1376
- } catch (err) {
1377
- ctx.rollbackTo(checkpoint);
1378
- errors++;
1379
- logger3.error("Subgraph handler error", {
1380
- subgraph: subgraph.name,
1381
- sourceName,
1382
- txId: tx.tx_id,
1383
- ...event !== null ? { eventId: event.id, eventType: event.type } : {},
1384
- error: getErrorMessage(err)
1385
- });
1386
- }
1387
- }
1388
- return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1389
- }
1390
-
1391
- // src/runtime/source-matcher.ts
1392
- var patternCache = new Map;
1393
- function matchAny(value, pattern) {
1394
- return Array.isArray(pattern) ? pattern.some((p) => matchPattern(value, p)) : matchPattern(value, pattern);
1395
- }
1396
- function matchPattern(value, pattern) {
1397
- if (!pattern.includes("*"))
1398
- return value === pattern;
1399
- let re = patternCache.get(pattern);
1400
- if (!re) {
1401
- const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1402
- re = new RegExp(`^${regex}$`);
1403
- patternCache.set(pattern, re);
1404
- }
1405
- return re.test(value);
1406
- }
1407
- var EMPTY_SET = new Set;
1408
- function factoryAllows(filter, contractId, factoryContracts) {
1409
- if (!filter.factory)
1410
- return true;
1411
- if (!contractId)
1412
- return false;
1413
- return (factoryContracts.get(filter.factory.from) ?? EMPTY_SET).has(contractId);
1414
- }
1415
- function readPath(payload, path) {
1416
- let cur = payload;
1417
- for (const key of path.split(".")) {
1418
- if (cur === null || typeof cur !== "object")
1419
- return;
1420
- cur = cur[key];
1421
- }
1422
- return cur;
1423
- }
1424
- function traitAllows(filter, contractId, traitContracts) {
1425
- const trait = filter.trait;
1426
- if (!trait)
1427
- return true;
1428
- if (!contractId)
1429
- return false;
1430
- return (traitContracts.get(trait) ?? EMPTY_SET).has(contractId);
1431
- }
1432
- function assetContract(assetId) {
1433
- return assetId?.split("::")[0];
1434
- }
1435
- function matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts = new Map) {
1436
- const results = [];
1437
- switch (filter.type) {
1438
- case "stx_transfer":
1439
- case "stx_mint":
1440
- case "stx_burn":
1441
- case "stx_lock": {
1442
- const eventType = `${filter.type}_event`;
1443
- for (const tx of transactions) {
1444
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1445
- const matched = txEvents.filter((e) => e.type === eventType);
1446
- if (matched.length === 0)
1447
- continue;
1448
- const filtered = matched.filter((e) => {
1449
- const data = e.data;
1450
- if (!data)
1451
- return false;
1452
- if ("sender" in filter && filter.sender) {
1453
- if (!matchPattern(data.sender, filter.sender))
1454
- return false;
1455
- }
1456
- if ("recipient" in filter && filter.recipient) {
1457
- if (!matchPattern(data.recipient, filter.recipient))
1458
- return false;
1459
- }
1460
- if ("lockedAddress" in filter && filter.lockedAddress) {
1461
- if (!matchPattern(data.locked_address, filter.lockedAddress))
1462
- return false;
1463
- }
1464
- if ("minAmount" in filter && filter.minAmount !== undefined) {
1465
- const amount = BigInt(data.amount ?? data.locked_amount ?? "0");
1466
- if (amount < filter.minAmount)
1467
- return false;
1468
- }
1469
- if ("maxAmount" in filter && filter.maxAmount !== undefined) {
1470
- const amount = BigInt(data.amount ?? "0");
1471
- if (amount > filter.maxAmount)
1472
- return false;
1473
- }
1474
- return true;
1475
- });
1476
- if (filtered.length > 0) {
1477
- results.push({ tx, events: filtered });
1478
- }
1479
- }
1480
- break;
1991
+ units.push({ tx, sourceName, event: null });
1992
+ } else {
1993
+ for (const event of events)
1994
+ units.push({ tx, sourceName, event });
1481
1995
  }
1482
- case "ft_transfer":
1483
- case "ft_mint":
1484
- case "ft_burn": {
1485
- const eventType = `${filter.type}_event`;
1486
- for (const tx of transactions) {
1487
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1488
- const matched = txEvents.filter((e) => {
1489
- if (e.type !== eventType)
1490
- return false;
1491
- const data = e.data;
1492
- if (!data)
1493
- return false;
1494
- if (filter.assetIdentifier) {
1495
- const assetId = data.asset_identifier;
1496
- if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
1497
- return false;
1498
- }
1499
- if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
1500
- return false;
1501
- if ("sender" in filter && filter.sender) {
1502
- if (!matchPattern(data.sender, filter.sender))
1503
- return false;
1504
- }
1505
- if ("recipient" in filter && filter.recipient) {
1506
- if (!matchPattern(data.recipient, filter.recipient))
1507
- return false;
1508
- }
1509
- if (filter.minAmount !== undefined) {
1510
- const amount = BigInt(data.amount ?? "0");
1511
- if (amount < filter.minAmount)
1512
- return false;
1513
- }
1514
- return true;
1515
- });
1516
- if (matched.length > 0) {
1517
- results.push({ tx, events: matched });
1518
- }
1519
- }
1520
- break;
1996
+ }
1997
+ units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
1998
+ for (const { tx, event, sourceName } of units) {
1999
+ if (errors >= threshold) {
2000
+ logger4.error("Subgraph error threshold reached, skipping remaining events", {
2001
+ subgraph: subgraph.name,
2002
+ errors,
2003
+ threshold
2004
+ });
2005
+ return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1521
2006
  }
1522
- case "nft_transfer":
1523
- case "nft_mint":
1524
- case "nft_burn": {
1525
- const eventType = `${filter.type}_event`;
1526
- for (const tx of transactions) {
1527
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1528
- const matched = txEvents.filter((e) => {
1529
- if (e.type !== eventType)
1530
- return false;
1531
- const data = e.data;
1532
- if (!data)
1533
- return false;
1534
- if (filter.assetIdentifier) {
1535
- const assetId = data.asset_identifier;
1536
- if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
1537
- return false;
1538
- }
1539
- if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
1540
- return false;
1541
- if ("sender" in filter && filter.sender) {
1542
- if (!matchPattern(data.sender, filter.sender))
1543
- return false;
2007
+ const filter = filterLookup.get(sourceName);
2008
+ const materialize = filter && "materialize" in filter && filter.materialize !== undefined ? filter.materialize : undefined;
2009
+ const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
2010
+ if (!handler && !materialize) {
2011
+ logger4.warn("No handler found for source", {
2012
+ subgraph: subgraph.name,
2013
+ sourceName,
2014
+ txId: tx.tx_id
2015
+ });
2016
+ continue;
2017
+ }
2018
+ ctx.setTx({
2019
+ txId: tx.tx_id,
2020
+ sender: tx.sender,
2021
+ type: tx.type,
2022
+ status: tx.status,
2023
+ contractId: tx.contract_id ?? null,
2024
+ functionName: tx.function_name ?? null
2025
+ });
2026
+ const checkpoint = ctx.opsCheckpoint();
2027
+ try {
2028
+ let payload;
2029
+ if (event === null) {
2030
+ payload = filter ? buildEventPayload(filter, tx, null) : {
2031
+ tx: {
2032
+ txId: tx.tx_id,
2033
+ sender: tx.sender,
2034
+ type: tx.type,
2035
+ status: tx.status,
2036
+ contractId: tx.contract_id,
2037
+ functionName: tx.function_name
1544
2038
  }
1545
- if ("recipient" in filter && filter.recipient) {
1546
- if (!matchPattern(data.recipient, filter.recipient))
1547
- return false;
2039
+ };
2040
+ } else if (filter) {
2041
+ payload = buildEventPayload(filter, tx, event);
2042
+ } else {
2043
+ const decoded = decodeEventData(event.data);
2044
+ payload = {
2045
+ ...decoded,
2046
+ _eventId: event.id,
2047
+ _eventType: event.type,
2048
+ _eventIndex: event.event_index,
2049
+ tx: {
2050
+ txId: tx.tx_id,
2051
+ sender: tx.sender,
2052
+ type: tx.type,
2053
+ status: tx.status,
2054
+ contractId: tx.contract_id,
2055
+ functionName: tx.function_name
1548
2056
  }
1549
- return true;
1550
- });
1551
- if (matched.length > 0) {
1552
- results.push({ tx, events: matched });
1553
- }
2057
+ };
1554
2058
  }
1555
- break;
1556
- }
1557
- case "contract_call": {
1558
- for (const tx of transactions) {
1559
- if (tx.type !== "contract_call")
1560
- continue;
1561
- if (filter.contractId) {
1562
- if (!tx.contract_id || !matchAny(tx.contract_id, filter.contractId))
1563
- continue;
1564
- }
1565
- if (!factoryAllows(filter, tx.contract_id, factoryContracts))
1566
- continue;
1567
- if (filter.functionName) {
1568
- if (!tx.function_name || !matchPattern(tx.function_name, filter.functionName))
1569
- continue;
1570
- }
1571
- if (filter.caller) {
1572
- if (!matchPattern(tx.sender, filter.caller))
1573
- continue;
1574
- }
1575
- if (!traitAllows(filter, tx.contract_id, traitContracts))
1576
- continue;
1577
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1578
- results.push({ tx, events: txEvents });
2059
+ if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
2060
+ continue;
1579
2061
  }
1580
- break;
1581
- }
1582
- case "contract_deploy": {
1583
- for (const tx of transactions) {
1584
- if (tx.type !== "smart_contract")
2062
+ if (event !== null && filter?.type === "print_event" && filter.prints) {
2063
+ const printPayload = payload;
2064
+ const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
2065
+ if (!verdict.ok) {
2066
+ skipped++;
2067
+ logger4.warn("Print payload does not match declared prints schema", {
2068
+ subgraph: subgraph.name,
2069
+ sourceName,
2070
+ topic: printPayload.topic,
2071
+ txId: tx.tx_id,
2072
+ reason: verdict.reason
2073
+ });
2074
+ recordPrintViolation({
2075
+ subgraphName: subgraph.name,
2076
+ sourceName,
2077
+ blockHeight: ctx.block.height,
2078
+ txId: tx.tx_id,
2079
+ reason: verdict.reason,
2080
+ samplePayload: {
2081
+ topic: printPayload.topic ?? "",
2082
+ data: printPayload.data ?? {}
2083
+ }
2084
+ });
1585
2085
  continue;
1586
- if (filter.deployer) {
1587
- if (!matchPattern(tx.sender, filter.deployer))
1588
- continue;
1589
- }
1590
- if (filter.contractName) {
1591
- const name = tx.contract_id?.split(".")[1] ?? "";
1592
- if (!matchPattern(name, filter.contractName))
1593
- continue;
1594
2086
  }
1595
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1596
- results.push({ tx, events: txEvents });
1597
2087
  }
1598
- break;
1599
- }
1600
- case "print_event": {
1601
- for (const tx of transactions) {
1602
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
1603
- const matched = txEvents.filter((e) => {
1604
- if (e.type !== "smart_contract_event" && e.type !== "contract_event")
1605
- return false;
1606
- const data = e.data;
1607
- if (!data)
1608
- return false;
1609
- if (data.topic !== "print")
1610
- return false;
1611
- const printContractId = data.contract_identifier ?? data.contract_id;
1612
- if (filter.contractId) {
1613
- if (!printContractId || !matchAny(printContractId, filter.contractId))
1614
- return false;
1615
- }
1616
- if (!traitAllows(filter, printContractId, traitContracts))
1617
- return false;
1618
- if (!factoryAllows(filter, printContractId, factoryContracts))
1619
- return false;
1620
- return true;
1621
- });
1622
- if (matched.length > 0) {
1623
- results.push({ tx, events: matched });
2088
+ if (materialize) {
2089
+ const result = applyMaterializeInsert(materialize, payload, ctx, subgraph.schema);
2090
+ if (!result.ok) {
2091
+ skipped++;
2092
+ logger4.warn("Materialize skipped: required field missing", {
2093
+ subgraph: subgraph.name,
2094
+ sourceName,
2095
+ txId: tx.tx_id,
2096
+ reason: result.reason
2097
+ });
2098
+ continue;
1624
2099
  }
2100
+ processed++;
2101
+ } else if (handler) {
2102
+ await handler(payload, ctx);
2103
+ processed++;
1625
2104
  }
1626
- break;
1627
- }
1628
- }
1629
- return results;
1630
- }
1631
- function matchSources(sources, transactions, events, traitContracts = new Map, factoryContracts = new Map) {
1632
- const eventsByTx = new Map;
1633
- for (const event of events) {
1634
- const list = eventsByTx.get(event.tx_id) ?? [];
1635
- list.push(event);
1636
- eventsByTx.set(event.tx_id, list);
1637
- }
1638
- const seen = new Set;
1639
- const results = [];
1640
- for (const [sourceName, filter] of Object.entries(sources)) {
1641
- const matches = matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts);
1642
- for (const match of matches) {
1643
- const dedupeKey = `${match.tx.tx_id}:${sourceName}`;
1644
- if (!seen.has(dedupeKey)) {
1645
- seen.add(dedupeKey);
1646
- results.push({ ...match, sourceName });
1647
- }
2105
+ } catch (err) {
2106
+ ctx.rollbackTo(checkpoint);
2107
+ errors++;
2108
+ logger4.error("Subgraph handler error", {
2109
+ subgraph: subgraph.name,
2110
+ sourceName,
2111
+ txId: tx.tx_id,
2112
+ ...event !== null ? { eventId: event.id, eventType: event.type } : {},
2113
+ error: getErrorMessage(err)
2114
+ });
1648
2115
  }
1649
2116
  }
1650
- return results;
2117
+ return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1651
2118
  }
1652
2119
 
1653
2120
  // src/runtime/block-processor.ts
1654
- import { getTargetDb } from "@secondlayer/shared/db";
2121
+ import { getTargetDb as getTargetDb2 } from "@secondlayer/shared/db";
1655
2122
  import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contracts";
1656
2123
  import { recordGapBatch } from "@secondlayer/shared/db/queries/subgraph-gaps";
1657
2124
  import { advanceOperationCursor } from "@secondlayer/shared/db/queries/subgraph-operations";
@@ -1662,15 +2129,15 @@ import {
1662
2129
  rewindLiveProgress,
1663
2130
  updateSubgraphStatus
1664
2131
  } from "@secondlayer/shared/db/queries/subgraphs";
1665
- import { logger as logger7 } from "@secondlayer/shared/logger";
1666
- import { sql as sql3 } from "kysely";
2132
+ import { logger as logger8 } from "@secondlayer/shared/logger";
2133
+ import { sql as sql4 } from "kysely";
1667
2134
 
1668
2135
  // src/runtime/block-source.ts
1669
2136
  import { getSourceDb as getSourceDb2 } from "@secondlayer/shared/db";
1670
2137
  import {
1671
2138
  createInternalIndexHttpClient
1672
2139
  } from "@secondlayer/shared/index-http";
1673
- import { logger as logger4 } from "@secondlayer/shared/logger";
2140
+ import { logger as logger5 } from "@secondlayer/shared/logger";
1674
2141
 
1675
2142
  // src/runtime/batch-loader.ts
1676
2143
  async function loadBlockRange(db, fromHeight, toHeight) {
@@ -1990,7 +2457,7 @@ function reconstructEvent(e) {
1990
2457
  type: "contract_event",
1991
2458
  data: {
1992
2459
  topic: e.payload.topic,
1993
- contract_identifier: e.contract_id,
2460
+ contract_id: e.contract_id,
1994
2461
  value: e.payload.value,
1995
2462
  raw_value: e.payload.raw_value
1996
2463
  }
@@ -2157,7 +2624,7 @@ class FallbackBlockSource {
2157
2624
  try {
2158
2625
  return await this.primary.getTip();
2159
2626
  } catch (err) {
2160
- logger4.warn("block source primary getTip failed — using DB tap", {
2627
+ logger5.warn("block source primary getTip failed — using DB tap", {
2161
2628
  error: err instanceof Error ? err.message : String(err)
2162
2629
  });
2163
2630
  return this.fallback.getTip();
@@ -2167,7 +2634,7 @@ class FallbackBlockSource {
2167
2634
  try {
2168
2635
  return await this.primary.loadBlockRange(fromHeight, toHeight);
2169
2636
  } catch (err) {
2170
- logger4.warn("block source primary loadBlockRange failed — using DB tap", {
2637
+ logger5.warn("block source primary loadBlockRange failed — using DB tap", {
2171
2638
  from: fromHeight,
2172
2639
  to: toHeight,
2173
2640
  error: err instanceof Error ? err.message : String(err)
@@ -2204,7 +2671,7 @@ function resolveBlockSource(subgraph) {
2204
2671
  return new FallbackBlockSource(new PublicApiBlockSource(buildHttpClient(), referencedIndexEventTypes(subgraph), canSparseScan(subgraph) ? sparseProbeTargets(subgraph) : undefined, needsTransactionData(subgraph)), postgresBlockSource);
2205
2672
  }
2206
2673
  if (process.env.SUBGRAPH_SOURCE === "streams-index" && subgraph) {
2207
- logger4.debug("Subgraph not streams-index eligible, using DB tap", {
2674
+ logger5.debug("Subgraph not streams-index eligible, using DB tap", {
2208
2675
  subgraph: subgraph.name
2209
2676
  });
2210
2677
  }
@@ -2213,7 +2680,7 @@ function resolveBlockSource(subgraph) {
2213
2680
 
2214
2681
  // src/runtime/outbox-emit.ts
2215
2682
  import { createHash as createHash2 } from "node:crypto";
2216
- import { logger as logger5 } from "@secondlayer/shared/logger";
2683
+ import { logger as logger6 } from "@secondlayer/shared/logger";
2217
2684
  var loggedKillSwitch = false;
2218
2685
  var OP_VERB = {
2219
2686
  insert: "created",
@@ -2237,7 +2704,7 @@ function stableStringify(obj) {
2237
2704
  async function emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, blockHeight) {
2238
2705
  if (!isEmitOutboxEnabled()) {
2239
2706
  if (!loggedKillSwitch) {
2240
- logger5.warn("SECONDLAYER_EMIT_OUTBOX=false — outbox emission bypassed");
2707
+ logger6.warn("SECONDLAYER_EMIT_OUTBOX=false — outbox emission bypassed");
2241
2708
  loggedKillSwitch = true;
2242
2709
  }
2243
2710
  return 0;
@@ -2282,7 +2749,7 @@ function sandboxEnabled(subgraph) {
2282
2749
  // src/runtime/sandbox/host.ts
2283
2750
  import { tmpdir } from "node:os";
2284
2751
  import { fileURLToPath } from "node:url";
2285
- import { logger as logger6 } from "@secondlayer/shared/logger";
2752
+ import { logger as logger7 } from "@secondlayer/shared/logger";
2286
2753
 
2287
2754
  // src/runtime/sandbox/bundle.ts
2288
2755
  import esbuild from "esbuild";
@@ -2362,7 +2829,7 @@ function spawnSandboxProcess() {
2362
2829
  break;
2363
2830
  const text = decoder.decode(value, { stream: true });
2364
2831
  if (text.trim()) {
2365
- logger6.warn("Sandbox subprocess stderr", { stderr: text.trim() });
2832
+ logger7.warn("Sandbox subprocess stderr", { stderr: text.trim() });
2366
2833
  }
2367
2834
  }
2368
2835
  } catch {}
@@ -2420,7 +2887,7 @@ async function ensureSandboxProcess(subgraphName, version, handlerCode) {
2420
2887
  try {
2421
2888
  await initSandboxProcess(existing.proc, subgraphName, version, handlerCode);
2422
2889
  existing.version = version;
2423
- logger6.info("Sandbox subprocess handler reloaded", {
2890
+ logger7.info("Sandbox subprocess handler reloaded", {
2424
2891
  subgraph: subgraphName,
2425
2892
  version
2426
2893
  });
@@ -2428,7 +2895,7 @@ async function ensureSandboxProcess(subgraphName, version, handlerCode) {
2428
2895
  } catch (err) {
2429
2896
  existing.proc.kill();
2430
2897
  pool.delete(subgraphName);
2431
- logger6.warn("Sandbox subprocess re-init failed; respawning", {
2898
+ logger7.warn("Sandbox subprocess re-init failed; respawning", {
2432
2899
  subgraph: subgraphName,
2433
2900
  error: err instanceof Error ? err.message : String(err)
2434
2901
  });
@@ -2511,7 +2978,7 @@ async function runBlockThroughProcess(entry, subgraphName, hostCtx, block, match
2511
2978
  return result;
2512
2979
  } catch (err) {
2513
2980
  evictSandboxWorker(subgraphName);
2514
- logger6.warn("Sandbox subprocess run failed; subprocess evicted", {
2981
+ logger7.warn("Sandbox subprocess run failed; subprocess evicted", {
2515
2982
  subgraph: subgraphName,
2516
2983
  blockHeight: block.height,
2517
2984
  error: err instanceof Error ? err.message : String(err)
@@ -2582,7 +3049,7 @@ async function answerRead(hostCtx, msg) {
2582
3049
 
2583
3050
  // src/runtime/subscription-state.ts
2584
3051
  import { listSubscriptions } from "@secondlayer/shared/db/queries/subscriptions";
2585
- import { sql as sql2 } from "kysely";
3052
+ import { sql as sql3 } from "kysely";
2586
3053
 
2587
3054
  // src/runtime/emitter-matcher.ts
2588
3055
  function isPrimitive(v) {
@@ -2746,7 +3213,7 @@ class SubscriptionMatcher {
2746
3213
  // src/runtime/subscription-state.ts
2747
3214
  var matcher = new SubscriptionMatcher;
2748
3215
  async function refreshMatcher(db) {
2749
- const rows = await sql2`
3216
+ const rows = await sql3`
2750
3217
  SELECT * FROM subscriptions WHERE status = 'active'
2751
3218
  `.execute(db);
2752
3219
  matcher.setAll(rows.rows);
@@ -2768,7 +3235,7 @@ async function resolveRoute(subgraphName, targetDb) {
2768
3235
  version: row?.version ?? ""
2769
3236
  };
2770
3237
  routeCache.set(subgraphName, route);
2771
- logger7.info("subgraph handler dispatch path resolved", {
3238
+ logger8.info("subgraph handler dispatch path resolved", {
2772
3239
  subgraph: subgraphName,
2773
3240
  path: sandboxEnabled({ sandbox_workers: route.sandboxWorkers }) ? "sandbox" : "in-process"
2774
3241
  });
@@ -2792,8 +3259,8 @@ async function resolveFactoryContracts(subgraph, blockHeight, schemaName, db, tx
2792
3259
  for (const [discoveringSource, factory] of factories) {
2793
3260
  const known = new Set;
2794
3261
  try {
2795
- const rows = await sql3`
2796
- SELECT address FROM ${sql3.raw(`"${schemaName}"."_factory_addresses"`)}
3262
+ const rows = await sql4`
3263
+ SELECT address FROM ${sql4.raw(`"${schemaName}"."_factory_addresses"`)}
2797
3264
  WHERE source_name = ${discoveringSource} AND block_height <= ${blockHeight}
2798
3265
  `.execute(db);
2799
3266
  for (const row of rows.rows)
@@ -2821,8 +3288,8 @@ async function persistFactoryDiscoveries(schemaName, db, blockHeight, discovered
2821
3288
  if (discovered.length === 0)
2822
3289
  return;
2823
3290
  for (const { sourceName, address } of discovered) {
2824
- await sql3`
2825
- INSERT INTO ${sql3.raw(`"${schemaName}"."_factory_addresses"`)}
3291
+ await sql4`
3292
+ INSERT INTO ${sql4.raw(`"${schemaName}"."_factory_addresses"`)}
2826
3293
  (source_name, address, block_height)
2827
3294
  VALUES (${sourceName}, ${address}, ${blockHeight})
2828
3295
  ON CONFLICT (source_name, address) DO NOTHING
@@ -2882,7 +3349,7 @@ async function processBlockWithRetry(subgraph, subgraphName, blockHeight, opts,
2882
3349
  const delay = retryDelaysMs[attempt];
2883
3350
  if (delay === undefined)
2884
3351
  break;
2885
- logger7.warn("Block processing failed, retrying", {
3352
+ logger8.warn("Block processing failed, retrying", {
2886
3353
  subgraph: subgraphName,
2887
3354
  blockHeight,
2888
3355
  attempt: attempt + 1,
@@ -2895,7 +3362,7 @@ async function processBlockWithRetry(subgraph, subgraphName, blockHeight, opts,
2895
3362
  throw lastError;
2896
3363
  }
2897
3364
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2898
- const targetDb = getTargetDb();
3365
+ const targetDb = getTargetDb2();
2899
3366
  const blockStart = performance.now();
2900
3367
  const result = {
2901
3368
  blockHeight,
@@ -2914,7 +3381,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2914
3381
  } else {
2915
3382
  const data = (await resolveBlockSource(subgraph).loadBlockRange(blockHeight, blockHeight)).get(blockHeight);
2916
3383
  if (!data) {
2917
- logger7.debug("Block not found or non-canonical for subgraph processing", {
3384
+ logger8.debug("Block not found or non-canonical for subgraph processing", {
2918
3385
  subgraph: subgraphName,
2919
3386
  blockHeight
2920
3387
  });
@@ -2980,7 +3447,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2980
3447
  { start: blockHeight, end: blockHeight, reason: "processing_error" }
2981
3448
  ]);
2982
3449
  } else {
2983
- logger7.warn("Block errors not recorded as a gap — no subgraphId", {
3450
+ logger8.warn("Block errors not recorded as a gap — no subgraphId", {
2984
3451
  subgraph: subgraphName,
2985
3452
  blockHeight,
2986
3453
  errors: rr.errors
@@ -3053,7 +3520,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
3053
3520
  });
3054
3521
  } catch (err) {
3055
3522
  if (err instanceof CursorRaceLostError || err instanceof LiveCursorRaceLostError) {
3056
- logger7.warn("cursor race lost — block already covered", {
3523
+ logger8.warn("cursor race lost — block already covered", {
3057
3524
  subgraph: subgraphName,
3058
3525
  blockHeight,
3059
3526
  error: err.message
@@ -3078,10 +3545,10 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
3078
3545
  try {
3079
3546
  const tables = Object.keys(subgraph.schema);
3080
3547
  for (const table of tables) {
3081
- const { rows } = await sql3.raw(`SELECT n_live_tup AS count FROM pg_stat_user_tables WHERE schemaname = '${schemaName}' AND relname = '${table}'`).execute(route.dataDb);
3548
+ const { rows } = await sql4.raw(`SELECT n_live_tup AS count FROM pg_stat_user_tables WHERE schemaname = '${schemaName}' AND relname = '${table}'`).execute(route.dataDb);
3082
3549
  const count = Number(rows[0]?.count ?? 0);
3083
3550
  if (count >= 1e7) {
3084
- logger7.warn("Subgraph table exceeds 10M rows (estimate)", {
3551
+ logger8.warn("Subgraph table exceeds 10M rows (estimate)", {
3085
3552
  subgraph: subgraphName,
3086
3553
  table,
3087
3554
  count
@@ -3089,13 +3556,13 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
3089
3556
  }
3090
3557
  }
3091
3558
  } catch (err) {
3092
- logger7.debug("Row count sample failed", {
3559
+ logger8.debug("Row count sample failed", {
3093
3560
  subgraph: subgraphName,
3094
3561
  error: err instanceof Error ? err.message : String(err)
3095
3562
  });
3096
3563
  }
3097
3564
  if (journalEnabled(opts)) {
3098
- await sql3.raw(`DELETE FROM "${schemaName}"."_journal" WHERE "block_height" < ${blockHeight - JOURNAL_RETENTION_BLOCKS}`).execute(route.dataDb).catch(() => {});
3565
+ await sql4.raw(`DELETE FROM "${schemaName}"."_journal" WHERE "block_height" < ${blockHeight - JOURNAL_RETENTION_BLOCKS}`).execute(route.dataDb).catch(() => {});
3099
3566
  }
3100
3567
  }
3101
3568
  return result;
@@ -3108,5 +3575,5 @@ export {
3108
3575
  BLOCK_RETRY_DELAYS_MS
3109
3576
  };
3110
3577
 
3111
- //# debugId=ABF6EBB4150E31CC64756E2164756E21
3578
+ //# debugId=224EDC1D6107B84664756E2164756E21
3112
3579
  //# sourceMappingURL=block-processor.js.map