@waterx/sdk 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -7
- package/dist/cjs/src/oracle/aggregate.d.ts +98 -17
- package/dist/cjs/src/oracle/aggregate.js +191 -21
- package/dist/cjs/src/oracle/config.d.ts +103 -0
- package/dist/cjs/src/oracle/config.js +64 -1
- package/dist/cjs/src/oracle/host.d.ts +13 -0
- package/dist/cjs/src/oracle/index.d.ts +19 -5
- package/dist/cjs/src/oracle/index.js +40 -6
- package/dist/cjs/src/oracle/price-update-rule.d.ts +180 -0
- package/dist/cjs/src/oracle/price-update-rule.js +56 -0
- package/dist/cjs/src/oracle/pyth.d.ts +80 -11
- package/dist/cjs/src/oracle/pyth.js +84 -17
- package/dist/cjs/src/oracle/rule-registry.d.ts +37 -0
- package/dist/cjs/src/oracle/rule-registry.js +61 -0
- package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +15 -0
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +84 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +194 -0
- package/dist/cjs/src/oracle/rules/sponsor.d.ts +11 -7
- package/dist/cjs/src/oracle/rules/sponsor.js +11 -7
- package/dist/cjs/src/oracle/update-fetch.d.ts +85 -0
- package/dist/cjs/src/oracle/update-fetch.js +228 -0
- package/dist/cjs/src/perp/client.d.ts +22 -1
- package/dist/cjs/src/perp/client.js +11 -2
- package/dist/cjs/src/perp/config.d.ts +15 -20
- package/dist/cjs/src/perp/config.js +86 -30
- package/dist/cjs/src/perp/index.d.ts +4 -3
- package/dist/cjs/src/perp/index.js +8 -4
- package/dist/cjs/src/perp/tx-builders/common.d.ts +52 -15
- package/dist/cjs/src/perp/tx-builders/common.js +39 -6
- package/dist/cjs/src/perp/tx-builders/wlp.d.ts +11 -3
- package/dist/cjs/src/perp/tx-builders/wlp.js +29 -3
- package/dist/cjs/src/perp/tx-builders.d.ts +3 -3
- package/dist/cjs/src/perp/tx-builders.js +3 -3
- package/dist/cjs/src/prediction/config.d.ts +5 -15
- package/dist/cjs/src/prediction/config.js +4 -12
- package/dist/cjs/src/prediction/fetch.d.ts +6 -1
- package/dist/cjs/src/prediction/fetch.js +64 -0
- package/dist/cjs/src/prediction/index.d.ts +2 -2
- package/dist/cjs/src/prediction/index.js +7 -4
- package/dist/cjs/src/prediction/types.d.ts +19 -0
- package/dist/cjs/src/unified-client.d.ts +26 -4
- package/dist/cjs/src/unified-client.js +4 -2
- package/dist/src/oracle/aggregate.d.ts +98 -17
- package/dist/src/oracle/aggregate.js +192 -22
- package/dist/src/oracle/config.d.ts +103 -0
- package/dist/src/oracle/config.js +63 -0
- package/dist/src/oracle/host.d.ts +13 -0
- package/dist/src/oracle/index.d.ts +19 -5
- package/dist/src/oracle/index.js +34 -6
- package/dist/src/oracle/price-update-rule.d.ts +180 -0
- package/dist/src/oracle/price-update-rule.js +53 -0
- package/dist/src/oracle/pyth.d.ts +80 -11
- package/dist/src/oracle/pyth.js +82 -16
- package/dist/src/oracle/rule-registry.d.ts +37 -0
- package/dist/src/oracle/rule-registry.js +56 -0
- package/dist/src/oracle/rules/pyth-core-rule.d.ts +15 -0
- package/dist/src/oracle/rules/pyth-core-rule.js +81 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +41 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +189 -0
- package/dist/src/oracle/rules/sponsor.d.ts +11 -7
- package/dist/src/oracle/rules/sponsor.js +11 -7
- package/dist/src/oracle/update-fetch.d.ts +85 -0
- package/dist/src/oracle/update-fetch.js +223 -0
- package/dist/src/perp/client.d.ts +22 -1
- package/dist/src/perp/client.js +12 -3
- package/dist/src/perp/config.d.ts +15 -20
- package/dist/src/perp/config.js +85 -29
- package/dist/src/perp/index.d.ts +4 -3
- package/dist/src/perp/index.js +2 -2
- package/dist/src/perp/tx-builders/common.d.ts +52 -15
- package/dist/src/perp/tx-builders/common.js +39 -6
- package/dist/src/perp/tx-builders/wlp.d.ts +11 -3
- package/dist/src/perp/tx-builders/wlp.js +29 -3
- package/dist/src/perp/tx-builders.d.ts +3 -3
- package/dist/src/perp/tx-builders.js +3 -3
- package/dist/src/prediction/config.d.ts +5 -15
- package/dist/src/prediction/config.js +4 -11
- package/dist/src/prediction/fetch.d.ts +6 -1
- package/dist/src/prediction/fetch.js +60 -0
- package/dist/src/prediction/index.d.ts +2 -2
- package/dist/src/prediction/index.js +2 -2
- package/dist/src/prediction/types.d.ts +19 -0
- package/dist/src/unified-client.d.ts +26 -4
- package/dist/src/unified-client.js +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,13 +11,18 @@ The perp and prediction lines expose builder functions with **colliding names**
|
|
|
11
11
|
```ts
|
|
12
12
|
import { WaterXClient } from "@waterx/sdk";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// waterxConfigUrl is REQUIRED — the SDK has no built-in default and never reads env.
|
|
15
|
+
const client = await WaterXClient.create({
|
|
16
|
+
network: "TESTNET",
|
|
17
|
+
waterxConfigUrl: "https://raw.githubusercontent.com/WaterXProtocol/waterx-config/main/testnet.json",
|
|
18
|
+
});
|
|
15
19
|
client.account.createAccount(tx, { alias }); // shared waterx_account + funding (credit/custody)
|
|
16
20
|
client.perp.buildPlaceOrderTx(params); // perpetuals
|
|
17
21
|
client.predict.placeOrder(tx, params); // prediction markets
|
|
18
22
|
// client.perp / client.predict ARE the line clients — sign/execute on them directly:
|
|
19
23
|
// await client.perp.signAndExecuteTransaction({ transaction: tx, signer })
|
|
20
|
-
// each line can target a different network
|
|
24
|
+
// each line can target a different network + URL:
|
|
25
|
+
// WaterXClient.create({ perp: { network: "MAINNET", waterxConfigUrl: mainnetUrl }, predict: { network: "TESTNET", waterxConfigUrl: testnetUrl } })
|
|
21
26
|
```
|
|
22
27
|
|
|
23
28
|
> `WaterXClient` is the umbrella entry point. `Client` is kept as a **deprecated alias** for one major cycle.
|
|
@@ -41,13 +46,16 @@ Consumers: `pnpm add @waterx/sdk @mysten/sui`
|
|
|
41
46
|
|
|
42
47
|
## Quickstart (unified client)
|
|
43
48
|
|
|
44
|
-
`WaterXClient.create()` loads each line's deployment config from the canonical `waterx-config` JSON and returns a ready client. Builders are **build-only** — they return / mutate a `Transaction`; signing & execution stay with the caller (`client.perp` / `client.predict` are the line clients, or a frontend wallet), so multi-step Pyth injection and wallet flows keep working.
|
|
49
|
+
`WaterXClient.create()` loads each line's deployment config from the canonical `waterx-config` JSON (its URL passed via the **required** `waterxConfigUrl` option — the SDK has no default and never reads env) and returns a ready client. Builders are **build-only** — they return / mutate a `Transaction`; signing & execution stay with the caller (`client.perp` / `client.predict` are the line clients, or a frontend wallet), so multi-step Pyth injection and wallet flows keep working.
|
|
45
50
|
|
|
46
51
|
```ts
|
|
47
52
|
import { WaterXClient, rawPrice } from "@waterx/sdk";
|
|
48
53
|
import { Transaction } from "@mysten/sui/transactions";
|
|
49
54
|
|
|
50
|
-
const client = await WaterXClient.create({
|
|
55
|
+
const client = await WaterXClient.create({
|
|
56
|
+
network: "TESTNET",
|
|
57
|
+
waterxConfigUrl: "https://raw.githubusercontent.com/WaterXProtocol/waterx-config/main/testnet.json",
|
|
58
|
+
});
|
|
51
59
|
const signer = /* your Ed25519Keypair or wallet Signer */;
|
|
52
60
|
|
|
53
61
|
// --- Perp: place a market order ---
|
|
@@ -77,18 +85,56 @@ await client.predict.signAndExecuteTransaction({ transaction: ptx, signer });
|
|
|
77
85
|
|
|
78
86
|
## Per-line clients
|
|
79
87
|
|
|
80
|
-
If you only need one line, construct it directly (both factories are **async** — they fetch deployment config):
|
|
88
|
+
If you only need one line, construct it directly (both factories are **async** — they fetch deployment config; `waterxConfigUrl` is **required**):
|
|
81
89
|
|
|
82
90
|
```ts
|
|
83
91
|
import { PerpClient } from "@waterx/sdk/perp";
|
|
84
92
|
import { PredictClient } from "@waterx/sdk/prediction";
|
|
85
93
|
|
|
86
|
-
const
|
|
87
|
-
|
|
94
|
+
const waterxConfigUrl =
|
|
95
|
+
"https://raw.githubusercontent.com/WaterXProtocol/waterx-config/main/testnet.json";
|
|
96
|
+
const perp = await PerpClient.create("TESTNET", { waterxConfigUrl }); // or PerpClient.testnet({ waterxConfigUrl })
|
|
97
|
+
const predict = await PredictClient.create("TESTNET", { waterxConfigUrl }); // or PredictClient.testnet({ waterxConfigUrl })
|
|
88
98
|
```
|
|
89
99
|
|
|
90
100
|
Read-only queries use gRPC `simulateTransaction` (no signer) — the `getX` view helpers, e.g. `await perp.simulate(tx)` or `getMarketData(perp, …)`.
|
|
91
101
|
|
|
102
|
+
## Oracle sources & the Pyth Pro migration
|
|
103
|
+
|
|
104
|
+
Two independent client create options control oracle behavior. The SDK **never reads `process.env`** — each consumer wires them from its own env vars, so every environment runs the **same SDK version** and differs only by env:
|
|
105
|
+
|
|
106
|
+
| Option | Values | What it flips |
|
|
107
|
+
|--------|--------|---------------|
|
|
108
|
+
| `oracleSource` | `'pyth_rule'` (default) \| `'pyth_lazer_rule'` | Which `PriceUpdateRule` `refreshOraclePrices` uses for the on-chain price-update leg. |
|
|
109
|
+
| `pythGeneration` | `'core'` (default) \| `'pro'` | Which Pyth infra constants feed `client.pyth` when the config JSON has no explicit `pyth` block: `PYTH_DEFAULTS` (original contracts, keyless `hermes.pyth.network`) or `PYTH_PRO_DEFAULTS` (post-2026-08-18 Pro-compatible contracts + the Hermes-compatible `https://pyth.dourolabs.app/hermes`, auth-first). |
|
|
110
|
+
|
|
111
|
+
They are orthogonal: `pythGeneration` moves the Pyth **Core** state ids + endpoint; `oracleSource` picks the **rule** (Core VAA vs Lazer signed updates). An explicit `pyth` block in the config JSON always overrides the generation constants wholesale.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// Per-environment wiring — the consumer owns the env vars, not the SDK:
|
|
115
|
+
const perp = await PerpClient.create(network, {
|
|
116
|
+
waterxConfigUrl,
|
|
117
|
+
oracleSource: process.env.ORACLE_SOURCE as OracleSource | undefined, // e.g. staging: pyth_lazer_rule
|
|
118
|
+
pythGeneration: process.env.PYTH_GENERATION as PythGeneration | undefined, // e.g. staging: pro
|
|
119
|
+
});
|
|
120
|
+
// After the 2026-08-18 cutover, Pro-generation Hermes requires a key:
|
|
121
|
+
perp.pyth = { ...perp.pyth, api_key: process.env.PYTH_API_KEY };
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This is the staging-Pro / prod-Core rollout pattern: staging sets `ORACLE_SOURCE=pyth_lazer_rule` and/or `PYTH_GENERATION=pro` while production leaves both unset (Core defaults) — flipping an environment is an env-var change, never an SDK release. After August 18, 2026 (the Core-upgrade cutover — see https://docs.pyth.network/price-feeds/core/upgrade), consumers set `pythGeneration: 'pro'` + `pyth.api_key`.
|
|
125
|
+
|
|
126
|
+
### Adding an oracle source (runbook)
|
|
127
|
+
|
|
128
|
+
Every rule generation plugs in the same way — routing is driven **only** by the client's `oracleSource` option (never a config `enabled` flag, never `process.env`):
|
|
129
|
+
|
|
130
|
+
1. **Implement `PriceUpdateRule`** in `src/oracle/rules/<name>-rule.ts` — all port fields (`src/oracle/price-update-rule.ts`): `kind`, `requiresFeeSource` (`true` iff the on-chain verify draws a per-update fee — gates the fail-fast fee-source check), `supportedTickers`, `fetchUpdateData`, `narrowUpdateData` (subset a cached whole-universe payload to one build's tickers — a divisible payload returns a per-feed subset, an indivisible one returns itself whole iff fully covered; uncovered ticker → `null` miss), `buildUpdateCalls`.
|
|
131
|
+
2. **Register it** in `src/oracle/rule-registry.ts` (`DEFAULT_RULES`) under a new `OracleSource` value (added to the union in `price-update-rule.ts`).
|
|
132
|
+
3. **Publish the on-chain rule package** — its config entry (package ids, per-ticker `feeds`) arrives via the normal `waterx-config` deploy pipeline; type it in `OraclePackages` (`src/oracle/config.ts`).
|
|
133
|
+
4. **Add SDK infra constants** if the source needs external infra that is not part of the config JSON (API endpoints, verifier packages, state objects) — a per-network map in `src/oracle/config.ts`, mirroring `LAZER_DEFAULTS` / `PYTH_PRO_DEFAULTS`.
|
|
134
|
+
5. **Consumers flip `ORACLE_SOURCE`** per environment — no consumer code change, no SDK re-release.
|
|
135
|
+
|
|
136
|
+
The in-house `waterx_rule` (ed25519 enclave-signed CEX prices) follows exactly this path when it lands.
|
|
137
|
+
|
|
92
138
|
## Recipes & full surface
|
|
93
139
|
|
|
94
140
|
To avoid doc drift, per-action usage lives in maintained, lint-checked code rather than this README:
|
|
@@ -2,40 +2,58 @@
|
|
|
2
2
|
* Oracle aggregation — the orchestrator that composes rules into the shared
|
|
3
3
|
* `Oracle`. This is the ONE file that knows about every rule: it builds a
|
|
4
4
|
* `PriceCollector`, feeds whichever rules a ticker is configured for
|
|
5
|
-
* (Pyth / Supra / Constant), then `aggregate`s.
|
|
5
|
+
* (Pyth / Lazer / Supra / Constant), then `aggregate`s.
|
|
6
6
|
*
|
|
7
7
|
* Per ticker:
|
|
8
8
|
* collector = oracle::new_collector(ticker)
|
|
9
9
|
* [pyth_rule::feed] when the ticker has a pyth_rule.feeds entry
|
|
10
|
+
* [pyth_lazer_rule::feed] when the update leg produced a verified lazer Update
|
|
10
11
|
* [supra_rule::feed] when supra is enabled + wired
|
|
11
12
|
* [constant_rule::feed] when the ticker is a constant ticker
|
|
12
13
|
* oracle::aggregate(oracle, collector)
|
|
13
14
|
*
|
|
14
|
-
* The fed rule set must
|
|
15
|
+
* The fed rule set must cover the on-chain weighted set for the ticker —
|
|
15
16
|
* `aggregator::remove_outliers` aborts `EMissingPriceSource` if a weighted rule
|
|
16
|
-
* is missing from the collector
|
|
17
|
+
* is missing from the collector (an abstaining feed call counts as present;
|
|
18
|
+
* a fed-but-unweighted rule is silently dropped).
|
|
19
|
+
*
|
|
20
|
+
* `refreshOraclePrices` additionally routes the on-chain price *update* leg
|
|
21
|
+
* (the fetch + verify/push step, before any of the above feeding) through the
|
|
22
|
+
* `PriceUpdateRule` selected by `host.oracleSource` — see `rule-registry.ts`.
|
|
17
23
|
*/
|
|
18
24
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
19
25
|
import type { OracleHost } from "./host.ts";
|
|
20
|
-
import {
|
|
26
|
+
import type { OracleSource, PriceUpdateRule, UpdateDataProvider } from "./price-update-rule.ts";
|
|
27
|
+
import { type OracleFeeSource, type PythCache } from "./pyth.ts";
|
|
21
28
|
/**
|
|
22
29
|
* Aggregate one ticker's price into the shared `Oracle`: build a collector, feed
|
|
23
30
|
* every rule the ticker is configured for, then `aggregate`.
|
|
24
31
|
*
|
|
25
32
|
* - **Pyth** — fed when `priceInfoObjectId` is supplied (i.e. the ticker has a
|
|
26
|
-
* `pyth_rule.feeds` entry).
|
|
27
|
-
* `PriceInfoObject`
|
|
28
|
-
*
|
|
29
|
-
*
|
|
33
|
+
* `pyth_rule.feeds` entry). When this PTB's update leg refreshed the
|
|
34
|
+
* `PriceInfoObject` it contributes a fresh price; when it did not (a
|
|
35
|
+
* lazer-routed ticker), the on-chain rule only READS the object and abstains
|
|
36
|
+
* if it is stale — it never aborts — so the call stays mandatory while
|
|
37
|
+
* `pyth_rule` remains in the ticker's on-chain weighted set
|
|
38
|
+
* (`EMissingPriceSource` requires every weighted rule to appear).
|
|
39
|
+
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified
|
|
40
|
+
* `pyth_lazer::update::Update` produced by this PTB's lazer update leg
|
|
41
|
+
* (see `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
|
|
42
|
+
* not (yet) weight `PythLazerRule`, the contribution is silently dropped
|
|
43
|
+
* on-chain — feeding ahead of the weight migration is harmless.
|
|
44
|
+
* - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
|
|
45
|
+
* (abstains on-chain for symbols it has no pair for).
|
|
30
46
|
* - **Constant** — fed when the ticker is a constant ticker
|
|
31
47
|
* ({@link OracleHost.isConstantTicker}).
|
|
32
48
|
*
|
|
33
|
-
* "Dual-feed" (Pyth + Constant) and "constant-only" are not
|
|
34
|
-
* fall out of which rules the ticker is in. Throws if no
|
|
49
|
+
* "Dual-feed" (Pyth + Constant, or Pyth + Lazer) and "constant-only" are not
|
|
50
|
+
* special cases — they fall out of which rules the ticker is in. Throws if no
|
|
51
|
+
* rule applies.
|
|
35
52
|
*/
|
|
36
53
|
export declare function aggregateTicker(tx: Transaction, host: OracleHost, args: {
|
|
37
54
|
ticker: string;
|
|
38
55
|
priceInfoObjectId?: string;
|
|
56
|
+
lazerUpdate?: TransactionArgument;
|
|
39
57
|
}): void;
|
|
40
58
|
/**
|
|
41
59
|
* Thin wrapper over {@link aggregateTicker} for a Pyth-fed ticker. Kept for
|
|
@@ -61,14 +79,77 @@ export declare function aggregateTickerWithConstant(tx: Transaction, host: Oracl
|
|
|
61
79
|
/**
|
|
62
80
|
* Refresh multiple tickers in one PTB. For each ticker {@link aggregateTicker}
|
|
63
81
|
* feeds whichever rules it is configured for (Pyth if it has a `pyth_rule.feeds`
|
|
64
|
-
* entry,
|
|
65
|
-
*
|
|
66
|
-
*
|
|
82
|
+
* entry, Lazer if the lazer update leg served it — see below — Supra when
|
|
83
|
+
* enabled, Constant when it's a constant ticker).
|
|
84
|
+
*
|
|
85
|
+
* Before that, the on-chain price *update* leg is routed by `host.oracleSource`
|
|
86
|
+
* (see `rule-registry.ts`): the selected rule serves every ticker in its
|
|
87
|
+
* `supportedTickers(host)`; tickers it doesn't cover fall back to `pyth_rule`
|
|
88
|
+
* (`PythCoreRule`) when THEY support it — so when `oracleSource` IS `'pyth_rule'`
|
|
89
|
+
* there is exactly one group, identical to the pre-routing behavior. A ticker
|
|
90
|
+
* supported by neither is simply skipped from this leg (no fetch/build call for
|
|
91
|
+
* it) — the same way today's non-pyth tickers (e.g. constant-only) always were;
|
|
92
|
+
* it still gets aggregated below via whichever rule {@link aggregateTicker} finds.
|
|
93
|
+
*
|
|
94
|
+
* Each group's fetch + build runs against its own rule, which guarantees
|
|
95
|
+
* per-rule PTB atomicity (no mixed-generation payload within one rule's calls).
|
|
96
|
+
* When `oracleSource` isn't `'pyth_rule'`, one PTB may legitimately carry BOTH a
|
|
97
|
+
* non-Pyth-Core block (selected group) and a Pyth Core block (fallback group) —
|
|
98
|
+
* each verifies against its own contract objects, so that's fine. A fee-source
|
|
99
|
+
* pre-check runs first, across every group's `requiresFeeSource` — BEFORE any
|
|
100
|
+
* off-chain fetch or PTB mutation — so a fee-charging group with no
|
|
101
|
+
* `opts.feeSource` throws `OracleFeeSourceUnavailable` with zero wasted
|
|
102
|
+
* network calls and zero stray moveCalls, even in a mixed shape (e.g. a
|
|
103
|
+
* fee-free Lazer group ordered ahead of a Pyth Core fallback group). Only once
|
|
104
|
+
* that check passes do all groups' off-chain fetches run concurrently
|
|
105
|
+
* (`Promise.all`) and complete before any PTB mutation; on-chain reads inside
|
|
106
|
+
* `buildUpdateCalls` can still fail mid-append for other reasons — callers
|
|
107
|
+
* discard the tx on any throw.
|
|
108
|
+
*
|
|
109
|
+
* **Collector-feed leg is rule-aware:** a lazer-served group's
|
|
110
|
+
* `buildUpdateCalls` returns the verified `Update` PTB value
|
|
111
|
+
* ({@link RuleUpdateHandle}), and every ticker in that group is aggregated
|
|
112
|
+
* with `lazerUpdate` set so {@link aggregateTicker} appends
|
|
113
|
+
* `pyth_lazer_rule::feed` against it. A lazer-routed ticker that still has a
|
|
114
|
+
* `pyth_rule.feeds` entry ALSO keeps its `pyth_rule::feed` leg — required
|
|
115
|
+
* on-chain while `pyth_rule` stays in the ticker's weighted set
|
|
116
|
+
* (`aggregator::remove_outliers` aborts `EMissingPriceSource` unless every
|
|
117
|
+
* weighted rule appears in the collector; an abstention counts as
|
|
118
|
+
* appearing), and safe: `pyth_rule::feed` only READS the `PriceInfoObject`
|
|
119
|
+
* this PTB never refreshed and abstains when it is stale rather than
|
|
120
|
+
* aborting. Conversely, a lazer feed call on an aggregator that does not
|
|
121
|
+
* (yet) weight `PythLazerRule` is silently dropped on-chain — so
|
|
122
|
+
* lazer-routing a ticker ahead of its on-chain weight migration prices it
|
|
123
|
+
* from the remaining weighted rules instead of failing.
|
|
67
124
|
*/
|
|
68
125
|
export declare function refreshOraclePrices(tx: Transaction, host: OracleHost, tickers: string[], opts?: {
|
|
69
126
|
cache?: PythCache;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The single resolved fee source for the Pyth update fee, forwarded
|
|
129
|
+
* verbatim to each group's `PriceUpdateRule.buildUpdateCalls` as
|
|
130
|
+
* `BuildUpdateOpts.feeSource`. Already-resolved by the caller (see
|
|
131
|
+
* {@link OracleFeeSource}'s own doc for where/how) — this function makes
|
|
132
|
+
* no sponsor-vs-gas decision itself, it only checks whether a source was
|
|
133
|
+
* resolved at all. Ignored by rules with no update fee (e.g.
|
|
134
|
+
* `pyth_lazer_rule`). Building with `feeSource` unset throws
|
|
135
|
+
* `OracleFeeSourceUnavailable` (see `oracle/pyth.ts`) instead of
|
|
136
|
+
* silently drawing from `tx.gas`.
|
|
137
|
+
*/
|
|
138
|
+
feeSource?: OracleFeeSource;
|
|
139
|
+
/**
|
|
140
|
+
* @internal Test-only: layer fake `PriceUpdateRule`s on top of the
|
|
141
|
+
* production registry (see `rule-registry.ts`'s `resolveOracleRule`).
|
|
142
|
+
* Production callers never set this — routing is by `host.oracleSource`
|
|
143
|
+
* alone.
|
|
144
|
+
*/
|
|
145
|
+
ruleOverrides?: Partial<Record<OracleSource, PriceUpdateRule>>;
|
|
146
|
+
/**
|
|
147
|
+
* BE prefetch-cache seam: checked per group BEFORE that group's live
|
|
148
|
+
* `rule.fetchUpdateData`. See {@link UpdateDataProvider}. A cache miss
|
|
149
|
+
* (`null`) or a throw from the provider falls back to the live fetch —
|
|
150
|
+
* a degraded/broken cache must never break the money path; a
|
|
151
|
+
* kind-mismatched hit (the provider handed back the wrong rule's
|
|
152
|
+
* payload) throws instead, since that is a caller bug, not a cache miss.
|
|
153
|
+
*/
|
|
154
|
+
updateDataProvider?: UpdateDataProvider;
|
|
74
155
|
}): Promise<void>;
|
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
* Oracle aggregation — the orchestrator that composes rules into the shared
|
|
4
4
|
* `Oracle`. This is the ONE file that knows about every rule: it builds a
|
|
5
5
|
* `PriceCollector`, feeds whichever rules a ticker is configured for
|
|
6
|
-
* (Pyth / Supra / Constant), then `aggregate`s.
|
|
6
|
+
* (Pyth / Lazer / Supra / Constant), then `aggregate`s.
|
|
7
7
|
*
|
|
8
8
|
* Per ticker:
|
|
9
9
|
* collector = oracle::new_collector(ticker)
|
|
10
10
|
* [pyth_rule::feed] when the ticker has a pyth_rule.feeds entry
|
|
11
|
+
* [pyth_lazer_rule::feed] when the update leg produced a verified lazer Update
|
|
11
12
|
* [supra_rule::feed] when supra is enabled + wired
|
|
12
13
|
* [constant_rule::feed] when the ticker is a constant ticker
|
|
13
14
|
* oracle::aggregate(oracle, collector)
|
|
14
15
|
*
|
|
15
|
-
* The fed rule set must
|
|
16
|
+
* The fed rule set must cover the on-chain weighted set for the ticker —
|
|
16
17
|
* `aggregator::remove_outliers` aborts `EMissingPriceSource` if a weighted rule
|
|
17
|
-
* is missing from the collector
|
|
18
|
+
* is missing from the collector (an abstaining feed call counts as present;
|
|
19
|
+
* a fed-but-unweighted rule is silently dropped).
|
|
20
|
+
*
|
|
21
|
+
* `refreshOraclePrices` additionally routes the on-chain price *update* leg
|
|
22
|
+
* (the fetch + verify/push step, before any of the above feeding) through the
|
|
23
|
+
* `PriceUpdateRule` selected by `host.oracleSource` — see `rule-registry.ts`.
|
|
18
24
|
*/
|
|
19
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
26
|
exports.aggregateTicker = aggregateTicker;
|
|
@@ -23,23 +29,77 @@ exports.aggregateTickerWithConstant = aggregateTickerWithConstant;
|
|
|
23
29
|
exports.refreshOraclePrices = refreshOraclePrices;
|
|
24
30
|
const oracle_ts_1 = require("../generated/waterx_oracle/oracle.js");
|
|
25
31
|
const pyth_ts_1 = require("./pyth.js");
|
|
32
|
+
const rule_registry_ts_1 = require("./rule-registry.js");
|
|
26
33
|
const constant_rule_ts_1 = require("./rules/constant-rule.js");
|
|
34
|
+
const pyth_lazer_rule_ts_1 = require("./rules/pyth-lazer-rule.js");
|
|
27
35
|
const pyth_rule_ts_1 = require("./rules/pyth-rule.js");
|
|
28
36
|
const supra_rule_ts_1 = require("./rules/supra-rule.js");
|
|
37
|
+
/**
|
|
38
|
+
* Resolve one group's off-chain update payload for {@link refreshOraclePrices}:
|
|
39
|
+
* try `provider.get(source, tickers)` first (when a provider is configured),
|
|
40
|
+
* falling back to the group's own live `rule.fetchUpdateData` on a cache miss
|
|
41
|
+
* (`null`) or a throw from the provider — a broken/degraded cache must never
|
|
42
|
+
* break the money path.
|
|
43
|
+
*
|
|
44
|
+
* A cache HIT is treated as a payload for a POSSIBLY-WIDER ticker set (a
|
|
45
|
+
* provider commonly caches one whole-universe payload per source — see
|
|
46
|
+
* {@link UpdateDataProvider}), so it is narrowed to exactly `group.tickers`
|
|
47
|
+
* via `rule.narrowUpdateData` before use. This is load-bearing, not
|
|
48
|
+
* defensive: without it a Pyth Core hit would emit an
|
|
49
|
+
* `update_single_price_feed` — and charge its fee — for every cached feed
|
|
50
|
+
* instead of just this group's, and a payload that cannot cover the group
|
|
51
|
+
* (`narrowUpdateData` → `null`) would never reach the live-fetch fallback.
|
|
52
|
+
* Each rule owns its own subsetting (Core subsets per-feed entries; Lazer's
|
|
53
|
+
* indivisible payload passes whole iff fully covered), so the orchestrator
|
|
54
|
+
* never branches on `kind` here. A hit whose `kind` doesn't match the
|
|
55
|
+
* group's rule is a caller bug (the provider handed back a different rule's
|
|
56
|
+
* payload), so that throws — via `narrowUpdateData`'s own
|
|
57
|
+
* `assertRuleUpdateData` guard — instead of silently falling back.
|
|
58
|
+
*/
|
|
59
|
+
async function resolveGroupUpdateData(host, group, provider) {
|
|
60
|
+
if (provider) {
|
|
61
|
+
let cached = null;
|
|
62
|
+
try {
|
|
63
|
+
cached = await provider.get(group.source, group.tickers);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Provider errors must never break the money path — fall through to
|
|
67
|
+
// the live fetch below exactly as a cache miss (`null`) would.
|
|
68
|
+
}
|
|
69
|
+
if (cached !== null) {
|
|
70
|
+
// Wrong-kind hit throws inside narrowUpdateData (assertRuleUpdateData);
|
|
71
|
+
// a hit that can't cover the group narrows to null → live-fetch below.
|
|
72
|
+
const narrowed = group.rule.narrowUpdateData(host, cached, group.tickers);
|
|
73
|
+
if (narrowed !== null)
|
|
74
|
+
return narrowed;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return group.rule.fetchUpdateData(host, group.tickers);
|
|
78
|
+
}
|
|
29
79
|
/**
|
|
30
80
|
* Aggregate one ticker's price into the shared `Oracle`: build a collector, feed
|
|
31
81
|
* every rule the ticker is configured for, then `aggregate`.
|
|
32
82
|
*
|
|
33
83
|
* - **Pyth** — fed when `priceInfoObjectId` is supplied (i.e. the ticker has a
|
|
34
|
-
* `pyth_rule.feeds` entry).
|
|
35
|
-
* `PriceInfoObject`
|
|
36
|
-
*
|
|
37
|
-
*
|
|
84
|
+
* `pyth_rule.feeds` entry). When this PTB's update leg refreshed the
|
|
85
|
+
* `PriceInfoObject` it contributes a fresh price; when it did not (a
|
|
86
|
+
* lazer-routed ticker), the on-chain rule only READS the object and abstains
|
|
87
|
+
* if it is stale — it never aborts — so the call stays mandatory while
|
|
88
|
+
* `pyth_rule` remains in the ticker's on-chain weighted set
|
|
89
|
+
* (`EMissingPriceSource` requires every weighted rule to appear).
|
|
90
|
+
* - **Lazer** — fed when `lazerUpdate` is supplied: the verified
|
|
91
|
+
* `pyth_lazer::update::Update` produced by this PTB's lazer update leg
|
|
92
|
+
* (see `PythLazerRule.buildUpdateCalls`). If the ticker's aggregator does
|
|
93
|
+
* not (yet) weight `PythLazerRule`, the contribution is silently dropped
|
|
94
|
+
* on-chain — feeding ahead of the weight migration is harmless.
|
|
95
|
+
* - **Supra** — fed alongside Pyth/Lazer when supra is enabled + wired
|
|
96
|
+
* (abstains on-chain for symbols it has no pair for).
|
|
38
97
|
* - **Constant** — fed when the ticker is a constant ticker
|
|
39
98
|
* ({@link OracleHost.isConstantTicker}).
|
|
40
99
|
*
|
|
41
|
-
* "Dual-feed" (Pyth + Constant) and "constant-only" are not
|
|
42
|
-
* fall out of which rules the ticker is in. Throws if no
|
|
100
|
+
* "Dual-feed" (Pyth + Constant, or Pyth + Lazer) and "constant-only" are not
|
|
101
|
+
* special cases — they fall out of which rules the ticker is in. Throws if no
|
|
102
|
+
* rule applies.
|
|
43
103
|
*/
|
|
44
104
|
function aggregateTicker(tx, host, args) {
|
|
45
105
|
const oraclePkg = host.config.packages.waterx_oracle.published_at;
|
|
@@ -50,16 +110,22 @@ function aggregateTicker(tx, host, args) {
|
|
|
50
110
|
let fed = false;
|
|
51
111
|
if (args.priceInfoObjectId) {
|
|
52
112
|
(0, pyth_rule_ts_1.feedPythRule)(tx, host, collector, args.priceInfoObjectId);
|
|
113
|
+
fed = true;
|
|
114
|
+
}
|
|
115
|
+
if (args.lazerUpdate !== undefined) {
|
|
116
|
+
(0, pyth_lazer_rule_ts_1.feedLazerRule)(tx, host, collector, args.lazerUpdate);
|
|
117
|
+
fed = true;
|
|
118
|
+
}
|
|
119
|
+
if (fed) {
|
|
53
120
|
// Supra rides on the same collector when enabled (abstains on-chain otherwise).
|
|
54
121
|
(0, supra_rule_ts_1.maybeFeedSupra)(tx, host, collector);
|
|
55
|
-
fed = true;
|
|
56
122
|
}
|
|
57
123
|
if (host.isConstantTicker(args.ticker)) {
|
|
58
124
|
(0, constant_rule_ts_1.feedConstantRule)(tx, host, collector);
|
|
59
125
|
fed = true;
|
|
60
126
|
}
|
|
61
127
|
if (!fed) {
|
|
62
|
-
throw new Error(`no oracle rule configured for ticker '${args.ticker}' (no pyth feed, not a constant ticker)`);
|
|
128
|
+
throw new Error(`no oracle rule configured for ticker '${args.ticker}' (no pyth feed, no lazer update, not a constant ticker)`);
|
|
63
129
|
}
|
|
64
130
|
(0, oracle_ts_1.aggregate)({
|
|
65
131
|
package: oraclePkg,
|
|
@@ -95,24 +161,128 @@ function aggregateTickerWithConstant(tx, host, args) {
|
|
|
95
161
|
/**
|
|
96
162
|
* Refresh multiple tickers in one PTB. For each ticker {@link aggregateTicker}
|
|
97
163
|
* feeds whichever rules it is configured for (Pyth if it has a `pyth_rule.feeds`
|
|
98
|
-
* entry,
|
|
99
|
-
*
|
|
100
|
-
*
|
|
164
|
+
* entry, Lazer if the lazer update leg served it — see below — Supra when
|
|
165
|
+
* enabled, Constant when it's a constant ticker).
|
|
166
|
+
*
|
|
167
|
+
* Before that, the on-chain price *update* leg is routed by `host.oracleSource`
|
|
168
|
+
* (see `rule-registry.ts`): the selected rule serves every ticker in its
|
|
169
|
+
* `supportedTickers(host)`; tickers it doesn't cover fall back to `pyth_rule`
|
|
170
|
+
* (`PythCoreRule`) when THEY support it — so when `oracleSource` IS `'pyth_rule'`
|
|
171
|
+
* there is exactly one group, identical to the pre-routing behavior. A ticker
|
|
172
|
+
* supported by neither is simply skipped from this leg (no fetch/build call for
|
|
173
|
+
* it) — the same way today's non-pyth tickers (e.g. constant-only) always were;
|
|
174
|
+
* it still gets aggregated below via whichever rule {@link aggregateTicker} finds.
|
|
175
|
+
*
|
|
176
|
+
* Each group's fetch + build runs against its own rule, which guarantees
|
|
177
|
+
* per-rule PTB atomicity (no mixed-generation payload within one rule's calls).
|
|
178
|
+
* When `oracleSource` isn't `'pyth_rule'`, one PTB may legitimately carry BOTH a
|
|
179
|
+
* non-Pyth-Core block (selected group) and a Pyth Core block (fallback group) —
|
|
180
|
+
* each verifies against its own contract objects, so that's fine. A fee-source
|
|
181
|
+
* pre-check runs first, across every group's `requiresFeeSource` — BEFORE any
|
|
182
|
+
* off-chain fetch or PTB mutation — so a fee-charging group with no
|
|
183
|
+
* `opts.feeSource` throws `OracleFeeSourceUnavailable` with zero wasted
|
|
184
|
+
* network calls and zero stray moveCalls, even in a mixed shape (e.g. a
|
|
185
|
+
* fee-free Lazer group ordered ahead of a Pyth Core fallback group). Only once
|
|
186
|
+
* that check passes do all groups' off-chain fetches run concurrently
|
|
187
|
+
* (`Promise.all`) and complete before any PTB mutation; on-chain reads inside
|
|
188
|
+
* `buildUpdateCalls` can still fail mid-append for other reasons — callers
|
|
189
|
+
* discard the tx on any throw.
|
|
190
|
+
*
|
|
191
|
+
* **Collector-feed leg is rule-aware:** a lazer-served group's
|
|
192
|
+
* `buildUpdateCalls` returns the verified `Update` PTB value
|
|
193
|
+
* ({@link RuleUpdateHandle}), and every ticker in that group is aggregated
|
|
194
|
+
* with `lazerUpdate` set so {@link aggregateTicker} appends
|
|
195
|
+
* `pyth_lazer_rule::feed` against it. A lazer-routed ticker that still has a
|
|
196
|
+
* `pyth_rule.feeds` entry ALSO keeps its `pyth_rule::feed` leg — required
|
|
197
|
+
* on-chain while `pyth_rule` stays in the ticker's weighted set
|
|
198
|
+
* (`aggregator::remove_outliers` aborts `EMissingPriceSource` unless every
|
|
199
|
+
* weighted rule appears in the collector; an abstention counts as
|
|
200
|
+
* appearing), and safe: `pyth_rule::feed` only READS the `PriceInfoObject`
|
|
201
|
+
* this PTB never refreshed and abstains when it is stale rather than
|
|
202
|
+
* aborting. Conversely, a lazer feed call on an aggregator that does not
|
|
203
|
+
* (yet) weight `PythLazerRule` is silently dropped on-chain — so
|
|
204
|
+
* lazer-routing a ticker ahead of its on-chain weight migration prices it
|
|
205
|
+
* from the remaining weighted rules instead of failing.
|
|
101
206
|
*/
|
|
102
207
|
async function refreshOraclePrices(tx, host, tickers, opts = {}) {
|
|
103
208
|
if (tickers.length === 0)
|
|
104
209
|
return;
|
|
105
|
-
//
|
|
106
|
-
//
|
|
210
|
+
// price_info_object lookup for every ticker with a pyth_rule.feeds entry —
|
|
211
|
+
// needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
|
|
212
|
+
// which rule performed the on-chain update for that ticker.
|
|
107
213
|
const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds?.[t] !== undefined);
|
|
108
214
|
const priceInfoByTicker = new Map();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
215
|
+
pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
|
|
216
|
+
// Group tickers for the on-chain update leg: selected rule first, then the
|
|
217
|
+
// pyth_rule fallback for whatever the selected rule doesn't cover. `source`
|
|
218
|
+
// is tracked alongside each group (rather than read back off `rule.kind`,
|
|
219
|
+
// which is typed as the broader PriceUpdateRuleKind) so the provider lookup
|
|
220
|
+
// below has an OracleSource to key on without a cast.
|
|
221
|
+
const selectedRule = (0, rule_registry_ts_1.resolveOracleRule)(host.oracleSource, opts.ruleOverrides);
|
|
222
|
+
const selectedSupported = new Set(selectedRule.supportedTickers(host));
|
|
223
|
+
const selectedGroup = tickers.filter((t) => selectedSupported.has(t));
|
|
224
|
+
const groups = [];
|
|
225
|
+
if (selectedGroup.length > 0) {
|
|
226
|
+
groups.push({ source: host.oracleSource, rule: selectedRule, tickers: selectedGroup });
|
|
227
|
+
}
|
|
228
|
+
if (host.oracleSource !== "pyth_rule") {
|
|
229
|
+
const fallbackRule = (0, rule_registry_ts_1.resolveOracleRule)("pyth_rule", opts.ruleOverrides);
|
|
230
|
+
const fallbackSupported = new Set(fallbackRule.supportedTickers(host));
|
|
231
|
+
const fallbackGroup = tickers.filter((t) => !selectedSupported.has(t) && fallbackSupported.has(t));
|
|
232
|
+
if (fallbackGroup.length > 0) {
|
|
233
|
+
groups.push({ source: "pyth_rule", rule: fallbackRule, tickers: fallbackGroup });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Fee-source pre-check, hoisted ABOVE both the off-chain fetch below AND
|
|
237
|
+
// the per-group build loop further down. The condition only consults
|
|
238
|
+
// `group.rule.requiresFeeSource` — known the moment `groups` is built,
|
|
239
|
+
// before any fetch or PTB mutation — so this throws with ZERO wasted
|
|
240
|
+
// network calls and zero PTB commands. A per-call guard inside
|
|
241
|
+
// `buildPythPriceUpdateCalls` alone would not be early enough: in a mixed
|
|
242
|
+
// shape (e.g. a lazer-selected `oracleSource` with a `pyth_rule` fallback
|
|
243
|
+
// group for tickers Lazer doesn't cover), the build loop runs each
|
|
244
|
+
// group's `buildUpdateCalls` in sequence — a fee-free group ordered ahead
|
|
245
|
+
// of a fee-charging one would already have appended its verify/feed
|
|
246
|
+
// moveCalls to the shared `tx` by the time the fee-charging group's own
|
|
247
|
+
// guard fired, breaking the "throw before any PTB mutation" guarantee.
|
|
248
|
+
// Checking every group's `requiresFeeSource` up front — before ANY group
|
|
249
|
+
// fetches or builds — closes that gap, and (unlike a referential check
|
|
250
|
+
// against a specific rule instance) keeps protecting a future
|
|
251
|
+
// fee-charging rule or a test double standing in for one.
|
|
252
|
+
if (!opts.feeSource && groups.some((group) => group.rule.requiresFeeSource)) {
|
|
253
|
+
throw new pyth_ts_1.OracleFeeSourceUnavailableError();
|
|
254
|
+
}
|
|
255
|
+
// Fetch every group's off-chain payload concurrently (independent network
|
|
256
|
+
// calls — no reason to serialize) and let ALL of them settle before the
|
|
257
|
+
// first PTB mutation below, so a later group's fetch failure can never
|
|
258
|
+
// leave an earlier group's moveCalls stranded in a caller-owned tx.
|
|
259
|
+
const groupsWithData = await Promise.all(groups.map(async (group) => ({
|
|
260
|
+
rule: group.rule,
|
|
261
|
+
tickers: group.tickers,
|
|
262
|
+
data: await resolveGroupUpdateData(host, group, opts.updateDataProvider),
|
|
263
|
+
})));
|
|
264
|
+
// Verified-`Update` handle per lazer-served ticker (one shared PTB value per
|
|
265
|
+
// group) — consumed by the collector-feed leg below.
|
|
266
|
+
const lazerUpdateByTicker = new Map();
|
|
267
|
+
for (const group of groupsWithData) {
|
|
268
|
+
const handle = (await group.rule.buildUpdateCalls(tx, host, group.data, {
|
|
269
|
+
cache: opts.cache,
|
|
270
|
+
feeSource: opts.feeSource,
|
|
271
|
+
})) ?? undefined;
|
|
272
|
+
// Route by the handle's kind discriminant — the one site the tag exists to
|
|
273
|
+
// protect: a future non-lazer handle (e.g. a WaterxRule value) must never
|
|
274
|
+
// be silently fed into pyth_lazer_rule::feed.
|
|
275
|
+
if (handle?.kind === "pyth_lazer_rule") {
|
|
276
|
+
for (const ticker of group.tickers)
|
|
277
|
+
lazerUpdateByTicker.set(ticker, handle.update);
|
|
278
|
+
}
|
|
113
279
|
}
|
|
114
280
|
// Aggregate each ticker, feeding whichever rules it is configured for.
|
|
115
281
|
for (const ticker of tickers) {
|
|
116
|
-
aggregateTicker(tx, host, {
|
|
282
|
+
aggregateTicker(tx, host, {
|
|
283
|
+
ticker,
|
|
284
|
+
priceInfoObjectId: priceInfoByTicker.get(ticker),
|
|
285
|
+
lazerUpdate: lazerUpdateByTicker.get(ticker),
|
|
286
|
+
});
|
|
117
287
|
}
|
|
118
288
|
}
|