(Fix) [Contract Interaction] Parameter validation (#974)
This commit is contained in:
parent
5a7ced92ad
commit
cefb33b593
11
package.json
11
package.json
|
@ -170,7 +170,7 @@
|
|||
"electron-updater": "4.3.1",
|
||||
"eth-sig-util": "^2.5.3",
|
||||
"express": "^4.17.1",
|
||||
"final-form": "4.20.0",
|
||||
"final-form": "^4.20.0",
|
||||
"final-form-calculate": "^1.3.1",
|
||||
"history": "4.10.1",
|
||||
"immortal-db": "^1.0.2",
|
||||
|
@ -185,7 +185,7 @@
|
|||
"query-string": "6.12.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-final-form": "6.5.0",
|
||||
"react-final-form": "^6.5.0",
|
||||
"react-final-form-listeners": "^1.0.2",
|
||||
"react-ga": "^2.7.0",
|
||||
"react-hot-loader": "4.12.21",
|
||||
|
@ -234,8 +234,9 @@
|
|||
"node-sass": "^4.14.1",
|
||||
"prettier": "2.0.5",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"truffle": "5.1.28",
|
||||
"typescript": "3.9.3",
|
||||
"wait-on": "5.0.0"
|
||||
"truffle": "5.1.23",
|
||||
"typescript": "~3.7.2",
|
||||
"wait-on": "5.0.0",
|
||||
"web3-utils": "^1.2.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,13 +33,15 @@ class TextField extends React.PureComponent<any> {
|
|||
} = 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
|
||||
|
||||
const inputRoot = helperText ? classes.root : ''
|
||||
const statusClasses = meta.valid ? 'isValid' : meta.error && (meta.dirty || meta.touched) ? 'isInvalid' : ''
|
||||
const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : ''
|
||||
const inputProps = {
|
||||
...restInput,
|
||||
autoComplete: 'off',
|
||||
|
@ -53,8 +55,8 @@ class TextField extends React.PureComponent<any> {
|
|||
|
||||
return (
|
||||
<MuiTextField
|
||||
error={meta.error && (meta.touched || !meta.pristine)}
|
||||
helperText={showError ? meta.error : helperText || ' '}
|
||||
error={hasError && showError}
|
||||
helperText={hasError && showError ? errorMessage : helperText || ' '}
|
||||
inputProps={inputProps} // blank in order to force to have helper text
|
||||
InputProps={inputRootProps}
|
||||
multiline={multiline}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
class ABIService {
|
||||
static extractUsefulMethods(abi) {
|
||||
return abi
|
||||
.filter(({ constant, name, type }) => type === 'function' && !!name && typeof constant === 'boolean')
|
||||
.map((method) => ({
|
||||
action: method.constant ? 'read' : 'write',
|
||||
...ABIService.getMethodSignatureAndSignatureHash(method),
|
||||
...method,
|
||||
}))
|
||||
.sort(({ name: a }, { name: b }) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1))
|
||||
}
|
||||
|
||||
static getMethodHash(method) {
|
||||
const signature = ABIService.getMethodSignature(method)
|
||||
return ABIService.getSignatureHash(signature)
|
||||
}
|
||||
|
||||
static getMethodSignatureAndSignatureHash(method) {
|
||||
const signature = ABIService.getMethodSignature(method)
|
||||
const signatureHash = ABIService.getSignatureHash(signature)
|
||||
return { signature, signatureHash }
|
||||
}
|
||||
|
||||
static getMethodSignature({ inputs, name }) {
|
||||
const params = inputs.map((x) => x.type).join(',')
|
||||
return `${name}(${params})`
|
||||
}
|
||||
|
||||
static getSignatureHash(signature) {
|
||||
const web3 = getWeb3()
|
||||
return web3.utils.keccak256(signature).toString()
|
||||
}
|
||||
|
||||
static isPayable(method) {
|
||||
return method.payable
|
||||
}
|
||||
}
|
||||
|
||||
export default ABIService
|
|
@ -0,0 +1,48 @@
|
|||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
export interface AbiItemExtended extends AbiItem {
|
||||
action: string
|
||||
methodSignature: string
|
||||
signatureHash: string
|
||||
}
|
||||
|
||||
export const getMethodSignature = ({ inputs, name }: AbiItem) => {
|
||||
const params = inputs.map((x) => x.type).join(',')
|
||||
return `${name}(${params})`
|
||||
}
|
||||
|
||||
export const getSignatureHash = (signature: string): string => {
|
||||
return web3.utils.keccak256(signature).toString()
|
||||
}
|
||||
|
||||
export const getMethodHash = (method: AbiItem): string => {
|
||||
const signature = getMethodSignature(method)
|
||||
return getSignatureHash(signature)
|
||||
}
|
||||
|
||||
export const getMethodSignatureAndSignatureHash = (
|
||||
method: AbiItem,
|
||||
): { methodSignature: string; signatureHash: string } => {
|
||||
const methodSignature = getMethodSignature(method)
|
||||
const signatureHash = getSignatureHash(methodSignature)
|
||||
return { methodSignature, signatureHash }
|
||||
}
|
||||
|
||||
export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => {
|
||||
return abi
|
||||
.filter(({ constant, name, type }) => type === 'function' && !!name && typeof constant === 'boolean')
|
||||
.map(
|
||||
(method): AbiItemExtended => ({
|
||||
action: method.constant ? 'read' : 'write',
|
||||
...getMethodSignatureAndSignatureHash(method),
|
||||
...method,
|
||||
}),
|
||||
)
|
||||
.sort(({ name: a }, { name: b }) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1))
|
||||
}
|
||||
|
||||
export const isPayable = (method: AbiItem | AbiItemExtended): boolean => {
|
||||
return method.payable
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
import { RateLimit } from 'async-sema'
|
||||
import memoize from 'lodash.memoize'
|
||||
|
||||
import ABIService from 'src/logic/contractInteraction/sources/ABIService'
|
||||
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
|
||||
import { ETHERSCAN_API_KEY } from 'src/utils/constants'
|
||||
|
||||
class EtherscanService extends ABIService {
|
||||
class EtherscanService {
|
||||
_rateLimit = async () => {}
|
||||
|
||||
_endpointsUrls = {
|
||||
|
@ -38,7 +37,6 @@ class EtherscanService extends ABIService {
|
|||
)
|
||||
|
||||
constructor(options) {
|
||||
super()
|
||||
this._rateLimit = RateLimit(options.rps)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,57 +5,45 @@ import { useField, useFormState } from 'react-final-form'
|
|||
import Button from 'src/components/layout/Button'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
import { createTxObject } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import { isReadMethod } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Buttons = ({ onCallSubmit, onClose }) => {
|
||||
export interface ButtonProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const Buttons = ({ onClose }: ButtonProps) => {
|
||||
const classes = useStyles()
|
||||
const {
|
||||
input: { value: method },
|
||||
} = useField('selectedMethod', { value: true })
|
||||
const {
|
||||
input: { value: contractAddress },
|
||||
} = useField('contractAddress', { valid: true } as any)
|
||||
const { submitting, valid, validating, values } = useFormState({
|
||||
subscription: { submitting: true, valid: true, values: true, validating: true },
|
||||
} = useField('selectedMethod', { subscription: { value: true } })
|
||||
const { modifiedSinceLastSubmit, submitError, submitting, valid, validating } = useFormState({
|
||||
subscription: {
|
||||
modifiedSinceLastSubmit: true,
|
||||
submitError: true,
|
||||
submitting: true,
|
||||
valid: true,
|
||||
validating: true,
|
||||
},
|
||||
})
|
||||
|
||||
const handleCallSubmit = async () => {
|
||||
const results = await createTxObject(method, contractAddress, values).call()
|
||||
onCallSubmit(results)
|
||||
}
|
||||
|
||||
return (
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
{method && (method as any).action === 'read' ? (
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="review-tx-btn"
|
||||
disabled={validating || !valid}
|
||||
minWidth={140}
|
||||
onClick={handleCallSubmit}
|
||||
variant="contained"
|
||||
>
|
||||
Call
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="review-tx-btn"
|
||||
disabled={submitting || validating || !valid || !method || (method as any).action === 'read'}
|
||||
minWidth={140}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid={`${isReadMethod(method) ? 'call' : 'review'}-tx-btn`}
|
||||
disabled={submitting || validating || ((!valid || !!submitError) && !modifiedSinceLastSubmit) || !method}
|
||||
minWidth={140}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
{isReadMethod(method) ? 'Call' : 'Review'}
|
||||
</Button>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,19 +3,19 @@ import React from 'react'
|
|||
import TextareaField from 'src/components/forms/TextareaField'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import EtherscanService from 'src/logic/contractInteraction/sources/EtherscanService'
|
||||
import { extractUsefulMethods } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
|
||||
export const NO_DATA = 'no data'
|
||||
|
||||
const mustBeValidABI = (abi) => {
|
||||
const mustBeValidABI = (abi: string): undefined | string => {
|
||||
try {
|
||||
const parsedABI = EtherscanService.extractUsefulMethods(JSON.parse(abi))
|
||||
const parsedABI = extractUsefulMethods(JSON.parse(abi))
|
||||
|
||||
if (parsedABI.length === 0) {
|
||||
return NO_DATA
|
||||
}
|
||||
} catch (e) {
|
||||
return []
|
||||
return NO_DATA
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,9 +14,17 @@ import Col from 'src/components/layout/Col'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const EthAddressInput = ({ isContract = true, isRequired = true, name, onScannedValue, text }) => {
|
||||
export interface EthAddressProps {
|
||||
isContract?: boolean
|
||||
isRequired?: boolean
|
||||
name: string
|
||||
onScannedValue: (scannedValue: string) => void
|
||||
text: string
|
||||
}
|
||||
|
||||
const EthAddressInput = ({ isContract = true, isRequired = true, name, onScannedValue, text }: EthAddressProps) => {
|
||||
const classes = useStyles()
|
||||
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress]
|
||||
const validate = composeValidators(...validatorsList.filter((_) => _))
|
||||
|
|
|
@ -11,21 +11,24 @@ import ButtonLink from 'src/components/layout/ButtonLink'
|
|||
import Col from 'src/components/layout/Col'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import ABIService from 'src/logic/contractInteraction/sources/ABIService'
|
||||
import { isPayable } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const EthValue = ({ onSetMax }) => {
|
||||
interface EthValueProps {
|
||||
onSetMax: (ethBalance: string) => void
|
||||
}
|
||||
const EthValue = ({ onSetMax }: EthValueProps) => {
|
||||
const classes = useStyles()
|
||||
const { ethBalance } = useSelector(safeSelector)
|
||||
const {
|
||||
input: { value: method },
|
||||
} = useField('selectedMethod', { value: true })
|
||||
const disabled = !ABIService.isPayable(method)
|
||||
} = useField('selectedMethod', { subscription: { value: true } })
|
||||
const disabled = !isPayable(method)
|
||||
|
||||
return (
|
||||
return disabled ? null : (
|
||||
<>
|
||||
<Row className={classes.fullWidth} margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
|
@ -42,7 +45,6 @@ const EthValue = ({ onSetMax }) => {
|
|||
<Row margin="md">
|
||||
<Col>
|
||||
<Field
|
||||
className={classes.addressInput}
|
||||
component={TextField}
|
||||
disabled={disabled}
|
||||
inputAdornment={{
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import { useFormState } from 'react-final-form'
|
||||
|
||||
import Row from 'src/components/layout/Row'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const FormErrorMessage = () => {
|
||||
const classes = useStyles()
|
||||
const { modifiedSinceLastSubmit, submitError } = useFormState({
|
||||
subscription: { modifiedSinceLastSubmit: true, submitError: true },
|
||||
})
|
||||
|
||||
const hasNewSubmitError = !!submitError && !modifiedSinceLastSubmit
|
||||
return hasNewSubmitError ? (
|
||||
<Row align="center" className={classes.fullWidth} margin="xs">
|
||||
<Paragraph color="error" noMargin size="md" style={{ letterSpacing: '-0.5px', overflowWrap: 'anywhere' }}>
|
||||
{submitError}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default FormErrorMessage
|
|
@ -7,14 +7,20 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Header = ({ onClose, subTitle, title }) => {
|
||||
interface HeaderProps {
|
||||
onClose: () => void
|
||||
subTitle: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const Header = ({ onClose, subTitle, title }: HeaderProps) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
{title}
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>{subTitle}</Paragraph>
|
||||
|
|
|
@ -8,23 +8,28 @@ import SearchIcon from '@material-ui/icons/Search'
|
|||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import { useField, useFormState } from 'react-final-form'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import EtherscanService from 'src/logic/contractInteraction/sources/EtherscanService'
|
||||
import { NO_CONTRACT } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import CheckIcon from 'src/routes/safe/components/CurrencyDropdown/img/check.svg'
|
||||
import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style'
|
||||
import { DropdownListTheme } from 'src/theme/mui'
|
||||
import { extractUsefulMethods } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
|
||||
const MENU_WIDTH = '452px'
|
||||
|
||||
const MethodsDropdown = ({ onChange }) => {
|
||||
interface MethodsDropdownProps {
|
||||
onChange: (method: AbiItem) => void
|
||||
}
|
||||
|
||||
const MethodsDropdown = ({ onChange }: MethodsDropdownProps) => {
|
||||
const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH })
|
||||
const {
|
||||
input: { value: abi },
|
||||
meta: { valid },
|
||||
} = useField('abi', { value: true, valid: true } as any)
|
||||
} = useField('abi', { subscription: { value: true, valid: true } })
|
||||
const {
|
||||
initialValues: { selectedMethod: selectedMethodByDefault },
|
||||
} = useFormState({ subscription: { initialValues: true } })
|
||||
|
@ -37,14 +42,14 @@ const MethodsDropdown = ({ onChange }) => {
|
|||
React.useEffect(() => {
|
||||
if (abi) {
|
||||
try {
|
||||
setMethodsList(EtherscanService.extractUsefulMethods(JSON.parse(abi)))
|
||||
setMethodsList(extractUsefulMethods(JSON.parse(abi)))
|
||||
} catch (e) {
|
||||
setMethodsList([])
|
||||
}
|
||||
}
|
||||
}, [abi])
|
||||
|
||||
React.useMemo(() => {
|
||||
React.useEffect(() => {
|
||||
setMethodsListFiltered(methodsList.filter(({ name }) => name.toLowerCase().includes(searchParams.toLowerCase())))
|
||||
}, [methodsList, searchParams])
|
||||
|
||||
|
@ -56,7 +61,7 @@ const MethodsDropdown = ({ onChange }) => {
|
|||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const onMethodSelectedChanged = (chosenMethod) => {
|
||||
const onMethodSelectedChanged = (chosenMethod: AbiItem) => {
|
||||
setSelectedMethod(chosenMethod)
|
||||
onChange(chosenMethod)
|
||||
handleClose()
|
||||
|
|
|
@ -8,10 +8,10 @@ import InputComponent from './InputComponent'
|
|||
const RenderInputParams = () => {
|
||||
const {
|
||||
meta: { valid: validABI },
|
||||
} = useField('abi', { value: true })
|
||||
} = useField('abi', { subscription: { valid: true, value: true } })
|
||||
const {
|
||||
input: { value: method },
|
||||
}: any = useField('selectedMethod', { value: true })
|
||||
}: any = useField('selectedMethod', { subscription: { value: true } })
|
||||
const renderInputs = validABI && !!method && method.inputs.length
|
||||
|
||||
return !renderInputs
|
||||
|
|
|
@ -3,19 +3,26 @@ import { useField } from 'react-final-form'
|
|||
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
|
||||
const RenderOutputParams = () => {
|
||||
const {
|
||||
input: { value: method },
|
||||
}: any = useField('selectedMethod', { value: true })
|
||||
}: any = useField('selectedMethod', { subscription: { value: true } })
|
||||
const {
|
||||
input: { value: results },
|
||||
}: any = useField('callResults', { value: true })
|
||||
}: any = useField('callResults', { subscription: { value: true } })
|
||||
const multipleResults = !!method && method.outputs.length > 1
|
||||
|
||||
return results
|
||||
? method.outputs.map(({ name, type }, index) => {
|
||||
return results ? (
|
||||
<>
|
||||
<Row align="left" margin="xs">
|
||||
<Paragraph color="primary" size="lg" style={{ letterSpacing: '-0.5px' }}>
|
||||
Call result:
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{method.outputs.map(({ name, type }, index) => {
|
||||
const placeholder = name ? `${name} (${type})` : type
|
||||
const key = `methodCallResult-${method.name}_${index}_${type}`
|
||||
const value = multipleResults ? results[index] : results
|
||||
|
@ -33,8 +40,9 @@ const RenderOutputParams = () => {
|
|||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
: null
|
||||
})}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default RenderOutputParams
|
||||
|
|
|
@ -3,8 +3,6 @@ import { useSnackbar } from 'notistack'
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import AddressInfo from 'src/components/AddressInfo'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
|
@ -13,11 +11,13 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Img from 'src/components/layout/Img'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gasNew'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
|
||||
|
@ -31,14 +31,7 @@ export type TransactionReviewType = {
|
|||
contractAddress?: string
|
||||
data?: string
|
||||
value?: string
|
||||
selectedMethod?: {
|
||||
action: string
|
||||
signature: string
|
||||
signatureHash: string
|
||||
constant: boolean
|
||||
inputs: []
|
||||
name: string
|
||||
}
|
||||
selectedMethod?: AbiItemExtended
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
@ -102,7 +95,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => {
|
|||
<>
|
||||
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<Block className={classes.formContainer}>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Contract Address
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import { border, lg, md, secondaryText, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = createStyles({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'flex-start',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
},
|
||||
annotation: {
|
||||
letterSpacing: '-1px',
|
||||
color: secondaryText,
|
||||
marginRight: 'auto',
|
||||
marginLeft: '20px',
|
||||
},
|
||||
headingText: {
|
||||
fontSize: lg,
|
||||
},
|
||||
closeIcon: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
container: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
value: {
|
||||
marginLeft: sm,
|
||||
},
|
||||
outerData: {
|
||||
borderRadius: '5px',
|
||||
border: `1px solid ${border}`,
|
||||
padding: '11px',
|
||||
minHeight: '21px',
|
||||
},
|
||||
data: {
|
||||
wordBreak: 'break-all',
|
||||
overflow: 'auto',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Averta',
|
||||
maxHeight: '100px',
|
||||
letterSpacing: 'normal',
|
||||
fontStretch: 'normal',
|
||||
lineHeight: '1.43',
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
'& > button': {
|
||||
fontFamily: 'Averta',
|
||||
fontSize: md,
|
||||
},
|
||||
},
|
||||
submitButton: {
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
marginLeft: '15px',
|
||||
},
|
||||
})
|
|
@ -1,31 +1,45 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import Buttons from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Buttons'
|
||||
import ContractABI from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ContractABI'
|
||||
import EthAddressInput from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput'
|
||||
import EthValue from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthValue'
|
||||
import FormDivisor from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/FormDivisor'
|
||||
import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||
import MethodsDropdown from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown'
|
||||
import RenderInputParams from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams'
|
||||
import RenderOutputParams from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderOutputParams'
|
||||
import {
|
||||
abiExtractor,
|
||||
createTxObject,
|
||||
formMutators,
|
||||
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||
import Buttons from './Buttons'
|
||||
import ContractABI from './ContractABI'
|
||||
import EthAddressInput from './EthAddressInput'
|
||||
import EthValue from './EthValue'
|
||||
import FormDivisor from './FormDivisor'
|
||||
import FormErrorMessage from './FormErrorMessage'
|
||||
import Header from './Header'
|
||||
import MethodsDropdown from './MethodsDropdown'
|
||||
import RenderInputParams from './RenderInputParams'
|
||||
import RenderOutputParams from './RenderOutputParams'
|
||||
import { abiExtractor, createTxObject, formMutators, handleSubmitError, isReadMethod } from './utils'
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }) => {
|
||||
export interface CreatedTx {
|
||||
contractAddress: string
|
||||
data: string
|
||||
selectedMethod: {}
|
||||
value: string | number
|
||||
}
|
||||
|
||||
export interface ContractInteractionProps {
|
||||
contractAddress: string
|
||||
initialValues: { contractAddress?: string }
|
||||
onClose: () => void
|
||||
onNext: (tx: CreatedTx) => void
|
||||
}
|
||||
|
||||
const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }: ContractInteractionProps) => {
|
||||
const classes = useStyles()
|
||||
const { address: safeAddress = '' } = useSelector(safeSelector)
|
||||
let setCallResults
|
||||
|
||||
React.useMemo(() => {
|
||||
if (contractAddress) {
|
||||
|
@ -35,8 +49,22 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
|||
|
||||
const handleSubmit = async ({ contractAddress, selectedMethod, value, ...values }) => {
|
||||
if (value || (contractAddress && selectedMethod)) {
|
||||
const data = await createTxObject(selectedMethod, contractAddress, values).encodeABI()
|
||||
onNext({ ...values, contractAddress, data, selectedMethod, value })
|
||||
try {
|
||||
const txObject = createTxObject(selectedMethod, contractAddress, values)
|
||||
const data = txObject.encodeABI()
|
||||
|
||||
if (isReadMethod(selectedMethod)) {
|
||||
const result = await txObject.call({ from: safeAddress })
|
||||
setCallResults(result)
|
||||
|
||||
// this was a read method, so we won't go to the 'review' screen
|
||||
return
|
||||
}
|
||||
|
||||
onNext({ ...values, contractAddress, data, selectedMethod, value })
|
||||
} catch (error) {
|
||||
return handleSubmitError(error, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,6 +80,8 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
|||
subscription={{ submitting: true, pristine: true }}
|
||||
>
|
||||
{(submitting, validating, rest, mutators) => {
|
||||
setCallResults = mutators.setCallResults
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.formContainer}>
|
||||
|
@ -62,14 +92,15 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
|||
onScannedValue={mutators.setContractAddress}
|
||||
text="Contract Address*"
|
||||
/>
|
||||
<EthValue onSetMax={mutators.setMax} />
|
||||
<ContractABI />
|
||||
<MethodsDropdown onChange={mutators.setSelectedMethod} />
|
||||
<EthValue onSetMax={mutators.setMax} />
|
||||
<RenderInputParams />
|
||||
<RenderOutputParams />
|
||||
<FormErrorMessage />
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Buttons onCallSubmit={mutators.setCallResults} onClose={onClose} />
|
||||
<Buttons onClose={onClose} />
|
||||
</>
|
||||
)
|
||||
}}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { lg, md } from 'src/theme/variables'
|
||||
import { lg, md, secondaryText, sm, border } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'flex-start',
|
||||
|
@ -9,11 +10,11 @@ export const styles = () => ({
|
|||
},
|
||||
annotation: {
|
||||
letterSpacing: '-1px',
|
||||
color: '#a2a8ba',
|
||||
color: secondaryText,
|
||||
marginRight: 'auto',
|
||||
marginLeft: '20px',
|
||||
},
|
||||
manage: {
|
||||
headingText: {
|
||||
fontSize: lg,
|
||||
},
|
||||
closeIcon: {
|
||||
|
@ -26,6 +27,25 @@ export const styles = () => ({
|
|||
formContainer: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
value: {
|
||||
marginLeft: sm,
|
||||
},
|
||||
outerData: {
|
||||
borderRadius: '5px',
|
||||
border: `1px solid ${border}`,
|
||||
padding: '11px',
|
||||
minHeight: '21px',
|
||||
},
|
||||
data: {
|
||||
wordBreak: 'break-all',
|
||||
overflow: 'auto',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Averta',
|
||||
maxHeight: '100px',
|
||||
letterSpacing: 'normal',
|
||||
fontStretch: 'normal',
|
||||
lineHeight: '1.43',
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { FORM_ERROR } from 'final-form'
|
||||
import createDecorator from 'final-form-calculate'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||
import { getNetwork } from 'src/config'
|
||||
import { getConfiguredSource } from 'src/logic/contractInteraction/sources'
|
||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { TransactionReviewType } from '../Review'
|
||||
|
||||
|
@ -49,7 +52,18 @@ export const formMutators = {
|
|||
},
|
||||
}
|
||||
|
||||
export const createTxObject = (method, contractAddress, values) => {
|
||||
export const handleSubmitError = (error, values) => {
|
||||
for (const key in values) {
|
||||
if (values.hasOwnProperty(key) && values[key] === error.value) {
|
||||
return { [key]: error.reason }
|
||||
}
|
||||
}
|
||||
|
||||
// .call() failed and we're logging a generic error
|
||||
return { [FORM_ERROR]: error.message }
|
||||
}
|
||||
|
||||
export const createTxObject = (method: AbiItem, contractAddress: string, values) => {
|
||||
const web3 = getWeb3()
|
||||
const contract: any = new web3.eth.Contract([method], contractAddress)
|
||||
const { inputs, name } = method
|
||||
|
@ -58,6 +72,8 @@ export const createTxObject = (method, contractAddress, values) => {
|
|||
return contract.methods[name](...args)
|
||||
}
|
||||
|
||||
export const isReadMethod = (method: AbiItemExtended): boolean => method && method.action === 'read'
|
||||
|
||||
export const getValueFromTxInputs = (key: string, type: string, tx: TransactionReviewType): string => {
|
||||
let value = tx[key]
|
||||
if (type === 'bool') {
|
||||
|
|
Loading…
Reference in New Issue