@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.
- package/dist/consts/deployments-slp-mainnet.json +1 -1
- package/dist/consts/deployments-usdz-mainnet.json +1 -1
- package/dist/consts/deployments-zlp-mainnet.json +1 -1
- package/dist/implementations/SLPDataAPI.cjs +212 -47
- package/dist/implementations/SLPDataAPI.cjs.map +1 -1
- package/dist/implementations/SLPDataAPI.d.cts +8 -1
- package/dist/implementations/SLPDataAPI.d.cts.map +1 -1
- package/dist/implementations/SLPDataAPI.d.mts +8 -1
- package/dist/implementations/SLPDataAPI.d.mts.map +1 -1
- package/dist/implementations/SLPDataAPI.mjs +212 -47
- package/dist/implementations/SLPDataAPI.mjs.map +1 -1
- package/dist/implementations/USDZDataAPI.cjs +197 -44
- package/dist/implementations/USDZDataAPI.cjs.map +1 -1
- package/dist/implementations/USDZDataAPI.d.cts +8 -1
- package/dist/implementations/USDZDataAPI.d.cts.map +1 -1
- package/dist/implementations/USDZDataAPI.d.mts +8 -1
- package/dist/implementations/USDZDataAPI.d.mts.map +1 -1
- package/dist/implementations/USDZDataAPI.mjs +197 -44
- package/dist/implementations/USDZDataAPI.mjs.map +1 -1
- package/dist/implementations/ZLPDataAPI.cjs +200 -46
- package/dist/implementations/ZLPDataAPI.cjs.map +1 -1
- package/dist/implementations/ZLPDataAPI.d.cts +8 -1
- package/dist/implementations/ZLPDataAPI.d.cts.map +1 -1
- package/dist/implementations/ZLPDataAPI.d.mts +8 -1
- package/dist/implementations/ZLPDataAPI.d.mts.map +1 -1
- package/dist/implementations/ZLPDataAPI.mjs +200 -46
- package/dist/implementations/ZLPDataAPI.mjs.map +1 -1
- package/dist/interfaces/base.d.cts +22 -0
- package/dist/interfaces/base.d.cts.map +1 -1
- package/dist/interfaces/base.d.mts +22 -0
- package/dist/interfaces/base.d.mts.map +1 -1
- package/dist/interfaces/slp.d.cts +6 -1
- package/dist/interfaces/slp.d.cts.map +1 -1
- package/dist/interfaces/slp.d.mts +6 -1
- package/dist/interfaces/slp.d.mts.map +1 -1
- package/dist/interfaces/usdz.d.cts +6 -1
- package/dist/interfaces/usdz.d.cts.map +1 -1
- package/dist/interfaces/usdz.d.mts +6 -1
- package/dist/interfaces/usdz.d.mts.map +1 -1
- package/dist/interfaces/zlp.d.cts +6 -1
- package/dist/interfaces/zlp.d.cts.map +1 -1
- package/dist/interfaces/zlp.d.mts +6 -1
- package/dist/interfaces/zlp.d.mts.map +1 -1
- package/package.json +1 -1
- package/src/consts/deployments-slp-mainnet.json +1 -1
- package/src/consts/deployments-usdz-mainnet.json +1 -1
- package/src/consts/deployments-zlp-mainnet.json +1 -1
- package/src/implementations/SLPDataAPI.ts +235 -19
- package/src/implementations/USDZDataAPI.ts +221 -16
- package/src/implementations/ZLPDataAPI.ts +222 -17
- package/src/interfaces/base.ts +26 -0
- package/src/interfaces/slp.ts +6 -0
- package/src/interfaces/usdz.ts +6 -0
- 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
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
762
|
-
|
|
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
|
-
|
|
765
|
-
const
|
|
766
|
-
const
|
|
767
|
-
|
|
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
|
-
|
|
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
|
|
783
|
-
if (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|