@secondlayer/subgraphs 3.21.0 → 3.23.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 +238 -17
  2. package/dist/src/index.js +389 -52
  3. package/dist/src/index.js.map +12 -11
  4. package/dist/src/runtime/block-processor.d.ts +111 -10
  5. package/dist/src/runtime/block-processor.js +263 -31
  6. package/dist/src/runtime/block-processor.js.map +9 -8
  7. package/dist/src/runtime/catchup.d.ts +111 -10
  8. package/dist/src/runtime/catchup.js +263 -31
  9. package/dist/src/runtime/catchup.js.map +9 -8
  10. package/dist/src/runtime/context.d.ts +111 -11
  11. package/dist/src/runtime/context.js +166 -12
  12. package/dist/src/runtime/context.js.map +7 -6
  13. package/dist/src/runtime/processor.js +272 -35
  14. package/dist/src/runtime/processor.js.map +10 -9
  15. package/dist/src/runtime/reindex.d.ts +111 -10
  16. package/dist/src/runtime/reindex.js +263 -31
  17. package/dist/src/runtime/reindex.js.map +9 -8
  18. package/dist/src/runtime/reorg.d.ts +111 -10
  19. package/dist/src/runtime/reorg.js +268 -31
  20. package/dist/src/runtime/reorg.js.map +10 -9
  21. package/dist/src/runtime/replay.d.ts +3 -3
  22. package/dist/src/runtime/replay.js +30 -6
  23. package/dist/src/runtime/replay.js.map +4 -4
  24. package/dist/src/runtime/runner.d.ts +191 -17
  25. package/dist/src/runtime/source-matcher.d.ts +68 -8
  26. package/dist/src/runtime/source-matcher.js +30 -6
  27. package/dist/src/runtime/source-matcher.js.map +3 -3
  28. package/dist/src/runtime/stats.d.ts +3 -3
  29. package/dist/src/schema/index.d.ts +114 -13
  30. package/dist/src/schema/index.js +133 -20
  31. package/dist/src/schema/index.js.map +5 -5
  32. package/dist/src/service.js +272 -35
  33. package/dist/src/service.js.map +10 -9
  34. package/dist/src/testing/index.d.ts +667 -0
  35. package/dist/src/testing/index.js +1463 -0
  36. package/dist/src/testing/index.js.map +17 -0
  37. package/dist/src/triggers/index.d.ts +57 -6
  38. package/dist/src/types.d.ts +112 -11
  39. package/dist/src/validate.d.ts +129 -11
  40. package/dist/src/validate.js +117 -18
  41. package/dist/src/validate.js.map +3 -3
  42. package/package.json +9 -3
@@ -0,0 +1,1463 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/runtime/context.ts
5
+ import { logger } from "@secondlayer/shared/logger";
6
+ import { formatUnits } from "@secondlayer/stacks/utils";
7
+ import { sql } from "kysely";
8
+
9
+ // src/schema/generator.ts
10
+ import { createHash } from "node:crypto";
11
+
12
+ // src/schema/utils.ts
13
+ import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
14
+
15
+ // src/schema/generator.ts
16
+ var TYPE_MAP = {
17
+ text: "TEXT",
18
+ uint: "NUMERIC",
19
+ int: "NUMERIC",
20
+ principal: "TEXT",
21
+ boolean: "BOOLEAN",
22
+ timestamp: "TIMESTAMPTZ",
23
+ jsonb: "JSONB"
24
+ };
25
+ function escapeLiteralDefault(value) {
26
+ if (value === null || value === undefined)
27
+ return "NULL";
28
+ if (typeof value === "number" || typeof value === "bigint")
29
+ return String(value);
30
+ if (typeof value === "boolean")
31
+ return value ? "TRUE" : "FALSE";
32
+ return `'${String(value).replace(/'/g, "''")}'`;
33
+ }
34
+ function tableNeedsTrgm(tableDef) {
35
+ return Object.values(tableDef.columns).some((col) => col.search);
36
+ }
37
+ function emitTableDDL(schemaName, tableName, tableDef) {
38
+ const qualifiedName = `${schemaName}.${tableName}`;
39
+ const statements = [];
40
+ const columnDefs = [
41
+ "_id BIGSERIAL PRIMARY KEY",
42
+ "_block_height BIGINT NOT NULL",
43
+ "_tx_id TEXT NOT NULL",
44
+ "_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
45
+ ];
46
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
47
+ const sqlType = TYPE_MAP[col.type];
48
+ const nullable = col.nullable ? "" : " NOT NULL";
49
+ let colDef = `${colName} ${sqlType}${nullable}`;
50
+ if (col.default !== undefined) {
51
+ colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
52
+ }
53
+ if (col.type === "uint") {
54
+ colDef += ` CHECK (${colName} >= 0)`;
55
+ }
56
+ columnDefs.push(colDef);
57
+ }
58
+ statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
59
+ ${columnDefs.join(`,
60
+ `)}
61
+ )`);
62
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
63
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
64
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
65
+ if (col.indexed) {
66
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
67
+ }
68
+ }
69
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
70
+ if (col.search) {
71
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
72
+ }
73
+ }
74
+ if (tableDef.indexes) {
75
+ for (let i = 0;i < tableDef.indexes.length; i++) {
76
+ const cols = tableDef.indexes[i];
77
+ const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
78
+ statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
79
+ }
80
+ }
81
+ if (tableDef.uniqueKeys) {
82
+ for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
83
+ const cols = tableDef.uniqueKeys[i];
84
+ const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
85
+ statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
86
+ }
87
+ }
88
+ return statements;
89
+ }
90
+ function emitJournalDDL(schemaName) {
91
+ return [
92
+ `CREATE TABLE IF NOT EXISTS ${schemaName}._journal (
93
+ _jid BIGSERIAL PRIMARY KEY,
94
+ block_height BIGINT NOT NULL,
95
+ table_name TEXT NOT NULL,
96
+ row_key JSONB NOT NULL,
97
+ prev_row JSONB,
98
+ _created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
99
+ )`,
100
+ `CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
101
+ ];
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
+ }
115
+ function emitForeignKeyDDL(schemaName, tableName, tableDef) {
116
+ return (tableDef.relations ?? []).map((rel) => {
117
+ const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
118
+ return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
119
+ });
120
+ }
121
+ function generateSubgraphSQL(def, schemaNameOverride) {
122
+ const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
123
+ const statements = [];
124
+ const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
125
+ if (needsTrgm) {
126
+ statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
127
+ }
128
+ statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
129
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
130
+ statements.push(...emitTableDDL(schemaName, tableName, tableDef));
131
+ }
132
+ statements.push(...emitJournalDDL(schemaName));
133
+ if (Object.values(def.sources ?? {}).some((src) => src.factory !== undefined)) {
134
+ statements.push(...emitFactoryDDL(schemaName));
135
+ }
136
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
137
+ statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
138
+ }
139
+ const hashInput = JSON.stringify({
140
+ name: def.name,
141
+ schema: def.schema,
142
+ sources: def.sources
143
+ }, (_key, value) => typeof value === "bigint" ? value.toString() : value);
144
+ const hash = createHash("sha256").update(hashInput).digest("hex");
145
+ return { statements, hash };
146
+ }
147
+
148
+ // src/runtime/chain-read.ts
149
+ import { getSourceDb } from "@secondlayer/shared/db";
150
+ import { http, createPublicClient } from "@secondlayer/stacks";
151
+ import {
152
+ ContractResponseError,
153
+ buildFunctionArgs,
154
+ isResponseOutput,
155
+ readContract
156
+ } from "@secondlayer/stacks/actions";
157
+ import {
158
+ clarityValueToJSUntyped,
159
+ deserializeCV,
160
+ normalizeAbi,
161
+ serializeCV,
162
+ toCamelCase
163
+ } from "@secondlayer/stacks/clarity";
164
+ function readContractAt(ctx, contractId, abi, options) {
165
+ return ctx.client.contract(contractId, abi, options);
166
+ }
167
+
168
+ class ChainReadError extends Error {
169
+ name = "ChainReadError";
170
+ }
171
+ function nodeClient(rpcUrl) {
172
+ return createPublicClient({
173
+ transport: http(rpcUrl, { retryCount: 0, timeout: 1e4 })
174
+ });
175
+ }
176
+
177
+ class ConcurrencyGate {
178
+ limit;
179
+ active = 0;
180
+ queue = [];
181
+ constructor(limit) {
182
+ this.limit = limit;
183
+ }
184
+ async run(fn) {
185
+ if (this.active >= this.limit) {
186
+ await new Promise((resolve) => this.queue.push(resolve));
187
+ }
188
+ this.active++;
189
+ try {
190
+ return await fn();
191
+ } finally {
192
+ this.active--;
193
+ this.queue.shift()?.();
194
+ }
195
+ }
196
+ }
197
+ var DEFAULT_MAX_CONCURRENT_READS = 4;
198
+ function maxConcurrentReads() {
199
+ const raw = process.env.SUBGRAPH_CHAIN_READ_CONCURRENCY;
200
+ const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
201
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT_READS;
202
+ }
203
+ var gate = new ConcurrencyGate(maxConcurrentReads());
204
+ function hashArgs(args) {
205
+ const serialized = args.map((arg) => serializeCV(arg)).join(",");
206
+ return Bun.hash(serialized).toString(16);
207
+ }
208
+ function findFunction(abi, name) {
209
+ const fn = abi.functions.find((f) => f.name === name);
210
+ if (!fn) {
211
+ throw new ChainReadError(`No function "${name}" in the supplied ABI.`);
212
+ }
213
+ if (fn.access !== "read-only") {
214
+ throw new ChainReadError(`"${name}" is ${fn.access}, not read-only — a handler cannot call it.`);
215
+ }
216
+ return fn;
217
+ }
218
+ function createChainReadClient(ctx) {
219
+ const readValue = async (contractId, abi, functionName, args, options) => {
220
+ const rpcUrl = ctx.rpcUrl ?? process.env.STACKS_NODE_RPC_URL;
221
+ if (!rpcUrl) {
222
+ throw new ChainReadError("ctx.client needs a Stacks node: set STACKS_NODE_RPC_URL on the subgraph processor.");
223
+ }
224
+ const constant = options.cache === "contract-constant";
225
+ if (!constant && !ctx.indexBlockHash) {
226
+ throw new ChainReadError(`Block ${ctx.blockHeight} has no index_block_hash, so a read cannot be pinned to it. Reading at the node's tip would make this handler non-deterministic (the same reindex would produce different rows). Re-ingest the block, or pass cache: "contract-constant" if the value genuinely cannot change.`);
227
+ }
228
+ const fn = findFunction(abi, functionName);
229
+ const clarityArgs = buildFunctionArgs(fn, args);
230
+ const db = ctx.db ?? getSourceDb();
231
+ const argsHash = hashArgs(clarityArgs);
232
+ const pinnedTo = constant ? null : ctx.indexBlockHash;
233
+ const cached = await db.selectFrom("chain_read_cache").select("result_hex").where("contract_id", "=", contractId).where("function_name", "=", fn.name).where("args_hash", "=", argsHash).where((eb) => pinnedTo === null ? eb("index_block_hash", "is", null) : eb("index_block_hash", "=", pinnedTo)).executeTakeFirst();
234
+ const resultHex = cached?.result_hex ?? await gate.run(async () => {
235
+ const value2 = await readContract(nodeClient(rpcUrl), {
236
+ contract: contractId,
237
+ functionName: fn.name,
238
+ args: clarityArgs,
239
+ tip: ctx.indexBlockHash ?? undefined
240
+ });
241
+ return serializeCV(value2);
242
+ });
243
+ if (!cached) {
244
+ await db.insertInto("chain_read_cache").values({
245
+ contract_id: contractId,
246
+ function_name: fn.name,
247
+ args_hash: argsHash,
248
+ index_block_hash: pinnedTo,
249
+ block_height: constant ? null : ctx.blockHeight,
250
+ result_hex: resultHex
251
+ }).onConflict((oc) => oc.doNothing()).execute();
252
+ }
253
+ const value = deserializeCV(resultHex);
254
+ const js = clarityValueToJSUntyped(fn.outputs, value);
255
+ if (isResponseOutput(fn.outputs)) {
256
+ const response = js;
257
+ if ("ok" in response)
258
+ return response.ok;
259
+ throw new ContractResponseError(`${contractId}::${fn.name} returned an err response.`, response.err);
260
+ }
261
+ return js;
262
+ };
263
+ return {
264
+ contract(contractId, rawAbi, options = {}) {
265
+ const abi = normalizeAbi(rawAbi);
266
+ const camelToKebab = new Map;
267
+ for (const fn of abi.functions) {
268
+ camelToKebab.set(toCamelCase(fn.name), fn.name);
269
+ }
270
+ const read = new Proxy({}, {
271
+ get(_target, prop) {
272
+ const fnName = camelToKebab.get(prop) ?? prop;
273
+ return (args = {}) => readValue(contractId, abi, fnName, args, options);
274
+ }
275
+ });
276
+ return { read };
277
+ }
278
+ };
279
+ }
280
+
281
+ // src/runtime/context.ts
282
+ var JOURNAL_RETENTION_BLOCKS = 300;
283
+ var journalEnsured = new Set;
284
+ function validateColumnName(name) {
285
+ if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
286
+ throw new Error(`Invalid column name: ${name}`);
287
+ }
288
+ }
289
+
290
+ class SubgraphContext {
291
+ block;
292
+ _tx;
293
+ db;
294
+ pgSchemaName;
295
+ subgraphSchema;
296
+ ops = [];
297
+ byo;
298
+ journal;
299
+ _client;
300
+ constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
301
+ this.db = db;
302
+ this.pgSchemaName = pgSchemaName2;
303
+ this.subgraphSchema = subgraphSchema;
304
+ this.block = block;
305
+ this._tx = tx;
306
+ this.byo = byo;
307
+ this.journal = journal;
308
+ }
309
+ get client() {
310
+ this._client ??= createChainReadClient({
311
+ blockHeight: this.block.height,
312
+ indexBlockHash: this.block.indexBlockHash
313
+ });
314
+ return this._client;
315
+ }
316
+ get tx() {
317
+ return this._tx;
318
+ }
319
+ setTx(tx) {
320
+ this._tx = tx;
321
+ }
322
+ insert(table, row) {
323
+ this.validateTable(table);
324
+ this.ops.push({
325
+ kind: "insert",
326
+ table,
327
+ data: { ...row, _block_height: this.block.height, _tx_id: this._tx.txId }
328
+ });
329
+ }
330
+ update(table, where, set) {
331
+ this.validateTable(table);
332
+ this.ops.push({ kind: "update", table, data: where, set });
333
+ }
334
+ upsert(table, key, row) {
335
+ this.validateTable(table);
336
+ const tableDef = this.subgraphSchema[table];
337
+ if (!tableDef)
338
+ return;
339
+ const keyColumns = Object.keys(key);
340
+ const hasUniqueConstraint = tableDef.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
341
+ const meta = { _block_height: this.block.height, _tx_id: this._tx.txId };
342
+ if (hasUniqueConstraint) {
343
+ this.ops.push({
344
+ kind: "insert",
345
+ table,
346
+ data: { ...key, ...row, ...meta, _upsert_keys: keyColumns }
347
+ });
348
+ } else {
349
+ logger.warn("upsert called without matching uniqueKeys constraint, using fallback", {
350
+ table,
351
+ keys: keyColumns
352
+ });
353
+ this.ops.push({
354
+ kind: "insert",
355
+ table,
356
+ data: {
357
+ ...key,
358
+ ...row,
359
+ ...meta,
360
+ _upsert_fallback_keys: keyColumns,
361
+ _upsert_fallback_set: row
362
+ }
363
+ });
364
+ }
365
+ }
366
+ delete(table, where) {
367
+ this.validateTable(table);
368
+ this.ops.push({ kind: "delete", table, data: where });
369
+ }
370
+ increment(table, key, deltas) {
371
+ this.validateTable(table);
372
+ const tableDef = this.subgraphSchema[table];
373
+ const keyColumns = Object.keys(key);
374
+ const hasUniqueConstraint = tableDef?.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
375
+ if (!hasUniqueConstraint) {
376
+ throw new Error(`increment("${table}") requires a uniqueKeys constraint on [${keyColumns.join(", ")}]`);
377
+ }
378
+ for (const [col, v] of Object.entries(deltas)) {
379
+ validateColumnName(col);
380
+ if (keyColumns.includes(col)) {
381
+ throw new Error(`increment("${table}"): "${col}" is a key column`);
382
+ }
383
+ if (typeof v !== "bigint" && typeof v !== "number") {
384
+ throw new Error(`increment("${table}"): delta for "${col}" must be bigint or number`);
385
+ }
386
+ }
387
+ this.ops.push({
388
+ kind: "increment",
389
+ table,
390
+ data: {
391
+ ...key,
392
+ _block_height: this.block.height,
393
+ _tx_id: this._tx.txId,
394
+ _upsert_keys: keyColumns
395
+ },
396
+ set: { ...deltas }
397
+ });
398
+ }
399
+ opsCheckpoint() {
400
+ return this.ops.length;
401
+ }
402
+ rollbackTo(checkpoint) {
403
+ if (checkpoint < 0 || checkpoint > this.ops.length)
404
+ return;
405
+ this.ops.length = checkpoint;
406
+ }
407
+ patch(table, where, set) {
408
+ this.update(table, where, set);
409
+ }
410
+ async patchOrInsert(table, key, row) {
411
+ const existing = await this.findOne(table, key);
412
+ const resolved = {};
413
+ for (const [k, v] of Object.entries(row)) {
414
+ resolved[k] = typeof v === "function" ? v(existing) : v;
415
+ }
416
+ this.upsert(table, key, resolved);
417
+ }
418
+ formatUnits(value, decimals) {
419
+ return formatUnits(value, decimals);
420
+ }
421
+ async readRows(table, where, limit) {
422
+ const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
423
+ const { clause } = buildWhereClause(where);
424
+ const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}${limit !== undefined ? ` LIMIT ${limit}` : ""}`;
425
+ const { rows } = await sql.raw(query).execute(this.db);
426
+ return rows.map((r) => this.coerceRow(table, r));
427
+ }
428
+ async findOne(table, where) {
429
+ this.validateTable(table);
430
+ const row = (await this.readRows(table, where, 1))[0] ?? null;
431
+ return this.overlayOne(table, where, row);
432
+ }
433
+ async findMany(table, where) {
434
+ this.validateTable(table);
435
+ return this.overlayMany(table, where, await this.readRows(table, where));
436
+ }
437
+ overlayOne(table, where, dbRow) {
438
+ if (this.ops.length === 0)
439
+ return dbRow;
440
+ let row = dbRow;
441
+ for (const op of this.ops) {
442
+ if (op.table !== table)
443
+ continue;
444
+ row = this.applyOpToRow(op, row, where);
445
+ }
446
+ return row;
447
+ }
448
+ overlayMany(table, where, dbRows) {
449
+ if (this.ops.length === 0)
450
+ return [...dbRows];
451
+ let result = [...dbRows];
452
+ for (const op of this.ops) {
453
+ if (op.table !== table)
454
+ continue;
455
+ if (op.kind === "update") {
456
+ for (let i = 0;i < result.length; i++) {
457
+ if (rowMatches(result[i], op.data))
458
+ result[i] = { ...result[i], ...op.set ?? {} };
459
+ }
460
+ } else if (op.kind === "delete") {
461
+ result = result.filter((r) => !rowMatches(r, op.data));
462
+ } else {
463
+ const upsertKeys = op.data._upsert_keys;
464
+ const clean = stripControlKeys(op.data);
465
+ const idx = upsertKeys ? result.findIndex((r) => upsertKeys.every((k) => valEq(r[k], clean[k]))) : -1;
466
+ if (idx >= 0) {
467
+ const existing = result[idx];
468
+ result[idx] = this.applyOpToRow(op, existing, where) ?? existing;
469
+ } else {
470
+ const created = this.applyOpToRow(op, null, where);
471
+ if (created)
472
+ result.push(created);
473
+ }
474
+ }
475
+ }
476
+ return result;
477
+ }
478
+ applyOpToRow(op, row, where) {
479
+ const upsertKeys = op.data._upsert_keys;
480
+ const clean = stripControlKeys(op.data);
481
+ switch (op.kind) {
482
+ case "insert": {
483
+ if (row) {
484
+ if (upsertKeys?.every((k) => valEq(row[k], clean[k]))) {
485
+ const merged = { ...row };
486
+ for (const [k, v] of Object.entries(clean)) {
487
+ if (!upsertKeys.includes(k) && !k.startsWith("_"))
488
+ merged[k] = v;
489
+ }
490
+ return merged;
491
+ }
492
+ return row;
493
+ }
494
+ return rowMatches(clean, where) ? { ...clean } : null;
495
+ }
496
+ case "increment": {
497
+ const deltas = op.set ?? {};
498
+ if (row) {
499
+ if (upsertKeys.every((k) => valEq(row[k], clean[k]))) {
500
+ const merged = { ...row };
501
+ for (const [col, d] of Object.entries(deltas)) {
502
+ merged[col] = toBigIntOr0(merged[col]) + toBigIntOr0(d);
503
+ }
504
+ return merged;
505
+ }
506
+ return row;
507
+ }
508
+ if (!rowMatches(clean, where))
509
+ return null;
510
+ const created = { ...clean };
511
+ for (const [col, d] of Object.entries(deltas)) {
512
+ created[col] = toBigIntOr0(d);
513
+ }
514
+ return created;
515
+ }
516
+ case "update":
517
+ return row && rowMatches(row, op.data) ? { ...row, ...op.set ?? {} } : row;
518
+ case "delete":
519
+ return row && rowMatches(row, op.data) ? null : row;
520
+ }
521
+ }
522
+ async count(table, where) {
523
+ this.validateTable(table);
524
+ const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
525
+ const whereClause = where ? `WHERE ${buildWhereClause(where).clause}` : "";
526
+ const { rows } = await sql.raw(`SELECT COUNT(*)::int AS count FROM ${qualifiedTable} ${whereClause}`).execute(this.db);
527
+ return Number(rows[0]?.count ?? 0);
528
+ }
529
+ async sum(table, column, where) {
530
+ this.validateTable(table);
531
+ validateColumnName(column);
532
+ const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
533
+ const whereClause = where ? `WHERE ${buildWhereClause(where).clause}` : "";
534
+ const { rows } = await sql.raw(`SELECT COALESCE(SUM("${column}"), 0) AS total FROM ${qualifiedTable} ${whereClause}`).execute(this.db);
535
+ return BigInt(rows[0]?.total?.toString() ?? "0");
536
+ }
537
+ async min(table, column, where) {
538
+ this.validateTable(table);
539
+ validateColumnName(column);
540
+ const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
541
+ const whereClause = where ? `WHERE ${buildWhereClause(where).clause}` : "";
542
+ const { rows } = await sql.raw(`SELECT MIN("${column}") AS val FROM ${qualifiedTable} ${whereClause}`).execute(this.db);
543
+ const val = rows[0]?.val;
544
+ return val != null ? BigInt(val.toString()) : null;
545
+ }
546
+ async max(table, column, where) {
547
+ this.validateTable(table);
548
+ validateColumnName(column);
549
+ const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
550
+ const whereClause = where ? `WHERE ${buildWhereClause(where).clause}` : "";
551
+ const { rows } = await sql.raw(`SELECT MAX("${column}") AS val FROM ${qualifiedTable} ${whereClause}`).execute(this.db);
552
+ const val = rows[0]?.val;
553
+ return val != null ? BigInt(val.toString()) : null;
554
+ }
555
+ async countDistinct(table, column, where) {
556
+ this.validateTable(table);
557
+ validateColumnName(column);
558
+ const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
559
+ const whereClause = where ? `WHERE ${buildWhereClause(where).clause}` : "";
560
+ const { rows } = await sql.raw(`SELECT COUNT(DISTINCT "${column}")::int AS count FROM ${qualifiedTable} ${whereClause}`).execute(this.db);
561
+ return Number(rows[0]?.count ?? 0);
562
+ }
563
+ coerceRow(table, row) {
564
+ const tableDef = this.subgraphSchema[table];
565
+ if (!tableDef)
566
+ return row;
567
+ const result = { ...row };
568
+ for (const [col, def] of Object.entries(tableDef.columns)) {
569
+ if ((def.type === "uint" || def.type === "int") && typeof result[col] === "string") {
570
+ result[col] = BigInt(result[col]);
571
+ }
572
+ }
573
+ return result;
574
+ }
575
+ get pendingOps() {
576
+ return this.ops.length;
577
+ }
578
+ async flush() {
579
+ if (this.ops.length === 0)
580
+ return { count: 0, writes: [] };
581
+ await this.ensureJournalTable();
582
+ const opsToFlush = [...this.ops];
583
+ this.ops.length = 0;
584
+ const statements = this.buildStatements(opsToFlush);
585
+ if ("isTransaction" in this.db) {
586
+ for (const stmt of statements) {
587
+ await sql.raw(stmt).execute(this.db);
588
+ }
589
+ } else {
590
+ await this.db.transaction().execute(async (tx) => {
591
+ for (const stmt of statements) {
592
+ await sql.raw(stmt).execute(tx);
593
+ }
594
+ });
595
+ }
596
+ const writes = opsToFlush.map((op, rowIndex) => {
597
+ const blockHeight = op.data._block_height ?? this.block.height;
598
+ const txId = op.data._tx_id ?? this._tx.txId;
599
+ const baseRow = op.kind === "update" || op.kind === "increment" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
600
+ baseRow._upsert_keys = undefined;
601
+ baseRow._upsert_fallback_keys = undefined;
602
+ baseRow._upsert_fallback_set = undefined;
603
+ return {
604
+ op: op.kind === "increment" ? "update" : op.kind,
605
+ table: op.table,
606
+ row: jsonSafe(baseRow),
607
+ pk: { blockHeight, txId, rowIndex }
608
+ };
609
+ });
610
+ return { count: opsToFlush.length, writes };
611
+ }
612
+ prepareInsert(op) {
613
+ const upsertKeys = op.data._upsert_keys;
614
+ const data = { ...op.data };
615
+ delete data._upsert_keys;
616
+ delete data._upsert_fallback_keys;
617
+ delete data._upsert_fallback_set;
618
+ if (!data._block_height)
619
+ data._block_height = this.block.height;
620
+ if (!data._tx_id)
621
+ data._tx_id = this._tx.txId;
622
+ data._created_at = "NOW()";
623
+ const cols = Object.keys(data);
624
+ cols.forEach(validateColumnName);
625
+ const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
626
+ const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
627
+ return { data, cols, vals, upsertKeys, batchKey };
628
+ }
629
+ async ensureJournalTable() {
630
+ if (!this.journal || journalEnsured.has(this.pgSchemaName))
631
+ return;
632
+ const { rows } = await sql.raw(`SELECT to_regclass('"${this.pgSchemaName}"."_journal"') AS r`).execute(this.db);
633
+ if (rows[0]?.r) {
634
+ journalEnsured.add(this.pgSchemaName);
635
+ return;
636
+ }
637
+ for (const stmt of emitJournalDDL(this.pgSchemaName)) {
638
+ await sql.raw(stmt).execute(this.db);
639
+ }
640
+ }
641
+ columnSqlType(table, col) {
642
+ const def = this.subgraphSchema[table]?.columns?.[col];
643
+ return def ? TYPE_MAP[def.type] : undefined;
644
+ }
645
+ journalCaptureSQL(table, keyCols, keyLiteralRows) {
646
+ const cast = (col, expr) => {
647
+ const t = this.columnSqlType(table, col);
648
+ return t ? `CAST(${expr} AS ${t})` : expr;
649
+ };
650
+ const keyObj = keyCols.map((k) => `'${k}', ${cast(k, `v."${k}"`)}`).join(", ");
651
+ const joinCond = keyCols.map((k) => `t."${k}" = ${cast(k, `v."${k}"`)}`).join(" AND ");
652
+ const valuesList = keyLiteralRows.map((r) => `(${r.join(", ")})`).join(", ");
653
+ return `INSERT INTO "${this.pgSchemaName}"."_journal" ("block_height", "table_name", "row_key", "prev_row") ` + `SELECT ${this.block.height}, '${table}', jsonb_build_object(${keyObj}), to_jsonb(t.*) ` + `FROM (VALUES ${valuesList}) AS v(${keyCols.map((k) => `"${k}"`).join(", ")}) ` + `LEFT JOIN "${this.pgSchemaName}"."${table}" t ON ${joinCond}`;
654
+ }
655
+ journalCaptureByWhereSQL(table, clause) {
656
+ return `INSERT INTO "${this.pgSchemaName}"."_journal" ("block_height", "table_name", "row_key", "prev_row") ` + `SELECT ${this.block.height}, '${table}', jsonb_build_object('_id', t."_id"), to_jsonb(t.*) ` + `FROM "${this.pgSchemaName}"."${table}" t WHERE ${clause}`;
657
+ }
658
+ buildStatements(ops) {
659
+ const statements = [];
660
+ if (this.byo) {
661
+ const insertTables = new Set;
662
+ for (const op of ops)
663
+ if (op.kind === "insert")
664
+ insertTables.add(op.table);
665
+ for (const table of insertTables) {
666
+ statements.push(`DELETE FROM "${this.pgSchemaName}"."${table}" WHERE "_block_height" = ${this.block.height}`);
667
+ }
668
+ }
669
+ let currentBatch = null;
670
+ let currentBatchKey = "";
671
+ let incBatch = null;
672
+ let incBatchKey = "";
673
+ const flushIncrementBatch = () => {
674
+ if (!incBatch)
675
+ return;
676
+ const batch = incBatch;
677
+ const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
678
+ const cols = [
679
+ ...batch.keyCols,
680
+ ...batch.deltaCols,
681
+ "_block_height",
682
+ "_tx_id",
683
+ "_created_at"
684
+ ];
685
+ if (this.journal) {
686
+ statements.push(this.journalCaptureSQL(batch.table, batch.keyCols, Array.from(batch.rows.values()).map((r) => batch.keyCols.map((k) => escapeLiteral(r.keys[k])))));
687
+ }
688
+ for (const r of batch.rows.values()) {
689
+ const keyPred = batch.keyCols.map((k) => `"${k}" = ${escapeLiteral(r.keys[k])}`).join(" AND ");
690
+ const updSet = batch.deltaCols.map((c) => `"${c}" = COALESCE("${c}", 0) + (${String(r.deltas[c] ?? 0n)})`).join(", ");
691
+ statements.push(`UPDATE ${qualifiedTable} SET ${updSet} WHERE ${keyPred}`);
692
+ const insertVals = [
693
+ ...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
694
+ ...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
695
+ escapeLiteral(r.meta.blockHeight),
696
+ escapeLiteral(r.meta.txId),
697
+ "NOW()"
698
+ ];
699
+ statements.push(`INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) ` + `SELECT ${insertVals.join(", ")} ` + `WHERE NOT EXISTS (SELECT 1 FROM ${qualifiedTable} WHERE ${keyPred})`);
700
+ }
701
+ incBatch = null;
702
+ incBatchKey = "";
703
+ };
704
+ const flushInsertBatch = () => {
705
+ if (!currentBatch)
706
+ return;
707
+ const batch = currentBatch;
708
+ const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
709
+ const colList = batch.cols.map((c) => `"${c}"`).join(", ");
710
+ let rows = batch.rows;
711
+ if (batch.upsertKeys && batch.upsertKeys.length > 0) {
712
+ const uKeys = batch.upsertKeys;
713
+ const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
714
+ const seen = new Map;
715
+ for (let i = 0;i < rows.length; i++) {
716
+ const key = keyIndices.map((ki) => rows[i][ki]).join("\x00");
717
+ seen.set(key, i);
718
+ }
719
+ if (seen.size < rows.length) {
720
+ rows = Array.from(seen.values()).map((i) => rows[i]);
721
+ }
722
+ }
723
+ const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
724
+ let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
725
+ if (this.journal && batch.upsertKeys && batch.upsertKeys.length > 0) {
726
+ const uKeys = batch.upsertKeys;
727
+ const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
728
+ statements.push(this.journalCaptureSQL(batch.table, uKeys, rows.map((r) => keyIndices.map((ki) => r[ki]))));
729
+ }
730
+ if (batch.upsertKeys && batch.upsertKeys.length > 0) {
731
+ const batchKeys = batch.upsertKeys;
732
+ const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !c.startsWith("_"));
733
+ if (updateCols.length > 0) {
734
+ const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
735
+ stmt += ` ON CONFLICT (${batchKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
736
+ } else {
737
+ stmt += ` ON CONFLICT (${batchKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
738
+ }
739
+ }
740
+ statements.push(stmt);
741
+ currentBatch = null;
742
+ currentBatchKey = "";
743
+ };
744
+ for (const op of ops) {
745
+ const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
746
+ if (op.kind === "insert") {
747
+ flushIncrementBatch();
748
+ const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
749
+ if (batchKey === currentBatchKey && currentBatch) {
750
+ currentBatch.rows.push(vals);
751
+ } else {
752
+ flushInsertBatch();
753
+ currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
754
+ currentBatchKey = batchKey;
755
+ }
756
+ } else if (op.kind === "increment") {
757
+ flushInsertBatch();
758
+ const keyCols = [...op.data._upsert_keys].sort();
759
+ const deltaCols = Object.keys(op.set ?? {}).sort();
760
+ const batchKey = `inc:${op.table}:${keyCols.join(",")}:${deltaCols.join(",")}`;
761
+ if (batchKey !== incBatchKey || !incBatch) {
762
+ flushIncrementBatch();
763
+ incBatch = { table: op.table, keyCols, deltaCols, rows: new Map };
764
+ incBatchKey = batchKey;
765
+ }
766
+ const clean = stripControlKeys(op.data);
767
+ const keySig = keyCols.map((k) => escapeLiteral(clean[k])).join("\x00");
768
+ const existing = incBatch.rows.get(keySig);
769
+ if (existing) {
770
+ for (const c of deltaCols) {
771
+ existing.deltas[c] = (existing.deltas[c] ?? 0n) + toBigIntOr0(op.set?.[c]);
772
+ }
773
+ } else {
774
+ const deltas = {};
775
+ for (const c of deltaCols)
776
+ deltas[c] = toBigIntOr0(op.set?.[c]);
777
+ incBatch.rows.set(keySig, {
778
+ keys: clean,
779
+ deltas,
780
+ meta: {
781
+ blockHeight: op.data._block_height ?? this.block.height,
782
+ txId: op.data._tx_id ?? this._tx.txId
783
+ }
784
+ });
785
+ }
786
+ } else {
787
+ flushInsertBatch();
788
+ flushIncrementBatch();
789
+ if (op.kind === "update") {
790
+ const setEntries = Object.entries(op.set ?? {});
791
+ for (const [k] of setEntries)
792
+ validateColumnName(k);
793
+ const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
794
+ const { clause } = buildWhereClause(op.data);
795
+ if (this.journal) {
796
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
797
+ }
798
+ statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
799
+ } else if (op.kind === "delete") {
800
+ const { clause } = buildWhereClause(op.data);
801
+ if (this.journal) {
802
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
803
+ }
804
+ statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
805
+ }
806
+ }
807
+ }
808
+ flushInsertBatch();
809
+ flushIncrementBatch();
810
+ return statements;
811
+ }
812
+ validateTable(table) {
813
+ if (!this.subgraphSchema[table]) {
814
+ throw new Error(`Table "${table}" not found in subgraph schema. Available: [${Object.keys(this.subgraphSchema).join(", ")}]`);
815
+ }
816
+ }
817
+ }
818
+ function stripControlKeys(data) {
819
+ const {
820
+ _upsert_keys: _a,
821
+ _upsert_fallback_keys: _b,
822
+ _upsert_fallback_set: _c,
823
+ ...clean
824
+ } = data;
825
+ return clean;
826
+ }
827
+ function valEq(a, b) {
828
+ if (a === b)
829
+ return true;
830
+ if (a == null || b == null)
831
+ return false;
832
+ return String(a) === String(b);
833
+ }
834
+ function rowMatches(row, where) {
835
+ return Object.entries(where).every(([k, v]) => valEq(row[k], v));
836
+ }
837
+ function toBigIntOr0(v) {
838
+ if (typeof v === "bigint")
839
+ return v;
840
+ if (v == null)
841
+ return 0n;
842
+ try {
843
+ return BigInt(String(v));
844
+ } catch {
845
+ return 0n;
846
+ }
847
+ }
848
+ function jsonSafe(row) {
849
+ const out = {};
850
+ for (const [k, v] of Object.entries(row)) {
851
+ out[k] = typeof v === "bigint" ? v.toString() : v;
852
+ }
853
+ return out;
854
+ }
855
+ function escapeLiteral(value) {
856
+ if (value === null || value === undefined)
857
+ return "NULL";
858
+ if (typeof value === "number" || typeof value === "bigint")
859
+ return String(value);
860
+ if (typeof value === "boolean")
861
+ return value ? "TRUE" : "FALSE";
862
+ if (typeof value === "object")
863
+ return `'${JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v).replace(/'/g, "''")}'::jsonb`;
864
+ return `'${String(value).replace(/'/g, "''")}'`;
865
+ }
866
+ function buildWhereClause(where) {
867
+ const entries = Object.entries(where);
868
+ if (entries.length === 0)
869
+ return { clause: "TRUE", values: [] };
870
+ for (const [k] of entries)
871
+ validateColumnName(k);
872
+ const parts = entries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
873
+ return { clause: parts.join(" AND "), values: [] };
874
+ }
875
+
876
+ // src/runtime/clarity.ts
877
+ import { cvToValue, deserializeCV as deserializeCV2 } from "@secondlayer/stacks/clarity";
878
+ function decodeClarityValue(hex) {
879
+ try {
880
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
881
+ const cv = deserializeCV2(cleanHex);
882
+ return cvToValue(cv);
883
+ } catch {
884
+ return hex;
885
+ }
886
+ }
887
+ function decodeEventData(data) {
888
+ if (typeof data === "string" && data.startsWith("0x") && data.length > 10) {
889
+ return decodeClarityValue(data);
890
+ }
891
+ if (Array.isArray(data)) {
892
+ return data.map(decodeEventData);
893
+ }
894
+ if (typeof data === "object" && data !== null) {
895
+ const decoded = {};
896
+ for (const [key, value] of Object.entries(data)) {
897
+ decoded[key] = decodeEventData(value);
898
+ }
899
+ return decoded;
900
+ }
901
+ return data;
902
+ }
903
+ function decodeFunctionArgs(args) {
904
+ return args.map(decodeClarityValue);
905
+ }
906
+
907
+ // src/runtime/runner.ts
908
+ import { getErrorMessage } from "@secondlayer/shared";
909
+ import { logger as logger2 } from "@secondlayer/shared/logger";
910
+ import {
911
+ clarityValueToJS,
912
+ deserializeCV as deserializeCV3,
913
+ toCamelCase as toCamelCase2
914
+ } from "@secondlayer/stacks/clarity";
915
+
916
+ // src/runtime/print-validate.ts
917
+ var COLUMN_TYPES = new Set([
918
+ "text",
919
+ "uint",
920
+ "int",
921
+ "principal",
922
+ "boolean",
923
+ "timestamp",
924
+ "jsonb"
925
+ ]);
926
+ function isColumnType(field) {
927
+ return typeof field === "string" && COLUMN_TYPES.has(field);
928
+ }
929
+ function matchesField(field, value) {
930
+ if (isColumnType(field)) {
931
+ switch (field) {
932
+ case "uint":
933
+ case "int":
934
+ return typeof value === "bigint" || typeof value === "number";
935
+ case "boolean":
936
+ return typeof value === "boolean";
937
+ case "text":
938
+ case "principal":
939
+ case "timestamp":
940
+ return typeof value === "string" || typeof value === "number" || typeof value === "bigint";
941
+ case "jsonb":
942
+ return value !== undefined;
943
+ }
944
+ }
945
+ if (typeof field !== "object" || field === null)
946
+ return false;
947
+ if ("optional" in field) {
948
+ return value === undefined || value === null || matchesField(field.type, value);
949
+ }
950
+ if ("tuple" in field) {
951
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
952
+ return false;
953
+ }
954
+ return matchesTopic(field.tuple, value).ok;
955
+ }
956
+ if ("list" in field) {
957
+ return Array.isArray(value) && value.every((item) => matchesField(field.list, item));
958
+ }
959
+ return false;
960
+ }
961
+ function matchesTopic(declared, data) {
962
+ for (const [name, field] of Object.entries(declared)) {
963
+ const present = name in data;
964
+ const optional = typeof field === "object" && field !== null && "optional" in field;
965
+ if (!present) {
966
+ if (optional)
967
+ continue;
968
+ return { ok: false, reason: `missing declared field "${name}"` };
969
+ }
970
+ if (!matchesField(field, data[name])) {
971
+ return {
972
+ ok: false,
973
+ reason: `field "${name}" does not match its declared type (got ${describe(data[name])})`
974
+ };
975
+ }
976
+ }
977
+ return { ok: true };
978
+ }
979
+ function describe(value) {
980
+ if (value === null)
981
+ return "null";
982
+ if (Array.isArray(value))
983
+ return "array";
984
+ return typeof value;
985
+ }
986
+ function validatePrintPayload(prints, topic, data) {
987
+ if (!prints)
988
+ return { ok: true };
989
+ const declared = prints[topic];
990
+ if (!declared)
991
+ return { ok: true };
992
+ return matchesTopic(declared, data);
993
+ }
994
+
995
+ // src/runtime/runner.ts
996
+ var DEFAULT_ERROR_THRESHOLD = 50;
997
+ function camelCase(str) {
998
+ return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
999
+ }
1000
+ function camelizeKeys(obj) {
1001
+ if (obj === null || obj === undefined)
1002
+ return obj;
1003
+ if (typeof obj !== "object")
1004
+ return obj;
1005
+ if (Array.isArray(obj))
1006
+ return obj.map(camelizeKeys);
1007
+ const result = {};
1008
+ for (const [k, v] of Object.entries(obj)) {
1009
+ result[camelCase(k)] = camelizeKeys(v);
1010
+ }
1011
+ return result;
1012
+ }
1013
+ function decodeFunctionArgs2(args) {
1014
+ let parsed = args;
1015
+ if (typeof parsed === "string") {
1016
+ try {
1017
+ parsed = JSON.parse(parsed);
1018
+ } catch {
1019
+ return [];
1020
+ }
1021
+ }
1022
+ if (!Array.isArray(parsed))
1023
+ return [];
1024
+ return parsed.map((arg) => {
1025
+ if (typeof arg === "string")
1026
+ return decodeClarityValue(arg);
1027
+ return arg;
1028
+ });
1029
+ }
1030
+ function decodeRawResult(raw) {
1031
+ if (typeof raw === "string" && raw.length > 2) {
1032
+ return decodeClarityValue(raw);
1033
+ }
1034
+ return null;
1035
+ }
1036
+ function safeBigInt(val) {
1037
+ if (typeof val === "bigint")
1038
+ return val;
1039
+ if (typeof val === "number")
1040
+ return BigInt(val);
1041
+ if (typeof val === "string") {
1042
+ try {
1043
+ return BigInt(val);
1044
+ } catch {
1045
+ return 0n;
1046
+ }
1047
+ }
1048
+ return 0n;
1049
+ }
1050
+ function buildContractCallInput(filter, tx) {
1051
+ const abi = filter.abi;
1052
+ const fnName = tx.function_name;
1053
+ if (!abi || !fnName)
1054
+ return;
1055
+ const fn = abi.functions?.find((f) => f.name === fnName);
1056
+ if (!fn || !Array.isArray(fn.args))
1057
+ return;
1058
+ let rawArgs = tx.function_args;
1059
+ if (typeof rawArgs === "string") {
1060
+ try {
1061
+ rawArgs = JSON.parse(rawArgs);
1062
+ } catch {
1063
+ return;
1064
+ }
1065
+ }
1066
+ if (!Array.isArray(rawArgs))
1067
+ return;
1068
+ const decodeArg = clarityValueToJS;
1069
+ const input = {};
1070
+ fn.args.forEach((arg, i) => {
1071
+ const hex = rawArgs[i];
1072
+ if (typeof hex !== "string")
1073
+ return;
1074
+ try {
1075
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
1076
+ input[toCamelCase2(arg.name)] = decodeArg(arg.type, deserializeCV3(clean));
1077
+ } catch {}
1078
+ });
1079
+ return input;
1080
+ }
1081
+ function buildEventPayload(filter, tx, event) {
1082
+ const txMeta = {
1083
+ txId: tx.tx_id,
1084
+ sender: tx.sender,
1085
+ type: tx.type,
1086
+ status: tx.status,
1087
+ contractId: tx.contract_id ?? null,
1088
+ functionName: tx.function_name ?? null
1089
+ };
1090
+ const decodedArgs = decodeFunctionArgs2(tx.function_args);
1091
+ const decodedResult = decodeRawResult(tx.raw_result);
1092
+ if (!event) {
1093
+ switch (filter.type) {
1094
+ case "contract_call": {
1095
+ const input = buildContractCallInput(filter, tx);
1096
+ return {
1097
+ type: "contract_call",
1098
+ contractId: tx.contract_id ?? "",
1099
+ functionName: tx.function_name ?? "",
1100
+ sender: tx.sender,
1101
+ args: decodedArgs,
1102
+ ...input !== undefined ? { input } : {},
1103
+ result: decodedResult,
1104
+ resultHex: tx.raw_result ?? null,
1105
+ tx: txMeta
1106
+ };
1107
+ }
1108
+ case "contract_deploy":
1109
+ return {
1110
+ contractId: tx.contract_id ?? "",
1111
+ deployer: tx.sender,
1112
+ tx: txMeta
1113
+ };
1114
+ default:
1115
+ return { tx: txMeta };
1116
+ }
1117
+ }
1118
+ const decoded = decodeEventData(event.data);
1119
+ switch (filter.type) {
1120
+ case "ft_transfer":
1121
+ return {
1122
+ sender: decoded.sender,
1123
+ recipient: decoded.recipient,
1124
+ amount: safeBigInt(decoded.amount),
1125
+ assetIdentifier: decoded.asset_identifier,
1126
+ tx: txMeta
1127
+ };
1128
+ case "ft_mint":
1129
+ return {
1130
+ recipient: decoded.recipient,
1131
+ amount: safeBigInt(decoded.amount),
1132
+ assetIdentifier: decoded.asset_identifier,
1133
+ tx: txMeta
1134
+ };
1135
+ case "ft_burn":
1136
+ return {
1137
+ sender: decoded.sender,
1138
+ amount: safeBigInt(decoded.amount),
1139
+ assetIdentifier: decoded.asset_identifier,
1140
+ tx: txMeta
1141
+ };
1142
+ case "nft_transfer":
1143
+ return {
1144
+ sender: decoded.sender,
1145
+ recipient: decoded.recipient,
1146
+ tokenId: decoded.raw_value ?? decoded.value,
1147
+ assetIdentifier: decoded.asset_identifier,
1148
+ tx: txMeta
1149
+ };
1150
+ case "nft_mint":
1151
+ return {
1152
+ recipient: decoded.recipient,
1153
+ tokenId: decoded.raw_value ?? decoded.value,
1154
+ assetIdentifier: decoded.asset_identifier,
1155
+ tx: txMeta
1156
+ };
1157
+ case "nft_burn":
1158
+ return {
1159
+ sender: decoded.sender,
1160
+ tokenId: decoded.raw_value ?? decoded.value,
1161
+ assetIdentifier: decoded.asset_identifier,
1162
+ tx: txMeta
1163
+ };
1164
+ case "stx_transfer":
1165
+ return {
1166
+ sender: decoded.sender,
1167
+ recipient: decoded.recipient,
1168
+ amount: safeBigInt(decoded.amount),
1169
+ memo: decoded.memo ?? "",
1170
+ tx: txMeta
1171
+ };
1172
+ case "stx_mint":
1173
+ return {
1174
+ recipient: decoded.recipient,
1175
+ amount: safeBigInt(decoded.amount),
1176
+ tx: txMeta
1177
+ };
1178
+ case "stx_burn":
1179
+ return {
1180
+ sender: decoded.sender,
1181
+ amount: safeBigInt(decoded.amount),
1182
+ tx: txMeta
1183
+ };
1184
+ case "stx_lock":
1185
+ return {
1186
+ lockedAddress: decoded.locked_address,
1187
+ lockedAmount: safeBigInt(decoded.locked_amount),
1188
+ unlockHeight: safeBigInt(decoded.unlock_height),
1189
+ tx: txMeta
1190
+ };
1191
+ case "print_event": {
1192
+ const rawHex = event.data?.raw_value;
1193
+ const rawValue = typeof rawHex === "string" && rawHex.startsWith("0x") ? decodeClarityValue(rawHex) : decoded.value;
1194
+ const clarityObj = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : null;
1195
+ const topic = clarityObj?.topic ? String(clarityObj.topic) : decoded.topic ?? "";
1196
+ const { topic: _, ...rest } = clarityObj ?? {};
1197
+ const data = Object.keys(rest).length > 0 ? camelizeKeys(rest) : rawValue && typeof rawValue !== "object" ? rawValue : {};
1198
+ return {
1199
+ contractId: decoded.contract_identifier ?? tx.contract_id ?? "",
1200
+ topic,
1201
+ data: data ?? {},
1202
+ tx: txMeta
1203
+ };
1204
+ }
1205
+ case "contract_call": {
1206
+ const ccRawHex = event.data?.raw_value;
1207
+ const normalized = typeof ccRawHex === "string" && ccRawHex.startsWith("0x") ? { ...decoded, value: decodeClarityValue(ccRawHex) } : decoded;
1208
+ const input = buildContractCallInput(filter, tx);
1209
+ return {
1210
+ ...normalized,
1211
+ type: "contract_call",
1212
+ _eventType: event.type,
1213
+ contractId: tx.contract_id ?? "",
1214
+ functionName: tx.function_name ?? "",
1215
+ sender: tx.sender,
1216
+ args: decodedArgs,
1217
+ ...input !== undefined ? { input } : {},
1218
+ result: decodedResult,
1219
+ resultHex: tx.raw_result ?? null,
1220
+ tx: txMeta
1221
+ };
1222
+ }
1223
+ case "contract_deploy":
1224
+ return {
1225
+ contractId: tx.contract_id ?? "",
1226
+ deployer: tx.sender,
1227
+ tx: txMeta
1228
+ };
1229
+ default:
1230
+ return {
1231
+ ...decoded,
1232
+ _eventType: event.type,
1233
+ tx: txMeta
1234
+ };
1235
+ }
1236
+ }
1237
+ async function runHandlers(subgraph, matched, ctx, opts) {
1238
+ let processed = 0;
1239
+ let errors = 0;
1240
+ let skipped = 0;
1241
+ const threshold = opts?.errorThreshold ?? DEFAULT_ERROR_THRESHOLD;
1242
+ const filterLookup = new Map;
1243
+ if (!Array.isArray(subgraph.sources)) {
1244
+ for (const [name, filter] of Object.entries(subgraph.sources)) {
1245
+ filterLookup.set(name, filter);
1246
+ }
1247
+ }
1248
+ const units = [];
1249
+ for (const { tx, events, sourceName } of matched) {
1250
+ if (events.length === 0) {
1251
+ units.push({ tx, sourceName, event: null });
1252
+ } else {
1253
+ for (const event of events)
1254
+ units.push({ tx, sourceName, event });
1255
+ }
1256
+ }
1257
+ units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
1258
+ for (const { tx, event, sourceName } of units) {
1259
+ if (errors >= threshold) {
1260
+ logger2.error("Subgraph error threshold reached, skipping remaining events", {
1261
+ subgraph: subgraph.name,
1262
+ errors,
1263
+ threshold
1264
+ });
1265
+ return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1266
+ }
1267
+ const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
1268
+ if (!handler) {
1269
+ logger2.warn("No handler found for source", {
1270
+ subgraph: subgraph.name,
1271
+ sourceName,
1272
+ txId: tx.tx_id
1273
+ });
1274
+ continue;
1275
+ }
1276
+ ctx.setTx({
1277
+ txId: tx.tx_id,
1278
+ sender: tx.sender,
1279
+ type: tx.type,
1280
+ status: tx.status,
1281
+ contractId: tx.contract_id ?? null,
1282
+ functionName: tx.function_name ?? null
1283
+ });
1284
+ const filter = filterLookup.get(sourceName);
1285
+ const checkpoint = ctx.opsCheckpoint();
1286
+ try {
1287
+ let payload;
1288
+ if (event === null) {
1289
+ payload = filter ? buildEventPayload(filter, tx, null) : {
1290
+ tx: {
1291
+ txId: tx.tx_id,
1292
+ sender: tx.sender,
1293
+ type: tx.type,
1294
+ status: tx.status,
1295
+ contractId: tx.contract_id,
1296
+ functionName: tx.function_name
1297
+ }
1298
+ };
1299
+ } else if (filter) {
1300
+ payload = buildEventPayload(filter, tx, event);
1301
+ } else {
1302
+ const decoded = decodeEventData(event.data);
1303
+ payload = {
1304
+ ...decoded,
1305
+ _eventId: event.id,
1306
+ _eventType: event.type,
1307
+ _eventIndex: event.event_index,
1308
+ tx: {
1309
+ txId: tx.tx_id,
1310
+ sender: tx.sender,
1311
+ type: tx.type,
1312
+ status: tx.status,
1313
+ contractId: tx.contract_id,
1314
+ functionName: tx.function_name
1315
+ }
1316
+ };
1317
+ }
1318
+ if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
1319
+ continue;
1320
+ }
1321
+ if (event !== null && filter?.type === "print_event" && filter.prints) {
1322
+ const printPayload = payload;
1323
+ const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
1324
+ if (!verdict.ok) {
1325
+ skipped++;
1326
+ logger2.warn("Print payload does not match declared prints schema", {
1327
+ subgraph: subgraph.name,
1328
+ sourceName,
1329
+ topic: printPayload.topic,
1330
+ txId: tx.tx_id,
1331
+ reason: verdict.reason
1332
+ });
1333
+ continue;
1334
+ }
1335
+ }
1336
+ await handler(payload, ctx);
1337
+ processed++;
1338
+ } catch (err) {
1339
+ ctx.rollbackTo(checkpoint);
1340
+ errors++;
1341
+ logger2.error("Subgraph handler error", {
1342
+ subgraph: subgraph.name,
1343
+ sourceName,
1344
+ txId: tx.tx_id,
1345
+ ...event !== null ? { eventId: event.id, eventType: event.type } : {},
1346
+ error: getErrorMessage(err)
1347
+ });
1348
+ }
1349
+ }
1350
+ return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1351
+ }
1352
+
1353
+ // src/testing/index.ts
1354
+ import { normalizeAbi as normalizeAbi2, toCamelCase as toCamelCase3 } from "@secondlayer/stacks/clarity";
1355
+ var TEST_SCHEMA_NAME = "subgraph_test";
1356
+ function defaultBlock(overrides = {}) {
1357
+ return {
1358
+ height: 1,
1359
+ hash: "0xtest",
1360
+ timestamp: 0,
1361
+ burnBlockHeight: 1,
1362
+ ...overrides
1363
+ };
1364
+ }
1365
+ function defaultTx(overrides = {}) {
1366
+ return {
1367
+ txId: "0xtest-tx",
1368
+ sender: "SP000000000000000000002Q6VF78",
1369
+ type: "contract_call",
1370
+ status: "success",
1371
+ contractId: null,
1372
+ functionName: null,
1373
+ ...overrides
1374
+ };
1375
+ }
1376
+
1377
+ class InMemorySubgraphContext extends SubgraphContext {
1378
+ store = new Map;
1379
+ constructor(schema, block, tx) {
1380
+ super(undefined, TEST_SCHEMA_NAME, schema, block, tx, false, false);
1381
+ }
1382
+ async readRows(table, where, limit) {
1383
+ const rows = (this.store.get(table) ?? []).filter((row) => Object.entries(where).every(([k, v]) => sameValue(row[k], v)));
1384
+ return limit === undefined ? rows : rows.slice(0, limit);
1385
+ }
1386
+ async rowsOf(table) {
1387
+ return this.overlayMany(table, {}, await this.readRows(table, {}));
1388
+ }
1389
+ setReads(reads) {
1390
+ this._client = {
1391
+ contract(contractId, abi) {
1392
+ const camelToKebab = new Map;
1393
+ for (const fn of normalizeAbi2(abi).functions) {
1394
+ camelToKebab.set(toCamelCase3(fn.name), fn.name);
1395
+ }
1396
+ const read = new Proxy({}, {
1397
+ get(_target, prop) {
1398
+ const fnName = camelToKebab.get(prop) ?? prop;
1399
+ const key = `${contractId}.${fnName}`;
1400
+ return async () => {
1401
+ if (!(key in reads)) {
1402
+ throw new Error(`No stubbed chain read for "${key}" — pass it via createTestContext(schema, { reads: { "${key}": … } }).`);
1403
+ }
1404
+ return reads[key];
1405
+ };
1406
+ }
1407
+ });
1408
+ return { read };
1409
+ }
1410
+ };
1411
+ }
1412
+ async commitOps() {
1413
+ const tables = new Set;
1414
+ for (const op of this.ops)
1415
+ tables.add(op.table);
1416
+ for (const table of tables) {
1417
+ this.store.set(table, await this.rowsOf(table));
1418
+ }
1419
+ this.ops.length = 0;
1420
+ }
1421
+ }
1422
+ function sameValue(a, b) {
1423
+ if (a === b)
1424
+ return true;
1425
+ if ((typeof a === "bigint" || typeof a === "number") && (typeof b === "bigint" || typeof b === "number")) {
1426
+ return BigInt(a) === BigInt(b);
1427
+ }
1428
+ return String(a) === String(b);
1429
+ }
1430
+ function createTestContext(schema, options = {}) {
1431
+ const impl = new InMemorySubgraphContext(schema, defaultBlock(options.block), defaultTx(options.tx));
1432
+ impl.setReads(options.reads ?? {});
1433
+ const ctx = impl;
1434
+ ctx.rows = (table) => impl.rowsOf(table);
1435
+ ctx.commit = () => impl.commitOps();
1436
+ ctx.setTx = (tx) => impl.setTx(defaultTx(tx));
1437
+ return ctx;
1438
+ }
1439
+ function buildEvent(source, payload, options = {}) {
1440
+ const tx = defaultTx(options.tx);
1441
+ const base = { type: source.type, tx };
1442
+ if (source.type === "print_event") {
1443
+ base.contractId = source.contractId ?? "";
1444
+ base.topic = "";
1445
+ base.data = {};
1446
+ } else if (source.type === "contract_call") {
1447
+ base.contractId = source.contractId ?? "";
1448
+ base.functionName = source.functionName ?? "";
1449
+ base.sender = tx.sender;
1450
+ base.args = [];
1451
+ base.result = null;
1452
+ base.resultHex = null;
1453
+ }
1454
+ return { ...base, ...payload };
1455
+ }
1456
+ export {
1457
+ createTestContext,
1458
+ buildEventPayload,
1459
+ buildEvent
1460
+ };
1461
+
1462
+ //# debugId=FEF5AC4898A3E56764756E2164756E21
1463
+ //# sourceMappingURL=index.js.map