@secondlayer/sdk 5.8.0 → 5.9.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.
@@ -13,11 +13,21 @@ type StreamsEvent = {
13
13
  contract_id: string | null
14
14
  payload: StreamsEventPayload
15
15
  ts: string
16
+ /**
17
+ * True when this event's block is past the finality boundary (immutable).
18
+ * Optional for back-compat; the API always sets it on Streams responses.
19
+ */
20
+ finalized?: boolean
16
21
  };
17
22
  type StreamsTip = {
18
23
  block_height: number
19
24
  block_hash: string
20
25
  burn_block_height: number
26
+ /**
27
+ * Highest Stacks block past the burn-confirmation finality boundary.
28
+ * Optional for back-compat; the API always sets it.
29
+ */
30
+ finalized_height?: number
21
31
  lag_seconds: number
22
32
  };
23
33
  type StreamsCanonicalBlock = {
@@ -57,12 +67,18 @@ type StreamsEventsListParams = {
57
67
  toHeight?: number
58
68
  types?: readonly StreamsEventType[]
59
69
  contractId?: string
70
+ sender?: string
71
+ recipient?: string
72
+ assetIdentifier?: string
60
73
  limit?: number
61
74
  };
62
75
  type StreamsEventsStreamParams = {
63
76
  fromCursor?: string | null
64
77
  types?: readonly StreamsEventType[]
65
78
  contractId?: string
79
+ sender?: string
80
+ recipient?: string
81
+ assetIdentifier?: string
66
82
  batchSize?: number
67
83
  emptyBackoffMs?: number
68
84
  maxPages?: number
@@ -74,6 +90,9 @@ type StreamsEventsConsumeParams = {
74
90
  mode?: "tail" | "bounded"
75
91
  types?: readonly StreamsEventType[]
76
92
  contractId?: string
93
+ sender?: string
94
+ recipient?: string
95
+ assetIdentifier?: string
77
96
  batchSize?: number
78
97
  onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope) => Promise<string | null | undefined> | string | null | undefined
79
98
  emptyBackoffMs?: number
@@ -86,7 +105,63 @@ type StreamsEventsConsumeResult = {
86
105
  pages: number
87
106
  emptyPolls: number
88
107
  };
108
+ type StreamsEventsReplayParams = {
109
+ /** Start point: `"genesis"` (default) or a `<block>:<index>` cursor. */
110
+ from?: "genesis" | string
111
+ /**
112
+ * Called once per finalized dump file, in block order, before live tailing.
113
+ * Process the parquet with your own tooling (e.g. DuckDB) — the SDK does not
114
+ * decode parquet. Use `client.dumps.download(file)` to fetch + verify bytes.
115
+ */
116
+ onDumpFile: (file: StreamsDumpFile) => Promise<void> | void
117
+ /** Called per live page after the dump phase, like `consume`. */
118
+ onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope) => Promise<string | null | undefined> | string | null | undefined
119
+ mode?: "tail" | "bounded"
120
+ batchSize?: number
121
+ emptyBackoffMs?: number
122
+ maxPages?: number
123
+ maxEmptyPolls?: number
124
+ signal?: AbortSignal
125
+ };
89
126
  type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
127
+ /** One bulk parquet file in the dumps manifest. `path` is the object key under
128
+ * the dumps base URL. */
129
+ type StreamsDumpFile = {
130
+ path: string
131
+ from_block: number
132
+ to_block: number
133
+ min_cursor: string | null
134
+ max_cursor: string | null
135
+ row_count: number
136
+ byte_size: number
137
+ sha256: string
138
+ schema_version: number
139
+ created_at: string
140
+ };
141
+ type StreamsDumpsManifest = {
142
+ dataset: string
143
+ network: string
144
+ version: string
145
+ schema_version: number
146
+ generated_at: string
147
+ producer_version: string
148
+ finality_lag_blocks: number
149
+ /** Cursor at the end of the finalized bulk coverage — hand to live tailing. */
150
+ latest_finalized_cursor: string | null
151
+ coverage: {
152
+ from_block: number
153
+ to_block: number
154
+ }
155
+ files: StreamsDumpFile[]
156
+ };
157
+ type StreamsDumps = {
158
+ /** Fetch and parse the latest dumps manifest. */
159
+ list(): Promise<StreamsDumpsManifest>
160
+ /** Absolute URL for a manifest file. */
161
+ fileUrl(file: StreamsDumpFile): string
162
+ /** Download a parquet file and verify its sha256 against the manifest. */
163
+ download(file: StreamsDumpFile): Promise<Uint8Array>
164
+ };
90
165
  type StreamsClient = {
91
166
  events: {
92
167
  list(params?: StreamsEventsListParams): Promise<StreamsEventsEnvelope>
@@ -102,6 +177,14 @@ type StreamsClient = {
102
177
  */
103
178
  consume(params: StreamsEventsConsumeParams): Promise<StreamsEventsConsumeResult>
104
179
  /**
180
+ * Backfill from bulk dumps, then continue live from the dump→live seam in
181
+ * one call. Iterates finalized dump files (via `onDumpFile`) in block
182
+ * order, then tails live from the manifest's `latest_finalized_cursor`
183
+ * (exclusive input → no gap or duplicate at the seam). Requires
184
+ * `dumpsBaseUrl`.
185
+ */
186
+ replay(params: StreamsEventsReplayParams): Promise<StreamsEventsConsumeResult>
187
+ /**
105
188
  * Follow Streams as an async iterator.
106
189
  *
107
190
  * Use `stream` for live processors and watch-style apps. It tails
@@ -116,6 +199,8 @@ type StreamsClient = {
116
199
  reorgs: {
117
200
  list(params: StreamsReorgsListParams): Promise<StreamsReorgsListEnvelope>
118
201
  }
202
+ /** Bulk parquet dumps. Requires `dumpsBaseUrl` on the client. */
203
+ dumps: StreamsDumps
119
204
  canonical(height: number): Promise<StreamsCanonicalBlock>
120
205
  tip(): Promise<StreamsTip>
121
206
  };
@@ -123,6 +208,20 @@ type CreateStreamsClientOptions = {
123
208
  apiKey: string
124
209
  baseUrl?: string
125
210
  fetchImpl?: FetchLike
211
+ /**
212
+ * Public base URL for bulk parquet dumps (the R2/CDN bucket root). Required
213
+ * to use `client.dumps`. See `GET /public/streams/dumps/manifest`.
214
+ */
215
+ dumpsBaseUrl?: string
216
+ /**
217
+ * Verify the ed25519 `X-Signature` on every response (default off). Pass
218
+ * `true` to fetch the server's public key from
219
+ * `/public/streams/signing-key`, or `{ publicKey }` to pin a known PEM. A
220
+ * failed or missing signature throws `StreamsSignatureError`.
221
+ */
222
+ verify?: boolean | {
223
+ publicKey: string
224
+ }
126
225
  };
127
226
  declare function createStreamsClient(options: CreateStreamsClientOptions): StreamsClient;
128
227
  declare class AuthError extends Error {
@@ -144,6 +243,10 @@ declare class StreamsServerError extends Error {
144
243
  readonly body?: unknown;
145
244
  constructor(message: string, status: number, body?: unknown);
146
245
  }
246
+ /** Thrown when response signature verification is enabled and fails. */
247
+ declare class StreamsSignatureError extends Error {
248
+ constructor(message?: string);
249
+ }
147
250
  type FtTransferPayload = {
148
251
  asset_identifier: string
149
252
  sender: string
@@ -410,4 +513,4 @@ type DecodedEventColumns = {
410
513
  payload?: unknown
411
514
  };
412
515
  type DecodedEventRow = DecodedFtTransfer | DecodedNftTransfer | DecodedStxTransfer | DecodedStxMint | DecodedStxBurn | DecodedStxLock | DecodedFtMint | DecodedFtBurn | DecodedNftMint | DecodedNftBurn | DecodedPrint;
413
- export { isStxTransfer, isStxMint, isStxLock, isStxBurn, isPrint, isNftTransfer, isNftMint, isNftBurn, isFtTransfer, isFtMint, isFtBurn, decodeStxTransfer, decodeStxMint, decodeStxLock, decodeStxBurn, decodePrint, decodeNftTransfer, decodeNftMint, decodeNftBurn, decodeFtTransfer, decodeFtMint, decodeFtBurn, createStreamsClient, ValidationError, StreamsTip, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorg, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsClient, StreamsCanonicalBlock, STREAMS_EVENT_TYPES, RateLimitError, NftTransferPayload, NftTransferEvent, FtTransferPayload, FtTransferEvent, FetchLike, DecodedStxTransferPayload, DecodedStxTransfer, DecodedStxMintPayload, DecodedStxMint, DecodedStxLockPayload, DecodedStxLock, DecodedStxBurnPayload, DecodedStxBurn, DecodedPrintValue, DecodedPrintPayload, DecodedPrint, DecodedNftTransferPayload, DecodedNftTransfer, DecodedNftMintPayload, DecodedNftMint, DecodedNftBurnPayload, DecodedNftBurn, DecodedFtTransferPayload, DecodedFtTransfer, DecodedFtMintPayload, DecodedFtMint, DecodedFtBurnPayload, DecodedFtBurn, DecodedEventRow, DecodedEventColumns, AuthError };
516
+ export { isStxTransfer, isStxMint, isStxLock, isStxBurn, isPrint, isNftTransfer, isNftMint, isNftBurn, isFtTransfer, isFtMint, isFtBurn, decodeStxTransfer, decodeStxMint, decodeStxLock, decodeStxBurn, decodePrint, decodeNftTransfer, decodeNftMint, decodeNftBurn, decodeFtTransfer, decodeFtMint, decodeFtBurn, createStreamsClient, ValidationError, StreamsTip, StreamsSignatureError, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorg, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsDumpsManifest, StreamsDumps, StreamsDumpFile, StreamsClient, StreamsCanonicalBlock, STREAMS_EVENT_TYPES, RateLimitError, NftTransferPayload, NftTransferEvent, FtTransferPayload, FtTransferEvent, FetchLike, DecodedStxTransferPayload, DecodedStxTransfer, DecodedStxMintPayload, DecodedStxMint, DecodedStxLockPayload, DecodedStxLock, DecodedStxBurnPayload, DecodedStxBurn, DecodedPrintValue, DecodedPrintPayload, DecodedPrint, DecodedNftTransferPayload, DecodedNftTransfer, DecodedNftMintPayload, DecodedNftMint, DecodedNftBurnPayload, DecodedNftBurn, DecodedFtTransferPayload, DecodedFtTransfer, DecodedFtMintPayload, DecodedFtMint, DecodedFtBurnPayload, DecodedFtBurn, DecodedEventRow, DecodedEventColumns, AuthError };
@@ -1,3 +1,6 @@
1
+ // src/streams/client.ts
2
+ import { ed25519 } from "@secondlayer/shared";
3
+
1
4
  // src/streams/consumer.ts
2
5
  async function defaultSleep(ms, signal) {
3
6
  if (signal?.aborted)
@@ -26,7 +29,10 @@ async function consumeStreamsEvents(opts) {
26
29
  cursor,
27
30
  limit: opts.batchSize,
28
31
  types: opts.types,
29
- contractId: opts.contractId
32
+ contractId: opts.contractId,
33
+ sender: opts.sender,
34
+ recipient: opts.recipient,
35
+ assetIdentifier: opts.assetIdentifier
30
36
  });
31
37
  pages++;
32
38
  const returnedCursor = await opts.onBatch(envelope.events, envelope);
@@ -61,7 +67,10 @@ async function* streamStreamsEvents(opts) {
61
67
  cursor,
62
68
  limit: opts.batchSize,
63
69
  types: opts.types,
64
- contractId: opts.contractId
70
+ contractId: opts.contractId,
71
+ sender: opts.sender,
72
+ recipient: opts.recipient,
73
+ assetIdentifier: opts.assetIdentifier
65
74
  });
66
75
  pages++;
67
76
  for (const event of envelope.events) {
@@ -86,6 +95,9 @@ async function* streamStreamsEvents(opts) {
86
95
  }
87
96
  }
88
97
 
98
+ // src/streams/dumps.ts
99
+ import { createHash } from "node:crypto";
100
+
89
101
  // src/streams/errors.ts
90
102
  class AuthError extends Error {
91
103
  status = 401;
@@ -127,7 +139,60 @@ class StreamsServerError extends Error {
127
139
  }
128
140
  }
129
141
 
142
+ class StreamsSignatureError extends Error {
143
+ constructor(message = "Streams response signature verification failed.") {
144
+ super(message);
145
+ this.name = "StreamsSignatureError";
146
+ }
147
+ }
148
+
149
+ // src/streams/dumps.ts
150
+ function createStreamsDumps(opts) {
151
+ const baseUrl = opts.baseUrl?.replace(/\/+$/, "");
152
+ function requireBaseUrl() {
153
+ if (!baseUrl) {
154
+ throw new StreamsServerError("Streams dumps require `dumpsBaseUrl` on createStreamsClient.", 0);
155
+ }
156
+ return baseUrl;
157
+ }
158
+ function fileUrl(file) {
159
+ return `${requireBaseUrl()}/${file.path.replace(/^\/+/, "")}`;
160
+ }
161
+ async function list() {
162
+ const url = `${requireBaseUrl()}/manifest/latest.json`;
163
+ const res = await opts.fetchImpl(url);
164
+ if (!res.ok) {
165
+ throw new StreamsServerError(`Could not fetch dumps manifest (${res.status}).`, res.status);
166
+ }
167
+ return await res.json();
168
+ }
169
+ async function download(file) {
170
+ const res = await opts.fetchImpl(fileUrl(file));
171
+ if (!res.ok) {
172
+ throw new StreamsServerError(`Could not download dump ${file.path} (${res.status}).`, res.status);
173
+ }
174
+ const bytes = new Uint8Array(await res.arrayBuffer());
175
+ const digest = createHash("sha256").update(bytes).digest("hex");
176
+ if (digest !== file.sha256) {
177
+ throw new StreamsSignatureError(`Dump ${file.path} sha256 mismatch (expected ${file.sha256}, got ${digest}).`);
178
+ }
179
+ return bytes;
180
+ }
181
+ return { list, fileUrl, download };
182
+ }
183
+
130
184
  // src/streams/client.ts
185
+ function cursorTuple(cursor) {
186
+ if (!cursor)
187
+ return [-1, -1];
188
+ const [block, index] = cursor.split(":");
189
+ return [Number(block), Number(index)];
190
+ }
191
+ function maxCursor(a, b) {
192
+ const [ah, ai] = cursorTuple(a);
193
+ const [bh, bi] = cursorTuple(b);
194
+ return ah > bh || ah === bh && ai >= bi ? a : b;
195
+ }
131
196
  var DEFAULT_STREAMS_BASE_URL = "https://api.secondlayer.tools";
132
197
  function normalizeBaseUrl(baseUrl) {
133
198
  return baseUrl.replace(/\/+$/, "");
@@ -175,21 +240,68 @@ async function mapStreamsError(response) {
175
240
  function createStreamsClient(options) {
176
241
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_STREAMS_BASE_URL);
177
242
  const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
243
+ const verify = options.verify ?? false;
244
+ const dumps = createStreamsDumps({
245
+ baseUrl: options.dumpsBaseUrl,
246
+ fetchImpl
247
+ });
248
+ let publicKeyPromise = null;
249
+ function getPublicKey() {
250
+ if (publicKeyPromise)
251
+ return publicKeyPromise;
252
+ publicKeyPromise = (async () => {
253
+ if (typeof verify === "object") {
254
+ return ed25519.loadEd25519PublicKey(verify.publicKey);
255
+ }
256
+ const res = await fetchImpl(`${baseUrl}/public/streams/signing-key`);
257
+ if (!res.ok) {
258
+ throw new StreamsSignatureError(`Could not fetch signing key (${res.status}).`);
259
+ }
260
+ const body = await res.json();
261
+ if (!body.public_key_pem) {
262
+ throw new StreamsSignatureError("Signing key response missing key.");
263
+ }
264
+ return ed25519.loadEd25519PublicKey(body.public_key_pem);
265
+ })();
266
+ return publicKeyPromise;
267
+ }
178
268
  async function request(path) {
179
269
  const response = await fetchImpl(`${baseUrl}${path}`, {
180
270
  headers: { Authorization: `Bearer ${options.apiKey}` }
181
271
  });
182
272
  if (!response.ok)
183
273
  await mapStreamsError(response);
184
- return await response.json();
274
+ const text = await response.text();
275
+ if (verify) {
276
+ const signature = response.headers.get("X-Signature");
277
+ if (!signature) {
278
+ throw new StreamsSignatureError("Response is missing X-Signature.");
279
+ }
280
+ const publicKey = await getPublicKey();
281
+ if (!ed25519.verifyEd25519(text, signature, publicKey)) {
282
+ throw new StreamsSignatureError;
283
+ }
284
+ }
285
+ return JSON.parse(text);
185
286
  }
186
287
  const fetchEvents = async ({
187
288
  cursor,
188
289
  limit,
189
290
  types,
190
- contractId
291
+ contractId,
292
+ sender,
293
+ recipient,
294
+ assetIdentifier
191
295
  }) => {
192
- return listEvents({ cursor, limit, types, contractId });
296
+ return listEvents({
297
+ cursor,
298
+ limit,
299
+ types,
300
+ contractId,
301
+ sender,
302
+ recipient,
303
+ assetIdentifier
304
+ });
193
305
  };
194
306
  async function listEvents(params = {}) {
195
307
  const searchParams = new URLSearchParams;
@@ -198,6 +310,9 @@ function createStreamsClient(options) {
198
310
  appendSearchParam(searchParams, "to_height", params.toHeight);
199
311
  appendSearchParam(searchParams, "limit", params.limit);
200
312
  appendSearchParam(searchParams, "contract_id", params.contractId);
313
+ appendSearchParam(searchParams, "sender", params.sender);
314
+ appendSearchParam(searchParams, "recipient", params.recipient);
315
+ appendSearchParam(searchParams, "asset_identifier", params.assetIdentifier);
201
316
  if (params.types?.length) {
202
317
  searchParams.set("types", params.types.join(","));
203
318
  }
@@ -216,6 +331,9 @@ function createStreamsClient(options) {
216
331
  mode: params.mode,
217
332
  types: params.types,
218
333
  contractId: params.contractId,
334
+ sender: params.sender,
335
+ recipient: params.recipient,
336
+ assetIdentifier: params.assetIdentifier,
219
337
  batchSize: params.batchSize ?? 100,
220
338
  fetchEvents,
221
339
  onBatch: params.onBatch,
@@ -230,6 +348,9 @@ function createStreamsClient(options) {
230
348
  fromCursor: params.fromCursor,
231
349
  types: params.types,
232
350
  contractId: params.contractId,
351
+ sender: params.sender,
352
+ recipient: params.recipient,
353
+ assetIdentifier: params.assetIdentifier,
233
354
  batchSize: params.batchSize ?? 100,
234
355
  emptyBackoffMs: params.emptyBackoffMs,
235
356
  maxPages: params.maxPages,
@@ -237,6 +358,29 @@ function createStreamsClient(options) {
237
358
  signal: params.signal,
238
359
  fetchEvents
239
360
  });
361
+ },
362
+ async replay(params) {
363
+ const fromCursor = params.from === "genesis" ? null : params.from ?? null;
364
+ const fromBlock = fromCursor ? cursorTuple(fromCursor)[0] : 0;
365
+ const manifest = await dumps.list();
366
+ const files = manifest.files.filter((file) => file.to_block >= fromBlock).sort((a, b) => a.from_block - b.from_block || a.to_block - b.to_block);
367
+ for (const file of files) {
368
+ if (params.signal?.aborted)
369
+ break;
370
+ await params.onDumpFile(file);
371
+ }
372
+ const seam = maxCursor(fromCursor, manifest.latest_finalized_cursor);
373
+ return consumeStreamsEvents({
374
+ fromCursor: seam,
375
+ mode: params.mode ?? "tail",
376
+ batchSize: params.batchSize ?? 100,
377
+ fetchEvents,
378
+ onBatch: params.onBatch,
379
+ emptyBackoffMs: params.emptyBackoffMs,
380
+ maxPages: params.maxPages,
381
+ maxEmptyPolls: params.maxEmptyPolls,
382
+ signal: params.signal
383
+ });
240
384
  }
241
385
  },
242
386
  blocks: {
@@ -253,6 +397,7 @@ function createStreamsClient(options) {
253
397
  return request(`/v1/streams/reorgs${query ? `?${query}` : ""}`);
254
398
  }
255
399
  },
400
+ dumps,
256
401
  canonical(height) {
257
402
  return request(`/v1/streams/canonical/${height}`);
258
403
  },
@@ -652,11 +797,12 @@ export {
652
797
  decodeFtBurn,
653
798
  createStreamsClient,
654
799
  ValidationError,
800
+ StreamsSignatureError,
655
801
  StreamsServerError,
656
802
  STREAMS_EVENT_TYPES,
657
803
  RateLimitError,
658
804
  AuthError
659
805
  };
660
806
 
661
- //# debugId=3A844A761118CEDE64756E2164756E21
807
+ //# debugId=AC24B4EAD103932F64756E2164756E21
662
808
  //# sourceMappingURL=index.js.map