@secondlayer/subgraphs 3.22.0 → 3.23.1
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 +6 -4
|
@@ -1,3 +1,62 @@
|
|
|
1
|
+
import { UnwrapResponse } from "@secondlayer/stacks/actions";
|
|
2
|
+
import { AbiContract, ExtractFunctionArgs, ExtractFunctionOutput, ExtractReadOnlyFunctions, ToCamelCase } from "@secondlayer/stacks/clarity";
|
|
3
|
+
/**
|
|
4
|
+
* How long a read stays valid.
|
|
5
|
+
*
|
|
6
|
+
* - `per-block` (default): pinned to the block being processed, cached under
|
|
7
|
+
* its `index_block_hash`. Sound for any read-only function — reindexing the
|
|
8
|
+
* same block always returns the same value, and a reorg-replaced block has a
|
|
9
|
+
* different id so it can't inherit the orphaned answer.
|
|
10
|
+
* - `contract-constant`: the caller asserts the value cannot change for this
|
|
11
|
+
* contract and these args (SIP-010 `get-decimals`/`get-symbol`), so it is
|
|
12
|
+
* resolved ONCE instead of once per block. This is the mode that makes a
|
|
13
|
+
* backfill affordable — under `per-block` every block is a distinct key and
|
|
14
|
+
* therefore a distinct RPC. Declaring it on a value that does change pins the
|
|
15
|
+
* first answer forever, so it is opt-in and never inferred.
|
|
16
|
+
*/
|
|
17
|
+
type ChainReadCacheMode = "per-block" | "contract-constant";
|
|
18
|
+
type ChainReadOptions = {
|
|
19
|
+
cache?: ChainReadCacheMode
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The chain-read surface, fully typed by an ABI.
|
|
23
|
+
*
|
|
24
|
+
* Shaped like `getContract` from `@secondlayer/stacks` — the same ABI in, the
|
|
25
|
+
* same camelCased read methods out — minus `call`/`buildCall`: a handler
|
|
26
|
+
* indexes the chain, it never writes to it.
|
|
27
|
+
*
|
|
28
|
+
* Reached through {@link readContractAt} rather than off `ctx.client`
|
|
29
|
+
* directly: an ABI-generic method living on the context interface makes every
|
|
30
|
+
* structural comparison of a subgraph definition instantiate it, which exceeds
|
|
31
|
+
* TS's depth budget. At a call site the ABI is already concrete and it costs
|
|
32
|
+
* nothing.
|
|
33
|
+
*/
|
|
34
|
+
interface ChainReadClient {
|
|
35
|
+
/**
|
|
36
|
+
* Read-only methods of a contract, evaluated against the exact block being
|
|
37
|
+
* processed.
|
|
38
|
+
*
|
|
39
|
+
* Every call is pinned to the block's `index_block_hash`, so a handler is a
|
|
40
|
+
* pure function of its block: reindexing a range produces byte-identical
|
|
41
|
+
* rows. Reading at the node's tip instead would make the same reindex
|
|
42
|
+
* return different values — that is the corruption this refuses to allow,
|
|
43
|
+
* so a block whose id was never persisted throws rather than falling back.
|
|
44
|
+
*
|
|
45
|
+
* Results are memoized in Postgres, so a value is fetched once per block
|
|
46
|
+
* rather than once per event (or once per contract with
|
|
47
|
+
* `cache: "contract-constant"`).
|
|
48
|
+
*/
|
|
49
|
+
contract<const C extends AbiContract>(contractId: string, abi: C, options?: ChainReadOptions): {
|
|
50
|
+
read: ChainReadMethods<C>
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The camelCased read methods of an ABI. Spelled out rather than reused from
|
|
55
|
+
* `ContractInstance<C>["read"]`: that type is a conditional over the codegen
|
|
56
|
+
* brand, and evaluating it wherever a subgraph definition is structurally
|
|
57
|
+
* compared pushes TS past its instantiation depth.
|
|
58
|
+
*/
|
|
59
|
+
type ChainReadMethods<C extends AbiContract> = { [N in ExtractReadOnlyFunctions<C> as ToCamelCase<N>] : (args: ExtractFunctionArgs<C, N>) => Promise<UnwrapResponse<ExtractFunctionOutput<C, N>>> };
|
|
1
60
|
/** Supported column types for subgraph schemas */
|
|
2
61
|
type ColumnType = "text" | "uint" | "int" | "principal" | "boolean" | "timestamp" | "jsonb";
|
|
3
62
|
/** Column definition in a subgraph table */
|
|
@@ -49,9 +108,9 @@ type SubgraphSchema = Record<string, SubgraphTable>;
|
|
|
49
108
|
type RowValue = string | number | bigint | boolean | null | undefined | Record<string, unknown> | unknown[];
|
|
50
109
|
/** Value or computed function that receives existing row */
|
|
51
110
|
type ComputedValue = RowValue | ((existing: Record<string, unknown> | null) => unknown);
|
|
52
|
-
import { Database } from "@secondlayer/shared/db";
|
|
53
|
-
import { Kysely, Transaction } from "kysely";
|
|
54
|
-
type AnyDb =
|
|
111
|
+
import { Database as Database2 } from "@secondlayer/shared/db";
|
|
112
|
+
import { Kysely as Kysely2, Transaction } from "kysely";
|
|
113
|
+
type AnyDb = Kysely2<Database2> | Transaction<Database2>;
|
|
55
114
|
/** Reorg journal entries older than this many blocks are prunable — far past
|
|
56
115
|
* Stacks finality (observed reorg depth is single digits). */
|
|
57
116
|
declare const JOURNAL_RETENTION_BLOCKS = 300;
|
|
@@ -83,6 +142,13 @@ interface BlockMeta {
|
|
|
83
142
|
hash: string;
|
|
84
143
|
timestamp: number;
|
|
85
144
|
burnBlockHeight: number;
|
|
145
|
+
/**
|
|
146
|
+
* Nakamoto StacksBlockId — the identifier `?tip=` accepts, and the only way
|
|
147
|
+
* to pin a read-only call to THIS block. Null on rows ingested before it
|
|
148
|
+
* was persisted; `ctx.client.readOnly` refuses to run rather than silently
|
|
149
|
+
* reading at the node's tip.
|
|
150
|
+
*/
|
|
151
|
+
indexBlockHash?: string | null;
|
|
86
152
|
}
|
|
87
153
|
interface TxMeta2 {
|
|
88
154
|
txId: string;
|
|
@@ -127,7 +193,15 @@ declare class SubgraphContext2 {
|
|
|
127
193
|
* journal would just be churn the pruner deletes.
|
|
128
194
|
*/
|
|
129
195
|
private readonly journal;
|
|
196
|
+
/** Memoized per block; see the `client` getter. */
|
|
197
|
+
protected _client?: ChainReadClient;
|
|
130
198
|
constructor(db: AnyDb, pgSchemaName: string, subgraphSchema: SubgraphSchema, block: BlockMeta, tx: TxMeta2, byo?: boolean, journal?: boolean);
|
|
199
|
+
/**
|
|
200
|
+
* Chain reads pinned to THIS block. Lazily built so a handler that never
|
|
201
|
+
* reads never touches the node config, and so the in-memory test context
|
|
202
|
+
* can override it without a node.
|
|
203
|
+
*/
|
|
204
|
+
get client(): ChainReadClient;
|
|
131
205
|
get tx(): TxMeta2;
|
|
132
206
|
/** Update the current transaction context (used by runner between events) */
|
|
133
207
|
setTx(tx: TxMeta2): 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
|
}
|
|
@@ -736,5 +877,5 @@ export {
|
|
|
736
877
|
JOURNAL_RETENTION_BLOCKS
|
|
737
878
|
};
|
|
738
879
|
|
|
739
|
-
//# debugId=
|
|
880
|
+
//# debugId=943E34F234EEBFC564756E2164756E21
|
|
740
881
|
//# sourceMappingURL=context.js.map
|