@zofai/zo-sdk 0.2.27 → 0.2.29

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 (36) hide show
  1. package/dist/consts/deployments-zo-oracle-mainnet.json +1 -1
  2. package/dist/implementations/SLPDataAPI.cjs +31 -7
  3. package/dist/implementations/SLPDataAPI.cjs.map +1 -1
  4. package/dist/implementations/SLPDataAPI.d.cts.map +1 -1
  5. package/dist/implementations/SLPDataAPI.d.mts.map +1 -1
  6. package/dist/implementations/SLPDataAPI.mjs +33 -9
  7. package/dist/implementations/SLPDataAPI.mjs.map +1 -1
  8. package/dist/oracle.cjs +4 -1
  9. package/dist/oracle.cjs.map +1 -1
  10. package/dist/oracle.d.cts.map +1 -1
  11. package/dist/oracle.d.mts.map +1 -1
  12. package/dist/oracle.mjs +4 -1
  13. package/dist/oracle.mjs.map +1 -1
  14. package/dist/oraclePro.cjs +52 -0
  15. package/dist/oraclePro.cjs.map +1 -1
  16. package/dist/oraclePro.d.cts +19 -1
  17. package/dist/oraclePro.d.cts.map +1 -1
  18. package/dist/oraclePro.d.mts +19 -1
  19. package/dist/oraclePro.d.mts.map +1 -1
  20. package/dist/oraclePro.mjs +49 -0
  21. package/dist/oraclePro.mjs.map +1 -1
  22. package/dist/pythProClient.cjs +12 -8
  23. package/dist/pythProClient.cjs.map +1 -1
  24. package/dist/pythProClient.d.cts +1 -0
  25. package/dist/pythProClient.d.cts.map +1 -1
  26. package/dist/pythProClient.d.mts +1 -0
  27. package/dist/pythProClient.d.mts.map +1 -1
  28. package/dist/pythProClient.mjs +11 -7
  29. package/dist/pythProClient.mjs.map +1 -1
  30. package/package.json +1 -1
  31. package/src/consts/deployments-zo-oracle-mainnet.json +1 -1
  32. package/src/implementations/SLPDataAPI.ts +42 -10
  33. package/src/oracle.ts +4 -1
  34. package/src/oraclePro.ts +82 -1
  35. package/src/pythProClient.ts +12 -7
  36. package/tests/pythProClient.test.ts +1 -1
@@ -12,7 +12,7 @@ import { SUI_CLOCK_OBJECT_ID } from '@mysten/sui/utils'
12
12
  import { BaseDataAPI } from '../abstract'
13
13
  import { Rate, SymbolsValuation, VaultsValuation } from '../bcs'
14
14
  import type { Network } from '../consts'
15
- import { LPToken, SLP_TOKEN_DECIMALS } from '../consts'
15
+ import { getZoOracleConfig, LPToken, SLP_TOKEN_DECIMALS } from '../consts'
16
16
  import type {
17
17
  IBaseHistoryResponse,
18
18
  IBaseOrderType,
@@ -40,7 +40,12 @@ import type {
40
40
  } from '../interfaces'
41
41
  import type { OracleInputs } from '../oraclePro'
42
42
  import type { DynamicFieldInfo, SuiClient } from '../suiClient'
43
- import { valuateSymbolsV3, valuateVaultsV3 } from '../oraclePro'
43
+ import {
44
+ buildPythProPriceFeedMap,
45
+ resolveTokenUsdPriceFromPythPro,
46
+ valuateSymbolsV3,
47
+ valuateVaultsV3,
48
+ } from '../oraclePro'
44
49
  import { joinSymbol, parseSymbolKey, parseValue, suiSymbolToSymbol } from '../utils'
45
50
 
46
51
  export interface GetCumulativeAprResponse {
@@ -261,11 +266,29 @@ export class SLPDataAPI extends BaseDataAPI implements ISLPDataAPI {
261
266
  let value = 0
262
267
 
263
268
  const vaultKeys = Object.keys(this.consts.sudoCore.vaults)
269
+ const symbolTokenIds = [
270
+ ...new Set(
271
+ Object.keys(this.consts.sudoCore.symbols).map(symbol => parseSymbolKey(symbol)[1]),
272
+ ),
273
+ ]
274
+ const valuationTokens = [...new Set([...vaultKeys, ...symbolTokenIds])]
275
+ const pythProData = await this.getLatestPythProPricesForTokens(valuationTokens)
276
+ const feedById = buildPythProPriceFeedMap(pythProData)
277
+ const zoOracle = getZoOracleConfig(this.network)
278
+ const resolvePrice = (token: string) => resolveTokenUsdPriceFromPythPro({
279
+ token,
280
+ feedById,
281
+ zoOracle,
282
+ coins: this.consts.coins,
283
+ network: this.network,
284
+ legacyFeeders: this.consts.pythFeeder.feeder,
285
+ })
286
+
264
287
  const vaultData = await Promise.all(vaultKeys.map(async (vault) => {
265
288
  const vaultInfo = await this.getVaultInfo(vault)
266
289
  const reservingFeeDelta = SLPDataAPI.calculateVaultReservingFee(vaultInfo, vaultInfo.reservingFeeModel, Date.now() / 1000)
267
290
  const totalVaultAmount = reservingFeeDelta + vaultInfo.liquidity + vaultInfo.reservedAmount
268
- const oraclePrice = (await this.getOraclePrice(vault)).getPriceUnchecked().getPriceAsNumberUnchecked()
291
+ const oraclePrice = resolvePrice(vault)
269
292
  const { decimals } = this.consts.coins[vault]
270
293
  const vaultValue = totalVaultAmount * oraclePrice / (10 ** decimals)
271
294
  return { vault, oraclePrice, vaultValue }
@@ -276,7 +299,7 @@ export class SLPDataAPI extends BaseDataAPI implements ISLPDataAPI {
276
299
  const symbolPromises = Object.keys(this.consts.sudoCore.symbols).map(async (symbol) => {
277
300
  const [direction, tokenId] = parseSymbolKey(symbol)
278
301
  const symbolInfo = await this.getSymbolInfo(tokenId, direction === 'long')
279
- const price = (await this.getOraclePrice(tokenId)).getPriceUnchecked().getPriceAsNumberUnchecked()
302
+ const price = resolvePrice(tokenId)
280
303
 
281
304
  const oiState = await this.getSymbolOiFundingState(tokenId)
282
305
  const pairedInfo = await this.getSymbolInfo(tokenId, direction !== 'long')
@@ -393,13 +416,22 @@ export class SLPDataAPI extends BaseDataAPI implements ISLPDataAPI {
393
416
  const slpPrice = value / marketInfo.lpSupplyWithDecimals
394
417
 
395
418
  const vaultKeys = Object.keys(this.consts.sudoCore.vaults)
396
- const vaultPricesEntries = await Promise.all(
397
- vaultKeys.map(async (vault) => {
398
- const oraclePrice = (await this.getOraclePrice(vault)).getPriceUnchecked().getPriceAsNumberUnchecked()
399
- return [vault, oraclePrice] as const
400
- }),
419
+ const pythProData = await this.getLatestPythProPricesForTokens(vaultKeys)
420
+ const feedById = buildPythProPriceFeedMap(pythProData)
421
+ const zoOracle = getZoOracleConfig(this.network)
422
+ const vaultPrices = Object.fromEntries(
423
+ vaultKeys.map(vault => [
424
+ vault,
425
+ resolveTokenUsdPriceFromPythPro({
426
+ token: vault,
427
+ feedById,
428
+ zoOracle,
429
+ coins: this.consts.coins,
430
+ network: this.network,
431
+ legacyFeeders: this.consts.pythFeeder.feeder,
432
+ }),
433
+ ]),
401
434
  )
402
- const vaultPrices = Object.fromEntries(vaultPricesEntries)
403
435
 
404
436
  return {
405
437
  marketCap: value,
package/src/oracle.ts CHANGED
@@ -345,10 +345,11 @@ export class OracleAPI {
345
345
  tokens: string[],
346
346
  options?: Omit<IGetLatestPythProPricesOptions, 'priceFeedIds'>,
347
347
  ): Promise<PythProLatestPriceData> {
348
+ const config = this.getOracleProConfig()
348
349
  const priceFeedIds = this.resolveLazerFeedIdsForTokens(tokens)
349
350
  return getLatestPythProPrices(this.resolveKronosUrl(options?.kronosUrl), {
350
351
  priceFeedIds,
351
- channel: options?.channel,
352
+ channel: options?.channel ?? (config.lazerChannel as PythProChannel),
352
353
  includeEmaPrice: options?.includeEmaPrice,
353
354
  includeUpdateBytes: options?.includeUpdateBytes,
354
355
  })
@@ -375,6 +376,7 @@ export class OracleAPI {
375
376
  options?: Omit<IPythProStreamSubscription, 'subscriptionId' | 'priceFeedIds'>,
376
377
  subscriptionId = 1,
377
378
  ): Promise<() => void> {
379
+ const config = this.getOracleProConfig()
378
380
  const client = this.createPythProStreamClient()
379
381
  const priceFeedIds = this.resolveLazerFeedIdsForTokens(tokens)
380
382
  await client.connect()
@@ -383,6 +385,7 @@ export class OracleAPI {
383
385
  subscriptionId,
384
386
  priceFeedIds,
385
387
  ...options,
388
+ channel: options?.channel ?? (config.lazerChannel as PythProChannel),
386
389
  },
387
390
  onUpdate,
388
391
  )
package/src/oraclePro.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getPythFeederToPriceId,
14
14
  getZoOracleConfig,
15
15
  } from './consts'
16
- import type { PythProChannel } from './pythProClient'
16
+ import type { PythProChannel, PythProLatestPriceData, PythProPriceFeed } from './pythProClient'
17
17
  import { fetchPythProUpdateBytesFromKronos } from './pythProClient'
18
18
  import { hexToBytes } from './storkClient'
19
19
  import { parseSymbolKey } from './utils'
@@ -174,6 +174,87 @@ export function resolveLazerFeedIds(
174
174
  return [...ids]
175
175
  }
176
176
 
177
+ /** Parse a Pyth Pro feed's raw price + exponent into a decimal number (matches on-chain normalization). */
178
+ export function parsePythProFeedDecimalPrice(feed: PythProPriceFeed): number {
179
+ const raw = Number(feed.price)
180
+ const expo = feed.exponent
181
+ if (expo < 0) {
182
+ return raw / 10 ** -expo
183
+ }
184
+ return raw * 10 ** expo
185
+ }
186
+
187
+ export function buildPythProPriceFeedMap(
188
+ data: PythProLatestPriceData,
189
+ ): Map<number, PythProPriceFeed> {
190
+ const map = new Map<number, PythProPriceFeed>()
191
+ for (const feed of data.priceFeeds) {
192
+ map.set(feed.priceFeedId, feed)
193
+ }
194
+ return map
195
+ }
196
+
197
+ export interface IResolveTokenUsdPriceFromPythProParams {
198
+ token: string
199
+ feedById: Map<number, PythProPriceFeed>
200
+ zoOracle: IZoOracleConfig
201
+ coins: Record<string, { module: string }>
202
+ network: Network
203
+ legacyFeeders?: Record<string, string>
204
+ }
205
+
206
+ /**
207
+ * Resolve a token's USD price from batched Pyth Pro feeds.
208
+ * LST SUI vaults (afSui, vSui, haSui, stSui) use RR feed rate × SUI/USD.
209
+ */
210
+ export function resolveTokenUsdPriceFromPythPro(
211
+ params: IResolveTokenUsdPriceFromPythProParams,
212
+ ): number {
213
+ const { token, feedById, zoOracle, coins, network, legacyFeeders } = params
214
+ const coinModuleMap = buildCoinModuleToLazerFeedId(zoOracle.pythLazer, zoOracle, network)
215
+
216
+ if (LST_SUI_TOKENS.has(token)) {
217
+ const rrFeedId = LST_SUI_PYTH_LAZER_RR_FEED_IDS[token]
218
+ const rrFeed = feedById.get(rrFeedId)
219
+ if (!rrFeed) {
220
+ throw new Error(`Missing Pyth Pro RR feed ${rrFeedId} for LST token "${token}"`)
221
+ }
222
+ const suiLazerId = lookupLazerFeedIdForToken(
223
+ 'sui',
224
+ zoOracle,
225
+ coins,
226
+ network,
227
+ coinModuleMap,
228
+ legacyFeeders,
229
+ )
230
+ if (suiLazerId === undefined) {
231
+ throw new Error(`No Pyth Lazer feed id for SUI (required for LST token "${token}")`)
232
+ }
233
+ const suiFeed = feedById.get(suiLazerId)
234
+ if (!suiFeed) {
235
+ throw new Error(`Missing Pyth Pro SUI feed ${suiLazerId} for LST token "${token}"`)
236
+ }
237
+ return parsePythProFeedDecimalPrice(rrFeed) * parsePythProFeedDecimalPrice(suiFeed)
238
+ }
239
+
240
+ const lazerId = lookupLazerFeedIdForToken(
241
+ token,
242
+ zoOracle,
243
+ coins,
244
+ network,
245
+ coinModuleMap,
246
+ legacyFeeders,
247
+ )
248
+ if (lazerId === undefined) {
249
+ throw new Error(`No Pyth Lazer feed id for token "${token}"`)
250
+ }
251
+ const feed = feedById.get(lazerId)
252
+ if (!feed) {
253
+ throw new Error(`Missing Pyth Pro feed ${lazerId} for token "${token}"`)
254
+ }
255
+ return parsePythProFeedDecimalPrice(feed)
256
+ }
257
+
177
258
  function resolveStorkAssetId(symbolKey: string, config: IZoOracleSymbolConfig): string {
178
259
  return config.storkAssetId ?? symbolKey.replace('/', '')
179
260
  }
@@ -6,6 +6,8 @@ export type PythProChannel
6
6
  | 'fixed_rate@200ms'
7
7
  | 'fixed_rate@1000ms'
8
8
 
9
+ export const DEFAULT_PYTH_PRO_CHANNEL: PythProChannel = 'fixed_rate@1000ms'
10
+
9
11
  export interface PythProPriceFeed {
10
12
  priceFeedId: number
11
13
  price: string
@@ -106,7 +108,8 @@ export async function getLatestPythProPrices(
106
108
  baseUrl: string,
107
109
  options: IGetLatestPythProPricesOptions,
108
110
  ): Promise<PythProLatestPriceData> {
109
- const { priceFeedIds, channel, includeEmaPrice, includeUpdateBytes } = options
111
+ const { priceFeedIds, includeEmaPrice, includeUpdateBytes } = options
112
+ const channel = options.channel ?? DEFAULT_PYTH_PRO_CHANNEL
110
113
  if (priceFeedIds.length === 0) {
111
114
  throw new Error('priceFeedIds is required')
112
115
  }
@@ -116,9 +119,7 @@ export async function getLatestPythProPrices(
116
119
  if (priceFeedIds.length <= 20) {
117
120
  const params = new URLSearchParams()
118
121
  params.set('priceFeedIds', priceFeedIds.join(','))
119
- if (channel) {
120
- params.set('channel', channel)
121
- }
122
+ params.set('channel', channel)
122
123
  if (includeEmaPrice) {
123
124
  params.set('includeEmaPrice', 'true')
124
125
  }
@@ -280,7 +281,11 @@ export class PythProStreamClient {
280
281
  options: IPythProStreamSubscription,
281
282
  onUpdate: StreamUpdateHandler,
282
283
  ): () => void {
283
- this.subscriptions.set(options.subscriptionId, options)
284
+ const subscription = {
285
+ ...options,
286
+ channel: options.channel ?? DEFAULT_PYTH_PRO_CHANNEL,
287
+ }
288
+ this.subscriptions.set(subscription.subscriptionId, subscription)
284
289
 
285
290
  let handlers = this.updateHandlers.get(options.subscriptionId)
286
291
  if (!handlers) {
@@ -290,11 +295,11 @@ export class PythProStreamClient {
290
295
  handlers.add(onUpdate)
291
296
 
292
297
  void this.connect().then(() => {
293
- this.send({ type: 'subscribe', ...options })
298
+ this.send({ type: 'subscribe', ...subscription })
294
299
  })
295
300
 
296
301
  return () => {
297
- this.unsubscribe(options.subscriptionId, onUpdate)
302
+ this.unsubscribe(subscription.subscriptionId, onUpdate)
298
303
  }
299
304
  }
300
305
 
@@ -41,7 +41,7 @@ describe('pythProClient REST', () => {
41
41
 
42
42
  expect(data.priceFeeds).toHaveLength(1)
43
43
  expect(fetchMock).toHaveBeenCalledWith(
44
- 'http://localhost:8080/pyth-pro/latest-price?priceFeedIds=1%2C2&includeEmaPrice=true',
44
+ 'http://localhost:8080/pyth-pro/latest-price?priceFeedIds=1%2C2&channel=fixed_rate%401000ms&includeEmaPrice=true',
45
45
  )
46
46
  })
47
47