@waterx/sdk 4.3.0 → 4.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/cjs/src/oracle/aggregate.d.ts +4 -3
- package/dist/cjs/src/oracle/aggregate.js +7 -5
- package/dist/cjs/src/oracle/index.d.ts +3 -1
- package/dist/cjs/src/oracle/index.js +11 -1
- package/dist/cjs/src/oracle/price-update-rule.d.ts +31 -9
- package/dist/cjs/src/oracle/price-update-rule.js +20 -0
- package/dist/cjs/src/oracle/read-plane.js +11 -4
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +2 -1
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +25 -7
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +46 -19
- package/dist/cjs/src/oracle/rules/waterx-rule.js +4 -1
- 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 +1 -1
- package/dist/cjs/src/perp/client.js +11 -7
- package/dist/cjs/src/perp/config-view.js +7 -6
- package/dist/cjs/src/perp/index.d.ts +1 -1
- package/dist/cjs/src/perp/index.js +8 -4
- package/dist/cjs/src/perp/user/staking.js +2 -1
- 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/oracle/aggregate.d.ts +4 -3
- package/dist/src/oracle/aggregate.js +7 -5
- package/dist/src/oracle/index.d.ts +3 -1
- package/dist/src/oracle/index.js +8 -1
- package/dist/src/oracle/price-update-rule.d.ts +31 -9
- package/dist/src/oracle/price-update-rule.js +19 -0
- package/dist/src/oracle/read-plane.js +11 -4
- package/dist/src/oracle/rules/pyth-core-rule.js +2 -1
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +25 -7
- package/dist/src/oracle/rules/pyth-lazer-rule.js +46 -19
- package/dist/src/oracle/rules/waterx-rule.js +4 -1
- 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 +1 -1
- package/dist/src/perp/client.js +11 -7
- package/dist/src/perp/config-view.js +7 -6
- package/dist/src/perp/index.d.ts +1 -1
- package/dist/src/perp/index.js +1 -1
- package/dist/src/perp/user/staking.js +2 -1
- 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
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
|
|
5
5
|
* requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
|
|
6
6
|
* via the `pythApiKey` create option), verifies it ONCE on-chain via
|
|
7
|
-
* `pyth_lazer
|
|
8
|
-
* `Update` PTB value back through a `RuleUpdateHandle` for the feed
|
|
7
|
+
* `pyth_lazer`'s verify entry for that network (see `LAZER_INFRA`), and hands the
|
|
8
|
+
* resulting `Update` PTB value back through a `RuleUpdateHandle` for the feed
|
|
9
|
+
* calls.
|
|
9
10
|
*/
|
|
10
11
|
import { fromHex } from "@mysten/bcs";
|
|
12
|
+
import { ownEntry } from "../../utils/record.js";
|
|
11
13
|
import { assertRuleUpdateData, } from "../price-update-rule.js";
|
|
12
14
|
import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch } from "../update-fetch.js";
|
|
13
15
|
/**
|
|
@@ -20,20 +22,33 @@ import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch } from "../upd
|
|
|
20
22
|
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
21
23
|
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
22
24
|
* Lazer signers), so both networks share the production host.
|
|
23
|
-
* - `verifier_package` — the Sui package carrying
|
|
24
|
-
* `
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* - `verifier_package` / `verify_entry` — the Sui package carrying the verify
|
|
26
|
+
* call, and which entry to call. These track what `pyth_lazer_rule` binds on
|
|
27
|
+
* that network, so they move together:
|
|
28
|
+
* - **mainnet** — the v2 package `0xefbfd064…` and the **v2** entry. The rule
|
|
29
|
+
* was republished v2-bound after a 2026-08-05 mainnet probe: the ORIGINAL
|
|
30
|
+
* package `0x7b502c…` now aborts `EDifferentVersion` (`state::current_cap`)
|
|
31
|
+
* for any payload — the shared `State` has been migrated past that code —
|
|
32
|
+
* and its v1 entry aborts `EInvalidChannel` on `fixed_rate@1000ms`, the only
|
|
33
|
+
* channel WaterX's Pyth Pro grant permits.
|
|
34
|
+
* - **testnet** — still the original v1 publish, which has no `update_v2`
|
|
35
|
+
* module at all, so the v1 entry is the only one that exists there.
|
|
36
|
+
* Both entries take `(state, clock, bytes)` and accept the same `leEcdsa`
|
|
37
|
+
* payload. Values mirror the contract repo's `pyth_lazer_rule/Move.toml`
|
|
38
|
+
* published-at pins.
|
|
28
39
|
*/
|
|
29
40
|
export const LAZER_INFRA = {
|
|
30
41
|
MAINNET: {
|
|
31
42
|
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
32
43
|
verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
|
|
44
|
+
verify_entry: "parse_and_verify_le_ecdsa_update_v2",
|
|
45
|
+
channel: "fixed_rate@1000ms",
|
|
33
46
|
},
|
|
34
47
|
TESTNET: {
|
|
35
48
|
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
36
49
|
verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
|
|
50
|
+
verify_entry: "parse_and_verify_le_ecdsa_update",
|
|
51
|
+
channel: "fixed_rate@200ms",
|
|
37
52
|
},
|
|
38
53
|
};
|
|
39
54
|
/**
|
|
@@ -51,9 +66,15 @@ export const LAZER_INFRA = {
|
|
|
51
66
|
* and every xStock — are `min_channel: fixed_rate@200ms` (Lazer symbol
|
|
52
67
|
* registry, verified 2026-07-22: the same 29-feed batch 400s at
|
|
53
68
|
* `real_time`/`50ms` and serves 200 with the leEcdsa blob at `200ms`).
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
69
|
+
* The channel is therefore per-network (`LAZER_INFRA[network].channel`), and
|
|
70
|
+
* it is bounded from BOTH sides — by what the feeds publish and by what the
|
|
71
|
+
* grant allows:
|
|
72
|
+
* - **mainnet: `fixed_rate@1000ms`.** WaterX's Pyth Pro grant no longer
|
|
73
|
+
* permits anything faster ("Channel fixed_rate@200ms violates rate limit.
|
|
74
|
+
* Minimum allowed channel is 1000ms", measured 2026-08-05), and the
|
|
75
|
+
* mainnet rule is v2-bound, so it accepts that channel.
|
|
76
|
+
* - **testnet: `fixed_rate@200ms`.** Its rule is still v1-bound, and the v1
|
|
77
|
+
* on-chain `channel::from_u8` aborts on the 1000ms channel.
|
|
57
78
|
* - `formats: leEcdsa` + `jsonBinaryEncoding: hex` — the Sui verifier takes
|
|
58
79
|
* the `leEcdsa` framing; hex matches `fromHex` below.
|
|
59
80
|
*/
|
|
@@ -61,7 +82,6 @@ const LAZER_LATEST_PRICE_REQUEST = {
|
|
|
61
82
|
properties: ["price", "exponent", "confidence"],
|
|
62
83
|
formats: ["leEcdsa"],
|
|
63
84
|
jsonBinaryEncoding: "hex",
|
|
64
|
-
channel: "fixed_rate@200ms",
|
|
65
85
|
};
|
|
66
86
|
/**
|
|
67
87
|
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
@@ -103,7 +123,7 @@ function requireLazerPackage(host) {
|
|
|
103
123
|
* retry/timeout/Bearer policy as `fetchPriceFeedsUpdateData`, unified so
|
|
104
124
|
* both oracle sources fail the same way under upstream degradation.
|
|
105
125
|
*/
|
|
106
|
-
async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
|
|
126
|
+
async function fetchLazerSignedUpdate(endpoint, channel, apiKey, feedIds, fetchOpts) {
|
|
107
127
|
// joinEndpointPath preserves any base path on the endpoint — the same
|
|
108
128
|
// leading-slash `new URL` footgun that 404'd every feed on the Pyth Pro
|
|
109
129
|
// Hermes endpoint (see update-fetch.ts). Defensive here: the default
|
|
@@ -114,7 +134,7 @@ async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
|
|
|
114
134
|
res = await fetchWithPolicy(url.toString(), {
|
|
115
135
|
method: "POST",
|
|
116
136
|
headers: { "Content-Type": "application/json" },
|
|
117
|
-
body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST }),
|
|
137
|
+
body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST, channel }),
|
|
118
138
|
}, { apiKey, ...fetchOpts });
|
|
119
139
|
}
|
|
120
140
|
catch (err) {
|
|
@@ -162,7 +182,9 @@ export const PythLazerRule = {
|
|
|
162
182
|
// not fail per ticker as if only that feed were missing.
|
|
163
183
|
const { feeds } = requireLazerPackage(host);
|
|
164
184
|
const feedIds = tickers.map((ticker) => {
|
|
165
|
-
|
|
185
|
+
// ownEntry: a prototype-key ticker ("toString") must throw here as
|
|
186
|
+
// unlisted, not turn an inherited Function into a "feed id".
|
|
187
|
+
const feedId = ownEntry(feeds, ticker);
|
|
166
188
|
if (feedId === undefined) {
|
|
167
189
|
throw new Error(`No pyth_lazer_rule feed listed for ticker: ${ticker}`);
|
|
168
190
|
}
|
|
@@ -172,13 +194,13 @@ export const PythLazerRule = {
|
|
|
172
194
|
if (!apiKey) {
|
|
173
195
|
throw new LazerApiKeyMissingError();
|
|
174
196
|
}
|
|
175
|
-
const update = await fetchLazerSignedUpdate(LAZER_INFRA[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
|
|
197
|
+
const update = await fetchLazerSignedUpdate(LAZER_INFRA[host.network].endpoint, LAZER_INFRA[host.network].channel, apiKey, feedIds, host.pyth.fetch);
|
|
176
198
|
return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
|
|
177
199
|
},
|
|
178
200
|
/**
|
|
179
201
|
* A Lazer payload is ONE signed `leEcdsa` message covering every feed it was
|
|
180
202
|
* fetched for — verification is a single flat signature check over the whole
|
|
181
|
-
* message (`parse_and_verify_le_ecdsa_update
|
|
203
|
+
* message (one `parse_and_verify_le_ecdsa_update*` call, no per-feed cost), so the
|
|
182
204
|
* payload is indivisible: it can only be served whole. Returns the whole
|
|
183
205
|
* payload iff every requested ticker's integer feed id is packed in THIS
|
|
184
206
|
* payload's `feedIds`; any coverage gap (unlisted ticker, or a feed this
|
|
@@ -190,26 +212,31 @@ export const PythLazerRule = {
|
|
|
190
212
|
return null;
|
|
191
213
|
const packedFeedIds = new Set(payload.feedIds);
|
|
192
214
|
for (const ticker of tickers) {
|
|
193
|
-
const feedId = host.config.packages.pyth_lazer_rule?.feeds
|
|
215
|
+
const feedId = ownEntry(host.config.packages.pyth_lazer_rule?.feeds, ticker);
|
|
194
216
|
if (feedId === undefined || !packedFeedIds.has(feedId))
|
|
195
217
|
return null;
|
|
196
218
|
}
|
|
197
219
|
return { kind: "pyth_lazer_rule", payload };
|
|
198
220
|
},
|
|
199
221
|
/**
|
|
200
|
-
* Appends the single `parse_and_verify_le_ecdsa_update(state, clock, bytes)`
|
|
222
|
+
* Appends the single `parse_and_verify_le_ecdsa_update*(state, clock, bytes)`
|
|
201
223
|
* call — one secp256k1 signature check covering every feed in the payload —
|
|
202
224
|
* and returns its `Update` result as the handle the per-ticker feed leg
|
|
203
225
|
* consumes. `opts.cache` / `opts.feeSource` are Pyth-Core-specific and
|
|
204
226
|
* ignored (Lazer verification charges no update fee).
|
|
227
|
+
*
|
|
228
|
+
* The entry name comes from `LAZER_INFRA[network].verify_entry`: mainnet's
|
|
229
|
+
* rule binds `update_v2`, testnet's is still the v1 publish. Both take
|
|
230
|
+
* `(state, clock, bytes)` and accept the same `leEcdsa` payload.
|
|
205
231
|
*/
|
|
206
232
|
buildUpdateCalls(tx, host, data, _opts) {
|
|
207
233
|
const payload = assertRuleUpdateData(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
|
|
208
234
|
if (!payload)
|
|
209
235
|
return undefined;
|
|
210
236
|
const lazer = requireLazerPackage(host);
|
|
237
|
+
const infra = LAZER_INFRA[host.network];
|
|
211
238
|
const [update] = tx.moveCall({
|
|
212
|
-
target: `${
|
|
239
|
+
target: `${infra.verifier_package}::pyth_lazer::${infra.verify_entry}`,
|
|
213
240
|
arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
|
|
214
241
|
});
|
|
215
242
|
return { kind: "pyth_lazer_rule", update };
|
|
@@ -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
|
-
|
|
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
|
|
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";
|
package/dist/src/perp/client.js
CHANGED
|
@@ -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
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
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) =>
|
|
58
|
-
throw new Error(`oracleSource is REQUIRED and must name at least one
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 }));
|
package/dist/src/perp/index.d.ts
CHANGED
|
@@ -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,
|
|
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";
|
package/dist/src/perp/index.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
}
|
package/dist/src/utils/config.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|