@waterx/sdk 3.1.1 → 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 +54 -18
- package/dist/cjs/src/account/config.d.ts +0 -16
- package/dist/cjs/src/oracle/aggregate.d.ts +96 -17
- package/dist/cjs/src/oracle/aggregate.js +179 -21
- package/dist/cjs/src/oracle/config.d.ts +85 -2
- package/dist/cjs/src/oracle/config.js +30 -1
- package/dist/cjs/src/oracle/host.d.ts +14 -1
- package/dist/cjs/src/oracle/index.d.ts +19 -5
- package/dist/cjs/src/oracle/index.js +54 -6
- package/dist/cjs/src/oracle/price-update-rule.d.ts +180 -0
- package/dist/cjs/src/oracle/price-update-rule.js +56 -0
- package/dist/cjs/src/oracle/pyth.d.ts +142 -11
- package/dist/cjs/src/oracle/pyth.js +408 -25
- package/dist/cjs/src/oracle/rule-registry.d.ts +41 -0
- package/dist/cjs/src/oracle/rule-registry.js +65 -0
- package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +15 -0
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +99 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +197 -0
- package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -0
- package/dist/cjs/src/oracle/rules/sponsor.d.ts +11 -7
- package/dist/cjs/src/oracle/rules/sponsor.js +11 -7
- package/dist/cjs/src/oracle/update-fetch.d.ts +115 -0
- package/dist/cjs/src/oracle/update-fetch.js +285 -0
- package/dist/cjs/src/perp/client.d.ts +40 -5
- package/dist/cjs/src/perp/client.js +18 -3
- package/dist/cjs/src/perp/config.d.ts +10 -7
- package/dist/cjs/src/perp/config.js +77 -17
- package/dist/cjs/src/perp/index.d.ts +3 -2
- package/dist/cjs/src/perp/index.js +6 -2
- package/dist/cjs/src/perp/tx-builders/common.d.ts +52 -15
- package/dist/cjs/src/perp/tx-builders/common.js +39 -6
- package/dist/cjs/src/perp/tx-builders/wlp.d.ts +11 -3
- package/dist/cjs/src/perp/tx-builders/wlp.js +29 -3
- package/dist/cjs/src/perp/tx-builders.d.ts +3 -3
- package/dist/cjs/src/perp/tx-builders.js +3 -3
- package/dist/cjs/src/unified-client.d.ts +25 -0
- package/dist/cjs/src/unified-client.js +3 -0
- package/dist/src/account/config.d.ts +0 -16
- package/dist/src/oracle/aggregate.d.ts +96 -17
- package/dist/src/oracle/aggregate.js +180 -22
- package/dist/src/oracle/config.d.ts +85 -2
- package/dist/src/oracle/config.js +29 -0
- package/dist/src/oracle/host.d.ts +14 -1
- package/dist/src/oracle/index.d.ts +19 -5
- package/dist/src/oracle/index.js +42 -6
- package/dist/src/oracle/price-update-rule.d.ts +180 -0
- package/dist/src/oracle/price-update-rule.js +53 -0
- package/dist/src/oracle/pyth.d.ts +142 -11
- package/dist/src/oracle/pyth.js +402 -24
- package/dist/src/oracle/rule-registry.d.ts +41 -0
- package/dist/src/oracle/rule-registry.js +60 -0
- package/dist/src/oracle/rules/pyth-core-rule.d.ts +15 -0
- package/dist/src/oracle/rules/pyth-core-rule.js +96 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +192 -0
- package/dist/src/oracle/rules/pyth-rule.js +5 -0
- package/dist/src/oracle/rules/sponsor.d.ts +11 -7
- package/dist/src/oracle/rules/sponsor.js +11 -7
- package/dist/src/oracle/update-fetch.d.ts +115 -0
- package/dist/src/oracle/update-fetch.js +277 -0
- package/dist/src/perp/client.d.ts +40 -5
- package/dist/src/perp/client.js +18 -3
- package/dist/src/perp/config.d.ts +10 -7
- package/dist/src/perp/config.js +77 -17
- package/dist/src/perp/index.d.ts +3 -2
- package/dist/src/perp/index.js +1 -1
- package/dist/src/perp/tx-builders/common.d.ts +52 -15
- package/dist/src/perp/tx-builders/common.js +39 -6
- package/dist/src/perp/tx-builders/wlp.d.ts +11 -3
- package/dist/src/perp/tx-builders/wlp.js +29 -3
- package/dist/src/perp/tx-builders.d.ts +3 -3
- package/dist/src/perp/tx-builders.js +3 -3
- package/dist/src/unified-client.d.ts +25 -0
- package/dist/src/unified-client.js +3 -0
- package/package.json +1 -1
|
@@ -2,38 +2,98 @@
|
|
|
2
2
|
* Oracle aggregation — the orchestrator that composes rules into the shared
|
|
3
3
|
* `Oracle`. This is the ONE file that knows about every rule: it builds a
|
|
4
4
|
* `PriceCollector`, feeds whichever rules a ticker is configured for
|
|
5
|
-
* (Pyth / Supra / Constant), then `aggregate`s.
|
|
5
|
+
* (Pyth / Lazer / Supra / Constant), then `aggregate`s.
|
|
6
6
|
*
|
|
7
7
|
* Per ticker:
|
|
8
8
|
* collector = oracle::new_collector(ticker)
|
|
9
9
|
* [pyth_rule::feed] when the ticker has a pyth_rule.feeds entry
|
|
10
|
+
* [pyth_lazer_rule::feed] when the update leg produced a verified lazer Update
|
|
10
11
|
* [supra_rule::feed] when supra is enabled + wired
|
|
11
12
|
* [constant_rule::feed] when the ticker is a constant ticker
|
|
12
13
|
* oracle::aggregate(oracle, collector)
|
|
13
14
|
*
|
|
14
|
-
* The fed rule set must
|
|
15
|
+
* The fed rule set must cover the on-chain weighted set for the ticker —
|
|
15
16
|
* `aggregator::remove_outliers` aborts `EMissingPriceSource` if a weighted rule
|
|
16
|
-
* is missing from the collector
|
|
17
|
+
* is missing from the collector (an abstaining feed call counts as present;
|
|
18
|
+
* a fed-but-unweighted rule is silently dropped).
|
|
19
|
+
*
|
|
20
|
+
* `refreshOraclePrices` additionally routes the on-chain price *update* leg
|
|
21
|
+
* (the fetch + verify/push step, before any of the above feeding) through the
|
|
22
|
+
* `PriceUpdateRule` selected by `host.oracleSource` — see `rule-registry.ts`.
|
|
17
23
|
*/
|
|
18
24
|
import { aggregate as aggregateCall, newCollector } from "../generated/waterx_oracle/oracle.js";
|
|
19
|
-
import {
|
|
25
|
+
import { OracleFeeSourceUnavailableError } from "./pyth.js";
|
|
26
|
+
import { resolveOracleRule } from "./rule-registry.js";
|
|
20
27
|
import { feedConstantRule } from "./rules/constant-rule.js";
|
|
28
|
+
import { feedLazerRule } from "./rules/pyth-lazer-rule.js";
|
|
21
29
|
import { feedPythRule } from "./rules/pyth-rule.js";
|
|
22
30
|
import { maybeFeedSupra } from "./rules/supra-rule.js";
|
|
31
|
+
/**
|
|
32
|
+
* Resolve one group's off-chain update payload for {@link refreshOraclePrices}:
|
|
33
|
+
* try `provider.get(source, tickers)` first (when a provider is configured),
|
|
34
|
+
* falling back to the group's own live `rule.fetchUpdateData` on a cache miss
|
|
35
|
+
* (`null`) or a throw from the provider — a broken/degraded cache must never
|
|
36
|
+
* break the money path.
|
|
37
|
+
*
|
|
38
|
+
* A cache HIT is treated as a payload for a POSSIBLY-WIDER ticker set (a
|
|
39
|
+
* provider commonly caches one whole-universe payload per source — see
|
|
40
|
+
* {@link UpdateDataProvider}), so it is narrowed to exactly `group.tickers`
|
|
41
|
+
* via `rule.narrowUpdateData` before use. This is load-bearing, not
|
|
42
|
+
* defensive: without it a Pyth Core hit would emit an
|
|
43
|
+
* `update_single_price_feed` — and charge its fee — for every cached feed
|
|
44
|
+
* instead of just this group's, and a payload that cannot cover the group
|
|
45
|
+
* (`narrowUpdateData` → `null`) would never reach the live-fetch fallback.
|
|
46
|
+
* Each rule owns its own subsetting (Core subsets per-feed entries; Lazer's
|
|
47
|
+
* indivisible payload passes whole iff fully covered), so the orchestrator
|
|
48
|
+
* never branches on `kind` here. A hit whose `kind` doesn't match the
|
|
49
|
+
* group's rule is a caller bug (the provider handed back a different rule's
|
|
50
|
+
* payload), so that throws — via `narrowUpdateData`'s own
|
|
51
|
+
* `assertRuleUpdateData` guard — instead of silently falling back.
|
|
52
|
+
*/
|
|
53
|
+
async function resolveGroupUpdateData(host, group, provider) {
|
|
54
|
+
if (provider) {
|
|
55
|
+
let cached = null;
|
|
56
|
+
try {
|
|
57
|
+
cached = await provider.get(group.source, group.tickers);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Provider errors must never break the money path — fall through to
|
|
61
|
+
// the live fetch below exactly as a cache miss (`null`) would.
|
|
62
|
+
}
|
|
63
|
+
if (cached !== null) {
|
|
64
|
+
// Wrong-kind hit throws inside narrowUpdateData (assertRuleUpdateData);
|
|
65
|
+
// a hit that can't cover the group narrows to null → live-fetch below.
|
|
66
|
+
const narrowed = group.rule.narrowUpdateData(host, cached, group.tickers);
|
|
67
|
+
if (narrowed !== null)
|
|
68
|
+
return narrowed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return group.rule.fetchUpdateData(host, group.tickers);
|
|
72
|
+
}
|
|
23
73
|
/**
|
|
24
74
|
* Aggregate one ticker's price into the shared `Oracle`: build a collector, feed
|
|
25
75
|
* every rule the ticker is configured for, then `aggregate`.
|
|
26
76
|
*
|
|
27
77
|
* - **Pyth** — fed when `priceInfoObjectId` is supplied (i.e. the ticker has a
|
|
28
|
-
* `pyth_rule.feeds` entry).
|
|
29
|
-
* `PriceInfoObject`
|
|
30
|
-
*
|
|
31
|
-
*
|
|
78
|
+
* `pyth_rule.feeds` entry). When this PTB's update leg refreshed the
|
|
79
|
+
* `PriceInfoObject` it contributes a fresh price; when it did not (a
|
|
80
|
+
* lazer-routed ticker), the on-chain rule only READS the object and abstains
|
|
81
|
+
* if it is stale — it never aborts — so the call stays mandatory while
|
|
82
|
+
* `pyth_rule` remains in the ticker's on-chain weighted set
|
|
83
|
+
* (`EMissingPriceSource` requires every weighted rule to appear).
|
|
84
|
+
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified
|
|
85
|
+
* `pyth_lazer::update::Update` produced by this PTB's lazer update leg
|
|
86
|
+
* (see `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
|
|
87
|
+
* not (yet) weight `PythLazerRule`, the contribution is silently dropped
|
|
88
|
+
* on-chain — feeding ahead of the weight migration is harmless.
|
|
89
|
+
* - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
|
|
90
|
+
* (abstains on-chain for symbols it has no pair for).
|
|
32
91
|
* - **Constant** — fed when the ticker is a constant ticker
|
|
33
92
|
* ({@link OracleHost.isConstantTicker}).
|
|
34
93
|
*
|
|
35
|
-
* "Dual-feed" (Pyth + Constant) and "constant-only" are not
|
|
36
|
-
* fall out of which rules the ticker is in. Throws if no
|
|
94
|
+
* "Dual-feed" (Pyth + Constant, or Pyth + Lazer) and "constant-only" are not
|
|
95
|
+
* special cases — they fall out of which rules the ticker is in. Throws if no
|
|
96
|
+
* rule applies.
|
|
37
97
|
*/
|
|
38
98
|
export function aggregateTicker(tx, host, args) {
|
|
39
99
|
const oraclePkg = host.config.packages.waterx_oracle.published_at;
|
|
@@ -44,16 +104,22 @@ export function aggregateTicker(tx, host, args) {
|
|
|
44
104
|
let fed = false;
|
|
45
105
|
if (args.priceInfoObjectId) {
|
|
46
106
|
feedPythRule(tx, host, collector, args.priceInfoObjectId);
|
|
107
|
+
fed = true;
|
|
108
|
+
}
|
|
109
|
+
if (args.lazerUpdate !== undefined) {
|
|
110
|
+
feedLazerRule(tx, host, collector, args.lazerUpdate);
|
|
111
|
+
fed = true;
|
|
112
|
+
}
|
|
113
|
+
if (fed) {
|
|
47
114
|
// Supra rides on the same collector when enabled (abstains on-chain otherwise).
|
|
48
115
|
maybeFeedSupra(tx, host, collector);
|
|
49
|
-
fed = true;
|
|
50
116
|
}
|
|
51
117
|
if (host.isConstantTicker(args.ticker)) {
|
|
52
118
|
feedConstantRule(tx, host, collector);
|
|
53
119
|
fed = true;
|
|
54
120
|
}
|
|
55
121
|
if (!fed) {
|
|
56
|
-
throw new Error(`no oracle rule configured for ticker '${args.ticker}' (no pyth feed, not a constant ticker)`);
|
|
122
|
+
throw new Error(`no oracle rule configured for ticker '${args.ticker}' (no pyth feed, no lazer update, not a constant ticker)`);
|
|
57
123
|
}
|
|
58
124
|
aggregateCall({
|
|
59
125
|
package: oraclePkg,
|
|
@@ -89,24 +155,116 @@ export function aggregateTickerWithConstant(tx, host, args) {
|
|
|
89
155
|
/**
|
|
90
156
|
* Refresh multiple tickers in one PTB. For each ticker {@link aggregateTicker}
|
|
91
157
|
* feeds whichever rules it is configured for (Pyth if it has a `pyth_rule.feeds`
|
|
92
|
-
* entry,
|
|
93
|
-
*
|
|
94
|
-
*
|
|
158
|
+
* entry, Lazer if the lazer update leg served it — see below — Supra when
|
|
159
|
+
* enabled, Constant when it's a constant ticker).
|
|
160
|
+
*
|
|
161
|
+
* Before that, the on-chain price *update* leg is routed by `host.oracleSource`
|
|
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.
|
|
172
|
+
*
|
|
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.
|
|
182
|
+
*
|
|
183
|
+
* **Collector-feed leg is rule-aware:** a lazer-served group's
|
|
184
|
+
* `buildUpdateCalls` returns the verified `Update` PTB value
|
|
185
|
+
* ({@link RuleUpdateHandle}), and every ticker in that group is aggregated
|
|
186
|
+
* with `lazerUpdate` set so {@link aggregateTicker} appends
|
|
187
|
+
* `pyth_lazer_rule::feed` against it. A lazer-routed ticker that still has a
|
|
188
|
+
* `pyth_rule.feeds` entry ALSO keeps its `pyth_rule::feed` leg — required
|
|
189
|
+
* on-chain while `pyth_rule` stays in the ticker's weighted set
|
|
190
|
+
* (`aggregator::remove_outliers` aborts `EMissingPriceSource` unless every
|
|
191
|
+
* weighted rule appears in the collector; an abstention counts as
|
|
192
|
+
* appearing), and safe: `pyth_rule::feed` only READS the `PriceInfoObject`
|
|
193
|
+
* this PTB never refreshed and abstains when it is stale rather than
|
|
194
|
+
* aborting. Conversely, a lazer feed call on an aggregator that does not
|
|
195
|
+
* (yet) weight `PythLazerRule` is silently dropped on-chain — so
|
|
196
|
+
* lazer-routing a ticker ahead of its on-chain weight migration prices it
|
|
197
|
+
* from the remaining weighted rules instead of failing.
|
|
95
198
|
*/
|
|
96
199
|
export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
|
|
97
200
|
if (tickers.length === 0)
|
|
98
201
|
return;
|
|
99
|
-
//
|
|
100
|
-
//
|
|
202
|
+
// price_info_object lookup for every ticker with a pyth_rule.feeds entry —
|
|
203
|
+
// needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
|
|
204
|
+
// which rule performed the on-chain update for that ticker.
|
|
101
205
|
const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
|
|
102
206
|
const priceInfoByTicker = new Map();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
207
|
+
pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
|
|
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.
|
|
212
|
+
const selectedRule = resolveOracleRule(host.oracleSource, opts.ruleOverrides);
|
|
213
|
+
const selectedSupported = new Set(selectedRule.supportedTickers(host));
|
|
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.`);
|
|
231
|
+
}
|
|
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) {
|
|
240
|
+
throw new OracleFeeSourceUnavailableError();
|
|
241
|
+
}
|
|
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.
|
|
246
|
+
const lazerUpdateByTicker = new Map();
|
|
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, {
|
|
251
|
+
cache: opts.cache,
|
|
252
|
+
feeSource: opts.feeSource,
|
|
253
|
+
})) ?? undefined;
|
|
254
|
+
// Route by the handle's kind discriminant — the one site the tag exists to
|
|
255
|
+
// protect: a future non-lazer handle (e.g. a WaterxRule value) must never
|
|
256
|
+
// be silently fed into pyth_lazer_rule::feed.
|
|
257
|
+
if (handle?.kind === "pyth_lazer_rule") {
|
|
258
|
+
for (const ticker of selectedGroup)
|
|
259
|
+
lazerUpdateByTicker.set(ticker, handle.update);
|
|
260
|
+
}
|
|
107
261
|
}
|
|
108
262
|
// Aggregate each ticker, feeding whichever rules it is configured for.
|
|
109
263
|
for (const ticker of tickers) {
|
|
110
|
-
aggregateTicker(tx, host, {
|
|
264
|
+
aggregateTicker(tx, host, {
|
|
265
|
+
ticker,
|
|
266
|
+
priceInfoObjectId: priceInfoByTicker.get(ticker),
|
|
267
|
+
lazerUpdate: lazerUpdateByTicker.get(ticker),
|
|
268
|
+
});
|
|
111
269
|
}
|
|
112
270
|
}
|
|
@@ -22,6 +22,24 @@ export interface PythRulePackage extends BasePackageEntry {
|
|
|
22
22
|
export interface PythSponsorRulePackage extends BasePackageEntry {
|
|
23
23
|
pyth_sponsor: string;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* `pyth_lazer_rule` deployment entry — present in the deployed testnet
|
|
27
|
+
* `waterx-config` JSON. Read by `PythLazerRule` (`rules/pyth-lazer-rule.ts`):
|
|
28
|
+
* `feeds` for ticker support + integer feed-id resolution, `state` for the
|
|
29
|
+
* verify call, `published_at`/`config` for the per-ticker feed call.
|
|
30
|
+
*
|
|
31
|
+
* `enabled` mirrors the JSON field verbatim but MUST NOT be read for routing —
|
|
32
|
+
* which rule prices a ticker is decided solely by the client's `oracleSource`
|
|
33
|
+
* create option (see `OracleHost.oracleSource`), never by this flag or any
|
|
34
|
+
* other config value.
|
|
35
|
+
*/
|
|
36
|
+
export interface PythLazerRulePackage extends BasePackageEntry {
|
|
37
|
+
config: string;
|
|
38
|
+
state: string;
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
/** Oracle ticker → integer Pyth Lazer feed id (distinct id scheme from `pyth_rule`'s hex `feed_id`). */
|
|
41
|
+
feeds: Record<string, number>;
|
|
42
|
+
}
|
|
25
43
|
/** Per-ticker `constant_rule` feed entry (mirrors the `pyth_rule.feeds` shape). */
|
|
26
44
|
export interface ConstantFeedEntry {
|
|
27
45
|
/**
|
|
@@ -77,16 +95,83 @@ export interface WaterxOraclePackage extends BasePackageEntry {
|
|
|
77
95
|
export interface OraclePackages {
|
|
78
96
|
pyth_rule: PythRulePackage;
|
|
79
97
|
pyth_sponsor_rule?: PythSponsorRulePackage;
|
|
98
|
+
/** See {@link PythLazerRulePackage} — typed only, not read for routing. */
|
|
99
|
+
pyth_lazer_rule?: PythLazerRulePackage;
|
|
80
100
|
constant_rule?: WaterxConstantRulePackage;
|
|
81
101
|
supra_rule?: SupraRulePackage;
|
|
82
102
|
waterx_oracle: WaterxOraclePackage;
|
|
83
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
|
+
};
|
|
84
127
|
export interface PythInfraConfig {
|
|
85
128
|
state_id: string;
|
|
86
129
|
wormhole_state_id: string;
|
|
87
130
|
hermes_endpoint: string;
|
|
131
|
+
/**
|
|
132
|
+
* Pyth Pro / Lazer access token (`Authorization: Bearer …`) for
|
|
133
|
+
* `PythLazerRule`'s signed-update fetch — Lazer is auth-first, so there is
|
|
134
|
+
* no keyless default. Optional: Pyth-Core-only deployments never need it.
|
|
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 →
|
|
137
|
+
* `LazerApiKeyMissing` is thrown at fetch time. As of the Pyth Pro
|
|
138
|
+
* migration (post-2026-08-18, per
|
|
139
|
+
* https://docs.pyth.network/price-feeds/core/upgrade) this is ALSO required
|
|
140
|
+
* for `pyth_rule`'s Hermes fetch (`fetchPriceFeedsUpdateData`) — see
|
|
141
|
+
* `fetch` below.
|
|
142
|
+
*/
|
|
143
|
+
api_key?: string;
|
|
144
|
+
/**
|
|
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.
|
|
151
|
+
*/
|
|
152
|
+
fetch?: PythFetchPolicy;
|
|
88
153
|
}
|
|
89
154
|
export declare const PYTH_DEFAULTS: Record<Network, PythInfraConfig>;
|
|
155
|
+
/**
|
|
156
|
+
* Pyth Lazer (Pyth Pro) external infra the `PythLazerRule` needs, by network.
|
|
157
|
+
* Mirrors {@link PYTH_DEFAULTS}: per-network constants for infrastructure Pyth
|
|
158
|
+
* operates (not part of the `waterx-config` JSON). A fuller `PYTH_INFRA`
|
|
159
|
+
* restructure is deferred — this stays a minimal map until then.
|
|
160
|
+
*
|
|
161
|
+
* - `endpoint` — Lazer HTTP API base; signed updates come from
|
|
162
|
+
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
163
|
+
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
164
|
+
* Lazer signers), so both networks share the production host.
|
|
165
|
+
* - `verifier_package` — the Sui package carrying
|
|
166
|
+
* `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
|
|
167
|
+
* still the original v1 publish; mainnet is the v2-upgraded package (which
|
|
168
|
+
* still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
|
|
169
|
+
* contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
|
|
170
|
+
*/
|
|
171
|
+
export declare const LAZER_DEFAULTS: Record<Network, {
|
|
172
|
+
endpoint: string;
|
|
173
|
+
verifier_package: string;
|
|
174
|
+
}>;
|
|
90
175
|
/**
|
|
91
176
|
* The narrow config shape the oracle/refresh code needs. `WaterXConfig`
|
|
92
177
|
* (the perp line's full config) is assignable to this, so `PerpClient` satisfies
|
|
@@ -94,6 +179,4 @@ export declare const PYTH_DEFAULTS: Record<Network, PythInfraConfig>;
|
|
|
94
179
|
*/
|
|
95
180
|
export interface OracleConfig extends BaseLineConfig {
|
|
96
181
|
packages: OraclePackages;
|
|
97
|
-
/** Pyth infra override (defaults from {@link PYTH_DEFAULTS}). */
|
|
98
|
-
pyth?: PythInfraConfig;
|
|
99
182
|
}
|
|
@@ -21,3 +21,32 @@ export const PYTH_DEFAULTS = {
|
|
|
21
21
|
hermes_endpoint: "https://hermes-beta.pyth.network",
|
|
22
22
|
},
|
|
23
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
|
+
};
|
|
@@ -8,14 +8,27 @@
|
|
|
8
8
|
* clause, and a future `PredictClient` (or a test double) can too.
|
|
9
9
|
*/
|
|
10
10
|
import type { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
11
|
+
import type { Network } from "../constants.ts";
|
|
11
12
|
import type { OracleConfig, PythInfraConfig } from "./config.ts";
|
|
13
|
+
import type { OracleSource } from "./price-update-rule.ts";
|
|
12
14
|
export interface OracleHost {
|
|
15
|
+
/** Sui network this client targets — selects per-network external-infra defaults (e.g. `LAZER_DEFAULTS`). */
|
|
16
|
+
readonly network: Network;
|
|
13
17
|
/** Oracle slice of the canonical `waterx-config` JSON (rule packages + per-ticker feeds). */
|
|
14
18
|
readonly config: OracleConfig;
|
|
15
|
-
/** External Pyth/Wormhole/Hermes infra (network
|
|
19
|
+
/** External Pyth/Wormhole/Hermes infra — fixed per `(network, generation)`; api_key/fetch layered from create options. */
|
|
16
20
|
readonly pyth: PythInfraConfig;
|
|
17
21
|
/** gRPC client for the on-chain reads the Pyth update path needs. */
|
|
18
22
|
readonly grpcClient: SuiGrpcClient;
|
|
23
|
+
/**
|
|
24
|
+
* Client-selected oracle rule source for `refreshOraclePrices`'s on-chain
|
|
25
|
+
* update leg — resolved at client creation from the `oracleSource` create
|
|
26
|
+
* option (default `'pyth_rule'`). Routing is driven by this value ALONE:
|
|
27
|
+
* never by a config JSON `enabled` flag (e.g. a future `pyth_lazer_rule.enabled`)
|
|
28
|
+
* and never by `process.env` — the SDK never reads it; consumers (BE/FE) wire
|
|
29
|
+
* this option from their own env var.
|
|
30
|
+
*/
|
|
31
|
+
readonly oracleSource: OracleSource;
|
|
19
32
|
/** True when `ticker` is priced by `constant_rule`. */
|
|
20
33
|
isConstantTicker(ticker: string): boolean;
|
|
21
34
|
/** The `supra_rule` config when deployed, enabled, and fully wired; else `undefined`. */
|
|
@@ -2,15 +2,29 @@
|
|
|
2
2
|
* Oracle module — the single source of truth for price freshness.
|
|
3
3
|
*
|
|
4
4
|
* Layering (no cross-imports between siblings except via `aggregate.ts`):
|
|
5
|
-
* - `host.ts`
|
|
6
|
-
* - `
|
|
7
|
-
*
|
|
8
|
-
* - `
|
|
5
|
+
* - `host.ts` — `OracleHost`, the narrow client slice this module reads.
|
|
6
|
+
* - `update-fetch.ts` — `fetchWithPolicy`, the shared retry/timeout/Bearer resilience
|
|
7
|
+
* wrapper every off-chain oracle (and config) fetch goes through.
|
|
8
|
+
* - `pyth.ts` — Pyth as a price source: Hermes REST + on-chain update PTB.
|
|
9
|
+
* - `price-update-rule.ts`— `PriceUpdateRule`, the fetch/build strategy port a rule
|
|
10
|
+
* implements; `rule-registry.ts` + `aggregate.ts` wire
|
|
11
|
+
* routing across rules.
|
|
12
|
+
* - `rules/*` — one file per oracle rule (pyth / supra / constant / sponsor).
|
|
13
|
+
* - `aggregate.ts` — the orchestrator that feeds rules into a collector + aggregates.
|
|
9
14
|
*
|
|
10
15
|
* `pyth.ts` deliberately imports NO rule package — Pyth-the-source and the
|
|
11
16
|
* rules that consume it are separate concerns.
|
|
12
17
|
*/
|
|
13
18
|
export type { OracleHost } from "./host.ts";
|
|
14
|
-
export {
|
|
19
|
+
export { FetchPolicyError, fetchWithPolicy, joinEndpointPath } from "./update-fetch.ts";
|
|
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";
|
|
22
|
+
export type { OracleFeeSource } from "./pyth.ts";
|
|
23
|
+
export type { PriceUpdateRule, PriceUpdateRuleKind, RuleUpdateData, RuleUpdateHandle, BuildUpdateOpts, OracleSource, UpdateDataProvider, } from "./price-update-rule.ts";
|
|
24
|
+
export { PythCoreRule } from "./rules/pyth-core-rule.ts";
|
|
25
|
+
export type { PythCoreUpdatePayload } from "./rules/pyth-core-rule.ts";
|
|
26
|
+
export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.ts";
|
|
27
|
+
export type { PythLazerUpdatePayload } from "./rules/pyth-lazer-rule.ts";
|
|
28
|
+
export { OracleSourceNotImplementedError } from "./rule-registry.ts";
|
|
15
29
|
export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.ts";
|
|
16
30
|
export { openPythSponsorFund, reimbursePythSponsor } from "./rules/sponsor.ts";
|
package/dist/src/oracle/index.js
CHANGED
|
@@ -2,16 +2,52 @@
|
|
|
2
2
|
* Oracle module — the single source of truth for price freshness.
|
|
3
3
|
*
|
|
4
4
|
* Layering (no cross-imports between siblings except via `aggregate.ts`):
|
|
5
|
-
* - `host.ts`
|
|
6
|
-
* - `
|
|
7
|
-
*
|
|
8
|
-
* - `
|
|
5
|
+
* - `host.ts` — `OracleHost`, the narrow client slice this module reads.
|
|
6
|
+
* - `update-fetch.ts` — `fetchWithPolicy`, the shared retry/timeout/Bearer resilience
|
|
7
|
+
* wrapper every off-chain oracle (and config) fetch goes through.
|
|
8
|
+
* - `pyth.ts` — Pyth as a price source: Hermes REST + on-chain update PTB.
|
|
9
|
+
* - `price-update-rule.ts`— `PriceUpdateRule`, the fetch/build strategy port a rule
|
|
10
|
+
* implements; `rule-registry.ts` + `aggregate.ts` wire
|
|
11
|
+
* routing across rules.
|
|
12
|
+
* - `rules/*` — one file per oracle rule (pyth / supra / constant / sponsor).
|
|
13
|
+
* - `aggregate.ts` — the orchestrator that feeds rules into a collector + aggregates.
|
|
9
14
|
*
|
|
10
15
|
* `pyth.ts` deliberately imports NO rule package — Pyth-the-source and the
|
|
11
16
|
* rules that consume it are separate concerns.
|
|
12
17
|
*/
|
|
13
|
-
//
|
|
14
|
-
|
|
18
|
+
// Shared fetch resilience wrapper — `FetchPolicyError` is re-exported (not
|
|
19
|
+
// just the type) so a consumer (e.g. a BE prefetch cache) can `instanceof`
|
|
20
|
+
// it off the failure `fetchPriceFeedsUpdateData` / `PythLazerRule` /
|
|
21
|
+
// `loadConfig` surface, without a deep import of `./update-fetch.ts`.
|
|
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";
|
|
37
|
+
// Pyth Core rule (PriceUpdateRule wrapper over the Pyth source above)
|
|
38
|
+
export { PythCoreRule } from "./rules/pyth-core-rule.js";
|
|
39
|
+
// Pyth Lazer rule (signed-update generation; `feedLazerRule` stays internal to `aggregate.ts`)
|
|
40
|
+
// `LazerApiKeyMissingError` is re-exported (not just the type) for the same
|
|
41
|
+
// `instanceof` reason as `OracleFeeSourceUnavailableError` above.
|
|
42
|
+
export { PythLazerRule, LazerApiKeyMissingError } from "./rules/pyth-lazer-rule.js";
|
|
43
|
+
// `resolveOracleRule` (rule-registry.ts) is NOT re-exported here — it has one
|
|
44
|
+
// production consumer (`refreshOraclePrices` below) and no external caller;
|
|
45
|
+
// per repo convention ("no unused exports") it stays module-internal. Tests
|
|
46
|
+
// import it directly from `./rule-registry.ts`. `OracleSourceNotImplementedError`
|
|
47
|
+
// IS re-exported (the same `instanceof` reason as `OracleFeeSourceUnavailableError`
|
|
48
|
+
// above) — a consumer of `refreshOraclePrices` can catch it without importing
|
|
49
|
+
// `resolveOracleRule` itself.
|
|
50
|
+
export { OracleSourceNotImplementedError } from "./rule-registry.js";
|
|
15
51
|
// Aggregation orchestrator
|
|
16
52
|
export { aggregateTicker, aggregateTickerWithPyth, aggregateTickerWithConstant, refreshOraclePrices, } from "./aggregate.js";
|
|
17
53
|
// Sponsor rule (fund open / reimburse + witness attach)
|