(Fix) Estimate gas for wallet connect (#1806)

* Guard for empty result

* Type TextField

* Fix warning of InputAdornment in SendFunds modal

* Re-enable gas estimation for wallet connect

* Replace web3.call on parseRequiredTxGasResponse with axios post to infura

* Adds estimateGasWithInfura and estimateGasWithWeb3Provider for changing the estimation method if we are in a non-infura-supported network

* Revert calculateMinimumGasForTransaction change to leave the change for the already-open pr

* Renames estimateGasWithInfura with estimateGasWithRPCCall
Replaces web3 with web3ReadOnly in estimateGasWithRPCCall

Co-authored-by: Mati Dastugue <matias.dastugue@altoros.com>
Co-authored-by: Fernando <fernando.greco@gmail.com>
Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Agustin Pane 2021-01-28 05:12:02 -03:00 committed by GitHub
parent 3d43db039b
commit 758fc3c0c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 151 additions and 90 deletions

View File

@ -3,10 +3,6 @@ import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
import Paragraph from 'src/components/layout/Paragraph'
import { getNetworkInfo } from 'src/config'
import { TransactionFailText } from 'src/components/TransactionFailText'
import { useSelector } from 'react-redux'
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
import { sameString } from 'src/utils/strings'
import { WALLETS } from 'src/config/networks/network.d'
type TransactionFailTextProps = {
txEstimationExecutionStatus: EstimationStatus
@ -24,8 +20,6 @@ export const TransactionFees = ({
isOffChainSignature,
txEstimationExecutionStatus,
}: TransactionFailTextProps): React.ReactElement | null => {
const providerName = useSelector(providerNameSelector)
let transactionAction
if (isCreation) {
transactionAction = 'create'
@ -35,11 +29,6 @@ export const TransactionFees = ({
transactionAction = 'approve'
}
// FIXME this should be removed when estimating with WalletConnect correctly
if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) {
return null
}
return (
<>
<Paragraph>

View File

@ -1,5 +1,5 @@
import MuiTextField from '@material-ui/core/TextField'
import { withStyles } from '@material-ui/core/styles'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import React from 'react'
import { lg } from 'src/theme/variables'
@ -10,65 +10,93 @@ const overflowStyle = {
width: '100%',
}
const styles = () => ({
root: {
paddingTop: lg,
paddingBottom: '12px',
lineHeight: 0,
},
})
const styles = () =>
createStyles({
root: {
paddingTop: lg,
paddingBottom: '12px',
lineHeight: 0,
},
})
class TextField extends React.PureComponent<any> {
render() {
const {
classes,
input: { name, onChange, value, ...restInput },
inputAdornment,
meta,
multiline,
rows,
testId,
text,
...rest
} = this.props
const helperText = value ? text : undefined
const showError = (meta.touched || !meta.pristine) && !meta.valid
const hasError = !!meta.error || (!meta.modifiedSinceLastSubmit && !!meta.submitError)
const errorMessage = meta.error || meta.submitError
const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched)
const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched
const useStyles = makeStyles(styles)
const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched
const inputRoot = helperText ? classes.root : ''
const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : ''
const inputProps = {
...restInput,
autoComplete: 'off',
'data-testid': testId,
}
const inputRootProps = {
...inputAdornment,
className: `${inputRoot} ${statusClasses}`,
disableUnderline: disableUnderline,
}
return (
<MuiTextField
error={hasError && showError}
helperText={hasError && showError ? errorMessage : helperText || ' '}
inputProps={inputProps} // blank in order to force to have helper text
InputProps={inputRootProps}
multiline={multiline}
name={name}
onChange={onChange}
rows={rows}
style={overflowStyle}
value={value}
{...rest}
/>
)
type Props = {
input: {
name: string
onChange?: () => void
value: string
placeholder: string
type: string
}
meta: {
touched?: boolean
pristine?: boolean
valid?: boolean
error?: string
modifiedSinceLastSubmit?: boolean
submitError?: boolean
active?: boolean
}
inputAdornment?: { endAdornment: React.ReactElement } | undefined
multiline: boolean
rows?: string
testId: string
text: string
disabled?: boolean
rowsMax?: number
className?: string
}
export default withStyles(styles as any)(TextField)
const TextField = (props: Props): React.ReactElement => {
const {
input: { name, onChange, value, ...restInput },
inputAdornment,
meta,
multiline,
rows,
testId,
text,
...rest
} = props
const classes = useStyles()
const helperText = value ? text : undefined
const showError = (meta.touched || !meta.pristine) && !meta.valid
const hasError = !!meta.error || (!meta.modifiedSinceLastSubmit && !!meta.submitError)
const errorMessage = meta.error || meta.submitError
const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched)
const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched
const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched
const inputRoot = helperText ? classes.root : ''
const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : ''
const inputProps = {
...restInput,
autoComplete: 'off',
'data-testid': testId,
}
const inputRootProps = {
...inputAdornment,
className: `${inputRoot} ${statusClasses}`,
disableUnderline: disableUnderline,
}
return (
<MuiTextField
error={hasError && showError}
helperText={hasError && showError ? errorMessage : helperText || ' '}
inputProps={inputProps} // blank in order to force to have helper text
InputProps={inputRootProps}
multiline={multiline}
name={name}
onChange={onChange}
rows={rows}
style={overflowStyle}
value={value}
{...rest}
/>
)
}
export default TextField

View File

@ -17,6 +17,8 @@ export const getNetworkId = (): ETHEREUM_NETWORK => ETHEREUM_NETWORK[NETWORK]
export const getNetworkName = (): string => ETHEREUM_NETWORK[getNetworkId()]
export const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
const getCurrentEnvironment = (): string => {
switch (NODE_ENV) {
case 'test': {
@ -76,15 +78,8 @@ export const getGasPrice = (): number | undefined => getConfig()?.gasPrice
export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.gasPriceOracle
export const getRpcServiceUrl = (): string => {
const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
if (usesInfuraRPC) {
return `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}`
}
return getConfig().rpcServiceUrl
}
export const getRpcServiceUrl = (): string =>
usesInfuraRPC ? `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` : getConfig().rpcServiceUrl
export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}`

View File

@ -22,7 +22,6 @@ import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { sameString } from 'src/utils/strings'
import { WALLETS } from 'src/config/networks/network.d'
export enum EstimationStatus {
LOADING = 'LOADING',
@ -177,10 +176,6 @@ export const useEstimateTransactionGas = ({
if (!txData.length) {
return
}
// FIXME this should be removed when estimating with WalletConnect correctly
if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) {
return null
}
const isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size, txType)
const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType)

View File

@ -1,12 +1,14 @@
import { BigNumber } from 'bignumber.js'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { sameString } from 'src/utils/strings'
import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
import { List } from 'immutable'
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
import axios from 'axios'
import { getRpcServiceUrl, usesInfuraRPC } from 'src/config'
import { sameString } from 'src/utils/strings'
// Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount
const parseRequiredTxGasResponse = (data: string): number => {
@ -88,7 +90,7 @@ export const getDataFromNodeErrorMessage = (errorMessage: string): string | unde
}
}
export const getGasEstimationTxResponse = async (txConfig: {
const estimateGasWithWeb3Provider = async (txConfig: {
to: string
from: string
data: string
@ -116,12 +118,56 @@ export const getGasEstimationTxResponse = async (txConfig: {
return new BigNumber(estimationData.substring(138), 16).toNumber()
}
// This will fail in case that we receive an EMPTY_DATA on the GETH node gas estimation (for version < v1.9.24 of geth nodes)
// We cannot throw this error above because it will be captured again on the catch block bellow
throw new Error('Error while estimating the gas required for tx')
}
const estimateGasWithRPCCall = async (txConfig: {
to: string
from: string
data: string
gasPrice?: number
gas?: number
}): Promise<number> => {
try {
const { data } = await axios.post(getRpcServiceUrl(), {
jsonrpc: '2.0',
method: 'eth_call',
id: 1,
params: [
{
...txConfig,
gasPrice: web3ReadOnly.utils.toHex(txConfig.gasPrice || 0),
gas: txConfig.gas ? web3ReadOnly.utils.toHex(txConfig.gas) : undefined,
},
'latest',
],
})
const { error } = data
if (error?.data) {
return new BigNumber(data.error.data.substring(138), 16).toNumber()
}
} catch (error) {
console.log('Gas estimation endpoint errored: ', error.message)
}
throw new Error('Error while estimating the gas required for tx')
}
export const getGasEstimationTxResponse = async (txConfig: {
to: string
from: string
data: string
gasPrice?: number
gas?: number
}): Promise<number> => {
// If we are in a infura supported network we estimate using infura
if (usesInfuraRPC) {
return estimateGasWithRPCCall(txConfig)
}
// Otherwise we estimate using the current connected provider
return estimateGasWithWeb3Provider(txConfig)
}
const calculateMinimumGasForTransaction = async (
additionalGasBatches: number[],
safeAddress: string,

View File

@ -75,6 +75,10 @@ type SendFundsProps = {
amount?: string
}
const InputAdornmentChildSymbol = ({ symbol }: { symbol?: string }): ReactElement => {
return <>{symbol}</>
}
const SendFunds = ({ onClose, onNext, recipientAddress, selectedToken = '', amount }: SendFundsProps): ReactElement => {
const classes = useStyles()
const tokens = useSelector(extendedSafeTokensSelector)
@ -295,7 +299,11 @@ const SendFunds = ({ onClose, onNext, recipientAddress, selectedToken = '', amou
<Field
component={TextField}
inputAdornment={{
endAdornment: <InputAdornment position="end">{selectedToken?.symbol}</InputAdornment>,
endAdornment: (
<InputAdornment position="end">
<InputAdornmentChildSymbol symbol={selectedToken?.symbol} />
</InputAdornment>
),
}}
name="amount"
placeholder="Amount*"