@zofai/zo-sdk 0.2.14 → 0.2.16

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.
Files changed (138) hide show
  1. package/dist/abstract/BaseAPI.cjs +5 -0
  2. package/dist/abstract/BaseAPI.cjs.map +1 -1
  3. package/dist/abstract/BaseAPI.d.cts +2 -1
  4. package/dist/abstract/BaseAPI.d.cts.map +1 -1
  5. package/dist/abstract/BaseAPI.d.mts +2 -1
  6. package/dist/abstract/BaseAPI.d.mts.map +1 -1
  7. package/dist/abstract/BaseAPI.mjs +5 -0
  8. package/dist/abstract/BaseAPI.mjs.map +1 -1
  9. package/dist/abstract/BaseDataAPI.cjs +1 -1
  10. package/dist/abstract/BaseDataAPI.cjs.map +1 -1
  11. package/dist/abstract/BaseDataAPI.mjs +1 -1
  12. package/dist/abstract/BaseDataAPI.mjs.map +1 -1
  13. package/dist/consts/deployments-usdz-mainnet.json +1 -1
  14. package/dist/consts/deployments-zo-oracle-mainnet.json +329 -0
  15. package/dist/consts/index.cjs +35 -1
  16. package/dist/consts/index.cjs.map +1 -1
  17. package/dist/consts/index.d.cts +45 -0
  18. package/dist/consts/index.d.cts.map +1 -1
  19. package/dist/consts/index.d.mts +45 -0
  20. package/dist/consts/index.d.mts.map +1 -1
  21. package/dist/consts/index.mjs +31 -0
  22. package/dist/consts/index.mjs.map +1 -1
  23. package/dist/data.cjs +1 -1
  24. package/dist/data.cjs.map +1 -1
  25. package/dist/data.mjs +1 -1
  26. package/dist/data.mjs.map +1 -1
  27. package/dist/implementations/SLPAPI.cjs +3 -2
  28. package/dist/implementations/SLPAPI.cjs.map +1 -1
  29. package/dist/implementations/SLPAPI.d.cts.map +1 -1
  30. package/dist/implementations/SLPAPI.d.mts.map +1 -1
  31. package/dist/implementations/SLPAPI.mjs +3 -2
  32. package/dist/implementations/SLPAPI.mjs.map +1 -1
  33. package/dist/implementations/USDZAPI.cjs +531 -81
  34. package/dist/implementations/USDZAPI.cjs.map +1 -1
  35. package/dist/implementations/USDZAPI.d.cts +62 -0
  36. package/dist/implementations/USDZAPI.d.cts.map +1 -1
  37. package/dist/implementations/USDZAPI.d.mts +62 -0
  38. package/dist/implementations/USDZAPI.d.mts.map +1 -1
  39. package/dist/implementations/USDZAPI.mjs +532 -82
  40. package/dist/implementations/USDZAPI.mjs.map +1 -1
  41. package/dist/implementations/USDZDataAPI.cjs +38 -0
  42. package/dist/implementations/USDZDataAPI.cjs.map +1 -1
  43. package/dist/implementations/USDZDataAPI.d.cts +16 -0
  44. package/dist/implementations/USDZDataAPI.d.cts.map +1 -1
  45. package/dist/implementations/USDZDataAPI.d.mts +16 -0
  46. package/dist/implementations/USDZDataAPI.d.mts.map +1 -1
  47. package/dist/implementations/USDZDataAPI.mjs +38 -0
  48. package/dist/implementations/USDZDataAPI.mjs.map +1 -1
  49. package/dist/implementations/ZBTCVCAPI.cjs +3 -1
  50. package/dist/implementations/ZBTCVCAPI.cjs.map +1 -1
  51. package/dist/implementations/ZBTCVCAPI.d.cts.map +1 -1
  52. package/dist/implementations/ZBTCVCAPI.d.mts.map +1 -1
  53. package/dist/implementations/ZBTCVCAPI.mjs +3 -1
  54. package/dist/implementations/ZBTCVCAPI.mjs.map +1 -1
  55. package/dist/implementations/ZLPAPI.cjs +3 -2
  56. package/dist/implementations/ZLPAPI.cjs.map +1 -1
  57. package/dist/implementations/ZLPAPI.d.cts.map +1 -1
  58. package/dist/implementations/ZLPAPI.d.mts.map +1 -1
  59. package/dist/implementations/ZLPAPI.mjs +3 -2
  60. package/dist/implementations/ZLPAPI.mjs.map +1 -1
  61. package/dist/index.cjs +3 -0
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.d.cts +3 -0
  64. package/dist/index.d.cts.map +1 -1
  65. package/dist/index.d.mts +3 -0
  66. package/dist/index.d.mts.map +1 -1
  67. package/dist/index.mjs +3 -0
  68. package/dist/index.mjs.map +1 -1
  69. package/dist/interfaces/base.d.cts +20 -2
  70. package/dist/interfaces/base.d.cts.map +1 -1
  71. package/dist/interfaces/base.d.mts +20 -2
  72. package/dist/interfaces/base.d.mts.map +1 -1
  73. package/dist/interfaces/usdz.d.cts +19 -0
  74. package/dist/interfaces/usdz.d.cts.map +1 -1
  75. package/dist/interfaces/usdz.d.mts +19 -0
  76. package/dist/interfaces/usdz.d.mts.map +1 -1
  77. package/dist/oracle.cjs +119 -1
  78. package/dist/oracle.cjs.map +1 -1
  79. package/dist/oracle.d.cts +57 -1
  80. package/dist/oracle.d.cts.map +1 -1
  81. package/dist/oracle.d.mts +57 -1
  82. package/dist/oracle.d.mts.map +1 -1
  83. package/dist/oracle.mjs +120 -2
  84. package/dist/oracle.mjs.map +1 -1
  85. package/dist/oraclePro.cjs +211 -0
  86. package/dist/oraclePro.cjs.map +1 -0
  87. package/dist/oraclePro.d.cts +90 -0
  88. package/dist/oraclePro.d.cts.map +1 -0
  89. package/dist/oraclePro.d.mts +90 -0
  90. package/dist/oraclePro.d.mts.map +1 -0
  91. package/dist/oraclePro.mjs +194 -0
  92. package/dist/oraclePro.mjs.map +1 -0
  93. package/dist/pythProClient.cjs +277 -0
  94. package/dist/pythProClient.cjs.map +1 -0
  95. package/dist/pythProClient.d.cts +143 -0
  96. package/dist/pythProClient.d.cts.map +1 -0
  97. package/dist/pythProClient.d.mts +143 -0
  98. package/dist/pythProClient.d.mts.map +1 -0
  99. package/dist/pythProClient.mjs +269 -0
  100. package/dist/pythProClient.mjs.map +1 -0
  101. package/dist/storkClient.cjs +217 -0
  102. package/dist/storkClient.cjs.map +1 -0
  103. package/dist/storkClient.d.cts +90 -0
  104. package/dist/storkClient.d.cts.map +1 -0
  105. package/dist/storkClient.d.mts +90 -0
  106. package/dist/storkClient.d.mts.map +1 -0
  107. package/dist/storkClient.mjs +208 -0
  108. package/dist/storkClient.mjs.map +1 -0
  109. package/dist/storkOracle.cjs +61 -0
  110. package/dist/storkOracle.cjs.map +1 -0
  111. package/dist/storkOracle.d.cts +21 -0
  112. package/dist/storkOracle.d.cts.map +1 -0
  113. package/dist/storkOracle.d.mts +21 -0
  114. package/dist/storkOracle.d.mts.map +1 -0
  115. package/dist/storkOracle.mjs +56 -0
  116. package/dist/storkOracle.mjs.map +1 -0
  117. package/package.json +1 -1
  118. package/src/abstract/BaseAPI.ts +11 -0
  119. package/src/abstract/BaseDataAPI.ts +1 -1
  120. package/src/consts/deployments-usdz-mainnet.json +1 -1
  121. package/src/consts/deployments-zo-oracle-mainnet.json +329 -0
  122. package/src/consts/index.ts +80 -0
  123. package/src/data.ts +1 -1
  124. package/src/implementations/SLPAPI.ts +5 -5
  125. package/src/implementations/USDZAPI.ts +906 -107
  126. package/src/implementations/USDZDataAPI.ts +42 -0
  127. package/src/implementations/ZBTCVCAPI.ts +6 -4
  128. package/src/implementations/ZLPAPI.ts +6 -5
  129. package/src/index.ts +3 -0
  130. package/src/interfaces/base.ts +55 -1
  131. package/src/interfaces/usdz.ts +170 -0
  132. package/src/oracle.ts +222 -1
  133. package/src/oraclePro.ts +309 -0
  134. package/src/pythProClient.ts +406 -0
  135. package/src/storkClient.ts +328 -0
  136. package/src/storkOracle.ts +82 -0
  137. package/tests/pythProClient.test.ts +155 -0
  138. 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
+ })