@waterx/sdk 4.1.0 → 4.3.1
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 +29 -19
- 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 +121 -71
- 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 +7 -3
- package/dist/cjs/src/oracle/index.js +41 -8
- package/dist/cjs/src/oracle/price-update-rule.d.ts +32 -11
- package/dist/cjs/src/oracle/price-update-rule.js +23 -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 +85 -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 +6 -5
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +34 -6
- 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 +61 -18
- package/dist/cjs/src/oracle/source-list.d.ts +36 -0
- package/dist/cjs/src/oracle/source-list.js +57 -0
- package/dist/cjs/src/perp/client.d.ts +41 -28
- package/dist/cjs/src/perp/client.js +32 -20
- package/dist/cjs/src/perp/config-view.js +7 -6
- 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 +11 -4
- package/dist/cjs/src/perp/user/staking.js +2 -1
- package/dist/cjs/src/unified-client.d.ts +14 -9
- package/dist/cjs/src/unified-client.js +2 -2
- package/dist/cjs/src/utils/config.js +2 -1
- package/dist/cjs/src/utils/record.d.ts +12 -0
- package/dist/cjs/src/utils/record.js +22 -0
- 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 +121 -71
- 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 +7 -3
- package/dist/src/oracle/index.js +32 -10
- package/dist/src/oracle/price-update-rule.d.ts +32 -11
- package/dist/src/oracle/price-update-rule.js +22 -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 +81 -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 +7 -6
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +33 -5
- 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 +57 -15
- package/dist/src/oracle/source-list.d.ts +36 -0
- package/dist/src/oracle/source-list.js +53 -0
- package/dist/src/perp/client.d.ts +41 -28
- package/dist/src/perp/client.js +33 -21
- package/dist/src/perp/config-view.js +7 -6
- 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/perp/user/staking.js +2 -1
- package/dist/src/unified-client.d.ts +14 -9
- package/dist/src/unified-client.js +2 -2
- package/dist/src/utils/config.js +2 -1
- package/dist/src/utils/record.d.ts +12 -0
- package/dist/src/utils/record.js +19 -0
- package/package.json +1 -1
|
@@ -41,9 +41,6 @@ export const BatchPriceItem = new MoveStruct({ name: `${$moduleName}::BatchPrice
|
|
|
41
41
|
export const BatchPricePayload = new MoveStruct({ name: `${$moduleName}::BatchPricePayload`, fields: {
|
|
42
42
|
items: bcs.vector(BatchPriceItem)
|
|
43
43
|
} });
|
|
44
|
-
export const MerkleRoot = new MoveStruct({ name: `${$moduleName}::MerkleRoot`, fields: {
|
|
45
|
-
root: bcs.vector(bcs.u8())
|
|
46
|
-
} });
|
|
47
44
|
export const FeedConfig = new MoveStruct({ name: `${$moduleName}::FeedConfig`, fields: {
|
|
48
45
|
ticker: bcs.string(),
|
|
49
46
|
sources: bcs.vector(bcs.u64()),
|
|
@@ -129,17 +126,15 @@ export function feedBatchLatest(options) {
|
|
|
129
126
|
* and so aborts on any stale item, this path runs alongside Pyth/Supra. To let
|
|
130
127
|
* those cover a lagging TEE — the blast-radius bound in the GCP ADR — a
|
|
131
128
|
* _freshness_ miss ABSTAINS (records `none`, which `aggregator::remove_outliers`
|
|
132
|
-
* drops before the `weight_threshold` fail-closed check)
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* not
|
|
136
|
-
*
|
|
137
|
-
* with the on-chain feed config is a red flag, not a liveness blip. Bad signature,
|
|
138
|
-
* malformed batch, and a future timestamp always abort.
|
|
129
|
+
* drops before the `weight_threshold` fail-closed check). A _config/integrity_
|
|
130
|
+
* mismatch (source / ticker / method / confidence / deviation) still ABORTS: an
|
|
131
|
+
* enclave whose signed payload disagrees with the on-chain feed config is a red
|
|
132
|
+
* flag, not a liveness blip. Bad signature, malformed batch, a future timestamp,
|
|
133
|
+
* and replay always abort.
|
|
139
134
|
*
|
|
140
135
|
* Every gate is identical to `feed_batch_item_latest`; only the abort-vs-abstain
|
|
141
|
-
* disposition of the freshness
|
|
142
|
-
*
|
|
136
|
+
* disposition of the two freshness gates differs, and the sink is the caller's
|
|
137
|
+
* collector rather than a fresh single-rule one.
|
|
143
138
|
*/
|
|
144
139
|
export function collectBatchLatest(options) {
|
|
145
140
|
const packageAddress = options.package ?? '@waterx/rule';
|
|
@@ -161,63 +156,6 @@ export function collectBatchLatest(options) {
|
|
|
161
156
|
arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames),
|
|
162
157
|
});
|
|
163
158
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Streaming per-symbol sibling of `feed_batch_latest`. Verifies ONE enclave
|
|
166
|
-
* signature over a Merkle `root`, checks `item` is a leaf of that root via
|
|
167
|
-
* `proof`, then runs the identical per-item gates + `oracle::aggregate`. Use for a
|
|
168
|
-
* WaterxRule-only aggregator; for a Pyth-coexisting aggregator use
|
|
169
|
-
* `collect_single_with_proof`.
|
|
170
|
-
*/
|
|
171
|
-
export function feedSingleWithProof(options) {
|
|
172
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
173
|
-
const argumentsTypes = [
|
|
174
|
-
null,
|
|
175
|
-
null,
|
|
176
|
-
'0x2::clock::Clock',
|
|
177
|
-
null,
|
|
178
|
-
null,
|
|
179
|
-
'u64',
|
|
180
|
-
null,
|
|
181
|
-
'vector<vector<u8>>',
|
|
182
|
-
'vector<u8>'
|
|
183
|
-
];
|
|
184
|
-
const parameterNames = ["oracleObj", "config", "enclaveConfig", "enclave", "timestampMs", "item", "proof", "sig"];
|
|
185
|
-
return (tx) => tx.moveCall({
|
|
186
|
-
package: packageAddress,
|
|
187
|
-
module: 'waterx_rule',
|
|
188
|
-
function: 'feed_single_with_proof',
|
|
189
|
-
arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames),
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Collect-only sibling of `feed_single_with_proof` for the dual-rule latest path
|
|
194
|
-
* (WaterX alongside Pyth on one aggregator), mirroring `collect_batch_latest`:
|
|
195
|
-
* verifies root + proof, feeds the leaf into the caller's `PriceCollector` WITHOUT
|
|
196
|
-
* calling `oracle::aggregate`. Abort vs abstain matches `collect_batch_latest`
|
|
197
|
-
* exactly — a config/integrity mismatch aborts, a freshness miss abstains so Pyth
|
|
198
|
-
* can cover a lagging TEE.
|
|
199
|
-
*/
|
|
200
|
-
export function collectSingleWithProof(options) {
|
|
201
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
202
|
-
const argumentsTypes = [
|
|
203
|
-
null,
|
|
204
|
-
null,
|
|
205
|
-
'0x2::clock::Clock',
|
|
206
|
-
null,
|
|
207
|
-
null,
|
|
208
|
-
'u64',
|
|
209
|
-
null,
|
|
210
|
-
'vector<vector<u8>>',
|
|
211
|
-
'vector<u8>'
|
|
212
|
-
];
|
|
213
|
-
const parameterNames = ["collector", "config", "enclaveConfig", "enclave", "timestampMs", "item", "proof", "sig"];
|
|
214
|
-
return (tx) => tx.moveCall({
|
|
215
|
-
package: packageAddress,
|
|
216
|
-
module: 'waterx_rule',
|
|
217
|
-
function: 'collect_single_with_proof',
|
|
218
|
-
arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames),
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
159
|
/**
|
|
222
160
|
* Verifies a historical quote without touching `waterx_oracle`.
|
|
223
161
|
*
|
|
@@ -409,32 +347,6 @@ export function batchPriceIntent(options = {}) {
|
|
|
409
347
|
function: 'batch_price_intent',
|
|
410
348
|
});
|
|
411
349
|
}
|
|
412
|
-
export function merkleRootIntent(options = {}) {
|
|
413
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
414
|
-
return (tx) => tx.moveCall({
|
|
415
|
-
package: packageAddress,
|
|
416
|
-
module: 'waterx_rule',
|
|
417
|
-
function: 'merkle_root_intent',
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* The keccak256 leaf hash for `item` — `keccak256(0x00 || BCS(item))`. Exposed so
|
|
422
|
-
* the off-chain builder / SDK can be pinned against the on-chain encoding in a
|
|
423
|
-
* cross-implementation golden test.
|
|
424
|
-
*/
|
|
425
|
-
export function leafHashOf(options) {
|
|
426
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
427
|
-
const argumentsTypes = [
|
|
428
|
-
null
|
|
429
|
-
];
|
|
430
|
-
const parameterNames = ["item"];
|
|
431
|
-
return (tx) => tx.moveCall({
|
|
432
|
-
package: packageAddress,
|
|
433
|
-
module: 'waterx_rule',
|
|
434
|
-
function: 'leaf_hash_of',
|
|
435
|
-
arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames),
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
350
|
export function maxBatchSize(options = {}) {
|
|
439
351
|
const packageAddress = options.package ?? '@waterx/rule';
|
|
440
352
|
return (tx) => tx.moveCall({
|
|
@@ -821,54 +733,6 @@ export function sourceGataUsdtPerpWs(options = {}) {
|
|
|
821
733
|
function: 'source_gata_usdt_perp_ws',
|
|
822
734
|
});
|
|
823
735
|
}
|
|
824
|
-
export function sourceBybitSpotWs(options = {}) {
|
|
825
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
826
|
-
return (tx) => tx.moveCall({
|
|
827
|
-
package: packageAddress,
|
|
828
|
-
module: 'waterx_rule',
|
|
829
|
-
function: 'source_bybit_spot_ws',
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
export function sourceXstockEquityRest(options = {}) {
|
|
833
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
834
|
-
return (tx) => tx.moveCall({
|
|
835
|
-
package: packageAddress,
|
|
836
|
-
module: 'waterx_rule',
|
|
837
|
-
function: 'source_xstock_equity_rest',
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
export function sourceOkxSpotWs(options = {}) {
|
|
841
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
842
|
-
return (tx) => tx.moveCall({
|
|
843
|
-
package: packageAddress,
|
|
844
|
-
module: 'waterx_rule',
|
|
845
|
-
function: 'source_okx_spot_ws',
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
export function sourceHyperliquidPerpWs(options = {}) {
|
|
849
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
850
|
-
return (tx) => tx.moveCall({
|
|
851
|
-
package: packageAddress,
|
|
852
|
-
module: 'waterx_rule',
|
|
853
|
-
function: 'source_hyperliquid_perp_ws',
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
export function sourceGateioSpotWs(options = {}) {
|
|
857
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
858
|
-
return (tx) => tx.moveCall({
|
|
859
|
-
package: packageAddress,
|
|
860
|
-
module: 'waterx_rule',
|
|
861
|
-
function: 'source_gateio_spot_ws',
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
export function sourceKrakenSpotWs(options = {}) {
|
|
865
|
-
const packageAddress = options.package ?? '@waterx/rule';
|
|
866
|
-
return (tx) => tx.moveCall({
|
|
867
|
-
package: packageAddress,
|
|
868
|
-
module: 'waterx_rule',
|
|
869
|
-
function: 'source_kraken_spot_ws',
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
736
|
export function methodDirect(options = {}) {
|
|
873
737
|
const packageAddress = options.package ?? '@waterx/rule';
|
|
874
738
|
return (tx) => tx.moveCall({
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
*
|
|
20
20
|
* `refreshOraclePrices` additionally routes the on-chain price *update* leg
|
|
21
21
|
* (the fetch + verify/push step, before any of the above feeding) through the
|
|
22
|
-
* `PriceUpdateRule`
|
|
22
|
+
* `PriceUpdateRule` of EVERY source in the `host.oracleSources` fed set — see
|
|
23
|
+
* `rule-registry.ts`.
|
|
23
24
|
*/
|
|
24
25
|
import type { Transaction, TransactionArgument } from "@mysten/sui/transactions";
|
|
25
26
|
import type { OracleHost } from "./host.ts";
|
|
@@ -84,27 +85,26 @@ export declare function aggregateTickerWithConstant(tx: Transaction, host: Oracl
|
|
|
84
85
|
* entry, Lazer if the lazer update leg served it — see below — Supra when
|
|
85
86
|
* enabled, Constant when it's a constant ticker).
|
|
86
87
|
*
|
|
87
|
-
* Before that, the on-chain price *update* leg is routed by
|
|
88
|
-
* (see `rule-registry.ts`):
|
|
89
|
-
* `supportedTickers(host)
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* dry-run, which is correct.
|
|
88
|
+
* Before that, the on-chain price *update* leg is routed by the
|
|
89
|
+
* `host.oracleSources` fed set (see `rule-registry.ts`): EVERY listed source
|
|
90
|
+
* updates the tickers its own `supportedTickers(host)` serves, all in this one
|
|
91
|
+
* PTB. There is **no cross-source fallback** — a requested ticker NO listed
|
|
92
|
+
* source serves, and that is not a constant-only ticker (which needs no
|
|
93
|
+
* price-update leg), fails the build immediately with a clear error naming
|
|
94
|
+
* the ticker and the list. That is the deliberate "fail the tx-build, don't
|
|
95
|
+
* silently reroute" contract: a wrong-but-present feed id is NOT validated
|
|
96
|
+
* here (it surfaces on-chain at dry-run); a ticker MISSING from every listed
|
|
97
|
+
* source's feeds is caught here.
|
|
98
98
|
*
|
|
99
|
-
*
|
|
100
|
-
* per-rule PTB atomicity. A fee-source pre-check runs first (
|
|
101
|
-
* `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
|
|
102
|
-
* fee-charging source with no `opts.feeSource` throws
|
|
99
|
+
* Each source's fetch + build runs against its own infra, guaranteeing
|
|
100
|
+
* per-rule PTB atomicity. A fee-source pre-check runs first (any listed
|
|
101
|
+
* source's `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
|
|
102
|
+
* so a fee-charging source with no `opts.feeSource` throws
|
|
103
103
|
* `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
|
|
104
|
-
* moveCalls. Only once that check passes
|
|
105
|
-
*
|
|
106
|
-
* can still fail mid-append for
|
|
107
|
-
* throw.
|
|
104
|
+
* moveCalls. Only once that check passes do the off-chain fetches run — in
|
|
105
|
+
* parallel across sources — and ALL settle before the first PTB mutation;
|
|
106
|
+
* on-chain reads inside `buildUpdateCalls` can still fail mid-append for
|
|
107
|
+
* other reasons — callers discard the tx on any throw.
|
|
108
108
|
*
|
|
109
109
|
* **Collector-feed leg is rule-aware:** a lazer-served group's
|
|
110
110
|
* `buildUpdateCalls` returns the verified `Update` PTB value
|
|
@@ -139,7 +139,7 @@ export declare function refreshOraclePrices(tx: Transaction, host: OracleHost, t
|
|
|
139
139
|
/**
|
|
140
140
|
* @internal Test-only: layer fake `PriceUpdateRule`s on top of the
|
|
141
141
|
* production registry (see `rule-registry.ts`'s `resolveOracleRule`).
|
|
142
|
-
* Production callers never set this — routing is by `host.
|
|
142
|
+
* Production callers never set this — routing is by `host.oracleSources`
|
|
143
143
|
* alone.
|
|
144
144
|
*/
|
|
145
145
|
ruleOverrides?: Partial<Record<OracleSource, PriceUpdateRule>>;
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
*
|
|
20
20
|
* `refreshOraclePrices` additionally routes the on-chain price *update* leg
|
|
21
21
|
* (the fetch + verify/push step, before any of the above feeding) through the
|
|
22
|
-
* `PriceUpdateRule`
|
|
22
|
+
* `PriceUpdateRule` of EVERY source in the `host.oracleSources` fed set — see
|
|
23
|
+
* `rule-registry.ts`.
|
|
23
24
|
*/
|
|
24
25
|
import { aggregate as aggregateCall, newCollector } from "../generated/waterx_oracle/oracle.js";
|
|
26
|
+
import { ownEntry } from "../utils/record.js";
|
|
25
27
|
import { OracleFeeSourceUnavailableError } from "./pyth.js";
|
|
26
28
|
import { resolveOracleRule } from "./rule-registry.js";
|
|
27
29
|
import { feedConstantRule } from "./rules/constant-rule.js";
|
|
@@ -115,7 +117,13 @@ export function aggregateTicker(tx, host, args) {
|
|
|
115
117
|
// waterx_rule::collect_batch_latest verifies the batch signature and feeds
|
|
116
118
|
// this collector's symbol from the batch. If the ticker's aggregator does
|
|
117
119
|
// not (yet) weight `WaterxRule`, the contribution is silently dropped
|
|
118
|
-
// on-chain — feeding ahead of the weight migration is
|
|
120
|
+
// on-chain — feeding ahead of the weight migration is safe for THIS tx.
|
|
121
|
+
// CAVEAT (unlike lazer): the feed call records a per-symbol signed-
|
|
122
|
+
// timestamp high-water mark REGARDLESS of weights, and a replayed
|
|
123
|
+
// timestamp ABORTS (`EReplayedSignature`, audit F-014) — so two PTBs
|
|
124
|
+
// carrying the same envelope for the same symbol cannot both land; the
|
|
125
|
+
// second aborts even where waterx is unweighted. See WaterxRule's module
|
|
126
|
+
// header.
|
|
119
127
|
feedWaterxRule(tx, host, collector, args.waterxEnvelope);
|
|
120
128
|
fed = true;
|
|
121
129
|
}
|
|
@@ -156,7 +164,7 @@ export function aggregateTickerWithPyth(tx, host, args) {
|
|
|
156
164
|
* {@link refreshOraclePrices}), which feeds both.
|
|
157
165
|
*/
|
|
158
166
|
export function aggregateTickerWithConstant(tx, host, args) {
|
|
159
|
-
if (host.config.packages.pyth_rule?.feeds
|
|
167
|
+
if (ownEntry(host.config.packages.pyth_rule?.feeds, args.ticker) !== undefined) {
|
|
160
168
|
throw new Error(`'${args.ticker}' is in pyth_rule.feeds (dual-feed) — feed both via aggregateTicker({ priceInfoObjectId }) / refreshOraclePrices, not aggregateTickerWithConstant`);
|
|
161
169
|
}
|
|
162
170
|
aggregateTicker(tx, host, { ticker: args.ticker });
|
|
@@ -167,27 +175,26 @@ export function aggregateTickerWithConstant(tx, host, args) {
|
|
|
167
175
|
* entry, Lazer if the lazer update leg served it — see below — Supra when
|
|
168
176
|
* enabled, Constant when it's a constant ticker).
|
|
169
177
|
*
|
|
170
|
-
* Before that, the on-chain price *update* leg is routed by
|
|
171
|
-
* (see `rule-registry.ts`):
|
|
172
|
-
* `supportedTickers(host)
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* dry-run, which is correct.
|
|
178
|
+
* Before that, the on-chain price *update* leg is routed by the
|
|
179
|
+
* `host.oracleSources` fed set (see `rule-registry.ts`): EVERY listed source
|
|
180
|
+
* updates the tickers its own `supportedTickers(host)` serves, all in this one
|
|
181
|
+
* PTB. There is **no cross-source fallback** — a requested ticker NO listed
|
|
182
|
+
* source serves, and that is not a constant-only ticker (which needs no
|
|
183
|
+
* price-update leg), fails the build immediately with a clear error naming
|
|
184
|
+
* the ticker and the list. That is the deliberate "fail the tx-build, don't
|
|
185
|
+
* silently reroute" contract: a wrong-but-present feed id is NOT validated
|
|
186
|
+
* here (it surfaces on-chain at dry-run); a ticker MISSING from every listed
|
|
187
|
+
* source's feeds is caught here.
|
|
181
188
|
*
|
|
182
|
-
*
|
|
183
|
-
* per-rule PTB atomicity. A fee-source pre-check runs first (
|
|
184
|
-
* `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
|
|
185
|
-
* fee-charging source with no `opts.feeSource` throws
|
|
189
|
+
* Each source's fetch + build runs against its own infra, guaranteeing
|
|
190
|
+
* per-rule PTB atomicity. A fee-source pre-check runs first (any listed
|
|
191
|
+
* source's `requiresFeeSource`) BEFORE any off-chain fetch or PTB mutation —
|
|
192
|
+
* so a fee-charging source with no `opts.feeSource` throws
|
|
186
193
|
* `OracleFeeSourceUnavailable` with zero wasted network calls and zero stray
|
|
187
|
-
* moveCalls. Only once that check passes
|
|
188
|
-
*
|
|
189
|
-
* can still fail mid-append for
|
|
190
|
-
* throw.
|
|
194
|
+
* moveCalls. Only once that check passes do the off-chain fetches run — in
|
|
195
|
+
* parallel across sources — and ALL settle before the first PTB mutation;
|
|
196
|
+
* on-chain reads inside `buildUpdateCalls` can still fail mid-append for
|
|
197
|
+
* other reasons — callers discard the tx on any throw.
|
|
191
198
|
*
|
|
192
199
|
* **Collector-feed leg is rule-aware:** a lazer-served group's
|
|
193
200
|
* `buildUpdateCalls` returns the verified `Update` PTB value
|
|
@@ -208,78 +215,121 @@ export function aggregateTickerWithConstant(tx, host, args) {
|
|
|
208
215
|
export async function refreshOraclePrices(tx, host, tickers, opts = {}) {
|
|
209
216
|
if (tickers.length === 0)
|
|
210
217
|
return;
|
|
218
|
+
// Dedupe the caller's list (order-preserving): a repeated ticker would
|
|
219
|
+
// otherwise aggregate TWICE in this one PTB — wasted gas for every rule,
|
|
220
|
+
// and a hard ABORT under waterx: the second `collect_batch_latest` carries
|
|
221
|
+
// the same envelope, and the on-chain per-symbol replay guard rejects an
|
|
222
|
+
// already-accepted signed timestamp (`EReplayedSignature`, F-014) even
|
|
223
|
+
// inside a single transaction.
|
|
224
|
+
tickers = [...new Set(tickers)];
|
|
211
225
|
// price_info_object lookup for every ticker with a pyth_rule.feeds entry —
|
|
212
226
|
// needed by aggregateTicker's (unchanged) Pyth feed step below regardless of
|
|
213
227
|
// which rule performed the on-chain update for that ticker.
|
|
214
|
-
const pythTickers = tickers.filter((t) => host.config.packages.pyth_rule?.feeds
|
|
228
|
+
const pythTickers = tickers.filter((t) => ownEntry(host.config.packages.pyth_rule?.feeds, t) !== undefined);
|
|
215
229
|
const priceInfoByTicker = new Map();
|
|
216
230
|
pythTickers.forEach((t) => priceInfoByTicker.set(t, host.getPythFeed(t).price_info_object));
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
//
|
|
231
|
+
// The fed set is a LIST (`host.oracleSources`, normalized + deduped at
|
|
232
|
+
// client creation): ONE build carries every listed source's data, and the
|
|
233
|
+
// chain's per-ticker weight tables decide which contributions count —
|
|
234
|
+
// feeding an unweighted rule's PRICE is dropped on-chain, while starving a
|
|
235
|
+
// weighted one aborts. That asymmetry is what makes weight migrations
|
|
236
|
+
// safe: flip weights per ticker at any time while the fed set stays a
|
|
237
|
+
// superset of every ticker's weighted set. (One caveat: waterx's feed call
|
|
238
|
+
// burns a per-symbol signed-timestamp high-water mark regardless of
|
|
239
|
+
// weights — see aggregateTicker's waterx branch.) Still NO fallback
|
|
240
|
+
// BETWEEN sources: each group serves only the tickers its own feeds list.
|
|
241
|
+
// Zero-ticker groups are dropped here so everything downstream (fee check,
|
|
242
|
+
// fetch fan-out, update-leg build) can assume every group has work.
|
|
243
|
+
const groups = host.oracleSources
|
|
244
|
+
.map((source) => {
|
|
245
|
+
const rule = resolveOracleRule(source, opts.ruleOverrides);
|
|
246
|
+
const supported = new Set(rule.supportedTickers(host));
|
|
247
|
+
return { source, rule, tickers: tickers.filter((t) => supported.has(t)) };
|
|
248
|
+
})
|
|
249
|
+
.filter((group) => group.tickers.length > 0);
|
|
250
|
+
// Fail the tx-build (NOT client init, NOT a silent reroute) when NO listed
|
|
251
|
+
// source has a feed for a requested ticker that actually needs a price
|
|
225
252
|
// update. Only a CONSTANT-ONLY ticker is exempt — priced entirely by
|
|
226
253
|
// `constant_rule`, it needs no update leg from any source. A DUAL-FEED ticker
|
|
227
254
|
// (constant AND pyth) still needs its Pyth leg refreshed, so `isConstantTicker`
|
|
228
|
-
// alone must NOT exempt it:
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
255
|
+
// alone must NOT exempt it: with no source able to serve it, feeding an
|
|
256
|
+
// unrefreshed Pyth leg would price it stale (or abort on a missing weighted
|
|
257
|
+
// source). `priceInfoByTicker.has(t)` ⇔ the ticker has a `pyth_rule.feeds`
|
|
258
|
+
// entry, so `constant && !hasPyth` is exactly constant-only. This catches a
|
|
259
|
+
// MISSING feed; a present-but-WRONG feed id is deliberately not validated
|
|
260
|
+
// here (it aborts on-chain at dry-run).
|
|
261
|
+
const covered = new Set(groups.flatMap((group) => group.tickers));
|
|
234
262
|
const isConstantOnly = (t) => host.isConstantTicker(t) && !priceInfoByTicker.has(t);
|
|
235
|
-
const unservable = tickers.filter((t) => !
|
|
263
|
+
const unservable = tickers.filter((t) => !covered.has(t) && !isConstantOnly(t));
|
|
236
264
|
if (unservable.length > 0) {
|
|
237
|
-
|
|
265
|
+
const sources = host.oracleSources.join(", ");
|
|
266
|
+
throw new Error(`oracleSource [${sources}] has no feed configured for ticker(s): ` +
|
|
238
267
|
`${unservable.join(", ")}. Sources are self-contained with no fallback — add ` +
|
|
239
|
-
|
|
268
|
+
`feeds for them under a listed source, or list a source that serves them.`);
|
|
240
269
|
}
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
if (selectedGroup.length > 0 && !opts.feeSource && selectedRule.requiresFeeSource) {
|
|
270
|
+
// Fee-source pre-check, hoisted ABOVE the off-chain fetches and PTB build
|
|
271
|
+
// below. It consults only `rule.requiresFeeSource` — known before any fetch
|
|
272
|
+
// or PTB mutation — so a fee-charging source (Pyth Core) in the fed set with
|
|
273
|
+
// no `feeSource` throws with ZERO wasted network calls and zero PTB
|
|
274
|
+
// commands, rather than waiting for `buildPythPriceUpdateCalls`'s own
|
|
275
|
+
// per-call guard to fire after the off-chain fetches already ran.
|
|
276
|
+
if (!opts.feeSource && groups.some((group) => group.rule.requiresFeeSource)) {
|
|
249
277
|
throw new OracleFeeSourceUnavailableError();
|
|
250
278
|
}
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
279
|
+
// Phase 1 — resolve every group's update data IN PARALLEL: the per-source
|
|
280
|
+
// fetches (Hermes VAA / Lazer POST / quote-center GET) are independent
|
|
281
|
+
// network calls on the tx-build money path, so a multi-source fed set must
|
|
282
|
+
// not pay one RTT per source sequentially. ALL fetches settle before the
|
|
283
|
+
// first PTB mutation below, so a fetch failure never strands moveCalls in a
|
|
284
|
+
// caller-owned tx — and a failure in ANY group fails the whole build (a
|
|
285
|
+
// listed source is load-bearing; silently building without it would starve
|
|
286
|
+
// its weighted tickers on-chain).
|
|
287
|
+
const dataByGroup = await Promise.all(groups.map((group) => resolveGroupUpdateData(host, group, opts.updateDataProvider)));
|
|
288
|
+
// Phase 2 — build each group's update leg sequentially, in list order, so
|
|
289
|
+
// PTB command order stays deterministic. The carry step below is an
|
|
290
|
+
// exhaustive switch over the group's rule kind: a future source whose feed
|
|
291
|
+
// leg needs per-ticker data from its update leg must decide its carry here
|
|
292
|
+
// — falling through silently would starve its weighted tickers on-chain.
|
|
255
293
|
const lazerUpdateByTicker = new Map();
|
|
256
294
|
// Signed batch envelope per waterx-served ticker. Unlike Lazer's shared PTB
|
|
257
295
|
// handle, waterx's verify+feed is bundled into `collect_batch_latest` in the
|
|
258
296
|
// per-ticker feed leg, so its `buildUpdateCalls` emits nothing and the
|
|
259
297
|
// envelope is carried straight from the group's fetched data.
|
|
260
298
|
const waterxEnvelopeByTicker = new Map();
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
const handle = (await selectedRule.buildUpdateCalls(tx, host, data, {
|
|
299
|
+
for (const [i, group] of groups.entries()) {
|
|
300
|
+
const data = dataByGroup[i] ?? null;
|
|
301
|
+
const handle = (await group.rule.buildUpdateCalls(tx, host, data, {
|
|
265
302
|
cache: opts.cache,
|
|
266
303
|
feeSource: opts.feeSource,
|
|
267
304
|
})) ?? undefined;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
305
|
+
switch (group.rule.kind) {
|
|
306
|
+
case "pyth_rule":
|
|
307
|
+
// Core's update leg wrote the PriceInfoObjects in place — the feed
|
|
308
|
+
// leg reads them by id (`priceInfoByTicker`), nothing to carry.
|
|
309
|
+
break;
|
|
310
|
+
case "pyth_lazer_rule":
|
|
311
|
+
// Route by the handle's kind discriminant — the tag exists so a
|
|
312
|
+
// non-lazer handle can never be silently fed into
|
|
313
|
+
// pyth_lazer_rule::feed.
|
|
314
|
+
if (handle?.kind === "pyth_lazer_rule") {
|
|
315
|
+
for (const ticker of group.tickers)
|
|
316
|
+
lazerUpdateByTicker.set(ticker, handle.update);
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
case "waterx_rule": {
|
|
320
|
+
// waterx_rule emits no shared handle (verify+feed is bundled into the
|
|
321
|
+
// per-ticker `collect_batch_latest`), so the envelope is carried
|
|
322
|
+
// straight from this group's fetched data to the feed leg below.
|
|
323
|
+
const envelope = waterxEnvelopeOf(data);
|
|
324
|
+
if (envelope) {
|
|
325
|
+
for (const ticker of group.tickers)
|
|
326
|
+
waterxEnvelopeByTicker.set(ticker, envelope);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
default: {
|
|
331
|
+
const exhausted = group.rule.kind;
|
|
332
|
+
throw new Error(`refreshOraclePrices: unhandled rule kind '${String(exhausted)}'`);
|
|
283
333
|
}
|
|
284
334
|
}
|
|
285
335
|
}
|