@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.
- package/dist/src/index.d.ts +118 -9
- package/dist/src/index.js +159 -14
- package/dist/src/index.js.map +7 -6
- package/dist/src/runtime/block-processor.d.ts +43 -2
- package/dist/src/runtime/block-processor.js +155 -12
- package/dist/src/runtime/block-processor.js.map +7 -6
- package/dist/src/runtime/catchup.d.ts +43 -2
- package/dist/src/runtime/catchup.js +155 -12
- package/dist/src/runtime/catchup.js.map +7 -6
- package/dist/src/runtime/context.d.ts +77 -3
- package/dist/src/runtime/context.js +142 -1
- package/dist/src/runtime/context.js.map +6 -5
- package/dist/src/runtime/processor.js +159 -16
- package/dist/src/runtime/processor.js.map +7 -6
- package/dist/src/runtime/reindex.d.ts +43 -2
- package/dist/src/runtime/reindex.js +155 -12
- package/dist/src/runtime/reindex.js.map +7 -6
- package/dist/src/runtime/reorg.d.ts +43 -2
- package/dist/src/runtime/reorg.js +155 -12
- package/dist/src/runtime/reorg.js.map +7 -6
- package/dist/src/runtime/replay.d.ts +3 -3
- package/dist/src/runtime/replay.js +2 -1
- package/dist/src/runtime/replay.js.map +3 -3
- package/dist/src/runtime/runner.d.ts +102 -5
- package/dist/src/runtime/source-matcher.d.ts +2 -2
- package/dist/src/runtime/stats.d.ts +3 -3
- package/dist/src/schema/index.d.ts +46 -5
- package/dist/src/service.js +159 -16
- package/dist/src/service.js.map +7 -6
- package/dist/src/testing/index.d.ts +52 -5
- package/dist/src/testing/index.js +172 -6
- package/dist/src/testing/index.js.map +6 -5
- package/dist/src/triggers/index.d.ts +2 -2
- package/dist/src/types.d.ts +43 -2
- package/dist/src/validate.d.ts +43 -2
- 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 */
|
|
@@ -216,7 +244,7 @@ interface ContractCallFilter extends TraitScope, FactoryScope {
|
|
|
216
244
|
* definition and used at runtime to decode args by name. Omit to keep
|
|
217
245
|
* `event.args` as a positional `unknown[]`.
|
|
218
246
|
*/
|
|
219
|
-
abi?:
|
|
247
|
+
abi?: AbiContract2;
|
|
220
248
|
}
|
|
221
249
|
interface ContractDeployFilter {
|
|
222
250
|
type: "contract_deploy";
|
|
@@ -285,6 +313,13 @@ interface BlockMeta {
|
|
|
285
313
|
hash: string;
|
|
286
314
|
timestamp: number;
|
|
287
315
|
burnBlockHeight: number;
|
|
316
|
+
/**
|
|
317
|
+
* Nakamoto StacksBlockId — the identifier `?tip=` accepts, and the only way
|
|
318
|
+
* to pin a read-only call to THIS block. Null on rows ingested before it
|
|
319
|
+
* was persisted; `ctx.client.readOnly` refuses to run rather than silently
|
|
320
|
+
* reading at the node's tip.
|
|
321
|
+
*/
|
|
322
|
+
indexBlockHash?: string | null;
|
|
288
323
|
}
|
|
289
324
|
interface TxMeta2 {
|
|
290
325
|
txId: string;
|
|
@@ -324,7 +359,7 @@ type EventRecord = {
|
|
|
324
359
|
* Returns the payload the handler will receive.
|
|
325
360
|
*/
|
|
326
361
|
declare function buildEventPayload(filter: SubgraphFilter, tx: MatchedTx["tx"], event: MatchedTx["events"][0] | null): Record<string, unknown>;
|
|
327
|
-
import { AbiContract as
|
|
362
|
+
import { AbiContract as AbiContract3, ExtractFunctionArgs as ExtractFunctionArgs2, ExtractFunctionNames } from "@secondlayer/stacks/clarity";
|
|
328
363
|
/** Maps a ColumnType string literal to its TypeScript equivalent */
|
|
329
364
|
type ColumnToTS<T extends string> = T extends "uint" | "int" ? bigint : T extends "text" | "principal" | "timestamp" ? string : T extends "boolean" ? boolean : T extends "jsonb" ? Record<string, unknown> : unknown;
|
|
330
365
|
/** Infer TS type for a single column definition, respecting nullable */
|
|
@@ -396,8 +431,17 @@ interface TypedSubgraphContext<S extends SubgraphSchema> {
|
|
|
396
431
|
hash: string
|
|
397
432
|
timestamp: number
|
|
398
433
|
burnBlockHeight: number
|
|
434
|
+
/** Nakamoto StacksBlockId, when the block row carries one. */
|
|
435
|
+
indexBlockHash?: string | null
|
|
399
436
|
};
|
|
400
437
|
tx: TxMeta;
|
|
438
|
+
/**
|
|
439
|
+
* Read-only contract calls pinned to this block. Erased here for the same
|
|
440
|
+
* reason as on `SubgraphContext`: an ABI-generic method on a structurally
|
|
441
|
+
* compared interface exceeds TS's instantiation budget. Wrap it with
|
|
442
|
+
* `readContractAt(ctx, id, abi)` for full ABI typing at the call site.
|
|
443
|
+
*/
|
|
444
|
+
client: ErasedChainReadClient;
|
|
401
445
|
insert<T extends TableName<S>>(table: T, row: WriteRow<S[T]>): void;
|
|
402
446
|
update<T extends TableName<S>>(table: T, where: RowWhere<S, T>, set: Partial<WriteRow<S[T]>>): void;
|
|
403
447
|
upsert<
|
|
@@ -508,10 +552,10 @@ interface ContractDeployPayload {
|
|
|
508
552
|
* present for back-compat. Without an `abi`, the payload is {@link ContractCallEvent}.
|
|
509
553
|
*/
|
|
510
554
|
type ContractCallPayload<F> = F extends {
|
|
511
|
-
abi: infer A extends
|
|
555
|
+
abi: infer A extends AbiContract3
|
|
512
556
|
functionName: infer N extends string
|
|
513
557
|
} ? N extends ExtractFunctionNames<A> ? ContractCallEvent & {
|
|
514
|
-
input:
|
|
558
|
+
input: ExtractFunctionArgs2<A, N>
|
|
515
559
|
} : ContractCallEvent : ContractCallEvent;
|
|
516
560
|
/**
|
|
517
561
|
* Print event typed per topic. When a `print_event` source declares a `prints`
|
|
@@ -601,6 +645,9 @@ interface TestSubgraphContext<S extends SubgraphSchema> extends Omit<TypedSubgra
|
|
|
601
645
|
declare function createTestContext<const S extends SubgraphSchema>(schema: S, options?: {
|
|
602
646
|
block?: Partial<BlockMeta>
|
|
603
647
|
tx?: Partial<TxMeta2>
|
|
648
|
+
/** Stubbed `ctx.client.readOnly` results, keyed
|
|
649
|
+
* `"<contractId>.<function-name>"`. */
|
|
650
|
+
reads?: Record<string, unknown>
|
|
604
651
|
}): TestSubgraphContext<S>;
|
|
605
652
|
/**
|
|
606
653
|
* Build a handler-shaped event payload for a source, typed by that source's
|
|
@@ -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 =
|
|
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
|
|
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[
|
|
1076
|
+
input[toCamelCase2(arg.name)] = decodeArg(arg.type, deserializeCV3(clean));
|
|
936
1077
|
} catch {}
|
|
937
1078
|
});
|
|
938
1079
|
return input;
|
|
@@ -1210,6 +1351,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
1210
1351
|
}
|
|
1211
1352
|
|
|
1212
1353
|
// src/testing/index.ts
|
|
1354
|
+
import { normalizeAbi as normalizeAbi2, toCamelCase as toCamelCase3 } from "@secondlayer/stacks/clarity";
|
|
1213
1355
|
var TEST_SCHEMA_NAME = "subgraph_test";
|
|
1214
1356
|
function defaultBlock(overrides = {}) {
|
|
1215
1357
|
return {
|
|
@@ -1244,6 +1386,29 @@ class InMemorySubgraphContext extends SubgraphContext {
|
|
|
1244
1386
|
async rowsOf(table) {
|
|
1245
1387
|
return this.overlayMany(table, {}, await this.readRows(table, {}));
|
|
1246
1388
|
}
|
|
1389
|
+
setReads(reads) {
|
|
1390
|
+
this._client = {
|
|
1391
|
+
contract(contractId, abi) {
|
|
1392
|
+
const camelToKebab = new Map;
|
|
1393
|
+
for (const fn of normalizeAbi2(abi).functions) {
|
|
1394
|
+
camelToKebab.set(toCamelCase3(fn.name), fn.name);
|
|
1395
|
+
}
|
|
1396
|
+
const read = new Proxy({}, {
|
|
1397
|
+
get(_target, prop) {
|
|
1398
|
+
const fnName = camelToKebab.get(prop) ?? prop;
|
|
1399
|
+
const key = `${contractId}.${fnName}`;
|
|
1400
|
+
return async () => {
|
|
1401
|
+
if (!(key in reads)) {
|
|
1402
|
+
throw new Error(`No stubbed chain read for "${key}" — pass it via createTestContext(schema, { reads: { "${key}": … } }).`);
|
|
1403
|
+
}
|
|
1404
|
+
return reads[key];
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
return { read };
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1247
1412
|
async commitOps() {
|
|
1248
1413
|
const tables = new Set;
|
|
1249
1414
|
for (const op of this.ops)
|
|
@@ -1264,6 +1429,7 @@ function sameValue(a, b) {
|
|
|
1264
1429
|
}
|
|
1265
1430
|
function createTestContext(schema, options = {}) {
|
|
1266
1431
|
const impl = new InMemorySubgraphContext(schema, defaultBlock(options.block), defaultTx(options.tx));
|
|
1432
|
+
impl.setReads(options.reads ?? {});
|
|
1267
1433
|
const ctx = impl;
|
|
1268
1434
|
ctx.rows = (table) => impl.rowsOf(table);
|
|
1269
1435
|
ctx.commit = () => impl.commitOps();
|
|
@@ -1293,5 +1459,5 @@ export {
|
|
|
1293
1459
|
buildEvent
|
|
1294
1460
|
};
|
|
1295
1461
|
|
|
1296
|
-
//# debugId=
|
|
1462
|
+
//# debugId=FEF5AC4898A3E56764756E2164756E21
|
|
1297
1463
|
//# sourceMappingURL=index.js.map
|