@waterx/sdk 4.0.0 → 4.1.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 (128) hide show
  1. package/README.md +52 -37
  2. package/dist/cjs/src/account/account.js +2 -1
  3. package/dist/cjs/src/account/config.d.ts +0 -16
  4. package/dist/cjs/src/account/funding/balance.d.ts +25 -2
  5. package/dist/cjs/src/account/funding/balance.js +36 -24
  6. package/dist/cjs/src/account/funding/credit.js +6 -10
  7. package/dist/cjs/src/constants.d.ts +15 -1
  8. package/dist/cjs/src/constants.js +18 -4
  9. package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
  10. package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +13 -0
  11. package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
  12. package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.js +31 -0
  13. package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +940 -0
  14. package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +1092 -0
  15. package/dist/cjs/src/oracle/aggregate.d.ts +21 -21
  16. package/dist/cjs/src/oracle/aggregate.js +84 -71
  17. package/dist/cjs/src/oracle/config.d.ts +107 -52
  18. package/dist/cjs/src/oracle/config.js +15 -35
  19. package/dist/cjs/src/oracle/host.d.ts +13 -2
  20. package/dist/cjs/src/oracle/index.d.ts +4 -2
  21. package/dist/cjs/src/oracle/index.js +25 -6
  22. package/dist/cjs/src/oracle/price-update-rule.d.ts +3 -4
  23. package/dist/cjs/src/oracle/pyth.d.ts +68 -6
  24. package/dist/cjs/src/oracle/pyth.js +338 -22
  25. package/dist/cjs/src/oracle/rule-registry.d.ts +11 -6
  26. package/dist/cjs/src/oracle/rule-registry.js +13 -6
  27. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +17 -2
  28. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +6 -6
  29. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +25 -22
  30. package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -0
  31. package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +89 -0
  32. package/dist/cjs/src/oracle/rules/waterx-rule.js +272 -0
  33. package/dist/cjs/src/oracle/update-fetch.d.ts +32 -2
  34. package/dist/cjs/src/oracle/update-fetch.js +60 -3
  35. package/dist/cjs/src/perp/client.d.ts +71 -19
  36. package/dist/cjs/src/perp/client.js +30 -10
  37. package/dist/cjs/src/perp/config.d.ts +4 -7
  38. package/dist/cjs/src/perp/config.js +9 -12
  39. package/dist/cjs/src/perp/constants.d.ts +0 -6
  40. package/dist/cjs/src/perp/constants.js +11 -9
  41. package/dist/cjs/src/perp/fetch/account.js +3 -0
  42. package/dist/cjs/src/perp/fetch/bridge.js +2 -1
  43. package/dist/cjs/src/perp/fetch/market.js +2 -1
  44. package/dist/cjs/src/perp/fetch/positions.d.ts +16 -10
  45. package/dist/cjs/src/perp/fetch/positions.js +23 -20
  46. package/dist/cjs/src/perp/index.d.ts +8 -4
  47. package/dist/cjs/src/perp/index.js +12 -7
  48. package/dist/cjs/src/perp/liq-view.d.ts +64 -0
  49. package/dist/cjs/src/perp/liq-view.js +74 -0
  50. package/dist/cjs/src/perp/user/order.d.ts +13 -0
  51. package/dist/cjs/src/perp/user/order.js +30 -16
  52. package/dist/cjs/src/perp/user/staking.js +3 -2
  53. package/dist/cjs/src/perp/user/trading.js +25 -24
  54. package/dist/cjs/src/perp/user/wlp.js +6 -5
  55. package/dist/cjs/src/prediction/utils.d.ts +11 -2
  56. package/dist/cjs/src/prediction/utils.js +22 -22
  57. package/dist/cjs/src/unified-client.d.ts +49 -20
  58. package/dist/cjs/src/unified-client.js +4 -1
  59. package/dist/cjs/src/utils/format.d.ts +14 -0
  60. package/dist/cjs/src/utils/format.js +24 -0
  61. package/dist/cjs/src/utils/math.d.ts +304 -12
  62. package/dist/cjs/src/utils/math.js +397 -17
  63. package/dist/cjs/src/utils/validate.d.ts +69 -0
  64. package/dist/cjs/src/utils/validate.js +183 -0
  65. package/dist/src/account/account.js +2 -1
  66. package/dist/src/account/config.d.ts +0 -16
  67. package/dist/src/account/funding/balance.d.ts +25 -2
  68. package/dist/src/account/funding/balance.js +36 -24
  69. package/dist/src/account/funding/credit.js +6 -10
  70. package/dist/src/constants.d.ts +15 -1
  71. package/dist/src/constants.js +17 -3
  72. package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
  73. package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +10 -0
  74. package/dist/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
  75. package/dist/src/generated/waterx_rule/deps/sui/vec_map.js +27 -0
  76. package/dist/src/generated/waterx_rule/waterx_rule.d.ts +940 -0
  77. package/dist/src/generated/waterx_rule/waterx_rule.js +991 -0
  78. package/dist/src/oracle/aggregate.d.ts +21 -21
  79. package/dist/src/oracle/aggregate.js +84 -71
  80. package/dist/src/oracle/config.d.ts +107 -52
  81. package/dist/src/oracle/config.js +14 -34
  82. package/dist/src/oracle/host.d.ts +13 -2
  83. package/dist/src/oracle/index.d.ts +4 -2
  84. package/dist/src/oracle/index.js +18 -7
  85. package/dist/src/oracle/price-update-rule.d.ts +3 -4
  86. package/dist/src/oracle/pyth.d.ts +68 -6
  87. package/dist/src/oracle/pyth.js +334 -22
  88. package/dist/src/oracle/rule-registry.d.ts +11 -6
  89. package/dist/src/oracle/rule-registry.js +13 -6
  90. package/dist/src/oracle/rules/pyth-core-rule.js +18 -3
  91. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +6 -6
  92. package/dist/src/oracle/rules/pyth-lazer-rule.js +26 -23
  93. package/dist/src/oracle/rules/pyth-rule.js +5 -0
  94. package/dist/src/oracle/rules/waterx-rule.d.ts +89 -0
  95. package/dist/src/oracle/rules/waterx-rule.js +266 -0
  96. package/dist/src/oracle/update-fetch.d.ts +32 -2
  97. package/dist/src/oracle/update-fetch.js +57 -3
  98. package/dist/src/perp/client.d.ts +71 -19
  99. package/dist/src/perp/client.js +31 -11
  100. package/dist/src/perp/config.d.ts +4 -7
  101. package/dist/src/perp/config.js +9 -12
  102. package/dist/src/perp/constants.d.ts +0 -6
  103. package/dist/src/perp/constants.js +10 -8
  104. package/dist/src/perp/fetch/account.js +3 -0
  105. package/dist/src/perp/fetch/bridge.js +2 -1
  106. package/dist/src/perp/fetch/market.js +2 -1
  107. package/dist/src/perp/fetch/positions.d.ts +16 -10
  108. package/dist/src/perp/fetch/positions.js +28 -20
  109. package/dist/src/perp/index.d.ts +8 -4
  110. package/dist/src/perp/index.js +5 -3
  111. package/dist/src/perp/liq-view.d.ts +64 -0
  112. package/dist/src/perp/liq-view.js +71 -0
  113. package/dist/src/perp/user/order.d.ts +13 -0
  114. package/dist/src/perp/user/order.js +30 -16
  115. package/dist/src/perp/user/staking.js +3 -2
  116. package/dist/src/perp/user/trading.js +25 -24
  117. package/dist/src/perp/user/wlp.js +6 -5
  118. package/dist/src/prediction/utils.d.ts +11 -2
  119. package/dist/src/prediction/utils.js +22 -22
  120. package/dist/src/unified-client.d.ts +49 -20
  121. package/dist/src/unified-client.js +4 -1
  122. package/dist/src/utils/format.d.ts +14 -0
  123. package/dist/src/utils/format.js +21 -0
  124. package/dist/src/utils/math.d.ts +304 -12
  125. package/dist/src/utils/math.js +394 -17
  126. package/dist/src/utils/validate.d.ts +69 -0
  127. package/dist/src/utils/validate.js +167 -0
  128. package/package.json +4 -1
@@ -25,6 +25,7 @@ import type { Transaction, TransactionArgument } from "@mysten/sui/transactions"
25
25
  import type { OracleHost } from "./host.ts";
26
26
  import type { OracleSource, PriceUpdateRule, UpdateDataProvider } from "./price-update-rule.ts";
27
27
  import { type OracleFeeSource, type PythCache } from "./pyth.ts";
28
+ import { type WaterxSignedEnvelope } from "./rules/waterx-rule.ts";
28
29
  /**
29
30
  * Aggregate one ticker's price into the shared `Oracle`: build a collector, feed
30
31
  * every rule the ticker is configured for, then `aggregate`.
@@ -54,6 +55,7 @@ export declare function aggregateTicker(tx: Transaction, host: OracleHost, args:
54
55
  ticker: string;
55
56
  priceInfoObjectId?: string;
56
57
  lazerUpdate?: TransactionArgument;
58
+ waterxEnvelope?: WaterxSignedEnvelope;
57
59
  }): void;
58
60
  /**
59
61
  * Thin wrapper over {@link aggregateTicker} for a Pyth-fed ticker. Kept for
@@ -83,28 +85,26 @@ export declare function aggregateTickerWithConstant(tx: Transaction, host: Oracl
83
85
  * enabled, Constant when it's a constant ticker).
84
86
  *
85
87
  * 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.
88
+ * (see `rule-registry.ts`): the ONE selected source serves every ticker in its
89
+ * `supportedTickers(host)`. There is **no cross-source fallback** a requested
90
+ * ticker the selected source does not serve, and that is not a constant-only
91
+ * ticker (which needs no price-update leg), fails the build immediately with a
92
+ * clear error naming the ticker and source. That is the deliberate "fail the
93
+ * tx-build, don't silently reroute" contract: a wrong-but-present feed id is
94
+ * NOT validated here (it surfaces on-chain at dry-run); a MISSING feed for the
95
+ * selected source is caught here. When the selected source's feed exists but is
96
+ * wrong, this function does nothing special — the on-chain aggregate aborts at
97
+ * dry-run, which is correct.
93
98
  *
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.
99
+ * The selected source's fetch + build runs against its own infra, guaranteeing
100
+ * per-rule PTB atomicity. A fee-source pre-check runs first (the source's
101
+ * `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation so a
102
+ * fee-charging source with no `opts.feeSource` throws
103
+ * `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
104
+ * moveCalls. Only once that check passes does the off-chain fetch run and
105
+ * complete before any PTB mutation; on-chain reads inside `buildUpdateCalls`
106
+ * can still fail mid-append for other reasons — callers discard the tx on any
107
+ * throw.
108
108
  *
109
109
  * **Collector-feed leg is rule-aware:** a lazer-served group's
110
110
  * `buildUpdateCalls` returns the verified `Update` PTB value
@@ -28,6 +28,7 @@ import { feedConstantRule } from "./rules/constant-rule.js";
28
28
  import { feedLazerRule } from "./rules/pyth-lazer-rule.js";
29
29
  import { feedPythRule } from "./rules/pyth-rule.js";
30
30
  import { maybeFeedSupra } from "./rules/supra-rule.js";
31
+ import { feedWaterxRule, waterxEnvelopeOf, } from "./rules/waterx-rule.js";
31
32
  /**
32
33
  * Resolve one group's off-chain update payload for {@link refreshOraclePrices}:
33
34
  * try `provider.get(source, tickers)` first (when a provider is configured),
@@ -110,6 +111,14 @@ export function aggregateTicker(tx, host, args) {
110
111
  feedLazerRule(tx, host, collector, args.lazerUpdate);
111
112
  fed = true;
112
113
  }
114
+ if (args.waterxEnvelope !== undefined) {
115
+ // waterx_rule::collect_batch_latest verifies the batch signature and feeds
116
+ // this collector's symbol from the batch. If the ticker's aggregator does
117
+ // not (yet) weight `WaterxRule`, the contribution is silently dropped
118
+ // on-chain — feeding ahead of the weight migration is harmless.
119
+ feedWaterxRule(tx, host, collector, args.waterxEnvelope);
120
+ fed = true;
121
+ }
113
122
  if (fed) {
114
123
  // Supra rides on the same collector when enabled (abstains on-chain otherwise).
115
124
  maybeFeedSupra(tx, host, collector);
@@ -159,28 +168,26 @@ export function aggregateTickerWithConstant(tx, host, args) {
159
168
  * enabled, Constant when it's a constant ticker).
160
169
  *
161
170
  * 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.
171
+ * (see `rule-registry.ts`): the ONE selected source serves every ticker in its
172
+ * `supportedTickers(host)`. There is **no cross-source fallback** a requested
173
+ * ticker the selected source does not serve, and that is not a constant-only
174
+ * ticker (which needs no price-update leg), fails the build immediately with a
175
+ * clear error naming the ticker and source. That is the deliberate "fail the
176
+ * tx-build, don't silently reroute" contract: a wrong-but-present feed id is
177
+ * NOT validated here (it surfaces on-chain at dry-run); a MISSING feed for the
178
+ * selected source is caught here. When the selected source's feed exists but is
179
+ * wrong, this function does nothing special — the on-chain aggregate aborts at
180
+ * dry-run, which is correct.
169
181
  *
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.
182
+ * The selected source's fetch + build runs against its own infra, guaranteeing
183
+ * per-rule PTB atomicity. A fee-source pre-check runs first (the source's
184
+ * `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation so a
185
+ * fee-charging source with no `opts.feeSource` throws
186
+ * `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
187
+ * moveCalls. Only once that check passes does the off-chain fetch run and
188
+ * complete before any PTB mutation; on-chain reads inside `buildUpdateCalls`
189
+ * can still fail mid-append for other reasons — callers discard the tx on any
190
+ * throw.
184
191
  *
185
192
  * **Collector-feed leg is rule-aware:** a lazer-served group's
186
193
  * `buildUpdateCalls` returns the verified `Update` PTB value
@@ -207,69 +214,74 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
207
214
  const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
208
215
  const priceInfoByTicker = new Map();
209
216
  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.
217
+ // ONE source, no fallback. The selected source serves the tickers in its
218
+ // `supportedTickers(host)`; `source` is tracked alongside the group (rather
219
+ // than read back off `rule.kind`, typed as the broader PriceUpdateRuleKind)
220
+ // so the provider lookup below has an OracleSource to key on without a cast.
215
221
  const selectedRule = resolveOracleRule(host.oracleSource, opts.ruleOverrides);
216
222
  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 });
223
+ // Fail the tx-build (NOT client init, NOT a silent reroute) when the selected
224
+ // source has no feed for a requested ticker that actually needs a price
225
+ // update. Only a CONSTANT-ONLY ticker is exempt — priced entirely by
226
+ // `constant_rule`, it needs no update leg from any source. A DUAL-FEED ticker
227
+ // (constant AND pyth) still needs its Pyth leg refreshed, so `isConstantTicker`
228
+ // alone must NOT exempt it: under a source that can't serve it, with no
229
+ // fallback, feeding an unrefreshed Pyth leg would price it stale (or abort on a
230
+ // missing weighted source). `priceInfoByTicker.has(t)` ⇔ the ticker has a
231
+ // `pyth_rule.feeds` entry, so `constant && !hasPyth` is exactly constant-only.
232
+ // This catches a MISSING feed; a present-but-WRONG feed id is deliberately not
233
+ // validated here (it aborts on-chain at dry-run).
234
+ const isConstantOnly = (t) => host.isConstantTicker(t) && !priceInfoByTicker.has(t);
235
+ const unservable = tickers.filter((t) => !selectedSupported.has(t) && !isConstantOnly(t));
236
+ if (unservable.length > 0) {
237
+ throw new Error(`oracleSource '${host.oracleSource}' has no feed configured for ticker(s): ` +
238
+ `${unservable.join(", ")}. Sources are self-contained with no fallback — add ` +
239
+ `${host.oracleSource} feeds for them, or select a source that serves them.`);
221
240
  }
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
- }
229
- }
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)) {
241
+ const selectedGroup = tickers.filter((t) => selectedSupported.has(t));
242
+ // Fee-source pre-check, hoisted ABOVE the off-chain fetch and PTB build below.
243
+ // It consults only `rule.requiresFeeSource` — known before any fetch or PTB
244
+ // mutation so a fee-charging source with no `feeSource` throws with ZERO
245
+ // wasted network calls and zero PTB commands, rather than waiting for
246
+ // `buildPythPriceUpdateCalls`'s own per-call guard to fire after the off-chain
247
+ // fetch already ran.
248
+ if (selectedGroup.length > 0 && !opts.feeSource && selectedRule.requiresFeeSource) {
247
249
  throw new OracleFeeSourceUnavailableError();
248
250
  }
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.
251
+ // Resolve + build the selected source's update leg. The off-chain fetch
252
+ // settles before the first PTB mutation, so a fetch failure never strands
253
+ // moveCalls in a caller-owned tx. Map each lazer-served ticker to the one
254
+ // verified `Update` PTB value for the collector-feed leg below.
260
255
  const lazerUpdateByTicker = new Map();
261
- for (const group of groupsWithData) {
262
- const handle = (await group.rule.buildUpdateCalls(tx, host, group.data, {
256
+ // Signed batch envelope per waterx-served ticker. Unlike Lazer's shared PTB
257
+ // handle, waterx's verify+feed is bundled into `collect_batch_latest` in the
258
+ // per-ticker feed leg, so its `buildUpdateCalls` emits nothing and the
259
+ // envelope is carried straight from the group's fetched data.
260
+ const waterxEnvelopeByTicker = new Map();
261
+ if (selectedGroup.length > 0) {
262
+ const group = { source: host.oracleSource, rule: selectedRule, tickers: selectedGroup };
263
+ const data = await resolveGroupUpdateData(host, group, opts.updateDataProvider);
264
+ const handle = (await selectedRule.buildUpdateCalls(tx, host, data, {
263
265
  cache: opts.cache,
264
266
  feeSource: opts.feeSource,
265
267
  })) ?? undefined;
266
268
  // Route by the handle's kind discriminant — the one site the tag exists to
267
- // protect: a future non-lazer handle (e.g. a WaterxRule value) must never
268
- // be silently fed into pyth_lazer_rule::feed.
269
+ // protect: a future non-lazer handle must never be silently fed into
270
+ // pyth_lazer_rule::feed.
269
271
  if (handle?.kind === "pyth_lazer_rule") {
270
- for (const ticker of group.tickers)
272
+ for (const ticker of selectedGroup)
271
273
  lazerUpdateByTicker.set(ticker, handle.update);
272
274
  }
275
+ // waterx_rule emits no shared handle (verify+feed is bundled into the
276
+ // per-ticker `collect_batch_latest`), so the envelope is carried straight
277
+ // from this group's fetched data to the feed leg below.
278
+ if (selectedRule.kind === "waterx_rule") {
279
+ const envelope = waterxEnvelopeOf(data);
280
+ if (envelope) {
281
+ for (const ticker of selectedGroup)
282
+ waterxEnvelopeByTicker.set(ticker, envelope);
283
+ }
284
+ }
273
285
  }
274
286
  // Aggregate each ticker, feeding whichever rules it is configured for.
275
287
  for (const ticker of tickers) {
@@ -277,6 +289,7 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
277
289
  ticker,
278
290
  priceInfoObjectId: priceInfoByTicker.get(ticker),
279
291
  lazerUpdate: lazerUpdateByTicker.get(ticker),
292
+ waterxEnvelope: waterxEnvelopeByTicker.get(ticker),
280
293
  });
281
294
  }
282
295
  }
@@ -12,6 +12,7 @@
12
12
  import type { BasePackageEntry } from "../account/config.ts";
13
13
  import type { BaseLineConfig } from "../base-client.ts";
14
14
  import type { Network } from "../constants.ts";
15
+ import type { FetchPolicy } from "./update-fetch.ts";
15
16
  export interface PythRulePackage extends BasePackageEntry {
16
17
  config: string;
17
18
  feeds: Record<string, {
@@ -82,6 +83,39 @@ export interface SupraRulePackage extends BasePackageEntry {
82
83
  */
83
84
  enabled?: boolean;
84
85
  }
86
+ /**
87
+ * Per-ticker `waterx_rule` feed entry. Keyed in `feeds` by the oracle **ticker**
88
+ * (e.g. `"SUIUSD"`, the same key `pyth_rule.feeds` uses), so
89
+ * `Object.keys(feeds)` is the SDK's supported-ticker set. The fields are
90
+ * informational off-chain — the SDK keys routing/support off the entry's
91
+ * presence and pushes the enclave-signed price verbatim, never re-deriving it.
92
+ */
93
+ export interface WaterxRuleFeedEntry {
94
+ /** Exchange ticker the quote-center aggregates from (e.g. `"SUIUSDT"`). */
95
+ ticker?: string;
96
+ }
97
+ /**
98
+ * `waterx_rule` deployment entry — the first-party Nautilus-TEE oracle rule.
99
+ * Read by `WaterxRule` (`rules/waterx-rule.ts`): `feeds` for ticker support,
100
+ * `config`/`enclave_config`/`enclave` for the `collect_batch_latest` call,
101
+ * `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.
103
+ *
104
+ * `enabled` mirrors the JSON field verbatim but MUST NOT be read for routing —
105
+ * which rule prices a ticker is decided solely by the client's `oracleSource`
106
+ * create option (see `OracleHost.oracleSource`), mirroring `pyth_lazer_rule`.
107
+ */
108
+ export interface WaterxRulePackage extends BasePackageEntry {
109
+ /** Shared `waterx_rule::Config` (per-symbol on-chain feed_config). */
110
+ config: string;
111
+ /** Shared `EnclaveConfig<WATERX_RULE>` the on-chain signature verify runs against. */
112
+ enclave_config: string;
113
+ /** Shared `Enclave<WATERX_RULE>` holding the registered TEE signing pubkey. */
114
+ enclave: string;
115
+ enabled?: boolean;
116
+ /** Oracle ticker → feed entry; presence marks the ticker as waterx-served. */
117
+ feeds: Record<string, WaterxRuleFeedEntry>;
118
+ }
85
119
  export interface WaterxOraclePackage extends BasePackageEntry {
86
120
  listing_cap: string;
87
121
  oracle: string;
@@ -99,8 +133,33 @@ export interface OraclePackages {
99
133
  pyth_lazer_rule?: PythLazerRulePackage;
100
134
  constant_rule?: WaterxConstantRulePackage;
101
135
  supra_rule?: SupraRulePackage;
136
+ /** See {@link WaterxRulePackage} — read by `WaterxRule` when `oracleSource` selects it. */
137
+ waterx_rule?: WaterxRulePackage;
102
138
  waterx_oracle: WaterxOraclePackage;
103
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
+ /**
152
+ * The caller-tunable subset of `fetchWithPolicy`'s policy exposed on the
153
+ * `pythFetch` create option and `client.pyth.fetch` — the retry/timeout budget
154
+ * for the off-chain Hermes (`fetchPriceFeedsUpdateData`) and Lazer
155
+ * (`PythLazerRule`) update fetches. Deliberately narrower than the internal
156
+ * `FetchPolicy` (no `retryDelayMs` / `apiKey` / `fetchImpl`). Both fetches fall
157
+ * back to `fetchWithPolicy`'s defaults (15s timeout, 2 retries) when unset.
158
+ */
159
+ export type PythFetchPolicy = {
160
+ timeoutMs?: number;
161
+ retries?: number;
162
+ };
104
163
  export interface PythInfraConfig {
105
164
  state_id: string;
106
165
  wormhole_state_id: string;
@@ -109,8 +168,8 @@ export interface PythInfraConfig {
109
168
  * Pyth Pro / Lazer access token (`Authorization: Bearer …`) for
110
169
  * `PythLazerRule`'s signed-update fetch — Lazer is auth-first, so there is
111
170
  * 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 →
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 →
114
173
  * `LazerApiKeyMissing` is thrown at fetch time. As of the Pyth Pro
115
174
  * migration (post-2026-08-18, per
116
175
  * https://docs.pyth.network/price-feeds/core/upgrade) this is ALSO required
@@ -119,57 +178,16 @@ export interface PythInfraConfig {
119
178
  */
120
179
  api_key?: string;
121
180
  /**
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.
181
+ * Retry/timeout policy for the Hermes (`fetchPriceFeedsUpdateData`) and
182
+ * Lazer (`PythLazerRule`) off-chain update fetches — see `fetchWithPolicy`
183
+ * (`./update-fetch.ts`) for the full policy (backoff, which statuses retry,
184
+ * Bearer attachment). Supplied via the `pythFetch` create option. Optional:
185
+ * both fetches default to `fetchWithPolicy`'s built-in defaults (15s
186
+ * timeout, 2 retries) when unset.
128
187
  */
129
- fetch?: {
130
- timeoutMs?: number;
131
- retries?: number;
132
- };
188
+ fetch?: PythFetchPolicy;
133
189
  }
134
190
  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
191
  /**
174
192
  * Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
175
193
  * Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
@@ -190,6 +208,45 @@ export declare const LAZER_DEFAULTS: Record<Network, {
190
208
  endpoint: string;
191
209
  verifier_package: string;
192
210
  }>;
211
+ /**
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`).
215
+ *
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.
222
+ */
223
+ export interface WaterxInfraConfig {
224
+ /**
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
227
+ * `…/api/quote-center/v1/quotes/update` and a proxy route is not rewritten
228
+ * away. A trailing slash is trimmed.
229
+ */
230
+ endpoint: string;
231
+ /**
232
+ * Retry/timeout policy (and `fetchImpl`) for the quote-center fetch — see
233
+ * `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).
236
+ */
237
+ fetch?: FetchPolicy;
238
+ }
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>;
193
250
  /**
194
251
  * The narrow config shape the oracle/refresh code needs. `WaterXConfig`
195
252
  * (the perp line's full config) is assignable to this, so `PerpClient` satisfies
@@ -197,6 +254,4 @@ export declare const LAZER_DEFAULTS: Record<Network, {
197
254
  */
198
255
  export interface OracleConfig extends BaseLineConfig {
199
256
  packages: OraclePackages;
200
- /** Pyth infra override (defaults from {@link PYTH_DEFAULTS}). */
201
- pyth?: PythInfraConfig;
202
257
  }
@@ -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
  // ============================================================================
@@ -84,3 +50,17 @@ export const LAZER_DEFAULTS = {
84
50
  verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
85
51
  },
86
52
  };
53
+ /**
54
+ * WaterX quote-center base URL by network — the first-party TEE-signed price
55
+ * hub `WaterxRule` pulls from (`GET /v1/quotes/update?symbols=…`). Mirrors
56
+ * {@link LAZER_DEFAULTS}: infra WaterX operates, not part of the `waterx-config`
57
+ * JSON. Public read (no auth), so there is no api_key. `endpoint` has no
58
+ * trailing slash — the rule appends the path.
59
+ *
60
+ * These are DEFAULTS, not a hard pin: a consumer overrides them per client via
61
+ * `waterxEndpoint` / `waterxFetch` (resolved onto `client.waterx`).
62
+ */
63
+ export const WATERX_DEFAULTS = {
64
+ MAINNET: { endpoint: "https://quote-center.waterx.app" },
65
+ TESTNET: { endpoint: "https://quote-center-staging.waterx.app" },
66
+ };
@@ -9,15 +9,26 @@
9
9
  */
10
10
  import type { SuiGrpcClient } from "@mysten/sui/grpc";
11
11
  import type { Network } from "../constants.ts";
12
- import type { OracleConfig, PythInfraConfig } from "./config.ts";
12
+ import type { OracleConfig, PythInfraConfig, WaterxInfraConfig } from "./config.ts";
13
13
  import type { OracleSource } from "./price-update-rule.ts";
14
14
  export interface OracleHost {
15
15
  /** Sui network this client targets — selects per-network external-infra defaults (e.g. `LAZER_DEFAULTS`). */
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
+ /**
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.
30
+ */
31
+ readonly waterx?: WaterxInfraConfig;
21
32
  /** gRPC client for the on-chain reads the Pyth update path needs. */
22
33
  readonly grpcClient: SuiGrpcClient;
23
34
  /**
@@ -16,15 +16,17 @@
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";
25
25
  export type { PythCoreUpdatePayload } from "./rules/pyth-core-rule.ts";
26
26
  export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.ts";
27
27
  export type { PythLazerUpdatePayload } from "./rules/pyth-lazer-rule.ts";
28
+ export { WaterxRule, parseSignedEnvelope } from "./rules/waterx-rule.ts";
29
+ export type { WaterxUpdatePayload, WaterxSignedEnvelope, WaterxBatchItem, } from "./rules/waterx-rule.ts";
28
30
  export { OracleSourceNotImplementedError } from "./rule-registry.ts";
29
31
  export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.ts";
30
32
  export { openPythSponsorFund, reimbursePythSponsor } from "./rules/sponsor.ts";