@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>>;
@@ -19,7 +19,8 @@
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 { aggregate as aggregateCall, newCollector } from "../generated/waterx_oracle/oracle.js";
25
26
  import { OracleFeeSourceUnavailableError } from "./pyth.js";
@@ -28,6 +29,7 @@ import { feedConstantRule } from "./rules/constant-rule.js";
28
29
  import { feedLazerRule } from "./rules/pyth-lazer-rule.js";
29
30
  import { feedPythRule } from "./rules/pyth-rule.js";
30
31
  import { maybeFeedSupra } from "./rules/supra-rule.js";
32
+ import { feedWaterxRule, waterxEnvelopeOf, } from "./rules/waterx-rule.js";
31
33
  /**
32
34
  * Resolve one group's off-chain update payload for {@link refreshOraclePrices}:
33
35
  * try `provider.get(source, tickers)` first (when a provider is configured),
@@ -110,6 +112,20 @@ export function aggregateTicker(tx, host, args) {
110
112
  feedLazerRule(tx, host, collector, args.lazerUpdate);
111
113
  fed = true;
112
114
  }
115
+ if (args.waterxEnvelope !== undefined) {
116
+ // waterx_rule::collect_batch_latest verifies the batch signature and feeds
117
+ // this collector's symbol from the batch. If the ticker's aggregator does
118
+ // not (yet) weight `WaterxRule`, the contribution is silently dropped
119
+ // on-chain — feeding ahead of the weight migration is safe for THIS tx.
120
+ // CAVEAT (unlike lazer): the feed call records a per-symbol signed-
121
+ // timestamp high-water mark REGARDLESS of weights, and a replayed
122
+ // timestamp ABORTS (`EReplayedSignature`, audit F-014) — so two PTBs
123
+ // carrying the same envelope for the same symbol cannot both land; the
124
+ // second aborts even where waterx is unweighted. See WaterxRule's module
125
+ // header.
126
+ feedWaterxRule(tx, host, collector, args.waterxEnvelope);
127
+ fed = true;
128
+ }
113
129
  if (fed) {
114
130
  // Supra rides on the same collector when enabled (abstains on-chain otherwise).
115
131
  maybeFeedSupra(tx, host, collector);
@@ -158,27 +174,26 @@ export function aggregateTickerWithConstant(tx, host, args) {
158
174
  * entry, Lazer if the lazer update leg served it — see below — Supra when
159
175
  * enabled, Constant when it's a constant ticker).
160
176
  *
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.
177
+ * Before that, the on-chain price *update* leg is routed by the
178
+ * `host.oracleSources` fed set (see `rule-registry.ts`): EVERY listed source
179
+ * updates the tickers its own `supportedTickers(host)` serves, all in this one
180
+ * PTB. There is **no cross-source fallback** a requested ticker NO listed
181
+ * source serves, and that is not a constant-only ticker (which needs no
182
+ * price-update leg), fails the build immediately with a clear error naming
183
+ * the ticker and the list. That is the deliberate "fail the tx-build, don't
184
+ * silently reroute" contract: a wrong-but-present feed id is NOT validated
185
+ * here (it surfaces on-chain at dry-run); a ticker MISSING from every listed
186
+ * source's feeds is caught here.
172
187
  *
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
188
+ * Each source's fetch + build runs against its own infra, guaranteeing
189
+ * per-rule PTB atomicity. A fee-source pre-check runs first (any listed
190
+ * source's `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
191
+ * so a fee-charging source with no `opts.feeSource` throws
177
192
  * `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.
193
+ * moveCalls. Only once that check passes do the off-chain fetches run — in
194
+ * parallel across sources — and ALL settle before the first PTB mutation;
195
+ * on-chain reads inside `buildUpdateCalls` can still fail mid-append for
196
+ * other reasons — callers discard the tx on any throw.
182
197
  *
183
198
  * **Collector-feed leg is rule-aware:** a lazer-served group's
184
199
  * `buildUpdateCalls` returns the verified `Update` PTB value
@@ -199,64 +214,122 @@ export function aggregateTickerWithConstant(tx, host, args) {
199
214
  export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
200
215
  if (tickers.length === 0)
201
216
  return;
217
+ // Dedupe the caller's list (order-preserving): a repeated ticker would
218
+ // otherwise aggregate TWICE in this one PTB — wasted gas for every rule,
219
+ // and a hard ABORT under waterx: the second `collect_batch_latest` carries
220
+ // the same envelope, and the on-chain per-symbol replay guard rejects an
221
+ // already-accepted signed timestamp (`EReplayedSignature`, F-014) even
222
+ // inside a single transaction.
223
+ tickers = [...new Set(tickers)];
202
224
  // price_info_object lookup for every ticker with a pyth_rule.feeds entry —
203
225
  // needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
204
226
  // which rule performed the on-chain update for that ticker.
205
227
  const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
206
228
  const priceInfoByTicker = new Map();
207
229
  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
230
+ // The fed set is a LIST (`host.oracleSources`, normalized + deduped at
231
+ // client creation): ONE build carries every listed source's data, and the
232
+ // chain's per-ticker weight tables decide which contributions count
233
+ // feeding an unweighted rule's PRICE is dropped on-chain, while starving a
234
+ // weighted one aborts. That asymmetry is what makes weight migrations
235
+ // safe: flip weights per ticker at any time while the fed set stays a
236
+ // superset of every ticker's weighted set. (One caveat: waterx's feed call
237
+ // burns a per-symbol signed-timestamp high-water mark regardless of
238
+ // weights — see aggregateTicker's waterx branch.) Still NO fallback
239
+ // BETWEEN sources: each group serves only the tickers its own feeds list.
240
+ // Zero-ticker groups are dropped here so everything downstream (fee check,
241
+ // fetch fan-out, update-leg build) can assume every group has work.
242
+ const groups = host.oracleSources
243
+ .map((source) => {
244
+ const rule = resolveOracleRule(source, opts.ruleOverrides);
245
+ const supported = new Set(rule.supportedTickers(host));
246
+ return { source, rule, tickers: tickers.filter((t) => supported.has(t)) };
247
+ })
248
+ .filter((group) => group.tickers.length > 0);
249
+ // Fail the tx-build (NOT client init, NOT a silent reroute) when NO listed
250
+ // source has a feed for a requested ticker that actually needs a price
216
251
  // update. Only a CONSTANT-ONLY ticker is exempt — priced entirely by
217
252
  // `constant_rule`, it needs no update leg from any source. A DUAL-FEED ticker
218
253
  // (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).
254
+ // alone must NOT exempt it: with no source able to serve it, feeding an
255
+ // unrefreshed Pyth leg would price it stale (or abort on a missing weighted
256
+ // source). `priceInfoByTicker.has(t)` ⇔ the ticker has a `pyth_rule.feeds`
257
+ // entry, so `constant && !hasPyth` is exactly constant-only. This catches a
258
+ // MISSING feed; a present-but-WRONG feed id is deliberately not validated
259
+ // here (it aborts on-chain at dry-run).
260
+ const covered = new Set(groups.flatMap((group) => group.tickers));
225
261
  const isConstantOnly = (t) => host.isConstantTicker(t) && !priceInfoByTicker.has(t);
226
- const unservable = tickers.filter((t) => !selectedSupported.has(t) && !isConstantOnly(t));
262
+ const unservable = tickers.filter((t) => !covered.has(t) && !isConstantOnly(t));
227
263
  if (unservable.length > 0) {
228
- throw new Error(`oracleSource '${host.oracleSource}' has no feed configured for ticker(s): ` +
264
+ const sources = host.oracleSources.join(", ");
265
+ throw new Error(`oracleSource [${sources}] has no feed configured for ticker(s): ` +
229
266
  `${unservable.join(", ")}. Sources are self-contained with no fallback — add ` +
230
- `${host.oracleSource} feeds for them, or select a source that serves them.`);
267
+ `feeds for them under a listed source, or list a source that serves them.`);
231
268
  }
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) {
269
+ // Fee-source pre-check, hoisted ABOVE the off-chain fetches and PTB build
270
+ // below. It consults only `rule.requiresFeeSource` known before any fetch
271
+ // or PTB mutationso a fee-charging source (Pyth Core) in the fed set with
272
+ // no `feeSource` throws with ZERO wasted network calls and zero PTB
273
+ // commands, rather than waiting for `buildPythPriceUpdateCalls`'s own
274
+ // per-call guard to fire after the off-chain fetches already ran.
275
+ if (!opts.feeSource && groups.some((group) => group.rule.requiresFeeSource)) {
240
276
  throw new OracleFeeSourceUnavailableError();
241
277
  }
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.
278
+ // Phase 1 resolve every group's update data IN PARALLEL: the per-source
279
+ // fetches (Hermes VAA / Lazer POST / quote-center GET) are independent
280
+ // network calls on the tx-build money path, so a multi-source fed set must
281
+ // not pay one RTT per source sequentially. ALL fetches settle before the
282
+ // first PTB mutation below, so a fetch failure never strands moveCalls in a
283
+ // caller-owned tx — and a failure in ANY group fails the whole build (a
284
+ // listed source is load-bearing; silently building without it would starve
285
+ // its weighted tickers on-chain).
286
+ const dataByGroup = await Promise.all(groups.map((group) => resolveGroupUpdateData(host, group, opts.updateDataProvider)));
287
+ // Phase 2 — build each group's update leg sequentially, in list order, so
288
+ // PTB command order stays deterministic. The carry step below is an
289
+ // exhaustive switch over the group's rule kind: a future source whose feed
290
+ // leg needs per-ticker data from its update leg must decide its carry here
291
+ // — falling through silently would starve its weighted tickers on-chain.
246
292
  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, {
293
+ // Signed batch envelope per waterx-served ticker. Unlike Lazer's shared PTB
294
+ // handle, waterx's verify+feed is bundled into `collect_batch_latest` in the
295
+ // per-ticker feed leg, so its `buildUpdateCalls` emits nothing and the
296
+ // envelope is carried straight from the group's fetched data.
297
+ const waterxEnvelopeByTicker = new Map();
298
+ for (const [i, group] of groups.entries()) {
299
+ const data = dataByGroup[i] ?? null;
300
+ const handle = (await group.rule.buildUpdateCalls(tx, host, data, {
251
301
  cache: opts.cache,
252
302
  feeSource: opts.feeSource,
253
303
  })) ?? 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);
304
+ switch (group.rule.kind) {
305
+ case "pyth_rule":
306
+ // Core's update leg wrote the PriceInfoObjects in place — the feed
307
+ // leg reads them by id (`priceInfoByTicker`), nothing to carry.
308
+ break;
309
+ case "pyth_lazer_rule":
310
+ // Route by the handle's kind discriminant — the tag exists so a
311
+ // non-lazer handle can never be silently fed into
312
+ // pyth_lazer_rule::feed.
313
+ if (handle?.kind === "pyth_lazer_rule") {
314
+ for (const ticker of group.tickers)
315
+ lazerUpdateByTicker.set(ticker, handle.update);
316
+ }
317
+ break;
318
+ case "waterx_rule": {
319
+ // waterx_rule emits no shared handle (verify+feed is bundled into the
320
+ // per-ticker `collect_batch_latest`), so the envelope is carried
321
+ // straight from this group's fetched data to the feed leg below.
322
+ const envelope = waterxEnvelopeOf(data);
323
+ if (envelope) {
324
+ for (const ticker of group.tickers)
325
+ waterxEnvelopeByTicker.set(ticker, envelope);
326
+ }
327
+ break;
328
+ }
329
+ default: {
330
+ const exhausted = group.rule.kind;
331
+ throw new Error(`refreshOraclePrices: unhandled rule kind '${String(exhausted)}'`);
332
+ }
260
333
  }
261
334
  }
262
335
  // Aggregate each ticker, feeding whichever rules it is configured for.
@@ -265,6 +338,7 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
265
338
  ticker,
266
339
  priceInfoObjectId: priceInfoByTicker.get(ticker),
267
340
  lazerUpdate: lazerUpdateByTicker.get(ticker),
341
+ waterxEnvelope: waterxEnvelopeByTicker.get(ticker),
268
342
  });
269
343
  }
270
344
  }
@@ -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
@@ -9,44 +9,4 @@
9
9
  * depends on this shared schema instead of the perp line's full config —
10
10
  * mirrors the earlier account-config hoist.
11
11
  */
12
- export const PYTH_DEFAULTS = {
13
- MAINNET: {
14
- state_id: "0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8",
15
- wormhole_state_id: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
16
- hermes_endpoint: "https://hermes.pyth.network",
17
- },
18
- TESTNET: {
19
- state_id: "0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c",
20
- wormhole_state_id: "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
21
- hermes_endpoint: "https://hermes-beta.pyth.network",
22
- },
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
- };
12
+ export {};
@@ -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`. */