@waterx/sdk 4.0.0 → 4.0.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.
Files changed (54) hide show
  1. package/README.md +34 -34
  2. package/dist/cjs/src/account/config.d.ts +0 -16
  3. package/dist/cjs/src/oracle/aggregate.d.ts +19 -21
  4. package/dist/cjs/src/oracle/aggregate.js +57 -69
  5. package/dist/cjs/src/oracle/config.d.ts +32 -52
  6. package/dist/cjs/src/oracle/config.js +1 -35
  7. package/dist/cjs/src/oracle/host.d.ts +1 -1
  8. package/dist/cjs/src/oracle/index.d.ts +2 -2
  9. package/dist/cjs/src/oracle/index.js +20 -6
  10. package/dist/cjs/src/oracle/pyth.d.ts +68 -6
  11. package/dist/cjs/src/oracle/pyth.js +338 -22
  12. package/dist/cjs/src/oracle/rule-registry.d.ts +10 -6
  13. package/dist/cjs/src/oracle/rule-registry.js +10 -6
  14. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +17 -2
  15. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +6 -6
  16. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +25 -22
  17. package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -0
  18. package/dist/cjs/src/oracle/update-fetch.d.ts +32 -2
  19. package/dist/cjs/src/oracle/update-fetch.js +60 -3
  20. package/dist/cjs/src/perp/client.d.ts +33 -19
  21. package/dist/cjs/src/perp/client.js +16 -10
  22. package/dist/cjs/src/perp/config.d.ts +4 -6
  23. package/dist/cjs/src/perp/config.js +8 -12
  24. package/dist/cjs/src/perp/index.d.ts +2 -2
  25. package/dist/cjs/src/perp/index.js +3 -4
  26. package/dist/cjs/src/unified-client.d.ts +19 -11
  27. package/dist/cjs/src/unified-client.js +2 -1
  28. package/dist/src/account/config.d.ts +0 -16
  29. package/dist/src/oracle/aggregate.d.ts +19 -21
  30. package/dist/src/oracle/aggregate.js +57 -69
  31. package/dist/src/oracle/config.d.ts +32 -52
  32. package/dist/src/oracle/config.js +0 -34
  33. package/dist/src/oracle/host.d.ts +1 -1
  34. package/dist/src/oracle/index.d.ts +2 -2
  35. package/dist/src/oracle/index.js +15 -7
  36. package/dist/src/oracle/pyth.d.ts +68 -6
  37. package/dist/src/oracle/pyth.js +334 -22
  38. package/dist/src/oracle/rule-registry.d.ts +10 -6
  39. package/dist/src/oracle/rule-registry.js +10 -6
  40. package/dist/src/oracle/rules/pyth-core-rule.js +18 -3
  41. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +6 -6
  42. package/dist/src/oracle/rules/pyth-lazer-rule.js +26 -23
  43. package/dist/src/oracle/rules/pyth-rule.js +5 -0
  44. package/dist/src/oracle/update-fetch.d.ts +32 -2
  45. package/dist/src/oracle/update-fetch.js +57 -3
  46. package/dist/src/perp/client.d.ts +33 -19
  47. package/dist/src/perp/client.js +17 -11
  48. package/dist/src/perp/config.d.ts +4 -6
  49. package/dist/src/perp/config.js +9 -12
  50. package/dist/src/perp/index.d.ts +2 -2
  51. package/dist/src/perp/index.js +1 -1
  52. package/dist/src/unified-client.d.ts +19 -11
  53. package/dist/src/unified-client.js +2 -1
  54. package/package.json +1 -1
@@ -16,7 +16,11 @@
16
16
  * `Authorization` header at all). This is the Phase-0 invariant of the
17
17
  * Pyth Pro migration: existing keyless deployments see no behavior change.
18
18
  * - Retries on network errors, HTTP 429, and HTTP 5xx, with exponential
19
- * backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). Other
19
+ * backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). A 429
20
+ * carrying a numeric `Retry-After` header uses the SERVER'S delay instead,
21
+ * when it fits under the same cap — a longer ask degrades to normal
22
+ * backoff rather than stalling a money-path build for tens of seconds.
23
+ * Other
20
24
  * 4xx statuses (401/400/403/404/…) are NOT retried — auth/bad-request
21
25
  * failures are deterministic, so that `Response` (`ok: false`) is handed
22
26
  * back on the first attempt for the caller to format its own
@@ -46,7 +50,7 @@
46
50
  * - Retry worst case: with the defaults (15s timeout × 3 attempts + ~0.75s of
47
51
  * backoff between them) a FULL outage takes up to ~46s to surface as a
48
52
  * `FetchPolicyError`, vs ~15s pre-3.2.0's single bare-`fetch` attempt.
49
- * Tunable per client via `config.pyth.fetch.{timeoutMs,retries}`.
53
+ * Tunable per client via the `pythFetch` create option (`{timeoutMs,retries}`).
50
54
  */
51
55
  export interface FetchPolicy {
52
56
  /** Per-attempt timeout (ms). Default 15_000. */
@@ -75,6 +79,32 @@ export declare class FetchPolicyError extends Error {
75
79
  attempts: number;
76
80
  });
77
81
  }
82
+ /**
83
+ * Rethrow a `catch`-ed {@link fetchWithPolicy} failure. When it is a
84
+ * status-carrying `FetchPolicyError` — a retryable status (429/5xx) that never
85
+ * recovered — throw a new Error `${describe(err)} (retries exhausted after N
86
+ * attempts)` with the original as `cause`; otherwise (a network-level
87
+ * exhaustion with no status, or any non-`FetchPolicyError`) rethrow it verbatim,
88
+ * since there is no domain reframing to add. `describe` builds the
89
+ * status-bearing prefix so each caller keeps its own message shape (the e2e
90
+ * transient detector keys off those prefixes) while the guard, the `retries
91
+ * exhausted` suffix, and the `cause` wrapping live in one place. `: never` so a
92
+ * caller's `catch` block is understood not to fall through.
93
+ */
94
+ export declare function rethrowExhaustedFetch(err: unknown, describe: (err: FetchPolicyError) => string): never;
95
+ /**
96
+ * Join an API `path` onto an `endpoint` PRESERVING the endpoint's own base
97
+ * path. `new URL(path, endpoint)` is the footgun this replaces: a
98
+ * leading-slash path is *absolute* and silently discards the endpoint's path
99
+ * — harmless for a bare-origin endpoint (`https://hermes.pyth.network`) but
100
+ * it dropped the `/hermes` prefix of the Pyth Pro compat endpoint and 404'd
101
+ * every feed (see `fetchPriceFeedsUpdateData`). Every oracle fetch that
102
+ * targets `<endpoint><fixed path>` must build its URL here.
103
+ */
104
+ /** One canonical trailing-slash trim — `joinEndpointPath` (URL building) and
105
+ * `pyth.ts`'s `memoKey` (endpoint identity) must never drift apart on it. */
106
+ export declare function trimTrailingSlashes(endpoint: string): string;
107
+ export declare function joinEndpointPath(endpoint: string, path: string): URL;
78
108
  /**
79
109
  * `fetch` with per-attempt timeout, bounded retry + backoff, and optional
80
110
  * Bearer auth. See the module header for the full policy. Both `init.signal`
@@ -16,7 +16,11 @@
16
16
  * `Authorization` header at all). This is the Phase-0 invariant of the
17
17
  * Pyth Pro migration: existing keyless deployments see no behavior change.
18
18
  * - Retries on network errors, HTTP 429, and HTTP 5xx, with exponential
19
- * backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). Other
19
+ * backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). A 429
20
+ * carrying a numeric `Retry-After` header uses the SERVER'S delay instead,
21
+ * when it fits under the same cap — a longer ask degrades to normal
22
+ * backoff rather than stalling a money-path build for tens of seconds.
23
+ * Other
20
24
  * 4xx statuses (401/400/403/404/…) are NOT retried — auth/bad-request
21
25
  * failures are deterministic, so that `Response` (`ok: false`) is handed
22
26
  * back on the first attempt for the caller to format its own
@@ -46,7 +50,7 @@
46
50
  * - Retry worst case: with the defaults (15s timeout × 3 attempts + ~0.75s of
47
51
  * backoff between them) a FULL outage takes up to ~46s to surface as a
48
52
  * `FetchPolicyError`, vs ~15s pre-3.2.0's single bare-`fetch` attempt.
49
- * Tunable per client via `config.pyth.fetch.{timeoutMs,retries}`.
53
+ * Tunable per client via the `pythFetch` create option (`{timeoutMs,retries}`).
50
54
  */
51
55
  const DEFAULT_TIMEOUT_MS = 15_000;
52
56
  const DEFAULT_RETRIES = 2;
@@ -70,12 +74,60 @@ export class FetchPolicyError extends Error {
70
74
  this.attempts = opts.attempts;
71
75
  }
72
76
  }
77
+ /**
78
+ * Rethrow a `catch`-ed {@link fetchWithPolicy} failure. When it is a
79
+ * status-carrying `FetchPolicyError` — a retryable status (429/5xx) that never
80
+ * recovered — throw a new Error `${describe(err)} (retries exhausted after N
81
+ * attempts)` with the original as `cause`; otherwise (a network-level
82
+ * exhaustion with no status, or any non-`FetchPolicyError`) rethrow it verbatim,
83
+ * since there is no domain reframing to add. `describe` builds the
84
+ * status-bearing prefix so each caller keeps its own message shape (the e2e
85
+ * transient detector keys off those prefixes) while the guard, the `retries
86
+ * exhausted` suffix, and the `cause` wrapping live in one place. `: never` so a
87
+ * caller's `catch` block is understood not to fall through.
88
+ */
89
+ export function rethrowExhaustedFetch(err, describe) {
90
+ if (err instanceof FetchPolicyError && err.status !== undefined) {
91
+ throw new Error(`${describe(err)} (retries exhausted after ${err.attempts} attempts)`, {
92
+ cause: err,
93
+ });
94
+ }
95
+ throw err;
96
+ }
97
+ /**
98
+ * Join an API `path` onto an `endpoint` PRESERVING the endpoint's own base
99
+ * path. `new URL(path, endpoint)` is the footgun this replaces: a
100
+ * leading-slash path is *absolute* and silently discards the endpoint's path
101
+ * — harmless for a bare-origin endpoint (`https://hermes.pyth.network`) but
102
+ * it dropped the `/hermes` prefix of the Pyth Pro compat endpoint and 404'd
103
+ * every feed (see `fetchPriceFeedsUpdateData`). Every oracle fetch that
104
+ * targets `<endpoint><fixed path>` must build its URL here.
105
+ */
106
+ /** One canonical trailing-slash trim — `joinEndpointPath` (URL building) and
107
+ * `pyth.ts`'s `memoKey` (endpoint identity) must never drift apart on it. */
108
+ export function trimTrailingSlashes(endpoint) {
109
+ return endpoint.replace(/\/+$/, "");
110
+ }
111
+ export function joinEndpointPath(endpoint, path) {
112
+ return new URL(`${trimTrailingSlashes(endpoint)}/${path.replace(/^\/+/, "")}`);
113
+ }
73
114
  function isRetryableStatus(status) {
74
115
  return status === 429 || status >= 500;
75
116
  }
76
117
  function backoffMs(retryDelayMs, attempt) {
77
118
  return Math.min(retryDelayMs * 2 ** attempt, MAX_BACKOFF_MS);
78
119
  }
120
+ /**
121
+ * The server's own `Retry-After` (numeric-seconds form only), when present
122
+ * and within `MAX_BACKOFF_MS` — `undefined` otherwise (absent, HTTP-date
123
+ * form, zero/garbage, or an ask too long to honor inside a build path).
124
+ */
125
+ function retryAfterMs(response) {
126
+ // Optional-chained: minimal test doubles (and some fetch shims) carry no
127
+ // `headers` — a missing header must read as "no hint", never throw.
128
+ const ms = Number(response.headers?.get?.("retry-after")) * 1_000;
129
+ return ms > 0 && ms <= MAX_BACKOFF_MS ? ms : undefined;
130
+ }
79
131
  /** `host + pathname` only — never the query string (feed ids are noise, not diagnostic). */
80
132
  function describeTarget(url) {
81
133
  const parsed = new URL(url);
@@ -182,11 +234,13 @@ export async function fetchWithPolicy(url, init = {}, policy = {}, externalSigna
182
234
  let status;
183
235
  let bodySnippet;
184
236
  let cause;
237
+ let serverRetryDelay;
185
238
  try {
186
239
  const response = await doFetch(url, { ...init, headers, signal });
187
240
  if (response.ok || !isRetryableStatus(response.status))
188
241
  return response;
189
242
  status = response.status;
243
+ serverRetryDelay = retryAfterMs(response);
190
244
  if (attempt === retries) {
191
245
  bodySnippet = await readBodySnippet(response);
192
246
  }
@@ -213,7 +267,7 @@ export async function fetchWithPolicy(url, init = {}, policy = {}, externalSigna
213
267
  const detail = statusDetail ?? causeMessage(cause);
214
268
  throw new FetchPolicyError(`fetchWithPolicy: ${describeTarget(url)} failed after ${attempt + 1} attempt(s) — ${detail}`, { status, bodySnippet, cause, attempts: attempt + 1 });
215
269
  }
216
- await sleep(backoffMs(retryDelayMs, attempt), combinedExternalSignal);
270
+ await sleep(serverRetryDelay ?? backoffMs(retryDelayMs, attempt), combinedExternalSignal);
217
271
  }
218
272
  // Unreachable: the loop above always returns or throws on its final
219
273
  // (attempt === retries) iteration — this satisfies the compiler only.
@@ -12,44 +12,58 @@
12
12
  */
13
13
  import { BaseLineClient } from "../base-client.ts";
14
14
  import type { OracleSource } from "../oracle/price-update-rule.ts";
15
- import { type LoadConfigOptions, type PythGeneration, type PythInfraConfig, type WaterXConfig, type WormholeInfraConfig } from "./config.ts";
15
+ import { type LoadConfigOptions, type PythFetchPolicy, type PythInfraConfig, type WaterXConfig, type WormholeInfraConfig } from "./config.ts";
16
16
  import type { Network } from "./constants.ts";
17
17
  export interface CreateClientOptions extends LoadConfigOptions {
18
18
  grpcUrl?: string;
19
19
  /**
20
- * Selects which `PriceUpdateRule` `refreshOraclePrices` uses for the
21
- * on-chain price-update leg (see `OracleHost.oracleSource`). Default:
22
- * `'pyth_rule'`. The SDK never reads `process.env` — pass this from your
23
- * own env var (e.g. `ORACLE_SOURCE`).
20
+ * Which oracle price-update source drives `refreshOraclePrices`. Each source
21
+ * is self-contained (own infra + config) with NO cross-source fallback:
22
+ *
23
+ * - `'pyth_rule'` (default) — Pyth Core `pyth_rule` updates (Hermes VAA +
24
+ * per-feed update fees), Core state + keyless Core Hermes.
25
+ * - `'pyth_lazer_rule'` — Pyth Lazer signed updates (ONE `leEcdsa` verify
26
+ * per PTB, no per-feed fees); needs `packages.pyth_lazer_rule` with feeds
27
+ * and a `pythApiKey` (Lazer is auth-first).
28
+ *
29
+ * A source-neutral name on purpose — a future source need not be Pyth.
30
+ * Selecting a source whose feed for a requested ticker is absent is NOT an
31
+ * error at client creation: it fails at tx-build time for exactly those
32
+ * tickers (see `refreshOraclePrices`). The Pyth Core infra is fixed per
33
+ * network by `PYTH_DEFAULTS` and is not deployment-overridable.
24
34
  */
25
35
  oracleSource?: OracleSource;
26
36
  /**
27
- * Selects which Pyth Core contract generation feeds `client.pyth` when the
28
- * config JSON has no explicit `pyth` override: `'core'` (default,
29
- * `PYTH_DEFAULTS`) or `'pro'` (`PYTH_PRO_DEFAULTS` the post-2026-08-18
30
- * Pro-compatible contracts + Hermes-compatible endpoint; pair with
31
- * `pyth.api_key`). Orthogonal to `oracleSource`. An explicit `config.pyth`
32
- * always wins wholesale (see `PythGeneration`).
37
+ * Pyth Lazer access token (`Authorization: Bearer …`). Required under
38
+ * `oracleSource: 'pyth_lazer_rule'` (Lazer is auth-first) and unused by
39
+ * `'pyth_rule'` (keyless Core Hermes). This is a SECRET and never belongs in
40
+ * the canonical `waterx-config` JSON pass it at client init from your own
41
+ * env var (e.g. `PYTH_API_KEY`); the SDK never reads `process.env`.
33
42
  */
34
- pythGeneration?: PythGeneration;
43
+ pythApiKey?: string;
44
+ /**
45
+ * Retry/timeout policy for the off-chain Hermes / Lazer update fetches (see
46
+ * `fetchWithPolicy`). Optional — defaults to 15s timeout, 2 retries.
47
+ */
48
+ pythFetch?: PythFetchPolicy;
35
49
  }
36
50
  export declare class PerpClient extends BaseLineClient<WaterXConfig> {
37
- /** Pyth infra (network defaults unless overridden in JSON). */
51
+ /** Pyth Core infra (fixed per network) plus the caller-supplied credential/policy. */
38
52
  pyth: PythInfraConfig;
39
53
  /** Wormhole infra for the credit bridge (network defaults unless overridden). */
40
54
  wormhole: WormholeInfraConfig;
41
- /** Selected oracle rule source (client option, resolved at creation; default `'pyth_rule'`). See `OracleHost.oracleSource`. */
55
+ /** Selected oracle price-update source (`oracleSource` create option; default `'pyth_rule'`). */
42
56
  readonly oracleSource: OracleSource;
43
57
  /** Canonical-schema lookups (delegated to below); no transport. */
44
58
  private readonly view;
45
- constructor(network: Network, config: WaterXConfig, opts?: {
46
- grpcUrl?: string;
47
- oracleSource?: OracleSource;
48
- pythGeneration?: PythGeneration;
49
- });
59
+ constructor(network: Network, config: WaterXConfig, opts?: CreateClientOptions);
50
60
  /**
51
61
  * Async factory: fetches the deployment config for `network` and returns
52
62
  * a ready-to-use client. Pass `opts.cache=true` to memoize the JSON.
63
+ *
64
+ * No oracle-config guard here: selecting a source whose feeds are absent is
65
+ * not an error at init — it surfaces at tx-build time for the specific
66
+ * tickers that source can't serve (see `refreshOraclePrices`).
53
67
  */
54
68
  static create(network: Network, opts?: CreateClientOptions): Promise<PerpClient>;
55
69
  static mainnet(opts?: CreateClientOptions): Promise<PerpClient>;
@@ -12,21 +12,27 @@
12
12
  */
13
13
  import { BaseLineClient } from "../base-client.js";
14
14
  import { PerpConfigView } from "./config-view.js";
15
- import { loadConfig, PYTH_DEFAULTS, PYTH_PRO_DEFAULTS, WORMHOLE_DEFAULTS, } from "./config.js";
15
+ import { loadConfig, PYTH_DEFAULTS, WORMHOLE_DEFAULTS, } from "./config.js";
16
16
  export class PerpClient extends BaseLineClient {
17
- /** Pyth infra (network defaults unless overridden in JSON). */
17
+ /** Pyth Core infra (fixed per network) plus the caller-supplied credential/policy. */
18
18
  pyth;
19
19
  /** Wormhole infra for the credit bridge (network defaults unless overridden). */
20
20
  wormhole;
21
- /** Selected oracle rule source (client option, resolved at creation; default `'pyth_rule'`). See `OracleHost.oracleSource`. */
21
+ /** Selected oracle price-update source (`oracleSource` create option; default `'pyth_rule'`). */
22
22
  oracleSource;
23
23
  /** Canonical-schema lookups (delegated to below); no transport. */
24
24
  view;
25
25
  constructor(network, config, opts = {}) {
26
26
  super(network, config, opts);
27
- // Precedence: explicit config.pyth override > generation constants.
28
- this.pyth =
29
- config.pyth ?? (opts.pythGeneration === "pro" ? PYTH_PRO_DEFAULTS : PYTH_DEFAULTS)[network];
27
+ // Pyth Core infra is fixed per network — NOT deployment-overridable and
28
+ // NOT source-dependent (the pyth_lazer_rule source reads only api_key/fetch
29
+ // from here). The api_key + fetch policy are caller-supplied at init: a
30
+ // secret has no place in the canonical waterx-config JSON.
31
+ this.pyth = {
32
+ ...PYTH_DEFAULTS[network],
33
+ ...(opts.pythApiKey !== undefined ? { api_key: opts.pythApiKey } : {}),
34
+ ...(opts.pythFetch !== undefined ? { fetch: opts.pythFetch } : {}),
35
+ };
30
36
  this.wormhole = config.wormhole ?? WORMHOLE_DEFAULTS[network];
31
37
  this.oracleSource = opts.oracleSource ?? "pyth_rule";
32
38
  this.view = new PerpConfigView(() => this.config, () => this.wormhole);
@@ -34,14 +40,14 @@ export class PerpClient extends BaseLineClient {
34
40
  /**
35
41
  * Async factory: fetches the deployment config for `network` and returns
36
42
  * a ready-to-use client. Pass `opts.cache=true` to memoize the JSON.
43
+ *
44
+ * No oracle-config guard here: selecting a source whose feeds are absent is
45
+ * not an error at init — it surfaces at tx-build time for the specific
46
+ * tickers that source can't serve (see `refreshOraclePrices`).
37
47
  */
38
48
  static async create(network, opts = {}) {
39
49
  const config = await loadConfig(network, opts);
40
- return new PerpClient(network, config, {
41
- grpcUrl: opts.grpcUrl,
42
- oracleSource: opts.oracleSource,
43
- pythGeneration: opts.pythGeneration,
44
- });
50
+ return new PerpClient(network, config, opts);
45
51
  }
46
52
  static mainnet(opts = {}) {
47
53
  return PerpClient.create("MAINNET", opts);
@@ -9,11 +9,11 @@
9
9
  * below, keyed by network.
10
10
  */
11
11
  import type { AccountPackages, BasePackageEntry, WormholeInfraConfig } from "../account/config.ts";
12
- import type { OraclePackages, PythInfraConfig } from "../oracle/config.ts";
12
+ import type { OraclePackages } from "../oracle/config.ts";
13
13
  import type { Network } from "./constants.ts";
14
- export type { AccountConfig, AccountPackages, BasePackageEntry, NativeCustodyAsset, NativeCustodyPackage, TrustedEmitterRow, WaterxCreditPackage, WaterxReferralPackage, WithdrawalQueuePackage, WormholeBridgePackage, WormholeInfraConfig, WxaAccountPackage, } from "../account/config.ts";
15
- export type { ConstantFeedEntry, OracleConfig, OraclePackages, PythGeneration, PythInfraConfig, PythLazerRulePackage, PythRulePackage, PythSponsorRulePackage, SupraFeedEntry, SupraRulePackage, WaterxConstantRulePackage, WaterxOraclePackage, } from "../oracle/config.ts";
16
- export { PYTH_DEFAULTS, PYTH_PRO_DEFAULTS } from "../oracle/config.ts";
14
+ export type { AccountConfig, AccountPackages, BasePackageEntry, NativeCustodyAsset, NativeCustodyPackage, WaterxCreditPackage, WaterxReferralPackage, WithdrawalQueuePackage, WormholeBridgePackage, WormholeInfraConfig, WxaAccountPackage, } from "../account/config.ts";
15
+ export type { ConstantFeedEntry, OracleConfig, OraclePackages, PythFetchPolicy, PythInfraConfig, PythLazerRulePackage, PythRulePackage, PythSponsorRulePackage, SupraFeedEntry, SupraRulePackage, WaterxConstantRulePackage, WaterxOraclePackage, } from "../oracle/config.ts";
16
+ export { PYTH_DEFAULTS } from "../oracle/config.ts";
17
17
  export interface WaterxPerpMarketEntry {
18
18
  market: string;
19
19
  config: string;
@@ -82,8 +82,6 @@ export interface WaterXConfig {
82
82
  /** Sui gRPC base URL (default: public Mysten fullnode for the network). */
83
83
  grpcUrl?: string;
84
84
  packages: WaterXPackages;
85
- /** Pyth infra override (defaults from `PYTH_DEFAULTS[network]`). */
86
- pyth?: PythInfraConfig;
87
85
  /** Wormhole infra override (defaults from `WORMHOLE_DEFAULTS[network]`). */
88
86
  wormhole?: WormholeInfraConfig;
89
87
  /** Sui `CoinRegistry` shared object (credit deployments). */
@@ -8,8 +8,8 @@
8
8
  * **not** in the JSON — it lives in `PYTH_DEFAULTS` / `WORMHOLE_DEFAULTS`
9
9
  * below, keyed by network.
10
10
  */
11
- import { FetchPolicyError, fetchWithPolicy } from "../oracle/update-fetch.js";
12
- export { PYTH_DEFAULTS, PYTH_PRO_DEFAULTS } from "../oracle/config.js";
11
+ import { fetchWithPolicy, rethrowExhaustedFetch } from "../oracle/update-fetch.js";
12
+ export { PYTH_DEFAULTS } from "../oracle/config.js";
13
13
  // ============================================================================
14
14
  // Wormhole / Hermes — external chain infra, defaults by network
15
15
  // ============================================================================
@@ -113,16 +113,13 @@ export async function loadConfig(network, opts = {}) {
113
113
  const stale = configCache.get(cacheKey);
114
114
  if (stale)
115
115
  return stale;
116
- // Reformat a status-carrying FetchPolicyError (retries exhausted on a
117
- // retryable status) into this function's own message shape, mirroring
118
- // the non-retried `!response.ok` throw above. A network-level
119
- // exhaustion (no status), a `!response.ok` throw, or a JSON parse /
120
- // `validateConfig` failure has no domain-specific reframing to add —
121
- // propagate that error's own message as-is.
122
- if (err instanceof FetchPolicyError && err.status !== undefined) {
123
- throw new Error(`loadConfig: HTTP ${err.status} fetching ${url} (retries exhausted after ${err.attempts} attempts)`, { cause: err });
124
- }
125
- throw err;
116
+ // Reframe a status-carrying FetchPolicyError into this function's own
117
+ // message shape, mirroring the non-retried `!response.ok` throw above and
118
+ // carrying the URL (the key datum for a config-fetch failure). A
119
+ // network-level exhaustion (no status), a `!response.ok` throw, or a JSON
120
+ // parse / `validateConfig` failure has no reframing to add — the helper
121
+ // propagates those verbatim.
122
+ rethrowExhaustedFetch(err, (e) => `loadConfig: HTTP ${e.status} fetching ${url}`);
126
123
  }
127
124
  configCache.set(cacheKey, raw);
128
125
  return raw;
@@ -1,7 +1,7 @@
1
1
  export { PerpClient } from "./client.ts";
2
2
  export type { CreateClientOptions } from "./client.ts";
3
- export { PYTH_DEFAULTS, PYTH_PRO_DEFAULTS, WORMHOLE_DEFAULTS, clearConfigCache, loadConfig, } from "./config.ts";
4
- export type { BasePackageEntry, ConstantFeedEntry, WaterxReferralPackage, LoadConfigOptions, NativeCustodyAsset, NativeCustodyPackage, PythGeneration, PythInfraConfig, PythLazerRulePackage, PythRulePackage, PythSponsorRulePackage, SupraFeedEntry, SupraRulePackage, TestnetFaucetPackage, TrustedEmitterRow, WaterXConfig, WaterXPackages, WaterxCreditPackage, WaterxOraclePackage, WaterxPerpMarketEntry, WaterxPerpPackage, WaterxStakingPackage, WithdrawalQueuePackage, WlpPackage, WormholeBridgePackage, WormholeInfraConfig, WxaAccountPackage, } from "./config.ts";
3
+ export { PYTH_DEFAULTS, WORMHOLE_DEFAULTS, clearConfigCache, loadConfig } from "./config.ts";
4
+ export type { BasePackageEntry, ConstantFeedEntry, WaterxReferralPackage, LoadConfigOptions, NativeCustodyAsset, NativeCustodyPackage, PythFetchPolicy, PythInfraConfig, PythLazerRulePackage, PythRulePackage, PythSponsorRulePackage, SupraFeedEntry, SupraRulePackage, TestnetFaucetPackage, WaterXConfig, WaterXPackages, WaterxCreditPackage, WaterxOraclePackage, WaterxPerpMarketEntry, WaterxPerpPackage, WaterxStakingPackage, WithdrawalQueuePackage, WlpPackage, WormholeBridgePackage, WormholeInfraConfig, WxaAccountPackage, } from "./config.ts";
5
5
  export { ACTION_ADD_PRE_ORDER, ACTION_CANCEL_ORDER, ACTION_CANCEL_PRE_ORDER, ACTION_CLOSE_POSITION, ACTION_DECREASE_POSITION, ACTION_DEPOSIT_COLLATERAL, ACTION_INCREASE_POSITION, ACTION_LIQUIDATE, ACTION_OPEN_POSITION, ACTION_PLACE_ORDER, ACTION_UPDATE_ORDER, ACTION_WITHDRAW_COLLATERAL, BPS_SCALE, CRYPTO_FEE_RATE, DOUBLE_SCALE, DRY_RUN_SENDER, FLOAT_SCALE, MAINTENANCE_MARGIN_RATE, ORDER_LIMIT_BUY, ORDER_LIMIT_SELL, ORDER_STOP_BUY, ORDER_STOP_SELL, ORDER_TAG_WILDCARD, PERM_ALL, PERM_ALL_TRADING, PERM_CANCEL_ORDER, PERM_CLOSE_POSITION, PERM_DECREASE_POSITION, PERM_DEPOSIT_COLLATERAL, PERM_INCREASE_POSITION, PERM_MINT_WLP, PERM_OPEN_POSITION, PERM_PLACE_ORDER, PERM_REDEEM_WLP, PERM_WITHDRAW_COLLATERAL, STAKING_PERM_DEPOSIT_STAKE, STAKING_PERM_REDEEM_STAKE, STAKING_PERM_CLAIM_REWARD, STAKING_PERM_ALL, STOCK_FEE_RATE, MS_PER_YEAR, SUI_DECIMALS, WLP_DECIMALS, COLLATERAL_DECIMALS, TOKEN_DECIMALS, } from "./constants.ts";
6
6
  export type { Network } from "./constants.ts";
7
7
  export { getMarketTickers, getCollateralAssets } from "../utils/config.ts";
@@ -1,6 +1,6 @@
1
1
  // ======== Core ========
2
2
  export { PerpClient } from "./client.js";
3
- export { PYTH_DEFAULTS, PYTH_PRO_DEFAULTS, WORMHOLE_DEFAULTS, clearConfigCache, loadConfig, } from "./config.js";
3
+ export { PYTH_DEFAULTS, WORMHOLE_DEFAULTS, clearConfigCache, loadConfig } from "./config.js";
4
4
  // ======== Constants & enums ========
5
5
  export { ACTION_ADD_PRE_ORDER, ACTION_CANCEL_ORDER, ACTION_CANCEL_PRE_ORDER, ACTION_CLOSE_POSITION, ACTION_DECREASE_POSITION, ACTION_DEPOSIT_COLLATERAL, ACTION_INCREASE_POSITION, ACTION_LIQUIDATE, ACTION_OPEN_POSITION, ACTION_PLACE_ORDER, ACTION_UPDATE_ORDER, ACTION_WITHDRAW_COLLATERAL, BPS_SCALE, CRYPTO_FEE_RATE, DOUBLE_SCALE, DRY_RUN_SENDER, FLOAT_SCALE, MAINTENANCE_MARGIN_RATE, ORDER_LIMIT_BUY, ORDER_LIMIT_SELL, ORDER_STOP_BUY, ORDER_STOP_SELL, ORDER_TAG_WILDCARD, PERM_ALL, PERM_ALL_TRADING, PERM_CANCEL_ORDER, PERM_CLOSE_POSITION, PERM_DECREASE_POSITION, PERM_DEPOSIT_COLLATERAL, PERM_INCREASE_POSITION, PERM_MINT_WLP, PERM_OPEN_POSITION, PERM_PLACE_ORDER, PERM_REDEEM_WLP, PERM_WITHDRAW_COLLATERAL, STAKING_PERM_DEPOSIT_STAKE, STAKING_PERM_REDEEM_STAKE, STAKING_PERM_CLAIM_REWARD, STAKING_PERM_ALL, STOCK_FEE_RATE, MS_PER_YEAR, SUI_DECIMALS, WLP_DECIMALS, COLLATERAL_DECIMALS, TOKEN_DECIMALS, } from "./constants.js";
6
6
  // ======== Utilities ========
@@ -32,7 +32,7 @@ import { Transaction } from "@mysten/sui/transactions";
32
32
  import * as accountOps from "./account/index.ts";
33
33
  import * as perpReferral from "./account/referral.ts";
34
34
  import type { Network } from "./constants.ts";
35
- import type { PythGeneration } from "./oracle/config.ts";
35
+ import type { PythFetchPolicy } from "./oracle/config.ts";
36
36
  import type { OracleSource } from "./oracle/price-update-rule.ts";
37
37
  import { PerpClient, type CreateClientOptions as PerpCreateOptions } from "./perp/client.ts";
38
38
  import * as perpFetch from "./perp/fetch.ts";
@@ -366,20 +366,28 @@ export interface ClientCreateOptions {
366
366
  /** Memoize the fetched config JSON. */
367
367
  cache?: boolean;
368
368
  /**
369
- * Selects which `PriceUpdateRule` the perp line's `refreshOraclePrices` uses
370
- * for the on-chain price-update leg (see `OracleHost.oracleSource`).
371
- * Default: `'pyth_rule'`. Perp-line only. The SDK never reads `process.env`
372
- * — pass this from your own env var (e.g. `ORACLE_SOURCE`).
369
+ * The perp line's oracle price-update source (perp-line only — the
370
+ * prediction line has no oracle leg), forwarded to `PerpClient.create`.
371
+ * Source-neutral by design: a future source need not be Pyth.
372
+ *
373
+ * - `'pyth_rule'` (default) — Pyth Core updates on Core infra.
374
+ * - `'pyth_lazer_rule'` — Pyth Lazer signed updates (pair with `pythApiKey`
375
+ * and a config carrying `packages.pyth_lazer_rule`).
376
+ *
377
+ * Each source is self-contained with no cross-source fallback; selecting a
378
+ * source whose feed for a ticker is absent fails at tx-build (not at init).
379
+ * See perp `CreateClientOptions.oracleSource` for the full note.
373
380
  */
374
381
  oracleSource?: OracleSource;
375
382
  /**
376
- * Selects which Pyth Core contract generation feeds the perp line's
377
- * `client.perp.pyth` when the config JSON has no explicit `pyth` override:
378
- * `'core'` (default) or `'pro'` (post-2026-08-18 Pro-compatible contracts +
379
- * Hermes-compatible endpoint; pair with `pyth.api_key`). Perp-line only.
380
- * See `PythGeneration` / `PYTH_PRO_DEFAULTS`.
383
+ * Pyth Lazer access token, forwarded to the perp line. Required under
384
+ * `oracleSource: 'pyth_lazer_rule'`, unused by `'pyth_rule'`. A SECRET
385
+ * pass it at init from your own env var; it is never read from the config
386
+ * JSON or `process.env`.
381
387
  */
382
- pythGeneration?: PythGeneration;
388
+ pythApiKey?: string;
389
+ /** Retry/timeout policy for the perp line's off-chain oracle fetches. */
390
+ pythFetch?: PythFetchPolicy;
383
391
  /** Perp-line overrides (network, grpcUrl, waterxConfigUrl, cache, …). */
384
392
  perp?: PerpLineOptions;
385
393
  /** Prediction-line overrides (network, grpcUrl, waterxConfigUrl, cache, settlement, …). */
@@ -169,7 +169,8 @@ export class WaterXClient {
169
169
  waterxConfigUrl: opts.waterxConfigUrl,
170
170
  cache: opts.cache,
171
171
  oracleSource: opts.oracleSource,
172
- pythGeneration: opts.pythGeneration,
172
+ pythApiKey: opts.pythApiKey,
173
+ pythFetch: opts.pythFetch,
173
174
  ...perpRest,
174
175
  });
175
176
  const predictClient = await PredictClient.create(resolvedPredictNetwork, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waterx/sdk",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "WaterX SDK — perpetuals and prediction markets on Sui",
5
5
  "license": "MIT",
6
6
  "author": "WaterX",