From 8ff684ff3eaab98fcecf1ca0cf25cc2970874138 Mon Sep 17 00:00:00 2001 From: 0xdef1cafe <88504456+0xdef1cafe@users.noreply.github.com> Date: Fri, 21 Apr 2023 18:29:07 -0600 Subject: [PATCH 01/12] perf: don't compute icons for dynamic LP pairs (#4340) --- src/state/slices/assetsSlice/assetsSlice.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/state/slices/assetsSlice/assetsSlice.ts b/src/state/slices/assetsSlice/assetsSlice.ts index 17e08ec5ba1..e4e4cc3fc33 100644 --- a/src/state/slices/assetsSlice/assetsSlice.ts +++ b/src/state/slices/assetsSlice/assetsSlice.ts @@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import { createApi } from '@reduxjs/toolkit/dist/query/react' import type { Asset } from '@shapeshiftoss/asset-service' -import { AssetService, getRenderedIdenticonBase64 } from '@shapeshiftoss/asset-service' +import { AssetService } from '@shapeshiftoss/asset-service' import type { AssetId } from '@shapeshiftoss/caip' import { bscChainId, @@ -79,10 +79,8 @@ export const makeAsset = (minimalAsset: MinimalAsset): Asset => { return fromAssetId(assetId).chainId })() - const icon = (() => { - if (minimalAsset.icon) return minimalAsset.icon - return getRenderedIdenticonBase64(assetId) - })() + // currently, dynamic assets are LP pairs, and they have two icon urls and are rendered differently + const icon = minimalAsset?.icon ?? '' type ExplorerLinks = Pick @@ -97,13 +95,7 @@ export const makeAsset = (minimalAsset: MinimalAsset): Asset => { } })() - return { - ...minimalAsset, - ...explorerLinks, - chainId, - color, - icon, - } + return Object.assign({}, minimalAsset, explorerLinks, { chainId, color, icon }) } export const assets = createSlice({ From c8e33858f7ec8bc3db3fd78dd7c94733e0b2e68b Mon Sep 17 00:00:00 2001 From: Highlander Date: Tue, 25 Apr 2023 06:45:55 -0500 Subject: [PATCH 02/12] chore: update readme to match package.json (#4345) Co-authored-by: gomes <17035424+gomesalexandre@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a8577f0479..bc225aa1518 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ If you are using Linux and macOS it works out of the box following these steps: 5. Build Packages: ```sh - yarn build + yarn build:packages ``` 6. Run `yarn env dev` to generate a `.env` file. From da72eeaae68316fdffe134700e503581371a8ddc Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 25 Apr 2023 15:25:16 +0200 Subject: [PATCH 03/12] feat: disable yearn asset generation (#4346) --- .../src/generateAssetData/ethereum/index.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/asset-service/src/generateAssetData/ethereum/index.ts b/packages/asset-service/src/generateAssetData/ethereum/index.ts index db006c0f536..6bc050d2d72 100644 --- a/packages/asset-service/src/generateAssetData/ethereum/index.ts +++ b/packages/asset-service/src/generateAssetData/ethereum/index.ts @@ -13,7 +13,8 @@ import { ethereum } from '../baseAssets' import * as coingecko from '../coingecko' import { getIdleTokens } from './idleVaults' import { getUniswapV2Pools } from './uniswapV2Pools' -import { getUnderlyingVaultTokens, getYearnVaults, getZapperTokens } from './yearnVaults' +// Yearn SDK is currently rugged upstream +// import { getUnderlyingVaultTokens, getYearnVaults, getZapperTokens } from './yearnVaults' const foxyToken: Asset = { assetId: toAssetId({ @@ -33,22 +34,21 @@ const foxyToken: Asset = { } export const getAssets = async (): Promise => { - const [ethTokens, yearnVaults, zapperTokens, underlyingTokens, uniV2PoolTokens, idleTokens] = - await Promise.all([ - coingecko.getAssets(ethChainId), - getYearnVaults(), - getZapperTokens(), - getUnderlyingVaultTokens(), - getUniswapV2Pools(), - getIdleTokens(), - ]) + const [ethTokens, uniV2PoolTokens, idleTokens] = await Promise.all([ + coingecko.getAssets(ethChainId), + // getYearnVaults(), + // getZapperTokens(), + // getUnderlyingVaultTokens(), + getUniswapV2Pools(), + getIdleTokens(), + ]) const ethAssets = [ foxyToken, ...ethTokens, - ...yearnVaults, - ...zapperTokens, - ...underlyingTokens, + // ...yearnVaults, + // ...zapperTokens, + // ...underlyingTokens, ...uniV2PoolTokens, ...idleTokens, ] From 1047a1aabd2795947a2d7b5f083f38f5c449f6b7 Mon Sep 17 00:00:00 2001 From: kevin <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 25 Apr 2023 12:33:09 -0600 Subject: [PATCH 04/12] feat: eip1559 fees (#4324) --- jest.config.js | 20 - package.json | 33 +- .../chain-adapters/src/evm/EvmBaseAdapter.ts | 3 +- .../unchained-client/src/evm/parser/index.ts | 7 - src/features/defi/helpers/utils.ts | 92 +++- .../FoxFarmingManager/Claim/ClaimConfirm.tsx | 10 +- .../Deposit/components/Approve.tsx | 10 +- .../Deposit/components/Deposit.tsx | 20 +- .../Withdraw/components/Approve.tsx | 10 +- .../Withdraw/components/ExpiredWithdraw.tsx | 14 +- .../Withdraw/components/Withdraw.tsx | 18 +- .../fox-farming/hooks/useFoxFarming.ts | 436 +++++++----------- .../Deposit/components/Approve.tsx | 11 +- .../Deposit/components/Deposit.tsx | 28 +- .../Withdraw/components/Approve.tsx | 11 +- .../Withdraw/components/Withdraw.tsx | 14 +- .../univ2/hooks/useUniV2LiquidityPool.ts | 391 ++++++---------- src/lib/swapper/api.ts | 81 ++-- .../CowSwapper/cowApprove/cowApprove.ts | 68 +-- .../cowBuildTrade/cowBuildTrade.test.ts | 16 +- .../CowSwapper/cowBuildTrade/cowBuildTrade.ts | 110 +---- .../getCowSwapTradeQuote.test.ts | 12 +- .../getCowSwapTradeQuote.ts | 75 ++- .../swappers/LifiSwapper/approve/approve.ts | 34 +- .../buildThorTrade/buildThorTrade.ts | 9 +- .../ThorchainSwapper/evm/makeTradeTx.ts | 39 +- .../getThorTradeQuote/getTradeQuote.test.ts | 2 + .../getThorTradeQuote/getTradeQuote.ts | 3 +- .../thorTradeApproveInfinite.ts | 74 +-- .../txFeeHelpers/evmTxFees/getEvmTxFees.ts | 36 +- .../swapper/swappers/ZrxSwapper/ZrxSwapper.ts | 6 +- .../getZrxTradeQuote/getZrxTradeQuote.test.ts | 68 ++- .../getZrxTradeQuote/getZrxTradeQuote.ts | 123 ++--- src/lib/swapper/swappers/ZrxSwapper/types.ts | 8 +- .../ZrxSwapper/utils/helpers/helpers.ts | 32 +- .../utils/test-data/setupZrxSwapQuote.ts | 2 + .../zrxApprovalNeeded.test.ts | 2 +- .../zrxApprovalNeeded/zrxApprovalNeeded.ts | 45 +- .../ZrxSwapper/zrxApprove/zrxApprove.ts | 73 +-- .../zrxBuildTrade/zrxBuildTrade.test.ts | 32 +- .../ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts | 124 ++--- .../zrxExecuteTrade/zrxExecuteTrade.test.ts | 6 +- .../zrxExecuteTrade/zrxExecuteTrade.ts | 57 +-- .../swappers/utils/helpers/helpers.test.ts | 26 +- .../swapper/swappers/utils/helpers/helpers.ts | 159 +++++-- .../swappers/utils/test-data/setupDeps.ts | 44 ++ .../utils/test-data/setupSwapQuote.ts | 6 +- .../v1/useApprovalHandler.tsx | 5 +- .../v2/utils/EIP155RequestHandlerUtil.ts | 6 +- 49 files changed, 1082 insertions(+), 1429 deletions(-) delete mode 100644 jest.config.js diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index c606a144281..00000000000 --- a/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testPathIgnorePatterns: ['/node_modules/', '.d.ts', '.js', '__mocks__', 'mockData'], - clearMocks: true, - roots: [''], - collectCoverage: false, - setupFiles: ['/.jest/setup.js'], - coverageDirectory: 'coverage', - coveragePathIgnorePatterns: ['/node_modules/', 'dist', '__mocks__', 'mockData'], - moduleNameMapper: { - '^@shapeshiftoss\\/([^/]+)': ['@shapeshiftoss/$1/src', '@shapeshiftoss/$1'], - }, - globals: { - 'ts-jest': { - sourceMap: true, - isolatedModules: true, - }, - }, -} diff --git a/package.json b/package.json index b7dd7a1b196..d78ff532191 100644 --- a/package.json +++ b/package.json @@ -272,6 +272,35 @@ "react-dom@^18.2.0": "patch:react-dom@npm%3A18.2.0#./.yarn/patches/react-dom-npm-18.2.0-dd675bca1c.patch" }, "jest": { - "resetMocks": false + "preset": "ts-jest", + "testEnvironment": "node", + "testPathIgnorePatterns": [ + "/node_modules/", + ".d.ts", + ".js", + "__mocks__", + "mockData" + ], + "clearMocks": true, + "resetMocks": false, + "roots": [ + "" + ], + "collectCoverage": false, + "setupFiles": [ + "/.jest/setup.js" + ], + "moduleNameMapper": { + "^@shapeshiftoss\\/([^/ ]+)": [ + "@shapeshiftoss/$1", + "@shapeshiftoss/$1/src" + ] + }, + "globals": { + "ts-jest": { + "sourceMap": true, + "isolatedModules": true + } + } } -} +} \ No newline at end of file diff --git a/packages/chain-adapters/src/evm/EvmBaseAdapter.ts b/packages/chain-adapters/src/evm/EvmBaseAdapter.ts index 60875bd0d21..a814b32c78a 100644 --- a/packages/chain-adapters/src/evm/EvmBaseAdapter.ts +++ b/packages/chain-adapters/src/evm/EvmBaseAdapter.ts @@ -37,7 +37,6 @@ import type { import { ValidAddressResultType } from '../types' import { chainIdToChainLabel, - convertNumberToHex, getAssetNamespace, toAddressNList, toRootDerivationPath, @@ -548,7 +547,7 @@ export abstract class EvmBaseAdapter implements IChainAdap const bip44Params = this.getBIP44Params({ accountNumber }) const txToSign = { addressNList: toAddressNList(bip44Params), - value: convertNumberToHex(value), + value: numberToHex(value), to, chainId: Number(fromChainId(this.chainId).chainReference), data, diff --git a/packages/unchained-client/src/evm/parser/index.ts b/packages/unchained-client/src/evm/parser/index.ts index f869b80c4df..9c1c0d1c269 100644 --- a/packages/unchained-client/src/evm/parser/index.ts +++ b/packages/unchained-client/src/evm/parser/index.ts @@ -1,6 +1,5 @@ import type { AssetId, ChainId } from '@shapeshiftoss/caip' import { ASSET_NAMESPACE, ASSET_REFERENCE, ethChainId, toAssetId } from '@shapeshiftoss/caip' -import { Logger } from '@shapeshiftoss/logger' import { BigNumber } from 'bignumber.js' import { ethers } from 'ethers' @@ -12,11 +11,6 @@ import type { ParsedTx, SubParser, Tx, TxSpecific } from './types' export * from './types' export * from './utils' -const logger = new Logger({ - namespace: ['unchained-client', 'evm', 'parser'], - level: process.env.LOG_LEVEL, -}) - export interface TransactionParserArgs { chainId: ChainId assetId: AssetId @@ -170,7 +164,6 @@ export class BaseTransactionParser { case 'BEP721': return ASSET_NAMESPACE.bep721 default: - logger.warn(`unsupported asset namespace: ${transfer.type}`) return } })() diff --git a/src/features/defi/helpers/utils.ts b/src/features/defi/helpers/utils.ts index 64e5bae9a9f..386df394559 100644 --- a/src/features/defi/helpers/utils.ts +++ b/src/features/defi/helpers/utils.ts @@ -1,7 +1,16 @@ import type { Asset } from '@shapeshiftoss/asset-service' import type { AccountId, ChainId } from '@shapeshiftoss/caip' import { cosmosChainId, osmosisChainId } from '@shapeshiftoss/caip' -import { bnOrZero } from 'lib/bignumber/bignumber' +import type { + evm, + EvmChainAdapter, + EvmChainId, + FeeData, + FeeDataEstimate, +} from '@shapeshiftoss/chain-adapters' +import type { HDWallet } from '@shapeshiftoss/hdwallet-core' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' +import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import { selectPortfolioCryptoPrecisionBalanceByFilter } from 'state/slices/selectors' import { store } from 'state/store' @@ -34,3 +43,84 @@ export const canCoverTxFees = ({ return bnOrZero(feeAssetBalanceCryptoHuman).minus(bnOrZero(estimatedGasCryptoPrecision)).gte(0) } + +export const getFeeDataFromEstimate = ({ + average, + fast, +}: FeeDataEstimate): FeeData => ({ + txFee: BigNumber.max( + average.txFee, + bnOrZero(bn(fast.chainSpecific.gasPrice).times(average.chainSpecific.gasLimit)), + ).toFixed(0), // use worst case average eip1559 vs fast legacy + chainSpecific: { + gasLimit: average.chainSpecific.gasLimit, // average and fast gasLimit values are the same + gasPrice: fast.chainSpecific.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.chainSpecific.maxFeePerGas, + maxPriorityFeePerGas: average.chainSpecific.maxPriorityFeePerGas, + }, +}) + +type GetFeesFromFeeDataArgs = { + wallet: HDWallet + feeData: evm.FeeData +} + +export const getFeesFromFeeData = async ({ + wallet, + feeData: { gasLimit, gasPrice, maxFeePerGas, maxPriorityFeePerGas }, +}: GetFeesFromFeeDataArgs): Promise => { + if (!supportsETH(wallet)) throw new Error('wallet has no evm support') + if (!gasLimit) throw new Error('gasLimit is required') + + const supportsEip1559 = await wallet.ethSupportsEIP1559() + + // use eip1559 fees if able + if (supportsEip1559 && maxFeePerGas && maxPriorityFeePerGas) { + return { gasLimit, maxFeePerGas, maxPriorityFeePerGas } + } + + // fallback to legacy fees if unable to use eip1559 + if (gasPrice) return { gasLimit, gasPrice } + + throw new Error('legacy gas or eip1559 gas required') +} + +type BuildAndBroadcastArgs = GetFeesFromFeeDataArgs & { + accountNumber: number + adapter: EvmChainAdapter + data: string + to: string + value: string +} + +export const buildAndBroadcast = async ({ + accountNumber, + adapter, + data, + feeData, + to, + value, + wallet, +}: BuildAndBroadcastArgs) => { + const { txToSign } = await adapter.buildCustomTx({ + wallet, + to, + accountNumber, + value, + data, + ...(await getFeesFromFeeData({ wallet, feeData })), + }) + + if (wallet.supportsOfflineSigning()) { + const signedTx = await adapter.signTransaction({ txToSign, wallet }) + const txid = await adapter.broadcastTransaction(signedTx) + return txid + } + + if (wallet.supportsBroadcast() && adapter.signAndBroadcastTransaction) { + const txid = await adapter.signAndBroadcastTransaction({ txToSign, wallet }) + return txid + } + + throw new Error('buildAndBroadcast: no broadcast support') +} diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Claim/ClaimConfirm.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Claim/ClaimConfirm.tsx index 3c580eb6307..73eed33267a 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Claim/ClaimConfirm.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Claim/ClaimConfirm.tsx @@ -70,7 +70,7 @@ export const ClaimConfirm = ({ accountId, assetId, amount, onBack }: ClaimConfir assertIsFoxEthStakingContractAddress(contractAddress) - const { claimRewards, getClaimGasData, foxFarmingContract } = useFoxFarming(contractAddress) + const { claimRewards, getClaimFeeData, foxFarmingContract } = useFoxFarming(contractAddress) const translate = useTranslate() const mixpanel = getMixPanel() const { onOngoingFarmingTxIdChange } = useFoxEth() @@ -156,9 +156,9 @@ export const ClaimConfirm = ({ accountId, assetId, amount, onBack }: ClaimConfir !(walletState.wallet && feeAsset && feeMarketData && foxFarmingContract && accountAddress) ) return - const gasEstimate = await getClaimGasData(accountAddress) - if (!gasEstimate) throw new Error('Gas estimation failed') - const estimatedGasCrypto = bnOrZero(gasEstimate.average.txFee) + const feeData = await getClaimFeeData(accountAddress) + if (!feeData) throw new Error('Gas estimation failed') + const estimatedGasCrypto = bnOrZero(feeData.txFee) .div(`1e${feeAsset.precision}`) .toPrecision() setCanClaim(true) @@ -174,7 +174,7 @@ export const ClaimConfirm = ({ accountId, assetId, amount, onBack }: ClaimConfir feeAsset.precision, feeMarketData, feeMarketData.price, - getClaimGasData, + getClaimFeeData, walletState.wallet, foxFarmingContract, ]) diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Approve.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Approve.tsx index 71a25f3fdb6..7dd36af1af9 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Approve.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Approve.tsx @@ -68,7 +68,7 @@ export const Approve: React.FC = ({ accountId, onNext }) ) assertIsFoxEthStakingContractAddress(contractAddress) - const { allowance, approve, getStakeGasData } = useFoxFarming(contractAddress) + const { allowance, approve, getStakeFeeData } = useFoxFarming(contractAddress) const assets = useAppSelector(selectAssets) @@ -109,9 +109,9 @@ export const Approve: React.FC = ({ accountId, onNext }) maxAttempts: 30, }) // Get deposit gas estimate - const gasData = await getStakeGasData(state.deposit.cryptoAmount) - if (!gasData) return - const estimatedGasCryptoPrecision = bnOrZero(gasData.average.txFee) + const feeData = await getStakeFeeData(state.deposit.cryptoAmount) + if (!feeData) return + const estimatedGasCryptoPrecision = bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision() dispatch({ @@ -147,7 +147,7 @@ export const Approve: React.FC = ({ accountId, onNext }) wallet, asset, approve, - getStakeGasData, + getStakeFeeData, feeAsset.precision, onNext, assets, diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Deposit.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Deposit.tsx index 2046d9b47b3..9d7bbd8625d 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Deposit.tsx @@ -95,8 +95,8 @@ export const Deposit: React.FC = ({ const { allowance: foxFarmingAllowance, - getStakeGasData, - getApproveGasData, + getStakeFeeData, + getApproveFeeData, } = useFoxFarming(contractAddress) const feeAssetId = getChainAdapterManager().get(chainId)?.getFeeAssetId() @@ -132,9 +132,9 @@ export const Deposit: React.FC = ({ ): Promise => { if (!assetReference) return try { - const gasData = await getStakeGasData(deposit.cryptoAmount) - if (!gasData) return - return bnOrZero(gasData.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + const feeData = await getStakeFeeData(deposit.cryptoAmount) + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { moduleLogger.error( { fn: 'getDepositGasEstimateCryptoPrecision', error }, @@ -180,12 +180,12 @@ export const Deposit: React.FC = ({ assets, ) } else { - const estimatedGasCryptoBaseUnit = await getApproveGasData() - if (!estimatedGasCryptoBaseUnit) return + const feeData = await getApproveFeeData() + if (!feeData) return dispatch({ type: FoxFarmingDepositActionType.SET_APPROVE, payload: { - estimatedGasCryptoPrecision: bnOrZero(estimatedGasCryptoBaseUnit.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, @@ -210,14 +210,14 @@ export const Deposit: React.FC = ({ feeAsset, foxFarmingOpportunity, assetReference, - getStakeGasData, + getStakeFeeData, toast, translate, asset, foxFarmingAllowance, onNext, assets, - getApproveGasData, + getApproveFeeData, ], ) diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Approve.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Approve.tsx index 852a50a1968..af092c354a2 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Approve.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Approve.tsx @@ -67,7 +67,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { assertIsFoxEthStakingContractAddress(contractAddress) - const { allowance, approve, getUnstakeGasData } = useFoxFarming(contractAddress) + const { allowance, approve, getUnstakeFeeData } = useFoxFarming(contractAddress) const toast = useToast() const assets = useAppSelector(selectAssets) @@ -107,9 +107,9 @@ export const Approve: React.FC = ({ accountId, onNext }) => { maxAttempts: 30, }) // Get withdraw gas estimate - const gasData = await getUnstakeGasData(state.withdraw.lpAmount, state.withdraw.isExiting) - if (!gasData) return - const estimatedGasCrypto = bnOrZero(gasData.average.txFee) + const feeData = await getUnstakeFeeData(state.withdraw.lpAmount, state.withdraw.isExiting) + if (!feeData) return + const estimatedGasCrypto = bnOrZero(feeData.txFee) .div(bn(10).pow(underlyingAsset?.precision ?? 0)) .toPrecision() dispatch({ @@ -145,7 +145,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { opportunity, wallet, approve, - getUnstakeGasData, + getUnstakeFeeData, underlyingAsset?.precision, onNext, assets, diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/ExpiredWithdraw.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/ExpiredWithdraw.tsx index 9ec49a4acd8..a168666c48e 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/ExpiredWithdraw.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/ExpiredWithdraw.tsx @@ -70,7 +70,7 @@ export const ExpiredWithdraw: React.FC = ({ assertIsFoxEthStakingContractAddress(contractAddress) - const { getUnstakeGasData, allowance, getApproveGasData } = useFoxFarming(contractAddress) + const { getUnstakeFeeData, allowance, getApproveFeeData } = useFoxFarming(contractAddress) const methods = useForm({ mode: 'onChange' }) @@ -108,9 +108,9 @@ export const ExpiredWithdraw: React.FC = ({ const getWithdrawGasEstimate = async () => { try { - const fee = await getUnstakeGasData(amountAvailableCryptoPrecision.toFixed(), true) - if (!fee) return - return bnOrZero(fee.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + const feeData = await getUnstakeFeeData(amountAvailableCryptoPrecision.toFixed(), true) + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { // TODO: handle client side errors maybe add a toast? moduleLogger.error( @@ -162,12 +162,12 @@ export const ExpiredWithdraw: React.FC = ({ assets, ) } else { - const estimatedGasCrypto = await getApproveGasData() - if (!estimatedGasCrypto) return + const feeData = await getApproveFeeData() + if (!feeData) return dispatch({ type: FoxFarmingWithdrawActionType.SET_APPROVE, payload: { - estimatedGasCryptoPrecision: bnOrZero(estimatedGasCrypto.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Withdraw.tsx index dd06de2ba68..fa2665da183 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Withdraw.tsx @@ -64,7 +64,7 @@ export const Withdraw: React.FC = ({ assertIsFoxEthStakingContractAddress(contractAddress) - const { getUnstakeGasData, allowance, getApproveGasData } = useFoxFarming(contractAddress) + const { getUnstakeFeeData, allowance, getApproveFeeData } = useFoxFarming(contractAddress) const methods = useForm({ mode: 'onChange' }) const { setValue } = methods @@ -95,15 +95,15 @@ export const Withdraw: React.FC = ({ const getWithdrawGasEstimateCryptoPrecision = useCallback( async (withdraw: WithdrawValues) => { try { - const fee = await getUnstakeGasData(withdraw.cryptoAmount, isExiting) - if (!fee) return - return bnOrZero(fee.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + const feeData = await getUnstakeFeeData(withdraw.cryptoAmount, isExiting) + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { // TODO: handle client side errors maybe add a toast? moduleLogger.error(error, 'FoxFarmingWithdraw:getWithdrawGasEstimate error:') } }, - [feeAsset.precision, getUnstakeGasData, isExiting], + [feeAsset.precision, getUnstakeFeeData, isExiting], ) const handleContinue = useCallback( @@ -147,12 +147,12 @@ export const Withdraw: React.FC = ({ assets, ) } else { - const estimatedGasCrypto = await getApproveGasData() - if (!estimatedGasCrypto) return + const feeData = await getApproveFeeData() + if (!feeData) return dispatch({ type: FoxFarmingWithdrawActionType.SET_APPROVE, payload: { - estimatedGasCryptoPrecision: bnOrZero(estimatedGasCrypto.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, @@ -166,7 +166,7 @@ export const Withdraw: React.FC = ({ assets, dispatch, feeAsset.precision, - getApproveGasData, + getApproveFeeData, getWithdrawGasEstimateCryptoPrecision, isExiting, onNext, diff --git a/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts b/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts index 863427bff6a..fcc36f8533b 100644 --- a/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts +++ b/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts @@ -1,16 +1,15 @@ import { MaxUint256 } from '@ethersproject/constants' import { ethAssetId, fromAccountId } from '@shapeshiftoss/caip' -import type { ethereum, EvmChainId, FeeData } from '@shapeshiftoss/chain-adapters' -import { supportsETH } from '@shapeshiftoss/hdwallet-core' +import type { ethereum } from '@shapeshiftoss/chain-adapters' import { ETH_FOX_POOL_CONTRACT_ADDRESS, UNISWAP_V2_ROUTER_02_CONTRACT_ADDRESS, } from 'contracts/constants' import { getOrCreateContractByAddress } from 'contracts/contractManager' +import { buildAndBroadcast, getFeeDataFromEstimate } from 'features/defi/helpers/utils' import { useCallback, useMemo } from 'react' import { useFoxEth } from 'context/FoxEthProvider/FoxEthProvider' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' -import { useEvm } from 'hooks/useEvm/useEvm' import { useWallet } from 'hooks/useWallet/useWallet' import { bnOrZero } from 'lib/bignumber/bignumber' import { logger } from 'lib/logger' @@ -35,7 +34,6 @@ export const useFoxFarming = ( { skip }: UseFoxFarmingOptions = {}, ) => { const { farmingAccountId } = useFoxEth() - const { supportedEvmChainIds } = useEvm() const ethAsset = useAppSelector(state => selectAssetById(state, ethAssetId)) const lpAsset = useAppSelector(state => selectAssetById(state, foxEthLpAssetId)) @@ -46,12 +44,12 @@ export const useFoxFarming = ( const accountNumber = useAppSelector(state => selectAccountNumberByAccountId(state, filter)) - const { - state: { wallet }, - } = useWallet() + const wallet = useWallet().state.wallet const chainAdapterManager = getChainAdapterManager() - const adapter = chainAdapterManager.get(ethAsset.chainId) as unknown as ethereum.ChainAdapter + const adapter = chainAdapterManager.get(ethAsset.chainId) as unknown as + | ethereum.ChainAdapter + | undefined const uniswapRouterContract = useMemo( () => (skip ? null : getOrCreateContractByAddress(UNISWAP_V2_ROUTER_02_CONTRACT_ADDRESS)), @@ -71,21 +69,19 @@ export const useFoxFarming = ( const stake = useCallback( async (lpAmount: string) => { try { - if ( - skip || - !farmingAccountId || - !isValidAccountNumber(accountNumber) || - !foxFarmingContract || - !wallet - ) - return - if (!adapter) - throw new Error(`addLiquidityEth: no adapter available for ${ethAsset.chainId}`) + if (skip) return + if (!farmingAccountId) return + if (!isValidAccountNumber(accountNumber)) return + if (!foxFarmingContract) return + if (!wallet) return + + if (!adapter) throw new Error(`no adapter available for ${ethAsset.chainId}`) + const data = foxFarmingContract.interface.encodeFunctionData('stake', [ bnOrZero(lpAmount).times(bnOrZero(10).exponentiatedBy(lpAsset.precision)).toFixed(0), ]) - const adapterType = adapter.getChainId() - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -93,63 +89,20 @@ export const useFoxFarming = ( from: fromAccountId(farmingAccountId).account, }, }) - const result = await (async () => { - if (supportedEvmChainIds.includes(adapterType)) { - if (!supportsETH(wallet)) - throw new Error(`addLiquidityEthFox: wallet does not support ethereum`) - const fees = estimatedFees.average as FeeData - const { - chainSpecific: { gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas }, - } = fees - const shouldUseEIP1559Fees = - (await wallet.ethSupportsEIP1559()) && - maxFeePerGas !== undefined && - maxPriorityFeePerGas !== undefined - if (!shouldUseEIP1559Fees && gasPrice === undefined) { - throw new Error(`addLiquidityEthFox: missing gasPrice for non-EIP-1559 tx`) - } - return await adapter.buildCustomTx({ - to: contractAddress, - value: '0', - wallet, - data, - gasLimit, - accountNumber, - ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), - }) - } else { - throw new Error(`addLiquidityEthFox: wallet does not support ethereum`) - } - })() - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - - if (!broadcastTXID) { - throw new Error('Broadcast failed') - } - return broadcastTXID - } catch (error) { - moduleLogger.warn(error, 'useFoxFarming:stake error') + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, + to: contractAddress, + value: '0', + wallet, + data, + }) + + return txid + } catch (err) { + moduleLogger.error(err, 'failed to stake') } }, [ @@ -160,7 +113,6 @@ export const useFoxFarming = ( ethAsset.chainId, foxFarmingContract, lpAsset.precision, - supportedEvmChainIds, skip, wallet, ], @@ -169,27 +121,21 @@ export const useFoxFarming = ( const unstake = useCallback( async (lpAmount: string, isExiting: boolean) => { try { - if ( - skip || - !farmingAccountId || - !isValidAccountNumber(accountNumber) || - !foxFarmingContract || - !wallet - ) - return - const chainAdapterManager = getChainAdapterManager() - const adapter = chainAdapterManager.get( - ethAsset.chainId, - ) as unknown as ethereum.ChainAdapter - if (!adapter) - throw new Error(`foxFarmingUnstake: no adapter available for ${ethAsset.chainId}`) + if (skip) return + if (!farmingAccountId) return + if (!isValidAccountNumber(accountNumber)) return + if (!foxFarmingContract) return + if (!wallet) return + + if (!adapter) throw new Error(`no adapter available for ${ethAsset.chainId}`) + const data = isExiting ? foxFarmingContract.interface.encodeFunctionData('exit') : foxFarmingContract.interface.encodeFunctionData('withdraw', [ bnOrZero(lpAmount).times(bnOrZero(10).exponentiatedBy(lpAsset.precision)).toFixed(0), ]) - const adapterType = adapter.getChainId() - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -197,73 +143,30 @@ export const useFoxFarming = ( from: fromAccountId(farmingAccountId).account, }, }) - const result = await (async () => { - if (supportedEvmChainIds.includes(adapterType)) { - if (!supportsETH(wallet)) - throw new Error(`unstakeEthFoxLp: wallet does not support ethereum`) - const fees = estimatedFees.average as FeeData - const { - chainSpecific: { gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas }, - } = fees - const shouldUseEIP1559Fees = - (await wallet.ethSupportsEIP1559()) && - maxFeePerGas !== undefined && - maxPriorityFeePerGas !== undefined - if (!shouldUseEIP1559Fees && gasPrice === undefined) { - throw new Error(`addLiquidityEthFox: missing gasPrice for non-EIP-1559 tx`) - } - return await adapter.buildCustomTx({ - to: contractAddress, - value: '0', - wallet, - data, - gasLimit, - accountNumber, - ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), - }) - } else { - throw new Error(`addLiquidityEthFox: wallet does not support ethereum`) - } - })() - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - - if (!broadcastTXID) { - throw new Error('Broadcast failed') - } - return broadcastTXID - } catch (error) { - moduleLogger.warn(error, 'useFoxFarming:unstake error') + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, + to: contractAddress, + value: '0', + wallet, + data, + }) + + return txid + } catch (err) { + moduleLogger.error(err, 'failed to unstake') } }, [ + adapter, farmingAccountId, accountNumber, contractAddress, ethAsset.chainId, foxFarmingContract, lpAsset.precision, - supportedEvmChainIds, wallet, skip, ], @@ -271,39 +174,47 @@ export const useFoxFarming = ( const allowance = useCallback(async () => { if (skip || !farmingAccountId || !uniV2LPContract) return + const userAddress = fromAccountId(farmingAccountId).account const _allowance = await uniV2LPContract.allowance(userAddress, contractAddress) + return _allowance.toString() }, [farmingAccountId, contractAddress, uniV2LPContract, skip]) - const getApproveGasDataCryptoBaseUnit = useCallback(async () => { - if (adapter && farmingAccountId && uniV2LPContract) { - const data = uniV2LPContract.interface.encodeFunctionData('approve', [ - contractAddress, - MaxUint256, - ]) - const farmingAccountAddress = fromAccountId(farmingAccountId).account - const fees = await adapter.getFeeData({ - to: uniV2LPContract.address, - value: '0', - chainSpecific: { - contractData: data, - from: farmingAccountAddress, - contractAddress: uniV2LPContract.address, - }, - }) - return fees - } + const getApproveFeeData = useCallback(async () => { + if (!adapter || !farmingAccountId || !uniV2LPContract) return + + const data = uniV2LPContract.interface.encodeFunctionData('approve', [ + contractAddress, + MaxUint256, + ]) + + const farmingAccountAddress = fromAccountId(farmingAccountId).account + + const feeData = await adapter.getFeeData({ + to: uniV2LPContract.address, + value: '0', + chainSpecific: { + contractData: data, + from: farmingAccountAddress, + contractAddress: uniV2LPContract.address, + }, + }) + + return getFeeDataFromEstimate(feeData) }, [adapter, farmingAccountId, contractAddress, uniV2LPContract]) - const getStakeGasData = useCallback( + const getStakeFeeData = useCallback( async (lpAmount: string) => { - if (skip || !farmingAccountId || !uniswapRouterContract) return + if (skip || !adapter || !farmingAccountId || !uniswapRouterContract) return + const data = foxFarmingContract!.interface.encodeFunctionData('stake', [ bnOrZero(lpAmount).times(bnOrZero(10).exponentiatedBy(lpAsset.precision)).toFixed(0), ]) + const farmingAccountAddress = fromAccountId(farmingAccountId).account - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -311,7 +222,8 @@ export const useFoxFarming = ( from: farmingAccountAddress, }, }) - return estimatedFees + + return getFeeDataFromEstimate(feeData) }, [ adapter, @@ -324,16 +236,19 @@ export const useFoxFarming = ( ], ) - const getUnstakeGasData = useCallback( + const getUnstakeFeeData = useCallback( async (lpAmount: string, isExiting: boolean) => { - if (skip || !farmingAccountId || !uniswapRouterContract) return + if (skip || !adapter || !farmingAccountId || !uniswapRouterContract) return + const data = isExiting ? foxFarmingContract!.interface.encodeFunctionData('exit') : foxFarmingContract!.interface.encodeFunctionData('withdraw', [ bnOrZero(lpAmount).times(bnOrZero(10).exponentiatedBy(lpAsset.precision)).toFixed(0), ]) + const farmingAccountAddress = fromAccountId(farmingAccountId).account - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -341,7 +256,8 @@ export const useFoxFarming = ( from: farmingAccountAddress, }, }) - return estimatedFees + + return getFeeDataFromEstimate(feeData) }, [ adapter, @@ -354,91 +270,73 @@ export const useFoxFarming = ( ], ) + const getClaimFeeData = useCallback( + async (userAddress: string) => { + if (!adapter || !foxFarmingContract || !userAddress) return + + const data = foxFarmingContract.interface.encodeFunctionData('getReward') + + const feeData = await adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, + from: userAddress, + }, + }) + + return getFeeDataFromEstimate(feeData) + }, + [adapter, contractAddress, foxFarmingContract], + ) + const approve = useCallback(async () => { if (!wallet || !isValidAccountNumber(accountNumber) || !uniV2LPContract) return + + if (!adapter) throw new Error(`no adapter available for ${ethAsset.chainId}`) + const data = uniV2LPContract.interface.encodeFunctionData('approve', [ contractAddress, MaxUint256, ]) - const gasData = await getApproveGasDataCryptoBaseUnit() - if (!gasData) return - const fees = gasData.average as FeeData - const { - chainSpecific: { gasPrice, gasLimit }, - } = fees - if (gasPrice === undefined) { - throw new Error(`approve: missing gasPrice for non-EIP-1559 tx`) - } - const result = await adapter.buildCustomTx({ + + const feeData = await getApproveFeeData() + if (!feeData) return + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: feeData.chainSpecific, to: uniV2LPContract.address, value: '0', wallet, data, - gasLimit, - accountNumber, - gasPrice, }) - const txToSign = result.txToSign - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - return broadcastTXID + return txid }, [ accountNumber, adapter, + ethAsset.chainId, contractAddress, - getApproveGasDataCryptoBaseUnit, + getApproveFeeData, uniV2LPContract, wallet, ]) - const getClaimGasData = useCallback( - async (userAddress: string) => { - if (!foxFarmingContract || !userAddress) return - const data = foxFarmingContract.interface.encodeFunctionData('getReward') - const estimatedFees = await adapter.getFeeData({ - to: contractAddress, - value: '0', - chainSpecific: { - contractData: data, - from: userAddress, - }, - }) - return estimatedFees - }, - [adapter, contractAddress, foxFarmingContract], - ) - const claimRewards = useCallback(async () => { - if ( - skip || - !wallet || - !isValidAccountNumber(accountNumber) || - !foxFarmingContract || - !farmingAccountId - ) - return + if (skip) return + if (!wallet) return + if (!isValidAccountNumber(accountNumber)) return + if (!foxFarmingContract) return + if (!farmingAccountId) return + + if (!adapter) throw new Error(`no adapter available for ${ethAsset.chainId}`) + const data = foxFarmingContract.interface.encodeFunctionData('getReward') const farmingAccountAddress = fromAccountId(farmingAccountId).account - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -446,54 +344,36 @@ export const useFoxFarming = ( from: farmingAccountAddress, }, }) - const fees = estimatedFees.average as FeeData - const { - chainSpecific: { gasPrice, gasLimit }, - } = fees - if (gasPrice === undefined) { - throw new Error(`approve: missing gasPrice for non-EIP-1559 tx`) - } - const result = await adapter.buildCustomTx({ + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, to: contractAddress, value: '0', wallet, data, - gasLimit, - accountNumber, - gasPrice, }) - const txToSign = result.txToSign - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - return broadcastTXID - }, [accountNumber, adapter, farmingAccountId, contractAddress, foxFarmingContract, skip, wallet]) + return txid + }, [ + accountNumber, + adapter, + farmingAccountId, + ethAsset.chainId, + contractAddress, + foxFarmingContract, + skip, + wallet, + ]) return { allowance, approve, - getApproveGasData: getApproveGasDataCryptoBaseUnit, - getStakeGasData, - getClaimGasData, - getUnstakeGasData, + getApproveFeeData, + getStakeFeeData, + getClaimFeeData, + getUnstakeFeeData, stake, unstake, claimRewards, diff --git a/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Approve.tsx b/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Approve.tsx index e7e006d69ce..e72d33f3f77 100644 --- a/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Approve.tsx +++ b/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Approve.tsx @@ -113,7 +113,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { isApprove0Needed && asset0ContractAddress, isApprove1Needed && asset1ContractAddress, ].filter(Boolean) - const { approveAsset, asset0Allowance, asset1Allowance, getDepositGasDataCryptoBaseUnit } = + const { approveAsset, asset0Allowance, asset1Allowance, getDepositFeeData } = useUniV2LiquidityPool({ accountId: accountId ?? '', lpAssetId, @@ -241,13 +241,12 @@ export const Approve: React.FC = ({ accountId, onNext }) => { if (!(state && dispatch && lpOpportunity)) return if (!(isApprove0Needed || isApprove1Needed)) return if (isAsset0AllowanceGranted && isAsset1AllowanceGranted) { - // Get deposit gas estimate - const gasData = await getDepositGasDataCryptoBaseUnit({ + const feeData = await getDepositFeeData({ token0Amount: state.deposit.asset0CryptoAmount, token1Amount: state.deposit.asset1CryptoAmount, }) - if (!gasData) return - const estimatedGasCryptoPrecision = bnOrZero(gasData.average.txFee) + if (!feeData) return + const estimatedGasCryptoPrecision = bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision() dispatch({ @@ -290,7 +289,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { assets, dispatch, feeAsset.precision, - getDepositGasDataCryptoBaseUnit, + getDepositFeeData, isApprove0Needed, isApprove1Needed, isAsset0AllowanceGranted, diff --git a/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Deposit.tsx b/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Deposit.tsx index 62f4fd0b1f3..0f7aa77cd21 100644 --- a/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Deposit.tsx @@ -73,7 +73,7 @@ export const Deposit: React.FC = ({ }) const assetId0 = lpOpportunity?.underlyingAssetIds[0] ?? '' const assetId1 = lpOpportunity?.underlyingAssetIds[1] ?? '' - const { asset0Allowance, asset1Allowance, getApproveGasData, getDepositGasDataCryptoBaseUnit } = + const { asset0Allowance, asset1Allowance, getApproveFeeData, getDepositFeeData } = useUniV2LiquidityPool({ accountId: accountId ?? '', lpAssetId, @@ -118,12 +118,12 @@ export const Deposit: React.FC = ({ if (!feeAsset) return const { cryptoAmount0: token0Amount, cryptoAmount1: token1Amount } = deposit try { - const gasData = await getDepositGasDataCryptoBaseUnit({ + const feeData = await getDepositFeeData({ token0Amount, token1Amount, }) - if (!gasData) return - return bnOrZero(gasData.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { moduleLogger.error( { fn: 'getDepositGasEstimateCryptoPrecision', error }, @@ -198,31 +198,33 @@ export const Deposit: React.FC = ({ assetId1 !== ethAssetId ? ethers.utils.getAddress(fromAssetId(assetId1).assetReference) : undefined + // While the naive approach would be to think both assets approve() calls are going to result in the same gas estimation, // this is not necesssarly true. Some ERC-20s approve() might have a bit more logic, and thus require more gas. // e.g https://github.com/Uniswap/governance/blob/eabd8c71ad01f61fb54ed6945162021ee419998e/contracts/Uni.sol#L119 - const asset0EstimatedGasCrypto = - assetId0 !== ethAssetId && (await getApproveGasData(asset0ContractAddress!)) - const asset1EstimatedGasCrypto = - assetId1 !== ethAssetId && (await getApproveGasData(asset1ContractAddress!)) - if (!(asset0EstimatedGasCrypto || asset1EstimatedGasCrypto)) return + const asset0ApprovalFee = + asset0ContractAddress && bnOrZero((await getApproveFeeData(asset0ContractAddress))?.txFee) + const asset1ApprovalFee = + asset1ContractAddress && bnOrZero((await getApproveFeeData(asset1ContractAddress))?.txFee) + + if (!(asset0ApprovalFee || asset1ApprovalFee)) return - if (!isAsset0AllowanceGranted && asset0EstimatedGasCrypto) { + if (!isAsset0AllowanceGranted && asset0ApprovalFee) { dispatch({ type: UniV2DepositActionType.SET_APPROVE_0, payload: { - estimatedGasCryptoPrecision: bnOrZero(asset0EstimatedGasCrypto.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(asset0ApprovalFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, }) } - if (!isAsset1AllowanceGranted && asset1EstimatedGasCrypto) { + if (!isAsset1AllowanceGranted && asset1ApprovalFee) { dispatch({ type: UniV2DepositActionType.SET_APPROVE_1, payload: { - estimatedGasCryptoPrecision: bnOrZero(asset1EstimatedGasCrypto.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(asset1ApprovalFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, diff --git a/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Approve.tsx b/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Approve.tsx index f6fb68c879e..3d9bfd06d49 100644 --- a/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Approve.tsx +++ b/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Approve.tsx @@ -77,7 +77,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { const assetId0 = lpOpportunity?.underlyingAssetIds[0] ?? '' const assetId1 = lpOpportunity?.underlyingAssetIds[1] ?? '' - const { approveAsset, lpAllowance, getWithdrawGasData } = useUniV2LiquidityPool({ + const { approveAsset, lpAllowance, getWithdrawFeeData } = useUniV2LiquidityPool({ accountId: accountId ?? '', assetId0: lpOpportunity?.underlyingAssetIds[0] ?? '', assetId1: lpOpportunity?.underlyingAssetIds[1] ?? '', @@ -127,14 +127,13 @@ export const Approve: React.FC = ({ accountId, onNext }) => { interval: 15000, maxAttempts: 30, }) - // Get withdraw gas estimate - const gasData = await getWithdrawGasData( + const feeData = await getWithdrawFeeData( state.withdraw.lpAmount, state.withdraw.asset0Amount, state.withdraw.asset1Amount, ) - if (!gasData) return - const estimatedGasCryptoPrecision = bnOrZero(gasData.average.txFee) + if (!feeData) return + const estimatedGasCryptoPrecision = bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision() dispatch({ @@ -174,7 +173,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { wallet, approveAsset, lpAssetId, - getWithdrawGasData, + getWithdrawFeeData, feeAsset.precision, onNext, assets, diff --git a/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Withdraw.tsx index 07fbc6f9035..6295461eb55 100644 --- a/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Withdraw.tsx @@ -81,7 +81,7 @@ export const Withdraw: React.FC = ({ const assetId0 = uniV2Opportunity?.underlyingAssetIds[0] ?? '' const assetId1 = uniV2Opportunity?.underlyingAssetIds[1] ?? '' - const { lpAllowance, getApproveGasData, getWithdrawGasData } = useUniV2LiquidityPool({ + const { lpAllowance, getApproveFeeData, getWithdrawFeeData } = useUniV2LiquidityPool({ accountId: accountId ?? '', assetId0: uniV2Opportunity?.underlyingAssetIds[0] ?? '', assetId1: uniV2Opportunity?.underlyingAssetIds[1] ?? '', @@ -136,13 +136,13 @@ export const Withdraw: React.FC = ({ const getWithdrawGasEstimateCryptoPrecision = async (withdraw: WithdrawValues) => { try { - const fee = await getWithdrawGasData( + const feeData = await getWithdrawFeeData( withdraw.cryptoAmount, asset0AmountCryptoPrecision, asset1AmountCryptoPrecision, ) - if (!fee) return - return bnOrZero(fee.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { // TODO: handle client side errors maybe add a toast? moduleLogger.error(error, 'UniV2Withdraw:getWithdrawGasEstimate error:') @@ -196,12 +196,12 @@ export const Withdraw: React.FC = ({ dispatch({ type: UniV2WithdrawActionType.SET_LOADING, payload: false }) } else { const lpAssetContractAddress = ethers.utils.getAddress(fromAssetId(lpAssetId).assetReference) - const estimatedGasCryptoPrecision = await getApproveGasData(lpAssetContractAddress) - if (!estimatedGasCryptoPrecision) return + const feeData = await getApproveFeeData(lpAssetContractAddress) + if (!feeData) return dispatch({ type: UniV2WithdrawActionType.SET_APPROVE, payload: { - estimatedGasCryptoPrecision: bnOrZero(estimatedGasCryptoPrecision.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, diff --git a/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts b/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts index a7431c9c0fd..a617b750862 100644 --- a/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts +++ b/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts @@ -1,8 +1,7 @@ import { MaxUint256 } from '@ethersproject/constants' import type { AccountId, AssetId } from '@shapeshiftoss/caip' import { ethAssetId, ethChainId, fromAccountId, fromAssetId, toAssetId } from '@shapeshiftoss/caip' -import type { ethereum, EvmChainId, FeeData } from '@shapeshiftoss/chain-adapters' -import { supportsETH } from '@shapeshiftoss/hdwallet-core' +import type { ethereum } from '@shapeshiftoss/chain-adapters' import { UNISWAP_V2_ROUTER_02_CONTRACT_ADDRESS, WETH_TOKEN_CONTRACT_ADDRESS, @@ -10,11 +9,11 @@ import { import { getOrCreateContractByAddress, getOrCreateContractByType } from 'contracts/contractManager' import { ContractType } from 'contracts/types' import { ethers } from 'ethers' +import { buildAndBroadcast, getFeeDataFromEstimate } from 'features/defi/helpers/utils' import isNumber from 'lodash/isNumber' import { useCallback, useMemo } from 'react' import type { Address } from 'viem' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' -import { useEvm } from 'hooks/useEvm/useEvm' import { useWallet } from 'hooks/useWallet/useWallet' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import { logger } from 'lib/logger' @@ -52,7 +51,6 @@ export const useUniV2LiquidityPool = ({ assetId1: AssetId lpAssetId: AssetId } & UseUniV2LiquidityPoolOptions) => { - const { supportedEvmChainIds } = useEvm() const assetId0OrWeth = assetId0 === ethAssetId ? wethAssetId : assetId0 const assetId1OrWeth = assetId1 === ethAssetId ? wethAssetId : assetId1 @@ -67,16 +65,15 @@ export const useUniV2LiquidityPool = ({ if (!weth) throw new Error(`Asset not found for AssetId ${wethAssetId}`) const filter = useMemo(() => ({ accountId }), [accountId]) - const accountNumber = useAppSelector(state => selectAccountNumberByAccountId(state, filter)) - - const { - state: { wallet }, - } = useWallet() + const wallet = useWallet().state.wallet const asset0Price = useAppSelector(state => selectMarketDataById(state, assetId0OrWeth)).price const chainAdapterManager = getChainAdapterManager() - const adapter = chainAdapterManager.get(ethChainId) as unknown as ethereum.ChainAdapter + const adapter = chainAdapterManager.get(ethChainId) as unknown as + | ethereum.ChainAdapter + | undefined + const uniswapRouterContract = useMemo( () => (skip ? null : getOrCreateContractByAddress(UNISWAP_V2_ROUTER_02_CONTRACT_ADDRESS)), [skip], @@ -193,24 +190,23 @@ export const useUniV2LiquidityPool = ({ try { if (skip || !accountId || !isNumber(accountNumber) || !uniswapRouterContract || !wallet) return - if (!adapter) throw new Error(`addLiquidity: no adapter available for ${asset0.chainId}`) + + if (!adapter) throw new Error(`no adapter available for ${asset0.chainId}`) + const maybeEthAmount = (() => { if (assetId0OrWeth === wethAssetId) return token0Amount if (assetId1OrWeth === wethAssetId) return token1Amount return '0' })() + const value = bnOrZero(maybeEthAmount) .times(bn(10).exponentiatedBy(weth.precision)) .toFixed(0) - const data = makeAddLiquidityData({ - token0Amount, - token1Amount, - }) - - const adapterType = adapter.getChainId() + const data = makeAddLiquidityData({ token0Amount, token1Amount }) const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value, chainSpecific: { @@ -218,71 +214,20 @@ export const useUniV2LiquidityPool = ({ from: fromAccountId(accountId).account, }, }) - const result = await (async () => { - if (supportedEvmChainIds.includes(adapterType)) { - if (!supportsETH(wallet)) - throw new Error(`addLiquidity: wallet does not support ethereum`) - const fees = estimatedFees.fast as FeeData - const { - chainSpecific: { - gasPrice, - gasLimit: gasLimitBase, - maxFeePerGas, - maxPriorityFeePerGas, - }, - } = fees - - const gasLimit = bnOrZero(gasLimitBase).times(1.1).toFixed(0) - const shouldUseEIP1559Fees = - (await wallet.ethSupportsEIP1559()) && - maxFeePerGas !== undefined && - maxPriorityFeePerGas !== undefined - if (!shouldUseEIP1559Fees && gasPrice === undefined) { - throw new Error(`addLiquidity: missing gasPrice for non-EIP-1559 tx`) - } - const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - return await adapter.buildCustomTx({ - to: contractAddress, - value, - wallet, - data, - gasLimit, - accountNumber, - ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), - }) - } else { - throw new Error(`addLiquidity: wallet does not support ethereum`) - } - })() - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - if (!broadcastTXID) { - throw new Error('Broadcast failed') - } - return broadcastTXID - } catch (error) { - moduleLogger.warn(error, 'useUniV2LiquidityPool:addLiquidity error') + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, + to: contractAddress, + value, + wallet, + data, + }) + + return txid + } catch (err) { + moduleLogger.error(err, 'failed to addLiquidity') } }, [ @@ -294,7 +239,6 @@ export const useUniV2LiquidityPool = ({ assetId1OrWeth, makeAddLiquidityData, skip, - supportedEvmChainIds, uniswapRouterContract, wallet, weth, @@ -320,8 +264,10 @@ export const useUniV2LiquidityPool = ({ const lpAmountBaseUnit = bnOrZero(lpAmount) .times(bn(10).exponentiatedBy(lpAsset.precision)) .toFixed(0) + const deadline = Date.now() + 1200000 // 20 minutes from now const to = fromAccountId(accountId).account + if ([assetId0OrWeth, assetId1OrWeth].includes(wethAssetId)) { const otherAssetContractAddress = assetId0OrWeth === wethAssetId ? asset1ContractAddress : asset0ContractAddress @@ -359,6 +305,7 @@ export const useUniV2LiquidityPool = ({ weth.precision, ], ) + const removeLiquidity = useCallback( async ({ lpAmount, @@ -372,9 +319,8 @@ export const useUniV2LiquidityPool = ({ try { if (skip || !accountId || !isNumber(accountNumber) || !uniswapRouterContract || !wallet) return - const chainAdapterManager = getChainAdapterManager() - const adapter = chainAdapterManager.get(asset0.chainId) as unknown as ethereum.ChainAdapter - if (!adapter) throw new Error(`removeLiquidity: no adapter available for ${asset0.chainId}`) + + if (!adapter) throw new Error(`no adapter available for ${asset0.chainId}`) const data = makeRemoveLiquidityData({ asset0ContractAddress, @@ -384,9 +330,9 @@ export const useUniV2LiquidityPool = ({ asset0Amount, }) - const adapterType = adapter.getChainId() const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -394,75 +340,24 @@ export const useUniV2LiquidityPool = ({ from: fromAccountId(accountId).account, }, }) - const result = await (async () => { - if (supportedEvmChainIds.includes(adapterType)) { - if (!supportsETH(wallet)) - throw new Error(`removeLiquidity: wallet does not support ethereum`) - const fees = estimatedFees.fast as FeeData - const { - chainSpecific: { - gasPrice, - gasLimit: gasLimitBase, - maxFeePerGas, - maxPriorityFeePerGas, - }, - } = fees - // Gas limit tends to be too low and make Txs revert - // So we artificially bump it by 10% to ensure Txs go through - const gasLimit = bnOrZero(gasLimitBase).times(1.1).toFixed(0) - const shouldUseEIP1559Fees = - (await wallet.ethSupportsEIP1559()) && - maxFeePerGas !== undefined && - maxPriorityFeePerGas !== undefined - if (!shouldUseEIP1559Fees && gasPrice === undefined) { - throw new Error(`removeLiquidity: missing gasPrice for non-EIP-1559 tx`) - } - const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - return await adapter.buildCustomTx({ - to: contractAddress, - value: '0', - wallet, - data, - gasLimit, - accountNumber, - ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), - }) - } else { - throw new Error(`removeLiquidity: wallet does not support ethereum`) - } - })() - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - if (!broadcastTXID) { - throw new Error('Broadcast failed') - } - return broadcastTXID - } catch (error) { - moduleLogger.warn(error, 'useUniV2LiquidityPool:remoLiquidity error') + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, + to: contractAddress, + value: '0', + wallet, + data, + }) + + return txid + } catch (err) { + moduleLogger.error(err, 'failed to removeLiquidity') } }, [ + adapter, skip, accountId, accountNumber, @@ -472,12 +367,12 @@ export const useUniV2LiquidityPool = ({ makeRemoveLiquidityData, asset0ContractAddress, asset1ContractAddress, - supportedEvmChainIds, ], ) const calculateHoldings = useCallback(async () => { if (skip || !uniV2LPContract || !accountId) return + const balance = await uniV2LPContract.balanceOf(fromAccountId(accountId).account) const totalSupply = await uniV2LPContract.totalSupply() const reserves = await uniV2LPContract.getReserves() @@ -498,89 +393,97 @@ export const useUniV2LiquidityPool = ({ }, [skip, uniV2LPContract, accountId, asset0.precision, asset1.precision]) const getLpTVL = useCallback(async () => { - if (uniV2LPContract) { - const reserves = await uniV2LPContract.getReserves() - // Amount of Eth in liquidity pool - const ethInReserve = bnOrZero(reserves?.[0]?.toString()).div(bn(10).pow(asset0.precision)) - - // Total market cap of liquidity pool in usdc. - // Multiplied by 2 to show equal amount of eth and fox. - const totalLiquidity = ethInReserve.times(asset0Price).times(2) - return totalLiquidity.toString() - } + if (!uniV2LPContract) return + + const reserves = await uniV2LPContract.getReserves() + // Amount of Eth in liquidity pool + const ethInReserve = bnOrZero(reserves?.[0]?.toString()).div(bn(10).pow(asset0.precision)) + + // Total market cap of liquidity pool in usdc. + // Multiplied by 2 to show equal amount of eth and fox. + const totalLiquidity = ethInReserve.times(asset0Price).times(2) + return totalLiquidity.toString() }, [asset0.precision, asset0Price, uniV2LPContract]) const getLpTokenPrice = useCallback(async () => { - if (!skip && uniV2LPContract) { - const tvl = await getLpTVL() - const totalSupply = await uniV2LPContract.totalSupply() - return bnOrZero(tvl).div(bnOrZero(totalSupply.toString()).div(bn(10).pow(lpAsset.precision))) - } + if (skip || !uniV2LPContract) return + + const tvl = await getLpTVL() + const totalSupply = await uniV2LPContract.totalSupply() + + return bnOrZero(tvl).div(bnOrZero(totalSupply.toString()).div(bn(10).pow(lpAsset.precision))) }, [skip, getLpTVL, lpAsset.precision, uniV2LPContract]) // TODO(gomes): consolidate me const asset0Allowance = useCallback(async () => { - if (skip) return - const contract = asset0Contract - if (!accountId || !contract) return + if (skip || !accountId || !asset0Contract) return + const accountAddress = fromAccountId(accountId).account const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const _allowance = await contract.allowance(accountAddress, contractAddress) + const _allowance = await asset0Contract.allowance(accountAddress, contractAddress) + return _allowance.toString() }, [skip, asset0Contract, accountId]) const asset1Allowance = useCallback(async () => { - if (skip) return - const contract = asset1Contract - if (!accountId || !contract) return + if (skip || !accountId || !asset1Contract) return + const accountAddress = fromAccountId(accountId).account const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const _allowance = await contract.allowance(accountAddress, contractAddress) + const _allowance = await asset1Contract.allowance(accountAddress, contractAddress) + return _allowance.toString() }, [skip, asset1Contract, accountId]) const lpAllowance = useCallback(async () => { - if (skip) return - const contract = uniV2LPContract - if (!accountId || !contract) return + if (skip || !accountId || !uniV2LPContract) return + const accountAddress = fromAccountId(accountId).account const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const _allowance = await contract.allowance(accountAddress, contractAddress) + const _allowance = await uniV2LPContract.allowance(accountAddress, contractAddress) + return _allowance.toString() }, [skip, uniV2LPContract, accountId]) - const getApproveGasData = useCallback( + const getApproveFeeData = useCallback( async (contractAddress: Address) => { - if (skip) return + if (skip || !adapter || !accountId) return + const contract = getOrCreateContractByType({ address: contractAddress, type: ContractType.ERC20, }) - if (adapter && accountId && contract) { - const data = contract.interface.encodeFunctionData('approve', [ - fromAssetId(uniswapV2Router02AssetId).assetReference, - MaxUint256, - ]) - const fees = await adapter.getFeeData({ - to: contract.address, - value: '0', - chainSpecific: { - contractData: data, - from: fromAccountId(accountId).account, - contractAddress: contract.address, - }, - }) - return fees - } + + if (!contract) return + + const data = contract.interface.encodeFunctionData('approve', [ + fromAssetId(uniswapV2Router02AssetId).assetReference, + MaxUint256, + ]) + + const feeData = await adapter.getFeeData({ + to: contract.address, + value: '0', + chainSpecific: { + contractData: data, + from: fromAccountId(accountId).account, + contractAddress: contract.address, + }, + }) + + return getFeeDataFromEstimate(feeData) }, [skip, adapter, accountId], ) - const getDepositGasDataCryptoBaseUnit = useCallback( + const getDepositFeeData = useCallback( async ({ token0Amount, token1Amount }: { token0Amount: string; token1Amount: string }) => { - if (skip || !accountId || !uniswapRouterContract) return + if (skip || !adapter || !accountId || !uniswapRouterContract) return + // https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02#addliquidityeth const deadline = Date.now() + 1200000 // 20 minutes from now + + // TODO(gomes): consolidate branching, surely we can do better if ([assetId0OrWeth, assetId1OrWeth].includes(wethAssetId)) { const otherAssetContractAddress = assetId0OrWeth === wethAssetId ? asset1ContractAddress : asset0ContractAddress @@ -605,7 +508,8 @@ export const useUniV2LiquidityPool = ({ accountAddress, deadline, ]) - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: ethValueBaseUnit, chainSpecific: { @@ -613,8 +517,8 @@ export const useUniV2LiquidityPool = ({ from: accountAddress, }, }) - return estimatedFees - // TODO(gomes): consolidate branching, surely we can do better + + return getFeeDataFromEstimate(feeData) } else { const accountAddress = fromAccountId(accountId).account const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference @@ -632,7 +536,8 @@ export const useUniV2LiquidityPool = ({ accountAddress, deadline, ]) - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', // 0 ETH since these are ERC20 <-> ERC20 pools chainSpecific: { @@ -640,7 +545,8 @@ export const useUniV2LiquidityPool = ({ from: accountAddress, }, }) - return estimatedFees + + return getFeeDataFromEstimate(feeData) } }, [ @@ -658,9 +564,10 @@ export const useUniV2LiquidityPool = ({ ], ) - const getWithdrawGasData = useCallback( + const getWithdrawFeeData = useCallback( async (lpAmount: string, asset0Amount: string, asset1Amount: string) => { - if (skip || !accountId || !uniswapRouterContract) return + if (skip || !adapter || !accountId || !uniswapRouterContract) return + const data = makeRemoveLiquidityData({ lpAmount, asset0Amount, @@ -670,7 +577,8 @@ export const useUniV2LiquidityPool = ({ }) const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -679,11 +587,7 @@ export const useUniV2LiquidityPool = ({ }, }) - const gasLimitBase = estimatedFees.fast.chainSpecific.gasLimit - const gasLimit = bnOrZero(gasLimitBase).times(1.1).toFixed(0) - estimatedFees.fast.chainSpecific.gasLimit = gasLimit - - return estimatedFees + return getFeeDataFromEstimate(feeData) }, [ skip, @@ -699,6 +603,9 @@ export const useUniV2LiquidityPool = ({ const approveAsset = useCallback( async (contractAddress: Address) => { if (skip || !wallet || !isNumber(accountNumber)) return + + if (!adapter) throw new Error(`no adapter available for ${ethChainId}`) + const contract = getOrCreateContractByType({ address: contractAddress, type: ContractType.ERC20, @@ -711,49 +618,23 @@ export const useUniV2LiquidityPool = ({ uniV2ContractAddress, MaxUint256, ]) - const gasData = await getApproveGasData(contractAddress) - if (!gasData) return - const fees = gasData.fast as FeeData - const { - chainSpecific: { gasPrice, gasLimit }, - } = fees - if (gasPrice === undefined) { - throw new Error(`approveAsset: missing gasPrice for non-EIP-1559 tx`) - } - const result = await adapter.buildCustomTx({ - to: contract!.address, + + const feeData = await getApproveFeeData(contractAddress) + if (!feeData) return + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: feeData.chainSpecific, + to: contractAddress, value: '0', wallet, data, - gasLimit, - accountNumber, - gasPrice, }) - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - return broadcastTXID + + return txid }, - [accountNumber, adapter, getApproveGasData, skip, wallet], + [accountNumber, adapter, getApproveFeeData, skip, wallet], ) return { @@ -763,10 +644,10 @@ export const useUniV2LiquidityPool = ({ asset1Allowance, approveAsset, calculateHoldings, - getApproveGasData, - getDepositGasDataCryptoBaseUnit, + getApproveFeeData, + getDepositFeeData, getLpTVL, - getWithdrawGasData, + getWithdrawFeeData, removeLiquidity, getLpTokenPrice, } diff --git a/src/lib/swapper/api.ts b/src/lib/swapper/api.ts index ed2f478dff3..36ab3cd5506 100644 --- a/src/lib/swapper/api.ts +++ b/src/lib/swapper/api.ts @@ -34,61 +34,38 @@ export const makeSwapErrorRight = ({ code, }) +export type EvmFeeData = { + estimatedGasCryptoBaseUnit?: string + gasPriceCryptoBaseUnit?: string + approvalFeeCryptoBaseUnit?: string + totalFee?: string + maxFeePerGas?: string + maxPriorityFeePerGas?: string +} + +export type UtxoFeeData = { + byteCount: string + satsPerByte: string +} + +export type CosmosSdkFeeData = { + estimatedGasCryptoBaseUnit: string +} + type ChainSpecificQuoteFeeData = ChainSpecific< T, { - [KnownChainIds.EthereumMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.AvalancheMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.OptimismMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.BnbSmartChainMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.PolygonMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.BitcoinMainnet]: { - byteCount: string - satsPerByte: string - } - [KnownChainIds.DogecoinMainnet]: { - byteCount: string - satsPerByte: string - } - [KnownChainIds.LitecoinMainnet]: { - byteCount: string - satsPerByte: string - } - [KnownChainIds.BitcoinCashMainnet]: { - byteCount: string - satsPerByte: string - } - [KnownChainIds.CosmosMainnet]: { - estimatedGasCryptoBaseUnit: string - } - [KnownChainIds.ThorchainMainnet]: { - estimatedGasCryptoBaseUnit: string - } + [KnownChainIds.EthereumMainnet]: EvmFeeData + [KnownChainIds.AvalancheMainnet]: EvmFeeData + [KnownChainIds.OptimismMainnet]: EvmFeeData + [KnownChainIds.BnbSmartChainMainnet]: EvmFeeData + [KnownChainIds.PolygonMainnet]: EvmFeeData + [KnownChainIds.BitcoinMainnet]: UtxoFeeData + [KnownChainIds.DogecoinMainnet]: UtxoFeeData + [KnownChainIds.LitecoinMainnet]: UtxoFeeData + [KnownChainIds.BitcoinCashMainnet]: UtxoFeeData + [KnownChainIds.CosmosMainnet]: CosmosSdkFeeData + [KnownChainIds.ThorchainMainnet]: CosmosSdkFeeData } > diff --git a/src/lib/swapper/swappers/CowSwapper/cowApprove/cowApprove.ts b/src/lib/swapper/swappers/CowSwapper/cowApprove/cowApprove.ts index 869f0ba9cb3..c25ca2d4fbd 100644 --- a/src/lib/swapper/swappers/CowSwapper/cowApprove/cowApprove.ts +++ b/src/lib/swapper/swappers/CowSwapper/cowApprove/cowApprove.ts @@ -1,60 +1,30 @@ +import { fromAssetId } from '@shapeshiftoss/caip' import type { KnownChainIds } from '@shapeshiftoss/types' import type { ApproveAmountInput, ApproveInfiniteInput } from 'lib/swapper/api' -import { SwapError, SwapErrorType } from 'lib/swapper/api' import type { CowSwapperDeps } from 'lib/swapper/swappers/CowSwapper/CowSwapper' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/CowSwapper/utils/constants' -import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' import { grantAllowance } from 'lib/swapper/swappers/utils/helpers/helpers' -export async function cowApproveInfinite( - { adapter, web3 }: CowSwapperDeps, - { quote, wallet }: ApproveInfiniteInput, +export function cowApproveAmount( + deps: CowSwapperDeps, + { quote, wallet, amount }: ApproveAmountInput, ) { - try { - const allowanceGrantRequired = await grantAllowance({ - quote: { - ...quote, - sellAmountBeforeFeesCryptoBaseUnit: MAX_ALLOWANCE, - }, - wallet, - adapter, - erc20Abi, - web3, - }) + const { accountNumber, allowanceContract, feeData, sellAsset } = quote - return allowanceGrantRequired - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[cowApproveInfinite]', { - cause: e, - code: SwapErrorType.APPROVE_INFINITE_FAILED, - }) - } + return grantAllowance({ + ...deps, + accountNumber, + approvalAmount: amount ?? quote.sellAmountBeforeFeesCryptoBaseUnit, + to: fromAssetId(sellAsset.assetId).assetReference, + feeData: feeData.chainSpecific, + spender: allowanceContract, + wallet, + }) } -export async function cowApproveAmount( - { adapter, web3 }: CowSwapperDeps, - { quote, wallet, amount }: ApproveAmountInput, -) { - try { - const approvalAmount = amount ?? quote.sellAmountBeforeFeesCryptoBaseUnit - const allowanceGrantRequired = await grantAllowance({ - quote: { - ...quote, - sellAmountBeforeFeesCryptoBaseUnit: approvalAmount, - }, - wallet, - adapter, - erc20Abi, - web3, - }) - - return allowanceGrantRequired - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[cowApproveAmount]', { - cause: e, - code: SwapErrorType.APPROVE_AMOUNT_FAILED, - }) - } +export function cowApproveInfinite( + deps: CowSwapperDeps, + input: ApproveInfiniteInput, +): Promise { + return cowApproveAmount(deps, { ...input, amount: MAX_ALLOWANCE }) } diff --git a/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.test.ts b/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.test.ts index 7ed61175dcf..dd4219e07ac 100644 --- a/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.test.ts +++ b/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.test.ts @@ -116,10 +116,7 @@ const expectedApiInputFoxToEth: CowSwapSellQuoteApiInput = { const expectedTradeWethToFox: CowTrade = { rate: '14716.04718939437505555958', // 14716 FOX per WETH feeData: { - chainSpecific: { - estimatedGasCryptoBaseUnit: '100000', - gasPriceCryptoBaseUnit: '79036500000', - }, + chainSpecific: {}, buyAssetTradeFeeUsd: '0', networkFeeCryptoBaseUnit: '0', sellAssetTradeFeeUsd: '17.95954294012756741283729339486489192096', @@ -139,11 +136,7 @@ const expectedTradeQuoteWbtcToWethWithApprovalFeeCryptoBaseUnit: CowTrade = { rate: '0.00004995640398295996', feeData: { - chainSpecific: { - estimatedGasCryptoBaseUnit: '100000', - gasPriceCryptoBaseUnit: '79036500000', - }, + chainSpecific: {}, buyAssetTradeFeeUsd: '0', networkFeeCryptoBaseUnit: '0', sellAssetTradeFeeUsd: '5.3955565850972847808512', diff --git a/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.ts b/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.ts index 3c60d962157..3e419b0853d 100644 --- a/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.ts +++ b/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.ts @@ -2,7 +2,6 @@ import { ethAssetId, fromAssetId } from '@shapeshiftoss/caip' import { KnownChainIds } from '@shapeshiftoss/types' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import type { AxiosResponse } from 'axios' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import type { BuildTradeInput, SwapErrorRight } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' @@ -10,38 +9,29 @@ import type { CowSwapperDeps } from 'lib/swapper/swappers/CowSwapper/CowSwapper' import type { CowSwapQuoteResponse, CowTrade } from 'lib/swapper/swappers/CowSwapper/types' import { COW_SWAP_ETH_MARKER_ADDRESS, - COW_SWAP_VAULT_RELAYER_ADDRESS, DEFAULT_APP_DATA, DEFAULT_SOURCE, ORDER_KIND_SELL, } from 'lib/swapper/swappers/CowSwapper/utils/constants' import { cowService } from 'lib/swapper/swappers/CowSwapper/utils/cowService' +import type { CowSwapSellQuoteApiInput } from 'lib/swapper/swappers/CowSwapper/utils/helpers/helpers' import { getNowPlusThirtyMinutesTimestamp, getUsdRate, } from 'lib/swapper/swappers/CowSwapper/utils/helpers/helpers' -import { erc20AllowanceAbi } from 'lib/swapper/swappers/utils/abi/erc20Allowance-abi' -import { - getApproveContractData, - isApprovalRequired, -} from 'lib/swapper/swappers/utils/helpers/helpers' export async function cowBuildTrade( deps: CowSwapperDeps, input: BuildTradeInput, ): Promise, SwapErrorRight>> { try { - const { - sellAsset, - buyAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountExcludeFeeCryptoBaseUnit, - accountNumber, - wallet, - } = input - const { adapter, web3 } = deps + const { adapter } = deps + const { sellAsset, buyAsset, accountNumber, wallet } = input + const sellAmount = input.sellAmountBeforeFeesCryptoBaseUnit const { assetReference: sellAssetErc20Address, assetNamespace: sellAssetNamespace } = fromAssetId(sellAsset.assetId) + const { assetReference: buyAssetErc20Address, chainId: buyAssetChainId } = fromAssetId( buyAsset.assetId, ) @@ -62,44 +52,29 @@ export async function cowBuildTrade( const buyToken = buyAsset.assetId !== ethAssetId ? buyAssetErc20Address : COW_SWAP_ETH_MARKER_ADDRESS + const receiveAddress = await adapter.getAddress({ accountNumber, wallet }) - /** - * /v1/quote - * params: { - * sellToken: contract address of token to sell - * buyToken: contractAddress of token to buy - * receiver: receiver address can be defaulted to "0x0000000000000000000000000000000000000000" - * validTo: time duration during which quote is valid (eg : 1654851610 as timestamp) - * appData: appData for the CowSwap quote that can be used later, can be defaulted to "0x0000000000000000000000000000000000000000000000000000000000000000" - * partiallyFillable: false - * from: sender address can be defaulted to "0x0000000000000000000000000000000000000000" - * kind: "sell" or "buy" - * sellAmountBeforeFee / buyAmountAfterFee: amount in base unit - * } - */ - const quoteResponse: AxiosResponse = - await cowService.post(`${deps.apiUrl}/v1/quote/`, { - sellToken: sellAssetErc20Address, - buyToken, - receiver: receiveAddress, - validTo: getNowPlusThirtyMinutesTimestamp(), - appData: DEFAULT_APP_DATA, - partiallyFillable: false, - from: receiveAddress, - kind: ORDER_KIND_SELL, - sellAmountBeforeFee: sellAmountExcludeFeeCryptoBaseUnit, - }) + // https://api.cow.fi/docs/#/default/post_api_v1_quote + const { data } = await cowService.post(`${deps.apiUrl}/v1/quote/`, { + sellToken: sellAssetErc20Address, + buyToken, + receiver: receiveAddress, + validTo: getNowPlusThirtyMinutesTimestamp(), + appData: DEFAULT_APP_DATA, + partiallyFillable: false, + from: receiveAddress, + kind: ORDER_KIND_SELL, + sellAmountBeforeFee: sellAmount, + } as CowSwapSellQuoteApiInput) const { - data: { - quote: { - buyAmount: buyAmountCryptoBaseUnit, - sellAmount: quoteSellAmountExcludeFeeCryptoBaseUnit, - feeAmount: feeAmountInSellTokenCryptoBaseUnit, - }, + quote: { + buyAmount: buyAmountCryptoBaseUnit, + sellAmount: quoteSellAmountExcludeFeeCryptoBaseUnit, + feeAmount: feeAmountInSellTokenCryptoBaseUnit, }, - } = quoteResponse + } = data const sellAssetUsdRate = await getUsdRate(deps, sellAsset) const sellAssetTradeFeeUsd = bnOrZero(feeAmountInSellTokenCryptoBaseUnit) @@ -115,32 +90,15 @@ export async function cowBuildTrade( ) const rate = buyAmountCryptoPrecision.div(quoteSellAmountCryptoPrecision).toString() - const data = getApproveContractData({ - web3, - spenderAddress: COW_SWAP_VAULT_RELAYER_ADDRESS, - contractAddress: sellAssetErc20Address, - }) - - const feeDataOptions = await adapter.getFeeData({ - to: sellAssetErc20Address, - value: '0', - chainSpecific: { from: receiveAddress, contractData: data }, - }) - - const feeData = feeDataOptions['fast'] - const trade: CowTrade = { rate, feeData: { networkFeeCryptoBaseUnit: '0', // no miner fee for CowSwap - chainSpecific: { - estimatedGasCryptoBaseUnit: feeData.chainSpecific.gasLimit, - gasPriceCryptoBaseUnit: feeData.chainSpecific.gasPrice, - }, + chainSpecific: {}, // no on chain fees for CowSwap buyAssetTradeFeeUsd: '0', // Trade fees for buy Asset are always 0 since trade fees are subtracted from sell asset sellAssetTradeFeeUsd, }, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountExcludeFeeCryptoBaseUnit, + sellAmountBeforeFeesCryptoBaseUnit: sellAmount, buyAmountCryptoBaseUnit, sources: DEFAULT_SOURCE, buyAsset, @@ -151,24 +109,6 @@ export async function cowBuildTrade( sellAmountDeductFeeCryptoBaseUnit: quoteSellAmountExcludeFeeCryptoBaseUnit, } - const approvalRequired = await isApprovalRequired({ - adapter, - sellAsset, - allowanceContract: COW_SWAP_VAULT_RELAYER_ADDRESS, - receiveAddress, - sellAmountExcludeFeeCryptoBaseUnit, - web3: deps.web3, - erc20AllowanceAbi, - }) - - if (approvalRequired) { - trade.feeData.chainSpecific.approvalFeeCryptoBaseUnit = bnOrZero( - feeData.chainSpecific.gasLimit, - ) - .multipliedBy(bnOrZero(feeData.chainSpecific.gasPrice)) - .toString() - } - return Ok(trade) } catch (e) { if (e instanceof SwapError) diff --git a/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts b/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts index 16af9791d54..c373ae7ca66 100644 --- a/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts +++ b/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts @@ -134,7 +134,9 @@ const expectedTradeQuoteWethToFox: TradeQuote = { chainSpecific: { estimatedGasCryptoBaseUnit: '100000', gasPriceCryptoBaseUnit: '79036500000', - approvalFeeCryptoBaseUnit: '7903650000000000', + approvalFeeCryptoBaseUnit: '4080654495000000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '17.95954294012756741283729339486489192096', @@ -157,7 +159,9 @@ const expectedTradeQuoteFoxToEth: TradeQuote = { chainSpecific: { estimatedGasCryptoBaseUnit: '100000', gasPriceCryptoBaseUnit: '79036500000', - approvalFeeCryptoBaseUnit: '7903650000000000', + approvalFeeCryptoBaseUnit: '4080654495000000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '5.3955565850972847808512', @@ -180,7 +184,9 @@ const expectedTradeQuoteSmallAmountWethToFox: TradeQuote, SwapErrorRight>> { try { - const { - sellAsset, - buyAsset, - sellAmountBeforeFeesCryptoBaseUnit, - accountNumber, - receiveAddress, - } = input const { adapter, web3 } = deps + const { sellAsset, buyAsset, accountNumber, receiveAddress } = input + const sellAmount = input.sellAmountBeforeFeesCryptoBaseUnit const { assetReference: sellAssetErc20Address, assetNamespace: sellAssetNamespace } = fromAssetId(sellAsset.assetId) + const { assetReference: buyAssetErc20Address, chainId: buyAssetChainId } = fromAssetId( buyAsset.assetId, ) @@ -68,6 +64,7 @@ export async function getCowSwapTradeQuote( const buyToken = buyAsset.assetId !== ethAssetId ? buyAssetErc20Address : COW_SWAP_ETH_MARKER_ADDRESS + const { minimumAmountCryptoHuman, maximumAmountCryptoHuman } = await getCowSwapMinMax( deps, sellAsset, @@ -77,16 +74,15 @@ export async function getCowSwapTradeQuote( const minQuoteSellAmount = bnOrZero(minimumAmountCryptoHuman).times( bn(10).exponentiatedBy(sellAsset.precision), ) - const isSellAmountBelowMinimum = bnOrZero(sellAmountBeforeFeesCryptoBaseUnit).lt( - minQuoteSellAmount, - ) + const isSellAmountBelowMinimum = bnOrZero(sellAmount).lt(minQuoteSellAmount) // making sure we do not have decimals for cowswap api (can happen at least from minQuoteSellAmount) const normalizedSellAmountCryptoBaseUnit = normalizeIntegerAmount( - isSellAmountBelowMinimum ? minQuoteSellAmount : sellAmountBeforeFeesCryptoBaseUnit, + isSellAmountBelowMinimum ? minQuoteSellAmount : sellAmount, ) - const apiInput: CowSwapSellQuoteApiInput = { + // https://api.cow.fi/docs/#/default/post_api_v1_quote + const { data } = await cowService.post(`${deps.apiUrl}/v1/quote/`, { sellToken: sellAssetErc20Address, buyToken, receiver: DEFAULT_ADDRESS, @@ -96,34 +92,15 @@ export async function getCowSwapTradeQuote( from: DEFAULT_ADDRESS, kind: ORDER_KIND_SELL, sellAmountBeforeFee: normalizedSellAmountCryptoBaseUnit, - } - - /** - * /v1/quote - * params: { - * sellToken: contract address of token to sell - * buyToken: contractAddress of token to buy - * receiver: receiver address can be defaulted to "0x0000000000000000000000000000000000000000" - * validTo: time duration during which quote is valid (eg : 1654851610 as timestamp) - * appData: appData for the CowSwap quote that can be used later, can be defaulted to "0x0000000000000000000000000000000000000000000000000000000000000000" - * partiallyFillable: false - * from: sender address can be defaulted to "0x0000000000000000000000000000000000000000" - * kind: "sell" or "buy" - * sellAmountBeforeFee / buyAmountAfterFee: amount in base unit - * } - */ - const quoteResponse: AxiosResponse = - await cowService.post(`${deps.apiUrl}/v1/quote/`, apiInput) + } as CowSwapSellQuoteApiInput) const { - data: { - quote: { - buyAmount: buyAmountCryptoBaseUnit, - sellAmount: sellAmountCryptoBaseUnit, - feeAmount: feeAmountInSellTokenCryptoBaseUnit, - }, + quote: { + buyAmount: buyAmountCryptoBaseUnit, + sellAmount: sellAmountCryptoBaseUnit, + feeAmount: feeAmountInSellTokenCryptoBaseUnit, }, - } = quoteResponse + } = data const quoteSellAmountPlusFeesCryptoBaseUnit = bnOrZero(sellAmountCryptoBaseUnit).plus( feeAmountInSellTokenCryptoBaseUnit, @@ -137,17 +114,17 @@ export async function getCowSwapTradeQuote( ) const rate = buyCryptoAmount.div(sellCryptoAmount).toString() - const data = getApproveContractData({ + const approveData = getApproveContractData({ web3, spenderAddress: COW_SWAP_VAULT_RELAYER_ADDRESS, contractAddress: sellAssetErc20Address, }) - const [feeDataOptions, sellAssetUsdRate] = await Promise.all([ + const [feeData, sellAssetUsdRate] = await Promise.all([ adapter.getFeeData({ to: sellAssetErc20Address, value: '0', - chainSpecific: { from: receiveAddress, contractData: data }, + chainSpecific: { from: receiveAddress, contractData: approveData }, }), getUsdRate(deps, sellAsset), ]) @@ -157,15 +134,13 @@ export async function getCowSwapTradeQuote( .multipliedBy(bnOrZero(sellAssetUsdRate)) .toString() - const feeData = feeDataOptions['fast'] - const isQuoteSellAmountBelowMinimum = bnOrZero(quoteSellAmountPlusFeesCryptoBaseUnit).lt( minQuoteSellAmount, ) // If isQuoteSellAmountBelowMinimum we don't want to replace it with normalizedSellAmount // The purpose of this was to get a quote from CowSwap even with small amounts const quoteSellAmountCryptoBaseUnit = isQuoteSellAmountBelowMinimum - ? sellAmountBeforeFeesCryptoBaseUnit + ? sellAmount : normalizedSellAmountCryptoBaseUnit // Similarly, if isQuoteSellAmountBelowMinimum we can't use the buy amount from the quote @@ -174,6 +149,8 @@ export async function getCowSwapTradeQuote( ? '0' : buyAmountCryptoBaseUnit + const { average, fast } = feeData + return Ok({ rate, minimumCryptoHuman: minimumAmountCryptoHuman, @@ -181,11 +158,11 @@ export async function getCowSwapTradeQuote( feeData: { networkFeeCryptoBaseUnit: '0', // no miner fee for CowSwap chainSpecific: { - estimatedGasCryptoBaseUnit: feeData.chainSpecific.gasLimit, - gasPriceCryptoBaseUnit: feeData.chainSpecific.gasPrice, - approvalFeeCryptoBaseUnit: bnOrZero(feeData.chainSpecific.gasLimit) - .multipliedBy(bnOrZero(feeData.chainSpecific.gasPrice)) - .toString(), + estimatedGasCryptoBaseUnit: average.chainSpecific.gasLimit, + gasPriceCryptoBaseUnit: fast.chainSpecific.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.chainSpecific.maxFeePerGas, + maxPriorityFeePerGas: average.chainSpecific.maxPriorityFeePerGas, + approvalFeeCryptoBaseUnit: fast.txFee, // use worst case fast fee }, buyAssetTradeFeeUsd: '0', // Trade fees for buy Asset are always 0 since trade fees are subtracted from sell asset sellAssetTradeFeeUsd, diff --git a/src/lib/swapper/swappers/LifiSwapper/approve/approve.ts b/src/lib/swapper/swappers/LifiSwapper/approve/approve.ts index cbadd006625..d2dc5a72be3 100644 --- a/src/lib/swapper/swappers/LifiSwapper/approve/approve.ts +++ b/src/lib/swapper/swappers/LifiSwapper/approve/approve.ts @@ -1,31 +1,25 @@ +import { fromAssetId } from '@shapeshiftoss/caip' import type { EvmChainId } from '@shapeshiftoss/chain-adapters' -import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' -import type { ApproveAmountInput, ApproveInfiniteInput, TradeQuote } from 'lib/swapper/api' +import type { ApproveAmountInput, ApproveInfiniteInput } from 'lib/swapper/api' import { SwapError, SwapErrorType } from 'lib/swapper/api' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/LifiSwapper/utils/constants' -import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' import { grantAllowance } from 'lib/swapper/swappers/utils/helpers/helpers' import { isEvmChainAdapter } from 'lib/utils' import { getWeb3InstanceByChainId } from 'lib/web3-instance' -const grantAllowanceForAmount = async ( +const grantAllowanceForAmount = ( { quote, wallet }: ApproveAmountInput, approvalAmountCryptoBaseUnit: string, ) => { - const chainId = quote.sellAsset.chainId + const { accountNumber, allowanceContract, feeData, sellAsset } = quote + + const chainId = sellAsset.chainId const adapterManager = getChainAdapterManager() const adapter = adapterManager.get(chainId) const web3 = getWeb3InstanceByChainId(chainId) - if (!isEvmChainId(chainId)) { - throw new SwapError('[grantAllowanceForAmount] - only EVM chains are supported', { - code: SwapErrorType.UNSUPPORTED_CHAIN, - details: { chainId }, - }) - } - - if (adapter === undefined) { + if (!adapter) { throw new SwapError('[grantAllowanceForAmount] - getChainAdapterManager returned undefined', { code: SwapErrorType.UNSUPPORTED_CHAIN, details: { chainId }, @@ -42,16 +36,14 @@ const grantAllowanceForAmount = async ( }) } - const approvalQuote: TradeQuote = { - ...quote, - sellAmountBeforeFeesCryptoBaseUnit: approvalAmountCryptoBaseUnit, - } - - return await grantAllowance({ - quote: approvalQuote, + return grantAllowance({ + accountNumber, + spender: allowanceContract, + feeData: feeData.chainSpecific, + approvalAmount: approvalAmountCryptoBaseUnit, + to: fromAssetId(sellAsset.assetId).assetReference, wallet, adapter, - erc20Abi, web3, }) } diff --git a/src/lib/swapper/swappers/ThorchainSwapper/buildThorTrade/buildThorTrade.ts b/src/lib/swapper/swappers/ThorchainSwapper/buildThorTrade/buildThorTrade.ts index 01b1bf2492d..d29d3ddfe10 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/buildThorTrade/buildThorTrade.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/buildThorTrade/buildThorTrade.ts @@ -10,6 +10,7 @@ import { Err, Ok } from '@sniptt/monads' import type { BuildTradeInput, GetUtxoTradeQuoteInput, + QuoteFeeData, SwapErrorRight, TradeQuote, } from 'lib/swapper/api' @@ -75,14 +76,8 @@ export const buildTrade = async ({ adapter: sellAdapter as unknown as EvmBaseAdapter, sellAmountCryptoBaseUnit, destinationAddress, + feeData: quote.feeData as QuoteFeeData, deps, - gasPriceCryptoBaseUnit: - (quote as TradeQuote).feeData.chainSpecific - ?.gasPriceCryptoBaseUnit ?? '0', - gasLimit: - (quote as TradeQuote).feeData.chainSpecific - ?.estimatedGasCryptoBaseUnit ?? '0', - buyAssetTradeFeeUsd: quote.feeData.buyAssetTradeFeeUsd, }) return maybeEthTradeTx.map(ethTradeTx => ({ diff --git a/src/lib/swapper/swappers/ThorchainSwapper/evm/makeTradeTx.ts b/src/lib/swapper/swappers/ThorchainSwapper/evm/makeTradeTx.ts index 05d95361052..8897ac8cded 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/evm/makeTradeTx.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/evm/makeTradeTx.ts @@ -1,16 +1,18 @@ import type { Asset } from '@shapeshiftoss/asset-service' import { fromAssetId } from '@shapeshiftoss/caip' -import type { EvmBaseAdapter } from '@shapeshiftoss/chain-adapters' +import type { EvmBaseAdapter, EvmChainId } from '@shapeshiftoss/chain-adapters' import type { ETHSignTx, HDWallet } from '@shapeshiftoss/hdwallet-core' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import type { SwapErrorRight } from 'lib/swapper/api' +import type { QuoteFeeData, SwapErrorRight } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' import { getThorTxInfo } from 'lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData' import type { ThorEvmSupportedChainId } from 'lib/swapper/swappers/ThorchainSwapper/ThorchainSwapper' import type { ThorchainSwapperDeps } from 'lib/swapper/swappers/ThorchainSwapper/types' -type MakeTradeTxArgs = { +import { getFeesFromFeeData } from '../../utils/helpers/helpers' + +type MakeTradeTxArgs = { wallet: HDWallet accountNumber: number sellAmountCryptoBaseUnit: string @@ -19,21 +21,9 @@ type MakeTradeTxArgs = { destinationAddress: string adapter: EvmBaseAdapter slippageTolerance: string + feeData: QuoteFeeData deps: ThorchainSwapperDeps - gasLimit: string - buyAssetTradeFeeUsd: string -} & ( - | { - gasPriceCryptoBaseUnit: string - maxFeePerGas?: never - maxPriorityFeePerGas?: never - } - | { - gasPriceCryptoBaseUnit?: never - maxFeePerGas: string - maxPriorityFeePerGas: string - } -) +} export const makeTradeTx = async ({ wallet, @@ -43,14 +33,10 @@ export const makeTradeTx = async ({ sellAsset, destinationAddress, adapter, - maxFeePerGas, - maxPriorityFeePerGas, - gasPriceCryptoBaseUnit, slippageTolerance, + feeData, deps, - gasLimit, - buyAssetTradeFeeUsd, -}: MakeTradeTxArgs): Promise< +}: MakeTradeTxArgs): Promise< Result< { txToSign: ETHSignTx @@ -69,7 +55,7 @@ export const makeTradeTx = async ({ sellAmountCryptoBaseUnit, slippageTolerance, destinationAddress, - buyAssetTradeFeeUsd, + buyAssetTradeFeeUsd: feeData.buyAssetTradeFeeUsd, }) if (maybeThorTxInfo.isErr()) return Err(maybeThorTxInfo.unwrapErr()) @@ -83,12 +69,9 @@ export const makeTradeTx = async ({ wallet, accountNumber, to: router, - gasLimit, - ...(gasPriceCryptoBaseUnit !== undefined - ? { gasPrice: gasPriceCryptoBaseUnit } - : { maxFeePerGas, maxPriorityFeePerGas }), value: isErc20Trade ? '0' : sellAmountCryptoBaseUnit, data, + ...(await getFeesFromFeeData({ wallet, feeData: feeData.chainSpecific })), }), ) } catch (e) { diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts index 4feddaeb9b9..22a09c23dbe 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts @@ -28,6 +28,8 @@ const expectedQuoteResponse: TradeQuote = { estimatedGasCryptoBaseUnit: '100000', approvalFeeCryptoBaseUnit: '700000', gasPriceCryptoBaseUnit: '7', + maxFeePerGas: '5', + maxPriorityFeePerGas: '6', }, buyAssetTradeFeeUsd: '7.656', sellAssetTradeFeeUsd: '0', diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts index 7c8e59bd170..0fb522d1122 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts @@ -19,6 +19,7 @@ import { makeSwapErrorRight, SwapError, SwapErrorType, SwapperName } from 'lib/s import { RUNE_OUTBOUND_TRANSACTION_FEE_CRYPTO_HUMAN } from 'lib/swapper/swappers/ThorchainSwapper/constants' import { getSlippage } from 'lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getSlippage' import type { + ThorChainId, ThorCosmosSdkSupportedChainId, ThorEvmSupportedChainId, ThorUtxoSupportedChainId, @@ -48,7 +49,7 @@ type GetThorTradeQuoteInput = { input: GetTradeQuoteInput } -type GetThorTradeQuoteReturn = Promise, SwapErrorRight>> +type GetThorTradeQuoteReturn = Promise, SwapErrorRight>> type GetThorTradeQuote = (args: GetThorTradeQuoteInput) => GetThorTradeQuoteReturn diff --git a/src/lib/swapper/swappers/ThorchainSwapper/thorTradeApproveInfinite/thorTradeApproveInfinite.ts b/src/lib/swapper/swappers/ThorchainSwapper/thorTradeApproveInfinite/thorTradeApproveInfinite.ts index 9407c28d116..26a958b741d 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/thorTradeApproveInfinite/thorTradeApproveInfinite.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/thorTradeApproveInfinite/thorTradeApproveInfinite.ts @@ -1,66 +1,38 @@ +import { fromAssetId } from '@shapeshiftoss/caip' import type { ethereum } from '@shapeshiftoss/chain-adapters' import { KnownChainIds } from '@shapeshiftoss/types' import type { ApproveInfiniteInput } from 'lib/swapper/api' import { SwapError, SwapErrorType } from 'lib/swapper/api' import type { ThorchainSwapperDeps } from 'lib/swapper/swappers/ThorchainSwapper/types' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/ThorchainSwapper/utils/constants' -import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' -import { APPROVAL_GAS_LIMIT } from 'lib/swapper/swappers/utils/constants' import { grantAllowance } from 'lib/swapper/swappers/utils/helpers/helpers' -export const thorTradeApproveInfinite = async ({ - deps, - input, +export const thorTradeApproveInfinite = ({ + deps: { adapterManager, web3 }, + input: { quote, wallet }, }: { deps: ThorchainSwapperDeps input: ApproveInfiniteInput }): Promise => { - try { - const { adapterManager, web3 } = deps - const { quote, wallet } = input + const adapter = adapterManager.get(KnownChainIds.EthereumMainnet) as unknown as + | ethereum.ChainAdapter + | undefined - const approvalQuote = { - ...quote, - sellAmount: MAX_ALLOWANCE, - feeData: { - ...quote.feeData, - chainSpecific: { - ...quote.feeData.chainSpecific, - // Thor approvals are cheaper than trades, but we don't have dynamic quote data for them. - // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the Thor quote response. - estimatedGas: APPROVAL_GAS_LIMIT, - }, - }, - } - - const sellAssetChainId = approvalQuote.sellAsset.chainId - const adapter = adapterManager.get(KnownChainIds.EthereumMainnet) as unknown as - | ethereum.ChainAdapter - | undefined - - if (!adapter) - throw new SwapError( - `[thorTradeApproveInfinite] - No chain adapter found for ${sellAssetChainId}.`, - { - code: SwapErrorType.UNSUPPORTED_CHAIN, - details: { sellAssetChainId }, - }, - ) - - const allowanceGrantRequired = await grantAllowance({ - quote: approvalQuote, - wallet, - adapter, - erc20Abi, - web3, - }) - - return allowanceGrantRequired - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[zrxApproveInfinite]', { - cause: e, - code: SwapErrorType.APPROVE_INFINITE_FAILED, - }) + if (!adapter) { + throw new SwapError( + `[thorTradeApproveInfinite] - No chain adapter found for ${quote.sellAsset.chainId}.`, + { code: SwapErrorType.UNSUPPORTED_CHAIN }, + ) } + + return grantAllowance({ + accountNumber: quote.accountNumber, + spender: quote.allowanceContract, + feeData: quote.feeData.chainSpecific, + approvalAmount: MAX_ALLOWANCE, + to: fromAssetId(quote.sellAsset.assetId).assetReference, + wallet, + adapter, + web3, + }) } diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/txFeeHelpers/evmTxFees/getEvmTxFees.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/txFeeHelpers/evmTxFees/getEvmTxFees.ts index 48fcd90ca7d..8720d2603df 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/txFeeHelpers/evmTxFees/getEvmTxFees.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/txFeeHelpers/evmTxFees/getEvmTxFees.ts @@ -1,7 +1,6 @@ import type { AssetReference } from '@shapeshiftoss/caip' import type { EvmBaseAdapter } from '@shapeshiftoss/chain-adapters' -import { FeeDataKey } from '@shapeshiftoss/chain-adapters' -import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import type { QuoteFeeData } from 'lib/swapper/api' import { SwapError, SwapErrorType } from 'lib/swapper/api' import type { ThorEvmSupportedChainId } from 'lib/swapper/swappers/ThorchainSwapper/ThorchainSwapper' @@ -20,35 +19,26 @@ export const getEvmTxFees = async ({ buyAssetTradeFeeUsd, }: GetEvmTxFeesArgs): Promise> => { try { - const gasFeeData = await adapter.getGasFeeData() + const { average, fast } = await adapter.getGasFeeData() + + // use worst case average eip1559 vs fast legacy + const maxGasPrice = bnOrZero(BigNumber.max(average.maxFeePerGas ?? 0, fast.gasPrice)) // this is a good value to cover all thortrades out of EVMs // in the future we may want to look at doing this more precisely and in a future-proof way // TODO: calculate this dynamically - const gasLimit = THOR_EVM_GAS_LIMIT - - const feeDataOptions = { - fast: { - txFee: bn(gasLimit).times(gasFeeData[FeeDataKey.Fast].gasPrice).toString(), - chainSpecific: { - gasPrice: gasFeeData[FeeDataKey.Fast].gasPrice, - gasLimit, - }, - }, - } + const txFee = bn(THOR_EVM_GAS_LIMIT).times(maxGasPrice) - const feeData = feeDataOptions['fast'] + const approvalFee = sellAssetReference && bn(APPROVAL_GAS_LIMIT).times(maxGasPrice).toFixed(0) return { - networkFeeCryptoBaseUnit: feeData.txFee, + networkFeeCryptoBaseUnit: txFee.toFixed(0), chainSpecific: { - estimatedGasCryptoBaseUnit: feeData.chainSpecific.gasLimit, - gasPriceCryptoBaseUnit: feeData.chainSpecific.gasPrice, - approvalFeeCryptoBaseUnit: - sellAssetReference && - bnOrZero(APPROVAL_GAS_LIMIT) - .multipliedBy(bnOrZero(feeData.chainSpecific.gasPrice)) - .toString(), + estimatedGasCryptoBaseUnit: THOR_EVM_GAS_LIMIT, + gasPriceCryptoBaseUnit: fast.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.maxFeePerGas, + maxPriorityFeePerGas: average.maxPriorityFeePerGas, + approvalFeeCryptoBaseUnit: approvalFee, }, buyAssetTradeFeeUsd, sellAssetTradeFeeUsd: '0', diff --git a/src/lib/swapper/swappers/ZrxSwapper/ZrxSwapper.ts b/src/lib/swapper/swappers/ZrxSwapper/ZrxSwapper.ts index 33a80763bae..c5db960c7e3 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/ZrxSwapper.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/ZrxSwapper.ts @@ -86,12 +86,12 @@ export class ZrxSwapper implements Swapper { } } - buildTrade(args: BuildTradeInput): Promise, SwapErrorRight>> { - return zrxBuildTrade(this.deps, args) + buildTrade(input: BuildTradeInput): Promise, SwapErrorRight>> { + return zrxBuildTrade(this.deps, input) } getTradeQuote(input: GetEvmTradeQuoteInput): Promise, SwapErrorRight>> { - return getZrxTradeQuote(input) + return getZrxTradeQuote(this.deps, input) } getUsdRate(input: Asset): Promise { diff --git a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts index 448e80fb5c6..1b1c4e35b9b 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts @@ -1,15 +1,16 @@ import { btcChainId, ethChainId } from '@shapeshiftoss/caip' import type { ethereum } from '@shapeshiftoss/chain-adapters' +import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import { KnownChainIds } from '@shapeshiftoss/types' import type { AxiosStatic } from 'axios' import type Web3 from 'web3' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import { normalizeAmount } from '../../utils/helpers/helpers' +import { gasFeeData } from '../../utils/test-data/setupDeps' import { setupQuote } from '../../utils/test-data/setupSwapQuote' -import { baseUrlFromChainId } from '../utils/helpers/helpers' import { zrxServiceFactory } from '../utils/zrxService' -import { ZrxSwapper } from '../ZrxSwapper' +import { getZrxTradeQuote } from './getZrxTradeQuote' jest.mock('lib/swapper/swappers/ZrxSwapper/utils/zrxService', () => { const axios: AxiosStatic = jest.createMockFromModule('axios') @@ -22,40 +23,47 @@ jest.mock('lib/swapper/swappers/ZrxSwapper/utils/zrxService', () => { const zrxService = zrxServiceFactory('https://api.0x.org/') -jest.mock('../utils/helpers/helpers') +jest.mock('../utils/helpers/helpers', () => ({ + ...jest.requireActual('../utils/helpers/helpers'), + getUsdRate: jest.fn(), + baseUrlFromChainId: () => 'https://api.0x.org/', +})) jest.mock('../../utils/helpers/helpers') jest.mock('../utils/zrxService') +jest.mock('@shapeshiftoss/chain-adapters') +jest.mocked(isEvmChainId).mockReturnValue(true) describe('getZrxTradeQuote', () => { const sellAmount = '1000000000000000000' ;(normalizeAmount as jest.Mock).mockReturnValue(sellAmount) - ;(baseUrlFromChainId as jest.Mock).mockReturnValue('https://api.0x.org/') const zrxSwapperDeps = { web3: {} as Web3, adapter: { getChainId: () => KnownChainIds.EthereumMainnet, + getGasFeeData: () => Promise.resolve(gasFeeData), } as ethereum.ChainAdapter, } it('returns quote with fee data', async () => { const { quoteInput } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue( Promise.resolve({ - data: { price: '100', gasPrice: '1000', estimatedGas: '1000000' }, + data: { price: '100', gasPrice: '1000', gas: '1000000' }, }), ) - const maybeQuote = await swapper.getTradeQuote(quoteInput) + const maybeQuote = await getZrxTradeQuote(zrxSwapperDeps, quoteInput) expect(maybeQuote.isErr()).toBe(false) const quote = maybeQuote.unwrap() expect(quote.feeData).toStrictEqual({ chainSpecific: { - estimatedGasCryptoBaseUnit: '1500000', - gasPriceCryptoBaseUnit: '1000', - approvalFeeCryptoBaseUnit: '100000000', + estimatedGasCryptoBaseUnit: '1000000', + gasPriceCryptoBaseUnit: '79036500000', + approvalFeeCryptoBaseUnit: '21621475811200000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, buyAssetTradeFeeUsd: '0', - networkFeeCryptoBaseUnit: '1500000000', + networkFeeCryptoBaseUnit: '216214758112000000', sellAssetTradeFeeUsd: '0', }) expect(quote.rate).toBe('100') @@ -63,17 +71,13 @@ describe('getZrxTradeQuote', () => { it('returns an Err with a bad zrx response with no error indicated', async () => { const { quoteInput } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue(Promise.resolve({})) - const maybeTradeQuote = await swapper.getTradeQuote({ - ...quoteInput, - }) + const maybeTradeQuote = await getZrxTradeQuote(zrxSwapperDeps, quoteInput) expect(maybeTradeQuote.isErr()).toBe(true) expect(maybeTradeQuote.unwrapErr()).toMatchObject({ cause: undefined, code: 'TRADE_QUOTE_FAILED', - details: undefined, message: '[getZrxTradeQuote] Bad ZRX response, no data was returned', name: 'SwapError', }) @@ -81,14 +85,11 @@ describe('getZrxTradeQuote', () => { it('returns an Err with on errored zrx response', async () => { const { quoteInput } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockRejectedValue({ response: { data: { code: 502, reason: 'Failed to do some stuff' } }, } as never) - const maybeTradeQuote = await swapper.getTradeQuote({ - ...quoteInput, - }) + const maybeTradeQuote = await getZrxTradeQuote(zrxSwapperDeps, quoteInput) expect(maybeTradeQuote.isErr()).toBe(true) expect(maybeTradeQuote.unwrapErr()).toMatchObject({ @@ -100,23 +101,24 @@ describe('getZrxTradeQuote', () => { }) }) - it('returns quote without fee data', async () => { + it('returns quote without gas limit', async () => { const { quoteInput } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue( Promise.resolve({ data: { price: '100' }, }), ) - const maybeQuote = await swapper.getTradeQuote(quoteInput) + const maybeQuote = await getZrxTradeQuote(zrxSwapperDeps, quoteInput) expect(maybeQuote.isErr()).toBe(false) const quote = maybeQuote.unwrap() expect(quote?.feeData).toStrictEqual({ chainSpecific: { estimatedGasCryptoBaseUnit: '0', - approvalFeeCryptoBaseUnit: '0', - gasPriceCryptoBaseUnit: undefined, + approvalFeeCryptoBaseUnit: '21621475811200000', + gasPriceCryptoBaseUnit: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, sellAssetTradeFeeUsd: '0', buyAssetTradeFeeUsd: '0', @@ -126,9 +128,8 @@ describe('getZrxTradeQuote', () => { it('returns an Err on non ethereum chain for buyAsset', async () => { const { quoteInput, buyAsset } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue(Promise.resolve()) - const maybeTradeQuote = await swapper.getTradeQuote({ + const maybeTradeQuote = await getZrxTradeQuote(zrxSwapperDeps, { ...quoteInput, buyAsset: { ...buyAsset, chainId: btcChainId }, }) @@ -141,43 +142,38 @@ describe('getZrxTradeQuote', () => { buyAssetChainId: btcChainId, sellAssetChainId: ethChainId, }, - message: - '[getZrxTradeQuote] - Both assets need to be on the same supported EVM chain to use Zrx', + message: `[assertValidTradePair] - both assets must be on chainId eip155:1`, name: 'SwapError', }) }) it('returns an Err on non ethereum chain for sellAsset', async () => { const { quoteInput, sellAsset } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue(Promise.resolve()) - const maybeTradeQuote = await swapper.getTradeQuote({ + const maybeTradeQuote = await getZrxTradeQuote(zrxSwapperDeps, { ...quoteInput, sellAsset: { ...sellAsset, chainId: btcChainId }, }) expect(maybeTradeQuote.isErr()).toBe(true) expect(maybeTradeQuote.unwrapErr()).toMatchObject({ - cause: undefined, code: 'UNSUPPORTED_PAIR', details: { buyAssetChainId: ethChainId, sellAssetChainId: btcChainId, }, - message: - '[getZrxTradeQuote] - Both assets need to be on the same supported EVM chain to use Zrx', + message: '[assertValidTradePair] - both assets must be on chainId eip155:1', name: 'SwapError', }) }) it('use minQuoteSellAmount when sellAmount is 0', async () => { const { quoteInput, sellAsset } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue( Promise.resolve({ data: { sellAmount: '20000000000000000000' } }), ) const minimum = '20' - const maybeQuote = await swapper.getTradeQuote({ + const maybeQuote = await getZrxTradeQuote(zrxSwapperDeps, { ...quoteInput, sellAmountBeforeFeesCryptoBaseUnit: '0', }) diff --git a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index 002649c5106..1eebabcc759 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -1,15 +1,15 @@ import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import type { AxiosResponse } from 'axios' -import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import type { GetEvmTradeQuoteInput, SwapErrorRight, SwapSource, TradeQuote } from 'lib/swapper/api' +import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' +import type { GetEvmTradeQuoteInput, SwapErrorRight, TradeQuote } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' import { APPROVAL_GAS_LIMIT } from 'lib/swapper/swappers/utils/constants' import { normalizeAmount } from 'lib/swapper/swappers/utils/helpers/helpers' import { getZrxMinMax } from 'lib/swapper/swappers/ZrxSwapper/getZrxMinMax/getZrxMinMax' -import type { ZrxPriceResponse } from 'lib/swapper/swappers/ZrxSwapper/types' -import { DEFAULT_SOURCE } from 'lib/swapper/swappers/ZrxSwapper/utils/constants' +import type { ZrxPriceResponse, ZrxSwapperDeps } from 'lib/swapper/swappers/ZrxSwapper/types' +import { AFFILIATE_ADDRESS, DEFAULT_SOURCE } from 'lib/swapper/swappers/ZrxSwapper/utils/constants' import { + assertValidTradePair, assetToToken, baseUrlFromChainId, } from 'lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers' @@ -17,27 +17,18 @@ import { zrxServiceFactory } from 'lib/swapper/swappers/ZrxSwapper/utils/zrxServ import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' export async function getZrxTradeQuote( + { adapter }: ZrxSwapperDeps, input: GetEvmTradeQuoteInput, ): Promise, SwapErrorRight>> { try { - const { - sellAsset, - buyAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - accountNumber, - } = input - if (buyAsset.chainId !== input.chainId || sellAsset.chainId !== input.chainId) { - throw new SwapError( - '[getZrxTradeQuote] - Both assets need to be on the same supported EVM chain to use Zrx', - { - code: SwapErrorType.UNSUPPORTED_PAIR, - details: { buyAssetChainId: buyAsset.chainId, sellAssetChainId: sellAsset.chainId }, - }, - ) - } + const { sellAsset, buyAsset, accountNumber, receiveAddress } = input + const sellAmount = input.sellAmountBeforeFeesCryptoBaseUnit - const buyToken = assetToToken(buyAsset) - const sellToken = assetToToken(sellAsset) + const assertion = assertValidTradePair({ adapter, buyAsset, sellAsset }) + if (assertion.isErr()) return Err(assertion.unwrapErr()) + + const baseUrl = baseUrlFromChainId(buyAsset.chainId) + const zrxService = zrxServiceFactory(baseUrl) const { minimumAmountCryptoHuman, maximumAmountCryptoHuman } = await getZrxMinMax( sellAsset, @@ -48,87 +39,69 @@ export async function getZrxTradeQuote( ) const normalizedSellAmount = normalizeAmount( - bnOrZero(sellAmountCryptoBaseUnit).eq(0) - ? minQuoteSellAmountCryptoBaseUnit - : sellAmountCryptoBaseUnit, + bnOrZero(sellAmount).eq(0) ? minQuoteSellAmountCryptoBaseUnit : sellAmount, ) - const baseUrl = baseUrlFromChainId(buyAsset.chainId) - const zrxService = zrxServiceFactory(baseUrl) - /** - * /swap/v1/price - * params: { - * sellToken: contract address (or symbol) of token to sell - * buyToken: contractAddress (or symbol) of token to buy - * sellAmount?: integer string value of the smallest increment of the sell token - * buyAmount?: integer string value of the smallest increment of the buy token - * } - */ - const quoteResponse: AxiosResponse = await zrxService.get( - '/swap/v1/price', - { - params: { - sellToken, - buyToken, - sellAmount: normalizedSellAmount, - }, + // https://docs.0x.org/0x-swap-api/api-references/get-swap-v1-price + const { data } = await zrxService.get('/swap/v1/price', { + params: { + buyToken: assetToToken(buyAsset), + sellToken: assetToToken(sellAsset), + sellAmount: normalizedSellAmount, + takerAddress: receiveAddress, + affiliateAddress: AFFILIATE_ADDRESS, + skipValidation: true, }, - ) + }) - if (!quoteResponse.data) + if (!data) { return Err( makeSwapErrorRight({ message: '[getZrxTradeQuote] Bad ZRX response, no data was returned', code: SwapErrorType.TRADE_QUOTE_FAILED, }), ) + } - const { - data: { - estimatedGas: estimatedGasResponse, - gasPrice: gasPriceCryptoBaseUnit, - price, - sellAmount: sellAmountResponse, - buyAmount, - sources, - allowanceTarget, - }, - } = quoteResponse - - const useSellAmount = !!sellAmountCryptoBaseUnit - const rate = useSellAmount ? price : bn(1).div(price).toString() + const { average, fast } = await adapter.getGasFeeData() - const estimatedGas = bnOrZero(estimatedGasResponse).times(1.5) - const fee = estimatedGas.multipliedBy(bnOrZero(gasPriceCryptoBaseUnit)).toString() + // use worst case average eip1559 vs fast legacy + const maxGasPrice = bnOrZero(BigNumber.max(average.maxFeePerGas ?? 0, fast.gasPrice)) // 0x approvals are cheaper than trades, but we don't have dynamic quote data for them. // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the 0x quote response. - const approvalFeeCryptoBaseUnit = bnOrZero(APPROVAL_GAS_LIMIT) - .multipliedBy(bnOrZero(gasPriceCryptoBaseUnit)) - .toFixed() + const approvalFeeCryptoBaseUnit = bn(APPROVAL_GAS_LIMIT).times(maxGasPrice).toFixed(0) + + const useSellAmount = !!sellAmount + const rate = useSellAmount ? data.price : bn(1).div(data.price).toString() + const gasLimit = bnOrZero(data.gas) + const txFee = gasLimit.times(maxGasPrice) const tradeQuote: TradeQuote = { + buyAsset, + sellAsset, + accountNumber, rate, minimumCryptoHuman: minimumAmountCryptoHuman, maximumCryptoHuman: maximumAmountCryptoHuman, feeData: { chainSpecific: { - estimatedGasCryptoBaseUnit: estimatedGas.toString(), - gasPriceCryptoBaseUnit, + estimatedGasCryptoBaseUnit: gasLimit.toFixed(0), + gasPriceCryptoBaseUnit: fast.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.maxFeePerGas, + maxPriorityFeePerGas: average.maxPriorityFeePerGas, approvalFeeCryptoBaseUnit, }, - networkFeeCryptoBaseUnit: fee, + networkFeeCryptoBaseUnit: txFee.toFixed(0), buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '0', }, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountResponse, - buyAmountCryptoBaseUnit: buyAmount, - sources: sources?.filter((s: SwapSource) => parseFloat(s.proportion) > 0) || DEFAULT_SOURCE, - allowanceContract: allowanceTarget, - buyAsset, - sellAsset, - accountNumber, + allowanceContract: data.allowanceTarget, + buyAmountCryptoBaseUnit: data.buyAmount, + sellAmountBeforeFeesCryptoBaseUnit: data.sellAmount, + sources: data.sources?.filter(s => parseFloat(s.proportion) > 0) || DEFAULT_SOURCE, } + return Ok(tradeQuote as TradeQuote) } catch (e) { // TODO(gomes): scrutinize what can throw above and don't throw, because monads diff --git a/src/lib/swapper/swappers/ZrxSwapper/types.ts b/src/lib/swapper/swappers/ZrxSwapper/types.ts index 88a2a38cedd..a11ec3deaca 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/types.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/types.ts @@ -8,6 +8,8 @@ import type { export type ZrxCommonResponse = { price: string + estimatedGas: string + gas: string gasPrice: string buyAmount: string sellAmount: string @@ -15,14 +17,12 @@ export type ZrxCommonResponse = { sources: SwapSource[] } -export type ZrxPriceResponse = ZrxCommonResponse & { - estimatedGas: string -} +export type ZrxPriceResponse = ZrxCommonResponse export type ZrxQuoteResponse = ZrxCommonResponse & { to: string data: string - gas: string + value: string } export interface ZrxTrade extends Trade { diff --git a/src/lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers.ts b/src/lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers.ts index d22b2630fbc..eb63adf5723 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers.ts @@ -9,12 +9,17 @@ import { polygonAssetId, } from '@shapeshiftoss/caip' import { KnownChainIds } from '@shapeshiftoss/types' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' import type { AxiosResponse } from 'axios' import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import { SwapError, SwapErrorType } from 'lib/swapper/api' +import type { SwapErrorRight } from 'lib/swapper/api' +import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' import type { ZrxPriceResponse } from 'lib/swapper/swappers/ZrxSwapper/types' import { zrxServiceFactory } from 'lib/swapper/swappers/ZrxSwapper/utils/zrxService' +import type { ZrxSupportedChainAdapter } from '../../ZrxSwapper' + export const baseUrlFromChainId = (chainId: string): string => { switch (chainId) { case KnownChainIds.EthereumMainnet: @@ -112,3 +117,28 @@ export const getUsdRate = async (sellAsset: Asset): Promise => { }) } } + +export const assertValidTradePair = ({ + buyAsset, + sellAsset, + adapter, +}: { + buyAsset: Asset + sellAsset: Asset + adapter: ZrxSupportedChainAdapter +}): Result => { + const chainId = adapter.getChainId() + + if (buyAsset.chainId === chainId && sellAsset.chainId === chainId) return Ok(true) + + return Err( + makeSwapErrorRight({ + message: `[assertValidTradePair] - both assets must be on chainId ${chainId}`, + code: SwapErrorType.UNSUPPORTED_PAIR, + details: { + buyAssetChainId: buyAsset.chainId, + sellAssetChainId: sellAsset.chainId, + }, + }), + ) +} diff --git a/src/lib/swapper/swappers/ZrxSwapper/utils/test-data/setupZrxSwapQuote.ts b/src/lib/swapper/swappers/ZrxSwapper/utils/test-data/setupZrxSwapQuote.ts index 9b4ff1c1eef..ced9bf213fe 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/utils/test-data/setupZrxSwapQuote.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/utils/test-data/setupZrxSwapQuote.ts @@ -12,7 +12,9 @@ export const setupZrxTradeQuoteResponse = () => { to: '0x123', data: '0x1234', gas: '1235', + estimatedGas: '1235', gasPrice: '1236', + value: '0', sources: [], buyAmount: '', } diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.test.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.test.ts index 9b7fdb3ce42..fb1310548ab 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.test.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.test.ts @@ -59,7 +59,7 @@ describe('zrxApprovalNeeded', () => { wallet, } - await expect(zrxApprovalNeeded(deps, input)).rejects.toThrow('[zrxApprovalNeeded]') + await expect(zrxApprovalNeeded(deps, input)).rejects.toThrow() }) it('returns false if allowanceOnChain is greater than quote.sellAmount', async () => { diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.ts index af7af68edf8..5d265be421c 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.ts @@ -7,19 +7,22 @@ import { getERC20Allowance } from 'lib/swapper/swappers/utils/helpers/helpers' import type { ZrxSwapperDeps } from 'lib/swapper/swappers/ZrxSwapper/types' import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' +import { assertValidTradePair } from '../utils/helpers/helpers' + export async function zrxApprovalNeeded( { adapter, web3 }: ZrxSwapperDeps, { quote, wallet }: ApprovalNeededInput, ): Promise { - const { sellAsset } = quote - - const { assetReference: sellAssetErc20Address } = fromAssetId(sellAsset.assetId) - try { - if (sellAsset.chainId !== adapter.getChainId()) { - throw new SwapError('[zrxApprovalNeeded] - sellAsset chainId is not supported', { - code: SwapErrorType.UNSUPPORTED_CHAIN, - details: { chainId: sellAsset.chainId }, + const { accountNumber, allowanceContract, buyAsset, sellAsset } = quote + const sellAmount = quote.sellAmountBeforeFeesCryptoBaseUnit + + const assertion = assertValidTradePair({ adapter, buyAsset, sellAsset }) + if (assertion.isErr()) { + const { message, code, details } = assertion.unwrapErr() + throw new SwapError(message, { + code, + details: details as Record | undefined, }) } @@ -28,33 +31,25 @@ export async function zrxApprovalNeeded( return { approvalNeeded: false } } - const { accountNumber } = quote - - const receiveAddress = await adapter.getAddress({ accountNumber, wallet }) - - if (!quote.allowanceContract) { - throw new SwapError('[zrxApprovalNeeded] - allowanceTarget is required', { + if (!allowanceContract) { + throw new SwapError('[zrxApprovalNeeded] - quote contains no allowanceContract', { code: SwapErrorType.VALIDATION_FAILED, - details: { chainId: sellAsset.chainId }, + details: { quote }, }) } - const allowanceResult = await getERC20Allowance({ + const receiveAddress = await adapter.getAddress({ accountNumber, wallet }) + + const allowance = await getERC20Allowance({ web3, erc20AllowanceAbi, - sellAssetErc20Address, - spenderAddress: quote.allowanceContract, + sellAssetErc20Address: fromAssetId(sellAsset.assetId).assetReference, + spenderAddress: allowanceContract, ownerAddress: receiveAddress, }) - const allowanceOnChain = bnOrZero(allowanceResult) - if (!quote.feeData.chainSpecific?.gasPriceCryptoBaseUnit) - throw new SwapError('[zrxApprovalNeeded] - no gas price with quote', { - code: SwapErrorType.RESPONSE_ERROR, - details: { feeData: quote.feeData }, - }) return { - approvalNeeded: allowanceOnChain.lt(bnOrZero(quote.sellAmountBeforeFeesCryptoBaseUnit)), + approvalNeeded: bnOrZero(allowance).lt(bnOrZero(sellAmount)), } } catch (e) { if (e instanceof SwapError) throw e diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxApprove/zrxApprove.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxApprove/zrxApprove.ts index 095de5d9fc7..63902ea9968 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxApprove/zrxApprove.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxApprove/zrxApprove.ts @@ -1,67 +1,30 @@ -import type { ApproveAmountInput, ApproveInfiniteInput, TradeQuote } from 'lib/swapper/api' -import { SwapError, SwapErrorType } from 'lib/swapper/api' -import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' -import { APPROVAL_GAS_LIMIT } from 'lib/swapper/swappers/utils/constants' +import { fromAssetId } from '@shapeshiftoss/caip' +import type { ApproveAmountInput, ApproveInfiniteInput } from 'lib/swapper/api' import { grantAllowance } from 'lib/swapper/swappers/utils/helpers/helpers' import type { ZrxSwapperDeps } from 'lib/swapper/swappers/ZrxSwapper/types' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/ZrxSwapper/utils/constants' import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' -const grantAllowanceForAmount = ( - { adapter, web3 }: ZrxSwapperDeps, - { quote, wallet }: ApproveInfiniteInput, - approvalAmount: string, -) => { - const approvalQuote: TradeQuote = { - ...quote, - sellAmountBeforeFeesCryptoBaseUnit: approvalAmount, - feeData: { - ...quote.feeData, - chainSpecific: { - ...quote.feeData.chainSpecific, - // 0x approvals are cheaper than trades, but we don't have dynamic quote data for them. - // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the 0x quote response. - estimatedGas: APPROVAL_GAS_LIMIT, - }, - }, - } - return grantAllowance({ - quote: approvalQuote, - wallet, - adapter, - erc20Abi, - web3, - }) -} - export function zrxApproveAmount( deps: ZrxSwapperDeps, - args: ApproveAmountInput, -) { - try { - // If no amount is specified we use the quotes sell amount - const approvalAmount = args.amount ?? args.quote.sellAmountBeforeFeesCryptoBaseUnit - return grantAllowanceForAmount(deps, args, approvalAmount) - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[zrxApproveAmount]', { - cause: e, - code: SwapErrorType.APPROVE_AMOUNT_FAILED, - }) - } + { quote, wallet, amount }: ApproveAmountInput, +): Promise { + const { accountNumber, allowanceContract, feeData, sellAsset } = quote + + return grantAllowance({ + ...deps, + accountNumber, + spender: allowanceContract, + feeData: feeData.chainSpecific, + approvalAmount: amount ?? quote.sellAmountBeforeFeesCryptoBaseUnit, + to: fromAssetId(sellAsset.assetId).assetReference, + wallet, + }) } export function zrxApproveInfinite( deps: ZrxSwapperDeps, - args: ApproveInfiniteInput, -) { - try { - return grantAllowanceForAmount(deps, args, MAX_ALLOWANCE) - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[zrxApproveInfinite]', { - cause: e, - code: SwapErrorType.APPROVE_INFINITE_FAILED, - }) - } + input: ApproveInfiniteInput, +): Promise { + return zrxApproveAmount(deps, { ...input, amount: MAX_ALLOWANCE }) } diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.test.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.test.ts index 485578edd75..bd6e733abb4 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.test.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.test.ts @@ -4,10 +4,9 @@ import { KnownChainIds } from '@shapeshiftoss/types' import * as unchained from '@shapeshiftoss/unchained-client' import type { AxiosStatic } from 'axios' import Web3 from 'web3' -import { bnOrZero } from 'lib/bignumber/bignumber' import type { BuildTradeInput, QuoteFeeData } from '../../../api' -import { APPROVAL_GAS_LIMIT } from '../../utils/constants' +import { feeData } from '../../utils/test-data/setupDeps' import type { ZrxTrade } from '../types' import { setupZrxTradeQuoteResponse } from '../utils/test-data/setupZrxSwapQuote' import { zrxServiceFactory } from '../utils/zrxService' @@ -53,6 +52,7 @@ const setup = () => { }, rpcUrl: ethNodeUrl, }) + adapter.getFeeData = () => Promise.resolve(feeData) const zrxService = zrxServiceFactory('https://api.0x.org/') return { web3Instance, adapter, zrxService } @@ -93,13 +93,12 @@ describe('zrxBuildTrade', () => { rate: quoteResponse.price, feeData: { chainSpecific: { - approvalFeeCryptoBaseUnit: '123600000', estimatedGasCryptoBaseUnit: '1235', - gasPriceCryptoBaseUnit: '1236', + gasPriceCryptoBaseUnit: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, - networkFeeCryptoBaseUnit: ( - Number(quoteResponse.gas) * Number(quoteResponse.gasPrice) - ).toString(), + networkFeeCryptoBaseUnit: '21621475811200000', sellAssetTradeFeeUsd: '0', buyAssetTradeFeeUsd: '0', }, @@ -154,29 +153,24 @@ describe('zrxBuildTrade', () => { }) it('should return a quote response with gasPrice multiplied by estimatedGas', async () => { - const gasPriceCryptoBaseUnit = '10000' - const estimatedGas = '100' const data = { ...quoteResponse, allowanceTarget: 'allowanceTargetAddress', - gas: estimatedGas, - gasPrice: gasPriceCryptoBaseUnit, + gas: '100', + gasPrice: '10000', } ;(zrxService.get as jest.Mock).mockReturnValue(Promise.resolve({ data })) const expectedFeeData: QuoteFeeData = { chainSpecific: { - approvalFeeCryptoBaseUnit: bnOrZero(APPROVAL_GAS_LIMIT) - .multipliedBy(gasPriceCryptoBaseUnit) - .toString(), - gasPriceCryptoBaseUnit, - estimatedGasCryptoBaseUnit: estimatedGas, + gasPriceCryptoBaseUnit: '79036500000', + estimatedGasCryptoBaseUnit: '100', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '0', - networkFeeCryptoBaseUnit: bnOrZero(gasPriceCryptoBaseUnit) - .multipliedBy(estimatedGas) - .toString(), + networkFeeCryptoBaseUnit: '21621475811200000', } const maybeBuiltTrade = await zrxBuildTrade(deps, { ...buildTradeInput, wallet }) diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts index f9b4638003a..169476bbf5a 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts @@ -2,12 +2,11 @@ import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' import type { AxiosResponse } from 'axios' import * as rax from 'retry-axios' -import { bnOrZero } from 'lib/bignumber/bignumber' +import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import type { BuildTradeInput, SwapErrorRight } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' -import { erc20AllowanceAbi } from 'lib/swapper/swappers/utils/abi/erc20Allowance-abi' -import { APPROVAL_GAS_LIMIT, DEFAULT_SLIPPAGE } from 'lib/swapper/swappers/utils/constants' -import { isApprovalRequired, normalizeAmount } from 'lib/swapper/swappers/utils/helpers/helpers' +import { DEFAULT_SLIPPAGE } from 'lib/swapper/swappers/utils/constants' +import { normalizeAmount } from 'lib/swapper/swappers/utils/helpers/helpers' import type { ZrxQuoteResponse, ZrxSwapperDeps, @@ -16,6 +15,7 @@ import type { import { applyAxiosRetry } from 'lib/swapper/swappers/ZrxSwapper/utils/applyAxiosRetry' import { AFFILIATE_ADDRESS, DEFAULT_SOURCE } from 'lib/swapper/swappers/ZrxSwapper/utils/constants' import { + assertValidTradePair, assetToToken, baseUrlFromChainId, } from 'lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers' @@ -23,45 +23,18 @@ import { zrxServiceFactory } from 'lib/swapper/swappers/ZrxSwapper/utils/zrxServ import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' export async function zrxBuildTrade( - { adapter, web3 }: ZrxSwapperDeps, + { adapter }: ZrxSwapperDeps, input: BuildTradeInput, ): Promise, SwapErrorRight>> { - const { - sellAsset, - buyAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountExcludeFeeCryptoBaseUnit, - slippage, - accountNumber, - receiveAddress, - } = input try { - const adapterChainId = adapter.getChainId() + const { sellAsset, buyAsset, slippage, accountNumber, receiveAddress } = input + const sellAmount = input.sellAmountBeforeFeesCryptoBaseUnit - if (buyAsset.chainId !== adapterChainId) { - return Err( - makeSwapErrorRight({ - message: `[zrxBuildTrade] - buyAsset must be on chainId ${adapterChainId}`, - code: SwapErrorType.VALIDATION_FAILED, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - const slippagePercentage = slippage ? bnOrZero(slippage).toString() : DEFAULT_SLIPPAGE + const assertion = assertValidTradePair({ adapter, buyAsset, sellAsset }) + if (assertion.isErr()) return Err(assertion.unwrapErr()) const baseUrl = baseUrlFromChainId(buyAsset.chainId) - const zrxService = zrxServiceFactory(baseUrl) - - /** - * /swap/v1/quote - * params: { - * sellToken: contract address (or symbol) of token to sell - * buyToken: contractAddress (or symbol) of token to buy - * sellAmount?: integer string value of the smallest increment of the sell token - * } - */ - - const zrxRetry = applyAxiosRetry(zrxService, { + const zrxService = applyAxiosRetry(zrxServiceFactory(baseUrl), { statusCodesToRetry: [[400, 400]], shouldRetry: err => { const cfg = rax.getConfig(err) @@ -76,76 +49,65 @@ export async function zrxBuildTrade( return rax.shouldRetryRequest(err) }, }) - const quoteResponse: AxiosResponse = await zrxRetry.get( + + // https://docs.0x.org/0x-swap-api/api-references/get-swap-v1-quote + const { data: quote }: AxiosResponse = await zrxService.get( '/swap/v1/quote', { params: { buyToken: assetToToken(buyAsset), sellToken: assetToToken(sellAsset), - sellAmount: normalizeAmount(sellAmountExcludeFeeCryptoBaseUnit), + sellAmount: normalizeAmount(sellAmount), takerAddress: receiveAddress, - slippagePercentage, - skipValidation: false, + slippagePercentage: slippage ? bnOrZero(slippage).toString() : DEFAULT_SLIPPAGE, affiliateAddress: AFFILIATE_ADDRESS, + skipValidation: false, }, }, ) - const { - data: { - allowanceTarget, - sellAmount, - gasPrice: gasPriceCryptoBaseUnit, - gas: gasCryptoBaseUnit, - price, - to, - buyAmount: buyAmountCryptoBaseUnit, - data: txData, - sources, + const { average, fast } = await adapter.getFeeData({ + to: quote.to, + value: quote.value, + chainSpecific: { + from: receiveAddress, + contractAddress: quote.to, + contractData: quote.data, }, - } = quoteResponse - - const estimatedGas = bnOrZero(gasCryptoBaseUnit || 0) - const networkFee = bnOrZero(estimatedGas) - .multipliedBy(bnOrZero(gasPriceCryptoBaseUnit)) - .toString() - - const approvalRequired = await isApprovalRequired({ - adapter, - sellAsset, - allowanceContract: allowanceTarget, - receiveAddress, - sellAmountExcludeFeeCryptoBaseUnit, - web3, - erc20AllowanceAbi, }) - const approvalFee = bnOrZero(APPROVAL_GAS_LIMIT) - .multipliedBy(bnOrZero(gasPriceCryptoBaseUnit)) - .toString() + // use worst case average eip1559 vs fast legacy + const maxGasPrice = BigNumber.max( + average.chainSpecific.maxFeePerGas ?? 0, + fast.chainSpecific.gasPrice, + ) + + const txFee = bnOrZero(bn(fast.chainSpecific.gasLimit).times(maxGasPrice)) const trade: ZrxTrade = { sellAsset, buyAsset, accountNumber, receiveAddress, - rate: price, - depositAddress: to, + rate: quote.price, + depositAddress: quote.to, feeData: { chainSpecific: { - estimatedGasCryptoBaseUnit: estimatedGas.toString(), - gasPriceCryptoBaseUnit, - approvalFeeCryptoBaseUnit: approvalRequired ? approvalFee : undefined, + estimatedGasCryptoBaseUnit: quote.gas, + gasPriceCryptoBaseUnit: fast.chainSpecific.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.chainSpecific.maxFeePerGas, + maxPriorityFeePerGas: average.chainSpecific.maxPriorityFeePerGas, }, - networkFeeCryptoBaseUnit: networkFee, + networkFeeCryptoBaseUnit: txFee.toFixed(0), buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '0', }, - txData, - sellAmountBeforeFeesCryptoBaseUnit: sellAmount, - buyAmountCryptoBaseUnit, - sources: sources?.filter(s => parseFloat(s.proportion) > 0) || DEFAULT_SOURCE, + txData: quote.data, + buyAmountCryptoBaseUnit: quote.buyAmount, + sellAmountBeforeFeesCryptoBaseUnit: quote.sellAmount, + sources: quote.sources?.filter(s => parseFloat(s.proportion) > 0) || DEFAULT_SOURCE, } + return Ok(trade as ZrxTrade) } catch (e) { if (e instanceof SwapError) @@ -158,7 +120,7 @@ export async function zrxBuildTrade( ) return Err( makeSwapErrorRight({ - message: '[[zrxBuildTrade]]', + message: '[zrxBuildTrade]', cause: e, code: SwapErrorType.BUILD_TRADE_FAILED, }), diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.test.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.test.ts index de786cb5859..109a1c22180 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.test.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.test.ts @@ -2,6 +2,7 @@ import type { ChainAdapter } from '@shapeshiftoss/chain-adapters' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import type { KnownChainIds } from '@shapeshiftoss/types' +import { gasFeeData } from '../../utils/test-data/setupDeps' import { setupQuote } from '../../utils/test-data/setupSwapQuote' import type { ZrxExecuteTradeInput, ZrxSwapperDeps, ZrxTrade } from '../types' import { zrxExecuteTrade } from './zrxExecuteTrade' @@ -10,14 +11,17 @@ describe('ZrxExecuteTrade', () => { const { sellAsset, buyAsset } = setupQuote() const txid = '0xffaac3dd529171e8a9a2adaf36b0344877c4894720d65dfd86e4b3a56c5a857e' let wallet = { + _supportsETH: true, supportsOfflineSigning: jest.fn(() => true), + ethSupportsEIP1559: jest.fn(() => false), } as unknown as HDWallet const adapter = { - buildSendTransaction: jest.fn(() => Promise.resolve({ txToSign: '0000000000000000' })), + buildCustomTx: jest.fn(() => Promise.resolve({ txToSign: '0000000000000000' })), signTransaction: jest.fn(() => Promise.resolve('0000000000000000000')), broadcastTransaction: jest.fn(() => Promise.resolve(txid)), signAndBroadcastTransaction: jest.fn(() => Promise.resolve(txid)), + getGasFeeData: jest.fn(() => Promise.resolve(gasFeeData)), } as unknown as ChainAdapter<'eip155:1'> const deps = { adapter } as unknown as ZrxSwapperDeps diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.ts index da855891373..83fb949f13e 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.ts @@ -1,9 +1,8 @@ import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import { numberToHex } from 'web3-utils' import type { SwapErrorRight, TradeResult } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' -import { isNativeEvmAsset } from 'lib/swapper/swappers/utils/helpers/helpers' +import { buildAndBroadcast, isNativeEvmAsset } from 'lib/swapper/swappers/utils/helpers/helpers' import type { ZrxExecuteTradeInput, ZrxSwapperDeps } from 'lib/swapper/swappers/ZrxSwapper/types' import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' @@ -11,56 +10,24 @@ export async function zrxExecuteTrade( { adapter }: ZrxSwapperDeps, { trade, wallet }: ZrxExecuteTradeInput, ): Promise> { - const { accountNumber, sellAsset } = trade + const { accountNumber, depositAddress, feeData, sellAsset, txData } = trade + const { sellAmountBeforeFeesCryptoBaseUnit } = trade try { - // value is 0 for erc20s - const value = isNativeEvmAsset(sellAsset.assetId) - ? trade.sellAmountBeforeFeesCryptoBaseUnit - : '0' - - const buildTxResponse = await adapter.buildSendTransaction({ - value, - wallet, - to: trade.depositAddress, - chainSpecific: { - gasPrice: numberToHex(trade.feeData?.chainSpecific?.gasPriceCryptoBaseUnit || 0), - gasLimit: numberToHex(trade.feeData?.chainSpecific?.estimatedGasCryptoBaseUnit || 0), - }, + const txid = await buildAndBroadcast({ accountNumber, + adapter, + feeData: feeData.chainSpecific, + to: depositAddress, + value: isNativeEvmAsset(sellAsset.assetId) ? sellAmountBeforeFeesCryptoBaseUnit : '0', + wallet, + data: txData, }) - const { txToSign } = buildTxResponse - - const txWithQuoteData = { ...txToSign, data: trade.txData ?? '' } - - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ txToSign: txWithQuoteData, wallet }) - - const txid = await adapter.broadcastTransaction(signedTx) - - return Ok({ tradeId: txid }) - } else if (wallet.supportsBroadcast() && adapter.signAndBroadcastTransaction) { - const txid = await adapter.signAndBroadcastTransaction?.({ - txToSign: txWithQuoteData, - wallet, - }) - - return Ok({ tradeId: txid }) - } else { - throw new SwapError('[zrxExecuteTrade]', { - code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, - }) - } + return Ok({ tradeId: txid }) } catch (e) { if (e instanceof SwapError) - return Err( - makeSwapErrorRight({ - message: e.message, - code: e.code, - details: e.details, - }), - ) + return Err(makeSwapErrorRight({ message: e.message, code: e.code, details: e.details })) return Err( makeSwapErrorRight({ message: '[zrxExecuteTrade]', diff --git a/src/lib/swapper/swappers/utils/helpers/helpers.test.ts b/src/lib/swapper/swappers/utils/helpers/helpers.test.ts index e92d02d4bea..3313c674885 100644 --- a/src/lib/swapper/swappers/utils/helpers/helpers.test.ts +++ b/src/lib/swapper/swappers/utils/helpers/helpers.test.ts @@ -1,8 +1,8 @@ +import { fromAssetId } from '@shapeshiftoss/caip' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import Web3 from 'web3' import { bn } from '../../../../bignumber/bignumber' -import { erc20Abi } from '../abi/erc20-abi' import { erc20AllowanceAbi } from '../abi/erc20Allowance-abi' import { setupDeps } from '../test-data/setupDeps' import { setupQuote } from '../test-data/setupSwapQuote' @@ -97,14 +97,14 @@ describe('utils', () => { describe('grantAllowance', () => { const walletAddress = '0xc770eefad204b5180df6a14ee197d99d808ee52d' const wallet = { + _supportsETH: true, + ethSupportsEIP1559: jest.fn(() => false), supportsOfflineSigning: jest.fn(() => true), ethGetAddress: jest.fn(() => Promise.resolve(walletAddress)), } as unknown as HDWallet it('should return a txid', async () => { - const quote = { - ...tradeQuote, - } + const { accountNumber, allowanceContract: spender, feeData, sellAsset } = tradeQuote ;(web3.eth.Contract as jest.Mock).mockImplementation(() => ({ methods: { approve: jest.fn(() => ({ @@ -114,11 +114,21 @@ describe('utils', () => { })), }, })) - ;(adapter.buildSendTransaction as jest.Mock).mockResolvedValueOnce({ txToSign: {} }) + ;(adapter.buildCustomTx as jest.Mock).mockResolvedValueOnce({ txToSign: {} }) + ;(adapter.signTransaction as jest.Mock).mockResolvedValueOnce('signedTx') ;(adapter.broadcastTransaction as jest.Mock).mockResolvedValueOnce('broadcastedTx') - expect(await grantAllowance({ quote, wallet, adapter, erc20Abi, web3 })).toEqual( - 'broadcastedTx', - ) + expect( + await grantAllowance({ + accountNumber, + feeData: feeData.chainSpecific, + spender, + to: fromAssetId(sellAsset.assetId).assetReference, + approvalAmount: tradeQuote.sellAmountBeforeFeesCryptoBaseUnit, + wallet, + adapter, + web3, + }), + ).toEqual('broadcastedTx') }) }) diff --git a/src/lib/swapper/swappers/utils/helpers/helpers.ts b/src/lib/swapper/swappers/utils/helpers/helpers.ts index d4790847a9e..24c5a3b125f 100644 --- a/src/lib/swapper/swappers/utils/helpers/helpers.ts +++ b/src/lib/swapper/swappers/utils/helpers/helpers.ts @@ -8,18 +8,20 @@ import { optimismAssetId, polygonAssetId, } from '@shapeshiftoss/caip' -import type { EvmChainAdapter, EvmChainId } from '@shapeshiftoss/chain-adapters' +import type { evm, EvmChainAdapter } from '@shapeshiftoss/chain-adapters' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { KnownChainIds } from '@shapeshiftoss/types' import type Web3 from 'web3' import type { AbiItem } from 'web3-utils' -import { numberToHex } from 'web3-utils' import type { BigNumber } from 'lib/bignumber/bignumber' import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import type { TradeQuote } from 'lib/swapper/api' +import type { EvmFeeData } from 'lib/swapper/api' import { SwapError, SwapErrorType } from 'lib/swapper/api' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/CowSwapper/utils/constants' -import { erc20Abi as erc20AbiImported } from 'lib/swapper/swappers/utils/abi/erc20-abi' +import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' + +import { APPROVAL_GAS_LIMIT } from '../constants' export type IsApprovalRequiredArgs = { adapter: EvmChainAdapter @@ -45,11 +47,22 @@ export type GetApproveContractDataArgs = { contractAddress: string } -type GrantAllowanceArgs = { - quote: TradeQuote +type GetFeesFromFeeDataArgs = { wallet: HDWallet + feeData: EvmFeeData +} + +type BuildAndBroadcastArgs = GetFeesFromFeeDataArgs & { + accountNumber: number adapter: EvmChainAdapter - erc20Abi: AbiItem[] + data: string + to: string + value: string +} + +type GrantAllowanceArgs = Omit & { + approvalAmount: string + spender: string web3: Web3 } @@ -110,57 +123,109 @@ export const isApprovalRequired = async ({ } } -export const grantAllowance = async ({ - quote, +export const getFeesFromFeeData = async ({ wallet, - adapter, - erc20Abi, - web3, -}: GrantAllowanceArgs): Promise => { - try { - const { assetReference: sellAssetErc20Address } = fromAssetId(quote.sellAsset.assetId) + feeData, +}: GetFeesFromFeeDataArgs): Promise => { + if (!supportsETH(wallet)) { + throw new SwapError('[getFeesFromFeeData]', { + cause: 'eth wallet required', + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + details: { wallet }, + }) + } - const erc20Contract = new web3.eth.Contract(erc20Abi, sellAssetErc20Address) - const approveTx = erc20Contract.methods - .approve(quote.allowanceContract, quote.sellAmountBeforeFeesCryptoBaseUnit) - .encodeABI() + const gasLimit = feeData.estimatedGasCryptoBaseUnit + const gasPrice = feeData.gasPriceCryptoBaseUnit + const maxFeePerGas = feeData.maxFeePerGas + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas - const { accountNumber } = quote + if (!gasLimit) { + throw new SwapError('[getFeesFromFeeData]', { + cause: 'gasLimit is required', + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + }) + } + + const eip1559Support = await wallet.ethSupportsEIP1559() + + if (eip1559Support && maxFeePerGas && maxPriorityFeePerGas) + return { gasLimit, maxFeePerGas, maxPriorityFeePerGas } + if (gasPrice) return { gasLimit, gasPrice } + + throw new SwapError('[getFeesFromFeeData]', { + cause: 'legacy gas or eip1559 gas required', + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + }) +} - const { txToSign } = await adapter.buildSendTransaction({ +export const buildAndBroadcast = async ({ + accountNumber, + adapter, + data, + feeData, + to, + value, + wallet, +}: BuildAndBroadcastArgs) => { + try { + const { txToSign } = await adapter.buildCustomTx({ wallet, - to: sellAssetErc20Address, + to, accountNumber, - value: '0', - chainSpecific: { - tokenContractAddress: sellAssetErc20Address, - gasPrice: numberToHex(quote.feeData?.chainSpecific?.gasPriceCryptoBaseUnit || 0), - gasLimit: numberToHex(quote.feeData?.chainSpecific?.estimatedGasCryptoBaseUnit || 0), - }, + value, + data, + ...(await getFeesFromFeeData({ wallet, feeData })), }) - const grantAllowanceTxToSign = { - ...txToSign, - data: approveTx, - } if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ txToSign: grantAllowanceTxToSign, wallet }) + const signedTx = await adapter.signTransaction({ txToSign, wallet }) + const txid = await adapter.broadcastTransaction(signedTx) + return txid + } - const broadcastedTxId = await adapter.broadcastTransaction(signedTx) + if (wallet.supportsBroadcast() && adapter.signAndBroadcastTransaction) { + const txid = await adapter.signAndBroadcastTransaction({ txToSign, wallet }) + return txid + } - return broadcastedTxId - } else if (wallet.supportsBroadcast() && adapter.signAndBroadcastTransaction) { - const broadcastedTxId = await adapter.signAndBroadcastTransaction?.({ - txToSign: grantAllowanceTxToSign, - wallet, - }) + throw new SwapError('[buildAndBroadcast]', { + cause: 'no broadcast support', + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + }) + } catch (e) { + if (e instanceof SwapError) throw e + throw new SwapError('[buildAndBroadcast]', { + cause: e, + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + }) + } +} - return broadcastedTxId - } else { - throw new SwapError('[grantAllowance] - invalid HDWallet config', { - code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, - }) - } +export const grantAllowance = async ({ + feeData, + accountNumber, + approvalAmount, + spender, + to, + wallet, + adapter, + web3, +}: GrantAllowanceArgs): Promise => { + const erc20Contract = new web3.eth.Contract(erc20Abi, to) + const inputData = erc20Contract.methods.approve(spender, approvalAmount).encodeABI() + + try { + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: { ...feeData, estimatedGasCryptoBaseUnit: APPROVAL_GAS_LIMIT }, + to, + value: '0', + wallet, + data: inputData, + }) + return txid } catch (e) { if (e instanceof SwapError) throw e throw new SwapError('[grantAllowance]', { @@ -191,7 +256,7 @@ export const getApproveContractData = ({ spenderAddress, contractAddress, }: GetApproveContractDataArgs): string => { - const contract = new web3.eth.Contract(erc20AbiImported, contractAddress) + const contract = new web3.eth.Contract(erc20Abi, contractAddress) return contract.methods.approve(spenderAddress, MAX_ALLOWANCE).encodeABI() } diff --git a/src/lib/swapper/swappers/utils/test-data/setupDeps.ts b/src/lib/swapper/swappers/utils/test-data/setupDeps.ts index a3bba3e6a34..2c4ea4ddbbf 100644 --- a/src/lib/swapper/swappers/utils/test-data/setupDeps.ts +++ b/src/lib/swapper/swappers/utils/test-data/setupDeps.ts @@ -1,4 +1,5 @@ import { ethAssetId } from '@shapeshiftoss/caip' +import type { evm, EvmChainId, FeeDataEstimate } from '@shapeshiftoss/chain-adapters' import { ethereum } from '@shapeshiftoss/chain-adapters' import * as unchained from '@shapeshiftoss/unchained-client' import Web3 from 'web3' @@ -7,6 +8,48 @@ import { WETH } from 'lib/swapper/swappers/utils/test-data/assets' jest.mock('@shapeshiftoss/chain-adapters') jest.mock('web3') +export const gasFeeData: evm.GasFeeDataEstimate = { + fast: { + gasPrice: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', + }, + slow: { + gasPrice: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', + }, + average: { + gasPrice: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', + }, +} + +export const feeData: FeeDataEstimate = { + fast: { + txFee: '4080654495000000', + chainSpecific: { + gasLimit: '100000', + ...gasFeeData.fast, + }, + }, + average: { + txFee: '4080654495000000', + chainSpecific: { + gasLimit: '100000', + ...gasFeeData.average, + }, + }, + slow: { + txFee: '4080654495000000', + chainSpecific: { + gasLimit: '100000', + ...gasFeeData.slow, + }, + }, +} + export const setupDeps = () => { const ethChainAdapter = new ethereum.ChainAdapter({ providers: { @@ -21,6 +64,7 @@ export const setupDeps = () => { }) ethChainAdapter.getFeeAssetId = () => ethAssetId + ethChainAdapter.getGasFeeData = () => Promise.resolve(gasFeeData) const ethNodeUrl = 'http://localhost:1000' const web3Provider = new Web3.providers.HttpProvider(ethNodeUrl) diff --git a/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts b/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts index d8637e5efd9..ab349c06f22 100644 --- a/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts +++ b/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts @@ -17,7 +17,11 @@ export const setupQuote = () => { minimumCryptoHuman: '0', maximumCryptoHuman: '999999999999', feeData: { - chainSpecific: {}, + chainSpecific: { + gasPriceCryptoBaseUnit: '5', + maxFeePerGas: '6', + maxPriorityFeePerGas: '1', + }, sellAssetTradeFeeUsd: '0', networkFeeCryptoBaseUnit: '0', buyAssetTradeFeeUsd: '0', diff --git a/src/plugins/walletConnectToDapps/v1/useApprovalHandler.tsx b/src/plugins/walletConnectToDapps/v1/useApprovalHandler.tsx index 71536c6a821..7932f8745b9 100644 --- a/src/plugins/walletConnectToDapps/v1/useApprovalHandler.tsx +++ b/src/plugins/walletConnectToDapps/v1/useApprovalHandler.tsx @@ -111,9 +111,8 @@ export const useApprovalHandler = (wcAccountId: AccountId | undefined) => { to: tx.to, data: tx.data, value: tx.value ?? '0', - gasLimit: - (approveData.gasLimit ? convertNumberToHex(approveData.gasLimit) : tx.gas) ?? - convertNumberToHex(90000), // https://docs.walletconnect.com/1.0/json-rpc-api-methods/ethereum#eth_sendtransaction + // https://docs.walletconnect.com/1.0/json-rpc-api-methods/ethereum#eth_sendtransaction + gasLimit: approveData.gasLimit ?? tx.gas ?? '90000', ...gasData, }) const txToSign = { diff --git a/src/plugins/walletConnectToDapps/v2/utils/EIP155RequestHandlerUtil.ts b/src/plugins/walletConnectToDapps/v2/utils/EIP155RequestHandlerUtil.ts index 7dd52cca827..5d27e311127 100644 --- a/src/plugins/walletConnectToDapps/v2/utils/EIP155RequestHandlerUtil.ts +++ b/src/plugins/walletConnectToDapps/v2/utils/EIP155RequestHandlerUtil.ts @@ -95,10 +95,8 @@ export const approveEIP155Request = async ({ to: sendTransaction.to, data: sendTransaction.data, value: sendTransaction.value ?? '0', - gasLimit: - (customTransactionData.gasLimit - ? convertNumberToHex(customTransactionData.gasLimit) - : sendTransaction.gasLimit) ?? convertNumberToHex(90000), // https://docs.walletconnect.com/2.0/advanced/rpc-reference/ethereum-rpc#eth_sendtransaction + // https://docs.walletconnect.com/2.0/advanced/rpc-reference/ethereum-rpc#eth_sendtransaction + gasLimit: customTransactionData.gasLimit ?? sendTransaction.gasLimit ?? '90000', ...gasData, }) const txToSign = { From 9d797f6ce86f28d926ff47670175d7e0eba71e53 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 25 Apr 2023 22:18:51 +0200 Subject: [PATCH 05/12] feat: switch FOXy from web3 to ethers and JsonRpcBatchProvider (#3508) --- __mocks__/ethers.ts | 1 + package.json | 2 +- packages/asset-service/package.json | 1 - .../generateAssetData/ethereum/yearnVaults.ts | 4 +- packages/caip/src/adapters/yearn/utils.ts | 4 +- packages/investor-foxy/package.json | 5 +- packages/investor-foxy/src/abi/erc20-abi.ts | 4 +- packages/investor-foxy/src/abi/foxy-abi.ts | 4 +- .../investor-foxy/src/abi/foxy-staking-abi.ts | 4 +- .../src/abi/liquidity-reserve-abi.ts | 4 +- .../investor-foxy/src/abi/toke-manager-abi.ts | 4 +- .../investor-foxy/src/abi/toke-pool-abi.ts | 4 +- .../src/abi/toke-reward-hash-abi.ts | 4 +- packages/investor-foxy/src/api/api.ts | 771 +++++++++--------- packages/investor-foxy/src/api/foxy-types.ts | 23 +- .../investor-foxy/src/utils/buildTxToSign.ts | 35 - packages/investor-foxy/src/utils/index.ts | 1 - packages/investor-idle/package.json | 1 - packages/investor-yearn/package.json | 1 - packages/investor-yearn/src/YearnInvestor.ts | 4 +- packages/market-service/package.json | 1 - .../src/market-service-manager.ts | 1 - .../src/evm/bnbsmartchain/parser/bep20.ts | 4 +- .../src/evm/ethereum/parser/uniV2.ts | 5 +- .../src/evm/ethereum/parser/weth.ts | 5 +- .../unchained-client/src/evm/parser/erc20.ts | 4 +- .../unchained-client/src/evm/parser/index.ts | 5 +- .../FoxyManager/Deposit/FoxyDeposit.tsx | 23 +- .../Deposit/components/Approve.tsx | 28 +- .../Deposit/components/Confirm.tsx | 38 +- .../Deposit/components/Deposit.tsx | 42 +- .../Overview/Claim/ClaimConfirm.tsx | 15 +- .../Overview/Claim/ClaimStatus.tsx | 21 +- .../FoxyManager/Withdraw/FoxyWithdraw.tsx | 25 +- .../Withdraw/components/Approve.tsx | 32 +- .../Withdraw/components/Confirm.tsx | 40 +- .../Withdraw/components/Withdraw.tsx | 46 +- .../walletConnectToDapps/hooks/useGetAbi.tsx | 2 +- src/state/apis/foxy/foxyApiSingleton.ts | 4 +- yarn.lock | 13 +- 40 files changed, 631 insertions(+), 604 deletions(-) delete mode 100644 packages/investor-foxy/src/utils/buildTxToSign.ts diff --git a/__mocks__/ethers.ts b/__mocks__/ethers.ts index 7da501f0f23..bdf627aea50 100644 --- a/__mocks__/ethers.ts +++ b/__mocks__/ethers.ts @@ -3,6 +3,7 @@ const ethers = { ...jest.requireActual('ethers').ethers, providers: { JsonRpcProvider: jest.fn(), + JsonRpcBatchProvider: jest.fn(), }, Contract: jest.fn().mockImplementation(address => ({ decimals: () => { diff --git a/package.json b/package.json index d78ff532191..d1a5347fb40 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "embla-carousel-react": "^7.0.5", "envalid": "^7.3.1", "eth-url-parser": "^1.0.4", - "ethers": "^5.5.3", + "ethers": "^5.7.2", "framer-motion": "^6.3.11", "friendly-challenge": "0.9.2", "grapheme-splitter": "^1.0.4", diff --git a/packages/asset-service/package.json b/packages/asset-service/package.json index b93b0b4912c..b742328d827 100644 --- a/packages/asset-service/package.json +++ b/packages/asset-service/package.json @@ -29,7 +29,6 @@ "js-pixel-fonts": "^1.5.0" }, "devDependencies": { - "@ethersproject/providers": "^5.5.3", "@yfi/sdk": "^1.2.0", "colorthief": "^2.3.2" } diff --git a/packages/asset-service/src/generateAssetData/ethereum/yearnVaults.ts b/packages/asset-service/src/generateAssetData/ethereum/yearnVaults.ts index 17603da3c27..fc74776baec 100644 --- a/packages/asset-service/src/generateAssetData/ethereum/yearnVaults.ts +++ b/packages/asset-service/src/generateAssetData/ethereum/yearnVaults.ts @@ -1,7 +1,7 @@ -import { JsonRpcProvider } from '@ethersproject/providers' import { ethChainId as chainId, toAssetId } from '@shapeshiftoss/caip' import type { Token, Vault } from '@yfi/sdk' import { Yearn } from '@yfi/sdk' +import { ethers } from 'ethers' import toLower from 'lodash/toLower' import type { Asset } from '../../service/AssetService' @@ -9,7 +9,7 @@ import { ethereum } from '../baseAssets' import { colorMap } from '../colorMap' const network = 1 // 1 for mainnet -const provider = new JsonRpcProvider(process.env.ETHEREUM_NODE_URL) +const provider = new ethers.providers.JsonRpcBatchProvider(process.env.ETHEREUM_NODE_URL) export const yearnSdk = new Yearn(network, { provider }) const explorerData = { diff --git a/packages/caip/src/adapters/yearn/utils.ts b/packages/caip/src/adapters/yearn/utils.ts index bd2f7a5aff4..9882d69cb09 100644 --- a/packages/caip/src/adapters/yearn/utils.ts +++ b/packages/caip/src/adapters/yearn/utils.ts @@ -1,7 +1,7 @@ /* eslint-disable @shapeshiftoss/logger/no-native-console */ -import { JsonRpcProvider } from '@ethersproject/providers' import type { Token, Vault } from '@yfi/sdk' import { Yearn } from '@yfi/sdk' +import { ethers } from 'ethers' import fs from 'fs' import toLower from 'lodash/toLower' import uniqBy from 'lodash/uniqBy' @@ -11,7 +11,7 @@ import { toChainId } from '../../chainId/chainId' import { CHAIN_NAMESPACE, CHAIN_REFERENCE } from '../../constants' const network = 1 // 1 for mainnet -const provider = new JsonRpcProvider(process.env.REACT_APP_ETHEREUM_NODE_URL) +const provider = new ethers.providers.JsonRpcBatchProvider(process.env.REACT_APP_ETHEREUM_NODE_URL) const yearnSdk = new Yearn(network, { provider }) export const writeFiles = async (data: Record>) => { diff --git a/packages/investor-foxy/package.json b/packages/investor-foxy/package.json index 8b1fe8ec98f..26bf7a9cb21 100644 --- a/packages/investor-foxy/package.json +++ b/packages/investor-foxy/package.json @@ -21,14 +21,11 @@ "cli": "yarn build && yarn node dist/foxycli.js" }, "dependencies": { - "@ethersproject/providers": "^5.5.3", "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/logger": "workspace:^", "@shapeshiftoss/types": "workspace:^", - "readline-sync": "^1.4.10", - "web3-core": "1.7.4", - "web3-utils": "1.7.4" + "readline-sync": "^1.4.10" }, "devDependencies": { "@types/readline-sync": "^1.4.4" diff --git a/packages/investor-foxy/src/abi/erc20-abi.ts b/packages/investor-foxy/src/abi/erc20-abi.ts index bc1723c6421..ae7e19c78ef 100644 --- a/packages/investor-foxy/src/abi/erc20-abi.ts +++ b/packages/investor-foxy/src/abi/erc20-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const erc20Abi: AbiItem[] = [ +export const erc20Abi: ContractInterface = [ { constant: true, inputs: [], diff --git a/packages/investor-foxy/src/abi/foxy-abi.ts b/packages/investor-foxy/src/abi/foxy-abi.ts index cc8e6ccdbcb..99522a88a09 100644 --- a/packages/investor-foxy/src/abi/foxy-abi.ts +++ b/packages/investor-foxy/src/abi/foxy-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const foxyAbi: AbiItem[] = [ +export const foxyAbi: ContractInterface = [ { inputs: [], stateMutability: 'nonpayable', diff --git a/packages/investor-foxy/src/abi/foxy-staking-abi.ts b/packages/investor-foxy/src/abi/foxy-staking-abi.ts index 44d3ee93bed..0d62f0aae71 100644 --- a/packages/investor-foxy/src/abi/foxy-staking-abi.ts +++ b/packages/investor-foxy/src/abi/foxy-staking-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const foxyStakingAbi: AbiItem[] = [ +export const foxyStakingAbi: ContractInterface = [ { inputs: [ { diff --git a/packages/investor-foxy/src/abi/liquidity-reserve-abi.ts b/packages/investor-foxy/src/abi/liquidity-reserve-abi.ts index b2e9615dece..4cea3c40602 100644 --- a/packages/investor-foxy/src/abi/liquidity-reserve-abi.ts +++ b/packages/investor-foxy/src/abi/liquidity-reserve-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const liquidityReserveAbi: AbiItem[] = [ +export const liquidityReserveAbi: ContractInterface = [ { inputs: [ { diff --git a/packages/investor-foxy/src/abi/toke-manager-abi.ts b/packages/investor-foxy/src/abi/toke-manager-abi.ts index c6ee537cef2..a4a5250015e 100644 --- a/packages/investor-foxy/src/abi/toke-manager-abi.ts +++ b/packages/investor-foxy/src/abi/toke-manager-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const tokeManagerAbi: AbiItem[] = [ +export const tokeManagerAbi: ContractInterface = [ { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, { anonymous: false, diff --git a/packages/investor-foxy/src/abi/toke-pool-abi.ts b/packages/investor-foxy/src/abi/toke-pool-abi.ts index c8c9018fa3a..4ef28248b05 100644 --- a/packages/investor-foxy/src/abi/toke-pool-abi.ts +++ b/packages/investor-foxy/src/abi/toke-pool-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const tokePoolAbi: AbiItem[] = [ +export const tokePoolAbi: ContractInterface = [ { anonymous: false, inputs: [ diff --git a/packages/investor-foxy/src/abi/toke-reward-hash-abi.ts b/packages/investor-foxy/src/abi/toke-reward-hash-abi.ts index 758f382dcf1..c2d62f9274c 100644 --- a/packages/investor-foxy/src/abi/toke-reward-hash-abi.ts +++ b/packages/investor-foxy/src/abi/toke-reward-hash-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const tokeRewardHashAbi: AbiItem[] = [ +export const tokeRewardHashAbi: ContractInterface = [ { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, { anonymous: false, diff --git a/packages/investor-foxy/src/api/api.ts b/packages/investor-foxy/src/api/api.ts index d77125ce5cb..d10fefc058e 100644 --- a/packages/investor-foxy/src/api/api.ts +++ b/packages/investor-foxy/src/api/api.ts @@ -1,16 +1,12 @@ -import { JsonRpcProvider } from '@ethersproject/providers' import type { ChainReference } from '@shapeshiftoss/caip' import { CHAIN_NAMESPACE, CHAIN_REFERENCE, toAssetId } from '@shapeshiftoss/caip' -import type { ChainAdapter } from '@shapeshiftoss/chain-adapters' +import type { EvmBaseAdapter, FeeDataEstimate } from '@shapeshiftoss/chain-adapters' import { Logger } from '@shapeshiftoss/logger' import { KnownChainIds, WithdrawType } from '@shapeshiftoss/types' import axios from 'axios' import type { BigNumber } from 'bignumber.js' +import { ethers } from 'ethers' import { toLower } from 'lodash' -import Web3 from 'web3' -import type { HttpProvider, TransactionReceipt } from 'web3-core/types' -import type { Contract } from 'web3-eth-contract' -import { numberToHex } from 'web3-utils' import { erc20Abi } from '../abi/erc20-abi' import { foxyAbi } from '../abi/foxy-abi' @@ -26,7 +22,7 @@ import { tokePoolAddress, tokeRewardHashAddress, } from '../constants' -import { bn, bnOrZero, buildTxToSign } from '../utils' +import { bn, bnOrZero } from '../utils' import type { AllowanceInput, ApproveInput, @@ -34,8 +30,9 @@ import type { CanClaimWithdrawParams, ClaimWithdrawal, ContractAddressInput, - EstimateGasApproveInput, - EstimateGasTxInput, + EstimateApproveFeesInput, + EstimateFeesTxInput, + EstimateWithdrawFeesInput, FoxyAddressesType, FoxyOpportunityInputData, GetTokeRewardAmount, @@ -49,7 +46,6 @@ import type { TxInputWithoutAmount, TxInputWithoutAmountAndWallet, TxReceipt, - WithdrawEstimateGasInput, WithdrawInfo, WithdrawInput, } from './foxy-types' @@ -64,7 +60,7 @@ type EthereumChainReference = | typeof CHAIN_REFERENCE.EthereumRopsten export type ConstructorArgs = { - adapter: ChainAdapter + adapter: EvmBaseAdapter providerUrl: string foxyAddresses: FoxyAddressesType chainReference?: EthereumChainReference @@ -88,13 +84,12 @@ export const transformData = ({ tvl, apy, expired, ...contractData }: FoxyOpport const TOKE_IPFS_URL = 'https://ipfs.tokemaklabs.xyz/ipfs' export class FoxyApi { - public adapter: ChainAdapter - public provider: HttpProvider + public adapter: EvmBaseAdapter + public provider: ethers.providers.JsonRpcBatchProvider private providerUrl: string - public jsonRpcProvider: JsonRpcProvider - public web3: Web3 - private foxyStakingContracts: Contract[] - private liquidityReserveContracts: Contract[] + public jsonRpcProvider: ethers.providers.JsonRpcBatchProvider + private foxyStakingContracts: ethers.Contract[] + private liquidityReserveContracts: ethers.Contract[] private readonly ethereumChainReference: ChainReference private foxyAddresses: FoxyAddressesType @@ -105,14 +100,14 @@ export class FoxyApi { chainReference = CHAIN_REFERENCE.EthereumMainnet, }: ConstructorArgs) { this.adapter = adapter - this.provider = new Web3.providers.HttpProvider(providerUrl) - this.jsonRpcProvider = new JsonRpcProvider(providerUrl) - this.web3 = new Web3(this.provider) + this.provider = new ethers.providers.JsonRpcBatchProvider(providerUrl) + this.jsonRpcProvider = new ethers.providers.JsonRpcBatchProvider(providerUrl) this.foxyStakingContracts = foxyAddresses.map( - addresses => new this.web3.eth.Contract(foxyStakingAbi, addresses.staking), + addresses => new ethers.Contract(addresses.staking, foxyStakingAbi, this.provider), ) this.liquidityReserveContracts = foxyAddresses.map( - addresses => new this.web3.eth.Contract(liquidityReserveAbi, addresses.liquidityReserve), + addresses => + new ethers.Contract(addresses.liquidityReserve, liquidityReserveAbi, this.provider), ) this.ethereumChainReference = chainReference this.providerUrl = providerUrl @@ -124,24 +119,42 @@ export class FoxyApi { * to exponential notation ('1.6e+21') in javascript. * @param amount */ - private normalizeAmount(amount: BigNumber) { - return this.web3.utils.toBN(amount.toFixed()) + private normalizeAmount(amount: BigNumber): ethers.BigNumber { + return ethers.BigNumber.from(amount.toFixed()) } + // TODO(gomes): This is rank and should really belong in web for sanity sake. private async signAndBroadcastTx(input: SignAndBroadcastTx): Promise { const { payload, wallet, dryRun } = input - const txToSign = buildTxToSign(payload) + + const { + chainSpecific: { gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas }, + } = payload.estimatedFees.fast + const shouldUseEIP1559Fees = + (await wallet.ethSupportsEIP1559()) && + maxFeePerGas !== undefined && + maxPriorityFeePerGas !== undefined + + const { txToSign } = await this.adapter.buildCustomTx({ + to: payload.to, + value: payload.value, + gasLimit, + wallet, + data: payload.data, + accountNumber: payload.bip44Params.accountNumber, + ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), + }) if (wallet.supportsOfflineSigning()) { const signedTx = await this.adapter.signTransaction({ txToSign, wallet }) if (dryRun) return signedTx try { if (this.providerUrl.includes('localhost') || this.providerUrl.includes('127.0.0.1')) { - const sendSignedTx = await this.web3.eth.sendSignedTransaction(signedTx) - return sendSignedTx?.blockHash + const sendSignedTx = await this.provider.sendTransaction(signedTx) + return sendSignedTx?.blockHash ?? '' } return this.adapter.broadcastTransaction(signedTx) - } catch (err) { - throw new Error(`Failed to broadcast: ${err}`) + } catch (e) { + throw new Error(`Failed to broadcast: ${e}`) } } else if (wallet.supportsBroadcast() && this.adapter.signAndBroadcastTransaction) { if (dryRun) { @@ -154,71 +167,52 @@ export class FoxyApi { } checksumAddress(address: string): string { - return this.web3.utils.toChecksumAddress(address) + // ethers always returns checksum addresses from getAddress() calls + return ethers.utils.getAddress(address) } private verifyAddresses(addresses: string[]) { - try { - addresses.forEach(address => { - this.checksumAddress(address) - }) - } catch (err) { - throw new Error(`Verify Address: ${err}`) - } + addresses.forEach(address => { + this.checksumAddress(address) + }) } - private getStakingContract(contractAddress: string): Contract { + private getStakingContract(contractAddress: string): ethers.Contract { const stakingContract = this.foxyStakingContracts.find( - item => toLower(item.options.address) === toLower(contractAddress), + item => toLower(item.address) === toLower(contractAddress), ) if (!stakingContract) throw new Error('Not a valid contract address') return stakingContract } - private getLiquidityReserveContract(liquidityReserveAddress: string): Contract { + private getLiquidityReserveContract(liquidityReserveAddress: string): ethers.Contract { const liquidityReserveContract = this.liquidityReserveContracts.find( - item => toLower(item.options.address) === toLower(liquidityReserveAddress), + item => toLower(item.address) === toLower(liquidityReserveAddress), ) if (!liquidityReserveContract) throw new Error('Not a valid reserve contract address') return liquidityReserveContract } - private async getGasPriceAndNonce(userAddress: string) { - let nonce: number - try { - nonce = await this.web3.eth.getTransactionCount(userAddress) - } catch (err) { - throw new Error(`Get nonce Error: ${err}`) - } - let gasPrice: string - try { - gasPrice = await this.web3.eth.getGasPrice() - } catch (err) { - throw new Error(`Get gasPrice Error: ${err}`) - } - return { nonce: String(nonce), gasPrice } - } - async getFoxyOpportunities() { try { const opportunities = await Promise.all( this.foxyAddresses.map(async addresses => { const stakingContract = this.foxyStakingContracts.find( - item => toLower(item.options.address) === toLower(addresses.staking), + item => toLower(item.address) === toLower(addresses.staking), ) try { - const expired = await stakingContract?.methods.pauseStaking().call() + const expired = await stakingContract?.pauseStaking() const tvl = await this.tvl({ tokenContractAddress: addresses.foxy }) const apy = this.apy() return transformData({ ...addresses, expired, tvl, apy }) - } catch (err) { - throw new Error(`Failed to get contract data ${err}`) + } catch (e) { + throw new Error(`Failed to get contract data ${e}`) } }), ) return opportunities - } catch (err) { - throw new Error(`getFoxyOpportunities Error: ${err}`) + } catch (e) { + throw new Error(`getFoxyOpportunities Error: ${e}`) } } @@ -232,26 +226,23 @@ export class FoxyApi { const stakingContract = this.getStakingContract(addresses.staking) try { - const expired = await stakingContract.methods.pauseStaking().call() + const expired = await stakingContract.pauseStaking() const tvl = await this.tvl({ tokenContractAddress: addresses.foxy }) const apy = this.apy() return transformData({ ...addresses, tvl, apy, expired }) - } catch (err) { - throw new Error(`Failed to get contract data ${err}`) + } catch (e) { + throw new Error(`Failed to get contract data ${e}`) } } - async getGasPrice() { - const gasPrice = await this.web3.eth.getGasPrice() - return bnOrZero(gasPrice) - } - - getTxReceipt({ txid }: TxReceipt): Promise { + getTxReceipt({ txid }: TxReceipt): Promise { if (!txid) throw new Error('Must pass txid') - return this.web3.eth.getTransactionReceipt(txid) + return this.provider.getTransactionReceipt(txid) } - async estimateClaimWithdrawGas(input: ClaimWithdrawal): Promise { + async estimateClaimWithdrawFees( + input: ClaimWithdrawal, + ): Promise> { const { claimAddress, userAddress, contractAddress } = input const addressToClaim = claimAddress ?? userAddress this.verifyAddresses([addressToClaim, userAddress, contractAddress]) @@ -259,34 +250,62 @@ export class FoxyApi { const stakingContract = this.getStakingContract(contractAddress) try { - const estimatedGas = await stakingContract.methods.claimWithdraw(addressToClaim).estimateGas({ - from: userAddress, + const data = stakingContract.interface.encodeFunctionData('claimWithdraw', [addressToClaim]) + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, + from: userAddress, + }, }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateSendWithdrawalRequestsGas( + async estimateSendWithdrawalRequestsFees( input: TxInputWithoutAmountAndWallet, - ): Promise { + ): Promise> { const { userAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress]) const stakingContract = this.getStakingContract(contractAddress) try { - const estimatedGas = await stakingContract.methods.sendWithdrawalRequests().estimateGas({ - from: userAddress, + const data = stakingContract.interface.encodeFunctionData('sendWithdrawalRequests', []) + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, + from: userAddress, + }, }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateAddLiquidityGas(input: EstimateGasTxInput): Promise { + async estimateAddLiquidityFees( + input: EstimateFeesTxInput, + ): Promise> { const { amountDesired, userAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress]) if (!amountDesired.gt(0)) throw new Error('Must send valid amount') @@ -294,18 +313,33 @@ export class FoxyApi { const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) try { - const estimatedGas = await liquidityReserveContract.methods - .addLiquidity(this.normalizeAmount(amountDesired)) - .estimateGas({ + const data = liquidityReserveContract.interface.encodeFunctionData('addLiquidity', [ + this.normalizeAmount(amountDesired), + ]) + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateRemoveLiquidityGas(input: EstimateGasTxInput): Promise { + async estimateRemoveLiquidityFees( + input: EstimateFeesTxInput, + ): Promise> { const { amountDesired, userAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress]) if (!amountDesired.gt(0)) throw new Error('Must send valid amount') @@ -313,18 +347,34 @@ export class FoxyApi { const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) try { - const estimatedGas = await liquidityReserveContract.methods - .removeLiquidity(this.normalizeAmount(amountDesired)) - .estimateGas({ + const data = liquidityReserveContract.encodeFunctionData('removeLiquidity', [ + this.normalizeAmount(amountDesired), + ]) + + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateWithdrawGas(input: WithdrawEstimateGasInput): Promise { + async estimateWithdrawFees( + input: EstimateWithdrawFeesInput, + ): Promise> { const { amountDesired, userAddress, contractAddress, type } = input this.verifyAddresses([userAddress, contractAddress]) @@ -334,40 +384,71 @@ export class FoxyApi { if (isDelayed && !amountDesired.gt(0)) throw new Error('Must send valid amount') try { - const estimatedGas = isDelayed - ? await stakingContract.methods - .unstake(this.normalizeAmount(amountDesired), true) - .estimateGas({ - from: userAddress, - }) - : await stakingContract.methods.instantUnstake(true).estimateGas({ - from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + const data = isDelayed + ? stakingContract.interface.encodeFunctionData('unstake(uint256,bool)', [ + this.normalizeAmount(amountDesired), + true, + ]) + : stakingContract.interface.encodeFunctionData('instantUnstake', [true]) + + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, + from: userAddress, + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateApproveGas(input: EstimateGasApproveInput): Promise { + async estimateApproveFees( + input: EstimateApproveFeesInput, + ): Promise> { const { userAddress, tokenContractAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress, tokenContractAddress]) - const depositTokenContract = new this.web3.eth.Contract(erc20Abi, tokenContractAddress) + const depositTokenContract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) try { - const estimatedGas = await depositTokenContract.methods - .approve(contractAddress, MAX_ALLOWANCE) - .estimateGas({ + const data = depositTokenContract.interface.encodeFunctionData('approve', [ + contractAddress, + MAX_ALLOWANCE, + ]) + const feeData = await this.adapter.getFeeData({ + to: tokenContractAddress, + value: '0', + chainSpecific: { + contractData: data, from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateDepositGas(input: EstimateGasTxInput): Promise { + async estimateDepositFees( + input: EstimateFeesTxInput, + ): Promise> { const { amountDesired, userAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress]) if (!amountDesired.gt(0)) throw new Error('Must send valid amount') @@ -375,14 +456,28 @@ export class FoxyApi { const stakingContract = this.getStakingContract(contractAddress) try { - const estimatedGas = await stakingContract.methods - .stake(this.normalizeAmount(amountDesired), userAddress) - .estimateGas({ + const data = stakingContract.interface.encodeFunctionData('stake(uint256)', [ + this.normalizeAmount(amountDesired), + ]) + + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } @@ -399,32 +494,19 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress, tokenContractAddress]) if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateApproveGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } - const depositTokenContract = new this.web3.eth.Contract(erc20Abi, tokenContractAddress) - const data: string = depositTokenContract.methods - .approve( - contractAddress, - amount ? numberToHex(this.normalizeAmount(bnOrZero(amount))) : MAX_ALLOWANCE, - ) - .encodeABI({ - from: userAddress, - }) + const estimatedFees = await this.estimateApproveFees(input) + const depositTokenContract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) + const data: string = depositTokenContract.interface.encodeFunctionData('approve', [ + contractAddress, + amount ? this.normalizeAmount(bnOrZero(amount)) : MAX_ALLOWANCE, + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) const chainReferenceAsNumber = Number(this.ethereumChainReference) - const estimatedGas = estimatedGasBN.toString() const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: tokenContractAddress, value: '0', } @@ -435,18 +517,14 @@ export class FoxyApi { const { userAddress, tokenContractAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress, tokenContractAddress]) - const depositTokenContract: Contract = new this.web3.eth.Contract( - erc20Abi, + const depositTokenContract: ethers.Contract = new ethers.Contract( tokenContractAddress, + erc20Abi, + this.provider, ) - let allowance - try { - allowance = await depositTokenContract.methods.allowance(userAddress, contractAddress).call() - } catch (err) { - throw new Error(`Failed to get allowance ${err}`) - } - return allowance + const allowance = await depositTokenContract.allowance(userAddress, contractAddress) + return allowance.toString() } async deposit(input: TxInput): Promise { @@ -462,33 +540,21 @@ export class FoxyApi { if (!amountDesired.gt(0)) throw new Error('Must send valid amount') if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateDepositGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateDepositFees(input) const stakingContract = this.getStakingContract(contractAddress) - const userChecksum = this.web3.utils.toChecksumAddress(userAddress) - const data: string = await stakingContract.methods - .stake(this.normalizeAmount(amountDesired), userAddress) - .encodeABI({ - value: 0, - from: userChecksum, - }) + const data = stakingContract.interface.encodeFunctionData('stake(uint256,address)', [ + this.normalizeAmount(amountDesired), + userAddress, + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -508,36 +574,26 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress]) if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateWithdrawGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateWithdrawFees(input) const stakingContract = this.getStakingContract(contractAddress) const isDelayed = type === WithdrawType.DELAYED && amountDesired if (isDelayed && !amountDesired.gt(0)) throw new Error('Must send valid amount') - const data: string = isDelayed - ? stakingContract.methods.unstake(this.normalizeAmount(amountDesired), true).encodeABI({ - from: userAddress, - }) - : stakingContract.methods.instantUnstake(true).encodeABI({ - from: userAddress, - }) + const stakingContractCallInput: Parameters< + typeof stakingContract.interface.encodeFunctionData + > = isDelayed + ? ['unstake(uint256,bool)', [this.normalizeAmount(amountDesired), true]] + : ['instantUnstake', ['true']] + const data: string = stakingContract.interface.encodeFunctionData(...stakingContractCallInput) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -546,54 +602,58 @@ export class FoxyApi { async canClaimWithdraw(input: CanClaimWithdrawParams): Promise { const { userAddress, contractAddress } = input - const tokeManagerContract = new this.web3.eth.Contract(tokeManagerAbi, tokeManagerAddress) - const tokePoolContract = new this.web3.eth.Contract(tokePoolAbi, tokePoolAddress) + const tokeManagerContract = new ethers.Contract( + tokeManagerAddress, + tokeManagerAbi, + this.provider, + ) + const tokePoolContract = new ethers.Contract(tokePoolAddress, tokePoolAbi, this.provider) const stakingContract = this.getStakingContract(contractAddress) const coolDownInfo = await (async () => { try { - const coolDown = await stakingContract.methods.coolDownInfo(userAddress).call() + const coolDown = await stakingContract.coolDownInfo(userAddress) return { ...coolDown, endEpoch: coolDown.expiry, } - } catch (err) { - logger.error(err, 'failed to get coolDowninfo') + } catch (e) { + logger.error(e, 'failed to get coolDowninfo') } })() const epoch = await (() => { try { - return stakingContract.methods.epoch().call() - } catch (err) { - logger.error(err, 'failed to get epoch') + return stakingContract.epoch() + } catch (e) { + logger.error(e, 'failed to get epoch') return {} } })() const requestedWithdrawals = await (() => { try { - return tokePoolContract.methods.requestedWithdrawals(stakingContract.options.address).call() - } catch (err) { - logger.error(err, 'failed to get requestedWithdrawals') + return tokePoolContract.requestedWithdrawals(stakingContract.address) + } catch (e) { + logger.error(e, 'failed to get requestedWithdrawals') return {} } })() const currentCycleIndex = await (() => { try { - return tokeManagerContract.methods.getCurrentCycleIndex().call() - } catch (err) { - logger.error(err, 'failed to get currentCycleIndex') + return tokeManagerContract.getCurrentCycleIndex() + } catch (e) { + logger.error(e, 'failed to get currentCycleIndex') return 0 } })() const withdrawalAmount = await (() => { try { - return stakingContract.methods.withdrawalAmount().call() - } catch (err) { - logger.error(err, 'failed to get currentCycleIndex') + return stakingContract.withdrawalAmount() + } catch (e) { + logger.error(e, 'failed to get currentCycleIndex') return 0 } })() @@ -627,32 +687,23 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress, addressToClaim]) if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateClaimWithdrawGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateClaimWithdrawFees(input) const stakingContract = this.getStakingContract(contractAddress) const canClaim = await this.canClaimWithdraw({ userAddress, contractAddress }) if (!canClaim) throw new Error('Not ready to claim') - const data: string = stakingContract.methods.claimWithdraw(addressToClaim).encodeABI({ - from: userAddress, - }) + const data: string = stakingContract.interface.encodeFunctionData('claimWithdraw', [ + addressToClaim, + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -661,22 +712,26 @@ export class FoxyApi { async canSendWithdrawalRequest(input: StakingContract): Promise { const { stakingContract } = input - const tokeManagerContract = new this.web3.eth.Contract(tokeManagerAbi, tokeManagerAddress) + const tokeManagerContract = new ethers.Contract( + tokeManagerAddress, + tokeManagerAbi, + this.provider, + ) const requestWithdrawalAmount = await (() => { try { - return stakingContract.methods.requestWithdrawalAmount().call() - } catch (err) { - logger.error(err, 'failed to get requestWithdrawalAmount') + return stakingContract.requestWithdrawalAmount() + } catch (e) { + logger.error(e, 'failed to get requestWithdrawalAmount') return 0 } })() const timeLeftToRequestWithdrawal = await (() => { try { - return stakingContract.methods.timeLeftToRequestWithdrawal().call() - } catch (err) { - logger.error(err, 'failed to get timeLeftToRequestWithdrawal') + return stakingContract.timeLeftToRequestWithdrawal() + } catch (e) { + logger.error(e, 'failed to get timeLeftToRequestWithdrawal') return 0 } })() @@ -692,35 +747,35 @@ export class FoxyApi { const duration = await (() => { try { - return tokeManagerContract.methods.getCycleDuration().call() - } catch (err) { - logger.error(err, 'failed to get cycleDuration') + return tokeManagerContract.getCycleDuration() + } catch (e) { + logger.error(e, 'failed to get cycleDuration') return 0 } })() const currentCycleIndex = await (() => { try { - return tokeManagerContract.methods.getCurrentCycleIndex().call() - } catch (err) { - logger.error(err, 'failed to get currentCycleIndex') + return tokeManagerContract.getCurrentCycleIndex() + } catch (e) { + logger.error(e, 'failed to get currentCycleIndex') return 0 } })() const currentCycleStart = await (() => { try { - return tokeManagerContract.methods.getCurrentCycle().call() - } catch (err) { - logger.error(err, 'failed to get currentCycle') + return tokeManagerContract.getCurrentCycle() + } catch (e) { + logger.error(e, 'failed to get currentCycle') return 0 } })() const nextCycleStart = bnOrZero(currentCycleStart).plus(duration) - const blockNumber = await this.web3.eth.getBlockNumber() - const timestamp = (await this.web3.eth.getBlock(blockNumber)).timestamp + const blockNumber = await this.provider.getBlockNumber() + const timestamp = (await this.provider.getBlock(blockNumber)).timestamp const isTimeToRequest = bnOrZero(timestamp) .plus(timeLeftToRequestWithdrawal) @@ -736,32 +791,20 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress]) if (!wallet || !contractAddress) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateSendWithdrawalRequestsGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateSendWithdrawalRequestsFees(input) const stakingContract = this.getStakingContract(contractAddress) const canSendRequest = await this.canSendWithdrawalRequest({ stakingContract }) if (!canSendRequest) throw new Error('Not ready to send request') - const data: string = stakingContract.methods.sendWithdrawalRequests().encodeABI({ - from: userAddress, - }) - - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() + const data: string = stakingContract.interface.encodeFunctionData('sendWithdrawalRequests') const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -784,31 +827,19 @@ export class FoxyApi { if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateAddLiquidityGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } - + const estimatedFees = await this.estimateAddLiquidityFees(input) const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) - const data: string = liquidityReserveContract.methods - .addLiquidity(this.normalizeAmount(amountDesired)) - .encodeABI({ - from: userAddress, - }) + const data: string = liquidityReserveContract.interface.encodeFunctionData('addLiquidity', [ + this.normalizeAmount(amountDesired), + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -830,31 +861,20 @@ export class FoxyApi { if (!amountDesired.gt(0)) throw new Error('Must send valid amount') if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateRemoveLiquidityGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateRemoveLiquidityFees(input) const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) - const data: string = liquidityReserveContract.methods - .removeLiquidity(this.normalizeAmount(amountDesired)) - .encodeABI({ - from: userAddress, - }) + const data: string = liquidityReserveContract.interface.encodeFunctionData('removeLiquidity', [ + this.normalizeAmount(amountDesired), + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -870,25 +890,25 @@ export class FoxyApi { let coolDownInfo try { - const coolDown = await stakingContract.methods.coolDownInfo(userAddress).call() + const coolDown = await stakingContract.coolDownInfo(userAddress) coolDownInfo = { ...coolDown, endEpoch: coolDown.expiry, } - } catch (err) { - throw new Error(`Failed to get coolDowninfo: ${err}`) + } catch (e) { + throw new Error(`Failed to get coolDowninfo: ${e}`) } let epoch try { - epoch = await stakingContract.methods.epoch().call() - } catch (err) { - throw new Error(`Failed to get epoch: ${err}`) + epoch = await stakingContract.epoch() + } catch (e) { + throw new Error(`Failed to get epoch: ${e}`) } let currentBlock try { - currentBlock = await this.web3.eth.getBlockNumber() - } catch (err) { - throw new Error(`Failed to get block number: ${err}`) + currentBlock = await this.provider.getBlockNumber() + } catch (e) { + throw new Error(`Failed to get block number: ${e}`) } const epochsLeft = bnOrZero(coolDownInfo.endEpoch).minus(epoch.number) // epochs left until can claim const blocksLeftInCurrentEpoch = @@ -908,12 +928,12 @@ export class FoxyApi { const { tokenContractAddress, userAddress } = input this.verifyAddresses([userAddress, tokenContractAddress]) - const contract = new this.web3.eth.Contract(erc20Abi, tokenContractAddress) + const contract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) try { - const balance = await contract.methods.balanceOf(userAddress).call() + const balance = await contract.balanceOf(userAddress) return bnOrZero(balance) - } catch (err) { - throw new Error(`Failed to get balance: ${err}`) + } catch (e) { + throw new Error(`Failed to get balance: ${e}`) } } @@ -924,28 +944,28 @@ export class FoxyApi { let liquidityReserveAddress try { - liquidityReserveAddress = await stakingContract.methods.LIQUIDITY_RESERVE().call() - } catch (err) { - throw new Error(`Failed to get liquidityReserve address ${err}`) + liquidityReserveAddress = await stakingContract.LIQUIDITY_RESERVE() + } catch (e) { + throw new Error(`Failed to get liquidityReserve address ${e}`) } const liquidityReserveContract = this.getLiquidityReserveContract(liquidityReserveAddress) try { - const feeInBasisPoints = await liquidityReserveContract.methods.fee().call() + const feeInBasisPoints = await liquidityReserveContract.fee() return bnOrZero(feeInBasisPoints).div(10000) // convert from basis points to decimal percentage - } catch (err) { - throw new Error(`Failed to get instantUnstake fee ${err}`) + } catch (e) { + throw new Error(`Failed to get instantUnstake fee ${e}`) } } async totalSupply({ tokenContractAddress }: TokenAddressInput): Promise { this.verifyAddresses([tokenContractAddress]) - const contract = new this.web3.eth.Contract(erc20Abi, tokenContractAddress) + const contract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) try { - const totalSupply = await contract.methods.totalSupply().call() + const totalSupply = await contract.totalSupply() return bnOrZero(totalSupply) - } catch (err) { - throw new Error(`Failed to get totalSupply: ${err}`) + } catch (e) { + throw new Error(`Failed to get totalSupply: ${e}`) } } @@ -961,13 +981,13 @@ export class FoxyApi { async tvl(input: TokenAddressInput): Promise { const { tokenContractAddress } = input this.verifyAddresses([tokenContractAddress]) - const contract = new this.web3.eth.Contract(foxyAbi, tokenContractAddress) + const contract = new ethers.Contract(tokenContractAddress, foxyAbi, this.provider) try { - const balance = await contract.methods.circulatingSupply().call() + const balance = await contract.circulatingSupply() return bnOrZero(balance) - } catch (err) { - throw new Error(`Failed to get tvl: ${err}`) + } catch (e) { + throw new Error(`Failed to get tvl: ${e}`) } } @@ -976,39 +996,49 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress]) const stakingContract = this.getStakingContract(contractAddress) - let coolDownInfo + let coolDownInfo: [amount: string, gons: string, expiry: string] try { - coolDownInfo = await stakingContract.methods.coolDownInfo(userAddress).call() - } catch (err) { - throw new Error(`Failed to get coolDowninfo: ${err}`) + coolDownInfo = (await stakingContract.coolDownInfo(userAddress)).map( + (info: ethers.BigNumber) => info.toString(), + ) + } catch (e) { + throw new Error(`Failed to get coolDowninfo: ${e}`) } let releaseTime try { releaseTime = await this.getTimeUntilClaimable(input) - } catch (err) { - throw new Error(`Failed to getTimeUntilClaimable: ${err}`) + } catch (e) { + throw new Error(`Failed to getTimeUntilClaimable: ${e}`) } + + const [amount, gons, expiry] = coolDownInfo return { - ...coolDownInfo, + amount, + gons, + expiry, releaseTime, } } async getClaimFromTokemakArgs(input: ContractAddressInput): Promise { const { contractAddress } = input - const rewardHashContract = new this.web3.eth.Contract(tokeRewardHashAbi, tokeRewardHashAddress) + const rewardHashContract = new ethers.Contract( + tokeRewardHashAddress, + tokeRewardHashAbi, + this.provider, + ) const latestCycleIndex = await (() => { try { - return rewardHashContract.methods.latestCycleIndex().call() - } catch (err) { - throw new Error(`Failed to get latestCycleIndex, ${err}`) + return rewardHashContract.latestCycleIndex() + } catch (e) { + throw new Error(`Failed to get latestCycleIndex, ${e}`) } })() const cycleHashes = await (() => { try { - return rewardHashContract.methods.cycleHashes(latestCycleIndex).call() - } catch (err) { - throw new Error(`Failed to get latestCycleIndex, ${err}`) + return rewardHashContract.cycleHashes(latestCycleIndex) + } catch (e) { + throw new Error(`Failed to get latestCycleIndex, ${e}`) } })() @@ -1030,8 +1060,8 @@ export class FoxyApi { s, recipient: payload, } - } catch (err) { - throw new Error(`Failed to get information from Tokemak ipfs ${err}`) + } catch (e) { + throw new Error(`Failed to get information from Tokemak ipfs ${e}`) } } @@ -1039,20 +1069,19 @@ export class FoxyApi { const { tokenContractAddress, userAddress } = input this.verifyAddresses([tokenContractAddress]) - const foxyContract = new this.web3.eth.Contract(foxyAbi, tokenContractAddress) + const foxyContract = new ethers.Contract(tokenContractAddress, foxyAbi, this.provider) const fromBlock = 14381454 // genesis rebase const rebaseEvents = await (async () => { try { - const events = ( - await foxyContract.getPastEvents('LogRebase', { - fromBlock, - toBlock: 'latest', - }) - ).filter(rebase => rebase.returnValues.rebase !== '0') - return events - } catch (err) { - logger.error(err, 'failed to get rebase events') + const filter = foxyContract.filters.LogRebase() + const events = await foxyContract.queryFilter(filter, fromBlock, 'latest') + const filteredEvents = events.filter( + rebase => rebase.args?.rebase && !rebase.args.rebase.isZero(), + ) + return filteredEvents + } catch (e) { + logger.error(e, 'failed to get rebase events') return undefined } })() @@ -1061,22 +1090,17 @@ export class FoxyApi { const transferEvents = await (async () => { try { - const events = await foxyContract.getPastEvents('Transfer', { - fromBlock, - toBlock: 'latest', - }) + const filter = foxyContract.filters.Transfer() + const events = await foxyContract.queryFilter(filter, fromBlock, 'latest') return events - } catch (err) { - logger.error(err, 'failed to get transfer events') + } catch (e) { + logger.error(e, 'failed to get transfer events') return undefined } })() const events: RebaseEvent[] = rebaseEvents.map(rebaseEvent => { - const { - blockNumber, - returnValues: { epoch }, - } = rebaseEvent + const { blockNumber, args: { epoch } = { epoch: '' } } = rebaseEvent return { blockNumber, epoch, @@ -1097,23 +1121,20 @@ export class FoxyApi { // check transfer events to see if a user triggered a rebase through unstake or stake const unstakedTransferInfo = transferEvents?.filter( e => - e.blockNumber === event.blockNumber && - e.returnValues.from.toLowerCase() === userAddress, + e.blockNumber === event.blockNumber && e.args?.from.toLowerCase() === userAddress, ) - const unstakedTransferAmount = unstakedTransferInfo?.[0]?.returnValues?.value ?? 0 + const unstakedTransferAmount = unstakedTransferInfo?.[0]?.args?.value ?? 0 const stakedTransferInfo = transferEvents?.filter( - e => - e.blockNumber === event.blockNumber && - e.returnValues.to.toLowerCase() === userAddress, + e => e.blockNumber === event.blockNumber && e.args?.to.toLowerCase() === userAddress, ) - const stakedTransferAmount = stakedTransferInfo?.[0]?.returnValues?.value ?? 0 + const stakedTransferAmount = stakedTransferInfo?.[0]?.args?.value ?? 0 - const postRebaseBalanceResult = await foxyContract.methods - .balanceOf(userAddress) - .call(null, event.blockNumber) - const unadjustedPreRebaseBalance = await foxyContract.methods - .balanceOf(userAddress) - .call(null, event.blockNumber - 1) + const postRebaseBalanceResult = await foxyContract.balanceOf(userAddress, { + blockTag: event.blockNumber, + }) + const unadjustedPreRebaseBalance = await foxyContract.balanceOf(userAddress, { + blockTag: event.blockNumber - 1, + }) // unstake events can trigger rebases, if they do, adjust the amount to not include that unstake's transfer amount const preRebaseBalanceResult = bnOrZero(unadjustedPreRebaseBalance) @@ -1125,8 +1146,8 @@ export class FoxyApi { preRebaseBalance: preRebaseBalanceResult, postRebaseBalance: postRebaseBalanceResult, } - } catch (err) { - logger.error(err, 'failed to get balance of address') + } catch (e) { + logger.error(e, 'failed to get balance of address') return { preRebaseBalance: bn(0).toString(), postRebaseBalance: bn(0).toString(), @@ -1136,10 +1157,10 @@ export class FoxyApi { const blockTime = await (async () => { try { - const block = await this.web3.eth.getBlock(event.blockNumber) + const block = await this.provider.getBlock(event.blockNumber) return bnOrZero(block.timestamp).toNumber() - } catch (err) { - logger.error(err, 'failed to get timestamp of block') + } catch (e) { + logger.error(e, 'failed to get timestamp of block') return 0 } })() diff --git a/packages/investor-foxy/src/api/foxy-types.ts b/packages/investor-foxy/src/api/foxy-types.ts index ef81dde1806..ce166d5ef06 100644 --- a/packages/investor-foxy/src/api/foxy-types.ts +++ b/packages/investor-foxy/src/api/foxy-types.ts @@ -1,8 +1,9 @@ import type { AssetId } from '@shapeshiftoss/caip' -import type { HDWallet } from '@shapeshiftoss/hdwallet-core' -import type { BIP44Params, WithdrawType } from '@shapeshiftoss/types' +import type { FeeDataEstimate } from '@shapeshiftoss/chain-adapters' +import type { ETHWallet } from '@shapeshiftoss/hdwallet-core' +import type { BIP44Params, KnownChainIds, WithdrawType } from '@shapeshiftoss/types' import type { BigNumber } from 'bignumber.js' -import type { Contract } from 'web3-eth-contract' +import type { Contract } from 'ethers' export type FoxyAddressesType = { staking: string @@ -27,10 +28,10 @@ export type ApproveInput = { tokenContractAddress: string contractAddress: string userAddress: string - wallet: HDWallet + wallet: ETHWallet } -export type EstimateGasApproveInput = Pick< +export type EstimateApproveFeesInput = Pick< ApproveInput, 'userAddress' | 'tokenContractAddress' | 'contractAddress' > @@ -41,7 +42,7 @@ export type TxInput = { tokenContractAddress?: string userAddress: string contractAddress: string - wallet: HDWallet + wallet: ETHWallet amountDesired: BigNumber } @@ -57,7 +58,7 @@ export type WithdrawInput = Omit & { amountDesired?: BigNumber } -export type WithdrawEstimateGasInput = Omit +export type EstimateWithdrawFeesInput = Omit export type FoxyOpportunityInputData = { tvl: BigNumber @@ -69,7 +70,7 @@ export type FoxyOpportunityInputData = { liquidityReserve: string } -export type EstimateGasTxInput = Pick< +export type EstimateFeesTxInput = Pick< TxInput, 'tokenContractAddress' | 'contractAddress' | 'userAddress' | 'amountDesired' > @@ -102,16 +103,14 @@ export type SignAndBroadcastPayload = { bip44Params: BIP44Params chainId: number data: string - estimatedGas: string - gasPrice: string - nonce: string + estimatedFees: FeeDataEstimate to: string value: string } export type SignAndBroadcastTx = { payload: SignAndBroadcastPayload - wallet: HDWallet + wallet: ETHWallet dryRun: boolean } diff --git a/packages/investor-foxy/src/utils/buildTxToSign.ts b/packages/investor-foxy/src/utils/buildTxToSign.ts deleted file mode 100644 index 37c3843b67f..00000000000 --- a/packages/investor-foxy/src/utils/buildTxToSign.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { toAddressNList } from '@shapeshiftoss/chain-adapters' -import type { ETHSignTx } from '@shapeshiftoss/hdwallet-core' -import type { BIP44Params } from '@shapeshiftoss/types' -import { numberToHex } from 'web3-utils' - -type BuildTxToSignInput = { - bip44Params: BIP44Params - chainId: number - data: string - estimatedGas: string - gasPrice: string - nonce: string - value: string - to: string -} - -export const buildTxToSign = ({ - bip44Params, - chainId = 1, - data, - estimatedGas, - gasPrice, - nonce, - to, - value, -}: BuildTxToSignInput): ETHSignTx => ({ - addressNList: toAddressNList(bip44Params), - value: numberToHex(value), - to, - chainId, // TODO: implement for multiple chains - data, - nonce: numberToHex(nonce), - gasPrice: numberToHex(gasPrice), - gasLimit: numberToHex(estimatedGas), -}) diff --git a/packages/investor-foxy/src/utils/index.ts b/packages/investor-foxy/src/utils/index.ts index dfd9ab73da5..fcd8f6c533e 100644 --- a/packages/investor-foxy/src/utils/index.ts +++ b/packages/investor-foxy/src/utils/index.ts @@ -1,2 +1 @@ export * from './bignumber' -export * from './buildTxToSign' diff --git a/packages/investor-idle/package.json b/packages/investor-idle/package.json index 6c498e03ec4..62d528edaa0 100644 --- a/packages/investor-idle/package.json +++ b/packages/investor-idle/package.json @@ -21,7 +21,6 @@ "cli": "yarn build && yarn node dist/idlecli.js" }, "dependencies": { - "@ethersproject/providers": "^5.5.3", "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/investor": "workspace:^", diff --git a/packages/investor-yearn/package.json b/packages/investor-yearn/package.json index 3ff0b9af293..94156bb281e 100644 --- a/packages/investor-yearn/package.json +++ b/packages/investor-yearn/package.json @@ -21,7 +21,6 @@ "cli": "yarn build && yarn node dist/yearncli.js" }, "dependencies": { - "@ethersproject/providers": "^5.5.3", "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/investor": "workspace:^", diff --git a/packages/investor-yearn/src/YearnInvestor.ts b/packages/investor-yearn/src/YearnInvestor.ts index f31329da6ab..f0dc75c1497 100644 --- a/packages/investor-yearn/src/YearnInvestor.ts +++ b/packages/investor-yearn/src/YearnInvestor.ts @@ -1,8 +1,8 @@ -import { JsonRpcProvider } from '@ethersproject/providers' import type { ChainAdapter } from '@shapeshiftoss/chain-adapters' import type { Investor } from '@shapeshiftoss/investor' import type { KnownChainIds } from '@shapeshiftoss/types' import { type ChainId, type VaultMetadata, Yearn } from '@yfi/sdk' +import { ethers } from 'ethers' import { find } from 'lodash' import filter from 'lodash/filter' import Web3 from 'web3' @@ -31,7 +31,7 @@ export class YearnInvestor implements Investor implements SubParser { - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider readonly chainId: ChainId readonly abiInterface = new ethers.utils.Interface(bep20) diff --git a/packages/unchained-client/src/evm/ethereum/parser/uniV2.ts b/packages/unchained-client/src/evm/ethereum/parser/uniV2.ts index 4006b88c755..02929d69afb 100644 --- a/packages/unchained-client/src/evm/ethereum/parser/uniV2.ts +++ b/packages/unchained-client/src/evm/ethereum/parser/uniV2.ts @@ -23,12 +23,11 @@ export interface TxMetadata extends BaseTxMetadata { export interface ParserArgs { chainId: ChainId - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider } export class Parser implements SubParser { - provider: ethers.providers.JsonRpcProvider - + provider: ethers.providers.JsonRpcBatchProvider readonly chainId: ChainId readonly wethContract: string readonly abiInterface = new ethers.utils.Interface(UNIV2_ABI) diff --git a/packages/unchained-client/src/evm/ethereum/parser/weth.ts b/packages/unchained-client/src/evm/ethereum/parser/weth.ts index 1310dd86e12..704cc270d00 100644 --- a/packages/unchained-client/src/evm/ethereum/parser/weth.ts +++ b/packages/unchained-client/src/evm/ethereum/parser/weth.ts @@ -16,12 +16,11 @@ export interface TxMetadata extends BaseTxMetadata { export interface ParserArgs { chainId: ChainId - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider } export class Parser implements SubParser { - provider: ethers.providers.JsonRpcProvider - + provider: ethers.providers.JsonRpcBatchProvider readonly chainId: ChainId readonly wethContract: string readonly abiInterface = new ethers.utils.Interface(WETH_ABI) diff --git a/packages/unchained-client/src/evm/parser/erc20.ts b/packages/unchained-client/src/evm/parser/erc20.ts index 7f799af86a7..51a17cfa4b6 100644 --- a/packages/unchained-client/src/evm/parser/erc20.ts +++ b/packages/unchained-client/src/evm/parser/erc20.ts @@ -16,11 +16,11 @@ export interface TxMetadata extends BaseTxMetadata { interface ParserArgs { chainId: ChainId - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider } export class Parser implements SubParser { - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider readonly chainId: ChainId readonly abiInterface = new ethers.utils.Interface(ERC20_ABI) diff --git a/packages/unchained-client/src/evm/parser/index.ts b/packages/unchained-client/src/evm/parser/index.ts index 9c1c0d1c269..982c8f1e801 100644 --- a/packages/unchained-client/src/evm/parser/index.ts +++ b/packages/unchained-client/src/evm/parser/index.ts @@ -21,14 +21,13 @@ export class BaseTransactionParser { chainId: ChainId assetId: AssetId - protected readonly provider: ethers.providers.JsonRpcProvider - + protected readonly provider: ethers.providers.JsonRpcBatchProvider private parsers: SubParser[] = [] constructor(args: TransactionParserArgs) { this.chainId = args.chainId this.assetId = args.assetId - this.provider = new ethers.providers.JsonRpcProvider(args.rpcUrl) + this.provider = new ethers.providers.JsonRpcBatchProvider(args.rpcUrl) } /** diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/FoxyDeposit.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/FoxyDeposit.tsx index 25414b8f7f1..ba4c110db25 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/FoxyDeposit.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/FoxyDeposit.tsx @@ -2,6 +2,7 @@ import { Center } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { toAssetId } from '@shapeshiftoss/caip' import { KnownChainIds } from '@shapeshiftoss/types' +import { ethers } from 'ethers' import { DefiModalContent } from 'features/defi/components/DefiModal/DefiModalContent' import { DefiModalHeader } from 'features/defi/components/DefiModal/DefiModalHeader' import type { @@ -53,9 +54,14 @@ export const FoxyDeposit: React.FC<{ const translate = useTranslate() const [state, dispatch] = useReducer(reducer, initialState) const { query, history, location } = useBrowserRouter() - const { chainId, contractAddress, assetReference, assetNamespace } = query + const { + chainId, + contractAddress: foxyContractAddress, + assetReference: foxyStakingContractAddress, + assetNamespace, + } = query // ContractAssetId - const assetId = toAssetId({ chainId, assetNamespace, assetReference }) + const assetId = toAssetId({ chainId, assetNamespace, assetReference: foxyStakingContractAddress }) const opportunityMetadataFilter = useMemo(() => ({ stakingId: assetId as StakingId }), [assetId]) const opportunityMetadata = useAppSelector(state => @@ -83,7 +89,7 @@ export const FoxyDeposit: React.FC<{ if ( !( walletState.wallet && - contractAddress && + foxyStakingContractAddress && !isFoxyAprLoading && chainAdapter && foxyApi && @@ -91,7 +97,9 @@ export const FoxyDeposit: React.FC<{ ) ) return - const foxyOpportunity = await foxyApi.getFoxyOpportunityByStakingAddress(contractAddress) + const foxyOpportunity = await foxyApi.getFoxyOpportunityByStakingAddress( + ethers.utils.getAddress(foxyStakingContractAddress), + ) dispatch({ type: FoxyDepositActionType.SET_OPPORTUNITY, payload: { ...foxyOpportunity, apy: foxyAprData?.foxyApr ?? '' }, @@ -105,10 +113,11 @@ export const FoxyDeposit: React.FC<{ foxyApi, bip44Params, chainAdapterManager, - contractAddress, + foxyContractAddress, walletState.wallet, foxyAprData?.foxyApr, isFoxyAprLoading, + foxyStakingContractAddress, ]) const handleBack = () => { @@ -136,7 +145,7 @@ export const FoxyDeposit: React.FC<{ label: translate('defi.steps.approve.title'), component: ownProps => , props: { - contractAddress, + contractAddress: foxyContractAddress, }, }, [DefiStep.Confirm]: { @@ -148,7 +157,7 @@ export const FoxyDeposit: React.FC<{ component: Status, }, } - }, [accountId, handleAccountIdChange, contractAddress, translate, stakingAsset.symbol]) + }, [accountId, handleAccountIdChange, foxyContractAddress, translate, stakingAsset.symbol]) if (loading || !stakingAsset || !marketData) { return ( diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Approve.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Approve.tsx index a65168ab6fc..c30474b284d 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Approve.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Approve.tsx @@ -1,6 +1,7 @@ import { useToast } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId } from '@shapeshiftoss/caip' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { Approve as ReusableApprove } from 'features/defi/components/Approve/Approve' import { ApprovePreFooter } from 'features/defi/components/Approve/ApprovePreFooter' import type { DepositValues } from 'features/defi/components/Deposit/Deposit' @@ -68,17 +69,18 @@ export const Approve: React.FC = ({ accountId, onNext }) => { async (deposit: DepositValues) => { if (!accountAddress || !assetReference || !foxyApi) return try { - const [gasLimit, gasPrice] = await Promise.all([ - foxyApi.estimateDepositGas({ - tokenContractAddress: assetReference, - contractAddress, - amountDesired: bnOrZero(deposit.cryptoAmount) - .times(bn(10).pow(asset.precision)) - .decimalPlaces(0), - userAddress: accountAddress, - }), - foxyApi.getGasPrice(), - ]) + const feeDataEstimate = await foxyApi.estimateDepositFees({ + tokenContractAddress: assetReference, + contractAddress, + amountDesired: bnOrZero(deposit.cryptoAmount) + .times(bn(10).pow(asset.precision)) + .decimalPlaces(0), + userAddress: accountAddress, + }) + + const { + chainSpecific: { gasPrice, gasLimit }, + } = feeDataEstimate.fast return bnOrZero(gasPrice).times(gasLimit).toFixed(0) } catch (error) { moduleLogger.error( @@ -111,6 +113,10 @@ export const Approve: React.FC = ({ accountId, onNext }) => { return try { dispatch({ type: FoxyDepositActionType.SET_LOADING, payload: true }) + + if (!supportsETH(walletState.wallet)) + throw new Error(`handleApprove: wallet does not support ethereum`) + await foxyApi.approve({ tokenContractAddress: assetReference, contractAddress, diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Confirm.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Confirm.tsx index ccea6722c07..d9b89ad4e2c 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Confirm.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Confirm.tsx @@ -1,6 +1,8 @@ import { Alert, AlertIcon, Box, Stack, useToast } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId } from '@shapeshiftoss/caip' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' +import type { ethers } from 'ethers' import { Confirm as ReusableConfirm } from 'features/defi/components/Confirm/Confirm' import { Summary } from 'features/defi/components/Summary' import { DefiStep } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' @@ -8,7 +10,6 @@ import { useFoxyQuery } from 'features/defi/providers/foxy/components/FoxyManage import isNil from 'lodash/isNil' import { useCallback, useContext, useMemo } from 'react' import { useTranslate } from 'react-polyglot' -import type { TransactionReceipt } from 'web3-core/types' import { Amount } from 'components/Amount/Amount' import { AssetIcon } from 'components/AssetIcon' import type { StepComponentProps } from 'components/DeFi/components/Steps' @@ -81,33 +82,36 @@ export const Confirm: React.FC = ({ onNext, accountId }) => { return try { dispatch({ type: FoxyDepositActionType.SET_LOADING, payload: true }) - const [txid, gasPrice] = await Promise.all([ - foxyApi.deposit({ - amountDesired: bnOrZero(state?.deposit.cryptoAmount) - .times(bn(10).pow(asset.precision)) - .decimalPlaces(0), - tokenContractAddress: assetReference, - userAddress: accountAddress, - contractAddress, - wallet: walletState.wallet, - bip44Params, - }), - foxyApi.getGasPrice(), - ]) + + if (!supportsETH(walletState.wallet)) + throw new Error(`handleDeposit: wallet does not support ethereum`) + + const txid = await foxyApi.deposit({ + amountDesired: bnOrZero(state?.deposit.cryptoAmount) + .times(bn(10).pow(asset.precision)) + .decimalPlaces(0), + tokenContractAddress: assetReference, + userAddress: accountAddress, + contractAddress, + wallet: walletState.wallet, + bip44Params, + }) dispatch({ type: FoxyDepositActionType.SET_TXID, payload: txid }) onNext(DefiStep.Status) const transactionReceipt = await poll({ fn: () => foxyApi.getTxReceipt({ txid }), - validate: (result: TransactionReceipt) => !isNil(result), + validate: (result: ethers.providers.TransactionReceipt) => !isNil(result), interval: 15000, maxAttempts: 30, }) dispatch({ type: FoxyDepositActionType.SET_DEPOSIT, payload: { - txStatus: transactionReceipt.status === true ? 'success' : 'failed', - usedGasFeeCryptoBaseUnit: bnOrZero(gasPrice).times(transactionReceipt.gasUsed).toFixed(0), + txStatus: transactionReceipt.status ? 'success' : 'failed', + usedGasFeeCryptoBaseUnit: transactionReceipt.effectiveGasPrice + .mul(transactionReceipt.gasUsed) + .toString(), }, }) } catch (error) { diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Deposit.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Deposit.tsx index 2ba9a77c8c8..03082cb70aa 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Deposit.tsx @@ -69,14 +69,16 @@ export const Deposit: React.FC = ({ const getApproveGasEstimate = async () => { if (!accountAddress || !assetReference || !foxyApi) return try { - const [gasLimit, gasPrice] = await Promise.all([ - foxyApi.estimateApproveGas({ - tokenContractAddress: assetReference, - contractAddress, - userAddress: accountAddress, - }), - foxyApi.getGasPrice(), - ]) + const feeDataEstimate = await foxyApi.estimateApproveFees({ + tokenContractAddress: assetReference, + contractAddress, + userAddress: accountAddress, + }) + + const { + chainSpecific: { gasPrice, gasLimit }, + } = feeDataEstimate.fast + return bnOrZero(gasPrice).times(gasLimit).toFixed(0) } catch (error) { moduleLogger.error( @@ -95,17 +97,19 @@ export const Deposit: React.FC = ({ const getDepositGasEstimateCryptoBaseUnit = async (deposit: DepositValues) => { if (!accountAddress || !assetReference || !foxyApi) return try { - const [gasLimit, gasPrice] = await Promise.all([ - foxyApi.estimateDepositGas({ - tokenContractAddress: assetReference, - contractAddress, - amountDesired: bnOrZero(deposit.cryptoAmount) - .times(`1e+${asset.precision}`) - .decimalPlaces(0), - userAddress: accountAddress, - }), - foxyApi.getGasPrice(), - ]) + const feeDataEstimate = await foxyApi.estimateDepositFees({ + tokenContractAddress: assetReference, + contractAddress, + amountDesired: bnOrZero(deposit.cryptoAmount) + .times(`1e+${asset.precision}`) + .decimalPlaces(0), + userAddress: accountAddress, + }) + + const { + chainSpecific: { gasPrice, gasLimit }, + } = feeDataEstimate.fast + return bnOrZero(gasPrice).times(gasLimit).toFixed(0) } catch (error) { moduleLogger.error( diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx index 8ed9f9f8a69..f2f720bd775 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx @@ -10,6 +10,7 @@ import { } from '@chakra-ui/react' import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' import { ASSET_REFERENCE, toAssetId } from '@shapeshiftoss/caip' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { KnownChainIds } from '@shapeshiftoss/types' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' @@ -93,6 +94,8 @@ export const ClaimConfirm = ({ if (!(walletState.wallet && contractAddress && userAddress && foxyApi && bip44Params)) return setLoading(true) try { + if (!supportsETH(walletState.wallet)) + throw new Error(`handleConfirm: wallet does not support ethereum`) const txid = await foxyApi.claimWithdraw({ claimAddress: userAddress, userAddress, @@ -140,24 +143,30 @@ export const ClaimConfirm = ({ try { const chainAdapter = await chainAdapterManager.get(KnownChainIds.EthereumMainnet) if (!(walletState.wallet && contractAddress && foxyApi && chainAdapter)) return + if (!supportsETH(walletState.wallet)) + throw new Error(`ClaimConfirm::useEffect: wallet does not support ethereum`) + const { accountNumber } = bip44Params const userAddress = await chainAdapter.getAddress({ wallet: walletState.wallet, accountNumber, }) setUserAddress(userAddress) - const [gasLimit, gasPrice, canClaimWithdraw] = await Promise.all([ - foxyApi.estimateClaimWithdrawGas({ + const [feeDataEstimate, canClaimWithdraw] = await Promise.all([ + foxyApi.estimateClaimWithdrawFees({ claimAddress: userAddress, userAddress, contractAddress, wallet: walletState.wallet, bip44Params, }), - foxyApi.getGasPrice(), foxyApi.canClaimWithdraw({ contractAddress, userAddress }), ]) + const { + chainSpecific: { gasPrice, gasLimit }, + } = feeDataEstimate.fast + setCanClaim(canClaimWithdraw) const gasEstimate = bnOrZero(gasPrice).times(gasLimit).toFixed(0) setEstimatedGas(gasEstimate) diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimStatus.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimStatus.tsx index 8865be1aa0b..9f6c3775d1b 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimStatus.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimStatus.tsx @@ -1,13 +1,13 @@ import { Box, Button, Center, Link, ModalBody, ModalFooter, Stack } from '@chakra-ui/react' import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' import { ASSET_REFERENCE, toAssetId } from '@shapeshiftoss/caip' +import type { ethers } from 'ethers' import { DefiProvider, DefiType } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' import isNil from 'lodash/isNil' import { useCallback, useEffect, useState } from 'react' import { FaCheck, FaTimes } from 'react-icons/fa' import { useTranslate } from 'react-polyglot' import { useLocation } from 'react-router' -import type { TransactionReceipt } from 'web3-core/types' import { Amount } from 'components/Amount/Amount' import { AssetIcon } from 'components/AssetIcon' import { CircularProgress } from 'components/CircularProgress/CircularProgress' @@ -44,7 +44,7 @@ enum TxStatus { type ClaimState = { txStatus: TxStatus - usedGasFee?: string + usedGasFeeCryptoBaseUnit?: string } const StatusInfo = { @@ -121,11 +121,10 @@ export const ClaimStatus: React.FC = ({ accountId }) => { try { const transactionReceipt = await poll({ fn: () => foxyApi.getTxReceipt({ txid }), - validate: (result: TransactionReceipt) => !isNil(result), + validate: (result: ethers.providers.TransactionReceipt) => !isNil(result), interval: 15000, maxAttempts: 30, }) - const gasPrice = await foxyApi.getGasPrice() if (transactionReceipt.status) { refetchFoxyBalances() @@ -134,14 +133,16 @@ export const ClaimStatus: React.FC = ({ accountId }) => { setState({ ...state, txStatus: transactionReceipt.status ? TxStatus.SUCCESS : TxStatus.FAILED, - usedGasFee: bnOrZero(gasPrice).times(transactionReceipt.gasUsed).toFixed(0), + usedGasFeeCryptoBaseUnit: transactionReceipt.effectiveGasPrice + .mul(transactionReceipt.gasUsed) + .toString(), }) } catch (error) { moduleLogger.error(error, 'ClaimStatus error') setState({ ...state, txStatus: TxStatus.FAILED, - usedGasFee: estimatedGas, + usedGasFeeCryptoBaseUnit: estimatedGas, }) } })() @@ -220,7 +221,9 @@ export const ClaimStatus: React.FC = ({ accountId }) => { = ({ accountId }) => { () - const { assetReference: contractAddress } = query + const { assetReference: foxyStakingContractAddress } = query const { feeAssetId, underlyingAsset, underlyingAssetId, stakingAsset } = useFoxyQuery() const marketData = useAppSelector(state => selectMarketDataById(state, underlyingAssetId)) @@ -68,12 +69,22 @@ export const FoxyWithdraw: React.FC<{ useEffect(() => { ;(async () => { try { - if (!(walletState.wallet && contractAddress && chainAdapter && foxyApi && bip44Params)) + if ( + !( + walletState.wallet && + foxyStakingContractAddress && + chainAdapter && + foxyApi && + bip44Params + ) + ) return - const foxyOpportunity = await foxyApi.getFoxyOpportunityByStakingAddress(contractAddress) + const foxyOpportunity = await foxyApi.getFoxyOpportunityByStakingAddress( + ethers.utils.getAddress(foxyStakingContractAddress), + ) // Get foxy fee for instant sends const foxyFeePercentage = await foxyApi.instantUnstakeFee({ - contractAddress, + contractAddress: foxyStakingContractAddress, }) dispatch({ @@ -89,7 +100,7 @@ export const FoxyWithdraw: React.FC<{ moduleLogger.error(error, 'FoxyWithdraw error:') } })() - }, [foxyApi, bip44Params, chainAdapter, contractAddress, walletState.wallet]) + }, [foxyApi, bip44Params, chainAdapter, foxyStakingContractAddress, walletState.wallet]) const StepConfig: DefiStepProps = useMemo(() => { return { @@ -105,7 +116,7 @@ export const FoxyWithdraw: React.FC<{ [DefiStep.Approve]: { label: translate('defi.steps.approve.title'), component: ownProps => , - props: { contractAddress }, + props: { contractAddress: foxyStakingContractAddress }, }, [DefiStep.Confirm]: { label: translate('defi.steps.confirm.title'), @@ -116,7 +127,7 @@ export const FoxyWithdraw: React.FC<{ component: ownProps => , }, } - }, [accountId, handleAccountIdChange, contractAddress, translate, stakingAsset.symbol]) + }, [accountId, handleAccountIdChange, foxyStakingContractAddress, translate, stakingAsset.symbol]) const handleBack = () => { history.push({ diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Approve.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Approve.tsx index 0037948171a..f3fb4510d18 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Approve.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Approve.tsx @@ -1,6 +1,7 @@ import { useToast } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId } from '@shapeshiftoss/caip' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { Approve as ReusableApprove } from 'features/defi/components/Approve/Approve' import { ApprovePreFooter } from 'features/defi/components/Approve/ApprovePreFooter' import type { WithdrawValues } from 'features/defi/components/Withdraw/Withdraw' @@ -67,19 +68,21 @@ export const Approve: React.FC = ({ accountId, onNext }) => { if (!(rewardId && userAddress && state?.withdraw && foxyApi && dispatch && bip44Params)) return try { - const [gasLimit, gasPrice] = await Promise.all([ - foxyApi.estimateWithdrawGas({ - tokenContractAddress: rewardId, - contractAddress, - amountDesired: bnOrZero( - bn(withdraw.cryptoAmount).times(`1e+${asset.precision}`), - ).decimalPlaces(0), - userAddress, - type: state.withdraw.withdrawType, - bip44Params, - }), - foxyApi.getGasPrice(), - ]) + const feeDataEstimate = await foxyApi.estimateWithdrawFees({ + tokenContractAddress: rewardId, + contractAddress, + amountDesired: bnOrZero( + bn(withdraw.cryptoAmount).times(`1e+${asset.precision}`), + ).decimalPlaces(0), + userAddress, + type: state.withdraw.withdrawType, + bip44Params, + }) + + const { + chainSpecific: { gasPrice, gasLimit }, + } = feeDataEstimate.fast + const returVal = bnOrZero(bn(gasPrice).times(gasLimit)).toFixed(0) return returVal } catch (error) { @@ -123,7 +126,10 @@ export const Approve: React.FC = ({ accountId, onNext }) => { ) ) return + try { + if (!supportsETH(walletState.wallet)) + throw new Error(`handleApprove: wallet does not support ethereum`) dispatch({ type: FoxyWithdrawActionType.SET_LOADING, payload: true }) await foxyApi.approve({ tokenContractAddress: rewardId, diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Confirm.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Confirm.tsx index 8ff80bf9699..bdd7b92587f 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Confirm.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Confirm.tsx @@ -1,7 +1,9 @@ import { Alert, AlertIcon, Box, Stack } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId } from '@shapeshiftoss/caip' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { WithdrawType } from '@shapeshiftoss/types' +import type { ethers } from 'ethers' import { Confirm as ReusableConfirm } from 'features/defi/components/Confirm/Confirm' import { Summary } from 'features/defi/components/Summary' import { DefiStep } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' @@ -9,7 +11,6 @@ import { useFoxyQuery } from 'features/defi/providers/foxy/components/FoxyManage import isNil from 'lodash/isNil' import { useCallback, useContext, useMemo } from 'react' import { useTranslate } from 'react-polyglot' -import type { TransactionReceipt } from 'web3-core/types' import { Amount } from 'components/Amount/Amount' import { AssetIcon } from 'components/AssetIcon' import type { StepComponentProps } from 'components/DeFi/components/Steps' @@ -85,26 +86,27 @@ export const Confirm: React.FC foxyApi.getTxReceipt({ txid }), - validate: (result: TransactionReceipt) => !isNil(result), + validate: (result: ethers.providers.TransactionReceipt) => !isNil(result), interval: 15000, maxAttempts: 30, }) @@ -112,9 +114,9 @@ export const Confirm: React.FC new ethers.providers.JsonRpcProvider(getConfig().REACT_APP_ETHEREUM_NODE_URL), + () => new ethers.providers.JsonRpcBatchProvider(getConfig().REACT_APP_ETHEREUM_NODE_URL), [], ) diff --git a/src/state/apis/foxy/foxyApiSingleton.ts b/src/state/apis/foxy/foxyApiSingleton.ts index d590b36db6e..520f02992d5 100644 --- a/src/state/apis/foxy/foxyApiSingleton.ts +++ b/src/state/apis/foxy/foxyApiSingleton.ts @@ -1,4 +1,4 @@ -import type { ChainAdapter } from '@shapeshiftoss/chain-adapters' +import type { EvmBaseAdapter } from '@shapeshiftoss/chain-adapters' import { foxyAddresses, FoxyApi } from '@shapeshiftoss/investor-foxy' import { KnownChainIds } from '@shapeshiftoss/types' import { getConfig } from 'config' @@ -22,7 +22,7 @@ export const getFoxyApi = (): FoxyApi => { const foxyApi = new FoxyApi({ adapter: getChainAdapterManager().get( KnownChainIds.EthereumMainnet, - ) as ChainAdapter, + ) as unknown as EvmBaseAdapter, providerUrl: getConfig()[RPC_PROVIDER_ENV], foxyAddresses, }) diff --git a/yarn.lock b/yarn.lock index 1f985276cbc..81bea864737 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4180,7 +4180,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/providers@npm:5.7.2, @ethersproject/providers@npm:^5.5.3": +"@ethersproject/providers@npm:5.7.2": version: 5.7.2 resolution: "@ethersproject/providers@npm:5.7.2" dependencies: @@ -5830,7 +5830,6 @@ __metadata: version: 0.0.0-use.local resolution: "@shapeshiftoss/asset-service@workspace:packages/asset-service" dependencies: - "@ethersproject/providers": ^5.5.3 "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/investor-idle": "workspace:^" "@shapeshiftoss/types": "workspace:^" @@ -6140,15 +6139,12 @@ __metadata: version: 0.0.0-use.local resolution: "@shapeshiftoss/investor-foxy@workspace:packages/investor-foxy" dependencies: - "@ethersproject/providers": ^5.5.3 "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/chain-adapters": "workspace:^" "@shapeshiftoss/logger": "workspace:^" "@shapeshiftoss/types": "workspace:^" "@types/readline-sync": ^1.4.4 readline-sync: ^1.4.10 - web3-core: 1.7.4 - web3-utils: 1.7.4 languageName: unknown linkType: soft @@ -6156,7 +6152,6 @@ __metadata: version: 0.0.0-use.local resolution: "@shapeshiftoss/investor-idle@workspace:packages/investor-idle" dependencies: - "@ethersproject/providers": ^5.5.3 "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/chain-adapters": "workspace:^" "@shapeshiftoss/investor": "workspace:^" @@ -6171,7 +6166,6 @@ __metadata: version: 0.0.0-use.local resolution: "@shapeshiftoss/investor-yearn@workspace:packages/investor-yearn" dependencies: - "@ethersproject/providers": ^5.5.3 "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/chain-adapters": "workspace:^" "@shapeshiftoss/investor": "workspace:^" @@ -6201,7 +6195,6 @@ __metadata: version: 0.0.0-use.local resolution: "@shapeshiftoss/market-service@workspace:packages/market-service" dependencies: - "@ethersproject/providers": ^5.5.3 "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/chain-adapters": "workspace:^" "@shapeshiftoss/investor-foxy": "workspace:^" @@ -6384,7 +6377,7 @@ __metadata: eslint-plugin-prettier: ^4.0.0 eslint-plugin-simple-import-sort: ^7.0.0 eth-url-parser: ^1.0.4 - ethers: ^5.5.3 + ethers: ^5.7.2 fast-json-stable-stringify: ^2.1.0 framer-motion: ^6.3.11 friendly-challenge: 0.9.2 @@ -15430,7 +15423,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^5.4.7, ethers@npm:^5.5.3, ethers@npm:^5.6.5, ethers@npm:^5.6.9, ethers@npm:^5.7.0, ethers@npm:^5.7.2": +"ethers@npm:^5.4.7, ethers@npm:^5.6.5, ethers@npm:^5.6.9, ethers@npm:^5.7.0, ethers@npm:^5.7.2": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: From 9167d8aaa0264b4d0f5a1363e9b60c5242ba604b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 25 Apr 2023 23:26:16 +0200 Subject: [PATCH 06/12] fix: foxy empty overview --- .../foxy/components/FoxyManager/Overview/FoxyOverview.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/FoxyOverview.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/FoxyOverview.tsx index f444af8c0ba..0ff832ef556 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/FoxyOverview.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Overview/FoxyOverview.tsx @@ -76,11 +76,10 @@ export const FoxyOverview: React.FC = ({ }, [handleAccountIdChange, maybeAccountId]) const opportunityDataFilter = useMemo(() => { + const userStakingAccountId = accountId ?? highestBalanceAccountId ?? '' + if (!userStakingAccountId) return undefined return { - userStakingId: serializeUserStakingId( - accountId ?? highestBalanceAccountId ?? '', - assetId as StakingId, - ), + userStakingId: serializeUserStakingId(userStakingAccountId, assetId as StakingId), } }, [accountId, assetId, highestBalanceAccountId]) From ecd050a3e1c3648e96bcd6fda258f3273c85a61a Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 25 Apr 2023 15:32:42 -0600 Subject: [PATCH 07/12] hotfix: remove contractAddress as it only used to build erc20 tx for fee estimation (bad and unobvious side effect) --- src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts | 1 - src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts | 1 - .../swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts b/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts index fcc36f8533b..9696dae278f 100644 --- a/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts +++ b/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts @@ -197,7 +197,6 @@ export const useFoxFarming = ( chainSpecific: { contractData: data, from: farmingAccountAddress, - contractAddress: uniV2LPContract.address, }, }) diff --git a/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts b/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts index a617b750862..9705e999b5a 100644 --- a/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts +++ b/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts @@ -467,7 +467,6 @@ export const useUniV2LiquidityPool = ({ chainSpecific: { contractData: data, from: fromAccountId(accountId).account, - contractAddress: contract.address, }, }) diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts index 169476bbf5a..79a23ec6edf 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts @@ -71,7 +71,6 @@ export async function zrxBuildTrade( value: quote.value, chainSpecific: { from: receiveAddress, - contractAddress: quote.to, contractData: quote.data, }, }) From c713d6180b0c3aae4a8d0626a1a62a6e79c03ebd Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 25 Apr 2023 23:49:38 +0200 Subject: [PATCH 08/12] fix: handle ethers BNs in investor-foxy --- packages/investor-foxy/src/api/api.ts | 50 +++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/investor-foxy/src/api/api.ts b/packages/investor-foxy/src/api/api.ts index d10fefc058e..1941779aee6 100644 --- a/packages/investor-foxy/src/api/api.ts +++ b/packages/investor-foxy/src/api/api.ts @@ -718,57 +718,57 @@ export class FoxyApi { this.provider, ) - const requestWithdrawalAmount = await (() => { + const requestWithdrawalAmount = await (async () => { try { - return stakingContract.requestWithdrawalAmount() + return (await stakingContract.requestWithdrawalAmount()).toString() } catch (e) { logger.error(e, 'failed to get requestWithdrawalAmount') return 0 } })() - const timeLeftToRequestWithdrawal = await (() => { + const timeLeftToRequestWithdrawal: string = await (async () => { try { - return stakingContract.timeLeftToRequestWithdrawal() + return (await stakingContract.timeLeftToRequestWithdrawal()).toString() } catch (e) { logger.error(e, 'failed to get timeLeftToRequestWithdrawal') - return 0 + return '0' } })() - const lastTokeCycleIndex = await (() => { + const lastTokeCycleIndex: string = await (async () => { try { - return stakingContract.methods.lastTokeCycleIndex().call() + return (await stakingContract.lastTokeCycleIndex()).toString() } catch (err) { logger.error(err, 'failed to get lastTokeCycleIndex') - return 0 + return '0' } })() - const duration = await (() => { + const duration: string = await (async () => { try { - return tokeManagerContract.getCycleDuration() + return (await tokeManagerContract.getCycleDuration()).toString() } catch (e) { logger.error(e, 'failed to get cycleDuration') - return 0 + return '0' } })() - const currentCycleIndex = await (() => { + const currentCycleIndex: string = await (async () => { try { - return tokeManagerContract.getCurrentCycleIndex() + return (await tokeManagerContract.getCurrentCycleIndex()).toString() } catch (e) { logger.error(e, 'failed to get currentCycleIndex') - return 0 + return '0' } })() - const currentCycleStart = await (() => { + const currentCycleStart: string = await (async () => { try { - return tokeManagerContract.getCurrentCycle() + return (await tokeManagerContract.getCurrentCycle()).toString() } catch (e) { logger.error(e, 'failed to get currentCycle') - return 0 + return '0' } })() @@ -931,7 +931,7 @@ export class FoxyApi { const contract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) try { const balance = await contract.balanceOf(userAddress) - return bnOrZero(balance) + return bnOrZero(balance.toString()) } catch (e) { throw new Error(`Failed to get balance: ${e}`) } @@ -951,7 +951,7 @@ export class FoxyApi { const liquidityReserveContract = this.getLiquidityReserveContract(liquidityReserveAddress) try { const feeInBasisPoints = await liquidityReserveContract.fee() - return bnOrZero(feeInBasisPoints).div(10000) // convert from basis points to decimal percentage + return bnOrZero(feeInBasisPoints.div(10000).toString()) // convert from basis points to decimal percentage } catch (e) { throw new Error(`Failed to get instantUnstake fee ${e}`) } @@ -963,7 +963,7 @@ export class FoxyApi { try { const totalSupply = await contract.totalSupply() - return bnOrZero(totalSupply) + return bnOrZero(totalSupply.toString()) } catch (e) { throw new Error(`Failed to get totalSupply: ${e}`) } @@ -985,7 +985,7 @@ export class FoxyApi { try { const balance = await contract.circulatingSupply() - return bnOrZero(balance) + return bnOrZero(balance.toString()) } catch (e) { throw new Error(`Failed to get tvl: ${e}`) } @@ -1137,14 +1137,14 @@ export class FoxyApi { }) // unstake events can trigger rebases, if they do, adjust the amount to not include that unstake's transfer amount - const preRebaseBalanceResult = bnOrZero(unadjustedPreRebaseBalance) - .minus(unstakedTransferAmount) - .plus(stakedTransferAmount) + const preRebaseBalanceResult = bnOrZero(unadjustedPreRebaseBalance.toString()) + .minus(unstakedTransferAmount.toString()) + .plus(stakedTransferAmount.toString()) .toString() return { preRebaseBalance: preRebaseBalanceResult, - postRebaseBalance: postRebaseBalanceResult, + postRebaseBalance: postRebaseBalanceResult.toString() as string, } } catch (e) { logger.error(e, 'failed to get balance of address') From 92a8e6086d710dbe78ef0766a1c5c1d540fad8ba Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 26 Apr 2023 00:21:34 +0200 Subject: [PATCH 09/12] fix: ethers.js float issue in instantUnstakeFee --- packages/investor-foxy/src/api/api.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/investor-foxy/src/api/api.ts b/packages/investor-foxy/src/api/api.ts index 1941779aee6..d9747670b13 100644 --- a/packages/investor-foxy/src/api/api.ts +++ b/packages/investor-foxy/src/api/api.ts @@ -950,8 +950,10 @@ export class FoxyApi { } const liquidityReserveContract = this.getLiquidityReserveContract(liquidityReserveAddress) try { - const feeInBasisPoints = await liquidityReserveContract.fee() - return bnOrZero(feeInBasisPoints.div(10000).toString()) // convert from basis points to decimal percentage + // ethers BigNumber doesn't support floats, so we have to convert it to a regular bn first + // to be able to get a float bignumber.js as an output + const feeInBasisPoints = bnOrZero((await liquidityReserveContract.fee()).toString()) + return feeInBasisPoints.div(10000) // convert from basis points to decimal percentage } catch (e) { throw new Error(`Failed to get instantUnstake fee ${e}`) } From 89c5d188d5d1922ba249318870302fc0f46f2c89 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 26 Apr 2023 10:46:21 +0200 Subject: [PATCH 10/12] fix: foxy claim disabled state --- .../Overview/Claim/ClaimConfirm.tsx | 87 +++++++++++++------ .../Overview/Claim/ClaimRoutes.tsx | 2 +- .../FoxyManager/Overview/FoxyOverview.tsx | 39 ++++++--- 3 files changed, 89 insertions(+), 39 deletions(-) diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx index f2f720bd775..e5feb757e27 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx @@ -9,9 +9,10 @@ import { useToast, } from '@chakra-ui/react' import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' -import { ASSET_REFERENCE, toAssetId } from '@shapeshiftoss/caip' +import { ASSET_NAMESPACE, ASSET_REFERENCE, toAssetId } from '@shapeshiftoss/caip' import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { KnownChainIds } from '@shapeshiftoss/types' +import dayjs from 'dayjs' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' import { useHistory } from 'react-router' @@ -26,16 +27,22 @@ import { useWallet } from 'hooks/useWallet/useWallet' import { bnOrZero } from 'lib/bignumber/bignumber' import { logger } from 'lib/logger' import { getFoxyApi } from 'state/apis/foxy/foxyApiSingleton' +import type { StakingId } from 'state/slices/opportunitiesSlice/types' +import { + serializeUserStakingId, + supportsUndelegations, +} from 'state/slices/opportunitiesSlice/utils' import { selectAssetById, selectBIP44ParamsByAccountId, + selectEarnUserStakingOpportunityByUserStakingId, selectMarketDataById, } from 'state/slices/selectors' import { useAppSelector } from 'state/store' type ClaimConfirmProps = { accountId: AccountId | undefined - assetId: AssetId + stakingAssetId: AssetId amount?: string contractAddress: string chainId: ChainId @@ -48,7 +55,7 @@ const moduleLogger = logger.child({ export const ClaimConfirm = ({ accountId, - assetId, + stakingAssetId, amount, contractAddress, chainId, @@ -57,7 +64,6 @@ export const ClaimConfirm = ({ const [userAddress, setUserAddress] = useState('') const [estimatedGas, setEstimatedGas] = useState('0') const [loading, setLoading] = useState(false) - const [canClaim, setCanClaim] = useState(false) const foxyApi = getFoxyApi() const { state: walletState } = useWallet() const translate = useTranslate() @@ -67,8 +73,8 @@ export const ClaimConfirm = ({ const chainAdapterManager = getChainAdapterManager() // Asset Info - const asset = useAppSelector(state => selectAssetById(state, assetId)) - const assetMarketData = useAppSelector(state => selectMarketDataById(state, assetId)) + const stakingAsset = useAppSelector(state => selectAssetById(state, stakingAssetId)) + const assetMarketData = useAppSelector(state => selectMarketDataById(state, stakingAssetId)) const feeAssetId = toAssetId({ chainId, assetNamespace: 'slip44', @@ -77,7 +83,7 @@ export const ClaimConfirm = ({ const feeAsset = useAppSelector(state => selectAssetById(state, feeAssetId)) const feeMarketData = useAppSelector(state => selectMarketDataById(state, feeAssetId)) - if (!asset) throw new Error(`Asset not found for AssetId ${assetId}`) + if (!stakingAsset) throw new Error(`Asset not found for AssetId ${stakingAssetId}`) if (!feeAsset) throw new Error(`Fee asset not found for AssetId ${feeAssetId}`) const toast = useToast() @@ -86,8 +92,41 @@ export const ClaimConfirm = ({ const bip44Params = useAppSelector(state => selectBIP44ParamsByAccountId(state, accountFilter)) const cryptoHumanBalance = useMemo( - () => bnOrZero(claimAmount).div(`1e+${asset.precision}`), - [asset.precision, claimAmount], + () => bnOrZero(claimAmount).div(`1e+${stakingAsset.precision}`), + [stakingAsset.precision, claimAmount], + ) + // The highest level AssetId/OpportunityId, in this case of the single FOXy contract + const assetId = toAssetId({ + chainId, + assetNamespace: ASSET_NAMESPACE.erc20, + assetReference: contractAddress, + }) + const opportunityDataFilter = useMemo(() => { + if (!accountId) return undefined + return { + userStakingId: serializeUserStakingId(accountId, assetId as StakingId), + } + }, [accountId, assetId]) + + const foxyEarnOpportunityData = useAppSelector(state => + opportunityDataFilter + ? selectEarnUserStakingOpportunityByUserStakingId(state, opportunityDataFilter) + : undefined, + ) + + const undelegations = useMemo( + () => + foxyEarnOpportunityData && supportsUndelegations(foxyEarnOpportunityData) + ? foxyEarnOpportunityData.undelegations + : undefined, + [foxyEarnOpportunityData], + ) + + const hasPendingUndelegation = Boolean( + undelegations && + undelegations.some(undelegation => + dayjs().isAfter(dayjs(undelegation.completionTime).unix()), + ), ) const handleConfirm = useCallback(async () => { @@ -105,7 +144,7 @@ export const ClaimConfirm = ({ }) history.push('/status', { txid, - assetId, + assetId: stakingAssetId, amount, userAddress, estimatedGas, @@ -124,7 +163,7 @@ export const ClaimConfirm = ({ } }, [ amount, - assetId, + stakingAssetId, bip44Params, chainId, contractAddress, @@ -152,22 +191,18 @@ export const ClaimConfirm = ({ accountNumber, }) setUserAddress(userAddress) - const [feeDataEstimate, canClaimWithdraw] = await Promise.all([ - foxyApi.estimateClaimWithdrawFees({ - claimAddress: userAddress, - userAddress, - contractAddress, - wallet: walletState.wallet, - bip44Params, - }), - foxyApi.canClaimWithdraw({ contractAddress, userAddress }), - ]) + const feeDataEstimate = await foxyApi.estimateClaimWithdrawFees({ + claimAddress: userAddress, + userAddress, + contractAddress, + wallet: walletState.wallet, + bip44Params, + }) const { chainSpecific: { gasPrice, gasLimit }, } = feeDataEstimate.fast - setCanClaim(canClaimWithdraw) const gasEstimate = bnOrZero(gasPrice).times(gasLimit).toFixed(0) setEstimatedGas(gasEstimate) } catch (error) { @@ -191,12 +226,12 @@ export const ClaimConfirm = ({ - + @@ -260,7 +295,7 @@ export const ClaimConfirm = ({