@zofai/zo-sdk 0.2.14 → 0.2.15
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/dist/abstract/BaseDataAPI.cjs +1 -1
- package/dist/abstract/BaseDataAPI.cjs.map +1 -1
- package/dist/abstract/BaseDataAPI.mjs +1 -1
- package/dist/abstract/BaseDataAPI.mjs.map +1 -1
- package/dist/consts/deployments-usdz-mainnet.json +1 -1
- package/dist/consts/deployments-zo-oracle-mainnet.json +329 -0
- package/dist/consts/index.cjs +35 -1
- package/dist/consts/index.cjs.map +1 -1
- package/dist/consts/index.d.cts +45 -0
- package/dist/consts/index.d.cts.map +1 -1
- package/dist/consts/index.d.mts +45 -0
- package/dist/consts/index.d.mts.map +1 -1
- package/dist/consts/index.mjs +31 -0
- package/dist/consts/index.mjs.map +1 -1
- package/dist/data.cjs +1 -1
- package/dist/data.cjs.map +1 -1
- package/dist/data.mjs +1 -1
- package/dist/data.mjs.map +1 -1
- package/dist/implementations/USDZAPI.cjs +426 -78
- package/dist/implementations/USDZAPI.cjs.map +1 -1
- package/dist/implementations/USDZAPI.d.cts +54 -0
- package/dist/implementations/USDZAPI.d.cts.map +1 -1
- package/dist/implementations/USDZAPI.d.mts +54 -0
- package/dist/implementations/USDZAPI.d.mts.map +1 -1
- package/dist/implementations/USDZAPI.mjs +427 -79
- package/dist/implementations/USDZAPI.mjs.map +1 -1
- package/dist/implementations/USDZDataAPI.cjs +38 -0
- package/dist/implementations/USDZDataAPI.cjs.map +1 -1
- package/dist/implementations/USDZDataAPI.d.cts +16 -0
- package/dist/implementations/USDZDataAPI.d.cts.map +1 -1
- package/dist/implementations/USDZDataAPI.d.mts +16 -0
- package/dist/implementations/USDZDataAPI.d.mts.map +1 -1
- package/dist/implementations/USDZDataAPI.mjs +38 -0
- package/dist/implementations/USDZDataAPI.mjs.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -1
- package/dist/interfaces/base.d.cts +19 -1
- package/dist/interfaces/base.d.cts.map +1 -1
- package/dist/interfaces/base.d.mts +19 -1
- package/dist/interfaces/base.d.mts.map +1 -1
- package/dist/interfaces/usdz.d.cts +17 -0
- package/dist/interfaces/usdz.d.cts.map +1 -1
- package/dist/interfaces/usdz.d.mts +17 -0
- package/dist/interfaces/usdz.d.mts.map +1 -1
- package/dist/oracle.cjs +119 -1
- package/dist/oracle.cjs.map +1 -1
- package/dist/oracle.d.cts +57 -1
- package/dist/oracle.d.cts.map +1 -1
- package/dist/oracle.d.mts +57 -1
- package/dist/oracle.d.mts.map +1 -1
- package/dist/oracle.mjs +120 -2
- package/dist/oracle.mjs.map +1 -1
- package/dist/oraclePro.cjs +214 -0
- package/dist/oraclePro.cjs.map +1 -0
- package/dist/oraclePro.d.cts +92 -0
- package/dist/oraclePro.d.cts.map +1 -0
- package/dist/oraclePro.d.mts +92 -0
- package/dist/oraclePro.d.mts.map +1 -0
- package/dist/oraclePro.mjs +196 -0
- package/dist/oraclePro.mjs.map +1 -0
- package/dist/pythProClient.cjs +277 -0
- package/dist/pythProClient.cjs.map +1 -0
- package/dist/pythProClient.d.cts +143 -0
- package/dist/pythProClient.d.cts.map +1 -0
- package/dist/pythProClient.d.mts +143 -0
- package/dist/pythProClient.d.mts.map +1 -0
- package/dist/pythProClient.mjs +269 -0
- package/dist/pythProClient.mjs.map +1 -0
- package/dist/storkClient.cjs +217 -0
- package/dist/storkClient.cjs.map +1 -0
- package/dist/storkClient.d.cts +90 -0
- package/dist/storkClient.d.cts.map +1 -0
- package/dist/storkClient.d.mts +90 -0
- package/dist/storkClient.d.mts.map +1 -0
- package/dist/storkClient.mjs +208 -0
- package/dist/storkClient.mjs.map +1 -0
- package/dist/storkOracle.cjs +61 -0
- package/dist/storkOracle.cjs.map +1 -0
- package/dist/storkOracle.d.cts +21 -0
- package/dist/storkOracle.d.cts.map +1 -0
- package/dist/storkOracle.d.mts +21 -0
- package/dist/storkOracle.d.mts.map +1 -0
- package/dist/storkOracle.mjs +56 -0
- package/dist/storkOracle.mjs.map +1 -0
- package/package.json +1 -1
- package/src/abstract/BaseDataAPI.ts +1 -1
- package/src/consts/deployments-usdz-mainnet.json +1 -1
- package/src/consts/deployments-zo-oracle-mainnet.json +329 -0
- package/src/consts/index.ts +80 -0
- package/src/data.ts +1 -1
- package/src/implementations/USDZAPI.ts +731 -101
- package/src/implementations/USDZDataAPI.ts +42 -0
- package/src/index.ts +3 -0
- package/src/interfaces/base.ts +54 -1
- package/src/interfaces/usdz.ts +125 -0
- package/src/oracle.ts +222 -1
- package/src/oraclePro.ts +312 -0
- package/src/pythProClient.ts +406 -0
- package/src/storkClient.ts +328 -0
- package/src/storkOracle.ts +82 -0
- package/tests/pythProClient.test.ts +155 -0
- package/tests/storkClient.test.ts +316 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/** Kronos Stork price REST client + update payload parsing. */
|
|
2
|
+
|
|
3
|
+
export interface IStorkEvmSignature {
|
|
4
|
+
r: string
|
|
5
|
+
s: string
|
|
6
|
+
v: string | number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface IStorkSignedPrice {
|
|
10
|
+
public_key?: string
|
|
11
|
+
encoded_asset_id?: string
|
|
12
|
+
/** Stork quantized price used for on-chain signature verification. */
|
|
13
|
+
quantized_price?: string
|
|
14
|
+
price?: string
|
|
15
|
+
timestamped_signature?: {
|
|
16
|
+
timestamp?: number | string
|
|
17
|
+
/** Nanosecond timestamp used for on-chain updates (preferred). */
|
|
18
|
+
timestamp_nano?: number | string
|
|
19
|
+
signature?: IStorkEvmSignature
|
|
20
|
+
msg_hash?: string
|
|
21
|
+
}
|
|
22
|
+
publisher_merkle_root?: string
|
|
23
|
+
calculation_alg?: {
|
|
24
|
+
type?: string
|
|
25
|
+
version?: string
|
|
26
|
+
checksum?: string
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IStorkKronosPriceEntry {
|
|
31
|
+
asset_id: string
|
|
32
|
+
price: string
|
|
33
|
+
priceUsd?: number
|
|
34
|
+
timestamp?: number
|
|
35
|
+
signature_type?: string
|
|
36
|
+
stork_signed_price?: IStorkSignedPrice
|
|
37
|
+
signed_prices?: unknown[]
|
|
38
|
+
/** Pre-parsed on-chain update payload from kronos (preferred). */
|
|
39
|
+
update_data?: IStorkUpdateDataRaw
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IStorkUpdateDataRaw {
|
|
43
|
+
id?: string | number[]
|
|
44
|
+
temporal_numeric_value_timestamp_ns?: number | string
|
|
45
|
+
temporal_numeric_value_magnitude?: string | number
|
|
46
|
+
temporal_numeric_value_negative?: boolean
|
|
47
|
+
publisher_merkle_root?: string | number[]
|
|
48
|
+
value_compute_alg_hash?: string | number[]
|
|
49
|
+
r?: string | number[]
|
|
50
|
+
s?: string | number[]
|
|
51
|
+
v?: number | string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Normalized update payload for Stork Move calls. */
|
|
55
|
+
export interface IStorkUpdateData {
|
|
56
|
+
id: Uint8Array
|
|
57
|
+
temporalNumericValueTimestampNs: bigint
|
|
58
|
+
temporalNumericValueMagnitude: bigint
|
|
59
|
+
temporalNumericValueNegative: boolean
|
|
60
|
+
publisherMerkleRoot: Uint8Array
|
|
61
|
+
valueComputeAlgHash: Uint8Array
|
|
62
|
+
r: Uint8Array
|
|
63
|
+
s: Uint8Array
|
|
64
|
+
v: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface IStorkLatestPriceData {
|
|
68
|
+
prices: Record<string, IStorkKronosPriceEntry>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface IStorkLatestPriceResponse {
|
|
72
|
+
success: boolean
|
|
73
|
+
timestamp?: string
|
|
74
|
+
data?: IStorkLatestPriceData
|
|
75
|
+
message?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface IGetLatestStorkPricesOptions {
|
|
79
|
+
assets: string[]
|
|
80
|
+
/** Override kronos REST base URL. */
|
|
81
|
+
kronosUrl?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeBaseUrl(baseUrl: string): string {
|
|
85
|
+
return baseUrl.replace(/\/+$/, '')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseLatestStorkResponse(json: IStorkLatestPriceResponse): IStorkLatestPriceData {
|
|
89
|
+
if (!json.success || !json.data) {
|
|
90
|
+
throw new Error(json.message ?? 'Stork latest-price request failed')
|
|
91
|
+
}
|
|
92
|
+
return json.data
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
96
|
+
const normalized = hex.startsWith('0x') ? hex.slice(2) : hex
|
|
97
|
+
if (normalized.length % 2 !== 0) {
|
|
98
|
+
throw new Error(`Invalid hex length: ${hex}`)
|
|
99
|
+
}
|
|
100
|
+
const bytes = new Uint8Array(normalized.length / 2)
|
|
101
|
+
for (let i = 0; i < normalized.length; i += 2) {
|
|
102
|
+
bytes[i / 2] = Number.parseInt(normalized.slice(i, i + 2), 16)
|
|
103
|
+
}
|
|
104
|
+
return bytes
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function base64ToBytes(base64: string): Uint8Array {
|
|
108
|
+
const normalized = base64.replaceAll('-', '+').replaceAll('_', '/')
|
|
109
|
+
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4)
|
|
110
|
+
const binary = atob(padded)
|
|
111
|
+
const bytes = new Uint8Array(binary.length)
|
|
112
|
+
for (let i = 0; i < binary.length; i++) {
|
|
113
|
+
bytes[i] = binary.charCodeAt(i)
|
|
114
|
+
}
|
|
115
|
+
return bytes
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toBytes(value: string | number[] | Uint8Array | undefined, field: string): Uint8Array {
|
|
119
|
+
if (value === undefined || value === null) {
|
|
120
|
+
throw new Error(`Missing Stork field: ${field}`)
|
|
121
|
+
}
|
|
122
|
+
if (value instanceof Uint8Array) {
|
|
123
|
+
return value
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
return Uint8Array.from(value)
|
|
127
|
+
}
|
|
128
|
+
if (value.startsWith('0x') || /^[0-9a-f]+$/i.test(value)) {
|
|
129
|
+
return hexToBytes(value)
|
|
130
|
+
}
|
|
131
|
+
return base64ToBytes(value)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toBigInt(value: string | number | bigint | undefined | null): bigint {
|
|
135
|
+
if (value === undefined || value === null) {
|
|
136
|
+
return 0n
|
|
137
|
+
}
|
|
138
|
+
if (typeof value === 'bigint') {
|
|
139
|
+
return value
|
|
140
|
+
}
|
|
141
|
+
if (typeof value === 'string') {
|
|
142
|
+
return BigInt(value)
|
|
143
|
+
}
|
|
144
|
+
// `value` is already a float64 at this point — any u64 > 2^53 has already lost
|
|
145
|
+
// precision before reaching here. String(n) round-trips the truncated float64,
|
|
146
|
+
// not the original integer. Callers should pass string timestamps from the API
|
|
147
|
+
// to avoid this loss.
|
|
148
|
+
return BigInt(String(value))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseV(value: string | number): number {
|
|
152
|
+
if (typeof value === 'number') {
|
|
153
|
+
return value
|
|
154
|
+
}
|
|
155
|
+
const bytes = toBytes(value, 'v')
|
|
156
|
+
if (bytes.length > 0) {
|
|
157
|
+
return bytes[0]!
|
|
158
|
+
}
|
|
159
|
+
if (value.startsWith('0x')) {
|
|
160
|
+
return Number.parseInt(value, 16)
|
|
161
|
+
}
|
|
162
|
+
return Number.parseInt(value, 10)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function parseStorkUpdateDataRaw(raw: IStorkUpdateDataRaw): IStorkUpdateData {
|
|
166
|
+
const id = toBytes(raw.id, 'id')
|
|
167
|
+
if (id.length !== 32) {
|
|
168
|
+
throw new Error(`Stork update id must be 32 bytes, got ${id.length}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
id,
|
|
173
|
+
temporalNumericValueTimestampNs: toBigInt(raw.temporal_numeric_value_timestamp_ns),
|
|
174
|
+
temporalNumericValueMagnitude: toBigInt(raw.temporal_numeric_value_magnitude),
|
|
175
|
+
temporalNumericValueNegative: raw.temporal_numeric_value_negative ?? false,
|
|
176
|
+
publisherMerkleRoot: toBytes(raw.publisher_merkle_root, 'publisher_merkle_root'),
|
|
177
|
+
valueComputeAlgHash: toBytes(raw.value_compute_alg_hash, 'value_compute_alg_hash'),
|
|
178
|
+
r: toBytes(raw.r, 'r'),
|
|
179
|
+
s: toBytes(raw.s, 's'),
|
|
180
|
+
v: parseV(raw.v ?? 0),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseQuantizedPrice(value: string): { magnitude: bigint, negative: boolean } {
|
|
185
|
+
let price = BigInt(value)
|
|
186
|
+
const negative = price < 0n
|
|
187
|
+
if (negative) {
|
|
188
|
+
price = -price
|
|
189
|
+
}
|
|
190
|
+
return { magnitude: price, negative }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Build on-chain update data from kronos / Stork REST signed price payload. */
|
|
194
|
+
export function buildStorkUpdateDataFromSignedPrice(
|
|
195
|
+
entry: IStorkKronosPriceEntry,
|
|
196
|
+
/** Fallback 32-byte feeder id when `encoded_asset_id` is absent. */
|
|
197
|
+
feederId?: Uint8Array,
|
|
198
|
+
): IStorkUpdateData | null {
|
|
199
|
+
const signed = entry.stork_signed_price
|
|
200
|
+
if (signed?.timestamped_signature?.signature) {
|
|
201
|
+
const checksum = signed.calculation_alg?.checksum
|
|
202
|
+
if (!checksum) {
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const quantizedPrice = signed.quantized_price ?? signed.price ?? entry.price
|
|
207
|
+
if (!quantizedPrice) {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const assetId = signed.encoded_asset_id
|
|
212
|
+
if (!assetId && !feederId) {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { magnitude, negative } = parseQuantizedPrice(String(quantizedPrice))
|
|
217
|
+
|
|
218
|
+
// Nanosecond timestamps for dates in 2026+ (~1.78e18 ns) exceed float64 safe-integer
|
|
219
|
+
// range (2^53 ≈ 9e15). JSON.parse silently truncates them by up to ±127 ns (precision
|
|
220
|
+
// step = 256 at this scale), causing get_stork_message_hash to produce the wrong value
|
|
221
|
+
// and ecrecover to fail → EInvalidSignature (error code 0).
|
|
222
|
+
// Prefer string sources; fall back to update_data.temporal_numeric_value_timestamp_ns
|
|
223
|
+
// (a string in kronos pre-parsed payloads) when stork_signed_price only has a number.
|
|
224
|
+
const rawTimestampNs = signed.timestamped_signature.timestamp_nano
|
|
225
|
+
?? signed.timestamped_signature.timestamp
|
|
226
|
+
?? entry.timestamp
|
|
227
|
+
const timestampNs = (
|
|
228
|
+
typeof rawTimestampNs === 'number'
|
|
229
|
+
&& typeof entry.update_data?.temporal_numeric_value_timestamp_ns === 'string'
|
|
230
|
+
)
|
|
231
|
+
? entry.update_data.temporal_numeric_value_timestamp_ns
|
|
232
|
+
: rawTimestampNs
|
|
233
|
+
|
|
234
|
+
const id = assetId
|
|
235
|
+
? toBytes(assetId, 'encoded_asset_id')
|
|
236
|
+
: feederId!
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
id,
|
|
240
|
+
temporalNumericValueTimestampNs: toBigInt(timestampNs),
|
|
241
|
+
temporalNumericValueMagnitude: magnitude,
|
|
242
|
+
temporalNumericValueNegative: negative,
|
|
243
|
+
publisherMerkleRoot: toBytes(signed.publisher_merkle_root, 'publisher_merkle_root'),
|
|
244
|
+
valueComputeAlgHash: toBytes(checksum, 'value_compute_alg_hash'),
|
|
245
|
+
r: toBytes(signed.timestamped_signature.signature.r, 'r'),
|
|
246
|
+
s: toBytes(signed.timestamped_signature.signature.s, 's'),
|
|
247
|
+
v: parseV(signed.timestamped_signature.signature.v),
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (entry.update_data) {
|
|
252
|
+
return parseStorkUpdateDataRaw(entry.update_data)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function parseStorkUpdateDataList(
|
|
259
|
+
data: IStorkLatestPriceData,
|
|
260
|
+
/** Stork asset symbol → 32-byte on-chain feeder id from zo-oracle config. */
|
|
261
|
+
assetIdByAsset?: Map<string, Uint8Array>,
|
|
262
|
+
): IStorkUpdateData[] {
|
|
263
|
+
const updates: IStorkUpdateData[] = []
|
|
264
|
+
for (const entry of Object.values(data.prices)) {
|
|
265
|
+
const feederId = assetIdByAsset?.get(entry.asset_id)
|
|
266
|
+
const update = buildStorkUpdateDataFromSignedPrice(entry, feederId)
|
|
267
|
+
if (update) {
|
|
268
|
+
if (update.id.length !== 32) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Stork asset ${entry.asset_id}: id must be 32 bytes (got ${update.id.length}); `
|
|
271
|
+
+ 'pass assetIdByAsset from buildStorkAssetIdMap()',
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
updates.push(update)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return updates
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Fetch latest Stork prices from kronos REST (`GET` or `POST /stork/latest-price`).
|
|
282
|
+
*/
|
|
283
|
+
export async function getLatestStorkPrices(
|
|
284
|
+
baseUrl: string,
|
|
285
|
+
options: IGetLatestStorkPricesOptions,
|
|
286
|
+
): Promise<IStorkLatestPriceData> {
|
|
287
|
+
const { assets } = options
|
|
288
|
+
if (assets.length === 0) {
|
|
289
|
+
throw new Error('assets is required')
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const root = normalizeBaseUrl(baseUrl)
|
|
293
|
+
|
|
294
|
+
if (assets.length <= 20) {
|
|
295
|
+
const params = new URLSearchParams()
|
|
296
|
+
params.set('assets', assets.join(','))
|
|
297
|
+
const res = await fetch(`${root}/stork/latest-price?${params}`)
|
|
298
|
+
const json = await res.json() as IStorkLatestPriceResponse
|
|
299
|
+
if (!res.ok && json.message) {
|
|
300
|
+
throw new Error(json.message)
|
|
301
|
+
}
|
|
302
|
+
return parseLatestStorkResponse(json)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const res = await fetch(`${root}/stork/latest-price`, {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
headers: { 'Content-Type': 'application/json' },
|
|
308
|
+
body: JSON.stringify({ assets }),
|
|
309
|
+
})
|
|
310
|
+
const json = await res.json() as IStorkLatestPriceResponse
|
|
311
|
+
if (!res.ok && json.message) {
|
|
312
|
+
throw new Error(json.message)
|
|
313
|
+
}
|
|
314
|
+
return parseLatestStorkResponse(json)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function fetchStorkUpdateDataFromKronos(
|
|
318
|
+
baseUrl: string,
|
|
319
|
+
assets: string[],
|
|
320
|
+
assetIdByAsset?: Map<string, Uint8Array>,
|
|
321
|
+
): Promise<IStorkUpdateData[]> {
|
|
322
|
+
const data = await getLatestStorkPrices(baseUrl, { assets })
|
|
323
|
+
const updates = parseStorkUpdateDataList(data, assetIdByAsset)
|
|
324
|
+
if (updates.length === 0) {
|
|
325
|
+
throw new Error(`No Stork update data returned for assets: ${assets.join(',')}`)
|
|
326
|
+
}
|
|
327
|
+
return updates
|
|
328
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { TransactionObjectArgument } from '@mysten/sui/transactions'
|
|
2
|
+
import { Transaction } from '@mysten/sui/transactions'
|
|
3
|
+
|
|
4
|
+
import type { IOracleProConfig } from './consts'
|
|
5
|
+
import type { IStorkUpdateData } from './storkClient'
|
|
6
|
+
|
|
7
|
+
/** 1 MIST per Stork feed update (matches zo-go-executor). */
|
|
8
|
+
export const STORK_UPDATE_FEE_MIST = 1n
|
|
9
|
+
|
|
10
|
+
export interface IAppendStorkUpdatesOptions {
|
|
11
|
+
/** Coin to split update fees from. Defaults to `tx.gas`. */
|
|
12
|
+
feeSource?: TransactionObjectArgument
|
|
13
|
+
/** MIST per update. Defaults to 1. */
|
|
14
|
+
feePerUpdate?: bigint
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function bytesToPureVector(bytesList: Uint8Array[]): number[][] {
|
|
18
|
+
return bytesList.map(bytes => Array.from(bytes))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Append Stork on-chain price updates to an open PTB (before Pyth Pro verify + market op).
|
|
23
|
+
*
|
|
24
|
+
* 1. `update_temporal_numeric_value_evm_input_vec::new`
|
|
25
|
+
* 2. `stork::update_multiple_temporal_numeric_values_evm`
|
|
26
|
+
*/
|
|
27
|
+
export function appendStorkUpdatesToTransaction(
|
|
28
|
+
tx: Transaction,
|
|
29
|
+
config: Pick<IOracleProConfig, 'storkPackage' | 'storkState'>,
|
|
30
|
+
updates: IStorkUpdateData[],
|
|
31
|
+
options?: IAppendStorkUpdatesOptions,
|
|
32
|
+
): void {
|
|
33
|
+
if (updates.length === 0) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const feePerUpdate = options?.feePerUpdate ?? STORK_UPDATE_FEE_MIST
|
|
38
|
+
const totalFee = feePerUpdate * BigInt(updates.length)
|
|
39
|
+
const feeSource = options?.feeSource ?? tx.gas
|
|
40
|
+
const [feeCoin] = tx.splitCoins(feeSource, [tx.pure.u64(totalFee)])
|
|
41
|
+
|
|
42
|
+
const ids = bytesToPureVector(updates.map(u => u.id))
|
|
43
|
+
const timestampNss = updates.map(u => u.temporalNumericValueTimestampNs)
|
|
44
|
+
const magnitudes = updates.map(u => u.temporalNumericValueMagnitude)
|
|
45
|
+
const negatives = updates.map(u => u.temporalNumericValueNegative)
|
|
46
|
+
const publisherMerkleRoots = bytesToPureVector(updates.map(u => u.publisherMerkleRoot))
|
|
47
|
+
const valueComputeAlgHashes = bytesToPureVector(updates.map(u => u.valueComputeAlgHash))
|
|
48
|
+
const rs = bytesToPureVector(updates.map(u => u.r))
|
|
49
|
+
const ss = bytesToPureVector(updates.map(u => u.s))
|
|
50
|
+
const vs = updates.map(u => u.v)
|
|
51
|
+
|
|
52
|
+
const [updateInputVec] = tx.moveCall({
|
|
53
|
+
target: `${config.storkPackage}::update_temporal_numeric_value_evm_input_vec::new`,
|
|
54
|
+
arguments: [
|
|
55
|
+
tx.pure.vector('vector<u8>', ids),
|
|
56
|
+
tx.pure.vector('u64', timestampNss),
|
|
57
|
+
tx.pure.vector('u128', magnitudes),
|
|
58
|
+
tx.pure.vector('bool', negatives),
|
|
59
|
+
tx.pure.vector('vector<u8>', publisherMerkleRoots),
|
|
60
|
+
tx.pure.vector('vector<u8>', valueComputeAlgHashes),
|
|
61
|
+
tx.pure.vector('vector<u8>', rs),
|
|
62
|
+
tx.pure.vector('vector<u8>', ss),
|
|
63
|
+
tx.pure.vector('u8', vs),
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
tx.moveCall({
|
|
68
|
+
target: `${config.storkPackage}::stork::update_multiple_temporal_numeric_values_evm`,
|
|
69
|
+
arguments: [
|
|
70
|
+
tx.object(config.storkState),
|
|
71
|
+
updateInputVec,
|
|
72
|
+
feeCoin,
|
|
73
|
+
],
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function estimateStorkUpdateFeeMist(
|
|
78
|
+
updateCount: number,
|
|
79
|
+
feePerUpdate: bigint = STORK_UPDATE_FEE_MIST,
|
|
80
|
+
): bigint {
|
|
81
|
+
return feePerUpdate * BigInt(updateCount)
|
|
82
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
apiBaseUrlToPythProWsUrl,
|
|
5
|
+
fetchPythProUpdateBytesFromKronos,
|
|
6
|
+
getLatestPythProPrices,
|
|
7
|
+
hexToBytes,
|
|
8
|
+
PythProStreamClient,
|
|
9
|
+
} from '../src/pythProClient'
|
|
10
|
+
|
|
11
|
+
describe('pythProClient REST', () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('apiBaseUrlToPythProWsUrl converts http to ws', () => {
|
|
17
|
+
expect(apiBaseUrlToPythProWsUrl('http://localhost:8080/')).toBe('ws://localhost:8080/pyth-pro/stream')
|
|
18
|
+
expect(apiBaseUrlToPythProWsUrl('https://kronos.example.com/api')).toBe(
|
|
19
|
+
'wss://kronos.example.com/pyth-pro/stream',
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('getLatestPythProPrices uses GET with comma-separated feed ids', async () => {
|
|
24
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
25
|
+
ok: true,
|
|
26
|
+
json: async () => ({
|
|
27
|
+
success: true,
|
|
28
|
+
data: {
|
|
29
|
+
timestampUs: '1',
|
|
30
|
+
priceFeeds: [{ priceFeedId: 1, price: '100', exponent: -8, confidence: '1', feedUpdateTimestamp: '1', priceUsd: 1 }],
|
|
31
|
+
leEcdsaHex: null,
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
36
|
+
|
|
37
|
+
const data = await getLatestPythProPrices('http://localhost:8080', {
|
|
38
|
+
priceFeedIds: [1, 2],
|
|
39
|
+
includeEmaPrice: true,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(data.priceFeeds).toHaveLength(1)
|
|
43
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
44
|
+
'http://localhost:8080/pyth-pro/latest-price?priceFeedIds=1%2C2&includeEmaPrice=true',
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('fetchPythProUpdateBytesFromKronos decodes leEcdsaHex', async () => {
|
|
49
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
50
|
+
ok: true,
|
|
51
|
+
json: async () => ({
|
|
52
|
+
success: true,
|
|
53
|
+
data: {
|
|
54
|
+
timestampUs: '1',
|
|
55
|
+
priceFeeds: [],
|
|
56
|
+
leEcdsaHex: '0102ab',
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
const bytes = await fetchPythProUpdateBytesFromKronos('http://localhost:8080', [1])
|
|
62
|
+
expect(Array.from(bytes)).toEqual([1, 2, 171])
|
|
63
|
+
expect(hexToBytes('0x0102ab')).toEqual(bytes)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('throws on kronos error response', async () => {
|
|
67
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
68
|
+
ok: false,
|
|
69
|
+
json: async () => ({ success: false, message: 'PYTH_LAZER_ACCESS_TOKEN not configured' }),
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
await expect(
|
|
73
|
+
getLatestPythProPrices('http://localhost:8080', { priceFeedIds: [1] }),
|
|
74
|
+
).rejects.toThrow('PYTH_LAZER_ACCESS_TOKEN not configured')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('PythProStreamClient', () => {
|
|
79
|
+
class MockWebSocket {
|
|
80
|
+
static OPEN = 1
|
|
81
|
+
readyState = MockWebSocket.OPEN
|
|
82
|
+
sent: string[] = []
|
|
83
|
+
private listeners: Record<string, Array<(event: { data?: string }) => void>> = {}
|
|
84
|
+
|
|
85
|
+
constructor(public url: string) {
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
this.listeners.open?.forEach(cb => cb({}))
|
|
88
|
+
this.emit({ data: JSON.stringify({ type: 'connected' }) })
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
addEventListener(event: string, cb: (event: { data?: string }) => void, opts?: { once?: boolean }) {
|
|
93
|
+
if (!this.listeners[event]) {
|
|
94
|
+
this.listeners[event] = []
|
|
95
|
+
}
|
|
96
|
+
if (opts?.once) {
|
|
97
|
+
const onceWrapper = (ev: { data?: string }) => {
|
|
98
|
+
this.listeners[event] = this.listeners[event].filter(h => h !== onceWrapper)
|
|
99
|
+
cb(ev)
|
|
100
|
+
}
|
|
101
|
+
this.listeners[event].push(onceWrapper)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
this.listeners[event].push(cb)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
send(data: string) {
|
|
108
|
+
this.sent.push(data)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
close() {
|
|
112
|
+
this.readyState = 3
|
|
113
|
+
this.listeners.close?.forEach(cb => cb({}))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
emit(event: { data?: string }) {
|
|
117
|
+
this.listeners.message?.forEach(cb => cb(event))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
it('subscribes and forwards streamUpdated', async () => {
|
|
122
|
+
vi.stubGlobal('WebSocket', MockWebSocket)
|
|
123
|
+
|
|
124
|
+
const client = new PythProStreamClient('http://localhost:8080', {
|
|
125
|
+
WebSocketImpl: MockWebSocket as unknown as typeof WebSocket,
|
|
126
|
+
pingIntervalMs: 0,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const updates: number[] = []
|
|
130
|
+
client.subscribe(
|
|
131
|
+
{ subscriptionId: 1, priceFeedIds: [1, 2], channel: 'real_time' },
|
|
132
|
+
(update) => {
|
|
133
|
+
updates.push(update.priceFeeds[0]?.priceFeedId ?? 0)
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
await client.connect()
|
|
138
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0))
|
|
139
|
+
|
|
140
|
+
expect((client as any).ws.sent.length).toBeGreaterThan(0)
|
|
141
|
+
expect(JSON.parse((client as any).ws.sent[0]).type).toBe('subscribe')
|
|
142
|
+
|
|
143
|
+
;(client as any).ws.emit({
|
|
144
|
+
data: JSON.stringify({
|
|
145
|
+
type: 'streamUpdated',
|
|
146
|
+
subscriptionId: 1,
|
|
147
|
+
timestampUs: '99',
|
|
148
|
+
priceFeeds: [{ priceFeedId: 42, price: '1', exponent: -8, confidence: '1', feedUpdateTimestamp: '1', priceUsd: 1 }],
|
|
149
|
+
}),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(updates).toEqual([42])
|
|
153
|
+
client.disconnect()
|
|
154
|
+
})
|
|
155
|
+
})
|