@secondlayer/sdk 5.9.0 → 6.0.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/README.md CHANGED
@@ -19,9 +19,12 @@ const sl = new SecondLayer({
19
19
  });
20
20
  ```
21
21
 
22
- Reads are public during open beta — no key needed. Writes require an `sk-sl_`
23
- API key, created in the platform console at
24
- https://secondlayer.tools/platform/api-keys.
22
+ `sl.index` and `sl.subgraphs` reads are anonymous — no key needed. **`sl.streams`
23
+ reads require a bearer token** (`apiKey`) and resolve a per-tier tenant; a
24
+ publicly-known free-tier token exists but a bearer is always required. Writes
25
+ require an `sk-sl_` API key, created in the platform console at
26
+ https://secondlayer.tools/platform/api-keys. (Public Streams bulk dumps —
27
+ `client.dumps`, `events.replay` — need no key.)
25
28
 
26
29
  ## Mental model
27
30
 
@@ -31,16 +34,20 @@ https://secondlayer.tools/platform/api-keys.
31
34
 
32
35
  ## Stacks Streams
33
36
 
34
- Typed L1 HTTP client. Reads are public no key needed.
37
+ Typed L1 HTTP client. Reads require a bearer token (`apiKey`).
35
38
 
36
39
  ```typescript
37
40
  const tip = await sl.streams.tip();
41
+ // tip.finalized_height — highest immutable (past Bitcoin-anchored finality) block
38
42
  const page = await sl.streams.events.list({
39
43
  types: ["ft_transfer"],
40
44
  contractId: "SP...sbtc-token",
45
+ sender: "SP...", // exact payload sender (events that have one)
46
+ recipient: "SP...", // exact payload recipient
47
+ assetIdentifier: "SP...token::asset", // exact FT/NFT asset id
41
48
  limit: 10,
42
49
  });
43
-
50
+ // each event carries `finalized: boolean`
44
51
  console.log({ tip, firstCursor: page.events[0]?.cursor });
45
52
  ```
46
53
 
@@ -50,10 +57,19 @@ console.log({ tip, firstCursor: page.events[0]?.cursor });
50
57
  import { createStreamsClient } from "@secondlayer/sdk";
51
58
 
52
59
  const streams = createStreamsClient({
53
- apiKey: process.env.SL_API_KEY!, // sk-sl_...
60
+ apiKey: process.env.SL_API_KEY!, // sk-sl_... — required for reads
61
+ // verify: true, // verify ed25519 X-Signature on every read
62
+ // // (auto-fetches the public key; { publicKey } pins a PEM)
63
+ // dumpsBaseUrl: process.env.SL_STREAMS_DUMPS_URL, // required to use client.dumps
54
64
  });
55
65
  ```
56
66
 
67
+ Verified responses: every Streams read is signed (ed25519 `X-Signature` +
68
+ `X-Signature-KeyId`). Pass `verify: true` to check it on every read (or
69
+ `{ publicKey }` to pin a PEM); a missing/bad signature throws
70
+ `StreamsSignatureError`. The public key is at
71
+ `GET /public/streams/signing-key`.
72
+
57
73
  Convenience reads:
58
74
 
59
75
  ```typescript
@@ -102,6 +118,46 @@ for await (const event of streams.events.stream({
102
118
  }
103
119
  ```
104
120
 
121
+ Bulk parquet dumps.
122
+
123
+ Finalized history is published as public parquet files. Set `dumpsBaseUrl`
124
+ (or `SL_STREAMS_DUMPS_URL`) — no API key needed for dumps. The SDK does **not**
125
+ decode parquet; `download` hands you sha256-verified bytes to process with your
126
+ own tooling.
127
+
128
+ ```typescript
129
+ const streams = createStreamsClient({
130
+ apiKey: process.env.SL_API_KEY!,
131
+ dumpsBaseUrl: process.env.SL_STREAMS_DUMPS_URL!,
132
+ });
133
+
134
+ const manifest = await streams.dumps.list(); // parse the manifest
135
+ for (const file of manifest.files) {
136
+ const bytes = await streams.dumps.download(file); // fetch + verify sha256
137
+ await myParquetReader(bytes);
138
+ }
139
+ ```
140
+
141
+ Backfill then tail (`events.replay`).
142
+
143
+ Backfills from bulk dumps, then tails live from the manifest's
144
+ `latest_finalized_cursor` — no gap or dupe at the seam. `onDumpFile` hands you
145
+ each finalized file; `onBatch` receives live events after the seam.
146
+
147
+ ```typescript
148
+ await streams.events.replay({
149
+ from: lastCheckpoint,
150
+ async onDumpFile(file) {
151
+ const bytes = await streams.dumps.download(file);
152
+ await ingestParquet(bytes); // your tooling
153
+ },
154
+ async onBatch(events, envelope) {
155
+ for (const event of events) await handle(event);
156
+ return envelope.next_cursor;
157
+ },
158
+ });
159
+ ```
160
+
105
161
  Decoder helper.
106
162
 
107
163
  ```typescript
package/dist/index.d.ts CHANGED
@@ -346,8 +346,77 @@ declare class Index extends BaseClient {
346
346
  }
347
347
  declare const STREAMS_EVENT_TYPES: readonly ["stx_transfer", "stx_mint", "stx_burn", "stx_lock", "ft_transfer", "ft_mint", "ft_burn", "nft_transfer", "nft_mint", "nft_burn", "print"];
348
348
  type StreamsEventType = (typeof STREAMS_EVENT_TYPES)[number];
349
- type StreamsEventPayload = Record<string, unknown>;
350
- type StreamsEvent = {
349
+ /** A Clarity value as Streams serves it: the canonical hex string, a typed
350
+ * object carrying that hex (`{ hex }`), or a decoded Clarity-JSON object.
351
+ * Decode helpers (`decodeNftTransfer`, etc.) resolve it to a concrete value. */
352
+ type StreamsClarityValue = string | {
353
+ hex: string
354
+ } | Record<string, unknown>;
355
+ type StxTransferPayload = {
356
+ sender: string
357
+ recipient: string
358
+ amount: string
359
+ memo?: string
360
+ };
361
+ type StxMintPayload = {
362
+ recipient: string
363
+ amount: string
364
+ };
365
+ type StxBurnPayload = {
366
+ sender: string
367
+ amount: string
368
+ };
369
+ type StxLockPayload = {
370
+ locked_address: string
371
+ locked_amount: string
372
+ unlock_height: string
373
+ };
374
+ type FtTransferPayload = {
375
+ asset_identifier: string
376
+ sender: string
377
+ recipient: string
378
+ amount: string
379
+ };
380
+ type FtMintPayload = {
381
+ asset_identifier: string
382
+ recipient: string
383
+ amount: string
384
+ };
385
+ type FtBurnPayload = {
386
+ asset_identifier: string
387
+ sender: string
388
+ amount: string
389
+ };
390
+ type NftTransferPayload = {
391
+ asset_identifier: string
392
+ sender: string
393
+ recipient: string
394
+ value: StreamsClarityValue
395
+ /** Canonical serialized hex of `value`, when the stream carries it. */
396
+ raw_value?: string
397
+ };
398
+ type NftMintPayload = {
399
+ asset_identifier: string
400
+ recipient: string
401
+ value: StreamsClarityValue
402
+ raw_value?: string
403
+ };
404
+ type NftBurnPayload = {
405
+ asset_identifier: string
406
+ sender: string
407
+ value: StreamsClarityValue
408
+ raw_value?: string
409
+ };
410
+ type PrintPayload = {
411
+ contract_id?: string | null
412
+ topic?: string
413
+ value?: unknown
414
+ raw_value?: string
415
+ };
416
+ /** Union of every Streams payload shape, discriminated by `event_type` on the
417
+ * parent `StreamsEvent`. */
418
+ type StreamsEventPayload = StxTransferPayload | StxMintPayload | StxBurnPayload | StxLockPayload | FtTransferPayload | FtMintPayload | FtBurnPayload | NftTransferPayload | NftMintPayload | NftBurnPayload | PrintPayload;
419
+ type StreamsEventBase = {
351
420
  cursor: string
352
421
  block_height: number
353
422
  block_hash: string
@@ -355,9 +424,7 @@ type StreamsEvent = {
355
424
  tx_id: string
356
425
  tx_index: number
357
426
  event_index: number
358
- event_type: StreamsEventType
359
427
  contract_id: string | null
360
- payload: StreamsEventPayload
361
428
  ts: string
362
429
  /**
363
430
  * True when this event's block is past the finality boundary (immutable).
@@ -365,6 +432,16 @@ type StreamsEvent = {
365
432
  */
366
433
  finalized?: boolean
367
434
  };
435
+ type StreamsEventOf<
436
+ T extends StreamsEventType,
437
+ P
438
+ > = StreamsEventBase & {
439
+ event_type: T
440
+ payload: P
441
+ };
442
+ /** A raw Streams event. Discriminated on `event_type`, so `event.payload`
443
+ * narrows to the matching payload shape once the type is checked. */
444
+ type StreamsEvent = StreamsEventOf<"stx_transfer", StxTransferPayload> | StreamsEventOf<"stx_mint", StxMintPayload> | StreamsEventOf<"stx_burn", StxBurnPayload> | StreamsEventOf<"stx_lock", StxLockPayload> | StreamsEventOf<"ft_transfer", FtTransferPayload> | StreamsEventOf<"ft_mint", FtMintPayload> | StreamsEventOf<"ft_burn", FtBurnPayload> | StreamsEventOf<"nft_transfer", NftTransferPayload> | StreamsEventOf<"nft_mint", NftMintPayload> | StreamsEventOf<"nft_burn", NftBurnPayload> | StreamsEventOf<"print", PrintPayload>;
368
445
  type StreamsTip = {
369
446
  block_height: number
370
447
  block_hash: string
@@ -407,23 +484,28 @@ type StreamsReorgsListEnvelope = {
407
484
  reorgs: StreamsReorg[]
408
485
  next_since: string | null
409
486
  };
487
+ /** A filter that matches a single value or any value in a list. */
488
+ type StreamsFilterValue = string | readonly string[];
410
489
  type StreamsEventsListParams = {
411
490
  cursor?: string | null
412
491
  fromHeight?: number
413
492
  toHeight?: number
414
493
  types?: readonly StreamsEventType[]
415
- contractId?: string
416
- sender?: string
417
- recipient?: string
494
+ /** Event types to exclude (applied after `types`). */
495
+ notTypes?: readonly StreamsEventType[]
496
+ contractId?: StreamsFilterValue
497
+ sender?: StreamsFilterValue
498
+ recipient?: StreamsFilterValue
418
499
  assetIdentifier?: string
419
500
  limit?: number
420
501
  };
421
502
  type StreamsEventsStreamParams = {
422
503
  fromCursor?: string | null
423
504
  types?: readonly StreamsEventType[]
424
- contractId?: string
425
- sender?: string
426
- recipient?: string
505
+ notTypes?: readonly StreamsEventType[]
506
+ contractId?: StreamsFilterValue
507
+ sender?: StreamsFilterValue
508
+ recipient?: StreamsFilterValue
427
509
  assetIdentifier?: string
428
510
  batchSize?: number
429
511
  emptyBackoffMs?: number
@@ -435,9 +517,10 @@ type StreamsEventsConsumeParams = {
435
517
  fromCursor?: string | null
436
518
  mode?: "tail" | "bounded"
437
519
  types?: readonly StreamsEventType[]
438
- contractId?: string
439
- sender?: string
440
- recipient?: string
520
+ notTypes?: readonly StreamsEventType[]
521
+ contractId?: StreamsFilterValue
522
+ sender?: StreamsFilterValue
523
+ recipient?: StreamsFilterValue
441
524
  assetIdentifier?: string
442
525
  batchSize?: number
443
526
  onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope) => Promise<string | null | undefined> | string | null | undefined
@@ -647,16 +730,9 @@ declare class StreamsServerError extends Error {
647
730
  declare class StreamsSignatureError extends Error {
648
731
  constructor(message?: string);
649
732
  }
650
- type FtTransferPayload = {
651
- asset_identifier: string
652
- sender: string
653
- recipient: string
654
- amount: string
655
- };
656
- type FtTransferEvent = StreamsEvent & {
733
+ type FtTransferEvent = Extract<StreamsEvent, {
657
734
  event_type: "ft_transfer"
658
- payload: FtTransferPayload
659
- };
735
+ }>;
660
736
  type DecodedFtTransferPayload = {
661
737
  asset_identifier: string
662
738
  contract_id: string
@@ -677,18 +753,9 @@ type DecodedFtTransfer = {
677
753
  };
678
754
  declare function isFtTransfer(event: StreamsEvent): event is FtTransferEvent;
679
755
  declare function decodeFtTransfer(event: StreamsEvent): DecodedFtTransfer;
680
- type NftTransferPayload = {
681
- asset_identifier: string
682
- sender: string
683
- recipient: string
684
- value: string | {
685
- hex: string
686
- }
687
- };
688
- type NftTransferEvent = StreamsEvent & {
756
+ type NftTransferEvent = Extract<StreamsEvent, {
689
757
  event_type: "nft_transfer"
690
- payload: NftTransferPayload
691
- };
758
+ }>;
692
759
  type DecodedNftTransferPayload = {
693
760
  asset_identifier: string
694
761
  contract_id: string
package/dist/index.js CHANGED
@@ -532,6 +532,7 @@ async function consumeStreamsEvents(opts) {
532
532
  cursor,
533
533
  limit: opts.batchSize,
534
534
  types: opts.types,
535
+ notTypes: opts.notTypes,
535
536
  contractId: opts.contractId,
536
537
  sender: opts.sender,
537
538
  recipient: opts.recipient,
@@ -570,6 +571,7 @@ async function* streamStreamsEvents(opts) {
570
571
  cursor,
571
572
  limit: opts.batchSize,
572
573
  types: opts.types,
574
+ notTypes: opts.notTypes,
573
575
  contractId: opts.contractId,
574
576
  sender: opts.sender,
575
577
  recipient: opts.recipient,
@@ -705,6 +707,13 @@ function appendSearchParam2(params, name, value) {
705
707
  return;
706
708
  params.set(name, String(value));
707
709
  }
710
+ function appendListParam(params, name, value) {
711
+ if (value === undefined || value === null)
712
+ return;
713
+ const joined = Array.isArray(value) ? value.join(",") : value;
714
+ if (joined.length > 0)
715
+ params.set(name, joined);
716
+ }
708
717
  async function responseBody(response) {
709
718
  const text = await response.text();
710
719
  if (text.length === 0)
@@ -748,13 +757,16 @@ function createStreamsClient(options) {
748
757
  baseUrl: options.dumpsBaseUrl,
749
758
  fetchImpl
750
759
  });
751
- let publicKeyPromise = null;
752
- function getPublicKey() {
753
- if (publicKeyPromise)
754
- return publicKeyPromise;
755
- publicKeyPromise = (async () => {
760
+ let keyPromise = null;
761
+ function loadKey() {
762
+ if (keyPromise)
763
+ return keyPromise;
764
+ keyPromise = (async () => {
756
765
  if (typeof verify === "object") {
757
- return ed25519.loadEd25519PublicKey(verify.publicKey);
766
+ return {
767
+ keyId: ed25519.ed25519KeyId(verify.publicKey),
768
+ publicKey: ed25519.loadEd25519PublicKey(verify.publicKey)
769
+ };
758
770
  }
759
771
  const res = await fetchImpl(`${baseUrl}/public/streams/signing-key`);
760
772
  if (!res.ok) {
@@ -764,9 +776,12 @@ function createStreamsClient(options) {
764
776
  if (!body.public_key_pem) {
765
777
  throw new StreamsSignatureError("Signing key response missing key.");
766
778
  }
767
- return ed25519.loadEd25519PublicKey(body.public_key_pem);
779
+ return {
780
+ keyId: body.key_id ?? ed25519.ed25519KeyId(body.public_key_pem),
781
+ publicKey: ed25519.loadEd25519PublicKey(body.public_key_pem)
782
+ };
768
783
  })();
769
- return publicKeyPromise;
784
+ return keyPromise;
770
785
  }
771
786
  async function request(path) {
772
787
  const response = await fetchImpl(`${baseUrl}${path}`, {
@@ -780,8 +795,19 @@ function createStreamsClient(options) {
780
795
  if (!signature) {
781
796
  throw new StreamsSignatureError("Response is missing X-Signature.");
782
797
  }
783
- const publicKey = await getPublicKey();
784
- if (!ed25519.verifyEd25519(text, signature, publicKey)) {
798
+ const responseKeyId = response.headers.get("X-Signature-KeyId");
799
+ let key = await loadKey();
800
+ if (responseKeyId && responseKeyId !== key.keyId) {
801
+ if (typeof verify === "object") {
802
+ throw new StreamsSignatureError(`Response signed with key '${responseKeyId}', expected pinned key '${key.keyId}'.`);
803
+ }
804
+ keyPromise = null;
805
+ key = await loadKey();
806
+ if (responseKeyId !== key.keyId) {
807
+ throw new StreamsSignatureError(`Response signed with key '${responseKeyId}' not served by the signing-key endpoint.`);
808
+ }
809
+ }
810
+ if (!ed25519.verifyEd25519(text, signature, key.publicKey)) {
785
811
  throw new StreamsSignatureError;
786
812
  }
787
813
  }
@@ -791,6 +817,7 @@ function createStreamsClient(options) {
791
817
  cursor,
792
818
  limit,
793
819
  types,
820
+ notTypes,
794
821
  contractId,
795
822
  sender,
796
823
  recipient,
@@ -800,6 +827,7 @@ function createStreamsClient(options) {
800
827
  cursor,
801
828
  limit,
802
829
  types,
830
+ notTypes,
803
831
  contractId,
804
832
  sender,
805
833
  recipient,
@@ -812,13 +840,16 @@ function createStreamsClient(options) {
812
840
  appendSearchParam2(searchParams, "from_height", params.fromHeight);
813
841
  appendSearchParam2(searchParams, "to_height", params.toHeight);
814
842
  appendSearchParam2(searchParams, "limit", params.limit);
815
- appendSearchParam2(searchParams, "contract_id", params.contractId);
816
- appendSearchParam2(searchParams, "sender", params.sender);
817
- appendSearchParam2(searchParams, "recipient", params.recipient);
843
+ appendListParam(searchParams, "contract_id", params.contractId);
844
+ appendListParam(searchParams, "sender", params.sender);
845
+ appendListParam(searchParams, "recipient", params.recipient);
818
846
  appendSearchParam2(searchParams, "asset_identifier", params.assetIdentifier);
819
847
  if (params.types?.length) {
820
848
  searchParams.set("types", params.types.join(","));
821
849
  }
850
+ if (params.notTypes?.length) {
851
+ searchParams.set("not_types", params.notTypes.join(","));
852
+ }
822
853
  const query = searchParams.toString();
823
854
  return request(`/v1/streams/events${query ? `?${query}` : ""}`);
824
855
  }
@@ -833,6 +864,7 @@ function createStreamsClient(options) {
833
864
  fromCursor: params.fromCursor,
834
865
  mode: params.mode,
835
866
  types: params.types,
867
+ notTypes: params.notTypes,
836
868
  contractId: params.contractId,
837
869
  sender: params.sender,
838
870
  recipient: params.recipient,
@@ -850,6 +882,7 @@ function createStreamsClient(options) {
850
882
  return streamStreamsEvents({
851
883
  fromCursor: params.fromCursor,
852
884
  types: params.types,
885
+ notTypes: params.notTypes,
853
886
  contractId: params.contractId,
854
887
  sender: params.sender,
855
888
  recipient: params.recipient,
@@ -1558,5 +1591,5 @@ export {
1558
1591
  ApiError
1559
1592
  };
1560
1593
 
1561
- //# debugId=54289A1292072EB864756E2164756E21
1594
+ //# debugId=4712778CA4B6FFB664756E2164756E21
1562
1595
  //# sourceMappingURL=index.js.map