@secondlayer/sdk 6.36.0 → 6.37.1

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.
@@ -277,6 +277,8 @@ declare class Contracts extends BaseClient {
277
277
  includeAbi?: boolean
278
278
  }): Promise<ContractSummary | null>;
279
279
  }
280
+ import { Pox5EventTopic } from "@secondlayer/stacks/pox5";
281
+ import { SbtcEventTopic } from "@secondlayer/stacks/sbtc";
280
282
  import { InferredTopicSchema } from "@secondlayer/subgraphs";
281
283
  import { RewardSet } from "@secondlayer/shared/node/consensus";
282
284
  import { MerkleProofStep } from "@secondlayer/shared/node/nakamoto";
@@ -587,6 +589,18 @@ type StreamsEventsConsumeParams = {
587
589
  * stale rows from an orphaned fork are left in place).
588
590
  */
589
591
  onReorg?: (reorg: StreamsReorg, ctx: StreamsReorgContext) => Promise<void> | void
592
+ /** Page-fetch retries after the first failure (429/5xx/network only —
593
+ * 4xx and handler throws always propagate). Default 3; `0` disables. */
594
+ retryCount?: number
595
+ /** Base retry delay in ms; the n-th retry waits `retryDelay * n`. A server
596
+ * `Retry-After` overrides it. Default 1000. */
597
+ retryDelay?: number
598
+ /** Void observer, called before each retry sleep (metrics/logging). */
599
+ onError?: (err: unknown, ctx: {
600
+ attempt: number
601
+ retriesLeft: number
602
+ delayMs: number
603
+ }) => void
590
604
  emptyBackoffMs?: number
591
605
  maxPages?: number
592
606
  maxEmptyPolls?: number
@@ -774,6 +788,23 @@ type StreamsUsage = {
774
788
  }
775
789
  };
776
790
  type Sleep = (ms: number, signal?: AbortSignal) => Promise<void>;
791
+ /** Options shared by both consume loops' page-fetch retry. Vocabulary matches
792
+ * `@secondlayer/stacks` transports (`retryCount`/`retryDelay`) — one retry
793
+ * language across the family, not a third. */
794
+ type PageRetryOptions = {
795
+ /** Retries after the first failure. Default 3; `0` disables. */
796
+ retryCount?: number
797
+ /** Base delay in ms; the n-th retry waits `retryDelay * n` (matches the
798
+ * stacks transport). A server `Retry-After` overrides it. Default 1000. */
799
+ retryDelay?: number
800
+ /** Void observer, called before each retry sleep (metrics/logging). The
801
+ * retry policy owns the decision; this cannot change it. */
802
+ onError?: (err: unknown, ctx: {
803
+ attempt: number
804
+ retriesLeft: number
805
+ delayMs: number
806
+ }) => void
807
+ };
777
808
  /** Minimum shape a consumed Index row must expose. */
778
809
  type IndexFeedItem = {
779
810
  cursor: string
@@ -815,6 +846,15 @@ type IndexConsumeOptions<
815
846
  maxPages?: number
816
847
  maxEmptyPolls?: number
817
848
  signal?: AbortSignal
849
+ /** Page-fetch retries after the first failure (429/5xx/network only —
850
+ * 4xx and handler throws always propagate). Default 3; `0` disables. */
851
+ retryCount?: number
852
+ /** Base retry delay in ms; the n-th retry waits `retryDelay * n`. A server
853
+ * `Retry-After` overrides it. Default 1000. */
854
+ retryDelay?: number
855
+ /** Void observer, called before each retry sleep. Cannot change the retry
856
+ * decision — the policy owns it. */
857
+ onError?: PageRetryOptions["onError"]
818
858
  };
819
859
  type IndexTip = {
820
860
  block_height: number
@@ -1374,8 +1414,6 @@ type PrintSchemaResponse = {
1374
1414
  }
1375
1415
  tip: IndexTip
1376
1416
  };
1377
- /** The decoded `sbtc_events` topics. */
1378
- type SbtcEventTopic = "completed-deposit" | "withdrawal-create" | "withdrawal-accept" | "withdrawal-reject" | "key-rotation" | "update-protocol-contract";
1379
1417
  type SbtcWithdrawalStatus = "REQUESTED" | "ACCEPTED" | "REJECTED";
1380
1418
  /** A completed sBTC peg-in, keyed by `bitcoin_txid` (deposits carry no
1381
1419
  * `request_id`). One terminal `completed-deposit` event per deposit. */
@@ -1665,10 +1703,10 @@ interface PoxResource {
1665
1703
  get(rewardCycle: number): Promise<PoxCycleEnvelope | null>
1666
1704
  };
1667
1705
  }
1668
- /** The decoded `pox-5` boot-contract print topics (SIP-045 Bitcoin Staking).
1669
- * Declared locally rather than imported from the server packages — the SDK is
1670
- * published and owns its own dependency surface (same as {@link SbtcEventTopic}). */
1671
- type IndexPox5EventTopic = "set-bond-admin" | "set-pause-admin" | "pause-rewards" | "setup-bond" | "add-to-allowlist" | "register-for-bond" | "update-bond-registration" | "register-signer" | "stake" | "stake-update" | "announce-l1-early-exit" | "unstake-sbtc" | "unstake" | "calculate-rewards" | "bond-distribution" | "claim-rewards" | "claim-staker-rewards-for-signer" | "grant-signer-key" | "revoke-signer-grant";
1706
+ /** The decoded `pox-5` boot-contract print topics (SIP-045 Bitcoin Staking) —
1707
+ * one source of truth in `@secondlayer/stacks/pox5` (already a dependency;
1708
+ * the old "SDK owns its own dependency surface" rationale predated that). */
1709
+ type IndexPox5EventTopic = Pox5EventTopic;
1672
1710
  /** A raw decoded PoX-5 print event. Promoted fields cover the hot query paths;
1673
1711
  * `data` always carries the full decoded tuple, including nested shapes
1674
1712
  * (`btc-lockup`, `bond-rewards`, `bond-periods`) that have no flat column. */
@@ -1,17 +1,127 @@
1
1
  // src/errors.ts
2
- class ApiError extends Error {
2
+ var DOCS_BASE = "https://www.secondlayer.tools";
3
+
4
+ class SecondLayerError extends Error {
5
+ name = "SecondLayerError";
6
+ shortMessage;
7
+ code;
8
+ docsUrl;
9
+ metaMessages;
10
+ retryable;
11
+ retryAfterSeconds;
12
+ constructor(shortMessage, options = {}) {
13
+ const detailBlocks = [
14
+ ...options.metaMessages ?? [],
15
+ ...options.details ? [options.details] : [],
16
+ ...options.docsUrl ? [`Docs: ${options.docsUrl}`] : []
17
+ ];
18
+ const message = [
19
+ shortMessage,
20
+ detailBlocks.length > 0 ? `
21
+ ${detailBlocks.join(`
22
+ `)}` : ""
23
+ ].join("");
24
+ super(message, options.cause ? { cause: options.cause } : undefined);
25
+ this.shortMessage = shortMessage;
26
+ this.code = options.code;
27
+ this.docsUrl = options.docsUrl;
28
+ this.metaMessages = options.metaMessages;
29
+ this.retryable = options.retryable ?? false;
30
+ this.retryAfterSeconds = options.retryAfterSeconds;
31
+ }
32
+ walk(fn) {
33
+ let err = this;
34
+ let last = this;
35
+ while (err != null) {
36
+ if (fn?.(err))
37
+ return err;
38
+ last = err;
39
+ err = err.cause;
40
+ }
41
+ return fn ? null : last;
42
+ }
43
+ toJSON() {
44
+ return {
45
+ name: this.name,
46
+ message: this.message,
47
+ shortMessage: this.shortMessage,
48
+ cause: this.cause instanceof Error ? this.cause.message : undefined,
49
+ code: this.code,
50
+ docsUrl: this.docsUrl,
51
+ retryable: this.retryable,
52
+ retryAfterSeconds: this.retryAfterSeconds
53
+ };
54
+ }
55
+ }
56
+ function statusRetryable(status) {
57
+ return status === 429 || status >= 500;
58
+ }
59
+ function docsFor(status, code) {
60
+ if (code === "UPGRADE_REQUIRED" || code === "PLAN_REQUIRED" || status === 402)
61
+ return `${DOCS_BASE}/docs/authentication#pay-as-you-go-credits`;
62
+ if (status === 429 || status === 401)
63
+ return `${DOCS_BASE}/docs/authentication`;
64
+ return;
65
+ }
66
+
67
+ class ApiError extends SecondLayerError {
3
68
  status;
4
69
  body;
5
- code;
6
- constructor(status, message, body, code) {
7
- super(message);
70
+ constructor(status, message, body, code, options = {}) {
71
+ super(message, {
72
+ code,
73
+ retryable: options.retryable ?? statusRetryable(status),
74
+ docsUrl: options.docsUrl ?? docsFor(status, code),
75
+ ...options.cause ? { cause: options.cause } : {},
76
+ ...options.details ? { details: options.details } : {},
77
+ ...options.metaMessages ? { metaMessages: options.metaMessages } : {},
78
+ ...options.retryAfterSeconds !== undefined ? { retryAfterSeconds: options.retryAfterSeconds } : {}
79
+ });
8
80
  this.status = status;
9
81
  this.body = body;
10
- this.code = code;
11
82
  this.name = "ApiError";
12
83
  }
13
84
  }
14
85
 
86
+ class AuthError extends ApiError {
87
+ status = 401;
88
+ constructor(message = "API key invalid or expired.") {
89
+ super(401, message);
90
+ this.name = "AuthError";
91
+ }
92
+ }
93
+
94
+ class RateLimitError extends ApiError {
95
+ retryAfter;
96
+ status = 429;
97
+ constructor(message = "Rate limited. Try again later.", retryAfter) {
98
+ super(429, message, undefined, undefined, {
99
+ retryable: true,
100
+ retryAfterSeconds: parseRetryAfter(retryAfter)
101
+ });
102
+ this.retryAfter = retryAfter;
103
+ this.name = "RateLimitError";
104
+ }
105
+ }
106
+
107
+ class ValidationError extends ApiError {
108
+ constructor(message, status, body) {
109
+ super(status, message, body, undefined, { retryable: false });
110
+ this.name = "ValidationError";
111
+ }
112
+ }
113
+ function parseRetryAfter(value) {
114
+ if (!value)
115
+ return;
116
+ const seconds = Number(value);
117
+ if (Number.isFinite(seconds) && seconds >= 0)
118
+ return seconds;
119
+ const date = Date.parse(value);
120
+ if (!Number.isNaN(date))
121
+ return Math.max(0, (date - Date.now()) / 1000);
122
+ return;
123
+ }
124
+
15
125
  class VersionConflictError extends ApiError {
16
126
  currentVersion;
17
127
  expectedVersion;
@@ -26,7 +136,13 @@ class VersionConflictError extends ApiError {
26
136
  class ByoBreakingChangeError extends ApiError {
27
137
  details;
28
138
  constructor(message, details) {
29
- super(422, message, details, "BYO_BREAKING_CHANGE");
139
+ super(422, message, details, "BYO_BREAKING_CHANGE", {
140
+ metaMessages: [
141
+ "Review and run manually on your database:",
142
+ details.plan.dropStatement,
143
+ ...details.plan.statements
144
+ ]
145
+ });
30
146
  this.name = "ByoBreakingChangeError";
31
147
  this.details = details;
32
148
  }
@@ -112,7 +228,7 @@ class BaseClient {
112
228
  serializedBody = JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value);
113
229
  } catch (err) {
114
230
  const detail = err instanceof Error ? err.message : String(err);
115
- throw new ApiError(0, `Failed to serialize request body: ${detail}`);
231
+ throw new ApiError(0, `Failed to serialize request body: ${detail}`, undefined, undefined, { retryable: false });
116
232
  }
117
233
  }
118
234
  let response;
@@ -122,17 +238,17 @@ class BaseClient {
122
238
  headers,
123
239
  body: serializedBody
124
240
  });
125
- } catch {
126
- throw new ApiError(0, `Cannot reach API at ${this.baseUrl}. Check your connection or try again.`);
241
+ } catch (err) {
242
+ throw new ApiError(0, `Cannot reach API at ${this.baseUrl}. Check your connection or try again.`, undefined, undefined, { retryable: true, cause: err instanceof Error ? err : undefined });
127
243
  }
128
244
  if (!response.ok) {
129
245
  if (response.status === 401) {
130
- throw new ApiError(401, "API key invalid or expired.");
246
+ throw new AuthError;
131
247
  }
132
248
  if (response.status === 429) {
133
249
  const retryAfter = response.headers.get("Retry-After");
134
250
  const msg = retryAfter ? `Rate limited. Wait ${retryAfter} seconds.` : "Rate limited. Try again later.";
135
- throw new ApiError(429, msg);
251
+ throw new RateLimitError(msg, retryAfter ?? undefined);
136
252
  }
137
253
  if (response.status >= 500) {
138
254
  throw new ApiError(response.status, `Server error. Try again or check status at ${this.baseUrl}/health`);
@@ -426,47 +542,14 @@ class Contracts extends BaseClient {
426
542
  }
427
543
 
428
544
  // src/streams/errors.ts
429
- class AuthError extends Error {
430
- status = 401;
431
- constructor(message = "API key invalid or expired.") {
432
- super(message);
433
- this.name = "AuthError";
434
- }
435
- }
436
-
437
- class RateLimitError extends Error {
438
- retryAfter;
439
- status = 429;
440
- constructor(message = "Rate limited. Try again later.", retryAfter) {
441
- super(message);
442
- this.retryAfter = retryAfter;
443
- this.name = "RateLimitError";
444
- }
445
- }
446
-
447
- class ValidationError extends Error {
448
- status;
449
- body;
450
- constructor(message, status, body) {
451
- super(message);
452
- this.status = status;
453
- this.body = body;
454
- this.name = "ValidationError";
455
- }
456
- }
457
-
458
- class StreamsServerError extends Error {
459
- status;
460
- body;
545
+ class StreamsServerError extends ApiError {
461
546
  constructor(message, status, body) {
462
- super(message);
463
- this.status = status;
464
- this.body = body;
547
+ super(status, message, body, undefined, { retryable: true });
465
548
  this.name = "StreamsServerError";
466
549
  }
467
550
  }
468
551
 
469
- class StreamsSignatureError extends Error {
552
+ class StreamsSignatureError extends SecondLayerError {
470
553
  constructor(message = "Streams response signature verification failed.") {
471
554
  super(message);
472
555
  this.name = "StreamsSignatureError";
@@ -517,6 +600,41 @@ function batchContext(cursor, height, tipHeight) {
517
600
  blocksBehind: height === null ? null : Math.max(0, tipHeight - height)
518
601
  };
519
602
  }
603
+ var MAX_RETRY_AFTER_MS = 300000;
604
+ function errRetryable(err) {
605
+ if (err && typeof err === "object" && "retryable" in err) {
606
+ return err.retryable === true;
607
+ }
608
+ return err instanceof TypeError;
609
+ }
610
+ function errRetryAfterMs(err) {
611
+ if (err && typeof err === "object" && "retryAfterSeconds" in err) {
612
+ const seconds = err.retryAfterSeconds;
613
+ if (typeof seconds === "number" && Number.isFinite(seconds)) {
614
+ return Math.min(seconds * 1000, MAX_RETRY_AFTER_MS);
615
+ }
616
+ }
617
+ return;
618
+ }
619
+ async function fetchPageWithRetry(fetchPage, opts) {
620
+ const retryCount = opts.retryCount ?? 3;
621
+ const retryDelay = opts.retryDelay ?? 1000;
622
+ for (let attempt = 0;; attempt++) {
623
+ try {
624
+ return await fetchPage();
625
+ } catch (err) {
626
+ const retriesLeft = retryCount - attempt;
627
+ if (retriesLeft <= 0 || !errRetryable(err) || opts.signal?.aborted) {
628
+ throw err;
629
+ }
630
+ const delayMs = errRetryAfterMs(err) ?? retryDelay * (attempt + 1);
631
+ opts.onError?.(err, { attempt, retriesLeft, delayMs });
632
+ await opts.sleep(delayMs, opts.signal);
633
+ if (opts.signal?.aborted)
634
+ throw err;
635
+ }
636
+ }
637
+ }
520
638
  async function defaultSleep(ms, signal) {
521
639
  if (signal?.aborted)
522
640
  return;
@@ -543,7 +661,7 @@ async function consumeStreamsEvents(opts) {
543
661
  let emptyPolls = 0;
544
662
  let height = null;
545
663
  while (pages < maxPages && emptyPolls < maxEmptyPolls && !opts.signal?.aborted) {
546
- const envelope = await opts.fetchEvents({
664
+ const envelope = await fetchPageWithRetry(() => opts.fetchEvents({
547
665
  cursor,
548
666
  limit: opts.batchSize,
549
667
  types: opts.types,
@@ -552,6 +670,12 @@ async function consumeStreamsEvents(opts) {
552
670
  sender: opts.sender,
553
671
  recipient: opts.recipient,
554
672
  assetIdentifier: opts.assetIdentifier
673
+ }), {
674
+ retryCount: opts.retryCount,
675
+ retryDelay: opts.retryDelay,
676
+ onError: opts.onError,
677
+ sleep,
678
+ signal: opts.signal
555
679
  });
556
680
  pages++;
557
681
  if (!finalizedOnly && opts.onReorg) {
@@ -682,10 +806,16 @@ async function consumeIndexFeed(opts) {
682
806
  let emptyPolls = 0;
683
807
  let height = null;
684
808
  while (pages < maxPages && emptyPolls < maxEmptyPolls && !opts.signal?.aborted) {
685
- const envelope = await opts.fetchPage({
809
+ const envelope = await fetchPageWithRetry(() => opts.fetchPage({
686
810
  cursor,
687
811
  fromHeight: cursor === null ? opts.fromHeight : undefined,
688
812
  limit: batchSize
813
+ }), {
814
+ retryCount: opts.retryCount,
815
+ retryDelay: opts.retryDelay,
816
+ onError: opts.onError,
817
+ sleep,
818
+ signal: opts.signal
689
819
  });
690
820
  pages++;
691
821
  if (!finalizedOnly && opts.onReorg) {
@@ -1574,7 +1704,10 @@ function createStreamsClient(options) {
1574
1704
  emptyBackoffMs: params.emptyBackoffMs,
1575
1705
  maxPages: params.maxPages,
1576
1706
  maxEmptyPolls: params.maxEmptyPolls,
1577
- signal: params.signal
1707
+ signal: params.signal,
1708
+ retryCount: params.retryCount,
1709
+ retryDelay: params.retryDelay,
1710
+ onError: params.onError
1578
1711
  });
1579
1712
  },
1580
1713
  stream(params = {}) {
@@ -1809,5 +1942,5 @@ export {
1809
1942
  Subgraphs
1810
1943
  };
1811
1944
 
1812
- //# debugId=13B6D7F76CAEAFE964756E2164756E21
1945
+ //# debugId=A36A74FD0514129964756E2164756E21
1813
1946
  //# sourceMappingURL=index.js.map