@waterx/sdk 4.3.1 → 4.3.2
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/dist/cjs/src/oracle/aggregate.d.ts +4 -3
- package/dist/cjs/src/oracle/aggregate.js +4 -3
- 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/src/oracle/aggregate.d.ts +4 -3
- package/dist/src/oracle/aggregate.js +4 -3
- 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/package.json +1 -1
|
@@ -38,9 +38,10 @@ import { type WaterxSignedEnvelope } from "./rules/waterx-rule.ts";
|
|
|
38
38
|
* if it is stale — it never aborts — so the call stays mandatory while
|
|
39
39
|
* `pyth_rule` remains in the ticker's on-chain weighted set
|
|
40
40
|
* (`EMissingPriceSource` requires every weighted rule to appear).
|
|
41
|
-
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified
|
|
42
|
-
*
|
|
43
|
-
* (
|
|
41
|
+
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified update this
|
|
42
|
+
* PTB's lazer update leg produced with the network's verify entry
|
|
43
|
+
* (`update_v2::Update` on mainnet, `update::Update` on testnet — see
|
|
44
|
+
* `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
|
|
44
45
|
* not (yet) weight `PythLazerRule`, the contribution is silently dropped
|
|
45
46
|
* on-chain — feeding ahead of the weight migration is harmless.
|
|
46
47
|
* - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
|
|
@@ -90,9 +90,10 @@ async function resolveGroupUpdateData(host, group, provider) {
|
|
|
90
90
|
* if it is stale — it never aborts — so the call stays mandatory while
|
|
91
91
|
* `pyth_rule` remains in the ticker's on-chain weighted set
|
|
92
92
|
* (`EMissingPriceSource` requires every weighted rule to appear).
|
|
93
|
-
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified
|
|
94
|
-
*
|
|
95
|
-
* (
|
|
93
|
+
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified update this
|
|
94
|
+
* PTB's lazer update leg produced with the network's verify entry
|
|
95
|
+
* (`update_v2::Update` on mainnet, `update::Update` on testnet — see
|
|
96
|
+
* `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
|
|
96
97
|
* not (yet) weight `PythLazerRule`, the contribution is silently dropped
|
|
97
98
|
* on-chain — feeding ahead of the weight migration is harmless.
|
|
98
99
|
* - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
|
|
@@ -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 };
|
|
@@ -38,9 +38,10 @@ import { type WaterxSignedEnvelope } from "./rules/waterx-rule.ts";
|
|
|
38
38
|
* if it is stale — it never aborts — so the call stays mandatory while
|
|
39
39
|
* `pyth_rule` remains in the ticker's on-chain weighted set
|
|
40
40
|
* (`EMissingPriceSource` requires every weighted rule to appear).
|
|
41
|
-
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified
|
|
42
|
-
*
|
|
43
|
-
* (
|
|
41
|
+
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified update this
|
|
42
|
+
* PTB's lazer update leg produced with the network's verify entry
|
|
43
|
+
* (`update_v2::Update` on mainnet, `update::Update` on testnet — see
|
|
44
|
+
* `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
|
|
44
45
|
* not (yet) weight `PythLazerRule`, the contribution is silently dropped
|
|
45
46
|
* on-chain — feeding ahead of the weight migration is harmless.
|
|
46
47
|
* - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
|
|
@@ -84,9 +84,10 @@ async function resolveGroupUpdateData(host, group, provider) {
|
|
|
84
84
|
* if it is stale — it never aborts — so the call stays mandatory while
|
|
85
85
|
* `pyth_rule` remains in the ticker's on-chain weighted set
|
|
86
86
|
* (`EMissingPriceSource` requires every weighted rule to appear).
|
|
87
|
-
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified
|
|
88
|
-
*
|
|
89
|
-
* (
|
|
87
|
+
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified update this
|
|
88
|
+
* PTB's lazer update leg produced with the network's verify entry
|
|
89
|
+
* (`update_v2::Update` on mainnet, `update::Update` on testnet — see
|
|
90
|
+
* `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
|
|
90
91
|
* not (yet) weight `PythLazerRule`, the contribution is silently dropped
|
|
91
92
|
* on-chain — feeding ahead of the weight migration is harmless.
|
|
92
93
|
* - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
|
|
@@ -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. */
|
|
@@ -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 { fromHex } from "@mysten/bcs";
|
|
11
12
|
import { ownEntry } from "../../utils/record.js";
|
|
@@ -21,20 +22,33 @@ import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch } from "../upd
|
|
|
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 const LAZER_INFRA = {
|
|
31
41
|
MAINNET: {
|
|
32
42
|
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
33
43
|
verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
|
|
44
|
+
verify_entry: "parse_and_verify_le_ecdsa_update_v2",
|
|
45
|
+
channel: "fixed_rate@1000ms",
|
|
34
46
|
},
|
|
35
47
|
TESTNET: {
|
|
36
48
|
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
37
49
|
verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
|
|
50
|
+
verify_entry: "parse_and_verify_le_ecdsa_update",
|
|
51
|
+
channel: "fixed_rate@200ms",
|
|
38
52
|
},
|
|
39
53
|
};
|
|
40
54
|
/**
|
|
@@ -52,9 +66,15 @@ export const LAZER_INFRA = {
|
|
|
52
66
|
* and every xStock — are `min_channel: fixed_rate@200ms` (Lazer symbol
|
|
53
67
|
* registry, verified 2026-07-22: the same 29-feed batch 400s at
|
|
54
68
|
* `real_time`/`50ms` and serves 200 with the leEcdsa blob at `200ms`).
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
69
|
+
* The channel is therefore per-network (`LAZER_INFRA[network].channel`), and
|
|
70
|
+
* it is bounded from BOTH sides — by what the feeds publish and by what the
|
|
71
|
+
* grant allows:
|
|
72
|
+
* - **mainnet: `fixed_rate@1000ms`.** WaterX's Pyth Pro grant no longer
|
|
73
|
+
* permits anything faster ("Channel fixed_rate@200ms violates rate limit.
|
|
74
|
+
* Minimum allowed channel is 1000ms", measured 2026-08-05), and the
|
|
75
|
+
* mainnet rule is v2-bound, so it accepts that channel.
|
|
76
|
+
* - **testnet: `fixed_rate@200ms`.** Its rule is still v1-bound, and the v1
|
|
77
|
+
* on-chain `channel::from_u8` aborts on the 1000ms channel.
|
|
58
78
|
* - `formats: leEcdsa` + `jsonBinaryEncoding: hex` — the Sui verifier takes
|
|
59
79
|
* the `leEcdsa` framing; hex matches `fromHex` below.
|
|
60
80
|
*/
|
|
@@ -62,7 +82,6 @@ const LAZER_LATEST_PRICE_REQUEST = {
|
|
|
62
82
|
properties: ["price", "exponent", "confidence"],
|
|
63
83
|
formats: ["leEcdsa"],
|
|
64
84
|
jsonBinaryEncoding: "hex",
|
|
65
|
-
channel: "fixed_rate@200ms",
|
|
66
85
|
};
|
|
67
86
|
/**
|
|
68
87
|
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
@@ -104,7 +123,7 @@ function requireLazerPackage(host) {
|
|
|
104
123
|
* retry/timeout/Bearer policy as `fetchPriceFeedsUpdateData`, unified so
|
|
105
124
|
* both oracle sources fail the same way under upstream degradation.
|
|
106
125
|
*/
|
|
107
|
-
async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
|
|
126
|
+
async function fetchLazerSignedUpdate(endpoint, channel, apiKey, feedIds, fetchOpts) {
|
|
108
127
|
// joinEndpointPath preserves any base path on the endpoint — the same
|
|
109
128
|
// leading-slash `new URL` footgun that 404'd every feed on the Pyth Pro
|
|
110
129
|
// Hermes endpoint (see update-fetch.ts). Defensive here: the default
|
|
@@ -115,7 +134,7 @@ async function fetchLazerSignedUpdate(endpoint, apiKey, feedIds, fetchOpts) {
|
|
|
115
134
|
res = await fetchWithPolicy(url.toString(), {
|
|
116
135
|
method: "POST",
|
|
117
136
|
headers: { "Content-Type": "application/json" },
|
|
118
|
-
body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST }),
|
|
137
|
+
body: JSON.stringify({ priceFeedIds: feedIds, ...LAZER_LATEST_PRICE_REQUEST, channel }),
|
|
119
138
|
}, { apiKey, ...fetchOpts });
|
|
120
139
|
}
|
|
121
140
|
catch (err) {
|
|
@@ -175,13 +194,13 @@ export const PythLazerRule = {
|
|
|
175
194
|
if (!apiKey) {
|
|
176
195
|
throw new LazerApiKeyMissingError();
|
|
177
196
|
}
|
|
178
|
-
const update = await fetchLazerSignedUpdate(LAZER_INFRA[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
|
|
197
|
+
const update = await fetchLazerSignedUpdate(LAZER_INFRA[host.network].endpoint, LAZER_INFRA[host.network].channel, apiKey, feedIds, host.pyth.fetch);
|
|
179
198
|
return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
|
|
180
199
|
},
|
|
181
200
|
/**
|
|
182
201
|
* A Lazer payload is ONE signed `leEcdsa` message covering every feed it was
|
|
183
202
|
* fetched for — verification is a single flat signature check over the whole
|
|
184
|
-
* message (`parse_and_verify_le_ecdsa_update
|
|
203
|
+
* message (one `parse_and_verify_le_ecdsa_update*` call, no per-feed cost), so the
|
|
185
204
|
* payload is indivisible: it can only be served whole. Returns the whole
|
|
186
205
|
* payload iff every requested ticker's integer feed id is packed in THIS
|
|
187
206
|
* payload's `feedIds`; any coverage gap (unlisted ticker, or a feed this
|
|
@@ -200,19 +219,24 @@ export const PythLazerRule = {
|
|
|
200
219
|
return { kind: "pyth_lazer_rule", payload };
|
|
201
220
|
},
|
|
202
221
|
/**
|
|
203
|
-
* Appends the single `parse_and_verify_le_ecdsa_update(state, clock, bytes)`
|
|
222
|
+
* Appends the single `parse_and_verify_le_ecdsa_update*(state, clock, bytes)`
|
|
204
223
|
* call — one secp256k1 signature check covering every feed in the payload —
|
|
205
224
|
* and returns its `Update` result as the handle the per-ticker feed leg
|
|
206
225
|
* consumes. `opts.cache` / `opts.feeSource` are Pyth-Core-specific and
|
|
207
226
|
* ignored (Lazer verification charges no update fee).
|
|
227
|
+
*
|
|
228
|
+
* The entry name comes from `LAZER_INFRA[network].verify_entry`: mainnet's
|
|
229
|
+
* rule binds `update_v2`, testnet's is still the v1 publish. Both take
|
|
230
|
+
* `(state, clock, bytes)` and accept the same `leEcdsa` payload.
|
|
208
231
|
*/
|
|
209
232
|
buildUpdateCalls(tx, host, data, _opts) {
|
|
210
233
|
const payload = assertRuleUpdateData(data, "pyth_lazer_rule", isPythLazerUpdatePayloadShape, "{ update: Uint8Array; feedIds: number[] }");
|
|
211
234
|
if (!payload)
|
|
212
235
|
return undefined;
|
|
213
236
|
const lazer = requireLazerPackage(host);
|
|
237
|
+
const infra = LAZER_INFRA[host.network];
|
|
214
238
|
const [update] = tx.moveCall({
|
|
215
|
-
target: `${
|
|
239
|
+
target: `${infra.verifier_package}::pyth_lazer::${infra.verify_entry}`,
|
|
216
240
|
arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
|
|
217
241
|
});
|
|
218
242
|
return { kind: "pyth_lazer_rule", update };
|