@rhinestone/service-sdk 0.2.15 → 0.2.17

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Shared TypeScript utilities for Rhinestone backend services — the pieces every service
4
4
  needs and none of them should reimplement: OpenTelemetry setup and helpers, chain-aware gas
5
- fee estimation, and BigInt-safe JSON serialization.
5
+ fee estimation, graceful-shutdown plumbing, and BigInt-safe JSON serialization.
6
6
 
7
7
  Published to npm and consumed by `orchestrator`, `relayer`, `deposit-service-processor`,
8
8
  `route-stats`, `back-office` (api), and `1auth` (passkey, recovery-signer).
@@ -25,6 +25,7 @@ bun add @rhinestone/service-sdk
25
25
  |---|---|
26
26
  | `@rhinestone/service-sdk` | Re-exports `./serialization` only |
27
27
  | `@rhinestone/service-sdk/chains` | Chain-aware gas fee estimation |
28
+ | `@rhinestone/service-sdk/lifecycle` | Termination signals, crashes and explicit aborts as one channel |
28
29
  | `@rhinestone/service-sdk/opentelemetry` | SDK setup, tracing/metrics helpers, propagation, integrations |
29
30
  | `@rhinestone/service-sdk/serialization` | BigInt-safe JSON serialization |
30
31
 
@@ -105,6 +106,50 @@ from the value alone.
105
106
 
106
107
  ---
107
108
 
109
+ ## Lifecycle
110
+
111
+ ```ts
112
+ import {
113
+ installTerminationHandlers,
114
+ terminationExitCode,
115
+ describeReason,
116
+ } from '@rhinestone/service-sdk/lifecycle'
117
+
118
+ const termination = installTerminationHandlers()
119
+
120
+ const reason = await Promise.race([serve(termination), termination.waitForTermination()])
121
+
122
+ logger.info(`shutting down: ${describeReason(reason)}`)
123
+ await teardownInOrder()
124
+ process.exit(terminationExitCode(reason))
125
+ ```
126
+
127
+ Folds every way a process can be asked to stop into one channel: `SIGTERM` / `SIGINT`, an
128
+ uncaught exception, an unhandled rejection, and an explicit `terminate(reason)` from code
129
+ that found a broken invariant. `waitForTermination()` resolves once with a discriminated
130
+ `TerminationReason`, so the caller can log it and pick an exit code instead of every call
131
+ site inventing its own.
132
+
133
+ **The first reason wins; later ones are dropped.** That is the point rather than a detail:
134
+ sockets closing during a shutdown routinely orphan promises, and those rejections must not
135
+ restart the shutdown or flip a clean stop to a failure.
136
+
137
+ `isTerminating()` is the same state as a synchronous boolean, for the callers that can't
138
+ await — readiness probes returning 503 the instant a signal lands, admission gates refusing
139
+ new work while in-flight work drains.
140
+
141
+ Two things to know:
142
+
143
+ - **Registering an `uncaughtException` / `unhandledRejection` listener stops Node exiting on
144
+ either.** The caller *must* exit once teardown is done, or a crashed process lingers.
145
+ - Nothing is registered at import time, and each install is independent. The install call is
146
+ the only thing that touches `process`, and `dispose()` removes exactly what it added, so
147
+ tests get a fresh one and own its disposal.
148
+
149
+ This module logs nothing itself; reporting is the owner's job, once the promise resolves.
150
+
151
+ ---
152
+
108
153
  ## OpenTelemetry
109
154
 
110
155
  ### Setup
package/dist/chains.d.ts CHANGED
@@ -12,6 +12,54 @@ export type EstimateGasFeesOptions = {
12
12
  * Omit this option to retain the chain-aware EIP-1559 estimation strategy.
13
13
  */
14
14
  mode?: 'legacy';
15
+ /**
16
+ * Samples the per-block tip at this percentile instead of the chain's
17
+ * configured `tipPercentile`/`congestedTipPercentile`, for a caller that must
18
+ * deliberately price against something other than our own calibration.
19
+ *
20
+ * The one caller today is the Safe GTF gas-price feed (RHI-4992). Safe
21
+ * benchmarks that feed against Gelato's `relayer_getFeeData` and asked us to
22
+ * match its mainnet buffer, having previously used Gelato for the same
23
+ * self-pay flow. Gelato fits to `base + tip`, 20-block window, NO multiplier —
24
+ * the same shape as our windowed path, so tracking them is a percentile and
25
+ * nothing else. Which percentile, measured on 314 paired live samples over 95
26
+ * minutes (2026-08-10, mainnet):
27
+ *
28
+ * pctl model/gelato p10 median p90 mean|err| gwei
29
+ * p50 0.479 0.669 0.889 0.15124
30
+ * p60 0.624 0.879 1.085 0.10221
31
+ * p65 0.725 1.045 1.264 0.08906 <- parity
32
+ * p70 0.985 1.411 1.793 0.16159
33
+ *
34
+ * MEASURE IT OVER HOURS, NOT MINUTES. Two short fits of this same quantity, on
35
+ * the same day, gave p60 at 0.982 (25 samples / 100s) and 0.785 (40 samples /
36
+ * 4 min) — and the 314-sample answer is 0.879. Even the CUMULATIVE median
37
+ * inside the long run wandered 1.006 -> 0.844 -> 0.929 -> 0.881 as it filled
38
+ * up. A 5-percentile step moves the median 19-35%, so a short window can
39
+ * "prove" almost any percentile. An older fit also needed a x1.17 multiplier;
40
+ * that is gone. Re-fit against live data before trusting any number here.
41
+ *
42
+ * DELIBERATELY NOT the chain default. Our own mainnet calibration is p25 plus
43
+ * a realized-clearing floor. p65 sits on the steep face of mainnet's tip cliff
44
+ * and prices at ~1.5x what a transaction actually costs (Gelato itself: 1.494x
45
+ * median, p10 1.125 / p90 2.089 — they swing because they stand on the same
46
+ * cliff). This override exists so ONE surface can opt into a competitor's
47
+ * pricing without moving intent pricing, which shares this function.
48
+ *
49
+ * The cliff MOVES, which is why it is a cliff and not a constant: a 120-block
50
+ * replay read a realized p50 tip of 0.0094 gwei (39,312 txs) in the morning
51
+ * and 0.148 gwei (34,314 txs) a few hours later — 16x — with p75 going
52
+ * 0.25 -> 1.00 gwei over the same span.
53
+ *
54
+ * Applies to the windowed feeHistory path only (`HISTORICAL_FEE_CHAINS`).
55
+ * Ignored on the viem path, where the tip does not come from a percentile we
56
+ * choose. Mainnet — the only chain that needs it — is on the windowed path.
57
+ *
58
+ * Requests a single reward column and pins the read to it, so congestion
59
+ * escalation cannot silently move the sampled percentile somewhere else — the
60
+ * caller asked to track one number.
61
+ */
62
+ tipPercentileOverride?: number;
15
63
  };
16
64
  /**
17
65
  * {@link estimateGasFees}' result: viem's EIP-1559 fee values plus the next
@@ -1 +1 @@
1
- {"version":3,"file":"chains.d.ts","sourceRoot":"","sources":["../src/chains.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,YAAY,EAAE,MAAM,MAAM,CAAA;AAmB3E,oDAAoD;AAMpD,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,YAAY,EACZ,oBAAoB,GAAG,eAAe,CACvC,GACC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAA;AAEtC,2CAA2C;AAC3C,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG;IAChD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B,CAAA;AAyWD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,gBAAgB,CAAC,CAiD3B"}
1
+ {"version":3,"file":"chains.d.ts","sourceRoot":"","sources":["../src/chains.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,YAAY,EAAE,MAAM,MAAM,CAAA;AAmB3E,oDAAoD;AAMpD,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,YAAY,EACZ,oBAAoB,GAAG,eAAe,CACvC,GACC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAA;AAEtC,2CAA2C;AAC3C,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8CG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG;IAChD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B,CAAA;AAyWD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,gBAAgB,CAAC,CAqE3B"}
package/dist/chains.js CHANGED
@@ -335,6 +335,16 @@ async function nextBlockBaseFee(client, chainId) {
335
335
  * type.
336
336
  */
337
337
  async function estimateGasFees(client, chainId, options) {
338
+ // Validated HERE, at the public entry, rather than where it is used. A bad value
339
+ // otherwise surfaces as an opaque provider error from eth_feeHistory — and only on the
340
+ // chains that take the windowed path, so the same argument would throw on mainnet and
341
+ // be silently ignored on Base. This is a locally decidable input; fail locally and say
342
+ // what was wrong. (Copilot, PR #24.)
343
+ const override = options?.tipPercentileOverride;
344
+ if (override !== undefined &&
345
+ (!Number.isFinite(override) || override < 0 || override > 100)) {
346
+ throw new Error(`tipPercentileOverride must be a finite number in [0, 100], got ${override}`);
347
+ }
338
348
  if (options?.mode === 'legacy') {
339
349
  const legacyClient = client;
340
350
  const { gasPrice } = await legacyClient.estimateFeesPerGas({
@@ -352,7 +362,7 @@ async function estimateGasFees(client, chainId, options) {
352
362
  }
353
363
  if (HISTORICAL_FEE_CHAINS.has(chainId)) {
354
364
  const config = { ...DEFAULT_FEE_CONFIG, ...FEE_CONFIG_OVERRIDES[chainId] };
355
- return estimateFeeHistoryGasFees(client, chainId, config);
365
+ return estimateFeeHistoryGasFees(client, chainId, config, options?.tipPercentileOverride);
356
366
  }
357
367
  const tipFloorWei = TIP_FLOOR_WEI[chainId];
358
368
  if (tipFloorWei !== undefined) {
@@ -558,17 +568,27 @@ async function viemFallbackCap(client, config, priorityFee) {
558
568
  }
559
569
  return cap > priorityFee ? cap : priorityFee;
560
570
  }
561
- async function estimateFeeHistoryGasFees(client, chainId, config) {
571
+ async function estimateFeeHistoryGasFees(client, chainId, config, tipPercentileOverride) {
572
+ // An override asks for ONE percentile, so ask the RPC for one column. It
573
+ // cannot be requested as [p, p]: geth rejects a reward-percentile list that is
574
+ // not strictly increasing — "invalid reward percentile: #0:60.000000 >=
575
+ // #1:60.000000", verified live 2026-08-10 against publicnode and 1rpc — so the
576
+ // duplicate form is an RPC error, not a pricing choice.
577
+ const overridden = tipPercentileOverride !== undefined;
562
578
  const feeHistory = await client.getFeeHistory({
563
579
  blockCount: config.historyBlocks,
564
- rewardPercentiles: [config.tipPercentile, config.congestedTipPercentile],
580
+ rewardPercentiles: overridden
581
+ ? [tipPercentileOverride]
582
+ : [config.tipPercentile, config.congestedTipPercentile],
565
583
  });
566
584
  const ratios = feeHistory.gasUsedRatio ?? [];
567
585
  const meanRatio = ratios.length > 0
568
586
  ? ratios.reduce((sum, r) => sum + r, 0) / ratios.length
569
587
  : 0;
570
- // Under load, read the per-block tip from the higher percentile column.
571
- const column = meanRatio > config.congestionRatio ? 1 : 0;
588
+ // Under load, read the per-block tip from the higher percentile column. With
589
+ // an override there is only column 0, and escalating is not wanted anyway —
590
+ // the caller asked to track one percentile, so congestion must not move it.
591
+ const column = !overridden && meanRatio > config.congestionRatio ? 1 : 0;
572
592
  const perBlockTips = (feeHistory.reward ?? []).map((r) => r?.[column] ?? 0n);
573
593
  const windowedTip = percentileBigInt(perBlockTips, config.windowTipPercentile);
574
594
  const priorityFee = windowedTip > config.tipFloorWei ? windowedTip : config.tipFloorWei;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Process termination as one observable event.
3
+ *
4
+ * A shutdown needs two different things, and a bare promise only gives the
5
+ * first: an *ordered* teardown (release this, then close that), and an
6
+ * *immediate* answer to "should I still take on new work?" for readiness probes
7
+ * and admission gates. Handing those callers a promise means each of them keeps
8
+ * its own boolean in sync with it, which is the bug this shape avoids.
9
+ *
10
+ * Every way a process can be asked to stop lands in the same channel:
11
+ *
12
+ * - an external signal — SIGTERM from an orchestrator, SIGINT from a terminal
13
+ * - an uncaught exception or an unhandled rejection
14
+ * - an explicit `terminate()` from code that found a broken invariant
15
+ *
16
+ * The first reason wins; anything after it is dropped. That is what keeps a
17
+ * crash *during* shutdown from restarting the shutdown or changing its exit
18
+ * code — orphaned promises rejecting as sockets close is normal, and must not
19
+ * turn a clean stop into a failed one.
20
+ *
21
+ * Nothing is registered at import time. `installTerminationHandlers` is the only
22
+ * thing that touches `process`, and `dispose()` undoes exactly what it added.
23
+ */
24
+ /** Why the process was asked to stop. */
25
+ export type TerminationReason = {
26
+ kind: 'signal';
27
+ signal: NodeJS.Signals;
28
+ } | {
29
+ kind: 'uncaughtException';
30
+ error: Error;
31
+ } | {
32
+ kind: 'unhandledRejection';
33
+ reason: unknown;
34
+ } | {
35
+ kind: 'explicit';
36
+ reason: string | Error | object;
37
+ };
38
+ export interface Termination {
39
+ /**
40
+ * Resolves once, with whatever asked the process to stop. Never rejects, and
41
+ * safe to await from several places; awaiting after the fact returns the
42
+ * reason that already settled. Reporting the reason is the owner's job — this
43
+ * module logs nothing.
44
+ */
45
+ waitForTermination(): Promise<TerminationReason>;
46
+ /**
47
+ * True from the instant a reason arrives. For readiness gates and admission
48
+ * checks that must answer synchronously, long before teardown finishes.
49
+ */
50
+ isTerminating(): boolean;
51
+ /**
52
+ * Ask the process to stop: an invariant broke and it must not keep running.
53
+ * The reason is carried through to the awaiting caller — an `Error` where
54
+ * there is one, or a string / context object where the caller has facts
55
+ * rather than a throw. First caller wins.
56
+ */
57
+ terminate(reason: string | Error | object): void;
58
+ /** Remove the process listeners this install added. Idempotent. */
59
+ dispose(): void;
60
+ }
61
+ /**
62
+ * Register the process listeners and return the channel they feed.
63
+ *
64
+ * Each call is independent: it installs its own listeners and owns its own
65
+ * reason, so two installs in one process both fire. That is deliberate — tests
66
+ * want a fresh one — and it means the caller is responsible for `dispose()`.
67
+ *
68
+ * Registering an `uncaughtException` / `unhandledRejection` listener stops Node
69
+ * from exiting on either, so **the caller must exit itself** once teardown is
70
+ * done. Use `terminationExitCode` for the code.
71
+ */
72
+ export declare function installTerminationHandlers(): Termination;
73
+ /**
74
+ * The exit code to leave with: 0 for a signal, 1 for everything else.
75
+ *
76
+ * A handled SIGTERM means the process did what it was asked, so a clean 0 —
77
+ * exiting non-zero there makes an orderly rollout look like a failed one.
78
+ */
79
+ export declare function terminationExitCode(reason: TerminationReason): number;
80
+ /** One-line description of a reason, for the owner's shutdown log. */
81
+ export declare function describeReason(reason: TerminationReason): string;
82
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,yCAAyC;AACzC,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;CAAE,CAAA;AAEzD,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,kBAAkB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAChD;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAA;IACxB;;;;;OAKG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,CAAA;IAChD,mEAAmE;IACnE,OAAO,IAAI,IAAI,CAAA;CAChB;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,IAAI,WAAW,CA2CxD;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAErE;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAWhE"}
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installTerminationHandlers = installTerminationHandlers;
4
+ exports.terminationExitCode = terminationExitCode;
5
+ exports.describeReason = describeReason;
6
+ const serialization_1 = require("./serialization");
7
+ /** SIGTERM from orchestrators, SIGINT from terminals. */
8
+ const SIGNALS = ['SIGTERM', 'SIGINT'];
9
+ /**
10
+ * Register the process listeners and return the channel they feed.
11
+ *
12
+ * Each call is independent: it installs its own listeners and owns its own
13
+ * reason, so two installs in one process both fire. That is deliberate — tests
14
+ * want a fresh one — and it means the caller is responsible for `dispose()`.
15
+ *
16
+ * Registering an `uncaughtException` / `unhandledRejection` listener stops Node
17
+ * from exiting on either, so **the caller must exit itself** once teardown is
18
+ * done. Use `terminationExitCode` for the code.
19
+ */
20
+ function installTerminationHandlers() {
21
+ let settledReason;
22
+ let resolve;
23
+ const reasonPromise = new Promise((r) => {
24
+ resolve = r;
25
+ });
26
+ const accept = (next) => {
27
+ if (settledReason !== undefined)
28
+ return;
29
+ settledReason = next;
30
+ resolve(next);
31
+ };
32
+ const onSignal = (signal) => {
33
+ accept({ kind: 'signal', signal });
34
+ };
35
+ const onUncaughtException = (error) => {
36
+ accept({ kind: 'uncaughtException', error });
37
+ };
38
+ const onUnhandledRejection = (reason) => {
39
+ accept({ kind: 'unhandledRejection', reason });
40
+ };
41
+ for (const signal of SIGNALS) {
42
+ process.on(signal, onSignal);
43
+ }
44
+ process.on('uncaughtException', onUncaughtException);
45
+ process.on('unhandledRejection', onUnhandledRejection);
46
+ return {
47
+ waitForTermination: () => reasonPromise,
48
+ isTerminating: () => settledReason !== undefined,
49
+ terminate: (reason) => {
50
+ accept({ kind: 'explicit', reason });
51
+ },
52
+ dispose: () => {
53
+ for (const signal of SIGNALS) {
54
+ process.off(signal, onSignal);
55
+ }
56
+ process.off('uncaughtException', onUncaughtException);
57
+ process.off('unhandledRejection', onUnhandledRejection);
58
+ },
59
+ };
60
+ }
61
+ /**
62
+ * The exit code to leave with: 0 for a signal, 1 for everything else.
63
+ *
64
+ * A handled SIGTERM means the process did what it was asked, so a clean 0 —
65
+ * exiting non-zero there makes an orderly rollout look like a failed one.
66
+ */
67
+ function terminationExitCode(reason) {
68
+ return reason.kind === 'signal' ? 0 : 1;
69
+ }
70
+ /** One-line description of a reason, for the owner's shutdown log. */
71
+ function describeReason(reason) {
72
+ switch (reason.kind) {
73
+ case 'signal':
74
+ return `signal ${reason.signal}`;
75
+ case 'uncaughtException':
76
+ return `uncaught exception: ${formatValue(reason.error)}`;
77
+ case 'unhandledRejection':
78
+ return `unhandled rejection: ${formatValue(reason.reason)}`;
79
+ case 'explicit':
80
+ return `explicit termination: ${formatValue(reason.reason)}`;
81
+ }
82
+ }
83
+ function formatValue(value) {
84
+ if (value instanceof Error)
85
+ return value.stack ?? value.message;
86
+ if (typeof value === 'string')
87
+ return value;
88
+ try {
89
+ // Guards bigints and circular references, both plausible in a context
90
+ // object assembled at the point an invariant broke.
91
+ return (0, serialization_1.stringifyBigIntFields)(value);
92
+ }
93
+ catch {
94
+ return String(value);
95
+ }
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinestone/service-sdk",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "Shared SDK for Rhinestone services",
5
5
  "author": {
6
6
  "name": "Rhinestone",
@@ -18,6 +18,7 @@
18
18
  "exports": {
19
19
  ".": "./dist/index.js",
20
20
  "./chains": "./dist/chains.js",
21
+ "./lifecycle": "./dist/lifecycle.js",
21
22
  "./opentelemetry": "./dist/opentelemetry/index.js",
22
23
  "./serialization": "./dist/serialization.js"
23
24
  },
@@ -26,6 +27,9 @@
26
27
  "chains": [
27
28
  "dist/chains.d.ts"
28
29
  ],
30
+ "lifecycle": [
31
+ "dist/lifecycle.d.ts"
32
+ ],
29
33
  "opentelemetry": [
30
34
  "dist/opentelemetry/index.d.ts"
31
35
  ],