@zofai/zo-sdk 0.2.26 → 0.2.28

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 (54) hide show
  1. package/dist/consts/deployments-slp-mainnet.json +1 -1
  2. package/dist/consts/deployments-zo-oracle-mainnet.json +9 -0
  3. package/dist/implementations/SLPAPI.cjs +424 -209
  4. package/dist/implementations/SLPAPI.cjs.map +1 -1
  5. package/dist/implementations/SLPAPI.d.cts +19 -2
  6. package/dist/implementations/SLPAPI.d.cts.map +1 -1
  7. package/dist/implementations/SLPAPI.d.mts +19 -2
  8. package/dist/implementations/SLPAPI.d.mts.map +1 -1
  9. package/dist/implementations/SLPAPI.mjs +425 -210
  10. package/dist/implementations/SLPAPI.mjs.map +1 -1
  11. package/dist/implementations/SLPDataAPI.cjs +69 -7
  12. package/dist/implementations/SLPDataAPI.cjs.map +1 -1
  13. package/dist/implementations/SLPDataAPI.d.cts +10 -0
  14. package/dist/implementations/SLPDataAPI.d.cts.map +1 -1
  15. package/dist/implementations/SLPDataAPI.d.mts +10 -0
  16. package/dist/implementations/SLPDataAPI.d.mts.map +1 -1
  17. package/dist/implementations/SLPDataAPI.mjs +70 -8
  18. package/dist/implementations/SLPDataAPI.mjs.map +1 -1
  19. package/dist/interfaces/slp.d.cts +16 -0
  20. package/dist/interfaces/slp.d.cts.map +1 -1
  21. package/dist/interfaces/slp.d.mts +16 -0
  22. package/dist/interfaces/slp.d.mts.map +1 -1
  23. package/dist/oracle.cjs +4 -1
  24. package/dist/oracle.cjs.map +1 -1
  25. package/dist/oracle.d.cts.map +1 -1
  26. package/dist/oracle.d.mts.map +1 -1
  27. package/dist/oracle.mjs +4 -1
  28. package/dist/oracle.mjs.map +1 -1
  29. package/dist/oraclePro.cjs +145 -8
  30. package/dist/oraclePro.cjs.map +1 -1
  31. package/dist/oraclePro.d.cts +57 -1
  32. package/dist/oraclePro.d.cts.map +1 -1
  33. package/dist/oraclePro.d.mts +57 -1
  34. package/dist/oraclePro.d.mts.map +1 -1
  35. package/dist/oraclePro.mjs +138 -8
  36. package/dist/oraclePro.mjs.map +1 -1
  37. package/dist/pythProClient.cjs +12 -8
  38. package/dist/pythProClient.cjs.map +1 -1
  39. package/dist/pythProClient.d.cts +1 -0
  40. package/dist/pythProClient.d.cts.map +1 -1
  41. package/dist/pythProClient.d.mts +1 -0
  42. package/dist/pythProClient.d.mts.map +1 -1
  43. package/dist/pythProClient.mjs +11 -7
  44. package/dist/pythProClient.mjs.map +1 -1
  45. package/package.json +1 -1
  46. package/src/consts/deployments-slp-mainnet.json +1 -1
  47. package/src/consts/deployments-zo-oracle-mainnet.json +9 -0
  48. package/src/implementations/SLPAPI.ts +723 -239
  49. package/src/implementations/SLPDataAPI.ts +83 -9
  50. package/src/interfaces/slp.ts +170 -0
  51. package/src/oracle.ts +4 -1
  52. package/src/oraclePro.ts +229 -10
  53. package/src/pythProClient.ts +12 -7
  54. 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,
@@ -38,7 +38,14 @@ import type {
38
38
  ISLPVaultInfo,
39
39
  ISwapFeeBreakdown,
40
40
  } from '../interfaces'
41
+ import type { OracleInputs } from '../oraclePro'
41
42
  import type { DynamicFieldInfo, SuiClient } from '../suiClient'
43
+ import {
44
+ buildPythProPriceFeedMap,
45
+ resolveTokenUsdPriceFromPythPro,
46
+ valuateSymbolsV3,
47
+ valuateVaultsV3,
48
+ } from '../oraclePro'
42
49
  import { joinSymbol, parseSymbolKey, parseValue, suiSymbolToSymbol } from '../utils'
43
50
 
44
51
  export interface GetCumulativeAprResponse {
@@ -210,6 +217,46 @@ export class SLPDataAPI extends BaseDataAPI implements ISLPDataAPI {
210
217
  return { vaultsValuation, symbolsValuation }
211
218
  }
212
219
 
220
+ /** Creates vaults valuation using valuate_vault_v3 (Pyth Pro + Stork, sudo-core). */
221
+ public valuateVaultsV3(tx: Transaction, oracle: OracleInputs) {
222
+ if (!this.consts.sudoCore) {
223
+ throw new Error('Sudo Core configuration not found. Make sure you are using LPToken.SLP')
224
+ }
225
+ return valuateVaultsV3({
226
+ tx,
227
+ upgradedPackage: this.consts.sudoCore.upgradedPackage,
228
+ lpType: `${this.consts.sudoCore.package}::slp::SLP`,
229
+ market: this.consts.sudoCore.market,
230
+ vaults: this.consts.sudoCore.vaults,
231
+ coinModules: this.consts.coins,
232
+ oracle,
233
+ })
234
+ }
235
+
236
+ /** Creates symbols valuation using valuate_symbol_v3 (Pyth Pro + Stork, sudo-core). */
237
+ public valuateSymbolsV3(tx: Transaction, oracle: OracleInputs) {
238
+ if (!this.consts.sudoCore) {
239
+ throw new Error('Sudo Core configuration not found. Make sure you are using LPToken.SLP')
240
+ }
241
+ return valuateSymbolsV3({
242
+ tx,
243
+ upgradedPackage: this.consts.sudoCore.upgradedPackage,
244
+ marketPackage: this.consts.sudoCore.package,
245
+ lpType: `${this.consts.sudoCore.package}::slp::SLP`,
246
+ market: this.consts.sudoCore.market,
247
+ symbols: this.consts.sudoCore.symbols,
248
+ coinModules: this.consts.coins,
249
+ oracle,
250
+ })
251
+ }
252
+
253
+ /** Creates both vaults and symbols valuation (Pyth Pro + Stork, sudo-core). */
254
+ public valuateV3(tx: Transaction, oracle: OracleInputs) {
255
+ const vaultsValuation = this.valuateVaultsV3(tx, oracle)
256
+ const symbolsValuation = this.valuateSymbolsV3(tx, oracle)
257
+ return { vaultsValuation, symbolsValuation }
258
+ }
259
+
213
260
  /**
214
261
  * Valuates the SLP market using Sudo SDK's approach
215
262
  */
@@ -219,11 +266,29 @@ export class SLPDataAPI extends BaseDataAPI implements ISLPDataAPI {
219
266
  let value = 0
220
267
 
221
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
+
222
287
  const vaultData = await Promise.all(vaultKeys.map(async (vault) => {
223
288
  const vaultInfo = await this.getVaultInfo(vault)
224
289
  const reservingFeeDelta = SLPDataAPI.calculateVaultReservingFee(vaultInfo, vaultInfo.reservingFeeModel, Date.now() / 1000)
225
290
  const totalVaultAmount = reservingFeeDelta + vaultInfo.liquidity + vaultInfo.reservedAmount
226
- const oraclePrice = (await this.getOraclePrice(vault)).getPriceUnchecked().getPriceAsNumberUnchecked()
291
+ const oraclePrice = resolvePrice(vault)
227
292
  const { decimals } = this.consts.coins[vault]
228
293
  const vaultValue = totalVaultAmount * oraclePrice / (10 ** decimals)
229
294
  return { vault, oraclePrice, vaultValue }
@@ -234,7 +299,7 @@ export class SLPDataAPI extends BaseDataAPI implements ISLPDataAPI {
234
299
  const symbolPromises = Object.keys(this.consts.sudoCore.symbols).map(async (symbol) => {
235
300
  const [direction, tokenId] = parseSymbolKey(symbol)
236
301
  const symbolInfo = await this.getSymbolInfo(tokenId, direction === 'long')
237
- const price = (await this.getOraclePrice(tokenId)).getPriceUnchecked().getPriceAsNumberUnchecked()
302
+ const price = resolvePrice(tokenId)
238
303
 
239
304
  const oiState = await this.getSymbolOiFundingState(tokenId)
240
305
  const pairedInfo = await this.getSymbolInfo(tokenId, direction !== 'long')
@@ -351,13 +416,22 @@ export class SLPDataAPI extends BaseDataAPI implements ISLPDataAPI {
351
416
  const slpPrice = value / marketInfo.lpSupplyWithDecimals
352
417
 
353
418
  const vaultKeys = Object.keys(this.consts.sudoCore.vaults)
354
- const vaultPricesEntries = await Promise.all(
355
- vaultKeys.map(async (vault) => {
356
- const oraclePrice = (await this.getOraclePrice(vault)).getPriceUnchecked().getPriceAsNumberUnchecked()
357
- return [vault, oraclePrice] as const
358
- }),
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
+ ]),
359
434
  )
360
- const vaultPrices = Object.fromEntries(vaultPricesEntries)
361
435
 
362
436
  return {
363
437
  marketCap: value,
@@ -6,6 +6,7 @@
6
6
  import type { KioskClient, KioskOwnerCap } from '@mysten/kiosk'
7
7
  import type { Transaction, TransactionObjectArgument } from '@mysten/sui/transactions'
8
8
 
9
+ import type { OracleInputs } from '../oraclePro'
9
10
  import type {
10
11
  IBaseAPI,
11
12
  IBaseCredential,
@@ -217,6 +218,12 @@ export interface ISLPDataAPI extends IBaseDataAPI {
217
218
  getSymbolConfig: (indexToken: string, long: boolean) => Promise<ISLPSymbolConfig | null>
218
219
  getPriceImpactConfig: (indexToken: string) => Promise<ISLPPriceImpactConfig | null>
219
220
  calculateSwapFeeBreakdown: (fromToken: string, toToken: string, fromAmount: number) => Promise<ISwapFeeBreakdown>
221
+ valuateVaultsV3: (tx: Transaction, oracle: OracleInputs) => TransactionObjectArgument
222
+ valuateSymbolsV3: (tx: Transaction, oracle: OracleInputs) => TransactionObjectArgument
223
+ valuateV3: (tx: Transaction, oracle: OracleInputs) => {
224
+ vaultsValuation: TransactionObjectArgument
225
+ symbolsValuation: TransactionObjectArgument
226
+ }
220
227
  }
221
228
 
222
229
  // SLP-specific API interface
@@ -286,4 +293,167 @@ export interface ISLPAPI extends IBaseAPI {
286
293
  minAmountOut?: number,
287
294
  tx?: Transaction,
288
295
  ) => Promise<TransactionObjectArgument>
296
+
297
+ swapV4: (
298
+ fromToken: string,
299
+ toToken: string,
300
+ fromAmount: bigint,
301
+ fromCoinObjects: string[],
302
+ pythProUpdateBytes: Uint8Array | number[],
303
+ minAmountOut?: number,
304
+ tx?: Transaction,
305
+ ) => Promise<Transaction>
306
+
307
+ swapV4Ptb: (
308
+ fromToken: string,
309
+ toToken: string,
310
+ fromAmount: bigint,
311
+ fromCoinObjects: string[],
312
+ pythProUpdateBytes: Uint8Array | number[],
313
+ minAmountOut?: number,
314
+ tx?: Transaction,
315
+ ) => Promise<TransactionObjectArgument>
316
+
317
+ openPositionV3: (
318
+ collateralToken: string,
319
+ indexToken: string,
320
+ size: bigint,
321
+ collateralAmount: bigint,
322
+ coinObjects: string[],
323
+ long: boolean,
324
+ reserveAmount: bigint,
325
+ indexPrice: number,
326
+ collateralPrice: number,
327
+ pythProUpdateBytes: Uint8Array | number[],
328
+ isLimitOrder?: boolean,
329
+ isIocOrder?: boolean,
330
+ pricesSlippage?: number,
331
+ collateralSlippage?: number,
332
+ relayerFee?: bigint,
333
+ referralAddress?: string,
334
+ sender?: string,
335
+ sponsoredTx?: boolean,
336
+ ) => Promise<Transaction>
337
+
338
+ openPositionWithSCardV3: (
339
+ collateralToken: string,
340
+ indexToken: string,
341
+ size: bigint,
342
+ collateralAmount: bigint,
343
+ coinObjects: string[],
344
+ long: boolean,
345
+ reserveAmount: bigint,
346
+ indexPrice: number,
347
+ collateralPrice: number,
348
+ kioskClient: KioskClient,
349
+ kioskCap: KioskOwnerCap,
350
+ scard: string,
351
+ pythProUpdateBytes: Uint8Array | number[],
352
+ isLimitOrder?: boolean,
353
+ isIocOrder?: boolean,
354
+ pricesSlippage?: number,
355
+ collateralSlippage?: number,
356
+ relayerFee?: bigint,
357
+ referralAddress?: string,
358
+ sender?: string,
359
+ sponsoredTx?: boolean,
360
+ ) => Promise<Transaction>
361
+
362
+ openPositionWithCoinV3: (
363
+ collateralToken: string,
364
+ indexToken: string,
365
+ size: bigint,
366
+ coinObj: TransactionObjectArgument,
367
+ long: boolean,
368
+ reserveAmount: bigint,
369
+ indexPrice: number,
370
+ collateralPrice: number,
371
+ pythProUpdateBytes: Uint8Array | number[],
372
+ isLimitOrder?: boolean,
373
+ isIocOrder?: boolean,
374
+ pricesSlippage?: number,
375
+ collateralSlippage?: number,
376
+ relayerFee?: bigint,
377
+ referralAddress?: string,
378
+ sender?: string,
379
+ tx?: Transaction,
380
+ sponsoredTx?: boolean,
381
+ ) => Promise<Transaction>
382
+
383
+ openPositionWithCoinAndSCardV3: (
384
+ collateralToken: string,
385
+ indexToken: string,
386
+ size: bigint,
387
+ coinObj: TransactionObjectArgument,
388
+ long: boolean,
389
+ reserveAmount: bigint,
390
+ indexPrice: number,
391
+ collateralPrice: number,
392
+ kioskClient: KioskClient,
393
+ kioskCap: KioskOwnerCap,
394
+ scard: string,
395
+ pythProUpdateBytes: Uint8Array | number[],
396
+ isLimitOrder?: boolean,
397
+ isIocOrder?: boolean,
398
+ pricesSlippage?: number,
399
+ collateralSlippage?: number,
400
+ relayerFee?: bigint,
401
+ referralAddress?: string,
402
+ sender?: string,
403
+ tx?: Transaction,
404
+ sponsoredTx?: boolean,
405
+ ) => Promise<Transaction>
406
+
407
+ decreasePositionV3: (
408
+ pcpId: string,
409
+ collateralToken: string,
410
+ indexToken: string,
411
+ amount: bigint,
412
+ long: boolean,
413
+ indexPrice: number,
414
+ collateralPrice: number,
415
+ pythProUpdateBytes: Uint8Array | number[],
416
+ isTriggerOrder?: boolean,
417
+ isTakeProfitOrder?: boolean,
418
+ isIocOrder?: boolean,
419
+ pricesSlippage?: number,
420
+ collateralSlippage?: number,
421
+ relayerFee?: bigint,
422
+ coinObjects?: string[],
423
+ sponsoredTx?: boolean,
424
+ sender?: string,
425
+ ) => Promise<Transaction>
426
+
427
+ decreasePositionWithSCardV3: (
428
+ pcpId: string,
429
+ collateralToken: string,
430
+ indexToken: string,
431
+ amount: bigint,
432
+ long: boolean,
433
+ indexPrice: number,
434
+ collateralPrice: number,
435
+ kioskClient: KioskClient,
436
+ kioskCap: KioskOwnerCap,
437
+ scard: string,
438
+ pythProUpdateBytes: Uint8Array | number[],
439
+ isTriggerOrder?: boolean,
440
+ isTakeProfitOrder?: boolean,
441
+ isIocOrder?: boolean,
442
+ pricesSlippage?: number,
443
+ collateralSlippage?: number,
444
+ relayerFee?: bigint,
445
+ coinObjects?: string[],
446
+ sponsoredTx?: boolean,
447
+ sender?: string,
448
+ ) => Promise<Transaction>
449
+
450
+ redeemFromPositionV3: (
451
+ pcpId: string,
452
+ collateralToken: string,
453
+ indexToken: string,
454
+ amount: number,
455
+ long: boolean,
456
+ pythProUpdateBytes: Uint8Array | number[],
457
+ tx?: Transaction,
458
+ ) => Promise<Transaction>
289
459
  }
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'
@@ -60,6 +60,44 @@ export function requireOracleProConfig(network: Network): IOracleProConfig {
60
60
  return buildOracleProConfig(getZoOracleConfig(network))
61
61
  }
62
62
 
63
+ /**
64
+ * LST SUI vault tokens priced on-chain via Pyth Lazer RR feed × SUI/USD.
65
+ * Numeric ids are Pyth Lazer `priceFeedIds` (not Hermes price ids).
66
+ */
67
+ export const LST_SUI_PYTH_LAZER_RR_FEED_IDS: Record<string, number> = {
68
+ afSui: 3245,
69
+ stSui: 736,
70
+ vSui: 2349,
71
+ haSui: 3244,
72
+ }
73
+
74
+ const LST_SUI_TOKENS = new Set(Object.keys(LST_SUI_PYTH_LAZER_RR_FEED_IDS))
75
+
76
+ function lookupLazerFeedIdForToken(
77
+ token: string,
78
+ zoOracle: IZoOracleConfig,
79
+ coins: Record<string, { module: string }>,
80
+ network: Network,
81
+ coinModuleMap: Map<string, number>,
82
+ legacyFeeders?: Record<string, string>,
83
+ ): number | undefined {
84
+ const module = coins[token]?.module
85
+ if (!module) {
86
+ return undefined
87
+ }
88
+
89
+ let lazerId = coinModuleMap.get(normalizeCoinType(module))
90
+
91
+ if (lazerId === undefined && legacyFeeders?.[token]) {
92
+ const priceId = getPythFeederToPriceId(network)[legacyFeeders[token]]
93
+ if (priceId) {
94
+ lazerId = zoOracle.pythLazer.feedIds[priceId]
95
+ }
96
+ }
97
+
98
+ return lazerId
99
+ }
100
+
63
101
  /**
64
102
  * Build coin module -> Lazer feed id using zo_oracle symbol configs + Hermes price id map.
65
103
  */
@@ -93,9 +131,7 @@ export function resolveLazerFeedIds(
93
131
  network: Network,
94
132
  legacyFeeders?: Record<string, string>,
95
133
  ): number[] {
96
- const { pythLazer } = zoOracle
97
- const coinModuleMap = buildCoinModuleToLazerFeedId(pythLazer, zoOracle, network)
98
- const feederToPriceId = getPythFeederToPriceId(network)
134
+ const coinModuleMap = buildCoinModuleToLazerFeedId(zoOracle.pythLazer, zoOracle, network)
99
135
 
100
136
  const ids = new Set<number>()
101
137
  for (const token of tokens) {
@@ -104,15 +140,32 @@ export function resolveLazerFeedIds(
104
140
  throw new Error(`Unknown token: ${token}`)
105
141
  }
106
142
 
107
- let lazerId = coinModuleMap.get(normalizeCoinType(module))
108
-
109
- if (lazerId === undefined && legacyFeeders?.[token]) {
110
- const priceId = feederToPriceId[legacyFeeders[token]]
111
- if (priceId) {
112
- lazerId = pythLazer.feedIds[priceId]
143
+ if (LST_SUI_TOKENS.has(token)) {
144
+ ids.add(LST_SUI_PYTH_LAZER_RR_FEED_IDS[token])
145
+ const suiLazerId = lookupLazerFeedIdForToken(
146
+ 'sui',
147
+ zoOracle,
148
+ coins,
149
+ network,
150
+ coinModuleMap,
151
+ legacyFeeders,
152
+ )
153
+ if (suiLazerId === undefined) {
154
+ throw new Error(`No Pyth Lazer feed id for SUI (required for LST token "${token}")`)
113
155
  }
156
+ ids.add(suiLazerId)
157
+ continue
114
158
  }
115
159
 
160
+ const lazerId = lookupLazerFeedIdForToken(
161
+ token,
162
+ zoOracle,
163
+ coins,
164
+ network,
165
+ coinModuleMap,
166
+ legacyFeeders,
167
+ )
168
+
116
169
  if (lazerId === undefined) {
117
170
  throw new Error(`No Pyth Lazer feed id for token "${token}" (${module})`)
118
171
  }
@@ -121,6 +174,87 @@ export function resolveLazerFeedIds(
121
174
  return [...ids]
122
175
  }
123
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
+
124
258
  function resolveStorkAssetId(symbolKey: string, config: IZoOracleSymbolConfig): string {
125
259
  return config.storkAssetId ?? symbolKey.replace('/', '')
126
260
  }
@@ -212,6 +346,11 @@ export function oracleArgsAlt(oracle: OracleInputsAlt): TransactionObjectArgumen
212
346
  return [oracle.oracleRegistry, oracle.storkState, oracle.pythProUpdate]
213
347
  }
214
348
 
349
+ /** Sudo-core valuation/swap order: `[oracleRegistry, pythProUpdate, storkState, clock]`. */
350
+ export function oracleArgsRegistryFirst(oracle: OracleInputs): TransactionObjectArgument[] {
351
+ return [oracle.oracleRegistry, oracle.pythProUpdate, oracle.storkState, oracle.clock]
352
+ }
353
+
215
354
  export function buildOracleInputsAlt(oracle: OracleInputs): OracleInputsAlt {
216
355
  return {
217
356
  oracleRegistry: oracle.oracleRegistry,
@@ -312,3 +451,83 @@ export function valuateSymbolsV2(params: IValuateSymbolsV2Params): TransactionOb
312
451
 
313
452
  return symbolsValuation
314
453
  }
454
+
455
+ export interface IValuateVaultsV3Params {
456
+ tx: Transaction
457
+ upgradedPackage: string
458
+ lpType: string
459
+ market: string
460
+ vaults: Record<string, { reservingFeeModel: string }>
461
+ coinModules: Record<string, { module: string }>
462
+ oracle: OracleInputs
463
+ }
464
+
465
+ /** create_vaults_valuation + valuate_vault_v3 for each vault (sudo-core). */
466
+ export function valuateVaultsV3(params: IValuateVaultsV3Params): TransactionObjectArgument {
467
+ const { tx, upgradedPackage, lpType, market, vaults, coinModules, oracle } = params
468
+
469
+ const vaultsValuation = tx.moveCall({
470
+ target: `${upgradedPackage}::market::create_vaults_valuation`,
471
+ typeArguments: [lpType],
472
+ arguments: [oracle.clock, tx.object(market)],
473
+ })
474
+
475
+ for (const key of Object.keys(vaults)) {
476
+ const vault = vaults[key]
477
+ tx.moveCall({
478
+ target: `${upgradedPackage}::market::valuate_vault_v3`,
479
+ typeArguments: [lpType, coinModules[key].module],
480
+ arguments: [
481
+ tx.object(market),
482
+ tx.object(vault.reservingFeeModel),
483
+ ...oracleArgsRegistryFirst(oracle),
484
+ vaultsValuation,
485
+ ],
486
+ })
487
+ }
488
+
489
+ return vaultsValuation
490
+ }
491
+
492
+ export interface IValuateSymbolsV3Params {
493
+ tx: Transaction
494
+ upgradedPackage: string
495
+ marketPackage: string
496
+ lpType: string
497
+ market: string
498
+ symbols: Record<string, { fundingFeeModel: string }>
499
+ coinModules: Record<string, { module: string }>
500
+ oracle: OracleInputs
501
+ }
502
+
503
+ /** create_symbols_valuation + valuate_symbol_v3 for each symbol (sudo-core). */
504
+ export function valuateSymbolsV3(params: IValuateSymbolsV3Params): TransactionObjectArgument {
505
+ const { tx, upgradedPackage, marketPackage, lpType, market, symbols, coinModules, oracle } = params
506
+
507
+ const symbolsValuation = tx.moveCall({
508
+ target: `${upgradedPackage}::market::create_symbols_valuation`,
509
+ typeArguments: [lpType],
510
+ arguments: [oracle.clock, tx.object(market)],
511
+ })
512
+
513
+ for (const key of Object.keys(symbols)) {
514
+ const [direction, token] = parseSymbolKey(key)
515
+ const symbol = symbols[key]
516
+ tx.moveCall({
517
+ target: `${upgradedPackage}::market::valuate_symbol_v3`,
518
+ typeArguments: [
519
+ lpType,
520
+ coinModules[token].module,
521
+ `${marketPackage}::market::${direction.toUpperCase()}`,
522
+ ],
523
+ arguments: [
524
+ tx.object(market),
525
+ tx.object(symbol.fundingFeeModel),
526
+ ...oracleArgsRegistryFirst(oracle),
527
+ symbolsValuation,
528
+ ],
529
+ })
530
+ }
531
+
532
+ return symbolsValuation
533
+ }
@@ -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