@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,2377 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.safeBigInt = safeBigInt;
|
|
4
|
+
exports.resolveDepositTokenDecimals = resolveDepositTokenDecimals;
|
|
5
|
+
exports.resolveSpender = resolveSpender;
|
|
6
|
+
exports.validateAmountPrecision = validateAmountPrecision;
|
|
7
|
+
exports.isNonceParsing = isNonceParsing;
|
|
8
|
+
exports.safeWaitForTx = safeWaitForTx;
|
|
9
|
+
exports.safeSendTx = safeSendTx;
|
|
10
|
+
exports.tryRecoverTxHash = tryRecoverTxHash;
|
|
11
|
+
exports.getDepositRoute = getDepositRoute;
|
|
12
|
+
exports.vaultApprove = vaultApprove;
|
|
13
|
+
exports.approve = approve;
|
|
14
|
+
exports.vaultDeposit = vaultDeposit;
|
|
15
|
+
exports.vaultRequestRedeem = vaultRequestRedeem;
|
|
16
|
+
exports.vaultRedeem = vaultRedeem;
|
|
17
|
+
exports.claimWithdrawal = claimWithdrawal;
|
|
18
|
+
exports.depositNative = depositNative;
|
|
19
|
+
exports.rwaRedeemAsset = rwaRedeemAsset;
|
|
20
|
+
exports.swapAndDeposit = swapAndDeposit;
|
|
21
|
+
exports.depositViaSwapRouter = depositViaSwapRouter;
|
|
22
|
+
exports.depositNativeViaSwapRouter = depositNativeViaSwapRouter;
|
|
23
|
+
exports.isNativeSwapRouterDepositSupported = isNativeSwapRouterDepositSupported;
|
|
24
|
+
exports.swapRouterDeposit = swapRouterDeposit;
|
|
25
|
+
const ethers_1 = require("ethers");
|
|
26
|
+
const abis_1 = require("../../abis");
|
|
27
|
+
const core_1 = require("../../core");
|
|
28
|
+
const types_1 = require("../../types");
|
|
29
|
+
const swap_quotes_1 = require("../../services/swap-quotes");
|
|
30
|
+
const adapter_helpers_1 = require("./adapter.helpers");
|
|
31
|
+
const utils_1 = require("./utils");
|
|
32
|
+
// Treat unparseable RPC payloads (bare "0x", etc.) as 0n with a warn log —
|
|
33
|
+
// the defensive default is "approve again," and the log surfaces flaky RPCs
|
|
34
|
+
// before they cause silent gas waste from spurious approvals.
|
|
35
|
+
/** @internal */
|
|
36
|
+
function safeBigInt(value, context = 'safeBigInt') {
|
|
37
|
+
if (value === null || value === undefined)
|
|
38
|
+
return 0n;
|
|
39
|
+
const s = String(value);
|
|
40
|
+
if (s === '' || s === '0x') {
|
|
41
|
+
if (s === '0x') {
|
|
42
|
+
core_1.Logger.log.warn(context, 'RPC returned bare "0x"; treating as 0', {
|
|
43
|
+
raw: s,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return 0n;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return BigInt(s);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
core_1.Logger.log.warn(context, 'Could not parse RPC value as BigInt; treating as 0', { raw: s, error: String(err) });
|
|
53
|
+
return 0n;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// EVM-0/1 single-asset underlying deposits reuse vaultDecimals (vault and
|
|
57
|
+
// underlying decimals match under ERC-4626 inheritance), saving one RPC.
|
|
58
|
+
// Multi-asset / adapter / non-underlying paths must read from the deposit
|
|
59
|
+
// asset's own ERC-20 — using share decimals there silently misencodes amounts.
|
|
60
|
+
/** @internal */
|
|
61
|
+
async function resolveDepositTokenDecimals(args) {
|
|
62
|
+
const { actualDepositAsset, underlyingAsset, isNativeToken, isMultiAssetVault, vaultDecimals, readErc20Decimals, } = args;
|
|
63
|
+
if (isNativeToken)
|
|
64
|
+
return 18;
|
|
65
|
+
const isUnderlying = actualDepositAsset.toLowerCase() === underlyingAsset.toLowerCase();
|
|
66
|
+
if (isUnderlying && !isMultiAssetVault)
|
|
67
|
+
return vaultDecimals;
|
|
68
|
+
return readErc20Decimals(actualDepositAsset);
|
|
69
|
+
}
|
|
70
|
+
/** @internal */
|
|
71
|
+
function resolveSpender(args) {
|
|
72
|
+
if (args.isMultiAssetVault)
|
|
73
|
+
return args.target;
|
|
74
|
+
if (args.isAdapterDeposit && args.adapterWrapperAddress)
|
|
75
|
+
return args.adapterWrapperAddress;
|
|
76
|
+
return args.target;
|
|
77
|
+
}
|
|
78
|
+
// Reject JS-number amounts past Number.MAX_SAFE_INTEGER — any larger
|
|
79
|
+
// integer the caller passes is already wrong before we hit toNormalizedBn.
|
|
80
|
+
/** @internal */
|
|
81
|
+
function validateAmountPrecision(amount) {
|
|
82
|
+
if (typeof amount === 'string' || typeof amount === 'bigint')
|
|
83
|
+
return;
|
|
84
|
+
if (!Number.isFinite(amount)) {
|
|
85
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `amount must be finite, got ${amount}`);
|
|
86
|
+
}
|
|
87
|
+
if (amount < 0) {
|
|
88
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `amount must be non-negative, got ${amount}`);
|
|
89
|
+
}
|
|
90
|
+
if (Number.isInteger(amount) && !Number.isSafeInteger(amount)) {
|
|
91
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `amount ${amount} exceeds Number.MAX_SAFE_INTEGER; pass a string or bigint to preserve precision`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Tries tx.wait() first (preserves replacement detection & revert checks), then falls back to
|
|
95
|
+
// provider.waitForTransaction() for RPCs (e.g. Monad) that return malformed pending tx fields.
|
|
96
|
+
const SAFE_WAIT_TIMEOUT_MS = 120_000;
|
|
97
|
+
/** @internal */
|
|
98
|
+
function isNonceParsing(error) {
|
|
99
|
+
const code = error?.code;
|
|
100
|
+
const msg = String(error).toLowerCase();
|
|
101
|
+
return ((code === 'INVALID_ARGUMENT' || code === 'BAD_DATA') &&
|
|
102
|
+
msg.includes('nonce'));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Own-properties stamped onto an error describing a transaction that already
|
|
106
|
+
* reached the network. Prefixed to avoid colliding with the `hash` /
|
|
107
|
+
* `transaction` / `receipt` fields ethers puts on its own errors.
|
|
108
|
+
*
|
|
109
|
+
* These are an internal wire format between the tx-wait helpers and the write
|
|
110
|
+
* paths' catch blocks — consumers read the *sanitized* form
|
|
111
|
+
* (`txHash` / `broadcast` / `confirmationUnknown`) off `UpshiftSDKError.context`,
|
|
112
|
+
* never these.
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
const BROADCAST_HASH_KEY = 'upshiftBroadcastTxHash';
|
|
116
|
+
/** @internal */
|
|
117
|
+
const BROADCAST_UNKNOWN_KEY = 'upshiftConfirmationUnknown';
|
|
118
|
+
/**
|
|
119
|
+
* Stamp "this transaction was broadcast" onto an error, in place.
|
|
120
|
+
*
|
|
121
|
+
* Mutating rather than wrapping is deliberate: callers up the stack compare
|
|
122
|
+
* error identity (and ethers' own typed fields matter for downstream
|
|
123
|
+
* classification), so replacing the error object would lose information.
|
|
124
|
+
*
|
|
125
|
+
* Annotating is strictly best-effort. A frozen or sealed error, or one with a
|
|
126
|
+
* getter-only property of the same name, must never turn a *recoverable
|
|
127
|
+
* transport failure* into a `TypeError` thrown from inside the error handler —
|
|
128
|
+
* that would be a strictly worse outcome than losing the hash. Each write is
|
|
129
|
+
* therefore attempted independently and failures are swallowed; the caller then
|
|
130
|
+
* simply reports the transaction as un-annotated (no `txHash` in the error
|
|
131
|
+
* context), which is the same conservative state as "never broadcast".
|
|
132
|
+
*
|
|
133
|
+
* @param error - The error about to be thrown. Non-objects pass through
|
|
134
|
+
* untouched — there is nowhere to put the marker.
|
|
135
|
+
* @param txHash - Hash of the transaction that reached the network.
|
|
136
|
+
* @param confirmationUnknown - `true` when the transaction is on the wire but
|
|
137
|
+
* its outcome could not be determined (transport retries exhausted, or the
|
|
138
|
+
* wait timed out). `false` when the chain gave a definitive answer, e.g. a
|
|
139
|
+
* receipt with `status === 0`.
|
|
140
|
+
* @returns The same error instance (identity preserved), for
|
|
141
|
+
* `throw markBroadcast(...)`.
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
144
|
+
function markBroadcast(error, txHash, confirmationUnknown) {
|
|
145
|
+
if (!error || typeof error !== 'object')
|
|
146
|
+
return error;
|
|
147
|
+
// `defineProperty` with configurable/writable keeps the marker invisible to
|
|
148
|
+
// JSON.stringify and to spreads of the error, so it can't leak into a
|
|
149
|
+
// serialized crash report as a mystery field.
|
|
150
|
+
const stamp = (key, value) => {
|
|
151
|
+
try {
|
|
152
|
+
Object.defineProperty(error, key, {
|
|
153
|
+
value,
|
|
154
|
+
enumerable: false,
|
|
155
|
+
writable: true,
|
|
156
|
+
configurable: true,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Frozen/sealed error, or a non-configurable same-named property. The
|
|
161
|
+
// hash is a nice-to-have; never let annotation failure mask the real
|
|
162
|
+
// transport error we are in the middle of reporting.
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
stamp(BROADCAST_HASH_KEY, txHash);
|
|
166
|
+
stamp(BROADCAST_UNKNOWN_KEY, confirmationUnknown);
|
|
167
|
+
return error;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Read back the markers {@link markBroadcast} stamped, if any.
|
|
171
|
+
*
|
|
172
|
+
* @param error - The caught value, of unknown type.
|
|
173
|
+
* @returns The recovered hash and confirmation state; both `undefined` when the
|
|
174
|
+
* error never passed through a tx-wait helper.
|
|
175
|
+
* @internal
|
|
176
|
+
*/
|
|
177
|
+
function readBroadcastMarkers(error) {
|
|
178
|
+
if (!error || typeof error !== 'object')
|
|
179
|
+
return {};
|
|
180
|
+
const e = error;
|
|
181
|
+
const txHash = e[BROADCAST_HASH_KEY];
|
|
182
|
+
const confirmationUnknown = e[BROADCAST_UNKNOWN_KEY];
|
|
183
|
+
return {
|
|
184
|
+
txHash: typeof txHash === 'string' ? txHash : undefined,
|
|
185
|
+
confirmationUnknown: typeof confirmationUnknown === 'boolean'
|
|
186
|
+
? confirmationUnknown
|
|
187
|
+
: undefined,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Build the broadcast half of a failed write's structured error context, for a
|
|
192
|
+
* path that holds its main transaction in a local variable.
|
|
193
|
+
*
|
|
194
|
+
* The distinction this encodes, and which consumers branch on:
|
|
195
|
+
*
|
|
196
|
+
* - **no `txHash` key at all** — the write never reached the network. Nothing
|
|
197
|
+
* is pending; it is safe to tell the user it failed and safe to retry.
|
|
198
|
+
* - **`txHash` + `broadcast: true` + `confirmationUnknown: true`** — the
|
|
199
|
+
* transaction *is* on-chain (or in the mempool) and only the confirmation was
|
|
200
|
+
* lost. Render a pending state against `txHash`; **do not** prompt a retry,
|
|
201
|
+
* that is exactly how users double-spend a redeem.
|
|
202
|
+
* - **`txHash` + `broadcast: true` + `confirmationUnknown: false`** — the
|
|
203
|
+
* transaction reached the chain and definitively failed (reverted). A real
|
|
204
|
+
* error, with a hash the user can inspect on an explorer.
|
|
205
|
+
*
|
|
206
|
+
* `tx` is the gate on purpose. Several write paths broadcast an inner approval
|
|
207
|
+
* before the main call; if that approval's wait fails, the caught error carries
|
|
208
|
+
* *its* marker. Falling back to the error's marker here would make
|
|
209
|
+
* "Request redeem failed" report the approve hash and claim the redeem is
|
|
210
|
+
* pending when it was never sent.
|
|
211
|
+
*
|
|
212
|
+
* @param tx - The path's main transaction, or `undefined`/`null` when it was
|
|
213
|
+
* never sent.
|
|
214
|
+
* @param error - The caught value, used only for the confirmation state.
|
|
215
|
+
* @returns A context fragment to spread into the thrown error's `context`.
|
|
216
|
+
* Empty when nothing was broadcast.
|
|
217
|
+
* @internal
|
|
218
|
+
*/
|
|
219
|
+
function localTxBroadcastContext(tx, error) {
|
|
220
|
+
const txHash = tx?.hash;
|
|
221
|
+
if (!txHash)
|
|
222
|
+
return {};
|
|
223
|
+
const markers = readBroadcastMarkers(error);
|
|
224
|
+
return {
|
|
225
|
+
txHash,
|
|
226
|
+
broadcast: true,
|
|
227
|
+
// Only trust the marker when it describes *this* transaction.
|
|
228
|
+
confirmationUnknown: markers.txHash === txHash && markers.confirmationUnknown === true,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Same contract as {@link localTxBroadcastContext}, for write paths whose only
|
|
233
|
+
* transaction is sent through {@link safeSendTx} — there the hash never comes
|
|
234
|
+
* back to the caller on the failure path, so the error's own marker is the only
|
|
235
|
+
* source. Safe precisely because those paths send exactly one transaction, so
|
|
236
|
+
* the marker cannot belong to a different one.
|
|
237
|
+
*
|
|
238
|
+
* @param error - The caught value, of unknown type.
|
|
239
|
+
* @returns A context fragment to spread into the thrown error's `context`.
|
|
240
|
+
* Empty when nothing was broadcast.
|
|
241
|
+
* @internal
|
|
242
|
+
*/
|
|
243
|
+
function errorTxBroadcastContext(error) {
|
|
244
|
+
const { txHash, confirmationUnknown } = readBroadcastMarkers(error);
|
|
245
|
+
if (!txHash)
|
|
246
|
+
return {};
|
|
247
|
+
return {
|
|
248
|
+
txHash,
|
|
249
|
+
broadcast: true,
|
|
250
|
+
confirmationUnknown: confirmationUnknown === true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Await a broadcast transaction's receipt, surviving both malformed-nonce RPCs
|
|
255
|
+
* and transient transport faults while still reporting genuine on-chain
|
|
256
|
+
* failures.
|
|
257
|
+
*
|
|
258
|
+
* `tx.wait()` is tried first because it preserves ethers' replacement-tx
|
|
259
|
+
* detection and revert checks. Two recoverable failure modes are handled:
|
|
260
|
+
*
|
|
261
|
+
* 1. **Malformed nonce** ({@link isNonceParsing}) — Monad-style RPCs return
|
|
262
|
+
* `nonce: "undefined"` on the pending tx and ethers throws while parsing.
|
|
263
|
+
* One direct `provider.waitForTransaction()` recovers it (unchanged
|
|
264
|
+
* behaviour).
|
|
265
|
+
* 2. **Transient transport fault** ({@link isRetryableRpcError}) — the provider
|
|
266
|
+
* hiccups while polling `eth_getTransactionReceipt` and ethers surfaces
|
|
267
|
+
* `could not coalesce error (… "code": -32603 …)`. This used to propagate
|
|
268
|
+
* and make every write path report a **successfully mined** transaction as
|
|
269
|
+
* failed, driving users to retry into `ERC20InsufficientBalance`. The hash
|
|
270
|
+
* is already on the wire and receipt polling is idempotent, so we re-poll
|
|
271
|
+
* with bounded exponential backoff instead of rethrowing.
|
|
272
|
+
*
|
|
273
|
+
* Everything else — notably a real revert — is rethrown untouched.
|
|
274
|
+
*
|
|
275
|
+
* Every error this function throws is stamped by {@link markBroadcast}: by the
|
|
276
|
+
* time `tx.wait()` can fail, the transaction is already on the wire, so the
|
|
277
|
+
* caller must never report it as "never sent". See
|
|
278
|
+
* {@link localTxBroadcastContext} for how that reaches
|
|
279
|
+
* `UpshiftSDKError.context`.
|
|
280
|
+
*
|
|
281
|
+
* @param tx - The broadcast transaction response to await.
|
|
282
|
+
* @returns The mined receipt (never `null` on the fallback paths; `tx.wait()`
|
|
283
|
+
* itself may return `null` when the caller configured 0 confirmations).
|
|
284
|
+
* @throws {Error} `Transaction <hash> reverted on-chain` when the receipt
|
|
285
|
+
* reports `status === 0`. Marked `confirmationUnknown: false` — the chain
|
|
286
|
+
* answered.
|
|
287
|
+
* @throws {Error} `Transaction <hash> was not confirmed within <n>s` when the
|
|
288
|
+
* overall wait times out with no receipt. Marked
|
|
289
|
+
* `confirmationUnknown: true`.
|
|
290
|
+
* @throws The underlying transport error once receipt-poll retries are
|
|
291
|
+
* exhausted, marked `confirmationUnknown: true`.
|
|
292
|
+
* @internal
|
|
293
|
+
*/
|
|
294
|
+
async function safeWaitForTx(tx) {
|
|
295
|
+
try {
|
|
296
|
+
return await tx.wait();
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
const nonceParse = isNonceParsing(error);
|
|
300
|
+
// Not a shape we know how to recover from — surface it unchanged, but the
|
|
301
|
+
// tx is still on-chain (ethers reports a revert here), so keep the hash.
|
|
302
|
+
if (!nonceParse && !(0, core_1.isRetryableRpcError)(error)) {
|
|
303
|
+
throw markBroadcast(error, tx.hash, false);
|
|
304
|
+
}
|
|
305
|
+
const msg = error instanceof Error ? error.message : '';
|
|
306
|
+
core_1.Logger.log.warn(nonceParse
|
|
307
|
+
? 'safeWaitForTx:fallback'
|
|
308
|
+
: 'safeWaitForTx:transport-fallback', msg, { hash: tx.hash });
|
|
309
|
+
// The nonce path keeps its original single-shot behaviour: the RPC is
|
|
310
|
+
// responsive, it just mis-serialized the pending tx. The transport path
|
|
311
|
+
// re-polls, because the provider itself is the thing that failed.
|
|
312
|
+
let receipt;
|
|
313
|
+
try {
|
|
314
|
+
receipt = nonceParse
|
|
315
|
+
? await tx.provider.waitForTransaction(tx.hash, 1, SAFE_WAIT_TIMEOUT_MS)
|
|
316
|
+
: await (0, core_1.retryOnTransientRpc)('safeWaitForTx:transport-retry', () => tx.provider.waitForTransaction(tx.hash, 1, SAFE_WAIT_TIMEOUT_MS), { hash: tx.hash });
|
|
317
|
+
}
|
|
318
|
+
catch (pollError) {
|
|
319
|
+
// Retries exhausted. The transaction is on the wire and very likely
|
|
320
|
+
// mined; we simply could not read its receipt.
|
|
321
|
+
throw markBroadcast(pollError, tx.hash, true);
|
|
322
|
+
}
|
|
323
|
+
if (!receipt) {
|
|
324
|
+
throw markBroadcast(new Error(`Transaction ${tx.hash} was not confirmed within ${SAFE_WAIT_TIMEOUT_MS / 1000}s`), tx.hash, true);
|
|
325
|
+
}
|
|
326
|
+
if (receipt.status === 0) {
|
|
327
|
+
// Definitive: the chain executed it and it failed. Not pending.
|
|
328
|
+
throw markBroadcast(new Error(`Transaction ${tx.hash} reverted on-chain`), tx.hash, false);
|
|
329
|
+
}
|
|
330
|
+
return receipt;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Wraps an ethers contract call that returns a TransactionResponse.
|
|
335
|
+
* Some RPCs (e.g. Monad) return malformed pending tx fields like `nonce: "undefined"`,
|
|
336
|
+
* which causes ethers.js to throw during response parsing before safeWaitForTx runs.
|
|
337
|
+
* This wrapper catches that error, extracts the tx hash from the error context, and
|
|
338
|
+
* falls back to provider.waitForTransaction().
|
|
339
|
+
*/
|
|
340
|
+
/** @internal */
|
|
341
|
+
async function safeSendTx(contractCall, provider, shouldWait) {
|
|
342
|
+
try {
|
|
343
|
+
const tx = await contractCall();
|
|
344
|
+
if (shouldWait)
|
|
345
|
+
await safeWaitForTx(tx);
|
|
346
|
+
return { tx, hash: tx.hash };
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (!isNonceParsing(error))
|
|
350
|
+
throw error;
|
|
351
|
+
// The tx was sent but ethers couldn't parse the response.
|
|
352
|
+
// Try to extract the hash from the error payload.
|
|
353
|
+
const errStr = String(error);
|
|
354
|
+
const hashMatch = errStr.match(/"hash"\s*:\s*"(0x[0-9a-fA-F]{64})"/);
|
|
355
|
+
if (!hashMatch?.[1])
|
|
356
|
+
throw error;
|
|
357
|
+
const txHash = hashMatch[1];
|
|
358
|
+
core_1.Logger.log.warn('safeSendTx:nonce-fallback', { txHash });
|
|
359
|
+
if (shouldWait) {
|
|
360
|
+
const signerProvider = 'provider' in provider ? provider.provider : provider;
|
|
361
|
+
const receipt = await signerProvider.waitForTransaction(txHash, 1, SAFE_WAIT_TIMEOUT_MS);
|
|
362
|
+
if (!receipt) {
|
|
363
|
+
throw markBroadcast(new Error(`Transaction ${txHash} was not confirmed within ${SAFE_WAIT_TIMEOUT_MS / 1000}s`), txHash, true);
|
|
364
|
+
}
|
|
365
|
+
if (receipt.status === 0) {
|
|
366
|
+
throw markBroadcast(new Error(`Transaction ${txHash} reverted on-chain`), txHash, false);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return { tx: null, hash: txHash };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Shared nonce-recovery helper for catch blocks. When an RPC returns a
|
|
374
|
+
* malformed nonce and ethers throws, try to extract the tx hash from the
|
|
375
|
+
* error payload. Optionally waits for confirmation (with status check).
|
|
376
|
+
*
|
|
377
|
+
* Returns the recovered hash, or null if this error is not a nonce-parse error
|
|
378
|
+
* or if no hash could be extracted (caller should re-throw the original error).
|
|
379
|
+
*/
|
|
380
|
+
/** @internal */
|
|
381
|
+
async function tryRecoverTxHash(error, signer, wait) {
|
|
382
|
+
if (!isNonceParsing(error))
|
|
383
|
+
return null;
|
|
384
|
+
const hashMatch = String(error).match(/"hash"\s*:\s*"(0x[0-9a-fA-F]{64})"/);
|
|
385
|
+
if (!hashMatch?.[1])
|
|
386
|
+
return null;
|
|
387
|
+
const txHash = hashMatch[1];
|
|
388
|
+
if (wait) {
|
|
389
|
+
const provider = 'provider' in signer ? signer.provider : signer;
|
|
390
|
+
const receipt = await provider.waitForTransaction(txHash, 1, SAFE_WAIT_TIMEOUT_MS);
|
|
391
|
+
if (!receipt) {
|
|
392
|
+
throw markBroadcast(new Error(`Transaction ${txHash} was not confirmed within ${SAFE_WAIT_TIMEOUT_MS / 1000}s`), txHash, true);
|
|
393
|
+
}
|
|
394
|
+
if (receipt.status === 0) {
|
|
395
|
+
throw markBroadcast(new Error(`Transaction ${txHash} reverted on-chain`), txHash, false);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return txHash;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Rethrow a caught write-path error as a typed {@link UpshiftValidationError}
|
|
402
|
+
* when it is the chain node reporting the sender can't afford the transaction's
|
|
403
|
+
* gas/fee (see {@link isInsufficientFundsError}).
|
|
404
|
+
*
|
|
405
|
+
* Why: for an underfunded account ethers surfaces an opaque `CALL_EXCEPTION` /
|
|
406
|
+
* "missing revert data" that gives the caller no stable way to tell "user needs
|
|
407
|
+
* gas" apart from a genuine on-chain failure. Converting it to the
|
|
408
|
+
* `ACCOUNT_NOT_FUNDED` code lets a consuming UI branch on `err.code` (e.g. show
|
|
409
|
+
* a "top up to cover network fees" prompt) instead of string-matching. Every
|
|
410
|
+
* write path shares this so the classification is identical across
|
|
411
|
+
* deposit/redeem/approve rather than drifting per call site.
|
|
412
|
+
*
|
|
413
|
+
* A no-op when `isInsufficient` is `false`, so callers fall through to their
|
|
414
|
+
* existing handling unchanged. The caller passes the already-computed
|
|
415
|
+
* classification (it also feeds the telemetry-demotion decision) so
|
|
416
|
+
* {@link isInsufficientFundsError} runs once per catch, not twice.
|
|
417
|
+
*
|
|
418
|
+
* @param isInsufficient - Result of {@link isInsufficientFundsError} for
|
|
419
|
+
* `error`, computed once by the caller and shared with the benign-log flag.
|
|
420
|
+
* @param operation - Human-readable label for the attempted write (e.g.
|
|
421
|
+
* `'Request redeem'`), used verbatim in the thrown error's message.
|
|
422
|
+
* @param error - The caught value, of unknown type, preserved as the thrown
|
|
423
|
+
* error's `cause`.
|
|
424
|
+
* @param context - Structured context attached to the thrown error for logging.
|
|
425
|
+
* @throws {UpshiftValidationError} with code `ACCOUNT_NOT_FUNDED` when
|
|
426
|
+
* `isInsufficient` is `true`.
|
|
427
|
+
*/
|
|
428
|
+
function throwIfInsufficientFunds(isInsufficient, operation, error, context) {
|
|
429
|
+
if (!isInsufficient)
|
|
430
|
+
return;
|
|
431
|
+
throw new core_1.UpshiftValidationError('ACCOUNT_NOT_FUNDED', `${operation} failed: insufficient native balance to cover gas/network fees`, { cause: error, context });
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Shared catch path for the SwapRouter write methods, with the same recovery
|
|
435
|
+
* and classification as `vaultDeposit`: recover a nonce-parse hash, pass typed
|
|
436
|
+
* SDK errors through, log (demoting user rejections and unfunded accounts),
|
|
437
|
+
* map insufficient funds to `ACCOUNT_NOT_FUNDED`, and wrap the rest.
|
|
438
|
+
* @internal
|
|
439
|
+
*/
|
|
440
|
+
async function handleSwapRouterWriteError(e, tx, signer, wait, operation, label, context) {
|
|
441
|
+
const recovered = await tryRecoverTxHash(e, signer, wait);
|
|
442
|
+
if (recovered) {
|
|
443
|
+
core_1.Logger.log.warn(`${operation}:nonce-fallback`, { hash: recovered });
|
|
444
|
+
return recovered;
|
|
445
|
+
}
|
|
446
|
+
if (e instanceof core_1.UpshiftSDKError)
|
|
447
|
+
throw e;
|
|
448
|
+
const ctx = { ...context, ...localTxBroadcastContext(tx, e) };
|
|
449
|
+
const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
|
|
450
|
+
(0, core_1.logChainError)(operation, e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, ctx);
|
|
451
|
+
throwIfInsufficientFunds(insufficientFunds, label, e, ctx);
|
|
452
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `${label} failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: ctx });
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* The router vault type for an EVM vault: a multi-asset vault (`evm-2`) is a
|
|
456
|
+
* Tokenized Vault V2, any other EVM vault takes the ERC-4626
|
|
457
|
+
* `deposit(uint256, address)` interface.
|
|
458
|
+
* @internal
|
|
459
|
+
*/
|
|
460
|
+
function toSwapRouterVaultType(isMultiAssetVault) {
|
|
461
|
+
return isMultiAssetVault
|
|
462
|
+
? types_1.SwapRouterVaultType.TokenizedVaultV2
|
|
463
|
+
: types_1.SwapRouterVaultType.ERC4626;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Whether a vault takes `asset` as-is: its reference asset (`asset()`), or,
|
|
467
|
+
* for a multi-asset Tokenized Vault V2, an asset on the vault's assets
|
|
468
|
+
* whitelist. Shared by the deposit-route resolver and the `swapRouterDeposit`
|
|
469
|
+
* dispatch so neither swaps an asset the vault accepts.
|
|
470
|
+
* @internal
|
|
471
|
+
*/
|
|
472
|
+
async function isAcceptedDirectly(signer, target, asset, underlyingAsset, isMultiAssetVault) {
|
|
473
|
+
const lower = asset.toLowerCase();
|
|
474
|
+
return (lower === underlyingAsset.toLowerCase() ||
|
|
475
|
+
(isMultiAssetVault &&
|
|
476
|
+
(await (0, core_1.getWhitelistedAssets)(signer, await (0, core_1.createContract)({
|
|
477
|
+
address: target,
|
|
478
|
+
provider: signer,
|
|
479
|
+
abi: abis_1.ABI_TOKENIZED_VAULT_V2,
|
|
480
|
+
}).assetsWhitelistAddress())).some((a) => a.toLowerCase() === lower)));
|
|
481
|
+
}
|
|
482
|
+
/** @internal */
|
|
483
|
+
async function resolveDepositRoute(signer, args) {
|
|
484
|
+
const { target, chainId, actualDepositAsset, isMultiAssetVault } = args;
|
|
485
|
+
const asset = actualDepositAsset.toLowerCase();
|
|
486
|
+
const isNativeToken = actualDepositAsset === ethers_1.ZeroAddress;
|
|
487
|
+
const isAdapterDeposit = asset !== args.underlyingAsset.toLowerCase();
|
|
488
|
+
const wrapper = (0, core_1.getVaultAdapterConfig)(target)?.wrapperAddress;
|
|
489
|
+
const native = (reason) => ({
|
|
490
|
+
via: 'native',
|
|
491
|
+
spender: isNativeToken
|
|
492
|
+
? null
|
|
493
|
+
: resolveSpender({
|
|
494
|
+
target,
|
|
495
|
+
isMultiAssetVault,
|
|
496
|
+
isAdapterDeposit,
|
|
497
|
+
adapterWrapperAddress: wrapper,
|
|
498
|
+
}),
|
|
499
|
+
reason,
|
|
500
|
+
});
|
|
501
|
+
const router = chainId ? (0, core_1.getSwapRouterAddress)(chainId) : undefined;
|
|
502
|
+
if (!chainId || !router)
|
|
503
|
+
return native('no-router');
|
|
504
|
+
if (args.isDepositWithPermit)
|
|
505
|
+
return native('permit');
|
|
506
|
+
if (isNativeToken)
|
|
507
|
+
return native('native-token');
|
|
508
|
+
if (isAdapterDeposit && !isMultiAssetVault && wrapper) {
|
|
509
|
+
return native('adapter');
|
|
510
|
+
}
|
|
511
|
+
if (args.vaultVersion === 'evm-0')
|
|
512
|
+
return native('legacy-pool');
|
|
513
|
+
if (!(await isAcceptedDirectly(signer, target, actualDepositAsset, args.underlyingAsset, isMultiAssetVault))) {
|
|
514
|
+
return native('asset-not-accepted');
|
|
515
|
+
}
|
|
516
|
+
if (isMultiAssetVault &&
|
|
517
|
+
(await (0, core_1.readVaultHasSendersWhitelist)(signer, chainId, target))) {
|
|
518
|
+
return native('kyc-vault');
|
|
519
|
+
}
|
|
520
|
+
// After the cached gates, so only a deposit that would otherwise route pays
|
|
521
|
+
// the short-TTL read; before the origin check, so a paused router falls back
|
|
522
|
+
// to native instead of throwing under strictOrigin.
|
|
523
|
+
if (await (0, core_1.readSwapRouterPaused)(signer, chainId)) {
|
|
524
|
+
return native('router-paused');
|
|
525
|
+
}
|
|
526
|
+
const route = {
|
|
527
|
+
via: 'router',
|
|
528
|
+
spender: router,
|
|
529
|
+
originCode: (0, core_1.resolveOriginCode)(undefined, chainId),
|
|
530
|
+
vaultType: toSwapRouterVaultType(isMultiAssetVault),
|
|
531
|
+
};
|
|
532
|
+
// The router reverts `InvalidOrigin` on a non-zero code that is not
|
|
533
|
+
// registered, so an unregistered instance code is replaced by the zero code
|
|
534
|
+
// and the deposit still routes.
|
|
535
|
+
if (route.originCode !== core_1.ORIGIN_CODES.default &&
|
|
536
|
+
!(await (0, core_1.readOriginInfo)(chainId, route.originCode, () => signer)).registered) {
|
|
537
|
+
if ((0, core_1.isStrictOrigin)()) {
|
|
538
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `attribution.originCode is not registered on the SwapRouter for chain ${chainId} and attribution.strictOrigin is set. Register it with addOrigin, or unset strictOrigin to deposit with the zero origin code.`, { context: { chainId, target } });
|
|
539
|
+
}
|
|
540
|
+
route.originCode = core_1.ORIGIN_CODES.default;
|
|
541
|
+
route.reason = 'origin-not-registered';
|
|
542
|
+
}
|
|
543
|
+
return route;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Resolve whether a deposit goes through the SwapRouter (`router`) or takes
|
|
547
|
+
* the vault's native path (`native`), and which address to approve.
|
|
548
|
+
* Read-only. `vaultDeposit` and `approve` follow this route, so integrators
|
|
549
|
+
* that manage approvals themselves must approve `route.spender`, not the
|
|
550
|
+
* vault.
|
|
551
|
+
*
|
|
552
|
+
* Since SwapRouter v2 a normal deposit routes whether or not an origin code is
|
|
553
|
+
* configured: the route is `router` when the chain has a SwapRouter, the
|
|
554
|
+
* vault accepts the asset directly (its reference asset, or a Tokenized
|
|
555
|
+
* Vault V2 whitelisted asset — deposited as-is, never swapped), the vault has
|
|
556
|
+
* no senders whitelist (KYC), the router is not paused, and the deposit is not
|
|
557
|
+
* a permit, native-token, adapter or legacy lending-pool deposit. Otherwise it
|
|
558
|
+
* is `native` with a `reason`. The router's token whitelist does not apply: it
|
|
559
|
+
* only gates swaps.
|
|
560
|
+
*
|
|
561
|
+
* The router route carries the instance `attribution.originCode` when it is
|
|
562
|
+
* registered, else the zero code (with `reason: 'origin-not-registered'` when
|
|
563
|
+
* a configured code was dropped). A paused router gives `router-paused` even
|
|
564
|
+
* under `attribution.strictOrigin`.
|
|
565
|
+
*
|
|
566
|
+
* @param signer - ethers `Signer` / `Wallet` used for the reads.
|
|
567
|
+
* @param options - {@link IGetDepositRouteOptions}; `depositAsset` defaults
|
|
568
|
+
* to the vault's underlying.
|
|
569
|
+
* @returns {@link IDepositRoute}.
|
|
570
|
+
* @throws UpshiftValidationError on an invalid `target`, or an unregistered
|
|
571
|
+
* code under `attribution.strictOrigin` while the router is not paused.
|
|
572
|
+
* @throws UpshiftSDKError when a backend, vault or router read fails.
|
|
573
|
+
* @worstCaseRpcCalls 7 — the backend vault row, `asset()`, the vault's
|
|
574
|
+
* `assetsWhitelistAddress()` and `getWhitelistedAssets()` (Tokenized Vault
|
|
575
|
+
* V2 non-reference asset only), its `sendersWhitelistAddress()` (Tokenized
|
|
576
|
+
* Vault V2 only), the router's `isPaused()` and `origins` (non-zero code
|
|
577
|
+
* only). Every read except `asset()` is cached for 5 minutes, `isPaused()`
|
|
578
|
+
* for 15 seconds. No router read on a chain without a SwapRouter.
|
|
579
|
+
* @example
|
|
580
|
+
* ```ts
|
|
581
|
+
* const route = await upshiftSdk.evm.getDepositRoute({ target: vault, depositAsset: usdt });
|
|
582
|
+
* if (route.spender) await approveToken(usdt, route.spender, amount);
|
|
583
|
+
* ```
|
|
584
|
+
*/
|
|
585
|
+
async function getDepositRoute(signer, options) {
|
|
586
|
+
const { target, depositAsset } = options;
|
|
587
|
+
if (!(0, core_1.checkAddress)(target, console)) {
|
|
588
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', 'getDepositRoute: invalid target address');
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(target, undefined, false, false))?.[0];
|
|
592
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
593
|
+
const isMultiAssetVault = vaultVersion === 'evm-2';
|
|
594
|
+
const underlyingAsset = (await (0, core_1.createContract)({
|
|
595
|
+
address: target,
|
|
596
|
+
provider: signer,
|
|
597
|
+
abi: isMultiAssetVault ? abis_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOLS,
|
|
598
|
+
}).asset());
|
|
599
|
+
return await resolveDepositRoute(signer, {
|
|
600
|
+
target,
|
|
601
|
+
chainId: tokenizedVault?.chain ?? options.chainId,
|
|
602
|
+
actualDepositAsset: depositAsset || underlyingAsset,
|
|
603
|
+
underlyingAsset,
|
|
604
|
+
isMultiAssetVault,
|
|
605
|
+
vaultVersion,
|
|
606
|
+
isDepositWithPermit: options.isDepositWithPermit,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
catch (e) {
|
|
610
|
+
if (e instanceof core_1.UpshiftSDKError)
|
|
611
|
+
throw e;
|
|
612
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `getDepositRoute failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, chainId: options.chainId } });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Report whether a deposit carried the instance origin code: the bounded
|
|
617
|
+
* `sdk.attribution.origin` tag (`applied` | `skipped` | `none`), the code only
|
|
618
|
+
* as a span attribute, and — when a configured code was not applied — a
|
|
619
|
+
* warning with the resolver's reason, which the logger also forwards to
|
|
620
|
+
* Sentry as a breadcrumb.
|
|
621
|
+
* @internal
|
|
622
|
+
*/
|
|
623
|
+
function trackOriginAttribution(route, context, operation = 'vaultDeposit') {
|
|
624
|
+
// A router route can carry the zero code (no code configured, or an
|
|
625
|
+
// unregistered one replaced), so attribution is `applied` only when a real
|
|
626
|
+
// code was sent, and `skipped` only when a configured code was not.
|
|
627
|
+
const configured = (0, core_1.resolveOriginCode)(undefined, context.chainId) !== core_1.ORIGIN_CODES.default;
|
|
628
|
+
const status = route.via === 'router' &&
|
|
629
|
+
route.originCode !== undefined &&
|
|
630
|
+
route.originCode !== core_1.ORIGIN_CODES.default
|
|
631
|
+
? 'applied'
|
|
632
|
+
: configured
|
|
633
|
+
? 'skipped'
|
|
634
|
+
: 'none';
|
|
635
|
+
if (status === 'skipped') {
|
|
636
|
+
core_1.Logger.log.warn(`${operation}:attribution-skipped`, {
|
|
637
|
+
reason: route.reason,
|
|
638
|
+
...context,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (!(0, core_1.isAnalyticsEnabled)())
|
|
642
|
+
return;
|
|
643
|
+
const sentry = (0, core_1.getSentrySDK)();
|
|
644
|
+
sentry?.setTag('sdk.attribution.origin', status);
|
|
645
|
+
if (status === 'applied') {
|
|
646
|
+
sentry
|
|
647
|
+
?.getActiveSpan()
|
|
648
|
+
?.setAttribute('sdk.attribution.origin_code', route.originCode);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async function approveCore(signer, options) {
|
|
652
|
+
const { wallet, target, wait, amount, depositAsset } = options;
|
|
653
|
+
const [goodWallet, goodPool] = [
|
|
654
|
+
(0, core_1.checkAddress)(wallet, console, 'wallet'),
|
|
655
|
+
(0, core_1.checkAddress)(target, console),
|
|
656
|
+
];
|
|
657
|
+
if (!goodWallet || !goodPool) {
|
|
658
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `vaultApprove: invalid ${!goodWallet ? 'wallet' : 'target'} address`);
|
|
659
|
+
}
|
|
660
|
+
if (amount === undefined || amount === null) {
|
|
661
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'vaultApprove: amount is required');
|
|
662
|
+
}
|
|
663
|
+
validateAmountPrecision(amount);
|
|
664
|
+
try {
|
|
665
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(target, undefined, false, false))?.[0];
|
|
666
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
667
|
+
const isMultiAssetVault = vaultVersion === 'evm-2';
|
|
668
|
+
const poolContract = isMultiAssetVault
|
|
669
|
+
? (0, core_1.createContract)({
|
|
670
|
+
address: target,
|
|
671
|
+
provider: signer,
|
|
672
|
+
abi: abis_1.ABI_TOKENIZED_VAULT_V2,
|
|
673
|
+
})
|
|
674
|
+
: (0, core_1.createContract)({
|
|
675
|
+
address: target,
|
|
676
|
+
provider: signer,
|
|
677
|
+
abi: abis_1.ABI_LENDING_POOLS,
|
|
678
|
+
});
|
|
679
|
+
let underlyingAsset;
|
|
680
|
+
let vaultDecimals;
|
|
681
|
+
if (isMultiAssetVault) {
|
|
682
|
+
const [asset, receiptTokenAddr] = await Promise.all([
|
|
683
|
+
poolContract.asset(),
|
|
684
|
+
// Retried on transport blips only; a misroute still throws. See
|
|
685
|
+
// getReceiptTokenAddressOrThrow.
|
|
686
|
+
(0, core_1.getReceiptTokenAddressOrThrow)(signer, target, 'vaultApprove:receiptToken'),
|
|
687
|
+
]);
|
|
688
|
+
underlyingAsset = asset;
|
|
689
|
+
vaultDecimals = await (0, core_1.getDecimalsOrThrow)(signer, receiptTokenAddr, 'vaultApprove:receiptDecimals');
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
const [asset, rawDecimals] = await Promise.all([
|
|
693
|
+
poolContract.asset(),
|
|
694
|
+
(0, core_1.getDecimalsOrThrow)(signer, target, 'vaultApprove:poolDecimals'),
|
|
695
|
+
]);
|
|
696
|
+
underlyingAsset = asset;
|
|
697
|
+
vaultDecimals = rawDecimals;
|
|
698
|
+
}
|
|
699
|
+
const actualDepositAsset = (depositAsset || underlyingAsset);
|
|
700
|
+
const isNativeToken = actualDepositAsset === ethers_1.ZeroAddress ||
|
|
701
|
+
actualDepositAsset === '0x0000000000000000000000000000000000000000';
|
|
702
|
+
// Native tokens travel as msg.value — no ERC-20 allowance to grant.
|
|
703
|
+
if (isNativeToken)
|
|
704
|
+
return { kind: 'native' };
|
|
705
|
+
const spenderAddress = (await resolveDepositRoute(signer, {
|
|
706
|
+
target,
|
|
707
|
+
chainId: tokenizedVault?.chain ?? options.chainId,
|
|
708
|
+
actualDepositAsset,
|
|
709
|
+
underlyingAsset,
|
|
710
|
+
isMultiAssetVault,
|
|
711
|
+
vaultVersion,
|
|
712
|
+
isDepositWithPermit: options.isDepositWithPermit,
|
|
713
|
+
})).spender;
|
|
714
|
+
const depositTokenDecimals = await resolveDepositTokenDecimals({
|
|
715
|
+
actualDepositAsset,
|
|
716
|
+
underlyingAsset,
|
|
717
|
+
isNativeToken: false,
|
|
718
|
+
isMultiAssetVault,
|
|
719
|
+
vaultDecimals,
|
|
720
|
+
readErc20Decimals: async (addr) => (0, core_1.getDecimalsOrThrow)(signer, addr, 'vaultApprove:depositTokenDecimals'),
|
|
721
|
+
});
|
|
722
|
+
const normalizedAmt = (0, core_1.toNormalizedBn)(amount, depositTokenDecimals);
|
|
723
|
+
const needed = BigInt(normalizedAmt.raw);
|
|
724
|
+
const tokenContract = (0, core_1.createContract)({
|
|
725
|
+
address: actualDepositAsset,
|
|
726
|
+
abi: abis_1.ABI_ERC20,
|
|
727
|
+
provider: signer,
|
|
728
|
+
});
|
|
729
|
+
const currentAllowance = await tokenContract.allowance(wallet, spenderAddress);
|
|
730
|
+
const existing = safeBigInt(currentAllowance, 'vaultApprove:allowance');
|
|
731
|
+
if (existing >= needed) {
|
|
732
|
+
core_1.Logger.log.info('approve:allowance-sufficient', {
|
|
733
|
+
target,
|
|
734
|
+
spender: spenderAddress,
|
|
735
|
+
amount,
|
|
736
|
+
});
|
|
737
|
+
return { kind: 'sufficient', existing };
|
|
738
|
+
}
|
|
739
|
+
const { hash: approvalHash } = await safeSendTx(() => tokenContract
|
|
740
|
+
.connect(signer)
|
|
741
|
+
.approve(spenderAddress, normalizedAmt.raw), signer, wait);
|
|
742
|
+
core_1.Logger.log.info('approve:tx_hash', approvalHash);
|
|
743
|
+
return { kind: 'sent', hash: approvalHash };
|
|
744
|
+
}
|
|
745
|
+
catch (e) {
|
|
746
|
+
if (e instanceof core_1.UpshiftSDKError)
|
|
747
|
+
throw e;
|
|
748
|
+
// Only one tx is sent on this path, so any broadcast marker on the error
|
|
749
|
+
// necessarily describes the approval itself.
|
|
750
|
+
const broadcast = errorTxBroadcastContext(e);
|
|
751
|
+
// A user declining the approval in their wallet is product behaviour, not
|
|
752
|
+
// an SDK fault — demote it to a breadcrumb so it doesn't bill as an issue.
|
|
753
|
+
const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
|
|
754
|
+
(0, core_1.logChainError)('vaultApprove', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
|
|
755
|
+
target,
|
|
756
|
+
amount,
|
|
757
|
+
...broadcast,
|
|
758
|
+
});
|
|
759
|
+
throwIfInsufficientFunds(insufficientFunds, 'Approval', e, {
|
|
760
|
+
target,
|
|
761
|
+
amount,
|
|
762
|
+
...broadcast,
|
|
763
|
+
});
|
|
764
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `Approval failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, amount, ...broadcast } });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Approve a vault (or the appropriate adapter wrapper) to spend a token
|
|
769
|
+
* ahead of a separate deposit call. Spender routing mirrors
|
|
770
|
+
* {@link vaultDeposit} (multi-asset → vault, adapter → wrapper, standard →
|
|
771
|
+
* vault, SwapRouter when {@link getDepositRoute} routes there). Returns
|
|
772
|
+
* `undefined` when the existing allowance already covers `amount` **or**
|
|
773
|
+
* when the deposit asset is native.
|
|
774
|
+
*
|
|
775
|
+
* @returns Transaction hash when an approval was sent, `undefined` when
|
|
776
|
+
* the existing allowance already covers `amount` or the deposit asset is
|
|
777
|
+
* native (no ERC-20 allowance applies). Use {@link approve} for a
|
|
778
|
+
* discriminated `ApproveResult` if you need to tell those two cases apart.
|
|
779
|
+
* @throws UpshiftValidationError on invalid wallet/target/amount.
|
|
780
|
+
* @throws UpshiftValidationError when `attribution.strictOrigin` is set and the
|
|
781
|
+
* configured origin code is not registered on the SwapRouter (the spender
|
|
782
|
+
* follows {@link getDepositRoute}).
|
|
783
|
+
*
|
|
784
|
+
* @example
|
|
785
|
+
* ```ts
|
|
786
|
+
* const hash = await upshiftSdk.evm.vaultApprove({
|
|
787
|
+
* target: '0x...', // vault contract address
|
|
788
|
+
* wallet: walletAddress,
|
|
789
|
+
* amount: '100', // 100 of the deposit token (UI units)
|
|
790
|
+
* wait: true, // wait for the approval to confirm
|
|
791
|
+
* });
|
|
792
|
+
* if (hash) console.log('approve tx', hash);
|
|
793
|
+
* // hash === undefined means the on-chain allowance already covers the
|
|
794
|
+
* // amount, or the deposit asset is native (msg.value, no allowance).
|
|
795
|
+
* ```
|
|
796
|
+
*/
|
|
797
|
+
async function vaultApprove(signer, options) {
|
|
798
|
+
const result = await approveCore(signer, options);
|
|
799
|
+
return result.kind === 'sent' ? result.hash : undefined;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Same approval logic as {@link vaultApprove} but returns a discriminated
|
|
803
|
+
* union so callers can tell `sent` apart from `sufficient` and `native`
|
|
804
|
+
* without re-reading on-chain state. Preferred shape for new integrations.
|
|
805
|
+
*
|
|
806
|
+
* @example
|
|
807
|
+
* ```ts
|
|
808
|
+
* const r = await upshiftSdk.evm.approve({
|
|
809
|
+
* target: vaultAddress,
|
|
810
|
+
* wallet: walletAddress,
|
|
811
|
+
* amount: '100',
|
|
812
|
+
* wait: true,
|
|
813
|
+
* });
|
|
814
|
+
* switch (r.kind) {
|
|
815
|
+
* case 'sent':
|
|
816
|
+
* showToast('Approval submitted', r.hash);
|
|
817
|
+
* break;
|
|
818
|
+
* case 'sufficient':
|
|
819
|
+
* // r.existing is the existing on-chain allowance (raw bigint)
|
|
820
|
+
* break;
|
|
821
|
+
* case 'native':
|
|
822
|
+
* // No allowance needed; native token will travel as msg.value
|
|
823
|
+
* break;
|
|
824
|
+
* }
|
|
825
|
+
* ```
|
|
826
|
+
*
|
|
827
|
+
* @throws UpshiftValidationError on invalid wallet/target/amount.
|
|
828
|
+
* @throws UpshiftValidationError when `attribution.strictOrigin` is set and the
|
|
829
|
+
* configured origin code is not registered on the SwapRouter (the spender
|
|
830
|
+
* follows {@link getDepositRoute}).
|
|
831
|
+
* @throws UpshiftSDKError when the approval submission fails.
|
|
832
|
+
*/
|
|
833
|
+
async function approve(signer, options) {
|
|
834
|
+
return approveCore(signer, options);
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Deposit underlying token into the specified vault. Auto-routes to the
|
|
838
|
+
* correct contract path (single-asset ERC-4626, EVM-2 multi-asset,
|
|
839
|
+
* Paraswap adapter, native-token wrapper, depositWithPermit) based on the
|
|
840
|
+
* vault version and `depositAsset`.
|
|
841
|
+
*
|
|
842
|
+
* A deposit the SwapRouter can take directly goes through `SwapRouter.deposit`
|
|
843
|
+
* with or without an origin code, and the router is approved instead of the
|
|
844
|
+
* vault — see {@link getDepositRoute} for the rules and the fallback reasons.
|
|
845
|
+
* It carries the instance `attribution.originCode` when that code is
|
|
846
|
+
* registered, else the zero code.
|
|
847
|
+
*
|
|
848
|
+
* Amounts are encoded against the **deposit token's** decimals, not the
|
|
849
|
+
* vault's share decimals. The SDK reads the appropriate decimals via the
|
|
850
|
+
* vault metadata and the asset's ERC-20.
|
|
851
|
+
*
|
|
852
|
+
* Approval is handled automatically: if the existing allowance doesn't
|
|
853
|
+
* cover `amount`, an `approve` is sent and **always waited for** (regardless
|
|
854
|
+
* of the caller's `wait` flag) so the deposit tx can't be re-ordered ahead
|
|
855
|
+
* of the approval on Monad-style RPCs.
|
|
856
|
+
*
|
|
857
|
+
* @param signer ethers Signer / Wallet (or viem WalletClient via {@link setSigner})
|
|
858
|
+
* @param options Vault address, wallet, amount, optional `depositAsset` for
|
|
859
|
+
* adapter routes, optional `wait` to wait for the deposit receipt.
|
|
860
|
+
* @returns Deposit transaction hash.
|
|
861
|
+
* @throws UpshiftValidationError on invalid wallet/target/amount or missing
|
|
862
|
+
* adapter config when `depositAsset` differs from the vault underlying.
|
|
863
|
+
* @throws UpshiftValidationError (`INVALID_INPUT`) when the vault's adapter is
|
|
864
|
+
* the Paraswap bridge (`bridgeId === 2`): that route is not supported here
|
|
865
|
+
* and is refused before any approval or deposit transaction is sent — use
|
|
866
|
+
* {@link swapRouterDeposit} or deposit the underlying asset directly.
|
|
867
|
+
* @throws UpshiftValidationError when `attribution.strictOrigin` is set and the
|
|
868
|
+
* configured origin code is not registered on the SwapRouter.
|
|
869
|
+
* @throws UpshiftSDKError when the deposit submission or on-chain call fails.
|
|
870
|
+
*
|
|
871
|
+
* @example
|
|
872
|
+
* ```ts
|
|
873
|
+
* const hash = await upshiftSdk.evm.vaultDeposit({
|
|
874
|
+
* target: '0xE9B725010A9E419412ed67d0fA5f3A5f40159D32', // vault
|
|
875
|
+
* wallet: walletAddress,
|
|
876
|
+
* amount: '100', // 100 USDC (UI units)
|
|
877
|
+
* wait: true, // wait for the deposit receipt
|
|
878
|
+
* });
|
|
879
|
+
* ```
|
|
880
|
+
*
|
|
881
|
+
* @example Adapter deposit (different deposit asset than the vault's underlying)
|
|
882
|
+
* ```ts
|
|
883
|
+
* await upshiftSdk.evm.vaultDeposit({
|
|
884
|
+
* target: vaultAddress,
|
|
885
|
+
* wallet: walletAddress,
|
|
886
|
+
* amount: '0.5',
|
|
887
|
+
* depositAsset: WETH_ADDRESS, // swap WETH -> vault underlying via the adapter
|
|
888
|
+
* chainId: 1,
|
|
889
|
+
* });
|
|
890
|
+
* ```
|
|
891
|
+
*/
|
|
892
|
+
async function vaultDeposit(signer, options) {
|
|
893
|
+
const { wallet, target, wait, amount, depositAsset, chainId, poolName } = options;
|
|
894
|
+
const [goodWallet, goodPool] = [
|
|
895
|
+
(0, core_1.checkAddress)(wallet, console, 'wallet'),
|
|
896
|
+
(0, core_1.checkAddress)(target, console),
|
|
897
|
+
];
|
|
898
|
+
if (!goodWallet || !goodPool) {
|
|
899
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `vaultDeposit: invalid ${!goodWallet ? 'wallet' : 'target'} address`);
|
|
900
|
+
}
|
|
901
|
+
if (amount === undefined || amount === null) {
|
|
902
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'vaultDeposit: amount is required');
|
|
903
|
+
}
|
|
904
|
+
validateAmountPrecision(amount);
|
|
905
|
+
// Hoisted out of the try so the catch can tell "the deposit was broadcast and
|
|
906
|
+
// we lost the confirmation" from "the deposit never left". Any inner approval
|
|
907
|
+
// tx is deliberately NOT tracked here — see localTxBroadcastContext.
|
|
908
|
+
let depositTx;
|
|
909
|
+
try {
|
|
910
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(target, undefined, false, false))?.[0];
|
|
911
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
912
|
+
const adapterConfig = (0, core_1.getVaultAdapterConfig)(target);
|
|
913
|
+
let poolContract;
|
|
914
|
+
let underlyingAsset;
|
|
915
|
+
let vaultDecimals;
|
|
916
|
+
if (vaultVersion === 'evm-2') {
|
|
917
|
+
poolContract = (0, core_1.createContract)({
|
|
918
|
+
address: target,
|
|
919
|
+
provider: signer,
|
|
920
|
+
abi: options?.isDepositWithPermit
|
|
921
|
+
? abis_1.ABI_TOKENIZED_VAULT_V2_DEPOSIT_WITH_PERMIT
|
|
922
|
+
: abis_1.ABI_TOKENIZED_VAULT_V2,
|
|
923
|
+
});
|
|
924
|
+
const [asset, receiptTokenAddr] = await Promise.all([
|
|
925
|
+
poolContract.asset(),
|
|
926
|
+
// Retried on transport blips only; a misroute still throws. See
|
|
927
|
+
// getReceiptTokenAddressOrThrow.
|
|
928
|
+
(0, core_1.getReceiptTokenAddressOrThrow)(signer, target, 'vaultDeposit:receiptToken'),
|
|
929
|
+
]);
|
|
930
|
+
underlyingAsset = asset;
|
|
931
|
+
vaultDecimals = await (0, core_1.getDecimalsOrThrow)(signer, receiptTokenAddr, 'vaultDeposit:receiptDecimals');
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
poolContract = (0, core_1.createContract)({
|
|
935
|
+
address: target,
|
|
936
|
+
provider: signer,
|
|
937
|
+
abi: abis_1.ABI_LENDING_POOLS,
|
|
938
|
+
});
|
|
939
|
+
const [fetchedUnderlyingAsset, rawDecimals] = await Promise.all([
|
|
940
|
+
poolContract.asset(),
|
|
941
|
+
(0, core_1.getDecimalsOrThrow)(signer, target, 'vaultDeposit:poolDecimals'),
|
|
942
|
+
]);
|
|
943
|
+
vaultDecimals = rawDecimals;
|
|
944
|
+
underlyingAsset = fetchedUnderlyingAsset;
|
|
945
|
+
}
|
|
946
|
+
const actualDepositAsset = depositAsset || underlyingAsset;
|
|
947
|
+
const isNativeToken = actualDepositAsset === ethers_1.ZeroAddress ||
|
|
948
|
+
actualDepositAsset === '0x0000000000000000000000000000000000000000';
|
|
949
|
+
const isAdapterDeposit = actualDepositAsset.toLowerCase() !== underlyingAsset.toLowerCase();
|
|
950
|
+
const isMultiAssetVault = vaultVersion === 'evm-2';
|
|
951
|
+
// vaultDeposit never swaps. Any-token swap deposits are the explicit,
|
|
952
|
+
// opt-in job of {@link swapRouterDeposit}; keeping them out of here is what
|
|
953
|
+
// stops a natively-accepted asset from being silently swapped. The only
|
|
954
|
+
// SwapRouter use is a direct deposit of an asset the vault accepts.
|
|
955
|
+
if (isAdapterDeposit && !isMultiAssetVault && !adapterConfig) {
|
|
956
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `vaultDeposit: depositAsset ${actualDepositAsset} differs from vault underlying ${underlyingAsset} but no adapter is configured for vault ${target}. For an any-token deposit into a SwapRouter-eligible vault, use swapRouterDeposit instead.`);
|
|
957
|
+
}
|
|
958
|
+
// The Paraswap bridge adapter (`bridgeId === 2`) has no working deposit
|
|
959
|
+
// path in vaultDeposit: the SDK never built the Paraswap calldata, so the
|
|
960
|
+
// old branch sent `swapAndDeposit` with an empty quote and a zero
|
|
961
|
+
// `minAmountOut` — a transaction that could only revert, after an ERC-20
|
|
962
|
+
// approval had already been paid for. Refuse here, before the allowance
|
|
963
|
+
// check / approval below, so no on-chain write ever happens for this
|
|
964
|
+
// route. Callers wanting an any-token deposit use swapRouterDeposit.
|
|
965
|
+
if (isAdapterDeposit && adapterConfig?.bridgeId === 2) {
|
|
966
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `vaultDeposit: vault ${target} routes depositAsset ${actualDepositAsset} through a Paraswap bridge adapter, which vaultDeposit does not support. Use swapRouterDeposit for an any-token swap deposit, or deposit the vault's underlying asset ${underlyingAsset} directly.`);
|
|
967
|
+
}
|
|
968
|
+
const routeChainId = tokenizedVault?.chain ?? chainId;
|
|
969
|
+
const route = await resolveDepositRoute(signer, {
|
|
970
|
+
target,
|
|
971
|
+
chainId: routeChainId,
|
|
972
|
+
actualDepositAsset,
|
|
973
|
+
underlyingAsset,
|
|
974
|
+
isMultiAssetVault,
|
|
975
|
+
vaultVersion,
|
|
976
|
+
isDepositWithPermit: options?.isDepositWithPermit,
|
|
977
|
+
});
|
|
978
|
+
trackOriginAttribution(route, { chainId: routeChainId, target });
|
|
979
|
+
const depositTokenDecimals = await resolveDepositTokenDecimals({
|
|
980
|
+
actualDepositAsset,
|
|
981
|
+
underlyingAsset,
|
|
982
|
+
isNativeToken,
|
|
983
|
+
isMultiAssetVault,
|
|
984
|
+
vaultDecimals,
|
|
985
|
+
readErc20Decimals: async (addr) => (0, core_1.getDecimalsOrThrow)(signer, addr, 'vaultDeposit:depositTokenDecimals'),
|
|
986
|
+
});
|
|
987
|
+
const normalizedAmt = (0, core_1.toNormalizedBn)(amount, depositTokenDecimals);
|
|
988
|
+
if (!isNativeToken) {
|
|
989
|
+
const spenderAddress = route.spender;
|
|
990
|
+
const depositTokenContract = (0, core_1.createContract)({
|
|
991
|
+
address: actualDepositAsset,
|
|
992
|
+
provider: signer,
|
|
993
|
+
abi: abis_1.ABI_ERC20,
|
|
994
|
+
});
|
|
995
|
+
const currentAllowance = await depositTokenContract.allowance(wallet, spenderAddress);
|
|
996
|
+
if (safeBigInt(currentAllowance, 'vaultDeposit:allowance') <
|
|
997
|
+
BigInt(normalizedAmt.raw)) {
|
|
998
|
+
const { hash: approveHash } = await safeSendTx(() => depositTokenContract
|
|
999
|
+
.connect(signer)
|
|
1000
|
+
.approve(spenderAddress, normalizedAmt.raw), signer, true);
|
|
1001
|
+
core_1.Logger.log.info('approve:tx_hash', approveHash);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
// Route to appropriate deposit method
|
|
1005
|
+
if (route.via === 'router') {
|
|
1006
|
+
depositTx = await (0, core_1.createContract)({
|
|
1007
|
+
address: route.spender,
|
|
1008
|
+
provider: signer,
|
|
1009
|
+
abi: abis_1.ABI_SWAP_ROUTER,
|
|
1010
|
+
})
|
|
1011
|
+
.connect(signer)
|
|
1012
|
+
.deposit(route.originCode, normalizedAmt.raw, target, route.vaultType, actualDepositAsset, wallet);
|
|
1013
|
+
}
|
|
1014
|
+
else if (isMultiAssetVault) {
|
|
1015
|
+
if (options?.isDepositWithPermit) {
|
|
1016
|
+
// depositWithPermit flow
|
|
1017
|
+
// build structure hash of permit off chain
|
|
1018
|
+
const deadline = (0, utils_1.createDeadline)();
|
|
1019
|
+
//sign the hash with private key of depositor
|
|
1020
|
+
const { v, r, s } = await (0, utils_1.generatePermitSignature)(actualDepositAsset, wallet, await poolContract.getAddress(), normalizedAmt.raw, deadline, signer, tokenizedVault.chain);
|
|
1021
|
+
//actually call depositWithPermit func
|
|
1022
|
+
const { hash: permitHash } = await safeSendTx(() => poolContract
|
|
1023
|
+
.connect(signer)
|
|
1024
|
+
.depositWithPermit(actualDepositAsset, normalizedAmt.raw, wallet, deadline, r, s, v), signer, wait);
|
|
1025
|
+
core_1.Logger.log.info('deposit:tx_hash', permitHash);
|
|
1026
|
+
return permitHash;
|
|
1027
|
+
}
|
|
1028
|
+
// Multi-asset vault deposit (EVM-2)
|
|
1029
|
+
depositTx = await poolContract
|
|
1030
|
+
.connect(signer)
|
|
1031
|
+
.deposit(actualDepositAsset, normalizedAmt.raw, wallet);
|
|
1032
|
+
}
|
|
1033
|
+
else if (isAdapterDeposit && adapterConfig) {
|
|
1034
|
+
// Adapter deposit: native/wrapped token deposit via wrapper contract.
|
|
1035
|
+
// (The Paraswap bridge route was rejected above, before any approval.)
|
|
1036
|
+
const nativeDepositConfig = await (0, adapter_helpers_1.buildNativeDepositTransaction)({
|
|
1037
|
+
signer,
|
|
1038
|
+
srcToken: actualDepositAsset,
|
|
1039
|
+
amount: normalizedAmt.raw.toString(),
|
|
1040
|
+
poolName,
|
|
1041
|
+
adapterConfig,
|
|
1042
|
+
userAddress: wallet,
|
|
1043
|
+
});
|
|
1044
|
+
depositTx = await nativeDepositConfig.contract
|
|
1045
|
+
.connect(signer)[nativeDepositConfig.functionName](...nativeDepositConfig.args, nativeDepositConfig.value ? { value: nativeDepositConfig.value } : {});
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
// Standard deposit
|
|
1049
|
+
if (isNativeToken) {
|
|
1050
|
+
// Native ETH/AVAX deposit
|
|
1051
|
+
depositTx = await poolContract
|
|
1052
|
+
.connect(signer)
|
|
1053
|
+
.deposit(normalizedAmt.raw, wallet, { value: normalizedAmt.raw });
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
// Standard ERC20 deposit
|
|
1057
|
+
depositTx = await poolContract
|
|
1058
|
+
.connect(signer)
|
|
1059
|
+
.deposit(normalizedAmt.raw, wallet);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (wait)
|
|
1063
|
+
await safeWaitForTx(depositTx);
|
|
1064
|
+
core_1.Logger.log.info('deposit:tx_hash', depositTx?.hash);
|
|
1065
|
+
return depositTx?.hash;
|
|
1066
|
+
}
|
|
1067
|
+
catch (e) {
|
|
1068
|
+
const recovered = await tryRecoverTxHash(e, signer, wait);
|
|
1069
|
+
if (recovered) {
|
|
1070
|
+
core_1.Logger.log.warn('deposit:nonce-fallback', { hash: recovered });
|
|
1071
|
+
return recovered;
|
|
1072
|
+
}
|
|
1073
|
+
if (e instanceof core_1.UpshiftSDKError)
|
|
1074
|
+
throw e;
|
|
1075
|
+
// depositTx is only ever unset here when the depositWithPermit sub-path
|
|
1076
|
+
// threw — that branch sends its single main tx through safeSendTx and
|
|
1077
|
+
// never assigns depositTx, so the error's own markBroadcast marker is the
|
|
1078
|
+
// only place the broadcast hash survives. Every other sub-path assigns
|
|
1079
|
+
// depositTx before this catch can run.
|
|
1080
|
+
const broadcast = depositTx
|
|
1081
|
+
? localTxBroadcastContext(depositTx, e)
|
|
1082
|
+
: errorTxBroadcastContext(e);
|
|
1083
|
+
// User-cancelled deposits are normal; only genuine failures stay at error.
|
|
1084
|
+
const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
|
|
1085
|
+
(0, core_1.logChainError)('deposit', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
|
|
1086
|
+
target,
|
|
1087
|
+
amount,
|
|
1088
|
+
depositAsset,
|
|
1089
|
+
...broadcast,
|
|
1090
|
+
});
|
|
1091
|
+
throwIfInsufficientFunds(insufficientFunds, 'Deposit', e, {
|
|
1092
|
+
target,
|
|
1093
|
+
amount,
|
|
1094
|
+
depositAsset,
|
|
1095
|
+
...broadcast,
|
|
1096
|
+
});
|
|
1097
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `Deposit failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, amount, depositAsset, ...broadcast } });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Request to redeem vault shares. For EVM-1 vaults this queues a standard
|
|
1102
|
+
* redemption (claimable later via `vaultRedeem`). For EVM-2 vaults the queued
|
|
1103
|
+
* `requestRedeem` escrows the shares in the vault via `transferFrom`, so this
|
|
1104
|
+
* also approves the receipt token when its allowance is short; the instant
|
|
1105
|
+
* path (`isInstantRedeem: true`) burns the caller's shares directly and sends
|
|
1106
|
+
* no approval.
|
|
1107
|
+
*
|
|
1108
|
+
* The receipt-token approval (EVM-2 queued path) is always waited on before
|
|
1109
|
+
* the redeem call, regardless of the caller's `wait` flag.
|
|
1110
|
+
*
|
|
1111
|
+
* @param signer ethers Signer / Wallet (or viem WalletClient via {@link setSigner})
|
|
1112
|
+
* @param options Vault address, wallet, amount, optional `isInstantRedeem`,
|
|
1113
|
+
* optional `wait` to wait for the redeem receipt.
|
|
1114
|
+
* @returns Redeem transaction hash.
|
|
1115
|
+
* @throws UpshiftValidationError on invalid wallet/target/amount.
|
|
1116
|
+
* @throws UpshiftSDKError when the redeem submission or on-chain call fails.
|
|
1117
|
+
*
|
|
1118
|
+
* @example
|
|
1119
|
+
* ```ts
|
|
1120
|
+
* const hash = await upshiftSdk.evm.vaultRequestRedeem({
|
|
1121
|
+
* target: vaultAddress,
|
|
1122
|
+
* wallet: walletAddress,
|
|
1123
|
+
* amount: '10', // 10 shares (UI units)
|
|
1124
|
+
* wait: true,
|
|
1125
|
+
* });
|
|
1126
|
+
* ```
|
|
1127
|
+
*
|
|
1128
|
+
* @example Instant redemption
|
|
1129
|
+
* ```ts
|
|
1130
|
+
* await upshiftSdk.evm.vaultRequestRedeem({
|
|
1131
|
+
* target: vaultAddress,
|
|
1132
|
+
* wallet: walletAddress,
|
|
1133
|
+
* amount: '10',
|
|
1134
|
+
* isInstantRedeem: true,
|
|
1135
|
+
* });
|
|
1136
|
+
* ```
|
|
1137
|
+
*/
|
|
1138
|
+
async function vaultRequestRedeem(signer, options) {
|
|
1139
|
+
const { wallet, target, wait, amount } = options;
|
|
1140
|
+
const [goodWallet, goodPool] = [
|
|
1141
|
+
(0, core_1.checkAddress)(wallet, console, 'wallet'),
|
|
1142
|
+
(0, core_1.checkAddress)(target, console),
|
|
1143
|
+
];
|
|
1144
|
+
if (!goodWallet || !goodPool) {
|
|
1145
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `vaultRequestRedeem: invalid ${!goodWallet ? 'wallet' : 'target'} address`);
|
|
1146
|
+
}
|
|
1147
|
+
if (amount === undefined || amount === null) {
|
|
1148
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'vaultRequestRedeem: amount is required');
|
|
1149
|
+
}
|
|
1150
|
+
validateAmountPrecision(amount);
|
|
1151
|
+
// Hoisted out of the try so the catch can tell "the redeem was broadcast and
|
|
1152
|
+
// we lost the confirmation" from "the redeem never left". The EVM-2 receipt
|
|
1153
|
+
// token approval sent earlier in the try is deliberately NOT tracked here:
|
|
1154
|
+
// a failed approval means the redeem itself was never sent.
|
|
1155
|
+
let requestRedeemTx;
|
|
1156
|
+
try {
|
|
1157
|
+
// Get vault version to determine correct ABI
|
|
1158
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(target, undefined, false, false))?.[0];
|
|
1159
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
1160
|
+
let poolContract;
|
|
1161
|
+
let decimals;
|
|
1162
|
+
// Handle different vault versions
|
|
1163
|
+
if (vaultVersion === 'evm-2') {
|
|
1164
|
+
// For evm-2, always use ABI_TOKENIZED_VAULT_V2 to get lpTokenAddress
|
|
1165
|
+
// even for instant redeem, we need lpTokenAddress for receipt token decimals
|
|
1166
|
+
const vaultContract = (0, core_1.createContract)({
|
|
1167
|
+
address: target,
|
|
1168
|
+
provider: signer,
|
|
1169
|
+
abi: abis_1.ABI_TOKENIZED_VAULT_V2,
|
|
1170
|
+
});
|
|
1171
|
+
// Get receipt token for decimals. Retried on transport blips only; a
|
|
1172
|
+
// misroute (a non-evm-2 vault reaching this branch) still throws the
|
|
1173
|
+
// original error. See getReceiptTokenAddressOrThrow.
|
|
1174
|
+
const receiptTokenAddr = await (0, core_1.getReceiptTokenAddressOrThrow)(signer, target, 'vaultRequestRedeem:receiptToken');
|
|
1175
|
+
decimals = await (0, core_1.getDecimalsOrThrow)(signer, receiptTokenAddr, 'vaultRequestRedeem:receiptDecimals');
|
|
1176
|
+
// Queued requestRedeem escrows the shares in the vault through
|
|
1177
|
+
// receipt.transferFrom(user → vault): the vault is the spender, so it
|
|
1178
|
+
// needs an explicit allowance even on self-redeem (without it the call
|
|
1179
|
+
// reverts with ERC20InsufficientAllowance). instantRedeem burns the
|
|
1180
|
+
// caller's shares directly and needs none.
|
|
1181
|
+
if (!options.isInstantRedeem) {
|
|
1182
|
+
const receiptContract = (0, core_1.createContract)({
|
|
1183
|
+
address: receiptTokenAddr,
|
|
1184
|
+
provider: signer,
|
|
1185
|
+
abi: abis_1.ABI_ERC20,
|
|
1186
|
+
});
|
|
1187
|
+
const sharesRaw = (0, core_1.toNormalizedBn)(amount, decimals).raw;
|
|
1188
|
+
const allowance = await receiptContract.allowance(wallet, target);
|
|
1189
|
+
if (safeBigInt(allowance, 'vaultRequestRedeem:allowance') <
|
|
1190
|
+
BigInt(sharesRaw)) {
|
|
1191
|
+
const { hash: approveHash } = await safeSendTx(() => receiptContract.connect(signer).approve(target, sharesRaw), signer, true);
|
|
1192
|
+
core_1.Logger.log.info('approve:receipt_token:tx_hash', approveHash);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
poolContract = vaultContract;
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
poolContract = options.isInstantRedeem
|
|
1199
|
+
? (0, core_1.createContract)({
|
|
1200
|
+
address: target,
|
|
1201
|
+
provider: signer,
|
|
1202
|
+
abi: abis_1.ABI_LENDING_POOL_V3,
|
|
1203
|
+
})
|
|
1204
|
+
: (0, core_1.createContract)({
|
|
1205
|
+
address: target,
|
|
1206
|
+
provider: signer,
|
|
1207
|
+
abi: abis_1.ABI_LENDING_POOLS,
|
|
1208
|
+
});
|
|
1209
|
+
decimals = await (0, core_1.getDecimalsOrThrow)(signer, target, 'vaultRequestRedeem:poolDecimals');
|
|
1210
|
+
}
|
|
1211
|
+
// Convert amount to bignumber (for non-evm-2 vaults)
|
|
1212
|
+
const normalizedAmt = (0, core_1.toNormalizedBn)(amount, decimals);
|
|
1213
|
+
// Withdraw from vault - EVM-2 uses 2 params, others use 3 params
|
|
1214
|
+
if (options.isInstantRedeem) {
|
|
1215
|
+
if (vaultVersion === 'evm-2') {
|
|
1216
|
+
// EVM-2: instantRedeem(uint256 shares, address receiverAddr) - 2 params
|
|
1217
|
+
requestRedeemTx = await poolContract
|
|
1218
|
+
.connect(signer)
|
|
1219
|
+
.instantRedeem(BigInt(normalizedAmt.raw), wallet);
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
// EVM-0/EVM-1: instantRedeem(uint256 shares, address receiverAddr, address holderAddr) - 3 params
|
|
1223
|
+
requestRedeemTx = await poolContract
|
|
1224
|
+
.connect(signer)
|
|
1225
|
+
.instantRedeem(BigInt(normalizedAmt.raw), wallet, wallet);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
else if (vaultVersion === 'evm-2') {
|
|
1229
|
+
// EVM-2: requestRedeem(uint256 shares, address receiverAddr)
|
|
1230
|
+
requestRedeemTx = await poolContract
|
|
1231
|
+
.connect(signer)
|
|
1232
|
+
.requestRedeem(BigInt(normalizedAmt.raw), wallet);
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
// EVM-0/EVM-1: requestRedeem(uint256 shares, address receiver, address owner)
|
|
1236
|
+
requestRedeemTx = await poolContract
|
|
1237
|
+
.connect(signer)
|
|
1238
|
+
.requestRedeem(BigInt(normalizedAmt.raw), wallet, wallet);
|
|
1239
|
+
}
|
|
1240
|
+
if (wait)
|
|
1241
|
+
await safeWaitForTx(requestRedeemTx);
|
|
1242
|
+
core_1.Logger.log.info('requestRedeem:tx_hash', requestRedeemTx?.hash);
|
|
1243
|
+
return requestRedeemTx?.hash;
|
|
1244
|
+
}
|
|
1245
|
+
catch (e) {
|
|
1246
|
+
const recovered = await tryRecoverTxHash(e, signer, wait);
|
|
1247
|
+
if (recovered) {
|
|
1248
|
+
core_1.Logger.log.warn('requestRedeem:nonce-fallback', { hash: recovered });
|
|
1249
|
+
return recovered;
|
|
1250
|
+
}
|
|
1251
|
+
if (e instanceof core_1.UpshiftSDKError)
|
|
1252
|
+
throw e;
|
|
1253
|
+
const broadcast = localTxBroadcastContext(requestRedeemTx, e);
|
|
1254
|
+
// User-cancelled redeem requests are normal; only genuine failures stay at error.
|
|
1255
|
+
const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
|
|
1256
|
+
(0, core_1.logChainError)('requestRedeem', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
|
|
1257
|
+
target,
|
|
1258
|
+
amount,
|
|
1259
|
+
...broadcast,
|
|
1260
|
+
});
|
|
1261
|
+
throwIfInsufficientFunds(insufficientFunds, 'Request redeem', e, {
|
|
1262
|
+
target,
|
|
1263
|
+
amount,
|
|
1264
|
+
...broadcast,
|
|
1265
|
+
});
|
|
1266
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `Request redeem failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, amount, ...broadcast } });
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* @description withdraw funds from the specified pool including accrued rewards
|
|
1271
|
+
* @param signer signer / wallet object
|
|
1272
|
+
* @param options object including pool contract address, user wallet address, and more
|
|
1273
|
+
* @returns claim tx hash
|
|
1274
|
+
*/
|
|
1275
|
+
async function vaultRedeem(signer, options) {
|
|
1276
|
+
const { wallet, target, wait, year, day, month, receiverIndex } = options;
|
|
1277
|
+
const [goodWallet, goodPool] = [
|
|
1278
|
+
(0, core_1.checkAddress)(wallet, console, 'wallet'),
|
|
1279
|
+
(0, core_1.checkAddress)(target, console),
|
|
1280
|
+
];
|
|
1281
|
+
if (!goodWallet || !goodPool) {
|
|
1282
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `vaultRedeem: invalid ${!goodWallet ? 'wallet' : 'target'} address`);
|
|
1283
|
+
}
|
|
1284
|
+
if (!year || !month || !day || !receiverIndex) {
|
|
1285
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'vaultRedeem: year, month, day, and receiverIndex are required');
|
|
1286
|
+
}
|
|
1287
|
+
try {
|
|
1288
|
+
const poolContract = (0, core_1.createContract)({
|
|
1289
|
+
address: target,
|
|
1290
|
+
provider: signer,
|
|
1291
|
+
abi: abis_1.ABI_LENDING_POOLS,
|
|
1292
|
+
});
|
|
1293
|
+
const { hash: redeemHash } = await safeSendTx(() => poolContract
|
|
1294
|
+
.connect(signer)
|
|
1295
|
+
.redeem(year, month, day, receiverIndex, wallet), signer, wait);
|
|
1296
|
+
core_1.Logger.log.info('redeem:tx_hash', redeemHash);
|
|
1297
|
+
return redeemHash;
|
|
1298
|
+
}
|
|
1299
|
+
catch (e) {
|
|
1300
|
+
const recovered = await tryRecoverTxHash(e, signer, wait);
|
|
1301
|
+
if (recovered) {
|
|
1302
|
+
core_1.Logger.log.warn('redeem:nonce-fallback', { hash: recovered });
|
|
1303
|
+
return recovered;
|
|
1304
|
+
}
|
|
1305
|
+
if (e instanceof core_1.UpshiftSDKError)
|
|
1306
|
+
throw e;
|
|
1307
|
+
// Single tx on this path, so the error's own marker can only be the redeem.
|
|
1308
|
+
const broadcast = errorTxBroadcastContext(e);
|
|
1309
|
+
// User-cancelled redeems are normal; only genuine failures stay at error.
|
|
1310
|
+
const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
|
|
1311
|
+
(0, core_1.logChainError)('redeem', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
|
|
1312
|
+
target,
|
|
1313
|
+
year,
|
|
1314
|
+
month,
|
|
1315
|
+
day,
|
|
1316
|
+
receiverIndex,
|
|
1317
|
+
...broadcast,
|
|
1318
|
+
});
|
|
1319
|
+
throwIfInsufficientFunds(insufficientFunds, 'Redeem', e, {
|
|
1320
|
+
target,
|
|
1321
|
+
year,
|
|
1322
|
+
month,
|
|
1323
|
+
day,
|
|
1324
|
+
receiverIndex,
|
|
1325
|
+
...broadcast,
|
|
1326
|
+
});
|
|
1327
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `Redeem failed: ${e instanceof Error ? e.message : 'Unknown error'}`, {
|
|
1328
|
+
cause: e,
|
|
1329
|
+
context: { target, year, month, day, receiverIndex, ...broadcast },
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Claim a previously-requested queued withdrawal on an EVM-2 (Tokenized
|
|
1335
|
+
* Vault V2) vault, once its withdrawal epoch has passed.
|
|
1336
|
+
*
|
|
1337
|
+
* EVM-2 only — v1/v0 vaults use a different claim path ({@link vaultRedeem}).
|
|
1338
|
+
* The vault's on-chain `claim(year, month, day, receiverAddr)` reverts if the
|
|
1339
|
+
* epoch has not yet elapsed, or if the vault's idle reserve has not been
|
|
1340
|
+
* refilled from subaccounts to cover the queue for that date; neither
|
|
1341
|
+
* condition is checked client-side before sending.
|
|
1342
|
+
*
|
|
1343
|
+
* @param signer ethers Signer / Wallet.
|
|
1344
|
+
* @param options.target EVM-2 vault contract address.
|
|
1345
|
+
* @param options.wallet Wallet initiating the claim; receives the withdrawn assets unless `receiver` is set.
|
|
1346
|
+
* @param options.year Withdrawal-epoch year (see `getWithdrawalRequestsWithStatus` / `getWithdrawalEpoch`).
|
|
1347
|
+
* @param options.month Withdrawal-epoch month.
|
|
1348
|
+
* @param options.day Withdrawal-epoch day.
|
|
1349
|
+
* @param options.receiver Address receiving the withdrawn assets. Defaults to `wallet`.
|
|
1350
|
+
* @param options.wait Wait for transaction confirmation.
|
|
1351
|
+
* @returns Claim transaction hash.
|
|
1352
|
+
* @throws UpshiftValidationError on invalid address input, missing or non-integer date fields, or a non-EVM-2 vault.
|
|
1353
|
+
* @throws UpshiftSDKError when the on-chain claim call fails.
|
|
1354
|
+
*
|
|
1355
|
+
* @worstCaseRpcCalls 2 — one vault-version lookup, one claim transaction.
|
|
1356
|
+
*
|
|
1357
|
+
* @example
|
|
1358
|
+
* ```ts
|
|
1359
|
+
* // vaultRequestRedeem returns the request tx hash, not the epoch. Read the
|
|
1360
|
+
* // epoch of each queued request (and whether it is claimable yet) from
|
|
1361
|
+
* // getWithdrawalRequestsWithStatus (exported from '@upshiftfinance/sdk').
|
|
1362
|
+
* await upshiftSdk.evm.vaultRequestRedeem({
|
|
1363
|
+
* target: vaultAddress,
|
|
1364
|
+
* wallet: walletAddress,
|
|
1365
|
+
* amount: '10',
|
|
1366
|
+
* });
|
|
1367
|
+
* const requests = await getWithdrawalRequestsWithStatus({
|
|
1368
|
+
* vault: vaultAddress,
|
|
1369
|
+
* receiver: walletAddress,
|
|
1370
|
+
* options: { rpcUrl },
|
|
1371
|
+
* });
|
|
1372
|
+
* const ready = requests.find((r) => r.status === 'ready_to_claim');
|
|
1373
|
+
* if (!ready) return;
|
|
1374
|
+
* const { year, month, day } = ready.claimableDate;
|
|
1375
|
+
* await upshiftSdk.evm.claimWithdrawal({
|
|
1376
|
+
* target: vaultAddress,
|
|
1377
|
+
* wallet: walletAddress,
|
|
1378
|
+
* year,
|
|
1379
|
+
* month,
|
|
1380
|
+
* day,
|
|
1381
|
+
* wait: true,
|
|
1382
|
+
* });
|
|
1383
|
+
* ```
|
|
1384
|
+
*/
|
|
1385
|
+
async function claimWithdrawal(signer, options) {
|
|
1386
|
+
const { wallet, target, wait, year, month, day, receiver } = options;
|
|
1387
|
+
const [goodWallet, goodTarget] = [
|
|
1388
|
+
(0, core_1.checkAddress)(wallet, console, 'wallet'),
|
|
1389
|
+
(0, core_1.checkAddress)(target, console),
|
|
1390
|
+
];
|
|
1391
|
+
if (!goodWallet || !goodTarget) {
|
|
1392
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `claimWithdrawal: invalid ${!goodWallet ? 'wallet' : 'target'} address`);
|
|
1393
|
+
}
|
|
1394
|
+
if (receiver !== undefined && !(0, core_1.checkAddress)(receiver, console, 'wallet')) {
|
|
1395
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', 'claimWithdrawal: invalid receiver address');
|
|
1396
|
+
}
|
|
1397
|
+
if (year === undefined || month === undefined || day === undefined) {
|
|
1398
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'claimWithdrawal: year, month, and day are required');
|
|
1399
|
+
}
|
|
1400
|
+
const yearRaw = (0, core_1.toRawBigInt)(year, 'claimWithdrawal', 'year');
|
|
1401
|
+
const monthRaw = (0, core_1.toRawBigInt)(month, 'claimWithdrawal', 'month');
|
|
1402
|
+
const dayRaw = (0, core_1.toRawBigInt)(day, 'claimWithdrawal', 'day');
|
|
1403
|
+
try {
|
|
1404
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(target, undefined, false, false))?.[0];
|
|
1405
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
1406
|
+
if (vaultVersion !== 'evm-2') {
|
|
1407
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `claimWithdrawal: only EVM-2 (Tokenized Vault V2) vaults expose claim() — ${target} resolved to ${vaultVersion}`);
|
|
1408
|
+
}
|
|
1409
|
+
const vaultContract = (0, core_1.createContract)({
|
|
1410
|
+
address: target,
|
|
1411
|
+
provider: signer,
|
|
1412
|
+
abi: abis_1.ABI_TOKENIZED_VAULT_V2,
|
|
1413
|
+
});
|
|
1414
|
+
const { hash: claimHash } = await safeSendTx(() => vaultContract
|
|
1415
|
+
.connect(signer)
|
|
1416
|
+
.claim(yearRaw, monthRaw, dayRaw, receiver ?? wallet), signer, wait);
|
|
1417
|
+
core_1.Logger.log.info('claim:tx_hash', claimHash);
|
|
1418
|
+
return claimHash;
|
|
1419
|
+
}
|
|
1420
|
+
catch (e) {
|
|
1421
|
+
const recovered = await tryRecoverTxHash(e, signer, wait);
|
|
1422
|
+
if (recovered) {
|
|
1423
|
+
core_1.Logger.log.warn('claim:nonce-fallback', { hash: recovered });
|
|
1424
|
+
return recovered;
|
|
1425
|
+
}
|
|
1426
|
+
if (e instanceof core_1.UpshiftSDKError)
|
|
1427
|
+
throw e;
|
|
1428
|
+
const broadcast = errorTxBroadcastContext(e);
|
|
1429
|
+
const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
|
|
1430
|
+
(0, core_1.logChainError)('claim', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
|
|
1431
|
+
target,
|
|
1432
|
+
year,
|
|
1433
|
+
month,
|
|
1434
|
+
day,
|
|
1435
|
+
...broadcast,
|
|
1436
|
+
});
|
|
1437
|
+
throwIfInsufficientFunds(insufficientFunds, 'Claim withdrawal', e, {
|
|
1438
|
+
target,
|
|
1439
|
+
year,
|
|
1440
|
+
month,
|
|
1441
|
+
day,
|
|
1442
|
+
...broadcast,
|
|
1443
|
+
});
|
|
1444
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `Claim withdrawal failed: ${e instanceof Error ? e.message : 'Unknown error'}`, {
|
|
1445
|
+
cause: e,
|
|
1446
|
+
context: { target, year, month, day, ...broadcast },
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Deposit a native token (ETH / AVAX / etc.) into a vault via the
|
|
1452
|
+
* `MultiAssetNativeDepositWrapper`. The wrapper forwards `msg.value` to
|
|
1453
|
+
* the vault as the deposit underlying — no allowance is required.
|
|
1454
|
+
*
|
|
1455
|
+
* `amount` is the **raw on-chain value in wei-equivalent** (18 decimals
|
|
1456
|
+
* for ETH-family natives). If you have a UI-shaped amount in ether, scale
|
|
1457
|
+
* it first.
|
|
1458
|
+
*
|
|
1459
|
+
* @param signer ethers Signer / Wallet
|
|
1460
|
+
* @param options `wrapperAddress` is the deployed wrapper contract;
|
|
1461
|
+
* `receiver` defaults to the signer's address; `amount` is the native
|
|
1462
|
+
* amount in wei; `wait` waits for the deposit receipt.
|
|
1463
|
+
* @returns Deposit transaction hash.
|
|
1464
|
+
* @throws UpshiftValidationError on invalid wrapper / receiver / amount.
|
|
1465
|
+
* @throws UpshiftSDKError when the deposit submission or on-chain call fails.
|
|
1466
|
+
*
|
|
1467
|
+
* @example
|
|
1468
|
+
* ```ts
|
|
1469
|
+
* import { parseEther } from 'ethers';
|
|
1470
|
+
*
|
|
1471
|
+
* await upshiftSdk.evm.depositNative({
|
|
1472
|
+
* wrapperAddress: nativeWrapperAddress,
|
|
1473
|
+
* amount: parseEther('0.5'), // 0.5 ETH in wei
|
|
1474
|
+
* wait: true,
|
|
1475
|
+
* });
|
|
1476
|
+
* ```
|
|
1477
|
+
*/
|
|
1478
|
+
async function depositNative(signer, options) {
|
|
1479
|
+
const { wrapperAddress, receiver, amount, wait } = options;
|
|
1480
|
+
const goodWrapper = (0, core_1.checkAddress)(wrapperAddress, console, 'contract');
|
|
1481
|
+
if (!goodWrapper) {
|
|
1482
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', 'depositNative: invalid wrapper address');
|
|
1483
|
+
}
|
|
1484
|
+
if (amount === undefined || amount === null) {
|
|
1485
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'depositNative: amount is required');
|
|
1486
|
+
}
|
|
1487
|
+
validateAmountPrecision(amount);
|
|
1488
|
+
// Hoisted so the catch can distinguish "broadcast, confirmation lost" from
|
|
1489
|
+
// "never sent" — see localTxBroadcastContext.
|
|
1490
|
+
let depositTx;
|
|
1491
|
+
try {
|
|
1492
|
+
// Create wrapper contract instance
|
|
1493
|
+
const wrapperContract = (0, core_1.createContract)({
|
|
1494
|
+
address: wrapperAddress,
|
|
1495
|
+
provider: signer,
|
|
1496
|
+
abi: abis_1.ABI_MULTI_ASSET_NATIVE_DEPOSIT_WRAPPER,
|
|
1497
|
+
}); // TODO: provide typed interface
|
|
1498
|
+
// Convert amount to bigint (native tokens use 18 decimals typically)
|
|
1499
|
+
// For native deposits, amount should be in wei/smallest unit
|
|
1500
|
+
const amountBigInt = typeof amount === 'bigint'
|
|
1501
|
+
? amount
|
|
1502
|
+
: typeof amount === 'string'
|
|
1503
|
+
? BigInt(amount)
|
|
1504
|
+
: BigInt(amount);
|
|
1505
|
+
// Connect the contract first, then get the function to disambiguate between overloads
|
|
1506
|
+
const connectedContract = wrapperContract.connect(signer);
|
|
1507
|
+
// Call depositNative with or without receiver parameter
|
|
1508
|
+
if (receiver) {
|
|
1509
|
+
const goodReceiver = (0, core_1.checkAddress)(receiver, console, 'wallet');
|
|
1510
|
+
if (!goodReceiver) {
|
|
1511
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', 'depositNative: invalid receiver address');
|
|
1512
|
+
}
|
|
1513
|
+
// depositNative(address receiver) - with receiver
|
|
1514
|
+
const depositNativeWithReceiver = connectedContract.getFunction('depositNative(address)');
|
|
1515
|
+
depositTx = await depositNativeWithReceiver(receiver, {
|
|
1516
|
+
value: amountBigInt,
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
else {
|
|
1520
|
+
// depositNative() - without receiver (uses msg.sender as receiver)
|
|
1521
|
+
const depositNativeNoParams = connectedContract.getFunction('depositNative()');
|
|
1522
|
+
depositTx = await depositNativeNoParams({
|
|
1523
|
+
value: amountBigInt,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
if (wait)
|
|
1527
|
+
await safeWaitForTx(depositTx);
|
|
1528
|
+
core_1.Logger.log.info('depositNative:tx_hash', depositTx?.hash);
|
|
1529
|
+
return depositTx?.hash;
|
|
1530
|
+
}
|
|
1531
|
+
catch (e) {
|
|
1532
|
+
const recovered = await tryRecoverTxHash(e, signer, wait);
|
|
1533
|
+
if (recovered) {
|
|
1534
|
+
core_1.Logger.log.warn('depositNative:nonce-fallback', { hash: recovered });
|
|
1535
|
+
return recovered;
|
|
1536
|
+
}
|
|
1537
|
+
if (e instanceof core_1.UpshiftSDKError)
|
|
1538
|
+
throw e;
|
|
1539
|
+
const broadcast = localTxBroadcastContext(depositTx, e);
|
|
1540
|
+
// User-cancelled native deposits are normal; only genuine failures stay at error.
|
|
1541
|
+
const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
|
|
1542
|
+
(0, core_1.logChainError)('depositNative', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
|
|
1543
|
+
wrapperAddress,
|
|
1544
|
+
receiver,
|
|
1545
|
+
amount,
|
|
1546
|
+
...broadcast,
|
|
1547
|
+
});
|
|
1548
|
+
throwIfInsufficientFunds(insufficientFunds, 'Deposit native', e, {
|
|
1549
|
+
wrapperAddress,
|
|
1550
|
+
receiver,
|
|
1551
|
+
amount,
|
|
1552
|
+
...broadcast,
|
|
1553
|
+
});
|
|
1554
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `Deposit native failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { wrapperAddress, receiver, amount, ...broadcast } });
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Redeem vault shares for an underlying asset via the RwaRedeemSubaccount.
|
|
1559
|
+
* Caller must approve the **vault share token** (not the output asset) to
|
|
1560
|
+
* the subaccount beforehand — it pulls shares via `transferFrom`.
|
|
1561
|
+
*
|
|
1562
|
+
* Pass `minOut` to enforce slippage protection (in `outputDecimals` units).
|
|
1563
|
+
* The SDK does not currently compute `minOut` from a slippage percentage —
|
|
1564
|
+
* the caller is responsible for choosing a safe lower bound.
|
|
1565
|
+
*
|
|
1566
|
+
* @throws UpshiftValidationError on invalid addresses or amount inputs.
|
|
1567
|
+
* @throws UpshiftSDKError when the redeem submission or on-chain call fails.
|
|
1568
|
+
*
|
|
1569
|
+
* @example
|
|
1570
|
+
* ```ts
|
|
1571
|
+
* // 1. Approve the vault share token to the subaccount first.
|
|
1572
|
+
* await upshiftSdk.evm.vaultApprove({
|
|
1573
|
+
* target: subaccountAddress,
|
|
1574
|
+
* wallet: walletAddress,
|
|
1575
|
+
* amount: '10',
|
|
1576
|
+
* wait: true,
|
|
1577
|
+
* });
|
|
1578
|
+
*
|
|
1579
|
+
* // 2. Redeem 10 shares for USDC, requiring at least 9.5 USDC out.
|
|
1580
|
+
* await upshiftSdk.evm.rwaRedeemAsset({
|
|
1581
|
+
* target: subaccountAddress,
|
|
1582
|
+
* wallet: walletAddress,
|
|
1583
|
+
* asset: USDC_ADDRESS,
|
|
1584
|
+
* amount: '10',
|
|
1585
|
+
* minOut: '9.5',
|
|
1586
|
+
* decimals: 18, // vault share decimals
|
|
1587
|
+
* outputDecimals: 6, // USDC decimals
|
|
1588
|
+
* wait: true,
|
|
1589
|
+
* });
|
|
1590
|
+
* ```
|
|
1591
|
+
*/
|
|
1592
|
+
async function rwaRedeemAsset(signer, options) {
|
|
1593
|
+
const { target, wallet, asset, amount, minOut, decimals, outputDecimals, wait, } = options;
|
|
1594
|
+
const goodTarget = (0, core_1.checkAddress)(target, console, 'contract');
|
|
1595
|
+
const goodAsset = (0, core_1.checkAddress)(asset, console, 'contract');
|
|
1596
|
+
const goodWallet = (0, core_1.checkAddress)(wallet, console, 'wallet');
|
|
1597
|
+
if (!goodTarget || !goodAsset || !goodWallet) {
|
|
1598
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `rwaRedeemAsset: invalid ${!goodTarget ? 'target' : !goodAsset ? 'asset' : 'wallet'} address`);
|
|
1599
|
+
}
|
|
1600
|
+
validateAmountPrecision(amount);
|
|
1601
|
+
validateAmountPrecision(minOut);
|
|
1602
|
+
try {
|
|
1603
|
+
const subaccountContract = (0, core_1.createContract)({
|
|
1604
|
+
address: target,
|
|
1605
|
+
provider: signer,
|
|
1606
|
+
abi: abis_1.RWA_REDEEM_SUBACCOUNT,
|
|
1607
|
+
});
|
|
1608
|
+
if (!subaccountContract) {
|
|
1609
|
+
throw new Error(`rwaRedeemAsset: failed to instantiate subaccount contract at ${target}`);
|
|
1610
|
+
}
|
|
1611
|
+
const normalizedAmt = (0, core_1.toNormalizedBn)(amount, decimals);
|
|
1612
|
+
const normalizedMinOut = (0, core_1.toNormalizedBn)(minOut, outputDecimals);
|
|
1613
|
+
const { hash: redeemHash } = await safeSendTx(() => subaccountContract
|
|
1614
|
+
.connect(signer)
|
|
1615
|
+
.redeemAsset(asset, BigInt(normalizedAmt.raw), BigInt(normalizedMinOut.raw)), signer, wait);
|
|
1616
|
+
core_1.Logger.log.info('rwaRedeemAsset:tx_hash', redeemHash);
|
|
1617
|
+
return redeemHash;
|
|
1618
|
+
}
|
|
1619
|
+
catch (e) {
|
|
1620
|
+
const recovered = await tryRecoverTxHash(e, signer, wait);
|
|
1621
|
+
if (recovered) {
|
|
1622
|
+
core_1.Logger.log.warn('rwaRedeemAsset:nonce-fallback', { hash: recovered });
|
|
1623
|
+
return recovered;
|
|
1624
|
+
}
|
|
1625
|
+
if (e instanceof core_1.UpshiftSDKError)
|
|
1626
|
+
throw e;
|
|
1627
|
+
// Single tx on this path, so the error's own marker can only be the redeem.
|
|
1628
|
+
const broadcast = errorTxBroadcastContext(e);
|
|
1629
|
+
// User-cancelled RWA redeems are normal; only genuine failures stay at error.
|
|
1630
|
+
const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
|
|
1631
|
+
(0, core_1.logChainError)('rwaRedeemAsset', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
|
|
1632
|
+
target,
|
|
1633
|
+
asset,
|
|
1634
|
+
amount,
|
|
1635
|
+
minOut,
|
|
1636
|
+
...broadcast,
|
|
1637
|
+
});
|
|
1638
|
+
throwIfInsufficientFunds(insufficientFunds, 'RWA redeem', e, {
|
|
1639
|
+
target,
|
|
1640
|
+
asset,
|
|
1641
|
+
amount,
|
|
1642
|
+
minOut,
|
|
1643
|
+
...broadcast,
|
|
1644
|
+
});
|
|
1645
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `RWA redeem failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, asset, amount, minOut, ...broadcast } });
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
/** @internal */
|
|
1649
|
+
async function dispatchViaSwapRouter(args) {
|
|
1650
|
+
const amountRaw = BigInt(args.normalizedAmt.raw);
|
|
1651
|
+
const sharesReceiver = args.receiver ?? args.wallet;
|
|
1652
|
+
const vaultType = toSwapRouterVaultType(args.isMultiAssetVault);
|
|
1653
|
+
if (args.isNativeToken) {
|
|
1654
|
+
// SwapRouter native path requires underlying === wrapped native.
|
|
1655
|
+
const wrappedNative = core_1.SWAP_ROUTER_WRAPPED_NATIVE[args.chainId];
|
|
1656
|
+
if (!wrappedNative ||
|
|
1657
|
+
args.underlyingAsset.toLowerCase() !== wrappedNative.toLowerCase()) {
|
|
1658
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `vaultDeposit (SwapRouter): native deposit is only supported when the vault's reference asset is the chain's wrapped-native token. Vault ${args.target} underlying ${args.underlyingAsset} is not. Wrap to ${wrappedNative ?? 'WETH'} first and call again with depositAsset = the wrapped token. Check isNativeSwapRouterDepositSupported(chainId, vault, options) before offering this path to avoid hitting this error.`, {
|
|
1659
|
+
context: {
|
|
1660
|
+
vault: args.target,
|
|
1661
|
+
underlying: args.underlyingAsset,
|
|
1662
|
+
chainId: args.chainId,
|
|
1663
|
+
wrappedNative,
|
|
1664
|
+
},
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
return depositNativeViaSwapRouter(args.signer, {
|
|
1668
|
+
chainId: args.chainId,
|
|
1669
|
+
vault: args.target,
|
|
1670
|
+
vaultType,
|
|
1671
|
+
receiver: sharesReceiver,
|
|
1672
|
+
amount: amountRaw,
|
|
1673
|
+
originCode: args.originCode,
|
|
1674
|
+
wait: args.wait,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
const isUnderlying = args.actualDepositAsset.toLowerCase() ===
|
|
1678
|
+
args.underlyingAsset.toLowerCase();
|
|
1679
|
+
if (isUnderlying ||
|
|
1680
|
+
(args.isMultiAssetVault &&
|
|
1681
|
+
(await isAcceptedDirectly(args.signer, args.target, args.actualDepositAsset, args.underlyingAsset, true)))) {
|
|
1682
|
+
return depositViaSwapRouter(args.signer, {
|
|
1683
|
+
chainId: args.chainId,
|
|
1684
|
+
vault: args.target,
|
|
1685
|
+
vaultType,
|
|
1686
|
+
receiver: sharesReceiver,
|
|
1687
|
+
asset: args.actualDepositAsset,
|
|
1688
|
+
amount: amountRaw,
|
|
1689
|
+
originCode: args.originCode,
|
|
1690
|
+
wait: args.wait,
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
// Vault share decimals can diverge from the underlying asset decimals
|
|
1694
|
+
// (e.g. an 18-decimal share token over an 8-decimal WBTC underlying on
|
|
1695
|
+
// a multi-asset v2 vault). The swap quote must use the underlying's
|
|
1696
|
+
// on-chain decimals so Paraswap's /prices interprets the route correctly.
|
|
1697
|
+
const underlyingDecimals = await (0, core_1.getDecimalsOrThrow)(args.signer, args.underlyingAsset, 'dispatchViaSwapRouter:underlyingDecimals');
|
|
1698
|
+
// The on-chain SwapRouter only authorizes a specific (router, selector) pair
|
|
1699
|
+
// per chain (see SWAP_ROUTER_DEX_AGGREGATOR). Pin the aggregator to that
|
|
1700
|
+
// router's single generic method so the calldata selector is deterministic,
|
|
1701
|
+
// then reject any quote that would not match — otherwise the deposit reverts
|
|
1702
|
+
// on-chain with InvalidRouter() / InvalidNotWhitelisted() after the user has
|
|
1703
|
+
// already paid gas.
|
|
1704
|
+
const aggregator = core_1.SWAP_ROUTER_DEX_AGGREGATOR[args.chainId];
|
|
1705
|
+
if (!aggregator) {
|
|
1706
|
+
throw new core_1.UpshiftValidationError('INVALID_CHAIN', `vaultDeposit (SwapRouter): no DEX aggregator is configured for chainId ${args.chainId}, but a swap is required because the deposit asset (${args.actualDepositAsset}) differs from the vault's reference asset (${args.underlyingAsset}). Deposit the reference asset directly, or add a SWAP_ROUTER_DEX_AGGREGATOR entry once the router is whitelisted on-chain.`, {
|
|
1707
|
+
context: {
|
|
1708
|
+
chainId: args.chainId,
|
|
1709
|
+
depositAsset: args.actualDepositAsset,
|
|
1710
|
+
underlyingAsset: args.underlyingAsset,
|
|
1711
|
+
},
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
const quote = await (0, swap_quotes_1.fetchSwapQuote)({
|
|
1715
|
+
chainId: args.chainId,
|
|
1716
|
+
srcToken: args.actualDepositAsset,
|
|
1717
|
+
srcDecimals: args.depositTokenDecimals,
|
|
1718
|
+
destToken: args.underlyingAsset,
|
|
1719
|
+
destDecimals: underlyingDecimals,
|
|
1720
|
+
amount: amountRaw,
|
|
1721
|
+
receiver: args.routerAddress,
|
|
1722
|
+
slippageBps: args.slippageBps,
|
|
1723
|
+
contractMethod: aggregator.contractMethod,
|
|
1724
|
+
});
|
|
1725
|
+
// Fail closed on off-chain/on-chain config drift: if the aggregator's quote
|
|
1726
|
+
// targets a router or selector the deployed SwapRouter has not whitelisted,
|
|
1727
|
+
// the swapAndDeposit call would revert. Surfacing it here turns a wasted gas
|
|
1728
|
+
// burn into an actionable SDK error.
|
|
1729
|
+
const quotedSelector = quote.payload.slice(0, 10).toLowerCase();
|
|
1730
|
+
if (quote.router.toLowerCase() !== aggregator.router.toLowerCase() ||
|
|
1731
|
+
quotedSelector !== aggregator.selector.toLowerCase()) {
|
|
1732
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `vaultDeposit (SwapRouter): the aggregator quote (router ${quote.router}, selector ${quotedSelector}) does not match the router whitelisted on chain ${args.chainId} (router ${aggregator.router}, selector ${aggregator.selector}); the on-chain SwapRouter would revert. This signals drift between the SDK's SWAP_ROUTER_DEX_AGGREGATOR and the deployed router's enableRouter config.`, {
|
|
1733
|
+
context: {
|
|
1734
|
+
chainId: args.chainId,
|
|
1735
|
+
quotedRouter: quote.router,
|
|
1736
|
+
quotedSelector,
|
|
1737
|
+
expectedRouter: aggregator.router,
|
|
1738
|
+
expectedSelector: aggregator.selector,
|
|
1739
|
+
},
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
return swapAndDeposit(args.signer, {
|
|
1743
|
+
chainId: args.chainId,
|
|
1744
|
+
vault: args.target,
|
|
1745
|
+
vaultType,
|
|
1746
|
+
receiver: sharesReceiver,
|
|
1747
|
+
swaps: [
|
|
1748
|
+
{
|
|
1749
|
+
tokenIn: args.actualDepositAsset,
|
|
1750
|
+
tokenOut: args.underlyingAsset,
|
|
1751
|
+
amountIn: amountRaw,
|
|
1752
|
+
minAmountOut: quote.minAmountOut,
|
|
1753
|
+
router: quote.router,
|
|
1754
|
+
payload: quote.payload,
|
|
1755
|
+
},
|
|
1756
|
+
],
|
|
1757
|
+
originCode: args.originCode,
|
|
1758
|
+
wait: args.wait,
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Resolve the origin code a SwapRouter write method sends and check its
|
|
1763
|
+
* registration before any approval or transaction, so an unregistered code
|
|
1764
|
+
* never costs the user a reverted deposit. A code passed to the call must be
|
|
1765
|
+
* registered or the call throws. An unregistered instance code falls back to
|
|
1766
|
+
* the zero sentinel with the same `attribution-skipped` signal as
|
|
1767
|
+
* `vaultDeposit`, or throws under `attribution.strictOrigin`. The zero
|
|
1768
|
+
* sentinel is sent without a read.
|
|
1769
|
+
* @internal
|
|
1770
|
+
*/
|
|
1771
|
+
async function resolveRouterOriginCode(signer, operation, chainId, vault, provided) {
|
|
1772
|
+
const code = (0, core_1.resolveOriginCode)(provided, chainId);
|
|
1773
|
+
if (code === core_1.ORIGIN_CODES.default ||
|
|
1774
|
+
(await (0, core_1.readOriginInfo)(chainId, code, () => signer)).registered) {
|
|
1775
|
+
return code;
|
|
1776
|
+
}
|
|
1777
|
+
if (provided !== undefined) {
|
|
1778
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${operation}: originCode is not registered on the SwapRouter for chain ${chainId}. Register it with addOrigin (check it with getOriginInfo), or pass a registered code.`, { context: { chainId, vault } });
|
|
1779
|
+
}
|
|
1780
|
+
if ((0, core_1.isStrictOrigin)()) {
|
|
1781
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${operation}: attribution.originCode is not registered on the SwapRouter for chain ${chainId} and attribution.strictOrigin is set. Register it with addOrigin, or unset strictOrigin to deposit without an origin code.`, { context: { chainId, vault } });
|
|
1782
|
+
}
|
|
1783
|
+
trackOriginAttribution({ via: 'native', spender: null, reason: 'origin-not-registered' }, { chainId, target: vault }, operation);
|
|
1784
|
+
return core_1.ORIGIN_CODES.default;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Throw before any approval when the chain's SwapRouter is paused. Every
|
|
1788
|
+
* router entry point reverts with `ContractIsPaused` while it is paused, so an
|
|
1789
|
+
* approval sent first would only cost the user gas. Unlike `vaultDeposit`,
|
|
1790
|
+
* these methods have no native path to fall back to: the caller asked for the
|
|
1791
|
+
* router.
|
|
1792
|
+
* @internal
|
|
1793
|
+
*/
|
|
1794
|
+
async function assertSwapRouterNotPaused(signer, operation, chainId, vault) {
|
|
1795
|
+
if (!(await (0, core_1.readSwapRouterPaused)(signer, chainId)))
|
|
1796
|
+
return;
|
|
1797
|
+
throw new core_1.UpshiftSDKError('UNKNOWN', `${operation}: the SwapRouter on chain ${chainId} is paused and rejects every deposit. Deposit into the vault directly with vaultDeposit, or retry once the router is unpaused.`, { context: { chainId, vault, reason: 'router-paused' } });
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Reject a legacy `evm-0` lending pool on an explicit SwapRouter path. The v2
|
|
1801
|
+
* router has no vault registry to keep such pools off it, and they were never
|
|
1802
|
+
* tested against its ERC-4626 deposit call, so {@link getDepositRoute} keeps
|
|
1803
|
+
* them native (`legacy-pool`) and the explicit methods refuse them up front.
|
|
1804
|
+
*
|
|
1805
|
+
* @param operation - Calling method, used in the error message.
|
|
1806
|
+
* @param vault - Vault the deposit targets.
|
|
1807
|
+
* @param vaultVersion - The vault's version from {@link getVaultVersionV2}.
|
|
1808
|
+
* @throws {@link UpshiftValidationError} when `vaultVersion` is `evm-0`.
|
|
1809
|
+
* @internal
|
|
1810
|
+
*/
|
|
1811
|
+
function assertNotLegacyPool(operation, vault, vaultVersion) {
|
|
1812
|
+
if (vaultVersion === 'evm-0') {
|
|
1813
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${operation}: vault ${vault} is a legacy lending pool, which the SwapRouter does not support. Deposit with vaultDeposit instead.`, { context: { vault } });
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* The vault type a SwapRouter write sends, after checking the router can
|
|
1818
|
+
* deposit into the vault: the caller's `vaultType` when given, else derived
|
|
1819
|
+
* from the vault's version in the Upshift API (`evm-2` → Tokenized Vault V2,
|
|
1820
|
+
* any other EVM vault → ERC-4626).
|
|
1821
|
+
*
|
|
1822
|
+
* Runs before any approval, so a vault the router cannot serve is refused
|
|
1823
|
+
* without the user paying for an approval and a reverting deposit. The v2
|
|
1824
|
+
* router has no vault registry, so this replaces the registration gate: a
|
|
1825
|
+
* legacy `evm-0` pool is refused, and so is a Tokenized Vault V2 with a
|
|
1826
|
+
* senders whitelist (KYC), whose deposit would revert because the router is
|
|
1827
|
+
* not a whitelisted sender.
|
|
1828
|
+
*
|
|
1829
|
+
* @param signer - Signer or provider used for the senders-whitelist read.
|
|
1830
|
+
* @param operation - Calling method, used in error messages.
|
|
1831
|
+
* @param chainId - Chain the vault is on.
|
|
1832
|
+
* @param vault - Vault the deposit targets.
|
|
1833
|
+
* @param provided - Optional caller-supplied vault type.
|
|
1834
|
+
* @returns The vault type to send to the router.
|
|
1835
|
+
* @throws {@link UpshiftValidationError} when `provided` is not a known vault
|
|
1836
|
+
* type; when it is omitted and the vault is not in the Upshift API (pass
|
|
1837
|
+
* `vaultType` explicitly for such a vault) or is a legacy `evm-0` pool; or
|
|
1838
|
+
* when a Tokenized Vault V2 has a senders whitelist.
|
|
1839
|
+
* @worstCaseRpcCalls 2 — one Upshift API read when `provided` is omitted, and
|
|
1840
|
+
* one `sendersWhitelistAddress()` read for a Tokenized Vault V2 (cached for
|
|
1841
|
+
* 5 minutes).
|
|
1842
|
+
* @internal
|
|
1843
|
+
*/
|
|
1844
|
+
async function resolveRouterVaultType(signer, operation, chainId, vault, provided) {
|
|
1845
|
+
let vaultType;
|
|
1846
|
+
if (provided !== undefined) {
|
|
1847
|
+
if (provided !== types_1.SwapRouterVaultType.ERC4626 &&
|
|
1848
|
+
provided !== types_1.SwapRouterVaultType.TokenizedVaultV2) {
|
|
1849
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${operation}: vaultType must be ${types_1.SwapRouterVaultType.ERC4626} (ERC4626) or ${types_1.SwapRouterVaultType.TokenizedVaultV2} (TokenizedVaultV2), got ${provided}`, { context: { vault, vaultType: provided } });
|
|
1850
|
+
}
|
|
1851
|
+
vaultType = provided;
|
|
1852
|
+
}
|
|
1853
|
+
else {
|
|
1854
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, undefined, false, false))?.[0];
|
|
1855
|
+
if (!tokenizedVault) {
|
|
1856
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${operation}: vault ${vault} is not in the Upshift API, so its SwapRouter vault type cannot be derived. Pass vaultType explicitly.`, { context: { vault } });
|
|
1857
|
+
}
|
|
1858
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
1859
|
+
assertNotLegacyPool(operation, vault, vaultVersion);
|
|
1860
|
+
vaultType = toSwapRouterVaultType(vaultVersion === 'evm-2');
|
|
1861
|
+
}
|
|
1862
|
+
if (vaultType === types_1.SwapRouterVaultType.TokenizedVaultV2 &&
|
|
1863
|
+
(await (0, core_1.readVaultHasSendersWhitelist)(signer, chainId, vault))) {
|
|
1864
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${operation}: vault ${vault} has a senders whitelist (KYC), so the SwapRouter cannot deposit into it. Deposit with vaultDeposit instead.`, { context: { vault, chainId } });
|
|
1865
|
+
}
|
|
1866
|
+
return vaultType;
|
|
1867
|
+
}
|
|
1868
|
+
/** @internal */
|
|
1869
|
+
function resolveSwapRouterOrThrow(chainId) {
|
|
1870
|
+
const router = (0, core_1.getSwapRouterAddress)(chainId);
|
|
1871
|
+
if (!router) {
|
|
1872
|
+
throw new core_1.UpshiftValidationError('INVALID_CHAIN', `SwapRouter: no deployment registered for chainId ${chainId}`, { context: { chainId } });
|
|
1873
|
+
}
|
|
1874
|
+
return router;
|
|
1875
|
+
}
|
|
1876
|
+
/** @internal */
|
|
1877
|
+
async function ensureAllowance(signer, token, owner, spender, amount) {
|
|
1878
|
+
const tokenContract = (0, core_1.createContract)({
|
|
1879
|
+
address: token,
|
|
1880
|
+
provider: signer,
|
|
1881
|
+
abi: abis_1.ABI_ERC20,
|
|
1882
|
+
});
|
|
1883
|
+
const current = safeBigInt(await tokenContract.allowance(owner, spender), 'swapRouter:allowance');
|
|
1884
|
+
if (current >= amount)
|
|
1885
|
+
return;
|
|
1886
|
+
const { hash } = await safeSendTx(() => tokenContract.connect(signer).approve(spender, amount), signer, true);
|
|
1887
|
+
core_1.Logger.log.info('swapRouter:approve:tx_hash', hash);
|
|
1888
|
+
}
|
|
1889
|
+
/** @internal */
|
|
1890
|
+
function ensureSwapsValid(swaps) {
|
|
1891
|
+
if (swaps.length === 0) {
|
|
1892
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'swapAndDeposit: at least one swap leg is required');
|
|
1893
|
+
}
|
|
1894
|
+
if (swaps.length > core_1.SWAP_ROUTER_MAX_SWAPS) {
|
|
1895
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `swapAndDeposit: too many swaps (${swaps.length} > ${core_1.SWAP_ROUTER_MAX_SWAPS})`, { context: { count: swaps.length, max: core_1.SWAP_ROUTER_MAX_SWAPS } });
|
|
1896
|
+
}
|
|
1897
|
+
for (let i = 0; i < swaps.length; i += 1) {
|
|
1898
|
+
validateSwapParams(swaps[i], i);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
/** @internal */
|
|
1902
|
+
function validateSwapParams(swap, index) {
|
|
1903
|
+
const where = `swapAndDeposit: swaps[${index}]`;
|
|
1904
|
+
if (!(0, core_1.checkAddress)(swap.tokenIn, console, 'contract')) {
|
|
1905
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `${where}.tokenIn is not a valid address`);
|
|
1906
|
+
}
|
|
1907
|
+
if (!(0, core_1.checkAddress)(swap.tokenOut, console, 'contract')) {
|
|
1908
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `${where}.tokenOut is not a valid address`);
|
|
1909
|
+
}
|
|
1910
|
+
if (!(0, core_1.checkAddress)(swap.router, console, 'contract')) {
|
|
1911
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `${where}.router is not a valid address`);
|
|
1912
|
+
}
|
|
1913
|
+
if (swap.tokenIn.toLowerCase() === swap.tokenOut.toLowerCase()) {
|
|
1914
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${where}: tokenIn and tokenOut are identical — no swap needed`);
|
|
1915
|
+
}
|
|
1916
|
+
if (swap.amountIn <= 0n) {
|
|
1917
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${where}.amountIn must be greater than zero`);
|
|
1918
|
+
}
|
|
1919
|
+
if (swap.minAmountOut <= 0n) {
|
|
1920
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${where}.minAmountOut must be greater than zero — slippage protection cannot be disabled`);
|
|
1921
|
+
}
|
|
1922
|
+
if (!/^0x[0-9a-fA-F]+$/.test(swap.payload) || swap.payload.length < 10) {
|
|
1923
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${where}.payload must be ABI-encoded calldata (got "${swap.payload.slice(0, 12)}…")`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
/** @internal */
|
|
1927
|
+
function totalInputPerToken(swaps) {
|
|
1928
|
+
const totals = new Map();
|
|
1929
|
+
for (const swap of swaps) {
|
|
1930
|
+
const key = swap.tokenIn.toLowerCase();
|
|
1931
|
+
totals.set(key, (totals.get(key) ?? 0n) + swap.amountIn);
|
|
1932
|
+
}
|
|
1933
|
+
return totals;
|
|
1934
|
+
}
|
|
1935
|
+
// Allowance owner must equal msg.sender (contract uses transferFrom(msg.sender, ...));
|
|
1936
|
+
// smart-account wallets need explicit handling outside the SDK.
|
|
1937
|
+
/** @internal */
|
|
1938
|
+
async function resolveSignerEOA(signer, callerContext) {
|
|
1939
|
+
const getAddress = signer.getAddress;
|
|
1940
|
+
if (typeof getAddress !== 'function') {
|
|
1941
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', `${callerContext}: signer must implement getAddress(); got ${typeof getAddress}`);
|
|
1942
|
+
}
|
|
1943
|
+
const eoa = await getAddress.call(signer);
|
|
1944
|
+
if (!eoa || !(0, core_1.checkAddress)(eoa, console, 'wallet')) {
|
|
1945
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `${callerContext}: signer.getAddress() returned an invalid address: ${String(eoa)}`);
|
|
1946
|
+
}
|
|
1947
|
+
return eoa;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Swap one or more whitelisted ERC-20s into a vault's reference asset via
|
|
1951
|
+
* the on-chain SwapRouter, then deposit the proceeds into the vault.
|
|
1952
|
+
*
|
|
1953
|
+
* Approval is sent automatically when the caller's allowance on each
|
|
1954
|
+
* `swaps[i].tokenIn` against the SwapRouter is below `amountIn`.
|
|
1955
|
+
*
|
|
1956
|
+
* @param signer - ethers `Signer` or `Wallet` with the EOA that holds the input tokens.
|
|
1957
|
+
* @param options - {@link ISwapAndDepositOptions}.
|
|
1958
|
+
* @returns The transaction hash of the `swapAndDeposit` call.
|
|
1959
|
+
*
|
|
1960
|
+
* @throws {@link UpshiftValidationError} for unsupported chains, invalid
|
|
1961
|
+
* addresses, empty/oversized swap arrays, a vault the router cannot deposit
|
|
1962
|
+
* into (a legacy `evm-0` pool, or a Tokenized Vault V2 with a senders
|
|
1963
|
+
* whitelist), or an origin code not registered on the router (a per-call
|
|
1964
|
+
* code, or the instance code under `attribution.strictOrigin`), before any
|
|
1965
|
+
* approval.
|
|
1966
|
+
* @throws {@link UpshiftSDKError} while the SwapRouter is paused (checked before
|
|
1967
|
+
* any approval), or on unrecoverable contract or RPC failure.
|
|
1968
|
+
*
|
|
1969
|
+
* @example
|
|
1970
|
+
* ```ts
|
|
1971
|
+
* const quote = await fetchSwapQuote({
|
|
1972
|
+
* chainId: 1,
|
|
1973
|
+
* srcToken: WBTC,
|
|
1974
|
+
* srcDecimals: 8,
|
|
1975
|
+
* destToken: USDC,
|
|
1976
|
+
* destDecimals: 6,
|
|
1977
|
+
* amount: 100_000_000n,
|
|
1978
|
+
* receiver: SWAP_ROUTER_ADDRESSES[1]!,
|
|
1979
|
+
* });
|
|
1980
|
+
* const hash = await swapAndDeposit(signer, {
|
|
1981
|
+
* chainId: 1,
|
|
1982
|
+
* vault: '0x8AcA0841993ef4C87244d519166e767f49362C21',
|
|
1983
|
+
* receiver: wallet,
|
|
1984
|
+
* swaps: [{
|
|
1985
|
+
* tokenIn: WBTC,
|
|
1986
|
+
* tokenOut: USDC,
|
|
1987
|
+
* amountIn: 100_000_000n,
|
|
1988
|
+
* minAmountOut: quote.minAmountOut,
|
|
1989
|
+
* router: quote.router,
|
|
1990
|
+
* payload: quote.payload,
|
|
1991
|
+
* }],
|
|
1992
|
+
* });
|
|
1993
|
+
* ```
|
|
1994
|
+
*/
|
|
1995
|
+
async function swapAndDeposit(signer, options) {
|
|
1996
|
+
const { chainId, vault, receiver, swaps, originCode, wait } = options;
|
|
1997
|
+
const goodVault = (0, core_1.checkAddress)(vault, console, 'contract');
|
|
1998
|
+
const goodReceiver = (0, core_1.checkAddress)(receiver, console, 'wallet');
|
|
1999
|
+
if (!goodVault || !goodReceiver) {
|
|
2000
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `swapAndDeposit: invalid ${!goodVault ? 'vault' : 'receiver'} address`);
|
|
2001
|
+
}
|
|
2002
|
+
ensureSwapsValid(swaps);
|
|
2003
|
+
const routerAddress = resolveSwapRouterOrThrow(chainId);
|
|
2004
|
+
let tx;
|
|
2005
|
+
try {
|
|
2006
|
+
await assertSwapRouterNotPaused(signer, 'swapAndDeposit', chainId, vault);
|
|
2007
|
+
const vaultType = await resolveRouterVaultType(signer, 'swapAndDeposit', chainId, vault, options.vaultType);
|
|
2008
|
+
const origin = await resolveRouterOriginCode(signer, 'swapAndDeposit', chainId, vault, originCode);
|
|
2009
|
+
const eoa = await resolveSignerEOA(signer, 'swapAndDeposit');
|
|
2010
|
+
for (const [tokenIn, totalIn] of totalInputPerToken(swaps)) {
|
|
2011
|
+
await ensureAllowance(signer, tokenIn, eoa, routerAddress, totalIn);
|
|
2012
|
+
}
|
|
2013
|
+
const routerContract = (0, core_1.createContract)({
|
|
2014
|
+
address: routerAddress,
|
|
2015
|
+
provider: signer,
|
|
2016
|
+
abi: abis_1.ABI_SWAP_ROUTER,
|
|
2017
|
+
});
|
|
2018
|
+
tx = (await routerContract
|
|
2019
|
+
.connect(signer)
|
|
2020
|
+
.swapAndDeposit(origin, vault, vaultType, receiver, swaps));
|
|
2021
|
+
if (wait)
|
|
2022
|
+
await safeWaitForTx(tx);
|
|
2023
|
+
core_1.Logger.log.info('swapAndDeposit:tx_hash', tx.hash);
|
|
2024
|
+
return tx.hash;
|
|
2025
|
+
}
|
|
2026
|
+
catch (e) {
|
|
2027
|
+
return handleSwapRouterWriteError(e, tx, signer, wait, 'swapAndDeposit', 'Swap and deposit', { chainId, vault });
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Deposit a vault's reference asset directly through the SwapRouter — no
|
|
2032
|
+
* swap. Useful when the caller already holds the reference asset but wants
|
|
2033
|
+
* the origin/referral fee accounting that only the SwapRouter path provides.
|
|
2034
|
+
*
|
|
2035
|
+
* @param signer - ethers `Signer` or `Wallet`.
|
|
2036
|
+
* @param options - {@link ISwapRouterDirectDepositOptions}.
|
|
2037
|
+
* @returns The transaction hash of the `deposit` call.
|
|
2038
|
+
*
|
|
2039
|
+
* @throws {@link UpshiftValidationError} for unsupported chains, invalid
|
|
2040
|
+
* addresses, zero amount, a vault the router cannot deposit into (a legacy
|
|
2041
|
+
* `evm-0` pool, or a Tokenized Vault V2 with a senders whitelist), or an
|
|
2042
|
+
* origin code not registered on the router (a per-call code, or the instance
|
|
2043
|
+
* code under `attribution.strictOrigin`), before any approval; with code
|
|
2044
|
+
* `ACCOUNT_NOT_FUNDED` when the account cannot cover gas.
|
|
2045
|
+
* @throws {@link UpshiftSDKError} while the SwapRouter is paused (checked before
|
|
2046
|
+
* any approval), or on unrecoverable contract or RPC failure.
|
|
2047
|
+
*/
|
|
2048
|
+
async function depositViaSwapRouter(signer, options) {
|
|
2049
|
+
const { chainId, vault, receiver, asset, amount, originCode, wait } = options;
|
|
2050
|
+
const goodVault = (0, core_1.checkAddress)(vault, console, 'contract');
|
|
2051
|
+
const goodReceiver = (0, core_1.checkAddress)(receiver, console, 'wallet');
|
|
2052
|
+
const goodAsset = (0, core_1.checkAddress)(asset, console, 'contract');
|
|
2053
|
+
if (!goodVault || !goodReceiver || !goodAsset) {
|
|
2054
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', 'depositViaSwapRouter: invalid address');
|
|
2055
|
+
}
|
|
2056
|
+
if (amount === 0n) {
|
|
2057
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'depositViaSwapRouter: amount must be greater than zero');
|
|
2058
|
+
}
|
|
2059
|
+
const routerAddress = resolveSwapRouterOrThrow(chainId);
|
|
2060
|
+
let tx;
|
|
2061
|
+
try {
|
|
2062
|
+
await assertSwapRouterNotPaused(signer, 'depositViaSwapRouter', chainId, vault);
|
|
2063
|
+
const vaultType = await resolveRouterVaultType(signer, 'depositViaSwapRouter', chainId, vault, options.vaultType);
|
|
2064
|
+
const origin = await resolveRouterOriginCode(signer, 'depositViaSwapRouter', chainId, vault, originCode);
|
|
2065
|
+
const eoa = await resolveSignerEOA(signer, 'depositViaSwapRouter');
|
|
2066
|
+
await ensureAllowance(signer, asset, eoa, routerAddress, amount);
|
|
2067
|
+
const routerContract = (0, core_1.createContract)({
|
|
2068
|
+
address: routerAddress,
|
|
2069
|
+
provider: signer,
|
|
2070
|
+
abi: abis_1.ABI_SWAP_ROUTER,
|
|
2071
|
+
});
|
|
2072
|
+
tx = (await routerContract
|
|
2073
|
+
.connect(signer)
|
|
2074
|
+
.deposit(origin, amount, vault, vaultType, asset, receiver));
|
|
2075
|
+
if (wait)
|
|
2076
|
+
await safeWaitForTx(tx);
|
|
2077
|
+
core_1.Logger.log.info('depositViaSwapRouter:tx_hash', tx.hash);
|
|
2078
|
+
return tx.hash;
|
|
2079
|
+
}
|
|
2080
|
+
catch (e) {
|
|
2081
|
+
return handleSwapRouterWriteError(e, tx, signer, wait, 'depositViaSwapRouter', 'SwapRouter deposit', { chainId, vault });
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Deposit native ETH (or chain-native equivalent) into a vault via the
|
|
2086
|
+
* SwapRouter. The router wraps `amount` to the chain's wrapped-native token
|
|
2087
|
+
* before depositing — only valid when the vault's reference asset equals
|
|
2088
|
+
* the wrapped-native token.
|
|
2089
|
+
*
|
|
2090
|
+
* **Wrap-only limitation**: the on-chain SwapRouter has no ETH→other-asset
|
|
2091
|
+
* swap leg, so this only ever reaches a WETH-accepting vault (see
|
|
2092
|
+
* {@link SWAP_ROUTER_WRAPPED_NATIVE}). There is no atomic native-ETH deposit
|
|
2093
|
+
* into a non-WETH-reference vault (e.g. a USDC vault) — deposit the wrapped
|
|
2094
|
+
* native token instead (already whitelisted, routes through the swap leg for
|
|
2095
|
+
* a non-reference-asset vault). Use {@link isNativeSwapRouterDepositSupported}
|
|
2096
|
+
* to check eligibility before offering this path in a UI.
|
|
2097
|
+
*
|
|
2098
|
+
* No ERC-20 approval is needed; `amount` travels as `msg.value`.
|
|
2099
|
+
*
|
|
2100
|
+
* @param signer - ethers `Signer` or `Wallet`.
|
|
2101
|
+
* @param options - {@link ISwapRouterNativeDepositOptions}.
|
|
2102
|
+
* @returns The transaction hash of the `depositNativeToken` call.
|
|
2103
|
+
*
|
|
2104
|
+
* @throws {@link UpshiftValidationError} for unsupported chains, invalid
|
|
2105
|
+
* addresses, zero amount, a vault the router cannot deposit into (a legacy
|
|
2106
|
+
* `evm-0` pool, or a Tokenized Vault V2 with a senders whitelist), or an
|
|
2107
|
+
* origin code not registered on the router (a per-call code, or the instance
|
|
2108
|
+
* code under `attribution.strictOrigin`), before any approval; with code
|
|
2109
|
+
* `ACCOUNT_NOT_FUNDED` when the account cannot cover gas.
|
|
2110
|
+
* @throws {@link UpshiftSDKError} while the SwapRouter is paused (checked before
|
|
2111
|
+
* any approval), or on unrecoverable contract or RPC failure.
|
|
2112
|
+
*/
|
|
2113
|
+
async function depositNativeViaSwapRouter(signer, options) {
|
|
2114
|
+
const { chainId, vault, receiver, amount, originCode, wait } = options;
|
|
2115
|
+
const goodVault = (0, core_1.checkAddress)(vault, console, 'contract');
|
|
2116
|
+
const goodReceiver = (0, core_1.checkAddress)(receiver, console, 'wallet');
|
|
2117
|
+
if (!goodVault || !goodReceiver) {
|
|
2118
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', 'depositNativeViaSwapRouter: invalid address');
|
|
2119
|
+
}
|
|
2120
|
+
if (amount === 0n) {
|
|
2121
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'depositNativeViaSwapRouter: amount must be greater than zero');
|
|
2122
|
+
}
|
|
2123
|
+
const routerAddress = resolveSwapRouterOrThrow(chainId);
|
|
2124
|
+
let tx;
|
|
2125
|
+
try {
|
|
2126
|
+
await assertSwapRouterNotPaused(signer, 'depositNativeViaSwapRouter', chainId, vault);
|
|
2127
|
+
const vaultType = await resolveRouterVaultType(signer, 'depositNativeViaSwapRouter', chainId, vault, options.vaultType);
|
|
2128
|
+
const origin = await resolveRouterOriginCode(signer, 'depositNativeViaSwapRouter', chainId, vault, originCode);
|
|
2129
|
+
const routerContract = (0, core_1.createContract)({
|
|
2130
|
+
address: routerAddress,
|
|
2131
|
+
provider: signer,
|
|
2132
|
+
abi: abis_1.ABI_SWAP_ROUTER,
|
|
2133
|
+
});
|
|
2134
|
+
tx = (await routerContract
|
|
2135
|
+
.connect(signer)
|
|
2136
|
+
.depositNativeToken(origin, vault, vaultType, receiver, {
|
|
2137
|
+
value: amount,
|
|
2138
|
+
}));
|
|
2139
|
+
if (wait)
|
|
2140
|
+
await safeWaitForTx(tx);
|
|
2141
|
+
core_1.Logger.log.info('depositNativeViaSwapRouter:tx_hash', tx.hash);
|
|
2142
|
+
return tx.hash;
|
|
2143
|
+
}
|
|
2144
|
+
catch (e) {
|
|
2145
|
+
return handleSwapRouterWriteError(e, tx, signer, wait, 'depositNativeViaSwapRouter', 'SwapRouter native deposit', { chainId, vault });
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Whether a vault can take a native-ETH (chain-native token) SwapRouter
|
|
2150
|
+
* deposit — the pre-flight check for {@link depositNativeViaSwapRouter} and
|
|
2151
|
+
* {@link swapRouterDeposit} with a zero-address `depositAsset`.
|
|
2152
|
+
*
|
|
2153
|
+
* The on-chain SwapRouter only wraps native ETH to the chain's wrapped-native
|
|
2154
|
+
* token and deposits that — it has no ETH→other-asset swap leg (see
|
|
2155
|
+
* {@link SWAP_ROUTER_WRAPPED_NATIVE}). So a native deposit needs both: a
|
|
2156
|
+
* SwapRouter deployed on `chainId`, and the vault's own on-chain `asset()`
|
|
2157
|
+
* equal to that chain's wrapped-native token. The v2 router has no vault
|
|
2158
|
+
* registry, so no router read is needed. Use this to gate a
|
|
2159
|
+
* "deposit ETH" UI option before the user picks it, instead of discovering
|
|
2160
|
+
* the answer via a thrown {@link UpshiftValidationError} or an on-chain revert.
|
|
2161
|
+
*
|
|
2162
|
+
* It checks configuration only — a deposit that passes this check can still
|
|
2163
|
+
* fail on balance, gas, a paused vault, or a deposit cap.
|
|
2164
|
+
*
|
|
2165
|
+
* Read-only and provider-agnostic (no signer required) — safe to call before
|
|
2166
|
+
* a wallet is connected. Makes no backend call.
|
|
2167
|
+
*
|
|
2168
|
+
* @param chainId - EVM chain ID the vault is deployed on.
|
|
2169
|
+
* @param vault - Upshift vault address to check.
|
|
2170
|
+
* @param options - `rpcUrl` for `chainId`, passed to `createProvider`.
|
|
2171
|
+
* @returns `true` iff a SwapRouter exists on `chainId` and the vault's
|
|
2172
|
+
* `asset()` is the chain's wrapped-native token. `false` — never throws —
|
|
2173
|
+
* for a chain with no SwapRouter deployment, a missing/invalid `rpcUrl`, an
|
|
2174
|
+
* unreadable vault, or a vault whose reference asset isn't the
|
|
2175
|
+
* wrapped-native token.
|
|
2176
|
+
* @worstCaseRpcCalls 1 — the vault's `asset()`. None on a chain with no
|
|
2177
|
+
* SwapRouter.
|
|
2178
|
+
* @example
|
|
2179
|
+
* ```ts
|
|
2180
|
+
* const canDepositEth = await isNativeSwapRouterDepositSupported(
|
|
2181
|
+
* 1,
|
|
2182
|
+
* '0x1234000000000000000000000000000000005678',
|
|
2183
|
+
* { rpcUrl: 'https://eth-mainnet.example/v2/KEY' },
|
|
2184
|
+
* );
|
|
2185
|
+
* // true only if that vault's asset is WETH and mainnet has a SwapRouter
|
|
2186
|
+
* ```
|
|
2187
|
+
*/
|
|
2188
|
+
async function isNativeSwapRouterDepositSupported(chainId, vault, options) {
|
|
2189
|
+
const wrappedNative = core_1.SWAP_ROUTER_WRAPPED_NATIVE[chainId];
|
|
2190
|
+
const routerAddress = (0, core_1.getSwapRouterAddress)(chainId);
|
|
2191
|
+
if (!wrappedNative || !routerAddress)
|
|
2192
|
+
return false;
|
|
2193
|
+
try {
|
|
2194
|
+
const provider = (0, core_1.createProvider)(options.rpcUrl, chainId);
|
|
2195
|
+
const vaultContract = (0, core_1.createContract)({
|
|
2196
|
+
address: vault,
|
|
2197
|
+
provider,
|
|
2198
|
+
abi: abis_1.ABI_TOKENIZED_VAULT_V2,
|
|
2199
|
+
});
|
|
2200
|
+
const asset = (await vaultContract.asset());
|
|
2201
|
+
return asset.toLowerCase() === wrappedNative.toLowerCase();
|
|
2202
|
+
}
|
|
2203
|
+
catch (error) {
|
|
2204
|
+
core_1.Logger.log.warn('isNativeSwapRouterDepositSupported', 'vault read failed — treating as unsupported', { vault, chainId, error });
|
|
2205
|
+
return false;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* Deposit into an Upshift vault through the on-chain `SwapRouter`, selecting the
|
|
2210
|
+
* correct router path from the deposit asset.
|
|
2211
|
+
*
|
|
2212
|
+
* {@link vaultDeposit} never swaps; this function always routes through the
|
|
2213
|
+
* SwapRouter, because the caller asked it to. Keeping swaps opt-in is the
|
|
2214
|
+
* point: a native-deposit vault (e.g. a multi-asset Tokenized Vault V2 whose
|
|
2215
|
+
* non-reference assets are accepted directly) is never accidentally swapped —
|
|
2216
|
+
* the regression that implicit, allowlist-driven routing inside `vaultDeposit`
|
|
2217
|
+
* caused before it was removed. Use `vaultDeposit` for native / adapter
|
|
2218
|
+
* deposits and this for SwapRouter deposits.
|
|
2219
|
+
*
|
|
2220
|
+
* The vault's reference asset and its decimals are read on-chain (never trusted
|
|
2221
|
+
* from the caller — a wrong reference asset would mis-route the swap). The path
|
|
2222
|
+
* is then chosen from `depositAsset`:
|
|
2223
|
+
* - equals the reference asset, or is an asset a Tokenized Vault V2 accepts
|
|
2224
|
+
* as-is (e.g. USDT into Sentora USD) → direct router deposit, no swap and
|
|
2225
|
+
* no swap fee
|
|
2226
|
+
* ({@link depositViaSwapRouter});
|
|
2227
|
+
* - the zero address → native-token deposit ({@link depositNativeViaSwapRouter}),
|
|
2228
|
+
* valid only when the reference asset is the chain's wrapped-native token —
|
|
2229
|
+
* the router only wraps ETH, it never swaps it, so this throws for any
|
|
2230
|
+
* other reference asset (see {@link SWAP_ROUTER_WRAPPED_NATIVE} for why, and
|
|
2231
|
+
* {@link isNativeSwapRouterDepositSupported} to check first);
|
|
2232
|
+
* - any other ERC-20 → a swap to the reference asset (Paraswap quote pinned to
|
|
2233
|
+
* the chain's whitelisted aggregator, fail-closed on router/selector drift)
|
|
2234
|
+
* bundled into {@link swapAndDeposit}.
|
|
2235
|
+
*
|
|
2236
|
+
* The origin code — the per-call `originCode`, else the SDK instance's
|
|
2237
|
+
* `attribution.originCode` on Ethereum mainnet, else the all-zero sentinel
|
|
2238
|
+
* ("no origin fee") — is forwarded to whichever of those three entry points
|
|
2239
|
+
* runs, so a partner's origin fee accrues on every path.
|
|
2240
|
+
*
|
|
2241
|
+
* Side effects: reads tokenized-vault metadata, the vault's reference asset and
|
|
2242
|
+
* decimals, for a Tokenized Vault V2 non-reference asset the vault's assets
|
|
2243
|
+
* whitelist, the origin code's registration, and (on the swap path) one Paraswap quote;
|
|
2244
|
+
* sends one ERC-20 `approve` to the SwapRouter when allowance is short, then
|
|
2245
|
+
* the deposit tx.
|
|
2246
|
+
*
|
|
2247
|
+
* @param signer - ethers `Signer` or `Wallet` that signs the approval + deposit.
|
|
2248
|
+
* @param options - {@link ISwapRouterDepositOptions}.
|
|
2249
|
+
* @returns The transaction hash of the router deposit call.
|
|
2250
|
+
* @throws {@link UpshiftValidationError} for an unsupported chain (no SwapRouter
|
|
2251
|
+
* deployment), an invalid vault or deposit-asset address, a missing or zero
|
|
2252
|
+
* amount, a vault the router cannot deposit into (a legacy `evm-0` pool, or
|
|
2253
|
+
* a Tokenized Vault V2 with a senders whitelist), a native deposit into a
|
|
2254
|
+
* non-wrapped-native vault, drift between
|
|
2255
|
+
* the aggregator quote and the on-chain router whitelist, or an origin code
|
|
2256
|
+
* not registered on the router (a per-call code, or the instance code under
|
|
2257
|
+
* `attribution.strictOrigin`), before any approval; with code
|
|
2258
|
+
* `ACCOUNT_NOT_FUNDED` when the account cannot cover gas.
|
|
2259
|
+
* @throws {@link UpshiftSDKError} while the SwapRouter is paused (checked before
|
|
2260
|
+
* any approval), or on unrecoverable contract or RPC failure.
|
|
2261
|
+
* @worstCaseRpcCalls 10 on the swap path into a Tokenized Vault V2 —
|
|
2262
|
+
* tokenized-vault metadata, reference asset + decimals, deposit-token
|
|
2263
|
+
* decimals, the vault's `assetsWhitelistAddress()`, `getWhitelistedAssets()`
|
|
2264
|
+
* and `sendersWhitelistAddress()`, the quote, the router's `isPaused()`,
|
|
2265
|
+
* the origin code's `origins` (none for the zero sentinel), and the
|
|
2266
|
+
* allowance read. The router reads, `getWhitelistedAssets()` and
|
|
2267
|
+
* `sendersWhitelistAddress()` are cached for 5 minutes, except `isPaused()`
|
|
2268
|
+
* (15 seconds).
|
|
2269
|
+
* @example
|
|
2270
|
+
* ```ts
|
|
2271
|
+
* // USDT deposited into a USDC-reference vault → swapped to USDC en route.
|
|
2272
|
+
* const hash = await upshiftSdk.evm.swapRouterDeposit({
|
|
2273
|
+
* chainId: 1,
|
|
2274
|
+
* vault: '0xe9b725010a9e419412ed67d0fa5f3a5f40159d32',
|
|
2275
|
+
* depositAsset: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
|
2276
|
+
* amount: '100',
|
|
2277
|
+
* slippageBps: 50,
|
|
2278
|
+
* });
|
|
2279
|
+
* ```
|
|
2280
|
+
*/
|
|
2281
|
+
async function swapRouterDeposit(signer, options) {
|
|
2282
|
+
try {
|
|
2283
|
+
return await swapRouterDepositCore(signer, options);
|
|
2284
|
+
}
|
|
2285
|
+
catch (e) {
|
|
2286
|
+
return handleSwapRouterWriteError(e, undefined, signer, options.wait, 'swapRouterDeposit', 'Swap router deposit', { chainId: options.chainId, vault: options.vault });
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
/** @internal */
|
|
2290
|
+
async function swapRouterDepositCore(signer, options) {
|
|
2291
|
+
const { chainId, vault, depositAsset, amount, receiver, slippageBps, originCode, wait, } = options;
|
|
2292
|
+
if (!(0, core_1.checkAddress)(vault, console, 'contract')) {
|
|
2293
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `swapRouterDeposit: invalid vault address "${vault}"`);
|
|
2294
|
+
}
|
|
2295
|
+
if (!(0, core_1.checkAddress)(depositAsset, console, 'contract')) {
|
|
2296
|
+
throw new core_1.UpshiftValidationError('INVALID_ADDRESS', `swapRouterDeposit: invalid depositAsset address "${depositAsset}"`);
|
|
2297
|
+
}
|
|
2298
|
+
if (amount === undefined || amount === null) {
|
|
2299
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'swapRouterDeposit: amount is required');
|
|
2300
|
+
}
|
|
2301
|
+
validateAmountPrecision(amount);
|
|
2302
|
+
// Explicit opt-in: resolve the router straight from chainId rather than
|
|
2303
|
+
// consulting VAULTS_USING_SWAP_ROUTER, so this path never depends on the
|
|
2304
|
+
// implicit allowlist that could route a native-deposit vault through a swap.
|
|
2305
|
+
const routerAddress = resolveSwapRouterOrThrow(chainId);
|
|
2306
|
+
const eoa = await resolveSignerEOA(signer, 'swapRouterDeposit');
|
|
2307
|
+
// Reference asset + decimals from the vault on-chain. Mirrors vaultDeposit's
|
|
2308
|
+
// metadata resolution so the deposit-token decimals (and therefore the raw
|
|
2309
|
+
// amount) match exactly; kept separate to leave the vaultDeposit money path
|
|
2310
|
+
// untouched.
|
|
2311
|
+
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, undefined, false, false))?.[0];
|
|
2312
|
+
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
2313
|
+
assertNotLegacyPool('swapRouterDeposit', vault, vaultVersion);
|
|
2314
|
+
const isMultiAssetVault = vaultVersion === 'evm-2';
|
|
2315
|
+
let underlyingAsset;
|
|
2316
|
+
let vaultDecimals;
|
|
2317
|
+
if (isMultiAssetVault) {
|
|
2318
|
+
const poolContract = (0, core_1.createContract)({
|
|
2319
|
+
address: vault,
|
|
2320
|
+
provider: signer,
|
|
2321
|
+
abi: abis_1.ABI_TOKENIZED_VAULT_V2,
|
|
2322
|
+
});
|
|
2323
|
+
const [asset, receiptTokenAddr] = await Promise.all([
|
|
2324
|
+
poolContract.asset(),
|
|
2325
|
+
// Retried on transport blips only; a misroute still throws. See
|
|
2326
|
+
// getReceiptTokenAddressOrThrow.
|
|
2327
|
+
(0, core_1.getReceiptTokenAddressOrThrow)(signer, vault, 'swapRouterDeposit:receiptToken'),
|
|
2328
|
+
]);
|
|
2329
|
+
underlyingAsset = asset;
|
|
2330
|
+
vaultDecimals = await (0, core_1.getDecimalsOrThrow)(signer, receiptTokenAddr, 'swapRouterDeposit:receiptDecimals');
|
|
2331
|
+
}
|
|
2332
|
+
else {
|
|
2333
|
+
const poolContract = (0, core_1.createContract)({
|
|
2334
|
+
address: vault,
|
|
2335
|
+
provider: signer,
|
|
2336
|
+
abi: abis_1.ABI_LENDING_POOLS,
|
|
2337
|
+
});
|
|
2338
|
+
const [asset, rawDecimals] = await Promise.all([
|
|
2339
|
+
poolContract.asset(),
|
|
2340
|
+
(0, core_1.getDecimalsOrThrow)(signer, vault, 'swapRouterDeposit:poolDecimals'),
|
|
2341
|
+
]);
|
|
2342
|
+
underlyingAsset = asset;
|
|
2343
|
+
vaultDecimals = rawDecimals;
|
|
2344
|
+
}
|
|
2345
|
+
const isNativeToken = depositAsset === ethers_1.ZeroAddress ||
|
|
2346
|
+
depositAsset === '0x0000000000000000000000000000000000000000';
|
|
2347
|
+
const depositTokenDecimals = await resolveDepositTokenDecimals({
|
|
2348
|
+
actualDepositAsset: depositAsset,
|
|
2349
|
+
underlyingAsset,
|
|
2350
|
+
isNativeToken,
|
|
2351
|
+
isMultiAssetVault,
|
|
2352
|
+
vaultDecimals,
|
|
2353
|
+
readErc20Decimals: async (addr) => (0, core_1.getDecimalsOrThrow)(signer, addr, 'swapRouterDeposit:depositTokenDecimals'),
|
|
2354
|
+
});
|
|
2355
|
+
const normalizedAmt = (0, core_1.toNormalizedBn)(amount, depositTokenDecimals);
|
|
2356
|
+
if (BigInt(normalizedAmt.raw) === 0n) {
|
|
2357
|
+
throw new core_1.UpshiftValidationError('INVALID_INPUT', 'swapRouterDeposit: amount must be greater than zero');
|
|
2358
|
+
}
|
|
2359
|
+
return dispatchViaSwapRouter({
|
|
2360
|
+
signer,
|
|
2361
|
+
routerAddress,
|
|
2362
|
+
chainId,
|
|
2363
|
+
target: vault,
|
|
2364
|
+
wallet: eoa,
|
|
2365
|
+
receiver,
|
|
2366
|
+
actualDepositAsset: depositAsset,
|
|
2367
|
+
underlyingAsset,
|
|
2368
|
+
isNativeToken,
|
|
2369
|
+
isMultiAssetVault,
|
|
2370
|
+
depositTokenDecimals,
|
|
2371
|
+
normalizedAmt,
|
|
2372
|
+
slippageBps,
|
|
2373
|
+
originCode,
|
|
2374
|
+
wait,
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
//# sourceMappingURL=write.actions.js.map
|