@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
@@ -83,28 +83,26 @@ export declare function aggregateTickerWithConstant(tx: Transaction, host: Oracl
83
83
  * enabled, Constant when it's a constant ticker).
84
84
  *
85
85
  * Before that, the on-chain price *update* leg is routed by `host.oracleSource`
86
- * (see `rule-registry.ts`): the selected rule serves every ticker in its
87
- * `supportedTickers(host)`; tickers it doesn't cover fall back to `pyth_rule`
88
- * (`PythCoreRule`) when THEY support it so when `oracleSource` IS `'pyth_rule'`
89
- * there is exactly one group, identical to the pre-routing behavior. A ticker
90
- * supported by neither is simply skipped from this leg (no fetch/build call for
91
- * it) the same way today's non-pyth tickers (e.g. constant-only) always were;
92
- * it still gets aggregated below via whichever rule {@link aggregateTicker} finds.
86
+ * (see `rule-registry.ts`): the ONE selected source serves every ticker in its
87
+ * `supportedTickers(host)`. There is **no cross-source fallback** a requested
88
+ * ticker the selected source does not serve, and that is not a constant-only
89
+ * ticker (which needs no price-update leg), fails the build immediately with a
90
+ * clear error naming the ticker and source. That is the deliberate "fail the
91
+ * tx-build, don't silently reroute" contract: a wrong-but-present feed id is
92
+ * NOT validated here (it surfaces on-chain at dry-run); a MISSING feed for the
93
+ * selected source is caught here. When the selected source's feed exists but is
94
+ * wrong, this function does nothing special — the on-chain aggregate aborts at
95
+ * dry-run, which is correct.
93
96
  *
94
- * Each group's fetch + build runs against its own rule, which guarantees
95
- * per-rule PTB atomicity (no mixed-generation payload within one rule's calls).
96
- * When `oracleSource` isn't `'pyth_rule'`, one PTB may legitimately carry BOTH a
97
- * non-Pyth-Core block (selected group) and a Pyth Core block (fallback group) —
98
- * each verifies against its own contract objects, so that's fine. A fee-source
99
- * pre-check runs first, across every group's `requiresFeeSource` BEFORE any
100
- * off-chain fetch or PTB mutation — so a fee-charging group with no
101
- * `opts.feeSource` throws `OracleFeeSourceUnavailable` with zero wasted
102
- * network calls and zero stray moveCalls, even in a mixed shape (e.g. a
103
- * fee-free Lazer group ordered ahead of a Pyth Core fallback group). Only once
104
- * that check passes do all groups' off-chain fetches run concurrently
105
- * (`Promise.all`) and complete before any PTB mutation; on-chain reads inside
106
- * `buildUpdateCalls` can still fail mid-append for other reasons — callers
107
- * discard the tx on any throw.
97
+ * The selected source's fetch + build runs against its own infra, guaranteeing
98
+ * per-rule PTB atomicity. A fee-source pre-check runs first (the source's
99
+ * `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation so a
100
+ * fee-charging source with no `opts.feeSource` throws
101
+ * `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
102
+ * moveCalls. Only once that check passes does the off-chain fetch run and
103
+ * complete before any PTB mutation; on-chain reads inside `buildUpdateCalls`
104
+ * can still fail mid-append for other reasons — callers discard the tx on any
105
+ * throw.
108
106
  *
109
107
  * **Collector-feed leg is rule-aware:** a lazer-served group's
110
108
  * `buildUpdateCalls` returns the verified `Update` PTB value
@@ -159,28 +159,26 @@ export function aggregateTickerWithConstant(tx, host, args) {
159
159
  * enabled, Constant when it's a constant ticker).
160
160
  *
161
161
  * Before that, the on-chain price *update* leg is routed by `host.oracleSource`
162
- * (see `rule-registry.ts`): the selected rule serves every ticker in its
163
- * `supportedTickers(host)`; tickers it doesn't cover fall back to `pyth_rule`
164
- * (`PythCoreRule`) when THEY support it so when `oracleSource` IS `'pyth_rule'`
165
- * there is exactly one group, identical to the pre-routing behavior. A ticker
166
- * supported by neither is simply skipped from this leg (no fetch/build call for
167
- * it) the same way today's non-pyth tickers (e.g. constant-only) always were;
168
- * it still gets aggregated below via whichever rule {@link aggregateTicker} finds.
162
+ * (see `rule-registry.ts`): the ONE selected source serves every ticker in its
163
+ * `supportedTickers(host)`. There is **no cross-source fallback** a requested
164
+ * ticker the selected source does not serve, and that is not a constant-only
165
+ * ticker (which needs no price-update leg), fails the build immediately with a
166
+ * clear error naming the ticker and source. That is the deliberate "fail the
167
+ * tx-build, don't silently reroute" contract: a wrong-but-present feed id is
168
+ * NOT validated here (it surfaces on-chain at dry-run); a MISSING feed for the
169
+ * selected source is caught here. When the selected source's feed exists but is
170
+ * wrong, this function does nothing special — the on-chain aggregate aborts at
171
+ * dry-run, which is correct.
169
172
  *
170
- * Each group's fetch + build runs against its own rule, which guarantees
171
- * per-rule PTB atomicity (no mixed-generation payload within one rule's calls).
172
- * When `oracleSource` isn't `'pyth_rule'`, one PTB may legitimately carry BOTH a
173
- * non-Pyth-Core block (selected group) and a Pyth Core block (fallback group) —
174
- * each verifies against its own contract objects, so that's fine. A fee-source
175
- * pre-check runs first, across every group's `requiresFeeSource` BEFORE any
176
- * off-chain fetch or PTB mutation — so a fee-charging group with no
177
- * `opts.feeSource` throws `OracleFeeSourceUnavailable` with zero wasted
178
- * network calls and zero stray moveCalls, even in a mixed shape (e.g. a
179
- * fee-free Lazer group ordered ahead of a Pyth Core fallback group). Only once
180
- * that check passes do all groups' off-chain fetches run concurrently
181
- * (`Promise.all`) and complete before any PTB mutation; on-chain reads inside
182
- * `buildUpdateCalls` can still fail mid-append for other reasons — callers
183
- * discard the tx on any throw.
173
+ * The selected source's fetch + build runs against its own infra, guaranteeing
174
+ * per-rule PTB atomicity. A fee-source pre-check runs first (the source's
175
+ * `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation so a
176
+ * fee-charging source with no `opts.feeSource` throws
177
+ * `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
178
+ * moveCalls. Only once that check passes does the off-chain fetch run and
179
+ * complete before any PTB mutation; on-chain reads inside `buildUpdateCalls`
180
+ * can still fail mid-append for other reasons — callers discard the tx on any
181
+ * throw.
184
182
  *
185
183
  * **Collector-feed leg is rule-aware:** a lazer-served group's
186
184
  * `buildUpdateCalls` returns the verified `Update` PTB value
@@ -207,59 +205,49 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
207
205
  const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
208
206
  const priceInfoByTicker = new Map();
209
207
  pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
210
- // Group tickers for the on-chain update leg: selected rule first, then the
211
- // pyth_rule fallback for whatever the selected rule doesn't cover. `source`
212
- // is tracked alongside each group (rather than read back off `rule.kind`,
213
- // which is typed as the broader PriceUpdateRuleKind) so the provider lookup
214
- // below has an OracleSource to key on without a cast.
208
+ // ONE source, no fallback. The selected source serves the tickers in its
209
+ // `supportedTickers(host)`; `source` is tracked alongside the group (rather
210
+ // than read back off `rule.kind`, typed as the broader PriceUpdateRuleKind)
211
+ // so the provider lookup below has an OracleSource to key on without a cast.
215
212
  const selectedRule = resolveOracleRule(host.oracleSource, opts.ruleOverrides);
216
213
  const selectedSupported = new Set(selectedRule.supportedTickers(host));
217
- const selectedGroup = tickers.filter((t) => selectedSupported.has(t));
218
- const groups = [];
219
- if (selectedGroup.length > 0) {
220
- groups.push({ source: host.oracleSource, rule: selectedRule, tickers: selectedGroup });
221
- }
222
- if (host.oracleSource !== "pyth_rule") {
223
- const fallbackRule = resolveOracleRule("pyth_rule", opts.ruleOverrides);
224
- const fallbackSupported = new Set(fallbackRule.supportedTickers(host));
225
- const fallbackGroup = tickers.filter((t) => !selectedSupported.has(t) && fallbackSupported.has(t));
226
- if (fallbackGroup.length > 0) {
227
- groups.push({ source: "pyth_rule", rule: fallbackRule, tickers: fallbackGroup });
228
- }
214
+ // Fail the tx-build (NOT client init, NOT a silent reroute) when the selected
215
+ // source has no feed for a requested ticker that actually needs a price
216
+ // update. Only a CONSTANT-ONLY ticker is exempt — priced entirely by
217
+ // `constant_rule`, it needs no update leg from any source. A DUAL-FEED ticker
218
+ // (constant AND pyth) still needs its Pyth leg refreshed, so `isConstantTicker`
219
+ // alone must NOT exempt it: under a source that can't serve it, with no
220
+ // fallback, feeding an unrefreshed Pyth leg would price it stale (or abort on a
221
+ // missing weighted source). `priceInfoByTicker.has(t)` ⇔ the ticker has a
222
+ // `pyth_rule.feeds` entry, so `constant && !hasPyth` is exactly constant-only.
223
+ // This catches a MISSING feed; a present-but-WRONG feed id is deliberately not
224
+ // validated here (it aborts on-chain at dry-run).
225
+ const isConstantOnly = (t) => host.isConstantTicker(t) && !priceInfoByTicker.has(t);
226
+ const unservable = tickers.filter((t) => !selectedSupported.has(t) && !isConstantOnly(t));
227
+ if (unservable.length > 0) {
228
+ throw new Error(`oracleSource '${host.oracleSource}' has no feed configured for ticker(s): ` +
229
+ `${unservable.join(", ")}. Sources are self-contained with no fallback — add ` +
230
+ `${host.oracleSource} feeds for them, or select a source that serves them.`);
229
231
  }
230
- // Fee-source pre-check, hoisted ABOVE both the off-chain fetch below AND
231
- // the per-group build loop further down. The condition only consults
232
- // `group.rule.requiresFeeSource` — known the moment `groups` is built,
233
- // before any fetch or PTB mutation so this throws with ZERO wasted
234
- // network calls and zero PTB commands. A per-call guard inside
235
- // `buildPythPriceUpdateCalls` alone would not be early enough: in a mixed
236
- // shape (e.g. a lazer-selected `oracleSource` with a `pyth_rule` fallback
237
- // group for tickers Lazer doesn't cover), the build loop runs each
238
- // group's `buildUpdateCalls` in sequence — a fee-free group ordered ahead
239
- // of a fee-charging one would already have appended its verify/feed
240
- // moveCalls to the shared `tx` by the time the fee-charging group's own
241
- // guard fired, breaking the "throw before any PTB mutation" guarantee.
242
- // Checking every group's `requiresFeeSource` up front — before ANY group
243
- // fetches or builds — closes that gap, and (unlike a referential check
244
- // against a specific rule instance) keeps protecting a future
245
- // fee-charging rule or a test double standing in for one.
246
- if (!opts.feeSource && groups.some((group) => group.rule.requiresFeeSource)) {
232
+ const selectedGroup = tickers.filter((t) => selectedSupported.has(t));
233
+ // Fee-source pre-check, hoisted ABOVE the off-chain fetch and PTB build below.
234
+ // It consults only `rule.requiresFeeSource` — known before any fetch or PTB
235
+ // mutation so a fee-charging source with no `feeSource` throws with ZERO
236
+ // wasted network calls and zero PTB commands, rather than waiting for
237
+ // `buildPythPriceUpdateCalls`'s own per-call guard to fire after the off-chain
238
+ // fetch already ran.
239
+ if (selectedGroup.length > 0 && !opts.feeSource && selectedRule.requiresFeeSource) {
247
240
  throw new OracleFeeSourceUnavailableError();
248
241
  }
249
- // Fetch every group's off-chain payload concurrently (independent network
250
- // calls no reason to serialize) and let ALL of them settle before the
251
- // first PTB mutation below, so a later group's fetch failure can never
252
- // leave an earlier group's moveCalls stranded in a caller-owned tx.
253
- const groupsWithData = await Promise.all(groups.map(async (group) => ({
254
- rule: group.rule,
255
- tickers: group.tickers,
256
- data: await resolveGroupUpdateData(host, group, opts.updateDataProvider),
257
- })));
258
- // Verified-`Update` handle per lazer-served ticker (one shared PTB value per
259
- // group) — consumed by the collector-feed leg below.
242
+ // Resolve + build the selected source's update leg. The off-chain fetch
243
+ // settles before the first PTB mutation, so a fetch failure never strands
244
+ // moveCalls in a caller-owned tx. Map each lazer-served ticker to the one
245
+ // verified `Update` PTB value for the collector-feed leg below.
260
246
  const lazerUpdateByTicker = new Map();
261
- for (const group of groupsWithData) {
262
- const handle = (await group.rule.buildUpdateCalls(tx, host, group.data, {
247
+ if (selectedGroup.length > 0) {
248
+ const group = { source: host.oracleSource, rule: selectedRule, tickers: selectedGroup };
249
+ const data = await resolveGroupUpdateData(host, group, opts.updateDataProvider);
250
+ const handle = (await selectedRule.buildUpdateCalls(tx, host, data, {
263
251
  cache: opts.cache,
264
252
  feeSource: opts.feeSource,
265
253
  })) ?? undefined;
@@ -267,7 +255,7 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
267
255
  // protect: a future non-lazer handle (e.g. a WaterxRule value) must never
268
256
  // be silently fed into pyth_lazer_rule::feed.
269
257
  if (handle?.kind === "pyth_lazer_rule") {
270
- for (const ticker of group.tickers)
258
+ for (const ticker of selectedGroup)
271
259
  lazerUpdateByTicker.set(ticker, handle.update);
272
260
  }
273
261
  }
@@ -101,6 +101,29 @@ export interface OraclePackages {
101
101
  supra_rule?: SupraRulePackage;
102
102
  waterx_oracle: WaterxOraclePackage;
103
103
  }
104
+ /**
105
+ * Resolved Pyth Core infra as it lives on `client.pyth` — NOT a config-JSON
106
+ * shape. `state_id` / `wormhole_state_id` / `hermes_endpoint` come verbatim
107
+ * from the fixed per-network constant ({@link PYTH_DEFAULTS}); `api_key` /
108
+ * `fetch` are layered on from the caller's `pythApiKey` / `pythFetch` create
109
+ * options. None of it is sourced from the canonical `waterx-config` JSON — the
110
+ * SDK never reads a `pyth` block there (a Bearer secret has no place in a
111
+ * public CDN document). The infra is the same for every `oracleSource`; the
112
+ * `pyth_lazer_rule` source reads only the `api_key` / `fetch` from here and
113
+ * gets its on-chain infra from {@link LAZER_DEFAULTS} + config instead.
114
+ */
115
+ /**
116
+ * The caller-tunable subset of `fetchWithPolicy`'s policy exposed on the
117
+ * `pythFetch` create option and `client.pyth.fetch` — the retry/timeout budget
118
+ * for the off-chain Hermes (`fetchPriceFeedsUpdateData`) and Lazer
119
+ * (`PythLazerRule`) update fetches. Deliberately narrower than the internal
120
+ * `FetchPolicy` (no `retryDelayMs` / `apiKey` / `fetchImpl`). Both fetches fall
121
+ * back to `fetchWithPolicy`'s defaults (15s timeout, 2 retries) when unset.
122
+ */
123
+ export type PythFetchPolicy = {
124
+ timeoutMs?: number;
125
+ retries?: number;
126
+ };
104
127
  export interface PythInfraConfig {
105
128
  state_id: string;
106
129
  wormhole_state_id: string;
@@ -109,8 +132,8 @@ export interface PythInfraConfig {
109
132
  * Pyth Pro / Lazer access token (`Authorization: Bearer …`) for
110
133
  * `PythLazerRule`'s signed-update fetch — Lazer is auth-first, so there is
111
134
  * no keyless default. Optional: Pyth-Core-only deployments never need it.
112
- * Consumers pass it through client config (`config.pyth`); the SDK never
113
- * reads `process.env`. Absent when a lazer-routed fetch runs →
135
+ * Supplied via the `pythApiKey` create option (the SDK never reads
136
+ * `process.env` or the config JSON). Absent when a lazer-routed fetch runs →
114
137
  * `LazerApiKeyMissing` is thrown at fetch time. As of the Pyth Pro
115
138
  * migration (post-2026-08-18, per
116
139
  * https://docs.pyth.network/price-feeds/core/upgrade) this is ALSO required
@@ -119,57 +142,16 @@ export interface PythInfraConfig {
119
142
  */
120
143
  api_key?: string;
121
144
  /**
122
- * Retry/timeout policy override for the Hermes (`fetchPriceFeedsUpdateData`)
123
- * and Lazer (`PythLazerRule`) off-chain update fetches — see
124
- * `fetchWithPolicy` (`./update-fetch.ts`) for the full policy (backoff,
125
- * which statuses retry, Bearer attachment). Optional: both fetches default
126
- * to `fetchWithPolicy`'s built-in defaults (15s timeout, 2 retries) when
127
- * unset.
145
+ * Retry/timeout policy for the Hermes (`fetchPriceFeedsUpdateData`) and
146
+ * Lazer (`PythLazerRule`) off-chain update fetches — see `fetchWithPolicy`
147
+ * (`./update-fetch.ts`) for the full policy (backoff, which statuses retry,
148
+ * Bearer attachment). Supplied via the `pythFetch` create option. Optional:
149
+ * both fetches default to `fetchWithPolicy`'s built-in defaults (15s
150
+ * timeout, 2 retries) when unset.
128
151
  */
129
- fetch?: {
130
- timeoutMs?: number;
131
- retries?: number;
132
- };
152
+ fetch?: PythFetchPolicy;
133
153
  }
134
154
  export declare const PYTH_DEFAULTS: Record<Network, PythInfraConfig>;
135
- /**
136
- * Which Pyth Core contract generation feeds `host.pyth` when the config JSON
137
- * carries no explicit `pyth` override:
138
- *
139
- * - `'core'` (default) — the original contracts + keyless Hermes
140
- * ({@link PYTH_DEFAULTS}).
141
- * - `'pro'` — the Pro-compatible upgraded contracts + the Hermes-compatible
142
- * endpoint ({@link PYTH_PRO_DEFAULTS}); pair with `pyth.api_key` after the
143
- * 2026-08-18 cutover.
144
- *
145
- * Resolved once at client creation from the `pythGeneration` create option.
146
- * Orthogonal to `oracleSource` — this flips the Pyth-Core *infra* (state ids
147
- * + endpoint), not which `PriceUpdateRule` routes tickers.
148
- */
149
- export type PythGeneration = "core" | "pro";
150
- /**
151
- * Pyth **Pro-generation** Core-compatible infra — the post-2026-08-18
152
- * contracts (cutover date per
153
- * https://docs.pyth.network/price-feeds/core/upgrade) from Pyth's Core-Upgrade
154
- * docs
155
- * (https://docs.pyth.network/price-feeds/core/upgrade/contracts, Sui section;
156
- * package revs `sui-pro-compatible-contract-mainnet` /
157
- * `sui-pro-compatible-contract-testnet`). Selected via the client's
158
- * `pythGeneration: 'pro'` create option; `config.pyth` still overrides
159
- * wholesale (see `PerpClient`). All four state ids were verified on-chain
160
- * (shared `state::State` objects under the docs' upgraded package ids).
161
- *
162
- * Kept as a second flat map beside {@link PYTH_DEFAULTS} rather than a nested
163
- * `PYTH_INFRA[network][generation]` — `PYTH_DEFAULTS` is a published export
164
- * with external consumers, so the smallest honest surface is an additive
165
- * sibling (same deferral note as {@link LAZER_DEFAULTS}).
166
- *
167
- * The Hermes-compatible endpoint (`pyth.dourolabs.app/hermes`) serves the
168
- * same REST surface as `hermes.pyth.network` but requires `pyth.api_key`
169
- * (`Authorization: Bearer …`) after the cutover — see
170
- * {@link PythInfraConfig.api_key}.
171
- */
172
- export declare const PYTH_PRO_DEFAULTS: Record<Network, PythInfraConfig>;
173
155
  /**
174
156
  * Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
175
157
  * Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
@@ -197,6 +179,4 @@ export declare const LAZER_DEFAULTS: Record<Network, {
197
179
  */
198
180
  export interface OracleConfig extends BaseLineConfig {
199
181
  packages: OraclePackages;
200
- /** Pyth infra override (defaults from {@link PYTH_DEFAULTS}). */
201
- pyth?: PythInfraConfig;
202
182
  }
@@ -21,40 +21,6 @@ export const PYTH_DEFAULTS = {
21
21
  hermes_endpoint: "https://hermes-beta.pyth.network",
22
22
  },
23
23
  };
24
- /**
25
- * Pyth **Pro-generation** Core-compatible infra — the post-2026-08-18
26
- * contracts (cutover date per
27
- * https://docs.pyth.network/price-feeds/core/upgrade) from Pyth's Core-Upgrade
28
- * docs
29
- * (https://docs.pyth.network/price-feeds/core/upgrade/contracts, Sui section;
30
- * package revs `sui-pro-compatible-contract-mainnet` /
31
- * `sui-pro-compatible-contract-testnet`). Selected via the client's
32
- * `pythGeneration: 'pro'` create option; `config.pyth` still overrides
33
- * wholesale (see `PerpClient`). All four state ids were verified on-chain
34
- * (shared `state::State` objects under the docs' upgraded package ids).
35
- *
36
- * Kept as a second flat map beside {@link PYTH_DEFAULTS} rather than a nested
37
- * `PYTH_INFRA[network][generation]` — `PYTH_DEFAULTS` is a published export
38
- * with external consumers, so the smallest honest surface is an additive
39
- * sibling (same deferral note as {@link LAZER_DEFAULTS}).
40
- *
41
- * The Hermes-compatible endpoint (`pyth.dourolabs.app/hermes`) serves the
42
- * same REST surface as `hermes.pyth.network` but requires `pyth.api_key`
43
- * (`Authorization: Bearer …`) after the cutover — see
44
- * {@link PythInfraConfig.api_key}.
45
- */
46
- export const PYTH_PRO_DEFAULTS = {
47
- MAINNET: {
48
- state_id: "0x03719fae774ddab3cfcaa53bbc046f0cbe21410019b6280811bf3f9f4b05839d",
49
- wormhole_state_id: "0xdbca52b9fb4f712e25f61f974586d93ac541bcf8389564f0323bb07215168b5c",
50
- hermes_endpoint: "https://pyth.dourolabs.app/hermes",
51
- },
52
- TESTNET: {
53
- state_id: "0x3c48fe392912de6c18087a2b3f5fdbfbfdb4598e180947feff1f12f8e9ea073e",
54
- wormhole_state_id: "0x750da8e6d16b6a363a39fe2eaa8295ac224a1e6fce4e47b58845e2e8746164f0",
55
- hermes_endpoint: "https://pyth.dourolabs.app/hermes",
56
- },
57
- };
58
24
  // ============================================================================
59
25
  // Pyth Lazer — external infra, defaults by network
60
26
  // ============================================================================
@@ -16,7 +16,7 @@ export interface OracleHost {
16
16
  readonly network: Network;
17
17
  /** Oracle slice of the canonical `waterx-config` JSON (rule packages + per-ticker feeds). */
18
18
  readonly config: OracleConfig;
19
- /** External Pyth/Wormhole/Hermes infra (network default, overridable via config). */
19
+ /** External Pyth/Wormhole/Hermes infra — fixed per `(network, generation)`; api_key/fetch layered from create options. */
20
20
  readonly pyth: PythInfraConfig;
21
21
  /** gRPC client for the on-chain reads the Pyth update path needs. */
22
22
  readonly grpcClient: SuiGrpcClient;
@@ -16,9 +16,9 @@
16
16
  * rules that consume it are separate concerns.
17
17
  */
18
18
  export type { OracleHost } from "./host.ts";
19
- export { FetchPolicyError } from "./update-fetch.ts";
19
+ export { FetchPolicyError, fetchWithPolicy, joinEndpointPath } from "./update-fetch.ts";
20
20
  export type { FetchPolicy } from "./update-fetch.ts";
21
- export { PythCache, fetchPriceFeedsUpdateData, buildPythPriceUpdateCalls, updatePythPrices, OracleFeeSourceUnavailableError, } from "./pyth.ts";
21
+ export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls, updatePythPrices, HermesEndpointRejectedAllFeedsError, MISSING_FEED_MEMO_TTL_MS, OracleFeeSourceUnavailableError, } from "./pyth.ts";
22
22
  export type { OracleFeeSource } from "./pyth.ts";
23
23
  export type { PriceUpdateRule, PriceUpdateRuleKind, RuleUpdateData, RuleUpdateHandle, BuildUpdateOpts, OracleSource, UpdateDataProvider, } from "./price-update-rule.ts";
24
24
  export { PythCoreRule } from "./rules/pyth-core-rule.ts";
@@ -19,13 +19,21 @@
19
19
  // just the type) so a consumer (e.g. a BE prefetch cache) can `instanceof`
20
20
  // it off the failure `fetchPriceFeedsUpdateData` / `PythLazerRule` /
21
21
  // `loadConfig` surface, without a deep import of `./update-fetch.ts`.
22
- // `fetchWithPolicy` itself stays module-internal (no external caller yet).
23
- export { FetchPolicyError } from "./update-fetch.js";
24
- // Pyth source `OracleFeeSourceUnavailableError` is re-exported (not just
25
- // the type) for the same `instanceof` reason as `FetchPolicyError` above: a
26
- // consumer of `buildPythPriceUpdateCalls` / `updatePythPrices` /
27
- // `refreshOraclePrices` can branch on the fee-source failure directly.
28
- export { PythCache, fetchPriceFeedsUpdateData, buildPythPriceUpdateCalls, updatePythPrices, OracleFeeSourceUnavailableError, } from "./pyth.js";
22
+ // `fetchWithPolicy` + `joinEndpointPath` are exported for consumers that hit
23
+ // Hermes-compatible endpoints THEMSELVES (e.g. the BE's parsed latest-price
24
+ // bootstrap and Pyth schedule readers): one shared Bearer/timeout/retry
25
+ // policy and one base-path-safe URL join, instead of each caller re-rolling
26
+ // them (the hand-rolled copies were how the Pro `/hermes` base path got
27
+ // dropped and the Bearer went missing on sibling fetches).
28
+ export { FetchPolicyError, fetchWithPolicy, joinEndpointPath } from "./update-fetch.js";
29
+ // Pyth source — `OracleFeeSourceUnavailableError` and
30
+ // `HermesEndpointRejectedAllFeedsError` are re-exported (not just the types)
31
+ // for the same `instanceof` reason as `FetchPolicyError` above: a consumer of
32
+ // `buildPythPriceUpdateCalls` / `updatePythPrices` / `refreshOraclePrices` can
33
+ // branch on the fee-source failure directly, and a consumer of
34
+ // `fetchPriceFeedsUpdateData` / `probeMissingFeeds` can tell a misconfigured
35
+ // or unentitled endpoint apart from feeds that endpoint genuinely lacks.
36
+ export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls, updatePythPrices, HermesEndpointRejectedAllFeedsError, MISSING_FEED_MEMO_TTL_MS, OracleFeeSourceUnavailableError, } from "./pyth.js";
29
37
  // Pyth Core rule (PriceUpdateRule wrapper over the Pyth source above)
30
38
  export { PythCoreRule } from "./rules/pyth-core-rule.js";
31
39
  // Pyth Lazer rule (signed-update generation; `feedLazerRule` stays internal to `aggregate.ts`)
@@ -14,6 +14,7 @@
14
14
  * 4. hot_potato_vector::destroy
15
15
  */
16
16
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
17
+ import type { PythFetchPolicy } from "./config.ts";
17
18
  import type { OracleHost } from "./host.ts";
18
19
  type PriceTableInfo = {
19
20
  id: string;
@@ -29,13 +30,74 @@ export declare class PythCache {
29
30
  priceTableInfo?: PriceTableInfo;
30
31
  priceFeedObjectIdCache: Map<string, string | undefined>;
31
32
  }
32
- export declare function fetchPriceFeedsUpdateData(endpoint: string, priceIds: string[], opts?: {
33
+ type FetchOpts = {
33
34
  apiKey?: string;
34
- fetch?: {
35
- timeoutMs?: number;
36
- retries?: number;
37
- };
38
- }): Promise<Uint8Array[]>;
35
+ fetch?: PythFetchPolicy;
36
+ };
37
+ /**
38
+ * How long a "this endpoint lacks feed X" verdict stays memoized. The verdict
39
+ * is a claim about *someone else's* deployment — a feed can be added to the
40
+ * catalog, an entitlement can be granted, a Pro plan can be upgraded — so it
41
+ * must expire rather than bind the whole process lifetime. Long enough that a
42
+ * genuinely-absent feed costs one discovery per window instead of one per
43
+ * build; short enough that a recovered endpoint self-heals without a restart.
44
+ */
45
+ export declare const MISSING_FEED_MEMO_TTL_MS: number;
46
+ /**
47
+ * Thrown when discovery concludes that an endpoint rejects EVERY requested
48
+ * feed id without a catalog vouching for that verdict. `instanceof`-able
49
+ * (mirrors `FetchPolicyError` / {@link OracleFeeSourceUnavailableError}).
50
+ *
51
+ * "All of them are missing" is the signature of an endpoint/credential fault —
52
+ * a wrong base path (the Pyth Pro `/hermes` prefix dropped), a changed route,
53
+ * a revoked or downgraded entitlement — not of N individually-absent feeds.
54
+ * Memoizing it would convert a loud, fixable misconfiguration into a silent
55
+ * permanent one: every id marked missing ⇒ `fetchPriceFeedsUpdateData` returns
56
+ * `[]` ⇒ `buildPythPriceUpdateCalls` throws "Hermes returned empty results",
57
+ * blaming Hermes for having no data, for the rest of the process's life. So
58
+ * this case writes NOTHING to the memo and throws instead; the next call
59
+ * re-probes and recovers on its own once the endpoint does.
60
+ *
61
+ * The message keeps the `Hermes price fetch failed: <status>` prefix on its
62
+ * first line — the documented contract downstream consumers string-match (see
63
+ * {@link fetchPriceFeedsUpdateData} and the e2e transient detector).
64
+ */
65
+ export declare class HermesEndpointRejectedAllFeedsError extends Error {
66
+ readonly endpoint: string;
67
+ readonly requestedCount: number;
68
+ constructor(endpoint: string, requestedCount: number, catalogState: "unreadable" | "empty");
69
+ }
70
+ /**
71
+ * The subset of `feedIds` this `endpoint` is known to serve — i.e. minus any
72
+ * discovered to be absent within the last {@link MISSING_FEED_MEMO_TTL_MS}
73
+ * (see {@link fetchPriceFeedsUpdateData}). Callers building a
74
+ * `{ updates, feedIds }` payload use this to keep `feedIds` aligned with the
75
+ * feeds the fetch actually returned data for, so `buildPythPriceUpdateCalls`
76
+ * (one moveCall per feed id) never references a feed the accumulator blob
77
+ * doesn't cover.
78
+ *
79
+ * Expired entries are pruned here rather than on a timer: the memo is only
80
+ * ever consulted through this function, so a lazy sweep is both sufficient and
81
+ * free of a dangling interval in a library.
82
+ */
83
+ export declare function endpointSupportedFeedIds(endpoint: string, feedIds: string[], apiKey?: string): string[];
84
+ /** Test-only: forget everything learned about which feeds an endpoint lacks. */
85
+ export declare function __resetMissingFeedCacheForTest(): void;
86
+ /**
87
+ * Discovery-only entry for consumers that fetch Hermes THEMSELVES (e.g. a
88
+ * parsed latest-price reader) and just observed a whole-batch 404: resolves
89
+ * which ids the endpoint lacks, memoizes them (see {@link
90
+ * endpointSupportedFeedIds}), fetches NO survivor data. Without this, such a
91
+ * consumer's only way to populate the memo was calling {@link
92
+ * fetchPriceFeedsUpdateData} and discarding its accumulator blob — two full
93
+ * redundant transfers per cold discovery.
94
+ *
95
+ * @throws HermesEndpointRejectedAllFeedsError when the rejection looks
96
+ * endpoint-wide rather than per-feed — the caller's own 404 is then a
97
+ * misconfiguration to surface, not a set of feeds to quietly drop.
98
+ */
99
+ export declare function probeMissingFeeds(endpoint: string, ids: string[], opts?: FetchOpts): Promise<void>;
100
+ export declare function fetchPriceFeedsUpdateData(endpoint: string, priceIds: string[], opts?: FetchOpts): Promise<Uint8Array[]>;
39
101
  /**
40
102
  * Resolved source for the Pyth Core on-chain update fee. Deliberately a
41
103
  * closed two-variant union, not a `{ sponsorFund?, allowGasFee? }` pair — a