@waterx/sdk 3.1.0 → 4.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/README.md +53 -7
- package/dist/cjs/src/oracle/aggregate.d.ts +98 -17
- package/dist/cjs/src/oracle/aggregate.js +191 -21
- package/dist/cjs/src/oracle/config.d.ts +103 -0
- package/dist/cjs/src/oracle/config.js +64 -1
- package/dist/cjs/src/oracle/host.d.ts +13 -0
- package/dist/cjs/src/oracle/index.d.ts +19 -5
- package/dist/cjs/src/oracle/index.js +40 -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 +80 -11
- package/dist/cjs/src/oracle/pyth.js +84 -17
- package/dist/cjs/src/oracle/rule-registry.d.ts +37 -0
- package/dist/cjs/src/oracle/rule-registry.js +61 -0
- package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +15 -0
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +84 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +194 -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 +85 -0
- package/dist/cjs/src/oracle/update-fetch.js +228 -0
- package/dist/cjs/src/perp/client.d.ts +22 -1
- package/dist/cjs/src/perp/client.js +11 -2
- package/dist/cjs/src/perp/config.d.ts +15 -20
- package/dist/cjs/src/perp/config.js +86 -30
- package/dist/cjs/src/perp/index.d.ts +4 -3
- package/dist/cjs/src/perp/index.js +8 -4
- 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/prediction/config.d.ts +5 -15
- package/dist/cjs/src/prediction/config.js +4 -12
- package/dist/cjs/src/prediction/fetch.d.ts +6 -1
- package/dist/cjs/src/prediction/fetch.js +64 -0
- package/dist/cjs/src/prediction/index.d.ts +2 -2
- package/dist/cjs/src/prediction/index.js +7 -4
- package/dist/cjs/src/prediction/types.d.ts +19 -0
- package/dist/cjs/src/unified-client.d.ts +26 -4
- package/dist/cjs/src/unified-client.js +4 -2
- package/dist/src/oracle/aggregate.d.ts +98 -17
- package/dist/src/oracle/aggregate.js +192 -22
- package/dist/src/oracle/config.d.ts +103 -0
- package/dist/src/oracle/config.js +63 -0
- package/dist/src/oracle/host.d.ts +13 -0
- package/dist/src/oracle/index.d.ts +19 -5
- package/dist/src/oracle/index.js +34 -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 +80 -11
- package/dist/src/oracle/pyth.js +82 -16
- package/dist/src/oracle/rule-registry.d.ts +37 -0
- package/dist/src/oracle/rule-registry.js +56 -0
- package/dist/src/oracle/rules/pyth-core-rule.d.ts +15 -0
- package/dist/src/oracle/rules/pyth-core-rule.js +81 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +189 -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 +85 -0
- package/dist/src/oracle/update-fetch.js +223 -0
- package/dist/src/perp/client.d.ts +22 -1
- package/dist/src/perp/client.js +12 -3
- package/dist/src/perp/config.d.ts +15 -20
- package/dist/src/perp/config.js +85 -29
- package/dist/src/perp/index.d.ts +4 -3
- package/dist/src/perp/index.js +2 -2
- 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/prediction/config.d.ts +5 -15
- package/dist/src/prediction/config.js +4 -11
- package/dist/src/prediction/fetch.d.ts +6 -1
- package/dist/src/prediction/fetch.js +60 -0
- package/dist/src/prediction/index.d.ts +2 -2
- package/dist/src/prediction/index.js +2 -2
- package/dist/src/prediction/types.d.ts +19 -0
- package/dist/src/unified-client.d.ts +26 -4
- package/dist/src/unified-client.js +4 -2
- package/package.json +1 -1
|
@@ -29,24 +29,93 @@ export declare class PythCache {
|
|
|
29
29
|
priceTableInfo?: PriceTableInfo;
|
|
30
30
|
priceFeedObjectIdCache: Map<string, string | undefined>;
|
|
31
31
|
}
|
|
32
|
-
export declare function fetchPriceFeedsUpdateData(endpoint: string, priceIds: string[]
|
|
32
|
+
export declare function fetchPriceFeedsUpdateData(endpoint: string, priceIds: string[], opts?: {
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
fetch?: {
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
retries?: number;
|
|
37
|
+
};
|
|
38
|
+
}): Promise<Uint8Array[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Resolved source for the Pyth Core on-chain update fee. Deliberately a
|
|
41
|
+
* closed two-variant union, not a `{ sponsorFund?, allowGasFee? }` pair — a
|
|
42
|
+
* caller can no longer construct the "both supplied" or "neither supplied
|
|
43
|
+
* but some other truthy flag" shapes that used to require a priority rule to
|
|
44
|
+
* disambiguate.
|
|
45
|
+
*
|
|
46
|
+
* Resolved exactly ONCE, at the edges (`wrapRequestAndExecute` and the WLP
|
|
47
|
+
* builders' equivalent in `perp/tx-builders/`) from config presence
|
|
48
|
+
* (`pyth_sponsor_rule` deployed → open a fund → `'sponsor'`) and the
|
|
49
|
+
* caller's ergonomic `allowGasFee` opt-in (→ `'gas'`), then threaded
|
|
50
|
+
* verbatim through `refreshOraclePrices` → `BuildUpdateOpts` →
|
|
51
|
+
* `PythCoreRule` → {@link buildPythPriceUpdateCalls}. The sponsor-beats-gas
|
|
52
|
+
* priority from the old two-flag design is now structural — whichever edge
|
|
53
|
+
* resolves this value decides once; no downstream layer re-derives or
|
|
54
|
+
* re-documents a priority because none of them ever see more than one
|
|
55
|
+
* candidate source.
|
|
56
|
+
*/
|
|
57
|
+
export type OracleFeeSource = {
|
|
58
|
+
readonly kind: "sponsor";
|
|
59
|
+
readonly fund: TransactionArgument;
|
|
60
|
+
readonly packageId: string;
|
|
61
|
+
} | {
|
|
62
|
+
readonly kind: "gas";
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Thrown when no {@link OracleFeeSource} is available for the Pyth update fee
|
|
66
|
+
* — from `buildPythPriceUpdateCalls`'s own per-call guard, or `aggregate.ts`'s
|
|
67
|
+
* hoisted `refreshOraclePrices` pre-check (see its docblock). `instanceof`-able
|
|
68
|
+
* (mirrors `FetchPolicyError` in `update-fetch.ts`) so a consumer — e.g. a BE
|
|
69
|
+
* integration wiring its own `allowGasFee` decision — can branch on the error
|
|
70
|
+
* type directly instead of string-matching `error.message`.
|
|
71
|
+
*/
|
|
72
|
+
export declare class OracleFeeSourceUnavailableError extends Error {
|
|
73
|
+
constructor();
|
|
74
|
+
}
|
|
33
75
|
/**
|
|
34
76
|
* Append the on-chain Pyth update PTB block. Returns `PriceInfoObject` IDs
|
|
35
77
|
* (one per `feedIds`, same order). After this you can feed `pyth_rule` per
|
|
36
78
|
* ticker against the matching `PriceInfoObject` (see `rules/pyth-rule.ts`).
|
|
37
79
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
80
|
+
* `opts.feeSource` is resolved BEFORE any PTB mutation and is never silently
|
|
81
|
+
* defaulted — this function trusts whatever single {@link OracleFeeSource}
|
|
82
|
+
* it's handed, it does not choose between competing candidates:
|
|
83
|
+
* - `{ kind: 'sponsor' }` → the per-feed update fee is drawn from the
|
|
84
|
+
* sponsor pool (`pyth_sponsor_rule::split`) instead of `tx.gas`. Opening
|
|
85
|
+
* and reimbursing that fund is the caller's job (`rules/sponsor.ts` /
|
|
86
|
+
* `wrapRequestAndExecute`, which opens it whenever the client's config
|
|
87
|
+
* has `pyth_sponsor_rule` deployed) — this function only draws a fee
|
|
88
|
+
* coin from the already-open `fund` hot potato.
|
|
89
|
+
* - `{ kind: 'gas' }` → the fee is drawn from `tx.gas` via `tx.splitCoins`.
|
|
90
|
+
* Only safe in a non-sponsored context — Enoki-sponsored transactions
|
|
91
|
+
* reject any `tx.gas` draw.
|
|
92
|
+
* - `undefined` → throws `OracleFeeSourceUnavailable` instead of silently
|
|
93
|
+
* drawing from `tx.gas` (the old default), which broke under Enoki and,
|
|
94
|
+
* worse, could fail ON-CHAIN when the market's `request_checklist`
|
|
95
|
+
* requires the `PythSponsorRule` witness that only a real sponsor fund
|
|
96
|
+
* attaches.
|
|
97
|
+
*
|
|
98
|
+
* This function's own check runs AFTER `updates`/`feedIds` are already in
|
|
99
|
+
* hand, so for `updatePythPrices` (which fetches from Hermes, then calls
|
|
100
|
+
* straight into this function) the off-chain fetch has already completed by
|
|
101
|
+
* the time this throws — a wasted network call, never a stray PTB command.
|
|
102
|
+
* `refreshOraclePrices` avoids that waste entirely: it hoists an EQUIVALENT
|
|
103
|
+
* check ABOVE its off-chain fetch AND its per-group build loop (see its
|
|
104
|
+
* docblock in `aggregate.ts`), keyed on `PriceUpdateRule.requiresFeeSource`
|
|
105
|
+
* rather than waiting for a specific rule's fetch to complete — so for that
|
|
106
|
+
* route neither the network call NOR any PTB command happens before the
|
|
107
|
+
* throw. This function's own (later, per-call) guard alone could not
|
|
108
|
+
* provide that "before any group builds" guarantee in a mixed shape (e.g. a
|
|
109
|
+
* fee-free Lazer group ordered ahead of a Pyth Core fallback group in the
|
|
110
|
+
* same PTB) — `refreshOraclePrices`'s pre-check is what closes it.
|
|
42
111
|
*/
|
|
43
|
-
export declare function buildPythPriceUpdateCalls(tx: Transaction, host: OracleHost, updates: Uint8Array[], feedIds: string[],
|
|
44
|
-
|
|
45
|
-
|
|
112
|
+
export declare function buildPythPriceUpdateCalls(tx: Transaction, host: OracleHost, updates: Uint8Array[], feedIds: string[], opts?: {
|
|
113
|
+
cache?: PythCache;
|
|
114
|
+
feeSource?: OracleFeeSource;
|
|
46
115
|
}): Promise<string[]>;
|
|
47
116
|
/** All-in-one: fetch from Hermes, append update calls. Returns PriceInfoObject IDs. */
|
|
48
|
-
export declare function updatePythPrices(tx: Transaction, host: OracleHost, feedIds: string[],
|
|
49
|
-
|
|
50
|
-
|
|
117
|
+
export declare function updatePythPrices(tx: Transaction, host: OracleHost, feedIds: string[], opts?: {
|
|
118
|
+
cache?: PythCache;
|
|
119
|
+
feeSource?: OracleFeeSource;
|
|
51
120
|
}): Promise<string[]>;
|
|
52
121
|
export {};
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
* 4. hot_potato_vector::destroy
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.PythCache = void 0;
|
|
18
|
+
exports.OracleFeeSourceUnavailableError = exports.PythCache = void 0;
|
|
19
19
|
exports.fetchPriceFeedsUpdateData = fetchPriceFeedsUpdateData;
|
|
20
20
|
exports.buildPythPriceUpdateCalls = buildPythPriceUpdateCalls;
|
|
21
21
|
exports.updatePythPrices = updatePythPrices;
|
|
22
22
|
const bcs_1 = require("@mysten/bcs");
|
|
23
23
|
const bcs_2 = require("@mysten/sui/bcs");
|
|
24
|
+
const update_fetch_ts_1 = require("./update-fetch.js");
|
|
24
25
|
class PythCache {
|
|
25
26
|
pythStateInfo;
|
|
26
27
|
wormholePackageId;
|
|
@@ -31,12 +32,28 @@ exports.PythCache = PythCache;
|
|
|
31
32
|
// ============================================================================
|
|
32
33
|
// Hermes REST
|
|
33
34
|
// ============================================================================
|
|
34
|
-
async function fetchPriceFeedsUpdateData(endpoint, priceIds) {
|
|
35
|
+
async function fetchPriceFeedsUpdateData(endpoint, priceIds, opts) {
|
|
35
36
|
if (priceIds.length === 0)
|
|
36
37
|
return [];
|
|
37
38
|
const url = new URL("/v2/updates/price/latest", endpoint);
|
|
38
39
|
priceIds.forEach((id) => url.searchParams.append("ids[]", id));
|
|
39
|
-
|
|
40
|
+
let res;
|
|
41
|
+
try {
|
|
42
|
+
res = await (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), {}, { apiKey: opts?.apiKey, ...opts?.fetch });
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// A retryable status (429/5xx) that never recovered surfaces as a
|
|
46
|
+
// FetchPolicyError with `status` set — reformat it into this function's
|
|
47
|
+
// own message shape so callers (and the e2e transient-failure detector,
|
|
48
|
+
// which keys off "Hermes price fetch failed") see the same text whether
|
|
49
|
+
// the failure was retried or not. A network-level exhaustion (no status)
|
|
50
|
+
// has no domain-specific reframing to add — propagate it as-is.
|
|
51
|
+
if (err instanceof update_fetch_ts_1.FetchPolicyError && err.status !== undefined) {
|
|
52
|
+
const body = err.bodySnippet ? ` ${err.bodySnippet}` : "";
|
|
53
|
+
throw new Error(`Hermes price fetch failed: ${err.status}${body} (retries exhausted after ${err.attempts} attempts)`, { cause: err });
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
40
57
|
if (!res.ok)
|
|
41
58
|
throw new Error(`Hermes price fetch failed: ${res.status} ${await res.text()}`);
|
|
42
59
|
const json = (await res.json());
|
|
@@ -144,26 +161,73 @@ function extractVaaBytes(accumulatorMessage) {
|
|
|
144
161
|
const vaaOffset = vaaSizeOffset + 2;
|
|
145
162
|
return accumulatorMessage.subarray(vaaOffset, vaaOffset + vaaSize);
|
|
146
163
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Thrown when no {@link OracleFeeSource} is available for the Pyth update fee
|
|
166
|
+
* — from `buildPythPriceUpdateCalls`'s own per-call guard, or `aggregate.ts`'s
|
|
167
|
+
* hoisted `refreshOraclePrices` pre-check (see its docblock). `instanceof`-able
|
|
168
|
+
* (mirrors `FetchPolicyError` in `update-fetch.ts`) so a consumer — e.g. a BE
|
|
169
|
+
* integration wiring its own `allowGasFee` decision — can branch on the error
|
|
170
|
+
* type directly instead of string-matching `error.message`.
|
|
171
|
+
*/
|
|
172
|
+
class OracleFeeSourceUnavailableError extends Error {
|
|
173
|
+
constructor() {
|
|
174
|
+
super("OracleFeeSourceUnavailable: no fee source available for the Pyth update fee — " +
|
|
175
|
+
"deploy pyth_sponsor_rule to config so a sponsor fund can be opened (see " +
|
|
176
|
+
"openPythSponsorFund / wrapRequestAndExecute), or pass allowGasFee: true to draw " +
|
|
177
|
+
"the fee from tx.gas in a non-sponsored context");
|
|
178
|
+
this.name = "OracleFeeSourceUnavailableError";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.OracleFeeSourceUnavailableError = OracleFeeSourceUnavailableError;
|
|
150
182
|
/**
|
|
151
183
|
* Append the on-chain Pyth update PTB block. Returns `PriceInfoObject` IDs
|
|
152
184
|
* (one per `feedIds`, same order). After this you can feed `pyth_rule` per
|
|
153
185
|
* ticker against the matching `PriceInfoObject` (see `rules/pyth-rule.ts`).
|
|
154
186
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
187
|
+
* `opts.feeSource` is resolved BEFORE any PTB mutation and is never silently
|
|
188
|
+
* defaulted — this function trusts whatever single {@link OracleFeeSource}
|
|
189
|
+
* it's handed, it does not choose between competing candidates:
|
|
190
|
+
* - `{ kind: 'sponsor' }` → the per-feed update fee is drawn from the
|
|
191
|
+
* sponsor pool (`pyth_sponsor_rule::split`) instead of `tx.gas`. Opening
|
|
192
|
+
* and reimbursing that fund is the caller's job (`rules/sponsor.ts` /
|
|
193
|
+
* `wrapRequestAndExecute`, which opens it whenever the client's config
|
|
194
|
+
* has `pyth_sponsor_rule` deployed) — this function only draws a fee
|
|
195
|
+
* coin from the already-open `fund` hot potato.
|
|
196
|
+
* - `{ kind: 'gas' }` → the fee is drawn from `tx.gas` via `tx.splitCoins`.
|
|
197
|
+
* Only safe in a non-sponsored context — Enoki-sponsored transactions
|
|
198
|
+
* reject any `tx.gas` draw.
|
|
199
|
+
* - `undefined` → throws `OracleFeeSourceUnavailable` instead of silently
|
|
200
|
+
* drawing from `tx.gas` (the old default), which broke under Enoki and,
|
|
201
|
+
* worse, could fail ON-CHAIN when the market's `request_checklist`
|
|
202
|
+
* requires the `PythSponsorRule` witness that only a real sponsor fund
|
|
203
|
+
* attaches.
|
|
204
|
+
*
|
|
205
|
+
* This function's own check runs AFTER `updates`/`feedIds` are already in
|
|
206
|
+
* hand, so for `updatePythPrices` (which fetches from Hermes, then calls
|
|
207
|
+
* straight into this function) the off-chain fetch has already completed by
|
|
208
|
+
* the time this throws — a wasted network call, never a stray PTB command.
|
|
209
|
+
* `refreshOraclePrices` avoids that waste entirely: it hoists an EQUIVALENT
|
|
210
|
+
* check ABOVE its off-chain fetch AND its per-group build loop (see its
|
|
211
|
+
* docblock in `aggregate.ts`), keyed on `PriceUpdateRule.requiresFeeSource`
|
|
212
|
+
* rather than waiting for a specific rule's fetch to complete — so for that
|
|
213
|
+
* route neither the network call NOR any PTB command happens before the
|
|
214
|
+
* throw. This function's own (later, per-call) guard alone could not
|
|
215
|
+
* provide that "before any group builds" guarantee in a mixed shape (e.g. a
|
|
216
|
+
* fee-free Lazer group ordered ahead of a Pyth Core fallback group in the
|
|
217
|
+
* same PTB) — `refreshOraclePrices`'s pre-check is what closes it.
|
|
159
218
|
*/
|
|
160
|
-
async function buildPythPriceUpdateCalls(tx, host, updates, feedIds,
|
|
219
|
+
async function buildPythPriceUpdateCalls(tx, host, updates, feedIds, opts) {
|
|
161
220
|
if (updates.length === 0) {
|
|
162
221
|
throw new Error("No price update data provided; Hermes returned empty results");
|
|
163
222
|
}
|
|
164
223
|
if (updates.length > 1) {
|
|
165
224
|
throw new Error("Only a single accumulator message is supported per transaction");
|
|
166
225
|
}
|
|
226
|
+
const feeSource = opts?.feeSource;
|
|
227
|
+
if (!feeSource) {
|
|
228
|
+
throw new OracleFeeSourceUnavailableError();
|
|
229
|
+
}
|
|
230
|
+
const cache = opts?.cache;
|
|
167
231
|
const pyth = host.pyth;
|
|
168
232
|
const [stateInfo, wormholePackageId, table] = await Promise.all([
|
|
169
233
|
getPythStateInfo(host.grpcClient, pyth.state_id, cache),
|
|
@@ -195,10 +259,10 @@ async function buildPythPriceUpdateCalls(tx, host, updates, feedIds, cache, spon
|
|
|
195
259
|
if (!priceInfoObjectId) {
|
|
196
260
|
throw new Error(`Pyth feed ${feedIds[i]} not registered on-chain in Pyth state`);
|
|
197
261
|
}
|
|
198
|
-
const feeCoin =
|
|
262
|
+
const feeCoin = feeSource.kind === "sponsor"
|
|
199
263
|
? tx.moveCall({
|
|
200
|
-
target: `${
|
|
201
|
-
arguments: [
|
|
264
|
+
target: `${feeSource.packageId}::pyth_sponsor_rule::split`,
|
|
265
|
+
arguments: [feeSource.fund],
|
|
202
266
|
})[0]
|
|
203
267
|
: tx.splitCoins(tx.gas, [tx.pure.u64(baseUpdateFee)])[0];
|
|
204
268
|
[hotPotato] = tx.moveCall({
|
|
@@ -221,7 +285,10 @@ async function buildPythPriceUpdateCalls(tx, host, updates, feedIds, cache, spon
|
|
|
221
285
|
return priceInfoObjectIds;
|
|
222
286
|
}
|
|
223
287
|
/** All-in-one: fetch from Hermes, append update calls. Returns PriceInfoObject IDs. */
|
|
224
|
-
async function updatePythPrices(tx, host, feedIds,
|
|
225
|
-
const updates = await fetchPriceFeedsUpdateData(host.pyth.hermes_endpoint, feedIds
|
|
226
|
-
|
|
288
|
+
async function updatePythPrices(tx, host, feedIds, opts) {
|
|
289
|
+
const updates = await fetchPriceFeedsUpdateData(host.pyth.hermes_endpoint, feedIds, {
|
|
290
|
+
apiKey: host.pyth.api_key,
|
|
291
|
+
fetch: host.pyth.fetch,
|
|
292
|
+
});
|
|
293
|
+
return buildPythPriceUpdateCalls(tx, host, updates, feedIds, opts);
|
|
227
294
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rule-registry.ts` — maps a client-selected `OracleSource` to its concrete
|
|
3
|
+
* `PriceUpdateRule` implementation. `refreshOraclePrices` (`aggregate.ts`) is
|
|
4
|
+
* the only production caller; this is the one place `OracleSource` values are
|
|
5
|
+
* wired to a rule instance. Selection is driven purely by the value passed in
|
|
6
|
+
* (ultimately `OracleHost.oracleSource`, a client create option) — never by a
|
|
7
|
+
* config JSON `enabled` flag and never by `process.env`.
|
|
8
|
+
*
|
|
9
|
+
* Both sources are registered: `pyth_rule` (`PythCoreRule`, Hermes VAA) and
|
|
10
|
+
* `pyth_lazer_rule` (`PythLazerRule`, Lazer signed updates). Resolving a
|
|
11
|
+
* source with no registered rule throws a clear `OracleSourceNotImplemented`
|
|
12
|
+
* error instead of silently falling back to Pyth Core.
|
|
13
|
+
*/
|
|
14
|
+
import type { OracleSource, PriceUpdateRule } from "./price-update-rule.ts";
|
|
15
|
+
/**
|
|
16
|
+
* Thrown by {@link resolveOracleRule} when `source` has no `PriceUpdateRule`
|
|
17
|
+
* registered in either the production registry or a test's `overrides` map.
|
|
18
|
+
* `instanceof`-able (mirrors `OracleFeeSourceUnavailableError` in `pyth.ts`)
|
|
19
|
+
* so a consumer can branch on the failure type directly instead of
|
|
20
|
+
* string-matching `error.message`.
|
|
21
|
+
*/
|
|
22
|
+
export declare class OracleSourceNotImplementedError extends Error {
|
|
23
|
+
/** The unregistered `OracleSource` that was requested. */
|
|
24
|
+
readonly source: OracleSource;
|
|
25
|
+
constructor(source: OracleSource);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the `PriceUpdateRule` registered for `source`.
|
|
29
|
+
*
|
|
30
|
+
* `overrides` — test-only — layers on top of the production registry so a
|
|
31
|
+
* spec can inject a fake rule (e.g. a stub `pyth_lazer_rule`) without
|
|
32
|
+
* touching {@link DEFAULT_RULES}; production callers never pass it.
|
|
33
|
+
*
|
|
34
|
+
* Throws {@link OracleSourceNotImplementedError} (`OracleSourceNotImplemented:
|
|
35
|
+
* <source>`) when nothing is registered for `source` in either map.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveOracleRule(source: OracleSource, overrides?: Partial<Record<OracleSource, PriceUpdateRule>>): PriceUpdateRule;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `rule-registry.ts` — maps a client-selected `OracleSource` to its concrete
|
|
4
|
+
* `PriceUpdateRule` implementation. `refreshOraclePrices` (`aggregate.ts`) is
|
|
5
|
+
* the only production caller; this is the one place `OracleSource` values are
|
|
6
|
+
* wired to a rule instance. Selection is driven purely by the value passed in
|
|
7
|
+
* (ultimately `OracleHost.oracleSource`, a client create option) — never by a
|
|
8
|
+
* config JSON `enabled` flag and never by `process.env`.
|
|
9
|
+
*
|
|
10
|
+
* Both sources are registered: `pyth_rule` (`PythCoreRule`, Hermes VAA) and
|
|
11
|
+
* `pyth_lazer_rule` (`PythLazerRule`, Lazer signed updates). Resolving a
|
|
12
|
+
* source with no registered rule throws a clear `OracleSourceNotImplemented`
|
|
13
|
+
* error instead of silently falling back to Pyth Core.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.OracleSourceNotImplementedError = void 0;
|
|
17
|
+
exports.resolveOracleRule = resolveOracleRule;
|
|
18
|
+
const pyth_core_rule_ts_1 = require("./rules/pyth-core-rule.js");
|
|
19
|
+
const pyth_lazer_rule_ts_1 = require("./rules/pyth-lazer-rule.js");
|
|
20
|
+
/**
|
|
21
|
+
* Production registry. Frozen — tests inject a fake rule via
|
|
22
|
+
* `resolveOracleRule`'s `overrides` param instead of mutating this.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_RULES = Object.freeze({
|
|
25
|
+
pyth_rule: pyth_core_rule_ts_1.PythCoreRule,
|
|
26
|
+
pyth_lazer_rule: pyth_lazer_rule_ts_1.PythLazerRule,
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Thrown by {@link resolveOracleRule} when `source` has no `PriceUpdateRule`
|
|
30
|
+
* registered in either the production registry or a test's `overrides` map.
|
|
31
|
+
* `instanceof`-able (mirrors `OracleFeeSourceUnavailableError` in `pyth.ts`)
|
|
32
|
+
* so a consumer can branch on the failure type directly instead of
|
|
33
|
+
* string-matching `error.message`.
|
|
34
|
+
*/
|
|
35
|
+
class OracleSourceNotImplementedError extends Error {
|
|
36
|
+
/** The unregistered `OracleSource` that was requested. */
|
|
37
|
+
source;
|
|
38
|
+
constructor(source) {
|
|
39
|
+
super(`OracleSourceNotImplemented: ${source}`);
|
|
40
|
+
this.name = "OracleSourceNotImplementedError";
|
|
41
|
+
this.source = source;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.OracleSourceNotImplementedError = OracleSourceNotImplementedError;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the `PriceUpdateRule` registered for `source`.
|
|
47
|
+
*
|
|
48
|
+
* `overrides` — test-only — layers on top of the production registry so a
|
|
49
|
+
* spec can inject a fake rule (e.g. a stub `pyth_lazer_rule`) without
|
|
50
|
+
* touching {@link DEFAULT_RULES}; production callers never pass it.
|
|
51
|
+
*
|
|
52
|
+
* Throws {@link OracleSourceNotImplementedError} (`OracleSourceNotImplemented:
|
|
53
|
+
* <source>`) when nothing is registered for `source` in either map.
|
|
54
|
+
*/
|
|
55
|
+
function resolveOracleRule(source, overrides) {
|
|
56
|
+
const rule = overrides?.[source] ?? DEFAULT_RULES[source];
|
|
57
|
+
if (!rule) {
|
|
58
|
+
throw new OracleSourceNotImplementedError(source);
|
|
59
|
+
}
|
|
60
|
+
return rule;
|
|
61
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `PythCoreRule` — `PriceUpdateRule` wrapper around the existing Pyth Core
|
|
4
|
+
* (Hermes VAA) source in `../pyth.ts`. Delegates to `fetchPriceFeedsUpdateData`
|
|
5
|
+
* / `buildPythPriceUpdateCalls` verbatim; this file only adapts them to the
|
|
6
|
+
* `PriceUpdateRule` port shape (fetch → build) so a future router can select
|
|
7
|
+
* across rules by `kind`. Mechanical wrap only — no on-chain/off-chain logic
|
|
8
|
+
* changes vs `../pyth.ts` / `./pyth-rule.ts`.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.PythCoreRule = void 0;
|
|
12
|
+
const price_update_rule_ts_1 = require("../price-update-rule.js");
|
|
13
|
+
const pyth_ts_1 = require("../pyth.js");
|
|
14
|
+
/**
|
|
15
|
+
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
16
|
+
* caller before this runs, since a same-shaped payload from a different rule
|
|
17
|
+
* (e.g. a hypothetical Lazer payload also carrying `updates`/`feedIds`) must
|
|
18
|
+
* not silently pass as a Pyth Core VAA block.
|
|
19
|
+
*/
|
|
20
|
+
function isPythCoreUpdatePayloadShape(payload) {
|
|
21
|
+
return (typeof payload === "object" &&
|
|
22
|
+
payload !== null &&
|
|
23
|
+
Array.isArray(payload.updates) &&
|
|
24
|
+
Array.isArray(payload.feedIds));
|
|
25
|
+
}
|
|
26
|
+
exports.PythCoreRule = {
|
|
27
|
+
kind: "pyth_rule",
|
|
28
|
+
// Charges a per-feed `base_update_fee` via `pyth::update_single_price_feed` —
|
|
29
|
+
// see `PriceUpdateRule.requiresFeeSource`.
|
|
30
|
+
requiresFeeSource: true,
|
|
31
|
+
/** Tickers with a `pyth_rule.feeds` entry (mirrors `refreshOraclePrices`'s filter). */
|
|
32
|
+
supportedTickers(host) {
|
|
33
|
+
return Object.keys(host.config.packages.pyth_rule?.feeds ?? {});
|
|
34
|
+
},
|
|
35
|
+
/** Resolves feed ids for `tickers`, then fetches their Hermes accumulator update. */
|
|
36
|
+
async fetchUpdateData(host, tickers) {
|
|
37
|
+
if (tickers.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
const feedIds = tickers.map((ticker) => host.getPythFeed(ticker).feed_id);
|
|
40
|
+
const updates = await (0, pyth_ts_1.fetchPriceFeedsUpdateData)(host.pyth.hermes_endpoint, feedIds, {
|
|
41
|
+
apiKey: host.pyth.api_key,
|
|
42
|
+
fetch: host.pyth.fetch,
|
|
43
|
+
});
|
|
44
|
+
return { kind: "pyth_rule", payload: { updates, feedIds } };
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* Subsets a (typically whole-universe) payload from {@link fetchUpdateData}
|
|
48
|
+
* down to exactly `tickers`. Pyth Core charges a per-feed update fee (one
|
|
49
|
+
* `update_single_price_feed` moveCall per `feedIds` entry — see
|
|
50
|
+
* `buildPythPriceUpdateCalls`), so serving a full all-registry payload for a
|
|
51
|
+
* 2-ticker build would multiply both the fee and the PTB size ~N× — a
|
|
52
|
+
* per-feed subset is valid input by construction. Narrows `feedIds` only:
|
|
53
|
+
* the single combined Hermes accumulator blob in `updates` already covers
|
|
54
|
+
* every packed feed and needs no re-slicing. A ticker with no
|
|
55
|
+
* `pyth_rule.feeds` entry, or whose feed id is not packed in THIS payload's
|
|
56
|
+
* `feedIds`, is a coverage gap → `null` (miss), never a silent partial.
|
|
57
|
+
*/
|
|
58
|
+
narrowUpdateData(host, data, tickers) {
|
|
59
|
+
const payload = (0, price_update_rule_ts_1.assertRuleUpdateData)(data, "pyth_rule", isPythCoreUpdatePayloadShape, "{ updates: Uint8Array[]; feedIds: string[] }");
|
|
60
|
+
if (!payload || tickers.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
const packedFeedIds = new Set(payload.feedIds);
|
|
63
|
+
const feedIds = [];
|
|
64
|
+
for (const ticker of tickers) {
|
|
65
|
+
// Same lookup as `host.getPythFeed(ticker)` minus its throw — an
|
|
66
|
+
// unlisted ticker is a miss here, not an error.
|
|
67
|
+
const feedId = host.config.packages.pyth_rule?.feeds?.[ticker]?.feed_id;
|
|
68
|
+
if (feedId === undefined || !packedFeedIds.has(feedId))
|
|
69
|
+
return null;
|
|
70
|
+
feedIds.push(feedId);
|
|
71
|
+
}
|
|
72
|
+
return { kind: "pyth_rule", payload: { updates: payload.updates, feedIds } };
|
|
73
|
+
},
|
|
74
|
+
/** Appends the wormhole/pyth update PTB block for the payload from {@link fetchUpdateData}. */
|
|
75
|
+
async buildUpdateCalls(tx, host, data, opts) {
|
|
76
|
+
const payload = (0, price_update_rule_ts_1.assertRuleUpdateData)(data, "pyth_rule", isPythCoreUpdatePayloadShape, "{ updates: Uint8Array[]; feedIds: string[] }");
|
|
77
|
+
if (!payload)
|
|
78
|
+
return;
|
|
79
|
+
await (0, pyth_ts_1.buildPythPriceUpdateCalls)(tx, host, payload.updates, payload.feedIds, {
|
|
80
|
+
cache: opts?.cache,
|
|
81
|
+
feeSource: opts?.feeSource,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PythLazerRule` — `PriceUpdateRule` for Pyth Lazer (Pyth Pro) signed
|
|
3
|
+
* updates, plus `feedLazerRule`, the collector-feed leg `aggregateTicker`
|
|
4
|
+
* appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
|
|
5
|
+
* requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
|
|
6
|
+
* via `config.pyth.api_key`), verifies it ONCE on-chain via
|
|
7
|
+
* `pyth_lazer::parse_and_verify_le_ecdsa_update`, and hands the resulting
|
|
8
|
+
* `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
|
|
9
|
+
*/
|
|
10
|
+
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
11
|
+
import type { OracleHost } from "../host.ts";
|
|
12
|
+
import { type PriceUpdateRule } from "../price-update-rule.ts";
|
|
13
|
+
/** `pyth_lazer_rule`'s narrowed `RuleUpdateData.payload` shape. */
|
|
14
|
+
export interface PythLazerUpdatePayload {
|
|
15
|
+
/** One signed `leEcdsa` message carrying every requested feed. */
|
|
16
|
+
readonly update: Uint8Array;
|
|
17
|
+
/** Integer Lazer feed ids the update was requested for (debug/audit trail). */
|
|
18
|
+
readonly feedIds: number[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
|
|
22
|
+
* deployed in config but no `pyth.api_key` is set — the Lazer HTTP API
|
|
23
|
+
* requires a Bearer token and the SDK never reads `process.env` to find one.
|
|
24
|
+
* `instanceof`-able (mirrors `OracleFeeSourceUnavailableError` in `pyth.ts`)
|
|
25
|
+
* so a consumer can branch on the failure type directly instead of
|
|
26
|
+
* string-matching `error.message`.
|
|
27
|
+
*/
|
|
28
|
+
export declare class LazerApiKeyMissingError extends Error {
|
|
29
|
+
constructor();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `pyth_lazer_rule::feed(collector, config, clock, &update)` — contribute the
|
|
33
|
+
* verified Lazer price for `collector.symbol()` to the collector. `update` is
|
|
34
|
+
* the `RuleUpdateHandle` value from {@link PythLazerRule.buildUpdateCalls} in
|
|
35
|
+
* the SAME PTB; one verified update serves every ticker's feed call. On-chain
|
|
36
|
+
* the rule abstains (records `none`) instead of aborting when the symbol is
|
|
37
|
+
* unconfigured, the feed is absent from the update, the value is degenerate,
|
|
38
|
+
* or the Lazer timestamp is stale.
|
|
39
|
+
*/
|
|
40
|
+
export declare function feedLazerRule(tx: Transaction, host: OracleHost, collector: TransactionArgument, update: TransactionArgument): void;
|
|
41
|
+
export declare const PythLazerRule: PriceUpdateRule;
|