Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gas estimation #1726

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
56 changes: 25 additions & 31 deletions src/hooks/useGasFeesUSD.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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])
}
10 changes: 8 additions & 2 deletions src/hooks/useSwapCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
184 changes: 83 additions & 101 deletions src/hooks/useSwapsGasEstimate.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 }
}
11 changes: 6 additions & 5 deletions src/pages/Swap/Components/SwapPlatformSelector.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -99,6 +100,7 @@ export function SwapPlatformSelector({
selectedTrade,
onSelectedPlatformChange,
}: SwapPlatformSelectorProps) {
const { chainId } = useActiveWeb3React()
const isMobileByMedia = useIsMobileByMedia()
const { t } = useTranslation('swap')
const theme = useTheme()
Expand All @@ -111,17 +113,16 @@ 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)

useEffect(() => {
setShowAllPlatformsTrades(false)
}, [allPlatformTrades?.length])

const showGasFees = estimations.length === allPlatformTrades?.length
const showGasFees = chainId === ChainId.MAINNET

const handleSelectedTradeOverride = useCallback(
(platformName: string) => {
Expand Down
Loading