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