@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.
- package/dist/consts/deployments-zo-oracle-mainnet.json +1 -1
- package/dist/implementations/SLPDataAPI.cjs +31 -7
- package/dist/implementations/SLPDataAPI.cjs.map +1 -1
- package/dist/implementations/SLPDataAPI.d.cts.map +1 -1
- package/dist/implementations/SLPDataAPI.d.mts.map +1 -1
- package/dist/implementations/SLPDataAPI.mjs +33 -9
- package/dist/implementations/SLPDataAPI.mjs.map +1 -1
- package/dist/oracle.cjs +4 -1
- package/dist/oracle.cjs.map +1 -1
- package/dist/oracle.d.cts.map +1 -1
- package/dist/oracle.d.mts.map +1 -1
- package/dist/oracle.mjs +4 -1
- package/dist/oracle.mjs.map +1 -1
- package/dist/oraclePro.cjs +52 -0
- package/dist/oraclePro.cjs.map +1 -1
- package/dist/oraclePro.d.cts +19 -1
- package/dist/oraclePro.d.cts.map +1 -1
- package/dist/oraclePro.d.mts +19 -1
- package/dist/oraclePro.d.mts.map +1 -1
- package/dist/oraclePro.mjs +49 -0
- package/dist/oraclePro.mjs.map +1 -1
- package/dist/pythProClient.cjs +12 -8
- package/dist/pythProClient.cjs.map +1 -1
- package/dist/pythProClient.d.cts +1 -0
- package/dist/pythProClient.d.cts.map +1 -1
- package/dist/pythProClient.d.mts +1 -0
- package/dist/pythProClient.d.mts.map +1 -1
- package/dist/pythProClient.mjs +11 -7
- package/dist/pythProClient.mjs.map +1 -1
- package/package.json +1 -1
- package/src/consts/deployments-zo-oracle-mainnet.json +1 -1
- package/src/implementations/SLPDataAPI.ts +42 -10
- package/src/oracle.ts +4 -1
- package/src/oraclePro.ts +82 -1
- package/src/pythProClient.ts +12 -7
- 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 {
|
|
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 = (
|
|
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 = (
|
|
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
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
}
|
package/src/pythProClient.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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', ...
|
|
298
|
+
this.send({ type: 'subscribe', ...subscription })
|
|
294
299
|
})
|
|
295
300
|
|
|
296
301
|
return () => {
|
|
297
|
-
this.unsubscribe(
|
|
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
|
|