(Feature) - #1244 send tx again (#1582)

* Types

* Adds tokenAddress to getTxData for tokenTransfer transactions

* Adds sendModalOpenHandler to EllipsisTransactionDetails

* Adds getRawTxAmount util

* Add isTokenTransfer fix for ether in getTxData

* Uses sendFund modal for retry outgoing transfer transactions

* Adds ether address in getTxData result for outgoig transfers

* Uses nativeCoin

* Remove fragmnet

* Fix decimals for native coin

* Fix decimals usage in tx transfer amount

Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Agustin Pane 2020-11-11 14:20:10 -03:00 committed by GitHub
parent 19e6df725a
commit 294ba47142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 166 additions and 60 deletions

View File

@ -8,7 +8,7 @@ import CopyBtn from 'src/components/CopyBtn'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Span from 'src/components/layout/Span' import Span from 'src/components/layout/Span'
import { shortVersionOf } from 'src/logic/wallets/ethAddresses' import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
import EllipsisTransactionDetails from 'src/routes/safe/components/AddressBook/EllipsisTransactionDetails' import { EllipsisTransactionDetails } from 'src/routes/safe/components/AddressBook/EllipsisTransactionDetails'
import { ExplorerButton } from '@gnosis.pm/safe-react-components' import { ExplorerButton } from '@gnosis.pm/safe-react-components'
import { getExplorerInfo } from 'src/config' import { getExplorerInfo } from 'src/config'
@ -19,9 +19,16 @@ interface EtherscanLinkProps {
cut?: number cut?: number
knownAddress?: boolean knownAddress?: boolean
value: string value: string
sendModalOpenHandler?: () => void
} }
export const EtherscanLink = ({ className, cut, knownAddress, value }: EtherscanLinkProps): React.ReactElement => { export const EtherscanLink = ({
className,
cut,
knownAddress,
value,
sendModalOpenHandler,
}: EtherscanLinkProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
return ( return (
@ -31,7 +38,13 @@ export const EtherscanLink = ({ className, cut, knownAddress, value }: Etherscan
</Span> </Span>
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} /> <CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
<ExplorerButton explorerUrl={getExplorerInfo(value)} /> <ExplorerButton explorerUrl={getExplorerInfo(value)} />
{knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null} {knownAddress !== undefined ? (
<EllipsisTransactionDetails
address={value}
knownAddress={knownAddress}
sendModalOpenHandler={sendModalOpenHandler}
/>
) : null}
</Block> </Block>
) )
} }

View File

@ -1,4 +1,4 @@
import { ClickAwayListener, Divider } from '@material-ui/core' import { ClickAwayListener, createStyles, Divider } from '@material-ui/core'
import Menu from '@material-ui/core/Menu' import Menu from '@material-ui/core/Menu'
import MenuItem from '@material-ui/core/MenuItem' import MenuItem from '@material-ui/core/MenuItem'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
@ -11,26 +11,38 @@ import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { xs } from 'src/theme/variables' import { xs } from 'src/theme/variables'
const useStyles = makeStyles({ const useStyles = makeStyles(
container: { createStyles({
display: 'flex', container: {
justifyContent: 'center', display: 'flex',
alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', alignItems: 'center',
margin: `0 ${xs}`, cursor: 'pointer',
borderRadius: '50%', margin: `0 ${xs}`,
transition: 'background-color .2s ease-in-out', borderRadius: '50%',
'&:hover': { transition: 'background-color .2s ease-in-out',
backgroundColor: '#F0EFEE', '&:hover': {
backgroundColor: '#F0EFEE',
},
outline: 'none',
}, },
outline: 'none', increasedPopperZindex: {
}, zIndex: 2001,
increasedPopperZindex: { },
zIndex: 2001, }),
}, )
})
const EllipsisTransactionDetails = ({ address, knownAddress }) => { type EllipsisTransactionDetailsProps = {
address: string
knownAddress?: boolean
sendModalOpenHandler?: () => void
}
export const EllipsisTransactionDetails = ({
address,
knownAddress,
sendModalOpenHandler,
}: EllipsisTransactionDetailsProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const [anchorEl, setAnchorEl] = React.useState(null) const [anchorEl, setAnchorEl] = React.useState(null)
@ -51,10 +63,12 @@ const EllipsisTransactionDetails = ({ address, knownAddress }) => {
<div className={classes.container} role="menu" tabIndex={0}> <div className={classes.container} role="menu" tabIndex={0}>
<MoreHorizIcon onClick={handleClick} onKeyDown={handleClick} /> <MoreHorizIcon onClick={handleClick} onKeyDown={handleClick} />
<Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}> <Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
<MenuItem disabled onClick={closeMenuHandler}> {sendModalOpenHandler ? (
Send Again <>
</MenuItem> <MenuItem onClick={sendModalOpenHandler}>Send Again</MenuItem>
<Divider /> <Divider />
</>
) : null}
{knownAddress ? ( {knownAddress ? (
<MenuItem onClick={addOrEditEntryHandler}>Edit Address book Entry</MenuItem> <MenuItem onClick={addOrEditEntryHandler}>Edit Address book Entry</MenuItem>
) : ( ) : (
@ -65,5 +79,3 @@ const EllipsisTransactionDetails = ({ address, knownAddress }) => {
</ClickAwayListener> </ClickAwayListener>
) )
} }
export default EllipsisTransactionDetails

View File

@ -6,7 +6,6 @@ import React, { Suspense, useEffect, useState } from 'react'
import Modal from 'src/components/Modal' import Modal from 'src/components/Modal'
import { CollectibleTx } from './screens/ReviewCollectible' import { CollectibleTx } from './screens/ReviewCollectible'
import { CustomTx } from './screens/ContractInteraction/ReviewCustomTx' import { CustomTx } from './screens/ContractInteraction/ReviewCustomTx'
import { SendFundsTx } from './screens/SendFunds'
import { ContractInteractionTx } from './screens/ContractInteraction' import { ContractInteractionTx } from './screens/ContractInteraction'
import { CustomTxProps } from './screens/ContractInteraction/SendCustomTx' import { CustomTxProps } from './screens/ContractInteraction/SendCustomTx'
import { ReviewTxProp } from './screens/ReviewTx' import { ReviewTxProp } from './screens/ReviewTx'
@ -53,6 +52,7 @@ type Props = {
onClose: () => void onClose: () => void
recipientAddress?: string recipientAddress?: string
selectedToken?: string | NFTToken selectedToken?: string | NFTToken
tokenAmount?: string
} }
const SendModal = ({ const SendModal = ({
@ -61,6 +61,7 @@ const SendModal = ({
onClose, onClose,
recipientAddress, recipientAddress,
selectedToken, selectedToken,
tokenAmount,
}: Props): React.ReactElement => { }: Props): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType') const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType')
@ -119,11 +120,11 @@ const SendModal = ({
)} )}
{activeScreen === 'sendFunds' && ( {activeScreen === 'sendFunds' && (
<SendFunds <SendFunds
initialValues={tx as SendFundsTx}
onClose={onClose} onClose={onClose}
onNext={handleTxCreation} onNext={handleTxCreation}
recipientAddress={recipientAddress} recipientAddress={recipientAddress}
selectedToken={selectedToken as string} selectedToken={selectedToken as string}
amount={tokenAmount}
/> />
)} )}
{activeScreen === 'reviewTx' && ( {activeScreen === 'reviewTx' && (

View File

@ -48,44 +48,36 @@ const formMutators = {
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
export type SendFundsTx = {
amount?: string
recipientAddress?: string
token?: string
}
type SendFundsProps = { type SendFundsProps = {
initialValues: SendFundsTx
onClose: () => void onClose: () => void
onNext: (txInfo: unknown) => void onNext: (txInfo: unknown) => void
recipientAddress?: string recipientAddress?: string
selectedToken?: string selectedToken?: string
amount?: string
} }
const { nativeCoin } = getNetworkInfo() const { nativeCoin } = getNetworkInfo()
const SendFunds = ({ const SendFunds = ({
initialValues,
onClose, onClose,
onNext, onNext,
recipientAddress, recipientAddress,
selectedToken = '', selectedToken = '',
amount,
}: SendFundsProps): React.ReactElement => { }: SendFundsProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const tokens = useSelector(extendedSafeTokensSelector) const tokens = useSelector(extendedSafeTokensSelector)
const addressBook = useSelector(addressBookSelector) const addressBook = useSelector(addressBookSelector)
const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => { const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => {
const defaultEntry = { address: '', name: '' } const defaultEntry = { address: recipientAddress || '', name: '' }
// if there's nothing to lookup for, we return the default entry // if there's nothing to lookup for, we return the default entry
if (!initialValues?.recipientAddress && !recipientAddress) { if (!recipientAddress) {
return defaultEntry return defaultEntry
} }
// if there's something to lookup for, `initialValues` has precedence over `recipientAddress`
const predefinedAddress = initialValues?.recipientAddress ?? recipientAddress
const addressBookEntry = addressBook.find(({ address }) => { const addressBookEntry = addressBook.find(({ address }) => {
return sameAddress(predefinedAddress, address) return sameAddress(recipientAddress, address)
}) })
// if found in the Address Book, then we return the entry // if found in the Address Book, then we return the entry
@ -126,7 +118,11 @@ const SendFunds = ({
</IconButton> </IconButton>
</Row> </Row>
<Hairline /> <Hairline />
<GnoForm formMutators={formMutators} initialValues={initialValues} onSubmit={handleSubmit}> <GnoForm
formMutators={formMutators}
initialValues={{ amount, recipientAddress, token: selectedToken }}
onSubmit={handleSubmit}
>
{(...args) => { {(...args) => {
const formState = args[2] const formState = args[2]
const mutators = args[3] const mutators = args[3]

View File

@ -13,10 +13,11 @@ type OwnerAddressTableCellProps = {
knownAddress?: boolean knownAddress?: boolean
showLinks: boolean showLinks: boolean
userName?: string userName?: string
sendModalOpenHandler?: () => void
} }
const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactElement => { const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactElement => {
const { address, knownAddress, showLinks, userName } = props const { address, knownAddress, showLinks, userName, sendModalOpenHandler } = props
const [cut, setCut] = useState(0) const [cut, setCut] = useState(0)
const { width } = useWindowDimensions() const { width } = useWindowDimensions()
@ -36,7 +37,12 @@ const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactEl
{showLinks ? ( {showLinks ? (
<div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}> <div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}>
{userName && getValidAddressBookName(userName)} {userName && getValidAddressBookName(userName)}
<EtherscanLink knownAddress={knownAddress} value={address} cut={cut} /> <EtherscanLink
knownAddress={knownAddress}
value={address}
cut={cut}
sendModalOpenHandler={sendModalOpenHandler}
/>
</div> </div>
) : ( ) : (
<Paragraph style={{ marginLeft: 10 }}>{address}</Paragraph> <Paragraph style={{ marginLeft: 10 }}>{address}</Paragraph>

View File

@ -7,23 +7,59 @@ import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/sele
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell' import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
import { TRANSACTIONS_DESC_SEND_TEST_ID } from './index' import { TRANSACTIONS_DESC_SEND_TEST_ID } from './index'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
interface TransferDescriptionProps { interface TransferDescriptionProps {
amount: string amountWithSymbol: string
recipient: string recipient: string
tokenAddress?: string
rawAmount?: string
isTokenTransfer: boolean
} }
const TransferDescription = ({ amount = '', recipient }: TransferDescriptionProps): React.ReactElement => { const TransferDescription = ({
amountWithSymbol = '',
recipient,
tokenAddress,
rawAmount,
isTokenTransfer,
}: TransferDescriptionProps): React.ReactElement => {
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, recipient)) const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, recipient))
const [sendModalOpen, setSendModalOpen] = React.useState(false)
const sendModalOpenHandler = () => {
setSendModalOpen(true)
}
return ( return (
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}> <>
<Bold>Send {amount} to:</Bold> <Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
{recipientName ? ( <Bold>Send {amountWithSymbol} to:</Bold>
<OwnerAddressTableCell address={recipient} knownAddress showLinks userName={recipientName} /> {recipientName ? (
) : ( <OwnerAddressTableCell
<EtherscanLink knownAddress={false} value={recipient} /> address={recipient}
)} knownAddress
</Block> showLinks
userName={recipientName}
sendModalOpenHandler={isTokenTransfer ? sendModalOpenHandler : undefined}
/>
) : (
<EtherscanLink
knownAddress={false}
value={recipient}
sendModalOpenHandler={isTokenTransfer ? sendModalOpenHandler : undefined}
/>
)}
</Block>
<SendModal
activeScreenType="sendFunds"
isOpen={sendModalOpen}
onClose={() => setSendModalOpen(false)}
recipientAddress={recipient}
selectedToken={tokenAddress}
tokenAmount={rawAmount}
/>
</>
) )
} }

View File

@ -7,7 +7,7 @@ import SettingsDescription from './SettingsDescription'
import CustomDescription from './CustomDescription' import CustomDescription from './CustomDescription'
import TransferDescription from './TransferDescription' import TransferDescription from './TransferDescription'
import { getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns' import { getRawTxAmount, getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import { Transaction } from 'src/logic/safe/store/models/types/transaction' import { Transaction } from 'src/logic/safe/store/models/types/transaction'
@ -30,8 +30,12 @@ const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
recipient, recipient,
removedOwner, removedOwner,
upgradeTx, upgradeTx,
tokenAddress,
isTokenTransfer,
}: any = getTxData(tx) }: any = getTxData(tx)
const amount = getTxAmount(tx, false)
const amountWithSymbol = getTxAmount(tx, false)
const amount = getRawTxAmount(tx)
return ( return (
<Block className={classes.txDataContainer}> <Block className={classes.txDataContainer}>
{modifySettingsTx && action && ( {modifySettingsTx && action && (
@ -43,10 +47,18 @@ const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
module={module} module={module}
/> />
)} )}
{!upgradeTx && customTx && <CustomDescription amount={amount} data={data} recipient={recipient} storedTx={tx} />} {!upgradeTx && customTx && (
<CustomDescription amount={amountWithSymbol} data={data} recipient={recipient} storedTx={tx} />
)}
{upgradeTx && <div>{data}</div>} {upgradeTx && <div>{data}</div>}
{!cancellationTx && !modifySettingsTx && !customTx && !creationTx && !upgradeTx && ( {!cancellationTx && !modifySettingsTx && !customTx && !creationTx && !upgradeTx && (
<TransferDescription amount={amount} recipient={recipient} /> <TransferDescription
amountWithSymbol={amountWithSymbol}
recipient={recipient}
tokenAddress={tokenAddress}
rawAmount={amount}
isTokenTransfer={isTokenTransfer}
/>
)} )}
</Block> </Block>
) )

View File

@ -1,5 +1,7 @@
import { Transaction } from 'src/logic/safe/store/models/types/transaction' import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { SAFE_METHODS_NAMES } from 'src/routes/safe/store/models/types/transactions.d' import { SAFE_METHODS_NAMES } from 'src/routes/safe/store/models/types/transactions.d'
import { sameString } from 'src/utils/strings'
import { getNetworkInfo } from 'src/config'
const getSafeVersion = (data) => { const getSafeVersion = (data) => {
const contractAddress = data.substr(340, 40).toLowerCase() const contractAddress = data.substr(340, 40).toLowerCase()
@ -27,6 +29,7 @@ interface TxData {
cancellationTx?: boolean cancellationTx?: boolean
creationTx?: boolean creationTx?: boolean
upgradeTx?: boolean upgradeTx?: boolean
tokenAddress?: string
} }
const getTxDataForModifySettingsTxs = (tx: Transaction): TxData => { const getTxDataForModifySettingsTxs = (tx: Transaction): TxData => {
@ -97,6 +100,7 @@ const getTxDataForTxsWithDecodedParams = (tx: Transaction): TxData => {
const { to } = tx.decodedParams.transfer || {} const { to } = tx.decodedParams.transfer || {}
txData.recipient = to txData.recipient = to
txData.isTokenTransfer = true txData.isTokenTransfer = true
txData.tokenAddress = tx.recipient
return txData return txData
} }
@ -133,6 +137,12 @@ const getTxDataForTxsWithDecodedParams = (tx: Transaction): TxData => {
export const getTxData = (tx: Transaction): TxData => { export const getTxData = (tx: Transaction): TxData => {
const txData: TxData = {} const txData: TxData = {}
const { nativeCoin } = getNetworkInfo()
if (sameString(tx.type, 'outgoing') && tx.symbol && sameString(tx.symbol, nativeCoin.symbol)) {
txData.isTokenTransfer = true
txData.tokenAddress = nativeCoin.address
}
if (tx.decodedParams) { if (tx.decodedParams) {
return getTxDataForTxsWithDecodedParams(tx) return getTxDataForTxsWithDecodedParams(tx)
} }

View File

@ -13,6 +13,7 @@ import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { INCOMING_TX_TYPES } from 'src/logic/safe/store/models/incomingTransaction' import { INCOMING_TX_TYPES } from 'src/logic/safe/store/models/incomingTransaction'
import { Transaction } from 'src/logic/safe/store/models/types/transaction' import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions' import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions'
import { getNetworkInfo } from 'src/config'
export const TX_TABLE_ID = 'id' export const TX_TABLE_ID = 'id'
export const TX_TABLE_TYPE_ID = 'type' export const TX_TABLE_TYPE_ID = 'type'
@ -71,6 +72,25 @@ export const getTxAmount = (tx: Transaction, formatted = true): string => {
return getAmountWithSymbol({ decimals: decimals as string, symbol: symbol as string, value }, formatted) return getAmountWithSymbol({ decimals: decimals as string, symbol: symbol as string, value }, formatted)
} }
export const getRawTxAmount = (tx: Transaction): string => {
const { decimals, decodedParams, isTokenTransfer } = tx
const { nativeCoin } = getNetworkInfo()
const { value } = isTokenTransfer && !!decodedParams?.transfer ? decodedParams.transfer : tx
if (tx.isCollectibleTransfer) {
return '1'
}
if (!isTokenTransfer && !(Number(value) > 0)) {
return NOT_AVAILABLE
}
const tokenDecimals = decimals ?? nativeCoin.decimals
const finalValue = new BigNumber(value).times(`1e-${tokenDecimals}`).toFixed()
return finalValue === 'NaN' ? NOT_AVAILABLE : finalValue
}
export interface TableData { export interface TableData {
amount: string amount: string
cancelTx?: Transaction cancelTx?: Transaction