@waterx/sdk 2.3.1-dev.0 → 2.4.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.
@@ -70,35 +70,23 @@ export declare class WaterXClient {
70
70
  price_info_object: string;
71
71
  };
72
72
  /**
73
- * True when `ticker` is priced by `waterx_constant_rule` (a constant pin,
73
+ * True when `ticker` is priced by `constant_rule` (a constant pin,
74
74
  * e.g. `USDCUSD → $1`) rather than Pyth. Such tickers are fed via
75
75
  * `constant_rule::feed` and need no Pyth update; see {@link refreshOraclePrices}.
76
76
  *
77
77
  * All-or-nothing, mirroring {@link getSupraRule} and the keeper: only routes a
78
78
  * ticker to the constant rule when the rule is FULLY wired (`published_at` +
79
- * `config` present). A half-populated block — `prices` listed before the rule
80
- * is deployed, a realistic mid-rollout state — would otherwise make this true
81
- * while {@link aggregateTickerWithConstant} throws, aborting the whole
82
- * price-refresh PTB instead of safely falling back to Pyth.
79
+ * `config` present). A half-populated block — a `feeds` entry listed before the
80
+ * rule is deployed, a realistic mid-rollout state — would otherwise make this
81
+ * true while {@link aggregateTicker} throws, aborting the whole price-refresh PTB
82
+ * instead of safely falling back to Pyth.
83
+ *
84
+ * Whether a constant ticker is *also* Pyth-fed (the dual-feed transition state)
85
+ * or constant-only is not a separate flag — it falls out of whether the ticker
86
+ * still has a `pyth_rule.feeds` entry. {@link aggregateTicker} feeds each rule the
87
+ * ticker is configured for, so no `isDualFeed` / `isConstantOnly` predicate is needed.
83
88
  */
84
89
  isConstantTicker(ticker: string): boolean;
85
- /**
86
- * True when `ticker` is mid-migration and must be fed by *both* Pyth and the
87
- * constant rule into one collector — so the aggregator can hold the
88
- * `{Pyth, Constant}` weight set without an `EMissingPriceSource` window while
89
- * rule weights are flipped (on-chain `aggregator::remove_outliers` requires
90
- * every weighted rule present in the collector). Listed in
91
- * `waterx_constant_rule.dual_feed`; clamped to `prices` so a stray entry that
92
- * isn't a real constant ticker can't double-feed against no weight. See
93
- * {@link aggregateTickerWithDual} and the USDCUSD runbook in `waterx-contract`.
94
- */
95
- isDualFeedTicker(ticker: string): boolean;
96
- /**
97
- * True when `ticker` is fed by the constant rule *alone* (steady state): no
98
- * Pyth feed call, so it skips the Pyth update. Dual-feed tickers are constant
99
- * *and* Pyth, so they are NOT constant-only.
100
- */
101
- isConstantOnlyTicker(ticker: string): boolean;
102
90
  /**
103
91
  * The `supra_rule` config when it is deployed, enabled, and fully wired
104
92
  * (`config` + `oracle_holder`), else `undefined`. When present, callers feed
@@ -118,48 +118,27 @@ export class WaterXClient {
118
118
  return f;
119
119
  }
120
120
  /**
121
- * True when `ticker` is priced by `waterx_constant_rule` (a constant pin,
121
+ * True when `ticker` is priced by `constant_rule` (a constant pin,
122
122
  * e.g. `USDCUSD → $1`) rather than Pyth. Such tickers are fed via
123
123
  * `constant_rule::feed` and need no Pyth update; see {@link refreshOraclePrices}.
124
124
  *
125
125
  * All-or-nothing, mirroring {@link getSupraRule} and the keeper: only routes a
126
126
  * ticker to the constant rule when the rule is FULLY wired (`published_at` +
127
- * `config` present). A half-populated block — `prices` listed before the rule
128
- * is deployed, a realistic mid-rollout state — would otherwise make this true
129
- * while {@link aggregateTickerWithConstant} throws, aborting the whole
130
- * price-refresh PTB instead of safely falling back to Pyth.
127
+ * `config` present). A half-populated block — a `feeds` entry listed before the
128
+ * rule is deployed, a realistic mid-rollout state — would otherwise make this
129
+ * true while {@link aggregateTicker} throws, aborting the whole price-refresh PTB
130
+ * instead of safely falling back to Pyth.
131
+ *
132
+ * Whether a constant ticker is *also* Pyth-fed (the dual-feed transition state)
133
+ * or constant-only is not a separate flag — it falls out of whether the ticker
134
+ * still has a `pyth_rule.feeds` entry. {@link aggregateTicker} feeds each rule the
135
+ * ticker is configured for, so no `isDualFeed` / `isConstantOnly` predicate is needed.
131
136
  */
132
137
  isConstantTicker(ticker) {
133
- const c = this.config.packages.waterx_constant_rule;
134
- if (!c?.published_at || !c.config)
135
- return false;
136
- return c.prices?.[ticker] !== undefined;
137
- }
138
- /**
139
- * True when `ticker` is mid-migration and must be fed by *both* Pyth and the
140
- * constant rule into one collector — so the aggregator can hold the
141
- * `{Pyth, Constant}` weight set without an `EMissingPriceSource` window while
142
- * rule weights are flipped (on-chain `aggregator::remove_outliers` requires
143
- * every weighted rule present in the collector). Listed in
144
- * `waterx_constant_rule.dual_feed`; clamped to `prices` so a stray entry that
145
- * isn't a real constant ticker can't double-feed against no weight. See
146
- * {@link aggregateTickerWithDual} and the USDCUSD runbook in `waterx-contract`.
147
- */
148
- isDualFeedTicker(ticker) {
149
- const c = this.config.packages.waterx_constant_rule;
138
+ const c = this.config.packages.constant_rule;
150
139
  if (!c?.published_at || !c.config)
151
140
  return false;
152
- if (c.prices?.[ticker] === undefined)
153
- return false;
154
- return c.dual_feed?.includes(ticker) ?? false;
155
- }
156
- /**
157
- * True when `ticker` is fed by the constant rule *alone* (steady state): no
158
- * Pyth feed call, so it skips the Pyth update. Dual-feed tickers are constant
159
- * *and* Pyth, so they are NOT constant-only.
160
- */
161
- isConstantOnlyTicker(ticker) {
162
- return this.isConstantTicker(ticker) && !this.isDualFeedTicker(ticker);
141
+ return c.feeds?.[ticker] !== undefined;
163
142
  }
164
143
  /**
165
144
  * The `supra_rule` config when it is deployed, enabled, and fully wired
@@ -25,31 +25,40 @@ export interface PythRulePackage extends BasePackageEntry {
25
25
  export interface PythSponsorRulePackage extends BasePackageEntry {
26
26
  pyth_sponsor: string;
27
27
  }
28
+ /** Per-ticker `constant_rule` feed entry (mirrors the `pyth_rule.feeds` shape). */
29
+ export interface ConstantFeedEntry {
30
+ /**
31
+ * Constant 1e9-scaled price (decimal string), mirroring the on-chain
32
+ * `Config.prices` value (e.g. `"1000000000"` for $1). Informational off-chain —
33
+ * the on-chain `constant_rule::feed` reads the price from `Config`; the SDK only
34
+ * keys routing off the presence of the entry.
35
+ */
36
+ price: string;
37
+ }
28
38
  export interface WaterxConstantRulePackage extends BasePackageEntry {
29
39
  /** Shared `constant_rule::Config` holding the per-ticker constant prices. */
30
40
  config: string;
31
41
  /**
32
- * Oracle ticker → constant 1e9-scaled price (decimal string), mirroring the
33
- * on-chain `Config.prices` map. A ticker present here is fed via
34
- * `constant_rule::feed` instead of `pyth_rule::feed` (e.g. `USDCUSD → "1000000000"`).
42
+ * Oracle ticker → constant feed entry, mirroring `pyth_rule.feeds`. A ticker
43
+ * present here is fed via `constant_rule::feed` instead of (steady state) or
44
+ * alongside (dual-feed) `pyth_rule::feed` (e.g. `USDCUSD → { price: "1000000000" }`).
35
45
  */
36
- prices?: Record<string, string>;
37
- /**
38
- * Tickers in transition (subset of `prices`): fed via *both* `pyth_rule::feed`
39
- * and `constant_rule::feed` into one collector so the aggregator can hold the
40
- * `{Pyth, Constant}` weight set without an `EMissingPriceSource` window while
41
- * rule weights are flipped. Empty/absent (steady state) → each `prices` ticker
42
- * is constant-only. See the USDCUSD migration runbook in `waterx-contract`.
43
- */
44
- dual_feed?: string[];
46
+ feeds?: Record<string, ConstantFeedEntry>;
47
+ }
48
+ /** Per-ticker `supra_rule` feed entry (mirrors the `pyth_rule.feeds` shape). */
49
+ export interface SupraFeedEntry {
50
+ /** Supra pair id (mirrors the on-chain `Config`; informational off-chain). */
51
+ pair_id: number;
52
+ /** Optional per-ticker freshness tolerance override (ms). */
53
+ tolerance_ms?: number;
45
54
  }
46
55
  export interface SupraRulePackage extends BasePackageEntry {
47
56
  /** Shared `supra_rule::Config` (per-symbol Supra pair_id + freshness tolerance). */
48
57
  config: string;
49
58
  /** Supra `OracleHolder` shared object id (network-specific). Required to feed. */
50
59
  oracle_holder?: string;
51
- /** Oracle ticker → Supra pair id (mirrors the on-chain `Config`; informational). */
52
- pairs?: Record<string, number>;
60
+ /** Oracle ticker → Supra feed entry (mirrors the on-chain `Config`; informational). */
61
+ feeds?: Record<string, SupraFeedEntry>;
53
62
  /**
54
63
  * When true AND `config` + `oracle_holder` are set, `refreshOraclePrices`
55
64
  * feeds `supra_rule` on the same `PriceCollector` as Pyth before `aggregate`
@@ -190,7 +199,7 @@ export interface WaterXPackages {
190
199
  waterx_referral?: WaterxReferralPackage;
191
200
  pyth_rule: PythRulePackage;
192
201
  pyth_sponsor_rule?: PythSponsorRulePackage;
193
- waterx_constant_rule?: WaterxConstantRulePackage;
202
+ constant_rule?: WaterxConstantRulePackage;
194
203
  supra_rule?: SupraRulePackage;
195
204
  waterx_account: WxaAccountPackage;
196
205
  waterx_oracle: WaterxOraclePackage;
@@ -1,7 +1,7 @@
1
1
  export { WaterXClient } from "./client.ts";
2
2
  export type { CreateClientOptions } from "./client.ts";
3
3
  export { PYTH_DEFAULTS, WORMHOLE_DEFAULTS, clearConfigCache, defaultConfigUrl, loadConfig, } from "./config.ts";
4
- export type { BasePackageEntry, WaterxReferralPackage, LoadConfigOptions, NativeCustodyAsset, NativeCustodyPackage, PythInfraConfig, PythRulePackage, PythSponsorRulePackage, SupraRulePackage, TestnetFaucetPackage, TrustedEmitterRow, WaterXConfig, WaterXPackages, WaterxCreditPackage, WaterxOraclePackage, WaterxPerpMarketEntry, WaterxPerpPackage, WaterxStakingPackage, WithdrawalQueuePackage, WlpPackage, WormholeBridgePackage, WormholeInfraConfig, WxaAccountPackage, } from "./config.ts";
4
+ export type { BasePackageEntry, ConstantFeedEntry, WaterxReferralPackage, LoadConfigOptions, NativeCustodyAsset, NativeCustodyPackage, PythInfraConfig, PythRulePackage, PythSponsorRulePackage, SupraFeedEntry, SupraRulePackage, TestnetFaucetPackage, TrustedEmitterRow, WaterXConfig, WaterXPackages, WaterxCreditPackage, WaterxOraclePackage, WaterxPerpMarketEntry, WaterxPerpPackage, WaterxStakingPackage, WithdrawalQueuePackage, WlpPackage, WormholeBridgePackage, WormholeInfraConfig, WxaAccountPackage, } from "./config.ts";
5
5
  export { ACTION_ADD_PRE_ORDER, ACTION_CANCEL_ORDER, ACTION_CANCEL_PRE_ORDER, ACTION_CLOSE_POSITION, ACTION_DECREASE_POSITION, ACTION_DEPOSIT_COLLATERAL, ACTION_INCREASE_POSITION, ACTION_LIQUIDATE, ACTION_OPEN_POSITION, ACTION_PLACE_ORDER, ACTION_UPDATE_ORDER, ACTION_WITHDRAW_COLLATERAL, BPS_SCALE, CRYPTO_FEE_RATE, DOUBLE_SCALE, DRY_RUN_SENDER, FLOAT_SCALE, MAINTENANCE_MARGIN_RATE, ORDER_LIMIT_BUY, ORDER_LIMIT_SELL, ORDER_STOP_BUY, ORDER_STOP_SELL, ORDER_TAG_WILDCARD, PERM_ALL, PERM_ALL_TRADING, PERM_CANCEL_ORDER, PERM_CLOSE_POSITION, PERM_DECREASE_POSITION, PERM_DEPOSIT_COLLATERAL, PERM_INCREASE_POSITION, PERM_MINT_WLP, PERM_OPEN_POSITION, PERM_PLACE_ORDER, PERM_REDEEM_WLP, PERM_WITHDRAW_COLLATERAL, STOCK_FEE_RATE, MS_PER_YEAR, SUI_DECIMALS, WLP_DECIMALS, COLLATERAL_DECIMALS, TOKEN_DECIMALS, } from "./constants.ts";
6
6
  export type { Network } from "./constants.ts";
7
7
  export { getMarketTickers, getCollateralAssets } from "./utils/config.ts";
@@ -9,7 +9,7 @@ export { annualizedApyFromRatio, annualizeFundingRate, calcBorrowRate, calcBorro
9
9
  export * from "./user/index.ts";
10
10
  export * from "./tx-builders.ts";
11
11
  export * from "./fetch.ts";
12
- export { PythCache, aggregateTickerWithConstant, aggregateTickerWithDual, aggregateTickerWithPyth, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, refreshOraclePrices, updatePythPrices, } from "./utils/pyth.ts";
12
+ export { PythCache, aggregateTicker, aggregateTickerWithConstant, aggregateTickerWithPyth, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, refreshOraclePrices, updatePythPrices, } from "./utils/pyth.ts";
13
13
  export { fetchDepositVaa, fetchVaa, listBridgeWithdrawalVaas, listVaasByEmitter, padEvmEmitter, toWormholescanEmitter, vaaBase64ToBytes, vaaBase64ToHex, vaaBytesToBase64, waitForVaa, } from "./utils/wormhole.ts";
14
14
  export type { VaaListItem, VaaResponse, WormholescanOptions } from "./utils/wormhole.ts";
15
15
  export { AccountData as AccountDataBcs, GlobalConfigData as GlobalConfigDataBcs, MarketData as MarketDataBcs, OrderData as OrderDataBcs, PoolData as PoolDataBcs, PositionData as PositionDataBcs, RedeemRequestData as RedeemRequestDataBcs, TokenPoolData as TokenPoolDataBcs, } from "./generated/waterx_perp_view/view.ts";
package/dist/src/index.js CHANGED
@@ -13,7 +13,7 @@ export * from "./tx-builders.js";
13
13
  // ======== Read-only queries ========
14
14
  export * from "./fetch.js";
15
15
  // ======== Pyth oracle utilities ========
16
- export { PythCache, aggregateTickerWithConstant, aggregateTickerWithDual, aggregateTickerWithPyth, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, refreshOraclePrices, updatePythPrices, } from "./utils/pyth.js";
16
+ export { PythCache, aggregateTicker, aggregateTickerWithConstant, aggregateTickerWithPyth, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, refreshOraclePrices, updatePythPrices, } from "./utils/pyth.js";
17
17
  // ======== Wormhole / Wormholescan utilities (credit bridge) ========
18
18
  export { fetchDepositVaa, fetchVaa, listBridgeWithdrawalVaas, listVaasByEmitter, padEvmEmitter, toWormholescanEmitter, vaaBase64ToBytes, vaaBase64ToHex, vaaBytesToBase64, waitForVaa, } from "./utils/wormhole.js";
19
19
  // ======== Generated BCS types (sui-ts-codegen) ========
@@ -46,52 +46,55 @@ export declare function updatePythPrices(tx: Transaction, client: WaterXClient,
46
46
  packageId: string;
47
47
  }): Promise<string[]>;
48
48
  /**
49
- * For a given ticker, build the collector feed aggregate chain that
50
- * refreshes the on-chain `Oracle` aggregator for that ticker.
49
+ * Aggregate one ticker's price into the shared `Oracle`: build a collector, feed
50
+ * every rule the ticker is configured for, then `aggregate`. The fed rule set must
51
+ * match the on-chain weighted set for the ticker — `aggregator::remove_outliers`
52
+ * aborts `EMissingPriceSource` if a weighted rule is missing from the collector:
51
53
  *
52
- * Caller must have first called `buildPythPriceUpdateCalls` /
53
- * `updatePythPrices` so the corresponding `PriceInfoObject` is fresh.
54
+ * - **Pyth** fed when `priceInfoObjectId` is supplied (i.e. the ticker has a
55
+ * `pyth_rule.feeds` entry). Caller must run the Pyth update first so the
56
+ * `PriceInfoObject` is fresh.
57
+ * - **Supra** — fed alongside Pyth when supra is enabled + wired (abstains on-chain
58
+ * for symbols it has no pair for).
59
+ * - **Constant** — fed when the ticker is a constant ticker
60
+ * ({@link WaterXClient.isConstantTicker}).
61
+ *
62
+ * "Dual-feed" (Pyth + Constant) and "constant-only" are not special cases — they
63
+ * fall out of which rules the ticker is in: a constant ticker that also has a Pyth
64
+ * feed gets both; one with no Pyth feed (no `priceInfoObjectId`) gets constant only.
65
+ * Throws if no rule applies to the ticker.
54
66
  */
55
- export declare function aggregateTickerWithPyth(tx: Transaction, client: WaterXClient, args: {
67
+ export declare function aggregateTicker(tx: Transaction, client: WaterXClient, args: {
56
68
  ticker: string;
57
- priceInfoObjectId: string;
69
+ priceInfoObjectId?: string;
58
70
  }): void;
59
71
  /**
60
- * For a constant-priced ticker (e.g. `USDCUSD → $1`), build the
61
- * collector constant_rule::feed aggregate chain. No Pyth update is
62
- * needed — the price comes from the on-chain `constant_rule::Config`.
72
+ * Thin wrapper over {@link aggregateTicker} for a Pyth-fed ticker. Kept for
73
+ * back-compat (e.g. WLP mint builds). Caller must run the Pyth update first.
63
74
  */
64
- export declare function aggregateTickerWithConstant(tx: Transaction, client: WaterXClient, args: {
75
+ export declare function aggregateTickerWithPyth(tx: Transaction, client: WaterXClient, args: {
65
76
  ticker: string;
77
+ priceInfoObjectId: string;
66
78
  }): void;
67
79
  /**
68
- * For a dual-feed ticker mid-migration (e.g. `USDCUSD`), build one collector
69
- * fed by *both* `pyth_rule::feed` (+ `supra_rule::feed` when enabled) *and*
70
- * `constant_rule::feed`, then `aggregate`. Used while the aggregator carries the
71
- * `{Pyth, Constant}` weight set so neither weighted rule is missing from the
72
- * collector (`aggregator::remove_outliers` aborts `EMissingPriceSource`
73
- * otherwise). Still needs a fresh Pyth `PriceInfoObject`, so the caller must
74
- * have run the Pyth update for this ticker first.
80
+ * {@link aggregateTicker} for a **constant-only** ticker (no Pyth update needed
81
+ * the price comes from the on-chain `constant_rule::Config`).
75
82
  *
76
- * Stays correct once weights are flipped to `{Constant}` only the extra Pyth
77
- * observation is dropped as a non-weighted rule so a dual-feed ticker is safe
78
- * across the whole transition; drop it from `dual_feed` (→ constant-only) once
79
- * `PythRule` weight is 0.
83
+ * Throws if the ticker ALSO has a `pyth_rule.feeds` entry (a dual-feed transition
84
+ * ticker): feeding only the constant leg would leave the still-weighted Pyth rule
85
+ * absent from the collector and abort `aggregate` with `EMissingPriceSource`. Such
86
+ * tickers must go through {@link aggregateTicker} with a `priceInfoObjectId` (or
87
+ * {@link refreshOraclePrices}), which feeds both.
80
88
  */
81
- export declare function aggregateTickerWithDual(tx: Transaction, client: WaterXClient, args: {
89
+ export declare function aggregateTickerWithConstant(tx: Transaction, client: WaterXClient, args: {
82
90
  ticker: string;
83
- priceInfoObjectId: string;
84
91
  }): void;
85
92
  /**
86
- * Refresh multiple tickers in one PTB. Routing per ticker:
87
- * - **constant-only** (see {@link WaterXClient.isConstantOnlyTicker}) fed from
88
- * `constant_rule`, skips Pyth entirely;
89
- * - **dual-feed** ({@link WaterXClient.isDualFeedTicker}) fed by both Pyth and
90
- * `constant_rule` (needs the Pyth update);
91
- * - **Pyth** — the rest.
92
- *
93
- * Pyth-priced tickers (Pyth-only + dual) are updated on-chain via Pyth first
94
- * (one accumulator), then each ticker runs its collector → feed → aggregate.
93
+ * Refresh multiple tickers in one PTB. For each ticker {@link aggregateTicker}
94
+ * feeds whichever rules it is configured for (Pyth if it has a `pyth_rule.feeds`
95
+ * entry, Supra when enabled, Constant when it's a constant ticker). Tickers with a
96
+ * Pyth feed are updated on-chain via one shared Pyth accumulator first; the rest
97
+ * (constant-only) skip Pyth entirely.
95
98
  */
96
99
  export declare function refreshOraclePrices(tx: Transaction, client: WaterXClient, tickers: string[], opts?: {
97
100
  cache?: PythCache;
@@ -228,39 +228,71 @@ export async function updatePythPrices(tx, client, feedIds, cache, sponsorFund)
228
228
  // Per-ticker refresh: collector → pyth_rule::feed → oracle::aggregate
229
229
  // ============================================================================
230
230
  /**
231
- * For a given ticker, build the collector feed aggregate chain that
232
- * refreshes the on-chain `Oracle` aggregator for that ticker.
231
+ * Aggregate one ticker's price into the shared `Oracle`: build a collector, feed
232
+ * every rule the ticker is configured for, then `aggregate`. The fed rule set must
233
+ * match the on-chain weighted set for the ticker — `aggregator::remove_outliers`
234
+ * aborts `EMissingPriceSource` if a weighted rule is missing from the collector:
233
235
  *
234
- * Caller must have first called `buildPythPriceUpdateCalls` /
235
- * `updatePythPrices` so the corresponding `PriceInfoObject` is fresh.
236
+ * - **Pyth** fed when `priceInfoObjectId` is supplied (i.e. the ticker has a
237
+ * `pyth_rule.feeds` entry). Caller must run the Pyth update first so the
238
+ * `PriceInfoObject` is fresh.
239
+ * - **Supra** — fed alongside Pyth when supra is enabled + wired (abstains on-chain
240
+ * for symbols it has no pair for).
241
+ * - **Constant** — fed when the ticker is a constant ticker
242
+ * ({@link WaterXClient.isConstantTicker}).
243
+ *
244
+ * "Dual-feed" (Pyth + Constant) and "constant-only" are not special cases — they
245
+ * fall out of which rules the ticker is in: a constant ticker that also has a Pyth
246
+ * feed gets both; one with no Pyth feed (no `priceInfoObjectId`) gets constant only.
247
+ * Throws if no rule applies to the ticker.
236
248
  */
237
- export function aggregateTickerWithPyth(tx, client, args) {
249
+ export function aggregateTicker(tx, client, args) {
250
+ const oraclePkg = client.config.packages.waterx_oracle.published_at;
238
251
  const collector = newCollector({
239
- package: client.config.packages.waterx_oracle.published_at,
252
+ package: oraclePkg,
240
253
  arguments: { symbol: args.ticker },
241
254
  })(tx);
242
- pythRuleFeed({
243
- package: client.config.packages.pyth_rule.published_at,
244
- arguments: {
245
- collector: collector,
246
- config: tx.object(client.config.packages.pyth_rule.config),
247
- pythState: tx.object(client.pyth.state_id),
248
- pythPriceInfo: tx.object(args.priceInfoObjectId),
249
- },
250
- })(tx);
251
- // Second weighted rule: when supra_rule is deployed + enabled, feed it on the
252
- // SAME collector before aggregate. Supra reads the symbol from the collector
253
- // and abstains on-chain if it has no pair id for it, so this is safe to add
254
- // unconditionally for every Pyth ticker once enabled.
255
- maybeFeedSupra(tx, client, collector);
255
+ let fed = false;
256
+ if (args.priceInfoObjectId) {
257
+ pythRuleFeed({
258
+ package: client.config.packages.pyth_rule.published_at,
259
+ arguments: {
260
+ collector,
261
+ config: tx.object(client.config.packages.pyth_rule.config),
262
+ pythState: tx.object(client.pyth.state_id),
263
+ pythPriceInfo: tx.object(args.priceInfoObjectId),
264
+ },
265
+ })(tx);
266
+ // Supra rides on the same collector when enabled (abstains on-chain otherwise).
267
+ maybeFeedSupra(tx, client, collector);
268
+ fed = true;
269
+ }
270
+ if (client.isConstantTicker(args.ticker)) {
271
+ const constant = client.config.packages.constant_rule;
272
+ constantRuleFeed({
273
+ package: constant.published_at,
274
+ arguments: { collector, config: tx.object(constant.config) },
275
+ })(tx);
276
+ fed = true;
277
+ }
278
+ if (!fed) {
279
+ throw new Error(`no oracle rule configured for ticker '${args.ticker}' (no pyth feed, not a constant ticker)`);
280
+ }
256
281
  aggregateCall({
257
- package: client.config.packages.waterx_oracle.published_at,
282
+ package: oraclePkg,
258
283
  arguments: {
259
284
  oracle: tx.object(client.config.packages.waterx_oracle.oracle),
260
- collector: collector,
285
+ collector,
261
286
  },
262
287
  })(tx);
263
288
  }
289
+ /**
290
+ * Thin wrapper over {@link aggregateTicker} for a Pyth-fed ticker. Kept for
291
+ * back-compat (e.g. WLP mint builds). Caller must run the Pyth update first.
292
+ */
293
+ export function aggregateTickerWithPyth(tx, client, args) {
294
+ aggregateTicker(tx, client, args);
295
+ }
264
296
  /**
265
297
  * Append a `supra_rule::feed` on `collector` when the deployment has supra
266
298
  * enabled + wired (see {@link WaterXClient.getSupraRule}). No-op otherwise, so
@@ -280,130 +312,43 @@ function maybeFeedSupra(tx, client, collector) {
280
312
  })(tx);
281
313
  }
282
314
  /**
283
- * For a constant-priced ticker (e.g. `USDCUSD $1`), build the
284
- * collector constant_rule::feed aggregate chain. No Pyth update is
285
- * needed — the price comes from the on-chain `constant_rule::Config`.
286
- */
287
- export function aggregateTickerWithConstant(tx, client, args) {
288
- const constant = client.config.packages.waterx_constant_rule;
289
- if (!constant?.published_at || !constant.config) {
290
- throw new Error(`waterx_constant_rule.{published_at,config} missing — cannot feed constant ticker '${args.ticker}'`);
291
- }
292
- const collector = newCollector({
293
- package: client.config.packages.waterx_oracle.published_at,
294
- arguments: { symbol: args.ticker },
295
- })(tx);
296
- constantRuleFeed({
297
- package: constant.published_at,
298
- arguments: {
299
- collector: collector,
300
- config: tx.object(constant.config),
301
- },
302
- })(tx);
303
- aggregateCall({
304
- package: client.config.packages.waterx_oracle.published_at,
305
- arguments: {
306
- oracle: tx.object(client.config.packages.waterx_oracle.oracle),
307
- collector: collector,
308
- },
309
- })(tx);
310
- }
311
- /**
312
- * For a dual-feed ticker mid-migration (e.g. `USDCUSD`), build one collector
313
- * fed by *both* `pyth_rule::feed` (+ `supra_rule::feed` when enabled) *and*
314
- * `constant_rule::feed`, then `aggregate`. Used while the aggregator carries the
315
- * `{Pyth, Constant}` weight set so neither weighted rule is missing from the
316
- * collector (`aggregator::remove_outliers` aborts `EMissingPriceSource`
317
- * otherwise). Still needs a fresh Pyth `PriceInfoObject`, so the caller must
318
- * have run the Pyth update for this ticker first.
315
+ * {@link aggregateTicker} for a **constant-only** ticker (no Pyth update needed
316
+ * the price comes from the on-chain `constant_rule::Config`).
319
317
  *
320
- * Stays correct once weights are flipped to `{Constant}` only the extra Pyth
321
- * observation is dropped as a non-weighted rule so a dual-feed ticker is safe
322
- * across the whole transition; drop it from `dual_feed` (→ constant-only) once
323
- * `PythRule` weight is 0.
318
+ * Throws if the ticker ALSO has a `pyth_rule.feeds` entry (a dual-feed transition
319
+ * ticker): feeding only the constant leg would leave the still-weighted Pyth rule
320
+ * absent from the collector and abort `aggregate` with `EMissingPriceSource`. Such
321
+ * tickers must go through {@link aggregateTicker} with a `priceInfoObjectId` (or
322
+ * {@link refreshOraclePrices}), which feeds both.
324
323
  */
325
- export function aggregateTickerWithDual(tx, client, args) {
326
- const constant = client.config.packages.waterx_constant_rule;
327
- if (!constant?.published_at || !constant.config) {
328
- throw new Error(`waterx_constant_rule.{published_at,config} missing — cannot dual-feed ticker '${args.ticker}'`);
324
+ export function aggregateTickerWithConstant(tx, client, args) {
325
+ if (client.config.packages.pyth_rule?.feeds?.[args.ticker] !== undefined) {
326
+ throw new Error(`'${args.ticker}' is in pyth_rule.feeds (dual-feed) — feed both via aggregateTicker({ priceInfoObjectId }) / refreshOraclePrices, not aggregateTickerWithConstant`);
329
327
  }
330
- const collector = newCollector({
331
- package: client.config.packages.waterx_oracle.published_at,
332
- arguments: { symbol: args.ticker },
333
- })(tx);
334
- pythRuleFeed({
335
- package: client.config.packages.pyth_rule.published_at,
336
- arguments: {
337
- collector: collector,
338
- config: tx.object(client.config.packages.pyth_rule.config),
339
- pythState: tx.object(client.pyth.state_id),
340
- pythPriceInfo: tx.object(args.priceInfoObjectId),
341
- },
342
- })(tx);
343
- // Second weighted rule (when enabled) on the same collector, matching the
344
- // pure-Pyth path.
345
- maybeFeedSupra(tx, client, collector);
346
- // The constant rule — the leg that lets the aggregator carry a Constant weight.
347
- constantRuleFeed({
348
- package: constant.published_at,
349
- arguments: {
350
- collector: collector,
351
- config: tx.object(constant.config),
352
- },
353
- })(tx);
354
- aggregateCall({
355
- package: client.config.packages.waterx_oracle.published_at,
356
- arguments: {
357
- oracle: tx.object(client.config.packages.waterx_oracle.oracle),
358
- collector: collector,
359
- },
360
- })(tx);
328
+ aggregateTicker(tx, client, { ticker: args.ticker });
361
329
  }
362
330
  /**
363
- * Refresh multiple tickers in one PTB. Routing per ticker:
364
- * - **constant-only** (see {@link WaterXClient.isConstantOnlyTicker}) fed from
365
- * `constant_rule`, skips Pyth entirely;
366
- * - **dual-feed** ({@link WaterXClient.isDualFeedTicker}) fed by both Pyth and
367
- * `constant_rule` (needs the Pyth update);
368
- * - **Pyth** — the rest.
369
- *
370
- * Pyth-priced tickers (Pyth-only + dual) are updated on-chain via Pyth first
371
- * (one accumulator), then each ticker runs its collector → feed → aggregate.
331
+ * Refresh multiple tickers in one PTB. For each ticker {@link aggregateTicker}
332
+ * feeds whichever rules it is configured for (Pyth if it has a `pyth_rule.feeds`
333
+ * entry, Supra when enabled, Constant when it's a constant ticker). Tickers with a
334
+ * Pyth feed are updated on-chain via one shared Pyth accumulator first; the rest
335
+ * (constant-only) skip Pyth entirely.
372
336
  */
373
337
  export async function refreshOraclePrices(tx, client, tickers, opts = {}) {
374
338
  if (tickers.length === 0)
375
339
  return;
376
- // Route each ticker to its rule. Dual-feed tickers are constant *and* Pyth, so
377
- // they go through the Pyth update with the Pyth-only tickers but aggregate via
378
- // the dual leg; only constant-*only* tickers skip Pyth entirely.
379
- const constantOnlyTickers = tickers.filter((t) => client.isConstantOnlyTicker(t));
380
- const pythTickers = tickers.filter((t) => !client.isConstantTicker(t));
381
- const dualTickers = tickers.filter((t) => client.isDualFeedTicker(t));
382
- // Pyth update covers both Pyth-only and dual-feed tickers.
383
- const pythUpdateTickers = [...pythTickers, ...dualTickers];
384
- if (pythUpdateTickers.length > 0) {
385
- // Oracle tickers (e.g. "BTCUSD") → each one's pyth feed entry: feed_id
386
- // (off-chain) drives the accumulator; price_info_object (on-chain) is
387
- // consumed by the per-ticker aggregate cycle.
388
- const entries = pythUpdateTickers.map((t) => client.getPythFeed(t));
389
- const feedIds = entries.map((e) => e.feed_id);
390
- await updatePythPrices(tx, client, feedIds, opts.cache, opts.sponsorFund);
391
- pythTickers.forEach((ticker, i) => {
392
- aggregateTickerWithPyth(tx, client, {
393
- ticker,
394
- priceInfoObjectId: entries[i].price_info_object,
395
- });
396
- });
397
- dualTickers.forEach((ticker, i) => {
398
- // dual entries are appended after the pyth-only ones.
399
- aggregateTickerWithDual(tx, client, {
400
- ticker,
401
- priceInfoObjectId: entries[pythTickers.length + i].price_info_object,
402
- });
403
- });
340
+ // Every ticker with a pyth_rule.feeds entry needs the on-chain Pyth update
341
+ // first (one shared accumulator). Constant-only tickers (no pyth feed) skip it.
342
+ const pythTickers = tickers.filter((t) => client.config.packages.pyth_rule?.feeds?.[t] !== undefined);
343
+ const priceInfoByTicker = new Map();
344
+ if (pythTickers.length > 0) {
345
+ const entries = pythTickers.map((t) => client.getPythFeed(t));
346
+ await updatePythPrices(tx, client, entries.map((e) => e.feed_id), opts.cache, opts.sponsorFund);
347
+ pythTickers.forEach((t, i) => priceInfoByTicker.set(t, entries[i].price_info_object));
404
348
  }
405
- for (const ticker of constantOnlyTickers) {
406
- aggregateTickerWithConstant(tx, client, { ticker });
349
+ // Aggregate each ticker, feeding whichever rules it is configured for.
350
+ for (const ticker of tickers) {
351
+ aggregateTicker(tx, client, { ticker, priceInfoObjectId: priceInfoByTicker.get(ticker) });
407
352
  }
408
353
  }
409
354
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waterx/sdk",
3
- "version": "2.3.1-dev.0",
3
+ "version": "2.4.0",
4
4
  "description": "WaterX SDK — perpetuals and prediction markets on Sui",
5
5
  "license": "MIT",
6
6
  "author": "WaterX",