@secondlayer/subgraphs 3.2.1 → 3.3.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 (36) hide show
  1. package/README.md +81 -0
  2. package/dist/src/index.d.ts +71 -9
  3. package/dist/src/index.js +461 -54
  4. package/dist/src/index.js.map +11 -9
  5. package/dist/src/runtime/block-processor.d.ts +37 -9
  6. package/dist/src/runtime/block-processor.js +127 -37
  7. package/dist/src/runtime/block-processor.js.map +5 -5
  8. package/dist/src/runtime/catchup.d.ts +34 -8
  9. package/dist/src/runtime/catchup.js +125 -36
  10. package/dist/src/runtime/catchup.js.map +5 -5
  11. package/dist/src/runtime/context.d.ts +27 -1
  12. package/dist/src/runtime/context.js +13 -2
  13. package/dist/src/runtime/context.js.map +3 -3
  14. package/dist/src/runtime/processor.js +143 -39
  15. package/dist/src/runtime/processor.js.map +8 -8
  16. package/dist/src/runtime/reindex.d.ts +34 -8
  17. package/dist/src/runtime/reindex.js +131 -36
  18. package/dist/src/runtime/reindex.js.map +6 -6
  19. package/dist/src/runtime/reorg.d.ts +34 -8
  20. package/dist/src/runtime/reorg.js +131 -39
  21. package/dist/src/runtime/reorg.js.map +6 -6
  22. package/dist/src/runtime/runner.d.ts +43 -9
  23. package/dist/src/runtime/source-matcher.d.ts +19 -10
  24. package/dist/src/runtime/source-matcher.js +26 -6
  25. package/dist/src/runtime/source-matcher.js.map +3 -3
  26. package/dist/src/schema/index.d.ts +42 -8
  27. package/dist/src/schema/index.js +57 -19
  28. package/dist/src/schema/index.js.map +5 -5
  29. package/dist/src/service.js +143 -39
  30. package/dist/src/service.js.map +8 -8
  31. package/dist/src/triggers/index.d.ts +16 -8
  32. package/dist/src/types.d.ts +35 -9
  33. package/dist/src/validate.d.ts +34 -8
  34. package/dist/src/validate.js +10 -3
  35. package/dist/src/validate.js.map +3 -3
  36. package/package.json +3 -3
@@ -9,6 +9,22 @@ interface SubgraphColumn {
9
9
  search?: boolean;
10
10
  default?: string | number | boolean;
11
11
  }
12
+ /**
13
+ * A foreign-key relation to another table in the same subgraph. Drives DDL FK
14
+ * constraints and ORM codegen (`@relation` in Prisma, `relations()` in Drizzle)
15
+ * so generated clients get typed joins. The referenced columns must form a
16
+ * `uniqueKeys` entry on the target table.
17
+ */
18
+ interface SubgraphRelation {
19
+ /** Relation field name on this table's generated model (e.g. "pool"). */
20
+ name: string;
21
+ /** Target table name in this subgraph. */
22
+ references: string;
23
+ /** Local column(s) holding the foreign key. */
24
+ fields: string[];
25
+ /** Target column(s) the fields point at (a uniqueKeys entry on the target). */
26
+ referencedColumns: string[];
27
+ }
12
28
  /** Table definition within a subgraph schema */
13
29
  interface SubgraphTable {
14
30
  columns: Record<string, SubgraphColumn>;
@@ -16,6 +32,8 @@ interface SubgraphTable {
16
32
  indexes?: string[][];
17
33
  /** Unique key constraints (each entry is an array of column names). Required for upsert. */
18
34
  uniqueKeys?: string[][];
35
+ /** Foreign-key relations to other tables (for typed ORM joins). */
36
+ relations?: SubgraphRelation[];
19
37
  }
20
38
  /** Subgraph schema — maps table names to table definitions */
21
39
  type SubgraphSchema = Record<string, SubgraphTable>;
@@ -42,45 +60,53 @@ interface StxLockFilter {
42
60
  lockedAddress?: string;
43
61
  minAmount?: bigint;
44
62
  }
63
+ /**
64
+ * Restrict a source to contracts conforming to a trait/standard (e.g. "sip-010")
65
+ * instead of a fixed contract — resolved from the contract registry at match time,
66
+ * as-of each processed block. Lets a source index "all SIP-010 tokens" etc.
67
+ */
68
+ type TraitScope = {
69
+ trait?: string
70
+ };
45
71
  /** FT event filters */
46
- interface FtTransferFilter {
72
+ interface FtTransferFilter extends TraitScope {
47
73
  type: "ft_transfer";
48
74
  assetIdentifier?: string;
49
75
  sender?: string;
50
76
  recipient?: string;
51
77
  minAmount?: bigint;
52
78
  }
53
- interface FtMintFilter {
79
+ interface FtMintFilter extends TraitScope {
54
80
  type: "ft_mint";
55
81
  assetIdentifier?: string;
56
82
  recipient?: string;
57
83
  minAmount?: bigint;
58
84
  }
59
- interface FtBurnFilter {
85
+ interface FtBurnFilter extends TraitScope {
60
86
  type: "ft_burn";
61
87
  assetIdentifier?: string;
62
88
  sender?: string;
63
89
  minAmount?: bigint;
64
90
  }
65
91
  /** NFT event filters */
66
- interface NftTransferFilter {
92
+ interface NftTransferFilter extends TraitScope {
67
93
  type: "nft_transfer";
68
94
  assetIdentifier?: string;
69
95
  sender?: string;
70
96
  recipient?: string;
71
97
  }
72
- interface NftMintFilter {
98
+ interface NftMintFilter extends TraitScope {
73
99
  type: "nft_mint";
74
100
  assetIdentifier?: string;
75
101
  recipient?: string;
76
102
  }
77
- interface NftBurnFilter {
103
+ interface NftBurnFilter extends TraitScope {
78
104
  type: "nft_burn";
79
105
  assetIdentifier?: string;
80
106
  sender?: string;
81
107
  }
82
108
  /** Contract event filters */
83
- interface ContractCallFilter {
109
+ interface ContractCallFilter extends TraitScope {
84
110
  type: "contract_call";
85
111
  contractId?: string;
86
112
  functionName?: string;
@@ -98,7 +124,7 @@ interface ContractDeployFilter {
98
124
  deployer?: string;
99
125
  contractName?: string;
100
126
  }
101
- interface PrintEventFilter {
127
+ interface PrintEventFilter extends TraitScope {
102
128
  type: "print_event";
103
129
  contractId?: string;
104
130
  topic?: string;
@@ -18,12 +18,14 @@ class SubgraphContext {
18
18
  pgSchemaName;
19
19
  subgraphSchema;
20
20
  ops = [];
21
- constructor(db, pgSchemaName, subgraphSchema, block, tx) {
21
+ byo;
22
+ constructor(db, pgSchemaName, subgraphSchema, block, tx, byo = false) {
22
23
  this.db = db;
23
24
  this.pgSchemaName = pgSchemaName;
24
25
  this.subgraphSchema = subgraphSchema;
25
26
  this.block = block;
26
27
  this._tx = tx;
28
+ this.byo = byo;
27
29
  }
28
30
  get tx() {
29
31
  return this._tx;
@@ -218,6 +220,15 @@ class SubgraphContext {
218
220
  }
219
221
  buildStatements(ops) {
220
222
  const statements = [];
223
+ if (this.byo) {
224
+ const insertTables = new Set;
225
+ for (const op of ops)
226
+ if (op.kind === "insert")
227
+ insertTables.add(op.table);
228
+ for (const table of insertTables) {
229
+ statements.push(`DELETE FROM "${this.pgSchemaName}"."${table}" WHERE "_block_height" = ${this.block.height}`);
230
+ }
231
+ }
221
232
  let currentBatch = null;
222
233
  let currentBatchKey = "";
223
234
  const flushInsertBatch = () => {
@@ -711,7 +722,19 @@ function matchPattern(value, pattern) {
711
722
  }
712
723
  return re.test(value);
713
724
  }
714
- function matchFilter(filter, transactions, eventsByTx) {
725
+ var EMPTY_SET = new Set;
726
+ function traitAllows(filter, contractId, traitContracts) {
727
+ const trait = filter.trait;
728
+ if (!trait)
729
+ return true;
730
+ if (!contractId)
731
+ return false;
732
+ return (traitContracts.get(trait) ?? EMPTY_SET).has(contractId);
733
+ }
734
+ function assetContract(assetId) {
735
+ return assetId?.split("::")[0];
736
+ }
737
+ function matchFilter(filter, transactions, eventsByTx, traitContracts) {
715
738
  const results = [];
716
739
  switch (filter.type) {
717
740
  case "stx_transfer":
@@ -775,6 +798,8 @@ function matchFilter(filter, transactions, eventsByTx) {
775
798
  if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
776
799
  return false;
777
800
  }
801
+ if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
802
+ return false;
778
803
  if ("sender" in filter && filter.sender) {
779
804
  if (!matchPattern(data.sender, filter.sender))
780
805
  return false;
@@ -813,6 +838,8 @@ function matchFilter(filter, transactions, eventsByTx) {
813
838
  if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
814
839
  return false;
815
840
  }
841
+ if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
842
+ return false;
816
843
  if ("sender" in filter && filter.sender) {
817
844
  if (!matchPattern(data.sender, filter.sender))
818
845
  return false;
@@ -845,6 +872,8 @@ function matchFilter(filter, transactions, eventsByTx) {
845
872
  if (!matchPattern(tx.sender, filter.caller))
846
873
  continue;
847
874
  }
875
+ if (!traitAllows(filter, tx.contract_id, traitContracts))
876
+ continue;
848
877
  const txEvents = eventsByTx.get(tx.tx_id) ?? [];
849
878
  results.push({ tx, events: txEvents });
850
879
  }
@@ -879,11 +908,13 @@ function matchFilter(filter, transactions, eventsByTx) {
879
908
  return false;
880
909
  if (data.topic !== "print")
881
910
  return false;
911
+ const printContractId = data.contract_identifier ?? data.contract_id;
882
912
  if (filter.contractId) {
883
- const contractId = data.contract_identifier ?? data.contract_id;
884
- if (!contractId || !matchPattern(contractId, filter.contractId))
913
+ if (!printContractId || !matchPattern(printContractId, filter.contractId))
885
914
  return false;
886
915
  }
916
+ if (!traitAllows(filter, printContractId, traitContracts))
917
+ return false;
887
918
  return true;
888
919
  });
889
920
  if (matched.length > 0) {
@@ -895,7 +926,7 @@ function matchFilter(filter, transactions, eventsByTx) {
895
926
  }
896
927
  return results;
897
928
  }
898
- function matchSources(sources, transactions, events) {
929
+ function matchSources(sources, transactions, events, traitContracts = new Map) {
899
930
  const eventsByTx = new Map;
900
931
  for (const event of events) {
901
932
  const list = eventsByTx.get(event.tx_id) ?? [];
@@ -905,7 +936,7 @@ function matchSources(sources, transactions, events) {
905
936
  const seen = new Set;
906
937
  const results = [];
907
938
  for (const [sourceName, filter] of Object.entries(sources)) {
908
- const matches = matchFilter(filter, transactions, eventsByTx);
939
+ const matches = matchFilter(filter, transactions, eventsByTx, traitContracts);
909
940
  for (const match of matches) {
910
941
  const dedupeKey = `${match.tx.tx_id}:${sourceName}`;
911
942
  if (!seen.has(dedupeKey)) {
@@ -922,8 +953,11 @@ import {
922
953
  getSourceDb,
923
954
  getTargetDb
924
955
  } from "@secondlayer/shared/db";
956
+ import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contracts";
925
957
  import {
958
+ isByoSubgraph,
926
959
  recordSubgraphProcessed,
960
+ resolveSubgraphDb,
927
961
  updateSubgraphStatus
928
962
  } from "@secondlayer/shared/db/queries/subgraphs";
929
963
  import { logger as logger4 } from "@secondlayer/shared/logger";
@@ -1161,7 +1195,38 @@ async function refreshMatcher(db) {
1161
1195
  }
1162
1196
 
1163
1197
  // src/runtime/block-processor.ts
1164
- var schemaNameCache = new Map;
1198
+ var routeCache = new Map;
1199
+ async function resolveRoute(subgraphName, targetDb) {
1200
+ const cached = routeCache.get(subgraphName);
1201
+ if (cached)
1202
+ return cached;
1203
+ const row = await targetDb.selectFrom("subgraphs").selectAll().where("name", "=", subgraphName).executeTakeFirst();
1204
+ const byo = row ? isByoSubgraph(row) : false;
1205
+ const route = {
1206
+ schemaName: row?.schema_name ?? pgSchemaName(subgraphName),
1207
+ dataDb: row && byo ? resolveSubgraphDb(row) : targetDb,
1208
+ byo
1209
+ };
1210
+ routeCache.set(subgraphName, route);
1211
+ return route;
1212
+ }
1213
+ function invalidateSubgraphRoute(subgraphName) {
1214
+ routeCache.delete(subgraphName);
1215
+ }
1216
+ async function resolveTraitContracts(subgraph, blockHeight, db) {
1217
+ const traits = new Set;
1218
+ for (const source of Object.values(subgraph.sources)) {
1219
+ const trait = source.trait;
1220
+ if (trait)
1221
+ traits.add(trait);
1222
+ }
1223
+ const resolved = new Map;
1224
+ for (const trait of traits) {
1225
+ const ids = await resolveTraitContractIds(db, trait, blockHeight);
1226
+ resolved.set(trait, new Set(ids));
1227
+ }
1228
+ return resolved;
1229
+ }
1165
1230
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1166
1231
  const sourceDb = getSourceDb();
1167
1232
  const targetDb = getTargetDb();
@@ -1201,7 +1266,8 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1201
1266
  txs = await sourceDb.selectFrom("transactions").selectAll().where("block_height", "=", blockHeight).execute();
1202
1267
  evts = await sourceDb.selectFrom("events").selectAll().where("block_height", "=", blockHeight).execute();
1203
1268
  }
1204
- const matched = matchSources(subgraph.sources, txs, evts);
1269
+ const traitContracts = await resolveTraitContracts(subgraph, blockHeight, targetDb);
1270
+ const matched = matchSources(subgraph.sources, txs, evts, traitContracts);
1205
1271
  result.matched = matched.length;
1206
1272
  if (matched.length === 0) {
1207
1273
  if (!opts?.skipProgressUpdate) {
@@ -1209,12 +1275,8 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1209
1275
  }
1210
1276
  return result;
1211
1277
  }
1212
- let schemaName = schemaNameCache.get(subgraphName);
1213
- if (!schemaName) {
1214
- const subgraphRecord = await targetDb.selectFrom("subgraphs").select("schema_name").where("name", "=", subgraphName).executeTakeFirst();
1215
- schemaName = subgraphRecord?.schema_name ?? pgSchemaName(subgraphName);
1216
- schemaNameCache.set(subgraphName, schemaName);
1217
- }
1278
+ const route = await resolveRoute(subgraphName, targetDb);
1279
+ const schemaName = route.schemaName;
1218
1280
  const blockMeta = {
1219
1281
  height: block.height,
1220
1282
  hash: block.hash,
@@ -1229,30 +1291,57 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1229
1291
  };
1230
1292
  let handlerMs = 0;
1231
1293
  let flushMs = 0;
1232
- await targetDb.transaction().execute(async (tx) => {
1233
- const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx);
1234
- const handlerStart = performance.now();
1235
- const runResult = await runHandlers(subgraph, matched, ctx);
1236
- handlerMs = performance.now() - handlerStart;
1294
+ const applyProgress = async (tx, rr) => {
1295
+ if (opts?.skipProgressUpdate)
1296
+ return;
1297
+ const status = rr.errors > 0 && rr.processed === 0 ? "error" : "active";
1298
+ await updateSubgraphStatus(tx, subgraphName, status, blockHeight);
1299
+ if (rr.processed > 0 || rr.errors > 0) {
1300
+ const lastError = rr.errors > 0 ? `${rr.errors} error(s) at block ${blockHeight}` : undefined;
1301
+ await recordSubgraphProcessed(tx, subgraphName, rr.processed, rr.errors, lastError);
1302
+ }
1303
+ };
1304
+ if (route.byo) {
1305
+ let runResult = { processed: 0, errors: 0 };
1306
+ let manifest;
1307
+ await route.dataDb.transaction().execute(async (tx) => {
1308
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true);
1309
+ const handlerStart = performance.now();
1310
+ runResult = await runHandlers(subgraph, matched, ctx);
1311
+ handlerMs = performance.now() - handlerStart;
1312
+ if (ctx.pendingOps > 0) {
1313
+ const flushStart = performance.now();
1314
+ manifest = await ctx.flush();
1315
+ flushMs = performance.now() - flushStart;
1316
+ }
1317
+ });
1237
1318
  result.processed = runResult.processed;
1238
1319
  result.errors = runResult.errors;
1239
- if (ctx.pendingOps > 0) {
1240
- const flushStart = performance.now();
1241
- const manifest = await ctx.flush();
1242
- if (manifest.count > 0) {
1320
+ await targetDb.transaction().execute(async (tx) => {
1321
+ if (manifest && manifest.count > 0) {
1243
1322
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1244
1323
  }
1245
- flushMs = performance.now() - flushStart;
1246
- }
1247
- if (!opts?.skipProgressUpdate) {
1248
- const status = runResult.errors > 0 && runResult.processed === 0 ? "error" : "active";
1249
- await updateSubgraphStatus(tx, subgraphName, status, blockHeight);
1250
- }
1251
- if (!opts?.skipProgressUpdate && (runResult.processed > 0 || runResult.errors > 0)) {
1252
- const lastError = runResult.errors > 0 ? `${runResult.errors} error(s) at block ${blockHeight}` : undefined;
1253
- await recordSubgraphProcessed(tx, subgraphName, runResult.processed, runResult.errors, lastError);
1254
- }
1255
- });
1324
+ await applyProgress(tx, runResult);
1325
+ });
1326
+ } else {
1327
+ await targetDb.transaction().execute(async (tx) => {
1328
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx);
1329
+ const handlerStart = performance.now();
1330
+ const runResult = await runHandlers(subgraph, matched, ctx);
1331
+ handlerMs = performance.now() - handlerStart;
1332
+ result.processed = runResult.processed;
1333
+ result.errors = runResult.errors;
1334
+ if (ctx.pendingOps > 0) {
1335
+ const flushStart = performance.now();
1336
+ const manifest = await ctx.flush();
1337
+ if (manifest.count > 0) {
1338
+ await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1339
+ }
1340
+ flushMs = performance.now() - flushStart;
1341
+ }
1342
+ await applyProgress(tx, runResult);
1343
+ });
1344
+ }
1256
1345
  const totalMs = performance.now() - blockStart;
1257
1346
  result.timing = {
1258
1347
  totalMs: Math.round(totalMs),
@@ -1263,7 +1352,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1263
1352
  try {
1264
1353
  const tables = Object.keys(subgraph.schema);
1265
1354
  for (const table of tables) {
1266
- const { rows } = await sql3.raw(`SELECT n_live_tup AS count FROM pg_stat_user_tables WHERE schemaname = '${schemaName}' AND relname = '${table}'`).execute(targetDb);
1355
+ 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);
1267
1356
  const count = Number(rows[0]?.count ?? 0);
1268
1357
  if (count >= 1e7) {
1269
1358
  logger4.warn("Subgraph table exceeds 10M rows (estimate)", {
@@ -1285,12 +1374,14 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1285
1374
 
1286
1375
  // src/runtime/reorg.ts
1287
1376
  import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
1288
- import { getRawClient, getTargetDb as getTargetDb2 } from "@secondlayer/shared/db";
1289
- import { listSubgraphs } from "@secondlayer/shared/db/queries/subgraphs";
1377
+ import { getTargetDb as getTargetDb2 } from "@secondlayer/shared/db";
1378
+ import {
1379
+ listSubgraphs,
1380
+ resolveSubgraphRawClient
1381
+ } from "@secondlayer/shared/db/queries/subgraphs";
1290
1382
  import { logger as logger5 } from "@secondlayer/shared/logger";
1291
1383
  async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
1292
1384
  const targetDb = getTargetDb2();
1293
- const client = getRawClient("target");
1294
1385
  const activeSubgraphs = (await listSubgraphs(targetDb)).filter((v) => v.status === "active");
1295
1386
  if (activeSubgraphs.length === 0)
1296
1387
  return;
@@ -1304,6 +1395,7 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
1304
1395
  if (!schema)
1305
1396
  continue;
1306
1397
  const schemaName = sg.schema_name ?? pgSchemaName(sg.name);
1398
+ const client = resolveSubgraphRawClient(sg);
1307
1399
  const tableNames = Object.keys(schema);
1308
1400
  const revertedByTable = {};
1309
1401
  for (const tableName of tableNames) {
@@ -1373,5 +1465,5 @@ export {
1373
1465
  handleSubgraphReorg
1374
1466
  };
1375
1467
 
1376
- //# debugId=8E4E1692AE9A9B3964756E2164756E21
1468
+ //# debugId=39C8DEB104D91A4564756E2164756E21
1377
1469
  //# sourceMappingURL=reorg.js.map