upshift-sdk 8.20.1
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 +349 -0
- 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 +1043 -0
- package/lib/abis/SwapRouter.js +740 -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 +209 -0
- package/lib/adapters/evm/index.d.ts +353 -0
- package/lib/adapters/evm/index.js +434 -0
- package/lib/adapters/evm/utils.d.ts +8 -0
- package/lib/adapters/evm/utils.js +51 -0
- package/lib/adapters/solana/constants.d.ts +33 -0
- package/lib/adapters/solana/constants.js +52 -0
- package/lib/adapters/solana/getters.d.ts +11 -0
- package/lib/adapters/solana/getters.js +165 -0
- package/lib/adapters/solana/idl/vault-idl.d.ts +272 -0
- package/lib/adapters/solana/idl/vault-idl.js +1084 -0
- package/lib/adapters/solana/index.d.ts +233 -0
- package/lib/adapters/solana/index.js +291 -0
- package/lib/adapters/solana/types.d.ts +67 -0
- package/lib/adapters/solana/types.js +3 -0
- package/lib/adapters/solana/utils.d.ts +141 -0
- package/lib/adapters/solana/utils.js +595 -0
- package/lib/adapters/solana/vault.actions.d.ts +57 -0
- package/lib/adapters/solana/vault.actions.js +379 -0
- package/lib/adapters/stellar/actions.d.ts +28 -0
- package/lib/adapters/stellar/actions.js +77 -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 +114 -0
- package/lib/adapters/stellar/index.js +175 -0
- package/lib/adapters/stellar/soroban.d.ts +123 -0
- package/lib/adapters/stellar/soroban.js +613 -0
- package/lib/adapters/stellar/submit.d.ts +34 -0
- package/lib/adapters/stellar/submit.js +149 -0
- package/lib/adapters/stellar/types.d.ts +58 -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 +14 -0
- package/lib/adapters/sui/constants.js +29 -0
- package/lib/adapters/sui/getters.d.ts +9 -0
- package/lib/adapters/sui/getters.js +60 -0
- package/lib/adapters/sui/index.d.ts +45 -0
- package/lib/adapters/sui/index.js +101 -0
- package/lib/adapters/sui/transformer.d.ts +10 -0
- package/lib/adapters/sui/transformer.js +107 -0
- package/lib/adapters/sui/types.d.ts +66 -0
- package/lib/adapters/sui/types.js +3 -0
- package/lib/adapters/sui/utils.d.ts +10 -0
- package/lib/adapters/sui/utils.js +29 -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 +5 -0
- package/lib/core/analytics/constants.js +9 -0
- package/lib/core/analytics/env.d.ts +29 -0
- package/lib/core/analytics/env.js +59 -0
- package/lib/core/analytics/index.d.ts +36 -0
- package/lib/core/analytics/index.js +79 -0
- package/lib/core/analytics/instrumentation.d.ts +29 -0
- package/lib/core/analytics/instrumentation.js +277 -0
- package/lib/core/analytics/method-taxonomy.d.ts +19 -0
- package/lib/core/analytics/method-taxonomy.js +128 -0
- package/lib/core/analytics/metrics.d.ts +33 -0
- package/lib/core/analytics/metrics.js +116 -0
- package/lib/core/analytics/sanitize.d.ts +44 -0
- package/lib/core/analytics/sanitize.js +260 -0
- package/lib/core/analytics/sentry-runtime.d.ts +15 -0
- package/lib/core/analytics/sentry-runtime.js +97 -0
- package/lib/core/analytics/sentry.d.ts +67 -0
- package/lib/core/analytics/sentry.js +613 -0
- package/lib/core/analytics/types.d.ts +48 -0
- package/lib/core/analytics/types.js +3 -0
- package/lib/core/analytics/user-identity.d.ts +41 -0
- package/lib/core/analytics/user-identity.js +141 -0
- package/lib/core/analytics/version.d.ts +6 -0
- package/lib/core/analytics/version.js +10 -0
- package/lib/core/attribution.d.ts +111 -0
- package/lib/core/attribution.js +142 -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 +31 -0
- package/lib/core/base.class.d.ts +152 -0
- package/lib/core/base.class.js +172 -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 +133 -0
- package/lib/core/constants/core.js +209 -0
- package/lib/core/constants/swap-router.d.ts +150 -0
- package/lib/core/constants/swap-router.js +169 -0
- package/lib/core/constants/vaults.d.ts +93 -0
- package/lib/core/constants/vaults.js +273 -0
- package/lib/core/constants/web3.d.ts +91 -0
- package/lib/core/constants/web3.js +229 -0
- package/lib/core/errors/index.d.ts +114 -0
- package/lib/core/errors/index.js +183 -0
- package/lib/core/fetcher.d.ts +198 -0
- package/lib/core/fetcher.js +903 -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 +682 -0
- package/lib/core/helpers/chain-support.d.ts +80 -0
- package/lib/core/helpers/chain-support.js +115 -0
- package/lib/core/helpers/core.d.ts +68 -0
- package/lib/core/helpers/core.js +285 -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 +52 -0
- package/lib/core/helpers/signer.js +145 -0
- package/lib/core/helpers/swap-router.d.ts +211 -0
- package/lib/core/helpers/swap-router.js +480 -0
- package/lib/core/helpers/vault-version.d.ts +23 -0
- package/lib/core/helpers/vault-version.js +75 -0
- package/lib/core/helpers/vaults.d.ts +89 -0
- package/lib/core/helpers/vaults.js +235 -0
- package/lib/core/helpers/web3.d.ts +353 -0
- package/lib/core/helpers/web3.js +992 -0
- package/lib/core/index.d.ts +23 -0
- package/lib/core/index.js +40 -0
- package/lib/core/logger/index.d.ts +98 -0
- package/lib/core/logger/index.js +144 -0
- package/lib/core/logger/slack.d.ts +16 -0
- package/lib/core/logger/slack.js +57 -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 +128 -0
- package/lib/evm/methods/crossChainVault.js +853 -0
- package/lib/evm/methods/crossChainVaultRegistry.d.ts +93 -0
- package/lib/evm/methods/crossChainVaultRegistry.js +240 -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 +527 -0
- package/lib/main.js +601 -0
- package/lib/modules/api/fetcher.d.ts +82 -0
- package/lib/modules/api/fetcher.js +150 -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 +313 -0
- package/lib/modules/api/main.js +479 -0
- package/lib/modules/sub-accounts/fetcher.d.ts +53 -0
- package/lib/modules/sub-accounts/fetcher.js +120 -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 +243 -0
- package/lib/modules/sub-accounts/main.js +205 -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 +64 -0
- package/lib/modules/vaults/adapter.helpers.js +184 -0
- package/lib/modules/vaults/fetcher.d.ts +147 -0
- package/lib/modules/vaults/fetcher.js +368 -0
- package/lib/modules/vaults/getters.d.ts +570 -0
- package/lib/modules/vaults/getters.js +3051 -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 +601 -0
- package/lib/modules/vaults/main.js +1623 -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 +225 -0
- package/lib/modules/vaults/read.actions.js +596 -0
- package/lib/modules/vaults/types.d.ts +71 -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 +140 -0
- package/lib/modules/vaults/utils.js +799 -0
- package/lib/modules/vaults/write.actions.d.ts +529 -0
- package/lib/modules/vaults/write.actions.js +1749 -0
- package/lib/polyfills.d.ts +1 -0
- package/lib/polyfills.js +12 -0
- package/lib/sdk.d.ts +26721 -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 +38 -0
- package/lib/services/debank/utils.js +297 -0
- package/lib/services/layerzero/deposits.d.ts +49 -0
- package/lib/services/layerzero/deposits.js +166 -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 +9 -0
- package/lib/services/octavfi/fetcher.js +103 -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 +212 -0
- package/lib/services/subgraph/fetcher.d.ts +2 -0
- package/lib/services/subgraph/fetcher.js +61 -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 +45 -0
- package/lib/services/subgraph/schema.js +75 -0
- package/lib/services/subgraph/vaults.d.ts +25 -0
- package/lib/services/subgraph/vaults.js +1106 -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/types/api.d.ts +280 -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 +140 -0
- package/lib/types/sub-accounts.js +3 -0
- package/lib/types/subgraph.d.ts +74 -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 +714 -0
- package/lib/types/vaults.js +24 -0
- package/lib/types/web3.d.ts +51 -0
- package/lib/types/web3.js +14 -0
- package/lib/types/webserver.d.ts +698 -0
- package/lib/types/webserver.js +3 -0
- package/package.json +78 -0
|
@@ -0,0 +1,3051 @@
|
|
|
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.sumDepositsAtFaceValue = sumDepositsAtFaceValue;
|
|
58
|
+
exports.getVaultUserLifetimePnl = getVaultUserLifetimePnl;
|
|
59
|
+
exports.getVaultPnl = getVaultPnl;
|
|
60
|
+
exports.getVaultHistoricalTimeseries = getVaultHistoricalTimeseries;
|
|
61
|
+
exports.getVaultAnnualizedApy = getVaultAnnualizedApy;
|
|
62
|
+
exports.getVaultSummary = getVaultSummary;
|
|
63
|
+
exports.getVaultWithdrawals = getVaultWithdrawals;
|
|
64
|
+
exports.getVaultPendingRedemptions = getVaultPendingRedemptions;
|
|
65
|
+
exports.getPreviewRedemption = getPreviewRedemption;
|
|
66
|
+
exports.getWithdrawalRequestsWithStatus = getWithdrawalRequestsWithStatus;
|
|
67
|
+
const abis_1 = require("../../abis");
|
|
68
|
+
const types_1 = require("../../types");
|
|
69
|
+
const core_1 = require("../../core");
|
|
70
|
+
const ethers_1 = require("ethers");
|
|
71
|
+
const utils_1 = require("./utils");
|
|
72
|
+
const subgraph_1 = require("../../services/subgraph");
|
|
73
|
+
const vaults_1 = require("../../services/subgraph/vaults");
|
|
74
|
+
const debank_1 = require("../../services/debank");
|
|
75
|
+
const utils_2 = require("../../adapters/solana/utils");
|
|
76
|
+
const SolanaGetters = __importStar(require("../../adapters/solana/getters"));
|
|
77
|
+
const SolanaConstants = __importStar(require("../../adapters/solana/constants"));
|
|
78
|
+
const StellarGetters = __importStar(require("../../adapters/stellar/getters"));
|
|
79
|
+
const utils_3 = require("../../adapters/stellar/utils");
|
|
80
|
+
const constants_1 = require("../../adapters/stellar/constants");
|
|
81
|
+
const EVM = __importStar(require("../../adapters/evm"));
|
|
82
|
+
const TokenizedVaultV2_1 = require("../../abis/TokenizedVaultV2");
|
|
83
|
+
const TokenizedVaultV2Receipt_1 = require("../../abis/TokenizedVaultV2Receipt");
|
|
84
|
+
const ethereum_block_by_date_1 = __importDefault(require("ethereum-block-by-date"));
|
|
85
|
+
const octavfi_1 = require("../../services/octavfi");
|
|
86
|
+
const date_utils_1 = require("./utils/date-utils");
|
|
87
|
+
const call_data_decoder_1 = require("./utils/call-data-decoder");
|
|
88
|
+
const deposits_1 = require("../../services/layerzero/deposits");
|
|
89
|
+
const redeems_1 = require("../../services/layerzero/redeems");
|
|
90
|
+
const errors_1 = require("../../core/errors");
|
|
91
|
+
/**
|
|
92
|
+
* Vault Data Getters
|
|
93
|
+
*
|
|
94
|
+
* Main query functions for vault data across chains:
|
|
95
|
+
* - getVault: Comprehensive vault details with loans/allocations
|
|
96
|
+
* - getVaultLoans: Active loan data with on-chain enrichment
|
|
97
|
+
* - getVaultAllocations: DeFi/CeFi/OTC allocation breakdowns
|
|
98
|
+
* - getVaultAvailableRedemptions: Claimable redemption requests
|
|
99
|
+
* - getVaultRedemptionHistory: Historical redemption data
|
|
100
|
+
* - getVaultPositions: User positions across vaults
|
|
101
|
+
* - getVaultApy: @deprecated Current and historical APY data
|
|
102
|
+
* - getRewardsStakingPositions: Staking positions and rewards
|
|
103
|
+
* - getVaultTvl: Current and historical TVL
|
|
104
|
+
* - getVaultHistoricalTimeseries: Time series data for vault metrics
|
|
105
|
+
*/
|
|
106
|
+
/**
|
|
107
|
+
* Fetch comprehensive vault data including on-chain state and backend metadata.
|
|
108
|
+
* Routes to appropriate chain adapter (EVM v1/v2, Solana, or Stellar) based on vault version.
|
|
109
|
+
* Optionally enriches with loan and allocation data.
|
|
110
|
+
* @param vault - Vault contract address (EVM hex, Stellar C-address, or Solana program ID)
|
|
111
|
+
* @param loans - Include active loan data
|
|
112
|
+
* @param allocations - Include DeFi/CeFi allocation breakdowns
|
|
113
|
+
* @param options - RPC and service configuration
|
|
114
|
+
* @returns Complete vault object with optional enrichments
|
|
115
|
+
*/
|
|
116
|
+
async function getVault({ vault, loans = false, allocations = false, options, loadSubaccounts, loadSnapshots, }) {
|
|
117
|
+
let returnedVault;
|
|
118
|
+
try {
|
|
119
|
+
const tokenizedVaultRaw = await (0, core_1.fetchTokenizedVault)(vault, undefined, loadSubaccounts, loadSnapshots);
|
|
120
|
+
const tokenizedVault = tokenizedVaultRaw?.[0];
|
|
121
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
122
|
+
switch (vaultVersion) {
|
|
123
|
+
case 'sol-0': {
|
|
124
|
+
returnedVault = await SolanaGetters.getSolanaVault(tokenizedVault, options);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'stellar-0': {
|
|
128
|
+
returnedVault = await StellarGetters.getStellarVault(tokenizedVault, options);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'evm-2': {
|
|
132
|
+
returnedVault = await EVM.getEvmVaultV2(tokenizedVault, options);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
default: {
|
|
136
|
+
returnedVault = await EVM.getEvmVaultV1(tokenizedVault, options);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (returnedVault === null) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
core_1.Logger.log.error('getVault', err, { vault });
|
|
146
|
+
throw new Error(`#getVault::${vault}: ${err?.message}`);
|
|
147
|
+
}
|
|
148
|
+
// Loans/allocations enrichment is only supported for EVM vaults.
|
|
149
|
+
// Non-EVM vaults already include empty defaults.
|
|
150
|
+
const isEvmVault = returnedVault.version !== 'sol-0' &&
|
|
151
|
+
returnedVault.version !== 'stellar-0' &&
|
|
152
|
+
returnedVault.version !== 'sui-0';
|
|
153
|
+
if (!isEvmVault && (loans || allocations)) {
|
|
154
|
+
core_1.Logger.log.warn('getVault', 'Loans/allocations enrichment is not supported for non-EVM vaults — skipping', { vault, version: returnedVault.version });
|
|
155
|
+
}
|
|
156
|
+
if (isEvmVault && !(0, core_1.isBadVault)(vault) && (loans || allocations)) {
|
|
157
|
+
const [loansResult, allocationsResult] = await Promise.allSettled([
|
|
158
|
+
loans
|
|
159
|
+
? getVaultLoans(returnedVault, options)
|
|
160
|
+
: Promise.resolve(undefined),
|
|
161
|
+
allocations
|
|
162
|
+
? getVaultAllocations(vault, options)
|
|
163
|
+
: Promise.resolve(undefined),
|
|
164
|
+
]);
|
|
165
|
+
if (loans) {
|
|
166
|
+
if (loansResult.status === 'fulfilled') {
|
|
167
|
+
returnedVault = {
|
|
168
|
+
...returnedVault,
|
|
169
|
+
loans: loansResult.value,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
core_1.Logger.log.error('getVault:loans', loansResult.reason, {
|
|
174
|
+
symbol: returnedVault.receipt.symbol,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (allocations) {
|
|
179
|
+
if (allocationsResult.status === 'fulfilled') {
|
|
180
|
+
returnedVault = {
|
|
181
|
+
...returnedVault,
|
|
182
|
+
allocations: allocationsResult.value,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
core_1.Logger.log.error('getVault:allocations', allocationsResult.reason, {
|
|
187
|
+
symbol: returnedVault.receipt.symbol,
|
|
188
|
+
});
|
|
189
|
+
throw new Error('Failure fetching debank res');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return returnedVault;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Vault Loans
|
|
197
|
+
*/
|
|
198
|
+
async function getVaultLoans(vault, options) {
|
|
199
|
+
// sanitize
|
|
200
|
+
const vaultAddress = typeof vault === 'string' ? vault : vault.address;
|
|
201
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vaultAddress, options?.headers))?.[0];
|
|
202
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
203
|
+
if (vaultVersion !== 'evm-0')
|
|
204
|
+
return []; // only evm-0 uses subaccount loans
|
|
205
|
+
try {
|
|
206
|
+
let poolTotalSupply;
|
|
207
|
+
let poolDecimals;
|
|
208
|
+
const chainId = options?.chainId || tokenizedVault?.chain;
|
|
209
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
210
|
+
const tokenizedVaultLoans = await (0, core_1.fetchTokenizedVaultLoans)(vaultAddress, chainId);
|
|
211
|
+
// if user passed IAddress or a full IVault
|
|
212
|
+
if (typeof vault === 'string') {
|
|
213
|
+
// fetch necessary data
|
|
214
|
+
const poolContract = (0, core_1.createContract)({
|
|
215
|
+
provider,
|
|
216
|
+
address: vaultAddress,
|
|
217
|
+
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
218
|
+
});
|
|
219
|
+
[poolTotalSupply, poolDecimals] = await Promise.all([
|
|
220
|
+
poolContract.totalSupply(),
|
|
221
|
+
(0, core_1.getDecimals)(provider, vaultAddress),
|
|
222
|
+
]);
|
|
223
|
+
poolTotalSupply = BigInt(poolTotalSupply).toString();
|
|
224
|
+
poolDecimals = Number(poolDecimals);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
poolTotalSupply = vault.totalSupply.raw;
|
|
228
|
+
poolDecimals = vault.decimals;
|
|
229
|
+
}
|
|
230
|
+
// fetch and format all the loans
|
|
231
|
+
const newLoans = (await Promise.all((Array.isArray(tokenizedVaultLoans) ? tokenizedVaultLoans : [])?.map(async (l) => {
|
|
232
|
+
// variables
|
|
233
|
+
const borrower = l.borrower;
|
|
234
|
+
// calculate allocation
|
|
235
|
+
const allocation = l.principal_amount /
|
|
236
|
+
Number((0, ethers_1.formatUnits)(poolTotalSupply, poolDecimals));
|
|
237
|
+
// get august loan fee from oracle
|
|
238
|
+
const loanFeeRate = await (0, core_1.getLoanOracleFeeRate)(provider, 'LOAN.REPAY.INTERESTS', l.address, chainId);
|
|
239
|
+
// calculate loan level apr
|
|
240
|
+
const loanApr = Number(l.apr || 0) / 100;
|
|
241
|
+
const loanAprAfterFees = loanApr * (1 - loanFeeRate / 100);
|
|
242
|
+
// determine if capital is deployed
|
|
243
|
+
const isIdleCapital = core_1.IDLE_CAPITAL_BORROWER_ADDRESS.includes(borrower);
|
|
244
|
+
// build new obj
|
|
245
|
+
const newLoanObj = {
|
|
246
|
+
vault: vaultAddress,
|
|
247
|
+
address: l.address,
|
|
248
|
+
lender: l.lender,
|
|
249
|
+
borrower: l.borrower,
|
|
250
|
+
state: l.state,
|
|
251
|
+
totalRepaid: l.total_repaid,
|
|
252
|
+
principalToken: l.principal_token,
|
|
253
|
+
principalAmount: l.principal_amount,
|
|
254
|
+
interestAmount: l.interest_amount,
|
|
255
|
+
upcomingPayment: {
|
|
256
|
+
amount: l.upcoming_payment.amount,
|
|
257
|
+
dueDate: l.upcoming_payment.due_date,
|
|
258
|
+
},
|
|
259
|
+
apr: loanAprAfterFees,
|
|
260
|
+
initialPrincipalAmount: l.initial_principal_amount,
|
|
261
|
+
deployedDate: l.deployed_date,
|
|
262
|
+
isIdleCapital,
|
|
263
|
+
paymentInterval: l.payment_interval,
|
|
264
|
+
allocation,
|
|
265
|
+
};
|
|
266
|
+
return newLoanObj;
|
|
267
|
+
}))).filter((l) => l !== undefined);
|
|
268
|
+
return newLoans;
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
core_1.Logger.log.error('getVaultLoans', e, { vault });
|
|
272
|
+
throw new Error(`#getVaultLoans::${vault}:${e?.message}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Vault Subaccount Loans
|
|
277
|
+
*/
|
|
278
|
+
async function getVaultSubaccountLoans(vault, options) {
|
|
279
|
+
// sanitize
|
|
280
|
+
const vaultAddress = typeof vault === 'string' ? vault : vault.address;
|
|
281
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vaultAddress, options?.headers))?.[0];
|
|
282
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
283
|
+
if (vaultVersion !== 'evm-0')
|
|
284
|
+
return [];
|
|
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 (0, core_1.fetchTokenizedVaultSubaccountLoans)(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 august 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('getVaultSubaccountLoans', e, { vault });
|
|
352
|
+
throw new Error(`#getVaultSubaccountLoans::${vault}:${e?.message}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* HTTP statuses that mean "no such record for this subaccount", not "something
|
|
357
|
+
* broke". The backend returns 404 for a subaccount with no CeFi/OTC position,
|
|
358
|
+
* which is the normal case for most vault borrowers.
|
|
359
|
+
*/
|
|
360
|
+
const EXPECTED_SUBACCOUNT_FETCH_STATUSES = new Set([204, 404]);
|
|
361
|
+
/**
|
|
362
|
+
* Log a per-subaccount enrichment fetch failure at the right severity.
|
|
363
|
+
*
|
|
364
|
+
* Why: `getVaultAllocations` issues one CeFi and one OTC request per borrower
|
|
365
|
+
* on every call. Logging the "no record" responses at `error` level captured
|
|
366
|
+
* them as Sentry issues, producing thousands of `AugustServerError: Request
|
|
367
|
+
* failed: 404` events a day for a condition that is not a fault and that
|
|
368
|
+
* callers never need to act on. Expected statuses become breadcrumbs; genuine
|
|
369
|
+
* failures (5xx, auth, transport) stay at `error`.
|
|
370
|
+
*
|
|
371
|
+
* @param tag - Call-site tag, e.g. `getVaultAllocations:cefi`. Becomes the
|
|
372
|
+
* `sdk.origin` Sentry tag for grouping.
|
|
373
|
+
* @param status - HTTP status, or `undefined` when the failure was a thrown
|
|
374
|
+
* transport/typed error with no status attached.
|
|
375
|
+
* @param detail - Status text or error to attach as context.
|
|
376
|
+
* @param context - Identifiers scoping the failure (`vault`, `borrower`).
|
|
377
|
+
*/
|
|
378
|
+
function logSubaccountFetchFailure(tag, status, detail, context) {
|
|
379
|
+
if (status !== undefined && EXPECTED_SUBACCOUNT_FETCH_STATUSES.has(status)) {
|
|
380
|
+
core_1.Logger.log.warn(tag, { ...context, status, reason: 'no record' });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
core_1.Logger.log.error(tag, detail, { ...context, status });
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Extract the HTTP status from a thrown fetch error, when it carries one.
|
|
387
|
+
*
|
|
388
|
+
* `fetchAugustWithKey` throws `AugustServerError`/`AugustAuthError` for non-2xx
|
|
389
|
+
* responses, so the status-code branch in the callers is unreachable for those
|
|
390
|
+
* — the status has to be recovered from the error to classify it.
|
|
391
|
+
*
|
|
392
|
+
* @param error - Any thrown value.
|
|
393
|
+
* @returns The HTTP status, or `undefined` for errors without one (network
|
|
394
|
+
* failures, aborts, programming errors).
|
|
395
|
+
*/
|
|
396
|
+
function httpStatusFromError(error) {
|
|
397
|
+
const status = error?.status;
|
|
398
|
+
return typeof status === 'number' ? status : undefined;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Vault Allocations
|
|
402
|
+
*/
|
|
403
|
+
/**
|
|
404
|
+
* Fetch comprehensive vault asset allocation breakdown.
|
|
405
|
+
* Includes DeFi protocols (via DeBank), CeFi balances, OTC positions, and loans.
|
|
406
|
+
* Categorizes exposures by borrowing, supplying, lending, and wallet holdings.
|
|
407
|
+
* @param vault - Vault address
|
|
408
|
+
* @param options - RPC configuration
|
|
409
|
+
* @returns Detailed allocation data with exposure categorization
|
|
410
|
+
*/
|
|
411
|
+
async function getVaultAllocations(vault, options) {
|
|
412
|
+
// to be used on upshift UI
|
|
413
|
+
const protocolExposure = [];
|
|
414
|
+
const tokenExposure = [];
|
|
415
|
+
let cefiExposure = [];
|
|
416
|
+
const otcPositions = {};
|
|
417
|
+
const defiPerBorrower = {};
|
|
418
|
+
const exposurePerCategory = {
|
|
419
|
+
supplying: [],
|
|
420
|
+
borrowing: [],
|
|
421
|
+
wallet: [],
|
|
422
|
+
walletBalance: 0,
|
|
423
|
+
cefiBalance: 0,
|
|
424
|
+
defiBalance: 0,
|
|
425
|
+
lending: [],
|
|
426
|
+
loanBalance: 0,
|
|
427
|
+
};
|
|
428
|
+
const netValue = { value: 0 };
|
|
429
|
+
let debankErr = false;
|
|
430
|
+
let unfilteredTokens = [];
|
|
431
|
+
const borrowerPortfolioFetchers = {
|
|
432
|
+
solana: async (borrower) => {
|
|
433
|
+
const portfolios = await (0, octavfi_1.fetchOctavfiPortfolios)([borrower]);
|
|
434
|
+
return portfolios.length > 0
|
|
435
|
+
? (0, octavfi_1.transformOctavfiToDebank)(portfolios[0])
|
|
436
|
+
: null;
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
// Determine wallets to fetch allocation responses from
|
|
440
|
+
let uniqueBorrowers = [];
|
|
441
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers))?.[0];
|
|
442
|
+
// fetch all vault unique subaccounts/loan borrowers
|
|
443
|
+
try {
|
|
444
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
445
|
+
if (vaultVersion === 'evm-0') {
|
|
446
|
+
const loans = await getVaultLoans(vault, options);
|
|
447
|
+
uniqueBorrowers = [...new Set(loans.map((l) => l.borrower))];
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
const eoa_operators = tokenizedVault.eoa_operators.map((s) => s.address);
|
|
451
|
+
uniqueBorrowers = [
|
|
452
|
+
...tokenizedVault.subaccounts.map((s) => s.address),
|
|
453
|
+
...eoa_operators,
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch (e) {
|
|
458
|
+
core_1.Logger.log.error('getVaultAllocations:borrowers', e);
|
|
459
|
+
}
|
|
460
|
+
// if hardcoded subaccount exists for vault, use that
|
|
461
|
+
const hardcodedSubaccount = Object.values(core_1.VAULT_ALLOCATION_SUBACCOUNTS).find((entry) => entry.address.toLowerCase() === vault.toLowerCase() && !entry.useDebank);
|
|
462
|
+
if (hardcodedSubaccount) {
|
|
463
|
+
const debankRes = await (0, debank_1.fetchDebankResponse)(hardcodedSubaccount.subaccount);
|
|
464
|
+
if (debankRes === false) {
|
|
465
|
+
debankErr = true;
|
|
466
|
+
}
|
|
467
|
+
// pull data from debank results
|
|
468
|
+
(0, debank_1.parseVaultLevelDebank)(debankRes, protocolExposure, tokenExposure, hardcodedSubaccount.subaccount, exposurePerCategory, netValue);
|
|
469
|
+
unfilteredTokens = debankRes?.subaccount?.tokens;
|
|
470
|
+
const debankPerLoan = (0, debank_1.parseLoanLevelDebank)(debankRes);
|
|
471
|
+
defiPerBorrower[hardcodedSubaccount.subaccount] = debankPerLoan;
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// Fetch DeBank data for all subaccounts in a single request (EVM only)
|
|
475
|
+
if ((0, core_1.getAddressChainType)(vault) === 'evm') {
|
|
476
|
+
const vaultDebankData = await (0, debank_1.fetchVaultDebankResponse)(vault, options?.headers);
|
|
477
|
+
if (vaultDebankData === false) {
|
|
478
|
+
debankErr = true;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// Process DeBank data for each subaccount
|
|
482
|
+
for (const [subaccountAddress, subaccountData] of Object.entries(vaultDebankData)) {
|
|
483
|
+
// Skip if subaccount has an error
|
|
484
|
+
if ('error' in subaccountData) {
|
|
485
|
+
core_1.Logger.log.error('getVaultAllocations:debank:subaccount', {
|
|
486
|
+
subaccountAddress,
|
|
487
|
+
error: subaccountData.error,
|
|
488
|
+
});
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
// Transform to match existing parser format: { subaccount: { positions, app_positions, tokens } }
|
|
492
|
+
const debankRes = {
|
|
493
|
+
subaccount: {
|
|
494
|
+
positions: subaccountData.positions || [],
|
|
495
|
+
app_positions: subaccountData.app_positions || [],
|
|
496
|
+
tokens: subaccountData.tokens || [],
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
(0, debank_1.parseVaultLevelDebank)(debankRes, protocolExposure, tokenExposure, subaccountAddress, exposurePerCategory, netValue);
|
|
500
|
+
unfilteredTokens = debankRes?.subaccount?.tokens;
|
|
501
|
+
const debankPerLoan = (0, debank_1.parseLoanLevelDebank)(debankRes);
|
|
502
|
+
defiPerBorrower[subaccountAddress] = debankPerLoan;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Fetch portfolio data for non-EVM borrowers (EVM handled by fetchVaultDebankResponse above).
|
|
507
|
+
// CeFi and OTC fetches below run for ALL borrowers regardless of chain type.
|
|
508
|
+
for (const borrower of uniqueBorrowers) {
|
|
509
|
+
const chainType = (0, core_1.getAddressChainType)(borrower);
|
|
510
|
+
if (chainType !== 'evm') {
|
|
511
|
+
const fetcher = borrowerPortfolioFetchers[chainType];
|
|
512
|
+
if (fetcher) {
|
|
513
|
+
try {
|
|
514
|
+
const debankRes = await fetcher(borrower);
|
|
515
|
+
if (debankRes) {
|
|
516
|
+
(0, debank_1.parseVaultLevelDebank)(debankRes, protocolExposure, tokenExposure, borrower, exposurePerCategory, netValue);
|
|
517
|
+
unfilteredTokens = debankRes?.subaccount?.tokens;
|
|
518
|
+
defiPerBorrower[borrower] = (0, debank_1.parseLoanLevelDebank)(debankRes);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
debankErr = true;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
core_1.Logger.log.error(`getVaultAllocations:${chainType}`, error, {
|
|
526
|
+
vault,
|
|
527
|
+
borrower,
|
|
528
|
+
});
|
|
529
|
+
debankErr = true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// Not an error: portfolio providers simply have no coverage for this
|
|
534
|
+
// chain type yet (Stellar, Sui). Reporting it as an exception filled
|
|
535
|
+
// Sentry with a known, permanent gap, and setting `debankErr` made
|
|
536
|
+
// every Stellar vault throw `failure to fetch debank response`
|
|
537
|
+
// instead of returning the CeFi/OTC/loan allocations it did resolve.
|
|
538
|
+
core_1.Logger.log.warn(`getVaultAllocations:no_fetcher`, {
|
|
539
|
+
vault,
|
|
540
|
+
borrower,
|
|
541
|
+
chainType,
|
|
542
|
+
reason: `No portfolio fetcher for chain type: ${chainType}`,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// fetch cefi_positions
|
|
547
|
+
try {
|
|
548
|
+
const cefiResponse = await (0, core_1.fetchAugustWithKey)(options.augustKey, core_1.WEBSERVER_ENDPOINTS.subaccount.cefi(borrower), { headers: options?.headers });
|
|
549
|
+
if (cefiResponse.status !== 200) {
|
|
550
|
+
// 404 means "this subaccount has no CeFi position", which is the
|
|
551
|
+
// common case for most borrowers — a breadcrumb, not an exception.
|
|
552
|
+
logSubaccountFetchFailure('getVaultAllocations:cefi', cefiResponse.status, cefiResponse.statusText, { vault, borrower });
|
|
553
|
+
}
|
|
554
|
+
if (cefiResponse.status === 200) {
|
|
555
|
+
const cefiRes = (await cefiResponse.json());
|
|
556
|
+
cefiExposure = [...cefiExposure, ...cefiRes];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (e) {
|
|
560
|
+
logSubaccountFetchFailure('getVaultAllocations:cefi', httpStatusFromError(e), e, { vault, borrower });
|
|
561
|
+
}
|
|
562
|
+
// fetch otc_positions
|
|
563
|
+
try {
|
|
564
|
+
const otcResponse = await (0, core_1.fetchAugustWithKey)(options.augustKey, core_1.WEBSERVER_ENDPOINTS.subaccount.otc_positions(borrower), { headers: options?.headers });
|
|
565
|
+
if (otcResponse.status !== 200) {
|
|
566
|
+
// 404 means "this subaccount has no OTC positions" — expected for
|
|
567
|
+
// nearly every borrower. See logSubaccountFetchFailure.
|
|
568
|
+
logSubaccountFetchFailure('getVaultAllocations:otc', otcResponse.status, otcResponse.statusText, { vault, borrower });
|
|
569
|
+
}
|
|
570
|
+
if (otcResponse.status === 200) {
|
|
571
|
+
const otcRes = (await otcResponse.json());
|
|
572
|
+
otcPositions[borrower] = otcRes;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
catch (e) {
|
|
576
|
+
logSubaccountFetchFailure('getVaultAllocations:otc', httpStatusFromError(e), e, { vault, borrower });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const walletBalance = exposurePerCategory.wallet.reduce((sum, item) => sum + item.amount, 0);
|
|
581
|
+
const defiBalance = protocolExposure.reduce((sum, item) => sum + item.netUsdValue, 0);
|
|
582
|
+
const cefiBalance = cefiExposure.reduce((sum, item) => sum + item.value, 0);
|
|
583
|
+
exposurePerCategory.walletBalance = walletBalance;
|
|
584
|
+
exposurePerCategory.defiBalance = defiBalance;
|
|
585
|
+
exposurePerCategory.cefiBalance = cefiBalance;
|
|
586
|
+
netValue.value += cefiBalance;
|
|
587
|
+
if (debankErr) {
|
|
588
|
+
throw Error('failure to fetch debank response');
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
defi: protocolExposure,
|
|
592
|
+
cefi: cefiExposure,
|
|
593
|
+
otc: otcPositions,
|
|
594
|
+
tokens: tokenExposure,
|
|
595
|
+
defiPerBorrower,
|
|
596
|
+
exposurePerCategory,
|
|
597
|
+
netValue: netValue.value,
|
|
598
|
+
// this would be the debank token response field without any parsing. Different from
|
|
599
|
+
// tokens. Could use a better name tho
|
|
600
|
+
unfilteredTokens: unfilteredTokens,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Coerce a subgraph numeric field to `bigint`, treating absent fields as zero.
|
|
605
|
+
*
|
|
606
|
+
* Why: the evm-1 and evm-2 subgraphs expose different field names on
|
|
607
|
+
* `WithdrawalRequested` — evm-1 emits `assets` plus explicit `year`/`month`/
|
|
608
|
+
* `day`, evm-2 emits `shares` and a raw `timestamp_`. Reading the wrong one
|
|
609
|
+
* yields `undefined`, and bare `BigInt(undefined)` throws
|
|
610
|
+
* `TypeError: Cannot convert undefined to a BigInt`, which aborted the whole
|
|
611
|
+
* redemption scan and returned an empty list to the caller (the second
|
|
612
|
+
* highest-volume SDK error in production).
|
|
613
|
+
*
|
|
614
|
+
* `BigInt(x) || BigInt(0)` does **not** protect against this — the throw
|
|
615
|
+
* happens before `||` is ever evaluated.
|
|
616
|
+
*
|
|
617
|
+
* @param value - Subgraph field: decimal string, number, bigint, `null`, or
|
|
618
|
+
* `undefined`.
|
|
619
|
+
* @returns The value as `bigint`, or `0n` when absent or non-numeric.
|
|
620
|
+
*/
|
|
621
|
+
function subgraphAmountToBigInt(value) {
|
|
622
|
+
if (value === undefined || value === null || value === '')
|
|
623
|
+
return BigInt(0);
|
|
624
|
+
try {
|
|
625
|
+
return BigInt(value);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return BigInt(0);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async function getVaultAvailableRedemptions({ vault, wallet, options, prefetchedReads, }) {
|
|
632
|
+
try {
|
|
633
|
+
// Stellar vaults don't support on-chain redemptions yet
|
|
634
|
+
if ((0, utils_3.isStellarAddress)(vault)) {
|
|
635
|
+
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Available redemptions is not yet supported for Stellar vaults: ${vault}`);
|
|
636
|
+
return {
|
|
637
|
+
availableRedemptions: [],
|
|
638
|
+
pendingRedemptions: [],
|
|
639
|
+
...(options.verbose && {
|
|
640
|
+
processedWithdrawals: [],
|
|
641
|
+
requestedWithdrawals: [],
|
|
642
|
+
}),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
// setup
|
|
646
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
647
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
|
|
648
|
+
if (!tokenizedVault) {
|
|
649
|
+
return {
|
|
650
|
+
...{
|
|
651
|
+
availableRedemptions: [],
|
|
652
|
+
pendingRedemptions: [],
|
|
653
|
+
},
|
|
654
|
+
...(options.verbose && {
|
|
655
|
+
processedWithdrawals: [],
|
|
656
|
+
requestedWithdrawals: [],
|
|
657
|
+
}),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
// version split logic
|
|
661
|
+
const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
662
|
+
let decimals;
|
|
663
|
+
// Use ABI_LENDING_POOL_V2 for both versions as it has the methods we need
|
|
664
|
+
const vaultContract = (0, core_1.createContract)({
|
|
665
|
+
address: vault,
|
|
666
|
+
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
667
|
+
provider,
|
|
668
|
+
});
|
|
669
|
+
if (version === 'evm-2') {
|
|
670
|
+
decimals = await (0, core_1.getDecimals)(provider, await (0, core_1.getReceiptTokenAddress)(provider, vault), false);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
decimals = await (0, core_1.getDecimals)(provider, vault);
|
|
674
|
+
}
|
|
675
|
+
const lagDuration = typeof prefetchedReads?.lagDuration === 'number'
|
|
676
|
+
? prefetchedReads.lagDuration
|
|
677
|
+
: Number(await vaultContract.lagDuration());
|
|
678
|
+
const { withdrawalRequesteds, withdrawalProcesseds } = await (0, subgraph_1.getSubgraphAllWithdrawals)(vault, provider);
|
|
679
|
+
// format
|
|
680
|
+
const availableRedemptions = [];
|
|
681
|
+
const pendingRedemptions = [];
|
|
682
|
+
for (const ev of withdrawalRequesteds) {
|
|
683
|
+
// ensure event is defined
|
|
684
|
+
if (!ev || typeof ev !== 'object') {
|
|
685
|
+
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Skipping invalid event: ${ev}`);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
let month = 0;
|
|
689
|
+
let day = 0;
|
|
690
|
+
let year = 0;
|
|
691
|
+
if (version === 'evm-2') {
|
|
692
|
+
// Use UTC-based computeClaimableDate to mirror contract logic exactly
|
|
693
|
+
const claimable = (0, date_utils_1.computeClaimableDate)(Number(ev.timestamp_), lagDuration);
|
|
694
|
+
year = claimable.year;
|
|
695
|
+
month = claimable.month;
|
|
696
|
+
day = claimable.day;
|
|
697
|
+
}
|
|
698
|
+
// Validate required event properties
|
|
699
|
+
if (!ev.year || !ev.month || !ev.day || !ev.receiverAddr) {
|
|
700
|
+
if (version !== 'evm-2') {
|
|
701
|
+
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Skipping event with missing required properties: ${ev}`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
if (!year || !month || !day || !ev.receiverAddr) {
|
|
706
|
+
if (version !== 'evm-2') {
|
|
707
|
+
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Skipping event with missing required properties: ${ev}`);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const fullDate = version === 'evm-2'
|
|
714
|
+
? new Date(Date.UTC(year, month - 1, day))
|
|
715
|
+
: new Date(Number(ev.year), Number(ev.month) - 1, Number(ev.day));
|
|
716
|
+
// Match request to processed event using deterministic UTC date key
|
|
717
|
+
const requestDateKey = version === 'evm-2'
|
|
718
|
+
? (0, date_utils_1.formatDateKey)(year, month, day)
|
|
719
|
+
: (0, date_utils_1.formatDateKey)(Number(ev.year), Number(ev.month), Number(ev.day));
|
|
720
|
+
// NOTE: evm-2 subgraph does not emit WithdrawalProcessed events, so matching
|
|
721
|
+
// relies on timestamp-based date comparison for evm-2 (deterministic for evm-1).
|
|
722
|
+
const foundRedemptionAgainstClaim = withdrawalProcesseds.find((h) => {
|
|
723
|
+
const proc = h;
|
|
724
|
+
if (proc.receiverAddr !== ev.receiverAddr)
|
|
725
|
+
return false;
|
|
726
|
+
if (version === 'evm-2') {
|
|
727
|
+
const procDate = new Date(Number(proc.timestamp_) * 1000);
|
|
728
|
+
const procKey = (0, date_utils_1.formatDateKey)(procDate.getUTCFullYear(), procDate.getUTCMonth() + 1, procDate.getUTCDate());
|
|
729
|
+
return procKey === requestDateKey;
|
|
730
|
+
}
|
|
731
|
+
const procDate = new Date(proc.processedOn);
|
|
732
|
+
const procKey = (0, date_utils_1.formatDateKey)(procDate.getUTCFullYear(), procDate.getUTCMonth() + 1, procDate.getUTCDate());
|
|
733
|
+
return procKey === requestDateKey;
|
|
734
|
+
});
|
|
735
|
+
if (wallet && (0, ethers_1.isAddress)(wallet)) {
|
|
736
|
+
// check if connected users address
|
|
737
|
+
if (ev?.receiverAddr?.toLowerCase() === wallet.toLowerCase()) {
|
|
738
|
+
const alreadyRedeemed = version === 'evm-2'
|
|
739
|
+
? withdrawalProcesseds.find((red) => {
|
|
740
|
+
// Match by receiver + UTC date key (deterministic)
|
|
741
|
+
if (red.receiverAddr.toLowerCase() !== wallet.toLowerCase()) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
const redDate = new Date(Number(red.timestamp_) * 1000);
|
|
745
|
+
const redKey = (0, date_utils_1.formatDateKey)(redDate.getUTCFullYear(), redDate.getUTCMonth() + 1, redDate.getUTCDate());
|
|
746
|
+
return redKey === requestDateKey;
|
|
747
|
+
})
|
|
748
|
+
: availableRedemptions.find((red) => subgraphAmountToBigInt(red.day.raw) ===
|
|
749
|
+
subgraphAmountToBigInt(ev.day) &&
|
|
750
|
+
subgraphAmountToBigInt(red.month.raw) ===
|
|
751
|
+
subgraphAmountToBigInt(ev.month) &&
|
|
752
|
+
subgraphAmountToBigInt(red.year.raw) ===
|
|
753
|
+
subgraphAmountToBigInt(ev.year));
|
|
754
|
+
if (!(foundRedemptionAgainstClaim && alreadyRedeemed)) {
|
|
755
|
+
// double check if user has already claimed
|
|
756
|
+
try {
|
|
757
|
+
switch (version) {
|
|
758
|
+
case 'evm-2': {
|
|
759
|
+
// 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
|
|
760
|
+
if (alreadyRedeemed) {
|
|
761
|
+
const index = withdrawalProcesseds.findIndex((item) => item.id === alreadyRedeemed.id);
|
|
762
|
+
if (index !== -1) {
|
|
763
|
+
withdrawalProcesseds.splice(index, 1);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (!alreadyRedeemed) {
|
|
767
|
+
// Recompute claimable date (includes lag + 300s manipulation window) to determine if claimable now
|
|
768
|
+
const claimable = (0, date_utils_1.computeClaimableDate)(Number(ev.timestamp_), lagDuration);
|
|
769
|
+
const pendingCondition = (0, date_utils_1.isClaimableNow)(claimable.epoch, Math.floor(Date.now() / 1000));
|
|
770
|
+
if (pendingCondition) {
|
|
771
|
+
// Has passed lag duration - check if claim is not settled before adding to pending
|
|
772
|
+
try {
|
|
773
|
+
const burnableAmount = (await vaultContract?.getBurnableAmountByReceiver?.(BigInt(year), BigInt(month), BigInt(day), (0, ethers_1.getAddress)(ev.receiverAddr))) || BigInt(0);
|
|
774
|
+
const claimAmount = subgraphAmountToBigInt(ev.shares);
|
|
775
|
+
// If burnableAmount >= claimAmount, claim hasn't been settled by processAllClaims
|
|
776
|
+
// Only add to pending if it's still unsettled
|
|
777
|
+
if (burnableAmount >= claimAmount) {
|
|
778
|
+
pendingRedemptions.push({
|
|
779
|
+
id: ev.transactionHash_ || ev.id,
|
|
780
|
+
hash: ev.transactionHash_ || ev.id,
|
|
781
|
+
timestamp: Number(ev.timestamp_),
|
|
782
|
+
receiver: ev.receiverAddr,
|
|
783
|
+
day: (0, core_1.toNormalizedBn)(day, 0),
|
|
784
|
+
month: (0, core_1.toNormalizedBn)(month, 0),
|
|
785
|
+
year: (0, core_1.toNormalizedBn)(year, 0),
|
|
786
|
+
amount: (0, core_1.toNormalizedBn)(subgraphAmountToBigInt(ev.shares), decimals),
|
|
787
|
+
date: fullDate,
|
|
788
|
+
vault,
|
|
789
|
+
status: types_1.VaultRedemptionStatus.READY_TO_CLAIM,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
catch (burnableError) {
|
|
794
|
+
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Failed to check burnable amount for pending redemption: ${burnableError}`);
|
|
795
|
+
// If check fails, don't add to pending to be safe
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
// Hasn't passed lag duration - add to available redemptions only
|
|
800
|
+
availableRedemptions.push({
|
|
801
|
+
id: ev.transactionHash_ || ev.id,
|
|
802
|
+
hash: ev.transactionHash_ || ev.id,
|
|
803
|
+
timestamp: Number(ev.timestamp_),
|
|
804
|
+
receiver: ev.receiverAddr,
|
|
805
|
+
day: (0, core_1.toNormalizedBn)(day, 0),
|
|
806
|
+
month: (0, core_1.toNormalizedBn)(month, 0),
|
|
807
|
+
year: (0, core_1.toNormalizedBn)(year, 0),
|
|
808
|
+
amount: (0, core_1.toNormalizedBn)(subgraphAmountToBigInt(ev.shares), decimals),
|
|
809
|
+
date: fullDate,
|
|
810
|
+
vault,
|
|
811
|
+
status: types_1.VaultRedemptionStatus.AWAITING_COOLDOWN,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
default: {
|
|
818
|
+
// Safe to read the raw subgraph dates here: the validation
|
|
819
|
+
// block above `continue`s for any non-evm-2 event missing
|
|
820
|
+
// `year`/`month`/`day`, so this path is only reached with all
|
|
821
|
+
// three present. The coercion below therefore never
|
|
822
|
+
// substitutes 0 for a real date.
|
|
823
|
+
const trueClaimableAmount = await vaultContract?.getClaimableAmountByReceiver?.(subgraphAmountToBigInt(ev.year), subgraphAmountToBigInt(ev.month), subgraphAmountToBigInt(ev.day), (0, ethers_1.getAddress)(wallet));
|
|
824
|
+
if (trueClaimableAmount > BigInt(0)) {
|
|
825
|
+
// Use computeClaimableDate for UTC-correct date with lag
|
|
826
|
+
const v1Claimable = (0, date_utils_1.computeClaimableDate)(Number(ev.timestamp_), lagDuration);
|
|
827
|
+
const pendingCondition = (0, date_utils_1.isClaimableNow)(v1Claimable.epoch, Math.floor(Date.now() / 1000));
|
|
828
|
+
if (pendingCondition) {
|
|
829
|
+
// Has passed lag duration - check if claim is not settled before adding to pending
|
|
830
|
+
try {
|
|
831
|
+
year = v1Claimable.year;
|
|
832
|
+
month = v1Claimable.month;
|
|
833
|
+
day = v1Claimable.day;
|
|
834
|
+
const burnableAmount = (await vaultContract?.getBurnableAmountByReceiver?.(BigInt(year), BigInt(month), BigInt(day), (0, ethers_1.getAddress)(ev.receiverAddr))) || BigInt(0);
|
|
835
|
+
const claimAmount = trueClaimableAmount || BigInt(0);
|
|
836
|
+
// If burnableAmount >= claimAmount, claim hasn't been settled by processAllClaims
|
|
837
|
+
// Only add to pending if it's still unsettled
|
|
838
|
+
if (burnableAmount >= claimAmount) {
|
|
839
|
+
pendingRedemptions.push({
|
|
840
|
+
id: ev.transactionHash_ || ev.id,
|
|
841
|
+
hash: ev.transactionHash_ || ev.id,
|
|
842
|
+
timestamp: Number(ev.timestamp_),
|
|
843
|
+
receiver: ev.receiverAddr,
|
|
844
|
+
day: (0, core_1.toNormalizedBn)(ev.day, 0),
|
|
845
|
+
month: (0, core_1.toNormalizedBn)(ev.month, 0),
|
|
846
|
+
year: (0, core_1.toNormalizedBn)(ev.year, 0),
|
|
847
|
+
amount: (0, core_1.toNormalizedBn)(trueClaimableAmount || BigInt(0), decimals),
|
|
848
|
+
date: fullDate,
|
|
849
|
+
vault,
|
|
850
|
+
status: types_1.VaultRedemptionStatus.READY_TO_CLAIM,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
catch (burnableError) {
|
|
855
|
+
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Failed to check burnable amount for pending redemption: ${burnableError}`);
|
|
856
|
+
// If check fails, don't add to pending to be safe
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
// Hasn't passed lag duration - add to available redemptions only
|
|
861
|
+
availableRedemptions.push({
|
|
862
|
+
id: ev.transactionHash_ || ev.id,
|
|
863
|
+
hash: ev.transactionHash_ || ev.id,
|
|
864
|
+
timestamp: Number(ev.timestamp_),
|
|
865
|
+
receiver: ev.receiverAddr,
|
|
866
|
+
day: (0, core_1.toNormalizedBn)(ev.day, 0),
|
|
867
|
+
month: (0, core_1.toNormalizedBn)(ev.month, 0),
|
|
868
|
+
year: (0, core_1.toNormalizedBn)(ev.year, 0),
|
|
869
|
+
amount: (0, core_1.toNormalizedBn)(trueClaimableAmount, decimals),
|
|
870
|
+
date: fullDate,
|
|
871
|
+
vault,
|
|
872
|
+
status: types_1.VaultRedemptionStatus.AWAITING_COOLDOWN,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
catch (contractError) {
|
|
881
|
+
core_1.Logger.log.error('getVaultAvailableRedemptions', `Contract call failed: ${contractError}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
if (foundRedemptionAgainstClaim) {
|
|
888
|
+
availableRedemptions.push({
|
|
889
|
+
id: ev.transactionHash_ || ev.id,
|
|
890
|
+
hash: ev.transactionHash_ || ev.id,
|
|
891
|
+
timestamp: Number(ev.timestamp_),
|
|
892
|
+
receiver: ev.receiverAddr,
|
|
893
|
+
day: (0, core_1.toNormalizedBn)(ev.day, 0),
|
|
894
|
+
month: (0, core_1.toNormalizedBn)(ev.month, 0),
|
|
895
|
+
year: (0, core_1.toNormalizedBn)(ev.year, 0),
|
|
896
|
+
amount: (0, core_1.toNormalizedBn)(subgraphAmountToBigInt(ev.assets), decimals),
|
|
897
|
+
date: fullDate,
|
|
898
|
+
vault,
|
|
899
|
+
status: types_1.VaultRedemptionStatus.AWAITING_COOLDOWN,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
...{
|
|
906
|
+
availableRedemptions,
|
|
907
|
+
pendingRedemptions,
|
|
908
|
+
},
|
|
909
|
+
...(options.verbose && {
|
|
910
|
+
processedWithdrawals: withdrawalProcesseds,
|
|
911
|
+
requestedWithdrawals: withdrawalRequesteds,
|
|
912
|
+
}),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
catch (e) {
|
|
916
|
+
core_1.Logger.log.error('getVaultAvailableRedemptions', e, { vault, wallet });
|
|
917
|
+
return {
|
|
918
|
+
...{
|
|
919
|
+
availableRedemptions: [],
|
|
920
|
+
pendingRedemptions: [],
|
|
921
|
+
},
|
|
922
|
+
...(options.verbose && {
|
|
923
|
+
processedWithdrawals: [],
|
|
924
|
+
requestedWithdrawals: [],
|
|
925
|
+
}),
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, options, }) {
|
|
930
|
+
try {
|
|
931
|
+
// Stellar vaults: no on-chain redemption history yet
|
|
932
|
+
if ((0, utils_3.isStellarAddress)(vault)) {
|
|
933
|
+
core_1.Logger.log.warn('getVaultRedemptionHistory', `Redemption history is not yet supported for Stellar vaults: ${vault}`);
|
|
934
|
+
return [];
|
|
935
|
+
}
|
|
936
|
+
// setup
|
|
937
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
938
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
|
|
939
|
+
if (!tokenizedVault) {
|
|
940
|
+
core_1.Logger.log.warn('getVaultRedemptionHistory', `Vault not found in backend: ${vault}`);
|
|
941
|
+
return [];
|
|
942
|
+
}
|
|
943
|
+
const chainId = tokenizedVault.chain;
|
|
944
|
+
const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
945
|
+
const isV2 = version === 'evm-2';
|
|
946
|
+
// Use correct ABI based on vault version — different WithdrawalProcessed event signatures
|
|
947
|
+
const vaultAbi = isV2 ? TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOL_V2;
|
|
948
|
+
const poolContract = (0, core_1.createContract)({
|
|
949
|
+
address: vault,
|
|
950
|
+
abi: vaultAbi,
|
|
951
|
+
provider,
|
|
952
|
+
});
|
|
953
|
+
// query getLogs
|
|
954
|
+
const currentBlock = await provider.getBlockNumber();
|
|
955
|
+
const blockSkip = (0, core_1.determineBlockSkipInternal)(chainId);
|
|
956
|
+
const cutoffBlock = currentBlock - (lookbackBlocks ?? (0, core_1.determineBlockCutoff)(chainId));
|
|
957
|
+
const BATCH_SIZE = 20; // Max concurrent RPC requests per batch
|
|
958
|
+
core_1.Logger.log.info('getVaultRedemptionHistory', {
|
|
959
|
+
vault,
|
|
960
|
+
version,
|
|
961
|
+
isV2,
|
|
962
|
+
chainId,
|
|
963
|
+
currentBlock,
|
|
964
|
+
cutoffBlock,
|
|
965
|
+
blockSkip,
|
|
966
|
+
totalRangeBlocks: currentBlock - cutoffBlock,
|
|
967
|
+
});
|
|
968
|
+
// Build all query ranges
|
|
969
|
+
const ranges = [];
|
|
970
|
+
let endBlock = currentBlock;
|
|
971
|
+
while (endBlock >= cutoffBlock) {
|
|
972
|
+
const fromBlock = Math.max(endBlock - blockSkip, cutoffBlock);
|
|
973
|
+
ranges.push({ from: fromBlock, to: endBlock });
|
|
974
|
+
endBlock = fromBlock - 1;
|
|
975
|
+
}
|
|
976
|
+
core_1.Logger.log.info('getVaultRedemptionHistory', {
|
|
977
|
+
totalBatches: Math.ceil(ranges.length / BATCH_SIZE),
|
|
978
|
+
totalChunks: ranges.length,
|
|
979
|
+
firstRange: ranges[0],
|
|
980
|
+
lastRange: ranges[ranges.length - 1],
|
|
981
|
+
});
|
|
982
|
+
// Execute in batches to avoid RPC rate limits on high-block-count chains.
|
|
983
|
+
// Use allSettled + explicit throw so a chunk failure surfaces to the caller
|
|
984
|
+
// instead of silently returning an empty result (which used to corrupt
|
|
985
|
+
// `getWithdrawalRequestsWithStatus` into reporting `ready_to_claim` for
|
|
986
|
+
// requests that were already processed).
|
|
987
|
+
const logs = [];
|
|
988
|
+
for (let i = 0; i < ranges.length; i += BATCH_SIZE) {
|
|
989
|
+
const batch = ranges.slice(i, i + BATCH_SIZE);
|
|
990
|
+
const batchResults = await Promise.allSettled(batch.map((r) => poolContract.queryFilter('WithdrawalProcessed', BigInt(r.from), BigInt(r.to))));
|
|
991
|
+
const failed = batchResults.filter((r) => r.status === 'rejected');
|
|
992
|
+
if (failed.length > 0) {
|
|
993
|
+
const firstReason = failed[0].reason;
|
|
994
|
+
const firstMessage = firstReason instanceof Error
|
|
995
|
+
? firstReason.message
|
|
996
|
+
: String(firstReason);
|
|
997
|
+
// Breadcrumb only — the throw below is caught and logged by this
|
|
998
|
+
// function's own catch block, so capturing here duplicated every
|
|
999
|
+
// failure into a second Sentry issue ("log fetch chunk(s) failed").
|
|
1000
|
+
core_1.Logger.log.warn('getVaultRedemptionHistory', {
|
|
1001
|
+
reason: 'log fetch chunk(s) failed',
|
|
1002
|
+
failedChunks: failed.length,
|
|
1003
|
+
totalChunks: batch.length,
|
|
1004
|
+
firstReason: firstMessage,
|
|
1005
|
+
});
|
|
1006
|
+
throw new Error(`Failed to fetch WithdrawalProcessed logs (${failed.length}/${batch.length} chunk(s) rejected): ${firstMessage}`);
|
|
1007
|
+
}
|
|
1008
|
+
const batchLogs = batchResults.flatMap((r) => r.status === 'fulfilled' ? r.value : []);
|
|
1009
|
+
logs.push(...batchLogs);
|
|
1010
|
+
}
|
|
1011
|
+
// evm-1: WithdrawalProcessed(uint256 assetsAmount, uint256 processedOn, address receiverAddr, uint256 requestedOn)
|
|
1012
|
+
// evm-2: WithdrawalProcessed(uint256 assetsAmount, address indexed receiverAddr)
|
|
1013
|
+
const iface = new ethers_1.ethers.Interface(isV2
|
|
1014
|
+
? [
|
|
1015
|
+
'event WithdrawalProcessed(uint256 assetsAmount, address indexed receiverAddr)',
|
|
1016
|
+
]
|
|
1017
|
+
: [
|
|
1018
|
+
'event WithdrawalProcessed(uint256 assetsAmount, uint256 processedOn, address receiverAddr, uint256 requestedOn)',
|
|
1019
|
+
]);
|
|
1020
|
+
// get pool decimals
|
|
1021
|
+
const decimals = await (0, core_1.getDecimals)(provider, vault);
|
|
1022
|
+
// For evm-2 events (no timestamps in event), fetch block timestamps
|
|
1023
|
+
let blockTimestamps;
|
|
1024
|
+
if (isV2 && logs.length > 0) {
|
|
1025
|
+
const uniqueBlocks = [...new Set(logs.map((l) => l.blockNumber))];
|
|
1026
|
+
const blockResults = await Promise.all(uniqueBlocks.map((bn) => provider.getBlock(bn)));
|
|
1027
|
+
const timestamps = new Map();
|
|
1028
|
+
blockResults.forEach((block) => {
|
|
1029
|
+
if (block)
|
|
1030
|
+
timestamps.set(block.number, block.timestamp);
|
|
1031
|
+
});
|
|
1032
|
+
blockTimestamps = timestamps;
|
|
1033
|
+
}
|
|
1034
|
+
// format — iterate logs directly to preserve transactionHash
|
|
1035
|
+
const redemptions = [];
|
|
1036
|
+
logs.forEach((log) => {
|
|
1037
|
+
const ev = iface.parseLog(log);
|
|
1038
|
+
if (!ev?.args)
|
|
1039
|
+
return;
|
|
1040
|
+
let item;
|
|
1041
|
+
if (isV2) {
|
|
1042
|
+
const [assetsAmount, receiverAddr] = ev.args;
|
|
1043
|
+
const blockTs = blockTimestamps?.get(log.blockNumber) ?? 0;
|
|
1044
|
+
const blockDate = new Date(blockTs * 1000);
|
|
1045
|
+
item = {
|
|
1046
|
+
receiver: receiverAddr,
|
|
1047
|
+
amount: (0, core_1.toNormalizedBn)(assetsAmount, decimals),
|
|
1048
|
+
processed: blockDate,
|
|
1049
|
+
requested: new Date(0), // evm-2 event has no requestedOn field; epoch 0 is a sentinel for "unknown"
|
|
1050
|
+
vault,
|
|
1051
|
+
transactionHash: log.transactionHash,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
const [assetsAmount, processedOn, receiverAddr, requestedOn] = ev.args;
|
|
1056
|
+
item = {
|
|
1057
|
+
receiver: receiverAddr,
|
|
1058
|
+
amount: (0, core_1.toNormalizedBn)(assetsAmount, decimals),
|
|
1059
|
+
processed: new Date(Number(processedOn) * 1000),
|
|
1060
|
+
requested: new Date(Number(requestedOn) * 1000),
|
|
1061
|
+
vault,
|
|
1062
|
+
transactionHash: log.transactionHash,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
if (wallet) {
|
|
1066
|
+
if (item.receiver?.toLowerCase() === wallet.toLowerCase()) {
|
|
1067
|
+
redemptions.push(item);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
redemptions.push(item);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
core_1.Logger.log.info('getVaultRedemptionHistory', redemptions.slice(0, 1));
|
|
1075
|
+
return redemptions;
|
|
1076
|
+
}
|
|
1077
|
+
catch (e) {
|
|
1078
|
+
// Log loudly and re-throw. Previously this caught all errors and returned
|
|
1079
|
+
// [], which silently corrupted callers (notably getWithdrawalRequestsWithStatus,
|
|
1080
|
+
// which would then report `ready_to_claim` for already-processed requests).
|
|
1081
|
+
// Returning [] is reserved for the explicit early returns inside the try
|
|
1082
|
+
// (unsupported chains, vault not in backend) — not for unexpected failures.
|
|
1083
|
+
core_1.Logger.log.error('getVaultRedemptionHistory', e, { vault, wallet });
|
|
1084
|
+
throw e;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
// Moved to core/helpers/swap-router.ts to break the adapters/evm ↔
|
|
1088
|
+
// modules/vaults/getters import cycle (CLAUDE.md §9); re-exported here for
|
|
1089
|
+
// back-compat with existing importers.
|
|
1090
|
+
var swap_router_1 = require("../../core/helpers/swap-router");
|
|
1091
|
+
Object.defineProperty(exports, "getSwapRouterDepositResult", { enumerable: true, get: function () { return swap_router_1.getSwapRouterDepositResult; } });
|
|
1092
|
+
/**
|
|
1093
|
+
* Vault Positions
|
|
1094
|
+
*/
|
|
1095
|
+
async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, options, prefetchedReads, }) {
|
|
1096
|
+
try {
|
|
1097
|
+
const tokenizedVaults = await (0, core_1.fetchTokenizedVault)(vault ? vault : undefined, options?.headers, false, false);
|
|
1098
|
+
if (!tokenizedVaults || tokenizedVaults.length === 0) {
|
|
1099
|
+
return [];
|
|
1100
|
+
}
|
|
1101
|
+
// check if there are redemptions available or if you stake in this pool
|
|
1102
|
+
function renderStatus(_redemptions, _balance) {
|
|
1103
|
+
if (_redemptions?.length)
|
|
1104
|
+
return 'REDEEM';
|
|
1105
|
+
if (BigInt(_balance.raw) > BigInt(0))
|
|
1106
|
+
return 'STAKED';
|
|
1107
|
+
return 'PENDING';
|
|
1108
|
+
}
|
|
1109
|
+
const promises = await Promise.all(tokenizedVaults.map(async (v) => {
|
|
1110
|
+
try {
|
|
1111
|
+
// @solana: skip if not solana address
|
|
1112
|
+
if (utils_2.SolanaUtils.isSolanaAddress(v.address)) {
|
|
1113
|
+
// Default decimals: backend metadata, then SDK fallback. The mint's
|
|
1114
|
+
// own decimals (read below) override this once we have a real
|
|
1115
|
+
// tokenAmount response.
|
|
1116
|
+
const fallbackShareDecimals = v.solana_vault_metadata?.deposit_token_decimals ??
|
|
1117
|
+
SolanaConstants.fallbackDecimals;
|
|
1118
|
+
let balance = (0, core_1.toNormalizedBn)(0, fallbackShareDecimals);
|
|
1119
|
+
// Defensive: without a Solana adapter we can't fetch on-chain
|
|
1120
|
+
// state. The outer filter in modules/vaults/main.ts already gates
|
|
1121
|
+
// on `this.solanaService`, so this is a belt-and-suspenders guard
|
|
1122
|
+
// for any external caller that constructs `options` by hand.
|
|
1123
|
+
if (utils_2.SolanaUtils.isSolanaAddress(solanaWallet) &&
|
|
1124
|
+
options.solanaService) {
|
|
1125
|
+
const vaultProgramId = utils_2.SolanaUtils.resolveProgramId(String(v.address), v.solana_vault_metadata);
|
|
1126
|
+
const vaultStateRes = await options.solanaService.getVaultState(vaultProgramId, SolanaConstants.vaultIdl, v.solana_vault_metadata?.vault_state_pda ?? undefined);
|
|
1127
|
+
const vaultState = vaultStateRes?.vaultState;
|
|
1128
|
+
const { shareMint } = vaultState ?? { shareMint: null };
|
|
1129
|
+
if (shareMint) {
|
|
1130
|
+
// Always use the raw helper — it returns the on-chain u64
|
|
1131
|
+
// string and the mint's true decimals. The legacy `uiAmount`
|
|
1132
|
+
// (JS number) path lost precision and silently encoded `.raw`
|
|
1133
|
+
// against 18 decimals when fed back through `toNormalizedBn`
|
|
1134
|
+
// without a decimals argument, which broke every downstream
|
|
1135
|
+
// BigInt consumer (redemptions, max actions, allowances).
|
|
1136
|
+
const raw = await options.solanaService.fetchUserShareBalanceRaw(solanaWallet, shareMint);
|
|
1137
|
+
const decimals = raw.decimals ?? fallbackShareDecimals;
|
|
1138
|
+
balance = (0, core_1.toNormalizedBn)(BigInt(raw.amount), decimals);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
// Mirror the Stellar branch — return the backend's `v.address`,
|
|
1143
|
+
// not the outer `vault` parameter (which can be undefined for
|
|
1144
|
+
// multi-vault calls). No `as IAddress` cast: Solana base58 is
|
|
1145
|
+
// not 0x-hex; the IAddress typing across non-EVM positions is a
|
|
1146
|
+
// known wart documented in IVaultPosition.
|
|
1147
|
+
vault: v.address,
|
|
1148
|
+
status: renderStatus([], balance),
|
|
1149
|
+
availableRedemptions: [],
|
|
1150
|
+
pendingRedemptions: [],
|
|
1151
|
+
redeemable: balance,
|
|
1152
|
+
walletBalance: balance,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
if ((0, utils_3.isStellarAddress)(v.address)) {
|
|
1156
|
+
let balance = (0, core_1.toNormalizedBn)(0);
|
|
1157
|
+
if (stellarWallet && (0, utils_3.isStellarAddress)(stellarWallet)) {
|
|
1158
|
+
const stellarNetwork = v.stellar_vault_metadata?.network_name ?? 'mainnet';
|
|
1159
|
+
// Only use the configured RPC override for its own network.
|
|
1160
|
+
const stellarOverrideRpcUrl = options.stellarRpc?.network === stellarNetwork
|
|
1161
|
+
? options.stellarRpc.rpcUrl
|
|
1162
|
+
: undefined;
|
|
1163
|
+
const position = await StellarGetters.getStellarUserPosition(v.address, stellarWallet, stellarNetwork, stellarOverrideRpcUrl);
|
|
1164
|
+
if (position && !position.decimalsFromFallback) {
|
|
1165
|
+
balance = (0, core_1.toNormalizedBn)(BigInt(position.shares), position.decimals);
|
|
1166
|
+
}
|
|
1167
|
+
else if (position) {
|
|
1168
|
+
// decimals() read failed: trusting the fallback (7) would
|
|
1169
|
+
// mis-scale the balance for ERC4626 offset vaults (AUGUST-6381).
|
|
1170
|
+
// Show zero rather than a confidently-wrong value.
|
|
1171
|
+
core_1.Logger.log.warn('getVaultPositions', 'Stellar decimals() unresolved — refusing to scale balance with fallback decimals (showing zero)', { vault: v.address, wallet: stellarWallet });
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
core_1.Logger.log.warn('getVaultPositions', 'Stellar position query failed — showing zero balance', { vault: v.address, wallet: stellarWallet });
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
else if (stellarWallet) {
|
|
1178
|
+
core_1.Logger.log.warn('getVaultPositions', 'Invalid stellarWallet address format', { vault: v.address, stellarWallet });
|
|
1179
|
+
}
|
|
1180
|
+
// Stellar vaults have no on-chain redemption queue yet — redeemable is always 0
|
|
1181
|
+
return {
|
|
1182
|
+
vault: v.address,
|
|
1183
|
+
status: renderStatus([], balance),
|
|
1184
|
+
availableRedemptions: [],
|
|
1185
|
+
pendingRedemptions: [],
|
|
1186
|
+
redeemable: (0, core_1.toNormalizedBn)(0),
|
|
1187
|
+
walletBalance: balance,
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
// create provider
|
|
1191
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1192
|
+
// version split logic
|
|
1193
|
+
let decimals;
|
|
1194
|
+
const version = (0, core_1.getVaultVersionV2)(v);
|
|
1195
|
+
// Use ABI_LENDING_POOL_V2 as the common ABI for vault operations
|
|
1196
|
+
const vaultContract = (0, core_1.createContract)({
|
|
1197
|
+
provider,
|
|
1198
|
+
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
1199
|
+
address: v.address,
|
|
1200
|
+
});
|
|
1201
|
+
// Batched reads only apply to the vault this call was made for —
|
|
1202
|
+
// guard against a multi-row backend response reusing them.
|
|
1203
|
+
const prefetched = v.address?.toLowerCase?.() === String(vault).toLowerCase()
|
|
1204
|
+
? prefetchedReads
|
|
1205
|
+
: undefined;
|
|
1206
|
+
// get vault metadata and balance based on version
|
|
1207
|
+
let bal = BigInt(0);
|
|
1208
|
+
if (version === 'evm-2') {
|
|
1209
|
+
const receiptAddress = await (0, core_1.getReceiptTokenAddress)(provider, vault);
|
|
1210
|
+
decimals = await (0, core_1.getDecimals)(provider, receiptAddress, false);
|
|
1211
|
+
if (wallet && (0, ethers_1.isAddress)(wallet)) {
|
|
1212
|
+
if (typeof prefetched?.balance === 'bigint') {
|
|
1213
|
+
bal = prefetched.balance;
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
const receiptContract = (0, core_1.createContract)({
|
|
1217
|
+
provider,
|
|
1218
|
+
abi: TokenizedVaultV2Receipt_1.ABI_TOKENIZED_VAULT_V2_RECEIPT,
|
|
1219
|
+
address: receiptAddress,
|
|
1220
|
+
});
|
|
1221
|
+
bal = await receiptContract.balanceOf(wallet);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
decimals = await (0, core_1.getDecimals)(provider, vault);
|
|
1227
|
+
if (wallet && (0, ethers_1.isAddress)(wallet)) {
|
|
1228
|
+
bal =
|
|
1229
|
+
typeof prefetched?.balance === 'bigint'
|
|
1230
|
+
? prefetched.balance
|
|
1231
|
+
: await vaultContract.balanceOf(wallet);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const balance = (0, core_1.toNormalizedBn)(bal, decimals);
|
|
1235
|
+
// check available redemptions
|
|
1236
|
+
const { availableRedemptions, pendingRedemptions } = await getVaultAvailableRedemptions({
|
|
1237
|
+
vault: v.address,
|
|
1238
|
+
wallet,
|
|
1239
|
+
options,
|
|
1240
|
+
prefetchedReads: prefetched,
|
|
1241
|
+
});
|
|
1242
|
+
// get aggregate redemption amount
|
|
1243
|
+
const aggregateAvailableRedemptions = availableRedemptions.reduce((acc, curr) => acc + BigInt(curr.amount.raw), BigInt(0));
|
|
1244
|
+
// return obj
|
|
1245
|
+
return {
|
|
1246
|
+
vault,
|
|
1247
|
+
status: renderStatus(availableRedemptions, balance),
|
|
1248
|
+
availableRedemptions,
|
|
1249
|
+
pendingRedemptions,
|
|
1250
|
+
redeemable: (0, core_1.toNormalizedBn)(aggregateAvailableRedemptions, decimals),
|
|
1251
|
+
walletBalance: balance,
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
catch (vaultError) {
|
|
1255
|
+
core_1.Logger.log.error(`getVaultPositions`, `Error processing vault ${v.address}: ${vaultError}`);
|
|
1256
|
+
return {
|
|
1257
|
+
vault,
|
|
1258
|
+
status: 'PENDING',
|
|
1259
|
+
availableRedemptions: [],
|
|
1260
|
+
pendingRedemptions: [],
|
|
1261
|
+
redeemable: (0, core_1.toNormalizedBn)(0),
|
|
1262
|
+
walletBalance: (0, core_1.toNormalizedBn)(0),
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
}));
|
|
1266
|
+
return promises.flat();
|
|
1267
|
+
}
|
|
1268
|
+
catch (e) {
|
|
1269
|
+
core_1.Logger.log.error('getVaultPositions', e, {
|
|
1270
|
+
vault,
|
|
1271
|
+
wallet,
|
|
1272
|
+
solanaWallet,
|
|
1273
|
+
stellarWallet,
|
|
1274
|
+
});
|
|
1275
|
+
throw new Error(`#getVaultPositions::${vault}:${e?.message}`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* @deprecated use getVaultHistoricalTimeseries instead
|
|
1280
|
+
*/
|
|
1281
|
+
async function getVaultApy({ vault, options, historical, }) {
|
|
1282
|
+
try {
|
|
1283
|
+
if (!vault)
|
|
1284
|
+
throw new Error('Vault input parameter is undefined.');
|
|
1285
|
+
// only looking for current apy
|
|
1286
|
+
if (!historical) {
|
|
1287
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers))?.[0];
|
|
1288
|
+
return [
|
|
1289
|
+
{
|
|
1290
|
+
value: tokenizedVault?.reported_apy?.apy || 0,
|
|
1291
|
+
timestamp: new Date().toISOString(),
|
|
1292
|
+
},
|
|
1293
|
+
];
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
// historical apy
|
|
1297
|
+
function paramBuilder(params, baseUrl) {
|
|
1298
|
+
if (!params)
|
|
1299
|
+
return '';
|
|
1300
|
+
const urlParams = new URLSearchParams();
|
|
1301
|
+
if (params.daysAgo)
|
|
1302
|
+
urlParams.append('days_ago', params.daysAgo.toString());
|
|
1303
|
+
if (params.order)
|
|
1304
|
+
urlParams.append('order', params.order.toString());
|
|
1305
|
+
if (params.interval)
|
|
1306
|
+
urlParams.append('interval', params.interval.toString());
|
|
1307
|
+
const query = urlParams.toString();
|
|
1308
|
+
if (!query)
|
|
1309
|
+
return '';
|
|
1310
|
+
return baseUrl?.includes('?') ? `&${query}` : `?${query}`;
|
|
1311
|
+
}
|
|
1312
|
+
const baseUrl = core_1.WEBSERVER_ENDPOINTS.public.tokenizedVault.historicalApy(vault);
|
|
1313
|
+
const historicalApy = await (0, core_1.fetchAugustPublic)(`${baseUrl}${paramBuilder(historical, baseUrl)}`, {
|
|
1314
|
+
headers: options?.headers
|
|
1315
|
+
? options?.headers
|
|
1316
|
+
: undefined,
|
|
1317
|
+
});
|
|
1318
|
+
if (!historicalApy.ok)
|
|
1319
|
+
throw new Error('Failed to fetch historical apy.');
|
|
1320
|
+
const historicalApyData = (await historicalApy.json());
|
|
1321
|
+
return historicalApyData.data.values.map((val, index) => {
|
|
1322
|
+
const respectiveDate = historicalApyData.data.labels[index];
|
|
1323
|
+
return {
|
|
1324
|
+
timestamp: new Date(respectiveDate).toISOString(),
|
|
1325
|
+
value: val,
|
|
1326
|
+
};
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
catch (e) {
|
|
1331
|
+
core_1.Logger.log.error('getVaultApy', e, { vault });
|
|
1332
|
+
throw new Error(`#getVaultApy::${vault}:${e?.message}`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
async function getRewardsStakingPositions({ rpcUrl, wallet, coinGeckoKey, }) {
|
|
1336
|
+
const REWARDS_CHAIN = 43114; // avalanche
|
|
1337
|
+
const REWARDS_SYMBOL = 'AVAX'; // avalanche
|
|
1338
|
+
const REWARDS_DECIMALS = 18; // avalanche
|
|
1339
|
+
const REWARDS_NAME = 'Avalanche'; // avalanche
|
|
1340
|
+
const APR_MULTIPLIER = 31536000;
|
|
1341
|
+
const UP_AUSD_SYMBOL = 'upAUSD';
|
|
1342
|
+
try {
|
|
1343
|
+
const provider = (0, core_1.createProvider)(rpcUrl);
|
|
1344
|
+
const chainId = await (0, core_1.getChainId)(provider);
|
|
1345
|
+
// Validate chain has rewards distributor
|
|
1346
|
+
if (chainId !== REWARDS_CHAIN) {
|
|
1347
|
+
core_1.Logger.log.warn('getStakingPositions:unsupported_chain', chainId);
|
|
1348
|
+
return [];
|
|
1349
|
+
}
|
|
1350
|
+
if (wallet && !(0, ethers_1.isAddress)(wallet)) {
|
|
1351
|
+
core_1.Logger.log.warn('getStakingPositions:invalid_wallet', wallet);
|
|
1352
|
+
return [];
|
|
1353
|
+
}
|
|
1354
|
+
// get reward distributor contract addresses
|
|
1355
|
+
const rewardDistributorAddresses = (0, core_1.REWARD_DISTRIBUTOR_ADDRESS)(chainId);
|
|
1356
|
+
// loop through each reward distributor contract address
|
|
1357
|
+
const positions = await Promise.all(rewardDistributorAddresses.map(async (contract, i) => {
|
|
1358
|
+
const rewardContract = (0, core_1.createContract)({
|
|
1359
|
+
address: contract,
|
|
1360
|
+
provider: provider,
|
|
1361
|
+
abi: abis_1.ABI_REWARD_DISTRIBUTOR,
|
|
1362
|
+
});
|
|
1363
|
+
// get reward distributor metadata
|
|
1364
|
+
const totalStaked = await rewardContract.totalStaked();
|
|
1365
|
+
const rewardsPerSecond = await rewardContract.rewardsPerSecond();
|
|
1366
|
+
// get staking token metadata
|
|
1367
|
+
const stakingTokenCalls = ['decimals', 'symbol', 'name', 'totalSupply'];
|
|
1368
|
+
const stakingTokenAddress = (await rewardContract.stakingToken());
|
|
1369
|
+
const stakingTokenContract = (0, core_1.createContract)({
|
|
1370
|
+
address: stakingTokenAddress,
|
|
1371
|
+
provider: provider,
|
|
1372
|
+
abi: abis_1.ABI_ERC20,
|
|
1373
|
+
});
|
|
1374
|
+
const [decimals, symbol, name] = await Promise.all(stakingTokenCalls.map((staking) => stakingTokenContract[staking]()));
|
|
1375
|
+
// if wallet address is passed, pull user balance of staked tokens
|
|
1376
|
+
let balance;
|
|
1377
|
+
let earned;
|
|
1378
|
+
if (wallet) {
|
|
1379
|
+
const formattedWallet = (0, ethers_1.getAddress)(wallet);
|
|
1380
|
+
balance = await rewardContract.balanceOf(formattedWallet);
|
|
1381
|
+
earned = await rewardContract.earned(formattedWallet);
|
|
1382
|
+
}
|
|
1383
|
+
// get appropriate token price
|
|
1384
|
+
const rewardTokenPriceInUsd = await (0, core_1.fetchTokenPrice)(REWARDS_SYMBOL, null, coinGeckoKey);
|
|
1385
|
+
const normalizedRewardTokenPrice = (0, core_1.toNormalizedBn)(rewardTokenPriceInUsd);
|
|
1386
|
+
// normalize numbers
|
|
1387
|
+
const normalizedTotalStakedInPool = (0, core_1.toNormalizedBn)(totalStaked, decimals);
|
|
1388
|
+
const normalizedRedeemable = (0, core_1.toNormalizedBn)(earned, REWARDS_DECIMALS);
|
|
1389
|
+
const normalizedRewardsPerSecond = (0, core_1.toNormalizedBn)(rewardsPerSecond, REWARDS_DECIMALS);
|
|
1390
|
+
const normalizedTotalStakedBalance = (0, core_1.toNormalizedBn)(balance, decimals);
|
|
1391
|
+
// calculate APR's
|
|
1392
|
+
const rewardTokenPriceToMultiply = symbol === UP_AUSD_SYMBOL
|
|
1393
|
+
? Number(normalizedRewardTokenPrice?.normalized)
|
|
1394
|
+
: 1;
|
|
1395
|
+
const STAKED_APR = ((Number(normalizedRewardsPerSecond?.normalized) *
|
|
1396
|
+
APR_MULTIPLIER *
|
|
1397
|
+
rewardTokenPriceToMultiply) /
|
|
1398
|
+
(Number(normalizedTotalStakedInPool?.normalized) * Number(1))) *
|
|
1399
|
+
100;
|
|
1400
|
+
const MAX_APR = ((Number(normalizedRewardsPerSecond?.normalized) *
|
|
1401
|
+
APR_MULTIPLIER *
|
|
1402
|
+
rewardTokenPriceToMultiply) /
|
|
1403
|
+
(Number(normalizedTotalStakedInPool?.normalized) * Number(1))) *
|
|
1404
|
+
100;
|
|
1405
|
+
const activePosition = {
|
|
1406
|
+
id: String(i),
|
|
1407
|
+
rewardToken: {
|
|
1408
|
+
decimals: REWARDS_DECIMALS,
|
|
1409
|
+
symbol: REWARDS_SYMBOL,
|
|
1410
|
+
address: ethers_1.ZeroAddress,
|
|
1411
|
+
chain: REWARDS_CHAIN,
|
|
1412
|
+
redeemable: normalizedRedeemable,
|
|
1413
|
+
usd: normalizedRewardTokenPrice,
|
|
1414
|
+
name: REWARDS_NAME,
|
|
1415
|
+
},
|
|
1416
|
+
stakingToken: {
|
|
1417
|
+
decimals: Number(decimals),
|
|
1418
|
+
symbol: symbol,
|
|
1419
|
+
address: stakingTokenAddress,
|
|
1420
|
+
chain: REWARDS_CHAIN,
|
|
1421
|
+
totalStaked: normalizedTotalStakedBalance,
|
|
1422
|
+
usd: normalizedRewardTokenPrice,
|
|
1423
|
+
totalSupply: normalizedTotalStakedInPool,
|
|
1424
|
+
name: name,
|
|
1425
|
+
},
|
|
1426
|
+
rewardDistributor: contract,
|
|
1427
|
+
rewardPerSecond: (0, core_1.toNormalizedBn)(rewardsPerSecond),
|
|
1428
|
+
apy: STAKED_APR,
|
|
1429
|
+
maxApy: MAX_APR,
|
|
1430
|
+
chainId: REWARDS_CHAIN,
|
|
1431
|
+
};
|
|
1432
|
+
return activePosition;
|
|
1433
|
+
}));
|
|
1434
|
+
return positions;
|
|
1435
|
+
}
|
|
1436
|
+
catch (e) {
|
|
1437
|
+
core_1.Logger.log.error('getStakingPositions', e, { wallet });
|
|
1438
|
+
return [];
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
async function getVaultTvl({ vault, options, historical, }) {
|
|
1442
|
+
try {
|
|
1443
|
+
if (!vault)
|
|
1444
|
+
throw new Error('Vault input parameter is undefined.');
|
|
1445
|
+
// Verify vault exists
|
|
1446
|
+
const _vaultExists = (await (0, core_1.fetchTokenizedVault)(vault))?.[0];
|
|
1447
|
+
if (!_vaultExists) {
|
|
1448
|
+
throw new Error(`Vault ${vault} not found in backend`);
|
|
1449
|
+
}
|
|
1450
|
+
// only wanting current APR
|
|
1451
|
+
if (!historical) {
|
|
1452
|
+
const version = (0, core_1.getVaultVersionV2)(_vaultExists);
|
|
1453
|
+
// Non-EVM early returns (no provider needed)
|
|
1454
|
+
if (version === 'sol-0') {
|
|
1455
|
+
return [
|
|
1456
|
+
{
|
|
1457
|
+
value: (0, core_1.toNormalizedBn)(0),
|
|
1458
|
+
timestamp: new Date().toISOString(),
|
|
1459
|
+
},
|
|
1460
|
+
];
|
|
1461
|
+
}
|
|
1462
|
+
if (version === 'stellar-0') {
|
|
1463
|
+
const stellarDecimals = _vaultExists?.stellar_vault_metadata?.deposit_token_decimals ??
|
|
1464
|
+
constants_1.STELLAR_FALLBACK_DECIMALS;
|
|
1465
|
+
return [
|
|
1466
|
+
{
|
|
1467
|
+
value: (0, utils_1.backendTvlToNormalizedBn)(_vaultExists?.latest_reported_tvl, stellarDecimals),
|
|
1468
|
+
timestamp: new Date().toISOString(),
|
|
1469
|
+
},
|
|
1470
|
+
];
|
|
1471
|
+
}
|
|
1472
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl, options.chainId);
|
|
1473
|
+
switch (version) {
|
|
1474
|
+
case 'evm-2': {
|
|
1475
|
+
const receiptTokenAddress = await (0, core_1.getReceiptTokenAddress)(provider, vault);
|
|
1476
|
+
const receiptTokenDecimals = await (0, core_1.getDecimals)(provider, receiptTokenAddress, false);
|
|
1477
|
+
const vaultContract = (0, core_1.createContract)({
|
|
1478
|
+
address: vault,
|
|
1479
|
+
abi: TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2,
|
|
1480
|
+
provider,
|
|
1481
|
+
});
|
|
1482
|
+
const totalAssets = await vaultContract.getTotalAssets();
|
|
1483
|
+
return [
|
|
1484
|
+
{
|
|
1485
|
+
value: (0, core_1.toNormalizedBn)(totalAssets, receiptTokenDecimals),
|
|
1486
|
+
timestamp: new Date().toISOString(),
|
|
1487
|
+
},
|
|
1488
|
+
];
|
|
1489
|
+
}
|
|
1490
|
+
default: {
|
|
1491
|
+
const vaultContract = (0, core_1.createContract)({
|
|
1492
|
+
address: vault,
|
|
1493
|
+
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
1494
|
+
provider,
|
|
1495
|
+
});
|
|
1496
|
+
const decimals = await (0, core_1.getDecimals)(provider, vault);
|
|
1497
|
+
const totalAssets = await vaultContract.totalAssets();
|
|
1498
|
+
return [
|
|
1499
|
+
{
|
|
1500
|
+
value: (0, core_1.toNormalizedBn)(totalAssets, decimals),
|
|
1501
|
+
timestamp: new Date().toISOString(),
|
|
1502
|
+
},
|
|
1503
|
+
];
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
// const vaultRes = await getVault({
|
|
1507
|
+
// vault,
|
|
1508
|
+
// options,
|
|
1509
|
+
// loans: false,
|
|
1510
|
+
// allocations: false,
|
|
1511
|
+
// });
|
|
1512
|
+
// const returnObj = [
|
|
1513
|
+
// { value: vaultRes.totalSupply, timestamp: new Date().toISOString() },
|
|
1514
|
+
// ];
|
|
1515
|
+
// Logger.log.info('getVaultTvl:current', returnObj);
|
|
1516
|
+
// return returnObj;
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
// historical tvl
|
|
1520
|
+
// check vault version early to guard non-EVM chains
|
|
1521
|
+
const version = (0, core_1.getVaultVersionV2)(_vaultExists);
|
|
1522
|
+
if (version === 'stellar-0' || version === 'sol-0') {
|
|
1523
|
+
core_1.Logger.log.warn('getVaultTvl:historical', 'Historical TVL not supported for non-EVM vaults', { vault, version });
|
|
1524
|
+
return [];
|
|
1525
|
+
}
|
|
1526
|
+
// if no order is passed but inputParams are passed
|
|
1527
|
+
if (typeof historical !== 'undefined' && !historical.order)
|
|
1528
|
+
historical.order = 'desc';
|
|
1529
|
+
// days is default interval
|
|
1530
|
+
if (typeof historical !== 'undefined' && !historical.interval)
|
|
1531
|
+
historical.interval = 'days';
|
|
1532
|
+
// setup
|
|
1533
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1534
|
+
const vaultContract = (0, core_1.createContract)({
|
|
1535
|
+
address: vault,
|
|
1536
|
+
abi: version === 'evm-2' ? TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOL_V2,
|
|
1537
|
+
provider,
|
|
1538
|
+
});
|
|
1539
|
+
let minAbi = [];
|
|
1540
|
+
let functionName = '';
|
|
1541
|
+
let vaultDecimals = 0;
|
|
1542
|
+
switch (version) {
|
|
1543
|
+
case 'evm-2': {
|
|
1544
|
+
minAbi = ['function getTotalAssets() view returns (uint256)'];
|
|
1545
|
+
functionName = 'getTotalAssets';
|
|
1546
|
+
const receiptTokenAddress = await (0, core_1.getReceiptTokenAddress)(provider, vault);
|
|
1547
|
+
vaultDecimals = await (0, core_1.getDecimals)(provider, receiptTokenAddress, false);
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
default: {
|
|
1551
|
+
minAbi = ['function totalAssets() view returns (uint256)'];
|
|
1552
|
+
functionName = 'totalAssets';
|
|
1553
|
+
vaultDecimals = await (0, core_1.getDecimals)(provider, vault);
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
// block trackback setup
|
|
1558
|
+
// Get the latest finalized block to ensure all queried blocks are confirmed
|
|
1559
|
+
const finalizedBlock = await provider.getBlock('finalized');
|
|
1560
|
+
const finalizedTimestamp = finalizedBlock
|
|
1561
|
+
? finalizedBlock.timestamp * 1000
|
|
1562
|
+
: Date.now() * 60 * 1000;
|
|
1563
|
+
const daysAgo = historical.daysAgo * 24 * 60 * 60 * 1000;
|
|
1564
|
+
const dater = new ethereum_block_by_date_1.default(provider);
|
|
1565
|
+
const startDate = historical.daysAgo
|
|
1566
|
+
? new Date(finalizedTimestamp - daysAgo)
|
|
1567
|
+
: new Date(finalizedTimestamp);
|
|
1568
|
+
const blocks = await dater.getEvery(historical.interval, // Period
|
|
1569
|
+
startDate.toUTCString(), // Start date
|
|
1570
|
+
new Date(finalizedTimestamp).toUTCString());
|
|
1571
|
+
// order blocks array
|
|
1572
|
+
const orderedBlocks = historical.order === 'desc' ? (0, core_1.orderObjArrByDate)(blocks) : blocks;
|
|
1573
|
+
// minimal ABI
|
|
1574
|
+
const totalAssetsHistorical = await (0, core_1.promiseSettle)(orderedBlocks?.map(async ({ block, date }) => {
|
|
1575
|
+
// get totalAssets at said block
|
|
1576
|
+
const totalAssetsAtBlock = await provider.call({
|
|
1577
|
+
to: vaultContract,
|
|
1578
|
+
data: new ethers_1.Interface(minAbi).encodeFunctionData(functionName, []),
|
|
1579
|
+
blockTag: block,
|
|
1580
|
+
});
|
|
1581
|
+
// cover case when data returned is 0
|
|
1582
|
+
if (totalAssetsAtBlock === '0x')
|
|
1583
|
+
return {
|
|
1584
|
+
timestamp: (0, core_1.dateToUnix)(new Date(date)),
|
|
1585
|
+
value: (0, core_1.toNormalizedBn)(0, 0),
|
|
1586
|
+
};
|
|
1587
|
+
// decode back to readable
|
|
1588
|
+
const readableTotalAssetsAtBlock = new ethers_1.Interface(minAbi).decodeFunctionResult(functionName, totalAssetsAtBlock);
|
|
1589
|
+
return {
|
|
1590
|
+
timestamp: (0, core_1.dateToUnix)(new Date(date)),
|
|
1591
|
+
value: (0, core_1.toNormalizedBn)(readableTotalAssetsAtBlock?.[0], vaultDecimals),
|
|
1592
|
+
};
|
|
1593
|
+
}));
|
|
1594
|
+
core_1.Logger.log.info('getVaultTvl:historical', totalAssetsHistorical.length);
|
|
1595
|
+
return totalAssetsHistorical;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
catch (e) {
|
|
1599
|
+
core_1.Logger.log.error('getVaultTvl', e, { vault });
|
|
1600
|
+
throw new Error(`Failed to fetch TVL for ${vault}: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Build the per-loan borrower / health-factor list for a single vault.
|
|
1605
|
+
*
|
|
1606
|
+
* Resilience: individual loan reads (`loanState()`, `borrower()`) and the
|
|
1607
|
+
* per-borrower August backend call are isolated with `Promise.allSettled` and
|
|
1608
|
+
* try/catch so one bad loan — a test loan whose address isn't a real contract,
|
|
1609
|
+
* a borrower the backend doesn't recognize, an intermittent RPC error — drops
|
|
1610
|
+
* that single row from the result instead of rejecting the whole batch. Failed
|
|
1611
|
+
* rows are logged via `Logger.log.warn`; the loan is still returned with
|
|
1612
|
+
* `health_factor: undefined` so callers can render an explicit empty state
|
|
1613
|
+
* rather than a perpetual loading skeleton.
|
|
1614
|
+
*
|
|
1615
|
+
* @param vault - Vault address to scope the fetch to.
|
|
1616
|
+
* @param options - Standard vault options — `rpcUrl` must point at `vault`'s chain.
|
|
1617
|
+
* @returns Array of `IVaultBorrowerHealthFactor`, one entry per active loan.
|
|
1618
|
+
*/
|
|
1619
|
+
async function getVaultBorrowerHealthFactor({ vault, options, }) {
|
|
1620
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1621
|
+
const loans = await getVaultLoans(vault, options);
|
|
1622
|
+
// Read each loan's on-chain state; drop loans whose RPC call fails so a
|
|
1623
|
+
// single bad contract (e.g. a test loan address) can't sink the batch.
|
|
1624
|
+
const activeLoanResults = await Promise.allSettled(loans.map(async (l) => {
|
|
1625
|
+
const loanContract = (0, core_1.createContract)({
|
|
1626
|
+
provider,
|
|
1627
|
+
address: l.address,
|
|
1628
|
+
abi: abis_1.ABI_LOAN,
|
|
1629
|
+
});
|
|
1630
|
+
const loanState = await loanContract.loanState();
|
|
1631
|
+
if ((0, core_1.loanStateToReadable)(loanState) !== 'ACTIVE')
|
|
1632
|
+
return undefined;
|
|
1633
|
+
return l.address;
|
|
1634
|
+
}));
|
|
1635
|
+
const activeLoans = activeLoanResults
|
|
1636
|
+
.map((r, i) => {
|
|
1637
|
+
if (r.status === 'fulfilled')
|
|
1638
|
+
return r.value;
|
|
1639
|
+
core_1.Logger.log.warn('getVaultBorrowerHealthFactor:loanState', r.reason, {
|
|
1640
|
+
vault,
|
|
1641
|
+
loan: loans[i]?.address,
|
|
1642
|
+
});
|
|
1643
|
+
return undefined;
|
|
1644
|
+
})
|
|
1645
|
+
.filter((l) => l !== undefined);
|
|
1646
|
+
// Same fault-tolerance for the per-loan borrower() read.
|
|
1647
|
+
const formattedSettled = await Promise.allSettled(activeLoans.map(async (l) => {
|
|
1648
|
+
const loanContract = (0, core_1.createContract)({
|
|
1649
|
+
provider,
|
|
1650
|
+
address: l,
|
|
1651
|
+
abi: abis_1.ABI_LOAN,
|
|
1652
|
+
});
|
|
1653
|
+
return {
|
|
1654
|
+
borrower: (await loanContract.borrower()),
|
|
1655
|
+
loan: l,
|
|
1656
|
+
};
|
|
1657
|
+
}));
|
|
1658
|
+
const formattedLoansArray = formattedSettled
|
|
1659
|
+
.map((r, i) => {
|
|
1660
|
+
if (r.status === 'fulfilled')
|
|
1661
|
+
return r.value;
|
|
1662
|
+
core_1.Logger.log.warn('getVaultBorrowerHealthFactor:borrower', r.reason, {
|
|
1663
|
+
vault,
|
|
1664
|
+
loan: activeLoans[i],
|
|
1665
|
+
});
|
|
1666
|
+
return undefined;
|
|
1667
|
+
})
|
|
1668
|
+
.filter((entry) => entry !== undefined);
|
|
1669
|
+
const uniqueBorrowers = [
|
|
1670
|
+
...new Set(formattedLoansArray.map((l) => l.borrower)),
|
|
1671
|
+
];
|
|
1672
|
+
const borrowerResponses = {};
|
|
1673
|
+
for (const b of uniqueBorrowers) {
|
|
1674
|
+
try {
|
|
1675
|
+
const response = await (0, core_1.fetchAugustWithKey)(options.augustKey, core_1.WEBSERVER_ENDPOINTS.subaccount.health_factor(b));
|
|
1676
|
+
if (response.status === 200) {
|
|
1677
|
+
borrowerResponses[b] = await response.json();
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
catch (err) {
|
|
1681
|
+
core_1.Logger.log.warn('getVaultBorrowerHealthFactor:health_factor', err, {
|
|
1682
|
+
vault,
|
|
1683
|
+
borrower: b,
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
formattedLoansArray.forEach((loan) => {
|
|
1688
|
+
loan.health_factor = borrowerResponses[loan.borrower];
|
|
1689
|
+
});
|
|
1690
|
+
return formattedLoansArray;
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Aggregate borrower-health-factor data, optionally scoped to a single vault.
|
|
1694
|
+
*
|
|
1695
|
+
* - When `vault` is provided, only that vault's tokenized record is fetched
|
|
1696
|
+
* and only its loans are read. This is the path callers should use when
|
|
1697
|
+
* they already know which vault they care about — it skips the cross-chain
|
|
1698
|
+
* fanout entirely.
|
|
1699
|
+
* - When `vault` is omitted, the function fetches every tokenized vault and
|
|
1700
|
+
* builds health factors for each, preserving the legacy behavior. Per-vault
|
|
1701
|
+
* failures are isolated with `Promise.allSettled` so one bad vault returns
|
|
1702
|
+
* an empty array instead of rejecting the whole map.
|
|
1703
|
+
*
|
|
1704
|
+
* The returned map is keyed by **lowercased** vault address so callers can
|
|
1705
|
+
* look up entries without worrying about EIP-55 checksum casing in either the
|
|
1706
|
+
* backend response or the caller's input.
|
|
1707
|
+
*
|
|
1708
|
+
* @param options - Standard vault options — `rpcUrl` should match `vault`'s chain.
|
|
1709
|
+
* @param vault - Optional vault address to scope the fetch to.
|
|
1710
|
+
* @returns Map of lowercased vault address → array of borrower-health-factor rows.
|
|
1711
|
+
*/
|
|
1712
|
+
async function getHealthFactorOfBorrowersByVault({ options, vault, }) {
|
|
1713
|
+
const vaults = vault
|
|
1714
|
+
? ((await (0, core_1.fetchTokenizedVault)(vault, options?.headers)) ?? [])
|
|
1715
|
+
: await (0, core_1.fetchTokenizedVaults)(undefined, options?.headers, false, false);
|
|
1716
|
+
const healthFactorResults = await Promise.allSettled(vaults.map(async (v) => {
|
|
1717
|
+
return getVaultBorrowerHealthFactor({
|
|
1718
|
+
vault: v.address,
|
|
1719
|
+
options,
|
|
1720
|
+
});
|
|
1721
|
+
}));
|
|
1722
|
+
const healthFactorsByPool = {};
|
|
1723
|
+
vaults.forEach((pool, index) => {
|
|
1724
|
+
const result = healthFactorResults[index];
|
|
1725
|
+
const key = pool.address.toLowerCase();
|
|
1726
|
+
if (result?.status === 'fulfilled') {
|
|
1727
|
+
healthFactorsByPool[key] = result.value;
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
core_1.Logger.log.warn('getHealthFactorOfBorrowersByVault:vault', result?.reason, {
|
|
1731
|
+
vault: pool.address,
|
|
1732
|
+
});
|
|
1733
|
+
healthFactorsByPool[key] = [];
|
|
1734
|
+
});
|
|
1735
|
+
return healthFactorsByPool;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Fetch user points from the backend API endpoint.
|
|
1739
|
+
* This replaces client-side points calculation with server-side processing.
|
|
1740
|
+
* @param userAddress - User wallet address (EVM or Solana)
|
|
1741
|
+
* @param options - Request options including headers
|
|
1742
|
+
* @returns Points data from the backend API
|
|
1743
|
+
*/
|
|
1744
|
+
async function getUserPoints({ userAddress, options, }) {
|
|
1745
|
+
try {
|
|
1746
|
+
if (!userAddress) {
|
|
1747
|
+
throw new Error('User address parameter is undefined.');
|
|
1748
|
+
}
|
|
1749
|
+
const endpoint = core_1.WEBSERVER_ENDPOINTS.public.points.byUserAddress(userAddress);
|
|
1750
|
+
const response = await (0, core_1.fetchAugustPublic)(endpoint, {
|
|
1751
|
+
headers: options?.headers,
|
|
1752
|
+
});
|
|
1753
|
+
if (!response.ok) {
|
|
1754
|
+
throw new Error(`Failed to fetch points: ${response.statusText}`);
|
|
1755
|
+
}
|
|
1756
|
+
const data = await response.json();
|
|
1757
|
+
return data;
|
|
1758
|
+
}
|
|
1759
|
+
catch (e) {
|
|
1760
|
+
core_1.Logger.log.error('getUserPoints', e, { userAddress });
|
|
1761
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1762
|
+
throw new Error(`#getUserPoints::${userAddress}:${errorMessage}`);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Register a user for the points program, authenticated by a wallet signature.
|
|
1767
|
+
*
|
|
1768
|
+
* The caller must obtain a personal_sign (EIP-191) signature over the
|
|
1769
|
+
* canonical message template below — the backend reconstructs the same
|
|
1770
|
+
* string from these fields and verifies the signature recovers to
|
|
1771
|
+
* `userAddress`. Building the message client-side and server-side from the
|
|
1772
|
+
* same primitives means a hostile client cannot show one message in the
|
|
1773
|
+
* wallet prompt while submitting a different one for validation.
|
|
1774
|
+
*
|
|
1775
|
+
* Canonical message (`\n`-separated, no trailing newline):
|
|
1776
|
+
*
|
|
1777
|
+
* ```
|
|
1778
|
+
* Upshift: register {userAddress.toLowerCase()}
|
|
1779
|
+
* referrer: {referrerAddress.toLowerCase() || "none"}
|
|
1780
|
+
* chain: {chainId}
|
|
1781
|
+
* nonce: {nonce}
|
|
1782
|
+
* expires: {expiry}
|
|
1783
|
+
* ```
|
|
1784
|
+
*
|
|
1785
|
+
* @param userAddress - EVM wallet address being registered. Must match the
|
|
1786
|
+
* signer that produced `signature`. Lowercased before being embedded in
|
|
1787
|
+
* the canonical message.
|
|
1788
|
+
* @param referrerAddress - Optional EVM address that referred this user.
|
|
1789
|
+
* Lowercased when embedded; the literal string `"none"` is used when
|
|
1790
|
+
* absent. Tampering with this field after signing produces 401.
|
|
1791
|
+
* @param chainId - Chain on which the wallet signed. For smart-contract wallets
|
|
1792
|
+
* (Safe etc.) this is the chain whose RPC the backend will use to run the
|
|
1793
|
+
* EIP-1271 `isValidSignature` check; for EOAs it still pins the signature
|
|
1794
|
+
* to a chain so it can't be replayed cross-chain. Must be one of the chains
|
|
1795
|
+
* Upshift supports (see backend `SUPPORTED_REGISTRATION_CHAINS`).
|
|
1796
|
+
* @param signature - `0x`-prefixed hex of the personal_sign signature over
|
|
1797
|
+
* the canonical message. Single-use: replays within ~10 min are rejected.
|
|
1798
|
+
* @param nonce - Caller-generated random string (8–128 chars). Pair with
|
|
1799
|
+
* `userAddress` to form the single-use key in the backend's nonce store.
|
|
1800
|
+
* @param expiry - Unix timestamp (seconds). Must be in the future and within
|
|
1801
|
+
* the backend's TTL window (currently 10 min).
|
|
1802
|
+
* @param options - Request options (headers).
|
|
1803
|
+
* @returns Raw `Response` from the backend. 200 on success, 401 for
|
|
1804
|
+
* expired / replayed / mismatched-signer / tampered-referrer, 422 for an
|
|
1805
|
+
* unsupported `chainId`.
|
|
1806
|
+
*
|
|
1807
|
+
* @example
|
|
1808
|
+
* ```ts
|
|
1809
|
+
* const nonce = crypto.randomUUID().replace(/-/g, '');
|
|
1810
|
+
* const expiry = Math.floor(Date.now() / 1000) + 300;
|
|
1811
|
+
* const message =
|
|
1812
|
+
* `Upshift: register ${address.toLowerCase()}\n` +
|
|
1813
|
+
* `referrer: ${referrer?.toLowerCase() ?? 'none'}\n` +
|
|
1814
|
+
* `chain: ${chainId}\n` +
|
|
1815
|
+
* `nonce: ${nonce}\n` +
|
|
1816
|
+
* `expires: ${expiry}`;
|
|
1817
|
+
* const signature = await walletClient.signMessage({ account: address, message });
|
|
1818
|
+
* await registerUserForPoints({ userAddress: address, referrerAddress: referrer, chainId, signature, nonce, expiry });
|
|
1819
|
+
* ```
|
|
1820
|
+
*/
|
|
1821
|
+
async function registerUserForPoints({ userAddress, referrerAddress, chainId, signature, nonce, expiry, options, }) {
|
|
1822
|
+
try {
|
|
1823
|
+
if (!userAddress) {
|
|
1824
|
+
throw new Error('User address parameter is undefined.');
|
|
1825
|
+
}
|
|
1826
|
+
if (!chainId || !signature || !nonce || !expiry) {
|
|
1827
|
+
throw new Error('chainId, signature, nonce, and expiry are required for registering a user');
|
|
1828
|
+
}
|
|
1829
|
+
const response = await (0, core_1.fetchAugustPublic)(core_1.WEBSERVER_ENDPOINTS.public.points.register, {
|
|
1830
|
+
method: 'post',
|
|
1831
|
+
server: 'public',
|
|
1832
|
+
headers: options?.headers,
|
|
1833
|
+
data: {
|
|
1834
|
+
user_address: userAddress,
|
|
1835
|
+
...(referrerAddress && { referrer_address: referrerAddress }),
|
|
1836
|
+
chain_id: chainId,
|
|
1837
|
+
signature,
|
|
1838
|
+
nonce,
|
|
1839
|
+
expiry,
|
|
1840
|
+
},
|
|
1841
|
+
});
|
|
1842
|
+
return response;
|
|
1843
|
+
}
|
|
1844
|
+
catch (e) {
|
|
1845
|
+
core_1.Logger.log.error('registerUserForPoints', e, {
|
|
1846
|
+
userAddress,
|
|
1847
|
+
referrerAddress,
|
|
1848
|
+
});
|
|
1849
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1850
|
+
throw new Error(`#registerUserForPoints::${userAddress}:${errorMessage}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Fetch the points leaderboard data.
|
|
1855
|
+
* @param params - Optional parameters for pagination and sorting
|
|
1856
|
+
* @param options - Request options including headers
|
|
1857
|
+
* @returns Leaderboard response data
|
|
1858
|
+
*/
|
|
1859
|
+
async function fetchPointsLeaderboard({ params, options, }) {
|
|
1860
|
+
try {
|
|
1861
|
+
const { page = 1, perPage = 10, sortBy = 'totalPoints' } = params || {};
|
|
1862
|
+
const searchParams = new URLSearchParams({
|
|
1863
|
+
page: page.toString(),
|
|
1864
|
+
perPage: perPage.toString(),
|
|
1865
|
+
sortBy,
|
|
1866
|
+
});
|
|
1867
|
+
const response = await (0, core_1.fetchAugustPublic)(`${core_1.WEBSERVER_ENDPOINTS.public.points.leaderboard}?${searchParams.toString()}`, {
|
|
1868
|
+
headers: options?.headers,
|
|
1869
|
+
});
|
|
1870
|
+
if (!response.ok) {
|
|
1871
|
+
throw new Error(`Failed to fetch leaderboard: ${response.statusText}`);
|
|
1872
|
+
}
|
|
1873
|
+
return await response.json();
|
|
1874
|
+
}
|
|
1875
|
+
catch (e) {
|
|
1876
|
+
core_1.Logger.log.error('fetchPointsLeaderboard', e, { params });
|
|
1877
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1878
|
+
throw new Error(`#fetchPointsLeaderboard::${errorMessage}`);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Fetch the timestamp when yield was last realized for a vault.
|
|
1883
|
+
* Returns the assetsUpdatedOn timestamp from the vault contract.
|
|
1884
|
+
* @param vault - Vault contract address
|
|
1885
|
+
* @param options - RPC configuration
|
|
1886
|
+
* @returns Timestamp (Unix timestamp in seconds) when yield was last realized
|
|
1887
|
+
*/
|
|
1888
|
+
async function getYieldLastRealizedOn({ vault, options, }) {
|
|
1889
|
+
try {
|
|
1890
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1891
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers))?.[0];
|
|
1892
|
+
if (!tokenizedVault) {
|
|
1893
|
+
throw new Error(`Vault ${vault} not found`);
|
|
1894
|
+
}
|
|
1895
|
+
const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
1896
|
+
let assetsUpdatedOn;
|
|
1897
|
+
switch (version) {
|
|
1898
|
+
case 'evm-2': {
|
|
1899
|
+
const contract = (0, core_1.createContract)({
|
|
1900
|
+
provider,
|
|
1901
|
+
address: vault,
|
|
1902
|
+
abi: TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2,
|
|
1903
|
+
});
|
|
1904
|
+
assetsUpdatedOn = await contract.assetsUpdatedOn();
|
|
1905
|
+
break;
|
|
1906
|
+
}
|
|
1907
|
+
case 'evm-0': {
|
|
1908
|
+
const contract = (0, core_1.createContract)({
|
|
1909
|
+
provider,
|
|
1910
|
+
address: vault,
|
|
1911
|
+
abi: abis_1.ABI_LENDING_POOL_V3,
|
|
1912
|
+
});
|
|
1913
|
+
assetsUpdatedOn = await contract.assetsUpdatedOn();
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
case 'sol-0':
|
|
1917
|
+
case 'stellar-0': {
|
|
1918
|
+
return 0;
|
|
1919
|
+
}
|
|
1920
|
+
default: {
|
|
1921
|
+
// Old lending pools don't have assetsUpdatedOn function
|
|
1922
|
+
// Return 0 for legacy vaults
|
|
1923
|
+
return 0;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return Number(assetsUpdatedOn);
|
|
1927
|
+
}
|
|
1928
|
+
catch (e) {
|
|
1929
|
+
core_1.Logger.log.error('getYieldLastRealizedOn', e, { vault });
|
|
1930
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1931
|
+
throw new Error(`#getYieldLastRealizedOn::${vault}:${errorMessage}`);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Guard against the 2026-07-09 lifetime-PnL over-inflation failure mode.
|
|
1936
|
+
*
|
|
1937
|
+
* Lifetime PnL is `currentPosition + totalWithdrawn - totalDeposited`. When the
|
|
1938
|
+
* deposit/withdrawal history fails to load it collapses to empty, so both
|
|
1939
|
+
* `totalDeposited` and `totalWithdrawn` read as zero. If the wallet still holds
|
|
1940
|
+
* a live on-chain position, the formula degenerates to reporting the entire
|
|
1941
|
+
* position as pure profit — a plausible-looking but wrong number.
|
|
1942
|
+
*
|
|
1943
|
+
* This guard rejects exactly that state: a non-zero position with no recorded
|
|
1944
|
+
* deposits *and* no recorded withdrawals. Any of the following is treated as
|
|
1945
|
+
* consistent and passes:
|
|
1946
|
+
* - deposits are present (the normal, computable case);
|
|
1947
|
+
* - withdrawals are present (a fully-exited-then-re-entered position is still
|
|
1948
|
+
* computable — the withdrawal records prove history loaded);
|
|
1949
|
+
* - the current position is zero (the wallet never interacted or fully exited,
|
|
1950
|
+
* so a zero PnL is correct, not inflated).
|
|
1951
|
+
*
|
|
1952
|
+
* Pure and synchronous — no RPC, no I/O — so it is safe to call inside the hot
|
|
1953
|
+
* path with no added latency.
|
|
1954
|
+
*
|
|
1955
|
+
* Known imprecision — vault shares are transferable ERC-20 receipt tokens, so a
|
|
1956
|
+
* wallet that *received* shares via a direct token transfer (never deposited,
|
|
1957
|
+
* never withdrew) legitimately has `position > 0` with no history records and
|
|
1958
|
+
* will throw `HISTORY_UNAVAILABLE` even though its history loaded fine. This is
|
|
1959
|
+
* not a regression — the pre-guard formula reported that same position as 100%
|
|
1960
|
+
* profit, which was also wrong — but the error message is misleading for that
|
|
1961
|
+
* (rare) path. If receipt-token transfer becomes a real user flow, distinguish
|
|
1962
|
+
* it here (e.g. by checking for a share-transfer event) rather than widening
|
|
1963
|
+
* the guard.
|
|
1964
|
+
*
|
|
1965
|
+
* @param params - See {@link PnlHistoryConsistencyParams}.
|
|
1966
|
+
* @returns `void` when the history is consistent.
|
|
1967
|
+
* @throws {@link AugustHistoryUnavailableError} with code `HISTORY_UNAVAILABLE`
|
|
1968
|
+
* and `context: { vault, wallet }` when a non-zero position has neither
|
|
1969
|
+
* deposits nor withdrawals recorded. The message carries the
|
|
1970
|
+
* `#getVaultUserLifetimePnl::<vault>::<wallet>` prefix so it groups with the
|
|
1971
|
+
* other errors leaving the getter.
|
|
1972
|
+
* @example
|
|
1973
|
+
* ```ts
|
|
1974
|
+
* assertPnlHistoryConsistent({
|
|
1975
|
+
* vault,
|
|
1976
|
+
* wallet,
|
|
1977
|
+
* hasDeposits: depositsByAsset.size > 0,
|
|
1978
|
+
* totalWithdrawnRaw,
|
|
1979
|
+
* currentPositionRaw: currentPositionValue.raw,
|
|
1980
|
+
* });
|
|
1981
|
+
* ```
|
|
1982
|
+
*/
|
|
1983
|
+
function assertPnlHistoryConsistent({ vault, wallet, hasDeposits, totalWithdrawnRaw, currentPositionRaw, }) {
|
|
1984
|
+
const positionExists = currentPositionRaw > BigInt(0);
|
|
1985
|
+
const noHistory = !hasDeposits && totalWithdrawnRaw === BigInt(0);
|
|
1986
|
+
if (positionExists && noHistory) {
|
|
1987
|
+
throw new errors_1.AugustHistoryUnavailableError(`#getVaultUserLifetimePnl::${vault}::${wallet}:transaction history unavailable — a live position exists but no deposits or withdrawals were indexed, so lifetime PnL cannot be computed without over-reporting the entire position as profit`, { context: { vault, wallet } });
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Sum a wallet's per-asset deposits into the vault's own decimals, at face
|
|
1992
|
+
* value — 1 unit of any deposited asset is treated as 1 unit of the vault's
|
|
1993
|
+
* reporting currency, with no live price conversion.
|
|
1994
|
+
*
|
|
1995
|
+
* Used by {@link getVaultUserLifetimePnl} for `totalDeposited`. Pre-deposit
|
|
1996
|
+
* multi-asset vaults only accept dollar-pegged stablecoins (USDC, USDT, DAI,
|
|
1997
|
+
* USDS, RLUSD, etc.) into a dollar-denominated vault, so face value is a
|
|
1998
|
+
* reasonable stand-in for USD value. This intentionally does NOT re-quote
|
|
1999
|
+
* through a live price oracle: doing so priced a historical deposit at
|
|
2000
|
+
* today's market rate, so `totalDeposited` (and lifetimePnl) drifted between
|
|
2001
|
+
* calls purely from stablecoin peg noise, and approximately re-inflated
|
|
2002
|
+
* deposits back toward face value — which silently excluded the vault's
|
|
2003
|
+
* entry fee from PnL. The fee is money the user actually paid, so PnL must
|
|
2004
|
+
* be computed net of it.
|
|
2005
|
+
*
|
|
2006
|
+
* @param depositsByAsset - Raw deposited amount per asset, keyed by
|
|
2007
|
+
* lowercased asset address (or `'default'` / `'lz-default'` for
|
|
2008
|
+
* single-asset / cross-chain rows).
|
|
2009
|
+
* @param assetDecimals - Decimals for each key in `depositsByAsset`.
|
|
2010
|
+
* @param vaultDecimals - The vault's own decimals — the target scale every
|
|
2011
|
+
* asset is rescaled to.
|
|
2012
|
+
* @returns Total deposited, in raw base units at `vaultDecimals`.
|
|
2013
|
+
*/
|
|
2014
|
+
function sumDepositsAtFaceValue(depositsByAsset, assetDecimals, vaultDecimals) {
|
|
2015
|
+
let total = BigInt(0);
|
|
2016
|
+
for (const [assetAddress, rawAmount] of depositsByAsset) {
|
|
2017
|
+
const assetDec = assetDecimals.get(assetAddress) ?? vaultDecimals;
|
|
2018
|
+
if (assetDec === vaultDecimals) {
|
|
2019
|
+
total += rawAmount;
|
|
2020
|
+
}
|
|
2021
|
+
else if (assetDec > vaultDecimals) {
|
|
2022
|
+
total += rawAmount / BigInt(10 ** (assetDec - vaultDecimals));
|
|
2023
|
+
}
|
|
2024
|
+
else {
|
|
2025
|
+
total += rawAmount * BigInt(10 ** (vaultDecimals - assetDec));
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
return total;
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Calculate lifetime PnL for a user in a specific vault.
|
|
2032
|
+
*
|
|
2033
|
+
* Basic logic:
|
|
2034
|
+
* 1. Get user's list of deposits and withdrawals from subgraph
|
|
2035
|
+
* 2. Fetch user's current assets (balanceOf * sharePrice via convertToAssets)
|
|
2036
|
+
* 3. Calculate PnL = assets withdrawn + current assets - assets deposited
|
|
2037
|
+
*
|
|
2038
|
+
* `totalDeposited` is the face value of what the user actually paid in
|
|
2039
|
+
* (each deposited asset's raw amount, rescaled to the vault's decimals — no
|
|
2040
|
+
* live price conversion), so any entry/exit fee the vault charges is
|
|
2041
|
+
* automatically counted as a cost against PnL rather than excluded from it.
|
|
2042
|
+
* Pre-deposit multi-asset vaults are assumed to only accept dollar-pegged
|
|
2043
|
+
* stablecoins into a dollar-denominated vault; a vault accepting a
|
|
2044
|
+
* non-pegged deposit asset would need deposit-time price conversion, which
|
|
2045
|
+
* this function does not perform.
|
|
2046
|
+
*
|
|
2047
|
+
* @param vault - Vault contract address
|
|
2048
|
+
* @param wallet - User wallet address
|
|
2049
|
+
* @param options - RPC configuration and service options
|
|
2050
|
+
* @returns Lifetime PnL data in both native token and USD
|
|
2051
|
+
*/
|
|
2052
|
+
async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
2053
|
+
try {
|
|
2054
|
+
if (!vault)
|
|
2055
|
+
throw new Error('Vault input parameter is undefined.');
|
|
2056
|
+
if (!wallet)
|
|
2057
|
+
throw new Error('Wallet input parameter is undefined.');
|
|
2058
|
+
(0, utils_3.assertNotStellar)(vault, 'Lifetime PnL');
|
|
2059
|
+
if (!(0, ethers_1.isAddress)(vault))
|
|
2060
|
+
throw new Error(`Vault input parameter is not an address: ${String(vault)}`);
|
|
2061
|
+
if (!(0, ethers_1.isAddress)(wallet))
|
|
2062
|
+
throw new Error(`Wallet input parameter is not an address: ${String(wallet)}`);
|
|
2063
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
2064
|
+
// Parallelize independent operations
|
|
2065
|
+
const [decimals, userHistory, positions, tokenizedVaultResult] = await Promise.all([
|
|
2066
|
+
(0, core_1.getDecimals)(provider, vault),
|
|
2067
|
+
(0, vaults_1.getSubgraphUserHistory)(wallet, provider, vault),
|
|
2068
|
+
getVaultPositions({
|
|
2069
|
+
vault,
|
|
2070
|
+
wallet,
|
|
2071
|
+
options,
|
|
2072
|
+
}),
|
|
2073
|
+
(0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false),
|
|
2074
|
+
]);
|
|
2075
|
+
const tokenizedVault = tokenizedVaultResult?.[0];
|
|
2076
|
+
if (!tokenizedVault) {
|
|
2077
|
+
throw new Error(`Vault ${vault} not found`);
|
|
2078
|
+
}
|
|
2079
|
+
// getDecimals resolves to `undefined` on a failed/absent decimals() read.
|
|
2080
|
+
// Unguarded, that undefined flows into `10 ** decimals` (=> NaN) and
|
|
2081
|
+
// toNormalizedBn(x, undefined), and the first BigInt(NaN) throws the opaque
|
|
2082
|
+
// "The number NaN cannot be converted to a BigInt" error. Decimals must come
|
|
2083
|
+
// from the chain — never assume a fallback — so fail fast with actionable
|
|
2084
|
+
// context. Transient RPC blips recover on the caller's retry.
|
|
2085
|
+
if (typeof decimals !== 'number' || !Number.isFinite(decimals)) {
|
|
2086
|
+
throw new Error(`Failed to resolve decimals for vault ${vault}`);
|
|
2087
|
+
}
|
|
2088
|
+
// Fetch LayerZero deposits/redeems for supported vaults
|
|
2089
|
+
const lzVaultKey = (0, deposits_1.isLayerZeroVault)(vault);
|
|
2090
|
+
let lzDeposits = [];
|
|
2091
|
+
let lzRedeems = [];
|
|
2092
|
+
if (lzVaultKey) {
|
|
2093
|
+
const lzPromises = [
|
|
2094
|
+
(0, deposits_1.queryLayerZeroDeposits)(lzVaultKey, wallet)
|
|
2095
|
+
.then((d) => {
|
|
2096
|
+
lzDeposits = d;
|
|
2097
|
+
})
|
|
2098
|
+
.catch((e) => {
|
|
2099
|
+
core_1.Logger.log.warn('getVaultUserLifetimePnl:lzDeposits', `Failed to fetch LZ deposits for ${lzVaultKey}: ${e}`);
|
|
2100
|
+
}),
|
|
2101
|
+
];
|
|
2102
|
+
// Only earnAUSD has redeem events currently
|
|
2103
|
+
if (lzVaultKey === 'earnAUSD') {
|
|
2104
|
+
lzPromises.push((0, redeems_1.queryLayerZeroRedeems)(wallet)
|
|
2105
|
+
.then((r) => {
|
|
2106
|
+
lzRedeems = r;
|
|
2107
|
+
})
|
|
2108
|
+
.catch((e) => {
|
|
2109
|
+
core_1.Logger.log.warn('getVaultUserLifetimePnl:lzRedeems', `Failed to fetch LZ redeems for ${lzVaultKey}: ${e}`);
|
|
2110
|
+
}));
|
|
2111
|
+
}
|
|
2112
|
+
await Promise.all(lzPromises);
|
|
2113
|
+
}
|
|
2114
|
+
const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
2115
|
+
const currentPosition = positions.find((pos) => pos.vault?.toLowerCase() === vault.toLowerCase());
|
|
2116
|
+
// Start underlyingSymbol fetch (runs in parallel with processHistory)
|
|
2117
|
+
const underlyingSymbolPromise = (async () => {
|
|
2118
|
+
let underlyingTokenSymbol = 'UNKNOWN';
|
|
2119
|
+
let underlyingAssetAddress;
|
|
2120
|
+
try {
|
|
2121
|
+
// Use ABI_LENDING_POOL_V2 which has the asset() method
|
|
2122
|
+
const vaultContract = (0, core_1.createContract)({
|
|
2123
|
+
provider,
|
|
2124
|
+
address: vault,
|
|
2125
|
+
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
2126
|
+
});
|
|
2127
|
+
try {
|
|
2128
|
+
underlyingAssetAddress = (await vaultContract.asset());
|
|
2129
|
+
const underlyingContract = (0, core_1.createContract)({
|
|
2130
|
+
provider,
|
|
2131
|
+
address: underlyingAssetAddress,
|
|
2132
|
+
abi: abis_1.ABI_ERC20,
|
|
2133
|
+
});
|
|
2134
|
+
underlyingTokenSymbol = await underlyingContract.symbol();
|
|
2135
|
+
}
|
|
2136
|
+
catch (e) {
|
|
2137
|
+
core_1.Logger.log.warn('getVaultUserLifetimePnl:getUnderlyingSymbol', `Failed to get underlying token symbol: ${e}, falling back to receipt token symbol`);
|
|
2138
|
+
underlyingTokenSymbol =
|
|
2139
|
+
tokenizedVault.receipt_token_symbol || 'UNKNOWN';
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
catch (e) {
|
|
2143
|
+
core_1.Logger.log.warn('getVaultUserLifetimePnl:getUnderlyingSymbol', `Failed to get underlying token symbol: ${e}, falling back to receipt token symbol`);
|
|
2144
|
+
underlyingTokenSymbol =
|
|
2145
|
+
tokenizedVault.receipt_token_symbol || 'UNKNOWN';
|
|
2146
|
+
}
|
|
2147
|
+
return { underlyingTokenSymbol, underlyingAssetAddress };
|
|
2148
|
+
})();
|
|
2149
|
+
// Process history - count all withdrawal requests (both processed and pending)
|
|
2150
|
+
// We don't count processed withdrawals separately to avoid double counting
|
|
2151
|
+
// Group deposit amounts by asset address to handle different decimals
|
|
2152
|
+
const depositsByAsset = new Map();
|
|
2153
|
+
// Separate withdrawal tracking: shares need conversion, assets don't
|
|
2154
|
+
let totalWithdrawalsInShares = BigInt(0);
|
|
2155
|
+
let totalWithdrawalsInAssets = BigInt(0);
|
|
2156
|
+
for (const historyItem of userHistory) {
|
|
2157
|
+
const amount = BigInt(historyItem.amount);
|
|
2158
|
+
// ignore bad transactions when calculating pnl
|
|
2159
|
+
if ((0, core_1.isBadTransaction)(historyItem.transactionHash_, tokenizedVault.chain)) {
|
|
2160
|
+
continue;
|
|
2161
|
+
}
|
|
2162
|
+
if (historyItem.type === 'deposit') {
|
|
2163
|
+
// Use assetIn if available (V2 vaults), otherwise use a default key
|
|
2164
|
+
// Use proper type guard instead of type assertion
|
|
2165
|
+
const assetKey = 'assetIn' in historyItem && typeof historyItem.assetIn === 'string'
|
|
2166
|
+
? historyItem.assetIn.toLowerCase()
|
|
2167
|
+
: 'default';
|
|
2168
|
+
const current = depositsByAsset.get(assetKey) ?? BigInt(0);
|
|
2169
|
+
depositsByAsset.set(assetKey, current + amount);
|
|
2170
|
+
}
|
|
2171
|
+
else if (historyItem.type === 'withdraw-request') {
|
|
2172
|
+
// Withdraw requests are in shares, need conversion to assets
|
|
2173
|
+
totalWithdrawalsInShares += amount;
|
|
2174
|
+
}
|
|
2175
|
+
else if (historyItem.type === 'withdraw-processed' &&
|
|
2176
|
+
historyItem.isInstant) {
|
|
2177
|
+
// Instant withdrawals are already in assets, no conversion needed
|
|
2178
|
+
totalWithdrawalsInAssets += amount;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
// Include LayerZero cross-chain deposits and redeems
|
|
2182
|
+
if (lzVaultKey && lzDeposits.length > 0) {
|
|
2183
|
+
for (const lzDeposit of lzDeposits) {
|
|
2184
|
+
// LZ deposits assetAmt is the underlying asset amount deposited in hub-chain decimals.
|
|
2185
|
+
// This assumes the LZ subgraph indexes amounts in the same decimals as the hub vault
|
|
2186
|
+
// (e.g. 6 for USDC). If source/hub decimals diverge, normalization would be needed here.
|
|
2187
|
+
const assetKey = 'lz-default';
|
|
2188
|
+
const amount = BigInt(lzDeposit.assetAmt);
|
|
2189
|
+
const current = depositsByAsset.get(assetKey) ?? BigInt(0);
|
|
2190
|
+
depositsByAsset.set(assetKey, current + amount);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (lzVaultKey && lzRedeems.length > 0) {
|
|
2194
|
+
for (const lzRedeem of lzRedeems) {
|
|
2195
|
+
// LZ redeems shareAmt is in shares, needs conversion via share price
|
|
2196
|
+
totalWithdrawalsInShares += BigInt(lzRedeem.shareAmt);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
// Fetch decimals for each unique asset in parallel
|
|
2200
|
+
const assetDecimals = new Map();
|
|
2201
|
+
await Promise.all(Array.from(depositsByAsset.keys()).map(async (assetAddress) => {
|
|
2202
|
+
if (assetAddress === 'default' || assetAddress === 'lz-default') {
|
|
2203
|
+
assetDecimals.set(assetAddress, decimals);
|
|
2204
|
+
}
|
|
2205
|
+
else {
|
|
2206
|
+
const dec = await (0, core_1.getDecimals)(provider, assetAddress, false);
|
|
2207
|
+
assetDecimals.set(assetAddress, dec);
|
|
2208
|
+
}
|
|
2209
|
+
}));
|
|
2210
|
+
const totalDepositedNativeRaw = sumDepositsAtFaceValue(depositsByAsset, assetDecimals, decimals);
|
|
2211
|
+
const { underlyingTokenSymbol, underlyingAssetAddress } = await underlyingSymbolPromise;
|
|
2212
|
+
// Calculate current position value and fetch price in parallel
|
|
2213
|
+
let currentPositionValue = (0, core_1.toNormalizedBn)(0, decimals);
|
|
2214
|
+
const pricePromise = underlyingAssetAddress && tokenizedVault.chain
|
|
2215
|
+
? (0, core_1.fetchTokenPriceByAddress)(underlyingAssetAddress, tokenizedVault.chain, options?.headers).catch(() => 0)
|
|
2216
|
+
: Promise.resolve(0);
|
|
2217
|
+
const vaultContract = (0, core_1.createContract)({
|
|
2218
|
+
provider,
|
|
2219
|
+
address: vault,
|
|
2220
|
+
abi: version === 'evm-2' ? TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOL_V2,
|
|
2221
|
+
});
|
|
2222
|
+
let sharePriceRaw = BigInt(0);
|
|
2223
|
+
try {
|
|
2224
|
+
// Retried on transport blips only (bounded, no fallback); a vault that
|
|
2225
|
+
// genuinely has no `lpTokenAddress()` still throws into the catch below.
|
|
2226
|
+
const lpTokenAddress = version === 'evm-2'
|
|
2227
|
+
? await (0, core_1.getReceiptTokenAddressOrThrow)(provider, vault, 'getVaultUserLifetimePnl:receiptToken')
|
|
2228
|
+
: vault;
|
|
2229
|
+
let currentShares = BigInt(0);
|
|
2230
|
+
if (version === 'evm-2') {
|
|
2231
|
+
const lpTokenContract = (0, core_1.createContract)({
|
|
2232
|
+
provider,
|
|
2233
|
+
address: lpTokenAddress,
|
|
2234
|
+
abi: TokenizedVaultV2Receipt_1.ABI_TOKENIZED_VAULT_V2_RECEIPT,
|
|
2235
|
+
});
|
|
2236
|
+
currentShares = await lpTokenContract.balanceOf(wallet);
|
|
2237
|
+
}
|
|
2238
|
+
else {
|
|
2239
|
+
currentShares = await vaultContract.balanceOf(wallet);
|
|
2240
|
+
}
|
|
2241
|
+
// For LayerZero vaults, include receipt token balances on remote chains
|
|
2242
|
+
if (lzVaultKey) {
|
|
2243
|
+
// Build list of remote chains to check: prefer receipt_token_integrations from API,
|
|
2244
|
+
// fall back to LAYERZERO_VAULTS spoke chain config (OVault shareOFT addresses)
|
|
2245
|
+
const remoteChains = [];
|
|
2246
|
+
if (tokenizedVault.receipt_token_integrations?.length) {
|
|
2247
|
+
for (const rti of tokenizedVault.receipt_token_integrations) {
|
|
2248
|
+
if (rti.chain !== tokenizedVault.chain &&
|
|
2249
|
+
rti.address &&
|
|
2250
|
+
rti.chain) {
|
|
2251
|
+
remoteChains.push({
|
|
2252
|
+
chain: rti.chain,
|
|
2253
|
+
address: rti.address,
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
else {
|
|
2259
|
+
// Fallback: use spoke chain addresses from LAYERZERO_VAULTS config
|
|
2260
|
+
const lzVaultConfig = deposits_1.LAYERZERO_VAULTS[lzVaultKey];
|
|
2261
|
+
if (lzVaultConfig.spokeChains) {
|
|
2262
|
+
for (const [chainIdStr, address] of Object.entries(lzVaultConfig.spokeChains)) {
|
|
2263
|
+
const chainId = Number(chainIdStr);
|
|
2264
|
+
if (chainId !== tokenizedVault.chain) {
|
|
2265
|
+
remoteChains.push({
|
|
2266
|
+
chain: chainId,
|
|
2267
|
+
address: address,
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
if (remoteChains.length > 0) {
|
|
2274
|
+
const remoteBalances = await Promise.all(remoteChains.map(async ({ chain, address }) => {
|
|
2275
|
+
try {
|
|
2276
|
+
const rpcUrls = (0, core_1.getFallbackRpcUrls)(chain);
|
|
2277
|
+
if (!rpcUrls.length) {
|
|
2278
|
+
core_1.Logger.log.warn('getVaultUserLifetimePnl:lzRemoteBalance', `No RPC URL for chain ${chain}, skipping balance check`);
|
|
2279
|
+
return BigInt(0);
|
|
2280
|
+
}
|
|
2281
|
+
const remoteProvider = (0, core_1.createProvider)(rpcUrls[0]);
|
|
2282
|
+
const remoteTokenContract = (0, core_1.createContract)({
|
|
2283
|
+
provider: remoteProvider,
|
|
2284
|
+
address,
|
|
2285
|
+
abi: abis_1.ABI_ERC20,
|
|
2286
|
+
});
|
|
2287
|
+
const [rawBalance, remoteDecimals] = await Promise.all([
|
|
2288
|
+
remoteTokenContract.balanceOf(wallet),
|
|
2289
|
+
remoteTokenContract.decimals(),
|
|
2290
|
+
]);
|
|
2291
|
+
const remDec = Number(remoteDecimals) || decimals;
|
|
2292
|
+
// Normalize remote balance to hub chain decimals
|
|
2293
|
+
if (remDec > decimals) {
|
|
2294
|
+
return rawBalance / BigInt(10 ** (remDec - decimals));
|
|
2295
|
+
}
|
|
2296
|
+
else if (remDec < decimals) {
|
|
2297
|
+
return rawBalance * BigInt(10 ** (decimals - remDec));
|
|
2298
|
+
}
|
|
2299
|
+
return rawBalance;
|
|
2300
|
+
}
|
|
2301
|
+
catch (e) {
|
|
2302
|
+
core_1.Logger.log.warn('getVaultUserLifetimePnl:lzRemoteBalance', `Failed to fetch balance on chain ${chain}: ${e}`);
|
|
2303
|
+
return BigInt(0);
|
|
2304
|
+
}
|
|
2305
|
+
}));
|
|
2306
|
+
for (const bal of remoteBalances) {
|
|
2307
|
+
currentShares += bal;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
if (version === 'evm-2') {
|
|
2312
|
+
sharePriceRaw = await vaultContract.getSharePrice();
|
|
2313
|
+
}
|
|
2314
|
+
else {
|
|
2315
|
+
const [totalAssets, totalSupply] = await Promise.all([
|
|
2316
|
+
vaultContract.totalAssets(),
|
|
2317
|
+
vaultContract.totalSupply(),
|
|
2318
|
+
]);
|
|
2319
|
+
if (totalSupply === BigInt(0)) {
|
|
2320
|
+
sharePriceRaw = BigInt(10 ** decimals); // 1:1 if no supply
|
|
2321
|
+
}
|
|
2322
|
+
else {
|
|
2323
|
+
sharePriceRaw = (totalAssets * BigInt(10 ** decimals)) / totalSupply;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
const currentPositionValueRaw = (currentShares * sharePriceRaw) / BigInt(10 ** decimals);
|
|
2327
|
+
currentPositionValue = (0, core_1.toNormalizedBn)(currentPositionValueRaw, decimals);
|
|
2328
|
+
}
|
|
2329
|
+
catch (e) {
|
|
2330
|
+
core_1.Logger.log.warn('getVaultUserLifetimePnl:getCurrentSharesAndPrice', `Failed to get current shares and share price: ${e}, falling back to wallet balance`);
|
|
2331
|
+
if (currentPosition?.walletBalance) {
|
|
2332
|
+
currentPositionValue = currentPosition.walletBalance;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
// Calculate totalWithdrawn combining shares (converted) and assets (as-is)
|
|
2336
|
+
let totalWithdrawnRaw = totalWithdrawalsInAssets;
|
|
2337
|
+
if (version === 'evm-2' && sharePriceRaw > BigInt(0)) {
|
|
2338
|
+
// Only convert shares to assets, instant withdrawals are already in assets
|
|
2339
|
+
totalWithdrawnRaw +=
|
|
2340
|
+
(totalWithdrawalsInShares * sharePriceRaw) / BigInt(10 ** decimals);
|
|
2341
|
+
}
|
|
2342
|
+
else {
|
|
2343
|
+
// For non-evm-2 or when share price is 0, treat shares as assets
|
|
2344
|
+
totalWithdrawnRaw += totalWithdrawalsInShares;
|
|
2345
|
+
}
|
|
2346
|
+
const totalWithdrawn = (0, core_1.toNormalizedBn)(totalWithdrawnRaw, decimals);
|
|
2347
|
+
// Guard the 2026-07-09 over-inflation incident: a live position with no
|
|
2348
|
+
// deposits *and* no withdrawals means the history failed to load, and the
|
|
2349
|
+
// PnL formula would report the whole position as profit. Fail loudly with a
|
|
2350
|
+
// typed error instead of returning a fabricated number.
|
|
2351
|
+
assertPnlHistoryConsistent({
|
|
2352
|
+
vault,
|
|
2353
|
+
wallet,
|
|
2354
|
+
hasDeposits: depositsByAsset.size > 0,
|
|
2355
|
+
totalWithdrawnRaw,
|
|
2356
|
+
currentPositionRaw: BigInt(currentPositionValue.raw),
|
|
2357
|
+
});
|
|
2358
|
+
const tokenPriceUsd = await pricePromise;
|
|
2359
|
+
// Validate token price and log warning if invalid
|
|
2360
|
+
const effectiveTokenPrice = tokenPriceUsd > 0 ? tokenPriceUsd : 1;
|
|
2361
|
+
if (tokenPriceUsd <= 0) {
|
|
2362
|
+
core_1.Logger.log.warn('getVaultUserLifetimePnl:tokenPrice', `Invalid underlying token price: ${tokenPriceUsd}, using fallback of 1`);
|
|
2363
|
+
}
|
|
2364
|
+
// totalDeposited is already in the vault's own decimals (face value,
|
|
2365
|
+
// computed above) — convert to USD with the same underlying price used
|
|
2366
|
+
// for the other two legs below, so all three legs share one conversion
|
|
2367
|
+
// factor instead of drifting independently against each other.
|
|
2368
|
+
const totalDeposited = (0, core_1.toNormalizedBn)(totalDepositedNativeRaw, decimals);
|
|
2369
|
+
const totalDepositedUsd = Number(totalDeposited.normalized) * effectiveTokenPrice;
|
|
2370
|
+
const totalWithdrawnUsd = Number(totalWithdrawn.normalized) * effectiveTokenPrice;
|
|
2371
|
+
const currentPositionValueUsd = Number(currentPositionValue.normalized) * effectiveTokenPrice;
|
|
2372
|
+
// Calculate lifetimePnl in USD first (since deposits may be in different tokens with different prices)
|
|
2373
|
+
const lifetimePnlUsd = currentPositionValueUsd + totalWithdrawnUsd - totalDepositedUsd;
|
|
2374
|
+
// Use BigInt-scaled arithmetic to minimize precision loss when converting USD to native
|
|
2375
|
+
const scaleFactor = BigInt(10 ** 18); // High precision scale
|
|
2376
|
+
const tokenPriceScaled = BigInt(Math.round(effectiveTokenPrice * Number(scaleFactor)));
|
|
2377
|
+
// Convert lifetimePnlUsd to native units
|
|
2378
|
+
const lifetimePnlUsdScaled = BigInt(Math.round(lifetimePnlUsd * Number(scaleFactor)));
|
|
2379
|
+
const lifetimePnlRaw = tokenPriceScaled > BigInt(0)
|
|
2380
|
+
? (lifetimePnlUsdScaled * BigInt(10 ** decimals)) / tokenPriceScaled
|
|
2381
|
+
: BigInt(0);
|
|
2382
|
+
const lifetimePnl = (0, core_1.toNormalizedBn)(lifetimePnlRaw, decimals);
|
|
2383
|
+
const pnlPercentage = totalDepositedUsd > 0
|
|
2384
|
+
? Number.parseFloat(((lifetimePnlUsd / totalDepositedUsd) * 100).toFixed(2))
|
|
2385
|
+
: 0;
|
|
2386
|
+
return {
|
|
2387
|
+
tokenSymbol: underlyingTokenSymbol,
|
|
2388
|
+
totalDeposited,
|
|
2389
|
+
totalDepositedUsd,
|
|
2390
|
+
totalWithdrawn,
|
|
2391
|
+
totalWithdrawnUsd,
|
|
2392
|
+
currentPositionValue,
|
|
2393
|
+
currentPositionValueUsd,
|
|
2394
|
+
lifetimePnl,
|
|
2395
|
+
lifetimePnlUsd,
|
|
2396
|
+
pnlPercentage,
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
catch (e) {
|
|
2400
|
+
core_1.Logger.log.error('getVaultUserLifetimePnl', e, { vault, wallet });
|
|
2401
|
+
// Preserve the typed history guard so callers can distinguish an
|
|
2402
|
+
// unavailable-history failure from a generic getter error. Its message
|
|
2403
|
+
// already carries the `#getVaultUserLifetimePnl::vault::wallet` prefix.
|
|
2404
|
+
if (e instanceof errors_1.AugustHistoryUnavailableError)
|
|
2405
|
+
throw e;
|
|
2406
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2407
|
+
throw new Error(`#getVaultUserLifetimePnl::${vault}::${wallet}:${errorMessage}`);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Calculate PnL for a vault (vault-level, not user-specific).
|
|
2412
|
+
* Returns the vault's overall profit and loss across all users.
|
|
2413
|
+
*
|
|
2414
|
+
* @param vault - Vault contract address
|
|
2415
|
+
* @param options - RPC configuration and service options
|
|
2416
|
+
* @returns Vault PnL in USD and notional value
|
|
2417
|
+
*/
|
|
2418
|
+
async function getVaultPnl({ vault, options, }) {
|
|
2419
|
+
try {
|
|
2420
|
+
if (!vault)
|
|
2421
|
+
throw new Error('Vault input parameter is undefined.');
|
|
2422
|
+
(0, utils_3.assertNotStellar)(vault, 'Vault PnL');
|
|
2423
|
+
if (!(0, ethers_1.isAddress)(vault))
|
|
2424
|
+
throw new Error(`Vault input parameter is not an address: ${String(vault)}`);
|
|
2425
|
+
// Get vault data
|
|
2426
|
+
const vaultData = await getVault({
|
|
2427
|
+
vault,
|
|
2428
|
+
loans: false,
|
|
2429
|
+
allocations: false,
|
|
2430
|
+
options,
|
|
2431
|
+
});
|
|
2432
|
+
if (!vaultData) {
|
|
2433
|
+
throw new Error(`Vault ${vault} not found`);
|
|
2434
|
+
}
|
|
2435
|
+
// Check if vault has daily_pnl_per_share and historical_snapshots
|
|
2436
|
+
if (!vaultData.dailyPnlPerShare ||
|
|
2437
|
+
!Array.isArray(vaultData.dailyPnlPerShare) ||
|
|
2438
|
+
vaultData.dailyPnlPerShare.length === 0) {
|
|
2439
|
+
return {
|
|
2440
|
+
totalPnl: (0, core_1.toNormalizedBn)(0, vaultData.decimals),
|
|
2441
|
+
pnlUsd: 0,
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
if (!vaultData.historical_snapshots ||
|
|
2445
|
+
!Array.isArray(vaultData.historical_snapshots) ||
|
|
2446
|
+
vaultData.historical_snapshots.length === 0) {
|
|
2447
|
+
return {
|
|
2448
|
+
totalPnl: (0, core_1.toNormalizedBn)(0, vaultData.decimals),
|
|
2449
|
+
pnlUsd: 0,
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
const decimals = vaultData.decimals;
|
|
2453
|
+
// Filter out dates with zero PnL
|
|
2454
|
+
const validDailyPnl = vaultData.dailyPnlPerShare.filter((dayData) => dayData.value && dayData.value !== 0);
|
|
2455
|
+
// Create a map of historical_snapshots by date (YYYY-MM-DD) for quick lookup
|
|
2456
|
+
const snapshotsByDate = new Map();
|
|
2457
|
+
vaultData.historical_snapshots.forEach((snapshot) => {
|
|
2458
|
+
const snapshotDate = new Date(snapshot.snapshot_datetime);
|
|
2459
|
+
const dateKey = snapshotDate.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
2460
|
+
snapshotsByDate.set(dateKey, snapshot);
|
|
2461
|
+
});
|
|
2462
|
+
// Calculate PnL for each day using historical_snapshots
|
|
2463
|
+
let totalPnlRaw = BigInt(0);
|
|
2464
|
+
validDailyPnl.forEach((dayData) => {
|
|
2465
|
+
const dayTimestamp = dayData.timestamp;
|
|
2466
|
+
const dailyPnlPerShare = dayData.value;
|
|
2467
|
+
// Get date key (YYYY-MM-DD) from timestamp
|
|
2468
|
+
const dayDate = new Date(dayTimestamp);
|
|
2469
|
+
const dateKey = dayDate.toISOString().split('T')[0];
|
|
2470
|
+
// Find matching snapshot for this date
|
|
2471
|
+
const snapshot = snapshotsByDate.get(dateKey);
|
|
2472
|
+
if (!snapshot?.total_shares) {
|
|
2473
|
+
core_1.Logger.log.warn('getVaultPnl:snapshotNotFound', `No snapshot found for date ${dateKey}`);
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
// Convert normalized numbers to BigInt using toNormalizedBn's normalized string
|
|
2477
|
+
const dailyPnlNorm = (0, core_1.toNormalizedBn)(dailyPnlPerShare, decimals);
|
|
2478
|
+
const totalSharesNorm = (0, core_1.toNormalizedBn)(snapshot.total_shares, decimals);
|
|
2479
|
+
// Convert normalized strings to BigInt (split, pad fraction, combine) - all inline
|
|
2480
|
+
const [dailyWhole = '0', dailyFrac = ''] = dailyPnlNorm.normalized.split('.');
|
|
2481
|
+
const dailyPnlPerShareScaled = BigInt(dailyWhole + (dailyFrac || '').padEnd(decimals, '0').slice(0, decimals));
|
|
2482
|
+
const [sharesWhole = '0', sharesFrac = ''] = totalSharesNorm.normalized.split('.');
|
|
2483
|
+
const totalSharesScaled = BigInt(sharesWhole +
|
|
2484
|
+
(sharesFrac || '').padEnd(decimals, '0').slice(0, decimals));
|
|
2485
|
+
// Calculate: daily_pnl_per_share[day] * total_shares[day] (both in raw units)
|
|
2486
|
+
const dayPnlRaw = (dailyPnlPerShareScaled * totalSharesScaled) / BigInt(10 ** decimals);
|
|
2487
|
+
totalPnlRaw += dayPnlRaw;
|
|
2488
|
+
});
|
|
2489
|
+
// Convert totalPnlRaw (BigInt) to normalized number using toNormalizedBn
|
|
2490
|
+
const totalPnl = (0, core_1.toNormalizedBn)(totalPnlRaw, decimals);
|
|
2491
|
+
const totalPnlNormalized = Number(totalPnl.normalized);
|
|
2492
|
+
const chainId = vaultData.chainId;
|
|
2493
|
+
// Get receipt token address (HLPe) from vaultData - no need for on-chain lookup
|
|
2494
|
+
const lpTokenAddress = vaultData.receipt.address;
|
|
2495
|
+
// Fetch HLPe (lpTokenAddress) price in USD
|
|
2496
|
+
let tokenPriceUsd = 0;
|
|
2497
|
+
try {
|
|
2498
|
+
tokenPriceUsd = await (0, core_1.fetchTokenPriceByAddress)(lpTokenAddress, chainId, options?.headers);
|
|
2499
|
+
}
|
|
2500
|
+
catch (error) {
|
|
2501
|
+
core_1.Logger.log.warn('getVaultPnl:priceFetch', `Failed to fetch HLPe token price: ${error}`);
|
|
2502
|
+
}
|
|
2503
|
+
// Convert to USD and format to 6 decimal places
|
|
2504
|
+
const pnlUsd = Number((totalPnlNormalized * tokenPriceUsd).toFixed(6));
|
|
2505
|
+
return {
|
|
2506
|
+
totalPnl,
|
|
2507
|
+
pnlUsd,
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
catch (e) {
|
|
2511
|
+
core_1.Logger.log.error('getVaultPnl', e, { vault });
|
|
2512
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2513
|
+
throw new Error(`#getVaultPnl::${vault}:${errorMessage}`);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
/*
|
|
2517
|
+
* Fetch historical timeseries data for a vault.
|
|
2518
|
+
* Returns TVL, APY, PnL, share price, and other metrics over time.
|
|
2519
|
+
* @param vault - Vault contract address
|
|
2520
|
+
* @param nDays - Number of days of historical data (default 30, min 1)
|
|
2521
|
+
* @param options - Request options including headers
|
|
2522
|
+
* @returns Historical timeseries data with date string keys
|
|
2523
|
+
*/
|
|
2524
|
+
async function getVaultHistoricalTimeseries({ vault, nDays, options, }) {
|
|
2525
|
+
try {
|
|
2526
|
+
if (!vault)
|
|
2527
|
+
throw new Error('Vault input parameter is undefined.');
|
|
2528
|
+
const response = await (0, core_1.fetchVaultHistoricalTimeseries)(vault, nDays, options?.headers);
|
|
2529
|
+
return response;
|
|
2530
|
+
}
|
|
2531
|
+
catch (e) {
|
|
2532
|
+
core_1.Logger.log.error('getVaultHistoricalTimeseries', e, { vault, nDays });
|
|
2533
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2534
|
+
throw new Error(`#getVaultHistoricalTimeseries::${vault}:${errorMessage}`);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Fetch annualized APY metrics for a vault.
|
|
2539
|
+
*
|
|
2540
|
+
* Supported Vaults: cUSDO, tETH, wstETH, rsETH
|
|
2541
|
+
*
|
|
2542
|
+
* @deprecated The `hgETH30dLiquidAPY` and `hgETH7dLiquidAPY` response fields are deprecated.
|
|
2543
|
+
* These fields will be removed on 2026-01-01.
|
|
2544
|
+
* Use `liquidAPY30Day` and `liquidAPY7Day` fields instead.
|
|
2545
|
+
*
|
|
2546
|
+
* @param vault - Vault contract address
|
|
2547
|
+
* @param options - Request options including headers
|
|
2548
|
+
* @returns Annualized APY data including liquidity APY
|
|
2549
|
+
*/
|
|
2550
|
+
async function getVaultAnnualizedApy({ vault, options, }) {
|
|
2551
|
+
try {
|
|
2552
|
+
if (!vault) {
|
|
2553
|
+
throw new Error('Vault address parameter is undefined.');
|
|
2554
|
+
}
|
|
2555
|
+
if (!(0, ethers_1.isAddress)(vault)) {
|
|
2556
|
+
throw new Error(`Vault parameter is not an address: ${String(vault)}`);
|
|
2557
|
+
}
|
|
2558
|
+
const endpoint = core_1.WEBSERVER_ENDPOINTS.public.tokenizedVault.annualizedApy(vault);
|
|
2559
|
+
const response = await (0, core_1.fetchAugustPublic)(endpoint, {
|
|
2560
|
+
headers: options?.headers,
|
|
2561
|
+
server: 'public',
|
|
2562
|
+
});
|
|
2563
|
+
if (!response.ok) {
|
|
2564
|
+
throw new Error(`Failed to fetch annualized APY: ${response.statusText}`);
|
|
2565
|
+
}
|
|
2566
|
+
return (await response.json());
|
|
2567
|
+
}
|
|
2568
|
+
catch (e) {
|
|
2569
|
+
core_1.Logger.log.error('getVaultAnnualizedApy', e, { vault });
|
|
2570
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2571
|
+
throw new Error(`#getVaultAnnualizedApy::${vault}:${errorMessage}`);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Fetch summary data for a vault (name, type, chain, recent returns).
|
|
2576
|
+
* @param vault - Vault contract address
|
|
2577
|
+
* @param options - Request options including headers
|
|
2578
|
+
* @returns Vault summary data
|
|
2579
|
+
*/
|
|
2580
|
+
async function getVaultSummary({ vault, options, }) {
|
|
2581
|
+
try {
|
|
2582
|
+
if (!vault) {
|
|
2583
|
+
throw new Error('Vault address parameter is undefined.');
|
|
2584
|
+
}
|
|
2585
|
+
(0, utils_3.assertNotStellar)(vault, 'Vault summary');
|
|
2586
|
+
if (!(0, ethers_1.isAddress)(vault)) {
|
|
2587
|
+
throw new Error(`Vault parameter is not an address: ${String(vault)}`);
|
|
2588
|
+
}
|
|
2589
|
+
const endpoint = core_1.WEBSERVER_ENDPOINTS.public.tokenizedVault.summary(vault);
|
|
2590
|
+
const response = await (0, core_1.fetchAugustPublic)(endpoint, {
|
|
2591
|
+
headers: options?.headers,
|
|
2592
|
+
server: 'public',
|
|
2593
|
+
});
|
|
2594
|
+
if (!response.ok) {
|
|
2595
|
+
throw new Error(`Failed to fetch vault summary: ${response.statusText}`);
|
|
2596
|
+
}
|
|
2597
|
+
return (await response.json());
|
|
2598
|
+
}
|
|
2599
|
+
catch (e) {
|
|
2600
|
+
core_1.Logger.log.error('getVaultSummary', e, { vault });
|
|
2601
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2602
|
+
throw new Error(`#getVaultSummary::${vault}:${errorMessage}`);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* Fetch withdrawal summary and pending queue for a vault.
|
|
2607
|
+
* @param vault - Vault contract address
|
|
2608
|
+
* @param chain - Chain identifier (e.g., chain ID as string)
|
|
2609
|
+
* @param options - Request options including headers
|
|
2610
|
+
* @returns Withdrawal summary and pending queue
|
|
2611
|
+
*/
|
|
2612
|
+
async function getVaultWithdrawals({ vault, chain, options, }) {
|
|
2613
|
+
try {
|
|
2614
|
+
if (!vault) {
|
|
2615
|
+
throw new Error('Vault address parameter is undefined.');
|
|
2616
|
+
}
|
|
2617
|
+
(0, utils_3.assertNotStellar)(vault, 'Vault withdrawals');
|
|
2618
|
+
if (!(0, ethers_1.isAddress)(vault)) {
|
|
2619
|
+
throw new Error(`Vault parameter is not an address: ${String(vault)}`);
|
|
2620
|
+
}
|
|
2621
|
+
if (!chain) {
|
|
2622
|
+
throw new Error('Chain parameter is undefined.');
|
|
2623
|
+
}
|
|
2624
|
+
const endpoint = core_1.WEBSERVER_ENDPOINTS.public.tokenizedVault.withdrawals(chain, vault);
|
|
2625
|
+
const response = await (0, core_1.fetchAugustPublic)(endpoint, {
|
|
2626
|
+
headers: options?.headers,
|
|
2627
|
+
server: 'public',
|
|
2628
|
+
});
|
|
2629
|
+
if (!response.ok) {
|
|
2630
|
+
throw new Error(`Failed to fetch vault withdrawals: ${response.statusText}`);
|
|
2631
|
+
}
|
|
2632
|
+
return (await response.json());
|
|
2633
|
+
}
|
|
2634
|
+
catch (e) {
|
|
2635
|
+
core_1.Logger.log.error('getVaultWithdrawals', e, { vault, chain });
|
|
2636
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2637
|
+
throw new Error(`#getVaultWithdrawals::${vault}::${chain}:${errorMessage}`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Fetch pending redemptions for a vault with liquidity analysis.
|
|
2642
|
+
* @param vault - Vault contract address
|
|
2643
|
+
* @param pastDays - Number of past days to include (default 7, min 1, max 30)
|
|
2644
|
+
* @param futureDays - Number of future days to include (default 14, min 1, max 30)
|
|
2645
|
+
* @param options - Request options including headers
|
|
2646
|
+
* @returns Pending redemptions grouped by date with liquidity summary
|
|
2647
|
+
*/
|
|
2648
|
+
async function getVaultPendingRedemptions({ vault, pastDays, futureDays, options, }) {
|
|
2649
|
+
try {
|
|
2650
|
+
if (!vault)
|
|
2651
|
+
throw new Error('Vault address parameter is undefined.');
|
|
2652
|
+
if (!(0, ethers_1.isAddress)(vault))
|
|
2653
|
+
throw new Error(`Vault parameter is not an address: ${String(vault)}`);
|
|
2654
|
+
if (pastDays !== undefined && (pastDays < 1 || pastDays > 30))
|
|
2655
|
+
throw new Error('pastDays must be between 1 and 30');
|
|
2656
|
+
if (futureDays !== undefined && (futureDays < 1 || futureDays > 30))
|
|
2657
|
+
throw new Error('futureDays must be between 1 and 30');
|
|
2658
|
+
const params = new URLSearchParams();
|
|
2659
|
+
if (pastDays !== undefined)
|
|
2660
|
+
params.set('past_days', pastDays.toString());
|
|
2661
|
+
if (futureDays !== undefined)
|
|
2662
|
+
params.set('future_days', futureDays.toString());
|
|
2663
|
+
const qs = params.toString();
|
|
2664
|
+
const endpoint = `${core_1.WEBSERVER_ENDPOINTS.public.vaults.pendingRedemptions(vault)}${qs ? `?${qs}` : ''}`;
|
|
2665
|
+
const response = await (0, core_1.fetchAugustPublic)(endpoint, {
|
|
2666
|
+
headers: options?.headers,
|
|
2667
|
+
server: 'public',
|
|
2668
|
+
});
|
|
2669
|
+
if (!response.ok) {
|
|
2670
|
+
throw new Error(`Failed to fetch pending redemptions: ${response.statusText}`);
|
|
2671
|
+
}
|
|
2672
|
+
const raw = await response.json();
|
|
2673
|
+
return {
|
|
2674
|
+
vault: {
|
|
2675
|
+
address: raw.vault.address,
|
|
2676
|
+
chainId: raw.vault.chain_id,
|
|
2677
|
+
name: raw.vault.name,
|
|
2678
|
+
symbol: raw.vault.symbol,
|
|
2679
|
+
},
|
|
2680
|
+
asset: {
|
|
2681
|
+
address: raw.asset.address,
|
|
2682
|
+
symbol: raw.asset.symbol,
|
|
2683
|
+
decimals: raw.asset.decimals,
|
|
2684
|
+
},
|
|
2685
|
+
liquidity: {
|
|
2686
|
+
available: raw.liquidity.available,
|
|
2687
|
+
},
|
|
2688
|
+
summary: {
|
|
2689
|
+
totalPending: raw.summary.total_pending,
|
|
2690
|
+
pendingCount: raw.summary.pending_count,
|
|
2691
|
+
pendingToLiquidityRatio: raw.summary.pending_to_liquidity_ratio,
|
|
2692
|
+
isLiquiditySufficient: raw.summary.is_liquidity_sufficient,
|
|
2693
|
+
shortfall: raw.summary.shortfall,
|
|
2694
|
+
nextRedemptionDate: raw.summary.next_redemption_date,
|
|
2695
|
+
isOverdue: raw.summary.is_overdue,
|
|
2696
|
+
},
|
|
2697
|
+
redemptionsByDate: raw.redemptions_by_date.map((item) => ({
|
|
2698
|
+
date: item.date,
|
|
2699
|
+
assets: item.assets,
|
|
2700
|
+
daysUntilDue: item.days_until_due,
|
|
2701
|
+
isOverdue: item.is_overdue,
|
|
2702
|
+
})),
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
catch (e) {
|
|
2706
|
+
core_1.Logger.log.error('getVaultPendingRedemptions', e, {
|
|
2707
|
+
vault,
|
|
2708
|
+
pastDays,
|
|
2709
|
+
futureDays,
|
|
2710
|
+
});
|
|
2711
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2712
|
+
throw new Error(`#getVaultPendingRedemptions::${vault}:${errorMessage}`);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* Preview the amount of assets that would be received for redeeming shares (queued redemption).
|
|
2717
|
+
* @param vault - Vault contract address
|
|
2718
|
+
* @param sharesAmount - Amount of shares to redeem (human-readable, raw bigint, or string)
|
|
2719
|
+
* @param options - RPC configuration
|
|
2720
|
+
* @returns The amount of assets as an INormalizedNumber with `normalized` and `raw` fields
|
|
2721
|
+
*/
|
|
2722
|
+
async function getPreviewRedemption({ vault, sharesAmount, options, }) {
|
|
2723
|
+
try {
|
|
2724
|
+
if (!vault)
|
|
2725
|
+
throw new Error('Vault address parameter is undefined.');
|
|
2726
|
+
(0, utils_3.assertNotStellar)(vault, 'Preview redemption');
|
|
2727
|
+
if (!(0, ethers_1.isAddress)(vault)) {
|
|
2728
|
+
throw new Error(`Vault parameter is not an address: ${String(vault)}`);
|
|
2729
|
+
}
|
|
2730
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
2731
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers))?.[0];
|
|
2732
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
2733
|
+
let assets;
|
|
2734
|
+
let decimals;
|
|
2735
|
+
if (vaultVersion === 'evm-2') {
|
|
2736
|
+
const contract = (0, core_1.createContract)({
|
|
2737
|
+
address: vault,
|
|
2738
|
+
abi: TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2,
|
|
2739
|
+
provider,
|
|
2740
|
+
});
|
|
2741
|
+
// Retried on transport blips only (bounded, no fallback); a misroute
|
|
2742
|
+
// still surfaces the original error.
|
|
2743
|
+
const lpTokenAddress = await (0, core_1.getReceiptTokenAddressOrThrow)(provider, vault, 'getPreviewRedemption:receiptToken');
|
|
2744
|
+
const lpTokenContract = (0, core_1.createContract)({
|
|
2745
|
+
address: lpTokenAddress,
|
|
2746
|
+
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
2747
|
+
provider,
|
|
2748
|
+
});
|
|
2749
|
+
decimals = Number(await lpTokenContract.decimals());
|
|
2750
|
+
const sharesBigInt = BigInt((0, core_1.toNormalizedBn)(sharesAmount, decimals).raw);
|
|
2751
|
+
const [assetsAmount] = await contract.previewRedemption(sharesBigInt, false);
|
|
2752
|
+
assets = assetsAmount;
|
|
2753
|
+
}
|
|
2754
|
+
else {
|
|
2755
|
+
const contract = (0, core_1.createContract)({
|
|
2756
|
+
address: vault,
|
|
2757
|
+
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
2758
|
+
provider,
|
|
2759
|
+
});
|
|
2760
|
+
decimals = Number(await contract.decimals());
|
|
2761
|
+
const sharesBigInt = BigInt((0, core_1.toNormalizedBn)(sharesAmount, decimals).raw);
|
|
2762
|
+
assets = await contract.previewRedeem(sharesBigInt);
|
|
2763
|
+
}
|
|
2764
|
+
return (0, core_1.toNormalizedBn)(assets, decimals);
|
|
2765
|
+
}
|
|
2766
|
+
catch (e) {
|
|
2767
|
+
core_1.Logger.log.error('previewRedemption', e, { vault, sharesAmount });
|
|
2768
|
+
throw new Error(`Failed to preview redemption for ${vault}: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Helper: Decode a processing transaction to extract (year, month, day).
|
|
2773
|
+
*
|
|
2774
|
+
* Note: Multicall-wrapped transactions cannot be decoded from calldata or logs
|
|
2775
|
+
* (the WithdrawalProcessed event doesn't include the date). When decoding fails,
|
|
2776
|
+
* the caller falls back to timestamp-based date estimation.
|
|
2777
|
+
*/
|
|
2778
|
+
async function decodeProcessingTransaction(txHash, provider, iface) {
|
|
2779
|
+
const tx = await provider.getTransaction(txHash);
|
|
2780
|
+
if (!tx) {
|
|
2781
|
+
return {
|
|
2782
|
+
functionName: 'unknown',
|
|
2783
|
+
year: 0,
|
|
2784
|
+
month: 0,
|
|
2785
|
+
day: 0,
|
|
2786
|
+
error: 'Transaction not found',
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
return (0, call_data_decoder_1.decodeWithdrawalProcessing)(tx.data, iface);
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Returns all withdrawal requests for a vault, enriched with computed claimable dates
|
|
2793
|
+
* and current processing status.
|
|
2794
|
+
*
|
|
2795
|
+
* The unique identifier for matching requests to processed events is:
|
|
2796
|
+
* (receiver, claimableDate.year, claimableDate.month, claimableDate.day)
|
|
2797
|
+
*
|
|
2798
|
+
* This is the primary method for integrators to reliably track withdrawal status
|
|
2799
|
+
* when multiple requests are submitted by the same receiver.
|
|
2800
|
+
*
|
|
2801
|
+
* ## Data sources
|
|
2802
|
+
* - **Withdrawal requests** come from the subgraph (all historical requests).
|
|
2803
|
+
* - **Processed events** come from on-chain `WithdrawalProcessed` logs, which are
|
|
2804
|
+
* limited to a finite lookback window determined by the chain's block parameters.
|
|
2805
|
+
*
|
|
2806
|
+
* ## Lookback window & filtering
|
|
2807
|
+
* On-chain logs only cover a limited number of recent blocks (e.g. ~20 days on Monad,
|
|
2808
|
+
* ~21 days on Ethereum). The `lookbackBlocks` parameter controls how far back to scan
|
|
2809
|
+
* and is applied uniformly to both data sources: it bounds the `WithdrawalProcessed`
|
|
2810
|
+
* log query *and* drops any withdrawal request whose submission timestamp falls outside
|
|
2811
|
+
* the window — regardless of whether a matching processed event was found. Callers who
|
|
2812
|
+
* need deeper history should widen `lookbackBlocks`.
|
|
2813
|
+
*
|
|
2814
|
+
* ## Status determination
|
|
2815
|
+
* - `'processed'` — A matching `WithdrawalProcessed` event was found (by receiver + claimable date).
|
|
2816
|
+
* - `'ready_to_claim'` — Claimable date has passed (within the lookback window) but no processed event found.
|
|
2817
|
+
* - `'pending'` — Claimable date is still in the future.
|
|
2818
|
+
*
|
|
2819
|
+
* @param params - Query parameters:
|
|
2820
|
+
* - `vault` — Vault address to query.
|
|
2821
|
+
* - `receiver` — (Optional) Filter by receiver address.
|
|
2822
|
+
* - `lookbackBlocks` — (Optional) On-chain log lookback window in blocks.
|
|
2823
|
+
* Defaults to a chain-specific value (e.g. 150,000 for Ethereum, 3,456,000 for Monad).
|
|
2824
|
+
* Increasing this scans more history but requires more RPC calls.
|
|
2825
|
+
* - `options` — Standard vault query options (provider, RPC URL, etc.).
|
|
2826
|
+
* @returns Array of withdrawal requests with status and claimable dates
|
|
2827
|
+
*
|
|
2828
|
+
* @example
|
|
2829
|
+
* ```typescript
|
|
2830
|
+
* const requests = await getWithdrawalRequestsWithStatus({
|
|
2831
|
+
* vault: '0xVault...',
|
|
2832
|
+
* receiver: '0xReceiver...'
|
|
2833
|
+
* });
|
|
2834
|
+
*
|
|
2835
|
+
* // Results show exact status + claimable date
|
|
2836
|
+
* requests.forEach(req => {
|
|
2837
|
+
* console.log(
|
|
2838
|
+
* `${req.claimableDate.year}-${req.claimableDate.month}-${req.claimableDate.day}: ${req.status}`
|
|
2839
|
+
* );
|
|
2840
|
+
* });
|
|
2841
|
+
*
|
|
2842
|
+
* // With custom lookback window (e.g. 30 days on Ethereum ≈ 216,000 blocks)
|
|
2843
|
+
* const moreResults = await getWithdrawalRequestsWithStatus({
|
|
2844
|
+
* vault: '0xVault...',
|
|
2845
|
+
* lookbackBlocks: 216_000,
|
|
2846
|
+
* });
|
|
2847
|
+
* ```
|
|
2848
|
+
*/
|
|
2849
|
+
async function getWithdrawalRequestsWithStatus(params) {
|
|
2850
|
+
try {
|
|
2851
|
+
const { vault, receiver, lookbackBlocks, options } = params;
|
|
2852
|
+
// Input validation
|
|
2853
|
+
if (!vault || !(0, ethers_1.isAddress)(vault)) {
|
|
2854
|
+
throw new Error('Invalid vault address');
|
|
2855
|
+
}
|
|
2856
|
+
const vaultAddress = (0, ethers_1.getAddress)(vault);
|
|
2857
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
2858
|
+
// Fetch vault metadata and lag duration
|
|
2859
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vaultAddress, options?.headers, false, false))?.[0];
|
|
2860
|
+
if (!tokenizedVault) {
|
|
2861
|
+
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Vault not found in backend: ${vaultAddress}`);
|
|
2862
|
+
return [];
|
|
2863
|
+
}
|
|
2864
|
+
const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
2865
|
+
const isV2 = version === 'evm-2';
|
|
2866
|
+
const vaultAbi = isV2 ? TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOL_V2;
|
|
2867
|
+
const vaultContract = (0, core_1.createContract)({
|
|
2868
|
+
address: vaultAddress,
|
|
2869
|
+
abi: vaultAbi,
|
|
2870
|
+
provider,
|
|
2871
|
+
});
|
|
2872
|
+
let lagDuration = 86400; // Default: 1 day
|
|
2873
|
+
try {
|
|
2874
|
+
const lagValue = await vaultContract.lagDuration?.();
|
|
2875
|
+
if (lagValue !== undefined && lagValue !== null) {
|
|
2876
|
+
lagDuration = Number(lagValue);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
catch (e) {
|
|
2880
|
+
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Failed to fetch lagDuration from vault ${vaultAddress}, using default 1 day — status determinations may be inaccurate: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
2881
|
+
}
|
|
2882
|
+
// Fetch subgraph requests, on-chain processed events, and the current block
|
|
2883
|
+
// in parallel. Current block is needed to derive the block-based lookback
|
|
2884
|
+
// cutoff applied uniformly to both data sources.
|
|
2885
|
+
const [withdrawalRequests, processedEvents, currentBlock] = await Promise.all([
|
|
2886
|
+
(0, vaults_1.getSubgraphWithdrawRequests)(vaultAddress, provider),
|
|
2887
|
+
getVaultRedemptionHistory({
|
|
2888
|
+
vault: vaultAddress,
|
|
2889
|
+
wallet: receiver,
|
|
2890
|
+
lookbackBlocks,
|
|
2891
|
+
options,
|
|
2892
|
+
}),
|
|
2893
|
+
provider.getBlockNumber(),
|
|
2894
|
+
]);
|
|
2895
|
+
if (!withdrawalRequests.length) {
|
|
2896
|
+
return [];
|
|
2897
|
+
}
|
|
2898
|
+
const chainId = tokenizedVault.chain;
|
|
2899
|
+
const effectiveLookbackBlocks = lookbackBlocks ?? (0, core_1.determineBlockCutoff)(chainId);
|
|
2900
|
+
const blockCutoff = currentBlock - effectiveLookbackBlocks;
|
|
2901
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2902
|
+
// Filter by receiver (if provided) and drop any request whose submission
|
|
2903
|
+
// block is outside the lookback window — uniformly, regardless of whether
|
|
2904
|
+
// a matching processed event exists. Using block_number (not timestamp)
|
|
2905
|
+
// keeps this consistent with the on-chain log scan's block-based cutoff.
|
|
2906
|
+
const filteredRequests = withdrawalRequests.filter((req) => {
|
|
2907
|
+
if (receiver &&
|
|
2908
|
+
req.receiverAddr?.toLowerCase() !== receiver.toLowerCase()) {
|
|
2909
|
+
return false;
|
|
2910
|
+
}
|
|
2911
|
+
return Number(req.block_number) >= blockCutoff;
|
|
2912
|
+
});
|
|
2913
|
+
// Pre-decode all processed transactions in parallel for performance
|
|
2914
|
+
const iface = new ethers_1.Interface(vaultAbi);
|
|
2915
|
+
const decodedProcessed = await Promise.all((processedEvents || []).map(async (processed) => {
|
|
2916
|
+
// Try calldata decoding first (deterministic)
|
|
2917
|
+
if (processed.transactionHash) {
|
|
2918
|
+
try {
|
|
2919
|
+
const decoded = await decodeProcessingTransaction(processed.transactionHash, provider, iface);
|
|
2920
|
+
if (!decoded.error) {
|
|
2921
|
+
return {
|
|
2922
|
+
...processed,
|
|
2923
|
+
decodedDate: {
|
|
2924
|
+
year: decoded.year,
|
|
2925
|
+
month: decoded.month,
|
|
2926
|
+
day: decoded.day,
|
|
2927
|
+
},
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
catch (e) {
|
|
2932
|
+
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Failed to decode processed transaction ${processed.transactionHash}: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
// Fallback: use original request timestamp to compute claimable date,
|
|
2936
|
+
// or if unavailable (evm-2: requested=epoch 0), use the processed block
|
|
2937
|
+
// timestamp as the date directly (processing happens on the claimable date).
|
|
2938
|
+
const requestedEpoch = Math.floor(processed.requested.getTime() / 1000);
|
|
2939
|
+
if (requestedEpoch > 0) {
|
|
2940
|
+
// evm-1: we have the original request timestamp → compute claimable date
|
|
2941
|
+
const fallbackDate = (0, date_utils_1.computeClaimableDate)(requestedEpoch, lagDuration);
|
|
2942
|
+
return {
|
|
2943
|
+
...processed,
|
|
2944
|
+
decodedDate: {
|
|
2945
|
+
year: fallbackDate.year,
|
|
2946
|
+
month: fallbackDate.month,
|
|
2947
|
+
day: fallbackDate.day,
|
|
2948
|
+
},
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
// evm-2: no request timestamp — use processed block date directly
|
|
2952
|
+
// (processing occurs on the claimable date, so the block date IS the date)
|
|
2953
|
+
const processedDate = processed.processed;
|
|
2954
|
+
return {
|
|
2955
|
+
...processed,
|
|
2956
|
+
decodedDate: {
|
|
2957
|
+
year: processedDate.getUTCFullYear(),
|
|
2958
|
+
month: processedDate.getUTCMonth() + 1,
|
|
2959
|
+
day: processedDate.getUTCDate(),
|
|
2960
|
+
},
|
|
2961
|
+
};
|
|
2962
|
+
}));
|
|
2963
|
+
// Build deterministic lookup map: (receiver + year + month + day) → processed events
|
|
2964
|
+
// A single WithdrawalProcessed event covers the entire day's batch for a
|
|
2965
|
+
// receiver — all requests whose claimable date falls on that day should
|
|
2966
|
+
// match it. We keep an array per key only to preserve every event we saw
|
|
2967
|
+
// for visibility; matching is a non-destructive peek at the first entry.
|
|
2968
|
+
const processedMap = new Map();
|
|
2969
|
+
for (const processed of decodedProcessed) {
|
|
2970
|
+
if (!processed.decodedDate) {
|
|
2971
|
+
continue;
|
|
2972
|
+
}
|
|
2973
|
+
const dateKey = (0, date_utils_1.formatDateKey)(processed.decodedDate.year, processed.decodedDate.month, processed.decodedDate.day);
|
|
2974
|
+
const key = `${processed.receiver.toLowerCase()}:${dateKey}`;
|
|
2975
|
+
const existing = processedMap.get(key) ?? [];
|
|
2976
|
+
processedMap.set(key, [...existing, processed]);
|
|
2977
|
+
}
|
|
2978
|
+
// Compute status for each withdrawal request. The lookback window was
|
|
2979
|
+
// already applied up-front when pruning filteredRequests, so every request
|
|
2980
|
+
// reaching this loop is within the window by construction.
|
|
2981
|
+
const results = [];
|
|
2982
|
+
for (const req of filteredRequests) {
|
|
2983
|
+
// timestamp_ is a Unix epoch string (seconds)
|
|
2984
|
+
const requestTimestamp = Number(req.timestamp_);
|
|
2985
|
+
// Compute claimable date from timestamp + lagDuration (UTC)
|
|
2986
|
+
const claimableDate = (0, date_utils_1.computeClaimableDate)(requestTimestamp, lagDuration);
|
|
2987
|
+
// Determine status: check processed first, then claimability
|
|
2988
|
+
let status = 'pending';
|
|
2989
|
+
let processedTxHash;
|
|
2990
|
+
let assetsReceived;
|
|
2991
|
+
// Match deterministically by (receiver, year, month, day)
|
|
2992
|
+
const dateKey = (0, date_utils_1.formatDateKey)(claimableDate.year, claimableDate.month, claimableDate.day);
|
|
2993
|
+
const lookupKey = `${req.receiverAddr.toLowerCase()}:${dateKey}`;
|
|
2994
|
+
// Peek — do not consume. The processing event covers the whole day for
|
|
2995
|
+
// this receiver, so every request on that day matches the same event.
|
|
2996
|
+
const processedEvent = processedMap.get(lookupKey)?.[0];
|
|
2997
|
+
if (processedEvent) {
|
|
2998
|
+
status = 'processed';
|
|
2999
|
+
processedTxHash = processedEvent.transactionHash;
|
|
3000
|
+
try {
|
|
3001
|
+
assetsReceived = BigInt(processedEvent.amount.raw);
|
|
3002
|
+
}
|
|
3003
|
+
catch (e) {
|
|
3004
|
+
core_1.Logger.log.warn(`Failed to parse assets as BigInt: ${e}`);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
else if ((0, date_utils_1.isClaimableNow)(claimableDate.epoch, now)) {
|
|
3008
|
+
// Claimable date is recent (within lookback) and not yet processed
|
|
3009
|
+
status = 'ready_to_claim';
|
|
3010
|
+
}
|
|
3011
|
+
results.push({
|
|
3012
|
+
transactionHash: req.transactionHash_,
|
|
3013
|
+
requestTimestamp,
|
|
3014
|
+
shares: (() => {
|
|
3015
|
+
try {
|
|
3016
|
+
return BigInt(req.shares);
|
|
3017
|
+
}
|
|
3018
|
+
catch {
|
|
3019
|
+
// Handle decimal strings from subgraph (e.g. "1234.0")
|
|
3020
|
+
const truncated = Math.trunc(Number(req.shares));
|
|
3021
|
+
if (!Number.isFinite(truncated)) {
|
|
3022
|
+
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Cannot parse shares value "${req.shares}" — defaulting to 0`);
|
|
3023
|
+
return BigInt(0);
|
|
3024
|
+
}
|
|
3025
|
+
core_1.Logger.log.info('getWithdrawalRequestsWithStatus', `Truncated decimal shares "${req.shares}" → ${truncated}`);
|
|
3026
|
+
return BigInt(truncated);
|
|
3027
|
+
}
|
|
3028
|
+
})(),
|
|
3029
|
+
receiver: req.receiverAddr,
|
|
3030
|
+
claimableDate: {
|
|
3031
|
+
year: claimableDate.year,
|
|
3032
|
+
month: claimableDate.month,
|
|
3033
|
+
day: claimableDate.day,
|
|
3034
|
+
},
|
|
3035
|
+
claimableEpoch: claimableDate.epoch,
|
|
3036
|
+
status,
|
|
3037
|
+
processedTransactionHash: processedTxHash,
|
|
3038
|
+
assetsReceived,
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
return results;
|
|
3042
|
+
}
|
|
3043
|
+
catch (e) {
|
|
3044
|
+
core_1.Logger.log.error('getWithdrawalRequestsWithStatus', e, {
|
|
3045
|
+
vaultAddress: params.vault,
|
|
3046
|
+
hasReceiver: !!params.receiver,
|
|
3047
|
+
});
|
|
3048
|
+
throw new Error(`Failed to get withdrawal requests with status: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
//# sourceMappingURL=getters.js.map
|