@waterx/sdk 4.3.3 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/waterx-sdk-integration/SKILL.md +41 -35
- package/README.md +81 -77
- package/dist/cjs/src/account/config.d.ts +3 -5
- package/dist/cjs/src/account/funding/wormhole.d.ts +1 -2
- package/dist/cjs/src/account/funding/wormhole.js +1 -2
- package/dist/cjs/src/oracle/aggregate.d.ts +45 -61
- package/dist/cjs/src/oracle/aggregate.js +138 -138
- package/dist/cjs/src/oracle/config.d.ts +35 -49
- package/dist/cjs/src/oracle/config.js +1 -1
- package/dist/cjs/src/oracle/host.d.ts +14 -19
- package/dist/cjs/src/oracle/host.js +3 -3
- package/dist/cjs/src/oracle/index.d.ts +25 -16
- package/dist/cjs/src/oracle/index.js +79 -66
- package/dist/cjs/src/oracle/price-update-rule.d.ts +104 -66
- package/dist/cjs/src/oracle/price-update-rule.js +24 -16
- package/dist/cjs/src/oracle/pyth-pro-history.d.ts +36 -0
- package/dist/cjs/src/oracle/pyth-pro-history.js +41 -0
- package/dist/cjs/src/oracle/read-plane.d.ts +35 -53
- package/dist/cjs/src/oracle/read-plane.js +26 -48
- package/dist/cjs/src/oracle/read-prices.d.ts +105 -0
- package/dist/cjs/src/oracle/read-prices.js +162 -0
- package/dist/cjs/src/oracle/rule-registry.d.ts +12 -12
- package/dist/cjs/src/oracle/rule-registry.js +12 -14
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +40 -20
- package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +137 -12
- package/dist/cjs/src/oracle/rules/waterx-rule.js +318 -55
- package/dist/cjs/src/oracle/schedule.d.ts +109 -0
- package/dist/cjs/src/oracle/schedule.js +991 -0
- package/dist/cjs/src/oracle/source-list.d.ts +31 -27
- package/dist/cjs/src/oracle/source-list.js +43 -45
- package/dist/cjs/src/oracle/symbol-catalog.d.ts +54 -0
- package/dist/cjs/src/oracle/symbol-catalog.js +69 -0
- package/dist/cjs/src/oracle/update-fetch.d.ts +11 -15
- package/dist/cjs/src/oracle/update-fetch.js +12 -19
- package/dist/cjs/src/oracle/validate.d.ts +105 -0
- package/dist/cjs/src/oracle/validate.js +185 -0
- package/dist/cjs/src/oracle/weight-coverage.d.ts +79 -0
- package/dist/cjs/src/oracle/weight-coverage.js +173 -0
- package/dist/cjs/src/perp/client.d.ts +35 -52
- package/dist/cjs/src/perp/client.js +44 -17
- package/dist/cjs/src/perp/config-view.d.ts +5 -14
- package/dist/cjs/src/perp/config-view.js +5 -16
- package/dist/cjs/src/perp/config.d.ts +3 -3
- package/dist/cjs/src/perp/config.js +11 -13
- package/dist/cjs/src/perp/index.d.ts +4 -6
- package/dist/cjs/src/perp/index.js +35 -18
- package/dist/cjs/src/perp/tx-builders/common.d.ts +86 -48
- package/dist/cjs/src/perp/tx-builders/common.js +115 -47
- package/dist/cjs/src/perp/tx-builders/wlp.d.ts +3 -14
- package/dist/cjs/src/perp/tx-builders/wlp.js +6 -35
- package/dist/cjs/src/perp/tx-builders.d.ts +6 -5
- package/dist/cjs/src/perp/tx-builders.js +16 -10
- package/dist/cjs/src/unified-client.d.ts +8 -34
- package/dist/cjs/src/unified-client.js +3 -4
- package/dist/cjs/src/utils/config.d.ts +0 -8
- package/dist/cjs/src/utils/config.js +5 -13
- package/dist/src/account/config.d.ts +3 -5
- package/dist/src/account/funding/wormhole.d.ts +1 -2
- package/dist/src/account/funding/wormhole.js +1 -2
- package/dist/src/oracle/aggregate.d.ts +45 -61
- package/dist/src/oracle/aggregate.js +138 -137
- package/dist/src/oracle/config.d.ts +35 -49
- package/dist/src/oracle/config.js +1 -1
- package/dist/src/oracle/host.d.ts +14 -19
- package/dist/src/oracle/host.js +3 -3
- package/dist/src/oracle/index.d.ts +25 -16
- package/dist/src/oracle/index.js +55 -50
- package/dist/src/oracle/price-update-rule.d.ts +104 -66
- package/dist/src/oracle/price-update-rule.js +23 -16
- package/dist/src/oracle/pyth-pro-history.d.ts +36 -0
- package/dist/src/oracle/pyth-pro-history.js +38 -0
- package/dist/src/oracle/read-plane.d.ts +35 -53
- package/dist/src/oracle/read-plane.js +25 -47
- package/dist/src/oracle/read-prices.d.ts +105 -0
- package/dist/src/oracle/read-prices.js +156 -0
- package/dist/src/oracle/rule-registry.d.ts +12 -12
- package/dist/src/oracle/rule-registry.js +12 -14
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
- package/dist/src/oracle/rules/pyth-lazer-rule.js +39 -20
- package/dist/src/oracle/rules/waterx-rule.d.ts +137 -12
- package/dist/src/oracle/rules/waterx-rule.js +314 -56
- package/dist/src/oracle/schedule.d.ts +109 -0
- package/dist/src/oracle/schedule.js +985 -0
- package/dist/src/oracle/source-list.d.ts +31 -27
- package/dist/src/oracle/source-list.js +42 -43
- package/dist/src/oracle/symbol-catalog.d.ts +54 -0
- package/dist/src/oracle/symbol-catalog.js +65 -0
- package/dist/src/oracle/update-fetch.d.ts +11 -15
- package/dist/src/oracle/update-fetch.js +12 -18
- package/dist/src/oracle/validate.d.ts +105 -0
- package/dist/src/oracle/validate.js +177 -0
- package/dist/src/oracle/weight-coverage.d.ts +79 -0
- package/dist/src/oracle/weight-coverage.js +166 -0
- package/dist/src/perp/client.d.ts +35 -52
- package/dist/src/perp/client.js +45 -18
- package/dist/src/perp/config-view.d.ts +5 -14
- package/dist/src/perp/config-view.js +5 -16
- package/dist/src/perp/config.d.ts +3 -3
- package/dist/src/perp/config.js +11 -13
- package/dist/src/perp/index.d.ts +4 -6
- package/dist/src/perp/index.js +11 -5
- package/dist/src/perp/tx-builders/common.d.ts +86 -48
- package/dist/src/perp/tx-builders/common.js +114 -48
- package/dist/src/perp/tx-builders/wlp.d.ts +3 -14
- package/dist/src/perp/tx-builders/wlp.js +6 -35
- package/dist/src/perp/tx-builders.d.ts +6 -5
- package/dist/src/perp/tx-builders.js +15 -6
- package/dist/src/unified-client.d.ts +8 -34
- package/dist/src/unified-client.js +3 -4
- package/dist/src/utils/config.d.ts +0 -8
- package/dist/src/utils/config.js +5 -12
- package/package.json +1 -1
- package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
- package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -17
- package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
- package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -113
- package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
- package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -31
- package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
- package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.js +0 -214
- package/dist/cjs/src/oracle/pyth.d.ts +0 -227
- package/dist/cjs/src/oracle/pyth.js +0 -652
- package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +0 -15
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +0 -100
- package/dist/cjs/src/oracle/rules/pyth-rule.d.ts +0 -11
- package/dist/cjs/src/oracle/rules/pyth-rule.js +0 -29
- package/dist/cjs/src/oracle/rules/sponsor.d.ts +0 -32
- package/dist/cjs/src/oracle/rules/sponsor.js +0 -56
- package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
- package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -14
- package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
- package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -73
- package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
- package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -27
- package/dist/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
- package/dist/src/generated/waterx_pyth_rule/pyth_rule.js +0 -170
- package/dist/src/oracle/pyth.d.ts +0 -227
- package/dist/src/oracle/pyth.js +0 -638
- package/dist/src/oracle/rules/pyth-core-rule.d.ts +0 -15
- package/dist/src/oracle/rules/pyth-core-rule.js +0 -97
- package/dist/src/oracle/rules/pyth-rule.d.ts +0 -11
- package/dist/src/oracle/rules/pyth-rule.js +0 -26
- package/dist/src/oracle/rules/sponsor.d.ts +0 -32
- package/dist/src/oracle/rules/sponsor.js +0 -52
|
@@ -1,68 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* `read-plane.ts` — per-source READ-plane resolution: which of a caller's
|
|
4
|
-
* tickers a source can PRICE off-chain, and with which ids.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (FE/BE price facades) resolve through this instead
|
|
12
|
-
*
|
|
4
|
+
* tickers a source can PRICE off-chain, and with which ids. Every source
|
|
5
|
+
* reads through ITS OWN feeds namespace — `pyth_lazer_rule` via its integer
|
|
6
|
+
* ids on the Lazer HTTP API, `waterx_rule` via its tickers on the
|
|
7
|
+
* quote-center — so a source's write set and read set coincide by
|
|
8
|
+
* construction. (Until 5.0.0 Lazer reads borrowed `pyth_rule.feeds` hex ids
|
|
9
|
+
* on a Hermes-compatible endpoint; that cross-block dependency, the whole
|
|
10
|
+
* hermes plane, and the `unreadable` diagnostic are gone with the `pyth_rule`
|
|
11
|
+
* retirement.) Consumers (FE/BE price facades) resolve through this instead
|
|
12
|
+
* of hardcoding feed namespaces, and execute plans via the sibling
|
|
13
|
+
* `read-prices.ts` executors.
|
|
13
14
|
*/
|
|
14
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.
|
|
16
|
+
exports.readPlanTickers = readPlanTickers;
|
|
16
17
|
exports.resolveOracleReadPlan = resolveOracleReadPlan;
|
|
17
18
|
const record_ts_1 = require("../utils/record.js");
|
|
18
|
-
|
|
19
|
+
/** The tickers a resolved plan can actually serve, regardless of plane. */
|
|
20
|
+
function readPlanTickers(plan) {
|
|
21
|
+
return plan.plane === "quote_center" ? plan.tickers : [...plan.feedIdByTicker.keys()];
|
|
22
|
+
}
|
|
19
23
|
/**
|
|
20
24
|
* Resolve `source`'s read plan for `tickers`. Pure config lookup — no
|
|
21
|
-
* network, no endpoint resolution (endpoints come from
|
|
22
|
-
* `
|
|
23
|
-
* own env). A ticker absent from the returned plan is simply not servable by
|
|
24
|
-
* THIS source's read plane — callers decide how to degrade (typically: ask
|
|
25
|
-
* the next source in their `ORACLE_SOURCE` list, then omit).
|
|
26
|
-
*/
|
|
27
|
-
/**
|
|
28
|
-
* The Hermes-compatible REST base a deployment's hermes-plane read plans
|
|
29
|
-
* execute against — the endpoint half of the read contract
|
|
30
|
-
* ({@link resolveOracleReadPlan} is the ids half):
|
|
31
|
-
*
|
|
32
|
-
* - `pyth_rule` in the fed set → the Core source's own keyless endpoint
|
|
33
|
-
* (`pythCoreHermesEndpoint(network)`).
|
|
34
|
-
* - otherwise → `override` when the deployment set one (a proxy or
|
|
35
|
-
* self-hosted mirror), else the documented Pyth Pro base
|
|
36
|
-
* (`pythProHermesEndpoint()` — identical for every subscriber; auth is the
|
|
37
|
-
* caller's `pythApiKey` Bearer, not a per-deployment URL).
|
|
38
|
-
*
|
|
39
|
-
* Total — never throws, never falls back Core-ward: a fed set without
|
|
40
|
-
* `pyth_rule` reads Pro (or the override), full stop.
|
|
25
|
+
* network, no endpoint resolution (endpoints come from `LAZER_INFRA` /
|
|
26
|
+
* `waterxQuoteCenterEndpoint` / the deployment's own env).
|
|
41
27
|
*/
|
|
42
|
-
function resolveHermesReadEndpoint(network, sources, override) {
|
|
43
|
-
if (sources.includes("pyth_rule"))
|
|
44
|
-
return (0, pyth_ts_1.pythCoreHermesEndpoint)(network);
|
|
45
|
-
return override ?? (0, pyth_ts_1.pythProHermesEndpoint)();
|
|
46
|
-
}
|
|
47
28
|
function resolveOracleReadPlan(host, source, tickers) {
|
|
48
29
|
switch (source) {
|
|
49
|
-
case "pyth_rule":
|
|
50
30
|
case "pyth_lazer_rule": {
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
31
|
+
// Lazer reads through its OWN integer ids — the same
|
|
32
|
+
// `pyth_lazer_rule.feeds` entries its write leg uses — so the read set
|
|
33
|
+
// is exactly the write set. All ticker lookups go through `ownEntry`
|
|
34
|
+
// (own-keys-only): a ticker named like an Object.prototype key
|
|
35
|
+
// ("toString", "constructor", …) must read as not-listed, not as an
|
|
36
|
+
// inherited Function.
|
|
37
|
+
const feeds = host.config.packages.pyth_lazer_rule?.feeds;
|
|
55
38
|
const feedIdByTicker = new Map();
|
|
56
39
|
for (const ticker of tickers) {
|
|
57
|
-
const feedId = (0, record_ts_1.ownEntry)(
|
|
40
|
+
const feedId = (0, record_ts_1.ownEntry)(feeds, ticker);
|
|
58
41
|
if (feedId !== undefined)
|
|
59
42
|
feedIdByTicker.set(ticker, feedId);
|
|
60
43
|
}
|
|
61
|
-
|
|
62
|
-
// is always empty; for lazer it is exactly the hex-entry gap.
|
|
63
|
-
const writeFeeds = source === "pyth_lazer_rule" ? host.config.packages.pyth_lazer_rule?.feeds : hexFeeds;
|
|
64
|
-
const unreadable = tickers.filter((ticker) => (0, record_ts_1.ownEntry)(writeFeeds, ticker) !== undefined && !feedIdByTicker.has(ticker));
|
|
65
|
-
return { plane: "hermes", feedIdByTicker, unreadable };
|
|
44
|
+
return { plane: "lazer", feedIdByTicker };
|
|
66
45
|
}
|
|
67
46
|
case "waterx_rule": {
|
|
68
47
|
// Absent feeds block ⇒ serves nothing (see the OracleReadPlan doc) —
|
|
@@ -74,7 +53,6 @@ function resolveOracleReadPlan(host, source, tickers) {
|
|
|
74
53
|
return {
|
|
75
54
|
plane: "quote_center",
|
|
76
55
|
tickers: tickers.filter((ticker) => (0, record_ts_1.ownEntry)(feeds, ticker) !== undefined),
|
|
77
|
-
unreadable: [],
|
|
78
56
|
};
|
|
79
57
|
}
|
|
80
58
|
default: {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read-prices.ts` — the READ-plane executors for the two live sources: parsed
|
|
3
|
+
* (display-grade) prices from the Lazer HTTP API and the WaterX quote-center.
|
|
4
|
+
* The sibling `read-plane.ts` resolves WHICH tickers a source can price and
|
|
5
|
+
* with which ids; these functions execute that plan. Consumers (FE/BE price
|
|
6
|
+
* facades) fold onto these instead of hand-rolling per-source fetch + decode —
|
|
7
|
+
* the price DECODING here is the one place each source's wire scaling is
|
|
8
|
+
* interpreted (the retired hermes decode was deleted, not re-homed).
|
|
9
|
+
*
|
|
10
|
+
* Both executors return a Map keyed the way the plan is keyed (integer Lazer
|
|
11
|
+
* feed id / ticker) of {@link OraclePriceEntry} — plain numbers, for display
|
|
12
|
+
* and freshness policy, NEVER for rebuilding signed payloads (tx-builds carry
|
|
13
|
+
* the exact signed bytes through `refreshOraclePrices`).
|
|
14
|
+
*/
|
|
15
|
+
import type { Network } from "../constants.ts";
|
|
16
|
+
import type { PythFetchPolicy } from "./config.ts";
|
|
17
|
+
import type { FetchPolicy } from "./update-fetch.ts";
|
|
18
|
+
/**
|
|
19
|
+
* One decoded read-plane price. `price` / `conf` are display-grade floats in
|
|
20
|
+
* quote units (USD); `publishTimeMs` is the feed's own publish timestamp in
|
|
21
|
+
* ms — the input to freshness policy (`isFreshWaterxEntry`, consumers'
|
|
22
|
+
* max-age gates).
|
|
23
|
+
*/
|
|
24
|
+
export type OraclePriceEntry = {
|
|
25
|
+
price: number;
|
|
26
|
+
publishTimeMs: number;
|
|
27
|
+
conf: number;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Thrown by {@link readLazerPrices} on a Lazer `403` — the key's grant does
|
|
31
|
+
* not cover one of the requested feeds (Lazer rejects the WHOLE batch). The
|
|
32
|
+
* message carries the endpoint's own PLAIN-TEXT body verbatim (e.g.
|
|
33
|
+
* `Not entitled: feed 327 (no grant accepts this feed (asset type 'fx',
|
|
34
|
+
* instrument type 'spot'))`) — it names the offending feed and grant reason
|
|
35
|
+
* better than anything the SDK could synthesize. `instanceof`-able so a
|
|
36
|
+
* consumer can drop unentitled feeds and retry rather than string-matching.
|
|
37
|
+
*/
|
|
38
|
+
export declare class LazerNotEntitledError extends Error {
|
|
39
|
+
/** The integer Lazer feed ids the rejected request asked for. */
|
|
40
|
+
readonly feedIds: number[];
|
|
41
|
+
constructor(body: string, feedIds: number[]);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read parsed prices for `feedIds` (integer Lazer ids — the `"lazer"` arm of
|
|
45
|
+
* `resolveOracleReadPlan`) via `POST /v1/latest_price`.
|
|
46
|
+
*
|
|
47
|
+
* - `network` is REQUIRED: it selects the Lazer endpoint AND the channel, and
|
|
48
|
+
* the integer feed ids in a plan are network-specific, so defaulting it
|
|
49
|
+
* would silently read mainnet infra with testnet ids. Every caller already
|
|
50
|
+
* holds one (`resolveOracleReadPlan` takes the same host).
|
|
51
|
+
* - `channel` defaults to `LAZER_INFRA[network].channel` (the same channel
|
|
52
|
+
* the write leg uses — bounded by the feeds' `min_channel` AND the key's
|
|
53
|
+
* grant). A `400` naming an unsupported/rate-limited channel propagates as
|
|
54
|
+
* a plain error with the body attached — that is an operator
|
|
55
|
+
* misconfiguration, not a retry case.
|
|
56
|
+
* - Decoding: `price = Number(price) * 10 ** exponent` (the wire `price` is a
|
|
57
|
+
* decimal STRING), `conf = Number(confidence) * 10 ** exponent`,
|
|
58
|
+
* `publishTimeMs = feedUpdateTimestamp / 1000` (the wire value is µs). An
|
|
59
|
+
* entry with NO `feedUpdateTimestamp` is SKIPPED — never timestamped off
|
|
60
|
+
* the response's batch `timestampUs`, which says when Lazer answered, not
|
|
61
|
+
* when that feed last printed (a closed-market feed would masquerade as
|
|
62
|
+
* fresh).
|
|
63
|
+
* - `403` → {@link LazerNotEntitledError} (whole batch rejected; body text
|
|
64
|
+
* verbatim).
|
|
65
|
+
*/
|
|
66
|
+
export declare function readLazerPrices(opts: {
|
|
67
|
+
apiKey: string;
|
|
68
|
+
feedIds: number[];
|
|
69
|
+
network: Network;
|
|
70
|
+
channel?: string;
|
|
71
|
+
fetch?: PythFetchPolicy;
|
|
72
|
+
}): Promise<Map<number, OraclePriceEntry>>;
|
|
73
|
+
/**
|
|
74
|
+
* Read prices for `tickers` (the `"quote_center"` arm of
|
|
75
|
+
* `resolveOracleReadPlan`) through {@link pullWaterxQuotes}, the rule-owned
|
|
76
|
+
* route ladder the write path uses: per-symbol Merkle leaves by default, the
|
|
77
|
+
* batch envelope only against a quote-center with no leaf route. Public read,
|
|
78
|
+
* no auth, same retry/timeout policy as every oracle fetch.
|
|
79
|
+
*
|
|
80
|
+
* Going through the shared ladder is what keeps the read plane from drifting
|
|
81
|
+
* off the write plane, and it is why reads do not pull a whole-registry
|
|
82
|
+
* envelope: that is one signature over every symbol, so a plane polling a
|
|
83
|
+
* handful of them would transfer and bigint-revive the lot on every tick.
|
|
84
|
+
*
|
|
85
|
+
* Decoding per item: `price = Number(price_n) / Number(price_scale)` and
|
|
86
|
+
* `conf = Number(confidence_n) / Number(confidence_scale)`, each `0` when its
|
|
87
|
+
* scale is `0` (a zero divisor is "no value", not `Infinity`);
|
|
88
|
+
* `publishTimeMs = Number(price_timestamp_ms)`. Reads share the write path's
|
|
89
|
+
* fetch — a mispointed endpoint fails the same intent/shape checks tx-builds
|
|
90
|
+
* fail — but this NEVER hands its signed data to a tx-build: the on-chain
|
|
91
|
+
* per-symbol replay guard (F-014) burns one submission per signed timestamp,
|
|
92
|
+
* and reads must not race trades for it.
|
|
93
|
+
*/
|
|
94
|
+
export declare function readQuoteCenterPrices(opts: {
|
|
95
|
+
endpoint: string;
|
|
96
|
+
/**
|
|
97
|
+
* Oracle TICKERS, per the repo convention — this is the consumer boundary,
|
|
98
|
+
* fed directly by `readPlanTickers(plan)`, and naming the same values
|
|
99
|
+
* `symbols` here made every call site cross a rename. Below this point the
|
|
100
|
+
* wire helpers keep `symbols`: that mirrors the literal `?symbols=` query
|
|
101
|
+
* parameter and the `symbol` field the quote-center returns.
|
|
102
|
+
*/
|
|
103
|
+
tickers: string[];
|
|
104
|
+
fetch?: FetchPolicy;
|
|
105
|
+
}): Promise<Map<string, OraclePriceEntry>>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `read-prices.ts` — the READ-plane executors for the two live sources: parsed
|
|
4
|
+
* (display-grade) prices from the Lazer HTTP API and the WaterX quote-center.
|
|
5
|
+
* The sibling `read-plane.ts` resolves WHICH tickers a source can price and
|
|
6
|
+
* with which ids; these functions execute that plan. Consumers (FE/BE price
|
|
7
|
+
* facades) fold onto these instead of hand-rolling per-source fetch + decode —
|
|
8
|
+
* the price DECODING here is the one place each source's wire scaling is
|
|
9
|
+
* interpreted (the retired hermes decode was deleted, not re-homed).
|
|
10
|
+
*
|
|
11
|
+
* Both executors return a Map keyed the way the plan is keyed (integer Lazer
|
|
12
|
+
* feed id / ticker) of {@link OraclePriceEntry} — plain numbers, for display
|
|
13
|
+
* and freshness policy, NEVER for rebuilding signed payloads (tx-builds carry
|
|
14
|
+
* the exact signed bytes through `refreshOraclePrices`).
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.LazerNotEntitledError = void 0;
|
|
18
|
+
exports.readLazerPrices = readLazerPrices;
|
|
19
|
+
exports.readQuoteCenterPrices = readQuoteCenterPrices;
|
|
20
|
+
const pyth_lazer_rule_ts_1 = require("./rules/pyth-lazer-rule.js");
|
|
21
|
+
const waterx_rule_ts_1 = require("./rules/waterx-rule.js");
|
|
22
|
+
/**
|
|
23
|
+
* Thrown by {@link readLazerPrices} on a Lazer `403` — the key's grant does
|
|
24
|
+
* not cover one of the requested feeds (Lazer rejects the WHOLE batch). The
|
|
25
|
+
* message carries the endpoint's own PLAIN-TEXT body verbatim (e.g.
|
|
26
|
+
* `Not entitled: feed 327 (no grant accepts this feed (asset type 'fx',
|
|
27
|
+
* instrument type 'spot'))`) — it names the offending feed and grant reason
|
|
28
|
+
* better than anything the SDK could synthesize. `instanceof`-able so a
|
|
29
|
+
* consumer can drop unentitled feeds and retry rather than string-matching.
|
|
30
|
+
*/
|
|
31
|
+
class LazerNotEntitledError extends Error {
|
|
32
|
+
/** The integer Lazer feed ids the rejected request asked for. */
|
|
33
|
+
feedIds;
|
|
34
|
+
constructor(body, feedIds) {
|
|
35
|
+
super(`Lazer price read not entitled: ${body}`);
|
|
36
|
+
this.name = "LazerNotEntitledError";
|
|
37
|
+
this.feedIds = [...feedIds];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.LazerNotEntitledError = LazerNotEntitledError;
|
|
41
|
+
/**
|
|
42
|
+
* The parsed-read request pins, mirroring the live-probed Lazer contract:
|
|
43
|
+
* `formats: []` is REQUIRED — an ABSENT `formats` is rejected while an empty
|
|
44
|
+
* array means "parsed only, no signed blob", which is exactly what a read
|
|
45
|
+
* plane wants (no signature bytes to pay bandwidth for). `properties` pins
|
|
46
|
+
* the fields the decoder below consumes; `marketSession` rides along for
|
|
47
|
+
* consumers that surface venue state.
|
|
48
|
+
*/
|
|
49
|
+
const LAZER_PARSED_READ_REQUEST = {
|
|
50
|
+
properties: ["price", "exponent", "confidence", "feedUpdateTimestamp", "marketSession"],
|
|
51
|
+
formats: [],
|
|
52
|
+
parsed: true,
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Read parsed prices for `feedIds` (integer Lazer ids — the `"lazer"` arm of
|
|
56
|
+
* `resolveOracleReadPlan`) via `POST /v1/latest_price`.
|
|
57
|
+
*
|
|
58
|
+
* - `network` is REQUIRED: it selects the Lazer endpoint AND the channel, and
|
|
59
|
+
* the integer feed ids in a plan are network-specific, so defaulting it
|
|
60
|
+
* would silently read mainnet infra with testnet ids. Every caller already
|
|
61
|
+
* holds one (`resolveOracleReadPlan` takes the same host).
|
|
62
|
+
* - `channel` defaults to `LAZER_INFRA[network].channel` (the same channel
|
|
63
|
+
* the write leg uses — bounded by the feeds' `min_channel` AND the key's
|
|
64
|
+
* grant). A `400` naming an unsupported/rate-limited channel propagates as
|
|
65
|
+
* a plain error with the body attached — that is an operator
|
|
66
|
+
* misconfiguration, not a retry case.
|
|
67
|
+
* - Decoding: `price = Number(price) * 10 ** exponent` (the wire `price` is a
|
|
68
|
+
* decimal STRING), `conf = Number(confidence) * 10 ** exponent`,
|
|
69
|
+
* `publishTimeMs = feedUpdateTimestamp / 1000` (the wire value is µs). An
|
|
70
|
+
* entry with NO `feedUpdateTimestamp` is SKIPPED — never timestamped off
|
|
71
|
+
* the response's batch `timestampUs`, which says when Lazer answered, not
|
|
72
|
+
* when that feed last printed (a closed-market feed would masquerade as
|
|
73
|
+
* fresh).
|
|
74
|
+
* - `403` → {@link LazerNotEntitledError} (whole batch rejected; body text
|
|
75
|
+
* verbatim).
|
|
76
|
+
*/
|
|
77
|
+
async function readLazerPrices(opts) {
|
|
78
|
+
const out = new Map();
|
|
79
|
+
if (opts.feedIds.length === 0)
|
|
80
|
+
return out;
|
|
81
|
+
const infra = pyth_lazer_rule_ts_1.LAZER_INFRA[opts.network];
|
|
82
|
+
// Same POST transport the rule's write leg uses (URL join, method, headers,
|
|
83
|
+
// Bearer + retry policy) — only the request pins and the decoding below are
|
|
84
|
+
// read-specific.
|
|
85
|
+
const res = await (0, pyth_lazer_rule_ts_1.postLazerLatestPrice)(infra.endpoint, opts.channel ?? infra.channel, opts.apiKey, opts.feedIds, LAZER_PARSED_READ_REQUEST, opts.fetch);
|
|
86
|
+
if (res.status === 403) {
|
|
87
|
+
// Plain-text body naming the unentitled feed + grant reason — verbatim.
|
|
88
|
+
throw new LazerNotEntitledError((await res.text()).trim(), opts.feedIds);
|
|
89
|
+
}
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
// Includes the 400 channel rejections ("Feeds do not support channel …" /
|
|
92
|
+
// rate-limit minimum-channel) — deterministic misconfigurations that must
|
|
93
|
+
// reach the operator with the endpoint's own wording.
|
|
94
|
+
throw new Error(`Lazer price read failed: ${res.status} ${await res.text()}`);
|
|
95
|
+
}
|
|
96
|
+
const json = (await res.json());
|
|
97
|
+
for (const feed of json.parsed?.priceFeeds ?? []) {
|
|
98
|
+
if (typeof feed.priceFeedId !== "number" ||
|
|
99
|
+
feed.price === undefined ||
|
|
100
|
+
typeof feed.exponent !== "number" ||
|
|
101
|
+
// No per-feed timestamp ⇒ no honest freshness claim ⇒ skip (see doc).
|
|
102
|
+
typeof feed.feedUpdateTimestamp !== "number") {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const scale = 10 ** feed.exponent;
|
|
106
|
+
const price = Number(feed.price) * scale;
|
|
107
|
+
const conf = typeof feed.confidence === "number" ? feed.confidence * scale : 0;
|
|
108
|
+
// A non-finite decode is a MISSING price, not a price. `Number("abc")` is
|
|
109
|
+
// NaN and an absurd exponent overflows to Infinity; either would flow
|
|
110
|
+
// straight into a consumer's staleness/deviation maths, where NaN silently
|
|
111
|
+
// fails every comparison it appears in. Skipping matches how a feed with no
|
|
112
|
+
// timestamp is handled above — omitted, so the caller sees it as unserved.
|
|
113
|
+
if (!Number.isFinite(price) || !Number.isFinite(conf))
|
|
114
|
+
continue;
|
|
115
|
+
out.set(feed.priceFeedId, {
|
|
116
|
+
price,
|
|
117
|
+
conf,
|
|
118
|
+
publishTimeMs: feed.feedUpdateTimestamp / 1000,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read prices for `tickers` (the `"quote_center"` arm of
|
|
125
|
+
* `resolveOracleReadPlan`) through {@link pullWaterxQuotes}, the rule-owned
|
|
126
|
+
* route ladder the write path uses: per-symbol Merkle leaves by default, the
|
|
127
|
+
* batch envelope only against a quote-center with no leaf route. Public read,
|
|
128
|
+
* no auth, same retry/timeout policy as every oracle fetch.
|
|
129
|
+
*
|
|
130
|
+
* Going through the shared ladder is what keeps the read plane from drifting
|
|
131
|
+
* off the write plane, and it is why reads do not pull a whole-registry
|
|
132
|
+
* envelope: that is one signature over every symbol, so a plane polling a
|
|
133
|
+
* handful of them would transfer and bigint-revive the lot on every tick.
|
|
134
|
+
*
|
|
135
|
+
* Decoding per item: `price = Number(price_n) / Number(price_scale)` and
|
|
136
|
+
* `conf = Number(confidence_n) / Number(confidence_scale)`, each `0` when its
|
|
137
|
+
* scale is `0` (a zero divisor is "no value", not `Infinity`);
|
|
138
|
+
* `publishTimeMs = Number(price_timestamp_ms)`. Reads share the write path's
|
|
139
|
+
* fetch — a mispointed endpoint fails the same intent/shape checks tx-builds
|
|
140
|
+
* fail — but this NEVER hands its signed data to a tx-build: the on-chain
|
|
141
|
+
* per-symbol replay guard (F-014) burns one submission per signed timestamp,
|
|
142
|
+
* and reads must not race trades for it.
|
|
143
|
+
*/
|
|
144
|
+
async function readQuoteCenterPrices(opts) {
|
|
145
|
+
const out = new Map();
|
|
146
|
+
if (opts.tickers.length === 0)
|
|
147
|
+
return out;
|
|
148
|
+
const { items } = await (0, waterx_rule_ts_1.pullWaterxQuotes)(opts.endpoint, opts.tickers, opts.fetch);
|
|
149
|
+
const requested = new Set(opts.tickers);
|
|
150
|
+
for (const item of items) {
|
|
151
|
+
if (!requested.has(item.symbol))
|
|
152
|
+
continue;
|
|
153
|
+
out.set(item.symbol, {
|
|
154
|
+
price: item.price_scale === 0n ? 0 : Number(item.price_n) / Number(item.price_scale),
|
|
155
|
+
conf: item.confidence_scale === 0n
|
|
156
|
+
? 0
|
|
157
|
+
: Number(item.confidence_n) / Number(item.confidence_scale),
|
|
158
|
+
publishTimeMs: Number(item.price_timestamp_ms),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
@@ -5,26 +5,26 @@
|
|
|
5
5
|
* every `host.oracleSources` entry through it, and consumers (e.g. a BE
|
|
6
6
|
* prefetch cache keying per source) import `resolveOracleRule` rather than
|
|
7
7
|
* hand-mirroring this map. Selection is driven purely by the value passed in
|
|
8
|
-
* (ultimately `OracleHost.oracleSources`, the
|
|
8
|
+
* (ultimately `OracleHost.oracleSources`, derived from the deployment config
|
|
9
9
|
* option) — never by a config JSON `enabled` flag and never by `process.env`.
|
|
10
10
|
*
|
|
11
11
|
* Each source is self-contained: it owns its own infra + config and does NOT
|
|
12
|
-
* back-stop any other source.
|
|
13
|
-
* (`
|
|
14
|
-
* signed
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* back-stop any other source. Two are registered: `pyth_lazer_rule`
|
|
13
|
+
* (`PythLazerRule`, Lazer signed updates) and `waterx_rule` (`WaterxRule`,
|
|
14
|
+
* first-party quote-center ed25519 signed batches). Resolving a source with
|
|
15
|
+
* no registered rule throws a clear `OracleSourceNotImplemented` error. There
|
|
16
|
+
* is deliberately no cross-source fallback and no client-creation config
|
|
17
|
+
* guard: selecting a source whose feeds are absent is not an error at init —
|
|
18
|
+
* it surfaces at tx-build time for the specific tickers that source can't
|
|
19
|
+
* serve (see `refreshOraclePrices`).
|
|
20
20
|
*/
|
|
21
21
|
import type { OracleSource, PriceUpdateRule } from "./price-update-rule.ts";
|
|
22
22
|
/**
|
|
23
23
|
* Thrown by {@link resolveOracleRule} when `source` has no `PriceUpdateRule`
|
|
24
24
|
* registered in either the production registry or a test's `overrides` map.
|
|
25
|
-
* `instanceof`-able (mirrors `
|
|
26
|
-
* so a consumer can branch on the failure type
|
|
27
|
-
* string-matching `error.message`.
|
|
25
|
+
* `instanceof`-able (mirrors `LazerApiKeyMissingError` in
|
|
26
|
+
* `rules/pyth-lazer-rule.ts`) so a consumer can branch on the failure type
|
|
27
|
+
* directly instead of string-matching `error.message`.
|
|
28
28
|
*/
|
|
29
29
|
export declare class OracleSourceNotImplementedError extends Error {
|
|
30
30
|
/** The unregistered `OracleSource` that was requested. */
|
|
@@ -6,23 +6,22 @@
|
|
|
6
6
|
* every `host.oracleSources` entry through it, and consumers (e.g. a BE
|
|
7
7
|
* prefetch cache keying per source) import `resolveOracleRule` rather than
|
|
8
8
|
* hand-mirroring this map. Selection is driven purely by the value passed in
|
|
9
|
-
* (ultimately `OracleHost.oracleSources`, the
|
|
9
|
+
* (ultimately `OracleHost.oracleSources`, derived from the deployment config
|
|
10
10
|
* option) — never by a config JSON `enabled` flag and never by `process.env`.
|
|
11
11
|
*
|
|
12
12
|
* Each source is self-contained: it owns its own infra + config and does NOT
|
|
13
|
-
* back-stop any other source.
|
|
14
|
-
* (`
|
|
15
|
-
* signed
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
13
|
+
* back-stop any other source. Two are registered: `pyth_lazer_rule`
|
|
14
|
+
* (`PythLazerRule`, Lazer signed updates) and `waterx_rule` (`WaterxRule`,
|
|
15
|
+
* first-party quote-center ed25519 signed batches). Resolving a source with
|
|
16
|
+
* no registered rule throws a clear `OracleSourceNotImplemented` error. There
|
|
17
|
+
* is deliberately no cross-source fallback and no client-creation config
|
|
18
|
+
* guard: selecting a source whose feeds are absent is not an error at init —
|
|
19
|
+
* it surfaces at tx-build time for the specific tickers that source can't
|
|
20
|
+
* serve (see `refreshOraclePrices`).
|
|
21
21
|
*/
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.OracleSourceNotImplementedError = void 0;
|
|
24
24
|
exports.resolveOracleRule = resolveOracleRule;
|
|
25
|
-
const pyth_core_rule_ts_1 = require("./rules/pyth-core-rule.js");
|
|
26
25
|
const pyth_lazer_rule_ts_1 = require("./rules/pyth-lazer-rule.js");
|
|
27
26
|
const waterx_rule_ts_1 = require("./rules/waterx-rule.js");
|
|
28
27
|
/**
|
|
@@ -30,16 +29,15 @@ const waterx_rule_ts_1 = require("./rules/waterx-rule.js");
|
|
|
30
29
|
* `resolveOracleRule`'s `overrides` param instead of mutating this.
|
|
31
30
|
*/
|
|
32
31
|
const DEFAULT_RULES = Object.freeze({
|
|
33
|
-
pyth_rule: pyth_core_rule_ts_1.PythCoreRule,
|
|
34
32
|
pyth_lazer_rule: pyth_lazer_rule_ts_1.PythLazerRule,
|
|
35
33
|
waterx_rule: waterx_rule_ts_1.WaterxRule,
|
|
36
34
|
});
|
|
37
35
|
/**
|
|
38
36
|
* Thrown by {@link resolveOracleRule} when `source` has no `PriceUpdateRule`
|
|
39
37
|
* registered in either the production registry or a test's `overrides` map.
|
|
40
|
-
* `instanceof`-able (mirrors `
|
|
41
|
-
* so a consumer can branch on the failure type
|
|
42
|
-
* string-matching `error.message`.
|
|
38
|
+
* `instanceof`-able (mirrors `LazerApiKeyMissingError` in
|
|
39
|
+
* `rules/pyth-lazer-rule.ts`) so a consumer can branch on the failure type
|
|
40
|
+
* directly instead of string-matching `error.message`.
|
|
43
41
|
*/
|
|
44
42
|
class OracleSourceNotImplementedError extends Error {
|
|
45
43
|
/** The unregistered `OracleSource` that was requested. */
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
12
12
|
import type { Network } from "../../constants.ts";
|
|
13
|
+
import type { PythFetchPolicy } from "../config.ts";
|
|
13
14
|
import type { OracleHost } from "../host.ts";
|
|
14
15
|
import { type PriceUpdateRule } from "../price-update-rule.ts";
|
|
15
16
|
/**
|
|
@@ -60,13 +61,33 @@ export interface PythLazerUpdatePayload {
|
|
|
60
61
|
* Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
|
|
61
62
|
* deployed in config but no `pythApiKey` was supplied at client init — the
|
|
62
63
|
* Lazer HTTP API requires a Bearer token and the SDK never reads
|
|
63
|
-
* `process.env` to find one. `
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* `process.env` to find one. Also thrown by `refreshOraclePrices`'s hoisted
|
|
65
|
+
* credential pre-check (`aggregate.ts`) BEFORE any fetch, keyed off this
|
|
66
|
+
* rule's `credential` declaration. `instanceof`-able (mirrors `FetchPolicyError`
|
|
67
|
+
* in `../update-fetch.ts`) so a consumer can branch on the failure type
|
|
68
|
+
* directly instead of string-matching `error.message`.
|
|
66
69
|
*/
|
|
67
70
|
export declare class LazerApiKeyMissingError extends Error {
|
|
68
71
|
constructor();
|
|
69
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* THE `POST {endpoint}/v1/latest_price` transport — shared by BOTH legs of
|
|
75
|
+
* this endpoint: the signed-update WRITE fetch below and the parsed-price READ
|
|
76
|
+
* executor (`readLazerPrices` in `../read-prices.ts`). They differ only in the
|
|
77
|
+
* request pins they send and how they decode/blame the response, so keeping
|
|
78
|
+
* one transport stops the URL, method, headers, and auth/retry policy drifting
|
|
79
|
+
* between them.
|
|
80
|
+
*
|
|
81
|
+
* `requestPins` is spread into the body alongside `priceFeedIds` + `channel`;
|
|
82
|
+
* the raw `Response` comes back undecoded so each caller owns its own error
|
|
83
|
+
* framing (the write leg reframes an exhausted retry, the read leg maps 403 to
|
|
84
|
+
* an entitlement error).
|
|
85
|
+
*
|
|
86
|
+
* Goes through the shared `fetchWithPolicy` (`../update-fetch.ts`) — the one
|
|
87
|
+
* retry/timeout/Bearer policy every oracle fetch shares, so all sources fail
|
|
88
|
+
* the same way under upstream degradation.
|
|
89
|
+
*/
|
|
90
|
+
export declare function postLazerLatestPrice(endpoint: string, channel: string, apiKey: string, feedIds: number[], requestPins: Record<string, unknown>, fetchOpts?: PythFetchPolicy): Promise<Response>;
|
|
70
91
|
/**
|
|
71
92
|
* `pyth_lazer_rule::feed(collector, config, clock, &update)` — contribute the
|
|
72
93
|
* verified Lazer price for `collector.symbol()` to the collector. `update` is
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.PythLazerRule = exports.LazerApiKeyMissingError = exports.LAZER_INFRA = void 0;
|
|
14
|
+
exports.postLazerLatestPrice = postLazerLatestPrice;
|
|
14
15
|
exports.feedLazerRule = feedLazerRule;
|
|
15
16
|
const bcs_1 = require("@mysten/bcs");
|
|
16
17
|
const record_ts_1 = require("../../utils/record.js");
|
|
@@ -89,7 +90,7 @@ const LAZER_LATEST_PRICE_REQUEST = {
|
|
|
89
90
|
};
|
|
90
91
|
/**
|
|
91
92
|
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
92
|
-
* caller before this runs (mirrors `
|
|
93
|
+
* caller before this runs (mirrors `WaterxRule`'s guard split), so a
|
|
93
94
|
* same-shaped payload from a different rule can never silently pass.
|
|
94
95
|
*/
|
|
95
96
|
function isPythLazerUpdatePayloadShape(payload) {
|
|
@@ -102,9 +103,11 @@ function isPythLazerUpdatePayloadShape(payload) {
|
|
|
102
103
|
* Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
|
|
103
104
|
* deployed in config but no `pythApiKey` was supplied at client init — the
|
|
104
105
|
* Lazer HTTP API requires a Bearer token and the SDK never reads
|
|
105
|
-
* `process.env` to find one. `
|
|
106
|
-
*
|
|
107
|
-
*
|
|
106
|
+
* `process.env` to find one. Also thrown by `refreshOraclePrices`'s hoisted
|
|
107
|
+
* credential pre-check (`aggregate.ts`) BEFORE any fetch, keyed off this
|
|
108
|
+
* rule's `credential` declaration. `instanceof`-able (mirrors `FetchPolicyError`
|
|
109
|
+
* in `../update-fetch.ts`) so a consumer can branch on the failure type
|
|
110
|
+
* directly instead of string-matching `error.message`.
|
|
108
111
|
*/
|
|
109
112
|
class LazerApiKeyMissingError extends Error {
|
|
110
113
|
constructor() {
|
|
@@ -123,24 +126,39 @@ function requireLazerPackage(host) {
|
|
|
123
126
|
return entry;
|
|
124
127
|
}
|
|
125
128
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
129
|
+
* THE `POST {endpoint}/v1/latest_price` transport — shared by BOTH legs of
|
|
130
|
+
* this endpoint: the signed-update WRITE fetch below and the parsed-price READ
|
|
131
|
+
* executor (`readLazerPrices` in `../read-prices.ts`). They differ only in the
|
|
132
|
+
* request pins they send and how they decode/blame the response, so keeping
|
|
133
|
+
* one transport stops the URL, method, headers, and auth/retry policy drifting
|
|
134
|
+
* between them.
|
|
135
|
+
*
|
|
136
|
+
* `requestPins` is spread into the body alongside `priceFeedIds` + `channel`;
|
|
137
|
+
* the raw `Response` comes back undecoded so each caller owns its own error
|
|
138
|
+
* framing (the write leg reframes an exhausted retry, the read leg maps 403 to
|
|
139
|
+
* an entitlement error).
|
|
140
|
+
*
|
|
141
|
+
* Goes through the shared `fetchWithPolicy` (`../update-fetch.ts`) — the one
|
|
142
|
+
* retry/timeout/Bearer policy every oracle fetch shares, so all sources fail
|
|
143
|
+
* the same way under upstream degradation.
|
|
130
144
|
*/
|
|
131
|
-
|
|
145
|
+
function postLazerLatestPrice(endpoint, channel, apiKey, feedIds, requestPins, fetchOpts) {
|
|
132
146
|
// joinEndpointPath preserves any base path on the endpoint — the same
|
|
133
147
|
// leading-slash `new URL` footgun that 404'd every feed on the Pyth Pro
|
|
134
148
|
// Hermes endpoint (see update-fetch.ts). Defensive here: the default
|
|
135
149
|
// Lazer endpoint has no base path, but a config override may.
|
|
136
150
|
const url = (0, update_fetch_ts_1.joinEndpointPath)(endpoint, "v1/latest_price");
|
|
151
|
+
return (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: { "Content-Type": "application/json" },
|
|
154
|
+
body: JSON.stringify({ priceFeedIds: feedIds, ...requestPins, channel }),
|
|
155
|
+
}, { apiKey, ...fetchOpts });
|
|
156
|
+
}
|
|
157
|
+
/** Fetch one signed `leEcdsa` update for `feedIds` from the Lazer HTTP API. */
|
|
158
|
+
async function fetchLazerSignedUpdate(endpoint, channel, apiKey, feedIds, fetchOpts) {
|
|
137
159
|
let res;
|
|
138
160
|
try {
|
|
139
|
-
res = await (
|
|
140
|
-
method: "POST",
|
|
141
|
-
headers: { "Content-Type": "application/json" },
|
|
142
|
-
body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST, channel }),
|
|
143
|
-
}, { apiKey, ...fetchOpts });
|
|
161
|
+
res = await postLazerLatestPrice(endpoint, channel, apiKey, feedIds, LAZER_LATEST_PRICE_REQUEST, fetchOpts);
|
|
144
162
|
}
|
|
145
163
|
catch (err) {
|
|
146
164
|
(0, update_fetch_ts_1.rethrowExhaustedFetch)(err, (e) => `Lazer price fetch failed: ${e.status}${e.bodySnippet ? ` ${e.bodySnippet}` : ""}`);
|
|
@@ -172,9 +190,12 @@ function feedLazerRule(tx, host, collector, update) {
|
|
|
172
190
|
}
|
|
173
191
|
exports.PythLazerRule = {
|
|
174
192
|
kind: "pyth_lazer_rule",
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
193
|
+
// Lazer is auth-first: the signed-update fetch cannot run without the
|
|
194
|
+
// caller's `pythApiKey` Bearer. Declared on the port — kind AND error
|
|
195
|
+
// together — so `refreshOraclePrices`'s hoisted pre-check and consumers'
|
|
196
|
+
// `missingOracleCredentials` boot asserts both key off the rule itself, and
|
|
197
|
+
// the orchestrator never constructs this error on the rule's behalf.
|
|
198
|
+
credential: { kind: "pyth_api_key", missing: () => new LazerApiKeyMissingError() },
|
|
178
199
|
/** Tickers with a `pyth_lazer_rule.feeds` entry (integer Lazer feed ids). */
|
|
179
200
|
supportedTickers(host) {
|
|
180
201
|
return Object.keys(host.config.packages.pyth_lazer_rule?.feeds ?? {});
|
|
@@ -227,14 +248,13 @@ exports.PythLazerRule = {
|
|
|
227
248
|
* Appends the single `parse_and_verify_le_ecdsa_update*(state, clock, bytes)`
|
|
228
249
|
* call — one secp256k1 signature check covering every feed in the payload —
|
|
229
250
|
* and returns its `Update` result as the handle the per-ticker feed leg
|
|
230
|
-
* consumes.
|
|
231
|
-
* ignored (Lazer verification charges no update fee).
|
|
251
|
+
* consumes.
|
|
232
252
|
*
|
|
233
253
|
* The entry name comes from `LAZER_INFRA[network].verify_entry`: mainnet's
|
|
234
254
|
* rule binds `update_v2`, testnet's is still the v1 publish. Both take
|
|
235
255
|
* `(state, clock, bytes)` and accept the same `leEcdsa` payload.
|
|
236
256
|
*/
|
|
237
|
-
buildUpdateCalls(tx, host, data
|
|
257
|
+
buildUpdateCalls(tx, host, data) {
|
|
238
258
|
const payload = (0, price_update_rule_ts_1.assertRuleUpdateData)(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
|
|
239
259
|
if (!payload)
|
|
240
260
|
return undefined;
|