@secondlayer/sdk 6.37.1 → 6.38.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
@@ -1103,7 +1192,10 @@ type EventsWalkParams<T extends IndexEventType = IndexEventType> = Omit<EventsLi
1103
1192
  batchSize?: number
1104
1193
  signal?: AbortSignal
1105
1194
  };
1106
- type EventsConsumeParams<T extends IndexEventType = IndexEventType> = Omit<EventsListParams<T>, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexEventOf<T>, EventsEnvelope<T>>;
1195
+ type EventsConsumeParams<
1196
+ T extends IndexEventType = IndexEventType,
1197
+ TTx = never
1198
+ > = Omit<EventsListParams<T>, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexEventOf<T>, EventsEnvelope<T>, TTx>;
1107
1199
  type IndexContractCall = {
1108
1200
  cursor: string
1109
1201
  block_height: number
@@ -1143,7 +1235,7 @@ type ContractCallsWalkParams = Omit<ContractCallsListParams, "limit"> & {
1143
1235
  batchSize?: number
1144
1236
  signal?: AbortSignal
1145
1237
  };
1146
- type ContractCallsConsumeParams = Omit<ContractCallsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexContractCall, ContractCallsEnvelope>;
1238
+ type ContractCallsConsumeParams<TTx = never> = Omit<ContractCallsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexContractCall, ContractCallsEnvelope, TTx>;
1147
1239
  /** One canonical block in the sync map. Lean by design — block + parent hash
1148
1240
  * for chain linkage, burn anchor for Bitcoin confirmations. Use `blocks` for
1149
1241
  * the full block resource. */
@@ -1614,8 +1706,8 @@ type SbtcEventsWalkParams = Omit<SbtcEventsListParams, "limit"> & {
1614
1706
  batchSize?: number
1615
1707
  signal?: AbortSignal
1616
1708
  };
1617
- type SbtcEventsConsumeParams = Omit<SbtcEventsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexSbtcEvent, SbtcEventsEnvelope>;
1618
- type SbtcDepositsConsumeParams = Omit<SbtcDepositsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexSbtcDeposit, SbtcDepositsEnvelope>;
1709
+ type SbtcEventsConsumeParams<TTx = never> = Omit<SbtcEventsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexSbtcEvent, SbtcEventsEnvelope, TTx>;
1710
+ type SbtcDepositsConsumeParams<TTx = never> = Omit<SbtcDepositsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexSbtcDeposit, SbtcDepositsEnvelope, TTx>;
1619
1711
  /** `index.sbtc` — the decoded sBTC peg surface (deposits, withdrawals, raw
1620
1712
  * events, scoreboard). The only productized decoded sBTC peg feed on Stacks. */
1621
1713
  interface SbtcResource {
@@ -1624,7 +1716,9 @@ interface SbtcResource {
1624
1716
  walk(params?: SbtcDepositsWalkParams): AsyncIterable<IndexSbtcDeposit>
1625
1717
  /** Checkpointed sweep of completed deposits — append-only, so safe to
1626
1718
  * mirror. See {@link IndexConsumeOptions}. */
1627
- consume(params: SbtcDepositsConsumeParams): Promise<{
1719
+ consume<TTx = never>(params: SbtcDepositsConsumeParams<TTx> & {
1720
+ sink?: ConsumerSink<TTx>
1721
+ }): Promise<{
1628
1722
  cursor: string | null
1629
1723
  pages: number
1630
1724
  emptyPolls: number
@@ -1643,7 +1737,9 @@ interface SbtcResource {
1643
1737
  walk(params?: SbtcEventsWalkParams): AsyncIterable<IndexSbtcEvent>
1644
1738
  /** Checkpointed sweep of the raw peg event log — append-only across all
1645
1739
  * six topics. See {@link IndexConsumeOptions}. */
1646
- consume(params: SbtcEventsConsumeParams): Promise<{
1740
+ consume<TTx = never>(params: SbtcEventsConsumeParams<TTx> & {
1741
+ sink?: ConsumerSink<TTx>
1742
+ }): Promise<{
1647
1743
  cursor: string | null
1648
1744
  pages: number
1649
1745
  emptyPolls: number
@@ -1793,7 +1889,12 @@ interface IndexEventsResource {
1793
1889
  <T extends IndexEventType>(params: EventsListParams<T>): Promise<EventsEnvelope<T>>;
1794
1890
  list<T extends IndexEventType>(params: EventsListParams<T>): Promise<EventsEnvelope<T>>;
1795
1891
  walk<T extends IndexEventType>(params: EventsWalkParams<T>): AsyncIterable<IndexEventOf<T>>;
1796
- consume<T extends IndexEventType>(params: EventsConsumeParams<T>): Promise<{
1892
+ consume<
1893
+ T extends IndexEventType,
1894
+ TTx = never
1895
+ >(params: EventsConsumeParams<T, TTx> & {
1896
+ sink?: ConsumerSink<TTx>
1897
+ }): Promise<{
1797
1898
  cursor: string | null
1798
1899
  pages: number
1799
1900
  emptyPolls: number
@@ -1839,7 +1940,9 @@ declare class Index extends BaseClient {
1839
1940
  readonly contractCalls: {
1840
1941
  list: (params?: ContractCallsListParams) => Promise<ContractCallsEnvelope>
1841
1942
  walk: (params?: ContractCallsWalkParams) => AsyncIterable<IndexContractCall>
1842
- consume: (params: ContractCallsConsumeParams) => Promise<{
1943
+ consume: <TTx = never>(params: ContractCallsConsumeParams<TTx> & {
1944
+ sink?: ConsumerSink<TTx>
1945
+ }) => Promise<{
1843
1946
  cursor: string | null
1844
1947
  pages: number
1845
1948
  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;
@@ -1313,6 +1347,175 @@ class Projects extends BaseClient {
1313
1347
  // src/streams/client.ts
1314
1348
  import { ed25519 as ed255192 } from "@secondlayer/shared";
1315
1349
 
1350
+ // src/streams/_payload.ts
1351
+ function requireString(payload, field, eventType) {
1352
+ const value = payload[field];
1353
+ if (typeof value !== "string" || value.length === 0) {
1354
+ throw new Error(`${eventType} payload missing ${field}`);
1355
+ }
1356
+ return value;
1357
+ }
1358
+ function optionalString(value) {
1359
+ return typeof value === "string" && value.length > 0 ? value : null;
1360
+ }
1361
+ function requireAmountField(payload, field, eventType) {
1362
+ const amount = requireString(payload, field, eventType);
1363
+ if (!/^(0|[1-9]\d*)$/.test(amount)) {
1364
+ throw new Error(`${eventType} payload has malformed ${field}`);
1365
+ }
1366
+ return amount;
1367
+ }
1368
+ function requireAmount(payload, eventType) {
1369
+ return requireAmountField(payload, "amount", eventType);
1370
+ }
1371
+ function parseAssetIdentifier(assetIdentifier, eventType) {
1372
+ const [contractId, tokenName] = assetIdentifier.split("::");
1373
+ if (!contractId) {
1374
+ throw new Error(`${eventType} payload has malformed asset_identifier`);
1375
+ }
1376
+ return {
1377
+ contract_id: contractId,
1378
+ token_name: tokenName && tokenName.length > 0 ? tokenName : null
1379
+ };
1380
+ }
1381
+ function requireHexValue(payload, eventType) {
1382
+ const rawValue = payload.raw_value;
1383
+ if (typeof rawValue === "string") {
1384
+ if (!/^0x[0-9a-fA-F]*$/.test(rawValue)) {
1385
+ throw new Error(`${eventType} payload has malformed value`);
1386
+ }
1387
+ return rawValue;
1388
+ }
1389
+ const value = payload.value;
1390
+ const hex = typeof value === "string" ? value : value && typeof value === "object" && typeof value.hex === "string" ? value.hex : null;
1391
+ if (!hex) {
1392
+ throw new Error(`${eventType} payload missing value`);
1393
+ }
1394
+ if (!/^0x[0-9a-fA-F]*$/.test(hex)) {
1395
+ throw new Error(`${eventType} payload has malformed value`);
1396
+ }
1397
+ return hex;
1398
+ }
1399
+ function decodedRow(event, eventType, decoded_payload) {
1400
+ return {
1401
+ cursor: event.cursor,
1402
+ block_height: event.block_height,
1403
+ tx_id: event.tx_id,
1404
+ tx_index: event.tx_index,
1405
+ event_index: event.event_index,
1406
+ event_type: eventType,
1407
+ decoded_payload,
1408
+ source_cursor: event.cursor
1409
+ };
1410
+ }
1411
+
1412
+ // src/streams/decode.ts
1413
+ function decode(event) {
1414
+ const base = {
1415
+ cursor: event.cursor,
1416
+ block_height: event.block_height,
1417
+ block_time: event.ts,
1418
+ tx_id: event.tx_id,
1419
+ tx_index: event.tx_index,
1420
+ event_index: event.event_index,
1421
+ contract_id: event.contract_id
1422
+ };
1423
+ const p = event.payload;
1424
+ switch (event.event_type) {
1425
+ case "stx_transfer":
1426
+ return {
1427
+ ...base,
1428
+ event_type: "stx_transfer",
1429
+ sender: requireString(p, "sender", event.event_type),
1430
+ recipient: requireString(p, "recipient", event.event_type),
1431
+ amount: requireAmountField(p, "amount", event.event_type),
1432
+ memo: optionalString(p.memo)
1433
+ };
1434
+ case "stx_mint":
1435
+ return {
1436
+ ...base,
1437
+ event_type: "stx_mint",
1438
+ recipient: requireString(p, "recipient", event.event_type),
1439
+ amount: requireAmountField(p, "amount", event.event_type)
1440
+ };
1441
+ case "stx_burn":
1442
+ return {
1443
+ ...base,
1444
+ event_type: "stx_burn",
1445
+ sender: requireString(p, "sender", event.event_type),
1446
+ amount: requireAmountField(p, "amount", event.event_type)
1447
+ };
1448
+ case "stx_lock":
1449
+ return {
1450
+ ...base,
1451
+ event_type: "stx_lock",
1452
+ sender: requireString(p, "locked_address", event.event_type),
1453
+ amount: requireAmountField(p, "locked_amount", event.event_type),
1454
+ payload: { unlock_height: optionalString(p.unlock_height) }
1455
+ };
1456
+ case "ft_transfer":
1457
+ return {
1458
+ ...base,
1459
+ event_type: "ft_transfer",
1460
+ asset_identifier: requireString(p, "asset_identifier", event.event_type),
1461
+ sender: requireString(p, "sender", event.event_type),
1462
+ recipient: requireString(p, "recipient", event.event_type),
1463
+ amount: requireAmountField(p, "amount", event.event_type)
1464
+ };
1465
+ case "ft_mint":
1466
+ return {
1467
+ ...base,
1468
+ event_type: "ft_mint",
1469
+ asset_identifier: requireString(p, "asset_identifier", event.event_type),
1470
+ recipient: requireString(p, "recipient", event.event_type),
1471
+ amount: requireAmountField(p, "amount", event.event_type)
1472
+ };
1473
+ case "ft_burn":
1474
+ return {
1475
+ ...base,
1476
+ event_type: "ft_burn",
1477
+ asset_identifier: requireString(p, "asset_identifier", event.event_type),
1478
+ sender: requireString(p, "sender", event.event_type),
1479
+ amount: requireAmountField(p, "amount", event.event_type)
1480
+ };
1481
+ case "nft_transfer":
1482
+ return {
1483
+ ...base,
1484
+ event_type: "nft_transfer",
1485
+ asset_identifier: requireString(p, "asset_identifier", event.event_type),
1486
+ sender: requireString(p, "sender", event.event_type),
1487
+ recipient: requireString(p, "recipient", event.event_type),
1488
+ value: requireHexValue(p, event.event_type)
1489
+ };
1490
+ case "nft_mint":
1491
+ return {
1492
+ ...base,
1493
+ event_type: "nft_mint",
1494
+ asset_identifier: requireString(p, "asset_identifier", event.event_type),
1495
+ recipient: requireString(p, "recipient", event.event_type),
1496
+ value: requireHexValue(p, event.event_type)
1497
+ };
1498
+ case "nft_burn":
1499
+ return {
1500
+ ...base,
1501
+ event_type: "nft_burn",
1502
+ asset_identifier: requireString(p, "asset_identifier", event.event_type),
1503
+ sender: requireString(p, "sender", event.event_type),
1504
+ value: requireHexValue(p, event.event_type)
1505
+ };
1506
+ case "print":
1507
+ return {
1508
+ ...base,
1509
+ event_type: "print",
1510
+ payload: {
1511
+ topic: optionalString(p.topic),
1512
+ value: p.value,
1513
+ raw_value: optionalString(p.raw_value)
1514
+ }
1515
+ };
1516
+ }
1517
+ }
1518
+
1316
1519
  // src/streams/dumps.ts
1317
1520
  import { createHash } from "node:crypto";
1318
1521
  import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
@@ -1687,8 +1890,11 @@ function createStreamsClient(options) {
1687
1890
  return request(`/v1/streams/events/${encodeURIComponent(txId)}`);
1688
1891
  },
1689
1892
  consume(params) {
1893
+ const onBatch = params.decoded ? (events, envelope, ctx) => params.onBatch(events.map(decode), envelope, ctx) : params.onBatch;
1690
1894
  return consumeStreamsEvents({
1691
1895
  fromCursor: params.fromCursor,
1896
+ sink: params.sink,
1897
+ onProgress: params.onProgress,
1692
1898
  mode: params.mode,
1693
1899
  finalizedOnly: params.finalizedOnly,
1694
1900
  types: params.types,
@@ -1699,7 +1905,7 @@ function createStreamsClient(options) {
1699
1905
  assetIdentifier: params.assetIdentifier,
1700
1906
  batchSize: params.batchSize ?? 100,
1701
1907
  fetchEvents,
1702
- onBatch: params.onBatch,
1908
+ onBatch,
1703
1909
  onReorg: params.onReorg,
1704
1910
  emptyBackoffMs: params.emptyBackoffMs,
1705
1911
  maxPages: params.maxPages,
@@ -1942,5 +2148,5 @@ export {
1942
2148
  Subgraphs
1943
2149
  };
1944
2150
 
1945
- //# debugId=A36A74FD0514129964756E2164756E21
2151
+ //# debugId=E16E22A43378758864756E2164756E21
1946
2152
  //# sourceMappingURL=index.js.map