Merge pull request #1038 from gnosis/feature/supportCustomTX
[Contract Interaction] Support Hex Encoded Payload
This commit is contained in:
commit
6240afc07a
|
@ -18,7 +18,6 @@ module.exports = {
|
|||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/camelcase': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
|
|
|
@ -19,6 +19,10 @@ const ContractInteraction = React.lazy(() => import('./screens/ContractInteracti
|
|||
|
||||
const ContractInteractionReview: any = React.lazy(() => import('./screens/ContractInteraction/Review'))
|
||||
|
||||
const SendCustomTx = React.lazy(() => import('./screens/ContractInteraction/SendCustomTx'))
|
||||
|
||||
const ReviewCustomTx = React.lazy(() => import('./screens/ContractInteraction/ReviewCustomTx'))
|
||||
|
||||
const useStyles = makeStyles({
|
||||
scalableModalWindow: {
|
||||
height: 'auto',
|
||||
|
@ -40,9 +44,11 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
|||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType')
|
||||
const [tx, setTx] = useState({})
|
||||
const [isABI, setIsABI] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
setActiveScreen(activeScreenType || 'chooseTxType')
|
||||
setIsABI(true)
|
||||
setTx({})
|
||||
}, [activeScreenType, isOpen])
|
||||
|
||||
|
@ -53,9 +59,14 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
|||
setTx(txInfo)
|
||||
}
|
||||
|
||||
const handleContractInteractionCreation = (contractInteractionInfo) => {
|
||||
const handleContractInteractionCreation = (contractInteractionInfo: any, submit: boolean): void => {
|
||||
setTx(contractInteractionInfo)
|
||||
setActiveScreen('contractInteractionReview')
|
||||
if (submit) setActiveScreen('contractInteractionReview')
|
||||
}
|
||||
|
||||
const handleCustomTxCreation = (customTxInfo: any, submit: boolean): void => {
|
||||
setTx(customTxInfo)
|
||||
if (submit) setActiveScreen('reviewCustomTx')
|
||||
}
|
||||
|
||||
const handleSendCollectible = (txInfo) => {
|
||||
|
@ -63,6 +74,10 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
|||
setTx(txInfo)
|
||||
}
|
||||
|
||||
const handleSwitchMethod = (): void => {
|
||||
setIsABI(!isABI)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
description="Send Tokens Form"
|
||||
|
@ -93,17 +108,32 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
|||
{activeScreen === 'reviewTx' && (
|
||||
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx} />
|
||||
)}
|
||||
{activeScreen === 'contractInteraction' && (
|
||||
{activeScreen === 'contractInteraction' && isABI && (
|
||||
<ContractInteraction
|
||||
isABI={isABI}
|
||||
switchMethod={handleSwitchMethod}
|
||||
contractAddress={recipientAddress}
|
||||
initialValues={tx}
|
||||
onClose={onClose}
|
||||
onNext={handleContractInteractionCreation}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'contractInteractionReview' && tx && (
|
||||
{activeScreen === 'contractInteractionReview' && isABI && tx && (
|
||||
<ContractInteractionReview onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
|
||||
)}
|
||||
{activeScreen === 'contractInteraction' && !isABI && (
|
||||
<SendCustomTx
|
||||
initialValues={tx}
|
||||
isABI={isABI}
|
||||
switchMethod={handleSwitchMethod}
|
||||
onClose={onClose}
|
||||
onNext={handleCustomTxCreation}
|
||||
contractAddress={recipientAddress}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'reviewCustomTx' && (
|
||||
<ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
|
||||
)}
|
||||
{activeScreen === 'sendCollectible' && (
|
||||
<SendCollectible
|
||||
initialValues={tx}
|
||||
|
|
|
@ -50,7 +50,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => {
|
|||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const estimateGas = async () => {
|
||||
const estimateGas = async (): Promise<void> => {
|
||||
const { fromWei, toBN } = getWeb3().utils
|
||||
const txData = tx.data ? tx.data.trim() : ''
|
||||
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import ArrowDown from '../../assets/arrow-down.svg'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Col from 'src/components/layout/Col'
|
||||
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 { 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 SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
|
||||
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||
import { sm } from 'src/theme/variables'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
onPrev: () => void
|
||||
tx: { contractAddress?: string; data?: string; value?: string }
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ReviewCustomTx = ({ onClose, onPrev, tx }: Props) => {
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress } = useSelector(safeSelector)
|
||||
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const estimateGas = async () => {
|
||||
const { fromWei, toBN } = getWeb3().utils
|
||||
const txData = tx.data ? tx.data.trim() : ''
|
||||
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.contractAddress, txData)
|
||||
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
|
||||
const formattedGasCosts = formatAmount(gasCostsAsEth)
|
||||
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [safeAddress, tx.data, tx.contractAddress])
|
||||
|
||||
const submitTx = async (): Promise<void> => {
|
||||
const web3 = getWeb3()
|
||||
const txRecipient = tx.contractAddress
|
||||
const txData = tx.data ? tx.data.trim() : ''
|
||||
const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : '0'
|
||||
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: txRecipient,
|
||||
valueInWei: txValue,
|
||||
txData,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
} as any),
|
||||
)
|
||||
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Send Custom Tx
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.contractAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph noMargin weight="bolder">
|
||||
{tx.contractAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={tx.contractAddress} />
|
||||
<EtherscanBtn type="address" value={tx.contractAddress} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Value
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Img alt="Ether" height={28} onError={setImageToPlaceholder} src={getEthAsToken('0').logoUri} />
|
||||
<Paragraph className={classes.value} noMargin size="md">
|
||||
{tx.value || 0}
|
||||
{' ETH'}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Data (hex encoded)
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col className={classes.outerData}>
|
||||
<Row className={classes.data} size="md">
|
||||
{tx.data}
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
minWidth={140}
|
||||
onClick={submitTx}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReviewCustomTx
|
|
@ -0,0 +1,58 @@
|
|||
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',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,278 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React, { useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import ArrowDown from '../../assets/arrow-down.svg'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import QRIcon from 'src/assets/icons/qrcode.svg'
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import ScanQRModal from 'src/components/ScanQRModal'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import TextareaField from 'src/components/forms/TextareaField'
|
||||
import { composeValidators, maxValue, mustBeFloat, greaterThan } from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||
import Col from 'src/components/layout/Col'
|
||||
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 SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||
import { sm } from 'src/theme/variables'
|
||||
|
||||
export interface CreatedTx {
|
||||
contractAddress: string
|
||||
data: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
initialValues: { contractAddress?: string }
|
||||
onClose: () => void
|
||||
onNext: (tx: CreatedTx, submit: boolean) => void
|
||||
isABI: boolean
|
||||
switchMethod: () => void
|
||||
contractAddress: string
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contractAddress, switchMethod, isABI }) => {
|
||||
const classes = useStyles()
|
||||
const { ethBalance } = useSelector(safeSelector)
|
||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({
|
||||
address: contractAddress || initialValues.contractAddress,
|
||||
name: '',
|
||||
})
|
||||
const [isValidAddress, setIsValidAddress] = useState<boolean>(true)
|
||||
|
||||
const saveForm = async (values) => {
|
||||
await handleSubmit(values, false)
|
||||
switchMethod()
|
||||
}
|
||||
|
||||
const handleSubmit = (values: any, submit = true) => {
|
||||
if (values.data || values.value) {
|
||||
onNext(values, submit)
|
||||
}
|
||||
}
|
||||
|
||||
const openQrModal = () => {
|
||||
setQrModalOpen(true)
|
||||
}
|
||||
|
||||
const closeQrModal = () => {
|
||||
setQrModalOpen(false)
|
||||
}
|
||||
|
||||
const formMutators = {
|
||||
setMax: (args, state, utils) => {
|
||||
utils.changeValue(state, 'value', () => ethBalance)
|
||||
},
|
||||
setRecipient: (args, state, utils) => {
|
||||
utils.changeValue(state, 'contractAddress', () => args[0])
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Send custom transactions
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>1 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<GnoForm
|
||||
formMutators={formMutators}
|
||||
initialValues={initialValues}
|
||||
subscription={{ submitting: true, pristine: true, values: true }}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{(...args) => {
|
||||
const mutators = args[3]
|
||||
const pristine = args[2].pristine
|
||||
let shouldDisableSubmitButton = !isValidAddress
|
||||
if (selectedEntry) {
|
||||
shouldDisableSubmitButton = !selectedEntry.address
|
||||
}
|
||||
|
||||
const handleScan = (value) => {
|
||||
let scannedAddress = value
|
||||
|
||||
if (scannedAddress.startsWith('ethereum:')) {
|
||||
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||
}
|
||||
|
||||
mutators.setRecipient(scannedAddress)
|
||||
closeQrModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.formContainer}>
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
{selectedEntry && selectedEntry.address ? (
|
||||
<div
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode !== 9) {
|
||||
setSelectedEntry(null)
|
||||
}
|
||||
}}
|
||||
role="listbox"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={selectedEntry.address} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Block>
|
||||
<Paragraph
|
||||
className={classes.selectAddress}
|
||||
noMargin
|
||||
onClick={() => setSelectedEntry(null)}
|
||||
weight="bolder"
|
||||
>
|
||||
{selectedEntry.name}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
className={classes.selectAddress}
|
||||
noMargin
|
||||
onClick={() => setSelectedEntry(null)}
|
||||
weight="bolder"
|
||||
>
|
||||
{selectedEntry.address}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<CopyBtn content={selectedEntry.address} />
|
||||
<EtherscanBtn type="address" value={selectedEntry.address} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<AddressBookInput
|
||||
fieldMutator={mutators.setRecipient}
|
||||
isCustomTx
|
||||
pristine={pristine}
|
||||
setIsValidAddress={setIsValidAddress}
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
/>
|
||||
</Col>
|
||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||
<Img
|
||||
alt="Scan QR"
|
||||
className={classes.qrCodeBtn}
|
||||
height={20}
|
||||
onClick={() => {
|
||||
openQrModal()
|
||||
}}
|
||||
role="button"
|
||||
src={QRIcon}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
<Row margin="xs">
|
||||
<Col between="lg">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Value
|
||||
</Paragraph>
|
||||
<ButtonLink onClick={mutators.setMax} weight="bold">
|
||||
Send max
|
||||
</ButtonLink>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="md">
|
||||
<Col>
|
||||
<Field
|
||||
component={TextField}
|
||||
inputAdornment={{
|
||||
endAdornment: <InputAdornment position="end">ETH</InputAdornment>,
|
||||
}}
|
||||
name="value"
|
||||
placeholder="Value*"
|
||||
text="Value*"
|
||||
type="text"
|
||||
validate={composeValidators(mustBeFloat, maxValue(ethBalance), greaterThan(0))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="sm">
|
||||
<Col>
|
||||
<TextareaField
|
||||
name="data"
|
||||
placeholder="Data (hex encoded)*"
|
||||
text="Data (hex encoded)*"
|
||||
type="text"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Use custom data (hex encoded)
|
||||
<Switch onChange={() => saveForm(args[2].values)} checked={!isABI} />
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="review-tx-btn"
|
||||
disabled={shouldDisableSubmitButton}
|
||||
minWidth={140}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
</Row>
|
||||
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</GnoForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendCustomTx
|
|
@ -0,0 +1,51 @@
|
|||
import { lg, md } 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: '#a2a8ba',
|
||||
marginRight: 'auto',
|
||||
marginLeft: '20px',
|
||||
},
|
||||
manage: {
|
||||
fontSize: lg,
|
||||
},
|
||||
closeIcon: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
qrCodeBtn: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
formContainer: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
'& > button': {
|
||||
fontFamily: 'Averta',
|
||||
fontSize: md,
|
||||
},
|
||||
},
|
||||
submitButton: {
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
marginLeft: '15px',
|
||||
},
|
||||
dataInput: {
|
||||
'& TextField-root-294': {
|
||||
lineHeight: 'auto',
|
||||
border: 'green',
|
||||
},
|
||||
},
|
||||
selectAddress: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
})
|
|
@ -1,13 +1,14 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
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 { safeSelector } from 'src/routes/safe/store/selectors'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Buttons from './Buttons'
|
||||
import ContractABI from './ContractABI'
|
||||
import EthAddressInput from './EthAddressInput'
|
||||
|
@ -33,11 +34,20 @@ export interface CreatedTx {
|
|||
export interface ContractInteractionProps {
|
||||
contractAddress: string
|
||||
initialValues: { contractAddress?: string }
|
||||
isABI: boolean
|
||||
onClose: () => void
|
||||
onNext: (tx: CreatedTx) => void
|
||||
switchMethod: () => void
|
||||
onNext: (tx: CreatedTx, submit: boolean) => void
|
||||
}
|
||||
|
||||
const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }: ContractInteractionProps) => {
|
||||
const ContractInteraction: React.FC<ContractInteractionProps> = ({
|
||||
contractAddress,
|
||||
initialValues,
|
||||
onClose,
|
||||
onNext,
|
||||
switchMethod,
|
||||
isABI,
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const { address: safeAddress = '' } = useSelector(safeSelector)
|
||||
let setCallResults
|
||||
|
@ -48,13 +58,21 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
|||
}
|
||||
}, [contractAddress, initialValues.contractAddress])
|
||||
|
||||
const handleSubmit = async ({ contractAddress, selectedMethod, value, ...values }) => {
|
||||
const saveForm = async (values: CreatedTx): Promise<void> => {
|
||||
await handleSubmit(values, false)
|
||||
switchMethod()
|
||||
}
|
||||
|
||||
const handleSubmit = async (
|
||||
{ contractAddress, selectedMethod, value, ...values },
|
||||
submit = true,
|
||||
): Promise<void | any> => {
|
||||
if (value || (contractAddress && selectedMethod)) {
|
||||
try {
|
||||
const txObject = createTxObject(selectedMethod, contractAddress, values)
|
||||
const data = txObject.encodeABI()
|
||||
|
||||
if (isReadMethod(selectedMethod)) {
|
||||
if (isReadMethod(selectedMethod) && submit) {
|
||||
const result = await txObject.call({ from: safeAddress })
|
||||
setCallResults(result)
|
||||
|
||||
|
@ -62,7 +80,7 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
|||
return
|
||||
}
|
||||
|
||||
onNext({ ...values, contractAddress, data, selectedMethod, value })
|
||||
onNext({ ...values, contractAddress, data, selectedMethod, value }, submit)
|
||||
} catch (error) {
|
||||
return handleSubmitError(error, values)
|
||||
}
|
||||
|
@ -78,7 +96,7 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
|||
formMutators={formMutators}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
subscription={{ submitting: true, pristine: true }}
|
||||
subscription={{ submitting: true, pristine: true, values: true }}
|
||||
>
|
||||
{(submitting, validating, rest, mutators) => {
|
||||
setCallResults = mutators.setCallResults
|
||||
|
@ -99,6 +117,10 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
|||
<RenderInputParams />
|
||||
<RenderOutputParams />
|
||||
<FormErrorMessage />
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Use custom data (hex encoded)
|
||||
<Switch checked={!isABI} onChange={() => saveForm(rest.values)} />
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Buttons onClose={onClose} />
|
||||
|
|
Loading…
Reference in New Issue