* Add `cancel transactions` state to the store * Populate store with `cancelTransactions` separated from `transactions` - also fixed some related typing errors/warnings * Avoid flow typing errors * WiP - Merge Tx with its cancellation * Prevent notification of pending transaction * Mark transaction as cancelled * Fix check errors * Code cleanup * Use `cancel` flags for confirm/execute of a cancelling tx modal * Fix `Cancel Tx` button row display conditions * Fix transaction row condition for cancelled tx * Fix execute icon display conditions * Fix condition to display `Confirm Tx` buttons for cancel tx * Fix conditions for Execute icon * Add test for `getTxTableData` * Fix `updateAddressBookEntry` to make tests run * (Feature) Transaction cancellation: implement design (#465) * (fix) button icons positions * (add) new buttons layout * (fix) executor block style * (remove) unused file * (fix) transaction timeline styles * (fix) overflowing left contents * Show buttons only in the confirmation thread * Green line on top of Cancel flow only when tx is executed * Avoid checking `INCOMING_TX_TYPE` for a cancelTx * Clean up code conditions * Rename `cancelTransactions` to `cancellationTransactions` * Start functions with a verb. `get` in this case. Co-authored-by: Fernando <fernando.greco@gmail.com> * Change notification message for cancelling txs - Added `TX_CANCELLATION_EXECUTED_MSG` - Also did some flow typing error fixes * missing file for cancelling tx message * Always display Reject flow - display buttons independently (reject buttons in reject flow, confirmation buttons in confirmation flow) * Reject a Tx when threshold is reached * Use `safeGasPrice` instead of `gasPrice` to prevent sending `null`s to web3 * Revert "Use `safeGasPrice` instead of `gasPrice` to prevent sending `null`s to web3" This reverts commit db4cd728 * Revert "Use `safeGasPrice` instead of `gasPrice` to prevent sending `null`s to web3" This reverts commit db4cd728 Also sets '0' as a default value if `gasPrice` is falsy * Do not use current `threshold` for closed/executed transactions * Add closing square bracket * Verify if txNonce is invalid, zero is a falsy value but a valid nonce * Display Execute Reject for those tx that met the threshold but weren't able to be executed * Show pending txs messages for non executed rejections * wrap `getTimelineCircle` into useMemo * Remove unnecessary comments * Verify tx nonce by using `Number.isInteger` * Parse tx nonce before verifying it Co-authored-by: Gabriel Rodríguez Alsina <gabitoesmiapodo@users.noreply.github.com>
This commit is contained in:
parent
226b525c7e
commit
3e5d4f6646
|
@ -2,6 +2,7 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import Tooltip from '@material-ui/core/Tooltip'
|
import Tooltip from '@material-ui/core/Tooltip'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import cn from 'classnames'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
import { copyToClipboard } from '~/utils/clipboard'
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
import { xs } from '~/theme/variables'
|
import { xs } from '~/theme/variables'
|
||||||
|
@ -26,11 +27,12 @@ const useStyles = makeStyles({
|
||||||
})
|
})
|
||||||
|
|
||||||
type CopyBtnProps = {
|
type CopyBtnProps = {
|
||||||
|
className?: any,
|
||||||
content: string,
|
content: string,
|
||||||
increaseZindex?: boolean,
|
increaseZindex?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CopyBtn = ({ content, increaseZindex = false }: CopyBtnProps) => {
|
const CopyBtn = ({ className, content, increaseZindex = false }: CopyBtnProps) => {
|
||||||
const [clicked, setClicked] = useState<boolean>(false)
|
const [clicked, setClicked] = useState<boolean>(false)
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
|
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
|
||||||
|
@ -50,7 +52,7 @@ const CopyBtn = ({ content, increaseZindex = false }: CopyBtnProps) => {
|
||||||
}}
|
}}
|
||||||
classes={customClasses}
|
classes={customClasses}
|
||||||
>
|
>
|
||||||
<div className={classes.container}>
|
<div className={cn(classes.container, className)}>
|
||||||
<Img
|
<Img
|
||||||
src={CopyIcon}
|
src={CopyIcon}
|
||||||
height={20}
|
height={20}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Tooltip from '@material-ui/core/Tooltip'
|
import Tooltip from '@material-ui/core/Tooltip'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import cn from 'classnames'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
import EtherscanOpenIcon from './img/etherscan-open.svg'
|
import EtherscanOpenIcon from './img/etherscan-open.svg'
|
||||||
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
||||||
|
@ -26,12 +27,15 @@ const useStyles = makeStyles({
|
||||||
})
|
})
|
||||||
|
|
||||||
type EtherscanBtnProps = {
|
type EtherscanBtnProps = {
|
||||||
|
className?: any,
|
||||||
|
increaseZindex?: boolean,
|
||||||
type: 'tx' | 'address',
|
type: 'tx' | 'address',
|
||||||
value: string,
|
value: string,
|
||||||
increaseZindex?: boolean,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EtherscanBtn = ({ type, value, increaseZindex = false }: EtherscanBtnProps) => {
|
const EtherscanBtn = ({
|
||||||
|
type, value, className, increaseZindex = false,
|
||||||
|
}: EtherscanBtnProps) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
|
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
|
||||||
|
|
||||||
|
@ -39,7 +43,7 @@ const EtherscanBtn = ({ type, value, increaseZindex = false }: EtherscanBtnProps
|
||||||
<Tooltip title="Show details on Etherscan" placement="top" classes={customClasses}>
|
<Tooltip title="Show details on Etherscan" placement="top" classes={customClasses}>
|
||||||
<a
|
<a
|
||||||
aria-label="Show details on Etherscan"
|
aria-label="Show details on Etherscan"
|
||||||
className={classes.container}
|
className={cn(classes.container, className)}
|
||||||
href={getEtherScanLink(type, value)}
|
href={getEtherScanLink(type, value)}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
@ -11,11 +11,11 @@ import EllipsisTransactionDetails from '~/routes/safe/components/AddressBook/Ell
|
||||||
import Span from '~/components/layout/Span'
|
import Span from '~/components/layout/Span'
|
||||||
|
|
||||||
type EtherscanLinkProps = {
|
type EtherscanLinkProps = {
|
||||||
type: 'tx' | 'address',
|
classes: Object,
|
||||||
value: string,
|
|
||||||
cut?: number,
|
cut?: number,
|
||||||
knownAddress?: boolean,
|
knownAddress?: boolean,
|
||||||
classes: Object,
|
type: 'tx' | 'address',
|
||||||
|
value: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const EtherscanLink = ({
|
const EtherscanLink = ({
|
||||||
|
@ -23,13 +23,13 @@ const EtherscanLink = ({
|
||||||
}: EtherscanLinkProps) => (
|
}: EtherscanLinkProps) => (
|
||||||
<Block className={classes.etherscanLink}>
|
<Block className={classes.etherscanLink}>
|
||||||
<Span
|
<Span
|
||||||
|
className={cn(knownAddress && classes.addressParagraph, classes.address)}
|
||||||
size="md"
|
size="md"
|
||||||
className={cn(knownAddress && classes.addressParagraph)}
|
|
||||||
>
|
>
|
||||||
{cut ? shortVersionOf(value, cut) : value}
|
{cut ? shortVersionOf(value, cut) : value}
|
||||||
</Span>
|
</Span>
|
||||||
<CopyBtn content={value} />
|
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
|
||||||
<EtherscanBtn type={type} value={value} />
|
<EtherscanBtn className={classes.button} type={type} value={value} />
|
||||||
{knownAddress !== undefined ? <EllipsisTransactionDetails knownAddress={knownAddress} address={value} /> : null}
|
{knownAddress !== undefined ? <EllipsisTransactionDetails knownAddress={knownAddress} address={value} /> : null}
|
||||||
</Block>
|
</Block>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,11 +1,30 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { secondaryText } from '~/theme/variables'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = () => ({
|
||||||
etherscanLink: {
|
etherscanLink: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
||||||
|
'& svg': {
|
||||||
|
fill: secondaryText,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
display: 'block',
|
||||||
|
flexShrink: '1',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
},
|
},
|
||||||
addressParagraph: {
|
addressParagraph: {
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
},
|
},
|
||||||
|
button: {
|
||||||
|
height: '24px',
|
||||||
|
margin: '0',
|
||||||
|
width: '24px',
|
||||||
|
},
|
||||||
|
firstButton: {
|
||||||
|
marginLeft: '8px',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
import { type GlobalState } from '~/store/index'
|
import { type GlobalState } from '~/store/index'
|
||||||
import { saveAddressBook } from '~/logic/addressBook/utils'
|
import { saveAddressBook } from '~/logic/addressBook/utils'
|
||||||
import { updateAddressBook } from '~/logic/addressBook/store/actions/updateAddressBook'
|
import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||||
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
|
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
|
||||||
|
|
||||||
const saveAndUpdateAddressBook = (addressBook: AddressBook) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
const saveAndUpdateAddressBook = (addressBook: AddressBook) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
try {
|
try {
|
||||||
dispatch(updateAddressBook(addressBook))
|
dispatch(updateAddressBookEntry(addressBook))
|
||||||
await saveAddressBook(addressBook)
|
await saveAddressBook(addressBook)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
|
|
|
@ -10,11 +10,11 @@ import { type Notification, NOTIFICATIONS } from './notificationTypes'
|
||||||
export type NotificationsQueue = {
|
export type NotificationsQueue = {
|
||||||
beforeExecution: Notification | null,
|
beforeExecution: Notification | null,
|
||||||
pendingExecution: Notification | null,
|
pendingExecution: Notification | null,
|
||||||
waitingConfirmation: Notification | null,
|
waitingConfirmation?: Notification | null,
|
||||||
afterExecution: {
|
afterExecution: {
|
||||||
noMoreConfirmationsNeeded: Notification | null,
|
noMoreConfirmationsNeeded: Notification | null,
|
||||||
moreConfirmationsNeeded: Notification | null,
|
moreConfirmationsNeeded: Notification | null,
|
||||||
},
|
} | null,
|
||||||
afterExecutionError: Notification | null,
|
afterExecutionError: Notification | null,
|
||||||
afterRejection: Notification | null,
|
afterRejection: Notification | null,
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ const cancellationTxNotificationsQueue: NotificationsQueue = {
|
||||||
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
|
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
|
||||||
afterExecution: {
|
afterExecution: {
|
||||||
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_EXECUTED_MSG,
|
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_EXECUTED_MSG,
|
||||||
moreConfirmationsNeeded: NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG,
|
moreConfirmationsNeeded: NOTIFICATIONS.TX_CANCELLATION_EXECUTED_MSG,
|
||||||
},
|
},
|
||||||
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
|
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
|
||||||
}
|
}
|
||||||
|
@ -200,19 +200,26 @@ export const enhanceSnackbarForAction = (notification: Notification, key?: strin
|
||||||
options: {
|
options: {
|
||||||
...notification.options,
|
...notification.options,
|
||||||
onClick,
|
onClick,
|
||||||
action: (key: number) => (
|
action: (actionKey: number) => (
|
||||||
<IconButton onClick={() => store.dispatch(closeSnackbarAction({ key }))}>
|
<IconButton onClick={() => store.dispatch(closeSnackbarAction({ key: actionKey }))}>
|
||||||
<IconClose />
|
<IconClose />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const showSnackbar = (notification: Notification, enqueueSnackbar: Function, closeSnackbar: Function) => enqueueSnackbar(notification.message, {
|
export const showSnackbar = (
|
||||||
|
notification: Notification,
|
||||||
|
enqueueSnackbar: Function,
|
||||||
|
closeSnackbar: Function,
|
||||||
|
) => enqueueSnackbar(
|
||||||
|
notification.message,
|
||||||
|
{
|
||||||
...notification.options,
|
...notification.options,
|
||||||
action: (key) => (
|
action: (key) => (
|
||||||
<IconButton onClick={() => closeSnackbar(key)}>
|
<IconButton onClick={() => closeSnackbar(key)}>
|
||||||
<IconClose />
|
<IconClose />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
),
|
),
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
|
@ -37,6 +37,7 @@ export type Notifications = {
|
||||||
TX_PENDING_MSG: Notification,
|
TX_PENDING_MSG: Notification,
|
||||||
TX_REJECTED_MSG: Notification,
|
TX_REJECTED_MSG: Notification,
|
||||||
TX_EXECUTED_MSG: Notification,
|
TX_EXECUTED_MSG: Notification,
|
||||||
|
TX_CANCELLATION_EXECUTED_MSG: Notification,
|
||||||
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: Notification,
|
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: Notification,
|
||||||
TX_FAILED_MSG: Notification,
|
TX_FAILED_MSG: Notification,
|
||||||
TX_WAITING_MSG: Notification,
|
TX_WAITING_MSG: Notification,
|
||||||
|
@ -57,6 +58,7 @@ export type Notifications = {
|
||||||
SIGN_SETTINGS_CHANGE_MSG: Notification,
|
SIGN_SETTINGS_CHANGE_MSG: Notification,
|
||||||
SETTINGS_CHANGE_PENDING_MSG: Notification,
|
SETTINGS_CHANGE_PENDING_MSG: Notification,
|
||||||
SETTINGS_CHANGE_REJECTED_MSG: Notification,
|
SETTINGS_CHANGE_REJECTED_MSG: Notification,
|
||||||
|
SETTINGS_CHANGE_EXECUTED_MSG: Notification,
|
||||||
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: Notification,
|
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: Notification,
|
||||||
SETTINGS_CHANGE_FAILED_MSG: Notification,
|
SETTINGS_CHANGE_FAILED_MSG: Notification,
|
||||||
|
|
||||||
|
@ -126,6 +128,10 @@ export const NOTIFICATIONS: Notifications = {
|
||||||
message: 'Transaction successfully executed',
|
message: 'Transaction successfully executed',
|
||||||
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
||||||
},
|
},
|
||||||
|
TX_CANCELLATION_EXECUTED_MSG: {
|
||||||
|
message: 'Rejection successfully submitted',
|
||||||
|
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
||||||
|
},
|
||||||
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: {
|
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: {
|
||||||
message: 'Transaction successfully created. More confirmations needed to execute',
|
message: 'Transaction successfully created. More confirmations needed to execute',
|
||||||
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type { Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
|
||||||
export const getAwaitingTransactions = (
|
export const getAwaitingTransactions = (
|
||||||
allTransactions: Map<string, List<Transaction>>,
|
allTransactions: Map<string, List<Transaction>>,
|
||||||
|
cancellationTransactionsByNonce: Map<string | number, List<Transaction>>,
|
||||||
userAccount: string,
|
userAccount: string,
|
||||||
): Map<string, List<Transaction>> => {
|
): Map<string, List<Transaction>> => {
|
||||||
if (!allTransactions) {
|
if (!allTransactions) {
|
||||||
|
@ -16,17 +17,15 @@ export const getAwaitingTransactions = (
|
||||||
// If transactions are not executed, but there's a transaction with the same nonce EXECUTED later
|
// If transactions are not executed, but there's a transaction with the same nonce EXECUTED later
|
||||||
// it means that the transaction was cancelled (Replaced) and shouldn't get executed
|
// it means that the transaction was cancelled (Replaced) and shouldn't get executed
|
||||||
if (!transaction.isExecuted) {
|
if (!transaction.isExecuted) {
|
||||||
const replacementTransaction = safeTransactions.findLast(
|
if (cancellationTransactionsByNonce.get(transaction.nonce)) {
|
||||||
(tx) => tx.isExecuted && tx.nonce === transaction.nonce,
|
|
||||||
)
|
|
||||||
if (replacementTransaction) {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
transaction = transaction.set('cancelled', true)
|
transaction = transaction.set('cancelled', true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The transaction is not executed and is not cancelled, so it's still waiting confirmations
|
// The transaction is not executed and is not cancelled, so it's still waiting confirmations
|
||||||
if (!transaction.executionTxHash && !transaction.cancelled) {
|
if (!transaction.executionTxHash && !transaction.cancelled) {
|
||||||
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this transaction
|
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this
|
||||||
|
// transaction
|
||||||
const transactionWaitingUser = transaction.confirmations.filter(
|
const transactionWaitingUser = transaction.confirmations.filter(
|
||||||
(confirmation) => confirmation.owner && confirmation.owner.address !== userAccount,
|
(confirmation) => confirmation.owner && confirmation.owner.address !== userAccount,
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,6 @@ export type NotifiedTransaction = {
|
||||||
SAFE_NAME_CHANGE_TX: string,
|
SAFE_NAME_CHANGE_TX: string,
|
||||||
OWNER_NAME_CHANGE_TX: string,
|
OWNER_NAME_CHANGE_TX: string,
|
||||||
ADDRESSBOOK_NEW_ENTRY: string,
|
ADDRESSBOOK_NEW_ENTRY: string,
|
||||||
ADDRESSBOOK_EDIT_ENTRY: string,
|
|
||||||
ADDRESSBOOK_DELETE_ENTRY: string,
|
ADDRESSBOOK_DELETE_ENTRY: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,4 +49,4 @@ export const isAddressAToken = async (tokenAddress: string) => {
|
||||||
return call !== '0x'
|
return call !== '0x'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isTokenTransfer = async (data: string, value: number) => data && data.substring(0, 10) === '0xa9059cbb' && value === 0
|
export const isTokenTransfer = (data: string, value: number): boolean => !!data && data.substring(0, 10) === '0xa9059cbb' && value === 0
|
||||||
|
|
|
@ -71,6 +71,7 @@ const Layout = (props: Props) => {
|
||||||
fetchTokens,
|
fetchTokens,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
transactions,
|
transactions,
|
||||||
|
cancellationTransactions,
|
||||||
userAddress,
|
userAddress,
|
||||||
sendFunds,
|
sendFunds,
|
||||||
showReceive,
|
showReceive,
|
||||||
|
@ -190,6 +191,7 @@ const Layout = (props: Props) => {
|
||||||
owners={safe.owners}
|
owners={safe.owners}
|
||||||
nonce={safe.nonce}
|
nonce={safe.nonce}
|
||||||
transactions={transactions}
|
transactions={transactions}
|
||||||
|
cancellationTransactions={cancellationTransactions}
|
||||||
safeAddress={address}
|
safeAddress={address}
|
||||||
userAddress={userAddress}
|
userAddress={userAddress}
|
||||||
currentNetwork={network}
|
currentNetwork={network}
|
||||||
|
|
|
@ -20,7 +20,7 @@ const OwnerAddressTableCell = (props: Props) => {
|
||||||
<Block justify="left">
|
<Block justify="left">
|
||||||
<Identicon address={address} diameter={32} />
|
<Identicon address={address} diameter={32} />
|
||||||
{ showLinks ? (
|
{ showLinks ? (
|
||||||
<div style={{ marginLeft: 10 }}>
|
<div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}>
|
||||||
{ userName }
|
{ userName }
|
||||||
<EtherScanLink type="address" value={address} knownAddress={knownAddress} />
|
<EtherScanLink type="address" value={address} knownAddress={knownAddress} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,11 +21,13 @@ import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
export const APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID = 'approve-tx-modal-submit-btn'
|
export const APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID = 'approve-tx-modal-submit-btn'
|
||||||
|
export const REJECT_TX_MODAL_SUBMIT_BTN_TEST_ID = 'reject-tx-modal-submit-btn'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
|
isCancelTx?: boolean,
|
||||||
processTransaction: Function,
|
processTransaction: Function,
|
||||||
tx: Transaction,
|
tx: Transaction,
|
||||||
nonce: string,
|
nonce: string,
|
||||||
|
@ -38,23 +40,31 @@ type Props = {
|
||||||
closeSnackbar: Function
|
closeSnackbar: Function
|
||||||
}
|
}
|
||||||
|
|
||||||
const getModalTitleAndDescription = (thresholdReached: boolean) => {
|
const getModalTitleAndDescription = (thresholdReached: boolean, isCancelTx?: boolean) => {
|
||||||
const title = thresholdReached ? 'Execute Transaction' : 'Approve Transaction'
|
const modalInfo = {
|
||||||
const description = `This action will ${
|
title: 'Execute Transaction Rejection',
|
||||||
thresholdReached ? 'execute' : 'approve'
|
description: 'This action will execute this transaction.',
|
||||||
} this transaction. A separate transaction will be performed to submit the ${
|
|
||||||
thresholdReached ? 'execution' : 'approval'
|
|
||||||
}.`
|
|
||||||
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCancelTx) {
|
||||||
|
return modalInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thresholdReached) {
|
||||||
|
modalInfo.title = 'Execute Transaction'
|
||||||
|
modalInfo.description = 'This action will execute this transaction. A separate Transaction will be performed to submit the execution.'
|
||||||
|
} else {
|
||||||
|
modalInfo.title = 'Approve Transaction'
|
||||||
|
modalInfo.description = 'This action will approve this transaction. A separate Transaction will be performed to submit the approval.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return modalInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
const ApproveTxModal = ({
|
const ApproveTxModal = ({
|
||||||
onClose,
|
onClose,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
isCancelTx,
|
||||||
classes,
|
classes,
|
||||||
processTransaction,
|
processTransaction,
|
||||||
tx,
|
tx,
|
||||||
|
@ -68,7 +78,7 @@ const ApproveTxModal = ({
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [approveAndExecute, setApproveAndExecute] = useState<boolean>(canExecute)
|
const [approveAndExecute, setApproveAndExecute] = useState<boolean>(canExecute)
|
||||||
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
||||||
const { title, description } = getModalTitleAndDescription(thresholdReached)
|
const { title, description } = getModalTitleAndDescription(thresholdReached, isCancelTx)
|
||||||
const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
|
const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
|
||||||
const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached
|
const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached
|
||||||
|
|
||||||
|
@ -132,7 +142,7 @@ const ApproveTxModal = ({
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block className={classes.container}>
|
<Block className={classes.container}>
|
||||||
<Row>
|
<Row style={{ flexDirection: 'column' }}>
|
||||||
<Paragraph>{description}</Paragraph>
|
<Paragraph>{description}</Paragraph>
|
||||||
<Paragraph size="sm" color="medium">
|
<Paragraph size="sm" color="medium">
|
||||||
Transaction nonce:
|
Transaction nonce:
|
||||||
|
@ -142,10 +152,11 @@ const ApproveTxModal = ({
|
||||||
{oneConfirmationLeft && canExecute && (
|
{oneConfirmationLeft && canExecute && (
|
||||||
<>
|
<>
|
||||||
<Paragraph color="error">
|
<Paragraph color="error">
|
||||||
Approving this transaction executes it right away. If you want
|
Approving this transaction executes it right away.
|
||||||
approve but execute the transaction manually later, click on the
|
{!isCancelTx && ' If you want approve but execute the transaction manually later, click on the '
|
||||||
checkbox below.
|
+ 'checkbox below.'}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
{!isCancelTx && (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={(
|
control={(
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -156,6 +167,7 @@ const ApproveTxModal = ({
|
||||||
)}
|
)}
|
||||||
label="Execute transaction"
|
label="Execute transaction"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -176,9 +188,9 @@ const ApproveTxModal = ({
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minWidth={214}
|
minWidth={214}
|
||||||
minHeight={42}
|
minHeight={42}
|
||||||
color="primary"
|
color={isCancelTx ? 'secondary' : 'primary'}
|
||||||
onClick={approveTx}
|
onClick={approveTx}
|
||||||
testId={APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID}
|
testId={isCancelTx ? REJECT_TX_MODAL_SUBMIT_BTN_TEST_ID : APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react'
|
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
|
||||||
import Row from '~/components/layout/Row'
|
|
||||||
import Button from '~/components/layout/Button'
|
|
||||||
import { xl, sm, border } from '~/theme/variables'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
classes: Object,
|
|
||||||
onTxCancel: Function,
|
|
||||||
showCancelBtn: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = () => ({
|
|
||||||
buttonRow: {
|
|
||||||
borderTop: `2px solid ${border}`,
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
padding: '10px 20px',
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
height: xl,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
width: '14px',
|
|
||||||
height: '14px',
|
|
||||||
marginRight: sm,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const ButtonRow = ({
|
|
||||||
classes,
|
|
||||||
onTxCancel,
|
|
||||||
showCancelBtn,
|
|
||||||
}: Props) => (
|
|
||||||
<Row align="end" className={classes.buttonRow}>
|
|
||||||
{showCancelBtn && (
|
|
||||||
<Button className={classes.button} variant="contained" minWidth={140} color="secondary" onClick={onTxCancel}>
|
|
||||||
Cancel tx
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default withStyles(styles)(ButtonRow)
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import cn from 'classnames'
|
||||||
import Button from '~/components/layout/Button'
|
import Button from '~/components/layout/Button'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
import EtherscanLink from '~/components/EtherscanLink'
|
import EtherscanLink from '~/components/EtherscanLink'
|
||||||
|
@ -9,58 +10,70 @@ import Block from '~/components/layout/Block'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
import ConfirmSmallGreyIcon from './assets/confirm-small-grey.svg'
|
import ConfirmSmallGreyCircle from './assets/confirm-small-grey.svg'
|
||||||
import ConfirmSmallGreenIcon from './assets/confirm-small-green.svg'
|
import ConfirmSmallGreenCircle from './assets/confirm-small-green.svg'
|
||||||
import ConfirmSmallFilledIcon from './assets/confirm-small-filled.svg'
|
import ConfirmSmallFilledCircle from './assets/confirm-small-filled.svg'
|
||||||
|
import ConfirmSmallRedCircle from './assets/confirm-small-red.svg'
|
||||||
|
import CancelSmallFilledCircle from './assets/cancel-small-filled.svg'
|
||||||
import { getNameFromAddressBook } from '~/logic/addressBook/utils'
|
import { getNameFromAddressBook } from '~/logic/addressBook/utils'
|
||||||
|
|
||||||
export const CONFIRM_TX_BTN_TEST_ID = 'confirm-btn'
|
export const CONFIRM_TX_BTN_TEST_ID = 'confirm-btn'
|
||||||
export const EXECUTE_TX_BTN_TEST_ID = 'execute-btn'
|
export const EXECUTE_TX_BTN_TEST_ID = 'execute-btn'
|
||||||
|
export const REJECT_TX_BTN_TEST_ID = 'reject-btn'
|
||||||
|
export const EXECUTE_REJECT_TX_BTN_TEST_ID = 'execute-reject-btn'
|
||||||
|
|
||||||
type OwnerProps = {
|
type OwnerProps = {
|
||||||
owner: Owner,
|
|
||||||
classes: Object,
|
classes: Object,
|
||||||
userAddress: string,
|
|
||||||
confirmed?: boolean,
|
confirmed?: boolean,
|
||||||
executor?: string,
|
executor?: string,
|
||||||
thresholdReached: boolean,
|
isCancelTx?: boolean,
|
||||||
|
onTxReject?: Function,
|
||||||
|
onTxConfirm: Function,
|
||||||
|
onTxExecute: Function,
|
||||||
|
owner: Owner,
|
||||||
|
showRejectBtn: boolean,
|
||||||
|
showExecuteRejectBtn: boolean,
|
||||||
showConfirmBtn: boolean,
|
showConfirmBtn: boolean,
|
||||||
showExecuteBtn: boolean,
|
showExecuteBtn: boolean,
|
||||||
onTxConfirm: Function,
|
thresholdReached: boolean,
|
||||||
onTxExecute: Function
|
userAddress: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const OwnerComponent = ({
|
const OwnerComponent = ({
|
||||||
owner,
|
onTxReject,
|
||||||
userAddress,
|
|
||||||
classes,
|
classes,
|
||||||
|
confirmed,
|
||||||
|
executor,
|
||||||
|
isCancelTx,
|
||||||
onTxConfirm,
|
onTxConfirm,
|
||||||
|
onTxExecute,
|
||||||
|
owner,
|
||||||
|
showRejectBtn,
|
||||||
|
showExecuteRejectBtn,
|
||||||
showConfirmBtn,
|
showConfirmBtn,
|
||||||
showExecuteBtn,
|
showExecuteBtn,
|
||||||
onTxExecute,
|
|
||||||
executor,
|
|
||||||
confirmed,
|
|
||||||
thresholdReached,
|
thresholdReached,
|
||||||
|
userAddress,
|
||||||
}: OwnerProps) => {
|
}: OwnerProps) => {
|
||||||
const nameInAdbk = getNameFromAddressBook(owner.address)
|
const nameInAdbk = getNameFromAddressBook(owner.address)
|
||||||
const ownerName = nameInAdbk || owner.name
|
const ownerName = nameInAdbk || owner.name
|
||||||
|
const [imgCircle, setImgCircle] = React.useState(ConfirmSmallGreyCircle)
|
||||||
|
|
||||||
|
React.useMemo(() => {
|
||||||
|
if (confirmed) {
|
||||||
|
setImgCircle(isCancelTx ? CancelSmallFilledCircle : ConfirmSmallFilledCircle)
|
||||||
|
} else if (thresholdReached || executor) {
|
||||||
|
setImgCircle(isCancelTx ? ConfirmSmallRedCircle : ConfirmSmallGreenCircle)
|
||||||
|
}
|
||||||
|
}, [confirmed, thresholdReached, executor, isCancelTx])
|
||||||
|
|
||||||
|
const getTimelineLine = () => (isCancelTx ? classes.verticalLineCancel : classes.verticalLineDone)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Block className={classes.container}>
|
<Block className={classes.container}>
|
||||||
<div
|
<div className={cn(classes.verticalLine, (confirmed || thresholdReached || executor) && getTimelineLine())} />
|
||||||
className={
|
<div className={classes.circleState}>
|
||||||
confirmed || thresholdReached || executor
|
<Img src={imgCircle} alt="" />
|
||||||
? classes.verticalLineProgressDone
|
|
||||||
: classes.verticalLineProgressPending
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className={classes.iconState}>
|
|
||||||
{confirmed ? (
|
|
||||||
<Img src={ConfirmSmallFilledIcon} />
|
|
||||||
) : thresholdReached || executor ? (
|
|
||||||
<Img src={ConfirmSmallGreenIcon} />
|
|
||||||
) : (
|
|
||||||
<Img src={ConfirmSmallGreyIcon} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Identicon address={owner.address} diameter={32} className={classes.icon} />
|
<Identicon address={owner.address} diameter={32} className={classes.icon} />
|
||||||
<Block>
|
<Block>
|
||||||
|
@ -75,30 +88,61 @@ const OwnerComponent = ({
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
<Block className={classes.spacer} />
|
<Block className={classes.spacer} />
|
||||||
{showConfirmBtn && owner.address === userAddress && (
|
{owner.address === userAddress && (
|
||||||
|
<Block>
|
||||||
|
{isCancelTx ? (
|
||||||
|
<>
|
||||||
|
{showRejectBtn && (
|
||||||
|
<Button
|
||||||
|
className={cn(classes.button, classes.lastButton)}
|
||||||
|
color="secondary"
|
||||||
|
onClick={onTxReject}
|
||||||
|
testId={REJECT_TX_BTN_TEST_ID}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showExecuteRejectBtn && (
|
||||||
|
<Button
|
||||||
|
className={cn(classes.button, classes.lastButton)}
|
||||||
|
color="secondary"
|
||||||
|
onClick={onTxReject}
|
||||||
|
testId={EXECUTE_REJECT_TX_BTN_TEST_ID}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Execute
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{showConfirmBtn && (
|
||||||
<Button
|
<Button
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
variant="contained"
|
|
||||||
minWidth={140}
|
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={onTxConfirm}
|
onClick={onTxConfirm}
|
||||||
testId={CONFIRM_TX_BTN_TEST_ID}
|
testId={CONFIRM_TX_BTN_TEST_ID}
|
||||||
|
variant="contained"
|
||||||
>
|
>
|
||||||
Confirm tx
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{showExecuteBtn && owner.address === userAddress && (
|
{showExecuteBtn && (
|
||||||
<Button
|
<Button
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
variant="contained"
|
|
||||||
minWidth={140}
|
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={onTxExecute}
|
onClick={onTxExecute}
|
||||||
testId={EXECUTE_TX_BTN_TEST_ID}
|
testId={EXECUTE_TX_BTN_TEST_ID}
|
||||||
|
variant="contained"
|
||||||
>
|
>
|
||||||
Execute tx
|
Execute
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Block>
|
||||||
|
)}
|
||||||
{owner.address === executor && (
|
{owner.address === executor && (
|
||||||
<Block className={classes.executor}>Executor</Block>
|
<Block className={classes.executor}>Executor</Block>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -7,56 +7,72 @@ import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
type ListProps = {
|
type ListProps = {
|
||||||
ownersWhoConfirmed: List<Owner>,
|
|
||||||
ownersUnconfirmed: List<Owner>,
|
|
||||||
classes: Object,
|
classes: Object,
|
||||||
userAddress: string,
|
|
||||||
executor: string,
|
executor: string,
|
||||||
thresholdReached: boolean,
|
isCancelTx?: boolean,
|
||||||
showConfirmBtn: boolean,
|
onTxReject?: Function,
|
||||||
showExecuteBtn: boolean,
|
|
||||||
onTxConfirm: Function,
|
onTxConfirm: Function,
|
||||||
onTxExecute: Function,
|
onTxExecute: Function,
|
||||||
|
ownersUnconfirmed: List<Owner>,
|
||||||
|
ownersWhoConfirmed: List<Owner>,
|
||||||
|
showRejectBtn: boolean,
|
||||||
|
showExecuteRejectBtn: boolean,
|
||||||
|
showConfirmBtn: boolean,
|
||||||
|
showExecuteBtn: boolean,
|
||||||
|
thresholdReached: boolean,
|
||||||
|
userAddress: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const OwnersList = ({
|
const OwnersList = ({
|
||||||
userAddress,
|
|
||||||
ownersWhoConfirmed,
|
|
||||||
ownersUnconfirmed,
|
|
||||||
classes,
|
classes,
|
||||||
executor,
|
executor,
|
||||||
thresholdReached,
|
isCancelTx,
|
||||||
showConfirmBtn,
|
onTxReject,
|
||||||
showExecuteBtn,
|
|
||||||
onTxConfirm,
|
onTxConfirm,
|
||||||
onTxExecute,
|
onTxExecute,
|
||||||
|
ownersUnconfirmed,
|
||||||
|
ownersWhoConfirmed,
|
||||||
|
showRejectBtn,
|
||||||
|
showExecuteRejectBtn,
|
||||||
|
showConfirmBtn,
|
||||||
|
showExecuteBtn,
|
||||||
|
thresholdReached,
|
||||||
|
userAddress,
|
||||||
}: ListProps) => (
|
}: ListProps) => (
|
||||||
<>
|
<>
|
||||||
{ownersWhoConfirmed.map((owner) => (
|
{ownersWhoConfirmed.map((owner) => (
|
||||||
<OwnerComponent
|
<OwnerComponent
|
||||||
key={owner.address}
|
|
||||||
owner={owner}
|
|
||||||
classes={classes}
|
classes={classes}
|
||||||
userAddress={userAddress}
|
|
||||||
executor={executor}
|
|
||||||
thresholdReached={thresholdReached}
|
|
||||||
confirmed
|
confirmed
|
||||||
showExecuteBtn={showExecuteBtn}
|
executor={executor}
|
||||||
|
isCancelTx={isCancelTx}
|
||||||
|
key={owner.address}
|
||||||
|
onTxReject={onTxReject}
|
||||||
onTxExecute={onTxExecute}
|
onTxExecute={onTxExecute}
|
||||||
|
owner={owner}
|
||||||
|
showRejectBtn={showRejectBtn}
|
||||||
|
showExecuteRejectBtn={showExecuteRejectBtn}
|
||||||
|
showExecuteBtn={showExecuteBtn}
|
||||||
|
thresholdReached={thresholdReached}
|
||||||
|
userAddress={userAddress}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{ownersUnconfirmed.map((owner) => (
|
{ownersUnconfirmed.map((owner) => (
|
||||||
<OwnerComponent
|
<OwnerComponent
|
||||||
key={owner.address}
|
|
||||||
owner={owner}
|
|
||||||
classes={classes}
|
classes={classes}
|
||||||
userAddress={userAddress}
|
|
||||||
executor={executor}
|
executor={executor}
|
||||||
thresholdReached={thresholdReached}
|
isCancelTx={isCancelTx}
|
||||||
|
key={owner.address}
|
||||||
|
onTxReject={onTxReject}
|
||||||
onTxConfirm={onTxConfirm}
|
onTxConfirm={onTxConfirm}
|
||||||
|
onTxExecute={onTxExecute}
|
||||||
|
owner={owner}
|
||||||
|
showRejectBtn={showRejectBtn}
|
||||||
|
showExecuteRejectBtn={showExecuteRejectBtn}
|
||||||
showConfirmBtn={showConfirmBtn}
|
showConfirmBtn={showConfirmBtn}
|
||||||
showExecuteBtn={showExecuteBtn}
|
showExecuteBtn={showExecuteBtn}
|
||||||
onTxExecute={onTxExecute}
|
thresholdReached={thresholdReached}
|
||||||
|
userAddress={userAddress}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
|
||||||
|
<circle cx="5" cy="5" r="5" fill="#f02525" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 160 B |
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path fill="#f02525" d="M10 0C4.489 0 0 4.489 0 10s4.489 10 10 10 10-4.489 10-10S15.511 0 10 0z"/>
|
||||||
|
<path fill="#FFF" d="M9.124 13.75L5 9.406l1.245-1.312 2.88 3.034 4.63-4.878L15 7.561z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 344 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||||
|
<circle cx="10" cy="10" r="9" fill="#FFFAF4" fill-rule="evenodd" stroke="#f02525" stroke-width="2"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 196 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
|
||||||
|
<circle cx="5" cy="5" r="4" fill="#FFFAF4" fill-rule="evenodd" stroke="#f02525" stroke-width="2"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 194 B |
|
@ -7,71 +7,111 @@ import Block from '~/components/layout/Block'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
import {
|
||||||
|
makeTransaction,
|
||||||
|
type Transaction,
|
||||||
|
} from '~/routes/safe/store/models/transaction'
|
||||||
import { TX_TYPE_CONFIRMATION } from '~/logic/safe/transactions/send'
|
import { TX_TYPE_CONFIRMATION } from '~/logic/safe/transactions/send'
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
|
||||||
import OwnersList from './OwnersList'
|
import OwnersList from './OwnersList'
|
||||||
import ButtonRow from './ButtonRow'
|
import CheckLargeFilledGreenCircle from './assets/check-large-filled-green.svg'
|
||||||
import CheckLargeFilledGreenIcon from './assets/check-large-filled-green.svg'
|
import ConfirmLargeGreenCircle from './assets/confirm-large-green.svg'
|
||||||
import ConfirmLargeGreenIcon from './assets/confirm-large-green.svg'
|
import CheckLargeFilledRedCircle from './assets/check-large-filled-red.svg'
|
||||||
import ConfirmLargeGreyIcon from './assets/confirm-large-grey.svg'
|
import ConfirmLargeRedCircle from './assets/confirm-large-red.svg'
|
||||||
|
import ConfirmLargeGreyCircle from './assets/confirm-large-grey.svg'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
import Paragraph from '~/components/layout/Paragraph/index'
|
import Paragraph from '~/components/layout/Paragraph/index'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tx: Transaction,
|
canExecute: boolean,
|
||||||
owners: List<Owner>,
|
canExecuteCancel: boolean,
|
||||||
|
cancelThresholdReached: boolean,
|
||||||
|
cancelTx: Transaction,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
granted: boolean,
|
granted: boolean,
|
||||||
threshold: number,
|
onTxReject: Function,
|
||||||
userAddress: string,
|
|
||||||
thresholdReached: boolean,
|
|
||||||
safeAddress: string,
|
|
||||||
canExecute: boolean,
|
|
||||||
onTxConfirm: Function,
|
onTxConfirm: Function,
|
||||||
onTxCancel: Function,
|
onTxExecute: Function,
|
||||||
onTxExecute: Function
|
owners: List<Owner>,
|
||||||
|
threshold: number,
|
||||||
|
thresholdReached: boolean,
|
||||||
|
tx: Transaction,
|
||||||
|
userAddress: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getOwnersConfirmations(tx, userAddress) {
|
||||||
|
const ownersWhoConfirmed = []
|
||||||
|
let currentUserAlreadyConfirmed = false
|
||||||
|
|
||||||
|
tx.confirmations.forEach((conf) => {
|
||||||
|
if (conf.owner.address === userAddress) {
|
||||||
|
currentUserAlreadyConfirmed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCancellationTransaction = (tx: Transaction, safeAddress: string) => !tx.value && tx.data === EMPTY_DATA && tx.recipient === safeAddress
|
if (conf.type === TX_TYPE_CONFIRMATION) {
|
||||||
|
ownersWhoConfirmed.push(conf.owner)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [ownersWhoConfirmed, currentUserAlreadyConfirmed]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPendingOwnersConfirmations(owners, tx, userAddress) {
|
||||||
|
const ownersUnconfirmed = owners.filter(
|
||||||
|
(owner) => tx.confirmations.findIndex(
|
||||||
|
(conf) => conf.owner.address === owner.address,
|
||||||
|
) === -1,
|
||||||
|
)
|
||||||
|
|
||||||
|
let userIsUnconfirmedOwner = false
|
||||||
|
|
||||||
|
ownersUnconfirmed.some((owner) => {
|
||||||
|
userIsUnconfirmedOwner = owner.address === userAddress
|
||||||
|
return userIsUnconfirmedOwner
|
||||||
|
})
|
||||||
|
|
||||||
|
return [ownersUnconfirmed, userIsUnconfirmedOwner]
|
||||||
|
}
|
||||||
|
|
||||||
const OwnersColumn = ({
|
const OwnersColumn = ({
|
||||||
tx,
|
tx,
|
||||||
|
cancelTx = makeTransaction(),
|
||||||
owners,
|
owners,
|
||||||
classes,
|
classes,
|
||||||
granted,
|
granted,
|
||||||
threshold,
|
threshold,
|
||||||
userAddress,
|
userAddress,
|
||||||
thresholdReached,
|
thresholdReached,
|
||||||
safeAddress,
|
cancelThresholdReached,
|
||||||
onTxConfirm,
|
onTxConfirm,
|
||||||
onTxCancel,
|
|
||||||
onTxExecute,
|
onTxExecute,
|
||||||
|
onTxReject,
|
||||||
canExecute,
|
canExecute,
|
||||||
|
canExecuteCancel,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const cancellationTx = isCancellationTransaction(tx, safeAddress)
|
let showOlderTxAnnotation: boolean
|
||||||
const showOlderTxAnnotation = thresholdReached && !canExecute && !tx.isExecuted
|
|
||||||
|
|
||||||
const ownersWhoConfirmed = []
|
if (tx.isExecuted || cancelTx.isExecuted) {
|
||||||
let currentUserAlreadyConfirmed = false
|
showOlderTxAnnotation = false
|
||||||
tx.confirmations.forEach((conf) => {
|
} else {
|
||||||
if (conf.owner.address === userAddress) {
|
showOlderTxAnnotation = (thresholdReached && !canExecute) || (cancelThresholdReached && !canExecuteCancel)
|
||||||
currentUserAlreadyConfirmed = true
|
|
||||||
}
|
}
|
||||||
if (conf.type === TX_TYPE_CONFIRMATION) {
|
|
||||||
ownersWhoConfirmed.push(conf.owner)
|
const [
|
||||||
}
|
ownersWhoConfirmed,
|
||||||
})
|
currentUserAlreadyConfirmed,
|
||||||
const ownersUnconfirmed = owners.filter(
|
] = getOwnersConfirmations(tx, userAddress)
|
||||||
(owner) => tx.confirmations.findIndex(
|
const [
|
||||||
(conf) => conf.owner.address === owner.address,
|
ownersUnconfirmed,
|
||||||
) === -1,
|
userIsUnconfirmedOwner,
|
||||||
)
|
] = getPendingOwnersConfirmations(owners, tx, userAddress)
|
||||||
let userIsUnconfirmedOwner
|
const [
|
||||||
ownersUnconfirmed.some((owner) => {
|
ownersWhoConfirmedCancel,
|
||||||
userIsUnconfirmedOwner = owner.address === userAddress
|
currentUserAlreadyConfirmedCancel,
|
||||||
return userIsUnconfirmedOwner
|
] = getOwnersConfirmations(cancelTx, userAddress)
|
||||||
})
|
const [
|
||||||
|
ownersUnconfirmedCancel,
|
||||||
|
userIsUnconfirmedCancelOwner,
|
||||||
|
] = getPendingOwnersConfirmations(owners, cancelTx, userAddress)
|
||||||
|
|
||||||
let displayButtonRow = true
|
let displayButtonRow = true
|
||||||
if (tx.executionTxHash) {
|
if (tx.executionTxHash) {
|
||||||
|
@ -80,13 +120,7 @@ const OwnersColumn = ({
|
||||||
} else if (tx.status === 'cancelled') {
|
} else if (tx.status === 'cancelled') {
|
||||||
// tx is cancelled (replaced) by another one
|
// tx is cancelled (replaced) by another one
|
||||||
displayButtonRow = false
|
displayButtonRow = false
|
||||||
} else if (
|
} else if (currentUserAlreadyConfirmedCancel) {
|
||||||
cancellationTx
|
|
||||||
&& currentUserAlreadyConfirmed
|
|
||||||
&& !thresholdReached
|
|
||||||
) {
|
|
||||||
// the TX is the cancellation (replacement) transaction for previous TX,
|
|
||||||
// current user has already confirmed it and threshold is not reached (so he can't execute/cancel it)
|
|
||||||
displayButtonRow = false
|
displayButtonRow = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,6 +133,19 @@ const OwnersColumn = ({
|
||||||
|
|
||||||
const showExecuteBtn = canExecute && !tx.isExecuted && thresholdReached
|
const showExecuteBtn = canExecute && !tx.isExecuted && thresholdReached
|
||||||
|
|
||||||
|
const showRejectBtn = !cancelTx.isExecuted
|
||||||
|
&& !tx.isExecuted
|
||||||
|
&& cancelTx.status !== 'pending'
|
||||||
|
&& userIsUnconfirmedCancelOwner
|
||||||
|
&& !currentUserAlreadyConfirmedCancel
|
||||||
|
&& !cancelThresholdReached
|
||||||
|
&& displayButtonRow
|
||||||
|
|
||||||
|
const showExecuteRejectBtn = !cancelTx.isExecuted && !tx.isExecuted && canExecuteCancel && cancelThresholdReached
|
||||||
|
|
||||||
|
const txThreshold = cancelTx.isExecuted ? tx.confirmations.size : threshold
|
||||||
|
const cancelThreshold = tx.isExecuted ? cancelTx.confirmations.size : threshold
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col xs={6} className={classes.rightCol} layout="block">
|
<Col xs={6} className={classes.rightCol} layout="block">
|
||||||
<Block
|
<Block
|
||||||
|
@ -107,54 +154,85 @@ const OwnersColumn = ({
|
||||||
(thresholdReached || tx.isExecuted) && classes.ownerListTitleDone,
|
(thresholdReached || tx.isExecuted) && classes.ownerListTitleDone,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={classes.iconState}>
|
<div className={classes.circleState}>
|
||||||
{thresholdReached || tx.isExecuted ? (
|
<Img src={thresholdReached || tx.isExecuted ? CheckLargeFilledGreenCircle : ConfirmLargeGreenCircle} alt="" />
|
||||||
<Img src={CheckLargeFilledGreenIcon} />
|
|
||||||
) : (
|
|
||||||
<Img src={ConfirmLargeGreenIcon} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{tx.isExecuted
|
{tx.isExecuted
|
||||||
? `Confirmed [${tx.confirmations.size}/${tx.confirmations.size}]`
|
? `Confirmed [${tx.confirmations.size}/${tx.confirmations.size}]`
|
||||||
: `Confirmed [${tx.confirmations.size}/${threshold}]`}
|
: `Confirmed [${tx.confirmations.size}/${txThreshold}]`}
|
||||||
</Block>
|
</Block>
|
||||||
<OwnersList
|
<OwnersList
|
||||||
userAddress={userAddress}
|
|
||||||
ownersWhoConfirmed={ownersWhoConfirmed}
|
|
||||||
ownersUnconfirmed={ownersUnconfirmed}
|
|
||||||
executor={tx.executor}
|
executor={tx.executor}
|
||||||
thresholdReached={thresholdReached}
|
|
||||||
onTxConfirm={onTxConfirm}
|
onTxConfirm={onTxConfirm}
|
||||||
onTxExecute={onTxExecute}
|
onTxExecute={onTxExecute}
|
||||||
|
ownersUnconfirmed={ownersUnconfirmed}
|
||||||
|
ownersWhoConfirmed={ownersWhoConfirmed}
|
||||||
showConfirmBtn={showConfirmBtn}
|
showConfirmBtn={showConfirmBtn}
|
||||||
showExecuteBtn={showExecuteBtn}
|
showExecuteBtn={showExecuteBtn}
|
||||||
|
thresholdReached={thresholdReached}
|
||||||
|
userAddress={userAddress}
|
||||||
/>
|
/>
|
||||||
|
{/* Cancel TX thread - START */}
|
||||||
|
<Block
|
||||||
|
className={cn(
|
||||||
|
classes.ownerListTitle,
|
||||||
|
(cancelThresholdReached || cancelTx.isExecuted) && classes.ownerListTitleCancelDone,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
classes.verticalLine,
|
||||||
|
tx.isExecuted ? classes.verticalLineDone : classes.verticalLinePending,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className={classes.circleState}>
|
||||||
|
<Img src={cancelThresholdReached || cancelTx.isExecuted ? CheckLargeFilledRedCircle : ConfirmLargeRedCircle} alt="" />
|
||||||
|
</div>
|
||||||
|
{cancelTx.isExecuted
|
||||||
|
? `Rejected [${cancelTx.confirmations.size}/${cancelTx.confirmations.size}]`
|
||||||
|
: `Rejected [${cancelTx.confirmations.size}/${cancelThreshold}]`}
|
||||||
|
</Block>
|
||||||
|
<OwnersList
|
||||||
|
isCancelTx
|
||||||
|
executor={cancelTx.executor}
|
||||||
|
onTxReject={onTxReject}
|
||||||
|
ownersUnconfirmed={ownersUnconfirmedCancel}
|
||||||
|
ownersWhoConfirmed={ownersWhoConfirmedCancel}
|
||||||
|
showRejectBtn={showRejectBtn}
|
||||||
|
showExecuteRejectBtn={showExecuteRejectBtn}
|
||||||
|
thresholdReached={cancelThresholdReached}
|
||||||
|
userAddress={userAddress}
|
||||||
|
/>
|
||||||
|
{/* Cancel TX thread - END */}
|
||||||
<Block
|
<Block
|
||||||
className={cn(
|
className={cn(
|
||||||
classes.ownerListTitle,
|
classes.ownerListTitle,
|
||||||
tx.isExecuted && classes.ownerListTitleDone,
|
tx.isExecuted && classes.ownerListTitleDone,
|
||||||
|
cancelTx.isExecuted && classes.ownerListTitleCancelDone,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={
|
className={cn(
|
||||||
thresholdReached || tx.isExecuted
|
classes.verticalLine,
|
||||||
? classes.verticalLineProgressDone
|
tx.isExecuted && classes.verticalLineDone,
|
||||||
: classes.verticalLineProgressPending
|
cancelTx.isExecuted && classes.verticalLineCancel,
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className={classes.iconState}>
|
|
||||||
{!thresholdReached && !tx.isExecuted && (
|
|
||||||
<Img src={ConfirmLargeGreyIcon} alt="Confirm tx" />
|
|
||||||
)}
|
)}
|
||||||
{thresholdReached && !tx.isExecuted && (
|
/>
|
||||||
<Img src={ConfirmLargeGreenIcon} alt="Execute tx" />
|
|
||||||
|
<div className={classes.circleState}>
|
||||||
|
{!tx.isExecuted && !cancelTx.isExecuted && (
|
||||||
|
<Img src={ConfirmLargeGreyCircle} alt="Confirm / Execute tx" />
|
||||||
)}
|
)}
|
||||||
{tx.isExecuted && (
|
{tx.isExecuted && (
|
||||||
<Img src={CheckLargeFilledGreenIcon} alt="TX Executed icon" />
|
<Img src={CheckLargeFilledGreenCircle} alt="TX Executed icon" />
|
||||||
|
)}
|
||||||
|
{cancelTx.isExecuted && (
|
||||||
|
<Img src={CheckLargeFilledRedCircle} alt="TX Executed icon" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
Executed
|
Executed
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
{showOlderTxAnnotation && (
|
{showOlderTxAnnotation && (
|
||||||
<Block className={classes.olderTxAnnotation}>
|
<Block className={classes.olderTxAnnotation}>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
|
@ -162,9 +240,6 @@ const OwnersColumn = ({
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
{granted && displayButtonRow && (
|
|
||||||
<ButtonRow onTxCancel={onTxCancel} showCancelBtn={!cancellationTx} />
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,36 @@
|
||||||
// @flow
|
// @flow
|
||||||
import {
|
import {
|
||||||
border, sm, boldFont, primary, secondary, secondaryText,
|
border, sm, boldFont, primary, secondary, secondaryText, error,
|
||||||
} from '~/theme/variables'
|
} from '~/theme/variables'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = () => ({
|
||||||
ownersList: {
|
ownersList: {
|
||||||
width: '100%',
|
|
||||||
padding: 0,
|
|
||||||
height: '192px',
|
height: '192px',
|
||||||
overflowY: 'scroll',
|
overflowY: 'scroll',
|
||||||
|
padding: 0,
|
||||||
|
width: '100%',
|
||||||
},
|
},
|
||||||
rightCol: {
|
rightCol: {
|
||||||
boxSizing: 'border-box',
|
|
||||||
borderLeft: `2px solid ${border}`,
|
borderLeft: `2px solid ${border}`,
|
||||||
|
boxSizing: 'border-box',
|
||||||
},
|
},
|
||||||
verticalLineProgressPending: {
|
verticalLine: {
|
||||||
|
backgroundColor: secondaryText,
|
||||||
|
height: '55px',
|
||||||
|
left: '27px',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
borderLeft: `2px solid ${secondaryText}`,
|
top: '-27px',
|
||||||
height: '52px',
|
width: '2px',
|
||||||
top: '-26px',
|
|
||||||
left: '29px',
|
|
||||||
zIndex: '10',
|
zIndex: '10',
|
||||||
},
|
},
|
||||||
verticalLineProgressDone: {
|
verticalLinePending: {
|
||||||
position: 'absolute',
|
backgroundColor: secondaryText,
|
||||||
borderLeft: `2px solid ${secondary}`,
|
},
|
||||||
height: '52px',
|
verticalLineDone: {
|
||||||
top: '-26px',
|
backgroundColor: secondary,
|
||||||
left: '29px',
|
},
|
||||||
zIndex: '10',
|
verticalLineCancel: {
|
||||||
|
backgroundColor: error,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
marginRight: sm,
|
marginRight: sm,
|
||||||
|
@ -37,21 +39,21 @@ export const styles = () => ({
|
||||||
borderBottom: `1px solid ${border}`,
|
borderBottom: `1px solid ${border}`,
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
position: 'relative',
|
alignItems: 'center',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
padding: '5px 20px',
|
padding: '13px 15px 13px 18px',
|
||||||
|
position: 'relative',
|
||||||
},
|
},
|
||||||
ownerListTitle: {
|
ownerListTitle: {
|
||||||
position: 'relative',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '15px',
|
display: 'flex',
|
||||||
paddingLeft: '20px',
|
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
fontWeight: boldFont,
|
fontWeight: boldFont,
|
||||||
lineHeight: 1.27,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '1px',
|
letterSpacing: '1px',
|
||||||
|
lineHeight: 1.3,
|
||||||
|
padding: '15px 15px 15px 18px',
|
||||||
|
position: 'relative',
|
||||||
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
olderTxAnnotation: {
|
olderTxAnnotation: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
@ -59,10 +61,13 @@ export const styles = () => ({
|
||||||
ownerListTitleDone: {
|
ownerListTitleDone: {
|
||||||
color: secondary,
|
color: secondary,
|
||||||
},
|
},
|
||||||
|
ownerListTitleCancelDone: {
|
||||||
|
color: error,
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
overflow: 'hidden',
|
|
||||||
height: '15px',
|
height: '15px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
},
|
},
|
||||||
address: {
|
address: {
|
||||||
height: '20px',
|
height: '20px',
|
||||||
|
@ -70,26 +75,37 @@ export const styles = () => ({
|
||||||
spacer: {
|
spacer: {
|
||||||
flex: 'auto',
|
flex: 'auto',
|
||||||
},
|
},
|
||||||
iconState: {
|
circleState: {
|
||||||
width: '20px',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: '10px',
|
marginRight: '18px',
|
||||||
|
width: '20px',
|
||||||
zIndex: '100',
|
zIndex: '100',
|
||||||
|
|
||||||
'& > img': {
|
'& > img': {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
justifyContent: 'center',
|
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
flexGrow: '0',
|
||||||
|
fontSize: '16px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingLeft: '14px',
|
||||||
|
paddingRight: '14px',
|
||||||
|
},
|
||||||
|
lastButton: {
|
||||||
|
marginLeft: '10px',
|
||||||
},
|
},
|
||||||
executor: {
|
executor: {
|
||||||
borderRadius: '3px',
|
|
||||||
padding: '3px 5px',
|
|
||||||
background: border,
|
|
||||||
color: primary,
|
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
background: border,
|
||||||
|
borderRadius: '3px',
|
||||||
|
color: primary,
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
|
height: '24px',
|
||||||
|
lineHeight: '24px',
|
||||||
|
padding: '0 12px',
|
||||||
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -30,7 +30,7 @@ type Props = {
|
||||||
closeSnackbar: Function,
|
closeSnackbar: Function,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CancelTxModal = ({
|
const RejectTxModal = ({
|
||||||
onClose,
|
onClose,
|
||||||
isOpen,
|
isOpen,
|
||||||
classes,
|
classes,
|
||||||
|
@ -78,15 +78,14 @@ const CancelTxModal = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Cancel Transaction"
|
title="Reject Transaction"
|
||||||
description="Cancel Transaction"
|
description="Reject Transaction"
|
||||||
handleClose={onClose}
|
handleClose={onClose}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
// paperClassName={cn(smallerModalSize && classes.smallerModalWindow)}
|
|
||||||
>
|
>
|
||||||
<Row align="center" grow className={classes.heading}>
|
<Row align="center" grow className={classes.heading}>
|
||||||
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
||||||
Cancel transaction
|
Reject transaction
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<IconButton onClick={onClose} disableRipple>
|
<IconButton onClick={onClose} disableRipple>
|
||||||
<Close className={classes.closeIcon} />
|
<Close className={classes.closeIcon} />
|
||||||
|
@ -96,8 +95,7 @@ const CancelTxModal = ({
|
||||||
<Block className={classes.container}>
|
<Block className={classes.container}>
|
||||||
<Row>
|
<Row>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
This action will cancel this transaction. A separate transaction will be performed to submit the
|
This action will cancel this transaction. A separate transaction will be performed to submit the rejection.
|
||||||
cancellation.
|
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph size="sm" color="medium">
|
<Paragraph size="sm" color="medium">
|
||||||
Transaction nonce:
|
Transaction nonce:
|
||||||
|
@ -123,11 +121,11 @@ const CancelTxModal = ({
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={sendReplacementTransaction}
|
onClick={sendReplacementTransaction}
|
||||||
>
|
>
|
||||||
Cancel Transaction
|
Reject Transaction
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(CancelTxModal))
|
export default withStyles(styles)(withSnackbar(RejectTxModal))
|
|
@ -15,7 +15,7 @@ import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
import TxDescription from './TxDescription'
|
import TxDescription from './TxDescription'
|
||||||
import OwnersColumn from './OwnersColumn'
|
import OwnersColumn from './OwnersColumn'
|
||||||
import CancelTxModal from './CancelTxModal'
|
import RejectTxModal from './RejectTxModal'
|
||||||
import ApproveTxModal from './ApproveTxModal'
|
import ApproveTxModal from './ApproveTxModal'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
import { formatDate } from '../columns'
|
import { formatDate } from '../columns'
|
||||||
|
@ -24,6 +24,7 @@ import { INCOMING_TX_TYPE } from '~/routes/safe/store/models/incomingTransaction
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tx: Transaction,
|
tx: Transaction,
|
||||||
|
cancelTx: Transaction,
|
||||||
threshold: number,
|
threshold: number,
|
||||||
owners: List<Owner>,
|
owners: List<Owner>,
|
||||||
granted: boolean,
|
granted: boolean,
|
||||||
|
@ -34,12 +35,13 @@ type Props = {
|
||||||
nonce: number
|
nonce: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenModal = "cancelTx" | "approveTx" | null
|
type OpenModal = "rejectTx" | "approveTx" | "executeRejectTx" | null
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const ExpandedTx = ({
|
const ExpandedTx = ({
|
||||||
tx,
|
tx,
|
||||||
|
cancelTx,
|
||||||
threshold,
|
threshold,
|
||||||
owners,
|
owners,
|
||||||
granted,
|
granted,
|
||||||
|
@ -52,10 +54,19 @@ const ExpandedTx = ({
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [openModal, setOpenModal] = useState<OpenModal>(null)
|
const [openModal, setOpenModal] = useState<OpenModal>(null)
|
||||||
const openApproveModal = () => setOpenModal('approveTx')
|
const openApproveModal = () => setOpenModal('approveTx')
|
||||||
const openCancelModal = () => setOpenModal('cancelTx')
|
|
||||||
const closeModal = () => setOpenModal(null)
|
const closeModal = () => setOpenModal(null)
|
||||||
const thresholdReached = tx.type !== INCOMING_TX_TYPE && threshold <= tx.confirmations.size
|
const thresholdReached = tx.type !== INCOMING_TX_TYPE && threshold <= tx.confirmations.size
|
||||||
const canExecute = tx.type !== INCOMING_TX_TYPE && nonce === tx.nonce
|
const canExecute = tx.type !== INCOMING_TX_TYPE && nonce === tx.nonce
|
||||||
|
const cancelThresholdReached = !!cancelTx && threshold <= cancelTx.confirmations.size
|
||||||
|
const canExecuteCancel = nonce === tx.nonce
|
||||||
|
|
||||||
|
const openRejectModal = () => {
|
||||||
|
if (!!cancelTx && nonce === cancelTx.nonce) {
|
||||||
|
setOpenModal('executeRejectTx')
|
||||||
|
} else {
|
||||||
|
setOpenModal('rejectTx')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -136,29 +147,23 @@ const ExpandedTx = ({
|
||||||
{tx.type !== INCOMING_TX_TYPE && (
|
{tx.type !== INCOMING_TX_TYPE && (
|
||||||
<OwnersColumn
|
<OwnersColumn
|
||||||
tx={tx}
|
tx={tx}
|
||||||
|
cancelTx={cancelTx}
|
||||||
owners={owners}
|
owners={owners}
|
||||||
granted={granted}
|
granted={granted}
|
||||||
canExecute={canExecute}
|
canExecute={canExecute}
|
||||||
|
canExecuteCancel={canExecuteCancel}
|
||||||
threshold={threshold}
|
threshold={threshold}
|
||||||
userAddress={userAddress}
|
userAddress={userAddress}
|
||||||
thresholdReached={thresholdReached}
|
thresholdReached={thresholdReached}
|
||||||
|
cancelThresholdReached={cancelThresholdReached}
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
onTxConfirm={openApproveModal}
|
onTxConfirm={openApproveModal}
|
||||||
onTxCancel={openCancelModal}
|
|
||||||
onTxExecute={openApproveModal}
|
onTxExecute={openApproveModal}
|
||||||
|
onTxReject={openRejectModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
{openModal === 'cancelTx' && (
|
|
||||||
<CancelTxModal
|
|
||||||
isOpen
|
|
||||||
createTransaction={createTransaction}
|
|
||||||
onClose={closeModal}
|
|
||||||
tx={tx}
|
|
||||||
safeAddress={safeAddress}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{openModal === 'approveTx' && (
|
{openModal === 'approveTx' && (
|
||||||
<ApproveTxModal
|
<ApproveTxModal
|
||||||
isOpen
|
isOpen
|
||||||
|
@ -172,6 +177,29 @@ const ExpandedTx = ({
|
||||||
thresholdReached={thresholdReached}
|
thresholdReached={thresholdReached}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{openModal === 'rejectTx' && (
|
||||||
|
<RejectTxModal
|
||||||
|
isOpen
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
onClose={closeModal}
|
||||||
|
tx={tx}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{openModal === 'executeRejectTx' && (
|
||||||
|
<ApproveTxModal
|
||||||
|
isOpen
|
||||||
|
isCancelTx
|
||||||
|
processTransaction={processTransaction}
|
||||||
|
onClose={closeModal}
|
||||||
|
canExecute={canExecuteCancel}
|
||||||
|
tx={cancelTx}
|
||||||
|
userAddress={userAddress}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
threshold={threshold}
|
||||||
|
thresholdReached={cancelThresholdReached}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,9 @@ export const styles = () => ({
|
||||||
padding: `${lg} ${md}`,
|
padding: `${lg} ${md}`,
|
||||||
},
|
},
|
||||||
txData: {
|
txData: {
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
lineHeight: '1.6',
|
||||||
},
|
},
|
||||||
awaiting_your_confirmation: {
|
awaiting_your_confirmation: {
|
||||||
color: disabled,
|
color: disabled,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// @flow
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { makeTransaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { getTxTableData, TX_TABLE_RAW_CANCEL_TX_ID } from '~/routes/safe/components/Transactions/TxsTable/columns'
|
||||||
|
|
||||||
|
describe('TxsTable Columns > getTxTableData', () => {
|
||||||
|
it('should include CancelTx object inside TxTableData', () => {
|
||||||
|
// Given
|
||||||
|
const mockedTransaction = makeTransaction({ nonce: 1, blockNumber: 100 })
|
||||||
|
const mockedCancelTransaction = makeTransaction({ nonce: 1, blockNumber: 123 })
|
||||||
|
|
||||||
|
// When
|
||||||
|
const txTableData = getTxTableData(List([mockedTransaction]), List([mockedCancelTransaction]))
|
||||||
|
const txRow = txTableData.first()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction)
|
||||||
|
})
|
||||||
|
it('should not include CancelTx object inside TxTableData', () => {
|
||||||
|
// Given
|
||||||
|
const mockedTransaction = makeTransaction({ nonce: 1, blockNumber: 100 })
|
||||||
|
const mockedCancelTransaction = makeTransaction({ nonce: 2, blockNumber: 123 })
|
||||||
|
|
||||||
|
// When
|
||||||
|
const txTableData = getTxTableData(List([mockedTransaction]), List([mockedCancelTransaction]))
|
||||||
|
const txRow = txTableData.first()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,7 +2,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { format, getTime, parseISO } from 'date-fns'
|
import { format, getTime, parseISO } from 'date-fns'
|
||||||
import { BigNumber } from 'bignumber.js'
|
import { BigNumber } from 'bignumber.js'
|
||||||
import { List } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import TxType from './TxType'
|
import TxType from './TxType'
|
||||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
import { INCOMING_TX_TYPE, type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
import { INCOMING_TX_TYPE, type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||||
|
@ -16,10 +16,11 @@ export const TX_TABLE_DATE_ID = 'date'
|
||||||
export const TX_TABLE_AMOUNT_ID = 'amount'
|
export const TX_TABLE_AMOUNT_ID = 'amount'
|
||||||
export const TX_TABLE_STATUS_ID = 'status'
|
export const TX_TABLE_STATUS_ID = 'status'
|
||||||
export const TX_TABLE_RAW_TX_ID = 'tx'
|
export const TX_TABLE_RAW_TX_ID = 'tx'
|
||||||
|
export const TX_TABLE_RAW_CANCEL_TX_ID = 'cancelTx'
|
||||||
export const TX_TABLE_EXPAND_ICON = 'expand'
|
export const TX_TABLE_EXPAND_ICON = 'expand'
|
||||||
|
|
||||||
type TxData = {
|
type TxData = {
|
||||||
id: number,
|
id: ?number,
|
||||||
type: React.ReactNode,
|
type: React.ReactNode,
|
||||||
date: string,
|
date: string,
|
||||||
dateOrder?: number,
|
dateOrder?: number,
|
||||||
|
@ -63,7 +64,10 @@ const getIncomingTxTableData = (tx: IncomingTransaction): TransactionRow => ({
|
||||||
[TX_TABLE_RAW_TX_ID]: tx,
|
[TX_TABLE_RAW_TX_ID]: tx,
|
||||||
})
|
})
|
||||||
|
|
||||||
const getTransactionTableData = (tx: Transaction): TransactionRow => {
|
const getTransactionTableData = (
|
||||||
|
tx: Transaction,
|
||||||
|
cancelTx: ?Transaction,
|
||||||
|
): TransactionRow => {
|
||||||
const txDate = tx.submissionDate
|
const txDate = tx.submissionDate
|
||||||
|
|
||||||
let txType = 'outgoing'
|
let txType = 'outgoing'
|
||||||
|
@ -85,16 +89,27 @@ const getTransactionTableData = (tx: Transaction): TransactionRow => {
|
||||||
[TX_TABLE_AMOUNT_ID]: getTxAmount(tx),
|
[TX_TABLE_AMOUNT_ID]: getTxAmount(tx),
|
||||||
[TX_TABLE_STATUS_ID]: tx.status,
|
[TX_TABLE_STATUS_ID]: tx.status,
|
||||||
[TX_TABLE_RAW_TX_ID]: tx,
|
[TX_TABLE_RAW_TX_ID]: tx,
|
||||||
|
[TX_TABLE_RAW_CANCEL_TX_ID]: cancelTx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTxTableData = (transactions: List<Transaction | IncomingTransaction>): List<TransactionRow> => transactions.map((tx) => {
|
export const getTxTableData = (
|
||||||
|
transactions: List<Transaction | IncomingTransaction>,
|
||||||
|
cancelTxs: List<Transaction>,
|
||||||
|
): List<TransactionRow> => {
|
||||||
|
const cancelTxsByNonce = cancelTxs.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
|
||||||
|
|
||||||
|
return transactions.map((tx) => {
|
||||||
if (tx.type === INCOMING_TX_TYPE) {
|
if (tx.type === INCOMING_TX_TYPE) {
|
||||||
return getIncomingTxTableData(tx)
|
return getIncomingTxTableData(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getTransactionTableData(tx)
|
return getTransactionTableData(
|
||||||
|
tx,
|
||||||
|
Number.isInteger(Number.parseInt(tx.nonce, 10)) ? cancelTxsByNonce.get(tx.nonce) : undefined,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const generateColumns = () => {
|
export const generateColumns = () => {
|
||||||
const nonceColumn: Column = {
|
const nonceColumn: Column = {
|
||||||
|
|
|
@ -21,10 +21,12 @@ import {
|
||||||
generateColumns,
|
generateColumns,
|
||||||
TX_TABLE_ID,
|
TX_TABLE_ID,
|
||||||
TX_TABLE_RAW_TX_ID,
|
TX_TABLE_RAW_TX_ID,
|
||||||
|
TX_TABLE_RAW_CANCEL_TX_ID,
|
||||||
type TransactionRow,
|
type TransactionRow,
|
||||||
} from './columns'
|
} from './columns'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
import Status from './Status'
|
import Status from './Status'
|
||||||
|
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||||
|
|
||||||
export const TRANSACTION_ROW_TEST_ID = 'transaction-row'
|
export const TRANSACTION_ROW_TEST_ID = 'transaction-row'
|
||||||
|
|
||||||
|
@ -35,7 +37,8 @@ const expandCellStyle = {
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
transactions: List<Transaction>,
|
transactions: List<Transaction | IncomingTransaction>,
|
||||||
|
cancellationTransactions: List<Transaction>,
|
||||||
threshold: number,
|
threshold: number,
|
||||||
owners: List<Owner>,
|
owners: List<Owner>,
|
||||||
userAddress: string,
|
userAddress: string,
|
||||||
|
@ -49,6 +52,7 @@ type Props = {
|
||||||
const TxsTable = ({
|
const TxsTable = ({
|
||||||
classes,
|
classes,
|
||||||
transactions,
|
transactions,
|
||||||
|
cancellationTransactions,
|
||||||
threshold,
|
threshold,
|
||||||
owners,
|
owners,
|
||||||
granted,
|
granted,
|
||||||
|
@ -66,7 +70,7 @@ const TxsTable = ({
|
||||||
|
|
||||||
const columns = generateColumns()
|
const columns = generateColumns()
|
||||||
const autoColumns = columns.filter((c) => !c.custom)
|
const autoColumns = columns.filter((c) => !c.custom)
|
||||||
const filteredData = getTxTableData(transactions)
|
const filteredData = getTxTableData(transactions, cancellationTransactions)
|
||||||
.sort(({ dateOrder: a }, { dateOrder: b }) => {
|
.sort(({ dateOrder: a }, { dateOrder: b }) => {
|
||||||
if (!a || !b) {
|
if (!a || !b) {
|
||||||
return 0
|
return 0
|
||||||
|
@ -132,6 +136,7 @@ const TxsTable = ({
|
||||||
component={ExpandedTxComponent}
|
component={ExpandedTxComponent}
|
||||||
unmountOnExit
|
unmountOnExit
|
||||||
tx={row[TX_TABLE_RAW_TX_ID]}
|
tx={row[TX_TABLE_RAW_TX_ID]}
|
||||||
|
cancelTx={row[TX_TABLE_RAW_CANCEL_TX_ID]}
|
||||||
threshold={threshold}
|
threshold={threshold}
|
||||||
owners={owners}
|
owners={owners}
|
||||||
granted={granted}
|
granted={granted}
|
||||||
|
|
|
@ -10,6 +10,7 @@ type Props = {
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
threshold: number,
|
threshold: number,
|
||||||
transactions: List<Transaction | IncomingTransaction>,
|
transactions: List<Transaction | IncomingTransaction>,
|
||||||
|
cancellationTransactions: List<Transaction>,
|
||||||
owners: List<Owner>,
|
owners: List<Owner>,
|
||||||
userAddress: string,
|
userAddress: string,
|
||||||
granted: boolean,
|
granted: boolean,
|
||||||
|
@ -21,6 +22,7 @@ type Props = {
|
||||||
|
|
||||||
const Transactions = ({
|
const Transactions = ({
|
||||||
transactions = List(),
|
transactions = List(),
|
||||||
|
cancellationTransactions = List(),
|
||||||
owners,
|
owners,
|
||||||
threshold,
|
threshold,
|
||||||
userAddress,
|
userAddress,
|
||||||
|
@ -33,6 +35,7 @@ const Transactions = ({
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<TxsTable
|
<TxsTable
|
||||||
transactions={transactions}
|
transactions={transactions}
|
||||||
|
cancellationTransactions={cancellationTransactions}
|
||||||
threshold={threshold}
|
threshold={threshold}
|
||||||
owners={owners}
|
owners={owners}
|
||||||
userAddress={userAddress}
|
userAddress={userAddress}
|
||||||
|
|
|
@ -137,6 +137,7 @@ class SafeView extends React.Component<Props, State> {
|
||||||
fetchTokens,
|
fetchTokens,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
transactions,
|
transactions,
|
||||||
|
cancellationTransactions,
|
||||||
currencySelected,
|
currencySelected,
|
||||||
fetchCurrencyValues,
|
fetchCurrencyValues,
|
||||||
currencyValues,
|
currencyValues,
|
||||||
|
@ -161,6 +162,7 @@ class SafeView extends React.Component<Props, State> {
|
||||||
fetchTokens={fetchTokens}
|
fetchTokens={fetchTokens}
|
||||||
updateSafe={updateSafe}
|
updateSafe={updateSafe}
|
||||||
transactions={transactions}
|
transactions={transactions}
|
||||||
|
cancellationTransactions={cancellationTransactions}
|
||||||
sendFunds={sendFunds}
|
sendFunds={sendFunds}
|
||||||
showReceive={showReceive}
|
showReceive={showReceive}
|
||||||
onShow={this.onShow}
|
onShow={this.onShow}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
safeBalancesSelector,
|
safeBalancesSelector,
|
||||||
safeBlacklistedTokensSelector,
|
safeBlacklistedTokensSelector,
|
||||||
safeTransactionsSelector,
|
safeTransactionsSelector,
|
||||||
|
safeCancellationTransactionsSelector,
|
||||||
safeIncomingTransactionsSelector,
|
safeIncomingTransactionsSelector,
|
||||||
type RouterProps,
|
type RouterProps,
|
||||||
type SafeSelectorProps,
|
type SafeSelectorProps,
|
||||||
|
@ -38,6 +39,7 @@ export type SelectorProps = {
|
||||||
currencySelected: string,
|
currencySelected: string,
|
||||||
currencyValues: BalanceCurrencyType[],
|
currencyValues: BalanceCurrencyType[],
|
||||||
transactions: List<Transaction | IncomingTransaction>,
|
transactions: List<Transaction | IncomingTransaction>,
|
||||||
|
cancellationTransactions: List<Transaction>,
|
||||||
addressBook: AddressBook,
|
addressBook: AddressBook,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,21 +110,15 @@ const extendedTransactionsSelector: Selector<GlobalState, RouterProps, List<Tran
|
||||||
safeSelector,
|
safeSelector,
|
||||||
userAccountSelector,
|
userAccountSelector,
|
||||||
safeTransactionsSelector,
|
safeTransactionsSelector,
|
||||||
|
safeCancellationTransactionsSelector,
|
||||||
safeIncomingTransactionsSelector,
|
safeIncomingTransactionsSelector,
|
||||||
(safe, userAddress, transactions, incomingTransactions) => {
|
(safe, userAddress, transactions, cancellationTransactions, incomingTransactions) => {
|
||||||
|
const cancellationTransactionsByNonce = cancellationTransactions.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
|
||||||
const extendedTransactions = transactions.map((tx: Transaction) => {
|
const extendedTransactions = transactions.map((tx: Transaction) => {
|
||||||
let extendedTx = tx
|
let extendedTx = tx
|
||||||
|
|
||||||
// If transactions are not executed, but there's a transaction with the same nonce submitted later
|
|
||||||
// it means that the transaction was cancelled (Replaced) and shouldn't get executed
|
|
||||||
let replacementTransaction
|
|
||||||
if (!tx.isExecuted) {
|
if (!tx.isExecuted) {
|
||||||
replacementTransaction = transactions.size > 1 && transactions.findLast(
|
if (cancellationTransactionsByNonce.get(tx.nonce) && cancellationTransactionsByNonce.get(tx.nonce).get('isExecuted')) {
|
||||||
(transaction) => (
|
|
||||||
transaction.isExecuted && transaction.nonce && transaction.nonce >= tx.nonce
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if (replacementTransaction) {
|
|
||||||
extendedTx = tx.set('cancelled', true)
|
extendedTx = tx.set('cancelled', true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,6 +141,7 @@ export default createStructuredSelector<Object, *>({
|
||||||
network: networkSelector,
|
network: networkSelector,
|
||||||
safeUrl: safeParamAddressSelector,
|
safeUrl: safeParamAddressSelector,
|
||||||
transactions: extendedTransactionsSelector,
|
transactions: extendedTransactionsSelector,
|
||||||
|
cancellationTransactions: safeCancellationTransactionsSelector,
|
||||||
currencySelected: currentCurrencySelector,
|
currencySelected: currentCurrencySelector,
|
||||||
currencyValues: currencyValuesListSelector,
|
currencyValues: currencyValuesListSelector,
|
||||||
addressBook: getAddressBook,
|
addressBook: getAddressBook,
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
export const ADD_CANCELLATION_TRANSACTIONS = 'ADD_CANCELLATION_TRANSACTIONS'
|
||||||
|
|
||||||
|
export const addCancellationTransactions = createAction<string, *>(ADD_CANCELLATION_TRANSACTIONS)
|
|
@ -78,7 +78,9 @@ const createTransaction = ({
|
||||||
const from = userAccountSelector(state)
|
const from = userAccountSelector(state)
|
||||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
const threshold = await safeInstance.getThreshold()
|
const threshold = await safeInstance.getThreshold()
|
||||||
const nonce = txNonce || await getLastPendingTxNonce(safeAddress)
|
const nonce = !Number.isInteger(Number.parseInt(txNonce, 10))
|
||||||
|
? await getLastPendingTxNonce(safeAddress)
|
||||||
|
: txNonce
|
||||||
const isExecution = threshold.toNumber() === 1 || shouldExecute
|
const isExecution = threshold.toNumber() === 1 || shouldExecute
|
||||||
|
|
||||||
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map, type RecordInstance } from 'immutable'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import bn from 'bignumber.js'
|
import bn from 'bignumber.js'
|
||||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
|
@ -20,6 +20,8 @@ import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
import { isTokenTransfer } from '~/logic/tokens/utils/tokenHelpers'
|
import { isTokenTransfer } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
|
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
|
||||||
import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi'
|
import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi'
|
||||||
|
import type { TransactionProps } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { addCancellationTransactions } from '~/routes/safe/store/actions/addCancellationTransactions'
|
||||||
|
|
||||||
let web3
|
let web3
|
||||||
|
|
||||||
|
@ -33,22 +35,23 @@ type ConfirmationServiceModel = {
|
||||||
type TxServiceModel = {
|
type TxServiceModel = {
|
||||||
to: string,
|
to: string,
|
||||||
value: number,
|
value: number,
|
||||||
data: string,
|
data: ?string,
|
||||||
operation: number,
|
operation: number,
|
||||||
nonce: number,
|
nonce: ?number,
|
||||||
blockNumber: number,
|
blockNumber: ?number,
|
||||||
safeTxGas: number,
|
safeTxGas: number,
|
||||||
baseGas: number,
|
baseGas: number,
|
||||||
gasPrice: number,
|
gasPrice: number,
|
||||||
gasToken: string,
|
gasToken: string,
|
||||||
refundReceiver: string,
|
refundReceiver: string,
|
||||||
safeTxHash: string,
|
safeTxHash: string,
|
||||||
submissionDate: string,
|
submissionDate: ?string,
|
||||||
executor: string,
|
executor: string,
|
||||||
executionDate: string,
|
executionDate: ?string,
|
||||||
confirmations: ConfirmationServiceModel[],
|
confirmations: ConfirmationServiceModel[],
|
||||||
isExecuted: boolean,
|
isExecuted: boolean,
|
||||||
transactionHash: string,
|
transactionHash: ?string,
|
||||||
|
creationTx?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type IncomingTxServiceModel = {
|
type IncomingTxServiceModel = {
|
||||||
|
@ -63,7 +66,7 @@ type IncomingTxServiceModel = {
|
||||||
export const buildTransactionFrom = async (
|
export const buildTransactionFrom = async (
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
tx: TxServiceModel,
|
tx: TxServiceModel,
|
||||||
) => {
|
): Promise<Transaction> => {
|
||||||
const { owners } = await getLocalSafe(safeAddress)
|
const { owners } = await getLocalSafe(safeAddress)
|
||||||
|
|
||||||
const confirmations = List(
|
const confirmations = List(
|
||||||
|
@ -88,7 +91,7 @@ export const buildTransactionFrom = async (
|
||||||
)
|
)
|
||||||
const modifySettingsTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !!tx.data
|
const modifySettingsTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !!tx.data
|
||||||
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 isSendTokenTx = await isTokenTransfer(tx.data, Number(tx.value))
|
const isSendTokenTx = isTokenTransfer(tx.data, Number(tx.value))
|
||||||
const customTx = !sameAddress(tx.to, safeAddress) && !!tx.data && !isSendTokenTx
|
const customTx = !sameAddress(tx.to, safeAddress) && !!tx.data && !isSendTokenTx
|
||||||
|
|
||||||
let refundParams = null
|
let refundParams = null
|
||||||
|
@ -173,7 +176,8 @@ export const buildTransactionFrom = async (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addMockSafeCreationTx = (safeAddress) => [{
|
const addMockSafeCreationTx = (safeAddress): Array<TxServiceModel> => [{
|
||||||
|
blockNumber: null,
|
||||||
baseGas: 0,
|
baseGas: 0,
|
||||||
confirmations: [],
|
confirmations: [],
|
||||||
data: null,
|
data: null,
|
||||||
|
@ -233,8 +237,14 @@ export const buildIncomingTransactionFrom = async (tx: IncomingTxServiceModel) =
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadSafeTransactions = async (safeAddress: string) => {
|
export type SafeTransactionsType = {
|
||||||
|
outgoing: Map<string, List<TransactionProps>>,
|
||||||
|
cancel: Map<string, List<TransactionProps>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadSafeTransactions = async (safeAddress: string): Promise<SafeTransactionsType> => {
|
||||||
let transactions: TxServiceModel[] = addMockSafeCreationTx(safeAddress)
|
let transactions: TxServiceModel[] = addMockSafeCreationTx(safeAddress)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = buildTxServiceUrl(safeAddress)
|
const url = buildTxServiceUrl(safeAddress)
|
||||||
const response = await axios.get(url)
|
const response = await axios.get(url)
|
||||||
|
@ -245,11 +255,16 @@ export const loadSafeTransactions = async (safeAddress: string) => {
|
||||||
console.error(`Requests for outgoing transactions for ${safeAddress} failed with 404`, err)
|
console.error(`Requests for outgoing transactions for ${safeAddress} failed with 404`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const txsRecord = await Promise.all(
|
const txsRecord: Array<RecordInstance<TransactionProps>> = await Promise.all(
|
||||||
transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx)),
|
transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Map().set(safeAddress, List(txsRecord))
|
const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
outgoing: Map().set(safeAddress, groupedTxs.get('outgoing')),
|
||||||
|
cancel: Map().set(safeAddress, groupedTxs.get('cancel')),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadSafeIncomingTransactions = async (safeAddress: string) => {
|
export const loadSafeIncomingTransactions = async (safeAddress: string) => {
|
||||||
|
@ -272,9 +287,10 @@ export const loadSafeIncomingTransactions = async (safeAddress: string) => {
|
||||||
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
web3 = await getWeb3()
|
web3 = await getWeb3()
|
||||||
|
|
||||||
const transactions: Map<string, List<Transaction>> = await loadSafeTransactions(safeAddress)
|
const { outgoing, cancel }: SafeTransactionsType = await loadSafeTransactions(safeAddress)
|
||||||
const incomingTransactions: Map<string, List<IncomingTransaction>> = await loadSafeIncomingTransactions(safeAddress)
|
const incomingTransactions: Map<string, List<IncomingTransaction>> = await loadSafeIncomingTransactions(safeAddress)
|
||||||
|
|
||||||
dispatch(addTransactions(transactions))
|
dispatch(addCancellationTransactions(cancel))
|
||||||
|
dispatch(addTransactions(outgoing))
|
||||||
dispatch(addIncomingTransactions(incomingTransactions))
|
dispatch(addIncomingTransactions(incomingTransactions))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Action, Store } from 'redux'
|
import type { Action, Store } from 'redux'
|
||||||
import { List } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { push } from 'connected-react-router'
|
import { push } from 'connected-react-router'
|
||||||
import { type GlobalState } from '~/store/'
|
import { type GlobalState } from '~/store/'
|
||||||
import { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/addTransactions'
|
import { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/addTransactions'
|
||||||
|
@ -32,8 +32,11 @@ const notificationsMiddleware = (store: Store<GlobalState>) => (
|
||||||
const transactionsList = action.payload
|
const transactionsList = action.payload
|
||||||
const userAddress: string = userAccountSelector(state)
|
const userAddress: string = userAccountSelector(state)
|
||||||
const safeAddress = action.payload.keySeq().get(0)
|
const safeAddress = action.payload.keySeq().get(0)
|
||||||
|
const cancellationTransactions = state.cancellationTransactions.get(safeAddress)
|
||||||
|
const cancellationTransactionsByNonce = cancellationTransactions ? cancellationTransactions.reduce((acc, tx) => acc.set(tx.nonce, tx), Map()) : Map()
|
||||||
const awaitingTransactions = getAwaitingTransactions(
|
const awaitingTransactions = getAwaitingTransactions(
|
||||||
transactionsList,
|
transactionsList,
|
||||||
|
cancellationTransactionsByNonce,
|
||||||
userAddress,
|
userAddress,
|
||||||
)
|
)
|
||||||
const awaitingTransactionsList = awaitingTransactions.get(
|
const awaitingTransactionsList = awaitingTransactions.get(
|
||||||
|
|
|
@ -18,6 +18,27 @@ export type IncomingTransactionProps = {
|
||||||
executionDate: string,
|
executionDate: string,
|
||||||
type: string,
|
type: string,
|
||||||
status: string,
|
status: string,
|
||||||
|
nonce: null,
|
||||||
|
confirmations: null,
|
||||||
|
recipient: null,
|
||||||
|
data: null,
|
||||||
|
operation: null,
|
||||||
|
safeTxGas: null,
|
||||||
|
baseGas: null,
|
||||||
|
gasPrice: null,
|
||||||
|
gasToken: null,
|
||||||
|
refundReceiver: null,
|
||||||
|
isExecuted: null,
|
||||||
|
submissionDate: null,
|
||||||
|
executor: null,
|
||||||
|
cancelled: null,
|
||||||
|
modifySettingsTx: null,
|
||||||
|
cancellationTx: null,
|
||||||
|
customTx: null,
|
||||||
|
creationTx: null,
|
||||||
|
isTokenTransfer: null,
|
||||||
|
decodedParams: null,
|
||||||
|
refundParams: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeIncomingTransaction: RecordFactory<IncomingTransactionProps> = Record({
|
export const makeIncomingTransaction: RecordFactory<IncomingTransactionProps> = Record({
|
||||||
|
@ -34,6 +55,27 @@ export const makeIncomingTransaction: RecordFactory<IncomingTransactionProps> =
|
||||||
executionDate: '',
|
executionDate: '',
|
||||||
type: INCOMING_TX_TYPE,
|
type: INCOMING_TX_TYPE,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
nonce: null,
|
||||||
|
confirmations: null,
|
||||||
|
recipient: null,
|
||||||
|
data: null,
|
||||||
|
operation: null,
|
||||||
|
safeTxGas: null,
|
||||||
|
baseGas: null,
|
||||||
|
gasPrice: null,
|
||||||
|
gasToken: null,
|
||||||
|
refundReceiver: null,
|
||||||
|
isExecuted: null,
|
||||||
|
submissionDate: null,
|
||||||
|
executor: null,
|
||||||
|
cancelled: null,
|
||||||
|
modifySettingsTx: null,
|
||||||
|
cancellationTx: null,
|
||||||
|
customTx: null,
|
||||||
|
creationTx: null,
|
||||||
|
isTokenTransfer: null,
|
||||||
|
decodedParams: null,
|
||||||
|
refundParams: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type IncomingTransaction = RecordOf<IncomingTransactionProps>
|
export type IncomingTransaction = RecordOf<IncomingTransactionProps>
|
||||||
|
|
|
@ -17,8 +17,8 @@ export type TransactionStatus =
|
||||||
| 'pending'
|
| 'pending'
|
||||||
|
|
||||||
export type TransactionProps = {
|
export type TransactionProps = {
|
||||||
nonce: number,
|
nonce: ?number,
|
||||||
blockNumber: number,
|
blockNumber: ?number,
|
||||||
value: string,
|
value: string,
|
||||||
confirmations: List<Confirmation>,
|
confirmations: List<Confirmation>,
|
||||||
recipient: string,
|
recipient: string,
|
||||||
|
@ -30,8 +30,8 @@ export type TransactionProps = {
|
||||||
gasToken: string,
|
gasToken: string,
|
||||||
refundReceiver: string,
|
refundReceiver: string,
|
||||||
isExecuted: boolean,
|
isExecuted: boolean,
|
||||||
submissionDate: string,
|
submissionDate: ?string,
|
||||||
executionDate: string,
|
executionDate: ?string,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
modifySettingsTx: boolean,
|
modifySettingsTx: boolean,
|
||||||
cancellationTx: boolean,
|
cancellationTx: boolean,
|
||||||
|
@ -39,7 +39,7 @@ export type TransactionProps = {
|
||||||
creationTx: boolean,
|
creationTx: boolean,
|
||||||
safeTxHash: string,
|
safeTxHash: string,
|
||||||
executor: string,
|
executor: string,
|
||||||
executionTxHash?: string,
|
executionTxHash?: ?string,
|
||||||
decimals?: number,
|
decimals?: number,
|
||||||
cancelled?: boolean,
|
cancelled?: boolean,
|
||||||
status?: TransactionStatus,
|
status?: TransactionStatus,
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
// @flow
|
||||||
|
import { List, Map } from 'immutable'
|
||||||
|
import { handleActions, type ActionType } from 'redux-actions'
|
||||||
|
import { ADD_CANCELLATION_TRANSACTIONS } from '~/routes/safe/store/actions/addCancellationTransactions'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
|
||||||
|
export const CANCELLATION_TRANSACTIONS_REDUCER_ID = 'cancellationTransactions'
|
||||||
|
|
||||||
|
export type CancelState = Map<string, List<Transaction>>
|
||||||
|
|
||||||
|
export default handleActions<CancelState, *>(
|
||||||
|
{
|
||||||
|
[ADD_CANCELLATION_TRANSACTIONS]: (state: CancelState, action: ActionType<Function>): CancelState => action.payload,
|
||||||
|
},
|
||||||
|
Map(),
|
||||||
|
)
|
|
@ -6,6 +6,10 @@ import { type GlobalState } from '~/store/index'
|
||||||
import { SAFE_PARAM_ADDRESS, SAFELIST_ADDRESS } from '~/routes/routes'
|
import { SAFE_PARAM_ADDRESS, SAFELIST_ADDRESS } from '~/routes/routes'
|
||||||
import { type Safe } from '~/routes/safe/store/models/safe'
|
import { type Safe } from '~/routes/safe/store/models/safe'
|
||||||
import { type State as TransactionsState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
|
import { type State as TransactionsState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
|
||||||
|
import {
|
||||||
|
type CancelState as CancelTransactionsState,
|
||||||
|
CANCELLATION_TRANSACTIONS_REDUCER_ID,
|
||||||
|
} from '~/routes/safe/store/reducer/cancellationTransactions'
|
||||||
import {
|
import {
|
||||||
type IncomingState as IncomingTransactionsState,
|
type IncomingState as IncomingTransactionsState,
|
||||||
INCOMING_TRANSACTIONS_REDUCER_ID,
|
INCOMING_TRANSACTIONS_REDUCER_ID,
|
||||||
|
@ -48,13 +52,21 @@ export const defaultSafeSelector: Selector<GlobalState, {}, string> = createSele
|
||||||
|
|
||||||
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
||||||
|
|
||||||
const incomingTransactionsSelector = (state: GlobalState): IncomingTransactionsState => state[INCOMING_TRANSACTIONS_REDUCER_ID]
|
const cancellationTransactionsSelector = (state: GlobalState): CancelTransactionsState => state[
|
||||||
|
CANCELLATION_TRANSACTIONS_REDUCER_ID
|
||||||
|
]
|
||||||
|
|
||||||
|
const incomingTransactionsSelector = (state: GlobalState): IncomingTransactionsState => state[
|
||||||
|
INCOMING_TRANSACTIONS_REDUCER_ID
|
||||||
|
]
|
||||||
|
|
||||||
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
||||||
|
|
||||||
export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
|
export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
|
||||||
|
|
||||||
export const safeTransactionsSelector: Selector<GlobalState, RouterProps, List<Transaction>> = createSelector(
|
type TxSelectorType = Selector<GlobalState, RouterProps, List<Transaction>>
|
||||||
|
|
||||||
|
export const safeTransactionsSelector: TxSelectorType = createSelector(
|
||||||
transactionsSelector,
|
transactionsSelector,
|
||||||
safeParamAddressSelector,
|
safeParamAddressSelector,
|
||||||
(transactions: TransactionsState, address: string): List<Transaction> => {
|
(transactions: TransactionsState, address: string): List<Transaction> => {
|
||||||
|
@ -80,6 +92,22 @@ export const addressBookQueryParamsSelector = (state: GlobalState): string => {
|
||||||
return entryAddressToEditOrCreateNew
|
return entryAddressToEditOrCreateNew
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const safeCancellationTransactionsSelector: TxSelectorType = createSelector(
|
||||||
|
cancellationTransactionsSelector,
|
||||||
|
safeParamAddressSelector,
|
||||||
|
(cancellationTransactions: TransactionsState, address: string): List<Transaction> => {
|
||||||
|
if (!cancellationTransactions) {
|
||||||
|
return List([])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
return List([])
|
||||||
|
}
|
||||||
|
|
||||||
|
return cancellationTransactions.get(address) || List([])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const safeParamAddressFromStateSelector = (state: GlobalState): string => {
|
export const safeParamAddressFromStateSelector = (state: GlobalState): string => {
|
||||||
const match = matchPath(
|
const match = matchPath(
|
||||||
state.router.location.pathname,
|
state.router.location.pathname,
|
||||||
|
@ -89,7 +117,9 @@ export const safeParamAddressFromStateSelector = (state: GlobalState): string =>
|
||||||
return match ? match.params.safeAddress : null
|
return match ? match.params.safeAddress : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const safeIncomingTransactionsSelector: Selector<GlobalState, RouterProps, List<IncomingTransaction>> = createSelector(
|
type IncomingTxSelectorType = Selector<GlobalState, RouterProps, List<IncomingTransaction>>
|
||||||
|
|
||||||
|
export const safeIncomingTransactionsSelector: IncomingTxSelectorType = createSelector(
|
||||||
incomingTransactionsSelector,
|
incomingTransactionsSelector,
|
||||||
safeParamAddressSelector,
|
safeParamAddressSelector,
|
||||||
(incomingTransactions: IncomingTransactionsState, address: string): List<IncomingTransaction> => {
|
(incomingTransactions: IncomingTransactionsState, address: string): List<IncomingTransaction> => {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { createBrowserHistory } from 'history'
|
import { createBrowserHistory } from 'history'
|
||||||
import { connectRouter, routerMiddleware } from 'connected-react-router'
|
import { connectRouter, routerMiddleware } from 'connected-react-router'
|
||||||
import {
|
import {
|
||||||
combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store,
|
combineReducers, createStore, applyMiddleware, compose, type CombinedReducer, type Store,
|
||||||
} from 'redux'
|
} from 'redux'
|
||||||
import thunk from 'redux-thunk'
|
import thunk from 'redux-thunk'
|
||||||
import safe, { SAFE_REDUCER_ID, type SafeReducerState as SafeState } from '~/routes/safe/store/reducer/safe'
|
import safe, { SAFE_REDUCER_ID, type SafeReducerState as SafeState } from '~/routes/safe/store/reducer/safe'
|
||||||
|
@ -12,6 +12,10 @@ import transactions, {
|
||||||
type State as TransactionsState,
|
type State as TransactionsState,
|
||||||
TRANSACTIONS_REDUCER_ID,
|
TRANSACTIONS_REDUCER_ID,
|
||||||
} from '~/routes/safe/store/reducer/transactions'
|
} from '~/routes/safe/store/reducer/transactions'
|
||||||
|
import cancellationTransactions, {
|
||||||
|
type CancelState as CancelTransactionsState,
|
||||||
|
CANCELLATION_TRANSACTIONS_REDUCER_ID,
|
||||||
|
} from '~/routes/safe/store/reducer/cancellationTransactions'
|
||||||
import incomingTransactions, {
|
import incomingTransactions, {
|
||||||
type IncomingState as IncomingTransactionsState,
|
type IncomingState as IncomingTransactionsState,
|
||||||
INCOMING_TRANSACTIONS_REDUCER_ID,
|
INCOMING_TRANSACTIONS_REDUCER_ID,
|
||||||
|
@ -36,15 +40,21 @@ export const history = createBrowserHistory()
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
|
||||||
const finalCreateStore = composeEnhancers(
|
const finalCreateStore = composeEnhancers(applyMiddleware(
|
||||||
applyMiddleware(thunk, routerMiddleware(history), safeStorage, providerWatcher, notificationsMiddleware, addressBookMiddleware),
|
thunk,
|
||||||
)
|
routerMiddleware(history),
|
||||||
|
safeStorage,
|
||||||
|
providerWatcher,
|
||||||
|
notificationsMiddleware,
|
||||||
|
addressBookMiddleware,
|
||||||
|
))
|
||||||
|
|
||||||
export type GlobalState = {
|
export type GlobalState = {
|
||||||
providers: ProviderState,
|
providers: ProviderState,
|
||||||
safes: SafeState,
|
safes: SafeState,
|
||||||
tokens: TokensState,
|
tokens: TokensState,
|
||||||
transactions: TransactionsState,
|
transactions: TransactionsState,
|
||||||
|
cancellationTransactions: CancelTransactionsState,
|
||||||
incomingTransactions: IncomingTransactionsState,
|
incomingTransactions: IncomingTransactionsState,
|
||||||
notifications: NotificationsState,
|
notifications: NotificationsState,
|
||||||
currentSession: CurrentSessionState,
|
currentSession: CurrentSessionState,
|
||||||
|
@ -52,12 +62,13 @@ export type GlobalState = {
|
||||||
|
|
||||||
export type GetState = () => GlobalState
|
export type GetState = () => GlobalState
|
||||||
|
|
||||||
const reducers: Reducer<GlobalState> = combineReducers({
|
const reducers: CombinedReducer<GlobalState, *> = combineReducers({
|
||||||
router: connectRouter(history),
|
router: connectRouter(history),
|
||||||
[PROVIDER_REDUCER_ID]: provider,
|
[PROVIDER_REDUCER_ID]: provider,
|
||||||
[SAFE_REDUCER_ID]: safe,
|
[SAFE_REDUCER_ID]: safe,
|
||||||
[TOKEN_REDUCER_ID]: tokens,
|
[TOKEN_REDUCER_ID]: tokens,
|
||||||
[TRANSACTIONS_REDUCER_ID]: transactions,
|
[TRANSACTIONS_REDUCER_ID]: transactions,
|
||||||
|
[CANCELLATION_TRANSACTIONS_REDUCER_ID]: cancellationTransactions,
|
||||||
[INCOMING_TRANSACTIONS_REDUCER_ID]: incomingTransactions,
|
[INCOMING_TRANSACTIONS_REDUCER_ID]: incomingTransactions,
|
||||||
[NOTIFICATIONS_REDUCER_ID]: notifications,
|
[NOTIFICATIONS_REDUCER_ID]: notifications,
|
||||||
[CURRENCY_VALUES_KEY]: currencyValues,
|
[CURRENCY_VALUES_KEY]: currencyValues,
|
||||||
|
@ -66,7 +77,10 @@ const reducers: Reducer<GlobalState> = combineReducers({
|
||||||
[CURRENT_SESSION_REDUCER_ID]: currentSession,
|
[CURRENT_SESSION_REDUCER_ID]: currentSession,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const store: Store<GlobalState> = createStore(reducers, finalCreateStore)
|
export const store: Store<GlobalState, *> = createStore(reducers, finalCreateStore)
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
export const aNewStore = (localState?: Object): Store<GlobalState, *> => createStore(
|
||||||
export const aNewStore = (localState?: Object): Store<GlobalState> => createStore(reducers, localState, finalCreateStore)
|
reducers,
|
||||||
|
localState,
|
||||||
|
finalCreateStore,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue