(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 Span from 'src/components/layout/Span'
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 { getExplorerInfo } from 'src/config'
@ -19,9 +19,16 @@ interface EtherscanLinkProps {
cut?: number
knownAddress?: boolean
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()
return (
@ -31,7 +38,13 @@ export const EtherscanLink = ({ className, cut, knownAddress, value }: Etherscan
</Span>
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
<ExplorerButton explorerUrl={getExplorerInfo(value)} />
{knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null}
{knownAddress !== undefined ? (
<EllipsisTransactionDetails
address={value}
knownAddress={knownAddress}
sendModalOpenHandler={sendModalOpenHandler}
/>
) : null}
</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 MenuItem from '@material-ui/core/MenuItem'
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 { xs } from 'src/theme/variables'
const useStyles = makeStyles({
container: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
margin: `0 ${xs}`,
borderRadius: '50%',
transition: 'background-color .2s ease-in-out',
'&:hover': {
backgroundColor: '#F0EFEE',
const useStyles = makeStyles(
createStyles({
container: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
margin: `0 ${xs}`,
borderRadius: '50%',
transition: 'background-color .2s ease-in-out',
'&: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 [anchorEl, setAnchorEl] = React.useState(null)
@ -51,10 +63,12 @@ const EllipsisTransactionDetails = ({ address, knownAddress }) => {
<div className={classes.container} role="menu" tabIndex={0}>
<MoreHorizIcon onClick={handleClick} onKeyDown={handleClick} />
<Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
<MenuItem disabled onClick={closeMenuHandler}>
Send Again
</MenuItem>
<Divider />
{sendModalOpenHandler ? (
<>
<MenuItem onClick={sendModalOpenHandler}>Send Again</MenuItem>
<Divider />
</>
) : null}
{knownAddress ? (
<MenuItem onClick={addOrEditEntryHandler}>Edit Address book Entry</MenuItem>
) : (
@ -65,5 +79,3 @@ const EllipsisTransactionDetails = ({ address, knownAddress }) => {
</ClickAwayListener>
)
}
export default EllipsisTransactionDetails

View File

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

View File

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

View File

@ -13,10 +13,11 @@ type OwnerAddressTableCellProps = {
knownAddress?: boolean
showLinks: boolean
userName?: string
sendModalOpenHandler?: () => void
}
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 { width } = useWindowDimensions()
@ -36,7 +37,12 @@ const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactEl
{showLinks ? (
<div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}>
{userName && getValidAddressBookName(userName)}
<EtherscanLink knownAddress={knownAddress} value={address} cut={cut} />
<EtherscanLink
knownAddress={knownAddress}
value={address}
cut={cut}
sendModalOpenHandler={sendModalOpenHandler}
/>
</div>
) : (
<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 { TRANSACTIONS_DESC_SEND_TEST_ID } from './index'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
interface TransferDescriptionProps {
amount: string
amountWithSymbol: 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 [sendModalOpen, setSendModalOpen] = React.useState(false)
const sendModalOpenHandler = () => {
setSendModalOpen(true)
}
return (
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
<Bold>Send {amount} to:</Bold>
{recipientName ? (
<OwnerAddressTableCell address={recipient} knownAddress showLinks userName={recipientName} />
) : (
<EtherscanLink knownAddress={false} value={recipient} />
)}
</Block>
<>
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
<Bold>Send {amountWithSymbol} to:</Bold>
{recipientName ? (
<OwnerAddressTableCell
address={recipient}
knownAddress
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 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 { Transaction } from 'src/logic/safe/store/models/types/transaction'
@ -30,8 +30,12 @@ const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
recipient,
removedOwner,
upgradeTx,
tokenAddress,
isTokenTransfer,
}: any = getTxData(tx)
const amount = getTxAmount(tx, false)
const amountWithSymbol = getTxAmount(tx, false)
const amount = getRawTxAmount(tx)
return (
<Block className={classes.txDataContainer}>
{modifySettingsTx && action && (
@ -43,10 +47,18 @@ const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
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>}
{!cancellationTx && !modifySettingsTx && !customTx && !creationTx && !upgradeTx && (
<TransferDescription amount={amount} recipient={recipient} />
<TransferDescription
amountWithSymbol={amountWithSymbol}
recipient={recipient}
tokenAddress={tokenAddress}
rawAmount={amount}
isTokenTransfer={isTokenTransfer}
/>
)}
</Block>
)

View File

@ -1,5 +1,7 @@
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
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 contractAddress = data.substr(340, 40).toLowerCase()
@ -27,6 +29,7 @@ interface TxData {
cancellationTx?: boolean
creationTx?: boolean
upgradeTx?: boolean
tokenAddress?: string
}
const getTxDataForModifySettingsTxs = (tx: Transaction): TxData => {
@ -97,6 +100,7 @@ const getTxDataForTxsWithDecodedParams = (tx: Transaction): TxData => {
const { to } = tx.decodedParams.transfer || {}
txData.recipient = to
txData.isTokenTransfer = true
txData.tokenAddress = tx.recipient
return txData
}
@ -133,6 +137,12 @@ const getTxDataForTxsWithDecodedParams = (tx: Transaction): TxData => {
export const getTxData = (tx: Transaction): 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) {
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 { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions'
import { getNetworkInfo } from 'src/config'
export const TX_TABLE_ID = 'id'
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)
}
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 {
amount: string
cancelTx?: Transaction