@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
|
@@ -41,9 +41,12 @@ import { collectBatchLatest, collectSingleWithProof, newBatchItem, newBatchPaylo
|
|
|
41
41
|
import { ownEntry } from "../../utils/record.js";
|
|
42
42
|
import { assertRuleUpdateData, } from "../price-update-rule.js";
|
|
43
43
|
import { FetchPolicyError, fetchWithPolicy, joinEndpointPath, } from "../update-fetch.js";
|
|
44
|
-
/** Intent the quote-center signs a whole BATCH payload under
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
/** Intent the quote-center signs a whole BATCH payload under. Exported to NAME
|
|
45
|
+
* the signing scheme only — consumers no longer mirror the intent gate
|
|
46
|
+
* themselves: every quote-center pull (tx-build fetch, read executor, BE
|
|
47
|
+
* prefetch) goes through {@link fetchWaterxSignedUpdate} /
|
|
48
|
+
* {@link fetchWaterxSignedLeaves}, which enforce it, so a mispointed endpoint
|
|
49
|
+
* is rejected identically on every path. */
|
|
47
50
|
export const BATCH_PRICE_INTENT = 1;
|
|
48
51
|
/**
|
|
49
52
|
* Intent the quote-center signs a snapshot's Merkle ROOT under
|
|
@@ -57,12 +60,11 @@ export const BATCH_PRICE_INTENT = 1;
|
|
|
57
60
|
export const MERKLE_ROOT_INTENT = 2;
|
|
58
61
|
/**
|
|
59
62
|
* WaterX quote-center external infra — owned by THIS source, by network.
|
|
60
|
-
* Mirrors `
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* appends the path.
|
|
63
|
+
* Mirrors `LAZER_INFRA` (rules/pyth-lazer-rule.ts): per-network constants for
|
|
64
|
+
* infrastructure the source's operator runs, co-located with the only rule
|
|
65
|
+
* that reads them — no other oracle source ever touches a quote-center
|
|
66
|
+
* endpoint. Public read (no auth), so there is no api_key. `endpoint` has no
|
|
67
|
+
* trailing slash — the rule appends the path.
|
|
66
68
|
*
|
|
67
69
|
* These are the DEFAULTS behind the caller's `client.waterx` access slice
|
|
68
70
|
* (`waterxEndpoint` / `waterxFetch` create options) — the browser-CORS proxy
|
|
@@ -75,34 +77,83 @@ export const WATERX_INFRA = {
|
|
|
75
77
|
/**
|
|
76
78
|
* The waterx source's quote-center base for `network` — the ONE accessor
|
|
77
79
|
* consumers (BE/FE read planes) use when, and only when, their own
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* source's own configuration — never this one.
|
|
80
|
+
* the config wires `waterx_rule`. Under any other source the
|
|
81
|
+
* read endpoint is that source's own configuration — never this one.
|
|
81
82
|
*/
|
|
82
83
|
export function waterxQuoteCenterEndpoint(network) {
|
|
83
84
|
return WATERX_INFRA[network].endpoint;
|
|
84
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Off-chain mirror of the on-chain `waterx_rule` `FeedConfig.max_age` DEFAULT
|
|
88
|
+
* (90s): a price older than this ABSTAINS on-chain, so a read plane serving it
|
|
89
|
+
* as live would show a price no trade could execute against. The single
|
|
90
|
+
* source of truth for consumers' post-cache freshness filters — import this,
|
|
91
|
+
* never re-declare the number. (A deployment that overrides `max_age`
|
|
92
|
+
* per-feed on-chain diverges from this mirror; none does today.)
|
|
93
|
+
*/
|
|
94
|
+
export const WATERX_MAX_PRICE_AGE_MS = 90_000;
|
|
95
|
+
/**
|
|
96
|
+
* `true` iff a quote-center read entry is still within
|
|
97
|
+
* {@link WATERX_MAX_PRICE_AGE_MS} of `nowMs` — the freshness predicate
|
|
98
|
+
* consumers apply to `readQuoteCenterPrices` output (post-cache), matching
|
|
99
|
+
* the on-chain abstain boundary instead of each inventing a policy.
|
|
100
|
+
*/
|
|
101
|
+
export function isFreshWaterxEntry(entry, nowMs) {
|
|
102
|
+
const age = nowMs - entry.publishTimeMs;
|
|
103
|
+
// A FUTURE timestamp is not fresh — it is a broken clock or a malformed
|
|
104
|
+
// payload, and a bare `age <= MAX` treats it as the freshest possible price.
|
|
105
|
+
// One tolerance of clock skew is allowed in the other direction, since the
|
|
106
|
+
// quote-center and the caller keep independent clocks; beyond that, reject.
|
|
107
|
+
if (age < -WATERX_CLOCK_SKEW_TOLERANCE_MS)
|
|
108
|
+
return false;
|
|
109
|
+
return age <= WATERX_MAX_PRICE_AGE_MS;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* How far ahead of the reader's clock a quote-center timestamp may sit before
|
|
113
|
+
* it is treated as broken rather than merely skewed. Two independent clocks
|
|
114
|
+
* drift; a price minutes in the future does not.
|
|
115
|
+
*/
|
|
116
|
+
export const WATERX_CLOCK_SKEW_TOLERANCE_MS = 5_000;
|
|
117
|
+
/**
|
|
118
|
+
* Structural gate for the ENVELOPE payload shape — symmetric with
|
|
119
|
+
* {@link isWaterxLeafPayloadShape}, which validates every leaf.
|
|
120
|
+
*
|
|
121
|
+
* This runs on payloads that never went through {@link parseSignedEnvelope}:
|
|
122
|
+
* a consumer's `UpdateDataProvider` hands back whatever its cache holds, and a
|
|
123
|
+
* value revived from JSON can have lost its bigints or been written half-built.
|
|
124
|
+
* A gate that checked only `signature: string` + `Array.isArray(items)` let
|
|
125
|
+
* those through, and the failure landed mid-PTB-build inside `newItemArg` —
|
|
126
|
+
* after legs were already appended to the caller's `tx`. Every item is checked
|
|
127
|
+
* with the SAME guard the wire door uses.
|
|
128
|
+
*/
|
|
85
129
|
function isWaterxEnvelopePayloadShape(payload) {
|
|
86
130
|
const env = payload?.envelope;
|
|
87
131
|
return (typeof env === "object" &&
|
|
88
132
|
env !== null &&
|
|
89
|
-
|
|
133
|
+
// ed25519 over the batch — a wrong-length signature is an on-chain abort.
|
|
134
|
+
isHexOfBytes(env.signature, 64) &&
|
|
90
135
|
typeof env.timestamp_ms === "bigint" &&
|
|
91
|
-
|
|
136
|
+
env.timestamp_ms >= 0n &&
|
|
137
|
+
Array.isArray(env.payload?.items) &&
|
|
138
|
+
env.payload.items.every(isBatchItemShape));
|
|
92
139
|
}
|
|
93
140
|
function isWaterxLeafPayloadShape(payload) {
|
|
94
141
|
const leaves = payload?.leaves;
|
|
95
142
|
return Array.isArray(leaves) && leaves.every(isSignedLeafShape);
|
|
96
143
|
}
|
|
97
144
|
/** Every u64 field of a leaf — each one is signed, so each must be present and exact. */
|
|
98
|
-
|
|
145
|
+
/**
|
|
146
|
+
* The signed u64 fields every quote-center ITEM carries — the BCS bytes the
|
|
147
|
+
* enclave signed over. A leaf adds `signed_timestamp_ms` on top (see
|
|
148
|
+
* {@link isSignedLeafShape}); the batch envelope's items do not carry it.
|
|
149
|
+
*/
|
|
150
|
+
const BATCH_ITEM_U64_FIELDS = [
|
|
99
151
|
"price_timestamp_ms",
|
|
100
152
|
"price_n",
|
|
101
153
|
"price_scale",
|
|
102
154
|
"confidence_n",
|
|
103
155
|
"confidence_scale",
|
|
104
156
|
"max_source_deviation_bps",
|
|
105
|
-
"signed_timestamp_ms",
|
|
106
157
|
];
|
|
107
158
|
/**
|
|
108
159
|
* FULL structural check on a leaf, not just the fields the feed leg happens to
|
|
@@ -131,8 +182,15 @@ function isHexOfBytes(hex, bytes) {
|
|
|
131
182
|
const body = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
132
183
|
return body.length === bytes * 2 && HEX_ONLY.test(body);
|
|
133
184
|
}
|
|
134
|
-
|
|
135
|
-
|
|
185
|
+
/**
|
|
186
|
+
* The ITEM half — every field the feed leg rebuilds into the BCS the enclave
|
|
187
|
+
* signed. Shared by BOTH wire doors: a leaf is an item plus its signature and
|
|
188
|
+
* proof material ({@link isSignedLeafShape}), and the batch envelope's items go
|
|
189
|
+
* through the same gate ({@link assertBatchItemShape}). One definition, so a
|
|
190
|
+
* new signed field cannot be validated at one door and waved through the other.
|
|
191
|
+
*/
|
|
192
|
+
function isBatchItemShape(item) {
|
|
193
|
+
const l = item;
|
|
136
194
|
if (typeof l !== "object" || l === null)
|
|
137
195
|
return false;
|
|
138
196
|
if (typeof l.symbol !== "string" || l.symbol === "")
|
|
@@ -163,11 +221,20 @@ function isSignedLeafShape(leaf) {
|
|
|
163
221
|
return false;
|
|
164
222
|
if (l.sources.some((s) => typeof s !== "bigint" || s < 0n))
|
|
165
223
|
return false;
|
|
166
|
-
for (const field of
|
|
224
|
+
for (const field of BATCH_ITEM_U64_FIELDS) {
|
|
167
225
|
const v = l[field];
|
|
168
226
|
if (typeof v !== "bigint" || v < 0n)
|
|
169
227
|
return false;
|
|
170
228
|
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
function isSignedLeafShape(leaf) {
|
|
232
|
+
if (!isBatchItemShape(leaf))
|
|
233
|
+
return false;
|
|
234
|
+
const l = leaf;
|
|
235
|
+
const signedAt = l.signed_timestamp_ms;
|
|
236
|
+
if (typeof signedAt !== "bigint" || signedAt < 0n)
|
|
237
|
+
return false;
|
|
171
238
|
// ed25519 is always 64 bytes and the root is always a 32-byte keccak256; a
|
|
172
239
|
// wrong-length signature is an on-chain abort, so it is rejected here.
|
|
173
240
|
if (!isHexOfBytes(l.signature, 64))
|
|
@@ -243,15 +310,42 @@ export function parseSignedEnvelope(text) {
|
|
|
243
310
|
if (typeof raw.signature !== "string" || !Array.isArray(raw.payload?.items)) {
|
|
244
311
|
throw new Error("WaterX quote-center returned a malformed signed envelope");
|
|
245
312
|
}
|
|
313
|
+
// `timestamp_ms` is signed OVER, so defaulting a missing one to `0n` does not
|
|
314
|
+
// produce a harmless zero — it produces a batch whose signature cannot verify,
|
|
315
|
+
// surfacing as an opaque on-chain `EInvalidSignature` indistinguishable from a
|
|
316
|
+
// forgery. Reject it at the door instead, where the message can say what is
|
|
317
|
+
// actually wrong.
|
|
318
|
+
if (typeof raw.timestamp_ms !== "bigint") {
|
|
319
|
+
throw new Error("WaterX quote-center envelope is missing an integer timestamp_ms");
|
|
320
|
+
}
|
|
321
|
+
const items = raw.payload.items.map((item) => {
|
|
322
|
+
// The leaf door is structurally validated; this one was not, so a 200 with
|
|
323
|
+
// half-built items passed the wire gate and blew up mid-PTB-build inside
|
|
324
|
+
// `newItemArg` — after commands had already been appended to the caller's
|
|
325
|
+
// transaction. Both doors now reject before anything is built.
|
|
326
|
+
assertBatchItemShape(item);
|
|
327
|
+
return { ...item, num_sources: Number(item.num_sources) };
|
|
328
|
+
});
|
|
246
329
|
return {
|
|
247
330
|
intent: Number(raw.intent),
|
|
248
|
-
timestamp_ms:
|
|
331
|
+
timestamp_ms: raw.timestamp_ms,
|
|
249
332
|
signature: raw.signature,
|
|
250
|
-
payload: {
|
|
251
|
-
items: raw.payload.items.map((i) => ({ ...i, num_sources: Number(i.num_sources) })),
|
|
252
|
-
},
|
|
333
|
+
payload: { items },
|
|
253
334
|
};
|
|
254
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* The envelope door's throwing form of {@link isBatchItemShape} — same gate the
|
|
338
|
+
* leaf door applies, so neither shape can carry a half-built item into a PTB.
|
|
339
|
+
*/
|
|
340
|
+
function assertBatchItemShape(item) {
|
|
341
|
+
if (!isBatchItemShape(item)) {
|
|
342
|
+
const symbol = item?.symbol;
|
|
343
|
+
const label = typeof symbol === "string" ? symbol : "<unknown symbol>";
|
|
344
|
+
throw new Error(`WaterX quote-center envelope item ${label} is malformed — expected symbol, ticker, ` +
|
|
345
|
+
`method, a u8 num_sources, a non-empty u64 sources vector, and integer ` +
|
|
346
|
+
`${BATCH_ITEM_U64_FIELDS.join(", ")}.`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
255
349
|
/**
|
|
256
350
|
* A Merkle proof element must be a 32-byte keccak256 hash, and nothing else.
|
|
257
351
|
*
|
|
@@ -346,13 +440,27 @@ async function fetchQuoteCenter(endpoint, path, symbols, what, fetchOpts) {
|
|
|
346
440
|
}
|
|
347
441
|
}
|
|
348
442
|
/**
|
|
349
|
-
* Pull one enclave-signed batch envelope covering `symbols
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
443
|
+
* Pull one enclave-signed batch envelope covering `symbols` — the fallback
|
|
444
|
+
* update shape AND the read executor's transport
|
|
445
|
+
* (`readQuoteCenterPrices` in `../read-prices.ts`). Public seam (WL-2345):
|
|
446
|
+
* consumers that need the raw envelope (BE prefetch caches, read planes)
|
|
447
|
+
* call this instead of re-rolling the fetch + intent/shape gate.
|
|
448
|
+
* `fellBackFrom`, when set, names the leaf-route failure that sent us here,
|
|
449
|
+
* so a deployment whose quote-center serves NEITHER route reports both
|
|
450
|
+
* statuses instead of only the second one.
|
|
353
451
|
*/
|
|
354
|
-
async function fetchWaterxSignedUpdate(endpoint, symbols, fetchOpts, fellBackFrom) {
|
|
452
|
+
export async function fetchWaterxSignedUpdate(endpoint, symbols, fetchOpts, fellBackFrom) {
|
|
355
453
|
const context = fellBackFrom ? ` (fell back from ${fellBackFrom})` : "";
|
|
454
|
+
// Unlike leaves, this route cannot be chunked: the response is ONE signature
|
|
455
|
+
// over the whole batch, so two envelopes are two different snapshots and the
|
|
456
|
+
// payload shape holds one. Say so explicitly rather than let the enclave
|
|
457
|
+
// answer a non-retryable 400 that reads as a generic fetch failure.
|
|
458
|
+
if (symbols.length > WATERX_MAX_BATCH_SYMBOLS) {
|
|
459
|
+
throw new Error(`WaterX quote-center batch fetch needs ${String(symbols.length)} symbols but the enclave ` +
|
|
460
|
+
`signs at most ${String(WATERX_MAX_BATCH_SYMBOLS)} per request, and a batch envelope ` +
|
|
461
|
+
`cannot be split (one signature covers the whole batch). Request fewer tickers, or use ` +
|
|
462
|
+
`a quote-center that serves the per-symbol leaf route, which IS chunked.${context}`);
|
|
463
|
+
}
|
|
356
464
|
const res = await fetchQuoteCenter(endpoint, "v1/quotes/update", symbols, "fetch", fetchOpts);
|
|
357
465
|
if (!res.ok) {
|
|
358
466
|
throw new Error(`WaterX quote-center fetch failed: ${res.status} ${await res.text()}${context}`);
|
|
@@ -387,8 +495,48 @@ async function fetchWaterxSignedUpdate(endpoint, symbols, fetchOpts, fellBackFro
|
|
|
387
495
|
* from its feed registry). That is config drift between this SDK's `feeds` and
|
|
388
496
|
* the quote-center's registry, and the fallback surfaces it honestly: the
|
|
389
497
|
* envelope route 404s on the same symbol, and its error names both attempts.
|
|
498
|
+
*
|
|
499
|
+
* Public seam (WL-2345): consumers holding per-symbol leaves (BE prefetch
|
|
500
|
+
* caches) pull through this instead of re-rolling the fetch + parse gate.
|
|
390
501
|
*/
|
|
391
|
-
async function fetchWaterxSignedLeaves(endpoint, symbols, fetchOpts) {
|
|
502
|
+
export async function fetchWaterxSignedLeaves(endpoint, symbols, fetchOpts) {
|
|
503
|
+
// Chunked against the enclave's per-request cap. Leaves are independently
|
|
504
|
+
// verifiable per symbol — each carries its own proof against a signed root —
|
|
505
|
+
// so splitting the request changes nothing about what the PTB can do with
|
|
506
|
+
// them. Unchunked, a universe prefetch or an all-markets refresh takes a
|
|
507
|
+
// non-retryable 400 the moment a deployment crosses the cap, surfacing as a
|
|
508
|
+
// bare "leaf fetch failed: 400" with nothing pointing at batch size.
|
|
509
|
+
const chunks = chunkSymbols(symbols);
|
|
510
|
+
// Probe with the first chunk: a quote-center with no leaf route answers 404
|
|
511
|
+
// for every chunk, so there is no point spending the rest to learn it.
|
|
512
|
+
const first = await fetchLeafChunk(endpoint, chunks[0] ?? [], fetchOpts);
|
|
513
|
+
if ("unavailable" in first || chunks.length <= 1)
|
|
514
|
+
return first;
|
|
515
|
+
const rest = await Promise.all(chunks.slice(1).map((chunk) => fetchLeafChunk(endpoint, chunk, fetchOpts)));
|
|
516
|
+
const leaves = [...first.leaves];
|
|
517
|
+
for (const pull of rest) {
|
|
518
|
+
if ("unavailable" in pull)
|
|
519
|
+
return pull;
|
|
520
|
+
leaves.push(...pull.leaves);
|
|
521
|
+
}
|
|
522
|
+
return { leaves };
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* The quote-center enclave signs at most this many symbols per request
|
|
526
|
+
* (`MAX_BATCH_SIZE` in `quote-service`). Over the cap it answers a
|
|
527
|
+
* non-retryable 400.
|
|
528
|
+
*/
|
|
529
|
+
export const WATERX_MAX_BATCH_SYMBOLS = 32;
|
|
530
|
+
function chunkSymbols(symbols) {
|
|
531
|
+
if (symbols.length <= WATERX_MAX_BATCH_SYMBOLS)
|
|
532
|
+
return [symbols];
|
|
533
|
+
const out = [];
|
|
534
|
+
for (let i = 0; i < symbols.length; i += WATERX_MAX_BATCH_SYMBOLS) {
|
|
535
|
+
out.push(symbols.slice(i, i + WATERX_MAX_BATCH_SYMBOLS));
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
539
|
+
async function fetchLeafChunk(endpoint, symbols, fetchOpts) {
|
|
392
540
|
const res = await fetchQuoteCenter(endpoint, "v1/quotes/leaves", symbols, "leaf fetch", fetchOpts);
|
|
393
541
|
if (res.status === 404) {
|
|
394
542
|
return { unavailable: `GET /v1/quotes/leaves → 404 ${(await res.text()).trim()}`.trim() };
|
|
@@ -547,44 +695,103 @@ export function feedWaterxRule(tx, host, collector, envelope) {
|
|
|
547
695
|
},
|
|
548
696
|
})(tx);
|
|
549
697
|
}
|
|
698
|
+
/**
|
|
699
|
+
* THE quote-center route ladder, owned by the rule that owns the protocol:
|
|
700
|
+
* pull per-symbol Merkle leaves (the default), and fall back to one batch
|
|
701
|
+
* envelope only when this quote-center has no leaf route (see
|
|
702
|
+
* {@link fetchWaterxSignedLeaves} for exactly which statuses mean that, and
|
|
703
|
+
* why nothing else falls back).
|
|
704
|
+
*
|
|
705
|
+
* Both the write path ({@link pullWaterxData}, which layers coverage policy on
|
|
706
|
+
* top) and the READ executor (`readQuoteCenterPrices` in `../read-prices.ts`,
|
|
707
|
+
* which only decodes prices) go through here, so which route wins, which
|
|
708
|
+
* status falls back, and how the fallback context is threaded are stated once.
|
|
709
|
+
* `items` is the flat symbol-bearing view both callers actually want —
|
|
710
|
+
* `WaterxSignedLeaf extends WaterxBatchItem`, so leaves widen to it for free —
|
|
711
|
+
* while `payload` keeps the shape-specific form the on-chain feed leg needs.
|
|
712
|
+
*/
|
|
713
|
+
export async function pullWaterxQuotes(endpoint, symbols, fetchOpts) {
|
|
714
|
+
const pulled = await fetchWaterxSignedLeaves(endpoint, symbols, fetchOpts);
|
|
715
|
+
if ("leaves" in pulled) {
|
|
716
|
+
return { route: "leaves", payload: { leaves: pulled.leaves }, items: pulled.leaves };
|
|
717
|
+
}
|
|
718
|
+
const envelope = await fetchWaterxSignedUpdate(endpoint, symbols, fetchOpts, pulled.unavailable);
|
|
719
|
+
return { route: "envelope", payload: { envelope }, items: envelope.payload.items };
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* THE quote-center pull — the one pipeline both coverage policies share:
|
|
723
|
+
* package guard → own-key feeds partition → leaf route (default) → batch
|
|
724
|
+
* envelope only when this quote-center has no leaf route (see
|
|
725
|
+
* {@link fetchWaterxSignedLeaves} for exactly which statuses mean that, and
|
|
726
|
+
* why nothing else falls back).
|
|
727
|
+
*
|
|
728
|
+
* `coverage` decides only what a GAP means, never how the pull runs:
|
|
729
|
+
*
|
|
730
|
+
* - `"strict"` — every requested ticker must be config-listed AND served, or
|
|
731
|
+
* this THROWS (the per-ticker unlisted message, before any network call;
|
|
732
|
+
* then {@link assertCoverage} naming the route). `missing` is always `[]`.
|
|
733
|
+
* - `"partial"` — an unlisted or unserved ticker lands in `missing` and the
|
|
734
|
+
* payload covers the rest, narrowed through the rule's own
|
|
735
|
+
* {@link WaterxRule.narrowUpdateData} so leaf-vs-envelope divisibility has
|
|
736
|
+
* exactly one definition.
|
|
737
|
+
*/
|
|
738
|
+
async function pullWaterxData(host, tickers, coverage) {
|
|
739
|
+
// Package-level check first: a config without the deployment must say so,
|
|
740
|
+
// not fail per ticker as if only that feed were missing.
|
|
741
|
+
const { feeds } = requireWaterxPackage(host);
|
|
742
|
+
// One partition pass, own-keys-only: a prototype-key ticker ("toString")
|
|
743
|
+
// must read as unlisted, not pass as an inherited Function and reach the
|
|
744
|
+
// network.
|
|
745
|
+
const missing = [];
|
|
746
|
+
const listed = [];
|
|
747
|
+
for (const ticker of tickers) {
|
|
748
|
+
(ownEntry(feeds, ticker) === undefined ? missing : listed).push(ticker);
|
|
749
|
+
}
|
|
750
|
+
// Unlisted tickers never reach the network on EITHER policy — the
|
|
751
|
+
// quote-center 404s a whole batch on one unknown symbol. Strict surfaces
|
|
752
|
+
// the per-ticker message; partial just records the gap and pulls the rest.
|
|
753
|
+
if (coverage === "strict" && missing.length > 0) {
|
|
754
|
+
throw new Error(`No waterx_rule feed listed for ticker: ${missing[0]}`);
|
|
755
|
+
}
|
|
756
|
+
if (listed.length === 0)
|
|
757
|
+
return { data: null, missing };
|
|
758
|
+
const { endpoint, fetch: fetchOpts } = resolveWaterxInfra(host);
|
|
759
|
+
const { route, payload, items } = await pullWaterxQuotes(endpoint, listed, fetchOpts);
|
|
760
|
+
const served = new Set(items.map((item) => item.symbol));
|
|
761
|
+
if (coverage === "strict") {
|
|
762
|
+
assertCoverage(route, listed, served);
|
|
763
|
+
return { data: { kind: "waterx_rule", payload }, missing };
|
|
764
|
+
}
|
|
765
|
+
const covered = [];
|
|
766
|
+
for (const ticker of listed)
|
|
767
|
+
(served.has(ticker) ? covered : missing).push(ticker);
|
|
768
|
+
// Divisibility is the rule's own knowledge, so the subset decision is
|
|
769
|
+
// delegated rather than re-encoded here: leaves subset per symbol, an
|
|
770
|
+
// envelope is indivisible and passes whole (or `null` when it covers none).
|
|
771
|
+
return {
|
|
772
|
+
data: covered.length > 0
|
|
773
|
+
? WaterxRule.narrowUpdateData(host, { kind: "waterx_rule", payload }, covered)
|
|
774
|
+
: null,
|
|
775
|
+
missing,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
550
778
|
export const WaterxRule = {
|
|
551
779
|
kind: "waterx_rule",
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
requiresFeeSource: false,
|
|
780
|
+
// No credential: the quote-center read surface is public (no `credential`
|
|
781
|
+
// declared — see `PriceUpdateRule.credential`).
|
|
555
782
|
/** Tickers with a `waterx_rule.feeds` entry (keyed by oracle ticker). */
|
|
556
783
|
supportedTickers(host) {
|
|
557
784
|
return Object.keys(host.config.packages.waterx_rule?.feeds ?? {});
|
|
558
785
|
},
|
|
559
786
|
/**
|
|
560
787
|
* Pulls per-symbol Merkle leaves for `tickers`, falling back to one batch
|
|
561
|
-
* envelope only when this quote-center has no leaf route
|
|
562
|
-
*
|
|
563
|
-
* why nothing else falls back). Either way, returns only what covers ALL of
|
|
564
|
-
* `tickers` — see {@link assertCoverage}.
|
|
788
|
+
* envelope only when this quote-center has no leaf route. Returns only what
|
|
789
|
+
* covers ALL of `tickers` — the strict arm of {@link pullWaterxData}.
|
|
565
790
|
*/
|
|
566
791
|
async fetchUpdateData(host, tickers) {
|
|
567
792
|
if (tickers.length === 0)
|
|
568
793
|
return null;
|
|
569
|
-
|
|
570
|
-
// not fail per ticker as if only that feed were missing.
|
|
571
|
-
const { feeds } = requireWaterxPackage(host);
|
|
572
|
-
for (const ticker of tickers) {
|
|
573
|
-
// ownEntry: a prototype-key ticker ("toString") must throw here as
|
|
574
|
-
// unlisted, not pass as an inherited Function and reach the network.
|
|
575
|
-
if (ownEntry(feeds, ticker) === undefined) {
|
|
576
|
-
throw new Error(`No waterx_rule feed listed for ticker: ${ticker}`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
const { endpoint, fetch: fetchOpts } = resolveWaterxInfra(host);
|
|
580
|
-
const pulled = await fetchWaterxSignedLeaves(endpoint, tickers, fetchOpts);
|
|
581
|
-
if ("leaves" in pulled) {
|
|
582
|
-
assertCoverage("leaves", tickers, pulled.leaves.map((l) => l.symbol));
|
|
583
|
-
return { kind: "waterx_rule", payload: { leaves: pulled.leaves } };
|
|
584
|
-
}
|
|
585
|
-
const envelope = await fetchWaterxSignedUpdate(endpoint, tickers, fetchOpts, pulled.unavailable);
|
|
586
|
-
assertCoverage("envelope", tickers, envelope.payload.items.map((i) => i.symbol));
|
|
587
|
-
return { kind: "waterx_rule", payload: { envelope } };
|
|
794
|
+
return (await pullWaterxData(host, tickers, "strict")).data;
|
|
588
795
|
},
|
|
589
796
|
/**
|
|
590
797
|
* Divisibility differs by variant, which is the whole reason this method
|
|
@@ -627,6 +834,28 @@ export const WaterxRule = {
|
|
|
627
834
|
}
|
|
628
835
|
return { kind: "waterx_rule", payload: { envelope } };
|
|
629
836
|
},
|
|
837
|
+
/**
|
|
838
|
+
* The on-chain F-014 single-use replay key, rule-owned (see
|
|
839
|
+
* `PriceUpdateRule.updateIdentityBySymbol`): each leaf's identity is its
|
|
840
|
+
* OWN `signed_timestamp_ms`; an envelope's one `timestamp_ms` is the
|
|
841
|
+
* identity of EVERY symbol it covers (one batch signature ⇒ one submission
|
|
842
|
+
* burns the mark for all of them). A consumer's serve-at-most-once cache
|
|
843
|
+
* keys off this map for BOTH payload shapes — including the leaf-first
|
|
844
|
+
* default an envelope-only identity check would miss.
|
|
845
|
+
*/
|
|
846
|
+
updateIdentityBySymbol(data) {
|
|
847
|
+
// Narrowed ONCE: a serve-at-most-once cache calls this per serve, and the
|
|
848
|
+
// accessors each re-run the full structural payload validation.
|
|
849
|
+
const payload = waterxPayloadOf(data);
|
|
850
|
+
if (!payload)
|
|
851
|
+
return null;
|
|
852
|
+
return "leaves" in payload
|
|
853
|
+
? new Map(payload.leaves.map((leaf) => [leaf.symbol, leaf.signed_timestamp_ms]))
|
|
854
|
+
: new Map(payload.envelope.payload.items.map((item) => [
|
|
855
|
+
item.symbol,
|
|
856
|
+
payload.envelope.timestamp_ms,
|
|
857
|
+
]));
|
|
858
|
+
},
|
|
630
859
|
/**
|
|
631
860
|
* No shared verify step: both `waterx_rule` collect entries bundle verify AND
|
|
632
861
|
* feed into one per-collector call, appended by
|
|
@@ -635,7 +864,36 @@ export const WaterxRule = {
|
|
|
635
864
|
* reaches the feed leg via `aggregate.ts`'s per-ticker map (built from the
|
|
636
865
|
* group's fetched data), not a `RuleUpdateHandle`.
|
|
637
866
|
*/
|
|
638
|
-
buildUpdateCalls(_tx, _host, _data
|
|
867
|
+
buildUpdateCalls(_tx, _host, _data) {
|
|
639
868
|
return;
|
|
640
869
|
},
|
|
641
870
|
};
|
|
871
|
+
/**
|
|
872
|
+
* Coverage-policy seam over the rule's quote-center pull (WL-2345): fetch
|
|
873
|
+
* signed waterx update data for `tickers` with the caller choosing what a
|
|
874
|
+
* coverage gap means.
|
|
875
|
+
*
|
|
876
|
+
* - `coverage: "strict"` (default) — exactly `WaterxRule.fetchUpdateData`:
|
|
877
|
+
* every requested ticker must be config-listed AND served, or the fetch
|
|
878
|
+
* THROWS (`assertCoverage`); `missing` is always `[]`. Trade-path semantics
|
|
879
|
+
* — `refreshOraclePrices` keeps consuming the rule's own strict fetch, so
|
|
880
|
+
* `aggregate.ts`'s uncarried-ticker throw (04117a1) still can't be reached
|
|
881
|
+
* by a payload that under-covers its group.
|
|
882
|
+
* - `coverage: "partial"` — universe-prefetch semantics (a BE cache warming
|
|
883
|
+
* every known ticker at once): a ticker with no `waterx_rule.feeds` entry,
|
|
884
|
+
* or one the quote-center response does not serve, lands in `missing`
|
|
885
|
+
* instead of throwing, and `data` covers the rest. On the leaf route the
|
|
886
|
+
* payload is the covering leaf SUBSET; on the envelope route the envelope
|
|
887
|
+
* is kept iff it covers ≥1 requested ticker (it is indivisible — an
|
|
888
|
+
* envelope serving none is `data: null`). `data: null` + all-missing when
|
|
889
|
+
* nothing is servable.
|
|
890
|
+
*
|
|
891
|
+
* Consumers must not hand a partial payload to a build for tickers in
|
|
892
|
+
* `missing` — those tickers are simply not servable by waterx right now (log
|
|
893
|
+
* the gap; the chain's weight tables decide whether that starves anything).
|
|
894
|
+
*/
|
|
895
|
+
export async function fetchWaterxUpdateData(host, tickers, opts) {
|
|
896
|
+
if (tickers.length === 0)
|
|
897
|
+
return { data: null, missing: [] };
|
|
898
|
+
return pullWaterxData(host, tickers, opts?.coverage ?? "strict");
|
|
899
|
+
}
|
|
@@ -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;
|