@upshiftfinance/sdk 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +408 -2
  3. package/lib/abis/AddressResolver.d.ts +28 -0
  4. package/lib/abis/AddressResolver.js +23 -0
  5. package/lib/abis/ChainlinkV3.d.ts +87 -0
  6. package/lib/abis/ChainlinkV3.js +117 -0
  7. package/lib/abis/ERC20.d.ts +168 -0
  8. package/lib/abis/ERC20.js +226 -0
  9. package/lib/abis/ERC20_Bytes32.d.ts +139 -0
  10. package/lib/abis/ERC20_Bytes32.js +196 -0
  11. package/lib/abis/ERC4626.d.ts +364 -0
  12. package/lib/abis/ERC4626.js +507 -0
  13. package/lib/abis/ERC721.d.ts +231 -0
  14. package/lib/abis/ERC721.js +321 -0
  15. package/lib/abis/FeeOracle.d.ts +120 -0
  16. package/lib/abis/FeeOracle.js +162 -0
  17. package/lib/abis/LendingPool.d.ts +1393 -0
  18. package/lib/abis/LendingPool.js +1807 -0
  19. package/lib/abis/LendingPoolV2.d.ts +1413 -0
  20. package/lib/abis/LendingPoolV2.js +1833 -0
  21. package/lib/abis/LendingPoolV3.d.ts +1677 -0
  22. package/lib/abis/LendingPoolV3.js +1160 -0
  23. package/lib/abis/Loan.d.ts +837 -0
  24. package/lib/abis/Loan.js +1080 -0
  25. package/lib/abis/MultiAssetNativeDepositWrapper.d.ts +137 -0
  26. package/lib/abis/MultiAssetNativeDepositWrapper.js +125 -0
  27. package/lib/abis/Multicall3.d.ts +30 -0
  28. package/lib/abis/Multicall3.js +97 -0
  29. package/lib/abis/OFT.d.ts +116 -0
  30. package/lib/abis/OFT.js +85 -0
  31. package/lib/abis/PoolAdapter.d.ts +36 -0
  32. package/lib/abis/PoolAdapter.js +51 -0
  33. package/lib/abis/RewardDistributor.d.ts +267 -0
  34. package/lib/abis/RewardDistributor.js +352 -0
  35. package/lib/abis/RwaRedeemSubaccount.d.ts +747 -0
  36. package/lib/abis/RwaRedeemSubaccount.js +548 -0
  37. package/lib/abis/SmartAccount.d.ts +17 -0
  38. package/lib/abis/SmartAccount.js +19 -0
  39. package/lib/abis/SwapRouter.d.ts +953 -0
  40. package/lib/abis/SwapRouter.js +1228 -0
  41. package/lib/abis/TextResolver.d.ts +16 -0
  42. package/lib/abis/TextResolver.js +16 -0
  43. package/lib/abis/TokenizedVaultV2.d.ts +1364 -0
  44. package/lib/abis/TokenizedVaultV2.js +1041 -0
  45. package/lib/abis/TokenizedVaultV2DepositWithPermit.d.ts +1456 -0
  46. package/lib/abis/TokenizedVaultV2DepositWithPermit.js +1878 -0
  47. package/lib/abis/TokenizedVaultV2Receipt.d.ts +1568 -0
  48. package/lib/abis/TokenizedVaultV2Receipt.js +1061 -0
  49. package/lib/abis/TokenizedVaultV2SenderAllocationWhitelist.d.ts +454 -0
  50. package/lib/abis/TokenizedVaultV2SenderAllocationWhitelist.js +327 -0
  51. package/lib/abis/TokenizedVaultV2WhitelistedAllocation.d.ts +1466 -0
  52. package/lib/abis/TokenizedVaultV2WhitelistedAllocation.js +1092 -0
  53. package/lib/abis/TokenizedVaultV2WhitelistedAssets.d.ts +274 -0
  54. package/lib/abis/TokenizedVaultV2WhitelistedAssets.js +167 -0
  55. package/lib/abis/UniversalResolverResolve.d.ts +69 -0
  56. package/lib/abis/UniversalResolverResolve.js +35 -0
  57. package/lib/abis/UniversalSignatureValidator.d.ts +17 -0
  58. package/lib/abis/UniversalSignatureValidator.js +30 -0
  59. package/lib/abis/WrapperAdapter.d.ts +71 -0
  60. package/lib/abis/WrapperAdapter.js +77 -0
  61. package/lib/abis/index.d.ts +34 -0
  62. package/lib/abis/index.js +51 -0
  63. package/lib/adapters/evm/getters.d.ts +19 -0
  64. package/lib/adapters/evm/getters.js +249 -0
  65. package/lib/adapters/evm/index.d.ts +539 -0
  66. package/lib/adapters/evm/index.js +656 -0
  67. package/lib/adapters/evm/utils.d.ts +8 -0
  68. package/lib/adapters/evm/utils.js +51 -0
  69. package/lib/adapters/solana/confirm.d.ts +88 -0
  70. package/lib/adapters/solana/confirm.js +291 -0
  71. package/lib/adapters/solana/constants.d.ts +42 -0
  72. package/lib/adapters/solana/constants.js +59 -0
  73. package/lib/adapters/solana/getters.d.ts +42 -0
  74. package/lib/adapters/solana/getters.js +275 -0
  75. package/lib/adapters/solana/idl/vault-idl.d.ts +700 -0
  76. package/lib/adapters/solana/idl/vault-idl.js +2101 -0
  77. package/lib/adapters/solana/idl/withdrawal-queue-idl.d.ts +795 -0
  78. package/lib/adapters/solana/idl/withdrawal-queue-idl.js +1864 -0
  79. package/lib/adapters/solana/index.d.ts +372 -0
  80. package/lib/adapters/solana/index.js +459 -0
  81. package/lib/adapters/solana/pricing.d.ts +184 -0
  82. package/lib/adapters/solana/pricing.js +239 -0
  83. package/lib/adapters/solana/queue-hints.d.ts +29 -0
  84. package/lib/adapters/solana/queue-hints.js +59 -0
  85. package/lib/adapters/solana/queue.actions.d.ts +97 -0
  86. package/lib/adapters/solana/queue.actions.js +468 -0
  87. package/lib/adapters/solana/token-reads.d.ts +108 -0
  88. package/lib/adapters/solana/token-reads.js +145 -0
  89. package/lib/adapters/solana/types.d.ts +325 -0
  90. package/lib/adapters/solana/types.js +3 -0
  91. package/lib/adapters/solana/utils.d.ts +229 -0
  92. package/lib/adapters/solana/utils.js +773 -0
  93. package/lib/adapters/solana/vault.actions.d.ts +140 -0
  94. package/lib/adapters/solana/vault.actions.js +657 -0
  95. package/lib/adapters/solana/withdrawal-queue.d.ts +125 -0
  96. package/lib/adapters/solana/withdrawal-queue.js +301 -0
  97. package/lib/adapters/stellar/actions.d.ts +41 -0
  98. package/lib/adapters/stellar/actions.js +126 -0
  99. package/lib/adapters/stellar/constants.d.ts +43 -0
  100. package/lib/adapters/stellar/constants.js +53 -0
  101. package/lib/adapters/stellar/getters.d.ts +68 -0
  102. package/lib/adapters/stellar/getters.js +290 -0
  103. package/lib/adapters/stellar/index.d.ts +126 -0
  104. package/lib/adapters/stellar/index.js +187 -0
  105. package/lib/adapters/stellar/invocation.d.ts +55 -0
  106. package/lib/adapters/stellar/invocation.js +92 -0
  107. package/lib/adapters/stellar/soroban.d.ts +131 -0
  108. package/lib/adapters/stellar/soroban.js +617 -0
  109. package/lib/adapters/stellar/submit.d.ts +42 -0
  110. package/lib/adapters/stellar/submit.js +195 -0
  111. package/lib/adapters/stellar/types.d.ts +57 -0
  112. package/lib/adapters/stellar/types.js +6 -0
  113. package/lib/adapters/stellar/utils.d.ts +24 -0
  114. package/lib/adapters/stellar/utils.js +34 -0
  115. package/lib/adapters/sui/client.d.ts +83 -0
  116. package/lib/adapters/sui/client.js +216 -0
  117. package/lib/adapters/sui/constants.d.ts +33 -0
  118. package/lib/adapters/sui/constants.js +31 -0
  119. package/lib/adapters/sui/index.d.ts +57 -0
  120. package/lib/adapters/sui/index.js +100 -0
  121. package/lib/adapters/sui/targets.d.ts +171 -0
  122. package/lib/adapters/sui/targets.js +225 -0
  123. package/lib/adapters/sui/utils.d.ts +8 -0
  124. package/lib/adapters/sui/utils.js +17 -0
  125. package/lib/core/analytics/app-name.d.ts +27 -0
  126. package/lib/core/analytics/app-name.js +48 -0
  127. package/lib/core/analytics/chain-name.d.ts +9 -0
  128. package/lib/core/analytics/chain-name.js +34 -0
  129. package/lib/core/analytics/constants.d.ts +15 -0
  130. package/lib/core/analytics/constants.js +28 -0
  131. package/lib/core/analytics/env.d.ts +46 -0
  132. package/lib/core/analytics/env.js +147 -0
  133. package/lib/core/analytics/index.d.ts +39 -0
  134. package/lib/core/analytics/index.js +83 -0
  135. package/lib/core/analytics/instrumentation.d.ts +29 -0
  136. package/lib/core/analytics/instrumentation.js +292 -0
  137. package/lib/core/analytics/method-taxonomy.d.ts +19 -0
  138. package/lib/core/analytics/method-taxonomy.js +156 -0
  139. package/lib/core/analytics/metrics.d.ts +48 -0
  140. package/lib/core/analytics/metrics.js +142 -0
  141. package/lib/core/analytics/sanitize.d.ts +57 -0
  142. package/lib/core/analytics/sanitize.js +272 -0
  143. package/lib/core/analytics/sentry-runtime.d.ts +15 -0
  144. package/lib/core/analytics/sentry-runtime.js +131 -0
  145. package/lib/core/analytics/sentry.d.ts +100 -0
  146. package/lib/core/analytics/sentry.js +696 -0
  147. package/lib/core/analytics/types.d.ts +55 -0
  148. package/lib/core/analytics/types.js +3 -0
  149. package/lib/core/analytics/user-identity.d.ts +55 -0
  150. package/lib/core/analytics/user-identity.js +218 -0
  151. package/lib/core/analytics/version.d.ts +6 -0
  152. package/lib/core/analytics/version.js +10 -0
  153. package/lib/core/attribution.d.ts +159 -0
  154. package/lib/core/attribution.js +194 -0
  155. package/lib/core/auth/index.d.ts +1 -0
  156. package/lib/core/auth/index.js +18 -0
  157. package/lib/core/auth/verify.d.ts +2 -0
  158. package/lib/core/auth/verify.js +35 -0
  159. package/lib/core/base.class.d.ts +193 -0
  160. package/lib/core/base.class.js +163 -0
  161. package/lib/core/cache.d.ts +9 -0
  162. package/lib/core/cache.js +31 -0
  163. package/lib/core/constants/adapters.d.ts +103 -0
  164. package/lib/core/constants/adapters.js +180 -0
  165. package/lib/core/constants/core.d.ts +162 -0
  166. package/lib/core/constants/core.js +281 -0
  167. package/lib/core/constants/swap-router.d.ts +180 -0
  168. package/lib/core/constants/swap-router.js +203 -0
  169. package/lib/core/constants/vaults.d.ts +93 -0
  170. package/lib/core/constants/vaults.js +277 -0
  171. package/lib/core/constants/web3.d.ts +106 -0
  172. package/lib/core/constants/web3.js +251 -0
  173. package/lib/core/errors/index.d.ts +177 -0
  174. package/lib/core/errors/index.js +257 -0
  175. package/lib/core/fetcher.d.ts +237 -0
  176. package/lib/core/fetcher.js +1103 -0
  177. package/lib/core/helpers/adapters.d.ts +13 -0
  178. package/lib/core/helpers/adapters.js +39 -0
  179. package/lib/core/helpers/chain-address.d.ts +13 -0
  180. package/lib/core/helpers/chain-address.js +47 -0
  181. package/lib/core/helpers/chain-error.d.ts +207 -0
  182. package/lib/core/helpers/chain-error.js +697 -0
  183. package/lib/core/helpers/chain-support.d.ts +98 -0
  184. package/lib/core/helpers/chain-support.js +139 -0
  185. package/lib/core/helpers/concurrency.d.ts +51 -0
  186. package/lib/core/helpers/concurrency.js +79 -0
  187. package/lib/core/helpers/core.d.ts +92 -0
  188. package/lib/core/helpers/core.js +368 -0
  189. package/lib/core/helpers/explorer-link.d.ts +16 -0
  190. package/lib/core/helpers/explorer-link.js +26 -0
  191. package/lib/core/helpers/multicall.d.ts +68 -0
  192. package/lib/core/helpers/multicall.js +103 -0
  193. package/lib/core/helpers/revert-decode.d.ts +248 -0
  194. package/lib/core/helpers/revert-decode.js +515 -0
  195. package/lib/core/helpers/signer.d.ts +61 -0
  196. package/lib/core/helpers/signer.js +184 -0
  197. package/lib/core/helpers/swap-router.d.ts +269 -0
  198. package/lib/core/helpers/swap-router.js +542 -0
  199. package/lib/core/helpers/vault-version.d.ts +24 -0
  200. package/lib/core/helpers/vault-version.js +83 -0
  201. package/lib/core/helpers/vaults.d.ts +89 -0
  202. package/lib/core/helpers/vaults.js +241 -0
  203. package/lib/core/helpers/web3.d.ts +366 -0
  204. package/lib/core/helpers/web3.js +1034 -0
  205. package/lib/core/index.d.ts +23 -0
  206. package/lib/core/index.js +40 -0
  207. package/lib/core/logger/curator-alert.d.ts +217 -0
  208. package/lib/core/logger/curator-alert.js +276 -0
  209. package/lib/core/logger/index.d.ts +99 -0
  210. package/lib/core/logger/index.js +163 -0
  211. package/lib/core/logger/slack.d.ts +49 -0
  212. package/lib/core/logger/slack.js +111 -0
  213. package/lib/core/vault-metadata.d.ts +12 -0
  214. package/lib/core/vault-metadata.js +42 -0
  215. package/lib/core/version-check.d.ts +58 -0
  216. package/lib/core/version-check.js +182 -0
  217. package/lib/evm/index.d.ts +2 -0
  218. package/lib/evm/index.js +19 -0
  219. package/lib/evm/methods/crossChainVault.d.ts +132 -0
  220. package/lib/evm/methods/crossChainVault.js +857 -0
  221. package/lib/evm/methods/crossChainVaultRegistry.d.ts +92 -0
  222. package/lib/evm/methods/crossChainVaultRegistry.js +239 -0
  223. package/lib/evm/methods/index.d.ts +2 -0
  224. package/lib/evm/methods/index.js +19 -0
  225. package/lib/evm/types/crossChain.d.ts +363 -0
  226. package/lib/evm/types/crossChain.js +20 -0
  227. package/lib/evm/types/index.d.ts +1 -0
  228. package/lib/evm/types/index.js +18 -0
  229. package/lib/index.d.ts +30 -0
  230. package/lib/index.js +52 -0
  231. package/lib/main.d.ts +596 -0
  232. package/lib/main.js +681 -0
  233. package/lib/modules/api/fetcher.d.ts +108 -0
  234. package/lib/modules/api/fetcher.js +187 -0
  235. package/lib/modules/api/index.d.ts +1 -0
  236. package/lib/modules/api/index.js +6 -0
  237. package/lib/modules/api/main.d.ts +587 -0
  238. package/lib/modules/api/main.js +911 -0
  239. package/lib/modules/sub-accounts/fetcher.d.ts +53 -0
  240. package/lib/modules/sub-accounts/fetcher.js +96 -0
  241. package/lib/modules/sub-accounts/index.d.ts +2 -0
  242. package/lib/modules/sub-accounts/index.js +19 -0
  243. package/lib/modules/sub-accounts/main.d.ts +249 -0
  244. package/lib/modules/sub-accounts/main.js +211 -0
  245. package/lib/modules/sub-accounts/utils.d.ts +106 -0
  246. package/lib/modules/sub-accounts/utils.js +112 -0
  247. package/lib/modules/vaults/adapter.helpers.d.ts +84 -0
  248. package/lib/modules/vaults/adapter.helpers.js +202 -0
  249. package/lib/modules/vaults/fetcher.d.ts +147 -0
  250. package/lib/modules/vaults/fetcher.js +357 -0
  251. package/lib/modules/vaults/getters.d.ts +1060 -0
  252. package/lib/modules/vaults/getters.js +4079 -0
  253. package/lib/modules/vaults/index.d.ts +20 -0
  254. package/lib/modules/vaults/index.js +44 -0
  255. package/lib/modules/vaults/main.d.ts +607 -0
  256. package/lib/modules/vaults/main.js +1687 -0
  257. package/lib/modules/vaults/prefetch.d.ts +65 -0
  258. package/lib/modules/vaults/prefetch.js +120 -0
  259. package/lib/modules/vaults/read.actions.d.ts +406 -0
  260. package/lib/modules/vaults/read.actions.js +891 -0
  261. package/lib/modules/vaults/types.d.ts +149 -0
  262. package/lib/modules/vaults/types.js +3 -0
  263. package/lib/modules/vaults/utils/call-data-decoder.d.ts +61 -0
  264. package/lib/modules/vaults/utils/call-data-decoder.js +194 -0
  265. package/lib/modules/vaults/utils/date-utils.d.ts +50 -0
  266. package/lib/modules/vaults/utils/date-utils.js +84 -0
  267. package/lib/modules/vaults/utils.d.ts +153 -0
  268. package/lib/modules/vaults/utils.js +818 -0
  269. package/lib/modules/vaults/write.actions.d.ts +812 -0
  270. package/lib/modules/vaults/write.actions.js +2377 -0
  271. package/lib/polyfills.d.ts +1 -0
  272. package/lib/polyfills.js +12 -0
  273. package/lib/sdk.d.ts +30173 -0
  274. package/lib/services/coingecko/fetcher.d.ts +15 -0
  275. package/lib/services/coingecko/fetcher.js +67 -0
  276. package/lib/services/coingecko/index.d.ts +2 -0
  277. package/lib/services/coingecko/index.js +19 -0
  278. package/lib/services/coingecko/utils.d.ts +1 -0
  279. package/lib/services/coingecko/utils.js +24 -0
  280. package/lib/services/debank/fetcher.d.ts +126 -0
  281. package/lib/services/debank/fetcher.js +47 -0
  282. package/lib/services/debank/index.d.ts +2 -0
  283. package/lib/services/debank/index.js +19 -0
  284. package/lib/services/debank/utils.d.ts +50 -0
  285. package/lib/services/debank/utils.js +325 -0
  286. package/lib/services/layerzero/deposits.d.ts +55 -0
  287. package/lib/services/layerzero/deposits.js +174 -0
  288. package/lib/services/layerzero/redeems.d.ts +20 -0
  289. package/lib/services/layerzero/redeems.js +92 -0
  290. package/lib/services/layerzero/utils.d.ts +9 -0
  291. package/lib/services/layerzero/utils.js +22 -0
  292. package/lib/services/octavfi/fetcher.d.ts +33 -0
  293. package/lib/services/octavfi/fetcher.js +39 -0
  294. package/lib/services/octavfi/index.d.ts +3 -0
  295. package/lib/services/octavfi/index.js +20 -0
  296. package/lib/services/octavfi/types.d.ts +34 -0
  297. package/lib/services/octavfi/types.js +3 -0
  298. package/lib/services/octavfi/utils.d.ts +16 -0
  299. package/lib/services/octavfi/utils.js +244 -0
  300. package/lib/services/subgraph/fetcher.d.ts +2 -0
  301. package/lib/services/subgraph/fetcher.js +66 -0
  302. package/lib/services/subgraph/index.d.ts +2 -0
  303. package/lib/services/subgraph/index.js +19 -0
  304. package/lib/services/subgraph/schema.d.ts +48 -0
  305. package/lib/services/subgraph/schema.js +87 -0
  306. package/lib/services/subgraph/vaults.d.ts +83 -0
  307. package/lib/services/subgraph/vaults.js +1282 -0
  308. package/lib/services/swap-quotes/index.d.ts +105 -0
  309. package/lib/services/swap-quotes/index.js +77 -0
  310. package/lib/services/swap-quotes/paraswap.d.ts +51 -0
  311. package/lib/services/swap-quotes/paraswap.js +138 -0
  312. package/lib/services/untangled/fetcher.d.ts +40 -0
  313. package/lib/services/untangled/fetcher.js +110 -0
  314. package/lib/services/untangled/index.d.ts +3 -0
  315. package/lib/services/untangled/index.js +20 -0
  316. package/lib/services/untangled/types.d.ts +83 -0
  317. package/lib/services/untangled/types.js +10 -0
  318. package/lib/services/untangled/utils.d.ts +91 -0
  319. package/lib/services/untangled/utils.js +339 -0
  320. package/lib/types/api.d.ts +498 -0
  321. package/lib/types/api.js +3 -0
  322. package/lib/types/index.d.ts +12 -0
  323. package/lib/types/index.js +28 -0
  324. package/lib/types/points.d.ts +28 -0
  325. package/lib/types/points.js +3 -0
  326. package/lib/types/pools.d.ts +144 -0
  327. package/lib/types/pools.js +3 -0
  328. package/lib/types/staking.d.ts +28 -0
  329. package/lib/types/staking.js +3 -0
  330. package/lib/types/sub-accounts.d.ts +150 -0
  331. package/lib/types/sub-accounts.js +3 -0
  332. package/lib/types/subgraph.d.ts +84 -0
  333. package/lib/types/subgraph.js +3 -0
  334. package/lib/types/typed-contract.d.ts +102 -0
  335. package/lib/types/typed-contract.js +3 -0
  336. package/lib/types/user.d.ts +1 -0
  337. package/lib/types/user.js +3 -0
  338. package/lib/types/vaults.d.ts +886 -0
  339. package/lib/types/vaults.js +30 -0
  340. package/lib/types/web3.d.ts +100 -0
  341. package/lib/types/web3.js +14 -0
  342. package/lib/types/webserver.d.ts +730 -0
  343. package/lib/types/webserver.js +3 -0
  344. package/package.json +79 -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