@secondlayer/subgraphs 3.22.0 → 3.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/src/index.d.ts +118 -9
  2. package/dist/src/index.js +159 -14
  3. package/dist/src/index.js.map +7 -6
  4. package/dist/src/runtime/block-processor.d.ts +43 -2
  5. package/dist/src/runtime/block-processor.js +155 -12
  6. package/dist/src/runtime/block-processor.js.map +7 -6
  7. package/dist/src/runtime/catchup.d.ts +43 -2
  8. package/dist/src/runtime/catchup.js +155 -12
  9. package/dist/src/runtime/catchup.js.map +7 -6
  10. package/dist/src/runtime/context.d.ts +77 -3
  11. package/dist/src/runtime/context.js +142 -1
  12. package/dist/src/runtime/context.js.map +6 -5
  13. package/dist/src/runtime/processor.js +159 -16
  14. package/dist/src/runtime/processor.js.map +7 -6
  15. package/dist/src/runtime/reindex.d.ts +43 -2
  16. package/dist/src/runtime/reindex.js +155 -12
  17. package/dist/src/runtime/reindex.js.map +7 -6
  18. package/dist/src/runtime/reorg.d.ts +43 -2
  19. package/dist/src/runtime/reorg.js +155 -12
  20. package/dist/src/runtime/reorg.js.map +7 -6
  21. package/dist/src/runtime/replay.d.ts +3 -3
  22. package/dist/src/runtime/replay.js +2 -1
  23. package/dist/src/runtime/replay.js.map +3 -3
  24. package/dist/src/runtime/runner.d.ts +102 -5
  25. package/dist/src/runtime/source-matcher.d.ts +2 -2
  26. package/dist/src/runtime/stats.d.ts +3 -3
  27. package/dist/src/schema/index.d.ts +46 -5
  28. package/dist/src/service.js +159 -16
  29. package/dist/src/service.js.map +7 -6
  30. package/dist/src/testing/index.d.ts +52 -5
  31. package/dist/src/testing/index.js +172 -6
  32. package/dist/src/testing/index.js.map +6 -5
  33. package/dist/src/triggers/index.d.ts +2 -2
  34. package/dist/src/types.d.ts +43 -2
  35. package/dist/src/validate.d.ts +43 -2
  36. package/package.json +5 -3
@@ -1,4 +1,32 @@
1
- import { AbiContract } from "@secondlayer/stacks/clarity";
1
+ import { AbiContract as AbiContract2 } from "@secondlayer/stacks/clarity";
2
+ /**
3
+ * How long a read stays valid.
4
+ *
5
+ * - `per-block` (default): pinned to the block being processed, cached under
6
+ * its `index_block_hash`. Sound for any read-only function — reindexing the
7
+ * same block always returns the same value, and a reorg-replaced block has a
8
+ * different id so it can't inherit the orphaned answer.
9
+ * - `contract-constant`: the caller asserts the value cannot change for this
10
+ * contract and these args (SIP-010 `get-decimals`/`get-symbol`), so it is
11
+ * resolved ONCE instead of once per block. This is the mode that makes a
12
+ * backfill affordable — under `per-block` every block is a distinct key and
13
+ * therefore a distinct RPC. Declaring it on a value that does change pins the
14
+ * first answer forever, so it is opt-in and never inferred.
15
+ */
16
+ type ChainReadCacheMode = "per-block" | "contract-constant";
17
+ type ChainReadOptions = {
18
+ cache?: ChainReadCacheMode
19
+ };
20
+ /**
21
+ * `ChainReadClient` with the ABI generic erased. Structurally cheap, so the
22
+ * base `SubgraphContext` can carry it; `TypedSubgraphContext` uses the real
23
+ * one and handlers get full inference.
24
+ */
25
+ interface ErasedChainReadClient {
26
+ contract(contractId: string, abi: unknown, options?: ChainReadOptions): {
27
+ read: Record<string, (args?: Record<string, unknown>) => Promise<unknown>>
28
+ };
29
+ }
2
30
  /** Supported column types for subgraph schemas */
3
31
  type ColumnType = "text" | "uint" | "int" | "principal" | "boolean" | "timestamp" | "jsonb";
4
32
  /** Column definition in a subgraph table */
@@ -175,7 +203,7 @@ interface ContractCallFilter extends TraitScope, FactoryScope {
175
203
  * definition and used at runtime to decode args by name. Omit to keep
176
204
  * `event.args` as a positional `unknown[]`.
177
205
  */
178
- abi?: AbiContract;
206
+ abi?: AbiContract2;
179
207
  }
180
208
  interface ContractDeployFilter {
181
209
  type: "contract_deploy";
@@ -246,8 +274,21 @@ interface SubgraphContext {
246
274
  hash: string
247
275
  timestamp: number
248
276
  burnBlockHeight: number
277
+ /** Nakamoto StacksBlockId, when the block row carries one. */
278
+ indexBlockHash?: string | null
249
279
  };
250
280
  tx: TxMeta;
281
+ /**
282
+ * Read-only contract calls pinned to this block. Cached, so the same
283
+ * (contract, function, args) is fetched once per block rather than once
284
+ * per event.
285
+ *
286
+ * Erased here on purpose: this interface is compared structurally wherever
287
+ * a subgraph definition is passed around, and carrying an ABI-generic
288
+ * method pushes those comparisons past TS's instantiation depth. Handlers
289
+ * receive `TypedSubgraphContext`, which exposes the fully typed version.
290
+ */
291
+ client: ErasedChainReadClient;
251
292
  insert(table: string, row: Record<string, unknown>): void;
252
293
  update(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;
253
294
  upsert(table: string, key: Record<string, unknown>, row: Record<string, unknown>): void;
@@ -145,6 +145,139 @@ function generateSubgraphSQL(def, schemaNameOverride) {
145
145
  return { statements, hash };
146
146
  }
147
147
 
148
+ // src/runtime/chain-read.ts
149
+ import { getSourceDb } from "@secondlayer/shared/db";
150
+ import { http, createPublicClient } from "@secondlayer/stacks";
151
+ import {
152
+ ContractResponseError,
153
+ buildFunctionArgs,
154
+ isResponseOutput,
155
+ readContract
156
+ } from "@secondlayer/stacks/actions";
157
+ import {
158
+ clarityValueToJSUntyped,
159
+ deserializeCV,
160
+ normalizeAbi,
161
+ serializeCV,
162
+ toCamelCase
163
+ } from "@secondlayer/stacks/clarity";
164
+ function readContractAt(ctx, contractId, abi, options) {
165
+ return ctx.client.contract(contractId, abi, options);
166
+ }
167
+
168
+ class ChainReadError extends Error {
169
+ name = "ChainReadError";
170
+ }
171
+ function nodeClient(rpcUrl) {
172
+ return createPublicClient({
173
+ transport: http(rpcUrl, { retryCount: 0, timeout: 1e4 })
174
+ });
175
+ }
176
+
177
+ class ConcurrencyGate {
178
+ limit;
179
+ active = 0;
180
+ queue = [];
181
+ constructor(limit) {
182
+ this.limit = limit;
183
+ }
184
+ async run(fn) {
185
+ if (this.active >= this.limit) {
186
+ await new Promise((resolve) => this.queue.push(resolve));
187
+ }
188
+ this.active++;
189
+ try {
190
+ return await fn();
191
+ } finally {
192
+ this.active--;
193
+ this.queue.shift()?.();
194
+ }
195
+ }
196
+ }
197
+ var DEFAULT_MAX_CONCURRENT_READS = 4;
198
+ function maxConcurrentReads() {
199
+ const raw = process.env.SUBGRAPH_CHAIN_READ_CONCURRENCY;
200
+ const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
201
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT_READS;
202
+ }
203
+ var gate = new ConcurrencyGate(maxConcurrentReads());
204
+ function hashArgs(args) {
205
+ const serialized = args.map((arg) => serializeCV(arg)).join(",");
206
+ return Bun.hash(serialized).toString(16);
207
+ }
208
+ function findFunction(abi, name) {
209
+ const fn = abi.functions.find((f) => f.name === name);
210
+ if (!fn) {
211
+ throw new ChainReadError(`No function "${name}" in the supplied ABI.`);
212
+ }
213
+ if (fn.access !== "read-only") {
214
+ throw new ChainReadError(`"${name}" is ${fn.access}, not read-only — a handler cannot call it.`);
215
+ }
216
+ return fn;
217
+ }
218
+ function createChainReadClient(ctx) {
219
+ const readValue = async (contractId, abi, functionName, args, options) => {
220
+ const rpcUrl = ctx.rpcUrl ?? process.env.STACKS_NODE_RPC_URL;
221
+ if (!rpcUrl) {
222
+ throw new ChainReadError("ctx.client needs a Stacks node: set STACKS_NODE_RPC_URL on the subgraph processor.");
223
+ }
224
+ const constant = options.cache === "contract-constant";
225
+ if (!constant && !ctx.indexBlockHash) {
226
+ throw new ChainReadError(`Block ${ctx.blockHeight} has no index_block_hash, so a read cannot be pinned to it. Reading at the node's tip would make this handler non-deterministic (the same reindex would produce different rows). Re-ingest the block, or pass cache: "contract-constant" if the value genuinely cannot change.`);
227
+ }
228
+ const fn = findFunction(abi, functionName);
229
+ const clarityArgs = buildFunctionArgs(fn, args);
230
+ const db = ctx.db ?? getSourceDb();
231
+ const argsHash = hashArgs(clarityArgs);
232
+ const pinnedTo = constant ? null : ctx.indexBlockHash;
233
+ const cached = await db.selectFrom("chain_read_cache").select("result_hex").where("contract_id", "=", contractId).where("function_name", "=", fn.name).where("args_hash", "=", argsHash).where((eb) => pinnedTo === null ? eb("index_block_hash", "is", null) : eb("index_block_hash", "=", pinnedTo)).executeTakeFirst();
234
+ const resultHex = cached?.result_hex ?? await gate.run(async () => {
235
+ const value2 = await readContract(nodeClient(rpcUrl), {
236
+ contract: contractId,
237
+ functionName: fn.name,
238
+ args: clarityArgs,
239
+ tip: ctx.indexBlockHash ?? undefined
240
+ });
241
+ return serializeCV(value2);
242
+ });
243
+ if (!cached) {
244
+ await db.insertInto("chain_read_cache").values({
245
+ contract_id: contractId,
246
+ function_name: fn.name,
247
+ args_hash: argsHash,
248
+ index_block_hash: pinnedTo,
249
+ block_height: constant ? null : ctx.blockHeight,
250
+ result_hex: resultHex
251
+ }).onConflict((oc) => oc.doNothing()).execute();
252
+ }
253
+ const value = deserializeCV(resultHex);
254
+ const js = clarityValueToJSUntyped(fn.outputs, value);
255
+ if (isResponseOutput(fn.outputs)) {
256
+ const response = js;
257
+ if ("ok" in response)
258
+ return response.ok;
259
+ throw new ContractResponseError(`${contractId}::${fn.name} returned an err response.`, response.err);
260
+ }
261
+ return js;
262
+ };
263
+ return {
264
+ contract(contractId, rawAbi, options = {}) {
265
+ const abi = normalizeAbi(rawAbi);
266
+ const camelToKebab = new Map;
267
+ for (const fn of abi.functions) {
268
+ camelToKebab.set(toCamelCase(fn.name), fn.name);
269
+ }
270
+ const read = new Proxy({}, {
271
+ get(_target, prop) {
272
+ const fnName = camelToKebab.get(prop) ?? prop;
273
+ return (args = {}) => readValue(contractId, abi, fnName, args, options);
274
+ }
275
+ });
276
+ return { read };
277
+ }
278
+ };
279
+ }
280
+
148
281
  // src/runtime/context.ts
149
282
  var JOURNAL_RETENTION_BLOCKS = 300;
150
283
  var journalEnsured = new Set;
@@ -163,6 +296,7 @@ class SubgraphContext {
163
296
  ops = [];
164
297
  byo;
165
298
  journal;
299
+ _client;
166
300
  constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
167
301
  this.db = db;
168
302
  this.pgSchemaName = pgSchemaName2;
@@ -172,6 +306,13 @@ class SubgraphContext {
172
306
  this.byo = byo;
173
307
  this.journal = journal;
174
308
  }
309
+ get client() {
310
+ this._client ??= createChainReadClient({
311
+ blockHeight: this.block.height,
312
+ indexBlockHash: this.block.indexBlockHash
313
+ });
314
+ return this._client;
315
+ }
175
316
  get tx() {
176
317
  return this._tx;
177
318
  }
@@ -733,11 +874,11 @@ function buildWhereClause(where) {
733
874
  }
734
875
 
735
876
  // src/runtime/clarity.ts
736
- import { cvToValue, deserializeCV } from "@secondlayer/stacks/clarity";
877
+ import { cvToValue, deserializeCV as deserializeCV2 } from "@secondlayer/stacks/clarity";
737
878
  function decodeClarityValue(hex) {
738
879
  try {
739
880
  const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
740
- const cv = deserializeCV(cleanHex);
881
+ const cv = deserializeCV2(cleanHex);
741
882
  return cvToValue(cv);
742
883
  } catch {
743
884
  return hex;
@@ -768,8 +909,8 @@ import { getErrorMessage } from "@secondlayer/shared";
768
909
  import { logger as logger2 } from "@secondlayer/shared/logger";
769
910
  import {
770
911
  clarityValueToJS,
771
- deserializeCV as deserializeCV2,
772
- toCamelCase
912
+ deserializeCV as deserializeCV3,
913
+ toCamelCase as toCamelCase2
773
914
  } from "@secondlayer/stacks/clarity";
774
915
 
775
916
  // src/runtime/print-validate.ts
@@ -932,7 +1073,7 @@ function buildContractCallInput(filter, tx) {
932
1073
  return;
933
1074
  try {
934
1075
  const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
935
- input[toCamelCase(arg.name)] = decodeArg(arg.type, deserializeCV2(clean));
1076
+ input[toCamelCase2(arg.name)] = decodeArg(arg.type, deserializeCV3(clean));
936
1077
  } catch {}
937
1078
  });
938
1079
  return input;
@@ -1486,7 +1627,7 @@ import { logger as logger5 } from "@secondlayer/shared/logger";
1486
1627
  import { sql as sql3 } from "kysely";
1487
1628
 
1488
1629
  // src/runtime/block-source.ts
1489
- import { getSourceDb } from "@secondlayer/shared/db";
1630
+ import { getSourceDb as getSourceDb2 } from "@secondlayer/shared/db";
1490
1631
  import {
1491
1632
  IndexHttpClient
1492
1633
  } from "@secondlayer/shared/index-http";
@@ -1547,6 +1688,7 @@ function reconstructBlock(b) {
1547
1688
  parent_hash: b.parent_hash,
1548
1689
  burn_block_height: b.burn_block_height,
1549
1690
  burn_block_hash: b.burn_block_hash,
1691
+ index_block_hash: b.index_block_hash ?? null,
1550
1692
  timestamp: isoToUnixSeconds(b.block_time),
1551
1693
  canonical: true,
1552
1694
  created_at: new Date(0)
@@ -1681,11 +1823,11 @@ function sparseProbeTargets(subgraph) {
1681
1823
 
1682
1824
  class PostgresBlockSource {
1683
1825
  async getTip() {
1684
- const progress = await getSourceDb().selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
1826
+ const progress = await getSourceDb2().selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
1685
1827
  return progress ? Number(progress.highest_seen_block) : 0;
1686
1828
  }
1687
1829
  loadBlockRange(fromHeight, toHeight) {
1688
- return loadBlockRange(getSourceDb(), fromHeight, toHeight);
1830
+ return loadBlockRange(getSourceDb2(), fromHeight, toHeight);
1689
1831
  }
1690
1832
  }
1691
1833
  var EVENT_FILTER_TO_INDEX_TYPE = {
@@ -1754,8 +1896,8 @@ class PublicApiBlockSource {
1754
1896
  eventTypes;
1755
1897
  probeTargets;
1756
1898
  needsTransactions;
1757
- constructor(http, eventTypes, probeTargets, needsTransactions = true) {
1758
- this.http = http;
1899
+ constructor(http2, eventTypes, probeTargets, needsTransactions = true) {
1900
+ this.http = http2;
1759
1901
  this.eventTypes = eventTypes;
1760
1902
  this.probeTargets = probeTargets;
1761
1903
  this.needsTransactions = needsTransactions;
@@ -2267,7 +2409,8 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2267
2409
  height: block.height,
2268
2410
  hash: block.hash,
2269
2411
  timestamp: block.timestamp,
2270
- burnBlockHeight: block.burn_block_height
2412
+ burnBlockHeight: block.burn_block_height,
2413
+ indexBlockHash: block.index_block_hash
2271
2414
  };
2272
2415
  const initialTx = {
2273
2416
  txId: "",
@@ -2430,5 +2573,5 @@ export {
2430
2573
  BLOCK_RETRY_DELAYS_MS
2431
2574
  };
2432
2575
 
2433
- //# debugId=0089DCBC0C86751064756E2164756E21
2576
+ //# debugId=0225B7830F6ABAFA64756E2164756E21
2434
2577
  //# sourceMappingURL=block-processor.js.map