@waterx/sdk 4.0.0 → 4.1.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 (128) hide show
  1. package/README.md +52 -37
  2. package/dist/cjs/src/account/account.js +2 -1
  3. package/dist/cjs/src/account/config.d.ts +0 -16
  4. package/dist/cjs/src/account/funding/balance.d.ts +25 -2
  5. package/dist/cjs/src/account/funding/balance.js +36 -24
  6. package/dist/cjs/src/account/funding/credit.js +6 -10
  7. package/dist/cjs/src/constants.d.ts +15 -1
  8. package/dist/cjs/src/constants.js +18 -4
  9. package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
  10. package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +13 -0
  11. package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
  12. package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.js +31 -0
  13. package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +940 -0
  14. package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +1092 -0
  15. package/dist/cjs/src/oracle/aggregate.d.ts +21 -21
  16. package/dist/cjs/src/oracle/aggregate.js +84 -71
  17. package/dist/cjs/src/oracle/config.d.ts +107 -52
  18. package/dist/cjs/src/oracle/config.js +15 -35
  19. package/dist/cjs/src/oracle/host.d.ts +13 -2
  20. package/dist/cjs/src/oracle/index.d.ts +4 -2
  21. package/dist/cjs/src/oracle/index.js +25 -6
  22. package/dist/cjs/src/oracle/price-update-rule.d.ts +3 -4
  23. package/dist/cjs/src/oracle/pyth.d.ts +68 -6
  24. package/dist/cjs/src/oracle/pyth.js +338 -22
  25. package/dist/cjs/src/oracle/rule-registry.d.ts +11 -6
  26. package/dist/cjs/src/oracle/rule-registry.js +13 -6
  27. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +17 -2
  28. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +6 -6
  29. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +25 -22
  30. package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -0
  31. package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +89 -0
  32. package/dist/cjs/src/oracle/rules/waterx-rule.js +272 -0
  33. package/dist/cjs/src/oracle/update-fetch.d.ts +32 -2
  34. package/dist/cjs/src/oracle/update-fetch.js +60 -3
  35. package/dist/cjs/src/perp/client.d.ts +71 -19
  36. package/dist/cjs/src/perp/client.js +30 -10
  37. package/dist/cjs/src/perp/config.d.ts +4 -7
  38. package/dist/cjs/src/perp/config.js +9 -12
  39. package/dist/cjs/src/perp/constants.d.ts +0 -6
  40. package/dist/cjs/src/perp/constants.js +11 -9
  41. package/dist/cjs/src/perp/fetch/account.js +3 -0
  42. package/dist/cjs/src/perp/fetch/bridge.js +2 -1
  43. package/dist/cjs/src/perp/fetch/market.js +2 -1
  44. package/dist/cjs/src/perp/fetch/positions.d.ts +16 -10
  45. package/dist/cjs/src/perp/fetch/positions.js +23 -20
  46. package/dist/cjs/src/perp/index.d.ts +8 -4
  47. package/dist/cjs/src/perp/index.js +12 -7
  48. package/dist/cjs/src/perp/liq-view.d.ts +64 -0
  49. package/dist/cjs/src/perp/liq-view.js +74 -0
  50. package/dist/cjs/src/perp/user/order.d.ts +13 -0
  51. package/dist/cjs/src/perp/user/order.js +30 -16
  52. package/dist/cjs/src/perp/user/staking.js +3 -2
  53. package/dist/cjs/src/perp/user/trading.js +25 -24
  54. package/dist/cjs/src/perp/user/wlp.js +6 -5
  55. package/dist/cjs/src/prediction/utils.d.ts +11 -2
  56. package/dist/cjs/src/prediction/utils.js +22 -22
  57. package/dist/cjs/src/unified-client.d.ts +49 -20
  58. package/dist/cjs/src/unified-client.js +4 -1
  59. package/dist/cjs/src/utils/format.d.ts +14 -0
  60. package/dist/cjs/src/utils/format.js +24 -0
  61. package/dist/cjs/src/utils/math.d.ts +304 -12
  62. package/dist/cjs/src/utils/math.js +397 -17
  63. package/dist/cjs/src/utils/validate.d.ts +69 -0
  64. package/dist/cjs/src/utils/validate.js +183 -0
  65. package/dist/src/account/account.js +2 -1
  66. package/dist/src/account/config.d.ts +0 -16
  67. package/dist/src/account/funding/balance.d.ts +25 -2
  68. package/dist/src/account/funding/balance.js +36 -24
  69. package/dist/src/account/funding/credit.js +6 -10
  70. package/dist/src/constants.d.ts +15 -1
  71. package/dist/src/constants.js +17 -3
  72. package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
  73. package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +10 -0
  74. package/dist/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
  75. package/dist/src/generated/waterx_rule/deps/sui/vec_map.js +27 -0
  76. package/dist/src/generated/waterx_rule/waterx_rule.d.ts +940 -0
  77. package/dist/src/generated/waterx_rule/waterx_rule.js +991 -0
  78. package/dist/src/oracle/aggregate.d.ts +21 -21
  79. package/dist/src/oracle/aggregate.js +84 -71
  80. package/dist/src/oracle/config.d.ts +107 -52
  81. package/dist/src/oracle/config.js +14 -34
  82. package/dist/src/oracle/host.d.ts +13 -2
  83. package/dist/src/oracle/index.d.ts +4 -2
  84. package/dist/src/oracle/index.js +18 -7
  85. package/dist/src/oracle/price-update-rule.d.ts +3 -4
  86. package/dist/src/oracle/pyth.d.ts +68 -6
  87. package/dist/src/oracle/pyth.js +334 -22
  88. package/dist/src/oracle/rule-registry.d.ts +11 -6
  89. package/dist/src/oracle/rule-registry.js +13 -6
  90. package/dist/src/oracle/rules/pyth-core-rule.js +18 -3
  91. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +6 -6
  92. package/dist/src/oracle/rules/pyth-lazer-rule.js +26 -23
  93. package/dist/src/oracle/rules/pyth-rule.js +5 -0
  94. package/dist/src/oracle/rules/waterx-rule.d.ts +89 -0
  95. package/dist/src/oracle/rules/waterx-rule.js +266 -0
  96. package/dist/src/oracle/update-fetch.d.ts +32 -2
  97. package/dist/src/oracle/update-fetch.js +57 -3
  98. package/dist/src/perp/client.d.ts +71 -19
  99. package/dist/src/perp/client.js +31 -11
  100. package/dist/src/perp/config.d.ts +4 -7
  101. package/dist/src/perp/config.js +9 -12
  102. package/dist/src/perp/constants.d.ts +0 -6
  103. package/dist/src/perp/constants.js +10 -8
  104. package/dist/src/perp/fetch/account.js +3 -0
  105. package/dist/src/perp/fetch/bridge.js +2 -1
  106. package/dist/src/perp/fetch/market.js +2 -1
  107. package/dist/src/perp/fetch/positions.d.ts +16 -10
  108. package/dist/src/perp/fetch/positions.js +28 -20
  109. package/dist/src/perp/index.d.ts +8 -4
  110. package/dist/src/perp/index.js +5 -3
  111. package/dist/src/perp/liq-view.d.ts +64 -0
  112. package/dist/src/perp/liq-view.js +71 -0
  113. package/dist/src/perp/user/order.d.ts +13 -0
  114. package/dist/src/perp/user/order.js +30 -16
  115. package/dist/src/perp/user/staking.js +3 -2
  116. package/dist/src/perp/user/trading.js +25 -24
  117. package/dist/src/perp/user/wlp.js +6 -5
  118. package/dist/src/prediction/utils.d.ts +11 -2
  119. package/dist/src/prediction/utils.js +22 -22
  120. package/dist/src/unified-client.d.ts +49 -20
  121. package/dist/src/unified-client.js +4 -1
  122. package/dist/src/utils/format.d.ts +14 -0
  123. package/dist/src/utils/format.js +21 -0
  124. package/dist/src/utils/math.d.ts +304 -12
  125. package/dist/src/utils/math.js +394 -17
  126. package/dist/src/utils/validate.d.ts +69 -0
  127. package/dist/src/utils/validate.js +167 -0
  128. package/package.json +4 -1
@@ -36,12 +36,27 @@ exports.PythCoreRule = {
36
36
  async fetchUpdateData(host, tickers) {
37
37
  if (tickers.length === 0)
38
38
  return null;
39
+ // `host.pyth` is this source's own infra — the fixed per-network Core
40
+ // Pyth block plus the caller's api_key/fetch. Endpoint, credential and
41
+ // retry/timeout policy all come from it.
42
+ const endpoint = host.pyth.hermes_endpoint;
39
43
  const feedIds = tickers.map((ticker) => host.getPythFeed(ticker).feed_id);
40
- const updates = await (0, pyth_ts_1.fetchPriceFeedsUpdateData)(host.pyth.hermes_endpoint, feedIds, {
44
+ const updates = await (0, pyth_ts_1.fetchPriceFeedsUpdateData)(endpoint, feedIds, {
41
45
  apiKey: host.pyth.api_key,
42
46
  fetch: host.pyth.fetch,
43
47
  });
44
- return { kind: "pyth_rule", payload: { updates, feedIds } };
48
+ // `updates` covers only the feeds this endpoint actually served — the fetch
49
+ // drops (and memoizes) any it lacks, e.g. Core feeds absent from Pyth Pro
50
+ // (WTIUSD/BRENTUSD). Align `feedIds` with them: buildPythPriceUpdateCalls
51
+ // emits one moveCall per feedId and must not reference a feed the
52
+ // accumulator blob doesn't cover.
53
+ return {
54
+ kind: "pyth_rule",
55
+ payload: {
56
+ updates,
57
+ feedIds: (0, pyth_ts_1.endpointSupportedFeedIds)(endpoint, feedIds, host.pyth.api_key),
58
+ },
59
+ };
45
60
  },
46
61
  /**
47
62
  * Subsets a (typically whole-universe) payload from {@link fetchUpdateData}
@@ -3,7 +3,7 @@
3
3
  * updates, plus `feedLazerRule`, the collector-feed leg `aggregateTicker`
4
4
  * appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
5
5
  * requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
6
- * via `config.pyth.api_key`), verifies it ONCE on-chain via
6
+ * via the `pythApiKey` create option), verifies it ONCE on-chain via
7
7
  * `pyth_lazer::parse_and_verify_le_ecdsa_update`, and hands the resulting
8
8
  * `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
9
9
  */
@@ -19,11 +19,11 @@ export interface PythLazerUpdatePayload {
19
19
  }
20
20
  /**
21
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`.
22
+ * deployed in config but no `pythApiKey` was supplied at client init the
23
+ * Lazer HTTP API requires a Bearer token and the SDK never reads
24
+ * `process.env` to find one. `instanceof`-able (mirrors
25
+ * `OracleFeeSourceUnavailableError` in `pyth.ts`) so a consumer can branch on
26
+ * the failure type directly instead of string-matching `error.message`.
27
27
  */
28
28
  export declare class LazerApiKeyMissingError extends Error {
29
29
  constructor();
@@ -4,7 +4,7 @@
4
4
  * updates, plus `feedLazerRule`, the collector-feed leg `aggregateTicker`
5
5
  * appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
6
6
  * requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
7
- * via `config.pyth.api_key`), verifies it ONCE on-chain via
7
+ * via the `pythApiKey` create option), verifies it ONCE on-chain via
8
8
  * `pyth_lazer::parse_and_verify_le_ecdsa_update`, and hands the resulting
9
9
  * `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
10
10
  */
@@ -22,10 +22,17 @@ const update_fetch_ts_1 = require("../update-fetch.js");
22
22
  * `confidence` is optional on-chain but requested so the rule's
23
23
  * fail-closed confidence gate actually engages (a payload without
24
24
  * confidence passes the gate unchecked).
25
- * - `channel` — `real_time`: the deployed rule binds the v1 Lazer API, whose
26
- * `channel::from_u8` aborts on the 1000ms fixed-rate channel; real_time /
27
- * 50ms / 200ms are the safe subscriptions, and for an on-demand pull
28
- * real_time is the freshest.
25
+ * - `channel` — `fixed_rate@200ms`, NOT `real_time`: Lazer rejects a request
26
+ * whose channel is faster than ANY requested feed's `min_channel`, and it
27
+ * rejects the WHOLE batch (`400 Feeds do not support channel …`). Only the
28
+ * majors (BTC/ETH/SOL/USDC/DOGE/XRP/BNB/HYPE + EUR/JPY FX) publish
29
+ * `real_time`; the other 19 of the 29 configured feeds — including SUIUSD
30
+ * and every xStock — are `min_channel: fixed_rate@200ms` (Lazer symbol
31
+ * registry, verified 2026-07-22: the same 29-feed batch 400s at
32
+ * `real_time`/`50ms` and serves 200 with the leEcdsa blob at `200ms`).
33
+ * 200ms is the fastest channel every configured feed supports, and the
34
+ * deployed rule accepts it: the v1 on-chain `channel::from_u8` aborts only
35
+ * on the 1000ms fixed-rate channel (real_time / 50ms / 200ms are safe).
29
36
  * - `formats: leEcdsa` + `jsonBinaryEncoding: hex` — the Sui verifier takes
30
37
  * the `leEcdsa` framing; hex matches `fromHex` below.
31
38
  */
@@ -33,7 +40,7 @@ const LAZER_LATEST_PRICE_REQUEST = {
33
40
  properties: ["price", "exponent", "confidence"],
34
41
  formats: ["leEcdsa"],
35
42
  jsonBinaryEncoding: "hex",
36
- channel: "real_time",
43
+ channel: "fixed_rate@200ms",
37
44
  };
38
45
  /**
39
46
  * Shape check ONLY — the `kind` discriminant is checked separately by the
@@ -48,16 +55,16 @@ function isPythLazerUpdatePayloadShape(payload) {
48
55
  }
49
56
  /**
50
57
  * Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
51
- * deployed in config but no `pyth.api_key` is set the Lazer HTTP API
52
- * requires a Bearer token and the SDK never reads `process.env` to find one.
53
- * `instanceof`-able (mirrors `OracleFeeSourceUnavailableError` in `pyth.ts`)
54
- * so a consumer can branch on the failure type directly instead of
55
- * string-matching `error.message`.
58
+ * deployed in config but no `pythApiKey` was supplied at client init the
59
+ * Lazer HTTP API requires a Bearer token and the SDK never reads
60
+ * `process.env` to find one. `instanceof`-able (mirrors
61
+ * `OracleFeeSourceUnavailableError` in `pyth.ts`) so a consumer can branch on
62
+ * the failure type directly instead of string-matching `error.message`.
56
63
  */
57
64
  class LazerApiKeyMissingError extends Error {
58
65
  constructor() {
59
66
  super("LazerApiKeyMissing: pyth_lazer_rule requires a Pyth Lazer access token — " +
60
- "set `pyth.api_key` in the client config (the SDK never reads process.env)");
67
+ "pass `pythApiKey` when creating the client (the SDK never reads process.env)");
61
68
  this.name = "LazerApiKeyMissingError";
62
69
  }
63
70
  }
@@ -77,7 +84,11 @@ function requireLazerPackage(host) {
77
84
  * both oracle sources fail the same way under upstream degradation.
78
85
  */
79
86
  async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
80
- const url = new URL("/v1/latest_price", endpoint);
87
+ // joinEndpointPath preserves any base path on the endpoint — the same
88
+ // leading-slash `new URL` footgun that 404'd every feed on the Pyth Pro
89
+ // Hermes endpoint (see update-fetch.ts). Defensive here: the default
90
+ // Lazer endpoint has no base path, but a config override may.
91
+ const url = (0, update_fetch_ts_1.joinEndpointPath)(endpoint, "v1/latest_price");
81
92
  let res;
82
93
  try {
83
94
  res = await (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), {
@@ -87,15 +98,7 @@ async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
87
98
  }, { apiKey, ...fetchOpts });
88
99
  }
89
100
  catch (err) {
90
- // Mirrors fetchPriceFeedsUpdateData's reframing: a retryable status that
91
- // never recovered carries `status` on the FetchPolicyError — reformat
92
- // into this function's own message shape; a network-level exhaustion
93
- // (no status) propagates as-is.
94
- if (err instanceof update_fetch_ts_1.FetchPolicyError && err.status !== undefined) {
95
- const body = err.bodySnippet ? ` ${err.bodySnippet}` : "";
96
- throw new Error(`Lazer price fetch failed: ${err.status}${body} (retries exhausted after ${err.attempts} attempts)`, { cause: err });
97
- }
98
- throw err;
101
+ (0, update_fetch_ts_1.rethrowExhaustedFetch)(err, (e) => `Lazer price fetch failed: ${e.status}${e.bodySnippet ? ` ${e.bodySnippet}` : ""}`);
99
102
  }
100
103
  if (!res.ok)
101
104
  throw new Error(`Lazer price fetch failed: ${res.status} ${await res.text()}`);
@@ -16,6 +16,11 @@ function feedPythRule(tx, host, collector, priceInfoObjectId) {
16
16
  arguments: {
17
17
  collector,
18
18
  config: tx.object(host.config.packages.pyth_rule.config),
19
+ // The deployed pyth_rule package is compiled against the Core pyth
20
+ // dependency, so its `&PythState` parameter is the Core-package-qualified
21
+ // type and `host.pyth` (the fixed per-network Core infra) is always the
22
+ // right state to pass. The config's price_info_object entries are Core
23
+ // objects to match.
19
24
  pythState: tx.object(host.pyth.state_id),
20
25
  pythPriceInfo: tx.object(priceInfoObjectId),
21
26
  },
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `WaterxRule` — `PriceUpdateRule` for the first-party WaterX quote-center
3
+ * (Nautilus-TEE, ed25519), plus `feedWaterxRule`, the collector-feed leg
4
+ * `aggregateTicker` appends per waterx-routed ticker. Pulls one enclave-signed
5
+ * batch envelope covering every requested ticker from the quote-center
6
+ * (`GET /v1/quotes/update?symbols=…`, endpoint from `host.waterx` — the
7
+ * `waterxEndpoint`/`waterxFetch` create options — else `WATERX_DEFAULTS`), then —
8
+ * unlike Pyth Lazer, whose verify is a single shared PTB step — verifies AND
9
+ * feeds in ONE `waterx_rule::collect_batch_latest` call per collector (the Move
10
+ * API bundles the two). So `buildUpdateCalls` emits nothing and the signed
11
+ * envelope is handed straight to the per-ticker feed leg.
12
+ *
13
+ * `collect_batch_latest` is the dual-rule path: it feeds the item matching
14
+ * `collector.symbol()` WITHOUT aggregating, so a waterx-routed ticker composes
15
+ * onto the same collector as Pyth/Supra (compose-then-aggregate). On-chain a
16
+ * freshness miss / replayed timestamp ABSTAINS (the other weighted rules
17
+ * cover); a config/integrity mismatch or bad signature aborts.
18
+ */
19
+ import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
20
+ import type { OracleHost } from "../host.ts";
21
+ import { type PriceUpdateRule, type RuleUpdateData } from "../price-update-rule.ts";
22
+ /**
23
+ * One item inside a signed batch payload, mirroring the quote-center
24
+ * `/v1/quotes/update` JSON 1:1 (snake_case). The u64 integer fields are the
25
+ * EXACT values the enclave signed over BCS — `collect_batch_latest` rebuilds
26
+ * the payload on-chain and re-verifies, so they must round-trip byte-for-byte.
27
+ * They are `bigint` (not `number`): {@link parseSignedEnvelope} decodes them
28
+ * from the raw JSON integer literals so a value above `Number.MAX_SAFE_INTEGER`
29
+ * (2^53) can never lose precision and silently abort the on-chain signature
30
+ * check. `num_sources` is a `u8` (≤ 255) and stays a `number`.
31
+ */
32
+ export interface WaterxBatchItem {
33
+ symbol: string;
34
+ ticker: string;
35
+ sources: bigint[];
36
+ method: string;
37
+ price_timestamp_ms: bigint;
38
+ price_n: bigint;
39
+ price_scale: bigint;
40
+ confidence_n: bigint;
41
+ confidence_scale: bigint;
42
+ max_source_deviation_bps: bigint;
43
+ num_sources: number;
44
+ }
45
+ /** The enclave-signed batch envelope from `GET /v1/quotes/update`. */
46
+ export interface WaterxSignedEnvelope {
47
+ intent: number;
48
+ /** Enclave signing timestamp (ms) — the on-chain `timestamp_ms` argument. */
49
+ timestamp_ms: bigint;
50
+ payload: {
51
+ items: WaterxBatchItem[];
52
+ };
53
+ /** ed25519 signature over `BCS(IntentMessage<BatchPricePayload>)`, hex (± `0x`). */
54
+ signature: string;
55
+ }
56
+ /** `waterx_rule`'s narrowed `RuleUpdateData.payload` shape. */
57
+ export interface WaterxUpdatePayload {
58
+ readonly envelope: WaterxSignedEnvelope;
59
+ }
60
+ /**
61
+ * Parse a quote-center `/v1/quotes/update` response body into a
62
+ * {@link WaterxSignedEnvelope} with the u64 fields decoded as `bigint`, exact.
63
+ *
64
+ * The signature is over `BCS(IntentMessage<BatchPricePayload>)`, so every u64
65
+ * the SDK rebuilds in-PTB must equal the enclave's byte-for-byte or
66
+ * `collect_batch_latest` aborts the whole trade PTB (bad signature — not an
67
+ * abstain). A plain `JSON.parse` yields IEEE-754 doubles that lose precision
68
+ * above 2^53, so instead we recover each integer's exact source literal via the
69
+ * ES2023 reviver `context.source` (Node 21+ / modern browsers) and `BigInt()`
70
+ * it. On an older runtime that passes no `context`, a value within 2^53 is
71
+ * still exact (`BigInt(number)`); a value ABOVE it throws loudly here rather
72
+ * than silently corrupting the payload into an on-chain abort. `num_sources`
73
+ * (u8) and `intent` are coerced back to `number` — both are tiny.
74
+ */
75
+ export declare function parseSignedEnvelope(text: string): WaterxSignedEnvelope;
76
+ /** Narrow a `RuleUpdateData` to its `WaterxSignedEnvelope`, or `null`. */
77
+ export declare function waterxEnvelopeOf(data: RuleUpdateData): WaterxSignedEnvelope | null;
78
+ /**
79
+ * `waterx_rule::collect_batch_latest(collector, config, clock, enclave_config,
80
+ * enclave, timestamp_ms, payload, sig)` — rebuild the enclave-signed batch
81
+ * payload in-PTB (`new_batch_payload` + one `new_batch_item`/`push_batch_item`
82
+ * per item, the exact shape the enclave signed) and contribute the price for
83
+ * `collector.symbol()` to the collector. One collect call re-verifies the batch
84
+ * signature and picks this collector's symbol out of the batch; on-chain it
85
+ * abstains (records `none`) instead of aborting when the symbol is stale,
86
+ * absent from the batch, or its timestamp was already accepted (replay).
87
+ */
88
+ export declare function feedWaterxRule(tx: Transaction, host: OracleHost, collector: TransactionArgument, envelope: WaterxSignedEnvelope): void;
89
+ export declare const WaterxRule: PriceUpdateRule;
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ /**
3
+ * `WaterxRule` — `PriceUpdateRule` for the first-party WaterX quote-center
4
+ * (Nautilus-TEE, ed25519), plus `feedWaterxRule`, the collector-feed leg
5
+ * `aggregateTicker` appends per waterx-routed ticker. Pulls one enclave-signed
6
+ * batch envelope covering every requested ticker from the quote-center
7
+ * (`GET /v1/quotes/update?symbols=…`, endpoint from `host.waterx` — the
8
+ * `waterxEndpoint`/`waterxFetch` create options — else `WATERX_DEFAULTS`), then —
9
+ * unlike Pyth Lazer, whose verify is a single shared PTB step — verifies AND
10
+ * feeds in ONE `waterx_rule::collect_batch_latest` call per collector (the Move
11
+ * API bundles the two). So `buildUpdateCalls` emits nothing and the signed
12
+ * envelope is handed straight to the per-ticker feed leg.
13
+ *
14
+ * `collect_batch_latest` is the dual-rule path: it feeds the item matching
15
+ * `collector.symbol()` WITHOUT aggregating, so a waterx-routed ticker composes
16
+ * onto the same collector as Pyth/Supra (compose-then-aggregate). On-chain a
17
+ * freshness miss / replayed timestamp ABSTAINS (the other weighted rules
18
+ * cover); a config/integrity mismatch or bad signature aborts.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.WaterxRule = void 0;
22
+ exports.parseSignedEnvelope = parseSignedEnvelope;
23
+ exports.waterxEnvelopeOf = waterxEnvelopeOf;
24
+ exports.feedWaterxRule = feedWaterxRule;
25
+ const bcs_1 = require("@mysten/bcs");
26
+ const waterx_rule_ts_1 = require("../../generated/waterx_rule/waterx_rule.js");
27
+ const config_ts_1 = require("../config.js");
28
+ const price_update_rule_ts_1 = require("../price-update-rule.js");
29
+ const update_fetch_ts_1 = require("../update-fetch.js");
30
+ /** The single signing intent (`BATCH_PRICE_INTENT`) the quote-center emits. */
31
+ const BATCH_PRICE_INTENT = 1;
32
+ /**
33
+ * Shape check ONLY — the `kind` discriminant is checked separately by the
34
+ * caller before this runs (mirrors the other rules' guard split), so a
35
+ * same-shaped payload from a different rule can never silently pass.
36
+ */
37
+ function isWaterxUpdatePayloadShape(payload) {
38
+ const env = payload?.envelope;
39
+ return (typeof env === "object" &&
40
+ env !== null &&
41
+ typeof env.signature === "string" &&
42
+ typeof env.timestamp_ms === "bigint" &&
43
+ Array.isArray(env.payload?.items));
44
+ }
45
+ /**
46
+ * Parse a quote-center `/v1/quotes/update` response body into a
47
+ * {@link WaterxSignedEnvelope} with the u64 fields decoded as `bigint`, exact.
48
+ *
49
+ * The signature is over `BCS(IntentMessage<BatchPricePayload>)`, so every u64
50
+ * the SDK rebuilds in-PTB must equal the enclave's byte-for-byte or
51
+ * `collect_batch_latest` aborts the whole trade PTB (bad signature — not an
52
+ * abstain). A plain `JSON.parse` yields IEEE-754 doubles that lose precision
53
+ * above 2^53, so instead we recover each integer's exact source literal via the
54
+ * ES2023 reviver `context.source` (Node 21+ / modern browsers) and `BigInt()`
55
+ * it. On an older runtime that passes no `context`, a value within 2^53 is
56
+ * still exact (`BigInt(number)`); a value ABOVE it throws loudly here rather
57
+ * than silently corrupting the payload into an on-chain abort. `num_sources`
58
+ * (u8) and `intent` are coerced back to `number` — both are tiny.
59
+ */
60
+ function parseSignedEnvelope(text) {
61
+ const raw = JSON.parse(text, (_key, value, context) => {
62
+ if (typeof value !== "number" || !Number.isInteger(value))
63
+ return value;
64
+ if (context?.source !== undefined)
65
+ return BigInt(context.source);
66
+ if (!Number.isSafeInteger(value)) {
67
+ throw new Error("waterx envelope carries an integer above 2^53 and this runtime lacks JSON " +
68
+ "source access — cannot preserve u64 precision for the signed payload");
69
+ }
70
+ return BigInt(value);
71
+ });
72
+ if (typeof raw.signature !== "string" || !Array.isArray(raw.payload?.items)) {
73
+ throw new Error("WaterX quote-center returned a malformed signed envelope");
74
+ }
75
+ return {
76
+ intent: Number(raw.intent),
77
+ timestamp_ms: (raw.timestamp_ms ?? 0n),
78
+ signature: raw.signature,
79
+ payload: {
80
+ items: raw.payload.items.map((i) => ({ ...i, num_sources: Number(i.num_sources) })),
81
+ },
82
+ };
83
+ }
84
+ /** The `waterx_rule` deployment entry; throws when the config carries none. */
85
+ function requireWaterxPackage(host) {
86
+ const entry = host.config.packages.waterx_rule;
87
+ if (!entry) {
88
+ throw new Error("waterx_rule package is not deployed in this config");
89
+ }
90
+ return entry;
91
+ }
92
+ /**
93
+ * Resolve the quote-center infra for this host: the `waterxEndpoint` /
94
+ * `waterxFetch` create options when the client carries them, else the network
95
+ * default. The fetch policy falls back to the shared `pyth.fetch` policy so a
96
+ * consumer that already tuned timeouts/retries once keeps them here.
97
+ *
98
+ * This is the seam a browser consumer needs: the envelope is fetched FROM THE
99
+ * PAGE, so a front end whose origin the quote-center does not allow (CORS)
100
+ * points `endpoint` at a same-origin proxy, or supplies its own `fetchImpl`.
101
+ */
102
+ function resolveWaterxInfra(host) {
103
+ const infra = host.waterx ?? config_ts_1.WATERX_DEFAULTS[host.network];
104
+ return { endpoint: infra.endpoint, fetch: infra.fetch ?? host.pyth.fetch };
105
+ }
106
+ /**
107
+ * Pull one enclave-signed batch envelope covering `symbols` from the
108
+ * quote-center. Goes through the shared `fetchWithPolicy` (`../update-fetch.ts`)
109
+ * — same retry/timeout policy as the Pyth/Lazer fetches. No auth: the
110
+ * quote-center read surface is public.
111
+ *
112
+ * The URL is built with `joinEndpointPath`, not `new URL(path, endpoint)`: a
113
+ * leading-slash path is ABSOLUTE and silently drops the endpoint's own base
114
+ * path, which is exactly what a `waterxEndpoint` proxy route is (a
115
+ * `https://app.example/api/quote-center` override would have been rewritten to
116
+ * `https://app.example/v1/quotes/update`, bypassing the proxy). Same footgun
117
+ * that 404'd every Pyth Pro feed by dropping its `/hermes` prefix.
118
+ */
119
+ async function fetchWaterxSignedUpdate(endpoint, symbols, fetchOpts) {
120
+ const url = (0, update_fetch_ts_1.joinEndpointPath)(endpoint, "v1/quotes/update");
121
+ url.searchParams.set("symbols", symbols.join(","));
122
+ let res;
123
+ try {
124
+ res = await (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), { method: "GET" }, { ...fetchOpts });
125
+ }
126
+ catch (err) {
127
+ if (err instanceof update_fetch_ts_1.FetchPolicyError && err.status !== undefined) {
128
+ const body = err.bodySnippet ? ` ${err.bodySnippet}` : "";
129
+ throw new Error(`WaterX quote-center fetch failed: ${err.status}${body} (retries exhausted after ${err.attempts} attempts)`, { cause: err });
130
+ }
131
+ throw err;
132
+ }
133
+ if (!res.ok) {
134
+ throw new Error(`WaterX quote-center fetch failed: ${res.status} ${await res.text()}`);
135
+ }
136
+ // Parse from raw text (not res.json()) so the u64 fields are decoded exact as
137
+ // bigint — see parseSignedEnvelope. Malformed-shape check lives there.
138
+ const envelope = parseSignedEnvelope(await res.text());
139
+ if (envelope.intent !== BATCH_PRICE_INTENT) {
140
+ throw new Error(`WaterX quote-center returned intent ${envelope.intent}, expected BATCH_PRICE_INTENT ${BATCH_PRICE_INTENT}`);
141
+ }
142
+ return envelope;
143
+ }
144
+ /** Narrow a `RuleUpdateData` to its `WaterxSignedEnvelope`, or `null`. */
145
+ function waterxEnvelopeOf(data) {
146
+ const payload = (0, price_update_rule_ts_1.assertRuleUpdateData)(data, "waterx_rule", isWaterxUpdatePayloadShape, "{ envelope: { intent, timestamp_ms, payload: { items }, signature } }");
147
+ return payload?.envelope ?? null;
148
+ }
149
+ /** Strip an optional `0x` prefix, then decode hex → bytes. */
150
+ function decodeSig(hex) {
151
+ return (0, bcs_1.fromHex)(hex.startsWith("0x") ? hex.slice(2) : hex);
152
+ }
153
+ /**
154
+ * `waterx_rule::collect_batch_latest(collector, config, clock, enclave_config,
155
+ * enclave, timestamp_ms, payload, sig)` — rebuild the enclave-signed batch
156
+ * payload in-PTB (`new_batch_payload` + one `new_batch_item`/`push_batch_item`
157
+ * per item, the exact shape the enclave signed) and contribute the price for
158
+ * `collector.symbol()` to the collector. One collect call re-verifies the batch
159
+ * signature and picks this collector's symbol out of the batch; on-chain it
160
+ * abstains (records `none`) instead of aborting when the symbol is stale,
161
+ * absent from the batch, or its timestamp was already accepted (replay).
162
+ */
163
+ function feedWaterxRule(tx, host, collector, envelope) {
164
+ const wr = requireWaterxPackage(host);
165
+ const pkg = wr.published_at;
166
+ const payload = (0, waterx_rule_ts_1.newBatchPayload)({ package: pkg })(tx);
167
+ for (const item of envelope.payload.items) {
168
+ // u64 fields are already exact bigints (see parseSignedEnvelope) — passed
169
+ // through verbatim so the rebuilt BCS matches the enclave's signed bytes.
170
+ const itemArg = (0, waterx_rule_ts_1.newBatchItem)({
171
+ package: pkg,
172
+ arguments: {
173
+ symbol: item.symbol,
174
+ ticker: item.ticker,
175
+ sources: item.sources,
176
+ method: item.method,
177
+ priceTimestampMs: item.price_timestamp_ms,
178
+ priceN: item.price_n,
179
+ priceScale: item.price_scale,
180
+ confidenceN: item.confidence_n,
181
+ confidenceScale: item.confidence_scale,
182
+ maxSourceDeviationBps: item.max_source_deviation_bps,
183
+ numSources: item.num_sources,
184
+ },
185
+ })(tx);
186
+ (0, waterx_rule_ts_1.pushBatchItem)({ package: pkg, arguments: { payload, item: itemArg } })(tx);
187
+ }
188
+ (0, waterx_rule_ts_1.collectBatchLatest)({
189
+ package: pkg,
190
+ arguments: {
191
+ collector,
192
+ config: tx.object(wr.config),
193
+ enclaveConfig: tx.object(wr.enclave_config),
194
+ enclave: tx.object(wr.enclave),
195
+ timestampMs: envelope.timestamp_ms,
196
+ payload,
197
+ sig: Array.from(decodeSig(envelope.signature)),
198
+ },
199
+ })(tx);
200
+ }
201
+ exports.WaterxRule = {
202
+ kind: "waterx_rule",
203
+ // Verification is an in-Move ed25519 check with no Coin argument — no
204
+ // update fee — see `PriceUpdateRule.requiresFeeSource`.
205
+ requiresFeeSource: false,
206
+ /** Tickers with a `waterx_rule.feeds` entry (keyed by oracle ticker). */
207
+ supportedTickers(host) {
208
+ return Object.keys(host.config.packages.waterx_rule?.feeds ?? {});
209
+ },
210
+ /**
211
+ * Pulls one enclave-signed batch envelope covering `tickers` from the
212
+ * quote-center, and only returns it when it actually covers ALL of them.
213
+ *
214
+ * A 200 whose `items` omit a requested symbol is a valid, well-signed
215
+ * envelope — nothing downstream would reject it, and the build would emit a
216
+ * `collect_batch_latest` that abstains for the missing symbol, surfacing as
217
+ * an on-chain `EMissingPriceSource` (or a silently thinner weighted set) much
218
+ * later. Same coverage rule the cached path enforces in
219
+ * {@link WaterxRule.narrowUpdateData}; the difference is disposition — a
220
+ * cache miss falls back to this live fetch, whereas the live source itself
221
+ * coming up short has no fallback left, so it throws deterministically here.
222
+ */
223
+ async fetchUpdateData(host, tickers) {
224
+ if (tickers.length === 0)
225
+ return null;
226
+ // Package-level check first: a config without the deployment must say so,
227
+ // not fail per ticker as if only that feed were missing.
228
+ const { feeds } = requireWaterxPackage(host);
229
+ for (const ticker of tickers) {
230
+ if (feeds[ticker] === undefined) {
231
+ throw new Error(`No waterx_rule feed listed for ticker: ${ticker}`);
232
+ }
233
+ }
234
+ const { endpoint, fetch: fetchOpts } = resolveWaterxInfra(host);
235
+ const envelope = await fetchWaterxSignedUpdate(endpoint, tickers, fetchOpts);
236
+ const covered = new Set(envelope.payload.items.map((i) => i.symbol));
237
+ const missing = tickers.filter((t) => !covered.has(t));
238
+ if (missing.length > 0) {
239
+ throw new Error(`WaterX quote-center envelope does not cover ticker(s): ${missing.join(", ")} ` +
240
+ `(requested ${tickers.join(", ")}; served ${[...covered].join(", ") || "none"})`);
241
+ }
242
+ return { kind: "waterx_rule", payload: { envelope } };
243
+ },
244
+ /**
245
+ * One signed batch envelope carries a single ed25519 signature over its whole
246
+ * `payload` — it is indivisible: it can only be served whole (re-verified from
247
+ * the full item set). Returns the whole payload iff every requested ticker's
248
+ * item is present in THIS envelope; any coverage gap → `null` (miss), never a
249
+ * silent partial.
250
+ */
251
+ narrowUpdateData(_host, data, tickers) {
252
+ const envelope = waterxEnvelopeOf(data);
253
+ if (!envelope || tickers.length === 0)
254
+ return null;
255
+ const covered = new Set(envelope.payload.items.map((i) => i.symbol));
256
+ for (const ticker of tickers) {
257
+ if (!covered.has(ticker))
258
+ return null;
259
+ }
260
+ return { kind: "waterx_rule", payload: { envelope } };
261
+ },
262
+ /**
263
+ * No shared verify step: `waterx_rule::collect_batch_latest` bundles verify
264
+ * AND feed into one per-collector call, appended by {@link feedWaterxRule} in
265
+ * the per-ticker aggregate leg. So this emits nothing and returns `void` — the
266
+ * signed envelope reaches the feed leg via `aggregate.ts`'s per-ticker map
267
+ * (built from the group's fetched data), not a `RuleUpdateHandle`.
268
+ */
269
+ buildUpdateCalls(_tx, _host, _data, _opts) {
270
+ return;
271
+ },
272
+ };
@@ -16,7 +16,11 @@
16
16
  * `Authorization` header at all). This is the Phase-0 invariant of the
17
17
  * Pyth Pro migration: existing keyless deployments see no behavior change.
18
18
  * - Retries on network errors, HTTP 429, and HTTP 5xx, with exponential
19
- * backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). Other
19
+ * backoff (`retryDelayMs * 2^attempt`, capped at `MAX_BACKOFF_MS`). A 429
20
+ * carrying a numeric `Retry-After` header uses the SERVER'S delay instead,
21
+ * when it fits under the same cap — a longer ask degrades to normal
22
+ * backoff rather than stalling a money-path build for tens of seconds.
23
+ * Other
20
24
  * 4xx statuses (401/400/403/404/…) are NOT retried — auth/bad-request
21
25
  * failures are deterministic, so that `Response` (`ok: false`) is handed
22
26
  * back on the first attempt for the caller to format its own
@@ -46,7 +50,7 @@
46
50
  * - Retry worst case: with the defaults (15s timeout × 3 attempts + ~0.75s of
47
51
  * backoff between them) a FULL outage takes up to ~46s to surface as a
48
52
  * `FetchPolicyError`, vs ~15s pre-3.2.0's single bare-`fetch` attempt.
49
- * Tunable per client via `config.pyth.fetch.{timeoutMs,retries}`.
53
+ * Tunable per client via the `pythFetch` create option (`{timeoutMs,retries}`).
50
54
  */
51
55
  export interface FetchPolicy {
52
56
  /** Per-attempt timeout (ms). Default 15_000. */
@@ -75,6 +79,32 @@ export declare class FetchPolicyError extends Error {
75
79
  attempts: number;
76
80
  });
77
81
  }
82
+ /**
83
+ * Rethrow a `catch`-ed {@link fetchWithPolicy} failure. When it is a
84
+ * status-carrying `FetchPolicyError` — a retryable status (429/5xx) that never
85
+ * recovered — throw a new Error `${describe(err)} (retries exhausted after N
86
+ * attempts)` with the original as `cause`; otherwise (a network-level
87
+ * exhaustion with no status, or any non-`FetchPolicyError`) rethrow it verbatim,
88
+ * since there is no domain reframing to add. `describe` builds the
89
+ * status-bearing prefix so each caller keeps its own message shape (the e2e
90
+ * transient detector keys off those prefixes) while the guard, the `retries
91
+ * exhausted` suffix, and the `cause` wrapping live in one place. `: never` so a
92
+ * caller's `catch` block is understood not to fall through.
93
+ */
94
+ export declare function rethrowExhaustedFetch(err: unknown, describe: (err: FetchPolicyError) => string): never;
95
+ /**
96
+ * Join an API `path` onto an `endpoint` PRESERVING the endpoint's own base
97
+ * path. `new URL(path, endpoint)` is the footgun this replaces: a
98
+ * leading-slash path is *absolute* and silently discards the endpoint's path
99
+ * — harmless for a bare-origin endpoint (`https://hermes.pyth.network`) but
100
+ * it dropped the `/hermes` prefix of the Pyth Pro compat endpoint and 404'd
101
+ * every feed (see `fetchPriceFeedsUpdateData`). Every oracle fetch that
102
+ * targets `<endpoint><fixed path>` must build its URL here.
103
+ */
104
+ /** One canonical trailing-slash trim — `joinEndpointPath` (URL building) and
105
+ * `pyth.ts`'s `memoKey` (endpoint identity) must never drift apart on it. */
106
+ export declare function trimTrailingSlashes(endpoint: string): string;
107
+ export declare function joinEndpointPath(endpoint: string, path: string): URL;
78
108
  /**
79
109
  * `fetch` with per-attempt timeout, bounded retry + backoff, and optional
80
110
  * Bearer auth. See the module header for the full policy. Both `init.signal`