@secondlayer/sdk 6.9.1 → 6.10.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 +67 -1
- package/dist/index.js +173 -12
- package/dist/index.js.map +9 -8
- package/dist/streams/index.d.ts +36 -1
- package/dist/streams/index.js +166 -12
- package/dist/streams/index.js.map +7 -6
- package/dist/subgraphs/index.d.ts +30 -0
- package/dist/subgraphs/index.js +166 -12
- package/dist/subgraphs/index.js.map +7 -6
- package/package.json +3 -3
package/dist/streams/index.d.ts
CHANGED
|
@@ -171,6 +171,23 @@ type StreamsEventsStreamParams = {
|
|
|
171
171
|
maxEmptyPolls?: number
|
|
172
172
|
signal?: AbortSignal
|
|
173
173
|
};
|
|
174
|
+
type StreamsEventsSubscribeParams = {
|
|
175
|
+
/** Resume strictly after this cursor; omit to live-tail from the tip. */
|
|
176
|
+
fromCursor?: string | null
|
|
177
|
+
types?: readonly StreamsEventType[]
|
|
178
|
+
notTypes?: readonly StreamsEventType[]
|
|
179
|
+
contractId?: StreamsFilterValue
|
|
180
|
+
sender?: StreamsFilterValue
|
|
181
|
+
recipient?: StreamsFilterValue
|
|
182
|
+
assetIdentifier?: string
|
|
183
|
+
/** Abort to unsubscribe (the returned function does the same). */
|
|
184
|
+
signal?: AbortSignal
|
|
185
|
+
/** Called for each pushed event, in order. */
|
|
186
|
+
onEvent: (event: StreamsEvent) => void | Promise<void>
|
|
187
|
+
/** Called on a connection error; the subscription auto-reconnects from the
|
|
188
|
+
* last delivered cursor unless the signal has aborted. */
|
|
189
|
+
onError?: (err: unknown) => void
|
|
190
|
+
};
|
|
174
191
|
/**
|
|
175
192
|
* The checkpoint the SDK computes for a batch. Persist `cursor` inside the same
|
|
176
193
|
* transaction as your projection writes, then resume from it via `fromCursor`.
|
|
@@ -276,6 +293,11 @@ type StreamsDumpsManifest = {
|
|
|
276
293
|
to_block: number
|
|
277
294
|
}
|
|
278
295
|
files: StreamsDumpFile[]
|
|
296
|
+
/** ed25519 signature over the manifest's canonical bytes. Absent on legacy
|
|
297
|
+
* unsigned manifests. Verified by `list()` when `verifyDumpsManifest` is on. */
|
|
298
|
+
signature?: string
|
|
299
|
+
/** Short id of the signing public key. */
|
|
300
|
+
key_id?: string
|
|
279
301
|
};
|
|
280
302
|
type StreamsDumps = {
|
|
281
303
|
/** Fetch and parse the latest dumps manifest. */
|
|
@@ -315,6 +337,13 @@ type StreamsClient = {
|
|
|
315
337
|
* `maxEmptyPolls` stops it.
|
|
316
338
|
*/
|
|
317
339
|
stream(params?: StreamsEventsStreamParams): AsyncIterable<StreamsEvent>
|
|
340
|
+
/**
|
|
341
|
+
* Subscribe to the real-time SSE push surface. Calls `onEvent` for each new
|
|
342
|
+
* canonical event as the server pushes it (chain cadence, not poll-bounded),
|
|
343
|
+
* and verifies each frame's inline ed25519 signature when the client was
|
|
344
|
+
* created with `verify`. Returns an unsubscribe function.
|
|
345
|
+
*/
|
|
346
|
+
subscribe(params: StreamsEventsSubscribeParams): () => void
|
|
318
347
|
}
|
|
319
348
|
blocks: {
|
|
320
349
|
events(heightOrHash: number | string): Promise<StreamsEventsListEnvelope>
|
|
@@ -359,6 +388,12 @@ type CreateStreamsClientOptions = {
|
|
|
359
388
|
verify?: boolean | {
|
|
360
389
|
publicKey: string
|
|
361
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Verify the bulk dumps manifest's ed25519 signature in `client.dumps.list()`
|
|
393
|
+
* before trusting any file sha256 (default off). Uses the same key source as
|
|
394
|
+
* `verify`. Default off until historical manifests carry signatures.
|
|
395
|
+
*/
|
|
396
|
+
verifyDumpsManifest?: boolean
|
|
362
397
|
};
|
|
363
398
|
declare function createStreamsClient(options: CreateStreamsClientOptions): StreamsClient;
|
|
364
399
|
declare class AuthError extends Error {
|
|
@@ -653,4 +688,4 @@ declare const Cursor: {
|
|
|
653
688
|
}
|
|
654
689
|
};
|
|
655
690
|
type DecodedEventRow = DecodedFtTransfer | DecodedNftTransfer | DecodedStxTransfer | DecodedStxMint | DecodedStxBurn | DecodedStxLock | DecodedFtMint | DecodedFtBurn | DecodedNftMint | DecodedNftBurn | DecodedPrint;
|
|
656
|
-
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, StreamsUsage, StreamsTip, StreamsSignatureError, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorgContext, StreamsReorg, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsDumpsManifest, StreamsDumps, StreamsDumpFile, StreamsClient, StreamsCanonicalBlock, StreamsBatchContext, 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, Cursor, AuthError };
|
|
691
|
+
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, StreamsUsage, StreamsTip, StreamsSignatureError, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorgContext, StreamsReorg, StreamsEventsSubscribeParams, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsDumpsManifest, StreamsDumps, StreamsDumpFile, StreamsClient, StreamsCanonicalBlock, StreamsBatchContext, 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, Cursor, AuthError };
|
package/dist/streams/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/streams/client.ts
|
|
2
|
-
import { ed25519 } from "@secondlayer/shared";
|
|
2
|
+
import { ed25519 as ed255192 } from "@secondlayer/shared";
|
|
3
3
|
|
|
4
4
|
// src/errors.ts
|
|
5
5
|
class ApiError extends Error {
|
|
@@ -317,6 +317,7 @@ async function* streamStreamsEvents(opts) {
|
|
|
317
317
|
|
|
318
318
|
// src/streams/dumps.ts
|
|
319
319
|
import { createHash } from "node:crypto";
|
|
320
|
+
import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
|
|
320
321
|
function createStreamsDumps(opts) {
|
|
321
322
|
const baseUrl = opts.baseUrl?.replace(/\/+$/, "");
|
|
322
323
|
function requireBaseUrl() {
|
|
@@ -334,7 +335,17 @@ function createStreamsDumps(opts) {
|
|
|
334
335
|
if (!res.ok) {
|
|
335
336
|
throw new StreamsServerError(`Could not fetch dumps manifest (${res.status}).`, res.status);
|
|
336
337
|
}
|
|
337
|
-
|
|
338
|
+
const manifest = await res.json();
|
|
339
|
+
if (opts.verifyManifest) {
|
|
340
|
+
if (!opts.loadPublicKeyPem) {
|
|
341
|
+
throw new StreamsSignatureError("Manifest verification is on but no signing key source is configured.");
|
|
342
|
+
}
|
|
343
|
+
const publicKeyPem = await opts.loadPublicKeyPem();
|
|
344
|
+
if (!verifyStreamsBulkManifestSignature(manifest, publicKeyPem)) {
|
|
345
|
+
throw new StreamsSignatureError("Dumps manifest signature is missing or invalid.");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return manifest;
|
|
338
349
|
}
|
|
339
350
|
async function download(file) {
|
|
340
351
|
const res = await opts.fetchImpl(fileUrl(file));
|
|
@@ -351,6 +362,135 @@ function createStreamsDumps(opts) {
|
|
|
351
362
|
return { list, fileUrl, download };
|
|
352
363
|
}
|
|
353
364
|
|
|
365
|
+
// src/streams/subscribe.ts
|
|
366
|
+
import { ed25519 } from "@secondlayer/shared";
|
|
367
|
+
function subscribeStreamsEvents(opts) {
|
|
368
|
+
const { params } = opts;
|
|
369
|
+
const controller = new AbortController;
|
|
370
|
+
const external = params.signal;
|
|
371
|
+
if (external) {
|
|
372
|
+
if (external.aborted)
|
|
373
|
+
controller.abort();
|
|
374
|
+
else
|
|
375
|
+
external.addEventListener("abort", () => controller.abort(), {
|
|
376
|
+
once: true
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
let cursor = params.fromCursor ?? null;
|
|
380
|
+
const reconnectDelayMs = opts.reconnectDelayMs ?? 1000;
|
|
381
|
+
const run = async () => {
|
|
382
|
+
while (!controller.signal.aborted) {
|
|
383
|
+
try {
|
|
384
|
+
const url = `${opts.baseUrl}/v1/streams/events/stream${buildQuery({
|
|
385
|
+
from_cursor: cursor ?? undefined,
|
|
386
|
+
types: params.types,
|
|
387
|
+
not_types: params.notTypes,
|
|
388
|
+
contract_id: params.contractId,
|
|
389
|
+
sender: params.sender,
|
|
390
|
+
recipient: params.recipient,
|
|
391
|
+
asset_identifier: params.assetIdentifier
|
|
392
|
+
})}`;
|
|
393
|
+
const res = await opts.fetchImpl(url, {
|
|
394
|
+
headers: {
|
|
395
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
396
|
+
Accept: "text/event-stream"
|
|
397
|
+
},
|
|
398
|
+
signal: controller.signal
|
|
399
|
+
});
|
|
400
|
+
if (!res.ok) {
|
|
401
|
+
throw new StreamsServerError(`Streams SSE returned ${res.status}.`, res.status);
|
|
402
|
+
}
|
|
403
|
+
if (!res.body) {
|
|
404
|
+
throw new StreamsServerError("Streams SSE response has no body.", 0);
|
|
405
|
+
}
|
|
406
|
+
for await (const frame of parseSseFrames(res.body, controller.signal)) {
|
|
407
|
+
if (frame.event === "ping" || !frame.data)
|
|
408
|
+
continue;
|
|
409
|
+
let parsed;
|
|
410
|
+
try {
|
|
411
|
+
parsed = JSON.parse(frame.data);
|
|
412
|
+
} catch {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (!parsed.event)
|
|
416
|
+
continue;
|
|
417
|
+
if (opts.verify) {
|
|
418
|
+
const key = await opts.loadKey();
|
|
419
|
+
if (!parsed.sig || !ed25519.verifyEd25519(JSON.stringify(parsed.event), parsed.sig, key.publicKey)) {
|
|
420
|
+
throw new StreamsSignatureError("Streams SSE frame signature is missing or invalid.");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
cursor = parsed.event.cursor ?? cursor;
|
|
424
|
+
await params.onEvent(parsed.event);
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
if (controller.signal.aborted)
|
|
428
|
+
return;
|
|
429
|
+
params.onError?.(err);
|
|
430
|
+
await sleep(reconnectDelayMs, controller.signal);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
run();
|
|
435
|
+
return () => controller.abort();
|
|
436
|
+
}
|
|
437
|
+
function sleep(ms, signal) {
|
|
438
|
+
return new Promise((resolve) => {
|
|
439
|
+
if (signal.aborted)
|
|
440
|
+
return resolve();
|
|
441
|
+
const onAbort = () => {
|
|
442
|
+
clearTimeout(timer);
|
|
443
|
+
resolve();
|
|
444
|
+
};
|
|
445
|
+
const timer = setTimeout(() => {
|
|
446
|
+
signal.removeEventListener("abort", onAbort);
|
|
447
|
+
resolve();
|
|
448
|
+
}, ms);
|
|
449
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async function* parseSseFrames(body, signal) {
|
|
453
|
+
const reader = body.getReader();
|
|
454
|
+
const decoder = new TextDecoder;
|
|
455
|
+
let buffer = "";
|
|
456
|
+
try {
|
|
457
|
+
while (!signal.aborted) {
|
|
458
|
+
const { value, done } = await reader.read();
|
|
459
|
+
if (done)
|
|
460
|
+
break;
|
|
461
|
+
buffer += decoder.decode(value, { stream: true });
|
|
462
|
+
let sep = buffer.indexOf(`
|
|
463
|
+
|
|
464
|
+
`);
|
|
465
|
+
while (sep !== -1) {
|
|
466
|
+
yield parseFrame(buffer.slice(0, sep));
|
|
467
|
+
buffer = buffer.slice(sep + 2);
|
|
468
|
+
sep = buffer.indexOf(`
|
|
469
|
+
|
|
470
|
+
`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} finally {
|
|
474
|
+
try {
|
|
475
|
+
await reader.cancel();
|
|
476
|
+
} catch {}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function parseFrame(raw) {
|
|
480
|
+
let event;
|
|
481
|
+
const data = [];
|
|
482
|
+
for (const line of raw.split(`
|
|
483
|
+
`)) {
|
|
484
|
+
if (line.startsWith("data:")) {
|
|
485
|
+
data.push(line.slice(line.startsWith("data: ") ? 6 : 5));
|
|
486
|
+
} else if (line.startsWith("event:")) {
|
|
487
|
+
event = line.slice(line.startsWith("event: ") ? 7 : 6).trim();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return { event, data: data.length > 0 ? data.join(`
|
|
491
|
+
`) : undefined };
|
|
492
|
+
}
|
|
493
|
+
|
|
354
494
|
// src/streams/client.ts
|
|
355
495
|
function cursorTuple(cursor) {
|
|
356
496
|
if (!cursor)
|
|
@@ -410,10 +550,6 @@ function createStreamsClient(options) {
|
|
|
410
550
|
const baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_STREAMS_BASE_URL);
|
|
411
551
|
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
412
552
|
const verify = options.verify ?? false;
|
|
413
|
-
const dumps = createStreamsDumps({
|
|
414
|
-
baseUrl: options.dumpsBaseUrl,
|
|
415
|
-
fetchImpl
|
|
416
|
-
});
|
|
417
553
|
let keyPromise = null;
|
|
418
554
|
function loadKey() {
|
|
419
555
|
if (keyPromise)
|
|
@@ -421,8 +557,9 @@ function createStreamsClient(options) {
|
|
|
421
557
|
keyPromise = (async () => {
|
|
422
558
|
if (typeof verify === "object") {
|
|
423
559
|
return {
|
|
424
|
-
keyId:
|
|
425
|
-
|
|
560
|
+
keyId: ed255192.ed25519KeyId(verify.publicKey),
|
|
561
|
+
publicKeyPem: verify.publicKey,
|
|
562
|
+
publicKey: ed255192.loadEd25519PublicKey(verify.publicKey)
|
|
426
563
|
};
|
|
427
564
|
}
|
|
428
565
|
const res = await fetchImpl(`${baseUrl}/public/streams/signing-key`);
|
|
@@ -434,12 +571,19 @@ function createStreamsClient(options) {
|
|
|
434
571
|
throw new StreamsSignatureError("Signing key response missing key.");
|
|
435
572
|
}
|
|
436
573
|
return {
|
|
437
|
-
keyId: body.key_id ??
|
|
438
|
-
|
|
574
|
+
keyId: body.key_id ?? ed255192.ed25519KeyId(body.public_key_pem),
|
|
575
|
+
publicKeyPem: body.public_key_pem,
|
|
576
|
+
publicKey: ed255192.loadEd25519PublicKey(body.public_key_pem)
|
|
439
577
|
};
|
|
440
578
|
})();
|
|
441
579
|
return keyPromise;
|
|
442
580
|
}
|
|
581
|
+
const dumps = createStreamsDumps({
|
|
582
|
+
baseUrl: options.dumpsBaseUrl,
|
|
583
|
+
fetchImpl,
|
|
584
|
+
verifyManifest: options.verifyDumpsManifest ?? false,
|
|
585
|
+
loadPublicKeyPem: async () => (await loadKey()).publicKeyPem
|
|
586
|
+
});
|
|
443
587
|
async function request(path) {
|
|
444
588
|
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
445
589
|
headers: { Authorization: `Bearer ${options.apiKey}` }
|
|
@@ -464,7 +608,7 @@ function createStreamsClient(options) {
|
|
|
464
608
|
throw new StreamsSignatureError(`Response signed with key '${responseKeyId}' not served by the signing-key endpoint.`);
|
|
465
609
|
}
|
|
466
610
|
}
|
|
467
|
-
if (!
|
|
611
|
+
if (!ed255192.verifyEd25519(text, signature, key.publicKey)) {
|
|
468
612
|
throw new StreamsSignatureError;
|
|
469
613
|
}
|
|
470
614
|
}
|
|
@@ -549,6 +693,16 @@ function createStreamsClient(options) {
|
|
|
549
693
|
fetchEvents
|
|
550
694
|
});
|
|
551
695
|
},
|
|
696
|
+
subscribe(params) {
|
|
697
|
+
return subscribeStreamsEvents({
|
|
698
|
+
baseUrl,
|
|
699
|
+
apiKey: options.apiKey,
|
|
700
|
+
fetchImpl,
|
|
701
|
+
verify: Boolean(verify),
|
|
702
|
+
loadKey,
|
|
703
|
+
params
|
|
704
|
+
});
|
|
705
|
+
},
|
|
552
706
|
async replay(params) {
|
|
553
707
|
const fromCursor = params.from === "genesis" ? null : params.from ?? null;
|
|
554
708
|
const fromBlock = fromCursor ? cursorTuple(fromCursor)[0] : 0;
|
|
@@ -914,5 +1068,5 @@ export {
|
|
|
914
1068
|
AuthError
|
|
915
1069
|
};
|
|
916
1070
|
|
|
917
|
-
//# debugId=
|
|
1071
|
+
//# debugId=202DB745ED61199F64756E2164756E21
|
|
918
1072
|
//# sourceMappingURL=index.js.map
|