@waterx/sdk 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +22 -17
  2. package/dist/cjs/src/account/config.d.ts +3 -2
  3. package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
  4. package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +8 -154
  5. package/dist/cjs/src/oracle/aggregate.d.ts +21 -21
  6. package/dist/cjs/src/oracle/aggregate.js +118 -69
  7. package/dist/cjs/src/oracle/config.d.ts +38 -72
  8. package/dist/cjs/src/oracle/config.js +0 -56
  9. package/dist/cjs/src/oracle/host.d.ts +24 -20
  10. package/dist/cjs/src/oracle/index.d.ts +5 -3
  11. package/dist/cjs/src/oracle/index.js +31 -8
  12. package/dist/cjs/src/oracle/price-update-rule.d.ts +13 -6
  13. package/dist/cjs/src/oracle/price-update-rule.js +3 -3
  14. package/dist/cjs/src/oracle/pyth.d.ts +44 -0
  15. package/dist/cjs/src/oracle/pyth.js +48 -6
  16. package/dist/cjs/src/oracle/read-plane.d.ts +70 -0
  17. package/dist/cjs/src/oracle/read-plane.js +78 -0
  18. package/dist/cjs/src/oracle/rule-registry.d.ts +6 -4
  19. package/dist/cjs/src/oracle/rule-registry.js +6 -4
  20. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +4 -4
  21. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
  22. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +29 -4
  23. package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -4
  24. package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +42 -5
  25. package/dist/cjs/src/oracle/rules/waterx-rule.js +57 -17
  26. package/dist/cjs/src/perp/client.d.ts +40 -27
  27. package/dist/cjs/src/perp/client.js +28 -20
  28. package/dist/cjs/src/perp/config.d.ts +6 -6
  29. package/dist/cjs/src/perp/config.js +12 -12
  30. package/dist/cjs/src/perp/index.d.ts +3 -3
  31. package/dist/cjs/src/perp/index.js +7 -4
  32. package/dist/cjs/src/unified-client.d.ts +14 -9
  33. package/dist/cjs/src/unified-client.js +2 -2
  34. package/dist/src/account/config.d.ts +3 -2
  35. package/dist/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
  36. package/dist/src/generated/waterx_rule/waterx_rule.js +7 -143
  37. package/dist/src/oracle/aggregate.d.ts +21 -21
  38. package/dist/src/oracle/aggregate.js +118 -69
  39. package/dist/src/oracle/config.d.ts +38 -72
  40. package/dist/src/oracle/config.js +1 -55
  41. package/dist/src/oracle/host.d.ts +24 -20
  42. package/dist/src/oracle/index.d.ts +5 -3
  43. package/dist/src/oracle/index.js +25 -10
  44. package/dist/src/oracle/price-update-rule.d.ts +13 -6
  45. package/dist/src/oracle/price-update-rule.js +3 -3
  46. package/dist/src/oracle/pyth.d.ts +44 -0
  47. package/dist/src/oracle/pyth.js +45 -5
  48. package/dist/src/oracle/read-plane.d.ts +70 -0
  49. package/dist/src/oracle/read-plane.js +74 -0
  50. package/dist/src/oracle/rule-registry.d.ts +6 -4
  51. package/dist/src/oracle/rule-registry.js +6 -4
  52. package/dist/src/oracle/rules/pyth-core-rule.js +5 -5
  53. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
  54. package/dist/src/oracle/rules/pyth-lazer-rule.js +28 -3
  55. package/dist/src/oracle/rules/pyth-rule.js +5 -4
  56. package/dist/src/oracle/rules/waterx-rule.d.ts +42 -5
  57. package/dist/src/oracle/rules/waterx-rule.js +53 -14
  58. package/dist/src/perp/client.d.ts +40 -27
  59. package/dist/src/perp/client.js +29 -21
  60. package/dist/src/perp/config.d.ts +6 -6
  61. package/dist/src/perp/config.js +11 -9
  62. package/dist/src/perp/index.d.ts +3 -3
  63. package/dist/src/perp/index.js +2 -2
  64. package/dist/src/unified-client.d.ts +14 -9
  65. package/dist/src/unified-client.js +2 -2
  66. package/package.json +1 -1
@@ -20,7 +20,8 @@
20
20
  *
21
21
  * `refreshOraclePrices` additionally routes the on-chain price *update* leg
22
22
  * (the fetch + verify/push step, before any of the above feeding) through the
23
- * `PriceUpdateRule` selected by `host.oracleSource` — see `rule-registry.ts`.
23
+ * `PriceUpdateRule` of EVERY source in the `host.oracleSources` fed set — see
24
+ * `rule-registry.ts`.
24
25
  */
25
26
  Object.defineProperty(exports, "__esModule", { value: true });
26
27
  exports.aggregateTicker = aggregateTicker;
@@ -121,7 +122,13 @@ function aggregateTicker(tx, host, args) {
121
122
  // waterx_rule::collect_batch_latest verifies the batch signature and feeds
122
123
  // this collector's symbol from the batch. If the ticker's aggregator does
123
124
  // not (yet) weight `WaterxRule`, the contribution is silently dropped
124
- // on-chain — feeding ahead of the weight migration is harmless.
125
+ // on-chain — feeding ahead of the weight migration is safe for THIS tx.
126
+ // CAVEAT (unlike lazer): the feed call records a per-symbol signed-
127
+ // timestamp high-water mark REGARDLESS of weights, and a replayed
128
+ // timestamp ABORTS (`EReplayedSignature`, audit F-014) — so two PTBs
129
+ // carrying the same envelope for the same symbol cannot both land; the
130
+ // second aborts even where waterx is unweighted. See WaterxRule's module
131
+ // header.
125
132
  (0, waterx_rule_ts_1.feedWaterxRule)(tx, host, collector, args.waterxEnvelope);
126
133
  fed = true;
127
134
  }
@@ -173,27 +180,26 @@ function aggregateTickerWithConstant(tx, host, args) {
173
180
  * entry, Lazer if the lazer update leg served it — see below — Supra when
174
181
  * enabled, Constant when it's a constant ticker).
175
182
  *
176
- * Before that, the on-chain price *update* leg is routed by `host.oracleSource`
177
- * (see `rule-registry.ts`): the ONE selected source serves every ticker in its
178
- * `supportedTickers(host)`. There is **no cross-source fallback** — a requested
179
- * ticker the selected source does not serve, and that is not a constant-only
180
- * ticker (which needs no price-update leg), fails the build immediately with a
181
- * clear error naming the ticker and source. That is the deliberate "fail the
182
- * tx-build, don't silently reroute" contract: a wrong-but-present feed id is
183
- * NOT validated here (it surfaces on-chain at dry-run); a MISSING feed for the
184
- * selected source is caught here. When the selected source's feed exists but is
185
- * wrong, this function does nothing special — the on-chain aggregate aborts at
186
- * dry-run, which is correct.
183
+ * Before that, the on-chain price *update* leg is routed by the
184
+ * `host.oracleSources` fed set (see `rule-registry.ts`): EVERY listed source
185
+ * updates the tickers its own `supportedTickers(host)` serves, all in this one
186
+ * PTB. There is **no cross-source fallback** a requested ticker NO listed
187
+ * source serves, and that is not a constant-only ticker (which needs no
188
+ * price-update leg), fails the build immediately with a clear error naming
189
+ * the ticker and the list. That is the deliberate "fail the tx-build, don't
190
+ * silently reroute" contract: a wrong-but-present feed id is NOT validated
191
+ * here (it surfaces on-chain at dry-run); a ticker MISSING from every listed
192
+ * source's feeds is caught here.
187
193
  *
188
- * The selected source's fetch + build runs against its own infra, guaranteeing
189
- * per-rule PTB atomicity. A fee-source pre-check runs first (the source's
190
- * `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation — so a
191
- * fee-charging source with no `opts.feeSource` throws
194
+ * Each source's fetch + build runs against its own infra, guaranteeing
195
+ * per-rule PTB atomicity. A fee-source pre-check runs first (any listed
196
+ * source's `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
197
+ * so a fee-charging source with no `opts.feeSource` throws
192
198
  * `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
193
- * moveCalls. Only once that check passes does the off-chain fetch run and
194
- * complete before any PTB mutation; on-chain reads inside `buildUpdateCalls`
195
- * can still fail mid-append for other reasons — callers discard the tx on any
196
- * throw.
199
+ * moveCalls. Only once that check passes do the off-chain fetches run — in
200
+ * parallel across sources — and ALL settle before the first PTB mutation;
201
+ * on-chain reads inside `buildUpdateCalls` can still fail mid-append for
202
+ * other reasons — callers discard the tx on any throw.
197
203
  *
198
204
  * **Collector-feed leg is rule-aware:** a lazer-served group's
199
205
  * `buildUpdateCalls` returns the verified `Update` PTB value
@@ -214,78 +220,121 @@ function aggregateTickerWithConstant(tx, host, args) {
214
220
  async function refreshOraclePrices(tx, host, tickers, opts = {}) {
215
221
  if (tickers.length === 0)
216
222
  return;
223
+ // Dedupe the caller's list (order-preserving): a repeated ticker would
224
+ // otherwise aggregate TWICE in this one PTB — wasted gas for every rule,
225
+ // and a hard ABORT under waterx: the second `collect_batch_latest` carries
226
+ // the same envelope, and the on-chain per-symbol replay guard rejects an
227
+ // already-accepted signed timestamp (`EReplayedSignature`, F-014) even
228
+ // inside a single transaction.
229
+ tickers = [...new Set(tickers)];
217
230
  // price_info_object lookup for every ticker with a pyth_rule.feeds entry —
218
231
  // needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
219
232
  // which rule performed the on-chain update for that ticker.
220
233
  const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
221
234
  const priceInfoByTicker = new Map();
222
235
  pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
223
- // ONE source, no fallback. The selected source serves the tickers in its
224
- // `supportedTickers(host)`; `source` is tracked alongside the group (rather
225
- // than read back off `rule.kind`, typed as the broader PriceUpdateRuleKind)
226
- // so the provider lookup below has an OracleSource to key on without a cast.
227
- const selectedRule = (0, rule_registry_ts_1.resolveOracleRule)(host.oracleSource, opts.ruleOverrides);
228
- const selectedSupported = new Set(selectedRule.supportedTickers(host));
229
- // Fail the tx-build (NOT client init, NOT a silent reroute) when the selected
230
- // source has no feed for a requested ticker that actually needs a price
236
+ // The fed set is a LIST (`host.oracleSources`, normalized + deduped at
237
+ // client creation): ONE build carries every listed source's data, and the
238
+ // chain's per-ticker weight tables decide which contributions count
239
+ // feeding an unweighted rule's PRICE is dropped on-chain, while starving a
240
+ // weighted one aborts. That asymmetry is what makes weight migrations
241
+ // safe: flip weights per ticker at any time while the fed set stays a
242
+ // superset of every ticker's weighted set. (One caveat: waterx's feed call
243
+ // burns a per-symbol signed-timestamp high-water mark regardless of
244
+ // weights — see aggregateTicker's waterx branch.) Still NO fallback
245
+ // BETWEEN sources: each group serves only the tickers its own feeds list.
246
+ // Zero-ticker groups are dropped here so everything downstream (fee check,
247
+ // fetch fan-out, update-leg build) can assume every group has work.
248
+ const groups = host.oracleSources
249
+ .map((source) => {
250
+ const rule = (0, rule_registry_ts_1.resolveOracleRule)(source, opts.ruleOverrides);
251
+ const supported = new Set(rule.supportedTickers(host));
252
+ return { source, rule, tickers: tickers.filter((t) => supported.has(t)) };
253
+ })
254
+ .filter((group) => group.tickers.length > 0);
255
+ // Fail the tx-build (NOT client init, NOT a silent reroute) when NO listed
256
+ // source has a feed for a requested ticker that actually needs a price
231
257
  // update. Only a CONSTANT-ONLY ticker is exempt — priced entirely by
232
258
  // `constant_rule`, it needs no update leg from any source. A DUAL-FEED ticker
233
259
  // (constant AND pyth) still needs its Pyth leg refreshed, so `isConstantTicker`
234
- // alone must NOT exempt it: under a source that can't serve it, with no
235
- // fallback, feeding an unrefreshed Pyth leg would price it stale (or abort on a
236
- // missing weighted source). `priceInfoByTicker.has(t)` ⇔ the ticker has a
237
- // `pyth_rule.feeds` entry, so `constant && !hasPyth` is exactly constant-only.
238
- // This catches a MISSING feed; a present-but-WRONG feed id is deliberately not
239
- // validated here (it aborts on-chain at dry-run).
260
+ // alone must NOT exempt it: with no source able to serve it, feeding an
261
+ // unrefreshed Pyth leg would price it stale (or abort on a missing weighted
262
+ // source). `priceInfoByTicker.has(t)` ⇔ the ticker has a `pyth_rule.feeds`
263
+ // entry, so `constant && !hasPyth` is exactly constant-only. This catches a
264
+ // MISSING feed; a present-but-WRONG feed id is deliberately not validated
265
+ // here (it aborts on-chain at dry-run).
266
+ const covered = new Set(groups.flatMap((group) => group.tickers));
240
267
  const isConstantOnly = (t) => host.isConstantTicker(t) && !priceInfoByTicker.has(t);
241
- const unservable = tickers.filter((t) => !selectedSupported.has(t) && !isConstantOnly(t));
268
+ const unservable = tickers.filter((t) => !covered.has(t) && !isConstantOnly(t));
242
269
  if (unservable.length > 0) {
243
- throw new Error(`oracleSource '${host.oracleSource}' has no feed configured for ticker(s): ` +
270
+ const sources = host.oracleSources.join(", ");
271
+ throw new Error(`oracleSource [${sources}] has no feed configured for ticker(s): ` +
244
272
  `${unservable.join(", ")}. Sources are self-contained with no fallback — add ` +
245
- `${host.oracleSource} feeds for them, or select a source that serves them.`);
273
+ `feeds for them under a listed source, or list a source that serves them.`);
246
274
  }
247
- const selectedGroup = tickers.filter((t) => selectedSupported.has(t));
248
- // Fee-source pre-check, hoisted ABOVE the off-chain fetch and PTB build below.
249
- // It consults only `rule.requiresFeeSource` known before any fetch or PTB
250
- // mutation — so a fee-charging source with no `feeSource` throws with ZERO
251
- // wasted network calls and zero PTB commands, rather than waiting for
252
- // `buildPythPriceUpdateCalls`'s own per-call guard to fire after the off-chain
253
- // fetch already ran.
254
- if (selectedGroup.length > 0 && !opts.feeSource && selectedRule.requiresFeeSource) {
275
+ // Fee-source pre-check, hoisted ABOVE the off-chain fetches and PTB build
276
+ // below. It consults only `rule.requiresFeeSource` known before any fetch
277
+ // or PTB mutationso a fee-charging source (Pyth Core) in the fed set with
278
+ // no `feeSource` throws with ZERO wasted network calls and zero PTB
279
+ // commands, rather than waiting for `buildPythPriceUpdateCalls`'s own
280
+ // per-call guard to fire after the off-chain fetches already ran.
281
+ if (!opts.feeSource && groups.some((group) => group.rule.requiresFeeSource)) {
255
282
  throw new pyth_ts_1.OracleFeeSourceUnavailableError();
256
283
  }
257
- // Resolve + build the selected source's update leg. The off-chain fetch
258
- // settles before the first PTB mutation, so a fetch failure never strands
259
- // moveCalls in a caller-owned tx. Map each lazer-served ticker to the one
260
- // verified `Update` PTB value for the collector-feed leg below.
284
+ // Phase 1 resolve every group's update data IN PARALLEL: the per-source
285
+ // fetches (Hermes VAA / Lazer POST / quote-center GET) are independent
286
+ // network calls on the tx-build money path, so a multi-source fed set must
287
+ // not pay one RTT per source sequentially. ALL fetches settle before the
288
+ // first PTB mutation below, so a fetch failure never strands moveCalls in a
289
+ // caller-owned tx — and a failure in ANY group fails the whole build (a
290
+ // listed source is load-bearing; silently building without it would starve
291
+ // its weighted tickers on-chain).
292
+ const dataByGroup = await Promise.all(groups.map((group) => resolveGroupUpdateData(host, group, opts.updateDataProvider)));
293
+ // Phase 2 — build each group's update leg sequentially, in list order, so
294
+ // PTB command order stays deterministic. The carry step below is an
295
+ // exhaustive switch over the group's rule kind: a future source whose feed
296
+ // leg needs per-ticker data from its update leg must decide its carry here
297
+ // — falling through silently would starve its weighted tickers on-chain.
261
298
  const lazerUpdateByTicker = new Map();
262
299
  // Signed batch envelope per waterx-served ticker. Unlike Lazer's shared PTB
263
300
  // handle, waterx's verify+feed is bundled into `collect_batch_latest` in the
264
301
  // per-ticker feed leg, so its `buildUpdateCalls` emits nothing and the
265
302
  // envelope is carried straight from the group's fetched data.
266
303
  const waterxEnvelopeByTicker = new Map();
267
- if (selectedGroup.length > 0) {
268
- const group = { source: host.oracleSource, rule: selectedRule, tickers: selectedGroup };
269
- const data = await resolveGroupUpdateData(host, group, opts.updateDataProvider);
270
- const handle = (await selectedRule.buildUpdateCalls(tx, host, data, {
304
+ for (const [i, group] of groups.entries()) {
305
+ const data = dataByGroup[i] ?? null;
306
+ const handle = (await group.rule.buildUpdateCalls(tx, host, data, {
271
307
  cache: opts.cache,
272
308
  feeSource: opts.feeSource,
273
309
  })) ?? undefined;
274
- // Route by the handle's kind discriminant — the one site the tag exists to
275
- // protect: a future non-lazer handle must never be silently fed into
276
- // pyth_lazer_rule::feed.
277
- if (handle?.kind === "pyth_lazer_rule") {
278
- for (const ticker of selectedGroup)
279
- lazerUpdateByTicker.set(ticker, handle.update);
280
- }
281
- // waterx_rule emits no shared handle (verify+feed is bundled into the
282
- // per-ticker `collect_batch_latest`), so the envelope is carried straight
283
- // from this group's fetched data to the feed leg below.
284
- if (selectedRule.kind === "waterx_rule") {
285
- const envelope = (0, waterx_rule_ts_1.waterxEnvelopeOf)(data);
286
- if (envelope) {
287
- for (const ticker of selectedGroup)
288
- waterxEnvelopeByTicker.set(ticker, envelope);
310
+ switch (group.rule.kind) {
311
+ case "pyth_rule":
312
+ // Core's update leg wrote the PriceInfoObjects in place — the feed
313
+ // leg reads them by id (`priceInfoByTicker`), nothing to carry.
314
+ break;
315
+ case "pyth_lazer_rule":
316
+ // Route by the handle's kind discriminant — the tag exists so a
317
+ // non-lazer handle can never be silently fed into
318
+ // pyth_lazer_rule::feed.
319
+ if (handle?.kind === "pyth_lazer_rule") {
320
+ for (const ticker of group.tickers)
321
+ lazerUpdateByTicker.set(ticker, handle.update);
322
+ }
323
+ break;
324
+ case "waterx_rule": {
325
+ // waterx_rule emits no shared handle (verify+feed is bundled into the
326
+ // per-ticker `collect_batch_latest`), so the envelope is carried
327
+ // straight from this group's fetched data to the feed leg below.
328
+ const envelope = (0, waterx_rule_ts_1.waterxEnvelopeOf)(data);
329
+ if (envelope) {
330
+ for (const ticker of group.tickers)
331
+ waterxEnvelopeByTicker.set(ticker, envelope);
332
+ }
333
+ break;
334
+ }
335
+ default: {
336
+ const exhausted = group.rule.kind;
337
+ throw new Error(`refreshOraclePrices: unhandled rule kind '${String(exhausted)}'`);
289
338
  }
290
339
  }
291
340
  }
@@ -11,7 +11,6 @@
11
11
  */
12
12
  import type { BasePackageEntry } from "../account/config.ts";
13
13
  import type { BaseLineConfig } from "../base-client.ts";
14
- import type { Network } from "../constants.ts";
15
14
  import type { FetchPolicy } from "./update-fetch.ts";
16
15
  export interface PythRulePackage extends BasePackageEntry {
17
16
  config: string;
@@ -31,7 +30,7 @@ export interface PythSponsorRulePackage extends BasePackageEntry {
31
30
  *
32
31
  * `enabled` mirrors the JSON field verbatim but MUST NOT be read for routing —
33
32
  * which rule prices a ticker is decided solely by the client's `oracleSource`
34
- * create option (see `OracleHost.oracleSource`), never by this flag or any
33
+ * create option (see `OracleHost.oracleSources`), never by this flag or any
35
34
  * other config value.
36
35
  */
37
36
  export interface PythLazerRulePackage extends BasePackageEntry {
@@ -99,11 +98,12 @@ export interface WaterxRuleFeedEntry {
99
98
  * Read by `WaterxRule` (`rules/waterx-rule.ts`): `feeds` for ticker support,
100
99
  * `config`/`enclave_config`/`enclave` for the `collect_batch_latest` call,
101
100
  * `published_at` for the package address. The off-chain signed price is pulled
102
- * from the quote-center (endpoint from {@link WATERX_DEFAULTS}), not this JSON.
101
+ * from the quote-center (endpoint from the rule-owned `WATERX_INFRA` table in
102
+ * `rules/waterx-rule.ts`), not this JSON.
103
103
  *
104
104
  * `enabled` mirrors the JSON field verbatim but MUST NOT be read for routing —
105
105
  * which rule prices a ticker is decided solely by the client's `oracleSource`
106
- * create option (see `OracleHost.oracleSource`), mirroring `pyth_lazer_rule`.
106
+ * create option (see `OracleHost.oracleSources`), mirroring `pyth_lazer_rule`.
107
107
  */
108
108
  export interface WaterxRulePackage extends BasePackageEntry {
109
109
  /** Shared `waterx_rule::Config` (per-symbol on-chain feed_config). */
@@ -137,17 +137,6 @@ export interface OraclePackages {
137
137
  waterx_rule?: WaterxRulePackage;
138
138
  waterx_oracle: WaterxOraclePackage;
139
139
  }
140
- /**
141
- * Resolved Pyth Core infra as it lives on `client.pyth` — NOT a config-JSON
142
- * shape. `state_id` / `wormhole_state_id` / `hermes_endpoint` come verbatim
143
- * from the fixed per-network constant ({@link PYTH_DEFAULTS}); `api_key` /
144
- * `fetch` are layered on from the caller's `pythApiKey` / `pythFetch` create
145
- * options. None of it is sourced from the canonical `waterx-config` JSON — the
146
- * SDK never reads a `pyth` block there (a Bearer secret has no place in a
147
- * public CDN document). The infra is the same for every `oracleSource`; the
148
- * `pyth_lazer_rule` source reads only the `api_key` / `fetch` from here and
149
- * gets its on-chain infra from {@link LAZER_DEFAULTS} + config instead.
150
- */
151
140
  /**
152
141
  * The caller-tunable subset of `fetchWithPolicy`'s policy exposed on the
153
142
  * `pythFetch` create option and `client.pyth.fetch` — the retry/timeout budget
@@ -160,18 +149,24 @@ export type PythFetchPolicy = {
160
149
  timeoutMs?: number;
161
150
  retries?: number;
162
151
  };
163
- export interface PythInfraConfig {
164
- state_id: string;
165
- wormhole_state_id: string;
166
- hermes_endpoint: string;
152
+ /**
153
+ * `client.pyth` — ONLY the caller-supplied Pyth credential + fetch policy,
154
+ * shared by the Pyth-family rules (`pyth_rule`, `pyth_lazer_rule`). It carries
155
+ * NO endpoints and NO on-chain object ids: every oracle source owns its own
156
+ * infra, co-located with its rule (`PYTH_CORE_INFRA` in `oracle/pyth.ts`;
157
+ * the Lazer constants inside `rules/pyth-lazer-rule.ts`). A non-Pyth source
158
+ * never reads this slice.
159
+ * Nothing here is sourced from the canonical `waterx-config` JSON — a Bearer
160
+ * secret has no place in a public CDN document.
161
+ */
162
+ export interface PythAccessConfig {
167
163
  /**
168
- * Pyth Pro / Lazer access token (`Authorization: Bearer …`) for
164
+ * Pyth access token (`Authorization: Bearer …`). Required by
169
165
  * `PythLazerRule`'s signed-update fetch — Lazer is auth-first, so there is
170
- * no keyless default. Optional: Pyth-Core-only deployments never need it.
171
- * Supplied via the `pythApiKey` create option (the SDK never reads
172
- * `process.env` or the config JSON). Absent when a lazer-routed fetch runs →
173
- * `LazerApiKeyMissing` is thrown at fetch time. As of the Pyth Pro
174
- * migration (post-2026-08-18, per
166
+ * no keyless default; absent when a lazer-routed fetch runs
167
+ * `LazerApiKeyMissing` is thrown at fetch time. Supplied via the
168
+ * `pythApiKey` create option (the SDK never reads `process.env` or the
169
+ * config JSON). As of the Pyth Pro migration (post-2026-08-18, per
175
170
  * https://docs.pyth.network/price-feeds/core/upgrade) this is ALSO required
176
171
  * for `pyth_rule`'s Hermes fetch (`fetchPriceFeedsUpdateData`) — see
177
172
  * `fetch` below.
@@ -187,66 +182,37 @@ export interface PythInfraConfig {
187
182
  */
188
183
  fetch?: PythFetchPolicy;
189
184
  }
190
- export declare const PYTH_DEFAULTS: Record<Network, PythInfraConfig>;
191
- /**
192
- * Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
193
- * Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
194
- * operates (not part of the `waterx-config` JSON). A fuller `PYTH_INFRA`
195
- * restructure is deferred — this stays a minimal map until then.
196
- *
197
- * - `endpoint` — Lazer HTTP API base; signed updates come from
198
- * `POST /v1/latest_price` (Bearer-authenticated). The service is
199
- * network-agnostic (one signed payload verifies on any chain that trusts the
200
- * Lazer signers), so both networks share the production host.
201
- * - `verifier_package` — the Sui package carrying
202
- * `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
203
- * still the original v1 publish; mainnet is the v2-upgraded package (which
204
- * still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
205
- * contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
206
- */
207
- export declare const LAZER_DEFAULTS: Record<Network, {
208
- endpoint: string;
209
- verifier_package: string;
210
- }>;
211
185
  /**
212
- * Resolved WaterX quote-center infra for `WaterxRule` — the network default
213
- * from {@link WATERX_DEFAULTS}, with both fields overridable at client init
214
- * (`waterxEndpoint` / `waterxFetch`).
186
+ * `client.waterx` ONLY the caller-supplied quote-center overrides for
187
+ * `WaterxRule`, mirroring {@link PythAccessConfig}: no resolved infra lives on
188
+ * the client. When a field is unset the rule resolves it against its OWN
189
+ * per-network table (`WATERX_INFRA` in `rules/waterx-rule.ts`) — no other
190
+ * source's endpoint or policy is ever consulted.
215
191
  *
216
- * The override exists because this is the one oracle source a BROWSER fetches
217
- * itself: the rule pulls the signed envelope from the page, so it is subject to
218
- * the quote-center deployment's CORS allowlist. A front end whose origin is not
219
- * on that list — or one that must route egress through its own backend — points
220
- * `endpoint` at a same-origin proxy (or supplies `fetch.fetchImpl`) instead of
221
- * being locked to the hardcoded host.
192
+ * The endpoint override exists because this is the one oracle source a BROWSER
193
+ * fetches itself: the rule pulls the signed envelope from the page, so it is
194
+ * subject to the quote-center deployment's CORS allowlist. A front end whose
195
+ * origin is not on that list — or one that must route egress through its own
196
+ * backend — points `endpoint` at a same-origin proxy (or supplies
197
+ * `fetch.fetchImpl`) instead of being locked to the default host.
222
198
  */
223
- export interface WaterxInfraConfig {
199
+ export interface WaterxAccessConfig {
224
200
  /**
225
- * Quote-center base URL. A base PATH is preserved — the rule appends via
226
- * `joinEndpointPath`, so `https://app.example/api/quote-center` resolves to
201
+ * Quote-center base URL override (`waterxEndpoint` create option). A base
202
+ * PATH is preserved — the rule appends via `joinEndpointPath`, so
203
+ * `https://app.example/api/quote-center` resolves to
227
204
  * `…/api/quote-center/v1/quotes/update` and a proxy route is not rewritten
228
205
  * away. A trailing slash is trimmed.
229
206
  */
230
- endpoint: string;
207
+ endpoint?: string;
231
208
  /**
232
209
  * Retry/timeout policy (and `fetchImpl`) for the quote-center fetch — see
233
210
  * `fetchWithPolicy` (`./update-fetch.ts`). Supplied via the `waterxFetch`
234
- * create option. When unset the rule falls back to the shared `pyth.fetch`
235
- * policy, then to `fetchWithPolicy`'s defaults (15s timeout, 2 retries).
211
+ * create option. Falls back to `fetchWithPolicy`'s built-in defaults (15s
212
+ * timeout, 2 retries) when unset never to another source's policy.
236
213
  */
237
214
  fetch?: FetchPolicy;
238
215
  }
239
- /**
240
- * WaterX quote-center base URL by network — the first-party TEE-signed price
241
- * hub `WaterxRule` pulls from (`GET /v1/quotes/update?symbols=…`). Mirrors
242
- * {@link LAZER_DEFAULTS}: infra WaterX operates, not part of the `waterx-config`
243
- * JSON. Public read (no auth), so there is no api_key. `endpoint` has no
244
- * trailing slash — the rule appends the path.
245
- *
246
- * These are DEFAULTS, not a hard pin: a consumer overrides them per client via
247
- * `waterxEndpoint` / `waterxFetch` (resolved onto `client.waterx`).
248
- */
249
- export declare const WATERX_DEFAULTS: Record<Network, WaterxInfraConfig>;
250
216
  /**
251
217
  * The narrow config shape the oracle/refresh code needs. `WaterXConfig`
252
218
  * (the perp line's full config) is assignable to this, so `PerpClient` satisfies
@@ -11,59 +11,3 @@
11
11
  * mirrors the earlier account-config hoist.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.WATERX_DEFAULTS = exports.LAZER_DEFAULTS = exports.PYTH_DEFAULTS = void 0;
15
- exports.PYTH_DEFAULTS = {
16
- MAINNET: {
17
- state_id: "0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8",
18
- wormhole_state_id: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
19
- hermes_endpoint: "https://hermes.pyth.network",
20
- },
21
- TESTNET: {
22
- state_id: "0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c",
23
- wormhole_state_id: "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
24
- hermes_endpoint: "https://hermes-beta.pyth.network",
25
- },
26
- };
27
- // ============================================================================
28
- // Pyth Lazer — external infra, defaults by network
29
- // ============================================================================
30
- /**
31
- * Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
32
- * Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
33
- * operates (not part of the `waterx-config` JSON). A fuller `PYTH_INFRA`
34
- * restructure is deferred — this stays a minimal map until then.
35
- *
36
- * - `endpoint` — Lazer HTTP API base; signed updates come from
37
- * `POST /v1/latest_price` (Bearer-authenticated). The service is
38
- * network-agnostic (one signed payload verifies on any chain that trusts the
39
- * Lazer signers), so both networks share the production host.
40
- * - `verifier_package` — the Sui package carrying
41
- * `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
42
- * still the original v1 publish; mainnet is the v2-upgraded package (which
43
- * still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
44
- * contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
45
- */
46
- exports.LAZER_DEFAULTS = {
47
- MAINNET: {
48
- endpoint: "https://pyth-lazer.dourolabs.app",
49
- verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
50
- },
51
- TESTNET: {
52
- endpoint: "https://pyth-lazer.dourolabs.app",
53
- verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
54
- },
55
- };
56
- /**
57
- * WaterX quote-center base URL by network — the first-party TEE-signed price
58
- * hub `WaterxRule` pulls from (`GET /v1/quotes/update?symbols=…`). Mirrors
59
- * {@link LAZER_DEFAULTS}: infra WaterX operates, not part of the `waterx-config`
60
- * JSON. Public read (no auth), so there is no api_key. `endpoint` has no
61
- * trailing slash — the rule appends the path.
62
- *
63
- * These are DEFAULTS, not a hard pin: a consumer overrides them per client via
64
- * `waterxEndpoint` / `waterxFetch` (resolved onto `client.waterx`).
65
- */
66
- exports.WATERX_DEFAULTS = {
67
- MAINNET: { endpoint: "https://quote-center.waterx.app" },
68
- TESTNET: { endpoint: "https://quote-center-staging.waterx.app" },
69
- };
@@ -9,37 +9,41 @@
9
9
  */
10
10
  import type { SuiGrpcClient } from "@mysten/sui/grpc";
11
11
  import type { Network } from "../constants.ts";
12
- import type { OracleConfig, PythInfraConfig, WaterxInfraConfig } from "./config.ts";
12
+ import type { OracleConfig, PythAccessConfig, WaterxAccessConfig } from "./config.ts";
13
13
  import type { OracleSource } from "./price-update-rule.ts";
14
14
  export interface OracleHost {
15
- /** Sui network this client targets — selects per-network external-infra defaults (e.g. `LAZER_DEFAULTS`). */
15
+ /** Sui network this client targets — each rule keys its OWN infra table by it (`PYTH_CORE_INFRA`, `LAZER_INFRA`). */
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 fixed per `(network, generation)`; api_key/fetch layered from create options. */
20
- readonly pyth: PythInfraConfig;
19
+ /** Caller-supplied Pyth credential + fetch policy (create options) NO endpoints, NO object ids. */
20
+ readonly pyth: PythAccessConfig;
21
21
  /**
22
- * WaterX quote-center infra for `WaterxRule` — endpoint + fetch policy,
23
- * resolved from the `waterxEndpoint` / `waterxFetch` create options.
24
- *
25
- * OPTIONAL so an existing host stays a valid `OracleHost`: when absent the
26
- * rule falls back to `WATERX_DEFAULTS[network]`. This is the hook a browser
27
- * consumer uses to route the quote-center fetch through a same-origin proxy
28
- * (`endpoint`) or its own transport (`fetch.fetchImpl`) — that request is
29
- * made from the page, so it is bound by the quote-center's CORS allowlist.
22
+ * Caller-supplied WaterX quote-center overrides for `WaterxRule`
23
+ * (`waterxEndpoint` / `waterxFetch` create options) — access-only, mirroring
24
+ * `pyth` above. OPTIONAL so an existing host stays a valid `OracleHost`;
25
+ * unset fields resolve against the rule's own `WATERX_INFRA[network]` table.
26
+ * This is the hook a browser consumer uses to route the quote-center fetch
27
+ * through a same-origin proxy (`endpoint`) or its own transport
28
+ * (`fetch.fetchImpl`) — that request is made from the page, so it is bound
29
+ * by the quote-center's CORS allowlist.
30
30
  */
31
- readonly waterx?: WaterxInfraConfig;
31
+ readonly waterx?: WaterxAccessConfig;
32
32
  /** gRPC client for the on-chain reads the Pyth update path needs. */
33
33
  readonly grpcClient: SuiGrpcClient;
34
34
  /**
35
- * Client-selected oracle rule source for `refreshOraclePrices`'s on-chain
36
- * update leg resolved at client creation from the `oracleSource` create
37
- * option (default `'pyth_rule'`). Routing is driven by this value ALONE:
38
- * never by a config JSON `enabled` flag (e.g. a future `pyth_lazer_rule.enabled`)
39
- * and never by `process.env` the SDK never reads it; consumers (BE/FE) wire
40
- * this option from their own env var.
35
+ * The FED SET for `refreshOraclePrices`'s update legs — the REQUIRED
36
+ * `oracleSource` create option normalized to a non-empty, deduped list.
37
+ * Every listed source's data is fetched and fed in one build; the chain's
38
+ * per-ticker weight tables decide which contributions count (feeding an
39
+ * unweighted rule is dropped on-chain; starving a weighted one aborts), so
40
+ * during weight migrations the list stays a SUPERSET of every ticker's
41
+ * weighted set. Routing is driven by this value ALONE: never by a config
42
+ * JSON `enabled` flag and never by `process.env` — the SDK never reads it;
43
+ * consumers (BE/FE) wire this option from their own env var
44
+ * (`ORACLE_SOURCE`, comma-separated).
41
45
  */
42
- readonly oracleSource: OracleSource;
46
+ readonly oracleSources: readonly OracleSource[];
43
47
  /** True when `ticker` is priced by `constant_rule`. */
44
48
  isConstantTicker(ticker: string): boolean;
45
49
  /** The `supra_rule` config when deployed, enabled, and fully wired; else `undefined`. */
@@ -18,15 +18,17 @@
18
18
  export type { OracleHost } from "./host.ts";
19
19
  export { FetchPolicyError, fetchWithPolicy, joinEndpointPath } from "./update-fetch.ts";
20
20
  export type { FetchPolicy } from "./update-fetch.ts";
21
- export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls, updatePythPrices, HermesEndpointRejectedAllFeedsError, MISSING_FEED_MEMO_TTL_MS, OracleFeeSourceUnavailableError, } from "./pyth.ts";
21
+ export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls, pythCoreHermesEndpoint, pythProHermesEndpoint, PYTH_PRO_HERMES_ENDPOINT, 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
+ export { resolveOracleReadPlan, resolveHermesReadEndpoint } from "./read-plane.ts";
25
+ export type { OracleReadPlan } from "./read-plane.ts";
24
26
  export { PythCoreRule } from "./rules/pyth-core-rule.ts";
25
27
  export type { PythCoreUpdatePayload } from "./rules/pyth-core-rule.ts";
26
28
  export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.ts";
27
29
  export type { PythLazerUpdatePayload } from "./rules/pyth-lazer-rule.ts";
28
- export { WaterxRule, parseSignedEnvelope } from "./rules/waterx-rule.ts";
30
+ export { WaterxRule, parseSignedEnvelope, BATCH_PRICE_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint, } from "./rules/waterx-rule.ts";
29
31
  export type { WaterxUpdatePayload, WaterxSignedEnvelope, WaterxBatchItem, } from "./rules/waterx-rule.ts";
30
- export { OracleSourceNotImplementedError } from "./rule-registry.ts";
32
+ export { OracleSourceNotImplementedError, resolveOracleRule } from "./rule-registry.ts";
31
33
  export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.ts";
32
34
  export { openPythSponsorFund, reimbursePythSponsor } from "./rules/sponsor.ts";