@waterx/sdk 3.1.1 → 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 (76) hide show
  1. package/README.md +54 -18
  2. package/dist/cjs/src/account/config.d.ts +0 -16
  3. package/dist/cjs/src/oracle/aggregate.d.ts +96 -17
  4. package/dist/cjs/src/oracle/aggregate.js +179 -21
  5. package/dist/cjs/src/oracle/config.d.ts +85 -2
  6. package/dist/cjs/src/oracle/config.js +30 -1
  7. package/dist/cjs/src/oracle/host.d.ts +14 -1
  8. package/dist/cjs/src/oracle/index.d.ts +19 -5
  9. package/dist/cjs/src/oracle/index.js +54 -6
  10. package/dist/cjs/src/oracle/price-update-rule.d.ts +180 -0
  11. package/dist/cjs/src/oracle/price-update-rule.js +56 -0
  12. package/dist/cjs/src/oracle/pyth.d.ts +142 -11
  13. package/dist/cjs/src/oracle/pyth.js +408 -25
  14. package/dist/cjs/src/oracle/rule-registry.d.ts +41 -0
  15. package/dist/cjs/src/oracle/rule-registry.js +65 -0
  16. package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +15 -0
  17. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +99 -0
  18. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
  19. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +197 -0
  20. package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -0
  21. package/dist/cjs/src/oracle/rules/sponsor.d.ts +11 -7
  22. package/dist/cjs/src/oracle/rules/sponsor.js +11 -7
  23. package/dist/cjs/src/oracle/update-fetch.d.ts +115 -0
  24. package/dist/cjs/src/oracle/update-fetch.js +285 -0
  25. package/dist/cjs/src/perp/client.d.ts +40 -5
  26. package/dist/cjs/src/perp/client.js +18 -3
  27. package/dist/cjs/src/perp/config.d.ts +10 -7
  28. package/dist/cjs/src/perp/config.js +77 -17
  29. package/dist/cjs/src/perp/index.d.ts +3 -2
  30. package/dist/cjs/src/perp/index.js +6 -2
  31. package/dist/cjs/src/perp/tx-builders/common.d.ts +52 -15
  32. package/dist/cjs/src/perp/tx-builders/common.js +39 -6
  33. package/dist/cjs/src/perp/tx-builders/wlp.d.ts +11 -3
  34. package/dist/cjs/src/perp/tx-builders/wlp.js +29 -3
  35. package/dist/cjs/src/perp/tx-builders.d.ts +3 -3
  36. package/dist/cjs/src/perp/tx-builders.js +3 -3
  37. package/dist/cjs/src/unified-client.d.ts +25 -0
  38. package/dist/cjs/src/unified-client.js +3 -0
  39. package/dist/src/account/config.d.ts +0 -16
  40. package/dist/src/oracle/aggregate.d.ts +96 -17
  41. package/dist/src/oracle/aggregate.js +180 -22
  42. package/dist/src/oracle/config.d.ts +85 -2
  43. package/dist/src/oracle/config.js +29 -0
  44. package/dist/src/oracle/host.d.ts +14 -1
  45. package/dist/src/oracle/index.d.ts +19 -5
  46. package/dist/src/oracle/index.js +42 -6
  47. package/dist/src/oracle/price-update-rule.d.ts +180 -0
  48. package/dist/src/oracle/price-update-rule.js +53 -0
  49. package/dist/src/oracle/pyth.d.ts +142 -11
  50. package/dist/src/oracle/pyth.js +402 -24
  51. package/dist/src/oracle/rule-registry.d.ts +41 -0
  52. package/dist/src/oracle/rule-registry.js +60 -0
  53. package/dist/src/oracle/rules/pyth-core-rule.d.ts +15 -0
  54. package/dist/src/oracle/rules/pyth-core-rule.js +96 -0
  55. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
  56. package/dist/src/oracle/rules/pyth-lazer-rule.js +192 -0
  57. package/dist/src/oracle/rules/pyth-rule.js +5 -0
  58. package/dist/src/oracle/rules/sponsor.d.ts +11 -7
  59. package/dist/src/oracle/rules/sponsor.js +11 -7
  60. package/dist/src/oracle/update-fetch.d.ts +115 -0
  61. package/dist/src/oracle/update-fetch.js +277 -0
  62. package/dist/src/perp/client.d.ts +40 -5
  63. package/dist/src/perp/client.js +18 -3
  64. package/dist/src/perp/config.d.ts +10 -7
  65. package/dist/src/perp/config.js +77 -17
  66. package/dist/src/perp/index.d.ts +3 -2
  67. package/dist/src/perp/index.js +1 -1
  68. package/dist/src/perp/tx-builders/common.d.ts +52 -15
  69. package/dist/src/perp/tx-builders/common.js +39 -6
  70. package/dist/src/perp/tx-builders/wlp.d.ts +11 -3
  71. package/dist/src/perp/tx-builders/wlp.js +29 -3
  72. package/dist/src/perp/tx-builders.d.ts +3 -3
  73. package/dist/src/perp/tx-builders.js +3 -3
  74. package/dist/src/unified-client.d.ts +25 -0
  75. package/dist/src/unified-client.js +3 -0
  76. package/package.json +1 -1
@@ -2,38 +2,98 @@
2
2
  * Oracle aggregation — the orchestrator that composes rules into the shared
3
3
  * `Oracle`. This is the ONE file that knows about every rule: it builds a
4
4
  * `PriceCollector`, feeds whichever rules a ticker is configured for
5
- * (Pyth / Supra / Constant), then `aggregate`s.
5
+ * (Pyth / Lazer / Supra / Constant), then `aggregate`s.
6
6
  *
7
7
  * Per ticker:
8
8
  * collector = oracle::new_collector(ticker)
9
9
  * [pyth_rule::feed] when the ticker has a pyth_rule.feeds entry
10
+ * [pyth_lazer_rule::feed] when the update leg produced a verified lazer Update
10
11
  * [supra_rule::feed] when supra is enabled + wired
11
12
  * [constant_rule::feed] when the ticker is a constant ticker
12
13
  * oracle::aggregate(oracle, collector)
13
14
  *
14
- * The fed rule set must match the on-chain weighted set for the ticker —
15
+ * The fed rule set must cover the on-chain weighted set for the ticker —
15
16
  * `aggregator::remove_outliers` aborts `EMissingPriceSource` if a weighted rule
16
- * is missing from the collector.
17
+ * is missing from the collector (an abstaining feed call counts as present;
18
+ * a fed-but-unweighted rule is silently dropped).
19
+ *
20
+ * `refreshOraclePrices` additionally routes the on-chain price *update* leg
21
+ * (the fetch + verify/push step, before any of the above feeding) through the
22
+ * `PriceUpdateRule` selected by `host.oracleSource` — see `rule-registry.ts`.
17
23
  */
18
24
  import { aggregate as aggregateCall, newCollector } from "../generated/waterx_oracle/oracle.js";
19
- import { updatePythPrices } from "./pyth.js";
25
+ import { OracleFeeSourceUnavailableError } from "./pyth.js";
26
+ import { resolveOracleRule } from "./rule-registry.js";
20
27
  import { feedConstantRule } from "./rules/constant-rule.js";
28
+ import { feedLazerRule } from "./rules/pyth-lazer-rule.js";
21
29
  import { feedPythRule } from "./rules/pyth-rule.js";
22
30
  import { maybeFeedSupra } from "./rules/supra-rule.js";
31
+ /**
32
+ * Resolve one group's off-chain update payload for {@link refreshOraclePrices}:
33
+ * try `provider.get(source, tickers)` first (when a provider is configured),
34
+ * falling back to the group's own live `rule.fetchUpdateData` on a cache miss
35
+ * (`null`) or a throw from the provider — a broken/degraded cache must never
36
+ * break the money path.
37
+ *
38
+ * A cache HIT is treated as a payload for a POSSIBLY-WIDER ticker set (a
39
+ * provider commonly caches one whole-universe payload per source — see
40
+ * {@link UpdateDataProvider}), so it is narrowed to exactly `group.tickers`
41
+ * via `rule.narrowUpdateData` before use. This is load-bearing, not
42
+ * defensive: without it a Pyth Core hit would emit an
43
+ * `update_single_price_feed` — and charge its fee — for every cached feed
44
+ * instead of just this group's, and a payload that cannot cover the group
45
+ * (`narrowUpdateData` → `null`) would never reach the live-fetch fallback.
46
+ * Each rule owns its own subsetting (Core subsets per-feed entries; Lazer's
47
+ * indivisible payload passes whole iff fully covered), so the orchestrator
48
+ * never branches on `kind` here. A hit whose `kind` doesn't match the
49
+ * group's rule is a caller bug (the provider handed back a different rule's
50
+ * payload), so that throws — via `narrowUpdateData`'s own
51
+ * `assertRuleUpdateData` guard — instead of silently falling back.
52
+ */
53
+ async function resolveGroupUpdateData(host, group, provider) {
54
+ if (provider) {
55
+ let cached = null;
56
+ try {
57
+ cached = await provider.get(group.source, group.tickers);
58
+ }
59
+ catch {
60
+ // Provider errors must never break the money path — fall through to
61
+ // the live fetch below exactly as a cache miss (`null`) would.
62
+ }
63
+ if (cached !== null) {
64
+ // Wrong-kind hit throws inside narrowUpdateData (assertRuleUpdateData);
65
+ // a hit that can't cover the group narrows to null → live-fetch below.
66
+ const narrowed = group.rule.narrowUpdateData(host, cached, group.tickers);
67
+ if (narrowed !== null)
68
+ return narrowed;
69
+ }
70
+ }
71
+ return group.rule.fetchUpdateData(host, group.tickers);
72
+ }
23
73
  /**
24
74
  * Aggregate one ticker's price into the shared `Oracle`: build a collector, feed
25
75
  * every rule the ticker is configured for, then `aggregate`.
26
76
  *
27
77
  * - **Pyth** — fed when `priceInfoObjectId` is supplied (i.e. the ticker has a
28
- * `pyth_rule.feeds` entry). Caller must run the Pyth update first so the
29
- * `PriceInfoObject` is fresh.
30
- * - **Supra** fed alongside Pyth when supra is enabled + wired (abstains
31
- * on-chain for symbols it has no pair for).
78
+ * `pyth_rule.feeds` entry). When this PTB's update leg refreshed the
79
+ * `PriceInfoObject` it contributes a fresh price; when it did not (a
80
+ * lazer-routed ticker), the on-chain rule only READS the object and abstains
81
+ * if it is stale — it never aborts so the call stays mandatory while
82
+ * `pyth_rule` remains in the ticker's on-chain weighted set
83
+ * (`EMissingPriceSource` requires every weighted rule to appear).
84
+ * - **Lazer** — fed when `lazerUpdate` is supplied: the verified
85
+ * `pyth_lazer::update::Update` produced by this PTB's lazer update leg
86
+ * (see `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
87
+ * not (yet) weight `PythLazerRule`, the contribution is silently dropped
88
+ * on-chain — feeding ahead of the weight migration is harmless.
89
+ * - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
90
+ * (abstains on-chain for symbols it has no pair for).
32
91
  * - **Constant** — fed when the ticker is a constant ticker
33
92
  * ({@link OracleHost.isConstantTicker}).
34
93
  *
35
- * "Dual-feed" (Pyth + Constant) and "constant-only" are not special cases — they
36
- * fall out of which rules the ticker is in. Throws if no rule applies.
94
+ * "Dual-feed" (Pyth + Constant, or Pyth + Lazer) and "constant-only" are not
95
+ * special cases — they fall out of which rules the ticker is in. Throws if no
96
+ * rule applies.
37
97
  */
38
98
  export function aggregateTicker(tx, host, args) {
39
99
  const oraclePkg = host.config.packages.waterx_oracle.published_at;
@@ -44,16 +104,22 @@ export function aggregateTicker(tx, host, args) {
44
104
  let fed = false;
45
105
  if (args.priceInfoObjectId) {
46
106
  feedPythRule(tx, host, collector, args.priceInfoObjectId);
107
+ fed = true;
108
+ }
109
+ if (args.lazerUpdate !== undefined) {
110
+ feedLazerRule(tx, host, collector, args.lazerUpdate);
111
+ fed = true;
112
+ }
113
+ if (fed) {
47
114
  // Supra rides on the same collector when enabled (abstains on-chain otherwise).
48
115
  maybeFeedSupra(tx, host, collector);
49
- fed = true;
50
116
  }
51
117
  if (host.isConstantTicker(args.ticker)) {
52
118
  feedConstantRule(tx, host, collector);
53
119
  fed = true;
54
120
  }
55
121
  if (!fed) {
56
- throw new Error(`no oracle rule configured for ticker '${args.ticker}' (no pyth feed, not a constant ticker)`);
122
+ throw new Error(`no oracle rule configured for ticker '${args.ticker}' (no pyth feed, no lazer update, not a constant ticker)`);
57
123
  }
58
124
  aggregateCall({
59
125
  package: oraclePkg,
@@ -89,24 +155,116 @@ export function aggregateTickerWithConstant(tx, host, args) {
89
155
  /**
90
156
  * Refresh multiple tickers in one PTB. For each ticker {@link aggregateTicker}
91
157
  * feeds whichever rules it is configured for (Pyth if it has a `pyth_rule.feeds`
92
- * entry, Supra when enabled, Constant when it's a constant ticker). Tickers with a
93
- * Pyth feed are updated on-chain via one shared Pyth accumulator first; the rest
94
- * (constant-only) skip Pyth entirely.
158
+ * entry, Lazer if the lazer update leg served it see below Supra when
159
+ * enabled, Constant when it's a constant ticker).
160
+ *
161
+ * Before that, the on-chain price *update* leg is routed by `host.oracleSource`
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.
172
+ *
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.
182
+ *
183
+ * **Collector-feed leg is rule-aware:** a lazer-served group's
184
+ * `buildUpdateCalls` returns the verified `Update` PTB value
185
+ * ({@link RuleUpdateHandle}), and every ticker in that group is aggregated
186
+ * with `lazerUpdate` set so {@link aggregateTicker} appends
187
+ * `pyth_lazer_rule::feed` against it. A lazer-routed ticker that still has a
188
+ * `pyth_rule.feeds` entry ALSO keeps its `pyth_rule::feed` leg — required
189
+ * on-chain while `pyth_rule` stays in the ticker's weighted set
190
+ * (`aggregator::remove_outliers` aborts `EMissingPriceSource` unless every
191
+ * weighted rule appears in the collector; an abstention counts as
192
+ * appearing), and safe: `pyth_rule::feed` only READS the `PriceInfoObject`
193
+ * this PTB never refreshed and abstains when it is stale rather than
194
+ * aborting. Conversely, a lazer feed call on an aggregator that does not
195
+ * (yet) weight `PythLazerRule` is silently dropped on-chain — so
196
+ * lazer-routing a ticker ahead of its on-chain weight migration prices it
197
+ * from the remaining weighted rules instead of failing.
95
198
  */
96
199
  export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
97
200
  if (tickers.length === 0)
98
201
  return;
99
- // Every ticker with a pyth_rule.feeds entry needs the on-chain Pyth update
100
- // first (one shared accumulator). Constant-only tickers (no pyth feed) skip it.
202
+ // price_info_object lookup for every ticker with a pyth_rule.feeds entry
203
+ // needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
204
+ // which rule performed the on-chain update for that ticker.
101
205
  const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
102
206
  const priceInfoByTicker = new Map();
103
- if (pythTickers.length > 0) {
104
- const entries = pythTickers.map((t) => host.getPythFeed(t));
105
- await updatePythPrices(tx, host, entries.map((e) => e.feed_id), opts.cache, opts.sponsorFund);
106
- pythTickers.forEach((t, i) => priceInfoByTicker.set(t, entries[i].price_info_object));
207
+ pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
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.
212
+ const selectedRule = resolveOracleRule(host.oracleSource, opts.ruleOverrides);
213
+ const selectedSupported = new Set(selectedRule.supportedTickers(host));
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.`);
231
+ }
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) {
240
+ throw new OracleFeeSourceUnavailableError();
241
+ }
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.
246
+ const lazerUpdateByTicker = new Map();
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, {
251
+ cache: opts.cache,
252
+ feeSource: opts.feeSource,
253
+ })) ?? undefined;
254
+ // Route by the handle's kind discriminant — the one site the tag exists to
255
+ // protect: a future non-lazer handle (e.g. a WaterxRule value) must never
256
+ // be silently fed into pyth_lazer_rule::feed.
257
+ if (handle?.kind === "pyth_lazer_rule") {
258
+ for (const ticker of selectedGroup)
259
+ lazerUpdateByTicker.set(ticker, handle.update);
260
+ }
107
261
  }
108
262
  // Aggregate each ticker, feeding whichever rules it is configured for.
109
263
  for (const ticker of tickers) {
110
- aggregateTicker(tx, host, { ticker, priceInfoObjectId: priceInfoByTicker.get(ticker) });
264
+ aggregateTicker(tx, host, {
265
+ ticker,
266
+ priceInfoObjectId: priceInfoByTicker.get(ticker),
267
+ lazerUpdate: lazerUpdateByTicker.get(ticker),
268
+ });
111
269
  }
112
270
  }
@@ -22,6 +22,24 @@ export interface PythRulePackage extends BasePackageEntry {
22
22
  export interface PythSponsorRulePackage extends BasePackageEntry {
23
23
  pyth_sponsor: string;
24
24
  }
25
+ /**
26
+ * `pyth_lazer_rule` deployment entry — present in the deployed testnet
27
+ * `waterx-config` JSON. Read by `PythLazerRule` (`rules/pyth-lazer-rule.ts`):
28
+ * `feeds` for ticker support + integer feed-id resolution, `state` for the
29
+ * verify call, `published_at`/`config` for the per-ticker feed call.
30
+ *
31
+ * `enabled` mirrors the JSON field verbatim but MUST NOT be read for routing —
32
+ * which rule prices a ticker is decided solely by the client's `oracleSource`
33
+ * create option (see `OracleHost.oracleSource`), never by this flag or any
34
+ * other config value.
35
+ */
36
+ export interface PythLazerRulePackage extends BasePackageEntry {
37
+ config: string;
38
+ state: string;
39
+ enabled?: boolean;
40
+ /** Oracle ticker → integer Pyth Lazer feed id (distinct id scheme from `pyth_rule`'s hex `feed_id`). */
41
+ feeds: Record<string, number>;
42
+ }
25
43
  /** Per-ticker `constant_rule` feed entry (mirrors the `pyth_rule.feeds` shape). */
26
44
  export interface ConstantFeedEntry {
27
45
  /**
@@ -77,16 +95,83 @@ export interface WaterxOraclePackage extends BasePackageEntry {
77
95
  export interface OraclePackages {
78
96
  pyth_rule: PythRulePackage;
79
97
  pyth_sponsor_rule?: PythSponsorRulePackage;
98
+ /** See {@link PythLazerRulePackage} — typed only, not read for routing. */
99
+ pyth_lazer_rule?: PythLazerRulePackage;
80
100
  constant_rule?: WaterxConstantRulePackage;
81
101
  supra_rule?: SupraRulePackage;
82
102
  waterx_oracle: WaterxOraclePackage;
83
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
+ };
84
127
  export interface PythInfraConfig {
85
128
  state_id: string;
86
129
  wormhole_state_id: string;
87
130
  hermes_endpoint: string;
131
+ /**
132
+ * Pyth Pro / Lazer access token (`Authorization: Bearer …`) for
133
+ * `PythLazerRule`'s signed-update fetch — Lazer is auth-first, so there is
134
+ * no keyless default. Optional: Pyth-Core-only deployments never need it.
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 →
137
+ * `LazerApiKeyMissing` is thrown at fetch time. As of the Pyth Pro
138
+ * migration (post-2026-08-18, per
139
+ * https://docs.pyth.network/price-feeds/core/upgrade) this is ALSO required
140
+ * for `pyth_rule`'s Hermes fetch (`fetchPriceFeedsUpdateData`) — see
141
+ * `fetch` below.
142
+ */
143
+ api_key?: string;
144
+ /**
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.
151
+ */
152
+ fetch?: PythFetchPolicy;
88
153
  }
89
154
  export declare const PYTH_DEFAULTS: Record<Network, PythInfraConfig>;
155
+ /**
156
+ * Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
157
+ * Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
158
+ * operates (not part of the `waterx-config` JSON). A fuller `PYTH_INFRA`
159
+ * restructure is deferred — this stays a minimal map until then.
160
+ *
161
+ * - `endpoint` — Lazer HTTP API base; signed updates come from
162
+ * `POST /v1/latest_price` (Bearer-authenticated). The service is
163
+ * network-agnostic (one signed payload verifies on any chain that trusts the
164
+ * Lazer signers), so both networks share the production host.
165
+ * - `verifier_package` — the Sui package carrying
166
+ * `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
167
+ * still the original v1 publish; mainnet is the v2-upgraded package (which
168
+ * still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
169
+ * contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
170
+ */
171
+ export declare const LAZER_DEFAULTS: Record<Network, {
172
+ endpoint: string;
173
+ verifier_package: string;
174
+ }>;
90
175
  /**
91
176
  * The narrow config shape the oracle/refresh code needs. `WaterXConfig`
92
177
  * (the perp line's full config) is assignable to this, so `PerpClient` satisfies
@@ -94,6 +179,4 @@ export declare const PYTH_DEFAULTS: Record<Network, PythInfraConfig>;
94
179
  */
95
180
  export interface OracleConfig extends BaseLineConfig {
96
181
  packages: OraclePackages;
97
- /** Pyth infra override (defaults from {@link PYTH_DEFAULTS}). */
98
- pyth?: PythInfraConfig;
99
182
  }
@@ -21,3 +21,32 @@ export const PYTH_DEFAULTS = {
21
21
  hermes_endpoint: "https://hermes-beta.pyth.network",
22
22
  },
23
23
  };
24
+ // ============================================================================
25
+ // Pyth Lazer — external infra, defaults by network
26
+ // ============================================================================
27
+ /**
28
+ * Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
29
+ * Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
30
+ * operates (not part of the `waterx-config` JSON). A fuller `PYTH_INFRA`
31
+ * restructure is deferred — this stays a minimal map until then.
32
+ *
33
+ * - `endpoint` — Lazer HTTP API base; signed updates come from
34
+ * `POST /v1/latest_price` (Bearer-authenticated). The service is
35
+ * network-agnostic (one signed payload verifies on any chain that trusts the
36
+ * Lazer signers), so both networks share the production host.
37
+ * - `verifier_package` — the Sui package carrying
38
+ * `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
39
+ * still the original v1 publish; mainnet is the v2-upgraded package (which
40
+ * still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
41
+ * contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
42
+ */
43
+ export const LAZER_DEFAULTS = {
44
+ MAINNET: {
45
+ endpoint: "https://pyth-lazer.dourolabs.app",
46
+ verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
47
+ },
48
+ TESTNET: {
49
+ endpoint: "https://pyth-lazer.dourolabs.app",
50
+ verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
51
+ },
52
+ };
@@ -8,14 +8,27 @@
8
8
  * clause, and a future `PredictClient` (or a test double) can too.
9
9
  */
10
10
  import type { SuiGrpcClient } from "@mysten/sui/grpc";
11
+ import type { Network } from "../constants.ts";
11
12
  import type { OracleConfig, PythInfraConfig } from "./config.ts";
13
+ import type { OracleSource } from "./price-update-rule.ts";
12
14
  export interface OracleHost {
15
+ /** Sui network this client targets — selects per-network external-infra defaults (e.g. `LAZER_DEFAULTS`). */
16
+ readonly network: Network;
13
17
  /** Oracle slice of the canonical `waterx-config` JSON (rule packages + per-ticker feeds). */
14
18
  readonly config: OracleConfig;
15
- /** 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. */
16
20
  readonly pyth: PythInfraConfig;
17
21
  /** gRPC client for the on-chain reads the Pyth update path needs. */
18
22
  readonly grpcClient: SuiGrpcClient;
23
+ /**
24
+ * Client-selected oracle rule source for `refreshOraclePrices`'s on-chain
25
+ * update leg — resolved at client creation from the `oracleSource` create
26
+ * option (default `'pyth_rule'`). Routing is driven by this value ALONE:
27
+ * never by a config JSON `enabled` flag (e.g. a future `pyth_lazer_rule.enabled`)
28
+ * and never by `process.env` — the SDK never reads it; consumers (BE/FE) wire
29
+ * this option from their own env var.
30
+ */
31
+ readonly oracleSource: OracleSource;
19
32
  /** True when `ticker` is priced by `constant_rule`. */
20
33
  isConstantTicker(ticker: string): boolean;
21
34
  /** The `supra_rule` config when deployed, enabled, and fully wired; else `undefined`. */
@@ -2,15 +2,29 @@
2
2
  * Oracle module — the single source of truth for price freshness.
3
3
  *
4
4
  * Layering (no cross-imports between siblings except via `aggregate.ts`):
5
- * - `host.ts` — `OracleHost`, the narrow client slice this module reads.
6
- * - `pyth.ts` — Pyth as a price source: Hermes REST + on-chain update PTB.
7
- * - `rules/*` — one file per oracle rule (pyth / supra / constant / sponsor).
8
- * - `aggregate.ts`— the orchestrator that feeds rules into a collector + aggregates.
5
+ * - `host.ts` — `OracleHost`, the narrow client slice this module reads.
6
+ * - `update-fetch.ts` — `fetchWithPolicy`, the shared retry/timeout/Bearer resilience
7
+ * wrapper every off-chain oracle (and config) fetch goes through.
8
+ * - `pyth.ts` — Pyth as a price source: Hermes REST + on-chain update PTB.
9
+ * - `price-update-rule.ts`— `PriceUpdateRule`, the fetch/build strategy port a rule
10
+ * implements; `rule-registry.ts` + `aggregate.ts` wire
11
+ * routing across rules.
12
+ * - `rules/*` — one file per oracle rule (pyth / supra / constant / sponsor).
13
+ * - `aggregate.ts` — the orchestrator that feeds rules into a collector + aggregates.
9
14
  *
10
15
  * `pyth.ts` deliberately imports NO rule package — Pyth-the-source and the
11
16
  * rules that consume it are separate concerns.
12
17
  */
13
18
  export type { OracleHost } from "./host.ts";
14
- export { PythCache, fetchPriceFeedsUpdateData, buildPythPriceUpdateCalls, updatePythPrices, } from "./pyth.ts";
19
+ export { FetchPolicyError, fetchWithPolicy, joinEndpointPath } from "./update-fetch.ts";
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";
22
+ export type { OracleFeeSource } from "./pyth.ts";
23
+ export type { PriceUpdateRule, PriceUpdateRuleKind, RuleUpdateData, RuleUpdateHandle, BuildUpdateOpts, OracleSource, UpdateDataProvider, } from "./price-update-rule.ts";
24
+ export { PythCoreRule } from "./rules/pyth-core-rule.ts";
25
+ export type { PythCoreUpdatePayload } from "./rules/pyth-core-rule.ts";
26
+ export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.ts";
27
+ export type { PythLazerUpdatePayload } from "./rules/pyth-lazer-rule.ts";
28
+ export { OracleSourceNotImplementedError } from "./rule-registry.ts";
15
29
  export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.ts";
16
30
  export { openPythSponsorFund, reimbursePythSponsor } from "./rules/sponsor.ts";
@@ -2,16 +2,52 @@
2
2
  * Oracle module — the single source of truth for price freshness.
3
3
  *
4
4
  * Layering (no cross-imports between siblings except via `aggregate.ts`):
5
- * - `host.ts` — `OracleHost`, the narrow client slice this module reads.
6
- * - `pyth.ts` — Pyth as a price source: Hermes REST + on-chain update PTB.
7
- * - `rules/*` — one file per oracle rule (pyth / supra / constant / sponsor).
8
- * - `aggregate.ts`— the orchestrator that feeds rules into a collector + aggregates.
5
+ * - `host.ts` — `OracleHost`, the narrow client slice this module reads.
6
+ * - `update-fetch.ts` — `fetchWithPolicy`, the shared retry/timeout/Bearer resilience
7
+ * wrapper every off-chain oracle (and config) fetch goes through.
8
+ * - `pyth.ts` — Pyth as a price source: Hermes REST + on-chain update PTB.
9
+ * - `price-update-rule.ts`— `PriceUpdateRule`, the fetch/build strategy port a rule
10
+ * implements; `rule-registry.ts` + `aggregate.ts` wire
11
+ * routing across rules.
12
+ * - `rules/*` — one file per oracle rule (pyth / supra / constant / sponsor).
13
+ * - `aggregate.ts` — the orchestrator that feeds rules into a collector + aggregates.
9
14
  *
10
15
  * `pyth.ts` deliberately imports NO rule package — Pyth-the-source and the
11
16
  * rules that consume it are separate concerns.
12
17
  */
13
- // Pyth source
14
- export { PythCache, fetchPriceFeedsUpdateData, buildPythPriceUpdateCalls, updatePythPrices, } from "./pyth.js";
18
+ // Shared fetch resilience wrapper — `FetchPolicyError` is re-exported (not
19
+ // just the type) so a consumer (e.g. a BE prefetch cache) can `instanceof`
20
+ // it off the failure `fetchPriceFeedsUpdateData` / `PythLazerRule` /
21
+ // `loadConfig` surface, without a deep import of `./update-fetch.ts`.
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";
37
+ // Pyth Core rule (PriceUpdateRule wrapper over the Pyth source above)
38
+ export { PythCoreRule } from "./rules/pyth-core-rule.js";
39
+ // Pyth Lazer rule (signed-update generation; `feedLazerRule` stays internal to `aggregate.ts`)
40
+ // `LazerApiKeyMissingError` is re-exported (not just the type) for the same
41
+ // `instanceof` reason as `OracleFeeSourceUnavailableError` above.
42
+ export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.js";
43
+ // `resolveOracleRule` (rule-registry.ts) is NOT re-exported here — it has one
44
+ // production consumer (`refreshOraclePrices` below) and no external caller;
45
+ // per repo convention ("no unused exports") it stays module-internal. Tests
46
+ // import it directly from `./rule-registry.ts`. `OracleSourceNotImplementedError`
47
+ // IS re-exported (the same `instanceof` reason as `OracleFeeSourceUnavailableError`
48
+ // above) — a consumer of `refreshOraclePrices` can catch it without importing
49
+ // `resolveOracleRule` itself.
50
+ export { OracleSourceNotImplementedError } from "./rule-registry.js";
15
51
  // Aggregation orchestrator
16
52
  export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.js";
17
53
  // Sponsor rule (fund open / reimburse + witness attach)