@upshiftfinance/sdk 0.0.0-stage → 0.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 (346) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +408 -2
  3. package/lib/abis/AddressResolver.d.ts +28 -0
  4. package/lib/abis/AddressResolver.js +23 -0
  5. package/lib/abis/ChainlinkV3.d.ts +87 -0
  6. package/lib/abis/ChainlinkV3.js +117 -0
  7. package/lib/abis/ERC20.d.ts +168 -0
  8. package/lib/abis/ERC20.js +226 -0
  9. package/lib/abis/ERC20_Bytes32.d.ts +139 -0
  10. package/lib/abis/ERC20_Bytes32.js +196 -0
  11. package/lib/abis/ERC4626.d.ts +364 -0
  12. package/lib/abis/ERC4626.js +507 -0
  13. package/lib/abis/ERC721.d.ts +231 -0
  14. package/lib/abis/ERC721.js +321 -0
  15. package/lib/abis/FeeOracle.d.ts +120 -0
  16. package/lib/abis/FeeOracle.js +162 -0
  17. package/lib/abis/LendingPool.d.ts +1393 -0
  18. package/lib/abis/LendingPool.js +1807 -0
  19. package/lib/abis/LendingPoolV2.d.ts +1413 -0
  20. package/lib/abis/LendingPoolV2.js +1833 -0
  21. package/lib/abis/LendingPoolV3.d.ts +1677 -0
  22. package/lib/abis/LendingPoolV3.js +1160 -0
  23. package/lib/abis/Loan.d.ts +837 -0
  24. package/lib/abis/Loan.js +1080 -0
  25. package/lib/abis/MultiAssetNativeDepositWrapper.d.ts +137 -0
  26. package/lib/abis/MultiAssetNativeDepositWrapper.js +125 -0
  27. package/lib/abis/Multicall3.d.ts +30 -0
  28. package/lib/abis/Multicall3.js +97 -0
  29. package/lib/abis/OFT.d.ts +116 -0
  30. package/lib/abis/OFT.js +85 -0
  31. package/lib/abis/PoolAdapter.d.ts +36 -0
  32. package/lib/abis/PoolAdapter.js +51 -0
  33. package/lib/abis/RewardDistributor.d.ts +267 -0
  34. package/lib/abis/RewardDistributor.js +352 -0
  35. package/lib/abis/RwaRedeemSubaccount.d.ts +747 -0
  36. package/lib/abis/RwaRedeemSubaccount.js +548 -0
  37. package/lib/abis/SmartAccount.d.ts +17 -0
  38. package/lib/abis/SmartAccount.js +19 -0
  39. package/lib/abis/SwapRouter.d.ts +953 -0
  40. package/lib/abis/SwapRouter.js +1228 -0
  41. package/lib/abis/TextResolver.d.ts +16 -0
  42. package/lib/abis/TextResolver.js +16 -0
  43. package/lib/abis/TokenizedVaultV2.d.ts +1364 -0
  44. package/lib/abis/TokenizedVaultV2.js +1041 -0
  45. package/lib/abis/TokenizedVaultV2DepositWithPermit.d.ts +1456 -0
  46. package/lib/abis/TokenizedVaultV2DepositWithPermit.js +1878 -0
  47. package/lib/abis/TokenizedVaultV2Receipt.d.ts +1568 -0
  48. package/lib/abis/TokenizedVaultV2Receipt.js +1061 -0
  49. package/lib/abis/TokenizedVaultV2SenderAllocationWhitelist.d.ts +454 -0
  50. package/lib/abis/TokenizedVaultV2SenderAllocationWhitelist.js +327 -0
  51. package/lib/abis/TokenizedVaultV2WhitelistedAllocation.d.ts +1466 -0
  52. package/lib/abis/TokenizedVaultV2WhitelistedAllocation.js +1092 -0
  53. package/lib/abis/TokenizedVaultV2WhitelistedAssets.d.ts +274 -0
  54. package/lib/abis/TokenizedVaultV2WhitelistedAssets.js +167 -0
  55. package/lib/abis/UniversalResolverResolve.d.ts +69 -0
  56. package/lib/abis/UniversalResolverResolve.js +35 -0
  57. package/lib/abis/UniversalSignatureValidator.d.ts +17 -0
  58. package/lib/abis/UniversalSignatureValidator.js +30 -0
  59. package/lib/abis/WrapperAdapter.d.ts +71 -0
  60. package/lib/abis/WrapperAdapter.js +77 -0
  61. package/lib/abis/index.d.ts +34 -0
  62. package/lib/abis/index.js +51 -0
  63. package/lib/adapters/evm/getters.d.ts +19 -0
  64. package/lib/adapters/evm/getters.js +249 -0
  65. package/lib/adapters/evm/index.d.ts +539 -0
  66. package/lib/adapters/evm/index.js +656 -0
  67. package/lib/adapters/evm/utils.d.ts +8 -0
  68. package/lib/adapters/evm/utils.js +51 -0
  69. package/lib/adapters/solana/confirm.d.ts +88 -0
  70. package/lib/adapters/solana/confirm.js +291 -0
  71. package/lib/adapters/solana/constants.d.ts +42 -0
  72. package/lib/adapters/solana/constants.js +59 -0
  73. package/lib/adapters/solana/getters.d.ts +42 -0
  74. package/lib/adapters/solana/getters.js +275 -0
  75. package/lib/adapters/solana/idl/vault-idl.d.ts +700 -0
  76. package/lib/adapters/solana/idl/vault-idl.js +2101 -0
  77. package/lib/adapters/solana/idl/withdrawal-queue-idl.d.ts +795 -0
  78. package/lib/adapters/solana/idl/withdrawal-queue-idl.js +1864 -0
  79. package/lib/adapters/solana/index.d.ts +372 -0
  80. package/lib/adapters/solana/index.js +459 -0
  81. package/lib/adapters/solana/pricing.d.ts +184 -0
  82. package/lib/adapters/solana/pricing.js +239 -0
  83. package/lib/adapters/solana/queue-hints.d.ts +29 -0
  84. package/lib/adapters/solana/queue-hints.js +59 -0
  85. package/lib/adapters/solana/queue.actions.d.ts +97 -0
  86. package/lib/adapters/solana/queue.actions.js +468 -0
  87. package/lib/adapters/solana/token-reads.d.ts +108 -0
  88. package/lib/adapters/solana/token-reads.js +145 -0
  89. package/lib/adapters/solana/types.d.ts +325 -0
  90. package/lib/adapters/solana/types.js +3 -0
  91. package/lib/adapters/solana/utils.d.ts +229 -0
  92. package/lib/adapters/solana/utils.js +773 -0
  93. package/lib/adapters/solana/vault.actions.d.ts +140 -0
  94. package/lib/adapters/solana/vault.actions.js +657 -0
  95. package/lib/adapters/solana/withdrawal-queue.d.ts +125 -0
  96. package/lib/adapters/solana/withdrawal-queue.js +301 -0
  97. package/lib/adapters/stellar/actions.d.ts +41 -0
  98. package/lib/adapters/stellar/actions.js +126 -0
  99. package/lib/adapters/stellar/constants.d.ts +43 -0
  100. package/lib/adapters/stellar/constants.js +53 -0
  101. package/lib/adapters/stellar/getters.d.ts +68 -0
  102. package/lib/adapters/stellar/getters.js +290 -0
  103. package/lib/adapters/stellar/index.d.ts +126 -0
  104. package/lib/adapters/stellar/index.js +187 -0
  105. package/lib/adapters/stellar/invocation.d.ts +55 -0
  106. package/lib/adapters/stellar/invocation.js +92 -0
  107. package/lib/adapters/stellar/soroban.d.ts +131 -0
  108. package/lib/adapters/stellar/soroban.js +617 -0
  109. package/lib/adapters/stellar/submit.d.ts +42 -0
  110. package/lib/adapters/stellar/submit.js +195 -0
  111. package/lib/adapters/stellar/types.d.ts +57 -0
  112. package/lib/adapters/stellar/types.js +6 -0
  113. package/lib/adapters/stellar/utils.d.ts +24 -0
  114. package/lib/adapters/stellar/utils.js +34 -0
  115. package/lib/adapters/sui/constants.d.ts +1 -0
  116. package/lib/adapters/sui/constants.js +9 -0
  117. package/lib/adapters/sui/getters.d.ts +24 -0
  118. package/lib/adapters/sui/getters.js +134 -0
  119. package/lib/adapters/sui/index.d.ts +53 -0
  120. package/lib/adapters/sui/index.js +96 -0
  121. package/lib/adapters/sui/transformer.d.ts +10 -0
  122. package/lib/adapters/sui/transformer.js +115 -0
  123. package/lib/adapters/sui/types.d.ts +70 -0
  124. package/lib/adapters/sui/types.js +3 -0
  125. package/lib/adapters/sui/utils.d.ts +8 -0
  126. package/lib/adapters/sui/utils.js +17 -0
  127. package/lib/core/analytics/app-name.d.ts +27 -0
  128. package/lib/core/analytics/app-name.js +48 -0
  129. package/lib/core/analytics/chain-name.d.ts +9 -0
  130. package/lib/core/analytics/chain-name.js +34 -0
  131. package/lib/core/analytics/constants.d.ts +15 -0
  132. package/lib/core/analytics/constants.js +28 -0
  133. package/lib/core/analytics/env.d.ts +46 -0
  134. package/lib/core/analytics/env.js +147 -0
  135. package/lib/core/analytics/index.d.ts +39 -0
  136. package/lib/core/analytics/index.js +83 -0
  137. package/lib/core/analytics/instrumentation.d.ts +29 -0
  138. package/lib/core/analytics/instrumentation.js +292 -0
  139. package/lib/core/analytics/method-taxonomy.d.ts +19 -0
  140. package/lib/core/analytics/method-taxonomy.js +156 -0
  141. package/lib/core/analytics/metrics.d.ts +48 -0
  142. package/lib/core/analytics/metrics.js +142 -0
  143. package/lib/core/analytics/sanitize.d.ts +57 -0
  144. package/lib/core/analytics/sanitize.js +272 -0
  145. package/lib/core/analytics/sentry-runtime.d.ts +15 -0
  146. package/lib/core/analytics/sentry-runtime.js +131 -0
  147. package/lib/core/analytics/sentry.d.ts +100 -0
  148. package/lib/core/analytics/sentry.js +696 -0
  149. package/lib/core/analytics/types.d.ts +55 -0
  150. package/lib/core/analytics/types.js +3 -0
  151. package/lib/core/analytics/user-identity.d.ts +55 -0
  152. package/lib/core/analytics/user-identity.js +218 -0
  153. package/lib/core/analytics/version.d.ts +6 -0
  154. package/lib/core/analytics/version.js +10 -0
  155. package/lib/core/attribution.d.ts +159 -0
  156. package/lib/core/attribution.js +194 -0
  157. package/lib/core/auth/index.d.ts +1 -0
  158. package/lib/core/auth/index.js +18 -0
  159. package/lib/core/auth/verify.d.ts +2 -0
  160. package/lib/core/auth/verify.js +35 -0
  161. package/lib/core/base.class.d.ts +193 -0
  162. package/lib/core/base.class.js +163 -0
  163. package/lib/core/cache.d.ts +9 -0
  164. package/lib/core/cache.js +31 -0
  165. package/lib/core/constants/adapters.d.ts +103 -0
  166. package/lib/core/constants/adapters.js +180 -0
  167. package/lib/core/constants/core.d.ts +162 -0
  168. package/lib/core/constants/core.js +281 -0
  169. package/lib/core/constants/swap-router.d.ts +180 -0
  170. package/lib/core/constants/swap-router.js +203 -0
  171. package/lib/core/constants/vaults.d.ts +93 -0
  172. package/lib/core/constants/vaults.js +277 -0
  173. package/lib/core/constants/web3.d.ts +106 -0
  174. package/lib/core/constants/web3.js +251 -0
  175. package/lib/core/errors/index.d.ts +177 -0
  176. package/lib/core/errors/index.js +257 -0
  177. package/lib/core/fetcher.d.ts +237 -0
  178. package/lib/core/fetcher.js +1103 -0
  179. package/lib/core/helpers/adapters.d.ts +13 -0
  180. package/lib/core/helpers/adapters.js +39 -0
  181. package/lib/core/helpers/chain-address.d.ts +13 -0
  182. package/lib/core/helpers/chain-address.js +47 -0
  183. package/lib/core/helpers/chain-error.d.ts +207 -0
  184. package/lib/core/helpers/chain-error.js +697 -0
  185. package/lib/core/helpers/chain-support.d.ts +98 -0
  186. package/lib/core/helpers/chain-support.js +139 -0
  187. package/lib/core/helpers/concurrency.d.ts +51 -0
  188. package/lib/core/helpers/concurrency.js +79 -0
  189. package/lib/core/helpers/core.d.ts +92 -0
  190. package/lib/core/helpers/core.js +368 -0
  191. package/lib/core/helpers/explorer-link.d.ts +16 -0
  192. package/lib/core/helpers/explorer-link.js +26 -0
  193. package/lib/core/helpers/multicall.d.ts +68 -0
  194. package/lib/core/helpers/multicall.js +103 -0
  195. package/lib/core/helpers/revert-decode.d.ts +248 -0
  196. package/lib/core/helpers/revert-decode.js +515 -0
  197. package/lib/core/helpers/signer.d.ts +61 -0
  198. package/lib/core/helpers/signer.js +184 -0
  199. package/lib/core/helpers/swap-router.d.ts +269 -0
  200. package/lib/core/helpers/swap-router.js +542 -0
  201. package/lib/core/helpers/vault-version.d.ts +24 -0
  202. package/lib/core/helpers/vault-version.js +83 -0
  203. package/lib/core/helpers/vaults.d.ts +89 -0
  204. package/lib/core/helpers/vaults.js +241 -0
  205. package/lib/core/helpers/web3.d.ts +366 -0
  206. package/lib/core/helpers/web3.js +1034 -0
  207. package/lib/core/index.d.ts +23 -0
  208. package/lib/core/index.js +40 -0
  209. package/lib/core/logger/curator-alert.d.ts +217 -0
  210. package/lib/core/logger/curator-alert.js +276 -0
  211. package/lib/core/logger/index.d.ts +99 -0
  212. package/lib/core/logger/index.js +163 -0
  213. package/lib/core/logger/slack.d.ts +49 -0
  214. package/lib/core/logger/slack.js +111 -0
  215. package/lib/core/vault-metadata.d.ts +12 -0
  216. package/lib/core/vault-metadata.js +42 -0
  217. package/lib/core/version-check.d.ts +58 -0
  218. package/lib/core/version-check.js +182 -0
  219. package/lib/evm/index.d.ts +2 -0
  220. package/lib/evm/index.js +19 -0
  221. package/lib/evm/methods/crossChainVault.d.ts +132 -0
  222. package/lib/evm/methods/crossChainVault.js +857 -0
  223. package/lib/evm/methods/crossChainVaultRegistry.d.ts +92 -0
  224. package/lib/evm/methods/crossChainVaultRegistry.js +239 -0
  225. package/lib/evm/methods/index.d.ts +2 -0
  226. package/lib/evm/methods/index.js +19 -0
  227. package/lib/evm/types/crossChain.d.ts +363 -0
  228. package/lib/evm/types/crossChain.js +20 -0
  229. package/lib/evm/types/index.d.ts +1 -0
  230. package/lib/evm/types/index.js +18 -0
  231. package/lib/index.d.ts +30 -0
  232. package/lib/index.js +52 -0
  233. package/lib/main.d.ts +596 -0
  234. package/lib/main.js +681 -0
  235. package/lib/modules/api/fetcher.d.ts +108 -0
  236. package/lib/modules/api/fetcher.js +187 -0
  237. package/lib/modules/api/index.d.ts +1 -0
  238. package/lib/modules/api/index.js +6 -0
  239. package/lib/modules/api/main.d.ts +587 -0
  240. package/lib/modules/api/main.js +911 -0
  241. package/lib/modules/sub-accounts/fetcher.d.ts +53 -0
  242. package/lib/modules/sub-accounts/fetcher.js +96 -0
  243. package/lib/modules/sub-accounts/index.d.ts +2 -0
  244. package/lib/modules/sub-accounts/index.js +19 -0
  245. package/lib/modules/sub-accounts/main.d.ts +249 -0
  246. package/lib/modules/sub-accounts/main.js +211 -0
  247. package/lib/modules/sub-accounts/utils.d.ts +106 -0
  248. package/lib/modules/sub-accounts/utils.js +112 -0
  249. package/lib/modules/vaults/adapter.helpers.d.ts +84 -0
  250. package/lib/modules/vaults/adapter.helpers.js +202 -0
  251. package/lib/modules/vaults/fetcher.d.ts +147 -0
  252. package/lib/modules/vaults/fetcher.js +357 -0
  253. package/lib/modules/vaults/getters.d.ts +1060 -0
  254. package/lib/modules/vaults/getters.js +4079 -0
  255. package/lib/modules/vaults/index.d.ts +20 -0
  256. package/lib/modules/vaults/index.js +44 -0
  257. package/lib/modules/vaults/main.d.ts +607 -0
  258. package/lib/modules/vaults/main.js +1687 -0
  259. package/lib/modules/vaults/prefetch.d.ts +65 -0
  260. package/lib/modules/vaults/prefetch.js +120 -0
  261. package/lib/modules/vaults/read.actions.d.ts +406 -0
  262. package/lib/modules/vaults/read.actions.js +891 -0
  263. package/lib/modules/vaults/types.d.ts +149 -0
  264. package/lib/modules/vaults/types.js +3 -0
  265. package/lib/modules/vaults/utils/call-data-decoder.d.ts +61 -0
  266. package/lib/modules/vaults/utils/call-data-decoder.js +194 -0
  267. package/lib/modules/vaults/utils/date-utils.d.ts +50 -0
  268. package/lib/modules/vaults/utils/date-utils.js +84 -0
  269. package/lib/modules/vaults/utils.d.ts +153 -0
  270. package/lib/modules/vaults/utils.js +818 -0
  271. package/lib/modules/vaults/write.actions.d.ts +812 -0
  272. package/lib/modules/vaults/write.actions.js +2377 -0
  273. package/lib/polyfills.d.ts +1 -0
  274. package/lib/polyfills.js +12 -0
  275. package/lib/sdk.d.ts +30154 -0
  276. package/lib/services/coingecko/fetcher.d.ts +15 -0
  277. package/lib/services/coingecko/fetcher.js +67 -0
  278. package/lib/services/coingecko/index.d.ts +2 -0
  279. package/lib/services/coingecko/index.js +19 -0
  280. package/lib/services/coingecko/utils.d.ts +1 -0
  281. package/lib/services/coingecko/utils.js +24 -0
  282. package/lib/services/debank/fetcher.d.ts +126 -0
  283. package/lib/services/debank/fetcher.js +47 -0
  284. package/lib/services/debank/index.d.ts +2 -0
  285. package/lib/services/debank/index.js +19 -0
  286. package/lib/services/debank/utils.d.ts +50 -0
  287. package/lib/services/debank/utils.js +325 -0
  288. package/lib/services/layerzero/deposits.d.ts +55 -0
  289. package/lib/services/layerzero/deposits.js +174 -0
  290. package/lib/services/layerzero/redeems.d.ts +20 -0
  291. package/lib/services/layerzero/redeems.js +92 -0
  292. package/lib/services/layerzero/utils.d.ts +9 -0
  293. package/lib/services/layerzero/utils.js +22 -0
  294. package/lib/services/octavfi/fetcher.d.ts +33 -0
  295. package/lib/services/octavfi/fetcher.js +39 -0
  296. package/lib/services/octavfi/index.d.ts +3 -0
  297. package/lib/services/octavfi/index.js +20 -0
  298. package/lib/services/octavfi/types.d.ts +34 -0
  299. package/lib/services/octavfi/types.js +3 -0
  300. package/lib/services/octavfi/utils.d.ts +16 -0
  301. package/lib/services/octavfi/utils.js +244 -0
  302. package/lib/services/subgraph/fetcher.d.ts +2 -0
  303. package/lib/services/subgraph/fetcher.js +66 -0
  304. package/lib/services/subgraph/index.d.ts +2 -0
  305. package/lib/services/subgraph/index.js +19 -0
  306. package/lib/services/subgraph/schema.d.ts +48 -0
  307. package/lib/services/subgraph/schema.js +87 -0
  308. package/lib/services/subgraph/vaults.d.ts +83 -0
  309. package/lib/services/subgraph/vaults.js +1282 -0
  310. package/lib/services/swap-quotes/index.d.ts +105 -0
  311. package/lib/services/swap-quotes/index.js +77 -0
  312. package/lib/services/swap-quotes/paraswap.d.ts +51 -0
  313. package/lib/services/swap-quotes/paraswap.js +138 -0
  314. package/lib/services/untangled/fetcher.d.ts +40 -0
  315. package/lib/services/untangled/fetcher.js +110 -0
  316. package/lib/services/untangled/index.d.ts +3 -0
  317. package/lib/services/untangled/index.js +20 -0
  318. package/lib/services/untangled/types.d.ts +83 -0
  319. package/lib/services/untangled/types.js +10 -0
  320. package/lib/services/untangled/utils.d.ts +91 -0
  321. package/lib/services/untangled/utils.js +339 -0
  322. package/lib/types/api.d.ts +498 -0
  323. package/lib/types/api.js +3 -0
  324. package/lib/types/index.d.ts +12 -0
  325. package/lib/types/index.js +28 -0
  326. package/lib/types/points.d.ts +28 -0
  327. package/lib/types/points.js +3 -0
  328. package/lib/types/pools.d.ts +144 -0
  329. package/lib/types/pools.js +3 -0
  330. package/lib/types/staking.d.ts +28 -0
  331. package/lib/types/staking.js +3 -0
  332. package/lib/types/sub-accounts.d.ts +150 -0
  333. package/lib/types/sub-accounts.js +3 -0
  334. package/lib/types/subgraph.d.ts +84 -0
  335. package/lib/types/subgraph.js +3 -0
  336. package/lib/types/typed-contract.d.ts +102 -0
  337. package/lib/types/typed-contract.js +3 -0
  338. package/lib/types/user.d.ts +1 -0
  339. package/lib/types/user.js +3 -0
  340. package/lib/types/vaults.d.ts +886 -0
  341. package/lib/types/vaults.js +30 -0
  342. package/lib/types/web3.d.ts +81 -0
  343. package/lib/types/web3.js +14 -0
  344. package/lib/types/webserver.d.ts +730 -0
  345. package/lib/types/webserver.js +3 -0
  346. package/package.json +78 -3
@@ -0,0 +1,4079 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.getSwapRouterDepositResult = void 0;
40
+ exports.getVault = getVault;
41
+ exports.getVaultLoans = getVaultLoans;
42
+ exports.getVaultSubaccountLoans = getVaultSubaccountLoans;
43
+ exports.getVaultAllocations = getVaultAllocations;
44
+ exports.getVaultAvailableRedemptions = getVaultAvailableRedemptions;
45
+ exports.getVaultRedemptionHistory = getVaultRedemptionHistory;
46
+ exports.getVaultPositions = getVaultPositions;
47
+ exports.getVaultApy = getVaultApy;
48
+ exports.getRewardsStakingPositions = getRewardsStakingPositions;
49
+ exports.getVaultTvl = getVaultTvl;
50
+ exports.getVaultBorrowerHealthFactor = getVaultBorrowerHealthFactor;
51
+ exports.getHealthFactorOfBorrowersByVault = getHealthFactorOfBorrowersByVault;
52
+ exports.getUserPoints = getUserPoints;
53
+ exports.registerUserForPoints = registerUserForPoints;
54
+ exports.fetchPointsLeaderboard = fetchPointsLeaderboard;
55
+ exports.getYieldLastRealizedOn = getYieldLastRealizedOn;
56
+ exports.assertPnlHistoryConsistent = assertPnlHistoryConsistent;
57
+ exports.assertPnlTokenPrice = assertPnlTokenPrice;
58
+ exports.sumDepositsAtFaceValue = sumDepositsAtFaceValue;
59
+ exports.selectPeerTransfers = selectPeerTransfers;
60
+ exports.transferCostBasis = transferCostBasis;
61
+ exports.historyCoversMints = historyCoversMints;
62
+ exports.selectReserveDeposits = selectReserveDeposits;
63
+ exports.countWithdrawalRequestsOnce = countWithdrawalRequestsOnce;
64
+ exports.settleV2Redemptions = settleV2Redemptions;
65
+ exports.pnlCostBasis = pnlCostBasis;
66
+ exports.getVaultUserLifetimePnl = getVaultUserLifetimePnl;
67
+ exports.getVaultPnl = getVaultPnl;
68
+ exports.getVaultHistoricalTimeseries = getVaultHistoricalTimeseries;
69
+ exports.getVaultAnnualizedApy = getVaultAnnualizedApy;
70
+ exports.getVaultSummary = getVaultSummary;
71
+ exports.getVaultWithdrawals = getVaultWithdrawals;
72
+ exports.getVaultPendingRedemptions = getVaultPendingRedemptions;
73
+ exports.getPreviewRedemption = getPreviewRedemption;
74
+ exports.getWithdrawalRequestsWithStatus = getWithdrawalRequestsWithStatus;
75
+ const abis_1 = require("../../abis");
76
+ const types_1 = require("../../types");
77
+ const core_1 = require("../../core");
78
+ const ethers_1 = require("ethers");
79
+ const cache_1 = require("../../core/cache");
80
+ const concurrency_1 = require("../../core/helpers/concurrency");
81
+ const utils_1 = require("./utils");
82
+ const subgraph_1 = require("../../services/subgraph");
83
+ const vaults_1 = require("../../services/subgraph/vaults");
84
+ const debank_1 = require("../../services/debank");
85
+ const utils_2 = require("../../adapters/solana/utils");
86
+ const SolanaGetters = __importStar(require("../../adapters/solana/getters"));
87
+ const SolanaConstants = __importStar(require("../../adapters/solana/constants"));
88
+ const withdrawal_queue_1 = require("../../adapters/solana/withdrawal-queue");
89
+ const web3_js_1 = require("@solana/web3.js");
90
+ const StellarGetters = __importStar(require("../../adapters/stellar/getters"));
91
+ const utils_3 = require("../../adapters/stellar/utils");
92
+ const constants_1 = require("../../adapters/stellar/constants");
93
+ const EVM = __importStar(require("../../adapters/evm"));
94
+ const TokenizedVaultV2_1 = require("../../abis/TokenizedVaultV2");
95
+ const TokenizedVaultV2Receipt_1 = require("../../abis/TokenizedVaultV2Receipt");
96
+ const ethereum_block_by_date_1 = __importDefault(require("ethereum-block-by-date"));
97
+ const octavfi_1 = require("../../services/octavfi");
98
+ const untangled_1 = require("../../services/untangled");
99
+ const date_utils_1 = require("./utils/date-utils");
100
+ const call_data_decoder_1 = require("./utils/call-data-decoder");
101
+ const deposits_1 = require("../../services/layerzero/deposits");
102
+ const redeems_1 = require("../../services/layerzero/redeems");
103
+ const errors_1 = require("../../core/errors");
104
+ /**
105
+ * Vault Data Getters
106
+ *
107
+ * Main query functions for vault data across chains:
108
+ * - getVault: Comprehensive vault details with loans/allocations
109
+ * - getVaultLoans: Active loan data with on-chain enrichment
110
+ * - getVaultAllocations: DeFi/CeFi/OTC allocation breakdowns
111
+ * - getVaultAvailableRedemptions: Claimable redemption requests
112
+ * - getVaultRedemptionHistory: Historical redemption data
113
+ * - getVaultPositions: User positions across vaults
114
+ * - getVaultApy: @deprecated Current and historical APY data
115
+ * - getRewardsStakingPositions: Staking positions and rewards
116
+ * - getVaultTvl: Current and historical TVL
117
+ * - getVaultHistoricalTimeseries: Time series data for vault metrics
118
+ */
119
+ /**
120
+ * Fetch comprehensive vault data including on-chain state and backend metadata.
121
+ * Routes to appropriate chain adapter (EVM v1/v2, Solana, or Stellar) based on vault version.
122
+ * Optionally enriches with loan and allocation data.
123
+ * @param vault - Vault contract address (EVM hex, Stellar C-address, or Solana program ID)
124
+ * @param loans - Include active loan data. EVM vaults only — there is no
125
+ * non-EVM loan book to read, so other families keep the empty default.
126
+ * @param allocations - Include DeFi/CeFi allocation breakdowns. Resolved for EVM
127
+ * vaults (via DeBank), Stellar vaults (via the Untangled portfolio API) and
128
+ * Solana vaults (each operator's cached Octav portfolio). Sui has no
129
+ * portfolio provider and keeps the empty default.
130
+ * @param options - RPC and service configuration
131
+ * @param tokenizedVault - Optional pre-fetched backend row for `vault`. When a
132
+ * caller already holds the row (e.g. `getVaults` fetched the whole list one
133
+ * call earlier), passing it skips this function's own
134
+ * `GET /tokenized_vault/{address}` — a pure de-duplication of backend
135
+ * traffic. The row must have been fetched with the same
136
+ * `loadSubaccounts`/`loadSnapshots` flags the caller passes here.
137
+ * @returns Complete vault object with optional enrichments
138
+ * @throws {@link UpshiftNetworkError} (`NETWORK_ERROR`) when `allocations` is
139
+ * requested and a borrower's portfolio read (DeBank for EVM, Octav for
140
+ * Solana) fails. The whole call fails rather than returning the vault with
141
+ * empty allocations; pass `allocations: false` for a read that does not
142
+ * depend on the portfolio provider.
143
+ */
144
+ async function getVault({ vault, loans = false, allocations = false, options, loadSubaccounts, loadSnapshots, tokenizedVault: prefetchedRow, }) {
145
+ let returnedVault;
146
+ // Hoisted out of the try so the allocations enrichment below can reuse the
147
+ // row instead of re-fetching it.
148
+ let tokenizedVault;
149
+ try {
150
+ tokenizedVault =
151
+ prefetchedRow ??
152
+ (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, loadSubaccounts, loadSnapshots))?.[0];
153
+ const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
154
+ switch (vaultVersion) {
155
+ case 'sol-0': {
156
+ returnedVault = await SolanaGetters.getSolanaVault(tokenizedVault, options);
157
+ break;
158
+ }
159
+ case 'stellar-0': {
160
+ returnedVault = await StellarGetters.getStellarVault(tokenizedVault, options);
161
+ break;
162
+ }
163
+ case 'evm-2': {
164
+ returnedVault = await EVM.getEvmVaultV2(tokenizedVault, options);
165
+ break;
166
+ }
167
+ default: {
168
+ returnedVault = await EVM.getEvmVaultV1(tokenizedVault, options);
169
+ break;
170
+ }
171
+ }
172
+ if (returnedVault === null) {
173
+ return null;
174
+ }
175
+ }
176
+ catch (err) {
177
+ // A provider fault (rate limit, socket reset, an empty response to one of
178
+ // the vault's argument-free views) is not an SDK or caller defect: the
179
+ // caller sees the same throw either way, so it is recorded as a breadcrumb
180
+ // rather than a billed Sentry issue. Everything else stays at error.
181
+ (0, core_1.logChainError)('getVault', err, (0, core_1.isRetryableRpcError)(err) || (0, core_1.isEmptyViewResponse)(err), { vault });
182
+ throw new Error(`#getVault::${vault}: ${err?.message}`);
183
+ }
184
+ // Loans enrichment stays EVM-only — there is no non-EVM loan book to read.
185
+ // Allocations additionally covers Stellar (Untangled) and Solana (Octav, per
186
+ // operator); gating it on `isEvmVault` left `getVaultAllocations` unreachable
187
+ // for both, so callers got the empty default while the standalone getter
188
+ // returned real data. Sui has no provider and keeps the empty default.
189
+ const isEvmVault = returnedVault.version !== 'sol-0' &&
190
+ returnedVault.version !== 'stellar-0' &&
191
+ returnedVault.version !== 'sui-0';
192
+ const supportsLoans = isEvmVault;
193
+ const supportsAllocations = isEvmVault ||
194
+ returnedVault.version === 'stellar-0' ||
195
+ returnedVault.version === 'sol-0';
196
+ const readLoans = loans && supportsLoans;
197
+ const readAllocations = allocations && supportsAllocations;
198
+ if ((loans && !supportsLoans) || (allocations && !supportsAllocations)) {
199
+ core_1.Logger.log.warn('getVault', 'Loans/allocations enrichment is not supported for this vault — skipping', { vault, version: returnedVault.version });
200
+ }
201
+ if (!(0, core_1.isBadVault)(vault) && (readLoans || readAllocations)) {
202
+ // `getVaultAllocations` reads `.subaccounts`/`.eoa_operators` off the row to
203
+ // discover borrowers, so the row is withheld unless it can carry them and
204
+ // the getter fetches a full one itself. Both guards matter: `loadSubaccounts:
205
+ // false` can answer with the keys present but empty, and a row threaded in
206
+ // from `getVaults` comes from the list endpoint rather than this one. Getting
207
+ // either wrong resolves zero borrowers with no error — the silent-empty
208
+ // failure this de-duplication exists to avoid.
209
+ const allocationsRow = loadSubaccounts !== false &&
210
+ tokenizedVault?.subaccounts &&
211
+ tokenizedVault?.eoa_operators
212
+ ? tokenizedVault
213
+ : undefined;
214
+ const [loansResult, allocationsResult] = await Promise.allSettled([
215
+ readLoans
216
+ ? getVaultLoans(returnedVault, options)
217
+ : Promise.resolve(undefined),
218
+ readAllocations
219
+ ? getVaultAllocations(vault, options, allocationsRow)
220
+ : Promise.resolve(undefined),
221
+ ]);
222
+ if (readLoans) {
223
+ if (loansResult.status === 'fulfilled') {
224
+ returnedVault = {
225
+ ...returnedVault,
226
+ loans: loansResult.value,
227
+ };
228
+ }
229
+ else {
230
+ core_1.Logger.log.error('getVault:loans', loansResult.reason, {
231
+ symbol: returnedVault.receipt.symbol,
232
+ });
233
+ }
234
+ }
235
+ if (readAllocations) {
236
+ if (allocationsResult.status === 'fulfilled') {
237
+ returnedVault = {
238
+ ...returnedVault,
239
+ allocations: allocationsResult.value,
240
+ };
241
+ }
242
+ else {
243
+ core_1.Logger.log.error('getVault:allocations', allocationsResult.reason, {
244
+ symbol: returnedVault.receipt.symbol,
245
+ });
246
+ // Fail the whole read, matching EVM: a vault returned without the
247
+ // allocations the caller asked for would be indistinguishable from
248
+ // one that has none. Callers that prefer a partial vault can retry
249
+ // with `allocations: false`.
250
+ throw new errors_1.UpshiftNetworkError('getVault: failed to fetch vault allocations from the portfolio provider. Retry, or call getVault with allocations: false.', {
251
+ context: { vault: String(vault) },
252
+ cause: allocationsResult.reason,
253
+ });
254
+ }
255
+ }
256
+ }
257
+ return returnedVault;
258
+ }
259
+ /**
260
+ * Shared implementation behind {@link getVaultLoans} and
261
+ * {@link getVaultSubaccountLoans}, which differ only in which backend loan
262
+ * list they read and in the label used for logs and error messages.
263
+ *
264
+ * Returns `[]` for any vault that is not `evm-0` (only evm-0 lending pools
265
+ * have loans). Otherwise fetches the loan list once, reads the pool's
266
+ * `totalSupply` and decimals on-chain when `vault` is an address (skipped
267
+ * when a full `IVault` is passed), and reads one loan-oracle fee rate per
268
+ * loan in parallel.
269
+ *
270
+ * @param vault - Vault address, or an already-loaded `IVault`.
271
+ * @param options - RPC URL, optional chain ID override, and monitoring headers.
272
+ * @param fetchLoans - Backend fetcher for the loan list
273
+ * (`fetchTokenizedVaultLoans` or `fetchTokenizedVaultSubaccountLoans`).
274
+ * @param label - Public method name used in the log entry and error message.
275
+ * @returns The formatted loans.
276
+ * @throws Error prefixed `#<label>::<vault>:` when any fetch fails.
277
+ */
278
+ async function buildVaultLoans(vault, options, fetchLoans, label) {
279
+ // sanitize
280
+ const vaultAddress = typeof vault === 'string' ? vault : vault.address;
281
+ const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vaultAddress, options?.headers, false, false))?.[0];
282
+ const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
283
+ if (vaultVersion !== 'evm-0')
284
+ return []; // only evm-0 uses subaccount loans
285
+ try {
286
+ let poolTotalSupply;
287
+ let poolDecimals;
288
+ const chainId = options?.chainId || tokenizedVault?.chain;
289
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
290
+ const tokenizedVaultLoans = await fetchLoans(vaultAddress, chainId);
291
+ // if user passed IAddress or a full IVault
292
+ if (typeof vault === 'string') {
293
+ // fetch necessary data
294
+ const poolContract = (0, core_1.createContract)({
295
+ provider,
296
+ address: vaultAddress,
297
+ abi: abis_1.ABI_LENDING_POOL_V2,
298
+ });
299
+ [poolTotalSupply, poolDecimals] = await Promise.all([
300
+ poolContract.totalSupply(),
301
+ (0, core_1.getDecimals)(provider, vaultAddress),
302
+ ]);
303
+ poolTotalSupply = BigInt(poolTotalSupply).toString();
304
+ poolDecimals = Number(poolDecimals);
305
+ }
306
+ else {
307
+ poolTotalSupply = vault.totalSupply.raw;
308
+ poolDecimals = vault.decimals;
309
+ }
310
+ // fetch and format all the loans
311
+ const newLoans = (await Promise.all((Array.isArray(tokenizedVaultLoans) ? tokenizedVaultLoans : [])?.map(async (l) => {
312
+ // variables
313
+ const borrower = l.borrower;
314
+ // calculate allocation
315
+ const allocation = l.principal_amount /
316
+ Number((0, ethers_1.formatUnits)(poolTotalSupply, poolDecimals));
317
+ // get upshift loan fee from oracle
318
+ const loanFeeRate = await (0, core_1.getLoanOracleFeeRate)(provider, 'LOAN.REPAY.INTERESTS', l.address, chainId);
319
+ // calculate loan level apr
320
+ const loanApr = Number(l.apr || 0) / 100;
321
+ const loanAprAfterFees = loanApr * (1 - loanFeeRate / 100);
322
+ // determine if capital is deployed
323
+ const isIdleCapital = core_1.IDLE_CAPITAL_BORROWER_ADDRESS.includes(borrower);
324
+ // build new obj
325
+ const newLoanObj = {
326
+ vault: vaultAddress,
327
+ address: l.address,
328
+ lender: l.lender,
329
+ borrower: l.borrower,
330
+ state: l.state,
331
+ totalRepaid: l.total_repaid,
332
+ principalToken: l.principal_token,
333
+ principalAmount: l.principal_amount,
334
+ interestAmount: l.interest_amount,
335
+ upcomingPayment: {
336
+ amount: l.upcoming_payment.amount,
337
+ dueDate: l.upcoming_payment.due_date,
338
+ },
339
+ apr: loanAprAfterFees,
340
+ initialPrincipalAmount: l.initial_principal_amount,
341
+ deployedDate: l.deployed_date,
342
+ isIdleCapital,
343
+ paymentInterval: l.payment_interval,
344
+ allocation,
345
+ };
346
+ return newLoanObj;
347
+ }))).filter((l) => l !== undefined);
348
+ return newLoans;
349
+ }
350
+ catch (e) {
351
+ core_1.Logger.log.error(label, e, { vault });
352
+ throw new Error(`#${label}::${vault}:${e?.message}`);
353
+ }
354
+ }
355
+ /**
356
+ * Vault Loans
357
+ */
358
+ async function getVaultLoans(vault, options) {
359
+ return buildVaultLoans(vault, options, core_1.fetchTokenizedVaultLoans, 'getVaultLoans');
360
+ }
361
+ /**
362
+ * Vault Subaccount Loans
363
+ */
364
+ async function getVaultSubaccountLoans(vault, options) {
365
+ return buildVaultLoans(vault, options, core_1.fetchTokenizedVaultSubaccountLoans, 'getVaultSubaccountLoans');
366
+ }
367
+ /**
368
+ * HTTP statuses that mean "no such record for this subaccount", not "something
369
+ * broke". The backend returns 404 for a subaccount with no CeFi/OTC position,
370
+ * which is the normal case for most vault borrowers.
371
+ *
372
+ * `400` is here for the same reason. The CeFi and OTC endpoints answer `400`
373
+ * for a borrower they do not track — a well-formed, checksummed EVM address
374
+ * that simply has no record on that side — and every `getVaultAllocations`
375
+ * call re-asks for the same untracked borrowers on every render. In Sentry
376
+ * that produced ~2.7k `UpshiftServerError: Request failed: 400` events across
377
+ * two issues in two days, all from the same handful of borrowers, none of them
378
+ * actionable. Both fetches are strictly best-effort enrichment: the vault's
379
+ * allocations are returned either way, so a `400` here can never mean the
380
+ * caller's request was malformed.
381
+ *
382
+ * This demotes the *log severity* only — nothing about the response handling
383
+ * changes, and any other status (401, 5xx, transport failures) is still an
384
+ * error-level Sentry issue.
385
+ *
386
+ * **Maintainer note on the `400` entry specifically.** Unlike `204`/`404`, a
387
+ * `400` is semantically "the client sent something wrong", so this demotion is
388
+ * safe only for as long as these two endpoints use `400` to mean "unknown
389
+ * borrower". If either ever starts returning `400` for a genuinely malformed
390
+ * request — a real defect in how the SDK builds the URL or body — this set
391
+ * would silence it. Should that contract change, drop `400` from here and let
392
+ * the backend distinguish the two cases with a status of its own.
393
+ */
394
+ const EXPECTED_SUBACCOUNT_FETCH_STATUSES = new Set([204, 400, 404]);
395
+ /**
396
+ * How long a per-borrower CeFi/OTC enrichment result stays cached, in ms.
397
+ *
398
+ * `getVaultAllocations` asks for the same borrowers on every call, and a vault
399
+ * page re-renders far more often than a CeFi balance changes. Without a cache
400
+ * each render re-issued one request per borrower — together, the two endpoints
401
+ * accounted for ~15.7k of the weekly `UpshiftTimeoutError` events in production.
402
+ *
403
+ * A `null` answer ("this borrower has no CeFi/OTC record") is cached alongside
404
+ * real data: it is the common case and just as worth remembering as a hit.
405
+ * Failures are NOT cached — they leave the key absent so the next call retries.
406
+ *
407
+ * 60s keeps a burst of renders on a single request while staying well inside
408
+ * the freshness any allocation view needs. Entries live in the shared process
409
+ * `CACHE`, so `CACHE.clear()` drops them along with everything else.
410
+ */
411
+ const SUBACCOUNT_ENRICHMENT_TTL_MS = 1000 * 60;
412
+ /** Shared-cache key for a borrower's CeFi positions. */
413
+ const cefiCacheKey = (borrower) => `subaccount-cefi-${borrower}`;
414
+ /** Shared-cache key for a borrower's OTC positions. */
415
+ const otcCacheKey = (borrower) => `subaccount-otc-${borrower}`;
416
+ /**
417
+ * Maximum simultaneous subaccount enrichment requests.
418
+ *
419
+ * Each borrower costs 2 requests (CeFi + OTC), so a window of 5 means at most
420
+ * 5 open connections to the Upshift API from one `getVaultAllocations` call.
421
+ * Worst-case RPC/HTTP volume for a 40-borrower vault: 80 requests, 5 at a time,
422
+ * versus 80 sequential before — and versus 80 simultaneous had this been a bare
423
+ * `Promise.all`.
424
+ */
425
+ const SUBACCOUNT_FETCH_CONCURRENCY = 5;
426
+ /**
427
+ * Fetch one borrower's CeFi positions, memoized for
428
+ * {@link SUBACCOUNT_ENRICHMENT_TTL_MS}.
429
+ *
430
+ * @param borrower - Subaccount/borrower address.
431
+ * @param options - Vault options carrying `upshiftKey` and monitoring headers.
432
+ * @param vault - Vault address, for log context only.
433
+ * @returns The borrower's CeFi positions, or `null` when the backend has no
434
+ * record (or the fetch failed — this is best-effort enrichment and never
435
+ * throws).
436
+ */
437
+ async function fetchSubaccountCefi(borrower, options, vault) {
438
+ const key = cefiCacheKey(borrower);
439
+ // `has` rather than a truthiness check: `null` is a cached answer ("no
440
+ // record for this borrower"), not a miss.
441
+ if (cache_1.CACHE.has(key))
442
+ return cache_1.CACHE.get(key);
443
+ try {
444
+ const cefiResponse = await (0, core_1.fetchUpshiftWithKey)(options.upshiftKey, core_1.WEBSERVER_ENDPOINTS.subaccount.cefi(borrower), { headers: options?.headers });
445
+ if (cefiResponse.status === 200) {
446
+ const cefiRes = (await cefiResponse.json());
447
+ cache_1.CACHE.set(key, cefiRes, { ttl: SUBACCOUNT_ENRICHMENT_TTL_MS });
448
+ return cefiRes;
449
+ }
450
+ // 404 means "this subaccount has no CeFi position", which is the common
451
+ // case for most borrowers — a breadcrumb, not an exception.
452
+ logSubaccountFetchFailure('getVaultAllocations:cefi', cefiResponse.status, cefiResponse.statusText, { vault, borrower });
453
+ if (EXPECTED_SUBACCOUNT_FETCH_STATUSES.has(cefiResponse.status)) {
454
+ // A known "no record" answer is a stable fact about this borrower; cache
455
+ // it so the next render does not re-ask.
456
+ cache_1.CACHE.set(key, null, { ttl: SUBACCOUNT_ENRICHMENT_TTL_MS });
457
+ }
458
+ return null;
459
+ }
460
+ catch (e) {
461
+ const status = httpStatusFromError(e);
462
+ logSubaccountFetchFailure('getVaultAllocations:cefi', status, e, {
463
+ vault,
464
+ borrower,
465
+ });
466
+ if (status !== undefined &&
467
+ EXPECTED_SUBACCOUNT_FETCH_STATUSES.has(status)) {
468
+ cache_1.CACHE.set(key, null, { ttl: SUBACCOUNT_ENRICHMENT_TTL_MS });
469
+ }
470
+ return null;
471
+ }
472
+ }
473
+ /**
474
+ * Fetch one borrower's OTC positions, memoized for
475
+ * {@link SUBACCOUNT_ENRICHMENT_TTL_MS}. Mirrors {@link fetchSubaccountCefi}.
476
+ *
477
+ * @param borrower - Subaccount/borrower address.
478
+ * @param options - Vault options carrying `upshiftKey` and monitoring headers.
479
+ * @param vault - Vault address, for log context only.
480
+ * @returns The borrower's OTC position, or `null` when absent or unfetchable.
481
+ */
482
+ async function fetchSubaccountOtc(borrower, options, vault) {
483
+ const key = otcCacheKey(borrower);
484
+ if (cache_1.CACHE.has(key))
485
+ return cache_1.CACHE.get(key);
486
+ try {
487
+ const otcResponse = await (0, core_1.fetchUpshiftWithKey)(options.upshiftKey, core_1.WEBSERVER_ENDPOINTS.subaccount.otc_positions(borrower), { headers: options?.headers });
488
+ if (otcResponse.status === 200) {
489
+ const otcRes = (await otcResponse.json());
490
+ cache_1.CACHE.set(key, otcRes, { ttl: SUBACCOUNT_ENRICHMENT_TTL_MS });
491
+ return otcRes;
492
+ }
493
+ // 404 means "this subaccount has no OTC positions" — expected for nearly
494
+ // every borrower. See logSubaccountFetchFailure.
495
+ logSubaccountFetchFailure('getVaultAllocations:otc', otcResponse.status, otcResponse.statusText, { vault, borrower });
496
+ if (EXPECTED_SUBACCOUNT_FETCH_STATUSES.has(otcResponse.status)) {
497
+ cache_1.CACHE.set(key, null, { ttl: SUBACCOUNT_ENRICHMENT_TTL_MS });
498
+ }
499
+ return null;
500
+ }
501
+ catch (e) {
502
+ const status = httpStatusFromError(e);
503
+ logSubaccountFetchFailure('getVaultAllocations:otc', status, e, {
504
+ vault,
505
+ borrower,
506
+ });
507
+ if (status !== undefined &&
508
+ EXPECTED_SUBACCOUNT_FETCH_STATUSES.has(status)) {
509
+ cache_1.CACHE.set(key, null, { ttl: SUBACCOUNT_ENRICHMENT_TTL_MS });
510
+ }
511
+ return null;
512
+ }
513
+ }
514
+ /**
515
+ * Log a per-subaccount enrichment fetch failure at the right severity.
516
+ *
517
+ * Why: `getVaultAllocations` issues one CeFi and one OTC request per borrower
518
+ * on every call. Logging the "no record" responses at `error` level captured
519
+ * them as Sentry issues, producing thousands of `UpshiftServerError: Request
520
+ * failed: 404` events a day for a condition that is not a fault and that
521
+ * callers never need to act on. Expected statuses become breadcrumbs; genuine
522
+ * failures (5xx, auth, transport) stay at `error`.
523
+ *
524
+ * @param tag - Call-site tag, e.g. `getVaultAllocations:cefi`. Becomes the
525
+ * `sdk.origin` Sentry tag for grouping.
526
+ * @param status - HTTP status, or `undefined` when the failure was a thrown
527
+ * transport/typed error with no status attached.
528
+ * @param detail - Status text or error to attach as context.
529
+ * @param context - Identifiers scoping the failure (`vault`, `borrower`).
530
+ */
531
+ function logSubaccountFetchFailure(tag, status, detail, context) {
532
+ if (status !== undefined && EXPECTED_SUBACCOUNT_FETCH_STATUSES.has(status)) {
533
+ core_1.Logger.log.warn(tag, { ...context, status, reason: 'no record' });
534
+ return;
535
+ }
536
+ core_1.Logger.log.error(tag, detail, { ...context, status });
537
+ }
538
+ /**
539
+ * Extract the HTTP status from a thrown fetch error, when it carries one.
540
+ *
541
+ * `fetchUpshiftWithKey` throws `UpshiftServerError`/`UpshiftAuthError` for non-2xx
542
+ * responses, so the status-code branch in the callers is unreachable for those
543
+ * — the status has to be recovered from the error to classify it.
544
+ *
545
+ * @param error - Any thrown value.
546
+ * @returns The HTTP status, or `undefined` for errors without one (network
547
+ * failures, aborts, programming errors).
548
+ */
549
+ function httpStatusFromError(error) {
550
+ const status = error?.status;
551
+ return typeof status === 'number' ? status : undefined;
552
+ }
553
+ /**
554
+ * Vault Allocations
555
+ */
556
+ /**
557
+ * Fetch comprehensive vault asset allocation breakdown.
558
+ * Includes DeFi protocols (via DeBank), CeFi balances, OTC positions, and loans.
559
+ * Categorizes exposures by borrowing, supplying, lending, and wallet holdings.
560
+ *
561
+ * DeFi exposure resolves per chain family: EVM vaults via DeBank, Solana
562
+ * borrowers via Octav.fi, and Stellar vaults via the Untangled portfolio API —
563
+ * one vault-level call covering the idle buffer, Stellar protocol positions,
564
+ * custody wallets, and capital bridged to other chains. The Stellar path adds
565
+ * exactly 1 HTTP call and zero for every other chain; when it fails, the
566
+ * exposure section comes back empty and the CeFi, OTC and loan allocations are
567
+ * still returned.
568
+ *
569
+ * @param vault - Vault address
570
+ * @param options - RPC configuration
571
+ * @param tokenizedVault - Optional pre-fetched backend row for `vault`. When a
572
+ * caller already holds the row (e.g. `getVault` fetched it one call earlier),
573
+ * passing it skips this function's own `GET /tokenized_vault/{address}` — a
574
+ * pure de-duplication of backend traffic. The row must carry the subaccount
575
+ * data this function reads (`.subaccounts`, `.eoa_operators`), so it must not
576
+ * have been fetched with `loadSubaccounts: false`; a row fetched that way
577
+ * would silently resolve zero borrowers.
578
+ * @returns Detailed allocation data with exposure categorization
579
+ * @throws {@link UpshiftNetworkError} (`NETWORK_ERROR`) when any borrower's
580
+ * portfolio read (DeBank for EVM, Octav for Solana) fails.
581
+ */
582
+ async function getVaultAllocations(vault, options, tokenizedVault) {
583
+ // to be used on upshift UI
584
+ const protocolExposure = [];
585
+ const tokenExposure = [];
586
+ let cefiExposure = [];
587
+ const otcPositions = {};
588
+ const defiPerBorrower = {};
589
+ const exposurePerCategory = {
590
+ supplying: [],
591
+ borrowing: [],
592
+ wallet: [],
593
+ walletBalance: 0,
594
+ cefiBalance: 0,
595
+ defiBalance: 0,
596
+ lending: [],
597
+ loanBalance: 0,
598
+ };
599
+ const netValue = { value: 0 };
600
+ let debankErr = false;
601
+ let unfilteredTokens = [];
602
+ // Untangled publishes the vault's asset composition as its own decomposition of
603
+ // tvlUsd, with no join to `byProtocol`. It therefore cannot be accumulated into
604
+ // `tokenExposure` alongside the synthesized position tokens without
605
+ // double-counting, so it replaces those rows below for Stellar vaults.
606
+ let untangledAssets = null;
607
+ // How much of `tokenExposure` the Untangled parse produced. Only that prefix is
608
+ // replaced: the accumulator is shared with the per-borrower loop that runs
609
+ // afterwards, and a borrower on a chain with portfolio coverage would otherwise
610
+ // have its rows silently dropped rather than double-counted.
611
+ let untangledTokenCount = 0;
612
+ let vaultOctav;
613
+ const borrowerPortfolioFetchers = {
614
+ solana: async (borrower) => {
615
+ vaultOctav ??= (0, octavfi_1.fetchVaultOctavResponse)(vault, options?.headers);
616
+ const response = await vaultOctav;
617
+ const portfolio = response ? response[borrower] : undefined;
618
+ return portfolio && !('error' in portfolio)
619
+ ? (0, octavfi_1.transformOctavfiToDebank)(portfolio)
620
+ : null;
621
+ },
622
+ };
623
+ // Determine wallets to fetch allocation responses from
624
+ let uniqueBorrowers = [];
625
+ const vaultRow = tokenizedVault ??
626
+ (await (0, core_1.fetchTokenizedVault)(vault, options?.headers))?.[0];
627
+ // Started now so the fresh vault-state read overlaps the portfolio fetches
628
+ // below. It never rejects.
629
+ const solanaAumRead = (0, core_1.getAddressChainType)(vault) === 'solana' && vaultRow
630
+ ? SolanaGetters.getSolanaVaultAumUsd(vaultRow, options)
631
+ : undefined;
632
+ // fetch all vault unique subaccounts/loan borrowers
633
+ try {
634
+ const vaultVersion = (0, core_1.getVaultVersionV2)(vaultRow);
635
+ if (vaultVersion === 'evm-0') {
636
+ const loans = await getVaultLoans(vault, options);
637
+ uniqueBorrowers = [...new Set(loans.map((l) => l.borrower))];
638
+ }
639
+ else {
640
+ const eoa_operators = vaultRow.eoa_operators.map((s) => s.address);
641
+ uniqueBorrowers = [
642
+ ...vaultRow.subaccounts.map((s) => s.address),
643
+ ...eoa_operators,
644
+ ];
645
+ }
646
+ }
647
+ catch (e) {
648
+ core_1.Logger.log.error('getVaultAllocations:borrowers', e);
649
+ }
650
+ // if hardcoded subaccount exists for vault, use that
651
+ const hardcodedSubaccount = Object.values(core_1.VAULT_ALLOCATION_SUBACCOUNTS).find((entry) => entry.address.toLowerCase() === vault.toLowerCase() && !entry.useDebank);
652
+ if (hardcodedSubaccount) {
653
+ const debankRes = await (0, debank_1.fetchDebankResponse)(hardcodedSubaccount.subaccount);
654
+ if (debankRes === false) {
655
+ debankErr = true;
656
+ }
657
+ // pull data from debank results
658
+ (0, debank_1.parseVaultLevelDebank)(debankRes, protocolExposure, tokenExposure, hardcodedSubaccount.subaccount, exposurePerCategory, netValue);
659
+ unfilteredTokens = debankRes?.subaccount?.tokens;
660
+ const debankPerLoan = (0, debank_1.parseLoanLevelDebank)(debankRes);
661
+ defiPerBorrower[hardcodedSubaccount.subaccount] = debankPerLoan;
662
+ }
663
+ else {
664
+ // Fetch DeBank data for all subaccounts in a single request (EVM only)
665
+ if ((0, core_1.getAddressChainType)(vault) === 'evm') {
666
+ const vaultDebankData = await (0, debank_1.fetchVaultDebankResponse)(vault, options?.headers);
667
+ if (vaultDebankData === false) {
668
+ debankErr = true;
669
+ }
670
+ else {
671
+ // Process DeBank data for each subaccount
672
+ for (const [subaccountAddress, subaccountData] of Object.entries(vaultDebankData)) {
673
+ // Skip if subaccount has an error
674
+ if ('error' in subaccountData) {
675
+ core_1.Logger.log.error('getVaultAllocations:debank:subaccount', {
676
+ subaccountAddress,
677
+ error: subaccountData.error,
678
+ });
679
+ continue;
680
+ }
681
+ // Transform to match existing parser format: { subaccount: { positions, app_positions, tokens } }
682
+ const debankRes = {
683
+ subaccount: {
684
+ positions: subaccountData.positions || [],
685
+ app_positions: subaccountData.app_positions || [],
686
+ tokens: subaccountData.tokens || [],
687
+ },
688
+ };
689
+ (0, debank_1.parseVaultLevelDebank)(debankRes, protocolExposure, tokenExposure, subaccountAddress, exposurePerCategory, netValue);
690
+ unfilteredTokens = debankRes?.subaccount?.tokens;
691
+ const debankPerLoan = (0, debank_1.parseLoanLevelDebank)(debankRes);
692
+ defiPerBorrower[subaccountAddress] = debankPerLoan;
693
+ }
694
+ }
695
+ }
696
+ else if ((0, core_1.getAddressChainType)(vault) === 'stellar') {
697
+ // Untangled reconciles the whole Stellar vault in one call — idle buffer,
698
+ // Stellar protocol positions, custody wallets, and value bridged to other
699
+ // chains. It is keyed by vault, not by borrower, so it runs here rather
700
+ // than in the per-borrower loop below, which would multiply the totals.
701
+ const untangledExposure = await (0, untangled_1.fetchUntangledVaultExposure)(vault);
702
+ // A miss leaves the exposure section empty — never `debankErr`. See the
703
+ // `no_fetcher` branch below for why throwing here was wrong.
704
+ if (untangledExposure) {
705
+ const untangledRes = (0, untangled_1.transformUntangledToDebank)(untangledExposure);
706
+ (0, debank_1.parseVaultLevelDebank)(untangledRes, protocolExposure, tokenExposure, vault, exposurePerCategory, netValue);
707
+ unfilteredTokens = untangledRes.subaccount.tokens;
708
+ untangledAssets = (0, untangled_1.untangledAssetExposure)(untangledExposure);
709
+ untangledTokenCount = tokenExposure.length;
710
+ defiPerBorrower[vault] = (0, debank_1.parseLoanLevelDebank)(untangledRes);
711
+ }
712
+ }
713
+ // Fetch portfolio data for non-EVM borrowers (EVM handled by fetchVaultDebankResponse above).
714
+ // Sequential by design: this branch only runs for non-EVM borrowers (a
715
+ // handful per vault) and each parser call mutates the shared accumulators
716
+ // below, which are not safe to interleave.
717
+ for (const borrower of uniqueBorrowers) {
718
+ const chainType = (0, core_1.getAddressChainType)(borrower);
719
+ if (chainType === 'evm')
720
+ continue;
721
+ const fetcher = borrowerPortfolioFetchers[chainType];
722
+ if (!fetcher) {
723
+ // Not an error: portfolio providers simply have no coverage for this
724
+ // chain type yet (Stellar, Sui). Reporting it as an exception filled
725
+ // Sentry with a known, permanent gap, and setting `debankErr` made
726
+ // every Stellar vault throw `failure to fetch debank response`
727
+ // instead of returning the CeFi/OTC/loan allocations it did resolve.
728
+ core_1.Logger.log.warn(`getVaultAllocations:no_fetcher`, {
729
+ vault,
730
+ borrower,
731
+ chainType,
732
+ reason: `No portfolio fetcher for chain type: ${chainType}`,
733
+ });
734
+ continue;
735
+ }
736
+ try {
737
+ const debankRes = await fetcher(borrower);
738
+ if (debankRes) {
739
+ (0, debank_1.parseVaultLevelDebank)(debankRes, protocolExposure, tokenExposure, borrower, exposurePerCategory, netValue);
740
+ unfilteredTokens = debankRes?.subaccount?.tokens;
741
+ defiPerBorrower[borrower] = (0, debank_1.parseLoanLevelDebank)(debankRes);
742
+ }
743
+ else {
744
+ debankErr = true;
745
+ }
746
+ }
747
+ catch (error) {
748
+ core_1.Logger.log.error(`getVaultAllocations:${chainType}`, error, {
749
+ vault,
750
+ borrower,
751
+ });
752
+ debankErr = true;
753
+ }
754
+ }
755
+ // CeFi and OTC enrichment runs for ALL borrowers regardless of chain type.
756
+ // Previously sequential — one borrower's slow (or timing-out) request
757
+ // delayed every borrower behind it, so a vault with N borrowers serialised
758
+ // 2N requests. Now cached per borrower and run through a bounded window:
759
+ // fewer requests, and the ones that remain overlap without stampeding the
760
+ // API. Both fetchers swallow their own failures, so no result is rejected.
761
+ const subaccountPositions = await (0, concurrency_1.mapWithConcurrency)(uniqueBorrowers, SUBACCOUNT_FETCH_CONCURRENCY, async (borrower) => ({
762
+ borrower,
763
+ cefi: await fetchSubaccountCefi(borrower, options, vault),
764
+ otc: await fetchSubaccountOtc(borrower, options, vault),
765
+ }));
766
+ // Fold sequentially so the accumulated order stays deterministic and
767
+ // independent of which request finished first.
768
+ for (const { borrower, cefi, otc } of subaccountPositions) {
769
+ if (cefi) {
770
+ cefiExposure = [...cefiExposure, ...cefi];
771
+ }
772
+ if (otc) {
773
+ otcPositions[borrower] = otc;
774
+ }
775
+ }
776
+ }
777
+ const walletBalance = exposurePerCategory.wallet.reduce((sum, item) => sum + item.amount, 0);
778
+ const defiBalance = protocolExposure.reduce((sum, item) => sum + item.netUsdValue, 0);
779
+ const cefiBalance = cefiExposure.reduce((sum, item) => sum + item.value, 0);
780
+ exposurePerCategory.walletBalance = walletBalance;
781
+ exposurePerCategory.defiBalance = defiBalance;
782
+ exposurePerCategory.cefiBalance = cefiBalance;
783
+ netValue.value += cefiBalance;
784
+ if (debankErr) {
785
+ // Provider-agnostic: EVM borrowers resolve through DeBank, Solana
786
+ // borrowers through Octav.
787
+ throw new errors_1.UpshiftNetworkError('getVaultAllocations: failed to fetch a borrower portfolio from the portfolio provider (DeBank/Octav). Retry the call.', { context: { vault: String(vault) } });
788
+ }
789
+ // The idle balance sits with no operator, so the residual is taken against
790
+ // deployed AUM alone; counting idle would report it as unattributed.
791
+ const solanaAum = await solanaAumRead;
792
+ const unattributed = solanaAum
793
+ ? {
794
+ usd: solanaAum.deployedUsd - (defiBalance + walletBalance + cefiBalance),
795
+ onChainAumUsd: solanaAum.deployedUsd + solanaAum.idleUsd,
796
+ idleUsd: solanaAum.idleUsd,
797
+ }
798
+ : undefined;
799
+ return {
800
+ defi: protocolExposure,
801
+ cefi: cefiExposure,
802
+ otc: otcPositions,
803
+ tokens: untangledAssets?.length
804
+ ? [...untangledAssets, ...tokenExposure.slice(untangledTokenCount)]
805
+ : tokenExposure,
806
+ defiPerBorrower,
807
+ exposurePerCategory,
808
+ netValue: netValue.value,
809
+ ...(unattributed && { unattributed }),
810
+ // this would be the debank token response field without any parsing. Different from
811
+ // tokens. Could use a better name tho
812
+ unfilteredTokens: unfilteredTokens,
813
+ };
814
+ }
815
+ /**
816
+ * Coerce a subgraph numeric field to `bigint`, treating absent fields as zero.
817
+ *
818
+ * Why: the evm-1 and evm-2 subgraphs expose different field names on
819
+ * `WithdrawalRequested` — evm-1 emits `assets` plus explicit `year`/`month`/
820
+ * `day`, evm-2 emits `shares` and a raw `timestamp_`. Reading the wrong one
821
+ * yields `undefined`, and bare `BigInt(undefined)` throws
822
+ * `TypeError: Cannot convert undefined to a BigInt`, which aborted the whole
823
+ * redemption scan and returned an empty list to the caller (the second
824
+ * highest-volume SDK error in production).
825
+ *
826
+ * `BigInt(x) || BigInt(0)` does **not** protect against this — the throw
827
+ * happens before `||` is ever evaluated.
828
+ *
829
+ * @param value - Subgraph field: decimal string, number, bigint, `null`, or
830
+ * `undefined`.
831
+ * @returns The value as `bigint`, or `0n` when absent or non-numeric.
832
+ */
833
+ function subgraphAmountToBigInt(value) {
834
+ if (value === undefined || value === null || value === '')
835
+ return BigInt(0);
836
+ try {
837
+ return BigInt(value);
838
+ }
839
+ catch {
840
+ return BigInt(0);
841
+ }
842
+ }
843
+ /**
844
+ * Read `lagDuration()` off a vault, tolerating vaults that do not implement it.
845
+ *
846
+ * Why this exists: `getVaultAvailableRedemptions` binds every vault — v1 and
847
+ * v2 — to `ABI_LENDING_POOL_V2` because that ABI carries the superset of
848
+ * methods the scan needs. `lagDuration()` is not in that superset for every
849
+ * deployed pool. Calling it on a pool without the function reaches the
850
+ * fallback-less contract, which returns empty calldata, and ethers reports it
851
+ * as `missing revert data (action="call", data=null, …, code=CALL_EXCEPTION)`.
852
+ * That threw out of the whole redemption scan and became the single
853
+ * highest-volume error in production Sentry (~5.5k events over two days from
854
+ * three server deployments), while also returning an empty redemption list to
855
+ * every caller for the affected vaults.
856
+ *
857
+ * A missing `lagDuration()` means "this pool has no claim lag", which is what
858
+ * `0` encodes — the same value the v1 branch of the caller assumes.
859
+ *
860
+ * The empty response is **retried before it is believed**, because a provider
861
+ * that truncates an `eth_call` produces the identical shape: falling straight
862
+ * to `0` for a vault that really does have a lag would shift every computed
863
+ * claimable date. Only after the retries agree is the function treated as
864
+ * absent. A genuine revert carrying revert data still propagates, so real
865
+ * breakage stays loud.
866
+ *
867
+ * **The absence is memoized, the value is not.** Paying the retry budget (3
868
+ * `eth_call`s plus ~750ms of backoff) on every call for a vault that will never
869
+ * implement `lagDuration()` would trade an error flood for a latency and
870
+ * RPC-volume regression on exactly the vaults this path targets — a dashboard
871
+ * re-rendering redemptions pays it per render (CLAUDE.md §4.1, §4.2). Whether a
872
+ * deployed pool implements the function is fixed by its bytecode, so a
873
+ * confirmed absence is cacheable forever; the lag *value* is operator-settable
874
+ * and is deliberately never cached.
875
+ *
876
+ * Caveat on "fixed by its bytecode": an **upgradeable proxy** could gain a
877
+ * `lagDuration()` in a later implementation, and a process holding the memo
878
+ * would keep answering `0` for it. Bounded, not unbounded — `CACHE` carries a
879
+ * 24h TTL and `allowStale: true`, which serves an expired entry exactly once
880
+ * before evicting it, so the vault is re-read on the following call. Vaults
881
+ * behind an upgradeable proxy that add the function mid-process therefore
882
+ * misreport a zero lag for at most that window. Accepted deliberately: no
883
+ * deployed pool does this today, and the alternative is re-paying the retry
884
+ * budget forever for the vaults this path exists to serve.
885
+ *
886
+ * @param vaultContract - Anything exposing the vault's `lagDuration()` view;
887
+ * structurally typed so both `ethers.Contract` and the ABI-typed contract
888
+ * returned by `createContract` satisfy it.
889
+ * @param vault - Vault address, for the breadcrumb only.
890
+ * @param scope - Cache scope for the absence memo — the RPC endpoint the read
891
+ * went through. Scoping by endpoint (rather than globally by address) keeps
892
+ * the same address on two chains in separate entries.
893
+ * @returns The lag in seconds, or `0` when the vault has no `lagDuration()`.
894
+ * @throws The original error for any failure that is not an empty response.
895
+ */
896
+ async function readLagDuration(vaultContract, vault, scope) {
897
+ const absenceKey = `lag-absent-${scope}-${vault.toLowerCase()}`;
898
+ // A vault proven not to implement the function cannot start implementing it.
899
+ if (cache_1.CACHE.get(absenceKey) === true)
900
+ return 0;
901
+ try {
902
+ return Number(await (0, core_1.retryOnTransientRpc)('getVaultAvailableRedemptions:lagDuration', () => vaultContract.lagDuration(), { vault }, (error) => (0, core_1.isRetryableRpcError)(error) ||
903
+ (0, core_1.isEmptyViewResponse)(error, LAG_DURATION_SELECTOR)));
904
+ }
905
+ catch (error) {
906
+ if (!(0, core_1.isEmptyViewResponse)(error, LAG_DURATION_SELECTOR))
907
+ throw error;
908
+ // Retries agreed the response is empty — record the absence so the next
909
+ // call short-circuits instead of re-paying the full retry budget.
910
+ cache_1.CACHE.set(absenceKey, true);
911
+ core_1.Logger.log.warn('getVaultAvailableRedemptions', {
912
+ vault,
913
+ reason: 'vault does not implement lagDuration(); assuming no claim lag',
914
+ });
915
+ return 0;
916
+ }
917
+ }
918
+ /**
919
+ * `lagDuration()` — `keccak256("lagDuration()")[0..4]`. Scopes the
920
+ * empty-response tolerance in {@link readLagDuration} to exactly that call, so
921
+ * an empty response to any *other* view still surfaces as an error.
922
+ */
923
+ const LAG_DURATION_SELECTOR = '0x24e86d67';
924
+ /**
925
+ * A Solana wallet's withdrawal-queue requests on one vault, for
926
+ * `getVaultPositions` and `getVaultAvailableRedemptions`. Empty when the vault
927
+ * has no queue, the wallet has no request, or the service cannot name the queue
928
+ * program (including where the queue is not deployed); a failed read is logged
929
+ * and reads as empty, like the balance reads beside it.
930
+ *
931
+ * RPC: see `getSolanaWithdrawalRedemptions` (one `getAccountInfo` for a vault
932
+ * with no queue; up to one more `getProgramAccounts` and one
933
+ * `getMultipleAccountsInfo`).
934
+ */
935
+ async function readSolanaRedemptions(input) {
936
+ const empty = {
937
+ availableRedemptions: [],
938
+ pendingRedemptions: [],
939
+ };
940
+ const { tokenizedVault, solanaService, vaultState } = input;
941
+ if (!vaultState.shareMint)
942
+ return empty;
943
+ try {
944
+ const queueProgramId = (0, withdrawal_queue_1.resolveQueueProgramId)(solanaService);
945
+ if (!queueProgramId)
946
+ return empty;
947
+ const metadata = tokenizedVault.solana_vault_metadata;
948
+ const programId = utils_2.SolanaUtils.resolveProgramId(String(tokenizedVault.address), metadata);
949
+ return await (0, withdrawal_queue_1.getSolanaWithdrawalRedemptions)({
950
+ connection: solanaService.connection,
951
+ queueProgramId,
952
+ vaultStateAddress: metadata?.vault_state_pda
953
+ ? new web3_js_1.PublicKey(metadata.vault_state_pda)
954
+ : utils_2.SolanaUtils.deriveVaultStatePda(programId),
955
+ shareMint: new web3_js_1.PublicKey(vaultState.shareMint),
956
+ owner: new web3_js_1.PublicKey(input.wallet),
957
+ vault: tokenizedVault.address,
958
+ depositDecimals: input.depositDecimals,
959
+ });
960
+ }
961
+ catch (error) {
962
+ core_1.Logger.log.warn('readSolanaRedemptions', 'Withdrawal requests read failed', {
963
+ vault: tokenizedVault.address,
964
+ error,
965
+ });
966
+ return empty;
967
+ }
968
+ }
969
+ async function getVaultAvailableRedemptions({ vault, wallet, options, prefetchedReads, }) {
970
+ try {
971
+ // Stellar vaults don't support on-chain redemptions yet
972
+ if ((0, utils_3.isStellarAddress)(vault)) {
973
+ core_1.Logger.log.warn('getVaultAvailableRedemptions', `Available redemptions is not yet supported for Stellar vaults: ${vault}`);
974
+ return {
975
+ availableRedemptions: [],
976
+ pendingRedemptions: [],
977
+ ...(options.verbose && {
978
+ processedWithdrawals: [],
979
+ requestedWithdrawals: [],
980
+ }),
981
+ };
982
+ }
983
+ // Solana: the wallet's withdrawal-queue requests. There is no processed
984
+ // history to report, so the verbose lists are always empty.
985
+ if (utils_2.SolanaUtils.isSolanaAddress(vault)) {
986
+ const empty = {
987
+ availableRedemptions: [],
988
+ pendingRedemptions: [],
989
+ ...(options.verbose && {
990
+ processedWithdrawals: [],
991
+ requestedWithdrawals: [],
992
+ }),
993
+ };
994
+ const solanaService = options.solanaService;
995
+ if (!wallet || !utils_2.SolanaUtils.isSolanaAddress(wallet) || !solanaService) {
996
+ return empty;
997
+ }
998
+ const solanaVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
999
+ if (!solanaVault)
1000
+ return empty;
1001
+ const vaultStateRes = await solanaService.getVaultState(utils_2.SolanaUtils.resolveProgramId(String(solanaVault.address), solanaVault.solana_vault_metadata), SolanaConstants.vaultIdl, solanaVault.solana_vault_metadata?.vault_state_pda ?? undefined);
1002
+ const vaultState = vaultStateRes?.vaultState;
1003
+ if (!vaultState)
1004
+ return empty;
1005
+ return {
1006
+ ...empty,
1007
+ ...(await readSolanaRedemptions({
1008
+ tokenizedVault: solanaVault,
1009
+ wallet,
1010
+ solanaService,
1011
+ vaultState,
1012
+ depositDecimals: vaultStateRes?.depositMintDecimals ??
1013
+ solanaVault.solana_vault_metadata?.deposit_token_decimals ??
1014
+ SolanaConstants.fallbackDecimals,
1015
+ })),
1016
+ };
1017
+ }
1018
+ // setup
1019
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
1020
+ const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
1021
+ if (!tokenizedVault) {
1022
+ return {
1023
+ ...{
1024
+ availableRedemptions: [],
1025
+ pendingRedemptions: [],
1026
+ },
1027
+ ...(options.verbose && {
1028
+ processedWithdrawals: [],
1029
+ requestedWithdrawals: [],
1030
+ }),
1031
+ };
1032
+ }
1033
+ // version split logic
1034
+ const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
1035
+ let decimals;
1036
+ // Use ABI_LENDING_POOL_V2 for both versions as it has the methods we need
1037
+ const vaultContract = (0, core_1.createContract)({
1038
+ address: vault,
1039
+ abi: abis_1.ABI_LENDING_POOL_V2,
1040
+ provider,
1041
+ });
1042
+ if (version === 'evm-2') {
1043
+ decimals = await (0, core_1.getDecimals)(provider, await (0, core_1.getReceiptTokenAddress)(provider, vault), false);
1044
+ }
1045
+ else {
1046
+ decimals = await (0, core_1.getDecimals)(provider, vault);
1047
+ }
1048
+ const lagDuration = typeof prefetchedReads?.lagDuration === 'number'
1049
+ ? prefetchedReads.lagDuration
1050
+ : await readLagDuration(vaultContract, vault, options.rpcUrl);
1051
+ // With a wallet, every row of another receiver is discarded below, so
1052
+ // filter server-side to cut the Goldsky payload (~0.9 MB on busy vaults,
1053
+ // AUGUST-8317). `verbose` callers get the raw lists back, so they keep the
1054
+ // unfiltered read.
1055
+ const receiverFilter = wallet && (0, ethers_1.isAddress)(wallet) && !options.verbose ? wallet : undefined;
1056
+ const { withdrawalRequesteds, withdrawalProcesseds } = await (0, subgraph_1.getSubgraphAllWithdrawals)(vault, provider, {
1057
+ receiver: receiverFilter,
1058
+ });
1059
+ // format
1060
+ const availableRedemptions = [];
1061
+ const pendingRedemptions = [];
1062
+ for (const ev of withdrawalRequesteds) {
1063
+ // ensure event is defined
1064
+ if (!ev || typeof ev !== 'object') {
1065
+ core_1.Logger.log.warn('getVaultAvailableRedemptions', `Skipping invalid event: ${ev}`);
1066
+ continue;
1067
+ }
1068
+ let month = 0;
1069
+ let day = 0;
1070
+ let year = 0;
1071
+ if (version === 'evm-2') {
1072
+ // Use UTC-based computeClaimableDate to mirror contract logic exactly
1073
+ const claimable = (0, date_utils_1.computeClaimableDate)(Number(ev.timestamp_), lagDuration);
1074
+ year = claimable.year;
1075
+ month = claimable.month;
1076
+ day = claimable.day;
1077
+ }
1078
+ // Validate required event properties. evm-2 events carry a raw
1079
+ // `timestamp_` from which the date was just computed — a malformed
1080
+ // timestamp yields NaN date parts, which would otherwise flow into the
1081
+ // date keys below and never match anything. evm-1 events carry the
1082
+ // date explicitly.
1083
+ const missingRequired = version === 'evm-2'
1084
+ ? !year || !month || !day || !ev.receiverAddr
1085
+ : !ev.year || !ev.month || !ev.day || !ev.receiverAddr;
1086
+ if (missingRequired) {
1087
+ core_1.Logger.log.warn('getVaultAvailableRedemptions', `Skipping event with missing required properties: ${ev}`);
1088
+ continue;
1089
+ }
1090
+ const fullDate = version === 'evm-2'
1091
+ ? new Date(Date.UTC(year, month - 1, day))
1092
+ : new Date(Number(ev.year), Number(ev.month) - 1, Number(ev.day));
1093
+ // Match request to processed event using deterministic UTC date key
1094
+ const requestDateKey = version === 'evm-2'
1095
+ ? (0, date_utils_1.formatDateKey)(year, month, day)
1096
+ : (0, date_utils_1.formatDateKey)(Number(ev.year), Number(ev.month), Number(ev.day));
1097
+ // NOTE: evm-2 subgraph does not emit WithdrawalProcessed events, so matching
1098
+ // relies on timestamp-based date comparison for evm-2 (deterministic for evm-1).
1099
+ const foundRedemptionAgainstClaim = withdrawalProcesseds.find((h) => {
1100
+ const proc = h;
1101
+ if (proc.receiverAddr !== ev.receiverAddr)
1102
+ return false;
1103
+ if (version === 'evm-2') {
1104
+ const procDate = new Date(Number(proc.timestamp_) * 1000);
1105
+ const procKey = (0, date_utils_1.formatDateKey)(procDate.getUTCFullYear(), procDate.getUTCMonth() + 1, procDate.getUTCDate());
1106
+ return procKey === requestDateKey;
1107
+ }
1108
+ const procDate = new Date(proc.processedOn);
1109
+ const procKey = (0, date_utils_1.formatDateKey)(procDate.getUTCFullYear(), procDate.getUTCMonth() + 1, procDate.getUTCDate());
1110
+ return procKey === requestDateKey;
1111
+ });
1112
+ if (wallet && (0, ethers_1.isAddress)(wallet)) {
1113
+ // check if connected users address
1114
+ if (ev?.receiverAddr?.toLowerCase() === wallet.toLowerCase()) {
1115
+ const alreadyRedeemed = version === 'evm-2'
1116
+ ? withdrawalProcesseds.find((red) => {
1117
+ // Match by receiver + UTC date key (deterministic)
1118
+ if (red.receiverAddr.toLowerCase() !== wallet.toLowerCase()) {
1119
+ return false;
1120
+ }
1121
+ const redDate = new Date(Number(red.timestamp_) * 1000);
1122
+ const redKey = (0, date_utils_1.formatDateKey)(redDate.getUTCFullYear(), redDate.getUTCMonth() + 1, redDate.getUTCDate());
1123
+ return redKey === requestDateKey;
1124
+ })
1125
+ : availableRedemptions.find((red) => subgraphAmountToBigInt(red.day.raw) ===
1126
+ subgraphAmountToBigInt(ev.day) &&
1127
+ subgraphAmountToBigInt(red.month.raw) ===
1128
+ subgraphAmountToBigInt(ev.month) &&
1129
+ subgraphAmountToBigInt(red.year.raw) ===
1130
+ subgraphAmountToBigInt(ev.year));
1131
+ if (!(foundRedemptionAgainstClaim && alreadyRedeemed)) {
1132
+ // double check if user has already claimed
1133
+ try {
1134
+ switch (version) {
1135
+ case 'evm-2': {
1136
+ // this is to avoid edge case where old withdrawal A is already redeemed but we are now looking at withdrawal B that is not yet pending
1137
+ if (alreadyRedeemed) {
1138
+ const index = withdrawalProcesseds.findIndex((item) => item.id === alreadyRedeemed.id);
1139
+ if (index !== -1) {
1140
+ withdrawalProcesseds.splice(index, 1);
1141
+ }
1142
+ }
1143
+ if (!alreadyRedeemed) {
1144
+ // Recompute claimable date (includes lag + 300s manipulation window) to determine if claimable now
1145
+ const claimable = (0, date_utils_1.computeClaimableDate)(Number(ev.timestamp_), lagDuration);
1146
+ const pendingCondition = (0, date_utils_1.isClaimableNow)(claimable.epoch, Math.floor(Date.now() / 1000));
1147
+ if (pendingCondition) {
1148
+ // Has passed lag duration - check if claim is not settled before adding to pending
1149
+ try {
1150
+ const burnableAmount = (await vaultContract?.getBurnableAmountByReceiver?.(BigInt(year), BigInt(month), BigInt(day), (0, ethers_1.getAddress)(ev.receiverAddr))) || BigInt(0);
1151
+ const claimAmount = subgraphAmountToBigInt(ev.shares);
1152
+ // If burnableAmount >= claimAmount, claim hasn't been settled by processAllClaims
1153
+ // Only add to pending if it's still unsettled
1154
+ if (burnableAmount >= claimAmount) {
1155
+ pendingRedemptions.push({
1156
+ id: ev.transactionHash_ || ev.id,
1157
+ hash: ev.transactionHash_ || ev.id,
1158
+ timestamp: Number(ev.timestamp_),
1159
+ receiver: ev.receiverAddr,
1160
+ day: (0, core_1.toNormalizedBn)(day, 0),
1161
+ month: (0, core_1.toNormalizedBn)(month, 0),
1162
+ year: (0, core_1.toNormalizedBn)(year, 0),
1163
+ amount: (0, core_1.toNormalizedBn)(subgraphAmountToBigInt(ev.shares), decimals),
1164
+ date: fullDate,
1165
+ vault,
1166
+ status: types_1.VaultRedemptionStatus.READY_TO_CLAIM,
1167
+ });
1168
+ }
1169
+ }
1170
+ catch (burnableError) {
1171
+ core_1.Logger.log.warn('getVaultAvailableRedemptions', `Failed to check burnable amount for pending redemption: ${burnableError}`);
1172
+ // If check fails, don't add to pending to be safe
1173
+ }
1174
+ }
1175
+ else {
1176
+ // Hasn't passed lag duration - add to available redemptions only
1177
+ availableRedemptions.push({
1178
+ id: ev.transactionHash_ || ev.id,
1179
+ hash: ev.transactionHash_ || ev.id,
1180
+ timestamp: Number(ev.timestamp_),
1181
+ receiver: ev.receiverAddr,
1182
+ day: (0, core_1.toNormalizedBn)(day, 0),
1183
+ month: (0, core_1.toNormalizedBn)(month, 0),
1184
+ year: (0, core_1.toNormalizedBn)(year, 0),
1185
+ amount: (0, core_1.toNormalizedBn)(subgraphAmountToBigInt(ev.shares), decimals),
1186
+ date: fullDate,
1187
+ vault,
1188
+ status: types_1.VaultRedemptionStatus.AWAITING_COOLDOWN,
1189
+ });
1190
+ }
1191
+ }
1192
+ break;
1193
+ }
1194
+ default: {
1195
+ // Safe to read the raw subgraph dates here: the validation
1196
+ // block above `continue`s for any non-evm-2 event missing
1197
+ // `year`/`month`/`day`, so this path is only reached with all
1198
+ // three present. The coercion below therefore never
1199
+ // substitutes 0 for a real date.
1200
+ const trueClaimableAmount = await vaultContract?.getClaimableAmountByReceiver?.(subgraphAmountToBigInt(ev.year), subgraphAmountToBigInt(ev.month), subgraphAmountToBigInt(ev.day), (0, ethers_1.getAddress)(wallet));
1201
+ if (trueClaimableAmount > BigInt(0)) {
1202
+ // Use computeClaimableDate for UTC-correct date with lag
1203
+ const v1Claimable = (0, date_utils_1.computeClaimableDate)(Number(ev.timestamp_), lagDuration);
1204
+ const pendingCondition = (0, date_utils_1.isClaimableNow)(v1Claimable.epoch, Math.floor(Date.now() / 1000));
1205
+ if (pendingCondition) {
1206
+ // Has passed lag duration - check if claim is not settled before adding to pending
1207
+ try {
1208
+ year = v1Claimable.year;
1209
+ month = v1Claimable.month;
1210
+ day = v1Claimable.day;
1211
+ const burnableAmount = (await vaultContract?.getBurnableAmountByReceiver?.(BigInt(year), BigInt(month), BigInt(day), (0, ethers_1.getAddress)(ev.receiverAddr))) || BigInt(0);
1212
+ const claimAmount = trueClaimableAmount || BigInt(0);
1213
+ // If burnableAmount >= claimAmount, claim hasn't been settled by processAllClaims
1214
+ // Only add to pending if it's still unsettled
1215
+ if (burnableAmount >= claimAmount) {
1216
+ pendingRedemptions.push({
1217
+ id: ev.transactionHash_ || ev.id,
1218
+ hash: ev.transactionHash_ || ev.id,
1219
+ timestamp: Number(ev.timestamp_),
1220
+ receiver: ev.receiverAddr,
1221
+ day: (0, core_1.toNormalizedBn)(ev.day, 0),
1222
+ month: (0, core_1.toNormalizedBn)(ev.month, 0),
1223
+ year: (0, core_1.toNormalizedBn)(ev.year, 0),
1224
+ amount: (0, core_1.toNormalizedBn)(trueClaimableAmount || BigInt(0), decimals),
1225
+ date: fullDate,
1226
+ vault,
1227
+ status: types_1.VaultRedemptionStatus.READY_TO_CLAIM,
1228
+ });
1229
+ }
1230
+ }
1231
+ catch (burnableError) {
1232
+ core_1.Logger.log.warn('getVaultAvailableRedemptions', `Failed to check burnable amount for pending redemption: ${burnableError}`);
1233
+ // If check fails, don't add to pending to be safe
1234
+ }
1235
+ }
1236
+ else {
1237
+ // Hasn't passed lag duration - add to available redemptions only
1238
+ availableRedemptions.push({
1239
+ id: ev.transactionHash_ || ev.id,
1240
+ hash: ev.transactionHash_ || ev.id,
1241
+ timestamp: Number(ev.timestamp_),
1242
+ receiver: ev.receiverAddr,
1243
+ day: (0, core_1.toNormalizedBn)(ev.day, 0),
1244
+ month: (0, core_1.toNormalizedBn)(ev.month, 0),
1245
+ year: (0, core_1.toNormalizedBn)(ev.year, 0),
1246
+ amount: (0, core_1.toNormalizedBn)(trueClaimableAmount, decimals),
1247
+ date: fullDate,
1248
+ vault,
1249
+ status: types_1.VaultRedemptionStatus.AWAITING_COOLDOWN,
1250
+ });
1251
+ }
1252
+ }
1253
+ break;
1254
+ }
1255
+ }
1256
+ }
1257
+ catch (contractError) {
1258
+ core_1.Logger.log.error('getVaultAvailableRedemptions', `Contract call failed: ${contractError}`);
1259
+ }
1260
+ }
1261
+ }
1262
+ }
1263
+ else {
1264
+ if (foundRedemptionAgainstClaim) {
1265
+ availableRedemptions.push({
1266
+ id: ev.transactionHash_ || ev.id,
1267
+ hash: ev.transactionHash_ || ev.id,
1268
+ timestamp: Number(ev.timestamp_),
1269
+ receiver: ev.receiverAddr,
1270
+ day: (0, core_1.toNormalizedBn)(ev.day, 0),
1271
+ month: (0, core_1.toNormalizedBn)(ev.month, 0),
1272
+ year: (0, core_1.toNormalizedBn)(ev.year, 0),
1273
+ amount: (0, core_1.toNormalizedBn)(subgraphAmountToBigInt(ev.assets), decimals),
1274
+ date: fullDate,
1275
+ vault,
1276
+ status: types_1.VaultRedemptionStatus.AWAITING_COOLDOWN,
1277
+ });
1278
+ }
1279
+ }
1280
+ }
1281
+ return {
1282
+ ...{
1283
+ availableRedemptions,
1284
+ pendingRedemptions,
1285
+ },
1286
+ ...(options.verbose && {
1287
+ processedWithdrawals: withdrawalProcesseds,
1288
+ requestedWithdrawals: withdrawalRequesteds,
1289
+ }),
1290
+ };
1291
+ }
1292
+ catch (e) {
1293
+ core_1.Logger.log.error('getVaultAvailableRedemptions', e, { vault, wallet });
1294
+ return {
1295
+ ...{
1296
+ availableRedemptions: [],
1297
+ pendingRedemptions: [],
1298
+ },
1299
+ ...(options.verbose && {
1300
+ processedWithdrawals: [],
1301
+ requestedWithdrawals: [],
1302
+ }),
1303
+ };
1304
+ }
1305
+ }
1306
+ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, options, }) {
1307
+ try {
1308
+ // Stellar vaults: no on-chain redemption history yet
1309
+ if ((0, utils_3.isStellarAddress)(vault)) {
1310
+ core_1.Logger.log.warn('getVaultRedemptionHistory', `Redemption history is not yet supported for Stellar vaults: ${vault}`);
1311
+ return [];
1312
+ }
1313
+ // setup
1314
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
1315
+ const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
1316
+ if (!tokenizedVault) {
1317
+ core_1.Logger.log.warn('getVaultRedemptionHistory', `Vault not found in backend: ${vault}`);
1318
+ return [];
1319
+ }
1320
+ const chainId = tokenizedVault.chain;
1321
+ const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
1322
+ const isV2 = version === 'evm-2';
1323
+ // Use correct ABI based on vault version — different WithdrawalProcessed event signatures
1324
+ const vaultAbi = isV2 ? TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOL_V2;
1325
+ const poolContract = (0, core_1.createContract)({
1326
+ address: vault,
1327
+ abi: vaultAbi,
1328
+ provider,
1329
+ });
1330
+ // query getLogs
1331
+ const currentBlock = await provider.getBlockNumber();
1332
+ const blockSkip = (0, core_1.determineBlockSkipInternal)(chainId);
1333
+ const cutoffBlock = currentBlock - (lookbackBlocks ?? (0, core_1.determineBlockCutoff)(chainId));
1334
+ // Max concurrent `eth_getLogs` requests per batch. Kept well under the
1335
+ // per-second cap of the smallest provider plan we run against (QuickNode:
1336
+ // 50/s) because this scan is not the only caller on the connection — a
1337
+ // page rendering several vaults runs several of these concurrently. At 20
1338
+ // the batches reliably tripped `-32007 50/second request limit reached`,
1339
+ // which the retry below now absorbs and this ceiling mostly avoids.
1340
+ const BATCH_SIZE = 8;
1341
+ core_1.Logger.log.info('getVaultRedemptionHistory', {
1342
+ vault,
1343
+ version,
1344
+ isV2,
1345
+ chainId,
1346
+ currentBlock,
1347
+ cutoffBlock,
1348
+ blockSkip,
1349
+ totalRangeBlocks: currentBlock - cutoffBlock,
1350
+ });
1351
+ // Build all query ranges
1352
+ const ranges = [];
1353
+ let endBlock = currentBlock;
1354
+ while (endBlock >= cutoffBlock) {
1355
+ const fromBlock = Math.max(endBlock - blockSkip, cutoffBlock);
1356
+ ranges.push({ from: fromBlock, to: endBlock });
1357
+ endBlock = fromBlock - 1;
1358
+ }
1359
+ core_1.Logger.log.info('getVaultRedemptionHistory', {
1360
+ totalBatches: Math.ceil(ranges.length / BATCH_SIZE),
1361
+ totalChunks: ranges.length,
1362
+ firstRange: ranges[0],
1363
+ lastRange: ranges[ranges.length - 1],
1364
+ });
1365
+ // Execute in batches to avoid RPC rate limits on high-block-count chains.
1366
+ // Use allSettled + explicit throw so a chunk failure surfaces to the caller
1367
+ // instead of silently returning an empty result (which used to corrupt
1368
+ // `getWithdrawalRequestsWithStatus` into reporting `ready_to_claim` for
1369
+ // requests that were already processed).
1370
+ const logs = [];
1371
+ for (let i = 0; i < ranges.length; i += BATCH_SIZE) {
1372
+ const batch = ranges.slice(i, i + BATCH_SIZE);
1373
+ const batchResults = await Promise.allSettled(batch.map((r) =>
1374
+ // `eth_getLogs` over a fixed block range is idempotent, so a chunk
1375
+ // rejected by a rate limiter or a flaky socket is safe to repeat.
1376
+ // Without this a single `-32007` burst aborted the whole scan and
1377
+ // the caller lost the redemption history entirely — the same failure
1378
+ // mode the throw below was added to make visible.
1379
+ (0, core_1.retryOnTransientRpc)('getVaultRedemptionHistory:chunk', () => poolContract.queryFilter('WithdrawalProcessed', BigInt(r.from), BigInt(r.to)), { vault, fromBlock: r.from, toBlock: r.to })));
1380
+ const failed = batchResults.filter((r) => r.status === 'rejected');
1381
+ if (failed.length > 0) {
1382
+ const firstReason = failed[0].reason;
1383
+ const firstMessage = firstReason instanceof Error
1384
+ ? firstReason.message
1385
+ : String(firstReason);
1386
+ // Breadcrumb only — the throw below is caught and logged by this
1387
+ // function's own catch block, so capturing here duplicated every
1388
+ // failure into a second Sentry issue ("log fetch chunk(s) failed").
1389
+ core_1.Logger.log.warn('getVaultRedemptionHistory', {
1390
+ reason: 'log fetch chunk(s) failed',
1391
+ failedChunks: failed.length,
1392
+ totalChunks: batch.length,
1393
+ firstReason: firstMessage,
1394
+ });
1395
+ throw new Error(`Failed to fetch WithdrawalProcessed logs (${failed.length}/${batch.length} chunk(s) rejected): ${firstMessage}`);
1396
+ }
1397
+ const batchLogs = batchResults.flatMap((r) => r.status === 'fulfilled' ? r.value : []);
1398
+ logs.push(...batchLogs);
1399
+ }
1400
+ // evm-1: WithdrawalProcessed(uint256 assetsAmount, uint256 processedOn, address receiverAddr, uint256 requestedOn)
1401
+ // evm-2: WithdrawalProcessed(uint256 assetsAmount, address indexed receiverAddr)
1402
+ const iface = new ethers_1.ethers.Interface(isV2
1403
+ ? [
1404
+ 'event WithdrawalProcessed(uint256 assetsAmount, address indexed receiverAddr)',
1405
+ ]
1406
+ : [
1407
+ 'event WithdrawalProcessed(uint256 assetsAmount, uint256 processedOn, address receiverAddr, uint256 requestedOn)',
1408
+ ]);
1409
+ // get pool decimals
1410
+ const decimals = await (0, core_1.getDecimals)(provider, vault);
1411
+ // For evm-2 events (no timestamps in event), fetch block timestamps
1412
+ let blockTimestamps;
1413
+ if (isV2 && logs.length > 0) {
1414
+ const uniqueBlocks = [...new Set(logs.map((l) => l.blockNumber))];
1415
+ const blockResults = await Promise.all(uniqueBlocks.map((bn) => provider.getBlock(bn)));
1416
+ const timestamps = new Map();
1417
+ blockResults.forEach((block) => {
1418
+ if (block)
1419
+ timestamps.set(block.number, block.timestamp);
1420
+ });
1421
+ blockTimestamps = timestamps;
1422
+ }
1423
+ // format — iterate logs directly to preserve transactionHash
1424
+ const redemptions = [];
1425
+ logs.forEach((log) => {
1426
+ const ev = iface.parseLog(log);
1427
+ if (!ev?.args)
1428
+ return;
1429
+ let item;
1430
+ if (isV2) {
1431
+ const [assetsAmount, receiverAddr] = ev.args;
1432
+ const blockTs = blockTimestamps?.get(log.blockNumber) ?? 0;
1433
+ const blockDate = new Date(blockTs * 1000);
1434
+ item = {
1435
+ receiver: receiverAddr,
1436
+ amount: (0, core_1.toNormalizedBn)(assetsAmount, decimals),
1437
+ processed: blockDate,
1438
+ requested: new Date(0), // evm-2 event has no requestedOn field; epoch 0 is a sentinel for "unknown"
1439
+ vault,
1440
+ transactionHash: log.transactionHash,
1441
+ };
1442
+ }
1443
+ else {
1444
+ const [assetsAmount, processedOn, receiverAddr, requestedOn] = ev.args;
1445
+ item = {
1446
+ receiver: receiverAddr,
1447
+ amount: (0, core_1.toNormalizedBn)(assetsAmount, decimals),
1448
+ processed: new Date(Number(processedOn) * 1000),
1449
+ requested: new Date(Number(requestedOn) * 1000),
1450
+ vault,
1451
+ transactionHash: log.transactionHash,
1452
+ };
1453
+ }
1454
+ if (wallet) {
1455
+ if (item.receiver?.toLowerCase() === wallet.toLowerCase()) {
1456
+ redemptions.push(item);
1457
+ }
1458
+ }
1459
+ else {
1460
+ redemptions.push(item);
1461
+ }
1462
+ });
1463
+ core_1.Logger.log.info('getVaultRedemptionHistory', redemptions.slice(0, 1));
1464
+ return redemptions;
1465
+ }
1466
+ catch (e) {
1467
+ // Log loudly and re-throw. Previously this caught all errors and returned
1468
+ // [], which silently corrupted callers (notably getWithdrawalRequestsWithStatus,
1469
+ // which would then report `ready_to_claim` for already-processed requests).
1470
+ // Returning [] is reserved for the explicit early returns inside the try
1471
+ // (unsupported chains, vault not in backend) — not for unexpected failures.
1472
+ core_1.Logger.log.error('getVaultRedemptionHistory', e, { vault, wallet });
1473
+ throw e;
1474
+ }
1475
+ }
1476
+ // Moved to core/helpers/swap-router.ts to break the adapters/evm ↔
1477
+ // modules/vaults/getters import cycle (CLAUDE.md §9); re-exported here for
1478
+ // back-compat with existing importers.
1479
+ var swap_router_1 = require("../../core/helpers/swap-router");
1480
+ Object.defineProperty(exports, "getSwapRouterDepositResult", { enumerable: true, get: function () { return swap_router_1.getSwapRouterDepositResult; } });
1481
+ /**
1482
+ * Vault Positions
1483
+ */
1484
+ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, options, prefetchedReads, }) {
1485
+ try {
1486
+ const tokenizedVaults = await (0, core_1.fetchTokenizedVault)(vault ? vault : undefined, options?.headers, false, false);
1487
+ if (!tokenizedVaults || tokenizedVaults.length === 0) {
1488
+ return [];
1489
+ }
1490
+ // check if there are redemptions available or if you stake in this pool
1491
+ function renderStatus(_redemptions, _balance) {
1492
+ if (_redemptions?.length)
1493
+ return 'REDEEM';
1494
+ if (BigInt(_balance.raw) > BigInt(0))
1495
+ return 'STAKED';
1496
+ return 'PENDING';
1497
+ }
1498
+ const promises = await Promise.all(tokenizedVaults.map(async (v) => {
1499
+ try {
1500
+ // @solana: skip if not solana address
1501
+ if (utils_2.SolanaUtils.isSolanaAddress(v.address)) {
1502
+ // Default decimals: backend metadata, then SDK fallback. The mint's
1503
+ // own decimals (read below) override this once we have a real
1504
+ // tokenAmount response.
1505
+ const fallbackShareDecimals = v.solana_vault_metadata?.deposit_token_decimals ??
1506
+ SolanaConstants.fallbackDecimals;
1507
+ let balance = (0, core_1.toNormalizedBn)(0, fallbackShareDecimals);
1508
+ let redemptions = {
1509
+ availableRedemptions: [],
1510
+ pendingRedemptions: [],
1511
+ };
1512
+ // Defensive: without a Solana adapter we can't fetch on-chain
1513
+ // state. The outer filter in modules/vaults/main.ts already gates
1514
+ // on `this.solanaService`, so this is a belt-and-suspenders guard
1515
+ // for any external caller that constructs `options` by hand.
1516
+ if (utils_2.SolanaUtils.isSolanaAddress(solanaWallet) &&
1517
+ options.solanaService) {
1518
+ const vaultProgramId = utils_2.SolanaUtils.resolveProgramId(String(v.address), v.solana_vault_metadata);
1519
+ const vaultStateRes = await options.solanaService.getVaultState(vaultProgramId, SolanaConstants.vaultIdl, v.solana_vault_metadata?.vault_state_pda ?? undefined);
1520
+ const vaultState = vaultStateRes?.vaultState;
1521
+ const { shareMint } = vaultState ?? { shareMint: null };
1522
+ if (shareMint) {
1523
+ // Always use the raw helper — it returns the on-chain u64
1524
+ // string and the mint's true decimals. The legacy `uiAmount`
1525
+ // (JS number) path lost precision and silently encoded `.raw`
1526
+ // against 18 decimals when fed back through `toNormalizedBn`
1527
+ // without a decimals argument, which broke every downstream
1528
+ // BigInt consumer (redemptions, max actions, allowances).
1529
+ const raw = await options.solanaService.fetchUserShareBalanceRaw(solanaWallet, shareMint);
1530
+ const decimals = raw.decimals ?? fallbackShareDecimals;
1531
+ balance = (0, core_1.toNormalizedBn)(BigInt(raw.amount), decimals);
1532
+ }
1533
+ if (vaultState) {
1534
+ redemptions = await readSolanaRedemptions({
1535
+ tokenizedVault: v,
1536
+ wallet: solanaWallet,
1537
+ solanaService: options.solanaService,
1538
+ vaultState,
1539
+ depositDecimals: vaultStateRes?.depositMintDecimals ?? fallbackShareDecimals,
1540
+ });
1541
+ }
1542
+ }
1543
+ return {
1544
+ // Mirror the Stellar branch — return the backend's `v.address`,
1545
+ // not the outer `vault` parameter (which can be undefined for
1546
+ // multi-vault calls). No `as IAddress` cast: Solana base58 is
1547
+ // not 0x-hex; the IAddress typing across non-EVM positions is a
1548
+ // known wart documented in IVaultPosition.
1549
+ vault: v.address,
1550
+ // Any queued request, waiting or claimable, marks the position
1551
+ // REDEEM; the shares it escrows are no longer in the wallet.
1552
+ status: renderStatus([
1553
+ ...redemptions.availableRedemptions,
1554
+ ...redemptions.pendingRedemptions,
1555
+ ], balance),
1556
+ availableRedemptions: redemptions.availableRedemptions,
1557
+ pendingRedemptions: redemptions.pendingRedemptions,
1558
+ redeemable: balance,
1559
+ walletBalance: balance,
1560
+ };
1561
+ }
1562
+ if ((0, utils_3.isStellarAddress)(v.address)) {
1563
+ let balance = (0, core_1.toNormalizedBn)(0);
1564
+ if (stellarWallet && (0, utils_3.isStellarAddress)(stellarWallet)) {
1565
+ const stellarNetwork = v.stellar_vault_metadata?.network_name ?? 'mainnet';
1566
+ // Only use the configured RPC override for its own network.
1567
+ const stellarOverrideRpcUrl = options.stellarRpc?.network === stellarNetwork
1568
+ ? options.stellarRpc.rpcUrl
1569
+ : undefined;
1570
+ const position = await StellarGetters.getStellarUserPosition(v.address, stellarWallet, stellarNetwork, stellarOverrideRpcUrl);
1571
+ if (position && !position.decimalsFromFallback) {
1572
+ balance = (0, core_1.toNormalizedBn)(BigInt(position.shares), position.decimals);
1573
+ }
1574
+ else if (position) {
1575
+ // decimals() read failed: trusting the fallback (7) would
1576
+ // mis-scale the balance for ERC4626 offset vaults (AUGUST-6381).
1577
+ // Show zero rather than a confidently-wrong value.
1578
+ core_1.Logger.log.warn('getVaultPositions', 'Stellar decimals() unresolved — refusing to scale balance with fallback decimals (showing zero)', { vault: v.address, wallet: stellarWallet });
1579
+ }
1580
+ else {
1581
+ core_1.Logger.log.warn('getVaultPositions', 'Stellar position query failed — showing zero balance', { vault: v.address, wallet: stellarWallet });
1582
+ }
1583
+ }
1584
+ else if (stellarWallet) {
1585
+ core_1.Logger.log.warn('getVaultPositions', 'Invalid stellarWallet address format', { vault: v.address, stellarWallet });
1586
+ }
1587
+ // Stellar vaults have no on-chain redemption queue yet — redeemable is always 0
1588
+ return {
1589
+ vault: v.address,
1590
+ status: renderStatus([], balance),
1591
+ availableRedemptions: [],
1592
+ pendingRedemptions: [],
1593
+ redeemable: (0, core_1.toNormalizedBn)(0),
1594
+ walletBalance: balance,
1595
+ };
1596
+ }
1597
+ // create provider
1598
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
1599
+ // version split logic
1600
+ let decimals;
1601
+ const version = (0, core_1.getVaultVersionV2)(v);
1602
+ // Use ABI_LENDING_POOL_V2 as the common ABI for vault operations
1603
+ const vaultContract = (0, core_1.createContract)({
1604
+ provider,
1605
+ abi: abis_1.ABI_LENDING_POOL_V2,
1606
+ address: v.address,
1607
+ });
1608
+ // Batched reads only apply to the vault this call was made for —
1609
+ // guard against a multi-row backend response reusing them.
1610
+ const prefetched = v.address?.toLowerCase?.() === String(vault).toLowerCase()
1611
+ ? prefetchedReads
1612
+ : undefined;
1613
+ // get vault metadata and balance based on version
1614
+ let bal = BigInt(0);
1615
+ if (version === 'evm-2') {
1616
+ const receiptAddress = await (0, core_1.getReceiptTokenAddress)(provider, v.address);
1617
+ // `getReceiptTokenAddress` never throws: after its bounded retries
1618
+ // it resolves `undefined`. Passing that on used to crash with
1619
+ // `Cannot read properties of undefined (reading 'balanceOf')`
1620
+ // (Sentry AUGUST-JS-SDK-V2-ZF, AUGUST-8347), which hid the real
1621
+ // cause — a failed RPC read. Throw a typed error instead; the
1622
+ // per-vault catch below turns it into a `readFailed` row.
1623
+ if (!receiptAddress) {
1624
+ throw new errors_1.UpshiftNetworkError('getVaultPositions: receipt token address unresolved for evm-2 vault', { context: { vault: v.address, chainId: v.chain } });
1625
+ }
1626
+ decimals = await (0, core_1.getDecimals)(provider, receiptAddress, false);
1627
+ if (wallet && (0, ethers_1.isAddress)(wallet)) {
1628
+ if (typeof prefetched?.balance === 'bigint') {
1629
+ bal = prefetched.balance;
1630
+ }
1631
+ else {
1632
+ const receiptContract = (0, core_1.createContract)({
1633
+ provider,
1634
+ abi: TokenizedVaultV2Receipt_1.ABI_TOKENIZED_VAULT_V2_RECEIPT,
1635
+ address: receiptAddress,
1636
+ });
1637
+ bal = await receiptContract.balanceOf(wallet);
1638
+ }
1639
+ }
1640
+ }
1641
+ else {
1642
+ decimals = await (0, core_1.getDecimals)(provider, v.address);
1643
+ if (wallet && (0, ethers_1.isAddress)(wallet)) {
1644
+ bal =
1645
+ typeof prefetched?.balance === 'bigint'
1646
+ ? prefetched.balance
1647
+ : await vaultContract.balanceOf(wallet);
1648
+ }
1649
+ }
1650
+ const balance = (0, core_1.toNormalizedBn)(bal, decimals);
1651
+ // Redemption requests are keyed by receiver, so without an EVM
1652
+ // wallet there is nothing of the caller's to find. Skip the read
1653
+ // rather than delegate: the wallet-less path of
1654
+ // `getVaultAvailableRedemptions` lists EVERY receiver's processed
1655
+ // requests for the vault, which in a positions read surfaced other
1656
+ // users' withdrawals as the caller's and flipped the status to
1657
+ // REDEEM for every EVM vault in a Stellar/Solana-only session
1658
+ // (AUGUST-7938). Skipping also saves one subgraph query per EVM
1659
+ // vault for those callers.
1660
+ const { availableRedemptions, pendingRedemptions } = wallet && (0, ethers_1.isAddress)(wallet)
1661
+ ? await getVaultAvailableRedemptions({
1662
+ vault: v.address,
1663
+ wallet,
1664
+ options,
1665
+ prefetchedReads: prefetched,
1666
+ })
1667
+ : {
1668
+ availableRedemptions: [],
1669
+ pendingRedemptions: [],
1670
+ };
1671
+ // get aggregate redemption amount
1672
+ const aggregateAvailableRedemptions = availableRedemptions.reduce((acc, curr) => acc + BigInt(curr.amount.raw), BigInt(0));
1673
+ // return obj
1674
+ return {
1675
+ vault: v.address,
1676
+ status: renderStatus(availableRedemptions, balance),
1677
+ availableRedemptions,
1678
+ pendingRedemptions,
1679
+ redeemable: (0, core_1.toNormalizedBn)(aggregateAvailableRedemptions, decimals),
1680
+ walletBalance: balance,
1681
+ };
1682
+ }
1683
+ catch (vaultError) {
1684
+ // Pass the error object (not a string) so typed errors keep
1685
+ // their code/context/correlationId through to the logger.
1686
+ core_1.Logger.log.error(`getVaultPositions`, vaultError, {
1687
+ vault: v.address,
1688
+ chainId: v.chain,
1689
+ });
1690
+ // Keep the zero `PENDING` row so callers' default filter still
1691
+ // drops it, but flag it so `showAllVaults` callers can tell a
1692
+ // failed read from an empty position (AUGUST-8347).
1693
+ return {
1694
+ vault: v.address,
1695
+ status: 'PENDING',
1696
+ readFailed: true,
1697
+ availableRedemptions: [],
1698
+ pendingRedemptions: [],
1699
+ redeemable: (0, core_1.toNormalizedBn)(0),
1700
+ walletBalance: (0, core_1.toNormalizedBn)(0),
1701
+ };
1702
+ }
1703
+ }));
1704
+ return promises.flat();
1705
+ }
1706
+ catch (e) {
1707
+ core_1.Logger.log.error('getVaultPositions', e, {
1708
+ vault,
1709
+ wallet,
1710
+ solanaWallet,
1711
+ stellarWallet,
1712
+ });
1713
+ throw new Error(`#getVaultPositions::${vault}:${e?.message}`);
1714
+ }
1715
+ }
1716
+ /**
1717
+ * @deprecated use getVaultHistoricalTimeseries instead
1718
+ */
1719
+ async function getVaultApy({ vault, options, historical, }) {
1720
+ try {
1721
+ if (!vault)
1722
+ throw new Error('Vault input parameter is undefined.');
1723
+ // only looking for current apy
1724
+ if (!historical) {
1725
+ const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
1726
+ return [
1727
+ {
1728
+ value: tokenizedVault?.reported_apy?.apy || 0,
1729
+ timestamp: new Date().toISOString(),
1730
+ },
1731
+ ];
1732
+ }
1733
+ else {
1734
+ // historical apy
1735
+ function paramBuilder(params, baseUrl) {
1736
+ if (!params)
1737
+ return '';
1738
+ const urlParams = new URLSearchParams();
1739
+ if (params.daysAgo)
1740
+ urlParams.append('days_ago', params.daysAgo.toString());
1741
+ if (params.order)
1742
+ urlParams.append('order', params.order.toString());
1743
+ if (params.interval)
1744
+ urlParams.append('interval', params.interval.toString());
1745
+ const query = urlParams.toString();
1746
+ if (!query)
1747
+ return '';
1748
+ return baseUrl?.includes('?') ? `&${query}` : `?${query}`;
1749
+ }
1750
+ const baseUrl = core_1.WEBSERVER_ENDPOINTS.public.tokenizedVault.historicalApy(vault);
1751
+ const historicalApy = await (0, core_1.fetchUpshiftPublic)(`${baseUrl}${paramBuilder(historical, baseUrl)}`, {
1752
+ headers: options?.headers
1753
+ ? options?.headers
1754
+ : undefined,
1755
+ });
1756
+ if (!historicalApy.ok)
1757
+ throw new Error('Failed to fetch historical apy.');
1758
+ const historicalApyData = (await historicalApy.json());
1759
+ return historicalApyData.data.values.map((val, index) => {
1760
+ const respectiveDate = historicalApyData.data.labels[index];
1761
+ return {
1762
+ timestamp: new Date(respectiveDate).toISOString(),
1763
+ value: val,
1764
+ };
1765
+ });
1766
+ }
1767
+ }
1768
+ catch (e) {
1769
+ core_1.Logger.log.error('getVaultApy', e, { vault });
1770
+ throw new Error(`#getVaultApy::${vault}:${e?.message}`);
1771
+ }
1772
+ }
1773
+ async function getRewardsStakingPositions({ rpcUrl, wallet, coinGeckoKey, }) {
1774
+ const REWARDS_CHAIN = 43114; // avalanche
1775
+ const REWARDS_SYMBOL = 'AVAX'; // avalanche
1776
+ const REWARDS_DECIMALS = 18; // avalanche
1777
+ const REWARDS_NAME = 'Avalanche'; // avalanche
1778
+ const APR_MULTIPLIER = 31536000;
1779
+ const UP_AUSD_SYMBOL = 'upAUSD';
1780
+ try {
1781
+ const provider = (0, core_1.createProvider)(rpcUrl);
1782
+ const chainId = await (0, core_1.getChainId)(provider);
1783
+ // Validate chain has rewards distributor
1784
+ if (chainId !== REWARDS_CHAIN) {
1785
+ core_1.Logger.log.warn('getStakingPositions:unsupported_chain', chainId);
1786
+ return [];
1787
+ }
1788
+ if (wallet && !(0, ethers_1.isAddress)(wallet)) {
1789
+ core_1.Logger.log.warn('getStakingPositions:invalid_wallet', wallet);
1790
+ return [];
1791
+ }
1792
+ // get reward distributor contract addresses
1793
+ const rewardDistributorAddresses = (0, core_1.REWARD_DISTRIBUTOR_ADDRESS)(chainId);
1794
+ // loop through each reward distributor contract address
1795
+ const positions = await Promise.all(rewardDistributorAddresses.map(async (contract, i) => {
1796
+ const rewardContract = (0, core_1.createContract)({
1797
+ address: contract,
1798
+ provider: provider,
1799
+ abi: abis_1.ABI_REWARD_DISTRIBUTOR,
1800
+ });
1801
+ // get reward distributor metadata
1802
+ const totalStaked = await rewardContract.totalStaked();
1803
+ const rewardsPerSecond = await rewardContract.rewardsPerSecond();
1804
+ // get staking token metadata
1805
+ const stakingTokenCalls = ['decimals', 'symbol', 'name', 'totalSupply'];
1806
+ const stakingTokenAddress = (await rewardContract.stakingToken());
1807
+ const stakingTokenContract = (0, core_1.createContract)({
1808
+ address: stakingTokenAddress,
1809
+ provider: provider,
1810
+ abi: abis_1.ABI_ERC20,
1811
+ });
1812
+ const [decimals, symbol, name] = await Promise.all(stakingTokenCalls.map((staking) => stakingTokenContract[staking]()));
1813
+ // if wallet address is passed, pull user balance of staked tokens
1814
+ let balance;
1815
+ let earned;
1816
+ if (wallet) {
1817
+ const formattedWallet = (0, ethers_1.getAddress)(wallet);
1818
+ balance = await rewardContract.balanceOf(formattedWallet);
1819
+ earned = await rewardContract.earned(formattedWallet);
1820
+ }
1821
+ // get appropriate token price
1822
+ const rewardTokenPriceInUsd = await (0, core_1.fetchTokenPrice)(REWARDS_SYMBOL, null, coinGeckoKey);
1823
+ const normalizedRewardTokenPrice = (0, core_1.toNormalizedBn)(rewardTokenPriceInUsd);
1824
+ // normalize numbers
1825
+ const normalizedTotalStakedInPool = (0, core_1.toNormalizedBn)(totalStaked, decimals);
1826
+ const normalizedRedeemable = (0, core_1.toNormalizedBn)(earned, REWARDS_DECIMALS);
1827
+ const normalizedRewardsPerSecond = (0, core_1.toNormalizedBn)(rewardsPerSecond, REWARDS_DECIMALS);
1828
+ const normalizedTotalStakedBalance = (0, core_1.toNormalizedBn)(balance, decimals);
1829
+ // calculate APR's
1830
+ const rewardTokenPriceToMultiply = symbol === UP_AUSD_SYMBOL
1831
+ ? Number(normalizedRewardTokenPrice?.normalized)
1832
+ : 1;
1833
+ const STAKED_APR = ((Number(normalizedRewardsPerSecond?.normalized) *
1834
+ APR_MULTIPLIER *
1835
+ rewardTokenPriceToMultiply) /
1836
+ (Number(normalizedTotalStakedInPool?.normalized) * Number(1))) *
1837
+ 100;
1838
+ const MAX_APR = ((Number(normalizedRewardsPerSecond?.normalized) *
1839
+ APR_MULTIPLIER *
1840
+ rewardTokenPriceToMultiply) /
1841
+ (Number(normalizedTotalStakedInPool?.normalized) * Number(1))) *
1842
+ 100;
1843
+ const activePosition = {
1844
+ id: String(i),
1845
+ rewardToken: {
1846
+ decimals: REWARDS_DECIMALS,
1847
+ symbol: REWARDS_SYMBOL,
1848
+ address: ethers_1.ZeroAddress,
1849
+ chain: REWARDS_CHAIN,
1850
+ redeemable: normalizedRedeemable,
1851
+ usd: normalizedRewardTokenPrice,
1852
+ name: REWARDS_NAME,
1853
+ },
1854
+ stakingToken: {
1855
+ decimals: Number(decimals),
1856
+ symbol: symbol,
1857
+ address: stakingTokenAddress,
1858
+ chain: REWARDS_CHAIN,
1859
+ totalStaked: normalizedTotalStakedBalance,
1860
+ usd: normalizedRewardTokenPrice,
1861
+ totalSupply: normalizedTotalStakedInPool,
1862
+ name: name,
1863
+ },
1864
+ rewardDistributor: contract,
1865
+ rewardPerSecond: (0, core_1.toNormalizedBn)(rewardsPerSecond),
1866
+ apy: STAKED_APR,
1867
+ maxApy: MAX_APR,
1868
+ chainId: REWARDS_CHAIN,
1869
+ };
1870
+ return activePosition;
1871
+ }));
1872
+ return positions;
1873
+ }
1874
+ catch (e) {
1875
+ core_1.Logger.log.error('getStakingPositions', e, { wallet });
1876
+ return [];
1877
+ }
1878
+ }
1879
+ async function getVaultTvl({ vault, options, historical, }) {
1880
+ try {
1881
+ if (!vault)
1882
+ throw new Error('Vault input parameter is undefined.');
1883
+ // Verify vault exists
1884
+ const _vaultExists = (await (0, core_1.fetchTokenizedVault)(vault, undefined, false, false))?.[0];
1885
+ if (!_vaultExists) {
1886
+ throw new Error(`Vault ${vault} not found in backend`);
1887
+ }
1888
+ // only wanting current APR
1889
+ if (!historical) {
1890
+ const version = (0, core_1.getVaultVersionV2)(_vaultExists);
1891
+ // Non-EVM early returns (no provider needed)
1892
+ if (version === 'sol-0') {
1893
+ return [
1894
+ {
1895
+ value: (0, core_1.toNormalizedBn)(0),
1896
+ timestamp: new Date().toISOString(),
1897
+ },
1898
+ ];
1899
+ }
1900
+ if (version === 'stellar-0') {
1901
+ const stellarDecimals = _vaultExists?.stellar_vault_metadata?.deposit_token_decimals ??
1902
+ constants_1.STELLAR_FALLBACK_DECIMALS;
1903
+ return [
1904
+ {
1905
+ value: (0, utils_1.backendTvlToNormalizedBn)(_vaultExists?.latest_reported_tvl, stellarDecimals),
1906
+ timestamp: new Date().toISOString(),
1907
+ },
1908
+ ];
1909
+ }
1910
+ const provider = (0, core_1.createProvider)(options.rpcUrl, options.chainId);
1911
+ switch (version) {
1912
+ case 'evm-2': {
1913
+ const receiptTokenAddress = await (0, core_1.getReceiptTokenAddress)(provider, vault);
1914
+ const receiptTokenDecimals = await (0, core_1.getDecimals)(provider, receiptTokenAddress, false);
1915
+ const vaultContract = (0, core_1.createContract)({
1916
+ address: vault,
1917
+ abi: TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2,
1918
+ provider,
1919
+ });
1920
+ const totalAssets = await vaultContract.getTotalAssets();
1921
+ return [
1922
+ {
1923
+ value: (0, core_1.toNormalizedBn)(totalAssets, receiptTokenDecimals),
1924
+ timestamp: new Date().toISOString(),
1925
+ },
1926
+ ];
1927
+ }
1928
+ default: {
1929
+ const vaultContract = (0, core_1.createContract)({
1930
+ address: vault,
1931
+ abi: abis_1.ABI_LENDING_POOL_V2,
1932
+ provider,
1933
+ });
1934
+ const decimals = await (0, core_1.getDecimals)(provider, vault);
1935
+ const totalAssets = await vaultContract.totalAssets();
1936
+ return [
1937
+ {
1938
+ value: (0, core_1.toNormalizedBn)(totalAssets, decimals),
1939
+ timestamp: new Date().toISOString(),
1940
+ },
1941
+ ];
1942
+ }
1943
+ }
1944
+ // const vaultRes = await getVault({
1945
+ // vault,
1946
+ // options,
1947
+ // loans: false,
1948
+ // allocations: false,
1949
+ // });
1950
+ // const returnObj = [
1951
+ // { value: vaultRes.totalSupply, timestamp: new Date().toISOString() },
1952
+ // ];
1953
+ // Logger.log.info('getVaultTvl:current', returnObj);
1954
+ // return returnObj;
1955
+ }
1956
+ else {
1957
+ // historical tvl
1958
+ // check vault version early to guard non-EVM chains
1959
+ const version = (0, core_1.getVaultVersionV2)(_vaultExists);
1960
+ if (version === 'stellar-0' || version === 'sol-0') {
1961
+ core_1.Logger.log.warn('getVaultTvl:historical', 'Historical TVL not supported for non-EVM vaults', { vault, version });
1962
+ return [];
1963
+ }
1964
+ // if no order is passed but inputParams are passed
1965
+ if (typeof historical !== 'undefined' && !historical.order)
1966
+ historical.order = 'desc';
1967
+ // days is default interval
1968
+ if (typeof historical !== 'undefined' && !historical.interval)
1969
+ historical.interval = 'days';
1970
+ // setup
1971
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
1972
+ const vaultContract = (0, core_1.createContract)({
1973
+ address: vault,
1974
+ abi: version === 'evm-2' ? TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOL_V2,
1975
+ provider,
1976
+ });
1977
+ let minAbi = [];
1978
+ let functionName = '';
1979
+ let vaultDecimals = 0;
1980
+ switch (version) {
1981
+ case 'evm-2': {
1982
+ minAbi = ['function getTotalAssets() view returns (uint256)'];
1983
+ functionName = 'getTotalAssets';
1984
+ const receiptTokenAddress = await (0, core_1.getReceiptTokenAddress)(provider, vault);
1985
+ vaultDecimals = await (0, core_1.getDecimals)(provider, receiptTokenAddress, false);
1986
+ break;
1987
+ }
1988
+ default: {
1989
+ minAbi = ['function totalAssets() view returns (uint256)'];
1990
+ functionName = 'totalAssets';
1991
+ vaultDecimals = await (0, core_1.getDecimals)(provider, vault);
1992
+ break;
1993
+ }
1994
+ }
1995
+ // block trackback setup
1996
+ // Get the latest finalized block to ensure all queried blocks are confirmed
1997
+ const finalizedBlock = await provider.getBlock('finalized');
1998
+ // Fall back to wall-clock "now" (ms) when the provider returns no
1999
+ // finalized block — the old `Date.now() * 60 * 1000` placed the window
2000
+ // 60,000x into the future.
2001
+ const finalizedTimestamp = finalizedBlock
2002
+ ? finalizedBlock.timestamp * 1000
2003
+ : Date.now();
2004
+ const daysAgo = historical.daysAgo * 24 * 60 * 60 * 1000;
2005
+ const dater = new ethereum_block_by_date_1.default(provider);
2006
+ const startDate = historical.daysAgo
2007
+ ? new Date(finalizedTimestamp - daysAgo)
2008
+ : new Date(finalizedTimestamp);
2009
+ const blocks = await dater.getEvery(historical.interval, // Period
2010
+ startDate.toUTCString(), // Start date
2011
+ new Date(finalizedTimestamp).toUTCString());
2012
+ // order blocks array
2013
+ const orderedBlocks = historical.order === 'desc' ? (0, core_1.orderObjArrByDate)(blocks) : blocks;
2014
+ // minimal ABI
2015
+ const totalAssetsHistorical = await (0, core_1.promiseSettle)(orderedBlocks?.map(async ({ block, date }) => {
2016
+ // get totalAssets at said block
2017
+ const totalAssetsAtBlock = await provider.call({
2018
+ to: vaultContract,
2019
+ data: new ethers_1.Interface(minAbi).encodeFunctionData(functionName, []),
2020
+ blockTag: block,
2021
+ });
2022
+ // cover case when data returned is 0
2023
+ if (totalAssetsAtBlock === '0x')
2024
+ return {
2025
+ timestamp: (0, core_1.dateToUnix)(new Date(date)),
2026
+ value: (0, core_1.toNormalizedBn)(0, 0),
2027
+ };
2028
+ // decode back to readable
2029
+ const readableTotalAssetsAtBlock = new ethers_1.Interface(minAbi).decodeFunctionResult(functionName, totalAssetsAtBlock);
2030
+ return {
2031
+ timestamp: (0, core_1.dateToUnix)(new Date(date)),
2032
+ value: (0, core_1.toNormalizedBn)(readableTotalAssetsAtBlock?.[0], vaultDecimals),
2033
+ };
2034
+ }));
2035
+ core_1.Logger.log.info('getVaultTvl:historical', totalAssetsHistorical.length);
2036
+ return totalAssetsHistorical;
2037
+ }
2038
+ }
2039
+ catch (e) {
2040
+ core_1.Logger.log.error('getVaultTvl', e, { vault });
2041
+ throw new Error(`Failed to fetch TVL for ${vault}: ${e instanceof Error ? e.message : 'Unknown error'}`);
2042
+ }
2043
+ }
2044
+ /**
2045
+ * Build the per-loan borrower / health-factor list for a single vault.
2046
+ *
2047
+ * Resilience: individual loan reads (`loanState()`, `borrower()`) and the
2048
+ * per-borrower Upshift backend call are isolated with `Promise.allSettled` and
2049
+ * try/catch so one bad loan — a test loan whose address isn't a real contract,
2050
+ * a borrower the backend doesn't recognize, an intermittent RPC error — drops
2051
+ * that single row from the result instead of rejecting the whole batch. Failed
2052
+ * rows are logged via `Logger.log.warn`; the loan is still returned with
2053
+ * `health_factor: undefined` so callers can render an explicit empty state
2054
+ * rather than a perpetual loading skeleton.
2055
+ *
2056
+ * @param vault - Vault address to scope the fetch to.
2057
+ * @param options - Standard vault options — `rpcUrl` must point at `vault`'s chain.
2058
+ * @returns Array of `IVaultBorrowerHealthFactor`, one entry per active loan.
2059
+ */
2060
+ async function getVaultBorrowerHealthFactor({ vault, options, }) {
2061
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
2062
+ const loans = await getVaultLoans(vault, options);
2063
+ // Read each loan's on-chain state; drop loans whose RPC call fails so a
2064
+ // single bad contract (e.g. a test loan address) can't sink the batch.
2065
+ const activeLoanResults = await Promise.allSettled(loans.map(async (l) => {
2066
+ const loanContract = (0, core_1.createContract)({
2067
+ provider,
2068
+ address: l.address,
2069
+ abi: abis_1.ABI_LOAN,
2070
+ });
2071
+ const loanState = await loanContract.loanState();
2072
+ if ((0, core_1.loanStateToReadable)(loanState) !== 'ACTIVE')
2073
+ return undefined;
2074
+ return l.address;
2075
+ }));
2076
+ const activeLoans = activeLoanResults
2077
+ .map((r, i) => {
2078
+ if (r.status === 'fulfilled')
2079
+ return r.value;
2080
+ core_1.Logger.log.warn('getVaultBorrowerHealthFactor:loanState', r.reason, {
2081
+ vault,
2082
+ loan: loans[i]?.address,
2083
+ });
2084
+ return undefined;
2085
+ })
2086
+ .filter((l) => l !== undefined);
2087
+ // Same fault-tolerance for the per-loan borrower() read.
2088
+ const formattedSettled = await Promise.allSettled(activeLoans.map(async (l) => {
2089
+ const loanContract = (0, core_1.createContract)({
2090
+ provider,
2091
+ address: l,
2092
+ abi: abis_1.ABI_LOAN,
2093
+ });
2094
+ return {
2095
+ borrower: (await loanContract.borrower()),
2096
+ loan: l,
2097
+ };
2098
+ }));
2099
+ const formattedLoansArray = formattedSettled
2100
+ .map((r, i) => {
2101
+ if (r.status === 'fulfilled')
2102
+ return r.value;
2103
+ core_1.Logger.log.warn('getVaultBorrowerHealthFactor:borrower', r.reason, {
2104
+ vault,
2105
+ loan: activeLoans[i],
2106
+ });
2107
+ return undefined;
2108
+ })
2109
+ .filter((entry) => entry !== undefined);
2110
+ const uniqueBorrowers = [
2111
+ ...new Set(formattedLoansArray.map((l) => l.borrower)),
2112
+ ];
2113
+ const borrowerResponses = {};
2114
+ for (const b of uniqueBorrowers) {
2115
+ try {
2116
+ const response = await (0, core_1.fetchUpshiftWithKey)(options.upshiftKey, core_1.WEBSERVER_ENDPOINTS.subaccount.health_factor(b));
2117
+ if (response.status === 200) {
2118
+ borrowerResponses[b] = await response.json();
2119
+ }
2120
+ }
2121
+ catch (err) {
2122
+ core_1.Logger.log.warn('getVaultBorrowerHealthFactor:health_factor', err, {
2123
+ vault,
2124
+ borrower: b,
2125
+ });
2126
+ }
2127
+ }
2128
+ formattedLoansArray.forEach((loan) => {
2129
+ loan.health_factor = borrowerResponses[loan.borrower];
2130
+ });
2131
+ return formattedLoansArray;
2132
+ }
2133
+ /**
2134
+ * Aggregate borrower-health-factor data, optionally scoped to a single vault.
2135
+ *
2136
+ * - When `vault` is provided, only that vault's tokenized record is fetched
2137
+ * and only its loans are read. This is the path callers should use when
2138
+ * they already know which vault they care about — it skips the cross-chain
2139
+ * fanout entirely.
2140
+ * - When `vault` is omitted, the function fetches every tokenized vault and
2141
+ * builds health factors for each, preserving the legacy behavior. Per-vault
2142
+ * failures are isolated with `Promise.allSettled` so one bad vault returns
2143
+ * an empty array instead of rejecting the whole map.
2144
+ *
2145
+ * The returned map is keyed by **lowercased** vault address so callers can
2146
+ * look up entries without worrying about EIP-55 checksum casing in either the
2147
+ * backend response or the caller's input.
2148
+ *
2149
+ * @param options - Standard vault options — `rpcUrl` should match `vault`'s chain.
2150
+ * @param vault - Optional vault address to scope the fetch to.
2151
+ * @returns Map of lowercased vault address → array of borrower-health-factor rows.
2152
+ */
2153
+ async function getHealthFactorOfBorrowersByVault({ options, vault, }) {
2154
+ const vaults = vault
2155
+ ? ((await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false)) ?? [])
2156
+ : await (0, core_1.fetchTokenizedVaults)(undefined, options?.headers, false, false);
2157
+ const healthFactorResults = await Promise.allSettled(vaults.map(async (v) => {
2158
+ return getVaultBorrowerHealthFactor({
2159
+ vault: v.address,
2160
+ options,
2161
+ });
2162
+ }));
2163
+ const healthFactorsByPool = {};
2164
+ vaults.forEach((pool, index) => {
2165
+ const result = healthFactorResults[index];
2166
+ const key = pool.address.toLowerCase();
2167
+ if (result?.status === 'fulfilled') {
2168
+ healthFactorsByPool[key] = result.value;
2169
+ return;
2170
+ }
2171
+ core_1.Logger.log.warn('getHealthFactorOfBorrowersByVault:vault', result?.reason, {
2172
+ vault: pool.address,
2173
+ });
2174
+ healthFactorsByPool[key] = [];
2175
+ });
2176
+ return healthFactorsByPool;
2177
+ }
2178
+ /**
2179
+ * Fetch user points from the backend API endpoint.
2180
+ * This replaces client-side points calculation with server-side processing.
2181
+ * @param userAddress - User wallet address (EVM or Solana)
2182
+ * @param options - Request options including headers
2183
+ * @returns Points data from the backend API
2184
+ */
2185
+ async function getUserPoints({ userAddress, options, }) {
2186
+ try {
2187
+ if (!userAddress) {
2188
+ throw new Error('User address parameter is undefined.');
2189
+ }
2190
+ const endpoint = core_1.WEBSERVER_ENDPOINTS.public.points.byUserAddress(userAddress);
2191
+ const response = await (0, core_1.fetchUpshiftPublic)(endpoint, {
2192
+ headers: options?.headers,
2193
+ });
2194
+ const data = await response.json();
2195
+ return data;
2196
+ }
2197
+ catch (e) {
2198
+ core_1.Logger.log.error('getUserPoints', e, { userAddress });
2199
+ const errorMessage = e instanceof Error ? e.message : String(e);
2200
+ throw new Error(`#getUserPoints::${userAddress}:${errorMessage}`);
2201
+ }
2202
+ }
2203
+ /**
2204
+ * Register a user for the points program, authenticated by a wallet signature.
2205
+ *
2206
+ * The caller must obtain a personal_sign (EIP-191) signature over the
2207
+ * canonical message template below — the backend reconstructs the same
2208
+ * string from these fields and verifies the signature recovers to
2209
+ * `userAddress`. Building the message client-side and server-side from the
2210
+ * same primitives means a hostile client cannot show one message in the
2211
+ * wallet prompt while submitting a different one for validation.
2212
+ *
2213
+ * Canonical message (`\n`-separated, no trailing newline):
2214
+ *
2215
+ * ```
2216
+ * Upshift: register {userAddress.toLowerCase()}
2217
+ * referrer: {referrerAddress.toLowerCase() || "none"}
2218
+ * chain: {chainId}
2219
+ * nonce: {nonce}
2220
+ * expires: {expiry}
2221
+ * ```
2222
+ *
2223
+ * @param userAddress - EVM wallet address being registered. Must match the
2224
+ * signer that produced `signature`. Lowercased before being embedded in
2225
+ * the canonical message.
2226
+ * @param referrerAddress - Optional EVM address that referred this user.
2227
+ * Lowercased when embedded; the literal string `"none"` is used when
2228
+ * absent. Tampering with this field after signing produces 401.
2229
+ * @param chainId - Chain on which the wallet signed. For smart-contract wallets
2230
+ * (Safe etc.) this is the chain whose RPC the backend will use to run the
2231
+ * EIP-1271 `isValidSignature` check; for EOAs it still pins the signature
2232
+ * to a chain so it can't be replayed cross-chain. Must be one of the chains
2233
+ * Upshift supports (see backend `SUPPORTED_REGISTRATION_CHAINS`).
2234
+ * @param signature - `0x`-prefixed hex of the personal_sign signature over
2235
+ * the canonical message. Single-use: replays within ~10 min are rejected.
2236
+ * @param nonce - Caller-generated random string (8–128 chars). Pair with
2237
+ * `userAddress` to form the single-use key in the backend's nonce store.
2238
+ * @param expiry - Unix timestamp (seconds). Must be in the future and within
2239
+ * the backend's TTL window (currently 10 min).
2240
+ * @param options - Request options (headers).
2241
+ * @returns Raw `Response` from the backend. 200 on success, 401 for
2242
+ * expired / replayed / mismatched-signer / tampered-referrer, 422 for an
2243
+ * unsupported `chainId`.
2244
+ *
2245
+ * @example
2246
+ * ```ts
2247
+ * const nonce = crypto.randomUUID().replace(/-/g, '');
2248
+ * const expiry = Math.floor(Date.now() / 1000) + 300;
2249
+ * const message =
2250
+ * `Upshift: register ${address.toLowerCase()}\n` +
2251
+ * `referrer: ${referrer?.toLowerCase() ?? 'none'}\n` +
2252
+ * `chain: ${chainId}\n` +
2253
+ * `nonce: ${nonce}\n` +
2254
+ * `expires: ${expiry}`;
2255
+ * const signature = await walletClient.signMessage({ account: address, message });
2256
+ * await registerUserForPoints({ userAddress: address, referrerAddress: referrer, chainId, signature, nonce, expiry });
2257
+ * ```
2258
+ */
2259
+ async function registerUserForPoints({ userAddress, referrerAddress, chainId, signature, nonce, expiry, options, }) {
2260
+ try {
2261
+ if (!userAddress) {
2262
+ throw new Error('User address parameter is undefined.');
2263
+ }
2264
+ if (!chainId || !signature || !nonce || !expiry) {
2265
+ throw new Error('chainId, signature, nonce, and expiry are required for registering a user');
2266
+ }
2267
+ const response = await (0, core_1.fetchUpshiftPublic)(core_1.WEBSERVER_ENDPOINTS.public.points.register, {
2268
+ method: 'post',
2269
+ server: 'public',
2270
+ headers: options?.headers,
2271
+ data: {
2272
+ user_address: userAddress,
2273
+ ...(referrerAddress && { referrer_address: referrerAddress }),
2274
+ chain_id: chainId,
2275
+ signature,
2276
+ nonce,
2277
+ expiry,
2278
+ },
2279
+ });
2280
+ return response;
2281
+ }
2282
+ catch (e) {
2283
+ core_1.Logger.log.error('registerUserForPoints', e, {
2284
+ userAddress,
2285
+ referrerAddress,
2286
+ });
2287
+ const errorMessage = e instanceof Error ? e.message : String(e);
2288
+ throw new Error(`#registerUserForPoints::${userAddress}:${errorMessage}`);
2289
+ }
2290
+ }
2291
+ /**
2292
+ * Fetch the points leaderboard data.
2293
+ * @param params - Optional parameters for pagination and sorting
2294
+ * @param options - Request options including headers
2295
+ * @returns Leaderboard response data
2296
+ */
2297
+ async function fetchPointsLeaderboard({ params, options, }) {
2298
+ try {
2299
+ const { page = 1, perPage = 10, sortBy = 'totalPoints' } = params || {};
2300
+ const searchParams = new URLSearchParams({
2301
+ page: page.toString(),
2302
+ perPage: perPage.toString(),
2303
+ sortBy,
2304
+ });
2305
+ const response = await (0, core_1.fetchUpshiftPublic)(`${core_1.WEBSERVER_ENDPOINTS.public.points.leaderboard}?${searchParams.toString()}`, {
2306
+ headers: options?.headers,
2307
+ });
2308
+ return await response.json();
2309
+ }
2310
+ catch (e) {
2311
+ core_1.Logger.log.error('fetchPointsLeaderboard', e, { params });
2312
+ const errorMessage = e instanceof Error ? e.message : String(e);
2313
+ throw new Error(`#fetchPointsLeaderboard::${errorMessage}`);
2314
+ }
2315
+ }
2316
+ /**
2317
+ * Fetch the timestamp when yield was last realized for a vault.
2318
+ * Returns the assetsUpdatedOn timestamp from the vault contract.
2319
+ * @param vault - Vault contract address
2320
+ * @param options - RPC configuration
2321
+ * @returns Timestamp (Unix timestamp in seconds) when yield was last realized
2322
+ */
2323
+ async function getYieldLastRealizedOn({ vault, options, }) {
2324
+ try {
2325
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
2326
+ const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
2327
+ if (!tokenizedVault) {
2328
+ throw new Error(`Vault ${vault} not found`);
2329
+ }
2330
+ const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
2331
+ let assetsUpdatedOn;
2332
+ switch (version) {
2333
+ case 'evm-2': {
2334
+ const contract = (0, core_1.createContract)({
2335
+ provider,
2336
+ address: vault,
2337
+ abi: TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2,
2338
+ });
2339
+ assetsUpdatedOn = await contract.assetsUpdatedOn();
2340
+ break;
2341
+ }
2342
+ case 'evm-0': {
2343
+ const contract = (0, core_1.createContract)({
2344
+ provider,
2345
+ address: vault,
2346
+ abi: abis_1.ABI_LENDING_POOL_V3,
2347
+ });
2348
+ assetsUpdatedOn = await contract.assetsUpdatedOn();
2349
+ break;
2350
+ }
2351
+ case 'sol-0':
2352
+ case 'stellar-0': {
2353
+ return 0;
2354
+ }
2355
+ default: {
2356
+ // Old lending pools don't have assetsUpdatedOn function
2357
+ // Return 0 for legacy vaults
2358
+ return 0;
2359
+ }
2360
+ }
2361
+ return Number(assetsUpdatedOn);
2362
+ }
2363
+ catch (e) {
2364
+ core_1.Logger.log.error('getYieldLastRealizedOn', e, { vault });
2365
+ const errorMessage = e instanceof Error ? e.message : String(e);
2366
+ throw new Error(`#getYieldLastRealizedOn::${vault}:${errorMessage}`);
2367
+ }
2368
+ }
2369
+ /**
2370
+ * Guard against lifetime-PnL over-inflation when the deposit history fails to load.
2371
+ *
2372
+ * Lifetime PnL is `currentPosition + totalWithdrawn - totalDeposited`. When the
2373
+ * deposit/withdrawal history fails to load it collapses to empty, so both
2374
+ * `totalDeposited` and `totalWithdrawn` read as zero. If the wallet still holds
2375
+ * a live on-chain position, the formula degenerates to reporting the entire
2376
+ * position as pure profit — a plausible-looking but wrong number.
2377
+ *
2378
+ * This guard rejects exactly that state: a non-zero position with no recorded
2379
+ * deposits, no shares received by transfer *and* no recorded withdrawals. It
2380
+ * also rejects a non-zero position with no recorded deposits when a share mint
2381
+ * to the wallet has no matching deposit (`mintsMatchHistory: false`): the mint
2382
+ * proves a deposit the history failed to return, so neither a peer transfer
2383
+ * nor a withdrawal makes the cost basis complete. Otherwise, any of the
2384
+ * following is treated as consistent and passes:
2385
+ * - deposits are present (the normal, computable case);
2386
+ * - shares were received by transfer and priced (see {@link transferCostBasis});
2387
+ * - withdrawals are present (a fully-exited-then-re-entered position is still
2388
+ * computable — the withdrawal records prove history loaded);
2389
+ * - the current position is zero (the wallet never interacted or fully exited,
2390
+ * so a zero PnL is correct, not inflated).
2391
+ *
2392
+ * Pure and synchronous — no RPC, no I/O — so it is safe to call inside the hot
2393
+ * path with no added latency.
2394
+ *
2395
+ * Shares received by a direct token transfer (never deposited, never withdrew)
2396
+ * reach this guard as `receivedByTransferRaw`: {@link getVaultUserLifetimePnl}
2397
+ * prices them with {@link transferCostBasis} first. The guard still throws for
2398
+ * such a wallet when its transfers cannot be read or priced, or when a share
2399
+ * mint to it has no indexed deposit.
2400
+ *
2401
+ * @param params - See {@link PnlHistoryConsistencyParams}.
2402
+ * @returns `void` when the history is consistent.
2403
+ * @throws {@link UpshiftHistoryUnavailableError} with code `HISTORY_UNAVAILABLE`
2404
+ * and `context: { vault, wallet }` when a non-zero position has no deposits
2405
+ * recorded and either a share mint without a matching deposit or neither
2406
+ * received shares nor withdrawals. The message carries the
2407
+ * `#getVaultUserLifetimePnl::<vault>::<wallet>` prefix so it groups with the
2408
+ * other errors leaving the getter.
2409
+ * @example
2410
+ * ```ts
2411
+ * assertPnlHistoryConsistent({
2412
+ * vault,
2413
+ * wallet,
2414
+ * hasDeposits: depositsByAsset.size > 0,
2415
+ * mintsMatchHistory,
2416
+ * receivedByTransferRaw: costBasis.receivedByTransferRaw,
2417
+ * totalWithdrawnRaw,
2418
+ * currentPositionRaw: currentPositionValue.raw,
2419
+ * });
2420
+ * ```
2421
+ */
2422
+ function assertPnlHistoryConsistent({ vault, wallet, hasDeposits, mintsMatchHistory = true, receivedByTransferRaw = BigInt(0), totalWithdrawnRaw, currentPositionRaw, }) {
2423
+ if (currentPositionRaw <= BigInt(0) || hasDeposits)
2424
+ return;
2425
+ const missingDeposit = !mintsMatchHistory;
2426
+ const noHistory = receivedByTransferRaw === BigInt(0) && totalWithdrawnRaw === BigInt(0);
2427
+ if (missingDeposit || noHistory) {
2428
+ const reason = missingDeposit
2429
+ ? 'a share mint has no indexed deposit'
2430
+ : 'no deposits or withdrawals were indexed';
2431
+ throw new errors_1.UpshiftHistoryUnavailableError(`#getVaultUserLifetimePnl::${vault}::${wallet}:transaction history unavailable — a live position exists but ${reason}, so lifetime PnL cannot be computed without over-reporting the entire position as profit`, { context: { vault, wallet } });
2432
+ }
2433
+ }
2434
+ /**
2435
+ * Guard against converting lifetime PnL to USD with a fabricated price.
2436
+ *
2437
+ * Every USD field `getVaultUserLifetimePnl` returns is a native amount times
2438
+ * the underlying asset's USD price. When the price server has no price, the
2439
+ * only honest outcome is "no USD value" — substituting a stand-in (the getter
2440
+ * previously used `1`) reports a non-dollar vault's position at $1 per asset,
2441
+ * a plausible-looking but wrong number that callers cannot detect.
2442
+ *
2443
+ * Pure and synchronous — no RPC, no I/O.
2444
+ *
2445
+ * @param params - See {@link PnlTokenPriceParams}.
2446
+ * @returns `tokenPriceUsd`, unchanged, when it is a finite number above zero.
2447
+ * @throws {@link UpshiftPriceUnavailableError} with code `PRICE_UNAVAILABLE`
2448
+ * and `context: { vault, wallet, asset }` when the price is zero, negative,
2449
+ * or not finite. The message carries the
2450
+ * `#getVaultUserLifetimePnl::<vault>::<wallet>` prefix so it groups with the
2451
+ * other errors leaving the getter.
2452
+ * @example
2453
+ * ```ts
2454
+ * const price = assertPnlTokenPrice({
2455
+ * vault,
2456
+ * wallet,
2457
+ * asset: underlyingAssetAddress,
2458
+ * tokenPriceUsd: await pricePromise,
2459
+ * });
2460
+ * ```
2461
+ */
2462
+ function assertPnlTokenPrice({ vault, wallet, asset, tokenPriceUsd, }) {
2463
+ if (Number.isFinite(tokenPriceUsd) && tokenPriceUsd > 0)
2464
+ return tokenPriceUsd;
2465
+ throw new errors_1.UpshiftPriceUnavailableError(`#getVaultUserLifetimePnl::${vault}::${wallet}:underlying token price unavailable for ${asset ?? 'an unreadable asset()'} — USD values cannot be computed without fabricating a price`, { context: { vault, wallet, asset: asset ?? null } });
2466
+ }
2467
+ /**
2468
+ * Sum a wallet's per-asset deposits into the vault's own decimals, at face
2469
+ * value — 1 unit of any deposited asset is treated as 1 unit of the vault's
2470
+ * reporting currency, with no live price conversion.
2471
+ *
2472
+ * Used by {@link getVaultUserLifetimePnl} for `totalDeposited`. Pre-deposit
2473
+ * multi-asset vaults only accept dollar-pegged stablecoins (USDC, USDT, DAI,
2474
+ * USDS, RLUSD, etc.) into a dollar-denominated vault, so face value is a
2475
+ * reasonable stand-in for USD value. This intentionally does NOT re-quote
2476
+ * through a live price oracle: doing so priced a historical deposit at
2477
+ * today's market rate, so `totalDeposited` (and lifetimePnl) drifted between
2478
+ * calls purely from stablecoin peg noise, and approximately re-inflated
2479
+ * deposits back toward face value — which silently excluded the vault's
2480
+ * entry fee from PnL. The fee is money the user actually paid, so PnL must
2481
+ * be computed net of it.
2482
+ *
2483
+ * @param depositsByAsset - Raw deposited amount per asset, keyed by
2484
+ * lowercased asset address (or `'default'` / `'lz-default'` for
2485
+ * single-asset / cross-chain rows).
2486
+ * @param assetDecimals - Decimals for each key in `depositsByAsset`.
2487
+ * @param vaultDecimals - The vault's own decimals — the target scale every
2488
+ * asset is rescaled to.
2489
+ * @returns Total deposited, in raw base units at `vaultDecimals`.
2490
+ */
2491
+ function sumDepositsAtFaceValue(depositsByAsset, assetDecimals, vaultDecimals) {
2492
+ let total = BigInt(0);
2493
+ for (const [assetAddress, rawAmount] of depositsByAsset) {
2494
+ const assetDec = assetDecimals.get(assetAddress) ?? vaultDecimals;
2495
+ if (assetDec === vaultDecimals) {
2496
+ total += rawAmount;
2497
+ }
2498
+ else if (assetDec > vaultDecimals) {
2499
+ total += rawAmount / BigInt(10 ** (assetDec - vaultDecimals));
2500
+ }
2501
+ else {
2502
+ total += rawAmount * BigInt(10 ** (vaultDecimals - assetDec));
2503
+ }
2504
+ }
2505
+ return total;
2506
+ }
2507
+ /**
2508
+ * Keep the receipt-token transfers that moved shares between `wallet` and
2509
+ * another holder.
2510
+ *
2511
+ * Vault shares are transferable ERC-20 tokens, so a wallet can hold a position
2512
+ * it never deposited into — a migration airdrop, a DEX purchase, a transfer from
2513
+ * another wallet. Mints (from the zero address), burns (to it) and transfers to
2514
+ * or from the vault itself are dropped: those are the deposit and withdrawal
2515
+ * legs the subgraph history already records, and counting them again would
2516
+ * double the cost basis. Transfers to or from `exclude` are dropped too.
2517
+ *
2518
+ * @param transfers - Receipt-token transfers touching `wallet`, as returned by
2519
+ * the vault's transfer subgraph.
2520
+ * @param wallet - The wallet whose position is being valued.
2521
+ * @param vault - The vault contract address.
2522
+ * @param exclude - Other counterparties whose transfers are not peer
2523
+ * transfers, such as a LayerZero OVault's hub composer and share OFT
2524
+ * (`hubComposer` / `hubShareOFT` in `LAYERZERO_VAULTS`). Defaults to none.
2525
+ * @returns The peer-to-peer transfers, in input order. Addresses compare
2526
+ * case-insensitively.
2527
+ * @example
2528
+ * ```ts
2529
+ * const peer = selectPeerTransfers(transfers, wallet, vault);
2530
+ * ```
2531
+ */
2532
+ function selectPeerTransfers(transfers, wallet, vault, exclude = []) {
2533
+ const self = wallet.toLowerCase();
2534
+ const excluded = new Set([ethers_1.ZeroAddress, vault, ...exclude].map((address) => address.toLowerCase()));
2535
+ return transfers.filter((transfer) => {
2536
+ const from = transfer.from.toLowerCase();
2537
+ const to = transfer.to.toLowerCase();
2538
+ if (from === to || excluded.has(from) || excluded.has(to))
2539
+ return false;
2540
+ return from === self || to === self;
2541
+ });
2542
+ }
2543
+ /**
2544
+ * Value peer share transfers in the vault's asset, at the vault's share ratio
2545
+ * on the day each one happened.
2546
+ *
2547
+ * Used by {@link getVaultUserLifetimePnl} when a wallet has no indexed
2548
+ * deposits: received shares become its cost basis and sent shares count as
2549
+ * withdrawn, so lifetime PnL for a transfer recipient reads "PnL since the
2550
+ * shares arrived" rather than failing or reporting the whole position as
2551
+ * profit. A day with no usable ratio takes the latest earlier day's, and a
2552
+ * transfer older than the history takes the earliest day's.
2553
+ *
2554
+ * Sent shares count only against a received basis by default. A wallet that
2555
+ * sent shares but received none gets a zero basis, so the lifetime-PnL history
2556
+ * guard still decides for it instead of reporting the sent shares as profit.
2557
+ * `countSentWithoutReceipt: true` reports them anyway, for a caller that knows
2558
+ * the wallet also deposited.
2559
+ *
2560
+ * Pure and synchronous. Assumes, as the getter does, that shares and assets
2561
+ * share the vault's decimals.
2562
+ *
2563
+ * @param params - See {@link TransferCostBasisParams}.
2564
+ * @returns The received and sent asset amounts, raw at the vault's decimals;
2565
+ * both zero when no shares were received.
2566
+ * @throws {@link UpshiftHistoryUnavailableError} (`HISTORY_UNAVAILABLE`) when no
2567
+ * day in `history` carries a positive, finite `asset_share_ratio`.
2568
+ * @example
2569
+ * ```ts
2570
+ * const { data } = await fetchVaultHistoricalTimeseries(vault, nDays);
2571
+ * const { receivedRaw, sentRaw } = transferCostBasis({
2572
+ * transfers: selectPeerTransfers(transfers, wallet, vault),
2573
+ * wallet,
2574
+ * vault,
2575
+ * history: data,
2576
+ * });
2577
+ * ```
2578
+ */
2579
+ function transferCostBasis({ transfers, wallet, vault, history, countSentWithoutReceipt = false, }) {
2580
+ const ratios = Object.entries(history)
2581
+ .filter(([, day]) => Number.isFinite(day.asset_share_ratio) &&
2582
+ (day.asset_share_ratio ?? 0) > 0)
2583
+ .map(([date, day]) => ({ date, ratio: day.asset_share_ratio }))
2584
+ .sort((a, b) => a.date.localeCompare(b.date));
2585
+ if (ratios.length === 0) {
2586
+ throw new errors_1.UpshiftHistoryUnavailableError(`#getVaultUserLifetimePnl::${vault}::${wallet}:no share ratio history to price shares received by transfer`, { context: { vault, wallet } });
2587
+ }
2588
+ const scale = BigInt(10) ** BigInt(18);
2589
+ const assetsAt = (shares, timestamp) => {
2590
+ const date = new Date(Number(timestamp) * 1000).toISOString().slice(0, 10);
2591
+ let ratio = ratios[0].ratio;
2592
+ for (const entry of ratios) {
2593
+ if (entry.date > date)
2594
+ break;
2595
+ ratio = entry.ratio;
2596
+ }
2597
+ return (shares * BigInt(Math.round(ratio * 1e18))) / scale;
2598
+ };
2599
+ const self = wallet.toLowerCase();
2600
+ let receivedRaw = BigInt(0);
2601
+ let sentRaw = BigInt(0);
2602
+ for (const transfer of transfers) {
2603
+ const shares = BigInt(transfer.value);
2604
+ if (transfer.to.toLowerCase() === self) {
2605
+ receivedRaw += assetsAt(shares, transfer.timestamp_);
2606
+ }
2607
+ else if (transfer.from.toLowerCase() === self) {
2608
+ sentRaw += assetsAt(shares, transfer.timestamp_);
2609
+ }
2610
+ }
2611
+ if (receivedRaw === BigInt(0) && !countSentWithoutReceipt) {
2612
+ return { receivedRaw, sentRaw: BigInt(0) };
2613
+ }
2614
+ return { receivedRaw, sentRaw };
2615
+ }
2616
+ /**
2617
+ * Check that a wallet's indexed deposit history accounts for every share mint
2618
+ * its share-token ledger shows.
2619
+ *
2620
+ * A deposit mints shares to the wallet in the same transaction, so a mint with
2621
+ * no deposit of the same hash means the history is missing that deposit (an
2622
+ * older deposit the subgraph never indexed, or one a third party made for the
2623
+ * wallet). Folding peer transfers into such a history would credit the wallet
2624
+ * for shares it sent while missing what it paid for them, so
2625
+ * {@link getVaultUserLifetimePnl} only folds transfers for a depositor when
2626
+ * this holds.
2627
+ *
2628
+ * Pure and synchronous. Hashes and addresses compare case-insensitively.
2629
+ *
2630
+ * @param params - See {@link HistoryCoversMintsParams}.
2631
+ * @returns `true` when every mint to `wallet` has a matching deposit, including
2632
+ * when there are no mints.
2633
+ * @example
2634
+ * ```ts
2635
+ * const covered = historyCoversMints({
2636
+ * transfers,
2637
+ * wallet,
2638
+ * depositTransactionHashes: history
2639
+ * .filter((item) => item.type === 'deposit')
2640
+ * .map((item) => item.transactionHash_),
2641
+ * });
2642
+ * ```
2643
+ */
2644
+ function historyCoversMints({ transfers, wallet, depositTransactionHashes, }) {
2645
+ const self = wallet.toLowerCase();
2646
+ const zero = ethers_1.ZeroAddress.toLowerCase();
2647
+ const deposits = new Set(depositTransactionHashes.map((hash) => hash.toLowerCase()));
2648
+ return transfers.every((transfer) => transfer.from.toLowerCase() !== zero ||
2649
+ transfer.to.toLowerCase() !== self ||
2650
+ deposits.has(transfer.transactionHash_.toLowerCase()));
2651
+ }
2652
+ /**
2653
+ * Keep the `ReserveDeposit` events that explain a share mint the indexed
2654
+ * deposit history misses.
2655
+ *
2656
+ * High Growth ETH mints shares for deposits made through Kelp's gain adapter
2657
+ * with a `ReserveDeposit` event instead of an ERC-4626 `Deposit`, so those
2658
+ * deposits are absent from the user history and fail {@link historyCoversMints}.
2659
+ * An event counts only when its transaction mints shares to `wallet` and holds
2660
+ * no indexed deposit, so a deposit is never counted twice.
2661
+ *
2662
+ * Pure and synchronous. Hashes and addresses compare case-insensitively.
2663
+ *
2664
+ * @param params - See {@link SelectReserveDepositsParams}.
2665
+ * @returns The matching events, in input order.
2666
+ * @example
2667
+ * ```ts
2668
+ * const reserve = selectReserveDeposits({
2669
+ * reserveDeposits: await getSubgraphUserReserveDeposits(wallet, provider, vault),
2670
+ * transfers,
2671
+ * wallet,
2672
+ * depositTransactionHashes,
2673
+ * });
2674
+ * ```
2675
+ */
2676
+ function selectReserveDeposits({ reserveDeposits, transfers, wallet, depositTransactionHashes, }) {
2677
+ const self = wallet.toLowerCase();
2678
+ const zero = ethers_1.ZeroAddress.toLowerCase();
2679
+ const deposits = new Set(depositTransactionHashes.map((hash) => hash.toLowerCase()));
2680
+ const unmatchedMints = new Set(transfers
2681
+ .filter((transfer) => transfer.from.toLowerCase() === zero &&
2682
+ transfer.to.toLowerCase() === self &&
2683
+ !deposits.has(transfer.transactionHash_.toLowerCase()))
2684
+ .map((transfer) => transfer.transactionHash_.toLowerCase()));
2685
+ return reserveDeposits.filter((deposit) => deposit.account.toLowerCase() === self &&
2686
+ unmatchedMints.has(deposit.transactionHash_.toLowerCase()));
2687
+ }
2688
+ /**
2689
+ * Drop withdrawal-request rows the vault logged more than once for a single
2690
+ * request.
2691
+ *
2692
+ * A redemption requested through Kelp's gain adapter makes High Growth ETH emit
2693
+ * `WithdrawalRequested` twice for one share transfer, and the subgraph indexes
2694
+ * both. In a transaction where the share ledger shows the wallet sending shares
2695
+ * to the vault `n` times, at most `n` request rows are kept. A transaction the
2696
+ * ledger shows no such transfer for keeps every row, so a missing ledger leaves
2697
+ * the history unchanged.
2698
+ *
2699
+ * Pure and synchronous. Hashes and addresses compare case-insensitively.
2700
+ *
2701
+ * @param params - See {@link CountWithdrawalRequestsOnceParams}.
2702
+ * @returns The history without the surplus request rows, in input order.
2703
+ * @example
2704
+ * ```ts
2705
+ * const history = countWithdrawalRequestsOnce({ history: userHistory, transfers, wallet, vault });
2706
+ * ```
2707
+ */
2708
+ function countWithdrawalRequestsOnce({ history, transfers, wallet, vault, }) {
2709
+ const self = wallet.toLowerCase();
2710
+ const target = vault.toLowerCase();
2711
+ const remaining = new Map();
2712
+ for (const transfer of transfers) {
2713
+ if (transfer.from.toLowerCase() !== self ||
2714
+ transfer.to.toLowerCase() !== target)
2715
+ continue;
2716
+ const hash = transfer.transactionHash_.toLowerCase();
2717
+ remaining.set(hash, (remaining.get(hash) ?? 0) + 1);
2718
+ }
2719
+ return history.filter((item) => {
2720
+ if (item.type !== 'withdraw-request')
2721
+ return true;
2722
+ const hash = item.transactionHash_.toLowerCase();
2723
+ const left = remaining.get(hash);
2724
+ if (left === undefined)
2725
+ return true;
2726
+ if (left === 0)
2727
+ return false;
2728
+ remaining.set(hash, left - 1);
2729
+ return true;
2730
+ });
2731
+ }
2732
+ /**
2733
+ * Value a wallet's Tokenized Vault V2 redemption requests, which carry only
2734
+ * shares.
2735
+ *
2736
+ * Every processed withdrawal (`WithdrawalProcessed.assetsAmount`) counts at the
2737
+ * assets the vault paid. Its shares, estimated at the vault's
2738
+ * `asset_share_ratio` on the day it was processed, retire the oldest requests
2739
+ * first. Request shares left over count at the ratio on their claim date (see
2740
+ * `computeClaimableDate`) once claimable: the subgraph lacks some processed rows
2741
+ * (none for earnAUSD before 2026-02-01), and that ratio lands within about
2742
+ * 0.05% of what the vault paid. Shares not yet claimable are left to the
2743
+ * current share price. Matching by amount, not by claim date, keeps the result
2744
+ * right when a vault processes a request before the date today's `lagDuration`
2745
+ * implies. A leftover under 0.5% of a request that a later processed
2746
+ * withdrawal followed is rounding from the daily ratio, so it is dropped.
2747
+ *
2748
+ * Pure and synchronous. Assumes, as the getter does, that shares and assets
2749
+ * share the vault's decimals. A day with no usable ratio takes the latest
2750
+ * earlier day's, and a date older than the history takes the earliest day's.
2751
+ *
2752
+ * @param params - See {@link SettleV2RedemptionsParams}.
2753
+ * @returns The settled assets and the shares left to the current price; every
2754
+ * requested share is pending when `ratios` holds no usable day.
2755
+ * @example
2756
+ * ```ts
2757
+ * const { settledAssetsRaw, pendingShares } = settleV2Redemptions({
2758
+ * history,
2759
+ * lagDuration: 259_200,
2760
+ * ratios: (await fetchVaultHistoricalTimeseries(vault, nDays)).data,
2761
+ * now: Math.floor(Date.now() / 1000),
2762
+ * });
2763
+ * ```
2764
+ */
2765
+ function settleV2Redemptions({ history, lagDuration, ratios, now, }) {
2766
+ const requests = history
2767
+ .filter((item) => item.type === 'withdraw-request')
2768
+ .sort((a, b) => Number(a.timestamp_) - Number(b.timestamp_));
2769
+ const days = Object.entries(ratios)
2770
+ .filter(([, day]) => Number.isFinite(day.asset_share_ratio) &&
2771
+ (day.asset_share_ratio ?? 0) > 0)
2772
+ .map(([date, day]) => ({
2773
+ date,
2774
+ ratio: BigInt(Math.round(day.asset_share_ratio * 1e18)),
2775
+ }))
2776
+ .sort((a, b) => a.date.localeCompare(b.date));
2777
+ if (days.length === 0) {
2778
+ return {
2779
+ settledAssetsRaw: BigInt(0),
2780
+ pendingShares: requests.reduce((sum, item) => sum + BigInt(item.amount), BigInt(0)),
2781
+ };
2782
+ }
2783
+ const scale = BigInt(10) ** BigInt(18);
2784
+ const ratioOn = (date) => days.filter((day) => day.date <= date).pop()?.ratio ?? days[0].ratio;
2785
+ const utcDay = (timestamp) => new Date(Number(timestamp) * 1000).toISOString().slice(0, 10);
2786
+ let settledAssetsRaw = BigInt(0);
2787
+ let retiredShares = BigInt(0);
2788
+ let lastProcessedAt = Number.NEGATIVE_INFINITY;
2789
+ for (const item of history) {
2790
+ if (item.type !== 'withdraw-processed' || item.isInstant)
2791
+ continue;
2792
+ const assets = BigInt(item.amount);
2793
+ settledAssetsRaw += assets;
2794
+ retiredShares += (assets * scale) / ratioOn(utcDay(item.timestamp_));
2795
+ lastProcessedAt = Math.max(lastProcessedAt, Number(item.timestamp_));
2796
+ }
2797
+ let pendingShares = BigInt(0);
2798
+ for (const item of requests) {
2799
+ const shares = BigInt(item.amount);
2800
+ const retired = retiredShares < shares ? retiredShares : shares;
2801
+ retiredShares -= retired;
2802
+ const open = shares - retired;
2803
+ // A sliver left on a request processed later is ratio rounding.
2804
+ if (open === BigInt(0) ||
2805
+ (open * BigInt(200) <= shares &&
2806
+ lastProcessedAt >= Number(item.timestamp_)))
2807
+ continue;
2808
+ const claim = (0, date_utils_1.computeClaimableDate)(Number(item.timestamp_), lagDuration);
2809
+ if ((0, date_utils_1.isClaimableNow)(claim.epoch, now)) {
2810
+ const key = (0, date_utils_1.formatDateKey)(claim.year, claim.month, claim.day);
2811
+ settledAssetsRaw += (open * ratioOn(key)) / scale;
2812
+ }
2813
+ else {
2814
+ pendingShares += open;
2815
+ }
2816
+ }
2817
+ return { settledAssetsRaw, pendingShares };
2818
+ }
2819
+ /**
2820
+ * Fold peer share transfers into a wallet's lifetime-PnL cost basis.
2821
+ *
2822
+ * Shares a wallet received from another holder (a migration airdrop, a DEX
2823
+ * purchase, a transfer from another wallet) cost the wallet their value on
2824
+ * arrival, so they add to the deposited amount; shares it sent away count as
2825
+ * withdrawn. This applies whether or not the wallet also deposited, so a holder
2826
+ * who received shares and then topped up is not credited with the received
2827
+ * shares as profit, and a depositor who moved its shares elsewhere (a lending
2828
+ * market, a DEX) is not shown as having lost them. A wallet with no deposit and
2829
+ * no received shares keeps a zero basis, so the history guard still decides.
2830
+ *
2831
+ * A depositor whose history misses a mint ({@link historyCoversMints}) is the
2832
+ * exception: its transfers do not count, so the result matches what the getter
2833
+ * returned before transfers counted for depositors. A wallet with no deposit
2834
+ * still gets a basis here when its history misses a mint, but
2835
+ * {@link assertPnlHistoryConsistent} rejects it. On a LayerZero OVault the
2836
+ * caller leaves the hub composer and share OFT out of the transfers (see
2837
+ * {@link selectPeerTransfers}): the composer's deliveries are already counted
2838
+ * as LayerZero deposits, and shares bridged to a spoke chain still count in the
2839
+ * position.
2840
+ *
2841
+ * Pure and synchronous.
2842
+ *
2843
+ * @param params - See {@link PnlCostBasisParams}.
2844
+ * @returns The deposited and withdrawn amounts to compute PnL from, and the
2845
+ * transfer-priced part of the deposited amount (zero when transfers did not
2846
+ * count).
2847
+ * @example
2848
+ * ```ts
2849
+ * const basis = pnlCostBasis({
2850
+ * depositedRaw,
2851
+ * withdrawnAssetsRaw,
2852
+ * hasDeposits: depositedRaw > BigInt(0),
2853
+ * mintsMatchHistory: historyCoversMints({
2854
+ * transfers,
2855
+ * wallet,
2856
+ * depositTransactionHashes,
2857
+ * }),
2858
+ * transfers: transferCostBasis({
2859
+ * transfers,
2860
+ * wallet,
2861
+ * vault,
2862
+ * history,
2863
+ * countSentWithoutReceipt: true,
2864
+ * }),
2865
+ * });
2866
+ * ```
2867
+ */
2868
+ function pnlCostBasis({ depositedRaw, withdrawnAssetsRaw, hasDeposits, mintsMatchHistory, transfers, }) {
2869
+ if (hasDeposits && !mintsMatchHistory) {
2870
+ return {
2871
+ depositedRaw,
2872
+ withdrawnAssetsRaw,
2873
+ receivedByTransferRaw: BigInt(0),
2874
+ };
2875
+ }
2876
+ const sentRaw = hasDeposits || transfers.receivedRaw > BigInt(0)
2877
+ ? transfers.sentRaw
2878
+ : BigInt(0);
2879
+ return {
2880
+ depositedRaw: depositedRaw + transfers.receivedRaw,
2881
+ withdrawnAssetsRaw: withdrawnAssetsRaw + sentRaw,
2882
+ receivedByTransferRaw: transfers.receivedRaw,
2883
+ };
2884
+ }
2885
+ // One receipt-transfer subgraph query, plus one backend timeseries request when
2886
+ // peer transfers exist. Any failure yields a zero basis, which leaves the
2887
+ // getter exactly where it was without transfers (the history guard decides).
2888
+ async function fetchTransferCostBasis(vault, wallet, provider, headers, exclude = []) {
2889
+ const none = { receivedRaw: BigInt(0), sentRaw: BigInt(0) };
2890
+ let shareTransfers = [];
2891
+ try {
2892
+ shareTransfers = await (0, vaults_1.getSubgraphUserTransfers)(wallet, provider, vault, undefined, { receiptSubgraph: true });
2893
+ const transfers = selectPeerTransfers(shareTransfers, wallet, vault, exclude);
2894
+ if (transfers.length === 0)
2895
+ return { basis: none, shareTransfers };
2896
+ const data = await fetchShareRatios(vault, Math.min(...transfers.map((t) => Number(t.timestamp_))), headers);
2897
+ return {
2898
+ basis: transferCostBasis({
2899
+ transfers,
2900
+ wallet,
2901
+ vault,
2902
+ history: data,
2903
+ countSentWithoutReceipt: true,
2904
+ }),
2905
+ shareTransfers,
2906
+ };
2907
+ }
2908
+ catch (e) {
2909
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:transferCostBasis', `Failed to price shares received by transfer: ${e}`);
2910
+ return { basis: none, shareTransfers };
2911
+ }
2912
+ }
2913
+ // The vault's daily history from `since` (unix seconds) on. A vault whose
2914
+ // history stops before then (a closed vault) is read in full instead, so the
2915
+ // latest earlier day still prices it.
2916
+ async function fetchShareRatios(vault, since, headers) {
2917
+ const nDays = Math.ceil((Date.now() / 1000 - since) / 86_400) + 1;
2918
+ const { data } = await (0, core_1.fetchVaultHistoricalTimeseries)(vault, nDays, headers);
2919
+ const usable = Object.values(data ?? {}).some((day) => Number.isFinite(day.asset_share_ratio) &&
2920
+ (day.asset_share_ratio ?? 0) > 0);
2921
+ if (usable)
2922
+ return data;
2923
+ return (await (0, core_1.fetchVaultHistoricalTimeseries)(vault, 3650, headers)).data;
2924
+ }
2925
+ // Values a Tokenized Vault V2 wallet's redemption requests: one lagDuration()
2926
+ // read and one backend timeseries request. Undefined when either fails, so the
2927
+ // caller values every request at the current share price.
2928
+ async function readV2Redemptions(provider, vault, history, headers) {
2929
+ try {
2930
+ const contract = (0, core_1.createContract)({
2931
+ provider,
2932
+ address: vault,
2933
+ abi: TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2,
2934
+ });
2935
+ const now = Math.floor(Date.now() / 1000);
2936
+ const earliest = Math.min(...history
2937
+ .filter((item) => item.type === 'withdraw-request')
2938
+ .map((item) => Number(item.timestamp_)));
2939
+ const [lagDuration, data] = await Promise.all([
2940
+ readLagDuration(contract, vault, 'getVaultUserLifetimePnl'),
2941
+ fetchShareRatios(vault, earliest, headers),
2942
+ ]);
2943
+ return settleV2Redemptions({ history, lagDuration, ratios: data, now });
2944
+ }
2945
+ catch (e) {
2946
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:v2Redemptions', `Failed to value redemption requests: ${e}`);
2947
+ return undefined;
2948
+ }
2949
+ }
2950
+ /**
2951
+ * Calculate lifetime PnL for a user in a specific vault.
2952
+ *
2953
+ * Basic logic:
2954
+ * 1. Get user's list of deposits and withdrawals from subgraph
2955
+ * 2. Fetch user's current assets (balanceOf * sharePrice via convertToAssets)
2956
+ * 3. Calculate PnL = assets withdrawn + current assets - assets deposited
2957
+ *
2958
+ * `totalDeposited` is the face value of what the user actually paid in
2959
+ * (each deposited asset's raw amount, rescaled to the vault's decimals — no
2960
+ * live price conversion), so any entry/exit fee the vault charges is
2961
+ * automatically counted as a cost against PnL rather than excluded from it.
2962
+ * Pre-deposit multi-asset vaults are assumed to only accept dollar-pegged
2963
+ * stablecoins into a dollar-denominated vault; a vault accepting a
2964
+ * non-pegged deposit asset would need deposit-time price conversion, which
2965
+ * this function does not perform.
2966
+ *
2967
+ * Every USD field is the native amount times the underlying asset's USD price
2968
+ * from the price server. There is no stand-in price: when none resolves the
2969
+ * getter throws rather than returning USD values it cannot know.
2970
+ *
2971
+ * Shares moved by transfer: peer transfers of the share token (see
2972
+ * {@link selectPeerTransfers}) are valued at the vault's daily share ratio
2973
+ * ({@link transferCostBasis}) and folded in by {@link pnlCostBasis} — received
2974
+ * shares add to `totalDeposited` (and are reported again as
2975
+ * `receivedByTransfer`), sent shares add to `totalWithdrawn`. This holds
2976
+ * alongside indexed deposits, so a wallet that received shares and also
2977
+ * deposited is not credited with the received shares as profit. On LayerZero
2978
+ * OVaults, transfers from or to the hub composer and share OFT do not count,
2979
+ * because the composer delivers cross-chain deposits by transfer and those
2980
+ * are already counted as LayerZero deposits.
2981
+ *
2982
+ * Gaps in the indexed history: a share mint with no `Deposit` event counts as
2983
+ * a deposit of the assets in its `ReserveDeposit` event
2984
+ * ({@link selectReserveDeposits}; High Growth ETH emits it for Kelp gain-adapter
2985
+ * deposits), and a withdrawal request the vault logged twice counts once
2986
+ * ({@link countWithdrawalRequestsOnce}). Transfers fold in for a depositor only
2987
+ * once every mint is explained. A wallet with no indexed deposit and a mint
2988
+ * that no deposit explains throws `HISTORY_UNAVAILABLE`.
2989
+ *
2990
+ * Tokenized Vault V2 redemptions carry only shares, so a settled one counts at
2991
+ * the assets the vault paid, or at the share ratio on its claim date when the
2992
+ * subgraph has no processed row for it ({@link settleV2Redemptions}). Only a
2993
+ * request not yet claimable is valued at the current share price. When the lag
2994
+ * or the ratio history cannot be read, every request is valued at the current
2995
+ * share price instead.
2996
+ *
2997
+ * Request cost: one receipt-transfer subgraph query (and a `symbol()` RPC read)
2998
+ * per call, plus one backend timeseries request when peer transfers exist and
2999
+ * one `reserveDeposits` subgraph query when a mint has no indexed deposit, and
3000
+ * for a Tokenized Vault V2 wallet with redemption requests, a `lagDuration()`
3001
+ * read plus one backend timeseries request. A timeseries window with no usable
3002
+ * ratio (a closed vault whose history stopped earlier) is read again in full,
3003
+ * so the latest earlier day still prices it. The transfer query runs for
3004
+ * every vault, in parallel with the history read.
3005
+ *
3006
+ * @param vault - Vault contract address
3007
+ * @param wallet - User wallet address
3008
+ * @param options - RPC configuration and service options
3009
+ * @returns Lifetime PnL data in both native token and USD
3010
+ * @throws {@link UpshiftHistoryUnavailableError} (`HISTORY_UNAVAILABLE`) when a
3011
+ * live position has no indexed deposits, withdrawals or priceable share
3012
+ * transfers, or has no indexed deposit while a share mint to the wallet has
3013
+ * no matching deposit.
3014
+ * @throws {@link UpshiftPriceUnavailableError} (`PRICE_UNAVAILABLE`) when the
3015
+ * underlying asset has no USD price, or `asset()` cannot be read.
3016
+ */
3017
+ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
3018
+ try {
3019
+ if (!vault)
3020
+ throw new Error('Vault input parameter is undefined.');
3021
+ if (!wallet)
3022
+ throw new Error('Wallet input parameter is undefined.');
3023
+ (0, utils_3.assertNotStellar)(vault, 'Lifetime PnL');
3024
+ if (!(0, ethers_1.isAddress)(vault))
3025
+ throw new Error(`Vault input parameter is not an address: ${String(vault)}`);
3026
+ if (!(0, ethers_1.isAddress)(wallet))
3027
+ throw new Error(`Wallet input parameter is not an address: ${String(wallet)}`);
3028
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
3029
+ const lzVaultKey = (0, deposits_1.isLayerZeroVault)(vault);
3030
+ const headers = options?.headers;
3031
+ // Peer transfers always count (see pnlCostBasis), so start that read
3032
+ // alongside the history instead of after it. A LayerZero OVault's hub
3033
+ // composer and share OFT are not peers.
3034
+ const transferCostBasisPromise = fetchTransferCostBasis(vault, wallet, provider, headers, lzVaultKey
3035
+ ? [
3036
+ deposits_1.LAYERZERO_VAULTS[lzVaultKey].hubComposer,
3037
+ deposits_1.LAYERZERO_VAULTS[lzVaultKey].hubShareOFT,
3038
+ ]
3039
+ : []);
3040
+ // Parallelize independent operations
3041
+ const [decimals, userHistory, positions, tokenizedVaultResult] = await Promise.all([
3042
+ (0, core_1.getDecimals)(provider, vault),
3043
+ (0, vaults_1.getSubgraphUserHistory)(wallet, provider, vault),
3044
+ getVaultPositions({
3045
+ vault,
3046
+ wallet,
3047
+ options,
3048
+ }),
3049
+ (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false),
3050
+ ]);
3051
+ const tokenizedVault = tokenizedVaultResult?.[0];
3052
+ if (!tokenizedVault) {
3053
+ throw new Error(`Vault ${vault} not found`);
3054
+ }
3055
+ // getDecimals resolves to `undefined` on a failed/absent decimals() read.
3056
+ // Unguarded, that undefined flows into `10 ** decimals` (=> NaN) and
3057
+ // toNormalizedBn(x, undefined), and the first BigInt(NaN) throws the opaque
3058
+ // "The number NaN cannot be converted to a BigInt" error. Decimals must come
3059
+ // from the chain — never assume a fallback — so fail fast with actionable
3060
+ // context. Transient RPC blips recover on the caller's retry.
3061
+ if (typeof decimals !== 'number' || !Number.isFinite(decimals)) {
3062
+ throw new Error(`Failed to resolve decimals for vault ${vault}`);
3063
+ }
3064
+ // Fetch LayerZero deposits/redeems for supported vaults
3065
+ let lzDeposits = [];
3066
+ let lzRedeems = [];
3067
+ if (lzVaultKey) {
3068
+ const lzPromises = [
3069
+ (0, deposits_1.queryLayerZeroDeposits)(lzVaultKey, wallet)
3070
+ .then((d) => {
3071
+ lzDeposits = d;
3072
+ })
3073
+ .catch((e) => {
3074
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:lzDeposits', `Failed to fetch LZ deposits for ${lzVaultKey}: ${e}`);
3075
+ }),
3076
+ ];
3077
+ // Only earnAUSD has redeem events currently
3078
+ if (lzVaultKey === 'earnAUSD') {
3079
+ lzPromises.push((0, redeems_1.queryLayerZeroRedeems)(wallet)
3080
+ .then((r) => {
3081
+ lzRedeems = r;
3082
+ })
3083
+ .catch((e) => {
3084
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:lzRedeems', `Failed to fetch LZ redeems for ${lzVaultKey}: ${e}`);
3085
+ }));
3086
+ }
3087
+ await Promise.all(lzPromises);
3088
+ }
3089
+ const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
3090
+ const currentPosition = positions.find((pos) => pos.vault?.toLowerCase() === vault.toLowerCase());
3091
+ // Start underlyingSymbol fetch (runs in parallel with processHistory)
3092
+ const underlyingSymbolPromise = (async () => {
3093
+ let underlyingTokenSymbol = 'UNKNOWN';
3094
+ let underlyingAssetAddress;
3095
+ try {
3096
+ // Use ABI_LENDING_POOL_V2 which has the asset() method
3097
+ const vaultContract = (0, core_1.createContract)({
3098
+ provider,
3099
+ address: vault,
3100
+ abi: abis_1.ABI_LENDING_POOL_V2,
3101
+ });
3102
+ try {
3103
+ underlyingAssetAddress = (await vaultContract.asset());
3104
+ const underlyingContract = (0, core_1.createContract)({
3105
+ provider,
3106
+ address: underlyingAssetAddress,
3107
+ abi: abis_1.ABI_ERC20,
3108
+ });
3109
+ underlyingTokenSymbol = await underlyingContract.symbol();
3110
+ }
3111
+ catch (e) {
3112
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:getUnderlyingSymbol', `Failed to get underlying token symbol: ${e}, falling back to receipt token symbol`);
3113
+ underlyingTokenSymbol =
3114
+ tokenizedVault.receipt_token_symbol || 'UNKNOWN';
3115
+ }
3116
+ }
3117
+ catch (e) {
3118
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:getUnderlyingSymbol', `Failed to get underlying token symbol: ${e}, falling back to receipt token symbol`);
3119
+ underlyingTokenSymbol =
3120
+ tokenizedVault.receipt_token_symbol || 'UNKNOWN';
3121
+ }
3122
+ return { underlyingTokenSymbol, underlyingAssetAddress };
3123
+ })();
3124
+ const { basis: transfers, shareTransfers } = await transferCostBasisPromise;
3125
+ // Process history - count all withdrawal requests (both processed and pending)
3126
+ // We don't count processed withdrawals separately to avoid double counting,
3127
+ // except on Tokenized Vault V2 (below), whose requests carry only shares.
3128
+ // Group deposit amounts by asset address to handle different decimals
3129
+ const depositsByAsset = new Map();
3130
+ // Separate withdrawal tracking: shares need conversion, assets don't
3131
+ let totalWithdrawalsInShares = BigInt(0);
3132
+ let totalWithdrawalsInAssets = BigInt(0);
3133
+ let requestedShares = BigInt(0);
3134
+ // ignore bad transactions when calculating pnl
3135
+ const history = countWithdrawalRequestsOnce({
3136
+ history: userHistory,
3137
+ transfers: shareTransfers,
3138
+ wallet,
3139
+ vault,
3140
+ }).filter((item) => !(0, core_1.isBadTransaction)(item.transactionHash_, tokenizedVault.chain));
3141
+ for (const historyItem of history) {
3142
+ const amount = BigInt(historyItem.amount);
3143
+ if (historyItem.type === 'deposit') {
3144
+ // Use assetIn if available (V2 vaults), otherwise use a default key
3145
+ // Use proper type guard instead of type assertion
3146
+ const assetKey = 'assetIn' in historyItem && typeof historyItem.assetIn === 'string'
3147
+ ? historyItem.assetIn.toLowerCase()
3148
+ : 'default';
3149
+ const current = depositsByAsset.get(assetKey) ?? BigInt(0);
3150
+ depositsByAsset.set(assetKey, current + amount);
3151
+ }
3152
+ else if (historyItem.type === 'withdraw-request') {
3153
+ // Withdraw requests are in shares, need conversion to assets
3154
+ requestedShares += amount;
3155
+ }
3156
+ else if (historyItem.type === 'withdraw-processed' &&
3157
+ historyItem.isInstant) {
3158
+ // Instant withdrawals are already in assets, no conversion needed
3159
+ totalWithdrawalsInAssets += amount;
3160
+ }
3161
+ }
3162
+ // A V2 request is worth what the vault paid when it settled it, not the
3163
+ // current share price (see settleV2Redemptions).
3164
+ const v2Redemptions = version === 'evm-2' && requestedShares > BigInt(0)
3165
+ ? await readV2Redemptions(provider, vault, history, headers)
3166
+ : undefined;
3167
+ if (v2Redemptions) {
3168
+ totalWithdrawalsInShares += v2Redemptions.pendingShares;
3169
+ totalWithdrawalsInAssets += v2Redemptions.settledAssetsRaw;
3170
+ }
3171
+ else {
3172
+ totalWithdrawalsInShares += requestedShares;
3173
+ }
3174
+ // Include LayerZero cross-chain deposits and redeems
3175
+ if (lzVaultKey && lzDeposits.length > 0) {
3176
+ for (const lzDeposit of lzDeposits) {
3177
+ // LZ deposits assetAmt is the underlying asset amount deposited in hub-chain decimals.
3178
+ // This assumes the LZ subgraph indexes amounts in the same decimals as the hub vault
3179
+ // (e.g. 6 for USDC). If source/hub decimals diverge, normalization would be needed here.
3180
+ const assetKey = 'lz-default';
3181
+ const amount = BigInt(lzDeposit.assetAmt);
3182
+ const current = depositsByAsset.get(assetKey) ?? BigInt(0);
3183
+ depositsByAsset.set(assetKey, current + amount);
3184
+ }
3185
+ }
3186
+ if (lzVaultKey && lzRedeems.length > 0) {
3187
+ for (const lzRedeem of lzRedeems) {
3188
+ // LZ redeems shareAmt is in shares, needs conversion via share price
3189
+ totalWithdrawalsInShares += BigInt(lzRedeem.shareAmt);
3190
+ }
3191
+ }
3192
+ const depositTransactionHashes = userHistory
3193
+ .filter((item) => item.type === 'deposit')
3194
+ .map((item) => item.transactionHash_);
3195
+ let mintsMatchHistory = historyCoversMints({
3196
+ transfers: shareTransfers,
3197
+ wallet,
3198
+ depositTransactionHashes,
3199
+ });
3200
+ if (!mintsMatchHistory) {
3201
+ const reserveDeposits = selectReserveDeposits({
3202
+ reserveDeposits: await (0, vaults_1.getSubgraphUserReserveDeposits)(wallet, provider, vault),
3203
+ transfers: shareTransfers,
3204
+ wallet,
3205
+ depositTransactionHashes,
3206
+ }).filter((deposit) => !(0, core_1.isBadTransaction)(deposit.transactionHash_, tokenizedVault.chain));
3207
+ for (const deposit of reserveDeposits) {
3208
+ depositsByAsset.set('default', (depositsByAsset.get('default') ?? BigInt(0)) +
3209
+ BigInt(deposit.assets));
3210
+ }
3211
+ mintsMatchHistory = historyCoversMints({
3212
+ transfers: shareTransfers,
3213
+ wallet,
3214
+ depositTransactionHashes: [
3215
+ ...depositTransactionHashes,
3216
+ ...reserveDeposits.map((deposit) => deposit.transactionHash_),
3217
+ ],
3218
+ });
3219
+ }
3220
+ const hasDeposits = depositsByAsset.size > 0;
3221
+ // Fetch decimals for each unique asset in parallel
3222
+ const assetDecimals = new Map();
3223
+ await Promise.all(Array.from(depositsByAsset.keys()).map(async (assetAddress) => {
3224
+ if (assetAddress === 'default' || assetAddress === 'lz-default') {
3225
+ assetDecimals.set(assetAddress, decimals);
3226
+ }
3227
+ else {
3228
+ const dec = await (0, core_1.getDecimals)(provider, assetAddress, false);
3229
+ assetDecimals.set(assetAddress, dec);
3230
+ }
3231
+ }));
3232
+ const totalDepositedNativeRaw = sumDepositsAtFaceValue(depositsByAsset, assetDecimals, decimals);
3233
+ const { underlyingTokenSymbol, underlyingAssetAddress } = await underlyingSymbolPromise;
3234
+ // Calculate current position value and fetch price in parallel
3235
+ let currentPositionValue = (0, core_1.toNormalizedBn)(0, decimals);
3236
+ const pricePromise = underlyingAssetAddress && tokenizedVault.chain
3237
+ ? (0, core_1.fetchTokenPriceByAddress)(underlyingAssetAddress, tokenizedVault.chain, options?.headers).catch(() => 0)
3238
+ : Promise.resolve(0);
3239
+ const vaultContract = (0, core_1.createContract)({
3240
+ provider,
3241
+ address: vault,
3242
+ abi: version === 'evm-2' ? TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOL_V2,
3243
+ });
3244
+ let sharePriceRaw = BigInt(0);
3245
+ try {
3246
+ // Retried on transport blips only (bounded, no fallback); a vault that
3247
+ // genuinely has no `lpTokenAddress()` still throws into the catch below.
3248
+ const lpTokenAddress = version === 'evm-2'
3249
+ ? await (0, core_1.getReceiptTokenAddressOrThrow)(provider, vault, 'getVaultUserLifetimePnl:receiptToken')
3250
+ : vault;
3251
+ let currentShares = BigInt(0);
3252
+ if (version === 'evm-2') {
3253
+ const lpTokenContract = (0, core_1.createContract)({
3254
+ provider,
3255
+ address: lpTokenAddress,
3256
+ abi: TokenizedVaultV2Receipt_1.ABI_TOKENIZED_VAULT_V2_RECEIPT,
3257
+ });
3258
+ currentShares = await lpTokenContract.balanceOf(wallet);
3259
+ }
3260
+ else {
3261
+ currentShares = await vaultContract.balanceOf(wallet);
3262
+ }
3263
+ // For LayerZero vaults, include receipt token balances on remote chains
3264
+ if (lzVaultKey) {
3265
+ // Build list of remote chains to check: prefer receipt_token_integrations from API,
3266
+ // fall back to LAYERZERO_VAULTS spoke chain config (OVault shareOFT addresses)
3267
+ const remoteChains = [];
3268
+ if (tokenizedVault.receipt_token_integrations?.length) {
3269
+ for (const rti of tokenizedVault.receipt_token_integrations) {
3270
+ if (rti.chain !== tokenizedVault.chain &&
3271
+ rti.address &&
3272
+ rti.chain) {
3273
+ remoteChains.push({
3274
+ chain: rti.chain,
3275
+ address: rti.address,
3276
+ });
3277
+ }
3278
+ }
3279
+ }
3280
+ else {
3281
+ // Fallback: use spoke chain addresses from LAYERZERO_VAULTS config
3282
+ const lzVaultConfig = deposits_1.LAYERZERO_VAULTS[lzVaultKey];
3283
+ if (lzVaultConfig.spokeChains) {
3284
+ for (const [chainIdStr, address] of Object.entries(lzVaultConfig.spokeChains)) {
3285
+ const chainId = Number(chainIdStr);
3286
+ if (chainId !== tokenizedVault.chain) {
3287
+ remoteChains.push({
3288
+ chain: chainId,
3289
+ address: address,
3290
+ });
3291
+ }
3292
+ }
3293
+ }
3294
+ }
3295
+ if (remoteChains.length > 0) {
3296
+ const remoteBalances = await Promise.all(remoteChains.map(async ({ chain, address }) => {
3297
+ try {
3298
+ const rpcUrls = (0, core_1.getFallbackRpcUrls)(chain);
3299
+ if (!rpcUrls.length) {
3300
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:lzRemoteBalance', `No RPC URL for chain ${chain}, skipping balance check`);
3301
+ return BigInt(0);
3302
+ }
3303
+ const remoteProvider = (0, core_1.createProvider)(rpcUrls[0]);
3304
+ const remoteTokenContract = (0, core_1.createContract)({
3305
+ provider: remoteProvider,
3306
+ address,
3307
+ abi: abis_1.ABI_ERC20,
3308
+ });
3309
+ const [rawBalance, remoteDecimals] = await Promise.all([
3310
+ remoteTokenContract.balanceOf(wallet),
3311
+ remoteTokenContract.decimals(),
3312
+ ]);
3313
+ const remDec = Number(remoteDecimals) || decimals;
3314
+ // Normalize remote balance to hub chain decimals
3315
+ if (remDec > decimals) {
3316
+ return rawBalance / BigInt(10 ** (remDec - decimals));
3317
+ }
3318
+ else if (remDec < decimals) {
3319
+ return rawBalance * BigInt(10 ** (decimals - remDec));
3320
+ }
3321
+ return rawBalance;
3322
+ }
3323
+ catch (e) {
3324
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:lzRemoteBalance', `Failed to fetch balance on chain ${chain}: ${e}`);
3325
+ return BigInt(0);
3326
+ }
3327
+ }));
3328
+ for (const bal of remoteBalances) {
3329
+ currentShares += bal;
3330
+ }
3331
+ }
3332
+ }
3333
+ if (version === 'evm-2') {
3334
+ sharePriceRaw = await vaultContract.getSharePrice();
3335
+ }
3336
+ else {
3337
+ const [totalAssets, totalSupply] = await Promise.all([
3338
+ vaultContract.totalAssets(),
3339
+ vaultContract.totalSupply(),
3340
+ ]);
3341
+ if (totalSupply === BigInt(0)) {
3342
+ sharePriceRaw = BigInt(10 ** decimals); // 1:1 if no supply
3343
+ }
3344
+ else {
3345
+ sharePriceRaw = (totalAssets * BigInt(10 ** decimals)) / totalSupply;
3346
+ }
3347
+ }
3348
+ const currentPositionValueRaw = (currentShares * sharePriceRaw) / BigInt(10 ** decimals);
3349
+ currentPositionValue = (0, core_1.toNormalizedBn)(currentPositionValueRaw, decimals);
3350
+ }
3351
+ catch (e) {
3352
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:getCurrentSharesAndPrice', `Failed to get current shares and share price: ${e}, falling back to wallet balance`);
3353
+ if (currentPosition?.walletBalance) {
3354
+ currentPositionValue = currentPosition.walletBalance;
3355
+ }
3356
+ }
3357
+ // Calculate totalWithdrawn combining shares (converted) and assets (as-is)
3358
+ let totalWithdrawnRaw = totalWithdrawalsInAssets;
3359
+ if (version === 'evm-2' && sharePriceRaw > BigInt(0)) {
3360
+ // Only convert shares to assets, instant withdrawals are already in assets
3361
+ totalWithdrawnRaw +=
3362
+ (totalWithdrawalsInShares * sharePriceRaw) / BigInt(10 ** decimals);
3363
+ }
3364
+ else {
3365
+ // For non-evm-2 or when share price is 0, treat shares as assets
3366
+ totalWithdrawnRaw += totalWithdrawalsInShares;
3367
+ }
3368
+ const costBasis = pnlCostBasis({
3369
+ depositedRaw: totalDepositedNativeRaw,
3370
+ withdrawnAssetsRaw: totalWithdrawnRaw,
3371
+ hasDeposits,
3372
+ mintsMatchHistory,
3373
+ transfers,
3374
+ });
3375
+ const totalWithdrawn = (0, core_1.toNormalizedBn)(costBasis.withdrawnAssetsRaw, decimals);
3376
+ // Guard the 2026-07-09 over-inflation incident: a live position with no
3377
+ // deposits *and* no withdrawals means the history failed to load, and the
3378
+ // PnL formula would report the whole position as profit. Fail loudly with a
3379
+ // typed error instead of returning a fabricated number.
3380
+ assertPnlHistoryConsistent({
3381
+ vault,
3382
+ wallet,
3383
+ hasDeposits,
3384
+ mintsMatchHistory,
3385
+ receivedByTransferRaw: costBasis.receivedByTransferRaw,
3386
+ totalWithdrawnRaw: costBasis.withdrawnAssetsRaw,
3387
+ currentPositionRaw: BigInt(currentPositionValue.raw),
3388
+ });
3389
+ const tokenPriceUsd = assertPnlTokenPrice({
3390
+ vault,
3391
+ wallet,
3392
+ asset: underlyingAssetAddress,
3393
+ tokenPriceUsd: await pricePromise,
3394
+ });
3395
+ // totalDeposited is already in the vault's own decimals (face value,
3396
+ // computed above) — convert to USD with the same underlying price used
3397
+ // for the other two legs below, so all three legs share one conversion
3398
+ // factor instead of drifting independently against each other.
3399
+ const totalDeposited = (0, core_1.toNormalizedBn)(costBasis.depositedRaw, decimals);
3400
+ const totalDepositedUsd = Number(totalDeposited.normalized) * tokenPriceUsd;
3401
+ const receivedByTransfer = (0, core_1.toNormalizedBn)(costBasis.receivedByTransferRaw, decimals);
3402
+ const receivedByTransferUsd = Number(receivedByTransfer.normalized) * tokenPriceUsd;
3403
+ const totalWithdrawnUsd = Number(totalWithdrawn.normalized) * tokenPriceUsd;
3404
+ const currentPositionValueUsd = Number(currentPositionValue.normalized) * tokenPriceUsd;
3405
+ // Calculate lifetimePnl in USD first (since deposits may be in different tokens with different prices)
3406
+ const lifetimePnlUsd = currentPositionValueUsd + totalWithdrawnUsd - totalDepositedUsd;
3407
+ // Use BigInt-scaled arithmetic to minimize precision loss when converting USD to native
3408
+ const scaleFactor = BigInt(10 ** 18); // High precision scale
3409
+ const tokenPriceScaled = BigInt(Math.round(tokenPriceUsd * Number(scaleFactor)));
3410
+ // Convert lifetimePnlUsd to native units
3411
+ const lifetimePnlUsdScaled = BigInt(Math.round(lifetimePnlUsd * Number(scaleFactor)));
3412
+ const lifetimePnlRaw = tokenPriceScaled > BigInt(0)
3413
+ ? (lifetimePnlUsdScaled * BigInt(10 ** decimals)) / tokenPriceScaled
3414
+ : BigInt(0);
3415
+ const lifetimePnl = (0, core_1.toNormalizedBn)(lifetimePnlRaw, decimals);
3416
+ const pnlPercentage = totalDepositedUsd > 0
3417
+ ? Number.parseFloat(((lifetimePnlUsd / totalDepositedUsd) * 100).toFixed(2))
3418
+ : 0;
3419
+ return {
3420
+ tokenSymbol: underlyingTokenSymbol,
3421
+ totalDeposited,
3422
+ totalDepositedUsd,
3423
+ totalWithdrawn,
3424
+ totalWithdrawnUsd,
3425
+ currentPositionValue,
3426
+ currentPositionValueUsd,
3427
+ lifetimePnl,
3428
+ lifetimePnlUsd,
3429
+ pnlPercentage,
3430
+ receivedByTransfer,
3431
+ receivedByTransferUsd,
3432
+ };
3433
+ }
3434
+ catch (e) {
3435
+ core_1.Logger.log.error('getVaultUserLifetimePnl', e, { vault, wallet });
3436
+ // Preserve the typed history and price guards so callers can distinguish
3437
+ // an unavailable-history or unavailable-price failure from a generic getter
3438
+ // error. Their messages already carry the
3439
+ // `#getVaultUserLifetimePnl::vault::wallet` prefix.
3440
+ if (e instanceof errors_1.UpshiftHistoryUnavailableError ||
3441
+ e instanceof errors_1.UpshiftPriceUnavailableError)
3442
+ throw e;
3443
+ const errorMessage = e instanceof Error ? e.message : String(e);
3444
+ throw new Error(`#getVaultUserLifetimePnl::${vault}::${wallet}:${errorMessage}`);
3445
+ }
3446
+ }
3447
+ /**
3448
+ * Calculate PnL for a vault (vault-level, not user-specific).
3449
+ * Returns the vault's overall profit and loss across all users.
3450
+ *
3451
+ * Sums, over the vault's whole history, each day's change in assets per share
3452
+ * times that day's total shares, then prices the result at the receipt
3453
+ * token's current USD price. The daily history comes from
3454
+ * `GET /tokenized_vault/{vault}/historical-timeseries`, the replacement for the
3455
+ * deprecated `load_snapshots` flag on the vault read.
3456
+ *
3457
+ * Network: one backend vault read, one timeseries read and one price read.
3458
+ *
3459
+ * @param vault - Vault contract address
3460
+ * @param options - RPC configuration and service options
3461
+ * @returns Vault PnL in the deposit asset (`totalPnl`) and in USD (`pnlUsd`).
3462
+ * Both are zero for a vault with no recorded history. `pnlUsd` is also zero
3463
+ * when the receipt token price cannot be fetched.
3464
+ * @throws Error when `vault` is missing, is a Stellar vault, is not an
3465
+ * address, or the vault or its history cannot be read.
3466
+ */
3467
+ async function getVaultPnl({ vault, options, }) {
3468
+ try {
3469
+ if (!vault)
3470
+ throw new Error('Vault input parameter is undefined.');
3471
+ (0, utils_3.assertNotStellar)(vault, 'Vault PnL');
3472
+ if (!(0, ethers_1.isAddress)(vault))
3473
+ throw new Error(`Vault input parameter is not an address: ${String(vault)}`);
3474
+ // The vault row only supplies decimals, chain and receipt token, so skip
3475
+ // its subaccounts and snapshots. 3650 days reads the full history, as
3476
+ // `fetchShareRatios` does.
3477
+ const [vaultData, { data: history }] = await Promise.all([
3478
+ getVault({
3479
+ vault,
3480
+ loans: false,
3481
+ allocations: false,
3482
+ options,
3483
+ loadSubaccounts: false,
3484
+ loadSnapshots: false,
3485
+ }),
3486
+ (0, core_1.fetchVaultHistoricalTimeseries)(vault, 3650, options?.headers),
3487
+ ]);
3488
+ if (!vaultData) {
3489
+ throw new Error(`Vault ${vault} not found`);
3490
+ }
3491
+ const decimals = vaultData.decimals;
3492
+ // `daily_pnl` is the day's change in assets per share. It is zero on the
3493
+ // first day and on forward-filled (`missing`) days, so those add nothing.
3494
+ let totalPnlRaw = BigInt(0);
3495
+ Object.values(history ?? {}).forEach((day) => {
3496
+ if (!day.daily_pnl || !day.total_shares)
3497
+ return;
3498
+ // Convert normalized numbers to BigInt using toNormalizedBn's normalized string
3499
+ const dailyPnlNorm = (0, core_1.toNormalizedBn)(day.daily_pnl, decimals);
3500
+ const totalSharesNorm = (0, core_1.toNormalizedBn)(day.total_shares, decimals);
3501
+ // Convert normalized strings to BigInt (split, pad fraction, combine) - all inline
3502
+ const [dailyWhole = '0', dailyFrac = ''] = dailyPnlNorm.normalized.split('.');
3503
+ const dailyPnlPerShareScaled = BigInt(dailyWhole + (dailyFrac || '').padEnd(decimals, '0').slice(0, decimals));
3504
+ const [sharesWhole = '0', sharesFrac = ''] = totalSharesNorm.normalized.split('.');
3505
+ const totalSharesScaled = BigInt(sharesWhole +
3506
+ (sharesFrac || '').padEnd(decimals, '0').slice(0, decimals));
3507
+ // Calculate: daily_pnl_per_share[day] * total_shares[day] (both in raw units)
3508
+ const dayPnlRaw = (dailyPnlPerShareScaled * totalSharesScaled) / BigInt(10 ** decimals);
3509
+ totalPnlRaw += dayPnlRaw;
3510
+ });
3511
+ // No history (or no PnL in it) prices to zero, so skip the price read.
3512
+ if (totalPnlRaw === BigInt(0)) {
3513
+ return { totalPnl: (0, core_1.toNormalizedBn)(0, decimals), pnlUsd: 0 };
3514
+ }
3515
+ // Convert totalPnlRaw (BigInt) to normalized number using toNormalizedBn
3516
+ const totalPnl = (0, core_1.toNormalizedBn)(totalPnlRaw, decimals);
3517
+ const totalPnlNormalized = Number(totalPnl.normalized);
3518
+ const chainId = vaultData.chainId;
3519
+ // Get receipt token address (HLPe) from vaultData - no need for on-chain lookup
3520
+ const lpTokenAddress = vaultData.receipt.address;
3521
+ // Fetch HLPe (lpTokenAddress) price in USD
3522
+ let tokenPriceUsd = 0;
3523
+ try {
3524
+ tokenPriceUsd = await (0, core_1.fetchTokenPriceByAddress)(lpTokenAddress, chainId, options?.headers);
3525
+ }
3526
+ catch (error) {
3527
+ core_1.Logger.log.warn('getVaultPnl:priceFetch', `Failed to fetch HLPe token price: ${error}`);
3528
+ }
3529
+ // Convert to USD and format to 6 decimal places
3530
+ const pnlUsd = Number((totalPnlNormalized * tokenPriceUsd).toFixed(6));
3531
+ return {
3532
+ totalPnl,
3533
+ pnlUsd,
3534
+ };
3535
+ }
3536
+ catch (e) {
3537
+ core_1.Logger.log.error('getVaultPnl', e, { vault });
3538
+ const errorMessage = e instanceof Error ? e.message : String(e);
3539
+ throw new Error(`#getVaultPnl::${vault}:${errorMessage}`);
3540
+ }
3541
+ }
3542
+ /*
3543
+ * Fetch historical timeseries data for a vault.
3544
+ * Returns TVL, APY, PnL, share price, and other metrics over time.
3545
+ * @param vault - Vault contract address
3546
+ * @param nDays - Number of days of historical data (default 30, min 1)
3547
+ * @param options - Request options including headers
3548
+ * @returns Historical timeseries data with date string keys
3549
+ */
3550
+ async function getVaultHistoricalTimeseries({ vault, nDays, options, }) {
3551
+ try {
3552
+ if (!vault)
3553
+ throw new Error('Vault input parameter is undefined.');
3554
+ const response = await (0, core_1.fetchVaultHistoricalTimeseries)(vault, nDays, options?.headers);
3555
+ return response;
3556
+ }
3557
+ catch (e) {
3558
+ core_1.Logger.log.error('getVaultHistoricalTimeseries', e, { vault, nDays });
3559
+ const errorMessage = e instanceof Error ? e.message : String(e);
3560
+ throw new Error(`#getVaultHistoricalTimeseries::${vault}:${errorMessage}`);
3561
+ }
3562
+ }
3563
+ /**
3564
+ * Fetch annualized APY metrics for a vault.
3565
+ *
3566
+ * Supported Vaults: cUSDO, tETH, wstETH, rsETH
3567
+ *
3568
+ * @deprecated The `hgETH30dLiquidAPY` and `hgETH7dLiquidAPY` response fields are deprecated.
3569
+ * These fields will be removed in the next major release.
3570
+ * Use `liquidAPY30Day` and `liquidAPY7Day` fields instead.
3571
+ *
3572
+ * @param vault - Vault contract address
3573
+ * @param options - Request options including headers
3574
+ * @returns Annualized APY data including liquidity APY
3575
+ */
3576
+ async function getVaultAnnualizedApy({ vault, options, }) {
3577
+ try {
3578
+ if (!vault) {
3579
+ throw new Error('Vault address parameter is undefined.');
3580
+ }
3581
+ if (!(0, ethers_1.isAddress)(vault)) {
3582
+ throw new Error(`Vault parameter is not an address: ${String(vault)}`);
3583
+ }
3584
+ const endpoint = core_1.WEBSERVER_ENDPOINTS.public.tokenizedVault.annualizedApy(vault);
3585
+ const response = await (0, core_1.fetchUpshiftPublic)(endpoint, {
3586
+ headers: options?.headers,
3587
+ server: 'public',
3588
+ });
3589
+ return (await response.json());
3590
+ }
3591
+ catch (e) {
3592
+ core_1.Logger.log.error('getVaultAnnualizedApy', e, { vault });
3593
+ const errorMessage = e instanceof Error ? e.message : String(e);
3594
+ throw new Error(`#getVaultAnnualizedApy::${vault}:${errorMessage}`);
3595
+ }
3596
+ }
3597
+ /**
3598
+ * Fetch summary data for a vault (name, type, chain, recent returns).
3599
+ * @param vault - Vault contract address
3600
+ * @param options - Request options including headers
3601
+ * @returns Vault summary data
3602
+ */
3603
+ async function getVaultSummary({ vault, options, }) {
3604
+ try {
3605
+ if (!vault) {
3606
+ throw new Error('Vault address parameter is undefined.');
3607
+ }
3608
+ (0, utils_3.assertNotStellar)(vault, 'Vault summary');
3609
+ if (!(0, ethers_1.isAddress)(vault)) {
3610
+ throw new Error(`Vault parameter is not an address: ${String(vault)}`);
3611
+ }
3612
+ const endpoint = core_1.WEBSERVER_ENDPOINTS.public.tokenizedVault.summary(vault);
3613
+ const response = await (0, core_1.fetchUpshiftPublic)(endpoint, {
3614
+ headers: options?.headers,
3615
+ server: 'public',
3616
+ });
3617
+ return (await response.json());
3618
+ }
3619
+ catch (e) {
3620
+ core_1.Logger.log.error('getVaultSummary', e, { vault });
3621
+ const errorMessage = e instanceof Error ? e.message : String(e);
3622
+ throw new Error(`#getVaultSummary::${vault}:${errorMessage}`);
3623
+ }
3624
+ }
3625
+ /**
3626
+ * Fetch withdrawal summary and pending queue for a vault.
3627
+ * @param vault - Vault contract address
3628
+ * @param chain - Chain identifier (e.g., chain ID as string)
3629
+ * @param options - Request options including headers
3630
+ * @returns Withdrawal summary and pending queue
3631
+ */
3632
+ async function getVaultWithdrawals({ vault, chain, options, }) {
3633
+ try {
3634
+ if (!vault) {
3635
+ throw new Error('Vault address parameter is undefined.');
3636
+ }
3637
+ (0, utils_3.assertNotStellar)(vault, 'Vault withdrawals');
3638
+ if (!(0, ethers_1.isAddress)(vault)) {
3639
+ throw new Error(`Vault parameter is not an address: ${String(vault)}`);
3640
+ }
3641
+ if (!chain) {
3642
+ throw new Error('Chain parameter is undefined.');
3643
+ }
3644
+ const endpoint = core_1.WEBSERVER_ENDPOINTS.public.tokenizedVault.withdrawals(chain, vault);
3645
+ const response = await (0, core_1.fetchUpshiftPublic)(endpoint, {
3646
+ headers: options?.headers,
3647
+ server: 'public',
3648
+ });
3649
+ return (await response.json());
3650
+ }
3651
+ catch (e) {
3652
+ core_1.Logger.log.error('getVaultWithdrawals', e, { vault, chain });
3653
+ const errorMessage = e instanceof Error ? e.message : String(e);
3654
+ throw new Error(`#getVaultWithdrawals::${vault}::${chain}:${errorMessage}`);
3655
+ }
3656
+ }
3657
+ /**
3658
+ * Fetch pending redemptions for a vault with liquidity analysis.
3659
+ * @param vault - Vault contract address
3660
+ * @param pastDays - Number of past days to include (default 7, min 1, max 30)
3661
+ * @param futureDays - Number of future days to include (default 14, min 1, max 30)
3662
+ * @param options - Request options including headers
3663
+ * @returns Pending redemptions grouped by date with liquidity summary
3664
+ */
3665
+ async function getVaultPendingRedemptions({ vault, pastDays, futureDays, options, }) {
3666
+ try {
3667
+ if (!vault)
3668
+ throw new Error('Vault address parameter is undefined.');
3669
+ if (!(0, ethers_1.isAddress)(vault))
3670
+ throw new Error(`Vault parameter is not an address: ${String(vault)}`);
3671
+ if (pastDays !== undefined && (pastDays < 1 || pastDays > 30))
3672
+ throw new Error('pastDays must be between 1 and 30');
3673
+ if (futureDays !== undefined && (futureDays < 1 || futureDays > 30))
3674
+ throw new Error('futureDays must be between 1 and 30');
3675
+ const params = new URLSearchParams();
3676
+ if (pastDays !== undefined)
3677
+ params.set('past_days', pastDays.toString());
3678
+ if (futureDays !== undefined)
3679
+ params.set('future_days', futureDays.toString());
3680
+ const qs = params.toString();
3681
+ const endpoint = `${core_1.WEBSERVER_ENDPOINTS.public.vaults.pendingRedemptions(vault)}${qs ? `?${qs}` : ''}`;
3682
+ const response = await (0, core_1.fetchUpshiftPublic)(endpoint, {
3683
+ headers: options?.headers,
3684
+ server: 'public',
3685
+ });
3686
+ const raw = await response.json();
3687
+ return {
3688
+ vault: {
3689
+ address: raw.vault.address,
3690
+ chainId: raw.vault.chain_id,
3691
+ name: raw.vault.name,
3692
+ symbol: raw.vault.symbol,
3693
+ },
3694
+ asset: {
3695
+ address: raw.asset.address,
3696
+ symbol: raw.asset.symbol,
3697
+ decimals: raw.asset.decimals,
3698
+ },
3699
+ liquidity: {
3700
+ available: raw.liquidity.available,
3701
+ },
3702
+ summary: {
3703
+ totalPending: raw.summary.total_pending,
3704
+ pendingCount: raw.summary.pending_count,
3705
+ pendingToLiquidityRatio: raw.summary.pending_to_liquidity_ratio,
3706
+ isLiquiditySufficient: raw.summary.is_liquidity_sufficient,
3707
+ shortfall: raw.summary.shortfall,
3708
+ nextRedemptionDate: raw.summary.next_redemption_date,
3709
+ isOverdue: raw.summary.is_overdue,
3710
+ },
3711
+ redemptionsByDate: raw.redemptions_by_date.map((item) => ({
3712
+ date: item.date,
3713
+ assets: item.assets,
3714
+ daysUntilDue: item.days_until_due,
3715
+ isOverdue: item.is_overdue,
3716
+ })),
3717
+ };
3718
+ }
3719
+ catch (e) {
3720
+ core_1.Logger.log.error('getVaultPendingRedemptions', e, {
3721
+ vault,
3722
+ pastDays,
3723
+ futureDays,
3724
+ });
3725
+ const errorMessage = e instanceof Error ? e.message : String(e);
3726
+ throw new Error(`#getVaultPendingRedemptions::${vault}:${errorMessage}`);
3727
+ }
3728
+ }
3729
+ /**
3730
+ * Preview the amount of assets that would be received for redeeming shares (queued redemption).
3731
+ * @param vault - Vault contract address
3732
+ * @param sharesAmount - Amount of shares to redeem (human-readable, raw bigint, or string)
3733
+ * @param options - RPC configuration
3734
+ * @returns The amount of assets as an INormalizedNumber with `normalized` and `raw` fields
3735
+ */
3736
+ async function getPreviewRedemption({ vault, sharesAmount, options, }) {
3737
+ try {
3738
+ if (!vault)
3739
+ throw new Error('Vault address parameter is undefined.');
3740
+ (0, utils_3.assertNotStellar)(vault, 'Preview redemption');
3741
+ if (!(0, ethers_1.isAddress)(vault)) {
3742
+ throw new Error(`Vault parameter is not an address: ${String(vault)}`);
3743
+ }
3744
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
3745
+ const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
3746
+ const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
3747
+ let assets;
3748
+ let decimals;
3749
+ if (vaultVersion === 'evm-2') {
3750
+ const contract = (0, core_1.createContract)({
3751
+ address: vault,
3752
+ abi: TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2,
3753
+ provider,
3754
+ });
3755
+ // Retried on transport blips only (bounded, no fallback); a misroute
3756
+ // still surfaces the original error.
3757
+ const lpTokenAddress = await (0, core_1.getReceiptTokenAddressOrThrow)(provider, vault, 'getPreviewRedemption:receiptToken');
3758
+ const lpTokenContract = (0, core_1.createContract)({
3759
+ address: lpTokenAddress,
3760
+ abi: abis_1.ABI_LENDING_POOL_V2,
3761
+ provider,
3762
+ });
3763
+ decimals = Number(await lpTokenContract.decimals());
3764
+ const sharesBigInt = BigInt((0, core_1.toNormalizedBn)(sharesAmount, decimals).raw);
3765
+ const [assetsAmount] = await contract.previewRedemption(sharesBigInt, false);
3766
+ assets = assetsAmount;
3767
+ }
3768
+ else {
3769
+ const contract = (0, core_1.createContract)({
3770
+ address: vault,
3771
+ abi: abis_1.ABI_LENDING_POOL_V2,
3772
+ provider,
3773
+ });
3774
+ decimals = Number(await contract.decimals());
3775
+ const sharesBigInt = BigInt((0, core_1.toNormalizedBn)(sharesAmount, decimals).raw);
3776
+ assets = await contract.previewRedeem(sharesBigInt);
3777
+ }
3778
+ return (0, core_1.toNormalizedBn)(assets, decimals);
3779
+ }
3780
+ catch (e) {
3781
+ core_1.Logger.log.error('previewRedemption', e, { vault, sharesAmount });
3782
+ throw new Error(`Failed to preview redemption for ${vault}: ${e instanceof Error ? e.message : 'Unknown error'}`);
3783
+ }
3784
+ }
3785
+ /**
3786
+ * Helper: Decode a processing transaction to extract (year, month, day).
3787
+ *
3788
+ * Note: Multicall-wrapped transactions cannot be decoded from calldata or logs
3789
+ * (the WithdrawalProcessed event doesn't include the date). When decoding fails,
3790
+ * the caller falls back to timestamp-based date estimation.
3791
+ */
3792
+ async function decodeProcessingTransaction(txHash, provider, iface) {
3793
+ const tx = await provider.getTransaction(txHash);
3794
+ if (!tx) {
3795
+ return {
3796
+ functionName: 'unknown',
3797
+ year: 0,
3798
+ month: 0,
3799
+ day: 0,
3800
+ error: 'Transaction not found',
3801
+ };
3802
+ }
3803
+ return (0, call_data_decoder_1.decodeWithdrawalProcessing)(tx.data, iface);
3804
+ }
3805
+ /**
3806
+ * Upper bound on concurrent `getTransaction` decodes inside
3807
+ * {@link getWithdrawalRequestsWithStatus}. One RPC per processed event; eight
3808
+ * in flight keeps latency near the fully-parallel case without letting a long
3809
+ * lookback window open hundreds of simultaneous requests to one endpoint.
3810
+ */
3811
+ const PROCESSED_DECODE_CONCURRENCY = 8;
3812
+ /**
3813
+ * Returns all withdrawal requests for a vault, enriched with computed claimable dates
3814
+ * and current processing status.
3815
+ *
3816
+ * The unique identifier for matching requests to processed events is:
3817
+ * (receiver, claimableDate.year, claimableDate.month, claimableDate.day)
3818
+ *
3819
+ * This is the primary method for integrators to reliably track withdrawal status
3820
+ * when multiple requests are submitted by the same receiver.
3821
+ *
3822
+ * ## Data sources
3823
+ * - **Withdrawal requests** come from the subgraph (all historical requests).
3824
+ * - **Processed events** come from on-chain `WithdrawalProcessed` logs, which are
3825
+ * limited to a finite lookback window determined by the chain's block parameters.
3826
+ *
3827
+ * ## Lookback window & filtering
3828
+ * On-chain logs only cover a limited number of recent blocks (e.g. ~20 days on Monad,
3829
+ * ~21 days on Ethereum). The `lookbackBlocks` parameter controls how far back to scan
3830
+ * and is applied uniformly to both data sources: it bounds the `WithdrawalProcessed`
3831
+ * log query *and* drops any withdrawal request whose submission timestamp falls outside
3832
+ * the window — regardless of whether a matching processed event was found. Callers who
3833
+ * need deeper history should widen `lookbackBlocks`.
3834
+ *
3835
+ * ## Status determination
3836
+ * - `'processed'` — A matching `WithdrawalProcessed` event was found (by receiver + claimable date).
3837
+ * - `'ready_to_claim'` — Claimable date has passed (within the lookback window) but no processed event found.
3838
+ * - `'pending'` — Claimable date is still in the future.
3839
+ *
3840
+ * @param params - Query parameters:
3841
+ * - `vault` — Vault address to query.
3842
+ * - `receiver` — (Optional) Filter by receiver address.
3843
+ * - `lookbackBlocks` — (Optional) On-chain log lookback window in blocks.
3844
+ * Defaults to a chain-specific value (e.g. 150,000 for Ethereum, 3,456,000 for Monad).
3845
+ * Increasing this scans more history but requires more RPC calls.
3846
+ * - `options` — Standard vault query options (provider, RPC URL, etc.).
3847
+ * @returns Array of withdrawal requests with status and claimable dates
3848
+ *
3849
+ * @worstCaseRpcCalls 1 (`lagDuration`) + the `eth_getLogs` scan in
3850
+ * `getVaultRedemptionHistory` + 1 (`eth_blockNumber`) + one `getTransaction`
3851
+ * per `WithdrawalProcessed` event in the lookback window, run at most
3852
+ * {@link PROCESSED_DECODE_CONCURRENCY} at a time. A vault with 50 processed
3853
+ * withdrawals in the window therefore makes ~53 RPC calls plus the log
3854
+ * scan, never more than 8 of them concurrently.
3855
+ *
3856
+ * @example
3857
+ * ```typescript
3858
+ * const requests = await getWithdrawalRequestsWithStatus({
3859
+ * vault: '0xVault...',
3860
+ * receiver: '0xReceiver...'
3861
+ * });
3862
+ *
3863
+ * // Results show exact status + claimable date
3864
+ * requests.forEach(req => {
3865
+ * console.log(
3866
+ * `${req.claimableDate.year}-${req.claimableDate.month}-${req.claimableDate.day}: ${req.status}`
3867
+ * );
3868
+ * });
3869
+ *
3870
+ * // With custom lookback window (e.g. 30 days on Ethereum ≈ 216,000 blocks)
3871
+ * const moreResults = await getWithdrawalRequestsWithStatus({
3872
+ * vault: '0xVault...',
3873
+ * lookbackBlocks: 216_000,
3874
+ * });
3875
+ * ```
3876
+ */
3877
+ async function getWithdrawalRequestsWithStatus(params) {
3878
+ try {
3879
+ const { vault, receiver, lookbackBlocks, options } = params;
3880
+ // Input validation
3881
+ if (!vault || !(0, ethers_1.isAddress)(vault)) {
3882
+ throw new Error('Invalid vault address');
3883
+ }
3884
+ const vaultAddress = (0, ethers_1.getAddress)(vault);
3885
+ const provider = (0, core_1.createProvider)(options.rpcUrl);
3886
+ // Fetch vault metadata and lag duration
3887
+ const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vaultAddress, options?.headers, false, false))?.[0];
3888
+ if (!tokenizedVault) {
3889
+ core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Vault not found in backend: ${vaultAddress}`);
3890
+ return [];
3891
+ }
3892
+ const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
3893
+ const isV2 = version === 'evm-2';
3894
+ const vaultAbi = isV2 ? TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOL_V2;
3895
+ const vaultContract = (0, core_1.createContract)({
3896
+ address: vaultAddress,
3897
+ abi: vaultAbi,
3898
+ provider,
3899
+ });
3900
+ // Same resolution as getVaultAvailableRedemptions: a vault that does not
3901
+ // implement `lagDuration()` has no claim lag (0). The old 86400 default
3902
+ // here put every claimable-date key one day after the one the
3903
+ // redemptions scan computed, so matching requests to processed events
3904
+ // failed and already-processed requests surfaced as `ready_to_claim`.
3905
+ const lagDuration = await readLagDuration(vaultContract, vaultAddress, options.rpcUrl);
3906
+ // Fetch subgraph requests, on-chain processed events, and the current block
3907
+ // in parallel. Current block is needed to derive the block-based lookback
3908
+ // cutoff applied uniformly to both data sources.
3909
+ const [withdrawalRequests, processedEvents, currentBlock] = await Promise.all([
3910
+ (0, vaults_1.getSubgraphWithdrawRequests)(vaultAddress, provider),
3911
+ getVaultRedemptionHistory({
3912
+ vault: vaultAddress,
3913
+ wallet: receiver,
3914
+ lookbackBlocks,
3915
+ options,
3916
+ }),
3917
+ provider.getBlockNumber(),
3918
+ ]);
3919
+ if (!withdrawalRequests.length) {
3920
+ return [];
3921
+ }
3922
+ const chainId = tokenizedVault.chain;
3923
+ const effectiveLookbackBlocks = lookbackBlocks ?? (0, core_1.determineBlockCutoff)(chainId);
3924
+ const blockCutoff = currentBlock - effectiveLookbackBlocks;
3925
+ const now = Math.floor(Date.now() / 1000);
3926
+ // Filter by receiver (if provided) and drop any request whose submission
3927
+ // block is outside the lookback window — uniformly, regardless of whether
3928
+ // a matching processed event exists. Using block_number (not timestamp)
3929
+ // keeps this consistent with the on-chain log scan's block-based cutoff.
3930
+ const filteredRequests = withdrawalRequests.filter((req) => {
3931
+ if (receiver &&
3932
+ req.receiverAddr?.toLowerCase() !== receiver.toLowerCase()) {
3933
+ return false;
3934
+ }
3935
+ return Number(req.block_number) >= blockCutoff;
3936
+ });
3937
+ // Pre-decode all processed transactions in parallel, at most
3938
+ // PROCESSED_DECODE_CONCURRENCY in flight: each decode is a
3939
+ // `getTransaction` RPC, and a vault with hundreds of processed
3940
+ // withdrawals in the lookback window would otherwise open them all at
3941
+ // once against a single endpoint.
3942
+ const iface = new ethers_1.Interface(vaultAbi);
3943
+ const decodedProcessed = await (0, concurrency_1.mapWithConcurrency)(processedEvents || [], PROCESSED_DECODE_CONCURRENCY, async (processed) => {
3944
+ // Try calldata decoding first (deterministic)
3945
+ if (processed.transactionHash) {
3946
+ try {
3947
+ const decoded = await decodeProcessingTransaction(processed.transactionHash, provider, iface);
3948
+ if (!decoded.error) {
3949
+ return {
3950
+ ...processed,
3951
+ decodedDate: {
3952
+ year: decoded.year,
3953
+ month: decoded.month,
3954
+ day: decoded.day,
3955
+ },
3956
+ };
3957
+ }
3958
+ }
3959
+ catch (e) {
3960
+ core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Failed to decode processed transaction ${processed.transactionHash}: ${e instanceof Error ? e.message : 'Unknown error'}`);
3961
+ }
3962
+ }
3963
+ // Fallback: use original request timestamp to compute claimable date,
3964
+ // or if unavailable (evm-2: requested=epoch 0), use the processed block
3965
+ // timestamp as the date directly (processing happens on the claimable date).
3966
+ const requestedEpoch = Math.floor(processed.requested.getTime() / 1000);
3967
+ if (requestedEpoch > 0) {
3968
+ // evm-1: we have the original request timestamp → compute claimable date
3969
+ const fallbackDate = (0, date_utils_1.computeClaimableDate)(requestedEpoch, lagDuration);
3970
+ return {
3971
+ ...processed,
3972
+ decodedDate: {
3973
+ year: fallbackDate.year,
3974
+ month: fallbackDate.month,
3975
+ day: fallbackDate.day,
3976
+ },
3977
+ };
3978
+ }
3979
+ // evm-2: no request timestamp — use processed block date directly
3980
+ // (processing occurs on the claimable date, so the block date IS the date)
3981
+ const processedDate = processed.processed;
3982
+ return {
3983
+ ...processed,
3984
+ decodedDate: {
3985
+ year: processedDate.getUTCFullYear(),
3986
+ month: processedDate.getUTCMonth() + 1,
3987
+ day: processedDate.getUTCDate(),
3988
+ },
3989
+ };
3990
+ });
3991
+ // Build deterministic lookup map: (receiver + year + month + day) → processed events
3992
+ // A single WithdrawalProcessed event covers the entire day's batch for a
3993
+ // receiver — all requests whose claimable date falls on that day should
3994
+ // match it. We keep an array per key only to preserve every event we saw
3995
+ // for visibility; matching is a non-destructive peek at the first entry.
3996
+ const processedMap = new Map();
3997
+ for (const processed of decodedProcessed) {
3998
+ if (!processed.decodedDate) {
3999
+ continue;
4000
+ }
4001
+ const dateKey = (0, date_utils_1.formatDateKey)(processed.decodedDate.year, processed.decodedDate.month, processed.decodedDate.day);
4002
+ const key = `${processed.receiver.toLowerCase()}:${dateKey}`;
4003
+ const existing = processedMap.get(key) ?? [];
4004
+ processedMap.set(key, [...existing, processed]);
4005
+ }
4006
+ // Compute status for each withdrawal request. The lookback window was
4007
+ // already applied up-front when pruning filteredRequests, so every request
4008
+ // reaching this loop is within the window by construction.
4009
+ const results = [];
4010
+ for (const req of filteredRequests) {
4011
+ // timestamp_ is a Unix epoch string (seconds)
4012
+ const requestTimestamp = Number(req.timestamp_);
4013
+ // Compute claimable date from timestamp + lagDuration (UTC)
4014
+ const claimableDate = (0, date_utils_1.computeClaimableDate)(requestTimestamp, lagDuration);
4015
+ // Determine status: check processed first, then claimability
4016
+ let status = 'pending';
4017
+ let processedTxHash;
4018
+ let assetsReceived;
4019
+ // Match deterministically by (receiver, year, month, day)
4020
+ const dateKey = (0, date_utils_1.formatDateKey)(claimableDate.year, claimableDate.month, claimableDate.day);
4021
+ const lookupKey = `${req.receiverAddr.toLowerCase()}:${dateKey}`;
4022
+ // Peek — do not consume. The processing event covers the whole day for
4023
+ // this receiver, so every request on that day matches the same event.
4024
+ const processedEvent = processedMap.get(lookupKey)?.[0];
4025
+ if (processedEvent) {
4026
+ status = 'processed';
4027
+ processedTxHash = processedEvent.transactionHash;
4028
+ try {
4029
+ assetsReceived = BigInt(processedEvent.amount.raw);
4030
+ }
4031
+ catch (e) {
4032
+ core_1.Logger.log.warn(`Failed to parse assets as BigInt: ${e}`);
4033
+ }
4034
+ }
4035
+ else if ((0, date_utils_1.isClaimableNow)(claimableDate.epoch, now)) {
4036
+ // Claimable date is recent (within lookback) and not yet processed
4037
+ status = 'ready_to_claim';
4038
+ }
4039
+ results.push({
4040
+ transactionHash: req.transactionHash_,
4041
+ requestTimestamp,
4042
+ shares: (() => {
4043
+ try {
4044
+ return BigInt(req.shares);
4045
+ }
4046
+ catch {
4047
+ // Handle decimal strings from subgraph (e.g. "1234.0")
4048
+ const truncated = Math.trunc(Number(req.shares));
4049
+ if (!Number.isFinite(truncated)) {
4050
+ core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Cannot parse shares value "${req.shares}" — defaulting to 0`);
4051
+ return BigInt(0);
4052
+ }
4053
+ core_1.Logger.log.info('getWithdrawalRequestsWithStatus', `Truncated decimal shares "${req.shares}" → ${truncated}`);
4054
+ return BigInt(truncated);
4055
+ }
4056
+ })(),
4057
+ receiver: req.receiverAddr,
4058
+ claimableDate: {
4059
+ year: claimableDate.year,
4060
+ month: claimableDate.month,
4061
+ day: claimableDate.day,
4062
+ },
4063
+ claimableEpoch: claimableDate.epoch,
4064
+ status,
4065
+ processedTransactionHash: processedTxHash,
4066
+ assetsReceived,
4067
+ });
4068
+ }
4069
+ return results;
4070
+ }
4071
+ catch (e) {
4072
+ core_1.Logger.log.error('getWithdrawalRequestsWithStatus', e, {
4073
+ vaultAddress: params.vault,
4074
+ hasReceiver: !!params.receiver,
4075
+ });
4076
+ throw new Error(`Failed to get withdrawal requests with status: ${e instanceof Error ? e.message : 'Unknown error'}`);
4077
+ }
4078
+ }
4079
+ //# sourceMappingURL=getters.js.map