@waterx/sdk 4.3.2 → 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 (150) hide show
  1. package/.claude/skills/waterx-sdk-integration/SKILL.md +225 -0
  2. package/README.md +216 -45
  3. package/SKILLS.md +34 -0
  4. package/dist/cjs/src/account/config.d.ts +3 -5
  5. package/dist/cjs/src/account/funding/wormhole.d.ts +1 -2
  6. package/dist/cjs/src/account/funding/wormhole.js +1 -2
  7. package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +140 -7
  8. package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +163 -8
  9. package/dist/cjs/src/oracle/aggregate.d.ts +49 -62
  10. package/dist/cjs/src/oracle/aggregate.js +208 -164
  11. package/dist/cjs/src/oracle/config.d.ts +38 -51
  12. package/dist/cjs/src/oracle/config.js +1 -1
  13. package/dist/cjs/src/oracle/host.d.ts +14 -19
  14. package/dist/cjs/src/oracle/host.js +3 -3
  15. package/dist/cjs/src/oracle/index.d.ts +25 -16
  16. package/dist/cjs/src/oracle/index.js +87 -69
  17. package/dist/cjs/src/oracle/price-update-rule.d.ts +104 -66
  18. package/dist/cjs/src/oracle/price-update-rule.js +24 -16
  19. package/dist/cjs/src/oracle/pyth-pro-history.d.ts +36 -0
  20. package/dist/cjs/src/oracle/pyth-pro-history.js +41 -0
  21. package/dist/cjs/src/oracle/read-plane.d.ts +35 -53
  22. package/dist/cjs/src/oracle/read-plane.js +26 -48
  23. package/dist/cjs/src/oracle/read-prices.d.ts +105 -0
  24. package/dist/cjs/src/oracle/read-prices.js +162 -0
  25. package/dist/cjs/src/oracle/rule-registry.d.ts +12 -12
  26. package/dist/cjs/src/oracle/rule-registry.js +12 -14
  27. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
  28. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +40 -20
  29. package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +271 -57
  30. package/dist/cjs/src/oracle/rules/waterx-rule.js +743 -144
  31. package/dist/cjs/src/oracle/schedule.d.ts +109 -0
  32. package/dist/cjs/src/oracle/schedule.js +991 -0
  33. package/dist/cjs/src/oracle/source-list.d.ts +31 -27
  34. package/dist/cjs/src/oracle/source-list.js +43 -45
  35. package/dist/cjs/src/oracle/symbol-catalog.d.ts +54 -0
  36. package/dist/cjs/src/oracle/symbol-catalog.js +69 -0
  37. package/dist/cjs/src/oracle/update-fetch.d.ts +11 -15
  38. package/dist/cjs/src/oracle/update-fetch.js +12 -19
  39. package/dist/cjs/src/oracle/validate.d.ts +105 -0
  40. package/dist/cjs/src/oracle/validate.js +185 -0
  41. package/dist/cjs/src/oracle/weight-coverage.d.ts +79 -0
  42. package/dist/cjs/src/oracle/weight-coverage.js +173 -0
  43. package/dist/cjs/src/perp/client.d.ts +40 -55
  44. package/dist/cjs/src/perp/client.js +44 -17
  45. package/dist/cjs/src/perp/config-view.d.ts +5 -14
  46. package/dist/cjs/src/perp/config-view.js +5 -16
  47. package/dist/cjs/src/perp/config.d.ts +3 -3
  48. package/dist/cjs/src/perp/config.js +11 -13
  49. package/dist/cjs/src/perp/index.d.ts +4 -6
  50. package/dist/cjs/src/perp/index.js +43 -18
  51. package/dist/cjs/src/perp/tx-builders/common.d.ts +86 -48
  52. package/dist/cjs/src/perp/tx-builders/common.js +115 -47
  53. package/dist/cjs/src/perp/tx-builders/wlp.d.ts +3 -14
  54. package/dist/cjs/src/perp/tx-builders/wlp.js +6 -35
  55. package/dist/cjs/src/perp/tx-builders.d.ts +6 -5
  56. package/dist/cjs/src/perp/tx-builders.js +16 -10
  57. package/dist/cjs/src/unified-client.d.ts +9 -35
  58. package/dist/cjs/src/unified-client.js +3 -4
  59. package/dist/cjs/src/utils/config.d.ts +0 -8
  60. package/dist/cjs/src/utils/config.js +5 -13
  61. package/dist/src/account/config.d.ts +3 -5
  62. package/dist/src/account/funding/wormhole.d.ts +1 -2
  63. package/dist/src/account/funding/wormhole.js +1 -2
  64. package/dist/src/generated/waterx_rule/waterx_rule.d.ts +140 -7
  65. package/dist/src/generated/waterx_rule/waterx_rule.js +151 -7
  66. package/dist/src/oracle/aggregate.d.ts +49 -62
  67. package/dist/src/oracle/aggregate.js +209 -164
  68. package/dist/src/oracle/config.d.ts +38 -51
  69. package/dist/src/oracle/config.js +1 -1
  70. package/dist/src/oracle/host.d.ts +14 -19
  71. package/dist/src/oracle/host.js +3 -3
  72. package/dist/src/oracle/index.d.ts +25 -16
  73. package/dist/src/oracle/index.js +61 -54
  74. package/dist/src/oracle/price-update-rule.d.ts +104 -66
  75. package/dist/src/oracle/price-update-rule.js +23 -16
  76. package/dist/src/oracle/pyth-pro-history.d.ts +36 -0
  77. package/dist/src/oracle/pyth-pro-history.js +38 -0
  78. package/dist/src/oracle/read-plane.d.ts +35 -53
  79. package/dist/src/oracle/read-plane.js +25 -47
  80. package/dist/src/oracle/read-prices.d.ts +105 -0
  81. package/dist/src/oracle/read-prices.js +156 -0
  82. package/dist/src/oracle/rule-registry.d.ts +12 -12
  83. package/dist/src/oracle/rule-registry.js +12 -14
  84. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
  85. package/dist/src/oracle/rules/pyth-lazer-rule.js +39 -20
  86. package/dist/src/oracle/rules/waterx-rule.d.ts +271 -57
  87. package/dist/src/oracle/rules/waterx-rule.js +735 -144
  88. package/dist/src/oracle/schedule.d.ts +109 -0
  89. package/dist/src/oracle/schedule.js +985 -0
  90. package/dist/src/oracle/source-list.d.ts +31 -27
  91. package/dist/src/oracle/source-list.js +42 -43
  92. package/dist/src/oracle/symbol-catalog.d.ts +54 -0
  93. package/dist/src/oracle/symbol-catalog.js +65 -0
  94. package/dist/src/oracle/update-fetch.d.ts +11 -15
  95. package/dist/src/oracle/update-fetch.js +12 -18
  96. package/dist/src/oracle/validate.d.ts +105 -0
  97. package/dist/src/oracle/validate.js +177 -0
  98. package/dist/src/oracle/weight-coverage.d.ts +79 -0
  99. package/dist/src/oracle/weight-coverage.js +166 -0
  100. package/dist/src/perp/client.d.ts +40 -55
  101. package/dist/src/perp/client.js +45 -18
  102. package/dist/src/perp/config-view.d.ts +5 -14
  103. package/dist/src/perp/config-view.js +5 -16
  104. package/dist/src/perp/config.d.ts +3 -3
  105. package/dist/src/perp/config.js +11 -13
  106. package/dist/src/perp/index.d.ts +4 -6
  107. package/dist/src/perp/index.js +17 -5
  108. package/dist/src/perp/tx-builders/common.d.ts +86 -48
  109. package/dist/src/perp/tx-builders/common.js +114 -48
  110. package/dist/src/perp/tx-builders/wlp.d.ts +3 -14
  111. package/dist/src/perp/tx-builders/wlp.js +6 -35
  112. package/dist/src/perp/tx-builders.d.ts +6 -5
  113. package/dist/src/perp/tx-builders.js +15 -6
  114. package/dist/src/unified-client.d.ts +9 -35
  115. package/dist/src/unified-client.js +3 -4
  116. package/dist/src/utils/config.d.ts +0 -8
  117. package/dist/src/utils/config.js +5 -12
  118. package/package.json +8 -2
  119. package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
  120. package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -17
  121. package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
  122. package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -113
  123. package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
  124. package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -31
  125. package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
  126. package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.js +0 -214
  127. package/dist/cjs/src/oracle/pyth.d.ts +0 -227
  128. package/dist/cjs/src/oracle/pyth.js +0 -652
  129. package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +0 -15
  130. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +0 -100
  131. package/dist/cjs/src/oracle/rules/pyth-rule.d.ts +0 -11
  132. package/dist/cjs/src/oracle/rules/pyth-rule.js +0 -29
  133. package/dist/cjs/src/oracle/rules/sponsor.d.ts +0 -32
  134. package/dist/cjs/src/oracle/rules/sponsor.js +0 -56
  135. package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
  136. package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -14
  137. package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
  138. package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -73
  139. package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
  140. package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -27
  141. package/dist/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
  142. package/dist/src/generated/waterx_pyth_rule/pyth_rule.js +0 -170
  143. package/dist/src/oracle/pyth.d.ts +0 -227
  144. package/dist/src/oracle/pyth.js +0 -638
  145. package/dist/src/oracle/rules/pyth-core-rule.d.ts +0 -15
  146. package/dist/src/oracle/rules/pyth-core-rule.js +0 -97
  147. package/dist/src/oracle/rules/pyth-rule.d.ts +0 -11
  148. package/dist/src/oracle/rules/pyth-rule.js +0 -26
  149. package/dist/src/oracle/rules/sponsor.d.ts +0 -32
  150. package/dist/src/oracle/rules/sponsor.js +0 -52
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ /**
3
+ * `validate.ts` — boot-time oracle-deployment asserts consumers (FE/BE) fold
4
+ * onto instead of each hand-rolling them:
5
+ *
6
+ * - {@link assertOracleWriteCoverage} — the "this fed set can actually price
7
+ * the tickers I care about" guard. Because every source reads through its
8
+ * own feeds namespace, write set == read set — passing this ALSO validates
9
+ * the read plane (there is deliberately no separate read-coverage assert).
10
+ * - {@link missingOracleCredentials} — the env-shaped credential audit: which
11
+ * listed sources cannot run with the credentials this deployment supplied.
12
+ * Boot-time mirror of `refreshOraclePrices`'s own per-build credential
13
+ * pre-check (`aggregate.ts`), keyed off the same rule-owned
14
+ * `credential` declaration so the two can never disagree.
15
+ *
16
+ * Deliberately NOT called by `PerpClient` itself: client creation stays
17
+ * guard-free (a source with absent feeds surfaces at tx-build for exactly the
18
+ * tickers it can't serve). These are for consumers whose deployment policy is
19
+ * "fail the BOOT, not the first trade".
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.OracleTickerUnservedError = void 0;
23
+ exports.assertOracleWriteCoverage = assertOracleWriteCoverage;
24
+ exports.partitionServableTickers = partitionServableTickers;
25
+ exports.servableTickers = servableTickers;
26
+ exports.missingOracleCredentials = missingOracleCredentials;
27
+ const price_update_rule_ts_1 = require("./price-update-rule.js");
28
+ const rule_registry_ts_1 = require("./rule-registry.js");
29
+ /**
30
+ * A build depends on a ticker this client's fed set cannot price.
31
+ *
32
+ * Raised from two places, deliberately the same type: the per-BUILD composers
33
+ * (`refreshOraclePrices` skipped the ticker by design — see
34
+ * `OracleRefreshSummary` — and the composer decided it was load-bearing for
35
+ * the action) and the boot-time {@link assertOracleWriteCoverage}. One error
36
+ * for one question: "this fed set cannot price these tickers".
37
+ */
38
+ class OracleTickerUnservedError extends Error {
39
+ tickers;
40
+ sources;
41
+ constructor(tickers, sources, why) {
42
+ super(`fed set [${sources.join(", ")}] has no feed for ticker(s): ${tickers.join(", ")}. ` +
43
+ (why ?? "This build depends on their prices, so it cannot proceed. ") +
44
+ "Add feeds under a listed source, list a source that serves them, or pass " +
45
+ "allowUnrefreshedPrices: true to build anyway against whatever price the " +
46
+ "chain already holds.");
47
+ this.name = "OracleTickerUnservedError";
48
+ this.tickers = tickers;
49
+ this.sources = sources;
50
+ }
51
+ }
52
+ exports.OracleTickerUnservedError = OracleTickerUnservedError;
53
+ /**
54
+ * Assert this deployment's fed set can price every ticker in `tickers`.
55
+ *
56
+ * Throws {@link OracleTickerUnservedError} naming ALL the unservable ones (not
57
+ * just the first — an operator fixing a config wants the whole list).
58
+ *
59
+ * This is the boot-time twin of the per-build behaviour, and it guards the gap
60
+ * that per-build handling deliberately leaves open: `refreshOraclePrices`
61
+ * SKIPS a ticker no source serves, and only a composer that happens to depend
62
+ * on that ticker turns the skip into an error. A market nobody trades today
63
+ * would therefore stay silently unpriceable until someone did. Pass the ticker
64
+ * set your deployment cares about (typically every market) and find out at
65
+ * boot instead.
66
+ *
67
+ * It does NOT check "every listed source has feeds" any more: the fed set is
68
+ * derived from exactly that condition (see `deriveOracleSources`), so that
69
+ * assert became unreachable for any real client — it could only fire for a
70
+ * config mutated after construction, which is a test fixture, not a
71
+ * deployment. Write set == read set by construction (each source reads its own
72
+ * feeds), so this single assert still covers both planes.
73
+ */
74
+ function assertOracleWriteCoverage(host, tickers) {
75
+ const { unservable } = partitionServableTickers(host, tickers);
76
+ if (unservable.length > 0) {
77
+ throw new OracleTickerUnservedError(unservable, host.oracleSources);
78
+ }
79
+ }
80
+ /** Tickers the client's LISTED sources carry a feed for. */
81
+ function fedSetTickers(host) {
82
+ return new Set(host.oracleSources.flatMap((source) => (0, rule_registry_ts_1.resolveOracleRule)(source).supportedTickers(host)));
83
+ }
84
+ /**
85
+ * Tickers ANY rule in this config carries a feed for — listed or not.
86
+ *
87
+ * The fed set answers "can THIS client price it"; this answers "does the chain
88
+ * plausibly weight a price-update rule for it", which is what decides whether
89
+ * a constant leg alone is the whole picture. `supra_rule` counts (it is a
90
+ * weighted leg even though it is not an `OracleSource`) but only when it is
91
+ * actually wired — an unwired block's feeds map is documented as
92
+ * informational, and honouring a stale one would strand constant-only tickers
93
+ * for no benefit.
94
+ */
95
+ function tickersWithAnySourceFeed(host) {
96
+ const out = new Set();
97
+ for (const source of price_update_rule_ts_1.ORACLE_SOURCES) {
98
+ // Skip a source the deployment DISABLED — routing already honours
99
+ // `enabled: false` (`deriveOracleSources`), and applying it in one place
100
+ // but not the other meant a disabled block's informational feeds
101
+ // disqualified a constant-only ticker that nothing would ever feed. The
102
+ // predicate has to match what actually gets emitted.
103
+ if (host.config.packages[source]?.enabled === false)
104
+ continue;
105
+ for (const ticker of (0, rule_registry_ts_1.resolveOracleRule)(source).supportedTickers(host))
106
+ out.add(ticker);
107
+ }
108
+ // `getSupraRule()` already requires `enabled` — see `config-view.ts`.
109
+ if (host.getSupraRule() !== undefined) {
110
+ for (const ticker of Object.keys(host.config.packages.supra_rule?.feeds ?? {}))
111
+ out.add(ticker);
112
+ }
113
+ return out;
114
+ }
115
+ /**
116
+ * THE acceptance predicate, in partition form — the single definition of
117
+ * "will `refreshOraclePrices` put a price on chain for this ticker".
118
+ *
119
+ * A ticker is servable when some LISTED source's feeds carry it, or when it is
120
+ * CONSTANT-ONLY: `constant_rule` pins it and no other rule in the config feeds
121
+ * it. The stricter constant test matters — a constant-pinned ticker that some
122
+ * other rule also feeds gets aggregated from a constant-only collector, and if
123
+ * the chain weights that other rule the aggregate aborts `EMissingPriceSource`.
124
+ * (`aggregateTicker` only appends a supra leg when a price-update source
125
+ * already fed the collector, so a constant-only collector can never carry one.)
126
+ *
127
+ * `covered` lets a caller that has ALREADY resolved which of its tickers its
128
+ * fed set serves — `refreshOraclePrices`, off its rule groups — pass that in
129
+ * rather than have it recomputed. Both callers therefore share one rule, which
130
+ * is the point: a consumer pre-filtering with {@link servableTickers} cannot
131
+ * hand the build a ticker it will silently skip.
132
+ *
133
+ * Order-preserving. The `anyFeed` set is built at most once per call, and only
134
+ * when a constant-pinned ticker actually needs it.
135
+ */
136
+ function partitionServableTickers(host, tickers, covered) {
137
+ const fed = covered ?? fedSetTickers(host);
138
+ const servable = [];
139
+ const unservable = [];
140
+ let anyFeed;
141
+ for (const ticker of tickers) {
142
+ if (fed.has(ticker)) {
143
+ servable.push(ticker);
144
+ }
145
+ else if (host.isConstantTicker(ticker)) {
146
+ anyFeed ??= tickersWithAnySourceFeed(host);
147
+ (anyFeed.has(ticker) ? unservable : servable).push(ticker);
148
+ }
149
+ else {
150
+ unservable.push(ticker);
151
+ }
152
+ }
153
+ return { servable, unservable };
154
+ }
155
+ /**
156
+ * The subset of `tickers` this deployment's fed set can actually price — the
157
+ * servable half of {@link partitionServableTickers}, which is literally the
158
+ * rule `refreshOraclePrices` applies.
159
+ */
160
+ function servableTickers(host, tickers) {
161
+ return partitionServableTickers(host, tickers).servable;
162
+ }
163
+ /**
164
+ * Which of `sources` cannot run with the supplied credentials — one row per
165
+ * (source, missing credential). Empty array ⇒ the fed set is fully
166
+ * credentialed. Pure and env-shaped on purpose: consumers call it from their
167
+ * boot-time env asserts (zod superRefine, config validators) BEFORE any
168
+ * client exists, passing the raw values their env resolved. The per-build
169
+ * enforcement twin — `refreshOraclePrices`'s credential pre-check — reads the
170
+ * same rule-owned `credential` declaration, so a deployment this function passes
171
+ * cannot later trip that check for a listed source.
172
+ */
173
+ function missingOracleCredentials(sources, creds) {
174
+ // The env-shaped bag, normalized to the kind-keyed shape ONCE — the check
175
+ // below then never mentions a specific kind.
176
+ const supplied = { pyth_api_key: creds.pythApiKey };
177
+ const missing = [];
178
+ for (const source of sources) {
179
+ const required = (0, rule_registry_ts_1.resolveOracleRule)(source).credential?.kind;
180
+ if (required !== undefined && !supplied[required]) {
181
+ missing.push({ source, credential: required });
182
+ }
183
+ }
184
+ return missing;
185
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `weight-coverage.ts` — the ON-CHAIN half of "can this deployment price these
3
+ * tickers", and the only place the fed-set invariant is actually enforced.
4
+ *
5
+ * Everything in `validate.ts` reasons from config alone: it answers "does some
6
+ * listed source carry a feed for this ticker". That is necessary and not
7
+ * sufficient, because the aggregator decides which contributions COUNT. The
8
+ * dangerous shape is a ticker that is servable by config yet weighted to a rule
9
+ * the fed set cannot supply: `refreshOraclePrices` happily aggregates it from a
10
+ * source the chain does not weight, `remove_outliers` finds no weighted
11
+ * contribution, and the abort takes down the WHOLE PTB — every other ticker in
12
+ * the same build with it.
13
+ *
14
+ * The mirror-image shape is just as dangerous and less obvious: a ticker
15
+ * weighted to a rule this client DOES list, but whose feed that source does not
16
+ * carry. Being in the fed set buys nothing per ticker — `refreshOraclePrices`
17
+ * groups a ticker under a source only when that source's feeds have it — so the
18
+ * leg never appears and the weighted rule is starved just the same.
19
+ *
20
+ * That is live on mainnet today, not hypothetical. `XAGUSD` / `WTIUSD` /
21
+ * `BRENTUSD` are in `waterx_rule.feeds` (so: servable) while their aggregators
22
+ * still weight the retired `PythRule@1`, which 5.0.0 cannot feed at all. The
23
+ * config-only asserts wave all three through; only reading the weights catches
24
+ * them.
25
+ *
26
+ * Async and chain-reading, so it is NOT on the build path — it belongs in a
27
+ * deployment's boot sequence or a pre-release check, next to
28
+ * `assertOracleWriteCoverage`.
29
+ */
30
+ import type { OracleHost } from "./host.ts";
31
+ /**
32
+ * Auxiliary witnesses — fed alongside a source rather than being one.
33
+ *
34
+ * Their emission is CONDITIONAL, per ticker, so they cannot be treated as
35
+ * globally available (which certified a ticker clean that then aborted):
36
+ *
37
+ * - `ConstantRule` rides only when `host.isConstantTicker(ticker)`.
38
+ * - `SupraRule` rides only when the deployment has supra wired AND a price
39
+ * -update source already fed that collector — `aggregateTicker` puts the
40
+ * supra leg inside `if (fed)`, so a ticker no listed source serves never
41
+ * gets one, constant-only tickers included.
42
+ */
43
+ /** One ticker's on-chain weighting, as far as fed-set coverage is concerned. */
44
+ export interface TickerWeightCoverage {
45
+ ticker: string;
46
+ /** Weighted rule witness names, as the aggregator holds them. */
47
+ weighted: string[];
48
+ /** Weighted rules this client's fed set cannot feed (retired, or unlisted). */
49
+ unsuppliable: string[];
50
+ }
51
+ /** Raised by {@link assertOracleWeightCoverage}; `rows` names every offender. */
52
+ export declare class OracleWeightCoverageError extends Error {
53
+ readonly rows: TickerWeightCoverage[];
54
+ constructor(rows: TickerWeightCoverage[]);
55
+ }
56
+ /** Raised when an aggregator object cannot be decoded — see `readOracleWeightCoverage`. */
57
+ export declare class OracleWeightUnreadableError extends Error {
58
+ readonly ticker: string;
59
+ constructor(ticker: string, aggregatorId: string, why: string);
60
+ }
61
+ /**
62
+ * Read each ticker's aggregator and report which weighted rules this fed set
63
+ * cannot supply.
64
+ *
65
+ * Tickers with no aggregator entry in the config are skipped, not failed: an
66
+ * unlisted aggregator means the deployment does not price that ticker on chain
67
+ * at all, which {@link assertOracleWriteCoverage} is the right check for.
68
+ */
69
+ export declare function readOracleWeightCoverage(host: OracleHost, tickers: readonly string[]): Promise<TickerWeightCoverage[]>;
70
+ /**
71
+ * Throw {@link OracleWeightCoverageError} when any of `tickers` is weighted to
72
+ * a rule this fed set cannot feed.
73
+ *
74
+ * The chain-reading twin of `assertOracleWriteCoverage`: that one catches a
75
+ * ticker NO source serves (loud at build anyway), this one catches the quieter
76
+ * and more damaging case — a ticker every config check passes, which aborts the
77
+ * entire transaction on chain.
78
+ */
79
+ export declare function assertOracleWeightCoverage(host: OracleHost, tickers: readonly string[]): Promise<void>;
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ /**
3
+ * `weight-coverage.ts` — the ON-CHAIN half of "can this deployment price these
4
+ * tickers", and the only place the fed-set invariant is actually enforced.
5
+ *
6
+ * Everything in `validate.ts` reasons from config alone: it answers "does some
7
+ * listed source carry a feed for this ticker". That is necessary and not
8
+ * sufficient, because the aggregator decides which contributions COUNT. The
9
+ * dangerous shape is a ticker that is servable by config yet weighted to a rule
10
+ * the fed set cannot supply: `refreshOraclePrices` happily aggregates it from a
11
+ * source the chain does not weight, `remove_outliers` finds no weighted
12
+ * contribution, and the abort takes down the WHOLE PTB — every other ticker in
13
+ * the same build with it.
14
+ *
15
+ * The mirror-image shape is just as dangerous and less obvious: a ticker
16
+ * weighted to a rule this client DOES list, but whose feed that source does not
17
+ * carry. Being in the fed set buys nothing per ticker — `refreshOraclePrices`
18
+ * groups a ticker under a source only when that source's feeds have it — so the
19
+ * leg never appears and the weighted rule is starved just the same.
20
+ *
21
+ * That is live on mainnet today, not hypothetical. `XAGUSD` / `WTIUSD` /
22
+ * `BRENTUSD` are in `waterx_rule.feeds` (so: servable) while their aggregators
23
+ * still weight the retired `PythRule@1`, which 5.0.0 cannot feed at all. The
24
+ * config-only asserts wave all three through; only reading the weights catches
25
+ * them.
26
+ *
27
+ * Async and chain-reading, so it is NOT on the build path — it belongs in a
28
+ * deployment's boot sequence or a pre-release check, next to
29
+ * `assertOracleWriteCoverage`.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.OracleWeightUnreadableError = exports.OracleWeightCoverageError = void 0;
33
+ exports.readOracleWeightCoverage = readOracleWeightCoverage;
34
+ exports.assertOracleWeightCoverage = assertOracleWeightCoverage;
35
+ const rule_registry_ts_1 = require("./rule-registry.js");
36
+ /**
37
+ * The on-chain witness struct each SDK source feeds. The aggregator's weight
38
+ * table is keyed by these type names, so this is the join between what the
39
+ * chain requires and what the fed set can supply.
40
+ *
41
+ * `ConstantRule` and `SupraRule` are auxiliary legs rather than sources — the
42
+ * SDK feeds them alongside whichever source ran, so a ticker weighted only to
43
+ * them needs no source. Anything NOT in this map (notably the retired
44
+ * `PythRule`) cannot be supplied by this SDK at any fed set.
45
+ */
46
+ const WITNESS_TO_SOURCE = Object.freeze({
47
+ PythLazerRule: "pyth_lazer_rule",
48
+ WaterxRule: "waterx_rule",
49
+ });
50
+ /** Raised by {@link assertOracleWeightCoverage}; `rows` names every offender. */
51
+ class OracleWeightCoverageError extends Error {
52
+ rows;
53
+ constructor(rows) {
54
+ const detail = rows.map((r) => `${r.ticker} weights ${r.unsuppliable.join(" + ")}`).join("; ");
55
+ super(`on-chain aggregator weights name rule(s) this fed set cannot supply: ${detail}. ` +
56
+ `Aggregating such a ticker emits a collector with no WEIGHTED contribution, so ` +
57
+ `remove_outliers aborts EMissingPriceSource and takes the whole PTB down — every ` +
58
+ `other ticker in the same build with it. Fix by migrating the on-chain weight to a ` +
59
+ `rule this deployment feeds, or by not requesting these tickers.`);
60
+ this.name = "OracleWeightCoverageError";
61
+ this.rows = rows;
62
+ }
63
+ }
64
+ exports.OracleWeightCoverageError = OracleWeightCoverageError;
65
+ /** Raised when an aggregator object cannot be decoded — see `readOracleWeightCoverage`. */
66
+ class OracleWeightUnreadableError extends Error {
67
+ ticker;
68
+ constructor(ticker, aggregatorId, why) {
69
+ super(`cannot read aggregator ${aggregatorId} for ${ticker}: ${why}. This check exists to fail ` +
70
+ `CLOSED, so an undecodable weight table is an error rather than an empty one — treating ` +
71
+ `it as "no weights" would certify every ticker clean without verifying anything.`);
72
+ this.name = "OracleWeightUnreadableError";
73
+ this.ticker = ticker;
74
+ }
75
+ }
76
+ exports.OracleWeightUnreadableError = OracleWeightUnreadableError;
77
+ /** `0x…::aggregator::PythRule` → `PythRule`; already-short names pass through. */
78
+ function witnessName(raw) {
79
+ const s = typeof raw === "string" ? raw : String(raw?.name ?? raw);
80
+ return s.split("::").pop() ?? s;
81
+ }
82
+ /**
83
+ * Read each ticker's aggregator and report which weighted rules this fed set
84
+ * cannot supply.
85
+ *
86
+ * Tickers with no aggregator entry in the config are skipped, not failed: an
87
+ * unlisted aggregator means the deployment does not price that ticker on chain
88
+ * at all, which {@link assertOracleWriteCoverage} is the right check for.
89
+ */
90
+ async function readOracleWeightCoverage(host, tickers) {
91
+ const aggregators = host.config.packages.waterx_oracle.aggregators;
92
+ // Which tickers each LISTED source actually feeds — per source, not merged.
93
+ //
94
+ // Being in the fed set is not enough: `refreshOraclePrices` groups a ticker
95
+ // under a source only when that source's feeds carry it, so a lazer-weighted
96
+ // ticker that only WaterX feeds gets no lazer leg no matter that lazer is
97
+ // listed. Treating a listed source as suppliable for EVERY ticker passed
98
+ // exactly that case, which then aborts `EMissingPriceSource` on chain.
99
+ const feedsByWitness = new Map();
100
+ for (const [witness, source] of Object.entries(WITNESS_TO_SOURCE)) {
101
+ if (host.oracleSources.includes(source)) {
102
+ feedsByWitness.set(witness, new Set((0, rule_registry_ts_1.resolveOracleRule)(source).supportedTickers(host)));
103
+ }
104
+ }
105
+ const supraWired = host.getSupraRule() !== undefined;
106
+ /** Some listed source feeds this ticker, so a collector gets fed at all. */
107
+ const anySourceFeeds = (ticker) => {
108
+ for (const served of feedsByWitness.values())
109
+ if (served.has(ticker))
110
+ return true;
111
+ return false;
112
+ };
113
+ const suppliableFor = (ticker, witness) => {
114
+ const served = feedsByWitness.get(witness);
115
+ // A source witness: suppliable only where that source has THIS ticker's feed.
116
+ if (served !== undefined)
117
+ return served.has(ticker);
118
+ if (witness === "ConstantRule")
119
+ return host.isConstantTicker(ticker);
120
+ // Supra rides on a collector a SOURCE already fed — never on its own, and
121
+ // never on a constant-only collector.
122
+ if (witness === "SupraRule")
123
+ return supraWired && anySourceFeeds(ticker);
124
+ return false;
125
+ };
126
+ const wanted = tickers.filter((t) => Object.hasOwn(aggregators, t));
127
+ // Independent reads — one round trip each would make a 30-market boot assert
128
+ // needlessly serial.
129
+ const objects = await Promise.all(wanted.map((t) =>
130
+ // Explicit field mask: the client's `getObject` wrapper requests none, so
131
+ // `json` would come back undefined and every aggregator would look
132
+ // weightless — an assert that passes exactly where it must fail.
133
+ host.grpcClient.getObject({ objectId: aggregators[t], include: { json: true } })));
134
+ return wanted.map((ticker, i) => {
135
+ // FAIL CLOSED on anything undecodable. Defaulting a missing object / JSON /
136
+ // weights map to an empty list made the assert succeed without verifying a
137
+ // single weight — the exact fail-open shape this gate exists to prevent.
138
+ const id = aggregators[ticker];
139
+ const json = objects[i]?.object?.json;
140
+ if (json === undefined || json === null || typeof json !== "object") {
141
+ throw new OracleWeightUnreadableError(ticker, id, "no JSON payload in the object read");
142
+ }
143
+ const raw = json.weights;
144
+ const entries = Array.isArray(raw) ? raw : raw?.contents;
145
+ if (!Array.isArray(entries)) {
146
+ throw new OracleWeightUnreadableError(ticker, id, "no decodable `weights` table");
147
+ }
148
+ const weighted = entries.map((e) => witnessName(e.key));
149
+ if (weighted.some((w) => w === "" || w === "undefined")) {
150
+ throw new OracleWeightUnreadableError(ticker, id, "a weight entry has no rule type name");
151
+ }
152
+ return {
153
+ ticker,
154
+ weighted,
155
+ unsuppliable: weighted.filter((w) => !suppliableFor(ticker, w)),
156
+ };
157
+ });
158
+ }
159
+ /**
160
+ * Throw {@link OracleWeightCoverageError} when any of `tickers` is weighted to
161
+ * a rule this fed set cannot feed.
162
+ *
163
+ * The chain-reading twin of `assertOracleWriteCoverage`: that one catches a
164
+ * ticker NO source serves (loud at build anyway), this one catches the quieter
165
+ * and more damaging case — a ticker every config check passes, which aborts the
166
+ * entire transaction on chain.
167
+ */
168
+ async function assertOracleWeightCoverage(host, tickers) {
169
+ const rows = await readOracleWeightCoverage(host, tickers);
170
+ const bad = rows.filter((r) => r.unsuppliable.length > 0);
171
+ if (bad.length > 0)
172
+ throw new OracleWeightCoverageError(bad);
173
+ }
@@ -17,70 +17,33 @@ import { type LoadConfigOptions, type PythAccessConfig, type PythFetchPolicy, ty
17
17
  import type { Network } from "./constants.ts";
18
18
  export interface CreateClientOptions extends LoadConfigOptions {
19
19
  grpcUrl?: string;
20
- /**
21
- * Which oracle price-update source drives `refreshOraclePrices`. REQUIRED —
22
- * there is NO default source: every deployment names its source explicitly
23
- * (wire it from your own env var, e.g. `ORACLE_SOURCE`). Each source is
24
- * self-contained (own infra, own endpoints, own config) with NO cross-source
25
- * fallback:
26
- *
27
- * - `'pyth_rule'` — Pyth Core `pyth_rule` updates (Hermes VAA + per-feed
28
- * update fees); Core state + Hermes endpoint live in the source's own
29
- * `PYTH_CORE_INFRA` table.
30
- * - `'pyth_lazer_rule'` — Pyth Lazer signed updates (ONE `leEcdsa` verify
31
- * per PTB, no per-feed fees); needs `packages.pyth_lazer_rule` with feeds
32
- * and a `pythApiKey` (Lazer is auth-first); Lazer infra lives in the
33
- * source's own `LAZER_INFRA` table.
34
- * - `'waterx_rule'` — the first-party WaterX quote-center (Nautilus-TEE,
35
- * ed25519-signed batches): ONE envelope covering the build's tickers,
36
- * verified AND fed by a single `collect_batch_latest` per collector. No
37
- * credential and no per-update fee; needs `packages.waterx_rule` with
38
- * feeds. Quote-center infra lives in the source's own `WATERX_INFRA`
39
- * table; endpoint/transport overridable via
40
- * {@link CreateClientOptions.waterxEndpoint} /
41
- * {@link CreateClientOptions.waterxFetch} — the browser-CORS proxy hook,
42
- * since this is the one source fetched from the page.
43
- *
44
- * The name is source-neutral on purpose — a source need not be Pyth (as
45
- * `'waterx_rule'` shows). Selecting a source whose feed for a requested
46
- * ticker is absent is NOT an error at client creation: it fails at tx-build
47
- * time for exactly those tickers (see `refreshOraclePrices`).
48
- *
49
- * Accepts a SINGLE source or a LIST. A list means every listed source's
50
- * data is fetched and fed in one build — required whenever the on-chain
51
- * weight tables have more than one rule weighted (e.g. a Core→Pro or
52
- * Pro+Waterx coexistence window): the chain drops unweighted contributions
53
- * (harmless) but aborts on a starved weighted rule, so the list must stay a
54
- * SUPERSET of every ticker's weighted set. Order is meaningful to consumers
55
- * (read-plane priority), not to the on-chain build.
56
- */
57
- oracleSource: OracleSource | OracleSource[];
58
20
  /**
59
21
  * Pyth Lazer access token (`Authorization: Bearer …`). Required under
60
- * `oracleSource: 'pyth_lazer_rule'` (Lazer is auth-first) and unused by
61
- * `'pyth_rule'` (keyless Core Hermes). This is a SECRET and never belongs in
62
- * the canonical `waterx-config` JSON — pass it at client init from your own
63
- * env var (e.g. `PYTH_API_KEY`); the SDK never reads `process.env`.
22
+ * a config that wires `pyth_lazer_rule` (Lazer is auth-first) and unused by
23
+ * `waterx_rule` (public quote-center). This is a SECRET and never belongs
24
+ * in the canonical `waterx-config` JSON — pass it at client init from your
25
+ * own env var (e.g. `PYTH_API_KEY`); the SDK never reads `process.env`.
64
26
  */
65
27
  pythApiKey?: string;
66
28
  /**
67
- * Retry/timeout policy for the off-chain Hermes / Lazer update fetches (see
29
+ * Retry/timeout policy for the off-chain Lazer update fetch (see
68
30
  * `fetchWithPolicy`). Optional — defaults to 15s timeout, 2 retries.
69
31
  */
70
32
  pythFetch?: PythFetchPolicy;
71
33
  /**
72
- * Quote-center base URL for `oracleSource: 'waterx_rule'` — overrides the
34
+ * Quote-center base URL for `waterx_rule` — overrides the
73
35
  * source's own per-network `WATERX_INFRA` default.
74
36
  *
75
- * This is the one source a BROWSER fetches itself (the signed envelope is
76
- * pulled from the page), so it is bound by the quote-center deployment's CORS
37
+ * This is the one source a BROWSER fetches itself (the signed price is pulled
38
+ * from the page), so it is bound by the quote-center deployment's CORS
77
39
  * allowlist. A front end whose origin is not allowed — or one that must route
78
40
  * egress through its own backend — points this at a same-origin proxy that
79
- * forwards `GET /v1/quotes/update`. Unused by the Pyth sources.
41
+ * forwards `GET /v1/quotes/leaves` (and `GET /v1/quotes/update`, the fallback
42
+ * route). Unused by the Pyth sources.
80
43
  *
81
44
  * An absolute URL. Any base PATH is preserved (`joinEndpointPath`), so
82
45
  * `https://app.example/api/quote-center` fetches
83
- * `https://app.example/api/quote-center/v1/quotes/update` — a proxy route
46
+ * `https://app.example/api/quote-center/v1/quotes/leaves` — a proxy route
84
47
  * survives instead of being rewritten to the origin root.
85
48
  */
86
49
  waterxEndpoint?: string;
@@ -96,14 +59,14 @@ export declare class PerpClient extends BaseLineClient<WaterXConfig> {
96
59
  /** Caller-supplied Pyth credential + fetch policy — NO infra; each source owns its own tables. */
97
60
  pyth: PythAccessConfig;
98
61
  /**
99
- * Caller-supplied quote-center overrides for `oracleSource: 'waterx_rule'`
62
+ * Caller-supplied quote-center overrides for `waterx_rule`
100
63
  * (`waterxEndpoint` / `waterxFetch` create options) — access-only, mirroring
101
64
  * `pyth` above; unset fields resolve against the rule's own `WATERX_INFRA`.
102
65
  */
103
66
  waterx: WaterxAccessConfig;
104
67
  /** Wormhole infra for the credit bridge (network defaults unless overridden). */
105
68
  wormhole: WormholeInfraConfig;
106
- /** The fed set: `oracleSource` create option normalized to a non-empty, deduped list. */
69
+ /** The fed set, derived from the config see {@link deriveOracleSources}. */
107
70
  readonly oracleSources: readonly OracleSource[];
108
71
  /** Canonical-schema lookups (delegated to below); no transport. */
109
72
  private readonly view;
@@ -123,11 +86,33 @@ export declare class PerpClient extends BaseLineClient<WaterXConfig> {
123
86
  getMarket(ticker: string): import("./config.ts").WaterxPerpMarketEntry;
124
87
  /** @see PerpConfigView.getAggregator */
125
88
  getAggregator(ticker: string): string;
126
- /** @see PerpConfigView.getPythFeed */
127
- getPythFeed(ticker: string): {
128
- feed_id: string;
129
- price_info_object: string;
130
- };
89
+ /**
90
+ * WLP pool-token tickers THIS client's fed set can actually price — the
91
+ * ticker set the WLP builders refresh before `assert_prices_fresh`.
92
+ *
93
+ * Filtered, not raw `Object.keys(pool_tokens)`: a token only an UNLISTED
94
+ * source serves is not priceable by this client. The predicate is
95
+ * `refreshOraclePrices`'s own (`servableTickers`), so this list is exactly
96
+ * what a refresh would accept.
97
+ *
98
+ * NOTE this is a QUERY, not what the WLP builders use — they deliberately
99
+ * refresh and bump the WHOLE pool, because dropping an unpriceable asset
100
+ * from both halves is silent (see `assertWlpPoolRefreshed`).
101
+ */
102
+ pricedPoolTickers(): string[];
103
+ /**
104
+ * {@link pricedPoolTickers} already joined to each token's Move type.
105
+ *
106
+ * A query, not a build primitive: the WLP builders deliberately refresh and
107
+ * bump the WHOLE pool (`refreshWlpPoolOracles`), because pre-filtering to
108
+ * what this client can price silently drops an asset from both halves. Use
109
+ * this to ASK what a fed set covers — e.g. a dashboard, or a boot assert
110
+ * pairing it with `assertOracleWriteCoverage`.
111
+ */
112
+ pricedPoolTokens(): {
113
+ ticker: string;
114
+ tokenType: string;
115
+ }[];
131
116
  /** @see PerpConfigView.isConstantTicker */
132
117
  isConstantTicker(ticker: string): boolean;
133
118
  /** @see PerpConfigView.getSupraRule */