@waterx/sdk 4.1.0 → 4.3.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.
- package/README.md +22 -17
- package/dist/cjs/src/account/config.d.ts +3 -2
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +8 -154
- package/dist/cjs/src/oracle/aggregate.d.ts +21 -21
- package/dist/cjs/src/oracle/aggregate.js +118 -69
- package/dist/cjs/src/oracle/config.d.ts +38 -72
- package/dist/cjs/src/oracle/config.js +0 -56
- package/dist/cjs/src/oracle/host.d.ts +24 -20
- package/dist/cjs/src/oracle/index.d.ts +5 -3
- package/dist/cjs/src/oracle/index.js +31 -8
- package/dist/cjs/src/oracle/price-update-rule.d.ts +13 -6
- package/dist/cjs/src/oracle/price-update-rule.js +3 -3
- package/dist/cjs/src/oracle/pyth.d.ts +44 -0
- package/dist/cjs/src/oracle/pyth.js +48 -6
- package/dist/cjs/src/oracle/read-plane.d.ts +70 -0
- package/dist/cjs/src/oracle/read-plane.js +78 -0
- package/dist/cjs/src/oracle/rule-registry.d.ts +6 -4
- package/dist/cjs/src/oracle/rule-registry.js +6 -4
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +4 -4
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +29 -4
- package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -4
- package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +42 -5
- package/dist/cjs/src/oracle/rules/waterx-rule.js +57 -17
- package/dist/cjs/src/perp/client.d.ts +40 -27
- package/dist/cjs/src/perp/client.js +28 -20
- package/dist/cjs/src/perp/config.d.ts +6 -6
- package/dist/cjs/src/perp/config.js +12 -12
- package/dist/cjs/src/perp/index.d.ts +3 -3
- package/dist/cjs/src/perp/index.js +7 -4
- package/dist/cjs/src/unified-client.d.ts +14 -9
- package/dist/cjs/src/unified-client.js +2 -2
- package/dist/src/account/config.d.ts +3 -2
- package/dist/src/generated/waterx_rule/waterx_rule.d.ts +7 -134
- package/dist/src/generated/waterx_rule/waterx_rule.js +7 -143
- package/dist/src/oracle/aggregate.d.ts +21 -21
- package/dist/src/oracle/aggregate.js +118 -69
- package/dist/src/oracle/config.d.ts +38 -72
- package/dist/src/oracle/config.js +1 -55
- package/dist/src/oracle/host.d.ts +24 -20
- package/dist/src/oracle/index.d.ts +5 -3
- package/dist/src/oracle/index.js +25 -10
- package/dist/src/oracle/price-update-rule.d.ts +13 -6
- package/dist/src/oracle/price-update-rule.js +3 -3
- package/dist/src/oracle/pyth.d.ts +44 -0
- package/dist/src/oracle/pyth.js +45 -5
- package/dist/src/oracle/read-plane.d.ts +70 -0
- package/dist/src/oracle/read-plane.js +74 -0
- package/dist/src/oracle/rule-registry.d.ts +6 -4
- package/dist/src/oracle/rule-registry.js +6 -4
- package/dist/src/oracle/rules/pyth-core-rule.js +5 -5
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +28 -3
- package/dist/src/oracle/rules/pyth-rule.js +5 -4
- package/dist/src/oracle/rules/waterx-rule.d.ts +42 -5
- package/dist/src/oracle/rules/waterx-rule.js +53 -14
- package/dist/src/perp/client.d.ts +40 -27
- package/dist/src/perp/client.js +29 -21
- package/dist/src/perp/config.d.ts +6 -6
- package/dist/src/perp/config.js +11 -9
- package/dist/src/perp/index.d.ts +3 -3
- package/dist/src/perp/index.js +2 -2
- package/dist/src/unified-client.d.ts +14 -9
- package/dist/src/unified-client.js +2 -2
- package/package.json +1 -1
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
* remain plain collector-feed helpers wired directly into `aggregate.ts`.
|
|
12
12
|
*
|
|
13
13
|
* This file defines the port only — routing IS wired: `aggregate.ts`'s
|
|
14
|
-
* `refreshOraclePrices`
|
|
15
|
-
* `rule-registry.ts`, then drives fetch + `buildUpdateCalls`
|
|
16
|
-
* port; `aggregate.ts` stays the sole orchestrator.
|
|
14
|
+
* `refreshOraclePrices` resolves a concrete rule per `host.oracleSources`
|
|
15
|
+
* entry via `rule-registry.ts`, then drives fetch + `buildUpdateCalls`
|
|
16
|
+
* through this port; `aggregate.ts` stays the sole orchestrator.
|
|
17
17
|
*/
|
|
18
18
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
19
19
|
import type { OracleHost } from "./host.ts";
|
|
20
20
|
import type { OracleFeeSource, PythCache } from "./pyth.ts";
|
|
21
21
|
export type PriceUpdateRuleKind = "pyth_rule" | "pyth_lazer_rule" | "supra_rule" | "constant_rule" | "waterx_rule";
|
|
22
22
|
/**
|
|
23
|
-
* The subset of `PriceUpdateRuleKind`s
|
|
24
|
-
* create option (see `OracleHost.
|
|
23
|
+
* The subset of `PriceUpdateRuleKind`s listable in a client's `oracleSource`
|
|
24
|
+
* create option (see `OracleHost.oracleSources`) — i.e. rules that can serve as
|
|
25
25
|
* the on-chain price *update* leg `refreshOraclePrices` runs before aggregating.
|
|
26
26
|
* `supra_rule` and `constant_rule` are auxiliary rules fed alongside whichever
|
|
27
27
|
* source is selected (see `aggregateTicker`), not sources themselves. The SDK
|
|
@@ -116,7 +116,14 @@ export interface UpdateDataProvider {
|
|
|
116
116
|
get(source: OracleSource, tickers: string[]): Promise<RuleUpdateData | null>;
|
|
117
117
|
}
|
|
118
118
|
export interface PriceUpdateRule {
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* `OracleSource`, not the wider `PriceUpdateRuleKind`: only selectable
|
|
121
|
+
* sources implement this port (`supra_rule`/`constant_rule` are plain
|
|
122
|
+
* collector-feed helpers), and the narrower type is what lets
|
|
123
|
+
* `refreshOraclePrices`'s per-source carry step switch exhaustively —
|
|
124
|
+
* adding a source without deciding its carry becomes a compile error.
|
|
125
|
+
*/
|
|
126
|
+
readonly kind: OracleSource;
|
|
120
127
|
/**
|
|
121
128
|
* `true` when this rule's on-chain update leg charges a per-update fee
|
|
122
129
|
* that must be paid from either a sponsor fund or `tx.gas` (Pyth Core:
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* remain plain collector-feed helpers wired directly into `aggregate.ts`.
|
|
12
12
|
*
|
|
13
13
|
* This file defines the port only — routing IS wired: `aggregate.ts`'s
|
|
14
|
-
* `refreshOraclePrices`
|
|
15
|
-
* `rule-registry.ts`, then drives fetch + `buildUpdateCalls`
|
|
16
|
-
* port; `aggregate.ts` stays the sole orchestrator.
|
|
14
|
+
* `refreshOraclePrices` resolves a concrete rule per `host.oracleSources`
|
|
15
|
+
* entry via `rule-registry.ts`, then drives fetch + `buildUpdateCalls`
|
|
16
|
+
* through this port; `aggregate.ts` stays the sole orchestrator.
|
|
17
17
|
*/
|
|
18
18
|
/**
|
|
19
19
|
* Shared null → kind → shape guard ladder for a `PriceUpdateRule.buildUpdateCalls`
|
|
@@ -14,8 +14,52 @@
|
|
|
14
14
|
* 4. hot_potato_vector::destroy
|
|
15
15
|
*/
|
|
16
16
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
17
|
+
import type { Network } from "../constants.ts";
|
|
17
18
|
import type { PythFetchPolicy } from "./config.ts";
|
|
18
19
|
import type { OracleHost } from "./host.ts";
|
|
20
|
+
/**
|
|
21
|
+
* Everything the Core source needs that is not in the canonical
|
|
22
|
+
* `waterx-config` JSON lives HERE, in the source that consumes it —
|
|
23
|
+
* `client.pyth` carries only the caller's credential/fetch policy
|
|
24
|
+
* (see `PythAccessConfig`). No other oracle source reads this table:
|
|
25
|
+
* selecting `pyth_lazer_rule` (or any future source) must never touch a Core
|
|
26
|
+
* endpoint or Core state object, so nothing Core-shaped stays on the shared
|
|
27
|
+
* client to leak across sources.
|
|
28
|
+
*
|
|
29
|
+
* - `state_id` / `wormhole_state_id` — the Core Pyth + Wormhole state objects
|
|
30
|
+
* the on-chain update PTB reads (below).
|
|
31
|
+
* - `hermes_endpoint` — the Core Hermes REST base for
|
|
32
|
+
* `fetchPriceFeedsUpdateData`. Keyless today; auth-first after the Pyth Pro
|
|
33
|
+
* migration (post-2026-08-18) via `client.pyth.api_key`.
|
|
34
|
+
*/
|
|
35
|
+
export interface PythCoreInfra {
|
|
36
|
+
state_id: string;
|
|
37
|
+
wormhole_state_id: string;
|
|
38
|
+
hermes_endpoint: string;
|
|
39
|
+
}
|
|
40
|
+
export declare const PYTH_CORE_INFRA: Record<Network, PythCoreInfra>;
|
|
41
|
+
/**
|
|
42
|
+
* The Core source's Hermes REST base for `network` — for consumers (BE/FE
|
|
43
|
+
* read planes) whose `ORACLE_SOURCE` fed set includes `'pyth_rule'`. A fed
|
|
44
|
+
* set WITHOUT `pyth_rule` reads the keyed Pyth Pro base instead — resolve
|
|
45
|
+
* through `resolveHermesReadEndpoint` (`oracle/read-plane.ts`) rather than
|
|
46
|
+
* branching by hand.
|
|
47
|
+
*/
|
|
48
|
+
export declare function pythCoreHermesEndpoint(network: Network): string;
|
|
49
|
+
/**
|
|
50
|
+
* The Pyth Pro Hermes-compatible REST base — the documented drop-in
|
|
51
|
+
* replacement for the Core endpoint (`https://pyth.dourolabs.app/hermes/…`),
|
|
52
|
+
* IDENTICAL for every subscriber: only the `Authorization: Bearer` key
|
|
53
|
+
* (`client.pyth.api_key` / the `pythApiKey` create option) is
|
|
54
|
+
* account-specific, so the URL is SDK infra, not deployment config. One base
|
|
55
|
+
* for both networks — Pro has no testnet variant (Pyth feed ids are
|
|
56
|
+
* chain-agnostic; `hermes-beta` is a Core-testnet concern). Consumers with a
|
|
57
|
+
* proxy/self-hosted mirror override it per deployment via
|
|
58
|
+
* `resolveHermesReadEndpoint`'s `override` param.
|
|
59
|
+
*/
|
|
60
|
+
export declare const PYTH_PRO_HERMES_ENDPOINT = "https://pyth.dourolabs.app/hermes";
|
|
61
|
+
/** Accessor mirror of {@link pythCoreHermesEndpoint} for the Pro base. */
|
|
62
|
+
export declare function pythProHermesEndpoint(): string;
|
|
19
63
|
type PriceTableInfo = {
|
|
20
64
|
id: string;
|
|
21
65
|
fieldType: string;
|
package/dist/src/oracle/pyth.js
CHANGED
|
@@ -16,6 +16,44 @@
|
|
|
16
16
|
import { fromHex, toHex } from "@mysten/bcs";
|
|
17
17
|
import { bcs } from "@mysten/sui/bcs";
|
|
18
18
|
import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch, trimTrailingSlashes, } from "./update-fetch.js";
|
|
19
|
+
export const PYTH_CORE_INFRA = {
|
|
20
|
+
MAINNET: {
|
|
21
|
+
state_id: "0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8",
|
|
22
|
+
wormhole_state_id: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
|
|
23
|
+
hermes_endpoint: "https://hermes.pyth.network",
|
|
24
|
+
},
|
|
25
|
+
TESTNET: {
|
|
26
|
+
state_id: "0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c",
|
|
27
|
+
wormhole_state_id: "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
|
|
28
|
+
hermes_endpoint: "https://hermes-beta.pyth.network",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The Core source's Hermes REST base for `network` — for consumers (BE/FE
|
|
33
|
+
* read planes) whose `ORACLE_SOURCE` fed set includes `'pyth_rule'`. A fed
|
|
34
|
+
* set WITHOUT `pyth_rule` reads the keyed Pyth Pro base instead — resolve
|
|
35
|
+
* through `resolveHermesReadEndpoint` (`oracle/read-plane.ts`) rather than
|
|
36
|
+
* branching by hand.
|
|
37
|
+
*/
|
|
38
|
+
export function pythCoreHermesEndpoint(network) {
|
|
39
|
+
return PYTH_CORE_INFRA[network].hermes_endpoint;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The Pyth Pro Hermes-compatible REST base — the documented drop-in
|
|
43
|
+
* replacement for the Core endpoint (`https://pyth.dourolabs.app/hermes/…`),
|
|
44
|
+
* IDENTICAL for every subscriber: only the `Authorization: Bearer` key
|
|
45
|
+
* (`client.pyth.api_key` / the `pythApiKey` create option) is
|
|
46
|
+
* account-specific, so the URL is SDK infra, not deployment config. One base
|
|
47
|
+
* for both networks — Pro has no testnet variant (Pyth feed ids are
|
|
48
|
+
* chain-agnostic; `hermes-beta` is a Core-testnet concern). Consumers with a
|
|
49
|
+
* proxy/self-hosted mirror override it per deployment via
|
|
50
|
+
* `resolveHermesReadEndpoint`'s `override` param.
|
|
51
|
+
*/
|
|
52
|
+
export const PYTH_PRO_HERMES_ENDPOINT = "https://pyth.dourolabs.app/hermes";
|
|
53
|
+
/** Accessor mirror of {@link pythCoreHermesEndpoint} for the Pro base. */
|
|
54
|
+
export function pythProHermesEndpoint() {
|
|
55
|
+
return PYTH_PRO_HERMES_ENDPOINT;
|
|
56
|
+
}
|
|
19
57
|
export class PythCache {
|
|
20
58
|
pythStateInfo;
|
|
21
59
|
wormholePackageId;
|
|
@@ -428,7 +466,7 @@ async function getPriceTableInfo(client, pythStateId, cache) {
|
|
|
428
466
|
}
|
|
429
467
|
async function getPriceFeedObjectId(client, table, feedId, cache, pythStateId) {
|
|
430
468
|
const normalized = feedId.replace(/^0x/, "");
|
|
431
|
-
const cacheKey =
|
|
469
|
+
const cacheKey = `${pythStateId}:${normalized}`;
|
|
432
470
|
if (cache?.priceFeedObjectIdCache.has(cacheKey)) {
|
|
433
471
|
return cache.priceFeedObjectIdCache.get(cacheKey);
|
|
434
472
|
}
|
|
@@ -523,7 +561,9 @@ export async function buildPythPriceUpdateCalls(tx, host, updates, feedIds, opts
|
|
|
523
561
|
throw new OracleFeeSourceUnavailableError();
|
|
524
562
|
}
|
|
525
563
|
const cache = opts?.cache;
|
|
526
|
-
|
|
564
|
+
// Core on-chain infra is the `pyth_rule` source's own table, keyed by the
|
|
565
|
+
// host's network — `client.pyth` carries only the caller's credential/policy.
|
|
566
|
+
const pyth = PYTH_CORE_INFRA[host.network];
|
|
527
567
|
const [stateInfo, wormholePackageId, table] = await Promise.all([
|
|
528
568
|
getPythStateInfo(host.grpcClient, pyth.state_id, cache),
|
|
529
569
|
getWormholePackageId(host.grpcClient, pyth.wormhole_state_id, cache),
|
|
@@ -581,9 +621,9 @@ export async function buildPythPriceUpdateCalls(tx, host, updates, feedIds, opts
|
|
|
581
621
|
}
|
|
582
622
|
/** All-in-one: fetch from Hermes, append update calls. Returns PriceInfoObject IDs. */
|
|
583
623
|
export async function updatePythPrices(tx, host, feedIds, opts) {
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
const endpoint = host.
|
|
624
|
+
// The Core source's own Hermes endpoint (per-network, rule-owned table);
|
|
625
|
+
// credential + fetch policy are the caller-supplied `client.pyth` slice.
|
|
626
|
+
const endpoint = PYTH_CORE_INFRA[host.network].hermes_endpoint;
|
|
587
627
|
const updates = await fetchPriceFeedsUpdateData(endpoint, feedIds, {
|
|
588
628
|
apiKey: host.pyth.api_key,
|
|
589
629
|
fetch: host.pyth.fetch,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read-plane.ts` — per-source READ-plane resolution: which of a caller's
|
|
3
|
+
* tickers a source can PRICE off-chain, and with which ids. The write plane
|
|
4
|
+
* (`PriceUpdateRule` + each source's `feeds` block) and the read plane are
|
|
5
|
+
* DIFFERENT namespaces for the pyth sources: Lazer WRITES with the integer
|
|
6
|
+
* ids in `pyth_lazer_rule.feeds`, but every Hermes-compatible price READ is
|
|
7
|
+
* keyed by the hex ids only `pyth_rule.feeds` carries. That cross-block
|
|
8
|
+
* dependency is a config invariant ("a lazer-fed ticker must also carry a
|
|
9
|
+
* Core hex entry to be read-visible"), and it lives HERE, once — consumers
|
|
10
|
+
* (FE/BE price facades) resolve through this instead of hardcoding which
|
|
11
|
+
* sources share which feeds namespace.
|
|
12
|
+
*/
|
|
13
|
+
import type { Network } from "../constants.ts";
|
|
14
|
+
import type { OracleHost } from "./host.ts";
|
|
15
|
+
import type { OracleSource } from "./price-update-rule.ts";
|
|
16
|
+
/**
|
|
17
|
+
* One source's read plan for a requested ticker set.
|
|
18
|
+
*
|
|
19
|
+
* - `plane: "hermes"` (pyth sources) — price via a Hermes-compatible REST
|
|
20
|
+
* endpoint, one entry per servable ticker mapped to its HEX feed id. The
|
|
21
|
+
* endpoint to execute against is {@link resolveHermesReadEndpoint} — the
|
|
22
|
+
* plan (ids) plus that resolver (host) is the complete read contract.
|
|
23
|
+
* - `plane: "quote_center"` (waterx) — price via the quote-center symbols
|
|
24
|
+
* api, keyed by ticker; served set = the `waterx_rule.feeds` block. An
|
|
25
|
+
* ABSENT block (source listed, package missing from the loaded config)
|
|
26
|
+
* serves NOTHING: claiming tickers would silently reroute reads to the
|
|
27
|
+
* quote-center — it happily serves symbols regardless of on-chain config —
|
|
28
|
+
* and swallow tickers a later-listed source could price. The
|
|
29
|
+
* misconfiguration is caught loudly by the consumer's feeds assert at
|
|
30
|
+
* client creation instead.
|
|
31
|
+
* - `unreadable` — requested tickers this source WRITES on-chain (its update
|
|
32
|
+
* leg serves them) but its read plane cannot price: the silent-invisibility
|
|
33
|
+
* trap (e.g. a lazer-fed ticker with no `pyth_rule.feeds` hex entry).
|
|
34
|
+
* Callers should surface these loudly; empty for sources whose write and
|
|
35
|
+
* read namespaces coincide.
|
|
36
|
+
*/
|
|
37
|
+
export type OracleReadPlan = {
|
|
38
|
+
unreadable: string[];
|
|
39
|
+
} & ({
|
|
40
|
+
plane: "hermes";
|
|
41
|
+
feedIdByTicker: Map<string, string>;
|
|
42
|
+
} | {
|
|
43
|
+
plane: "quote_center";
|
|
44
|
+
tickers: string[];
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* Resolve `source`'s read plan for `tickers`. Pure config lookup — no
|
|
48
|
+
* network, no endpoint resolution (endpoints come from
|
|
49
|
+
* `pythCoreHermesEndpoint` / `waterxQuoteCenterEndpoint` / the deployment's
|
|
50
|
+
* own env). A ticker absent from the returned plan is simply not servable by
|
|
51
|
+
* THIS source's read plane — callers decide how to degrade (typically: ask
|
|
52
|
+
* the next source in their `ORACLE_SOURCE` list, then omit).
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* The Hermes-compatible REST base a deployment's hermes-plane read plans
|
|
56
|
+
* execute against — the endpoint half of the read contract
|
|
57
|
+
* ({@link resolveOracleReadPlan} is the ids half):
|
|
58
|
+
*
|
|
59
|
+
* - `pyth_rule` in the fed set → the Core source's own keyless endpoint
|
|
60
|
+
* (`pythCoreHermesEndpoint(network)`).
|
|
61
|
+
* - otherwise → `override` when the deployment set one (a proxy or
|
|
62
|
+
* self-hosted mirror), else the documented Pyth Pro base
|
|
63
|
+
* (`pythProHermesEndpoint()` — identical for every subscriber; auth is the
|
|
64
|
+
* caller's `pythApiKey` Bearer, not a per-deployment URL).
|
|
65
|
+
*
|
|
66
|
+
* Total — never throws, never falls back Core-ward: a fed set without
|
|
67
|
+
* `pyth_rule` reads Pro (or the override), full stop.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveHermesReadEndpoint(network: Network, sources: readonly OracleSource[], override?: string): string;
|
|
70
|
+
export declare function resolveOracleReadPlan(host: OracleHost, source: OracleSource, tickers: string[]): OracleReadPlan;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read-plane.ts` — per-source READ-plane resolution: which of a caller's
|
|
3
|
+
* tickers a source can PRICE off-chain, and with which ids. The write plane
|
|
4
|
+
* (`PriceUpdateRule` + each source's `feeds` block) and the read plane are
|
|
5
|
+
* DIFFERENT namespaces for the pyth sources: Lazer WRITES with the integer
|
|
6
|
+
* ids in `pyth_lazer_rule.feeds`, but every Hermes-compatible price READ is
|
|
7
|
+
* keyed by the hex ids only `pyth_rule.feeds` carries. That cross-block
|
|
8
|
+
* dependency is a config invariant ("a lazer-fed ticker must also carry a
|
|
9
|
+
* Core hex entry to be read-visible"), and it lives HERE, once — consumers
|
|
10
|
+
* (FE/BE price facades) resolve through this instead of hardcoding which
|
|
11
|
+
* sources share which feeds namespace.
|
|
12
|
+
*/
|
|
13
|
+
import { pythCoreHermesEndpoint, pythProHermesEndpoint } from "./pyth.js";
|
|
14
|
+
/**
|
|
15
|
+
* Resolve `source`'s read plan for `tickers`. Pure config lookup — no
|
|
16
|
+
* network, no endpoint resolution (endpoints come from
|
|
17
|
+
* `pythCoreHermesEndpoint` / `waterxQuoteCenterEndpoint` / the deployment's
|
|
18
|
+
* own env). A ticker absent from the returned plan is simply not servable by
|
|
19
|
+
* THIS source's read plane — callers decide how to degrade (typically: ask
|
|
20
|
+
* the next source in their `ORACLE_SOURCE` list, then omit).
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The Hermes-compatible REST base a deployment's hermes-plane read plans
|
|
24
|
+
* execute against — the endpoint half of the read contract
|
|
25
|
+
* ({@link resolveOracleReadPlan} is the ids half):
|
|
26
|
+
*
|
|
27
|
+
* - `pyth_rule` in the fed set → the Core source's own keyless endpoint
|
|
28
|
+
* (`pythCoreHermesEndpoint(network)`).
|
|
29
|
+
* - otherwise → `override` when the deployment set one (a proxy or
|
|
30
|
+
* self-hosted mirror), else the documented Pyth Pro base
|
|
31
|
+
* (`pythProHermesEndpoint()` — identical for every subscriber; auth is the
|
|
32
|
+
* caller's `pythApiKey` Bearer, not a per-deployment URL).
|
|
33
|
+
*
|
|
34
|
+
* Total — never throws, never falls back Core-ward: a fed set without
|
|
35
|
+
* `pyth_rule` reads Pro (or the override), full stop.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveHermesReadEndpoint(network, sources, override) {
|
|
38
|
+
if (sources.includes("pyth_rule"))
|
|
39
|
+
return pythCoreHermesEndpoint(network);
|
|
40
|
+
return override ?? pythProHermesEndpoint();
|
|
41
|
+
}
|
|
42
|
+
export function resolveOracleReadPlan(host, source, tickers) {
|
|
43
|
+
switch (source) {
|
|
44
|
+
case "pyth_rule":
|
|
45
|
+
case "pyth_lazer_rule": {
|
|
46
|
+
const hexFeeds = host.config.packages.pyth_rule?.feeds;
|
|
47
|
+
const feedIdByTicker = new Map();
|
|
48
|
+
for (const ticker of tickers) {
|
|
49
|
+
const feedId = hexFeeds?.[ticker]?.feed_id;
|
|
50
|
+
if (feedId !== undefined)
|
|
51
|
+
feedIdByTicker.set(ticker, feedId);
|
|
52
|
+
}
|
|
53
|
+
// For pyth_rule the write and read namespaces coincide, so `unreadable`
|
|
54
|
+
// is always empty; for lazer it is exactly the hex-entry gap.
|
|
55
|
+
const writeFeeds = source === "pyth_lazer_rule" ? host.config.packages.pyth_lazer_rule?.feeds : hexFeeds;
|
|
56
|
+
const unreadable = tickers.filter((ticker) => writeFeeds?.[ticker] !== undefined && !feedIdByTicker.has(ticker));
|
|
57
|
+
return { plane: "hermes", feedIdByTicker, unreadable };
|
|
58
|
+
}
|
|
59
|
+
case "waterx_rule": {
|
|
60
|
+
// Absent feeds block ⇒ serves nothing (see the OracleReadPlan doc) —
|
|
61
|
+
// never claim tickers the config doesn't name.
|
|
62
|
+
const feeds = host.config.packages.waterx_rule?.feeds;
|
|
63
|
+
return {
|
|
64
|
+
plane: "quote_center",
|
|
65
|
+
tickers: feeds ? tickers.filter((ticker) => ticker in feeds) : [],
|
|
66
|
+
unreadable: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
default: {
|
|
70
|
+
const exhausted = source;
|
|
71
|
+
throw new Error(`resolveOracleReadPlan: unhandled OracleSource '${String(exhausted)}'`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `rule-registry.ts` — maps a client-selected `OracleSource` to its concrete
|
|
3
|
-
* `PriceUpdateRule` implementation
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* `PriceUpdateRule` implementation; the ONE place `OracleSource` values are
|
|
4
|
+
* wired to a rule instance. `refreshOraclePrices` (`aggregate.ts`) resolves
|
|
5
|
+
* every `host.oracleSources` entry through it, and consumers (e.g. a BE
|
|
6
|
+
* prefetch cache keying per source) import `resolveOracleRule` rather than
|
|
7
|
+
* hand-mirroring this map. Selection is driven purely by the value passed in
|
|
8
|
+
* (ultimately `OracleHost.oracleSources`, the `oracleSource` client create
|
|
7
9
|
* option) — never by a config JSON `enabled` flag and never by `process.env`.
|
|
8
10
|
*
|
|
9
11
|
* Each source is self-contained: it owns its own infra + config and does NOT
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `rule-registry.ts` — maps a client-selected `OracleSource` to its concrete
|
|
3
|
-
* `PriceUpdateRule` implementation
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* `PriceUpdateRule` implementation; the ONE place `OracleSource` values are
|
|
4
|
+
* wired to a rule instance. `refreshOraclePrices` (`aggregate.ts`) resolves
|
|
5
|
+
* every `host.oracleSources` entry through it, and consumers (e.g. a BE
|
|
6
|
+
* prefetch cache keying per source) import `resolveOracleRule` rather than
|
|
7
|
+
* hand-mirroring this map. Selection is driven purely by the value passed in
|
|
8
|
+
* (ultimately `OracleHost.oracleSources`, the `oracleSource` client create
|
|
7
9
|
* option) — never by a config JSON `enabled` flag and never by `process.env`.
|
|
8
10
|
*
|
|
9
11
|
* Each source is self-contained: it owns its own infra + config and does NOT
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* changes vs `../pyth.ts` / `./pyth-rule.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import { assertRuleUpdateData, } from "../price-update-rule.js";
|
|
10
|
-
import { buildPythPriceUpdateCalls, endpointSupportedFeedIds, fetchPriceFeedsUpdateData, } from "../pyth.js";
|
|
10
|
+
import { buildPythPriceUpdateCalls, endpointSupportedFeedIds, fetchPriceFeedsUpdateData, pythCoreHermesEndpoint, } from "../pyth.js";
|
|
11
11
|
/**
|
|
12
12
|
* Shape check ONLY — the `kind` discriminant is checked separately by the
|
|
13
13
|
* caller before this runs, since a same-shaped payload from a different rule
|
|
@@ -33,10 +33,10 @@ export const PythCoreRule = {
|
|
|
33
33
|
async fetchUpdateData(host, tickers) {
|
|
34
34
|
if (tickers.length === 0)
|
|
35
35
|
return null;
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
const endpoint = host.
|
|
36
|
+
// Endpoint is this source's own per-network infra (`PYTH_CORE_INFRA`);
|
|
37
|
+
// credential + retry/timeout policy are the caller-supplied `client.pyth`
|
|
38
|
+
// access slice.
|
|
39
|
+
const endpoint = pythCoreHermesEndpoint(host.network);
|
|
40
40
|
const feedIds = tickers.map((ticker) => host.getPythFeed(ticker).feed_id);
|
|
41
41
|
const updates = await fetchPriceFeedsUpdateData(endpoint, feedIds, {
|
|
42
42
|
apiKey: host.pyth.api_key,
|
|
@@ -8,8 +8,29 @@
|
|
|
8
8
|
* `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
|
|
9
9
|
*/
|
|
10
10
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
11
|
+
import type { Network } from "../../constants.ts";
|
|
11
12
|
import type { OracleHost } from "../host.ts";
|
|
12
13
|
import { type PriceUpdateRule } from "../price-update-rule.ts";
|
|
14
|
+
/**
|
|
15
|
+
* Pyth Lazer (Pyth Pro) external infra — owned by THIS source, by network.
|
|
16
|
+
* Per-network constants for infrastructure Pyth operates (not part of the
|
|
17
|
+
* `waterx-config` JSON), co-located with the only rule that reads them —
|
|
18
|
+
* no other oracle source ever touches a Lazer endpoint or verifier.
|
|
19
|
+
*
|
|
20
|
+
* - `endpoint` — Lazer HTTP API base; signed updates come from
|
|
21
|
+
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
22
|
+
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
23
|
+
* Lazer signers), so both networks share the production host.
|
|
24
|
+
* - `verifier_package` — the Sui package carrying
|
|
25
|
+
* `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
|
|
26
|
+
* still the original v1 publish; mainnet is the v2-upgraded package (which
|
|
27
|
+
* still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
|
|
28
|
+
* contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
|
|
29
|
+
*/
|
|
30
|
+
export declare const LAZER_INFRA: Record<Network, {
|
|
31
|
+
endpoint: string;
|
|
32
|
+
verifier_package: string;
|
|
33
|
+
}>;
|
|
13
34
|
/** `pyth_lazer_rule`'s narrowed `RuleUpdateData.payload` shape. */
|
|
14
35
|
export interface PythLazerUpdatePayload {
|
|
15
36
|
/** One signed `leEcdsa` message carrying every requested feed. */
|
|
@@ -8,9 +8,34 @@
|
|
|
8
8
|
* `Update` PTB value back through a `RuleUpdateHandle` for the feed calls.
|
|
9
9
|
*/
|
|
10
10
|
import { fromHex } from "@mysten/bcs";
|
|
11
|
-
import { LAZER_DEFAULTS } from "../config.js";
|
|
12
11
|
import { assertRuleUpdateData, } from "../price-update-rule.js";
|
|
13
12
|
import { fetchWithPolicy, joinEndpointPath, rethrowExhaustedFetch } from "../update-fetch.js";
|
|
13
|
+
/**
|
|
14
|
+
* Pyth Lazer (Pyth Pro) external infra — owned by THIS source, by network.
|
|
15
|
+
* Per-network constants for infrastructure Pyth operates (not part of the
|
|
16
|
+
* `waterx-config` JSON), co-located with the only rule that reads them —
|
|
17
|
+
* no other oracle source ever touches a Lazer endpoint or verifier.
|
|
18
|
+
*
|
|
19
|
+
* - `endpoint` — Lazer HTTP API base; signed updates come from
|
|
20
|
+
* `POST /v1/latest_price` (Bearer-authenticated). The service is
|
|
21
|
+
* network-agnostic (one signed payload verifies on any chain that trusts the
|
|
22
|
+
* Lazer signers), so both networks share the production host.
|
|
23
|
+
* - `verifier_package` — the Sui package carrying
|
|
24
|
+
* `pyth_lazer::parse_and_verify_le_ecdsa_update`. Per-network: testnet is
|
|
25
|
+
* still the original v1 publish; mainnet is the v2-upgraded package (which
|
|
26
|
+
* still exposes the v1 entry `pyth_lazer_rule` binds). Values mirror the
|
|
27
|
+
* contract repo's `pyth_lazer_rule/Move.toml` published-at pins.
|
|
28
|
+
*/
|
|
29
|
+
export const LAZER_INFRA = {
|
|
30
|
+
MAINNET: {
|
|
31
|
+
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
32
|
+
verifier_package: "0xefbfd064480777699fd9c557a5804d72ace7bc82661fdc8d1f1a44ea6d92ee10",
|
|
33
|
+
},
|
|
34
|
+
TESTNET: {
|
|
35
|
+
endpoint: "https://pyth-lazer.dourolabs.app",
|
|
36
|
+
verifier_package: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
14
39
|
/**
|
|
15
40
|
* Signed-update request pins, mirroring what the on-chain rule consumes:
|
|
16
41
|
* - `properties` — `price` + `exponent` are REQUIRED by
|
|
@@ -147,7 +172,7 @@ export const PythLazerRule = {
|
|
|
147
172
|
if (!apiKey) {
|
|
148
173
|
throw new LazerApiKeyMissingError();
|
|
149
174
|
}
|
|
150
|
-
const update = await fetchLazerSignedUpdate(
|
|
175
|
+
const update = await fetchLazerSignedUpdate(LAZER_INFRA[host.network].endpoint, apiKey, feedIds, host.pyth.fetch);
|
|
151
176
|
return { kind: "pyth_lazer_rule", payload: { update, feedIds } };
|
|
152
177
|
},
|
|
153
178
|
/**
|
|
@@ -184,7 +209,7 @@ export const PythLazerRule = {
|
|
|
184
209
|
return undefined;
|
|
185
210
|
const lazer = requireLazerPackage(host);
|
|
186
211
|
const [update] = tx.moveCall({
|
|
187
|
-
target: `${
|
|
212
|
+
target: `${LAZER_INFRA[host.network].verifier_package}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
|
|
188
213
|
arguments: [tx.object(lazer.state), tx.object.clock(), tx.pure.vector("u8", payload.update)],
|
|
189
214
|
});
|
|
190
215
|
return { kind: "pyth_lazer_rule", update };
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* `PriceInfoObject` is fresh.
|
|
8
8
|
*/
|
|
9
9
|
import { feed as pythRuleFeed } from "../../generated/waterx_pyth_rule/pyth_rule.js";
|
|
10
|
+
import { PYTH_CORE_INFRA } from "../pyth.js";
|
|
10
11
|
export function feedPythRule(tx, host, collector, priceInfoObjectId) {
|
|
11
12
|
pythRuleFeed({
|
|
12
13
|
package: host.config.packages.pyth_rule.published_at,
|
|
@@ -15,10 +16,10 @@ export function feedPythRule(tx, host, collector, priceInfoObjectId) {
|
|
|
15
16
|
config: tx.object(host.config.packages.pyth_rule.config),
|
|
16
17
|
// The deployed pyth_rule package is compiled against the Core pyth
|
|
17
18
|
// dependency, so its `&PythState` parameter is the Core-package-qualified
|
|
18
|
-
// type and
|
|
19
|
-
// right state to pass. The config's price_info_object
|
|
20
|
-
// objects to match.
|
|
21
|
-
pythState: tx.object(host.
|
|
19
|
+
// type and the Core source's own per-network state (`PYTH_CORE_INFRA`)
|
|
20
|
+
// is always the right state to pass. The config's price_info_object
|
|
21
|
+
// entries are Core objects to match.
|
|
22
|
+
pythState: tx.object(PYTH_CORE_INFRA[host.network].state_id),
|
|
22
23
|
pythPriceInfo: tx.object(priceInfoObjectId),
|
|
23
24
|
},
|
|
24
25
|
})(tx);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `aggregateTicker` appends per waterx-routed ticker. Pulls one enclave-signed
|
|
5
5
|
* batch envelope covering every requested ticker from the quote-center
|
|
6
6
|
* (`GET /v1/quotes/update?symbols=…`, endpoint from `host.waterx` — the
|
|
7
|
-
* `waterxEndpoint`/`waterxFetch` create options — else `
|
|
7
|
+
* `waterxEndpoint`/`waterxFetch` create options — else its own `WATERX_INFRA`), then —
|
|
8
8
|
* unlike Pyth Lazer, whose verify is a single shared PTB step — verifies AND
|
|
9
9
|
* feeds in ONE `waterx_rule::collect_batch_latest` call per collector (the Move
|
|
10
10
|
* API bundles the two). So `buildUpdateCalls` emits nothing and the signed
|
|
@@ -13,12 +13,47 @@
|
|
|
13
13
|
* `collect_batch_latest` is the dual-rule path: it feeds the item matching
|
|
14
14
|
* `collector.symbol()` WITHOUT aggregating, so a waterx-routed ticker composes
|
|
15
15
|
* onto the same collector as Pyth/Supra (compose-then-aggregate). On-chain a
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|
|
18
24
|
*/
|
|
19
25
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
26
|
+
import type { Network } from "../../constants.ts";
|
|
20
27
|
import type { OracleHost } from "../host.ts";
|
|
21
28
|
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). */
|
|
32
|
+
export declare const BATCH_PRICE_INTENT = 1;
|
|
33
|
+
/**
|
|
34
|
+
* WaterX quote-center external infra — owned by THIS source, by network.
|
|
35
|
+
* Mirrors `PYTH_CORE_INFRA` (oracle/pyth.ts) and `LAZER_INFRA`
|
|
36
|
+
* (rules/pyth-lazer-rule.ts): per-network constants for infrastructure the
|
|
37
|
+
* source's operator runs, co-located with the only rule that reads them — no
|
|
38
|
+
* other oracle source ever touches a quote-center endpoint. Public read (no
|
|
39
|
+
* auth), so there is no api_key. `endpoint` has no trailing slash — the rule
|
|
40
|
+
* appends the path.
|
|
41
|
+
*
|
|
42
|
+
* These are the DEFAULTS behind the caller's `client.waterx` access slice
|
|
43
|
+
* (`waterxEndpoint` / `waterxFetch` create options) — the browser-CORS proxy
|
|
44
|
+
* hook, since this is the one source fetched from the page.
|
|
45
|
+
*/
|
|
46
|
+
export declare const WATERX_INFRA: Record<Network, {
|
|
47
|
+
endpoint: string;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* The waterx source's quote-center base for `network` — the ONE accessor
|
|
51
|
+
* consumers (BE/FE read planes) use when, and only when, their own
|
|
52
|
+
* `ORACLE_SOURCE` resolves to `'waterx_rule'`. Mirrors
|
|
53
|
+
* `pythCoreHermesEndpoint`. Under any other source the read endpoint is that
|
|
54
|
+
* source's own configuration — never this one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function waterxQuoteCenterEndpoint(network: Network): string;
|
|
22
57
|
/**
|
|
23
58
|
* One item inside a signed batch payload, mirroring the quote-center
|
|
24
59
|
* `/v1/quotes/update` JSON 1:1 (snake_case). The u64 integer fields are the
|
|
@@ -82,8 +117,10 @@ export declare function waterxEnvelopeOf(data: RuleUpdateData): WaterxSignedEnve
|
|
|
82
117
|
* per item, the exact shape the enclave signed) and contribute the price for
|
|
83
118
|
* `collector.symbol()` to the collector. One collect call re-verifies the batch
|
|
84
119
|
* signature and picks this collector's symbol out of the batch; on-chain it
|
|
85
|
-
* abstains (records `none`)
|
|
86
|
-
*
|
|
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.
|
|
87
124
|
*/
|
|
88
125
|
export declare function feedWaterxRule(tx: Transaction, host: OracleHost, collector: TransactionArgument, envelope: WaterxSignedEnvelope): void;
|
|
89
126
|
export declare const WaterxRule: PriceUpdateRule;
|