@secondlayer/subgraphs 7.1.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +16 -2
- package/dist/index.js +198 -128
- package/dist/index.js.map +7 -7
- package/dist/runtime/replay.js +279 -26
- package/dist/runtime/replay.js.map +10 -8
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.js +6 -4
- package/dist/schema/index.js.map +3 -3
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +1738 -1631
- package/dist/testing/index.js.map +9 -9
- package/dist/types.d.ts +6 -0
- package/dist/validate.d.ts +6 -0
- package/dist/validate.js +3 -1
- package/dist/validate.js.map +3 -3
- package/dist/verification.d.ts +6 -0
- package/dist/verification.js +4 -4
- package/dist/verification.js.map +1 -1
- package/dist/verify/index.d.ts +6 -0
- package/dist/verify/index.js +138 -41
- package/dist/verify/index.js.map +6 -6
- package/package.json +3 -3
package/dist/testing/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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.3.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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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.2.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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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`);
|
|
416
427
|
}
|
|
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
428
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
429
|
+
if (def.backfillMode === "concurrent") {
|
|
430
|
+
raise("none");
|
|
431
|
+
reasons.push("backfillMode concurrent: tip-first order is not chain order");
|
|
452
432
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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;
|
|
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));
|
|
463
437
|
}
|
|
464
|
-
|
|
465
|
-
|
|
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;
|
|
438
|
+
if (findings.length > MAX_LISTED_FINDINGS) {
|
|
439
|
+
reasons.push(`…and ${findings.length - MAX_LISTED_FINDINGS} more handler findings`);
|
|
493
440
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function isDeterminismViolation(v) {
|
|
451
|
+
return v.level === "state" && v.reasons.length > 0;
|
|
452
|
+
}
|
|
453
|
+
function sha256(input) {
|
|
454
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
455
|
+
}
|
|
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);
|
|
465
|
+
}
|
|
466
|
+
function computePin(input) {
|
|
467
|
+
return sha256(pinPreimage(input));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/runtime/realm.ts
|
|
471
|
+
class NondeterminismError extends Error {
|
|
472
|
+
name = "NondeterminismError";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
class HandlerReadError extends Error {
|
|
476
|
+
name = "HandlerReadError";
|
|
477
|
+
}
|
|
478
|
+
function abortsBlock(err) {
|
|
479
|
+
return err instanceof NondeterminismError || err instanceof HandlerReadError;
|
|
480
|
+
}
|
|
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
|
+
};
|
|
505
|
+
}
|
|
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];
|
|
537
512
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
return result;
|
|
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];
|
|
549
522
|
}
|
|
550
|
-
|
|
551
|
-
|
|
523
|
+
for (const [name, reason] of Object.entries(FORBIDDEN_GLOBALS)) {
|
|
524
|
+
Object.defineProperty(g, name, { get: forbid(name, reason) });
|
|
552
525
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
} else {
|
|
563
|
-
await this.db.transaction().execute(async (tx) => {
|
|
564
|
-
await this.execStatements(tx, statements);
|
|
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")
|
|
565
535
|
});
|
|
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
536
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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`);
|
|
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
|
+
});
|
|
599
545
|
}
|
|
600
546
|
}
|
|
601
547
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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 };
|
|
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}`);
|
|
618
554
|
}
|
|
619
|
-
|
|
620
|
-
|
|
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
|
+
}
|
|
621
562
|
return;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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:
|
|
625
577
|
return;
|
|
578
|
+
}
|
|
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`);
|
|
626
585
|
}
|
|
627
|
-
|
|
628
|
-
|
|
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`);
|
|
629
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);
|
|
677
|
+
}
|
|
678
|
+
return handler(event, facade);
|
|
679
|
+
};
|
|
630
680
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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);
|
|
647
701
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
});
|
|
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.");
|
|
705
|
+
}
|
|
706
|
+
return { kind: "hosted", apiUrl, apiKey };
|
|
707
|
+
}
|
|
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}`);
|
|
713
|
+
}
|
|
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}` }
|
|
686
734
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
735
|
+
})
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
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
|
+
}
|
|
758
|
+
}
|
|
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.`);
|
|
773
|
+
}
|
|
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;
|
|
778
|
+
}
|
|
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'
|
|
710
808
|
});
|
|
711
809
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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`;
|
|
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;
|
|
729
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();
|
|
730
831
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
}
|
|
832
|
+
} catch (err) {
|
|
833
|
+
if (isChainAnswer(err) || err instanceof ChainReadError || err instanceof HandlerReadError) {
|
|
834
|
+
throw err;
|
|
797
835
|
}
|
|
836
|
+
throw new HandlerReadError(`Chain read ${contractId}::${fn.name} at block ${ctx.blockHeight} failed: ${getErrorMessage(err)}`, { cause: err });
|
|
798
837
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
throw new
|
|
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);
|
|
806
845
|
}
|
|
807
|
-
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
return false;
|
|
826
|
-
return String(a) === String(b);
|
|
827
|
-
}
|
|
828
|
-
function rowMatches(row, where) {
|
|
829
|
-
return Object.entries(where).every(([k, v]) => valEq(row[k], v));
|
|
830
|
-
}
|
|
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
|
-
}
|
|
841
|
-
}
|
|
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;
|
|
848
|
-
}
|
|
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, "''")}'`;
|
|
859
|
-
}
|
|
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: [] };
|
|
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);
|
|
854
|
+
}
|
|
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
|
+
};
|
|
868
864
|
}
|
|
869
865
|
|
|
870
|
-
// src/runtime/
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
+
}
|
|
874
872
|
}
|
|
875
873
|
|
|
876
|
-
class
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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;
|
|
883
890
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
891
|
+
get client() {
|
|
892
|
+
this._client ??= createChainReadClient({
|
|
893
|
+
blockHeight: this.block.height,
|
|
894
|
+
indexBlockHash: this.block.indexBlockHash
|
|
895
|
+
});
|
|
896
|
+
return this._client;
|
|
887
897
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
this.readDependent ??= what;
|
|
898
|
+
get tx() {
|
|
899
|
+
return this._tx;
|
|
891
900
|
}
|
|
892
|
-
|
|
893
|
-
this.
|
|
894
|
-
return super.findOne(table, where);
|
|
901
|
+
setTx(tx) {
|
|
902
|
+
this._tx = tx;
|
|
895
903
|
}
|
|
896
|
-
|
|
897
|
-
this.
|
|
898
|
-
|
|
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
|
+
});
|
|
899
911
|
}
|
|
900
912
|
update(table, where, set) {
|
|
901
|
-
this.
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
delete(table, where) {
|
|
905
|
-
this.dependsOnPrior(`delete("${table}")`);
|
|
906
|
-
super.delete(table, where);
|
|
907
|
-
}
|
|
908
|
-
increment(table, key, deltas) {
|
|
909
|
-
this.dependsOnPrior(`increment("${table}")`);
|
|
910
|
-
super.increment(table, key, deltas);
|
|
913
|
+
this.validateTable(table);
|
|
914
|
+
this.ops.push({ kind: "update", table, data: where, set });
|
|
911
915
|
}
|
|
912
916
|
upsert(table, key, row) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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 };
|
|
948
|
-
}
|
|
949
|
-
};
|
|
950
|
-
}
|
|
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;
|
|
959
|
-
}
|
|
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
|
|
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
|
+
}
|
|
970
945
|
});
|
|
971
946
|
}
|
|
972
|
-
return out;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
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);
|
|
980
947
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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());
|
|
999
|
-
}
|
|
1000
|
-
function camelizeKeys(obj) {
|
|
1001
|
-
if (obj === null || obj === undefined)
|
|
1002
|
-
return obj;
|
|
1003
|
-
if (typeof obj !== "object")
|
|
1004
|
-
return obj;
|
|
1005
|
-
if (Array.isArray(obj))
|
|
1006
|
-
return obj.map(camelizeKeys);
|
|
1007
|
-
const result = {};
|
|
1008
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
1009
|
-
result[camelizeDataKey(k)] = camelizeKeys(v);
|
|
948
|
+
delete(table, where) {
|
|
949
|
+
this.validateTable(table);
|
|
950
|
+
this.ops.push({ kind: "delete", table, data: where });
|
|
1010
951
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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;
|
|
1043
|
-
}
|
|
1044
|
-
return { kind: "list", inner };
|
|
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(", ")}]`);
|
|
1045
959
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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`);
|
|
1050
967
|
}
|
|
1051
|
-
return { kind: "tuple", count: 1, entries };
|
|
1052
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
|
+
});
|
|
1053
980
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
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) };
|
|
981
|
+
opsCheckpoint() {
|
|
982
|
+
return this.ops.length;
|
|
1083
983
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
};
|
|
1112
|
-
}
|
|
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 });
|
|
1122
|
-
}
|
|
1123
|
-
return { kind: "tuple", count: a.count + tb.count, entries };
|
|
1124
|
-
}
|
|
984
|
+
rollbackTo(checkpoint) {
|
|
985
|
+
if (checkpoint < 0 || checkpoint > this.ops.length)
|
|
986
|
+
return;
|
|
987
|
+
this.ops.length = checkpoint;
|
|
1125
988
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
return acc;
|
|
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));
|
|
1133
995
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
continue;
|
|
1139
|
-
const merged = unify(member, t);
|
|
1140
|
-
if (merged.kind !== "union") {
|
|
1141
|
-
next[i] = merged;
|
|
1142
|
-
return makeUnion(next);
|
|
1143
|
-
}
|
|
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);
|
|
1144
1000
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
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(" ")})`;
|
|
1178
|
-
}
|
|
1179
|
-
case "union":
|
|
1180
|
-
return t.members.map(renderClarity).join(" | ");
|
|
1001
|
+
async findMany(table, where) {
|
|
1002
|
+
this.validateTable(table);
|
|
1003
|
+
return this.overlayMany(table, where, await this.readRows(table, where));
|
|
1181
1004
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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(" | ");
|
|
1206
|
-
}
|
|
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)}`;
|
|
1211
|
-
});
|
|
1212
|
-
return `{ ${parts.join("; ")} }`;
|
|
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);
|
|
1213
1013
|
}
|
|
1214
|
-
|
|
1215
|
-
return [...new Set(t.members.map((m) => renderTs(m)))].join(" | ");
|
|
1216
|
-
}
|
|
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";
|
|
1243
|
-
}
|
|
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]);
|
|
1014
|
+
return row;
|
|
1255
1015
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
let
|
|
1260
|
-
for (const
|
|
1261
|
-
if (
|
|
1262
|
-
|
|
1263
|
-
if (
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
+
}
|
|
1281
1043
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
if (value.type === "none")
|
|
1296
|
-
existing.noneCount++;
|
|
1297
|
-
if (value.type === "none" || value.type === "some") {
|
|
1298
|
-
existing.optionalSeen = true;
|
|
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;
|
|
1299
1057
|
}
|
|
1300
|
-
|
|
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
|
-
});
|
|
1058
|
+
return merged;
|
|
1307
1059
|
}
|
|
1060
|
+
return row;
|
|
1308
1061
|
}
|
|
1062
|
+
return rowMatches(clean, where) ? { ...clean } : null;
|
|
1309
1063
|
}
|
|
1310
|
-
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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;
|
|
1321
1075
|
}
|
|
1322
|
-
|
|
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;
|
|
1323
1083
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
last_height: last,
|
|
1330
|
-
non_tuple: nonTuple,
|
|
1331
|
-
fields
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
return out.sort((a, b) => b.count - a.count);
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// src/runtime/clarity.ts
|
|
1338
|
-
import { cvToValue, deserializeCV as deserializeCV3 } from "@secondlayer/stacks/clarity";
|
|
1339
|
-
function decodeClarityValue(hex) {
|
|
1340
|
-
try {
|
|
1341
|
-
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1342
|
-
const cv = deserializeCV3(cleanHex);
|
|
1343
|
-
return cvToValue(cv);
|
|
1344
|
-
} catch {
|
|
1345
|
-
return hex;
|
|
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;
|
|
1088
|
+
}
|
|
1346
1089
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return result;
|
|
1351
1101
|
}
|
|
1352
|
-
|
|
1353
|
-
return
|
|
1102
|
+
get pendingOps() {
|
|
1103
|
+
return this.ops.length;
|
|
1354
1104
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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);
|
|
1117
|
+
});
|
|
1359
1118
|
}
|
|
1360
|
-
|
|
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 };
|
|
1361
1134
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1378
|
-
|
|
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;
|
|
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
|
+
}
|
|
1392
1152
|
}
|
|
1393
1153
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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 };
|
|
1398
1170
|
}
|
|
1399
|
-
|
|
1400
|
-
if (
|
|
1401
|
-
return
|
|
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;
|
|
1178
|
+
}
|
|
1179
|
+
for (const stmt of emitJournalDDL(this.pgSchemaName)) {
|
|
1180
|
+
await sql.raw(stmt).execute(this.db);
|
|
1402
1181
|
}
|
|
1403
|
-
return matchesTopic(field.tuple, value).ok;
|
|
1404
1182
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1183
|
+
columnSqlType(table, col) {
|
|
1184
|
+
const def = this.subgraphSchema[table]?.columns?.[col];
|
|
1185
|
+
return def ? TYPE_MAP[def.type] : undefined;
|
|
1407
1186
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
-
};
|
|
1424
|
-
}
|
|
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}`;
|
|
1425
1196
|
}
|
|
1426
|
-
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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]);
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
batch.cols.forEach((c, ci) => {
|
|
1260
|
+
if (!isSystemColumn(c))
|
|
1261
|
+
first[ci] = row[ci];
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
rows = [...byKey.values()];
|
|
1265
|
+
}
|
|
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}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1500
1350
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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.0.0",
|
|
1513
|
-
"@secondlayer/stacks": "^6.3.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"
|
|
1351
|
+
flushInsertBatch();
|
|
1352
|
+
flushIncrementBatch();
|
|
1353
|
+
return statements;
|
|
1526
1354
|
}
|
|
1527
|
-
|
|
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
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
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;
|
|
1388
|
+
try {
|
|
1389
|
+
return BigInt(String(v));
|
|
1390
|
+
} catch {
|
|
1391
|
+
return 0n;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
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;
|
|
1398
|
+
}
|
|
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
|
+
}
|
|
1528
1421
|
|
|
1529
|
-
// src/
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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}`;
|
|
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;
|
|
1647
1426
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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);
|
|
1459
|
+
}
|
|
1460
|
+
increment(table, key, deltas) {
|
|
1461
|
+
this.dependsOnPrior(`increment("${table}")`);
|
|
1462
|
+
super.increment(table, key, deltas);
|
|
1463
|
+
}
|
|
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`);
|
|
1673
1474
|
}
|
|
1475
|
+
super.upsert(table, key, row);
|
|
1674
1476
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
reasons.push("backfillMode concurrent: tip-first order is not chain order");
|
|
1477
|
+
async rowsOf(table) {
|
|
1478
|
+
return this.overlayMany(table, {}, await this.readRows(table, {}));
|
|
1678
1479
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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
|
+
};
|
|
1683
1502
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
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;
|
|
1511
|
+
}
|
|
1512
|
+
insertsSince(checkpoint) {
|
|
1513
|
+
const out = [];
|
|
1514
|
+
for (const op of this.ops.slice(Math.max(0, checkpoint))) {
|
|
1515
|
+
if (op.kind !== "insert")
|
|
1516
|
+
continue;
|
|
1517
|
+
const row = { ...op.data };
|
|
1518
|
+
out.push({
|
|
1519
|
+
table: op.table,
|
|
1520
|
+
keys: Object.keys(row).filter((k) => !k.startsWith("_")),
|
|
1521
|
+
row
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
return out;
|
|
1686
1525
|
}
|
|
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
1526
|
}
|
|
1696
|
-
function
|
|
1697
|
-
|
|
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);
|
|
1698
1534
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1535
|
+
|
|
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";
|
|
1544
|
+
|
|
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());
|
|
1701
1551
|
}
|
|
1702
|
-
function
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
};
|
|
1710
|
-
|
|
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;
|
|
1711
1564
|
}
|
|
1712
|
-
function
|
|
1713
|
-
|
|
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 };
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1714
1606
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1607
|
+
function unifyNullable(a, b) {
|
|
1608
|
+
if (!a)
|
|
1609
|
+
return b;
|
|
1610
|
+
if (!b)
|
|
1611
|
+
return a;
|
|
1612
|
+
return unify(a, b);
|
|
1719
1613
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
|
1625
|
+
};
|
|
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 };
|
|
1629
|
+
}
|
|
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) };
|
|
1635
|
+
}
|
|
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
|
+
}
|
|
1677
|
+
}
|
|
1723
1678
|
}
|
|
1724
|
-
function
|
|
1725
|
-
|
|
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;
|
|
1685
|
+
}
|
|
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
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
next.push(t);
|
|
1698
|
+
return makeUnion(next);
|
|
1726
1699
|
}
|
|
1727
|
-
|
|
1728
|
-
"
|
|
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
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
1770
|
-
|
|
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
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1784
|
-
for (const
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1795
|
-
return context;
|
|
1886
|
+
return out.sort((a, b) => b.count - a.count);
|
|
1796
1887
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
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);
|
|
1824
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;
|
|
1913
|
+
}
|
|
1914
|
+
return data;
|
|
1825
1915
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
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
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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
|
|
@@ -1970,6 +2034,41 @@ function printContractId(data) {
|
|
|
1970
2034
|
const id = (typeof data.contract_identifier === "string" ? data.contract_identifier : undefined) ?? (typeof data.contract_id === "string" ? data.contract_id : undefined);
|
|
1971
2035
|
return id || undefined;
|
|
1972
2036
|
}
|
|
2037
|
+
var decodedPrints = new WeakMap;
|
|
2038
|
+
function decodedPrintTuple(event, data) {
|
|
2039
|
+
if (decodedPrints.has(event))
|
|
2040
|
+
return decodedPrints.get(event) ?? null;
|
|
2041
|
+
const raw = data.raw_value;
|
|
2042
|
+
const value = typeof raw === "string" && raw.startsWith("0x") ? decodeClarityValue(raw) : null;
|
|
2043
|
+
const tuple = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2044
|
+
decodedPrints.set(event, tuple);
|
|
2045
|
+
return tuple;
|
|
2046
|
+
}
|
|
2047
|
+
function printFieldEquals(decoded, key, expected) {
|
|
2048
|
+
const value = decoded[key];
|
|
2049
|
+
let actual;
|
|
2050
|
+
if (typeof value === "string")
|
|
2051
|
+
actual = value;
|
|
2052
|
+
else if (typeof value === "bigint" || typeof value === "boolean")
|
|
2053
|
+
actual = String(value);
|
|
2054
|
+
else
|
|
2055
|
+
return false;
|
|
2056
|
+
return typeof expected === "string" ? actual === expected : expected.includes(actual);
|
|
2057
|
+
}
|
|
2058
|
+
function printFieldsAllow(filter, event, data) {
|
|
2059
|
+
if (!filter.topic && !filter.where)
|
|
2060
|
+
return true;
|
|
2061
|
+
const decoded = decodedPrintTuple(event, data);
|
|
2062
|
+
if (!decoded)
|
|
2063
|
+
return false;
|
|
2064
|
+
if (filter.topic && !printFieldEquals(decoded, "topic", filter.topic))
|
|
2065
|
+
return false;
|
|
2066
|
+
for (const [key, expected] of Object.entries(filter.where ?? {})) {
|
|
2067
|
+
if (!printFieldEquals(decoded, key, expected))
|
|
2068
|
+
return false;
|
|
2069
|
+
}
|
|
2070
|
+
return true;
|
|
2071
|
+
}
|
|
1973
2072
|
function matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts = new Map, vmEventsByTx = new Map) {
|
|
1974
2073
|
const results = [];
|
|
1975
2074
|
switch (filter.type) {
|
|
@@ -2206,7 +2305,7 @@ function matchFilter(filter, transactions, eventsByTx, traitContracts, factoryCo
|
|
|
2206
2305
|
return false;
|
|
2207
2306
|
if (!factoryAllows(filter, contractId, factoryContracts))
|
|
2208
2307
|
return false;
|
|
2209
|
-
return
|
|
2308
|
+
return printFieldsAllow(filter, e, data);
|
|
2210
2309
|
});
|
|
2211
2310
|
if (matched.length > 0) {
|
|
2212
2311
|
results.push({ tx, events: matched });
|
|
@@ -2735,7 +2834,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
2735
2834
|
sourceName,
|
|
2736
2835
|
txId: tx.tx_id,
|
|
2737
2836
|
...event !== null ? { eventId: event.id, eventType: event.type } : {},
|
|
2738
|
-
error:
|
|
2837
|
+
error: getErrorMessage2(err)
|
|
2739
2838
|
});
|
|
2740
2839
|
}
|
|
2741
2840
|
}
|
|
@@ -2803,6 +2902,14 @@ function txFromRow(row) {
|
|
|
2803
2902
|
...typeof r.function_name === "string" ? { functionName: r.function_name } : {}
|
|
2804
2903
|
};
|
|
2805
2904
|
}
|
|
2905
|
+
function indexPrintWhereAllows(where, row) {
|
|
2906
|
+
if (!where)
|
|
2907
|
+
return true;
|
|
2908
|
+
const value = row.payload?.value;
|
|
2909
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2910
|
+
return false;
|
|
2911
|
+
return Object.entries(where).every(([key, expected]) => printFieldEquals(value, key, expected));
|
|
2912
|
+
}
|
|
2806
2913
|
function toHandlerPayload(_filter, row) {
|
|
2807
2914
|
const tx = defaultTx(txFromRow(row));
|
|
2808
2915
|
if (row.event_type === "print") {
|
|
@@ -2960,7 +3067,7 @@ async function runSubgraphTest(input) {
|
|
|
2960
3067
|
continue;
|
|
2961
3068
|
for (const row of rows) {
|
|
2962
3069
|
const payload = filter?.type === "contract_call" ? toContractCallPayload(row) : toHandlerPayload(filter, row);
|
|
2963
|
-
if (filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
|
|
3070
|
+
if (filter?.type === "print_event" && (filter.topic && payload.topic !== filter.topic || !indexPrintWhereAllows(filter.where, row))) {
|
|
2964
3071
|
continue;
|
|
2965
3072
|
}
|
|
2966
3073
|
if (!firstEvent) {
|
|
@@ -3099,5 +3206,5 @@ export {
|
|
|
3099
3206
|
toHandlerPayload
|
|
3100
3207
|
};
|
|
3101
3208
|
|
|
3102
|
-
//# debugId=
|
|
3209
|
+
//# debugId=1050D35A53F758C264756E2164756E21
|
|
3103
3210
|
//# sourceMappingURL=index.js.map
|