@secondlayer/sdk 3.3.2 → 3.5.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,158 @@ const sl = new SecondLayer({
19
19
  });
20
20
  ```
21
21
 
22
- ## Subgraphs
22
+ ## Mental model
23
23
 
24
- Deploy and query subgraphs (custom indexers).
24
+ - `sl.streams` reads raw ordered L1 events from Stacks Streams.
25
+ - `sl.index` reads decoded L2 FT/NFT transfer events from Stacks Index.
26
+ - `sl.subgraphs` reads app-specific L3 tables from Stacks Subgraphs.
27
+
28
+ ## Stacks Streams
29
+
30
+ Typed L1 HTTP client.
31
+
32
+ `sk-sl_streams_status_public` is a public, non-secret Free-tier key used by the
33
+ Second Layer status page. Production apps should use their own Streams API key.
34
+
35
+ ```typescript
36
+ const tip = await sl.streams.tip();
37
+ const page = await sl.streams.events.list({
38
+ types: ["ft_transfer"],
39
+ contractId: "SP...sbtc-token",
40
+ limit: 10,
41
+ });
42
+
43
+ console.log({ tip, firstCursor: page.events[0]?.cursor });
44
+ ```
45
+
46
+ `createStreamsClient` remains available for focused Streams-only consumers:
47
+
48
+ ```typescript
49
+ import { createStreamsClient } from "@secondlayer/sdk";
50
+
51
+ const streams = createStreamsClient({
52
+ apiKey: process.env.SECONDLAYER_API_KEY!,
53
+ });
54
+ ```
55
+
56
+ Convenience reads:
57
+
58
+ ```typescript
59
+ await sl.streams.canonical(182431);
60
+ await sl.streams.events.byTxId("0x...");
61
+ await sl.streams.blocks.events(182431);
62
+ await sl.streams.blocks.events("0xindex-block-hash");
63
+ await sl.streams.reorgs.list({ since: "2026-05-03T00:00:00.000Z" });
64
+ ```
65
+
66
+ Checkpointed consumer.
67
+
68
+ Use `client.events.consume` for indexers and ETL jobs. Write your database rows
69
+ inside `onBatch`, then return the cursor you committed. It exits when
70
+ `maxPages`, `maxEmptyPolls`, or `signal` stops it.
71
+
72
+ ```typescript
73
+ import { createStreamsClient } from "@secondlayer/sdk";
74
+
75
+ const client = createStreamsClient({
76
+ apiKey: process.env.SECONDLAYER_API_KEY!,
77
+ });
78
+
79
+ await client.events.consume({
80
+ types: ["ft_transfer"],
81
+ batchSize: 100,
82
+ maxPages: 1,
83
+ onBatch: async (events, envelope) => {
84
+ for (const event of events) {
85
+ console.log(event.cursor, event.tx_id);
86
+ }
87
+ return envelope.next_cursor;
88
+ },
89
+ });
90
+ ```
91
+
92
+ Live stream.
93
+
94
+ Use `client.events.stream` for live processors and watch-style apps. It follows
95
+ the tip indefinitely. Stop it with an `AbortSignal`.
96
+
97
+ ```typescript
98
+ import { createStreamsClient } from "@secondlayer/sdk";
99
+
100
+ const client = createStreamsClient({
101
+ apiKey: process.env.SECONDLAYER_API_KEY!,
102
+ });
103
+
104
+ const abort = new AbortController();
105
+ process.once("SIGINT", () => abort.abort());
106
+
107
+ for await (const event of client.events.stream({
108
+ types: ["ft_transfer"],
109
+ batchSize: 100,
110
+ signal: abort.signal,
111
+ })) {
112
+ console.log(event.cursor, event.tx_id);
113
+ }
114
+ ```
115
+
116
+ Decoder helper.
117
+
118
+ ```typescript
119
+ import {
120
+ createStreamsClient,
121
+ decodeFtTransfer,
122
+ isFtTransfer,
123
+ } from "@secondlayer/sdk";
124
+
125
+ const client = createStreamsClient({
126
+ apiKey: process.env.SECONDLAYER_API_KEY!,
127
+ });
128
+
129
+ for await (const event of client.events.stream({ types: ["ft_transfer"] })) {
130
+ if (!isFtTransfer(event)) continue;
131
+ const transfer = decodeFtTransfer(event);
132
+ console.log(transfer.decoded_payload);
133
+ break;
134
+ }
135
+ ```
136
+
137
+ Helper convention: each event helper is a pure function with no shared state.
138
+ Use `is<EventName>(event)` as the type guard and `decode<EventName>(event)` as
139
+ the decoder. Decoders throw when the event type or payload is malformed. Add new
140
+ helpers beside `src/streams/ft-transfer.ts` and export them through
141
+ `src/streams/index.ts`.
142
+
143
+ ## Stacks Index
144
+
145
+ Decoded L2 transfer events.
146
+
147
+ ```typescript
148
+ const ftPage = await sl.index.ftTransfers.list({
149
+ contractId: "SP...sbtc-token",
150
+ sender: "SP...",
151
+ limit: 100,
152
+ });
153
+
154
+ const nftPage = await sl.index.nftTransfers.list({
155
+ assetIdentifier: "SP...collection::token",
156
+ recipient: "SP...",
157
+ });
158
+ ```
159
+
160
+ Backfill with SDK walkers:
161
+
162
+ ```typescript
163
+ for await (const transfer of sl.index.ftTransfers.walk({
164
+ fromHeight: 0,
165
+ batchSize: 500,
166
+ })) {
167
+ console.log(transfer.cursor, transfer.amount);
168
+ }
169
+ ```
170
+
171
+ ## Stacks Subgraphs
172
+
173
+ Deploy and query app-specific L3 tables.
25
174
 
26
175
  ```typescript
27
176
  // List
@@ -37,6 +186,15 @@ const rows = await sl.subgraphs.queryTable("my-subgraph", "transfers", {
37
186
  limit: 50,
38
187
  });
39
188
 
189
+ const { count } = await sl.subgraphs.queryTableCount(
190
+ "my-subgraph",
191
+ "transfers",
192
+ );
193
+
194
+ const spec = await sl.subgraphs.openapi("my-subgraph");
195
+ const source = await sl.subgraphs.getSource("my-subgraph");
196
+ const gaps = await sl.subgraphs.gaps("my-subgraph");
197
+
40
198
  // Deploy
41
199
  const result = await sl.subgraphs.deploy({ name, sources, schema, handlerCode });
42
200
  ```
package/dist/index.d.ts CHANGED
@@ -2,11 +2,14 @@ import { ReindexResponse, SubgraphDetail, SubgraphGapsResponse, SubgraphQueryPar
2
2
  import { DeploySubgraphRequest, DeploySubgraphResponse } from "@secondlayer/shared/schemas/subgraphs";
3
3
  import { SubgraphAgentSchema, SubgraphSpecOptions } from "@secondlayer/shared/subgraphs/spec";
4
4
  import { InferSubgraphClient } from "@secondlayer/subgraphs";
5
+ type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
6
  interface SecondLayerOptions {
6
7
  /** Base URL of the Secondlayer API (trailing slashes are stripped). */
7
8
  baseUrl: string;
8
9
  /** Bearer token for authenticated requests. */
9
10
  apiKey?: string;
11
+ /** Fetch implementation. Tests and edge runtimes can provide their own. */
12
+ fetchImpl?: FetchLike;
10
13
  /** Deploy origin label sent as `x-sl-origin` (telemetry). Defaults to `cli`. */
11
14
  origin?: "cli" | "mcp" | "session";
12
15
  }
@@ -102,6 +105,211 @@ declare class Subgraphs extends BaseClient {
102
105
  private createTableClient;
103
106
  }
104
107
  import { InferSubgraphClient as InferSubgraphClient2 } from "@secondlayer/subgraphs";
108
+ type IndexTip = {
109
+ block_height: number
110
+ lag_seconds: number
111
+ };
112
+ type FtTransfer = {
113
+ cursor: string
114
+ block_height: number
115
+ tx_id: string
116
+ tx_index: number
117
+ event_index: number
118
+ event_type: "ft_transfer"
119
+ contract_id: string
120
+ asset_identifier: string
121
+ sender: string
122
+ recipient: string
123
+ amount: string
124
+ };
125
+ type FtTransfersEnvelope = {
126
+ events: FtTransfer[]
127
+ next_cursor: string | null
128
+ tip: IndexTip
129
+ reorgs: never[]
130
+ };
131
+ type FtTransfersListParams = {
132
+ cursor?: string | null
133
+ fromCursor?: string | null
134
+ limit?: number
135
+ contractId?: string
136
+ sender?: string
137
+ recipient?: string
138
+ fromHeight?: number
139
+ toHeight?: number
140
+ };
141
+ type FtTransfersWalkParams = Omit<FtTransfersListParams, "limit"> & {
142
+ batchSize?: number
143
+ signal?: AbortSignal
144
+ };
145
+ type NftTransfer = {
146
+ cursor: string
147
+ block_height: number
148
+ tx_id: string
149
+ tx_index: number
150
+ event_index: number
151
+ event_type: "nft_transfer"
152
+ contract_id: string
153
+ asset_identifier: string
154
+ sender: string
155
+ recipient: string
156
+ value: string
157
+ };
158
+ type NftTransfersEnvelope = {
159
+ events: NftTransfer[]
160
+ next_cursor: string | null
161
+ tip: IndexTip
162
+ reorgs: never[]
163
+ };
164
+ type NftTransfersListParams = {
165
+ cursor?: string | null
166
+ fromCursor?: string | null
167
+ limit?: number
168
+ contractId?: string
169
+ assetIdentifier?: string
170
+ sender?: string
171
+ recipient?: string
172
+ fromHeight?: number
173
+ toHeight?: number
174
+ };
175
+ type NftTransfersWalkParams = Omit<NftTransfersListParams, "limit"> & {
176
+ batchSize?: number
177
+ signal?: AbortSignal
178
+ };
179
+ declare class Index extends BaseClient {
180
+ constructor(options?: Partial<SecondLayerOptions>);
181
+ readonly ftTransfers: {
182
+ list: (params?: FtTransfersListParams) => Promise<FtTransfersEnvelope>
183
+ walk: (params?: FtTransfersWalkParams) => AsyncIterable<FtTransfer>
184
+ };
185
+ readonly nftTransfers: {
186
+ list: (params?: NftTransfersListParams) => Promise<NftTransfersEnvelope>
187
+ walk: (params?: NftTransfersWalkParams) => AsyncIterable<NftTransfer>
188
+ };
189
+ private listFtTransfers;
190
+ private listNftTransfers;
191
+ private walkFtTransfers;
192
+ private walkNftTransfers;
193
+ }
194
+ 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"];
195
+ type StreamsEventType = (typeof STREAMS_EVENT_TYPES)[number];
196
+ type StreamsEventPayload = Record<string, unknown>;
197
+ type StreamsEvent = {
198
+ cursor: string
199
+ block_height: number
200
+ index_block_hash: string
201
+ burn_block_height: number
202
+ tx_id: string
203
+ tx_index: number
204
+ event_index: number
205
+ event_type: StreamsEventType
206
+ contract_id: string | null
207
+ payload: StreamsEventPayload
208
+ ts: string
209
+ };
210
+ type StreamsTip = {
211
+ block_height: number
212
+ index_block_hash: string
213
+ burn_block_height: number
214
+ lag_seconds: number
215
+ };
216
+ type StreamsCanonicalBlock = {
217
+ block_height: number
218
+ index_block_hash: string
219
+ burn_block_height: number
220
+ burn_block_hash: string | null
221
+ is_canonical: true
222
+ };
223
+ type StreamsReorg = {
224
+ detected_at: string
225
+ fork_point_height: number
226
+ orphaned_range: {
227
+ from: string
228
+ to: string
229
+ }
230
+ new_canonical_tip: string
231
+ };
232
+ type StreamsEventsEnvelope = {
233
+ events: StreamsEvent[]
234
+ next_cursor: string | null
235
+ tip: StreamsTip
236
+ reorgs: StreamsReorg[]
237
+ };
238
+ type StreamsEventsListEnvelope = Omit<StreamsEventsEnvelope, "next_cursor">;
239
+ type StreamsReorgsListParams = {
240
+ since: string
241
+ limit?: number
242
+ };
243
+ type StreamsReorgsListEnvelope = {
244
+ reorgs: StreamsReorg[]
245
+ next_since: string | null
246
+ };
247
+ type StreamsEventsListParams = {
248
+ cursor?: string | null
249
+ fromHeight?: number
250
+ toHeight?: number
251
+ types?: readonly StreamsEventType[]
252
+ contractId?: string
253
+ limit?: number
254
+ };
255
+ type StreamsEventsStreamParams = {
256
+ fromCursor?: string | null
257
+ types?: readonly StreamsEventType[]
258
+ batchSize?: number
259
+ emptyBackoffMs?: number
260
+ maxPages?: number
261
+ maxEmptyPolls?: number
262
+ signal?: AbortSignal
263
+ };
264
+ type StreamsEventsConsumeParams = {
265
+ fromCursor?: string | null
266
+ mode?: "tail" | "bounded"
267
+ types?: readonly StreamsEventType[]
268
+ batchSize?: number
269
+ onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope) => Promise<string | null | undefined> | string | null | undefined
270
+ emptyBackoffMs?: number
271
+ maxPages?: number
272
+ maxEmptyPolls?: number
273
+ signal?: AbortSignal
274
+ };
275
+ type StreamsEventsConsumeResult = {
276
+ cursor: string | null
277
+ pages: number
278
+ emptyPolls: number
279
+ };
280
+ type FetchLike2 = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
281
+ type StreamsClient = {
282
+ events: {
283
+ list(params?: StreamsEventsListParams): Promise<StreamsEventsEnvelope>
284
+ byTxId(txId: string): Promise<StreamsEventsListEnvelope>
285
+ /**
286
+ * Pull pages from Streams and call `onBatch` after each page.
287
+ *
288
+ * Use `consume` for indexers and ETL jobs that own checkpointing. Return
289
+ * the checkpoint cursor from `onBatch`. Default `mode: "tail"` keeps
290
+ * polling when caught up; `mode: "bounded"` exits on the first empty page.
291
+ * The consumer also exits when `maxPages`, `maxEmptyPolls`, or `signal`
292
+ * stops it.
293
+ */
294
+ consume(params: StreamsEventsConsumeParams): Promise<StreamsEventsConsumeResult>
295
+ /**
296
+ * Follow Streams as an async iterator.
297
+ *
298
+ * Use `stream` for live processors and watch-style apps. It tails
299
+ * indefinitely by default and stops when its `AbortSignal`, `maxPages`, or
300
+ * `maxEmptyPolls` stops it.
301
+ */
302
+ stream(params?: StreamsEventsStreamParams): AsyncIterable<StreamsEvent>
303
+ }
304
+ blocks: {
305
+ events(heightOrHash: number | string): Promise<StreamsEventsListEnvelope>
306
+ }
307
+ reorgs: {
308
+ list(params: StreamsReorgsListParams): Promise<StreamsReorgsListEnvelope>
309
+ }
310
+ canonical(height: number): Promise<StreamsCanonicalBlock>
311
+ tip(): Promise<StreamsTip>
312
+ };
105
313
  import { CreateSubscriptionRequest, CreateSubscriptionResponse, DeadRow, DeliveryRow, ReplayResult, RotateSecretResponse, SubscriptionDetail, SubscriptionSummary, UpdateSubscriptionRequest } from "@secondlayer/shared/schemas/subscriptions";
106
314
  import { CreateSubscriptionRequest as CreateSubscriptionRequest2, CreateSubscriptionResponse as CreateSubscriptionResponse2, DeadRow as DeadRow2, DeliveryRow as DeliveryRow2, ReplayResult as ReplayResult2, RotateSecretResponse as RotateSecretResponse2, SubscriptionDetail as SubscriptionDetail2, SubscriptionFormat, SubscriptionRuntime, SubscriptionStatus, SubscriptionSummary as SubscriptionSummary2, UpdateSubscriptionRequest as UpdateSubscriptionRequest2 } from "@secondlayer/shared/schemas/subscriptions";
107
315
  declare class Subscriptions extends BaseClient {
@@ -132,6 +340,8 @@ declare class Subscriptions extends BaseClient {
132
340
  }>;
133
341
  }
134
342
  declare class SecondLayer extends BaseClient {
343
+ readonly streams: StreamsClient;
344
+ readonly index: Index;
135
345
  readonly subgraphs: Subgraphs;
136
346
  readonly subscriptions: Subscriptions;
137
347
  constructor(options?: Partial<SecondLayerOptions>);
@@ -154,6 +364,94 @@ declare function getSubgraph<T extends {
154
364
  name: string
155
365
  schema: Record<string, unknown>
156
366
  }>(def: T, options?: Partial<SecondLayerOptions> | SecondLayer | Subgraphs): InferSubgraphClient2<T>;
367
+ type CreateStreamsClientOptions = {
368
+ apiKey: string
369
+ baseUrl?: string
370
+ fetchImpl?: FetchLike2
371
+ };
372
+ declare function createStreamsClient(options: CreateStreamsClientOptions): StreamsClient;
373
+ declare class AuthError extends Error {
374
+ readonly status = 401;
375
+ constructor(message?: string);
376
+ }
377
+ declare class RateLimitError extends Error {
378
+ readonly retryAfter?: string;
379
+ readonly status = 429;
380
+ constructor(message?: string, retryAfter?: string);
381
+ }
382
+ declare class ValidationError extends Error {
383
+ readonly status: number;
384
+ readonly body?: unknown;
385
+ constructor(message: string, status: number, body?: unknown);
386
+ }
387
+ declare class StreamsServerError extends Error {
388
+ readonly status: number;
389
+ readonly body?: unknown;
390
+ constructor(message: string, status: number, body?: unknown);
391
+ }
392
+ type FtTransferPayload = {
393
+ asset_identifier: string
394
+ sender: string
395
+ recipient: string
396
+ amount: string
397
+ };
398
+ type FtTransferEvent = StreamsEvent & {
399
+ event_type: "ft_transfer"
400
+ payload: FtTransferPayload
401
+ };
402
+ type DecodedFtTransferPayload = {
403
+ asset_identifier: string
404
+ contract_id: string
405
+ token_name: string | null
406
+ sender: string
407
+ recipient: string
408
+ amount: string
409
+ };
410
+ type DecodedFtTransfer = {
411
+ cursor: string
412
+ block_height: number
413
+ tx_id: string
414
+ tx_index: number
415
+ event_index: number
416
+ event_type: "ft_transfer"
417
+ decoded_payload: DecodedFtTransferPayload
418
+ source_cursor: string
419
+ };
420
+ declare function isFtTransfer(event: StreamsEvent): event is FtTransferEvent;
421
+ declare function decodeFtTransfer(event: StreamsEvent): DecodedFtTransfer;
422
+ type NftTransferPayload = {
423
+ asset_identifier: string
424
+ sender: string
425
+ recipient: string
426
+ value: string | {
427
+ hex: string
428
+ }
429
+ };
430
+ type NftTransferEvent = StreamsEvent & {
431
+ event_type: "nft_transfer"
432
+ payload: NftTransferPayload
433
+ };
434
+ type DecodedNftTransferPayload = {
435
+ asset_identifier: string
436
+ contract_id: string
437
+ token_name: string | null
438
+ sender: string
439
+ recipient: string
440
+ value: string
441
+ };
442
+ type DecodedNftTransfer = {
443
+ cursor: string
444
+ block_height: number
445
+ tx_id: string
446
+ tx_index: number
447
+ event_index: number
448
+ event_type: "nft_transfer"
449
+ decoded_payload: DecodedNftTransferPayload
450
+ source_cursor: string
451
+ };
452
+ declare function isNftTransfer(event: StreamsEvent): event is NftTransferEvent;
453
+ declare function decodeNftTransfer(event: StreamsEvent): DecodedNftTransfer;
454
+ type DecodedEventRow = DecodedFtTransfer | DecodedNftTransfer;
157
455
  import { SubgraphAgentSchema as SubgraphAgentSchema3, SubgraphSpecFormat as SubgraphSpecFormat2, SubgraphSpecOptions as SubgraphSpecOptions3 } from "@secondlayer/shared/subgraphs/spec";
158
456
  /**
159
457
  * Error thrown by {@link SecondLayer} when an API request fails.
@@ -214,4 +512,4 @@ declare class VersionConflictError extends ApiError {
214
512
  * ```
215
513
  */
216
514
  declare function verifyWebhookSignature(rawBody: string, signatureHeader: string, secret: string, toleranceSeconds?: number): boolean;
217
- export { verifyWebhookSignature, getSubgraph, VersionConflictError, UpdateSubscriptionRequest2 as UpdateSubscriptionRequest, Subscriptions, SubscriptionSummary2 as SubscriptionSummary, SubscriptionStatus, SubscriptionRuntime, SubscriptionFormat, SubscriptionDetail2 as SubscriptionDetail, Subgraphs, SubgraphSpecOptions3 as SubgraphSpecOptions, SubgraphSpecFormat2 as SubgraphSpecFormat, SubgraphAgentSchema3 as SubgraphAgentSchema, SecondLayerOptions, SecondLayer, RotateSecretResponse2 as RotateSecretResponse, ReplayResult2 as ReplayResult, DeliveryRow2 as DeliveryRow, DeadRow2 as DeadRow, CreateSubscriptionResponse2 as CreateSubscriptionResponse, CreateSubscriptionRequest2 as CreateSubscriptionRequest, ApiError };
515
+ export { verifyWebhookSignature, isNftTransfer, isFtTransfer, getSubgraph, decodeNftTransfer, decodeFtTransfer, createStreamsClient, VersionConflictError, ValidationError, UpdateSubscriptionRequest2 as UpdateSubscriptionRequest, Subscriptions, SubscriptionSummary2 as SubscriptionSummary, SubscriptionStatus, SubscriptionRuntime, SubscriptionFormat, SubscriptionDetail2 as SubscriptionDetail, Subgraphs, SubgraphSpecOptions3 as SubgraphSpecOptions, SubgraphSpecFormat2 as SubgraphSpecFormat, SubgraphAgentSchema3 as SubgraphAgentSchema, StreamsTip, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorg, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsClient, StreamsCanonicalBlock, SecondLayerOptions, SecondLayer, RotateSecretResponse2 as RotateSecretResponse, ReplayResult2 as ReplayResult, RateLimitError, NftTransfersWalkParams, NftTransfersListParams, NftTransfersEnvelope, NftTransferPayload, NftTransferEvent, NftTransfer, IndexTip, Index, FtTransfersWalkParams, FtTransfersListParams, FtTransfersEnvelope, FtTransferPayload, FtTransferEvent, FtTransfer, FetchLike2 as FetchLike, DeliveryRow2 as DeliveryRow, DecodedNftTransferPayload, DecodedNftTransfer, DecodedFtTransferPayload, DecodedFtTransfer, DecodedEventRow, DeadRow2 as DeadRow, CreateSubscriptionResponse2 as CreateSubscriptionResponse, CreateSubscriptionRequest2 as CreateSubscriptionRequest, AuthError, ApiError };