@waterx/sdk 4.0.0 → 4.1.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/README.md +52 -37
- package/dist/cjs/src/account/account.js +2 -1
- package/dist/cjs/src/account/config.d.ts +0 -16
- package/dist/cjs/src/account/funding/balance.d.ts +25 -2
- package/dist/cjs/src/account/funding/balance.js +36 -24
- package/dist/cjs/src/account/funding/credit.js +6 -10
- package/dist/cjs/src/constants.d.ts +15 -1
- package/dist/cjs/src/constants.js +18 -4
- package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
- package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +13 -0
- package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
- package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.js +31 -0
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +940 -0
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +1092 -0
- package/dist/cjs/src/oracle/aggregate.d.ts +21 -21
- package/dist/cjs/src/oracle/aggregate.js +84 -71
- package/dist/cjs/src/oracle/config.d.ts +107 -52
- package/dist/cjs/src/oracle/config.js +15 -35
- package/dist/cjs/src/oracle/host.d.ts +13 -2
- package/dist/cjs/src/oracle/index.d.ts +4 -2
- package/dist/cjs/src/oracle/index.js +25 -6
- package/dist/cjs/src/oracle/price-update-rule.d.ts +3 -4
- package/dist/cjs/src/oracle/pyth.d.ts +68 -6
- package/dist/cjs/src/oracle/pyth.js +338 -22
- package/dist/cjs/src/oracle/rule-registry.d.ts +11 -6
- package/dist/cjs/src/oracle/rule-registry.js +13 -6
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +17 -2
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +6 -6
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +25 -22
- package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -0
- package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +89 -0
- package/dist/cjs/src/oracle/rules/waterx-rule.js +272 -0
- package/dist/cjs/src/oracle/update-fetch.d.ts +32 -2
- package/dist/cjs/src/oracle/update-fetch.js +60 -3
- package/dist/cjs/src/perp/client.d.ts +71 -19
- package/dist/cjs/src/perp/client.js +30 -10
- package/dist/cjs/src/perp/config.d.ts +4 -7
- package/dist/cjs/src/perp/config.js +9 -12
- package/dist/cjs/src/perp/constants.d.ts +0 -6
- package/dist/cjs/src/perp/constants.js +11 -9
- package/dist/cjs/src/perp/fetch/account.js +3 -0
- package/dist/cjs/src/perp/fetch/bridge.js +2 -1
- package/dist/cjs/src/perp/fetch/market.js +2 -1
- package/dist/cjs/src/perp/fetch/positions.d.ts +16 -10
- package/dist/cjs/src/perp/fetch/positions.js +23 -20
- package/dist/cjs/src/perp/index.d.ts +8 -4
- package/dist/cjs/src/perp/index.js +12 -7
- package/dist/cjs/src/perp/liq-view.d.ts +64 -0
- package/dist/cjs/src/perp/liq-view.js +74 -0
- package/dist/cjs/src/perp/user/order.d.ts +13 -0
- package/dist/cjs/src/perp/user/order.js +30 -16
- package/dist/cjs/src/perp/user/staking.js +3 -2
- package/dist/cjs/src/perp/user/trading.js +25 -24
- package/dist/cjs/src/perp/user/wlp.js +6 -5
- package/dist/cjs/src/prediction/utils.d.ts +11 -2
- package/dist/cjs/src/prediction/utils.js +22 -22
- package/dist/cjs/src/unified-client.d.ts +49 -20
- package/dist/cjs/src/unified-client.js +4 -1
- package/dist/cjs/src/utils/format.d.ts +14 -0
- package/dist/cjs/src/utils/format.js +24 -0
- package/dist/cjs/src/utils/math.d.ts +304 -12
- package/dist/cjs/src/utils/math.js +397 -17
- package/dist/cjs/src/utils/validate.d.ts +69 -0
- package/dist/cjs/src/utils/validate.js +183 -0
- package/dist/src/account/account.js +2 -1
- package/dist/src/account/config.d.ts +0 -16
- package/dist/src/account/funding/balance.d.ts +25 -2
- package/dist/src/account/funding/balance.js +36 -24
- package/dist/src/account/funding/credit.js +6 -10
- package/dist/src/constants.d.ts +15 -1
- package/dist/src/constants.js +17 -3
- package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
- package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +10 -0
- package/dist/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
- package/dist/src/generated/waterx_rule/deps/sui/vec_map.js +27 -0
- package/dist/src/generated/waterx_rule/waterx_rule.d.ts +940 -0
- package/dist/src/generated/waterx_rule/waterx_rule.js +991 -0
- package/dist/src/oracle/aggregate.d.ts +21 -21
- package/dist/src/oracle/aggregate.js +84 -71
- package/dist/src/oracle/config.d.ts +107 -52
- package/dist/src/oracle/config.js +14 -34
- package/dist/src/oracle/host.d.ts +13 -2
- package/dist/src/oracle/index.d.ts +4 -2
- package/dist/src/oracle/index.js +18 -7
- package/dist/src/oracle/price-update-rule.d.ts +3 -4
- package/dist/src/oracle/pyth.d.ts +68 -6
- package/dist/src/oracle/pyth.js +334 -22
- package/dist/src/oracle/rule-registry.d.ts +11 -6
- package/dist/src/oracle/rule-registry.js +13 -6
- package/dist/src/oracle/rules/pyth-core-rule.js +18 -3
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +6 -6
- package/dist/src/oracle/rules/pyth-lazer-rule.js +26 -23
- package/dist/src/oracle/rules/pyth-rule.js +5 -0
- package/dist/src/oracle/rules/waterx-rule.d.ts +89 -0
- package/dist/src/oracle/rules/waterx-rule.js +266 -0
- package/dist/src/oracle/update-fetch.d.ts +32 -2
- package/dist/src/oracle/update-fetch.js +57 -3
- package/dist/src/perp/client.d.ts +71 -19
- package/dist/src/perp/client.js +31 -11
- package/dist/src/perp/config.d.ts +4 -7
- package/dist/src/perp/config.js +9 -12
- package/dist/src/perp/constants.d.ts +0 -6
- package/dist/src/perp/constants.js +10 -8
- package/dist/src/perp/fetch/account.js +3 -0
- package/dist/src/perp/fetch/bridge.js +2 -1
- package/dist/src/perp/fetch/market.js +2 -1
- package/dist/src/perp/fetch/positions.d.ts +16 -10
- package/dist/src/perp/fetch/positions.js +28 -20
- package/dist/src/perp/index.d.ts +8 -4
- package/dist/src/perp/index.js +5 -3
- package/dist/src/perp/liq-view.d.ts +64 -0
- package/dist/src/perp/liq-view.js +71 -0
- package/dist/src/perp/user/order.d.ts +13 -0
- package/dist/src/perp/user/order.js +30 -16
- package/dist/src/perp/user/staking.js +3 -2
- package/dist/src/perp/user/trading.js +25 -24
- package/dist/src/perp/user/wlp.js +6 -5
- package/dist/src/prediction/utils.d.ts +11 -2
- package/dist/src/prediction/utils.js +22 -22
- package/dist/src/unified-client.d.ts +49 -20
- package/dist/src/unified-client.js +4 -1
- package/dist/src/utils/format.d.ts +14 -0
- package/dist/src/utils/format.js +21 -0
- package/dist/src/utils/math.d.ts +304 -12
- package/dist/src/utils/math.js +394 -17
- package/dist/src/utils/validate.d.ts +69 -0
- package/dist/src/utils/validate.js +167 -0
- package/package.json +4 -1
|
@@ -36,12 +36,27 @@ exports.PythCoreRule = {
|
|
|
36
36
|
async fetchUpdateData(host, tickers) {
|
|
37
37
|
if (tickers.length === 0)
|
|
38
38
|
return null;
|
|
39
|
+
// `host.pyth` is this source's own infra — the fixed per-network Core
|
|
40
|
+
// Pyth block plus the caller's api_key/fetch. Endpoint, credential and
|
|
41
|
+
// retry/timeout policy all come from it.
|
|
42
|
+
const endpoint = host.pyth.hermes_endpoint;
|
|
39
43
|
const feedIds = tickers.map((ticker) => host.getPythFeed(ticker).feed_id);
|
|
40
|
-
const updates = await (0, pyth_ts_1.fetchPriceFeedsUpdateData)(
|
|
44
|
+
const updates = await (0, pyth_ts_1.fetchPriceFeedsUpdateData)(endpoint, feedIds, {
|
|
41
45
|
apiKey: host.pyth.api_key,
|
|
42
46
|
fetch: host.pyth.fetch,
|
|
43
47
|
});
|
|
44
|
-
|
|
48
|
+
// `updates` covers only the feeds this endpoint actually served — the fetch
|
|
49
|
+
// drops (and memoizes) any it lacks, e.g. Core feeds absent from Pyth Pro
|
|
50
|
+
// (WTIUSD/BRENTUSD). Align `feedIds` with them: buildPythPriceUpdateCalls
|
|
51
|
+
// emits one moveCall per feedId and must not reference a feed the
|
|
52
|
+
// accumulator blob doesn't cover.
|
|
53
|
+
return {
|
|
54
|
+
kind: "pyth_rule",
|
|
55
|
+
payload: {
|
|
56
|
+
updates,
|
|
57
|
+
feedIds: (0, pyth_ts_1.endpointSupportedFeedIds)(endpoint, feedIds, host.pyth.api_key),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
45
60
|
},
|
|
46
61
|
/**
|
|
47
62
|
* Subsets a (typically whole-universe) payload from {@link fetchUpdateData}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* updates, plus `feedLazerRule`, the collector-feed leg `aggregateTicker`
|
|
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
|
-
* via `
|
|
6
|
+
* via the `pythApiKey` create option), verifies it ONCE on-chain via
|
|
7
7
|
* `pyth_lazer::parse_and_verify_le_ecdsa_update`, and hands the resulting
|
|
8
8
|
* `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
|
|
9
9
|
*/
|
|
@@ -19,11 +19,11 @@ export interface PythLazerUpdatePayload {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
|
|
22
|
-
* deployed in config but no `
|
|
23
|
-
* requires a Bearer token and the SDK never reads
|
|
24
|
-
* `instanceof`-able (mirrors
|
|
25
|
-
* so a consumer can branch on
|
|
26
|
-
* string-matching `error.message`.
|
|
22
|
+
* deployed in config but no `pythApiKey` was supplied at client init — the
|
|
23
|
+
* Lazer HTTP API requires a Bearer token and the SDK never reads
|
|
24
|
+
* `process.env` to find one. `instanceof`-able (mirrors
|
|
25
|
+
* `OracleFeeSourceUnavailableError` in `pyth.ts`) so a consumer can branch on
|
|
26
|
+
* the failure type directly instead of string-matching `error.message`.
|
|
27
27
|
*/
|
|
28
28
|
export declare class LazerApiKeyMissingError extends Error {
|
|
29
29
|
constructor();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* updates, plus `feedLazerRule`, the collector-feed leg `aggregateTicker`
|
|
5
5
|
* appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
|
|
6
6
|
* requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
|
|
7
|
-
* via `
|
|
7
|
+
* via the `pythApiKey` create option), verifies it ONCE on-chain via
|
|
8
8
|
* `pyth_lazer::parse_and_verify_le_ecdsa_update`, and hands the resulting
|
|
9
9
|
* `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
|
|
10
10
|
*/
|
|
@@ -22,10 +22,17 @@ const update_fetch_ts_1 = require("../update-fetch.js");
|
|
|
22
22
|
* `confidence` is optional on-chain but requested so the rule's
|
|
23
23
|
* fail-closed confidence gate actually engages (a payload without
|
|
24
24
|
* confidence passes the gate unchecked).
|
|
25
|
-
* - `channel` — `real_time`:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* - `channel` — `fixed_rate@200ms`, NOT `real_time`: Lazer rejects a request
|
|
26
|
+
* whose channel is faster than ANY requested feed's `min_channel`, and it
|
|
27
|
+
* rejects the WHOLE batch (`400 Feeds do not support channel …`). Only the
|
|
28
|
+
* majors (BTC/ETH/SOL/USDC/DOGE/XRP/BNB/HYPE + EUR/JPY FX) publish
|
|
29
|
+
* `real_time`; the other 19 of the 29 configured feeds — including SUIUSD
|
|
30
|
+
* and every xStock — are `min_channel: fixed_rate@200ms` (Lazer symbol
|
|
31
|
+
* registry, verified 2026-07-22: the same 29-feed batch 400s at
|
|
32
|
+
* `real_time`/`50ms` and serves 200 with the leEcdsa blob at `200ms`).
|
|
33
|
+
* 200ms is the fastest channel every configured feed supports, and the
|
|
34
|
+
* deployed rule accepts it: the v1 on-chain `channel::from_u8` aborts only
|
|
35
|
+
* on the 1000ms fixed-rate channel (real_time / 50ms / 200ms are safe).
|
|
29
36
|
* - `formats: leEcdsa` + `jsonBinaryEncoding: hex` — the Sui verifier takes
|
|
30
37
|
* the `leEcdsa` framing; hex matches `fromHex` below.
|
|
31
38
|
*/
|
|
@@ -33,7 +40,7 @@ const LAZER_LATEST_PRICE_REQUEST = {
|
|
|
33
40
|
properties: ["price", "exponent", "confidence"],
|
|
34
41
|
formats: ["leEcdsa"],
|
|
35
42
|
jsonBinaryEncoding: "hex",
|
|
36
|
-
channel: "
|
|
43
|
+
channel: "fixed_rate@200ms",
|
|
37
44
|
};
|
|
38
45
|
/**
|
|
39
46
|
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
@@ -48,16 +55,16 @@ function isPythLazerUpdatePayloadShape(payload) {
|
|
|
48
55
|
}
|
|
49
56
|
/**
|
|
50
57
|
* Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
|
|
51
|
-
* deployed in config but no `
|
|
52
|
-
* requires a Bearer token and the SDK never reads
|
|
53
|
-
* `instanceof`-able (mirrors
|
|
54
|
-
* so a consumer can branch on
|
|
55
|
-
* string-matching `error.message`.
|
|
58
|
+
* deployed in config but no `pythApiKey` was supplied at client init — the
|
|
59
|
+
* Lazer HTTP API requires a Bearer token and the SDK never reads
|
|
60
|
+
* `process.env` to find one. `instanceof`-able (mirrors
|
|
61
|
+
* `OracleFeeSourceUnavailableError` in `pyth.ts`) so a consumer can branch on
|
|
62
|
+
* the failure type directly instead of string-matching `error.message`.
|
|
56
63
|
*/
|
|
57
64
|
class LazerApiKeyMissingError extends Error {
|
|
58
65
|
constructor() {
|
|
59
66
|
super("LazerApiKeyMissing: pyth_lazer_rule requires a Pyth Lazer access token — " +
|
|
60
|
-
"
|
|
67
|
+
"pass `pythApiKey` when creating the client (the SDK never reads process.env)");
|
|
61
68
|
this.name = "LazerApiKeyMissingError";
|
|
62
69
|
}
|
|
63
70
|
}
|
|
@@ -77,7 +84,11 @@ function requireLazerPackage(host) {
|
|
|
77
84
|
* both oracle sources fail the same way under upstream degradation.
|
|
78
85
|
*/
|
|
79
86
|
async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
|
|
80
|
-
|
|
87
|
+
// joinEndpointPath preserves any base path on the endpoint — the same
|
|
88
|
+
// leading-slash `new URL` footgun that 404'd every feed on the Pyth Pro
|
|
89
|
+
// Hermes endpoint (see update-fetch.ts). Defensive here: the default
|
|
90
|
+
// Lazer endpoint has no base path, but a config override may.
|
|
91
|
+
const url = (0, update_fetch_ts_1.joinEndpointPath)(endpoint, "v1/latest_price");
|
|
81
92
|
let res;
|
|
82
93
|
try {
|
|
83
94
|
res = await (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), {
|
|
@@ -87,15 +98,7 @@ async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
|
|
|
87
98
|
}, { apiKey, ...fetchOpts });
|
|
88
99
|
}
|
|
89
100
|
catch (err) {
|
|
90
|
-
|
|
91
|
-
// never recovered carries `status` on the FetchPolicyError — reformat
|
|
92
|
-
// into this function's own message shape; a network-level exhaustion
|
|
93
|
-
// (no status) propagates as-is.
|
|
94
|
-
if (err instanceof update_fetch_ts_1.FetchPolicyError && err.status !== undefined) {
|
|
95
|
-
const body = err.bodySnippet ? ` ${err.bodySnippet}` : "";
|
|
96
|
-
throw new Error(`Lazer price fetch failed: ${err.status}${body} (retries exhausted after ${err.attempts} attempts)`, { cause: err });
|
|
97
|
-
}
|
|
98
|
-
throw err;
|
|
101
|
+
(0, update_fetch_ts_1.rethrowExhaustedFetch)(err, (e) => `Lazer price fetch failed: ${e.status}${e.bodySnippet ? ` ${e.bodySnippet}` : ""}`);
|
|
99
102
|
}
|
|
100
103
|
if (!res.ok)
|
|
101
104
|
throw new Error(`Lazer price fetch failed: ${res.status} ${await res.text()}`);
|
|
@@ -16,6 +16,11 @@ function feedPythRule(tx, host, collector, priceInfoObjectId) {
|
|
|
16
16
|
arguments: {
|
|
17
17
|
collector,
|
|
18
18
|
config: tx.object(host.config.packages.pyth_rule.config),
|
|
19
|
+
// The deployed pyth_rule package is compiled against the Core pyth
|
|
20
|
+
// dependency, so its `&PythState` parameter is the Core-package-qualified
|
|
21
|
+
// type and `host.pyth` (the fixed per-network Core infra) is always the
|
|
22
|
+
// right state to pass. The config's price_info_object entries are Core
|
|
23
|
+
// objects to match.
|
|
19
24
|
pythState: tx.object(host.pyth.state_id),
|
|
20
25
|
pythPriceInfo: tx.object(priceInfoObjectId),
|
|
21
26
|
},
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WaterxRule` — `PriceUpdateRule` for the first-party WaterX quote-center
|
|
3
|
+
* (Nautilus-TEE, ed25519), plus `feedWaterxRule`, the collector-feed leg
|
|
4
|
+
* `aggregateTicker` appends per waterx-routed ticker. Pulls one enclave-signed
|
|
5
|
+
* batch envelope covering every requested ticker from the quote-center
|
|
6
|
+
* (`GET /v1/quotes/update?symbols=…`, endpoint from `host.waterx` — the
|
|
7
|
+
* `waterxEndpoint`/`waterxFetch` create options — else `WATERX_DEFAULTS`), then —
|
|
8
|
+
* unlike Pyth Lazer, whose verify is a single shared PTB step — verifies AND
|
|
9
|
+
* feeds in ONE `waterx_rule::collect_batch_latest` call per collector (the Move
|
|
10
|
+
* API bundles the two). So `buildUpdateCalls` emits nothing and the signed
|
|
11
|
+
* envelope is handed straight to the per-ticker feed leg.
|
|
12
|
+
*
|
|
13
|
+
* `collect_batch_latest` is the dual-rule path: it feeds the item matching
|
|
14
|
+
* `collector.symbol()` WITHOUT aggregating, so a waterx-routed ticker composes
|
|
15
|
+
* onto the same collector as Pyth/Supra (compose-then-aggregate). On-chain a
|
|
16
|
+
* freshness miss / replayed timestamp ABSTAINS (the other weighted rules
|
|
17
|
+
* cover); a config/integrity mismatch or bad signature aborts.
|
|
18
|
+
*/
|
|
19
|
+
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
20
|
+
import type { OracleHost } from "../host.ts";
|
|
21
|
+
import { type PriceUpdateRule, type RuleUpdateData } from "../price-update-rule.ts";
|
|
22
|
+
/**
|
|
23
|
+
* One item inside a signed batch payload, mirroring the quote-center
|
|
24
|
+
* `/v1/quotes/update` JSON 1:1 (snake_case). The u64 integer fields are the
|
|
25
|
+
* EXACT values the enclave signed over BCS — `collect_batch_latest` rebuilds
|
|
26
|
+
* the payload on-chain and re-verifies, so they must round-trip byte-for-byte.
|
|
27
|
+
* They are `bigint` (not `number`): {@link parseSignedEnvelope} decodes them
|
|
28
|
+
* from the raw JSON integer literals so a value above `Number.MAX_SAFE_INTEGER`
|
|
29
|
+
* (2^53) can never lose precision and silently abort the on-chain signature
|
|
30
|
+
* check. `num_sources` is a `u8` (≤ 255) and stays a `number`.
|
|
31
|
+
*/
|
|
32
|
+
export interface WaterxBatchItem {
|
|
33
|
+
symbol: string;
|
|
34
|
+
ticker: string;
|
|
35
|
+
sources: bigint[];
|
|
36
|
+
method: string;
|
|
37
|
+
price_timestamp_ms: bigint;
|
|
38
|
+
price_n: bigint;
|
|
39
|
+
price_scale: bigint;
|
|
40
|
+
confidence_n: bigint;
|
|
41
|
+
confidence_scale: bigint;
|
|
42
|
+
max_source_deviation_bps: bigint;
|
|
43
|
+
num_sources: number;
|
|
44
|
+
}
|
|
45
|
+
/** The enclave-signed batch envelope from `GET /v1/quotes/update`. */
|
|
46
|
+
export interface WaterxSignedEnvelope {
|
|
47
|
+
intent: number;
|
|
48
|
+
/** Enclave signing timestamp (ms) — the on-chain `timestamp_ms` argument. */
|
|
49
|
+
timestamp_ms: bigint;
|
|
50
|
+
payload: {
|
|
51
|
+
items: WaterxBatchItem[];
|
|
52
|
+
};
|
|
53
|
+
/** ed25519 signature over `BCS(IntentMessage<BatchPricePayload>)`, hex (± `0x`). */
|
|
54
|
+
signature: string;
|
|
55
|
+
}
|
|
56
|
+
/** `waterx_rule`'s narrowed `RuleUpdateData.payload` shape. */
|
|
57
|
+
export interface WaterxUpdatePayload {
|
|
58
|
+
readonly envelope: WaterxSignedEnvelope;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse a quote-center `/v1/quotes/update` response body into a
|
|
62
|
+
* {@link WaterxSignedEnvelope} with the u64 fields decoded as `bigint`, exact.
|
|
63
|
+
*
|
|
64
|
+
* The signature is over `BCS(IntentMessage<BatchPricePayload>)`, so every u64
|
|
65
|
+
* the SDK rebuilds in-PTB must equal the enclave's byte-for-byte or
|
|
66
|
+
* `collect_batch_latest` aborts the whole trade PTB (bad signature — not an
|
|
67
|
+
* abstain). A plain `JSON.parse` yields IEEE-754 doubles that lose precision
|
|
68
|
+
* above 2^53, so instead we recover each integer's exact source literal via the
|
|
69
|
+
* ES2023 reviver `context.source` (Node 21+ / modern browsers) and `BigInt()`
|
|
70
|
+
* it. On an older runtime that passes no `context`, a value within 2^53 is
|
|
71
|
+
* still exact (`BigInt(number)`); a value ABOVE it throws loudly here rather
|
|
72
|
+
* than silently corrupting the payload into an on-chain abort. `num_sources`
|
|
73
|
+
* (u8) and `intent` are coerced back to `number` — both are tiny.
|
|
74
|
+
*/
|
|
75
|
+
export declare function parseSignedEnvelope(text: string): WaterxSignedEnvelope;
|
|
76
|
+
/** Narrow a `RuleUpdateData` to its `WaterxSignedEnvelope`, or `null`. */
|
|
77
|
+
export declare function waterxEnvelopeOf(data: RuleUpdateData): WaterxSignedEnvelope | null;
|
|
78
|
+
/**
|
|
79
|
+
* `waterx_rule::collect_batch_latest(collector, config, clock, enclave_config,
|
|
80
|
+
* enclave, timestamp_ms, payload, sig)` — rebuild the enclave-signed batch
|
|
81
|
+
* payload in-PTB (`new_batch_payload` + one `new_batch_item`/`push_batch_item`
|
|
82
|
+
* per item, the exact shape the enclave signed) and contribute the price for
|
|
83
|
+
* `collector.symbol()` to the collector. One collect call re-verifies the batch
|
|
84
|
+
* signature and picks this collector's symbol out of the batch; on-chain it
|
|
85
|
+
* abstains (records `none`) instead of aborting when the symbol is stale,
|
|
86
|
+
* absent from the batch, or its timestamp was already accepted (replay).
|
|
87
|
+
*/
|
|
88
|
+
export declare function feedWaterxRule(tx: Transaction, host: OracleHost, collector: TransactionArgument, envelope: WaterxSignedEnvelope): void;
|
|
89
|
+
export declare const WaterxRule: PriceUpdateRule;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `WaterxRule` — `PriceUpdateRule` for the first-party WaterX quote-center
|
|
4
|
+
* (Nautilus-TEE, ed25519), plus `feedWaterxRule`, the collector-feed leg
|
|
5
|
+
* `aggregateTicker` appends per waterx-routed ticker. Pulls one enclave-signed
|
|
6
|
+
* batch envelope covering every requested ticker from the quote-center
|
|
7
|
+
* (`GET /v1/quotes/update?symbols=…`, endpoint from `host.waterx` — the
|
|
8
|
+
* `waterxEndpoint`/`waterxFetch` create options — else `WATERX_DEFAULTS`), then —
|
|
9
|
+
* unlike Pyth Lazer, whose verify is a single shared PTB step — verifies AND
|
|
10
|
+
* feeds in ONE `waterx_rule::collect_batch_latest` call per collector (the Move
|
|
11
|
+
* API bundles the two). So `buildUpdateCalls` emits nothing and the signed
|
|
12
|
+
* envelope is handed straight to the per-ticker feed leg.
|
|
13
|
+
*
|
|
14
|
+
* `collect_batch_latest` is the dual-rule path: it feeds the item matching
|
|
15
|
+
* `collector.symbol()` WITHOUT aggregating, so a waterx-routed ticker composes
|
|
16
|
+
* onto the same collector as Pyth/Supra (compose-then-aggregate). On-chain a
|
|
17
|
+
* freshness miss / replayed timestamp ABSTAINS (the other weighted rules
|
|
18
|
+
* cover); a config/integrity mismatch or bad signature aborts.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.WaterxRule = void 0;
|
|
22
|
+
exports.parseSignedEnvelope = parseSignedEnvelope;
|
|
23
|
+
exports.waterxEnvelopeOf = waterxEnvelopeOf;
|
|
24
|
+
exports.feedWaterxRule = feedWaterxRule;
|
|
25
|
+
const bcs_1 = require("@mysten/bcs");
|
|
26
|
+
const waterx_rule_ts_1 = require("../../generated/waterx_rule/waterx_rule.js");
|
|
27
|
+
const config_ts_1 = require("../config.js");
|
|
28
|
+
const price_update_rule_ts_1 = require("../price-update-rule.js");
|
|
29
|
+
const update_fetch_ts_1 = require("../update-fetch.js");
|
|
30
|
+
/** The single signing intent (`BATCH_PRICE_INTENT`) the quote-center emits. */
|
|
31
|
+
const BATCH_PRICE_INTENT = 1;
|
|
32
|
+
/**
|
|
33
|
+
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
34
|
+
* caller before this runs (mirrors the other rules' guard split), so a
|
|
35
|
+
* same-shaped payload from a different rule can never silently pass.
|
|
36
|
+
*/
|
|
37
|
+
function isWaterxUpdatePayloadShape(payload) {
|
|
38
|
+
const env = payload?.envelope;
|
|
39
|
+
return (typeof env === "object" &&
|
|
40
|
+
env !== null &&
|
|
41
|
+
typeof env.signature === "string" &&
|
|
42
|
+
typeof env.timestamp_ms === "bigint" &&
|
|
43
|
+
Array.isArray(env.payload?.items));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse a quote-center `/v1/quotes/update` response body into a
|
|
47
|
+
* {@link WaterxSignedEnvelope} with the u64 fields decoded as `bigint`, exact.
|
|
48
|
+
*
|
|
49
|
+
* The signature is over `BCS(IntentMessage<BatchPricePayload>)`, so every u64
|
|
50
|
+
* the SDK rebuilds in-PTB must equal the enclave's byte-for-byte or
|
|
51
|
+
* `collect_batch_latest` aborts the whole trade PTB (bad signature — not an
|
|
52
|
+
* abstain). A plain `JSON.parse` yields IEEE-754 doubles that lose precision
|
|
53
|
+
* above 2^53, so instead we recover each integer's exact source literal via the
|
|
54
|
+
* ES2023 reviver `context.source` (Node 21+ / modern browsers) and `BigInt()`
|
|
55
|
+
* it. On an older runtime that passes no `context`, a value within 2^53 is
|
|
56
|
+
* still exact (`BigInt(number)`); a value ABOVE it throws loudly here rather
|
|
57
|
+
* than silently corrupting the payload into an on-chain abort. `num_sources`
|
|
58
|
+
* (u8) and `intent` are coerced back to `number` — both are tiny.
|
|
59
|
+
*/
|
|
60
|
+
function parseSignedEnvelope(text) {
|
|
61
|
+
const raw = JSON.parse(text, (_key, value, context) => {
|
|
62
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
63
|
+
return value;
|
|
64
|
+
if (context?.source !== undefined)
|
|
65
|
+
return BigInt(context.source);
|
|
66
|
+
if (!Number.isSafeInteger(value)) {
|
|
67
|
+
throw new Error("waterx envelope carries an integer above 2^53 and this runtime lacks JSON " +
|
|
68
|
+
"source access — cannot preserve u64 precision for the signed payload");
|
|
69
|
+
}
|
|
70
|
+
return BigInt(value);
|
|
71
|
+
});
|
|
72
|
+
if (typeof raw.signature !== "string" || !Array.isArray(raw.payload?.items)) {
|
|
73
|
+
throw new Error("WaterX quote-center returned a malformed signed envelope");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
intent: Number(raw.intent),
|
|
77
|
+
timestamp_ms: (raw.timestamp_ms ?? 0n),
|
|
78
|
+
signature: raw.signature,
|
|
79
|
+
payload: {
|
|
80
|
+
items: raw.payload.items.map((i) => ({ ...i, num_sources: Number(i.num_sources) })),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** The `waterx_rule` deployment entry; throws when the config carries none. */
|
|
85
|
+
function requireWaterxPackage(host) {
|
|
86
|
+
const entry = host.config.packages.waterx_rule;
|
|
87
|
+
if (!entry) {
|
|
88
|
+
throw new Error("waterx_rule package is not deployed in this config");
|
|
89
|
+
}
|
|
90
|
+
return entry;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the quote-center infra for this host: the `waterxEndpoint` /
|
|
94
|
+
* `waterxFetch` create options when the client carries them, else the network
|
|
95
|
+
* default. The fetch policy falls back to the shared `pyth.fetch` policy so a
|
|
96
|
+
* consumer that already tuned timeouts/retries once keeps them here.
|
|
97
|
+
*
|
|
98
|
+
* This is the seam a browser consumer needs: the envelope is fetched FROM THE
|
|
99
|
+
* PAGE, so a front end whose origin the quote-center does not allow (CORS)
|
|
100
|
+
* points `endpoint` at a same-origin proxy, or supplies its own `fetchImpl`.
|
|
101
|
+
*/
|
|
102
|
+
function resolveWaterxInfra(host) {
|
|
103
|
+
const infra = host.waterx ?? config_ts_1.WATERX_DEFAULTS[host.network];
|
|
104
|
+
return { endpoint: infra.endpoint, fetch: infra.fetch ?? host.pyth.fetch };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Pull one enclave-signed batch envelope covering `symbols` from the
|
|
108
|
+
* quote-center. Goes through the shared `fetchWithPolicy` (`../update-fetch.ts`)
|
|
109
|
+
* — same retry/timeout policy as the Pyth/Lazer fetches. No auth: the
|
|
110
|
+
* quote-center read surface is public.
|
|
111
|
+
*
|
|
112
|
+
* The URL is built with `joinEndpointPath`, not `new URL(path, endpoint)`: a
|
|
113
|
+
* leading-slash path is ABSOLUTE and silently drops the endpoint's own base
|
|
114
|
+
* path, which is exactly what a `waterxEndpoint` proxy route is (a
|
|
115
|
+
* `https://app.example/api/quote-center` override would have been rewritten to
|
|
116
|
+
* `https://app.example/v1/quotes/update`, bypassing the proxy). Same footgun
|
|
117
|
+
* that 404'd every Pyth Pro feed by dropping its `/hermes` prefix.
|
|
118
|
+
*/
|
|
119
|
+
async function fetchWaterxSignedUpdate(endpoint, symbols, fetchOpts) {
|
|
120
|
+
const url = (0, update_fetch_ts_1.joinEndpointPath)(endpoint, "v1/quotes/update");
|
|
121
|
+
url.searchParams.set("symbols", symbols.join(","));
|
|
122
|
+
let res;
|
|
123
|
+
try {
|
|
124
|
+
res = await (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), { method: "GET" }, { ...fetchOpts });
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
if (err instanceof update_fetch_ts_1.FetchPolicyError && err.status !== undefined) {
|
|
128
|
+
const body = err.bodySnippet ? ` ${err.bodySnippet}` : "";
|
|
129
|
+
throw new Error(`WaterX quote-center fetch failed: ${err.status}${body} (retries exhausted after ${err.attempts} attempts)`, { cause: err });
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
throw new Error(`WaterX quote-center fetch failed: ${res.status} ${await res.text()}`);
|
|
135
|
+
}
|
|
136
|
+
// Parse from raw text (not res.json()) so the u64 fields are decoded exact as
|
|
137
|
+
// bigint — see parseSignedEnvelope. Malformed-shape check lives there.
|
|
138
|
+
const envelope = parseSignedEnvelope(await res.text());
|
|
139
|
+
if (envelope.intent !== BATCH_PRICE_INTENT) {
|
|
140
|
+
throw new Error(`WaterX quote-center returned intent ${envelope.intent}, expected BATCH_PRICE_INTENT ${BATCH_PRICE_INTENT}`);
|
|
141
|
+
}
|
|
142
|
+
return envelope;
|
|
143
|
+
}
|
|
144
|
+
/** Narrow a `RuleUpdateData` to its `WaterxSignedEnvelope`, or `null`. */
|
|
145
|
+
function waterxEnvelopeOf(data) {
|
|
146
|
+
const payload = (0, price_update_rule_ts_1.assertRuleUpdateData)(data, "waterx_rule", isWaterxUpdatePayloadShape, "{ envelope: { intent, timestamp_ms, payload: { items }, signature } }");
|
|
147
|
+
return payload?.envelope ?? null;
|
|
148
|
+
}
|
|
149
|
+
/** Strip an optional `0x` prefix, then decode hex → bytes. */
|
|
150
|
+
function decodeSig(hex) {
|
|
151
|
+
return (0, bcs_1.fromHex)(hex.startsWith("0x") ? hex.slice(2) : hex);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `waterx_rule::collect_batch_latest(collector, config, clock, enclave_config,
|
|
155
|
+
* enclave, timestamp_ms, payload, sig)` — rebuild the enclave-signed batch
|
|
156
|
+
* payload in-PTB (`new_batch_payload` + one `new_batch_item`/`push_batch_item`
|
|
157
|
+
* per item, the exact shape the enclave signed) and contribute the price for
|
|
158
|
+
* `collector.symbol()` to the collector. One collect call re-verifies the batch
|
|
159
|
+
* signature and picks this collector's symbol out of the batch; on-chain it
|
|
160
|
+
* abstains (records `none`) instead of aborting when the symbol is stale,
|
|
161
|
+
* absent from the batch, or its timestamp was already accepted (replay).
|
|
162
|
+
*/
|
|
163
|
+
function feedWaterxRule(tx, host, collector, envelope) {
|
|
164
|
+
const wr = requireWaterxPackage(host);
|
|
165
|
+
const pkg = wr.published_at;
|
|
166
|
+
const payload = (0, waterx_rule_ts_1.newBatchPayload)({ package: pkg })(tx);
|
|
167
|
+
for (const item of envelope.payload.items) {
|
|
168
|
+
// u64 fields are already exact bigints (see parseSignedEnvelope) — passed
|
|
169
|
+
// through verbatim so the rebuilt BCS matches the enclave's signed bytes.
|
|
170
|
+
const itemArg = (0, waterx_rule_ts_1.newBatchItem)({
|
|
171
|
+
package: pkg,
|
|
172
|
+
arguments: {
|
|
173
|
+
symbol: item.symbol,
|
|
174
|
+
ticker: item.ticker,
|
|
175
|
+
sources: item.sources,
|
|
176
|
+
method: item.method,
|
|
177
|
+
priceTimestampMs: item.price_timestamp_ms,
|
|
178
|
+
priceN: item.price_n,
|
|
179
|
+
priceScale: item.price_scale,
|
|
180
|
+
confidenceN: item.confidence_n,
|
|
181
|
+
confidenceScale: item.confidence_scale,
|
|
182
|
+
maxSourceDeviationBps: item.max_source_deviation_bps,
|
|
183
|
+
numSources: item.num_sources,
|
|
184
|
+
},
|
|
185
|
+
})(tx);
|
|
186
|
+
(0, waterx_rule_ts_1.pushBatchItem)({ package: pkg, arguments: { payload, item: itemArg } })(tx);
|
|
187
|
+
}
|
|
188
|
+
(0, waterx_rule_ts_1.collectBatchLatest)({
|
|
189
|
+
package: pkg,
|
|
190
|
+
arguments: {
|
|
191
|
+
collector,
|
|
192
|
+
config: tx.object(wr.config),
|
|
193
|
+
enclaveConfig: tx.object(wr.enclave_config),
|
|
194
|
+
enclave: tx.object(wr.enclave),
|
|
195
|
+
timestampMs: envelope.timestamp_ms,
|
|
196
|
+
payload,
|
|
197
|
+
sig: Array.from(decodeSig(envelope.signature)),
|
|
198
|
+
},
|
|
199
|
+
})(tx);
|
|
200
|
+
}
|
|
201
|
+
exports.WaterxRule = {
|
|
202
|
+
kind: "waterx_rule",
|
|
203
|
+
// Verification is an in-Move ed25519 check with no Coin argument — no
|
|
204
|
+
// update fee — see `PriceUpdateRule.requiresFeeSource`.
|
|
205
|
+
requiresFeeSource: false,
|
|
206
|
+
/** Tickers with a `waterx_rule.feeds` entry (keyed by oracle ticker). */
|
|
207
|
+
supportedTickers(host) {
|
|
208
|
+
return Object.keys(host.config.packages.waterx_rule?.feeds ?? {});
|
|
209
|
+
},
|
|
210
|
+
/**
|
|
211
|
+
* Pulls one enclave-signed batch envelope covering `tickers` from the
|
|
212
|
+
* quote-center, and only returns it when it actually covers ALL of them.
|
|
213
|
+
*
|
|
214
|
+
* A 200 whose `items` omit a requested symbol is a valid, well-signed
|
|
215
|
+
* envelope — nothing downstream would reject it, and the build would emit a
|
|
216
|
+
* `collect_batch_latest` that abstains for the missing symbol, surfacing as
|
|
217
|
+
* an on-chain `EMissingPriceSource` (or a silently thinner weighted set) much
|
|
218
|
+
* later. Same coverage rule the cached path enforces in
|
|
219
|
+
* {@link WaterxRule.narrowUpdateData}; the difference is disposition — a
|
|
220
|
+
* cache miss falls back to this live fetch, whereas the live source itself
|
|
221
|
+
* coming up short has no fallback left, so it throws deterministically here.
|
|
222
|
+
*/
|
|
223
|
+
async fetchUpdateData(host, tickers) {
|
|
224
|
+
if (tickers.length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
// Package-level check first: a config without the deployment must say so,
|
|
227
|
+
// not fail per ticker as if only that feed were missing.
|
|
228
|
+
const { feeds } = requireWaterxPackage(host);
|
|
229
|
+
for (const ticker of tickers) {
|
|
230
|
+
if (feeds[ticker] === undefined) {
|
|
231
|
+
throw new Error(`No waterx_rule feed listed for ticker: ${ticker}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const { endpoint, fetch: fetchOpts } = resolveWaterxInfra(host);
|
|
235
|
+
const envelope = await fetchWaterxSignedUpdate(endpoint, tickers, fetchOpts);
|
|
236
|
+
const covered = new Set(envelope.payload.items.map((i) => i.symbol));
|
|
237
|
+
const missing = tickers.filter((t) => !covered.has(t));
|
|
238
|
+
if (missing.length > 0) {
|
|
239
|
+
throw new Error(`WaterX quote-center envelope does not cover ticker(s): ${missing.join(", ")} ` +
|
|
240
|
+
`(requested ${tickers.join(", ")}; served ${[...covered].join(", ") || "none"})`);
|
|
241
|
+
}
|
|
242
|
+
return { kind: "waterx_rule", payload: { envelope } };
|
|
243
|
+
},
|
|
244
|
+
/**
|
|
245
|
+
* One signed batch envelope carries a single ed25519 signature over its whole
|
|
246
|
+
* `payload` — it is indivisible: it can only be served whole (re-verified from
|
|
247
|
+
* the full item set). Returns the whole payload iff every requested ticker's
|
|
248
|
+
* item is present in THIS envelope; any coverage gap → `null` (miss), never a
|
|
249
|
+
* silent partial.
|
|
250
|
+
*/
|
|
251
|
+
narrowUpdateData(_host, data, tickers) {
|
|
252
|
+
const envelope = waterxEnvelopeOf(data);
|
|
253
|
+
if (!envelope || tickers.length === 0)
|
|
254
|
+
return null;
|
|
255
|
+
const covered = new Set(envelope.payload.items.map((i) => i.symbol));
|
|
256
|
+
for (const ticker of tickers) {
|
|
257
|
+
if (!covered.has(ticker))
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return { kind: "waterx_rule", payload: { envelope } };
|
|
261
|
+
},
|
|
262
|
+
/**
|
|
263
|
+
* No shared verify step: `waterx_rule::collect_batch_latest` bundles verify
|
|
264
|
+
* AND feed into one per-collector call, appended by {@link feedWaterxRule} in
|
|
265
|
+
* the per-ticker aggregate leg. So this emits nothing and returns `void` — the
|
|
266
|
+
* signed envelope reaches the feed leg via `aggregate.ts`'s per-ticker map
|
|
267
|
+
* (built from the group's fetched data), not a `RuleUpdateHandle`.
|
|
268
|
+
*/
|
|
269
|
+
buildUpdateCalls(_tx, _host, _data, _opts) {
|
|
270
|
+
return;
|
|
271
|
+
},
|
|
272
|
+
};
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
* `Authorization` header at all). This is the Phase-0 invariant of the
|
|
17
17
|
* Pyth Pro migration: existing keyless deployments see no behavior change.
|
|
18
18
|
* - Retries on network errors, HTTP 429, and HTTP 5xx, with exponential
|
|
19
|
-
* backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`).
|
|
19
|
+
* backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). A 429
|
|
20
|
+
* carrying a numeric `Retry-After` header uses the SERVER'S delay instead,
|
|
21
|
+
* when it fits under the same cap — a longer ask degrades to normal
|
|
22
|
+
* backoff rather than stalling a money-path build for tens of seconds.
|
|
23
|
+
* Other
|
|
20
24
|
* 4xx statuses (401/400/403/404/…) are NOT retried — auth/bad-request
|
|
21
25
|
* failures are deterministic, so that `Response` (`ok: false`) is handed
|
|
22
26
|
* back on the first attempt for the caller to format its own
|
|
@@ -46,7 +50,7 @@
|
|
|
46
50
|
* - Retry worst case: with the defaults (15s timeout × 3 attempts + ~0.75s of
|
|
47
51
|
* backoff between them) a FULL outage takes up to ~46s to surface as a
|
|
48
52
|
* `FetchPolicyError`, vs ~15s pre-3.2.0's single bare-`fetch` attempt.
|
|
49
|
-
* Tunable per client via `
|
|
53
|
+
* Tunable per client via the `pythFetch` create option (`{timeoutMs,retries}`).
|
|
50
54
|
*/
|
|
51
55
|
export interface FetchPolicy {
|
|
52
56
|
/** Per-attempt timeout (ms). Default 15_000. */
|
|
@@ -75,6 +79,32 @@ export declare class FetchPolicyError extends Error {
|
|
|
75
79
|
attempts: number;
|
|
76
80
|
});
|
|
77
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Rethrow a `catch`-ed {@link fetchWithPolicy} failure. When it is a
|
|
84
|
+
* status-carrying `FetchPolicyError` — a retryable status (429/5xx) that never
|
|
85
|
+
* recovered — throw a new Error `${describe(err)} (retries exhausted after N
|
|
86
|
+
* attempts)` with the original as `cause`; otherwise (a network-level
|
|
87
|
+
* exhaustion with no status, or any non-`FetchPolicyError`) rethrow it verbatim,
|
|
88
|
+
* since there is no domain reframing to add. `describe` builds the
|
|
89
|
+
* status-bearing prefix so each caller keeps its own message shape (the e2e
|
|
90
|
+
* transient detector keys off those prefixes) while the guard, the `retries
|
|
91
|
+
* exhausted` suffix, and the `cause` wrapping live in one place. `: never` so a
|
|
92
|
+
* caller's `catch` block is understood not to fall through.
|
|
93
|
+
*/
|
|
94
|
+
export declare function rethrowExhaustedFetch(err: unknown, describe: (err: FetchPolicyError) => string): never;
|
|
95
|
+
/**
|
|
96
|
+
* Join an API `path` onto an `endpoint` PRESERVING the endpoint's own base
|
|
97
|
+
* path. `new URL(path, endpoint)` is the footgun this replaces: a
|
|
98
|
+
* leading-slash path is *absolute* and silently discards the endpoint's path
|
|
99
|
+
* — harmless for a bare-origin endpoint (`https://hermes.pyth.network`) but
|
|
100
|
+
* it dropped the `/hermes` prefix of the Pyth Pro compat endpoint and 404'd
|
|
101
|
+
* every feed (see `fetchPriceFeedsUpdateData`). Every oracle fetch that
|
|
102
|
+
* targets `<endpoint><fixed path>` must build its URL here.
|
|
103
|
+
*/
|
|
104
|
+
/** One canonical trailing-slash trim — `joinEndpointPath` (URL building) and
|
|
105
|
+
* `pyth.ts`'s `memoKey` (endpoint identity) must never drift apart on it. */
|
|
106
|
+
export declare function trimTrailingSlashes(endpoint: string): string;
|
|
107
|
+
export declare function joinEndpointPath(endpoint: string, path: string): URL;
|
|
78
108
|
/**
|
|
79
109
|
* `fetch` with per-attempt timeout, bounded retry + backoff, and optional
|
|
80
110
|
* Bearer auth. See the module header for the full policy. Both `init.signal`
|