@scallop-io/sui-scallop-sdk 3.0.2 → 4.1.0

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 (283) hide show
  1. package/dist/Logger-Cg2iFdpH.d.ts +8 -0
  2. package/dist/builder.cjs +22 -0
  3. package/dist/builder.d.cts +28 -0
  4. package/dist/builder.d.ts +28 -0
  5. package/dist/builder.js +4 -0
  6. package/dist/chunk-33AK5RWM.js +15 -0
  7. package/dist/chunk-6UQ4NWEU.js +3 -0
  8. package/dist/chunk-DIC6P5HB.js +11 -0
  9. package/dist/chunk-S34VOEZN.js +3 -0
  10. package/dist/chunk-VTGQACWU.js +6 -0
  11. package/dist/client.cjs +21 -0
  12. package/dist/client.d.cts +401 -0
  13. package/dist/client.d.ts +401 -0
  14. package/dist/client.js +5 -0
  15. package/dist/errors.cjs +10 -0
  16. package/dist/errors.d.cts +32 -0
  17. package/dist/errors.d.ts +32 -0
  18. package/dist/errors.js +1 -0
  19. package/dist/index.cjs +51 -34
  20. package/dist/index.d.cts +130 -3145
  21. package/dist/index.d.ts +130 -3145
  22. package/dist/index.js +8 -12
  23. package/dist/logger.cjs +6 -0
  24. package/dist/logger.d.cts +7 -0
  25. package/dist/logger.d.ts +7 -0
  26. package/dist/logger.js +1 -0
  27. package/dist/query-DkitepDi.d.ts +3305 -0
  28. package/dist/query.cjs +19 -0
  29. package/dist/query.d.cts +9 -0
  30. package/dist/query.d.ts +9 -0
  31. package/dist/query.js +3 -0
  32. package/dist/types.cjs +2 -0
  33. package/dist/types.d.cts +90 -0
  34. package/dist/types.d.ts +90 -0
  35. package/dist/types.js +1 -0
  36. package/package.json +98 -56
  37. package/src/constants/api.ts +2 -0
  38. package/src/constants/coinType.ts +2 -0
  39. package/src/constants/common.ts +0 -7
  40. package/src/constants/index.ts +2 -0
  41. package/src/constants/queryKeys.ts +14 -7
  42. package/src/constants/testAddress.ts +1 -0
  43. package/src/datasources/api.ts +34 -0
  44. package/src/datasources/indexer.ts +8 -0
  45. package/src/datasources/onchain.ts +121 -0
  46. package/src/entries/builder.ts +5 -0
  47. package/src/entries/client.ts +3 -0
  48. package/src/entries/errors.ts +2 -0
  49. package/src/entries/index.ts +17 -0
  50. package/src/entries/logger.ts +2 -0
  51. package/src/entries/query.ts +6 -0
  52. package/src/entries/types.ts +2 -0
  53. package/src/errors/ScallopConfigError.ts +8 -0
  54. package/src/errors/ScallopError.ts +30 -0
  55. package/src/errors/ScallopIndexerError.ts +8 -0
  56. package/src/errors/ScallopParseError.ts +8 -0
  57. package/src/errors/ScallopRpcError.ts +8 -0
  58. package/src/errors/ScallopTransactionBuildError.ts +8 -0
  59. package/src/errors/index.ts +6 -0
  60. package/src/logger/Logger.ts +6 -0
  61. package/src/logger/consoleLogger.ts +13 -0
  62. package/src/logger/index.ts +3 -0
  63. package/src/logger/noopLogger.ts +8 -0
  64. package/src/mappers/index.ts +1 -0
  65. package/src/mappers/moveTypeMapper.ts +48 -0
  66. package/src/models/index.ts +8 -10
  67. package/src/models/interface.ts +9 -9
  68. package/src/models/scallop.ts +9 -16
  69. package/src/models/scallopAddress/const.ts +416 -0
  70. package/src/models/scallopAddress/index.ts +188 -0
  71. package/src/models/scallopAddress/types.ts +212 -0
  72. package/src/models/{scallopBuilder.ts → scallopBuilder/index.ts} +65 -29
  73. package/src/models/scallopBuilder/types.ts +37 -0
  74. package/src/models/{scallopClient.ts → scallopClient/index.ts} +157 -598
  75. package/src/models/scallopClient/types.ts +25 -0
  76. package/src/models/scallopConstants/config/AddressConfigSource.ts +13 -0
  77. package/src/models/scallopConstants/config/ConfigValidator.ts +70 -0
  78. package/src/models/scallopConstants/config/PoolAddressConfigSource.ts +12 -0
  79. package/src/models/scallopConstants/config/ScallopConfig.ts +80 -0
  80. package/src/models/scallopConstants/config/ScallopConfigSnapshot.ts +46 -0
  81. package/src/models/scallopConstants/config/WhitelistConfigSource.ts +16 -0
  82. package/src/models/scallopConstants/config/index.ts +6 -0
  83. package/src/models/scallopConstants/const.ts +19 -0
  84. package/src/models/scallopConstants/constantsSource.ts +98 -0
  85. package/src/models/scallopConstants/deriveConstants.ts +150 -0
  86. package/src/models/scallopConstants/index.ts +257 -0
  87. package/src/models/scallopConstants/loadConstantsState.ts +135 -0
  88. package/src/models/scallopConstants/types.ts +77 -0
  89. package/src/models/scallopConstants/utils.ts +69 -0
  90. package/src/models/scallopQuery/index.ts +1279 -0
  91. package/src/models/scallopQuery/types.ts +9 -0
  92. package/src/models/scallopQuery/utils.ts +13 -0
  93. package/src/models/scallopUtils/index.ts +505 -0
  94. package/src/models/scallopUtils/types.ts +30 -0
  95. package/src/models/transactionExecutor.ts +122 -0
  96. package/src/repositories/addressApi/helpers.ts +14 -0
  97. package/src/repositories/addressApi/index.ts +24 -0
  98. package/src/repositories/addressApi/schema.ts +0 -0
  99. package/src/repositories/addressApi/types.ts +177 -0
  100. package/src/repositories/base.ts +38 -0
  101. package/src/repositories/borrowIncentive/bcs.ts +11 -0
  102. package/src/repositories/borrowIncentive/helpers.ts +396 -0
  103. package/src/repositories/borrowIncentive/index.ts +52 -0
  104. package/src/{types/query/borrowIncentive.ts → repositories/borrowIncentive/types.ts} +69 -23
  105. package/src/repositories/borrowIncentive/utils.ts +234 -0
  106. package/src/repositories/cache.ts +4 -0
  107. package/src/repositories/coinBalance/helpers.ts +394 -0
  108. package/src/repositories/coinBalance/index.ts +69 -0
  109. package/src/repositories/coinBalance/types.ts +33 -0
  110. package/src/repositories/flashloan/const.ts +3 -0
  111. package/src/repositories/flashloan/helpers.ts +107 -0
  112. package/src/repositories/flashloan/index.ts +34 -0
  113. package/src/repositories/flashloan/types.ts +16 -0
  114. package/src/repositories/isolatedAssets/bcs.ts +4 -0
  115. package/src/repositories/isolatedAssets/const.ts +2 -0
  116. package/src/repositories/isolatedAssets/helpers.ts +91 -0
  117. package/src/repositories/isolatedAssets/index.ts +42 -0
  118. package/src/repositories/isolatedAssets/types.ts +35 -0
  119. package/src/repositories/loyaltyProgram/bcs.ts +7 -0
  120. package/src/repositories/loyaltyProgram/helpers.ts +117 -0
  121. package/src/repositories/loyaltyProgram/index.ts +28 -0
  122. package/src/repositories/loyaltyProgram/schema.ts +18 -0
  123. package/src/repositories/loyaltyProgram/types.ts +37 -0
  124. package/src/repositories/market/const.ts +6 -0
  125. package/src/repositories/market/helpers.ts +740 -0
  126. package/src/repositories/market/index.ts +137 -0
  127. package/src/repositories/market/mapper.ts +30 -0
  128. package/src/repositories/market/types.ts +443 -0
  129. package/src/repositories/market/utils.ts +285 -0
  130. package/src/repositories/obligation/helpers.ts +228 -0
  131. package/src/repositories/obligation/index.ts +51 -0
  132. package/src/repositories/obligation/types.ts +71 -0
  133. package/src/repositories/obligation/utils.ts +57 -0
  134. package/src/repositories/poolAddresses/const.ts +5 -0
  135. package/src/repositories/poolAddresses/helpers.ts +422 -0
  136. package/src/repositories/poolAddresses/index.ts +56 -0
  137. package/src/repositories/poolAddresses/schema.ts +34 -0
  138. package/src/repositories/poolAddresses/types.ts +69 -0
  139. package/src/repositories/price/const.ts +1 -0
  140. package/src/repositories/price/helpers.ts +237 -0
  141. package/src/repositories/price/index.ts +91 -0
  142. package/src/repositories/price/schema.ts +24 -0
  143. package/src/repositories/price/types.ts +56 -0
  144. package/src/repositories/price/utils.ts +21 -0
  145. package/src/repositories/referral/bcs.ts +3 -0
  146. package/src/repositories/referral/helper.ts +24 -0
  147. package/src/repositories/referral/index.ts +31 -0
  148. package/src/repositories/referral/types.ts +33 -0
  149. package/src/repositories/spool/const.ts +1 -0
  150. package/src/repositories/spool/helpers.ts +471 -0
  151. package/src/repositories/spool/index.ts +135 -0
  152. package/src/{types/query/spool.ts → repositories/spool/types.ts} +67 -2
  153. package/src/repositories/spool/utils.ts +215 -0
  154. package/src/repositories/types.ts +39 -0
  155. package/src/repositories/utils.ts +137 -0
  156. package/src/repositories/veSca/bcs.ts +6 -0
  157. package/src/repositories/veSca/helpers.ts +341 -0
  158. package/src/repositories/veSca/index.ts +57 -0
  159. package/src/repositories/veSca/types.ts +79 -0
  160. package/src/repositories/veScaLoyaltyProgram/bcs.ts +3 -0
  161. package/src/repositories/veScaLoyaltyProgram/helpers.ts +135 -0
  162. package/src/repositories/veScaLoyaltyProgram/index.ts +28 -0
  163. package/src/repositories/veScaLoyaltyProgram/schema.ts +15 -0
  164. package/src/repositories/veScaLoyaltyProgram/types.ts +47 -0
  165. package/src/repositories/wiring/datasources.ts +42 -0
  166. package/src/repositories/wiring/metadata.ts +292 -0
  167. package/src/repositories/wiring/registry.ts +194 -0
  168. package/src/repositories/wiring/source.ts +63 -0
  169. package/src/repositories/xOracle/bcs.ts +7 -0
  170. package/src/repositories/xOracle/const.ts +1 -0
  171. package/src/repositories/xOracle/helpers.ts +304 -0
  172. package/src/repositories/xOracle/index.ts +45 -0
  173. package/src/repositories/xOracle/types.ts +96 -0
  174. package/src/repositories/xOracle/utils.ts +3 -0
  175. package/src/services/client/BorrowService.ts +229 -0
  176. package/src/services/client/CollateralService.ts +76 -0
  177. package/src/services/client/LendingService.ts +75 -0
  178. package/src/services/client/ReferralService.ts +71 -0
  179. package/src/services/client/SpoolService.ts +170 -0
  180. package/src/services/client/VeScaService.ts +142 -0
  181. package/src/services/client/types.ts +44 -0
  182. package/src/services/index.ts +8 -0
  183. package/src/services/query/portfolioCalculations.ts +1171 -0
  184. package/src/txBuilders/borrowIncentive/index.ts +80 -0
  185. package/src/txBuilders/borrowIncentive/moveCalls.ts +124 -0
  186. package/src/txBuilders/borrowIncentive/quick.ts +207 -0
  187. package/src/txBuilders/context.ts +62 -0
  188. package/src/txBuilders/core/index.ts +95 -0
  189. package/src/txBuilders/core/moveCalls.ts +241 -0
  190. package/src/txBuilders/core/quick.ts +306 -0
  191. package/src/{builders → txBuilders}/index.ts +35 -9
  192. package/src/txBuilders/loyaltyProgram/index.ts +78 -0
  193. package/src/txBuilders/loyaltyProgram/moveCalls.ts +44 -0
  194. package/src/txBuilders/loyaltyProgram/quick.ts +58 -0
  195. package/src/txBuilders/manifest.ts +160 -0
  196. package/src/txBuilders/modules.ts +70 -0
  197. package/src/{builders → txBuilders}/oracles/index.ts +3 -2
  198. package/src/{builders → txBuilders}/oracles/pyth.ts +4 -3
  199. package/src/txBuilders/referral/index.ts +72 -0
  200. package/src/txBuilders/referral/moveCalls.ts +93 -0
  201. package/src/txBuilders/referral/quick.ts +57 -0
  202. package/src/txBuilders/sCoin/index.ts +72 -0
  203. package/src/txBuilders/sCoin/moveCalls.ts +36 -0
  204. package/src/txBuilders/sCoin/quick.ts +34 -0
  205. package/src/txBuilders/spool/index.ts +82 -0
  206. package/src/txBuilders/spool/moveCalls.ts +79 -0
  207. package/src/{builders/spoolBuilder.ts → txBuilders/spool/quick.ts} +22 -162
  208. package/src/txBuilders/utils.ts +36 -0
  209. package/src/txBuilders/verify.ts +45 -0
  210. package/src/txBuilders/vesca/index.ts +75 -0
  211. package/src/txBuilders/vesca/moveCalls.ts +147 -0
  212. package/src/{builders/vescaBuilder.ts → txBuilders/vesca/quick.ts} +85 -274
  213. package/src/types/address.ts +4 -178
  214. package/src/types/builder/borrowIncentive.ts +4 -3
  215. package/src/types/builder/core.ts +6 -33
  216. package/src/types/builder/index.ts +5 -1
  217. package/src/types/builder/loyaltyProgram.ts +4 -3
  218. package/src/types/builder/modules.ts +49 -0
  219. package/src/types/builder/referral.ts +5 -4
  220. package/src/types/builder/sCoin.ts +20 -2
  221. package/src/types/builder/spool.ts +22 -2
  222. package/src/types/builder/vesca.ts +4 -12
  223. package/src/types/constant/index.ts +3 -1
  224. package/src/types/constant/queryKeys.ts +15 -8
  225. package/src/types/index.ts +5 -6
  226. package/src/types/internal/dto.ts +46 -0
  227. package/src/types/internal/index.ts +2 -0
  228. package/src/types/internal/move.ts +13 -0
  229. package/src/types/public/index.ts +21 -0
  230. package/src/types/query/core.ts +0 -317
  231. package/src/types/query/index.ts +3 -4
  232. package/src/types/query/portfolio.ts +2 -15
  233. package/src/types/repositories/borrowIncentive.ts +12 -0
  234. package/src/types/repositories/index.ts +9 -0
  235. package/src/types/repositories/loyaltyProgram.ts +2 -0
  236. package/src/types/repositories/market.ts +13 -0
  237. package/src/types/repositories/spool.ts +21 -0
  238. package/src/types/repositories/veSca.ts +5 -0
  239. package/src/types/repositories/veScaLoyaltyProgram.ts +2 -0
  240. package/src/types/sui.ts +0 -1
  241. package/src/utils/array.ts +7 -0
  242. package/src/utils/cache.ts +17 -0
  243. package/src/utils/object.ts +30 -12
  244. package/src/utils/query.ts +9 -190
  245. package/src/utils/vesca.ts +0 -8
  246. package/src/builders/borrowIncentiveBuilder.ts +0 -372
  247. package/src/builders/coreBuilder.ts +0 -636
  248. package/src/builders/loyaltyProgramBuilder.ts +0 -142
  249. package/src/builders/referralBuilder.ts +0 -192
  250. package/src/builders/sCoinBuilder.ts +0 -124
  251. package/src/index.ts +0 -13
  252. package/src/models/scallopAddress.ts +0 -841
  253. package/src/models/scallopAxios.ts +0 -185
  254. package/src/models/scallopConstants.ts +0 -380
  255. package/src/models/scallopIndexer.ts +0 -226
  256. package/src/models/scallopQuery.ts +0 -974
  257. package/src/models/scallopQueryClient.ts +0 -29
  258. package/src/models/scallopSuiKit.ts +0 -480
  259. package/src/models/scallopUtils.ts +0 -734
  260. package/src/queries/borrowIncentiveQuery.ts +0 -361
  261. package/src/queries/borrowLimitQuery.ts +0 -33
  262. package/src/queries/coreQuery.ts +0 -1173
  263. package/src/queries/flashloanFeeQuery.ts +0 -92
  264. package/src/queries/index.ts +0 -15
  265. package/src/queries/isolatedAssetQuery.ts +0 -103
  266. package/src/queries/loyaltyProgramQuery.ts +0 -178
  267. package/src/queries/ownerQuery.ts +0 -32
  268. package/src/queries/poolAddressesQuery.ts +0 -340
  269. package/src/queries/portfolioQuery.ts +0 -1149
  270. package/src/queries/priceQuery.ts +0 -37
  271. package/src/queries/referralQuery.ts +0 -42
  272. package/src/queries/sCoinQuery.ts +0 -195
  273. package/src/queries/spoolQuery.ts +0 -542
  274. package/src/queries/supplyLimitQuery.ts +0 -32
  275. package/src/queries/switchboardQuery.ts +0 -65
  276. package/src/queries/vescaQuery.ts +0 -312
  277. package/src/queries/xOracleQuery.ts +0 -149
  278. package/src/types/constant/common.ts +0 -55
  279. package/src/types/query/loyaltyProgram.ts +0 -12
  280. package/src/types/query/vesca.ts +0 -27
  281. package/src/utils/index.ts +0 -7
  282. package/src/utils/indexer.ts +0 -47
  283. /package/src/{models → datasources}/rateLimiter.ts +0 -0
@@ -0,0 +1,1171 @@
1
+ import { BigNumber } from 'bignumber.js';
2
+ import { normalizeStructTag, SUI_TYPE_ARG } from '@scallop-io/sui-kit';
3
+ import { estimatedFactor, minBigNumber } from 'src/utils/query.js';
4
+ import type {
5
+ BorrowIncentivePool,
6
+ CoinAmounts,
7
+ CoinPrices,
8
+ Lending,
9
+ Lendings,
10
+ MarketCollateral,
11
+ MarketCollaterals,
12
+ MarketPool,
13
+ MarketPools,
14
+ ObligationAccount,
15
+ ObligationAccounts,
16
+ ObligationBorrowIncentiveReward,
17
+ ObligationCollateral,
18
+ ObligationDebt,
19
+ ParsedBorrowIncentiveAccountData,
20
+ Spool,
21
+ StakeAccount,
22
+ } from 'src/types/index.js';
23
+
24
+ /* ============================================================
25
+ * User-portfolio aggregation helpers
26
+ *
27
+ * Pure transforms over already-fetched data, lifted out of
28
+ * `getUserPortfolio` in `src/queries/portfolioQuery.ts` so the orchestrator
29
+ * shrinks to fetch + compose. Public return shape of `getUserPortfolio` is
30
+ * intentionally preserved.
31
+ * ============================================================ */
32
+
33
+ export type PortfolioLending = {
34
+ suppliedCoin: number;
35
+ suppliedValue: number;
36
+ stakedCoin: number;
37
+ coinName: string;
38
+ symbol: string;
39
+ coinType: string;
40
+ coinPrice: number;
41
+ coinDecimals: number;
42
+ supplyApr: number;
43
+ supplyApy: number;
44
+ incentiveApr: number;
45
+ };
46
+
47
+ export type PortfolioBorrowing = {
48
+ obligationId: string;
49
+ totalDebtsInUsd: number;
50
+ totalCollateralInUsd: number;
51
+ riskLevel: number;
52
+ availableCollateralInUsd: number;
53
+ totalUnhealthyCollateralInUsd: number;
54
+ collaterals: Array<{
55
+ coinName: string;
56
+ symbol: string;
57
+ coinDecimals: number;
58
+ coinType: string;
59
+ coinPrice: number;
60
+ depositedCoin: number;
61
+ depositedValueInUsd: number;
62
+ }>;
63
+ borrowedPools: Array<{
64
+ coinName: string;
65
+ symbol: string;
66
+ coinDecimals: number;
67
+ coinType: string;
68
+ coinPrice: number;
69
+ borrowedCoin: number;
70
+ borrowedValueInUsd: number;
71
+ borrowApr?: number;
72
+ borrowApy?: number;
73
+ incentiveInfos: Array<{
74
+ coinName: string;
75
+ symbol: string;
76
+ coinType: string;
77
+ boostValue: number;
78
+ maxBoost: number;
79
+ incentiveApr: number;
80
+ boostedIncentiveApr: number;
81
+ }>;
82
+ }>;
83
+ };
84
+
85
+ export type PendingReward = {
86
+ coinType: string;
87
+ symbol: string;
88
+ coinPrice: number;
89
+ pendingRewardInCoin: number;
90
+ };
91
+
92
+ export type PortfolioVeSca = {
93
+ veScaKey: string;
94
+ coinPrice: number;
95
+ lockedScaInCoin: number;
96
+ lockedScaInUsd: number;
97
+ currentVeScaBalance: number;
98
+ remainingLockPeriodInDays: number;
99
+ unlockAt: number;
100
+ };
101
+
102
+ export const parseLendingsForPortfolio = (
103
+ lendings: Lendings
104
+ ): PortfolioLending[] =>
105
+ Object.values(lendings)
106
+ .filter(
107
+ (t): t is NonNullable<typeof t> => !!t && t.availableWithdrawCoin > 0
108
+ )
109
+ .map((lending) => ({
110
+ suppliedCoin: lending.availableWithdrawCoin,
111
+ suppliedValue: lending.suppliedValue,
112
+ stakedCoin: lending.availableUnstakeCoin,
113
+ coinName: lending.coinName,
114
+ symbol: lending.symbol,
115
+ coinType: lending.coinType,
116
+ coinPrice: lending.coinPrice,
117
+ coinDecimals: lending.coinDecimal,
118
+ supplyApr: lending.supplyApr,
119
+ supplyApy: lending.supplyApy,
120
+ incentiveApr: isFinite(lending.rewardApr) ? lending.rewardApr : 0,
121
+ }));
122
+
123
+ export const parseObligationAccountsForPortfolio = (
124
+ obligationAccounts: ObligationAccounts,
125
+ marketPools: Record<string, MarketPool | undefined>
126
+ ): PortfolioBorrowing[] =>
127
+ Object.values(obligationAccounts)
128
+ .filter(
129
+ (t): t is NonNullable<typeof t> =>
130
+ !!t && (t.totalBorrowedValueWithWeight > 0 || t.totalDepositedValue > 0)
131
+ )
132
+ .map((obligationAccount) => ({
133
+ obligationId: obligationAccount.obligationId,
134
+ totalDebtsInUsd: obligationAccount.totalBorrowedValueWithWeight,
135
+ totalCollateralInUsd: obligationAccount.totalDepositedValue,
136
+ riskLevel: obligationAccount.totalRiskLevel,
137
+ availableCollateralInUsd: obligationAccount.totalAvailableCollateralValue,
138
+ totalUnhealthyCollateralInUsd:
139
+ obligationAccount.totalUnhealthyCollateralValue,
140
+ collaterals: Object.values(obligationAccount.collaterals)
141
+ .filter(
142
+ (collateral): collateral is NonNullable<typeof collateral> =>
143
+ !!collateral && collateral.depositedCoin > 0
144
+ )
145
+ .map((collateral) => ({
146
+ coinName: collateral.coinName,
147
+ symbol: collateral.symbol,
148
+ coinDecimals: collateral.coinDecimal,
149
+ coinType: collateral.coinType,
150
+ coinPrice: collateral.coinPrice,
151
+ depositedCoin: collateral.depositedCoin,
152
+ depositedValueInUsd: collateral.depositedValue,
153
+ })),
154
+ borrowedPools: Object.values(obligationAccount.debts)
155
+ .filter(
156
+ (debt): debt is NonNullable<typeof debt> =>
157
+ !!debt && debt.borrowedCoin > 0
158
+ )
159
+ .map((debt) => ({
160
+ coinName: debt.coinName,
161
+ symbol: debt.symbol,
162
+ coinDecimals: debt.coinDecimal,
163
+ coinType: debt.coinType,
164
+ coinPrice: debt.coinPrice,
165
+ borrowedCoin: debt.borrowedCoin,
166
+ borrowedValueInUsd: debt.borrowedValueWithWeight,
167
+ borrowApr: marketPools[debt.coinName]?.borrowApr,
168
+ borrowApy: marketPools[debt.coinName]?.borrowApy,
169
+ incentiveInfos: (
170
+ obligationAccount.borrowIncentives[debt.coinName]?.rewards ?? []
171
+ )
172
+ .filter(
173
+ (t): t is NonNullable<typeof t> =>
174
+ !!t && isFinite(t.baseRewardApr)
175
+ )
176
+ .map((t) => ({
177
+ coinName: t.coinName,
178
+ symbol: t.symbol,
179
+ coinType: t.coinType,
180
+ boostValue: t.boostValue,
181
+ maxBoost: t.maxBoost,
182
+ incentiveApr: t.baseRewardApr,
183
+ boostedIncentiveApr: t.boostedRewardApr,
184
+ })),
185
+ })),
186
+ }));
187
+
188
+ const LENDING_SPOOL_REWARD_COIN_NAME = 'sui' as const;
189
+ const LENDING_SPOOL_REWARD_COIN_SYMBOL = 'SUI' as const;
190
+
191
+ export const aggregatePendingLendingRewards = (
192
+ lendings: Lendings,
193
+ coinPrices: CoinPrices
194
+ ): Record<string, PendingReward> =>
195
+ Object.values(lendings).reduce(
196
+ (acc, reward) => {
197
+ if (reward) {
198
+ if (reward.availableClaimCoin === 0) return acc;
199
+ if (!acc[LENDING_SPOOL_REWARD_COIN_NAME]) {
200
+ acc[LENDING_SPOOL_REWARD_COIN_NAME] = {
201
+ symbol: LENDING_SPOOL_REWARD_COIN_SYMBOL,
202
+ // For now lending reward is all in SUI; mirrors the legacy
203
+ // `getUserPortfolio` behaviour.
204
+ coinType: normalizeStructTag(SUI_TYPE_ARG),
205
+ coinPrice: coinPrices[LENDING_SPOOL_REWARD_COIN_NAME] ?? 0,
206
+ pendingRewardInCoin: reward.availableClaimCoin,
207
+ };
208
+ } else {
209
+ acc[LENDING_SPOOL_REWARD_COIN_NAME].pendingRewardInCoin +=
210
+ reward.availableClaimCoin;
211
+ }
212
+ }
213
+ return acc;
214
+ },
215
+ {} as Record<string, PendingReward>
216
+ );
217
+
218
+ export const aggregatePendingBorrowIncentiveRewards = (
219
+ obligationAccounts: ObligationAccounts
220
+ ): Record<string, PendingReward> =>
221
+ Object.values(obligationAccounts)
222
+ .filter((t): t is NonNullable<typeof t> => !!t)
223
+ .reduce(
224
+ (acc, curr) => {
225
+ Object.values(curr.borrowIncentives).forEach((incentive) => {
226
+ incentive?.rewards.forEach((reward) => {
227
+ if (reward.availableClaimCoin === 0) return acc;
228
+ if (!acc[reward.coinName]) {
229
+ acc[reward.coinName] = {
230
+ symbol: reward.symbol,
231
+ coinType: reward.coinType,
232
+ coinPrice: reward.coinPrice,
233
+ pendingRewardInCoin: reward.availableClaimCoin,
234
+ };
235
+ } else {
236
+ acc[reward.coinName].pendingRewardInCoin +=
237
+ reward.availableClaimCoin;
238
+ }
239
+ });
240
+ });
241
+ return acc;
242
+ },
243
+ {} as Record<string, PendingReward>
244
+ );
245
+
246
+ export const parseVeScasForPortfolio = (
247
+ veScas: Array<{
248
+ keyId: string;
249
+ lockedScaCoin: number;
250
+ currentVeScaBalance: number;
251
+ unlockAt: number;
252
+ }>,
253
+ scaPrice: number,
254
+ nowMs: number = Date.now()
255
+ ): PortfolioVeSca[] =>
256
+ veScas.map(({ keyId, lockedScaCoin, currentVeScaBalance, unlockAt }) => ({
257
+ veScaKey: keyId,
258
+ coinPrice: scaPrice,
259
+ lockedScaInCoin: lockedScaCoin,
260
+ lockedScaInUsd: lockedScaCoin * scaPrice,
261
+ currentVeScaBalance,
262
+ remainingLockPeriodInDays:
263
+ unlockAt - nowMs > 0 ? (unlockAt - nowMs) / 86400000 : 0,
264
+ unlockAt,
265
+ }));
266
+
267
+ export const summarisePortfolioTotals = (input: {
268
+ parsedLendings: PortfolioLending[];
269
+ parsedObligationAccounts: PortfolioBorrowing[];
270
+ parsedVeScas: PortfolioVeSca[];
271
+ }) => {
272
+ const totalSupplyValue = input.parsedLendings.reduce(
273
+ (acc, curr) => acc + curr.suppliedValue,
274
+ 0
275
+ );
276
+ const obligationTotals = input.parsedObligationAccounts.reduce(
277
+ (acc, curr) => {
278
+ acc.totalDebtValue += curr.totalDebtsInUsd;
279
+ acc.totalCollateralValue += curr.totalCollateralInUsd;
280
+ return acc;
281
+ },
282
+ { totalDebtValue: 0, totalCollateralValue: 0 }
283
+ );
284
+ const totalLockedScaValue = input.parsedVeScas.reduce(
285
+ (acc, curr) => acc + curr.lockedScaInUsd,
286
+ 0
287
+ );
288
+ return { totalSupplyValue, ...obligationTotals, totalLockedScaValue };
289
+ };
290
+
291
+ /* ============================================================
292
+ * Obligation-account helpers
293
+ *
294
+ * Pure transforms lifted out of `getObligationAccount` in
295
+ * `src/queries/portfolioQuery.ts`. Each helper computes one
296
+ * responsibility — a single entry, the rewards list, or one of the
297
+ * estimated balances — and returns plain numbers (or an `ObligationXxx`
298
+ * entry) so the orchestrator can shrink to fetch + compose.
299
+ * ============================================================ */
300
+
301
+ export type BuildObligationCollateralInput = {
302
+ assetCoinName: string;
303
+ coinType: string;
304
+ symbol: string;
305
+ coinDecimal: number;
306
+ coinPrice: number;
307
+ /** Wallet balance (in the smallest denomination) available to deposit. */
308
+ coinAmount: number;
309
+ marketCollateral: MarketCollateral;
310
+ /** Raw on-chain `amount` field from the obligation collateral, if any. */
311
+ depositedRawAmount?: string | number;
312
+ };
313
+
314
+ export type BuildObligationCollateralResult = {
315
+ entry: ObligationCollateral;
316
+ depositedValue: BigNumber;
317
+ borrowCapacityValue: BigNumber;
318
+ requiredCollateralValue: BigNumber;
319
+ isDeposited: boolean;
320
+ };
321
+
322
+ export const buildObligationCollateralEntry = (
323
+ input: BuildObligationCollateralInput
324
+ ): BuildObligationCollateralResult => {
325
+ const depositedAmount = BigNumber(input.depositedRawAmount ?? 0);
326
+ const depositedCoin = depositedAmount.shiftedBy(-input.coinDecimal);
327
+ const depositedValue = depositedCoin.multipliedBy(input.coinPrice);
328
+ const borrowCapacityValue = depositedValue.multipliedBy(
329
+ input.marketCollateral.collateralFactor
330
+ );
331
+ const requiredCollateralValue = depositedValue.multipliedBy(
332
+ input.marketCollateral.liquidationFactor
333
+ );
334
+ const availableDepositAmount = BigNumber(input.coinAmount);
335
+ const availableDepositCoin = availableDepositAmount.shiftedBy(
336
+ -input.coinDecimal
337
+ );
338
+ const entry: ObligationCollateral = {
339
+ coinName: input.assetCoinName,
340
+ coinType: input.coinType,
341
+ symbol: input.symbol,
342
+ coinDecimal: input.coinDecimal,
343
+ coinPrice: input.coinPrice,
344
+ depositedAmount: depositedAmount.toNumber(),
345
+ depositedCoin: depositedCoin.toNumber(),
346
+ depositedValue: depositedValue.toNumber(),
347
+ borrowCapacityValue: borrowCapacityValue.toNumber(),
348
+ requiredCollateralValue: requiredCollateralValue.toNumber(),
349
+ availableDepositAmount: availableDepositAmount.toNumber(),
350
+ availableDepositCoin: availableDepositCoin.toNumber(),
351
+ availableWithdrawAmount: 0,
352
+ availableWithdrawCoin: 0,
353
+ };
354
+ return {
355
+ entry,
356
+ depositedValue,
357
+ borrowCapacityValue,
358
+ requiredCollateralValue,
359
+ isDeposited: depositedAmount.isGreaterThan(0),
360
+ };
361
+ };
362
+
363
+ export type BuildBorrowIncentiveRewardsInput = {
364
+ borrowIncentivePool: BorrowIncentivePool;
365
+ borrowIncentiveAccount: ParsedBorrowIncentiveAccountData;
366
+ /** Translate the on-chain account-point key to the market-coin name used
367
+ * by `borrowIncentivePool.points`. Injected so this helper stays pure. */
368
+ toMarketCoinName: (key: string) => string;
369
+ };
370
+
371
+ export type BuildBorrowIncentiveRewardsResult = {
372
+ rewards: ObligationBorrowIncentiveReward[];
373
+ /** Whether this pool contributes a rewarded-pool count toward
374
+ * `totalRewardedPools`. True only when at least one of the pool's points
375
+ * has a finite, positive `rewardApr` AND the account has debt. */
376
+ contributesRewardedPool: boolean;
377
+ };
378
+
379
+ const BASE_POINT_INDEX_RATE = 1_000_000_000;
380
+ const WEIGHT_SCALE = BigNumber(1_000_000_000_000);
381
+
382
+ export const buildBorrowIncentiveRewards = (
383
+ input: BuildBorrowIncentiveRewardsInput
384
+ ): BuildBorrowIncentiveRewardsResult => {
385
+ const rewards: ObligationBorrowIncentiveReward[] = [];
386
+ Object.entries(input.borrowIncentiveAccount.pointList).forEach(
387
+ ([key, accountPoint]) => {
388
+ const poolPoint =
389
+ input.borrowIncentivePool.points[input.toMarketCoinName(key)];
390
+ if (!accountPoint || !poolPoint) return;
391
+
392
+ const accountBorrowedAmount = BigNumber(accountPoint.weightedAmount);
393
+ const increasedPointRate = poolPoint.currentPointIndex
394
+ ? Math.max(
395
+ BigNumber(poolPoint.currentPointIndex - accountPoint.index)
396
+ .dividedBy(BASE_POINT_INDEX_RATE)
397
+ .toNumber(),
398
+ 0
399
+ )
400
+ : 1;
401
+ const availableClaimAmount = accountBorrowedAmount
402
+ .multipliedBy(increasedPointRate)
403
+ .plus(accountPoint.points);
404
+ const availableClaimCoin = availableClaimAmount.shiftedBy(
405
+ -poolPoint.coinDecimal
406
+ );
407
+
408
+ // veSCA boost
409
+ const boostScale = BigNumber(poolPoint.baseWeight).dividedBy(
410
+ WEIGHT_SCALE
411
+ );
412
+ const boostRatio = BigNumber(accountPoint.weightedAmount).div(
413
+ BigNumber(input.borrowIncentiveAccount.debtAmount).multipliedBy(
414
+ boostScale
415
+ )
416
+ );
417
+ const boostValue = boostRatio.isFinite() ? boostRatio.toNumber() : 1;
418
+ const rewardApr = isFinite(poolPoint.rewardApr) ? poolPoint.rewardApr : 0;
419
+
420
+ if (availableClaimAmount.isGreaterThanOrEqualTo(0)) {
421
+ rewards.push({
422
+ coinName: poolPoint.coinName,
423
+ coinType: poolPoint.coinType,
424
+ symbol: poolPoint.symbol,
425
+ coinDecimal: poolPoint.coinDecimal,
426
+ coinPrice: poolPoint.coinPrice,
427
+ weightedBorrowAmount: accountBorrowedAmount.toNumber(),
428
+ availableClaimAmount: availableClaimAmount.toNumber(),
429
+ availableClaimCoin: availableClaimCoin.toNumber(),
430
+ baseRewardApr: rewardApr,
431
+ boostedRewardApr: rewardApr * boostValue,
432
+ maxBoost: 1 / boostScale.toNumber(),
433
+ boostValue,
434
+ });
435
+ }
436
+ }
437
+ );
438
+
439
+ const anyPositiveRewardApr = Object.keys(
440
+ input.borrowIncentivePool.points
441
+ ).some((coinName) => {
442
+ const apr = input.borrowIncentivePool.points[coinName]?.rewardApr;
443
+ return apr !== Infinity && typeof apr === 'number' && apr > 0;
444
+ });
445
+ const contributesRewardedPool =
446
+ anyPositiveRewardApr && input.borrowIncentiveAccount.debtAmount > 0;
447
+
448
+ return { rewards, contributesRewardedPool };
449
+ };
450
+
451
+ export type BuildObligationDebtInput = {
452
+ assetCoinName: string;
453
+ coinType: string;
454
+ symbol: string;
455
+ coinDecimal: number;
456
+ coinPrice: number;
457
+ /** Wallet balance (in the smallest denomination) available to repay. */
458
+ coinAmount: number;
459
+ marketPool: MarketPool;
460
+ /** Raw on-chain debt fields from the obligation, if any. */
461
+ debt?: { amount: string | number; borrowIndex?: string | number };
462
+ /** Rewards from the matching borrow-incentive pool (already built). */
463
+ rewards?: ObligationBorrowIncentiveReward[];
464
+ };
465
+
466
+ export type BuildObligationDebtResult = {
467
+ entry: ObligationDebt;
468
+ borrowedValue: BigNumber;
469
+ borrowedValueWithWeight: BigNumber;
470
+ isBorrowed: boolean;
471
+ };
472
+
473
+ export const buildObligationDebtEntry = (
474
+ input: BuildObligationDebtInput
475
+ ): BuildObligationDebtResult => {
476
+ const increasedRate = input.debt?.borrowIndex
477
+ ? input.marketPool.borrowIndex / Number(input.debt.borrowIndex) - 1
478
+ : 0;
479
+ const borrowedAmount = BigNumber(input.debt?.amount ?? 0).multipliedBy(
480
+ increasedRate + 1
481
+ );
482
+ const borrowedCoin = borrowedAmount.shiftedBy(-input.coinDecimal);
483
+
484
+ const requiredRepayAmount = borrowedAmount;
485
+ const requiredRepayCoin = requiredRepayAmount.shiftedBy(-input.coinDecimal);
486
+
487
+ const availableRepayAmount = BigNumber(input.coinAmount);
488
+ const availableRepayCoin = availableRepayAmount.shiftedBy(-input.coinDecimal);
489
+
490
+ const borrowedValue = requiredRepayCoin.multipliedBy(input.coinPrice);
491
+ const borrowedValueWithWeight = borrowedValue.multipliedBy(
492
+ input.marketPool.borrowWeight
493
+ );
494
+
495
+ const entry: ObligationDebt = {
496
+ coinName: input.assetCoinName,
497
+ coinType: input.coinType,
498
+ symbol: input.symbol,
499
+ coinDecimal: input.coinDecimal,
500
+ coinPrice: input.coinPrice,
501
+ borrowedAmount: borrowedAmount.toNumber(),
502
+ borrowedCoin: borrowedCoin.toNumber(),
503
+ borrowedValue: borrowedValue.toNumber(),
504
+ borrowedValueWithWeight: borrowedValueWithWeight.toNumber(),
505
+ borrowIndex: Number(input.debt?.borrowIndex ?? 0),
506
+ requiredRepayAmount: requiredRepayAmount.toNumber(),
507
+ requiredRepayCoin: requiredRepayCoin.toNumber(),
508
+ availableBorrowAmount: 0,
509
+ availableBorrowCoin: 0,
510
+ availableRepayAmount: availableRepayAmount.toNumber(),
511
+ availableRepayCoin: availableRepayCoin.toNumber(),
512
+ rewards: (input.rewards ?? []).filter(
513
+ ({ weightedBorrowAmount }) => weightedBorrowAmount > 0
514
+ ),
515
+ };
516
+
517
+ return {
518
+ entry,
519
+ borrowedValue,
520
+ borrowedValueWithWeight,
521
+ isBorrowed: borrowedAmount.isGreaterThan(0),
522
+ };
523
+ };
524
+
525
+ export type ObligationSummaryInput = {
526
+ totalDepositedValue: BigNumber;
527
+ totalBorrowedValue: BigNumber;
528
+ totalBorrowCapacityValue: BigNumber;
529
+ totalBorrowedValueWithWeight: BigNumber;
530
+ totalRequiredCollateralValue: BigNumber;
531
+ };
532
+
533
+ export type ObligationSummary = {
534
+ riskLevel: number;
535
+ accountBalanceValue: number;
536
+ availableCollateralValue: number;
537
+ requiredCollateralValue: number;
538
+ unhealthyCollateralValue: number;
539
+ };
540
+
541
+ export const calculateObligationSummary = (
542
+ input: ObligationSummaryInput
543
+ ): ObligationSummary => {
544
+ let risk = input.totalRequiredCollateralValue.isZero()
545
+ ? // No collateral + non-zero debt => bad-debt situation, capped below.
546
+ input.totalBorrowedValueWithWeight.isGreaterThan(0)
547
+ ? BigNumber(100)
548
+ : BigNumber(0)
549
+ : input.totalBorrowedValueWithWeight.dividedBy(
550
+ input.totalRequiredCollateralValue
551
+ );
552
+ // 100% is the safety upper bound; cap to avoid >1 risk levels showing pre-liquidation.
553
+ risk = risk.isLessThan(1) ? risk : BigNumber(1);
554
+
555
+ const accountBalanceValue = input.totalDepositedValue
556
+ .minus(input.totalBorrowedValue)
557
+ .isGreaterThan(0)
558
+ ? input.totalDepositedValue.minus(input.totalBorrowedValue)
559
+ : BigNumber(0);
560
+ const availableCollateralValue = input.totalBorrowCapacityValue
561
+ .minus(input.totalBorrowedValueWithWeight)
562
+ .isGreaterThan(0)
563
+ ? input.totalBorrowCapacityValue.minus(input.totalBorrowedValueWithWeight)
564
+ : BigNumber(0);
565
+ const requiredCollateralValue =
566
+ input.totalBorrowedValueWithWeight.isGreaterThan(0)
567
+ ? input.totalRequiredCollateralValue
568
+ : BigNumber(0);
569
+ const unhealthyCollateralValue = input.totalBorrowedValueWithWeight
570
+ .minus(requiredCollateralValue)
571
+ .isGreaterThan(0)
572
+ ? input.totalBorrowedValueWithWeight.minus(requiredCollateralValue)
573
+ : BigNumber(0);
574
+
575
+ return {
576
+ riskLevel: risk.toNumber(),
577
+ accountBalanceValue: accountBalanceValue.toNumber(),
578
+ availableCollateralValue: availableCollateralValue.toNumber(),
579
+ requiredCollateralValue: requiredCollateralValue.toNumber(),
580
+ unhealthyCollateralValue: unhealthyCollateralValue.toNumber(),
581
+ };
582
+ };
583
+
584
+ export type EstimateAvailableWithdrawInput = {
585
+ obligationCollateral: Pick<
586
+ ObligationCollateral,
587
+ 'depositedAmount' | 'coinDecimal'
588
+ >;
589
+ marketCollateral: Pick<
590
+ MarketCollateral,
591
+ 'collateralFactor' | 'coinPrice' | 'coinDecimal' | 'depositAmount'
592
+ >;
593
+ totalAvailableCollateralValue: number;
594
+ totalBorrowedValueWithWeight: number;
595
+ };
596
+
597
+ export const estimateAvailableWithdrawAmount = (
598
+ input: EstimateAvailableWithdrawInput
599
+ ): { availableWithdrawAmount: number; availableWithdrawCoin: number } => {
600
+ const baseEstimate = BigNumber(input.totalAvailableCollateralValue)
601
+ .dividedBy(input.marketCollateral.collateralFactor)
602
+ .dividedBy(input.marketCollateral.coinPrice)
603
+ .shiftedBy(input.marketCollateral.coinDecimal);
604
+
605
+ const estimated =
606
+ input.totalBorrowedValueWithWeight === 0
607
+ ? // No debt => no need to estimate; deposited amount is fully withdrawable.
608
+ BigNumber(input.obligationCollateral.depositedAmount)
609
+ : minBigNumber(
610
+ baseEstimate
611
+ // Cushion factor to reduce inaccurate-math failures.
612
+ .multipliedBy(
613
+ estimatedFactor(
614
+ BigNumber(input.totalAvailableCollateralValue)
615
+ .dividedBy(input.marketCollateral.collateralFactor)
616
+ .toNumber(),
617
+ 3,
618
+ 'increase'
619
+ )
620
+ )
621
+ .toNumber(),
622
+ input.obligationCollateral.depositedAmount,
623
+ input.marketCollateral.depositAmount
624
+ );
625
+
626
+ return {
627
+ availableWithdrawAmount: estimated.toNumber(),
628
+ availableWithdrawCoin: estimated
629
+ .shiftedBy(-input.obligationCollateral.coinDecimal)
630
+ .toNumber(),
631
+ };
632
+ };
633
+
634
+ export type EstimateAvailableBorrowInput = {
635
+ obligationDebt: Pick<
636
+ ObligationDebt,
637
+ 'requiredRepayAmount' | 'borrowedValue' | 'coinDecimal'
638
+ >;
639
+ marketPool: Pick<
640
+ MarketPool,
641
+ | 'borrowWeight'
642
+ | 'coinDecimal'
643
+ | 'coinPrice'
644
+ | 'maxBorrowCoin'
645
+ | 'borrowCoin'
646
+ | 'supplyAmount'
647
+ >;
648
+ totalAvailableCollateralValue: number;
649
+ };
650
+
651
+ export const estimateAvailableBorrowAmount = (
652
+ input: EstimateAvailableBorrowInput
653
+ ): {
654
+ availableBorrowAmount: number;
655
+ availableBorrowCoin: number;
656
+ requiredRepayAmount: number;
657
+ requiredRepayCoin: number;
658
+ } => {
659
+ const estimatedRequiredRepayAmount = BigNumber(
660
+ input.obligationDebt.requiredRepayAmount
661
+ )
662
+ // Cushion: reduces under-repay failures from inaccurate math; the
663
+ // contract refuses excess amounts so over-repay is safe.
664
+ .multipliedBy(
665
+ estimatedFactor(input.obligationDebt.borrowedValue, 3, 'decrease')
666
+ );
667
+
668
+ const baseEstimate = BigNumber(input.totalAvailableCollateralValue)
669
+ .dividedBy(input.marketPool.borrowWeight)
670
+ .shiftedBy(input.marketPool.coinDecimal)
671
+ .dividedBy(input.marketPool.coinPrice);
672
+
673
+ const estimated =
674
+ input.totalAvailableCollateralValue !== 0 &&
675
+ BigNumber(input.marketPool.maxBorrowCoin).isGreaterThan(
676
+ input.marketPool.borrowCoin
677
+ )
678
+ ? minBigNumber(
679
+ baseEstimate
680
+ // Cushion factor to reduce inaccurate-math failures.
681
+ .multipliedBy(
682
+ estimatedFactor(
683
+ baseEstimate
684
+ .shiftedBy(-input.marketPool.coinDecimal)
685
+ .multipliedBy(input.marketPool.coinPrice)
686
+ .toNumber(),
687
+ 3,
688
+ 'increase'
689
+ )
690
+ )
691
+ .toNumber(),
692
+ input.marketPool.supplyAmount
693
+ )
694
+ : BigNumber(0);
695
+
696
+ return {
697
+ availableBorrowAmount: estimated.toNumber(),
698
+ availableBorrowCoin: estimated
699
+ .shiftedBy(-input.obligationDebt.coinDecimal)
700
+ .toNumber(),
701
+ requiredRepayAmount: estimatedRequiredRepayAmount.toNumber(),
702
+ requiredRepayCoin: estimatedRequiredRepayAmount
703
+ .shiftedBy(-input.obligationDebt.coinDecimal)
704
+ .toNumber(),
705
+ };
706
+ };
707
+
708
+ /**
709
+ * Pure assembly of a single `Lending` from pre-fetched inputs. All I/O (market
710
+ * pool, spool, stake accounts, coin/sCoin amounts, price) and name parsing are
711
+ * resolved by the caller (`ScallopQuery.getLendings`) — this only does the math.
712
+ */
713
+ export const buildLending = (input: {
714
+ coinName: string;
715
+ symbol: string;
716
+ coinType: string;
717
+ marketCoinType: string;
718
+ coinDecimal: number;
719
+ coinPrice: number;
720
+ marketPool?: MarketPool;
721
+ spool?: Spool;
722
+ stakeAccounts: StakeAccount[];
723
+ coinAmount: number;
724
+ marketCoinAmount: number;
725
+ sCoinAmount: number;
726
+ }): Lending => {
727
+ const {
728
+ coinName,
729
+ symbol,
730
+ coinType,
731
+ marketCoinType,
732
+ coinDecimal,
733
+ coinPrice,
734
+ marketPool,
735
+ spool,
736
+ stakeAccounts,
737
+ coinAmount,
738
+ marketCoinAmount,
739
+ sCoinAmount,
740
+ } = input;
741
+
742
+ let stakedMarketAmount = BigNumber(0);
743
+ let stakedMarketCoin = BigNumber(0);
744
+ let stakedAmount = BigNumber(0);
745
+ let stakedCoin = BigNumber(0);
746
+ let stakedValue = BigNumber(0);
747
+ let availableUnstakeAmount = BigNumber(0);
748
+ let availableUnstakeCoin = BigNumber(0);
749
+ let availableClaimAmount = BigNumber(0);
750
+ let availableClaimCoin = BigNumber(0);
751
+
752
+ if (spool) {
753
+ for (const stakeAccount of stakeAccounts) {
754
+ const accountStakedMarketCoinAmount = BigNumber(stakeAccount.staked);
755
+ const accountStakedMarketCoin = accountStakedMarketCoinAmount.shiftedBy(
756
+ -spool.coinDecimal
757
+ );
758
+ const accountStakedAmount = accountStakedMarketCoinAmount.multipliedBy(
759
+ marketPool?.conversionRate ?? 1
760
+ );
761
+ const accountStakedCoin = accountStakedAmount.shiftedBy(
762
+ -spool.coinDecimal
763
+ );
764
+ const accountStakedValue = accountStakedCoin.multipliedBy(
765
+ spool.coinPrice
766
+ );
767
+
768
+ stakedMarketAmount = stakedMarketAmount.plus(
769
+ accountStakedMarketCoinAmount
770
+ );
771
+ stakedMarketCoin = stakedMarketCoin.plus(accountStakedMarketCoin);
772
+ stakedAmount = stakedAmount.plus(accountStakedAmount);
773
+ stakedCoin = stakedCoin.plus(accountStakedCoin);
774
+ stakedValue = stakedValue.plus(accountStakedValue);
775
+ availableUnstakeAmount = availableUnstakeAmount.plus(
776
+ accountStakedMarketCoinAmount
777
+ );
778
+ availableUnstakeCoin = availableUnstakeAmount.shiftedBy(
779
+ -spool.coinDecimal
780
+ );
781
+
782
+ const baseIndexRate = 1_000_000_000;
783
+ const increasedPointRate = spool.currentPointIndex
784
+ ? BigNumber(spool.currentPointIndex - stakeAccount.index).dividedBy(
785
+ baseIndexRate
786
+ )
787
+ : 1;
788
+ availableClaimAmount = availableClaimAmount.plus(
789
+ accountStakedMarketCoinAmount
790
+ .multipliedBy(increasedPointRate)
791
+ .plus(stakeAccount.points)
792
+ .multipliedBy(spool.exchangeRateNumerator)
793
+ .dividedBy(spool.exchangeRateDenominator)
794
+ );
795
+ availableClaimCoin = availableClaimAmount.shiftedBy(
796
+ -spool.rewardCoinDecimal
797
+ );
798
+ }
799
+ }
800
+
801
+ // Handle supplied coin
802
+ const suppliedAmount = BigNumber(marketCoinAmount)
803
+ .plus(BigNumber(sCoinAmount))
804
+ .multipliedBy(marketPool?.conversionRate ?? 1);
805
+ const suppliedCoin = suppliedAmount.shiftedBy(-coinDecimal);
806
+ const suppliedValue = suppliedCoin.multipliedBy(coinPrice ?? 0);
807
+
808
+ const marketCoinPrice = BigNumber(coinPrice ?? 0).multipliedBy(
809
+ marketPool?.conversionRate ?? 1
810
+ );
811
+ const unstakedMarketAmount = BigNumber(marketCoinAmount).plus(
812
+ BigNumber(sCoinAmount)
813
+ );
814
+ const unstakedMarketCoin = unstakedMarketAmount.shiftedBy(-coinDecimal);
815
+
816
+ const availableSupplyAmount = BigNumber(coinAmount);
817
+ const availableSupplyCoin = availableSupplyAmount.shiftedBy(-coinDecimal);
818
+ const availableWithdrawAmount = minBigNumber(
819
+ suppliedAmount,
820
+ marketPool?.supplyAmount ?? Infinity
821
+ ).plus(stakedAmount);
822
+ const availableWithdrawCoin = minBigNumber(
823
+ suppliedCoin,
824
+ marketPool?.supplyCoin ?? Infinity
825
+ ).plus(stakedCoin);
826
+
827
+ return {
828
+ coinName,
829
+ symbol,
830
+ coinType,
831
+ marketCoinType,
832
+ sCoinType: marketPool?.sCoinType ?? '',
833
+ coinDecimal,
834
+ coinPrice: coinPrice ?? 0,
835
+ conversionRate: marketPool?.conversionRate ?? 1,
836
+ marketCoinPrice: marketCoinPrice.toNumber(),
837
+ supplyApr: marketPool?.supplyApr ?? 0,
838
+ supplyApy: marketPool?.supplyApy ?? 0,
839
+ rewardApr: spool?.rewardApr ?? 0,
840
+ suppliedAmount: suppliedAmount.plus(stakedAmount).toNumber(),
841
+ suppliedCoin: suppliedCoin.plus(stakedCoin).toNumber(),
842
+ suppliedValue: suppliedValue.plus(stakedValue).toNumber(),
843
+ stakedMarketAmount: stakedMarketAmount.toNumber(),
844
+ stakedMarketCoin: stakedMarketCoin.toNumber(),
845
+ stakedAmount: stakedAmount.toNumber(),
846
+ stakedCoin: stakedCoin.toNumber(),
847
+ stakedValue: stakedValue.toNumber(),
848
+ unstakedMarketAmount: unstakedMarketAmount.toNumber(),
849
+ unstakedMarketCoin: unstakedMarketCoin.toNumber(),
850
+ unstakedAmount: suppliedAmount.toNumber(),
851
+ unstakedCoin: suppliedCoin.toNumber(),
852
+ unstakedValue: suppliedValue.toNumber(),
853
+ availableSupplyAmount: availableSupplyAmount.toNumber(),
854
+ availableSupplyCoin: availableSupplyCoin.toNumber(),
855
+ availableWithdrawAmount: availableWithdrawAmount.toNumber(),
856
+ availableWithdrawCoin: availableWithdrawCoin.toNumber(),
857
+ availableStakeAmount: unstakedMarketAmount.toNumber(),
858
+ availableStakeCoin: unstakedMarketCoin.toNumber(),
859
+ availableUnstakeAmount: availableUnstakeAmount.toNumber(),
860
+ availableUnstakeCoin: availableUnstakeCoin.toNumber(),
861
+ availableClaimAmount: availableClaimAmount.toNumber(),
862
+ availableClaimCoin: availableClaimCoin.toNumber(),
863
+ isIsolated: marketPool ? marketPool.isIsolated : false,
864
+ };
865
+ };
866
+
867
+ /**
868
+ * Pure assembly of a single `ObligationAccount` from pre-fetched inputs. All I/O
869
+ * (market, prices, coin amounts, the obligation's on-chain collaterals/debts,
870
+ * borrow-incentive pools + accounts) and name parsing are resolved by the caller
871
+ * (`ScallopQuery.getObligationAccount*`) — this only does the math/assembly.
872
+ */
873
+ export const buildObligationAccount = (input: {
874
+ obligationId: string;
875
+ collateralCoinNames: string[];
876
+ market: { pools: MarketPools; collaterals: MarketCollaterals };
877
+ coinPrices: CoinPrices;
878
+ coinAmounts: CoinAmounts;
879
+ obligationQuery:
880
+ | {
881
+ collaterals: Array<{ type: string; amount: string | number }>;
882
+ debts: Array<{
883
+ type: string;
884
+ amount: string | number;
885
+ borrowIndex: string | number;
886
+ }>;
887
+ }
888
+ | null
889
+ | undefined;
890
+ borrowIncentivePools: Record<string, BorrowIncentivePool | undefined>;
891
+ borrowIncentiveAccounts: Record<
892
+ string,
893
+ ParsedBorrowIncentiveAccountData | undefined
894
+ >;
895
+ utils: {
896
+ parseCoinNameFromType: (type: string) => string;
897
+ parseCoinType: (coinName: string) => string;
898
+ parseSymbol: (coinName: string) => string;
899
+ getCoinDecimal: (coinName: string) => number;
900
+ parseSCoinTypeNameToMarketCoinName: (key: string) => string;
901
+ };
902
+ }): ObligationAccount => {
903
+ const {
904
+ obligationId,
905
+ collateralCoinNames,
906
+ market,
907
+ coinPrices,
908
+ coinAmounts,
909
+ obligationQuery,
910
+ borrowIncentivePools,
911
+ borrowIncentiveAccounts,
912
+ utils,
913
+ } = input;
914
+
915
+ const collaterals: ObligationAccount['collaterals'] = {};
916
+ const debts: ObligationAccount['debts'] = {};
917
+ const borrowIncentives: ObligationAccount['borrowIncentives'] = {};
918
+ let totalDepositedPools = 0;
919
+ let totalDepositedValue = BigNumber(0);
920
+ let totalBorrowCapacityValue = BigNumber(0);
921
+ let totalRequiredCollateralValue = BigNumber(0);
922
+ let totalBorrowedPools = 0;
923
+ let totalRewardedPools = 0;
924
+ let totalBorrowedValue = BigNumber(0);
925
+ let totalBorrowedValueWithWeight = BigNumber(0);
926
+
927
+ // -------- per-coin collateral entries --------
928
+ for (const assetCoinName of collateralCoinNames) {
929
+ const onchainCollateral = obligationQuery?.collaterals.find(
930
+ (collateral) =>
931
+ utils.parseCoinNameFromType(collateral.type) === assetCoinName
932
+ );
933
+
934
+ const marketCollateral = market.collaterals[assetCoinName];
935
+ if (!marketCollateral) continue;
936
+
937
+ const built = buildObligationCollateralEntry({
938
+ assetCoinName,
939
+ coinType: utils.parseCoinType(assetCoinName),
940
+ symbol: utils.parseSymbol(assetCoinName),
941
+ coinDecimal: utils.getCoinDecimal(assetCoinName),
942
+ coinPrice: coinPrices?.[assetCoinName] ?? 0,
943
+ coinAmount: coinAmounts?.[assetCoinName] ?? 0,
944
+ marketCollateral,
945
+ depositedRawAmount: onchainCollateral?.amount,
946
+ });
947
+ totalDepositedValue = totalDepositedValue.plus(built.depositedValue);
948
+ totalBorrowCapacityValue = totalBorrowCapacityValue.plus(
949
+ built.borrowCapacityValue
950
+ );
951
+ totalRequiredCollateralValue = totalRequiredCollateralValue.plus(
952
+ built.requiredCollateralValue
953
+ );
954
+ if (built.isDeposited) totalDepositedPools++;
955
+ collaterals[assetCoinName] = built.entry;
956
+ }
957
+
958
+ // -------- borrow-incentive rewards per pool --------
959
+ for (const [poolCoinName, borrowIncentiveAccount] of Object.entries(
960
+ borrowIncentiveAccounts
961
+ )) {
962
+ if (!borrowIncentiveAccount) continue;
963
+ const borrowIncentivePool = borrowIncentivePools[poolCoinName];
964
+ if (!borrowIncentivePool) continue;
965
+
966
+ const built = buildBorrowIncentiveRewards({
967
+ borrowIncentivePool,
968
+ borrowIncentiveAccount,
969
+ toMarketCoinName: (key) => utils.parseSCoinTypeNameToMarketCoinName(key),
970
+ });
971
+ if (built.contributesRewardedPool) totalRewardedPools++;
972
+ borrowIncentives[poolCoinName] = {
973
+ coinName: borrowIncentivePool.coinName,
974
+ coinType: borrowIncentivePool.coinType,
975
+ symbol: borrowIncentivePool.symbol,
976
+ coinDecimal: borrowIncentivePool.coinDecimal,
977
+ coinPrice: borrowIncentivePool.coinPrice,
978
+ rewards: built.rewards,
979
+ };
980
+ }
981
+
982
+ // -------- per-coin debt entries --------
983
+ const borrowAssetCoinNames: string[] = [
984
+ ...new Set(
985
+ Object.values(market.pools)
986
+ .filter((t): t is NonNullable<typeof t> => !!t)
987
+ .map((pool) => pool.coinName)
988
+ ),
989
+ ];
990
+ for (const assetCoinName of borrowAssetCoinNames) {
991
+ const onchainDebt = obligationQuery?.debts.find(
992
+ (debt) => utils.parseCoinNameFromType(debt.type) === assetCoinName
993
+ );
994
+ const marketPool = market.pools[assetCoinName];
995
+ if (!marketPool) continue;
996
+
997
+ const built = buildObligationDebtEntry({
998
+ assetCoinName,
999
+ coinType: utils.parseCoinType(assetCoinName),
1000
+ symbol: utils.parseSymbol(assetCoinName),
1001
+ coinDecimal: utils.getCoinDecimal(assetCoinName),
1002
+ coinPrice: coinPrices?.[assetCoinName] ?? 0,
1003
+ coinAmount: coinAmounts?.[assetCoinName] ?? 0,
1004
+ marketPool,
1005
+ debt: onchainDebt
1006
+ ? { amount: onchainDebt.amount, borrowIndex: onchainDebt.borrowIndex }
1007
+ : undefined,
1008
+ rewards: borrowIncentives[assetCoinName]?.rewards,
1009
+ });
1010
+ totalBorrowedValue = totalBorrowedValue.plus(built.borrowedValue);
1011
+ totalBorrowedValueWithWeight = totalBorrowedValueWithWeight.plus(
1012
+ built.borrowedValueWithWeight
1013
+ );
1014
+ if (built.isBorrowed) totalBorrowedPools++;
1015
+ debts[assetCoinName] = built.entry;
1016
+ }
1017
+
1018
+ // -------- aggregate risk / balances --------
1019
+ const summary = calculateObligationSummary({
1020
+ totalDepositedValue,
1021
+ totalBorrowedValue,
1022
+ totalBorrowCapacityValue,
1023
+ totalBorrowedValueWithWeight,
1024
+ totalRequiredCollateralValue,
1025
+ });
1026
+
1027
+ const obligationAccount: ObligationAccount = {
1028
+ obligationId,
1029
+ totalDepositedValue: totalDepositedValue.toNumber(),
1030
+ totalBorrowedValue: totalBorrowedValue.toNumber(),
1031
+ totalBalanceValue: summary.accountBalanceValue,
1032
+ totalBorrowCapacityValue: totalBorrowCapacityValue.toNumber(),
1033
+ totalAvailableCollateralValue: summary.availableCollateralValue,
1034
+ totalBorrowedValueWithWeight: totalBorrowedValueWithWeight.toNumber(),
1035
+ totalRequiredCollateralValue: summary.requiredCollateralValue,
1036
+ totalUnhealthyCollateralValue: summary.unhealthyCollateralValue,
1037
+ totalRiskLevel: summary.riskLevel,
1038
+ totalDepositedPools,
1039
+ totalBorrowedPools,
1040
+ totalRewardedPools,
1041
+ collaterals,
1042
+ debts,
1043
+ borrowIncentives,
1044
+ };
1045
+
1046
+ // -------- second pass: estimate available withdraw/borrow --------
1047
+ for (const [collateralCoinName, obligationCollateral] of Object.entries(
1048
+ obligationAccount.collaterals
1049
+ )) {
1050
+ if (!obligationCollateral) continue;
1051
+ const marketCollateral = market.collaterals[collateralCoinName];
1052
+ if (!marketCollateral) continue;
1053
+ const { availableWithdrawAmount, availableWithdrawCoin } =
1054
+ estimateAvailableWithdrawAmount({
1055
+ obligationCollateral,
1056
+ marketCollateral,
1057
+ totalAvailableCollateralValue:
1058
+ obligationAccount.totalAvailableCollateralValue,
1059
+ totalBorrowedValueWithWeight:
1060
+ obligationAccount.totalBorrowedValueWithWeight,
1061
+ });
1062
+ obligationCollateral.availableWithdrawAmount = availableWithdrawAmount;
1063
+ obligationCollateral.availableWithdrawCoin = availableWithdrawCoin;
1064
+ }
1065
+ for (const [poolCoinName, obligationDebt] of Object.entries(
1066
+ obligationAccount.debts
1067
+ )) {
1068
+ if (!obligationDebt) continue;
1069
+ const marketPool = market.pools[poolCoinName];
1070
+ if (!marketPool) continue;
1071
+ const {
1072
+ availableBorrowAmount,
1073
+ availableBorrowCoin,
1074
+ requiredRepayAmount,
1075
+ requiredRepayCoin,
1076
+ } = estimateAvailableBorrowAmount({
1077
+ obligationDebt,
1078
+ marketPool,
1079
+ totalAvailableCollateralValue:
1080
+ obligationAccount.totalAvailableCollateralValue,
1081
+ });
1082
+ obligationDebt.availableBorrowAmount = availableBorrowAmount;
1083
+ obligationDebt.availableBorrowCoin = availableBorrowCoin;
1084
+ obligationDebt.requiredRepayAmount = requiredRepayAmount;
1085
+ obligationDebt.requiredRepayCoin = requiredRepayCoin;
1086
+ }
1087
+
1088
+ return obligationAccount;
1089
+ };
1090
+
1091
+ /**
1092
+ * Pure assembly of the full user portfolio from already-fetched lendings,
1093
+ * obligation accounts, veScas, and prices. Composes the parse/aggregate/
1094
+ * summarise helpers above; `ScallopQuery.getUserPortfolio` only does the I/O.
1095
+ */
1096
+ export const buildUserPortfolio = (input: {
1097
+ lendings: Lendings;
1098
+ obligationAccounts: ObligationAccounts;
1099
+ veScas: Array<{
1100
+ keyId: string;
1101
+ lockedScaCoin: number;
1102
+ currentVeScaBalance: number;
1103
+ unlockAt: number;
1104
+ }>;
1105
+ coinPrices: CoinPrices;
1106
+ marketPools: MarketPools;
1107
+ }) => {
1108
+ const { lendings, obligationAccounts, veScas, coinPrices, marketPools } =
1109
+ input;
1110
+
1111
+ const parsedLendings = parseLendingsForPortfolio(lendings);
1112
+ const parsedObligationAccounts = parseObligationAccountsForPortfolio(
1113
+ obligationAccounts,
1114
+ marketPools
1115
+ );
1116
+ const pendingLendingRewards = aggregatePendingLendingRewards(
1117
+ lendings,
1118
+ coinPrices
1119
+ );
1120
+ const pendingBorrowIncentiveRewards =
1121
+ aggregatePendingBorrowIncentiveRewards(obligationAccounts);
1122
+ const parsedVeScas = parseVeScasForPortfolio(veScas, coinPrices.sca ?? 0);
1123
+ const totals = summarisePortfolioTotals({
1124
+ parsedLendings,
1125
+ parsedObligationAccounts,
1126
+ parsedVeScas,
1127
+ });
1128
+
1129
+ return {
1130
+ ...totals,
1131
+ lendings: parsedLendings,
1132
+ borrowings: parsedObligationAccounts,
1133
+ pendingRewards: {
1134
+ lendings: Object.values(pendingLendingRewards).map((value) => ({
1135
+ ...value,
1136
+ coinName: LENDING_SPOOL_REWARD_COIN_NAME,
1137
+ pendingRewardInUsd: value.coinPrice * value.pendingRewardInCoin,
1138
+ })),
1139
+ borrowIncentives: Object.entries(pendingBorrowIncentiveRewards).map(
1140
+ ([coinName, value]) => ({
1141
+ coinName,
1142
+ ...value,
1143
+ pendingRewardInUsd: value.coinPrice * value.pendingRewardInCoin,
1144
+ })
1145
+ ),
1146
+ },
1147
+ veScas: parsedVeScas,
1148
+ };
1149
+ };
1150
+
1151
+ /**
1152
+ * Pure: extend a coin-price map with sCoin prices derived from each pool's
1153
+ * conversion rate. The caller fetches `coinPrices` + `marketPools`;
1154
+ * `ScallopQuery.getAllCoinPrices` is the thin orchestrator.
1155
+ */
1156
+ export const buildAllCoinPrices = (input: {
1157
+ coinPrices: CoinPrices;
1158
+ marketPools: MarketPools;
1159
+ sCoinNames: string[];
1160
+ parseCoinName: (sCoinName: string) => string;
1161
+ }): CoinPrices => {
1162
+ const { coinPrices, marketPools, sCoinNames, parseCoinName } = input;
1163
+ const sCoinPrices: Record<string, number> = {};
1164
+ for (const sCoinName of sCoinNames) {
1165
+ const coinName = parseCoinName(sCoinName);
1166
+ sCoinPrices[sCoinName] = BigNumber(coinPrices[coinName] ?? 0)
1167
+ .multipliedBy(marketPools[coinName]?.conversionRate ?? 1)
1168
+ .toNumber();
1169
+ }
1170
+ return { ...coinPrices, ...sCoinPrices };
1171
+ };