@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,177 @@
1
+ /**
2
+ * `validate.ts` — boot-time oracle-deployment asserts consumers (FE/BE) fold
3
+ * onto instead of each hand-rolling them:
4
+ *
5
+ * - {@link assertOracleWriteCoverage} — the "this fed set can actually price
6
+ * the tickers I care about" guard. Because every source reads through its
7
+ * own feeds namespace, write set == read set — passing this ALSO validates
8
+ * the read plane (there is deliberately no separate read-coverage assert).
9
+ * - {@link missingOracleCredentials} — the env-shaped credential audit: which
10
+ * listed sources cannot run with the credentials this deployment supplied.
11
+ * Boot-time mirror of `refreshOraclePrices`'s own per-build credential
12
+ * pre-check (`aggregate.ts`), keyed off the same rule-owned
13
+ * `credential` declaration so the two can never disagree.
14
+ *
15
+ * Deliberately NOT called by `PerpClient` itself: client creation stays
16
+ * guard-free (a source with absent feeds surfaces at tx-build for exactly the
17
+ * tickers it can't serve). These are for consumers whose deployment policy is
18
+ * "fail the BOOT, not the first trade".
19
+ */
20
+ import { ORACLE_SOURCES, } from "./price-update-rule.js";
21
+ import { resolveOracleRule } from "./rule-registry.js";
22
+ /**
23
+ * A build depends on a ticker this client's fed set cannot price.
24
+ *
25
+ * Raised from two places, deliberately the same type: the per-BUILD composers
26
+ * (`refreshOraclePrices` skipped the ticker by design — see
27
+ * `OracleRefreshSummary` — and the composer decided it was load-bearing for
28
+ * the action) and the boot-time {@link assertOracleWriteCoverage}. One error
29
+ * for one question: "this fed set cannot price these tickers".
30
+ */
31
+ export class OracleTickerUnservedError extends Error {
32
+ tickers;
33
+ sources;
34
+ constructor(tickers, sources, why) {
35
+ super(`fed set [${sources.join(", ")}] has no feed for ticker(s): ${tickers.join(", ")}. ` +
36
+ (why ?? "This build depends on their prices, so it cannot proceed. ") +
37
+ "Add feeds under a listed source, list a source that serves them, or pass " +
38
+ "allowUnrefreshedPrices: true to build anyway against whatever price the " +
39
+ "chain already holds.");
40
+ this.name = "OracleTickerUnservedError";
41
+ this.tickers = tickers;
42
+ this.sources = sources;
43
+ }
44
+ }
45
+ /**
46
+ * Assert this deployment's fed set can price every ticker in `tickers`.
47
+ *
48
+ * Throws {@link OracleTickerUnservedError} naming ALL the unservable ones (not
49
+ * just the first — an operator fixing a config wants the whole list).
50
+ *
51
+ * This is the boot-time twin of the per-build behaviour, and it guards the gap
52
+ * that per-build handling deliberately leaves open: `refreshOraclePrices`
53
+ * SKIPS a ticker no source serves, and only a composer that happens to depend
54
+ * on that ticker turns the skip into an error. A market nobody trades today
55
+ * would therefore stay silently unpriceable until someone did. Pass the ticker
56
+ * set your deployment cares about (typically every market) and find out at
57
+ * boot instead.
58
+ *
59
+ * It does NOT check "every listed source has feeds" any more: the fed set is
60
+ * derived from exactly that condition (see `deriveOracleSources`), so that
61
+ * assert became unreachable for any real client — it could only fire for a
62
+ * config mutated after construction, which is a test fixture, not a
63
+ * deployment. Write set == read set by construction (each source reads its own
64
+ * feeds), so this single assert still covers both planes.
65
+ */
66
+ export function assertOracleWriteCoverage(host, tickers) {
67
+ const { unservable } = partitionServableTickers(host, tickers);
68
+ if (unservable.length > 0) {
69
+ throw new OracleTickerUnservedError(unservable, host.oracleSources);
70
+ }
71
+ }
72
+ /** Tickers the client's LISTED sources carry a feed for. */
73
+ function fedSetTickers(host) {
74
+ return new Set(host.oracleSources.flatMap((source) => resolveOracleRule(source).supportedTickers(host)));
75
+ }
76
+ /**
77
+ * Tickers ANY rule in this config carries a feed for — listed or not.
78
+ *
79
+ * The fed set answers "can THIS client price it"; this answers "does the chain
80
+ * plausibly weight a price-update rule for it", which is what decides whether
81
+ * a constant leg alone is the whole picture. `supra_rule` counts (it is a
82
+ * weighted leg even though it is not an `OracleSource`) but only when it is
83
+ * actually wired — an unwired block's feeds map is documented as
84
+ * informational, and honouring a stale one would strand constant-only tickers
85
+ * for no benefit.
86
+ */
87
+ function tickersWithAnySourceFeed(host) {
88
+ const out = new Set();
89
+ for (const source of ORACLE_SOURCES) {
90
+ // Skip a source the deployment DISABLED — routing already honours
91
+ // `enabled: false` (`deriveOracleSources`), and applying it in one place
92
+ // but not the other meant a disabled block's informational feeds
93
+ // disqualified a constant-only ticker that nothing would ever feed. The
94
+ // predicate has to match what actually gets emitted.
95
+ if (host.config.packages[source]?.enabled === false)
96
+ continue;
97
+ for (const ticker of resolveOracleRule(source).supportedTickers(host))
98
+ out.add(ticker);
99
+ }
100
+ // `getSupraRule()` already requires `enabled` — see `config-view.ts`.
101
+ if (host.getSupraRule() !== undefined) {
102
+ for (const ticker of Object.keys(host.config.packages.supra_rule?.feeds ?? {}))
103
+ out.add(ticker);
104
+ }
105
+ return out;
106
+ }
107
+ /**
108
+ * THE acceptance predicate, in partition form — the single definition of
109
+ * "will `refreshOraclePrices` put a price on chain for this ticker".
110
+ *
111
+ * A ticker is servable when some LISTED source's feeds carry it, or when it is
112
+ * CONSTANT-ONLY: `constant_rule` pins it and no other rule in the config feeds
113
+ * it. The stricter constant test matters — a constant-pinned ticker that some
114
+ * other rule also feeds gets aggregated from a constant-only collector, and if
115
+ * the chain weights that other rule the aggregate aborts `EMissingPriceSource`.
116
+ * (`aggregateTicker` only appends a supra leg when a price-update source
117
+ * already fed the collector, so a constant-only collector can never carry one.)
118
+ *
119
+ * `covered` lets a caller that has ALREADY resolved which of its tickers its
120
+ * fed set serves — `refreshOraclePrices`, off its rule groups — pass that in
121
+ * rather than have it recomputed. Both callers therefore share one rule, which
122
+ * is the point: a consumer pre-filtering with {@link servableTickers} cannot
123
+ * hand the build a ticker it will silently skip.
124
+ *
125
+ * Order-preserving. The `anyFeed` set is built at most once per call, and only
126
+ * when a constant-pinned ticker actually needs it.
127
+ */
128
+ export function partitionServableTickers(host, tickers, covered) {
129
+ const fed = covered ?? fedSetTickers(host);
130
+ const servable = [];
131
+ const unservable = [];
132
+ let anyFeed;
133
+ for (const ticker of tickers) {
134
+ if (fed.has(ticker)) {
135
+ servable.push(ticker);
136
+ }
137
+ else if (host.isConstantTicker(ticker)) {
138
+ anyFeed ??= tickersWithAnySourceFeed(host);
139
+ (anyFeed.has(ticker) ? unservable : servable).push(ticker);
140
+ }
141
+ else {
142
+ unservable.push(ticker);
143
+ }
144
+ }
145
+ return { servable, unservable };
146
+ }
147
+ /**
148
+ * The subset of `tickers` this deployment's fed set can actually price — the
149
+ * servable half of {@link partitionServableTickers}, which is literally the
150
+ * rule `refreshOraclePrices` applies.
151
+ */
152
+ export function servableTickers(host, tickers) {
153
+ return partitionServableTickers(host, tickers).servable;
154
+ }
155
+ /**
156
+ * Which of `sources` cannot run with the supplied credentials — one row per
157
+ * (source, missing credential). Empty array ⇒ the fed set is fully
158
+ * credentialed. Pure and env-shaped on purpose: consumers call it from their
159
+ * boot-time env asserts (zod superRefine, config validators) BEFORE any
160
+ * client exists, passing the raw values their env resolved. The per-build
161
+ * enforcement twin — `refreshOraclePrices`'s credential pre-check — reads the
162
+ * same rule-owned `credential` declaration, so a deployment this function passes
163
+ * cannot later trip that check for a listed source.
164
+ */
165
+ export function missingOracleCredentials(sources, creds) {
166
+ // The env-shaped bag, normalized to the kind-keyed shape ONCE — the check
167
+ // below then never mentions a specific kind.
168
+ const supplied = { pyth_api_key: creds.pythApiKey };
169
+ const missing = [];
170
+ for (const source of sources) {
171
+ const required = resolveOracleRule(source).credential?.kind;
172
+ if (required !== undefined && !supplied[required]) {
173
+ missing.push({ source, credential: required });
174
+ }
175
+ }
176
+ return missing;
177
+ }
@@ -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,166 @@
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 { resolveOracleRule } from "./rule-registry.js";
31
+ /**
32
+ * The on-chain witness struct each SDK source feeds. The aggregator's weight
33
+ * table is keyed by these type names, so this is the join between what the
34
+ * chain requires and what the fed set can supply.
35
+ *
36
+ * `ConstantRule` and `SupraRule` are auxiliary legs rather than sources — the
37
+ * SDK feeds them alongside whichever source ran, so a ticker weighted only to
38
+ * them needs no source. Anything NOT in this map (notably the retired
39
+ * `PythRule`) cannot be supplied by this SDK at any fed set.
40
+ */
41
+ const WITNESS_TO_SOURCE = Object.freeze({
42
+ PythLazerRule: "pyth_lazer_rule",
43
+ WaterxRule: "waterx_rule",
44
+ });
45
+ /** Raised by {@link assertOracleWeightCoverage}; `rows` names every offender. */
46
+ export class OracleWeightCoverageError extends Error {
47
+ rows;
48
+ constructor(rows) {
49
+ const detail = rows.map((r) => `${r.ticker} weights ${r.unsuppliable.join(" + ")}`).join("; ");
50
+ super(`on-chain aggregator weights name rule(s) this fed set cannot supply: ${detail}. ` +
51
+ `Aggregating such a ticker emits a collector with no WEIGHTED contribution, so ` +
52
+ `remove_outliers aborts EMissingPriceSource and takes the whole PTB down — every ` +
53
+ `other ticker in the same build with it. Fix by migrating the on-chain weight to a ` +
54
+ `rule this deployment feeds, or by not requesting these tickers.`);
55
+ this.name = "OracleWeightCoverageError";
56
+ this.rows = rows;
57
+ }
58
+ }
59
+ /** Raised when an aggregator object cannot be decoded — see `readOracleWeightCoverage`. */
60
+ export class OracleWeightUnreadableError extends Error {
61
+ ticker;
62
+ constructor(ticker, aggregatorId, why) {
63
+ super(`cannot read aggregator ${aggregatorId} for ${ticker}: ${why}. This check exists to fail ` +
64
+ `CLOSED, so an undecodable weight table is an error rather than an empty one — treating ` +
65
+ `it as "no weights" would certify every ticker clean without verifying anything.`);
66
+ this.name = "OracleWeightUnreadableError";
67
+ this.ticker = ticker;
68
+ }
69
+ }
70
+ /** `0x…::aggregator::PythRule` → `PythRule`; already-short names pass through. */
71
+ function witnessName(raw) {
72
+ const s = typeof raw === "string" ? raw : String(raw?.name ?? raw);
73
+ return s.split("::").pop() ?? s;
74
+ }
75
+ /**
76
+ * Read each ticker's aggregator and report which weighted rules this fed set
77
+ * cannot supply.
78
+ *
79
+ * Tickers with no aggregator entry in the config are skipped, not failed: an
80
+ * unlisted aggregator means the deployment does not price that ticker on chain
81
+ * at all, which {@link assertOracleWriteCoverage} is the right check for.
82
+ */
83
+ export async function readOracleWeightCoverage(host, tickers) {
84
+ const aggregators = host.config.packages.waterx_oracle.aggregators;
85
+ // Which tickers each LISTED source actually feeds — per source, not merged.
86
+ //
87
+ // Being in the fed set is not enough: `refreshOraclePrices` groups a ticker
88
+ // under a source only when that source's feeds carry it, so a lazer-weighted
89
+ // ticker that only WaterX feeds gets no lazer leg no matter that lazer is
90
+ // listed. Treating a listed source as suppliable for EVERY ticker passed
91
+ // exactly that case, which then aborts `EMissingPriceSource` on chain.
92
+ const feedsByWitness = new Map();
93
+ for (const [witness, source] of Object.entries(WITNESS_TO_SOURCE)) {
94
+ if (host.oracleSources.includes(source)) {
95
+ feedsByWitness.set(witness, new Set(resolveOracleRule(source).supportedTickers(host)));
96
+ }
97
+ }
98
+ const supraWired = host.getSupraRule() !== undefined;
99
+ /** Some listed source feeds this ticker, so a collector gets fed at all. */
100
+ const anySourceFeeds = (ticker) => {
101
+ for (const served of feedsByWitness.values())
102
+ if (served.has(ticker))
103
+ return true;
104
+ return false;
105
+ };
106
+ const suppliableFor = (ticker, witness) => {
107
+ const served = feedsByWitness.get(witness);
108
+ // A source witness: suppliable only where that source has THIS ticker's feed.
109
+ if (served !== undefined)
110
+ return served.has(ticker);
111
+ if (witness === "ConstantRule")
112
+ return host.isConstantTicker(ticker);
113
+ // Supra rides on a collector a SOURCE already fed — never on its own, and
114
+ // never on a constant-only collector.
115
+ if (witness === "SupraRule")
116
+ return supraWired && anySourceFeeds(ticker);
117
+ return false;
118
+ };
119
+ const wanted = tickers.filter((t) => Object.hasOwn(aggregators, t));
120
+ // Independent reads — one round trip each would make a 30-market boot assert
121
+ // needlessly serial.
122
+ const objects = await Promise.all(wanted.map((t) =>
123
+ // Explicit field mask: the client's `getObject` wrapper requests none, so
124
+ // `json` would come back undefined and every aggregator would look
125
+ // weightless — an assert that passes exactly where it must fail.
126
+ host.grpcClient.getObject({ objectId: aggregators[t], include: { json: true } })));
127
+ return wanted.map((ticker, i) => {
128
+ // FAIL CLOSED on anything undecodable. Defaulting a missing object / JSON /
129
+ // weights map to an empty list made the assert succeed without verifying a
130
+ // single weight — the exact fail-open shape this gate exists to prevent.
131
+ const id = aggregators[ticker];
132
+ const json = objects[i]?.object?.json;
133
+ if (json === undefined || json === null || typeof json !== "object") {
134
+ throw new OracleWeightUnreadableError(ticker, id, "no JSON payload in the object read");
135
+ }
136
+ const raw = json.weights;
137
+ const entries = Array.isArray(raw) ? raw : raw?.contents;
138
+ if (!Array.isArray(entries)) {
139
+ throw new OracleWeightUnreadableError(ticker, id, "no decodable `weights` table");
140
+ }
141
+ const weighted = entries.map((e) => witnessName(e.key));
142
+ if (weighted.some((w) => w === "" || w === "undefined")) {
143
+ throw new OracleWeightUnreadableError(ticker, id, "a weight entry has no rule type name");
144
+ }
145
+ return {
146
+ ticker,
147
+ weighted,
148
+ unsuppliable: weighted.filter((w) => !suppliableFor(ticker, w)),
149
+ };
150
+ });
151
+ }
152
+ /**
153
+ * Throw {@link OracleWeightCoverageError} when any of `tickers` is weighted to
154
+ * a rule this fed set cannot feed.
155
+ *
156
+ * The chain-reading twin of `assertOracleWriteCoverage`: that one catches a
157
+ * ticker NO source serves (loud at build anyway), this one catches the quieter
158
+ * and more damaging case — a ticker every config check passes, which aborts the
159
+ * entire transaction on chain.
160
+ */
161
+ export async function assertOracleWeightCoverage(host, tickers) {
162
+ const rows = await readOracleWeightCoverage(host, tickers);
163
+ const bad = rows.filter((r) => r.unsuppliable.length > 0);
164
+ if (bad.length > 0)
165
+ throw new OracleWeightCoverageError(bad);
166
+ }
@@ -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 */
@@ -12,21 +12,22 @@
12
12
  */
13
13
  import { BaseLineClient } from "../base-client.js";
14
14
  import { ORACLE_SOURCES } from "../oracle/price-update-rule.js";
15
- import { isOracleSource } from "../oracle/source-list.js";
15
+ import { deriveOracleSources } from "../oracle/source-list.js";
16
+ import { servableTickers } from "../oracle/validate.js";
16
17
  import { PerpConfigView } from "./config-view.js";
17
18
  import { loadConfig, WORMHOLE_DEFAULTS, } from "./config.js";
18
19
  export class PerpClient extends BaseLineClient {
19
20
  /** Caller-supplied Pyth credential + fetch policy — NO infra; each source owns its own tables. */
20
21
  pyth;
21
22
  /**
22
- * Caller-supplied quote-center overrides for `oracleSource: 'waterx_rule'`
23
+ * Caller-supplied quote-center overrides for `waterx_rule`
23
24
  * (`waterxEndpoint` / `waterxFetch` create options) — access-only, mirroring
24
25
  * `pyth` above; unset fields resolve against the rule's own `WATERX_INFRA`.
25
26
  */
26
27
  waterx;
27
28
  /** Wormhole infra for the credit bridge (network defaults unless overridden). */
28
29
  wormhole;
29
- /** The fed set: `oracleSource` create option normalized to a non-empty, deduped list. */
30
+ /** The fed set, derived from the config see {@link deriveOracleSources}. */
30
31
  oracleSources;
31
32
  /** Canonical-schema lookups (delegated to below); no transport. */
32
33
  view;
@@ -48,18 +49,17 @@ export class PerpClient extends BaseLineClient {
48
49
  ...(opts.waterxEndpoint !== undefined ? { endpoint: opts.waterxEndpoint } : {}),
49
50
  ...(opts.waterxFetch !== undefined ? { fetch: opts.waterxFetch } : {}),
50
51
  };
51
- // Normalize single-or-list to a deduped, order-preserving list, gated by
52
- // `isOracleSource` the SAME predicate `parseOracleSourceList` uses. An
53
- // empty list, a nullish entry (an untyped caller omitting the REQUIRED
54
- // option), or an unregistered value (`'core'`, `'pyth'`, ) fails
55
- // construction loudly instead of booting green and surfacing as
56
- // `OracleSourceNotImplemented` at the first tx-build.
57
- const sources = Array.isArray(opts.oracleSource) ? opts.oracleSource : [opts.oracleSource];
58
- this.oracleSources = [...new Set(sources)];
59
- if (this.oracleSources.length === 0 ||
60
- this.oracleSources.some((source) => !isOracleSource(source))) {
61
- throw new Error(`oracleSource is REQUIRED and must name at least one of ${ORACLE_SOURCES.join(" | ")} ` +
62
- `(got ${JSON.stringify(sources)})`);
52
+ // The fed set is a property of the DEPLOYMENT, read off the same config
53
+ // that wires the rules never a create option and never an env var.
54
+ this.oracleSources = deriveOracleSources(config);
55
+ if (this.oracleSources.length === 0) {
56
+ // Not a per-ticker coverage question (that is left to tx-build, on
57
+ // purpose): a config wiring NO price-update source at all cannot price
58
+ // anything, so every build would skip every ticker and every trade would
59
+ // abort on chain. Fail at construction, where the config is in hand.
60
+ throw new Error(`this deployment's config wires no price-update source — expected a published ` +
61
+ `package with a non-empty feeds map for at least one of ` +
62
+ `${ORACLE_SOURCES.join(" | ")}.`);
63
63
  }
64
64
  this.view = new PerpConfigView(() => this.config, () => this.wormhole);
65
65
  }
@@ -94,9 +94,36 @@ export class PerpClient extends BaseLineClient {
94
94
  getAggregator(ticker) {
95
95
  return this.view.getAggregator(ticker);
96
96
  }
97
- /** @see PerpConfigView.getPythFeed */
98
- getPythFeed(ticker) {
99
- return this.view.getPythFeed(ticker);
97
+ /**
98
+ * WLP pool-token tickers THIS client's fed set can actually price — the
99
+ * ticker set the WLP builders refresh before `assert_prices_fresh`.
100
+ *
101
+ * Filtered, not raw `Object.keys(pool_tokens)`: a token only an UNLISTED
102
+ * source serves is not priceable by this client. The predicate is
103
+ * `refreshOraclePrices`'s own (`servableTickers`), so this list is exactly
104
+ * what a refresh would accept.
105
+ *
106
+ * NOTE this is a QUERY, not what the WLP builders use — they deliberately
107
+ * refresh and bump the WHOLE pool, because dropping an unpriceable asset
108
+ * from both halves is silent (see `assertWlpPoolRefreshed`).
109
+ */
110
+ pricedPoolTickers() {
111
+ return servableTickers(this, Object.keys(this.config.packages.wlp?.pool_tokens ?? {}));
112
+ }
113
+ /**
114
+ * {@link pricedPoolTickers} already joined to each token's Move type.
115
+ *
116
+ * A query, not a build primitive: the WLP builders deliberately refresh and
117
+ * bump the WHOLE pool (`refreshWlpPoolOracles`), because pre-filtering to
118
+ * what this client can price silently drops an asset from both halves. Use
119
+ * this to ASK what a fed set covers — e.g. a dashboard, or a boot assert
120
+ * pairing it with `assertOracleWriteCoverage`.
121
+ */
122
+ pricedPoolTokens() {
123
+ return this.pricedPoolTickers().map((ticker) => ({
124
+ ticker,
125
+ tokenType: this.getPoolTokenType(ticker),
126
+ }));
100
127
  }
101
128
  /** @see PerpConfigView.isConstantTicker */
102
129
  isConstantTicker(ticker) {