@zofai/zo-sdk 0.1.92

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 (275) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.gitattributes +4 -0
  3. package/.prettierrc.js +9 -0
  4. package/README.md +28 -0
  5. package/dist/abstract/BaseAPI.cjs +206 -0
  6. package/dist/abstract/BaseAPI.cjs.map +1 -0
  7. package/dist/abstract/BaseAPI.d.cts +172 -0
  8. package/dist/abstract/BaseAPI.d.cts.map +1 -0
  9. package/dist/abstract/BaseAPI.d.mts +172 -0
  10. package/dist/abstract/BaseAPI.d.mts.map +1 -0
  11. package/dist/abstract/BaseAPI.mjs +202 -0
  12. package/dist/abstract/BaseAPI.mjs.map +1 -0
  13. package/dist/abstract/BaseDataAPI.cjs +140 -0
  14. package/dist/abstract/BaseDataAPI.cjs.map +1 -0
  15. package/dist/abstract/BaseDataAPI.d.cts +89 -0
  16. package/dist/abstract/BaseDataAPI.d.cts.map +1 -0
  17. package/dist/abstract/BaseDataAPI.d.mts +89 -0
  18. package/dist/abstract/BaseDataAPI.d.mts.map +1 -0
  19. package/dist/abstract/BaseDataAPI.mjs +136 -0
  20. package/dist/abstract/BaseDataAPI.mjs.map +1 -0
  21. package/dist/abstract/index.cjs +12 -0
  22. package/dist/abstract/index.cjs.map +1 -0
  23. package/dist/abstract/index.d.cts +7 -0
  24. package/dist/abstract/index.d.cts.map +1 -0
  25. package/dist/abstract/index.d.mts +7 -0
  26. package/dist/abstract/index.d.mts.map +1 -0
  27. package/dist/abstract/index.mjs +7 -0
  28. package/dist/abstract/index.mjs.map +1 -0
  29. package/dist/api.cjs +779 -0
  30. package/dist/api.cjs.map +1 -0
  31. package/dist/api.d.cts +75 -0
  32. package/dist/api.d.cts.map +1 -0
  33. package/dist/api.d.mts +75 -0
  34. package/dist/api.d.mts.map +1 -0
  35. package/dist/api.mjs +775 -0
  36. package/dist/api.mjs.map +1 -0
  37. package/dist/bcs.cjs +42 -0
  38. package/dist/bcs.cjs.map +1 -0
  39. package/dist/bcs.d.cts +91 -0
  40. package/dist/bcs.d.cts.map +1 -0
  41. package/dist/bcs.d.mts +91 -0
  42. package/dist/bcs.d.mts.map +1 -0
  43. package/dist/bcs.mjs +39 -0
  44. package/dist/bcs.mjs.map +1 -0
  45. package/dist/consts/deployments-shared-mainnet.json +50 -0
  46. package/dist/consts/deployments-shared-testnet.json +45 -0
  47. package/dist/consts/deployments-slp-mainnet.json +600 -0
  48. package/dist/consts/deployments-slp-testnet.json +87 -0
  49. package/dist/consts/deployments-usdz-mainnet.json +494 -0
  50. package/dist/consts/deployments-usdz-testnet.json +98 -0
  51. package/dist/consts/deployments-zbtcvc-mainnet.json +180 -0
  52. package/dist/consts/deployments-zlp-mainnet.json +791 -0
  53. package/dist/consts/deployments-zlp-testnet.json +76 -0
  54. package/dist/consts/index.cjs +200 -0
  55. package/dist/consts/index.cjs.map +1 -0
  56. package/dist/consts/index.d.cts +157 -0
  57. package/dist/consts/index.d.cts.map +1 -0
  58. package/dist/consts/index.d.mts +157 -0
  59. package/dist/consts/index.d.mts.map +1 -0
  60. package/dist/consts/index.mjs +189 -0
  61. package/dist/consts/index.mjs.map +1 -0
  62. package/dist/consts/price_id_to_object_id.mainnet.json +56 -0
  63. package/dist/consts/price_id_to_object_id.testnet.json +17 -0
  64. package/dist/data.cjs +919 -0
  65. package/dist/data.cjs.map +1 -0
  66. package/dist/data.d.cts +235 -0
  67. package/dist/data.d.cts.map +1 -0
  68. package/dist/data.d.mts +235 -0
  69. package/dist/data.d.mts.map +1 -0
  70. package/dist/data.mjs +915 -0
  71. package/dist/data.mjs.map +1 -0
  72. package/dist/factory/SDKFactory.cjs +228 -0
  73. package/dist/factory/SDKFactory.cjs.map +1 -0
  74. package/dist/factory/SDKFactory.d.cts +84 -0
  75. package/dist/factory/SDKFactory.d.cts.map +1 -0
  76. package/dist/factory/SDKFactory.d.mts +84 -0
  77. package/dist/factory/SDKFactory.d.mts.map +1 -0
  78. package/dist/factory/SDKFactory.mjs +222 -0
  79. package/dist/factory/SDKFactory.mjs.map +1 -0
  80. package/dist/implementations/SLPAPI.cjs +1794 -0
  81. package/dist/implementations/SLPAPI.cjs.map +1 -0
  82. package/dist/implementations/SLPAPI.d.cts +183 -0
  83. package/dist/implementations/SLPAPI.d.cts.map +1 -0
  84. package/dist/implementations/SLPAPI.d.mts +183 -0
  85. package/dist/implementations/SLPAPI.d.mts.map +1 -0
  86. package/dist/implementations/SLPAPI.mjs +1790 -0
  87. package/dist/implementations/SLPAPI.mjs.map +1 -0
  88. package/dist/implementations/SLPDataAPI.cjs +1384 -0
  89. package/dist/implementations/SLPDataAPI.cjs.map +1 -0
  90. package/dist/implementations/SLPDataAPI.d.cts +158 -0
  91. package/dist/implementations/SLPDataAPI.d.cts.map +1 -0
  92. package/dist/implementations/SLPDataAPI.d.mts +158 -0
  93. package/dist/implementations/SLPDataAPI.d.mts.map +1 -0
  94. package/dist/implementations/SLPDataAPI.mjs +1380 -0
  95. package/dist/implementations/SLPDataAPI.mjs.map +1 -0
  96. package/dist/implementations/USDZAPI.cjs +1676 -0
  97. package/dist/implementations/USDZAPI.cjs.map +1 -0
  98. package/dist/implementations/USDZAPI.d.cts +180 -0
  99. package/dist/implementations/USDZAPI.d.cts.map +1 -0
  100. package/dist/implementations/USDZAPI.d.mts +180 -0
  101. package/dist/implementations/USDZAPI.d.mts.map +1 -0
  102. package/dist/implementations/USDZAPI.mjs +1672 -0
  103. package/dist/implementations/USDZAPI.mjs.map +1 -0
  104. package/dist/implementations/USDZDataAPI.cjs +1209 -0
  105. package/dist/implementations/USDZDataAPI.cjs.map +1 -0
  106. package/dist/implementations/USDZDataAPI.d.cts +191 -0
  107. package/dist/implementations/USDZDataAPI.d.cts.map +1 -0
  108. package/dist/implementations/USDZDataAPI.d.mts +191 -0
  109. package/dist/implementations/USDZDataAPI.d.mts.map +1 -0
  110. package/dist/implementations/USDZDataAPI.mjs +1205 -0
  111. package/dist/implementations/USDZDataAPI.mjs.map +1 -0
  112. package/dist/implementations/ZBTCVCAPI.cjs +906 -0
  113. package/dist/implementations/ZBTCVCAPI.cjs.map +1 -0
  114. package/dist/implementations/ZBTCVCAPI.d.cts +107 -0
  115. package/dist/implementations/ZBTCVCAPI.d.cts.map +1 -0
  116. package/dist/implementations/ZBTCVCAPI.d.mts +107 -0
  117. package/dist/implementations/ZBTCVCAPI.d.mts.map +1 -0
  118. package/dist/implementations/ZBTCVCAPI.mjs +902 -0
  119. package/dist/implementations/ZBTCVCAPI.mjs.map +1 -0
  120. package/dist/implementations/ZBTCVCDataAPI.cjs +829 -0
  121. package/dist/implementations/ZBTCVCDataAPI.cjs.map +1 -0
  122. package/dist/implementations/ZBTCVCDataAPI.d.cts +94 -0
  123. package/dist/implementations/ZBTCVCDataAPI.d.cts.map +1 -0
  124. package/dist/implementations/ZBTCVCDataAPI.d.mts +94 -0
  125. package/dist/implementations/ZBTCVCDataAPI.d.mts.map +1 -0
  126. package/dist/implementations/ZBTCVCDataAPI.mjs +825 -0
  127. package/dist/implementations/ZBTCVCDataAPI.mjs.map +1 -0
  128. package/dist/implementations/ZLPAPI.cjs +1948 -0
  129. package/dist/implementations/ZLPAPI.cjs.map +1 -0
  130. package/dist/implementations/ZLPAPI.d.cts +192 -0
  131. package/dist/implementations/ZLPAPI.d.cts.map +1 -0
  132. package/dist/implementations/ZLPAPI.d.mts +192 -0
  133. package/dist/implementations/ZLPAPI.d.mts.map +1 -0
  134. package/dist/implementations/ZLPAPI.mjs +1944 -0
  135. package/dist/implementations/ZLPAPI.mjs.map +1 -0
  136. package/dist/implementations/ZLPDataAPI.cjs +1267 -0
  137. package/dist/implementations/ZLPDataAPI.cjs.map +1 -0
  138. package/dist/implementations/ZLPDataAPI.d.cts +193 -0
  139. package/dist/implementations/ZLPDataAPI.d.cts.map +1 -0
  140. package/dist/implementations/ZLPDataAPI.d.mts +193 -0
  141. package/dist/implementations/ZLPDataAPI.d.mts.map +1 -0
  142. package/dist/implementations/ZLPDataAPI.mjs +1263 -0
  143. package/dist/implementations/ZLPDataAPI.mjs.map +1 -0
  144. package/dist/implementations/index.cjs +26 -0
  145. package/dist/implementations/index.cjs.map +1 -0
  146. package/dist/implementations/index.d.cts +13 -0
  147. package/dist/implementations/index.d.cts.map +1 -0
  148. package/dist/implementations/index.d.mts +13 -0
  149. package/dist/implementations/index.d.mts.map +1 -0
  150. package/dist/implementations/index.mjs +15 -0
  151. package/dist/implementations/index.mjs.map +1 -0
  152. package/dist/index.cjs +69 -0
  153. package/dist/index.cjs.map +1 -0
  154. package/dist/index.d.cts +51 -0
  155. package/dist/index.d.cts.map +1 -0
  156. package/dist/index.d.mts +51 -0
  157. package/dist/index.d.mts.map +1 -0
  158. package/dist/index.mjs +51 -0
  159. package/dist/index.mjs.map +1 -0
  160. package/dist/interfaces/base.cjs +7 -0
  161. package/dist/interfaces/base.cjs.map +1 -0
  162. package/dist/interfaces/base.d.cts +346 -0
  163. package/dist/interfaces/base.d.cts.map +1 -0
  164. package/dist/interfaces/base.d.mts +346 -0
  165. package/dist/interfaces/base.d.mts.map +1 -0
  166. package/dist/interfaces/base.mjs +6 -0
  167. package/dist/interfaces/base.mjs.map +1 -0
  168. package/dist/interfaces/index.cjs +31 -0
  169. package/dist/interfaces/index.cjs.map +1 -0
  170. package/dist/interfaces/index.d.cts +15 -0
  171. package/dist/interfaces/index.d.cts.map +1 -0
  172. package/dist/interfaces/index.d.mts +15 -0
  173. package/dist/interfaces/index.d.mts.map +1 -0
  174. package/dist/interfaces/index.mjs +15 -0
  175. package/dist/interfaces/index.mjs.map +1 -0
  176. package/dist/interfaces/slp.cjs +7 -0
  177. package/dist/interfaces/slp.cjs.map +1 -0
  178. package/dist/interfaces/slp.d.cts +179 -0
  179. package/dist/interfaces/slp.d.cts.map +1 -0
  180. package/dist/interfaces/slp.d.mts +179 -0
  181. package/dist/interfaces/slp.d.mts.map +1 -0
  182. package/dist/interfaces/slp.mjs +6 -0
  183. package/dist/interfaces/slp.mjs.map +1 -0
  184. package/dist/interfaces/usdz.cjs +7 -0
  185. package/dist/interfaces/usdz.cjs.map +1 -0
  186. package/dist/interfaces/usdz.d.cts +104 -0
  187. package/dist/interfaces/usdz.d.cts.map +1 -0
  188. package/dist/interfaces/usdz.d.mts +104 -0
  189. package/dist/interfaces/usdz.d.mts.map +1 -0
  190. package/dist/interfaces/usdz.mjs +6 -0
  191. package/dist/interfaces/usdz.mjs.map +1 -0
  192. package/dist/interfaces/zbtcvc.cjs +7 -0
  193. package/dist/interfaces/zbtcvc.cjs.map +1 -0
  194. package/dist/interfaces/zbtcvc.d.cts +64 -0
  195. package/dist/interfaces/zbtcvc.d.cts.map +1 -0
  196. package/dist/interfaces/zbtcvc.d.mts +64 -0
  197. package/dist/interfaces/zbtcvc.d.mts.map +1 -0
  198. package/dist/interfaces/zbtcvc.mjs +6 -0
  199. package/dist/interfaces/zbtcvc.mjs.map +1 -0
  200. package/dist/interfaces/zlp.cjs +7 -0
  201. package/dist/interfaces/zlp.cjs.map +1 -0
  202. package/dist/interfaces/zlp.d.cts +114 -0
  203. package/dist/interfaces/zlp.d.cts.map +1 -0
  204. package/dist/interfaces/zlp.d.mts +114 -0
  205. package/dist/interfaces/zlp.d.mts.map +1 -0
  206. package/dist/interfaces/zlp.mjs +6 -0
  207. package/dist/interfaces/zlp.mjs.map +1 -0
  208. package/dist/oracle.cjs +118 -0
  209. package/dist/oracle.cjs.map +1 -0
  210. package/dist/oracle.d.cts +25 -0
  211. package/dist/oracle.d.cts.map +1 -0
  212. package/dist/oracle.d.mts +25 -0
  213. package/dist/oracle.d.mts.map +1 -0
  214. package/dist/oracle.mjs +114 -0
  215. package/dist/oracle.mjs.map +1 -0
  216. package/dist/utils.cjs +129 -0
  217. package/dist/utils.cjs.map +1 -0
  218. package/dist/utils.d.cts +44 -0
  219. package/dist/utils.d.cts.map +1 -0
  220. package/dist/utils.d.mts +44 -0
  221. package/dist/utils.d.mts.map +1 -0
  222. package/dist/utils.mjs +115 -0
  223. package/dist/utils.mjs.map +1 -0
  224. package/docs/SUMMARY.md +10 -0
  225. package/docs/api-reference.md +32 -0
  226. package/docs/architecture.md +14 -0
  227. package/docs/common-operations.md +52 -0
  228. package/docs/error-handling.md +17 -0
  229. package/docs/getting-started.md +60 -0
  230. package/docs/introduction.md +15 -0
  231. package/docs/lp-specific-features.md +96 -0
  232. package/docs/type-safety.md +29 -0
  233. package/eslint.config.mjs +18 -0
  234. package/package.json +42 -0
  235. package/src/abstract/BaseAPI.ts +575 -0
  236. package/src/abstract/BaseDataAPI.ts +207 -0
  237. package/src/abstract/index.ts +7 -0
  238. package/src/api.ts +1100 -0
  239. package/src/bcs.ts +45 -0
  240. package/src/consts/deployments-shared-mainnet.json +50 -0
  241. package/src/consts/deployments-shared-testnet.json +45 -0
  242. package/src/consts/deployments-slp-mainnet.json +600 -0
  243. package/src/consts/deployments-slp-testnet.json +87 -0
  244. package/src/consts/deployments-usdz-mainnet.json +494 -0
  245. package/src/consts/deployments-usdz-testnet.json +98 -0
  246. package/src/consts/deployments-zbtcvc-mainnet.json +180 -0
  247. package/src/consts/deployments-zlp-mainnet.json +791 -0
  248. package/src/consts/deployments-zlp-testnet.json +76 -0
  249. package/src/consts/index.ts +345 -0
  250. package/src/consts/price_id_to_object_id.mainnet.json +56 -0
  251. package/src/consts/price_id_to_object_id.testnet.json +17 -0
  252. package/src/data.ts +1279 -0
  253. package/src/factory/SDKFactory.ts +340 -0
  254. package/src/implementations/SLPAPI.ts +2722 -0
  255. package/src/implementations/SLPDataAPI.ts +1839 -0
  256. package/src/implementations/USDZAPI.ts +2488 -0
  257. package/src/implementations/USDZDataAPI.ts +1548 -0
  258. package/src/implementations/ZBTCVCAPI.ts +1337 -0
  259. package/src/implementations/ZBTCVCDataAPI.ts +993 -0
  260. package/src/implementations/ZLPAPI.ts +2888 -0
  261. package/src/implementations/ZLPDataAPI.ts +1603 -0
  262. package/src/implementations/index.ts +16 -0
  263. package/src/index.ts +58 -0
  264. package/src/interfaces/base.ts +838 -0
  265. package/src/interfaces/index.ts +50 -0
  266. package/src/interfaces/slp.ts +268 -0
  267. package/src/interfaces/usdz.ts +181 -0
  268. package/src/interfaces/zbtcvc.ts +116 -0
  269. package/src/interfaces/zlp.ts +244 -0
  270. package/src/oracle.ts +153 -0
  271. package/src/utils.ts +168 -0
  272. package/tests/api.test.ts +219 -0
  273. package/tests/data.test.ts +156 -0
  274. package/tests/oracle.test.ts +33 -0
  275. package/tsconfig.json +22 -0
@@ -0,0 +1,1839 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ /* eslint-disable @stylistic/indent-binary-ops */
3
+ /* eslint-disable @stylistic/indent */
4
+ /**
5
+ * SLP DataAPI implementation
6
+ * Implements SLP-specific data access methods for Sudo SDK
7
+ */
8
+
9
+ import type { DynamicFieldInfo, SuiClient } from '@mysten/sui/client'
10
+ import { Transaction } from '@mysten/sui/transactions'
11
+ import { SUI_CLOCK_OBJECT_ID } from '@mysten/sui/utils'
12
+
13
+ import { BaseDataAPI } from '../abstract'
14
+ import { Rate, SymbolsValuation, VaultsValuation } from '../bcs'
15
+ import type { Network } from '../consts'
16
+ import { LPToken, SLP_TOKEN_DECIMALS } from '../consts'
17
+ import type {
18
+ IBaseHistoryResponse,
19
+ IBaseOrderType,
20
+ ISLPCredential,
21
+ ISLPDataAPI,
22
+ ISLPFundingFeeModel,
23
+ ISLPMarketInfo,
24
+ ISLPMarketValuationInfo,
25
+ ISLPOiFundingModel,
26
+ ISLPOiFundingState,
27
+ ISLPOrderCapInfo,
28
+ ISLPOrderInfo,
29
+ ISLPPositionCapInfo,
30
+ ISLPPositionConfig,
31
+ ISLPPositionInfo,
32
+ ISLPPriceImpactConfig,
33
+ ISLPRebaseFeeModel,
34
+ ISLPReservingFeeModel,
35
+ ISLPStaked,
36
+ ISLPStakePool,
37
+ ISLPSymbolConfig,
38
+ ISLPSymbolInfo,
39
+ ISLPVaultInfo,
40
+ } from '../interfaces'
41
+ import { joinSymbol, parseSymbolKey, parseValue, suiSymbolToSymbol } from '../utils'
42
+
43
+ export interface GetCumulativeAprResponse {
44
+ generatedAt?: string
45
+ apr?: string
46
+ }
47
+
48
+ let aprResponse: GetCumulativeAprResponse = {}
49
+
50
+ const SECONDS_PER_EIGHT_HOUR = 8 * 60 * 60 // 28800 seconds
51
+
52
+ export class SLPDataAPI extends BaseDataAPI implements ISLPDataAPI {
53
+ constructor(
54
+ network: Network,
55
+ provider: SuiClient,
56
+ apiEndpoint: string,
57
+ connectionURL: string,
58
+ ) {
59
+ super(network, provider, apiEndpoint, connectionURL, LPToken.SLP)
60
+ }
61
+
62
+ private static calculateVaultReservingFee(
63
+ vaultInfo: ISLPVaultInfo,
64
+ reservingFeeModel: { multiplier: number },
65
+ currentTime: number,
66
+ ): number {
67
+ const timeDelta = currentTime - vaultInfo.lastUpdate
68
+ const periods = Math.floor(timeDelta / SECONDS_PER_EIGHT_HOUR)
69
+ return vaultInfo.unrealisedReservingFeeAmount
70
+ + (vaultInfo.reservedAmount * reservingFeeModel.multiplier * periods) / 1e18
71
+ }
72
+
73
+ private static calculateSymbolFundingFee(
74
+ symbol: ISLPSymbolInfo,
75
+ model: ISLPFundingFeeModel,
76
+ price: number,
77
+ lpSupplyAmount: number,
78
+ timestamp: number,
79
+ oiModel?: ISLPOiFundingModel,
80
+ pairedOpeningSize?: number,
81
+ ): number {
82
+ const accFundingRate = SLPDataAPI.calcAccFundingFeeRate(
83
+ symbol,
84
+ model,
85
+ price,
86
+ lpSupplyAmount,
87
+ timestamp,
88
+ symbol.long,
89
+ oiModel,
90
+ pairedOpeningSize,
91
+ )
92
+ return symbol.unrealisedFundingFeeValue + (accFundingRate - symbol.accFundingRate) * symbol.openingSize
93
+ }
94
+
95
+ public async getRebaseFeeModel(): Promise<ISLPRebaseFeeModel> {
96
+ this.validateCache()
97
+ if (this.rebaseFeeModelCache) {
98
+ return this.rebaseFeeModelCache
99
+ }
100
+ const rawData = await this.provider.getObject({
101
+ id: this.consts.sudoCore.rebaseFeeModel,
102
+ options: {
103
+ showContent: true,
104
+ },
105
+ })
106
+ const model = SLPDataAPI.parseRebaseFeeModel(rawData)
107
+ const exponent = await SLPDataAPI.getRebaseFeeExponent(
108
+ this.provider,
109
+ this.consts.sudoCore.rebaseFeeModel,
110
+ this.consts.sudoCore.upgradedPackage,
111
+ )
112
+ return { ...model, exponent }
113
+ }
114
+
115
+ /**
116
+ * Creates vaults valuation for SLP using Sudo SDK approach
117
+ */
118
+ public valuateVaults(tx: Transaction) {
119
+ if (!this.consts.sudoCore) {
120
+ throw new Error('Sudo Core configuration not found. Make sure you are using LPToken.SLP')
121
+ }
122
+
123
+ const vaultsValuation = tx.moveCall({
124
+ target: `${this.consts.sudoCore.upgradedPackage}::market::create_vaults_valuation`,
125
+ typeArguments: [`${this.consts.sudoCore.package}::slp::SLP`],
126
+ arguments: [
127
+ tx.object(SUI_CLOCK_OBJECT_ID),
128
+ tx.object(this.consts.sudoCore.market),
129
+ ],
130
+ })
131
+
132
+ for (const key of Object.keys(this.consts.sudoCore.vaults)) {
133
+ const vault = this.consts.sudoCore.vaults[key]
134
+
135
+ tx.moveCall({
136
+ target: `${this.consts.sudoCore.upgradedPackage}::market::valuate_vault_v1_1`,
137
+ typeArguments: [
138
+ `${this.consts.sudoCore.package}::slp::SLP`,
139
+ this.consts.coins[key].module,
140
+ ],
141
+ arguments: [
142
+ tx.object(this.consts.sudoCore.market),
143
+ tx.object(vault.reservingFeeModel),
144
+ tx.object(this.consts.pythFeeder.feeder[key]),
145
+ vaultsValuation,
146
+ ],
147
+ })
148
+ }
149
+ return vaultsValuation
150
+ }
151
+
152
+ /**
153
+ * Creates symbols valuation for SLP using Sudo SDK approach
154
+ */
155
+ public valuateSymbols(tx: Transaction) {
156
+ if (!this.consts.sudoCore) {
157
+ throw new Error('Sudo Core configuration not found. Make sure you are using LPToken.SLP')
158
+ }
159
+
160
+ const symbolsValuation = tx.moveCall({
161
+ target: `${this.consts.sudoCore.upgradedPackage}::market::create_symbols_valuation`,
162
+ typeArguments: [`${this.consts.sudoCore.package}::slp::SLP`],
163
+ arguments: [
164
+ tx.object(SUI_CLOCK_OBJECT_ID),
165
+ tx.object(this.consts.sudoCore.market),
166
+ ],
167
+ })
168
+ for (const key of Object.keys(this.consts.sudoCore.symbols)) {
169
+ const [direction, token] = parseSymbolKey(key)
170
+ const symbol = this.consts.sudoCore.symbols[key]
171
+ tx.moveCall({
172
+ target: `${this.consts.sudoCore.upgradedPackage}::market::valuate_symbol_v1_1`,
173
+ typeArguments: [
174
+ `${this.consts.sudoCore.package}::slp::SLP`,
175
+ this.consts.coins[token].module,
176
+ `${this.consts.sudoCore.package}::market::${direction.toUpperCase()}`,
177
+ ],
178
+ arguments: [
179
+ tx.object(this.consts.sudoCore.market),
180
+ tx.object(symbol.fundingFeeModel),
181
+ tx.object(this.consts.pythFeeder.feeder[token]),
182
+ symbolsValuation,
183
+ ],
184
+ })
185
+ }
186
+ return symbolsValuation
187
+ }
188
+
189
+ /**
190
+ * Creates both vaults and symbols valuation for SLP
191
+ */
192
+ public valuate(tx: Transaction) {
193
+ const vaultsValuation = this.valuateVaults(tx)
194
+ const symbolsValuation = this.valuateSymbols(tx)
195
+ return { vaultsValuation, symbolsValuation }
196
+ }
197
+
198
+ /**
199
+ * Valuates the SLP market using Sudo SDK's approach
200
+ */
201
+ public async valuateMarket(): Promise<ISLPMarketValuationInfo> {
202
+ const marketInfo = await this.getMarketInfo()
203
+ let slpPrice = 0
204
+ let value = 0
205
+
206
+ const vaultPromises = Object.keys(this.consts.sudoCore.vaults).map(async (vault) => {
207
+ const vaultInfo = await this.getVaultInfo(vault)
208
+ const reservingFeeDelta = SLPDataAPI.calculateVaultReservingFee(vaultInfo, vaultInfo.reservingFeeModel, Date.now() / 1000)
209
+ const totalVaultAmount = reservingFeeDelta + vaultInfo.liquidity + vaultInfo.reservedAmount
210
+ const oraclePrice = (await this.getOraclePrice(vault)).getPriceUnchecked().getPriceAsNumberUnchecked()
211
+ const { decimals } = this.consts.coins[vault]
212
+ const vaultValue = totalVaultAmount * oraclePrice / (10 ** decimals)
213
+ return vaultValue
214
+ })
215
+
216
+ const symbolPromises = Object.keys(this.consts.sudoCore.symbols).map(async (symbol) => {
217
+ const [direction, tokenId] = parseSymbolKey(symbol)
218
+ const symbolInfo = await this.getSymbolInfo(tokenId, direction === 'long')
219
+ const price = (await this.getOraclePrice(tokenId)).getPriceUnchecked().getPriceAsNumberUnchecked()
220
+ const deltaSize = SLPDataAPI.calcDeltaSize(symbolInfo, price, direction === 'long')
221
+
222
+ const oiState = await this.getSymbolOiFundingState(tokenId)
223
+ const pairedInfo = await this.getSymbolInfo(tokenId, direction !== 'long')
224
+ const fundingFeeDelta = SLPDataAPI.calculateSymbolFundingFee(
225
+ symbolInfo,
226
+ symbolInfo.fundingFeeModel,
227
+ price,
228
+ marketInfo.lpSupplyWithDecimals,
229
+ Date.now() / 1000,
230
+ oiState && oiState.enabled ? oiState.model : undefined,
231
+ pairedInfo.openingSize,
232
+ )
233
+ const symbolValue = fundingFeeDelta + deltaSize
234
+ return symbolValue
235
+ })
236
+
237
+ const [vaultValues, symbolValues] = await Promise.all([Promise.all(vaultPromises), Promise.all(symbolPromises)])
238
+
239
+ const totalVaultValue = vaultValues.reduce((acc: number, curr: number) => acc + curr, 0)
240
+ const totalSymbolValue = symbolValues.reduce((acc: number, curr: number) => acc + curr, 0)
241
+
242
+ value = totalVaultValue + totalSymbolValue
243
+
244
+ slpPrice = value / marketInfo.lpSupplyWithDecimals
245
+ return {
246
+ marketCap: value,
247
+ price: slpPrice,
248
+ supply: marketInfo.lpSupplyWithDecimals,
249
+ apr: Number(marketInfo.apr),
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Valuates market using simulation (Sudo SDK specific)
255
+ */
256
+ public async simValuate(sender: string): Promise<number> {
257
+ const tx = await this.initOracleTxb(
258
+ Object.keys(this.consts.pythFeeder.feeder),
259
+ )
260
+ this.valuate(tx)
261
+ const res = await this.provider.devInspectTransactionBlock({
262
+ transactionBlock: tx,
263
+ sender,
264
+ })
265
+
266
+ const symbolsValuationOffset
267
+ = Object.keys(this.consts.sudoCore.symbols).length + 1
268
+
269
+ const vaultsValuation = VaultsValuation.parse(
270
+ new Uint8Array(
271
+ (
272
+ (res.results as any)[
273
+ (res.results?.length || 0) - symbolsValuationOffset - 1
274
+ ].mutableReferenceOutputs as any
275
+ )[1][1],
276
+ ),
277
+ )
278
+
279
+ const symbolsValuation = SymbolsValuation.parse(
280
+ new Uint8Array(
281
+ (
282
+ (res.results as any)[(res.results?.length || 0) - 1]
283
+ .mutableReferenceOutputs as any
284
+ )[1][1],
285
+ ),
286
+ )
287
+
288
+ const result
289
+ = Number(
290
+ BigInt(vaultsValuation.value)
291
+ + BigInt(symbolsValuation.value.value)
292
+ * BigInt(symbolsValuation.value.is_positive ? 1 : -1),
293
+ ) / 1e18
294
+ return result
295
+ }
296
+
297
+ /**
298
+ * Valuates only vaults (Sudo SDK specific)
299
+ */
300
+ public async simValuateVaults(sender: string): Promise<number> {
301
+ const tx = await this.initOracleTxb(
302
+ Object.keys(this.consts.sudoCore.vaults),
303
+ )
304
+ this.valuateVaults(tx)
305
+
306
+ const res = await this.provider.devInspectTransactionBlock({
307
+ transactionBlock: tx,
308
+ sender,
309
+ })
310
+ const vaultsValuation = VaultsValuation.parse(
311
+ new Uint8Array(
312
+ (
313
+ (res.results as any)[(res.results?.length || 0) - 1]
314
+ .mutableReferenceOutputs as any
315
+ )[1][1],
316
+ ),
317
+ )
318
+
319
+ const result = Number(BigInt(vaultsValuation.value)) / 1e18
320
+ return result
321
+ }
322
+
323
+ /**
324
+ * Valuates market with vaults only
325
+ */
326
+ public async valuateMarketWithVaultsOnly(): Promise<ISLPMarketValuationInfo> {
327
+ if (!this.consts.sudoCore) {
328
+ throw new Error('Sudo Core configuration not found')
329
+ }
330
+
331
+ const marketInfo = await this.getMarketInfo()
332
+ const value = await this.simValuateVaults(this.consts.sudoCore.adminCap)
333
+ const slpPrice = value / marketInfo.lpSupplyWithDecimals
334
+
335
+ return {
336
+ marketCap: value,
337
+ price: slpPrice,
338
+ supply: marketInfo.lpSupplyWithDecimals,
339
+ apr: Number(marketInfo.apr),
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Gets SLP market information
345
+ */
346
+ public async getMarketInfo(): Promise<ISLPMarketInfo> {
347
+ this.validateCache()
348
+ if (this.marketInfoCache) {
349
+ return this.marketInfoCache
350
+ }
351
+
352
+ const rawData = await this.provider.getObject({
353
+ id: this.consts.sudoCore.market,
354
+ options: {
355
+ showContent: true,
356
+ },
357
+ })
358
+ const apr = await this.getCumulativeApr()
359
+ return {
360
+ ...SLPDataAPI.parseMarketInfo(rawData),
361
+ apr,
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Gets SLP vault information
367
+ */
368
+ public async getVaultInfo(vaultToken: string): Promise<ISLPVaultInfo> {
369
+ this.validateCache()
370
+ if (this.vaultInfoCache[vaultToken]) {
371
+ return this.vaultInfoCache[vaultToken]
372
+ }
373
+
374
+ const rawData = await this.provider.getDynamicFieldObject({
375
+ parentId: this.consts.sudoCore.vaultsParent,
376
+ name: {
377
+ type: `${this.consts.sudoCore.package}::market::VaultName<${this.consts.coins[vaultToken].module}>`,
378
+ value: { dummy_field: false },
379
+ },
380
+ })
381
+ return await this.parseVaultInfo(rawData)
382
+ }
383
+
384
+ /**
385
+ * Gets SLP symbol information
386
+ */
387
+ public async getSymbolInfo(indexToken: string, long: boolean): Promise<ISLPSymbolInfo> {
388
+ this.validateCache()
389
+ const symbol = joinSymbol(long ? 'long' : 'short', indexToken)
390
+ if (this.symbolInfoCache[symbol]) {
391
+ return this.symbolInfoCache[symbol]
392
+ }
393
+
394
+ const rawData = await this.provider.getDynamicFieldObject({
395
+ parentId: this.consts.sudoCore.symbolsParent,
396
+ name: {
397
+ type: `${this.consts.sudoCore.package}::market::SymbolName<${this.consts.coins[indexToken].module
398
+ }, ${this.consts.sudoCore.package}::market::${long ? 'LONG' : 'SHORT'
399
+ }>`,
400
+ value: { dummy_field: false },
401
+ },
402
+ })
403
+
404
+ return await this.parseSymbolInfo(rawData, long)
405
+ }
406
+
407
+ /**
408
+ * Gets SLP symbol configuration
409
+ */
410
+ public async getSymbolConfig(indexToken: string, long: boolean): Promise<ISLPSymbolConfig | null> {
411
+ this.validateCache()
412
+ try {
413
+ const rawData = await this.provider.getDynamicFieldObject({
414
+ parentId: this.consts.sudoCore.market,
415
+ name: {
416
+ type: `${this.consts.sudoCore.package}::market::SymbolName<${this.consts.coins[indexToken].module}, ${this.consts.sudoCore.package}::market::${long ? 'LONG' : 'SHORT'}>`,
417
+ value: { dummy_field: false },
418
+ },
419
+ })
420
+ return SLPDataAPI.parseSymbolConfig(rawData)
421
+ }
422
+ catch {
423
+ // If the dynamic field doesn't exist, return null
424
+ console.error('Symbol Config Not Found')
425
+ return null
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Gets price impact configuration for a symbol.
431
+ * Price impact config applies to both long and short directions for the same index token.
432
+ */
433
+ public async getPriceImpactConfig(indexToken: string): Promise<ISLPPriceImpactConfig | null> {
434
+ this.validateCache()
435
+ try {
436
+ const rawData = await this.provider.getDynamicFieldObject({
437
+ parentId: this.consts.sudoCore.market,
438
+ name: {
439
+ type: `0x699769d16326684d94039c23d8568fe8f438854bc69e681328647d6682425a7b::price_impact::PriceImpactConfigKey<${this.consts.coins[indexToken].module}>`,
440
+ value: { dummy_field: false },
441
+ },
442
+ })
443
+ return SLPDataAPI.parsePriceImpactConfig(rawData)
444
+ }
445
+ catch (e: any) {
446
+ // If the dynamic field doesn't exist, return null (price impact not configured for this symbol)
447
+ console.error('Error Fetching Price Impact Config:', e)
448
+ return null
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Gets SLP symbol OI funding state (global per symbol)
454
+ */
455
+ public async getSymbolOiFundingState(indexToken: string): Promise<ISLPOiFundingState | null> {
456
+ this.validateCache()
457
+ try {
458
+ const rawData = await this.provider.getDynamicFieldObject({
459
+ parentId: this.consts.sudoCore.market,
460
+ name: {
461
+ type: `0x719955743d23874522250584b34d19462b41eed2816334adb5744308217f378a::funding::FundingStateKey<${this.consts.coins[indexToken].module}>`,
462
+ value: { dummy_field: false },
463
+ },
464
+ })
465
+ return SLPDataAPI.parseOiFundingState(rawData)
466
+ }
467
+ catch (e: any) {
468
+ // If the dynamic field doesn't exist, return null
469
+ console.error('Error Fetching SLP Symbol OI Funding State:', e)
470
+ return null
471
+ }
472
+ }
473
+
474
+ public async getPositionInfoList(
475
+ positionCapInfoList: ISLPPositionCapInfo[],
476
+ owner: string,
477
+ batchSize = 10,
478
+ ): Promise<ISLPPositionInfo[]> {
479
+ const positionInfoList: ISLPPositionInfo[] = []
480
+
481
+ // Process in batches of 10
482
+ for (let i = 0; i < positionCapInfoList.length; i += batchSize) {
483
+ const batch = positionCapInfoList.slice(i, i + batchSize)
484
+
485
+ await Promise.all(batch.map(async (positionCapInfo) => {
486
+ try {
487
+ const positionRaw = await this.provider.getDynamicFieldObject({
488
+ parentId: this.consts.sudoCore.positionsParent,
489
+ name: {
490
+ type: `${this.consts.sudoCore.package}::market::PositionName<${positionCapInfo.symbol0
491
+ }, ${positionCapInfo.symbol1}, ${this.consts.sudoCore.package
492
+ }::market::${positionCapInfo.long ? 'LONG' : 'SHORT'}>`,
493
+ value: {
494
+ owner,
495
+ id: positionCapInfo.positionCapId,
496
+ },
497
+ },
498
+ })
499
+
500
+ if (positionRaw?.data?.content) {
501
+ positionInfoList.push(
502
+ await this.parsePositionInfo(
503
+ positionRaw,
504
+ positionCapInfo.positionCapId,
505
+ ),
506
+ )
507
+ }
508
+ }
509
+ catch (error) {
510
+ // Position might have been deleted after force settlement
511
+ console.warn(`Failed to parse position info for position cap ID ${positionCapInfo.positionCapId}: ${error}`)
512
+ // Continue with next position without adding this one to the list
513
+ }
514
+ }))
515
+ }
516
+
517
+ return positionInfoList.sort((a, b) =>
518
+ a.openTimestamp > b.openTimestamp ? 1 : -1,
519
+ )
520
+ }
521
+
522
+ /**
523
+ * Gets user history for SLP
524
+ */
525
+ public async getHistory(
526
+ trader: string,
527
+ page: number,
528
+ limit: number,
529
+ orderType?: string,
530
+ symbol?: string,
531
+ ): Promise<IBaseHistoryResponse> {
532
+ const params = new URLSearchParams({
533
+ trader,
534
+ page: page.toString(),
535
+ limit: limit.toString(),
536
+ })
537
+
538
+ if (orderType) {
539
+ params.append('orderType', orderType)
540
+ }
541
+ if (symbol) {
542
+ params.append('symbol', symbol)
543
+ }
544
+
545
+ const url = `${this.apiEndpoint}/traderEvents?${params}`
546
+ const res = await fetch(url, {
547
+ method: 'GET',
548
+ headers: {
549
+ 'Content-Type': 'application/json',
550
+ },
551
+ })
552
+ const response = await res.json() as any
553
+
554
+ return {
555
+ histories: response.data?.histories || [],
556
+ pagination: response.data?.pagination || {
557
+ total: 0,
558
+ page: 1,
559
+ limit: 20,
560
+ pages: 0,
561
+ },
562
+ }
563
+ }
564
+
565
+ public async getStaked(owner: string): Promise<ISLPStaked> {
566
+ let rawCredentialsData: any[] = []
567
+ let queryNextPage = true
568
+ let queryCursor
569
+ const limit = 50
570
+
571
+ while (queryNextPage) {
572
+ const { data, hasNextPage, nextCursor } = await this.provider.getOwnedObjects({
573
+ owner,
574
+ filter: {
575
+ MoveModule: {
576
+ package: this.consts.sudoStaking.package,
577
+ module: 'pool',
578
+ },
579
+ },
580
+ options: {
581
+ showType: true,
582
+ showContent: true,
583
+ },
584
+ cursor: queryCursor,
585
+ limit,
586
+ })
587
+
588
+ queryNextPage = hasNextPage
589
+ queryCursor = nextCursor!
590
+ if (!data)
591
+ break
592
+ rawCredentialsData = [...rawCredentialsData, ...data]
593
+ }
594
+
595
+ const pool = await this.getStakePool()
596
+ const credentials = rawCredentialsData.map((item: any) =>
597
+ SLPDataAPI.parseCredential(item, pool),
598
+ )
599
+ return {
600
+ credentials,
601
+ amount: credentials.reduce(
602
+ (acc: bigint, cur: ISLPCredential) => acc + cur.amount,
603
+ BigInt(0),
604
+ ),
605
+ claimable: credentials.reduce(
606
+ (acc: bigint, cur: ISLPCredential) => acc + cur.claimable,
607
+ BigInt(0),
608
+ ),
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Legacy method: Get stake pool from sudo_staking
614
+ */
615
+ public async getStakePool(): Promise<ISLPStakePool> {
616
+ const raw = await this.provider.getObject({
617
+ id: this.consts.sudoStaking.pool,
618
+ options: {
619
+ showContent: true,
620
+ },
621
+ })
622
+ return SLPDataAPI.parseStakePool(raw)
623
+ }
624
+
625
+ public async getStakePoolV2(): Promise<ISLPStakePool> {
626
+ const poolId = this.sharedConfig.zoStaking.pools.slp
627
+ const raw = await this.provider.getObject({
628
+ id: poolId,
629
+ options: {
630
+ showContent: true,
631
+ },
632
+ })
633
+ return SLPDataAPI.parseStakePool(raw)
634
+ }
635
+
636
+ public async getStakedV2(owner: string): Promise<ISLPStaked> {
637
+ let rawCredentialsData: any[] = []
638
+ let queryNextPage = true
639
+ let queryCursor
640
+ const limit = 50
641
+
642
+ while (queryNextPage) {
643
+ const { data, hasNextPage, nextCursor } = await this.provider.getOwnedObjects({
644
+ owner,
645
+ filter: {
646
+ MoveModule: {
647
+ package: this.sharedConfig.zoStaking.package,
648
+ module: 'pool',
649
+ },
650
+ },
651
+ options: {
652
+ showType: true,
653
+ showContent: true,
654
+ },
655
+ cursor: queryCursor,
656
+ limit,
657
+ })
658
+
659
+ queryNextPage = hasNextPage
660
+ queryCursor = nextCursor!
661
+ if (!data)
662
+ break
663
+ rawCredentialsData = [...rawCredentialsData, ...data]
664
+ }
665
+
666
+ const pool = await this.getStakePoolV2()
667
+
668
+ const credentials = rawCredentialsData
669
+ .filter(
670
+ (item: any) =>
671
+ item.data.type
672
+ === `${this.sharedConfig.zoStaking.package}::pool::Credential<${this.consts.sudoCore.package}::slp::SLP, ${this.consts.sudoCore.package}::slp::SLP>`,
673
+ )
674
+ .map((item: any) =>
675
+ SLPDataAPI.parseCredential(item, pool),
676
+ )
677
+ return {
678
+ credentials,
679
+ amount: credentials.reduce(
680
+ (acc: bigint, cur: ISLPCredential) => acc + cur.amount,
681
+ BigInt(0),
682
+ ),
683
+ claimable: credentials.reduce(
684
+ (acc: bigint, cur: ISLPCredential) => acc + cur.claimable,
685
+ BigInt(0),
686
+ ),
687
+ }
688
+ }
689
+
690
+ public async fundingFeeRate(indexToken: string, long: boolean): Promise<number> {
691
+ const oiState = await this.getSymbolOiFundingState(indexToken)
692
+ if (oiState && oiState.enabled) {
693
+ const longSymbol = await this.getSymbolInfo(indexToken, true)
694
+ const shortSymbol = await this.getSymbolInfo(indexToken, false)
695
+
696
+ if (longSymbol.lastUpdate <= 0 && shortSymbol.lastUpdate <= 0) {
697
+ return 0
698
+ }
699
+
700
+ const elapsed = SECONDS_PER_EIGHT_HOUR
701
+ const deltaRate = SLPDataAPI.calcOiFundingFeeRate(
702
+ oiState.model,
703
+ longSymbol.openingSize,
704
+ shortSymbol.openingSize,
705
+ elapsed,
706
+ )
707
+ return long ? deltaRate : -deltaRate
708
+ }
709
+
710
+ const symbol = await this.getSymbolInfo(indexToken, long)
711
+ if (symbol.lastUpdate <= 0) {
712
+ return 0
713
+ }
714
+ const price = (await this.getOraclePrice(indexToken)).getPriceUnchecked().getPriceAsNumberUnchecked()
715
+ const lpSupplyAmount = (await this.getMarketInfo()).lpSupplyWithDecimals
716
+ const model = symbol.fundingFeeModel
717
+ const elapsed = SECONDS_PER_EIGHT_HOUR
718
+
719
+ const deltaSize = SLPDataAPI.calcDeltaSize(symbol, price, symbol.long)
720
+ const pnlPerLp = (symbol.realisedPnl + symbol.unrealisedFundingFeeValue + deltaSize) / lpSupplyAmount
721
+ return SLPDataAPI.calcFundingFeeRate(model, pnlPerLp, elapsed)
722
+ }
723
+
724
+ public async rebaseFeeRate(collateralToken: string, increase: boolean, amount: number, _sender?: string): Promise<number> {
725
+ let vaultValue = 0
726
+ if (!increase && amount > 0) {
727
+ amount = -amount
728
+ }
729
+ const value = amount * (await this.getOraclePrice(collateralToken)).getPriceUnchecked().getPriceAsNumberUnchecked() / (10 ** this.consts.coins[collateralToken].decimals)
730
+
731
+ const vaultPromises = Object.keys(this.consts.sudoCore.vaults).map(async (vault) => {
732
+ const vaultInfo = await this.getVaultInfo(vault)
733
+ const reservingFeeDelta = SLPDataAPI.calculateVaultReservingFee(vaultInfo, vaultInfo.reservingFeeModel, Date.now() / 1000)
734
+ const totalVaultAmount = reservingFeeDelta + vaultInfo.liquidity + vaultInfo.reservedAmount
735
+ const oraclePrice = (await this.getOraclePrice(vault)).getPriceUnchecked().getPriceAsNumberUnchecked()
736
+ const res = totalVaultAmount * oraclePrice / (10 ** this.consts.coins[vault].decimals)
737
+ if (collateralToken === vault) {
738
+ vaultValue = res
739
+ }
740
+ return res
741
+ })
742
+
743
+ const vaultValues = await Promise.all(vaultPromises)
744
+ const totalVaultValue = vaultValues.reduce((acc, curr) => acc + curr, 0)
745
+ const targetRatio = Number.parseInt(
746
+ this.consts.sudoCore.vaults[collateralToken].weight,
747
+ 10,
748
+ ) / Object.values(this.consts.sudoCore.vaults)
749
+ .map(e => Number.parseInt(e.weight, 10))
750
+ .reduce((acc, curr) => acc + curr, 0)
751
+
752
+ return SLPDataAPI.calcRebaseFeeRate(
753
+ await this.getRebaseFeeModel(),
754
+ increase,
755
+ (vaultValue + value) / (totalVaultValue + value),
756
+ targetRatio,
757
+ )
758
+ }
759
+
760
+ public async reservingFeeRate(collateralToken: string, amount: number, sender?: string): Promise<number> {
761
+ if (!sender) {
762
+ throw new Error('Sender address is required for reservingFeeRate calculation')
763
+ }
764
+ const vaultInfo = await this.getVaultInfo(collateralToken)
765
+ const vaultSupply
766
+ = vaultInfo.liquidity
767
+ + vaultInfo.reservedAmount
768
+ + vaultInfo.unrealisedReservingFeeAmount
769
+ + amount
770
+ const utilization = vaultSupply
771
+ ? Number.parseInt(
772
+ (((vaultInfo.reservedAmount + amount) / vaultSupply) * 1e18).toFixed(
773
+ 0,
774
+ ),
775
+ 10,
776
+ )
777
+ : 0
778
+ const tx = new Transaction()
779
+ tx.moveCall({
780
+ target: `${this.consts.sudoCore.upgradedPackage}::model::compute_reserving_fee_rate`,
781
+ arguments: [
782
+ tx.object(
783
+ this.consts.sudoCore.vaults[collateralToken].reservingFeeModel,
784
+ ),
785
+ tx.pure.u128(utilization),
786
+ tx.pure.u64(8 * 3600),
787
+ ],
788
+ })
789
+ const res: any = await this.provider.devInspectTransactionBlock({
790
+ transactionBlock: tx,
791
+ sender,
792
+ })
793
+ const de = Rate.parse(
794
+ new Uint8Array(res.results.at(-1).returnValues[0][0]),
795
+ )
796
+ return Number(BigInt(de)) / 1e18
797
+ }
798
+
799
+ public async getPositionConfig(indexToken: string, long: boolean): Promise<ISLPPositionConfig> {
800
+ this.validateCache()
801
+ const symbol = joinSymbol(long ? 'long' : 'short', indexToken)
802
+ if (this.positionConfigCache[symbol]) {
803
+ return this.positionConfigCache[symbol]
804
+ }
805
+
806
+ const rawData = await this.provider.getObject({
807
+ id: this.consts.sudoCore.symbols[symbol].positionConfig,
808
+ options: {
809
+ showContent: true,
810
+ },
811
+ })
812
+ return SLPDataAPI.parsePositionConfig(rawData)
813
+ }
814
+
815
+ public async getOpenPositions(batchSize = 50, symbol = 'sui'): Promise<ISLPPositionInfo[]> {
816
+ let positionDynamicFields: DynamicFieldInfo[] = []
817
+ let _continue = true
818
+ let cursor
819
+ while (_continue) {
820
+ // data here will be a list of dynamic fields containing name and value
821
+ const { data, nextCursor, hasNextPage }
822
+ = await this.provider.getDynamicFields({
823
+ parentId: this.consts.sudoCore.positionsParent,
824
+ cursor,
825
+ })
826
+
827
+ positionDynamicFields = positionDynamicFields.concat(data)
828
+ _continue = hasNextPage
829
+ cursor = nextCursor
830
+ }
831
+
832
+ // Filter by symbol if provided
833
+ if (!(symbol && this.consts.coins[symbol])) {
834
+ return []
835
+ }
836
+ const coinModule = symbol === 'sui' ? '0x2::sui::SUI' : this.consts.coins[symbol].module
837
+ positionDynamicFields = positionDynamicFields.filter((field) => {
838
+ // Extract the second coin module from PositionName<coin1, coin2, direction>
839
+ const typeStr = field.name?.type
840
+ if (!typeStr)
841
+ return false
842
+
843
+ const match = typeStr.match(/PositionName<([^,]+),([^,]+),([^>]+)>/)
844
+ if (!match)
845
+ return false
846
+
847
+ const secondCoin = match[2].trim()
848
+ return secondCoin === coinModule
849
+ })
850
+
851
+ // then we query by dynamic field names and order by time
852
+ const positionInfoList: ISLPPositionInfo[] = []
853
+
854
+ for (let i = 0; i < positionDynamicFields.length; i += batchSize) {
855
+ const batch = positionDynamicFields.slice(i, i + batchSize)
856
+
857
+ await Promise.all(
858
+ batch.map(async (positionDynamicField) => {
859
+ const positionRaw = await this.provider.getDynamicFieldObject({
860
+ parentId: this.consts.sudoCore.positionsParent,
861
+ name: positionDynamicField.name,
862
+ })
863
+
864
+ if (positionRaw?.data?.content) {
865
+ // @ts-expect-error: content fields type is not properly defined
866
+ if (positionRaw?.data?.content?.fields?.value?.fields?.closed) {
867
+ // skip closed positions
868
+ return
869
+ }
870
+ const positionInfo = await this.parsePositionInfo(
871
+ positionRaw,
872
+ positionDynamicField.objectId,
873
+ )
874
+ if (positionInfo) {
875
+ positionInfoList.push(positionInfo)
876
+ }
877
+ }
878
+ }),
879
+ )
880
+ }
881
+
882
+ return positionInfoList
883
+ .filter(positionInfo => !positionInfo.closed)
884
+ .sort((a, b) => (a.openTimestamp > b.openTimestamp ? 1 : -1))
885
+ }
886
+
887
+ public async getPositionCapInfoList(owner: string): Promise<ISLPPositionCapInfo[]> {
888
+ let cursor: string | undefined | null
889
+ let hasNextPage = true
890
+ const positionCapInfoList = []
891
+
892
+ while (hasNextPage) {
893
+ const positionCaps = await this.provider.getOwnedObjects({
894
+ owner,
895
+ filter: {
896
+ StructType: `${this.consts.sudoCore.package}::market::PositionCap`,
897
+ },
898
+ options: {
899
+ showType: true,
900
+ },
901
+ cursor,
902
+ })
903
+
904
+ for (const positionCap of positionCaps.data) {
905
+ if (positionCap.data?.type?.includes('PositionCap')) {
906
+ positionCapInfoList.push({
907
+ positionCapId: positionCap.data.objectId,
908
+ symbol0: positionCap.data.type.split('<')[1].split(',')[0].trim(),
909
+ symbol1: positionCap.data.type
910
+ .split('<')[1]
911
+ .split(',')[1]
912
+ .split(',')[0]
913
+ .trim(),
914
+ long: positionCap.data.type.includes('LONG'),
915
+ })
916
+ }
917
+ }
918
+
919
+ hasNextPage = positionCaps.hasNextPage
920
+ cursor = positionCaps.nextCursor
921
+ }
922
+
923
+ return positionCapInfoList
924
+ }
925
+
926
+ public async getOrderCapInfoList(owner: string): Promise<ISLPOrderCapInfo[]> {
927
+ let cursor: string | undefined | null
928
+ let hasNextPage = true
929
+ const orderCapInfoList = []
930
+
931
+ while (hasNextPage) {
932
+ const orderCaps = await this.provider.getOwnedObjects({
933
+ owner,
934
+ filter: {
935
+ StructType: `${this.consts.sudoCore.package}::market::OrderCap`,
936
+ },
937
+ options: {
938
+ showType: true,
939
+ showContent: true,
940
+ },
941
+ cursor,
942
+ })
943
+
944
+ for (const orderCap of orderCaps.data) {
945
+ if (orderCap.data?.type?.includes('OrderCap')) {
946
+ orderCapInfoList.push({
947
+ orderCapId: orderCap.data.objectId,
948
+ symbol0: orderCap.data.type.split('<')[1].split(',')[0].trim(),
949
+ symbol1: orderCap.data.type
950
+ .split('<')[1]
951
+ .split(',')[1]
952
+ .split(',')[0]
953
+ .trim(),
954
+ long: orderCap.data.type.includes('LONG'),
955
+ positionId: (orderCap.data.content as any)?.fields?.position_id,
956
+ })
957
+ }
958
+ }
959
+
960
+ hasNextPage = orderCaps.hasNextPage
961
+ cursor = orderCaps.nextCursor
962
+ }
963
+
964
+ return orderCapInfoList
965
+ }
966
+
967
+ public async hasReferral(referree: string): Promise<boolean> {
968
+ const raw = await this.getReferralData(referree)
969
+ return !raw.error
970
+ }
971
+
972
+ public async getReferralData(referree: string): Promise<any> {
973
+ const raw = await this.provider.getDynamicFieldObject({
974
+ parentId: this.consts.sudoCore.referralsParent,
975
+ name: {
976
+ type: 'address',
977
+ value: referree,
978
+ },
979
+ })
980
+ return raw
981
+ }
982
+
983
+ /**
984
+ * Gets user orders for SLP
985
+ */
986
+ public async getOrderInfoList(
987
+ orderCapInfoList: ISLPOrderCapInfo[],
988
+ owner: string,
989
+ batchSize = 10,
990
+ ): Promise<ISLPOrderInfo[]> {
991
+ const orderInfoList: ISLPOrderInfo[] = []
992
+
993
+ // Process in batches of 10
994
+ for (let i = 0; i < orderCapInfoList.length; i += batchSize) {
995
+ const batch = orderCapInfoList.slice(i, i + batchSize)
996
+
997
+ await Promise.all(batch.map(async (orderCapInfo) => {
998
+ try {
999
+ let orderRaw = await this.provider.getDynamicFieldObject({
1000
+ parentId: this.consts.sudoCore.ordersParent,
1001
+ name: {
1002
+ type: `${this.consts.sudoCore.package}::market::OrderName<${orderCapInfo.symbol0
1003
+ }, ${orderCapInfo.symbol1}, ${this.consts.sudoCore.package
1004
+ }::market::${orderCapInfo.long ? 'LONG' : 'SHORT'}, ${orderCapInfo.symbol0}>`,
1005
+ value: {
1006
+ owner,
1007
+ id: orderCapInfo.orderCapId,
1008
+ position_id: {
1009
+ vec: orderCapInfo.positionId ? [orderCapInfo.positionId] : [],
1010
+ },
1011
+ },
1012
+ },
1013
+ })
1014
+
1015
+ if (orderRaw?.error?.code === 'dynamicFieldNotFound') {
1016
+ orderRaw = await this.provider.getDynamicFieldObject({
1017
+ parentId: this.consts.sudoCore.ordersParent,
1018
+ name: {
1019
+ type: `${this.consts.sudoCore.package}::market::OrderName<${orderCapInfo.symbol0
1020
+ }, ${orderCapInfo.symbol1}, ${this.consts.sudoCore.package
1021
+ }::market::${orderCapInfo.long ? 'LONG' : 'SHORT'}, ${this.consts.coins.sui.module}>`,
1022
+ value: {
1023
+ owner,
1024
+ id: orderCapInfo.orderCapId,
1025
+ position_id: {
1026
+ vec: orderCapInfo.positionId ? [orderCapInfo.positionId] : [],
1027
+ },
1028
+ },
1029
+ },
1030
+ })
1031
+ }
1032
+ orderInfoList.push(
1033
+ this.parseOrderInfo(orderRaw, orderCapInfo.orderCapId),
1034
+ )
1035
+ }
1036
+ catch (error) {
1037
+ // Order might have been deleted
1038
+ console.warn(`Failed to parse order info for order cap ID ${orderCapInfo.orderCapId}: ${error}`)
1039
+ // Continue with next order without adding this one to the list
1040
+ }
1041
+ }))
1042
+ }
1043
+
1044
+ return orderInfoList.sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))
1045
+ }
1046
+
1047
+ public async getCumulativeApr() {
1048
+ const refetchDate = new Date(Date.now() - 3600_000)
1049
+ // fetch new every hour
1050
+ if (
1051
+ !aprResponse?.generatedAt
1052
+ || (aprResponse?.generatedAt
1053
+ && refetchDate > new Date(aprResponse?.generatedAt))
1054
+ ) {
1055
+ try {
1056
+ const url = `${this.apiEndpoint}/cumulativeApr`
1057
+ const res = await fetch(url, {
1058
+ method: 'GET',
1059
+ headers: {
1060
+ 'Content-Type': 'application/json',
1061
+ },
1062
+ })
1063
+ const data = await res.json()
1064
+ // eslint-disable-next-line require-atomic-updates
1065
+ aprResponse = { ...data }
1066
+ return data.cumulativeApr
1067
+ }
1068
+ catch {
1069
+ console.error('Failed to get cumulative APR')
1070
+ }
1071
+
1072
+ return 0
1073
+ }
1074
+ return aprResponse.apr
1075
+ }
1076
+
1077
+ // Private helper methods
1078
+ private static calculatePositionFundingFee(
1079
+ position: ISLPPositionInfo,
1080
+ symbol: ISLPSymbolInfo,
1081
+ model: ISLPFundingFeeModel,
1082
+ price: number,
1083
+ lpSupplyAmount: number,
1084
+ timestamp: number,
1085
+ oiModel?: ISLPOiFundingModel,
1086
+ pairedOpeningSize?: number,
1087
+ ): number {
1088
+ const accFundingRate = this.calcAccFundingFeeRate(
1089
+ symbol,
1090
+ model,
1091
+ price,
1092
+ lpSupplyAmount,
1093
+ timestamp,
1094
+ position.long,
1095
+ oiModel,
1096
+ pairedOpeningSize,
1097
+ )
1098
+ return position.fundingFeeValue + (accFundingRate - position.lastFundingRate) * position.positionSize
1099
+ }
1100
+
1101
+ private static calcAccFundingFeeRate(
1102
+ symbol: ISLPSymbolInfo,
1103
+ model: ISLPFundingFeeModel,
1104
+ price: number,
1105
+ lpSupplyAmount: number,
1106
+ timestamp: number,
1107
+ isLong: boolean,
1108
+ oiModel?: ISLPOiFundingModel,
1109
+ pairedOpeningSize?: number,
1110
+ ): number {
1111
+ if (symbol.lastUpdate > 0) {
1112
+ const elapsed = timestamp - symbol.lastUpdate
1113
+ if (elapsed > 0) {
1114
+ // Prefer OI-based delta when OI model and paired side are available
1115
+ if (oiModel && typeof pairedOpeningSize === 'number') {
1116
+ const longSize = isLong ? symbol.openingSize : pairedOpeningSize
1117
+ const shortSize = isLong ? pairedOpeningSize : symbol.openingSize
1118
+ const deltaRate = SLPDataAPI.calcOiFundingFeeRate(oiModel, longSize, shortSize, elapsed)
1119
+ const appliedRate = isLong ? deltaRate : -deltaRate
1120
+ return symbol.accFundingRate + appliedRate
1121
+ }
1122
+
1123
+ // Fallback to PnL-based funding delta
1124
+ const deltaSize = this.calcDeltaSize(symbol, price, isLong)
1125
+ const pnlPerLp = (symbol.realisedPnl + symbol.unrealisedFundingFeeValue + deltaSize) / lpSupplyAmount
1126
+ return symbol.accFundingRate + SLPDataAPI.calcFundingFeeRate(model, pnlPerLp, elapsed)
1127
+ }
1128
+ }
1129
+ return symbol.accFundingRate
1130
+ }
1131
+
1132
+ private static calcDeltaSize(symbol: ISLPSymbolInfo, price: number, isLong: boolean): number {
1133
+ const latestSize = symbol.openingAmount / symbol.priceConfig.precision * price
1134
+ return isLong ? symbol.openingSize - latestSize : latestSize - symbol.openingSize
1135
+ }
1136
+
1137
+ private static calcFundingFeeRate(model: ISLPFundingFeeModel, pnlPerRate: number, elapsed: number): number {
1138
+ const dailyRate = Math.min(model.multiplier * Math.abs(pnlPerRate), model.max)
1139
+ const secondsRate = dailyRate * elapsed / SECONDS_PER_EIGHT_HOUR
1140
+ return pnlPerRate >= 0 ? -secondsRate : secondsRate
1141
+ }
1142
+
1143
+ private static calcOiFundingFeeRate(model: ISLPOiFundingModel, longSize: number, shortSize: number, elapsed: number): number {
1144
+ const imbalance = Math.abs(longSize - shortSize)
1145
+ const denom = longSize + shortSize > 0 ? longSize + shortSize : 1
1146
+
1147
+ const dailyRate = Math.min(
1148
+ (model.multiplier * (imbalance ** model.exponent)) / denom,
1149
+ model.max,
1150
+ )
1151
+ const secondsRate = dailyRate * elapsed / SECONDS_PER_EIGHT_HOUR
1152
+ return longSize >= shortSize ? secondsRate : -secondsRate
1153
+ }
1154
+
1155
+ private static calculatePositionReserveFee(position: ISLPPositionInfo, vault: ISLPVaultInfo, model: ISLPReservingFeeModel, timestamp: number): number {
1156
+ const accReservingRate = SLPDataAPI.calcAccReservingFeeRate(vault, model, timestamp)
1157
+ return position.reservingFeeAmount + (accReservingRate - position.lastReservingRate) * position.reservedAmount
1158
+ }
1159
+
1160
+ private static calcAccReservingFeeRate(vault: ISLPVaultInfo, model: ISLPReservingFeeModel, timestamp: number): number {
1161
+ if (vault.lastUpdate > 0) {
1162
+ const elapsed = timestamp - vault.lastUpdate
1163
+ if (elapsed > 0) {
1164
+ const utilization = SLPDataAPI.vaultUtilization(vault)
1165
+ return vault.accReservingRate + SLPDataAPI.calcReservingFeeRate(model, utilization, elapsed)
1166
+ }
1167
+ }
1168
+ return vault.accReservingRate
1169
+ }
1170
+
1171
+ private static vaultUtilization(vault: ISLPVaultInfo): number {
1172
+ const supplyAmount = vault.liquidity + vault.reservedAmount + vault.unrealisedReservingFeeAmount
1173
+ if (supplyAmount === 0) {
1174
+ return 0
1175
+ }
1176
+ return vault.reservedAmount / supplyAmount
1177
+ }
1178
+
1179
+ private static calcReservingFeeRate(model: ISLPReservingFeeModel, utilization: number, elapsed: number): number {
1180
+ return model.multiplier * utilization * elapsed / SECONDS_PER_EIGHT_HOUR
1181
+ }
1182
+
1183
+ private static parsePositionConfig(raw: any): ISLPPositionConfig {
1184
+ const positionConfigFields = raw.data.content.fields.inner.fields
1185
+
1186
+ return {
1187
+ decreaseFeeBps: parseValue(positionConfigFields.decrease_fee_bps),
1188
+ liquidationBonus: parseValue(positionConfigFields.liquidation_bonus),
1189
+ liquidationThreshold: parseValue(
1190
+ positionConfigFields.liquidation_threshold,
1191
+ ),
1192
+ maxLeverage: parseValue(positionConfigFields.max_leverage),
1193
+ minHoldingDuration: parseValue(positionConfigFields.min_holding_duration),
1194
+ openFeeBps: parseValue(positionConfigFields.open_fee_bps),
1195
+ maxReservedMultiplier: parseValue(
1196
+ positionConfigFields.max_reserved_multiplier,
1197
+ ),
1198
+ minCollateralValue: parseValue(positionConfigFields.min_collateral_value),
1199
+ }
1200
+ }
1201
+
1202
+ private static parseSymbolConfig(raw: any): ISLPSymbolConfig {
1203
+ const { fields } = raw.data.content
1204
+
1205
+ return {
1206
+ id: fields.id.id,
1207
+ max_opening_size: parseValue(fields.max_opening_size),
1208
+ max_opening_size_enabled: fields.max_opening_size_enabled,
1209
+ max_opening_size_per_position: parseValue(fields.max_opening_size_per_position),
1210
+ max_opening_size_per_position_enabled: fields.max_opening_size_per_position_enabled,
1211
+ instant_exit_fee_config: {
1212
+ instant_exit_tier_1_duration_threshold: parseValue(fields.instant_exit_fee_config.fields.instant_exit_tier_1_duration_threshold),
1213
+ instant_exit_tier_1_fee_bps: parseValue(fields.instant_exit_fee_config.fields.instant_exit_tier_1_fee_bps.fields.value),
1214
+ instant_exit_tier_1_fee_enabled: fields.instant_exit_fee_config.fields.instant_exit_tier_1_fee_enabled,
1215
+ instant_exit_tier_2_duration_threshold: parseValue(fields.instant_exit_fee_config.fields.instant_exit_tier_2_duration_threshold),
1216
+ instant_exit_tier_2_fee_bps: parseValue(fields.instant_exit_fee_config.fields.instant_exit_tier_2_fee_bps.fields.value),
1217
+ instant_exit_tier_2_fee_enabled: fields.instant_exit_fee_config.fields.instant_exit_tier_2_fee_enabled,
1218
+ instant_exit_tier_3_duration_threshold: parseValue(fields.instant_exit_fee_config.fields.instant_exit_tier_3_duration_threshold),
1219
+ instant_exit_tier_3_fee_bps: parseValue(fields.instant_exit_fee_config.fields.instant_exit_tier_3_fee_bps.fields.value),
1220
+ instant_exit_tier_3_fee_enabled: fields.instant_exit_fee_config.fields.instant_exit_tier_3_fee_enabled,
1221
+ },
1222
+ }
1223
+ }
1224
+
1225
+ private static parseRebaseFeeModel(raw: any): ISLPRebaseFeeModel {
1226
+ const { fields } = raw.data.content
1227
+
1228
+ return {
1229
+ base: parseValue(fields.base),
1230
+ multiplier: parseValue(fields.multiplier),
1231
+ }
1232
+ }
1233
+
1234
+ private static calcRebaseFeeRate(model: ISLPRebaseFeeModel, increase: boolean, ratio: number, targetRatio: number): number {
1235
+ if ((increase && ratio <= targetRatio) || (!increase && ratio >= targetRatio)) {
1236
+ return model.base
1237
+ }
1238
+ const exponent = model.exponent ?? 1
1239
+ return model.base + model.multiplier * (Math.abs(ratio - targetRatio) ** exponent)
1240
+ }
1241
+
1242
+ /**
1243
+ * Fetches rebase fee exponent from chain (dynamic field RebaseFeeExponentKey).
1244
+ * Returns 1 (linear) for backward compatibility when not set.
1245
+ */
1246
+ private static async getRebaseFeeExponent(
1247
+ provider: SuiClient,
1248
+ rebaseFeeModelId: string,
1249
+ packageId: string,
1250
+ ): Promise<number> {
1251
+ const WAD = 1e18
1252
+ try {
1253
+ const raw = await provider.getDynamicFieldObject({
1254
+ parentId: rebaseFeeModelId,
1255
+ name: {
1256
+ type: `${packageId}::model::RebaseFeeExponentKey`,
1257
+ value: {},
1258
+ },
1259
+ })
1260
+ if (raw.data?.content && 'fields' in raw.data.content) {
1261
+ const value = (raw.data.content as any).fields?.value
1262
+ if (value !== undefined) {
1263
+ return Number(BigInt(value)) / WAD
1264
+ }
1265
+ }
1266
+ }
1267
+ catch {
1268
+ // Dynamic field doesn't exist - use default WAD (linear)
1269
+ }
1270
+ return 1
1271
+ }
1272
+
1273
+ /**
1274
+ * Computes the spread rate based on OI skew between long and short sides using point evaluation.
1275
+ * This is the offchain equivalent of price_impact::compute_spread_rate in the Move contract.
1276
+ */
1277
+ public static computeSpreadRate(
1278
+ config: ISLPPriceImpactConfig,
1279
+ currentOiThisSide: number,
1280
+ newPositionSize: number,
1281
+ currentOiOpposite: number,
1282
+ maxOiThisSide: number,
1283
+ maxOiOpposite: number,
1284
+ ): number {
1285
+ if (!config.enabled) {
1286
+ return 0
1287
+ }
1288
+ if (maxOiThisSide === 0) {
1289
+ return config.baseSpreadRate
1290
+ }
1291
+ const totalOiThis = currentOiThisSide + newPositionSize
1292
+ const thisUtil = Math.min(totalOiThis / maxOiThisSide, 1)
1293
+ let oppositeUtil = 0
1294
+ if (maxOiOpposite > 0) {
1295
+ oppositeUtil = Math.min(currentOiOpposite / maxOiOpposite, 1)
1296
+ }
1297
+ let skewRatio = thisUtil - oppositeUtil
1298
+ if (skewRatio < 0) {
1299
+ skewRatio = 0
1300
+ }
1301
+ else if (skewRatio > 1) {
1302
+ skewRatio = 1
1303
+ }
1304
+ const exponent = config.impactExponent
1305
+ const exponentInt = Math.floor(exponent)
1306
+ const exponentFrac = exponent - exponentInt
1307
+ let ratioPowered = skewRatio ** exponentInt
1308
+ if (exponentFrac > 0) {
1309
+ const ratioPoweredNext = skewRatio ** (exponentInt + 1)
1310
+ const diff = ratioPoweredNext - ratioPowered
1311
+ if (diff >= 0) {
1312
+ ratioPowered += exponentFrac * diff
1313
+ }
1314
+ }
1315
+ const dynamicSpread = config.maxDynamicSpreadRate * ratioPowered
1316
+ let totalSpread = config.baseSpreadRate + dynamicSpread
1317
+ if (totalSpread > config.maxTotalSpreadRate) {
1318
+ totalSpread = config.maxTotalSpreadRate
1319
+ }
1320
+ return totalSpread
1321
+ }
1322
+
1323
+ private static computeSizeFactor(positionSize: number, referenceSize: number): number {
1324
+ if (referenceSize === 0) {
1325
+ return 1
1326
+ }
1327
+ const ratio = Math.min(positionSize / referenceSize, 1)
1328
+ return 1 + ratio
1329
+ }
1330
+
1331
+ private static computeIntegralAverage(
1332
+ maxDynamic: number,
1333
+ s1: number,
1334
+ s2: number,
1335
+ exponent: number,
1336
+ ): number {
1337
+ if (s1 === s2) {
1338
+ return maxDynamic * (s1 ** exponent)
1339
+ }
1340
+ const nPlus1 = exponent + 1
1341
+ const s2Pow = s2 ** nPlus1
1342
+ const s1Pow = s1 ** nPlus1
1343
+ const numerator = Math.abs(s2Pow - s1Pow)
1344
+ const sDiff = Math.abs(s2 - s1)
1345
+ const denominator = nPlus1 * sDiff
1346
+ if (denominator === 0) {
1347
+ return 0
1348
+ }
1349
+ return maxDynamic * (numerator / denominator)
1350
+ }
1351
+
1352
+ /**
1353
+ * Computes the average spread rate over the trade path with size scaling.
1354
+ * This is the offchain equivalent of price_impact::compute_average_spread_rate in the Move contract.
1355
+ */
1356
+ public static computeAverageSpreadRate(
1357
+ config: ISLPPriceImpactConfig,
1358
+ currentOiThisSide: number,
1359
+ newPositionSize: number,
1360
+ currentOiOpposite: number,
1361
+ maxOiThisSide: number,
1362
+ maxOiOpposite: number,
1363
+ ): number {
1364
+ if (!config.enabled) {
1365
+ return 0
1366
+ }
1367
+ if (maxOiThisSide === 0) {
1368
+ return config.baseSpreadRate
1369
+ }
1370
+ let oppositeUtil = 0
1371
+ if (maxOiOpposite > 0) {
1372
+ oppositeUtil = Math.min(currentOiOpposite / maxOiOpposite, 1)
1373
+ }
1374
+ const startUtil = Math.min(currentOiThisSide / maxOiThisSide, 1)
1375
+ const totalOiThis = currentOiThisSide + newPositionSize
1376
+ const endUtil = Math.min(totalOiThis / maxOiThisSide, 1)
1377
+ let startSkew = startUtil - oppositeUtil
1378
+ if (startSkew < 0) {
1379
+ startSkew = 0
1380
+ }
1381
+ else if (startSkew > 1) {
1382
+ startSkew = 1
1383
+ }
1384
+ let endSkew = endUtil - oppositeUtil
1385
+ if (endSkew < 0) {
1386
+ endSkew = 0
1387
+ }
1388
+ else if (endSkew > 1) {
1389
+ endSkew = 1
1390
+ }
1391
+ const avgDynamicRate = SLPDataAPI.computeIntegralAverage(
1392
+ config.maxDynamicSpreadRate,
1393
+ startSkew,
1394
+ endSkew,
1395
+ config.impactExponent,
1396
+ )
1397
+ const sizeFactor = SLPDataAPI.computeSizeFactor(newPositionSize, config.referenceSize)
1398
+ const scaledDynamicRate = avgDynamicRate * sizeFactor
1399
+ let totalSpread = config.baseSpreadRate + scaledDynamicRate
1400
+ if (totalSpread > config.maxTotalSpreadRate) {
1401
+ totalSpread = config.maxTotalSpreadRate
1402
+ }
1403
+ return totalSpread
1404
+ }
1405
+
1406
+ /**
1407
+ * Applies price impact to a price.
1408
+ * This is the offchain equivalent of price_impact::apply_price_impact in the Move contract.
1409
+ */
1410
+ public static applyPriceImpact(
1411
+ originalPrice: number,
1412
+ spreadRate: number,
1413
+ isLong: boolean,
1414
+ isOpening: boolean,
1415
+ ): number {
1416
+ if (spreadRate === 0) {
1417
+ return originalPrice
1418
+ }
1419
+ const increasePrice = (isLong && isOpening) || (!isLong && !isOpening)
1420
+ const spreadAmount = originalPrice * spreadRate
1421
+ if (increasePrice) {
1422
+ return originalPrice + spreadAmount
1423
+ }
1424
+ const adjusted = originalPrice - spreadAmount
1425
+ return Math.max(adjusted, 0)
1426
+ }
1427
+
1428
+ /**
1429
+ * Convenience method to compute the estimated execution price with price impact.
1430
+ */
1431
+ public async estimatePriceImpact(
1432
+ indexToken: string,
1433
+ isLong: boolean,
1434
+ isOpening: boolean,
1435
+ positionSizeValue: number,
1436
+ ): Promise<{ spreadRate: number, originalPrice: number, adjustedPrice: number } | null> {
1437
+ const config = await this.getPriceImpactConfig(indexToken)
1438
+ if (!config) {
1439
+ return null
1440
+ }
1441
+ const longSymbol = await this.getSymbolInfo(indexToken, true)
1442
+ const shortSymbol = await this.getSymbolInfo(indexToken, false)
1443
+ const oraclePrice = (await this.getOraclePrice(indexToken)).getPriceUnchecked().getPriceAsNumberUnchecked()
1444
+ const currentOiThisSide = isLong ? longSymbol.openingSize : shortSymbol.openingSize
1445
+ const currentOiOpposite = isLong ? shortSymbol.openingSize : longSymbol.openingSize
1446
+ const maxOiThisSide = isLong ? config.maxOiLong : config.maxOiShort
1447
+ const maxOiOpposite = isLong ? config.maxOiShort : config.maxOiLong
1448
+ const spreadRate = SLPDataAPI.computeAverageSpreadRate(
1449
+ config,
1450
+ currentOiThisSide,
1451
+ positionSizeValue,
1452
+ currentOiOpposite,
1453
+ maxOiThisSide,
1454
+ maxOiOpposite,
1455
+ )
1456
+ const adjustedPrice = SLPDataAPI.applyPriceImpact(oraclePrice, spreadRate, isLong, isOpening)
1457
+ return {
1458
+ spreadRate,
1459
+ originalPrice: oraclePrice,
1460
+ adjustedPrice,
1461
+ }
1462
+ }
1463
+
1464
+ private static parseFundingFeeModel(raw: any): ISLPFundingFeeModel {
1465
+ const { fields } = raw.data.content
1466
+
1467
+ return {
1468
+ multiplier: parseValue(fields.multiplier),
1469
+ max: parseValue(fields.max),
1470
+ }
1471
+ }
1472
+
1473
+ private static parseOiFundingState(raw: any): ISLPOiFundingState {
1474
+ const content = raw.data.content.fields
1475
+ return {
1476
+ id: content.id.id,
1477
+ enabled: content.enabled,
1478
+ last_update: parseValue(content.last_update),
1479
+ model: {
1480
+ id: content.model.fields.id.id,
1481
+ multiplier: parseValue(content.model.fields.multiplier),
1482
+ exponent: parseValue(content.model.fields.exponent),
1483
+ max: parseValue(content.model.fields.max),
1484
+ },
1485
+ }
1486
+ }
1487
+
1488
+ private static parsePriceImpactConfig(raw: any): ISLPPriceImpactConfig {
1489
+ const content = raw.data.content.fields
1490
+ return {
1491
+ id: content.id.id,
1492
+ enabled: content.enabled,
1493
+ baseSpreadRate: parseValue(content.base_spread_rate),
1494
+ maxDynamicSpreadRate: parseValue(content.max_dynamic_spread_rate),
1495
+ maxTotalSpreadRate: parseValue(content.max_total_spread_rate),
1496
+ impactExponent: parseValue(content.impact_exponent),
1497
+ maxOiLong: parseValue(content.max_oi_long),
1498
+ maxOiShort: parseValue(content.max_oi_short),
1499
+ referenceSize: parseValue(content.reference_size),
1500
+ }
1501
+ }
1502
+
1503
+ private static parseMarketInfo(raw: any): ISLPMarketInfo {
1504
+ const content = raw.data.content.fields
1505
+
1506
+ return {
1507
+ lpSupply: content.lp_supply.fields.value,
1508
+ positionId: content.positions.fields.id.id,
1509
+ vaultId: content.vaults.fields.id.id,
1510
+ symbolId: content.symbols.fields.id.id,
1511
+ lpSupplyWithDecimals:
1512
+ content.lp_supply.fields.value / 10 ** SLP_TOKEN_DECIMALS,
1513
+ }
1514
+ }
1515
+
1516
+ private async parseVaultInfo(raw: any): Promise<ISLPVaultInfo> {
1517
+ const vaultFields = raw.data.content.fields.value.fields
1518
+ const reservingFeeModelAddr = vaultFields.reserving_fee_model
1519
+ const reservingFeeModelRaw = await this.provider.getObject({
1520
+ id: reservingFeeModelAddr,
1521
+ options: {
1522
+ showContent: true,
1523
+ },
1524
+ })
1525
+ const reservingFeeModel = SLPDataAPI.parseReservingFeeModel(reservingFeeModelRaw)
1526
+
1527
+ return {
1528
+ liquidity: parseValue(vaultFields.liquidity),
1529
+ reservedAmount: parseValue(vaultFields.reserved_amount),
1530
+ unrealisedReservingFeeAmount: parseValue(
1531
+ vaultFields.unrealised_reserving_fee_amount,
1532
+ ),
1533
+ accReservingRate: parseValue(vaultFields.acc_reserving_rate),
1534
+ enabled: vaultFields.enabled,
1535
+ weight: parseValue(vaultFields.weight),
1536
+ lastUpdate: parseValue(vaultFields.last_update),
1537
+ reservingFeeModel,
1538
+ priceConfig: {
1539
+ maxInterval: parseValue(vaultFields.price_config.fields.max_interval),
1540
+ maxConfidence: parseValue(vaultFields.price_config.fields.max_confidence),
1541
+ precision: parseValue(vaultFields.price_config.fields.precision),
1542
+ feeder: vaultFields.price_config.fields.feeder,
1543
+ },
1544
+ }
1545
+ }
1546
+
1547
+ private static parseReservingFeeModel(raw: any): { multiplier: number } {
1548
+ const content = raw.data.content.fields
1549
+ return {
1550
+ multiplier: parseValue(content.multiplier),
1551
+ }
1552
+ }
1553
+
1554
+ private async parseSymbolInfo(raw: any, long: boolean): Promise<ISLPSymbolInfo> {
1555
+ const { objectId } = raw.data
1556
+ const { fields } = raw.data.content.fields.value
1557
+ const fundingFeeModelAddr = fields.funding_fee_model
1558
+ const fundingFeeModelRaw = await this.provider.getObject({
1559
+ id: fundingFeeModelAddr,
1560
+ options: {
1561
+ showContent: true,
1562
+ },
1563
+ })
1564
+ const fundingFeeModel = SLPDataAPI.parseFundingFeeModel(fundingFeeModelRaw)
1565
+
1566
+ return {
1567
+ objectId,
1568
+ openingSize: parseValue(fields.opening_size),
1569
+ openingAmount: parseValue(fields.opening_amount),
1570
+ accFundingRate: parseValue(fields.acc_funding_rate),
1571
+ realisedPnl: parseValue(fields.realised_pnl),
1572
+ unrealisedFundingFeeValue: parseValue(
1573
+ fields.unrealised_funding_fee_value,
1574
+ ),
1575
+ openEnabled: fields.open_enabled,
1576
+ liquidateEnabled: fields.liquidate_enabled,
1577
+ decreaseEnabled: fields.decrease_enabled,
1578
+ lastUpdate: parseValue(fields.last_update),
1579
+ fundingFeeModel,
1580
+ long,
1581
+ priceConfig: {
1582
+ maxInterval: parseValue(fields.price_config.fields.max_interval),
1583
+ maxConfidence: parseValue(fields.price_config.fields.max_confidence),
1584
+ precision: parseValue(fields.price_config.fields.precision),
1585
+ feeder: fields.price_config.fields.feeder,
1586
+ },
1587
+ }
1588
+ }
1589
+
1590
+ private async parsePositionInfo(raw: any, id_: string): Promise<ISLPPositionInfo> {
1591
+ const { content } = raw.data
1592
+ const { fields } = content
1593
+ const positionFields = fields.value.fields
1594
+ const dataType = fields.name.type
1595
+
1596
+ const positionInfo = {
1597
+ id: id_,
1598
+ long: dataType.includes('::market::LONG'),
1599
+ owner: fields.name.fields.owner,
1600
+ version: Number.parseInt(raw.data.version, 10),
1601
+ collateralToken: suiSymbolToSymbol(
1602
+ dataType.split('<')[1].split(',')[0].trim(),
1603
+ this.consts,
1604
+ ),
1605
+ indexToken: suiSymbolToSymbol(dataType.split(',')[1].trim(), this.consts),
1606
+ collateralAmount: parseValue(positionFields.collateral),
1607
+ positionAmount: parseValue(positionFields.position_amount),
1608
+ reservedAmount: parseValue(positionFields.reserved),
1609
+ positionSize: parseValue(positionFields.position_size),
1610
+ lastFundingRate: parseValue(positionFields.last_funding_rate),
1611
+ lastReservingRate: parseValue(positionFields.last_reserving_rate),
1612
+ reservingFeeAmount: parseValue(positionFields.reserving_fee_amount),
1613
+ fundingFeeValue: parseValue(positionFields.funding_fee_value),
1614
+ closed: positionFields.closed,
1615
+ openTimestamp: parseValue(positionFields.open_timestamp),
1616
+ protocol: 'sudo',
1617
+ }
1618
+
1619
+ if (!positionFields.closed) {
1620
+ try {
1621
+ positionInfo.reservingFeeAmount = SLPDataAPI.calculatePositionReserveFee(
1622
+ positionInfo,
1623
+ await this.getVaultInfo(positionInfo.collateralToken),
1624
+ (await this.getVaultInfo(positionInfo.collateralToken)).reservingFeeModel,
1625
+ Date.now() / 1000,
1626
+ )
1627
+
1628
+ // OI context for funding: fetch state and paired side size when enabled
1629
+ const oiState = await this.getSymbolOiFundingState(positionInfo.indexToken)
1630
+ const pairedSymbol = await this.getSymbolInfo(positionInfo.indexToken, !positionInfo.long)
1631
+
1632
+ positionInfo.fundingFeeValue = SLPDataAPI.calculatePositionFundingFee(
1633
+ positionInfo,
1634
+ await this.getSymbolInfo(positionInfo.indexToken, positionInfo.long),
1635
+ (await this.getSymbolInfo(positionInfo.indexToken, positionInfo.long)).fundingFeeModel,
1636
+ (await this.getOraclePrice(positionInfo.indexToken)).getPriceUnchecked().getPriceAsNumberUnchecked(),
1637
+ (await this.getMarketInfo()).lpSupplyWithDecimals,
1638
+ Date.now() / 1000,
1639
+ oiState && oiState.enabled ? oiState.model : undefined,
1640
+ pairedSymbol.openingSize,
1641
+ )
1642
+ }
1643
+ catch (e) {
1644
+ console.error(e)
1645
+ positionInfo.reservingFeeAmount = 0
1646
+ positionInfo.fundingFeeValue = 0
1647
+ }
1648
+ }
1649
+
1650
+ return positionInfo
1651
+ }
1652
+
1653
+ private parseOrderInfo(raw: any, capId: string): ISLPOrderInfo {
1654
+ const { content } = raw.data
1655
+ const { fields } = content.fields.value
1656
+
1657
+ // Extract tokens from dataType
1658
+ const dataType = content?.type
1659
+
1660
+ let orderType: IBaseOrderType
1661
+ if (content.fields.value?.type.includes('OpenPositionOrder')) {
1662
+ orderType = 'OPEN_POSITION'
1663
+ }
1664
+ else if (content.fields.value?.type.includes('OpenMarketOrder')) {
1665
+ orderType = 'OPEN_MARKET'
1666
+ }
1667
+ else if (content.fields.value?.type.includes('DecreasePositionOrder')) {
1668
+ orderType = 'DECREASE_POSITION'
1669
+ }
1670
+ else {
1671
+ orderType = 'DECREASE_MARKET'
1672
+ }
1673
+ const orderVersion = content.fields.value?.type.includes('V2')
1674
+ ? 'V2'
1675
+ : 'V1_1'
1676
+
1677
+ const ret: ISLPOrderInfo = {
1678
+ id: content.fields.id.id,
1679
+ capId,
1680
+ executed: fields.executed,
1681
+ owner: content.fields.name.fields.owner,
1682
+ collateralToken: suiSymbolToSymbol(
1683
+ dataType.split('<')[2].split(',')[0].trim(),
1684
+ this.consts,
1685
+ ),
1686
+ indexToken: suiSymbolToSymbol(dataType.split(',')[1].trim(), this.consts),
1687
+ feeToken: suiSymbolToSymbol(
1688
+ dataType.split(',')[3].split('>')[0].trim(),
1689
+ this.consts,
1690
+ ),
1691
+ // Use index_price for open orders, limited_index_price for decrease orders
1692
+ indexPrice: orderType === 'OPEN_MARKET'
1693
+ ? parseValue(fields.index_price)
1694
+ : parseValue(fields.limited_index_price?.fields?.price || fields.limited_index_price),
1695
+ collateralPriceThreshold: fields.collateral_price_threshold ? parseValue(fields.collateral_price_threshold) : 0,
1696
+ // New index price threshold
1697
+ indexPriceThreshold: fields.index_price_threshold ? parseValue(fields.index_price_threshold) : undefined,
1698
+ feeAmount: BigInt(fields.fee),
1699
+ long: dataType.includes('::market::LONG'),
1700
+ orderType,
1701
+ orderVersion,
1702
+ createdAt: parseValue(fields.created_at),
1703
+ v11Order: !fields.limited_index_price?.fields?.price,
1704
+ // New: referrer and scard fields
1705
+ referrer: fields.referrer,
1706
+ scardId: (
1707
+ fields.scard_id?.fields?.some?.fields?.id
1708
+ || fields.scard_id?.fields?.value?.fields?.id
1709
+ || fields.scard_id?.fields?.id
1710
+ || fields.scard_id?.id
1711
+ ),
1712
+ scardRebateRate: (() => {
1713
+ const inner = fields.scard_rebate_rate?.fields?.some ?? fields.scard_rebate_rate?.fields?.value
1714
+ return inner ? parseValue(inner) : undefined
1715
+ })(),
1716
+ }
1717
+
1718
+ if (orderType === 'OPEN_POSITION' || orderType === 'OPEN_MARKET') {
1719
+ ret.openOrder = {
1720
+ reserveAmount: BigInt(fields.reserve_amount),
1721
+ collateralAmount: BigInt(fields.collateral),
1722
+ openAmount: BigInt(fields.open_amount),
1723
+ // New: position_config from OpenMarketOrder
1724
+ positionConfig: fields.position_config?.fields
1725
+ ? {
1726
+ decreaseFeeBps: parseValue(fields.position_config.fields.decrease_fee_bps),
1727
+ liquidationBonus: parseValue(fields.position_config.fields.liquidation_bonus),
1728
+ liquidationThreshold: parseValue(fields.position_config.fields.liquidation_threshold),
1729
+ maxLeverage: parseValue(fields.position_config.fields.max_leverage),
1730
+ minHoldingDuration: parseValue(fields.position_config.fields.min_holding_duration),
1731
+ openFeeBps: parseValue(fields.position_config.fields.open_fee_bps),
1732
+ maxReservedMultiplier: parseValue(fields.position_config.fields.max_reserved_multiplier),
1733
+ minCollateralValue: parseValue(fields.position_config.fields.min_collateral_value),
1734
+ }
1735
+ : undefined,
1736
+ }
1737
+ }
1738
+ else if (orderType === 'DECREASE_POSITION' || orderType === 'DECREASE_MARKET') {
1739
+ ret.decreaseOrder = {
1740
+ decreaseAmount: BigInt(fields.decrease_amount),
1741
+ takeProfit: fields.take_profit,
1742
+ }
1743
+ }
1744
+
1745
+ return ret
1746
+ }
1747
+
1748
+ private static parseCredential(raw: any, pool: ISLPStakePool): ISLPCredential {
1749
+ const stakedAmount = BigInt(raw.data.content.fields.stake)
1750
+ const accRewardPerShare = BigInt(
1751
+ raw.data.content.fields.acc_reward_per_share,
1752
+ )
1753
+ return {
1754
+ id: raw.data.objectId,
1755
+ lockUntil: parseValue(raw.data.content.fields.lock_until),
1756
+ amount: stakedAmount,
1757
+ accRewardPerShare,
1758
+ claimable:
1759
+ ((pool.accRewardPerShare - accRewardPerShare) * stakedAmount)
1760
+ / BigInt(1e18),
1761
+ }
1762
+ }
1763
+
1764
+ private static parseStakePool(raw: any): ISLPStakePool {
1765
+ const content = raw.data.content.fields
1766
+
1767
+ // Check if this is the new version with reward_vault and reward_rate
1768
+ const isNewVersion = content.reward_vault !== undefined && content.reward_rate !== undefined
1769
+
1770
+ const pool: ISLPStakePool = {
1771
+ id: content.id.id,
1772
+ enabled: content.enabled,
1773
+ lastUpdatedTime: parseValue(content.last_updated_time),
1774
+ stakedAmount: BigInt(content.staked_amount),
1775
+ // Support both old 'reward' and new 'reward_vault' field names
1776
+ reward: BigInt(content.reward_vault || content.reward),
1777
+ startTime: parseValue(content.start_time),
1778
+ endTime: parseValue(content.end_time),
1779
+ accRewardPerShare: BigInt(content.acc_reward_per_share),
1780
+ lockDuration: parseValue(content.lock_duration),
1781
+ // Add rewardRate if available (new version)
1782
+ ...(isNewVersion && { rewardRate: BigInt(content.reward_rate) }),
1783
+ }
1784
+
1785
+ // Use appropriate refresh method based on version
1786
+ if (isNewVersion) {
1787
+ SLPDataAPI.refreshPoolV2(pool, Math.floor(Date.now() / 1000))
1788
+ }
1789
+ else {
1790
+ SLPDataAPI.refreshPool(pool, Math.floor(Date.now() / 1000))
1791
+ }
1792
+
1793
+ return pool
1794
+ }
1795
+
1796
+ // Legacy refresh method for backward compatibility
1797
+ private static refreshPool(pool: ISLPStakePool, timestamp: number): void {
1798
+ if (timestamp === pool.lastUpdatedTime || timestamp < pool.startTime) {
1799
+ return
1800
+ }
1801
+ if (
1802
+ pool.lastUpdatedTime === pool.endTime
1803
+ || pool.stakedAmount === BigInt(0)
1804
+ ) {
1805
+ return
1806
+ }
1807
+ if (timestamp > pool.endTime) {
1808
+ timestamp = pool.endTime
1809
+ }
1810
+
1811
+ const rewardAmount
1812
+ = (pool.reward * BigInt(timestamp - pool.lastUpdatedTime))
1813
+ / BigInt(pool.endTime - pool.lastUpdatedTime)
1814
+
1815
+ pool.lastUpdatedTime = timestamp
1816
+ const rewardPerShare = (rewardAmount * BigInt(1e18)) / pool.stakedAmount
1817
+ pool.accRewardPerShare += rewardPerShare
1818
+ }
1819
+
1820
+ // New refresh method matching SLP implementation
1821
+ private static refreshPoolV2(pool: ISLPStakePool, timestamp: number): void {
1822
+ if (timestamp <= pool.lastUpdatedTime || timestamp < pool.startTime) {
1823
+ return
1824
+ }
1825
+
1826
+ const applicableEndTime = Math.max(pool.endTime, pool.lastUpdatedTime)
1827
+
1828
+ const calculationEndTime = Math.min(timestamp, applicableEndTime)
1829
+
1830
+ if (calculationEndTime > pool.lastUpdatedTime && pool.stakedAmount > BigInt(0) && pool.rewardRate && pool.rewardRate > BigInt(0)) {
1831
+ const timeDiff = BigInt(calculationEndTime - pool.lastUpdatedTime)
1832
+ const rewardAmount = timeDiff * pool.rewardRate
1833
+ const rewardPerShare = (rewardAmount * BigInt(1e18)) / pool.stakedAmount
1834
+ pool.accRewardPerShare += rewardPerShare
1835
+ }
1836
+
1837
+ pool.lastUpdatedTime = calculationEndTime
1838
+ }
1839
+ }