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