@zofai/zo-sdk 0.1.93 → 0.1.94

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-usdz-mainnet.json +1 -1
  3. package/dist/consts/deployments-zlp-mainnet.json +1 -1
  4. package/dist/implementations/SLPDataAPI.cjs +212 -47
  5. package/dist/implementations/SLPDataAPI.cjs.map +1 -1
  6. package/dist/implementations/SLPDataAPI.d.cts +8 -1
  7. package/dist/implementations/SLPDataAPI.d.cts.map +1 -1
  8. package/dist/implementations/SLPDataAPI.d.mts +8 -1
  9. package/dist/implementations/SLPDataAPI.d.mts.map +1 -1
  10. package/dist/implementations/SLPDataAPI.mjs +212 -47
  11. package/dist/implementations/SLPDataAPI.mjs.map +1 -1
  12. package/dist/implementations/USDZDataAPI.cjs +197 -44
  13. package/dist/implementations/USDZDataAPI.cjs.map +1 -1
  14. package/dist/implementations/USDZDataAPI.d.cts +8 -1
  15. package/dist/implementations/USDZDataAPI.d.cts.map +1 -1
  16. package/dist/implementations/USDZDataAPI.d.mts +8 -1
  17. package/dist/implementations/USDZDataAPI.d.mts.map +1 -1
  18. package/dist/implementations/USDZDataAPI.mjs +197 -44
  19. package/dist/implementations/USDZDataAPI.mjs.map +1 -1
  20. package/dist/implementations/ZLPDataAPI.cjs +200 -46
  21. package/dist/implementations/ZLPDataAPI.cjs.map +1 -1
  22. package/dist/implementations/ZLPDataAPI.d.cts +8 -1
  23. package/dist/implementations/ZLPDataAPI.d.cts.map +1 -1
  24. package/dist/implementations/ZLPDataAPI.d.mts +8 -1
  25. package/dist/implementations/ZLPDataAPI.d.mts.map +1 -1
  26. package/dist/implementations/ZLPDataAPI.mjs +200 -46
  27. package/dist/implementations/ZLPDataAPI.mjs.map +1 -1
  28. package/dist/interfaces/base.d.cts +22 -0
  29. package/dist/interfaces/base.d.cts.map +1 -1
  30. package/dist/interfaces/base.d.mts +22 -0
  31. package/dist/interfaces/base.d.mts.map +1 -1
  32. package/dist/interfaces/slp.d.cts +6 -1
  33. package/dist/interfaces/slp.d.cts.map +1 -1
  34. package/dist/interfaces/slp.d.mts +6 -1
  35. package/dist/interfaces/slp.d.mts.map +1 -1
  36. package/dist/interfaces/usdz.d.cts +6 -1
  37. package/dist/interfaces/usdz.d.cts.map +1 -1
  38. package/dist/interfaces/usdz.d.mts +6 -1
  39. package/dist/interfaces/usdz.d.mts.map +1 -1
  40. package/dist/interfaces/zlp.d.cts +6 -1
  41. package/dist/interfaces/zlp.d.cts.map +1 -1
  42. package/dist/interfaces/zlp.d.mts +6 -1
  43. package/dist/interfaces/zlp.d.mts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/consts/deployments-slp-mainnet.json +1 -1
  46. package/src/consts/deployments-usdz-mainnet.json +1 -1
  47. package/src/consts/deployments-zlp-mainnet.json +1 -1
  48. package/src/implementations/SLPDataAPI.ts +235 -19
  49. package/src/implementations/USDZDataAPI.ts +221 -16
  50. package/src/implementations/ZLPDataAPI.ts +222 -17
  51. package/src/interfaces/base.ts +26 -0
  52. package/src/interfaces/slp.ts +6 -0
  53. package/src/interfaces/usdz.ts +6 -0
  54. package/src/interfaces/zlp.ts +6 -0
@@ -15,6 +15,7 @@ import type {
15
15
  IBaseHistoryResponse,
16
16
  IBaseOrderType,
17
17
  IBaseStaked,
18
+ ISwapFeeBreakdown,
18
19
  IUSDZCredential,
19
20
  IUSDZDataAPI,
20
21
  IUSDZFundingFeeModel,
@@ -37,6 +38,20 @@ import type {
37
38
  } from '../interfaces'
38
39
  import { joinSymbol, parseSymbolKey, parseValue, suiSymbolToSymbol } from '../utils'
39
40
 
41
+ interface SwapImpactConfig {
42
+ id: string
43
+ enabled: boolean
44
+ impactMultiplier: number
45
+ maxImpactRate: number
46
+ }
47
+
48
+ interface EmaVolatilityFeeConfig {
49
+ id: string
50
+ enabled: boolean
51
+ multiplier: number
52
+ maxFeeRate: number
53
+ }
54
+
40
55
  export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
41
56
  constructor(
42
57
  network: Network,
@@ -226,7 +241,7 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
226
241
  (await this.getOraclePrice(tokenId)).getPriceUnchecked().getPriceAsNumberUnchecked(),
227
242
  marketInfo.lpSupplyWithDecimals,
228
243
  Date.now() / 1000,
229
- oiState && oiState.enabled ? oiState.model : undefined,
244
+ oiState && oiState.enabled ? oiState : undefined,
230
245
  pairedSymbol.openingSize,
231
246
  )
232
247
 
@@ -385,6 +400,164 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
385
400
  }
386
401
  }
387
402
 
403
+ public async calculateSwapFeeBreakdown(fromToken: string, toToken: string, fromAmount: number): Promise<ISwapFeeBreakdown> {
404
+ const timestamp = Date.now() / 1000
405
+
406
+ const fromDecimals = this.consts.coins[fromToken]?.decimals
407
+ const toDecimals = this.consts.coins[toToken]?.decimals
408
+ if (fromDecimals === undefined || toDecimals === undefined) {
409
+ throw new Error(`Unknown token decimals for swap: ${fromToken} -> ${toToken}`)
410
+ }
411
+
412
+ const fromFeed = await this.getOraclePrice(fromToken)
413
+ const toFeed = await this.getOraclePrice(toToken)
414
+ const fromPrice = fromFeed.getPriceUnchecked().getPriceAsNumberUnchecked()
415
+ const toPrice = toFeed.getPriceUnchecked().getPriceAsNumberUnchecked()
416
+
417
+ const swapValue = (fromAmount * fromPrice) / (10 ** fromDecimals)
418
+ const totalVaultsValue = await this.#getTotalVaultsValueUsd(timestamp)
419
+
420
+ const rebaseFeeInRate = await this.rebaseFeeRate(fromToken, true, fromAmount)
421
+ const rebaseFeeInValue = swapValue * rebaseFeeInRate
422
+
423
+ const estimatedToAmount = toPrice !== 0
424
+ ? (swapValue * (10 ** toDecimals)) / toPrice
425
+ : 0
426
+ const rebaseFeeOutRate = await this.rebaseFeeRate(toToken, false, estimatedToAmount)
427
+ const rebaseFeeOutValue = swapValue * rebaseFeeOutRate
428
+
429
+ const swapImpactCfg = await this.#getSwapImpactConfig()
430
+ const swapImpactFeeValue = swapImpactCfg?.enabled
431
+ ? USDZDataAPI.#computeSwapImpactFeeValue(swapValue, totalVaultsValue, swapImpactCfg.impactMultiplier, swapImpactCfg.maxImpactRate)
432
+ : 0
433
+
434
+ const emaCfg = await this.#getEmaVolatilityFeeConfig()
435
+ const emaVolatilityFeeValue = emaCfg?.enabled
436
+ ? USDZDataAPI.#computeEmaVolatilityFeeValue(
437
+ swapValue,
438
+ USDZDataAPI.#maxEmaDivergenceRate(fromFeed, toFeed),
439
+ emaCfg.multiplier,
440
+ emaCfg.maxFeeRate,
441
+ )
442
+ : 0
443
+
444
+ const totalFeeValue = rebaseFeeInValue + rebaseFeeOutValue + swapImpactFeeValue + emaVolatilityFeeValue
445
+ const totalFeeRate = swapValue !== 0 ? totalFeeValue / swapValue : 0
446
+
447
+ return {
448
+ swapValue,
449
+ totalVaultsValue,
450
+ rebaseFeeInRate,
451
+ rebaseFeeOutRate,
452
+ rebaseFeeInValue,
453
+ rebaseFeeOutValue,
454
+ swapImpactFeeValue,
455
+ emaVolatilityFeeValue,
456
+ totalFeeValue,
457
+ totalFeeRate,
458
+ }
459
+ }
460
+
461
+ async #getTotalVaultsValueUsd(timestamp: number): Promise<number> {
462
+ const vaultKeys = Object.keys(this.consts.zoCore.vaults)
463
+ const vaultValues = await Promise.all(vaultKeys.map(async (vault) => {
464
+ const vaultInfo = await this.getVaultInfo(vault)
465
+ const reservingFeeDelta = USDZDataAPI.calculateVaultReservingFee(vaultInfo, vaultInfo.reservingFeeModel, timestamp)
466
+ const totalVaultAmount = reservingFeeDelta + vaultInfo.liquidity + vaultInfo.reservedAmount
467
+ const oraclePrice = (await this.getOraclePrice(vault)).getPriceUnchecked().getPriceAsNumberUnchecked()
468
+ const { decimals } = this.consts.coins[vault]
469
+ return totalVaultAmount * oraclePrice / (10 ** decimals)
470
+ }))
471
+ return vaultValues.reduce((acc, curr) => acc + curr, 0)
472
+ }
473
+
474
+ async #getSwapImpactConfig(): Promise<SwapImpactConfig | null> {
475
+ const raw = await this.#getMarketDynamicFieldObjectByKeySuffix(this.consts.zoCore.market, 'SwapImpactConfigKey')
476
+ if (!raw)
477
+ return null
478
+ return USDZDataAPI.#parseSwapImpactConfig(raw)
479
+ }
480
+
481
+ async #getEmaVolatilityFeeConfig(): Promise<EmaVolatilityFeeConfig | null> {
482
+ const raw = await this.#getMarketDynamicFieldObjectByKeySuffix(this.consts.zoCore.market, 'EmaVolatilityFeeConfigKey')
483
+ if (!raw)
484
+ return null
485
+ return USDZDataAPI.#parseEmaVolatilityFeeConfig(raw)
486
+ }
487
+
488
+ async #getMarketDynamicFieldObjectByKeySuffix(parentId: string, keyTypeSuffix: string): Promise<any | null> {
489
+ let cursor: string | null | undefined
490
+ let hasNextPage = true
491
+
492
+ while (hasNextPage) {
493
+ const page = await this.provider.getDynamicFields({ parentId, cursor })
494
+ for (const field of page.data) {
495
+ const type = (field.name as any)?.type
496
+ if (typeof type === 'string' && type.endsWith(`::${keyTypeSuffix}`)) {
497
+ return await this.provider.getDynamicFieldObject({
498
+ parentId,
499
+ name: field.name as any,
500
+ })
501
+ }
502
+ }
503
+ hasNextPage = page.hasNextPage
504
+ cursor = page.nextCursor
505
+ }
506
+ return null
507
+ }
508
+
509
+ static #parseSwapImpactConfig(raw: any): SwapImpactConfig {
510
+ const { fields } = raw.data.content
511
+ return {
512
+ id: fields.id.id,
513
+ enabled: fields.enabled,
514
+ impactMultiplier: parseValue(fields.impact_multiplier),
515
+ maxImpactRate: parseValue(fields.max_impact_rate),
516
+ }
517
+ }
518
+
519
+ static #parseEmaVolatilityFeeConfig(raw: any): EmaVolatilityFeeConfig {
520
+ const { fields } = raw.data.content
521
+ return {
522
+ id: fields.id.id,
523
+ enabled: fields.enabled,
524
+ multiplier: parseValue(fields.multiplier),
525
+ maxFeeRate: parseValue(fields.max_fee_rate),
526
+ }
527
+ }
528
+
529
+ static #computeSwapImpactFeeValue(swapValue: number, totalVaultsValue: number, impactMultiplier: number, maxImpactRate: number): number {
530
+ if (swapValue <= 0 || totalVaultsValue <= 0)
531
+ return 0
532
+ const utilization = swapValue / totalVaultsValue
533
+ const rawImpactRate = impactMultiplier * utilization
534
+ const impactRate = Math.min(rawImpactRate, maxImpactRate)
535
+ return swapValue * impactRate
536
+ }
537
+
538
+ static #computeEmaVolatilityFeeValue(swapValue: number, maxDiv: number, multiplier: number, maxFeeRate: number): number {
539
+ if (swapValue <= 0 || maxDiv <= 0)
540
+ return 0
541
+ const rawFeeRate = multiplier * maxDiv
542
+ const feeRate = Math.min(rawFeeRate, maxFeeRate)
543
+ return swapValue * feeRate
544
+ }
545
+
546
+ static #maxEmaDivergenceRate(sourceFeed: any, destFeed: any): number {
547
+ const sourceDiv = USDZDataAPI.#emaDivergenceRate(sourceFeed)
548
+ const destDiv = USDZDataAPI.#emaDivergenceRate(destFeed)
549
+ return Math.max(sourceDiv, destDiv)
550
+ }
551
+
552
+ static #emaDivergenceRate(priceFeed: any): number {
553
+ const price = priceFeed.getPriceUnchecked().getPriceAsNumberUnchecked()
554
+ const ema = priceFeed.getEmaPriceUnchecked().getPriceAsNumberUnchecked()
555
+ const denom = Math.abs(price)
556
+ if (denom === 0)
557
+ return 0
558
+ return Math.abs(price - ema) / denom
559
+ }
560
+
388
561
  public async getPositionCapInfoList(owner: string): Promise<IUSDZPositionCapInfo[]> {
389
562
  const positionCapInfoList: IUSDZPositionCapInfo[] = []
390
563
  let cursor: string | undefined | null
@@ -638,7 +811,7 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
638
811
  const longSize = longSymbol.openingSize
639
812
  const shortSize = shortSymbol.openingSize
640
813
 
641
- const deltaRate = USDZDataAPI.calcOiFundingFeeRate(oiState.model, longSize, shortSize, elapsed)
814
+ const deltaRate = USDZDataAPI.calcOiFundingFeeRate(oiState.model, longSize, shortSize, elapsed, oiState.maxOiLong, oiState.maxOiShort)
642
815
  return long ? deltaRate : -deltaRate
643
816
  }
644
817
 
@@ -725,12 +898,42 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
725
898
  return pnlPerRate >= 0 ? -secondsRate : secondsRate
726
899
  }
727
900
 
728
- private static calcOiFundingFeeRate(model: IUSDZOiFundingModel, longSize: number, shortSize: number, elapsed: number): number {
729
- const imbalance = Math.abs(longSize - shortSize)
730
- const total = longSize + shortSize > 0 ? longSize + shortSize : 1
731
- const dailyRate = Math.min((model.multiplier * (imbalance ** model.exponent)) / total, model.max)
732
- const secondsRate = dailyRate * elapsed / SECONDS_PER_EIGHT_HOUR
733
- return longSize >= shortSize ? secondsRate : -secondsRate
901
+ /**
902
+ * OI funding rate matching Move compute_oi_funding_rate_capped.
903
+ * When both maxOiLong and maxOiShort are set and > 0, uses normalized skew (oi/cap);
904
+ * otherwise falls back to (long - short) / total.
905
+ */
906
+ private static calcOiFundingFeeRate(
907
+ model: IUSDZOiFundingModel,
908
+ oiLong: number,
909
+ oiShort: number,
910
+ elapsed: number,
911
+ maxOiLong?: number,
912
+ maxOiShort?: number,
913
+ ): number {
914
+ let skew: number
915
+ if (maxOiLong && maxOiShort && maxOiLong > 0 && maxOiShort > 0) {
916
+ const normLong = Math.min(oiLong / maxOiLong, 1)
917
+ const normShort = Math.min(oiShort / maxOiShort, 1)
918
+ skew = normLong - normShort
919
+ }
920
+ else {
921
+ const total = oiLong + oiShort
922
+ if (total === 0)
923
+ return 0
924
+ skew = (oiLong - oiShort) / total
925
+ }
926
+
927
+ if (skew === 0)
928
+ return 0
929
+
930
+ const skewIsPositive = skew > 0
931
+ const skewAbs = Math.abs(skew)
932
+ const exponentInt = Math.floor(model.exponent)
933
+ const skewPow = skewAbs ** exponentInt
934
+ const dailyRate = Math.min(model.multiplier * skewPow, model.max)
935
+ const secondsRate = (dailyRate * elapsed) / SECONDS_PER_EIGHT_HOUR
936
+ return skewIsPositive ? secondsRate : -secondsRate
734
937
  }
735
938
 
736
939
  private static calcAccFundingFeeRate(
@@ -739,16 +942,16 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
739
942
  price: number,
740
943
  lpSupplyAmount: number,
741
944
  timestamp: number,
742
- oiModel?: IUSDZOiFundingModel,
945
+ oiState?: IUSDZOiFundingState,
743
946
  pairedOpeningSize?: number,
744
947
  ): number {
745
948
  if (symbol.lastUpdate > 0) {
746
949
  const elapsed = timestamp - symbol.lastUpdate
747
950
  if (elapsed > 0) {
748
- if (oiModel && typeof pairedOpeningSize === 'number') {
951
+ if (oiState?.enabled && oiState.model && typeof pairedOpeningSize === 'number') {
749
952
  const longSize = symbol.long ? symbol.openingSize : pairedOpeningSize
750
953
  const shortSize = symbol.long ? pairedOpeningSize : symbol.openingSize
751
- const deltaRate = USDZDataAPI.calcOiFundingFeeRate(oiModel, longSize, shortSize, elapsed)
954
+ const deltaRate = USDZDataAPI.calcOiFundingFeeRate(oiState.model, longSize, shortSize, elapsed, oiState.maxOiLong, oiState.maxOiShort)
752
955
  return symbol.accFundingRate + deltaRate
753
956
  }
754
957
  const deltaSize = USDZDataAPI.calcDeltaSize(symbol, price)
@@ -765,7 +968,7 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
765
968
  price: number,
766
969
  lpSupplyAmount: number,
767
970
  timestamp: number,
768
- oiModel?: IUSDZOiFundingModel,
971
+ oiState?: IUSDZOiFundingState,
769
972
  pairedOpeningSize?: number,
770
973
  ): number {
771
974
  const accFundingRate = USDZDataAPI.calcAccFundingFeeRate(
@@ -774,7 +977,7 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
774
977
  price,
775
978
  lpSupplyAmount,
776
979
  timestamp,
777
- oiModel,
980
+ oiState,
778
981
  pairedOpeningSize,
779
982
  )
780
983
  return symbol.unrealisedFundingFeeValue + (accFundingRate - symbol.accFundingRate) * symbol.openingSize
@@ -980,7 +1183,7 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
980
1183
  (await this.getOraclePrice(positionInfo.indexToken)).getPriceUnchecked().getPriceAsNumberUnchecked(),
981
1184
  (await this.getMarketInfo()).lpSupplyWithDecimals,
982
1185
  Date.now() / 1000,
983
- oiState && oiState.enabled ? oiState.model : undefined,
1186
+ oiState && oiState.enabled ? oiState : undefined,
984
1187
  pairedSymbol.openingSize,
985
1188
  )
986
1189
 
@@ -994,7 +1197,7 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
994
1197
  price: number,
995
1198
  lpSupplyAmount: number,
996
1199
  timestamp: number,
997
- oiModel?: IUSDZOiFundingModel,
1200
+ oiState?: IUSDZOiFundingState,
998
1201
  pairedOpeningSize?: number,
999
1202
  ): number {
1000
1203
  const accFundingRate = USDZDataAPI.calcAccFundingFeeRate(
@@ -1003,7 +1206,7 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
1003
1206
  price,
1004
1207
  lpSupplyAmount,
1005
1208
  timestamp,
1006
- oiModel,
1209
+ oiState,
1007
1210
  pairedOpeningSize,
1008
1211
  )
1009
1212
  return position.fundingFeeValue + (accFundingRate - position.lastFundingRate) * position.positionSize
@@ -1045,6 +1248,8 @@ export class USDZDataAPI extends BaseDataAPI implements IUSDZDataAPI {
1045
1248
  exponent: parseValue(content.model.fields.exponent),
1046
1249
  max: parseValue(content.model.fields.max),
1047
1250
  },
1251
+ maxOiLong: content.max_oi_long !== null && content.max_oi_long !== undefined ? parseValue(content.max_oi_long) : undefined,
1252
+ maxOiShort: content.max_oi_short !== null && content.max_oi_short !== undefined ? parseValue(content.max_oi_short) : undefined,
1048
1253
  }
1049
1254
  }
1050
1255
 
@@ -15,6 +15,7 @@ import type {
15
15
  IBaseHistoryResponse,
16
16
  IBaseOrderType,
17
17
  IBaseStaked,
18
+ ISwapFeeBreakdown,
18
19
  IZLPCredential,
19
20
  IZLPDataAPI,
20
21
  IZLPFundingFeeModel,
@@ -37,6 +38,20 @@ import type {
37
38
  } from '../interfaces'
38
39
  import { joinSymbol, parseSymbolKey, parseValue, suiSymbolToSymbol } from '../utils'
39
40
 
41
+ interface SwapImpactConfig {
42
+ id: string
43
+ enabled: boolean
44
+ impactMultiplier: number
45
+ maxImpactRate: number
46
+ }
47
+
48
+ interface EmaVolatilityFeeConfig {
49
+ id: string
50
+ enabled: boolean
51
+ multiplier: number
52
+ maxFeeRate: number
53
+ }
54
+
40
55
  export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
41
56
  constructor(
42
57
  network: Network,
@@ -223,7 +238,7 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
223
238
  price,
224
239
  marketInfo.lpSupplyWithDecimals,
225
240
  Date.now() / 1000,
226
- oiState && oiState.enabled ? oiState.model : undefined,
241
+ oiState && oiState.enabled ? oiState : undefined,
227
242
  pairedInfo.openingSize,
228
243
  )
229
244
  return fundingFeeDelta + deltaSize
@@ -382,6 +397,165 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
382
397
  }
383
398
  }
384
399
 
400
+ public async calculateSwapFeeBreakdown(fromToken: string, toToken: string, fromAmount: number): Promise<ISwapFeeBreakdown> {
401
+ const timestamp = Date.now() / 1000
402
+
403
+ const fromDecimals = this.consts.coins[fromToken]?.decimals
404
+ const toDecimals = this.consts.coins[toToken]?.decimals
405
+ if (fromDecimals === undefined || toDecimals === undefined) {
406
+ throw new Error(`Unknown token decimals for swap: ${fromToken} -> ${toToken}`)
407
+ }
408
+
409
+ const fromFeed = await this.getOraclePrice(fromToken)
410
+ const toFeed = await this.getOraclePrice(toToken)
411
+ const fromPrice = fromFeed.getPriceUnchecked().getPriceAsNumberUnchecked()
412
+ const toPrice = toFeed.getPriceUnchecked().getPriceAsNumberUnchecked()
413
+
414
+ const swapValue = (fromAmount * fromPrice) / (10 ** fromDecimals)
415
+ const totalVaultsValue = await this.#getTotalVaultsValueUsd(timestamp)
416
+
417
+ const rebaseFeeInRate = await this.rebaseFeeRate(fromToken, true, fromAmount)
418
+ const rebaseFeeInValue = swapValue * rebaseFeeInRate
419
+
420
+ // Estimate out-amount by notional value (ignoring price impact / spread / fees).
421
+ const estimatedToAmount = toPrice !== 0
422
+ ? (swapValue * (10 ** toDecimals)) / toPrice
423
+ : 0
424
+ const rebaseFeeOutRate = await this.rebaseFeeRate(toToken, false, estimatedToAmount)
425
+ const rebaseFeeOutValue = swapValue * rebaseFeeOutRate
426
+
427
+ const swapImpactCfg = await this.#getSwapImpactConfig()
428
+ const swapImpactFeeValue = swapImpactCfg?.enabled
429
+ ? ZLPDataAPI.#computeSwapImpactFeeValue(swapValue, totalVaultsValue, swapImpactCfg.impactMultiplier, swapImpactCfg.maxImpactRate)
430
+ : 0
431
+
432
+ const emaCfg = await this.#getEmaVolatilityFeeConfig()
433
+ const emaVolatilityFeeValue = emaCfg?.enabled
434
+ ? ZLPDataAPI.#computeEmaVolatilityFeeValue(
435
+ swapValue,
436
+ ZLPDataAPI.#maxEmaDivergenceRate(fromFeed, toFeed),
437
+ emaCfg.multiplier,
438
+ emaCfg.maxFeeRate,
439
+ )
440
+ : 0
441
+
442
+ const totalFeeValue = rebaseFeeInValue + rebaseFeeOutValue + swapImpactFeeValue + emaVolatilityFeeValue
443
+ const totalFeeRate = swapValue !== 0 ? totalFeeValue / swapValue : 0
444
+
445
+ return {
446
+ swapValue,
447
+ totalVaultsValue,
448
+ rebaseFeeInRate,
449
+ rebaseFeeOutRate,
450
+ rebaseFeeInValue,
451
+ rebaseFeeOutValue,
452
+ swapImpactFeeValue,
453
+ emaVolatilityFeeValue,
454
+ totalFeeValue,
455
+ totalFeeRate,
456
+ }
457
+ }
458
+
459
+ async #getTotalVaultsValueUsd(timestamp: number): Promise<number> {
460
+ const vaultKeys = Object.keys(this.consts.zoCore.vaults)
461
+ const vaultValues = await Promise.all(vaultKeys.map(async (vault) => {
462
+ const vaultInfo = await this.getVaultInfo(vault)
463
+ const reservingFeeDelta = ZLPDataAPI.calculateVaultReservingFee(vaultInfo, vaultInfo.reservingFeeModel, timestamp)
464
+ const totalVaultAmount = reservingFeeDelta + vaultInfo.liquidity + vaultInfo.reservedAmount
465
+ const oraclePrice = (await this.getOraclePrice(vault)).getPriceUnchecked().getPriceAsNumberUnchecked()
466
+ const { decimals } = this.consts.coins[vault]
467
+ return totalVaultAmount * oraclePrice / (10 ** decimals)
468
+ }))
469
+ return vaultValues.reduce((acc, curr) => acc + curr, 0)
470
+ }
471
+
472
+ async #getSwapImpactConfig(): Promise<SwapImpactConfig | null> {
473
+ const raw = await this.#getMarketDynamicFieldObjectByKeySuffix(this.consts.zoCore.market, 'SwapImpactConfigKey')
474
+ if (!raw)
475
+ return null
476
+ return ZLPDataAPI.#parseSwapImpactConfig(raw)
477
+ }
478
+
479
+ async #getEmaVolatilityFeeConfig(): Promise<EmaVolatilityFeeConfig | null> {
480
+ const raw = await this.#getMarketDynamicFieldObjectByKeySuffix(this.consts.zoCore.market, 'EmaVolatilityFeeConfigKey')
481
+ if (!raw)
482
+ return null
483
+ return ZLPDataAPI.#parseEmaVolatilityFeeConfig(raw)
484
+ }
485
+
486
+ async #getMarketDynamicFieldObjectByKeySuffix(parentId: string, keyTypeSuffix: string): Promise<any | null> {
487
+ let cursor: string | null | undefined
488
+ let hasNextPage = true
489
+
490
+ while (hasNextPage) {
491
+ const page = await this.provider.getDynamicFields({ parentId, cursor })
492
+ for (const field of page.data) {
493
+ const type = (field.name as any)?.type
494
+ if (typeof type === 'string' && type.endsWith(`::${keyTypeSuffix}`)) {
495
+ return await this.provider.getDynamicFieldObject({
496
+ parentId,
497
+ name: field.name as any,
498
+ })
499
+ }
500
+ }
501
+ hasNextPage = page.hasNextPage
502
+ cursor = page.nextCursor
503
+ }
504
+ return null
505
+ }
506
+
507
+ static #parseSwapImpactConfig(raw: any): SwapImpactConfig {
508
+ const { fields } = raw.data.content
509
+ return {
510
+ id: fields.id.id,
511
+ enabled: fields.enabled,
512
+ impactMultiplier: parseValue(fields.impact_multiplier),
513
+ maxImpactRate: parseValue(fields.max_impact_rate),
514
+ }
515
+ }
516
+
517
+ static #parseEmaVolatilityFeeConfig(raw: any): EmaVolatilityFeeConfig {
518
+ const { fields } = raw.data.content
519
+ return {
520
+ id: fields.id.id,
521
+ enabled: fields.enabled,
522
+ multiplier: parseValue(fields.multiplier),
523
+ maxFeeRate: parseValue(fields.max_fee_rate),
524
+ }
525
+ }
526
+
527
+ static #computeSwapImpactFeeValue(swapValue: number, totalVaultsValue: number, impactMultiplier: number, maxImpactRate: number): number {
528
+ if (swapValue <= 0 || totalVaultsValue <= 0)
529
+ return 0
530
+ const utilization = swapValue / totalVaultsValue
531
+ const rawImpactRate = impactMultiplier * utilization
532
+ const impactRate = Math.min(rawImpactRate, maxImpactRate)
533
+ return swapValue * impactRate
534
+ }
535
+
536
+ static #computeEmaVolatilityFeeValue(swapValue: number, maxDiv: number, multiplier: number, maxFeeRate: number): number {
537
+ if (swapValue <= 0 || maxDiv <= 0)
538
+ return 0
539
+ const rawFeeRate = multiplier * maxDiv
540
+ const feeRate = Math.min(rawFeeRate, maxFeeRate)
541
+ return swapValue * feeRate
542
+ }
543
+
544
+ static #maxEmaDivergenceRate(sourceFeed: any, destFeed: any): number {
545
+ const sourceDiv = ZLPDataAPI.#emaDivergenceRate(sourceFeed)
546
+ const destDiv = ZLPDataAPI.#emaDivergenceRate(destFeed)
547
+ return Math.max(sourceDiv, destDiv)
548
+ }
549
+
550
+ static #emaDivergenceRate(priceFeed: any): number {
551
+ const price = priceFeed.getPriceUnchecked().getPriceAsNumberUnchecked()
552
+ const ema = priceFeed.getEmaPriceUnchecked().getPriceAsNumberUnchecked()
553
+ const denom = Math.abs(price)
554
+ if (denom === 0)
555
+ return 0
556
+ return Math.abs(price - ema) / denom
557
+ }
558
+
385
559
  public async getPositionCapInfoList(owner: string): Promise<IZLPPositionCapInfo[]> {
386
560
  const positionCapInfoList: IZLPPositionCapInfo[] = []
387
561
  let cursor: string | undefined | null
@@ -671,7 +845,7 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
671
845
  const longSize = longSymbol.openingSize
672
846
  const shortSize = shortSymbol.openingSize
673
847
 
674
- const deltaRate = ZLPDataAPI.calcOiFundingFeeRate(oiState.model, longSize, shortSize, elapsed)
848
+ const deltaRate = ZLPDataAPI.calcOiFundingFeeRate(oiState.model, longSize, shortSize, elapsed, oiState.maxOiLong, oiState.maxOiShort)
675
849
  return long ? deltaRate : -deltaRate
676
850
  }
677
851
 
@@ -758,13 +932,42 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
758
932
  return pnlPerRate >= 0 ? -secondsRate : secondsRate
759
933
  }
760
934
 
761
- private static calcOiFundingFeeRate(model: IZLPOiFundingModel, longSize: number, shortSize: number, elapsed: number): number {
762
- const imbalance = Math.abs(longSize - shortSize)
935
+ /**
936
+ * OI funding rate matching Move compute_oi_funding_rate_capped.
937
+ * When both maxOiLong and maxOiShort are set and > 0, uses normalized skew (oi/cap);
938
+ * otherwise falls back to (long - short) / total.
939
+ */
940
+ private static calcOiFundingFeeRate(
941
+ model: IZLPOiFundingModel,
942
+ oiLong: number,
943
+ oiShort: number,
944
+ elapsed: number,
945
+ maxOiLong?: number,
946
+ maxOiShort?: number,
947
+ ): number {
948
+ let skew: number
949
+ if (maxOiLong && maxOiShort && maxOiLong > 0 && maxOiShort > 0) {
950
+ const normLong = Math.min(oiLong / maxOiLong, 1)
951
+ const normShort = Math.min(oiShort / maxOiShort, 1)
952
+ skew = normLong - normShort
953
+ }
954
+ else {
955
+ const total = oiLong + oiShort
956
+ if (total === 0)
957
+ return 0
958
+ skew = (oiLong - oiShort) / total
959
+ }
960
+
961
+ if (skew === 0)
962
+ return 0
763
963
 
764
- // multiplier = 0.1%, exponent = 1
765
- const dailyRate = Math.min(model.multiplier * (imbalance ** model.exponent) / (longSize + shortSize > 0 ? longSize + shortSize : 1), model.max)
766
- const secondsRate = dailyRate * elapsed / SECONDS_PER_EIGHT_HOUR
767
- return longSize >= shortSize ? secondsRate : -secondsRate
964
+ const skewIsPositive = skew > 0
965
+ const skewAbs = Math.abs(skew)
966
+ const exponentInt = Math.floor(model.exponent)
967
+ const skewPow = skewAbs ** exponentInt
968
+ const dailyRate = Math.min(model.multiplier * skewPow, model.max)
969
+ const secondsRate = (dailyRate * elapsed) / SECONDS_PER_EIGHT_HOUR
970
+ return skewIsPositive ? secondsRate : -secondsRate
768
971
  }
769
972
 
770
973
  private static calcAccFundingFeeRate(
@@ -773,17 +976,17 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
773
976
  price: number,
774
977
  lpSupplyAmount: number,
775
978
  timestamp: number,
776
- oiModel?: IZLPOiFundingModel,
979
+ oiState?: IZLPOiFundingState,
777
980
  pairedOpeningSize?: number,
778
981
  ): number {
779
982
  if (symbol.lastUpdate > 0) {
780
983
  const elapsed = timestamp - symbol.lastUpdate
781
984
  if (elapsed > 0) {
782
- // Prefer OI-based delta when model and paired side are available
783
- if (oiModel && typeof pairedOpeningSize === 'number') {
985
+ // Prefer OI-based delta when state and paired side are available
986
+ if (oiState?.enabled && oiState.model && typeof pairedOpeningSize === 'number') {
784
987
  const longSize = symbol.long ? symbol.openingSize : pairedOpeningSize
785
988
  const shortSize = symbol.long ? pairedOpeningSize : symbol.openingSize
786
- const deltaRate = ZLPDataAPI.calcOiFundingFeeRate(oiModel, longSize, shortSize, elapsed)
989
+ const deltaRate = ZLPDataAPI.calcOiFundingFeeRate(oiState.model, longSize, shortSize, elapsed, oiState.maxOiLong, oiState.maxOiShort)
787
990
  const appliedRate = symbol.long ? deltaRate : -deltaRate
788
991
  return symbol.accFundingRate + appliedRate
789
992
  }
@@ -803,7 +1006,7 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
803
1006
  price: number,
804
1007
  lpSupplyAmount: number,
805
1008
  timestamp: number,
806
- oiModel?: IZLPOiFundingModel,
1009
+ oiState?: IZLPOiFundingState,
807
1010
  pairedOpeningSize?: number,
808
1011
  ): number {
809
1012
  const accFundingRate = ZLPDataAPI.calcAccFundingFeeRate(
@@ -812,7 +1015,7 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
812
1015
  price,
813
1016
  lpSupplyAmount,
814
1017
  timestamp,
815
- oiModel,
1018
+ oiState,
816
1019
  pairedOpeningSize,
817
1020
  )
818
1021
  return symbol.unrealisedFundingFeeValue + (accFundingRate - symbol.accFundingRate) * symbol.openingSize
@@ -1006,7 +1209,7 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
1006
1209
  // OI context for funding: fetch state and paired side size when enabled
1007
1210
  const oiState = await this.getSymbolOiFundingState(positionInfo.indexToken)
1008
1211
  const pairedSymbol = await this.getSymbolInfo(positionInfo.indexToken, !positionInfo.long)
1009
- positionInfo.fundingFeeValue = ZLPDataAPI.calculatePositionFundingFee(positionInfo, await this.getSymbolInfo(positionInfo.indexToken, positionInfo.long), (await this.getSymbolInfo(positionInfo.indexToken, positionInfo.long)).fundingFeeModel, (await this.getOraclePrice(positionInfo.indexToken)).getPriceUnchecked().getPriceAsNumberUnchecked(), (await this.getMarketInfo()).lpSupplyWithDecimals, Date.now() / 1000, oiState && oiState.enabled ? oiState.model : undefined, pairedSymbol.openingSize)
1212
+ positionInfo.fundingFeeValue = ZLPDataAPI.calculatePositionFundingFee(positionInfo, await this.getSymbolInfo(positionInfo.indexToken, positionInfo.long), (await this.getSymbolInfo(positionInfo.indexToken, positionInfo.long)).fundingFeeModel, (await this.getOraclePrice(positionInfo.indexToken)).getPriceUnchecked().getPriceAsNumberUnchecked(), (await this.getMarketInfo()).lpSupplyWithDecimals, Date.now() / 1000, oiState && oiState.enabled ? oiState : undefined, pairedSymbol.openingSize)
1010
1213
 
1011
1214
  return positionInfo
1012
1215
  }
@@ -1018,7 +1221,7 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
1018
1221
  price: number,
1019
1222
  lpSupplyAmount: number,
1020
1223
  timestamp: number,
1021
- oiModel?: IZLPOiFundingModel,
1224
+ oiState?: IZLPOiFundingState,
1022
1225
  pairedOpeningSize?: number,
1023
1226
  ): number {
1024
1227
  const accFundingRate = ZLPDataAPI.calcAccFundingFeeRate(
@@ -1027,7 +1230,7 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
1027
1230
  price,
1028
1231
  lpSupplyAmount,
1029
1232
  timestamp,
1030
- oiModel,
1233
+ oiState,
1031
1234
  pairedOpeningSize,
1032
1235
  )
1033
1236
  return position.fundingFeeValue + (accFundingRate - position.lastFundingRate) * position.positionSize
@@ -1100,6 +1303,8 @@ export class ZLPDataAPI extends BaseDataAPI implements IZLPDataAPI {
1100
1303
  exponent: parseValue(content.model.fields.exponent),
1101
1304
  max: parseValue(content.model.fields.max),
1102
1305
  },
1306
+ maxOiLong: content.max_oi_long !== null && content.max_oi_long !== undefined ? parseValue(content.max_oi_long) : undefined,
1307
+ maxOiShort: content.max_oi_short !== null && content.max_oi_short !== undefined ? parseValue(content.max_oi_short) : undefined,
1103
1308
  }
1104
1309
  }
1105
1310