@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.
- package/dist/src/index.d.ts +17 -5
- package/dist/src/index.js +467 -191
- package/dist/src/index.js.map +16 -15
- package/dist/src/runtime/block-processor.d.ts +9 -1
- package/dist/src/runtime/block-processor.js +216 -144
- package/dist/src/runtime/block-processor.js.map +9 -9
- package/dist/src/runtime/catchup.d.ts +2 -2
- package/dist/src/runtime/catchup.js +366 -160
- package/dist/src/runtime/catchup.js.map +12 -11
- package/dist/src/runtime/clarity.js.map +2 -2
- package/dist/src/runtime/context.d.ts +4 -2
- package/dist/src/runtime/context.js +79 -36
- package/dist/src/runtime/context.js.map +3 -3
- package/dist/src/runtime/processor.js +384 -166
- package/dist/src/runtime/processor.js.map +14 -13
- package/dist/src/runtime/reindex.d.ts +13 -1
- package/dist/src/runtime/reindex.js +459 -189
- package/dist/src/runtime/reindex.js.map +13 -12
- package/dist/src/runtime/reorg.js +229 -148
- package/dist/src/runtime/reorg.js.map +10 -10
- package/dist/src/runtime/runner.d.ts +4 -2
- package/dist/src/runtime/runner.js +7 -3
- package/dist/src/runtime/runner.js.map +4 -4
- package/dist/src/runtime/source-matcher.js.map +3 -3
- package/dist/src/runtime/stats.d.ts +1 -1
- package/dist/src/runtime/stats.js +2 -2
- package/dist/src/runtime/stats.js.map +3 -3
- package/dist/src/schema/index.d.ts +1 -1
- package/dist/src/schema/index.js +9 -3
- package/dist/src/schema/index.js.map +5 -5
- package/dist/src/service.js +385 -167
- package/dist/src/service.js.map +15 -14
- package/dist/src/templates.js.map +2 -2
- package/dist/src/types.js.map +2 -2
- package/dist/src/validate.js +2 -2
- package/dist/src/validate.js.map +3 -3
- package/package.json +51 -51
package/dist/src/service.js
CHANGED
|
@@ -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({
|
|
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({
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", {
|
|
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 {
|
|
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
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
453
|
-
|
|
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", {
|
|
570
|
+
logger3.warn("Subgraph table exceeds 10M rows", {
|
|
571
|
+
subgraph: subgraphName,
|
|
572
|
+
table,
|
|
573
|
+
count
|
|
574
|
+
});
|
|
503
575
|
}
|
|
504
576
|
}
|
|
505
577
|
} catch {}
|
|
@@ -553,7 +625,7 @@ class StatsAccumulator {
|
|
|
553
625
|
flush_time_ms: Math.round(entry.flushTimeMs),
|
|
554
626
|
max_block_time_ms: Math.round(entry.maxBlockTimeMs),
|
|
555
627
|
max_handler_time_ms: Math.round(entry.maxHandlerTimeMs),
|
|
556
|
-
avg_ops_per_block: parseFloat(avgOpsPerBlock.toFixed(2)),
|
|
628
|
+
avg_ops_per_block: Number.parseFloat(avgOpsPerBlock.toFixed(2)),
|
|
557
629
|
is_catchup: entry.isCatchup
|
|
558
630
|
}).execute();
|
|
559
631
|
}
|
|
@@ -576,10 +648,89 @@ class StatsAccumulator {
|
|
|
576
648
|
|
|
577
649
|
// src/runtime/catchup.ts
|
|
578
650
|
import { getDb as getDb2 } from "@secondlayer/shared/db";
|
|
579
|
-
import {
|
|
651
|
+
import {
|
|
652
|
+
recordGapBatch
|
|
653
|
+
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
580
654
|
import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
|
|
655
|
+
import { logger as logger4 } from "@secondlayer/shared/logger";
|
|
656
|
+
|
|
657
|
+
// src/runtime/batch-loader.ts
|
|
658
|
+
async function loadBlockRange(db, fromHeight, toHeight) {
|
|
659
|
+
const [blocks, txs, events] = await Promise.all([
|
|
660
|
+
db.selectFrom("blocks").selectAll().where("height", ">=", fromHeight).where("height", "<=", toHeight).where("canonical", "=", true).execute(),
|
|
661
|
+
db.selectFrom("transactions").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
|
|
662
|
+
db.selectFrom("events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute()
|
|
663
|
+
]);
|
|
664
|
+
const txsByHeight = new Map;
|
|
665
|
+
for (const tx of txs) {
|
|
666
|
+
const h = Number(tx.block_height);
|
|
667
|
+
const list = txsByHeight.get(h) ?? [];
|
|
668
|
+
list.push(tx);
|
|
669
|
+
txsByHeight.set(h, list);
|
|
670
|
+
}
|
|
671
|
+
const eventsByHeight = new Map;
|
|
672
|
+
for (const evt of events) {
|
|
673
|
+
const h = Number(evt.block_height);
|
|
674
|
+
const list = eventsByHeight.get(h) ?? [];
|
|
675
|
+
list.push(evt);
|
|
676
|
+
eventsByHeight.set(h, list);
|
|
677
|
+
}
|
|
678
|
+
const result = new Map;
|
|
679
|
+
for (const block of blocks) {
|
|
680
|
+
const h = Number(block.height);
|
|
681
|
+
result.set(h, {
|
|
682
|
+
block,
|
|
683
|
+
txs: txsByHeight.get(h) ?? [],
|
|
684
|
+
events: eventsByHeight.get(h) ?? []
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return result;
|
|
688
|
+
}
|
|
689
|
+
function avgEventsPerBlock(batch) {
|
|
690
|
+
if (batch.size === 0)
|
|
691
|
+
return 0;
|
|
692
|
+
let totalEvents = 0;
|
|
693
|
+
for (const data of batch.values()) {
|
|
694
|
+
totalEvents += data.events.length;
|
|
695
|
+
}
|
|
696
|
+
return totalEvents / batch.size;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/runtime/catchup.ts
|
|
581
700
|
var LOG_INTERVAL = 1000;
|
|
701
|
+
var DEFAULT_BATCH_SIZE = 500;
|
|
702
|
+
var MIN_BATCH_SIZE = 100;
|
|
703
|
+
var MAX_BATCH_SIZE = 1000;
|
|
582
704
|
var catchingUp = new Set;
|
|
705
|
+
function coalesceGaps(blocks) {
|
|
706
|
+
if (blocks.length === 0)
|
|
707
|
+
return [];
|
|
708
|
+
blocks.sort((a, b) => a.height - b.height);
|
|
709
|
+
const ranges = [];
|
|
710
|
+
let start = blocks[0].height;
|
|
711
|
+
let end = blocks[0].height;
|
|
712
|
+
let reason = blocks[0].reason;
|
|
713
|
+
for (let i = 1;i < blocks.length; i++) {
|
|
714
|
+
const b = blocks[i];
|
|
715
|
+
if (b.height === end + 1 && b.reason === reason) {
|
|
716
|
+
end = b.height;
|
|
717
|
+
} else {
|
|
718
|
+
ranges.push({ start, end, reason });
|
|
719
|
+
start = b.height;
|
|
720
|
+
end = b.height;
|
|
721
|
+
reason = b.reason;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
ranges.push({ start, end, reason });
|
|
725
|
+
return ranges;
|
|
726
|
+
}
|
|
727
|
+
function adjustBatchSize(current, avgEvents) {
|
|
728
|
+
if (avgEvents > 50)
|
|
729
|
+
return Math.max(Math.round(current * 0.5), MIN_BATCH_SIZE);
|
|
730
|
+
if (avgEvents < 10)
|
|
731
|
+
return Math.min(Math.round(current * 1.5), MAX_BATCH_SIZE);
|
|
732
|
+
return current;
|
|
733
|
+
}
|
|
583
734
|
async function catchUpSubgraph(subgraph, subgraphName) {
|
|
584
735
|
if (catchingUp.has(subgraphName))
|
|
585
736
|
return 0;
|
|
@@ -606,24 +757,79 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
606
757
|
});
|
|
607
758
|
const stats = new StatsAccumulator(subgraphName, subgraphRow.api_key_id, true);
|
|
608
759
|
let processed = 0;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
760
|
+
let batchSize = DEFAULT_BATCH_SIZE;
|
|
761
|
+
let currentHeight = startBlock;
|
|
762
|
+
let nextBatchPromise = loadBlockRange(db, currentHeight, Math.min(currentHeight + batchSize - 1, chainTip));
|
|
763
|
+
while (currentHeight <= chainTip) {
|
|
764
|
+
const currentRow = await getSubgraph(db, subgraphName);
|
|
765
|
+
if (!currentRow || currentRow.status !== "active") {
|
|
766
|
+
logger4.info("Subgraph status changed, stopping catch-up", {
|
|
767
|
+
subgraph: subgraphName,
|
|
768
|
+
status: currentRow?.status ?? "deleted"
|
|
769
|
+
});
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
const batch = await nextBatchPromise;
|
|
773
|
+
const batchEnd = Math.min(currentHeight + batchSize - 1, chainTip);
|
|
774
|
+
const nextStart = batchEnd + 1;
|
|
775
|
+
if (nextStart <= chainTip) {
|
|
776
|
+
const nextEnd = Math.min(nextStart + batchSize - 1, chainTip);
|
|
777
|
+
nextBatchPromise = loadBlockRange(db, nextStart, nextEnd);
|
|
778
|
+
}
|
|
779
|
+
const batchFailedBlocks = [];
|
|
780
|
+
for (let height = currentHeight;height <= batchEnd; height++) {
|
|
781
|
+
const blockData = batch.get(height);
|
|
782
|
+
if (!blockData) {
|
|
783
|
+
batchFailedBlocks.push({ height, reason: "block_missing" });
|
|
784
|
+
processed++;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
let result;
|
|
788
|
+
try {
|
|
789
|
+
result = await processBlock(subgraph, subgraphName, height, {
|
|
790
|
+
preloaded: blockData
|
|
791
|
+
});
|
|
792
|
+
} catch (err) {
|
|
793
|
+
logger4.error("Block processing error during catch-up", {
|
|
794
|
+
subgraph: subgraphName,
|
|
795
|
+
blockHeight: height,
|
|
796
|
+
error: err instanceof Error ? err.message : String(err)
|
|
797
|
+
});
|
|
798
|
+
batchFailedBlocks.push({ height, reason: "processing_error" });
|
|
799
|
+
const { updateSubgraphStatus: updateSubgraphStatus2 } = await import("@secondlayer/shared/db/queries/subgraphs");
|
|
800
|
+
await updateSubgraphStatus2(db, subgraphName, "active", height).catch(() => {});
|
|
801
|
+
processed++;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
processed++;
|
|
805
|
+
if (result.timing) {
|
|
806
|
+
stats.record(result.timing, result.processed);
|
|
807
|
+
if (stats.shouldFlush()) {
|
|
808
|
+
await stats.flush(db);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (processed % LOG_INTERVAL === 0) {
|
|
812
|
+
logger4.info("Subgraph catch-up progress", {
|
|
813
|
+
subgraph: subgraphName,
|
|
814
|
+
processed,
|
|
815
|
+
total: totalBlocks,
|
|
816
|
+
currentBlock: height,
|
|
817
|
+
pct: Math.round(processed / totalBlocks * 100)
|
|
818
|
+
});
|
|
616
819
|
}
|
|
617
820
|
}
|
|
618
|
-
if (
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
821
|
+
if (batchFailedBlocks.length > 0) {
|
|
822
|
+
const gaps = coalesceGaps(batchFailedBlocks);
|
|
823
|
+
await recordGapBatch(db, subgraphRow.id, subgraphName, gaps).catch((err) => {
|
|
824
|
+
logger4.warn("Failed to record subgraph gaps", {
|
|
825
|
+
subgraph: subgraphName,
|
|
826
|
+
error: err instanceof Error ? err.message : String(err)
|
|
827
|
+
});
|
|
625
828
|
});
|
|
626
829
|
}
|
|
830
|
+
const avg = avgEventsPerBlock(batch);
|
|
831
|
+
batchSize = adjustBatchSize(batchSize, avg);
|
|
832
|
+
currentHeight = batchEnd + 1;
|
|
627
833
|
}
|
|
628
834
|
await stats.flush(db);
|
|
629
835
|
logger4.info("Subgraph catch-up complete", {
|
|
@@ -637,17 +843,20 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
637
843
|
}
|
|
638
844
|
|
|
639
845
|
// src/runtime/reorg.ts
|
|
846
|
+
import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
|
|
640
847
|
import { getDb as getDb3, getRawClient } from "@secondlayer/shared/db";
|
|
641
848
|
import { listSubgraphs } from "@secondlayer/shared/db/queries/subgraphs";
|
|
642
849
|
import { logger as logger5 } from "@secondlayer/shared/logger";
|
|
643
|
-
import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
|
|
644
850
|
async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
645
851
|
const db = getDb3();
|
|
646
852
|
const client = getRawClient();
|
|
647
853
|
const activeSubgraphs = (await listSubgraphs(db)).filter((v) => v.status === "active");
|
|
648
854
|
if (activeSubgraphs.length === 0)
|
|
649
855
|
return;
|
|
650
|
-
logger5.info("Propagating reorg to subgraphs", {
|
|
856
|
+
logger5.info("Propagating reorg to subgraphs", {
|
|
857
|
+
blockHeight,
|
|
858
|
+
subgraphCount: activeSubgraphs.length
|
|
859
|
+
});
|
|
651
860
|
for (const sg of activeSubgraphs) {
|
|
652
861
|
try {
|
|
653
862
|
const schema = sg.definition.schema;
|
|
@@ -657,10 +866,16 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
|
657
866
|
for (const tableName of Object.keys(schema)) {
|
|
658
867
|
await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" = $1`, [blockHeight]);
|
|
659
868
|
}
|
|
660
|
-
logger5.info("Subgraph reorg cleanup done", {
|
|
869
|
+
logger5.info("Subgraph reorg cleanup done", {
|
|
870
|
+
subgraph: sg.name,
|
|
871
|
+
blockHeight
|
|
872
|
+
});
|
|
661
873
|
const def = await loadSubgraphDef(sg.handler_path);
|
|
662
874
|
await processBlock(def, sg.name, blockHeight);
|
|
663
|
-
logger5.info("Subgraph reorg reprocessed", {
|
|
875
|
+
logger5.info("Subgraph reorg reprocessed", {
|
|
876
|
+
subgraph: sg.name,
|
|
877
|
+
blockHeight
|
|
878
|
+
});
|
|
664
879
|
} catch (err) {
|
|
665
880
|
logger5.error("Subgraph reorg handling failed", {
|
|
666
881
|
subgraph: sg.name,
|
|
@@ -672,11 +887,14 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
|
672
887
|
}
|
|
673
888
|
|
|
674
889
|
// src/runtime/processor.ts
|
|
890
|
+
import { getErrorMessage as getErrorMessage3 } from "@secondlayer/shared";
|
|
675
891
|
import { getDb as getDb4 } from "@secondlayer/shared/db";
|
|
892
|
+
import {
|
|
893
|
+
listSubgraphs as listSubgraphs2,
|
|
894
|
+
updateSubgraphStatus as updateSubgraphStatus2
|
|
895
|
+
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
676
896
|
import { logger as logger6 } from "@secondlayer/shared/logger";
|
|
677
|
-
import { getErrorMessage as getErrorMessage3 } from "@secondlayer/shared";
|
|
678
897
|
import { listen } from "@secondlayer/shared/queue/listener";
|
|
679
|
-
import { listSubgraphs as listSubgraphs2, updateSubgraphStatus as updateSubgraphStatus2 } from "@secondlayer/shared/db/queries/subgraphs";
|
|
680
898
|
var CHANNEL_NEW_BLOCK = "streams:new_job";
|
|
681
899
|
var DEFAULT_CONCURRENCY = 5;
|
|
682
900
|
var POLL_INTERVAL_MS = 5000;
|
|
@@ -779,7 +997,7 @@ async function startSubgraphProcessor(opts) {
|
|
|
779
997
|
// src/service.ts
|
|
780
998
|
import { logger as logger7 } from "@secondlayer/shared/logger";
|
|
781
999
|
var processor = await startSubgraphProcessor({
|
|
782
|
-
concurrency: parseInt(process.env.SUBGRAPH_CONCURRENCY ?? "5")
|
|
1000
|
+
concurrency: Number.parseInt(process.env.SUBGRAPH_CONCURRENCY ?? "5")
|
|
783
1001
|
});
|
|
784
1002
|
var shutdown = async () => {
|
|
785
1003
|
logger7.info("Shutting down subgraph processor...");
|
|
@@ -789,5 +1007,5 @@ var shutdown = async () => {
|
|
|
789
1007
|
process.on("SIGINT", shutdown);
|
|
790
1008
|
process.on("SIGTERM", shutdown);
|
|
791
1009
|
|
|
792
|
-
//# debugId=
|
|
1010
|
+
//# debugId=BC466B3425B1559464756E2164756E21
|
|
793
1011
|
//# sourceMappingURL=service.js.map
|