@waterx/sdk 4.1.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.
- package/README.md +29 -19
- package/dist/cjs/src/account/config.d.ts +3 -2
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +8 -154
- package/dist/cjs/src/oracle/aggregate.d.ts +21 -21
- package/dist/cjs/src/oracle/aggregate.js +121 -71
- package/dist/cjs/src/oracle/config.d.ts +38 -72
- package/dist/cjs/src/oracle/config.js +0 -56
- package/dist/cjs/src/oracle/host.d.ts +24 -20
- package/dist/cjs/src/oracle/index.d.ts +7 -3
- package/dist/cjs/src/oracle/index.js +41 -8
- package/dist/cjs/src/oracle/price-update-rule.d.ts +32 -11
- package/dist/cjs/src/oracle/price-update-rule.js +23 -3
- package/dist/cjs/src/oracle/pyth.d.ts +44 -0
- package/dist/cjs/src/oracle/pyth.js +48 -6
- package/dist/cjs/src/oracle/read-plane.d.ts +70 -0
- package/dist/cjs/src/oracle/read-plane.js +85 -0
- package/dist/cjs/src/oracle/rule-registry.d.ts +6 -4
- package/dist/cjs/src/oracle/rule-registry.js +6 -4
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +6 -5
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +34 -6
- package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -4
- package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +42 -5
- package/dist/cjs/src/oracle/rules/waterx-rule.js +61 -18
- package/dist/cjs/src/oracle/source-list.d.ts +36 -0
- package/dist/cjs/src/oracle/source-list.js +57 -0
- package/dist/cjs/src/perp/client.d.ts +41 -28
- package/dist/cjs/src/perp/client.js +32 -20
- package/dist/cjs/src/perp/config-view.js +7 -6
- package/dist/cjs/src/perp/config.d.ts +6 -6
- package/dist/cjs/src/perp/config.js +12 -12
- package/dist/cjs/src/perp/index.d.ts +3 -3
- package/dist/cjs/src/perp/index.js +11 -4
- package/dist/cjs/src/perp/user/staking.js +2 -1
- package/dist/cjs/src/unified-client.d.ts +14 -9
- package/dist/cjs/src/unified-client.js +2 -2
- package/dist/cjs/src/utils/config.js +2 -1
- package/dist/cjs/src/utils/record.d.ts +12 -0
- package/dist/cjs/src/utils/record.js +22 -0
- package/dist/src/account/config.d.ts +3 -2
- package/dist/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
- package/dist/src/generated/waterx_rule/waterx_rule.js +7 -143
- package/dist/src/oracle/aggregate.d.ts +21 -21
- package/dist/src/oracle/aggregate.js +121 -71
- package/dist/src/oracle/config.d.ts +38 -72
- package/dist/src/oracle/config.js +1 -55
- package/dist/src/oracle/host.d.ts +24 -20
- package/dist/src/oracle/index.d.ts +7 -3
- package/dist/src/oracle/index.js +32 -10
- package/dist/src/oracle/price-update-rule.d.ts +32 -11
- package/dist/src/oracle/price-update-rule.js +22 -3
- package/dist/src/oracle/pyth.d.ts +44 -0
- package/dist/src/oracle/pyth.js +45 -5
- package/dist/src/oracle/read-plane.d.ts +70 -0
- package/dist/src/oracle/read-plane.js +81 -0
- package/dist/src/oracle/rule-registry.d.ts +6 -4
- package/dist/src/oracle/rule-registry.js +6 -4
- package/dist/src/oracle/rules/pyth-core-rule.js +7 -6
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +33 -5
- package/dist/src/oracle/rules/pyth-rule.js +5 -4
- package/dist/src/oracle/rules/waterx-rule.d.ts +42 -5
- package/dist/src/oracle/rules/waterx-rule.js +57 -15
- package/dist/src/oracle/source-list.d.ts +36 -0
- package/dist/src/oracle/source-list.js +53 -0
- package/dist/src/perp/client.d.ts +41 -28
- package/dist/src/perp/client.js +33 -21
- package/dist/src/perp/config-view.js +7 -6
- package/dist/src/perp/config.d.ts +6 -6
- package/dist/src/perp/config.js +11 -9
- package/dist/src/perp/index.d.ts +3 -3
- package/dist/src/perp/index.js +2 -2
- package/dist/src/perp/user/staking.js +2 -1
- package/dist/src/unified-client.d.ts +14 -9
- package/dist/src/unified-client.js +2 -2
- package/dist/src/utils/config.js +2 -1
- package/dist/src/utils/record.d.ts +12 -0
- package/dist/src/utils/record.js +19 -0
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read-plane.ts` — per-source READ-plane resolution: which of a caller's
|
|
3
|
+
* tickers a source can PRICE off-chain, and with which ids. The write plane
|
|
4
|
+
* (`PriceUpdateRule` + each source's `feeds` block) and the read plane are
|
|
5
|
+
* DIFFERENT namespaces for the pyth sources: Lazer WRITES with the integer
|
|
6
|
+
* ids in `pyth_lazer_rule.feeds`, but every Hermes-compatible price READ is
|
|
7
|
+
* keyed by the hex ids only `pyth_rule.feeds` carries. That cross-block
|
|
8
|
+
* dependency is a config invariant ("a lazer-fed ticker must also carry a
|
|
9
|
+
* Core hex entry to be read-visible"), and it lives HERE, once — consumers
|
|
10
|
+
* (FE/BE price facades) resolve through this instead of hardcoding which
|
|
11
|
+
* sources share which feeds namespace.
|
|
12
|
+
*/
|
|
13
|
+
import type { Network } from "../constants.ts";
|
|
14
|
+
import type { OracleHost } from "./host.ts";
|
|
15
|
+
import type { OracleSource } from "./price-update-rule.ts";
|
|
16
|
+
/**
|
|
17
|
+
* One source's read plan for a requested ticker set.
|
|
18
|
+
*
|
|
19
|
+
* - `plane: "hermes"` (pyth sources) — price via a Hermes-compatible REST
|
|
20
|
+
* endpoint, one entry per servable ticker mapped to its HEX feed id. The
|
|
21
|
+
* endpoint to execute against is {@link resolveHermesReadEndpoint} — the
|
|
22
|
+
* plan (ids) plus that resolver (host) is the complete read contract.
|
|
23
|
+
* - `plane: "quote_center"` (waterx) — price via the quote-center symbols
|
|
24
|
+
* api, keyed by ticker; served set = the `waterx_rule.feeds` block. An
|
|
25
|
+
* ABSENT block (source listed, package missing from the loaded config)
|
|
26
|
+
* serves NOTHING: claiming tickers would silently reroute reads to the
|
|
27
|
+
* quote-center — it happily serves symbols regardless of on-chain config —
|
|
28
|
+
* and swallow tickers a later-listed source could price. The
|
|
29
|
+
* misconfiguration is caught loudly by the consumer's feeds assert at
|
|
30
|
+
* client creation instead.
|
|
31
|
+
* - `unreadable` — requested tickers this source WRITES on-chain (its update
|
|
32
|
+
* leg serves them) but its read plane cannot price: the silent-invisibility
|
|
33
|
+
* trap (e.g. a lazer-fed ticker with no `pyth_rule.feeds` hex entry).
|
|
34
|
+
* Callers should surface these loudly; empty for sources whose write and
|
|
35
|
+
* read namespaces coincide.
|
|
36
|
+
*/
|
|
37
|
+
export type OracleReadPlan = {
|
|
38
|
+
unreadable: string[];
|
|
39
|
+
} & ({
|
|
40
|
+
plane: "hermes";
|
|
41
|
+
feedIdByTicker: Map<string, string>;
|
|
42
|
+
} | {
|
|
43
|
+
plane: "quote_center";
|
|
44
|
+
tickers: string[];
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* Resolve `source`'s read plan for `tickers`. Pure config lookup — no
|
|
48
|
+
* network, no endpoint resolution (endpoints come from
|
|
49
|
+
* `pythCoreHermesEndpoint` / `waterxQuoteCenterEndpoint` / the deployment's
|
|
50
|
+
* own env). A ticker absent from the returned plan is simply not servable by
|
|
51
|
+
* THIS source's read plane — callers decide how to degrade (typically: ask
|
|
52
|
+
* the next source in their `ORACLE_SOURCE` list, then omit).
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* The Hermes-compatible REST base a deployment's hermes-plane read plans
|
|
56
|
+
* execute against — the endpoint half of the read contract
|
|
57
|
+
* ({@link resolveOracleReadPlan} is the ids half):
|
|
58
|
+
*
|
|
59
|
+
* - `pyth_rule` in the fed set → the Core source's own keyless endpoint
|
|
60
|
+
* (`pythCoreHermesEndpoint(network)`).
|
|
61
|
+
* - otherwise → `override` when the deployment set one (a proxy or
|
|
62
|
+
* self-hosted mirror), else the documented Pyth Pro base
|
|
63
|
+
* (`pythProHermesEndpoint()` — identical for every subscriber; auth is the
|
|
64
|
+
* caller's `pythApiKey` Bearer, not a per-deployment URL).
|
|
65
|
+
*
|
|
66
|
+
* Total — never throws, never falls back Core-ward: a fed set without
|
|
67
|
+
* `pyth_rule` reads Pro (or the override), full stop.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveHermesReadEndpoint(network: Network, sources: readonly OracleSource[], override?: string): string;
|
|
70
|
+
export declare function resolveOracleReadPlan(host: OracleHost, source: OracleSource, tickers: string[]): OracleReadPlan;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read-plane.ts` — per-source READ-plane resolution: which of a caller's
|
|
3
|
+
* tickers a source can PRICE off-chain, and with which ids. The write plane
|
|
4
|
+
* (`PriceUpdateRule` + each source's `feeds` block) and the read plane are
|
|
5
|
+
* DIFFERENT namespaces for the pyth sources: Lazer WRITES with the integer
|
|
6
|
+
* ids in `pyth_lazer_rule.feeds`, but every Hermes-compatible price READ is
|
|
7
|
+
* keyed by the hex ids only `pyth_rule.feeds` carries. That cross-block
|
|
8
|
+
* dependency is a config invariant ("a lazer-fed ticker must also carry a
|
|
9
|
+
* Core hex entry to be read-visible"), and it lives HERE, once — consumers
|
|
10
|
+
* (FE/BE price facades) resolve through this instead of hardcoding which
|
|
11
|
+
* sources share which feeds namespace.
|
|
12
|
+
*/
|
|
13
|
+
import { ownEntry } from "../utils/record.js";
|
|
14
|
+
import { pythCoreHermesEndpoint, pythProHermesEndpoint } from "./pyth.js";
|
|
15
|
+
/**
|
|
16
|
+
* Resolve `source`'s read plan for `tickers`. Pure config lookup — no
|
|
17
|
+
* network, no endpoint resolution (endpoints come from
|
|
18
|
+
* `pythCoreHermesEndpoint` / `waterxQuoteCenterEndpoint` / the deployment's
|
|
19
|
+
* own env). A ticker absent from the returned plan is simply not servable by
|
|
20
|
+
* THIS source's read plane — callers decide how to degrade (typically: ask
|
|
21
|
+
* the next source in their `ORACLE_SOURCE` list, then omit).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The Hermes-compatible REST base a deployment's hermes-plane read plans
|
|
25
|
+
* execute against — the endpoint half of the read contract
|
|
26
|
+
* ({@link resolveOracleReadPlan} is the ids half):
|
|
27
|
+
*
|
|
28
|
+
* - `pyth_rule` in the fed set → the Core source's own keyless endpoint
|
|
29
|
+
* (`pythCoreHermesEndpoint(network)`).
|
|
30
|
+
* - otherwise → `override` when the deployment set one (a proxy or
|
|
31
|
+
* self-hosted mirror), else the documented Pyth Pro base
|
|
32
|
+
* (`pythProHermesEndpoint()` — identical for every subscriber; auth is the
|
|
33
|
+
* caller's `pythApiKey` Bearer, not a per-deployment URL).
|
|
34
|
+
*
|
|
35
|
+
* Total — never throws, never falls back Core-ward: a fed set without
|
|
36
|
+
* `pyth_rule` reads Pro (or the override), full stop.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveHermesReadEndpoint(network, sources, override) {
|
|
39
|
+
if (sources.includes("pyth_rule"))
|
|
40
|
+
return pythCoreHermesEndpoint(network);
|
|
41
|
+
return override ?? pythProHermesEndpoint();
|
|
42
|
+
}
|
|
43
|
+
export function resolveOracleReadPlan(host, source, tickers) {
|
|
44
|
+
switch (source) {
|
|
45
|
+
case "pyth_rule":
|
|
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.
|
|
50
|
+
const hexFeeds = host.config.packages.pyth_rule?.feeds;
|
|
51
|
+
const feedIdByTicker = new Map();
|
|
52
|
+
for (const ticker of tickers) {
|
|
53
|
+
const feedId = ownEntry(hexFeeds, ticker)?.feed_id;
|
|
54
|
+
if (feedId !== undefined)
|
|
55
|
+
feedIdByTicker.set(ticker, feedId);
|
|
56
|
+
}
|
|
57
|
+
// For pyth_rule the write and read namespaces coincide, so `unreadable`
|
|
58
|
+
// is always empty; for lazer it is exactly the hex-entry gap.
|
|
59
|
+
const writeFeeds = source === "pyth_lazer_rule" ? host.config.packages.pyth_lazer_rule?.feeds : hexFeeds;
|
|
60
|
+
const unreadable = tickers.filter((ticker) => ownEntry(writeFeeds, ticker) !== undefined && !feedIdByTicker.has(ticker));
|
|
61
|
+
return { plane: "hermes", feedIdByTicker, unreadable };
|
|
62
|
+
}
|
|
63
|
+
case "waterx_rule": {
|
|
64
|
+
// Absent feeds block ⇒ serves nothing (see the OracleReadPlan doc) —
|
|
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).
|
|
69
|
+
const feeds = host.config.packages.waterx_rule?.feeds;
|
|
70
|
+
return {
|
|
71
|
+
plane: "quote_center",
|
|
72
|
+
tickers: tickers.filter((ticker) => ownEntry(feeds, ticker) !== undefined),
|
|
73
|
+
unreadable: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
default: {
|
|
77
|
+
const exhausted = source;
|
|
78
|
+
throw new Error(`resolveOracleReadPlan: unhandled OracleSource '${String(exhausted)}'`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `rule-registry.ts` — maps a client-selected `OracleSource` to its concrete
|
|
3
|
-
* `PriceUpdateRule` implementation
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* `PriceUpdateRule` implementation; the ONE place `OracleSource` values are
|
|
4
|
+
* wired to a rule instance. `refreshOraclePrices` (`aggregate.ts`) resolves
|
|
5
|
+
* every `host.oracleSources` entry through it, and consumers (e.g. a BE
|
|
6
|
+
* prefetch cache keying per source) import `resolveOracleRule` rather than
|
|
7
|
+
* hand-mirroring this map. Selection is driven purely by the value passed in
|
|
8
|
+
* (ultimately `OracleHost.oracleSources`, the `oracleSource` client create
|
|
7
9
|
* option) — never by a config JSON `enabled` flag and never by `process.env`.
|
|
8
10
|
*
|
|
9
11
|
* Each source is self-contained: it owns its own infra + config and does NOT
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `rule-registry.ts` — maps a client-selected `OracleSource` to its concrete
|
|
3
|
-
* `PriceUpdateRule` implementation
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* `PriceUpdateRule` implementation; the ONE place `OracleSource` values are
|
|
4
|
+
* wired to a rule instance. `refreshOraclePrices` (`aggregate.ts`) resolves
|
|
5
|
+
* every `host.oracleSources` entry through it, and consumers (e.g. a BE
|
|
6
|
+
* prefetch cache keying per source) import `resolveOracleRule` rather than
|
|
7
|
+
* hand-mirroring this map. Selection is driven purely by the value passed in
|
|
8
|
+
* (ultimately `OracleHost.oracleSources`, the `oracleSource` client create
|
|
7
9
|
* option) — never by a config JSON `enabled` flag and never by `process.env`.
|
|
8
10
|
*
|
|
9
11
|
* Each source is self-contained: it owns its own infra + config and does NOT
|
|
@@ -6,8 +6,9 @@
|
|
|
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
|
-
import { buildPythPriceUpdateCalls, endpointSupportedFeedIds, fetchPriceFeedsUpdateData, } from "../pyth.js";
|
|
11
|
+
import { buildPythPriceUpdateCalls, endpointSupportedFeedIds, fetchPriceFeedsUpdateData, pythCoreHermesEndpoint, } from "../pyth.js";
|
|
11
12
|
/**
|
|
12
13
|
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
13
14
|
* caller before this runs, since a same-shaped payload from a different rule
|
|
@@ -33,10 +34,10 @@ export const PythCoreRule = {
|
|
|
33
34
|
async fetchUpdateData(host, tickers) {
|
|
34
35
|
if (tickers.length === 0)
|
|
35
36
|
return null;
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
const endpoint = host.
|
|
37
|
+
// Endpoint is this source's own per-network infra (`PYTH_CORE_INFRA`);
|
|
38
|
+
// credential + retry/timeout policy are the caller-supplied `client.pyth`
|
|
39
|
+
// access slice.
|
|
40
|
+
const endpoint = pythCoreHermesEndpoint(host.network);
|
|
40
41
|
const feedIds = tickers.map((ticker) => host.getPythFeed(ticker).feed_id);
|
|
41
42
|
const updates = await fetchPriceFeedsUpdateData(endpoint, feedIds, {
|
|
42
43
|
apiKey: host.pyth.api_key,
|
|
@@ -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
|
|
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,8 +8,29 @@
|
|
|
8
8
|
* `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
|
|
9
9
|
*/
|
|
10
10
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
11
|
+
import type { Network } from "../../constants.ts";
|
|
11
12
|
import type { OracleHost } from "../host.ts";
|
|
12
13
|
import { type PriceUpdateRule } from "../price-update-rule.ts";
|
|
14
|
+
/**
|
|
15
|
+
* Pyth Lazer (Pyth Pro) external infra — owned by THIS source, by network.
|
|
16
|
+
* Per-network constants for infrastructure Pyth operates (not part of the
|
|
17
|
+
* `waterx-config` JSON), co-located with the only rule that reads them —
|
|
18
|
+
* no other oracle source ever touches a Lazer endpoint or verifier.
|
|
19
|
+
*
|
|
20
|
+
* - `endpoint` — Lazer HTTP API base; signed updates come from
|
|
21
|
+
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
22
|
+
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
23
|
+
* Lazer signers), so both networks share the production host.
|
|
24
|
+
* - `verifier_package` — the Sui package carrying
|
|
25
|
+
* `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
|
|
26
|
+
* still the original v1 publish; mainnet is the v2-upgraded package (which
|
|
27
|
+
* still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
|
|
28
|
+
* contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
|
|
29
|
+
*/
|
|
30
|
+
export declare const LAZER_INFRA: Record<Network, {
|
|
31
|
+
endpoint: string;
|
|
32
|
+
verifier_package: string;
|
|
33
|
+
}>;
|
|
13
34
|
/** `pyth_lazer_rule`'s narrowed `RuleUpdateData.payload` shape. */
|
|
14
35
|
export interface PythLazerUpdatePayload {
|
|
15
36
|
/** One signed `leEcdsa` message carrying every requested feed. */
|
|
@@ -8,9 +8,35 @@
|
|
|
8
8
|
* `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
|
|
9
9
|
*/
|
|
10
10
|
import { fromHex } from "@mysten/bcs";
|
|
11
|
-
import {
|
|
11
|
+
import { ownEntry } from "../../utils/record.js";
|
|
12
12
|
import { assertRuleUpdateData, } from "../price-update-rule.js";
|
|
13
13
|
import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch } from "../update-fetch.js";
|
|
14
|
+
/**
|
|
15
|
+
* Pyth Lazer (Pyth Pro) external infra — owned by THIS source, by network.
|
|
16
|
+
* Per-network constants for infrastructure Pyth operates (not part of the
|
|
17
|
+
* `waterx-config` JSON), co-located with the only rule that reads them —
|
|
18
|
+
* no other oracle source ever touches a Lazer endpoint or verifier.
|
|
19
|
+
*
|
|
20
|
+
* - `endpoint` — Lazer HTTP API base; signed updates come from
|
|
21
|
+
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
22
|
+
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
23
|
+
* Lazer signers), so both networks share the production host.
|
|
24
|
+
* - `verifier_package` — the Sui package carrying
|
|
25
|
+
* `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
|
|
26
|
+
* still the original v1 publish; mainnet is the v2-upgraded package (which
|
|
27
|
+
* still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
|
|
28
|
+
* contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
|
|
29
|
+
*/
|
|
30
|
+
export const LAZER_INFRA = {
|
|
31
|
+
MAINNET: {
|
|
32
|
+
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
33
|
+
verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
|
|
34
|
+
},
|
|
35
|
+
TESTNET: {
|
|
36
|
+
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
37
|
+
verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
14
40
|
/**
|
|
15
41
|
* Signed-update request pins, mirroring what the on-chain rule consumes:
|
|
16
42
|
* - `properties` — `price` + `exponent` are REQUIRED by
|
|
@@ -137,7 +163,9 @@ export const PythLazerRule = {
|
|
|
137
163
|
// not fail per ticker as if only that feed were missing.
|
|
138
164
|
const { feeds } = requireLazerPackage(host);
|
|
139
165
|
const feedIds = tickers.map((ticker) => {
|
|
140
|
-
|
|
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);
|
|
141
169
|
if (feedId === undefined) {
|
|
142
170
|
throw new Error(`No pyth_lazer_rule feed listed for ticker: ${ticker}`);
|
|
143
171
|
}
|
|
@@ -147,7 +175,7 @@ export const PythLazerRule = {
|
|
|
147
175
|
if (!apiKey) {
|
|
148
176
|
throw new LazerApiKeyMissingError();
|
|
149
177
|
}
|
|
150
|
-
const update = await fetchLazerSignedUpdate(
|
|
178
|
+
const update = await fetchLazerSignedUpdate(LAZER_INFRA[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
|
|
151
179
|
return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
|
|
152
180
|
},
|
|
153
181
|
/**
|
|
@@ -165,7 +193,7 @@ export const PythLazerRule = {
|
|
|
165
193
|
return null;
|
|
166
194
|
const packedFeedIds = new Set(payload.feedIds);
|
|
167
195
|
for (const ticker of tickers) {
|
|
168
|
-
const feedId = host.config.packages.pyth_lazer_rule?.feeds
|
|
196
|
+
const feedId = ownEntry(host.config.packages.pyth_lazer_rule?.feeds, ticker);
|
|
169
197
|
if (feedId === undefined || !packedFeedIds.has(feedId))
|
|
170
198
|
return null;
|
|
171
199
|
}
|
|
@@ -184,7 +212,7 @@ export const PythLazerRule = {
|
|
|
184
212
|
return undefined;
|
|
185
213
|
const lazer = requireLazerPackage(host);
|
|
186
214
|
const [update] = tx.moveCall({
|
|
187
|
-
target: `${
|
|
215
|
+
target: `${LAZER_INFRA[host.network].verifier_package}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
|
|
188
216
|
arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
|
|
189
217
|
});
|
|
190
218
|
return { kind: "pyth_lazer_rule", update };
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* `PriceInfoObject` is fresh.
|
|
8
8
|
*/
|
|
9
9
|
import { feed as pythRuleFeed } from "../../generated/waterx_pyth_rule/pyth_rule.js";
|
|
10
|
+
import { PYTH_CORE_INFRA } from "../pyth.js";
|
|
10
11
|
export function feedPythRule(tx, host, collector, priceInfoObjectId) {
|
|
11
12
|
pythRuleFeed({
|
|
12
13
|
package: host.config.packages.pyth_rule.published_at,
|
|
@@ -15,10 +16,10 @@ export function feedPythRule(tx, host, collector, priceInfoObjectId) {
|
|
|
15
16
|
config: tx.object(host.config.packages.pyth_rule.config),
|
|
16
17
|
// The deployed pyth_rule package is compiled against the Core pyth
|
|
17
18
|
// dependency, so its `&PythState` parameter is the Core-package-qualified
|
|
18
|
-
// type and
|
|
19
|
-
// right state to pass. The config's price_info_object
|
|
20
|
-
// objects to match.
|
|
21
|
-
pythState: tx.object(host.
|
|
19
|
+
// type and the Core source's own per-network state (`PYTH_CORE_INFRA`)
|
|
20
|
+
// is always the right state to pass. The config's price_info_object
|
|
21
|
+
// entries are Core objects to match.
|
|
22
|
+
pythState: tx.object(PYTH_CORE_INFRA[host.network].state_id),
|
|
22
23
|
pythPriceInfo: tx.object(priceInfoObjectId),
|
|
23
24
|
},
|
|
24
25
|
})(tx);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `aggregateTicker` appends per waterx-routed ticker. Pulls one enclave-signed
|
|
5
5
|
* batch envelope covering every requested ticker from the quote-center
|
|
6
6
|
* (`GET /v1/quotes/update?symbols=…`, endpoint from `host.waterx` — the
|
|
7
|
-
* `waterxEndpoint`/`waterxFetch` create options — else `
|
|
7
|
+
* `waterxEndpoint`/`waterxFetch` create options — else its own `WATERX_INFRA`), then —
|
|
8
8
|
* unlike Pyth Lazer, whose verify is a single shared PTB step — verifies AND
|
|
9
9
|
* feeds in ONE `waterx_rule::collect_batch_latest` call per collector (the Move
|
|
10
10
|
* API bundles the two). So `buildUpdateCalls` emits nothing and the signed
|
|
@@ -13,12 +13,47 @@
|
|
|
13
13
|
* `collect_batch_latest` is the dual-rule path: it feeds the item matching
|
|
14
14
|
* `collector.symbol()` WITHOUT aggregating, so a waterx-routed ticker composes
|
|
15
15
|
* onto the same collector as Pyth/Supra (compose-then-aggregate). On-chain a
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* FRESHNESS miss ABSTAINS (the other weighted rules cover); a config/
|
|
17
|
+
* integrity mismatch, bad signature, future timestamp — or a REPLAYED signed
|
|
18
|
+
* timestamp — ABORTS (`EReplayedSignature`, audit F-014: a signed tuple is
|
|
19
|
+
* single-use per symbol, enforced by a per-symbol high-water mark BEFORE any
|
|
20
|
+
* weight arbitration). Consequence for concurrent builds: two PTBs carrying
|
|
21
|
+
* the same envelope for the same symbol cannot both land — the second aborts
|
|
22
|
+
* even if the rule is unweighted for that ticker. Never share one fetched
|
|
23
|
+
* envelope across builds that may execute concurrently for the same symbol.
|
|
18
24
|
*/
|
|
19
25
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
26
|
+
import type { Network } from "../../constants.ts";
|
|
20
27
|
import type { OracleHost } from "../host.ts";
|
|
21
28
|
import { type PriceUpdateRule, type RuleUpdateData } from "../price-update-rule.ts";
|
|
29
|
+
/** The single signing intent the quote-center emits — exported so read-plane
|
|
30
|
+
* consumers can mirror the rule's own envelope intent check (a mispointed
|
|
31
|
+
* endpoint must be rejected by reads exactly as tx-builds reject it). */
|
|
32
|
+
export declare const BATCH_PRICE_INTENT = 1;
|
|
33
|
+
/**
|
|
34
|
+
* WaterX quote-center external infra — owned by THIS source, by network.
|
|
35
|
+
* Mirrors `PYTH_CORE_INFRA` (oracle/pyth.ts) and `LAZER_INFRA`
|
|
36
|
+
* (rules/pyth-lazer-rule.ts): per-network constants for infrastructure the
|
|
37
|
+
* source's operator runs, co-located with the only rule that reads them — no
|
|
38
|
+
* other oracle source ever touches a quote-center endpoint. Public read (no
|
|
39
|
+
* auth), so there is no api_key. `endpoint` has no trailing slash — the rule
|
|
40
|
+
* appends the path.
|
|
41
|
+
*
|
|
42
|
+
* These are the DEFAULTS behind the caller's `client.waterx` access slice
|
|
43
|
+
* (`waterxEndpoint` / `waterxFetch` create options) — the browser-CORS proxy
|
|
44
|
+
* hook, since this is the one source fetched from the page.
|
|
45
|
+
*/
|
|
46
|
+
export declare const WATERX_INFRA: Record<Network, {
|
|
47
|
+
endpoint: string;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* The waterx source's quote-center base for `network` — the ONE accessor
|
|
51
|
+
* consumers (BE/FE read planes) use when, and only when, their own
|
|
52
|
+
* `ORACLE_SOURCE` resolves to `'waterx_rule'`. Mirrors
|
|
53
|
+
* `pythCoreHermesEndpoint`. Under any other source the read endpoint is that
|
|
54
|
+
* source's own configuration — never this one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function waterxQuoteCenterEndpoint(network: Network): string;
|
|
22
57
|
/**
|
|
23
58
|
* One item inside a signed batch payload, mirroring the quote-center
|
|
24
59
|
* `/v1/quotes/update` JSON 1:1 (snake_case). The u64 integer fields are the
|
|
@@ -82,8 +117,10 @@ export declare function waterxEnvelopeOf(data: RuleUpdateData): WaterxSignedEnve
|
|
|
82
117
|
* per item, the exact shape the enclave signed) and contribute the price for
|
|
83
118
|
* `collector.symbol()` to the collector. One collect call re-verifies the batch
|
|
84
119
|
* signature and picks this collector's symbol out of the batch; on-chain it
|
|
85
|
-
* abstains (records `none`)
|
|
86
|
-
*
|
|
120
|
+
* abstains (records `none`) when the symbol is stale or absent from the batch,
|
|
121
|
+
* but ABORTS `EReplayedSignature` when the symbol's signed timestamp was
|
|
122
|
+
* already accepted (per-symbol high-water mark, audit F-014) — see the module
|
|
123
|
+
* header for the concurrent-build consequence.
|
|
87
124
|
*/
|
|
88
125
|
export declare function feedWaterxRule(tx: Transaction, host: OracleHost, collector: TransactionArgument, envelope: WaterxSignedEnvelope): void;
|
|
89
126
|
export declare const WaterxRule: PriceUpdateRule;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `aggregateTicker` appends per waterx-routed ticker. Pulls one enclave-signed
|
|
5
5
|
* batch envelope covering every requested ticker from the quote-center
|
|
6
6
|
* (`GET /v1/quotes/update?symbols=…`, endpoint from `host.waterx` — the
|
|
7
|
-
* `waterxEndpoint`/`waterxFetch` create options — else `
|
|
7
|
+
* `waterxEndpoint`/`waterxFetch` create options — else its own `WATERX_INFRA`), then —
|
|
8
8
|
* unlike Pyth Lazer, whose verify is a single shared PTB step — verifies AND
|
|
9
9
|
* feeds in ONE `waterx_rule::collect_batch_latest` call per collector (the Move
|
|
10
10
|
* API bundles the two). So `buildUpdateCalls` emits nothing and the signed
|
|
@@ -13,16 +13,51 @@
|
|
|
13
13
|
* `collect_batch_latest` is the dual-rule path: it feeds the item matching
|
|
14
14
|
* `collector.symbol()` WITHOUT aggregating, so a waterx-routed ticker composes
|
|
15
15
|
* onto the same collector as Pyth/Supra (compose-then-aggregate). On-chain a
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* FRESHNESS miss ABSTAINS (the other weighted rules cover); a config/
|
|
17
|
+
* integrity mismatch, bad signature, future timestamp — or a REPLAYED signed
|
|
18
|
+
* timestamp — ABORTS (`EReplayedSignature`, audit F-014: a signed tuple is
|
|
19
|
+
* single-use per symbol, enforced by a per-symbol high-water mark BEFORE any
|
|
20
|
+
* weight arbitration). Consequence for concurrent builds: two PTBs carrying
|
|
21
|
+
* the same envelope for the same symbol cannot both land — the second aborts
|
|
22
|
+
* even if the rule is unweighted for that ticker. Never share one fetched
|
|
23
|
+
* envelope across builds that may execute concurrently for the same symbol.
|
|
18
24
|
*/
|
|
19
25
|
import { fromHex } from "@mysten/bcs";
|
|
20
26
|
import { collectBatchLatest, newBatchItem, newBatchPayload, pushBatchItem, } from "../../generated/waterx_rule/waterx_rule.js";
|
|
21
|
-
import {
|
|
27
|
+
import { ownEntry } from "../../utils/record.js";
|
|
22
28
|
import { assertRuleUpdateData, } from "../price-update-rule.js";
|
|
23
29
|
import { FetchPolicyError, fetchWithPolicy, joinEndpointPath, } from "../update-fetch.js";
|
|
24
|
-
/** The single signing intent
|
|
25
|
-
|
|
30
|
+
/** The single signing intent the quote-center emits — exported so read-plane
|
|
31
|
+
* consumers can mirror the rule's own envelope intent check (a mispointed
|
|
32
|
+
* endpoint must be rejected by reads exactly as tx-builds reject it). */
|
|
33
|
+
export const BATCH_PRICE_INTENT = 1;
|
|
34
|
+
/**
|
|
35
|
+
* WaterX quote-center external infra — owned by THIS source, by network.
|
|
36
|
+
* Mirrors `PYTH_CORE_INFRA` (oracle/pyth.ts) and `LAZER_INFRA`
|
|
37
|
+
* (rules/pyth-lazer-rule.ts): per-network constants for infrastructure the
|
|
38
|
+
* source's operator runs, co-located with the only rule that reads them — no
|
|
39
|
+
* other oracle source ever touches a quote-center endpoint. Public read (no
|
|
40
|
+
* auth), so there is no api_key. `endpoint` has no trailing slash — the rule
|
|
41
|
+
* appends the path.
|
|
42
|
+
*
|
|
43
|
+
* These are the DEFAULTS behind the caller's `client.waterx` access slice
|
|
44
|
+
* (`waterxEndpoint` / `waterxFetch` create options) — the browser-CORS proxy
|
|
45
|
+
* hook, since this is the one source fetched from the page.
|
|
46
|
+
*/
|
|
47
|
+
export const WATERX_INFRA = {
|
|
48
|
+
MAINNET: { endpoint: "https://quote-center.waterx.app" },
|
|
49
|
+
TESTNET: { endpoint: "https://quote-center-staging.waterx.app" },
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The waterx source's quote-center base for `network` — the ONE accessor
|
|
53
|
+
* consumers (BE/FE read planes) use when, and only when, their own
|
|
54
|
+
* `ORACLE_SOURCE` resolves to `'waterx_rule'`. Mirrors
|
|
55
|
+
* `pythCoreHermesEndpoint`. Under any other source the read endpoint is that
|
|
56
|
+
* source's own configuration — never this one.
|
|
57
|
+
*/
|
|
58
|
+
export function waterxQuoteCenterEndpoint(network) {
|
|
59
|
+
return WATERX_INFRA[network].endpoint;
|
|
60
|
+
}
|
|
26
61
|
/**
|
|
27
62
|
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
28
63
|
* caller before this runs (mirrors the other rules' guard split), so a
|
|
@@ -84,18 +119,21 @@ function requireWaterxPackage(host) {
|
|
|
84
119
|
return entry;
|
|
85
120
|
}
|
|
86
121
|
/**
|
|
87
|
-
* Resolve the quote-center infra for this host:
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
122
|
+
* Resolve the quote-center infra for this host: each field independently from
|
|
123
|
+
* the caller's `client.waterx` access slice (`waterxEndpoint` / `waterxFetch`
|
|
124
|
+
* create options) when set, else this source's own `WATERX_INFRA` default /
|
|
125
|
+
* `fetchWithPolicy`'s built-ins. Deliberately NO fallback onto `pyth.fetch`
|
|
126
|
+
* or any other source's policy — sources stay fully independent.
|
|
91
127
|
*
|
|
92
128
|
* This is the seam a browser consumer needs: the envelope is fetched FROM THE
|
|
93
129
|
* PAGE, so a front end whose origin the quote-center does not allow (CORS)
|
|
94
130
|
* points `endpoint` at a same-origin proxy, or supplies its own `fetchImpl`.
|
|
95
131
|
*/
|
|
96
132
|
function resolveWaterxInfra(host) {
|
|
97
|
-
|
|
98
|
-
|
|
133
|
+
return {
|
|
134
|
+
endpoint: host.waterx?.endpoint ?? WATERX_INFRA[host.network].endpoint,
|
|
135
|
+
fetch: host.waterx?.fetch,
|
|
136
|
+
};
|
|
99
137
|
}
|
|
100
138
|
/**
|
|
101
139
|
* Pull one enclave-signed batch envelope covering `symbols` from the
|
|
@@ -151,8 +189,10 @@ function decodeSig(hex) {
|
|
|
151
189
|
* per item, the exact shape the enclave signed) and contribute the price for
|
|
152
190
|
* `collector.symbol()` to the collector. One collect call re-verifies the batch
|
|
153
191
|
* signature and picks this collector's symbol out of the batch; on-chain it
|
|
154
|
-
* abstains (records `none`)
|
|
155
|
-
*
|
|
192
|
+
* abstains (records `none`) when the symbol is stale or absent from the batch,
|
|
193
|
+
* but ABORTS `EReplayedSignature` when the symbol's signed timestamp was
|
|
194
|
+
* already accepted (per-symbol high-water mark, audit F-014) — see the module
|
|
195
|
+
* header for the concurrent-build consequence.
|
|
156
196
|
*/
|
|
157
197
|
export function feedWaterxRule(tx, host, collector, envelope) {
|
|
158
198
|
const wr = requireWaterxPackage(host);
|
|
@@ -221,7 +261,9 @@ export const WaterxRule = {
|
|
|
221
261
|
// not fail per ticker as if only that feed were missing.
|
|
222
262
|
const { feeds } = requireWaterxPackage(host);
|
|
223
263
|
for (const ticker of tickers) {
|
|
224
|
-
|
|
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) {
|
|
225
267
|
throw new Error(`No waterx_rule feed listed for ticker: ${ticker}`);
|
|
226
268
|
}
|
|
227
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
|
+
}
|