@waterx/sdk 4.3.3 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/waterx-sdk-integration/SKILL.md +41 -35
- package/README.md +81 -77
- package/dist/cjs/src/account/config.d.ts +3 -5
- package/dist/cjs/src/account/funding/wormhole.d.ts +1 -2
- package/dist/cjs/src/account/funding/wormhole.js +1 -2
- package/dist/cjs/src/oracle/aggregate.d.ts +45 -61
- package/dist/cjs/src/oracle/aggregate.js +138 -138
- package/dist/cjs/src/oracle/config.d.ts +35 -49
- package/dist/cjs/src/oracle/config.js +1 -1
- package/dist/cjs/src/oracle/host.d.ts +14 -19
- package/dist/cjs/src/oracle/host.js +3 -3
- package/dist/cjs/src/oracle/index.d.ts +25 -16
- package/dist/cjs/src/oracle/index.js +79 -66
- package/dist/cjs/src/oracle/price-update-rule.d.ts +104 -66
- package/dist/cjs/src/oracle/price-update-rule.js +24 -16
- package/dist/cjs/src/oracle/pyth-pro-history.d.ts +36 -0
- package/dist/cjs/src/oracle/pyth-pro-history.js +41 -0
- package/dist/cjs/src/oracle/read-plane.d.ts +35 -53
- package/dist/cjs/src/oracle/read-plane.js +26 -48
- package/dist/cjs/src/oracle/read-prices.d.ts +105 -0
- package/dist/cjs/src/oracle/read-prices.js +162 -0
- package/dist/cjs/src/oracle/rule-registry.d.ts +12 -12
- package/dist/cjs/src/oracle/rule-registry.js +12 -14
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +40 -20
- package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +137 -12
- package/dist/cjs/src/oracle/rules/waterx-rule.js +318 -55
- package/dist/cjs/src/oracle/schedule.d.ts +109 -0
- package/dist/cjs/src/oracle/schedule.js +991 -0
- package/dist/cjs/src/oracle/source-list.d.ts +31 -27
- package/dist/cjs/src/oracle/source-list.js +43 -45
- package/dist/cjs/src/oracle/symbol-catalog.d.ts +54 -0
- package/dist/cjs/src/oracle/symbol-catalog.js +69 -0
- package/dist/cjs/src/oracle/update-fetch.d.ts +11 -15
- package/dist/cjs/src/oracle/update-fetch.js +12 -19
- package/dist/cjs/src/oracle/validate.d.ts +105 -0
- package/dist/cjs/src/oracle/validate.js +185 -0
- package/dist/cjs/src/oracle/weight-coverage.d.ts +79 -0
- package/dist/cjs/src/oracle/weight-coverage.js +173 -0
- package/dist/cjs/src/perp/client.d.ts +35 -52
- package/dist/cjs/src/perp/client.js +44 -17
- package/dist/cjs/src/perp/config-view.d.ts +5 -14
- package/dist/cjs/src/perp/config-view.js +5 -16
- package/dist/cjs/src/perp/config.d.ts +3 -3
- package/dist/cjs/src/perp/config.js +11 -13
- package/dist/cjs/src/perp/index.d.ts +4 -6
- package/dist/cjs/src/perp/index.js +35 -18
- package/dist/cjs/src/perp/tx-builders/common.d.ts +86 -48
- package/dist/cjs/src/perp/tx-builders/common.js +115 -47
- package/dist/cjs/src/perp/tx-builders/wlp.d.ts +3 -14
- package/dist/cjs/src/perp/tx-builders/wlp.js +6 -35
- package/dist/cjs/src/perp/tx-builders.d.ts +6 -5
- package/dist/cjs/src/perp/tx-builders.js +16 -10
- package/dist/cjs/src/unified-client.d.ts +8 -34
- package/dist/cjs/src/unified-client.js +3 -4
- package/dist/cjs/src/utils/config.d.ts +0 -8
- package/dist/cjs/src/utils/config.js +5 -13
- package/dist/src/account/config.d.ts +3 -5
- package/dist/src/account/funding/wormhole.d.ts +1 -2
- package/dist/src/account/funding/wormhole.js +1 -2
- package/dist/src/oracle/aggregate.d.ts +45 -61
- package/dist/src/oracle/aggregate.js +138 -137
- package/dist/src/oracle/config.d.ts +35 -49
- package/dist/src/oracle/config.js +1 -1
- package/dist/src/oracle/host.d.ts +14 -19
- package/dist/src/oracle/host.js +3 -3
- package/dist/src/oracle/index.d.ts +25 -16
- package/dist/src/oracle/index.js +55 -50
- package/dist/src/oracle/price-update-rule.d.ts +104 -66
- package/dist/src/oracle/price-update-rule.js +23 -16
- package/dist/src/oracle/pyth-pro-history.d.ts +36 -0
- package/dist/src/oracle/pyth-pro-history.js +38 -0
- package/dist/src/oracle/read-plane.d.ts +35 -53
- package/dist/src/oracle/read-plane.js +25 -47
- package/dist/src/oracle/read-prices.d.ts +105 -0
- package/dist/src/oracle/read-prices.js +156 -0
- package/dist/src/oracle/rule-registry.d.ts +12 -12
- package/dist/src/oracle/rule-registry.js +12 -14
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
- package/dist/src/oracle/rules/pyth-lazer-rule.js +39 -20
- package/dist/src/oracle/rules/waterx-rule.d.ts +137 -12
- package/dist/src/oracle/rules/waterx-rule.js +314 -56
- package/dist/src/oracle/schedule.d.ts +109 -0
- package/dist/src/oracle/schedule.js +985 -0
- package/dist/src/oracle/source-list.d.ts +31 -27
- package/dist/src/oracle/source-list.js +42 -43
- package/dist/src/oracle/symbol-catalog.d.ts +54 -0
- package/dist/src/oracle/symbol-catalog.js +65 -0
- package/dist/src/oracle/update-fetch.d.ts +11 -15
- package/dist/src/oracle/update-fetch.js +12 -18
- package/dist/src/oracle/validate.d.ts +105 -0
- package/dist/src/oracle/validate.js +177 -0
- package/dist/src/oracle/weight-coverage.d.ts +79 -0
- package/dist/src/oracle/weight-coverage.js +166 -0
- package/dist/src/perp/client.d.ts +35 -52
- package/dist/src/perp/client.js +45 -18
- package/dist/src/perp/config-view.d.ts +5 -14
- package/dist/src/perp/config-view.js +5 -16
- package/dist/src/perp/config.d.ts +3 -3
- package/dist/src/perp/config.js +11 -13
- package/dist/src/perp/index.d.ts +4 -6
- package/dist/src/perp/index.js +11 -5
- package/dist/src/perp/tx-builders/common.d.ts +86 -48
- package/dist/src/perp/tx-builders/common.js +114 -48
- package/dist/src/perp/tx-builders/wlp.d.ts +3 -14
- package/dist/src/perp/tx-builders/wlp.js +6 -35
- package/dist/src/perp/tx-builders.d.ts +6 -5
- package/dist/src/perp/tx-builders.js +15 -6
- package/dist/src/unified-client.d.ts +8 -34
- package/dist/src/unified-client.js +3 -4
- package/dist/src/utils/config.d.ts +0 -8
- package/dist/src/utils/config.js +5 -12
- package/package.json +1 -1
- package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
- package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -17
- package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
- package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -113
- package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
- package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -31
- package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
- package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.js +0 -214
- package/dist/cjs/src/oracle/pyth.d.ts +0 -227
- package/dist/cjs/src/oracle/pyth.js +0 -652
- package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +0 -15
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +0 -100
- package/dist/cjs/src/oracle/rules/pyth-rule.d.ts +0 -11
- package/dist/cjs/src/oracle/rules/pyth-rule.js +0 -29
- package/dist/cjs/src/oracle/rules/sponsor.d.ts +0 -32
- package/dist/cjs/src/oracle/rules/sponsor.js +0 -56
- package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
- package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -14
- package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
- package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -73
- package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
- package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -27
- package/dist/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
- package/dist/src/generated/waterx_pyth_rule/pyth_rule.js +0 -170
- package/dist/src/oracle/pyth.d.ts +0 -227
- package/dist/src/oracle/pyth.js +0 -638
- package/dist/src/oracle/rules/pyth-core-rule.d.ts +0 -15
- package/dist/src/oracle/rules/pyth-core-rule.js +0 -97
- package/dist/src/oracle/rules/pyth-rule.d.ts +0 -11
- package/dist/src/oracle/rules/pyth-rule.js +0 -26
- package/dist/src/oracle/rules/sponsor.d.ts +0 -32
- package/dist/src/oracle/rules/sponsor.js +0 -52
|
@@ -1,652 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Pyth as a price *source*: Hermes REST + the on-chain Pyth update PTB.
|
|
4
|
-
*
|
|
5
|
-
* This file knows nothing about oracle *rules* (pyth_rule / supra_rule /
|
|
6
|
-
* constant_rule / sponsor) — it only fetches price update VAAs from Hermes and
|
|
7
|
-
* appends the Pyth on-chain update block, returning the refreshed
|
|
8
|
-
* `PriceInfoObject` IDs. Feeding rules into a collector and aggregating lives in
|
|
9
|
-
* `aggregate.ts`; the rule wrappers live in `rules/`.
|
|
10
|
-
*
|
|
11
|
-
* On-chain a single PTB:
|
|
12
|
-
* 1. wormhole::vaa::parse_and_verify
|
|
13
|
-
* 2. pyth::create_authenticated_price_infos_using_accumulator
|
|
14
|
-
* 3. pyth::update_single_price_feed (one per feed)
|
|
15
|
-
* 4. hot_potato_vector::destroy
|
|
16
|
-
*/
|
|
17
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.OracleFeeSourceUnavailableError = exports.HermesEndpointRejectedAllFeedsError = exports.MISSING_FEED_MEMO_TTL_MS = exports.PythCache = exports.PYTH_PRO_HERMES_ENDPOINT = exports.PYTH_CORE_INFRA = void 0;
|
|
19
|
-
exports.pythCoreHermesEndpoint = pythCoreHermesEndpoint;
|
|
20
|
-
exports.pythProHermesEndpoint = pythProHermesEndpoint;
|
|
21
|
-
exports.endpointSupportedFeedIds = endpointSupportedFeedIds;
|
|
22
|
-
exports.__resetMissingFeedCacheForTest = __resetMissingFeedCacheForTest;
|
|
23
|
-
exports.probeMissingFeeds = probeMissingFeeds;
|
|
24
|
-
exports.fetchPriceFeedsUpdateData = fetchPriceFeedsUpdateData;
|
|
25
|
-
exports.buildPythPriceUpdateCalls = buildPythPriceUpdateCalls;
|
|
26
|
-
exports.updatePythPrices = updatePythPrices;
|
|
27
|
-
const bcs_1 = require("@mysten/bcs");
|
|
28
|
-
const bcs_2 = require("@mysten/sui/bcs");
|
|
29
|
-
const update_fetch_ts_1 = require("./update-fetch.js");
|
|
30
|
-
exports.PYTH_CORE_INFRA = {
|
|
31
|
-
MAINNET: {
|
|
32
|
-
state_id: "0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8",
|
|
33
|
-
wormhole_state_id: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
|
|
34
|
-
hermes_endpoint: "https://hermes.pyth.network",
|
|
35
|
-
},
|
|
36
|
-
TESTNET: {
|
|
37
|
-
state_id: "0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c",
|
|
38
|
-
wormhole_state_id: "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
|
|
39
|
-
hermes_endpoint: "https://hermes-beta.pyth.network",
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* The Core source's Hermes REST base for `network` — for consumers (BE/FE
|
|
44
|
-
* read planes) whose `ORACLE_SOURCE` fed set includes `'pyth_rule'`. A fed
|
|
45
|
-
* set WITHOUT `pyth_rule` reads the keyed Pyth Pro base instead — resolve
|
|
46
|
-
* through `resolveHermesReadEndpoint` (`oracle/read-plane.ts`) rather than
|
|
47
|
-
* branching by hand.
|
|
48
|
-
*/
|
|
49
|
-
function pythCoreHermesEndpoint(network) {
|
|
50
|
-
return exports.PYTH_CORE_INFRA[network].hermes_endpoint;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* The Pyth Pro Hermes-compatible REST base — the documented drop-in
|
|
54
|
-
* replacement for the Core endpoint (`https://pyth.dourolabs.app/hermes/…`),
|
|
55
|
-
* IDENTICAL for every subscriber: only the `Authorization: Bearer` key
|
|
56
|
-
* (`client.pyth.api_key` / the `pythApiKey` create option) is
|
|
57
|
-
* account-specific, so the URL is SDK infra, not deployment config. One base
|
|
58
|
-
* for both networks — Pro has no testnet variant (Pyth feed ids are
|
|
59
|
-
* chain-agnostic; `hermes-beta` is a Core-testnet concern). Consumers with a
|
|
60
|
-
* proxy/self-hosted mirror override it per deployment via
|
|
61
|
-
* `resolveHermesReadEndpoint`'s `override` param.
|
|
62
|
-
*/
|
|
63
|
-
exports.PYTH_PRO_HERMES_ENDPOINT = "https://pyth.dourolabs.app/hermes";
|
|
64
|
-
/** Accessor mirror of {@link pythCoreHermesEndpoint} for the Pro base. */
|
|
65
|
-
function pythProHermesEndpoint() {
|
|
66
|
-
return exports.PYTH_PRO_HERMES_ENDPOINT;
|
|
67
|
-
}
|
|
68
|
-
class PythCache {
|
|
69
|
-
pythStateInfo;
|
|
70
|
-
wormholePackageId;
|
|
71
|
-
priceTableInfo;
|
|
72
|
-
priceFeedObjectIdCache = new Map();
|
|
73
|
-
}
|
|
74
|
-
exports.PythCache = PythCache;
|
|
75
|
-
/**
|
|
76
|
-
* How long a "this endpoint lacks feed X" verdict stays memoized. The verdict
|
|
77
|
-
* is a claim about *someone else's* deployment — a feed can be added to the
|
|
78
|
-
* catalog, an entitlement can be granted, a Pro plan can be upgraded — so it
|
|
79
|
-
* must expire rather than bind the whole process lifetime. Long enough that a
|
|
80
|
-
* genuinely-absent feed costs one discovery per window instead of one per
|
|
81
|
-
* build; short enough that a recovered endpoint self-heals without a restart.
|
|
82
|
-
*/
|
|
83
|
-
exports.MISSING_FEED_MEMO_TTL_MS = 15 * 60_000;
|
|
84
|
-
/**
|
|
85
|
-
* Per-endpoint memo of feed ids this endpoint has rejected as unknown (a 404
|
|
86
|
-
* on `/v2/updates/price/latest`). The Pyth Pro compat endpoint carries a
|
|
87
|
-
* SUBSET of Core's feeds — mainnet `WTIUSD`/`BRENTUSD` (Commodities
|
|
88
|
-
* USOILSPOT/UKOILSPOT) are absent, for example. Pyth 404s the WHOLE batch if
|
|
89
|
-
* ANY id is unknown, and the body naming the bad ids is NOT reliably delivered
|
|
90
|
-
* to `fetch` (Cloudflare returns it to curl but `content-length: 0` to node's
|
|
91
|
-
* undici), so the ids are isolated by catalog read (or bisection) and
|
|
92
|
-
* remembered here — later batches skip them instead of 404ing every time.
|
|
93
|
-
*
|
|
94
|
-
* Entries carry an expiry ({@link MISSING_FEED_MEMO_TTL_MS}); an
|
|
95
|
-
* endpoint-level fault never lands here at all (see {@link
|
|
96
|
-
* HermesEndpointRejectedAllFeedsError}).
|
|
97
|
-
*/
|
|
98
|
-
const missingFeedIdsByEndpoint = new Map();
|
|
99
|
-
/**
|
|
100
|
-
* Thrown when discovery concludes that an endpoint rejects EVERY requested
|
|
101
|
-
* feed id without a catalog vouching for that verdict. `instanceof`-able
|
|
102
|
-
* (mirrors `FetchPolicyError` / {@link OracleFeeSourceUnavailableError}).
|
|
103
|
-
*
|
|
104
|
-
* "All of them are missing" is the signature of an endpoint/credential fault —
|
|
105
|
-
* a wrong base path (the Pyth Pro `/hermes` prefix dropped), a changed route,
|
|
106
|
-
* a revoked or downgraded entitlement — not of N individually-absent feeds.
|
|
107
|
-
* Memoizing it would convert a loud, fixable misconfiguration into a silent
|
|
108
|
-
* permanent one: every id marked missing ⇒ `fetchPriceFeedsUpdateData` returns
|
|
109
|
-
* `[]` ⇒ `buildPythPriceUpdateCalls` throws "Hermes returned empty results",
|
|
110
|
-
* blaming Hermes for having no data, for the rest of the process's life. So
|
|
111
|
-
* this case writes NOTHING to the memo and throws instead; the next call
|
|
112
|
-
* re-probes and recovers on its own once the endpoint does.
|
|
113
|
-
*
|
|
114
|
-
* The message keeps the `Hermes price fetch failed: <status>` prefix on its
|
|
115
|
-
* first line — the documented contract downstream consumers string-match (see
|
|
116
|
-
* {@link fetchPriceFeedsUpdateData} and the e2e transient detector).
|
|
117
|
-
*/
|
|
118
|
-
class HermesEndpointRejectedAllFeedsError extends Error {
|
|
119
|
-
endpoint;
|
|
120
|
-
requestedCount;
|
|
121
|
-
constructor(endpoint, requestedCount, catalogState) {
|
|
122
|
-
super(`Hermes price fetch failed: 404 — endpoint rejected ALL ${requestedCount} requested feed id(s) ` +
|
|
123
|
-
`and its feed catalog was ${catalogState}, so nothing vouches for those feeds being ` +
|
|
124
|
-
`individually absent. Treating this as an endpoint/credential fault (wrong base path — ` +
|
|
125
|
-
`e.g. a dropped Pyth Pro '/hermes' prefix — changed route, or revoked entitlement) rather ` +
|
|
126
|
-
`than caching every feed as missing. Endpoint: ${endpoint}`);
|
|
127
|
-
this.endpoint = endpoint;
|
|
128
|
-
this.requestedCount = requestedCount;
|
|
129
|
-
this.name = "HermesEndpointRejectedAllFeedsError";
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
exports.HermesEndpointRejectedAllFeedsError = HermesEndpointRejectedAllFeedsError;
|
|
133
|
-
/**
|
|
134
|
-
* Memo key for `(endpoint, credential)` — the same trailing-slash trim
|
|
135
|
-
* {@link joinEndpointPath} applies to the request URL (shared helper, so the
|
|
136
|
-
* two can't drift), plus the apiKey: "feed X is missing" is a property of the
|
|
137
|
-
* endpoint AND the credential (Pyth Pro entitlements are per-key), so two
|
|
138
|
-
* clients in one process with different keys must not cross-poison each
|
|
139
|
-
* other's memo. Without the trim, two consumers spelling the same endpoint
|
|
140
|
-
* differently (`…/hermes` vs `…/hermes/`) would fragment the memo and re-run
|
|
141
|
-
* the whole 404 discovery despite one of them having already paid for it.
|
|
142
|
-
*/
|
|
143
|
-
function memoKey(endpoint, apiKey) {
|
|
144
|
-
return `${(0, update_fetch_ts_1.trimTrailingSlashes)(endpoint)}\u0000${apiKey ?? ""}`;
|
|
145
|
-
}
|
|
146
|
-
function recordMissingFeeds(endpoint, feedIds, apiKey) {
|
|
147
|
-
const key = memoKey(endpoint, apiKey);
|
|
148
|
-
let entries = missingFeedIdsByEndpoint.get(key);
|
|
149
|
-
if (!entries) {
|
|
150
|
-
entries = new Map();
|
|
151
|
-
missingFeedIdsByEndpoint.set(key, entries);
|
|
152
|
-
}
|
|
153
|
-
const expiresAt = Date.now() + exports.MISSING_FEED_MEMO_TTL_MS;
|
|
154
|
-
// Key on the bare (0x-stripped, lowercased) form — the SAME normalization the
|
|
155
|
-
// catalog comparison and the in-flight latch use — so a feed can't fragment
|
|
156
|
-
// the memo across `0xAB`/`ab` spellings and slip back through as unfiltered.
|
|
157
|
-
for (const feedId of feedIds)
|
|
158
|
-
entries.set(bareFeedId(feedId), expiresAt);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* The subset of `feedIds` this `endpoint` is known to serve — i.e. minus any
|
|
162
|
-
* discovered to be absent within the last {@link MISSING_FEED_MEMO_TTL_MS}
|
|
163
|
-
* (see {@link fetchPriceFeedsUpdateData}). Callers building a
|
|
164
|
-
* `{ updates, feedIds }` payload use this to keep `feedIds` aligned with the
|
|
165
|
-
* feeds the fetch actually returned data for, so `buildPythPriceUpdateCalls`
|
|
166
|
-
* (one moveCall per feed id) never references a feed the accumulator blob
|
|
167
|
-
* doesn't cover.
|
|
168
|
-
*
|
|
169
|
-
* Expired entries are pruned here rather than on a timer: the memo is only
|
|
170
|
-
* ever consulted through this function, so a lazy sweep is both sufficient and
|
|
171
|
-
* free of a dangling interval in a library.
|
|
172
|
-
*/
|
|
173
|
-
function endpointSupportedFeedIds(endpoint, feedIds, apiKey) {
|
|
174
|
-
const key = memoKey(endpoint, apiKey);
|
|
175
|
-
const entries = missingFeedIdsByEndpoint.get(key);
|
|
176
|
-
if (!entries)
|
|
177
|
-
return feedIds;
|
|
178
|
-
const now = Date.now();
|
|
179
|
-
for (const [id, expiresAt] of entries) {
|
|
180
|
-
if (expiresAt <= now)
|
|
181
|
-
entries.delete(id);
|
|
182
|
-
}
|
|
183
|
-
if (entries.size === 0) {
|
|
184
|
-
missingFeedIdsByEndpoint.delete(key);
|
|
185
|
-
return feedIds;
|
|
186
|
-
}
|
|
187
|
-
return feedIds.filter((id) => !entries.has(bareFeedId(id)));
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Discovery runs currently in flight, keyed by `(endpoint, credential,
|
|
191
|
-
* requested id set)`. A cold memo plus two concurrent tx-builds asking the
|
|
192
|
-
* same question ran two full independent discoveries — duplicate catalog reads
|
|
193
|
-
* (or duplicate bisection probe trees) on the money path, for one answer. The
|
|
194
|
-
* second caller now joins the first run's promise: each still gets its own
|
|
195
|
-
* survivor data, but they share the one discovery behind it.
|
|
196
|
-
*/
|
|
197
|
-
const inFlightDiscoveries = new Map();
|
|
198
|
-
/** Test-only: forget everything learned about which feeds an endpoint lacks. */
|
|
199
|
-
function __resetMissingFeedCacheForTest() {
|
|
200
|
-
missingFeedIdsByEndpoint.clear();
|
|
201
|
-
inFlightDiscoveries.clear();
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* One `GET /v2/price_feeds` — the set of feed ids this endpoint serves for
|
|
205
|
-
* THIS credential (verified against the Pro compat endpoint: WTI absent from
|
|
206
|
-
* the catalog AND 404 on latest-price; BTC present AND 200 —
|
|
207
|
-
* entitlement-filtered per key), normalized to bare lowercase hex.
|
|
208
|
-
*
|
|
209
|
-
* Returns `null` when the catalog is unreadable (non-2xx, unparseable, network
|
|
210
|
-
* error) — the catalog is an optimization, {@link bisectMissingFeeds} remains
|
|
211
|
-
* the ground truth derived from the money-path fetch itself. Note that "read
|
|
212
|
-
* fine, served nothing" (`size === 0`) is NOT the same as unreadable: an empty
|
|
213
|
-
* catalog is an entitlement/route fault in its own right, and the caller
|
|
214
|
-
* treats it as one.
|
|
215
|
-
*/
|
|
216
|
-
async function readEndpointCatalog(endpoint, opts) {
|
|
217
|
-
try {
|
|
218
|
-
const res = await (0, update_fetch_ts_1.fetchWithPolicy)((0, update_fetch_ts_1.joinEndpointPath)(endpoint, "v2/price_feeds").toString(), {}, { apiKey: opts?.apiKey, ...opts?.fetch });
|
|
219
|
-
if (!res.ok) {
|
|
220
|
-
void res.body?.cancel().catch(() => { });
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
const catalog = (await res.json());
|
|
224
|
-
if (!Array.isArray(catalog))
|
|
225
|
-
return null;
|
|
226
|
-
// Catalog ids come WITHOUT the 0x prefix; callers pass either form.
|
|
227
|
-
return new Set(catalog.map((f) => bareFeedId(f.id)));
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
/** Feed ids are compared prefix- and case-insensitively (`0xAB` ≡ `ab`). */
|
|
234
|
-
function bareFeedId(feedId) {
|
|
235
|
-
return feedId.toLowerCase().replace(/^0x/, "");
|
|
236
|
-
}
|
|
237
|
-
async function rawFetch(endpoint, ids, opts) {
|
|
238
|
-
// joinEndpointPath preserves the endpoint's own base path — `new URL`
|
|
239
|
-
// with a leading-slash path would discard it (the Pyth Pro `/hermes`
|
|
240
|
-
// prefix → 404 on EVERY feed); see its doc in update-fetch.ts.
|
|
241
|
-
const url = (0, update_fetch_ts_1.joinEndpointPath)(endpoint, "v2/updates/price/latest");
|
|
242
|
-
ids.forEach((id) => url.searchParams.append("ids[]", id));
|
|
243
|
-
return (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), {}, { apiKey: opts?.apiKey, ...opts?.fetch });
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Bisect `ids` down to the individual ones this endpoint 404s on — the
|
|
247
|
-
* response body isn't readable, so a single id that still 404s IS the unknown
|
|
248
|
-
* one. Data is discarded; this only *reports* (the caller decides whether the
|
|
249
|
-
* verdict is trustworthy enough to memoize). A non-404 (the subset is fine) or
|
|
250
|
-
* a network error stops that branch and contributes nothing.
|
|
251
|
-
*
|
|
252
|
-
* `known404: true` skips the root probe — the caller has already watched this
|
|
253
|
-
* exact batch 404, so re-fetching it would only re-learn a fact in hand (a
|
|
254
|
-
* wasted round trip on the money path during cold discovery). Recursive
|
|
255
|
-
* half-calls always probe: their status is genuinely unknown.
|
|
256
|
-
*/
|
|
257
|
-
async function bisectMissingFeeds(endpoint, ids, opts, known404 = false) {
|
|
258
|
-
if (ids.length === 0)
|
|
259
|
-
return new Set();
|
|
260
|
-
if (!known404) {
|
|
261
|
-
let res;
|
|
262
|
-
try {
|
|
263
|
-
res = await rawFetch(endpoint, ids, opts);
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
return new Set(); // transient/network failure — can't classify
|
|
267
|
-
}
|
|
268
|
-
void res.body?.cancel().catch(() => { });
|
|
269
|
-
if (res.status !== 404)
|
|
270
|
-
return new Set(); // this subset is serveable
|
|
271
|
-
}
|
|
272
|
-
if (ids.length === 1)
|
|
273
|
-
return new Set([ids[0]]);
|
|
274
|
-
const mid = Math.floor(ids.length / 2);
|
|
275
|
-
const [lo, hi] = await Promise.all([
|
|
276
|
-
bisectMissingFeeds(endpoint, ids.slice(0, mid), opts),
|
|
277
|
-
bisectMissingFeeds(endpoint, ids.slice(mid), opts),
|
|
278
|
-
]);
|
|
279
|
-
return new Set([...lo, ...hi]);
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Work out which of `ids` this endpoint lacks and memoize exactly those.
|
|
283
|
-
*
|
|
284
|
-
* Catalog first: one `GET /v2/price_feeds` answers for the whole batch, so a
|
|
285
|
-
* confirmed 404 costs one extra request instead of O(log n) bisection probes.
|
|
286
|
-
* The catalog is also the only *authoritative* source here — it says what the
|
|
287
|
-
* endpoint DOES serve, which is what separates "these two feeds are absent"
|
|
288
|
-
* from "this endpoint is serving nothing to me". Bisection can only observe
|
|
289
|
-
* 404s, and a wrong base path 404s identically to an unknown feed.
|
|
290
|
-
*
|
|
291
|
-
* Hence the guard: a verdict of "every requested id is missing" is only
|
|
292
|
-
* committed when a non-empty catalog vouches for it. Otherwise nothing is
|
|
293
|
-
* written and {@link HermesEndpointRejectedAllFeedsError} is thrown — see its
|
|
294
|
-
* docblock for why silently memoizing that case is worse than failing.
|
|
295
|
-
*
|
|
296
|
-
* Concurrent callers asking the identical question share ONE run (see {@link
|
|
297
|
-
* inFlightDiscoveries}); the latch is released on failure too, so a blip never
|
|
298
|
-
* pins later callers to a stale outcome.
|
|
299
|
-
*
|
|
300
|
-
* @throws HermesEndpointRejectedAllFeedsError on an endpoint-level rejection.
|
|
301
|
-
*/
|
|
302
|
-
function discoverMissingFeeds(endpoint, ids, opts, known404 = false) {
|
|
303
|
-
// Same endpoint + credential + requested set ⇒ same answer; anything else
|
|
304
|
-
// is a different question and runs on its own.
|
|
305
|
-
const key = `${memoKey(endpoint, opts?.apiKey)}${ids.map(bareFeedId).sort().join(",")}`;
|
|
306
|
-
const inFlight = inFlightDiscoveries.get(key);
|
|
307
|
-
if (inFlight)
|
|
308
|
-
return inFlight;
|
|
309
|
-
const run = runDiscovery(endpoint, ids, opts, known404).finally(() => {
|
|
310
|
-
inFlightDiscoveries.delete(key);
|
|
311
|
-
});
|
|
312
|
-
inFlightDiscoveries.set(key, run);
|
|
313
|
-
return run;
|
|
314
|
-
}
|
|
315
|
-
async function runDiscovery(endpoint, ids, opts, known404 = false) {
|
|
316
|
-
if (ids.length === 0)
|
|
317
|
-
return;
|
|
318
|
-
if (!known404) {
|
|
319
|
-
let res;
|
|
320
|
-
try {
|
|
321
|
-
res = await rawFetch(endpoint, ids, opts);
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
return; // transient/network failure — can't classify; leave the memo untouched
|
|
325
|
-
}
|
|
326
|
-
void res.body?.cancel().catch(() => { });
|
|
327
|
-
if (res.status !== 404)
|
|
328
|
-
return; // this batch is serveable — nothing to record
|
|
329
|
-
}
|
|
330
|
-
const catalog = await readEndpointCatalog(endpoint, opts);
|
|
331
|
-
if (catalog !== null && catalog.size === 0) {
|
|
332
|
-
// Read fine, serves nothing: an entitlement/route fault, not N absent feeds.
|
|
333
|
-
throw new HermesEndpointRejectedAllFeedsError(endpoint, ids.length, "empty");
|
|
334
|
-
}
|
|
335
|
-
const missing = catalog !== null
|
|
336
|
-
? new Set(ids.filter((id) => !catalog.has(bareFeedId(id))))
|
|
337
|
-
: await bisectMissingFeeds(endpoint, ids, opts, true);
|
|
338
|
-
if (missing.size === 0)
|
|
339
|
-
return;
|
|
340
|
-
if (catalog === null && missing.size === ids.length) {
|
|
341
|
-
throw new HermesEndpointRejectedAllFeedsError(endpoint, ids.length, "unreadable");
|
|
342
|
-
}
|
|
343
|
-
recordMissingFeeds(endpoint, missing, opts?.apiKey);
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Discovery-only entry for consumers that fetch Hermes THEMSELVES (e.g. a
|
|
347
|
-
* parsed latest-price reader) and just observed a whole-batch 404: resolves
|
|
348
|
-
* which ids the endpoint lacks, memoizes them (see {@link
|
|
349
|
-
* endpointSupportedFeedIds}), fetches NO survivor data. Without this, such a
|
|
350
|
-
* consumer's only way to populate the memo was calling {@link
|
|
351
|
-
* fetchPriceFeedsUpdateData} and discarding its accumulator blob — two full
|
|
352
|
-
* redundant transfers per cold discovery.
|
|
353
|
-
*
|
|
354
|
-
* @throws HermesEndpointRejectedAllFeedsError when the rejection looks
|
|
355
|
-
* endpoint-wide rather than per-feed — the caller's own 404 is then a
|
|
356
|
-
* misconfiguration to surface, not a set of feeds to quietly drop.
|
|
357
|
-
*/
|
|
358
|
-
function probeMissingFeeds(endpoint, ids, opts) {
|
|
359
|
-
return discoverMissingFeeds(endpoint, ids, opts, true);
|
|
360
|
-
}
|
|
361
|
-
async function fetchPriceFeedsUpdateData(endpoint, priceIds, opts) {
|
|
362
|
-
// Skip feeds this endpoint has already told us it doesn't have.
|
|
363
|
-
const ids = endpointSupportedFeedIds(endpoint, priceIds, opts?.apiKey);
|
|
364
|
-
if (ids.length === 0)
|
|
365
|
-
return [];
|
|
366
|
-
let res;
|
|
367
|
-
try {
|
|
368
|
-
res = await rawFetch(endpoint, ids, opts);
|
|
369
|
-
}
|
|
370
|
-
catch (err) {
|
|
371
|
-
(0, update_fetch_ts_1.rethrowExhaustedFetch)(err, (e) => `Hermes price fetch failed: ${e.status}${e.bodySnippet ? ` ${e.bodySnippet}` : ""}`);
|
|
372
|
-
}
|
|
373
|
-
if (!res.ok) {
|
|
374
|
-
// Drain the body ONCE, here. A `Response` body can only be read once, and
|
|
375
|
-
// the 404 branch below finishes with it before the throw is reached — so
|
|
376
|
-
// reading it inside the throw surfaced `TypeError: Body is unusable`
|
|
377
|
-
// instead of this function's documented message whenever a 404 fell
|
|
378
|
-
// through. Reading up front also releases the connection on every path.
|
|
379
|
-
const body = await res.text().catch(() => "");
|
|
380
|
-
// Pyth 404s the ENTIRE batch if ANY id is unknown to the endpoint (a Core
|
|
381
|
-
// feed absent from the Pyth Pro compat endpoint). This is on the money
|
|
382
|
-
// path of every order/position/WLP tx-build, so instead of failing the
|
|
383
|
-
// whole refresh: discover the unknown ids (catalog, else bisection — the
|
|
384
|
-
// body isn't reliably delivered), memoize them, and re-fetch the survivors
|
|
385
|
-
// as ONE clean batch (a single combined accumulator blob, which
|
|
386
|
-
// buildPythPriceUpdateCalls requires). A genuinely-absent ticker just
|
|
387
|
-
// isn't in the payload — its on-chain aggregate abstains/aborts, which is
|
|
388
|
-
// correct. Steady state: once discovered, survivors are filtered up front
|
|
389
|
-
// and this never runs. A rejection that looks endpoint-wide instead of
|
|
390
|
-
// per-feed throws out of here (HermesEndpointRejectedAllFeedsError).
|
|
391
|
-
if (res.status === 404) {
|
|
392
|
-
// known404: this exact batch just 404'd above — skip the root re-probe.
|
|
393
|
-
await discoverMissingFeeds(endpoint, ids, opts, true);
|
|
394
|
-
const survivors = endpointSupportedFeedIds(endpoint, ids, opts?.apiKey);
|
|
395
|
-
if (survivors.length === 0)
|
|
396
|
-
return [];
|
|
397
|
-
// survivors < ids ⇒ we removed the offender(s); re-fetch cleanly. Equal
|
|
398
|
-
// ⇒ nothing was recorded (a 404 that wasn't a missing-feed rejection) —
|
|
399
|
-
// surface it rather than loop.
|
|
400
|
-
if (survivors.length < ids.length) {
|
|
401
|
-
return fetchPriceFeedsUpdateData(endpoint, survivors, opts);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
throw new Error(`Hermes price fetch failed: ${res.status}${body ? ` ${body}` : ""}`);
|
|
405
|
-
}
|
|
406
|
-
const json = (await res.json());
|
|
407
|
-
const data = json.binary?.data;
|
|
408
|
-
if (!Array.isArray(data) || data.length === 0) {
|
|
409
|
-
throw new Error("Hermes returned no binary price data");
|
|
410
|
-
}
|
|
411
|
-
return data.map((hex) => (0, bcs_1.fromHex)(hex));
|
|
412
|
-
}
|
|
413
|
-
// ============================================================================
|
|
414
|
-
// On-chain helpers (cached)
|
|
415
|
-
// ============================================================================
|
|
416
|
-
function pkgFromUpgradeCap(json, objectId) {
|
|
417
|
-
const upgradeCap = json.upgrade_cap ??
|
|
418
|
-
json.fields?.upgrade_cap;
|
|
419
|
-
const nested = upgradeCap?.fields?.package;
|
|
420
|
-
const pkg = typeof upgradeCap?.package === "string"
|
|
421
|
-
? upgradeCap.package
|
|
422
|
-
: typeof nested === "string"
|
|
423
|
-
? nested
|
|
424
|
-
: undefined;
|
|
425
|
-
if (!pkg)
|
|
426
|
-
throw new Error(`Cannot resolve package id for object ${objectId}`);
|
|
427
|
-
return pkg;
|
|
428
|
-
}
|
|
429
|
-
async function getPythStateInfo(client, pythStateId, cache) {
|
|
430
|
-
if (cache?.pythStateInfo)
|
|
431
|
-
return cache.pythStateInfo;
|
|
432
|
-
const result = await client.getObject({ objectId: pythStateId, include: { json: true } });
|
|
433
|
-
const json = result.object?.json;
|
|
434
|
-
if (!json)
|
|
435
|
-
throw new Error("Unable to fetch pyth state");
|
|
436
|
-
const packageId = pkgFromUpgradeCap(json, pythStateId);
|
|
437
|
-
const fields = json.fields;
|
|
438
|
-
const fee = (fields?.base_update_fee ?? json.base_update_fee);
|
|
439
|
-
if (fee === undefined)
|
|
440
|
-
throw new Error("Unable to fetch pyth state base_update_fee");
|
|
441
|
-
const info = { packageId, baseUpdateFee: BigInt(fee) };
|
|
442
|
-
if (cache)
|
|
443
|
-
cache.pythStateInfo = info;
|
|
444
|
-
return info;
|
|
445
|
-
}
|
|
446
|
-
async function getWormholePackageId(client, wormholeStateId, cache) {
|
|
447
|
-
if (cache?.wormholePackageId)
|
|
448
|
-
return cache.wormholePackageId;
|
|
449
|
-
const result = await client.getObject({ objectId: wormholeStateId, include: { json: true } });
|
|
450
|
-
const json = result.object?.json;
|
|
451
|
-
if (!json)
|
|
452
|
-
throw new Error(`Cannot resolve wormhole package id from ${wormholeStateId}`);
|
|
453
|
-
const pkg = pkgFromUpgradeCap(json, wormholeStateId);
|
|
454
|
-
if (cache)
|
|
455
|
-
cache.wormholePackageId = pkg;
|
|
456
|
-
return pkg;
|
|
457
|
-
}
|
|
458
|
-
async function getPriceTableInfo(client, pythStateId, cache) {
|
|
459
|
-
if (cache?.priceTableInfo)
|
|
460
|
-
return cache.priceTableInfo;
|
|
461
|
-
const list = await client.listDynamicFields({ parentId: pythStateId });
|
|
462
|
-
const entry = list.dynamicFields.find((e) => {
|
|
463
|
-
const vt = e.valueType || e.objectType || "";
|
|
464
|
-
return vt.includes("PriceIdentifier") || vt.includes("price_info");
|
|
465
|
-
});
|
|
466
|
-
if (!entry)
|
|
467
|
-
throw new Error("Price table not found in Pyth state dynamic fields");
|
|
468
|
-
const childId = entry.childId ?? entry.objectId;
|
|
469
|
-
const typeStr = entry.valueType || entry.objectType || "";
|
|
470
|
-
if (!childId)
|
|
471
|
-
throw new Error("Price table missing childId");
|
|
472
|
-
const pkgMatch = typeStr.match(/(0x[a-fA-F0-9]+)::price_identifier::PriceIdentifier/);
|
|
473
|
-
if (!pkgMatch)
|
|
474
|
-
throw new Error(`Cannot extract package from price table type: ${typeStr}`);
|
|
475
|
-
const info = { id: childId, fieldType: pkgMatch[1] };
|
|
476
|
-
if (cache)
|
|
477
|
-
cache.priceTableInfo = info;
|
|
478
|
-
return info;
|
|
479
|
-
}
|
|
480
|
-
async function getPriceFeedObjectId(client, table, feedId, cache, pythStateId) {
|
|
481
|
-
const normalized = feedId.replace(/^0x/, "");
|
|
482
|
-
const cacheKey = `${pythStateId}:${normalized}`;
|
|
483
|
-
if (cache?.priceFeedObjectIdCache.has(cacheKey)) {
|
|
484
|
-
return cache.priceFeedObjectIdCache.get(cacheKey);
|
|
485
|
-
}
|
|
486
|
-
const keyBytes = bcs_2.bcs
|
|
487
|
-
.struct("PriceIdentifier", { bytes: bcs_2.bcs.vector(bcs_2.bcs.u8()) })
|
|
488
|
-
.serialize({ bytes: (0, bcs_1.fromHex)(normalized) })
|
|
489
|
-
.toBytes();
|
|
490
|
-
const result = await client.getDynamicField({
|
|
491
|
-
parentId: table.id,
|
|
492
|
-
name: { type: `${table.fieldType}::price_identifier::PriceIdentifier`, bcs: keyBytes },
|
|
493
|
-
});
|
|
494
|
-
const value = result.dynamicField?.value;
|
|
495
|
-
const objectId = !value?.bcs || value.bcs.length < 32 ? undefined : "0x" + (0, bcs_1.toHex)(value.bcs);
|
|
496
|
-
if (cache)
|
|
497
|
-
cache.priceFeedObjectIdCache.set(cacheKey, objectId);
|
|
498
|
-
return objectId;
|
|
499
|
-
}
|
|
500
|
-
// ============================================================================
|
|
501
|
-
// Accumulator parsing
|
|
502
|
-
// ============================================================================
|
|
503
|
-
function extractVaaBytes(accumulatorMessage) {
|
|
504
|
-
const view = new DataView(accumulatorMessage.buffer, accumulatorMessage.byteOffset, accumulatorMessage.byteLength);
|
|
505
|
-
const trailingPayloadSize = view.getUint8(6);
|
|
506
|
-
const vaaSizeOffset = 7 + trailingPayloadSize + 1; // +1 for proof_type
|
|
507
|
-
const vaaSize = view.getUint16(vaaSizeOffset, false);
|
|
508
|
-
const vaaOffset = vaaSizeOffset + 2;
|
|
509
|
-
return accumulatorMessage.subarray(vaaOffset, vaaOffset + vaaSize);
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Thrown when no {@link OracleFeeSource} is available for the Pyth update fee
|
|
513
|
-
* — from `buildPythPriceUpdateCalls`'s own per-call guard, or `aggregate.ts`'s
|
|
514
|
-
* hoisted `refreshOraclePrices` pre-check (see its docblock). `instanceof`-able
|
|
515
|
-
* (mirrors `FetchPolicyError` in `update-fetch.ts`) so a consumer — e.g. a BE
|
|
516
|
-
* integration wiring its own `allowGasFee` decision — can branch on the error
|
|
517
|
-
* type directly instead of string-matching `error.message`.
|
|
518
|
-
*/
|
|
519
|
-
class OracleFeeSourceUnavailableError extends Error {
|
|
520
|
-
constructor() {
|
|
521
|
-
super("OracleFeeSourceUnavailable: no fee source available for the Pyth update fee — " +
|
|
522
|
-
"deploy pyth_sponsor_rule to config so a sponsor fund can be opened (see " +
|
|
523
|
-
"openPythSponsorFund / wrapRequestAndExecute), or pass allowGasFee: true to draw " +
|
|
524
|
-
"the fee from tx.gas in a non-sponsored context");
|
|
525
|
-
this.name = "OracleFeeSourceUnavailableError";
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
exports.OracleFeeSourceUnavailableError = OracleFeeSourceUnavailableError;
|
|
529
|
-
/**
|
|
530
|
-
* Append the on-chain Pyth update PTB block. Returns `PriceInfoObject` IDs
|
|
531
|
-
* (one per `feedIds`, same order). After this you can feed `pyth_rule` per
|
|
532
|
-
* ticker against the matching `PriceInfoObject` (see `rules/pyth-rule.ts`).
|
|
533
|
-
*
|
|
534
|
-
* `opts.feeSource` is resolved BEFORE any PTB mutation and is never silently
|
|
535
|
-
* defaulted — this function trusts whatever single {@link OracleFeeSource}
|
|
536
|
-
* it's handed, it does not choose between competing candidates:
|
|
537
|
-
* - `{ kind: 'sponsor' }` → the per-feed update fee is drawn from the
|
|
538
|
-
* sponsor pool (`pyth_sponsor_rule::split`) instead of `tx.gas`. Opening
|
|
539
|
-
* and reimbursing that fund is the caller's job (`rules/sponsor.ts` /
|
|
540
|
-
* `wrapRequestAndExecute`, which opens it whenever the client's config
|
|
541
|
-
* has `pyth_sponsor_rule` deployed) — this function only draws a fee
|
|
542
|
-
* coin from the already-open `fund` hot potato.
|
|
543
|
-
* - `{ kind: 'gas' }` → the fee is drawn from `tx.gas` via `tx.splitCoins`.
|
|
544
|
-
* Only safe in a non-sponsored context — Enoki-sponsored transactions
|
|
545
|
-
* reject any `tx.gas` draw.
|
|
546
|
-
* - `undefined` → throws `OracleFeeSourceUnavailable` instead of silently
|
|
547
|
-
* drawing from `tx.gas` (the old default), which broke under Enoki and,
|
|
548
|
-
* worse, could fail ON-CHAIN when the market's `request_checklist`
|
|
549
|
-
* requires the `PythSponsorRule` witness that only a real sponsor fund
|
|
550
|
-
* attaches.
|
|
551
|
-
*
|
|
552
|
-
* This function's own check runs AFTER `updates`/`feedIds` are already in
|
|
553
|
-
* hand, so for `updatePythPrices` (which fetches from Hermes, then calls
|
|
554
|
-
* straight into this function) the off-chain fetch has already completed by
|
|
555
|
-
* the time this throws — a wasted network call, never a stray PTB command.
|
|
556
|
-
* `refreshOraclePrices` avoids that waste entirely: it hoists an EQUIVALENT
|
|
557
|
-
* check ABOVE its off-chain fetch AND its per-group build loop (see its
|
|
558
|
-
* docblock in `aggregate.ts`), keyed on `PriceUpdateRule.requiresFeeSource`
|
|
559
|
-
* rather than waiting for a specific rule's fetch to complete — so for that
|
|
560
|
-
* route neither the network call NOR any PTB command happens before the
|
|
561
|
-
* throw. This function's own (later, per-call) guard alone could not
|
|
562
|
-
* provide that "before any group builds" guarantee in a mixed shape (e.g. a
|
|
563
|
-
* fee-free Lazer group ordered ahead of a Pyth Core fallback group in the
|
|
564
|
-
* same PTB) — `refreshOraclePrices`'s pre-check is what closes it.
|
|
565
|
-
*/
|
|
566
|
-
async function buildPythPriceUpdateCalls(tx, host, updates, feedIds, opts) {
|
|
567
|
-
if (updates.length === 0) {
|
|
568
|
-
throw new Error("No price update data provided; Hermes returned empty results");
|
|
569
|
-
}
|
|
570
|
-
if (updates.length > 1) {
|
|
571
|
-
throw new Error("Only a single accumulator message is supported per transaction");
|
|
572
|
-
}
|
|
573
|
-
const feeSource = opts?.feeSource;
|
|
574
|
-
if (!feeSource) {
|
|
575
|
-
throw new OracleFeeSourceUnavailableError();
|
|
576
|
-
}
|
|
577
|
-
const cache = opts?.cache;
|
|
578
|
-
// Core on-chain infra is the `pyth_rule` source's own table, keyed by the
|
|
579
|
-
// host's network — `client.pyth` carries only the caller's credential/policy.
|
|
580
|
-
const pyth = exports.PYTH_CORE_INFRA[host.network];
|
|
581
|
-
const [stateInfo, wormholePackageId, table] = await Promise.all([
|
|
582
|
-
getPythStateInfo(host.grpcClient, pyth.state_id, cache),
|
|
583
|
-
getWormholePackageId(host.grpcClient, pyth.wormhole_state_id, cache),
|
|
584
|
-
getPriceTableInfo(host.grpcClient, pyth.state_id, cache),
|
|
585
|
-
]);
|
|
586
|
-
const priceInfoObjectIds = await Promise.all(feedIds.map((feedId) => getPriceFeedObjectId(host.grpcClient, table, feedId, cache, pyth.state_id)));
|
|
587
|
-
const { packageId: pythPackageId, baseUpdateFee } = stateInfo;
|
|
588
|
-
// 1. Verify VAA
|
|
589
|
-
const vaa = extractVaaBytes(updates[0]);
|
|
590
|
-
const [verifiedVaa] = tx.moveCall({
|
|
591
|
-
target: `${wormholePackageId}::vaa::parse_and_verify`,
|
|
592
|
-
arguments: [tx.object(pyth.wormhole_state_id), tx.pure.vector("u8", vaa), tx.object.clock()],
|
|
593
|
-
});
|
|
594
|
-
// 2. Authenticate price infos
|
|
595
|
-
const [hotPotato0] = tx.moveCall({
|
|
596
|
-
target: `${pythPackageId}::pyth::create_authenticated_price_infos_using_accumulator`,
|
|
597
|
-
arguments: [
|
|
598
|
-
tx.object(pyth.state_id),
|
|
599
|
-
tx.pure.vector("u8", updates[0]),
|
|
600
|
-
verifiedVaa,
|
|
601
|
-
tx.object.clock(),
|
|
602
|
-
],
|
|
603
|
-
});
|
|
604
|
-
// 3. Per-feed update
|
|
605
|
-
let hotPotato = hotPotato0;
|
|
606
|
-
for (let i = 0; i < feedIds.length; i++) {
|
|
607
|
-
const priceInfoObjectId = priceInfoObjectIds[i];
|
|
608
|
-
if (!priceInfoObjectId) {
|
|
609
|
-
throw new Error(`Pyth feed ${feedIds[i]} not registered on-chain in Pyth state`);
|
|
610
|
-
}
|
|
611
|
-
const feeCoin = feeSource.kind === "sponsor"
|
|
612
|
-
? tx.moveCall({
|
|
613
|
-
target: `${feeSource.packageId}::pyth_sponsor_rule::split`,
|
|
614
|
-
arguments: [feeSource.fund],
|
|
615
|
-
})[0]
|
|
616
|
-
: tx.splitCoins(tx.gas, [tx.pure.u64(baseUpdateFee)])[0];
|
|
617
|
-
[hotPotato] = tx.moveCall({
|
|
618
|
-
target: `${pythPackageId}::pyth::update_single_price_feed`,
|
|
619
|
-
arguments: [
|
|
620
|
-
tx.object(pyth.state_id),
|
|
621
|
-
hotPotato,
|
|
622
|
-
tx.object(priceInfoObjectId),
|
|
623
|
-
feeCoin,
|
|
624
|
-
tx.object.clock(),
|
|
625
|
-
],
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
// 4. Destroy hot potato
|
|
629
|
-
tx.moveCall({
|
|
630
|
-
target: `${pythPackageId}::hot_potato_vector::destroy`,
|
|
631
|
-
arguments: [hotPotato],
|
|
632
|
-
typeArguments: [`${pythPackageId}::price_info::PriceInfo`],
|
|
633
|
-
});
|
|
634
|
-
return priceInfoObjectIds;
|
|
635
|
-
}
|
|
636
|
-
/** All-in-one: fetch from Hermes, append update calls. Returns PriceInfoObject IDs. */
|
|
637
|
-
async function updatePythPrices(tx, host, feedIds, opts) {
|
|
638
|
-
// The Core source's own Hermes endpoint (per-network, rule-owned table);
|
|
639
|
-
// credential + fetch policy are the caller-supplied `client.pyth` slice.
|
|
640
|
-
const endpoint = exports.PYTH_CORE_INFRA[host.network].hermes_endpoint;
|
|
641
|
-
const updates = await fetchPriceFeedsUpdateData(endpoint, feedIds, {
|
|
642
|
-
apiKey: host.pyth.api_key,
|
|
643
|
-
fetch: host.pyth.fetch,
|
|
644
|
-
});
|
|
645
|
-
// Align feedIds with the feeds the endpoint actually served — the fetch drops
|
|
646
|
-
// (and memoizes) any it lacks, and `buildPythPriceUpdateCalls` emits one
|
|
647
|
-
// update call per feedId, so a dropped feed would reference a PriceInfoObject
|
|
648
|
-
// the accumulator blob doesn't cover (invalid PTB / on-chain abort). Mirrors
|
|
649
|
-
// `PythCoreRule.fetchUpdateData`.
|
|
650
|
-
const servedFeedIds = endpointSupportedFeedIds(endpoint, feedIds, host.pyth.api_key);
|
|
651
|
-
return buildPythPriceUpdateCalls(tx, host, updates, servedFeedIds, opts);
|
|
652
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `PythCoreRule` — `PriceUpdateRule` wrapper around the existing Pyth Core
|
|
3
|
-
* (Hermes VAA) source in `../pyth.ts`. Delegates to `fetchPriceFeedsUpdateData`
|
|
4
|
-
* / `buildPythPriceUpdateCalls` verbatim; this file only adapts them to the
|
|
5
|
-
* `PriceUpdateRule` port shape (fetch → build) so a future router can select
|
|
6
|
-
* across rules by `kind`. Mechanical wrap only — no on-chain/off-chain logic
|
|
7
|
-
* changes vs `../pyth.ts` / `./pyth-rule.ts`.
|
|
8
|
-
*/
|
|
9
|
-
import { type PriceUpdateRule } from "../price-update-rule.ts";
|
|
10
|
-
/** `pyth_rule`'s narrowed `RuleUpdateData.payload` shape. */
|
|
11
|
-
export interface PythCoreUpdatePayload {
|
|
12
|
-
readonly updates: Uint8Array[];
|
|
13
|
-
readonly feedIds: string[];
|
|
14
|
-
}
|
|
15
|
-
export declare const PythCoreRule: PriceUpdateRule;
|