@waterx/sdk 4.3.1 → 4.3.3

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 (36) hide show
  1. package/.claude/skills/waterx-sdk-integration/SKILL.md +219 -0
  2. package/README.md +177 -10
  3. package/SKILLS.md +34 -0
  4. package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +140 -7
  5. package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +163 -8
  6. package/dist/cjs/src/oracle/aggregate.d.ts +8 -4
  7. package/dist/cjs/src/oracle/aggregate.js +74 -29
  8. package/dist/cjs/src/oracle/config.d.ts +3 -2
  9. package/dist/cjs/src/oracle/index.d.ts +2 -2
  10. package/dist/cjs/src/oracle/index.js +10 -5
  11. package/dist/cjs/src/oracle/price-update-rule.d.ts +10 -2
  12. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +25 -7
  13. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +41 -17
  14. package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +136 -47
  15. package/dist/cjs/src/oracle/rules/waterx-rule.js +450 -114
  16. package/dist/cjs/src/perp/client.d.ts +10 -8
  17. package/dist/cjs/src/perp/index.d.ts +2 -2
  18. package/dist/cjs/src/perp/index.js +10 -2
  19. package/dist/cjs/src/unified-client.d.ts +1 -1
  20. package/dist/src/generated/waterx_rule/waterx_rule.d.ts +140 -7
  21. package/dist/src/generated/waterx_rule/waterx_rule.js +151 -7
  22. package/dist/src/oracle/aggregate.d.ts +8 -4
  23. package/dist/src/oracle/aggregate.js +75 -30
  24. package/dist/src/oracle/config.d.ts +3 -2
  25. package/dist/src/oracle/index.d.ts +2 -2
  26. package/dist/src/oracle/index.js +8 -6
  27. package/dist/src/oracle/price-update-rule.d.ts +10 -2
  28. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +25 -7
  29. package/dist/src/oracle/rules/pyth-lazer-rule.js +41 -17
  30. package/dist/src/oracle/rules/waterx-rule.d.ts +136 -47
  31. package/dist/src/oracle/rules/waterx-rule.js +447 -114
  32. package/dist/src/perp/client.d.ts +10 -8
  33. package/dist/src/perp/index.d.ts +2 -2
  34. package/dist/src/perp/index.js +7 -1
  35. package/dist/src/unified-client.d.ts +1 -1
  36. package/package.json +8 -2
@@ -30,7 +30,7 @@ import { feedConstantRule } from "./rules/constant-rule.js";
30
30
  import { feedLazerRule } from "./rules/pyth-lazer-rule.js";
31
31
  import { feedPythRule } from "./rules/pyth-rule.js";
32
32
  import { maybeFeedSupra } from "./rules/supra-rule.js";
33
- import { feedWaterxRule, waterxEnvelopeOf, } from "./rules/waterx-rule.js";
33
+ import { feedWaterxRule, feedWaterxRuleWithProof, waterxEnvelopeOf, waterxLeavesOf, } from "./rules/waterx-rule.js";
34
34
  /**
35
35
  * Resolve one group's off-chain update payload for {@link refreshOraclePrices}:
36
36
  * try `provider.get(source, tickers)` first (when a provider is configured),
@@ -84,9 +84,10 @@ async function resolveGroupUpdateData(host, group, provider) {
84
84
  * if it is stale — it never aborts — so the call stays mandatory while
85
85
  * `pyth_rule` remains in the ticker's on-chain weighted set
86
86
  * (`EMissingPriceSource` requires every weighted rule to appear).
87
- * - **Lazer** — fed when `lazerUpdate` is supplied: the verified
88
- * `pyth_lazer::update::Update` produced by this PTB's lazer update leg
89
- * (see `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
87
+ * - **Lazer** — fed when `lazerUpdate` is supplied: the verified update this
88
+ * PTB's lazer update leg produced with the network's verify entry
89
+ * (`update_v2::Update` on mainnet, `update::Update` on testnet see
90
+ * `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
90
91
  * not (yet) weight `PythLazerRule`, the contribution is silently dropped
91
92
  * on-chain — feeding ahead of the weight migration is harmless.
92
93
  * - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
@@ -113,17 +114,25 @@ export function aggregateTicker(tx, host, args) {
113
114
  feedLazerRule(tx, host, collector, args.lazerUpdate);
114
115
  fed = true;
115
116
  }
116
- if (args.waterxEnvelope !== undefined) {
117
- // waterx_rule::collect_batch_latest verifies the batch signature and feeds
118
- // this collector's symbol from the batch. If the ticker's aggregator does
119
- // not (yet) weight `WaterxRule`, the contribution is silently dropped
120
- // on-chain feeding ahead of the weight migration is safe for THIS tx.
121
- // CAVEAT (unlike lazer): the feed call records a per-symbol signed-
122
- // timestamp high-water mark REGARDLESS of weights, and a replayed
123
- // timestamp ABORTS (`EReplayedSignature`, audit F-014) so two PTBs
124
- // carrying the same envelope for the same symbol cannot both land; the
125
- // second aborts even where waterx is unweighted. See WaterxRule's module
126
- // header.
117
+ // One waterx leg at most, and the leaf shape wins: both entries record the
118
+ // same per-symbol signed-timestamp high-water mark and feed the same rule
119
+ // witness into this collector, so emitting both would make the second one
120
+ // abstain on its own predecessor's mark for no gain. `refreshOraclePrices`
121
+ // only ever supplies one; a caller that passes both gets the cheaper leg.
122
+ if (args.waterxLeaf !== undefined) {
123
+ // waterx_rule::collect_single_with_proof re-derives the snapshot root from
124
+ // this leaf + its proof, verifies the enclave signature over that root, and
125
+ // feeds the price. If the ticker's aggregator does not (yet) weight
126
+ // `WaterxRule`, the contribution is silently dropped on-chain feeding
127
+ // ahead of the weight migration is safe. See WaterxRule's module header for
128
+ // the abort-vs-abstain split.
129
+ feedWaterxRuleWithProof(tx, host, collector, args.waterxLeaf);
130
+ fed = true;
131
+ }
132
+ else if (args.waterxEnvelope !== undefined) {
133
+ // Fallback shape: collect_batch_latest re-verifies the WHOLE batch
134
+ // signature (every item rebuilt in-PTB) and feeds this collector's symbol
135
+ // out of it. Only reached against a quote-center with no leaf route.
127
136
  feedWaterxRule(tx, host, collector, args.waterxEnvelope);
128
137
  fed = true;
129
138
  }
@@ -216,11 +225,11 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
216
225
  if (tickers.length === 0)
217
226
  return;
218
227
  // Dedupe the caller's list (order-preserving): a repeated ticker would
219
- // otherwise aggregate TWICE in this one PTB — wasted gas for every rule,
220
- // and a hard ABORT under waterx: the second `collect_batch_latest` carries
221
- // the same envelope, and the on-chain per-symbol replay guard rejects an
222
- // already-accepted signed timestamp (`EReplayedSignature`, F-014) even
223
- // inside a single transaction.
228
+ // otherwise aggregate TWICE in this one PTB — wasted gas for every rule, and
229
+ // under waterx the second collect would be dead weight on top of that: the
230
+ // on-chain per-symbol replay guard (F-014) sees its own predecessor's
231
+ // high-water mark from earlier in this same transaction and abstains, so the
232
+ // repeat pays full verification cost to contribute nothing.
224
233
  tickers = [...new Set(tickers)];
225
234
  // price_info_object lookup for every ticker with a pyth_rule.feeds entry —
226
235
  // needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
@@ -291,10 +300,12 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
291
300
  // leg needs per-ticker data from its update leg must decide its carry here
292
301
  // — falling through silently would starve its weighted tickers on-chain.
293
302
  const lazerUpdateByTicker = new Map();
294
- // Signed batch envelope per waterx-served ticker. Unlike Lazer's shared PTB
295
- // handle, waterx's verify+feed is bundled into `collect_batch_latest` in the
296
- // per-ticker feed leg, so its `buildUpdateCalls` emits nothing and the
297
- // envelope is carried straight from the group's fetched data.
303
+ // Signed waterx data per served ticker a per-symbol Merkle leaf normally, a
304
+ // shared batch envelope on the fallback shape. Unlike Lazer's shared PTB
305
+ // handle, waterx's verify+feed is bundled into the per-ticker collect call, so
306
+ // its `buildUpdateCalls` emits nothing and the signed data is carried straight
307
+ // from the group's fetched data.
308
+ const waterxLeafByTicker = new Map();
298
309
  const waterxEnvelopeByTicker = new Map();
299
310
  for (const [i, group] of groups.entries()) {
300
311
  const data = dataByGroup[i] ?? null;
@@ -318,12 +329,45 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
318
329
  break;
319
330
  case "waterx_rule": {
320
331
  // waterx_rule emits no shared handle (verify+feed is bundled into the
321
- // per-ticker `collect_batch_latest`), so the envelope is carried
322
- // straight from this group's fetched data to the feed leg below.
323
- const envelope = waterxEnvelopeOf(data);
324
- if (envelope) {
325
- for (const ticker of group.tickers)
326
- waterxEnvelopeByTicker.set(ticker, envelope);
332
+ // per-ticker collect call), so the signed data is carried straight from
333
+ // this group's fetched data to the feed leg below. Leaves are per-symbol
334
+ // and indexed BY symbol — never fanned out across the group like the
335
+ // envelope, since each leaf only verifies for its own symbol
336
+ // (`ECollectorSymbolMismatch`). A leaf for a symbol outside this group is
337
+ // dropped rather than carried: the feed leg is keyed by ticker anyway.
338
+ const leaves = waterxLeavesOf(data);
339
+ // `length > 0` matters: `{ leaves: [] }` is shape-valid (`[].every(...)`
340
+ // is `true`), so a bare truthiness test would TAKE this branch, carry
341
+ // nothing, and `break` past the envelope branch below.
342
+ if (leaves && leaves.length > 0) {
343
+ const served = new Set(group.tickers);
344
+ for (const leaf of leaves) {
345
+ if (served.has(leaf.symbol))
346
+ waterxLeafByTicker.set(leaf.symbol, leaf);
347
+ }
348
+ }
349
+ else {
350
+ const envelope = waterxEnvelopeOf(data);
351
+ if (envelope) {
352
+ for (const ticker of group.tickers)
353
+ waterxEnvelopeByTicker.set(ticker, envelope);
354
+ }
355
+ }
356
+ // Fail the BUILD, not the chain. Both suppliers of `data` already
357
+ // guarantee full coverage — the live fetch through `assertCoverage`, a
358
+ // cached payload through `narrowUpdateData` (which returns `null`, i.e.
359
+ // "miss → live fetch", rather than a partial) — so reaching here with a
360
+ // ticker uncarried means one of those invariants broke. Left alone it
361
+ // emits a collector with no waterx leg, which surfaces MUCH later as an
362
+ // opaque on-chain `EMissingPriceSource` (or, if the rule is unweighted
363
+ // for that ticker, as a silently thinner weighted set). This names the
364
+ // tickers instead.
365
+ const uncarried = group.tickers.filter((t) => !waterxLeafByTicker.has(t) && !waterxEnvelopeByTicker.has(t));
366
+ if (uncarried.length > 0) {
367
+ throw new Error(`waterx_rule update data carries no signed price for ticker(s): ${uncarried.join(", ")}. ` +
368
+ "Expected one signed leaf per ticker (or a batch envelope covering all of " +
369
+ "them) — a payload that serves none of a group's tickers must be reported " +
370
+ "as a miss, not fed.");
327
371
  }
328
372
  break;
329
373
  }
@@ -339,6 +383,7 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
339
383
  ticker,
340
384
  priceInfoObjectId: priceInfoByTicker.get(ticker),
341
385
  lazerUpdate: lazerUpdateByTicker.get(ticker),
386
+ waterxLeaf: waterxLeafByTicker.get(ticker),
342
387
  waterxEnvelope: waterxEnvelopeByTicker.get(ticker),
343
388
  });
344
389
  }
@@ -96,7 +96,8 @@ export interface WaterxRuleFeedEntry {
96
96
  /**
97
97
  * `waterx_rule` deployment entry — the first-party Nautilus-TEE oracle rule.
98
98
  * Read by `WaterxRule` (`rules/waterx-rule.ts`): `feeds` for ticker support,
99
- * `config`/`enclave_config`/`enclave` for the `collect_batch_latest` call,
99
+ * `config`/`enclave_config`/`enclave` for the collect call
100
+ * (`collect_single_with_proof`, or `collect_batch_latest` on the fallback shape),
100
101
  * `published_at` for the package address. The off-chain signed price is pulled
101
102
  * from the quote-center (endpoint from the rule-owned `WATERX_INFRA` table in
102
103
  * `rules/waterx-rule.ts`), not this JSON.
@@ -201,7 +202,7 @@ export interface WaterxAccessConfig {
201
202
  * Quote-center base URL override (`waterxEndpoint` create option). A base
202
203
  * PATH is preserved — the rule appends via `joinEndpointPath`, so
203
204
  * `https://app.example/api/quote-center` resolves to
204
- * `…/api/quote-center/v1/quotes/update` and a proxy route is not rewritten
205
+ * `…/api/quote-center/v1/quotes/leaves` and a proxy route is not rewritten
205
206
  * away. A trailing slash is trimmed.
206
207
  */
207
208
  endpoint?: string;
@@ -29,8 +29,8 @@ export { PythCoreRule } from "./rules/pyth-core-rule.ts";
29
29
  export type { PythCoreUpdatePayload } from "./rules/pyth-core-rule.ts";
30
30
  export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.ts";
31
31
  export type { PythLazerUpdatePayload } from "./rules/pyth-lazer-rule.ts";
32
- export { WaterxRule, parseSignedEnvelope, BATCH_PRICE_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint, waterxEnvelopeOf, } from "./rules/waterx-rule.ts";
33
- export type { WaterxUpdatePayload, WaterxSignedEnvelope, WaterxBatchItem, } from "./rules/waterx-rule.ts";
32
+ export { WaterxRule, parseSignedEnvelope, parseSignedLeaves, BATCH_PRICE_INTENT, MERKLE_ROOT_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint, waterxLeavesOf, waterxEnvelopeOf, } from "./rules/waterx-rule.ts";
33
+ export type { WaterxUpdatePayload, WaterxLeafPayload, WaterxEnvelopePayload, WaterxSignedEnvelope, WaterxSignedLeaf, WaterxBatchItem, } from "./rules/waterx-rule.ts";
34
34
  export { OracleSourceNotImplementedError, resolveOracleRule } from "./rule-registry.ts";
35
35
  export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.ts";
36
36
  export { openPythSponsorFund, reimbursePythSponsor } from "./rules/sponsor.ts";
@@ -60,12 +60,14 @@ export { PythCoreRule } from "./rules/pyth-core-rule.js";
60
60
  export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.js";
61
61
  // `WATERX_INFRA` / `waterxQuoteCenterEndpoint` are the source's own infra table +
62
62
  // read-plane accessor (mirrors `pythCoreHermesEndpoint`).
63
- // WaterX quote-center rule (first-party ed25519 signed batches; `feedWaterxRule`
64
- // stays internal to `aggregate.ts`).
65
- export { WaterxRule, parseSignedEnvelope, BATCH_PRICE_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint,
66
- // Rule-owned payload accessor (kind-check + unwrap in one place) — never
67
- // hand-cast the payload shape.
68
- waterxEnvelopeOf, } from "./rules/waterx-rule.js";
63
+ // WaterX quote-center rule (first-party ed25519 signed prices; the `feedWaterxRule*`
64
+ // legs stay internal to `aggregate.ts`). Both wire shapes are exported because a
65
+ // BE prefetch cache holds whichever one its quote-center serves: per-symbol
66
+ // Merkle leaves (default) or one indivisible batch envelope (fallback).
67
+ export { WaterxRule, parseSignedEnvelope, parseSignedLeaves, BATCH_PRICE_INTENT, MERKLE_ROOT_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint,
68
+ // Rule-owned payload accessors (kind-check + unwrap in one place) — never
69
+ // hand-cast the payload shape, and never assume which variant it is.
70
+ waterxLeavesOf, waterxEnvelopeOf, } from "./rules/waterx-rule.js";
69
71
  // `resolveOracleRule` is the ONE source→rule registry — exported so external
70
72
  // consumers (e.g. a BE prefetch cache that keys per source and needs each
71
73
  // source's `supportedTickers`/`fetchUpdateData`) resolve through it instead of
@@ -83,14 +83,22 @@ export declare function assertRuleUpdateData<T>(data: RuleUpdateData, kind: Pric
83
83
  * return when its collector-feed leg needs a value produced by the update leg
84
84
  * *within the same PTB*. Pyth Core needs none (its feed leg reads the shared
85
85
  * `PriceInfoObject` the update leg refreshed), so it returns `void`. The Lazer
86
- * rule returns the verified `pyth_lazer::update::Update` result — one
86
+ * rule returns the verified-update result of its network's verify entry — one
87
87
  * signature verification covers every feed in the payload, and
88
88
  * `pyth_lazer_rule::feed` takes it by reference per ticker (see
89
89
  * `aggregateTicker`'s `lazerUpdate` arg).
90
90
  */
91
91
  export type RuleUpdateHandle = {
92
92
  readonly kind: "pyth_lazer_rule";
93
- /** Result of `pyth_lazer::parse_and_verify_le_ecdsa_update` in this PTB. */
93
+ /**
94
+ * Opaque result of this network's `LAZER_INFRA.verify_entry` in this PTB,
95
+ * passed straight to `pyth_lazer_rule::feed`. The Move type is
96
+ * network-dependent and never named here: mainnet's
97
+ * `pyth_lazer::parse_and_verify_le_ecdsa_update_v2` yields
98
+ * `pyth_lazer::update_v2::Update`, testnet's v1
99
+ * `…_le_ecdsa_update` yields `pyth_lazer::update::Update`, and each
100
+ * network's `pyth_lazer_rule` is published bound to the matching one.
101
+ */
94
102
  readonly update: TransactionArgument;
95
103
  };
96
104
  /**
@@ -4,8 +4,9 @@
4
4
  * appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
5
5
  * requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
6
6
  * via the `pythApiKey` create option), verifies it ONCE on-chain via
7
- * `pyth_lazer::parse_and_verify_le_ecdsa_update`, and hands the resulting
8
- * `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
7
+ * `pyth_lazer`'s verify entry for that network (see `LAZER_INFRA`), and hands the
8
+ * resulting `Update` PTB value back through a `RuleUpdateHandle` for the feed
9
+ * calls.
9
10
  */
10
11
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
11
12
  import type { Network } from "../../constants.ts";
@@ -21,16 +22,33 @@ import { type PriceUpdateRule } from "../price-update-rule.ts";
21
22
  * `POST /v1/latest_price` (Bearer-authenticated). The service is
22
23
  * network-agnostic (one signed payload verifies on any chain that trusts the
23
24
  * Lazer signers), so both networks share the production host.
24
- * - `verifier_package` — the Sui package carrying
25
- * `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
26
- * still the original v1 publish; mainnet is the v2-upgraded package (which
27
- * still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
28
- * contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
25
+ * - `verifier_package` / `verify_entry` — the Sui package carrying the verify
26
+ * call, and which entry to call. These track what `pyth_lazer_rule` binds on
27
+ * that network, so they move together:
28
+ * - **mainnet** the v2 package `0xefbfd064…` and the **v2** entry. The rule
29
+ * was republished v2-bound after a 2026-08-05 mainnet probe: the ORIGINAL
30
+ * package `0x7b502c…` now aborts `EDifferentVersion` (`state::current_cap`)
31
+ * for any payload — the shared `State` has been migrated past that code —
32
+ * and its v1 entry aborts `EInvalidChannel` on `fixed_rate@1000ms`, the only
33
+ * channel WaterX's Pyth Pro grant permits.
34
+ * - **testnet** — still the original v1 publish, which has no `update_v2`
35
+ * module at all, so the v1 entry is the only one that exists there.
36
+ * Both entries take `(state, clock, bytes)` and accept the same `leEcdsa`
37
+ * payload. Values mirror the contract repo's `pyth_lazer_rule/Move.toml`
38
+ * published-at pins.
29
39
  */
30
40
  export declare const LAZER_INFRA: Record<Network, {
31
41
  endpoint: string;
32
42
  verifier_package: string;
43
+ verify_entry: LazerVerifyEntry;
44
+ channel: string;
33
45
  }>;
46
+ /**
47
+ * The `pyth_lazer` verify entry a network's deployed rule consumes. `_v2`
48
+ * returns `update_v2::Update`; the v1 entry returns `update::Update`, and the
49
+ * two are NOT interchangeable — the rule's `feed` takes one concrete type.
50
+ */
51
+ export type LazerVerifyEntry = "parse_and_verify_le_ecdsa_update" | "parse_and_verify_le_ecdsa_update_v2";
34
52
  /** `pyth_lazer_rule`'s narrowed `RuleUpdateData.payload` shape. */
35
53
  export interface PythLazerUpdatePayload {
36
54
  /** One signed `leEcdsa` message carrying every requested feed. */
@@ -4,8 +4,9 @@
4
4
  * appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
5
5
  * requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
6
6
  * via the `pythApiKey` create option), verifies it ONCE on-chain via
7
- * `pyth_lazer::parse_and_verify_le_ecdsa_update`, and hands the resulting
8
- * `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
7
+ * `pyth_lazer`'s verify entry for that network (see `LAZER_INFRA`), and hands the
8
+ * resulting `Update` PTB value back through a `RuleUpdateHandle` for the feed
9
+ * calls.
9
10
  */
10
11
  import { fromHex } from "@mysten/bcs";
11
12
  import { ownEntry } from "../../utils/record.js";
@@ -21,20 +22,33 @@ import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch } from "../upd
21
22
  * `POST /v1/latest_price` (Bearer-authenticated). The service is
22
23
  * network-agnostic (one signed payload verifies on any chain that trusts the
23
24
  * Lazer signers), so both networks share the production host.
24
- * - `verifier_package` — the Sui package carrying
25
- * `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
26
- * still the original v1 publish; mainnet is the v2-upgraded package (which
27
- * still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
28
- * contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
25
+ * - `verifier_package` / `verify_entry` — the Sui package carrying the verify
26
+ * call, and which entry to call. These track what `pyth_lazer_rule` binds on
27
+ * that network, so they move together:
28
+ * - **mainnet** the v2 package `0xefbfd064…` and the **v2** entry. The rule
29
+ * was republished v2-bound after a 2026-08-05 mainnet probe: the ORIGINAL
30
+ * package `0x7b502c…` now aborts `EDifferentVersion` (`state::current_cap`)
31
+ * for any payload — the shared `State` has been migrated past that code —
32
+ * and its v1 entry aborts `EInvalidChannel` on `fixed_rate@1000ms`, the only
33
+ * channel WaterX's Pyth Pro grant permits.
34
+ * - **testnet** — still the original v1 publish, which has no `update_v2`
35
+ * module at all, so the v1 entry is the only one that exists there.
36
+ * Both entries take `(state, clock, bytes)` and accept the same `leEcdsa`
37
+ * payload. Values mirror the contract repo's `pyth_lazer_rule/Move.toml`
38
+ * published-at pins.
29
39
  */
30
40
  export const LAZER_INFRA = {
31
41
  MAINNET: {
32
42
  endpoint: "https://pyth-lazer.dourolabs.app",
33
43
  verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
44
+ verify_entry: "parse_and_verify_le_ecdsa_update_v2",
45
+ channel: "fixed_rate@1000ms",
34
46
  },
35
47
  TESTNET: {
36
48
  endpoint: "https://pyth-lazer.dourolabs.app",
37
49
  verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
50
+ verify_entry: "parse_and_verify_le_ecdsa_update",
51
+ channel: "fixed_rate@200ms",
38
52
  },
39
53
  };
40
54
  /**
@@ -52,9 +66,15 @@ export const LAZER_INFRA = {
52
66
  * and every xStock — are `min_channel: fixed_rate@200ms` (Lazer symbol
53
67
  * registry, verified 2026-07-22: the same 29-feed batch 400s at
54
68
  * `real_time`/`50ms` and serves 200 with the leEcdsa blob at `200ms`).
55
- * 200ms is the fastest channel every configured feed supports, and the
56
- * deployed rule accepts it: the v1 on-chain `channel::from_u8` aborts only
57
- * on the 1000ms fixed-rate channel (real_time / 50ms / 200ms are safe).
69
+ * The channel is therefore per-network (`LAZER_INFRA[network].channel`), and
70
+ * it is bounded from BOTH sides — by what the feeds publish and by what the
71
+ * grant allows:
72
+ * - **mainnet: `fixed_rate@1000ms`.** WaterX's Pyth Pro grant no longer
73
+ * permits anything faster ("Channel fixed_rate@200ms violates rate limit.
74
+ * Minimum allowed channel is 1000ms", measured 2026-08-05), and the
75
+ * mainnet rule is v2-bound, so it accepts that channel.
76
+ * - **testnet: `fixed_rate@200ms`.** Its rule is still v1-bound, and the v1
77
+ * on-chain `channel::from_u8` aborts on the 1000ms channel.
58
78
  * - `formats: leEcdsa` + `jsonBinaryEncoding: hex` — the Sui verifier takes
59
79
  * the `leEcdsa` framing; hex matches `fromHex` below.
60
80
  */
@@ -62,7 +82,6 @@ const LAZER_LATEST_PRICE_REQUEST = {
62
82
  properties: ["price", "exponent", "confidence"],
63
83
  formats: ["leEcdsa"],
64
84
  jsonBinaryEncoding: "hex",
65
- channel: "fixed_rate@200ms",
66
85
  };
67
86
  /**
68
87
  * Shape check ONLY — the `kind` discriminant is checked separately by the
@@ -104,7 +123,7 @@ function requireLazerPackage(host) {
104
123
  * retry/timeout/Bearer policy as `fetchPriceFeedsUpdateData`, unified so
105
124
  * both oracle sources fail the same way under upstream degradation.
106
125
  */
107
- async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
126
+ async function fetchLazerSignedUpdate(endpoint, channel, apiKey, feedIds, fetchOpts) {
108
127
  // joinEndpointPath preserves any base path on the endpoint — the same
109
128
  // leading-slash `new URL` footgun that 404'd every feed on the Pyth Pro
110
129
  // Hermes endpoint (see update-fetch.ts). Defensive here: the default
@@ -115,7 +134,7 @@ async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
115
134
  res = await fetchWithPolicy(url.toString(), {
116
135
  method: "POST",
117
136
  headers: { "Content-Type": "application/json" },
118
- body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST }),
137
+ body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST, channel }),
119
138
  }, { apiKey, ...fetchOpts });
120
139
  }
121
140
  catch (err) {
@@ -175,13 +194,13 @@ export const PythLazerRule = {
175
194
  if (!apiKey) {
176
195
  throw new LazerApiKeyMissingError();
177
196
  }
178
- const update = await fetchLazerSignedUpdate(LAZER_INFRA[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
197
+ const update = await fetchLazerSignedUpdate(LAZER_INFRA[host.network].endpoint, LAZER_INFRA[host.network].channel, apiKey, feedIds, host.pyth.fetch);
179
198
  return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
180
199
  },
181
200
  /**
182
201
  * A Lazer payload is ONE signed `leEcdsa` message covering every feed it was
183
202
  * fetched for — verification is a single flat signature check over the whole
184
- * message (`parse_and_verify_le_ecdsa_update`, no per-feed cost), so the
203
+ * message (one `parse_and_verify_le_ecdsa_update*` call, no per-feed cost), so the
185
204
  * payload is indivisible: it can only be served whole. Returns the whole
186
205
  * payload iff every requested ticker's integer feed id is packed in THIS
187
206
  * payload's `feedIds`; any coverage gap (unlisted ticker, or a feed this
@@ -200,19 +219,24 @@ export const PythLazerRule = {
200
219
  return { kind: "pyth_lazer_rule", payload };
201
220
  },
202
221
  /**
203
- * Appends the single `parse_and_verify_le_ecdsa_update(state, clock, bytes)`
222
+ * Appends the single `parse_and_verify_le_ecdsa_update*(state, clock, bytes)`
204
223
  * call — one secp256k1 signature check covering every feed in the payload —
205
224
  * and returns its `Update` result as the handle the per-ticker feed leg
206
225
  * consumes. `opts.cache` / `opts.feeSource` are Pyth-Core-specific and
207
226
  * ignored (Lazer verification charges no update fee).
227
+ *
228
+ * The entry name comes from `LAZER_INFRA[network].verify_entry`: mainnet's
229
+ * rule binds `update_v2`, testnet's is still the v1 publish. Both take
230
+ * `(state, clock, bytes)` and accept the same `leEcdsa` payload.
208
231
  */
209
232
  buildUpdateCalls(tx, host, data, _opts) {
210
233
  const payload = assertRuleUpdateData(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
211
234
  if (!payload)
212
235
  return undefined;
213
236
  const lazer = requireLazerPackage(host);
237
+ const infra = LAZER_INFRA[host.network];
214
238
  const [update] = tx.moveCall({
215
- target: `${LAZER_INFRA[host.network].verifier_package}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
239
+ target: `${infra.verifier_package}::pyth_lazer::${infra.verify_entry}`,
216
240
  arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
217
241
  });
218
242
  return { kind: "pyth_lazer_rule", update };