@secondlayer/subgraphs 7.2.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 CHANGED
@@ -108,6 +108,12 @@ declare function readContractAt<const C extends AbiContract>(ctx: {
108
108
  }, contractId: string, abi: C, options?: ChainReadOptions): {
109
109
  read: ChainReadMethods<C>;
110
110
  };
111
+ /**
112
+ * A handler asked for something no node can answer: a function missing from
113
+ * the ABI, or one that isn't read-only. Deterministic, so it fails only the
114
+ * event. A read the processor couldn't complete (no node, node down, cache
115
+ * error) throws {@link HandlerReadError} instead and aborts the block.
116
+ */
111
117
  declare class ChainReadError extends Error {
112
118
  name: string;
113
119
  }
@@ -1206,9 +1212,11 @@ declare class NondeterminismError extends Error {
1206
1212
  name: string;
1207
1213
  }
1208
1214
  /**
1209
- * A ctx read (DB, chain) failed under a verifiable subgraph. Infra-class like
1215
+ * A ctx read failed for a reason outside the handler: the database under a
1216
+ * verifiable subgraph, or a chain read (`ctx.client`) on any subgraph that
1217
+ * had no node, a node that didn't answer, or a cache error. Infra-class like
1210
1218
  * {@link NondeterminismError}: skipping the event would make its rows depend
1211
- * on whether the database was reachable.
1219
+ * on whether the database or node was reachable.
1212
1220
  */
1213
1221
  declare class HandlerReadError extends Error {
1214
1222
  name: string;
package/dist/index.js CHANGED
@@ -648,9 +648,17 @@ function inferPrintTopics(samples) {
648
648
  return out.sort((a, b) => b.count - a.count);
649
649
  }
650
650
  // src/runtime/chain-read.ts
651
+ import { getErrorMessage } from "@secondlayer/shared";
652
+ import { chainReadArgsHash } from "@secondlayer/shared/chain-read";
651
653
  import { getSourceDb } from "@secondlayer/shared/db";
654
+ import { hasNoLocalChain } from "@secondlayer/shared/db/queries/instance";
652
655
  import { logger } from "@secondlayer/shared/logger";
653
- import { http, createPublicClient } from "@secondlayer/stacks";
656
+ import {
657
+ http,
658
+ HttpRequestError,
659
+ ReadContractError,
660
+ createPublicClient
661
+ } from "@secondlayer/stacks";
654
662
  import {
655
663
  ContractResponseError,
656
664
  buildFunctionArgs,
@@ -664,136 +672,16 @@ import {
664
672
  serializeCV,
665
673
  toCamelCase
666
674
  } from "@secondlayer/stacks/clarity";
667
- function readContractAt(ctx, contractId, abi, options) {
668
- return ctx.client.contract(contractId, abi, options);
669
- }
670
675
 
671
- class ChainReadError extends Error {
672
- name = "ChainReadError";
673
- }
674
- function nodeClient(rpcUrl) {
675
- return createPublicClient({
676
- transport: http(rpcUrl, { retryCount: 0, timeout: 1e4 })
677
- });
678
- }
676
+ // src/runtime/realm.ts
677
+ import vm from "node:vm";
679
678
 
680
- class ConcurrencyGate {
681
- limit;
682
- active = 0;
683
- queue = [];
684
- constructor(limit) {
685
- this.limit = limit;
686
- }
687
- async run(fn) {
688
- if (this.active >= this.limit) {
689
- await new Promise((resolve) => this.queue.push(resolve));
690
- }
691
- this.active++;
692
- try {
693
- return await fn();
694
- } finally {
695
- this.active--;
696
- this.queue.shift()?.();
697
- }
698
- }
699
- }
700
- var DEFAULT_MAX_CONCURRENT_READS = 4;
701
- function maxConcurrentReads() {
702
- const raw = process.env.SUBGRAPH_CHAIN_READ_CONCURRENCY;
703
- const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
704
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT_READS;
705
- }
706
- var gate = new ConcurrencyGate(maxConcurrentReads());
707
- function hashArgs(args) {
708
- const serialized = args.map((arg) => serializeCV(arg)).join(",");
709
- return Bun.hash(serialized).toString(16);
710
- }
711
- function findFunction(abi, name) {
712
- const fn = abi.functions.find((f) => f.name === name);
713
- if (!fn) {
714
- throw new ChainReadError(`No function "${name}" in the supplied ABI.`);
715
- }
716
- if (fn.access !== "read-only") {
717
- throw new ChainReadError(`"${name}" is ${fn.access}, not read-only — a handler cannot call it.`);
718
- }
719
- return fn;
720
- }
721
- function createChainReadClient(ctx) {
722
- const readValue = async (contractId, abi, functionName, args, options) => {
723
- const rpcUrl = ctx.rpcUrl ?? process.env.STACKS_NODE_RPC_URL;
724
- if (!rpcUrl) {
725
- throw new ChainReadError("ctx.client needs a Stacks node: set STACKS_NODE_RPC_URL on the subgraph processor.");
726
- }
727
- const constant = options.cache === "contract-constant";
728
- if (!constant && !ctx.indexBlockHash) {
729
- throw new ChainReadError(`Block ${ctx.blockHeight} has no index_block_hash, so a read cannot be pinned to it. Reading at the node's tip would make this handler non-deterministic (the same reindex would produce different rows). Re-ingest the block, or pass cache: "contract-constant" if the value genuinely cannot change.`);
730
- }
731
- const fn = findFunction(abi, functionName);
732
- const clarityArgs = buildFunctionArgs(fn, args);
733
- const db = ctx.db ?? getSourceDb();
734
- const argsHash = hashArgs(clarityArgs);
735
- const pinnedTo = constant ? null : ctx.indexBlockHash;
736
- 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();
737
- const resultHex = cached?.result_hex ?? await gate.run(async () => {
738
- if (!ctx.indexBlockHash) {
739
- logger.warn("Chain read is not pinned to a block (no index_block_hash)", {
740
- contractId,
741
- functionName: fn.name,
742
- blockHeight: ctx.blockHeight,
743
- note: 'cache: "contract-constant" asserts this value cannot change; the node answered at its own tip'
744
- });
745
- }
746
- const value = await readContract(nodeClient(rpcUrl), {
747
- contract: contractId,
748
- functionName: fn.name,
749
- args: clarityArgs,
750
- tip: ctx.indexBlockHash ?? undefined
751
- });
752
- return serializeCV(value);
753
- });
754
- if (!cached) {
755
- await db.insertInto("chain_read_cache").values({
756
- contract_id: contractId,
757
- function_name: fn.name,
758
- args_hash: argsHash,
759
- index_block_hash: pinnedTo,
760
- block_height: constant ? null : ctx.blockHeight,
761
- result_hex: resultHex
762
- }).onConflict((oc) => oc.doNothing()).execute();
763
- }
764
- const value = deserializeCV2(resultHex);
765
- const js = clarityValueToJSUntyped(fn.outputs, value);
766
- if (isResponseOutput(fn.outputs)) {
767
- const response = js;
768
- if ("ok" in response)
769
- return response.ok;
770
- throw new ContractResponseError(`${contractId}::${fn.name} returned an err response.`, response.err);
771
- }
772
- return js;
773
- };
774
- return {
775
- contract(contractId, rawAbi, options = {}) {
776
- const abi = normalizeAbi(rawAbi);
777
- const camelToKebab = new Map;
778
- for (const fn of abi.functions) {
779
- camelToKebab.set(toCamelCase(fn.name), fn.name);
780
- }
781
- const read = new Proxy({}, {
782
- get(_target, prop) {
783
- const fnName = camelToKebab.get(prop) ?? prop;
784
- return (args = {}) => readValue(contractId, abi, fnName, args, options);
785
- }
786
- });
787
- return { read };
788
- }
789
- };
790
- }
791
679
  // src/verification.ts
792
680
  import { createHash } from "node:crypto";
793
681
  // package.json
794
682
  var package_default = {
795
683
  name: "@secondlayer/subgraphs",
796
- version: "7.2.0",
684
+ version: "7.3.0",
797
685
  description: "Describe the Stacks data you want as one TypeScript file; it fills itself and survives reorgs.",
798
686
  license: "MIT",
799
687
  repository: {
@@ -853,7 +741,7 @@ var package_default = {
853
741
  prepublishOnly: "bun run build"
854
742
  },
855
743
  dependencies: {
856
- "@secondlayer/shared": "^13.1.0",
744
+ "@secondlayer/shared": "^13.2.0",
857
745
  "@secondlayer/stacks": "^6.4.0",
858
746
  "@secondlayer/verify": "^0.6.0",
859
747
  esbuild: "^0.28.2",
@@ -1056,8 +944,8 @@ function pinPreimage(input) {
1056
944
  function computePin(input) {
1057
945
  return sha256(pinPreimage(input));
1058
946
  }
947
+
1059
948
  // src/runtime/realm.ts
1060
- import vm from "node:vm";
1061
949
  class NondeterminismError extends Error {
1062
950
  name = "NondeterminismError";
1063
951
  }
@@ -1272,6 +1160,186 @@ ${code}
1272
1160
  realmDefinitions.add(loaded);
1273
1161
  return loaded;
1274
1162
  }
1163
+
1164
+ // src/runtime/chain-read.ts
1165
+ function readContractAt(ctx, contractId, abi, options) {
1166
+ return ctx.client.contract(contractId, abi, options);
1167
+ }
1168
+
1169
+ class ChainReadError extends Error {
1170
+ name = "ChainReadError";
1171
+ }
1172
+ var NO_NODE_MESSAGE = "ctx.client needs a Stacks node: set STACKS_NODE_RPC_URL on the subgraph processor.";
1173
+ async function resolveReadTarget(rpcUrl, db) {
1174
+ if (rpcUrl)
1175
+ return { kind: "node", rpcUrl };
1176
+ const apiUrl = process.env.SUBGRAPH_INDEX_API_URL;
1177
+ if (!apiUrl || !await hasNoLocalChain(db)) {
1178
+ throw new HandlerReadError(NO_NODE_MESSAGE);
1179
+ }
1180
+ const apiKey = process.env.INDEX_INTERNAL_API_KEY?.trim();
1181
+ if (!apiKey) {
1182
+ 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.");
1183
+ }
1184
+ return { kind: "hosted", apiUrl, apiKey };
1185
+ }
1186
+ function hostedReadError(err, label) {
1187
+ if (!(err instanceof HttpRequestError))
1188
+ return;
1189
+ if (err.status === 400) {
1190
+ return new ChainReadError(`${label} was refused as invalid: ${err.details ?? err.message}`);
1191
+ }
1192
+ if (err.status === 403) {
1193
+ return new HandlerReadError(`${label}: hosted contract reads serve hosted subgraphs only. ${NO_NODE_MESSAGE}`, { cause: err });
1194
+ }
1195
+ return;
1196
+ }
1197
+ function isChainAnswer(err) {
1198
+ return err instanceof ContractResponseError || err instanceof ReadContractError;
1199
+ }
1200
+ function readClient(target) {
1201
+ if (target.kind === "node") {
1202
+ return createPublicClient({
1203
+ transport: http(target.rpcUrl, { retryCount: 0, timeout: 1e4 })
1204
+ });
1205
+ }
1206
+ return createPublicClient({
1207
+ transport: http(`${target.apiUrl.replace(/\/+$/, "")}/v1/chain-read`, {
1208
+ retryCount: 3,
1209
+ timeout: 1e4,
1210
+ fetchOptions: {
1211
+ headers: { authorization: `Bearer ${target.apiKey}` }
1212
+ }
1213
+ })
1214
+ });
1215
+ }
1216
+
1217
+ class ConcurrencyGate {
1218
+ limit;
1219
+ active = 0;
1220
+ queue = [];
1221
+ constructor(limit) {
1222
+ this.limit = limit;
1223
+ }
1224
+ async run(fn) {
1225
+ if (this.active >= this.limit) {
1226
+ await new Promise((resolve) => this.queue.push(resolve));
1227
+ }
1228
+ this.active++;
1229
+ try {
1230
+ return await fn();
1231
+ } finally {
1232
+ this.active--;
1233
+ this.queue.shift()?.();
1234
+ }
1235
+ }
1236
+ }
1237
+ var DEFAULT_MAX_CONCURRENT_READS = 4;
1238
+ function maxConcurrentReads() {
1239
+ const raw = process.env.SUBGRAPH_CHAIN_READ_CONCURRENCY;
1240
+ const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
1241
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT_READS;
1242
+ }
1243
+ var gate = new ConcurrencyGate(maxConcurrentReads());
1244
+ function hashArgs(args) {
1245
+ return chainReadArgsHash(args.map((arg) => serializeCV(arg)));
1246
+ }
1247
+ function findFunction(abi, name) {
1248
+ const fn = abi.functions.find((f) => f.name === name);
1249
+ if (!fn) {
1250
+ throw new ChainReadError(`No function "${name}" in the supplied ABI.`);
1251
+ }
1252
+ if (fn.access !== "read-only") {
1253
+ throw new ChainReadError(`"${name}" is ${fn.access}, not read-only — a handler cannot call it.`);
1254
+ }
1255
+ return fn;
1256
+ }
1257
+ function createChainReadClient(ctx) {
1258
+ const readValue = async (contractId, abi, functionName, args, options) => {
1259
+ const db = ctx.db ?? getSourceDb();
1260
+ const rpcUrl = ctx.rpcUrl || process.env.STACKS_NODE_RPC_URL || undefined;
1261
+ if (!rpcUrl && !process.env.SUBGRAPH_INDEX_API_URL) {
1262
+ throw new HandlerReadError(NO_NODE_MESSAGE);
1263
+ }
1264
+ const constant = options.cache === "contract-constant";
1265
+ if (!constant && !ctx.indexBlockHash) {
1266
+ 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.`);
1267
+ }
1268
+ const fn = findFunction(abi, functionName);
1269
+ const clarityArgs = buildFunctionArgs(fn, args);
1270
+ const argsHash = hashArgs(clarityArgs);
1271
+ const pinnedTo = constant ? null : ctx.indexBlockHash;
1272
+ let resultHex;
1273
+ try {
1274
+ 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();
1275
+ resultHex = cached?.result_hex ?? await gate.run(async () => {
1276
+ const target = await resolveReadTarget(rpcUrl, db);
1277
+ if (!ctx.indexBlockHash && target.kind === "hosted") {
1278
+ 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.`);
1279
+ }
1280
+ if (!ctx.indexBlockHash) {
1281
+ logger.warn("Chain read is not pinned to a block (no index_block_hash)", {
1282
+ contractId,
1283
+ functionName: fn.name,
1284
+ blockHeight: ctx.blockHeight,
1285
+ note: 'cache: "contract-constant" asserts this value cannot change; the node answered at its own tip'
1286
+ });
1287
+ }
1288
+ try {
1289
+ const value = await readContract(readClient(target), {
1290
+ contract: contractId,
1291
+ functionName: fn.name,
1292
+ args: clarityArgs,
1293
+ tip: ctx.indexBlockHash ?? undefined
1294
+ });
1295
+ return serializeCV(value);
1296
+ } catch (err) {
1297
+ throw target.kind === "hosted" && hostedReadError(err, `Chain read ${contractId}::${fn.name}`) || err;
1298
+ }
1299
+ });
1300
+ if (!cached) {
1301
+ await db.insertInto("chain_read_cache").values({
1302
+ contract_id: contractId,
1303
+ function_name: fn.name,
1304
+ args_hash: argsHash,
1305
+ index_block_hash: pinnedTo,
1306
+ block_height: constant ? null : ctx.blockHeight,
1307
+ result_hex: resultHex
1308
+ }).onConflict((oc) => oc.doNothing()).execute();
1309
+ }
1310
+ } catch (err) {
1311
+ if (isChainAnswer(err) || err instanceof ChainReadError || err instanceof HandlerReadError) {
1312
+ throw err;
1313
+ }
1314
+ throw new HandlerReadError(`Chain read ${contractId}::${fn.name} at block ${ctx.blockHeight} failed: ${getErrorMessage(err)}`, { cause: err });
1315
+ }
1316
+ const value = deserializeCV2(resultHex);
1317
+ const js = clarityValueToJSUntyped(fn.outputs, value);
1318
+ if (isResponseOutput(fn.outputs)) {
1319
+ const response = js;
1320
+ if ("ok" in response)
1321
+ return response.ok;
1322
+ throw new ContractResponseError(`${contractId}::${fn.name} returned an err response.`, response.err);
1323
+ }
1324
+ return js;
1325
+ };
1326
+ return {
1327
+ contract(contractId, rawAbi, options = {}) {
1328
+ const abi = normalizeAbi(rawAbi);
1329
+ const camelToKebab = new Map;
1330
+ for (const fn of abi.functions) {
1331
+ camelToKebab.set(toCamelCase(fn.name), fn.name);
1332
+ }
1333
+ const read = new Proxy({}, {
1334
+ get(_target, prop) {
1335
+ const fnName = camelToKebab.get(prop) ?? prop;
1336
+ return (args = {}) => readValue(contractId, abi, fnName, args, options);
1337
+ }
1338
+ });
1339
+ return { read };
1340
+ }
1341
+ };
1342
+ }
1275
1343
  // src/schema/generator.ts
1276
1344
  import { createHash as createHash2 } from "node:crypto";
1277
1345
 
@@ -2955,5 +3023,5 @@ export {
2955
3023
  validateSubgraphDefinition
2956
3024
  };
2957
3025
 
2958
- //# debugId=A0749A4C359E248364756E2164756E21
3026
+ //# debugId=B682843D48E7715C64756E2164756E21
2959
3027
  //# sourceMappingURL=index.js.map