(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:
parent
3d43db039b
commit
758fc3c0c0
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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*"
|
||||
|
|
Loading…
Reference in New Issue