@waterx/sdk 4.0.1 → 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 (130) hide show
  1. package/README.md +37 -17
  2. package/dist/cjs/src/account/account.js +2 -1
  3. package/dist/cjs/src/account/config.d.ts +3 -2
  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 +813 -0
  14. package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +946 -0
  15. package/dist/cjs/src/oracle/aggregate.d.ts +23 -21
  16. package/dist/cjs/src/oracle/aggregate.js +133 -59
  17. package/dist/cjs/src/oracle/config.d.ts +82 -41
  18. package/dist/cjs/src/oracle/config.js +0 -42
  19. package/dist/cjs/src/oracle/host.d.ts +26 -11
  20. package/dist/cjs/src/oracle/index.d.ts +6 -2
  21. package/dist/cjs/src/oracle/index.js +36 -8
  22. package/dist/cjs/src/oracle/price-update-rule.d.ts +16 -10
  23. package/dist/cjs/src/oracle/price-update-rule.js +3 -3
  24. package/dist/cjs/src/oracle/pyth.d.ts +44 -0
  25. package/dist/cjs/src/oracle/pyth.js +48 -6
  26. package/dist/cjs/src/oracle/read-plane.d.ts +70 -0
  27. package/dist/cjs/src/oracle/read-plane.js +78 -0
  28. package/dist/cjs/src/oracle/rule-registry.d.ts +14 -11
  29. package/dist/cjs/src/oracle/rule-registry.js +16 -11
  30. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +4 -4
  31. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
  32. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +29 -4
  33. package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -4
  34. package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +126 -0
  35. package/dist/cjs/src/oracle/rules/waterx-rule.js +312 -0
  36. package/dist/cjs/src/perp/client.d.ts +71 -20
  37. package/dist/cjs/src/perp/client.js +35 -13
  38. package/dist/cjs/src/perp/config.d.ts +6 -7
  39. package/dist/cjs/src/perp/config.js +12 -11
  40. package/dist/cjs/src/perp/constants.d.ts +0 -6
  41. package/dist/cjs/src/perp/constants.js +11 -9
  42. package/dist/cjs/src/perp/fetch/account.js +3 -0
  43. package/dist/cjs/src/perp/fetch/bridge.js +2 -1
  44. package/dist/cjs/src/perp/fetch/market.js +2 -1
  45. package/dist/cjs/src/perp/fetch/positions.d.ts +16 -10
  46. package/dist/cjs/src/perp/fetch/positions.js +23 -20
  47. package/dist/cjs/src/perp/index.d.ts +9 -5
  48. package/dist/cjs/src/perp/index.js +16 -7
  49. package/dist/cjs/src/perp/liq-view.d.ts +64 -0
  50. package/dist/cjs/src/perp/liq-view.js +74 -0
  51. package/dist/cjs/src/perp/user/order.d.ts +13 -0
  52. package/dist/cjs/src/perp/user/order.js +30 -16
  53. package/dist/cjs/src/perp/user/staking.js +3 -2
  54. package/dist/cjs/src/perp/user/trading.js +25 -24
  55. package/dist/cjs/src/perp/user/wlp.js +6 -5
  56. package/dist/cjs/src/prediction/utils.d.ts +11 -2
  57. package/dist/cjs/src/prediction/utils.js +22 -22
  58. package/dist/cjs/src/unified-client.d.ts +41 -15
  59. package/dist/cjs/src/unified-client.js +4 -2
  60. package/dist/cjs/src/utils/format.d.ts +14 -0
  61. package/dist/cjs/src/utils/format.js +24 -0
  62. package/dist/cjs/src/utils/math.d.ts +304 -12
  63. package/dist/cjs/src/utils/math.js +397 -17
  64. package/dist/cjs/src/utils/validate.d.ts +69 -0
  65. package/dist/cjs/src/utils/validate.js +183 -0
  66. package/dist/src/account/account.js +2 -1
  67. package/dist/src/account/config.d.ts +3 -2
  68. package/dist/src/account/funding/balance.d.ts +25 -2
  69. package/dist/src/account/funding/balance.js +36 -24
  70. package/dist/src/account/funding/credit.js +6 -10
  71. package/dist/src/constants.d.ts +15 -1
  72. package/dist/src/constants.js +17 -3
  73. package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
  74. package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +10 -0
  75. package/dist/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
  76. package/dist/src/generated/waterx_rule/deps/sui/vec_map.js +27 -0
  77. package/dist/src/generated/waterx_rule/waterx_rule.d.ts +813 -0
  78. package/dist/src/generated/waterx_rule/waterx_rule.js +855 -0
  79. package/dist/src/oracle/aggregate.d.ts +23 -21
  80. package/dist/src/oracle/aggregate.js +133 -59
  81. package/dist/src/oracle/config.d.ts +82 -41
  82. package/dist/src/oracle/config.js +1 -41
  83. package/dist/src/oracle/host.d.ts +26 -11
  84. package/dist/src/oracle/index.d.ts +6 -2
  85. package/dist/src/oracle/index.js +27 -9
  86. package/dist/src/oracle/price-update-rule.d.ts +16 -10
  87. package/dist/src/oracle/price-update-rule.js +3 -3
  88. package/dist/src/oracle/pyth.d.ts +44 -0
  89. package/dist/src/oracle/pyth.js +45 -5
  90. package/dist/src/oracle/read-plane.d.ts +70 -0
  91. package/dist/src/oracle/read-plane.js +74 -0
  92. package/dist/src/oracle/rule-registry.d.ts +14 -11
  93. package/dist/src/oracle/rule-registry.js +16 -11
  94. package/dist/src/oracle/rules/pyth-core-rule.js +5 -5
  95. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
  96. package/dist/src/oracle/rules/pyth-lazer-rule.js +28 -3
  97. package/dist/src/oracle/rules/pyth-rule.js +5 -4
  98. package/dist/src/oracle/rules/waterx-rule.d.ts +126 -0
  99. package/dist/src/oracle/rules/waterx-rule.js +305 -0
  100. package/dist/src/perp/client.d.ts +71 -20
  101. package/dist/src/perp/client.js +36 -14
  102. package/dist/src/perp/config.d.ts +6 -7
  103. package/dist/src/perp/config.js +11 -9
  104. package/dist/src/perp/constants.d.ts +0 -6
  105. package/dist/src/perp/constants.js +10 -8
  106. package/dist/src/perp/fetch/account.js +3 -0
  107. package/dist/src/perp/fetch/bridge.js +2 -1
  108. package/dist/src/perp/fetch/market.js +2 -1
  109. package/dist/src/perp/fetch/positions.d.ts +16 -10
  110. package/dist/src/perp/fetch/positions.js +28 -20
  111. package/dist/src/perp/index.d.ts +9 -5
  112. package/dist/src/perp/index.js +6 -4
  113. package/dist/src/perp/liq-view.d.ts +64 -0
  114. package/dist/src/perp/liq-view.js +71 -0
  115. package/dist/src/perp/user/order.d.ts +13 -0
  116. package/dist/src/perp/user/order.js +30 -16
  117. package/dist/src/perp/user/staking.js +3 -2
  118. package/dist/src/perp/user/trading.js +25 -24
  119. package/dist/src/perp/user/wlp.js +6 -5
  120. package/dist/src/prediction/utils.d.ts +11 -2
  121. package/dist/src/prediction/utils.js +22 -22
  122. package/dist/src/unified-client.d.ts +41 -15
  123. package/dist/src/unified-client.js +4 -2
  124. package/dist/src/utils/format.d.ts +14 -0
  125. package/dist/src/utils/format.js +21 -0
  126. package/dist/src/utils/math.d.ts +304 -12
  127. package/dist/src/utils/math.js +394 -17
  128. package/dist/src/utils/validate.d.ts +69 -0
  129. package/dist/src/utils/validate.js +167 -0
  130. package/package.json +4 -1
@@ -19,12 +19,14 @@
19
19
  *
20
20
  * `refreshOraclePrices` additionally routes the on-chain price *update* leg
21
21
  * (the fetch + verify/push step, before any of the above feeding) through the
22
- * `PriceUpdateRule` selected by `host.oracleSource` — see `rule-registry.ts`.
22
+ * `PriceUpdateRule` of EVERY source in the `host.oracleSources` fed set — see
23
+ * `rule-registry.ts`.
23
24
  */
24
25
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
25
26
  import type { OracleHost } from "./host.ts";
26
27
  import type { OracleSource, PriceUpdateRule, UpdateDataProvider } from "./price-update-rule.ts";
27
28
  import { type OracleFeeSource, type PythCache } from "./pyth.ts";
29
+ import { type WaterxSignedEnvelope } from "./rules/waterx-rule.ts";
28
30
  /**
29
31
  * Aggregate one ticker's price into the shared `Oracle`: build a collector, feed
30
32
  * every rule the ticker is configured for, then `aggregate`.
@@ -54,6 +56,7 @@ export declare function aggregateTicker(tx: Transaction, host: OracleHost, args:
54
56
  ticker: string;
55
57
  priceInfoObjectId?: string;
56
58
  lazerUpdate?: TransactionArgument;
59
+ waterxEnvelope?: WaterxSignedEnvelope;
57
60
  }): void;
58
61
  /**
59
62
  * Thin wrapper over {@link aggregateTicker} for a Pyth-fed ticker. Kept for
@@ -82,27 +85,26 @@ export declare function aggregateTickerWithConstant(tx: Transaction, host: Oracl
82
85
  * entry, Lazer if the lazer update leg served it — see below — Supra when
83
86
  * enabled, Constant when it's a constant ticker).
84
87
  *
85
- * Before that, the on-chain price *update* leg is routed by `host.oracleSource`
86
- * (see `rule-registry.ts`): the ONE selected source serves every ticker in its
87
- * `supportedTickers(host)`. There is **no cross-source fallback** — a requested
88
- * ticker the selected source does not serve, and that is not a constant-only
89
- * ticker (which needs no price-update leg), fails the build immediately with a
90
- * clear error naming the ticker and source. That is the deliberate "fail the
91
- * tx-build, don't silently reroute" contract: a wrong-but-present feed id is
92
- * NOT validated here (it surfaces on-chain at dry-run); a MISSING feed for the
93
- * selected source is caught here. When the selected source's feed exists but is
94
- * wrong, this function does nothing special — the on-chain aggregate aborts at
95
- * dry-run, which is correct.
88
+ * Before that, the on-chain price *update* leg is routed by the
89
+ * `host.oracleSources` fed set (see `rule-registry.ts`): EVERY listed source
90
+ * updates the tickers its own `supportedTickers(host)` serves, all in this one
91
+ * PTB. There is **no cross-source fallback** a requested ticker NO listed
92
+ * source serves, and that is not a constant-only ticker (which needs no
93
+ * price-update leg), fails the build immediately with a clear error naming
94
+ * the ticker and the list. That is the deliberate "fail the tx-build, don't
95
+ * silently reroute" contract: a wrong-but-present feed id is NOT validated
96
+ * here (it surfaces on-chain at dry-run); a ticker MISSING from every listed
97
+ * source's feeds is caught here.
96
98
  *
97
- * The selected source's fetch + build runs against its own infra, guaranteeing
98
- * per-rule PTB atomicity. A fee-source pre-check runs first (the source's
99
- * `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation — so a
100
- * fee-charging source with no `opts.feeSource` throws
99
+ * Each source's fetch + build runs against its own infra, guaranteeing
100
+ * per-rule PTB atomicity. A fee-source pre-check runs first (any listed
101
+ * source's `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
102
+ * so a fee-charging source with no `opts.feeSource` throws
101
103
  * `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
102
- * moveCalls. Only once that check passes does the off-chain fetch run and
103
- * complete before any PTB mutation; on-chain reads inside `buildUpdateCalls`
104
- * can still fail mid-append for other reasons — callers discard the tx on any
105
- * throw.
104
+ * moveCalls. Only once that check passes do the off-chain fetches run — in
105
+ * parallel across sources — and ALL settle before the first PTB mutation;
106
+ * on-chain reads inside `buildUpdateCalls` can still fail mid-append for
107
+ * other reasons — callers discard the tx on any throw.
106
108
  *
107
109
  * **Collector-feed leg is rule-aware:** a lazer-served group's
108
110
  * `buildUpdateCalls` returns the verified `Update` PTB value
@@ -137,7 +139,7 @@ export declare function refreshOraclePrices(tx: Transaction, host: OracleHost, t
137
139
  /**
138
140
  * @internal Test-only: layer fake `PriceUpdateRule`s on top of the
139
141
  * production registry (see `rule-registry.ts`'s `resolveOracleRule`).
140
- * Production callers never set this — routing is by `host.oracleSource`
142
+ * Production callers never set this — routing is by `host.oracleSources`
141
143
  * alone.
142
144
  */
143
145
  ruleOverrides?: Partial<Record<OracleSource, PriceUpdateRule>>;
@@ -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;
@@ -34,6 +35,7 @@ const constant_rule_ts_1 = require("./rules/constant-rule.js");
34
35
  const pyth_lazer_rule_ts_1 = require("./rules/pyth-lazer-rule.js");
35
36
  const pyth_rule_ts_1 = require("./rules/pyth-rule.js");
36
37
  const supra_rule_ts_1 = require("./rules/supra-rule.js");
38
+ const waterx_rule_ts_1 = require("./rules/waterx-rule.js");
37
39
  /**
38
40
  * Resolve one group's off-chain update payload for {@link refreshOraclePrices}:
39
41
  * try `provider.get(source, tickers)` first (when a provider is configured),
@@ -116,6 +118,20 @@ function aggregateTicker(tx, host, args) {
116
118
  (0, pyth_lazer_rule_ts_1.feedLazerRule)(tx, host, collector, args.lazerUpdate);
117
119
  fed = true;
118
120
  }
121
+ if (args.waterxEnvelope !== undefined) {
122
+ // waterx_rule::collect_batch_latest verifies the batch signature and feeds
123
+ // this collector's symbol from the batch. If the ticker's aggregator does
124
+ // not (yet) weight `WaterxRule`, the contribution is silently dropped
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.
132
+ (0, waterx_rule_ts_1.feedWaterxRule)(tx, host, collector, args.waterxEnvelope);
133
+ fed = true;
134
+ }
119
135
  if (fed) {
120
136
  // Supra rides on the same collector when enabled (abstains on-chain otherwise).
121
137
  (0, supra_rule_ts_1.maybeFeedSupra)(tx, host, collector);
@@ -164,27 +180,26 @@ function aggregateTickerWithConstant(tx, host, args) {
164
180
  * entry, Lazer if the lazer update leg served it — see below — Supra when
165
181
  * enabled, Constant when it's a constant ticker).
166
182
  *
167
- * Before that, the on-chain price *update* leg is routed by `host.oracleSource`
168
- * (see `rule-registry.ts`): the ONE selected source serves every ticker in its
169
- * `supportedTickers(host)`. There is **no cross-source fallback** — a requested
170
- * ticker the selected source does not serve, and that is not a constant-only
171
- * ticker (which needs no price-update leg), fails the build immediately with a
172
- * clear error naming the ticker and source. That is the deliberate "fail the
173
- * tx-build, don't silently reroute" contract: a wrong-but-present feed id is
174
- * NOT validated here (it surfaces on-chain at dry-run); a MISSING feed for the
175
- * selected source is caught here. When the selected source's feed exists but is
176
- * wrong, this function does nothing special — the on-chain aggregate aborts at
177
- * 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.
178
193
  *
179
- * The selected source's fetch + build runs against its own infra, guaranteeing
180
- * per-rule PTB atomicity. A fee-source pre-check runs first (the source's
181
- * `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation — so a
182
- * 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
183
198
  * `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
184
- * moveCalls. Only once that check passes does the off-chain fetch run and
185
- * complete before any PTB mutation; on-chain reads inside `buildUpdateCalls`
186
- * can still fail mid-append for other reasons — callers discard the tx on any
187
- * 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.
188
203
  *
189
204
  * **Collector-feed leg is rule-aware:** a lazer-served group's
190
205
  * `buildUpdateCalls` returns the verified `Update` PTB value
@@ -205,64 +220,122 @@ function aggregateTickerWithConstant(tx, host, args) {
205
220
  async function refreshOraclePrices(tx, host, tickers, opts = {}) {
206
221
  if (tickers.length === 0)
207
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)];
208
230
  // price_info_object lookup for every ticker with a pyth_rule.feeds entry —
209
231
  // needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
210
232
  // which rule performed the on-chain update for that ticker.
211
233
  const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
212
234
  const priceInfoByTicker = new Map();
213
235
  pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
214
- // ONE source, no fallback. The selected source serves the tickers in its
215
- // `supportedTickers(host)`; `source` is tracked alongside the group (rather
216
- // than read back off `rule.kind`, typed as the broader PriceUpdateRuleKind)
217
- // so the provider lookup below has an OracleSource to key on without a cast.
218
- const selectedRule = (0, rule_registry_ts_1.resolveOracleRule)(host.oracleSource, opts.ruleOverrides);
219
- const selectedSupported = new Set(selectedRule.supportedTickers(host));
220
- // Fail the tx-build (NOT client init, NOT a silent reroute) when the selected
221
- // 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
222
257
  // update. Only a CONSTANT-ONLY ticker is exempt — priced entirely by
223
258
  // `constant_rule`, it needs no update leg from any source. A DUAL-FEED ticker
224
259
  // (constant AND pyth) still needs its Pyth leg refreshed, so `isConstantTicker`
225
- // alone must NOT exempt it: under a source that can't serve it, with no
226
- // fallback, feeding an unrefreshed Pyth leg would price it stale (or abort on a
227
- // missing weighted source). `priceInfoByTicker.has(t)` ⇔ the ticker has a
228
- // `pyth_rule.feeds` entry, so `constant && !hasPyth` is exactly constant-only.
229
- // This catches a MISSING feed; a present-but-WRONG feed id is deliberately not
230
- // 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));
231
267
  const isConstantOnly = (t) => host.isConstantTicker(t) && !priceInfoByTicker.has(t);
232
- const unservable = tickers.filter((t) => !selectedSupported.has(t) && !isConstantOnly(t));
268
+ const unservable = tickers.filter((t) => !covered.has(t) && !isConstantOnly(t));
233
269
  if (unservable.length > 0) {
234
- 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): ` +
235
272
  `${unservable.join(", ")}. Sources are self-contained with no fallback — add ` +
236
- `${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.`);
237
274
  }
238
- const selectedGroup = tickers.filter((t) => selectedSupported.has(t));
239
- // Fee-source pre-check, hoisted ABOVE the off-chain fetch and PTB build below.
240
- // It consults only `rule.requiresFeeSource` known before any fetch or PTB
241
- // mutation — so a fee-charging source with no `feeSource` throws with ZERO
242
- // wasted network calls and zero PTB commands, rather than waiting for
243
- // `buildPythPriceUpdateCalls`'s own per-call guard to fire after the off-chain
244
- // fetch already ran.
245
- 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)) {
246
282
  throw new pyth_ts_1.OracleFeeSourceUnavailableError();
247
283
  }
248
- // Resolve + build the selected source's update leg. The off-chain fetch
249
- // settles before the first PTB mutation, so a fetch failure never strands
250
- // moveCalls in a caller-owned tx. Map each lazer-served ticker to the one
251
- // 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.
252
298
  const lazerUpdateByTicker = new Map();
253
- if (selectedGroup.length > 0) {
254
- const group = { source: host.oracleSource, rule: selectedRule, tickers: selectedGroup };
255
- const data = await resolveGroupUpdateData(host, group, opts.updateDataProvider);
256
- const handle = (await selectedRule.buildUpdateCalls(tx, host, data, {
299
+ // Signed batch envelope per waterx-served ticker. Unlike Lazer's shared PTB
300
+ // handle, waterx's verify+feed is bundled into `collect_batch_latest` in the
301
+ // per-ticker feed leg, so its `buildUpdateCalls` emits nothing and the
302
+ // envelope is carried straight from the group's fetched data.
303
+ const waterxEnvelopeByTicker = new Map();
304
+ for (const [i, group] of groups.entries()) {
305
+ const data = dataByGroup[i] ?? null;
306
+ const handle = (await group.rule.buildUpdateCalls(tx, host, data, {
257
307
  cache: opts.cache,
258
308
  feeSource: opts.feeSource,
259
309
  })) ?? undefined;
260
- // Route by the handle's kind discriminant — the one site the tag exists to
261
- // protect: a future non-lazer handle (e.g. a WaterxRule value) must never
262
- // be silently fed into pyth_lazer_rule::feed.
263
- if (handle?.kind === "pyth_lazer_rule") {
264
- for (const ticker of selectedGroup)
265
- lazerUpdateByTicker.set(ticker, handle.update);
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)}'`);
338
+ }
266
339
  }
267
340
  }
268
341
  // Aggregate each ticker, feeding whichever rules it is configured for.
@@ -271,6 +344,7 @@ async function refreshOraclePrices(tx, host, tickers, opts = {}) {
271
344
  ticker,
272
345
  priceInfoObjectId: priceInfoByTicker.get(ticker),
273
346
  lazerUpdate: lazerUpdateByTicker.get(ticker),
347
+ waterxEnvelope: waterxEnvelopeByTicker.get(ticker),
274
348
  });
275
349
  }
276
350
  }
@@ -11,7 +11,7 @@
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";
14
+ import type { FetchPolicy } from "./update-fetch.ts";
15
15
  export interface PythRulePackage extends BasePackageEntry {
16
16
  config: string;
17
17
  feeds: Record<string, {
@@ -30,7 +30,7 @@ export interface PythSponsorRulePackage extends BasePackageEntry {
30
30
  *
31
31
  * `enabled` mirrors the JSON field verbatim but MUST NOT be read for routing —
32
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
33
+ * create option (see `OracleHost.oracleSources`), never by this flag or any
34
34
  * other config value.
35
35
  */
36
36
  export interface PythLazerRulePackage extends BasePackageEntry {
@@ -82,6 +82,40 @@ export interface SupraRulePackage extends BasePackageEntry {
82
82
  */
83
83
  enabled?: boolean;
84
84
  }
85
+ /**
86
+ * Per-ticker `waterx_rule` feed entry. Keyed in `feeds` by the oracle **ticker**
87
+ * (e.g. `"SUIUSD"`, the same key `pyth_rule.feeds` uses), so
88
+ * `Object.keys(feeds)` is the SDK's supported-ticker set. The fields are
89
+ * informational off-chain — the SDK keys routing/support off the entry's
90
+ * presence and pushes the enclave-signed price verbatim, never re-deriving it.
91
+ */
92
+ export interface WaterxRuleFeedEntry {
93
+ /** Exchange ticker the quote-center aggregates from (e.g. `"SUIUSDT"`). */
94
+ ticker?: string;
95
+ }
96
+ /**
97
+ * `waterx_rule` deployment entry — the first-party Nautilus-TEE oracle rule.
98
+ * Read by `WaterxRule` (`rules/waterx-rule.ts`): `feeds` for ticker support,
99
+ * `config`/`enclave_config`/`enclave` for the `collect_batch_latest` call,
100
+ * `published_at` for the package address. The off-chain signed price is pulled
101
+ * from the quote-center (endpoint from the rule-owned `WATERX_INFRA` table in
102
+ * `rules/waterx-rule.ts`), 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.oracleSources`), 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,19 +133,10 @@ 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
  }
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
140
  /**
116
141
  * The caller-tunable subset of `fetchWithPolicy`'s policy exposed on the
117
142
  * `pythFetch` create option and `client.pyth.fetch` — the retry/timeout budget
@@ -124,18 +149,24 @@ export type PythFetchPolicy = {
124
149
  timeoutMs?: number;
125
150
  retries?: number;
126
151
  };
127
- export interface PythInfraConfig {
128
- state_id: string;
129
- wormhole_state_id: string;
130
- 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 {
131
163
  /**
132
- * Pyth Pro / Lazer access token (`Authorization: Bearer …`) for
164
+ * Pyth access token (`Authorization: Bearer …`). Required by
133
165
  * `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
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
139
170
  * https://docs.pyth.network/price-feeds/core/upgrade) this is ALSO required
140
171
  * for `pyth_rule`'s Hermes fetch (`fetchPriceFeedsUpdateData`) — see
141
172
  * `fetch` below.
@@ -151,27 +182,37 @@ export interface PythInfraConfig {
151
182
  */
152
183
  fetch?: PythFetchPolicy;
153
184
  }
154
- export declare const PYTH_DEFAULTS: Record<Network, PythInfraConfig>;
155
185
  /**
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.
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.
160
191
  *
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.
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.
170
198
  */
171
- export declare const LAZER_DEFAULTS: Record<Network, {
172
- endpoint: string;
173
- verifier_package: string;
174
- }>;
199
+ export interface WaterxAccessConfig {
200
+ /**
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
204
+ * `…/api/quote-center/v1/quotes/update` and a proxy route is not rewritten
205
+ * away. A trailing slash is trimmed.
206
+ */
207
+ endpoint?: string;
208
+ /**
209
+ * Retry/timeout policy (and `fetchImpl`) for the quote-center fetch — see
210
+ * `fetchWithPolicy` (`./update-fetch.ts`). Supplied via the `waterxFetch`
211
+ * create option. Falls back to `fetchWithPolicy`'s built-in defaults (15s
212
+ * timeout, 2 retries) when unset — never to another source's policy.
213
+ */
214
+ fetch?: FetchPolicy;
215
+ }
175
216
  /**
176
217
  * The narrow config shape the oracle/refresh code needs. `WaterXConfig`
177
218
  * (the perp line's full config) is assignable to this, so `PerpClient` satisfies
@@ -11,45 +11,3 @@
11
11
  * mirrors the earlier account-config hoist.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- 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
- };
@@ -9,26 +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 } 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
+ /**
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
+ */
31
+ readonly waterx?: WaterxAccessConfig;
21
32
  /** gRPC client for the on-chain reads the Pyth update path needs. */
22
33
  readonly grpcClient: SuiGrpcClient;
23
34
  /**
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.
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).
30
45
  */
31
- readonly oracleSource: OracleSource;
46
+ readonly oracleSources: readonly OracleSource[];
32
47
  /** True when `ticker` is priced by `constant_rule`. */
33
48
  isConstantTicker(ticker: string): boolean;
34
49
  /** The `supra_rule` config when deployed, enabled, and fully wired; else `undefined`. */