@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,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?:
|
|
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 =
|
|
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;
|
|
@@ -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
|
|
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(
|
|
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(
|
|
1758
|
-
this.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: "",
|
|
@@ -2719,5 +2862,5 @@ export {
|
|
|
2719
2862
|
bumpReorgEpoch
|
|
2720
2863
|
};
|
|
2721
2864
|
|
|
2722
|
-
//# debugId=
|
|
2865
|
+
//# debugId=ADCDCD9F1956197464756E2164756E21
|
|
2723
2866
|
//# sourceMappingURL=catchup.js.map
|