@secondlayer/sdk 6.37.0 → 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.
- package/dist/index.d.ts +184 -24
- package/dist/index.js +265 -78
- 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 +124 -21
- package/dist/subgraphs/index.js +222 -16
- package/dist/subgraphs/index.js.map +9 -7
- package/package.json +20 -7
|
@@ -277,6 +277,8 @@ declare class Contracts extends BaseClient {
|
|
|
277
277
|
includeAbi?: boolean
|
|
278
278
|
}): Promise<ContractSummary | null>;
|
|
279
279
|
}
|
|
280
|
+
import { Pox5EventTopic } from "@secondlayer/stacks/pox5";
|
|
281
|
+
import { SbtcEventTopic } from "@secondlayer/stacks/sbtc";
|
|
280
282
|
import { InferredTopicSchema } from "@secondlayer/subgraphs";
|
|
281
283
|
import { RewardSet } from "@secondlayer/shared/node/consensus";
|
|
282
284
|
import { MerkleProofStep } from "@secondlayer/shared/node/nakamoto";
|
|
@@ -314,6 +316,55 @@ interface TransactionProof {
|
|
|
314
316
|
reward_set: RewardSet
|
|
315
317
|
};
|
|
316
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
|
+
};
|
|
317
368
|
import { StreamsEventType } from "@secondlayer/shared";
|
|
318
369
|
/** A Clarity value as Streams serves it: the canonical hex string, a typed
|
|
319
370
|
* object carrying that hex (`{ hex }`), or a decoded Clarity-JSON object.
|
|
@@ -556,8 +607,28 @@ type StreamsBatchContext = ConsumerBatchContext;
|
|
|
556
607
|
type StreamsReorgContext = {
|
|
557
608
|
cursor: string
|
|
558
609
|
};
|
|
559
|
-
type StreamsEventsConsumeParams
|
|
610
|
+
type StreamsEventsConsumeParams<
|
|
611
|
+
TTx = never,
|
|
612
|
+
D extends boolean = false
|
|
613
|
+
> = {
|
|
560
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
|
|
561
632
|
mode?: "tail" | "bounded"
|
|
562
633
|
/**
|
|
563
634
|
* Emit only finalized (immutable) events and never surface reorgs. The SDK
|
|
@@ -578,7 +649,7 @@ type StreamsEventsConsumeParams = {
|
|
|
578
649
|
* transaction as your writes. Returning a cursor overrides `ctx.cursor` as
|
|
579
650
|
* the resume point (advanced manual control); returning nothing uses it.
|
|
580
651
|
*/
|
|
581
|
-
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>
|
|
582
653
|
/**
|
|
583
654
|
* Roll your projection back to `reorg.fork_point_height`, persisting
|
|
584
655
|
* `ctx.cursor` in the same transaction. Called once per *new* reorg
|
|
@@ -735,7 +806,12 @@ type StreamsClient = {
|
|
|
735
806
|
* The consumer also exits when `maxPages`, `maxEmptyPolls`, or `signal`
|
|
736
807
|
* stops it.
|
|
737
808
|
*/
|
|
738
|
-
consume
|
|
809
|
+
consume<
|
|
810
|
+
TTx = never,
|
|
811
|
+
D extends boolean = false
|
|
812
|
+
>(params: StreamsEventsConsumeParams<TTx, D> & {
|
|
813
|
+
sink?: ConsumerSink<TTx>
|
|
814
|
+
}): Promise<StreamsEventsConsumeResult>
|
|
739
815
|
/**
|
|
740
816
|
* Backfill from bulk dumps, then continue live from the dump→live seam in
|
|
741
817
|
* one call. Iterates finalized dump files (via `onDumpFile`) in block
|
|
@@ -816,16 +892,18 @@ type IndexFeedEnvelope = {
|
|
|
816
892
|
};
|
|
817
893
|
/** Consumer options shared by `index.events.consume` and
|
|
818
894
|
* `index.contractCalls.consume`. Same contract as the Streams consumer:
|
|
819
|
-
* 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. */
|
|
820
897
|
type IndexConsumeOptions<
|
|
821
898
|
TItem extends IndexFeedItem,
|
|
822
|
-
TEnvelope extends IndexFeedEnvelope
|
|
899
|
+
TEnvelope extends IndexFeedEnvelope,
|
|
900
|
+
TTx = never
|
|
823
901
|
> = {
|
|
824
902
|
/** Resume from a committed checkpoint. Without it (and without
|
|
825
903
|
* `fromHeight`) the API serves only the recent default window. */
|
|
826
904
|
fromCursor?: string | null
|
|
827
905
|
/** Start a fresh sweep at this height (e.g. `0` for genesis backfill).
|
|
828
|
-
* Ignored once a cursor exists. */
|
|
906
|
+
* Ignored once a cursor exists (including a sink's committed cursor). */
|
|
829
907
|
fromHeight?: number
|
|
830
908
|
/** `tail` (default) keeps polling at the tip; `bounded` returns on the
|
|
831
909
|
* first empty page. */
|
|
@@ -835,7 +913,20 @@ type IndexConsumeOptions<
|
|
|
835
913
|
* never reorgs, so `onReorg` is skipped entirely. */
|
|
836
914
|
finalizedOnly?: boolean
|
|
837
915
|
batchSize?: number
|
|
838
|
-
|
|
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>
|
|
839
930
|
onReorg?: (reorg: IndexReorg, ctx: {
|
|
840
931
|
cursor: string
|
|
841
932
|
}) => Promise<void> | void
|
|
@@ -1101,7 +1192,10 @@ type EventsWalkParams<T extends IndexEventType = IndexEventType> = Omit<EventsLi
|
|
|
1101
1192
|
batchSize?: number
|
|
1102
1193
|
signal?: AbortSignal
|
|
1103
1194
|
};
|
|
1104
|
-
type EventsConsumeParams<
|
|
1195
|
+
type EventsConsumeParams<
|
|
1196
|
+
T extends IndexEventType = IndexEventType,
|
|
1197
|
+
TTx = never
|
|
1198
|
+
> = Omit<EventsListParams<T>, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexEventOf<T>, EventsEnvelope<T>, TTx>;
|
|
1105
1199
|
type IndexContractCall = {
|
|
1106
1200
|
cursor: string
|
|
1107
1201
|
block_height: number
|
|
@@ -1141,7 +1235,7 @@ type ContractCallsWalkParams = Omit<ContractCallsListParams, "limit"> & {
|
|
|
1141
1235
|
batchSize?: number
|
|
1142
1236
|
signal?: AbortSignal
|
|
1143
1237
|
};
|
|
1144
|
-
type ContractCallsConsumeParams = Omit<ContractCallsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexContractCall, ContractCallsEnvelope>;
|
|
1238
|
+
type ContractCallsConsumeParams<TTx = never> = Omit<ContractCallsListParams, "cursor" | "fromCursor" | "limit"> & IndexConsumeOptions<IndexContractCall, ContractCallsEnvelope, TTx>;
|
|
1145
1239
|
/** One canonical block in the sync map. Lean by design — block + parent hash
|
|
1146
1240
|
* for chain linkage, burn anchor for Bitcoin confirmations. Use `blocks` for
|
|
1147
1241
|
* the full block resource. */
|
|
@@ -1412,8 +1506,6 @@ type PrintSchemaResponse = {
|
|
|
1412
1506
|
}
|
|
1413
1507
|
tip: IndexTip
|
|
1414
1508
|
};
|
|
1415
|
-
/** The decoded `sbtc_events` topics. */
|
|
1416
|
-
type SbtcEventTopic = "completed-deposit" | "withdrawal-create" | "withdrawal-accept" | "withdrawal-reject" | "key-rotation" | "update-protocol-contract";
|
|
1417
1509
|
type SbtcWithdrawalStatus = "REQUESTED" | "ACCEPTED" | "REJECTED";
|
|
1418
1510
|
/** A completed sBTC peg-in, keyed by `bitcoin_txid` (deposits carry no
|
|
1419
1511
|
* `request_id`). One terminal `completed-deposit` event per deposit. */
|
|
@@ -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
|
|
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
|
|
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
|
|
@@ -1703,10 +1799,10 @@ interface PoxResource {
|
|
|
1703
1799
|
get(rewardCycle: number): Promise<PoxCycleEnvelope | null>
|
|
1704
1800
|
};
|
|
1705
1801
|
}
|
|
1706
|
-
/** The decoded `pox-5` boot-contract print topics (SIP-045 Bitcoin Staking)
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
type IndexPox5EventTopic =
|
|
1802
|
+
/** The decoded `pox-5` boot-contract print topics (SIP-045 Bitcoin Staking) —
|
|
1803
|
+
* one source of truth in `@secondlayer/stacks/pox5` (already a dependency;
|
|
1804
|
+
* the old "SDK owns its own dependency surface" rationale predated that). */
|
|
1805
|
+
type IndexPox5EventTopic = Pox5EventTopic;
|
|
1710
1806
|
/** A raw decoded PoX-5 print event. Promoted fields cover the hot query paths;
|
|
1711
1807
|
* `data` always carries the full decoded tuple, including nested shapes
|
|
1712
1808
|
* (`btc-lockup`, `bond-rewards`, `bond-periods`) that have no flat column. */
|
|
@@ -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<
|
|
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
|
|
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
|
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;
|
|
@@ -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
|
|
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=
|
|
2151
|
+
//# debugId=E16E22A43378758864756E2164756E21
|
|
1946
2152
|
//# sourceMappingURL=index.js.map
|