@secondlayer/subgraphs 4.4.1 → 5.0.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 (64) hide show
  1. package/README.md +13 -21
  2. package/dist/{src/index.d.ts → index.d.ts} +59 -189
  3. package/dist/index.js +2151 -0
  4. package/dist/index.js.map +24 -0
  5. package/dist/{src/runtime → runtime}/emitter.d.ts +1 -1
  6. package/dist/{src/runtime → runtime}/emitter.js +8 -12
  7. package/dist/{src/runtime → runtime}/emitter.js.map +3 -3
  8. package/dist/{src/runtime → runtime}/replay.d.ts +1 -1
  9. package/dist/{src/runtime → runtime}/replay.js +342 -683
  10. package/dist/{src/runtime → runtime}/replay.js.map +5 -5
  11. package/dist/{src/schema → schema}/index.d.ts +10 -69
  12. package/dist/{src/schema → schema}/index.js +188 -281
  13. package/dist/schema/index.js.map +13 -0
  14. package/dist/{src/testing → testing}/index.d.ts +65 -115
  15. package/dist/{src/testing → testing}/index.js +554 -968
  16. package/dist/testing/index.js.map +21 -0
  17. package/dist/{src/types.d.ts → types.d.ts} +10 -66
  18. package/dist/{src/validate.d.ts → validate.d.ts} +11 -69
  19. package/dist/{src/validate.js → validate.js} +18 -98
  20. package/dist/validate.js.map +10 -0
  21. package/package.json +18 -26
  22. package/dist/src/index.js +0 -6081
  23. package/dist/src/index.js.map +0 -44
  24. package/dist/src/runtime/block-processor.d.ts +0 -559
  25. package/dist/src/runtime/block-processor.js +0 -3810
  26. package/dist/src/runtime/block-processor.js.map +0 -31
  27. package/dist/src/runtime/catchup.d.ts +0 -473
  28. package/dist/src/runtime/catchup.js +0 -4228
  29. package/dist/src/runtime/catchup.js.map +0 -35
  30. package/dist/src/runtime/clarity.d.ts +0 -15
  31. package/dist/src/runtime/clarity.js +0 -41
  32. package/dist/src/runtime/clarity.js.map +0 -10
  33. package/dist/src/runtime/context.d.ts +0 -325
  34. package/dist/src/runtime/context.js +0 -936
  35. package/dist/src/runtime/context.js.map +0 -13
  36. package/dist/src/runtime/processor.d.ts +0 -11
  37. package/dist/src/runtime/processor.js +0 -5372
  38. package/dist/src/runtime/processor.js.map +0 -40
  39. package/dist/src/runtime/reindex.d.ts +0 -539
  40. package/dist/src/runtime/reindex.js +0 -4802
  41. package/dist/src/runtime/reindex.js.map +0 -37
  42. package/dist/src/runtime/reorg.d.ts +0 -432
  43. package/dist/src/runtime/reorg.js +0 -4355
  44. package/dist/src/runtime/reorg.js.map +0 -36
  45. package/dist/src/runtime/runner.d.ts +0 -719
  46. package/dist/src/runtime/runner.js +0 -1332
  47. package/dist/src/runtime/runner.js.map +0 -16
  48. package/dist/src/runtime/source-matcher.d.ts +0 -304
  49. package/dist/src/runtime/source-matcher.js +0 -335
  50. package/dist/src/runtime/source-matcher.js.map +0 -10
  51. package/dist/src/runtime/stats.d.ts +0 -23
  52. package/dist/src/runtime/stats.js +0 -71
  53. package/dist/src/runtime/stats.js.map +0 -10
  54. package/dist/src/schema/index.js.map +0 -13
  55. package/dist/src/service.d.ts +0 -0
  56. package/dist/src/service.js +0 -5398
  57. package/dist/src/service.js.map +0 -41
  58. package/dist/src/testing/index.js.map +0 -22
  59. package/dist/src/triggers/index.d.ts +0 -381
  60. package/dist/src/triggers/index.js +0 -31
  61. package/dist/src/triggers/index.js.map +0 -10
  62. package/dist/src/validate.js.map +0 -10
  63. /package/dist/{src/types.js → types.js} +0 -0
  64. /package/dist/{src/types.js.map → types.js.map} +0 -0
@@ -1,719 +0,0 @@
1
- import { AbiContract as AbiContract2 } from "@secondlayer/stacks/clarity";
2
- import { AssetIdentifier } from "@secondlayer/stacks/filters";
3
- import { UnwrapResponse } from "@secondlayer/stacks/actions";
4
- import { AbiContract, ExtractFunctionArgs, ExtractFunctionOutput, ExtractReadOnlyFunctions, ToCamelCase } from "@secondlayer/stacks/clarity";
5
- /**
6
- * How long a read stays valid.
7
- *
8
- * - `per-block` (default): pinned to the block being processed, cached under
9
- * its `index_block_hash`. Sound for any read-only function — reindexing the
10
- * same block always returns the same value, and a reorg-replaced block has a
11
- * different id so it can't inherit the orphaned answer.
12
- * - `contract-constant`: the caller asserts the value cannot change for this
13
- * contract and these args (SIP-010 `get-decimals`/`get-symbol`), so it is
14
- * resolved ONCE instead of once per block. This is the mode that makes a
15
- * backfill affordable — under `per-block` every block is a distinct key and
16
- * therefore a distinct RPC. Declaring it on a value that does change pins the
17
- * first answer forever, so it is opt-in and never inferred.
18
- */
19
- type ChainReadCacheMode = "per-block" | "contract-constant";
20
- type ChainReadOptions = {
21
- cache?: ChainReadCacheMode
22
- };
23
- /**
24
- * The chain-read surface, fully typed by an ABI.
25
- *
26
- * Shaped like `getContract` from `@secondlayer/stacks` — the same ABI in, the
27
- * same camelCased read methods out — minus `call`/`buildCall`: a handler
28
- * indexes the chain, it never writes to it.
29
- *
30
- * Reached through {@link readContractAt} rather than off `ctx.client`
31
- * directly: an ABI-generic method living on the context interface makes every
32
- * structural comparison of a subgraph definition instantiate it, which exceeds
33
- * TS's depth budget. At a call site the ABI is already concrete and it costs
34
- * nothing.
35
- */
36
- interface ChainReadClient {
37
- /**
38
- * Read-only methods of a contract, evaluated against the exact block being
39
- * processed.
40
- *
41
- * Every call is pinned to the block's `index_block_hash`, so a handler is a
42
- * pure function of its block: reindexing a range produces byte-identical
43
- * rows. Reading at the node's tip instead would make the same reindex
44
- * return different values — that is the corruption this refuses to allow,
45
- * so a block whose id was never persisted throws rather than falling back.
46
- *
47
- * Results are memoized in Postgres, so a value is fetched once per block
48
- * rather than once per event (or once per contract with
49
- * `cache: "contract-constant"`).
50
- */
51
- contract<const C extends AbiContract>(contractId: string, abi: C, options?: ChainReadOptions): {
52
- read: ChainReadMethods<C>
53
- };
54
- }
55
- /**
56
- * True for a structural empty mapped type (`{}`) and for codegen's
57
- * `Record<string, never>`. `keyof Record<string, never>` is `string`, so the
58
- * first check alone is not enough. Local copy of the `getContract` helper —
59
- * importing `ContractInstance<C>["read"]` would re-enter the codegen-brand
60
- * conditional this type exists to avoid.
61
- */
62
- type IsEmptyArgs<A> = [keyof A & string] extends [never] ? true : Record<string, never> extends A ? true : false;
63
- type ChainReadMethod<
64
- A,
65
- R
66
- > = IsEmptyArgs<A> extends true ? (args?: A) => R : (args: A) => R;
67
- /**
68
- * The camelCased read methods of an ABI. Spelled out rather than reused from
69
- * `ContractInstance<C>["read"]`: that type is a conditional over the codegen
70
- * brand, and evaluating it wherever a subgraph definition is structurally
71
- * compared pushes TS past its instantiation depth.
72
- */
73
- type ChainReadMethods<C extends AbiContract> = { [N in ExtractReadOnlyFunctions<C> as ToCamelCase<N>] : ChainReadMethod<ExtractFunctionArgs<C, N>, Promise<UnwrapResponse<ExtractFunctionOutput<C, N>>>> };
74
- /**
75
- * `ChainReadClient` with the ABI generic erased. Structurally cheap, so the
76
- * base `SubgraphContext` can carry it; `TypedSubgraphContext` uses the real
77
- * one and handlers get full inference.
78
- */
79
- interface ErasedChainReadClient {
80
- contract(contractId: string, abi: unknown, options?: ChainReadOptions): {
81
- read: Record<string, (args?: Record<string, unknown>) => Promise<unknown>>
82
- };
83
- }
84
- /** Supported column types for subgraph schemas */
85
- type ColumnType = "text" | "uint" | "int" | "principal" | "boolean" | "timestamp" | "jsonb";
86
- /** Column definition in a subgraph table */
87
- interface SubgraphColumn {
88
- type: ColumnType;
89
- nullable?: boolean;
90
- indexed?: boolean;
91
- search?: boolean;
92
- default?: string | number | boolean;
93
- }
94
- /**
95
- * A foreign-key relation to another table in the same subgraph. Drives DDL FK
96
- * constraints and ORM codegen (`@relation` in Prisma, `relations()` in Drizzle)
97
- * so generated clients get typed joins. The referenced columns must form a
98
- * `uniqueKeys` entry on the target table.
99
- */
100
- interface SubgraphRelation {
101
- /** Relation field name on this table's generated model (e.g. "pool"). */
102
- name: string;
103
- /** Target table name in this subgraph. */
104
- references: string;
105
- /** Local column(s) holding the foreign key. */
106
- fields: string[];
107
- /** Target column(s) the fields point at (a uniqueKeys entry on the target). */
108
- referencedColumns: string[];
109
- }
110
- /**
111
- * Table definition within a subgraph schema.
112
- *
113
- * The array fields are `readonly` so a schema literal can be hoisted out of
114
- * the `defineSubgraph()` call with `as const` (or {@link defineSchema}) and
115
- * still typecheck — `as const` produces `readonly [readonly ["holder"]]`,
116
- * which a mutable `string[][]` rejects. Without that, extracting a helper
117
- * that takes `ctx` meant losing the entire typed surface, which is why the
118
- * shipped starter reached for `ctx: any`.
119
- */
120
- interface SubgraphTable {
121
- columns: Record<string, SubgraphColumn>;
122
- /** Composite indexes (each entry is an array of column names) */
123
- indexes?: readonly (readonly string[])[];
124
- /** Unique key constraints (each entry is an array of column names). Required for upsert. */
125
- uniqueKeys?: readonly (readonly string[])[];
126
- /** Foreign-key relations to other tables (for typed ORM joins). */
127
- relations?: readonly SubgraphRelation[];
128
- }
129
- /** Subgraph schema — maps table names to table definitions */
130
- type SubgraphSchema = Record<string, SubgraphTable>;
131
- /** STX event filters */
132
- interface StxTransferFilter {
133
- type: "stx_transfer";
134
- sender?: string;
135
- recipient?: string;
136
- minAmount?: bigint;
137
- maxAmount?: bigint;
138
- }
139
- interface StxMintFilter {
140
- type: "stx_mint";
141
- recipient?: string;
142
- minAmount?: bigint;
143
- }
144
- interface StxBurnFilter {
145
- type: "stx_burn";
146
- sender?: string;
147
- minAmount?: bigint;
148
- }
149
- interface StxLockFilter {
150
- type: "stx_lock";
151
- lockedAddress?: string;
152
- minAmount?: bigint;
153
- }
154
- /**
155
- * Restrict a source to contracts conforming to a trait/standard (e.g. "sip-010")
156
- * instead of a fixed contract — resolved from the contract registry at match time,
157
- * as-of each processed block. Lets a source index "all SIP-010 tokens" etc.
158
- */
159
- type TraitScope = {
160
- trait?: string
161
- };
162
- /**
163
- * Scope a source to a DYNAMIC address set discovered from another source's
164
- * events, instead of a fixed contract list.
165
- *
166
- * The standard shape it unlocks: a router plus pools created after you
167
- * deploy. `contractId` arrays cover a set you know at authoring time; a
168
- * factory covers the set that grows. It is deliberately NOT tied to contract
169
- * deployment — the address can come from any field of any event, so it also
170
- * covers launchpad-minted tokens, DAO extension allowlists, and registry
171
- * entries.
172
- *
173
- * ```ts
174
- * sources: {
175
- * registry: {
176
- * type: "print_event",
177
- * contractId: REGISTRY,
178
- * topic: "pool-created",
179
- * prints: { "pool-created": { pool: "principal" } },
180
- * },
181
- * swaps: {
182
- * type: "print_event",
183
- * topic: "swap",
184
- * // every pool the registry has ever announced, including new ones
185
- * factory: { from: "registry", field: "data.pool" },
186
- * },
187
- * }
188
- * ```
189
- *
190
- * Two guarantees the runtime provides:
191
- * - **Same-block ordering** — a contract discovered in block N receives its
192
- * own block-N events. Discovery runs before matching, within the block.
193
- * - **Reorg-scoped** — the discovered set is stamped with the block that
194
- * revealed it and rolled back with everything else on a fork, so an address
195
- * announced on an orphaned chain does not linger forever.
196
- */
197
- interface FactoryScope {
198
- factory?: {
199
- /** Source name whose events reveal the addresses. */
200
- from: string
201
- /** Dotted path to the address on that source's payload
202
- * (e.g. `"data.pool"`, `"contractId"`, `"data.token.id"`). */
203
- field: string
204
- };
205
- }
206
- /** FT event filters */
207
- interface FtTransferFilter extends TraitScope {
208
- type: "ft_transfer";
209
- assetIdentifier?: AssetIdentifier;
210
- sender?: string;
211
- recipient?: string;
212
- minAmount?: bigint;
213
- }
214
- interface FtMintFilter extends TraitScope {
215
- type: "ft_mint";
216
- assetIdentifier?: AssetIdentifier;
217
- recipient?: string;
218
- minAmount?: bigint;
219
- }
220
- interface FtBurnFilter extends TraitScope {
221
- type: "ft_burn";
222
- assetIdentifier?: AssetIdentifier;
223
- sender?: string;
224
- minAmount?: bigint;
225
- }
226
- /** NFT event filters */
227
- interface NftTransferFilter extends TraitScope {
228
- type: "nft_transfer";
229
- assetIdentifier?: AssetIdentifier;
230
- sender?: string;
231
- recipient?: string;
232
- }
233
- interface NftMintFilter extends TraitScope {
234
- type: "nft_mint";
235
- assetIdentifier?: AssetIdentifier;
236
- recipient?: string;
237
- }
238
- interface NftBurnFilter extends TraitScope {
239
- type: "nft_burn";
240
- assetIdentifier?: AssetIdentifier;
241
- sender?: string;
242
- }
243
- /** Contract event filters */
244
- interface ContractCallFilter extends TraitScope, FactoryScope {
245
- type: "contract_call";
246
- /**
247
- * One contract id, or a SET of them (max 20) — the standard DeFi shape is
248
- * a router plus N pools, which used to cost N near-identical sources and
249
- * handlers, or the `"*"` catch-all that erases per-source typing. Payload
250
- * types are identical per `type`, so one handler serves the set and
251
- * `event.contractId` disambiguates.
252
- *
253
- * Supports `*` wildcards (e.g. `"SP1ABC….pool-*"`). Mutually exclusive
254
- * with `trait`, which resolves its own set per block.
255
- */
256
- contractId?: string | readonly string[];
257
- functionName?: string;
258
- caller?: string;
259
- /**
260
- * Contract ABI (pass it `as const`) used to type `event.input` — the named,
261
- * decoded function arguments. Dev-provided; serialized into the deployed
262
- * definition and used at runtime to decode args by name. Omit to keep
263
- * `event.args` as a positional `unknown[]`.
264
- *
265
- * Deploy validation requires `abi` whenever `functionName` is set
266
- * (`normalizeAbi()` from `@secondlayer/stacks/clarity`).
267
- */
268
- abi?: AbiContract2;
269
- }
270
- interface ContractDeployFilter {
271
- type: "contract_deploy";
272
- deployer?: string;
273
- contractName?: string;
274
- }
275
- interface NestedContractCallFilter extends TraitScope, FactoryScope {
276
- type: "nested_contract_call";
277
- contractId?: string | readonly string[];
278
- functionName?: string;
279
- caller?: string;
280
- sender?: string;
281
- }
282
- interface VarSetFilter extends TraitScope, FactoryScope {
283
- type: "var_set";
284
- contractId?: string | readonly string[];
285
- varName?: string;
286
- }
287
- interface MapWriteFilter extends TraitScope, FactoryScope {
288
- type: "map_set" | "map_insert" | "map_delete";
289
- contractId?: string | readonly string[];
290
- map?: string;
291
- }
292
- /**
293
- * One declared field of a print payload.
294
- *
295
- * A bare {@link ColumnType} is a required scalar (`amount: "uint"`). The
296
- * composite forms exist because real print payloads are not flat: BNS-V2
297
- * emits `name` as a NESTED tuple, and a `prints` vocabulary that could only
298
- * say `"jsonb"` is why a handler reading flat `data.name` type-checked,
299
- * deployed, and then decoded to null on every event.
300
- *
301
- * - `{ type: F, optional: true }` — the KEY becomes optional on `event.data`
302
- * (mirrors `always_present: false` in the inferred schema).
303
- * - `{ tuple: { … } }` — a nested tuple, typed all the way down.
304
- * - `{ list: F }` — a list of `F`.
305
- */
306
- type PrintField = ColumnType | {
307
- type: PrintField
308
- optional: true
309
- } | {
310
- tuple: Record<string, PrintField>
311
- } | {
312
- list: PrintField
313
- };
314
- /**
315
- * Per-topic field schema for a print_event source. When declared, the handler's
316
- * `event` is a discriminated union keyed by `topic` and `event.data` is typed
317
- * per topic. Nested tuples, lists, and optional fields — see {@link PrintField}.
318
- *
319
- * Also opts the source into RUNTIME validation: a payload that does not match
320
- * is skipped and logged rather than written as nulls.
321
- */
322
- type PrintEventPrints = Record<string, Record<string, PrintField>>;
323
- /**
324
- * One column in a static identity map. Literals only — the bundler never
325
- * executes user code, so `{ from: "tokenX" }` is valid and `from("tokenX")`
326
- * is not.
327
- */
328
- type MaterializeColumn = {
329
- from: string
330
- } | {
331
- fromTx: "sender" | "txId"
332
- } | {
333
- fromBlock: "height" | "hash" | "timestamp"
334
- };
335
- /** Static insert from a source event — replaces a 1:1 handler. */
336
- interface MaterializeSpec {
337
- table: string;
338
- /** Keys = schema column names (snake). */
339
- columns: Record<string, MaterializeColumn>;
340
- }
341
- type PrintEventFields = FactoryScope & {
342
- type: "print_event"
343
- topic?: string
344
- /** Identity map: insert without a handler function. XOR with handlers[name]. */
345
- materialize?: MaterializeSpec
346
- };
347
- /**
348
- * Pinned to one or more contract ids with no trait — `prints` is required so
349
- * `event.data` is typed and empty-table handlers cannot typecheck.
350
- */
351
- type PinnedPrintEventFilter = PrintEventFields & {
352
- contractId: string | readonly string[]
353
- trait?: undefined
354
- prints: PrintEventPrints
355
- };
356
- /**
357
- * Trait-scoped print source (optionally narrowed by contractId). No single
358
- * schema — `prints` stays optional.
359
- */
360
- type TraitPrintEventFilter = PrintEventFields & {
361
- contractId?: string | readonly string[]
362
- trait: string
363
- prints?: PrintEventPrints
364
- };
365
- /**
366
- * Unpinned / factory-only print source (no contractId). `prints` optional.
367
- */
368
- type UnpinnedPrintEventFilter = PrintEventFields & {
369
- contractId?: undefined
370
- trait?: string
371
- prints?: PrintEventPrints
372
- };
373
- /**
374
- * Print-event filter. Pinned sources (contractId set, no trait) require
375
- * `prints`; trait and unpinned sources may omit it.
376
- */
377
- type PrintEventFilter = PinnedPrintEventFilter | TraitPrintEventFilter | UnpinnedPrintEventFilter;
378
- /** All subgraph filter types — discriminated on `type` */
379
- type SubgraphFilter = StxTransferFilter | StxMintFilter | StxBurnFilter | StxLockFilter | FtTransferFilter | FtMintFilter | FtBurnFilter | NftTransferFilter | NftMintFilter | NftBurnFilter | ContractCallFilter | ContractDeployFilter | PrintEventFilter | NestedContractCallFilter | VarSetFilter | MapWriteFilter;
380
- /** Transaction metadata available in handlers */
381
- interface TxMeta {
382
- txId: string;
383
- sender: string;
384
- type: string;
385
- status: string;
386
- contractId?: string | null;
387
- functionName?: string | null;
388
- }
389
- /** Scalar types that can appear as row values */
390
- type RowValue = string | number | bigint | boolean | null | undefined | Record<string, unknown> | unknown[];
391
- /** Value or computed function that receives existing row */
392
- type ComputedValue = RowValue | ((existing: Record<string, unknown> | null) => unknown);
393
- /** Context passed to subgraph handlers during event processing */
394
- interface SubgraphContext {
395
- block: {
396
- height: number
397
- hash: string
398
- timestamp: number
399
- burnBlockHeight: number
400
- /** Nakamoto StacksBlockId, when the block row carries one. */
401
- indexBlockHash?: string | null
402
- };
403
- tx: TxMeta;
404
- /**
405
- * Read-only contract calls pinned to this block. Cached, so the same
406
- * (contract, function, args) is fetched once per block rather than once
407
- * per event.
408
- *
409
- * Erased here on purpose: this interface is compared structurally wherever
410
- * a subgraph definition is passed around, and carrying an ABI-generic
411
- * method pushes those comparisons past TS's instantiation depth. Handlers
412
- * receive `TypedSubgraphContext`, which exposes the fully typed version.
413
- */
414
- client: ErasedChainReadClient;
415
- insert(table: string, row: Record<string, unknown>): void;
416
- update(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;
417
- upsert(table: string, key: Record<string, unknown>, row: Record<string, unknown>): void;
418
- delete(table: string, where: Record<string, unknown>): void;
419
- /**
420
- * Atomic counter update — the blessed accumulator primitive. Applies
421
- * `col = COALESCE(col, 0) + delta` per column (insert-or-add); deltas may
422
- * be negative. Requires a uniqueKeys constraint matching `key`. Prefer
423
- * this over patchOrInsert with functional updaters for running totals.
424
- */
425
- increment(table: string, key: Record<string, unknown>, deltas: Record<string, bigint | number>): void;
426
- /** Partial update — sets only specified fields, preserves others */
427
- patch(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;
428
- /** Find-then-merge-or-insert. Values can be functions: (existing) => newValue */
429
- patchOrInsert(table: string, key: Record<string, unknown>, row: Record<string, ComputedValue>): Promise<void>;
430
- findOne(table: string, where: Record<string, unknown>): Promise<Record<string, unknown> | null>;
431
- findMany(table: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
432
- /** Format a bigint amount with decimal places */
433
- formatUnits(value: bigint, decimals: number): string;
434
- /** Count rows matching filter */
435
- count(table: string, where?: Record<string, unknown>): Promise<number>;
436
- /** Sum a numeric column */
437
- sum(table: string, column: string, where?: Record<string, unknown>): Promise<bigint>;
438
- /** Min of a numeric column */
439
- min(table: string, column: string, where?: Record<string, unknown>): Promise<bigint | null>;
440
- /** Max of a numeric column */
441
- max(table: string, column: string, where?: Record<string, unknown>): Promise<bigint | null>;
442
- /** Count distinct values in a column */
443
- countDistinct(table: string, column: string, where?: Record<string, unknown>): Promise<number>;
444
- }
445
- /** Handler function that processes events and writes to the subgraph */
446
- type SubgraphHandler = (event: Record<string, unknown>, ctx: SubgraphContext) => Promise<void> | void;
447
- /** Complete subgraph definition */
448
- interface SubgraphDefinition {
449
- /** Unique subgraph name (lowercase, alphanumeric + hyphens) */
450
- name: string;
451
- /** Semantic version */
452
- version?: string;
453
- /** Human description */
454
- description?: string;
455
- /** Block height to start indexing from (default: 1) */
456
- startBlock?: number;
457
- /** 'concurrent' = tip-first: live at tip now, history backfills behind.
458
- * Requires order-tolerant handlers. Default 'blocking'. */
459
- backfillMode?: "blocking" | "concurrent";
460
- /** Named source filters — keys become handler keys */
461
- sources: Record<string, SubgraphFilter>;
462
- /** Tables in this subgraph */
463
- schema: SubgraphSchema;
464
- /** Handler functions — keys must match source names (or "*" for catch-all) */
465
- handlers: Record<string, SubgraphHandler>;
466
- }
467
- interface MatchedTx {
468
- tx: TxRecord;
469
- events: EventRecord[];
470
- /** Source object key — used for handler dispatch */
471
- sourceName: string;
472
- }
473
- type TxRecord = {
474
- tx_id: string
475
- type: string
476
- sender: string
477
- status: string
478
- /** Position within the block — the runner sorts dispatch into chain order. */
479
- tx_index?: number
480
- contract_id?: string | null
481
- function_name?: string | null
482
- function_args?: unknown | null
483
- raw_result?: string | null
484
- };
485
- type EventRecord = {
486
- id: string
487
- tx_id: string
488
- type: string
489
- /** Classic `event_index`, or `ordinal` when `clock === "vm"`. */
490
- event_index: number
491
- data: unknown
492
- /** Set on opt-in `vm_events` rows. Two clocks never sort or dedupe together. */
493
- clock?: "vm"
494
- };
495
- import { Database as Database2 } from "@secondlayer/shared/db";
496
- import { Kysely as Kysely2, Transaction } from "kysely";
497
- type AnyDb = Kysely2<Database2> | Transaction<Database2>;
498
- interface WriteOp {
499
- kind: "insert" | "update" | "delete" | "increment";
500
- table: string;
501
- data: Record<string, unknown>;
502
- /** For update: SET clause. For increment: column → signed delta. */
503
- set?: Record<string, unknown>;
504
- }
505
- interface FlushWrite {
506
- op: "insert" | "update" | "delete";
507
- table: string;
508
- /** Full row data (for inserts) or where+set merged (for updates). Bigints stringified. */
509
- row: Record<string, unknown>;
510
- /** Stable identifier for dedup — `{blockHeight, txId, rowIndex}` */
511
- pk: {
512
- blockHeight: number
513
- txId: string
514
- rowIndex: number
515
- };
516
- }
517
- interface FlushManifest {
518
- count: number;
519
- writes: FlushWrite[];
520
- }
521
- interface BlockMeta {
522
- height: number;
523
- hash: string;
524
- timestamp: number;
525
- burnBlockHeight: number;
526
- /**
527
- * Nakamoto StacksBlockId — the identifier `?tip=` accepts, and the only way
528
- * to pin a read-only call to THIS block. Null on rows ingested before it
529
- * was persisted; `ctx.client.readOnly` refuses to run rather than silently
530
- * reading at the node's tip.
531
- */
532
- indexBlockHash?: string | null;
533
- }
534
- interface TxMeta2 {
535
- txId: string;
536
- sender: string;
537
- type: string;
538
- status: string;
539
- contractId?: string | null;
540
- functionName?: string | null;
541
- }
542
- /**
543
- * Runtime context passed to subgraph handlers.
544
- * Batches writes and flushes them atomically at the end of a block.
545
- *
546
- * Row reads (findOne/findMany) are read-your-writes: they overlay the pending
547
- * ops queue on the DB state, so a handler observes every write queued earlier
548
- * in the same block. Without this, accumulator patterns (balance = f(existing))
549
- * silently lose all but the last same-block delta per row (fix-f040 B1).
550
- * Aggregate reads (count/sum/min/max) remain pre-flush DB state.
551
- */
552
- declare class SubgraphContext2 {
553
- readonly block: BlockMeta;
554
- private _tx;
555
- private readonly db;
556
- private readonly pgSchemaName;
557
- private readonly subgraphSchema;
558
- /** Pending writes for this block. `protected` so the in-memory test
559
- * context (`@secondlayer/subgraphs/testing`) can materialize them through
560
- * the SAME overlay this class uses — never a second implementation. */
561
- protected readonly ops: WriteOp[];
562
- /**
563
- * Record pre-images of keyed mutations into the schema's `_journal` so a
564
- * reorg can restore prior row states (fix-f040 B2). Enabled on the live
565
- * path only — deep reindex/backfill heights are past finality, and the
566
- * journal would just be churn the pruner deletes.
567
- */
568
- private readonly journal;
569
- /** Memoized per block; see the `client` getter. */
570
- protected _client?: ChainReadClient;
571
- constructor(db: AnyDb, pgSchemaName: string, subgraphSchema: SubgraphSchema, block: BlockMeta, tx: TxMeta2, journal?: boolean);
572
- /**
573
- * Chain reads pinned to THIS block. Lazily built so a handler that never
574
- * reads never touches the node config, and so the in-memory test context
575
- * can override it without a node.
576
- */
577
- get client(): ChainReadClient;
578
- get tx(): TxMeta2;
579
- /** Update the current transaction context (used by runner between events) */
580
- setTx(tx: TxMeta2): void;
581
- insert(table: string, row: Record<string, unknown>): void;
582
- update(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;
583
- upsert(table: string, key: Record<string, unknown>, row: Record<string, unknown>): void;
584
- delete(table: string, where: Record<string, unknown>): void;
585
- /**
586
- * Atomic counter update — the blessed accumulator primitive. Compiles to
587
- * `INSERT ... ON CONFLICT (key) DO UPDATE SET col = COALESCE(t.col,0) + delta`,
588
- * so deltas commute: same-block, replayed-in-order, and concurrent updates
589
- * all land correctly without read-modify-write. Missing row inserts the
590
- * delta as the initial value. Requires a uniqueKeys constraint matching
591
- * `key`; deltas may be negative.
592
- */
593
- increment(table: string, key: Record<string, unknown>, deltas: Record<string, bigint | number>): void;
594
- /** Current length of the pending-ops queue. Pair with {@link rollbackTo}. */
595
- opsCheckpoint(): number;
596
- /**
597
- * Discard ops queued after a checkpoint. The runner wraps each handler
598
- * invocation so a thrown handler contributes nothing — without this, a
599
- * transfer handler that debited then threw flushes a one-sided debit
600
- * (fix-f040 B6).
601
- */
602
- rollbackTo(checkpoint: number): void;
603
- /** Partial update — sets only specified fields, preserves everything else */
604
- patch(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;
605
- /**
606
- * Find-then-merge-or-insert. Values can be functions: (existing) => newValue.
607
- * Async because it reads existing row first.
608
- */
609
- patchOrInsert(table: string, key: Record<string, unknown>, row: Record<string, ComputedValue>): Promise<void>;
610
- /** Format a bigint amount with decimal places */
611
- formatUnits(value: bigint, decimals: number): string;
612
- /**
613
- * Committed rows matching `where`, BEFORE the pending-ops overlay. The one
614
- * seam a different backing store replaces (the in-memory test context);
615
- * everything above it — validation, overlay, op queueing, flush semantics
616
- * — stays here so the two can't diverge.
617
- */
618
- protected readRows(table: string, where: Record<string, unknown>, limit?: number): Promise<Record<string, unknown>[]>;
619
- findOne(table: string, where: Record<string, unknown>): Promise<Record<string, unknown> | null>;
620
- findMany(table: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
621
- /**
622
- * Replay pending ops for `table` over a single DB-read result so reads
623
- * observe earlier same-block writes. Mirrors flush semantics: upserts
624
- * merge non-key/non-meta columns, increments add deltas, updates/deletes
625
- * apply by where-match. Overlaid rows synthesized from pending inserts
626
- * lack DB-generated columns (_id, _created_at).
627
- */
628
- protected overlayOne(table: string, where: Record<string, unknown>, dbRow: Record<string, unknown> | null): Record<string, unknown> | null;
629
- protected overlayMany(table: string, where: Record<string, unknown>, dbRows: Record<string, unknown>[]): Record<string, unknown>[];
630
- /** Apply one pending op to a candidate row state (null = row absent). */
631
- protected applyOpToRow(op: WriteOp, row: Record<string, unknown> | null, where: Record<string, unknown>): Record<string, unknown> | null;
632
- count(table: string, where?: Record<string, unknown>): Promise<number>;
633
- sum(table: string, column: string, where?: Record<string, unknown>): Promise<bigint>;
634
- min(table: string, column: string, where?: Record<string, unknown>): Promise<bigint | null>;
635
- max(table: string, column: string, where?: Record<string, unknown>): Promise<bigint | null>;
636
- countDistinct(table: string, column: string, where?: Record<string, unknown>): Promise<number>;
637
- /** Coerce string values from Postgres back to BigInt for uint/int columns */
638
- private coerceRow;
639
- /** Number of pending write operations */
640
- get pendingOps(): number;
641
- /**
642
- * Execute all batched writes in a single transaction.
643
- * Auto-populates _block_height, _tx_id, _created_at on inserts.
644
- *
645
- * Returns a {@link FlushManifest} describing every write so downstream
646
- * consumers (webhook emitter) can fan out outbox rows atomically
647
- * with the flush itself.
648
- */
649
- flush(): Promise<FlushManifest>;
650
- /**
651
- * Run a built statement list on an open transaction.
652
- *
653
- * Plain statements run in order. An increment runs UPDATE-first and then,
654
- * ONLY if that matched nothing, the guarded INSERT — and if the INSERT also
655
- * wrote nothing, the UPDATE again.
656
- *
657
- * That last step is the whole point. Under READ COMMITTED, two block
658
- * transactions flushing the same key while the row is absent both see
659
- * `UPDATE → 0 rows`; the first commits its INSERT, and the second's
660
- * `WHERE NOT EXISTS` then sees that committed row and inserts nothing. No
661
- * error, no constraint violation — the second delta was simply dropped.
662
- * Re-running the UPDATE takes a fresh snapshot that DOES see the winner's
663
- * row and applies the delta onto it.
664
- *
665
- * It cannot double-apply: the retry is reached only when the first UPDATE
666
- * matched no row AND the INSERT wrote no row, so the delta has not been
667
- * applied by either.
668
- */
669
- private execStatements;
670
- /** Prepare a single insert row, returning its data, columns, upsert key for batching. */
671
- private prepareInsert;
672
- /**
673
- * Lazily create `_journal` for schemas deployed before it existed. Cached
674
- * per process only once CONFIRMED present (to_regclass) — a CREATE issued
675
- * inside a block tx could roll back with it, so self-created tables are
676
- * re-verified on the next flush instead of trusted.
677
- */
678
- private ensureJournalTable;
679
- /** SQL type of a user column (for casting journal key VALUES), if known. */
680
- private columnSqlType;
681
- /**
682
- * Journal pre-images for a keyed batch: one `_journal` row per key with the
683
- * row's current state (`prev_row`), or NULL when the key doesn't exist yet
684
- * (the mutation will create it — a revert deletes it). Emitted BEFORE the
685
- * mutation statement, same transaction.
686
- */
687
- private journalCaptureSQL;
688
- /** Journal pre-images of rows a where-clause mutation will touch, keyed by `_id`. */
689
- private journalCaptureByWhereSQL;
690
- /** Build SQL statements from write ops, batching compatible INSERTs. */
691
- private buildStatements;
692
- private validateTable;
693
- }
694
- interface RunResult {
695
- processed: number;
696
- errors: number;
697
- /** Print events skipped because their decoded payload did not match the
698
- * source's declared `prints` schema. Never an error: skipping keeps the
699
- * block committable, which the checkpoint model requires. */
700
- skipped?: number;
701
- }
702
- /**
703
- * Build the named, ABI-decoded `event.input` for a contract_call source that
704
- * declares an `abi`. Each function arg is decoded via `clarityValueToJS` so the
705
- * values match the types `ExtractFunctionArgs` promises (Uint8Array buffers,
706
- * camelCase tuple keys, wrapped responses). Returns undefined when no abi /
707
- * function match, leaving handlers with the positional `args` only.
708
- */
709
- declare function buildContractCallInput(filter: ContractCallFilter, tx: MatchedTx["tx"]): Record<string, unknown> | undefined;
710
- /**
711
- * Build a typed event payload based on the source filter type.
712
- * Returns the payload the handler will receive.
713
- */
714
- declare function buildEventPayload(filter: SubgraphFilter, tx: MatchedTx["tx"], event: MatchedTx["events"][0] | null): Record<string, unknown>;
715
- /**\\n* Run a subgraph's keyed handlers against all matched transactions/events.\\n*\\n* Each MatchedTx carries a sourceName from the matcher. The runner looks up\\n* the corresponding handler in subgraph.handlers, falling back to "*".\\n*\\n* Does NOT flush — caller is responsible for flushing ctx after run.\\n*/
716
- declare function runHandlers(subgraph: SubgraphDefinition, matched: MatchedTx[], ctx: SubgraphContext2, opts?: {
717
- errorThreshold?: number
718
- }): Promise<RunResult>;
719
- export { runHandlers, buildEventPayload, buildContractCallInput, RunResult };