@secondlayer/subgraphs 0.5.7 → 0.7.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 (37) hide show
  1. package/dist/src/index.d.ts +17 -5
  2. package/dist/src/index.js +467 -191
  3. package/dist/src/index.js.map +16 -15
  4. package/dist/src/runtime/block-processor.d.ts +9 -1
  5. package/dist/src/runtime/block-processor.js +216 -144
  6. package/dist/src/runtime/block-processor.js.map +9 -9
  7. package/dist/src/runtime/catchup.d.ts +2 -2
  8. package/dist/src/runtime/catchup.js +366 -160
  9. package/dist/src/runtime/catchup.js.map +12 -11
  10. package/dist/src/runtime/clarity.js.map +2 -2
  11. package/dist/src/runtime/context.d.ts +4 -2
  12. package/dist/src/runtime/context.js +79 -36
  13. package/dist/src/runtime/context.js.map +3 -3
  14. package/dist/src/runtime/processor.js +384 -166
  15. package/dist/src/runtime/processor.js.map +14 -13
  16. package/dist/src/runtime/reindex.d.ts +13 -1
  17. package/dist/src/runtime/reindex.js +459 -189
  18. package/dist/src/runtime/reindex.js.map +13 -12
  19. package/dist/src/runtime/reorg.js +229 -148
  20. package/dist/src/runtime/reorg.js.map +10 -10
  21. package/dist/src/runtime/runner.d.ts +4 -2
  22. package/dist/src/runtime/runner.js +7 -3
  23. package/dist/src/runtime/runner.js.map +4 -4
  24. package/dist/src/runtime/source-matcher.js.map +3 -3
  25. package/dist/src/runtime/stats.d.ts +1 -1
  26. package/dist/src/runtime/stats.js +2 -2
  27. package/dist/src/runtime/stats.js.map +3 -3
  28. package/dist/src/schema/index.d.ts +1 -1
  29. package/dist/src/schema/index.js +9 -3
  30. package/dist/src/schema/index.js.map +5 -5
  31. package/dist/src/service.js +385 -167
  32. package/dist/src/service.js.map +15 -14
  33. package/dist/src/templates.js.map +2 -2
  34. package/dist/src/types.js.map +2 -2
  35. package/dist/src/validate.js +2 -2
  36. package/dist/src/validate.js.map +3 -3
  37. package/package.json +51 -51
@@ -16,8 +16,8 @@ function sourceKey(source) {
16
16
  }
17
17
 
18
18
  // src/runtime/context.ts
19
- import { sql } from "kysely";
20
19
  import { logger } from "@secondlayer/shared/logger";
20
+ import { sql } from "kysely";
21
21
  function validateColumnName(name) {
22
22
  if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
23
23
  throw new Error(`Invalid column name: ${name}`);
@@ -58,13 +58,26 @@ class SubgraphContext {
58
58
  const keyColumns = Object.keys(key);
59
59
  const hasUniqueConstraint = tableDef.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
60
60
  if (hasUniqueConstraint) {
61
- this.ops.push({ kind: "insert", table, data: { ...key, ...row, _upsert_keys: keyColumns } });
61
+ this.ops.push({
62
+ kind: "insert",
63
+ table,
64
+ data: { ...key, ...row, _upsert_keys: keyColumns }
65
+ });
62
66
  } else {
63
67
  logger.warn("upsert called without matching uniqueKeys constraint, using fallback", {
64
68
  table,
65
69
  keys: keyColumns
66
70
  });
67
- this.ops.push({ kind: "insert", table, data: { ...key, ...row, _upsert_fallback_keys: keyColumns, _upsert_fallback_set: row } });
71
+ this.ops.push({
72
+ kind: "insert",
73
+ table,
74
+ data: {
75
+ ...key,
76
+ ...row,
77
+ _upsert_fallback_keys: keyColumns,
78
+ _upsert_fallback_set: row
79
+ }
80
+ });
68
81
  }
69
82
  }
70
83
  delete(table, where) {
@@ -122,53 +135,83 @@ class SubgraphContext {
122
135
  }
123
136
  return opsToFlush.length;
124
137
  }
138
+ prepareInsert(op) {
139
+ const upsertKeys = op.data._upsert_keys;
140
+ const data = { ...op.data };
141
+ delete data._upsert_keys;
142
+ delete data._upsert_fallback_keys;
143
+ delete data._upsert_fallback_set;
144
+ data._block_height = this.block.height;
145
+ data._tx_id = this._tx.txId;
146
+ data._created_at = "NOW()";
147
+ const cols = Object.keys(data);
148
+ cols.forEach(validateColumnName);
149
+ const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
150
+ const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
151
+ return { data, cols, vals, upsertKeys, batchKey };
152
+ }
125
153
  buildStatements(ops) {
126
154
  const statements = [];
155
+ let currentBatch = null;
156
+ let currentBatchKey = "";
157
+ const flushInsertBatch = () => {
158
+ if (!currentBatch)
159
+ return;
160
+ const qualifiedTable = `"${this.pgSchemaName}"."${currentBatch.table}"`;
161
+ const colList = currentBatch.cols.map((c) => `"${c}"`).join(", ");
162
+ let rows = currentBatch.rows;
163
+ if (currentBatch.upsertKeys && currentBatch.upsertKeys.length > 0) {
164
+ const keyIndices = currentBatch.upsertKeys.map((k) => currentBatch.cols.indexOf(k));
165
+ const seen = new Map;
166
+ for (let i = 0;i < rows.length; i++) {
167
+ const key = keyIndices.map((ki) => rows[i][ki]).join("\x00");
168
+ seen.set(key, i);
169
+ }
170
+ if (seen.size < rows.length) {
171
+ rows = Array.from(seen.values()).map((i) => rows[i]);
172
+ }
173
+ }
174
+ const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
175
+ let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
176
+ if (currentBatch.upsertKeys && currentBatch.upsertKeys.length > 0) {
177
+ const updateCols = currentBatch.cols.filter((c) => !currentBatch.upsertKeys.includes(c) && !c.startsWith("_"));
178
+ if (updateCols.length > 0) {
179
+ const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
180
+ stmt += ` ON CONFLICT (${currentBatch.upsertKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
181
+ } else {
182
+ stmt += ` ON CONFLICT (${currentBatch.upsertKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
183
+ }
184
+ }
185
+ statements.push(stmt);
186
+ currentBatch = null;
187
+ currentBatchKey = "";
188
+ };
127
189
  for (const op of ops) {
128
190
  const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
129
- switch (op.kind) {
130
- case "insert": {
131
- const upsertKeys = op.data._upsert_keys;
132
- const fallbackKeys = op.data._upsert_fallback_keys;
133
- const fallbackSet = op.data._upsert_fallback_set;
134
- const data = { ...op.data };
135
- delete data._upsert_keys;
136
- delete data._upsert_fallback_keys;
137
- delete data._upsert_fallback_set;
138
- data._block_height = this.block.height;
139
- data._tx_id = this._tx.txId;
140
- data._created_at = "NOW()";
141
- const cols = Object.keys(data);
142
- cols.forEach(validateColumnName);
143
- const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
144
- let stmt = `INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${vals.join(", ")})`;
145
- if (upsertKeys && upsertKeys.length > 0) {
146
- const updateCols = cols.filter((c) => !upsertKeys.includes(c) && !c.startsWith("_"));
147
- if (updateCols.length > 0) {
148
- const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
149
- stmt += ` ON CONFLICT (${upsertKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
150
- } else {
151
- stmt += ` ON CONFLICT (${upsertKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
152
- }
153
- } else if (fallbackKeys && fallbackSet) {}
154
- statements.push(stmt);
155
- break;
191
+ if (op.kind === "insert") {
192
+ const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
193
+ if (batchKey === currentBatchKey && currentBatch) {
194
+ currentBatch.rows.push(vals);
195
+ } else {
196
+ flushInsertBatch();
197
+ currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
198
+ currentBatchKey = batchKey;
156
199
  }
157
- case "update": {
200
+ } else {
201
+ flushInsertBatch();
202
+ if (op.kind === "update") {
158
203
  const setEntries = Object.entries(op.set);
159
204
  setEntries.forEach(([k]) => validateColumnName(k));
160
205
  const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
161
206
  const { clause } = buildWhereClause(op.data);
162
207
  statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
163
- break;
164
- }
165
- case "delete": {
208
+ } else if (op.kind === "delete") {
166
209
  const { clause } = buildWhereClause(op.data);
167
210
  statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
168
- break;
169
211
  }
170
212
  }
171
213
  }
214
+ flushInsertBatch();
172
215
  return statements;
173
216
  }
174
217
  validateTable(table) {
@@ -197,95 +240,6 @@ function buildWhereClause(where) {
197
240
  return { clause: parts.join(" AND "), values: [] };
198
241
  }
199
242
 
200
- // src/runtime/source-matcher.ts
201
- function matchPattern(value, pattern) {
202
- if (!pattern.includes("*"))
203
- return value === pattern;
204
- const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
205
- return new RegExp(`^${regex}$`).test(value);
206
- }
207
- function matchSource(source, transactions, eventsByTx) {
208
- const results = [];
209
- const key = sourceKey(source);
210
- for (const tx of transactions) {
211
- if (source.type) {
212
- if (!matchPattern(tx.type, source.type))
213
- continue;
214
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
215
- let matchedEvents = txEvents;
216
- if (source.minAmount !== undefined) {
217
- const amountEvents = matchedEvents.filter((e) => {
218
- const data = e.data;
219
- const rawAmount = data?.amount;
220
- if (rawAmount === undefined)
221
- return false;
222
- const amount = BigInt(rawAmount);
223
- return amount >= source.minAmount;
224
- });
225
- if (amountEvents.length === 0)
226
- continue;
227
- matchedEvents = amountEvents;
228
- }
229
- results.push({ tx, events: matchedEvents, sourceKey: key });
230
- continue;
231
- }
232
- if (source.contract) {
233
- const txContractMatch = tx.contract_id && matchPattern(tx.contract_id, source.contract);
234
- if (source.function && tx.function_name) {
235
- if (!matchPattern(tx.function_name, source.function))
236
- continue;
237
- } else if (source.function && !tx.function_name) {
238
- continue;
239
- }
240
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
241
- let matchedEvents = txEvents;
242
- if (!txContractMatch) {
243
- matchedEvents = txEvents.filter((e) => {
244
- const data = e.data;
245
- const contractIdentifier = data?.contract_identifier;
246
- return contractIdentifier && matchPattern(contractIdentifier, source.contract);
247
- });
248
- if (matchedEvents.length === 0)
249
- continue;
250
- }
251
- if (source.event) {
252
- matchedEvents = matchedEvents.filter((e) => {
253
- if (matchPattern(e.type, source.event))
254
- return true;
255
- const data = e.data;
256
- const topic = data?.topic;
257
- return topic ? matchPattern(topic, source.event) : false;
258
- });
259
- }
260
- if (txContractMatch || matchedEvents.length > 0) {
261
- results.push({ tx, events: matchedEvents, sourceKey: key });
262
- }
263
- }
264
- }
265
- return results;
266
- }
267
- function matchSources(sources, transactions, events) {
268
- const eventsByTx = new Map;
269
- for (const event of events) {
270
- const list = eventsByTx.get(event.tx_id) ?? [];
271
- list.push(event);
272
- eventsByTx.set(event.tx_id, list);
273
- }
274
- const seen = new Set;
275
- const results = [];
276
- for (const source of sources) {
277
- const matches = matchSource(source, transactions, eventsByTx);
278
- for (const match of matches) {
279
- const dedupeKey = `${match.tx.tx_id}:${match.sourceKey}`;
280
- if (!seen.has(dedupeKey)) {
281
- seen.add(dedupeKey);
282
- results.push(match);
283
- }
284
- }
285
- }
286
- return results;
287
- }
288
-
289
243
  // src/runtime/clarity.ts
290
244
  import { cvToJSON, deserializeCV } from "@secondlayer/stacks/clarity";
291
245
  function decodeClarityValue(hex) {
@@ -318,8 +272,8 @@ function decodeFunctionArgs(args) {
318
272
  }
319
273
 
320
274
  // src/runtime/runner.ts
321
- import { logger as logger2 } from "@secondlayer/shared/logger";
322
275
  import { getErrorMessage } from "@secondlayer/shared";
276
+ import { logger as logger2 } from "@secondlayer/shared/logger";
323
277
  var DEFAULT_ERROR_THRESHOLD = 50;
324
278
  function resolveHandler(handlers, key) {
325
279
  return handlers[key] ?? handlers["*"] ?? null;
@@ -331,7 +285,11 @@ async function runHandlers(subgraph, matched, ctx, opts) {
331
285
  for (const { tx, events, sourceKey: sourceKey2 } of matched) {
332
286
  const handler = resolveHandler(subgraph.handlers, sourceKey2);
333
287
  if (!handler) {
334
- logger2.warn("No handler found for source key", { subgraph: subgraph.name, sourceKey: sourceKey2, txId: tx.tx_id });
288
+ logger2.warn("No handler found for source key", {
289
+ subgraph: subgraph.name,
290
+ sourceKey: sourceKey2,
291
+ txId: tx.tx_id
292
+ });
335
293
  continue;
336
294
  }
337
295
  ctx.setTx({
@@ -408,16 +366,109 @@ async function runHandlers(subgraph, matched, ctx, opts) {
408
366
  return { processed, errors };
409
367
  }
410
368
 
369
+ // src/runtime/source-matcher.ts
370
+ function matchPattern(value, pattern) {
371
+ if (!pattern.includes("*"))
372
+ return value === pattern;
373
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
374
+ return new RegExp(`^${regex}$`).test(value);
375
+ }
376
+ function matchSource(source, transactions, eventsByTx) {
377
+ const results = [];
378
+ const key = sourceKey(source);
379
+ for (const tx of transactions) {
380
+ if (source.type) {
381
+ if (!matchPattern(tx.type, source.type))
382
+ continue;
383
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
384
+ let matchedEvents = txEvents;
385
+ if (source.minAmount !== undefined) {
386
+ const amountEvents = matchedEvents.filter((e) => {
387
+ const data = e.data;
388
+ const rawAmount = data?.amount;
389
+ if (rawAmount === undefined)
390
+ return false;
391
+ const amount = BigInt(rawAmount);
392
+ return amount >= source.minAmount;
393
+ });
394
+ if (amountEvents.length === 0)
395
+ continue;
396
+ matchedEvents = amountEvents;
397
+ }
398
+ results.push({ tx, events: matchedEvents, sourceKey: key });
399
+ continue;
400
+ }
401
+ if (source.contract) {
402
+ const txContractMatch = tx.contract_id && matchPattern(tx.contract_id, source.contract);
403
+ if (source.function && tx.function_name) {
404
+ if (!matchPattern(tx.function_name, source.function))
405
+ continue;
406
+ } else if (source.function && !tx.function_name) {
407
+ continue;
408
+ }
409
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
410
+ let matchedEvents = txEvents;
411
+ if (!txContractMatch) {
412
+ matchedEvents = txEvents.filter((e) => {
413
+ const data = e.data;
414
+ const contractIdentifier = data?.contract_identifier;
415
+ return contractIdentifier && matchPattern(contractIdentifier, source.contract);
416
+ });
417
+ if (matchedEvents.length === 0)
418
+ continue;
419
+ }
420
+ if (source.event) {
421
+ matchedEvents = matchedEvents.filter((e) => {
422
+ if (matchPattern(e.type, source.event))
423
+ return true;
424
+ const data = e.data;
425
+ const topic = data?.topic;
426
+ return topic ? matchPattern(topic, source.event) : false;
427
+ });
428
+ }
429
+ if (txContractMatch || matchedEvents.length > 0) {
430
+ results.push({ tx, events: matchedEvents, sourceKey: key });
431
+ }
432
+ }
433
+ }
434
+ return results;
435
+ }
436
+ function matchSources(sources, transactions, events) {
437
+ const eventsByTx = new Map;
438
+ for (const event of events) {
439
+ const list = eventsByTx.get(event.tx_id) ?? [];
440
+ list.push(event);
441
+ eventsByTx.set(event.tx_id, list);
442
+ }
443
+ const seen = new Set;
444
+ const results = [];
445
+ for (const source of sources) {
446
+ const matches = matchSource(source, transactions, eventsByTx);
447
+ for (const match of matches) {
448
+ const dedupeKey = `${match.tx.tx_id}:${match.sourceKey}`;
449
+ if (!seen.has(dedupeKey)) {
450
+ seen.add(dedupeKey);
451
+ results.push(match);
452
+ }
453
+ }
454
+ }
455
+ return results;
456
+ }
457
+
411
458
  // src/runtime/block-processor.ts
412
- import { sql as sql2 } from "kysely";
413
459
  import { getDb } from "@secondlayer/shared/db";
460
+ import {
461
+ recordSubgraphProcessed,
462
+ updateSubgraphStatus
463
+ } from "@secondlayer/shared/db/queries/subgraphs";
414
464
  import { logger as logger3 } from "@secondlayer/shared/logger";
415
- import { updateSubgraphStatus, recordSubgraphProcessed } from "@secondlayer/shared/db/queries/subgraphs";
465
+ import { sql as sql2 } from "kysely";
416
466
 
417
467
  // src/schema/utils.ts
418
468
  import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
419
469
 
420
470
  // src/runtime/block-processor.ts
471
+ var schemaNameCache = new Map;
421
472
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
422
473
  const db = getDb();
423
474
  const blockStart = performance.now();
@@ -428,19 +479,32 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
428
479
  errors: 0,
429
480
  skipped: false
430
481
  };
431
- const block = await db.selectFrom("blocks").selectAll().where("height", "=", blockHeight).executeTakeFirst();
432
- if (!block) {
433
- logger3.warn("Block not found for subgraph processing", { subgraph: subgraphName, blockHeight });
434
- result.skipped = true;
435
- return result;
436
- }
437
- if (!block.canonical) {
438
- logger3.debug("Skipping non-canonical block", { subgraph: subgraphName, blockHeight });
439
- result.skipped = true;
440
- return result;
482
+ let block, txs, evts;
483
+ if (opts?.preloaded) {
484
+ block = opts.preloaded.block;
485
+ txs = opts.preloaded.txs;
486
+ evts = opts.preloaded.events;
487
+ } else {
488
+ block = await db.selectFrom("blocks").selectAll().where("height", "=", blockHeight).executeTakeFirst();
489
+ if (!block) {
490
+ logger3.warn("Block not found for subgraph processing", {
491
+ subgraph: subgraphName,
492
+ blockHeight
493
+ });
494
+ result.skipped = true;
495
+ return result;
496
+ }
497
+ if (!block.canonical) {
498
+ logger3.debug("Skipping non-canonical block", {
499
+ subgraph: subgraphName,
500
+ blockHeight
501
+ });
502
+ result.skipped = true;
503
+ return result;
504
+ }
505
+ txs = await db.selectFrom("transactions").selectAll().where("block_height", "=", blockHeight).execute();
506
+ evts = await db.selectFrom("events").selectAll().where("block_height", "=", blockHeight).execute();
441
507
  }
442
- const txs = await db.selectFrom("transactions").selectAll().where("block_height", "=", blockHeight).execute();
443
- const evts = await db.selectFrom("events").selectAll().where("block_height", "=", blockHeight).execute();
444
508
  const matched = matchSources(subgraph.sources, txs, evts);
445
509
  result.matched = matched.length;
446
510
  if (matched.length === 0) {
@@ -449,8 +513,12 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
449
513
  }
450
514
  return result;
451
515
  }
452
- const subgraphRecord = await db.selectFrom("subgraphs").select("schema_name").where("name", "=", subgraphName).executeTakeFirst();
453
- const schemaName = subgraphRecord?.schema_name ?? pgSchemaName(subgraphName);
516
+ let schemaName = schemaNameCache.get(subgraphName);
517
+ if (!schemaName) {
518
+ const subgraphRecord = await db.selectFrom("subgraphs").select("schema_name").where("name", "=", subgraphName).executeTakeFirst();
519
+ schemaName = subgraphRecord?.schema_name ?? pgSchemaName(subgraphName);
520
+ schemaNameCache.set(subgraphName, schemaName);
521
+ }
454
522
  const blockMeta = {
455
523
  height: block.height,
456
524
  hash: block.hash,
@@ -499,7 +567,11 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
499
567
  const { rows } = await sql2.raw(`SELECT COUNT(*) as count FROM "${schemaName}"."${table}"`).execute(db);
500
568
  const count = Number(rows[0].count);
501
569
  if (count >= 1e7) {
502
- logger3.warn("Subgraph table exceeds 10M rows", { subgraph: subgraphName, table, count });
570
+ logger3.warn("Subgraph table exceeds 10M rows", {
571
+ subgraph: subgraphName,
572
+ table,
573
+ count
574
+ });
503
575
  }
504
576
  }
505
577
  } catch {}
@@ -510,5 +582,5 @@ export {
510
582
  processBlock
511
583
  };
512
584
 
513
- //# debugId=11059A8A65310A0964756E2164756E21
585
+ //# debugId=256005336347A12F64756E2164756E21
514
586
  //# sourceMappingURL=block-processor.js.map