@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
@@ -0,0 +1,81 @@
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 { assertRuleUpdateData, } from "../price-update-rule.js";
10
+ import { buildPythPriceUpdateCalls, fetchPriceFeedsUpdateData } from "../pyth.js";
11
+ /**
12
+ * Shape check ONLY — the `kind` discriminant is checked separately by the
13
+ * caller before this runs, since a same-shaped payload from a different rule
14
+ * (e.g. a hypothetical Lazer payload also carrying `updates`/`feedIds`) must
15
+ * not silently pass as a Pyth Core VAA block.
16
+ */
17
+ function isPythCoreUpdatePayloadShape(payload) {
18
+ return (typeof payload === "object" &&
19
+ payload !== null &&
20
+ Array.isArray(payload.updates) &&
21
+ Array.isArray(payload.feedIds));
22
+ }
23
+ export const PythCoreRule = {
24
+ kind: "pyth_rule",
25
+ // Charges a per-feed `base_update_fee` via `pyth::update_single_price_feed` —
26
+ // see `PriceUpdateRule.requiresFeeSource`.
27
+ requiresFeeSource: true,
28
+ /** Tickers with a `pyth_rule.feeds` entry (mirrors `refreshOraclePrices`'s filter). */
29
+ supportedTickers(host) {
30
+ return Object.keys(host.config.packages.pyth_rule?.feeds ?? {});
31
+ },
32
+ /** Resolves feed ids for `tickers`, then fetches their Hermes accumulator update. */
33
+ async fetchUpdateData(host, tickers) {
34
+ if (tickers.length === 0)
35
+ return null;
36
+ const feedIds = tickers.map((ticker) => host.getPythFeed(ticker).feed_id);
37
+ const updates = await fetchPriceFeedsUpdateData(host.pyth.hermes_endpoint, feedIds, {
38
+ apiKey: host.pyth.api_key,
39
+ fetch: host.pyth.fetch,
40
+ });
41
+ return { kind: "pyth_rule", payload: { updates, feedIds } };
42
+ },
43
+ /**
44
+ * Subsets a (typically whole-universe) payload from {@link fetchUpdateData}
45
+ * down to exactly `tickers`. Pyth Core charges a per-feed update fee (one
46
+ * `update_single_price_feed` moveCall per `feedIds` entry — see
47
+ * `buildPythPriceUpdateCalls`), so serving a full all-registry payload for a
48
+ * 2-ticker build would multiply both the fee and the PTB size ~N× — a
49
+ * per-feed subset is valid input by construction. Narrows `feedIds` only:
50
+ * the single combined Hermes accumulator blob in `updates` already covers
51
+ * every packed feed and needs no re-slicing. A ticker with no
52
+ * `pyth_rule.feeds` entry, or whose feed id is not packed in THIS payload's
53
+ * `feedIds`, is a coverage gap → `null` (miss), never a silent partial.
54
+ */
55
+ narrowUpdateData(host, data, tickers) {
56
+ const payload = assertRuleUpdateData(data, "pyth_rule", isPythCoreUpdatePayloadShape, "{ updates: Uint8Array[]; feedIds: string[] }");
57
+ if (!payload || tickers.length === 0)
58
+ return null;
59
+ const packedFeedIds = new Set(payload.feedIds);
60
+ const feedIds = [];
61
+ for (const ticker of tickers) {
62
+ // Same lookup as `host.getPythFeed(ticker)` minus its throw — an
63
+ // unlisted ticker is a miss here, not an error.
64
+ const feedId = host.config.packages.pyth_rule?.feeds?.[ticker]?.feed_id;
65
+ if (feedId === undefined || !packedFeedIds.has(feedId))
66
+ return null;
67
+ feedIds.push(feedId);
68
+ }
69
+ return { kind: "pyth_rule", payload: { updates: payload.updates, feedIds } };
70
+ },
71
+ /** Appends the wormhole/pyth update PTB block for the payload from {@link fetchUpdateData}. */
72
+ async buildUpdateCalls(tx, host, data, opts) {
73
+ const payload = assertRuleUpdateData(data, "pyth_rule", isPythCoreUpdatePayloadShape, "{ updates: Uint8Array[]; feedIds: string[] }");
74
+ if (!payload)
75
+ return;
76
+ await buildPythPriceUpdateCalls(tx, host, payload.updates, payload.feedIds, {
77
+ cache: opts?.cache,
78
+ feeSource: opts?.feeSource,
79
+ });
80
+ },
81
+ };
@@ -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;
@@ -0,0 +1,189 @@
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 { fromHex } from "@mysten/bcs";
11
+ import { LAZER_DEFAULTS } from "../config.js";
12
+ import { assertRuleUpdateData, } from "../price-update-rule.js";
13
+ import { FetchPolicyError, fetchWithPolicy } from "../update-fetch.js";
14
+ /**
15
+ * Signed-update request pins, mirroring what the on-chain rule consumes:
16
+ * - `properties` — `price` + `exponent` are REQUIRED by
17
+ * `pyth_lazer_rule::price_or_abstain` (a missing exponent abstains);
18
+ * `confidence` is optional on-chain but requested so the rule's
19
+ * fail-closed confidence gate actually engages (a payload without
20
+ * confidence passes the gate unchecked).
21
+ * - `channel` — `real_time`: the deployed rule binds the v1 Lazer API, whose
22
+ * `channel::from_u8` aborts on the 1000ms fixed-rate channel; real_time /
23
+ * 50ms / 200ms are the safe subscriptions, and for an on-demand pull
24
+ * real_time is the freshest.
25
+ * - `formats: leEcdsa` + `jsonBinaryEncoding: hex` — the Sui verifier takes
26
+ * the `leEcdsa` framing; hex matches `fromHex` below.
27
+ */
28
+ const LAZER_LATEST_PRICE_REQUEST = {
29
+ properties: ["price", "exponent", "confidence"],
30
+ formats: ["leEcdsa"],
31
+ jsonBinaryEncoding: "hex",
32
+ channel: "real_time",
33
+ };
34
+ /**
35
+ * Shape check ONLY — the `kind` discriminant is checked separately by the
36
+ * caller before this runs (mirrors `PythCoreRule`'s guard split), so a
37
+ * same-shaped payload from a different rule can never silently pass.
38
+ */
39
+ function isPythLazerUpdatePayloadShape(payload) {
40
+ return (typeof payload === "object" &&
41
+ payload !== null &&
42
+ payload.update instanceof Uint8Array &&
43
+ Array.isArray(payload.feedIds));
44
+ }
45
+ /**
46
+ * Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
47
+ * deployed in config but no `pyth.api_key` is set — the Lazer HTTP API
48
+ * requires a Bearer token and the SDK never reads `process.env` to find one.
49
+ * `instanceof`-able (mirrors `OracleFeeSourceUnavailableError` in `pyth.ts`)
50
+ * so a consumer can branch on the failure type directly instead of
51
+ * string-matching `error.message`.
52
+ */
53
+ export class LazerApiKeyMissingError extends Error {
54
+ constructor() {
55
+ super("LazerApiKeyMissing: pyth_lazer_rule requires a Pyth Lazer access token — " +
56
+ "set `pyth.api_key` in the client config (the SDK never reads process.env)");
57
+ this.name = "LazerApiKeyMissingError";
58
+ }
59
+ }
60
+ /** The `pyth_lazer_rule` deployment entry; throws when the config carries none. */
61
+ function requireLazerPackage(host) {
62
+ const entry = host.config.packages.pyth_lazer_rule;
63
+ if (!entry) {
64
+ throw new Error("pyth_lazer_rule package is not deployed in this config");
65
+ }
66
+ return entry;
67
+ }
68
+ /**
69
+ * Fetch one signed `leEcdsa` update for `feedIds` from the Lazer HTTP API.
70
+ * Goes through the shared `fetchWithPolicy` (`../update-fetch.ts`) — same
71
+ * retry/timeout/Bearer policy as `fetchPriceFeedsUpdateData`, unified so
72
+ * both oracle sources fail the same way under upstream degradation.
73
+ */
74
+ async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
75
+ const url = new URL("/v1/latest_price", endpoint);
76
+ let res;
77
+ try {
78
+ res = await fetchWithPolicy(url.toString(), {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST }),
82
+ }, { apiKey, ...fetchOpts });
83
+ }
84
+ catch (err) {
85
+ // Mirrors fetchPriceFeedsUpdateData's reframing: a retryable status that
86
+ // never recovered carries `status` on the FetchPolicyError — reformat
87
+ // into this function's own message shape; a network-level exhaustion
88
+ // (no status) propagates as-is.
89
+ if (err instanceof FetchPolicyError && err.status !== undefined) {
90
+ const body = err.bodySnippet ? ` ${err.bodySnippet}` : "";
91
+ throw new Error(`Lazer price fetch failed: ${err.status}${body} (retries exhausted after ${err.attempts} attempts)`, { cause: err });
92
+ }
93
+ throw err;
94
+ }
95
+ if (!res.ok)
96
+ throw new Error(`Lazer price fetch failed: ${res.status} ${await res.text()}`);
97
+ const json = (await res.json());
98
+ const hex = json.leEcdsa?.data;
99
+ if (typeof hex !== "string" || hex.length === 0) {
100
+ throw new Error("Lazer returned no leEcdsa update data");
101
+ }
102
+ return fromHex(hex);
103
+ }
104
+ /**
105
+ * `pyth_lazer_rule::feed(collector, config, clock, &update)` — contribute the
106
+ * verified Lazer price for `collector.symbol()` to the collector. `update` is
107
+ * the `RuleUpdateHandle` value from {@link PythLazerRule.buildUpdateCalls} in
108
+ * the SAME PTB; one verified update serves every ticker's feed call. On-chain
109
+ * the rule abstains (records `none`) instead of aborting when the symbol is
110
+ * unconfigured, the feed is absent from the update, the value is degenerate,
111
+ * or the Lazer timestamp is stale.
112
+ */
113
+ export function feedLazerRule(tx, host, collector, update) {
114
+ const lazer = requireLazerPackage(host);
115
+ tx.moveCall({
116
+ target: `${lazer.published_at}::pyth_lazer_rule::feed`,
117
+ arguments: [collector, tx.object(lazer.config), tx.object.clock(), update],
118
+ });
119
+ }
120
+ export const PythLazerRule = {
121
+ kind: "pyth_lazer_rule",
122
+ // Verification is a flat signature check with no Coin argument — no
123
+ // update fee — see `PriceUpdateRule.requiresFeeSource`.
124
+ requiresFeeSource: false,
125
+ /** Tickers with a `pyth_lazer_rule.feeds` entry (integer Lazer feed ids). */
126
+ supportedTickers(host) {
127
+ return Object.keys(host.config.packages.pyth_lazer_rule?.feeds ?? {});
128
+ },
129
+ /** Resolves integer feed ids for `tickers`, then fetches one signed `leEcdsa` update. */
130
+ async fetchUpdateData(host, tickers) {
131
+ if (tickers.length === 0)
132
+ return null;
133
+ // Package-level check first: a config without the deployment must say so,
134
+ // not fail per ticker as if only that feed were missing.
135
+ const { feeds } = requireLazerPackage(host);
136
+ const feedIds = tickers.map((ticker) => {
137
+ const feedId = feeds[ticker];
138
+ if (feedId === undefined) {
139
+ throw new Error(`No pyth_lazer_rule feed listed for ticker: ${ticker}`);
140
+ }
141
+ return feedId;
142
+ });
143
+ const apiKey = host.pyth.api_key;
144
+ if (!apiKey) {
145
+ throw new LazerApiKeyMissingError();
146
+ }
147
+ const update = await fetchLazerSignedUpdate(LAZER_DEFAULTS[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
148
+ return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
149
+ },
150
+ /**
151
+ * A Lazer payload is ONE signed `leEcdsa` message covering every feed it was
152
+ * fetched for — verification is a single flat signature check over the whole
153
+ * message (`parse_and_verify_le_ecdsa_update`, no per-feed cost), so the
154
+ * payload is indivisible: it can only be served whole. Returns the whole
155
+ * payload iff every requested ticker's integer feed id is packed in THIS
156
+ * payload's `feedIds`; any coverage gap (unlisted ticker, or a feed this
157
+ * payload does not carry) → `null` (miss), never a silent partial.
158
+ */
159
+ narrowUpdateData(host, data, tickers) {
160
+ const payload = assertRuleUpdateData(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
161
+ if (!payload || tickers.length === 0)
162
+ return null;
163
+ const packedFeedIds = new Set(payload.feedIds);
164
+ for (const ticker of tickers) {
165
+ const feedId = host.config.packages.pyth_lazer_rule?.feeds?.[ticker];
166
+ if (feedId === undefined || !packedFeedIds.has(feedId))
167
+ return null;
168
+ }
169
+ return { kind: "pyth_lazer_rule", payload };
170
+ },
171
+ /**
172
+ * Appends the single `parse_and_verify_le_ecdsa_update(state, clock, bytes)`
173
+ * call — one secp256k1 signature check covering every feed in the payload —
174
+ * and returns its `Update` result as the handle the per-ticker feed leg
175
+ * consumes. `opts.cache` / `opts.feeSource` are Pyth-Core-specific and
176
+ * ignored (Lazer verification charges no update fee).
177
+ */
178
+ buildUpdateCalls(tx, host, data, _opts) {
179
+ const payload = assertRuleUpdateData(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
180
+ if (!payload)
181
+ return undefined;
182
+ const lazer = requireLazerPackage(host);
183
+ const [update] = tx.moveCall({
184
+ target: `${LAZER_DEFAULTS[host.network].verifier_package}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
185
+ arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
186
+ });
187
+ return { kind: "pyth_lazer_rule", update };
188
+ },
189
+ };
@@ -3,17 +3,21 @@
3
3
  * attach the `PythSponsorRule` witness to a `TradingRequest`. Required when the
4
4
  * market's `request_checklist` contains `PythSponsorRule`.
5
5
  *
6
- * Flow: {@link openPythSponsorFund} opens a `Fund` hot potato; pass the returned
7
- * `{ fund, packageId }` to the Pyth update path as its `sponsorFund` (it draws
8
- * per-feed fees via `pyth_sponsor_rule::split`); then {@link reimbursePythSponsor}
9
- * consumes the `Fund`, returns leftover SUI, and attaches the witness.
6
+ * Flow: {@link openPythSponsorFund} opens a `Fund` hot potato; the caller wraps
7
+ * the returned `{ fund, packageId }` into an `OracleFeeSource` (`{ kind:
8
+ * 'sponsor', fund, packageId }`) and passes that to the Pyth update path (it
9
+ * draws per-feed fees via `pyth_sponsor_rule::split`); then
10
+ * {@link reimbursePythSponsor} consumes the `Fund`, returns leftover SUI, and
11
+ * attaches the witness.
10
12
  */
11
13
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
12
14
  import type { OracleHost } from "../host.ts";
13
15
  /**
14
- * Opens a `Fund` hot potato from the shared PythSponsor pool. Pass the returned
15
- * `{ fund, packageId }` straight to `refreshOraclePrices` as `sponsorFund`, then
16
- * {@link reimbursePythSponsor} once the TradingRequest is built.
16
+ * Opens a `Fund` hot potato from the shared PythSponsor pool. Wrap the
17
+ * returned `{ fund, packageId }` into an `OracleFeeSource`
18
+ * (`{ kind: 'sponsor', fund, packageId }`) and pass that to
19
+ * `refreshOraclePrices` as `feeSource`, then {@link reimbursePythSponsor} once
20
+ * the TradingRequest is built.
17
21
  */
18
22
  export declare function openPythSponsorFund(tx: Transaction, host: OracleHost): {
19
23
  fund: TransactionArgument;
@@ -3,16 +3,20 @@
3
3
  * attach the `PythSponsorRule` witness to a `TradingRequest`. Required when the
4
4
  * market's `request_checklist` contains `PythSponsorRule`.
5
5
  *
6
- * Flow: {@link openPythSponsorFund} opens a `Fund` hot potato; pass the returned
7
- * `{ fund, packageId }` to the Pyth update path as its `sponsorFund` (it draws
8
- * per-feed fees via `pyth_sponsor_rule::split`); then {@link reimbursePythSponsor}
9
- * consumes the `Fund`, returns leftover SUI, and attaches the witness.
6
+ * Flow: {@link openPythSponsorFund} opens a `Fund` hot potato; the caller wraps
7
+ * the returned `{ fund, packageId }` into an `OracleFeeSource` (`{ kind:
8
+ * 'sponsor', fund, packageId }`) and passes that to the Pyth update path (it
9
+ * draws per-feed fees via `pyth_sponsor_rule::split`); then
10
+ * {@link reimbursePythSponsor} consumes the `Fund`, returns leftover SUI, and
11
+ * attaches the witness.
10
12
  */
11
13
  import { reimburse as sponsorReimburse, request as sponsorRequest, } from "../../generated/pyth_sponsor_rule/pyth_sponsor_rule.js";
12
14
  /**
13
- * Opens a `Fund` hot potato from the shared PythSponsor pool. Pass the returned
14
- * `{ fund, packageId }` straight to `refreshOraclePrices` as `sponsorFund`, then
15
- * {@link reimbursePythSponsor} once the TradingRequest is built.
15
+ * Opens a `Fund` hot potato from the shared PythSponsor pool. Wrap the
16
+ * returned `{ fund, packageId }` into an `OracleFeeSource`
17
+ * (`{ kind: 'sponsor', fund, packageId }`) and pass that to
18
+ * `refreshOraclePrices` as `feeSource`, then {@link reimbursePythSponsor} once
19
+ * the TradingRequest is built.
16
20
  */
17
21
  export function openPythSponsorFund(tx, host) {
18
22
  const entry = host.config.packages.pyth_sponsor_rule;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `fetchWithPolicy` — resilience wrapper around `fetch` for the oracle money
3
+ * path's off-chain update-data fetches. Every order/position/collateral
4
+ * tx-build depends on one of these REST calls landing (Hermes VAA for
5
+ * `pyth_rule`, Lazer signed updates for `pyth_lazer_rule`); a bare `fetch`
6
+ * with a single attempt and no retry means one Hermes 429/5xx or timeout
7
+ * fails every trade. `fetchPriceFeedsUpdateData` (`./pyth.ts`),
8
+ * `PythLazerRule.fetchUpdateData`'s Lazer POST (`./rules/pyth-lazer-rule.ts`),
9
+ * and `loadConfig` (`../perp/config.ts`) all delegate here instead of calling
10
+ * `fetch` directly — this is the ONE place a retry/timeout/auth policy is
11
+ * implemented for these fetches.
12
+ *
13
+ * Policy semantics:
14
+ * - Bearer auth is attached iff `policy.apiKey` is a non-empty string —
15
+ * absent/empty is byte-identical to today's keyless request (no
16
+ * `Authorization` header at all). This is the Phase-0 invariant of the
17
+ * Pyth Pro migration: existing keyless deployments see no behavior change.
18
+ * - Retries on network errors, HTTP 429, and HTTP 5xx, with exponential
19
+ * backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). Other
20
+ * 4xx statuses (401/400/403/404/…) are NOT retried — auth/bad-request
21
+ * failures are deterministic, so that `Response` (`ok: false`) is handed
22
+ * back on the first attempt for the caller to format its own
23
+ * domain-specific error, exactly as it did before this wrapper existed.
24
+ * - `init.body`, if set, MUST be replayable across attempts — a retry
25
+ * re-sends the SAME `init` object to `fetch` on every attempt. A string /
26
+ * `URLSearchParams` / BCS-serialized `Uint8Array` body (every caller today)
27
+ * is fine; a one-shot `ReadableStream` body would not survive a second
28
+ * attempt and must not be passed through this function.
29
+ * - Each attempt gets its own `AbortSignal.timeout(policy.timeoutMs)`
30
+ * combined with whichever of `init.signal` / the `externalSignal` param are
31
+ * set — ALL of them can end the whole policy (not just the in-flight
32
+ * attempt), including a queued backoff sleep, via `AbortSignal.any`
33
+ * (runtime floor: Node ≥20.3 / any modern browser — matches this repo's
34
+ * `target: ES2023` + `lib: ["dom", "esnext"]`).
35
+ * - Exhausting retries with no successful/non-retryable response (i.e. every
36
+ * attempt was a network error, or the final attempt was still a retryable
37
+ * HTTP failure) throws a {@link FetchPolicyError} naming the target's
38
+ * `host + pathname` (never the query string — feed ids are off-chain
39
+ * noise, not diagnostic value), the attempt count, and whichever of
40
+ * `status` (a retryable HTTP failure — plus a truncated response-body
41
+ * snippet, when the final attempt's response carried one) or `cause` (a
42
+ * network error) the final attempt produced. An INTERMEDIATE (non-final)
43
+ * retryable response's body is discarded via `response.body?.cancel()`
44
+ * instead of read, so a doomed-to-retry response doesn't pin its
45
+ * connection's socket open for no reason.
46
+ * - Retry worst case: with the defaults (15s timeout × 3 attempts + ~0.75s of
47
+ * backoff between them) a FULL outage takes up to ~46s to surface as a
48
+ * `FetchPolicyError`, vs ~15s pre-3.2.0's single bare-`fetch` attempt.
49
+ * Tunable per client via `config.pyth.fetch.{timeoutMs,retries}`.
50
+ */
51
+ export interface FetchPolicy {
52
+ /** Per-attempt timeout (ms). Default 15_000. */
53
+ readonly timeoutMs?: number;
54
+ /** Retry attempts AFTER the first try — `retries: 2` ⇒ 3 attempts total. Default 2. */
55
+ readonly retries?: number;
56
+ /** Base backoff (ms); doubled per attempt, capped at 2_000ms. Default 250. */
57
+ readonly retryDelayMs?: number;
58
+ /** Bearer token. Attached iff non-empty; empty/missing ⇒ no `Authorization` header. */
59
+ readonly apiKey?: string;
60
+ /** Override the fetch implementation (tests / non-global-`fetch` environments). Default: global `fetch`. */
61
+ readonly fetchImpl?: typeof fetch;
62
+ }
63
+ /** Thrown by {@link fetchWithPolicy} when every attempt failed. */
64
+ export declare class FetchPolicyError extends Error {
65
+ /** HTTP status of the final attempt, when it got a (retryable-but-failing) response. */
66
+ readonly status?: number;
67
+ /** Truncated (~200 char) body of the final attempt's response, when one was readable. */
68
+ readonly bodySnippet?: string;
69
+ /** Total attempts made (first try + retries actually used). */
70
+ readonly attempts: number;
71
+ constructor(message: string, opts: {
72
+ status?: number;
73
+ bodySnippet?: string;
74
+ cause?: unknown;
75
+ attempts: number;
76
+ });
77
+ }
78
+ /**
79
+ * `fetch` with per-attempt timeout, bounded retry + backoff, and optional
80
+ * Bearer auth. See the module header for the full policy. Both `init.signal`
81
+ * (if the caller set one) AND the separate `externalSignal` param cancel the
82
+ * WHOLE policy — in-flight attempts AND queued backoff sleeps — not just a
83
+ * single attempt.
84
+ */
85
+ export declare function fetchWithPolicy(url: string, init?: RequestInit, policy?: FetchPolicy, externalSignal?: AbortSignal): Promise<Response>;