@secondlayer/sdk 5.8.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 +62 -6
- package/dist/index.d.ts +199 -29
- package/dist/index.js +186 -7
- package/dist/index.js.map +10 -9
- package/dist/streams/index.d.ts +199 -29
- package/dist/streams/index.js +186 -7
- package/dist/streams/index.js.map +10 -9
- package/dist/subgraphs/index.d.ts +173 -8
- package/dist/subgraphs/index.js +185 -7
- package/dist/subgraphs/index.js.map +7 -6
- package/package.json +2 -2
package/dist/streams/index.d.ts
CHANGED
|
@@ -1,7 +1,76 @@
|
|
|
1
1
|
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"];
|
|
2
2
|
type StreamsEventType = (typeof STREAMS_EVENT_TYPES)[number];
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/** A Clarity value as Streams serves it: the canonical hex string, a typed
|
|
4
|
+
* object carrying that hex (`{ hex }`), or a decoded Clarity-JSON object.
|
|
5
|
+
* Decode helpers (`decodeNftTransfer`, etc.) resolve it to a concrete value. */
|
|
6
|
+
type StreamsClarityValue = string | {
|
|
7
|
+
hex: string
|
|
8
|
+
} | Record<string, unknown>;
|
|
9
|
+
type StxTransferPayload = {
|
|
10
|
+
sender: string
|
|
11
|
+
recipient: string
|
|
12
|
+
amount: string
|
|
13
|
+
memo?: string
|
|
14
|
+
};
|
|
15
|
+
type StxMintPayload = {
|
|
16
|
+
recipient: string
|
|
17
|
+
amount: string
|
|
18
|
+
};
|
|
19
|
+
type StxBurnPayload = {
|
|
20
|
+
sender: string
|
|
21
|
+
amount: string
|
|
22
|
+
};
|
|
23
|
+
type StxLockPayload = {
|
|
24
|
+
locked_address: string
|
|
25
|
+
locked_amount: string
|
|
26
|
+
unlock_height: string
|
|
27
|
+
};
|
|
28
|
+
type FtTransferPayload = {
|
|
29
|
+
asset_identifier: string
|
|
30
|
+
sender: string
|
|
31
|
+
recipient: string
|
|
32
|
+
amount: string
|
|
33
|
+
};
|
|
34
|
+
type FtMintPayload = {
|
|
35
|
+
asset_identifier: string
|
|
36
|
+
recipient: string
|
|
37
|
+
amount: string
|
|
38
|
+
};
|
|
39
|
+
type FtBurnPayload = {
|
|
40
|
+
asset_identifier: string
|
|
41
|
+
sender: string
|
|
42
|
+
amount: string
|
|
43
|
+
};
|
|
44
|
+
type NftTransferPayload = {
|
|
45
|
+
asset_identifier: string
|
|
46
|
+
sender: string
|
|
47
|
+
recipient: string
|
|
48
|
+
value: StreamsClarityValue
|
|
49
|
+
/** Canonical serialized hex of `value`, when the stream carries it. */
|
|
50
|
+
raw_value?: string
|
|
51
|
+
};
|
|
52
|
+
type NftMintPayload = {
|
|
53
|
+
asset_identifier: string
|
|
54
|
+
recipient: string
|
|
55
|
+
value: StreamsClarityValue
|
|
56
|
+
raw_value?: string
|
|
57
|
+
};
|
|
58
|
+
type NftBurnPayload = {
|
|
59
|
+
asset_identifier: string
|
|
60
|
+
sender: string
|
|
61
|
+
value: StreamsClarityValue
|
|
62
|
+
raw_value?: string
|
|
63
|
+
};
|
|
64
|
+
type PrintPayload = {
|
|
65
|
+
contract_id?: string | null
|
|
66
|
+
topic?: string
|
|
67
|
+
value?: unknown
|
|
68
|
+
raw_value?: string
|
|
69
|
+
};
|
|
70
|
+
/** Union of every Streams payload shape, discriminated by `event_type` on the
|
|
71
|
+
* parent `StreamsEvent`. */
|
|
72
|
+
type StreamsEventPayload = StxTransferPayload | StxMintPayload | StxBurnPayload | StxLockPayload | FtTransferPayload | FtMintPayload | FtBurnPayload | NftTransferPayload | NftMintPayload | NftBurnPayload | PrintPayload;
|
|
73
|
+
type StreamsEventBase = {
|
|
5
74
|
cursor: string
|
|
6
75
|
block_height: number
|
|
7
76
|
block_hash: string
|
|
@@ -9,15 +78,33 @@ type StreamsEvent = {
|
|
|
9
78
|
tx_id: string
|
|
10
79
|
tx_index: number
|
|
11
80
|
event_index: number
|
|
12
|
-
event_type: StreamsEventType
|
|
13
81
|
contract_id: string | null
|
|
14
|
-
payload: StreamsEventPayload
|
|
15
82
|
ts: string
|
|
16
|
-
|
|
83
|
+
/**
|
|
84
|
+
* True when this event's block is past the finality boundary (immutable).
|
|
85
|
+
* Optional for back-compat; the API always sets it on Streams responses.
|
|
86
|
+
*/
|
|
87
|
+
finalized?: boolean
|
|
88
|
+
};
|
|
89
|
+
type StreamsEventOf<
|
|
90
|
+
T extends StreamsEventType,
|
|
91
|
+
P
|
|
92
|
+
> = StreamsEventBase & {
|
|
93
|
+
event_type: T
|
|
94
|
+
payload: P
|
|
95
|
+
};
|
|
96
|
+
/** A raw Streams event. Discriminated on `event_type`, so `event.payload`
|
|
97
|
+
* narrows to the matching payload shape once the type is checked. */
|
|
98
|
+
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>;
|
|
17
99
|
type StreamsTip = {
|
|
18
100
|
block_height: number
|
|
19
101
|
block_hash: string
|
|
20
102
|
burn_block_height: number
|
|
103
|
+
/**
|
|
104
|
+
* Highest Stacks block past the burn-confirmation finality boundary.
|
|
105
|
+
* Optional for back-compat; the API always sets it.
|
|
106
|
+
*/
|
|
107
|
+
finalized_height?: number
|
|
21
108
|
lag_seconds: number
|
|
22
109
|
};
|
|
23
110
|
type StreamsCanonicalBlock = {
|
|
@@ -51,18 +138,29 @@ type StreamsReorgsListEnvelope = {
|
|
|
51
138
|
reorgs: StreamsReorg[]
|
|
52
139
|
next_since: string | null
|
|
53
140
|
};
|
|
141
|
+
/** A filter that matches a single value or any value in a list. */
|
|
142
|
+
type StreamsFilterValue = string | readonly string[];
|
|
54
143
|
type StreamsEventsListParams = {
|
|
55
144
|
cursor?: string | null
|
|
56
145
|
fromHeight?: number
|
|
57
146
|
toHeight?: number
|
|
58
147
|
types?: readonly StreamsEventType[]
|
|
59
|
-
|
|
148
|
+
/** Event types to exclude (applied after `types`). */
|
|
149
|
+
notTypes?: readonly StreamsEventType[]
|
|
150
|
+
contractId?: StreamsFilterValue
|
|
151
|
+
sender?: StreamsFilterValue
|
|
152
|
+
recipient?: StreamsFilterValue
|
|
153
|
+
assetIdentifier?: string
|
|
60
154
|
limit?: number
|
|
61
155
|
};
|
|
62
156
|
type StreamsEventsStreamParams = {
|
|
63
157
|
fromCursor?: string | null
|
|
64
158
|
types?: readonly StreamsEventType[]
|
|
65
|
-
|
|
159
|
+
notTypes?: readonly StreamsEventType[]
|
|
160
|
+
contractId?: StreamsFilterValue
|
|
161
|
+
sender?: StreamsFilterValue
|
|
162
|
+
recipient?: StreamsFilterValue
|
|
163
|
+
assetIdentifier?: string
|
|
66
164
|
batchSize?: number
|
|
67
165
|
emptyBackoffMs?: number
|
|
68
166
|
maxPages?: number
|
|
@@ -73,7 +171,11 @@ type StreamsEventsConsumeParams = {
|
|
|
73
171
|
fromCursor?: string | null
|
|
74
172
|
mode?: "tail" | "bounded"
|
|
75
173
|
types?: readonly StreamsEventType[]
|
|
76
|
-
|
|
174
|
+
notTypes?: readonly StreamsEventType[]
|
|
175
|
+
contractId?: StreamsFilterValue
|
|
176
|
+
sender?: StreamsFilterValue
|
|
177
|
+
recipient?: StreamsFilterValue
|
|
178
|
+
assetIdentifier?: string
|
|
77
179
|
batchSize?: number
|
|
78
180
|
onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope) => Promise<string | null | undefined> | string | null | undefined
|
|
79
181
|
emptyBackoffMs?: number
|
|
@@ -86,7 +188,63 @@ type StreamsEventsConsumeResult = {
|
|
|
86
188
|
pages: number
|
|
87
189
|
emptyPolls: number
|
|
88
190
|
};
|
|
191
|
+
type StreamsEventsReplayParams = {
|
|
192
|
+
/** Start point: `"genesis"` (default) or a `<block>:<index>` cursor. */
|
|
193
|
+
from?: "genesis" | string
|
|
194
|
+
/**
|
|
195
|
+
* Called once per finalized dump file, in block order, before live tailing.
|
|
196
|
+
* Process the parquet with your own tooling (e.g. DuckDB) — the SDK does not
|
|
197
|
+
* decode parquet. Use `client.dumps.download(file)` to fetch + verify bytes.
|
|
198
|
+
*/
|
|
199
|
+
onDumpFile: (file: StreamsDumpFile) => Promise<void> | void
|
|
200
|
+
/** Called per live page after the dump phase, like `consume`. */
|
|
201
|
+
onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope) => Promise<string | null | undefined> | string | null | undefined
|
|
202
|
+
mode?: "tail" | "bounded"
|
|
203
|
+
batchSize?: number
|
|
204
|
+
emptyBackoffMs?: number
|
|
205
|
+
maxPages?: number
|
|
206
|
+
maxEmptyPolls?: number
|
|
207
|
+
signal?: AbortSignal
|
|
208
|
+
};
|
|
89
209
|
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
210
|
+
/** One bulk parquet file in the dumps manifest. `path` is the object key under
|
|
211
|
+
* the dumps base URL. */
|
|
212
|
+
type StreamsDumpFile = {
|
|
213
|
+
path: string
|
|
214
|
+
from_block: number
|
|
215
|
+
to_block: number
|
|
216
|
+
min_cursor: string | null
|
|
217
|
+
max_cursor: string | null
|
|
218
|
+
row_count: number
|
|
219
|
+
byte_size: number
|
|
220
|
+
sha256: string
|
|
221
|
+
schema_version: number
|
|
222
|
+
created_at: string
|
|
223
|
+
};
|
|
224
|
+
type StreamsDumpsManifest = {
|
|
225
|
+
dataset: string
|
|
226
|
+
network: string
|
|
227
|
+
version: string
|
|
228
|
+
schema_version: number
|
|
229
|
+
generated_at: string
|
|
230
|
+
producer_version: string
|
|
231
|
+
finality_lag_blocks: number
|
|
232
|
+
/** Cursor at the end of the finalized bulk coverage — hand to live tailing. */
|
|
233
|
+
latest_finalized_cursor: string | null
|
|
234
|
+
coverage: {
|
|
235
|
+
from_block: number
|
|
236
|
+
to_block: number
|
|
237
|
+
}
|
|
238
|
+
files: StreamsDumpFile[]
|
|
239
|
+
};
|
|
240
|
+
type StreamsDumps = {
|
|
241
|
+
/** Fetch and parse the latest dumps manifest. */
|
|
242
|
+
list(): Promise<StreamsDumpsManifest>
|
|
243
|
+
/** Absolute URL for a manifest file. */
|
|
244
|
+
fileUrl(file: StreamsDumpFile): string
|
|
245
|
+
/** Download a parquet file and verify its sha256 against the manifest. */
|
|
246
|
+
download(file: StreamsDumpFile): Promise<Uint8Array>
|
|
247
|
+
};
|
|
90
248
|
type StreamsClient = {
|
|
91
249
|
events: {
|
|
92
250
|
list(params?: StreamsEventsListParams): Promise<StreamsEventsEnvelope>
|
|
@@ -102,6 +260,14 @@ type StreamsClient = {
|
|
|
102
260
|
*/
|
|
103
261
|
consume(params: StreamsEventsConsumeParams): Promise<StreamsEventsConsumeResult>
|
|
104
262
|
/**
|
|
263
|
+
* Backfill from bulk dumps, then continue live from the dump→live seam in
|
|
264
|
+
* one call. Iterates finalized dump files (via `onDumpFile`) in block
|
|
265
|
+
* order, then tails live from the manifest's `latest_finalized_cursor`
|
|
266
|
+
* (exclusive input → no gap or duplicate at the seam). Requires
|
|
267
|
+
* `dumpsBaseUrl`.
|
|
268
|
+
*/
|
|
269
|
+
replay(params: StreamsEventsReplayParams): Promise<StreamsEventsConsumeResult>
|
|
270
|
+
/**
|
|
105
271
|
* Follow Streams as an async iterator.
|
|
106
272
|
*
|
|
107
273
|
* Use `stream` for live processors and watch-style apps. It tails
|
|
@@ -116,6 +282,8 @@ type StreamsClient = {
|
|
|
116
282
|
reorgs: {
|
|
117
283
|
list(params: StreamsReorgsListParams): Promise<StreamsReorgsListEnvelope>
|
|
118
284
|
}
|
|
285
|
+
/** Bulk parquet dumps. Requires `dumpsBaseUrl` on the client. */
|
|
286
|
+
dumps: StreamsDumps
|
|
119
287
|
canonical(height: number): Promise<StreamsCanonicalBlock>
|
|
120
288
|
tip(): Promise<StreamsTip>
|
|
121
289
|
};
|
|
@@ -123,6 +291,20 @@ type CreateStreamsClientOptions = {
|
|
|
123
291
|
apiKey: string
|
|
124
292
|
baseUrl?: string
|
|
125
293
|
fetchImpl?: FetchLike
|
|
294
|
+
/**
|
|
295
|
+
* Public base URL for bulk parquet dumps (the R2/CDN bucket root). Required
|
|
296
|
+
* to use `client.dumps`. See `GET /public/streams/dumps/manifest`.
|
|
297
|
+
*/
|
|
298
|
+
dumpsBaseUrl?: string
|
|
299
|
+
/**
|
|
300
|
+
* Verify the ed25519 `X-Signature` on every response (default off). Pass
|
|
301
|
+
* `true` to fetch the server's public key from
|
|
302
|
+
* `/public/streams/signing-key`, or `{ publicKey }` to pin a known PEM. A
|
|
303
|
+
* failed or missing signature throws `StreamsSignatureError`.
|
|
304
|
+
*/
|
|
305
|
+
verify?: boolean | {
|
|
306
|
+
publicKey: string
|
|
307
|
+
}
|
|
126
308
|
};
|
|
127
309
|
declare function createStreamsClient(options: CreateStreamsClientOptions): StreamsClient;
|
|
128
310
|
declare class AuthError extends Error {
|
|
@@ -144,16 +326,13 @@ declare class StreamsServerError extends Error {
|
|
|
144
326
|
readonly body?: unknown;
|
|
145
327
|
constructor(message: string, status: number, body?: unknown);
|
|
146
328
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
};
|
|
153
|
-
type FtTransferEvent = StreamsEvent & {
|
|
329
|
+
/** Thrown when response signature verification is enabled and fails. */
|
|
330
|
+
declare class StreamsSignatureError extends Error {
|
|
331
|
+
constructor(message?: string);
|
|
332
|
+
}
|
|
333
|
+
type FtTransferEvent = Extract<StreamsEvent, {
|
|
154
334
|
event_type: "ft_transfer"
|
|
155
|
-
|
|
156
|
-
};
|
|
335
|
+
}>;
|
|
157
336
|
type DecodedFtTransferPayload = {
|
|
158
337
|
asset_identifier: string
|
|
159
338
|
contract_id: string
|
|
@@ -174,18 +353,9 @@ type DecodedFtTransfer = {
|
|
|
174
353
|
};
|
|
175
354
|
declare function isFtTransfer(event: StreamsEvent): event is FtTransferEvent;
|
|
176
355
|
declare function decodeFtTransfer(event: StreamsEvent): DecodedFtTransfer;
|
|
177
|
-
type
|
|
178
|
-
asset_identifier: string
|
|
179
|
-
sender: string
|
|
180
|
-
recipient: string
|
|
181
|
-
value: string | {
|
|
182
|
-
hex: string
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
type NftTransferEvent = StreamsEvent & {
|
|
356
|
+
type NftTransferEvent = Extract<StreamsEvent, {
|
|
186
357
|
event_type: "nft_transfer"
|
|
187
|
-
|
|
188
|
-
};
|
|
358
|
+
}>;
|
|
189
359
|
type DecodedNftTransferPayload = {
|
|
190
360
|
asset_identifier: string
|
|
191
361
|
contract_id: string
|
|
@@ -410,4 +580,4 @@ type DecodedEventColumns = {
|
|
|
410
580
|
payload?: unknown
|
|
411
581
|
};
|
|
412
582
|
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 };
|
|
583
|
+
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 };
|
package/dist/streams/index.js
CHANGED
|
@@ -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,11 @@ async function consumeStreamsEvents(opts) {
|
|
|
26
29
|
cursor,
|
|
27
30
|
limit: opts.batchSize,
|
|
28
31
|
types: opts.types,
|
|
29
|
-
|
|
32
|
+
notTypes: opts.notTypes,
|
|
33
|
+
contractId: opts.contractId,
|
|
34
|
+
sender: opts.sender,
|
|
35
|
+
recipient: opts.recipient,
|
|
36
|
+
assetIdentifier: opts.assetIdentifier
|
|
30
37
|
});
|
|
31
38
|
pages++;
|
|
32
39
|
const returnedCursor = await opts.onBatch(envelope.events, envelope);
|
|
@@ -61,7 +68,11 @@ async function* streamStreamsEvents(opts) {
|
|
|
61
68
|
cursor,
|
|
62
69
|
limit: opts.batchSize,
|
|
63
70
|
types: opts.types,
|
|
64
|
-
|
|
71
|
+
notTypes: opts.notTypes,
|
|
72
|
+
contractId: opts.contractId,
|
|
73
|
+
sender: opts.sender,
|
|
74
|
+
recipient: opts.recipient,
|
|
75
|
+
assetIdentifier: opts.assetIdentifier
|
|
65
76
|
});
|
|
66
77
|
pages++;
|
|
67
78
|
for (const event of envelope.events) {
|
|
@@ -86,6 +97,9 @@ async function* streamStreamsEvents(opts) {
|
|
|
86
97
|
}
|
|
87
98
|
}
|
|
88
99
|
|
|
100
|
+
// src/streams/dumps.ts
|
|
101
|
+
import { createHash } from "node:crypto";
|
|
102
|
+
|
|
89
103
|
// src/streams/errors.ts
|
|
90
104
|
class AuthError extends Error {
|
|
91
105
|
status = 401;
|
|
@@ -127,7 +141,60 @@ class StreamsServerError extends Error {
|
|
|
127
141
|
}
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
class StreamsSignatureError extends Error {
|
|
145
|
+
constructor(message = "Streams response signature verification failed.") {
|
|
146
|
+
super(message);
|
|
147
|
+
this.name = "StreamsSignatureError";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/streams/dumps.ts
|
|
152
|
+
function createStreamsDumps(opts) {
|
|
153
|
+
const baseUrl = opts.baseUrl?.replace(/\/+$/, "");
|
|
154
|
+
function requireBaseUrl() {
|
|
155
|
+
if (!baseUrl) {
|
|
156
|
+
throw new StreamsServerError("Streams dumps require `dumpsBaseUrl` on createStreamsClient.", 0);
|
|
157
|
+
}
|
|
158
|
+
return baseUrl;
|
|
159
|
+
}
|
|
160
|
+
function fileUrl(file) {
|
|
161
|
+
return `${requireBaseUrl()}/${file.path.replace(/^\/+/, "")}`;
|
|
162
|
+
}
|
|
163
|
+
async function list() {
|
|
164
|
+
const url = `${requireBaseUrl()}/manifest/latest.json`;
|
|
165
|
+
const res = await opts.fetchImpl(url);
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
throw new StreamsServerError(`Could not fetch dumps manifest (${res.status}).`, res.status);
|
|
168
|
+
}
|
|
169
|
+
return await res.json();
|
|
170
|
+
}
|
|
171
|
+
async function download(file) {
|
|
172
|
+
const res = await opts.fetchImpl(fileUrl(file));
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
throw new StreamsServerError(`Could not download dump ${file.path} (${res.status}).`, res.status);
|
|
175
|
+
}
|
|
176
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
177
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
178
|
+
if (digest !== file.sha256) {
|
|
179
|
+
throw new StreamsSignatureError(`Dump ${file.path} sha256 mismatch (expected ${file.sha256}, got ${digest}).`);
|
|
180
|
+
}
|
|
181
|
+
return bytes;
|
|
182
|
+
}
|
|
183
|
+
return { list, fileUrl, download };
|
|
184
|
+
}
|
|
185
|
+
|
|
130
186
|
// src/streams/client.ts
|
|
187
|
+
function cursorTuple(cursor) {
|
|
188
|
+
if (!cursor)
|
|
189
|
+
return [-1, -1];
|
|
190
|
+
const [block, index] = cursor.split(":");
|
|
191
|
+
return [Number(block), Number(index)];
|
|
192
|
+
}
|
|
193
|
+
function maxCursor(a, b) {
|
|
194
|
+
const [ah, ai] = cursorTuple(a);
|
|
195
|
+
const [bh, bi] = cursorTuple(b);
|
|
196
|
+
return ah > bh || ah === bh && ai >= bi ? a : b;
|
|
197
|
+
}
|
|
131
198
|
var DEFAULT_STREAMS_BASE_URL = "https://api.secondlayer.tools";
|
|
132
199
|
function normalizeBaseUrl(baseUrl) {
|
|
133
200
|
return baseUrl.replace(/\/+$/, "");
|
|
@@ -137,6 +204,13 @@ function appendSearchParam(params, name, value) {
|
|
|
137
204
|
return;
|
|
138
205
|
params.set(name, String(value));
|
|
139
206
|
}
|
|
207
|
+
function appendListParam(params, name, value) {
|
|
208
|
+
if (value === undefined || value === null)
|
|
209
|
+
return;
|
|
210
|
+
const joined = Array.isArray(value) ? value.join(",") : value;
|
|
211
|
+
if (joined.length > 0)
|
|
212
|
+
params.set(name, joined);
|
|
213
|
+
}
|
|
140
214
|
async function responseBody(response) {
|
|
141
215
|
const text = await response.text();
|
|
142
216
|
if (text.length === 0)
|
|
@@ -175,21 +249,87 @@ async function mapStreamsError(response) {
|
|
|
175
249
|
function createStreamsClient(options) {
|
|
176
250
|
const baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_STREAMS_BASE_URL);
|
|
177
251
|
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
252
|
+
const verify = options.verify ?? false;
|
|
253
|
+
const dumps = createStreamsDumps({
|
|
254
|
+
baseUrl: options.dumpsBaseUrl,
|
|
255
|
+
fetchImpl
|
|
256
|
+
});
|
|
257
|
+
let keyPromise = null;
|
|
258
|
+
function loadKey() {
|
|
259
|
+
if (keyPromise)
|
|
260
|
+
return keyPromise;
|
|
261
|
+
keyPromise = (async () => {
|
|
262
|
+
if (typeof verify === "object") {
|
|
263
|
+
return {
|
|
264
|
+
keyId: ed25519.ed25519KeyId(verify.publicKey),
|
|
265
|
+
publicKey: ed25519.loadEd25519PublicKey(verify.publicKey)
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const res = await fetchImpl(`${baseUrl}/public/streams/signing-key`);
|
|
269
|
+
if (!res.ok) {
|
|
270
|
+
throw new StreamsSignatureError(`Could not fetch signing key (${res.status}).`);
|
|
271
|
+
}
|
|
272
|
+
const body = await res.json();
|
|
273
|
+
if (!body.public_key_pem) {
|
|
274
|
+
throw new StreamsSignatureError("Signing key response missing key.");
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
keyId: body.key_id ?? ed25519.ed25519KeyId(body.public_key_pem),
|
|
278
|
+
publicKey: ed25519.loadEd25519PublicKey(body.public_key_pem)
|
|
279
|
+
};
|
|
280
|
+
})();
|
|
281
|
+
return keyPromise;
|
|
282
|
+
}
|
|
178
283
|
async function request(path) {
|
|
179
284
|
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
180
285
|
headers: { Authorization: `Bearer ${options.apiKey}` }
|
|
181
286
|
});
|
|
182
287
|
if (!response.ok)
|
|
183
288
|
await mapStreamsError(response);
|
|
184
|
-
|
|
289
|
+
const text = await response.text();
|
|
290
|
+
if (verify) {
|
|
291
|
+
const signature = response.headers.get("X-Signature");
|
|
292
|
+
if (!signature) {
|
|
293
|
+
throw new StreamsSignatureError("Response is missing X-Signature.");
|
|
294
|
+
}
|
|
295
|
+
const responseKeyId = response.headers.get("X-Signature-KeyId");
|
|
296
|
+
let key = await loadKey();
|
|
297
|
+
if (responseKeyId && responseKeyId !== key.keyId) {
|
|
298
|
+
if (typeof verify === "object") {
|
|
299
|
+
throw new StreamsSignatureError(`Response signed with key '${responseKeyId}', expected pinned key '${key.keyId}'.`);
|
|
300
|
+
}
|
|
301
|
+
keyPromise = null;
|
|
302
|
+
key = await loadKey();
|
|
303
|
+
if (responseKeyId !== key.keyId) {
|
|
304
|
+
throw new StreamsSignatureError(`Response signed with key '${responseKeyId}' not served by the signing-key endpoint.`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!ed25519.verifyEd25519(text, signature, key.publicKey)) {
|
|
308
|
+
throw new StreamsSignatureError;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return JSON.parse(text);
|
|
185
312
|
}
|
|
186
313
|
const fetchEvents = async ({
|
|
187
314
|
cursor,
|
|
188
315
|
limit,
|
|
189
316
|
types,
|
|
190
|
-
|
|
317
|
+
notTypes,
|
|
318
|
+
contractId,
|
|
319
|
+
sender,
|
|
320
|
+
recipient,
|
|
321
|
+
assetIdentifier
|
|
191
322
|
}) => {
|
|
192
|
-
return listEvents({
|
|
323
|
+
return listEvents({
|
|
324
|
+
cursor,
|
|
325
|
+
limit,
|
|
326
|
+
types,
|
|
327
|
+
notTypes,
|
|
328
|
+
contractId,
|
|
329
|
+
sender,
|
|
330
|
+
recipient,
|
|
331
|
+
assetIdentifier
|
|
332
|
+
});
|
|
193
333
|
};
|
|
194
334
|
async function listEvents(params = {}) {
|
|
195
335
|
const searchParams = new URLSearchParams;
|
|
@@ -197,10 +337,16 @@ function createStreamsClient(options) {
|
|
|
197
337
|
appendSearchParam(searchParams, "from_height", params.fromHeight);
|
|
198
338
|
appendSearchParam(searchParams, "to_height", params.toHeight);
|
|
199
339
|
appendSearchParam(searchParams, "limit", params.limit);
|
|
200
|
-
|
|
340
|
+
appendListParam(searchParams, "contract_id", params.contractId);
|
|
341
|
+
appendListParam(searchParams, "sender", params.sender);
|
|
342
|
+
appendListParam(searchParams, "recipient", params.recipient);
|
|
343
|
+
appendSearchParam(searchParams, "asset_identifier", params.assetIdentifier);
|
|
201
344
|
if (params.types?.length) {
|
|
202
345
|
searchParams.set("types", params.types.join(","));
|
|
203
346
|
}
|
|
347
|
+
if (params.notTypes?.length) {
|
|
348
|
+
searchParams.set("not_types", params.notTypes.join(","));
|
|
349
|
+
}
|
|
204
350
|
const query = searchParams.toString();
|
|
205
351
|
return request(`/v1/streams/events${query ? `?${query}` : ""}`);
|
|
206
352
|
}
|
|
@@ -215,7 +361,11 @@ function createStreamsClient(options) {
|
|
|
215
361
|
fromCursor: params.fromCursor,
|
|
216
362
|
mode: params.mode,
|
|
217
363
|
types: params.types,
|
|
364
|
+
notTypes: params.notTypes,
|
|
218
365
|
contractId: params.contractId,
|
|
366
|
+
sender: params.sender,
|
|
367
|
+
recipient: params.recipient,
|
|
368
|
+
assetIdentifier: params.assetIdentifier,
|
|
219
369
|
batchSize: params.batchSize ?? 100,
|
|
220
370
|
fetchEvents,
|
|
221
371
|
onBatch: params.onBatch,
|
|
@@ -229,7 +379,11 @@ function createStreamsClient(options) {
|
|
|
229
379
|
return streamStreamsEvents({
|
|
230
380
|
fromCursor: params.fromCursor,
|
|
231
381
|
types: params.types,
|
|
382
|
+
notTypes: params.notTypes,
|
|
232
383
|
contractId: params.contractId,
|
|
384
|
+
sender: params.sender,
|
|
385
|
+
recipient: params.recipient,
|
|
386
|
+
assetIdentifier: params.assetIdentifier,
|
|
233
387
|
batchSize: params.batchSize ?? 100,
|
|
234
388
|
emptyBackoffMs: params.emptyBackoffMs,
|
|
235
389
|
maxPages: params.maxPages,
|
|
@@ -237,6 +391,29 @@ function createStreamsClient(options) {
|
|
|
237
391
|
signal: params.signal,
|
|
238
392
|
fetchEvents
|
|
239
393
|
});
|
|
394
|
+
},
|
|
395
|
+
async replay(params) {
|
|
396
|
+
const fromCursor = params.from === "genesis" ? null : params.from ?? null;
|
|
397
|
+
const fromBlock = fromCursor ? cursorTuple(fromCursor)[0] : 0;
|
|
398
|
+
const manifest = await dumps.list();
|
|
399
|
+
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);
|
|
400
|
+
for (const file of files) {
|
|
401
|
+
if (params.signal?.aborted)
|
|
402
|
+
break;
|
|
403
|
+
await params.onDumpFile(file);
|
|
404
|
+
}
|
|
405
|
+
const seam = maxCursor(fromCursor, manifest.latest_finalized_cursor);
|
|
406
|
+
return consumeStreamsEvents({
|
|
407
|
+
fromCursor: seam,
|
|
408
|
+
mode: params.mode ?? "tail",
|
|
409
|
+
batchSize: params.batchSize ?? 100,
|
|
410
|
+
fetchEvents,
|
|
411
|
+
onBatch: params.onBatch,
|
|
412
|
+
emptyBackoffMs: params.emptyBackoffMs,
|
|
413
|
+
maxPages: params.maxPages,
|
|
414
|
+
maxEmptyPolls: params.maxEmptyPolls,
|
|
415
|
+
signal: params.signal
|
|
416
|
+
});
|
|
240
417
|
}
|
|
241
418
|
},
|
|
242
419
|
blocks: {
|
|
@@ -253,6 +430,7 @@ function createStreamsClient(options) {
|
|
|
253
430
|
return request(`/v1/streams/reorgs${query ? `?${query}` : ""}`);
|
|
254
431
|
}
|
|
255
432
|
},
|
|
433
|
+
dumps,
|
|
256
434
|
canonical(height) {
|
|
257
435
|
return request(`/v1/streams/canonical/${height}`);
|
|
258
436
|
},
|
|
@@ -652,11 +830,12 @@ export {
|
|
|
652
830
|
decodeFtBurn,
|
|
653
831
|
createStreamsClient,
|
|
654
832
|
ValidationError,
|
|
833
|
+
StreamsSignatureError,
|
|
655
834
|
StreamsServerError,
|
|
656
835
|
STREAMS_EVENT_TYPES,
|
|
657
836
|
RateLimitError,
|
|
658
837
|
AuthError
|
|
659
838
|
};
|
|
660
839
|
|
|
661
|
-
//# debugId=
|
|
840
|
+
//# debugId=E556011474C772DE64756E2164756E21
|
|
662
841
|
//# sourceMappingURL=index.js.map
|