@waterx/sdk 4.1.0 → 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 (66) hide show
  1. package/README.md +22 -17
  2. package/dist/cjs/src/account/config.d.ts +3 -2
  3. package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
  4. package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +8 -154
  5. package/dist/cjs/src/oracle/aggregate.d.ts +21 -21
  6. package/dist/cjs/src/oracle/aggregate.js +118 -69
  7. package/dist/cjs/src/oracle/config.d.ts +38 -72
  8. package/dist/cjs/src/oracle/config.js +0 -56
  9. package/dist/cjs/src/oracle/host.d.ts +24 -20
  10. package/dist/cjs/src/oracle/index.d.ts +5 -3
  11. package/dist/cjs/src/oracle/index.js +31 -8
  12. package/dist/cjs/src/oracle/price-update-rule.d.ts +13 -6
  13. package/dist/cjs/src/oracle/price-update-rule.js +3 -3
  14. package/dist/cjs/src/oracle/pyth.d.ts +44 -0
  15. package/dist/cjs/src/oracle/pyth.js +48 -6
  16. package/dist/cjs/src/oracle/read-plane.d.ts +70 -0
  17. package/dist/cjs/src/oracle/read-plane.js +78 -0
  18. package/dist/cjs/src/oracle/rule-registry.d.ts +6 -4
  19. package/dist/cjs/src/oracle/rule-registry.js +6 -4
  20. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +4 -4
  21. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
  22. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +29 -4
  23. package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -4
  24. package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +42 -5
  25. package/dist/cjs/src/oracle/rules/waterx-rule.js +57 -17
  26. package/dist/cjs/src/perp/client.d.ts +40 -27
  27. package/dist/cjs/src/perp/client.js +28 -20
  28. package/dist/cjs/src/perp/config.d.ts +6 -6
  29. package/dist/cjs/src/perp/config.js +12 -12
  30. package/dist/cjs/src/perp/index.d.ts +3 -3
  31. package/dist/cjs/src/perp/index.js +7 -4
  32. package/dist/cjs/src/unified-client.d.ts +14 -9
  33. package/dist/cjs/src/unified-client.js +2 -2
  34. package/dist/src/account/config.d.ts +3 -2
  35. package/dist/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
  36. package/dist/src/generated/waterx_rule/waterx_rule.js +7 -143
  37. package/dist/src/oracle/aggregate.d.ts +21 -21
  38. package/dist/src/oracle/aggregate.js +118 -69
  39. package/dist/src/oracle/config.d.ts +38 -72
  40. package/dist/src/oracle/config.js +1 -55
  41. package/dist/src/oracle/host.d.ts +24 -20
  42. package/dist/src/oracle/index.d.ts +5 -3
  43. package/dist/src/oracle/index.js +25 -10
  44. package/dist/src/oracle/price-update-rule.d.ts +13 -6
  45. package/dist/src/oracle/price-update-rule.js +3 -3
  46. package/dist/src/oracle/pyth.d.ts +44 -0
  47. package/dist/src/oracle/pyth.js +45 -5
  48. package/dist/src/oracle/read-plane.d.ts +70 -0
  49. package/dist/src/oracle/read-plane.js +74 -0
  50. package/dist/src/oracle/rule-registry.d.ts +6 -4
  51. package/dist/src/oracle/rule-registry.js +6 -4
  52. package/dist/src/oracle/rules/pyth-core-rule.js +5 -5
  53. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
  54. package/dist/src/oracle/rules/pyth-lazer-rule.js +28 -3
  55. package/dist/src/oracle/rules/pyth-rule.js +5 -4
  56. package/dist/src/oracle/rules/waterx-rule.d.ts +42 -5
  57. package/dist/src/oracle/rules/waterx-rule.js +53 -14
  58. package/dist/src/perp/client.d.ts +40 -27
  59. package/dist/src/perp/client.js +29 -21
  60. package/dist/src/perp/config.d.ts +6 -6
  61. package/dist/src/perp/config.js +11 -9
  62. package/dist/src/perp/index.d.ts +3 -3
  63. package/dist/src/perp/index.js +2 -2
  64. package/dist/src/unified-client.d.ts +14 -9
  65. package/dist/src/unified-client.js +2 -2
  66. package/package.json +1 -1
@@ -11,17 +11,17 @@
11
11
  * remain plain collector-feed helpers wired directly into `aggregate.ts`.
12
12
  *
13
13
  * This file defines the port only — routing IS wired: `aggregate.ts`'s
14
- * `refreshOraclePrices` selects the concrete rule per `host.oracleSource` via
15
- * `rule-registry.ts`, then drives fetch + `buildUpdateCalls` through this
16
- * port; `aggregate.ts` stays the sole orchestrator.
14
+ * `refreshOraclePrices` resolves a concrete rule per `host.oracleSources`
15
+ * entry via `rule-registry.ts`, then drives fetch + `buildUpdateCalls`
16
+ * through this port; `aggregate.ts` stays the sole orchestrator.
17
17
  */
18
18
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
19
19
  import type { OracleHost } from "./host.ts";
20
20
  import type { OracleFeeSource, PythCache } from "./pyth.ts";
21
21
  export type PriceUpdateRuleKind = "pyth_rule" | "pyth_lazer_rule" | "supra_rule" | "constant_rule" | "waterx_rule";
22
22
  /**
23
- * The subset of `PriceUpdateRuleKind`s selectable via a client's `oracleSource`
24
- * create option (see `OracleHost.oracleSource`) — i.e. rules that can serve as
23
+ * The subset of `PriceUpdateRuleKind`s listable in a client's `oracleSource`
24
+ * create option (see `OracleHost.oracleSources`) — i.e. rules that can serve as
25
25
  * the on-chain price *update* leg `refreshOraclePrices` runs before aggregating.
26
26
  * `supra_rule` and `constant_rule` are auxiliary rules fed alongside whichever
27
27
  * source is selected (see `aggregateTicker`), not sources themselves. The SDK
@@ -116,7 +116,14 @@ export interface UpdateDataProvider {
116
116
  get(source: OracleSource, tickers: string[]): Promise<RuleUpdateData | null>;
117
117
  }
118
118
  export interface PriceUpdateRule {
119
- readonly kind: PriceUpdateRuleKind;
119
+ /**
120
+ * `OracleSource`, not the wider `PriceUpdateRuleKind`: only selectable
121
+ * sources implement this port (`supra_rule`/`constant_rule` are plain
122
+ * collector-feed helpers), and the narrower type is what lets
123
+ * `refreshOraclePrices`'s per-source carry step switch exhaustively —
124
+ * adding a source without deciding its carry becomes a compile error.
125
+ */
126
+ readonly kind: OracleSource;
120
127
  /**
121
128
  * `true` when this rule's on-chain update leg charges a per-update fee
122
129
  * that must be paid from either a sponsor fund or `tx.gas` (Pyth Core:
@@ -11,9 +11,9 @@
11
11
  * remain plain collector-feed helpers wired directly into `aggregate.ts`.
12
12
  *
13
13
  * This file defines the port only — routing IS wired: `aggregate.ts`'s
14
- * `refreshOraclePrices` selects the concrete rule per `host.oracleSource` via
15
- * `rule-registry.ts`, then drives fetch + `buildUpdateCalls` through this
16
- * port; `aggregate.ts` stays the sole orchestrator.
14
+ * `refreshOraclePrices` resolves a concrete rule per `host.oracleSources`
15
+ * entry via `rule-registry.ts`, then drives fetch + `buildUpdateCalls`
16
+ * through this port; `aggregate.ts` stays the sole orchestrator.
17
17
  */
18
18
  /**
19
19
  * Shared null → kind → shape guard ladder for a `PriceUpdateRule.buildUpdateCalls`
@@ -14,8 +14,52 @@
14
14
  * 4. hot_potato_vector::destroy
15
15
  */
16
16
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
17
+ import type { Network } from "../constants.ts";
17
18
  import type { PythFetchPolicy } from "./config.ts";
18
19
  import type { OracleHost } from "./host.ts";
20
+ /**
21
+ * Everything the Core source needs that is not in the canonical
22
+ * `waterx-config` JSON lives HERE, in the source that consumes it —
23
+ * `client.pyth` carries only the caller's credential/fetch policy
24
+ * (see `PythAccessConfig`). No other oracle source reads this table:
25
+ * selecting `pyth_lazer_rule` (or any future source) must never touch a Core
26
+ * endpoint or Core state object, so nothing Core-shaped stays on the shared
27
+ * client to leak across sources.
28
+ *
29
+ * - `state_id` / `wormhole_state_id` — the Core Pyth + Wormhole state objects
30
+ * the on-chain update PTB reads (below).
31
+ * - `hermes_endpoint` — the Core Hermes REST base for
32
+ * `fetchPriceFeedsUpdateData`. Keyless today; auth-first after the Pyth Pro
33
+ * migration (post-2026-08-18) via `client.pyth.api_key`.
34
+ */
35
+ export interface PythCoreInfra {
36
+ state_id: string;
37
+ wormhole_state_id: string;
38
+ hermes_endpoint: string;
39
+ }
40
+ export declare const PYTH_CORE_INFRA: Record<Network, PythCoreInfra>;
41
+ /**
42
+ * The Core source's Hermes REST base for `network` — for consumers (BE/FE
43
+ * read planes) whose `ORACLE_SOURCE` fed set includes `'pyth_rule'`. A fed
44
+ * set WITHOUT `pyth_rule` reads the keyed Pyth Pro base instead — resolve
45
+ * through `resolveHermesReadEndpoint` (`oracle/read-plane.ts`) rather than
46
+ * branching by hand.
47
+ */
48
+ export declare function pythCoreHermesEndpoint(network: Network): string;
49
+ /**
50
+ * The Pyth Pro Hermes-compatible REST base — the documented drop-in
51
+ * replacement for the Core endpoint (`https://pyth.dourolabs.app/hermes/…`),
52
+ * IDENTICAL for every subscriber: only the `Authorization: Bearer` key
53
+ * (`client.pyth.api_key` / the `pythApiKey` create option) is
54
+ * account-specific, so the URL is SDK infra, not deployment config. One base
55
+ * for both networks — Pro has no testnet variant (Pyth feed ids are
56
+ * chain-agnostic; `hermes-beta` is a Core-testnet concern). Consumers with a
57
+ * proxy/self-hosted mirror override it per deployment via
58
+ * `resolveHermesReadEndpoint`'s `override` param.
59
+ */
60
+ export declare const PYTH_PRO_HERMES_ENDPOINT = "https://pyth.dourolabs.app/hermes";
61
+ /** Accessor mirror of {@link pythCoreHermesEndpoint} for the Pro base. */
62
+ export declare function pythProHermesEndpoint(): string;
19
63
  type PriceTableInfo = {
20
64
  id: string;
21
65
  fieldType: string;
@@ -16,6 +16,44 @@
16
16
  import { fromHex, toHex } from "@mysten/bcs";
17
17
  import { bcs } from "@mysten/sui/bcs";
18
18
  import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch, trimTrailingSlashes, } from "./update-fetch.js";
19
+ export const PYTH_CORE_INFRA = {
20
+ MAINNET: {
21
+ state_id: "0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8",
22
+ wormhole_state_id: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
23
+ hermes_endpoint: "https://hermes.pyth.network",
24
+ },
25
+ TESTNET: {
26
+ state_id: "0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c",
27
+ wormhole_state_id: "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
28
+ hermes_endpoint: "https://hermes-beta.pyth.network",
29
+ },
30
+ };
31
+ /**
32
+ * The Core source's Hermes REST base for `network` — for consumers (BE/FE
33
+ * read planes) whose `ORACLE_SOURCE` fed set includes `'pyth_rule'`. A fed
34
+ * set WITHOUT `pyth_rule` reads the keyed Pyth Pro base instead — resolve
35
+ * through `resolveHermesReadEndpoint` (`oracle/read-plane.ts`) rather than
36
+ * branching by hand.
37
+ */
38
+ export function pythCoreHermesEndpoint(network) {
39
+ return PYTH_CORE_INFRA[network].hermes_endpoint;
40
+ }
41
+ /**
42
+ * The Pyth Pro Hermes-compatible REST base — the documented drop-in
43
+ * replacement for the Core endpoint (`https://pyth.dourolabs.app/hermes/…`),
44
+ * IDENTICAL for every subscriber: only the `Authorization: Bearer` key
45
+ * (`client.pyth.api_key` / the `pythApiKey` create option) is
46
+ * account-specific, so the URL is SDK infra, not deployment config. One base
47
+ * for both networks — Pro has no testnet variant (Pyth feed ids are
48
+ * chain-agnostic; `hermes-beta` is a Core-testnet concern). Consumers with a
49
+ * proxy/self-hosted mirror override it per deployment via
50
+ * `resolveHermesReadEndpoint`'s `override` param.
51
+ */
52
+ export const PYTH_PRO_HERMES_ENDPOINT = "https://pyth.dourolabs.app/hermes";
53
+ /** Accessor mirror of {@link pythCoreHermesEndpoint} for the Pro base. */
54
+ export function pythProHermesEndpoint() {
55
+ return PYTH_PRO_HERMES_ENDPOINT;
56
+ }
19
57
  export class PythCache {
20
58
  pythStateInfo;
21
59
  wormholePackageId;
@@ -428,7 +466,7 @@ async function getPriceTableInfo(client, pythStateId, cache) {
428
466
  }
429
467
  async function getPriceFeedObjectId(client, table, feedId, cache, pythStateId) {
430
468
  const normalized = feedId.replace(/^0x/, "");
431
- const cacheKey = pythStateId ? `${pythStateId}:${normalized}` : normalized;
469
+ const cacheKey = `${pythStateId}:${normalized}`;
432
470
  if (cache?.priceFeedObjectIdCache.has(cacheKey)) {
433
471
  return cache.priceFeedObjectIdCache.get(cacheKey);
434
472
  }
@@ -523,7 +561,9 @@ export async function buildPythPriceUpdateCalls(tx, host, updates, feedIds, opts
523
561
  throw new OracleFeeSourceUnavailableError();
524
562
  }
525
563
  const cache = opts?.cache;
526
- const pyth = host.pyth;
564
+ // Core on-chain infra is the `pyth_rule` source's own table, keyed by the
565
+ // host's network — `client.pyth` carries only the caller's credential/policy.
566
+ const pyth = PYTH_CORE_INFRA[host.network];
527
567
  const [stateInfo, wormholePackageId, table] = await Promise.all([
528
568
  getPythStateInfo(host.grpcClient, pyth.state_id, cache),
529
569
  getWormholePackageId(host.grpcClient, pyth.wormhole_state_id, cache),
@@ -581,9 +621,9 @@ export async function buildPythPriceUpdateCalls(tx, host, updates, feedIds, opts
581
621
  }
582
622
  /** All-in-one: fetch from Hermes, append update calls. Returns PriceInfoObject IDs. */
583
623
  export async function updatePythPrices(tx, host, feedIds, opts) {
584
- // `host.pyth` is the Pyth Core infra (fixed per network) plus the caller's
585
- // api_key/fetch endpoint, credential and policy all come from it.
586
- const endpoint = host.pyth.hermes_endpoint;
624
+ // The Core source's own Hermes endpoint (per-network, rule-owned table);
625
+ // credential + fetch policy are the caller-supplied `client.pyth` slice.
626
+ const endpoint = PYTH_CORE_INFRA[host.network].hermes_endpoint;
587
627
  const updates = await fetchPriceFeedsUpdateData(endpoint, feedIds, {
588
628
  apiKey: host.pyth.api_key,
589
629
  fetch: host.pyth.fetch,
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `read-plane.ts` — per-source READ-plane resolution: which of a caller's
3
+ * tickers a source can PRICE off-chain, and with which ids. The write plane
4
+ * (`PriceUpdateRule` + each source's `feeds` block) and the read plane are
5
+ * DIFFERENT namespaces for the pyth sources: Lazer WRITES with the integer
6
+ * ids in `pyth_lazer_rule.feeds`, but every Hermes-compatible price READ is
7
+ * keyed by the hex ids only `pyth_rule.feeds` carries. That cross-block
8
+ * dependency is a config invariant ("a lazer-fed ticker must also carry a
9
+ * Core hex entry to be read-visible"), and it lives HERE, once — consumers
10
+ * (FE/BE price facades) resolve through this instead of hardcoding which
11
+ * sources share which feeds namespace.
12
+ */
13
+ import type { Network } from "../constants.ts";
14
+ import type { OracleHost } from "./host.ts";
15
+ import type { OracleSource } from "./price-update-rule.ts";
16
+ /**
17
+ * One source's read plan for a requested ticker set.
18
+ *
19
+ * - `plane: "hermes"` (pyth sources) — price via a Hermes-compatible REST
20
+ * endpoint, one entry per servable ticker mapped to its HEX feed id. The
21
+ * endpoint to execute against is {@link resolveHermesReadEndpoint} — the
22
+ * plan (ids) plus that resolver (host) is the complete read contract.
23
+ * - `plane: "quote_center"` (waterx) — price via the quote-center symbols
24
+ * api, keyed by ticker; served set = the `waterx_rule.feeds` block. An
25
+ * ABSENT block (source listed, package missing from the loaded config)
26
+ * serves NOTHING: claiming tickers would silently reroute reads to the
27
+ * quote-center — it happily serves symbols regardless of on-chain config —
28
+ * and swallow tickers a later-listed source could price. The
29
+ * misconfiguration is caught loudly by the consumer's feeds assert at
30
+ * client creation instead.
31
+ * - `unreadable` — requested tickers this source WRITES on-chain (its update
32
+ * leg serves them) but its read plane cannot price: the silent-invisibility
33
+ * trap (e.g. a lazer-fed ticker with no `pyth_rule.feeds` hex entry).
34
+ * Callers should surface these loudly; empty for sources whose write and
35
+ * read namespaces coincide.
36
+ */
37
+ export type OracleReadPlan = {
38
+ unreadable: string[];
39
+ } & ({
40
+ plane: "hermes";
41
+ feedIdByTicker: Map<string, string>;
42
+ } | {
43
+ plane: "quote_center";
44
+ tickers: string[];
45
+ });
46
+ /**
47
+ * Resolve `source`'s read plan for `tickers`. Pure config lookup — no
48
+ * network, no endpoint resolution (endpoints come from
49
+ * `pythCoreHermesEndpoint` / `waterxQuoteCenterEndpoint` / the deployment's
50
+ * own env). A ticker absent from the returned plan is simply not servable by
51
+ * THIS source's read plane — callers decide how to degrade (typically: ask
52
+ * the next source in their `ORACLE_SOURCE` list, then omit).
53
+ */
54
+ /**
55
+ * The Hermes-compatible REST base a deployment's hermes-plane read plans
56
+ * execute against — the endpoint half of the read contract
57
+ * ({@link resolveOracleReadPlan} is the ids half):
58
+ *
59
+ * - `pyth_rule` in the fed set → the Core source's own keyless endpoint
60
+ * (`pythCoreHermesEndpoint(network)`).
61
+ * - otherwise → `override` when the deployment set one (a proxy or
62
+ * self-hosted mirror), else the documented Pyth Pro base
63
+ * (`pythProHermesEndpoint()` — identical for every subscriber; auth is the
64
+ * caller's `pythApiKey` Bearer, not a per-deployment URL).
65
+ *
66
+ * Total — never throws, never falls back Core-ward: a fed set without
67
+ * `pyth_rule` reads Pro (or the override), full stop.
68
+ */
69
+ export declare function resolveHermesReadEndpoint(network: Network, sources: readonly OracleSource[], override?: string): string;
70
+ export declare function resolveOracleReadPlan(host: OracleHost, source: OracleSource, tickers: string[]): OracleReadPlan;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `read-plane.ts` — per-source READ-plane resolution: which of a caller's
3
+ * tickers a source can PRICE off-chain, and with which ids. The write plane
4
+ * (`PriceUpdateRule` + each source's `feeds` block) and the read plane are
5
+ * DIFFERENT namespaces for the pyth sources: Lazer WRITES with the integer
6
+ * ids in `pyth_lazer_rule.feeds`, but every Hermes-compatible price READ is
7
+ * keyed by the hex ids only `pyth_rule.feeds` carries. That cross-block
8
+ * dependency is a config invariant ("a lazer-fed ticker must also carry a
9
+ * Core hex entry to be read-visible"), and it lives HERE, once — consumers
10
+ * (FE/BE price facades) resolve through this instead of hardcoding which
11
+ * sources share which feeds namespace.
12
+ */
13
+ import { pythCoreHermesEndpoint, pythProHermesEndpoint } from "./pyth.js";
14
+ /**
15
+ * Resolve `source`'s read plan for `tickers`. Pure config lookup — no
16
+ * network, no endpoint resolution (endpoints come from
17
+ * `pythCoreHermesEndpoint` / `waterxQuoteCenterEndpoint` / the deployment's
18
+ * own env). A ticker absent from the returned plan is simply not servable by
19
+ * THIS source's read plane — callers decide how to degrade (typically: ask
20
+ * the next source in their `ORACLE_SOURCE` list, then omit).
21
+ */
22
+ /**
23
+ * The Hermes-compatible REST base a deployment's hermes-plane read plans
24
+ * execute against — the endpoint half of the read contract
25
+ * ({@link resolveOracleReadPlan} is the ids half):
26
+ *
27
+ * - `pyth_rule` in the fed set → the Core source's own keyless endpoint
28
+ * (`pythCoreHermesEndpoint(network)`).
29
+ * - otherwise → `override` when the deployment set one (a proxy or
30
+ * self-hosted mirror), else the documented Pyth Pro base
31
+ * (`pythProHermesEndpoint()` — identical for every subscriber; auth is the
32
+ * caller's `pythApiKey` Bearer, not a per-deployment URL).
33
+ *
34
+ * Total — never throws, never falls back Core-ward: a fed set without
35
+ * `pyth_rule` reads Pro (or the override), full stop.
36
+ */
37
+ export function resolveHermesReadEndpoint(network, sources, override) {
38
+ if (sources.includes("pyth_rule"))
39
+ return pythCoreHermesEndpoint(network);
40
+ return override ?? pythProHermesEndpoint();
41
+ }
42
+ export function resolveOracleReadPlan(host, source, tickers) {
43
+ switch (source) {
44
+ case "pyth_rule":
45
+ case "pyth_lazer_rule": {
46
+ const hexFeeds = host.config.packages.pyth_rule?.feeds;
47
+ const feedIdByTicker = new Map();
48
+ for (const ticker of tickers) {
49
+ const feedId = hexFeeds?.[ticker]?.feed_id;
50
+ if (feedId !== undefined)
51
+ feedIdByTicker.set(ticker, feedId);
52
+ }
53
+ // For pyth_rule the write and read namespaces coincide, so `unreadable`
54
+ // is always empty; for lazer it is exactly the hex-entry gap.
55
+ const writeFeeds = source === "pyth_lazer_rule" ? host.config.packages.pyth_lazer_rule?.feeds : hexFeeds;
56
+ const unreadable = tickers.filter((ticker) => writeFeeds?.[ticker] !== undefined && !feedIdByTicker.has(ticker));
57
+ return { plane: "hermes", feedIdByTicker, unreadable };
58
+ }
59
+ case "waterx_rule": {
60
+ // Absent feeds block ⇒ serves nothing (see the OracleReadPlan doc) —
61
+ // never claim tickers the config doesn't name.
62
+ const feeds = host.config.packages.waterx_rule?.feeds;
63
+ return {
64
+ plane: "quote_center",
65
+ tickers: feeds ? tickers.filter((ticker) => ticker in feeds) : [],
66
+ unreadable: [],
67
+ };
68
+ }
69
+ default: {
70
+ const exhausted = source;
71
+ throw new Error(`resolveOracleReadPlan: unhandled OracleSource '${String(exhausted)}'`);
72
+ }
73
+ }
74
+ }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * `rule-registry.ts` — maps a client-selected `OracleSource` to its concrete
3
- * `PriceUpdateRule` implementation. `refreshOraclePrices` (`aggregate.ts`) is
4
- * the only production caller; this is the one place `OracleSource` values are
5
- * wired to a rule instance. Selection is driven purely by the value passed in
6
- * (ultimately `OracleHost.oracleSource`, the `oracleSource` client create
3
+ * `PriceUpdateRule` implementation; the ONE place `OracleSource` values are
4
+ * wired to a rule instance. `refreshOraclePrices` (`aggregate.ts`) resolves
5
+ * every `host.oracleSources` entry through it, and consumers (e.g. a BE
6
+ * prefetch cache keying per source) import `resolveOracleRule` rather than
7
+ * hand-mirroring this map. Selection is driven purely by the value passed in
8
+ * (ultimately `OracleHost.oracleSources`, the `oracleSource` client create
7
9
  * option) — never by a config JSON `enabled` flag and never by `process.env`.
8
10
  *
9
11
  * Each source is self-contained: it owns its own infra + config and does NOT
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * `rule-registry.ts` — maps a client-selected `OracleSource` to its concrete
3
- * `PriceUpdateRule` implementation. `refreshOraclePrices` (`aggregate.ts`) is
4
- * the only production caller; this is the one place `OracleSource` values are
5
- * wired to a rule instance. Selection is driven purely by the value passed in
6
- * (ultimately `OracleHost.oracleSource`, the `oracleSource` client create
3
+ * `PriceUpdateRule` implementation; the ONE place `OracleSource` values are
4
+ * wired to a rule instance. `refreshOraclePrices` (`aggregate.ts`) resolves
5
+ * every `host.oracleSources` entry through it, and consumers (e.g. a BE
6
+ * prefetch cache keying per source) import `resolveOracleRule` rather than
7
+ * hand-mirroring this map. Selection is driven purely by the value passed in
8
+ * (ultimately `OracleHost.oracleSources`, the `oracleSource` client create
7
9
  * option) — never by a config JSON `enabled` flag and never by `process.env`.
8
10
  *
9
11
  * Each source is self-contained: it owns its own infra + config and does NOT
@@ -7,7 +7,7 @@
7
7
  * changes vs `../pyth.ts` / `./pyth-rule.ts`.
8
8
  */
9
9
  import { assertRuleUpdateData, } from "../price-update-rule.js";
10
- import { buildPythPriceUpdateCalls, endpointSupportedFeedIds, fetchPriceFeedsUpdateData, } from "../pyth.js";
10
+ import { buildPythPriceUpdateCalls, endpointSupportedFeedIds, fetchPriceFeedsUpdateData, pythCoreHermesEndpoint, } from "../pyth.js";
11
11
  /**
12
12
  * Shape check ONLY — the `kind` discriminant is checked separately by the
13
13
  * caller before this runs, since a same-shaped payload from a different rule
@@ -33,10 +33,10 @@ export const PythCoreRule = {
33
33
  async fetchUpdateData(host, tickers) {
34
34
  if (tickers.length === 0)
35
35
  return null;
36
- // `host.pyth` is this source's own infra — the fixed per-network Core
37
- // Pyth block plus the caller's api_key/fetch. Endpoint, credential and
38
- // retry/timeout policy all come from it.
39
- const endpoint = host.pyth.hermes_endpoint;
36
+ // Endpoint is this source's own per-network infra (`PYTH_CORE_INFRA`);
37
+ // credential + retry/timeout policy are the caller-supplied `client.pyth`
38
+ // access slice.
39
+ const endpoint = pythCoreHermesEndpoint(host.network);
40
40
  const feedIds = tickers.map((ticker) => host.getPythFeed(ticker).feed_id);
41
41
  const updates = await fetchPriceFeedsUpdateData(endpoint, feedIds, {
42
42
  apiKey: host.pyth.api_key,
@@ -8,8 +8,29 @@
8
8
  * `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
9
9
  */
10
10
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
11
+ import type { Network } from "../../constants.ts";
11
12
  import type { OracleHost } from "../host.ts";
12
13
  import { type PriceUpdateRule } from "../price-update-rule.ts";
14
+ /**
15
+ * Pyth Lazer (Pyth Pro) external infra — owned by THIS source, by network.
16
+ * Per-network constants for infrastructure Pyth operates (not part of the
17
+ * `waterx-config` JSON), co-located with the only rule that reads them —
18
+ * no other oracle source ever touches a Lazer endpoint or verifier.
19
+ *
20
+ * - `endpoint` — Lazer HTTP API base; signed updates come from
21
+ * `POST /v1/latest_price` (Bearer-authenticated). The service is
22
+ * network-agnostic (one signed payload verifies on any chain that trusts the
23
+ * 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.
29
+ */
30
+ export declare const LAZER_INFRA: Record<Network, {
31
+ endpoint: string;
32
+ verifier_package: string;
33
+ }>;
13
34
  /** `pyth_lazer_rule`'s narrowed `RuleUpdateData.payload` shape. */
14
35
  export interface PythLazerUpdatePayload {
15
36
  /** One signed `leEcdsa` message carrying every requested feed. */
@@ -8,9 +8,34 @@
8
8
  * `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
9
9
  */
10
10
  import { fromHex } from "@mysten/bcs";
11
- import { LAZER_DEFAULTS } from "../config.js";
12
11
  import { assertRuleUpdateData, } from "../price-update-rule.js";
13
12
  import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch } from "../update-fetch.js";
13
+ /**
14
+ * Pyth Lazer (Pyth Pro) external infra — owned by THIS source, by network.
15
+ * Per-network constants for infrastructure Pyth operates (not part of the
16
+ * `waterx-config` JSON), co-located with the only rule that reads them —
17
+ * no other oracle source ever touches a Lazer endpoint or verifier.
18
+ *
19
+ * - `endpoint` — Lazer HTTP API base; signed updates come from
20
+ * `POST /v1/latest_price` (Bearer-authenticated). The service is
21
+ * network-agnostic (one signed payload verifies on any chain that trusts the
22
+ * Lazer signers), so both networks share the production host.
23
+ * - `verifier_package` — the Sui package carrying
24
+ * `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
25
+ * still the original v1 publish; mainnet is the v2-upgraded package (which
26
+ * still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
27
+ * contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
28
+ */
29
+ export const LAZER_INFRA = {
30
+ MAINNET: {
31
+ endpoint: "https://pyth-lazer.dourolabs.app",
32
+ verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
33
+ },
34
+ TESTNET: {
35
+ endpoint: "https://pyth-lazer.dourolabs.app",
36
+ verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
37
+ },
38
+ };
14
39
  /**
15
40
  * Signed-update request pins, mirroring what the on-chain rule consumes:
16
41
  * - `properties` — `price` + `exponent` are REQUIRED by
@@ -147,7 +172,7 @@ export const PythLazerRule = {
147
172
  if (!apiKey) {
148
173
  throw new LazerApiKeyMissingError();
149
174
  }
150
- const update = await fetchLazerSignedUpdate(LAZER_DEFAULTS[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
175
+ const update = await fetchLazerSignedUpdate(LAZER_INFRA[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
151
176
  return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
152
177
  },
153
178
  /**
@@ -184,7 +209,7 @@ export const PythLazerRule = {
184
209
  return undefined;
185
210
  const lazer = requireLazerPackage(host);
186
211
  const [update] = tx.moveCall({
187
- target: `${LAZER_DEFAULTS[host.network].verifier_package}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
212
+ target: `${LAZER_INFRA[host.network].verifier_package}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
188
213
  arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
189
214
  });
190
215
  return { kind: "pyth_lazer_rule", update };
@@ -7,6 +7,7 @@
7
7
  * `PriceInfoObject` is fresh.
8
8
  */
9
9
  import { feed as pythRuleFeed } from "../../generated/waterx_pyth_rule/pyth_rule.js";
10
+ import { PYTH_CORE_INFRA } from "../pyth.js";
10
11
  export function feedPythRule(tx, host, collector, priceInfoObjectId) {
11
12
  pythRuleFeed({
12
13
  package: host.config.packages.pyth_rule.published_at,
@@ -15,10 +16,10 @@ export function feedPythRule(tx, host, collector, priceInfoObjectId) {
15
16
  config: tx.object(host.config.packages.pyth_rule.config),
16
17
  // The deployed pyth_rule package is compiled against the Core pyth
17
18
  // dependency, so its `&PythState` parameter is the Core-package-qualified
18
- // type and `host.pyth` (the fixed per-network Core infra) is always the
19
- // right state to pass. The config's price_info_object entries are Core
20
- // objects to match.
21
- pythState: tx.object(host.pyth.state_id),
19
+ // type and the Core source's own per-network state (`PYTH_CORE_INFRA`)
20
+ // is always the right state to pass. The config's price_info_object
21
+ // entries are Core objects to match.
22
+ pythState: tx.object(PYTH_CORE_INFRA[host.network].state_id),
22
23
  pythPriceInfo: tx.object(priceInfoObjectId),
23
24
  },
24
25
  })(tx);
@@ -4,7 +4,7 @@
4
4
  * `aggregateTicker` appends per waterx-routed ticker. Pulls one enclave-signed
5
5
  * batch envelope covering every requested ticker from the quote-center
6
6
  * (`GET /v1/quotes/update?symbols=…`, endpoint from `host.waterx` — the
7
- * `waterxEndpoint`/`waterxFetch` create options — else `WATERX_DEFAULTS`), then —
7
+ * `waterxEndpoint`/`waterxFetch` create options — else its own `WATERX_INFRA`), then —
8
8
  * unlike Pyth Lazer, whose verify is a single shared PTB step — verifies AND
9
9
  * feeds in ONE `waterx_rule::collect_batch_latest` call per collector (the Move
10
10
  * API bundles the two). So `buildUpdateCalls` emits nothing and the signed
@@ -13,12 +13,47 @@
13
13
  * `collect_batch_latest` is the dual-rule path: it feeds the item matching
14
14
  * `collector.symbol()` WITHOUT aggregating, so a waterx-routed ticker composes
15
15
  * onto the same collector as Pyth/Supra (compose-then-aggregate). On-chain a
16
- * freshness miss / replayed timestamp ABSTAINS (the other weighted rules
17
- * cover); a config/integrity mismatch or bad signature aborts.
16
+ * FRESHNESS miss ABSTAINS (the other weighted rules cover); a config/
17
+ * integrity mismatch, bad signature, future timestamp — or a REPLAYED signed
18
+ * timestamp — ABORTS (`EReplayedSignature`, audit F-014: a signed tuple is
19
+ * single-use per symbol, enforced by a per-symbol high-water mark BEFORE any
20
+ * weight arbitration). Consequence for concurrent builds: two PTBs carrying
21
+ * the same envelope for the same symbol cannot both land — the second aborts
22
+ * even if the rule is unweighted for that ticker. Never share one fetched
23
+ * envelope across builds that may execute concurrently for the same symbol.
18
24
  */
19
25
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
26
+ import type { Network } from "../../constants.ts";
20
27
  import type { OracleHost } from "../host.ts";
21
28
  import { type PriceUpdateRule, type RuleUpdateData } from "../price-update-rule.ts";
29
+ /** The single signing intent the quote-center emits — exported so read-plane
30
+ * consumers can mirror the rule's own envelope intent check (a mispointed
31
+ * endpoint must be rejected by reads exactly as tx-builds reject it). */
32
+ export declare const BATCH_PRICE_INTENT = 1;
33
+ /**
34
+ * WaterX quote-center external infra — owned by THIS source, by network.
35
+ * Mirrors `PYTH_CORE_INFRA` (oracle/pyth.ts) and `LAZER_INFRA`
36
+ * (rules/pyth-lazer-rule.ts): per-network constants for infrastructure the
37
+ * source's operator runs, co-located with the only rule that reads them — no
38
+ * other oracle source ever touches a quote-center endpoint. Public read (no
39
+ * auth), so there is no api_key. `endpoint` has no trailing slash — the rule
40
+ * appends the path.
41
+ *
42
+ * These are the DEFAULTS behind the caller's `client.waterx` access slice
43
+ * (`waterxEndpoint` / `waterxFetch` create options) — the browser-CORS proxy
44
+ * hook, since this is the one source fetched from the page.
45
+ */
46
+ export declare const WATERX_INFRA: Record<Network, {
47
+ endpoint: string;
48
+ }>;
49
+ /**
50
+ * The waterx source's quote-center base for `network` — the ONE accessor
51
+ * consumers (BE/FE read planes) use when, and only when, their own
52
+ * `ORACLE_SOURCE` resolves to `'waterx_rule'`. Mirrors
53
+ * `pythCoreHermesEndpoint`. Under any other source the read endpoint is that
54
+ * source's own configuration — never this one.
55
+ */
56
+ export declare function waterxQuoteCenterEndpoint(network: Network): string;
22
57
  /**
23
58
  * One item inside a signed batch payload, mirroring the quote-center
24
59
  * `/v1/quotes/update` JSON 1:1 (snake_case). The u64 integer fields are the
@@ -82,8 +117,10 @@ export declare function waterxEnvelopeOf(data: RuleUpdateData): WaterxSignedEnve
82
117
  * per item, the exact shape the enclave signed) and contribute the price for
83
118
  * `collector.symbol()` to the collector. One collect call re-verifies the batch
84
119
  * signature and picks this collector's symbol out of the batch; on-chain it
85
- * abstains (records `none`) instead of aborting when the symbol is stale,
86
- * absent from the batch, or its timestamp was already accepted (replay).
120
+ * abstains (records `none`) when the symbol is stale or absent from the batch,
121
+ * but ABORTS `EReplayedSignature` when the symbol's signed timestamp was
122
+ * already accepted (per-symbol high-water mark, audit F-014) — see the module
123
+ * header for the concurrent-build consequence.
87
124
  */
88
125
  export declare function feedWaterxRule(tx: Transaction, host: OracleHost, collector: TransactionArgument, envelope: WaterxSignedEnvelope): void;
89
126
  export declare const WaterxRule: PriceUpdateRule;