@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.
@@ -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(params: StreamsEventsConsumeParams): Promise<StreamsEventsConsumeResult>
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
- onBatch: (items: TItem[], envelope: TEnvelope, ctx: ConsumerBatchContext) => void | string | null | undefined | Promise<void> | Promise<string | null | undefined>
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
- type EventsEnvelope<T extends IndexEventType = IndexEventType> = {
1068
- events: IndexEventOf<T>[]
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<T extends IndexEventType = IndexEventType> = Omit<EventsListParams<T>, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexEventOf<T>, EventsEnvelope<T>>;
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): Promise<{
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): Promise<{
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<T extends IndexEventType>(params: EventsConsumeParams<T>): Promise<{
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) => Promise<{
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
@@ -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.onReorg(reorg, { cursor: rewind });
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 returnedCursor = await opts.onBatch(emitted, envelope, batchContext(checkpoint, height, envelope.tip.block_height));
700
- if (finalizedOnly)
701
- assertFinalizedCheckpoint(returnedCursor, checkpoint);
702
- const nextCursor = returnedCursor ?? checkpoint;
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.onReorg(reorg, { cursor: rewind });
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 returnedCursor = await opts.onBatch(emitted, envelope, batchContext(checkpoint, height, envelope.tip.block_height));
841
- if (finalizedOnly)
842
- assertFinalizedCheckpoint(returnedCursor, checkpoint);
843
- const nextCursor = returnedCursor ?? checkpoint;
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: params.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=A36A74FD0514129964756E2164756E21
2152
+ //# debugId=F62665719058CC2664756E2164756E21
1946
2153
  //# sourceMappingURL=index.js.map