@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.
- package/LICENSE +21 -0
- package/README.md +408 -2
- package/lib/abis/AddressResolver.d.ts +28 -0
- package/lib/abis/AddressResolver.js +23 -0
- package/lib/abis/ChainlinkV3.d.ts +87 -0
- package/lib/abis/ChainlinkV3.js +117 -0
- package/lib/abis/ERC20.d.ts +168 -0
- package/lib/abis/ERC20.js +226 -0
- package/lib/abis/ERC20_Bytes32.d.ts +139 -0
- package/lib/abis/ERC20_Bytes32.js +196 -0
- package/lib/abis/ERC4626.d.ts +364 -0
- package/lib/abis/ERC4626.js +507 -0
- package/lib/abis/ERC721.d.ts +231 -0
- package/lib/abis/ERC721.js +321 -0
- package/lib/abis/FeeOracle.d.ts +120 -0
- package/lib/abis/FeeOracle.js +162 -0
- package/lib/abis/LendingPool.d.ts +1393 -0
- package/lib/abis/LendingPool.js +1807 -0
- package/lib/abis/LendingPoolV2.d.ts +1413 -0
- package/lib/abis/LendingPoolV2.js +1833 -0
- package/lib/abis/LendingPoolV3.d.ts +1677 -0
- package/lib/abis/LendingPoolV3.js +1160 -0
- package/lib/abis/Loan.d.ts +837 -0
- package/lib/abis/Loan.js +1080 -0
- package/lib/abis/MultiAssetNativeDepositWrapper.d.ts +137 -0
- package/lib/abis/MultiAssetNativeDepositWrapper.js +125 -0
- package/lib/abis/Multicall3.d.ts +30 -0
- package/lib/abis/Multicall3.js +97 -0
- package/lib/abis/OFT.d.ts +116 -0
- package/lib/abis/OFT.js +85 -0
- package/lib/abis/PoolAdapter.d.ts +36 -0
- package/lib/abis/PoolAdapter.js +51 -0
- package/lib/abis/RewardDistributor.d.ts +267 -0
- package/lib/abis/RewardDistributor.js +352 -0
- package/lib/abis/RwaRedeemSubaccount.d.ts +747 -0
- package/lib/abis/RwaRedeemSubaccount.js +548 -0
- package/lib/abis/SmartAccount.d.ts +17 -0
- package/lib/abis/SmartAccount.js +19 -0
- package/lib/abis/SwapRouter.d.ts +953 -0
- package/lib/abis/SwapRouter.js +1228 -0
- package/lib/abis/TextResolver.d.ts +16 -0
- package/lib/abis/TextResolver.js +16 -0
- package/lib/abis/TokenizedVaultV2.d.ts +1364 -0
- package/lib/abis/TokenizedVaultV2.js +1041 -0
- package/lib/abis/TokenizedVaultV2DepositWithPermit.d.ts +1456 -0
- package/lib/abis/TokenizedVaultV2DepositWithPermit.js +1878 -0
- package/lib/abis/TokenizedVaultV2Receipt.d.ts +1568 -0
- package/lib/abis/TokenizedVaultV2Receipt.js +1061 -0
- package/lib/abis/TokenizedVaultV2SenderAllocationWhitelist.d.ts +454 -0
- package/lib/abis/TokenizedVaultV2SenderAllocationWhitelist.js +327 -0
- package/lib/abis/TokenizedVaultV2WhitelistedAllocation.d.ts +1466 -0
- package/lib/abis/TokenizedVaultV2WhitelistedAllocation.js +1092 -0
- package/lib/abis/TokenizedVaultV2WhitelistedAssets.d.ts +274 -0
- package/lib/abis/TokenizedVaultV2WhitelistedAssets.js +167 -0
- package/lib/abis/UniversalResolverResolve.d.ts +69 -0
- package/lib/abis/UniversalResolverResolve.js +35 -0
- package/lib/abis/UniversalSignatureValidator.d.ts +17 -0
- package/lib/abis/UniversalSignatureValidator.js +30 -0
- package/lib/abis/WrapperAdapter.d.ts +71 -0
- package/lib/abis/WrapperAdapter.js +77 -0
- package/lib/abis/index.d.ts +34 -0
- package/lib/abis/index.js +51 -0
- package/lib/adapters/evm/getters.d.ts +19 -0
- package/lib/adapters/evm/getters.js +249 -0
- package/lib/adapters/evm/index.d.ts +539 -0
- package/lib/adapters/evm/index.js +656 -0
- package/lib/adapters/evm/utils.d.ts +8 -0
- package/lib/adapters/evm/utils.js +51 -0
- package/lib/adapters/solana/confirm.d.ts +88 -0
- package/lib/adapters/solana/confirm.js +291 -0
- package/lib/adapters/solana/constants.d.ts +42 -0
- package/lib/adapters/solana/constants.js +59 -0
- package/lib/adapters/solana/getters.d.ts +42 -0
- package/lib/adapters/solana/getters.js +275 -0
- package/lib/adapters/solana/idl/vault-idl.d.ts +700 -0
- package/lib/adapters/solana/idl/vault-idl.js +2101 -0
- package/lib/adapters/solana/idl/withdrawal-queue-idl.d.ts +795 -0
- package/lib/adapters/solana/idl/withdrawal-queue-idl.js +1864 -0
- package/lib/adapters/solana/index.d.ts +372 -0
- package/lib/adapters/solana/index.js +459 -0
- package/lib/adapters/solana/pricing.d.ts +184 -0
- package/lib/adapters/solana/pricing.js +239 -0
- package/lib/adapters/solana/queue-hints.d.ts +29 -0
- package/lib/adapters/solana/queue-hints.js +59 -0
- package/lib/adapters/solana/queue.actions.d.ts +97 -0
- package/lib/adapters/solana/queue.actions.js +468 -0
- package/lib/adapters/solana/token-reads.d.ts +108 -0
- package/lib/adapters/solana/token-reads.js +145 -0
- package/lib/adapters/solana/types.d.ts +325 -0
- package/lib/adapters/solana/types.js +3 -0
- package/lib/adapters/solana/utils.d.ts +229 -0
- package/lib/adapters/solana/utils.js +773 -0
- package/lib/adapters/solana/vault.actions.d.ts +140 -0
- package/lib/adapters/solana/vault.actions.js +657 -0
- package/lib/adapters/solana/withdrawal-queue.d.ts +125 -0
- package/lib/adapters/solana/withdrawal-queue.js +301 -0
- package/lib/adapters/stellar/actions.d.ts +41 -0
- package/lib/adapters/stellar/actions.js +126 -0
- package/lib/adapters/stellar/constants.d.ts +43 -0
- package/lib/adapters/stellar/constants.js +53 -0
- package/lib/adapters/stellar/getters.d.ts +68 -0
- package/lib/adapters/stellar/getters.js +290 -0
- package/lib/adapters/stellar/index.d.ts +126 -0
- package/lib/adapters/stellar/index.js +187 -0
- package/lib/adapters/stellar/invocation.d.ts +55 -0
- package/lib/adapters/stellar/invocation.js +92 -0
- package/lib/adapters/stellar/soroban.d.ts +131 -0
- package/lib/adapters/stellar/soroban.js +617 -0
- package/lib/adapters/stellar/submit.d.ts +42 -0
- package/lib/adapters/stellar/submit.js +195 -0
- package/lib/adapters/stellar/types.d.ts +57 -0
- package/lib/adapters/stellar/types.js +6 -0
- package/lib/adapters/stellar/utils.d.ts +24 -0
- package/lib/adapters/stellar/utils.js +34 -0
- package/lib/adapters/sui/constants.d.ts +1 -0
- package/lib/adapters/sui/constants.js +9 -0
- package/lib/adapters/sui/getters.d.ts +24 -0
- package/lib/adapters/sui/getters.js +134 -0
- package/lib/adapters/sui/index.d.ts +53 -0
- package/lib/adapters/sui/index.js +96 -0
- package/lib/adapters/sui/transformer.d.ts +10 -0
- package/lib/adapters/sui/transformer.js +115 -0
- package/lib/adapters/sui/types.d.ts +70 -0
- package/lib/adapters/sui/types.js +3 -0
- package/lib/adapters/sui/utils.d.ts +8 -0
- package/lib/adapters/sui/utils.js +17 -0
- package/lib/core/analytics/app-name.d.ts +27 -0
- package/lib/core/analytics/app-name.js +48 -0
- package/lib/core/analytics/chain-name.d.ts +9 -0
- package/lib/core/analytics/chain-name.js +34 -0
- package/lib/core/analytics/constants.d.ts +15 -0
- package/lib/core/analytics/constants.js +28 -0
- package/lib/core/analytics/env.d.ts +46 -0
- package/lib/core/analytics/env.js +147 -0
- package/lib/core/analytics/index.d.ts +39 -0
- package/lib/core/analytics/index.js +83 -0
- package/lib/core/analytics/instrumentation.d.ts +29 -0
- package/lib/core/analytics/instrumentation.js +292 -0
- package/lib/core/analytics/method-taxonomy.d.ts +19 -0
- package/lib/core/analytics/method-taxonomy.js +156 -0
- package/lib/core/analytics/metrics.d.ts +48 -0
- package/lib/core/analytics/metrics.js +142 -0
- package/lib/core/analytics/sanitize.d.ts +57 -0
- package/lib/core/analytics/sanitize.js +272 -0
- package/lib/core/analytics/sentry-runtime.d.ts +15 -0
- package/lib/core/analytics/sentry-runtime.js +131 -0
- package/lib/core/analytics/sentry.d.ts +100 -0
- package/lib/core/analytics/sentry.js +696 -0
- package/lib/core/analytics/types.d.ts +55 -0
- package/lib/core/analytics/types.js +3 -0
- package/lib/core/analytics/user-identity.d.ts +55 -0
- package/lib/core/analytics/user-identity.js +218 -0
- package/lib/core/analytics/version.d.ts +6 -0
- package/lib/core/analytics/version.js +10 -0
- package/lib/core/attribution.d.ts +159 -0
- package/lib/core/attribution.js +194 -0
- package/lib/core/auth/index.d.ts +1 -0
- package/lib/core/auth/index.js +18 -0
- package/lib/core/auth/verify.d.ts +2 -0
- package/lib/core/auth/verify.js +35 -0
- package/lib/core/base.class.d.ts +193 -0
- package/lib/core/base.class.js +163 -0
- package/lib/core/cache.d.ts +9 -0
- package/lib/core/cache.js +31 -0
- package/lib/core/constants/adapters.d.ts +103 -0
- package/lib/core/constants/adapters.js +180 -0
- package/lib/core/constants/core.d.ts +162 -0
- package/lib/core/constants/core.js +281 -0
- package/lib/core/constants/swap-router.d.ts +180 -0
- package/lib/core/constants/swap-router.js +203 -0
- package/lib/core/constants/vaults.d.ts +93 -0
- package/lib/core/constants/vaults.js +277 -0
- package/lib/core/constants/web3.d.ts +106 -0
- package/lib/core/constants/web3.js +251 -0
- package/lib/core/errors/index.d.ts +177 -0
- package/lib/core/errors/index.js +257 -0
- package/lib/core/fetcher.d.ts +237 -0
- package/lib/core/fetcher.js +1103 -0
- package/lib/core/helpers/adapters.d.ts +13 -0
- package/lib/core/helpers/adapters.js +39 -0
- package/lib/core/helpers/chain-address.d.ts +13 -0
- package/lib/core/helpers/chain-address.js +47 -0
- package/lib/core/helpers/chain-error.d.ts +207 -0
- package/lib/core/helpers/chain-error.js +697 -0
- package/lib/core/helpers/chain-support.d.ts +98 -0
- package/lib/core/helpers/chain-support.js +139 -0
- package/lib/core/helpers/concurrency.d.ts +51 -0
- package/lib/core/helpers/concurrency.js +79 -0
- package/lib/core/helpers/core.d.ts +92 -0
- package/lib/core/helpers/core.js +368 -0
- package/lib/core/helpers/explorer-link.d.ts +16 -0
- package/lib/core/helpers/explorer-link.js +26 -0
- package/lib/core/helpers/multicall.d.ts +68 -0
- package/lib/core/helpers/multicall.js +103 -0
- package/lib/core/helpers/revert-decode.d.ts +248 -0
- package/lib/core/helpers/revert-decode.js +515 -0
- package/lib/core/helpers/signer.d.ts +61 -0
- package/lib/core/helpers/signer.js +184 -0
- package/lib/core/helpers/swap-router.d.ts +269 -0
- package/lib/core/helpers/swap-router.js +542 -0
- package/lib/core/helpers/vault-version.d.ts +24 -0
- package/lib/core/helpers/vault-version.js +83 -0
- package/lib/core/helpers/vaults.d.ts +89 -0
- package/lib/core/helpers/vaults.js +241 -0
- package/lib/core/helpers/web3.d.ts +366 -0
- package/lib/core/helpers/web3.js +1034 -0
- package/lib/core/index.d.ts +23 -0
- package/lib/core/index.js +40 -0
- package/lib/core/logger/curator-alert.d.ts +217 -0
- package/lib/core/logger/curator-alert.js +276 -0
- package/lib/core/logger/index.d.ts +99 -0
- package/lib/core/logger/index.js +163 -0
- package/lib/core/logger/slack.d.ts +49 -0
- package/lib/core/logger/slack.js +111 -0
- package/lib/core/vault-metadata.d.ts +12 -0
- package/lib/core/vault-metadata.js +42 -0
- package/lib/core/version-check.d.ts +58 -0
- package/lib/core/version-check.js +182 -0
- package/lib/evm/index.d.ts +2 -0
- package/lib/evm/index.js +19 -0
- package/lib/evm/methods/crossChainVault.d.ts +132 -0
- package/lib/evm/methods/crossChainVault.js +857 -0
- package/lib/evm/methods/crossChainVaultRegistry.d.ts +92 -0
- package/lib/evm/methods/crossChainVaultRegistry.js +239 -0
- package/lib/evm/methods/index.d.ts +2 -0
- package/lib/evm/methods/index.js +19 -0
- package/lib/evm/types/crossChain.d.ts +363 -0
- package/lib/evm/types/crossChain.js +20 -0
- package/lib/evm/types/index.d.ts +1 -0
- package/lib/evm/types/index.js +18 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +52 -0
- package/lib/main.d.ts +596 -0
- package/lib/main.js +681 -0
- package/lib/modules/api/fetcher.d.ts +108 -0
- package/lib/modules/api/fetcher.js +187 -0
- package/lib/modules/api/index.d.ts +1 -0
- package/lib/modules/api/index.js +6 -0
- package/lib/modules/api/main.d.ts +587 -0
- package/lib/modules/api/main.js +911 -0
- package/lib/modules/sub-accounts/fetcher.d.ts +53 -0
- package/lib/modules/sub-accounts/fetcher.js +96 -0
- package/lib/modules/sub-accounts/index.d.ts +2 -0
- package/lib/modules/sub-accounts/index.js +19 -0
- package/lib/modules/sub-accounts/main.d.ts +249 -0
- package/lib/modules/sub-accounts/main.js +211 -0
- package/lib/modules/sub-accounts/utils.d.ts +106 -0
- package/lib/modules/sub-accounts/utils.js +112 -0
- package/lib/modules/vaults/adapter.helpers.d.ts +84 -0
- package/lib/modules/vaults/adapter.helpers.js +202 -0
- package/lib/modules/vaults/fetcher.d.ts +147 -0
- package/lib/modules/vaults/fetcher.js +357 -0
- package/lib/modules/vaults/getters.d.ts +1060 -0
- package/lib/modules/vaults/getters.js +4079 -0
- package/lib/modules/vaults/index.d.ts +20 -0
- package/lib/modules/vaults/index.js +44 -0
- package/lib/modules/vaults/main.d.ts +607 -0
- package/lib/modules/vaults/main.js +1687 -0
- package/lib/modules/vaults/prefetch.d.ts +65 -0
- package/lib/modules/vaults/prefetch.js +120 -0
- package/lib/modules/vaults/read.actions.d.ts +406 -0
- package/lib/modules/vaults/read.actions.js +891 -0
- package/lib/modules/vaults/types.d.ts +149 -0
- package/lib/modules/vaults/types.js +3 -0
- package/lib/modules/vaults/utils/call-data-decoder.d.ts +61 -0
- package/lib/modules/vaults/utils/call-data-decoder.js +194 -0
- package/lib/modules/vaults/utils/date-utils.d.ts +50 -0
- package/lib/modules/vaults/utils/date-utils.js +84 -0
- package/lib/modules/vaults/utils.d.ts +153 -0
- package/lib/modules/vaults/utils.js +818 -0
- package/lib/modules/vaults/write.actions.d.ts +812 -0
- package/lib/modules/vaults/write.actions.js +2377 -0
- package/lib/polyfills.d.ts +1 -0
- package/lib/polyfills.js +12 -0
- package/lib/sdk.d.ts +30154 -0
- package/lib/services/coingecko/fetcher.d.ts +15 -0
- package/lib/services/coingecko/fetcher.js +67 -0
- package/lib/services/coingecko/index.d.ts +2 -0
- package/lib/services/coingecko/index.js +19 -0
- package/lib/services/coingecko/utils.d.ts +1 -0
- package/lib/services/coingecko/utils.js +24 -0
- package/lib/services/debank/fetcher.d.ts +126 -0
- package/lib/services/debank/fetcher.js +47 -0
- package/lib/services/debank/index.d.ts +2 -0
- package/lib/services/debank/index.js +19 -0
- package/lib/services/debank/utils.d.ts +50 -0
- package/lib/services/debank/utils.js +325 -0
- package/lib/services/layerzero/deposits.d.ts +55 -0
- package/lib/services/layerzero/deposits.js +174 -0
- package/lib/services/layerzero/redeems.d.ts +20 -0
- package/lib/services/layerzero/redeems.js +92 -0
- package/lib/services/layerzero/utils.d.ts +9 -0
- package/lib/services/layerzero/utils.js +22 -0
- package/lib/services/octavfi/fetcher.d.ts +33 -0
- package/lib/services/octavfi/fetcher.js +39 -0
- package/lib/services/octavfi/index.d.ts +3 -0
- package/lib/services/octavfi/index.js +20 -0
- package/lib/services/octavfi/types.d.ts +34 -0
- package/lib/services/octavfi/types.js +3 -0
- package/lib/services/octavfi/utils.d.ts +16 -0
- package/lib/services/octavfi/utils.js +244 -0
- package/lib/services/subgraph/fetcher.d.ts +2 -0
- package/lib/services/subgraph/fetcher.js +66 -0
- package/lib/services/subgraph/index.d.ts +2 -0
- package/lib/services/subgraph/index.js +19 -0
- package/lib/services/subgraph/schema.d.ts +48 -0
- package/lib/services/subgraph/schema.js +87 -0
- package/lib/services/subgraph/vaults.d.ts +83 -0
- package/lib/services/subgraph/vaults.js +1282 -0
- package/lib/services/swap-quotes/index.d.ts +105 -0
- package/lib/services/swap-quotes/index.js +77 -0
- package/lib/services/swap-quotes/paraswap.d.ts +51 -0
- package/lib/services/swap-quotes/paraswap.js +138 -0
- package/lib/services/untangled/fetcher.d.ts +40 -0
- package/lib/services/untangled/fetcher.js +110 -0
- package/lib/services/untangled/index.d.ts +3 -0
- package/lib/services/untangled/index.js +20 -0
- package/lib/services/untangled/types.d.ts +83 -0
- package/lib/services/untangled/types.js +10 -0
- package/lib/services/untangled/utils.d.ts +91 -0
- package/lib/services/untangled/utils.js +339 -0
- package/lib/types/api.d.ts +498 -0
- package/lib/types/api.js +3 -0
- package/lib/types/index.d.ts +12 -0
- package/lib/types/index.js +28 -0
- package/lib/types/points.d.ts +28 -0
- package/lib/types/points.js +3 -0
- package/lib/types/pools.d.ts +144 -0
- package/lib/types/pools.js +3 -0
- package/lib/types/staking.d.ts +28 -0
- package/lib/types/staking.js +3 -0
- package/lib/types/sub-accounts.d.ts +150 -0
- package/lib/types/sub-accounts.js +3 -0
- package/lib/types/subgraph.d.ts +84 -0
- package/lib/types/subgraph.js +3 -0
- package/lib/types/typed-contract.d.ts +102 -0
- package/lib/types/typed-contract.js +3 -0
- package/lib/types/user.d.ts +1 -0
- package/lib/types/user.js +3 -0
- package/lib/types/vaults.d.ts +886 -0
- package/lib/types/vaults.js +30 -0
- package/lib/types/web3.d.ts +81 -0
- package/lib/types/web3.js +14 -0
- package/lib/types/webserver.d.ts +730 -0
- package/lib/types/webserver.js +3 -0
- 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
|