@waterx/sdk 4.1.0 → 4.3.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 +22 -17
- package/dist/cjs/src/account/config.d.ts +3 -2
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +8 -154
- package/dist/cjs/src/oracle/aggregate.d.ts +21 -21
- package/dist/cjs/src/oracle/aggregate.js +118 -69
- package/dist/cjs/src/oracle/config.d.ts +38 -72
- package/dist/cjs/src/oracle/config.js +0 -56
- package/dist/cjs/src/oracle/host.d.ts +24 -20
- package/dist/cjs/src/oracle/index.d.ts +5 -3
- package/dist/cjs/src/oracle/index.js +31 -8
- package/dist/cjs/src/oracle/price-update-rule.d.ts +13 -6
- package/dist/cjs/src/oracle/price-update-rule.js +3 -3
- package/dist/cjs/src/oracle/pyth.d.ts +44 -0
- package/dist/cjs/src/oracle/pyth.js +48 -6
- package/dist/cjs/src/oracle/read-plane.d.ts +70 -0
- package/dist/cjs/src/oracle/read-plane.js +78 -0
- package/dist/cjs/src/oracle/rule-registry.d.ts +6 -4
- package/dist/cjs/src/oracle/rule-registry.js +6 -4
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +4 -4
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +29 -4
- package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -4
- package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +42 -5
- package/dist/cjs/src/oracle/rules/waterx-rule.js +57 -17
- package/dist/cjs/src/perp/client.d.ts +40 -27
- package/dist/cjs/src/perp/client.js +28 -20
- package/dist/cjs/src/perp/config.d.ts +6 -6
- package/dist/cjs/src/perp/config.js +12 -12
- package/dist/cjs/src/perp/index.d.ts +3 -3
- package/dist/cjs/src/perp/index.js +7 -4
- package/dist/cjs/src/unified-client.d.ts +14 -9
- package/dist/cjs/src/unified-client.js +2 -2
- package/dist/src/account/config.d.ts +3 -2
- package/dist/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
- package/dist/src/generated/waterx_rule/waterx_rule.js +7 -143
- package/dist/src/oracle/aggregate.d.ts +21 -21
- package/dist/src/oracle/aggregate.js +118 -69
- package/dist/src/oracle/config.d.ts +38 -72
- package/dist/src/oracle/config.js +1 -55
- package/dist/src/oracle/host.d.ts +24 -20
- package/dist/src/oracle/index.d.ts +5 -3
- package/dist/src/oracle/index.js +25 -10
- package/dist/src/oracle/price-update-rule.d.ts +13 -6
- package/dist/src/oracle/price-update-rule.js +3 -3
- package/dist/src/oracle/pyth.d.ts +44 -0
- package/dist/src/oracle/pyth.js +45 -5
- package/dist/src/oracle/read-plane.d.ts +70 -0
- package/dist/src/oracle/read-plane.js +74 -0
- package/dist/src/oracle/rule-registry.d.ts +6 -4
- package/dist/src/oracle/rule-registry.js +6 -4
- package/dist/src/oracle/rules/pyth-core-rule.js +5 -5
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +28 -3
- package/dist/src/oracle/rules/pyth-rule.js +5 -4
- package/dist/src/oracle/rules/waterx-rule.d.ts +42 -5
- package/dist/src/oracle/rules/waterx-rule.js +53 -14
- package/dist/src/perp/client.d.ts +40 -27
- package/dist/src/perp/client.js +29 -21
- package/dist/src/perp/config.d.ts +6 -6
- package/dist/src/perp/config.js +11 -9
- package/dist/src/perp/index.d.ts +3 -3
- package/dist/src/perp/index.js +2 -2
- package/dist/src/unified-client.d.ts +14 -9
- package/dist/src/unified-client.js +2 -2
- package/package.json +1 -1
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
*
|
|
20
20
|
* `refreshOraclePrices` additionally routes the on-chain price *update* leg
|
|
21
21
|
* (the fetch + verify/push step, before any of the above feeding) through the
|
|
22
|
-
* `PriceUpdateRule`
|
|
22
|
+
* `PriceUpdateRule` of EVERY source in the `host.oracleSources` fed set — see
|
|
23
|
+
* `rule-registry.ts`.
|
|
23
24
|
*/
|
|
24
25
|
import { aggregate as aggregateCall, newCollector } from "../generated/waterx_oracle/oracle.js";
|
|
25
26
|
import { OracleFeeSourceUnavailableError } from "./pyth.js";
|
|
@@ -115,7 +116,13 @@ export function aggregateTicker(tx, host, args) {
|
|
|
115
116
|
// waterx_rule::collect_batch_latest verifies the batch signature and feeds
|
|
116
117
|
// this collector's symbol from the batch. If the ticker's aggregator does
|
|
117
118
|
// not (yet) weight `WaterxRule`, the contribution is silently dropped
|
|
118
|
-
// on-chain — feeding ahead of the weight migration is
|
|
119
|
+
// on-chain — feeding ahead of the weight migration is safe for THIS tx.
|
|
120
|
+
// CAVEAT (unlike lazer): the feed call records a per-symbol signed-
|
|
121
|
+
// timestamp high-water mark REGARDLESS of weights, and a replayed
|
|
122
|
+
// timestamp ABORTS (`EReplayedSignature`, audit F-014) — so two PTBs
|
|
123
|
+
// carrying the same envelope for the same symbol cannot both land; the
|
|
124
|
+
// second aborts even where waterx is unweighted. See WaterxRule's module
|
|
125
|
+
// header.
|
|
119
126
|
feedWaterxRule(tx, host, collector, args.waterxEnvelope);
|
|
120
127
|
fed = true;
|
|
121
128
|
}
|
|
@@ -167,27 +174,26 @@ export function aggregateTickerWithConstant(tx, host, args) {
|
|
|
167
174
|
* entry, Lazer if the lazer update leg served it — see below — Supra when
|
|
168
175
|
* enabled, Constant when it's a constant ticker).
|
|
169
176
|
*
|
|
170
|
-
* Before that, the on-chain price *update* leg is routed by
|
|
171
|
-
* (see `rule-registry.ts`):
|
|
172
|
-
* `supportedTickers(host)
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* dry-run, which is correct.
|
|
177
|
+
* Before that, the on-chain price *update* leg is routed by the
|
|
178
|
+
* `host.oracleSources` fed set (see `rule-registry.ts`): EVERY listed source
|
|
179
|
+
* updates the tickers its own `supportedTickers(host)` serves, all in this one
|
|
180
|
+
* PTB. There is **no cross-source fallback** — a requested ticker NO listed
|
|
181
|
+
* source serves, and that is not a constant-only ticker (which needs no
|
|
182
|
+
* price-update leg), fails the build immediately with a clear error naming
|
|
183
|
+
* the ticker and the list. That is the deliberate "fail the tx-build, don't
|
|
184
|
+
* silently reroute" contract: a wrong-but-present feed id is NOT validated
|
|
185
|
+
* here (it surfaces on-chain at dry-run); a ticker MISSING from every listed
|
|
186
|
+
* source's feeds is caught here.
|
|
181
187
|
*
|
|
182
|
-
*
|
|
183
|
-
* per-rule PTB atomicity. A fee-source pre-check runs first (
|
|
184
|
-
* `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
|
|
185
|
-
* fee-charging source with no `opts.feeSource` throws
|
|
188
|
+
* Each source's fetch + build runs against its own infra, guaranteeing
|
|
189
|
+
* per-rule PTB atomicity. A fee-source pre-check runs first (any listed
|
|
190
|
+
* source's `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
|
|
191
|
+
* so a fee-charging source with no `opts.feeSource` throws
|
|
186
192
|
* `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
|
|
187
|
-
* moveCalls. Only once that check passes
|
|
188
|
-
*
|
|
189
|
-
* can still fail mid-append for
|
|
190
|
-
* throw.
|
|
193
|
+
* moveCalls. Only once that check passes do the off-chain fetches run — in
|
|
194
|
+
* parallel across sources — and ALL settle before the first PTB mutation;
|
|
195
|
+
* on-chain reads inside `buildUpdateCalls` can still fail mid-append for
|
|
196
|
+
* other reasons — callers discard the tx on any throw.
|
|
191
197
|
*
|
|
192
198
|
* **Collector-feed leg is rule-aware:** a lazer-served group's
|
|
193
199
|
* `buildUpdateCalls` returns the verified `Update` PTB value
|
|
@@ -208,78 +214,121 @@ export function aggregateTickerWithConstant(tx, host, args) {
|
|
|
208
214
|
export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
|
|
209
215
|
if (tickers.length === 0)
|
|
210
216
|
return;
|
|
217
|
+
// Dedupe the caller's list (order-preserving): a repeated ticker would
|
|
218
|
+
// otherwise aggregate TWICE in this one PTB — wasted gas for every rule,
|
|
219
|
+
// and a hard ABORT under waterx: the second `collect_batch_latest` carries
|
|
220
|
+
// the same envelope, and the on-chain per-symbol replay guard rejects an
|
|
221
|
+
// already-accepted signed timestamp (`EReplayedSignature`, F-014) even
|
|
222
|
+
// inside a single transaction.
|
|
223
|
+
tickers = [...new Set(tickers)];
|
|
211
224
|
// price_info_object lookup for every ticker with a pyth_rule.feeds entry —
|
|
212
225
|
// needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
|
|
213
226
|
// which rule performed the on-chain update for that ticker.
|
|
214
227
|
const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
|
|
215
228
|
const priceInfoByTicker = new Map();
|
|
216
229
|
pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
//
|
|
230
|
+
// The fed set is a LIST (`host.oracleSources`, normalized + deduped at
|
|
231
|
+
// client creation): ONE build carries every listed source's data, and the
|
|
232
|
+
// chain's per-ticker weight tables decide which contributions count —
|
|
233
|
+
// feeding an unweighted rule's PRICE is dropped on-chain, while starving a
|
|
234
|
+
// weighted one aborts. That asymmetry is what makes weight migrations
|
|
235
|
+
// safe: flip weights per ticker at any time while the fed set stays a
|
|
236
|
+
// superset of every ticker's weighted set. (One caveat: waterx's feed call
|
|
237
|
+
// burns a per-symbol signed-timestamp high-water mark regardless of
|
|
238
|
+
// weights — see aggregateTicker's waterx branch.) Still NO fallback
|
|
239
|
+
// BETWEEN sources: each group serves only the tickers its own feeds list.
|
|
240
|
+
// Zero-ticker groups are dropped here so everything downstream (fee check,
|
|
241
|
+
// fetch fan-out, update-leg build) can assume every group has work.
|
|
242
|
+
const groups = host.oracleSources
|
|
243
|
+
.map((source) => {
|
|
244
|
+
const rule = resolveOracleRule(source, opts.ruleOverrides);
|
|
245
|
+
const supported = new Set(rule.supportedTickers(host));
|
|
246
|
+
return { source, rule, tickers: tickers.filter((t) => supported.has(t)) };
|
|
247
|
+
})
|
|
248
|
+
.filter((group) => group.tickers.length > 0);
|
|
249
|
+
// Fail the tx-build (NOT client init, NOT a silent reroute) when NO listed
|
|
250
|
+
// source has a feed for a requested ticker that actually needs a price
|
|
225
251
|
// update. Only a CONSTANT-ONLY ticker is exempt — priced entirely by
|
|
226
252
|
// `constant_rule`, it needs no update leg from any source. A DUAL-FEED ticker
|
|
227
253
|
// (constant AND pyth) still needs its Pyth leg refreshed, so `isConstantTicker`
|
|
228
|
-
// alone must NOT exempt it:
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
254
|
+
// alone must NOT exempt it: with no source able to serve it, feeding an
|
|
255
|
+
// unrefreshed Pyth leg would price it stale (or abort on a missing weighted
|
|
256
|
+
// source). `priceInfoByTicker.has(t)` ⇔ the ticker has a `pyth_rule.feeds`
|
|
257
|
+
// entry, so `constant && !hasPyth` is exactly constant-only. This catches a
|
|
258
|
+
// MISSING feed; a present-but-WRONG feed id is deliberately not validated
|
|
259
|
+
// here (it aborts on-chain at dry-run).
|
|
260
|
+
const covered = new Set(groups.flatMap((group) => group.tickers));
|
|
234
261
|
const isConstantOnly = (t) => host.isConstantTicker(t) && !priceInfoByTicker.has(t);
|
|
235
|
-
const unservable = tickers.filter((t) => !
|
|
262
|
+
const unservable = tickers.filter((t) => !covered.has(t) && !isConstantOnly(t));
|
|
236
263
|
if (unservable.length > 0) {
|
|
237
|
-
|
|
264
|
+
const sources = host.oracleSources.join(", ");
|
|
265
|
+
throw new Error(`oracleSource [${sources}] has no feed configured for ticker(s): ` +
|
|
238
266
|
`${unservable.join(", ")}. Sources are self-contained with no fallback — add ` +
|
|
239
|
-
|
|
267
|
+
`feeds for them under a listed source, or list a source that serves them.`);
|
|
240
268
|
}
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
if (selectedGroup.length > 0 && !opts.feeSource && selectedRule.requiresFeeSource) {
|
|
269
|
+
// Fee-source pre-check, hoisted ABOVE the off-chain fetches and PTB build
|
|
270
|
+
// below. It consults only `rule.requiresFeeSource` — known before any fetch
|
|
271
|
+
// or PTB mutation — so a fee-charging source (Pyth Core) in the fed set with
|
|
272
|
+
// no `feeSource` throws with ZERO wasted network calls and zero PTB
|
|
273
|
+
// commands, rather than waiting for `buildPythPriceUpdateCalls`'s own
|
|
274
|
+
// per-call guard to fire after the off-chain fetches already ran.
|
|
275
|
+
if (!opts.feeSource && groups.some((group) => group.rule.requiresFeeSource)) {
|
|
249
276
|
throw new OracleFeeSourceUnavailableError();
|
|
250
277
|
}
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
278
|
+
// Phase 1 — resolve every group's update data IN PARALLEL: the per-source
|
|
279
|
+
// fetches (Hermes VAA / Lazer POST / quote-center GET) are independent
|
|
280
|
+
// network calls on the tx-build money path, so a multi-source fed set must
|
|
281
|
+
// not pay one RTT per source sequentially. ALL fetches settle before the
|
|
282
|
+
// first PTB mutation below, so a fetch failure never strands moveCalls in a
|
|
283
|
+
// caller-owned tx — and a failure in ANY group fails the whole build (a
|
|
284
|
+
// listed source is load-bearing; silently building without it would starve
|
|
285
|
+
// its weighted tickers on-chain).
|
|
286
|
+
const dataByGroup = await Promise.all(groups.map((group) => resolveGroupUpdateData(host, group, opts.updateDataProvider)));
|
|
287
|
+
// Phase 2 — build each group's update leg sequentially, in list order, so
|
|
288
|
+
// PTB command order stays deterministic. The carry step below is an
|
|
289
|
+
// exhaustive switch over the group's rule kind: a future source whose feed
|
|
290
|
+
// leg needs per-ticker data from its update leg must decide its carry here
|
|
291
|
+
// — falling through silently would starve its weighted tickers on-chain.
|
|
255
292
|
const lazerUpdateByTicker = new Map();
|
|
256
293
|
// Signed batch envelope per waterx-served ticker. Unlike Lazer's shared PTB
|
|
257
294
|
// handle, waterx's verify+feed is bundled into `collect_batch_latest` in the
|
|
258
295
|
// per-ticker feed leg, so its `buildUpdateCalls` emits nothing and the
|
|
259
296
|
// envelope is carried straight from the group's fetched data.
|
|
260
297
|
const waterxEnvelopeByTicker = new Map();
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
const handle = (await selectedRule.buildUpdateCalls(tx, host, data, {
|
|
298
|
+
for (const [i, group] of groups.entries()) {
|
|
299
|
+
const data = dataByGroup[i] ?? null;
|
|
300
|
+
const handle = (await group.rule.buildUpdateCalls(tx, host, data, {
|
|
265
301
|
cache: opts.cache,
|
|
266
302
|
feeSource: opts.feeSource,
|
|
267
303
|
})) ?? undefined;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
304
|
+
switch (group.rule.kind) {
|
|
305
|
+
case "pyth_rule":
|
|
306
|
+
// Core's update leg wrote the PriceInfoObjects in place — the feed
|
|
307
|
+
// leg reads them by id (`priceInfoByTicker`), nothing to carry.
|
|
308
|
+
break;
|
|
309
|
+
case "pyth_lazer_rule":
|
|
310
|
+
// Route by the handle's kind discriminant — the tag exists so a
|
|
311
|
+
// non-lazer handle can never be silently fed into
|
|
312
|
+
// pyth_lazer_rule::feed.
|
|
313
|
+
if (handle?.kind === "pyth_lazer_rule") {
|
|
314
|
+
for (const ticker of group.tickers)
|
|
315
|
+
lazerUpdateByTicker.set(ticker, handle.update);
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
case "waterx_rule": {
|
|
319
|
+
// waterx_rule emits no shared handle (verify+feed is bundled into the
|
|
320
|
+
// per-ticker `collect_batch_latest`), so the envelope is carried
|
|
321
|
+
// straight from this group's fetched data to the feed leg below.
|
|
322
|
+
const envelope = waterxEnvelopeOf(data);
|
|
323
|
+
if (envelope) {
|
|
324
|
+
for (const ticker of group.tickers)
|
|
325
|
+
waterxEnvelopeByTicker.set(ticker, envelope);
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
default: {
|
|
330
|
+
const exhausted = group.rule.kind;
|
|
331
|
+
throw new Error(`refreshOraclePrices: unhandled rule kind '${String(exhausted)}'`);
|
|
283
332
|
}
|
|
284
333
|
}
|
|
285
334
|
}
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { BasePackageEntry } from "../account/config.ts";
|
|
13
13
|
import type { BaseLineConfig } from "../base-client.ts";
|
|
14
|
-
import type { Network } from "../constants.ts";
|
|
15
14
|
import type { FetchPolicy } from "./update-fetch.ts";
|
|
16
15
|
export interface PythRulePackage extends BasePackageEntry {
|
|
17
16
|
config: string;
|
|
@@ -31,7 +30,7 @@ export interface PythSponsorRulePackage extends BasePackageEntry {
|
|
|
31
30
|
*
|
|
32
31
|
* `enabled` mirrors the JSON field verbatim but MUST NOT be read for routing —
|
|
33
32
|
* which rule prices a ticker is decided solely by the client's `oracleSource`
|
|
34
|
-
* create option (see `OracleHost.
|
|
33
|
+
* create option (see `OracleHost.oracleSources`), never by this flag or any
|
|
35
34
|
* other config value.
|
|
36
35
|
*/
|
|
37
36
|
export interface PythLazerRulePackage extends BasePackageEntry {
|
|
@@ -99,11 +98,12 @@ export interface WaterxRuleFeedEntry {
|
|
|
99
98
|
* Read by `WaterxRule` (`rules/waterx-rule.ts`): `feeds` for ticker support,
|
|
100
99
|
* `config`/`enclave_config`/`enclave` for the `collect_batch_latest` call,
|
|
101
100
|
* `published_at` for the package address. The off-chain signed price is pulled
|
|
102
|
-
* from the quote-center (endpoint from
|
|
101
|
+
* from the quote-center (endpoint from the rule-owned `WATERX_INFRA` table in
|
|
102
|
+
* `rules/waterx-rule.ts`), not this JSON.
|
|
103
103
|
*
|
|
104
104
|
* `enabled` mirrors the JSON field verbatim but MUST NOT be read for routing —
|
|
105
105
|
* which rule prices a ticker is decided solely by the client's `oracleSource`
|
|
106
|
-
* create option (see `OracleHost.
|
|
106
|
+
* create option (see `OracleHost.oracleSources`), mirroring `pyth_lazer_rule`.
|
|
107
107
|
*/
|
|
108
108
|
export interface WaterxRulePackage extends BasePackageEntry {
|
|
109
109
|
/** Shared `waterx_rule::Config` (per-symbol on-chain feed_config). */
|
|
@@ -137,17 +137,6 @@ export interface OraclePackages {
|
|
|
137
137
|
waterx_rule?: WaterxRulePackage;
|
|
138
138
|
waterx_oracle: WaterxOraclePackage;
|
|
139
139
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Resolved Pyth Core infra as it lives on `client.pyth` — NOT a config-JSON
|
|
142
|
-
* shape. `state_id` / `wormhole_state_id` / `hermes_endpoint` come verbatim
|
|
143
|
-
* from the fixed per-network constant ({@link PYTH_DEFAULTS}); `api_key` /
|
|
144
|
-
* `fetch` are layered on from the caller's `pythApiKey` / `pythFetch` create
|
|
145
|
-
* options. None of it is sourced from the canonical `waterx-config` JSON — the
|
|
146
|
-
* SDK never reads a `pyth` block there (a Bearer secret has no place in a
|
|
147
|
-
* public CDN document). The infra is the same for every `oracleSource`; the
|
|
148
|
-
* `pyth_lazer_rule` source reads only the `api_key` / `fetch` from here and
|
|
149
|
-
* gets its on-chain infra from {@link LAZER_DEFAULTS} + config instead.
|
|
150
|
-
*/
|
|
151
140
|
/**
|
|
152
141
|
* The caller-tunable subset of `fetchWithPolicy`'s policy exposed on the
|
|
153
142
|
* `pythFetch` create option and `client.pyth.fetch` — the retry/timeout budget
|
|
@@ -160,18 +149,24 @@ export type PythFetchPolicy = {
|
|
|
160
149
|
timeoutMs?: number;
|
|
161
150
|
retries?: number;
|
|
162
151
|
};
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
152
|
+
/**
|
|
153
|
+
* `client.pyth` — ONLY the caller-supplied Pyth credential + fetch policy,
|
|
154
|
+
* shared by the Pyth-family rules (`pyth_rule`, `pyth_lazer_rule`). It carries
|
|
155
|
+
* NO endpoints and NO on-chain object ids: every oracle source owns its own
|
|
156
|
+
* infra, co-located with its rule (`PYTH_CORE_INFRA` in `oracle/pyth.ts`;
|
|
157
|
+
* the Lazer constants inside `rules/pyth-lazer-rule.ts`). A non-Pyth source
|
|
158
|
+
* never reads this slice.
|
|
159
|
+
* Nothing here is sourced from the canonical `waterx-config` JSON — a Bearer
|
|
160
|
+
* secret has no place in a public CDN document.
|
|
161
|
+
*/
|
|
162
|
+
export interface PythAccessConfig {
|
|
167
163
|
/**
|
|
168
|
-
* Pyth
|
|
164
|
+
* Pyth access token (`Authorization: Bearer …`). Required by
|
|
169
165
|
* `PythLazerRule`'s signed-update fetch — Lazer is auth-first, so there is
|
|
170
|
-
* no keyless default
|
|
171
|
-
*
|
|
172
|
-
* `
|
|
173
|
-
*
|
|
174
|
-
* migration (post-2026-08-18, per
|
|
166
|
+
* no keyless default; absent when a lazer-routed fetch runs →
|
|
167
|
+
* `LazerApiKeyMissing` is thrown at fetch time. Supplied via the
|
|
168
|
+
* `pythApiKey` create option (the SDK never reads `process.env` or the
|
|
169
|
+
* config JSON). As of the Pyth Pro migration (post-2026-08-18, per
|
|
175
170
|
* https://docs.pyth.network/price-feeds/core/upgrade) this is ALSO required
|
|
176
171
|
* for `pyth_rule`'s Hermes fetch (`fetchPriceFeedsUpdateData`) — see
|
|
177
172
|
* `fetch` below.
|
|
@@ -187,66 +182,37 @@ export interface PythInfraConfig {
|
|
|
187
182
|
*/
|
|
188
183
|
fetch?: PythFetchPolicy;
|
|
189
184
|
}
|
|
190
|
-
export declare const PYTH_DEFAULTS: Record<Network, PythInfraConfig>;
|
|
191
|
-
/**
|
|
192
|
-
* Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
|
|
193
|
-
* Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
|
|
194
|
-
* operates (not part of the `waterx-config` JSON). A fuller `PYTH_INFRA`
|
|
195
|
-
* restructure is deferred — this stays a minimal map until then.
|
|
196
|
-
*
|
|
197
|
-
* - `endpoint` — Lazer HTTP API base; signed updates come from
|
|
198
|
-
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
199
|
-
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
200
|
-
* Lazer signers), so both networks share the production host.
|
|
201
|
-
* - `verifier_package` — the Sui package carrying
|
|
202
|
-
* `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
|
|
203
|
-
* still the original v1 publish; mainnet is the v2-upgraded package (which
|
|
204
|
-
* still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
|
|
205
|
-
* contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
|
|
206
|
-
*/
|
|
207
|
-
export declare const LAZER_DEFAULTS: Record<Network, {
|
|
208
|
-
endpoint: string;
|
|
209
|
-
verifier_package: string;
|
|
210
|
-
}>;
|
|
211
185
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
186
|
+
* `client.waterx` — ONLY the caller-supplied quote-center overrides for
|
|
187
|
+
* `WaterxRule`, mirroring {@link PythAccessConfig}: no resolved infra lives on
|
|
188
|
+
* the client. When a field is unset the rule resolves it against its OWN
|
|
189
|
+
* per-network table (`WATERX_INFRA` in `rules/waterx-rule.ts`) — no other
|
|
190
|
+
* source's endpoint or policy is ever consulted.
|
|
215
191
|
*
|
|
216
|
-
* The override exists because this is the one oracle source a BROWSER
|
|
217
|
-
* itself: the rule pulls the signed envelope from the page, so it is
|
|
218
|
-
* the quote-center deployment's CORS allowlist. A front end whose
|
|
219
|
-
* on that list — or one that must route egress through its own
|
|
220
|
-
* `endpoint` at a same-origin proxy (or supplies
|
|
221
|
-
* being locked to the
|
|
192
|
+
* The endpoint override exists because this is the one oracle source a BROWSER
|
|
193
|
+
* fetches itself: the rule pulls the signed envelope from the page, so it is
|
|
194
|
+
* subject to the quote-center deployment's CORS allowlist. A front end whose
|
|
195
|
+
* origin is not on that list — or one that must route egress through its own
|
|
196
|
+
* backend — points `endpoint` at a same-origin proxy (or supplies
|
|
197
|
+
* `fetch.fetchImpl`) instead of being locked to the default host.
|
|
222
198
|
*/
|
|
223
|
-
export interface
|
|
199
|
+
export interface WaterxAccessConfig {
|
|
224
200
|
/**
|
|
225
|
-
* Quote-center base URL
|
|
226
|
-
* `joinEndpointPath`, so
|
|
201
|
+
* Quote-center base URL override (`waterxEndpoint` create option). A base
|
|
202
|
+
* PATH is preserved — the rule appends via `joinEndpointPath`, so
|
|
203
|
+
* `https://app.example/api/quote-center` resolves to
|
|
227
204
|
* `…/api/quote-center/v1/quotes/update` and a proxy route is not rewritten
|
|
228
205
|
* away. A trailing slash is trimmed.
|
|
229
206
|
*/
|
|
230
|
-
endpoint
|
|
207
|
+
endpoint?: string;
|
|
231
208
|
/**
|
|
232
209
|
* Retry/timeout policy (and `fetchImpl`) for the quote-center fetch — see
|
|
233
210
|
* `fetchWithPolicy` (`./update-fetch.ts`). Supplied via the `waterxFetch`
|
|
234
|
-
* create option.
|
|
235
|
-
*
|
|
211
|
+
* create option. Falls back to `fetchWithPolicy`'s built-in defaults (15s
|
|
212
|
+
* timeout, 2 retries) when unset — never to another source's policy.
|
|
236
213
|
*/
|
|
237
214
|
fetch?: FetchPolicy;
|
|
238
215
|
}
|
|
239
|
-
/**
|
|
240
|
-
* WaterX quote-center base URL by network — the first-party TEE-signed price
|
|
241
|
-
* hub `WaterxRule` pulls from (`GET /v1/quotes/update?symbols=…`). Mirrors
|
|
242
|
-
* {@link LAZER_DEFAULTS}: infra WaterX operates, not part of the `waterx-config`
|
|
243
|
-
* JSON. Public read (no auth), so there is no api_key. `endpoint` has no
|
|
244
|
-
* trailing slash — the rule appends the path.
|
|
245
|
-
*
|
|
246
|
-
* These are DEFAULTS, not a hard pin: a consumer overrides them per client via
|
|
247
|
-
* `waterxEndpoint` / `waterxFetch` (resolved onto `client.waterx`).
|
|
248
|
-
*/
|
|
249
|
-
export declare const WATERX_DEFAULTS: Record<Network, WaterxInfraConfig>;
|
|
250
216
|
/**
|
|
251
217
|
* The narrow config shape the oracle/refresh code needs. `WaterXConfig`
|
|
252
218
|
* (the perp line's full config) is assignable to this, so `PerpClient` satisfies
|
|
@@ -9,58 +9,4 @@
|
|
|
9
9
|
* depends on this shared schema instead of the perp line's full config —
|
|
10
10
|
* mirrors the earlier account-config hoist.
|
|
11
11
|
*/
|
|
12
|
-
export
|
|
13
|
-
MAINNET: {
|
|
14
|
-
state_id: "0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8",
|
|
15
|
-
wormhole_state_id: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
|
|
16
|
-
hermes_endpoint: "https://hermes.pyth.network",
|
|
17
|
-
},
|
|
18
|
-
TESTNET: {
|
|
19
|
-
state_id: "0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c",
|
|
20
|
-
wormhole_state_id: "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
|
|
21
|
-
hermes_endpoint: "https://hermes-beta.pyth.network",
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// Pyth Lazer — external infra, defaults by network
|
|
26
|
-
// ============================================================================
|
|
27
|
-
/**
|
|
28
|
-
* Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
|
|
29
|
-
* Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
|
|
30
|
-
* operates (not part of the `waterx-config` JSON). A fuller `PYTH_INFRA`
|
|
31
|
-
* restructure is deferred — this stays a minimal map until then.
|
|
32
|
-
*
|
|
33
|
-
* - `endpoint` — Lazer HTTP API base; signed updates come from
|
|
34
|
-
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
35
|
-
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
36
|
-
* Lazer signers), so both networks share the production host.
|
|
37
|
-
* - `verifier_package` — the Sui package carrying
|
|
38
|
-
* `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
|
|
39
|
-
* still the original v1 publish; mainnet is the v2-upgraded package (which
|
|
40
|
-
* still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
|
|
41
|
-
* contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
|
|
42
|
-
*/
|
|
43
|
-
export const LAZER_DEFAULTS = {
|
|
44
|
-
MAINNET: {
|
|
45
|
-
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
46
|
-
verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
|
|
47
|
-
},
|
|
48
|
-
TESTNET: {
|
|
49
|
-
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
50
|
-
verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* WaterX quote-center base URL by network — the first-party TEE-signed price
|
|
55
|
-
* hub `WaterxRule` pulls from (`GET /v1/quotes/update?symbols=…`). Mirrors
|
|
56
|
-
* {@link LAZER_DEFAULTS}: infra WaterX operates, not part of the `waterx-config`
|
|
57
|
-
* JSON. Public read (no auth), so there is no api_key. `endpoint` has no
|
|
58
|
-
* trailing slash — the rule appends the path.
|
|
59
|
-
*
|
|
60
|
-
* These are DEFAULTS, not a hard pin: a consumer overrides them per client via
|
|
61
|
-
* `waterxEndpoint` / `waterxFetch` (resolved onto `client.waterx`).
|
|
62
|
-
*/
|
|
63
|
-
export const WATERX_DEFAULTS = {
|
|
64
|
-
MAINNET: { endpoint: "https://quote-center.waterx.app" },
|
|
65
|
-
TESTNET: { endpoint: "https://quote-center-staging.waterx.app" },
|
|
66
|
-
};
|
|
12
|
+
export {};
|
|
@@ -9,37 +9,41 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
11
11
|
import type { Network } from "../constants.ts";
|
|
12
|
-
import type { OracleConfig,
|
|
12
|
+
import type { OracleConfig, PythAccessConfig, WaterxAccessConfig } from "./config.ts";
|
|
13
13
|
import type { OracleSource } from "./price-update-rule.ts";
|
|
14
14
|
export interface OracleHost {
|
|
15
|
-
/** Sui network this client targets —
|
|
15
|
+
/** Sui network this client targets — each rule keys its OWN infra table by it (`PYTH_CORE_INFRA`, `LAZER_INFRA`). */
|
|
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
|
-
/**
|
|
20
|
-
readonly pyth:
|
|
19
|
+
/** Caller-supplied Pyth credential + fetch policy (create options) — NO endpoints, NO object ids. */
|
|
20
|
+
readonly pyth: PythAccessConfig;
|
|
21
21
|
/**
|
|
22
|
-
* WaterX quote-center
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* (`
|
|
29
|
-
*
|
|
22
|
+
* Caller-supplied WaterX quote-center overrides for `WaterxRule`
|
|
23
|
+
* (`waterxEndpoint` / `waterxFetch` create options) — access-only, mirroring
|
|
24
|
+
* `pyth` above. OPTIONAL so an existing host stays a valid `OracleHost`;
|
|
25
|
+
* unset fields resolve against the rule's own `WATERX_INFRA[network]` table.
|
|
26
|
+
* This is the hook a browser consumer uses to route the quote-center fetch
|
|
27
|
+
* through a same-origin proxy (`endpoint`) or its own transport
|
|
28
|
+
* (`fetch.fetchImpl`) — that request is made from the page, so it is bound
|
|
29
|
+
* by the quote-center's CORS allowlist.
|
|
30
30
|
*/
|
|
31
|
-
readonly waterx?:
|
|
31
|
+
readonly waterx?: WaterxAccessConfig;
|
|
32
32
|
/** gRPC client for the on-chain reads the Pyth update path needs. */
|
|
33
33
|
readonly grpcClient: SuiGrpcClient;
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
35
|
+
* The FED SET for `refreshOraclePrices`'s update legs — the REQUIRED
|
|
36
|
+
* `oracleSource` create option normalized to a non-empty, deduped list.
|
|
37
|
+
* Every listed source's data is fetched and fed in one build; the chain's
|
|
38
|
+
* per-ticker weight tables decide which contributions count (feeding an
|
|
39
|
+
* unweighted rule is dropped on-chain; starving a weighted one aborts), so
|
|
40
|
+
* during weight migrations the list stays a SUPERSET of every ticker's
|
|
41
|
+
* weighted set. Routing is driven by this value ALONE: never by a config
|
|
42
|
+
* JSON `enabled` flag and never by `process.env` — the SDK never reads it;
|
|
43
|
+
* consumers (BE/FE) wire this option from their own env var
|
|
44
|
+
* (`ORACLE_SOURCE`, comma-separated).
|
|
41
45
|
*/
|
|
42
|
-
readonly
|
|
46
|
+
readonly oracleSources: readonly OracleSource[];
|
|
43
47
|
/** True when `ticker` is priced by `constant_rule`. */
|
|
44
48
|
isConstantTicker(ticker: string): boolean;
|
|
45
49
|
/** The `supra_rule` config when deployed, enabled, and fully wired; else `undefined`. */
|
|
@@ -18,15 +18,17 @@
|
|
|
18
18
|
export type { OracleHost } from "./host.ts";
|
|
19
19
|
export { FetchPolicyError, fetchWithPolicy, joinEndpointPath } from "./update-fetch.ts";
|
|
20
20
|
export type { FetchPolicy } from "./update-fetch.ts";
|
|
21
|
-
export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls, updatePythPrices, HermesEndpointRejectedAllFeedsError, MISSING_FEED_MEMO_TTL_MS, OracleFeeSourceUnavailableError, } from "./pyth.ts";
|
|
21
|
+
export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls, pythCoreHermesEndpoint, pythProHermesEndpoint, PYTH_PRO_HERMES_ENDPOINT, 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
|
+
export { resolveOracleReadPlan, resolveHermesReadEndpoint } from "./read-plane.ts";
|
|
25
|
+
export type { OracleReadPlan } from "./read-plane.ts";
|
|
24
26
|
export { PythCoreRule } from "./rules/pyth-core-rule.ts";
|
|
25
27
|
export type { PythCoreUpdatePayload } from "./rules/pyth-core-rule.ts";
|
|
26
28
|
export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.ts";
|
|
27
29
|
export type { PythLazerUpdatePayload } from "./rules/pyth-lazer-rule.ts";
|
|
28
|
-
export { WaterxRule, parseSignedEnvelope } from "./rules/waterx-rule.ts";
|
|
30
|
+
export { WaterxRule, parseSignedEnvelope, BATCH_PRICE_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint, } from "./rules/waterx-rule.ts";
|
|
29
31
|
export type { WaterxUpdatePayload, WaterxSignedEnvelope, WaterxBatchItem, } from "./rules/waterx-rule.ts";
|
|
30
|
-
export { OracleSourceNotImplementedError } from "./rule-registry.ts";
|
|
32
|
+
export { OracleSourceNotImplementedError, resolveOracleRule } from "./rule-registry.ts";
|
|
31
33
|
export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.ts";
|
|
32
34
|
export { openPythSponsorFund, reimbursePythSponsor } from "./rules/sponsor.ts";
|
package/dist/src/oracle/index.js
CHANGED
|
@@ -33,24 +33,39 @@ export { FetchPolicyError, fetchWithPolicy, joinEndpointPath } from "./update-fe
|
|
|
33
33
|
// branch on the fee-source failure directly, and a consumer of
|
|
34
34
|
// `fetchPriceFeedsUpdateData` / `probeMissingFeeds` can tell a misconfigured
|
|
35
35
|
// or unentitled endpoint apart from feeds that endpoint genuinely lacks.
|
|
36
|
-
export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls,
|
|
36
|
+
export { PythCache, fetchPriceFeedsUpdateData, endpointSupportedFeedIds, probeMissingFeeds, buildPythPriceUpdateCalls,
|
|
37
|
+
// The pyth read-plane endpoint accessors — Core (keyless, per network) and
|
|
38
|
+
// Pro (the documented fixed base; auth via the caller's Bearer key). There
|
|
39
|
+
// is no client-level endpoint field: consumers pick via
|
|
40
|
+
// `resolveHermesReadEndpoint` (pyth_rule listed → Core, else override ??
|
|
41
|
+
// Pro) — never a hand-rolled branch, never a cross-source fallback.
|
|
42
|
+
pythCoreHermesEndpoint, pythProHermesEndpoint, PYTH_PRO_HERMES_ENDPOINT, updatePythPrices, HermesEndpointRejectedAllFeedsError, MISSING_FEED_MEMO_TTL_MS, OracleFeeSourceUnavailableError, } from "./pyth.js";
|
|
43
|
+
// Per-source READ-plane resolution — which tickers a source can price
|
|
44
|
+
// off-chain and with which ids (`resolveOracleReadPlan`), and which
|
|
45
|
+
// Hermes-compatible base the hermes plans execute against
|
|
46
|
+
// (`resolveHermesReadEndpoint`: pyth_rule listed → Core, else override ??
|
|
47
|
+
// the documented Pyth Pro base). The one place the "lazer reads through
|
|
48
|
+
// `pyth_rule.feeds` hex ids" invariant lives; consumers resolve through
|
|
49
|
+
// this instead of hardcoding namespace sharing or endpoint branching.
|
|
50
|
+
export { resolveOracleReadPlan, resolveHermesReadEndpoint } from "./read-plane.js";
|
|
37
51
|
// Pyth Core rule (PriceUpdateRule wrapper over the Pyth source above)
|
|
38
52
|
export { PythCoreRule } from "./rules/pyth-core-rule.js";
|
|
39
53
|
// Pyth Lazer rule (signed-update generation; `feedLazerRule` stays internal to `aggregate.ts`)
|
|
40
54
|
// `LazerApiKeyMissingError` is re-exported (not just the type) for the same
|
|
41
55
|
// `instanceof` reason as `OracleFeeSourceUnavailableError` above.
|
|
42
56
|
export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.js";
|
|
57
|
+
// `WATERX_INFRA` / `waterxQuoteCenterEndpoint` are the source's own infra table +
|
|
58
|
+
// read-plane accessor (mirrors `pythCoreHermesEndpoint`).
|
|
43
59
|
// WaterX quote-center rule (first-party ed25519 signed batches; `feedWaterxRule`
|
|
44
60
|
// stays internal to `aggregate.ts`).
|
|
45
|
-
export { WaterxRule, parseSignedEnvelope } from "./rules/waterx-rule.js";
|
|
46
|
-
// `resolveOracleRule`
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// above)
|
|
52
|
-
|
|
53
|
-
export { OracleSourceNotImplementedError } from "./rule-registry.js";
|
|
61
|
+
export { WaterxRule, parseSignedEnvelope, BATCH_PRICE_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint, } from "./rules/waterx-rule.js";
|
|
62
|
+
// `resolveOracleRule` is the ONE source→rule registry — exported so external
|
|
63
|
+
// consumers (e.g. a BE prefetch cache that keys per source and needs each
|
|
64
|
+
// source's `supportedTickers`/`fetchUpdateData`) resolve through it instead of
|
|
65
|
+
// hand-mirroring the map and drifting. `OracleSourceNotImplementedError` is
|
|
66
|
+
// its `instanceof`-able failure (same reason as `OracleFeeSourceUnavailableError`
|
|
67
|
+
// above).
|
|
68
|
+
export { OracleSourceNotImplementedError, resolveOracleRule } from "./rule-registry.js";
|
|
54
69
|
// Aggregation orchestrator
|
|
55
70
|
export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.js";
|
|
56
71
|
// Sponsor rule (fund open / reimburse + witness attach)
|