diff --git a/package.json b/package.json index cb69cf914..732480be8 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@reduxjs/toolkit": "^1.9.2", "@swapr/core": "^0.3.19", "@swapr/periphery": "^0.3.22", - "@swapr/sdk": "1.8.1", + "@swapr/sdk": "ImpeccableHQ/swapr-sdk#gas-estimation-build", "@tanstack/react-query": "4.24.6", "@types/react-helmet": "^6.1.6", "@uniswap/smart-order-router": "^2.9.3", diff --git a/src/hooks/useGasFeesUSD.ts b/src/hooks/useGasFeesUSD.ts index 0877f2dec..2d236f85d 100644 --- a/src/hooks/useGasFeesUSD.ts +++ b/src/hooks/useGasFeesUSD.ts @@ -5,46 +5,46 @@ import { BigNumber } from 'ethers' import { useMemo } from 'react' import { MainnetGasPrice } from '../state/application/actions' -import { useMainnetGasPrices } from '../state/application/hooks' import { useUserPreferredGasPrice } from '../state/user/hooks' +import { useGasInfo } from './useGasInfo' import { useNativeCurrencyUSDPrice } from './useNativeCurrencyUSDPrice' import { useActiveWeb3React } from './index' -export function useGasFeesUSD(gasEstimations: (BigNumber | null)[]): { +export function useGasFeesUSD(gasEstimations: (BigNumber | undefined)[]): { loading: boolean gasFeesUSD: (CurrencyAmount | null)[] } { const { chainId } = useActiveWeb3React() - const mainnetGasPrices = useMainnetGasPrices() + const [preferredGasPrice] = useUserPreferredGasPrice() const { loading: loadingNativeCurrencyUSDPrice, nativeCurrencyUSDPrice } = useNativeCurrencyUSDPrice() + const { gas, loading: loadingGasPrices } = useGasInfo() return useMemo(() => { - if (loadingNativeCurrencyUSDPrice) return { loading: true, gasFeesUSD: [] } - if ( - !gasEstimations || - gasEstimations.length === 0 || - !preferredGasPrice || - !chainId || - (preferredGasPrice in MainnetGasPrice && !mainnetGasPrices) - ) + if (loadingNativeCurrencyUSDPrice || loadingGasPrices) return { loading: true, gasFeesUSD: [] } + + if (!gasEstimations || gasEstimations.length === 0 || !chainId || !preferredGasPrice) return { loading: false, gasFeesUSD: [] } - const normalizedPreferredGasPrice = - mainnetGasPrices && preferredGasPrice in MainnetGasPrice - ? mainnetGasPrices[preferredGasPrice as MainnetGasPrice] - : preferredGasPrice - // protects cases in which mainnet gas prices is undefined but - // preferred gas price remained set to INSTANT, FAST or NORMAL - if (Number.isNaN(normalizedPreferredGasPrice)) return { loading: false, gasFeesUSD: [] } + + const gasMapped: { + [key in MainnetGasPrice]: number + } = { + [MainnetGasPrice.INSTANT]: gas.fast, + [MainnetGasPrice.FAST]: gas.slow, + [MainnetGasPrice.NORMAL]: gas.normal, + } + return { loading: false, gasFeesUSD: gasEstimations.map(gasEstimation => { - if (!gasEstimation) return null - const nativeCurrencyAmount = CurrencyAmount.nativeCurrency( - gasEstimation.mul(normalizedPreferredGasPrice).toString(), - chainId - ) + if (gasEstimation === undefined) return null + //hardcoded gas price to 20 gwei + + const gasCalc = gasMapped[preferredGasPrice as MainnetGasPrice] + '000000000' + + const nativeCurrencyAmount = CurrencyAmount.nativeCurrency(gasEstimation.mul(gasCalc).toString(), chainId) + return CurrencyAmount.usd( parseUnits( nativeCurrencyAmount.multiply(nativeCurrencyUSDPrice).toFixed(USD.decimals), @@ -53,12 +53,6 @@ export function useGasFeesUSD(gasEstimations: (BigNumber | null)[]): { ) }), } - }, [ - gasEstimations, - loadingNativeCurrencyUSDPrice, - mainnetGasPrices, - nativeCurrencyUSDPrice, - preferredGasPrice, - chainId, - ]) + //eslint-disable-next-line react-hooks/exhaustive-deps + }, [gasEstimations, loadingNativeCurrencyUSDPrice, nativeCurrencyUSDPrice, chainId]) } diff --git a/src/hooks/useSwapCallback.ts b/src/hooks/useSwapCallback.ts index 58ebe60e1..5fe807939 100644 --- a/src/hooks/useSwapCallback.ts +++ b/src/hooks/useSwapCallback.ts @@ -53,7 +53,7 @@ interface FailedCall { error: Error } -type EstimatedSwapCall = SuccessfulCall | FailedCall +export type EstimatedSwapCall = SuccessfulCall | FailedCall /** * Returns the swap calls that can be used to make the trade @@ -227,7 +227,13 @@ export function useSwapCallback({ const estimatedCalls: EstimatedSwapCall[] = await Promise.all( swapCalls.map(async call => { - const transactionRequest = await call.transactionParameters + let transactionRequest: any + try { + transactionRequest = await call.transactionParameters + } catch (e: any) { + console.error('Failed to get transaction parameters', e) + } + // Ignore gas estimation if the request has gasLimit property if (transactionRequest.gasLimit) { return { diff --git a/src/hooks/useSwapsGasEstimate.ts b/src/hooks/useSwapsGasEstimate.ts index 852f69a52..d168be25a 100644 --- a/src/hooks/useSwapsGasEstimate.ts +++ b/src/hooks/useSwapsGasEstimate.ts @@ -1,19 +1,10 @@ import { Web3Provider } from '@ethersproject/providers' -import { ChainId, Token, Trade, UniswapV2RoutablePlatform, UniswapV2Trade } from '@swapr/sdk' +import { RoutablePlatform, Trade } from '@swapr/sdk' import { BigNumber } from 'ethers' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { INITIAL_ALLOWED_SLIPPAGE } from '../constants' -import { useTokenAllowancesForMultipleSpenders } from '../data/Allowances' -import { MainnetGasPrice } from '../state/application/actions' -import { useMainnetGasPrices } from '../state/application/hooks' -import { tryParseAmount, useSwapState } from '../state/swap/hooks' -import { Field } from '../state/swap/types' -import { useUserPreferredGasPrice } from '../state/user/hooks' -import { useCurrencyBalance } from '../state/wallet/hooks' -import { calculateGasMargin } from '../utils' -import { useCurrency } from './Tokens' import useENS from './useENS' import { useSwapsCallArguments } from './useSwapCallback' @@ -23,121 +14,112 @@ export function useSwapsGasEstimations( allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, recipientAddressOrName: string | null, trades?: (Trade | undefined)[] -): { loading: boolean; estimations: (BigNumber | null)[][] } { +): { loading: boolean; estimations: (BigNumber | undefined)[] } { const { account, library, chainId } = useActiveWeb3React() - const platformSwapCalls = useSwapsCallArguments(trades, allowedSlippage, recipientAddressOrName) - const mainnetGasPrices = useMainnetGasPrices() - const [preferredGasPrice] = useUserPreferredGasPrice() - - const { - independentField, - typedValue, - INPUT: { currencyId: inputCurrencyId }, - OUTPUT: { currencyId: outputCurrencyId }, - } = useSwapState() - const isExactIn = independentField === Field.INPUT - const independentCurrencyId = isExactIn ? inputCurrencyId : outputCurrencyId - const independentCurrency = useCurrency(independentCurrencyId) - const independentCurrencyBalance = useCurrencyBalance(account || undefined, independentCurrency || undefined) - const typedIndependentCurrencyAmount = useMemo( - () => tryParseAmount(typedValue, independentCurrency || undefined, chainId), - [chainId, independentCurrency, typedValue] - ) - const routerAddresses = useMemo(() => { - if (!trades) return undefined - const rawRouterAddresses = trades.map(trade => { - return ( - trade instanceof UniswapV2Trade && - (trade.platform as UniswapV2RoutablePlatform).routerAddress[chainId || ChainId.MAINNET] - ) - }) - if (rawRouterAddresses.some(address => !address)) return undefined - return rawRouterAddresses as string[] - }, [chainId, trades]) - const routerAllowances = useTokenAllowancesForMultipleSpenders( - independentCurrency as Token, - account || undefined, - routerAddresses - ) - - // this boolean represents whether the user has approved the traded token and whether they - // have enough balance for the trade to go through or not. If any of the preconditions are - // not satisfied, the trade won't go through, so no gas estimations are performed - const calculateGasFees = useMemo(() => { - return ( - !!account && - !!trades && - trades.length > 0 && - !!preferredGasPrice && - (preferredGasPrice in MainnetGasPrice ? !!mainnetGasPrices : true) && - routerAllowances && - routerAllowances.length === trades.length && - typedIndependentCurrencyAmount && - independentCurrencyBalance && - (independentCurrencyBalance.greaterThan(typedIndependentCurrencyAmount) || - independentCurrencyBalance.equalTo(typedIndependentCurrencyAmount)) - ) - }, [ - account, - independentCurrencyBalance, - mainnetGasPrices, - preferredGasPrice, - routerAllowances, - trades, - typedIndependentCurrencyAmount, - ]) + + const platformSwapCalls = useSwapsCallArguments(trades, allowedSlippage, recipientAddressOrName || account) const [loading, setLoading] = useState(false) - const [estimations, setEstimations] = useState<(BigNumber | null)[][]>([]) + const [estimations, setEstimations] = useState<(BigNumber | undefined)[]>([]) const { address: recipientAddress } = useENS(recipientAddressOrName) const recipient = recipientAddress || account const updateEstimations = useCallback(async () => { - if (!routerAllowances || !trades || !typedIndependentCurrencyAmount || routerAllowances.length !== trades.length) - return + if (!trades || platformSwapCalls.length === 0 || !chainId) return + setLoading(true) const estimatedCalls = [] + for (let i = 0; i < platformSwapCalls.length; i++) { const platformCalls = platformSwapCalls[i] - let specificCalls = [] - // if the allowance to the router for the traded token is less than the - // types amount, avoid estimating gas since the tx would fail, printing - // an horrible error log in the console, continuously - if (routerAllowances[i].lessThan(typedIndependentCurrencyAmount)) { - specificCalls = platformCalls.map(() => null) + const call = platformCalls[0] + + let estimatedCall = undefined + if (trades[i]?.estimatedGas !== undefined) { + //get estimated gas from trade when it is available + estimatedCall = trades[i]?.estimatedGas + } else if (!call || call.transactionParameters === undefined || trades[i]?.platform === RoutablePlatform.COW) { + estimatedCall = undefined } else { - for (const call of platformCalls) { - const { transactionParameters } = call - // const { value } = await transactionParameters - // const options = !value || isZero(value as string) ? {} : { value } + try { + const { to, data, value } = await call.transactionParameters + const isNative = !(value as BigNumber).isZero() - let estimatedCall = null + const transactionObject = { + to, + data, + value, + from: !isNative ? account : undefined, + } as any try { - estimatedCall = calculateGasMargin( - await (library as Web3Provider).estimateGas(transactionParameters as any) - ) - } catch (error) { - console.error(error) - // silent fail - } finally { - specificCalls.push(estimatedCall) + //try to estimate gas from rpc function + estimatedCall = await (library as Web3Provider).estimateGas(transactionObject) + } catch { + //commented out for now since the alchemy api is not estimating gas correctly + //else estimate withc alchemy api using execution bundle + // try { + // //check if there user has inputted amount greate then max amount + // if (maxAmountInput && !isNative && !parsedAmount?.greaterThan(maxAmountInput.raw.toString())) { + // const amount = parseEther(maxAmountInput?.raw.toString()!) + // const tokenContractAddress = tokenContract?.address + // const approvalData = tokenContract?.interface.encodeFunctionData('approve', [to, amount]) + // const appovalTx = { + // to: tokenContractAddress, + // data: approvalData, + // from: account, + // } + // const transferData = tokenContract?.interface.encodeFunctionData('transferFrom', [ + // tokenContractAddress, + // account, + // amount, + // ]) + // const mintTx = { + // to: tokenContractAddress, + // data: transferData, + // from: tokenContractAddress, + // } + // const swapTransaction = { + // to, + // data, + // value, + // from: !isNative ? account : undefined, + // } as any + // const params = [appovalTx, mintTx, swapTransaction] + // const options = alchemyExectuionBundleOptions(params) + // const alchemyRequest = fetch( + // `https://eth-mainnet.g.alchemy.com/v2/${process.env.REACT_APP_ALCHEMY_KEY}`, + // options + // ) + // const gasCalculation = await calucalateGasFromAlchemyResponse(alchemyRequest) + // estimatedCall = BigNumber.from(gasCalculation) + // } + // } catch (e) { + // console.error(`Gas estimation failed for trade ${trades[i]?.platform.name}:`, e) + // } } + } catch (e) { + console.error('Error fetching call estimations:', e) } } - estimatedCalls.push(specificCalls) + + estimatedCalls.push(estimatedCall) } + setEstimations(estimatedCalls) setLoading(false) - }, [platformSwapCalls, library, routerAllowances, trades, typedIndependentCurrencyAmount]) + //eslint-disable-next-line react-hooks/exhaustive-deps + }, [platformSwapCalls]) useEffect(() => { - if (!trades || trades.length === 0 || !library || !chainId || !recipient || !account || !calculateGasFees) { + if (!platformSwapCalls || platformSwapCalls.length === 0 || !library || !chainId || !recipient) { setEstimations([]) - return + } else { + updateEstimations() } - updateEstimations() - }, [chainId, library, recipient, trades, updateEstimations, account, calculateGasFees]) + + //eslint-disable-next-line react-hooks/exhaustive-deps + }, [platformSwapCalls]) return { loading: loading, estimations } } diff --git a/src/pages/Swap/Components/SwapPlatformSelector.tsx b/src/pages/Swap/Components/SwapPlatformSelector.tsx index c0890ed74..3e6f0d4d5 100644 --- a/src/pages/Swap/Components/SwapPlatformSelector.tsx +++ b/src/pages/Swap/Components/SwapPlatformSelector.tsx @@ -1,4 +1,4 @@ -import { CurrencyAmount, Percent, RoutablePlatform, Trade, TradeType, UniswapV2Trade } from '@swapr/sdk' +import { CurrencyAmount, Percent, RoutablePlatform, Trade, TradeType, UniswapV2Trade, ChainId } from '@swapr/sdk' import { useCallback, useEffect, useState } from 'react' import { ChevronsDown } from 'react-feather' @@ -20,6 +20,7 @@ import { } from '../../../components/SelectionList' import WarningHelper from '../../../components/WarningHelper' import { ONE_BIPS, PRICE_IMPACT_MEDIUM, ROUTABLE_PLATFORM_LOGO } from '../../../constants' +import { useActiveWeb3React } from '../../../hooks' import useDebounce from '../../../hooks/useDebounce' import { useGasFeesUSD } from '../../../hooks/useGasFeesUSD' import { useIsMobileByMedia } from '../../../hooks/useIsMobileByMedia' @@ -99,6 +100,7 @@ export function SwapPlatformSelector({ selectedTrade, onSelectedPlatformChange, }: SwapPlatformSelectorProps) { + const { chainId } = useActiveWeb3React() const isMobileByMedia = useIsMobileByMedia() const { t } = useTranslation('swap') const theme = useTheme() @@ -111,9 +113,8 @@ export function SwapPlatformSelector({ recipient, allPlatformTrades ) - const { loading: loadingGasFeesUSD, gasFeesUSD } = useGasFeesUSD( - estimations.map(estimation => (estimation && estimation.length > 0 ? estimation[0] : null)) - ) + + const { loading: loadingGasFeesUSD, gasFeesUSD } = useGasFeesUSD(estimations) const loadingGasFees = loadingGasFeesUSD || loadingTradesGasEstimates const debouncedLoadingGasFees = useDebounce(loadingGasFees, 2000) @@ -121,7 +122,7 @@ export function SwapPlatformSelector({ setShowAllPlatformsTrades(false) }, [allPlatformTrades?.length]) - const showGasFees = estimations.length === allPlatformTrades?.length + const showGasFees = chainId === ChainId.MAINNET const handleSelectedTradeOverride = useCallback( (platformName: string) => { diff --git a/src/utils/alchemy.ts b/src/utils/alchemy.ts new file mode 100644 index 000000000..caf5e7d94 --- /dev/null +++ b/src/utils/alchemy.ts @@ -0,0 +1,68 @@ +interface TransactionParams { + from: string + to: string + value: string + data: string +} + +export const alchemyExectuionBundleOptions = (params: TransactionParams[]) => ({ + method: 'POST', + headers: { accept: 'application/json', 'content-type': 'application/json' }, + body: JSON.stringify({ + id: 1, + jsonrpc: '2.0', + method: 'alchemy_simulateExecutionBundle', + params: [params], + }), +}) +interface ApiResponse { + jsonrpc: string + id: number + result: { + calls: { + type: string + from: string + to: string + value: string + gas: string + gasUsed: string + input: string + output: string + decoded?: { + authority: string + methodName: string + inputs: { + name: string + value: string + type: string + }[] + outputs: { + name: string + value: string + type: string + }[] + } + }[] + logs: { + address: string + data: string + topics: string[] + }[] + }[] +} + +export const calucalateGasFromAlchemyResponse = async (response: Promise): Promise => { + const data = (await response).json() as Promise + const awaitedData = await data + const gasUsed = awaitedData.result.map(item => { + return item.calls.reduce((sum, call) => { + return sum + parseInt(call.gasUsed) + }, 0) + }) + + const totalGasUsed = gasUsed.reduce((sum, value) => { + return sum + value + }, 0) + + return totalGasUsed +} diff --git a/yarn.lock b/yarn.lock index 5dba02694..f596e923c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7685,7 +7685,7 @@ __metadata: "@storybook/testing-library": ^0.0.13 "@swapr/core": ^0.3.19 "@swapr/periphery": ^0.3.22 - "@swapr/sdk": 1.8.1 + "@swapr/sdk": "ImpeccableHQ/swapr-sdk#gas-estimation-build" "@synthetixio/synpress": ^2.3.3 "@tanstack/react-query": 4.24.6 "@testing-library/cypress": ^8.0.3 @@ -7824,9 +7824,9 @@ __metadata: languageName: node linkType: hard -"@swapr/sdk@npm:1.8.1": +"@swapr/sdk@ImpeccableHQ/swapr-sdk#gas-estimation-build": version: 1.8.1 - resolution: "@swapr/sdk@npm:1.8.1" + resolution: "@swapr/sdk@https://github.com/ImpeccableHQ/swapr-sdk.git#commit=689b703eb42b27bb44c64f01913601a3e4844e2d" dependencies: "@cowprotocol/cow-sdk": ^1.0.2-RC.0 "@ethersproject/abi": ^5.6.4 @@ -7857,7 +7857,7 @@ __metadata: tslib: ^2.3.1 peerDependencies: ethers: ^5.4.0 - checksum: 60c1f71211328d6b7ab3fd627b317bb4eef54bf73c2b05d6e9abebd69d745f5f737b47b677e20affdc353afeb73b14c8ba4d98e4caa15d5f8d5e15a4ecbb58fc + checksum: 979fa7b673c862861b96e3eab03fd25791430dd2903278a7dcd7250cd7815dc4278435dd8bd5649121f9dde5ea1114983e7067ca2112452246a7752f8a6ebc15 languageName: node linkType: hard