@waterx/sdk 4.3.0 → 4.3.2

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 (46) hide show
  1. package/README.md +9 -4
  2. package/dist/cjs/src/oracle/aggregate.d.ts +4 -3
  3. package/dist/cjs/src/oracle/aggregate.js +7 -5
  4. package/dist/cjs/src/oracle/index.d.ts +3 -1
  5. package/dist/cjs/src/oracle/index.js +11 -1
  6. package/dist/cjs/src/oracle/price-update-rule.d.ts +31 -9
  7. package/dist/cjs/src/oracle/price-update-rule.js +20 -0
  8. package/dist/cjs/src/oracle/read-plane.js +11 -4
  9. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +2 -1
  10. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +25 -7
  11. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +46 -19
  12. package/dist/cjs/src/oracle/rules/waterx-rule.js +4 -1
  13. package/dist/cjs/src/oracle/source-list.d.ts +36 -0
  14. package/dist/cjs/src/oracle/source-list.js +57 -0
  15. package/dist/cjs/src/perp/client.d.ts +1 -1
  16. package/dist/cjs/src/perp/client.js +11 -7
  17. package/dist/cjs/src/perp/config-view.js +7 -6
  18. package/dist/cjs/src/perp/index.d.ts +1 -1
  19. package/dist/cjs/src/perp/index.js +8 -4
  20. package/dist/cjs/src/perp/user/staking.js +2 -1
  21. package/dist/cjs/src/utils/config.js +2 -1
  22. package/dist/cjs/src/utils/record.d.ts +12 -0
  23. package/dist/cjs/src/utils/record.js +22 -0
  24. package/dist/src/oracle/aggregate.d.ts +4 -3
  25. package/dist/src/oracle/aggregate.js +7 -5
  26. package/dist/src/oracle/index.d.ts +3 -1
  27. package/dist/src/oracle/index.js +8 -1
  28. package/dist/src/oracle/price-update-rule.d.ts +31 -9
  29. package/dist/src/oracle/price-update-rule.js +19 -0
  30. package/dist/src/oracle/read-plane.js +11 -4
  31. package/dist/src/oracle/rules/pyth-core-rule.js +2 -1
  32. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +25 -7
  33. package/dist/src/oracle/rules/pyth-lazer-rule.js +46 -19
  34. package/dist/src/oracle/rules/waterx-rule.js +4 -1
  35. package/dist/src/oracle/source-list.d.ts +36 -0
  36. package/dist/src/oracle/source-list.js +53 -0
  37. package/dist/src/perp/client.d.ts +1 -1
  38. package/dist/src/perp/client.js +11 -7
  39. package/dist/src/perp/config-view.js +7 -6
  40. package/dist/src/perp/index.d.ts +1 -1
  41. package/dist/src/perp/index.js +1 -1
  42. package/dist/src/perp/user/staking.js +2 -1
  43. package/dist/src/utils/config.js +2 -1
  44. package/dist/src/utils/record.d.ts +12 -0
  45. package/dist/src/utils/record.js +19 -0
  46. package/package.json +1 -1
package/README.md CHANGED
@@ -113,15 +113,20 @@ ONE **required** client create option, `oracleSource`, names the price-update so
113
113
 
114
114
  **Multi-source fed sets.** With a list, ONE build fetches and feeds EVERY listed source's data in the same PTB; the chain's per-ticker aggregator **weight tables** decide which contributions count — feeding an unweighted rule's price is dropped on-chain, while starving a weighted rule aborts. That asymmetry is what makes weight migrations (Core→Pro, Pyth↔waterx coexistence) safe: keep the list a **superset** of every ticker's weighted rule set and flip weights per ticker at any time — an env edit, never an SDK release. (One caveat: waterx's feed call burns a per-symbol signed-timestamp high-water mark regardless of weights — see the replay note below.)
115
115
 
116
- **No cross-source fallback, no feeds guard at init.** Construction rejects an empty/nullish `oracleSource`, but a listed source whose feed for a requested ticker is absent is **not** an error at client creation — the build fails at **tx-build** only when **no** listed source serves the ticker (constant-only tickers, which need no price update, are exempt). A present-but-wrong feed id is not validated by the SDK; it aborts on-chain at dry-run.
116
+ **No cross-source fallback, no feeds guard at init.** Construction rejects an empty/nullish `oracleSource` **and any value outside `ORACLE_SOURCES`** (a legacy `'core'` / `'pyth'` string fails at `create`), but a listed source whose feed for a requested ticker is absent is **not** an error at client creation — the build fails at **tx-build** only when **no** listed source serves the ticker (constant-only tickers, which need no price update, are exempt). A present-but-wrong feed id is not validated by the SDK; it aborts on-chain at dry-run.
117
117
 
118
118
  Every source's external infra is a **rule-owned per-network table**, never deployment-overridable and never in the config JSON: `PYTH_CORE_INFRA` (`src/oracle/pyth.ts` — Pyth state ids + the keyless Core Hermes endpoint, read-plane accessor `pythCoreHermesEndpoint(network)`), `LAZER_INFRA` (`src/oracle/rules/pyth-lazer-rule.ts`), `WATERX_INFRA` (`src/oracle/rules/waterx-rule.ts` — testnet `quote-center-staging.waterx.app` / mainnet `quote-center.waterx.app`, accessor `waterxQuoteCenterEndpoint(network)`). For **price READS** under a fed set without `pyth_rule`, the documented Pyth Pro base (`pythProHermesEndpoint()` — identical for every subscriber, auth via the Bearer key) applies: resolve the read endpoint with `resolveHermesReadEndpoint(network, sources, override?)` instead of branching by hand, and pair it with `resolveOracleReadPlan` for the per-source served-sets/ids. `client.pyth` is the access-only `PythAccessConfig` — just the caller-supplied `pythApiKey` / `pythFetch` create options (a secret has no place in a public CDN JSON); `client.waterx` is likewise `WaterxAccessConfig` (`waterxEndpoint` / `waterxFetch` overrides only; fetch policy resolves **`waterxFetch` → built-in defaults** — deliberately no `pythFetch` fallback, sources never share config). See the browser/CORS note below.
119
119
 
120
120
  ```ts
121
- // Per-environment wiring — the consumer owns the env var, not the SDK:
121
+ // Per-environment wiring — the consumer owns the env var, not the SDK.
122
+ // parseOracleSourceList is THE canonical parser (trim, drop empty entries,
123
+ // validate every value, dedupe, throw operator-actionably) — never a bare
124
+ // split-and-cast, which would hand the strict constructor untrimmed junk.
125
+ import { parseOracleSourceList } from "@waterx/sdk/oracle";
126
+
122
127
  const perp = await PerpClient.create(network, {
123
128
  waterxConfigUrl,
124
- oracleSource: process.env.ORACLE_SOURCE!.split(",") as OracleSource[], // REQUIRED; comma list = the fed set
129
+ oracleSource: parseOracleSourceList(process.env.ORACLE_SOURCE), // REQUIRED; comma list = the fed set
125
130
  pythApiKey: process.env.PYTH_API_KEY, // required iff 'pyth_lazer_rule' is listed (Lazer is auth-first)
126
131
  });
127
132
  ```
@@ -133,7 +138,7 @@ This is the coexistence rollout pattern: staging lists every source under migrat
133
138
  Every source plugs in the same way — routing is driven **only** by the client's `oracleSource` option (never a config `enabled` flag, never `process.env`):
134
139
 
135
140
  1. **Implement `PriceUpdateRule`** in `src/oracle/rules/<name>-rule.ts` — all port fields (`src/oracle/price-update-rule.ts`): `kind`, `requiresFeeSource` (`true` iff the on-chain verify draws a per-update fee — gates the fail-fast fee-source check), `supportedTickers`, `fetchUpdateData`, `narrowUpdateData` (subset a cached whole-universe payload to one build's tickers — a divisible payload returns a per-feed subset, an indivisible one returns itself whole iff fully covered; uncovered ticker → `null` miss), `buildUpdateCalls`.
136
- 2. **Register it** in `src/oracle/rule-registry.ts` (`DEFAULT_RULES`) under a new `OracleSource` value (added to the union in `price-update-rule.ts`).
141
+ 2. **Register it** in `src/oracle/rule-registry.ts` (`DEFAULT_RULES`) under a new `OracleSource` value added to `ORACLE_SOURCES` in `price-update-rule.ts` (the union derives from that list; a registry test pins every listed value to a registered rule).
137
142
  3. **Publish the on-chain rule package** — its config entry (package ids, per-ticker `feeds`) arrives via the normal `waterx-config` deploy pipeline; type it in `OraclePackages` (`src/oracle/config.ts`).
138
143
  4. **Add SDK infra constants** if the source needs external infra that is not part of the config JSON (API endpoints, verifier packages, state objects) — a **rule-owned** per-network table inside the rule's own file, mirroring `LAZER_INFRA` / `WATERX_INFRA` (never on the shared client, never in `oracle/config.ts`). Wire its read-plane served-set/ids into `resolveOracleReadPlan` (`src/oracle/read-plane.ts`).
139
144
  5. **Consumers flip `oracleSource`** per environment — no consumer code change, no SDK re-release.
@@ -38,9 +38,10 @@ import { type WaterxSignedEnvelope } from "./rules/waterx-rule.ts";
38
38
  * if it is stale — it never aborts — so the call stays mandatory while
39
39
  * `pyth_rule` remains in the ticker's on-chain weighted set
40
40
  * (`EMissingPriceSource` requires every weighted rule to appear).
41
- * - **Lazer** — fed when `lazerUpdate` is supplied: the verified
42
- * `pyth_lazer::update::Update` produced by this PTB's lazer update leg
43
- * (see `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
41
+ * - **Lazer** — fed when `lazerUpdate` is supplied: the verified update this
42
+ * PTB's lazer update leg produced with the network's verify entry
43
+ * (`update_v2::Update` on mainnet, `update::Update` on testnet see
44
+ * `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
44
45
  * not (yet) weight `PythLazerRule`, the contribution is silently dropped
45
46
  * on-chain — feeding ahead of the weight migration is harmless.
46
47
  * - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
@@ -29,6 +29,7 @@ exports.aggregateTickerWithPyth = aggregateTickerWithPyth;
29
29
  exports.aggregateTickerWithConstant = aggregateTickerWithConstant;
30
30
  exports.refreshOraclePrices = refreshOraclePrices;
31
31
  const oracle_ts_1 = require("../generated/waterx_oracle/oracle.js");
32
+ const record_ts_1 = require("../utils/record.js");
32
33
  const pyth_ts_1 = require("./pyth.js");
33
34
  const rule_registry_ts_1 = require("./rule-registry.js");
34
35
  const constant_rule_ts_1 = require("./rules/constant-rule.js");
@@ -89,9 +90,10 @@ async function resolveGroupUpdateData(host, group, provider) {
89
90
  * if it is stale — it never aborts — so the call stays mandatory while
90
91
  * `pyth_rule` remains in the ticker's on-chain weighted set
91
92
  * (`EMissingPriceSource` requires every weighted rule to appear).
92
- * - **Lazer** — fed when `lazerUpdate` is supplied: the verified
93
- * `pyth_lazer::update::Update` produced by this PTB's lazer update leg
94
- * (see `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
93
+ * - **Lazer** — fed when `lazerUpdate` is supplied: the verified update this
94
+ * PTB's lazer update leg produced with the network's verify entry
95
+ * (`update_v2::Update` on mainnet, `update::Update` on testnet see
96
+ * `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
95
97
  * not (yet) weight `PythLazerRule`, the contribution is silently dropped
96
98
  * on-chain — feeding ahead of the weight migration is harmless.
97
99
  * - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
@@ -169,7 +171,7 @@ function aggregateTickerWithPyth(tx, host, args) {
169
171
  * {@link refreshOraclePrices}), which feeds both.
170
172
  */
171
173
  function aggregateTickerWithConstant(tx, host, args) {
172
- if (host.config.packages.pyth_rule?.feeds?.[args.ticker] !== undefined) {
174
+ if ((0, record_ts_1.ownEntry)(host.config.packages.pyth_rule?.feeds, args.ticker) !== undefined) {
173
175
  throw new Error(`'${args.ticker}' is in pyth_rule.feeds (dual-feed) — feed both via aggregateTicker({ priceInfoObjectId }) / refreshOraclePrices, not aggregateTickerWithConstant`);
174
176
  }
175
177
  aggregateTicker(tx, host, { ticker: args.ticker });
@@ -230,7 +232,7 @@ async function refreshOraclePrices(tx, host, tickers, opts = {}) {
230
232
  // price_info_object lookup for every ticker with a pyth_rule.feeds entry —
231
233
  // needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
232
234
  // which rule performed the on-chain update for that ticker.
233
- const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
235
+ const pythTickers = tickers.filter((t) => (0, record_ts_1.ownEntry)(host.config.packages.pyth_rule?.feeds, t) !== undefined);
234
236
  const priceInfoByTicker = new Map();
235
237
  pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
236
238
  // The fed set is a LIST (`host.oracleSources`, normalized + deduped at
@@ -21,13 +21,15 @@ export type { FetchPolicy } from "./update-fetch.ts";
21
21
  export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls, pythCoreHermesEndpoint, pythProHermesEndpoint, PYTH_PRO_HERMES_ENDPOINT, updatePythPrices, HermesEndpointRejectedAllFeedsError, MISSING_FEED_MEMO_TTL_MS, OracleFeeSourceUnavailableError, } from "./pyth.ts";
22
22
  export type { OracleFeeSource } from "./pyth.ts";
23
23
  export type { PriceUpdateRule, PriceUpdateRuleKind, RuleUpdateData, RuleUpdateHandle, BuildUpdateOpts, OracleSource, UpdateDataProvider, } from "./price-update-rule.ts";
24
+ export { ORACLE_SOURCES } from "./price-update-rule.ts";
25
+ export { isOracleSource, parseOracleSourceList } from "./source-list.ts";
24
26
  export { resolveOracleReadPlan, resolveHermesReadEndpoint } from "./read-plane.ts";
25
27
  export type { OracleReadPlan } from "./read-plane.ts";
26
28
  export { PythCoreRule } from "./rules/pyth-core-rule.ts";
27
29
  export type { PythCoreUpdatePayload } from "./rules/pyth-core-rule.ts";
28
30
  export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.ts";
29
31
  export type { PythLazerUpdatePayload } from "./rules/pyth-lazer-rule.ts";
30
- export { WaterxRule, parseSignedEnvelope, BATCH_PRICE_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint, } from "./rules/waterx-rule.ts";
32
+ export { WaterxRule, parseSignedEnvelope, BATCH_PRICE_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint, waterxEnvelopeOf, } from "./rules/waterx-rule.ts";
31
33
  export type { WaterxUpdatePayload, WaterxSignedEnvelope, WaterxBatchItem, } from "./rules/waterx-rule.ts";
32
34
  export { OracleSourceNotImplementedError, resolveOracleRule } from "./rule-registry.ts";
33
35
  export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.ts";
@@ -17,7 +17,7 @@
17
17
  * rules that consume it are separate concerns.
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.reimbursePythSponsor = exports.openPythSponsorFund = exports.refreshOraclePrices = exports.aggregateTickerWithConstant = exports.aggregateTickerWithPyth = exports.aggregateTicker = exports.resolveOracleRule = exports.OracleSourceNotImplementedError = exports.waterxQuoteCenterEndpoint = exports.WATERX_INFRA = exports.BATCH_PRICE_INTENT = exports.parseSignedEnvelope = exports.WaterxRule = exports.LazerApiKeyMissingError = exports.PythLazerRule = exports.PythCoreRule = exports.resolveHermesReadEndpoint = exports.resolveOracleReadPlan = exports.OracleFeeSourceUnavailableError = exports.MISSING_FEED_MEMO_TTL_MS = exports.HermesEndpointRejectedAllFeedsError = exports.updatePythPrices = exports.PYTH_PRO_HERMES_ENDPOINT = exports.pythProHermesEndpoint = exports.pythCoreHermesEndpoint = exports.buildPythPriceUpdateCalls = exports.probeMissingFeeds = exports.endpointSupportedFeedIds = exports.fetchPriceFeedsUpdateData = exports.PythCache = exports.joinEndpointPath = exports.fetchWithPolicy = exports.FetchPolicyError = void 0;
20
+ exports.reimbursePythSponsor = exports.openPythSponsorFund = exports.refreshOraclePrices = exports.aggregateTickerWithConstant = exports.aggregateTickerWithPyth = exports.aggregateTicker = exports.resolveOracleRule = exports.OracleSourceNotImplementedError = exports.waterxEnvelopeOf = exports.waterxQuoteCenterEndpoint = exports.WATERX_INFRA = exports.BATCH_PRICE_INTENT = exports.parseSignedEnvelope = exports.WaterxRule = exports.LazerApiKeyMissingError = exports.PythLazerRule = exports.PythCoreRule = exports.resolveHermesReadEndpoint = exports.resolveOracleReadPlan = exports.parseOracleSourceList = exports.isOracleSource = exports.ORACLE_SOURCES = exports.OracleFeeSourceUnavailableError = exports.MISSING_FEED_MEMO_TTL_MS = exports.HermesEndpointRejectedAllFeedsError = exports.updatePythPrices = exports.PYTH_PRO_HERMES_ENDPOINT = exports.pythProHermesEndpoint = exports.pythCoreHermesEndpoint = exports.buildPythPriceUpdateCalls = exports.probeMissingFeeds = exports.endpointSupportedFeedIds = exports.fetchPriceFeedsUpdateData = exports.PythCache = exports.joinEndpointPath = exports.fetchWithPolicy = exports.FetchPolicyError = void 0;
21
21
  // Shared fetch resilience wrapper — `FetchPolicyError` is re-exported (not
22
22
  // just the type) so a consumer (e.g. a BE prefetch cache) can `instanceof`
23
23
  // it off the failure `fetchPriceFeedsUpdateData` / `PythLazerRule` /
@@ -57,6 +57,13 @@ Object.defineProperty(exports, "updatePythPrices", { enumerable: true, get: func
57
57
  Object.defineProperty(exports, "HermesEndpointRejectedAllFeedsError", { enumerable: true, get: function () { return pyth_ts_1.HermesEndpointRejectedAllFeedsError; } });
58
58
  Object.defineProperty(exports, "MISSING_FEED_MEMO_TTL_MS", { enumerable: true, get: function () { return pyth_ts_1.MISSING_FEED_MEMO_TTL_MS; } });
59
59
  Object.defineProperty(exports, "OracleFeeSourceUnavailableError", { enumerable: true, get: function () { return pyth_ts_1.OracleFeeSourceUnavailableError; } });
60
+ // Canonical OracleSource value list + THE env-string parser consumers fold
61
+ // onto — semantics and rationale in `source-list.ts`'s header.
62
+ var price_update_rule_ts_1 = require("./price-update-rule.js");
63
+ Object.defineProperty(exports, "ORACLE_SOURCES", { enumerable: true, get: function () { return price_update_rule_ts_1.ORACLE_SOURCES; } });
64
+ var source_list_ts_1 = require("./source-list.js");
65
+ Object.defineProperty(exports, "isOracleSource", { enumerable: true, get: function () { return source_list_ts_1.isOracleSource; } });
66
+ Object.defineProperty(exports, "parseOracleSourceList", { enumerable: true, get: function () { return source_list_ts_1.parseOracleSourceList; } });
60
67
  // Per-source READ-plane resolution — which tickers a source can price
61
68
  // off-chain and with which ids (`resolveOracleReadPlan`), and which
62
69
  // Hermes-compatible base the hermes plans execute against
@@ -86,6 +93,9 @@ Object.defineProperty(exports, "parseSignedEnvelope", { enumerable: true, get: f
86
93
  Object.defineProperty(exports, "BATCH_PRICE_INTENT", { enumerable: true, get: function () { return waterx_rule_ts_1.BATCH_PRICE_INTENT; } });
87
94
  Object.defineProperty(exports, "WATERX_INFRA", { enumerable: true, get: function () { return waterx_rule_ts_1.WATERX_INFRA; } });
88
95
  Object.defineProperty(exports, "waterxQuoteCenterEndpoint", { enumerable: true, get: function () { return waterx_rule_ts_1.waterxQuoteCenterEndpoint; } });
96
+ // Rule-owned payload accessor (kind-check + unwrap in one place) — never
97
+ // hand-cast the payload shape.
98
+ Object.defineProperty(exports, "waterxEnvelopeOf", { enumerable: true, get: function () { return waterx_rule_ts_1.waterxEnvelopeOf; } });
89
99
  // `resolveOracleRule` is the ONE source→rule registry — exported so external
90
100
  // consumers (e.g. a BE prefetch cache that keys per source and needs each
91
101
  // source's `supportedTickers`/`fetchUpdateData`) resolve through it instead of
@@ -20,14 +20,28 @@ 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 listable in a client's `oracleSource`
24
- * create option (see `OracleHost.oracleSources`) i.e. rules that can serve as
25
- * the on-chain price *update* leg `refreshOraclePrices` runs before aggregating.
26
- * `supra_rule` and `constant_rule` are auxiliary rules fed alongside whichever
27
- * source is selected (see `aggregateTicker`), not sources themselves. The SDK
28
- * never reads `process.env` consumers resolve their own env var to this type.
23
+ * The canonical list of selectable oracle sources the SINGLE authority the
24
+ * {@link OracleSource} union derives from (the value-list→union derive
25
+ * idiom of `unified-client.ts`'s `NON_CLIENT_FIRST`, plus `Object.freeze`
26
+ * so the immutability is RUNTIME truth: `as const` alone would let a JS
27
+ * consumer push into the array and desync the membership Set built from it
28
+ * in `source-list.ts`). Runtime membership checks and the `ORACLE_SOURCE`
29
+ * env parser live there, on `isOracleSource` / `parseOracleSourceList`.
30
+ * Only sources belong here: `supra_rule` and `constant_rule` are auxiliary
31
+ * rules fed alongside whichever sources are selected (see
32
+ * `aggregateTicker`), not sources themselves — the `satisfies` keeps
33
+ * entries inside `PriceUpdateRuleKind` but adding an auxiliary rule to this
34
+ * list is an (incorrect) editorial decision this comment exists to prevent.
29
35
  */
30
- export type OracleSource = "pyth_rule" | "pyth_lazer_rule" | "waterx_rule";
36
+ export declare const ORACLE_SOURCES: readonly ["pyth_rule", "pyth_lazer_rule", "waterx_rule"];
37
+ /**
38
+ * The kinds listable in a client's `oracleSource` create option (see
39
+ * `OracleHost.oracleSources`) — i.e. rules that can serve as the on-chain
40
+ * price *update* leg `refreshOraclePrices` runs before aggregating. Derived
41
+ * from {@link ORACLE_SOURCES}. The SDK never reads `process.env` — consumers
42
+ * resolve their own env var to this type.
43
+ */
44
+ export type OracleSource = (typeof ORACLE_SOURCES)[number];
31
45
  /**
32
46
  * Off-chain payload fetched by a rule, tagged by `kind` so a caller holding
33
47
  * several rules' results can tell them apart. `payload` is `unknown` here —
@@ -69,14 +83,22 @@ export declare function assertRuleUpdateData<T>(data: RuleUpdateData, kind: Pric
69
83
  * return when its collector-feed leg needs a value produced by the update leg
70
84
  * *within the same PTB*. Pyth Core needs none (its feed leg reads the shared
71
85
  * `PriceInfoObject` the update leg refreshed), so it returns `void`. The Lazer
72
- * rule returns the verified `pyth_lazer::update::Update` result — one
86
+ * rule returns the verified-update result of its network's verify entry — one
73
87
  * signature verification covers every feed in the payload, and
74
88
  * `pyth_lazer_rule::feed` takes it by reference per ticker (see
75
89
  * `aggregateTicker`'s `lazerUpdate` arg).
76
90
  */
77
91
  export type RuleUpdateHandle = {
78
92
  readonly kind: "pyth_lazer_rule";
79
- /** 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
+ */
80
102
  readonly update: TransactionArgument;
81
103
  };
82
104
  /**
@@ -17,7 +17,27 @@
17
17
  * through this port; `aggregate.ts` stays the sole orchestrator.
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.ORACLE_SOURCES = void 0;
20
21
  exports.assertRuleUpdateData = assertRuleUpdateData;
22
+ /**
23
+ * The canonical list of selectable oracle sources — the SINGLE authority the
24
+ * {@link OracleSource} union derives from (the value-list→union derive
25
+ * idiom of `unified-client.ts`'s `NON_CLIENT_FIRST`, plus `Object.freeze`
26
+ * so the immutability is RUNTIME truth: `as const` alone would let a JS
27
+ * consumer push into the array and desync the membership Set built from it
28
+ * in `source-list.ts`). Runtime membership checks and the `ORACLE_SOURCE`
29
+ * env parser live there, on `isOracleSource` / `parseOracleSourceList`.
30
+ * Only sources belong here: `supra_rule` and `constant_rule` are auxiliary
31
+ * rules fed alongside whichever sources are selected (see
32
+ * `aggregateTicker`), not sources themselves — the `satisfies` keeps
33
+ * entries inside `PriceUpdateRuleKind` but adding an auxiliary rule to this
34
+ * list is an (incorrect) editorial decision this comment exists to prevent.
35
+ */
36
+ exports.ORACLE_SOURCES = Object.freeze([
37
+ "pyth_rule",
38
+ "pyth_lazer_rule",
39
+ "waterx_rule",
40
+ ]);
21
41
  /**
22
42
  * Shared null → kind → shape guard ladder for a `PriceUpdateRule.buildUpdateCalls`
23
43
  * payload — every rule's `buildUpdateCalls` needs the exact same three checks,
@@ -14,6 +14,7 @@
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.resolveHermesReadEndpoint = resolveHermesReadEndpoint;
16
16
  exports.resolveOracleReadPlan = resolveOracleReadPlan;
17
+ const record_ts_1 = require("../utils/record.js");
17
18
  const pyth_ts_1 = require("./pyth.js");
18
19
  /**
19
20
  * Resolve `source`'s read plan for `tickers`. Pure config lookup — no
@@ -47,26 +48,32 @@ function resolveOracleReadPlan(host, source, tickers) {
47
48
  switch (source) {
48
49
  case "pyth_rule":
49
50
  case "pyth_lazer_rule": {
51
+ // All ticker lookups go through `ownEntry` (own-keys-only): a ticker
52
+ // named like an Object.prototype key ("toString", "constructor", …)
53
+ // must read as not-listed, not as an inherited Function.
50
54
  const hexFeeds = host.config.packages.pyth_rule?.feeds;
51
55
  const feedIdByTicker = new Map();
52
56
  for (const ticker of tickers) {
53
- const feedId = hexFeeds?.[ticker]?.feed_id;
57
+ const feedId = (0, record_ts_1.ownEntry)(hexFeeds, ticker)?.feed_id;
54
58
  if (feedId !== undefined)
55
59
  feedIdByTicker.set(ticker, feedId);
56
60
  }
57
61
  // For pyth_rule the write and read namespaces coincide, so `unreadable`
58
62
  // is always empty; for lazer it is exactly the hex-entry gap.
59
63
  const writeFeeds = source === "pyth_lazer_rule" ? host.config.packages.pyth_lazer_rule?.feeds : hexFeeds;
60
- const unreadable = tickers.filter((ticker) => writeFeeds?.[ticker] !== undefined && !feedIdByTicker.has(ticker));
64
+ const unreadable = tickers.filter((ticker) => (0, record_ts_1.ownEntry)(writeFeeds, ticker) !== undefined && !feedIdByTicker.has(ticker));
61
65
  return { plane: "hermes", feedIdByTicker, unreadable };
62
66
  }
63
67
  case "waterx_rule": {
64
68
  // Absent feeds block ⇒ serves nothing (see the OracleReadPlan doc) —
65
- // never claim tickers the config doesn't name.
69
+ // never claim tickers the config doesn't name. `ownEntry` (own-keys-
70
+ // only, never the `in` operator or a bare bracket read) so a
71
+ // prototype-key ticker can't count as feeds-listed and poison the
72
+ // quote-center batch (which 404s whole batches on unknown symbols).
66
73
  const feeds = host.config.packages.waterx_rule?.feeds;
67
74
  return {
68
75
  plane: "quote_center",
69
- tickers: feeds ? tickers.filter((ticker) => ticker in feeds) : [],
76
+ tickers: tickers.filter((ticker) => (0, record_ts_1.ownEntry)(feeds, ticker) !== undefined),
70
77
  unreadable: [],
71
78
  };
72
79
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.PythCoreRule = void 0;
12
+ const record_ts_1 = require("../../utils/record.js");
12
13
  const price_update_rule_ts_1 = require("../price-update-rule.js");
13
14
  const pyth_ts_1 = require("../pyth.js");
14
15
  /**
@@ -79,7 +80,7 @@ exports.PythCoreRule = {
79
80
  for (const ticker of tickers) {
80
81
  // Same lookup as `host.getPythFeed(ticker)` minus its throw — an
81
82
  // unlisted ticker is a miss here, not an error.
82
- const feedId = host.config.packages.pyth_rule?.feeds?.[ticker]?.feed_id;
83
+ const feedId = (0, record_ts_1.ownEntry)(host.config.packages.pyth_rule?.feeds, ticker)?.feed_id;
83
84
  if (feedId === undefined || !packedFeedIds.has(feedId))
84
85
  return null;
85
86
  feedIds.push(feedId);
@@ -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. */
@@ -5,13 +5,15 @@
5
5
  * appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
6
6
  * requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
7
7
  * via the `pythApiKey` create option), verifies it ONCE on-chain via
8
- * `pyth_lazer::parse_and_verify_le_ecdsa_update`, and hands the resulting
9
- * `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
8
+ * `pyth_lazer`'s verify entry for that network (see `LAZER_INFRA`), and hands the
9
+ * resulting `Update` PTB value back through a `RuleUpdateHandle` for the feed
10
+ * calls.
10
11
  */
11
12
  Object.defineProperty(exports, "__esModule", { value: true });
12
13
  exports.PythLazerRule = exports.LazerApiKeyMissingError = exports.LAZER_INFRA = void 0;
13
14
  exports.feedLazerRule = feedLazerRule;
14
15
  const bcs_1 = require("@mysten/bcs");
16
+ const record_ts_1 = require("../../utils/record.js");
15
17
  const price_update_rule_ts_1 = require("../price-update-rule.js");
16
18
  const update_fetch_ts_1 = require("../update-fetch.js");
17
19
  /**
@@ -24,20 +26,33 @@ const update_fetch_ts_1 = require("../update-fetch.js");
24
26
  * `POST /v1/latest_price` (Bearer-authenticated). The service is
25
27
  * network-agnostic (one signed payload verifies on any chain that trusts the
26
28
  * Lazer signers), so both networks share the production host.
27
- * - `verifier_package` — the Sui package carrying
28
- * `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
29
- * still the original v1 publish; mainnet is the v2-upgraded package (which
30
- * still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
31
- * contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
29
+ * - `verifier_package` / `verify_entry` — the Sui package carrying the verify
30
+ * call, and which entry to call. These track what `pyth_lazer_rule` binds on
31
+ * that network, so they move together:
32
+ * - **mainnet** the v2 package `0xefbfd064…` and the **v2** entry. The rule
33
+ * was republished v2-bound after a 2026-08-05 mainnet probe: the ORIGINAL
34
+ * package `0x7b502c…` now aborts `EDifferentVersion` (`state::current_cap`)
35
+ * for any payload — the shared `State` has been migrated past that code —
36
+ * and its v1 entry aborts `EInvalidChannel` on `fixed_rate@1000ms`, the only
37
+ * channel WaterX's Pyth Pro grant permits.
38
+ * - **testnet** — still the original v1 publish, which has no `update_v2`
39
+ * module at all, so the v1 entry is the only one that exists there.
40
+ * Both entries take `(state, clock, bytes)` and accept the same `leEcdsa`
41
+ * payload. Values mirror the contract repo's `pyth_lazer_rule/Move.toml`
42
+ * published-at pins.
32
43
  */
33
44
  exports.LAZER_INFRA = {
34
45
  MAINNET: {
35
46
  endpoint: "https://pyth-lazer.dourolabs.app",
36
47
  verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
48
+ verify_entry: "parse_and_verify_le_ecdsa_update_v2",
49
+ channel: "fixed_rate@1000ms",
37
50
  },
38
51
  TESTNET: {
39
52
  endpoint: "https://pyth-lazer.dourolabs.app",
40
53
  verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
54
+ verify_entry: "parse_and_verify_le_ecdsa_update",
55
+ channel: "fixed_rate@200ms",
41
56
  },
42
57
  };
43
58
  /**
@@ -55,9 +70,15 @@ exports.LAZER_INFRA = {
55
70
  * and every xStock — are `min_channel: fixed_rate@200ms` (Lazer symbol
56
71
  * registry, verified 2026-07-22: the same 29-feed batch 400s at
57
72
  * `real_time`/`50ms` and serves 200 with the leEcdsa blob at `200ms`).
58
- * 200ms is the fastest channel every configured feed supports, and the
59
- * deployed rule accepts it: the v1 on-chain `channel::from_u8` aborts only
60
- * on the 1000ms fixed-rate channel (real_time / 50ms / 200ms are safe).
73
+ * The channel is therefore per-network (`LAZER_INFRA[network].channel`), and
74
+ * it is bounded from BOTH sides — by what the feeds publish and by what the
75
+ * grant allows:
76
+ * - **mainnet: `fixed_rate@1000ms`.** WaterX's Pyth Pro grant no longer
77
+ * permits anything faster ("Channel fixed_rate@200ms violates rate limit.
78
+ * Minimum allowed channel is 1000ms", measured 2026-08-05), and the
79
+ * mainnet rule is v2-bound, so it accepts that channel.
80
+ * - **testnet: `fixed_rate@200ms`.** Its rule is still v1-bound, and the v1
81
+ * on-chain `channel::from_u8` aborts on the 1000ms channel.
61
82
  * - `formats: leEcdsa` + `jsonBinaryEncoding: hex` — the Sui verifier takes
62
83
  * the `leEcdsa` framing; hex matches `fromHex` below.
63
84
  */
@@ -65,7 +86,6 @@ const LAZER_LATEST_PRICE_REQUEST = {
65
86
  properties: ["price", "exponent", "confidence"],
66
87
  formats: ["leEcdsa"],
67
88
  jsonBinaryEncoding: "hex",
68
- channel: "fixed_rate@200ms",
69
89
  };
70
90
  /**
71
91
  * Shape check ONLY — the `kind` discriminant is checked separately by the
@@ -108,7 +128,7 @@ function requireLazerPackage(host) {
108
128
  * retry/timeout/Bearer policy as `fetchPriceFeedsUpdateData`, unified so
109
129
  * both oracle sources fail the same way under upstream degradation.
110
130
  */
111
- async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
131
+ async function fetchLazerSignedUpdate(endpoint, channel, apiKey, feedIds, fetchOpts) {
112
132
  // joinEndpointPath preserves any base path on the endpoint — the same
113
133
  // leading-slash `new URL` footgun that 404'd every feed on the Pyth Pro
114
134
  // Hermes endpoint (see update-fetch.ts). Defensive here: the default
@@ -119,7 +139,7 @@ async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
119
139
  res = await (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), {
120
140
  method: "POST",
121
141
  headers: { "Content-Type": "application/json" },
122
- body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST }),
142
+ body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST, channel }),
123
143
  }, { apiKey, ...fetchOpts });
124
144
  }
125
145
  catch (err) {
@@ -167,7 +187,9 @@ exports.PythLazerRule = {
167
187
  // not fail per ticker as if only that feed were missing.
168
188
  const { feeds } = requireLazerPackage(host);
169
189
  const feedIds = tickers.map((ticker) => {
170
- const feedId = feeds[ticker];
190
+ // ownEntry: a prototype-key ticker ("toString") must throw here as
191
+ // unlisted, not turn an inherited Function into a "feed id".
192
+ const feedId = (0, record_ts_1.ownEntry)(feeds, ticker);
171
193
  if (feedId === undefined) {
172
194
  throw new Error(`No pyth_lazer_rule feed listed for ticker: ${ticker}`);
173
195
  }
@@ -177,13 +199,13 @@ exports.PythLazerRule = {
177
199
  if (!apiKey) {
178
200
  throw new LazerApiKeyMissingError();
179
201
  }
180
- const update = await fetchLazerSignedUpdate(exports.LAZER_INFRA[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
202
+ const update = await fetchLazerSignedUpdate(exports.LAZER_INFRA[host.network].endpoint, exports.LAZER_INFRA[host.network].channel, apiKey, feedIds, host.pyth.fetch);
181
203
  return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
182
204
  },
183
205
  /**
184
206
  * A Lazer payload is ONE signed `leEcdsa` message covering every feed it was
185
207
  * fetched for — verification is a single flat signature check over the whole
186
- * message (`parse_and_verify_le_ecdsa_update`, no per-feed cost), so the
208
+ * message (one `parse_and_verify_le_ecdsa_update*` call, no per-feed cost), so the
187
209
  * payload is indivisible: it can only be served whole. Returns the whole
188
210
  * payload iff every requested ticker's integer feed id is packed in THIS
189
211
  * payload's `feedIds`; any coverage gap (unlisted ticker, or a feed this
@@ -195,26 +217,31 @@ exports.PythLazerRule = {
195
217
  return null;
196
218
  const packedFeedIds = new Set(payload.feedIds);
197
219
  for (const ticker of tickers) {
198
- const feedId = host.config.packages.pyth_lazer_rule?.feeds?.[ticker];
220
+ const feedId = (0, record_ts_1.ownEntry)(host.config.packages.pyth_lazer_rule?.feeds, ticker);
199
221
  if (feedId === undefined || !packedFeedIds.has(feedId))
200
222
  return null;
201
223
  }
202
224
  return { kind: "pyth_lazer_rule", payload };
203
225
  },
204
226
  /**
205
- * Appends the single `parse_and_verify_le_ecdsa_update(state, clock, bytes)`
227
+ * Appends the single `parse_and_verify_le_ecdsa_update*(state, clock, bytes)`
206
228
  * call — one secp256k1 signature check covering every feed in the payload —
207
229
  * and returns its `Update` result as the handle the per-ticker feed leg
208
230
  * consumes. `opts.cache` / `opts.feeSource` are Pyth-Core-specific and
209
231
  * ignored (Lazer verification charges no update fee).
232
+ *
233
+ * The entry name comes from `LAZER_INFRA[network].verify_entry`: mainnet's
234
+ * rule binds `update_v2`, testnet's is still the v1 publish. Both take
235
+ * `(state, clock, bytes)` and accept the same `leEcdsa` payload.
210
236
  */
211
237
  buildUpdateCalls(tx, host, data, _opts) {
212
238
  const payload = (0, price_update_rule_ts_1.assertRuleUpdateData)(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
213
239
  if (!payload)
214
240
  return undefined;
215
241
  const lazer = requireLazerPackage(host);
242
+ const infra = exports.LAZER_INFRA[host.network];
216
243
  const [update] = tx.moveCall({
217
- target: `${exports.LAZER_INFRA[host.network].verifier_package}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
244
+ target: `${infra.verifier_package}::pyth_lazer::${infra.verify_entry}`,
218
245
  arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
219
246
  });
220
247
  return { kind: "pyth_lazer_rule", update };
@@ -31,6 +31,7 @@ exports.waterxEnvelopeOf = waterxEnvelopeOf;
31
31
  exports.feedWaterxRule = feedWaterxRule;
32
32
  const bcs_1 = require("@mysten/bcs");
33
33
  const waterx_rule_ts_1 = require("../../generated/waterx_rule/waterx_rule.js");
34
+ const record_ts_1 = require("../../utils/record.js");
34
35
  const price_update_rule_ts_1 = require("../price-update-rule.js");
35
36
  const update_fetch_ts_1 = require("../update-fetch.js");
36
37
  /** The single signing intent the quote-center emits — exported so read-plane
@@ -267,7 +268,9 @@ exports.WaterxRule = {
267
268
  // not fail per ticker as if only that feed were missing.
268
269
  const { feeds } = requireWaterxPackage(host);
269
270
  for (const ticker of tickers) {
270
- if (feeds[ticker] === undefined) {
271
+ // ownEntry: a prototype-key ticker ("toString") must throw here as
272
+ // unlisted, not pass as an inherited Function and reach the network.
273
+ if ((0, record_ts_1.ownEntry)(feeds, ticker) === undefined) {
271
274
  throw new Error(`No waterx_rule feed listed for ticker: ${ticker}`);
272
275
  }
273
276
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `source-list.ts` — THE parser for a consumer's `ORACLE_SOURCE` env string
3
+ * (comma list of `OracleSource` values → the fed set). The FE and BE
4
+ * previously carried twin hand-written parsers whose semantics drifted once
5
+ * in review (a trailing comma booted one deployment green and 500'd the
6
+ * other); this canonical behavior is what both fold onto:
7
+ *
8
+ * - split on `,`, trim entries, DROP empties (trailing/doubled commas are
9
+ * the most common env typo, never a boot failure)
10
+ * - validate every entry against {@link ORACLE_SOURCES}
11
+ * - dedupe, order-preserving (list order is consumer read-plane policy —
12
+ * the SDK's own fed-set build treats the list as a set)
13
+ * - throw an operator-actionable error on empty/unset/invalid input —
14
+ * there is NO default oracle source
15
+ *
16
+ * The SDK still never reads `process.env` — callers pass the raw string.
17
+ *
18
+ * STRICTER than the consumers' previous `in`-operator checks: a value named
19
+ * like an `Object.prototype` key (`toString`, `constructor`, …) passed those
20
+ * and died deep in the stack; `Set.has` rejects it here at parse.
21
+ *
22
+ * Zod adopters: this THROWS a plain Error. Inside a zod `.transform()` a
23
+ * throw escapes `schema.parse()` un-aggregated and masks sibling issues —
24
+ * wrap it: `try { return parseOracleSourceList(raw); } catch (e) {
25
+ * ctx.addIssue({ code: "custom", message: (e as Error).message }); return
26
+ * z.NEVER; }`.
27
+ */
28
+ import { type OracleSource } from "./price-update-rule.ts";
29
+ /**
30
+ * THE runtime membership check for {@link ORACLE_SOURCES} — the parser below
31
+ * and `PerpClient`'s ctor validation both use this one predicate, so the env
32
+ * parser and the create-option front door can never disagree. `Set.has`,
33
+ * never `in`/bracket reads (prototype-chain safe by construction).
34
+ */
35
+ export declare function isOracleSource(value: string): value is OracleSource;
36
+ export declare function parseOracleSourceList(raw: string | null | undefined): OracleSource[];