@waterx/sdk 4.1.0 → 4.3.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 +29 -19
- 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 +121 -71
- 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 +7 -3
- package/dist/cjs/src/oracle/index.js +41 -8
- package/dist/cjs/src/oracle/price-update-rule.d.ts +32 -11
- package/dist/cjs/src/oracle/price-update-rule.js +23 -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 +85 -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 +6 -5
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +34 -6
- 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 +61 -18
- package/dist/cjs/src/oracle/source-list.d.ts +36 -0
- package/dist/cjs/src/oracle/source-list.js +57 -0
- package/dist/cjs/src/perp/client.d.ts +41 -28
- package/dist/cjs/src/perp/client.js +32 -20
- package/dist/cjs/src/perp/config-view.js +7 -6
- 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 +11 -4
- package/dist/cjs/src/perp/user/staking.js +2 -1
- package/dist/cjs/src/unified-client.d.ts +14 -9
- package/dist/cjs/src/unified-client.js +2 -2
- package/dist/cjs/src/utils/config.js +2 -1
- package/dist/cjs/src/utils/record.d.ts +12 -0
- package/dist/cjs/src/utils/record.js +22 -0
- 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 +121 -71
- 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 +7 -3
- package/dist/src/oracle/index.js +32 -10
- package/dist/src/oracle/price-update-rule.d.ts +32 -11
- package/dist/src/oracle/price-update-rule.js +22 -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 +81 -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 +7 -6
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +33 -5
- 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 +57 -15
- package/dist/src/oracle/source-list.d.ts +36 -0
- package/dist/src/oracle/source-list.js +53 -0
- package/dist/src/perp/client.d.ts +41 -28
- package/dist/src/perp/client.js +33 -21
- package/dist/src/perp/config-view.js +7 -6
- 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/perp/user/staking.js +2 -1
- package/dist/src/unified-client.d.ts +14 -9
- package/dist/src/unified-client.js +2 -2
- package/dist/src/utils/config.js +2 -1
- package/dist/src/utils/record.d.ts +12 -0
- package/dist/src/utils/record.js +19 -0
- package/package.json +1 -1
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* `refreshOraclePrices` additionally routes the on-chain price *update* leg
|
|
22
22
|
* (the fetch + verify/push step, before any of the above feeding) through the
|
|
23
|
-
* `PriceUpdateRule`
|
|
23
|
+
* `PriceUpdateRule` of EVERY source in the `host.oracleSources` fed set — see
|
|
24
|
+
* `rule-registry.ts`.
|
|
24
25
|
*/
|
|
25
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
27
|
exports.aggregateTicker = aggregateTicker;
|
|
@@ -28,6 +29,7 @@ exports.aggregateTickerWithPyth = aggregateTickerWithPyth;
|
|
|
28
29
|
exports.aggregateTickerWithConstant = aggregateTickerWithConstant;
|
|
29
30
|
exports.refreshOraclePrices = refreshOraclePrices;
|
|
30
31
|
const oracle_ts_1 = require("../generated/waterx_oracle/oracle.js");
|
|
32
|
+
const record_ts_1 = require("../utils/record.js");
|
|
31
33
|
const pyth_ts_1 = require("./pyth.js");
|
|
32
34
|
const rule_registry_ts_1 = require("./rule-registry.js");
|
|
33
35
|
const constant_rule_ts_1 = require("./rules/constant-rule.js");
|
|
@@ -121,7 +123,13 @@ function aggregateTicker(tx, host, args) {
|
|
|
121
123
|
// waterx_rule::collect_batch_latest verifies the batch signature and feeds
|
|
122
124
|
// this collector's symbol from the batch. If the ticker's aggregator does
|
|
123
125
|
// not (yet) weight `WaterxRule`, the contribution is silently dropped
|
|
124
|
-
// on-chain — feeding ahead of the weight migration is
|
|
126
|
+
// on-chain — feeding ahead of the weight migration is safe for THIS tx.
|
|
127
|
+
// CAVEAT (unlike lazer): the feed call records a per-symbol signed-
|
|
128
|
+
// timestamp high-water mark REGARDLESS of weights, and a replayed
|
|
129
|
+
// timestamp ABORTS (`EReplayedSignature`, audit F-014) — so two PTBs
|
|
130
|
+
// carrying the same envelope for the same symbol cannot both land; the
|
|
131
|
+
// second aborts even where waterx is unweighted. See WaterxRule's module
|
|
132
|
+
// header.
|
|
125
133
|
(0, waterx_rule_ts_1.feedWaterxRule)(tx, host, collector, args.waterxEnvelope);
|
|
126
134
|
fed = true;
|
|
127
135
|
}
|
|
@@ -162,7 +170,7 @@ function aggregateTickerWithPyth(tx, host, args) {
|
|
|
162
170
|
* {@link refreshOraclePrices}), which feeds both.
|
|
163
171
|
*/
|
|
164
172
|
function aggregateTickerWithConstant(tx, host, args) {
|
|
165
|
-
if (host.config.packages.pyth_rule?.feeds
|
|
173
|
+
if ((0, record_ts_1.ownEntry)(host.config.packages.pyth_rule?.feeds, args.ticker) !== undefined) {
|
|
166
174
|
throw new Error(`'${args.ticker}' is in pyth_rule.feeds (dual-feed) — feed both via aggregateTicker({ priceInfoObjectId }) / refreshOraclePrices, not aggregateTickerWithConstant`);
|
|
167
175
|
}
|
|
168
176
|
aggregateTicker(tx, host, { ticker: args.ticker });
|
|
@@ -173,27 +181,26 @@ function aggregateTickerWithConstant(tx, host, args) {
|
|
|
173
181
|
* entry, Lazer if the lazer update leg served it — see below — Supra when
|
|
174
182
|
* enabled, Constant when it's a constant ticker).
|
|
175
183
|
*
|
|
176
|
-
* Before that, the on-chain price *update* leg is routed by
|
|
177
|
-
* (see `rule-registry.ts`):
|
|
178
|
-
* `supportedTickers(host)
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* dry-run, which is correct.
|
|
184
|
+
* Before that, the on-chain price *update* leg is routed by the
|
|
185
|
+
* `host.oracleSources` fed set (see `rule-registry.ts`): EVERY listed source
|
|
186
|
+
* updates the tickers its own `supportedTickers(host)` serves, all in this one
|
|
187
|
+
* PTB. There is **no cross-source fallback** — a requested ticker NO listed
|
|
188
|
+
* source serves, and that is not a constant-only ticker (which needs no
|
|
189
|
+
* price-update leg), fails the build immediately with a clear error naming
|
|
190
|
+
* the ticker and the list. That is the deliberate "fail the tx-build, don't
|
|
191
|
+
* silently reroute" contract: a wrong-but-present feed id is NOT validated
|
|
192
|
+
* here (it surfaces on-chain at dry-run); a ticker MISSING from every listed
|
|
193
|
+
* source's feeds is caught here.
|
|
187
194
|
*
|
|
188
|
-
*
|
|
189
|
-
* per-rule PTB atomicity. A fee-source pre-check runs first (
|
|
190
|
-
* `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
|
|
191
|
-
* fee-charging source with no `opts.feeSource` throws
|
|
195
|
+
* Each source's fetch + build runs against its own infra, guaranteeing
|
|
196
|
+
* per-rule PTB atomicity. A fee-source pre-check runs first (any listed
|
|
197
|
+
* source's `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
|
|
198
|
+
* so a fee-charging source with no `opts.feeSource` throws
|
|
192
199
|
* `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
|
|
193
|
-
* moveCalls. Only once that check passes
|
|
194
|
-
*
|
|
195
|
-
* can still fail mid-append for
|
|
196
|
-
* throw.
|
|
200
|
+
* moveCalls. Only once that check passes do the off-chain fetches run — in
|
|
201
|
+
* parallel across sources — and ALL settle before the first PTB mutation;
|
|
202
|
+
* on-chain reads inside `buildUpdateCalls` can still fail mid-append for
|
|
203
|
+
* other reasons — callers discard the tx on any throw.
|
|
197
204
|
*
|
|
198
205
|
* **Collector-feed leg is rule-aware:** a lazer-served group's
|
|
199
206
|
* `buildUpdateCalls` returns the verified `Update` PTB value
|
|
@@ -214,78 +221,121 @@ function aggregateTickerWithConstant(tx, host, args) {
|
|
|
214
221
|
async function refreshOraclePrices(tx, host, tickers, opts = {}) {
|
|
215
222
|
if (tickers.length === 0)
|
|
216
223
|
return;
|
|
224
|
+
// Dedupe the caller's list (order-preserving): a repeated ticker would
|
|
225
|
+
// otherwise aggregate TWICE in this one PTB — wasted gas for every rule,
|
|
226
|
+
// and a hard ABORT under waterx: the second `collect_batch_latest` carries
|
|
227
|
+
// the same envelope, and the on-chain per-symbol replay guard rejects an
|
|
228
|
+
// already-accepted signed timestamp (`EReplayedSignature`, F-014) even
|
|
229
|
+
// inside a single transaction.
|
|
230
|
+
tickers = [...new Set(tickers)];
|
|
217
231
|
// price_info_object lookup for every ticker with a pyth_rule.feeds entry —
|
|
218
232
|
// needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
|
|
219
233
|
// which rule performed the on-chain update for that ticker.
|
|
220
|
-
const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds
|
|
234
|
+
const pythTickers = tickers.filter((t) => (0, record_ts_1.ownEntry)(host.config.packages.pyth_rule?.feeds, t) !== undefined);
|
|
221
235
|
const priceInfoByTicker = new Map();
|
|
222
236
|
pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
//
|
|
237
|
+
// The fed set is a LIST (`host.oracleSources`, normalized + deduped at
|
|
238
|
+
// client creation): ONE build carries every listed source's data, and the
|
|
239
|
+
// chain's per-ticker weight tables decide which contributions count —
|
|
240
|
+
// feeding an unweighted rule's PRICE is dropped on-chain, while starving a
|
|
241
|
+
// weighted one aborts. That asymmetry is what makes weight migrations
|
|
242
|
+
// safe: flip weights per ticker at any time while the fed set stays a
|
|
243
|
+
// superset of every ticker's weighted set. (One caveat: waterx's feed call
|
|
244
|
+
// burns a per-symbol signed-timestamp high-water mark regardless of
|
|
245
|
+
// weights — see aggregateTicker's waterx branch.) Still NO fallback
|
|
246
|
+
// BETWEEN sources: each group serves only the tickers its own feeds list.
|
|
247
|
+
// Zero-ticker groups are dropped here so everything downstream (fee check,
|
|
248
|
+
// fetch fan-out, update-leg build) can assume every group has work.
|
|
249
|
+
const groups = host.oracleSources
|
|
250
|
+
.map((source) => {
|
|
251
|
+
const rule = (0, rule_registry_ts_1.resolveOracleRule)(source, opts.ruleOverrides);
|
|
252
|
+
const supported = new Set(rule.supportedTickers(host));
|
|
253
|
+
return { source, rule, tickers: tickers.filter((t) => supported.has(t)) };
|
|
254
|
+
})
|
|
255
|
+
.filter((group) => group.tickers.length > 0);
|
|
256
|
+
// Fail the tx-build (NOT client init, NOT a silent reroute) when NO listed
|
|
257
|
+
// source has a feed for a requested ticker that actually needs a price
|
|
231
258
|
// update. Only a CONSTANT-ONLY ticker is exempt — priced entirely by
|
|
232
259
|
// `constant_rule`, it needs no update leg from any source. A DUAL-FEED ticker
|
|
233
260
|
// (constant AND pyth) still needs its Pyth leg refreshed, so `isConstantTicker`
|
|
234
|
-
// alone must NOT exempt it:
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
261
|
+
// alone must NOT exempt it: with no source able to serve it, feeding an
|
|
262
|
+
// unrefreshed Pyth leg would price it stale (or abort on a missing weighted
|
|
263
|
+
// source). `priceInfoByTicker.has(t)` ⇔ the ticker has a `pyth_rule.feeds`
|
|
264
|
+
// entry, so `constant && !hasPyth` is exactly constant-only. This catches a
|
|
265
|
+
// MISSING feed; a present-but-WRONG feed id is deliberately not validated
|
|
266
|
+
// here (it aborts on-chain at dry-run).
|
|
267
|
+
const covered = new Set(groups.flatMap((group) => group.tickers));
|
|
240
268
|
const isConstantOnly = (t) => host.isConstantTicker(t) && !priceInfoByTicker.has(t);
|
|
241
|
-
const unservable = tickers.filter((t) => !
|
|
269
|
+
const unservable = tickers.filter((t) => !covered.has(t) && !isConstantOnly(t));
|
|
242
270
|
if (unservable.length > 0) {
|
|
243
|
-
|
|
271
|
+
const sources = host.oracleSources.join(", ");
|
|
272
|
+
throw new Error(`oracleSource [${sources}] has no feed configured for ticker(s): ` +
|
|
244
273
|
`${unservable.join(", ")}. Sources are self-contained with no fallback — add ` +
|
|
245
|
-
|
|
274
|
+
`feeds for them under a listed source, or list a source that serves them.`);
|
|
246
275
|
}
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
if (selectedGroup.length > 0 && !opts.feeSource && selectedRule.requiresFeeSource) {
|
|
276
|
+
// Fee-source pre-check, hoisted ABOVE the off-chain fetches and PTB build
|
|
277
|
+
// below. It consults only `rule.requiresFeeSource` — known before any fetch
|
|
278
|
+
// or PTB mutation — so a fee-charging source (Pyth Core) in the fed set with
|
|
279
|
+
// no `feeSource` throws with ZERO wasted network calls and zero PTB
|
|
280
|
+
// commands, rather than waiting for `buildPythPriceUpdateCalls`'s own
|
|
281
|
+
// per-call guard to fire after the off-chain fetches already ran.
|
|
282
|
+
if (!opts.feeSource && groups.some((group) => group.rule.requiresFeeSource)) {
|
|
255
283
|
throw new pyth_ts_1.OracleFeeSourceUnavailableError();
|
|
256
284
|
}
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
285
|
+
// Phase 1 — resolve every group's update data IN PARALLEL: the per-source
|
|
286
|
+
// fetches (Hermes VAA / Lazer POST / quote-center GET) are independent
|
|
287
|
+
// network calls on the tx-build money path, so a multi-source fed set must
|
|
288
|
+
// not pay one RTT per source sequentially. ALL fetches settle before the
|
|
289
|
+
// first PTB mutation below, so a fetch failure never strands moveCalls in a
|
|
290
|
+
// caller-owned tx — and a failure in ANY group fails the whole build (a
|
|
291
|
+
// listed source is load-bearing; silently building without it would starve
|
|
292
|
+
// its weighted tickers on-chain).
|
|
293
|
+
const dataByGroup = await Promise.all(groups.map((group) => resolveGroupUpdateData(host, group, opts.updateDataProvider)));
|
|
294
|
+
// Phase 2 — build each group's update leg sequentially, in list order, so
|
|
295
|
+
// PTB command order stays deterministic. The carry step below is an
|
|
296
|
+
// exhaustive switch over the group's rule kind: a future source whose feed
|
|
297
|
+
// leg needs per-ticker data from its update leg must decide its carry here
|
|
298
|
+
// — falling through silently would starve its weighted tickers on-chain.
|
|
261
299
|
const lazerUpdateByTicker = new Map();
|
|
262
300
|
// Signed batch envelope per waterx-served ticker. Unlike Lazer's shared PTB
|
|
263
301
|
// handle, waterx's verify+feed is bundled into `collect_batch_latest` in the
|
|
264
302
|
// per-ticker feed leg, so its `buildUpdateCalls` emits nothing and the
|
|
265
303
|
// envelope is carried straight from the group's fetched data.
|
|
266
304
|
const waterxEnvelopeByTicker = new Map();
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const handle = (await selectedRule.buildUpdateCalls(tx, host, data, {
|
|
305
|
+
for (const [i, group] of groups.entries()) {
|
|
306
|
+
const data = dataByGroup[i] ?? null;
|
|
307
|
+
const handle = (await group.rule.buildUpdateCalls(tx, host, data, {
|
|
271
308
|
cache: opts.cache,
|
|
272
309
|
feeSource: opts.feeSource,
|
|
273
310
|
})) ?? undefined;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
311
|
+
switch (group.rule.kind) {
|
|
312
|
+
case "pyth_rule":
|
|
313
|
+
// Core's update leg wrote the PriceInfoObjects in place — the feed
|
|
314
|
+
// leg reads them by id (`priceInfoByTicker`), nothing to carry.
|
|
315
|
+
break;
|
|
316
|
+
case "pyth_lazer_rule":
|
|
317
|
+
// Route by the handle's kind discriminant — the tag exists so a
|
|
318
|
+
// non-lazer handle can never be silently fed into
|
|
319
|
+
// pyth_lazer_rule::feed.
|
|
320
|
+
if (handle?.kind === "pyth_lazer_rule") {
|
|
321
|
+
for (const ticker of group.tickers)
|
|
322
|
+
lazerUpdateByTicker.set(ticker, handle.update);
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
case "waterx_rule": {
|
|
326
|
+
// waterx_rule emits no shared handle (verify+feed is bundled into the
|
|
327
|
+
// per-ticker `collect_batch_latest`), so the envelope is carried
|
|
328
|
+
// straight from this group's fetched data to the feed leg below.
|
|
329
|
+
const envelope = (0, waterx_rule_ts_1.waterxEnvelopeOf)(data);
|
|
330
|
+
if (envelope) {
|
|
331
|
+
for (const ticker of group.tickers)
|
|
332
|
+
waterxEnvelopeByTicker.set(ticker, envelope);
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
default: {
|
|
337
|
+
const exhausted = group.rule.kind;
|
|
338
|
+
throw new Error(`refreshOraclePrices: unhandled rule kind '${String(exhausted)}'`);
|
|
289
339
|
}
|
|
290
340
|
}
|
|
291
341
|
}
|
|
@@ -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
|
|
@@ -11,59 +11,3 @@
|
|
|
11
11
|
* mirrors the earlier account-config hoist.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.WATERX_DEFAULTS = exports.LAZER_DEFAULTS = exports.PYTH_DEFAULTS = void 0;
|
|
15
|
-
exports.PYTH_DEFAULTS = {
|
|
16
|
-
MAINNET: {
|
|
17
|
-
state_id: "0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8",
|
|
18
|
-
wormhole_state_id: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
|
|
19
|
-
hermes_endpoint: "https://hermes.pyth.network",
|
|
20
|
-
},
|
|
21
|
-
TESTNET: {
|
|
22
|
-
state_id: "0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c",
|
|
23
|
-
wormhole_state_id: "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
|
|
24
|
-
hermes_endpoint: "https://hermes-beta.pyth.network",
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
// ============================================================================
|
|
28
|
-
// Pyth Lazer — external infra, defaults by network
|
|
29
|
-
// ============================================================================
|
|
30
|
-
/**
|
|
31
|
-
* Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
|
|
32
|
-
* Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
|
|
33
|
-
* operates (not part of the `waterx-config` JSON). A fuller `PYTH_INFRA`
|
|
34
|
-
* restructure is deferred — this stays a minimal map until then.
|
|
35
|
-
*
|
|
36
|
-
* - `endpoint` — Lazer HTTP API base; signed updates come from
|
|
37
|
-
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
38
|
-
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
39
|
-
* Lazer signers), so both networks share the production host.
|
|
40
|
-
* - `verifier_package` — the Sui package carrying
|
|
41
|
-
* `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
|
|
42
|
-
* still the original v1 publish; mainnet is the v2-upgraded package (which
|
|
43
|
-
* still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
|
|
44
|
-
* contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
|
|
45
|
-
*/
|
|
46
|
-
exports.LAZER_DEFAULTS = {
|
|
47
|
-
MAINNET: {
|
|
48
|
-
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
49
|
-
verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
|
|
50
|
-
},
|
|
51
|
-
TESTNET: {
|
|
52
|
-
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
53
|
-
verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
/**
|
|
57
|
-
* WaterX quote-center base URL by network — the first-party TEE-signed price
|
|
58
|
-
* hub `WaterxRule` pulls from (`GET /v1/quotes/update?symbols=…`). Mirrors
|
|
59
|
-
* {@link LAZER_DEFAULTS}: infra WaterX operates, not part of the `waterx-config`
|
|
60
|
-
* JSON. Public read (no auth), so there is no api_key. `endpoint` has no
|
|
61
|
-
* trailing slash — the rule appends the path.
|
|
62
|
-
*
|
|
63
|
-
* These are DEFAULTS, not a hard pin: a consumer overrides them per client via
|
|
64
|
-
* `waterxEndpoint` / `waterxFetch` (resolved onto `client.waterx`).
|
|
65
|
-
*/
|
|
66
|
-
exports.WATERX_DEFAULTS = {
|
|
67
|
-
MAINNET: { endpoint: "https://quote-center.waterx.app" },
|
|
68
|
-
TESTNET: { endpoint: "https://quote-center-staging.waterx.app" },
|
|
69
|
-
};
|
|
@@ -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,19 @@
|
|
|
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 { ORACLE_SOURCES } from "./price-update-rule.ts";
|
|
25
|
+
export { isOracleSource, parseOracleSourceList } from "./source-list.ts";
|
|
26
|
+
export { resolveOracleReadPlan, resolveHermesReadEndpoint } from "./read-plane.ts";
|
|
27
|
+
export type { OracleReadPlan } from "./read-plane.ts";
|
|
24
28
|
export { PythCoreRule } from "./rules/pyth-core-rule.ts";
|
|
25
29
|
export type { PythCoreUpdatePayload } from "./rules/pyth-core-rule.ts";
|
|
26
30
|
export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.ts";
|
|
27
31
|
export type { PythLazerUpdatePayload } from "./rules/pyth-lazer-rule.ts";
|
|
28
|
-
export { WaterxRule, parseSignedEnvelope } from "./rules/waterx-rule.ts";
|
|
32
|
+
export { WaterxRule, parseSignedEnvelope, BATCH_PRICE_INTENT, WATERX_INFRA, waterxQuoteCenterEndpoint, waterxEnvelopeOf, } from "./rules/waterx-rule.ts";
|
|
29
33
|
export type { WaterxUpdatePayload, WaterxSignedEnvelope, WaterxBatchItem, } from "./rules/waterx-rule.ts";
|
|
30
|
-
export { OracleSourceNotImplementedError } from "./rule-registry.ts";
|
|
34
|
+
export { OracleSourceNotImplementedError, resolveOracleRule } from "./rule-registry.ts";
|
|
31
35
|
export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.ts";
|
|
32
36
|
export { openPythSponsorFund, reimbursePythSponsor } from "./rules/sponsor.ts";
|