* (Fix) Wrong value for ERC-20 tokens transfers (#679)

The fix attempts to properly differentiate an ERC-721 from an ERC-20 token transaction by identifying if it's a `transfer` transaction looking for a `decimals` method in its code. It the later is not found, then it's considered an ERC-721.

fixes #678

* (Fix) send tx from address book (#677)

* fix: Send funds not working when selecting receipt from addressBook

Also, this commit includes an intent to unify/simplify SendModal component

fixes #632

* Set default value to txData for custom txs

fixes #632

* bump version in package.json (#683)

Co-authored-by: Fernando <fernando.greco@gmail.com>
This commit is contained in:
Mikhail Mikheev 2020-03-23 16:02:03 +04:00 committed by GitHub
parent 7fae339f42
commit 39bcf37cd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 136 additions and 228 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "safe-react", "name": "safe-react",
"version": "1.8.1", "version": "1.8.2",
"description": "Allowing crypto users manage funds in a safer way", "description": "Allowing crypto users manage funds in a safer way",
"homepage": "https://github.com/gnosis/safe-react#readme", "homepage": "https://github.com/gnosis/safe-react#readme",
"bugs": { "bugs": {

View File

@ -6,6 +6,9 @@ import { type Token, makeToken } from '~/logic/tokens/store/model/token'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
export const ETH_ADDRESS = '0x000' export const ETH_ADDRESS = '0x000'
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '0x42842e0e'
export const DECIMALS_METHOD_HASH = '313ce567'
export const isEther = (symbol: string) => symbol === 'ETH' export const isEther = (symbol: string) => symbol === 'ETH'
export const getEthAsToken = (balance: string) => { export const getEthAsToken = (balance: string) => {

View File

@ -42,8 +42,7 @@ import RenameOwnerIcon from '~/routes/safe/components/Settings/ManageOwners/asse
import type { OwnerRow } from '~/routes/safe/components/Settings/ManageOwners/dataFetcher' import type { OwnerRow } from '~/routes/safe/components/Settings/ManageOwners/dataFetcher'
import RemoveOwnerIcon from '~/routes/safe/components/Settings/assets/icons/bin.svg' import RemoveOwnerIcon from '~/routes/safe/components/Settings/assets/icons/bin.svg'
import RemoveOwnerIconDisabled from '~/routes/safe/components/Settings/assets/icons/disabled-bin.svg' import RemoveOwnerIconDisabled from '~/routes/safe/components/Settings/assets/icons/disabled-bin.svg'
import { extendedSafeTokensSelector } from '~/routes/safe/container/selector' import { addressBookQueryParamsSelector, safesListSelector } from '~/routes/safe/store/selectors'
import { addressBookQueryParamsSelector, safeSelector, safesListSelector } from '~/routes/safe/store/selectors'
type Props = { type Props = {
classes: Object, classes: Object,
@ -86,10 +85,7 @@ const AddressBookTable = ({ classes }: Props) => {
} }
}, [addressBook]) }, [addressBook])
const safe = useSelector(safeSelector)
const safesList = useSelector(safesListSelector) const safesList = useSelector(safesListSelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const { address, ethBalance, name } = safe
const newEntryModalHandler = (entry: AddressBookEntry) => { const newEntryModalHandler = (entry: AddressBookEntry) => {
setEditCreateEntryModalOpen(false) setEditCreateEntryModalOpen(false)
@ -223,13 +219,9 @@ const AddressBookTable = ({ classes }: Props) => {
/> />
<SendModal <SendModal
activeScreenType="chooseTxType" activeScreenType="chooseTxType"
ethBalance={ethBalance}
isOpen={sendFundsModalOpen} isOpen={sendFundsModalOpen}
onClose={() => setSendFundsModalOpen(false)} onClose={() => setSendFundsModalOpen(false)}
recipientAddress={selectedEntry && selectedEntry.entry ? selectedEntry.entry.address : undefined} recipientAddress={selectedEntry && selectedEntry.entry ? selectedEntry.entry.address : undefined}
safeAddress={address}
safeName={name}
tokens={activeTokens}
/> />
</> </>
) )

View File

@ -10,7 +10,6 @@ import Paragraph from '~/components/layout/Paragraph'
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles' import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
import { nftAssetsSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors' import { nftAssetsSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
import SendModal from '~/routes/safe/components/Balances/SendModal' import SendModal from '~/routes/safe/components/Balances/SendModal'
import { safeSelector } from '~/routes/safe/store/selectors'
import { fontColor, lg, screenSm, screenXs } from '~/theme/variables' import { fontColor, lg, screenSm, screenXs } from '~/theme/variables'
const useStyles = makeStyles({ const useStyles = makeStyles({
@ -80,7 +79,6 @@ const Collectibles = () => {
const classes = useStyles() const classes = useStyles()
const [selectedToken, setSelectedToken] = React.useState({}) const [selectedToken, setSelectedToken] = React.useState({})
const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false) const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false)
const { address, ethBalance, name } = useSelector(safeSelector)
const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector) const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector)
const nftTokens: NFTTokensState = useSelector(nftTokensSelector) const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
const nftAssetsKeys = Object.keys(nftAssets) const nftAssetsKeys = Object.keys(nftAssets)
@ -124,11 +122,8 @@ const Collectibles = () => {
</div> </div>
<SendModal <SendModal
activeScreenType="sendCollectible" activeScreenType="sendCollectible"
ethBalance={ethBalance}
isOpen={sendNFTsModalOpen} isOpen={sendNFTsModalOpen}
onClose={() => setSendNFTsModalOpen(false)} onClose={() => setSendNFTsModalOpen(false)}
safeAddress={address}
safeName={name}
selectedToken={selectedToken} selectedToken={selectedToken}
/> />
</Card> </Card>

View File

@ -2,7 +2,6 @@
import CircularProgress from '@material-ui/core/CircularProgress' import CircularProgress from '@material-ui/core/CircularProgress'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import cn from 'classnames' import cn from 'classnames'
import { List } from 'immutable'
import React, { Suspense, useEffect, useState } from 'react' import React, { Suspense, useEffect, useState } from 'react'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
@ -33,16 +32,11 @@ type ActiveScreen =
| 'reviewCollectible' | 'reviewCollectible'
type Props = { type Props = {
onClose: () => void,
isOpen: boolean,
safeAddress: string,
safeName: string,
ethBalance: string,
tokens: List<Token>,
selectedToken?: string | NFTToken | {},
createTransaction?: Function,
activeScreenType: ActiveScreen, activeScreenType: ActiveScreen,
isOpen: boolean,
onClose: () => void,
recipientAddress?: string, recipientAddress?: string,
selectedToken?: string | NFTToken | {},
} }
type TxStateType = type TxStateType =
@ -71,18 +65,7 @@ const useStyles = makeStyles({
}, },
}) })
const SendModal = ({ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, selectedToken }: Props) => {
activeScreenType,
createTransaction,
ethBalance,
isOpen,
onClose,
recipientAddress,
safeAddress,
safeName,
selectedToken,
tokens,
}: Props) => {
const classes = useStyles() const classes = useStyles()
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(activeScreenType || 'chooseTxType') const [activeScreen, setActiveScreen] = useState<ActiveScreen>(activeScreenType || 'chooseTxType')
const [tx, setTx] = useState<TxStateType>({}) const [tx, setTx] = useState<TxStateType>({})
@ -129,50 +112,26 @@ const SendModal = ({
)} )}
{activeScreen === 'sendFunds' && ( {activeScreen === 'sendFunds' && (
<SendFunds <SendFunds
ethBalance={ethBalance}
initialValues={tx} initialValues={tx}
onClose={onClose} onClose={onClose}
onSubmit={handleTxCreation} onNext={handleTxCreation}
recipientAddress={recipientAddress} recipientAddress={recipientAddress}
safeAddress={safeAddress}
safeName={safeName}
selectedToken={selectedToken} selectedToken={selectedToken}
tokens={tokens}
/> />
)} )}
{activeScreen === 'reviewTx' && ( {activeScreen === 'reviewTx' && (
<ReviewTx <ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx} />
createTransaction={createTransaction}
ethBalance={ethBalance}
onClose={onClose}
safeAddress={safeAddress}
safeName={safeName}
setActiveScreen={setActiveScreen}
tokens={tokens}
tx={tx}
/>
)} )}
{activeScreen === 'sendCustomTx' && ( {activeScreen === 'sendCustomTx' && (
<SendCustomTx <SendCustomTx
ethBalance={ethBalance}
initialValues={tx} initialValues={tx}
onClose={onClose} onClose={onClose}
onSubmit={handleCustomTxCreation} onNext={handleCustomTxCreation}
recipientAddress={recipientAddress} recipientAddress={recipientAddress}
safeAddress={safeAddress}
safeName={safeName}
/> />
)} )}
{activeScreen === 'reviewCustomTx' && ( {activeScreen === 'reviewCustomTx' && (
<ReviewCustomTx <ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('sendCustomTx')} tx={tx} />
createTransaction={createTransaction}
ethBalance={ethBalance}
onClose={onClose}
safeAddress={safeAddress}
safeName={safeName}
setActiveScreen={setActiveScreen}
tx={tx}
/>
)} )}
{activeScreen === 'sendCollectible' && ( {activeScreen === 'sendCollectible' && (
<SendCollectible <SendCollectible

View File

@ -2,7 +2,6 @@
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import { List } from 'immutable'
import { withSnackbar } from 'notistack' import { withSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
@ -30,8 +29,8 @@ import {
getERC721TokenContract, getERC721TokenContract,
getHumanFriendlyToken, getHumanFriendlyToken,
} from '~/logic/tokens/store/actions/fetchTokens' } from '~/logic/tokens/store/actions/fetchTokens'
import { type Token } from '~/logic/tokens/store/model/token'
import { formatAmount } from '~/logic/tokens/utils/formatAmount' import { formatAmount } from '~/logic/tokens/utils/formatAmount'
import { SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH } from '~/logic/tokens/utils/tokenHelpers'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils' import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
@ -42,17 +41,12 @@ import { textShortener } from '~/utils/strings'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '0x42842e0e'
type Props = { type Props = {
closeSnackbar: Function,
enqueueSnackbar: Function,
onClose: () => void, onClose: () => void,
onPrev: () => void, onPrev: () => void,
classes: Object,
tx: Object, tx: Object,
tokens: List<Token>,
createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props) => { const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props) => {

View File

@ -1,9 +1,10 @@
// @flow // @flow
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import { withSnackbar } from 'notistack' import { withSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from '../assets/arrow-down.svg'
@ -26,44 +27,37 @@ import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils' import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import createTransaction from '~/routes/safe/store/actions/createTransaction'
import { safeSelector } from '~/routes/safe/store/selectors'
import { sm } from '~/theme/variables' import { sm } from '~/theme/variables'
type Props = { type Props = {
closeSnackbar: () => void,
enqueueSnackbar: () => void,
onClose: () => void, onClose: () => void,
setActiveScreen: Function, onPrev: () => void,
classes: Object,
safeAddress: string,
safeName: string,
ethBalance: string,
tx: Object, tx: Object,
createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
const ReviewCustomTx = ({ const useStyles = makeStyles(styles)
classes,
closeSnackbar, const ReviewCustomTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props) => {
createTransaction, const classes = useStyles()
enqueueSnackbar, const dispatch = useDispatch()
ethBalance, const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
onClose,
safeAddress,
safeName,
setActiveScreen,
tx,
}: Props) => {
const [gasCosts, setGasCosts] = useState<string>('< 0.001') const [gasCosts, setGasCosts] = useState<string>('< 0.001')
useEffect(() => { useEffect(() => {
let isCurrent = true let isCurrent = true
const estimateGas = async () => {
const web3 = getWeb3()
const { fromWei, toBN } = web3.utils
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.recipientAddress, tx.data.trim()) 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 gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
const formattedGasCosts = formatAmount(gasCostsAsEth) const formattedGasCosts = formatAmount(gasCostsAsEth)
if (isCurrent) { if (isCurrent) {
setGasCosts(formattedGasCosts) setGasCosts(formattedGasCosts)
} }
@ -79,18 +73,21 @@ const ReviewCustomTx = ({
const submitTx = async () => { const submitTx = async () => {
const web3 = getWeb3() const web3 = getWeb3()
const txRecipient = tx.recipientAddress const txRecipient = tx.recipientAddress
const txData = tx.data.trim() const txData = tx.data ? tx.data.trim() : ''
const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0 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,
}),
)
createTransaction({
safeAddress,
to: txRecipient,
valueInWei: txValue,
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar,
closeSnackbar,
})
onClose() onClose()
} }
@ -167,7 +164,7 @@ const ReviewCustomTx = ({
</Block> </Block>
<Hairline /> <Hairline />
<Row align="center" className={classes.buttonRow}> <Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={() => setActiveScreen('sendCustomTx')}> <Button minWidth={140} onClick={onPrev}>
Back Back
</Button> </Button>
<Button <Button
@ -186,4 +183,4 @@ const ReviewCustomTx = ({
) )
} }
export default withStyles(styles)(withSnackbar(ReviewCustomTx)) export default withSnackbar(ReviewCustomTx)

View File

@ -1,11 +1,11 @@
// @flow // @flow
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { List } from 'immutable'
import { withSnackbar } from 'notistack' import { withSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from '../assets/arrow-down.svg'
@ -24,65 +24,63 @@ import Row from '~/components/layout/Row'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew' import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew'
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens' import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
import { type Token } from '~/logic/tokens/store/model/token'
import { formatAmount } from '~/logic/tokens/utils/formatAmount' import { formatAmount } from '~/logic/tokens/utils/formatAmount'
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers' import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils' import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import { extendedSafeTokensSelector } from '~/routes/safe/container/selector'
import createTransaction from '~/routes/safe/store/actions/createTransaction'
import { safeSelector } from '~/routes/safe/store/selectors'
import { sm } from '~/theme/variables' import { sm } from '~/theme/variables'
type Props = { type Props = {
onClose: () => void,
setActiveScreen: Function,
classes: Object,
safeAddress: string,
safeName: string,
ethBalance: string,
tx: Object,
tokens: List<Token>,
createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function, closeSnackbar: Function,
enqueueSnackbar: Function,
onClose: () => void,
onPrev: () => void,
tx: Object,
} }
const ReviewTx = ({ const useStyles = makeStyles(styles)
classes,
closeSnackbar, const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props) => {
createTransaction, const classes = useStyles()
enqueueSnackbar, const dispatch = useDispatch()
ethBalance, const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
onClose, const tokens = useSelector(extendedSafeTokensSelector)
safeAddress,
safeName,
setActiveScreen,
tokens,
tx,
}: Props) => {
const [gasCosts, setGasCosts] = useState<string>('< 0.001') const [gasCosts, setGasCosts] = useState<string>('< 0.001')
const [data, setData] = useState('')
const txToken = tokens.find(token => token.address === tx.token) const txToken = tokens.find(token => token.address === tx.token)
const isSendingETH = txToken.address === ETH_ADDRESS const isSendingETH = txToken.address === ETH_ADDRESS
const txRecipient = isSendingETH ? tx.recipientAddress : txToken.address const txRecipient = isSendingETH ? tx.recipientAddress : txToken.address
useEffect(() => { useEffect(() => {
let isCurrent = true let isCurrent = true
const estimateGas = async () => { const estimateGas = async () => {
const web3 = getWeb3() const { fromWei, toBN } = getWeb3().utils
const { fromWei, toBN } = web3.utils
let txData = EMPTY_DATA let txData = EMPTY_DATA
if (!isSendingETH) { if (!isSendingETH) {
const StandardToken = await getHumanFriendlyToken() const StandardToken = await getHumanFriendlyToken()
const tokenInstance = await StandardToken.at(txToken.address) const tokenInstance = await StandardToken.at(txToken.address)
const decimals = await tokenInstance.decimals()
const txAmount = new BigNumber(tx.amount).times(10 ** decimals.toNumber()).toString()
txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, 0).encodeABI() txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI()
} }
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, txRecipient, txData) const estimatedGasCosts = await estimateTxGasCosts(safeAddress, txRecipient, txData)
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
const formattedGasCosts = formatAmount(gasCostsAsEth) const formattedGasCosts = formatAmount(gasCostsAsEth)
if (isCurrent) { if (isCurrent) {
setGasCosts(formattedGasCosts) setGasCosts(formattedGasCosts)
setData(txData)
} }
} }
@ -95,30 +93,22 @@ const ReviewTx = ({
const submitTx = async () => { const submitTx = async () => {
const web3 = getWeb3() const web3 = getWeb3()
let txData = EMPTY_DATA // txAmount should be 0 if we send tokens
let txAmount = web3.utils.toWei(tx.amount, 'ether') // the real value is encoded in txData and will be used by the contract
if (!isSendingETH) { // if txAmount > 0 it would send ETH from the Safe
const HumanFriendlyToken = await getHumanFriendlyToken() const txAmount = isSendingETH ? web3.utils.toWei(tx.amount, 'ether') : '0'
const tokenInstance = await HumanFriendlyToken.at(txToken.address)
const decimals = await tokenInstance.decimals()
txAmount = new BigNumber(tx.amount).times(10 ** decimals.toNumber()).toString()
txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() dispatch(
// txAmount should be 0 if we send tokens createTransaction({
// the real value is encoded in txData and will be used by the contract safeAddress,
// if txAmount > 0 it would send ETH from the Safe to: txRecipient,
txAmount = 0 valueInWei: txAmount,
} txData: data,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
createTransaction({ enqueueSnackbar,
safeAddress, closeSnackbar,
to: txRecipient, }),
valueInWei: txAmount, )
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar,
closeSnackbar,
})
onClose() onClose()
} }
@ -182,13 +172,14 @@ const ReviewTx = ({
</Block> </Block>
<Hairline style={{ position: 'absolute', bottom: 85 }} /> <Hairline style={{ position: 'absolute', bottom: 85 }} />
<Row align="center" className={classes.buttonRow}> <Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={() => setActiveScreen('sendFunds')}> <Button minWidth={140} onClick={onPrev}>
Back Back
</Button> </Button>
<Button <Button
className={classes.submitButton} className={classes.submitButton}
color="primary" color="primary"
data-testid="submit-tx-btn" data-testid="submit-tx-btn"
disabled={!data}
minWidth={140} minWidth={140}
onClick={submitTx} onClick={submitTx}
type="submit" type="submit"
@ -201,4 +192,4 @@ const ReviewTx = ({
) )
} }
export default withStyles(styles)(withSnackbar(ReviewTx)) export default withSnackbar(ReviewTx)

View File

@ -1,9 +1,10 @@
// @flow // @flow
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import InputAdornment from '@material-ui/core/InputAdornment' import InputAdornment from '@material-ui/core/InputAdornment'
import { withStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from '../assets/arrow-down.svg'
@ -29,29 +30,21 @@ import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import { safeSelector } from '~/routes/safe/store/selectors'
import { sm } from '~/theme/variables' import { sm } from '~/theme/variables'
type Props = { type Props = {
onClose: () => void,
classes: Object,
recipientAddress: string,
safeAddress: string,
safeName: string,
ethBalance: string,
onSubmit: Function,
initialValues: Object, initialValues: Object,
onClose: () => void,
onNext: any => void,
recipientAddress: string,
} }
const SendCustomTx = ({ const useStyles = makeStyles(styles)
classes,
ethBalance, const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Props) => {
initialValues, const classes = useStyles()
onClose, const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
onSubmit,
recipientAddress,
safeAddress,
safeName,
}: Props) => {
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false) const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({ const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress, address: recipientAddress || initialValues.recipientAddress,
@ -68,7 +61,7 @@ const SendCustomTx = ({
const handleSubmit = (values: Object) => { const handleSubmit = (values: Object) => {
if (values.data || values.value) { if (values.data || values.value) {
onSubmit(values) onNext(values)
} }
} }
@ -268,4 +261,4 @@ const SendCustomTx = ({
) )
} }
export default withStyles(styles)(SendCustomTx) export default SendCustomTx

View File

@ -1,11 +1,11 @@
// @flow // @flow
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import InputAdornment from '@material-ui/core/InputAdornment' import InputAdornment from '@material-ui/core/InputAdornment'
import { withStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import { List } from 'immutable'
import React, { useState } from 'react' import React, { useState } from 'react'
import { OnChange } from 'react-final-form-listeners' import { OnChange } from 'react-final-form-listeners'
import { useSelector } from 'react-redux'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from '../assets/arrow-down.svg'
@ -32,19 +32,16 @@ import { type Token } from '~/logic/tokens/store/model/token'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import TokenSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' import TokenSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField'
import { extendedSafeTokensSelector } from '~/routes/safe/container/selector'
import { safeSelector } from '~/routes/safe/store/selectors'
import { sm } from '~/theme/variables' import { sm } from '~/theme/variables'
type Props = { type Props = {
onClose: () => void,
classes: Object,
safeAddress: string,
safeName: string,
ethBalance: string,
selectedToken: string,
tokens: List<Token>,
onSubmit: Function,
initialValues: Object, initialValues: Object,
onClose: () => void,
onNext: any => void,
recipientAddress?: string, recipientAddress?: string,
selectedToken: string,
} }
const formMutators = { const formMutators = {
@ -59,21 +56,15 @@ const formMutators = {
}, },
} }
const SendFunds = ({ const useStyles = makeStyles(styles)
classes,
ethBalance, const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = '' }: Props) => {
initialValues, const classes = useStyles()
onClose, const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
onSubmit, const tokens: Token = useSelector(extendedSafeTokensSelector)
recipientAddress,
safeAddress,
safeName,
selectedToken,
tokens,
}: Props) => {
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false) const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({ const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress, address: recipientAddress || initialValues.recipientAddress,
name: '', name: '',
}) })
const [pristine, setPristine] = useState<boolean>(true) const [pristine, setPristine] = useState<boolean>(true)
@ -91,7 +82,7 @@ const SendFunds = ({
if (!values.recipientAddress) { if (!values.recipientAddress) {
submitValues.recipientAddress = selectedEntry.address submitValues.recipientAddress = selectedEntry.address
} }
onSubmit(submitValues) onNext(submitValues)
} }
const openQrModal = () => { const openQrModal = () => {
@ -294,4 +285,4 @@ const SendFunds = ({
) )
} }
export default withStyles(styles)(SendFunds) export default SendFunds

View File

@ -51,10 +51,10 @@ type Props = {
activeTokens: List<Token>, activeTokens: List<Token>,
blacklistedTokens: List<Token>, blacklistedTokens: List<Token>,
classes: Object, classes: Object,
createTransaction: Function, createTransaction?: Function,
currencySelected: string, currencySelected: string,
currencyValues: BalanceCurrencyType[], currencyValues: BalanceCurrencyType[],
ethBalance: string, ethBalance?: string,
featuresEnabled: string[], featuresEnabled: string[],
fetchCurrencyValues: Function, fetchCurrencyValues: Function,
fetchTokens: Function, fetchTokens: Function,
@ -156,10 +156,8 @@ class Balances extends React.Component<Props, State> {
activeTokens, activeTokens,
blacklistedTokens, blacklistedTokens,
classes, classes,
createTransaction,
currencySelected, currencySelected,
currencyValues, currencyValues,
ethBalance,
granted, granted,
safeAddress, safeAddress,
safeName, safeName,
@ -304,14 +302,9 @@ class Balances extends React.Component<Props, State> {
{erc721Enabled && showCollectibles && <Collectibles />} {erc721Enabled && showCollectibles && <Collectibles />}
<SendModal <SendModal
activeScreenType="sendFunds" activeScreenType="sendFunds"
createTransaction={createTransaction}
ethBalance={ethBalance}
isOpen={sendFunds.isOpen} isOpen={sendFunds.isOpen}
onClose={this.hideSendFunds} onClose={this.hideSendFunds}
safeAddress={safeAddress}
safeName={safeName}
selectedToken={sendFunds.selectedToken} selectedToken={sendFunds.selectedToken}
tokens={activeTokens}
/> />
<Modal <Modal
description="Receive Tokens Form" description="Receive Tokens Form"

View File

@ -316,10 +316,8 @@ const Layout = (props: Props) => {
activateTokensByBalance={activateTokensByBalance} activateTokensByBalance={activateTokensByBalance}
activeTokens={activeTokens} activeTokens={activeTokens}
blacklistedTokens={blacklistedTokens} blacklistedTokens={blacklistedTokens}
createTransaction={createTransaction}
currencySelected={currencySelected} currencySelected={currencySelected}
currencyValues={currencyValues} currencyValues={currencyValues}
ethBalance={ethBalance}
featuresEnabled={featuresEnabled} featuresEnabled={featuresEnabled}
fetchCurrencyValues={fetchCurrencyValues} fetchCurrencyValues={fetchCurrencyValues}
fetchTokens={fetchTokens} fetchTokens={fetchTokens}
@ -378,14 +376,9 @@ const Layout = (props: Props) => {
</Switch> </Switch>
<SendModal <SendModal
activeScreenType="chooseTxType" activeScreenType="chooseTxType"
createTransaction={createTransaction}
ethBalance={ethBalance}
isOpen={sendFunds.isOpen} isOpen={sendFunds.isOpen}
onClose={hideSendFunds} onClose={hideSendFunds}
safeAddress={address}
safeName={name}
selectedToken={sendFunds.selectedToken} selectedToken={sendFunds.selectedToken}
tokens={activeTokens}
/> />
<Modal <Modal
description="Receive Tokens Form" description="Receive Tokens Form"

View File

@ -13,7 +13,13 @@ import { type TxServiceType, buildTxServiceUrl } from '~/logic/safe/transactions
import { getLocalSafe } from '~/logic/safe/utils' import { getLocalSafe } from '~/logic/safe/utils'
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens' import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi' import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi'
import { isMultisendTransaction, isTokenTransfer, isUpgradeTransaction } from '~/logic/tokens/utils/tokenHelpers' import {
DECIMALS_METHOD_HASH,
SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH,
isMultisendTransaction,
isTokenTransfer,
isUpgradeTransaction,
} from '~/logic/tokens/utils/tokenHelpers'
import { ZERO_ADDRESS, sameAddress } from '~/logic/wallets/ethAddresses' import { ZERO_ADDRESS, sameAddress } from '~/logic/wallets/ethAddresses'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
@ -93,7 +99,8 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod
const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data
const code = tx.to ? await web3.eth.getCode(tx.to) : '' const code = tx.to ? await web3.eth.getCode(tx.to) : ''
const isERC721Token = const isERC721Token =
code.includes('42842e0e') || (isTokenTransfer(tx.data, Number(tx.value)) && code.includes('06fdde03')) code.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH) ||
(isTokenTransfer(tx.data, Number(tx.value)) && !code.includes(DECIMALS_METHOD_HASH))
const isSendTokenTx = !isERC721Token && isTokenTransfer(tx.data, Number(tx.value)) const isSendTokenTx = !isERC721Token && isTokenTransfer(tx.data, Number(tx.value))
const isMultiSendTx = isMultisendTransaction(tx.data, Number(tx.value)) const isMultiSendTx = isMultisendTransaction(tx.data, Number(tx.value))
const isUpgradeTx = isMultiSendTx && isUpgradeTransaction(tx.data) const isUpgradeTx = isMultiSendTx && isUpgradeTransaction(tx.data)