Add support for customTX

This commit is contained in:
Mati Dastugue 2020-06-18 16:17:47 -03:00
parent 6d1a349d87
commit 84fae2e845
7 changed files with 644 additions and 837 deletions

View File

@ -19,6 +19,10 @@ const ContractInteraction = React.lazy(() => import('./screens/ContractInteracti
const ContractInteractionReview: any = React.lazy(() => import('./screens/ContractInteraction/Review')) 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({ const useStyles = makeStyles({
scalableModalWindow: { scalableModalWindow: {
height: 'auto', height: 'auto',
@ -40,9 +44,11 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
const classes = useStyles() const classes = useStyles()
const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType') const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType')
const [tx, setTx] = useState({}) const [tx, setTx] = useState({})
const [isABI, setIsABI] = useState(true)
useEffect(() => { useEffect(() => {
setActiveScreen(activeScreenType || 'chooseTxType') setActiveScreen(activeScreenType || 'chooseTxType')
setIsABI(true)
setTx({}) setTx({})
}, [activeScreenType, isOpen]) }, [activeScreenType, isOpen])
@ -58,11 +64,20 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
setActiveScreen('contractInteractionReview') setActiveScreen('contractInteractionReview')
} }
const handleCustomTxCreation = (customTxInfo) => {
setActiveScreen('reviewCustomTx')
setTx(customTxInfo)
}
const handleSendCollectible = (txInfo) => { const handleSendCollectible = (txInfo) => {
setActiveScreen('reviewCollectible') setActiveScreen('reviewCollectible')
setTx(txInfo) setTx(txInfo)
} }
const handleSwitchMethod = () => {
setIsABI(!isABI)
}
return ( return (
<Modal <Modal
description="Send Tokens Form" description="Send Tokens Form"
@ -93,17 +108,32 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
{activeScreen === 'reviewTx' && ( {activeScreen === 'reviewTx' && (
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx} /> <ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx} />
)} )}
{activeScreen === 'contractInteraction' && ( {activeScreen === 'contractInteraction' && isABI && (
<ContractInteraction <ContractInteraction
isABI={isABI}
switchMethod={handleSwitchMethod}
contractAddress={recipientAddress} contractAddress={recipientAddress}
initialValues={tx} initialValues={tx}
onClose={onClose} onClose={onClose}
onNext={handleContractInteractionCreation} onNext={handleContractInteractionCreation}
/> />
)} )}
{activeScreen === 'contractInteractionReview' && tx && ( {activeScreen === 'contractInteractionReview' && isABI && tx && (
<ContractInteractionReview onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} /> <ContractInteractionReview onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
)} )}
{activeScreen === 'contractInteraction' && !isABI && (
<SendCustomTx
initialValues={tx}
isABI={isABI}
switchMethod={handleSwitchMethod}
onClose={onClose}
onNext={handleCustomTxCreation}
recipientAddress={recipientAddress}
/>
)}
{activeScreen === 'reviewCustomTx' && (
<ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
)}
{activeScreen === 'sendCollectible' && ( {activeScreen === 'sendCollectible' && (
<SendCollectible <SendCollectible
initialValues={tx} initialValues={tx}

View File

@ -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: { recipientAddress?: 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.recipientAddress, txData)
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
const formattedGasCosts = formatAmount(gasCostsAsEth)
if (isCurrent) {
setGasCosts(formattedGasCosts)
}
}
estimateGas()
return () => {
isCurrent = false
}
}, [safeAddress, tx.data, tx.recipientAddress])
const submitTx = async () => {
const web3 = getWeb3()
const txRecipient = tx.recipientAddress
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.recipientAddress} diameter={32} />
</Col>
<Col layout="column" xs={11}>
<Block justify="left">
<Paragraph noMargin weight="bolder">
{tx.recipientAddress}
</Paragraph>
<CopyBtn content={tx.recipientAddress} />
<EtherscanBtn type="address" value={tx.recipientAddress} />
</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

View File

@ -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',
},
})

View File

@ -0,0 +1,276 @@
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 } 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'
type Props = {
initialValues: { contractAddress?: string; recipientAddress?: string }
onClose: () => void
onNext: (any) => void
isABI: boolean
switchMethod: () => void
recipientAddress: string
}
const useStyles = makeStyles(styles)
const SendCustomTx: React.FC<Props> = ({
initialValues,
onClose,
onNext,
recipientAddress,
switchMethod,
isABI,
}: Props) => {
const classes = useStyles()
const { ethBalance } = useSelector(safeSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
})
const [pristine, setPristine] = useState<boolean>(true)
const [isValidAddress, setIsValidAddress] = useState<boolean>(true)
React.useMemo(() => {
if (selectedEntry === null && pristine) {
setPristine(false)
}
}, [selectedEntry, pristine])
const handleSubmit = (values: any) => {
if (values.data || values.value) {
onNext(values)
}
}
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, 'recipientAddress', () => 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} onSubmit={handleSubmit}>
{(...args) => {
const mutators = args[3]
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))}
/>
</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 checked={!isABI} onChange={switchMethod} />
</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

View File

@ -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',
},
})

View File

@ -1,13 +1,14 @@
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import React from 'react' import React from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import Switch from '@material-ui/core/Switch'
import { styles } from './style' import { styles } from './style'
import GnoForm from 'src/components/forms/GnoForm' import GnoForm from 'src/components/forms/GnoForm'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Hairline from 'src/components/layout/Hairline' import Hairline from 'src/components/layout/Hairline'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import { safeSelector } from 'src/routes/safe/store/selectors' import { safeSelector } from 'src/routes/safe/store/selectors'
import Paragraph from 'src/components/layout/Paragraph'
import Buttons from './Buttons' import Buttons from './Buttons'
import ContractABI from './ContractABI' import ContractABI from './ContractABI'
import EthAddressInput from './EthAddressInput' import EthAddressInput from './EthAddressInput'
@ -33,11 +34,20 @@ export interface CreatedTx {
export interface ContractInteractionProps { export interface ContractInteractionProps {
contractAddress: string contractAddress: string
initialValues: { contractAddress?: string } initialValues: { contractAddress?: string }
isABI: boolean
onClose: () => void onClose: () => void
switchMethod: () => void
onNext: (tx: CreatedTx) => void onNext: (tx: CreatedTx) => void
} }
const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }: ContractInteractionProps) => { const ContractInteraction: React.FC<ContractInteractionProps> = ({
contractAddress,
initialValues,
onClose,
onNext,
switchMethod,
isABI,
}) => {
const classes = useStyles() const classes = useStyles()
const { address: safeAddress = '' } = useSelector(safeSelector) const { address: safeAddress = '' } = useSelector(safeSelector)
let setCallResults let setCallResults
@ -99,6 +109,10 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
<RenderInputParams /> <RenderInputParams />
<RenderOutputParams /> <RenderOutputParams />
<FormErrorMessage /> <FormErrorMessage />
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
Use custom data (hex encoded)
<Switch checked={!isABI} onChange={switchMethod} />
</Paragraph>
</Block> </Block>
<Hairline /> <Hairline />
<Buttons onClose={onClose} /> <Buttons onClose={onClose} />

860
yarn.lock

File diff suppressed because it is too large Load Diff