@secondlayer/subgraphs 7.2.0 → 7.4.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.
@@ -170,9 +170,17 @@ function generateSubgraphSQL(def, schemaNameOverride) {
170
170
  }
171
171
 
172
172
  // src/runtime/chain-read.ts
173
+ import { getErrorMessage } from "@secondlayer/shared";
174
+ import { chainReadArgsHash } from "@secondlayer/shared/chain-read";
173
175
  import { getSourceDb } from "@secondlayer/shared/db";
176
+ import { hasNoLocalChain } from "@secondlayer/shared/db/queries/instance";
174
177
  import { logger } from "@secondlayer/shared/logger";
175
- import { http, createPublicClient } from "@secondlayer/stacks";
178
+ import {
179
+ http,
180
+ HttpRequestError,
181
+ ReadContractError,
182
+ createPublicClient
183
+ } from "@secondlayer/stacks";
176
184
  import {
177
185
  ContractResponseError,
178
186
  buildFunctionArgs,
@@ -186,1747 +194,1803 @@ import {
186
194
  serializeCV,
187
195
  toCamelCase
188
196
  } from "@secondlayer/stacks/clarity";
189
- function readContractAt(ctx, contractId, abi, options) {
190
- return ctx.client.contract(contractId, abi, options);
191
- }
192
197
 
193
- class ChainReadError extends Error {
194
- name = "ChainReadError";
195
- }
196
- function nodeClient(rpcUrl) {
197
- return createPublicClient({
198
- transport: http(rpcUrl, { retryCount: 0, timeout: 1e4 })
199
- });
200
- }
198
+ // src/runtime/realm.ts
199
+ import vm from "node:vm";
201
200
 
202
- class ConcurrencyGate {
203
- limit;
204
- active = 0;
205
- queue = [];
206
- constructor(limit) {
207
- this.limit = limit;
208
- }
209
- async run(fn) {
210
- if (this.active >= this.limit) {
211
- await new Promise((resolve) => this.queue.push(resolve));
212
- }
213
- this.active++;
214
- try {
215
- return await fn();
216
- } finally {
217
- this.active--;
218
- this.queue.shift()?.();
219
- }
220
- }
221
- }
222
- var DEFAULT_MAX_CONCURRENT_READS = 4;
223
- function maxConcurrentReads() {
224
- const raw = process.env.SUBGRAPH_CHAIN_READ_CONCURRENCY;
225
- const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
226
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT_READS;
227
- }
228
- var gate = new ConcurrencyGate(maxConcurrentReads());
229
- function hashArgs(args) {
230
- const serialized = args.map((arg) => serializeCV(arg)).join(",");
231
- return Bun.hash(serialized).toString(16);
232
- }
233
- function findFunction(abi, name) {
234
- const fn = abi.functions.find((f) => f.name === name);
235
- if (!fn) {
236
- throw new ChainReadError(`No function "${name}" in the supplied ABI.`);
237
- }
238
- if (fn.access !== "read-only") {
239
- throw new ChainReadError(`"${name}" is ${fn.access}, not read-only — a handler cannot call it.`);
240
- }
241
- return fn;
242
- }
243
- function createChainReadClient(ctx) {
244
- const readValue = async (contractId, abi, functionName, args, options) => {
245
- const rpcUrl = ctx.rpcUrl ?? process.env.STACKS_NODE_RPC_URL;
246
- if (!rpcUrl) {
247
- throw new ChainReadError("ctx.client needs a Stacks node: set STACKS_NODE_RPC_URL on the subgraph processor.");
248
- }
249
- const constant = options.cache === "contract-constant";
250
- if (!constant && !ctx.indexBlockHash) {
251
- 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.`);
252
- }
253
- const fn = findFunction(abi, functionName);
254
- const clarityArgs = buildFunctionArgs(fn, args);
255
- const db = ctx.db ?? getSourceDb();
256
- const argsHash = hashArgs(clarityArgs);
257
- const pinnedTo = constant ? null : ctx.indexBlockHash;
258
- 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();
259
- const resultHex = cached?.result_hex ?? await gate.run(async () => {
260
- if (!ctx.indexBlockHash) {
261
- logger.warn("Chain read is not pinned to a block (no index_block_hash)", {
262
- contractId,
263
- functionName: fn.name,
264
- blockHeight: ctx.blockHeight,
265
- note: 'cache: "contract-constant" asserts this value cannot change; the node answered at its own tip'
266
- });
267
- }
268
- const value = await readContract(nodeClient(rpcUrl), {
269
- contract: contractId,
270
- functionName: fn.name,
271
- args: clarityArgs,
272
- tip: ctx.indexBlockHash ?? undefined
273
- });
274
- return serializeCV(value);
275
- });
276
- if (!cached) {
277
- await db.insertInto("chain_read_cache").values({
278
- contract_id: contractId,
279
- function_name: fn.name,
280
- args_hash: argsHash,
281
- index_block_hash: pinnedTo,
282
- block_height: constant ? null : ctx.blockHeight,
283
- result_hex: resultHex
284
- }).onConflict((oc) => oc.doNothing()).execute();
285
- }
286
- const value = deserializeCV(resultHex);
287
- const js = clarityValueToJSUntyped(fn.outputs, value);
288
- if (isResponseOutput(fn.outputs)) {
289
- const response = js;
290
- if ("ok" in response)
291
- return response.ok;
292
- throw new ContractResponseError(`${contractId}::${fn.name} returned an err response.`, response.err);
293
- }
294
- return js;
295
- };
296
- return {
297
- contract(contractId, rawAbi, options = {}) {
298
- const abi = normalizeAbi(rawAbi);
299
- const camelToKebab = new Map;
300
- for (const fn of abi.functions) {
301
- camelToKebab.set(toCamelCase(fn.name), fn.name);
302
- }
303
- const read = new Proxy({}, {
304
- get(_target, prop) {
305
- const fnName = camelToKebab.get(prop) ?? prop;
306
- return (args = {}) => readValue(contractId, abi, fnName, args, options);
307
- }
308
- });
309
- return { read };
201
+ // src/verification.ts
202
+ import { createHash as createHash2 } from "node:crypto";
203
+ // package.json
204
+ var package_default = {
205
+ name: "@secondlayer/subgraphs",
206
+ version: "7.4.0",
207
+ description: "Describe the Stacks data you want as one TypeScript file; it fills itself and survives reorgs.",
208
+ license: "MIT",
209
+ repository: {
210
+ type: "git",
211
+ url: "git+https://github.com/ryanwaits/secondlayer.git",
212
+ directory: "packages/subgraphs"
213
+ },
214
+ homepage: "https://github.com/ryanwaits/secondlayer/tree/main/packages/subgraphs#readme",
215
+ type: "module",
216
+ main: "./dist/index.js",
217
+ types: "./dist/index.d.ts",
218
+ exports: {
219
+ ".": {
220
+ types: "./dist/index.d.ts",
221
+ import: "./dist/index.js"
222
+ },
223
+ "./types": {
224
+ types: "./dist/types.d.ts",
225
+ import: "./dist/types.js"
226
+ },
227
+ "./schema": {
228
+ types: "./dist/schema/index.d.ts",
229
+ import: "./dist/schema/index.js"
230
+ },
231
+ "./validate": {
232
+ types: "./dist/validate.d.ts",
233
+ import: "./dist/validate.js"
234
+ },
235
+ "./verification": {
236
+ types: "./dist/verification.d.ts",
237
+ import: "./dist/verification.js"
238
+ },
239
+ "./runtime/replay": {
240
+ types: "./dist/runtime/replay.d.ts",
241
+ import: "./dist/runtime/replay.js"
242
+ },
243
+ "./runtime/emitter": {
244
+ types: "./dist/runtime/emitter.d.ts",
245
+ import: "./dist/runtime/emitter.js"
246
+ },
247
+ "./testing": {
248
+ types: "./dist/testing/index.d.ts",
249
+ import: "./dist/testing/index.js"
250
+ },
251
+ "./verify": {
252
+ types: "./dist/verify/index.d.ts",
253
+ import: "./dist/verify/index.js"
310
254
  }
311
- };
312
- }
313
-
314
- // src/runtime/context.ts
315
- var journalEnsured = new Set;
316
- function validateColumnName(name) {
317
- if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
318
- throw new Error(`Invalid column name: ${name}`);
255
+ },
256
+ files: ["dist"],
257
+ scripts: {
258
+ build: "bunup",
259
+ dev: "bunup --watch",
260
+ test: "bun test",
261
+ typecheck: "tsc --noEmit",
262
+ openpkg: "bun scripts/sync-openpkg.ts",
263
+ prepublishOnly: "bun run build"
264
+ },
265
+ dependencies: {
266
+ "@secondlayer/shared": "^13.3.0",
267
+ "@secondlayer/stacks": "^6.4.0",
268
+ "@secondlayer/verify": "^0.6.0",
269
+ esbuild: "^0.28.2",
270
+ kysely: "0.28.17",
271
+ zod: "^4.3.6"
272
+ },
273
+ devDependencies: {
274
+ "@types/bun": "latest",
275
+ bunup: "0.16.32",
276
+ typescript: "^5"
277
+ },
278
+ publishConfig: {
279
+ access: "public"
319
280
  }
320
- }
281
+ };
321
282
 
322
- class SubgraphContext {
323
- block;
324
- _tx;
325
- db;
326
- pgSchemaName;
327
- subgraphSchema;
328
- ops = [];
329
- journal;
330
- _client;
331
- constructor(db, pgSchemaName, subgraphSchema, block, tx, journal = false) {
332
- this.db = db;
333
- this.pgSchemaName = pgSchemaName;
334
- this.subgraphSchema = subgraphSchema;
335
- this.block = block;
336
- this._tx = tx;
337
- this.journal = journal;
338
- }
339
- get client() {
340
- this._client ??= createChainReadClient({
341
- blockHeight: this.block.height,
342
- indexBlockHash: this.block.indexBlockHash
343
- });
344
- return this._client;
345
- }
346
- get tx() {
347
- return this._tx;
348
- }
349
- setTx(tx) {
350
- this._tx = tx;
351
- }
352
- insert(table, row) {
353
- this.validateTable(table);
354
- this.ops.push({
355
- kind: "insert",
356
- table,
357
- data: { ...row, _block_height: this.block.height, _tx_id: this._tx.txId }
358
- });
359
- }
360
- update(table, where, set) {
361
- this.validateTable(table);
362
- this.ops.push({ kind: "update", table, data: where, set });
363
- }
364
- upsert(table, key, row) {
365
- this.validateTable(table);
366
- const tableDef = this.subgraphSchema[table];
367
- if (!tableDef)
368
- return;
369
- const keyColumns = Object.keys(key);
370
- const hasUniqueConstraint = tableDef.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
371
- const meta = { _block_height: this.block.height, _tx_id: this._tx.txId };
372
- if (hasUniqueConstraint) {
373
- this.ops.push({
374
- kind: "insert",
375
- table,
376
- data: { ...key, ...row, ...meta, _upsert_keys: keyColumns }
377
- });
378
- } else {
379
- logger2.warn("upsert called without matching uniqueKeys constraint, using fallback", {
380
- table,
381
- keys: keyColumns
382
- });
383
- this.ops.push({
384
- kind: "insert",
385
- table,
386
- data: {
387
- ...key,
388
- ...row,
389
- ...meta,
390
- _upsert_fallback_keys: keyColumns,
391
- _upsert_fallback_set: row
392
- }
393
- });
394
- }
395
- }
396
- delete(table, where) {
397
- this.validateTable(table);
398
- this.ops.push({ kind: "delete", table, data: where });
399
- }
400
- increment(table, key, deltas) {
401
- this.validateTable(table);
402
- const tableDef = this.subgraphSchema[table];
403
- const keyColumns = Object.keys(key);
404
- const hasUniqueConstraint = tableDef?.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
405
- if (!hasUniqueConstraint) {
406
- throw new Error(`increment("${table}") requires a uniqueKeys constraint on [${keyColumns.join(", ")}]`);
407
- }
408
- for (const [col, v] of Object.entries(deltas)) {
409
- validateColumnName(col);
410
- if (keyColumns.includes(col)) {
411
- throw new Error(`increment("${table}"): "${col}" is a key column`);
412
- }
413
- if (typeof v !== "bigint" && typeof v !== "number") {
414
- throw new Error(`increment("${table}"): delta for "${col}" must be bigint or number`);
415
- }
416
- }
417
- this.ops.push({
418
- kind: "increment",
419
- table,
420
- data: {
421
- ...key,
422
- _block_height: this.block.height,
423
- _tx_id: this._tx.txId,
424
- _upsert_keys: keyColumns
425
- },
426
- set: { ...deltas }
427
- });
428
- }
429
- opsCheckpoint() {
430
- return this.ops.length;
431
- }
432
- rollbackTo(checkpoint) {
433
- if (checkpoint < 0 || checkpoint > this.ops.length)
434
- return;
435
- this.ops.length = checkpoint;
436
- }
437
- async readRows(table, where, limit) {
438
- const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
439
- const { clause } = buildWhereClause(where);
440
- const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} ORDER BY "_id"${limit !== undefined ? ` LIMIT ${limit}` : ""}`;
441
- const { rows } = await sql.raw(query).execute(this.db);
442
- return rows.map((r) => this.coerceRow(table, r));
443
- }
444
- async findOne(table, where) {
445
- this.validateTable(table);
446
- const row = (await this.readRows(table, where, 1))[0] ?? null;
447
- return this.overlayOne(table, where, row);
448
- }
449
- async findMany(table, where) {
450
- this.validateTable(table);
451
- return this.overlayMany(table, where, await this.readRows(table, where));
452
- }
453
- overlayOne(table, where, dbRow) {
454
- if (this.ops.length === 0)
455
- return dbRow;
456
- let row = dbRow;
457
- for (const op of this.ops) {
458
- if (op.table !== table)
459
- continue;
460
- row = this.applyOpToRow(op, row, where);
461
- }
462
- return row;
463
- }
464
- overlayMany(table, where, dbRows) {
465
- if (this.ops.length === 0)
466
- return [...dbRows];
467
- let result = [...dbRows];
468
- for (const op of this.ops) {
469
- if (op.table !== table)
470
- continue;
471
- if (op.kind === "update") {
472
- for (let i = 0;i < result.length; i++) {
473
- if (rowMatches(result[i], op.data))
474
- result[i] = { ...result[i], ...op.set ?? {} };
475
- }
476
- } else if (op.kind === "delete") {
477
- result = result.filter((r) => !rowMatches(r, op.data));
478
- } else {
479
- const upsertKeys = op.data._upsert_keys;
480
- const clean = stripControlKeys(op.data);
481
- const idx = upsertKeys ? result.findIndex((r) => upsertKeys.every((k) => valEq(r[k], clean[k]))) : -1;
482
- if (idx >= 0) {
483
- const existing = result[idx];
484
- result[idx] = this.applyOpToRow(op, existing, where) ?? existing;
485
- } else {
486
- const created = this.applyOpToRow(op, null, where);
487
- if (created)
488
- result.push(created);
489
- }
490
- }
491
- }
492
- return result;
493
- }
494
- applyOpToRow(op, row, where) {
495
- const upsertKeys = op.data._upsert_keys;
496
- const clean = stripControlKeys(op.data);
497
- switch (op.kind) {
498
- case "insert": {
499
- if (row) {
500
- if (upsertKeys?.every((k) => valEq(row[k], clean[k]))) {
501
- const merged = { ...row };
502
- for (const [k, v] of Object.entries(clean)) {
503
- if (!upsertKeys.includes(k) && !isSystemColumn(k))
504
- merged[k] = v;
505
- }
506
- return merged;
507
- }
508
- return row;
509
- }
510
- return rowMatches(clean, where) ? { ...clean } : null;
511
- }
512
- case "increment": {
513
- const deltas = op.set ?? {};
514
- if (row) {
515
- if (upsertKeys.every((k) => valEq(row[k], clean[k]))) {
516
- const merged = { ...row };
517
- for (const [col, d] of Object.entries(deltas)) {
518
- merged[col] = toBigIntOr0(merged[col]) + toBigIntOr0(d);
519
- }
520
- return merged;
521
- }
522
- return row;
523
- }
524
- if (!rowMatches(clean, where))
525
- return null;
526
- const created = { ...clean };
527
- for (const [col, d] of Object.entries(deltas)) {
528
- created[col] = toBigIntOr0(d);
529
- }
530
- return created;
531
- }
532
- case "update":
533
- return row && rowMatches(row, op.data) ? { ...row, ...op.set ?? {} } : row;
534
- case "delete":
535
- return row && rowMatches(row, op.data) ? null : row;
536
- }
537
- }
538
- coerceRow(table, row) {
539
- const tableDef = this.subgraphSchema[table];
540
- if (!tableDef)
541
- return row;
542
- const result = { ...row };
543
- for (const [col, def] of Object.entries(tableDef.columns)) {
544
- if ((def.type === "uint" || def.type === "int") && typeof result[col] === "string") {
545
- result[col] = BigInt(result[col]);
546
- }
547
- }
548
- return result;
549
- }
550
- get pendingOps() {
551
- return this.ops.length;
552
- }
553
- async flush() {
554
- if (this.ops.length === 0)
555
- return { count: 0, writes: [] };
556
- await this.ensureJournalTable();
557
- const opsToFlush = [...this.ops];
558
- this.ops.length = 0;
559
- const statements = this.buildStatements(opsToFlush);
560
- if ("isTransaction" in this.db) {
561
- await this.execStatements(this.db, statements);
562
- } else {
563
- await this.db.transaction().execute(async (tx) => {
564
- await this.execStatements(tx, statements);
565
- });
566
- }
567
- const writes = opsToFlush.map((op, rowIndex) => {
568
- const blockHeight = op.data._block_height ?? this.block.height;
569
- const txId = op.data._tx_id ?? this._tx.txId;
570
- const baseRow = op.kind === "update" || op.kind === "increment" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
571
- baseRow._upsert_keys = undefined;
572
- baseRow._upsert_fallback_keys = undefined;
573
- baseRow._upsert_fallback_set = undefined;
574
- return {
575
- op: op.kind === "increment" ? "update" : op.kind,
576
- table: op.table,
577
- row: jsonSafe(baseRow),
578
- pk: { blockHeight, txId, rowIndex }
579
- };
580
- });
581
- return { count: opsToFlush.length, writes };
582
- }
583
- async execStatements(executor, statements) {
584
- const affected = (r) => r.numAffectedRows ?? 0n;
585
- for (const stmt of statements) {
586
- if (typeof stmt === "string") {
587
- await sql.raw(stmt).execute(executor);
588
- continue;
589
- }
590
- const updated = await sql.raw(stmt.update).execute(executor);
591
- if (affected(updated) > 0n)
592
- continue;
593
- const inserted = await sql.raw(stmt.insert).execute(executor);
594
- if (affected(inserted) > 0n)
595
- continue;
596
- const retried = await sql.raw(stmt.update).execute(executor);
597
- if (affected(retried) === 0n) {
598
- throw new Error(`increment could not be applied to ${this.pgSchemaName}.${stmt.table} where ${stmt.keyPred}: row neither updated nor inserted`);
599
- }
600
- }
601
- }
602
- prepareInsert(op) {
603
- const upsertKeys = op.data._upsert_keys;
604
- const data = { ...op.data };
605
- delete data._upsert_keys;
606
- delete data._upsert_fallback_keys;
607
- delete data._upsert_fallback_set;
608
- if (!data._block_height)
609
- data._block_height = this.block.height;
610
- if (!data._tx_id)
611
- data._tx_id = this._tx.txId;
612
- data._created_at = "NOW()";
613
- const cols = Object.keys(data);
614
- cols.forEach(validateColumnName);
615
- const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
616
- const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
617
- return { data, cols, vals, upsertKeys, batchKey };
618
- }
619
- async ensureJournalTable() {
620
- if (!this.journal || journalEnsured.has(this.pgSchemaName))
621
- return;
622
- const { rows } = await sql.raw(`SELECT to_regclass('"${this.pgSchemaName}"."_journal"') AS r`).execute(this.db);
623
- if (rows[0]?.r) {
624
- journalEnsured.add(this.pgSchemaName);
625
- return;
626
- }
627
- for (const stmt of emitJournalDDL(this.pgSchemaName)) {
628
- await sql.raw(stmt).execute(this.db);
629
- }
630
- }
631
- columnSqlType(table, col) {
632
- const def = this.subgraphSchema[table]?.columns?.[col];
633
- return def ? TYPE_MAP[def.type] : undefined;
634
- }
635
- journalCaptureSQL(table, keyCols, keyLiteralRows) {
636
- const cast = (col, expr) => {
637
- const t = this.columnSqlType(table, col);
638
- return t ? `CAST(${expr} AS ${t})` : expr;
639
- };
640
- const keyObj = keyCols.map((k) => `'${k}', ${cast(k, `v."${k}"`)}`).join(", ");
641
- const joinCond = keyCols.map((k) => `t."${k}" = ${cast(k, `v."${k}"`)}`).join(" AND ");
642
- const valuesList = keyLiteralRows.map((r) => `(${r.join(", ")})`).join(", ");
643
- 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}`;
283
+ // src/verification.ts
284
+ var SUBGRAPHS_RUNTIME = `@secondlayer/subgraphs@${package_default.version}`;
285
+ var FORBIDDEN_GLOBALS = {
286
+ Date: "wall-clock time differs per run",
287
+ performance: "timers differ per run",
288
+ process: "host environment differs per run",
289
+ fetch: "network reads are not chain inputs",
290
+ crypto: "randomness differs per run",
291
+ setTimeout: "scheduling order differs per run",
292
+ setInterval: "scheduling order differs per run",
293
+ setImmediate: "scheduling order differs per run",
294
+ clearTimeout: "scheduling order differs per run",
295
+ clearInterval: "scheduling order differs per run",
296
+ queueMicrotask: "scheduling order differs per run",
297
+ WeakRef: "garbage collection timing differs per run",
298
+ FinalizationRegistry: "garbage collection timing differs per run",
299
+ Atomics: "shared memory is not single-threaded",
300
+ SharedArrayBuffer: "shared memory is not single-threaded",
301
+ Intl: "locale and time zone data differ per host",
302
+ Temporal: "locale and time zone data differ per host",
303
+ eval: "generated code hides inputs from the scan",
304
+ WebAssembly: "compiled code hides inputs from the scan",
305
+ ShadowRealm: "a nested realm escapes the allow-list",
306
+ require: "imports must be bundled into the handler"
307
+ };
308
+ var ALLOWED_GLOBALS = [
309
+ "undefined",
310
+ "NaN",
311
+ "Infinity",
312
+ "globalThis",
313
+ "isNaN",
314
+ "isFinite",
315
+ "parseInt",
316
+ "parseFloat",
317
+ "decodeURI",
318
+ "decodeURIComponent",
319
+ "encodeURI",
320
+ "encodeURIComponent",
321
+ "escape",
322
+ "unescape",
323
+ "Object",
324
+ "Function",
325
+ "Array",
326
+ "Number",
327
+ "Boolean",
328
+ "String",
329
+ "Symbol",
330
+ "BigInt",
331
+ "Math",
332
+ "JSON",
333
+ "Reflect",
334
+ "Proxy",
335
+ "Promise",
336
+ "Map",
337
+ "Set",
338
+ "WeakMap",
339
+ "WeakSet",
340
+ "RegExp",
341
+ "Iterator",
342
+ "DisposableStack",
343
+ "AsyncDisposableStack",
344
+ "Error",
345
+ "AggregateError",
346
+ "SuppressedError",
347
+ "EvalError",
348
+ "RangeError",
349
+ "ReferenceError",
350
+ "SyntaxError",
351
+ "TypeError",
352
+ "URIError",
353
+ "ArrayBuffer",
354
+ "DataView",
355
+ "Int8Array",
356
+ "Uint8Array",
357
+ "Uint8ClampedArray",
358
+ "Int16Array",
359
+ "Uint16Array",
360
+ "Int32Array",
361
+ "Uint32Array",
362
+ "Float16Array",
363
+ "Float32Array",
364
+ "Float64Array",
365
+ "BigInt64Array",
366
+ "BigUint64Array",
367
+ "TextEncoder",
368
+ "TextDecoder",
369
+ "atob",
370
+ "btoa",
371
+ "console"
372
+ ];
373
+ var ALLOWED_MATH = [
374
+ "abs",
375
+ "min",
376
+ "max",
377
+ "floor",
378
+ "ceil",
379
+ "trunc",
380
+ "sign",
381
+ "round",
382
+ "E",
383
+ "LN10",
384
+ "LN2",
385
+ "LOG10E",
386
+ "LOG2E",
387
+ "PI",
388
+ "SQRT1_2",
389
+ "SQRT2"
390
+ ];
391
+ var LOCALE_METHODS = [
392
+ "toLocaleString",
393
+ "toLocaleDateString",
394
+ "toLocaleTimeString",
395
+ "toLocaleUpperCase",
396
+ "toLocaleLowerCase",
397
+ "localeCompare"
398
+ ];
399
+ function formatFinding(f) {
400
+ return `${f.file}:${f.line}:${f.column} ${f.name}: ${f.reason}`;
401
+ }
402
+ var STATE_SOURCE_TYPES = new Set([
403
+ "var_set",
404
+ "map_set",
405
+ "map_delete"
406
+ ]);
407
+ var LEVEL_RANK = { state: 0, events: 1, none: 2 };
408
+ var MAX_LISTED_FINDINGS = 10;
409
+ function deriveVerification(def, findings = []) {
410
+ let level = "state";
411
+ const raise = (to) => {
412
+ if (LEVEL_RANK[to] > LEVEL_RANK[level])
413
+ level = to;
414
+ };
415
+ const reasons = [];
416
+ for (const [name, source] of Object.entries(def.sources ?? {})) {
417
+ const filter = source;
418
+ if (filter.trait) {
419
+ raise("none");
420
+ reasons.push(`source "${name}": trait scope needs a proven contract registry`);
421
+ } else if (filter.type === "map_insert") {
422
+ raise("events");
423
+ reasons.push(`map_insert source "${name}": map_insert vs map_set needs event proofs`);
424
+ } else if (!STATE_SOURCE_TYPES.has(filter.type)) {
425
+ raise("events");
426
+ reasons.push(`${filter.type} source "${name}" needs event proofs`);
427
+ }
644
428
  }
645
- journalCaptureByWhereSQL(table, clause) {
646
- 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}`;
429
+ if (def.backfillMode === "concurrent") {
430
+ raise("none");
431
+ reasons.push("backfillMode concurrent: tip-first order is not chain order");
647
432
  }
648
- buildStatements(ops) {
649
- const statements = [];
650
- let currentBatch = null;
651
- let currentBatchKey = "";
652
- let incBatch = null;
653
- let incBatchKey = "";
654
- const flushIncrementBatch = () => {
655
- if (!incBatch)
656
- return;
657
- const batch = incBatch;
658
- const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
659
- const cols = [
660
- ...batch.keyCols,
661
- ...batch.deltaCols,
662
- "_block_height",
663
- "_tx_id",
664
- "_created_at"
665
- ];
666
- if (this.journal) {
667
- statements.push(this.journalCaptureSQL(batch.table, batch.keyCols, Array.from(batch.rows.values()).map((r) => batch.keyCols.map((k) => escapeLiteral(r.keys[k])))));
668
- }
669
- for (const r of batch.rows.values()) {
670
- const keyPred = batch.keyCols.map((k) => `"${k}" = ${escapeLiteral(r.keys[k])}`).join(" AND ");
671
- const updSet = batch.deltaCols.map((c) => `"${c}" = COALESCE("${c}", 0) + (${String(r.deltas[c] ?? 0n)})`).join(", ");
672
- const insertVals = [
673
- ...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
674
- ...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
675
- escapeLiteral(r.meta.blockHeight),
676
- escapeLiteral(r.meta.txId),
677
- "NOW()"
678
- ];
679
- statements.push({
680
- kind: "increment",
681
- table: batch.table,
682
- keyPred,
683
- update: `UPDATE ${qualifiedTable} SET ${updSet} WHERE ${keyPred}`,
684
- insert: `INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) ` + `SELECT ${insertVals.join(", ")} ` + `WHERE NOT EXISTS (SELECT 1 FROM ${qualifiedTable} WHERE ${keyPred})`
685
- });
686
- }
687
- incBatch = null;
688
- incBatchKey = "";
689
- };
690
- const flushInsertBatch = () => {
691
- if (!currentBatch)
692
- return;
693
- const batch = currentBatch;
694
- const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
695
- const colList = batch.cols.map((c) => `"${c}"`).join(", ");
696
- let rows = batch.rows;
697
- if (batch.upsertKeys && batch.upsertKeys.length > 0) {
698
- const keyIndices = batch.upsertKeys.map((k) => batch.cols.indexOf(k));
699
- const byKey = new Map;
700
- for (const row of rows) {
701
- const key = keyIndices.map((ki) => row[ki]).join("\x00");
702
- const first = byKey.get(key);
703
- if (!first) {
704
- byKey.set(key, [...row]);
705
- continue;
706
- }
707
- batch.cols.forEach((c, ci) => {
708
- if (!isSystemColumn(c))
709
- first[ci] = row[ci];
710
- });
711
- }
712
- rows = [...byKey.values()];
713
- }
714
- const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
715
- let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
716
- if (this.journal && batch.upsertKeys && batch.upsertKeys.length > 0) {
717
- const uKeys = batch.upsertKeys;
718
- const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
719
- statements.push(this.journalCaptureSQL(batch.table, uKeys, rows.map((r) => keyIndices.map((ki) => r[ki]))));
720
- }
721
- if (batch.upsertKeys && batch.upsertKeys.length > 0) {
722
- const batchKeys = batch.upsertKeys;
723
- const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !isSystemColumn(c));
724
- if (updateCols.length > 0) {
725
- const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
726
- stmt += ` ON CONFLICT (${batchKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
727
- } else {
728
- stmt += ` ON CONFLICT (${batchKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
729
- }
730
- }
731
- statements.push(stmt);
732
- currentBatch = null;
733
- currentBatchKey = "";
734
- };
735
- for (const op of ops) {
736
- const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
737
- if (op.kind === "insert") {
738
- flushIncrementBatch();
739
- const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
740
- if (batchKey === currentBatchKey && currentBatch) {
741
- currentBatch.rows.push(vals);
742
- } else {
743
- flushInsertBatch();
744
- currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
745
- currentBatchKey = batchKey;
746
- }
747
- } else if (op.kind === "increment") {
748
- flushInsertBatch();
749
- const keyCols = [...op.data._upsert_keys].sort();
750
- const deltaCols = Object.keys(op.set ?? {}).sort();
751
- const batchKey = `inc:${op.table}:${keyCols.join(",")}:${deltaCols.join(",")}`;
752
- if (batchKey !== incBatchKey || !incBatch) {
753
- flushIncrementBatch();
754
- incBatch = { table: op.table, keyCols, deltaCols, rows: new Map };
755
- incBatchKey = batchKey;
756
- }
757
- const clean = stripControlKeys(op.data);
758
- const keySig = keyCols.map((k) => escapeLiteral(clean[k])).join("\x00");
759
- const existing = incBatch.rows.get(keySig);
760
- if (existing) {
761
- for (const c of deltaCols) {
762
- existing.deltas[c] = (existing.deltas[c] ?? 0n) + toBigIntOr0(op.set?.[c]);
763
- }
764
- } else {
765
- const deltas = {};
766
- for (const c of deltaCols)
767
- deltas[c] = toBigIntOr0(op.set?.[c]);
768
- incBatch.rows.set(keySig, {
769
- keys: clean,
770
- deltas,
771
- meta: {
772
- blockHeight: op.data._block_height ?? this.block.height,
773
- txId: op.data._tx_id ?? this._tx.txId
774
- }
775
- });
776
- }
777
- } else {
778
- flushInsertBatch();
779
- flushIncrementBatch();
780
- if (op.kind === "update") {
781
- const setEntries = Object.entries(op.set ?? {});
782
- for (const [k] of setEntries)
783
- validateColumnName(k);
784
- const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
785
- const { clause } = buildWhereClause(op.data);
786
- if (this.journal) {
787
- statements.push(this.journalCaptureByWhereSQL(op.table, clause));
788
- }
789
- statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
790
- } else if (op.kind === "delete") {
791
- const { clause } = buildWhereClause(op.data);
792
- if (this.journal) {
793
- statements.push(this.journalCaptureByWhereSQL(op.table, clause));
794
- }
795
- statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
796
- }
797
- }
798
- }
799
- flushInsertBatch();
800
- flushIncrementBatch();
801
- return statements;
433
+ if (findings.some((f) => f.kind === "needs-events"))
434
+ raise("events");
435
+ for (const f of findings.slice(0, MAX_LISTED_FINDINGS)) {
436
+ reasons.push(f.kind === "needs-events" ? `${formatFinding(f)} (needs event proofs)` : formatFinding(f));
802
437
  }
803
- validateTable(table) {
804
- if (!this.subgraphSchema[table]) {
805
- throw new Error(`Table "${table}" not found in subgraph schema. Available: [${Object.keys(this.subgraphSchema).join(", ")}]`);
806
- }
438
+ if (findings.length > MAX_LISTED_FINDINGS) {
439
+ reasons.push(`…and ${findings.length - MAX_LISTED_FINDINGS} more handler findings`);
807
440
  }
441
+ return {
442
+ level,
443
+ reasons,
444
+ unproven: reasons.length === 0 ? [
445
+ "tx attribution of writes: event.tx, _tx_id (needs event proofs)",
446
+ "intermediate writes within a block (needs event proofs)"
447
+ ] : []
448
+ };
808
449
  }
809
- function isSystemColumn(col) {
810
- return col.startsWith("_");
811
- }
812
- function stripControlKeys(data) {
813
- const {
814
- _upsert_keys: _a,
815
- _upsert_fallback_keys: _b,
816
- _upsert_fallback_set: _c,
817
- ...clean
818
- } = data;
819
- return clean;
450
+ function isDeterminismViolation(v) {
451
+ return v.level === "state" && v.reasons.length > 0;
820
452
  }
821
- function valEq(a, b) {
822
- if (a === b)
823
- return true;
824
- if (a == null || b == null)
825
- return false;
826
- return String(a) === String(b);
453
+ function sha256(input) {
454
+ return createHash2("sha256").update(input).digest("hex");
827
455
  }
828
- function rowMatches(row, where) {
829
- return Object.entries(where).every(([k, v]) => valEq(row[k], v));
456
+ function pinPreimage(input) {
457
+ const fields = {
458
+ handlerHash: sha256(input.handlerCode),
459
+ network: input.network,
460
+ runtime: input.runtime ?? SUBGRAPHS_RUNTIME,
461
+ schemaHash: input.schemaHash,
462
+ startBlock: input.startBlock ?? null
463
+ };
464
+ return JSON.stringify(fields);
830
465
  }
831
- function toBigIntOr0(v) {
832
- if (typeof v === "bigint")
833
- return v;
834
- if (v == null)
835
- return 0n;
836
- try {
837
- return BigInt(String(v));
838
- } catch {
839
- return 0n;
840
- }
466
+ function computePin(input) {
467
+ return sha256(pinPreimage(input));
841
468
  }
842
- function jsonSafe(row) {
843
- const out = {};
844
- for (const [k, v] of Object.entries(row)) {
845
- out[k] = typeof v === "bigint" ? v.toString() : v;
846
- }
847
- return out;
469
+
470
+ // src/runtime/realm.ts
471
+ class NondeterminismError extends Error {
472
+ name = "NondeterminismError";
848
473
  }
849
- function escapeLiteral(value) {
850
- if (value === null || value === undefined)
851
- return "NULL";
852
- if (typeof value === "number" || typeof value === "bigint")
853
- return String(value);
854
- if (typeof value === "boolean")
855
- return value ? "TRUE" : "FALSE";
856
- if (typeof value === "object")
857
- return `'${JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v).replace(/'/g, "''")}'::jsonb`;
858
- return `'${String(value).replace(/'/g, "''")}'`;
474
+
475
+ class HandlerReadError extends Error {
476
+ name = "HandlerReadError";
859
477
  }
860
- function buildWhereClause(where) {
861
- const entries = Object.entries(where);
862
- if (entries.length === 0)
863
- return { clause: "TRUE", values: [] };
864
- for (const [k] of entries)
865
- validateColumnName(k);
866
- const parts = entries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
867
- return { clause: parts.join(" AND "), values: [] };
478
+ function abortsBlock(err) {
479
+ return err instanceof NondeterminismError || err instanceof HandlerReadError;
868
480
  }
869
-
870
- // src/runtime/memory-store.ts
871
- import { normalizeAbi as normalizeAbi2, toCamelCase as toCamelCase2 } from "@secondlayer/stacks/clarity";
872
- class MemoryStore {
873
- tables = new Map;
481
+ var HOST_GLOBALS = [
482
+ "ArrayBuffer",
483
+ "DataView",
484
+ "Int8Array",
485
+ "Uint8Array",
486
+ "Uint8ClampedArray",
487
+ "Int16Array",
488
+ "Uint16Array",
489
+ "Int32Array",
490
+ "Uint32Array",
491
+ "Float16Array",
492
+ "Float32Array",
493
+ "Float64Array",
494
+ "BigInt64Array",
495
+ "BigUint64Array",
496
+ "TextEncoder",
497
+ "TextDecoder",
498
+ "atob",
499
+ "btoa"
500
+ ];
501
+ function forbid(name, reason) {
502
+ return () => {
503
+ throw new NondeterminismError(`${name}: ${reason}`);
504
+ };
874
505
  }
875
-
876
- class MemorySubgraphContext extends SubgraphContext {
877
- store;
878
- tables;
879
- constructor(store, schema, block, tx) {
880
- super(undefined, "memory", schema, block, tx, false);
881
- this.store = store;
882
- this.tables = schema;
883
- }
884
- async readRows(table, where, limit) {
885
- const rows = (this.store.tables.get(table) ?? []).filter((row) => Object.entries(where).every(([k, v]) => sameValue(row[k], v)));
886
- return limit === undefined ? rows : rows.slice(0, limit);
506
+ function createRealm() {
507
+ const host = globalThis;
508
+ const sandbox = Object.create(null);
509
+ for (const name of HOST_GLOBALS) {
510
+ if (host[name] !== undefined)
511
+ sandbox[name] = host[name];
887
512
  }
888
- readDependent = null;
889
- dependsOnPrior(what) {
890
- this.readDependent ??= what;
513
+ const context = vm.createContext(sandbox, {
514
+ name: "subgraph-handler",
515
+ codeGeneration: { strings: false, wasm: false }
516
+ });
517
+ const g = vm.runInContext("globalThis", context);
518
+ const allowed = new Set(ALLOWED_GLOBALS);
519
+ for (const name of Object.getOwnPropertyNames(g)) {
520
+ if (!allowed.has(name))
521
+ delete g[name];
891
522
  }
892
- findOne(table, where) {
893
- this.dependsOnPrior(`findOne("${table}")`);
894
- return super.findOne(table, where);
523
+ for (const [name, reason] of Object.entries(FORBIDDEN_GLOBALS)) {
524
+ Object.defineProperty(g, name, { get: forbid(name, reason) });
895
525
  }
896
- findMany(table, where) {
897
- this.dependsOnPrior(`findMany("${table}")`);
898
- return super.findMany(table, where);
526
+ const realmMath = g.Math;
527
+ const safeMath = {};
528
+ const allowedMath = new Set(ALLOWED_MATH);
529
+ for (const key of Object.getOwnPropertyNames(realmMath)) {
530
+ if (allowedMath.has(key))
531
+ safeMath[key] = realmMath[key];
532
+ else
533
+ Object.defineProperty(safeMath, key, {
534
+ get: forbid(`Math.${key}`, key === "random" ? "randomness differs per run" : "result is engine-approximated")
535
+ });
899
536
  }
900
- update(table, where, set) {
901
- this.dependsOnPrior(`update("${table}")`);
902
- super.update(table, where, set);
537
+ Object.defineProperty(g, "Math", { value: Object.freeze(safeMath) });
538
+ for (const ctor of ["String", "Number", "BigInt", "Array", "Object"]) {
539
+ const proto = g[ctor].prototype;
540
+ for (const method of LOCALE_METHODS) {
541
+ if (Object.prototype.hasOwnProperty.call(proto, method)) {
542
+ Object.defineProperty(proto, method, {
543
+ value: forbid(method, "output depends on the host's locale data")
544
+ });
545
+ }
546
+ }
903
547
  }
904
- delete(table, where) {
905
- this.dependsOnPrior(`delete("${table}")`);
906
- super.delete(table, where);
548
+ Object.preventExtensions(g);
549
+ return context;
550
+ }
551
+ function checkRowValues(where, values) {
552
+ for (const [col, value] of Object.entries(values)) {
553
+ checkValue(value, `${where}.${col}`);
907
554
  }
908
- increment(table, key, deltas) {
909
- this.dependsOnPrior(`increment("${table}")`);
910
- super.increment(table, key, deltas);
555
+ }
556
+ function checkValue(value, path) {
557
+ switch (typeof value) {
558
+ case "number":
559
+ if (!Number.isSafeInteger(value)) {
560
+ throw new TypeError(`${path}: ${value} is not a safe integer; verifiable subgraphs store bigint or integers only`);
561
+ }
562
+ return;
563
+ case "function":
564
+ case "symbol":
565
+ throw new TypeError(`${path}: a ${typeof value} is not a row value`);
566
+ case "object":
567
+ if (value === null || ArrayBuffer.isView(value))
568
+ return;
569
+ if (Array.isArray(value)) {
570
+ value.forEach((v, i) => checkValue(v, `${path}[${i}]`));
571
+ return;
572
+ }
573
+ for (const [k, v] of Object.entries(value))
574
+ checkValue(v, `${path}.${k}`);
575
+ return;
576
+ default:
577
+ return;
911
578
  }
912
- upsert(table, key, row) {
913
- const def = this.tables[table];
914
- if (def) {
915
- const keyCols = Object.keys(key);
916
- const keyed = def.uniqueKeys?.some((uk) => uk.length === keyCols.length && uk.every((c) => keyCols.includes(c)));
917
- const omitted = Object.keys(def.columns).find((c) => !(c in key) && !(c in row));
918
- if (!keyed)
919
- this.dependsOnPrior(`upsert("${table}") without a unique key`);
920
- else if (omitted)
921
- this.dependsOnPrior(`upsert("${table}") leaves "${omitted}" as it was`);
579
+ }
580
+ function deterministicContext(ctx, schema) {
581
+ let pendingReads = 0;
582
+ const knownTable = (table) => {
583
+ if (!Object.prototype.hasOwnProperty.call(schema, table)) {
584
+ throw new Error(`Table "${table}" not found in subgraph schema`);
922
585
  }
923
- super.upsert(table, key, row);
924
- }
925
- async rowsOf(table) {
926
- return this.overlayMany(table, {}, await this.readRows(table, {}));
927
- }
928
- setReads(reads) {
929
- this._client = {
930
- contract(contractId, abi) {
931
- const camelToKebab = new Map;
932
- for (const fn of normalizeAbi2(abi).functions) {
933
- camelToKebab.set(toCamelCase2(fn.name), fn.name);
934
- }
935
- const read = new Proxy({}, {
936
- get(_target, prop) {
937
- const fnName = camelToKebab.get(prop) ?? prop;
938
- const key = `${contractId}.${fnName}`;
939
- return async () => {
940
- if (!(key in reads)) {
941
- throw new Error(`No stubbed chain read for "${key}" — pass it via createTestContext(schema, { reads: { "${key}": … } }).`);
942
- }
943
- return reads[key];
944
- };
945
- }
946
- });
947
- return { read };
586
+ };
587
+ const guard = (op) => {
588
+ if (pendingReads > 0) {
589
+ throw new NondeterminismError(`ctx.${op} while a ctx read is pending: concurrent ctx calls order writes by database latency; await each read before the next call`);
590
+ }
591
+ };
592
+ const read = async (op, run) => {
593
+ guard(op);
594
+ pendingReads++;
595
+ try {
596
+ return await run();
597
+ } catch (err) {
598
+ if (err instanceof NondeterminismError)
599
+ throw err;
600
+ throw new HandlerReadError(`ctx.${op} failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
601
+ } finally {
602
+ pendingReads--;
603
+ }
604
+ };
605
+ return {
606
+ get block() {
607
+ return ctx.block;
608
+ },
609
+ get tx() {
610
+ return ctx.tx;
611
+ },
612
+ get client() {
613
+ throw new NondeterminismError("ctx.client: contract reads need re-execution proofs");
614
+ },
615
+ insert(table, row) {
616
+ guard("insert");
617
+ checkRowValues(`insert ${table}`, row);
618
+ ctx.insert(table, row);
619
+ },
620
+ update(table, where, set) {
621
+ guard("update");
622
+ checkRowValues(`update ${table}`, where);
623
+ checkRowValues(`update ${table}`, set);
624
+ ctx.update(table, where, set);
625
+ },
626
+ upsert(table, key, row) {
627
+ guard("upsert");
628
+ checkRowValues(`upsert ${table}`, key);
629
+ checkRowValues(`upsert ${table}`, row);
630
+ ctx.upsert(table, key, row);
631
+ },
632
+ delete(table, where) {
633
+ guard("delete");
634
+ ctx.delete(table, where);
635
+ },
636
+ increment(table, key, deltas) {
637
+ guard("increment");
638
+ checkRowValues(`increment ${table}`, key);
639
+ checkRowValues(`increment ${table}`, deltas);
640
+ ctx.increment(table, key, deltas);
641
+ },
642
+ findOne: (table, where) => {
643
+ knownTable(table);
644
+ return read("findOne", () => ctx.findOne(table, where));
645
+ },
646
+ findMany: (table, ...rest) => {
647
+ knownTable(table);
648
+ return read("findMany", () => ctx.findMany(table, ...rest));
649
+ }
650
+ };
651
+ }
652
+ var realmDefinitions = new WeakSet;
653
+ async function loadDeterministicDefinition(handlerCode) {
654
+ const { transform } = await import("esbuild");
655
+ const { code } = await transform(handlerCode, {
656
+ format: "cjs",
657
+ loader: "js",
658
+ logLevel: "silent"
659
+ });
660
+ const context = createRealm();
661
+ const evaluate = vm.runInContext(`(function (module, exports) {
662
+ ${code}
663
+ })`, context, { filename: "handler.js" });
664
+ const mod = { exports: {} };
665
+ evaluate(mod, mod.exports);
666
+ const def = mod.exports.default ?? mod.exports;
667
+ const sources = structuredClone(def.sources);
668
+ const schema = structuredClone(def.schema);
669
+ const facades = new WeakMap;
670
+ const handlers = {};
671
+ for (const [name, handler] of Object.entries(def.handlers ?? {})) {
672
+ handlers[name] = (event, ctx) => {
673
+ let facade = facades.get(ctx);
674
+ if (!facade) {
675
+ facade = deterministicContext(ctx, schema);
676
+ facades.set(ctx, facade);
948
677
  }
678
+ return handler(event, facade);
949
679
  };
950
680
  }
951
- async commitOps() {
952
- const tables = new Set;
953
- for (const op of this.ops)
954
- tables.add(op.table);
955
- for (const table of tables) {
956
- this.store.tables.set(table, await this.rowsOf(table));
957
- }
958
- this.ops.length = 0;
681
+ const loaded = { ...def, sources, schema, handlers };
682
+ realmDefinitions.add(loaded);
683
+ return loaded;
684
+ }
685
+
686
+ // src/runtime/chain-read.ts
687
+ function readContractAt(ctx, contractId, abi, options) {
688
+ return ctx.client.contract(contractId, abi, options);
689
+ }
690
+
691
+ class ChainReadError extends Error {
692
+ name = "ChainReadError";
693
+ }
694
+ var NO_NODE_MESSAGE = "ctx.client needs a Stacks node: set STACKS_NODE_RPC_URL on the subgraph processor.";
695
+ async function resolveReadTarget(rpcUrl, db) {
696
+ if (rpcUrl)
697
+ return { kind: "node", rpcUrl };
698
+ const apiUrl = process.env.SUBGRAPH_INDEX_API_URL;
699
+ if (!apiUrl || !await hasNoLocalChain(db)) {
700
+ throw new HandlerReadError(NO_NODE_MESSAGE);
959
701
  }
960
- insertsSince(checkpoint) {
961
- const out = [];
962
- for (const op of this.ops.slice(Math.max(0, checkpoint))) {
963
- if (op.kind !== "insert")
964
- continue;
965
- const row = { ...op.data };
966
- out.push({
967
- table: op.table,
968
- keys: Object.keys(row).filter((k) => !k.startsWith("_")),
969
- row
970
- });
971
- }
972
- return out;
702
+ const apiKey = process.env.INDEX_INTERNAL_API_KEY?.trim();
703
+ if (!apiKey) {
704
+ throw new HandlerReadError("Hosted contract reads need the processor's key (INDEX_INTERNAL_API_KEY), and none is set. Set STACKS_NODE_RPC_URL to read a node directly instead.");
973
705
  }
706
+ return { kind: "hosted", apiUrl, apiKey };
974
707
  }
975
- function sameValue(a, b) {
976
- if (a === b)
977
- return true;
978
- if ((typeof a === "bigint" || typeof a === "number") && (typeof b === "bigint" || typeof b === "number")) {
979
- return BigInt(a) === BigInt(b);
708
+ function hostedReadError(err, label) {
709
+ if (!(err instanceof HttpRequestError))
710
+ return;
711
+ if (err.status === 400) {
712
+ return new ChainReadError(`${label} was refused as invalid: ${err.details ?? err.message}`);
980
713
  }
981
- return String(a) === String(b);
714
+ if (err.status === 403) {
715
+ return new HandlerReadError(`${label}: hosted contract reads serve hosted subgraphs only. ${NO_NODE_MESSAGE}`, { cause: err });
716
+ }
717
+ return;
718
+ }
719
+ function isChainAnswer(err) {
720
+ return err instanceof ContractResponseError || err instanceof ReadContractError;
721
+ }
722
+ function readClient(target) {
723
+ if (target.kind === "node") {
724
+ return createPublicClient({
725
+ transport: http(target.rpcUrl, { retryCount: 0, timeout: 1e4 })
726
+ });
727
+ }
728
+ return createPublicClient({
729
+ transport: http(`${target.apiUrl.replace(/\/+$/, "")}/v1/chain-read`, {
730
+ retryCount: 3,
731
+ timeout: 1e4,
732
+ fetchOptions: {
733
+ headers: { authorization: `Bearer ${target.apiKey}` }
734
+ }
735
+ })
736
+ });
982
737
  }
983
738
 
984
- // src/runtime/runner.ts
985
- import { getErrorMessage } from "@secondlayer/shared";
986
- import { logger as logger4 } from "@secondlayer/shared/logger";
987
- import {
988
- clarityValueToJS,
989
- deserializeCV as deserializeCV4,
990
- toCamelCase as toCamelCase3
991
- } from "@secondlayer/stacks/clarity";
992
-
993
- // src/print-schema.ts
994
- import {
995
- deserializeCV as deserializeCV2
996
- } from "@secondlayer/stacks/clarity";
997
- function camelizeDataKey(str) {
998
- return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
739
+ class ConcurrencyGate {
740
+ limit;
741
+ active = 0;
742
+ queue = [];
743
+ constructor(limit) {
744
+ this.limit = limit;
745
+ }
746
+ async run(fn) {
747
+ if (this.active >= this.limit) {
748
+ await new Promise((resolve) => this.queue.push(resolve));
749
+ }
750
+ this.active++;
751
+ try {
752
+ return await fn();
753
+ } finally {
754
+ this.active--;
755
+ this.queue.shift()?.();
756
+ }
757
+ }
999
758
  }
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[camelizeDataKey(k)] = camelizeKeys(v);
759
+ var DEFAULT_MAX_CONCURRENT_READS = 4;
760
+ function maxConcurrentReads() {
761
+ const raw = process.env.SUBGRAPH_CHAIN_READ_CONCURRENCY;
762
+ const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
763
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT_READS;
764
+ }
765
+ var gate = new ConcurrencyGate(maxConcurrentReads());
766
+ function hashArgs(args) {
767
+ return chainReadArgsHash(args.map((arg) => serializeCV(arg)));
768
+ }
769
+ function findFunction(abi, name) {
770
+ const fn = abi.functions.find((f) => f.name === name);
771
+ if (!fn) {
772
+ throw new ChainReadError(`No function "${name}" in the supplied ABI.`);
1010
773
  }
1011
- return result;
774
+ if (fn.access !== "read-only") {
775
+ throw new ChainReadError(`"${name}" is ${fn.access}, not read-only — a handler cannot call it.`);
776
+ }
777
+ return fn;
1012
778
  }
1013
- function cvToTree(cv) {
1014
- switch (cv.type) {
1015
- case "uint":
1016
- case "int":
1017
- return { kind: cv.type };
1018
- case "true":
1019
- case "false":
1020
- return { kind: "bool" };
1021
- case "address":
1022
- case "contract":
1023
- return { kind: "principal" };
1024
- case "buffer":
1025
- return { kind: "buffer", len: cv.value.length / 2 };
1026
- case "ascii":
1027
- return { kind: "ascii", len: cv.value.length };
1028
- case "utf8":
1029
- return { kind: "utf8", len: new TextEncoder().encode(cv.value).length };
1030
- case "none":
1031
- return { kind: "optional", inner: null };
1032
- case "some":
1033
- return { kind: "optional", inner: cvToTree(cv.value) };
1034
- case "ok":
1035
- return { kind: "response", ok: cvToTree(cv.value), err: null };
1036
- case "err":
1037
- return { kind: "response", ok: null, err: cvToTree(cv.value) };
1038
- case "list": {
1039
- let inner = null;
1040
- for (const el of cv.value) {
1041
- const t = cvToTree(el);
1042
- inner = inner ? unify(inner, t) : t;
779
+ function createChainReadClient(ctx) {
780
+ const readValue = async (contractId, abi, functionName, args, options) => {
781
+ const db = ctx.db ?? getSourceDb();
782
+ const rpcUrl = ctx.rpcUrl || process.env.STACKS_NODE_RPC_URL || undefined;
783
+ if (!rpcUrl && !process.env.SUBGRAPH_INDEX_API_URL) {
784
+ throw new HandlerReadError(NO_NODE_MESSAGE);
785
+ }
786
+ const constant = options.cache === "contract-constant";
787
+ if (!constant && !ctx.indexBlockHash) {
788
+ throw new HandlerReadError(`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.`);
789
+ }
790
+ const fn = findFunction(abi, functionName);
791
+ const clarityArgs = buildFunctionArgs(fn, args);
792
+ const argsHash = hashArgs(clarityArgs);
793
+ const pinnedTo = constant ? null : ctx.indexBlockHash;
794
+ let resultHex;
795
+ try {
796
+ 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();
797
+ resultHex = cached?.result_hex ?? await gate.run(async () => {
798
+ const target = await resolveReadTarget(rpcUrl, db);
799
+ if (!ctx.indexBlockHash && target.kind === "hosted") {
800
+ throw new HandlerReadError(`Block ${ctx.blockHeight} has no index_block_hash, so a hosted contract read cannot be pinned to it. Reindex the subgraph from this block once the block is re-ingested.`);
801
+ }
802
+ if (!ctx.indexBlockHash) {
803
+ logger.warn("Chain read is not pinned to a block (no index_block_hash)", {
804
+ contractId,
805
+ functionName: fn.name,
806
+ blockHeight: ctx.blockHeight,
807
+ note: 'cache: "contract-constant" asserts this value cannot change; the node answered at its own tip'
808
+ });
809
+ }
810
+ try {
811
+ const value = await readContract(readClient(target), {
812
+ contract: contractId,
813
+ functionName: fn.name,
814
+ args: clarityArgs,
815
+ tip: ctx.indexBlockHash ?? undefined
816
+ });
817
+ return serializeCV(value);
818
+ } catch (err) {
819
+ throw target.kind === "hosted" && hostedReadError(err, `Chain read ${contractId}::${fn.name}`) || err;
820
+ }
821
+ });
822
+ if (!cached) {
823
+ await db.insertInto("chain_read_cache").values({
824
+ contract_id: contractId,
825
+ function_name: fn.name,
826
+ args_hash: argsHash,
827
+ index_block_hash: pinnedTo,
828
+ block_height: constant ? null : ctx.blockHeight,
829
+ result_hex: resultHex
830
+ }).onConflict((oc) => oc.doNothing()).execute();
1043
831
  }
1044
- return { kind: "list", inner };
832
+ } catch (err) {
833
+ if (isChainAnswer(err) || err instanceof ChainReadError || err instanceof HandlerReadError) {
834
+ throw err;
835
+ }
836
+ throw new HandlerReadError(`Chain read ${contractId}::${fn.name} at block ${ctx.blockHeight} failed: ${getErrorMessage(err)}`, { cause: err });
1045
837
  }
1046
- case "tuple": {
1047
- const entries = new Map;
1048
- for (const [k, v] of Object.entries(cv.value)) {
1049
- entries.set(k, { tree: cvToTree(v), present: 1 });
838
+ const value = deserializeCV(resultHex);
839
+ const js = clarityValueToJSUntyped(fn.outputs, value);
840
+ if (isResponseOutput(fn.outputs)) {
841
+ const response = js;
842
+ if ("ok" in response)
843
+ return response.ok;
844
+ throw new ContractResponseError(`${contractId}::${fn.name} returned an err response.`, response.err);
845
+ }
846
+ return js;
847
+ };
848
+ return {
849
+ contract(contractId, rawAbi, options = {}) {
850
+ const abi = normalizeAbi(rawAbi);
851
+ const camelToKebab = new Map;
852
+ for (const fn of abi.functions) {
853
+ camelToKebab.set(toCamelCase(fn.name), fn.name);
1050
854
  }
1051
- return { kind: "tuple", count: 1, entries };
855
+ const read = new Proxy({}, {
856
+ get(_target, prop) {
857
+ const fnName = camelToKebab.get(prop) ?? prop;
858
+ return (args = {}) => readValue(contractId, abi, fnName, args, options);
859
+ }
860
+ });
861
+ return { read };
862
+ }
863
+ };
864
+ }
865
+
866
+ // src/runtime/context.ts
867
+ var journalEnsured = new Set;
868
+ function validateColumnName(name) {
869
+ if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
870
+ throw new Error(`Invalid column name: ${name}`);
871
+ }
872
+ }
873
+
874
+ class SubgraphContext {
875
+ block;
876
+ _tx;
877
+ db;
878
+ pgSchemaName;
879
+ subgraphSchema;
880
+ ops = [];
881
+ journal;
882
+ _client;
883
+ constructor(db, pgSchemaName, subgraphSchema, block, tx, journal = false) {
884
+ this.db = db;
885
+ this.pgSchemaName = pgSchemaName;
886
+ this.subgraphSchema = subgraphSchema;
887
+ this.block = block;
888
+ this._tx = tx;
889
+ this.journal = journal;
890
+ }
891
+ get client() {
892
+ this._client ??= createChainReadClient({
893
+ blockHeight: this.block.height,
894
+ indexBlockHash: this.block.indexBlockHash
895
+ });
896
+ return this._client;
897
+ }
898
+ get tx() {
899
+ return this._tx;
900
+ }
901
+ setTx(tx) {
902
+ this._tx = tx;
903
+ }
904
+ insert(table, row) {
905
+ this.validateTable(table);
906
+ this.ops.push({
907
+ kind: "insert",
908
+ table,
909
+ data: { ...row, _block_height: this.block.height, _tx_id: this._tx.txId }
910
+ });
911
+ }
912
+ update(table, where, set) {
913
+ this.validateTable(table);
914
+ this.ops.push({ kind: "update", table, data: where, set });
915
+ }
916
+ upsert(table, key, row) {
917
+ this.validateTable(table);
918
+ const tableDef = this.subgraphSchema[table];
919
+ if (!tableDef)
920
+ return;
921
+ const keyColumns = Object.keys(key);
922
+ const hasUniqueConstraint = tableDef.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
923
+ const meta = { _block_height: this.block.height, _tx_id: this._tx.txId };
924
+ if (hasUniqueConstraint) {
925
+ this.ops.push({
926
+ kind: "insert",
927
+ table,
928
+ data: { ...key, ...row, ...meta, _upsert_keys: keyColumns }
929
+ });
930
+ } else {
931
+ logger2.warn("upsert called without matching uniqueKeys constraint, using fallback", {
932
+ table,
933
+ keys: keyColumns
934
+ });
935
+ this.ops.push({
936
+ kind: "insert",
937
+ table,
938
+ data: {
939
+ ...key,
940
+ ...row,
941
+ ...meta,
942
+ _upsert_fallback_keys: keyColumns,
943
+ _upsert_fallback_set: row
944
+ }
945
+ });
1052
946
  }
1053
947
  }
1054
- }
1055
- function unifyNullable(a, b) {
1056
- if (!a)
1057
- return b;
1058
- if (!b)
1059
- return a;
1060
- return unify(a, b);
1061
- }
1062
- var UNION_KIND_ORDER = {
1063
- uint: 0,
1064
- int: 1,
1065
- bool: 2,
1066
- principal: 3,
1067
- buffer: 4,
1068
- ascii: 5,
1069
- utf8: 6,
1070
- list: 7,
1071
- tuple: 8,
1072
- response: 9
1073
- };
1074
- function makeUnion(members) {
1075
- const sorted = [...members].sort((a, b) => (UNION_KIND_ORDER[a.kind] ?? 99) - (UNION_KIND_ORDER[b.kind] ?? 99));
1076
- return { kind: "union", members: sorted };
1077
- }
1078
- function unify(a, b) {
1079
- if (a.kind === "optional" || b.kind === "optional") {
1080
- const ai = a.kind === "optional" ? a.inner : a;
1081
- const bi = b.kind === "optional" ? b.inner : b;
1082
- return { kind: "optional", inner: unifyNullable(ai, bi) };
948
+ delete(table, where) {
949
+ this.validateTable(table);
950
+ this.ops.push({ kind: "delete", table, data: where });
1083
951
  }
1084
- if (a.kind === "union")
1085
- return unionAdd(a.members, b);
1086
- if (b.kind === "union")
1087
- return unionAdd(b.members, a);
1088
- if (a.kind !== b.kind)
1089
- return makeUnion([a, b]);
1090
- switch (a.kind) {
1091
- case "uint":
1092
- case "int":
1093
- case "bool":
1094
- case "principal":
1095
- return a;
1096
- case "buffer":
1097
- case "ascii":
1098
- case "utf8":
1099
- return { kind: a.kind, len: Math.max(a.len, b.len) };
1100
- case "list":
1101
- return {
1102
- kind: "list",
1103
- inner: unifyNullable(a.inner, b.inner)
1104
- };
1105
- case "response": {
1106
- const rb = b;
1107
- return {
1108
- kind: "response",
1109
- ok: unifyNullable(a.ok, rb.ok),
1110
- err: unifyNullable(a.err, rb.err)
1111
- };
952
+ increment(table, key, deltas) {
953
+ this.validateTable(table);
954
+ const tableDef = this.subgraphSchema[table];
955
+ const keyColumns = Object.keys(key);
956
+ const hasUniqueConstraint = tableDef?.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
957
+ if (!hasUniqueConstraint) {
958
+ throw new Error(`increment("${table}") requires a uniqueKeys constraint on [${keyColumns.join(", ")}]`);
1112
959
  }
1113
- case "tuple": {
1114
- const tb = b;
1115
- const entries = new Map([...a.entries].map(([k, e]) => [k, { ...e }]));
1116
- for (const [k, e] of tb.entries) {
1117
- const existing = entries.get(k);
1118
- entries.set(k, existing ? {
1119
- tree: unify(existing.tree, e.tree),
1120
- present: existing.present + e.present
1121
- } : { ...e });
960
+ for (const [col, v] of Object.entries(deltas)) {
961
+ validateColumnName(col);
962
+ if (keyColumns.includes(col)) {
963
+ throw new Error(`increment("${table}"): "${col}" is a key column`);
964
+ }
965
+ if (typeof v !== "bigint" && typeof v !== "number") {
966
+ throw new Error(`increment("${table}"): delta for "${col}" must be bigint or number`);
1122
967
  }
1123
- return { kind: "tuple", count: a.count + tb.count, entries };
1124
968
  }
969
+ this.ops.push({
970
+ kind: "increment",
971
+ table,
972
+ data: {
973
+ ...key,
974
+ _block_height: this.block.height,
975
+ _tx_id: this._tx.txId,
976
+ _upsert_keys: keyColumns
977
+ },
978
+ set: { ...deltas }
979
+ });
1125
980
  }
1126
- }
1127
- function unionAdd(members, t) {
1128
- if (t.kind === "union") {
1129
- let acc = { kind: "union", members };
1130
- for (const m of t.members)
1131
- acc = unify(acc, m);
1132
- return acc;
981
+ opsCheckpoint() {
982
+ return this.ops.length;
1133
983
  }
1134
- const next = [...members];
1135
- for (let i = 0;i < next.length; i++) {
1136
- const member = next[i];
1137
- if (!member)
1138
- continue;
1139
- const merged = unify(member, t);
1140
- if (merged.kind !== "union") {
1141
- next[i] = merged;
1142
- return makeUnion(next);
984
+ rollbackTo(checkpoint) {
985
+ if (checkpoint < 0 || checkpoint > this.ops.length)
986
+ return;
987
+ this.ops.length = checkpoint;
988
+ }
989
+ async readRows(table, where, limit) {
990
+ const qualifiedTable = `"${this.pgSchemaName}"."${table}"`;
991
+ const { clause } = buildWhereClause(where);
992
+ const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} ORDER BY "_id"${limit !== undefined ? ` LIMIT ${limit}` : ""}`;
993
+ const { rows } = await sql.raw(query).execute(this.db);
994
+ return rows.map((r) => this.coerceRow(table, r));
995
+ }
996
+ async findOne(table, where) {
997
+ this.validateTable(table);
998
+ const row = (await this.readRows(table, where, 1))[0] ?? null;
999
+ return this.overlayOne(table, where, row);
1000
+ }
1001
+ async findMany(table, where) {
1002
+ this.validateTable(table);
1003
+ return this.overlayMany(table, where, await this.readRows(table, where));
1004
+ }
1005
+ overlayOne(table, where, dbRow) {
1006
+ if (this.ops.length === 0)
1007
+ return dbRow;
1008
+ let row = dbRow;
1009
+ for (const op of this.ops) {
1010
+ if (op.table !== table)
1011
+ continue;
1012
+ row = this.applyOpToRow(op, row, where);
1143
1013
  }
1014
+ return row;
1144
1015
  }
1145
- next.push(t);
1146
- return makeUnion(next);
1147
- }
1148
- function wrapOptional(t) {
1149
- return t.kind === "optional" ? t : { kind: "optional", inner: t };
1150
- }
1151
- function renderClarity(t) {
1152
- if (!t)
1153
- return "?";
1154
- switch (t.kind) {
1155
- case "uint":
1156
- case "int":
1157
- case "bool":
1158
- case "principal":
1159
- return t.kind;
1160
- case "buffer":
1161
- return `(buff ${t.len})`;
1162
- case "ascii":
1163
- return `(string-ascii ${t.len})`;
1164
- case "utf8":
1165
- return `(string-utf8 ${t.len})`;
1166
- case "optional":
1167
- return `(optional ${renderClarity(t.inner)})`;
1168
- case "list":
1169
- return `(list ${renderClarity(t.inner)})`;
1170
- case "response":
1171
- return `(response ${renderClarity(t.ok)} ${renderClarity(t.err)})`;
1172
- case "tuple": {
1173
- const parts = [...t.entries].map(([k, e]) => {
1174
- const tree = e.present < t.count ? wrapOptional(e.tree) : e.tree;
1175
- return `(${k} ${renderClarity(tree)})`;
1176
- });
1177
- return `(tuple ${parts.join(" ")})`;
1016
+ overlayMany(table, where, dbRows) {
1017
+ if (this.ops.length === 0)
1018
+ return [...dbRows];
1019
+ let result = [...dbRows];
1020
+ for (const op of this.ops) {
1021
+ if (op.table !== table)
1022
+ continue;
1023
+ if (op.kind === "update") {
1024
+ for (let i = 0;i < result.length; i++) {
1025
+ if (rowMatches(result[i], op.data))
1026
+ result[i] = { ...result[i], ...op.set ?? {} };
1027
+ }
1028
+ } else if (op.kind === "delete") {
1029
+ result = result.filter((r) => !rowMatches(r, op.data));
1030
+ } else {
1031
+ const upsertKeys = op.data._upsert_keys;
1032
+ const clean = stripControlKeys(op.data);
1033
+ const idx = upsertKeys ? result.findIndex((r) => upsertKeys.every((k) => valEq(r[k], clean[k]))) : -1;
1034
+ if (idx >= 0) {
1035
+ const existing = result[idx];
1036
+ result[idx] = this.applyOpToRow(op, existing, where) ?? existing;
1037
+ } else {
1038
+ const created = this.applyOpToRow(op, null, where);
1039
+ if (created)
1040
+ result.push(created);
1041
+ }
1042
+ }
1043
+ }
1044
+ return result;
1045
+ }
1046
+ applyOpToRow(op, row, where) {
1047
+ const upsertKeys = op.data._upsert_keys;
1048
+ const clean = stripControlKeys(op.data);
1049
+ switch (op.kind) {
1050
+ case "insert": {
1051
+ if (row) {
1052
+ if (upsertKeys?.every((k) => valEq(row[k], clean[k]))) {
1053
+ const merged = { ...row };
1054
+ for (const [k, v] of Object.entries(clean)) {
1055
+ if (!upsertKeys.includes(k) && !isSystemColumn(k))
1056
+ merged[k] = v;
1057
+ }
1058
+ return merged;
1059
+ }
1060
+ return row;
1061
+ }
1062
+ return rowMatches(clean, where) ? { ...clean } : null;
1063
+ }
1064
+ case "increment": {
1065
+ const deltas = op.set ?? {};
1066
+ if (row) {
1067
+ if (upsertKeys.every((k) => valEq(row[k], clean[k]))) {
1068
+ const merged = { ...row };
1069
+ for (const [col, d] of Object.entries(deltas)) {
1070
+ merged[col] = toBigIntOr0(merged[col]) + toBigIntOr0(d);
1071
+ }
1072
+ return merged;
1073
+ }
1074
+ return row;
1075
+ }
1076
+ if (!rowMatches(clean, where))
1077
+ return null;
1078
+ const created = { ...clean };
1079
+ for (const [col, d] of Object.entries(deltas)) {
1080
+ created[col] = toBigIntOr0(d);
1081
+ }
1082
+ return created;
1083
+ }
1084
+ case "update":
1085
+ return row && rowMatches(row, op.data) ? { ...row, ...op.set ?? {} } : row;
1086
+ case "delete":
1087
+ return row && rowMatches(row, op.data) ? null : row;
1178
1088
  }
1179
- case "union":
1180
- return t.members.map(renderClarity).join(" | ");
1181
1089
  }
1182
- }
1183
- function renderTs(t) {
1184
- if (!t)
1185
- return "unknown";
1186
- switch (t.kind) {
1187
- case "uint":
1188
- case "int":
1189
- return "bigint";
1190
- case "bool":
1191
- return "boolean";
1192
- case "principal":
1193
- case "buffer":
1194
- case "ascii":
1195
- case "utf8":
1196
- return "string";
1197
- case "optional":
1198
- return t.inner ? `${renderTs(t.inner)} | null` : "unknown | null";
1199
- case "list": {
1200
- const inner = renderTs(t.inner);
1201
- return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
1202
- }
1203
- case "response": {
1204
- const sides = [...new Set([renderTs(t.ok), renderTs(t.err)])];
1205
- return sides.join(" | ");
1090
+ coerceRow(table, row) {
1091
+ const tableDef = this.subgraphSchema[table];
1092
+ if (!tableDef)
1093
+ return row;
1094
+ const result = { ...row };
1095
+ for (const [col, def] of Object.entries(tableDef.columns)) {
1096
+ if ((def.type === "uint" || def.type === "int") && typeof result[col] === "string") {
1097
+ result[col] = BigInt(result[col]);
1098
+ }
1206
1099
  }
1207
- case "tuple": {
1208
- const parts = [...t.entries].map(([k, e]) => {
1209
- const opt = e.present < t.count ? "?" : "";
1210
- return `${camelizeDataKey(k)}${opt}: ${renderTs(e.tree)}`;
1100
+ return result;
1101
+ }
1102
+ get pendingOps() {
1103
+ return this.ops.length;
1104
+ }
1105
+ async flush() {
1106
+ if (this.ops.length === 0)
1107
+ return { count: 0, writes: [] };
1108
+ await this.ensureJournalTable();
1109
+ const opsToFlush = [...this.ops];
1110
+ this.ops.length = 0;
1111
+ const statements = this.buildStatements(opsToFlush);
1112
+ if ("isTransaction" in this.db) {
1113
+ await this.execStatements(this.db, statements);
1114
+ } else {
1115
+ await this.db.transaction().execute(async (tx) => {
1116
+ await this.execStatements(tx, statements);
1211
1117
  });
1212
- return `{ ${parts.join("; ")} }`;
1213
1118
  }
1214
- case "union":
1215
- return [...new Set(t.members.map((m) => renderTs(m)))].join(" | ");
1119
+ const writes = opsToFlush.map((op, rowIndex) => {
1120
+ const blockHeight = op.data._block_height ?? this.block.height;
1121
+ const txId = op.data._tx_id ?? this._tx.txId;
1122
+ const baseRow = op.kind === "update" || op.kind === "increment" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
1123
+ baseRow._upsert_keys = undefined;
1124
+ baseRow._upsert_fallback_keys = undefined;
1125
+ baseRow._upsert_fallback_set = undefined;
1126
+ return {
1127
+ op: op.kind === "increment" ? "update" : op.kind,
1128
+ table: op.table,
1129
+ row: jsonSafe(baseRow),
1130
+ pk: { blockHeight, txId, rowIndex }
1131
+ };
1132
+ });
1133
+ return { count: opsToFlush.length, writes };
1216
1134
  }
1217
- }
1218
- function toColumnType(t) {
1219
- if (!t)
1220
- return "jsonb";
1221
- switch (t.kind) {
1222
- case "uint":
1223
- return "uint";
1224
- case "int":
1225
- return "int";
1226
- case "bool":
1227
- return "boolean";
1228
- case "principal":
1229
- return "principal";
1230
- case "buffer":
1231
- case "ascii":
1232
- case "utf8":
1233
- return "text";
1234
- case "list":
1235
- case "tuple":
1236
- return "jsonb";
1237
- case "optional":
1238
- return toColumnType(t.inner);
1239
- case "response":
1240
- return t.ok ? toColumnType(t.ok) : "jsonb";
1241
- case "union":
1242
- return "jsonb";
1135
+ async execStatements(executor, statements) {
1136
+ const affected = (r) => r.numAffectedRows ?? 0n;
1137
+ for (const stmt of statements) {
1138
+ if (typeof stmt === "string") {
1139
+ await sql.raw(stmt).execute(executor);
1140
+ continue;
1141
+ }
1142
+ const updated = await sql.raw(stmt.update).execute(executor);
1143
+ if (affected(updated) > 0n)
1144
+ continue;
1145
+ const inserted = await sql.raw(stmt.insert).execute(executor);
1146
+ if (affected(inserted) > 0n)
1147
+ continue;
1148
+ const retried = await sql.raw(stmt.update).execute(executor);
1149
+ if (affected(retried) === 0n) {
1150
+ throw new Error(`increment could not be applied to ${this.pgSchemaName}.${stmt.table} where ${stmt.keyPred}: row neither updated nor inserted`);
1151
+ }
1152
+ }
1243
1153
  }
1244
- }
1245
- var MAX_DECODED_PER_TOPIC_NEWEST = 75;
1246
- var MAX_DECODED_PER_TOPIC_OLDEST = 25;
1247
- function inferPrintTopics(samples) {
1248
- const groups = new Map;
1249
- for (const s of samples) {
1250
- const group = groups.get(s.topic);
1251
- if (group)
1252
- group.push(s);
1253
- else
1254
- groups.set(s.topic, [s]);
1154
+ prepareInsert(op) {
1155
+ const upsertKeys = op.data._upsert_keys;
1156
+ const data = { ...op.data };
1157
+ delete data._upsert_keys;
1158
+ delete data._upsert_fallback_keys;
1159
+ delete data._upsert_fallback_set;
1160
+ if (!data._block_height)
1161
+ data._block_height = this.block.height;
1162
+ if (!data._tx_id)
1163
+ data._tx_id = this._tx.txId;
1164
+ data._created_at = "NOW()";
1165
+ const cols = Object.keys(data);
1166
+ cols.forEach(validateColumnName);
1167
+ const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
1168
+ const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
1169
+ return { data, cols, vals, upsertKeys, batchKey };
1255
1170
  }
1256
- const out = [];
1257
- for (const [topic, rows] of groups) {
1258
- let first = Number.POSITIVE_INFINITY;
1259
- let last = Number.NEGATIVE_INFINITY;
1260
- for (const r of rows) {
1261
- if (r.blockHeight < first)
1262
- first = r.blockHeight;
1263
- if (r.blockHeight > last)
1264
- last = r.blockHeight;
1171
+ async ensureJournalTable() {
1172
+ if (!this.journal || journalEnsured.has(this.pgSchemaName))
1173
+ return;
1174
+ const { rows } = await sql.raw(`SELECT to_regclass('"${this.pgSchemaName}"."_journal"') AS r`).execute(this.db);
1175
+ if (rows[0]?.r) {
1176
+ journalEnsured.add(this.pgSchemaName);
1177
+ return;
1265
1178
  }
1266
- const withHex = [...rows].filter((r) => r.rawHex !== null).sort((a, b) => b.blockHeight - a.blockHeight);
1267
- const budget = MAX_DECODED_PER_TOPIC_NEWEST + MAX_DECODED_PER_TOPIC_OLDEST;
1268
- const picked = withHex.length <= budget ? withHex : [
1269
- ...withHex.slice(0, MAX_DECODED_PER_TOPIC_NEWEST),
1270
- ...withHex.slice(-MAX_DECODED_PER_TOPIC_OLDEST)
1271
- ];
1272
- const tuples = [];
1273
- let decoded = 0;
1274
- for (const p of picked) {
1275
- try {
1276
- const cv = deserializeCV2(p.rawHex);
1277
- decoded++;
1278
- if (cv.type === "tuple")
1279
- tuples.push(cv);
1280
- } catch {}
1179
+ for (const stmt of emitJournalDDL(this.pgSchemaName)) {
1180
+ await sql.raw(stmt).execute(this.db);
1281
1181
  }
1282
- const nonTuple = decoded > 0 && tuples.length === 0;
1283
- const fields = [];
1284
- if (!nonTuple) {
1285
- const stats = new Map;
1286
- for (const t of tuples) {
1287
- for (const [key, value] of Object.entries(t.value)) {
1288
- if (key === "topic")
1182
+ }
1183
+ columnSqlType(table, col) {
1184
+ const def = this.subgraphSchema[table]?.columns?.[col];
1185
+ return def ? TYPE_MAP[def.type] : undefined;
1186
+ }
1187
+ journalCaptureSQL(table, keyCols, keyLiteralRows) {
1188
+ const cast = (col, expr) => {
1189
+ const t = this.columnSqlType(table, col);
1190
+ return t ? `CAST(${expr} AS ${t})` : expr;
1191
+ };
1192
+ const keyObj = keyCols.map((k) => `'${k}', ${cast(k, `v."${k}"`)}`).join(", ");
1193
+ const joinCond = keyCols.map((k) => `t."${k}" = ${cast(k, `v."${k}"`)}`).join(" AND ");
1194
+ const valuesList = keyLiteralRows.map((r) => `(${r.join(", ")})`).join(", ");
1195
+ 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}`;
1196
+ }
1197
+ journalCaptureByWhereSQL(table, clause) {
1198
+ 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}`;
1199
+ }
1200
+ buildStatements(ops) {
1201
+ const statements = [];
1202
+ let currentBatch = null;
1203
+ let currentBatchKey = "";
1204
+ let incBatch = null;
1205
+ let incBatchKey = "";
1206
+ const flushIncrementBatch = () => {
1207
+ if (!incBatch)
1208
+ return;
1209
+ const batch = incBatch;
1210
+ const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
1211
+ const cols = [
1212
+ ...batch.keyCols,
1213
+ ...batch.deltaCols,
1214
+ "_block_height",
1215
+ "_tx_id",
1216
+ "_created_at"
1217
+ ];
1218
+ if (this.journal) {
1219
+ statements.push(this.journalCaptureSQL(batch.table, batch.keyCols, Array.from(batch.rows.values()).map((r) => batch.keyCols.map((k) => escapeLiteral(r.keys[k])))));
1220
+ }
1221
+ for (const r of batch.rows.values()) {
1222
+ const keyPred = batch.keyCols.map((k) => `"${k}" = ${escapeLiteral(r.keys[k])}`).join(" AND ");
1223
+ const updSet = batch.deltaCols.map((c) => `"${c}" = COALESCE("${c}", 0) + (${String(r.deltas[c] ?? 0n)})`).join(", ");
1224
+ const insertVals = [
1225
+ ...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
1226
+ ...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
1227
+ escapeLiteral(r.meta.blockHeight),
1228
+ escapeLiteral(r.meta.txId),
1229
+ "NOW()"
1230
+ ];
1231
+ statements.push({
1232
+ kind: "increment",
1233
+ table: batch.table,
1234
+ keyPred,
1235
+ update: `UPDATE ${qualifiedTable} SET ${updSet} WHERE ${keyPred}`,
1236
+ insert: `INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) ` + `SELECT ${insertVals.join(", ")} ` + `WHERE NOT EXISTS (SELECT 1 FROM ${qualifiedTable} WHERE ${keyPred})`
1237
+ });
1238
+ }
1239
+ incBatch = null;
1240
+ incBatchKey = "";
1241
+ };
1242
+ const flushInsertBatch = () => {
1243
+ if (!currentBatch)
1244
+ return;
1245
+ const batch = currentBatch;
1246
+ const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
1247
+ const colList = batch.cols.map((c) => `"${c}"`).join(", ");
1248
+ let rows = batch.rows;
1249
+ if (batch.upsertKeys && batch.upsertKeys.length > 0) {
1250
+ const keyIndices = batch.upsertKeys.map((k) => batch.cols.indexOf(k));
1251
+ const byKey = new Map;
1252
+ for (const row of rows) {
1253
+ const key = keyIndices.map((ki) => row[ki]).join("\x00");
1254
+ const first = byKey.get(key);
1255
+ if (!first) {
1256
+ byKey.set(key, [...row]);
1289
1257
  continue;
1290
- const tree = cvToTree(value);
1291
- const existing = stats.get(key);
1292
- if (existing) {
1293
- existing.present++;
1294
- existing.tree = unify(existing.tree, tree);
1295
- if (value.type === "none")
1296
- existing.noneCount++;
1297
- if (value.type === "none" || value.type === "some") {
1298
- existing.optionalSeen = true;
1299
- }
1300
- } else {
1301
- stats.set(key, {
1302
- tree,
1303
- present: 1,
1304
- noneCount: value.type === "none" ? 1 : 0,
1305
- optionalSeen: value.type === "none" || value.type === "some"
1306
- });
1307
1258
  }
1259
+ batch.cols.forEach((c, ci) => {
1260
+ if (!isSystemColumn(c))
1261
+ first[ci] = row[ci];
1262
+ });
1308
1263
  }
1264
+ rows = [...byKey.values()];
1309
1265
  }
1310
- for (const [name, st] of [...stats].sort(([a], [b]) => a.localeCompare(b))) {
1311
- const field = {
1312
- name,
1313
- camel_name: camelizeDataKey(name),
1314
- clarity_type: renderClarity(st.tree),
1315
- ts_type: renderTs(st.tree),
1316
- column_type: toColumnType(st.tree),
1317
- always_present: st.present === tuples.length
1318
- };
1319
- if (st.optionalSeen) {
1320
- field.optional_some_rate = (st.present - st.noneCount) / st.present;
1266
+ const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
1267
+ let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
1268
+ if (this.journal && batch.upsertKeys && batch.upsertKeys.length > 0) {
1269
+ const uKeys = batch.upsertKeys;
1270
+ const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
1271
+ statements.push(this.journalCaptureSQL(batch.table, uKeys, rows.map((r) => keyIndices.map((ki) => r[ki]))));
1272
+ }
1273
+ if (batch.upsertKeys && batch.upsertKeys.length > 0) {
1274
+ const batchKeys = batch.upsertKeys;
1275
+ const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !isSystemColumn(c));
1276
+ if (updateCols.length > 0) {
1277
+ const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
1278
+ stmt += ` ON CONFLICT (${batchKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
1279
+ } else {
1280
+ stmt += ` ON CONFLICT (${batchKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
1281
+ }
1282
+ }
1283
+ statements.push(stmt);
1284
+ currentBatch = null;
1285
+ currentBatchKey = "";
1286
+ };
1287
+ for (const op of ops) {
1288
+ const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
1289
+ if (op.kind === "insert") {
1290
+ flushIncrementBatch();
1291
+ const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
1292
+ if (batchKey === currentBatchKey && currentBatch) {
1293
+ currentBatch.rows.push(vals);
1294
+ } else {
1295
+ flushInsertBatch();
1296
+ currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
1297
+ currentBatchKey = batchKey;
1298
+ }
1299
+ } else if (op.kind === "increment") {
1300
+ flushInsertBatch();
1301
+ const keyCols = [...op.data._upsert_keys].sort();
1302
+ const deltaCols = Object.keys(op.set ?? {}).sort();
1303
+ const batchKey = `inc:${op.table}:${keyCols.join(",")}:${deltaCols.join(",")}`;
1304
+ if (batchKey !== incBatchKey || !incBatch) {
1305
+ flushIncrementBatch();
1306
+ incBatch = { table: op.table, keyCols, deltaCols, rows: new Map };
1307
+ incBatchKey = batchKey;
1308
+ }
1309
+ const clean = stripControlKeys(op.data);
1310
+ const keySig = keyCols.map((k) => escapeLiteral(clean[k])).join("\x00");
1311
+ const existing = incBatch.rows.get(keySig);
1312
+ if (existing) {
1313
+ for (const c of deltaCols) {
1314
+ existing.deltas[c] = (existing.deltas[c] ?? 0n) + toBigIntOr0(op.set?.[c]);
1315
+ }
1316
+ } else {
1317
+ const deltas = {};
1318
+ for (const c of deltaCols)
1319
+ deltas[c] = toBigIntOr0(op.set?.[c]);
1320
+ incBatch.rows.set(keySig, {
1321
+ keys: clean,
1322
+ deltas,
1323
+ meta: {
1324
+ blockHeight: op.data._block_height ?? this.block.height,
1325
+ txId: op.data._tx_id ?? this._tx.txId
1326
+ }
1327
+ });
1328
+ }
1329
+ } else {
1330
+ flushInsertBatch();
1331
+ flushIncrementBatch();
1332
+ if (op.kind === "update") {
1333
+ const setEntries = Object.entries(op.set ?? {});
1334
+ for (const [k] of setEntries)
1335
+ validateColumnName(k);
1336
+ const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
1337
+ const { clause } = buildWhereClause(op.data);
1338
+ if (this.journal) {
1339
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
1340
+ }
1341
+ statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
1342
+ } else if (op.kind === "delete") {
1343
+ const { clause } = buildWhereClause(op.data);
1344
+ if (this.journal) {
1345
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
1346
+ }
1347
+ statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
1321
1348
  }
1322
- fields.push(field);
1323
1349
  }
1324
1350
  }
1325
- out.push({
1326
- topic,
1327
- count: rows.length,
1328
- first_height: first,
1329
- last_height: last,
1330
- non_tuple: nonTuple,
1331
- fields
1332
- });
1351
+ flushInsertBatch();
1352
+ flushIncrementBatch();
1353
+ return statements;
1354
+ }
1355
+ validateTable(table) {
1356
+ if (!this.subgraphSchema[table]) {
1357
+ throw new Error(`Table "${table}" not found in subgraph schema. Available: [${Object.keys(this.subgraphSchema).join(", ")}]`);
1358
+ }
1333
1359
  }
1334
- return out.sort((a, b) => b.count - a.count);
1335
1360
  }
1336
-
1337
- // src/runtime/clarity.ts
1338
- import { cvToValue, deserializeCV as deserializeCV3 } from "@secondlayer/stacks/clarity";
1339
- function decodeClarityValue(hex) {
1361
+ function isSystemColumn(col) {
1362
+ return col.startsWith("_");
1363
+ }
1364
+ function stripControlKeys(data) {
1365
+ const {
1366
+ _upsert_keys: _a,
1367
+ _upsert_fallback_keys: _b,
1368
+ _upsert_fallback_set: _c,
1369
+ ...clean
1370
+ } = data;
1371
+ return clean;
1372
+ }
1373
+ function valEq(a, b) {
1374
+ if (a === b)
1375
+ return true;
1376
+ if (a == null || b == null)
1377
+ return false;
1378
+ return String(a) === String(b);
1379
+ }
1380
+ function rowMatches(row, where) {
1381
+ return Object.entries(where).every(([k, v]) => valEq(row[k], v));
1382
+ }
1383
+ function toBigIntOr0(v) {
1384
+ if (typeof v === "bigint")
1385
+ return v;
1386
+ if (v == null)
1387
+ return 0n;
1340
1388
  try {
1341
- const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
1342
- const cv = deserializeCV3(cleanHex);
1343
- return cvToValue(cv);
1389
+ return BigInt(String(v));
1344
1390
  } catch {
1345
- return hex;
1391
+ return 0n;
1346
1392
  }
1347
1393
  }
1348
- function decodeEventData(data) {
1349
- if (typeof data === "string" && data.startsWith("0x") && data.length > 10) {
1350
- return decodeClarityValue(data);
1394
+ function jsonSafe(row) {
1395
+ const out = {};
1396
+ for (const [k, v] of Object.entries(row)) {
1397
+ out[k] = typeof v === "bigint" ? v.toString() : v;
1351
1398
  }
1352
- if (Array.isArray(data)) {
1353
- return data.map(decodeEventData);
1399
+ return out;
1400
+ }
1401
+ function escapeLiteral(value) {
1402
+ if (value === null || value === undefined)
1403
+ return "NULL";
1404
+ if (typeof value === "number" || typeof value === "bigint")
1405
+ return String(value);
1406
+ if (typeof value === "boolean")
1407
+ return value ? "TRUE" : "FALSE";
1408
+ if (typeof value === "object")
1409
+ return `'${JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v).replace(/'/g, "''")}'::jsonb`;
1410
+ return `'${String(value).replace(/'/g, "''")}'`;
1411
+ }
1412
+ function buildWhereClause(where) {
1413
+ const entries = Object.entries(where);
1414
+ if (entries.length === 0)
1415
+ return { clause: "TRUE", values: [] };
1416
+ for (const [k] of entries)
1417
+ validateColumnName(k);
1418
+ const parts = entries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
1419
+ return { clause: parts.join(" AND "), values: [] };
1420
+ }
1421
+
1422
+ // src/runtime/memory-store.ts
1423
+ import { normalizeAbi as normalizeAbi2, toCamelCase as toCamelCase2 } from "@secondlayer/stacks/clarity";
1424
+ class MemoryStore {
1425
+ tables = new Map;
1426
+ }
1427
+
1428
+ class MemorySubgraphContext extends SubgraphContext {
1429
+ store;
1430
+ tables;
1431
+ constructor(store, schema, block, tx) {
1432
+ super(undefined, "memory", schema, block, tx, false);
1433
+ this.store = store;
1434
+ this.tables = schema;
1435
+ }
1436
+ async readRows(table, where, limit) {
1437
+ const rows = (this.store.tables.get(table) ?? []).filter((row) => Object.entries(where).every(([k, v]) => sameValue(row[k], v)));
1438
+ return limit === undefined ? rows : rows.slice(0, limit);
1439
+ }
1440
+ readDependent = null;
1441
+ dependsOnPrior(what) {
1442
+ this.readDependent ??= what;
1443
+ }
1444
+ findOne(table, where) {
1445
+ this.dependsOnPrior(`findOne("${table}")`);
1446
+ return super.findOne(table, where);
1447
+ }
1448
+ findMany(table, where) {
1449
+ this.dependsOnPrior(`findMany("${table}")`);
1450
+ return super.findMany(table, where);
1451
+ }
1452
+ update(table, where, set) {
1453
+ this.dependsOnPrior(`update("${table}")`);
1454
+ super.update(table, where, set);
1455
+ }
1456
+ delete(table, where) {
1457
+ this.dependsOnPrior(`delete("${table}")`);
1458
+ super.delete(table, where);
1354
1459
  }
1355
- if (typeof data === "object" && data !== null) {
1356
- const decoded = {};
1357
- for (const [key, value] of Object.entries(data)) {
1358
- decoded[key] = decodeEventData(value);
1359
- }
1360
- return decoded;
1460
+ increment(table, key, deltas) {
1461
+ this.dependsOnPrior(`increment("${table}")`);
1462
+ super.increment(table, key, deltas);
1361
1463
  }
1362
- return data;
1363
- }
1364
-
1365
- // src/runtime/print-validate.ts
1366
- var COLUMN_TYPES = new Set([
1367
- "text",
1368
- "uint",
1369
- "int",
1370
- "principal",
1371
- "boolean",
1372
- "timestamp",
1373
- "jsonb"
1374
- ]);
1375
- function isColumnType(field) {
1376
- return typeof field === "string" && COLUMN_TYPES.has(field);
1377
- }
1378
- function matchesField(field, value) {
1379
- if (isColumnType(field)) {
1380
- switch (field) {
1381
- case "uint":
1382
- case "int":
1383
- return typeof value === "bigint" || typeof value === "number";
1384
- case "boolean":
1385
- return typeof value === "boolean";
1386
- case "text":
1387
- case "principal":
1388
- case "timestamp":
1389
- return typeof value === "string" || typeof value === "number" || typeof value === "bigint";
1390
- case "jsonb":
1391
- return value !== undefined;
1464
+ upsert(table, key, row) {
1465
+ const def = this.tables[table];
1466
+ if (def) {
1467
+ const keyCols = Object.keys(key);
1468
+ const keyed = def.uniqueKeys?.some((uk) => uk.length === keyCols.length && uk.every((c) => keyCols.includes(c)));
1469
+ const omitted = Object.keys(def.columns).find((c) => !(c in key) && !(c in row));
1470
+ if (!keyed)
1471
+ this.dependsOnPrior(`upsert("${table}") without a unique key`);
1472
+ else if (omitted)
1473
+ this.dependsOnPrior(`upsert("${table}") leaves "${omitted}" as it was`);
1392
1474
  }
1475
+ super.upsert(table, key, row);
1393
1476
  }
1394
- if (typeof field !== "object" || field === null)
1395
- return false;
1396
- if ("optional" in field) {
1397
- return value === undefined || value === null || matchesField(field.type, value);
1477
+ async rowsOf(table) {
1478
+ return this.overlayMany(table, {}, await this.readRows(table, {}));
1398
1479
  }
1399
- if ("tuple" in field) {
1400
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1401
- return false;
1402
- }
1403
- return matchesTopic(field.tuple, value).ok;
1480
+ setReads(reads) {
1481
+ this._client = {
1482
+ contract(contractId, abi) {
1483
+ const camelToKebab = new Map;
1484
+ for (const fn of normalizeAbi2(abi).functions) {
1485
+ camelToKebab.set(toCamelCase2(fn.name), fn.name);
1486
+ }
1487
+ const read = new Proxy({}, {
1488
+ get(_target, prop) {
1489
+ const fnName = camelToKebab.get(prop) ?? prop;
1490
+ const key = `${contractId}.${fnName}`;
1491
+ return async () => {
1492
+ if (!(key in reads)) {
1493
+ throw new Error(`No stubbed chain read for "${key}" — pass it via createTestContext(schema, { reads: { "${key}": … } }).`);
1494
+ }
1495
+ return reads[key];
1496
+ };
1497
+ }
1498
+ });
1499
+ return { read };
1500
+ }
1501
+ };
1404
1502
  }
1405
- if ("list" in field) {
1406
- return Array.isArray(value) && value.every((item) => matchesField(field.list, item));
1503
+ async commitOps() {
1504
+ const tables = new Set;
1505
+ for (const op of this.ops)
1506
+ tables.add(op.table);
1507
+ for (const table of tables) {
1508
+ this.store.tables.set(table, await this.rowsOf(table));
1509
+ }
1510
+ this.ops.length = 0;
1407
1511
  }
1408
- return false;
1409
- }
1410
- function matchesTopic(declared, data) {
1411
- for (const [name, field] of Object.entries(declared)) {
1412
- const present = name in data;
1413
- const optional = typeof field === "object" && field !== null && "optional" in field;
1414
- if (!present) {
1415
- if (optional)
1512
+ insertsSince(checkpoint) {
1513
+ const out = [];
1514
+ for (const op of this.ops.slice(Math.max(0, checkpoint))) {
1515
+ if (op.kind !== "insert")
1416
1516
  continue;
1417
- return { ok: false, reason: `missing declared field "${name}"` };
1418
- }
1419
- if (!matchesField(field, data[name])) {
1420
- return {
1421
- ok: false,
1422
- reason: `field "${name}" does not match its declared type (got ${describe(data[name])})`
1423
- };
1517
+ const row = { ...op.data };
1518
+ out.push({
1519
+ table: op.table,
1520
+ keys: Object.keys(row).filter((k) => !k.startsWith("_")),
1521
+ row
1522
+ });
1424
1523
  }
1524
+ return out;
1425
1525
  }
1426
- return { ok: true };
1427
1526
  }
1428
- function describe(value) {
1429
- if (value === null)
1430
- return "null";
1431
- if (Array.isArray(value))
1432
- return "array";
1433
- return typeof value;
1434
- }
1435
- function validatePrintPayload(prints, topic, data) {
1436
- if (!prints)
1437
- return { ok: true };
1438
- const declared = prints[topic];
1439
- if (!declared)
1440
- return { ok: true };
1441
- return matchesTopic(declared, data);
1527
+ function sameValue(a, b) {
1528
+ if (a === b)
1529
+ return true;
1530
+ if ((typeof a === "bigint" || typeof a === "number") && (typeof b === "bigint" || typeof b === "number")) {
1531
+ return BigInt(a) === BigInt(b);
1532
+ }
1533
+ return String(a) === String(b);
1442
1534
  }
1443
1535
 
1444
- // src/runtime/realm.ts
1445
- import vm from "node:vm";
1536
+ // src/runtime/runner.ts
1537
+ import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
1538
+ import { logger as logger4 } from "@secondlayer/shared/logger";
1539
+ import {
1540
+ clarityValueToJS,
1541
+ deserializeCV as deserializeCV4,
1542
+ toCamelCase as toCamelCase3
1543
+ } from "@secondlayer/stacks/clarity";
1446
1544
 
1447
- // src/verification.ts
1448
- import { createHash as createHash2 } from "node:crypto";
1449
- // package.json
1450
- var package_default = {
1451
- name: "@secondlayer/subgraphs",
1452
- version: "7.2.0",
1453
- description: "Describe the Stacks data you want as one TypeScript file; it fills itself and survives reorgs.",
1454
- license: "MIT",
1455
- repository: {
1456
- type: "git",
1457
- url: "git+https://github.com/ryanwaits/secondlayer.git",
1458
- directory: "packages/subgraphs"
1459
- },
1460
- homepage: "https://github.com/ryanwaits/secondlayer/tree/main/packages/subgraphs#readme",
1461
- type: "module",
1462
- main: "./dist/index.js",
1463
- types: "./dist/index.d.ts",
1464
- exports: {
1465
- ".": {
1466
- types: "./dist/index.d.ts",
1467
- import: "./dist/index.js"
1468
- },
1469
- "./types": {
1470
- types: "./dist/types.d.ts",
1471
- import: "./dist/types.js"
1472
- },
1473
- "./schema": {
1474
- types: "./dist/schema/index.d.ts",
1475
- import: "./dist/schema/index.js"
1476
- },
1477
- "./validate": {
1478
- types: "./dist/validate.d.ts",
1479
- import: "./dist/validate.js"
1480
- },
1481
- "./verification": {
1482
- types: "./dist/verification.d.ts",
1483
- import: "./dist/verification.js"
1484
- },
1485
- "./runtime/replay": {
1486
- types: "./dist/runtime/replay.d.ts",
1487
- import: "./dist/runtime/replay.js"
1488
- },
1489
- "./runtime/emitter": {
1490
- types: "./dist/runtime/emitter.d.ts",
1491
- import: "./dist/runtime/emitter.js"
1492
- },
1493
- "./testing": {
1494
- types: "./dist/testing/index.d.ts",
1495
- import: "./dist/testing/index.js"
1496
- },
1497
- "./verify": {
1498
- types: "./dist/verify/index.d.ts",
1499
- import: "./dist/verify/index.js"
1545
+ // src/print-schema.ts
1546
+ import {
1547
+ deserializeCV as deserializeCV2
1548
+ } from "@secondlayer/stacks/clarity";
1549
+ function camelizeDataKey(str) {
1550
+ return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
1551
+ }
1552
+ function camelizeKeys(obj) {
1553
+ if (obj === null || obj === undefined)
1554
+ return obj;
1555
+ if (typeof obj !== "object")
1556
+ return obj;
1557
+ if (Array.isArray(obj))
1558
+ return obj.map(camelizeKeys);
1559
+ const result = {};
1560
+ for (const [k, v] of Object.entries(obj)) {
1561
+ result[camelizeDataKey(k)] = camelizeKeys(v);
1562
+ }
1563
+ return result;
1564
+ }
1565
+ function cvToTree(cv) {
1566
+ switch (cv.type) {
1567
+ case "uint":
1568
+ case "int":
1569
+ return { kind: cv.type };
1570
+ case "true":
1571
+ case "false":
1572
+ return { kind: "bool" };
1573
+ case "address":
1574
+ case "contract":
1575
+ return { kind: "principal" };
1576
+ case "buffer":
1577
+ return { kind: "buffer", len: cv.value.length / 2 };
1578
+ case "ascii":
1579
+ return { kind: "ascii", len: cv.value.length };
1580
+ case "utf8":
1581
+ return { kind: "utf8", len: new TextEncoder().encode(cv.value).length };
1582
+ case "none":
1583
+ return { kind: "optional", inner: null };
1584
+ case "some":
1585
+ return { kind: "optional", inner: cvToTree(cv.value) };
1586
+ case "ok":
1587
+ return { kind: "response", ok: cvToTree(cv.value), err: null };
1588
+ case "err":
1589
+ return { kind: "response", ok: null, err: cvToTree(cv.value) };
1590
+ case "list": {
1591
+ let inner = null;
1592
+ for (const el of cv.value) {
1593
+ const t = cvToTree(el);
1594
+ inner = inner ? unify(inner, t) : t;
1595
+ }
1596
+ return { kind: "list", inner };
1597
+ }
1598
+ case "tuple": {
1599
+ const entries = new Map;
1600
+ for (const [k, v] of Object.entries(cv.value)) {
1601
+ entries.set(k, { tree: cvToTree(v), present: 1 });
1602
+ }
1603
+ return { kind: "tuple", count: 1, entries };
1500
1604
  }
1501
- },
1502
- files: ["dist"],
1503
- scripts: {
1504
- build: "bunup",
1505
- dev: "bunup --watch",
1506
- test: "bun test",
1507
- typecheck: "tsc --noEmit",
1508
- openpkg: "bun scripts/sync-openpkg.ts",
1509
- prepublishOnly: "bun run build"
1510
- },
1511
- dependencies: {
1512
- "@secondlayer/shared": "^13.1.0",
1513
- "@secondlayer/stacks": "^6.4.0",
1514
- "@secondlayer/verify": "^0.6.0",
1515
- esbuild: "^0.28.2",
1516
- kysely: "0.28.17",
1517
- zod: "^4.3.6"
1518
- },
1519
- devDependencies: {
1520
- "@types/bun": "latest",
1521
- bunup: "0.16.32",
1522
- typescript: "^5"
1523
- },
1524
- publishConfig: {
1525
- access: "public"
1526
1605
  }
1606
+ }
1607
+ function unifyNullable(a, b) {
1608
+ if (!a)
1609
+ return b;
1610
+ if (!b)
1611
+ return a;
1612
+ return unify(a, b);
1613
+ }
1614
+ var UNION_KIND_ORDER = {
1615
+ uint: 0,
1616
+ int: 1,
1617
+ bool: 2,
1618
+ principal: 3,
1619
+ buffer: 4,
1620
+ ascii: 5,
1621
+ utf8: 6,
1622
+ list: 7,
1623
+ tuple: 8,
1624
+ response: 9
1527
1625
  };
1528
-
1529
- // src/verification.ts
1530
- var SUBGRAPHS_RUNTIME = `@secondlayer/subgraphs@${package_default.version}`;
1531
- var FORBIDDEN_GLOBALS = {
1532
- Date: "wall-clock time differs per run",
1533
- performance: "timers differ per run",
1534
- process: "host environment differs per run",
1535
- fetch: "network reads are not chain inputs",
1536
- crypto: "randomness differs per run",
1537
- setTimeout: "scheduling order differs per run",
1538
- setInterval: "scheduling order differs per run",
1539
- setImmediate: "scheduling order differs per run",
1540
- clearTimeout: "scheduling order differs per run",
1541
- clearInterval: "scheduling order differs per run",
1542
- queueMicrotask: "scheduling order differs per run",
1543
- WeakRef: "garbage collection timing differs per run",
1544
- FinalizationRegistry: "garbage collection timing differs per run",
1545
- Atomics: "shared memory is not single-threaded",
1546
- SharedArrayBuffer: "shared memory is not single-threaded",
1547
- Intl: "locale and time zone data differ per host",
1548
- Temporal: "locale and time zone data differ per host",
1549
- eval: "generated code hides inputs from the scan",
1550
- WebAssembly: "compiled code hides inputs from the scan",
1551
- ShadowRealm: "a nested realm escapes the allow-list",
1552
- require: "imports must be bundled into the handler"
1553
- };
1554
- var ALLOWED_GLOBALS = [
1555
- "undefined",
1556
- "NaN",
1557
- "Infinity",
1558
- "globalThis",
1559
- "isNaN",
1560
- "isFinite",
1561
- "parseInt",
1562
- "parseFloat",
1563
- "decodeURI",
1564
- "decodeURIComponent",
1565
- "encodeURI",
1566
- "encodeURIComponent",
1567
- "escape",
1568
- "unescape",
1569
- "Object",
1570
- "Function",
1571
- "Array",
1572
- "Number",
1573
- "Boolean",
1574
- "String",
1575
- "Symbol",
1576
- "BigInt",
1577
- "Math",
1578
- "JSON",
1579
- "Reflect",
1580
- "Proxy",
1581
- "Promise",
1582
- "Map",
1583
- "Set",
1584
- "WeakMap",
1585
- "WeakSet",
1586
- "RegExp",
1587
- "Iterator",
1588
- "DisposableStack",
1589
- "AsyncDisposableStack",
1590
- "Error",
1591
- "AggregateError",
1592
- "SuppressedError",
1593
- "EvalError",
1594
- "RangeError",
1595
- "ReferenceError",
1596
- "SyntaxError",
1597
- "TypeError",
1598
- "URIError",
1599
- "ArrayBuffer",
1600
- "DataView",
1601
- "Int8Array",
1602
- "Uint8Array",
1603
- "Uint8ClampedArray",
1604
- "Int16Array",
1605
- "Uint16Array",
1606
- "Int32Array",
1607
- "Uint32Array",
1608
- "Float16Array",
1609
- "Float32Array",
1610
- "Float64Array",
1611
- "BigInt64Array",
1612
- "BigUint64Array",
1613
- "TextEncoder",
1614
- "TextDecoder",
1615
- "atob",
1616
- "btoa",
1617
- "console"
1618
- ];
1619
- var ALLOWED_MATH = [
1620
- "abs",
1621
- "min",
1622
- "max",
1623
- "floor",
1624
- "ceil",
1625
- "trunc",
1626
- "sign",
1627
- "round",
1628
- "E",
1629
- "LN10",
1630
- "LN2",
1631
- "LOG10E",
1632
- "LOG2E",
1633
- "PI",
1634
- "SQRT1_2",
1635
- "SQRT2"
1636
- ];
1637
- var LOCALE_METHODS = [
1638
- "toLocaleString",
1639
- "toLocaleDateString",
1640
- "toLocaleTimeString",
1641
- "toLocaleUpperCase",
1642
- "toLocaleLowerCase",
1643
- "localeCompare"
1644
- ];
1645
- function formatFinding(f) {
1646
- return `${f.file}:${f.line}:${f.column} ${f.name}: ${f.reason}`;
1626
+ function makeUnion(members) {
1627
+ const sorted = [...members].sort((a, b) => (UNION_KIND_ORDER[a.kind] ?? 99) - (UNION_KIND_ORDER[b.kind] ?? 99));
1628
+ return { kind: "union", members: sorted };
1647
1629
  }
1648
- var STATE_SOURCE_TYPES = new Set([
1649
- "var_set",
1650
- "map_set",
1651
- "map_delete"
1652
- ]);
1653
- var LEVEL_RANK = { state: 0, events: 1, none: 2 };
1654
- var MAX_LISTED_FINDINGS = 10;
1655
- function deriveVerification(def, findings = []) {
1656
- let level = "state";
1657
- const raise = (to) => {
1658
- if (LEVEL_RANK[to] > LEVEL_RANK[level])
1659
- level = to;
1660
- };
1661
- const reasons = [];
1662
- for (const [name, source] of Object.entries(def.sources ?? {})) {
1663
- const filter = source;
1664
- if (filter.trait) {
1665
- raise("none");
1666
- reasons.push(`source "${name}": trait scope needs a proven contract registry`);
1667
- } else if (filter.type === "map_insert") {
1668
- raise("events");
1669
- reasons.push(`map_insert source "${name}": map_insert vs map_set needs event proofs`);
1670
- } else if (!STATE_SOURCE_TYPES.has(filter.type)) {
1671
- raise("events");
1672
- reasons.push(`${filter.type} source "${name}" needs event proofs`);
1673
- }
1630
+ function unify(a, b) {
1631
+ if (a.kind === "optional" || b.kind === "optional") {
1632
+ const ai = a.kind === "optional" ? a.inner : a;
1633
+ const bi = b.kind === "optional" ? b.inner : b;
1634
+ return { kind: "optional", inner: unifyNullable(ai, bi) };
1674
1635
  }
1675
- if (def.backfillMode === "concurrent") {
1676
- raise("none");
1677
- reasons.push("backfillMode concurrent: tip-first order is not chain order");
1636
+ if (a.kind === "union")
1637
+ return unionAdd(a.members, b);
1638
+ if (b.kind === "union")
1639
+ return unionAdd(b.members, a);
1640
+ if (a.kind !== b.kind)
1641
+ return makeUnion([a, b]);
1642
+ switch (a.kind) {
1643
+ case "uint":
1644
+ case "int":
1645
+ case "bool":
1646
+ case "principal":
1647
+ return a;
1648
+ case "buffer":
1649
+ case "ascii":
1650
+ case "utf8":
1651
+ return { kind: a.kind, len: Math.max(a.len, b.len) };
1652
+ case "list":
1653
+ return {
1654
+ kind: "list",
1655
+ inner: unifyNullable(a.inner, b.inner)
1656
+ };
1657
+ case "response": {
1658
+ const rb = b;
1659
+ return {
1660
+ kind: "response",
1661
+ ok: unifyNullable(a.ok, rb.ok),
1662
+ err: unifyNullable(a.err, rb.err)
1663
+ };
1664
+ }
1665
+ case "tuple": {
1666
+ const tb = b;
1667
+ const entries = new Map([...a.entries].map(([k, e]) => [k, { ...e }]));
1668
+ for (const [k, e] of tb.entries) {
1669
+ const existing = entries.get(k);
1670
+ entries.set(k, existing ? {
1671
+ tree: unify(existing.tree, e.tree),
1672
+ present: existing.present + e.present
1673
+ } : { ...e });
1674
+ }
1675
+ return { kind: "tuple", count: a.count + tb.count, entries };
1676
+ }
1678
1677
  }
1679
- if (findings.some((f) => f.kind === "needs-events"))
1680
- raise("events");
1681
- for (const f of findings.slice(0, MAX_LISTED_FINDINGS)) {
1682
- reasons.push(f.kind === "needs-events" ? `${formatFinding(f)} (needs event proofs)` : formatFinding(f));
1678
+ }
1679
+ function unionAdd(members, t) {
1680
+ if (t.kind === "union") {
1681
+ let acc = { kind: "union", members };
1682
+ for (const m of t.members)
1683
+ acc = unify(acc, m);
1684
+ return acc;
1683
1685
  }
1684
- if (findings.length > MAX_LISTED_FINDINGS) {
1685
- reasons.push(`…and ${findings.length - MAX_LISTED_FINDINGS} more handler findings`);
1686
+ const next = [...members];
1687
+ for (let i = 0;i < next.length; i++) {
1688
+ const member = next[i];
1689
+ if (!member)
1690
+ continue;
1691
+ const merged = unify(member, t);
1692
+ if (merged.kind !== "union") {
1693
+ next[i] = merged;
1694
+ return makeUnion(next);
1695
+ }
1686
1696
  }
1687
- return {
1688
- level,
1689
- reasons,
1690
- unproven: reasons.length === 0 ? [
1691
- "tx attribution of writes: event.tx, _tx_id (needs event proofs)",
1692
- "intermediate writes within a block (needs event proofs)"
1693
- ] : []
1694
- };
1695
- }
1696
- function isDeterminismViolation(v) {
1697
- return v.level === "state" && v.reasons.length > 0;
1698
- }
1699
- function sha256(input) {
1700
- return createHash2("sha256").update(input).digest("hex");
1701
- }
1702
- function pinPreimage(input) {
1703
- const fields = {
1704
- handlerHash: sha256(input.handlerCode),
1705
- network: input.network,
1706
- runtime: input.runtime ?? SUBGRAPHS_RUNTIME,
1707
- schemaHash: input.schemaHash,
1708
- startBlock: input.startBlock ?? null
1709
- };
1710
- return JSON.stringify(fields);
1711
- }
1712
- function computePin(input) {
1713
- return sha256(pinPreimage(input));
1714
- }
1715
-
1716
- // src/runtime/realm.ts
1717
- class NondeterminismError extends Error {
1718
- name = "NondeterminismError";
1719
- }
1720
-
1721
- class HandlerReadError extends Error {
1722
- name = "HandlerReadError";
1723
- }
1724
- function abortsBlock(err) {
1725
- return err instanceof NondeterminismError || err instanceof HandlerReadError;
1697
+ next.push(t);
1698
+ return makeUnion(next);
1726
1699
  }
1727
- var HOST_GLOBALS = [
1728
- "ArrayBuffer",
1729
- "DataView",
1730
- "Int8Array",
1731
- "Uint8Array",
1732
- "Uint8ClampedArray",
1733
- "Int16Array",
1734
- "Uint16Array",
1735
- "Int32Array",
1736
- "Uint32Array",
1737
- "Float16Array",
1738
- "Float32Array",
1739
- "Float64Array",
1740
- "BigInt64Array",
1741
- "BigUint64Array",
1742
- "TextEncoder",
1743
- "TextDecoder",
1744
- "atob",
1745
- "btoa"
1746
- ];
1747
- function forbid(name, reason) {
1748
- return () => {
1749
- throw new NondeterminismError(`${name}: ${reason}`);
1750
- };
1700
+ function wrapOptional(t) {
1701
+ return t.kind === "optional" ? t : { kind: "optional", inner: t };
1751
1702
  }
1752
- function createRealm() {
1753
- const host = globalThis;
1754
- const sandbox = Object.create(null);
1755
- for (const name of HOST_GLOBALS) {
1756
- if (host[name] !== undefined)
1757
- sandbox[name] = host[name];
1703
+ function renderClarity(t) {
1704
+ if (!t)
1705
+ return "?";
1706
+ switch (t.kind) {
1707
+ case "uint":
1708
+ case "int":
1709
+ case "bool":
1710
+ case "principal":
1711
+ return t.kind;
1712
+ case "buffer":
1713
+ return `(buff ${t.len})`;
1714
+ case "ascii":
1715
+ return `(string-ascii ${t.len})`;
1716
+ case "utf8":
1717
+ return `(string-utf8 ${t.len})`;
1718
+ case "optional":
1719
+ return `(optional ${renderClarity(t.inner)})`;
1720
+ case "list":
1721
+ return `(list ${renderClarity(t.inner)})`;
1722
+ case "response":
1723
+ return `(response ${renderClarity(t.ok)} ${renderClarity(t.err)})`;
1724
+ case "tuple": {
1725
+ const parts = [...t.entries].map(([k, e]) => {
1726
+ const tree = e.present < t.count ? wrapOptional(e.tree) : e.tree;
1727
+ return `(${k} ${renderClarity(tree)})`;
1728
+ });
1729
+ return `(tuple ${parts.join(" ")})`;
1730
+ }
1731
+ case "union":
1732
+ return t.members.map(renderClarity).join(" | ");
1758
1733
  }
1759
- const context = vm.createContext(sandbox, {
1760
- name: "subgraph-handler",
1761
- codeGeneration: { strings: false, wasm: false }
1762
- });
1763
- const g = vm.runInContext("globalThis", context);
1764
- const allowed = new Set(ALLOWED_GLOBALS);
1765
- for (const name of Object.getOwnPropertyNames(g)) {
1766
- if (!allowed.has(name))
1767
- delete g[name];
1734
+ }
1735
+ function renderTs(t) {
1736
+ if (!t)
1737
+ return "unknown";
1738
+ switch (t.kind) {
1739
+ case "uint":
1740
+ case "int":
1741
+ return "bigint";
1742
+ case "bool":
1743
+ return "boolean";
1744
+ case "principal":
1745
+ case "buffer":
1746
+ case "ascii":
1747
+ case "utf8":
1748
+ return "string";
1749
+ case "optional":
1750
+ return t.inner ? `${renderTs(t.inner)} | null` : "unknown | null";
1751
+ case "list": {
1752
+ const inner = renderTs(t.inner);
1753
+ return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
1754
+ }
1755
+ case "response": {
1756
+ const sides = [...new Set([renderTs(t.ok), renderTs(t.err)])];
1757
+ return sides.join(" | ");
1758
+ }
1759
+ case "tuple": {
1760
+ const parts = [...t.entries].map(([k, e]) => {
1761
+ const opt = e.present < t.count ? "?" : "";
1762
+ return `${camelizeDataKey(k)}${opt}: ${renderTs(e.tree)}`;
1763
+ });
1764
+ return `{ ${parts.join("; ")} }`;
1765
+ }
1766
+ case "union":
1767
+ return [...new Set(t.members.map((m) => renderTs(m)))].join(" | ");
1768
1768
  }
1769
- for (const [name, reason] of Object.entries(FORBIDDEN_GLOBALS)) {
1770
- Object.defineProperty(g, name, { get: forbid(name, reason) });
1769
+ }
1770
+ function toColumnType(t) {
1771
+ if (!t)
1772
+ return "jsonb";
1773
+ switch (t.kind) {
1774
+ case "uint":
1775
+ return "uint";
1776
+ case "int":
1777
+ return "int";
1778
+ case "bool":
1779
+ return "boolean";
1780
+ case "principal":
1781
+ return "principal";
1782
+ case "buffer":
1783
+ case "ascii":
1784
+ case "utf8":
1785
+ return "text";
1786
+ case "list":
1787
+ case "tuple":
1788
+ return "jsonb";
1789
+ case "optional":
1790
+ return toColumnType(t.inner);
1791
+ case "response":
1792
+ return t.ok ? toColumnType(t.ok) : "jsonb";
1793
+ case "union":
1794
+ return "jsonb";
1771
1795
  }
1772
- const realmMath = g.Math;
1773
- const safeMath = {};
1774
- const allowedMath = new Set(ALLOWED_MATH);
1775
- for (const key of Object.getOwnPropertyNames(realmMath)) {
1776
- if (allowedMath.has(key))
1777
- safeMath[key] = realmMath[key];
1796
+ }
1797
+ var MAX_DECODED_PER_TOPIC_NEWEST = 75;
1798
+ var MAX_DECODED_PER_TOPIC_OLDEST = 25;
1799
+ function inferPrintTopics(samples) {
1800
+ const groups = new Map;
1801
+ for (const s of samples) {
1802
+ const group = groups.get(s.topic);
1803
+ if (group)
1804
+ group.push(s);
1778
1805
  else
1779
- Object.defineProperty(safeMath, key, {
1780
- get: forbid(`Math.${key}`, key === "random" ? "randomness differs per run" : "result is engine-approximated")
1781
- });
1806
+ groups.set(s.topic, [s]);
1782
1807
  }
1783
- Object.defineProperty(g, "Math", { value: Object.freeze(safeMath) });
1784
- for (const ctor of ["String", "Number", "BigInt", "Array", "Object"]) {
1785
- const proto = g[ctor].prototype;
1786
- for (const method of LOCALE_METHODS) {
1787
- if (Object.prototype.hasOwnProperty.call(proto, method)) {
1788
- Object.defineProperty(proto, method, {
1789
- value: forbid(method, "output depends on the host's locale data")
1790
- });
1808
+ const out = [];
1809
+ for (const [topic, rows] of groups) {
1810
+ let first = Number.POSITIVE_INFINITY;
1811
+ let last = Number.NEGATIVE_INFINITY;
1812
+ for (const r of rows) {
1813
+ if (r.blockHeight < first)
1814
+ first = r.blockHeight;
1815
+ if (r.blockHeight > last)
1816
+ last = r.blockHeight;
1817
+ }
1818
+ const withHex = [...rows].filter((r) => r.rawHex !== null).sort((a, b) => b.blockHeight - a.blockHeight);
1819
+ const budget = MAX_DECODED_PER_TOPIC_NEWEST + MAX_DECODED_PER_TOPIC_OLDEST;
1820
+ const picked = withHex.length <= budget ? withHex : [
1821
+ ...withHex.slice(0, MAX_DECODED_PER_TOPIC_NEWEST),
1822
+ ...withHex.slice(-MAX_DECODED_PER_TOPIC_OLDEST)
1823
+ ];
1824
+ const tuples = [];
1825
+ let decoded = 0;
1826
+ for (const p of picked) {
1827
+ try {
1828
+ const cv = deserializeCV2(p.rawHex);
1829
+ decoded++;
1830
+ if (cv.type === "tuple")
1831
+ tuples.push(cv);
1832
+ } catch {}
1833
+ }
1834
+ const nonTuple = decoded > 0 && tuples.length === 0;
1835
+ const fields = [];
1836
+ if (!nonTuple) {
1837
+ const stats = new Map;
1838
+ for (const t of tuples) {
1839
+ for (const [key, value] of Object.entries(t.value)) {
1840
+ if (key === "topic")
1841
+ continue;
1842
+ const tree = cvToTree(value);
1843
+ const existing = stats.get(key);
1844
+ if (existing) {
1845
+ existing.present++;
1846
+ existing.tree = unify(existing.tree, tree);
1847
+ if (value.type === "none")
1848
+ existing.noneCount++;
1849
+ if (value.type === "none" || value.type === "some") {
1850
+ existing.optionalSeen = true;
1851
+ }
1852
+ } else {
1853
+ stats.set(key, {
1854
+ tree,
1855
+ present: 1,
1856
+ noneCount: value.type === "none" ? 1 : 0,
1857
+ optionalSeen: value.type === "none" || value.type === "some"
1858
+ });
1859
+ }
1860
+ }
1861
+ }
1862
+ for (const [name, st] of [...stats].sort(([a], [b]) => a.localeCompare(b))) {
1863
+ const field = {
1864
+ name,
1865
+ camel_name: camelizeDataKey(name),
1866
+ clarity_type: renderClarity(st.tree),
1867
+ ts_type: renderTs(st.tree),
1868
+ column_type: toColumnType(st.tree),
1869
+ always_present: st.present === tuples.length
1870
+ };
1871
+ if (st.optionalSeen) {
1872
+ field.optional_some_rate = (st.present - st.noneCount) / st.present;
1873
+ }
1874
+ fields.push(field);
1791
1875
  }
1792
1876
  }
1877
+ out.push({
1878
+ topic,
1879
+ count: rows.length,
1880
+ first_height: first,
1881
+ last_height: last,
1882
+ non_tuple: nonTuple,
1883
+ fields
1884
+ });
1793
1885
  }
1794
- Object.preventExtensions(g);
1795
- return context;
1886
+ return out.sort((a, b) => b.count - a.count);
1796
1887
  }
1797
- function checkRowValues(where, values) {
1798
- for (const [col, value] of Object.entries(values)) {
1799
- checkValue(value, `${where}.${col}`);
1888
+
1889
+ // src/runtime/clarity.ts
1890
+ import { cvToValue, deserializeCV as deserializeCV3 } from "@secondlayer/stacks/clarity";
1891
+ function decodeClarityValue(hex) {
1892
+ try {
1893
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
1894
+ const cv = deserializeCV3(cleanHex);
1895
+ return cvToValue(cv);
1896
+ } catch {
1897
+ return hex;
1800
1898
  }
1801
1899
  }
1802
- function checkValue(value, path) {
1803
- switch (typeof value) {
1804
- case "number":
1805
- if (!Number.isSafeInteger(value)) {
1806
- throw new TypeError(`${path}: ${value} is not a safe integer; verifiable subgraphs store bigint or integers only`);
1807
- }
1808
- return;
1809
- case "function":
1810
- case "symbol":
1811
- throw new TypeError(`${path}: a ${typeof value} is not a row value`);
1812
- case "object":
1813
- if (value === null || ArrayBuffer.isView(value))
1814
- return;
1815
- if (Array.isArray(value)) {
1816
- value.forEach((v, i) => checkValue(v, `${path}[${i}]`));
1817
- return;
1818
- }
1819
- for (const [k, v] of Object.entries(value))
1820
- checkValue(v, `${path}.${k}`);
1821
- return;
1822
- default:
1823
- return;
1900
+ function decodeEventData(data) {
1901
+ if (typeof data === "string" && data.startsWith("0x") && data.length > 10) {
1902
+ return decodeClarityValue(data);
1903
+ }
1904
+ if (Array.isArray(data)) {
1905
+ return data.map(decodeEventData);
1906
+ }
1907
+ if (typeof data === "object" && data !== null) {
1908
+ const decoded = {};
1909
+ for (const [key, value] of Object.entries(data)) {
1910
+ decoded[key] = decodeEventData(value);
1911
+ }
1912
+ return decoded;
1824
1913
  }
1914
+ return data;
1825
1915
  }
1826
- function deterministicContext(ctx, schema) {
1827
- let pendingReads = 0;
1828
- const knownTable = (table) => {
1829
- if (!Object.prototype.hasOwnProperty.call(schema, table)) {
1830
- throw new Error(`Table "${table}" not found in subgraph schema`);
1916
+
1917
+ // src/runtime/print-validate.ts
1918
+ var COLUMN_TYPES = new Set([
1919
+ "text",
1920
+ "uint",
1921
+ "int",
1922
+ "principal",
1923
+ "boolean",
1924
+ "timestamp",
1925
+ "jsonb"
1926
+ ]);
1927
+ function isColumnType(field) {
1928
+ return typeof field === "string" && COLUMN_TYPES.has(field);
1929
+ }
1930
+ function matchesField(field, value) {
1931
+ if (isColumnType(field)) {
1932
+ switch (field) {
1933
+ case "uint":
1934
+ case "int":
1935
+ return typeof value === "bigint" || typeof value === "number";
1936
+ case "boolean":
1937
+ return typeof value === "boolean";
1938
+ case "text":
1939
+ case "principal":
1940
+ case "timestamp":
1941
+ return typeof value === "string" || typeof value === "number" || typeof value === "bigint";
1942
+ case "jsonb":
1943
+ return value !== undefined;
1831
1944
  }
1832
- };
1833
- const guard = (op) => {
1834
- if (pendingReads > 0) {
1835
- throw new NondeterminismError(`ctx.${op} while a ctx read is pending: concurrent ctx calls order writes by database latency; await each read before the next call`);
1945
+ }
1946
+ if (typeof field !== "object" || field === null)
1947
+ return false;
1948
+ if ("optional" in field) {
1949
+ return value === undefined || value === null || matchesField(field.type, value);
1950
+ }
1951
+ if ("tuple" in field) {
1952
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1953
+ return false;
1836
1954
  }
1837
- };
1838
- const read = async (op, run) => {
1839
- guard(op);
1840
- pendingReads++;
1841
- try {
1842
- return await run();
1843
- } catch (err) {
1844
- if (err instanceof NondeterminismError)
1845
- throw err;
1846
- throw new HandlerReadError(`ctx.${op} failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
1847
- } finally {
1848
- pendingReads--;
1955
+ return matchesTopic(field.tuple, value).ok;
1956
+ }
1957
+ if ("list" in field) {
1958
+ return Array.isArray(value) && value.every((item) => matchesField(field.list, item));
1959
+ }
1960
+ return false;
1961
+ }
1962
+ function matchesTopic(declared, data) {
1963
+ for (const [name, field] of Object.entries(declared)) {
1964
+ const present = name in data;
1965
+ const optional = typeof field === "object" && field !== null && "optional" in field;
1966
+ if (!present) {
1967
+ if (optional)
1968
+ continue;
1969
+ return { ok: false, reason: `missing declared field "${name}"` };
1849
1970
  }
1850
- };
1851
- return {
1852
- get block() {
1853
- return ctx.block;
1854
- },
1855
- get tx() {
1856
- return ctx.tx;
1857
- },
1858
- get client() {
1859
- throw new NondeterminismError("ctx.client: contract reads need re-execution proofs");
1860
- },
1861
- insert(table, row) {
1862
- guard("insert");
1863
- checkRowValues(`insert ${table}`, row);
1864
- ctx.insert(table, row);
1865
- },
1866
- update(table, where, set) {
1867
- guard("update");
1868
- checkRowValues(`update ${table}`, where);
1869
- checkRowValues(`update ${table}`, set);
1870
- ctx.update(table, where, set);
1871
- },
1872
- upsert(table, key, row) {
1873
- guard("upsert");
1874
- checkRowValues(`upsert ${table}`, key);
1875
- checkRowValues(`upsert ${table}`, row);
1876
- ctx.upsert(table, key, row);
1877
- },
1878
- delete(table, where) {
1879
- guard("delete");
1880
- ctx.delete(table, where);
1881
- },
1882
- increment(table, key, deltas) {
1883
- guard("increment");
1884
- checkRowValues(`increment ${table}`, key);
1885
- checkRowValues(`increment ${table}`, deltas);
1886
- ctx.increment(table, key, deltas);
1887
- },
1888
- findOne: (table, where) => {
1889
- knownTable(table);
1890
- return read("findOne", () => ctx.findOne(table, where));
1891
- },
1892
- findMany: (table, ...rest) => {
1893
- knownTable(table);
1894
- return read("findMany", () => ctx.findMany(table, ...rest));
1971
+ if (!matchesField(field, data[name])) {
1972
+ return {
1973
+ ok: false,
1974
+ reason: `field "${name}" does not match its declared type (got ${describe(data[name])})`
1975
+ };
1895
1976
  }
1896
- };
1897
- }
1898
- var realmDefinitions = new WeakSet;
1899
- async function loadDeterministicDefinition(handlerCode) {
1900
- const { transform } = await import("esbuild");
1901
- const { code } = await transform(handlerCode, {
1902
- format: "cjs",
1903
- loader: "js",
1904
- logLevel: "silent"
1905
- });
1906
- const context = createRealm();
1907
- const evaluate = vm.runInContext(`(function (module, exports) {
1908
- ${code}
1909
- })`, context, { filename: "handler.js" });
1910
- const mod = { exports: {} };
1911
- evaluate(mod, mod.exports);
1912
- const def = mod.exports.default ?? mod.exports;
1913
- const sources = structuredClone(def.sources);
1914
- const schema = structuredClone(def.schema);
1915
- const facades = new WeakMap;
1916
- const handlers = {};
1917
- for (const [name, handler] of Object.entries(def.handlers ?? {})) {
1918
- handlers[name] = (event, ctx) => {
1919
- let facade = facades.get(ctx);
1920
- if (!facade) {
1921
- facade = deterministicContext(ctx, schema);
1922
- facades.set(ctx, facade);
1923
- }
1924
- return handler(event, facade);
1925
- };
1926
1977
  }
1927
- const loaded = { ...def, sources, schema, handlers };
1928
- realmDefinitions.add(loaded);
1929
- return loaded;
1978
+ return { ok: true };
1979
+ }
1980
+ function describe(value) {
1981
+ if (value === null)
1982
+ return "null";
1983
+ if (Array.isArray(value))
1984
+ return "array";
1985
+ return typeof value;
1986
+ }
1987
+ function validatePrintPayload(prints, topic, data) {
1988
+ if (!prints)
1989
+ return { ok: true };
1990
+ const declared = prints[topic];
1991
+ if (!declared)
1992
+ return { ok: true };
1993
+ return matchesTopic(declared, data);
1930
1994
  }
1931
1995
 
1932
1996
  // src/runtime/source-matcher.ts
@@ -2770,7 +2834,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
2770
2834
  sourceName,
2771
2835
  txId: tx.tx_id,
2772
2836
  ...event !== null ? { eventId: event.id, eventType: event.type } : {},
2773
- error: getErrorMessage(err)
2837
+ error: getErrorMessage2(err)
2774
2838
  });
2775
2839
  }
2776
2840
  }
@@ -3142,5 +3206,5 @@ export {
3142
3206
  toHandlerPayload
3143
3207
  };
3144
3208
 
3145
- //# debugId=6B86A4A41B6DB6DF64756E2164756E21
3209
+ //# debugId=9370182C1798E38564756E2164756E21
3146
3210
  //# sourceMappingURL=index.js.map