@waterx/sdk 4.3.3 → 5.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/.claude/skills/waterx-sdk-integration/SKILL.md +41 -35
- package/README.md +81 -77
- package/dist/cjs/src/account/config.d.ts +3 -5
- package/dist/cjs/src/account/funding/wormhole.d.ts +1 -2
- package/dist/cjs/src/account/funding/wormhole.js +1 -2
- package/dist/cjs/src/oracle/aggregate.d.ts +45 -61
- package/dist/cjs/src/oracle/aggregate.js +138 -138
- package/dist/cjs/src/oracle/config.d.ts +35 -49
- package/dist/cjs/src/oracle/config.js +1 -1
- package/dist/cjs/src/oracle/host.d.ts +14 -19
- package/dist/cjs/src/oracle/host.js +3 -3
- package/dist/cjs/src/oracle/index.d.ts +25 -16
- package/dist/cjs/src/oracle/index.js +79 -66
- package/dist/cjs/src/oracle/price-update-rule.d.ts +104 -66
- package/dist/cjs/src/oracle/price-update-rule.js +24 -16
- package/dist/cjs/src/oracle/pyth-pro-history.d.ts +36 -0
- package/dist/cjs/src/oracle/pyth-pro-history.js +41 -0
- package/dist/cjs/src/oracle/read-plane.d.ts +35 -53
- package/dist/cjs/src/oracle/read-plane.js +26 -48
- package/dist/cjs/src/oracle/read-prices.d.ts +105 -0
- package/dist/cjs/src/oracle/read-prices.js +162 -0
- package/dist/cjs/src/oracle/rule-registry.d.ts +12 -12
- package/dist/cjs/src/oracle/rule-registry.js +12 -14
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +40 -20
- package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +137 -12
- package/dist/cjs/src/oracle/rules/waterx-rule.js +318 -55
- package/dist/cjs/src/oracle/schedule.d.ts +109 -0
- package/dist/cjs/src/oracle/schedule.js +991 -0
- package/dist/cjs/src/oracle/source-list.d.ts +31 -27
- package/dist/cjs/src/oracle/source-list.js +43 -45
- package/dist/cjs/src/oracle/symbol-catalog.d.ts +54 -0
- package/dist/cjs/src/oracle/symbol-catalog.js +69 -0
- package/dist/cjs/src/oracle/update-fetch.d.ts +11 -15
- package/dist/cjs/src/oracle/update-fetch.js +12 -19
- package/dist/cjs/src/oracle/validate.d.ts +105 -0
- package/dist/cjs/src/oracle/validate.js +185 -0
- package/dist/cjs/src/oracle/weight-coverage.d.ts +79 -0
- package/dist/cjs/src/oracle/weight-coverage.js +173 -0
- package/dist/cjs/src/perp/client.d.ts +35 -52
- package/dist/cjs/src/perp/client.js +44 -17
- package/dist/cjs/src/perp/config-view.d.ts +5 -14
- package/dist/cjs/src/perp/config-view.js +5 -16
- package/dist/cjs/src/perp/config.d.ts +3 -3
- package/dist/cjs/src/perp/config.js +11 -13
- package/dist/cjs/src/perp/index.d.ts +4 -6
- package/dist/cjs/src/perp/index.js +35 -18
- package/dist/cjs/src/perp/tx-builders/common.d.ts +86 -48
- package/dist/cjs/src/perp/tx-builders/common.js +115 -47
- package/dist/cjs/src/perp/tx-builders/wlp.d.ts +3 -14
- package/dist/cjs/src/perp/tx-builders/wlp.js +6 -35
- package/dist/cjs/src/perp/tx-builders.d.ts +6 -5
- package/dist/cjs/src/perp/tx-builders.js +16 -10
- package/dist/cjs/src/unified-client.d.ts +8 -34
- package/dist/cjs/src/unified-client.js +3 -4
- package/dist/cjs/src/utils/config.d.ts +0 -8
- package/dist/cjs/src/utils/config.js +5 -13
- package/dist/src/account/config.d.ts +3 -5
- package/dist/src/account/funding/wormhole.d.ts +1 -2
- package/dist/src/account/funding/wormhole.js +1 -2
- package/dist/src/oracle/aggregate.d.ts +45 -61
- package/dist/src/oracle/aggregate.js +138 -137
- package/dist/src/oracle/config.d.ts +35 -49
- package/dist/src/oracle/config.js +1 -1
- package/dist/src/oracle/host.d.ts +14 -19
- package/dist/src/oracle/host.js +3 -3
- package/dist/src/oracle/index.d.ts +25 -16
- package/dist/src/oracle/index.js +55 -50
- package/dist/src/oracle/price-update-rule.d.ts +104 -66
- package/dist/src/oracle/price-update-rule.js +23 -16
- package/dist/src/oracle/pyth-pro-history.d.ts +36 -0
- package/dist/src/oracle/pyth-pro-history.js +38 -0
- package/dist/src/oracle/read-plane.d.ts +35 -53
- package/dist/src/oracle/read-plane.js +25 -47
- package/dist/src/oracle/read-prices.d.ts +105 -0
- package/dist/src/oracle/read-prices.js +156 -0
- package/dist/src/oracle/rule-registry.d.ts +12 -12
- package/dist/src/oracle/rule-registry.js +12 -14
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +24 -3
- package/dist/src/oracle/rules/pyth-lazer-rule.js +39 -20
- package/dist/src/oracle/rules/waterx-rule.d.ts +137 -12
- package/dist/src/oracle/rules/waterx-rule.js +314 -56
- package/dist/src/oracle/schedule.d.ts +109 -0
- package/dist/src/oracle/schedule.js +985 -0
- package/dist/src/oracle/source-list.d.ts +31 -27
- package/dist/src/oracle/source-list.js +42 -43
- package/dist/src/oracle/symbol-catalog.d.ts +54 -0
- package/dist/src/oracle/symbol-catalog.js +65 -0
- package/dist/src/oracle/update-fetch.d.ts +11 -15
- package/dist/src/oracle/update-fetch.js +12 -18
- package/dist/src/oracle/validate.d.ts +105 -0
- package/dist/src/oracle/validate.js +177 -0
- package/dist/src/oracle/weight-coverage.d.ts +79 -0
- package/dist/src/oracle/weight-coverage.js +166 -0
- package/dist/src/perp/client.d.ts +35 -52
- package/dist/src/perp/client.js +45 -18
- package/dist/src/perp/config-view.d.ts +5 -14
- package/dist/src/perp/config-view.js +5 -16
- package/dist/src/perp/config.d.ts +3 -3
- package/dist/src/perp/config.js +11 -13
- package/dist/src/perp/index.d.ts +4 -6
- package/dist/src/perp/index.js +11 -5
- package/dist/src/perp/tx-builders/common.d.ts +86 -48
- package/dist/src/perp/tx-builders/common.js +114 -48
- package/dist/src/perp/tx-builders/wlp.d.ts +3 -14
- package/dist/src/perp/tx-builders/wlp.js +6 -35
- package/dist/src/perp/tx-builders.d.ts +6 -5
- package/dist/src/perp/tx-builders.js +15 -6
- package/dist/src/unified-client.d.ts +8 -34
- package/dist/src/unified-client.js +3 -4
- package/dist/src/utils/config.d.ts +0 -8
- package/dist/src/utils/config.js +5 -12
- package/package.json +1 -1
- package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
- package/dist/cjs/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -17
- package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
- package/dist/cjs/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -113
- package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
- package/dist/cjs/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -31
- package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
- package/dist/cjs/src/generated/waterx_pyth_rule/pyth_rule.js +0 -214
- package/dist/cjs/src/oracle/pyth.d.ts +0 -227
- package/dist/cjs/src/oracle/pyth.js +0 -652
- package/dist/cjs/src/oracle/rules/pyth-core-rule.d.ts +0 -15
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +0 -100
- package/dist/cjs/src/oracle/rules/pyth-rule.d.ts +0 -11
- package/dist/cjs/src/oracle/rules/pyth-rule.js +0 -29
- package/dist/cjs/src/oracle/rules/sponsor.d.ts +0 -32
- package/dist/cjs/src/oracle/rules/sponsor.js +0 -56
- package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.d.ts +0 -12
- package/dist/src/generated/pyth_sponsor_rule/deps/sui/balance.js +0 -14
- package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.d.ts +0 -68
- package/dist/src/generated/pyth_sponsor_rule/pyth_sponsor_rule.js +0 -73
- package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.d.ts +0 -36
- package/dist/src/generated/waterx_pyth_rule/deps/sui/vec_map.js +0 -27
- package/dist/src/generated/waterx_pyth_rule/pyth_rule.d.ts +0 -192
- package/dist/src/generated/waterx_pyth_rule/pyth_rule.js +0 -170
- package/dist/src/oracle/pyth.d.ts +0 -227
- package/dist/src/oracle/pyth.js +0 -638
- package/dist/src/oracle/rules/pyth-core-rule.d.ts +0 -15
- package/dist/src/oracle/rules/pyth-core-rule.js +0 -97
- package/dist/src/oracle/rules/pyth-rule.d.ts +0 -11
- package/dist/src/oracle/rules/pyth-rule.js +0 -26
- package/dist/src/oracle/rules/sponsor.d.ts +0 -32
- package/dist/src/oracle/rules/sponsor.js +0 -52
|
@@ -38,22 +38,30 @@
|
|
|
38
38
|
* other; only the single-rule `feed_*` entries abort on a replay).
|
|
39
39
|
*/
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
-
exports.WaterxRule = exports.WATERX_INFRA = exports.MERKLE_ROOT_INTENT = exports.BATCH_PRICE_INTENT = void 0;
|
|
41
|
+
exports.WaterxRule = exports.WATERX_MAX_BATCH_SYMBOLS = exports.WATERX_CLOCK_SKEW_TOLERANCE_MS = exports.WATERX_MAX_PRICE_AGE_MS = exports.WATERX_INFRA = exports.MERKLE_ROOT_INTENT = exports.BATCH_PRICE_INTENT = void 0;
|
|
42
42
|
exports.waterxQuoteCenterEndpoint = waterxQuoteCenterEndpoint;
|
|
43
|
+
exports.isFreshWaterxEntry = isFreshWaterxEntry;
|
|
43
44
|
exports.parseSignedEnvelope = parseSignedEnvelope;
|
|
44
45
|
exports.parseSignedLeaves = parseSignedLeaves;
|
|
46
|
+
exports.fetchWaterxSignedUpdate = fetchWaterxSignedUpdate;
|
|
47
|
+
exports.fetchWaterxSignedLeaves = fetchWaterxSignedLeaves;
|
|
45
48
|
exports.waterxLeavesOf = waterxLeavesOf;
|
|
46
49
|
exports.waterxEnvelopeOf = waterxEnvelopeOf;
|
|
47
50
|
exports.feedWaterxRuleWithProof = feedWaterxRuleWithProof;
|
|
48
51
|
exports.feedWaterxRule = feedWaterxRule;
|
|
52
|
+
exports.pullWaterxQuotes = pullWaterxQuotes;
|
|
53
|
+
exports.fetchWaterxUpdateData = fetchWaterxUpdateData;
|
|
49
54
|
const bcs_1 = require("@mysten/bcs");
|
|
50
55
|
const waterx_rule_ts_1 = require("../../generated/waterx_rule/waterx_rule.js");
|
|
51
56
|
const record_ts_1 = require("../../utils/record.js");
|
|
52
57
|
const price_update_rule_ts_1 = require("../price-update-rule.js");
|
|
53
58
|
const update_fetch_ts_1 = require("../update-fetch.js");
|
|
54
|
-
/** Intent the quote-center signs a whole BATCH payload under
|
|
55
|
-
*
|
|
56
|
-
*
|
|
59
|
+
/** Intent the quote-center signs a whole BATCH payload under. Exported to NAME
|
|
60
|
+
* the signing scheme only — consumers no longer mirror the intent gate
|
|
61
|
+
* themselves: every quote-center pull (tx-build fetch, read executor, BE
|
|
62
|
+
* prefetch) goes through {@link fetchWaterxSignedUpdate} /
|
|
63
|
+
* {@link fetchWaterxSignedLeaves}, which enforce it, so a mispointed endpoint
|
|
64
|
+
* is rejected identically on every path. */
|
|
57
65
|
exports.BATCH_PRICE_INTENT = 1;
|
|
58
66
|
/**
|
|
59
67
|
* Intent the quote-center signs a snapshot's Merkle ROOT under
|
|
@@ -67,12 +75,11 @@ exports.BATCH_PRICE_INTENT = 1;
|
|
|
67
75
|
exports.MERKLE_ROOT_INTENT = 2;
|
|
68
76
|
/**
|
|
69
77
|
* WaterX quote-center external infra — owned by THIS source, by network.
|
|
70
|
-
* Mirrors `
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* appends the path.
|
|
78
|
+
* Mirrors `LAZER_INFRA` (rules/pyth-lazer-rule.ts): per-network constants for
|
|
79
|
+
* infrastructure the source's operator runs, co-located with the only rule
|
|
80
|
+
* that reads them — no other oracle source ever touches a quote-center
|
|
81
|
+
* endpoint. Public read (no auth), so there is no api_key. `endpoint` has no
|
|
82
|
+
* trailing slash — the rule appends the path.
|
|
76
83
|
*
|
|
77
84
|
* These are the DEFAULTS behind the caller's `client.waterx` access slice
|
|
78
85
|
* (`waterxEndpoint` / `waterxFetch` create options) — the browser-CORS proxy
|
|
@@ -85,34 +92,83 @@ exports.WATERX_INFRA = {
|
|
|
85
92
|
/**
|
|
86
93
|
* The waterx source's quote-center base for `network` — the ONE accessor
|
|
87
94
|
* consumers (BE/FE read planes) use when, and only when, their own
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* source's own configuration — never this one.
|
|
95
|
+
* the config wires `waterx_rule`. Under any other source the
|
|
96
|
+
* read endpoint is that source's own configuration — never this one.
|
|
91
97
|
*/
|
|
92
98
|
function waterxQuoteCenterEndpoint(network) {
|
|
93
99
|
return exports.WATERX_INFRA[network].endpoint;
|
|
94
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Off-chain mirror of the on-chain `waterx_rule` `FeedConfig.max_age` DEFAULT
|
|
103
|
+
* (90s): a price older than this ABSTAINS on-chain, so a read plane serving it
|
|
104
|
+
* as live would show a price no trade could execute against. The single
|
|
105
|
+
* source of truth for consumers' post-cache freshness filters — import this,
|
|
106
|
+
* never re-declare the number. (A deployment that overrides `max_age`
|
|
107
|
+
* per-feed on-chain diverges from this mirror; none does today.)
|
|
108
|
+
*/
|
|
109
|
+
exports.WATERX_MAX_PRICE_AGE_MS = 90_000;
|
|
110
|
+
/**
|
|
111
|
+
* `true` iff a quote-center read entry is still within
|
|
112
|
+
* {@link WATERX_MAX_PRICE_AGE_MS} of `nowMs` — the freshness predicate
|
|
113
|
+
* consumers apply to `readQuoteCenterPrices` output (post-cache), matching
|
|
114
|
+
* the on-chain abstain boundary instead of each inventing a policy.
|
|
115
|
+
*/
|
|
116
|
+
function isFreshWaterxEntry(entry, nowMs) {
|
|
117
|
+
const age = nowMs - entry.publishTimeMs;
|
|
118
|
+
// A FUTURE timestamp is not fresh — it is a broken clock or a malformed
|
|
119
|
+
// payload, and a bare `age <= MAX` treats it as the freshest possible price.
|
|
120
|
+
// One tolerance of clock skew is allowed in the other direction, since the
|
|
121
|
+
// quote-center and the caller keep independent clocks; beyond that, reject.
|
|
122
|
+
if (age < -exports.WATERX_CLOCK_SKEW_TOLERANCE_MS)
|
|
123
|
+
return false;
|
|
124
|
+
return age <= exports.WATERX_MAX_PRICE_AGE_MS;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* How far ahead of the reader's clock a quote-center timestamp may sit before
|
|
128
|
+
* it is treated as broken rather than merely skewed. Two independent clocks
|
|
129
|
+
* drift; a price minutes in the future does not.
|
|
130
|
+
*/
|
|
131
|
+
exports.WATERX_CLOCK_SKEW_TOLERANCE_MS = 5_000;
|
|
132
|
+
/**
|
|
133
|
+
* Structural gate for the ENVELOPE payload shape — symmetric with
|
|
134
|
+
* {@link isWaterxLeafPayloadShape}, which validates every leaf.
|
|
135
|
+
*
|
|
136
|
+
* This runs on payloads that never went through {@link parseSignedEnvelope}:
|
|
137
|
+
* a consumer's `UpdateDataProvider` hands back whatever its cache holds, and a
|
|
138
|
+
* value revived from JSON can have lost its bigints or been written half-built.
|
|
139
|
+
* A gate that checked only `signature: string` + `Array.isArray(items)` let
|
|
140
|
+
* those through, and the failure landed mid-PTB-build inside `newItemArg` —
|
|
141
|
+
* after legs were already appended to the caller's `tx`. Every item is checked
|
|
142
|
+
* with the SAME guard the wire door uses.
|
|
143
|
+
*/
|
|
95
144
|
function isWaterxEnvelopePayloadShape(payload) {
|
|
96
145
|
const env = payload?.envelope;
|
|
97
146
|
return (typeof env === "object" &&
|
|
98
147
|
env !== null &&
|
|
99
|
-
|
|
148
|
+
// ed25519 over the batch — a wrong-length signature is an on-chain abort.
|
|
149
|
+
isHexOfBytes(env.signature, 64) &&
|
|
100
150
|
typeof env.timestamp_ms === "bigint" &&
|
|
101
|
-
|
|
151
|
+
env.timestamp_ms >= 0n &&
|
|
152
|
+
Array.isArray(env.payload?.items) &&
|
|
153
|
+
env.payload.items.every(isBatchItemShape));
|
|
102
154
|
}
|
|
103
155
|
function isWaterxLeafPayloadShape(payload) {
|
|
104
156
|
const leaves = payload?.leaves;
|
|
105
157
|
return Array.isArray(leaves) && leaves.every(isSignedLeafShape);
|
|
106
158
|
}
|
|
107
159
|
/** Every u64 field of a leaf — each one is signed, so each must be present and exact. */
|
|
108
|
-
|
|
160
|
+
/**
|
|
161
|
+
* The signed u64 fields every quote-center ITEM carries — the BCS bytes the
|
|
162
|
+
* enclave signed over. A leaf adds `signed_timestamp_ms` on top (see
|
|
163
|
+
* {@link isSignedLeafShape}); the batch envelope's items do not carry it.
|
|
164
|
+
*/
|
|
165
|
+
const BATCH_ITEM_U64_FIELDS = [
|
|
109
166
|
"price_timestamp_ms",
|
|
110
167
|
"price_n",
|
|
111
168
|
"price_scale",
|
|
112
169
|
"confidence_n",
|
|
113
170
|
"confidence_scale",
|
|
114
171
|
"max_source_deviation_bps",
|
|
115
|
-
"signed_timestamp_ms",
|
|
116
172
|
];
|
|
117
173
|
/**
|
|
118
174
|
* FULL structural check on a leaf, not just the fields the feed leg happens to
|
|
@@ -141,8 +197,15 @@ function isHexOfBytes(hex, bytes) {
|
|
|
141
197
|
const body = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
142
198
|
return body.length === bytes * 2 && HEX_ONLY.test(body);
|
|
143
199
|
}
|
|
144
|
-
|
|
145
|
-
|
|
200
|
+
/**
|
|
201
|
+
* The ITEM half — every field the feed leg rebuilds into the BCS the enclave
|
|
202
|
+
* signed. Shared by BOTH wire doors: a leaf is an item plus its signature and
|
|
203
|
+
* proof material ({@link isSignedLeafShape}), and the batch envelope's items go
|
|
204
|
+
* through the same gate ({@link assertBatchItemShape}). One definition, so a
|
|
205
|
+
* new signed field cannot be validated at one door and waved through the other.
|
|
206
|
+
*/
|
|
207
|
+
function isBatchItemShape(item) {
|
|
208
|
+
const l = item;
|
|
146
209
|
if (typeof l !== "object" || l === null)
|
|
147
210
|
return false;
|
|
148
211
|
if (typeof l.symbol !== "string" || l.symbol === "")
|
|
@@ -173,11 +236,20 @@ function isSignedLeafShape(leaf) {
|
|
|
173
236
|
return false;
|
|
174
237
|
if (l.sources.some((s) => typeof s !== "bigint" || s < 0n))
|
|
175
238
|
return false;
|
|
176
|
-
for (const field of
|
|
239
|
+
for (const field of BATCH_ITEM_U64_FIELDS) {
|
|
177
240
|
const v = l[field];
|
|
178
241
|
if (typeof v !== "bigint" || v < 0n)
|
|
179
242
|
return false;
|
|
180
243
|
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
function isSignedLeafShape(leaf) {
|
|
247
|
+
if (!isBatchItemShape(leaf))
|
|
248
|
+
return false;
|
|
249
|
+
const l = leaf;
|
|
250
|
+
const signedAt = l.signed_timestamp_ms;
|
|
251
|
+
if (typeof signedAt !== "bigint" || signedAt < 0n)
|
|
252
|
+
return false;
|
|
181
253
|
// ed25519 is always 64 bytes and the root is always a 32-byte keccak256; a
|
|
182
254
|
// wrong-length signature is an on-chain abort, so it is rejected here.
|
|
183
255
|
if (!isHexOfBytes(l.signature, 64))
|
|
@@ -253,15 +325,42 @@ function parseSignedEnvelope(text) {
|
|
|
253
325
|
if (typeof raw.signature !== "string" || !Array.isArray(raw.payload?.items)) {
|
|
254
326
|
throw new Error("WaterX quote-center returned a malformed signed envelope");
|
|
255
327
|
}
|
|
328
|
+
// `timestamp_ms` is signed OVER, so defaulting a missing one to `0n` does not
|
|
329
|
+
// produce a harmless zero — it produces a batch whose signature cannot verify,
|
|
330
|
+
// surfacing as an opaque on-chain `EInvalidSignature` indistinguishable from a
|
|
331
|
+
// forgery. Reject it at the door instead, where the message can say what is
|
|
332
|
+
// actually wrong.
|
|
333
|
+
if (typeof raw.timestamp_ms !== "bigint") {
|
|
334
|
+
throw new Error("WaterX quote-center envelope is missing an integer timestamp_ms");
|
|
335
|
+
}
|
|
336
|
+
const items = raw.payload.items.map((item) => {
|
|
337
|
+
// The leaf door is structurally validated; this one was not, so a 200 with
|
|
338
|
+
// half-built items passed the wire gate and blew up mid-PTB-build inside
|
|
339
|
+
// `newItemArg` — after commands had already been appended to the caller's
|
|
340
|
+
// transaction. Both doors now reject before anything is built.
|
|
341
|
+
assertBatchItemShape(item);
|
|
342
|
+
return { ...item, num_sources: Number(item.num_sources) };
|
|
343
|
+
});
|
|
256
344
|
return {
|
|
257
345
|
intent: Number(raw.intent),
|
|
258
|
-
timestamp_ms:
|
|
346
|
+
timestamp_ms: raw.timestamp_ms,
|
|
259
347
|
signature: raw.signature,
|
|
260
|
-
payload: {
|
|
261
|
-
items: raw.payload.items.map((i) => ({ ...i, num_sources: Number(i.num_sources) })),
|
|
262
|
-
},
|
|
348
|
+
payload: { items },
|
|
263
349
|
};
|
|
264
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* The envelope door's throwing form of {@link isBatchItemShape} — same gate the
|
|
353
|
+
* leaf door applies, so neither shape can carry a half-built item into a PTB.
|
|
354
|
+
*/
|
|
355
|
+
function assertBatchItemShape(item) {
|
|
356
|
+
if (!isBatchItemShape(item)) {
|
|
357
|
+
const symbol = item?.symbol;
|
|
358
|
+
const label = typeof symbol === "string" ? symbol : "<unknown symbol>";
|
|
359
|
+
throw new Error(`WaterX quote-center envelope item ${label} is malformed — expected symbol, ticker, ` +
|
|
360
|
+
`method, a u8 num_sources, a non-empty u64 sources vector, and integer ` +
|
|
361
|
+
`${BATCH_ITEM_U64_FIELDS.join(", ")}.`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
265
364
|
/**
|
|
266
365
|
* A Merkle proof element must be a 32-byte keccak256 hash, and nothing else.
|
|
267
366
|
*
|
|
@@ -356,13 +455,27 @@ async function fetchQuoteCenter(endpoint, path, symbols, what, fetchOpts) {
|
|
|
356
455
|
}
|
|
357
456
|
}
|
|
358
457
|
/**
|
|
359
|
-
* Pull one enclave-signed batch envelope covering `symbols
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
458
|
+
* Pull one enclave-signed batch envelope covering `symbols` — the fallback
|
|
459
|
+
* update shape AND the read executor's transport
|
|
460
|
+
* (`readQuoteCenterPrices` in `../read-prices.ts`). Public seam (WL-2345):
|
|
461
|
+
* consumers that need the raw envelope (BE prefetch caches, read planes)
|
|
462
|
+
* call this instead of re-rolling the fetch + intent/shape gate.
|
|
463
|
+
* `fellBackFrom`, when set, names the leaf-route failure that sent us here,
|
|
464
|
+
* so a deployment whose quote-center serves NEITHER route reports both
|
|
465
|
+
* statuses instead of only the second one.
|
|
363
466
|
*/
|
|
364
467
|
async function fetchWaterxSignedUpdate(endpoint, symbols, fetchOpts, fellBackFrom) {
|
|
365
468
|
const context = fellBackFrom ? ` (fell back from ${fellBackFrom})` : "";
|
|
469
|
+
// Unlike leaves, this route cannot be chunked: the response is ONE signature
|
|
470
|
+
// over the whole batch, so two envelopes are two different snapshots and the
|
|
471
|
+
// payload shape holds one. Say so explicitly rather than let the enclave
|
|
472
|
+
// answer a non-retryable 400 that reads as a generic fetch failure.
|
|
473
|
+
if (symbols.length > exports.WATERX_MAX_BATCH_SYMBOLS) {
|
|
474
|
+
throw new Error(`WaterX quote-center batch fetch needs ${String(symbols.length)} symbols but the enclave ` +
|
|
475
|
+
`signs at most ${String(exports.WATERX_MAX_BATCH_SYMBOLS)} per request, and a batch envelope ` +
|
|
476
|
+
`cannot be split (one signature covers the whole batch). Request fewer tickers, or use ` +
|
|
477
|
+
`a quote-center that serves the per-symbol leaf route, which IS chunked.${context}`);
|
|
478
|
+
}
|
|
366
479
|
const res = await fetchQuoteCenter(endpoint, "v1/quotes/update", symbols, "fetch", fetchOpts);
|
|
367
480
|
if (!res.ok) {
|
|
368
481
|
throw new Error(`WaterX quote-center fetch failed: ${res.status} ${await res.text()}${context}`);
|
|
@@ -397,8 +510,48 @@ async function fetchWaterxSignedUpdate(endpoint, symbols, fetchOpts, fellBackFro
|
|
|
397
510
|
* from its feed registry). That is config drift between this SDK's `feeds` and
|
|
398
511
|
* the quote-center's registry, and the fallback surfaces it honestly: the
|
|
399
512
|
* envelope route 404s on the same symbol, and its error names both attempts.
|
|
513
|
+
*
|
|
514
|
+
* Public seam (WL-2345): consumers holding per-symbol leaves (BE prefetch
|
|
515
|
+
* caches) pull through this instead of re-rolling the fetch + parse gate.
|
|
400
516
|
*/
|
|
401
517
|
async function fetchWaterxSignedLeaves(endpoint, symbols, fetchOpts) {
|
|
518
|
+
// Chunked against the enclave's per-request cap. Leaves are independently
|
|
519
|
+
// verifiable per symbol — each carries its own proof against a signed root —
|
|
520
|
+
// so splitting the request changes nothing about what the PTB can do with
|
|
521
|
+
// them. Unchunked, a universe prefetch or an all-markets refresh takes a
|
|
522
|
+
// non-retryable 400 the moment a deployment crosses the cap, surfacing as a
|
|
523
|
+
// bare "leaf fetch failed: 400" with nothing pointing at batch size.
|
|
524
|
+
const chunks = chunkSymbols(symbols);
|
|
525
|
+
// Probe with the first chunk: a quote-center with no leaf route answers 404
|
|
526
|
+
// for every chunk, so there is no point spending the rest to learn it.
|
|
527
|
+
const first = await fetchLeafChunk(endpoint, chunks[0] ?? [], fetchOpts);
|
|
528
|
+
if ("unavailable" in first || chunks.length <= 1)
|
|
529
|
+
return first;
|
|
530
|
+
const rest = await Promise.all(chunks.slice(1).map((chunk) => fetchLeafChunk(endpoint, chunk, fetchOpts)));
|
|
531
|
+
const leaves = [...first.leaves];
|
|
532
|
+
for (const pull of rest) {
|
|
533
|
+
if ("unavailable" in pull)
|
|
534
|
+
return pull;
|
|
535
|
+
leaves.push(...pull.leaves);
|
|
536
|
+
}
|
|
537
|
+
return { leaves };
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* The quote-center enclave signs at most this many symbols per request
|
|
541
|
+
* (`MAX_BATCH_SIZE` in `quote-service`). Over the cap it answers a
|
|
542
|
+
* non-retryable 400.
|
|
543
|
+
*/
|
|
544
|
+
exports.WATERX_MAX_BATCH_SYMBOLS = 32;
|
|
545
|
+
function chunkSymbols(symbols) {
|
|
546
|
+
if (symbols.length <= exports.WATERX_MAX_BATCH_SYMBOLS)
|
|
547
|
+
return [symbols];
|
|
548
|
+
const out = [];
|
|
549
|
+
for (let i = 0; i < symbols.length; i += exports.WATERX_MAX_BATCH_SYMBOLS) {
|
|
550
|
+
out.push(symbols.slice(i, i + exports.WATERX_MAX_BATCH_SYMBOLS));
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
async function fetchLeafChunk(endpoint, symbols, fetchOpts) {
|
|
402
555
|
const res = await fetchQuoteCenter(endpoint, "v1/quotes/leaves", symbols, "leaf fetch", fetchOpts);
|
|
403
556
|
if (res.status === 404) {
|
|
404
557
|
return { unavailable: `GET /v1/quotes/leaves → 404 ${(await res.text()).trim()}`.trim() };
|
|
@@ -557,44 +710,103 @@ function feedWaterxRule(tx, host, collector, envelope) {
|
|
|
557
710
|
},
|
|
558
711
|
})(tx);
|
|
559
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* THE quote-center route ladder, owned by the rule that owns the protocol:
|
|
715
|
+
* pull per-symbol Merkle leaves (the default), and fall back to one batch
|
|
716
|
+
* envelope only when this quote-center has no leaf route (see
|
|
717
|
+
* {@link fetchWaterxSignedLeaves} for exactly which statuses mean that, and
|
|
718
|
+
* why nothing else falls back).
|
|
719
|
+
*
|
|
720
|
+
* Both the write path ({@link pullWaterxData}, which layers coverage policy on
|
|
721
|
+
* top) and the READ executor (`readQuoteCenterPrices` in `../read-prices.ts`,
|
|
722
|
+
* which only decodes prices) go through here, so which route wins, which
|
|
723
|
+
* status falls back, and how the fallback context is threaded are stated once.
|
|
724
|
+
* `items` is the flat symbol-bearing view both callers actually want —
|
|
725
|
+
* `WaterxSignedLeaf extends WaterxBatchItem`, so leaves widen to it for free —
|
|
726
|
+
* while `payload` keeps the shape-specific form the on-chain feed leg needs.
|
|
727
|
+
*/
|
|
728
|
+
async function pullWaterxQuotes(endpoint, symbols, fetchOpts) {
|
|
729
|
+
const pulled = await fetchWaterxSignedLeaves(endpoint, symbols, fetchOpts);
|
|
730
|
+
if ("leaves" in pulled) {
|
|
731
|
+
return { route: "leaves", payload: { leaves: pulled.leaves }, items: pulled.leaves };
|
|
732
|
+
}
|
|
733
|
+
const envelope = await fetchWaterxSignedUpdate(endpoint, symbols, fetchOpts, pulled.unavailable);
|
|
734
|
+
return { route: "envelope", payload: { envelope }, items: envelope.payload.items };
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* THE quote-center pull — the one pipeline both coverage policies share:
|
|
738
|
+
* package guard → own-key feeds partition → leaf route (default) → batch
|
|
739
|
+
* envelope only when this quote-center has no leaf route (see
|
|
740
|
+
* {@link fetchWaterxSignedLeaves} for exactly which statuses mean that, and
|
|
741
|
+
* why nothing else falls back).
|
|
742
|
+
*
|
|
743
|
+
* `coverage` decides only what a GAP means, never how the pull runs:
|
|
744
|
+
*
|
|
745
|
+
* - `"strict"` — every requested ticker must be config-listed AND served, or
|
|
746
|
+
* this THROWS (the per-ticker unlisted message, before any network call;
|
|
747
|
+
* then {@link assertCoverage} naming the route). `missing` is always `[]`.
|
|
748
|
+
* - `"partial"` — an unlisted or unserved ticker lands in `missing` and the
|
|
749
|
+
* payload covers the rest, narrowed through the rule's own
|
|
750
|
+
* {@link WaterxRule.narrowUpdateData} so leaf-vs-envelope divisibility has
|
|
751
|
+
* exactly one definition.
|
|
752
|
+
*/
|
|
753
|
+
async function pullWaterxData(host, tickers, coverage) {
|
|
754
|
+
// Package-level check first: a config without the deployment must say so,
|
|
755
|
+
// not fail per ticker as if only that feed were missing.
|
|
756
|
+
const { feeds } = requireWaterxPackage(host);
|
|
757
|
+
// One partition pass, own-keys-only: a prototype-key ticker ("toString")
|
|
758
|
+
// must read as unlisted, not pass as an inherited Function and reach the
|
|
759
|
+
// network.
|
|
760
|
+
const missing = [];
|
|
761
|
+
const listed = [];
|
|
762
|
+
for (const ticker of tickers) {
|
|
763
|
+
((0, record_ts_1.ownEntry)(feeds, ticker) === undefined ? missing : listed).push(ticker);
|
|
764
|
+
}
|
|
765
|
+
// Unlisted tickers never reach the network on EITHER policy — the
|
|
766
|
+
// quote-center 404s a whole batch on one unknown symbol. Strict surfaces
|
|
767
|
+
// the per-ticker message; partial just records the gap and pulls the rest.
|
|
768
|
+
if (coverage === "strict" && missing.length > 0) {
|
|
769
|
+
throw new Error(`No waterx_rule feed listed for ticker: ${missing[0]}`);
|
|
770
|
+
}
|
|
771
|
+
if (listed.length === 0)
|
|
772
|
+
return { data: null, missing };
|
|
773
|
+
const { endpoint, fetch: fetchOpts } = resolveWaterxInfra(host);
|
|
774
|
+
const { route, payload, items } = await pullWaterxQuotes(endpoint, listed, fetchOpts);
|
|
775
|
+
const served = new Set(items.map((item) => item.symbol));
|
|
776
|
+
if (coverage === "strict") {
|
|
777
|
+
assertCoverage(route, listed, served);
|
|
778
|
+
return { data: { kind: "waterx_rule", payload }, missing };
|
|
779
|
+
}
|
|
780
|
+
const covered = [];
|
|
781
|
+
for (const ticker of listed)
|
|
782
|
+
(served.has(ticker) ? covered : missing).push(ticker);
|
|
783
|
+
// Divisibility is the rule's own knowledge, so the subset decision is
|
|
784
|
+
// delegated rather than re-encoded here: leaves subset per symbol, an
|
|
785
|
+
// envelope is indivisible and passes whole (or `null` when it covers none).
|
|
786
|
+
return {
|
|
787
|
+
data: covered.length > 0
|
|
788
|
+
? exports.WaterxRule.narrowUpdateData(host, { kind: "waterx_rule", payload }, covered)
|
|
789
|
+
: null,
|
|
790
|
+
missing,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
560
793
|
exports.WaterxRule = {
|
|
561
794
|
kind: "waterx_rule",
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
requiresFeeSource: false,
|
|
795
|
+
// No credential: the quote-center read surface is public (no `credential`
|
|
796
|
+
// declared — see `PriceUpdateRule.credential`).
|
|
565
797
|
/** Tickers with a `waterx_rule.feeds` entry (keyed by oracle ticker). */
|
|
566
798
|
supportedTickers(host) {
|
|
567
799
|
return Object.keys(host.config.packages.waterx_rule?.feeds ?? {});
|
|
568
800
|
},
|
|
569
801
|
/**
|
|
570
802
|
* Pulls per-symbol Merkle leaves for `tickers`, falling back to one batch
|
|
571
|
-
* envelope only when this quote-center has no leaf route
|
|
572
|
-
*
|
|
573
|
-
* why nothing else falls back). Either way, returns only what covers ALL of
|
|
574
|
-
* `tickers` — see {@link assertCoverage}.
|
|
803
|
+
* envelope only when this quote-center has no leaf route. Returns only what
|
|
804
|
+
* covers ALL of `tickers` — the strict arm of {@link pullWaterxData}.
|
|
575
805
|
*/
|
|
576
806
|
async fetchUpdateData(host, tickers) {
|
|
577
807
|
if (tickers.length === 0)
|
|
578
808
|
return null;
|
|
579
|
-
|
|
580
|
-
// not fail per ticker as if only that feed were missing.
|
|
581
|
-
const { feeds } = requireWaterxPackage(host);
|
|
582
|
-
for (const ticker of tickers) {
|
|
583
|
-
// ownEntry: a prototype-key ticker ("toString") must throw here as
|
|
584
|
-
// unlisted, not pass as an inherited Function and reach the network.
|
|
585
|
-
if ((0, record_ts_1.ownEntry)(feeds, ticker) === undefined) {
|
|
586
|
-
throw new Error(`No waterx_rule feed listed for ticker: ${ticker}`);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
const { endpoint, fetch: fetchOpts } = resolveWaterxInfra(host);
|
|
590
|
-
const pulled = await fetchWaterxSignedLeaves(endpoint, tickers, fetchOpts);
|
|
591
|
-
if ("leaves" in pulled) {
|
|
592
|
-
assertCoverage("leaves", tickers, pulled.leaves.map((l) => l.symbol));
|
|
593
|
-
return { kind: "waterx_rule", payload: { leaves: pulled.leaves } };
|
|
594
|
-
}
|
|
595
|
-
const envelope = await fetchWaterxSignedUpdate(endpoint, tickers, fetchOpts, pulled.unavailable);
|
|
596
|
-
assertCoverage("envelope", tickers, envelope.payload.items.map((i) => i.symbol));
|
|
597
|
-
return { kind: "waterx_rule", payload: { envelope } };
|
|
809
|
+
return (await pullWaterxData(host, tickers, "strict")).data;
|
|
598
810
|
},
|
|
599
811
|
/**
|
|
600
812
|
* Divisibility differs by variant, which is the whole reason this method
|
|
@@ -637,6 +849,28 @@ exports.WaterxRule = {
|
|
|
637
849
|
}
|
|
638
850
|
return { kind: "waterx_rule", payload: { envelope } };
|
|
639
851
|
},
|
|
852
|
+
/**
|
|
853
|
+
* The on-chain F-014 single-use replay key, rule-owned (see
|
|
854
|
+
* `PriceUpdateRule.updateIdentityBySymbol`): each leaf's identity is its
|
|
855
|
+
* OWN `signed_timestamp_ms`; an envelope's one `timestamp_ms` is the
|
|
856
|
+
* identity of EVERY symbol it covers (one batch signature ⇒ one submission
|
|
857
|
+
* burns the mark for all of them). A consumer's serve-at-most-once cache
|
|
858
|
+
* keys off this map for BOTH payload shapes — including the leaf-first
|
|
859
|
+
* default an envelope-only identity check would miss.
|
|
860
|
+
*/
|
|
861
|
+
updateIdentityBySymbol(data) {
|
|
862
|
+
// Narrowed ONCE: a serve-at-most-once cache calls this per serve, and the
|
|
863
|
+
// accessors each re-run the full structural payload validation.
|
|
864
|
+
const payload = waterxPayloadOf(data);
|
|
865
|
+
if (!payload)
|
|
866
|
+
return null;
|
|
867
|
+
return "leaves" in payload
|
|
868
|
+
? new Map(payload.leaves.map((leaf) => [leaf.symbol, leaf.signed_timestamp_ms]))
|
|
869
|
+
: new Map(payload.envelope.payload.items.map((item) => [
|
|
870
|
+
item.symbol,
|
|
871
|
+
payload.envelope.timestamp_ms,
|
|
872
|
+
]));
|
|
873
|
+
},
|
|
640
874
|
/**
|
|
641
875
|
* No shared verify step: both `waterx_rule` collect entries bundle verify AND
|
|
642
876
|
* feed into one per-collector call, appended by
|
|
@@ -645,7 +879,36 @@ exports.WaterxRule = {
|
|
|
645
879
|
* reaches the feed leg via `aggregate.ts`'s per-ticker map (built from the
|
|
646
880
|
* group's fetched data), not a `RuleUpdateHandle`.
|
|
647
881
|
*/
|
|
648
|
-
buildUpdateCalls(_tx, _host, _data
|
|
882
|
+
buildUpdateCalls(_tx, _host, _data) {
|
|
649
883
|
return;
|
|
650
884
|
},
|
|
651
885
|
};
|
|
886
|
+
/**
|
|
887
|
+
* Coverage-policy seam over the rule's quote-center pull (WL-2345): fetch
|
|
888
|
+
* signed waterx update data for `tickers` with the caller choosing what a
|
|
889
|
+
* coverage gap means.
|
|
890
|
+
*
|
|
891
|
+
* - `coverage: "strict"` (default) — exactly `WaterxRule.fetchUpdateData`:
|
|
892
|
+
* every requested ticker must be config-listed AND served, or the fetch
|
|
893
|
+
* THROWS (`assertCoverage`); `missing` is always `[]`. Trade-path semantics
|
|
894
|
+
* — `refreshOraclePrices` keeps consuming the rule's own strict fetch, so
|
|
895
|
+
* `aggregate.ts`'s uncarried-ticker throw (04117a1) still can't be reached
|
|
896
|
+
* by a payload that under-covers its group.
|
|
897
|
+
* - `coverage: "partial"` — universe-prefetch semantics (a BE cache warming
|
|
898
|
+
* every known ticker at once): a ticker with no `waterx_rule.feeds` entry,
|
|
899
|
+
* or one the quote-center response does not serve, lands in `missing`
|
|
900
|
+
* instead of throwing, and `data` covers the rest. On the leaf route the
|
|
901
|
+
* payload is the covering leaf SUBSET; on the envelope route the envelope
|
|
902
|
+
* is kept iff it covers ≥1 requested ticker (it is indivisible — an
|
|
903
|
+
* envelope serving none is `data: null`). `data: null` + all-missing when
|
|
904
|
+
* nothing is servable.
|
|
905
|
+
*
|
|
906
|
+
* Consumers must not hand a partial payload to a build for tickers in
|
|
907
|
+
* `missing` — those tickers are simply not servable by waterx right now (log
|
|
908
|
+
* the gap; the chain's weight tables decide whether that starves anything).
|
|
909
|
+
*/
|
|
910
|
+
async function fetchWaterxUpdateData(host, tickers, opts) {
|
|
911
|
+
if (tickers.length === 0)
|
|
912
|
+
return { data: null, missing: [] };
|
|
913
|
+
return pullWaterxData(host, tickers, opts?.coverage ?? "strict");
|
|
914
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `schedule.ts` — Pyth `attributes.schedule` grammar parser + the pure
|
|
3
|
+
* market-status walker (WL-2345): the ONE implementation both consumers fold
|
|
4
|
+
* onto (the FE fallback `/api/markets` route and the BE
|
|
5
|
+
* `PythScheduleService` / markets service carried drifting twins).
|
|
6
|
+
*
|
|
7
|
+
* Parser grammar (https://docs.pyth.network/price-feeds/market-hours):
|
|
8
|
+
*
|
|
9
|
+
* {IANA_timezone};{Mon},{Tue},{Wed},{Thu},{Fri},{Sat},{Sun};{holidays_csv}
|
|
10
|
+
*
|
|
11
|
+
* Each weekday slot is `Open`/`O`/`open`, `Closed`/`C`/`closed`, one
|
|
12
|
+
* `HHMM-HHMM` range, `&`-joined ranges (the new multi-session encoding), or
|
|
13
|
+
* comma-joined ranges (the old encoding — disambiguated from the weekday
|
|
14
|
+
* separator by lookahead). Holidays are `MMDD` (old) or `MMDD/C` (new; a
|
|
15
|
+
* `MMDD/HHMM-HHMM` modified-hours entry is NOT a closure and is skipped, as
|
|
16
|
+
* are non-MMDD sentinels like Pyth's `"0"`). This is the reconciled SUPERSET
|
|
17
|
+
* of the two prior ports — accepting either era's tokens is what lets one
|
|
18
|
+
* parser serve the `v1/symbols` catalog and any cached Hermes-era strings.
|
|
19
|
+
*
|
|
20
|
+
* Pyth weekday order is Mon=0..Sun=6; `TradingHours.days` uses ISO-ish
|
|
21
|
+
* Sun=0..Sat=6 (converted in `groupIntoSessions`).
|
|
22
|
+
*
|
|
23
|
+
* The schedule catalog itself comes from `fetchPythSymbolCatalog`
|
|
24
|
+
* (`symbol-catalog.ts`); pairing a record's `schedule` with this parser and
|
|
25
|
+
* `getMarketStatus` is the whole market-hours pipeline.
|
|
26
|
+
*/
|
|
27
|
+
export interface TradingSession {
|
|
28
|
+
/** HH:MM (24h) open time in the market's timezone */
|
|
29
|
+
open: string;
|
|
30
|
+
/** HH:MM (24h) close time — may be next day (e.g. "17:00" Sunday open to "17:00" Friday) */
|
|
31
|
+
close: string;
|
|
32
|
+
/** ISO day-of-week: 0=Sunday … 6=Saturday */
|
|
33
|
+
days: number[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Year-agnostic `(month, day)` pair, matching Pyth's `MMDD` holiday encoding.
|
|
37
|
+
* The same `{month, day}` matches in *every* year, so moving holidays
|
|
38
|
+
* (Good Friday, MLK Day, Thanksgiving, etc.) need to be republished by Pyth
|
|
39
|
+
* each year and picked up by the consumer's next catalog refresh.
|
|
40
|
+
*/
|
|
41
|
+
export interface HolidayDate {
|
|
42
|
+
/** 1..12, in the schedule's timezone */
|
|
43
|
+
month: number;
|
|
44
|
+
/** 1..31, in the schedule's timezone */
|
|
45
|
+
day: number;
|
|
46
|
+
/**
|
|
47
|
+
* MODIFIED hours for this date, in place of the weekly schedule — an early
|
|
48
|
+
* close (`1224/0930-1300`) or a split session (`1224/0000-1430&1800-2400`).
|
|
49
|
+
* Absent means a FULL closure, the `MMDD/C` form.
|
|
50
|
+
*
|
|
51
|
+
* These were previously discarded, which meant the venue fell back to its
|
|
52
|
+
* NORMAL weekly hours on an early-close day and reported itself open after it
|
|
53
|
+
* had shut. They are not an edge case: the live Pyth catalog carries
|
|
54
|
+
* thousands (`0930-1300` alone appears ~2.5k times).
|
|
55
|
+
*/
|
|
56
|
+
sessions?: {
|
|
57
|
+
open: string;
|
|
58
|
+
close: string;
|
|
59
|
+
}[];
|
|
60
|
+
}
|
|
61
|
+
export interface TradingHours {
|
|
62
|
+
/** IANA timezone identifier, e.g. "America/New_York" */
|
|
63
|
+
timezone: string;
|
|
64
|
+
sessions: TradingSession[];
|
|
65
|
+
/**
|
|
66
|
+
* Days when the venue is fully closed regardless of the weekly schedule.
|
|
67
|
+
* Optional — many feeds (crypto, FX) have none. Encoded as `(month, day)`
|
|
68
|
+
* in `timezone`'s local calendar, matching Pyth's `attributes.schedule`
|
|
69
|
+
* holiday list.
|
|
70
|
+
*/
|
|
71
|
+
holidays?: HolidayDate[];
|
|
72
|
+
}
|
|
73
|
+
export interface ParsedPythSchedule {
|
|
74
|
+
tradingHours: TradingHours;
|
|
75
|
+
/** True iff every weekday is fully open and there are no holidays — callers treat as `tradingHours = null` (24/7, no schedule needed). */
|
|
76
|
+
alwaysOpen: boolean;
|
|
77
|
+
}
|
|
78
|
+
export declare class PythScheduleParseError extends Error {
|
|
79
|
+
constructor(message: string);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse a Pyth schedule string. Throws {@link PythScheduleParseError} on
|
|
83
|
+
* malformed input — callers guard with try/catch so one bad feed does not break
|
|
84
|
+
* a whole market list.
|
|
85
|
+
*
|
|
86
|
+
* DEGRADE CLOSED, not open. The obvious fallback — treat an unparseable
|
|
87
|
+
* schedule as "no schedule (24/7)" — is the wrong direction: the shapes that
|
|
88
|
+
* actually fail here are venues WITH sessions (a lunch-break equity schedule in
|
|
89
|
+
* the legacy comma encoding, an ambiguous weekday fold), and calling those 24/7
|
|
90
|
+
* reports a closed venue as tradable. That is the failure class this module
|
|
91
|
+
* exists to prevent. Prefer marking the market unavailable, or reusing the last
|
|
92
|
+
* schedule that parsed; use 24/7 only where a wrong "open" is harmless.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parsePythSchedule(input: string): ParsedPythSchedule;
|
|
95
|
+
export interface MarketStatusResult {
|
|
96
|
+
status: "open" | "closed" | "paused";
|
|
97
|
+
/** Milliseconds until next status change; null for 24/7 or paused. */
|
|
98
|
+
nextStatusChangeIn: number | null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Pure function — calculates market open/closed status from trading hours + now.
|
|
102
|
+
*
|
|
103
|
+
* @param tradingHours null means 24/7 (crypto)
|
|
104
|
+
* @param paused forces `paused`, short-circuiting the schedule. Checked
|
|
105
|
+
* BEFORE the 24/7 branch, so an on-chain-paused crypto
|
|
106
|
+
* market (tradingHours === null) still reports paused.
|
|
107
|
+
* @param now injectable for testing; defaults to new Date()
|
|
108
|
+
*/
|
|
109
|
+
export declare function getMarketStatus(tradingHours: TradingHours | null, paused: boolean, now?: Date): MarketStatusResult;
|