@secondlayer/sdk 6.37.1 → 6.39.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/index.d.ts +214 -20
- package/dist/index.js +267 -79
- package/dist/index.js.map +11 -9
- package/dist/sinks/kysely.d.ts +108 -0
- package/dist/sinks/kysely.js +236 -0
- package/dist/sinks/kysely.js.map +11 -0
- package/dist/streams/index.d.ts +188 -6
- package/dist/streams/index.js +199 -71
- package/dist/streams/index.js.map +8 -7
- package/dist/subgraphs/index.d.ts +155 -17
- package/dist/subgraphs/index.js +224 -17
- package/dist/subgraphs/index.js.map +9 -7
- package/package.json +18 -3
|
@@ -316,6 +316,55 @@ interface TransactionProof {
|
|
|
316
316
|
reward_set: RewardSet
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* A consumer sink: the destination adapter a consume loop writes through.
|
|
321
|
+
*
|
|
322
|
+
* Without a sink, the user owns the three hard parts of a durable indexer —
|
|
323
|
+
* checkpoint persistence, rows+cursor atomicity, and reorg rollback — and
|
|
324
|
+
* the reasoning that makes them safe lives in doc comments. A sink owns all
|
|
325
|
+
* three: the loop loads its committed cursor, hands the handler a
|
|
326
|
+
* transaction, commits rows AND cursor atomically, and rolls back reorgs
|
|
327
|
+
* without any user code. Crucially, a sink also closes the silent-skip
|
|
328
|
+
* hazard where omitting `onReorg` meant reorgs were ignored forever: with a
|
|
329
|
+
* sink attached, rollback is unconditional.
|
|
330
|
+
*
|
|
331
|
+
* The interface is dependency-free on purpose: implementations with real
|
|
332
|
+
* database drivers live behind subpath exports (`@secondlayer/sdk/sinks/*`)
|
|
333
|
+
* so the root entry ships no DB dependency.
|
|
334
|
+
*/
|
|
335
|
+
interface ConsumerSink<Tx = unknown> {
|
|
336
|
+
/** Phantom marker carrying `Tx` in a directly-inferable position, so the
|
|
337
|
+
* consume loops' `TTx` type parameter resolves from the `sink` option
|
|
338
|
+
* (never set at runtime). */
|
|
339
|
+
readonly _tx?: Tx;
|
|
340
|
+
/** The committed checkpoint, or `null` on first run. Called once, before
|
|
341
|
+
* the first page. Implementations may create their checkpoint storage
|
|
342
|
+
* here and SHOULD validate their rollback preconditions (e.g. that every
|
|
343
|
+
* declared table carries the height column). */
|
|
344
|
+
loadCursor(): Promise<string | null>;
|
|
345
|
+
/**
|
|
346
|
+
* Apply one batch atomically: open a transaction, run `write(tx)` (the
|
|
347
|
+
* user's inserts), and commit the rows AND `cursor` together. A throw
|
|
348
|
+
* from `write` must abort the whole transaction — leaving neither rows
|
|
349
|
+
* nor cursor, so a crashed batch is simply re-read on restart.
|
|
350
|
+
*/
|
|
351
|
+
commitBatch(cursor: string, write: (tx: Tx) => Promise<void> | void): Promise<void>;
|
|
352
|
+
/**
|
|
353
|
+
* Roll the projection back to the fork: delete everything AT OR ABOVE
|
|
354
|
+
* `forkPointHeight` (inclusive `>=` — the new chain re-supplies the fork
|
|
355
|
+
* block) and commit `rewindCursor` in the SAME transaction. Deleting
|
|
356
|
+
* without the rewound cursor is the classic silent-gap bug: a crash
|
|
357
|
+
* between the two writes resumes above the fork and the deleted range is
|
|
358
|
+
* never re-read.
|
|
359
|
+
*/
|
|
360
|
+
rollback(forkPointHeight: number, rewindCursor: string): Promise<void>;
|
|
361
|
+
}
|
|
362
|
+
/** `ctx` gains `tx` exactly when a sink is attached; without one the shape
|
|
363
|
+
* is unchanged (an intersection with `unknown` is an identity, so
|
|
364
|
+
* contextual typing of `onBatch` callbacks never degrades to a union). */
|
|
365
|
+
type WithSinkTx<TTx> = [TTx] extends [never] ? unknown : {
|
|
366
|
+
tx: TTx
|
|
367
|
+
};
|
|
319
368
|
import { StreamsEventType } from "@secondlayer/shared";
|
|
320
369
|
/** A Clarity value as Streams serves it: the canonical hex string, a typed
|
|
321
370
|
* object carrying that hex (`{ hex }`), or a decoded Clarity-JSON object.
|
|
@@ -558,8 +607,28 @@ type StreamsBatchContext = ConsumerBatchContext;
|
|
|
558
607
|
type StreamsReorgContext = {
|
|
559
608
|
cursor: string
|
|
560
609
|
};
|
|
561
|
-
type StreamsEventsConsumeParams
|
|
610
|
+
type StreamsEventsConsumeParams<
|
|
611
|
+
TTx = never,
|
|
612
|
+
D extends boolean = false
|
|
613
|
+
> = {
|
|
562
614
|
fromCursor?: string | null
|
|
615
|
+
/**
|
|
616
|
+
* Deliver events pre-decoded as the flat, `event_type`-discriminated rows
|
|
617
|
+
* Index serves (`IndexEvent`), so Streams consumption reads identically
|
|
618
|
+
* to Index consumption — no guard+decode pairs in your handler.
|
|
619
|
+
*/
|
|
620
|
+
decoded?: D
|
|
621
|
+
/**
|
|
622
|
+
* Destination adapter that owns the checkpoint + rollback transaction
|
|
623
|
+
* (e.g. `kyselySink` from `@secondlayer/sdk/sinks/kysely`). With a sink:
|
|
624
|
+
* the loop resumes from the sink's committed cursor, `onBatch` receives
|
|
625
|
+
* `ctx.tx` and must write ONLY through it (rows and cursor commit in one
|
|
626
|
+
* transaction), reorg rollback is automatic, and `onBatch`'s return value
|
|
627
|
+
* is ignored.
|
|
628
|
+
*/
|
|
629
|
+
sink?: ConsumerSink<TTx>
|
|
630
|
+
/** Fires once per page, before `onBatch` and before any early return. */
|
|
631
|
+
onProgress?: (ctx: ConsumerBatchContext) => void
|
|
563
632
|
mode?: "tail" | "bounded"
|
|
564
633
|
/**
|
|
565
634
|
* Emit only finalized (immutable) events and never surface reorgs. The SDK
|
|
@@ -580,7 +649,7 @@ type StreamsEventsConsumeParams = {
|
|
|
580
649
|
* transaction as your writes. Returning a cursor overrides `ctx.cursor` as
|
|
581
650
|
* the resume point (advanced manual control); returning nothing uses it.
|
|
582
651
|
*/
|
|
583
|
-
onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope, ctx: StreamsBatchContext) => void | string | null | undefined | Promise<void> | Promise<string | null | undefined>
|
|
652
|
+
onBatch: (events: D extends true ? IndexEvent[] : StreamsEvent[], envelope: StreamsEventsEnvelope, ctx: StreamsBatchContext & WithSinkTx<TTx>) => void | string | null | undefined | Promise<void> | Promise<string | null | undefined>
|
|
584
653
|
/**
|
|
585
654
|
* Roll your projection back to `reorg.fork_point_height`, persisting
|
|
586
655
|
* `ctx.cursor` in the same transaction. Called once per *new* reorg
|
|
@@ -737,7 +806,12 @@ type StreamsClient = {
|
|
|
737
806
|
* The consumer also exits when `maxPages`, `maxEmptyPolls`, or `signal`
|
|
738
807
|
* stops it.
|
|
739
808
|
*/
|
|
740
|
-
consume
|
|
809
|
+
consume<
|
|
810
|
+
TTx = never,
|
|
811
|
+
D extends boolean = false
|
|
812
|
+
>(params: StreamsEventsConsumeParams<TTx, D> & {
|
|
813
|
+
sink?: ConsumerSink<TTx>
|
|
814
|
+
}): Promise<StreamsEventsConsumeResult>
|
|
741
815
|
/**
|
|
742
816
|
* Backfill from bulk dumps, then continue live from the dump→live seam in
|
|
743
817
|
* one call. Iterates finalized dump files (via `onDumpFile`) in block
|
|
@@ -818,16 +892,18 @@ type IndexFeedEnvelope = {
|
|
|
818
892
|
};
|
|
819
893
|
/** Consumer options shared by `index.events.consume` and
|
|
820
894
|
* `index.contractCalls.consume`. Same contract as the Streams consumer:
|
|
821
|
-
* commit your writes inside `onBatch`, return the cursor you committed
|
|
895
|
+
* commit your writes inside `onBatch`, return the cursor you committed —
|
|
896
|
+
* or attach a `sink` and let it own checkpointing and rollback entirely. */
|
|
822
897
|
type IndexConsumeOptions<
|
|
823
898
|
TItem extends IndexFeedItem,
|
|
824
|
-
TEnvelope extends IndexFeedEnvelope
|
|
899
|
+
TEnvelope extends IndexFeedEnvelope,
|
|
900
|
+
TTx = never
|
|
825
901
|
> = {
|
|
826
902
|
/** Resume from a committed checkpoint. Without it (and without
|
|
827
903
|
* `fromHeight`) the API serves only the recent default window. */
|
|
828
904
|
fromCursor?: string | null
|
|
829
905
|
/** Start a fresh sweep at this height (e.g. `0` for genesis backfill).
|
|
830
|
-
* Ignored once a cursor exists. */
|
|
906
|
+
* Ignored once a cursor exists (including a sink's committed cursor). */
|
|
831
907
|
fromHeight?: number
|
|
832
908
|
/** `tail` (default) keeps polling at the tip; `bounded` returns on the
|
|
833
909
|
* first empty page. */
|
|
@@ -837,7 +913,20 @@ type IndexConsumeOptions<
|
|
|
837
913
|
* never reorgs, so `onReorg` is skipped entirely. */
|
|
838
914
|
finalizedOnly?: boolean
|
|
839
915
|
batchSize?: number
|
|
840
|
-
|
|
916
|
+
/**
|
|
917
|
+
* Destination adapter that owns the checkpoint + rollback transaction
|
|
918
|
+
* (e.g. `kyselySink` from `@secondlayer/sdk/sinks/kysely`). With a sink:
|
|
919
|
+
* the loop resumes from the sink's committed cursor, `onBatch` receives
|
|
920
|
+
* `ctx.tx` and must write ONLY through it (rows and cursor commit in one
|
|
921
|
+
* transaction — a throw aborts both), reorg rollback is automatic (no
|
|
922
|
+
* `onReorg` needed), and `onBatch`'s return value is ignored.
|
|
923
|
+
*/
|
|
924
|
+
sink?: ConsumerSink<TTx>
|
|
925
|
+
/** Fires once per page, before `onBatch` and before any early return —
|
|
926
|
+
* an empty page still proves the loop is alive. Feed it to
|
|
927
|
+
* `consumerHealth().record`. */
|
|
928
|
+
onProgress?: (ctx: ConsumerBatchContext) => void
|
|
929
|
+
onBatch: (items: TItem[], envelope: TEnvelope, ctx: ConsumerBatchContext & WithSinkTx<TTx>) => void | string | null | undefined | Promise<void> | Promise<string | null | undefined>
|
|
841
930
|
onReorg?: (reorg: IndexReorg, ctx: {
|
|
842
931
|
cursor: string
|
|
843
932
|
}) => Promise<void> | void
|
|
@@ -1064,8 +1153,19 @@ type IndexPrint = IndexEventBase & {
|
|
|
1064
1153
|
/** Decoded chain event, discriminated by `event_type`. */
|
|
1065
1154
|
type IndexEvent = IndexFtTransfer | IndexNftTransfer | IndexStxTransfer | IndexStxMint | IndexStxBurn | IndexStxLock | IndexFtMint | IndexFtBurn | IndexNftMint | IndexNftBurn | IndexPrint;
|
|
1066
1155
|
type IndexEventType = IndexEvent["event_type"];
|
|
1067
|
-
|
|
1068
|
-
|
|
1156
|
+
/** Fields every read returns regardless of `fields` — the consume contract
|
|
1157
|
+
* (`cursor`, `block_height`) plus the union discriminant. */
|
|
1158
|
+
type IndexAlwaysFields = "cursor" | "block_height" | "event_type";
|
|
1159
|
+
/** An event row narrowed to a `fields` selection (plus what always ships). */
|
|
1160
|
+
type IndexEventFields<
|
|
1161
|
+
T extends IndexEventType,
|
|
1162
|
+
F extends keyof IndexEventOf<T> & string
|
|
1163
|
+
> = Pick<IndexEventOf<T>, (F | IndexAlwaysFields) & keyof IndexEventOf<T>>;
|
|
1164
|
+
type EventsEnvelope<
|
|
1165
|
+
T extends IndexEventType = IndexEventType,
|
|
1166
|
+
TRow = IndexEventOf<T>
|
|
1167
|
+
> = {
|
|
1168
|
+
events: TRow[]
|
|
1069
1169
|
next_cursor: string | null
|
|
1070
1170
|
tip: IndexTip
|
|
1071
1171
|
reorgs: IndexReorg[]
|
|
@@ -1098,12 +1198,30 @@ type EventsListParams<T extends IndexEventType = IndexEventType> = {
|
|
|
1098
1198
|
* Avoids a `/v1/index/transactions` call per event; for `print` events it's
|
|
1099
1199
|
* the only source of the submitting sender. */
|
|
1100
1200
|
txContext?: boolean
|
|
1201
|
+
/**
|
|
1202
|
+
* Return only these columns. The server projects the SELECT, so an
|
|
1203
|
+
* unrequested field is physically absent — and the returned row type
|
|
1204
|
+
* narrows to match, making a read of one a compile error rather than
|
|
1205
|
+
* `undefined`.
|
|
1206
|
+
*
|
|
1207
|
+
* `cursor`, `block_height`, and `event_type` come back regardless: the
|
|
1208
|
+
* first two are the consume contract, the third carries the discriminant.
|
|
1209
|
+
* Don't list them.
|
|
1210
|
+
*
|
|
1211
|
+
* Cost note: this does NOT change your bill — Index meters per row read,
|
|
1212
|
+
* not per field. What it buys is wire bytes, plus skipping the `blocks`
|
|
1213
|
+
* join when `block_time` is omitted.
|
|
1214
|
+
*/
|
|
1215
|
+
fields?: readonly (keyof IndexEventOf<T> & string)[]
|
|
1101
1216
|
};
|
|
1102
1217
|
type EventsWalkParams<T extends IndexEventType = IndexEventType> = Omit<EventsListParams<T>, "limit"> & {
|
|
1103
1218
|
batchSize?: number
|
|
1104
1219
|
signal?: AbortSignal
|
|
1105
1220
|
};
|
|
1106
|
-
type EventsConsumeParams<
|
|
1221
|
+
type EventsConsumeParams<
|
|
1222
|
+
T extends IndexEventType = IndexEventType,
|
|
1223
|
+
TTx = never
|
|
1224
|
+
> = Omit<EventsListParams<T>, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexEventOf<T>, EventsEnvelope<T>, TTx>;
|
|
1107
1225
|
type IndexContractCall = {
|
|
1108
1226
|
cursor: string
|
|
1109
1227
|
block_height: number
|
|
@@ -1143,7 +1261,7 @@ type ContractCallsWalkParams = Omit<ContractCallsListParams, "limit"> & {
|
|
|
1143
1261
|
batchSize?: number
|
|
1144
1262
|
signal?: AbortSignal
|
|
1145
1263
|
};
|
|
1146
|
-
type ContractCallsConsumeParams = Omit<ContractCallsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexContractCall, ContractCallsEnvelope>;
|
|
1264
|
+
type ContractCallsConsumeParams<TTx = never> = Omit<ContractCallsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexContractCall, ContractCallsEnvelope, TTx>;
|
|
1147
1265
|
/** One canonical block in the sync map. Lean by design — block + parent hash
|
|
1148
1266
|
* for chain linkage, burn anchor for Bitcoin confirmations. Use `blocks` for
|
|
1149
1267
|
* the full block resource. */
|
|
@@ -1614,8 +1732,8 @@ type SbtcEventsWalkParams = Omit<SbtcEventsListParams, "limit"> & {
|
|
|
1614
1732
|
batchSize?: number
|
|
1615
1733
|
signal?: AbortSignal
|
|
1616
1734
|
};
|
|
1617
|
-
type SbtcEventsConsumeParams = Omit<SbtcEventsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexSbtcEvent, SbtcEventsEnvelope>;
|
|
1618
|
-
type SbtcDepositsConsumeParams = Omit<SbtcDepositsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexSbtcDeposit, SbtcDepositsEnvelope>;
|
|
1735
|
+
type SbtcEventsConsumeParams<TTx = never> = Omit<SbtcEventsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexSbtcEvent, SbtcEventsEnvelope, TTx>;
|
|
1736
|
+
type SbtcDepositsConsumeParams<TTx = never> = Omit<SbtcDepositsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexSbtcDeposit, SbtcDepositsEnvelope, TTx>;
|
|
1619
1737
|
/** `index.sbtc` — the decoded sBTC peg surface (deposits, withdrawals, raw
|
|
1620
1738
|
* events, scoreboard). The only productized decoded sBTC peg feed on Stacks. */
|
|
1621
1739
|
interface SbtcResource {
|
|
@@ -1624,7 +1742,9 @@ interface SbtcResource {
|
|
|
1624
1742
|
walk(params?: SbtcDepositsWalkParams): AsyncIterable<IndexSbtcDeposit>
|
|
1625
1743
|
/** Checkpointed sweep of completed deposits — append-only, so safe to
|
|
1626
1744
|
* mirror. See {@link IndexConsumeOptions}. */
|
|
1627
|
-
consume(params: SbtcDepositsConsumeParams
|
|
1745
|
+
consume<TTx = never>(params: SbtcDepositsConsumeParams<TTx> & {
|
|
1746
|
+
sink?: ConsumerSink<TTx>
|
|
1747
|
+
}): Promise<{
|
|
1628
1748
|
cursor: string | null
|
|
1629
1749
|
pages: number
|
|
1630
1750
|
emptyPolls: number
|
|
@@ -1643,7 +1763,9 @@ interface SbtcResource {
|
|
|
1643
1763
|
walk(params?: SbtcEventsWalkParams): AsyncIterable<IndexSbtcEvent>
|
|
1644
1764
|
/** Checkpointed sweep of the raw peg event log — append-only across all
|
|
1645
1765
|
* six topics. See {@link IndexConsumeOptions}. */
|
|
1646
|
-
consume(params: SbtcEventsConsumeParams
|
|
1766
|
+
consume<TTx = never>(params: SbtcEventsConsumeParams<TTx> & {
|
|
1767
|
+
sink?: ConsumerSink<TTx>
|
|
1768
|
+
}): Promise<{
|
|
1647
1769
|
cursor: string | null
|
|
1648
1770
|
pages: number
|
|
1649
1771
|
emptyPolls: number
|
|
@@ -1791,9 +1913,23 @@ interface NftTransfersResource {
|
|
|
1791
1913
|
/** `index.events` — callable shorthand for `.list()`; `eventType` is required. */
|
|
1792
1914
|
interface IndexEventsResource {
|
|
1793
1915
|
<T extends IndexEventType>(params: EventsListParams<T>): Promise<EventsEnvelope<T>>;
|
|
1916
|
+
/** Narrowing overload: `fields` shrinks the returned row type to exactly
|
|
1917
|
+
* the requested columns plus {@link IndexAlwaysFields}. `const F` means no
|
|
1918
|
+
* `as const` at the call site. */
|
|
1919
|
+
list<
|
|
1920
|
+
T extends IndexEventType,
|
|
1921
|
+
const F extends keyof IndexEventOf<T> & string
|
|
1922
|
+
>(params: EventsListParams<T> & {
|
|
1923
|
+
fields: readonly F[]
|
|
1924
|
+
}): Promise<EventsEnvelope<T, IndexEventFields<T, F>>>;
|
|
1794
1925
|
list<T extends IndexEventType>(params: EventsListParams<T>): Promise<EventsEnvelope<T>>;
|
|
1795
1926
|
walk<T extends IndexEventType>(params: EventsWalkParams<T>): AsyncIterable<IndexEventOf<T>>;
|
|
1796
|
-
consume<
|
|
1927
|
+
consume<
|
|
1928
|
+
T extends IndexEventType,
|
|
1929
|
+
TTx = never
|
|
1930
|
+
>(params: EventsConsumeParams<T, TTx> & {
|
|
1931
|
+
sink?: ConsumerSink<TTx>
|
|
1932
|
+
}): Promise<{
|
|
1797
1933
|
cursor: string | null
|
|
1798
1934
|
pages: number
|
|
1799
1935
|
emptyPolls: number
|
|
@@ -1839,7 +1975,9 @@ declare class Index extends BaseClient {
|
|
|
1839
1975
|
readonly contractCalls: {
|
|
1840
1976
|
list: (params?: ContractCallsListParams) => Promise<ContractCallsEnvelope>
|
|
1841
1977
|
walk: (params?: ContractCallsWalkParams) => AsyncIterable<IndexContractCall>
|
|
1842
|
-
consume: (params: ContractCallsConsumeParams
|
|
1978
|
+
consume: <TTx = never>(params: ContractCallsConsumeParams<TTx> & {
|
|
1979
|
+
sink?: ConsumerSink<TTx>
|
|
1980
|
+
}) => Promise<{
|
|
1843
1981
|
cursor: string | null
|
|
1844
1982
|
pages: number
|
|
1845
1983
|
emptyPolls: number
|
package/dist/subgraphs/index.js
CHANGED
|
@@ -655,7 +655,7 @@ async function consumeStreamsEvents(opts) {
|
|
|
655
655
|
const emptyBackoffMs = opts.emptyBackoffMs ?? 500;
|
|
656
656
|
const maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;
|
|
657
657
|
const maxEmptyPolls = opts.maxEmptyPolls ?? Number.POSITIVE_INFINITY;
|
|
658
|
-
let cursor = opts.fromCursor ?? null;
|
|
658
|
+
let cursor = opts.fromCursor ?? await opts.sink?.loadCursor() ?? null;
|
|
659
659
|
const handledReorgs = new Set;
|
|
660
660
|
let pages = 0;
|
|
661
661
|
let emptyPolls = 0;
|
|
@@ -678,13 +678,14 @@ async function consumeStreamsEvents(opts) {
|
|
|
678
678
|
signal: opts.signal
|
|
679
679
|
});
|
|
680
680
|
pages++;
|
|
681
|
-
if (!finalizedOnly && opts.onReorg) {
|
|
681
|
+
if (!finalizedOnly && (opts.onReorg || opts.sink)) {
|
|
682
682
|
const fresh = envelope.reorgs.filter((reorg) => !handledReorgs.has(reorgKey(reorg))).sort((a, b) => a.fork_point_height - b.fork_point_height);
|
|
683
683
|
if (fresh.length > 0) {
|
|
684
684
|
const forkPoint = Math.min(...fresh.map((reorg) => reorg.fork_point_height));
|
|
685
685
|
const rewind = Cursor.atHeight(forkPoint);
|
|
686
686
|
for (const reorg of fresh) {
|
|
687
|
-
await opts.
|
|
687
|
+
await opts.sink?.rollback(reorg.fork_point_height, rewind);
|
|
688
|
+
await opts.onReorg?.(reorg, { cursor: rewind });
|
|
688
689
|
handledReorgs.add(reorgKey(reorg));
|
|
689
690
|
}
|
|
690
691
|
cursor = rewind;
|
|
@@ -696,10 +697,26 @@ async function consumeStreamsEvents(opts) {
|
|
|
696
697
|
const emitted = finalizedOnly ? envelope.events.filter((event) => event.finalized) : envelope.events;
|
|
697
698
|
const checkpoint = finalizedOnly ? emitted.at(-1)?.cursor ?? cursor : envelope.next_cursor;
|
|
698
699
|
height = emitted.at(-1)?.block_height ?? height;
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
700
|
+
const ctx = batchContext(checkpoint, height, envelope.tip.block_height);
|
|
701
|
+
opts.onProgress?.(ctx);
|
|
702
|
+
let nextCursor;
|
|
703
|
+
if (opts.sink) {
|
|
704
|
+
const sink = opts.sink;
|
|
705
|
+
if (checkpoint !== null && (checkpoint !== cursor || emitted.length > 0)) {
|
|
706
|
+
await sink.commitBatch(checkpoint, async (tx) => {
|
|
707
|
+
await opts.onBatch(emitted, envelope, {
|
|
708
|
+
...ctx,
|
|
709
|
+
tx
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
nextCursor = checkpoint;
|
|
714
|
+
} else {
|
|
715
|
+
const returnedCursor = await opts.onBatch(emitted, envelope, ctx);
|
|
716
|
+
if (finalizedOnly)
|
|
717
|
+
assertFinalizedCheckpoint(returnedCursor, checkpoint);
|
|
718
|
+
nextCursor = returnedCursor ?? checkpoint;
|
|
719
|
+
}
|
|
703
720
|
if (nextCursor && nextCursor !== cursor) {
|
|
704
721
|
cursor = nextCursor;
|
|
705
722
|
emptyPolls = 0;
|
|
@@ -800,7 +817,7 @@ async function consumeIndexFeed(opts) {
|
|
|
800
817
|
const emptyBackoffMs = opts.emptyBackoffMs ?? 500;
|
|
801
818
|
const maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;
|
|
802
819
|
const maxEmptyPolls = opts.maxEmptyPolls ?? Number.POSITIVE_INFINITY;
|
|
803
|
-
let cursor = opts.fromCursor ?? null;
|
|
820
|
+
let cursor = opts.fromCursor ?? await opts.sink?.loadCursor() ?? null;
|
|
804
821
|
const handledReorgs = new Set;
|
|
805
822
|
let pages = 0;
|
|
806
823
|
let emptyPolls = 0;
|
|
@@ -818,13 +835,14 @@ async function consumeIndexFeed(opts) {
|
|
|
818
835
|
signal: opts.signal
|
|
819
836
|
});
|
|
820
837
|
pages++;
|
|
821
|
-
if (!finalizedOnly && opts.onReorg) {
|
|
838
|
+
if (!finalizedOnly && (opts.onReorg || opts.sink)) {
|
|
822
839
|
const fresh = envelope.reorgs.filter((reorg) => !handledReorgs.has(reorg.id)).sort((a, b) => a.fork_point_height - b.fork_point_height);
|
|
823
840
|
if (fresh.length > 0) {
|
|
824
841
|
const forkPoint = Math.min(...fresh.map((reorg) => reorg.fork_point_height));
|
|
825
842
|
const rewind = Cursor.atHeight(forkPoint);
|
|
826
843
|
for (const reorg of fresh) {
|
|
827
|
-
await opts.
|
|
844
|
+
await opts.sink?.rollback(reorg.fork_point_height, rewind);
|
|
845
|
+
await opts.onReorg?.(reorg, { cursor: rewind });
|
|
828
846
|
handledReorgs.add(reorg.id);
|
|
829
847
|
}
|
|
830
848
|
cursor = rewind;
|
|
@@ -837,10 +855,26 @@ async function consumeIndexFeed(opts) {
|
|
|
837
855
|
const emitted = finalizedOnly ? items.filter((item) => item.block_height <= envelope.tip.finalized_height) : items;
|
|
838
856
|
const checkpoint = finalizedOnly ? emitted.at(-1)?.cursor ?? cursor : envelope.next_cursor;
|
|
839
857
|
height = emitted.at(-1)?.block_height ?? height;
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
858
|
+
const ctx = batchContext(checkpoint, height, envelope.tip.block_height);
|
|
859
|
+
opts.onProgress?.(ctx);
|
|
860
|
+
let nextCursor;
|
|
861
|
+
if (opts.sink) {
|
|
862
|
+
const sink = opts.sink;
|
|
863
|
+
if (checkpoint !== null && (checkpoint !== cursor || emitted.length > 0)) {
|
|
864
|
+
await sink.commitBatch(checkpoint, async (tx) => {
|
|
865
|
+
await opts.onBatch(emitted, envelope, {
|
|
866
|
+
...ctx,
|
|
867
|
+
tx
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
nextCursor = checkpoint;
|
|
872
|
+
} else {
|
|
873
|
+
const returnedCursor = await opts.onBatch(emitted, envelope, ctx);
|
|
874
|
+
if (finalizedOnly)
|
|
875
|
+
assertFinalizedCheckpoint(returnedCursor, checkpoint);
|
|
876
|
+
nextCursor = returnedCursor ?? checkpoint;
|
|
877
|
+
}
|
|
844
878
|
if (nextCursor && nextCursor !== cursor) {
|
|
845
879
|
cursor = nextCursor;
|
|
846
880
|
emptyPolls = 0;
|
|
@@ -1079,7 +1113,8 @@ class Index extends BaseClient {
|
|
|
1079
1113
|
from_height: params.fromHeight,
|
|
1080
1114
|
to_height: params.toHeight,
|
|
1081
1115
|
trait: params.trait,
|
|
1082
|
-
tx_context: params.txContext ? "true" : undefined
|
|
1116
|
+
tx_context: params.txContext ? "true" : undefined,
|
|
1117
|
+
fields: params.fields?.join(",")
|
|
1083
1118
|
})}`);
|
|
1084
1119
|
}
|
|
1085
1120
|
walkEvents(params) {
|
|
@@ -1313,6 +1348,175 @@ class Projects extends BaseClient {
|
|
|
1313
1348
|
// src/streams/client.ts
|
|
1314
1349
|
import { ed25519 as ed255192 } from "@secondlayer/shared";
|
|
1315
1350
|
|
|
1351
|
+
// src/streams/_payload.ts
|
|
1352
|
+
function requireString(payload, field, eventType) {
|
|
1353
|
+
const value = payload[field];
|
|
1354
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1355
|
+
throw new Error(`${eventType} payload missing ${field}`);
|
|
1356
|
+
}
|
|
1357
|
+
return value;
|
|
1358
|
+
}
|
|
1359
|
+
function optionalString(value) {
|
|
1360
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1361
|
+
}
|
|
1362
|
+
function requireAmountField(payload, field, eventType) {
|
|
1363
|
+
const amount = requireString(payload, field, eventType);
|
|
1364
|
+
if (!/^(0|[1-9]\d*)$/.test(amount)) {
|
|
1365
|
+
throw new Error(`${eventType} payload has malformed ${field}`);
|
|
1366
|
+
}
|
|
1367
|
+
return amount;
|
|
1368
|
+
}
|
|
1369
|
+
function requireAmount(payload, eventType) {
|
|
1370
|
+
return requireAmountField(payload, "amount", eventType);
|
|
1371
|
+
}
|
|
1372
|
+
function parseAssetIdentifier(assetIdentifier, eventType) {
|
|
1373
|
+
const [contractId, tokenName] = assetIdentifier.split("::");
|
|
1374
|
+
if (!contractId) {
|
|
1375
|
+
throw new Error(`${eventType} payload has malformed asset_identifier`);
|
|
1376
|
+
}
|
|
1377
|
+
return {
|
|
1378
|
+
contract_id: contractId,
|
|
1379
|
+
token_name: tokenName && tokenName.length > 0 ? tokenName : null
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function requireHexValue(payload, eventType) {
|
|
1383
|
+
const rawValue = payload.raw_value;
|
|
1384
|
+
if (typeof rawValue === "string") {
|
|
1385
|
+
if (!/^0x[0-9a-fA-F]*$/.test(rawValue)) {
|
|
1386
|
+
throw new Error(`${eventType} payload has malformed value`);
|
|
1387
|
+
}
|
|
1388
|
+
return rawValue;
|
|
1389
|
+
}
|
|
1390
|
+
const value = payload.value;
|
|
1391
|
+
const hex = typeof value === "string" ? value : value && typeof value === "object" && typeof value.hex === "string" ? value.hex : null;
|
|
1392
|
+
if (!hex) {
|
|
1393
|
+
throw new Error(`${eventType} payload missing value`);
|
|
1394
|
+
}
|
|
1395
|
+
if (!/^0x[0-9a-fA-F]*$/.test(hex)) {
|
|
1396
|
+
throw new Error(`${eventType} payload has malformed value`);
|
|
1397
|
+
}
|
|
1398
|
+
return hex;
|
|
1399
|
+
}
|
|
1400
|
+
function decodedRow(event, eventType, decoded_payload) {
|
|
1401
|
+
return {
|
|
1402
|
+
cursor: event.cursor,
|
|
1403
|
+
block_height: event.block_height,
|
|
1404
|
+
tx_id: event.tx_id,
|
|
1405
|
+
tx_index: event.tx_index,
|
|
1406
|
+
event_index: event.event_index,
|
|
1407
|
+
event_type: eventType,
|
|
1408
|
+
decoded_payload,
|
|
1409
|
+
source_cursor: event.cursor
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/streams/decode.ts
|
|
1414
|
+
function decode(event) {
|
|
1415
|
+
const base = {
|
|
1416
|
+
cursor: event.cursor,
|
|
1417
|
+
block_height: event.block_height,
|
|
1418
|
+
block_time: event.ts,
|
|
1419
|
+
tx_id: event.tx_id,
|
|
1420
|
+
tx_index: event.tx_index,
|
|
1421
|
+
event_index: event.event_index,
|
|
1422
|
+
contract_id: event.contract_id
|
|
1423
|
+
};
|
|
1424
|
+
const p = event.payload;
|
|
1425
|
+
switch (event.event_type) {
|
|
1426
|
+
case "stx_transfer":
|
|
1427
|
+
return {
|
|
1428
|
+
...base,
|
|
1429
|
+
event_type: "stx_transfer",
|
|
1430
|
+
sender: requireString(p, "sender", event.event_type),
|
|
1431
|
+
recipient: requireString(p, "recipient", event.event_type),
|
|
1432
|
+
amount: requireAmountField(p, "amount", event.event_type),
|
|
1433
|
+
memo: optionalString(p.memo)
|
|
1434
|
+
};
|
|
1435
|
+
case "stx_mint":
|
|
1436
|
+
return {
|
|
1437
|
+
...base,
|
|
1438
|
+
event_type: "stx_mint",
|
|
1439
|
+
recipient: requireString(p, "recipient", event.event_type),
|
|
1440
|
+
amount: requireAmountField(p, "amount", event.event_type)
|
|
1441
|
+
};
|
|
1442
|
+
case "stx_burn":
|
|
1443
|
+
return {
|
|
1444
|
+
...base,
|
|
1445
|
+
event_type: "stx_burn",
|
|
1446
|
+
sender: requireString(p, "sender", event.event_type),
|
|
1447
|
+
amount: requireAmountField(p, "amount", event.event_type)
|
|
1448
|
+
};
|
|
1449
|
+
case "stx_lock":
|
|
1450
|
+
return {
|
|
1451
|
+
...base,
|
|
1452
|
+
event_type: "stx_lock",
|
|
1453
|
+
sender: requireString(p, "locked_address", event.event_type),
|
|
1454
|
+
amount: requireAmountField(p, "locked_amount", event.event_type),
|
|
1455
|
+
payload: { unlock_height: optionalString(p.unlock_height) }
|
|
1456
|
+
};
|
|
1457
|
+
case "ft_transfer":
|
|
1458
|
+
return {
|
|
1459
|
+
...base,
|
|
1460
|
+
event_type: "ft_transfer",
|
|
1461
|
+
asset_identifier: requireString(p, "asset_identifier", event.event_type),
|
|
1462
|
+
sender: requireString(p, "sender", event.event_type),
|
|
1463
|
+
recipient: requireString(p, "recipient", event.event_type),
|
|
1464
|
+
amount: requireAmountField(p, "amount", event.event_type)
|
|
1465
|
+
};
|
|
1466
|
+
case "ft_mint":
|
|
1467
|
+
return {
|
|
1468
|
+
...base,
|
|
1469
|
+
event_type: "ft_mint",
|
|
1470
|
+
asset_identifier: requireString(p, "asset_identifier", event.event_type),
|
|
1471
|
+
recipient: requireString(p, "recipient", event.event_type),
|
|
1472
|
+
amount: requireAmountField(p, "amount", event.event_type)
|
|
1473
|
+
};
|
|
1474
|
+
case "ft_burn":
|
|
1475
|
+
return {
|
|
1476
|
+
...base,
|
|
1477
|
+
event_type: "ft_burn",
|
|
1478
|
+
asset_identifier: requireString(p, "asset_identifier", event.event_type),
|
|
1479
|
+
sender: requireString(p, "sender", event.event_type),
|
|
1480
|
+
amount: requireAmountField(p, "amount", event.event_type)
|
|
1481
|
+
};
|
|
1482
|
+
case "nft_transfer":
|
|
1483
|
+
return {
|
|
1484
|
+
...base,
|
|
1485
|
+
event_type: "nft_transfer",
|
|
1486
|
+
asset_identifier: requireString(p, "asset_identifier", event.event_type),
|
|
1487
|
+
sender: requireString(p, "sender", event.event_type),
|
|
1488
|
+
recipient: requireString(p, "recipient", event.event_type),
|
|
1489
|
+
value: requireHexValue(p, event.event_type)
|
|
1490
|
+
};
|
|
1491
|
+
case "nft_mint":
|
|
1492
|
+
return {
|
|
1493
|
+
...base,
|
|
1494
|
+
event_type: "nft_mint",
|
|
1495
|
+
asset_identifier: requireString(p, "asset_identifier", event.event_type),
|
|
1496
|
+
recipient: requireString(p, "recipient", event.event_type),
|
|
1497
|
+
value: requireHexValue(p, event.event_type)
|
|
1498
|
+
};
|
|
1499
|
+
case "nft_burn":
|
|
1500
|
+
return {
|
|
1501
|
+
...base,
|
|
1502
|
+
event_type: "nft_burn",
|
|
1503
|
+
asset_identifier: requireString(p, "asset_identifier", event.event_type),
|
|
1504
|
+
sender: requireString(p, "sender", event.event_type),
|
|
1505
|
+
value: requireHexValue(p, event.event_type)
|
|
1506
|
+
};
|
|
1507
|
+
case "print":
|
|
1508
|
+
return {
|
|
1509
|
+
...base,
|
|
1510
|
+
event_type: "print",
|
|
1511
|
+
payload: {
|
|
1512
|
+
topic: optionalString(p.topic),
|
|
1513
|
+
value: p.value,
|
|
1514
|
+
raw_value: optionalString(p.raw_value)
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1316
1520
|
// src/streams/dumps.ts
|
|
1317
1521
|
import { createHash } from "node:crypto";
|
|
1318
1522
|
import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
|
|
@@ -1687,8 +1891,11 @@ function createStreamsClient(options) {
|
|
|
1687
1891
|
return request(`/v1/streams/events/${encodeURIComponent(txId)}`);
|
|
1688
1892
|
},
|
|
1689
1893
|
consume(params) {
|
|
1894
|
+
const onBatch = params.decoded ? (events, envelope, ctx) => params.onBatch(events.map(decode), envelope, ctx) : params.onBatch;
|
|
1690
1895
|
return consumeStreamsEvents({
|
|
1691
1896
|
fromCursor: params.fromCursor,
|
|
1897
|
+
sink: params.sink,
|
|
1898
|
+
onProgress: params.onProgress,
|
|
1692
1899
|
mode: params.mode,
|
|
1693
1900
|
finalizedOnly: params.finalizedOnly,
|
|
1694
1901
|
types: params.types,
|
|
@@ -1699,7 +1906,7 @@ function createStreamsClient(options) {
|
|
|
1699
1906
|
assetIdentifier: params.assetIdentifier,
|
|
1700
1907
|
batchSize: params.batchSize ?? 100,
|
|
1701
1908
|
fetchEvents,
|
|
1702
|
-
onBatch
|
|
1909
|
+
onBatch,
|
|
1703
1910
|
onReorg: params.onReorg,
|
|
1704
1911
|
emptyBackoffMs: params.emptyBackoffMs,
|
|
1705
1912
|
maxPages: params.maxPages,
|
|
@@ -1942,5 +2149,5 @@ export {
|
|
|
1942
2149
|
Subgraphs
|
|
1943
2150
|
};
|
|
1944
2151
|
|
|
1945
|
-
//# debugId=
|
|
2152
|
+
//# debugId=F62665719058CC2664756E2164756E21
|
|
1946
2153
|
//# sourceMappingURL=index.js.map
|