@waterx/sdk 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -34
- package/dist/cjs/src/account/config.d.ts +0 -16
- package/dist/cjs/src/oracle/aggregate.d.ts +19 -21
- package/dist/cjs/src/oracle/aggregate.js +57 -69
- package/dist/cjs/src/oracle/config.d.ts +32 -52
- package/dist/cjs/src/oracle/config.js +1 -35
- package/dist/cjs/src/oracle/host.d.ts +1 -1
- package/dist/cjs/src/oracle/index.d.ts +2 -2
- package/dist/cjs/src/oracle/index.js +20 -6
- 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 +10 -6
- package/dist/cjs/src/oracle/rule-registry.js +10 -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/update-fetch.d.ts +32 -2
- package/dist/cjs/src/oracle/update-fetch.js +60 -3
- package/dist/cjs/src/perp/client.d.ts +33 -19
- package/dist/cjs/src/perp/client.js +16 -10
- package/dist/cjs/src/perp/config.d.ts +4 -6
- package/dist/cjs/src/perp/config.js +8 -12
- package/dist/cjs/src/perp/index.d.ts +2 -2
- package/dist/cjs/src/perp/index.js +3 -4
- package/dist/cjs/src/unified-client.d.ts +19 -11
- package/dist/cjs/src/unified-client.js +2 -1
- package/dist/src/account/config.d.ts +0 -16
- package/dist/src/oracle/aggregate.d.ts +19 -21
- package/dist/src/oracle/aggregate.js +57 -69
- package/dist/src/oracle/config.d.ts +32 -52
- package/dist/src/oracle/config.js +0 -34
- package/dist/src/oracle/host.d.ts +1 -1
- package/dist/src/oracle/index.d.ts +2 -2
- package/dist/src/oracle/index.js +15 -7
- 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 +10 -6
- package/dist/src/oracle/rule-registry.js +10 -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/update-fetch.d.ts +32 -2
- package/dist/src/oracle/update-fetch.js +57 -3
- package/dist/src/perp/client.d.ts +33 -19
- package/dist/src/perp/client.js +17 -11
- package/dist/src/perp/config.d.ts +4 -6
- package/dist/src/perp/config.js +9 -12
- package/dist/src/perp/index.d.ts +2 -2
- package/dist/src/perp/index.js +1 -1
- package/dist/src/unified-client.d.ts +19 -11
- package/dist/src/unified-client.js +2 -1
- package/package.json +1 -1
|
@@ -83,28 +83,26 @@ export declare function aggregateTickerWithConstant(tx: Transaction, host: Oracl
|
|
|
83
83
|
* enabled, Constant when it's a constant ticker).
|
|
84
84
|
*
|
|
85
85
|
* Before that, the on-chain price *update* leg is routed by `host.oracleSource`
|
|
86
|
-
* (see `rule-registry.ts`): the selected
|
|
87
|
-
* `supportedTickers(host)
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* it
|
|
86
|
+
* (see `rule-registry.ts`): the ONE selected source serves every ticker in its
|
|
87
|
+
* `supportedTickers(host)`. There is **no cross-source fallback** — a requested
|
|
88
|
+
* ticker the selected source does not serve, and that is not a constant-only
|
|
89
|
+
* ticker (which needs no price-update leg), fails the build immediately with a
|
|
90
|
+
* clear error naming the ticker and source. That is the deliberate "fail the
|
|
91
|
+
* tx-build, don't silently reroute" contract: a wrong-but-present feed id is
|
|
92
|
+
* NOT validated here (it surfaces on-chain at dry-run); a MISSING feed for the
|
|
93
|
+
* selected source is caught here. When the selected source's feed exists but is
|
|
94
|
+
* wrong, this function does nothing special — the on-chain aggregate aborts at
|
|
95
|
+
* dry-run, which is correct.
|
|
93
96
|
*
|
|
94
|
-
*
|
|
95
|
-
* per-rule PTB atomicity
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* fee-free Lazer group ordered ahead of a Pyth Core fallback group). Only once
|
|
104
|
-
* that check passes do all groups' off-chain fetches run concurrently
|
|
105
|
-
* (`Promise.all`) and complete before any PTB mutation; on-chain reads inside
|
|
106
|
-
* `buildUpdateCalls` can still fail mid-append for other reasons — callers
|
|
107
|
-
* discard the tx on any throw.
|
|
97
|
+
* The selected source's fetch + build runs against its own infra, guaranteeing
|
|
98
|
+
* per-rule PTB atomicity. A fee-source pre-check runs first (the source's
|
|
99
|
+
* `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation — so a
|
|
100
|
+
* fee-charging source with no `opts.feeSource` throws
|
|
101
|
+
* `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
|
|
102
|
+
* moveCalls. Only once that check passes does the off-chain fetch run and
|
|
103
|
+
* complete before any PTB mutation; on-chain reads inside `buildUpdateCalls`
|
|
104
|
+
* can still fail mid-append for other reasons — callers discard the tx on any
|
|
105
|
+
* throw.
|
|
108
106
|
*
|
|
109
107
|
* **Collector-feed leg is rule-aware:** a lazer-served group's
|
|
110
108
|
* `buildUpdateCalls` returns the verified `Update` PTB value
|
|
@@ -159,28 +159,26 @@ export function aggregateTickerWithConstant(tx, host, args) {
|
|
|
159
159
|
* enabled, Constant when it's a constant ticker).
|
|
160
160
|
*
|
|
161
161
|
* Before that, the on-chain price *update* leg is routed by `host.oracleSource`
|
|
162
|
-
* (see `rule-registry.ts`): the selected
|
|
163
|
-
* `supportedTickers(host)
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* it
|
|
162
|
+
* (see `rule-registry.ts`): the ONE selected source serves every ticker in its
|
|
163
|
+
* `supportedTickers(host)`. There is **no cross-source fallback** — a requested
|
|
164
|
+
* ticker the selected source does not serve, and that is not a constant-only
|
|
165
|
+
* ticker (which needs no price-update leg), fails the build immediately with a
|
|
166
|
+
* clear error naming the ticker and source. That is the deliberate "fail the
|
|
167
|
+
* tx-build, don't silently reroute" contract: a wrong-but-present feed id is
|
|
168
|
+
* NOT validated here (it surfaces on-chain at dry-run); a MISSING feed for the
|
|
169
|
+
* selected source is caught here. When the selected source's feed exists but is
|
|
170
|
+
* wrong, this function does nothing special — the on-chain aggregate aborts at
|
|
171
|
+
* dry-run, which is correct.
|
|
169
172
|
*
|
|
170
|
-
*
|
|
171
|
-
* per-rule PTB atomicity
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* fee-free Lazer group ordered ahead of a Pyth Core fallback group). Only once
|
|
180
|
-
* that check passes do all groups' off-chain fetches run concurrently
|
|
181
|
-
* (`Promise.all`) and complete before any PTB mutation; on-chain reads inside
|
|
182
|
-
* `buildUpdateCalls` can still fail mid-append for other reasons — callers
|
|
183
|
-
* discard the tx on any throw.
|
|
173
|
+
* The selected source's fetch + build runs against its own infra, guaranteeing
|
|
174
|
+
* per-rule PTB atomicity. A fee-source pre-check runs first (the source's
|
|
175
|
+
* `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation — so a
|
|
176
|
+
* fee-charging source with no `opts.feeSource` throws
|
|
177
|
+
* `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
|
|
178
|
+
* moveCalls. Only once that check passes does the off-chain fetch run and
|
|
179
|
+
* complete before any PTB mutation; on-chain reads inside `buildUpdateCalls`
|
|
180
|
+
* can still fail mid-append for other reasons — callers discard the tx on any
|
|
181
|
+
* throw.
|
|
184
182
|
*
|
|
185
183
|
* **Collector-feed leg is rule-aware:** a lazer-served group's
|
|
186
184
|
* `buildUpdateCalls` returns the verified `Update` PTB value
|
|
@@ -207,59 +205,49 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
|
|
|
207
205
|
const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
|
|
208
206
|
const priceInfoByTicker = new Map();
|
|
209
207
|
pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
// below has an OracleSource to key on without a cast.
|
|
208
|
+
// ONE source, no fallback. The selected source serves the tickers in its
|
|
209
|
+
// `supportedTickers(host)`; `source` is tracked alongside the group (rather
|
|
210
|
+
// than read back off `rule.kind`, typed as the broader PriceUpdateRuleKind)
|
|
211
|
+
// so the provider lookup below has an OracleSource to key on without a cast.
|
|
215
212
|
const selectedRule = resolveOracleRule(host.oracleSource, opts.ruleOverrides);
|
|
216
213
|
const selectedSupported = new Set(selectedRule.supportedTickers(host));
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
214
|
+
// Fail the tx-build (NOT client init, NOT a silent reroute) when the selected
|
|
215
|
+
// source has no feed for a requested ticker that actually needs a price
|
|
216
|
+
// update. Only a CONSTANT-ONLY ticker is exempt — priced entirely by
|
|
217
|
+
// `constant_rule`, it needs no update leg from any source. A DUAL-FEED ticker
|
|
218
|
+
// (constant AND pyth) still needs its Pyth leg refreshed, so `isConstantTicker`
|
|
219
|
+
// alone must NOT exempt it: under a source that can't serve it, with no
|
|
220
|
+
// fallback, feeding an unrefreshed Pyth leg would price it stale (or abort on a
|
|
221
|
+
// missing weighted source). `priceInfoByTicker.has(t)` ⇔ the ticker has a
|
|
222
|
+
// `pyth_rule.feeds` entry, so `constant && !hasPyth` is exactly constant-only.
|
|
223
|
+
// This catches a MISSING feed; a present-but-WRONG feed id is deliberately not
|
|
224
|
+
// validated here (it aborts on-chain at dry-run).
|
|
225
|
+
const isConstantOnly = (t) => host.isConstantTicker(t) && !priceInfoByTicker.has(t);
|
|
226
|
+
const unservable = tickers.filter((t) => !selectedSupported.has(t) && !isConstantOnly(t));
|
|
227
|
+
if (unservable.length > 0) {
|
|
228
|
+
throw new Error(`oracleSource '${host.oracleSource}' has no feed configured for ticker(s): ` +
|
|
229
|
+
`${unservable.join(", ")}. Sources are self-contained with no fallback — add ` +
|
|
230
|
+
`${host.oracleSource} feeds for them, or select a source that serves them.`);
|
|
229
231
|
}
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
// `
|
|
233
|
-
//
|
|
234
|
-
// network calls and zero PTB commands
|
|
235
|
-
// `buildPythPriceUpdateCalls`
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
// group's `buildUpdateCalls` in sequence — a fee-free group ordered ahead
|
|
239
|
-
// of a fee-charging one would already have appended its verify/feed
|
|
240
|
-
// moveCalls to the shared `tx` by the time the fee-charging group's own
|
|
241
|
-
// guard fired, breaking the "throw before any PTB mutation" guarantee.
|
|
242
|
-
// Checking every group's `requiresFeeSource` up front — before ANY group
|
|
243
|
-
// fetches or builds — closes that gap, and (unlike a referential check
|
|
244
|
-
// against a specific rule instance) keeps protecting a future
|
|
245
|
-
// fee-charging rule or a test double standing in for one.
|
|
246
|
-
if (!opts.feeSource && groups.some((group) => group.rule.requiresFeeSource)) {
|
|
232
|
+
const selectedGroup = tickers.filter((t) => selectedSupported.has(t));
|
|
233
|
+
// Fee-source pre-check, hoisted ABOVE the off-chain fetch and PTB build below.
|
|
234
|
+
// It consults only `rule.requiresFeeSource` — known before any fetch or PTB
|
|
235
|
+
// mutation — so a fee-charging source with no `feeSource` throws with ZERO
|
|
236
|
+
// wasted network calls and zero PTB commands, rather than waiting for
|
|
237
|
+
// `buildPythPriceUpdateCalls`'s own per-call guard to fire after the off-chain
|
|
238
|
+
// fetch already ran.
|
|
239
|
+
if (selectedGroup.length > 0 && !opts.feeSource && selectedRule.requiresFeeSource) {
|
|
247
240
|
throw new OracleFeeSourceUnavailableError();
|
|
248
241
|
}
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
const groupsWithData = await Promise.all(groups.map(async (group) => ({
|
|
254
|
-
rule: group.rule,
|
|
255
|
-
tickers: group.tickers,
|
|
256
|
-
data: await resolveGroupUpdateData(host, group, opts.updateDataProvider),
|
|
257
|
-
})));
|
|
258
|
-
// Verified-`Update` handle per lazer-served ticker (one shared PTB value per
|
|
259
|
-
// group) — consumed by the collector-feed leg below.
|
|
242
|
+
// Resolve + build the selected source's update leg. The off-chain fetch
|
|
243
|
+
// settles before the first PTB mutation, so a fetch failure never strands
|
|
244
|
+
// moveCalls in a caller-owned tx. Map each lazer-served ticker to the one
|
|
245
|
+
// verified `Update` PTB value for the collector-feed leg below.
|
|
260
246
|
const lazerUpdateByTicker = new Map();
|
|
261
|
-
|
|
262
|
-
const
|
|
247
|
+
if (selectedGroup.length > 0) {
|
|
248
|
+
const group = { source: host.oracleSource, rule: selectedRule, tickers: selectedGroup };
|
|
249
|
+
const data = await resolveGroupUpdateData(host, group, opts.updateDataProvider);
|
|
250
|
+
const handle = (await selectedRule.buildUpdateCalls(tx, host, data, {
|
|
263
251
|
cache: opts.cache,
|
|
264
252
|
feeSource: opts.feeSource,
|
|
265
253
|
})) ?? undefined;
|
|
@@ -267,7 +255,7 @@ export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
|
|
|
267
255
|
// protect: a future non-lazer handle (e.g. a WaterxRule value) must never
|
|
268
256
|
// be silently fed into pyth_lazer_rule::feed.
|
|
269
257
|
if (handle?.kind === "pyth_lazer_rule") {
|
|
270
|
-
for (const ticker of
|
|
258
|
+
for (const ticker of selectedGroup)
|
|
271
259
|
lazerUpdateByTicker.set(ticker, handle.update);
|
|
272
260
|
}
|
|
273
261
|
}
|
|
@@ -101,6 +101,29 @@ export interface OraclePackages {
|
|
|
101
101
|
supra_rule?: SupraRulePackage;
|
|
102
102
|
waterx_oracle: WaterxOraclePackage;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolved Pyth Core infra as it lives on `client.pyth` — NOT a config-JSON
|
|
106
|
+
* shape. `state_id` / `wormhole_state_id` / `hermes_endpoint` come verbatim
|
|
107
|
+
* from the fixed per-network constant ({@link PYTH_DEFAULTS}); `api_key` /
|
|
108
|
+
* `fetch` are layered on from the caller's `pythApiKey` / `pythFetch` create
|
|
109
|
+
* options. None of it is sourced from the canonical `waterx-config` JSON — the
|
|
110
|
+
* SDK never reads a `pyth` block there (a Bearer secret has no place in a
|
|
111
|
+
* public CDN document). The infra is the same for every `oracleSource`; the
|
|
112
|
+
* `pyth_lazer_rule` source reads only the `api_key` / `fetch` from here and
|
|
113
|
+
* gets its on-chain infra from {@link LAZER_DEFAULTS} + config instead.
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* The caller-tunable subset of `fetchWithPolicy`'s policy exposed on the
|
|
117
|
+
* `pythFetch` create option and `client.pyth.fetch` — the retry/timeout budget
|
|
118
|
+
* for the off-chain Hermes (`fetchPriceFeedsUpdateData`) and Lazer
|
|
119
|
+
* (`PythLazerRule`) update fetches. Deliberately narrower than the internal
|
|
120
|
+
* `FetchPolicy` (no `retryDelayMs` / `apiKey` / `fetchImpl`). Both fetches fall
|
|
121
|
+
* back to `fetchWithPolicy`'s defaults (15s timeout, 2 retries) when unset.
|
|
122
|
+
*/
|
|
123
|
+
export type PythFetchPolicy = {
|
|
124
|
+
timeoutMs?: number;
|
|
125
|
+
retries?: number;
|
|
126
|
+
};
|
|
104
127
|
export interface PythInfraConfig {
|
|
105
128
|
state_id: string;
|
|
106
129
|
wormhole_state_id: string;
|
|
@@ -109,8 +132,8 @@ export interface PythInfraConfig {
|
|
|
109
132
|
* Pyth Pro / Lazer access token (`Authorization: Bearer …`) for
|
|
110
133
|
* `PythLazerRule`'s signed-update fetch — Lazer is auth-first, so there is
|
|
111
134
|
* no keyless default. Optional: Pyth-Core-only deployments never need it.
|
|
112
|
-
*
|
|
113
|
-
*
|
|
135
|
+
* Supplied via the `pythApiKey` create option (the SDK never reads
|
|
136
|
+
* `process.env` or the config JSON). Absent when a lazer-routed fetch runs →
|
|
114
137
|
* `LazerApiKeyMissing` is thrown at fetch time. As of the Pyth Pro
|
|
115
138
|
* migration (post-2026-08-18, per
|
|
116
139
|
* https://docs.pyth.network/price-feeds/core/upgrade) this is ALSO required
|
|
@@ -119,57 +142,16 @@ export interface PythInfraConfig {
|
|
|
119
142
|
*/
|
|
120
143
|
api_key?: string;
|
|
121
144
|
/**
|
|
122
|
-
* Retry/timeout policy
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* to `fetchWithPolicy`'s built-in defaults (15s
|
|
127
|
-
* unset.
|
|
145
|
+
* Retry/timeout policy for the Hermes (`fetchPriceFeedsUpdateData`) and
|
|
146
|
+
* Lazer (`PythLazerRule`) off-chain update fetches — see `fetchWithPolicy`
|
|
147
|
+
* (`./update-fetch.ts`) for the full policy (backoff, which statuses retry,
|
|
148
|
+
* Bearer attachment). Supplied via the `pythFetch` create option. Optional:
|
|
149
|
+
* both fetches default to `fetchWithPolicy`'s built-in defaults (15s
|
|
150
|
+
* timeout, 2 retries) when unset.
|
|
128
151
|
*/
|
|
129
|
-
fetch?:
|
|
130
|
-
timeoutMs?: number;
|
|
131
|
-
retries?: number;
|
|
132
|
-
};
|
|
152
|
+
fetch?: PythFetchPolicy;
|
|
133
153
|
}
|
|
134
154
|
export declare const PYTH_DEFAULTS: Record<Network, PythInfraConfig>;
|
|
135
|
-
/**
|
|
136
|
-
* Which Pyth Core contract generation feeds `host.pyth` when the config JSON
|
|
137
|
-
* carries no explicit `pyth` override:
|
|
138
|
-
*
|
|
139
|
-
* - `'core'` (default) — the original contracts + keyless Hermes
|
|
140
|
-
* ({@link PYTH_DEFAULTS}).
|
|
141
|
-
* - `'pro'` — the Pro-compatible upgraded contracts + the Hermes-compatible
|
|
142
|
-
* endpoint ({@link PYTH_PRO_DEFAULTS}); pair with `pyth.api_key` after the
|
|
143
|
-
* 2026-08-18 cutover.
|
|
144
|
-
*
|
|
145
|
-
* Resolved once at client creation from the `pythGeneration` create option.
|
|
146
|
-
* Orthogonal to `oracleSource` — this flips the Pyth-Core *infra* (state ids
|
|
147
|
-
* + endpoint), not which `PriceUpdateRule` routes tickers.
|
|
148
|
-
*/
|
|
149
|
-
export type PythGeneration = "core" | "pro";
|
|
150
|
-
/**
|
|
151
|
-
* Pyth **Pro-generation** Core-compatible infra — the post-2026-08-18
|
|
152
|
-
* contracts (cutover date per
|
|
153
|
-
* https://docs.pyth.network/price-feeds/core/upgrade) from Pyth's Core-Upgrade
|
|
154
|
-
* docs
|
|
155
|
-
* (https://docs.pyth.network/price-feeds/core/upgrade/contracts, Sui section;
|
|
156
|
-
* package revs `sui-pro-compatible-contract-mainnet` /
|
|
157
|
-
* `sui-pro-compatible-contract-testnet`). Selected via the client's
|
|
158
|
-
* `pythGeneration: 'pro'` create option; `config.pyth` still overrides
|
|
159
|
-
* wholesale (see `PerpClient`). All four state ids were verified on-chain
|
|
160
|
-
* (shared `state::State` objects under the docs' upgraded package ids).
|
|
161
|
-
*
|
|
162
|
-
* Kept as a second flat map beside {@link PYTH_DEFAULTS} rather than a nested
|
|
163
|
-
* `PYTH_INFRA[network][generation]` — `PYTH_DEFAULTS` is a published export
|
|
164
|
-
* with external consumers, so the smallest honest surface is an additive
|
|
165
|
-
* sibling (same deferral note as {@link LAZER_DEFAULTS}).
|
|
166
|
-
*
|
|
167
|
-
* The Hermes-compatible endpoint (`pyth.dourolabs.app/hermes`) serves the
|
|
168
|
-
* same REST surface as `hermes.pyth.network` but requires `pyth.api_key`
|
|
169
|
-
* (`Authorization: Bearer …`) after the cutover — see
|
|
170
|
-
* {@link PythInfraConfig.api_key}.
|
|
171
|
-
*/
|
|
172
|
-
export declare const PYTH_PRO_DEFAULTS: Record<Network, PythInfraConfig>;
|
|
173
155
|
/**
|
|
174
156
|
* Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
|
|
175
157
|
* Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
|
|
@@ -197,6 +179,4 @@ export declare const LAZER_DEFAULTS: Record<Network, {
|
|
|
197
179
|
*/
|
|
198
180
|
export interface OracleConfig extends BaseLineConfig {
|
|
199
181
|
packages: OraclePackages;
|
|
200
|
-
/** Pyth infra override (defaults from {@link PYTH_DEFAULTS}). */
|
|
201
|
-
pyth?: PythInfraConfig;
|
|
202
182
|
}
|
|
@@ -21,40 +21,6 @@ export const PYTH_DEFAULTS = {
|
|
|
21
21
|
hermes_endpoint: "https://hermes-beta.pyth.network",
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
|
-
/**
|
|
25
|
-
* Pyth **Pro-generation** Core-compatible infra — the post-2026-08-18
|
|
26
|
-
* contracts (cutover date per
|
|
27
|
-
* https://docs.pyth.network/price-feeds/core/upgrade) from Pyth's Core-Upgrade
|
|
28
|
-
* docs
|
|
29
|
-
* (https://docs.pyth.network/price-feeds/core/upgrade/contracts, Sui section;
|
|
30
|
-
* package revs `sui-pro-compatible-contract-mainnet` /
|
|
31
|
-
* `sui-pro-compatible-contract-testnet`). Selected via the client's
|
|
32
|
-
* `pythGeneration: 'pro'` create option; `config.pyth` still overrides
|
|
33
|
-
* wholesale (see `PerpClient`). All four state ids were verified on-chain
|
|
34
|
-
* (shared `state::State` objects under the docs' upgraded package ids).
|
|
35
|
-
*
|
|
36
|
-
* Kept as a second flat map beside {@link PYTH_DEFAULTS} rather than a nested
|
|
37
|
-
* `PYTH_INFRA[network][generation]` — `PYTH_DEFAULTS` is a published export
|
|
38
|
-
* with external consumers, so the smallest honest surface is an additive
|
|
39
|
-
* sibling (same deferral note as {@link LAZER_DEFAULTS}).
|
|
40
|
-
*
|
|
41
|
-
* The Hermes-compatible endpoint (`pyth.dourolabs.app/hermes`) serves the
|
|
42
|
-
* same REST surface as `hermes.pyth.network` but requires `pyth.api_key`
|
|
43
|
-
* (`Authorization: Bearer …`) after the cutover — see
|
|
44
|
-
* {@link PythInfraConfig.api_key}.
|
|
45
|
-
*/
|
|
46
|
-
export const PYTH_PRO_DEFAULTS = {
|
|
47
|
-
MAINNET: {
|
|
48
|
-
state_id: "0x03719fae774ddab3cfcaa53bbc046f0cbe21410019b6280811bf3f9f4b05839d",
|
|
49
|
-
wormhole_state_id: "0xdbca52b9fb4f712e25f61f974586d93ac541bcf8389564f0323bb07215168b5c",
|
|
50
|
-
hermes_endpoint: "https://pyth.dourolabs.app/hermes",
|
|
51
|
-
},
|
|
52
|
-
TESTNET: {
|
|
53
|
-
state_id: "0x3c48fe392912de6c18087a2b3f5fdbfbfdb4598e180947feff1f12f8e9ea073e",
|
|
54
|
-
wormhole_state_id: "0x750da8e6d16b6a363a39fe2eaa8295ac224a1e6fce4e47b58845e2e8746164f0",
|
|
55
|
-
hermes_endpoint: "https://pyth.dourolabs.app/hermes",
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
24
|
// ============================================================================
|
|
59
25
|
// Pyth Lazer — external infra, defaults by network
|
|
60
26
|
// ============================================================================
|
|
@@ -16,7 +16,7 @@ export interface OracleHost {
|
|
|
16
16
|
readonly network: Network;
|
|
17
17
|
/** Oracle slice of the canonical `waterx-config` JSON (rule packages + per-ticker feeds). */
|
|
18
18
|
readonly config: OracleConfig;
|
|
19
|
-
/** External Pyth/Wormhole/Hermes infra (network
|
|
19
|
+
/** External Pyth/Wormhole/Hermes infra — fixed per `(network, generation)`; api_key/fetch layered from create options. */
|
|
20
20
|
readonly pyth: PythInfraConfig;
|
|
21
21
|
/** gRPC client for the on-chain reads the Pyth update path needs. */
|
|
22
22
|
readonly grpcClient: SuiGrpcClient;
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
* rules that consume it are separate concerns.
|
|
17
17
|
*/
|
|
18
18
|
export type { OracleHost } from "./host.ts";
|
|
19
|
-
export { FetchPolicyError } from "./update-fetch.ts";
|
|
19
|
+
export { FetchPolicyError, fetchWithPolicy, joinEndpointPath } from "./update-fetch.ts";
|
|
20
20
|
export type { FetchPolicy } from "./update-fetch.ts";
|
|
21
|
-
export { PythCache, fetchPriceFeedsUpdateData, buildPythPriceUpdateCalls, updatePythPrices, OracleFeeSourceUnavailableError, } from "./pyth.ts";
|
|
21
|
+
export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls, updatePythPrices, HermesEndpointRejectedAllFeedsError, MISSING_FEED_MEMO_TTL_MS, OracleFeeSourceUnavailableError, } from "./pyth.ts";
|
|
22
22
|
export type { OracleFeeSource } from "./pyth.ts";
|
|
23
23
|
export type { PriceUpdateRule, PriceUpdateRuleKind, RuleUpdateData, RuleUpdateHandle, BuildUpdateOpts, OracleSource, UpdateDataProvider, } from "./price-update-rule.ts";
|
|
24
24
|
export { PythCoreRule } from "./rules/pyth-core-rule.ts";
|
package/dist/src/oracle/index.js
CHANGED
|
@@ -19,13 +19,21 @@
|
|
|
19
19
|
// just the type) so a consumer (e.g. a BE prefetch cache) can `instanceof`
|
|
20
20
|
// it off the failure `fetchPriceFeedsUpdateData` / `PythLazerRule` /
|
|
21
21
|
// `loadConfig` surface, without a deep import of `./update-fetch.ts`.
|
|
22
|
-
// `fetchWithPolicy`
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
export {
|
|
22
|
+
// `fetchWithPolicy` + `joinEndpointPath` are exported for consumers that hit
|
|
23
|
+
// Hermes-compatible endpoints THEMSELVES (e.g. the BE's parsed latest-price
|
|
24
|
+
// bootstrap and Pyth schedule readers): one shared Bearer/timeout/retry
|
|
25
|
+
// policy and one base-path-safe URL join, instead of each caller re-rolling
|
|
26
|
+
// them (the hand-rolled copies were how the Pro `/hermes` base path got
|
|
27
|
+
// dropped and the Bearer went missing on sibling fetches).
|
|
28
|
+
export { FetchPolicyError, fetchWithPolicy, joinEndpointPath } from "./update-fetch.js";
|
|
29
|
+
// Pyth source — `OracleFeeSourceUnavailableError` and
|
|
30
|
+
// `HermesEndpointRejectedAllFeedsError` are re-exported (not just the types)
|
|
31
|
+
// for the same `instanceof` reason as `FetchPolicyError` above: a consumer of
|
|
32
|
+
// `buildPythPriceUpdateCalls` / `updatePythPrices` / `refreshOraclePrices` can
|
|
33
|
+
// branch on the fee-source failure directly, and a consumer of
|
|
34
|
+
// `fetchPriceFeedsUpdateData` / `probeMissingFeeds` can tell a misconfigured
|
|
35
|
+
// or unentitled endpoint apart from feeds that endpoint genuinely lacks.
|
|
36
|
+
export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls, updatePythPrices, HermesEndpointRejectedAllFeedsError, MISSING_FEED_MEMO_TTL_MS, OracleFeeSourceUnavailableError, } from "./pyth.js";
|
|
29
37
|
// Pyth Core rule (PriceUpdateRule wrapper over the Pyth source above)
|
|
30
38
|
export { PythCoreRule } from "./rules/pyth-core-rule.js";
|
|
31
39
|
// Pyth Lazer rule (signed-update generation; `feedLazerRule` stays internal to `aggregate.ts`)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* 4. hot_potato_vector::destroy
|
|
15
15
|
*/
|
|
16
16
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
17
|
+
import type { PythFetchPolicy } from "./config.ts";
|
|
17
18
|
import type { OracleHost } from "./host.ts";
|
|
18
19
|
type PriceTableInfo = {
|
|
19
20
|
id: string;
|
|
@@ -29,13 +30,74 @@ export declare class PythCache {
|
|
|
29
30
|
priceTableInfo?: PriceTableInfo;
|
|
30
31
|
priceFeedObjectIdCache: Map<string, string | undefined>;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
type FetchOpts = {
|
|
33
34
|
apiKey?: string;
|
|
34
|
-
fetch?:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
fetch?: PythFetchPolicy;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* How long a "this endpoint lacks feed X" verdict stays memoized. The verdict
|
|
39
|
+
* is a claim about *someone else's* deployment — a feed can be added to the
|
|
40
|
+
* catalog, an entitlement can be granted, a Pro plan can be upgraded — so it
|
|
41
|
+
* must expire rather than bind the whole process lifetime. Long enough that a
|
|
42
|
+
* genuinely-absent feed costs one discovery per window instead of one per
|
|
43
|
+
* build; short enough that a recovered endpoint self-heals without a restart.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MISSING_FEED_MEMO_TTL_MS: number;
|
|
46
|
+
/**
|
|
47
|
+
* Thrown when discovery concludes that an endpoint rejects EVERY requested
|
|
48
|
+
* feed id without a catalog vouching for that verdict. `instanceof`-able
|
|
49
|
+
* (mirrors `FetchPolicyError` / {@link OracleFeeSourceUnavailableError}).
|
|
50
|
+
*
|
|
51
|
+
* "All of them are missing" is the signature of an endpoint/credential fault —
|
|
52
|
+
* a wrong base path (the Pyth Pro `/hermes` prefix dropped), a changed route,
|
|
53
|
+
* a revoked or downgraded entitlement — not of N individually-absent feeds.
|
|
54
|
+
* Memoizing it would convert a loud, fixable misconfiguration into a silent
|
|
55
|
+
* permanent one: every id marked missing ⇒ `fetchPriceFeedsUpdateData` returns
|
|
56
|
+
* `[]` ⇒ `buildPythPriceUpdateCalls` throws "Hermes returned empty results",
|
|
57
|
+
* blaming Hermes for having no data, for the rest of the process's life. So
|
|
58
|
+
* this case writes NOTHING to the memo and throws instead; the next call
|
|
59
|
+
* re-probes and recovers on its own once the endpoint does.
|
|
60
|
+
*
|
|
61
|
+
* The message keeps the `Hermes price fetch failed: <status>` prefix on its
|
|
62
|
+
* first line — the documented contract downstream consumers string-match (see
|
|
63
|
+
* {@link fetchPriceFeedsUpdateData} and the e2e transient detector).
|
|
64
|
+
*/
|
|
65
|
+
export declare class HermesEndpointRejectedAllFeedsError extends Error {
|
|
66
|
+
readonly endpoint: string;
|
|
67
|
+
readonly requestedCount: number;
|
|
68
|
+
constructor(endpoint: string, requestedCount: number, catalogState: "unreadable" | "empty");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The subset of `feedIds` this `endpoint` is known to serve — i.e. minus any
|
|
72
|
+
* discovered to be absent within the last {@link MISSING_FEED_MEMO_TTL_MS}
|
|
73
|
+
* (see {@link fetchPriceFeedsUpdateData}). Callers building a
|
|
74
|
+
* `{ updates, feedIds }` payload use this to keep `feedIds` aligned with the
|
|
75
|
+
* feeds the fetch actually returned data for, so `buildPythPriceUpdateCalls`
|
|
76
|
+
* (one moveCall per feed id) never references a feed the accumulator blob
|
|
77
|
+
* doesn't cover.
|
|
78
|
+
*
|
|
79
|
+
* Expired entries are pruned here rather than on a timer: the memo is only
|
|
80
|
+
* ever consulted through this function, so a lazy sweep is both sufficient and
|
|
81
|
+
* free of a dangling interval in a library.
|
|
82
|
+
*/
|
|
83
|
+
export declare function endpointSupportedFeedIds(endpoint: string, feedIds: string[], apiKey?: string): string[];
|
|
84
|
+
/** Test-only: forget everything learned about which feeds an endpoint lacks. */
|
|
85
|
+
export declare function __resetMissingFeedCacheForTest(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Discovery-only entry for consumers that fetch Hermes THEMSELVES (e.g. a
|
|
88
|
+
* parsed latest-price reader) and just observed a whole-batch 404: resolves
|
|
89
|
+
* which ids the endpoint lacks, memoizes them (see {@link
|
|
90
|
+
* endpointSupportedFeedIds}), fetches NO survivor data. Without this, such a
|
|
91
|
+
* consumer's only way to populate the memo was calling {@link
|
|
92
|
+
* fetchPriceFeedsUpdateData} and discarding its accumulator blob — two full
|
|
93
|
+
* redundant transfers per cold discovery.
|
|
94
|
+
*
|
|
95
|
+
* @throws HermesEndpointRejectedAllFeedsError when the rejection looks
|
|
96
|
+
* endpoint-wide rather than per-feed — the caller's own 404 is then a
|
|
97
|
+
* misconfiguration to surface, not a set of feeds to quietly drop.
|
|
98
|
+
*/
|
|
99
|
+
export declare function probeMissingFeeds(endpoint: string, ids: string[], opts?: FetchOpts): Promise<void>;
|
|
100
|
+
export declare function fetchPriceFeedsUpdateData(endpoint: string, priceIds: string[], opts?: FetchOpts): Promise<Uint8Array[]>;
|
|
39
101
|
/**
|
|
40
102
|
* Resolved source for the Pyth Core on-chain update fee. Deliberately a
|
|
41
103
|
* closed two-variant union, not a `{ sponsorFund?, allowGasFee? }` pair — a
|