@upshiftfinance/sdk 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +408 -2
- package/lib/abis/AddressResolver.d.ts +28 -0
- package/lib/abis/AddressResolver.js +23 -0
- package/lib/abis/ChainlinkV3.d.ts +87 -0
- package/lib/abis/ChainlinkV3.js +117 -0
- package/lib/abis/ERC20.d.ts +168 -0
- package/lib/abis/ERC20.js +226 -0
- package/lib/abis/ERC20_Bytes32.d.ts +139 -0
- package/lib/abis/ERC20_Bytes32.js +196 -0
- package/lib/abis/ERC4626.d.ts +364 -0
- package/lib/abis/ERC4626.js +507 -0
- package/lib/abis/ERC721.d.ts +231 -0
- package/lib/abis/ERC721.js +321 -0
- package/lib/abis/FeeOracle.d.ts +120 -0
- package/lib/abis/FeeOracle.js +162 -0
- package/lib/abis/LendingPool.d.ts +1393 -0
- package/lib/abis/LendingPool.js +1807 -0
- package/lib/abis/LendingPoolV2.d.ts +1413 -0
- package/lib/abis/LendingPoolV2.js +1833 -0
- package/lib/abis/LendingPoolV3.d.ts +1677 -0
- package/lib/abis/LendingPoolV3.js +1160 -0
- package/lib/abis/Loan.d.ts +837 -0
- package/lib/abis/Loan.js +1080 -0
- package/lib/abis/MultiAssetNativeDepositWrapper.d.ts +137 -0
- package/lib/abis/MultiAssetNativeDepositWrapper.js +125 -0
- package/lib/abis/Multicall3.d.ts +30 -0
- package/lib/abis/Multicall3.js +97 -0
- package/lib/abis/OFT.d.ts +116 -0
- package/lib/abis/OFT.js +85 -0
- package/lib/abis/PoolAdapter.d.ts +36 -0
- package/lib/abis/PoolAdapter.js +51 -0
- package/lib/abis/RewardDistributor.d.ts +267 -0
- package/lib/abis/RewardDistributor.js +352 -0
- package/lib/abis/RwaRedeemSubaccount.d.ts +747 -0
- package/lib/abis/RwaRedeemSubaccount.js +548 -0
- package/lib/abis/SmartAccount.d.ts +17 -0
- package/lib/abis/SmartAccount.js +19 -0
- package/lib/abis/SwapRouter.d.ts +953 -0
- package/lib/abis/SwapRouter.js +1228 -0
- package/lib/abis/TextResolver.d.ts +16 -0
- package/lib/abis/TextResolver.js +16 -0
- package/lib/abis/TokenizedVaultV2.d.ts +1364 -0
- package/lib/abis/TokenizedVaultV2.js +1041 -0
- package/lib/abis/TokenizedVaultV2DepositWithPermit.d.ts +1456 -0
- package/lib/abis/TokenizedVaultV2DepositWithPermit.js +1878 -0
- package/lib/abis/TokenizedVaultV2Receipt.d.ts +1568 -0
- package/lib/abis/TokenizedVaultV2Receipt.js +1061 -0
- package/lib/abis/TokenizedVaultV2SenderAllocationWhitelist.d.ts +454 -0
- package/lib/abis/TokenizedVaultV2SenderAllocationWhitelist.js +327 -0
- package/lib/abis/TokenizedVaultV2WhitelistedAllocation.d.ts +1466 -0
- package/lib/abis/TokenizedVaultV2WhitelistedAllocation.js +1092 -0
- package/lib/abis/TokenizedVaultV2WhitelistedAssets.d.ts +274 -0
- package/lib/abis/TokenizedVaultV2WhitelistedAssets.js +167 -0
- package/lib/abis/UniversalResolverResolve.d.ts +69 -0
- package/lib/abis/UniversalResolverResolve.js +35 -0
- package/lib/abis/UniversalSignatureValidator.d.ts +17 -0
- package/lib/abis/UniversalSignatureValidator.js +30 -0
- package/lib/abis/WrapperAdapter.d.ts +71 -0
- package/lib/abis/WrapperAdapter.js +77 -0
- package/lib/abis/index.d.ts +34 -0
- package/lib/abis/index.js +51 -0
- package/lib/adapters/evm/getters.d.ts +19 -0
- package/lib/adapters/evm/getters.js +249 -0
- package/lib/adapters/evm/index.d.ts +539 -0
- package/lib/adapters/evm/index.js +656 -0
- package/lib/adapters/evm/utils.d.ts +8 -0
- package/lib/adapters/evm/utils.js +51 -0
- package/lib/adapters/solana/confirm.d.ts +88 -0
- package/lib/adapters/solana/confirm.js +291 -0
- package/lib/adapters/solana/constants.d.ts +42 -0
- package/lib/adapters/solana/constants.js +59 -0
- package/lib/adapters/solana/getters.d.ts +42 -0
- package/lib/adapters/solana/getters.js +275 -0
- package/lib/adapters/solana/idl/vault-idl.d.ts +700 -0
- package/lib/adapters/solana/idl/vault-idl.js +2101 -0
- package/lib/adapters/solana/idl/withdrawal-queue-idl.d.ts +795 -0
- package/lib/adapters/solana/idl/withdrawal-queue-idl.js +1864 -0
- package/lib/adapters/solana/index.d.ts +372 -0
- package/lib/adapters/solana/index.js +459 -0
- package/lib/adapters/solana/pricing.d.ts +184 -0
- package/lib/adapters/solana/pricing.js +239 -0
- package/lib/adapters/solana/queue-hints.d.ts +29 -0
- package/lib/adapters/solana/queue-hints.js +59 -0
- package/lib/adapters/solana/queue.actions.d.ts +97 -0
- package/lib/adapters/solana/queue.actions.js +468 -0
- package/lib/adapters/solana/token-reads.d.ts +108 -0
- package/lib/adapters/solana/token-reads.js +145 -0
- package/lib/adapters/solana/types.d.ts +325 -0
- package/lib/adapters/solana/types.js +3 -0
- package/lib/adapters/solana/utils.d.ts +229 -0
- package/lib/adapters/solana/utils.js +773 -0
- package/lib/adapters/solana/vault.actions.d.ts +140 -0
- package/lib/adapters/solana/vault.actions.js +657 -0
- package/lib/adapters/solana/withdrawal-queue.d.ts +125 -0
- package/lib/adapters/solana/withdrawal-queue.js +301 -0
- package/lib/adapters/stellar/actions.d.ts +41 -0
- package/lib/adapters/stellar/actions.js +126 -0
- package/lib/adapters/stellar/constants.d.ts +43 -0
- package/lib/adapters/stellar/constants.js +53 -0
- package/lib/adapters/stellar/getters.d.ts +68 -0
- package/lib/adapters/stellar/getters.js +290 -0
- package/lib/adapters/stellar/index.d.ts +126 -0
- package/lib/adapters/stellar/index.js +187 -0
- package/lib/adapters/stellar/invocation.d.ts +55 -0
- package/lib/adapters/stellar/invocation.js +92 -0
- package/lib/adapters/stellar/soroban.d.ts +131 -0
- package/lib/adapters/stellar/soroban.js +617 -0
- package/lib/adapters/stellar/submit.d.ts +42 -0
- package/lib/adapters/stellar/submit.js +195 -0
- package/lib/adapters/stellar/types.d.ts +57 -0
- package/lib/adapters/stellar/types.js +6 -0
- package/lib/adapters/stellar/utils.d.ts +24 -0
- package/lib/adapters/stellar/utils.js +34 -0
- package/lib/adapters/sui/constants.d.ts +1 -0
- package/lib/adapters/sui/constants.js +9 -0
- package/lib/adapters/sui/getters.d.ts +24 -0
- package/lib/adapters/sui/getters.js +134 -0
- package/lib/adapters/sui/index.d.ts +53 -0
- package/lib/adapters/sui/index.js +96 -0
- package/lib/adapters/sui/transformer.d.ts +10 -0
- package/lib/adapters/sui/transformer.js +115 -0
- package/lib/adapters/sui/types.d.ts +70 -0
- package/lib/adapters/sui/types.js +3 -0
- package/lib/adapters/sui/utils.d.ts +8 -0
- package/lib/adapters/sui/utils.js +17 -0
- package/lib/core/analytics/app-name.d.ts +27 -0
- package/lib/core/analytics/app-name.js +48 -0
- package/lib/core/analytics/chain-name.d.ts +9 -0
- package/lib/core/analytics/chain-name.js +34 -0
- package/lib/core/analytics/constants.d.ts +15 -0
- package/lib/core/analytics/constants.js +28 -0
- package/lib/core/analytics/env.d.ts +46 -0
- package/lib/core/analytics/env.js +147 -0
- package/lib/core/analytics/index.d.ts +39 -0
- package/lib/core/analytics/index.js +83 -0
- package/lib/core/analytics/instrumentation.d.ts +29 -0
- package/lib/core/analytics/instrumentation.js +292 -0
- package/lib/core/analytics/method-taxonomy.d.ts +19 -0
- package/lib/core/analytics/method-taxonomy.js +156 -0
- package/lib/core/analytics/metrics.d.ts +48 -0
- package/lib/core/analytics/metrics.js +142 -0
- package/lib/core/analytics/sanitize.d.ts +57 -0
- package/lib/core/analytics/sanitize.js +272 -0
- package/lib/core/analytics/sentry-runtime.d.ts +15 -0
- package/lib/core/analytics/sentry-runtime.js +131 -0
- package/lib/core/analytics/sentry.d.ts +100 -0
- package/lib/core/analytics/sentry.js +696 -0
- package/lib/core/analytics/types.d.ts +55 -0
- package/lib/core/analytics/types.js +3 -0
- package/lib/core/analytics/user-identity.d.ts +55 -0
- package/lib/core/analytics/user-identity.js +218 -0
- package/lib/core/analytics/version.d.ts +6 -0
- package/lib/core/analytics/version.js +10 -0
- package/lib/core/attribution.d.ts +159 -0
- package/lib/core/attribution.js +194 -0
- package/lib/core/auth/index.d.ts +1 -0
- package/lib/core/auth/index.js +18 -0
- package/lib/core/auth/verify.d.ts +2 -0
- package/lib/core/auth/verify.js +35 -0
- package/lib/core/base.class.d.ts +193 -0
- package/lib/core/base.class.js +163 -0
- package/lib/core/cache.d.ts +9 -0
- package/lib/core/cache.js +31 -0
- package/lib/core/constants/adapters.d.ts +103 -0
- package/lib/core/constants/adapters.js +180 -0
- package/lib/core/constants/core.d.ts +162 -0
- package/lib/core/constants/core.js +281 -0
- package/lib/core/constants/swap-router.d.ts +180 -0
- package/lib/core/constants/swap-router.js +203 -0
- package/lib/core/constants/vaults.d.ts +93 -0
- package/lib/core/constants/vaults.js +277 -0
- package/lib/core/constants/web3.d.ts +106 -0
- package/lib/core/constants/web3.js +251 -0
- package/lib/core/errors/index.d.ts +177 -0
- package/lib/core/errors/index.js +257 -0
- package/lib/core/fetcher.d.ts +237 -0
- package/lib/core/fetcher.js +1103 -0
- package/lib/core/helpers/adapters.d.ts +13 -0
- package/lib/core/helpers/adapters.js +39 -0
- package/lib/core/helpers/chain-address.d.ts +13 -0
- package/lib/core/helpers/chain-address.js +47 -0
- package/lib/core/helpers/chain-error.d.ts +207 -0
- package/lib/core/helpers/chain-error.js +697 -0
- package/lib/core/helpers/chain-support.d.ts +98 -0
- package/lib/core/helpers/chain-support.js +139 -0
- package/lib/core/helpers/concurrency.d.ts +51 -0
- package/lib/core/helpers/concurrency.js +79 -0
- package/lib/core/helpers/core.d.ts +92 -0
- package/lib/core/helpers/core.js +368 -0
- package/lib/core/helpers/explorer-link.d.ts +16 -0
- package/lib/core/helpers/explorer-link.js +26 -0
- package/lib/core/helpers/multicall.d.ts +68 -0
- package/lib/core/helpers/multicall.js +103 -0
- package/lib/core/helpers/revert-decode.d.ts +248 -0
- package/lib/core/helpers/revert-decode.js +515 -0
- package/lib/core/helpers/signer.d.ts +61 -0
- package/lib/core/helpers/signer.js +184 -0
- package/lib/core/helpers/swap-router.d.ts +269 -0
- package/lib/core/helpers/swap-router.js +542 -0
- package/lib/core/helpers/vault-version.d.ts +24 -0
- package/lib/core/helpers/vault-version.js +83 -0
- package/lib/core/helpers/vaults.d.ts +89 -0
- package/lib/core/helpers/vaults.js +241 -0
- package/lib/core/helpers/web3.d.ts +366 -0
- package/lib/core/helpers/web3.js +1034 -0
- package/lib/core/index.d.ts +23 -0
- package/lib/core/index.js +40 -0
- package/lib/core/logger/curator-alert.d.ts +217 -0
- package/lib/core/logger/curator-alert.js +276 -0
- package/lib/core/logger/index.d.ts +99 -0
- package/lib/core/logger/index.js +163 -0
- package/lib/core/logger/slack.d.ts +49 -0
- package/lib/core/logger/slack.js +111 -0
- package/lib/core/vault-metadata.d.ts +12 -0
- package/lib/core/vault-metadata.js +42 -0
- package/lib/core/version-check.d.ts +58 -0
- package/lib/core/version-check.js +182 -0
- package/lib/evm/index.d.ts +2 -0
- package/lib/evm/index.js +19 -0
- package/lib/evm/methods/crossChainVault.d.ts +132 -0
- package/lib/evm/methods/crossChainVault.js +857 -0
- package/lib/evm/methods/crossChainVaultRegistry.d.ts +92 -0
- package/lib/evm/methods/crossChainVaultRegistry.js +239 -0
- package/lib/evm/methods/index.d.ts +2 -0
- package/lib/evm/methods/index.js +19 -0
- package/lib/evm/types/crossChain.d.ts +363 -0
- package/lib/evm/types/crossChain.js +20 -0
- package/lib/evm/types/index.d.ts +1 -0
- package/lib/evm/types/index.js +18 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +52 -0
- package/lib/main.d.ts +596 -0
- package/lib/main.js +681 -0
- package/lib/modules/api/fetcher.d.ts +108 -0
- package/lib/modules/api/fetcher.js +187 -0
- package/lib/modules/api/index.d.ts +1 -0
- package/lib/modules/api/index.js +6 -0
- package/lib/modules/api/main.d.ts +587 -0
- package/lib/modules/api/main.js +911 -0
- package/lib/modules/sub-accounts/fetcher.d.ts +53 -0
- package/lib/modules/sub-accounts/fetcher.js +96 -0
- package/lib/modules/sub-accounts/index.d.ts +2 -0
- package/lib/modules/sub-accounts/index.js +19 -0
- package/lib/modules/sub-accounts/main.d.ts +249 -0
- package/lib/modules/sub-accounts/main.js +211 -0
- package/lib/modules/sub-accounts/utils.d.ts +106 -0
- package/lib/modules/sub-accounts/utils.js +112 -0
- package/lib/modules/vaults/adapter.helpers.d.ts +84 -0
- package/lib/modules/vaults/adapter.helpers.js +202 -0
- package/lib/modules/vaults/fetcher.d.ts +147 -0
- package/lib/modules/vaults/fetcher.js +357 -0
- package/lib/modules/vaults/getters.d.ts +1060 -0
- package/lib/modules/vaults/getters.js +4079 -0
- package/lib/modules/vaults/index.d.ts +20 -0
- package/lib/modules/vaults/index.js +44 -0
- package/lib/modules/vaults/main.d.ts +607 -0
- package/lib/modules/vaults/main.js +1687 -0
- package/lib/modules/vaults/prefetch.d.ts +65 -0
- package/lib/modules/vaults/prefetch.js +120 -0
- package/lib/modules/vaults/read.actions.d.ts +406 -0
- package/lib/modules/vaults/read.actions.js +891 -0
- package/lib/modules/vaults/types.d.ts +149 -0
- package/lib/modules/vaults/types.js +3 -0
- package/lib/modules/vaults/utils/call-data-decoder.d.ts +61 -0
- package/lib/modules/vaults/utils/call-data-decoder.js +194 -0
- package/lib/modules/vaults/utils/date-utils.d.ts +50 -0
- package/lib/modules/vaults/utils/date-utils.js +84 -0
- package/lib/modules/vaults/utils.d.ts +153 -0
- package/lib/modules/vaults/utils.js +818 -0
- package/lib/modules/vaults/write.actions.d.ts +812 -0
- package/lib/modules/vaults/write.actions.js +2377 -0
- package/lib/polyfills.d.ts +1 -0
- package/lib/polyfills.js +12 -0
- package/lib/sdk.d.ts +30154 -0
- package/lib/services/coingecko/fetcher.d.ts +15 -0
- package/lib/services/coingecko/fetcher.js +67 -0
- package/lib/services/coingecko/index.d.ts +2 -0
- package/lib/services/coingecko/index.js +19 -0
- package/lib/services/coingecko/utils.d.ts +1 -0
- package/lib/services/coingecko/utils.js +24 -0
- package/lib/services/debank/fetcher.d.ts +126 -0
- package/lib/services/debank/fetcher.js +47 -0
- package/lib/services/debank/index.d.ts +2 -0
- package/lib/services/debank/index.js +19 -0
- package/lib/services/debank/utils.d.ts +50 -0
- package/lib/services/debank/utils.js +325 -0
- package/lib/services/layerzero/deposits.d.ts +55 -0
- package/lib/services/layerzero/deposits.js +174 -0
- package/lib/services/layerzero/redeems.d.ts +20 -0
- package/lib/services/layerzero/redeems.js +92 -0
- package/lib/services/layerzero/utils.d.ts +9 -0
- package/lib/services/layerzero/utils.js +22 -0
- package/lib/services/octavfi/fetcher.d.ts +33 -0
- package/lib/services/octavfi/fetcher.js +39 -0
- package/lib/services/octavfi/index.d.ts +3 -0
- package/lib/services/octavfi/index.js +20 -0
- package/lib/services/octavfi/types.d.ts +34 -0
- package/lib/services/octavfi/types.js +3 -0
- package/lib/services/octavfi/utils.d.ts +16 -0
- package/lib/services/octavfi/utils.js +244 -0
- package/lib/services/subgraph/fetcher.d.ts +2 -0
- package/lib/services/subgraph/fetcher.js +66 -0
- package/lib/services/subgraph/index.d.ts +2 -0
- package/lib/services/subgraph/index.js +19 -0
- package/lib/services/subgraph/schema.d.ts +48 -0
- package/lib/services/subgraph/schema.js +87 -0
- package/lib/services/subgraph/vaults.d.ts +83 -0
- package/lib/services/subgraph/vaults.js +1282 -0
- package/lib/services/swap-quotes/index.d.ts +105 -0
- package/lib/services/swap-quotes/index.js +77 -0
- package/lib/services/swap-quotes/paraswap.d.ts +51 -0
- package/lib/services/swap-quotes/paraswap.js +138 -0
- package/lib/services/untangled/fetcher.d.ts +40 -0
- package/lib/services/untangled/fetcher.js +110 -0
- package/lib/services/untangled/index.d.ts +3 -0
- package/lib/services/untangled/index.js +20 -0
- package/lib/services/untangled/types.d.ts +83 -0
- package/lib/services/untangled/types.js +10 -0
- package/lib/services/untangled/utils.d.ts +91 -0
- package/lib/services/untangled/utils.js +339 -0
- package/lib/types/api.d.ts +498 -0
- package/lib/types/api.js +3 -0
- package/lib/types/index.d.ts +12 -0
- package/lib/types/index.js +28 -0
- package/lib/types/points.d.ts +28 -0
- package/lib/types/points.js +3 -0
- package/lib/types/pools.d.ts +144 -0
- package/lib/types/pools.js +3 -0
- package/lib/types/staking.d.ts +28 -0
- package/lib/types/staking.js +3 -0
- package/lib/types/sub-accounts.d.ts +150 -0
- package/lib/types/sub-accounts.js +3 -0
- package/lib/types/subgraph.d.ts +84 -0
- package/lib/types/subgraph.js +3 -0
- package/lib/types/typed-contract.d.ts +102 -0
- package/lib/types/typed-contract.js +3 -0
- package/lib/types/user.d.ts +1 -0
- package/lib/types/user.js +3 -0
- package/lib/types/vaults.d.ts +886 -0
- package/lib/types/vaults.js +30 -0
- package/lib/types/web3.d.ts +81 -0
- package/lib/types/web3.js +14 -0
- package/lib/types/webserver.d.ts +730 -0
- package/lib/types/webserver.js +3 -0
- package/package.json +78 -3
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setSafeCache = exports.getSafeCache = exports.CACHE = void 0;
|
|
4
|
+
exports.setSdkRequestTimeout = setSdkRequestTimeout;
|
|
5
|
+
exports.getSdkRequestTimeout = getSdkRequestTimeout;
|
|
6
|
+
exports.setSdkAppName = setSdkAppName;
|
|
7
|
+
exports.setPublicApiBaseUrl = setPublicApiBaseUrl;
|
|
8
|
+
exports.getPublicApiBaseUrl = getPublicApiBaseUrl;
|
|
9
|
+
exports.fetchUpshiftWithKey = fetchUpshiftWithKey;
|
|
10
|
+
exports.fetchUpshiftPublic = fetchUpshiftPublic;
|
|
11
|
+
exports.fetchUpshiftWithBearer = fetchUpshiftWithBearer;
|
|
12
|
+
exports.isRetryableError = isRetryableError;
|
|
13
|
+
exports.withRetry = withRetry;
|
|
14
|
+
exports.fetchTokenizedVault = fetchTokenizedVault;
|
|
15
|
+
exports.fetchTokenizedVaults = fetchTokenizedVaults;
|
|
16
|
+
exports.fetchTokenPrice = fetchTokenPrice;
|
|
17
|
+
exports.fetchTokenPriceByAddress = fetchTokenPriceByAddress;
|
|
18
|
+
exports.fetchTokenizedVaultLoans = fetchTokenizedVaultLoans;
|
|
19
|
+
exports.fetchTokenizedVaultSubaccountLoans = fetchTokenizedVaultSubaccountLoans;
|
|
20
|
+
exports.fetchVaultHistoricalTimeseries = fetchVaultHistoricalTimeseries;
|
|
21
|
+
exports.promiseSettle = promiseSettle;
|
|
22
|
+
const core_1 = require("./helpers/core");
|
|
23
|
+
const ethers_1 = require("ethers");
|
|
24
|
+
const abis_1 = require("../abis");
|
|
25
|
+
const lru_cache_1 = require("lru-cache");
|
|
26
|
+
const chain_address_1 = require("./helpers/chain-address");
|
|
27
|
+
const chain_error_1 = require("./helpers/chain-error");
|
|
28
|
+
const fetcher_1 = require("../services/coingecko/fetcher");
|
|
29
|
+
const logger_1 = require("./logger");
|
|
30
|
+
const ethers_2 = require("ethers");
|
|
31
|
+
const web3_1 = require("./constants/web3");
|
|
32
|
+
const core_2 = require("./constants/core");
|
|
33
|
+
const vault_version_1 = require("./helpers/vault-version");
|
|
34
|
+
const analytics_1 = require("./analytics");
|
|
35
|
+
const sanitize_1 = require("./analytics/sanitize");
|
|
36
|
+
const sentry_1 = require("./analytics/sentry");
|
|
37
|
+
const errors_1 = require("./errors");
|
|
38
|
+
/** Map an HTTP status to the appropriate typed SDK error subclass. */
|
|
39
|
+
function errorFromResponseStatus(status, message, options) {
|
|
40
|
+
if (status === 401) {
|
|
41
|
+
return new errors_1.UpshiftAuthError('AUTH_UNAUTHORIZED', message, options);
|
|
42
|
+
}
|
|
43
|
+
if (status === 403) {
|
|
44
|
+
return new errors_1.UpshiftAuthError('AUTH_FORBIDDEN', message, options);
|
|
45
|
+
}
|
|
46
|
+
if (status === 429) {
|
|
47
|
+
return new errors_1.UpshiftRateLimitError(message, options);
|
|
48
|
+
}
|
|
49
|
+
return new errors_1.UpshiftServerError(status, message, options);
|
|
50
|
+
}
|
|
51
|
+
let SDK_REQUEST_TIMEOUT_OVERRIDE = null;
|
|
52
|
+
/**
|
|
53
|
+
* Override the default request timeout used by every Upshift fetcher helper
|
|
54
|
+
* when the caller doesn't supply a per-call `timeoutMs`. Pass `null` (or
|
|
55
|
+
* call with no arguments) to clear the override and fall back to the
|
|
56
|
+
* compiled-in `REQUEST_TIMEOUT_MS`.
|
|
57
|
+
*
|
|
58
|
+
* @param ms - Positive number of milliseconds to wait before aborting, or
|
|
59
|
+
* `null` to clear.
|
|
60
|
+
*/
|
|
61
|
+
function setSdkRequestTimeout(ms = null) {
|
|
62
|
+
if (ms === null) {
|
|
63
|
+
SDK_REQUEST_TIMEOUT_OVERRIDE = null;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
67
|
+
logger_1.Logger.log.warn('setSdkRequestTimeout', 'Ignoring non-positive timeout', {
|
|
68
|
+
ms,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
SDK_REQUEST_TIMEOUT_OVERRIDE = ms;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read the active default request timeout — the SDK-level override if set,
|
|
76
|
+
* otherwise the compiled-in `REQUEST_TIMEOUT_MS`.
|
|
77
|
+
*/
|
|
78
|
+
function getSdkRequestTimeout() {
|
|
79
|
+
return SDK_REQUEST_TIMEOUT_OVERRIDE ?? core_2.REQUEST_TIMEOUT_MS;
|
|
80
|
+
}
|
|
81
|
+
let SDK_APP_NAME = null;
|
|
82
|
+
/**
|
|
83
|
+
* Record the consuming application's `appName` so every request to the
|
|
84
|
+
* Upshift backend carries it as `x-august-app`, next to
|
|
85
|
+
* `x-august-sdk-version`. This gives the backend per-integrator request
|
|
86
|
+
* counts that do not depend on Sentry sampling.
|
|
87
|
+
*
|
|
88
|
+
* Process-global, last writer wins — the same contract as
|
|
89
|
+
* `setSdkRequestTimeout` and `setPublicApiBaseUrl`. The `UpshiftSDK`
|
|
90
|
+
* constructor calls this with its validated `appName`.
|
|
91
|
+
*
|
|
92
|
+
* @param appName - Validated app-name slug, or `null` to stop sending it.
|
|
93
|
+
*/
|
|
94
|
+
function setSdkAppName(appName = null) {
|
|
95
|
+
SDK_APP_NAME = appName;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Client-identification headers merged into every backend fetcher's
|
|
99
|
+
* defaults. Both values are low-cardinality slugs, never credentials.
|
|
100
|
+
*/
|
|
101
|
+
function sdkClientHeaders() {
|
|
102
|
+
const headers = {
|
|
103
|
+
'x-august-sdk-version': (0, sentry_1.getSDKVersion)(),
|
|
104
|
+
};
|
|
105
|
+
if (SDK_APP_NAME)
|
|
106
|
+
headers['x-august-app'] = SDK_APP_NAME;
|
|
107
|
+
return headers;
|
|
108
|
+
}
|
|
109
|
+
let PUBLIC_API_BASE_OVERRIDE = null;
|
|
110
|
+
/**
|
|
111
|
+
* Override the base URL used for `WEBSERVER_URL.public` requests — the
|
|
112
|
+
* unauthenticated vault-catalog API that `getVault` / `fetchTokenizedVault`
|
|
113
|
+
* (and the other `fetchUpshiftPublic` callers) read from. Defaults to the
|
|
114
|
+
* compiled-in prod base (`https://api.augustdigital.io/api/v1`); pass `null`
|
|
115
|
+
* (or call with no arguments) to clear.
|
|
116
|
+
*
|
|
117
|
+
* Intended for non-prod deployments that run against an ISOLATED backend
|
|
118
|
+
* (e.g. a staging API): set it once at construction via
|
|
119
|
+
* `IUpshiftBase.publicApiBaseUrl` so a staging-only vault resolves. Production
|
|
120
|
+
* leaves it unset, so behaviour is unchanged.
|
|
121
|
+
*
|
|
122
|
+
* This is a process-global override on the fetcher helpers. The SDK constructor
|
|
123
|
+
* calls it on EVERY instantiation (with `null` when `publicApiBaseUrl` is
|
|
124
|
+
* omitted), so the last `UpshiftSDK` instantiated is authoritative — a prod
|
|
125
|
+
* instance resets it to the default even after a prior staging instance set it.
|
|
126
|
+
* An invalid or non-http(s) URL is treated as "no override": it RESETS to the
|
|
127
|
+
* compiled-in default (warned) rather than retaining a prior value, so a bad env
|
|
128
|
+
* value can neither break fetches nor silently leave a previous instance's base
|
|
129
|
+
* in place.
|
|
130
|
+
*
|
|
131
|
+
* Whenever the effective base CHANGES, the shared public-response cache is
|
|
132
|
+
* cleared, so catalog data fetched from the previous backend is never served for
|
|
133
|
+
* the new one (e.g. a prod-cached vault leaking into a staging instance). Base
|
|
134
|
+
* changes happen at construction, when the cache is cold, so this is ~free.
|
|
135
|
+
*
|
|
136
|
+
* @param url - Absolute http(s) base URL (trailing slash is trimmed). `null`, or
|
|
137
|
+
* an invalid / non-http(s) value, resets to the compiled-in public base.
|
|
138
|
+
*/
|
|
139
|
+
function setPublicApiBaseUrl(url = null) {
|
|
140
|
+
const previous = PUBLIC_API_BASE_OVERRIDE;
|
|
141
|
+
if (url === null) {
|
|
142
|
+
PUBLIC_API_BASE_OVERRIDE = null;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
let parsed = null;
|
|
146
|
+
try {
|
|
147
|
+
parsed = new URL(url);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
parsed = null;
|
|
151
|
+
}
|
|
152
|
+
if (!parsed ||
|
|
153
|
+
(parsed.protocol !== 'https:' && parsed.protocol !== 'http:')) {
|
|
154
|
+
// Reset to the default rather than retaining a stale prior override, so
|
|
155
|
+
// each construction stays authoritative and a bad env value fails safe to
|
|
156
|
+
// prod instead of inheriting another instance's base.
|
|
157
|
+
logger_1.Logger.log.warn('setPublicApiBaseUrl', 'Ignoring invalid/non-http(s) URL; resetting to the default public base', { url });
|
|
158
|
+
PUBLIC_API_BASE_OVERRIDE = null;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Store WITHOUT a trailing slash: buildUpshiftUrl concatenates
|
|
162
|
+
// `${base}${relativeUrl}` (relativeUrl starts with '/'), so a trailing
|
|
163
|
+
// slash on the base would produce a double slash.
|
|
164
|
+
PUBLIC_API_BASE_OVERRIDE = url.replace(/\/+$/, '');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (PUBLIC_API_BASE_OVERRIDE !== previous) {
|
|
168
|
+
// The public base moved (or reset). Drop cached public-catalog responses
|
|
169
|
+
// keyed against the previous base (see fetchTokenizedVault(s) etc.) so they
|
|
170
|
+
// aren't served for the new backend.
|
|
171
|
+
cache_1.CACHE.clear();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Read the active base for the `public` server — the SDK-level override if set,
|
|
176
|
+
* otherwise the compiled-in `WEBSERVER_URL.public`.
|
|
177
|
+
*/
|
|
178
|
+
function getPublicApiBaseUrl() {
|
|
179
|
+
return PUBLIC_API_BASE_OVERRIDE ?? core_2.WEBSERVER_URL.public;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Combine the per-request timeout signal with any caller-supplied signal.
|
|
183
|
+
* Uses `AbortSignal.any` (Node 22+) with a manual relay fallback.
|
|
184
|
+
*
|
|
185
|
+
* Returns `timedOut()` (a closure over an internal flag set when the timeout
|
|
186
|
+
* fires) so the caller can distinguish "we timed out" from "caller cancelled",
|
|
187
|
+
* and `cleanup()` to release the fallback-relay listener.
|
|
188
|
+
*/
|
|
189
|
+
function buildRequestAbortSignal(options) {
|
|
190
|
+
const timeoutMs = options?.timeoutMs ?? SDK_REQUEST_TIMEOUT_OVERRIDE ?? core_2.REQUEST_TIMEOUT_MS;
|
|
191
|
+
const timeoutController = new AbortController();
|
|
192
|
+
let didTimeout = false;
|
|
193
|
+
const timeoutId = setTimeout(() => {
|
|
194
|
+
didTimeout = true;
|
|
195
|
+
timeoutController.abort();
|
|
196
|
+
}, timeoutMs);
|
|
197
|
+
let signal = timeoutController.signal;
|
|
198
|
+
// No-op by default; replaced with a real listener-removal closure only when
|
|
199
|
+
// the manual relay fallback path below registers an 'abort' listener.
|
|
200
|
+
let relayCleanup = () => undefined;
|
|
201
|
+
if (options?.signal) {
|
|
202
|
+
// Capture in a local so the cleanup closure below keeps the non-nullable
|
|
203
|
+
// narrowing (a closure over `options.signal` would otherwise be re-widened).
|
|
204
|
+
const callerSignal = options.signal;
|
|
205
|
+
if (typeof AbortSignal.any === 'function') {
|
|
206
|
+
signal = AbortSignal.any([timeoutController.signal, callerSignal]);
|
|
207
|
+
}
|
|
208
|
+
else if (callerSignal.aborted) {
|
|
209
|
+
timeoutController.abort();
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const relay = () => timeoutController.abort();
|
|
213
|
+
callerSignal.addEventListener('abort', relay, { once: true });
|
|
214
|
+
relayCleanup = () => callerSignal.removeEventListener('abort', relay);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
signal,
|
|
219
|
+
timeoutId,
|
|
220
|
+
timeoutMs,
|
|
221
|
+
timedOut: () => didTimeout,
|
|
222
|
+
cleanup: () => {
|
|
223
|
+
clearTimeout(timeoutId);
|
|
224
|
+
relayCleanup();
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Build the `UpshiftTimeoutError` a fetcher throws when its own timeout fired,
|
|
230
|
+
* and record it as a breadcrumb rather than a Sentry issue.
|
|
231
|
+
*
|
|
232
|
+
* Severity rationale: the error is *thrown* to the caller, so whoever owns the
|
|
233
|
+
* request decides whether it is fatal. Logging it at `error` here as well made
|
|
234
|
+
* every timeout a standalone Sentry issue even when the caller swallowed it
|
|
235
|
+
* (`Promise.allSettled` fan-outs in `getVaultAllocations` do exactly that) — a
|
|
236
|
+
* single stalled render produced 20–30 identical issues in the same second and
|
|
237
|
+
* ~40k events a week, none of them separately actionable. As a breadcrumb the
|
|
238
|
+
* timeout still rides along with whatever the caller ultimately reports, with
|
|
239
|
+
* the endpoint and elapsed budget attached.
|
|
240
|
+
*
|
|
241
|
+
* @param tag - Call-site tag (`fetchUpshiftPublic`, …) used as the breadcrumb label.
|
|
242
|
+
* @param relativeUrl - Endpoint path that timed out; sanitized before use.
|
|
243
|
+
* @param timeoutMs - Timeout budget that elapsed, in milliseconds.
|
|
244
|
+
* @returns The error to throw. Never throws.
|
|
245
|
+
* @internal
|
|
246
|
+
*/
|
|
247
|
+
function timeoutErrorFor(tag, relativeUrl, timeoutMs) {
|
|
248
|
+
const timeoutError = new errors_1.UpshiftTimeoutError((0, sanitize_1.sanitizeString)(`Request timeout after ${timeoutMs / 1000}s: ${relativeUrl}`), timeoutMs);
|
|
249
|
+
logger_1.Logger.log.warn(tag, { relativeUrl, timeoutMs, reason: 'request timeout' });
|
|
250
|
+
return timeoutError;
|
|
251
|
+
}
|
|
252
|
+
let _overrideDeprecationWarned = false;
|
|
253
|
+
function warnOverrideDeprecation(callerTag) {
|
|
254
|
+
if (_overrideDeprecationWarned)
|
|
255
|
+
return;
|
|
256
|
+
_overrideDeprecationWarned = true;
|
|
257
|
+
// Deliberately a direct console.warn, not Logger.log.warn: a deprecation
|
|
258
|
+
// notice must surface in the integrator's console in every environment.
|
|
259
|
+
// Logger.log.warn would downgrade it to a prod-silent Sentry breadcrumb.
|
|
260
|
+
// biome-ignore lint/suspicious/noConsole: integrator-facing deprecation notice must surface in every environment; Logger.log.warn would downgrade it to a prod-silent Sentry breadcrumb.
|
|
261
|
+
console.warn(`[@upshiftfinance/sdk] ${callerTag}: option "override" is deprecated and has no effect — an absent apiKey already sends an anonymous request. It will be removed in a future major version; drop it.`);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* LRU cache for API responses and frequently accessed data.
|
|
265
|
+
* 24-hour TTL with 1000 item capacity.
|
|
266
|
+
*/
|
|
267
|
+
/**
|
|
268
|
+
* Read a response header tolerantly. Real `fetch` returns a `Headers` instance,
|
|
269
|
+
* but some test mocks supply a plain object; both shapes are supported here.
|
|
270
|
+
*/
|
|
271
|
+
function readResponseHeader(headers, name) {
|
|
272
|
+
if (!headers)
|
|
273
|
+
return undefined;
|
|
274
|
+
const h = headers;
|
|
275
|
+
if (typeof h.get === 'function') {
|
|
276
|
+
return h.get(name) ?? undefined;
|
|
277
|
+
}
|
|
278
|
+
const v = headers[name];
|
|
279
|
+
return typeof v === 'string' ? v : undefined;
|
|
280
|
+
}
|
|
281
|
+
const cache_1 = require("./cache");
|
|
282
|
+
Object.defineProperty(exports, "CACHE", { enumerable: true, get: function () { return cache_1.CACHE; } });
|
|
283
|
+
Object.defineProperty(exports, "getSafeCache", { enumerable: true, get: function () { return cache_1.getSafeCache; } });
|
|
284
|
+
Object.defineProperty(exports, "setSafeCache", { enumerable: true, get: function () { return cache_1.setSafeCache; } });
|
|
285
|
+
/**
|
|
286
|
+
* TTL for the per-vault "active loans" lists returned by
|
|
287
|
+
* {@link fetchTokenizedVaultLoans} / {@link fetchTokenizedVaultSubaccountLoans}.
|
|
288
|
+
*
|
|
289
|
+
* Loan state changes as borrowers draw and repay, so these lists must not sit
|
|
290
|
+
* on the 24 h `CACHE` default — a loan that was repaid an hour ago would still
|
|
291
|
+
* be reported as active. 10 minutes matches the vault-catalog TTL used by
|
|
292
|
+
* `fetchTokenizedVault`.
|
|
293
|
+
*/
|
|
294
|
+
const ACTIVE_LOANS_TTL_MS = 10 * 60 * 1000;
|
|
295
|
+
/**
|
|
296
|
+
* Drop `null` / `undefined` entries from a header map before it reaches
|
|
297
|
+
* `fetch`.
|
|
298
|
+
*
|
|
299
|
+
* Why: `UpshiftBase.monitoring` starts life as
|
|
300
|
+
* `{ 'x-user-id': undefined, 'x-environment': 'PROD' }` and is spread straight
|
|
301
|
+
* into request headers. `fetch` stringifies header values, so an `undefined`
|
|
302
|
+
* slot is sent on the wire as the literal string `"undefined"` — the backend
|
|
303
|
+
* then sees a bogus user id until `updateWallet` runs. Stripping nullish
|
|
304
|
+
* values here means "not set" is actually absent from the request.
|
|
305
|
+
*
|
|
306
|
+
* @param headers - Header map that may contain nullish values (an
|
|
307
|
+
* `IWSMonitorHeaders` object or a spread of one). `undefined` as a whole
|
|
308
|
+
* yields `{}`.
|
|
309
|
+
* @returns A new map containing only the entries whose value is a string.
|
|
310
|
+
* Never mutates the input.
|
|
311
|
+
* @internal
|
|
312
|
+
*/
|
|
313
|
+
function compactHeaders(headers) {
|
|
314
|
+
const out = {};
|
|
315
|
+
if (!headers)
|
|
316
|
+
return out;
|
|
317
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
318
|
+
if (typeof value === 'string')
|
|
319
|
+
out[name] = value;
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Dedicated cache for token prices with short TTL to prevent rate limiting.
|
|
325
|
+
* 30-second TTL with 500 item capacity.
|
|
326
|
+
*/
|
|
327
|
+
const PRICE_CACHE = new lru_cache_1.LRUCache({
|
|
328
|
+
max: 500,
|
|
329
|
+
ttl: 1000 * 30, // 30 seconds
|
|
330
|
+
allowStale: false,
|
|
331
|
+
});
|
|
332
|
+
/**
|
|
333
|
+
* Cache for failed price requests to prevent retry storms.
|
|
334
|
+
* 5-second TTL to allow quick retry after temporary failures.
|
|
335
|
+
*/
|
|
336
|
+
const PRICE_ERROR_CACHE = new lru_cache_1.LRUCache({
|
|
337
|
+
max: 100,
|
|
338
|
+
ttl: 1000 * 5, // 5 seconds
|
|
339
|
+
allowStale: false,
|
|
340
|
+
});
|
|
341
|
+
/**
|
|
342
|
+
* In-flight price requests to prevent duplicate simultaneous API calls.
|
|
343
|
+
*/
|
|
344
|
+
const PRICE_REQUESTS = new Map();
|
|
345
|
+
/**
|
|
346
|
+
* In-flight `/tokenized_vault` requests, keyed by the same cache key the
|
|
347
|
+
* response is stored under.
|
|
348
|
+
*
|
|
349
|
+
* The response cache alone only de-duplicates *sequential* calls: a cold
|
|
350
|
+
* process (a fresh serverless container, a hard page load) fans out N
|
|
351
|
+
* concurrent `getVault` calls for the same vault before any of them has
|
|
352
|
+
* populated the cache, so all N hit the backend. Under load that self-inflicted
|
|
353
|
+
* stampede is what pushed `/tokenized_vault/{address}` past its timeout — the
|
|
354
|
+
* single largest source of `UpshiftTimeoutError` in production.
|
|
355
|
+
*
|
|
356
|
+
* Joining concurrent callers onto one promise collapses that fan-out to a
|
|
357
|
+
* single request. Entries are removed as soon as the request settles, so a
|
|
358
|
+
* failure is never cached — the next caller retries.
|
|
359
|
+
*/
|
|
360
|
+
const TOKENIZED_VAULT_REQUESTS = new Map();
|
|
361
|
+
/**
|
|
362
|
+
* Run `factory` at most once per `key` while it is in flight, joining any
|
|
363
|
+
* concurrent caller onto the same promise.
|
|
364
|
+
*
|
|
365
|
+
* This is a de-duplication primitive, not a cache: the entry lives only for the
|
|
366
|
+
* duration of the request and is cleared on both success and failure, so errors
|
|
367
|
+
* are never shared beyond the callers that were already waiting.
|
|
368
|
+
*
|
|
369
|
+
* @param registry - Map holding the in-flight promises for this resource type.
|
|
370
|
+
* @param key - Identity of the request; callers sharing a key share a result.
|
|
371
|
+
* @param factory - Produces the promise to run when no request is in flight.
|
|
372
|
+
* @returns The shared promise for `key`.
|
|
373
|
+
* @internal
|
|
374
|
+
*/
|
|
375
|
+
function dedupeInFlight(registry, key, factory) {
|
|
376
|
+
const existing = registry.get(key);
|
|
377
|
+
if (existing)
|
|
378
|
+
return existing;
|
|
379
|
+
const request = factory().finally(() => {
|
|
380
|
+
registry.delete(key);
|
|
381
|
+
});
|
|
382
|
+
registry.set(key, request);
|
|
383
|
+
return request;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Build an Upshift backend URL, rejecting unknown server keys and any
|
|
387
|
+
* `relativeUrl` that would escape the configured origin.
|
|
388
|
+
* Throws `UpshiftValidationError` with `code: 'INVALID_URL'`.
|
|
389
|
+
*/
|
|
390
|
+
function buildUpshiftUrl(server, relativeUrl) {
|
|
391
|
+
const serverKey = server;
|
|
392
|
+
// The `public` base is overridable (setPublicApiBaseUrl / IUpshiftBase
|
|
393
|
+
// .publicApiBaseUrl) so non-prod deployments can point vault-catalog reads at
|
|
394
|
+
// an isolated backend. Resolved once here via getPublicApiBaseUrl() (single
|
|
395
|
+
// source of truth), so the origin check below validates against the OVERRIDDEN
|
|
396
|
+
// origin.
|
|
397
|
+
const base = serverKey === 'public' ? getPublicApiBaseUrl() : core_2.WEBSERVER_URL[serverKey];
|
|
398
|
+
if (!base) {
|
|
399
|
+
throw new errors_1.UpshiftValidationError('INVALID_URL', `Unknown Upshift server "${String(server)}". Expected one of: ${Object.keys(core_2.WEBSERVER_URL).join(', ')}`);
|
|
400
|
+
}
|
|
401
|
+
if (typeof relativeUrl !== 'string' || relativeUrl.length === 0) {
|
|
402
|
+
throw new errors_1.UpshiftValidationError('INVALID_URL', 'relativeUrl must be a non-empty string');
|
|
403
|
+
}
|
|
404
|
+
// Reject anything that looks like a scheme or protocol-relative authority.
|
|
405
|
+
if (/^\s*[a-z][a-z0-9+.-]*:|^\s*\/\//i.test(relativeUrl)) {
|
|
406
|
+
throw new errors_1.UpshiftValidationError('INVALID_URL', `relativeUrl must be a path, not an absolute or protocol-relative URL`);
|
|
407
|
+
}
|
|
408
|
+
// Concat (rather than `new URL(rel, base)`) preserves base paths like /api/v1
|
|
409
|
+
// when relativeUrl starts with `/`. Origin check below is a second line.
|
|
410
|
+
const candidate = `${base}${relativeUrl}`;
|
|
411
|
+
let url;
|
|
412
|
+
try {
|
|
413
|
+
url = new URL(candidate);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
throw new errors_1.UpshiftValidationError('INVALID_URL', `Invalid URL path: ${(0, sanitize_1.sanitizeString)(relativeUrl)}`);
|
|
417
|
+
}
|
|
418
|
+
const baseOrigin = new URL(base).origin;
|
|
419
|
+
if (url.origin !== baseOrigin) {
|
|
420
|
+
throw new errors_1.UpshiftValidationError('INVALID_URL', `URL origin mismatch: refusing to fetch ${url.origin} from a "${serverKey}" request scoped to ${baseOrigin}`);
|
|
421
|
+
}
|
|
422
|
+
return url.toString();
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Make requests to Upshift backend services, sending an API key when one is
|
|
426
|
+
* configured. Many backend routes — vault-linked sub-account reads, the two
|
|
427
|
+
* collateral risk routes, timelock requests — answer anonymous callers, so a
|
|
428
|
+
* missing key is not an error here: the request goes out without the
|
|
429
|
+
* `x-api-key` header and the backend decides. Routes that do require a key
|
|
430
|
+
* answer `401`, which surfaces as an `UpshiftAuthError` with code
|
|
431
|
+
* `AUTH_UNAUTHORIZED`.
|
|
432
|
+
* Automatically logs errors with correlation IDs for debugging.
|
|
433
|
+
* @param apiKey - Upshift API key, or `undefined` for an anonymous request
|
|
434
|
+
* @param relativeUrl - Endpoint path relative to base URL
|
|
435
|
+
* @param options - Request configuration including method and headers
|
|
436
|
+
* @returns Response object if successful
|
|
437
|
+
* @throws Error with correlation ID if request fails
|
|
438
|
+
*/
|
|
439
|
+
async function fetchUpshiftWithKey(apiKey, relativeUrl, options) {
|
|
440
|
+
if (options?.override) {
|
|
441
|
+
warnOverrideDeprecation('fetchUpshiftWithKey');
|
|
442
|
+
}
|
|
443
|
+
const defaultHeaders = {
|
|
444
|
+
...sdkClientHeaders(),
|
|
445
|
+
...(apiKey ? { 'x-api-key': apiKey } : {}),
|
|
446
|
+
'content-type': 'application/json',
|
|
447
|
+
accept: 'application/json',
|
|
448
|
+
};
|
|
449
|
+
const { signal: requestSignal, timeoutMs: effectiveTimeoutMs, timedOut, cleanup, } = buildRequestAbortSignal(options);
|
|
450
|
+
const startTime = performance.now();
|
|
451
|
+
const server = options?.server || 'production';
|
|
452
|
+
// Set once the response has been recorded with its real status, so the
|
|
453
|
+
// `catch` below does not record the same non-OK request a second time as
|
|
454
|
+
// status 0 (which double-counted every 4xx/5xx in the API-call metrics).
|
|
455
|
+
let tracked = false;
|
|
456
|
+
try {
|
|
457
|
+
const safeUrl = buildUpshiftUrl(server, relativeUrl);
|
|
458
|
+
const res = await fetch(safeUrl, {
|
|
459
|
+
headers: compactHeaders({
|
|
460
|
+
...defaultHeaders,
|
|
461
|
+
...options?.headers,
|
|
462
|
+
}),
|
|
463
|
+
method: options?.method ?? 'GET',
|
|
464
|
+
body: JSON.stringify(options?.data),
|
|
465
|
+
signal: requestSignal,
|
|
466
|
+
});
|
|
467
|
+
cleanup();
|
|
468
|
+
// Track API call
|
|
469
|
+
(0, analytics_1.trackApiCall)(relativeUrl, options?.method ?? 'GET', startTime, res.status, server);
|
|
470
|
+
tracked = true;
|
|
471
|
+
const correlationId = readResponseHeader(res.headers, 'x-correlation-id');
|
|
472
|
+
const logger = logger_1.Logger.getLogger();
|
|
473
|
+
if (!res.ok) {
|
|
474
|
+
if (correlationId && logger) {
|
|
475
|
+
logger.setTag('correlation_id', correlationId);
|
|
476
|
+
}
|
|
477
|
+
const error = errorFromResponseStatus(res.status, `Request failed: ${res.status}`, { correlationId });
|
|
478
|
+
logger?.captureException(error, { correlationId, status: res.status });
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
return res;
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
cleanup();
|
|
485
|
+
if (!tracked) {
|
|
486
|
+
(0, analytics_1.trackApiCall)(relativeUrl, options?.method ?? 'GET', startTime, 0, server);
|
|
487
|
+
}
|
|
488
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
489
|
+
if (err.name === 'AbortError') {
|
|
490
|
+
if (!timedOut()) {
|
|
491
|
+
throw err;
|
|
492
|
+
}
|
|
493
|
+
throw timeoutErrorFor('fetchUpshiftWithKey', relativeUrl, effectiveTimeoutMs);
|
|
494
|
+
}
|
|
495
|
+
throw err;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Make unauthenticated requests to Upshift public API endpoints.
|
|
500
|
+
* Used for fetching vault metadata and public market data.
|
|
501
|
+
* @param relativeUrl - Endpoint path relative to public API base URL
|
|
502
|
+
* @param options - Request configuration including method and headers
|
|
503
|
+
* @returns Response object if successful
|
|
504
|
+
* @throws Error with correlation ID if request fails
|
|
505
|
+
*/
|
|
506
|
+
async function fetchUpshiftPublic(relativeUrl, options) {
|
|
507
|
+
const defaultHeaders = {
|
|
508
|
+
...sdkClientHeaders(),
|
|
509
|
+
'content-type': 'application/json',
|
|
510
|
+
accept: 'application/json',
|
|
511
|
+
};
|
|
512
|
+
const { signal: requestSignal, timeoutMs: effectiveTimeoutMs, timedOut, cleanup, } = buildRequestAbortSignal(options);
|
|
513
|
+
const startTime = performance.now();
|
|
514
|
+
// See fetchUpshiftWithKey: prevents a second status-0 record for non-OK responses.
|
|
515
|
+
let tracked = false;
|
|
516
|
+
try {
|
|
517
|
+
const safeUrl = buildUpshiftUrl('public', relativeUrl);
|
|
518
|
+
const res = await fetch(safeUrl, {
|
|
519
|
+
headers: compactHeaders({
|
|
520
|
+
...defaultHeaders,
|
|
521
|
+
...options?.headers,
|
|
522
|
+
}),
|
|
523
|
+
method: options?.method ?? 'GET',
|
|
524
|
+
body: JSON.stringify(options?.data),
|
|
525
|
+
signal: requestSignal,
|
|
526
|
+
});
|
|
527
|
+
cleanup();
|
|
528
|
+
(0, analytics_1.trackApiCall)(relativeUrl, options?.method ?? 'GET', startTime, res.status, 'public');
|
|
529
|
+
tracked = true;
|
|
530
|
+
const correlationId = readResponseHeader(res.headers, 'x-correlation-id');
|
|
531
|
+
const logger = logger_1.Logger.getLogger();
|
|
532
|
+
if (!res.ok) {
|
|
533
|
+
if (correlationId && logger) {
|
|
534
|
+
logger.setTag('correlation_id', correlationId);
|
|
535
|
+
}
|
|
536
|
+
const error = errorFromResponseStatus(res.status, `Request failed: ${res.status}`, { correlationId });
|
|
537
|
+
logger?.captureException(error, { correlationId, status: res.status });
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
return res;
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
cleanup();
|
|
544
|
+
if (!tracked) {
|
|
545
|
+
(0, analytics_1.trackApiCall)(relativeUrl, options?.method ?? 'GET', startTime, 0, 'public');
|
|
546
|
+
}
|
|
547
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
548
|
+
if (err.name === 'AbortError') {
|
|
549
|
+
if (!timedOut()) {
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
throw timeoutErrorFor('fetchUpshiftPublic', relativeUrl, effectiveTimeoutMs);
|
|
553
|
+
}
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Make authenticated requests using Bearer token authentication.
|
|
559
|
+
* Typically used for user-specific operations requiring OAuth tokens.
|
|
560
|
+
* @param bearerToken - OAuth bearer token
|
|
561
|
+
* @param relativeUrl - Endpoint path relative to base URL
|
|
562
|
+
* @param options - Request configuration including method and headers
|
|
563
|
+
* @returns Response object if successful
|
|
564
|
+
* @throws Error with correlation ID if request fails
|
|
565
|
+
*/
|
|
566
|
+
async function fetchUpshiftWithBearer(bearerToken, relativeUrl, options) {
|
|
567
|
+
const defaultHeaders = {
|
|
568
|
+
...sdkClientHeaders(),
|
|
569
|
+
authorization: `Bearer ${bearerToken}`,
|
|
570
|
+
'content-type': 'application/json',
|
|
571
|
+
accept: 'application/json',
|
|
572
|
+
};
|
|
573
|
+
const { signal: requestSignal, timeoutMs: effectiveTimeoutMs, timedOut, cleanup, } = buildRequestAbortSignal(options);
|
|
574
|
+
const startTime = performance.now();
|
|
575
|
+
const server = options?.server ?? 'production';
|
|
576
|
+
// See fetchUpshiftWithKey: prevents a second status-0 record for non-OK responses.
|
|
577
|
+
let tracked = false;
|
|
578
|
+
try {
|
|
579
|
+
const safeUrl = buildUpshiftUrl(server, relativeUrl);
|
|
580
|
+
const res = await fetch(safeUrl, {
|
|
581
|
+
headers: compactHeaders({
|
|
582
|
+
...defaultHeaders,
|
|
583
|
+
...options?.headers,
|
|
584
|
+
}),
|
|
585
|
+
method: options?.method ?? 'GET',
|
|
586
|
+
body: JSON.stringify(options?.data),
|
|
587
|
+
signal: requestSignal,
|
|
588
|
+
});
|
|
589
|
+
cleanup();
|
|
590
|
+
(0, analytics_1.trackApiCall)(relativeUrl, options?.method ?? 'GET', startTime, res.status, server);
|
|
591
|
+
tracked = true;
|
|
592
|
+
const correlationId = readResponseHeader(res.headers, 'x-correlation-id');
|
|
593
|
+
const logger = logger_1.Logger.getLogger();
|
|
594
|
+
if (!res.ok) {
|
|
595
|
+
if (correlationId && logger) {
|
|
596
|
+
logger.setTag('correlation_id', correlationId);
|
|
597
|
+
}
|
|
598
|
+
const error = errorFromResponseStatus(res.status, `Request failed: ${res.status}`, { correlationId });
|
|
599
|
+
logger?.captureException(error, { correlationId, status: res.status });
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
return res;
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
cleanup();
|
|
606
|
+
if (!tracked) {
|
|
607
|
+
(0, analytics_1.trackApiCall)(relativeUrl, options?.method ?? 'GET', startTime, 0, server);
|
|
608
|
+
}
|
|
609
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
610
|
+
if (err.name === 'AbortError') {
|
|
611
|
+
if (!timedOut()) {
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
throw timeoutErrorFor('fetchUpshiftWithBearer', relativeUrl, effectiveTimeoutMs);
|
|
615
|
+
}
|
|
616
|
+
throw err;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Check if an error is network-related and suitable for retry.
|
|
621
|
+
* Identifies timeouts, connection resets, and fetch failures.
|
|
622
|
+
*/
|
|
623
|
+
function isRetryableError(error) {
|
|
624
|
+
return ((error instanceof TypeError && error.message.includes('fetch failed')) ||
|
|
625
|
+
error.name === 'AbortError' ||
|
|
626
|
+
error.message.includes('ETIMEDOUT') ||
|
|
627
|
+
error.message.includes('timeout') ||
|
|
628
|
+
error.message.includes('ECONNRESET') ||
|
|
629
|
+
error.message.includes('ENOTFOUND') ||
|
|
630
|
+
error.message.includes('ECONNREFUSED'));
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Wrap async operations with exponential backoff retry logic.
|
|
634
|
+
* Retries on network errors with increasing delays between attempts.
|
|
635
|
+
* @param fn - Async function to execute with retry logic
|
|
636
|
+
* @param maxRetries - Maximum number of retry attempts
|
|
637
|
+
* @param baseDelay - Initial delay in milliseconds (doubles each retry)
|
|
638
|
+
* @param shouldRetry - Custom function to determine if error is retryable
|
|
639
|
+
* @returns Result of successful function execution
|
|
640
|
+
* @throws Last error if all retries exhausted
|
|
641
|
+
*/
|
|
642
|
+
async function withRetry(fn, maxRetries = 3, baseDelay = 1000, shouldRetry = isRetryableError) {
|
|
643
|
+
let lastError;
|
|
644
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
645
|
+
try {
|
|
646
|
+
return await fn();
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
lastError = error;
|
|
650
|
+
if (attempt < maxRetries && shouldRetry(error)) {
|
|
651
|
+
const delay = baseDelay * 2 ** attempt;
|
|
652
|
+
logger_1.Logger.log.warn('withRetry', `Attempt ${attempt + 1} failed, retrying in ${delay}ms: ${error.message}`);
|
|
653
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Unreachable in practice: the final attempt re-throws inside the loop. This
|
|
660
|
+
// satisfies the compiler's control-flow analysis without a non-null assertion.
|
|
661
|
+
throw lastError ?? new Error('withRetry exhausted retries without an error');
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Fetch tokenized vault configurations from backend API.
|
|
665
|
+
* Results are cached to reduce API calls. Optionally filter by specific vault address.
|
|
666
|
+
* @param pool - Optional vault address to filter results
|
|
667
|
+
* @param headers - Monitoring headers for request tracking
|
|
668
|
+
* @returns Array of tokenized vault configurations
|
|
669
|
+
*/
|
|
670
|
+
async function fetchTokenizedVault(pool, headers, loadSubaccounts, loadSnapshots) {
|
|
671
|
+
if (!pool) {
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
// Build cache key based on loading options
|
|
675
|
+
const keyParts = [`tokenized-vault-${pool}`];
|
|
676
|
+
if (loadSubaccounts === false)
|
|
677
|
+
keyParts.push('no-subaccounts');
|
|
678
|
+
if (loadSnapshots === false)
|
|
679
|
+
keyParts.push('no-snapshots');
|
|
680
|
+
const key = keyParts.join('-');
|
|
681
|
+
let tokenizedVault;
|
|
682
|
+
const cachedResponse = await (0, cache_1.getSafeCache)(key);
|
|
683
|
+
if (cachedResponse) {
|
|
684
|
+
tokenizedVault = cachedResponse;
|
|
685
|
+
}
|
|
686
|
+
if (!tokenizedVault) {
|
|
687
|
+
// Concurrent callers for the same vault share one request; see
|
|
688
|
+
// TOKENIZED_VAULT_REQUESTS.
|
|
689
|
+
tokenizedVault = await dedupeInFlight(TOKENIZED_VAULT_REQUESTS, key, async () => {
|
|
690
|
+
// Build endpoint URL with optional query parameters
|
|
691
|
+
let endpoint = core_2.WEBSERVER_ENDPOINTS.public.tokenizedVault.byVaultAddress(pool);
|
|
692
|
+
const params = new URLSearchParams();
|
|
693
|
+
if (loadSubaccounts === false) {
|
|
694
|
+
params.set('load_subaccounts', 'false');
|
|
695
|
+
}
|
|
696
|
+
if (loadSnapshots === false) {
|
|
697
|
+
params.set('load_snapshots', 'false');
|
|
698
|
+
}
|
|
699
|
+
const queryString = params.toString();
|
|
700
|
+
if (queryString) {
|
|
701
|
+
endpoint += `?${queryString}`;
|
|
702
|
+
}
|
|
703
|
+
const tokenizedVaultsResponse = await fetchUpshiftPublic(endpoint, {
|
|
704
|
+
headers: headers,
|
|
705
|
+
});
|
|
706
|
+
const parsed = (await tokenizedVaultsResponse.json());
|
|
707
|
+
cache_1.CACHE.set(key, parsed, { ttl: 1000 * 60 * 10 }); // Cache for 10 minutes
|
|
708
|
+
return parsed;
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
// Filter by specific vault if address provided
|
|
712
|
+
if (pool &&
|
|
713
|
+
((0, ethers_1.isAddress)(pool) ||
|
|
714
|
+
(0, chain_address_1.isSolanaAddress)(pool) ||
|
|
715
|
+
(0, chain_address_1.isStellarAddress)(pool) ||
|
|
716
|
+
(0, chain_address_1.isSuiAddress)(pool))) {
|
|
717
|
+
return tokenizedVault ? [tokenizedVault] : [];
|
|
718
|
+
}
|
|
719
|
+
return [tokenizedVault];
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Fetch tokenized vault configurations from backend API.
|
|
723
|
+
* Results are cached to reduce API calls. Optionally filter by specific vault address.
|
|
724
|
+
* @param pool - Optional vault address to filter results
|
|
725
|
+
* @param headers - Monitoring headers for request tracking
|
|
726
|
+
* @param loadSubaccounts - Optional flag to load subaccounts and EOA operators (defaults to true on API)
|
|
727
|
+
* @returns Array of tokenized vault configurations
|
|
728
|
+
*/
|
|
729
|
+
async function fetchTokenizedVaults(pool, headers, loadSubaccounts, loadSnapshots) {
|
|
730
|
+
// Build cache key from the request shape only. `pool` is NOT part of the
|
|
731
|
+
// request (the endpoint always returns the full catalog), so keying on it
|
|
732
|
+
// would store one full catalog copy per pool address and refetch it for
|
|
733
|
+
// every new pool. The pool filter is applied after the cached read below.
|
|
734
|
+
const keyParts = ['tokenized-vaults'];
|
|
735
|
+
if (loadSubaccounts === false)
|
|
736
|
+
keyParts.push('no-subaccounts');
|
|
737
|
+
if (loadSnapshots === false)
|
|
738
|
+
keyParts.push('no-snapshots');
|
|
739
|
+
const key = keyParts.join('-');
|
|
740
|
+
let tokenizedVaults;
|
|
741
|
+
const cachedResponse = await (0, cache_1.getSafeCache)(key);
|
|
742
|
+
if (cachedResponse) {
|
|
743
|
+
tokenizedVaults = cachedResponse;
|
|
744
|
+
}
|
|
745
|
+
if (!tokenizedVaults) {
|
|
746
|
+
// The vault list is requested by every vault page at once on a cold
|
|
747
|
+
// process; join concurrent callers onto one request.
|
|
748
|
+
tokenizedVaults = await dedupeInFlight(TOKENIZED_VAULT_REQUESTS, key, async () => {
|
|
749
|
+
// Build endpoint URL with optional query parameters
|
|
750
|
+
let endpoint = core_2.WEBSERVER_ENDPOINTS.public.tokenizedVault.list;
|
|
751
|
+
const params = new URLSearchParams();
|
|
752
|
+
if (loadSubaccounts === false) {
|
|
753
|
+
params.set('load_subaccounts', 'false');
|
|
754
|
+
}
|
|
755
|
+
if (loadSnapshots === false) {
|
|
756
|
+
params.set('load_snapshots', 'false');
|
|
757
|
+
}
|
|
758
|
+
const queryString = params.toString();
|
|
759
|
+
if (queryString) {
|
|
760
|
+
endpoint += `?${queryString}`;
|
|
761
|
+
}
|
|
762
|
+
const tokenizedVaultsResponse = await fetchUpshiftPublic(endpoint, {
|
|
763
|
+
headers: headers,
|
|
764
|
+
});
|
|
765
|
+
const parsed = (await tokenizedVaultsResponse.json());
|
|
766
|
+
const ttl = 1000 * 60 * 15; // 15 minute cache
|
|
767
|
+
cache_1.CACHE.set(key, parsed, { ttl });
|
|
768
|
+
return parsed;
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
// Filter by specific vault if address provided
|
|
772
|
+
if (pool &&
|
|
773
|
+
((0, ethers_1.isAddress)(pool) ||
|
|
774
|
+
(0, chain_address_1.isSolanaAddress)(pool) ||
|
|
775
|
+
(0, chain_address_1.isStellarAddress)(pool) ||
|
|
776
|
+
(0, chain_address_1.isSuiAddress)(pool))) {
|
|
777
|
+
const foundObj = tokenizedVaults.find((t) => t.address.toLowerCase() === pool.toLowerCase());
|
|
778
|
+
return foundObj ? [foundObj] : [];
|
|
779
|
+
}
|
|
780
|
+
return tokenizedVaults;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Fetch current USD price for a token symbol or vault LP token.
|
|
784
|
+
* For vault LP tokens, calculates share price using totalAssets/totalSupply ratio.
|
|
785
|
+
* Falls back to CoinGecko if primary source fails.
|
|
786
|
+
* @param symbol - Token symbol or contract address
|
|
787
|
+
* @param provider - Optional web3 provider for on-chain price calculations
|
|
788
|
+
* @param coinGeckoKey - Optional CoinGecko API key for fallback pricing
|
|
789
|
+
* @param headers - Monitoring headers for request tracking
|
|
790
|
+
* @returns Token price in USD
|
|
791
|
+
*/
|
|
792
|
+
async function fetchTokenPrice(symbol, provider, coinGeckoKey, headers) {
|
|
793
|
+
if (!symbol) {
|
|
794
|
+
logger_1.Logger.log.error('fetchTokenPrice', 'symbol parameter is undefined');
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
// Normalize symbol and apply aliases BEFORE generating cache key
|
|
798
|
+
let normalizedSymbol = symbol.toLowerCase();
|
|
799
|
+
normalizedSymbol = normalizedSymbol === 'xbtc' ? 'btc' : normalizedSymbol;
|
|
800
|
+
normalizedSymbol = normalizedSymbol === 'strbtc' ? 'btc' : normalizedSymbol;
|
|
801
|
+
// Generate cache key based on normalized symbol and provider presence
|
|
802
|
+
const cacheKey = `price:${normalizedSymbol}:${provider ? 'with-provider' : 'no-provider'}`;
|
|
803
|
+
// Check error cache to prevent retry storms after failures
|
|
804
|
+
const cachedError = PRICE_ERROR_CACHE.get(cacheKey);
|
|
805
|
+
if (cachedError) {
|
|
806
|
+
logger_1.Logger.log.info('fetchTokenPrice', `Error cache HIT for ${normalizedSymbol} - throwing cached error`);
|
|
807
|
+
throw cachedError;
|
|
808
|
+
}
|
|
809
|
+
// Check price cache first to avoid rate limiting
|
|
810
|
+
const cachedPrice = PRICE_CACHE.get(cacheKey);
|
|
811
|
+
if (cachedPrice !== undefined) {
|
|
812
|
+
logger_1.Logger.log.info('fetchTokenPrice', `Cache HIT for ${normalizedSymbol}: $${cachedPrice}`);
|
|
813
|
+
return cachedPrice;
|
|
814
|
+
}
|
|
815
|
+
// Check for in-flight request to deduplicate simultaneous calls
|
|
816
|
+
const inflightRequest = PRICE_REQUESTS.get(cacheKey);
|
|
817
|
+
if (inflightRequest) {
|
|
818
|
+
logger_1.Logger.log.info('fetchTokenPrice', `In-flight request deduplication for ${normalizedSymbol}`);
|
|
819
|
+
return inflightRequest;
|
|
820
|
+
}
|
|
821
|
+
// Create the price fetching promise
|
|
822
|
+
const pricePromise = (async () => {
|
|
823
|
+
try {
|
|
824
|
+
const price = await _fetchTokenPriceInternal(symbol, provider, coinGeckoKey, headers);
|
|
825
|
+
// Cache the successful result
|
|
826
|
+
PRICE_CACHE.set(cacheKey, price);
|
|
827
|
+
// Clear any previous error cache on success
|
|
828
|
+
PRICE_ERROR_CACHE.delete(cacheKey);
|
|
829
|
+
return price;
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
// Cache the error temporarily to prevent retry storms
|
|
833
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
834
|
+
PRICE_ERROR_CACHE.set(cacheKey, err);
|
|
835
|
+
logger_1.Logger.log.info('fetchTokenPrice', `Cached error for ${normalizedSymbol}: ${err.message}`);
|
|
836
|
+
throw err;
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
// Clean up in-flight request tracking
|
|
840
|
+
PRICE_REQUESTS.delete(cacheKey);
|
|
841
|
+
}
|
|
842
|
+
})();
|
|
843
|
+
// Track this request to prevent duplicates
|
|
844
|
+
PRICE_REQUESTS.set(cacheKey, pricePromise);
|
|
845
|
+
return pricePromise;
|
|
846
|
+
}
|
|
847
|
+
// Normalize both raw integers to the same decimal scale BEFORE dividing —
|
|
848
|
+
// dividing the raw bigints first floors the ratio to ~1 and loses all
|
|
849
|
+
// fractional precision. Returns 1 for an empty vault (priced 1:1).
|
|
850
|
+
function normalizedSharePrice(totalAssets, totalSupply, decimals) {
|
|
851
|
+
const supply = Number((0, core_1.toNormalizedBn)(totalSupply, decimals).normalized);
|
|
852
|
+
if (!supply)
|
|
853
|
+
return 1;
|
|
854
|
+
return Number((0, core_1.toNormalizedBn)(totalAssets, decimals).normalized) / supply;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Internal price fetching implementation.
|
|
858
|
+
* This is separated to allow caching and request deduplication in the public function.
|
|
859
|
+
*/
|
|
860
|
+
async function _fetchTokenPriceInternal(symbol, provider, coinGeckoKey, headers) {
|
|
861
|
+
// Fetch vault metadata to check if symbol is a vault LP token
|
|
862
|
+
const tokenizedVaults = await fetchTokenizedVaults(undefined, headers, false, false);
|
|
863
|
+
const vaultArray = tokenizedVaults.map((vault) => ({
|
|
864
|
+
vault: vault.address,
|
|
865
|
+
symbol: vault.receipt_token_symbol,
|
|
866
|
+
}));
|
|
867
|
+
const foundVaultLpAsset = vaultArray.find((v) => v.symbol?.toLowerCase() === symbol?.toLowerCase());
|
|
868
|
+
// @todo: handle multi-asset vaults
|
|
869
|
+
const tokenizedVault = tokenizedVaults.find((vault) => vault.address === foundVaultLpAsset?.vault);
|
|
870
|
+
const version = (0, vault_version_1.getVaultVersionV2)(tokenizedVault);
|
|
871
|
+
if (foundVaultLpAsset?.vault && provider && version === 'evm-2') {
|
|
872
|
+
try {
|
|
873
|
+
const vaultContract = new ethers_2.Contract((0, ethers_1.getAddress)(foundVaultLpAsset?.vault), abis_1.ABI_TOKENIZED_VAULT_V2, provider);
|
|
874
|
+
// Same bounded, no-fallback retry the write paths use for this selector
|
|
875
|
+
// (`getReceiptTokenAddressOrThrow` in `core/helpers/web3`). It cannot be
|
|
876
|
+
// reused here: `helpers/web3` imports `fetchTokenizedVault` from this
|
|
877
|
+
// module, so importing it back would be a circular dependency (CLAUDE.md
|
|
878
|
+
// §9). A transient empty `eth_call` response is absorbed; a vault that
|
|
879
|
+
// genuinely lacks `lpTokenAddress()` still falls into the catch below
|
|
880
|
+
// after the attempts are spent.
|
|
881
|
+
const receiptAddress = await (0, chain_error_1.retryOnTransientRpc)('fetchTokenPrice:receiptToken', () => vaultContract.lpTokenAddress(), { vault: foundVaultLpAsset.vault }, (error) => (0, chain_error_1.isRetryableRpcError)(error) ||
|
|
882
|
+
(0, chain_error_1.isEmptyViewResponse)(error, chain_error_1.LP_TOKEN_ADDRESS_SELECTOR));
|
|
883
|
+
const receiptContract = new ethers_2.Contract(receiptAddress, [web3_1.MIN_ABIS.decimals], provider);
|
|
884
|
+
const decimals = Number(await receiptContract.decimals());
|
|
885
|
+
const sharePrice = await vaultContract.getSharePrice();
|
|
886
|
+
return Number((0, core_1.toNormalizedBn)(sharePrice, decimals).normalized);
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
throw new Error('failed to fetch token price for evm-2 vault');
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// Apply symbol normalization and aliases for API calls
|
|
893
|
+
let _symbol = symbol?.toLowerCase();
|
|
894
|
+
_symbol = _symbol === 'xbtc' ? 'btc' : _symbol;
|
|
895
|
+
_symbol = _symbol === 'strbtc' ? 'btc' : _symbol;
|
|
896
|
+
try {
|
|
897
|
+
// Fetch base token price from Upshift backend
|
|
898
|
+
const res = await fetch(`https://prices.augustdigital.io/price/${_symbol}`, { headers: compactHeaders(headers) });
|
|
899
|
+
if (res.status !== 200) {
|
|
900
|
+
throw new Error(`fetching ${_symbol} price: ${res?.statusText}`);
|
|
901
|
+
}
|
|
902
|
+
const json = (await res.json());
|
|
903
|
+
// For vault LP tokens, calculate share price using on-chain ratio
|
|
904
|
+
if (foundVaultLpAsset && provider) {
|
|
905
|
+
const vaultContract = new ethers_2.Contract((0, ethers_1.getAddress)(foundVaultLpAsset.vault), abis_1.ABI_LENDING_POOL_V2, provider);
|
|
906
|
+
const [totalAssets, totalSupply, rawDecimals] = await Promise.all([
|
|
907
|
+
vaultContract.totalAssets(),
|
|
908
|
+
vaultContract.totalSupply(),
|
|
909
|
+
vaultContract.decimals(),
|
|
910
|
+
]);
|
|
911
|
+
const decimals = Number(rawDecimals);
|
|
912
|
+
const sharePrice = normalizedSharePrice(totalAssets, totalSupply, decimals);
|
|
913
|
+
return json.price * sharePrice;
|
|
914
|
+
}
|
|
915
|
+
return json.price;
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
// Fallback to CoinGecko if primary source fails
|
|
919
|
+
const fallbackPrice = await (0, fetcher_1.fetchTokenPricesFromCoinGecko)(symbol, coinGeckoKey);
|
|
920
|
+
// CoinGecko resolves to `null` when it has no price series for the coin.
|
|
921
|
+
// Guard BEFORE the LP branch: `null * sharePrice` is `0`, which the caller
|
|
922
|
+
// would cache in PRICE_CACHE as a genuine $0 price for the next 30 s.
|
|
923
|
+
// Throwing instead lands in PRICE_ERROR_CACHE (5 s) and surfaces as a
|
|
924
|
+
// typed error the consumer can branch on.
|
|
925
|
+
if (!fallbackPrice) {
|
|
926
|
+
throw new errors_1.UpshiftPriceUnavailableError(`failed to fetch token price: no price available for "${symbol}" from the Upshift price server or CoinGecko`, { context: { symbol } });
|
|
927
|
+
}
|
|
928
|
+
if (foundVaultLpAsset && provider) {
|
|
929
|
+
const vaultContract = new ethers_2.Contract((0, ethers_1.getAddress)(foundVaultLpAsset.vault), abis_1.ABI_LENDING_POOL_V2, provider);
|
|
930
|
+
const [totalAssets, totalSupply, rawDecimals] = await Promise.all([
|
|
931
|
+
vaultContract.totalAssets(),
|
|
932
|
+
vaultContract.totalSupply(),
|
|
933
|
+
vaultContract.decimals(),
|
|
934
|
+
]);
|
|
935
|
+
const decimals = Number(rawDecimals);
|
|
936
|
+
const sharePrice = normalizedSharePrice(totalAssets, totalSupply, decimals);
|
|
937
|
+
return fallbackPrice * sharePrice;
|
|
938
|
+
}
|
|
939
|
+
return fallbackPrice;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Fetch token price by contract address and chain ID from Upshift price server.
|
|
944
|
+
* @param address - Token contract address
|
|
945
|
+
* @param chainId - Network chain ID
|
|
946
|
+
* @param headers - Optional monitoring headers for request tracking
|
|
947
|
+
* @returns Token price in USD, or 0 if not found
|
|
948
|
+
*/
|
|
949
|
+
async function fetchTokenPriceByAddress(address, chainId, headers) {
|
|
950
|
+
if (!address || !chainId) {
|
|
951
|
+
return 0;
|
|
952
|
+
}
|
|
953
|
+
try {
|
|
954
|
+
const url = new URL(`${core_2.PRICE_SERVER_URL}/price_by_address_chain`);
|
|
955
|
+
url.searchParams.set('address', address);
|
|
956
|
+
url.searchParams.set('chain', chainId.toString());
|
|
957
|
+
url.searchParams.set('method', 'contract');
|
|
958
|
+
const res = await fetch(url.toString(), {
|
|
959
|
+
headers: compactHeaders({
|
|
960
|
+
accept: 'application/json',
|
|
961
|
+
...headers,
|
|
962
|
+
}),
|
|
963
|
+
});
|
|
964
|
+
if (res.status !== 200) {
|
|
965
|
+
throw new Error(`Failed to fetch price for ${address} on chain ${chainId}: ${res.statusText}`);
|
|
966
|
+
}
|
|
967
|
+
const json = (await res.json());
|
|
968
|
+
return json.price || 0;
|
|
969
|
+
}
|
|
970
|
+
catch (e) {
|
|
971
|
+
logger_1.Logger.log.warn('fetchTokenPriceByAddress', `Failed to fetch price for ${address} on chain ${chainId}: ${e}`);
|
|
972
|
+
return 0;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Fetch active loans for a specific vault from backend API.
|
|
977
|
+
* Results are cached per vault and chain to reduce API calls.
|
|
978
|
+
* @param pool - Vault address
|
|
979
|
+
* @param chainId - Network chain ID
|
|
980
|
+
* @param headers - Monitoring headers for request tracking
|
|
981
|
+
* @returns Array of active loan objects
|
|
982
|
+
*/
|
|
983
|
+
async function fetchTokenizedVaultLoans(pool, chainId, headers) {
|
|
984
|
+
const key = `loans-${chainId}-${pool}`;
|
|
985
|
+
let loans;
|
|
986
|
+
const cached = await (0, cache_1.getSafeCache)(key);
|
|
987
|
+
if (cached) {
|
|
988
|
+
loans = cached;
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
const tokenizedVaultsResponse = await fetchUpshiftPublic(core_2.WEBSERVER_ENDPOINTS.public.tokenizedVault.loans(pool, chainId, true), { headers: headers });
|
|
992
|
+
loans = (await tokenizedVaultsResponse.json()).loans;
|
|
993
|
+
// Short TTL: loan state is live data (see ACTIVE_LOANS_TTL_MS). The
|
|
994
|
+
// falsy guard mirrors `setSafeCache` so a missing `loans` field is not
|
|
995
|
+
// cached as a sticky empty result.
|
|
996
|
+
if (loans)
|
|
997
|
+
cache_1.CACHE.set(key, loans, { ttl: ACTIVE_LOANS_TTL_MS });
|
|
998
|
+
}
|
|
999
|
+
return loans?.filter((l) => l.state.toLowerCase() === 'active');
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Fetch subaccount loans for a specific tokenized vault (lending pool).
|
|
1003
|
+
* Retrieves all active subaccount loan data from the backend.
|
|
1004
|
+
* @param pool - Vault address
|
|
1005
|
+
* @param chainId - Network chain ID
|
|
1006
|
+
* @param headers - Monitoring headers for request tracking
|
|
1007
|
+
* @returns Array of active subaccount loan objects
|
|
1008
|
+
*/
|
|
1009
|
+
async function fetchTokenizedVaultSubaccountLoans(pool, chainId, headers) {
|
|
1010
|
+
const key = `subaccount-loans-${chainId}-${pool}`;
|
|
1011
|
+
let loans;
|
|
1012
|
+
const cached = await (0, cache_1.getSafeCache)(key);
|
|
1013
|
+
if (cached) {
|
|
1014
|
+
loans = cached;
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
const tokenizedVaultsResponse = await fetchUpshiftPublic(core_2.WEBSERVER_ENDPOINTS.public.tokenizedVault.subaccountLoans(pool, chainId, true), { headers: headers });
|
|
1018
|
+
loans = (await tokenizedVaultsResponse.json()).loans;
|
|
1019
|
+
// Short TTL: see fetchTokenizedVaultLoans / ACTIVE_LOANS_TTL_MS.
|
|
1020
|
+
if (loans)
|
|
1021
|
+
cache_1.CACHE.set(key, loans, { ttl: ACTIVE_LOANS_TTL_MS });
|
|
1022
|
+
}
|
|
1023
|
+
return loans?.filter((l) => l.state.toLowerCase() === 'active');
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Fetch historical timeseries data for a specific vault.
|
|
1027
|
+
* Returns TVL, APY, PnL, and share price data over time.
|
|
1028
|
+
* @param vaultAddress - Vault contract address
|
|
1029
|
+
* @param nDays - Number of days of historical data to fetch (default 30, min 1)
|
|
1030
|
+
* @param headers - Monitoring headers for request tracking
|
|
1031
|
+
* @returns Historical timeseries data with date keys
|
|
1032
|
+
*/
|
|
1033
|
+
async function fetchVaultHistoricalTimeseries(vaultAddress, nDays, headers) {
|
|
1034
|
+
const key = `vault-historical-timeseries-${vaultAddress}-${nDays}`;
|
|
1035
|
+
const cachedResponse = await (0, cache_1.getSafeCache)(key);
|
|
1036
|
+
if (cachedResponse) {
|
|
1037
|
+
return cachedResponse;
|
|
1038
|
+
}
|
|
1039
|
+
const response = await fetchUpshiftPublic(core_2.WEBSERVER_ENDPOINTS.public.tokenizedVault.historicalTimeseries(vaultAddress, nDays), { headers: headers });
|
|
1040
|
+
if (!response.ok) {
|
|
1041
|
+
throw new Error(`Failed to fetch historical timeseries data: ${response.statusText}`);
|
|
1042
|
+
}
|
|
1043
|
+
const json = (await response.json());
|
|
1044
|
+
cache_1.CACHE.set(key, json, { ttl: 1000 * 60 * 10 }); // Cache for 10 minutes
|
|
1045
|
+
return json;
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Settle a batch of async tasks, allowing partial failure.
|
|
1049
|
+
*
|
|
1050
|
+
* Unlike `Promise.all`, a failing task does not reject the batch: its slot in
|
|
1051
|
+
* the result array is `null`, so results stay index-aligned with the input.
|
|
1052
|
+
*
|
|
1053
|
+
* Retries only apply to **thunks** (`() => Promise<T>`): a thunk is invoked
|
|
1054
|
+
* again on each attempt via {@link withRetry} (exponential backoff from
|
|
1055
|
+
* `baseDelay`, retryable network errors only). A **bare promise** is already
|
|
1056
|
+
* in flight and cannot be re-executed — re-awaiting it just yields the same
|
|
1057
|
+
* rejection — so bare promises are settled exactly once, with no retry and
|
|
1058
|
+
* no backoff sleep. Pass thunks when a retry is actually wanted.
|
|
1059
|
+
*
|
|
1060
|
+
* @param tasks - Promises and/or thunks to settle. Mixed arrays are allowed.
|
|
1061
|
+
* An empty array resolves to `[]`.
|
|
1062
|
+
* @param maxRetries - Maximum retry attempts per **thunk** (ignored for bare
|
|
1063
|
+
* promises). Default `3`.
|
|
1064
|
+
* @param baseDelay - Initial backoff delay in milliseconds for thunk retries
|
|
1065
|
+
* (ignored for bare promises). Default `1000`.
|
|
1066
|
+
* @returns Results in input order; `null` in the slot of any task that
|
|
1067
|
+
* ultimately failed. Never rejects.
|
|
1068
|
+
* @example
|
|
1069
|
+
* ```ts
|
|
1070
|
+
* // Bare promises: settle once, failures become null.
|
|
1071
|
+
* const [a, b] = await promiseSettle([getA(), getB()]);
|
|
1072
|
+
* // Thunks: each failing call is retried up to 3 times with backoff.
|
|
1073
|
+
* const [c, d] = await promiseSettle([() => getC(), () => getD()]);
|
|
1074
|
+
* ```
|
|
1075
|
+
*/
|
|
1076
|
+
async function promiseSettle(tasks, maxRetries = 3, baseDelay = 1000) {
|
|
1077
|
+
const settling = tasks.map((task, index) => {
|
|
1078
|
+
if (typeof task !== 'function') {
|
|
1079
|
+
// Already-created promise: nothing to re-run, so settle it as-is.
|
|
1080
|
+
return task;
|
|
1081
|
+
}
|
|
1082
|
+
return withRetry(async () => {
|
|
1083
|
+
try {
|
|
1084
|
+
return await task();
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
logger_1.Logger.log.warn('promiseSettle', `Task at index ${index} failed`, error);
|
|
1088
|
+
throw error;
|
|
1089
|
+
}
|
|
1090
|
+
}, maxRetries, baseDelay);
|
|
1091
|
+
});
|
|
1092
|
+
const results = await Promise.allSettled(settling);
|
|
1093
|
+
return results.map((result, index) => {
|
|
1094
|
+
if (result.status === 'fulfilled') {
|
|
1095
|
+
return result.value;
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
logger_1.Logger.log.warn('promiseSettle', `Promise at index ${index} failed after retries`, result.reason);
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
//# sourceMappingURL=fetcher.js.map
|