@waterx/sdk 3.1.0 → 4.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/README.md +53 -7
- package/dist/cjs/src/oracle/aggregate.d.ts +98 -17
- package/dist/cjs/src/oracle/aggregate.js +191 -21
- package/dist/cjs/src/oracle/config.d.ts +103 -0
- package/dist/cjs/src/oracle/config.js +64 -1
- package/dist/cjs/src/oracle/host.d.ts +13 -0
- package/dist/cjs/src/oracle/index.d.ts +19 -5
- package/dist/cjs/src/oracle/index.js +40 -6
- package/dist/cjs/src/oracle/price-update-rule.d.ts +180 -0
- package/dist/cjs/src/oracle/price-update-rule.js +56 -0
- package/dist/cjs/src/oracle/pyth.d.ts +80 -11
- package/dist/cjs/src/oracle/pyth.js +84 -17
- package/dist/cjs/src/oracle/rule-registry.d.ts +37 -0
- package/dist/cjs/src/oracle/rule-registry.js +61 -0
- package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +15 -0
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +84 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +194 -0
- package/dist/cjs/src/oracle/rules/sponsor.d.ts +11 -7
- package/dist/cjs/src/oracle/rules/sponsor.js +11 -7
- package/dist/cjs/src/oracle/update-fetch.d.ts +85 -0
- package/dist/cjs/src/oracle/update-fetch.js +228 -0
- package/dist/cjs/src/perp/client.d.ts +22 -1
- package/dist/cjs/src/perp/client.js +11 -2
- package/dist/cjs/src/perp/config.d.ts +15 -20
- package/dist/cjs/src/perp/config.js +86 -30
- package/dist/cjs/src/perp/index.d.ts +4 -3
- package/dist/cjs/src/perp/index.js +8 -4
- package/dist/cjs/src/perp/tx-builders/common.d.ts +52 -15
- package/dist/cjs/src/perp/tx-builders/common.js +39 -6
- package/dist/cjs/src/perp/tx-builders/wlp.d.ts +11 -3
- package/dist/cjs/src/perp/tx-builders/wlp.js +29 -3
- package/dist/cjs/src/perp/tx-builders.d.ts +3 -3
- package/dist/cjs/src/perp/tx-builders.js +3 -3
- package/dist/cjs/src/prediction/config.d.ts +5 -15
- package/dist/cjs/src/prediction/config.js +4 -12
- package/dist/cjs/src/prediction/fetch.d.ts +6 -1
- package/dist/cjs/src/prediction/fetch.js +64 -0
- package/dist/cjs/src/prediction/index.d.ts +2 -2
- package/dist/cjs/src/prediction/index.js +7 -4
- package/dist/cjs/src/prediction/types.d.ts +19 -0
- package/dist/cjs/src/unified-client.d.ts +26 -4
- package/dist/cjs/src/unified-client.js +4 -2
- package/dist/src/oracle/aggregate.d.ts +98 -17
- package/dist/src/oracle/aggregate.js +192 -22
- package/dist/src/oracle/config.d.ts +103 -0
- package/dist/src/oracle/config.js +63 -0
- package/dist/src/oracle/host.d.ts +13 -0
- package/dist/src/oracle/index.d.ts +19 -5
- package/dist/src/oracle/index.js +34 -6
- package/dist/src/oracle/price-update-rule.d.ts +180 -0
- package/dist/src/oracle/price-update-rule.js +53 -0
- package/dist/src/oracle/pyth.d.ts +80 -11
- package/dist/src/oracle/pyth.js +82 -16
- package/dist/src/oracle/rule-registry.d.ts +37 -0
- package/dist/src/oracle/rule-registry.js +56 -0
- package/dist/src/oracle/rules/pyth-core-rule.d.ts +15 -0
- package/dist/src/oracle/rules/pyth-core-rule.js +81 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +189 -0
- package/dist/src/oracle/rules/sponsor.d.ts +11 -7
- package/dist/src/oracle/rules/sponsor.js +11 -7
- package/dist/src/oracle/update-fetch.d.ts +85 -0
- package/dist/src/oracle/update-fetch.js +223 -0
- package/dist/src/perp/client.d.ts +22 -1
- package/dist/src/perp/client.js +12 -3
- package/dist/src/perp/config.d.ts +15 -20
- package/dist/src/perp/config.js +85 -29
- package/dist/src/perp/index.d.ts +4 -3
- package/dist/src/perp/index.js +2 -2
- package/dist/src/perp/tx-builders/common.d.ts +52 -15
- package/dist/src/perp/tx-builders/common.js +39 -6
- package/dist/src/perp/tx-builders/wlp.d.ts +11 -3
- package/dist/src/perp/tx-builders/wlp.js +29 -3
- package/dist/src/perp/tx-builders.d.ts +3 -3
- package/dist/src/perp/tx-builders.js +3 -3
- package/dist/src/prediction/config.d.ts +5 -15
- package/dist/src/prediction/config.js +4 -11
- package/dist/src/prediction/fetch.d.ts +6 -1
- package/dist/src/prediction/fetch.js +60 -0
- package/dist/src/prediction/index.d.ts +2 -2
- package/dist/src/prediction/index.js +2 -2
- package/dist/src/prediction/types.d.ts +19 -0
- package/dist/src/unified-client.d.ts +26 -4
- package/dist/src/unified-client.js +4 -2
- package/package.json +1 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `PythLazerRule` — `PriceUpdateRule` for Pyth Lazer (Pyth Pro) signed
|
|
4
|
+
* updates, plus `feedLazerRule`, the collector-feed leg `aggregateTicker`
|
|
5
|
+
* appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
|
|
6
|
+
* requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
|
|
7
|
+
* via `config.pyth.api_key`), verifies it ONCE on-chain via
|
|
8
|
+
* `pyth_lazer::parse_and_verify_le_ecdsa_update`, and hands the resulting
|
|
9
|
+
* `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PythLazerRule = exports.LazerApiKeyMissingError = void 0;
|
|
13
|
+
exports.feedLazerRule = feedLazerRule;
|
|
14
|
+
const bcs_1 = require("@mysten/bcs");
|
|
15
|
+
const config_ts_1 = require("../config.js");
|
|
16
|
+
const price_update_rule_ts_1 = require("../price-update-rule.js");
|
|
17
|
+
const update_fetch_ts_1 = require("../update-fetch.js");
|
|
18
|
+
/**
|
|
19
|
+
* Signed-update request pins, mirroring what the on-chain rule consumes:
|
|
20
|
+
* - `properties` — `price` + `exponent` are REQUIRED by
|
|
21
|
+
* `pyth_lazer_rule::price_or_abstain` (a missing exponent abstains);
|
|
22
|
+
* `confidence` is optional on-chain but requested so the rule's
|
|
23
|
+
* fail-closed confidence gate actually engages (a payload without
|
|
24
|
+
* confidence passes the gate unchecked).
|
|
25
|
+
* - `channel` — `real_time`: the deployed rule binds the v1 Lazer API, whose
|
|
26
|
+
* `channel::from_u8` aborts on the 1000ms fixed-rate channel; real_time /
|
|
27
|
+
* 50ms / 200ms are the safe subscriptions, and for an on-demand pull
|
|
28
|
+
* real_time is the freshest.
|
|
29
|
+
* - `formats: leEcdsa` + `jsonBinaryEncoding: hex` — the Sui verifier takes
|
|
30
|
+
* the `leEcdsa` framing; hex matches `fromHex` below.
|
|
31
|
+
*/
|
|
32
|
+
const LAZER_LATEST_PRICE_REQUEST = {
|
|
33
|
+
properties: ["price", "exponent", "confidence"],
|
|
34
|
+
formats: ["leEcdsa"],
|
|
35
|
+
jsonBinaryEncoding: "hex",
|
|
36
|
+
channel: "real_time",
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
40
|
+
* caller before this runs (mirrors `PythCoreRule`'s guard split), so a
|
|
41
|
+
* same-shaped payload from a different rule can never silently pass.
|
|
42
|
+
*/
|
|
43
|
+
function isPythLazerUpdatePayloadShape(payload) {
|
|
44
|
+
return (typeof payload === "object" &&
|
|
45
|
+
payload !== null &&
|
|
46
|
+
payload.update instanceof Uint8Array &&
|
|
47
|
+
Array.isArray(payload.feedIds));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
|
|
51
|
+
* deployed in config but no `pyth.api_key` is set — the Lazer HTTP API
|
|
52
|
+
* requires a Bearer token and the SDK never reads `process.env` to find one.
|
|
53
|
+
* `instanceof`-able (mirrors `OracleFeeSourceUnavailableError` in `pyth.ts`)
|
|
54
|
+
* so a consumer can branch on the failure type directly instead of
|
|
55
|
+
* string-matching `error.message`.
|
|
56
|
+
*/
|
|
57
|
+
class LazerApiKeyMissingError extends Error {
|
|
58
|
+
constructor() {
|
|
59
|
+
super("LazerApiKeyMissing: pyth_lazer_rule requires a Pyth Lazer access token — " +
|
|
60
|
+
"set `pyth.api_key` in the client config (the SDK never reads process.env)");
|
|
61
|
+
this.name = "LazerApiKeyMissingError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.LazerApiKeyMissingError = LazerApiKeyMissingError;
|
|
65
|
+
/** The `pyth_lazer_rule` deployment entry; throws when the config carries none. */
|
|
66
|
+
function requireLazerPackage(host) {
|
|
67
|
+
const entry = host.config.packages.pyth_lazer_rule;
|
|
68
|
+
if (!entry) {
|
|
69
|
+
throw new Error("pyth_lazer_rule package is not deployed in this config");
|
|
70
|
+
}
|
|
71
|
+
return entry;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Fetch one signed `leEcdsa` update for `feedIds` from the Lazer HTTP API.
|
|
75
|
+
* Goes through the shared `fetchWithPolicy` (`../update-fetch.ts`) — same
|
|
76
|
+
* retry/timeout/Bearer policy as `fetchPriceFeedsUpdateData`, unified so
|
|
77
|
+
* both oracle sources fail the same way under upstream degradation.
|
|
78
|
+
*/
|
|
79
|
+
async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
|
|
80
|
+
const url = new URL("/v1/latest_price", endpoint);
|
|
81
|
+
let res;
|
|
82
|
+
try {
|
|
83
|
+
res = await (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST }),
|
|
87
|
+
}, { apiKey, ...fetchOpts });
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// Mirrors fetchPriceFeedsUpdateData's reframing: a retryable status that
|
|
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;
|
|
99
|
+
}
|
|
100
|
+
if (!res.ok)
|
|
101
|
+
throw new Error(`Lazer price fetch failed: ${res.status} ${await res.text()}`);
|
|
102
|
+
const json = (await res.json());
|
|
103
|
+
const hex = json.leEcdsa?.data;
|
|
104
|
+
if (typeof hex !== "string" || hex.length === 0) {
|
|
105
|
+
throw new Error("Lazer returned no leEcdsa update data");
|
|
106
|
+
}
|
|
107
|
+
return (0, bcs_1.fromHex)(hex);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* `pyth_lazer_rule::feed(collector, config, clock, &update)` — contribute the
|
|
111
|
+
* verified Lazer price for `collector.symbol()` to the collector. `update` is
|
|
112
|
+
* the `RuleUpdateHandle` value from {@link PythLazerRule.buildUpdateCalls} in
|
|
113
|
+
* the SAME PTB; one verified update serves every ticker's feed call. On-chain
|
|
114
|
+
* the rule abstains (records `none`) instead of aborting when the symbol is
|
|
115
|
+
* unconfigured, the feed is absent from the update, the value is degenerate,
|
|
116
|
+
* or the Lazer timestamp is stale.
|
|
117
|
+
*/
|
|
118
|
+
function feedLazerRule(tx, host, collector, update) {
|
|
119
|
+
const lazer = requireLazerPackage(host);
|
|
120
|
+
tx.moveCall({
|
|
121
|
+
target: `${lazer.published_at}::pyth_lazer_rule::feed`,
|
|
122
|
+
arguments: [collector, tx.object(lazer.config), tx.object.clock(), update],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
exports.PythLazerRule = {
|
|
126
|
+
kind: "pyth_lazer_rule",
|
|
127
|
+
// Verification is a flat signature check with no Coin argument — no
|
|
128
|
+
// update fee — see `PriceUpdateRule.requiresFeeSource`.
|
|
129
|
+
requiresFeeSource: false,
|
|
130
|
+
/** Tickers with a `pyth_lazer_rule.feeds` entry (integer Lazer feed ids). */
|
|
131
|
+
supportedTickers(host) {
|
|
132
|
+
return Object.keys(host.config.packages.pyth_lazer_rule?.feeds ?? {});
|
|
133
|
+
},
|
|
134
|
+
/** Resolves integer feed ids for `tickers`, then fetches one signed `leEcdsa` update. */
|
|
135
|
+
async fetchUpdateData(host, tickers) {
|
|
136
|
+
if (tickers.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
// Package-level check first: a config without the deployment must say so,
|
|
139
|
+
// not fail per ticker as if only that feed were missing.
|
|
140
|
+
const { feeds } = requireLazerPackage(host);
|
|
141
|
+
const feedIds = tickers.map((ticker) => {
|
|
142
|
+
const feedId = feeds[ticker];
|
|
143
|
+
if (feedId === undefined) {
|
|
144
|
+
throw new Error(`No pyth_lazer_rule feed listed for ticker: ${ticker}`);
|
|
145
|
+
}
|
|
146
|
+
return feedId;
|
|
147
|
+
});
|
|
148
|
+
const apiKey = host.pyth.api_key;
|
|
149
|
+
if (!apiKey) {
|
|
150
|
+
throw new LazerApiKeyMissingError();
|
|
151
|
+
}
|
|
152
|
+
const update = await fetchLazerSignedUpdate(config_ts_1.LAZER_DEFAULTS[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
|
|
153
|
+
return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
|
|
154
|
+
},
|
|
155
|
+
/**
|
|
156
|
+
* A Lazer payload is ONE signed `leEcdsa` message covering every feed it was
|
|
157
|
+
* fetched for — verification is a single flat signature check over the whole
|
|
158
|
+
* message (`parse_and_verify_le_ecdsa_update`, no per-feed cost), so the
|
|
159
|
+
* payload is indivisible: it can only be served whole. Returns the whole
|
|
160
|
+
* payload iff every requested ticker's integer feed id is packed in THIS
|
|
161
|
+
* payload's `feedIds`; any coverage gap (unlisted ticker, or a feed this
|
|
162
|
+
* payload does not carry) → `null` (miss), never a silent partial.
|
|
163
|
+
*/
|
|
164
|
+
narrowUpdateData(host, data, tickers) {
|
|
165
|
+
const payload = (0, price_update_rule_ts_1.assertRuleUpdateData)(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
|
|
166
|
+
if (!payload || tickers.length === 0)
|
|
167
|
+
return null;
|
|
168
|
+
const packedFeedIds = new Set(payload.feedIds);
|
|
169
|
+
for (const ticker of tickers) {
|
|
170
|
+
const feedId = host.config.packages.pyth_lazer_rule?.feeds?.[ticker];
|
|
171
|
+
if (feedId === undefined || !packedFeedIds.has(feedId))
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return { kind: "pyth_lazer_rule", payload };
|
|
175
|
+
},
|
|
176
|
+
/**
|
|
177
|
+
* Appends the single `parse_and_verify_le_ecdsa_update(state, clock, bytes)`
|
|
178
|
+
* call — one secp256k1 signature check covering every feed in the payload —
|
|
179
|
+
* and returns its `Update` result as the handle the per-ticker feed leg
|
|
180
|
+
* consumes. `opts.cache` / `opts.feeSource` are Pyth-Core-specific and
|
|
181
|
+
* ignored (Lazer verification charges no update fee).
|
|
182
|
+
*/
|
|
183
|
+
buildUpdateCalls(tx, host, data, _opts) {
|
|
184
|
+
const payload = (0, price_update_rule_ts_1.assertRuleUpdateData)(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
|
|
185
|
+
if (!payload)
|
|
186
|
+
return undefined;
|
|
187
|
+
const lazer = requireLazerPackage(host);
|
|
188
|
+
const [update] = tx.moveCall({
|
|
189
|
+
target: `${config_ts_1.LAZER_DEFAULTS[host.network].verifier_package}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
|
|
190
|
+
arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
|
|
191
|
+
});
|
|
192
|
+
return { kind: "pyth_lazer_rule", update };
|
|
193
|
+
},
|
|
194
|
+
};
|
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
* attach the `PythSponsorRule` witness to a `TradingRequest`. Required when the
|
|
4
4
|
* market's `request_checklist` contains `PythSponsorRule`.
|
|
5
5
|
*
|
|
6
|
-
* Flow: {@link openPythSponsorFund} opens a `Fund` hot potato;
|
|
7
|
-
* `{ fund, packageId }`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Flow: {@link openPythSponsorFund} opens a `Fund` hot potato; the caller wraps
|
|
7
|
+
* the returned `{ fund, packageId }` into an `OracleFeeSource` (`{ kind:
|
|
8
|
+
* 'sponsor', fund, packageId }`) and passes that to the Pyth update path (it
|
|
9
|
+
* draws per-feed fees via `pyth_sponsor_rule::split`); then
|
|
10
|
+
* {@link reimbursePythSponsor} consumes the `Fund`, returns leftover SUI, and
|
|
11
|
+
* attaches the witness.
|
|
10
12
|
*/
|
|
11
13
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
12
14
|
import type { OracleHost } from "../host.ts";
|
|
13
15
|
/**
|
|
14
|
-
* Opens a `Fund` hot potato from the shared PythSponsor pool.
|
|
15
|
-
* `{ fund, packageId }`
|
|
16
|
-
* {
|
|
16
|
+
* Opens a `Fund` hot potato from the shared PythSponsor pool. Wrap the
|
|
17
|
+
* returned `{ fund, packageId }` into an `OracleFeeSource`
|
|
18
|
+
* (`{ kind: 'sponsor', fund, packageId }`) and pass that to
|
|
19
|
+
* `refreshOraclePrices` as `feeSource`, then {@link reimbursePythSponsor} once
|
|
20
|
+
* the TradingRequest is built.
|
|
17
21
|
*/
|
|
18
22
|
export declare function openPythSponsorFund(tx: Transaction, host: OracleHost): {
|
|
19
23
|
fund: TransactionArgument;
|
|
@@ -4,19 +4,23 @@
|
|
|
4
4
|
* attach the `PythSponsorRule` witness to a `TradingRequest`. Required when the
|
|
5
5
|
* market's `request_checklist` contains `PythSponsorRule`.
|
|
6
6
|
*
|
|
7
|
-
* Flow: {@link openPythSponsorFund} opens a `Fund` hot potato;
|
|
8
|
-
* `{ fund, packageId }`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Flow: {@link openPythSponsorFund} opens a `Fund` hot potato; the caller wraps
|
|
8
|
+
* the returned `{ fund, packageId }` into an `OracleFeeSource` (`{ kind:
|
|
9
|
+
* 'sponsor', fund, packageId }`) and passes that to the Pyth update path (it
|
|
10
|
+
* draws per-feed fees via `pyth_sponsor_rule::split`); then
|
|
11
|
+
* {@link reimbursePythSponsor} consumes the `Fund`, returns leftover SUI, and
|
|
12
|
+
* attaches the witness.
|
|
11
13
|
*/
|
|
12
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
15
|
exports.openPythSponsorFund = openPythSponsorFund;
|
|
14
16
|
exports.reimbursePythSponsor = reimbursePythSponsor;
|
|
15
17
|
const pyth_sponsor_rule_ts_1 = require("../../generated/pyth_sponsor_rule/pyth_sponsor_rule.js");
|
|
16
18
|
/**
|
|
17
|
-
* Opens a `Fund` hot potato from the shared PythSponsor pool.
|
|
18
|
-
* `{ fund, packageId }`
|
|
19
|
-
* {
|
|
19
|
+
* Opens a `Fund` hot potato from the shared PythSponsor pool. Wrap the
|
|
20
|
+
* returned `{ fund, packageId }` into an `OracleFeeSource`
|
|
21
|
+
* (`{ kind: 'sponsor', fund, packageId }`) and pass that to
|
|
22
|
+
* `refreshOraclePrices` as `feeSource`, then {@link reimbursePythSponsor} once
|
|
23
|
+
* the TradingRequest is built.
|
|
20
24
|
*/
|
|
21
25
|
function openPythSponsorFund(tx, host) {
|
|
22
26
|
const entry = host.config.packages.pyth_sponsor_rule;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fetchWithPolicy` — resilience wrapper around `fetch` for the oracle money
|
|
3
|
+
* path's off-chain update-data fetches. Every order/position/collateral
|
|
4
|
+
* tx-build depends on one of these REST calls landing (Hermes VAA for
|
|
5
|
+
* `pyth_rule`, Lazer signed updates for `pyth_lazer_rule`); a bare `fetch`
|
|
6
|
+
* with a single attempt and no retry means one Hermes 429/5xx or timeout
|
|
7
|
+
* fails every trade. `fetchPriceFeedsUpdateData` (`./pyth.ts`),
|
|
8
|
+
* `PythLazerRule.fetchUpdateData`'s Lazer POST (`./rules/pyth-lazer-rule.ts`),
|
|
9
|
+
* and `loadConfig` (`../perp/config.ts`) all delegate here instead of calling
|
|
10
|
+
* `fetch` directly — this is the ONE place a retry/timeout/auth policy is
|
|
11
|
+
* implemented for these fetches.
|
|
12
|
+
*
|
|
13
|
+
* Policy semantics:
|
|
14
|
+
* - Bearer auth is attached iff `policy.apiKey` is a non-empty string —
|
|
15
|
+
* absent/empty is byte-identical to today's keyless request (no
|
|
16
|
+
* `Authorization` header at all). This is the Phase-0 invariant of the
|
|
17
|
+
* Pyth Pro migration: existing keyless deployments see no behavior change.
|
|
18
|
+
* - Retries on network errors, HTTP 429, and HTTP 5xx, with exponential
|
|
19
|
+
* backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). Other
|
|
20
|
+
* 4xx statuses (401/400/403/404/…) are NOT retried — auth/bad-request
|
|
21
|
+
* failures are deterministic, so that `Response` (`ok: false`) is handed
|
|
22
|
+
* back on the first attempt for the caller to format its own
|
|
23
|
+
* domain-specific error, exactly as it did before this wrapper existed.
|
|
24
|
+
* - `init.body`, if set, MUST be replayable across attempts — a retry
|
|
25
|
+
* re-sends the SAME `init` object to `fetch` on every attempt. A string /
|
|
26
|
+
* `URLSearchParams` / BCS-serialized `Uint8Array` body (every caller today)
|
|
27
|
+
* is fine; a one-shot `ReadableStream` body would not survive a second
|
|
28
|
+
* attempt and must not be passed through this function.
|
|
29
|
+
* - Each attempt gets its own `AbortSignal.timeout(policy.timeoutMs)`
|
|
30
|
+
* combined with whichever of `init.signal` / the `externalSignal` param are
|
|
31
|
+
* set — ALL of them can end the whole policy (not just the in-flight
|
|
32
|
+
* attempt), including a queued backoff sleep, via `AbortSignal.any`
|
|
33
|
+
* (runtime floor: Node ≥20.3 / any modern browser — matches this repo's
|
|
34
|
+
* `target: ES2023` + `lib: ["dom", "esnext"]`).
|
|
35
|
+
* - Exhausting retries with no successful/non-retryable response (i.e. every
|
|
36
|
+
* attempt was a network error, or the final attempt was still a retryable
|
|
37
|
+
* HTTP failure) throws a {@link FetchPolicyError} naming the target's
|
|
38
|
+
* `host + pathname` (never the query string — feed ids are off-chain
|
|
39
|
+
* noise, not diagnostic value), the attempt count, and whichever of
|
|
40
|
+
* `status` (a retryable HTTP failure — plus a truncated response-body
|
|
41
|
+
* snippet, when the final attempt's response carried one) or `cause` (a
|
|
42
|
+
* network error) the final attempt produced. An INTERMEDIATE (non-final)
|
|
43
|
+
* retryable response's body is discarded via `response.body?.cancel()`
|
|
44
|
+
* instead of read, so a doomed-to-retry response doesn't pin its
|
|
45
|
+
* connection's socket open for no reason.
|
|
46
|
+
* - Retry worst case: with the defaults (15s timeout × 3 attempts + ~0.75s of
|
|
47
|
+
* backoff between them) a FULL outage takes up to ~46s to surface as a
|
|
48
|
+
* `FetchPolicyError`, vs ~15s pre-3.2.0's single bare-`fetch` attempt.
|
|
49
|
+
* Tunable per client via `config.pyth.fetch.{timeoutMs,retries}`.
|
|
50
|
+
*/
|
|
51
|
+
export interface FetchPolicy {
|
|
52
|
+
/** Per-attempt timeout (ms). Default 15_000. */
|
|
53
|
+
readonly timeoutMs?: number;
|
|
54
|
+
/** Retry attempts AFTER the first try — `retries: 2` ⇒ 3 attempts total. Default 2. */
|
|
55
|
+
readonly retries?: number;
|
|
56
|
+
/** Base backoff (ms); doubled per attempt, capped at 2_000ms. Default 250. */
|
|
57
|
+
readonly retryDelayMs?: number;
|
|
58
|
+
/** Bearer token. Attached iff non-empty; empty/missing ⇒ no `Authorization` header. */
|
|
59
|
+
readonly apiKey?: string;
|
|
60
|
+
/** Override the fetch implementation (tests / non-global-`fetch` environments). Default: global `fetch`. */
|
|
61
|
+
readonly fetchImpl?: typeof fetch;
|
|
62
|
+
}
|
|
63
|
+
/** Thrown by {@link fetchWithPolicy} when every attempt failed. */
|
|
64
|
+
export declare class FetchPolicyError extends Error {
|
|
65
|
+
/** HTTP status of the final attempt, when it got a (retryable-but-failing) response. */
|
|
66
|
+
readonly status?: number;
|
|
67
|
+
/** Truncated (~200 char) body of the final attempt's response, when one was readable. */
|
|
68
|
+
readonly bodySnippet?: string;
|
|
69
|
+
/** Total attempts made (first try + retries actually used). */
|
|
70
|
+
readonly attempts: number;
|
|
71
|
+
constructor(message: string, opts: {
|
|
72
|
+
status?: number;
|
|
73
|
+
bodySnippet?: string;
|
|
74
|
+
cause?: unknown;
|
|
75
|
+
attempts: number;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* `fetch` with per-attempt timeout, bounded retry + backoff, and optional
|
|
80
|
+
* Bearer auth. See the module header for the full policy. Both `init.signal`
|
|
81
|
+
* (if the caller set one) AND the separate `externalSignal` param cancel the
|
|
82
|
+
* WHOLE policy — in-flight attempts AND queued backoff sleeps — not just a
|
|
83
|
+
* single attempt.
|
|
84
|
+
*/
|
|
85
|
+
export declare function fetchWithPolicy(url: string, init?: RequestInit, policy?: FetchPolicy, externalSignal?: AbortSignal): Promise<Response>;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `fetchWithPolicy` — resilience wrapper around `fetch` for the oracle money
|
|
4
|
+
* path's off-chain update-data fetches. Every order/position/collateral
|
|
5
|
+
* tx-build depends on one of these REST calls landing (Hermes VAA for
|
|
6
|
+
* `pyth_rule`, Lazer signed updates for `pyth_lazer_rule`); a bare `fetch`
|
|
7
|
+
* with a single attempt and no retry means one Hermes 429/5xx or timeout
|
|
8
|
+
* fails every trade. `fetchPriceFeedsUpdateData` (`./pyth.ts`),
|
|
9
|
+
* `PythLazerRule.fetchUpdateData`'s Lazer POST (`./rules/pyth-lazer-rule.ts`),
|
|
10
|
+
* and `loadConfig` (`../perp/config.ts`) all delegate here instead of calling
|
|
11
|
+
* `fetch` directly — this is the ONE place a retry/timeout/auth policy is
|
|
12
|
+
* implemented for these fetches.
|
|
13
|
+
*
|
|
14
|
+
* Policy semantics:
|
|
15
|
+
* - Bearer auth is attached iff `policy.apiKey` is a non-empty string —
|
|
16
|
+
* absent/empty is byte-identical to today's keyless request (no
|
|
17
|
+
* `Authorization` header at all). This is the Phase-0 invariant of the
|
|
18
|
+
* Pyth Pro migration: existing keyless deployments see no behavior change.
|
|
19
|
+
* - Retries on network errors, HTTP 429, and HTTP 5xx, with exponential
|
|
20
|
+
* backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). Other
|
|
21
|
+
* 4xx statuses (401/400/403/404/…) are NOT retried — auth/bad-request
|
|
22
|
+
* failures are deterministic, so that `Response` (`ok: false`) is handed
|
|
23
|
+
* back on the first attempt for the caller to format its own
|
|
24
|
+
* domain-specific error, exactly as it did before this wrapper existed.
|
|
25
|
+
* - `init.body`, if set, MUST be replayable across attempts — a retry
|
|
26
|
+
* re-sends the SAME `init` object to `fetch` on every attempt. A string /
|
|
27
|
+
* `URLSearchParams` / BCS-serialized `Uint8Array` body (every caller today)
|
|
28
|
+
* is fine; a one-shot `ReadableStream` body would not survive a second
|
|
29
|
+
* attempt and must not be passed through this function.
|
|
30
|
+
* - Each attempt gets its own `AbortSignal.timeout(policy.timeoutMs)`
|
|
31
|
+
* combined with whichever of `init.signal` / the `externalSignal` param are
|
|
32
|
+
* set — ALL of them can end the whole policy (not just the in-flight
|
|
33
|
+
* attempt), including a queued backoff sleep, via `AbortSignal.any`
|
|
34
|
+
* (runtime floor: Node ≥20.3 / any modern browser — matches this repo's
|
|
35
|
+
* `target: ES2023` + `lib: ["dom", "esnext"]`).
|
|
36
|
+
* - Exhausting retries with no successful/non-retryable response (i.e. every
|
|
37
|
+
* attempt was a network error, or the final attempt was still a retryable
|
|
38
|
+
* HTTP failure) throws a {@link FetchPolicyError} naming the target's
|
|
39
|
+
* `host + pathname` (never the query string — feed ids are off-chain
|
|
40
|
+
* noise, not diagnostic value), the attempt count, and whichever of
|
|
41
|
+
* `status` (a retryable HTTP failure — plus a truncated response-body
|
|
42
|
+
* snippet, when the final attempt's response carried one) or `cause` (a
|
|
43
|
+
* network error) the final attempt produced. An INTERMEDIATE (non-final)
|
|
44
|
+
* retryable response's body is discarded via `response.body?.cancel()`
|
|
45
|
+
* instead of read, so a doomed-to-retry response doesn't pin its
|
|
46
|
+
* connection's socket open for no reason.
|
|
47
|
+
* - Retry worst case: with the defaults (15s timeout × 3 attempts + ~0.75s of
|
|
48
|
+
* backoff between them) a FULL outage takes up to ~46s to surface as a
|
|
49
|
+
* `FetchPolicyError`, vs ~15s pre-3.2.0's single bare-`fetch` attempt.
|
|
50
|
+
* Tunable per client via `config.pyth.fetch.{timeoutMs,retries}`.
|
|
51
|
+
*/
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.FetchPolicyError = void 0;
|
|
54
|
+
exports.fetchWithPolicy = fetchWithPolicy;
|
|
55
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
56
|
+
const DEFAULT_RETRIES = 2;
|
|
57
|
+
const DEFAULT_RETRY_DELAY_MS = 250;
|
|
58
|
+
const MAX_BACKOFF_MS = 2_000;
|
|
59
|
+
/** Final-error diagnostic only — not a protocol limit. */
|
|
60
|
+
const MAX_BODY_SNIPPET_LENGTH = 200;
|
|
61
|
+
/** Thrown by {@link fetchWithPolicy} when every attempt failed. */
|
|
62
|
+
class FetchPolicyError extends Error {
|
|
63
|
+
/** HTTP status of the final attempt, when it got a (retryable-but-failing) response. */
|
|
64
|
+
status;
|
|
65
|
+
/** Truncated (~200 char) body of the final attempt's response, when one was readable. */
|
|
66
|
+
bodySnippet;
|
|
67
|
+
/** Total attempts made (first try + retries actually used). */
|
|
68
|
+
attempts;
|
|
69
|
+
constructor(message, opts) {
|
|
70
|
+
super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);
|
|
71
|
+
this.name = "FetchPolicyError";
|
|
72
|
+
this.status = opts.status;
|
|
73
|
+
this.bodySnippet = opts.bodySnippet;
|
|
74
|
+
this.attempts = opts.attempts;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.FetchPolicyError = FetchPolicyError;
|
|
78
|
+
function isRetryableStatus(status) {
|
|
79
|
+
return status === 429 || status >= 500;
|
|
80
|
+
}
|
|
81
|
+
function backoffMs(retryDelayMs, attempt) {
|
|
82
|
+
return Math.min(retryDelayMs * 2 ** attempt, MAX_BACKOFF_MS);
|
|
83
|
+
}
|
|
84
|
+
/** `host + pathname` only — never the query string (feed ids are noise, not diagnostic). */
|
|
85
|
+
function describeTarget(url) {
|
|
86
|
+
const parsed = new URL(url);
|
|
87
|
+
return `${parsed.host}${parsed.pathname}`;
|
|
88
|
+
}
|
|
89
|
+
function causeMessage(cause) {
|
|
90
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Best-effort, truncated body text for the FINAL failed attempt's error
|
|
94
|
+
* message — restores the diagnostic a plain `if (!res.ok) throw new
|
|
95
|
+
* Error(...res.status, await res.text())` had before this wrapper existed.
|
|
96
|
+
* Swallows any read failure (missing/consumed body, a test double with no
|
|
97
|
+
* `.text()`, …) — a diagnostic snippet is never worth failing the request
|
|
98
|
+
* differently than the status/cause already dictate.
|
|
99
|
+
*/
|
|
100
|
+
async function readBodySnippet(response) {
|
|
101
|
+
try {
|
|
102
|
+
const text = await response.text();
|
|
103
|
+
return text.length > MAX_BODY_SNIPPET_LENGTH
|
|
104
|
+
? `${text.slice(0, MAX_BODY_SNIPPET_LENGTH)}…`
|
|
105
|
+
: text;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return "";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Resolves after `ms`, or rejects immediately (with `signal.reason`) if `signal` fires first. */
|
|
112
|
+
function sleep(ms, signal) {
|
|
113
|
+
if (!signal)
|
|
114
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
115
|
+
const abortSignal = signal;
|
|
116
|
+
if (abortSignal.aborted)
|
|
117
|
+
return Promise.reject(abortSignal.reason);
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const onAbort = () => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
reject(abortSignal.reason);
|
|
122
|
+
};
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
125
|
+
resolve();
|
|
126
|
+
}, ms);
|
|
127
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** `undefined` if none are set; the lone signal if exactly one is; `AbortSignal.any(...)` otherwise. */
|
|
131
|
+
function combineSignals(signals) {
|
|
132
|
+
const present = signals.filter((s) => s != null);
|
|
133
|
+
if (present.length === 0)
|
|
134
|
+
return undefined;
|
|
135
|
+
if (present.length === 1)
|
|
136
|
+
return present[0];
|
|
137
|
+
return AbortSignal.any(present);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Merge `Authorization: Bearer <apiKey>` into `base` iff `apiKey` is
|
|
141
|
+
* non-empty; `base` is returned UNTOUCHED otherwise (byte-identical to
|
|
142
|
+
* today's keyless requests — no header, whatever shape the caller passed).
|
|
143
|
+
* Always builds a plain object (never a `Headers` instance) so callers that
|
|
144
|
+
* assert on the exact `init` they handed `fetch` keep working unchanged.
|
|
145
|
+
*/
|
|
146
|
+
function withBearerAuth(base, apiKey) {
|
|
147
|
+
if (!apiKey)
|
|
148
|
+
return base;
|
|
149
|
+
const merged = {};
|
|
150
|
+
if (base instanceof Headers) {
|
|
151
|
+
base.forEach((value, key) => {
|
|
152
|
+
merged[key] = value;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else if (Array.isArray(base)) {
|
|
156
|
+
for (const [key, value] of base)
|
|
157
|
+
merged[key] = value;
|
|
158
|
+
}
|
|
159
|
+
else if (base) {
|
|
160
|
+
Object.assign(merged, base);
|
|
161
|
+
}
|
|
162
|
+
merged.Authorization = `Bearer ${apiKey}`;
|
|
163
|
+
return merged;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* `fetch` with per-attempt timeout, bounded retry + backoff, and optional
|
|
167
|
+
* Bearer auth. See the module header for the full policy. Both `init.signal`
|
|
168
|
+
* (if the caller set one) AND the separate `externalSignal` param cancel the
|
|
169
|
+
* WHOLE policy — in-flight attempts AND queued backoff sleeps — not just a
|
|
170
|
+
* single attempt.
|
|
171
|
+
*/
|
|
172
|
+
async function fetchWithPolicy(url, init = {}, policy = {}, externalSignal) {
|
|
173
|
+
const timeoutMs = policy.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
174
|
+
const retries = policy.retries ?? DEFAULT_RETRIES;
|
|
175
|
+
const retryDelayMs = policy.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
176
|
+
const doFetch = policy.fetchImpl ?? fetch;
|
|
177
|
+
const headers = withBearerAuth(init.headers, policy.apiKey);
|
|
178
|
+
// init.signal is folded in here (not spread through per-attempt below) so
|
|
179
|
+
// it gates retries/backoff exactly like externalSignal, instead of being
|
|
180
|
+
// silently dropped by the `{ ...init, signal }` override per attempt.
|
|
181
|
+
const combinedExternalSignal = combineSignals([externalSignal, init.signal]);
|
|
182
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
183
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
184
|
+
const signal = combinedExternalSignal
|
|
185
|
+
? AbortSignal.any([timeoutSignal, combinedExternalSignal])
|
|
186
|
+
: timeoutSignal;
|
|
187
|
+
let status;
|
|
188
|
+
let bodySnippet;
|
|
189
|
+
let cause;
|
|
190
|
+
try {
|
|
191
|
+
const response = await doFetch(url, { ...init, headers, signal });
|
|
192
|
+
if (response.ok || !isRetryableStatus(response.status))
|
|
193
|
+
return response;
|
|
194
|
+
status = response.status;
|
|
195
|
+
if (attempt === retries) {
|
|
196
|
+
bodySnippet = await readBodySnippet(response);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// Doomed to be retried — discard rather than read, so this
|
|
200
|
+
// response's connection/socket isn't held open for a body nobody
|
|
201
|
+
// will consume. `.catch()` is mandatory here: an errored stream's
|
|
202
|
+
// `cancel()` returns a REJECTED promise, and a bare `void` on that
|
|
203
|
+
// is an unhandled rejection — process-fatal in Node — triggered by
|
|
204
|
+
// exactly the degraded-upstream condition this wrapper exists to
|
|
205
|
+
// survive.
|
|
206
|
+
void response.body?.cancel().catch(() => { });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
if (combinedExternalSignal?.aborted)
|
|
211
|
+
throw err;
|
|
212
|
+
cause = err;
|
|
213
|
+
}
|
|
214
|
+
if (attempt === retries) {
|
|
215
|
+
const statusDetail = status !== undefined
|
|
216
|
+
? `last status ${status}${bodySnippet ? ` — ${bodySnippet}` : ""}`
|
|
217
|
+
: undefined;
|
|
218
|
+
const detail = statusDetail ?? causeMessage(cause);
|
|
219
|
+
throw new FetchPolicyError(`fetchWithPolicy: ${describeTarget(url)} failed after ${attempt + 1} attempt(s) — ${detail}`, { status, bodySnippet, cause, attempts: attempt + 1 });
|
|
220
|
+
}
|
|
221
|
+
await sleep(backoffMs(retryDelayMs, attempt), combinedExternalSignal);
|
|
222
|
+
}
|
|
223
|
+
// Unreachable: the loop above always returns or throws on its final
|
|
224
|
+
// (attempt === retries) iteration — this satisfies the compiler only.
|
|
225
|
+
throw new FetchPolicyError(`fetchWithPolicy: ${describeTarget(url)} exhausted retries`, {
|
|
226
|
+
attempts: retries + 1,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -11,20 +11,41 @@
|
|
|
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
|
|
14
|
+
import type { OracleSource } from "../oracle/price-update-rule.ts";
|
|
15
|
+
import { type LoadConfigOptions, type PythGeneration, type PythInfraConfig, type WaterXConfig, type WormholeInfraConfig } from "./config.ts";
|
|
15
16
|
import type { Network } from "./constants.ts";
|
|
16
17
|
export interface CreateClientOptions extends LoadConfigOptions {
|
|
17
18
|
grpcUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Selects which `PriceUpdateRule` `refreshOraclePrices` uses for the
|
|
21
|
+
* on-chain price-update leg (see `OracleHost.oracleSource`). Default:
|
|
22
|
+
* `'pyth_rule'`. The SDK never reads `process.env` — pass this from your
|
|
23
|
+
* own env var (e.g. `ORACLE_SOURCE`).
|
|
24
|
+
*/
|
|
25
|
+
oracleSource?: OracleSource;
|
|
26
|
+
/**
|
|
27
|
+
* Selects which Pyth Core contract generation feeds `client.pyth` when the
|
|
28
|
+
* config JSON has no explicit `pyth` override: `'core'` (default,
|
|
29
|
+
* `PYTH_DEFAULTS`) or `'pro'` (`PYTH_PRO_DEFAULTS` — the post-2026-08-18
|
|
30
|
+
* Pro-compatible contracts + Hermes-compatible endpoint; pair with
|
|
31
|
+
* `pyth.api_key`). Orthogonal to `oracleSource`. An explicit `config.pyth`
|
|
32
|
+
* always wins wholesale (see `PythGeneration`).
|
|
33
|
+
*/
|
|
34
|
+
pythGeneration?: PythGeneration;
|
|
18
35
|
}
|
|
19
36
|
export declare class PerpClient extends BaseLineClient<WaterXConfig> {
|
|
20
37
|
/** Pyth infra (network defaults unless overridden in JSON). */
|
|
21
38
|
pyth: PythInfraConfig;
|
|
22
39
|
/** Wormhole infra for the credit bridge (network defaults unless overridden). */
|
|
23
40
|
wormhole: WormholeInfraConfig;
|
|
41
|
+
/** Selected oracle rule source (client option, resolved at creation; default `'pyth_rule'`). See `OracleHost.oracleSource`. */
|
|
42
|
+
readonly oracleSource: OracleSource;
|
|
24
43
|
/** Canonical-schema lookups (delegated to below); no transport. */
|
|
25
44
|
private readonly view;
|
|
26
45
|
constructor(network: Network, config: WaterXConfig, opts?: {
|
|
27
46
|
grpcUrl?: string;
|
|
47
|
+
oracleSource?: OracleSource;
|
|
48
|
+
pythGeneration?: PythGeneration;
|
|
28
49
|
});
|
|
29
50
|
/**
|
|
30
51
|
* Async factory: fetches the deployment config for `network` and returns
|
|
@@ -21,12 +21,17 @@ class PerpClient extends base_client_ts_1.BaseLineClient {
|
|
|
21
21
|
pyth;
|
|
22
22
|
/** Wormhole infra for the credit bridge (network defaults unless overridden). */
|
|
23
23
|
wormhole;
|
|
24
|
+
/** Selected oracle rule source (client option, resolved at creation; default `'pyth_rule'`). See `OracleHost.oracleSource`. */
|
|
25
|
+
oracleSource;
|
|
24
26
|
/** Canonical-schema lookups (delegated to below); no transport. */
|
|
25
27
|
view;
|
|
26
28
|
constructor(network, config, opts = {}) {
|
|
27
29
|
super(network, config, opts);
|
|
28
|
-
|
|
30
|
+
// Precedence: explicit config.pyth override > generation constants.
|
|
31
|
+
this.pyth =
|
|
32
|
+
config.pyth ?? (opts.pythGeneration === "pro" ? config_ts_1.PYTH_PRO_DEFAULTS : config_ts_1.PYTH_DEFAULTS)[network];
|
|
29
33
|
this.wormhole = config.wormhole ?? config_ts_1.WORMHOLE_DEFAULTS[network];
|
|
34
|
+
this.oracleSource = opts.oracleSource ?? "pyth_rule";
|
|
30
35
|
this.view = new config_view_ts_1.PerpConfigView(() => this.config, () => this.wormhole);
|
|
31
36
|
}
|
|
32
37
|
/**
|
|
@@ -35,7 +40,11 @@ class PerpClient extends base_client_ts_1.BaseLineClient {
|
|
|
35
40
|
*/
|
|
36
41
|
static async create(network, opts = {}) {
|
|
37
42
|
const config = await (0, config_ts_1.loadConfig)(network, opts);
|
|
38
|
-
return new PerpClient(network, config, {
|
|
43
|
+
return new PerpClient(network, config, {
|
|
44
|
+
grpcUrl: opts.grpcUrl,
|
|
45
|
+
oracleSource: opts.oracleSource,
|
|
46
|
+
pythGeneration: opts.pythGeneration,
|
|
47
|
+
});
|
|
39
48
|
}
|
|
40
49
|
static mainnet(opts = {}) {
|
|
41
50
|
return PerpClient.create("MAINNET", opts);
|