@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.
Files changed (86) hide show
  1. package/README.md +53 -7
  2. package/dist/cjs/src/oracle/aggregate.d.ts +98 -17
  3. package/dist/cjs/src/oracle/aggregate.js +191 -21
  4. package/dist/cjs/src/oracle/config.d.ts +103 -0
  5. package/dist/cjs/src/oracle/config.js +64 -1
  6. package/dist/cjs/src/oracle/host.d.ts +13 -0
  7. package/dist/cjs/src/oracle/index.d.ts +19 -5
  8. package/dist/cjs/src/oracle/index.js +40 -6
  9. package/dist/cjs/src/oracle/price-update-rule.d.ts +180 -0
  10. package/dist/cjs/src/oracle/price-update-rule.js +56 -0
  11. package/dist/cjs/src/oracle/pyth.d.ts +80 -11
  12. package/dist/cjs/src/oracle/pyth.js +84 -17
  13. package/dist/cjs/src/oracle/rule-registry.d.ts +37 -0
  14. package/dist/cjs/src/oracle/rule-registry.js +61 -0
  15. package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +15 -0
  16. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +84 -0
  17. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
  18. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +194 -0
  19. package/dist/cjs/src/oracle/rules/sponsor.d.ts +11 -7
  20. package/dist/cjs/src/oracle/rules/sponsor.js +11 -7
  21. package/dist/cjs/src/oracle/update-fetch.d.ts +85 -0
  22. package/dist/cjs/src/oracle/update-fetch.js +228 -0
  23. package/dist/cjs/src/perp/client.d.ts +22 -1
  24. package/dist/cjs/src/perp/client.js +11 -2
  25. package/dist/cjs/src/perp/config.d.ts +15 -20
  26. package/dist/cjs/src/perp/config.js +86 -30
  27. package/dist/cjs/src/perp/index.d.ts +4 -3
  28. package/dist/cjs/src/perp/index.js +8 -4
  29. package/dist/cjs/src/perp/tx-builders/common.d.ts +52 -15
  30. package/dist/cjs/src/perp/tx-builders/common.js +39 -6
  31. package/dist/cjs/src/perp/tx-builders/wlp.d.ts +11 -3
  32. package/dist/cjs/src/perp/tx-builders/wlp.js +29 -3
  33. package/dist/cjs/src/perp/tx-builders.d.ts +3 -3
  34. package/dist/cjs/src/perp/tx-builders.js +3 -3
  35. package/dist/cjs/src/prediction/config.d.ts +5 -15
  36. package/dist/cjs/src/prediction/config.js +4 -12
  37. package/dist/cjs/src/prediction/fetch.d.ts +6 -1
  38. package/dist/cjs/src/prediction/fetch.js +64 -0
  39. package/dist/cjs/src/prediction/index.d.ts +2 -2
  40. package/dist/cjs/src/prediction/index.js +7 -4
  41. package/dist/cjs/src/prediction/types.d.ts +19 -0
  42. package/dist/cjs/src/unified-client.d.ts +26 -4
  43. package/dist/cjs/src/unified-client.js +4 -2
  44. package/dist/src/oracle/aggregate.d.ts +98 -17
  45. package/dist/src/oracle/aggregate.js +192 -22
  46. package/dist/src/oracle/config.d.ts +103 -0
  47. package/dist/src/oracle/config.js +63 -0
  48. package/dist/src/oracle/host.d.ts +13 -0
  49. package/dist/src/oracle/index.d.ts +19 -5
  50. package/dist/src/oracle/index.js +34 -6
  51. package/dist/src/oracle/price-update-rule.d.ts +180 -0
  52. package/dist/src/oracle/price-update-rule.js +53 -0
  53. package/dist/src/oracle/pyth.d.ts +80 -11
  54. package/dist/src/oracle/pyth.js +82 -16
  55. package/dist/src/oracle/rule-registry.d.ts +37 -0
  56. package/dist/src/oracle/rule-registry.js +56 -0
  57. package/dist/src/oracle/rules/pyth-core-rule.d.ts +15 -0
  58. package/dist/src/oracle/rules/pyth-core-rule.js +81 -0
  59. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
  60. package/dist/src/oracle/rules/pyth-lazer-rule.js +189 -0
  61. package/dist/src/oracle/rules/sponsor.d.ts +11 -7
  62. package/dist/src/oracle/rules/sponsor.js +11 -7
  63. package/dist/src/oracle/update-fetch.d.ts +85 -0
  64. package/dist/src/oracle/update-fetch.js +223 -0
  65. package/dist/src/perp/client.d.ts +22 -1
  66. package/dist/src/perp/client.js +12 -3
  67. package/dist/src/perp/config.d.ts +15 -20
  68. package/dist/src/perp/config.js +85 -29
  69. package/dist/src/perp/index.d.ts +4 -3
  70. package/dist/src/perp/index.js +2 -2
  71. package/dist/src/perp/tx-builders/common.d.ts +52 -15
  72. package/dist/src/perp/tx-builders/common.js +39 -6
  73. package/dist/src/perp/tx-builders/wlp.d.ts +11 -3
  74. package/dist/src/perp/tx-builders/wlp.js +29 -3
  75. package/dist/src/perp/tx-builders.d.ts +3 -3
  76. package/dist/src/perp/tx-builders.js +3 -3
  77. package/dist/src/prediction/config.d.ts +5 -15
  78. package/dist/src/prediction/config.js +4 -11
  79. package/dist/src/prediction/fetch.d.ts +6 -1
  80. package/dist/src/prediction/fetch.js +60 -0
  81. package/dist/src/prediction/index.d.ts +2 -2
  82. package/dist/src/prediction/index.js +2 -2
  83. package/dist/src/prediction/types.d.ts +19 -0
  84. package/dist/src/unified-client.d.ts +26 -4
  85. package/dist/src/unified-client.js +4 -2
  86. 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[]): Promise<Uint8Array[]>;
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
- * If `sponsorFund` is provided, the per-feed update fee comes from the
39
- * sponsor pool (`pyth_sponsor_rule::split`) instead of `tx.gas`. Opening and
40
- * reimbursing that fund is the sponsor rule's job (`rules/sponsor.ts`); here we
41
- * only draw a fee coin from the already-open `fund` hot potato.
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[], cache?: PythCache, sponsorFund?: {
44
- fund: TransactionArgument;
45
- packageId: string;
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[], cache?: PythCache, sponsorFund?: {
49
- fund: TransactionArgument;
50
- packageId: string;
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
- const res = await fetch(url.toString(), { signal: AbortSignal.timeout(15_000) });
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
- // Pyth update calls
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
- * If `sponsorFund` is provided, the per-feed update fee comes from the
156
- * sponsor pool (`pyth_sponsor_rule::split`) instead of `tx.gas`. Opening and
157
- * reimbursing that fund is the sponsor rule's job (`rules/sponsor.ts`); here we
158
- * only draw a fee coin from the already-open `fund` hot potato.
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, cache, sponsorFund) {
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 = sponsorFund
262
+ const feeCoin = feeSource.kind === "sponsor"
199
263
  ? tx.moveCall({
200
- target: `${sponsorFund.packageId}::pyth_sponsor_rule::split`,
201
- arguments: [sponsorFund.fund],
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, cache, sponsorFund) {
225
- const updates = await fetchPriceFeedsUpdateData(host.pyth.hermes_endpoint, feedIds);
226
- return buildPythPriceUpdateCalls(tx, host, updates, feedIds, cache, sponsorFund);
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;