@waterx/sdk 4.3.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.claude/skills/waterx-sdk-integration/SKILL.md +41 -35
  2. package/README.md +81 -77
  3. package/dist/cjs/src/account/config.d.ts +3 -5
  4. package/dist/cjs/src/account/funding/wormhole.d.ts +1 -2
  5. package/dist/cjs/src/account/funding/wormhole.js +1 -2
  6. package/dist/cjs/src/oracle/aggregate.d.ts +45 -61
  7. package/dist/cjs/src/oracle/aggregate.js +138 -138
  8. package/dist/cjs/src/oracle/config.d.ts +35 -49
  9. package/dist/cjs/src/oracle/config.js +1 -1
  10. package/dist/cjs/src/oracle/host.d.ts +14 -19
  11. package/dist/cjs/src/oracle/host.js +3 -3
  12. package/dist/cjs/src/oracle/index.d.ts +25 -16
  13. package/dist/cjs/src/oracle/index.js +79 -66
  14. package/dist/cjs/src/oracle/price-update-rule.d.ts +104 -66
  15. package/dist/cjs/src/oracle/price-update-rule.js +24 -16
  16. package/dist/cjs/src/oracle/pyth-pro-history.d.ts +36 -0
  17. package/dist/cjs/src/oracle/pyth-pro-history.js +41 -0
  18. package/dist/cjs/src/oracle/read-plane.d.ts +35 -53
  19. package/dist/cjs/src/oracle/read-plane.js +26 -48
  20. package/dist/cjs/src/oracle/read-prices.d.ts +105 -0
  21. package/dist/cjs/src/oracle/read-prices.js +162 -0
  22. package/dist/cjs/src/oracle/rule-registry.d.ts +12 -12
  23. package/dist/cjs/src/oracle/rule-registry.js +12 -14
  24. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
  25. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +40 -20
  26. package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +137 -12
  27. package/dist/cjs/src/oracle/rules/waterx-rule.js +318 -55
  28. package/dist/cjs/src/oracle/schedule.d.ts +109 -0
  29. package/dist/cjs/src/oracle/schedule.js +991 -0
  30. package/dist/cjs/src/oracle/source-list.d.ts +31 -27
  31. package/dist/cjs/src/oracle/source-list.js +43 -45
  32. package/dist/cjs/src/oracle/symbol-catalog.d.ts +54 -0
  33. package/dist/cjs/src/oracle/symbol-catalog.js +69 -0
  34. package/dist/cjs/src/oracle/update-fetch.d.ts +11 -15
  35. package/dist/cjs/src/oracle/update-fetch.js +12 -19
  36. package/dist/cjs/src/oracle/validate.d.ts +105 -0
  37. package/dist/cjs/src/oracle/validate.js +185 -0
  38. package/dist/cjs/src/oracle/weight-coverage.d.ts +79 -0
  39. package/dist/cjs/src/oracle/weight-coverage.js +173 -0
  40. package/dist/cjs/src/perp/client.d.ts +35 -52
  41. package/dist/cjs/src/perp/client.js +44 -17
  42. package/dist/cjs/src/perp/config-view.d.ts +5 -14
  43. package/dist/cjs/src/perp/config-view.js +5 -16
  44. package/dist/cjs/src/perp/config.d.ts +3 -3
  45. package/dist/cjs/src/perp/config.js +11 -13
  46. package/dist/cjs/src/perp/index.d.ts +4 -6
  47. package/dist/cjs/src/perp/index.js +35 -18
  48. package/dist/cjs/src/perp/tx-builders/common.d.ts +86 -48
  49. package/dist/cjs/src/perp/tx-builders/common.js +115 -47
  50. package/dist/cjs/src/perp/tx-builders/wlp.d.ts +3 -14
  51. package/dist/cjs/src/perp/tx-builders/wlp.js +6 -35
  52. package/dist/cjs/src/perp/tx-builders.d.ts +6 -5
  53. package/dist/cjs/src/perp/tx-builders.js +16 -10
  54. package/dist/cjs/src/unified-client.d.ts +8 -34
  55. package/dist/cjs/src/unified-client.js +3 -4
  56. package/dist/cjs/src/utils/config.d.ts +0 -8
  57. package/dist/cjs/src/utils/config.js +5 -13
  58. package/dist/src/account/config.d.ts +3 -5
  59. package/dist/src/account/funding/wormhole.d.ts +1 -2
  60. package/dist/src/account/funding/wormhole.js +1 -2
  61. package/dist/src/oracle/aggregate.d.ts +45 -61
  62. package/dist/src/oracle/aggregate.js +138 -137
  63. package/dist/src/oracle/config.d.ts +35 -49
  64. package/dist/src/oracle/config.js +1 -1
  65. package/dist/src/oracle/host.d.ts +14 -19
  66. package/dist/src/oracle/host.js +3 -3
  67. package/dist/src/oracle/index.d.ts +25 -16
  68. package/dist/src/oracle/index.js +55 -50
  69. package/dist/src/oracle/price-update-rule.d.ts +104 -66
  70. package/dist/src/oracle/price-update-rule.js +23 -16
  71. package/dist/src/oracle/pyth-pro-history.d.ts +36 -0
  72. package/dist/src/oracle/pyth-pro-history.js +38 -0
  73. package/dist/src/oracle/read-plane.d.ts +35 -53
  74. package/dist/src/oracle/read-plane.js +25 -47
  75. package/dist/src/oracle/read-prices.d.ts +105 -0
  76. package/dist/src/oracle/read-prices.js +156 -0
  77. package/dist/src/oracle/rule-registry.d.ts +12 -12
  78. package/dist/src/oracle/rule-registry.js +12 -14
  79. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
  80. package/dist/src/oracle/rules/pyth-lazer-rule.js +39 -20
  81. package/dist/src/oracle/rules/waterx-rule.d.ts +137 -12
  82. package/dist/src/oracle/rules/waterx-rule.js +314 -56
  83. package/dist/src/oracle/schedule.d.ts +109 -0
  84. package/dist/src/oracle/schedule.js +985 -0
  85. package/dist/src/oracle/source-list.d.ts +31 -27
  86. package/dist/src/oracle/source-list.js +42 -43
  87. package/dist/src/oracle/symbol-catalog.d.ts +54 -0
  88. package/dist/src/oracle/symbol-catalog.js +65 -0
  89. package/dist/src/oracle/update-fetch.d.ts +11 -15
  90. package/dist/src/oracle/update-fetch.js +12 -18
  91. package/dist/src/oracle/validate.d.ts +105 -0
  92. package/dist/src/oracle/validate.js +177 -0
  93. package/dist/src/oracle/weight-coverage.d.ts +79 -0
  94. package/dist/src/oracle/weight-coverage.js +166 -0
  95. package/dist/src/perp/client.d.ts +35 -52
  96. package/dist/src/perp/client.js +45 -18
  97. package/dist/src/perp/config-view.d.ts +5 -14
  98. package/dist/src/perp/config-view.js +5 -16
  99. package/dist/src/perp/config.d.ts +3 -3
  100. package/dist/src/perp/config.js +11 -13
  101. package/dist/src/perp/index.d.ts +4 -6
  102. package/dist/src/perp/index.js +11 -5
  103. package/dist/src/perp/tx-builders/common.d.ts +86 -48
  104. package/dist/src/perp/tx-builders/common.js +114 -48
  105. package/dist/src/perp/tx-builders/wlp.d.ts +3 -14
  106. package/dist/src/perp/tx-builders/wlp.js +6 -35
  107. package/dist/src/perp/tx-builders.d.ts +6 -5
  108. package/dist/src/perp/tx-builders.js +15 -6
  109. package/dist/src/unified-client.d.ts +8 -34
  110. package/dist/src/unified-client.js +3 -4
  111. package/dist/src/utils/config.d.ts +0 -8
  112. package/dist/src/utils/config.js +5 -12
  113. package/package.json +1 -1
  114. package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
  115. package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -17
  116. package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
  117. package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -113
  118. package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
  119. package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -31
  120. package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
  121. package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.js +0 -214
  122. package/dist/cjs/src/oracle/pyth.d.ts +0 -227
  123. package/dist/cjs/src/oracle/pyth.js +0 -652
  124. package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +0 -15
  125. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +0 -100
  126. package/dist/cjs/src/oracle/rules/pyth-rule.d.ts +0 -11
  127. package/dist/cjs/src/oracle/rules/pyth-rule.js +0 -29
  128. package/dist/cjs/src/oracle/rules/sponsor.d.ts +0 -32
  129. package/dist/cjs/src/oracle/rules/sponsor.js +0 -56
  130. package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
  131. package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -14
  132. package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
  133. package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -73
  134. package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
  135. package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -27
  136. package/dist/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
  137. package/dist/src/generated/waterx_pyth_rule/pyth_rule.js +0 -170
  138. package/dist/src/oracle/pyth.d.ts +0 -227
  139. package/dist/src/oracle/pyth.js +0 -638
  140. package/dist/src/oracle/rules/pyth-core-rule.d.ts +0 -15
  141. package/dist/src/oracle/rules/pyth-core-rule.js +0 -97
  142. package/dist/src/oracle/rules/pyth-rule.d.ts +0 -11
  143. package/dist/src/oracle/rules/pyth-rule.js +0 -26
  144. package/dist/src/oracle/rules/sponsor.d.ts +0 -32
  145. package/dist/src/oracle/rules/sponsor.js +0 -52
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `read-prices.ts` — the READ-plane executors for the two live sources: parsed
3
+ * (display-grade) prices from the Lazer HTTP API and the WaterX quote-center.
4
+ * The sibling `read-plane.ts` resolves WHICH tickers a source can price and
5
+ * with which ids; these functions execute that plan. Consumers (FE/BE price
6
+ * facades) fold onto these instead of hand-rolling per-source fetch + decode —
7
+ * the price DECODING here is the one place each source's wire scaling is
8
+ * interpreted (the retired hermes decode was deleted, not re-homed).
9
+ *
10
+ * Both executors return a Map keyed the way the plan is keyed (integer Lazer
11
+ * feed id / ticker) of {@link OraclePriceEntry} — plain numbers, for display
12
+ * and freshness policy, NEVER for rebuilding signed payloads (tx-builds carry
13
+ * the exact signed bytes through `refreshOraclePrices`).
14
+ */
15
+ import type { Network } from "../constants.ts";
16
+ import type { PythFetchPolicy } from "./config.ts";
17
+ import type { FetchPolicy } from "./update-fetch.ts";
18
+ /**
19
+ * One decoded read-plane price. `price` / `conf` are display-grade floats in
20
+ * quote units (USD); `publishTimeMs` is the feed's own publish timestamp in
21
+ * ms — the input to freshness policy (`isFreshWaterxEntry`, consumers'
22
+ * max-age gates).
23
+ */
24
+ export type OraclePriceEntry = {
25
+ price: number;
26
+ publishTimeMs: number;
27
+ conf: number;
28
+ };
29
+ /**
30
+ * Thrown by {@link readLazerPrices} on a Lazer `403` — the key's grant does
31
+ * not cover one of the requested feeds (Lazer rejects the WHOLE batch). The
32
+ * message carries the endpoint's own PLAIN-TEXT body verbatim (e.g.
33
+ * `Not entitled: feed 327 (no grant accepts this feed (asset type 'fx',
34
+ * instrument type 'spot'))`) — it names the offending feed and grant reason
35
+ * better than anything the SDK could synthesize. `instanceof`-able so a
36
+ * consumer can drop unentitled feeds and retry rather than string-matching.
37
+ */
38
+ export declare class LazerNotEntitledError extends Error {
39
+ /** The integer Lazer feed ids the rejected request asked for. */
40
+ readonly feedIds: number[];
41
+ constructor(body: string, feedIds: number[]);
42
+ }
43
+ /**
44
+ * Read parsed prices for `feedIds` (integer Lazer ids — the `"lazer"` arm of
45
+ * `resolveOracleReadPlan`) via `POST /v1/latest_price`.
46
+ *
47
+ * - `network` is REQUIRED: it selects the Lazer endpoint AND the channel, and
48
+ * the integer feed ids in a plan are network-specific, so defaulting it
49
+ * would silently read mainnet infra with testnet ids. Every caller already
50
+ * holds one (`resolveOracleReadPlan` takes the same host).
51
+ * - `channel` defaults to `LAZER_INFRA[network].channel` (the same channel
52
+ * the write leg uses — bounded by the feeds' `min_channel` AND the key's
53
+ * grant). A `400` naming an unsupported/rate-limited channel propagates as
54
+ * a plain error with the body attached — that is an operator
55
+ * misconfiguration, not a retry case.
56
+ * - Decoding: `price = Number(price) * 10 ** exponent` (the wire `price` is a
57
+ * decimal STRING), `conf = Number(confidence) * 10 ** exponent`,
58
+ * `publishTimeMs = feedUpdateTimestamp / 1000` (the wire value is µs). An
59
+ * entry with NO `feedUpdateTimestamp` is SKIPPED — never timestamped off
60
+ * the response's batch `timestampUs`, which says when Lazer answered, not
61
+ * when that feed last printed (a closed-market feed would masquerade as
62
+ * fresh).
63
+ * - `403` → {@link LazerNotEntitledError} (whole batch rejected; body text
64
+ * verbatim).
65
+ */
66
+ export declare function readLazerPrices(opts: {
67
+ apiKey: string;
68
+ feedIds: number[];
69
+ network: Network;
70
+ channel?: string;
71
+ fetch?: PythFetchPolicy;
72
+ }): Promise<Map<number, OraclePriceEntry>>;
73
+ /**
74
+ * Read prices for `tickers` (the `"quote_center"` arm of
75
+ * `resolveOracleReadPlan`) through {@link pullWaterxQuotes}, the rule-owned
76
+ * route ladder the write path uses: per-symbol Merkle leaves by default, the
77
+ * batch envelope only against a quote-center with no leaf route. Public read,
78
+ * no auth, same retry/timeout policy as every oracle fetch.
79
+ *
80
+ * Going through the shared ladder is what keeps the read plane from drifting
81
+ * off the write plane, and it is why reads do not pull a whole-registry
82
+ * envelope: that is one signature over every symbol, so a plane polling a
83
+ * handful of them would transfer and bigint-revive the lot on every tick.
84
+ *
85
+ * Decoding per item: `price = Number(price_n) / Number(price_scale)` and
86
+ * `conf = Number(confidence_n) / Number(confidence_scale)`, each `0` when its
87
+ * scale is `0` (a zero divisor is "no value", not `Infinity`);
88
+ * `publishTimeMs = Number(price_timestamp_ms)`. Reads share the write path's
89
+ * fetch — a mispointed endpoint fails the same intent/shape checks tx-builds
90
+ * fail — but this NEVER hands its signed data to a tx-build: the on-chain
91
+ * per-symbol replay guard (F-014) burns one submission per signed timestamp,
92
+ * and reads must not race trades for it.
93
+ */
94
+ export declare function readQuoteCenterPrices(opts: {
95
+ endpoint: string;
96
+ /**
97
+ * Oracle TICKERS, per the repo convention — this is the consumer boundary,
98
+ * fed directly by `readPlanTickers(plan)`, and naming the same values
99
+ * `symbols` here made every call site cross a rename. Below this point the
100
+ * wire helpers keep `symbols`: that mirrors the literal `?symbols=` query
101
+ * parameter and the `symbol` field the quote-center returns.
102
+ */
103
+ tickers: string[];
104
+ fetch?: FetchPolicy;
105
+ }): Promise<Map<string, OraclePriceEntry>>;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * `read-prices.ts` — the READ-plane executors for the two live sources: parsed
3
+ * (display-grade) prices from the Lazer HTTP API and the WaterX quote-center.
4
+ * The sibling `read-plane.ts` resolves WHICH tickers a source can price and
5
+ * with which ids; these functions execute that plan. Consumers (FE/BE price
6
+ * facades) fold onto these instead of hand-rolling per-source fetch + decode —
7
+ * the price DECODING here is the one place each source's wire scaling is
8
+ * interpreted (the retired hermes decode was deleted, not re-homed).
9
+ *
10
+ * Both executors return a Map keyed the way the plan is keyed (integer Lazer
11
+ * feed id / ticker) of {@link OraclePriceEntry} — plain numbers, for display
12
+ * and freshness policy, NEVER for rebuilding signed payloads (tx-builds carry
13
+ * the exact signed bytes through `refreshOraclePrices`).
14
+ */
15
+ import { LAZER_INFRA, postLazerLatestPrice } from "./rules/pyth-lazer-rule.js";
16
+ import { pullWaterxQuotes } from "./rules/waterx-rule.js";
17
+ /**
18
+ * Thrown by {@link readLazerPrices} on a Lazer `403` — the key's grant does
19
+ * not cover one of the requested feeds (Lazer rejects the WHOLE batch). The
20
+ * message carries the endpoint's own PLAIN-TEXT body verbatim (e.g.
21
+ * `Not entitled: feed 327 (no grant accepts this feed (asset type 'fx',
22
+ * instrument type 'spot'))`) — it names the offending feed and grant reason
23
+ * better than anything the SDK could synthesize. `instanceof`-able so a
24
+ * consumer can drop unentitled feeds and retry rather than string-matching.
25
+ */
26
+ export class LazerNotEntitledError extends Error {
27
+ /** The integer Lazer feed ids the rejected request asked for. */
28
+ feedIds;
29
+ constructor(body, feedIds) {
30
+ super(`Lazer price read not entitled: ${body}`);
31
+ this.name = "LazerNotEntitledError";
32
+ this.feedIds = [...feedIds];
33
+ }
34
+ }
35
+ /**
36
+ * The parsed-read request pins, mirroring the live-probed Lazer contract:
37
+ * `formats: []` is REQUIRED — an ABSENT `formats` is rejected while an empty
38
+ * array means "parsed only, no signed blob", which is exactly what a read
39
+ * plane wants (no signature bytes to pay bandwidth for). `properties` pins
40
+ * the fields the decoder below consumes; `marketSession` rides along for
41
+ * consumers that surface venue state.
42
+ */
43
+ const LAZER_PARSED_READ_REQUEST = {
44
+ properties: ["price", "exponent", "confidence", "feedUpdateTimestamp", "marketSession"],
45
+ formats: [],
46
+ parsed: true,
47
+ };
48
+ /**
49
+ * Read parsed prices for `feedIds` (integer Lazer ids — the `"lazer"` arm of
50
+ * `resolveOracleReadPlan`) via `POST /v1/latest_price`.
51
+ *
52
+ * - `network` is REQUIRED: it selects the Lazer endpoint AND the channel, and
53
+ * the integer feed ids in a plan are network-specific, so defaulting it
54
+ * would silently read mainnet infra with testnet ids. Every caller already
55
+ * holds one (`resolveOracleReadPlan` takes the same host).
56
+ * - `channel` defaults to `LAZER_INFRA[network].channel` (the same channel
57
+ * the write leg uses — bounded by the feeds' `min_channel` AND the key's
58
+ * grant). A `400` naming an unsupported/rate-limited channel propagates as
59
+ * a plain error with the body attached — that is an operator
60
+ * misconfiguration, not a retry case.
61
+ * - Decoding: `price = Number(price) * 10 ** exponent` (the wire `price` is a
62
+ * decimal STRING), `conf = Number(confidence) * 10 ** exponent`,
63
+ * `publishTimeMs = feedUpdateTimestamp / 1000` (the wire value is µs). An
64
+ * entry with NO `feedUpdateTimestamp` is SKIPPED — never timestamped off
65
+ * the response's batch `timestampUs`, which says when Lazer answered, not
66
+ * when that feed last printed (a closed-market feed would masquerade as
67
+ * fresh).
68
+ * - `403` → {@link LazerNotEntitledError} (whole batch rejected; body text
69
+ * verbatim).
70
+ */
71
+ export async function readLazerPrices(opts) {
72
+ const out = new Map();
73
+ if (opts.feedIds.length === 0)
74
+ return out;
75
+ const infra = LAZER_INFRA[opts.network];
76
+ // Same POST transport the rule's write leg uses (URL join, method, headers,
77
+ // Bearer + retry policy) — only the request pins and the decoding below are
78
+ // read-specific.
79
+ const res = await postLazerLatestPrice(infra.endpoint, opts.channel ?? infra.channel, opts.apiKey, opts.feedIds, LAZER_PARSED_READ_REQUEST, opts.fetch);
80
+ if (res.status === 403) {
81
+ // Plain-text body naming the unentitled feed + grant reason — verbatim.
82
+ throw new LazerNotEntitledError((await res.text()).trim(), opts.feedIds);
83
+ }
84
+ if (!res.ok) {
85
+ // Includes the 400 channel rejections ("Feeds do not support channel …" /
86
+ // rate-limit minimum-channel) — deterministic misconfigurations that must
87
+ // reach the operator with the endpoint's own wording.
88
+ throw new Error(`Lazer price read failed: ${res.status} ${await res.text()}`);
89
+ }
90
+ const json = (await res.json());
91
+ for (const feed of json.parsed?.priceFeeds ?? []) {
92
+ if (typeof feed.priceFeedId !== "number" ||
93
+ feed.price === undefined ||
94
+ typeof feed.exponent !== "number" ||
95
+ // No per-feed timestamp ⇒ no honest freshness claim ⇒ skip (see doc).
96
+ typeof feed.feedUpdateTimestamp !== "number") {
97
+ continue;
98
+ }
99
+ const scale = 10 ** feed.exponent;
100
+ const price = Number(feed.price) * scale;
101
+ const conf = typeof feed.confidence === "number" ? feed.confidence * scale : 0;
102
+ // A non-finite decode is a MISSING price, not a price. `Number("abc")` is
103
+ // NaN and an absurd exponent overflows to Infinity; either would flow
104
+ // straight into a consumer's staleness/deviation maths, where NaN silently
105
+ // fails every comparison it appears in. Skipping matches how a feed with no
106
+ // timestamp is handled above — omitted, so the caller sees it as unserved.
107
+ if (!Number.isFinite(price) || !Number.isFinite(conf))
108
+ continue;
109
+ out.set(feed.priceFeedId, {
110
+ price,
111
+ conf,
112
+ publishTimeMs: feed.feedUpdateTimestamp / 1000,
113
+ });
114
+ }
115
+ return out;
116
+ }
117
+ /**
118
+ * Read prices for `tickers` (the `"quote_center"` arm of
119
+ * `resolveOracleReadPlan`) through {@link pullWaterxQuotes}, the rule-owned
120
+ * route ladder the write path uses: per-symbol Merkle leaves by default, the
121
+ * batch envelope only against a quote-center with no leaf route. Public read,
122
+ * no auth, same retry/timeout policy as every oracle fetch.
123
+ *
124
+ * Going through the shared ladder is what keeps the read plane from drifting
125
+ * off the write plane, and it is why reads do not pull a whole-registry
126
+ * envelope: that is one signature over every symbol, so a plane polling a
127
+ * handful of them would transfer and bigint-revive the lot on every tick.
128
+ *
129
+ * Decoding per item: `price = Number(price_n) / Number(price_scale)` and
130
+ * `conf = Number(confidence_n) / Number(confidence_scale)`, each `0` when its
131
+ * scale is `0` (a zero divisor is "no value", not `Infinity`);
132
+ * `publishTimeMs = Number(price_timestamp_ms)`. Reads share the write path's
133
+ * fetch — a mispointed endpoint fails the same intent/shape checks tx-builds
134
+ * fail — but this NEVER hands its signed data to a tx-build: the on-chain
135
+ * per-symbol replay guard (F-014) burns one submission per signed timestamp,
136
+ * and reads must not race trades for it.
137
+ */
138
+ export async function readQuoteCenterPrices(opts) {
139
+ const out = new Map();
140
+ if (opts.tickers.length === 0)
141
+ return out;
142
+ const { items } = await pullWaterxQuotes(opts.endpoint, opts.tickers, opts.fetch);
143
+ const requested = new Set(opts.tickers);
144
+ for (const item of items) {
145
+ if (!requested.has(item.symbol))
146
+ continue;
147
+ out.set(item.symbol, {
148
+ price: item.price_scale === 0n ? 0 : Number(item.price_n) / Number(item.price_scale),
149
+ conf: item.confidence_scale === 0n
150
+ ? 0
151
+ : Number(item.confidence_n) / Number(item.confidence_scale),
152
+ publishTimeMs: Number(item.price_timestamp_ms),
153
+ });
154
+ }
155
+ return out;
156
+ }
@@ -5,26 +5,26 @@
5
5
  * every `host.oracleSources` entry through it, and consumers (e.g. a BE
6
6
  * prefetch cache keying per source) import `resolveOracleRule` rather than
7
7
  * hand-mirroring this map. Selection is driven purely by the value passed in
8
- * (ultimately `OracleHost.oracleSources`, the `oracleSource` client create
8
+ * (ultimately `OracleHost.oracleSources`, derived from the deployment config
9
9
  * option) — never by a config JSON `enabled` flag and never by `process.env`.
10
10
  *
11
11
  * Each source is self-contained: it owns its own infra + config and does NOT
12
- * back-stop any other source. Three are registered: `pyth_rule`
13
- * (`PythCoreRule`, Hermes VAA), `pyth_lazer_rule` (`PythLazerRule`, Lazer
14
- * signed updates), and `waterx_rule` (`WaterxRule`, first-party quote-center
15
- * ed25519 signed batches). Resolving a source with no registered rule throws a
16
- * clear `OracleSourceNotImplemented` error. There is deliberately no
17
- * cross-source fallback and no client-creation config guard: selecting a source
18
- * whose feeds are absent is not an error at init it surfaces at tx-build time
19
- * for the specific tickers that source can't serve (see `refreshOraclePrices`).
12
+ * back-stop any other source. Two are registered: `pyth_lazer_rule`
13
+ * (`PythLazerRule`, Lazer signed updates) and `waterx_rule` (`WaterxRule`,
14
+ * first-party quote-center ed25519 signed batches). Resolving a source with
15
+ * no registered rule throws a clear `OracleSourceNotImplemented` error. There
16
+ * is deliberately no cross-source fallback and no client-creation config
17
+ * guard: selecting a source whose feeds are absent is not an error at init —
18
+ * it surfaces at tx-build time for the specific tickers that source can't
19
+ * serve (see `refreshOraclePrices`).
20
20
  */
21
21
  import type { OracleSource, PriceUpdateRule } from "./price-update-rule.ts";
22
22
  /**
23
23
  * Thrown by {@link resolveOracleRule} when `source` has no `PriceUpdateRule`
24
24
  * registered in either the production registry or a test's `overrides` map.
25
- * `instanceof`-able (mirrors `OracleFeeSourceUnavailableError` in `pyth.ts`)
26
- * so a consumer can branch on the failure type directly instead of
27
- * string-matching `error.message`.
25
+ * `instanceof`-able (mirrors `LazerApiKeyMissingError` in
26
+ * `rules/pyth-lazer-rule.ts`) so a consumer can branch on the failure type
27
+ * directly instead of string-matching `error.message`.
28
28
  */
29
29
  export declare class OracleSourceNotImplementedError extends Error {
30
30
  /** The unregistered `OracleSource` that was requested. */
@@ -5,20 +5,19 @@
5
5
  * every `host.oracleSources` entry through it, and consumers (e.g. a BE
6
6
  * prefetch cache keying per source) import `resolveOracleRule` rather than
7
7
  * hand-mirroring this map. Selection is driven purely by the value passed in
8
- * (ultimately `OracleHost.oracleSources`, the `oracleSource` client create
8
+ * (ultimately `OracleHost.oracleSources`, derived from the deployment config
9
9
  * option) — never by a config JSON `enabled` flag and never by `process.env`.
10
10
  *
11
11
  * Each source is self-contained: it owns its own infra + config and does NOT
12
- * back-stop any other source. Three are registered: `pyth_rule`
13
- * (`PythCoreRule`, Hermes VAA), `pyth_lazer_rule` (`PythLazerRule`, Lazer
14
- * signed updates), and `waterx_rule` (`WaterxRule`, first-party quote-center
15
- * ed25519 signed batches). Resolving a source with no registered rule throws a
16
- * clear `OracleSourceNotImplemented` error. There is deliberately no
17
- * cross-source fallback and no client-creation config guard: selecting a source
18
- * whose feeds are absent is not an error at init it surfaces at tx-build time
19
- * for the specific tickers that source can't serve (see `refreshOraclePrices`).
12
+ * back-stop any other source. Two are registered: `pyth_lazer_rule`
13
+ * (`PythLazerRule`, Lazer signed updates) and `waterx_rule` (`WaterxRule`,
14
+ * first-party quote-center ed25519 signed batches). Resolving a source with
15
+ * no registered rule throws a clear `OracleSourceNotImplemented` error. There
16
+ * is deliberately no cross-source fallback and no client-creation config
17
+ * guard: selecting a source whose feeds are absent is not an error at init —
18
+ * it surfaces at tx-build time for the specific tickers that source can't
19
+ * serve (see `refreshOraclePrices`).
20
20
  */
21
- import { PythCoreRule } from "./rules/pyth-core-rule.js";
22
21
  import { PythLazerRule } from "./rules/pyth-lazer-rule.js";
23
22
  import { WaterxRule } from "./rules/waterx-rule.js";
24
23
  /**
@@ -26,16 +25,15 @@ import { WaterxRule } from "./rules/waterx-rule.js";
26
25
  * `resolveOracleRule`'s `overrides` param instead of mutating this.
27
26
  */
28
27
  const DEFAULT_RULES = Object.freeze({
29
- pyth_rule: PythCoreRule,
30
28
  pyth_lazer_rule: PythLazerRule,
31
29
  waterx_rule: WaterxRule,
32
30
  });
33
31
  /**
34
32
  * Thrown by {@link resolveOracleRule} when `source` has no `PriceUpdateRule`
35
33
  * registered in either the production registry or a test's `overrides` map.
36
- * `instanceof`-able (mirrors `OracleFeeSourceUnavailableError` in `pyth.ts`)
37
- * so a consumer can branch on the failure type directly instead of
38
- * string-matching `error.message`.
34
+ * `instanceof`-able (mirrors `LazerApiKeyMissingError` in
35
+ * `rules/pyth-lazer-rule.ts`) so a consumer can branch on the failure type
36
+ * directly instead of string-matching `error.message`.
39
37
  */
40
38
  export class OracleSourceNotImplementedError extends Error {
41
39
  /** The unregistered `OracleSource` that was requested. */
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
12
12
  import type { Network } from "../../constants.ts";
13
+ import type { PythFetchPolicy } from "../config.ts";
13
14
  import type { OracleHost } from "../host.ts";
14
15
  import { type PriceUpdateRule } from "../price-update-rule.ts";
15
16
  /**
@@ -60,13 +61,33 @@ export interface PythLazerUpdatePayload {
60
61
  * Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
61
62
  * deployed in config but no `pythApiKey` was supplied at client init — the
62
63
  * Lazer HTTP API requires a Bearer token and the SDK never reads
63
- * `process.env` to find one. `instanceof`-able (mirrors
64
- * `OracleFeeSourceUnavailableError` in `pyth.ts`) so a consumer can branch on
65
- * the failure type directly instead of string-matching `error.message`.
64
+ * `process.env` to find one. Also thrown by `refreshOraclePrices`'s hoisted
65
+ * credential pre-check (`aggregate.ts`) BEFORE any fetch, keyed off this
66
+ * rule's `credential` declaration. `instanceof`-able (mirrors `FetchPolicyError`
67
+ * in `../update-fetch.ts`) so a consumer can branch on the failure type
68
+ * directly instead of string-matching `error.message`.
66
69
  */
67
70
  export declare class LazerApiKeyMissingError extends Error {
68
71
  constructor();
69
72
  }
73
+ /**
74
+ * THE `POST {endpoint}/v1/latest_price` transport — shared by BOTH legs of
75
+ * this endpoint: the signed-update WRITE fetch below and the parsed-price READ
76
+ * executor (`readLazerPrices` in `../read-prices.ts`). They differ only in the
77
+ * request pins they send and how they decode/blame the response, so keeping
78
+ * one transport stops the URL, method, headers, and auth/retry policy drifting
79
+ * between them.
80
+ *
81
+ * `requestPins` is spread into the body alongside `priceFeedIds` + `channel`;
82
+ * the raw `Response` comes back undecoded so each caller owns its own error
83
+ * framing (the write leg reframes an exhausted retry, the read leg maps 403 to
84
+ * an entitlement error).
85
+ *
86
+ * Goes through the shared `fetchWithPolicy` (`../update-fetch.ts`) — the one
87
+ * retry/timeout/Bearer policy every oracle fetch shares, so all sources fail
88
+ * the same way under upstream degradation.
89
+ */
90
+ export declare function postLazerLatestPrice(endpoint: string, channel: string, apiKey: string, feedIds: number[], requestPins: Record<string, unknown>, fetchOpts?: PythFetchPolicy): Promise<Response>;
70
91
  /**
71
92
  * `pyth_lazer_rule::feed(collector, config, clock, &update)` — contribute the
72
93
  * verified Lazer price for `collector.symbol()` to the collector. `update` is
@@ -85,7 +85,7 @@ const LAZER_LATEST_PRICE_REQUEST = {
85
85
  };
86
86
  /**
87
87
  * Shape check ONLY — the `kind` discriminant is checked separately by the
88
- * caller before this runs (mirrors `PythCoreRule`'s guard split), so a
88
+ * caller before this runs (mirrors `WaterxRule`'s guard split), so a
89
89
  * same-shaped payload from a different rule can never silently pass.
90
90
  */
91
91
  function isPythLazerUpdatePayloadShape(payload) {
@@ -98,9 +98,11 @@ function isPythLazerUpdatePayloadShape(payload) {
98
98
  * Thrown by {@link PythLazerRule.fetchUpdateData} when `pyth_lazer_rule` is
99
99
  * deployed in config but no `pythApiKey` was supplied at client init — the
100
100
  * Lazer HTTP API requires a Bearer token and the SDK never reads
101
- * `process.env` to find one. `instanceof`-able (mirrors
102
- * `OracleFeeSourceUnavailableError` in `pyth.ts`) so a consumer can branch on
103
- * the failure type directly instead of string-matching `error.message`.
101
+ * `process.env` to find one. Also thrown by `refreshOraclePrices`'s hoisted
102
+ * credential pre-check (`aggregate.ts`) BEFORE any fetch, keyed off this
103
+ * rule's `credential` declaration. `instanceof`-able (mirrors `FetchPolicyError`
104
+ * in `../update-fetch.ts`) so a consumer can branch on the failure type
105
+ * directly instead of string-matching `error.message`.
104
106
  */
105
107
  export class LazerApiKeyMissingError extends Error {
106
108
  constructor() {
@@ -118,24 +120,39 @@ function requireLazerPackage(host) {
118
120
  return entry;
119
121
  }
120
122
  /**
121
- * Fetch one signed `leEcdsa` update for `feedIds` from the Lazer HTTP API.
122
- * Goes through the shared `fetchWithPolicy` (`../update-fetch.ts`) same
123
- * retry/timeout/Bearer policy as `fetchPriceFeedsUpdateData`, unified so
124
- * both oracle sources fail the same way under upstream degradation.
123
+ * THE `POST {endpoint}/v1/latest_price` transport shared by BOTH legs of
124
+ * this endpoint: the signed-update WRITE fetch below and the parsed-price READ
125
+ * executor (`readLazerPrices` in `../read-prices.ts`). They differ only in the
126
+ * request pins they send and how they decode/blame the response, so keeping
127
+ * one transport stops the URL, method, headers, and auth/retry policy drifting
128
+ * between them.
129
+ *
130
+ * `requestPins` is spread into the body alongside `priceFeedIds` + `channel`;
131
+ * the raw `Response` comes back undecoded so each caller owns its own error
132
+ * framing (the write leg reframes an exhausted retry, the read leg maps 403 to
133
+ * an entitlement error).
134
+ *
135
+ * Goes through the shared `fetchWithPolicy` (`../update-fetch.ts`) — the one
136
+ * retry/timeout/Bearer policy every oracle fetch shares, so all sources fail
137
+ * the same way under upstream degradation.
125
138
  */
126
- async function fetchLazerSignedUpdate(endpoint, channel, apiKey, feedIds, fetchOpts) {
139
+ export function postLazerLatestPrice(endpoint, channel, apiKey, feedIds, requestPins, fetchOpts) {
127
140
  // joinEndpointPath preserves any base path on the endpoint — the same
128
141
  // leading-slash `new URL` footgun that 404'd every feed on the Pyth Pro
129
142
  // Hermes endpoint (see update-fetch.ts). Defensive here: the default
130
143
  // Lazer endpoint has no base path, but a config override may.
131
144
  const url = joinEndpointPath(endpoint, "v1/latest_price");
145
+ return fetchWithPolicy(url.toString(), {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify({ priceFeedIds: feedIds, ...requestPins, channel }),
149
+ }, { apiKey, ...fetchOpts });
150
+ }
151
+ /** Fetch one signed `leEcdsa` update for `feedIds` from the Lazer HTTP API. */
152
+ async function fetchLazerSignedUpdate(endpoint, channel, apiKey, feedIds, fetchOpts) {
132
153
  let res;
133
154
  try {
134
- res = await fetchWithPolicy(url.toString(), {
135
- method: "POST",
136
- headers: { "Content-Type": "application/json" },
137
- body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST, channel }),
138
- }, { apiKey, ...fetchOpts });
155
+ res = await postLazerLatestPrice(endpoint, channel, apiKey, feedIds, LAZER_LATEST_PRICE_REQUEST, fetchOpts);
139
156
  }
140
157
  catch (err) {
141
158
  rethrowExhaustedFetch(err, (e) => `Lazer price fetch failed: ${e.status}${e.bodySnippet ? ` ${e.bodySnippet}` : ""}`);
@@ -167,9 +184,12 @@ export function feedLazerRule(tx, host, collector, update) {
167
184
  }
168
185
  export const PythLazerRule = {
169
186
  kind: "pyth_lazer_rule",
170
- // Verification is a flat signature check with no Coin argument — no
171
- // update feesee `PriceUpdateRule.requiresFeeSource`.
172
- requiresFeeSource: false,
187
+ // Lazer is auth-first: the signed-update fetch cannot run without the
188
+ // caller's `pythApiKey` Bearer. Declared on the port kind AND error
189
+ // together — so `refreshOraclePrices`'s hoisted pre-check and consumers'
190
+ // `missingOracleCredentials` boot asserts both key off the rule itself, and
191
+ // the orchestrator never constructs this error on the rule's behalf.
192
+ credential: { kind: "pyth_api_key", missing: () => new LazerApiKeyMissingError() },
173
193
  /** Tickers with a `pyth_lazer_rule.feeds` entry (integer Lazer feed ids). */
174
194
  supportedTickers(host) {
175
195
  return Object.keys(host.config.packages.pyth_lazer_rule?.feeds ?? {});
@@ -222,14 +242,13 @@ export const PythLazerRule = {
222
242
  * Appends the single `parse_and_verify_le_ecdsa_update*(state, clock, bytes)`
223
243
  * call — one secp256k1 signature check covering every feed in the payload —
224
244
  * and returns its `Update` result as the handle the per-ticker feed leg
225
- * consumes. `opts.cache` / `opts.feeSource` are Pyth-Core-specific and
226
- * ignored (Lazer verification charges no update fee).
245
+ * consumes.
227
246
  *
228
247
  * The entry name comes from `LAZER_INFRA[network].verify_entry`: mainnet's
229
248
  * rule binds `update_v2`, testnet's is still the v1 publish. Both take
230
249
  * `(state, clock, bytes)` and accept the same `leEcdsa` payload.
231
250
  */
232
- buildUpdateCalls(tx, host, data, _opts) {
251
+ buildUpdateCalls(tx, host, data) {
233
252
  const payload = assertRuleUpdateData(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
234
253
  if (!payload)
235
254
  return undefined;