@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
|
@@ -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
|
}
|
|
@@ -575,10 +647,14 @@ class StatsAccumulator {
|
|
|
575
647
|
}
|
|
576
648
|
|
|
577
649
|
// src/runtime/reindex.ts
|
|
650
|
+
import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
|
|
578
651
|
import { getDb as getDb2, getRawClient } from "@secondlayer/shared/db";
|
|
652
|
+
import {
|
|
653
|
+
recordGapBatch,
|
|
654
|
+
resolveGaps
|
|
655
|
+
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
579
656
|
import { updateSubgraphStatus as updateSubgraphStatus2 } from "@secondlayer/shared/db/queries/subgraphs";
|
|
580
657
|
import { logger as logger4 } from "@secondlayer/shared/logger";
|
|
581
|
-
import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
|
|
582
658
|
|
|
583
659
|
// src/schema/generator.ts
|
|
584
660
|
var TYPE_MAP = {
|
|
@@ -665,45 +741,121 @@ function generateSubgraphSQL(def, schemaNameOverride) {
|
|
|
665
741
|
return { statements, hash };
|
|
666
742
|
}
|
|
667
743
|
|
|
744
|
+
// src/runtime/batch-loader.ts
|
|
745
|
+
async function loadBlockRange(db, fromHeight, toHeight) {
|
|
746
|
+
const [blocks, txs, events] = await Promise.all([
|
|
747
|
+
db.selectFrom("blocks").selectAll().where("height", ">=", fromHeight).where("height", "<=", toHeight).where("canonical", "=", true).execute(),
|
|
748
|
+
db.selectFrom("transactions").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
|
|
749
|
+
db.selectFrom("events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute()
|
|
750
|
+
]);
|
|
751
|
+
const txsByHeight = new Map;
|
|
752
|
+
for (const tx of txs) {
|
|
753
|
+
const h = Number(tx.block_height);
|
|
754
|
+
const list = txsByHeight.get(h) ?? [];
|
|
755
|
+
list.push(tx);
|
|
756
|
+
txsByHeight.set(h, list);
|
|
757
|
+
}
|
|
758
|
+
const eventsByHeight = new Map;
|
|
759
|
+
for (const evt of events) {
|
|
760
|
+
const h = Number(evt.block_height);
|
|
761
|
+
const list = eventsByHeight.get(h) ?? [];
|
|
762
|
+
list.push(evt);
|
|
763
|
+
eventsByHeight.set(h, list);
|
|
764
|
+
}
|
|
765
|
+
const result = new Map;
|
|
766
|
+
for (const block of blocks) {
|
|
767
|
+
const h = Number(block.height);
|
|
768
|
+
result.set(h, {
|
|
769
|
+
block,
|
|
770
|
+
txs: txsByHeight.get(h) ?? [],
|
|
771
|
+
events: eventsByHeight.get(h) ?? []
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
776
|
+
function avgEventsPerBlock(batch) {
|
|
777
|
+
if (batch.size === 0)
|
|
778
|
+
return 0;
|
|
779
|
+
let totalEvents = 0;
|
|
780
|
+
for (const data of batch.values()) {
|
|
781
|
+
totalEvents += data.events.length;
|
|
782
|
+
}
|
|
783
|
+
return totalEvents / batch.size;
|
|
784
|
+
}
|
|
785
|
+
|
|
668
786
|
// src/runtime/reindex.ts
|
|
669
787
|
var LOG_INTERVAL = 1000;
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
let toBlock;
|
|
686
|
-
if (opts?.toBlock != null) {
|
|
687
|
-
toBlock = opts.toBlock;
|
|
788
|
+
var DEFAULT_BATCH_SIZE = 500;
|
|
789
|
+
var MIN_BATCH_SIZE = 100;
|
|
790
|
+
var MAX_BATCH_SIZE = 1000;
|
|
791
|
+
function coalesceFailedBlocks(blocks) {
|
|
792
|
+
if (blocks.length === 0)
|
|
793
|
+
return [];
|
|
794
|
+
blocks.sort((a, b) => a.height - b.height);
|
|
795
|
+
const ranges = [];
|
|
796
|
+
let start = blocks[0].height;
|
|
797
|
+
let end = blocks[0].height;
|
|
798
|
+
let reason = blocks[0].reason;
|
|
799
|
+
for (let i = 1;i < blocks.length; i++) {
|
|
800
|
+
const b = blocks[i];
|
|
801
|
+
if (b.height === end + 1 && b.reason === reason) {
|
|
802
|
+
end = b.height;
|
|
688
803
|
} else {
|
|
689
|
-
|
|
690
|
-
|
|
804
|
+
ranges.push({ start, end, reason });
|
|
805
|
+
start = b.height;
|
|
806
|
+
end = b.height;
|
|
807
|
+
reason = b.reason;
|
|
691
808
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
809
|
+
}
|
|
810
|
+
ranges.push({ start, end, reason });
|
|
811
|
+
return ranges;
|
|
812
|
+
}
|
|
813
|
+
async function processBlockRange(def, opts) {
|
|
814
|
+
const db = getDb2();
|
|
815
|
+
const subgraphName = def.name;
|
|
816
|
+
const { fromBlock, toBlock, status } = opts;
|
|
817
|
+
const totalBlocks = toBlock - fromBlock + 1;
|
|
818
|
+
const stats = new StatsAccumulator(subgraphName, opts.apiKeyId, opts.isCatchup);
|
|
819
|
+
let blocksProcessed = 0;
|
|
820
|
+
let totalEventsProcessed = 0;
|
|
821
|
+
let totalErrors = 0;
|
|
822
|
+
let batchSize = DEFAULT_BATCH_SIZE;
|
|
823
|
+
let currentHeight = fromBlock;
|
|
824
|
+
let nextBatchPromise = loadBlockRange(db, currentHeight, Math.min(currentHeight + batchSize - 1, toBlock));
|
|
825
|
+
while (currentHeight <= toBlock) {
|
|
826
|
+
const batch = await nextBatchPromise;
|
|
827
|
+
const batchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
|
|
828
|
+
const nextStart = batchEnd + 1;
|
|
829
|
+
if (nextStart <= toBlock) {
|
|
830
|
+
const nextEnd = Math.min(nextStart + batchSize - 1, toBlock);
|
|
831
|
+
nextBatchPromise = loadBlockRange(db, nextStart, nextEnd);
|
|
696
832
|
}
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
833
|
+
const batchFailedBlocks = [];
|
|
834
|
+
for (let height = currentHeight;height <= batchEnd; height++) {
|
|
835
|
+
const blockData = batch.get(height);
|
|
836
|
+
if (!blockData) {
|
|
837
|
+
batchFailedBlocks.push({ height, reason: "block_missing" });
|
|
838
|
+
blocksProcessed++;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
let result;
|
|
842
|
+
try {
|
|
843
|
+
result = await processBlock(def, subgraphName, height, {
|
|
844
|
+
skipProgressUpdate: true,
|
|
845
|
+
preloaded: blockData
|
|
846
|
+
});
|
|
847
|
+
} catch (err) {
|
|
848
|
+
logger4.error("Block processing error", {
|
|
849
|
+
subgraph: subgraphName,
|
|
850
|
+
blockHeight: height,
|
|
851
|
+
error: err instanceof Error ? err.message : String(err)
|
|
852
|
+
});
|
|
853
|
+
batchFailedBlocks.push({ height, reason: "processing_error" });
|
|
854
|
+
await updateSubgraphStatus2(db, subgraphName, status, height).catch(() => {});
|
|
855
|
+
blocksProcessed++;
|
|
856
|
+
totalErrors++;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
707
859
|
blocksProcessed++;
|
|
708
860
|
totalEventsProcessed += result.processed;
|
|
709
861
|
totalErrors += result.errors;
|
|
@@ -713,11 +865,11 @@ async function reindexSubgraph(def, opts) {
|
|
|
713
865
|
await stats.flush(db);
|
|
714
866
|
}
|
|
715
867
|
}
|
|
716
|
-
if (blocksProcessed %
|
|
717
|
-
await updateSubgraphStatus2(db, subgraphName,
|
|
868
|
+
if (blocksProcessed % 100 === 0) {
|
|
869
|
+
await updateSubgraphStatus2(db, subgraphName, status, height);
|
|
718
870
|
}
|
|
719
871
|
if (blocksProcessed % LOG_INTERVAL === 0) {
|
|
720
|
-
logger4.info("Reindex
|
|
872
|
+
logger4.info(`${status === "reindexing" ? "Reindex" : "Backfill"} progress`, {
|
|
721
873
|
subgraph: subgraphName,
|
|
722
874
|
processed: blocksProcessed,
|
|
723
875
|
total: totalBlocks,
|
|
@@ -726,14 +878,87 @@ async function reindexSubgraph(def, opts) {
|
|
|
726
878
|
});
|
|
727
879
|
}
|
|
728
880
|
}
|
|
729
|
-
|
|
881
|
+
if (batchFailedBlocks.length > 0 && opts.subgraphId) {
|
|
882
|
+
const gaps = coalesceFailedBlocks(batchFailedBlocks);
|
|
883
|
+
await recordGapBatch(db, opts.subgraphId, subgraphName, gaps).catch((err) => {
|
|
884
|
+
logger4.warn("Failed to record subgraph gaps", {
|
|
885
|
+
subgraph: subgraphName,
|
|
886
|
+
error: err instanceof Error ? err.message : String(err)
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
const avg = avgEventsPerBlock(batch);
|
|
891
|
+
if (avg > 50)
|
|
892
|
+
batchSize = Math.max(Math.round(batchSize * 0.5), MIN_BATCH_SIZE);
|
|
893
|
+
else if (avg < 10)
|
|
894
|
+
batchSize = Math.min(Math.round(batchSize * 1.5), MAX_BATCH_SIZE);
|
|
895
|
+
currentHeight = batchEnd + 1;
|
|
896
|
+
}
|
|
897
|
+
await stats.flush(db);
|
|
898
|
+
return { blocksProcessed, totalEventsProcessed, totalErrors };
|
|
899
|
+
}
|
|
900
|
+
async function resolveBlockRange(db, opts) {
|
|
901
|
+
const fromBlock = opts?.fromBlock ?? 1;
|
|
902
|
+
let toBlock;
|
|
903
|
+
if (opts?.toBlock != null) {
|
|
904
|
+
toBlock = opts.toBlock;
|
|
905
|
+
} else {
|
|
906
|
+
const progress = await db.selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
|
|
907
|
+
toBlock = progress?.last_indexed_block ?? progress?.last_contiguous_block ?? 0;
|
|
908
|
+
}
|
|
909
|
+
return { fromBlock, toBlock };
|
|
910
|
+
}
|
|
911
|
+
async function reindexSubgraph(def, opts) {
|
|
912
|
+
const db = getDb2();
|
|
913
|
+
const client = getRawClient();
|
|
914
|
+
const subgraphName = def.name;
|
|
915
|
+
const schemaName = opts?.schemaName ?? pgSchemaName(subgraphName);
|
|
916
|
+
await updateSubgraphStatus2(db, subgraphName, "reindexing");
|
|
917
|
+
logger4.info("Reindex starting", { subgraph: subgraphName });
|
|
918
|
+
try {
|
|
919
|
+
await client.unsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
|
|
920
|
+
const { statements } = generateSubgraphSQL(def, schemaName);
|
|
921
|
+
for (const stmt of statements) {
|
|
922
|
+
await client.unsafe(stmt);
|
|
923
|
+
}
|
|
924
|
+
logger4.info("Schema recreated for reindex", { subgraph: subgraphName });
|
|
925
|
+
const { fromBlock, toBlock } = await resolveBlockRange(db, opts);
|
|
926
|
+
if (fromBlock > toBlock) {
|
|
927
|
+
logger4.info("No blocks to reindex", {
|
|
928
|
+
subgraph: subgraphName,
|
|
929
|
+
fromBlock,
|
|
930
|
+
toBlock
|
|
931
|
+
});
|
|
932
|
+
await updateSubgraphStatus2(db, subgraphName, "active", 0);
|
|
933
|
+
return { processed: 0 };
|
|
934
|
+
}
|
|
935
|
+
logger4.info("Reindexing blocks", {
|
|
936
|
+
subgraph: subgraphName,
|
|
937
|
+
fromBlock,
|
|
938
|
+
toBlock,
|
|
939
|
+
totalBlocks: toBlock - fromBlock + 1
|
|
940
|
+
});
|
|
941
|
+
const subgraphRow = await db.selectFrom("subgraphs").select(["id", "api_key_id"]).where("name", "=", subgraphName).executeTakeFirst();
|
|
942
|
+
const result = await processBlockRange(def, {
|
|
943
|
+
fromBlock,
|
|
944
|
+
toBlock,
|
|
945
|
+
status: "reindexing",
|
|
946
|
+
isCatchup: false,
|
|
947
|
+
apiKeyId: subgraphRow?.api_key_id ?? null,
|
|
948
|
+
subgraphId: subgraphRow?.id
|
|
949
|
+
});
|
|
730
950
|
const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
|
|
731
|
-
if (totalEventsProcessed > 0 || totalErrors > 0) {
|
|
732
|
-
await recordSubgraphProcessed2(db, subgraphName, totalEventsProcessed, totalErrors, totalErrors > 0 ? `${totalErrors} error(s) during reindex` : undefined);
|
|
951
|
+
if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
|
|
952
|
+
await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during reindex` : undefined);
|
|
733
953
|
}
|
|
734
954
|
await updateSubgraphStatus2(db, subgraphName, "active", toBlock);
|
|
735
|
-
logger4.info("Reindex complete", {
|
|
736
|
-
|
|
955
|
+
logger4.info("Reindex complete", {
|
|
956
|
+
subgraph: subgraphName,
|
|
957
|
+
blocks: result.blocksProcessed,
|
|
958
|
+
events: result.totalEventsProcessed,
|
|
959
|
+
errors: result.totalErrors
|
|
960
|
+
});
|
|
961
|
+
return { processed: result.blocksProcessed };
|
|
737
962
|
} catch (err) {
|
|
738
963
|
logger4.error("Reindex failed", {
|
|
739
964
|
subgraph: subgraphName,
|
|
@@ -743,9 +968,54 @@ async function reindexSubgraph(def, opts) {
|
|
|
743
968
|
throw err;
|
|
744
969
|
}
|
|
745
970
|
}
|
|
971
|
+
async function backfillSubgraph(def, opts) {
|
|
972
|
+
const db = getDb2();
|
|
973
|
+
const subgraphName = def.name;
|
|
974
|
+
logger4.info("Backfill starting", {
|
|
975
|
+
subgraph: subgraphName,
|
|
976
|
+
from: opts.fromBlock,
|
|
977
|
+
to: opts.toBlock
|
|
978
|
+
});
|
|
979
|
+
try {
|
|
980
|
+
const subgraphRow = await db.selectFrom("subgraphs").select(["id", "api_key_id"]).where("name", "=", subgraphName).executeTakeFirst();
|
|
981
|
+
const result = await processBlockRange(def, {
|
|
982
|
+
fromBlock: opts.fromBlock,
|
|
983
|
+
toBlock: opts.toBlock,
|
|
984
|
+
status: "active",
|
|
985
|
+
isCatchup: false,
|
|
986
|
+
apiKeyId: subgraphRow?.api_key_id ?? null,
|
|
987
|
+
subgraphId: subgraphRow?.id
|
|
988
|
+
});
|
|
989
|
+
const resolved = await resolveGaps(db, subgraphName, opts.fromBlock, opts.toBlock).catch(() => 0);
|
|
990
|
+
if (resolved > 0) {
|
|
991
|
+
logger4.info("Resolved subgraph gaps via backfill", {
|
|
992
|
+
subgraph: subgraphName,
|
|
993
|
+
resolved
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
|
|
997
|
+
if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
|
|
998
|
+
await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during backfill` : undefined);
|
|
999
|
+
}
|
|
1000
|
+
logger4.info("Backfill complete", {
|
|
1001
|
+
subgraph: subgraphName,
|
|
1002
|
+
blocks: result.blocksProcessed,
|
|
1003
|
+
events: result.totalEventsProcessed,
|
|
1004
|
+
errors: result.totalErrors
|
|
1005
|
+
});
|
|
1006
|
+
return { processed: result.blocksProcessed };
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
logger4.error("Backfill failed", {
|
|
1009
|
+
subgraph: subgraphName,
|
|
1010
|
+
error: getErrorMessage2(err)
|
|
1011
|
+
});
|
|
1012
|
+
throw err;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
746
1015
|
export {
|
|
747
|
-
reindexSubgraph
|
|
1016
|
+
reindexSubgraph,
|
|
1017
|
+
backfillSubgraph
|
|
748
1018
|
};
|
|
749
1019
|
|
|
750
|
-
//# debugId=
|
|
1020
|
+
//# debugId=114013EAB31F34E164756E2164756E21
|
|
751
1021
|
//# sourceMappingURL=reindex.js.map
|