@waterx/sdk 4.3.1 → 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.
- package/.claude/skills/waterx-sdk-integration/SKILL.md +219 -0
- package/README.md +177 -10
- package/SKILLS.md +34 -0
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +140 -7
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +163 -8
- package/dist/cjs/src/oracle/aggregate.d.ts +8 -4
- package/dist/cjs/src/oracle/aggregate.js +74 -29
- package/dist/cjs/src/oracle/config.d.ts +3 -2
- package/dist/cjs/src/oracle/index.d.ts +2 -2
- package/dist/cjs/src/oracle/index.js +10 -5
- package/dist/cjs/src/oracle/price-update-rule.d.ts +10 -2
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +25 -7
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +41 -17
- package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +136 -47
- package/dist/cjs/src/oracle/rules/waterx-rule.js +450 -114
- package/dist/cjs/src/perp/client.d.ts +10 -8
- package/dist/cjs/src/perp/index.d.ts +2 -2
- package/dist/cjs/src/perp/index.js +10 -2
- package/dist/cjs/src/unified-client.d.ts +1 -1
- package/dist/src/generated/waterx_rule/waterx_rule.d.ts +140 -7
- package/dist/src/generated/waterx_rule/waterx_rule.js +151 -7
- package/dist/src/oracle/aggregate.d.ts +8 -4
- package/dist/src/oracle/aggregate.js +75 -30
- package/dist/src/oracle/config.d.ts +3 -2
- package/dist/src/oracle/index.d.ts +2 -2
- package/dist/src/oracle/index.js +8 -6
- package/dist/src/oracle/price-update-rule.d.ts +10 -2
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +25 -7
- package/dist/src/oracle/rules/pyth-lazer-rule.js +41 -17
- package/dist/src/oracle/rules/waterx-rule.d.ts +136 -47
- package/dist/src/oracle/rules/waterx-rule.js +447 -114
- package/dist/src/perp/client.d.ts +10 -8
- package/dist/src/perp/index.d.ts +2 -2
- package/dist/src/perp/index.js +7 -1
- package/dist/src/unified-client.d.ts +1 -1
- package/package.json +8 -2
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* rules that consume it are separate concerns.
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.reimbursePythSponsor = exports.openPythSponsorFund = exports.refreshOraclePrices = exports.aggregateTickerWithConstant = exports.aggregateTickerWithPyth = exports.aggregateTicker = exports.resolveOracleRule = exports.OracleSourceNotImplementedError = exports.waterxEnvelopeOf = exports.waterxQuoteCenterEndpoint = exports.WATERX_INFRA = exports.BATCH_PRICE_INTENT = exports.parseSignedEnvelope = exports.WaterxRule = exports.LazerApiKeyMissingError = exports.PythLazerRule = exports.PythCoreRule = exports.resolveHermesReadEndpoint = exports.resolveOracleReadPlan = exports.parseOracleSourceList = exports.isOracleSource = exports.ORACLE_SOURCES = exports.OracleFeeSourceUnavailableError = exports.MISSING_FEED_MEMO_TTL_MS = exports.HermesEndpointRejectedAllFeedsError = exports.updatePythPrices = exports.PYTH_PRO_HERMES_ENDPOINT = exports.pythProHermesEndpoint = exports.pythCoreHermesEndpoint = exports.buildPythPriceUpdateCalls = exports.probeMissingFeeds = exports.endpointSupportedFeedIds = exports.fetchPriceFeedsUpdateData = exports.PythCache = exports.joinEndpointPath = exports.fetchWithPolicy = exports.FetchPolicyError = void 0;
|
|
20
|
+
exports.reimbursePythSponsor = exports.openPythSponsorFund = exports.refreshOraclePrices = exports.aggregateTickerWithConstant = exports.aggregateTickerWithPyth = exports.aggregateTicker = exports.resolveOracleRule = exports.OracleSourceNotImplementedError = exports.waterxEnvelopeOf = exports.waterxLeavesOf = exports.waterxQuoteCenterEndpoint = exports.WATERX_INFRA = exports.MERKLE_ROOT_INTENT = exports.BATCH_PRICE_INTENT = exports.parseSignedLeaves = exports.parseSignedEnvelope = exports.WaterxRule = exports.LazerApiKeyMissingError = exports.PythLazerRule = exports.PythCoreRule = exports.resolveHermesReadEndpoint = exports.resolveOracleReadPlan = exports.parseOracleSourceList = exports.isOracleSource = exports.ORACLE_SOURCES = exports.OracleFeeSourceUnavailableError = exports.MISSING_FEED_MEMO_TTL_MS = exports.HermesEndpointRejectedAllFeedsError = exports.updatePythPrices = exports.PYTH_PRO_HERMES_ENDPOINT = exports.pythProHermesEndpoint = exports.pythCoreHermesEndpoint = exports.buildPythPriceUpdateCalls = exports.probeMissingFeeds = exports.endpointSupportedFeedIds = exports.fetchPriceFeedsUpdateData = exports.PythCache = exports.joinEndpointPath = exports.fetchWithPolicy = exports.FetchPolicyError = void 0;
|
|
21
21
|
// Shared fetch resilience wrapper — `FetchPolicyError` is re-exported (not
|
|
22
22
|
// just the type) so a consumer (e.g. a BE prefetch cache) can `instanceof`
|
|
23
23
|
// it off the failure `fetchPriceFeedsUpdateData` / `PythLazerRule` /
|
|
@@ -85,16 +85,21 @@ Object.defineProperty(exports, "PythLazerRule", { enumerable: true, get: functio
|
|
|
85
85
|
Object.defineProperty(exports, "LazerApiKeyMissingError", { enumerable: true, get: function () { return pyth_lazer_rule_ts_1.LazerApiKeyMissingError; } });
|
|
86
86
|
// `WATERX_INFRA` / `waterxQuoteCenterEndpoint` are the source's own infra table +
|
|
87
87
|
// read-plane accessor (mirrors `pythCoreHermesEndpoint`).
|
|
88
|
-
// WaterX quote-center rule (first-party ed25519 signed
|
|
89
|
-
//
|
|
88
|
+
// WaterX quote-center rule (first-party ed25519 signed prices; the `feedWaterxRule*`
|
|
89
|
+
// legs stay internal to `aggregate.ts`). Both wire shapes are exported because a
|
|
90
|
+
// BE prefetch cache holds whichever one its quote-center serves: per-symbol
|
|
91
|
+
// Merkle leaves (default) or one indivisible batch envelope (fallback).
|
|
90
92
|
var waterx_rule_ts_1 = require("./rules/waterx-rule.js");
|
|
91
93
|
Object.defineProperty(exports, "WaterxRule", { enumerable: true, get: function () { return waterx_rule_ts_1.WaterxRule; } });
|
|
92
94
|
Object.defineProperty(exports, "parseSignedEnvelope", { enumerable: true, get: function () { return waterx_rule_ts_1.parseSignedEnvelope; } });
|
|
95
|
+
Object.defineProperty(exports, "parseSignedLeaves", { enumerable: true, get: function () { return waterx_rule_ts_1.parseSignedLeaves; } });
|
|
93
96
|
Object.defineProperty(exports, "BATCH_PRICE_INTENT", { enumerable: true, get: function () { return waterx_rule_ts_1.BATCH_PRICE_INTENT; } });
|
|
97
|
+
Object.defineProperty(exports, "MERKLE_ROOT_INTENT", { enumerable: true, get: function () { return waterx_rule_ts_1.MERKLE_ROOT_INTENT; } });
|
|
94
98
|
Object.defineProperty(exports, "WATERX_INFRA", { enumerable: true, get: function () { return waterx_rule_ts_1.WATERX_INFRA; } });
|
|
95
99
|
Object.defineProperty(exports, "waterxQuoteCenterEndpoint", { enumerable: true, get: function () { return waterx_rule_ts_1.waterxQuoteCenterEndpoint; } });
|
|
96
|
-
// Rule-owned payload
|
|
97
|
-
// hand-cast the payload shape.
|
|
100
|
+
// Rule-owned payload accessors (kind-check + unwrap in one place) — never
|
|
101
|
+
// hand-cast the payload shape, and never assume which variant it is.
|
|
102
|
+
Object.defineProperty(exports, "waterxLeavesOf", { enumerable: true, get: function () { return waterx_rule_ts_1.waterxLeavesOf; } });
|
|
98
103
|
Object.defineProperty(exports, "waterxEnvelopeOf", { enumerable: true, get: function () { return waterx_rule_ts_1.waterxEnvelopeOf; } });
|
|
99
104
|
// `resolveOracleRule` is the ONE source→rule registry — exported so external
|
|
100
105
|
// consumers (e.g. a BE prefetch cache that keys per source and needs each
|
|
@@ -83,14 +83,22 @@ export declare function assertRuleUpdateData<T>(data: RuleUpdateData, kind: Pric
|
|
|
83
83
|
* return when its collector-feed leg needs a value produced by the update leg
|
|
84
84
|
* *within the same PTB*. Pyth Core needs none (its feed leg reads the shared
|
|
85
85
|
* `PriceInfoObject` the update leg refreshed), so it returns `void`. The Lazer
|
|
86
|
-
* rule returns the verified
|
|
86
|
+
* rule returns the verified-update result of its network's verify entry — one
|
|
87
87
|
* signature verification covers every feed in the payload, and
|
|
88
88
|
* `pyth_lazer_rule::feed` takes it by reference per ticker (see
|
|
89
89
|
* `aggregateTicker`'s `lazerUpdate` arg).
|
|
90
90
|
*/
|
|
91
91
|
export type RuleUpdateHandle = {
|
|
92
92
|
readonly kind: "pyth_lazer_rule";
|
|
93
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Opaque result of this network's `LAZER_INFRA.verify_entry` in this PTB,
|
|
95
|
+
* passed straight to `pyth_lazer_rule::feed`. The Move type is
|
|
96
|
+
* network-dependent and never named here: mainnet's
|
|
97
|
+
* `pyth_lazer::parse_and_verify_le_ecdsa_update_v2` yields
|
|
98
|
+
* `pyth_lazer::update_v2::Update`, testnet's v1
|
|
99
|
+
* `…_le_ecdsa_update` yields `pyth_lazer::update::Update`, and each
|
|
100
|
+
* network's `pyth_lazer_rule` is published bound to the matching one.
|
|
101
|
+
*/
|
|
94
102
|
readonly update: TransactionArgument;
|
|
95
103
|
};
|
|
96
104
|
/**
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
|
|
5
5
|
* requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
|
|
6
6
|
* via the `pythApiKey` create option), verifies it ONCE on-chain via
|
|
7
|
-
* `pyth_lazer
|
|
8
|
-
* `Update` PTB value back through a `RuleUpdateHandle` for the feed
|
|
7
|
+
* `pyth_lazer`'s verify entry for that network (see `LAZER_INFRA`), and hands the
|
|
8
|
+
* resulting `Update` PTB value back through a `RuleUpdateHandle` for the feed
|
|
9
|
+
* calls.
|
|
9
10
|
*/
|
|
10
11
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
11
12
|
import type { Network } from "../../constants.ts";
|
|
@@ -21,16 +22,33 @@ import { type PriceUpdateRule } from "../price-update-rule.ts";
|
|
|
21
22
|
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
22
23
|
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
23
24
|
* Lazer signers), so both networks share the production host.
|
|
24
|
-
* - `verifier_package` — the Sui package carrying
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* - `verifier_package` / `verify_entry` — the Sui package carrying the verify
|
|
26
|
+
* call, and which entry to call. These track what `pyth_lazer_rule` binds on
|
|
27
|
+
* that network, so they move together:
|
|
28
|
+
* - **mainnet** — the v2 package `0xefbfd064…` and the **v2** entry. The rule
|
|
29
|
+
* was republished v2-bound after a 2026-08-05 mainnet probe: the ORIGINAL
|
|
30
|
+
* package `0x7b502c…` now aborts `EDifferentVersion` (`state::current_cap`)
|
|
31
|
+
* for any payload — the shared `State` has been migrated past that code —
|
|
32
|
+
* and its v1 entry aborts `EInvalidChannel` on `fixed_rate@1000ms`, the only
|
|
33
|
+
* channel WaterX's Pyth Pro grant permits.
|
|
34
|
+
* - **testnet** — still the original v1 publish, which has no `update_v2`
|
|
35
|
+
* module at all, so the v1 entry is the only one that exists there.
|
|
36
|
+
* Both entries take `(state, clock, bytes)` and accept the same `leEcdsa`
|
|
37
|
+
* payload. Values mirror the contract repo's `pyth_lazer_rule/Move.toml`
|
|
38
|
+
* published-at pins.
|
|
29
39
|
*/
|
|
30
40
|
export declare const LAZER_INFRA: Record<Network, {
|
|
31
41
|
endpoint: string;
|
|
32
42
|
verifier_package: string;
|
|
43
|
+
verify_entry: LazerVerifyEntry;
|
|
44
|
+
channel: string;
|
|
33
45
|
}>;
|
|
46
|
+
/**
|
|
47
|
+
* The `pyth_lazer` verify entry a network's deployed rule consumes. `_v2`
|
|
48
|
+
* returns `update_v2::Update`; the v1 entry returns `update::Update`, and the
|
|
49
|
+
* two are NOT interchangeable — the rule's `feed` takes one concrete type.
|
|
50
|
+
*/
|
|
51
|
+
export type LazerVerifyEntry = "parse_and_verify_le_ecdsa_update" | "parse_and_verify_le_ecdsa_update_v2";
|
|
34
52
|
/** `pyth_lazer_rule`'s narrowed `RuleUpdateData.payload` shape. */
|
|
35
53
|
export interface PythLazerUpdatePayload {
|
|
36
54
|
/** One signed `leEcdsa` message carrying every requested feed. */
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* appends per lazer-routed ticker. Fetches one `leEcdsa` payload for all
|
|
6
6
|
* requested integer feed ids from the Lazer HTTP API (Bearer-authenticated
|
|
7
7
|
* via the `pythApiKey` create option), verifies it ONCE on-chain via
|
|
8
|
-
* `pyth_lazer
|
|
9
|
-
* `Update` PTB value back through a `RuleUpdateHandle` for the feed
|
|
8
|
+
* `pyth_lazer`'s verify entry for that network (see `LAZER_INFRA`), and hands the
|
|
9
|
+
* resulting `Update` PTB value back through a `RuleUpdateHandle` for the feed
|
|
10
|
+
* calls.
|
|
10
11
|
*/
|
|
11
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
13
|
exports.PythLazerRule = exports.LazerApiKeyMissingError = exports.LAZER_INFRA = void 0;
|
|
@@ -25,20 +26,33 @@ const update_fetch_ts_1 = require("../update-fetch.js");
|
|
|
25
26
|
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
26
27
|
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
27
28
|
* Lazer signers), so both networks share the production host.
|
|
28
|
-
* - `verifier_package` — the Sui package carrying
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* - `verifier_package` / `verify_entry` — the Sui package carrying the verify
|
|
30
|
+
* call, and which entry to call. These track what `pyth_lazer_rule` binds on
|
|
31
|
+
* that network, so they move together:
|
|
32
|
+
* - **mainnet** — the v2 package `0xefbfd064…` and the **v2** entry. The rule
|
|
33
|
+
* was republished v2-bound after a 2026-08-05 mainnet probe: the ORIGINAL
|
|
34
|
+
* package `0x7b502c…` now aborts `EDifferentVersion` (`state::current_cap`)
|
|
35
|
+
* for any payload — the shared `State` has been migrated past that code —
|
|
36
|
+
* and its v1 entry aborts `EInvalidChannel` on `fixed_rate@1000ms`, the only
|
|
37
|
+
* channel WaterX's Pyth Pro grant permits.
|
|
38
|
+
* - **testnet** — still the original v1 publish, which has no `update_v2`
|
|
39
|
+
* module at all, so the v1 entry is the only one that exists there.
|
|
40
|
+
* Both entries take `(state, clock, bytes)` and accept the same `leEcdsa`
|
|
41
|
+
* payload. Values mirror the contract repo's `pyth_lazer_rule/Move.toml`
|
|
42
|
+
* published-at pins.
|
|
33
43
|
*/
|
|
34
44
|
exports.LAZER_INFRA = {
|
|
35
45
|
MAINNET: {
|
|
36
46
|
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
37
47
|
verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
|
|
48
|
+
verify_entry: "parse_and_verify_le_ecdsa_update_v2",
|
|
49
|
+
channel: "fixed_rate@1000ms",
|
|
38
50
|
},
|
|
39
51
|
TESTNET: {
|
|
40
52
|
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
41
53
|
verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
|
|
54
|
+
verify_entry: "parse_and_verify_le_ecdsa_update",
|
|
55
|
+
channel: "fixed_rate@200ms",
|
|
42
56
|
},
|
|
43
57
|
};
|
|
44
58
|
/**
|
|
@@ -56,9 +70,15 @@ exports.LAZER_INFRA = {
|
|
|
56
70
|
* and every xStock — are `min_channel: fixed_rate@200ms` (Lazer symbol
|
|
57
71
|
* registry, verified 2026-07-22: the same 29-feed batch 400s at
|
|
58
72
|
* `real_time`/`50ms` and serves 200 with the leEcdsa blob at `200ms`).
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
73
|
+
* The channel is therefore per-network (`LAZER_INFRA[network].channel`), and
|
|
74
|
+
* it is bounded from BOTH sides — by what the feeds publish and by what the
|
|
75
|
+
* grant allows:
|
|
76
|
+
* - **mainnet: `fixed_rate@1000ms`.** WaterX's Pyth Pro grant no longer
|
|
77
|
+
* permits anything faster ("Channel fixed_rate@200ms violates rate limit.
|
|
78
|
+
* Minimum allowed channel is 1000ms", measured 2026-08-05), and the
|
|
79
|
+
* mainnet rule is v2-bound, so it accepts that channel.
|
|
80
|
+
* - **testnet: `fixed_rate@200ms`.** Its rule is still v1-bound, and the v1
|
|
81
|
+
* on-chain `channel::from_u8` aborts on the 1000ms channel.
|
|
62
82
|
* - `formats: leEcdsa` + `jsonBinaryEncoding: hex` — the Sui verifier takes
|
|
63
83
|
* the `leEcdsa` framing; hex matches `fromHex` below.
|
|
64
84
|
*/
|
|
@@ -66,7 +86,6 @@ const LAZER_LATEST_PRICE_REQUEST = {
|
|
|
66
86
|
properties: ["price", "exponent", "confidence"],
|
|
67
87
|
formats: ["leEcdsa"],
|
|
68
88
|
jsonBinaryEncoding: "hex",
|
|
69
|
-
channel: "fixed_rate@200ms",
|
|
70
89
|
};
|
|
71
90
|
/**
|
|
72
91
|
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
@@ -109,7 +128,7 @@ function requireLazerPackage(host) {
|
|
|
109
128
|
* retry/timeout/Bearer policy as `fetchPriceFeedsUpdateData`, unified so
|
|
110
129
|
* both oracle sources fail the same way under upstream degradation.
|
|
111
130
|
*/
|
|
112
|
-
async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
|
|
131
|
+
async function fetchLazerSignedUpdate(endpoint, channel, apiKey, feedIds, fetchOpts) {
|
|
113
132
|
// joinEndpointPath preserves any base path on the endpoint — the same
|
|
114
133
|
// leading-slash `new URL` footgun that 404'd every feed on the Pyth Pro
|
|
115
134
|
// Hermes endpoint (see update-fetch.ts). Defensive here: the default
|
|
@@ -120,7 +139,7 @@ async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
|
|
|
120
139
|
res = await (0, update_fetch_ts_1.fetchWithPolicy)(url.toString(), {
|
|
121
140
|
method: "POST",
|
|
122
141
|
headers: { "Content-Type": "application/json" },
|
|
123
|
-
body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST }),
|
|
142
|
+
body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST, channel }),
|
|
124
143
|
}, { apiKey, ...fetchOpts });
|
|
125
144
|
}
|
|
126
145
|
catch (err) {
|
|
@@ -180,13 +199,13 @@ exports.PythLazerRule = {
|
|
|
180
199
|
if (!apiKey) {
|
|
181
200
|
throw new LazerApiKeyMissingError();
|
|
182
201
|
}
|
|
183
|
-
const update = await fetchLazerSignedUpdate(exports.LAZER_INFRA[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
|
|
202
|
+
const update = await fetchLazerSignedUpdate(exports.LAZER_INFRA[host.network].endpoint, exports.LAZER_INFRA[host.network].channel, apiKey, feedIds, host.pyth.fetch);
|
|
184
203
|
return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
|
|
185
204
|
},
|
|
186
205
|
/**
|
|
187
206
|
* A Lazer payload is ONE signed `leEcdsa` message covering every feed it was
|
|
188
207
|
* fetched for — verification is a single flat signature check over the whole
|
|
189
|
-
* message (`parse_and_verify_le_ecdsa_update
|
|
208
|
+
* message (one `parse_and_verify_le_ecdsa_update*` call, no per-feed cost), so the
|
|
190
209
|
* payload is indivisible: it can only be served whole. Returns the whole
|
|
191
210
|
* payload iff every requested ticker's integer feed id is packed in THIS
|
|
192
211
|
* payload's `feedIds`; any coverage gap (unlisted ticker, or a feed this
|
|
@@ -205,19 +224,24 @@ exports.PythLazerRule = {
|
|
|
205
224
|
return { kind: "pyth_lazer_rule", payload };
|
|
206
225
|
},
|
|
207
226
|
/**
|
|
208
|
-
* Appends the single `parse_and_verify_le_ecdsa_update(state, clock, bytes)`
|
|
227
|
+
* Appends the single `parse_and_verify_le_ecdsa_update*(state, clock, bytes)`
|
|
209
228
|
* call — one secp256k1 signature check covering every feed in the payload —
|
|
210
229
|
* and returns its `Update` result as the handle the per-ticker feed leg
|
|
211
230
|
* consumes. `opts.cache` / `opts.feeSource` are Pyth-Core-specific and
|
|
212
231
|
* ignored (Lazer verification charges no update fee).
|
|
232
|
+
*
|
|
233
|
+
* The entry name comes from `LAZER_INFRA[network].verify_entry`: mainnet's
|
|
234
|
+
* rule binds `update_v2`, testnet's is still the v1 publish. Both take
|
|
235
|
+
* `(state, clock, bytes)` and accept the same `leEcdsa` payload.
|
|
213
236
|
*/
|
|
214
237
|
buildUpdateCalls(tx, host, data, _opts) {
|
|
215
238
|
const payload = (0, price_update_rule_ts_1.assertRuleUpdateData)(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
|
|
216
239
|
if (!payload)
|
|
217
240
|
return undefined;
|
|
218
241
|
const lazer = requireLazerPackage(host);
|
|
242
|
+
const infra = exports.LAZER_INFRA[host.network];
|
|
219
243
|
const [update] = tx.moveCall({
|
|
220
|
-
target: `${
|
|
244
|
+
target: `${infra.verifier_package}::pyth_lazer::${infra.verify_entry}`,
|
|
221
245
|
arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
|
|
222
246
|
});
|
|
223
247
|
return { kind: "pyth_lazer_rule", update };
|
|
@@ -1,35 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `WaterxRule` — `PriceUpdateRule` for the first-party WaterX quote-center
|
|
3
|
-
* (Nautilus-TEE, ed25519), plus
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
/**
|
|
30
|
-
* consumers can mirror the rule's own envelope intent check (a
|
|
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
|
-
/**
|
|
92
|
-
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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)` —
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* `
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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;
|