@waterx/sdk 4.3.0 → 4.3.1

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 (42) hide show
  1. package/README.md +9 -4
  2. package/dist/cjs/src/oracle/aggregate.js +3 -2
  3. package/dist/cjs/src/oracle/index.d.ts +3 -1
  4. package/dist/cjs/src/oracle/index.js +11 -1
  5. package/dist/cjs/src/oracle/price-update-rule.d.ts +21 -7
  6. package/dist/cjs/src/oracle/price-update-rule.js +20 -0
  7. package/dist/cjs/src/oracle/read-plane.js +11 -4
  8. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +2 -1
  9. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +5 -2
  10. package/dist/cjs/src/oracle/rules/waterx-rule.js +4 -1
  11. package/dist/cjs/src/oracle/source-list.d.ts +36 -0
  12. package/dist/cjs/src/oracle/source-list.js +57 -0
  13. package/dist/cjs/src/perp/client.d.ts +1 -1
  14. package/dist/cjs/src/perp/client.js +11 -7
  15. package/dist/cjs/src/perp/config-view.js +7 -6
  16. package/dist/cjs/src/perp/index.d.ts +1 -1
  17. package/dist/cjs/src/perp/index.js +8 -4
  18. package/dist/cjs/src/perp/user/staking.js +2 -1
  19. package/dist/cjs/src/utils/config.js +2 -1
  20. package/dist/cjs/src/utils/record.d.ts +12 -0
  21. package/dist/cjs/src/utils/record.js +22 -0
  22. package/dist/src/oracle/aggregate.js +3 -2
  23. package/dist/src/oracle/index.d.ts +3 -1
  24. package/dist/src/oracle/index.js +8 -1
  25. package/dist/src/oracle/price-update-rule.d.ts +21 -7
  26. package/dist/src/oracle/price-update-rule.js +19 -0
  27. package/dist/src/oracle/read-plane.js +11 -4
  28. package/dist/src/oracle/rules/pyth-core-rule.js +2 -1
  29. package/dist/src/oracle/rules/pyth-lazer-rule.js +5 -2
  30. package/dist/src/oracle/rules/waterx-rule.js +4 -1
  31. package/dist/src/oracle/source-list.d.ts +36 -0
  32. package/dist/src/oracle/source-list.js +53 -0
  33. package/dist/src/perp/client.d.ts +1 -1
  34. package/dist/src/perp/client.js +11 -7
  35. package/dist/src/perp/config-view.js +7 -6
  36. package/dist/src/perp/index.d.ts +1 -1
  37. package/dist/src/perp/index.js +1 -1
  38. package/dist/src/perp/user/staking.js +2 -1
  39. package/dist/src/utils/config.js +2 -1
  40. package/dist/src/utils/record.d.ts +12 -0
  41. package/dist/src/utils/record.js +19 -0
  42. 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.
@@ -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");
@@ -169,7 +170,7 @@ function aggregateTickerWithPyth(tx, host, args) {
169
170
  * {@link refreshOraclePrices}), which feeds both.
170
171
  */
171
172
  function aggregateTickerWithConstant(tx, host, args) {
172
- if (host.config.packages.pyth_rule?.feeds?.[args.ticker] !== undefined) {
173
+ if ((0, record_ts_1.ownEntry)(host.config.packages.pyth_rule?.feeds, args.ticker) !== undefined) {
173
174
  throw new Error(`'${args.ticker}' is in pyth_rule.feeds (dual-feed) — feed both via aggregateTicker({ priceInfoObjectId }) / refreshOraclePrices, not aggregateTickerWithConstant`);
174
175
  }
175
176
  aggregateTicker(tx, host, { ticker: args.ticker });
@@ -230,7 +231,7 @@ async function refreshOraclePrices(tx, host, tickers, opts = {}) {
230
231
  // price_info_object lookup for every ticker with a pyth_rule.feeds entry —
231
232
  // needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
232
233
  // which rule performed the on-chain update for that ticker.
233
- const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
234
+ const pythTickers = tickers.filter((t) => (0, record_ts_1.ownEntry)(host.config.packages.pyth_rule?.feeds, t) !== undefined);
234
235
  const priceInfoByTicker = new Map();
235
236
  pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
236
237
  // 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 —
@@ -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);
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.PythLazerRule = exports.LazerApiKeyMissingError = exports.LAZER_INFRA = void 0;
13
13
  exports.feedLazerRule = feedLazerRule;
14
14
  const bcs_1 = require("@mysten/bcs");
15
+ const record_ts_1 = require("../../utils/record.js");
15
16
  const price_update_rule_ts_1 = require("../price-update-rule.js");
16
17
  const update_fetch_ts_1 = require("../update-fetch.js");
17
18
  /**
@@ -167,7 +168,9 @@ exports.PythLazerRule = {
167
168
  // not fail per ticker as if only that feed were missing.
168
169
  const { feeds } = requireLazerPackage(host);
169
170
  const feedIds = tickers.map((ticker) => {
170
- const feedId = feeds[ticker];
171
+ // ownEntry: a prototype-key ticker ("toString") must throw here as
172
+ // unlisted, not turn an inherited Function into a "feed id".
173
+ const feedId = (0, record_ts_1.ownEntry)(feeds, ticker);
171
174
  if (feedId === undefined) {
172
175
  throw new Error(`No pyth_lazer_rule feed listed for ticker: ${ticker}`);
173
176
  }
@@ -195,7 +198,7 @@ exports.PythLazerRule = {
195
198
  return null;
196
199
  const packedFeedIds = new Set(payload.feedIds);
197
200
  for (const ticker of tickers) {
198
- const feedId = host.config.packages.pyth_lazer_rule?.feeds?.[ticker];
201
+ const feedId = (0, record_ts_1.ownEntry)(host.config.packages.pyth_lazer_rule?.feeds, ticker);
199
202
  if (feedId === undefined || !packedFeedIds.has(feedId))
200
203
  return null;
201
204
  }
@@ -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[];
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * `source-list.ts` — THE parser for a consumer's `ORACLE_SOURCE` env string
4
+ * (comma list of `OracleSource` values → the fed set). The FE and BE
5
+ * previously carried twin hand-written parsers whose semantics drifted once
6
+ * in review (a trailing comma booted one deployment green and 500'd the
7
+ * other); this canonical behavior is what both fold onto:
8
+ *
9
+ * - split on `,`, trim entries, DROP empties (trailing/doubled commas are
10
+ * the most common env typo, never a boot failure)
11
+ * - validate every entry against {@link ORACLE_SOURCES}
12
+ * - dedupe, order-preserving (list order is consumer read-plane policy —
13
+ * the SDK's own fed-set build treats the list as a set)
14
+ * - throw an operator-actionable error on empty/unset/invalid input —
15
+ * there is NO default oracle source
16
+ *
17
+ * The SDK still never reads `process.env` — callers pass the raw string.
18
+ *
19
+ * STRICTER than the consumers' previous `in`-operator checks: a value named
20
+ * like an `Object.prototype` key (`toString`, `constructor`, …) passed those
21
+ * and died deep in the stack; `Set.has` rejects it here at parse.
22
+ *
23
+ * Zod adopters: this THROWS a plain Error. Inside a zod `.transform()` a
24
+ * throw escapes `schema.parse()` un-aggregated and masks sibling issues —
25
+ * wrap it: `try { return parseOracleSourceList(raw); } catch (e) {
26
+ * ctx.addIssue({ code: "custom", message: (e as Error).message }); return
27
+ * z.NEVER; }`.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.isOracleSource = isOracleSource;
31
+ exports.parseOracleSourceList = parseOracleSourceList;
32
+ const price_update_rule_ts_1 = require("./price-update-rule.js");
33
+ // Widened-annotation Set (not an assertion) so the type predicate below
34
+ // narrows by CONSTRUCTION rather than by cast.
35
+ const ORACLE_SOURCE_SET = new Set(price_update_rule_ts_1.ORACLE_SOURCES);
36
+ /**
37
+ * THE runtime membership check for {@link ORACLE_SOURCES} — the parser below
38
+ * and `PerpClient`'s ctor validation both use this one predicate, so the env
39
+ * parser and the create-option front door can never disagree. `Set.has`,
40
+ * never `in`/bracket reads (prototype-chain safe by construction).
41
+ */
42
+ function isOracleSource(value) {
43
+ return ORACLE_SOURCE_SET.has(value);
44
+ }
45
+ function parseOracleSourceList(raw) {
46
+ const parts = (raw ?? "")
47
+ .split(",")
48
+ .map((part) => part.trim())
49
+ .filter((part) => part !== "");
50
+ const sources = parts.filter(isOracleSource);
51
+ if (parts.length === 0 || sources.length !== parts.length) {
52
+ const got = raw == null || raw.trim() === "" ? "unset" : `'${raw}'`;
53
+ throw new Error(`ORACLE_SOURCE must be a comma-separated list of ${price_update_rule_ts_1.ORACLE_SOURCES.join(" | ")} ` +
54
+ `(got ${got}) — there is NO default oracle source; set it in the deployment's env.`);
55
+ }
56
+ return [...new Set(sources)];
57
+ }
@@ -11,7 +11,7 @@
11
11
  * {@link PerpConfigView}. This class is just the wiring + factory between them.
12
12
  */
13
13
  import { BaseLineClient } from "../base-client.ts";
14
- import type { OracleSource } from "../oracle/price-update-rule.ts";
14
+ import { type OracleSource } from "../oracle/price-update-rule.ts";
15
15
  import type { FetchPolicy } from "../oracle/update-fetch.ts";
16
16
  import { type LoadConfigOptions, type PythAccessConfig, type PythFetchPolicy, type WaterxAccessConfig, type WaterXConfig, type WormholeInfraConfig } from "./config.ts";
17
17
  import type { Network } from "./constants.ts";
@@ -14,6 +14,8 @@
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.PerpClient = void 0;
16
16
  const base_client_ts_1 = require("../base-client.js");
17
+ const price_update_rule_ts_1 = require("../oracle/price-update-rule.js");
18
+ const source_list_ts_1 = require("../oracle/source-list.js");
17
19
  const config_view_ts_1 = require("./config-view.js");
18
20
  const config_ts_1 = require("./config.js");
19
21
  class PerpClient extends base_client_ts_1.BaseLineClient {
@@ -49,16 +51,18 @@ class PerpClient extends base_client_ts_1.BaseLineClient {
49
51
  ...(opts.waterxEndpoint !== undefined ? { endpoint: opts.waterxEndpoint } : {}),
50
52
  ...(opts.waterxFetch !== undefined ? { fetch: opts.waterxFetch } : {}),
51
53
  };
52
- // Normalize single-or-list to a deduped, order-preserving list. An empty
53
- // listor a nullish/empty entry, the shape an untyped caller produces
54
- // by omitting the REQUIRED option is a caller bug, not "no oracle":
55
- // fail construction loudly instead of booting green and surfacing as
56
- // `OracleSourceNotImplemented: undefined` at the first tx-build.
54
+ // Normalize single-or-list to a deduped, order-preserving list, gated by
55
+ // `isOracleSource` — the SAME predicate `parseOracleSourceList` uses. An
56
+ // empty list, a nullish entry (an untyped caller omitting the REQUIRED
57
+ // option), or an unregistered value (`'core'`, `'pyth'`, …) fails
58
+ // construction loudly instead of booting green and surfacing as
59
+ // `OracleSourceNotImplemented` at the first tx-build.
57
60
  const sources = Array.isArray(opts.oracleSource) ? opts.oracleSource : [opts.oracleSource];
58
61
  this.oracleSources = [...new Set(sources)];
59
62
  if (this.oracleSources.length === 0 ||
60
- this.oracleSources.some((source) => typeof source !== "string" || source.length === 0)) {
61
- throw new Error(`oracleSource is REQUIRED and must name at least one source (got ${JSON.stringify(sources)})`);
63
+ this.oracleSources.some((source) => !(0, source_list_ts_1.isOracleSource)(source))) {
64
+ throw new Error(`oracleSource is REQUIRED and must name at least one of ${price_update_rule_ts_1.ORACLE_SOURCES.join(" | ")} ` +
65
+ `(got ${JSON.stringify(sources)})`);
62
66
  }
63
67
  this.view = new config_view_ts_1.PerpConfigView(() => this.config, () => this.wormhole);
64
68
  }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.PerpConfigView = void 0;
14
+ const record_ts_1 = require("../utils/record.js");
14
15
  class PerpConfigView {
15
16
  getConfig;
16
17
  getWormhole;
@@ -29,21 +30,21 @@ class PerpConfigView {
29
30
  }
30
31
  /** `waterx_perp.markets[ticker]`, throws if unknown. */
31
32
  getMarket(ticker) {
32
- const m = this.config.packages.waterx_perp?.markets?.[ticker];
33
+ const m = (0, record_ts_1.ownEntry)(this.config.packages.waterx_perp?.markets, ticker);
33
34
  if (!m)
34
35
  throw new Error(`Unknown market ticker: ${ticker}`);
35
36
  return m;
36
37
  }
37
38
  /** `waterx_oracle.aggregators[ticker]`, throws if unknown. */
38
39
  getAggregator(ticker) {
39
- const a = this.config.packages.waterx_oracle?.aggregators?.[ticker];
40
+ const a = (0, record_ts_1.ownEntry)(this.config.packages.waterx_oracle?.aggregators, ticker);
40
41
  if (!a)
41
42
  throw new Error(`No aggregator listed for ticker: ${ticker}`);
42
43
  return a;
43
44
  }
44
45
  /** `pyth_rule.feeds[ticker]`, throws if unknown. */
45
46
  getPythFeed(ticker) {
46
- const f = this.config.packages.pyth_rule?.feeds?.[ticker];
47
+ const f = (0, record_ts_1.ownEntry)(this.config.packages.pyth_rule?.feeds, ticker);
47
48
  if (!f)
48
49
  throw new Error(`No pyth feed listed for ticker: ${ticker}`);
49
50
  return f;
@@ -69,7 +70,7 @@ class PerpConfigView {
69
70
  const c = this.config.packages.constant_rule;
70
71
  if (!c?.published_at || !c.config)
71
72
  return false;
72
- return c.feeds?.[ticker] !== undefined;
73
+ return (0, record_ts_1.ownEntry)(c.feeds, ticker) !== undefined;
73
74
  }
74
75
  /**
75
76
  * The `supra_rule` config when it is deployed, enabled, and fully wired
@@ -95,7 +96,7 @@ class PerpConfigView {
95
96
  */
96
97
  getPoolTokenType(tickerOrName) {
97
98
  const poolTokens = this.config.packages.wlp?.pool_tokens ?? {};
98
- const exact = poolTokens[tickerOrName];
99
+ const exact = (0, record_ts_1.ownEntry)(poolTokens, tickerOrName);
99
100
  if (exact)
100
101
  return exact;
101
102
  for (const t of Object.values(poolTokens)) {
@@ -118,7 +119,7 @@ class PerpConfigView {
118
119
  * reward coin type, so callers don't need a separate alias-to-type lookup.
119
120
  */
120
121
  getRewarders(stakeAlias) {
121
- const map = this.config.packages.waterx_staking?.rewarders?.[stakeAlias];
122
+ const map = (0, record_ts_1.ownEntry)(this.config.packages.waterx_staking?.rewarders, stakeAlias);
122
123
  if (!map)
123
124
  return [];
124
125
  return Object.entries(map).map(([alias, entry]) => ({ alias, ...entry }));
@@ -13,7 +13,7 @@ export type { EstLiqPriceViewOpts } from "./liq-view.ts";
13
13
  export * from "./user/index.ts";
14
14
  export * from "./tx-builders.ts";
15
15
  export * from "./fetch.ts";
16
- export { FetchPolicyError, LazerApiKeyMissingError, OracleFeeSourceUnavailableError, OracleSourceNotImplementedError, PythCache, aggregateTicker, aggregateTickerWithConstant, aggregateTickerWithPyth, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, pythCoreHermesEndpoint, pythProHermesEndpoint, resolveHermesReadEndpoint, waterxQuoteCenterEndpoint, refreshOraclePrices, updatePythPrices, } from "../oracle/index.ts";
16
+ export { FetchPolicyError, LazerApiKeyMissingError, OracleFeeSourceUnavailableError, OracleSourceNotImplementedError, PythCache, aggregateTicker, aggregateTickerWithConstant, aggregateTickerWithPyth, ORACLE_SOURCES, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, isOracleSource, parseOracleSourceList, pythCoreHermesEndpoint, pythProHermesEndpoint, refreshOraclePrices, resolveHermesReadEndpoint, updatePythPrices, waterxEnvelopeOf, waterxQuoteCenterEndpoint, } from "../oracle/index.ts";
17
17
  export type { FetchPolicy, OracleFeeSource, OracleSource, UpdateDataProvider, } from "../oracle/index.ts";
18
18
  export { fetchDepositVaa, fetchVaa, listBridgeWithdrawalVaas, listVaasByEmitter, padEvmEmitter, toWormholescanEmitter, vaaBase64ToBytes, vaaBase64ToHex, vaaBytesToBase64, waitForVaa, } from "../account/funding/wormhole.ts";
19
19
  export type { VaaListItem, VaaResponse, WormholescanOptions } from "../account/funding/wormhole.ts";
@@ -37,8 +37,8 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.getCollateralAssets = exports.getMarketTickers = exports.TOKEN_DECIMALS = exports.COLLATERAL_DECIMALS = exports.WLP_DECIMALS = exports.SUI_DECIMALS = exports.MS_PER_YEAR = exports.MS_PER_HOUR = exports.MS_PER_MINUTE = exports.STAKING_PERM_ALL = exports.STAKING_PERM_CLAIM_REWARD = exports.STAKING_PERM_REDEEM_STAKE = exports.STAKING_PERM_DEPOSIT_STAKE = exports.PERM_WITHDRAW_COLLATERAL = exports.PERM_REDEEM_WLP = exports.PERM_PLACE_ORDER = exports.PERM_OPEN_POSITION = exports.PERM_MINT_WLP = exports.PERM_INCREASE_POSITION = exports.PERM_DEPOSIT_COLLATERAL = exports.PERM_DECREASE_POSITION = exports.PERM_CLOSE_POSITION = exports.PERM_CANCEL_ORDER = exports.PERM_ALL_TRADING = exports.PERM_ALL = exports.ORDER_TAG_WILDCARD = exports.ORDER_STOP_SELL = exports.ORDER_STOP_BUY = exports.ORDER_LIMIT_SELL = exports.ORDER_LIMIT_BUY = exports.FLOAT_SCALE = exports.DRY_RUN_SENDER = exports.DOUBLE_SCALE = exports.BPS_SCALE = exports.ACTION_WITHDRAW_COLLATERAL = exports.ACTION_UPDATE_ORDER = exports.ACTION_PLACE_ORDER = exports.ACTION_OPEN_POSITION = exports.ACTION_LIQUIDATE = exports.ACTION_INCREASE_POSITION = exports.ACTION_DEPOSIT_COLLATERAL = exports.ACTION_DECREASE_POSITION = exports.ACTION_CLOSE_POSITION = exports.ACTION_CANCEL_PRE_ORDER = exports.ACTION_CANCEL_ORDER = exports.ACTION_ADD_PRE_ORDER = exports.loadConfig = exports.clearConfigCache = exports.WORMHOLE_DEFAULTS = exports.PerpClient = void 0;
40
- exports.padEvmEmitter = exports.listVaasByEmitter = exports.listBridgeWithdrawalVaas = exports.fetchVaa = exports.fetchDepositVaa = exports.updatePythPrices = exports.refreshOraclePrices = exports.waterxQuoteCenterEndpoint = exports.resolveHermesReadEndpoint = exports.pythProHermesEndpoint = exports.pythCoreHermesEndpoint = exports.fetchPriceFeedsUpdateData = exports.buildPythPriceUpdateCalls = exports.aggregateTickerWithPyth = exports.aggregateTickerWithConstant = exports.aggregateTicker = exports.PythCache = exports.OracleSourceNotImplementedError = exports.OracleFeeSourceUnavailableError = exports.LazerApiKeyMissingError = exports.FetchPolicyError = exports.calcEstLiqPriceRawFromView = exports.formatFundingInterval = exports.rawPrice = exports.decodeFundingIndexDelta = exports.calcWlpRedeemOut = exports.calcWlpPrice = exports.calcWlpMintOut = exports.calcWlpIncentiveApy = exports.calcViewEstLiqFeesUsd = exports.calcUnrealizedPnl = exports.calcTotalTradingFeeRate = exports.calcTokenUtilizationBps = exports.calcRealLiqNetCostUsd = exports.calcPositionBorrowFee = exports.calcNotional = exports.calcMaxReducibleCollateralUsd = exports.calcLeverage = exports.calcImpactFeeRate = exports.calcFundingRate = exports.calcFundingFeeUsd = exports.calcFee = exports.calcEstLiqPriceRaw = exports.calcEstLiqPrice = exports.calcEffectiveCollateralUsd = exports.calcDynamicFeeBps = exports.calcBorrowRateAccrual = exports.calcBorrowRate = exports.annualizeFundingRate = exports.annualizedApyFromRatio = void 0;
41
- exports.withdrawalQueueCalls = exports.nativeCustodyCalls = exports.referralCalls = exports.pythSponsorRuleCalls = exports.pythRuleCalls = exports.oracleCalls = exports.stakingCalls = exports.wxaAccountCalls = exports.viewCalls = exports.lpPoolCalls = exports.tradingCalls = exports.MarketConfigBcs = exports.MarketBcs = exports.OrderBcs = exports.PositionBcs = exports.TokenPoolDataBcs = exports.RedeemRequestDataBcs = exports.PositionDataBcs = exports.PoolDataBcs = exports.OrderDataBcs = exports.MarketDataBcs = exports.GlobalConfigDataBcs = exports.AccountDataBcs = exports.waitForVaa = exports.vaaBytesToBase64 = exports.vaaBase64ToHex = exports.vaaBase64ToBytes = exports.toWormholescanEmitter = void 0;
40
+ exports.fetchDepositVaa = exports.waterxQuoteCenterEndpoint = exports.waterxEnvelopeOf = exports.updatePythPrices = exports.resolveHermesReadEndpoint = exports.refreshOraclePrices = exports.pythProHermesEndpoint = exports.pythCoreHermesEndpoint = exports.parseOracleSourceList = exports.isOracleSource = exports.fetchPriceFeedsUpdateData = exports.buildPythPriceUpdateCalls = exports.ORACLE_SOURCES = exports.aggregateTickerWithPyth = exports.aggregateTickerWithConstant = exports.aggregateTicker = exports.PythCache = exports.OracleSourceNotImplementedError = exports.OracleFeeSourceUnavailableError = exports.LazerApiKeyMissingError = exports.FetchPolicyError = exports.calcEstLiqPriceRawFromView = exports.formatFundingInterval = exports.rawPrice = exports.decodeFundingIndexDelta = exports.calcWlpRedeemOut = exports.calcWlpPrice = exports.calcWlpMintOut = exports.calcWlpIncentiveApy = exports.calcViewEstLiqFeesUsd = exports.calcUnrealizedPnl = exports.calcTotalTradingFeeRate = exports.calcTokenUtilizationBps = exports.calcRealLiqNetCostUsd = exports.calcPositionBorrowFee = exports.calcNotional = exports.calcMaxReducibleCollateralUsd = exports.calcLeverage = exports.calcImpactFeeRate = exports.calcFundingRate = exports.calcFundingFeeUsd = exports.calcFee = exports.calcEstLiqPriceRaw = exports.calcEstLiqPrice = exports.calcEffectiveCollateralUsd = exports.calcDynamicFeeBps = exports.calcBorrowRateAccrual = exports.calcBorrowRate = exports.annualizeFundingRate = exports.annualizedApyFromRatio = void 0;
41
+ exports.withdrawalQueueCalls = exports.nativeCustodyCalls = exports.referralCalls = exports.pythSponsorRuleCalls = exports.pythRuleCalls = exports.oracleCalls = exports.stakingCalls = exports.wxaAccountCalls = exports.viewCalls = exports.lpPoolCalls = exports.tradingCalls = exports.MarketConfigBcs = exports.MarketBcs = exports.OrderBcs = exports.PositionBcs = exports.TokenPoolDataBcs = exports.RedeemRequestDataBcs = exports.PositionDataBcs = exports.PoolDataBcs = exports.OrderDataBcs = exports.MarketDataBcs = exports.GlobalConfigDataBcs = exports.AccountDataBcs = exports.waitForVaa = exports.vaaBytesToBase64 = exports.vaaBase64ToHex = exports.vaaBase64ToBytes = exports.toWormholescanEmitter = exports.padEvmEmitter = exports.listVaasByEmitter = exports.listBridgeWithdrawalVaas = exports.fetchVaa = void 0;
42
42
  // ======== Core ========
43
43
  var client_ts_1 = require("./client.js");
44
44
  Object.defineProperty(exports, "PerpClient", { enumerable: true, get: function () { return client_ts_1.PerpClient; } });
@@ -144,14 +144,18 @@ Object.defineProperty(exports, "PythCache", { enumerable: true, get: function ()
144
144
  Object.defineProperty(exports, "aggregateTicker", { enumerable: true, get: function () { return index_ts_1.aggregateTicker; } });
145
145
  Object.defineProperty(exports, "aggregateTickerWithConstant", { enumerable: true, get: function () { return index_ts_1.aggregateTickerWithConstant; } });
146
146
  Object.defineProperty(exports, "aggregateTickerWithPyth", { enumerable: true, get: function () { return index_ts_1.aggregateTickerWithPyth; } });
147
+ Object.defineProperty(exports, "ORACLE_SOURCES", { enumerable: true, get: function () { return index_ts_1.ORACLE_SOURCES; } });
147
148
  Object.defineProperty(exports, "buildPythPriceUpdateCalls", { enumerable: true, get: function () { return index_ts_1.buildPythPriceUpdateCalls; } });
148
149
  Object.defineProperty(exports, "fetchPriceFeedsUpdateData", { enumerable: true, get: function () { return index_ts_1.fetchPriceFeedsUpdateData; } });
150
+ Object.defineProperty(exports, "isOracleSource", { enumerable: true, get: function () { return index_ts_1.isOracleSource; } });
151
+ Object.defineProperty(exports, "parseOracleSourceList", { enumerable: true, get: function () { return index_ts_1.parseOracleSourceList; } });
149
152
  Object.defineProperty(exports, "pythCoreHermesEndpoint", { enumerable: true, get: function () { return index_ts_1.pythCoreHermesEndpoint; } });
150
153
  Object.defineProperty(exports, "pythProHermesEndpoint", { enumerable: true, get: function () { return index_ts_1.pythProHermesEndpoint; } });
151
- Object.defineProperty(exports, "resolveHermesReadEndpoint", { enumerable: true, get: function () { return index_ts_1.resolveHermesReadEndpoint; } });
152
- Object.defineProperty(exports, "waterxQuoteCenterEndpoint", { enumerable: true, get: function () { return index_ts_1.waterxQuoteCenterEndpoint; } });
153
154
  Object.defineProperty(exports, "refreshOraclePrices", { enumerable: true, get: function () { return index_ts_1.refreshOraclePrices; } });
155
+ Object.defineProperty(exports, "resolveHermesReadEndpoint", { enumerable: true, get: function () { return index_ts_1.resolveHermesReadEndpoint; } });
154
156
  Object.defineProperty(exports, "updatePythPrices", { enumerable: true, get: function () { return index_ts_1.updatePythPrices; } });
157
+ Object.defineProperty(exports, "waterxEnvelopeOf", { enumerable: true, get: function () { return index_ts_1.waterxEnvelopeOf; } });
158
+ Object.defineProperty(exports, "waterxQuoteCenterEndpoint", { enumerable: true, get: function () { return index_ts_1.waterxQuoteCenterEndpoint; } });
155
159
  // ======== Wormhole / Wormholescan utilities (credit bridge) ========
156
160
  var wormhole_ts_1 = require("../account/funding/wormhole.js");
157
161
  Object.defineProperty(exports, "fetchDepositVaa", { enumerable: true, get: function () { return wormhole_ts_1.fetchDepositVaa; } });
@@ -55,9 +55,10 @@ exports.unstake = unstake;
55
55
  exports.claimReward = claimReward;
56
56
  const account_request_ts_1 = require("../../account/account-request.js");
57
57
  const staking = __importStar(require("../../generated/waterx_staking/waterx_staking.js"));
58
+ const record_ts_1 = require("../../utils/record.js");
58
59
  const validate_ts_1 = require("../../utils/validate.js");
59
60
  function pool(client, stakeAlias) {
60
- const id = client.config.packages.waterx_staking?.pools?.[stakeAlias];
61
+ const id = (0, record_ts_1.ownEntry)(client.config.packages.waterx_staking?.pools, stakeAlias);
61
62
  if (!id) {
62
63
  throw new Error(`config.packages.waterx_staking.pools[${stakeAlias}] is not set — staking is not deployed for this stake type`);
63
64
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getMarketTickers = getMarketTickers;
4
4
  exports.getCollateralAssets = getCollateralAssets;
5
+ const record_ts_1 = require("./record.js");
5
6
  /** Returns all registered market tickers (e.g. "BTCUSD") from waterx-config. */
6
7
  function getMarketTickers(config) {
7
8
  return Object.keys(config.packages.waterx_perp.markets);
@@ -15,5 +16,5 @@ function getMarketTickers(config) {
15
16
  */
16
17
  function getCollateralAssets(config) {
17
18
  const feeds = config.packages.pyth_rule?.feeds ?? {};
18
- return Object.keys(config.packages.wlp.pool_tokens).filter((t) => feeds[t] !== undefined);
19
+ return Object.keys(config.packages.wlp.pool_tokens).filter((t) => (0, record_ts_1.ownEntry)(feeds, t) !== undefined);
19
20
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Own-key record lookup. Ticker-keyed config records (`feeds`, `markets`,
3
+ * `aggregators`) are indexed with caller-supplied strings; a bare bracket
4
+ * read — like the `in` operator — walks the prototype chain, so a ticker
5
+ * named like an `Object.prototype` key ("toString", "constructor", …) reads
6
+ * as an inherited Function instead of "absent" and leaks into batches sent
7
+ * to the network. Every such lookup funnels through here so the answer is
8
+ * own-keys-only, everywhere, instead of per-site `Object.hasOwn` guards
9
+ * that drift.
10
+ */
11
+ /** `record[key]` iff `key` is an OWN key — `undefined` for an absent record, an absent key, or a prototype-chain hit. */
12
+ export declare function ownEntry<T extends Record<string, unknown>>(record: T | undefined, key: string): T[string] | undefined;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * Own-key record lookup. Ticker-keyed config records (`feeds`, `markets`,
4
+ * `aggregators`) are indexed with caller-supplied strings; a bare bracket
5
+ * read — like the `in` operator — walks the prototype chain, so a ticker
6
+ * named like an `Object.prototype` key ("toString", "constructor", …) reads
7
+ * as an inherited Function instead of "absent" and leaks into batches sent
8
+ * to the network. Every such lookup funnels through here so the answer is
9
+ * own-keys-only, everywhere, instead of per-site `Object.hasOwn` guards
10
+ * that drift.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.ownEntry = ownEntry;
14
+ /** `record[key]` iff `key` is an OWN key — `undefined` for an absent record, an absent key, or a prototype-chain hit. */
15
+ function ownEntry(record, key) {
16
+ // The `T extends Record<...>` form (rather than `Record<string, V>`) keeps
17
+ // union-typed records inferable; TS then resolves `record[key]` only to the
18
+ // constraint's `unknown`, so restate the definitionally-true index type.
19
+ return record !== undefined && Object.hasOwn(record, key)
20
+ ? record[key]
21
+ : undefined;
22
+ }
@@ -23,6 +23,7 @@
23
23
  * `rule-registry.ts`.
24
24
  */
25
25
  import { aggregate as aggregateCall, newCollector } from "../generated/waterx_oracle/oracle.js";
26
+ import { ownEntry } from "../utils/record.js";
26
27
  import { OracleFeeSourceUnavailableError } from "./pyth.js";
27
28
  import { resolveOracleRule } from "./rule-registry.js";
28
29
  import { feedConstantRule } from "./rules/constant-rule.js";
@@ -163,7 +164,7 @@ export function aggregateTickerWithPyth(tx, host, args) {
163
164
  * {@link refreshOraclePrices}), which feeds both.
164
165
  */
165
166
  export function aggregateTickerWithConstant(tx, host, args) {
166
- if (host.config.packages.pyth_rule?.feeds?.[args.ticker] !== undefined) {
167
+ if (ownEntry(host.config.packages.pyth_rule?.feeds, args.ticker) !== undefined) {
167
168
  throw new Error(`'${args.ticker}' is in pyth_rule.feeds (dual-feed) — feed both via aggregateTicker({ priceInfoObjectId }) / refreshOraclePrices, not aggregateTickerWithConstant`);
168
169
  }
169
170
  aggregateTicker(tx, host, { ticker: args.ticker });
@@ -224,7 +225,7 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
224
225
  // price_info_object lookup for every ticker with a pyth_rule.feeds entry —
225
226
  // needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
226
227
  // which rule performed the on-chain update for that ticker.
227
- const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
228
+ const pythTickers = tickers.filter((t) => ownEntry(host.config.packages.pyth_rule?.feeds, t) !== undefined);
228
229
  const priceInfoByTicker = new Map();
229
230
  pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
230
231
  // 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";
@@ -40,6 +40,10 @@ export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMi
40
40
  // `resolveHermesReadEndpoint` (pyth_rule listed → Core, else override ??
41
41
  // Pro) — never a hand-rolled branch, never a cross-source fallback.
42
42
  pythCoreHermesEndpoint, pythProHermesEndpoint, PYTH_PRO_HERMES_ENDPOINT, updatePythPrices, HermesEndpointRejectedAllFeedsError, MISSING_FEED_MEMO_TTL_MS, OracleFeeSourceUnavailableError, } from "./pyth.js";
43
+ // Canonical OracleSource value list + THE env-string parser consumers fold
44
+ // onto — semantics and rationale in `source-list.ts`'s header.
45
+ export { ORACLE_SOURCES } from "./price-update-rule.js";
46
+ export { isOracleSource, parseOracleSourceList } from "./source-list.js";
43
47
  // Per-source READ-plane resolution — which tickers a source can price
44
48
  // off-chain and with which ids (`resolveOracleReadPlan`), and which
45
49
  // Hermes-compatible base the hermes plans execute against
@@ -58,7 +62,10 @@ export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.
58
62
  // read-plane accessor (mirrors `pythCoreHermesEndpoint`).
59
63
  // WaterX quote-center rule (first-party ed25519 signed batches; `feedWaterxRule`
60
64
  // stays internal to `aggregate.ts`).
61
- export { WaterxRule, parseSignedEnvelope, BATCH_PRICE_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint, } from "./rules/waterx-rule.js";
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";
62
69
  // `resolveOracleRule` is the ONE source→rule registry — exported so external
63
70
  // consumers (e.g. a BE prefetch cache that keys per source and needs each
64
71
  // 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 —
@@ -15,6 +15,25 @@
15
15
  * entry via `rule-registry.ts`, then drives fetch + `buildUpdateCalls`
16
16
  * through this port; `aggregate.ts` stays the sole orchestrator.
17
17
  */
18
+ /**
19
+ * The canonical list of selectable oracle sources — the SINGLE authority the
20
+ * {@link OracleSource} union derives from (the value-list→union derive
21
+ * idiom of `unified-client.ts`'s `NON_CLIENT_FIRST`, plus `Object.freeze`
22
+ * so the immutability is RUNTIME truth: `as const` alone would let a JS
23
+ * consumer push into the array and desync the membership Set built from it
24
+ * in `source-list.ts`). Runtime membership checks and the `ORACLE_SOURCE`
25
+ * env parser live there, on `isOracleSource` / `parseOracleSourceList`.
26
+ * Only sources belong here: `supra_rule` and `constant_rule` are auxiliary
27
+ * rules fed alongside whichever sources are selected (see
28
+ * `aggregateTicker`), not sources themselves — the `satisfies` keeps
29
+ * entries inside `PriceUpdateRuleKind` but adding an auxiliary rule to this
30
+ * list is an (incorrect) editorial decision this comment exists to prevent.
31
+ */
32
+ export const ORACLE_SOURCES = Object.freeze([
33
+ "pyth_rule",
34
+ "pyth_lazer_rule",
35
+ "waterx_rule",
36
+ ]);
18
37
  /**
19
38
  * Shared null → kind → shape guard ladder for a `PriceUpdateRule.buildUpdateCalls`
20
39
  * payload — every rule's `buildUpdateCalls` needs the exact same three checks,
@@ -10,6 +10,7 @@
10
10
  * (FE/BE price facades) resolve through this instead of hardcoding which
11
11
  * sources share which feeds namespace.
12
12
  */
13
+ import { ownEntry } from "../utils/record.js";
13
14
  import { pythCoreHermesEndpoint, pythProHermesEndpoint } from "./pyth.js";
14
15
  /**
15
16
  * Resolve `source`'s read plan for `tickers`. Pure config lookup — no
@@ -43,26 +44,32 @@ export function resolveOracleReadPlan(host, source, tickers) {
43
44
  switch (source) {
44
45
  case "pyth_rule":
45
46
  case "pyth_lazer_rule": {
47
+ // All ticker lookups go through `ownEntry` (own-keys-only): a ticker
48
+ // named like an Object.prototype key ("toString", "constructor", …)
49
+ // must read as not-listed, not as an inherited Function.
46
50
  const hexFeeds = host.config.packages.pyth_rule?.feeds;
47
51
  const feedIdByTicker = new Map();
48
52
  for (const ticker of tickers) {
49
- const feedId = hexFeeds?.[ticker]?.feed_id;
53
+ const feedId = ownEntry(hexFeeds, ticker)?.feed_id;
50
54
  if (feedId !== undefined)
51
55
  feedIdByTicker.set(ticker, feedId);
52
56
  }
53
57
  // For pyth_rule the write and read namespaces coincide, so `unreadable`
54
58
  // is always empty; for lazer it is exactly the hex-entry gap.
55
59
  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));
60
+ const unreadable = tickers.filter((ticker) => ownEntry(writeFeeds, ticker) !== undefined && !feedIdByTicker.has(ticker));
57
61
  return { plane: "hermes", feedIdByTicker, unreadable };
58
62
  }
59
63
  case "waterx_rule": {
60
64
  // Absent feeds block ⇒ serves nothing (see the OracleReadPlan doc) —
61
- // never claim tickers the config doesn't name.
65
+ // never claim tickers the config doesn't name. `ownEntry` (own-keys-
66
+ // only, never the `in` operator or a bare bracket read) so a
67
+ // prototype-key ticker can't count as feeds-listed and poison the
68
+ // quote-center batch (which 404s whole batches on unknown symbols).
62
69
  const feeds = host.config.packages.waterx_rule?.feeds;
63
70
  return {
64
71
  plane: "quote_center",
65
- tickers: feeds ? tickers.filter((ticker) => ticker in feeds) : [],
72
+ tickers: tickers.filter((ticker) => ownEntry(feeds, ticker) !== undefined),
66
73
  unreadable: [],
67
74
  };
68
75
  }
@@ -6,6 +6,7 @@
6
6
  * across rules by `kind`. Mechanical wrap only — no on-chain/off-chain logic
7
7
  * changes vs `../pyth.ts` / `./pyth-rule.ts`.
8
8
  */
9
+ import { ownEntry } from "../../utils/record.js";
9
10
  import { assertRuleUpdateData, } from "../price-update-rule.js";
10
11
  import { buildPythPriceUpdateCalls, endpointSupportedFeedIds, fetchPriceFeedsUpdateData, pythCoreHermesEndpoint, } from "../pyth.js";
11
12
  /**
@@ -76,7 +77,7 @@ export const PythCoreRule = {
76
77
  for (const ticker of tickers) {
77
78
  // Same lookup as `host.getPythFeed(ticker)` minus its throw — an
78
79
  // unlisted ticker is a miss here, not an error.
79
- const feedId = host.config.packages.pyth_rule?.feeds?.[ticker]?.feed_id;
80
+ const feedId = ownEntry(host.config.packages.pyth_rule?.feeds, ticker)?.feed_id;
80
81
  if (feedId === undefined || !packedFeedIds.has(feedId))
81
82
  return null;
82
83
  feedIds.push(feedId);
@@ -8,6 +8,7 @@
8
8
  * `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
9
9
  */
10
10
  import { fromHex } from "@mysten/bcs";
11
+ import { ownEntry } from "../../utils/record.js";
11
12
  import { assertRuleUpdateData, } from "../price-update-rule.js";
12
13
  import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch } from "../update-fetch.js";
13
14
  /**
@@ -162,7 +163,9 @@ export const PythLazerRule = {
162
163
  // not fail per ticker as if only that feed were missing.
163
164
  const { feeds } = requireLazerPackage(host);
164
165
  const feedIds = tickers.map((ticker) => {
165
- const feedId = feeds[ticker];
166
+ // ownEntry: a prototype-key ticker ("toString") must throw here as
167
+ // unlisted, not turn an inherited Function into a "feed id".
168
+ const feedId = ownEntry(feeds, ticker);
166
169
  if (feedId === undefined) {
167
170
  throw new Error(`No pyth_lazer_rule feed listed for ticker: ${ticker}`);
168
171
  }
@@ -190,7 +193,7 @@ export const PythLazerRule = {
190
193
  return null;
191
194
  const packedFeedIds = new Set(payload.feedIds);
192
195
  for (const ticker of tickers) {
193
- const feedId = host.config.packages.pyth_lazer_rule?.feeds?.[ticker];
196
+ const feedId = ownEntry(host.config.packages.pyth_lazer_rule?.feeds, ticker);
194
197
  if (feedId === undefined || !packedFeedIds.has(feedId))
195
198
  return null;
196
199
  }
@@ -24,6 +24,7 @@
24
24
  */
25
25
  import { fromHex } from "@mysten/bcs";
26
26
  import { collectBatchLatest, newBatchItem, newBatchPayload, pushBatchItem, } from "../../generated/waterx_rule/waterx_rule.js";
27
+ import { ownEntry } from "../../utils/record.js";
27
28
  import { assertRuleUpdateData, } from "../price-update-rule.js";
28
29
  import { FetchPolicyError, fetchWithPolicy, joinEndpointPath, } from "../update-fetch.js";
29
30
  /** The single signing intent the quote-center emits — exported so read-plane
@@ -260,7 +261,9 @@ export const WaterxRule = {
260
261
  // not fail per ticker as if only that feed were missing.
261
262
  const { feeds } = requireWaterxPackage(host);
262
263
  for (const ticker of tickers) {
263
- if (feeds[ticker] === undefined) {
264
+ // ownEntry: a prototype-key ticker ("toString") must throw here as
265
+ // unlisted, not pass as an inherited Function and reach the network.
266
+ if (ownEntry(feeds, ticker) === undefined) {
264
267
  throw new Error(`No waterx_rule feed listed for ticker: ${ticker}`);
265
268
  }
266
269
  }
@@ -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[];
@@ -0,0 +1,53 @@
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 { ORACLE_SOURCES } from "./price-update-rule.js";
29
+ // Widened-annotation Set (not an assertion) so the type predicate below
30
+ // narrows by CONSTRUCTION rather than by cast.
31
+ const ORACLE_SOURCE_SET = new Set(ORACLE_SOURCES);
32
+ /**
33
+ * THE runtime membership check for {@link ORACLE_SOURCES} — the parser below
34
+ * and `PerpClient`'s ctor validation both use this one predicate, so the env
35
+ * parser and the create-option front door can never disagree. `Set.has`,
36
+ * never `in`/bracket reads (prototype-chain safe by construction).
37
+ */
38
+ export function isOracleSource(value) {
39
+ return ORACLE_SOURCE_SET.has(value);
40
+ }
41
+ export function parseOracleSourceList(raw) {
42
+ const parts = (raw ?? "")
43
+ .split(",")
44
+ .map((part) => part.trim())
45
+ .filter((part) => part !== "");
46
+ const sources = parts.filter(isOracleSource);
47
+ if (parts.length === 0 || sources.length !== parts.length) {
48
+ const got = raw == null || raw.trim() === "" ? "unset" : `'${raw}'`;
49
+ throw new Error(`ORACLE_SOURCE must be a comma-separated list of ${ORACLE_SOURCES.join(" | ")} ` +
50
+ `(got ${got}) — there is NO default oracle source; set it in the deployment's env.`);
51
+ }
52
+ return [...new Set(sources)];
53
+ }
@@ -11,7 +11,7 @@
11
11
  * {@link PerpConfigView}. This class is just the wiring + factory between them.
12
12
  */
13
13
  import { BaseLineClient } from "../base-client.ts";
14
- import type { OracleSource } from "../oracle/price-update-rule.ts";
14
+ import { type OracleSource } from "../oracle/price-update-rule.ts";
15
15
  import type { FetchPolicy } from "../oracle/update-fetch.ts";
16
16
  import { type LoadConfigOptions, type PythAccessConfig, type PythFetchPolicy, type WaterxAccessConfig, type WaterXConfig, type WormholeInfraConfig } from "./config.ts";
17
17
  import type { Network } from "./constants.ts";
@@ -11,6 +11,8 @@
11
11
  * {@link PerpConfigView}. This class is just the wiring + factory between them.
12
12
  */
13
13
  import { BaseLineClient } from "../base-client.js";
14
+ import { ORACLE_SOURCES } from "../oracle/price-update-rule.js";
15
+ import { isOracleSource } from "../oracle/source-list.js";
14
16
  import { PerpConfigView } from "./config-view.js";
15
17
  import { loadConfig, WORMHOLE_DEFAULTS, } from "./config.js";
16
18
  export class PerpClient extends BaseLineClient {
@@ -46,16 +48,18 @@ export class PerpClient extends BaseLineClient {
46
48
  ...(opts.waterxEndpoint !== undefined ? { endpoint: opts.waterxEndpoint } : {}),
47
49
  ...(opts.waterxFetch !== undefined ? { fetch: opts.waterxFetch } : {}),
48
50
  };
49
- // Normalize single-or-list to a deduped, order-preserving list. An empty
50
- // listor a nullish/empty entry, the shape an untyped caller produces
51
- // by omitting the REQUIRED option is a caller bug, not "no oracle":
52
- // fail construction loudly instead of booting green and surfacing as
53
- // `OracleSourceNotImplemented: undefined` at the first tx-build.
51
+ // Normalize single-or-list to a deduped, order-preserving list, gated by
52
+ // `isOracleSource` — the SAME predicate `parseOracleSourceList` uses. An
53
+ // empty list, a nullish entry (an untyped caller omitting the REQUIRED
54
+ // option), or an unregistered value (`'core'`, `'pyth'`, …) fails
55
+ // construction loudly instead of booting green and surfacing as
56
+ // `OracleSourceNotImplemented` at the first tx-build.
54
57
  const sources = Array.isArray(opts.oracleSource) ? opts.oracleSource : [opts.oracleSource];
55
58
  this.oracleSources = [...new Set(sources)];
56
59
  if (this.oracleSources.length === 0 ||
57
- this.oracleSources.some((source) => typeof source !== "string" || source.length === 0)) {
58
- throw new Error(`oracleSource is REQUIRED and must name at least one source (got ${JSON.stringify(sources)})`);
60
+ this.oracleSources.some((source) => !isOracleSource(source))) {
61
+ throw new Error(`oracleSource is REQUIRED and must name at least one of ${ORACLE_SOURCES.join(" | ")} ` +
62
+ `(got ${JSON.stringify(sources)})`);
59
63
  }
60
64
  this.view = new PerpConfigView(() => this.config, () => this.wormhole);
61
65
  }
@@ -8,6 +8,7 @@
8
8
  * callers keep calling `client.getMarket(...)` / `client.wlpType()` unchanged.
9
9
  * Tested in isolation against a plain config object — no network needed.
10
10
  */
11
+ import { ownEntry } from "../utils/record.js";
11
12
  export class PerpConfigView {
12
13
  getConfig;
13
14
  getWormhole;
@@ -26,21 +27,21 @@ export class PerpConfigView {
26
27
  }
27
28
  /** `waterx_perp.markets[ticker]`, throws if unknown. */
28
29
  getMarket(ticker) {
29
- const m = this.config.packages.waterx_perp?.markets?.[ticker];
30
+ const m = ownEntry(this.config.packages.waterx_perp?.markets, ticker);
30
31
  if (!m)
31
32
  throw new Error(`Unknown market ticker: ${ticker}`);
32
33
  return m;
33
34
  }
34
35
  /** `waterx_oracle.aggregators[ticker]`, throws if unknown. */
35
36
  getAggregator(ticker) {
36
- const a = this.config.packages.waterx_oracle?.aggregators?.[ticker];
37
+ const a = ownEntry(this.config.packages.waterx_oracle?.aggregators, ticker);
37
38
  if (!a)
38
39
  throw new Error(`No aggregator listed for ticker: ${ticker}`);
39
40
  return a;
40
41
  }
41
42
  /** `pyth_rule.feeds[ticker]`, throws if unknown. */
42
43
  getPythFeed(ticker) {
43
- const f = this.config.packages.pyth_rule?.feeds?.[ticker];
44
+ const f = ownEntry(this.config.packages.pyth_rule?.feeds, ticker);
44
45
  if (!f)
45
46
  throw new Error(`No pyth feed listed for ticker: ${ticker}`);
46
47
  return f;
@@ -66,7 +67,7 @@ export class PerpConfigView {
66
67
  const c = this.config.packages.constant_rule;
67
68
  if (!c?.published_at || !c.config)
68
69
  return false;
69
- return c.feeds?.[ticker] !== undefined;
70
+ return ownEntry(c.feeds, ticker) !== undefined;
70
71
  }
71
72
  /**
72
73
  * The `supra_rule` config when it is deployed, enabled, and fully wired
@@ -92,7 +93,7 @@ export class PerpConfigView {
92
93
  */
93
94
  getPoolTokenType(tickerOrName) {
94
95
  const poolTokens = this.config.packages.wlp?.pool_tokens ?? {};
95
- const exact = poolTokens[tickerOrName];
96
+ const exact = ownEntry(poolTokens, tickerOrName);
96
97
  if (exact)
97
98
  return exact;
98
99
  for (const t of Object.values(poolTokens)) {
@@ -115,7 +116,7 @@ export class PerpConfigView {
115
116
  * reward coin type, so callers don't need a separate alias-to-type lookup.
116
117
  */
117
118
  getRewarders(stakeAlias) {
118
- const map = this.config.packages.waterx_staking?.rewarders?.[stakeAlias];
119
+ const map = ownEntry(this.config.packages.waterx_staking?.rewarders, stakeAlias);
119
120
  if (!map)
120
121
  return [];
121
122
  return Object.entries(map).map(([alias, entry]) => ({ alias, ...entry }));
@@ -13,7 +13,7 @@ export type { EstLiqPriceViewOpts } from "./liq-view.ts";
13
13
  export * from "./user/index.ts";
14
14
  export * from "./tx-builders.ts";
15
15
  export * from "./fetch.ts";
16
- export { FetchPolicyError, LazerApiKeyMissingError, OracleFeeSourceUnavailableError, OracleSourceNotImplementedError, PythCache, aggregateTicker, aggregateTickerWithConstant, aggregateTickerWithPyth, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, pythCoreHermesEndpoint, pythProHermesEndpoint, resolveHermesReadEndpoint, waterxQuoteCenterEndpoint, refreshOraclePrices, updatePythPrices, } from "../oracle/index.ts";
16
+ export { FetchPolicyError, LazerApiKeyMissingError, OracleFeeSourceUnavailableError, OracleSourceNotImplementedError, PythCache, aggregateTicker, aggregateTickerWithConstant, aggregateTickerWithPyth, ORACLE_SOURCES, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, isOracleSource, parseOracleSourceList, pythCoreHermesEndpoint, pythProHermesEndpoint, refreshOraclePrices, resolveHermesReadEndpoint, updatePythPrices, waterxEnvelopeOf, waterxQuoteCenterEndpoint, } from "../oracle/index.ts";
17
17
  export type { FetchPolicy, OracleFeeSource, OracleSource, UpdateDataProvider, } from "../oracle/index.ts";
18
18
  export { fetchDepositVaa, fetchVaa, listBridgeWithdrawalVaas, listVaasByEmitter, padEvmEmitter, toWormholescanEmitter, vaaBase64ToBytes, vaaBase64ToHex, vaaBytesToBase64, waitForVaa, } from "../account/funding/wormhole.ts";
19
19
  export type { VaaListItem, VaaResponse, WormholescanOptions } from "../account/funding/wormhole.ts";
@@ -15,7 +15,7 @@ export * from "./tx-builders.js";
15
15
  // ======== Read-only queries ========
16
16
  export * from "./fetch.js";
17
17
  // ======== Oracle utilities (Pyth source + rule aggregation) ========
18
- export { FetchPolicyError, LazerApiKeyMissingError, OracleFeeSourceUnavailableError, OracleSourceNotImplementedError, PythCache, aggregateTicker, aggregateTickerWithConstant, aggregateTickerWithPyth, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, pythCoreHermesEndpoint, pythProHermesEndpoint, resolveHermesReadEndpoint, waterxQuoteCenterEndpoint, refreshOraclePrices, updatePythPrices, } from "../oracle/index.js";
18
+ export { FetchPolicyError, LazerApiKeyMissingError, OracleFeeSourceUnavailableError, OracleSourceNotImplementedError, PythCache, aggregateTicker, aggregateTickerWithConstant, aggregateTickerWithPyth, ORACLE_SOURCES, buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData, isOracleSource, parseOracleSourceList, pythCoreHermesEndpoint, pythProHermesEndpoint, refreshOraclePrices, resolveHermesReadEndpoint, updatePythPrices, waterxEnvelopeOf, waterxQuoteCenterEndpoint, } from "../oracle/index.js";
19
19
  // ======== Wormhole / Wormholescan utilities (credit bridge) ========
20
20
  export { fetchDepositVaa, fetchVaa, listBridgeWithdrawalVaas, listVaasByEmitter, padEvmEmitter, toWormholescanEmitter, vaaBase64ToBytes, vaaBase64ToHex, vaaBytesToBase64, waitForVaa, } from "../account/funding/wormhole.js";
21
21
  // ======== Generated BCS types (sui-ts-codegen) ========
@@ -17,9 +17,10 @@
17
17
  */
18
18
  import { makeSenderRequest } from "../../account/account-request.js";
19
19
  import * as staking from "../../generated/waterx_staking/waterx_staking.js";
20
+ import { ownEntry } from "../../utils/record.js";
20
21
  import { toU64Arg } from "../../utils/validate.js";
21
22
  function pool(client, stakeAlias) {
22
- const id = client.config.packages.waterx_staking?.pools?.[stakeAlias];
23
+ const id = ownEntry(client.config.packages.waterx_staking?.pools, stakeAlias);
23
24
  if (!id) {
24
25
  throw new Error(`config.packages.waterx_staking.pools[${stakeAlias}] is not set — staking is not deployed for this stake type`);
25
26
  }
@@ -1,3 +1,4 @@
1
+ import { ownEntry } from "./record.js";
1
2
  /** Returns all registered market tickers (e.g. "BTCUSD") from waterx-config. */
2
3
  export function getMarketTickers(config) {
3
4
  return Object.keys(config.packages.waterx_perp.markets);
@@ -11,5 +12,5 @@ export function getMarketTickers(config) {
11
12
  */
12
13
  export function getCollateralAssets(config) {
13
14
  const feeds = config.packages.pyth_rule?.feeds ?? {};
14
- return Object.keys(config.packages.wlp.pool_tokens).filter((t) => feeds[t] !== undefined);
15
+ return Object.keys(config.packages.wlp.pool_tokens).filter((t) => ownEntry(feeds, t) !== undefined);
15
16
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Own-key record lookup. Ticker-keyed config records (`feeds`, `markets`,
3
+ * `aggregators`) are indexed with caller-supplied strings; a bare bracket
4
+ * read — like the `in` operator — walks the prototype chain, so a ticker
5
+ * named like an `Object.prototype` key ("toString", "constructor", …) reads
6
+ * as an inherited Function instead of "absent" and leaks into batches sent
7
+ * to the network. Every such lookup funnels through here so the answer is
8
+ * own-keys-only, everywhere, instead of per-site `Object.hasOwn` guards
9
+ * that drift.
10
+ */
11
+ /** `record[key]` iff `key` is an OWN key — `undefined` for an absent record, an absent key, or a prototype-chain hit. */
12
+ export declare function ownEntry<T extends Record<string, unknown>>(record: T | undefined, key: string): T[string] | undefined;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Own-key record lookup. Ticker-keyed config records (`feeds`, `markets`,
3
+ * `aggregators`) are indexed with caller-supplied strings; a bare bracket
4
+ * read — like the `in` operator — walks the prototype chain, so a ticker
5
+ * named like an `Object.prototype` key ("toString", "constructor", …) reads
6
+ * as an inherited Function instead of "absent" and leaks into batches sent
7
+ * to the network. Every such lookup funnels through here so the answer is
8
+ * own-keys-only, everywhere, instead of per-site `Object.hasOwn` guards
9
+ * that drift.
10
+ */
11
+ /** `record[key]` iff `key` is an OWN key — `undefined` for an absent record, an absent key, or a prototype-chain hit. */
12
+ export function ownEntry(record, key) {
13
+ // The `T extends Record<...>` form (rather than `Record<string, V>`) keeps
14
+ // union-typed records inferable; TS then resolves `record[key]` only to the
15
+ // constraint's `unknown`, so restate the definitionally-true index type.
16
+ return record !== undefined && Object.hasOwn(record, key)
17
+ ? record[key]
18
+ : undefined;
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waterx/sdk",
3
- "version": "4.3.0",
3
+ "version": "4.3.1",
4
4
  "description": "WaterX SDK — perpetuals and prediction markets on Sui",
5
5
  "license": "MIT",
6
6
  "author": "WaterX",