@waterx/sdk 4.3.2 → 4.3.3

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.
@@ -1,35 +1,59 @@
1
1
  /**
2
2
  * `WaterxRule` — `PriceUpdateRule` for the first-party WaterX quote-center
3
- * (Nautilus-TEE, ed25519), plus `feedWaterxRule`, the collector-feed leg
4
- * `aggregateTicker` appends per waterx-routed ticker. Pulls one enclave-signed
5
- * batch envelope covering every requested ticker from the quote-center
6
- * (`GET /v1/quotes/update?symbols=…`, endpoint from `host.waterx` the
7
- * `waterxEndpoint`/`waterxFetch` create options else its own `WATERX_INFRA`), then —
8
- * unlike Pyth Lazer, whose verify is a single shared PTB step — verifies AND
9
- * feeds in ONE `waterx_rule::collect_batch_latest` call per collector (the Move
10
- * API bundles the two). So `buildUpdateCalls` emits nothing and the signed
11
- * envelope is handed straight to the per-ticker feed leg.
3
+ * (Nautilus-TEE, ed25519), plus the collector-feed legs `aggregateTicker`
4
+ * appends per waterx-routed ticker. Endpoint comes from `host.waterx` (the
5
+ * `waterxEndpoint`/`waterxFetch` create options), else this source's own
6
+ * `WATERX_INFRA`. Unlike Pyth Lazer, whose verify is a single shared PTB step,
7
+ * the Move API bundles verify AND feed into ONE call per collector, so
8
+ * `buildUpdateCalls` emits nothing and the signed data is handed straight to
9
+ * the per-ticker feed leg.
12
10
  *
13
- * `collect_batch_latest` is the dual-rule path: it feeds the item matching
14
- * `collector.symbol()` WITHOUT aggregating, so a waterx-routed ticker composes
15
- * onto the same collector as Pyth/Supra (compose-then-aggregate). On-chain a
16
- * FRESHNESS miss ABSTAINS (the other weighted rules cover); a config/
17
- * integrity mismatch, bad signature, future timestamp or a REPLAYED signed
18
- * timestamp ABORTS (`EReplayedSignature`, audit F-014: a signed tuple is
19
- * single-use per symbol, enforced by a per-symbol high-water mark BEFORE any
20
- * weight arbitration). Consequence for concurrent builds: two PTBs carrying
21
- * the same envelope for the same symbol cannot both land the second aborts
22
- * even if the rule is unweighted for that ticker. Never share one fetched
23
- * envelope across builds that may execute concurrently for the same symbol.
11
+ * TWO wire shapes carry the same prices, and this rule prefers the first:
12
+ *
13
+ * 1. **Merkle leaves** (default) `GET /v1/quotes/leaves?symbols=…` returns one
14
+ * `SignedLeaf` per symbol: the price fields, a Merkle `proof`, and the
15
+ * enclave's signature over the snapshot ROOT (`MERKLE_ROOT_INTENT`). Fed via
16
+ * {@link feedWaterxRuleWithProof} `waterx_rule::collect_single_with_proof`,
17
+ * which re-derives the root from the leaf + proof. One symbol costs ONE
18
+ * `new_batch_item` plus ~log2(n) 32-byte proof hashes.
19
+ * 2. **Batch envelope** (fallback)`GET /v1/quotes/update?symbols=…` returns
20
+ * ONE signature over the whole item vector (`BATCH_PRICE_INTENT`). It is
21
+ * indivisible: {@link feedWaterxRule} must rebuild EVERY item in-PTB for
22
+ * `waterx_rule::collect_batch_latest` to re-verify, even to use one symbol's
23
+ * price. With the 29-feed mainnet registry that is 58 extra moveCalls and
24
+ * ~320 extra pure inputs on every trade, which is why it is no longer the
25
+ * default. Used only when the quote-center has no leaf route yet (404),
26
+ * and by callers that still push whole batches.
27
+ *
28
+ * Both collect entries are the dual-rule path: they feed `collector.symbol()`
29
+ * WITHOUT aggregating, so a waterx-routed ticker composes onto the same
30
+ * collector as Pyth/Supra (compose-then-aggregate). Their abort-vs-abstain
31
+ * disposition is identical: a config/integrity mismatch, a bad signature or a
32
+ * signed timestamp AHEAD of the on-chain `Clock` ABORTS; a freshness miss
33
+ * ABSTAINS so the other weighted rules cover a lagging TEE, and so does a
34
+ * REPLAYED signed timestamp (the per-symbol high-water mark of audit F-014 —
35
+ * already-recorded means the chain holds a price at least this fresh, so two
36
+ * PTBs carrying the same snapshot for the same symbol no longer kill each
37
+ * other; only the single-rule `feed_*` entries abort on a replay).
24
38
  */
25
39
  import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
26
40
  import type { Network } from "../../constants.ts";
27
41
  import type { OracleHost } from "../host.ts";
28
42
  import { type PriceUpdateRule, type RuleUpdateData } from "../price-update-rule.ts";
29
- /** The single signing intent the quote-center emits — exported so read-plane
30
- * consumers can mirror the rule's own envelope intent check (a mispointed
31
- * endpoint must be rejected by reads exactly as tx-builds reject it). */
43
+ /** Intent the quote-center signs a whole BATCH payload under — exported so
44
+ * read-plane consumers can mirror the rule's own envelope intent check (a
45
+ * mispointed endpoint must be rejected by reads exactly as tx-builds reject it). */
32
46
  export declare const BATCH_PRICE_INTENT = 1;
47
+ /**
48
+ * Intent the quote-center signs a snapshot's Merkle ROOT under
49
+ * (`waterx_rule::MERKLE_ROOT_INTENT`). Distinct from
50
+ * {@link BATCH_PRICE_INTENT} on purpose: the intent byte is the first field of
51
+ * the signed `IntentMessage`, so a batch signature can never be replayed as a
52
+ * root signature or vice versa. Leaves carry no `intent` field of their own —
53
+ * they are only ever submitted through `collect_single_with_proof`, which pins
54
+ * the intent on-chain — so this exists to name the scheme, not to gate a parse.
55
+ */
56
+ export declare const MERKLE_ROOT_INTENT = 2;
33
57
  /**
34
58
  * WaterX quote-center external infra — owned by THIS source, by network.
35
59
  * Mirrors `PYTH_CORE_INFRA` (oracle/pyth.ts) and `LAZER_INFRA`
@@ -88,39 +112,104 @@ export interface WaterxSignedEnvelope {
88
112
  /** ed25519 signature over `BCS(IntentMessage<BatchPricePayload>)`, hex (± `0x`). */
89
113
  signature: string;
90
114
  }
91
- /** `waterx_rule`'s narrowed `RuleUpdateData.payload` shape. */
92
- export interface WaterxUpdatePayload {
115
+ /**
116
+ * One enclave-signed Merkle leaf from `GET /v1/quotes/leaves` — identical shape
117
+ * to the `/v1/quote/stream/signed` SSE/WS events (quote-center serves both from
118
+ * one conversion), so a leaf from either transport submits the same way.
119
+ *
120
+ * It is the batch item's fields PLUS its membership proof: on-chain,
121
+ * `collect_single_with_proof` recomputes `keccak256(0x00 || BCS(item))`, folds
122
+ * it through `proof` (sorted pairs, no direction flags), and verifies the
123
+ * enclave's signature over THAT root — so every item field must round-trip
124
+ * byte-for-byte, exactly as for the batch path.
125
+ */
126
+ export interface WaterxSignedLeaf extends WaterxBatchItem {
127
+ /**
128
+ * Enclave signing timestamp of the SNAPSHOT ROOT (ms) — the on-chain
129
+ * `timestamp_ms` argument. Shared by every leaf of one snapshot, and distinct
130
+ * from `price_timestamp_ms` (when the price itself was observed).
131
+ */
132
+ signed_timestamp_ms: bigint;
133
+ /** keccak256 Merkle root (hex). Diagnostic only — the chain re-derives it. */
134
+ root: string;
135
+ /** Sibling hashes (hex, 32 bytes each) folding this leaf to `root`. Empty for a one-leaf snapshot. */
136
+ proof: string[];
137
+ /** ed25519 signature over `BCS(IntentMessage<MerkleRoot>)`, hex (± `0x`). */
138
+ signature: string;
139
+ }
140
+ /** Leaf variant of `waterx_rule`'s `RuleUpdateData.payload` — the default path. */
141
+ export interface WaterxLeafPayload {
142
+ readonly leaves: readonly WaterxSignedLeaf[];
143
+ }
144
+ /** Batch-envelope variant of `waterx_rule`'s `RuleUpdateData.payload` — the fallback path. */
145
+ export interface WaterxEnvelopePayload {
93
146
  readonly envelope: WaterxSignedEnvelope;
94
147
  }
148
+ /**
149
+ * `waterx_rule`'s narrowed `RuleUpdateData.payload` shape: a per-symbol leaf set
150
+ * OR one indivisible batch envelope. The variant a payload carries decides which
151
+ * on-chain entry the feed leg emits, so consumers must route on it via
152
+ * {@link waterxLeavesOf} / {@link waterxEnvelopeOf} rather than assuming either.
153
+ */
154
+ export type WaterxUpdatePayload = WaterxLeafPayload | WaterxEnvelopePayload;
95
155
  /**
96
156
  * Parse a quote-center `/v1/quotes/update` response body into a
97
- * {@link WaterxSignedEnvelope} with the u64 fields decoded as `bigint`, exact.
98
- *
99
- * The signature is over `BCS(IntentMessage<BatchPricePayload>)`, so every u64
100
- * the SDK rebuilds in-PTB must equal the enclave's byte-for-byte or
101
- * `collect_batch_latest` aborts the whole trade PTB (bad signature — not an
102
- * abstain). A plain `JSON.parse` yields IEEE-754 doubles that lose precision
103
- * above 2^53, so instead we recover each integer's exact source literal via the
104
- * ES2023 reviver `context.source` (Node 21+ / modern browsers) and `BigInt()`
105
- * it. On an older runtime that passes no `context`, a value within 2^53 is
106
- * still exact (`BigInt(number)`); a value ABOVE it throws loudly here rather
107
- * than silently corrupting the payload into an on-chain abort. `num_sources`
108
- * (u8) and `intent` are coerced back to `number` — both are tiny.
157
+ * {@link WaterxSignedEnvelope} with the u64 fields decoded as `bigint`, exact
158
+ * (see {@link parseWithExactIntegers}). `num_sources` (u8) and `intent` are
159
+ * coerced back to `number` both are tiny.
109
160
  */
110
161
  export declare function parseSignedEnvelope(text: string): WaterxSignedEnvelope;
111
- /** Narrow a `RuleUpdateData` to its `WaterxSignedEnvelope`, or `null`. */
162
+ /**
163
+ * Parse a quote-center `/v1/quotes/leaves` response body (`{ leaves: [...] }`)
164
+ * into {@link WaterxSignedLeaf}s, u64s exact as `bigint`, rejecting a malformed
165
+ * leaf or proof element on the wire — before any PTB is touched.
166
+ */
167
+ export declare function parseSignedLeaves(text: string): WaterxSignedLeaf[];
168
+ /**
169
+ * Narrow a `RuleUpdateData` to its per-symbol {@link WaterxSignedLeaf}s, or
170
+ * `null` when it carries a batch envelope instead (the fallback shape).
171
+ */
172
+ export declare function waterxLeavesOf(data: RuleUpdateData): readonly WaterxSignedLeaf[] | null;
173
+ /**
174
+ * Narrow a `RuleUpdateData` to its `WaterxSignedEnvelope`, or `null` when it
175
+ * carries per-symbol leaves instead (the default shape).
176
+ */
112
177
  export declare function waterxEnvelopeOf(data: RuleUpdateData): WaterxSignedEnvelope | null;
178
+ /**
179
+ * `waterx_rule::collect_single_with_proof(collector, config, clock,
180
+ * enclave_config, enclave, timestamp_ms, item, proof, sig)` — the DEFAULT feed
181
+ * leg. Rebuilds ONE item in-PTB and hands it over with its Merkle proof; on-chain
182
+ * the leaf is hashed, folded through the proof, and the enclave's signature over
183
+ * the resulting root is verified before the price reaches the collector.
184
+ *
185
+ * Cost is what makes this the default: one item + `proof.length` 32-byte hashes
186
+ * (~log2 of the snapshot width — 4 to 5 for the 29-feed mainnet registry),
187
+ * against {@link feedWaterxRule}'s obligation to rebuild every item the batch
188
+ * signature covers.
189
+ *
190
+ * Abort vs abstain is identical to the batch path (see the module header): a
191
+ * mismatched root, a bad signature, a future signed timestamp, or a config
192
+ * mismatch ABORTS; a freshness miss or a replayed signed timestamp abstains. One
193
+ * extra abort of its own — `ECollectorSymbolMismatch` if the leaf's symbol isn't
194
+ * the collector's — which `aggregateTicker` prevents by construction, since it
195
+ * looks the leaf up BY the ticker it just built the collector for.
196
+ */
197
+ export declare function feedWaterxRuleWithProof(tx: Transaction, host: OracleHost, collector: TransactionArgument, leaf: WaterxSignedLeaf): void;
113
198
  /**
114
199
  * `waterx_rule::collect_batch_latest(collector, config, clock, enclave_config,
115
- * enclave, timestamp_ms, payload, sig)` — rebuild the enclave-signed batch
116
- * payload in-PTB (`new_batch_payload` + one `new_batch_item`/`push_batch_item`
117
- * per item, the exact shape the enclave signed) and contribute the price for
118
- * `collector.symbol()` to the collector. One collect call re-verifies the batch
119
- * signature and picks this collector's symbol out of the batch; on-chain it
120
- * abstains (records `none`) when the symbol is stale or absent from the batch,
121
- * but ABORTS `EReplayedSignature` when the symbol's signed timestamp was
122
- * already accepted (per-symbol high-water mark, audit F-014) see the module
123
- * header for the concurrent-build consequence.
200
+ * enclave, timestamp_ms, payload, sig)` — the FALLBACK feed leg, for a
201
+ * quote-center with no leaf route (and for callers that hold a whole batch).
202
+ * Rebuilds the enclave-signed batch payload in-PTB (`new_batch_payload` + one
203
+ * `new_batch_item`/`push_batch_item` per item, the exact shape the enclave
204
+ * signed) and contributes the price for `collector.symbol()` to the collector.
205
+ *
206
+ * Every item must be rebuilt, not just this collector's: the signature covers
207
+ * `BCS(IntentMessage)` over the whole vector, so a missing item is a failed
208
+ * verify. That is the cost {@link feedWaterxRuleWithProof} exists to avoid.
209
+ *
210
+ * On-chain it abstains (records `none`) when the symbol is stale, absent from the
211
+ * batch, or already recorded at this signed timestamp; it ABORTS on a bad
212
+ * signature, a signed timestamp ahead of the `Clock`, or a config mismatch.
124
213
  */
125
214
  export declare function feedWaterxRule(tx: Transaction, host: OracleHost, collector: TransactionArgument, envelope: WaterxSignedEnvelope): void;
126
215
  export declare const WaterxRule: PriceUpdateRule;