(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 Paragraph from 'src/components/layout/Paragraph'
import { getNetworkInfo } from 'src/config' import { getNetworkInfo } from 'src/config'
import { TransactionFailText } from 'src/components/TransactionFailText' 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 = { type TransactionFailTextProps = {
txEstimationExecutionStatus: EstimationStatus txEstimationExecutionStatus: EstimationStatus
@ -24,8 +20,6 @@ export const TransactionFees = ({
isOffChainSignature, isOffChainSignature,
txEstimationExecutionStatus, txEstimationExecutionStatus,
}: TransactionFailTextProps): React.ReactElement | null => { }: TransactionFailTextProps): React.ReactElement | null => {
const providerName = useSelector(providerNameSelector)
let transactionAction let transactionAction
if (isCreation) { if (isCreation) {
transactionAction = 'create' transactionAction = 'create'
@ -35,11 +29,6 @@ export const TransactionFees = ({
transactionAction = 'approve' transactionAction = 'approve'
} }
// FIXME this should be removed when estimating with WalletConnect correctly
if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) {
return null
}
return ( return (
<> <>
<Paragraph> <Paragraph>

View File

@ -1,5 +1,5 @@
import MuiTextField from '@material-ui/core/TextField' 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 React from 'react'
import { lg } from 'src/theme/variables' import { lg } from 'src/theme/variables'
@ -10,65 +10,93 @@ const overflowStyle = {
width: '100%', width: '100%',
} }
const styles = () => ({ const styles = () =>
root: { createStyles({
paddingTop: lg, root: {
paddingBottom: '12px', paddingTop: lg,
lineHeight: 0, paddingBottom: '12px',
}, lineHeight: 0,
}) },
})
class TextField extends React.PureComponent<any> { const useStyles = makeStyles(styles)
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 disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched type Props = {
input: {
const inputRoot = helperText ? classes.root : '' name: string
const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : '' onChange?: () => void
const inputProps = { value: string
...restInput, placeholder: string
autoComplete: 'off', type: string
'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}
/>
)
} }
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 getNetworkName = (): string => ETHEREUM_NETWORK[getNetworkId()]
export const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
const getCurrentEnvironment = (): string => { const getCurrentEnvironment = (): string => {
switch (NODE_ENV) { switch (NODE_ENV) {
case 'test': { case 'test': {
@ -76,15 +78,8 @@ export const getGasPrice = (): number | undefined => getConfig()?.gasPrice
export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.gasPriceOracle export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.gasPriceOracle
export const getRpcServiceUrl = (): string => { export const getRpcServiceUrl = (): string =>
const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId()) usesInfuraRPC ? `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` : getConfig().rpcServiceUrl
if (usesInfuraRPC) {
return `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}`
}
return getConfig().rpcServiceUrl
}
export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}` 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 { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { sameString } from 'src/utils/strings' import { sameString } from 'src/utils/strings'
import { WALLETS } from 'src/config/networks/network.d'
export enum EstimationStatus { export enum EstimationStatus {
LOADING = 'LOADING', LOADING = 'LOADING',
@ -177,10 +176,6 @@ export const useEstimateTransactionGas = ({
if (!txData.length) { if (!txData.length) {
return 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 isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size, txType)
const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType) const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType)

View File

@ -1,12 +1,14 @@
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { getWeb3 } from 'src/logic/wallets/getWeb3' import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { sameString } from 'src/utils/strings'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner' import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
import { List } from 'immutable' import { List } from 'immutable'
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' 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 // Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount
const parseRequiredTxGasResponse = (data: string): number => { 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 to: string
from: string from: string
data: string data: string
@ -116,12 +118,56 @@ export const getGasEstimationTxResponse = async (txConfig: {
return new BigNumber(estimationData.substring(138), 16).toNumber() 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') 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 ( const calculateMinimumGasForTransaction = async (
additionalGasBatches: number[], additionalGasBatches: number[],
safeAddress: string, safeAddress: string,

View File

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