@secondlayer/subgraphs 4.1.5 → 4.2.1

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 (39) hide show
  1. package/dist/src/index.d.ts +87 -29
  2. package/dist/src/index.js +376 -128
  3. package/dist/src/index.js.map +9 -7
  4. package/dist/src/runtime/block-processor.d.ts +72 -23
  5. package/dist/src/runtime/block-processor.js +186 -53
  6. package/dist/src/runtime/block-processor.js.map +7 -5
  7. package/dist/src/runtime/catchup.d.ts +72 -23
  8. package/dist/src/runtime/catchup.js +201 -68
  9. package/dist/src/runtime/catchup.js.map +7 -5
  10. package/dist/src/runtime/emitter.js +24 -1
  11. package/dist/src/runtime/emitter.js.map +5 -4
  12. package/dist/src/runtime/processor.js +328 -166
  13. package/dist/src/runtime/processor.js.map +10 -7
  14. package/dist/src/runtime/reindex.d.ts +72 -23
  15. package/dist/src/runtime/reindex.js +238 -105
  16. package/dist/src/runtime/reindex.js.map +7 -5
  17. package/dist/src/runtime/reorg.d.ts +72 -23
  18. package/dist/src/runtime/reorg.js +215 -81
  19. package/dist/src/runtime/reorg.js.map +8 -6
  20. package/dist/src/runtime/runner.d.ts +72 -23
  21. package/dist/src/runtime/runner.js +156 -23
  22. package/dist/src/runtime/runner.js.map +8 -6
  23. package/dist/src/runtime/source-matcher.d.ts +72 -23
  24. package/dist/src/schema/index.d.ts +72 -23
  25. package/dist/src/schema/index.js +115 -1
  26. package/dist/src/schema/index.js.map +3 -3
  27. package/dist/src/service.js +348 -171
  28. package/dist/src/service.js.map +11 -8
  29. package/dist/src/testing/index.d.ts +195 -24
  30. package/dist/src/testing/index.js +448 -25
  31. package/dist/src/testing/index.js.map +9 -6
  32. package/dist/src/triggers/index.d.ts +83 -24
  33. package/dist/src/triggers/index.js +5 -2
  34. package/dist/src/triggers/index.js.map +3 -3
  35. package/dist/src/types.d.ts +73 -24
  36. package/dist/src/validate.d.ts +72 -23
  37. package/dist/src/validate.js +115 -1
  38. package/dist/src/validate.js.map +3 -3
  39. package/package.json +2 -2
@@ -172,7 +172,12 @@ type TraitScope = {
172
172
  *
173
173
  * ```ts
174
174
  * sources: {
175
- * registry: { type: "print_event", contractId: REGISTRY, topic: "pool-created" },
175
+ * registry: {
176
+ * type: "print_event",
177
+ * contractId: REGISTRY,
178
+ * topic: "pool-created",
179
+ * prints: { "pool-created": { pool: "principal" } },
180
+ * },
176
181
  * swaps: {
177
182
  * type: "print_event",
178
183
  * topic: "swap",
@@ -256,6 +261,9 @@ interface ContractCallFilter extends TraitScope, FactoryScope {
256
261
  * decoded function arguments. Dev-provided; serialized into the deployed
257
262
  * definition and used at runtime to decode args by name. Omit to keep
258
263
  * `event.args` as a positional `unknown[]`.
264
+ *
265
+ * Deploy validation requires `abi` whenever `functionName` is set
266
+ * (`normalizeAbi()` from `@secondlayer/stacks/clarity`).
259
267
  */
260
268
  abi?: AbiContract2;
261
269
  }
@@ -286,29 +294,70 @@ type PrintField = ColumnType | {
286
294
  } | {
287
295
  list: PrintField
288
296
  };
289
- interface PrintEventFilter extends TraitScope, FactoryScope {
290
- type: "print_event";
291
- /** One contract id, or a set of them (max 20). Supports `*` wildcards.
292
- * COMPOSES with `trait` — the matcher ANDs them, so the pair means
293
- * "contracts conforming to this trait, narrowed to these ids". (The Index
294
- * read API refuses the combination, but that is its query planner's
295
- * constraint, not a semantic one.) */
296
- contractId?: string | readonly string[];
297
- topic?: string;
298
- /**
299
- * Per-topic field schema. When declared, the handler's `event` is a
300
- * discriminated union keyed by `topic` and `event.data` is typed per topic
301
- * (e.g. `{ "completed-deposit": { amount: "uint", sender: "principal" } }`).
302
- * Nested tuples, lists, and optional fields are expressible — see
303
- * {@link PrintField}.
304
- *
305
- * Declaring `prints` also opts the source into RUNTIME validation: an
306
- * event whose decoded payload does not match is skipped and logged rather
307
- * than written as nulls (and the deploy-time field lint is promoted from
308
- * a warning to an error).
309
- */
310
- prints?: Record<string, Record<string, PrintField>>;
297
+ /**
298
+ * Per-topic field schema for a print_event source. When declared, the handler's
299
+ * `event` is a discriminated union keyed by `topic` and `event.data` is typed
300
+ * per topic. Nested tuples, lists, and optional fields — see {@link PrintField}.
301
+ *
302
+ * Also opts the source into RUNTIME validation: a payload that does not match
303
+ * is skipped and logged rather than written as nulls.
304
+ */
305
+ type PrintEventPrints = Record<string, Record<string, PrintField>>;
306
+ /**
307
+ * One column in a static identity map. Literals only — the bundler never
308
+ * executes user code, so `{ from: "tokenX" }` is valid and `from("tokenX")`
309
+ * is not.
310
+ */
311
+ type MaterializeColumn = {
312
+ from: string
313
+ } | {
314
+ fromTx: "sender" | "txId"
315
+ } | {
316
+ fromBlock: "height" | "hash" | "timestamp"
317
+ };
318
+ /** Static insert from a source event — replaces a 1:1 handler. */
319
+ interface MaterializeSpec {
320
+ table: string;
321
+ /** Keys = schema column names (snake). */
322
+ columns: Record<string, MaterializeColumn>;
311
323
  }
324
+ type PrintEventFields = FactoryScope & {
325
+ type: "print_event"
326
+ topic?: string
327
+ /** Identity map: insert without a handler function. XOR with handlers[name]. */
328
+ materialize?: MaterializeSpec
329
+ };
330
+ /**
331
+ * Pinned to one or more contract ids with no trait — `prints` is required so
332
+ * `event.data` is typed and empty-table handlers cannot typecheck.
333
+ */
334
+ type PinnedPrintEventFilter = PrintEventFields & {
335
+ contractId: string | readonly string[]
336
+ trait?: undefined
337
+ prints: PrintEventPrints
338
+ };
339
+ /**
340
+ * Trait-scoped print source (optionally narrowed by contractId). No single
341
+ * schema — `prints` stays optional.
342
+ */
343
+ type TraitPrintEventFilter = PrintEventFields & {
344
+ contractId?: string | readonly string[]
345
+ trait: string
346
+ prints?: PrintEventPrints
347
+ };
348
+ /**
349
+ * Unpinned / factory-only print source (no contractId). `prints` optional.
350
+ */
351
+ type UnpinnedPrintEventFilter = PrintEventFields & {
352
+ contractId?: undefined
353
+ trait?: string
354
+ prints?: PrintEventPrints
355
+ };
356
+ /**
357
+ * Print-event filter. Pinned sources (contractId set, no trait) require
358
+ * `prints`; trait and unpinned sources may omit it.
359
+ */
360
+ type PrintEventFilter = PinnedPrintEventFilter | TraitPrintEventFilter | UnpinnedPrintEventFilter;
312
361
  /** All subgraph filter types — discriminated on `type` */
313
362
  type SubgraphFilter = StxTransferFilter | StxMintFilter | StxBurnFilter | StxLockFilter | FtTransferFilter | FtMintFilter | FtBurnFilter | NftTransferFilter | NftMintFilter | NftBurnFilter | ContractCallFilter | ContractDeployFilter | PrintEventFilter;
314
363
  /** Transaction metadata available in handlers */
@@ -302,7 +302,7 @@ function matchSources(sources, transactions, events, traitContracts = new Map, f
302
302
 
303
303
  // src/runtime/runner.ts
304
304
  import { getErrorMessage } from "@secondlayer/shared";
305
- import { logger } from "@secondlayer/shared/logger";
305
+ import { logger as logger2 } from "@secondlayer/shared/logger";
306
306
  import {
307
307
  clarityValueToJS,
308
308
  deserializeCV as deserializeCV3,
@@ -316,6 +316,19 @@ import {
316
316
  function camelizeDataKey(str) {
317
317
  return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
318
318
  }
319
+ function camelizeKeys(obj) {
320
+ if (obj === null || obj === undefined)
321
+ return obj;
322
+ if (typeof obj !== "object")
323
+ return obj;
324
+ if (Array.isArray(obj))
325
+ return obj.map(camelizeKeys);
326
+ const result = {};
327
+ for (const [k, v] of Object.entries(obj)) {
328
+ result[camelizeDataKey(k)] = camelizeKeys(v);
329
+ }
330
+ return result;
331
+ }
319
332
  function cvToTree(cv) {
320
333
  switch (cv.type) {
321
334
  case "uint":
@@ -640,6 +653,58 @@ function inferPrintTopics(samples) {
640
653
  return out.sort((a, b) => b.count - a.count);
641
654
  }
642
655
 
656
+ // src/runtime/materialize.ts
657
+ function resolveColumn(col, event, tx, block) {
658
+ if ("fromTx" in col) {
659
+ return { ok: true, value: tx[col.fromTx] };
660
+ }
661
+ if ("fromBlock" in col) {
662
+ return { ok: true, value: block[col.fromBlock] };
663
+ }
664
+ const data = event.data;
665
+ const dataObj = data !== null && typeof data === "object" && !Array.isArray(data) ? data : undefined;
666
+ if (dataObj && Object.prototype.hasOwnProperty.call(dataObj, col.from)) {
667
+ return { ok: true, value: dataObj[col.from] };
668
+ }
669
+ if (col.from === "topic" && "topic" in event) {
670
+ return { ok: true, value: event.topic };
671
+ }
672
+ if (Object.prototype.hasOwnProperty.call(event, col.from)) {
673
+ return { ok: true, value: event[col.from] };
674
+ }
675
+ return { ok: false, missing: col.from };
676
+ }
677
+ function buildMaterializeRow(spec, event, ctx, tableColumns) {
678
+ const row = {};
679
+ for (const [colName, mapping] of Object.entries(spec.columns)) {
680
+ const resolved = resolveColumn(mapping, event, ctx.tx, ctx.block);
681
+ if (!resolved.ok) {
682
+ const colDef = tableColumns[colName];
683
+ if (colDef?.nullable) {
684
+ row[colName] = null;
685
+ continue;
686
+ }
687
+ return {
688
+ ok: false,
689
+ reason: `required materialize field "${resolved.missing}" missing for column "${colName}"`
690
+ };
691
+ }
692
+ row[colName] = resolved.value ?? null;
693
+ }
694
+ return { ok: true, row };
695
+ }
696
+ function applyMaterializeInsert(spec, event, ctx, schema) {
697
+ const table = schema[spec.table];
698
+ if (!table) {
699
+ return { ok: false, reason: `materialize table "${spec.table}" missing` };
700
+ }
701
+ const built = buildMaterializeRow(spec, event, ctx, table.columns);
702
+ if (!built.ok)
703
+ return built;
704
+ ctx.insert(spec.table, built.row);
705
+ return { ok: true };
706
+ }
707
+
643
708
  // src/runtime/print-validate.ts
644
709
  var COLUMN_TYPES = new Set([
645
710
  "text",
@@ -719,21 +784,62 @@ function validatePrintPayload(prints, topic, data) {
719
784
  return matchesTopic(declared, data);
720
785
  }
721
786
 
722
- // src/runtime/runner.ts
723
- var DEFAULT_ERROR_THRESHOLD = 50;
724
- function camelizeKeys(obj) {
725
- if (obj === null || obj === undefined)
726
- return obj;
727
- if (typeof obj !== "object")
728
- return obj;
729
- if (Array.isArray(obj))
730
- return obj.map(camelizeKeys);
731
- const result = {};
732
- for (const [k, v] of Object.entries(obj)) {
733
- result[camelizeDataKey(k)] = camelizeKeys(v);
787
+ // src/runtime/violations.ts
788
+ import { getTargetDb } from "@secondlayer/shared/db";
789
+ import { logger } from "@secondlayer/shared/logger";
790
+ import { sql } from "kysely";
791
+ var VIOLATIONS_CAP_PER_SUBGRAPH = 100;
792
+ function jsonSafe(value) {
793
+ if (typeof value === "bigint")
794
+ return value.toString();
795
+ if (value === null || value === undefined)
796
+ return value;
797
+ if (Array.isArray(value))
798
+ return value.map(jsonSafe);
799
+ if (typeof value === "object") {
800
+ const out = {};
801
+ for (const [k, v] of Object.entries(value)) {
802
+ out[k] = jsonSafe(v);
803
+ }
804
+ return out;
734
805
  }
735
- return result;
806
+ return value;
807
+ }
808
+ async function defaultRecordPrintViolation(input) {
809
+ try {
810
+ const db = getTargetDb();
811
+ const sample = jsonSafe(input.samplePayload);
812
+ await db.insertInto("subgraph_violations").values({
813
+ subgraph_name: input.subgraphName,
814
+ source_name: input.sourceName,
815
+ block_height: input.blockHeight,
816
+ tx_id: input.txId,
817
+ reason: input.reason,
818
+ sample_payload: sample
819
+ }).execute();
820
+ await sql`
821
+ DELETE FROM subgraph_violations
822
+ WHERE id IN (
823
+ SELECT id FROM subgraph_violations
824
+ WHERE subgraph_name = ${input.subgraphName}
825
+ ORDER BY seen_at DESC
826
+ OFFSET ${VIOLATIONS_CAP_PER_SUBGRAPH}
827
+ )
828
+ `.execute(db);
829
+ } catch (err) {
830
+ logger.warn("Failed to record print violation", {
831
+ subgraph: input.subgraphName,
832
+ error: err instanceof Error ? err.message : String(err)
833
+ });
834
+ }
835
+ }
836
+ var recorder = defaultRecordPrintViolation;
837
+ async function recordPrintViolation(input) {
838
+ await recorder(input);
736
839
  }
840
+
841
+ // src/runtime/runner.ts
842
+ var DEFAULT_ERROR_THRESHOLD = 50;
737
843
  function decodeFunctionArgs2(args) {
738
844
  let parsed = args;
739
845
  if (typeof parsed === "string") {
@@ -981,16 +1087,18 @@ async function runHandlers(subgraph, matched, ctx, opts) {
981
1087
  units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
982
1088
  for (const { tx, event, sourceName } of units) {
983
1089
  if (errors >= threshold) {
984
- logger.error("Subgraph error threshold reached, skipping remaining events", {
1090
+ logger2.error("Subgraph error threshold reached, skipping remaining events", {
985
1091
  subgraph: subgraph.name,
986
1092
  errors,
987
1093
  threshold
988
1094
  });
989
1095
  return { processed, errors, ...skipped > 0 ? { skipped } : {} };
990
1096
  }
1097
+ const filter = filterLookup.get(sourceName);
1098
+ const materialize = filter && "materialize" in filter && filter.materialize !== undefined ? filter.materialize : undefined;
991
1099
  const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
992
- if (!handler) {
993
- logger.warn("No handler found for source", {
1100
+ if (!handler && !materialize) {
1101
+ logger2.warn("No handler found for source", {
994
1102
  subgraph: subgraph.name,
995
1103
  sourceName,
996
1104
  txId: tx.tx_id
@@ -1005,7 +1113,6 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1005
1113
  contractId: tx.contract_id ?? null,
1006
1114
  functionName: tx.function_name ?? null
1007
1115
  });
1008
- const filter = filterLookup.get(sourceName);
1009
1116
  const checkpoint = ctx.opsCheckpoint();
1010
1117
  try {
1011
1118
  let payload;
@@ -1047,22 +1154,48 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1047
1154
  const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
1048
1155
  if (!verdict.ok) {
1049
1156
  skipped++;
1050
- logger.warn("Print payload does not match declared prints schema", {
1157
+ logger2.warn("Print payload does not match declared prints schema", {
1051
1158
  subgraph: subgraph.name,
1052
1159
  sourceName,
1053
1160
  topic: printPayload.topic,
1054
1161
  txId: tx.tx_id,
1055
1162
  reason: verdict.reason
1056
1163
  });
1164
+ recordPrintViolation({
1165
+ subgraphName: subgraph.name,
1166
+ sourceName,
1167
+ blockHeight: ctx.block.height,
1168
+ txId: tx.tx_id,
1169
+ reason: verdict.reason,
1170
+ samplePayload: {
1171
+ topic: printPayload.topic ?? "",
1172
+ data: printPayload.data ?? {}
1173
+ }
1174
+ });
1175
+ continue;
1176
+ }
1177
+ }
1178
+ if (materialize) {
1179
+ const result = applyMaterializeInsert(materialize, payload, ctx, subgraph.schema);
1180
+ if (!result.ok) {
1181
+ skipped++;
1182
+ logger2.warn("Materialize skipped: required field missing", {
1183
+ subgraph: subgraph.name,
1184
+ sourceName,
1185
+ txId: tx.tx_id,
1186
+ reason: result.reason
1187
+ });
1057
1188
  continue;
1058
1189
  }
1190
+ processed++;
1191
+ } else if (handler) {
1192
+ await handler(payload, ctx);
1193
+ processed++;
1059
1194
  }
1060
- await handler(payload, ctx);
1061
- processed++;
1062
1195
  } catch (err) {
1063
1196
  ctx.rollbackTo(checkpoint);
1064
1197
  errors++;
1065
- logger.error("Subgraph handler error", {
1198
+ logger2.error("Subgraph handler error", {
1066
1199
  subgraph: subgraph.name,
1067
1200
  sourceName,
1068
1201
  txId: tx.tx_id,
@@ -1079,5 +1212,5 @@ export {
1079
1212
  buildContractCallInput
1080
1213
  };
1081
1214
 
1082
- //# debugId=88617EDFE963086E64756E2164756E21
1215
+ //# debugId=B6BA5F58B8903CEC64756E2164756E21
1083
1216
  //# sourceMappingURL=runner.js.map