@secondlayer/subgraphs 4.1.5 → 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.
- package/dist/src/index.d.ts +87 -29
- package/dist/src/index.js +376 -128
- package/dist/src/index.js.map +9 -7
- package/dist/src/runtime/block-processor.d.ts +72 -23
- package/dist/src/runtime/block-processor.js +186 -53
- package/dist/src/runtime/block-processor.js.map +7 -5
- package/dist/src/runtime/catchup.d.ts +72 -23
- package/dist/src/runtime/catchup.js +201 -68
- package/dist/src/runtime/catchup.js.map +7 -5
- package/dist/src/runtime/processor.js +281 -147
- package/dist/src/runtime/processor.js.map +8 -6
- package/dist/src/runtime/reindex.d.ts +72 -23
- package/dist/src/runtime/reindex.js +238 -105
- package/dist/src/runtime/reindex.js.map +7 -5
- package/dist/src/runtime/reorg.d.ts +72 -23
- package/dist/src/runtime/reorg.js +215 -81
- package/dist/src/runtime/reorg.js.map +8 -6
- package/dist/src/runtime/runner.d.ts +72 -23
- package/dist/src/runtime/runner.js +156 -23
- package/dist/src/runtime/runner.js.map +8 -6
- package/dist/src/runtime/source-matcher.d.ts +72 -23
- package/dist/src/schema/index.d.ts +72 -23
- package/dist/src/schema/index.js +115 -1
- package/dist/src/schema/index.js.map +3 -3
- package/dist/src/service.js +286 -152
- package/dist/src/service.js.map +8 -6
- package/dist/src/testing/index.d.ts +195 -24
- package/dist/src/testing/index.js +448 -25
- package/dist/src/testing/index.js.map +9 -6
- package/dist/src/triggers/index.d.ts +83 -24
- package/dist/src/triggers/index.js +5 -2
- package/dist/src/triggers/index.js.map +3 -3
- package/dist/src/types.d.ts +73 -24
- package/dist/src/validate.d.ts +72 -23
- package/dist/src/validate.js +115 -1
- package/dist/src/validate.js.map +3 -3
- package/package.json +2 -2
|
@@ -172,7 +172,12 @@ type TraitScope = {
|
|
|
172
172
|
*
|
|
173
173
|
* ```ts
|
|
174
174
|
* sources: {
|
|
175
|
-
* registry: {
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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/
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
1215
|
+
//# debugId=B6BA5F58B8903CEC64756E2164756E21
|
|
1083
1216
|
//# sourceMappingURL=runner.js.map
|