@secondlayer/subgraphs 3.20.0 → 3.22.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 (42) hide show
  1. package/dist/src/index.d.ts +188 -16
  2. package/dist/src/index.js +369 -41
  3. package/dist/src/index.js.map +12 -11
  4. package/dist/src/runtime/block-processor.d.ts +100 -13
  5. package/dist/src/runtime/block-processor.js +208 -22
  6. package/dist/src/runtime/block-processor.js.map +9 -8
  7. package/dist/src/runtime/catchup.d.ts +100 -13
  8. package/dist/src/runtime/catchup.js +208 -22
  9. package/dist/src/runtime/catchup.js.map +9 -8
  10. package/dist/src/runtime/context.d.ts +34 -8
  11. package/dist/src/runtime/context.js +25 -12
  12. package/dist/src/runtime/context.js.map +4 -4
  13. package/dist/src/runtime/processor.js +213 -22
  14. package/dist/src/runtime/processor.js.map +10 -9
  15. package/dist/src/runtime/reindex.d.ts +100 -13
  16. package/dist/src/runtime/reindex.js +208 -22
  17. package/dist/src/runtime/reindex.js.map +9 -8
  18. package/dist/src/runtime/reorg.d.ts +100 -13
  19. package/dist/src/runtime/reorg.js +213 -22
  20. package/dist/src/runtime/reorg.js.map +10 -9
  21. package/dist/src/runtime/replay.js +29 -6
  22. package/dist/src/runtime/replay.js.map +3 -3
  23. package/dist/src/runtime/runner.d.ts +125 -17
  24. package/dist/src/runtime/runner.js +100 -3
  25. package/dist/src/runtime/runner.js.map +5 -4
  26. package/dist/src/runtime/source-matcher.d.ts +98 -11
  27. package/dist/src/runtime/source-matcher.js +30 -6
  28. package/dist/src/runtime/source-matcher.js.map +3 -3
  29. package/dist/src/schema/index.d.ts +100 -13
  30. package/dist/src/schema/index.js +171 -20
  31. package/dist/src/schema/index.js.map +5 -5
  32. package/dist/src/service.js +213 -22
  33. package/dist/src/service.js.map +10 -9
  34. package/dist/src/testing/index.d.ts +620 -0
  35. package/dist/src/testing/index.js +1297 -0
  36. package/dist/src/testing/index.js.map +16 -0
  37. package/dist/src/triggers/index.d.ts +87 -9
  38. package/dist/src/types.d.ts +101 -14
  39. package/dist/src/validate.d.ts +136 -14
  40. package/dist/src/validate.js +158 -19
  41. package/dist/src/validate.js.map +3 -3
  42. package/package.json +7 -5
@@ -25,15 +25,24 @@ interface SubgraphRelation {
25
25
  /** Target column(s) the fields point at (a uniqueKeys entry on the target). */
26
26
  referencedColumns: string[];
27
27
  }
28
- /** Table definition within a subgraph schema */
28
+ /**
29
+ * Table definition within a subgraph schema.
30
+ *
31
+ * The array fields are `readonly` so a schema literal can be hoisted out of
32
+ * the `defineSubgraph()` call with `as const` (or {@link defineSchema}) and
33
+ * still typecheck — `as const` produces `readonly [readonly ["holder"]]`,
34
+ * which a mutable `string[][]` rejects. Without that, extracting a helper
35
+ * that takes `ctx` meant losing the entire typed surface, which is why the
36
+ * shipped starter reached for `ctx: any`.
37
+ */
29
38
  interface SubgraphTable {
30
39
  columns: Record<string, SubgraphColumn>;
31
40
  /** Composite indexes (each entry is an array of column names) */
32
- indexes?: string[][];
41
+ indexes?: readonly (readonly string[])[];
33
42
  /** Unique key constraints (each entry is an array of column names). Required for upsert. */
34
- uniqueKeys?: string[][];
43
+ uniqueKeys?: readonly (readonly string[])[];
35
44
  /** Foreign-key relations to other tables (for typed ORM joins). */
36
- relations?: SubgraphRelation[];
45
+ relations?: readonly SubgraphRelation[];
37
46
  }
38
47
  /** Subgraph schema — maps table names to table definitions */
39
48
  type SubgraphSchema = Record<string, SubgraphTable>;
@@ -68,6 +77,45 @@ interface StxLockFilter {
68
77
  type TraitScope = {
69
78
  trait?: string
70
79
  };
80
+ /**
81
+ * Scope a source to a DYNAMIC address set discovered from another source's
82
+ * events, instead of a fixed contract list.
83
+ *
84
+ * The standard shape it unlocks: a router plus pools created after you
85
+ * deploy. `contractId` arrays cover a set you know at authoring time; a
86
+ * factory covers the set that grows. It is deliberately NOT tied to contract
87
+ * deployment — the address can come from any field of any event, so it also
88
+ * covers launchpad-minted tokens, DAO extension allowlists, and registry
89
+ * entries.
90
+ *
91
+ * ```ts
92
+ * sources: {
93
+ * registry: { type: "print_event", contractId: REGISTRY, topic: "pool-created" },
94
+ * swaps: {
95
+ * type: "print_event",
96
+ * topic: "swap",
97
+ * // every pool the registry has ever announced, including new ones
98
+ * factory: { from: "registry", field: "data.pool" },
99
+ * },
100
+ * }
101
+ * ```
102
+ *
103
+ * Two guarantees the runtime provides:
104
+ * - **Same-block ordering** — a contract discovered in block N receives its
105
+ * own block-N events. Discovery runs before matching, within the block.
106
+ * - **Reorg-scoped** — the discovered set is stamped with the block that
107
+ * revealed it and rolled back with everything else on a fork, so an address
108
+ * announced on an orphaned chain does not linger forever.
109
+ */
110
+ interface FactoryScope {
111
+ factory?: {
112
+ /** Source name whose events reveal the addresses. */
113
+ from: string
114
+ /** Dotted path to the address on that source's payload
115
+ * (e.g. `"data.pool"`, `"contractId"`, `"data.token.id"`). */
116
+ field: string
117
+ };
118
+ }
71
119
  /** FT event filters */
72
120
  interface FtTransferFilter extends TraitScope {
73
121
  type: "ft_transfer";
@@ -106,9 +154,19 @@ interface NftBurnFilter extends TraitScope {
106
154
  sender?: string;
107
155
  }
108
156
  /** Contract event filters */
109
- interface ContractCallFilter extends TraitScope {
157
+ interface ContractCallFilter extends TraitScope, FactoryScope {
110
158
  type: "contract_call";
111
- contractId?: string;
159
+ /**
160
+ * One contract id, or a SET of them (max 20) — the standard DeFi shape is
161
+ * a router plus N pools, which used to cost N near-identical sources and
162
+ * handlers, or the `"*"` catch-all that erases per-source typing. Payload
163
+ * types are identical per `type`, so one handler serves the set and
164
+ * `event.contractId` disambiguates.
165
+ *
166
+ * Supports `*` wildcards (e.g. `"SP1ABC….pool-*"`). Mutually exclusive
167
+ * with `trait`, which resolves its own set per block.
168
+ */
169
+ contractId?: string | readonly string[];
112
170
  functionName?: string;
113
171
  caller?: string;
114
172
  /**
@@ -124,18 +182,47 @@ interface ContractDeployFilter {
124
182
  deployer?: string;
125
183
  contractName?: string;
126
184
  }
127
- interface PrintEventFilter extends TraitScope {
185
+ /**
186
+ * One declared field of a print payload.
187
+ *
188
+ * A bare {@link ColumnType} is a required scalar (`amount: "uint"`). The
189
+ * composite forms exist because real print payloads are not flat: BNS-V2
190
+ * emits `name` as a NESTED tuple, and a `prints` vocabulary that could only
191
+ * say `"jsonb"` is why a handler reading flat `data.name` type-checked,
192
+ * deployed, and then decoded to null on every event.
193
+ *
194
+ * - `{ type: F, optional: true }` — the KEY becomes optional on `event.data`
195
+ * (mirrors `always_present: false` in the inferred schema).
196
+ * - `{ tuple: { … } }` — a nested tuple, typed all the way down.
197
+ * - `{ list: F }` — a list of `F`.
198
+ */
199
+ type PrintField = ColumnType | {
200
+ type: PrintField
201
+ optional: true
202
+ } | {
203
+ tuple: Record<string, PrintField>
204
+ } | {
205
+ list: PrintField
206
+ };
207
+ interface PrintEventFilter extends TraitScope, FactoryScope {
128
208
  type: "print_event";
129
- contractId?: string;
209
+ /** One contract id, or a set of them (max 20). Supports `*` wildcards.
210
+ * Mutually exclusive with `trait`. */
211
+ contractId?: string | readonly string[];
130
212
  topic?: string;
131
213
  /**
132
- * Optional per-topic field schema. When declared, the handler's `event` is
133
- * a discriminated union keyed by `topic` and `event.data` is typed per topic
214
+ * Per-topic field schema. When declared, the handler's `event` is a
215
+ * discriminated union keyed by `topic` and `event.data` is typed per topic
134
216
  * (e.g. `{ "completed-deposit": { amount: "uint", sender: "principal" } }`).
135
- * Uses the same `ColumnType` vocab as `schema`; nested fields use `"jsonb"`.
136
- * Type-level only — not validated at runtime.
217
+ * Nested tuples, lists, and optional fields are expressible — see
218
+ * {@link PrintField}.
219
+ *
220
+ * Declaring `prints` also opts the source into RUNTIME validation: an
221
+ * event whose decoded payload does not match is skipped and logged rather
222
+ * than written as nulls (and the deploy-time field lint is promoted from
223
+ * a warning to an error).
137
224
  */
138
- prints?: Record<string, Record<string, ColumnType>>;
225
+ prints?: Record<string, Record<string, PrintField>>;
139
226
  }
140
227
  /** All subgraph filter types — discriminated on `type` */
141
228
  type SubgraphFilter = StxTransferFilter | StxMintFilter | StxBurnFilter | StxLockFilter | FtTransferFilter | FtMintFilter | FtBurnFilter | NftTransferFilter | NftMintFilter | NftBurnFilter | ContractCallFilter | ContractDeployFilter | PrintEventFilter;
@@ -100,6 +100,18 @@ function emitJournalDDL(schemaName) {
100
100
  `CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
101
101
  ];
102
102
  }
103
+ function emitFactoryDDL(schemaName) {
104
+ return [
105
+ `CREATE TABLE IF NOT EXISTS ${schemaName}._factory_addresses (
106
+ source_name TEXT NOT NULL,
107
+ address TEXT NOT NULL,
108
+ block_height BIGINT NOT NULL,
109
+ _created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
110
+ PRIMARY KEY (source_name, address)
111
+ )`,
112
+ `CREATE INDEX IF NOT EXISTS idx_${schemaName}_factory_height ON ${schemaName}._factory_addresses (block_height)`
113
+ ];
114
+ }
103
115
  function emitForeignKeyDDL(schemaName, tableName, tableDef) {
104
116
  return (tableDef.relations ?? []).map((rel) => {
105
117
  const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
@@ -118,6 +130,9 @@ function generateSubgraphSQL(def, schemaNameOverride) {
118
130
  statements.push(...emitTableDDL(schemaName, tableName, tableDef));
119
131
  }
120
132
  statements.push(...emitJournalDDL(schemaName));
133
+ if (Object.values(def.sources ?? {}).some((src) => src.factory !== undefined)) {
134
+ statements.push(...emitFactoryDDL(schemaName));
135
+ }
121
136
  for (const [tableName, tableDef] of Object.entries(def.schema)) {
122
137
  statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
123
138
  }
@@ -262,23 +277,21 @@ class SubgraphContext {
262
277
  formatUnits(value, decimals) {
263
278
  return formatUnits(value, decimals);
264
279
  }
265
- async findOne(table, where) {
266
- this.validateTable(table);
280
+ async readRows(table, where, limit) {
267
281
  const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
268
282
  const { clause } = buildWhereClause(where);
269
- const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} LIMIT 1`;
283
+ const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}${limit !== undefined ? ` LIMIT ${limit}` : ""}`;
270
284
  const { rows } = await sql.raw(query).execute(this.db);
271
- const row = rows[0] ?? null;
272
- return this.overlayOne(table, where, row ? this.coerceRow(table, row) : null);
285
+ return rows.map((r) => this.coerceRow(table, r));
286
+ }
287
+ async findOne(table, where) {
288
+ this.validateTable(table);
289
+ const row = (await this.readRows(table, where, 1))[0] ?? null;
290
+ return this.overlayOne(table, where, row);
273
291
  }
274
292
  async findMany(table, where) {
275
293
  this.validateTable(table);
276
- const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
277
- const { clause } = buildWhereClause(where);
278
- const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}`;
279
- const { rows } = await sql.raw(query).execute(this.db);
280
- const dbRows = rows.map((r) => this.coerceRow(table, r));
281
- return this.overlayMany(table, where, dbRows);
294
+ return this.overlayMany(table, where, await this.readRows(table, where));
282
295
  }
283
296
  overlayOne(table, where, dbRow) {
284
297
  if (this.ops.length === 0)
@@ -758,6 +771,87 @@ import {
758
771
  deserializeCV as deserializeCV2,
759
772
  toCamelCase
760
773
  } from "@secondlayer/stacks/clarity";
774
+
775
+ // src/runtime/print-validate.ts
776
+ var COLUMN_TYPES = new Set([
777
+ "text",
778
+ "uint",
779
+ "int",
780
+ "principal",
781
+ "boolean",
782
+ "timestamp",
783
+ "jsonb"
784
+ ]);
785
+ function isColumnType(field) {
786
+ return typeof field === "string" && COLUMN_TYPES.has(field);
787
+ }
788
+ function matchesField(field, value) {
789
+ if (isColumnType(field)) {
790
+ switch (field) {
791
+ case "uint":
792
+ case "int":
793
+ return typeof value === "bigint" || typeof value === "number";
794
+ case "boolean":
795
+ return typeof value === "boolean";
796
+ case "text":
797
+ case "principal":
798
+ case "timestamp":
799
+ return typeof value === "string" || typeof value === "number" || typeof value === "bigint";
800
+ case "jsonb":
801
+ return value !== undefined;
802
+ }
803
+ }
804
+ if (typeof field !== "object" || field === null)
805
+ return false;
806
+ if ("optional" in field) {
807
+ return value === undefined || value === null || matchesField(field.type, value);
808
+ }
809
+ if ("tuple" in field) {
810
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
811
+ return false;
812
+ }
813
+ return matchesTopic(field.tuple, value).ok;
814
+ }
815
+ if ("list" in field) {
816
+ return Array.isArray(value) && value.every((item) => matchesField(field.list, item));
817
+ }
818
+ return false;
819
+ }
820
+ function matchesTopic(declared, data) {
821
+ for (const [name, field] of Object.entries(declared)) {
822
+ const present = name in data;
823
+ const optional = typeof field === "object" && field !== null && "optional" in field;
824
+ if (!present) {
825
+ if (optional)
826
+ continue;
827
+ return { ok: false, reason: `missing declared field "${name}"` };
828
+ }
829
+ if (!matchesField(field, data[name])) {
830
+ return {
831
+ ok: false,
832
+ reason: `field "${name}" does not match its declared type (got ${describe(data[name])})`
833
+ };
834
+ }
835
+ }
836
+ return { ok: true };
837
+ }
838
+ function describe(value) {
839
+ if (value === null)
840
+ return "null";
841
+ if (Array.isArray(value))
842
+ return "array";
843
+ return typeof value;
844
+ }
845
+ function validatePrintPayload(prints, topic, data) {
846
+ if (!prints)
847
+ return { ok: true };
848
+ const declared = prints[topic];
849
+ if (!declared)
850
+ return { ok: true };
851
+ return matchesTopic(declared, data);
852
+ }
853
+
854
+ // src/runtime/runner.ts
761
855
  var DEFAULT_ERROR_THRESHOLD = 50;
762
856
  function camelCase(str) {
763
857
  return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
@@ -1002,6 +1096,7 @@ function buildEventPayload(filter, tx, event) {
1002
1096
  async function runHandlers(subgraph, matched, ctx, opts) {
1003
1097
  let processed = 0;
1004
1098
  let errors = 0;
1099
+ let skipped = 0;
1005
1100
  const threshold = opts?.errorThreshold ?? DEFAULT_ERROR_THRESHOLD;
1006
1101
  const filterLookup = new Map;
1007
1102
  if (!Array.isArray(subgraph.sources)) {
@@ -1026,7 +1121,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1026
1121
  errors,
1027
1122
  threshold
1028
1123
  });
1029
- return { processed, errors };
1124
+ return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1030
1125
  }
1031
1126
  const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
1032
1127
  if (!handler) {
@@ -1082,6 +1177,21 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1082
1177
  if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
1083
1178
  continue;
1084
1179
  }
1180
+ if (event !== null && filter?.type === "print_event" && filter.prints) {
1181
+ const printPayload = payload;
1182
+ const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
1183
+ if (!verdict.ok) {
1184
+ skipped++;
1185
+ logger2.warn("Print payload does not match declared prints schema", {
1186
+ subgraph: subgraph.name,
1187
+ sourceName,
1188
+ topic: printPayload.topic,
1189
+ txId: tx.tx_id,
1190
+ reason: verdict.reason
1191
+ });
1192
+ continue;
1193
+ }
1194
+ }
1085
1195
  await handler(payload, ctx);
1086
1196
  processed++;
1087
1197
  } catch (err) {
@@ -1096,11 +1206,14 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1096
1206
  });
1097
1207
  }
1098
1208
  }
1099
- return { processed, errors };
1209
+ return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1100
1210
  }
1101
1211
 
1102
1212
  // src/runtime/source-matcher.ts
1103
1213
  var patternCache = new Map;
1214
+ function matchAny(value, pattern) {
1215
+ return Array.isArray(pattern) ? pattern.some((p) => matchPattern(value, p)) : matchPattern(value, pattern);
1216
+ }
1104
1217
  function matchPattern(value, pattern) {
1105
1218
  if (!pattern.includes("*"))
1106
1219
  return value === pattern;
@@ -1113,6 +1226,22 @@ function matchPattern(value, pattern) {
1113
1226
  return re.test(value);
1114
1227
  }
1115
1228
  var EMPTY_SET = new Set;
1229
+ function factoryAllows(filter, contractId, factoryContracts) {
1230
+ if (!filter.factory)
1231
+ return true;
1232
+ if (!contractId)
1233
+ return false;
1234
+ return (factoryContracts.get(filter.factory.from) ?? EMPTY_SET).has(contractId);
1235
+ }
1236
+ function readPath(payload, path) {
1237
+ let cur = payload;
1238
+ for (const key of path.split(".")) {
1239
+ if (cur === null || typeof cur !== "object")
1240
+ return;
1241
+ cur = cur[key];
1242
+ }
1243
+ return cur;
1244
+ }
1116
1245
  function traitAllows(filter, contractId, traitContracts) {
1117
1246
  const trait = filter.trait;
1118
1247
  if (!trait)
@@ -1124,7 +1253,7 @@ function traitAllows(filter, contractId, traitContracts) {
1124
1253
  function assetContract(assetId) {
1125
1254
  return assetId?.split("::")[0];
1126
1255
  }
1127
- function matchFilter(filter, transactions, eventsByTx, traitContracts) {
1256
+ function matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts = new Map) {
1128
1257
  const results = [];
1129
1258
  switch (filter.type) {
1130
1259
  case "stx_transfer":
@@ -1251,9 +1380,11 @@ function matchFilter(filter, transactions, eventsByTx, traitContracts) {
1251
1380
  if (tx.type !== "contract_call")
1252
1381
  continue;
1253
1382
  if (filter.contractId) {
1254
- if (!tx.contract_id || !matchPattern(tx.contract_id, filter.contractId))
1383
+ if (!tx.contract_id || !matchAny(tx.contract_id, filter.contractId))
1255
1384
  continue;
1256
1385
  }
1386
+ if (!factoryAllows(filter, tx.contract_id, factoryContracts))
1387
+ continue;
1257
1388
  if (filter.functionName) {
1258
1389
  if (!tx.function_name || !matchPattern(tx.function_name, filter.functionName))
1259
1390
  continue;
@@ -1300,11 +1431,13 @@ function matchFilter(filter, transactions, eventsByTx, traitContracts) {
1300
1431
  return false;
1301
1432
  const printContractId = data.contract_identifier ?? data.contract_id;
1302
1433
  if (filter.contractId) {
1303
- if (!printContractId || !matchPattern(printContractId, filter.contractId))
1434
+ if (!printContractId || !matchAny(printContractId, filter.contractId))
1304
1435
  return false;
1305
1436
  }
1306
1437
  if (!traitAllows(filter, printContractId, traitContracts))
1307
1438
  return false;
1439
+ if (!factoryAllows(filter, printContractId, factoryContracts))
1440
+ return false;
1308
1441
  return true;
1309
1442
  });
1310
1443
  if (matched.length > 0) {
@@ -1316,7 +1449,7 @@ function matchFilter(filter, transactions, eventsByTx, traitContracts) {
1316
1449
  }
1317
1450
  return results;
1318
1451
  }
1319
- function matchSources(sources, transactions, events, traitContracts = new Map) {
1452
+ function matchSources(sources, transactions, events, traitContracts = new Map, factoryContracts = new Map) {
1320
1453
  const eventsByTx = new Map;
1321
1454
  for (const event of events) {
1322
1455
  const list = eventsByTx.get(event.tx_id) ?? [];
@@ -1326,7 +1459,7 @@ function matchSources(sources, transactions, events, traitContracts = new Map) {
1326
1459
  const seen = new Set;
1327
1460
  const results = [];
1328
1461
  for (const [sourceName, filter] of Object.entries(sources)) {
1329
- const matches = matchFilter(filter, transactions, eventsByTx, traitContracts);
1462
+ const matches = matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts);
1330
1463
  for (const match of matches) {
1331
1464
  const dedupeKey = `${match.tx.tx_id}:${sourceName}`;
1332
1465
  if (!seen.has(dedupeKey)) {
@@ -1981,6 +2114,57 @@ async function resolveRoute(subgraphName, targetDb) {
1981
2114
  function invalidateSubgraphRoute(subgraphName) {
1982
2115
  routeCache.delete(subgraphName);
1983
2116
  }
2117
+ async function resolveFactoryContracts(subgraph, blockHeight, schemaName, db, txs, evts) {
2118
+ const factories = new Map;
2119
+ for (const source of Object.values(subgraph.sources)) {
2120
+ const factory = source.factory;
2121
+ if (factory)
2122
+ factories.set(factory.from, factory);
2123
+ }
2124
+ const resolved = new Map;
2125
+ const discovered = [];
2126
+ if (factories.size === 0)
2127
+ return { resolved, discovered };
2128
+ for (const [discoveringSource, factory] of factories) {
2129
+ const known = new Set;
2130
+ try {
2131
+ const rows = await sql3`
2132
+ SELECT address FROM ${sql3.raw(`"${schemaName}"."_factory_addresses"`)}
2133
+ WHERE source_name = ${discoveringSource} AND block_height <= ${blockHeight}
2134
+ `.execute(db);
2135
+ for (const row of rows.rows)
2136
+ known.add(row.address);
2137
+ } catch {}
2138
+ const discovering = subgraph.sources[factory.from];
2139
+ if (discovering) {
2140
+ const matches = matchSources({ [factory.from]: discovering }, txs, evts, new Map, new Map);
2141
+ for (const match of matches) {
2142
+ for (const event of match.events ?? []) {
2143
+ const payload = buildEventPayload(discovering, match.tx, event);
2144
+ const value = readPath(payload, factory.field);
2145
+ if (typeof value === "string" && value.length > 0 && !known.has(value)) {
2146
+ known.add(value);
2147
+ discovered.push({ sourceName: discoveringSource, address: value });
2148
+ }
2149
+ }
2150
+ }
2151
+ }
2152
+ resolved.set(discoveringSource, known);
2153
+ }
2154
+ return { resolved, discovered };
2155
+ }
2156
+ async function persistFactoryDiscoveries(schemaName, db, blockHeight, discovered) {
2157
+ if (discovered.length === 0)
2158
+ return;
2159
+ for (const { sourceName, address } of discovered) {
2160
+ await sql3`
2161
+ INSERT INTO ${sql3.raw(`"${schemaName}"."_factory_addresses"`)}
2162
+ (source_name, address, block_height)
2163
+ VALUES (${sourceName}, ${address}, ${blockHeight})
2164
+ ON CONFLICT (source_name, address) DO NOTHING
2165
+ `.execute(db);
2166
+ }
2167
+ }
1984
2168
  async function resolveTraitContracts(subgraph, blockHeight, db) {
1985
2169
  const traits = new Set;
1986
2170
  for (const source of Object.values(subgraph.sources)) {
@@ -2067,8 +2251,11 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2067
2251
  txs = data.txs;
2068
2252
  evts = data.events;
2069
2253
  }
2254
+ const route = await resolveRoute(subgraphName, targetDb);
2255
+ const schemaName = route.schemaName;
2070
2256
  const traitContracts = await resolveTraitContracts(subgraph, blockHeight, targetDb);
2071
- const matched = matchSources(subgraph.sources, txs, evts, traitContracts);
2257
+ const { resolved: factoryContracts, discovered } = await resolveFactoryContracts(subgraph, blockHeight, schemaName, targetDb, txs, evts);
2258
+ const matched = matchSources(subgraph.sources, txs, evts, traitContracts, factoryContracts);
2072
2259
  result.matched = matched.length;
2073
2260
  if (matched.length === 0) {
2074
2261
  if (!opts?.skipProgressUpdate) {
@@ -2076,8 +2263,6 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2076
2263
  }
2077
2264
  return result;
2078
2265
  }
2079
- const route = await resolveRoute(subgraphName, targetDb);
2080
- const schemaName = route.schemaName;
2081
2266
  const blockMeta = {
2082
2267
  height: block.height,
2083
2268
  hash: block.hash,
@@ -2135,6 +2320,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2135
2320
  });
2136
2321
  result.processed = runResult.processed;
2137
2322
  result.errors = runResult.errors;
2323
+ await persistFactoryDiscoveries(schemaName, route.dataDb, blockHeight, discovered);
2138
2324
  await targetDb.transaction().execute(async (tx) => {
2139
2325
  if (manifest && manifest.count > 0) {
2140
2326
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
@@ -2584,6 +2770,11 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
2584
2770
  await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2585
2771
  }
2586
2772
  }
2773
+ await client.unsafe(`DO $$ BEGIN
2774
+ IF to_regclass('"${schemaName}"."_factory_addresses"') IS NOT NULL THEN
2775
+ DELETE FROM "${schemaName}"."_factory_addresses" WHERE block_height >= ${Number(blockHeight)};
2776
+ END IF;
2777
+ END $$`);
2587
2778
  for (const [tableName, rows] of Object.entries(revertedByTable)) {
2588
2779
  if (rows.length === 0)
2589
2780
  continue;
@@ -2651,5 +2842,5 @@ export {
2651
2842
  handleSubgraphReorg
2652
2843
  };
2653
2844
 
2654
- //# debugId=283CF7962DBBE8DC64756E2164756E21
2845
+ //# debugId=7F2499A1E1B069B564756E2164756E21
2655
2846
  //# sourceMappingURL=reorg.js.map